-
Notifications
You must be signed in to change notification settings - Fork 6
/
device.go
218 lines (192 loc) · 6.76 KB
/
device.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package lifxlan
import (
"context"
"fmt"
"net"
"sync/atomic"
)
// ServiceType define the type of the service this device provides.
type ServiceType uint8
// Documented values for ServiceType.
const (
ServiceUDP ServiceType = 1
)
func (s ServiceType) String() string {
switch s {
default:
return fmt.Sprintf("<UNKNOWN> (%d)", uint8(s))
case ServiceUDP:
return "UDP"
}
}
// Device defines the common interface between lifxlan devices.
//
// For the Foo() and GetFoo() function pairs (e.g. Label() and GetLabel()),
// the Foo() one will return an pointer to the cached property,
// guaranteed to be non-nil but could be the zero value,
// while the GetFoo() one will use an API call to update the cached property.
//
// There will also be an EmptyFoo string constant defined,
// so that you can compare against Device.Foo().String() to determine if a
// GetFoo() call is needed.
// Here is an example code snippet to get a device's label:
//
// func GetLabel(ctx context.Context, d lifxlanDevice) (string, error) {
// if d.Label().String() != lifxlan.EmptyLabel {
// return d.Label().String(), nil
// }
// if err := d.GetLabel(ctx, nil); err = nil {
// return "", nil
// }
// return d.Label().String(), nil
// }
//
// If you are extending a device code and you got the property as part of
// another API's return payload,
// you can also use the Foo() function to update the cached value.
// Here is an example code snippet to update a device's cached label:
//
// func UpdateLabel(d lifxlanDevice, newLabel *lifxlan.RawLabel) {
// *d.Label() = *newLabel
// }
//
// The conn arg in GetFoo() functions can be nil.
// In such cases,
// a new connection will be made and guaranteed to be closed before returning.
// You should pre-dial and pass in the conn if you plan to call APIs on this
// device repeatedly.
//
// In case of network error (e.g. response packet loss),
// the GetFoo() functions might block until the context is cancelled,
// as a result, it's a good idea to set a timeout to the context.
type Device interface {
// Target returns the target of this device, usually it's the MAC address.
Target() Target
// Dial tries to establish a connection to this device.
Dial() (net.Conn, error)
// Source returns a consistent random source to be used with API calls.
// It's guaranteed to be non-zero.
Source() uint32
// NextSequence returns the next sequence value to be used with API calls.
NextSequence() uint8
// Send generates and sends a message to the device.
//
// conn must be pre-dialed or this function will fail.
//
// It calls the device's Target(), Source(), and NextSequence() functions to
// fill the appropriate headers.
//
// The sequence used in this message will be returned.
Send(ctx context.Context, conn net.Conn, flags AckResFlag, message MessageType, payload interface{}) (seq uint8, err error)
// SanitizeColor tries to sanitize (keep values inside appropriate boundaries)
// color based on the device's feature, if available.
//
// Both the device's hardware version and firmware version can affect the
// boundaries used for sanitization.
// If the device's hardware version was never fetched and cached,
// it uses default boundaries (see doc for Color.Sanitize).
// If the device's firmware version was never fetched and cached,
// it uses the hardware's default boundaries (without potential firmware
// upgrades).
SanitizeColor(color Color) Color
// Echo sends a message to the device and waits for a response to ensure that
// the device is online and responding. A payload can optionally be provided
// to define the data that is sent to the device and expected to be returned
// in the echo response.
//
// If conn is nil,
// a new connection will be made and guaranteed to be closed before returning.
// You should pre-dial and pass in the conn if you plan to call APIs on this
// device repeatedly.
Echo(ctx context.Context, conn net.Conn, payload []byte) error
// GetPower returns the current power level of the device.
//
// If conn is nil,
// a new connection will be made and guaranteed to be closed before returning.
// You should pre-dial and pass in the conn if you plan to call APIs on this
// device repeatedly.
GetPower(ctx context.Context, conn net.Conn) (Power, error)
// SetPower sets the power level of the device.
// (Turn it on or off.)
//
// If conn is nil,
// a new connection will be made and guaranteed to be closed before returning.
// You should pre-dial and pass in the conn if you plan to call APIs on this
// device repeatedly.
//
// If ack is false,
// this function returns nil error after the API is sent successfully.
// If ack is true,
// this function will only return nil error after it received ack from the
// device.
SetPower(ctx context.Context, conn net.Conn, power Power, ack bool) error
// The label of the device.
Label() *Label
GetLabel(ctx context.Context, conn net.Conn) error
// The hardware version info of the device.
HardwareVersion() *HardwareVersion
GetHardwareVersion(ctx context.Context, conn net.Conn) error
// The firmware version of the device.
Firmware() *FirmwareUpgrade
GetFirmware(ctx context.Context, conn net.Conn) error
}
var _ Device = (*device)(nil)
// device defines the base type of a lifxlan device.
type device struct {
// The network address, in "ip:port" format.
addr string
// The type of service this device provides.
service ServiceType
// The target of this device, usually it's the MAC address.
target Target
source uint32
sequence uint32
// Cached properties.
label Label
version HardwareVersion
firmware FirmwareUpgrade
}
// NewDevice creates a new Device.
//
// addr must be in "host:port" format and service must be a known service type,
// otherwise the later Dial funcion will fail.
func NewDevice(addr string, service ServiceType, target Target) Device {
return &device{
addr: addr,
service: service,
target: target,
source: RandomSource(),
}
}
func (d *device) String() string {
if label := d.Label().String(); label != EmptyLabel {
return fmt.Sprintf("%s(%v)", label, d.Target())
}
if parsed := d.HardwareVersion().Parse(); parsed != nil {
return fmt.Sprintf("%s(%v)", parsed.ProductName, d.Target())
}
return fmt.Sprintf("Device(%v)", d.Target())
}
func (d *device) Target() Target {
return d.target
}
func (d *device) Dial() (net.Conn, error) {
var network string
switch d.service {
default:
return nil, fmt.Errorf(
"lifxlan.Device.Dial: unknown device service type: %v",
d.service,
)
case ServiceUDP:
network = "udp"
}
return net.Dial(network, d.addr)
}
func (d *device) Source() uint32 {
return d.source
}
const uint8mask = uint32(0xff)
func (d *device) NextSequence() uint8 {
return uint8(atomic.AddUint32(&d.sequence, 1) & uint8mask)
}