lifx 0.0.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile +10 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +71 -13
  6. data/Rakefile +12 -0
  7. data/bin/lifx-console +15 -0
  8. data/bin/lifx-snoop +50 -0
  9. data/examples/auto-off/Gemfile +3 -0
  10. data/examples/auto-off/auto-off.rb +35 -0
  11. data/examples/identify/Gemfile +3 -0
  12. data/examples/identify/identify.rb +70 -0
  13. data/examples/travis-build-light/Gemfile +4 -0
  14. data/examples/travis-build-light/build-light.rb +57 -0
  15. data/lib/bindata_ext/bool.rb +29 -0
  16. data/lib/bindata_ext/record.rb +11 -0
  17. data/lib/lifx/client.rb +136 -0
  18. data/lib/lifx/color.rb +190 -0
  19. data/lib/lifx/config.rb +12 -0
  20. data/lib/lifx/firmware.rb +55 -0
  21. data/lib/lifx/gateway_connection.rb +177 -0
  22. data/lib/lifx/light.rb +406 -0
  23. data/lib/lifx/light_collection.rb +105 -0
  24. data/lib/lifx/light_target.rb +189 -0
  25. data/lib/lifx/logging.rb +11 -0
  26. data/lib/lifx/message.rb +166 -0
  27. data/lib/lifx/network_context.rb +200 -0
  28. data/lib/lifx/observable.rb +46 -0
  29. data/lib/lifx/protocol/address.rb +21 -0
  30. data/lib/lifx/protocol/device.rb +225 -0
  31. data/lib/lifx/protocol/header.rb +24 -0
  32. data/lib/lifx/protocol/light.rb +110 -0
  33. data/lib/lifx/protocol/message.rb +17 -0
  34. data/lib/lifx/protocol/metadata.rb +21 -0
  35. data/lib/lifx/protocol/payload.rb +7 -0
  36. data/lib/lifx/protocol/sensor.rb +29 -0
  37. data/lib/lifx/protocol/type.rb +134 -0
  38. data/lib/lifx/protocol/wan.rb +50 -0
  39. data/lib/lifx/protocol/wifi.rb +76 -0
  40. data/lib/lifx/protocol_path.rb +84 -0
  41. data/lib/lifx/routing_manager.rb +110 -0
  42. data/lib/lifx/routing_table.rb +33 -0
  43. data/lib/lifx/seen.rb +15 -0
  44. data/lib/lifx/site.rb +89 -0
  45. data/lib/lifx/tag_manager.rb +105 -0
  46. data/lib/lifx/tag_table.rb +47 -0
  47. data/lib/lifx/target.rb +23 -0
  48. data/lib/lifx/timers.rb +18 -0
  49. data/lib/lifx/transport/tcp.rb +81 -0
  50. data/lib/lifx/transport/udp.rb +67 -0
  51. data/lib/lifx/transport.rb +41 -0
  52. data/lib/lifx/transport_manager/lan.rb +140 -0
  53. data/lib/lifx/transport_manager.rb +34 -0
  54. data/lib/lifx/utilities.rb +33 -0
  55. data/lib/lifx/version.rb +1 -1
  56. data/lib/lifx.rb +15 -1
  57. data/lifx.gemspec +11 -7
  58. data/spec/color_spec.rb +45 -0
  59. data/spec/gateway_connection_spec.rb +32 -0
  60. data/spec/integration/client_spec.rb +40 -0
  61. data/spec/integration/light_spec.rb +43 -0
  62. data/spec/integration/tags_spec.rb +31 -0
  63. data/spec/message_spec.rb +163 -0
  64. data/spec/protocol_path_spec.rb +109 -0
  65. data/spec/routing_manager_spec.rb +22 -0
  66. data/spec/spec_helper.rb +52 -0
  67. data/spec/transport/udp_spec.rb +38 -0
  68. data/spec/transport_spec.rb +14 -0
  69. metadata +143 -26
@@ -0,0 +1,166 @@
1
+ require 'forwardable'
2
+ require 'lifx/protocol_path'
3
+
4
+ module LIFX
5
+ # @api private
6
+
7
+ class Message
8
+ include Logging
9
+ extend Forwardable
10
+
11
+ class MessageError < StandardError; end
12
+ class UnpackError < MessageError; end
13
+ class PackError < MessageError; end
14
+ class NoPath < MessageError; end
15
+
16
+ class InvalidFrame < UnpackError; end
17
+ class UnsupportedProtocolVersion < UnpackError; end
18
+ class NotAddressableFrame < UnpackError; end
19
+ class NoPayload < PackError; end
20
+ class UnmappedPayload < MessageError; end
21
+ class InvalidFields < PackError; end
22
+
23
+ PROTOCOL_VERSION = 1024
24
+
25
+ class << self
26
+ attr_accessor :log_invalid_messages
27
+
28
+ def unpack(data)
29
+ raise InvalidFrame if data.length < 2
30
+
31
+ header = Protocol::Header.read(data)
32
+ raise UnsupportedProtocolVersion.new("Expected #{PROTOCOL_VERSION} but got #{header.protocol} instead") if header.protocol != PROTOCOL_VERSION
33
+ raise NotAddressableFrame if header.addressable == 0
34
+
35
+ message = Protocol::Message.read(data)
36
+ path = ProtocolPath.new(raw_site: message.raw_site, raw_target: message.raw_target, tagged: message.tagged)
37
+ payload_class = message_type_for_id(message.type.snapshot)
38
+ if payload_class.nil?
39
+ if self.log_invalid_messages
40
+ logger.error("Message.unpack: Unrecognised payload ID: #{message.type}")
41
+ logger.error("Message.unpack: Message: #{message}")
42
+ end
43
+ return nil # FIXME
44
+ raise UnmappedPayload.new("Unrecognised payload ID: #{message.type}")
45
+ end
46
+ begin
47
+ payload = payload_class.read(message.payload)
48
+ rescue => ex
49
+ if message.raw_site == "\x00" * 6
50
+ logger.info("Message.unpack: Ignoring malformed message from virgin bulb")
51
+ else
52
+ if self.log_invalid_messages
53
+ logger.error("Message.unpack: Exception while unpacking payload of type #{payload_class}: #{ex}")
54
+ logger.error("Message.unpack: Data: #{data.inspect}")
55
+ end
56
+ end
57
+ end
58
+ new(path, message, payload)
59
+ rescue => ex
60
+ if self.log_invalid_messages
61
+ logger.debug("Message.unpack: Exception while unpacking #{data.inspect}")
62
+ logger.debug("Message.unpack: #{ex} - #{ex.backtrace.join("\n")}")
63
+ end
64
+ raise ex
65
+ end
66
+
67
+ def message_type_for_id(type_id)
68
+ Protocol::TYPE_ID_TO_CLASS[type_id]
69
+ end
70
+
71
+ def type_id_for_message_class(klass)
72
+ Protocol::CLASS_TO_TYPE_ID[klass]
73
+ end
74
+
75
+ def valid_fields
76
+ @valid_fields ||= Protocol::Message.new.field_names.map(&:to_sym)
77
+ end
78
+ end
79
+
80
+ LIFX::Protocol::Message.fields.each do |field|
81
+ define_method(field.name) do
82
+ @message.send(field.name).snapshot
83
+ end
84
+
85
+ define_method("#{field.name}=") do |value|
86
+ @message.send("#{field.name}=", value)
87
+ end
88
+ end
89
+
90
+ alias_method :tagged?, :tagged
91
+ alias_method :addressable?, :addressable
92
+
93
+ def_delegators :path, :device_id, :site_id, :tagged
94
+
95
+ attr_accessor :path, :payload
96
+ def initialize(*args)
97
+ if args.count == 3
98
+ @path, @message, @payload = args
99
+ elsif (hash = args.first).is_a?(Hash)
100
+ path = hash.delete(:path)
101
+ payload = hash.delete(:payload)
102
+
103
+ check_valid_fields!(hash)
104
+
105
+ @message = Protocol::Message.new(hash)
106
+ self.payload = payload
107
+ self.path = path
108
+ @message.tagged = path.tagged? if path
109
+ else
110
+ @message = Protocol::Message.new
111
+ end
112
+ @message.msg_size = @message.num_bytes
113
+ @message.protocol = PROTOCOL_VERSION
114
+ rescue => ex
115
+ raise MessageError.new("Unable to initialize message with args: #{args.inspect} - #{ex}")
116
+ end
117
+
118
+ def payload=(payload)
119
+ @payload = payload
120
+ type_id = self.class.type_id_for_message_class(payload.class)
121
+ if type_id.nil?
122
+ raise UnmappedPayload.new("Unmapped payload class #{payload.class}")
123
+ end
124
+ @message.type = type_id
125
+ @message.payload = payload.pack
126
+ end
127
+
128
+ def pack
129
+ raise NoPayload if !payload
130
+ raise NoPath if !path
131
+ @message.raw_site = path.raw_site
132
+ @message.raw_target = path.raw_target
133
+ @message.tagged = path.tagged?
134
+ @message.msg_size = @message.num_bytes
135
+ @message.pack
136
+ end
137
+
138
+ def to_s
139
+ hash = {site: path.site_id}
140
+ if path.tagged?
141
+ hash[:tags] = path.tag_ids
142
+ hash[:tags] = 'all' if hash[:tags].empty?
143
+ else
144
+ hash[:device] = path.device_id
145
+ end
146
+ hash[:type] = payload.class.to_s.sub('LIFX::Protocol::', '')
147
+ hash[:addressable] = addressable? ? 'true' : 'false'
148
+ hash[:tagged] = path.tagged? ? 'true' : 'false'
149
+ hash[:at_time] = @message.at_time if @message.at_time && @message.at_time > 0
150
+ hash[:protocol] = protocol
151
+ hash[:payload] = payload.snapshot if payload
152
+ attrs = hash.map { |k, v| "#{k}=#{v}" }.join(' ')
153
+ %Q{#<LIFX::Message #{attrs}>}
154
+ end
155
+ alias_method :inspect, :to_s
156
+
157
+ protected
158
+
159
+ def check_valid_fields!(hash)
160
+ invalid_fields = hash.keys - self.class.valid_fields
161
+ if invalid_fields.count > 0
162
+ raise InvalidFields.new("Invalid fields for Message: #{invalid_fields.join(', ')}")
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,200 @@
1
+ require 'lifx/timers'
2
+ require 'lifx/transport_manager'
3
+ require 'lifx/routing_manager'
4
+ require 'lifx/tag_manager'
5
+ require 'lifx/light'
6
+ require 'lifx/protocol_path'
7
+
8
+ module LIFX
9
+ class NetworkContext
10
+ include Timers
11
+ include Logging
12
+ include Utilities
13
+ extend Forwardable
14
+
15
+ # NetworkContext stores lights and ties together TransportManager, TagManager and RoutingManager
16
+ attr_reader :transport_manager, :tag_manager, :routing_manager
17
+
18
+ def initialize(transport: :lan)
19
+ @devices = {}
20
+
21
+ @transport_manager = case transport
22
+ when :lan
23
+ TransportManager::LAN.new
24
+ else
25
+ raise ArgumentError.new("Unknown transport method: #{transport}")
26
+ end
27
+ @transport_manager.add_observer(self) do |message:, ip:, transport:|
28
+ handle_message(message, ip, transport)
29
+ end
30
+
31
+ reset!
32
+
33
+ @threads = []
34
+ @threads << initialize_timer_thread
35
+ initialize_periodic_refresh
36
+ initialize_message_rate_updater
37
+ end
38
+
39
+ def discover
40
+ @transport_manager.discover
41
+ end
42
+
43
+ def refresh
44
+ @routing_manager.refresh
45
+ end
46
+
47
+ def reset!
48
+ @routing_manager = RoutingManager.new(context: self)
49
+ @tag_manager = TagManager.new(context: self, tag_table: @routing_manager.tag_table)
50
+ end
51
+
52
+ def stop
53
+ @transport_manager.stop
54
+ @threads.each do |thread|
55
+ Thread.kill(thread)
56
+ end
57
+ end
58
+
59
+ # Sends a message to their destination(s)
60
+ # @param target: [Target] Target of the message
61
+ # @param payload: [Protocol::Payload] Message payload
62
+ # @param acknowledge: [Boolean] If recipients must acknowledge with a response
63
+ def send_message(target:, payload:, acknowledge: false)
64
+ paths = @routing_manager.resolve_target(target)
65
+
66
+ messages = paths.map do |path|
67
+ Message.new(path: path, payload: payload, acknowledge: acknowledge)
68
+ end
69
+
70
+ if within_sync?
71
+ Thread.current[:sync_messages].push(*messages)
72
+ return
73
+ end
74
+
75
+ messages.each do |message|
76
+ @transport_manager.write(message)
77
+ end
78
+ end
79
+
80
+ protected def within_sync?
81
+ !!Thread.current[:sync_enabled]
82
+ end
83
+
84
+ # Synchronize asynchronous set_color, set_waveform and set_power messages to multiple devices.
85
+ # You cannot use synchronous methods in the block
86
+ # @note This is alpha
87
+ # @yield Block to synchronize commands in
88
+ # @return [Float] Delay before messages are executed
89
+ def sync(&block)
90
+ if within_sync?
91
+ raise "You cannot nest sync"
92
+ end
93
+ messages = Thread.new do
94
+ Thread.current[:sync_enabled] = true
95
+ Thread.current[:sync_messages] = messages = []
96
+ block.call
97
+ Thread.current[:sync_enabled] = false
98
+ messages
99
+ end.join.value
100
+
101
+ time = nil
102
+ try_until -> { time } do
103
+ light = gateways.sample
104
+ time = light && light.time
105
+ end
106
+
107
+ delay = (messages.count + 1) * (1.0 / message_rate)
108
+ at_time = ((time.to_f + delay) * 1_000_000_000).to_i
109
+ messages.each do |m|
110
+ m.at_time = at_time
111
+ @transport_manager.write(m)
112
+ end
113
+ flush
114
+ delay
115
+ end
116
+
117
+ def flush(**options)
118
+ @transport_manager.flush(**options)
119
+ end
120
+
121
+ def register_device(device)
122
+ device_id = device.id
123
+ @devices[device_id] = device # What happens when there's already one registered?
124
+ end
125
+
126
+ def lights
127
+ LightCollection.new(context: self)
128
+ end
129
+
130
+ def all_lights
131
+ @devices.values
132
+ end
133
+
134
+ # Tags
135
+
136
+ def_delegators :@tag_manager, :tags,
137
+ :unused_tags,
138
+ :purge_unused_tags!,
139
+ :add_tag_to_device,
140
+ :remove_tag_from_device
141
+
142
+ def tags_for_device(device)
143
+ @routing_manager.tags_for_device_id(device.id)
144
+ end
145
+
146
+ def gateways
147
+ transport_manager.gateways.map(&:keys).flatten.map { |id| lights.with_id(id) }
148
+ end
149
+
150
+ def gateway_connections
151
+ transport_manager.gateways.map(&:values).flatten
152
+ end
153
+
154
+ protected
155
+
156
+ def handle_message(message, ip, transport)
157
+ logger.debug("<- #{self} #{transport}: #{message}")
158
+
159
+ @routing_manager.update_from_message(message)
160
+ if !message.tagged?
161
+ if @devices[message.device_id].nil?
162
+ device = Light.new(context: self, id: message.device_id, site_id: message.site_id)
163
+ register_device(device)
164
+ end
165
+ device = @devices[message.device_id]
166
+ device.handle_message(message, ip, transport)
167
+ end
168
+ end
169
+
170
+ def initialize_periodic_refresh
171
+ timers.every(10) do
172
+ refresh
173
+ end
174
+ end
175
+
176
+ def initialize_message_rate_updater
177
+ timers.every(5) do
178
+ missing_mesh_firmware = lights.select { |l| l.mesh_firmware(fetch: false).nil? }
179
+ if missing_mesh_firmware.count > 10
180
+ send_message(target: Target.new(broadcast: true), payload: Protocol::Device::GetMeshFirmware.new)
181
+ elsif missing_mesh_firmware.count > 0
182
+ missing_mesh_firmware.each { |l| l.send_message(Protocol::Device::GetMeshFirmware.new) }
183
+ else
184
+ @message_rate = lights.all? do |light|
185
+ m = light.mesh_firmware(fetch: false)
186
+ m && m >= '1.2'
187
+ end ? 20 : 5
188
+ gateway_connections.each do |connection|
189
+ connection.set_message_rate(@message_rate)
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ DEFAULT_MESSAGING_RATE = 5 # per second
196
+ def message_rate
197
+ @message_rate || 5
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,46 @@
1
+ module LIFX
2
+ module Observable
3
+ class ObserverCallbackMismatch < ArgumentError; end
4
+ def add_observer(obj, &callback)
5
+ if !callback_has_required_keys?(callback)
6
+ raise ObserverCallbackMismatch.new
7
+ end
8
+ observers[obj] = callback
9
+ end
10
+
11
+ def remove_observer(obj)
12
+ observers.delete(obj)
13
+ end
14
+
15
+ def notify_observers(**args)
16
+ observers.each do |_, callback|
17
+ callback.call(**args)
18
+ end
19
+ end
20
+
21
+ def callback_has_required_keys?(callback)
22
+ (required_keys_for_callback - required_keys_in_proc(callback)).empty?
23
+ end
24
+
25
+ def observer_callback_definition
26
+ nil
27
+ end
28
+
29
+ def required_keys_for_callback
30
+ @_required_keys_for_callback ||= begin
31
+ return [] if !observer_callback_definition
32
+ required_keys_in_proc(observer_callback_definition)
33
+ end
34
+ end
35
+
36
+ def required_keys_in_proc(proc)
37
+ proc.parameters.select do |type, _|
38
+ type == :keyreq
39
+ end.map(&:last)
40
+ end
41
+
42
+ def observers
43
+ @_observers ||= {}
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ module LIFX
2
+ module Protocol
3
+ module AddressFields
4
+ def AddressFields.included(mod)
5
+ mod.instance_eval do
6
+ hide :_reserved2
7
+ string :raw_target, length: 8
8
+ string :raw_site, length: 6
9
+ bool_bit1 :acknowledge
10
+ bit15le :_reserved2
11
+ end
12
+ end
13
+ end
14
+
15
+ class Address < BinData::Record
16
+ endian :little
17
+
18
+ include AddressFields
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,225 @@
1
+ # Generated code ahoy!
2
+ module LIFX
3
+ module Protocol
4
+ module Device
5
+ module Service
6
+ UDP = 1
7
+ TCP = 2
8
+ end
9
+
10
+ class SetSite < Payload
11
+ endian :little
12
+
13
+ string :site, length: 6
14
+ end
15
+
16
+ class GetPanGateway < Payload
17
+ endian :little
18
+
19
+ end
20
+
21
+ class StatePanGateway < Payload
22
+ endian :little
23
+
24
+ uint8 :service
25
+ uint32 :port
26
+ end
27
+
28
+ class GetTime < Payload
29
+ endian :little
30
+
31
+ end
32
+
33
+ class SetTime < Payload
34
+ endian :little
35
+
36
+ uint64 :time # Nanoseconds since epoch.
37
+ end
38
+
39
+ class StateTime < Payload
40
+ endian :little
41
+
42
+ uint64 :time # Nanoseconds since epoch.
43
+ end
44
+
45
+ class GetResetSwitch < Payload
46
+ endian :little
47
+
48
+ end
49
+
50
+ class StateResetSwitch < Payload
51
+ endian :little
52
+
53
+ uint8 :position
54
+ end
55
+
56
+ class GetMeshInfo < Payload
57
+ endian :little
58
+
59
+ end
60
+
61
+ class StateMeshInfo < Payload
62
+ endian :little
63
+
64
+ float :signal # Milliwatts.
65
+ uint32 :tx # Bytes.
66
+ uint32 :rx # Bytes.
67
+ int16 :mcu_temperature # Deci-celsius. 25.45 celsius is 2545
68
+ end
69
+
70
+ class GetMeshFirmware < Payload
71
+ endian :little
72
+
73
+ end
74
+
75
+ class StateMeshFirmware < Payload
76
+ endian :little
77
+
78
+ uint64 :build # Firmware build nanoseconds since epoch.
79
+ uint64 :install # Firmware install nanoseconds since epoch.
80
+ uint32 :version # Firmware human readable version.
81
+ end
82
+
83
+ class GetWifiInfo < Payload
84
+ endian :little
85
+
86
+ end
87
+
88
+ class StateWifiInfo < Payload
89
+ endian :little
90
+
91
+ float :signal # Milliwatts.
92
+ uint32 :tx # Bytes.
93
+ uint32 :rx # Bytes.
94
+ int16 :mcu_temperature # Deci-celsius. 25.45 celsius is 2545
95
+ end
96
+
97
+ class GetWifiFirmware < Payload
98
+ endian :little
99
+
100
+ end
101
+
102
+ class StateWifiFirmware < Payload
103
+ endian :little
104
+
105
+ uint64 :build # Firmware build nanoseconds since epoch.
106
+ uint64 :install # Firmware install nanoseconds since epoch.
107
+ uint32 :version # Firmware human readable version.
108
+ end
109
+
110
+ class GetPower < Payload
111
+ endian :little
112
+
113
+ end
114
+
115
+ class SetPower < Payload
116
+ endian :little
117
+
118
+ uint16 :level # 0 Standby. > 0 On.
119
+ end
120
+
121
+ class StatePower < Payload
122
+ endian :little
123
+
124
+ uint16 :level # 0 Standby. > 0 On.
125
+ end
126
+
127
+ class GetLabel < Payload
128
+ endian :little
129
+
130
+ end
131
+
132
+ class SetLabel < Payload
133
+ endian :little
134
+
135
+ string :label, length: 32, trim_padding: true
136
+ end
137
+
138
+ class StateLabel < Payload
139
+ endian :little
140
+
141
+ string :label, length: 32, trim_padding: true
142
+ end
143
+
144
+ class GetTags < Payload
145
+ endian :little
146
+
147
+ end
148
+
149
+ class SetTags < Payload
150
+ endian :little
151
+
152
+ uint64 :tags
153
+ end
154
+
155
+ class StateTags < Payload
156
+ endian :little
157
+
158
+ uint64 :tags
159
+ end
160
+
161
+ class GetTagLabels < Payload
162
+ endian :little
163
+
164
+ uint64 :tags
165
+ end
166
+
167
+ class SetTagLabels < Payload
168
+ endian :little
169
+
170
+ uint64 :tags
171
+ string :label, length: 32, trim_padding: true
172
+ end
173
+
174
+ class StateTagLabels < Payload
175
+ endian :little
176
+
177
+ uint64 :tags
178
+ string :label, length: 32, trim_padding: true
179
+ end
180
+
181
+ class GetVersion < Payload
182
+ endian :little
183
+
184
+ end
185
+
186
+ class StateVersion < Payload
187
+ endian :little
188
+
189
+ uint32 :vendor
190
+ uint32 :product
191
+ uint32 :version
192
+ end
193
+
194
+ class GetInfo < Payload
195
+ endian :little
196
+
197
+ end
198
+
199
+ class StateInfo < Payload
200
+ endian :little
201
+
202
+ uint64 :time # Nanoseconds since epoch.
203
+ uint64 :uptime # Nanoseconds since boot.
204
+ uint64 :downtime # Nanoseconds off last power cycle.
205
+ end
206
+
207
+ class GetMcuRailVoltage < Payload
208
+ endian :little
209
+
210
+ end
211
+
212
+ class StateMcuRailVoltage < Payload
213
+ endian :little
214
+
215
+ uint32 :voltage
216
+ end
217
+
218
+ class Reboot < Payload
219
+ endian :little
220
+
221
+ end
222
+
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,24 @@
1
+ module LIFX
2
+ module Protocol
3
+ module HeaderFields
4
+ def HeaderFields.included(mod)
5
+ mod.instance_eval do
6
+ hide :_reserved, :_reserved1
7
+
8
+ uint16 :msg_size
9
+ bit12le :protocol
10
+ bool_bit1 :addressable, value: true
11
+ bool_bit1 :tagged
12
+ bit2le :_reserved
13
+ uint32 :_reserved1
14
+ end
15
+ end
16
+ end
17
+
18
+ class Header < BinData::Record
19
+ endian :little
20
+
21
+ include HeaderFields
22
+ end
23
+ end
24
+ end