Skip to content

Commit 1a04a6b

Browse files
committed
Add discovery package
1 parent c8315f5 commit 1a04a6b

File tree

4 files changed

+464
-0
lines changed

4 files changed

+464
-0
lines changed

discovery/discovery.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Package discovery keeps an updated list of the devices connected to the
2+
// computer, via serial ports or found in the network
3+
//
4+
// Usage:
5+
// monitor := discovery.New(time.Millisecond)
6+
// ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
7+
// monitor.Start(ctx)
8+
// time.Sleep(10 * time.Second)
9+
// fmt.Println(monitor.Serial())
10+
// fmt.Println(monitor.Network())
11+
//
12+
// Output:
13+
// map[/dev/ttyACM0:0x2341/0x8036]
14+
// map[192.168.1.107:YunShield]
15+
//
16+
// You may also decide to subscribe to the Events channel of the Monitor:
17+
//
18+
// monitor := discovery.New(time.Millisecond)
19+
// ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
20+
// monitor.Start(ctx)
21+
// for ev := range monitor.Events {
22+
// fmt.Println(ev)
23+
// }
24+
//
25+
// Output:
26+
// {add 0x2341/0x8036 <nil>}
27+
// {add <nil> YunShield}
28+
package discovery
29+
30+
import (
31+
"fmt"
32+
"time"
33+
34+
"golang.org/x/net/context"
35+
36+
serial "github.com/facchinm/go-serial-native"
37+
)
38+
39+
// SerialDevice is something connected to the Serial Ports
40+
type SerialDevice struct {
41+
Port string `json:"port"`
42+
SerialNumber string `json:"serial_number"`
43+
ProductID string `json:"pid"`
44+
VendorID string `json:"vid"`
45+
Serial *serial.Info `json:"-"`
46+
}
47+
48+
func (d SerialDevice) String() string {
49+
if d.SerialNumber != "" {
50+
return fmt.Sprintf(`%s/%s/%s`, d.VendorID, d.ProductID, d.SerialNumber)
51+
}
52+
return fmt.Sprintf(`%s/%s`, d.VendorID, d.ProductID)
53+
}
54+
55+
//SerialDevices is a list of currently connected devices to the computer
56+
type SerialDevices map[string]*SerialDevice
57+
58+
// NetworkDevice is something connected to the Network Ports
59+
type NetworkDevice struct {
60+
Address string `json:"address"`
61+
Info string `json:"info"`
62+
Name string `json:"name"`
63+
Port int `json:"port"`
64+
}
65+
66+
func (d NetworkDevice) String() string {
67+
return d.Name
68+
}
69+
70+
//NetworkDevices is a list of currently connected devices to the computer
71+
type NetworkDevices map[string]*NetworkDevice
72+
73+
// Event tells you that something has changed in the list of connected devices.
74+
// Name can be one of ["Add", "Change", "Remove"]
75+
// SerialDevice or NetworkDevice can be present and they refer to the device
76+
// that has been added, changed, or removed
77+
type Event struct {
78+
Name string `json:"name"`
79+
SerialDevice *SerialDevice `json:"serial_device,omitempty"`
80+
NetworkDevice *NetworkDevice `json:"network_device,omitempty"`
81+
}
82+
83+
// Monitor periodically checks the serial ports and the network in order to have
84+
// an updated list of Serial and Network ports.
85+
//
86+
// You can subscribe to the Events channel to get realtime notification of what's changed
87+
type Monitor struct {
88+
Interval time.Duration
89+
Events chan Event
90+
91+
serial SerialDevices
92+
network NetworkDevices
93+
}
94+
95+
// New Creates a new monitor that can start querying the serial ports and
96+
// the local network for devices
97+
func New(interval time.Duration) *Monitor {
98+
m := Monitor{
99+
serial: SerialDevices{},
100+
network: NetworkDevices{},
101+
Interval: interval,
102+
}
103+
return &m
104+
}
105+
106+
// Start begins the loop that queries the serial ports and the local network.
107+
// It accepts a cancelable context
108+
func (m *Monitor) Start(ctx context.Context) {
109+
m.Events = make(chan (Event))
110+
111+
var done chan bool
112+
var stop = false
113+
114+
go func() {
115+
<-ctx.Done()
116+
stop = true
117+
}()
118+
119+
go func() {
120+
for {
121+
if stop {
122+
break
123+
}
124+
m.serialDiscover()
125+
}
126+
done <- true
127+
}()
128+
go func() {
129+
for {
130+
if stop {
131+
break
132+
}
133+
m.networkDiscover()
134+
}
135+
done <- true
136+
}()
137+
138+
go func() {
139+
// We need to wait until both goroutines have finished
140+
<-done
141+
<-done
142+
close(m.Events)
143+
}()
144+
}
145+
146+
// Serial returns a cached list of devices connected to the serial ports
147+
func (m *Monitor) Serial() SerialDevices {
148+
return m.serial
149+
}
150+
151+
// Network returns a cached list of devices found on the local network
152+
func (m *Monitor) Network() NetworkDevices {
153+
return m.network
154+
}

discovery/discovery_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package discovery_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"golang.org/x/net/context"
9+
10+
"github.com/arduino/arduino-create-agent/discovery"
11+
)
12+
13+
// TestUsage doesn't really test anything, since we don't have (yet) a way to reproduce hardware. It's useful to test by hand though
14+
func TestUsage(t *testing.T) {
15+
monitor := discovery.New(time.Millisecond)
16+
17+
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
18+
monitor.Start(ctx)
19+
20+
time.Sleep(10 * time.Second)
21+
22+
fmt.Println(monitor.Serial())
23+
fmt.Println(monitor.Network())
24+
}
25+
26+
// TestEvent doesn't really test anything, since we don't have (yet) a way to reproduce hardware. It's useful to test by hand though
27+
func TestEvents(t *testing.T) {
28+
monitor := discovery.New(time.Millisecond)
29+
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
30+
monitor.Start(ctx)
31+
32+
for ev := range monitor.Events {
33+
fmt.Println(ev)
34+
}
35+
}

discovery/network.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package discovery
2+
3+
import (
4+
"net"
5+
"strings"
6+
"time"
7+
8+
"github.com/juju/errors"
9+
"github.com/oleksandr/bonjour"
10+
)
11+
12+
// Merge updates the device with the new one, returning false if the operation
13+
// didn't change anything
14+
func (d *NetworkDevice) merge(dev NetworkDevice) bool {
15+
changed := false
16+
if d.Port != dev.Port {
17+
changed = true
18+
d.Port = dev.Port
19+
}
20+
if d.Address != dev.Address {
21+
changed = true
22+
d.Address = dev.Address
23+
}
24+
if d.Info != dev.Info {
25+
changed = true
26+
d.Info = dev.Info
27+
}
28+
if d.Name != dev.Name {
29+
changed = true
30+
d.Name = dev.Name
31+
}
32+
33+
return changed
34+
}
35+
36+
// IsUp checks if the device is listening on the given port
37+
// the timeout is 1.5 seconds
38+
// It checks up to the number of times specified. If at least one of the attempt
39+
// is successful it returns true
40+
func (d *NetworkDevice) isUp(times int) bool {
41+
for i := 0; i < times; i++ {
42+
if d._isUp() {
43+
return true
44+
}
45+
}
46+
return false
47+
}
48+
49+
func (d *NetworkDevice) _isUp() bool {
50+
timeout := time.Duration(1500 * time.Millisecond)
51+
conn, err := net.DialTimeout("tcp", d.Address+":"+string(d.Port), timeout)
52+
if err != nil {
53+
// Check if the port 22 is open
54+
conn, err = net.DialTimeout("tcp", d.Address+":22", timeout)
55+
if err != nil {
56+
return false
57+
}
58+
conn.Close()
59+
return true
60+
}
61+
conn.Close()
62+
return true
63+
}
64+
65+
func (m *Monitor) networkDiscover() error {
66+
entries, err := listEntries()
67+
if err != nil {
68+
return errors.Annotatef(err, "while listing the network ports")
69+
}
70+
71+
for _, entry := range entries {
72+
m.addNetwork(&entry)
73+
74+
}
75+
m.pruneNetwork()
76+
return nil
77+
}
78+
79+
// listEntries returns a list of bonjour entries. It's convoluted because for
80+
// some reason they decided they wanted to make it asynchronous. Seems like writing
81+
// javascript, bleah.
82+
func listEntries() ([]bonjour.ServiceEntry, error) {
83+
// Define some helper channels that don't have to survive the function
84+
finished := make(chan bool) // allows us to communicate that the reading has been completed
85+
errs := make(chan error) // allows us to communicate that an error has occurred
86+
defer func() {
87+
close(finished)
88+
close(errs)
89+
}()
90+
91+
// Instantiate the bonjour controller
92+
resolver, err := bonjour.NewResolver(nil)
93+
if err != nil {
94+
return nil, errors.Annotatef(err, "When initializing the bonjour resolver")
95+
}
96+
97+
results := make(chan *bonjour.ServiceEntry)
98+
99+
// entries is the list of entries we have to return
100+
entries := []bonjour.ServiceEntry{}
101+
102+
// Exit if after two seconds there was no response
103+
go func(exitCh chan<- bool) {
104+
time.Sleep(4 * time.Second)
105+
exitCh <- true
106+
close(results)
107+
}(resolver.Exit)
108+
109+
// Loop through the results
110+
go func(results chan *bonjour.ServiceEntry, exit chan<- bool) {
111+
for e := range results {
112+
entries = append(entries, *e)
113+
}
114+
finished <- true
115+
}(results, resolver.Exit)
116+
117+
// Start the resolving
118+
err = resolver.Browse("_arduino._tcp", "", results)
119+
if err != nil {
120+
close(results)
121+
errs <- errors.Annotatef(err, "When browsing the services")
122+
}
123+
124+
select {
125+
case <-finished:
126+
return entries, nil
127+
case err := <-errs:
128+
return nil, err
129+
}
130+
}
131+
132+
func (m *Monitor) addNetwork(e *bonjour.ServiceEntry) {
133+
device := NetworkDevice{
134+
Name: e.Instance,
135+
Address: e.AddrIPv4.String(),
136+
Info: strings.Join(e.Text, " "),
137+
Port: e.Port,
138+
}
139+
for address, dev := range m.network {
140+
if address == device.Address {
141+
changed := dev.merge(device)
142+
if changed {
143+
m.Events <- Event{Name: "change", NetworkDevice: dev}
144+
}
145+
return
146+
}
147+
}
148+
149+
m.network[device.Address] = &device
150+
m.Events <- Event{Name: "add", NetworkDevice: &device}
151+
}
152+
153+
func (m *Monitor) pruneNetwork() {
154+
toPrune := []string{}
155+
for address, dev := range m.network {
156+
if !dev.isUp(2) {
157+
toPrune = append(toPrune, address)
158+
}
159+
}
160+
161+
for _, port := range toPrune {
162+
m.Events <- Event{Name: "remove", NetworkDevice: m.network[port]}
163+
delete(m.network, port)
164+
}
165+
}
166+
167+
// filter returns a new slice containing all NetworkDevice in the slice that satisfy the predicate f.
168+
func filter(vs []NetworkDevice, f func(NetworkDevice) bool) []NetworkDevice {
169+
var vsf []NetworkDevice
170+
for _, v := range vs {
171+
if f(v) {
172+
vsf = append(vsf, v)
173+
}
174+
}
175+
return vsf
176+
}

0 commit comments

Comments
 (0)