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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +2 -0
- data/lib/ruuvi_decoder/base_data.rb +41 -0
- data/lib/ruuvi_decoder/c5_data.rb +93 -0
- data/lib/ruuvi_decoder/v3_data.rb +78 -0
- data/lib/ruuvi_decoder/v5_data.rb +2 -31
- data/lib/ruuvi_decoder/v8_data.rb +178 -0
- data/lib/ruuvi_decoder/version.rb +1 -1
- data/lib/ruuvi_decoder.rb +12 -2
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99b8b6bb2f2f5f314e67582f0aabc785fdfc198f24a1c726fdc0a191fb58aa4f
|
4
|
+
data.tar.gz: 99e31ef6fd0f52cf1ca5e92b7bb1f6099f810562030af3499aef51f75f7e499a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7586c315d75b937be794604c1644d0bc99113ada52430d23a98d4b437bef4d80b8279dd37e6017a4e4b4b7fda1efd9b6cf2f64a5f8bf6dd0985231d1db34cc2
|
7
|
+
data.tar.gz: 75f76eff5fb646a841140cfe6575338257ed4c691c5bd475f26bfa82780824b2595c3afb24c33591096a1cf54928e6437abd0d49112e8e9b6747fd5250ef1438
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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 =
|
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
|
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
|
-
|
14
|
-
|
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:
|
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:
|