ruuvi_decoder 0.1.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5a5c8e7c19d0f0baa600ff7c8c27e72c7dba7540de5bd40f048dd523f191c37
4
- data.tar.gz: 7db878cfe01d36f8e00c673b12955dc564d5d45e57447593bb7037620aafc564
3
+ metadata.gz: 99b8b6bb2f2f5f314e67582f0aabc785fdfc198f24a1c726fdc0a191fb58aa4f
4
+ data.tar.gz: 99e31ef6fd0f52cf1ca5e92b7bb1f6099f810562030af3499aef51f75f7e499a
5
5
  SHA512:
6
- metadata.gz: 8a07a24f94e91ac1669a5801a8a553b48fb5426ea4333fa984ab5c532be568acaad62c4be4ed1ab0633777b7578a7135a2a090fd7c8989dc04c63c279b86f35d
7
- data.tar.gz: 43dc03770ad562a31f0feafdb57d1d07f84587780af1bad06b4e4ac143200a43401b560f7cb2b1d1ea1b148e463e7dcede7d56f42163c3ebb06aa04afdc7f03d
6
+ metadata.gz: f7586c315d75b937be794604c1644d0bc99113ada52430d23a98d4b437bef4d80b8279dd37e6017a4e4b4b7fda1efd9b6cf2f64a5f8bf6dd0985231d1db34cc2
7
+ data.tar.gz: 75f76eff5fb646a841140cfe6575338257ed4c691c5bd475f26bfa82780824b2595c3afb24c33591096a1cf54928e6437abd0d49112e8e9b6747fd5250ef1438
data/.rubocop.yml CHANGED
@@ -15,4 +15,7 @@ RSpec/MultipleExpectations:
15
15
  Enabled: false
16
16
 
17
17
  Metrics/MethodLength:
18
- Max: 20
18
+ Max: 20
19
+
20
+ Metrics/ClassLength:
21
+ Max: 500
data/CHANGELOG.md CHANGED
@@ -1,4 +1,6 @@
1
1
  ## [Unreleased]
2
+ ## [1.0.0] - 2025-03-31
3
+ - Support all current data formats, V3, V5, C5, and encrypted V8.
2
4
 
3
5
  ## [0.1.0] - 2025-03-29
4
6
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuuviDecoder
4
+ # Abstract base class for decoders.
5
+ class BaseData
6
+ def initialize(raw_data)
7
+ @raw_data = RuuviDecoder.normalize_raw_data(raw_data)
8
+
9
+ raise ArgumentError, 'data is not valid for this format' unless self.class.detect(@raw_data)
10
+ end
11
+
12
+ def self.detect(_raw_data)
13
+ raise 'subclass must implement this method'
14
+ end
15
+
16
+ protected
17
+
18
+ attr_reader :raw_data
19
+
20
+ def decode_16_bits_signed(bytes, multiplier: 1, invalid: nil, offset: 0)
21
+ return if !invalid.nil? && bytes == [invalid].pack('s>').bytes
22
+
23
+ signed_int = bytes.pack('C*').unpack1('s>')
24
+ (signed_int + offset) * multiplier
25
+ end
26
+
27
+ def decode_16_bits_unsigned(bytes, multiplier: 1, invalid: nil, offset: 0)
28
+ return if !invalid.nil? && bytes == [invalid].pack('S>').bytes
29
+
30
+ signed_int = bytes.pack('C*').unpack1('S>')
31
+ (signed_int + offset) * multiplier
32
+ end
33
+
34
+ def decode_bitmasked_unsigned(value, mask, multiplier: 1, invalid: nil, offset: 0)
35
+ value &= mask
36
+ return if !invalid.nil? && value == invalid
37
+
38
+ (value + offset) * multiplier
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuuviDecoder
4
+ # Decoder for V5 formatted data.
5
+ # https://docs.ruuvi.com/communication/bluetooth-advertisements/data-format-5-rawv2
6
+ class C5Data < BaseData
7
+ MANUFACTURER_ID = 0x0499
8
+ DATA_LENGTH_BYTES = 18
9
+ VERSION_TAG = 0xC5
10
+
11
+ def self.detect(raw_data)
12
+ raw_data.size == DATA_LENGTH_BYTES && raw_data[0] == VERSION_TAG
13
+ end
14
+
15
+ def mac_address
16
+ return @mac_address if defined?(@mac_address)
17
+
18
+ @mac_address =
19
+ begin
20
+ bytes = raw_data[12..17]
21
+ if bytes.all?(0xff)
22
+ nil
23
+ else
24
+ bytes.map { |b| format('%02x', b) }.join(':')
25
+ end
26
+ end
27
+ end
28
+
29
+ def sequence_number
30
+ return @sequence_number if defined?(@sequence_number)
31
+
32
+ @sequence_number = decode_16_bits_unsigned(raw_data[10..11], multiplier: 1, invalid: 65_535)
33
+ end
34
+
35
+ def movement_counter
36
+ return @movement_counter if defined?(@movement_counter)
37
+
38
+ @movement_counter = decode_16_bits_unsigned([0, raw_data[9]], multiplier: 1, invalid: 255)
39
+ end
40
+
41
+ def tx_power_dbm
42
+ return @tx_power_dbm if defined?(@tx_power_dbm)
43
+
44
+ @tx_power_dbm = decode_bitmasked_unsigned(raw_power_info, 0x001F, offset: -20, multiplier: 2, invalid: 31)
45
+ end
46
+
47
+ def battery_v
48
+ return @battery_v if defined?(@battery_v)
49
+
50
+ @battery_v = decode_bitmasked_unsigned(raw_power_info >> 5, 0x07FF, offset: 1600, multiplier: 0.001,
51
+ invalid: 2047)
52
+ end
53
+
54
+ def pressure_hpa
55
+ return @pressure_hpa if defined?(@pressure_hpa)
56
+
57
+ @pressure_hpa = decode_16_bits_unsigned(raw_data[5..6], offset: 50_000, invalid: 65_535, multiplier: 0.01)
58
+ end
59
+
60
+ def humidity_pct
61
+ return @humidity_pct if defined?(@humidity_pct)
62
+
63
+ @humidity_pct = decode_16_bits_unsigned(raw_data[3..4], multiplier: 0.0025, invalid: 65_535)
64
+ end
65
+
66
+ def temperature_c
67
+ return @temperature_c if defined?(@temperature_c)
68
+
69
+ @temperature_c = decode_16_bits_signed(raw_data[1..2], multiplier: 0.005, invalid: 0x8000)
70
+ end
71
+
72
+ def inspect
73
+ <<~INSPECT.chomp
74
+ <#{self.class.name}
75
+ temperature: #{temperature_c} C,
76
+ humidity: #{humidity_pct} %,
77
+ pressure: #{pressure_hpa} hPa,
78
+ battery: #{battery_v} V,
79
+ tx_power: #{tx_power_dbm} dBm,
80
+ movement_counter: #{movement_counter},
81
+ sequence_number: #{sequence_number},
82
+ mac_address: #{mac_address}
83
+ >
84
+ INSPECT
85
+ end
86
+
87
+ private
88
+
89
+ def raw_power_info
90
+ @raw_power_info = decode_16_bits_unsigned(raw_data[7..8])
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuuviDecoder
4
+ # Decoder for V5 formatted data.
5
+ # https://docs.ruuvi.com/communication/bluetooth-advertisements/data-format-5-rawv2
6
+ class V3Data < BaseData
7
+ MANUFACTURER_ID = 0x0499
8
+ DATA_LENGTH_BYTES = 14
9
+ VERSION_TAG = 0x03
10
+
11
+ def self.detect(raw_data)
12
+ raw_data.size == DATA_LENGTH_BYTES && raw_data[0] == VERSION_TAG
13
+ end
14
+
15
+ def humidity_pct
16
+ return @humidity_pct if defined?(@humidity_pct)
17
+
18
+ @humidity_pct = raw_data[1] * 0.5
19
+ end
20
+
21
+ def temperature_c
22
+ return @temperature_c if defined?(@temperature_c)
23
+
24
+ sign = raw_data[2].nobits?(0x80) ? 1 : -1
25
+ decimal = raw_data[2] & 0x7F
26
+ return @temperature_c = nil if decimal > 127
27
+
28
+ fraction = raw_data[3] * 0.01
29
+ return @temperature_c = nil if raw_data[3] > 99
30
+
31
+ @temperature_c = sign * (decimal + fraction)
32
+ end
33
+
34
+ def pressure_hpa
35
+ return @pressure_hpa if defined?(@pressure_hpa)
36
+
37
+ @pressure_hpa = decode_16_bits_unsigned(raw_data[4..5], offset: 50_000, multiplier: 0.01)
38
+ end
39
+
40
+ def acceleration_x_g
41
+ return @acceleration_x_g if defined?(@acceleration_x_g)
42
+
43
+ @acceleration_x_g = decode_16_bits_signed(raw_data[6..7], multiplier: 0.001)
44
+ end
45
+
46
+ def acceleration_y_g
47
+ return @acceleration_y_g if defined?(@acceleration_y_g)
48
+
49
+ @acceleration_y_g = decode_16_bits_signed(raw_data[8..9], multiplier: 0.001)
50
+ end
51
+
52
+ def acceleration_z_g
53
+ return @acceleration_z_g if defined?(@acceleration_z_g)
54
+
55
+ @acceleration_z_g = decode_16_bits_signed(raw_data[10..11], multiplier: 0.001)
56
+ end
57
+
58
+ def battery_v
59
+ return @battery_v if defined?(@battery_v)
60
+
61
+ @battery_v = decode_16_bits_unsigned(raw_data[12..13], multiplier: 0.001)
62
+ end
63
+
64
+ def inspect
65
+ <<~INSPECT.chomp
66
+ <#{self.class.name}
67
+ temperature: #{temperature_c} C,
68
+ humidity: #{humidity_pct} %,
69
+ pressure: #{pressure_hpa} hPa,
70
+ acceleration_x: #{acceleration_x_g} G,
71
+ acceleration_y: #{acceleration_y_g} G,
72
+ acceleration_z: #{acceleration_z_g} G,
73
+ battery: #{battery_v} V,
74
+ >
75
+ INSPECT
76
+ end
77
+ end
78
+ end
@@ -3,16 +3,10 @@
3
3
  module RuuviDecoder
4
4
  # Decoder for V5 formatted data.
5
5
  # https://docs.ruuvi.com/communication/bluetooth-advertisements/data-format-5-rawv2
6
- class V5Data
6
+ class V5Data < BaseData
7
7
  MANUFACTURER_ID = 0x0499
8
8
  DATA_LENGTH_BYTES = 24
9
- VERSION_TAG = 5
10
-
11
- def initialize(raw_data)
12
- @raw_data = RuuviDecoder.normalize_raw_data(raw_data)
13
-
14
- raise ArgumentError, 'data is not valid v5 data' unless self.class.detect(@raw_data)
15
- end
9
+ VERSION_TAG = 0x05
16
10
 
17
11
  def self.detect(raw_data)
18
12
  raw_data.size == DATA_LENGTH_BYTES && raw_data[0] == VERSION_TAG
@@ -113,31 +107,8 @@ module RuuviDecoder
113
107
 
114
108
  private
115
109
 
116
- attr_reader :raw_data
117
-
118
110
  def raw_power_info
119
111
  @raw_power_info = decode_16_bits_unsigned(raw_data[13..14])
120
112
  end
121
-
122
- def decode_16_bits_signed(bytes, multiplier: 1, invalid: nil, offset: 0)
123
- return if !invalid.nil? && bytes == [invalid].pack('s>').bytes
124
-
125
- signed_int = bytes.pack('C*').unpack1('s>')
126
- (signed_int + offset) * multiplier
127
- end
128
-
129
- def decode_16_bits_unsigned(bytes, multiplier: 1, invalid: nil, offset: 0)
130
- return if !invalid.nil? && bytes == [invalid].pack('S>').bytes
131
-
132
- signed_int = bytes.pack('C*').unpack1('S>')
133
- (signed_int + offset) * multiplier
134
- end
135
-
136
- def decode_bitmasked_unsigned(value, mask, multiplier: 1, invalid: nil, offset: 0)
137
- value &= mask
138
- return if !invalid.nil? && value == invalid
139
-
140
- (value + offset) * multiplier
141
- end
142
113
  end
143
114
  end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module RuuviDecoder
6
+ # Decoder for V8 formatted encrypted data. (DRAFT specification subject to change!)
7
+ # https://docs.ruuvi.com/communication/bluetooth-advertisements/data-format-8-encrypted-environmental
8
+ class V8Data < BaseData
9
+ MANUFACTURER_ID = 0x0499
10
+ DATA_LENGTH_BYTES = 24
11
+ VERSION_TAG = 0x08
12
+ # https://github.com/ruuvi/ruuvi.endpoints.c/blob/master/src/ruuvi_endpoints.c#L8
13
+ CRC8_TABLE = [ # rubocop:disable Metrics/CollectionLiteralLength
14
+ 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
15
+ 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
16
+ 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
17
+ 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
18
+ 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
19
+ 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
20
+ 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
21
+ 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
22
+ 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
23
+ 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
24
+ 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
25
+ 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
26
+ 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
27
+ 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
28
+ 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
29
+ 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
30
+ 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
31
+ 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
32
+ 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
33
+ 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
34
+ 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
35
+ 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
36
+ 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
37
+ 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
38
+ 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
39
+ 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
40
+ 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
41
+ 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
42
+ 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
43
+ 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
44
+ 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
45
+ 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
46
+ ].freeze
47
+
48
+ def initialize(raw_data, ruuvi_tag_id, password)
49
+ super(raw_data)
50
+
51
+ @ruuvi_tag_id = RuuviDecoder.normalize_raw_data(ruuvi_tag_id)
52
+ @password = case password
53
+ when String
54
+ password.bytes
55
+ when Enumerable
56
+ password
57
+ else
58
+ raise 'password must be a String or Enumerable of bytes'
59
+ end
60
+
61
+ raise ArgumentError, 'ruuvi_tag_id must be 8 bytes' if @ruuvi_tag_id.size != 8
62
+ # fixed 16 byte key size is a quirk
63
+ # https://github.com/ruuvi/ruuvi.firmware.c/blob/f5e159e82d150f76988a41acf39f9d2699debc85/src/app_dataformats.c#L34
64
+ raise ArgumentError, 'password must be at least 8 bytes' if @password.size != 16
65
+ end
66
+
67
+ def self.detect(raw_data)
68
+ raw_data.size == DATA_LENGTH_BYTES && raw_data[0] == VERSION_TAG
69
+ end
70
+
71
+ def temperature_c
72
+ return @temperature_c if defined?(@temperature_c)
73
+
74
+ @temperature_c = decode_16_bits_signed(decrypted_raw_data[0..1], multiplier: 0.005, invalid: 0x8000)
75
+ end
76
+
77
+ def humidity_pct
78
+ return @humidity_pct if defined?(@humidity_pct)
79
+
80
+ @humidity_pct = decode_16_bits_unsigned(decrypted_raw_data[2..3], multiplier: 0.0025, invalid: 65_535)
81
+ end
82
+
83
+ def pressure_hpa
84
+ return @pressure_hpa if defined?(@pressure_hpa)
85
+
86
+ @pressure_hpa = decode_16_bits_unsigned(decrypted_raw_data[4..5], offset: 50_000, invalid: 65_535,
87
+ multiplier: 0.01)
88
+ end
89
+
90
+ def tx_power_dbm
91
+ return @tx_power_dbm if defined?(@tx_power_dbm)
92
+
93
+ @tx_power_dbm = decode_bitmasked_unsigned(raw_power_info, 0x001F, offset: -20, multiplier: 2, invalid: 31)
94
+ end
95
+
96
+ def battery_v
97
+ return @battery_v if defined?(@battery_v)
98
+
99
+ @battery_v = decode_bitmasked_unsigned(raw_power_info >> 5, 0x07FF, offset: 1600, multiplier: 0.001,
100
+ invalid: 2047)
101
+ end
102
+
103
+ def movement_counter
104
+ return @movement_counter if defined?(@movement_counter)
105
+
106
+ @movement_counter = decode_16_bits_unsigned(decrypted_raw_data[8..9], invalid: 65_535)
107
+ end
108
+
109
+ def sequence_number
110
+ return @sequence_number if defined?(@sequence_number)
111
+
112
+ @sequence_number = decode_16_bits_unsigned(decrypted_raw_data[10..11], invalid: 65_535)
113
+ end
114
+
115
+ def mac_address
116
+ return @mac_address if defined?(@mac_address)
117
+
118
+ @mac_address =
119
+ begin
120
+ bytes = raw_data[18..24]
121
+ if bytes.all?(0xff)
122
+ nil
123
+ else
124
+ bytes.map { |b| format('%02x', b) }.join(':')
125
+ end
126
+ end
127
+ end
128
+
129
+ def inspect
130
+ <<~INSPECT.chomp
131
+ <#{self.class.name}
132
+ temperature: #{temperature_c} C,
133
+ humidity: #{humidity_pct} %,
134
+ pressure: #{pressure_hpa} hPa,
135
+ battery: #{battery_v} V,
136
+ tx_power: #{tx_power_dbm} dBm,
137
+ movement_counter: #{movement_counter},
138
+ sequence_number: #{sequence_number},
139
+ mac_address: #{mac_address}
140
+ >
141
+ INSPECT
142
+ end
143
+
144
+ private
145
+
146
+ def aes_key
147
+ return @aes_key if defined?(@aes_key)
148
+
149
+ aes_key = @password.dup
150
+ (0...8).each do |idx|
151
+ aes_key[idx] = aes_key[idx] ^ @ruuvi_tag_id[idx]
152
+ end
153
+ @aes_key = aes_key.pack('C*')
154
+ end
155
+
156
+ def decrypted_raw_data
157
+ return @decrypted_raw_data if defined?(@decrypted_raw_data)
158
+
159
+ cipher = OpenSSL::Cipher.new('AES-128-ECB').decrypt
160
+ cipher.key = aes_key
161
+ cipher.padding = 0 # disable padding
162
+ @decrypted_raw_data = cipher.update(raw_data[1..16].pack('C*'))
163
+ @decrypted_raw_data << cipher.final
164
+ @decrypted_raw_data = @decrypted_raw_data.bytes
165
+ check_crc8!
166
+ @decrypted_raw_data
167
+ end
168
+
169
+ def check_crc8!
170
+ crc = @decrypted_raw_data.reduce(0x00) { |crc, byte| CRC8_TABLE[crc ^ byte] }
171
+ raise 'calculated CRC-8 does not match.' if crc != raw_data[17]
172
+ end
173
+
174
+ def raw_power_info
175
+ @raw_power_info = decode_16_bits_unsigned(decrypted_raw_data[6..7])
176
+ end
177
+ end
178
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuuviDecoder
4
- VERSION = '0.1.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/ruuvi_decoder.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'ruuvi_decoder/version'
4
+ require_relative 'ruuvi_decoder/base_data'
5
+ require_relative 'ruuvi_decoder/v3_data'
6
+ require_relative 'ruuvi_decoder/c5_data'
4
7
  require_relative 'ruuvi_decoder/v5_data'
8
+ require_relative 'ruuvi_decoder/v8_data'
5
9
 
6
10
  # Decoders for various binary data formats emitted by Ruuvi bluetooth sensors.
7
11
  # Use classes directly or the static decode method to automatically select one based
@@ -10,10 +14,16 @@ module RuuviDecoder
10
14
  def self.decode(raw_data)
11
15
  raw_data = normalize_raw_data(raw_data)
12
16
 
13
- # TODO(simo): implement more formats :)
14
- decoder_class = [V5Data].find { |data_format| data_format.detect(raw_data) }
17
+ decoder_class = [
18
+ V3Data,
19
+ V5Data,
20
+ C5Data,
21
+ V8Data
22
+ ].find { |data_format| data_format.detect(raw_data) }
15
23
  raise 'no decoder found' if decoder_class.nil?
16
24
 
25
+ raise 'instantiate V8Data directly, with tag id and password' if decoder_class == V8Data
26
+
17
27
  decoder_class.new(raw_data)
18
28
  end
19
29
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruuvi_decoder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simo Leone
@@ -22,7 +22,11 @@ files:
22
22
  - README.md
23
23
  - Rakefile
24
24
  - lib/ruuvi_decoder.rb
25
+ - lib/ruuvi_decoder/base_data.rb
26
+ - lib/ruuvi_decoder/c5_data.rb
27
+ - lib/ruuvi_decoder/v3_data.rb
25
28
  - lib/ruuvi_decoder/v5_data.rb
29
+ - lib/ruuvi_decoder/v8_data.rb
26
30
  - lib/ruuvi_decoder/version.rb
27
31
  homepage: https://github.com/simoleone/ruuvi_decoder
28
32
  licenses: