-
Notifications
You must be signed in to change notification settings - Fork 6
/
discover.go
157 lines (142 loc) · 3.22 KB
/
discover.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package lifxlan
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"net"
)
// RawStateServicePayload defines the struct to be used for encoding and
// decoding.
//
// https://lan.developer.lifx.com/docs/information-messages#stateservice---packet-3
type RawStateServicePayload struct {
Service ServiceType
Port uint32
}
// Default broadcast host and port.
const (
DefaultBroadcastHost = "255.255.255.255"
DefaultBroadcastPort = "56700"
)
// Discover discovers lifx products in the lan.
//
// When broadcastHost is empty (""), DefaultBroadcastHost will be used instead.
// In most cases that should just work.
// But if your network has special settings, you can override it via the arg.
//
// The function will write discovered devices into devices channel.
// It's the caller's responsibility to read from channel timely to avoid
// blocking writing.
// The function is guaranteed to close the channel upon retuning,
// so the caller could just range over the channel for reading, e.g.
//
// devices := make(chan Device)
// go func() {
// if err := Discover(ctx, devices, ""); err != nil {
// if err != context.DeadlineExceeded {
// // handle error
// }
// }
// }()
// for device := range devices {
// // Do something with device
// }
//
// The function will only return upon error or when ctx is cancelled.
// It's the caller's responsibility to make sure that the context is cancelled
// (e.g. Use context.WithTimeout).
func Discover(
ctx context.Context,
devices chan Device,
broadcastHost string,
) error {
defer close(devices)
if ctx.Err() != nil {
return ctx.Err()
}
msg, err := GenerateMessage(
Tagged,
0, // source
AllDevices,
0, // flags
0, // sequence
GetService,
nil, // payload
)
if err != nil {
return err
}
conn, err := net.ListenPacket("udp", ":"+DefaultBroadcastPort)
if err != nil {
return err
}
defer conn.Close()
if broadcastHost == "" {
broadcastHost = DefaultBroadcastHost
}
broadcast, err := net.ResolveUDPAddr(
"udp",
net.JoinHostPort(broadcastHost, DefaultBroadcastPort),
)
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
n, err := conn.WriteTo(msg, broadcast)
if err != nil {
return err
}
if n < len(msg) {
return fmt.Errorf(
"lifxlan.Discover: only wrote %d out of %d bytes",
n,
len(msg),
)
}
buf := make([]byte, ResponseReadBufferSize)
for {
if ctx.Err() != nil {
return ctx.Err()
}
if err := conn.SetReadDeadline(GetReadDeadline()); err != nil {
return err
}
n, addr, err := conn.ReadFrom(buf)
if err != nil {
if CheckTimeoutError(err) {
continue
}
return err
}
host, _, err := net.SplitHostPort(addr.String())
if err != nil {
return err
}
resp, err := ParseResponse(buf[:n])
if err != nil {
return err
}
if resp.Message != StateService {
continue
}
var d RawStateServicePayload
r := bytes.NewReader(resp.Payload)
if err := binary.Read(r, binary.LittleEndian, &d); err != nil {
return err
}
switch d.Service {
default:
// Unknown service, ignore.
continue
case ServiceUDP:
devices <- NewDevice(
net.JoinHostPort(host, fmt.Sprintf("%d", d.Port)),
d.Service,
resp.Target,
)
}
}
}