lifx-lan 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +8 -0
  4. data/.yardopts +3 -0
  5. data/CHANGES.md +45 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +23 -0
  8. data/README.md +15 -0
  9. data/Rakefile +20 -0
  10. data/bin/lifx-snoop +50 -0
  11. data/examples/auto-off/auto-off.rb +34 -0
  12. data/examples/blink/blink.rb +19 -0
  13. data/examples/identify/identify.rb +69 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +30 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx-lan.rb +27 -0
  18. data/lib/lifx/lan/client.rb +149 -0
  19. data/lib/lifx/lan/color.rb +199 -0
  20. data/lib/lifx/lan/config.rb +17 -0
  21. data/lib/lifx/lan/firmware.rb +60 -0
  22. data/lib/lifx/lan/gateway_connection.rb +185 -0
  23. data/lib/lifx/lan/light.rb +440 -0
  24. data/lib/lifx/lan/light_collection.rb +111 -0
  25. data/lib/lifx/lan/light_target.rb +185 -0
  26. data/lib/lifx/lan/logging.rb +14 -0
  27. data/lib/lifx/lan/message.rb +168 -0
  28. data/lib/lifx/lan/network_context.rb +188 -0
  29. data/lib/lifx/lan/observable.rb +66 -0
  30. data/lib/lifx/lan/protocol/address.rb +25 -0
  31. data/lib/lifx/lan/protocol/device.rb +387 -0
  32. data/lib/lifx/lan/protocol/header.rb +24 -0
  33. data/lib/lifx/lan/protocol/light.rb +142 -0
  34. data/lib/lifx/lan/protocol/message.rb +19 -0
  35. data/lib/lifx/lan/protocol/metadata.rb +23 -0
  36. data/lib/lifx/lan/protocol/payload.rb +12 -0
  37. data/lib/lifx/lan/protocol/sensor.rb +31 -0
  38. data/lib/lifx/lan/protocol/type.rb +204 -0
  39. data/lib/lifx/lan/protocol/wan.rb +51 -0
  40. data/lib/lifx/lan/protocol/wifi.rb +102 -0
  41. data/lib/lifx/lan/protocol_path.rb +85 -0
  42. data/lib/lifx/lan/required_keyword_arguments.rb +12 -0
  43. data/lib/lifx/lan/routing_manager.rb +114 -0
  44. data/lib/lifx/lan/routing_table.rb +48 -0
  45. data/lib/lifx/lan/seen.rb +25 -0
  46. data/lib/lifx/lan/site.rb +97 -0
  47. data/lib/lifx/lan/tag_manager.rb +111 -0
  48. data/lib/lifx/lan/tag_table.rb +49 -0
  49. data/lib/lifx/lan/target.rb +24 -0
  50. data/lib/lifx/lan/thread.rb +13 -0
  51. data/lib/lifx/lan/timers.rb +29 -0
  52. data/lib/lifx/lan/transport.rb +46 -0
  53. data/lib/lifx/lan/transport/tcp.rb +91 -0
  54. data/lib/lifx/lan/transport/udp.rb +87 -0
  55. data/lib/lifx/lan/transport_manager.rb +43 -0
  56. data/lib/lifx/lan/transport_manager/lan.rb +169 -0
  57. data/lib/lifx/lan/utilities.rb +36 -0
  58. data/lib/lifx/lan/version.rb +5 -0
  59. data/lifx-lan.gemspec +26 -0
  60. data/spec/color_spec.rb +43 -0
  61. data/spec/gateway_connection_spec.rb +30 -0
  62. data/spec/integration/client_spec.rb +42 -0
  63. data/spec/integration/light_spec.rb +56 -0
  64. data/spec/integration/tags_spec.rb +42 -0
  65. data/spec/light_collection_spec.rb +37 -0
  66. data/spec/message_spec.rb +183 -0
  67. data/spec/protocol_path_spec.rb +109 -0
  68. data/spec/routing_manager_spec.rb +25 -0
  69. data/spec/routing_table_spec.rb +23 -0
  70. data/spec/spec_helper.rb +56 -0
  71. data/spec/transport/udp_spec.rb +44 -0
  72. data/spec/transport_spec.rb +14 -0
  73. metadata +187 -0
@@ -0,0 +1,188 @@
1
+ require 'lifx/lan/timers'
2
+ require 'lifx/lan/transport_manager'
3
+ require 'lifx/lan/routing_manager'
4
+ require 'lifx/lan/tag_manager'
5
+ require 'lifx/lan/light'
6
+ require 'lifx/lan/protocol_path'
7
+ require 'lifx/lan/timers'
8
+
9
+ require 'weakref'
10
+
11
+ module LIFX
12
+ module LAN
13
+ class NetworkContext
14
+ include Logging
15
+ include Utilities
16
+ include RequiredKeywordArguments
17
+ include Timers
18
+ extend Forwardable
19
+
20
+ # NetworkContext stores lights and ties together TransportManager, TagManager and RoutingManager
21
+ attr_reader :transport_manager, :tag_manager, :routing_manager
22
+
23
+ def initialize(transport_manager: required!('transport_manager'))
24
+ @devices = {}
25
+
26
+ @transport_manager = transport_manager
27
+ @transport_manager.context = WeakRef.new(self)
28
+ @transport_manager.add_observer(self, :message_received) do |message: nil, ip: nil, transport: nil|
29
+ handle_message(message, ip, transport)
30
+ end
31
+
32
+ reset!
33
+
34
+ @threads = []
35
+ @threads << initialize_timer_thread
36
+ end
37
+
38
+ def discover
39
+ @transport_manager.discover
40
+ end
41
+
42
+ def refresh(force: true)
43
+ @routing_manager.refresh(force: force)
44
+ end
45
+
46
+ def reset!
47
+ @routing_manager = RoutingManager.new(context: self)
48
+ @tag_manager = TagManager.new(context: self, tag_table: @routing_manager.tag_table)
49
+ end
50
+
51
+ def stop_discovery
52
+ @transport_manager.stop_discovery
53
+ end
54
+
55
+ def stop
56
+ @transport_manager.stop
57
+ stop_timers
58
+ @threads.each do |thread|
59
+ thread.abort
60
+ thread.join
61
+ end
62
+ @threads = nil
63
+ end
64
+
65
+ # Sends a message to their destination(s)
66
+ # @param target: [Target] Target of the message
67
+ # @param payload: [Protocol::Payload] Message payload
68
+ # @param acknowledge: [Boolean] If recipients must acknowledge with a response
69
+ # @param at_time: [Integer] Unix epoch in milliseconds to run the payload. Only applicable to certain payload types.
70
+ def send_message(target: required!(:target), payload: required!(:payload), acknowledge: false, at_time: nil)
71
+ paths = @routing_manager.resolve_target(target)
72
+
73
+ messages = paths.map do |path|
74
+ Message.new(path: path, payload: payload, acknowledge: acknowledge, at_time: at_time)
75
+ end
76
+
77
+ if within_sync?
78
+ Thread.current[:sync_messages].push(*messages)
79
+ return
80
+ end
81
+
82
+ messages.each do |message|
83
+ @transport_manager.write(message)
84
+ end
85
+ end
86
+
87
+ def within_sync?
88
+ !!Thread.current[:sync_enabled]
89
+ end
90
+ protected :within_sync?
91
+
92
+ # Synchronize asynchronous set_color, set_waveform and set_power messages to multiple devices.
93
+ # You cannot use synchronous methods in the block
94
+ # @note This is alpha
95
+ # @param delay: [Float] The delay to add to sync commands when dealing with latency.
96
+ # @yield Block to synchronize commands in
97
+ # @return [Float] Delay before messages are executed
98
+ NSEC_PER_SEC = 1_000_000_000
99
+ AT_TIME_DELTA = 0.002
100
+ def sync(delay: 0, &block)
101
+ if within_sync?
102
+ raise "You cannot nest sync"
103
+ end
104
+ messages = Thread.start do
105
+ Thread.current[:sync_enabled] = true
106
+ Thread.current[:sync_messages] = messages = []
107
+ block.call
108
+ Thread.current[:sync_enabled] = false
109
+ messages
110
+ end.join.value
111
+
112
+ time = nil
113
+ failed_lights = []
114
+ try_until -> { time }, timeout: 5, action_interval: 1 do
115
+ light = (lights.alive - failed_lights).sample
116
+ begin
117
+ time = light && light.send_message!(Protocol::Device::GetTime.new, wait_for: Protocol::Device::StateTime, wait_timeout: 1, retry_interval: 0.5) do |payload|
118
+ Time.at(payload.time.to_f / NSEC_PER_SEC)
119
+ end
120
+ rescue => ex
121
+ logger.debug("!!! Add light failed")
122
+ failed_lights << light
123
+ end
124
+ end
125
+
126
+ delay += (messages.count + 1) * (1.0 / @transport_manager.message_rate)
127
+ at_time = ((time.to_f + delay) * NSEC_PER_SEC).to_i
128
+ messages.each_with_index do |m, i|
129
+ m.at_time = at_time + (i * AT_TIME_DELTA * NSEC_PER_SEC).to_i
130
+ @transport_manager.write(m)
131
+ end
132
+ flush
133
+ delay
134
+ end
135
+
136
+ def flush(**options)
137
+ @transport_manager.flush(**options)
138
+ end
139
+
140
+ def register_device(device)
141
+ return if device.site_id == NULL_SITE_ID
142
+ device_id = device.id
143
+ @devices[device_id] = device # What happens when there's already one registered?
144
+ end
145
+
146
+ def lights
147
+ LightCollection.new(context: self)
148
+ end
149
+
150
+ def all_lights
151
+ @devices.values
152
+ end
153
+
154
+ # Tags
155
+
156
+ def_delegators :@tag_manager, :tags,
157
+ :unused_tags,
158
+ :purge_unused_tags!,
159
+ :add_tag_to_device,
160
+ :remove_tag_from_device
161
+
162
+ def tags_for_device(device)
163
+ @routing_manager.tags_for_device_id(device.id)
164
+ end
165
+
166
+ def to_s
167
+ %Q{#<LIFX::LAN::NetworkContext transport_manager=#{transport_manager}>}
168
+ end
169
+ alias_method :inspect, :to_s
170
+
171
+ protected
172
+
173
+ def handle_message(message, ip, transport)
174
+ logger.debug("<- #{self} #{transport}: #{message}")
175
+
176
+ @routing_manager.update_from_message(message)
177
+ if !message.tagged?
178
+ if @devices[message.device_id].nil? && message.payload.is_a?(Protocol::Light::State)
179
+ device = Light.new(context: self, id: message.device_id, site_id: message.site_id)
180
+ end
181
+ device = @devices[message.device_id]
182
+ return if !device # Virgin bulb
183
+ device.handle_message(message, ip, transport)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,66 @@
1
+ module LIFX
2
+ module LAN
3
+ # @private
4
+ module Observable
5
+ class ObserverCallbackMismatch < ArgumentError; end
6
+ class ObserverCallbackNotFound < ArgumentError; end
7
+
8
+ def add_observer(obj, type, &callback)
9
+ if !callback_type_exists?(type)
10
+ raise ObserverCallbackNotFound.new("Callback #{type} not found in #{observer_callback_definition.keys}")
11
+ end
12
+ if !callback_has_required_keys?(type, callback)
13
+ raise ObserverCallbackMismatch.new
14
+ end
15
+ observers[type][obj] = callback
16
+ end
17
+
18
+ def remove_observer(obj, type)
19
+ observers[type].delete(obj)
20
+ end
21
+
22
+ def remove_observers
23
+ observers.clear
24
+ end
25
+
26
+ def notify_observers(type, **args)
27
+ if !callback_type_exists?(type)
28
+ raise ObserverCallbackNotFound.new("Callback #{type} not found in #{observer_callback_definition.keys}")
29
+ end
30
+ observers[type].each do |_, callback|
31
+ callback.call(**args)
32
+ end
33
+ end
34
+
35
+ def callback_type_exists?(type)
36
+ !!observer_callback_definition[type]
37
+ end
38
+
39
+ def callback_has_required_keys?(type, callback)
40
+ (required_keys_for_callback(type) - required_keys_in_proc(callback)).empty?
41
+ end
42
+
43
+ def observer_callback_definition
44
+ {}
45
+ end
46
+
47
+ def required_keys_for_callback(type)
48
+ @_required_keys_for_callback ||= {}
49
+ @_required_keys_for_callback[type] ||= begin
50
+ return [] if !observer_callback_definition[type]
51
+ required_keys_in_proc(observer_callback_definition[type])
52
+ end
53
+ end
54
+
55
+ def required_keys_in_proc(proc)
56
+ proc.parameters.select do |type, _|
57
+ type == :keyreq
58
+ end.map(&:last)
59
+ end
60
+
61
+ def observers
62
+ @_observers ||= Hash.new { |h, k| h[k] = {} }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ module LIFX
2
+ module LAN
3
+ module Protocol
4
+ module AddressFields
5
+ def AddressFields.included(mod)
6
+ mod.instance_eval do
7
+ hide :_reserved2
8
+ string :raw_target, length: 8
9
+ string :raw_site, length: 6 # Deprecated, should be zeros or "LIFXV2"
10
+ bit6le :_reserved2
11
+ bool_bit1 :acknowledge # Acknowledgement required
12
+ bool_bit1 :res_required # Response required
13
+ uint8 :sequence # Wrap around message sequence number
14
+ end
15
+ end
16
+ end
17
+
18
+ class Address < BinData::Record
19
+ endian :little
20
+
21
+ include AddressFields
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,387 @@
1
+ module LIFX
2
+ module LAN
3
+ module Protocol
4
+ # @api private
5
+ module Device
6
+
7
+ # Describes the services exposed by the device
8
+ module Service
9
+ UDP = 1 # Datagram transport on specified port
10
+ TCP = 2 # Reliable transport on specified port
11
+ ONBOARDING = 3 # Onboarding service on specified port
12
+ OTA = 4 # Active over-the-air update service on specified port. This is only exposed when the device is in active OTA mode with degraded service
13
+ end
14
+
15
+ # class SetSite < Payload
16
+ # endian :little
17
+
18
+ # string :site, length: 6
19
+ # end
20
+
21
+
22
+ # class GetPanGateway < Payload
23
+ # endian :little
24
+
25
+ # end
26
+
27
+ # class StatePanGateway < Payload
28
+ # endian :little
29
+
30
+ # uint8 :service
31
+ # uint32 :port
32
+ # end
33
+
34
+ # Discover devices and services exposed
35
+ class GetService < Payload
36
+ endian :little
37
+ end
38
+
39
+ # Response to GetService. Specifies service and port. Service temporarily unavailable if port is 0
40
+ class StateService < Payload
41
+ endian :little
42
+
43
+ uint8 :service # maps to Service enumerated type above
44
+ uint32 :port
45
+ end
46
+
47
+ # Gets device current time
48
+ class GetTime < Payload
49
+ endian :little
50
+ end
51
+
52
+ # Set the device time
53
+ class SetTime < Payload
54
+ endian :little
55
+
56
+ uint64 :time # Nanoseconds since epoch.
57
+ end
58
+
59
+ # Returns the time at device when message was sent
60
+ class StateTime < Payload
61
+ endian :little
62
+
63
+ uint64 :time # Nanoseconds since epoch.
64
+ end
65
+
66
+ # class GetResetSwitch < Payload
67
+ # endian :little
68
+
69
+ # end
70
+
71
+ # class StateResetSwitch < Payload
72
+ # endian :little
73
+
74
+ # uint8 :position
75
+ # end
76
+
77
+ # class GetMeshInfo < Payload
78
+ # endian :little
79
+
80
+ # end
81
+
82
+ # class StateMeshInfo < Payload
83
+ # endian :little
84
+
85
+ # float :signal # Milliwatts.
86
+ # uint32 :tx # Bytes.
87
+ # uint32 :rx # Bytes.
88
+ # int16 :mcu_temperature # Deci-celsius. 25.45 celsius is 2545
89
+ # end
90
+
91
+ # class GetMeshFirmware < Payload
92
+ # endian :little
93
+
94
+ # end
95
+
96
+ # class StateMeshFirmware < Payload
97
+ # endian :little
98
+
99
+ # uint64 :build # Firmware build nanoseconds since epoch.
100
+ # uint64 :install # Firmware install nanoseconds since epoch.
101
+ # uint32 :version # Firmware human readable version.
102
+ # end
103
+
104
+ # Get Host MCU information
105
+ class GetHostInfo < Payload
106
+ endian :little
107
+ end
108
+
109
+ # Host MCU information
110
+ class StateHostInfo < Payload
111
+ endian :little
112
+
113
+ float :signal # radio receive signal strength in Milliwatts (mw)
114
+ uint32 :tx # bytes transmitted since power on
115
+ uint32 :rx # bytes received since power on
116
+ int16 :mcu_temperature # internal temperature (deci-celsius). 25.45 celsius is 2545
117
+ end
118
+
119
+ # Gets Host MCU firmware
120
+ class GetHostFirmware < Payload
121
+ endian :little
122
+ end
123
+
124
+ class StateHostFirmware < Payload
125
+ endian :little
126
+
127
+ uint64 :build # build time in nanonseconds since epoch
128
+ uint64 :install # install time, depricated
129
+ uint32 :version # firmware version
130
+ end
131
+
132
+ # Get Wifi subsystem information
133
+ class GetWifiInfo < Payload
134
+ endian :little
135
+ end
136
+
137
+ # Wifi subsystem information
138
+ class StateWifiInfo < Payload
139
+ endian :little
140
+
141
+ float :signal # radio receive signal strength in Milliwatts (mw)
142
+ uint32 :tx # bytes transmitted since power on
143
+ uint32 :rx # bytes received since power on
144
+ int16 :mcu_temperature # internal temperature (deci-celsius). 25.45 celsius is 2545
145
+ end
146
+
147
+ # Get Wifi subsystem firmware
148
+ class GetWifiFirmware < Payload
149
+ endian :little
150
+ end
151
+
152
+ class StateWifiFirmware < Payload
153
+ endian :little
154
+
155
+ uint64 :build # build time in nanonseconds since epoch
156
+ uint64 :install # install time, depricated
157
+ uint32 :version # firmware version
158
+ end
159
+
160
+ # Get device power level
161
+ class GetPower < Payload
162
+ endian :little
163
+ end
164
+
165
+ # Set device power level
166
+ class SetPower < Payload
167
+ endian :little
168
+
169
+ uint16 :level # Zero implies standby and non-zero sets a corresponding power draw level on device. Currently only 0 and 65535 are supported
170
+ end
171
+
172
+ # Device power level
173
+ class StatePower < Payload
174
+ endian :little
175
+
176
+ uint16 :level # 0 Standby. > 0 On.
177
+ end
178
+
179
+ # Introspect the device label. Returns StateLabel
180
+ class GetLabel < Payload
181
+ endian :little
182
+ end
183
+
184
+ # Set the device label text
185
+ class SetLabel < Payload
186
+ endian :little
187
+
188
+ string :label, length: 32, trim_padding: true
189
+ end
190
+
191
+ # Device label
192
+ class StateLabel < Payload
193
+ endian :little
194
+
195
+ string :label, length: 32, trim_padding: true
196
+ end
197
+
198
+ # Get the device tags. Returns StateTags
199
+ class GetTags < Payload
200
+ endian :little
201
+ end
202
+
203
+ # Set the device tags
204
+ class SetTags < Payload
205
+ endian :little
206
+
207
+ uint64 :tags # Bitfield, allows 64 tags. A device can be tagged with one or more of them
208
+ end
209
+
210
+ # Device tags
211
+ class StateTags < Payload
212
+ endian :little
213
+
214
+ uint64 :tags
215
+ end
216
+
217
+ # Get the text labels describing the tags. Returns StateTagLabels
218
+ class GetTagLabels < Payload
219
+ endian :little
220
+
221
+ uint64 :tags # Retrieve the labels for tags selected in the bitfield
222
+ end
223
+
224
+ # Sets the text label for the corresponding tags. Note you can label or reset multiple labels at the same time
225
+ class SetTagLabels < Payload
226
+ endian :little
227
+
228
+ uint64 :tags # Set labels for tags selected in the bitfield
229
+ string :label, length: 32, trim_padding: true
230
+ end
231
+
232
+ # The tag label for tags in the bitfield
233
+ class StateTagLabels < Payload
234
+ endian :little
235
+
236
+ uint64 :tags
237
+ string :label, length: 32, trim_padding: true
238
+ end
239
+
240
+ # Get the hardware version. Returns StateVersion
241
+ class GetVersion < Payload
242
+ endian :little
243
+ end
244
+
245
+ # The hardware version of the device
246
+ class StateVersion < Payload
247
+ endian :little
248
+
249
+ uint32 :vendor # Vendor ID
250
+ uint32 :product # Product ID
251
+ uint32 :version # Hardware version
252
+ end
253
+
254
+ # Get runtime information. Returns StateInfo
255
+ class GetInfo < Payload
256
+ endian :little
257
+ end
258
+
259
+ # Runtime information of device
260
+ class StateInfo < Payload
261
+ endian :little
262
+
263
+ uint64 :time # wallclock time on device, nanoseconds since epoch
264
+ uint64 :uptime # device uptime, nanoseconds since boot
265
+ uint64 :downtime # last measured downtime, accurate to 5s, nanoseconds off last power cycle
266
+ end
267
+
268
+ # class GetMcuRailVoltage < Payload
269
+ # endian :little
270
+ # end
271
+
272
+ # class StateMcuRailVoltage < Payload
273
+ # endian :little
274
+
275
+ # uint32 :voltage
276
+ # end
277
+
278
+ # class Reboot < Payload
279
+ # endian :little
280
+ # end
281
+
282
+ # Soft reboot the device in 1000ms from time received
283
+ class SetReboot < Payload
284
+ endian :little
285
+ end
286
+
287
+ # Sent back if reboot is pending on SetReboot
288
+ class StateReboot < Payload
289
+ endian :little
290
+ end
291
+
292
+ # Sent back from device on receipt of a message with ack_required set to 1
293
+ class Acknowledgement < Payload
294
+ endian :little
295
+ end
296
+
297
+ # Clear all settings from the device
298
+ class SetFactoryReset < Payload
299
+ endian :little
300
+ end
301
+
302
+ # Signifies pending reset
303
+ class StateFactoryReset < Payload
304
+ endian :little
305
+ end
306
+
307
+ # Get the device location
308
+ class GetLocation < Payload
309
+ endian :little
310
+ end
311
+
312
+ # Set the device location
313
+ class SetLocation < Payload
314
+ endian :little
315
+
316
+ string :location, length: 16 # guid byte array
317
+ string :label, length: 32, trim_padding: true # text label for location
318
+ end
319
+
320
+ # Device location
321
+ class StateLocation < Payload
322
+ endian :little
323
+
324
+ string :location, length: 16 # guid byte array
325
+ string :label, length: 32, trim_padding: true # text label for location
326
+ end
327
+
328
+ # Get the device group. A device can belong to a single logical group
329
+ class GetGroup < Payload
330
+ endian :little
331
+ end
332
+
333
+ # Set the device group
334
+ class SetGroup < Payload
335
+ endian :little
336
+
337
+ string :group, length: 16 # guid byte array
338
+ string :label, length: 32, trim_padding: true # text label for group
339
+ end
340
+
341
+ # Set the device group
342
+ class StateGroup < Payload
343
+ endian :little
344
+
345
+ string :group, length: 16 # guid byte array
346
+ string :label, length: 32, trim_padding: true # text label for group
347
+ end
348
+
349
+ # Introspect the device owner. Returns StateOwner
350
+ class GetOwner < Payload
351
+ endian :little
352
+ end
353
+
354
+ # Set the device owner
355
+ class SetOwner < Payload
356
+ endian :little
357
+
358
+ string :owner, length: 16 # guid byte array
359
+ string :label, length: 32, trim_padding: true # text label of owner details
360
+ end
361
+
362
+ # Return the device owner information
363
+ class StateOwner < Payload
364
+ endian :little
365
+
366
+ string :owner, length: 16 # guid byte array
367
+ string :label, length: 32, trim_padding: true # text label of owner details
368
+ end
369
+
370
+ # Request an arbitrary payload be echoed back
371
+ class EchoRequest < Payload
372
+ endian :little
373
+
374
+ string :payload, length: 64 # byte array
375
+ end
376
+
377
+ # Echo response with payload sent in request
378
+ class EchoResponse < Payload
379
+ endian :little
380
+
381
+ string :payload, length: 64 # byte array
382
+ end
383
+
384
+ end
385
+ end
386
+ end
387
+ end