|
1
|
|
|
# Copyright 2019 Virantha N. Ekanayake |
|
2
|
|
|
# |
|
3
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
4
|
|
|
# you may not use this file except in compliance with the License. |
|
5
|
|
|
# You may obtain a copy of the License at |
|
6
|
|
|
# |
|
7
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
8
|
|
|
# |
|
9
|
|
|
# Unless required by applicable law or agreed to in writing, software |
|
10
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
11
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
12
|
|
|
# See the License for the specific language governing permissions and |
|
13
|
|
|
# limitations under the License. |
|
14
|
|
|
"""Message parsers for each message type |
|
15
|
|
|
|
|
16
|
|
|
""" |
|
17
|
|
|
import struct, logging |
|
18
|
|
|
from .const import DEVICES |
|
19
|
|
|
|
|
20
|
|
|
logger = logging.getLogger(__name__) |
|
21
|
|
|
class UnknownMessageError(Exception): pass |
|
22
|
|
|
|
|
23
|
|
|
class Message: |
|
24
|
|
|
"""Base class for each message parser. |
|
25
|
|
|
|
|
26
|
|
|
This class instance keeps track of each subclass and stores an object of |
|
27
|
|
|
each subclass in the attribue `parsers`. None of these subclass instances |
|
28
|
|
|
should ever store any instance data since these are shared across multiple |
|
29
|
|
|
hubs. |
|
30
|
|
|
|
|
31
|
|
|
Attributes: |
|
32
|
|
|
parsers (dict) : msg_type (int) -> Message parser |
|
33
|
|
|
msg_type(int) : msg_type of each subclassed message |
|
34
|
|
|
""" |
|
35
|
|
|
|
|
36
|
|
|
parsers = {} |
|
37
|
|
|
|
|
38
|
|
|
def __init_subclass__(cls): |
|
39
|
|
|
"""Register message subclasses""" |
|
40
|
|
|
logger.debug(f"registering {cls}") |
|
41
|
|
|
assert cls.msg_type not in Message.parsers, f'Duplicate Message parser type {cls.msg_type} found in code!' |
|
42
|
|
|
Message.parsers[cls.msg_type] = cls() |
|
43
|
|
|
|
|
44
|
|
|
def _parse_msg_bytes(self, msg_bytes): |
|
45
|
|
|
hex_bytes = ':'.join(hex(c) for c in msg_bytes) |
|
46
|
|
|
return hex_bytes |
|
47
|
|
|
|
|
48
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
49
|
|
|
"""Implement this handle parsing of each message body type. |
|
50
|
|
|
|
|
51
|
|
|
Args: |
|
52
|
|
|
msg_bytes (bytearray): Message body |
|
53
|
|
|
l (list): text description of what's being parsed for logging (just append details as you go along) |
|
54
|
|
|
dispatcher (:class:`bricknil.message_dispatch.MessageDispatch`): The dispatch object that is sending messages. |
|
55
|
|
|
Call back into its methods to send messages back to the hub. |
|
56
|
|
|
""" |
|
57
|
|
|
pass |
|
58
|
|
|
|
|
59
|
|
|
class PortValueMessage(Message): |
|
60
|
|
|
"""Single value update from a sensor |
|
61
|
|
|
""" |
|
62
|
|
|
msg_type = 0x45 |
|
63
|
|
|
|
|
64
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
65
|
|
|
port = msg_bytes.pop(0) |
|
66
|
|
|
dispatcher.message_update_value_to_peripheral(port, msg_bytes) |
|
67
|
|
|
l.append(f'Port {port} changed value to {msg_bytes}') |
|
68
|
|
|
|
|
69
|
|
|
class PortComboValueMessage(Message): |
|
70
|
|
|
"""Multiple (combination) value updates from different modes of a sensor |
|
71
|
|
|
""" |
|
72
|
|
|
msg_type = 0x46 |
|
73
|
|
|
|
|
74
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
75
|
|
|
port = msg_bytes.pop(0) |
|
76
|
|
|
dispatcher.message_update_value_to_peripheral(port, msg_bytes) |
|
77
|
|
|
l.append(f'Port {port} changed combo value to {msg_bytes}') |
|
78
|
|
|
|
|
79
|
|
|
class HubPropertiesMessage(Message): |
|
80
|
|
|
"""Used to get data on the hub as well as button press information on the hub |
|
81
|
|
|
""" |
|
82
|
|
|
msg_type = 0x01 |
|
83
|
|
|
prop_names = { 0x01: 'Advertising Name', |
|
84
|
|
|
0x02: 'Button', |
|
85
|
|
|
0x03: 'FW Version', |
|
86
|
|
|
0x04: 'HW Version', |
|
87
|
|
|
0x05: 'RSSI', |
|
88
|
|
|
0x06: 'Battery Voltage', |
|
89
|
|
|
0x07: 'Battery Type', |
|
90
|
|
|
0x08: 'Manufacturer Name', |
|
91
|
|
|
0x09: 'Radio FW Version', |
|
92
|
|
|
0x0A: 'LEGO Wireles Protocol Version', |
|
93
|
|
|
0x0B: 'System Type ID', |
|
94
|
|
|
0x0C: 'HW Network ID', |
|
95
|
|
|
0x0D: 'Primary MAC address', |
|
96
|
|
|
0x0E: 'Seconary MAC address', |
|
97
|
|
|
0X0F: 'HW Network Family', |
|
98
|
|
|
} |
|
99
|
|
|
operation_names = { 0x01: 'Set (downstream)', |
|
100
|
|
|
0x02: 'Enable Updates (Downstream)', |
|
101
|
|
|
0x03: 'Disable Updates (Downstream)', |
|
102
|
|
|
0x04: 'Reset (Downstream)', |
|
103
|
|
|
0x05: 'Request Update (Downstream)', |
|
104
|
|
|
0x06: 'Update (Upstream)', |
|
105
|
|
|
} |
|
106
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
107
|
|
|
l.append('Hub property: ') |
|
108
|
|
|
|
|
109
|
|
|
prop = msg_bytes.pop(0) |
|
110
|
|
|
if prop not in self.prop_names: |
|
111
|
|
|
raise UnknownMessageError |
|
112
|
|
|
l.append(self.prop_names[prop]) |
|
113
|
|
|
|
|
114
|
|
|
op = msg_bytes.pop(0) |
|
115
|
|
|
if op not in self.operation_names: |
|
116
|
|
|
raise UnknownMessageError |
|
117
|
|
|
l.append(self.operation_names[op]) |
|
118
|
|
|
|
|
119
|
|
|
# Now, just append the number |
|
120
|
|
|
l.append(self._parse_msg_bytes(msg_bytes)) |
|
121
|
|
|
|
|
122
|
|
|
# Now forward any button presses as if it were a "port value" change |
|
123
|
|
|
if prop==0x02 and op == 0x06: # Button and update op |
|
124
|
|
|
msg_bytes.insert(0, 0xFF) # Insert Dummy port value of 255 |
|
125
|
|
|
Message.parsers[PortValueMessage.msg_type].parse(msg_bytes, l, dispatcher) |
|
126
|
|
|
|
|
127
|
|
|
class PortInformationMessage(Message): |
|
128
|
|
|
"""Information on what modes are supported on a port and whether a port |
|
129
|
|
|
is input/output. |
|
130
|
|
|
""" |
|
131
|
|
|
msg_type = 0x43 |
|
132
|
|
|
|
|
133
|
|
|
def _parse_mode_info(self, msg_bytes, l, port_info): |
|
134
|
|
|
l.append(' INFO:') |
|
135
|
|
|
capabilities = msg_bytes.pop(0) |
|
136
|
|
|
bitmask = ['output', 'input', 'combinable', 'synchronizable'] |
|
137
|
|
|
for i, attr in enumerate(bitmask): |
|
138
|
|
|
port_info[attr] = capabilities & 1<<i |
|
139
|
|
|
if port_info[attr]: l.append(attr[:3]) |
|
140
|
|
|
|
|
141
|
|
|
def _parse_mode_info_input_output(self, msg_bytes, l, modes_info): |
|
142
|
|
|
input_modes = msg_bytes.pop(0) + msg_bytes.pop(0)*256 |
|
143
|
|
|
output_modes = msg_bytes.pop(0) + msg_bytes.pop(0)*256 |
|
144
|
|
|
for i in range(16): |
|
145
|
|
|
if input_modes & (1<<i): |
|
146
|
|
|
l.append(i) |
|
147
|
|
|
mode_info = modes_info.setdefault(i, {}) |
|
148
|
|
|
mode_info['input'] = True |
|
149
|
|
|
l.append(', output: ') |
|
150
|
|
|
for i in range(16): |
|
151
|
|
|
if output_modes & (1<<i): |
|
152
|
|
|
l.append(i) |
|
153
|
|
|
mode_info = modes_info.setdefault(i, {}) |
|
154
|
|
|
mode_info['output'] = True |
|
155
|
|
|
|
|
156
|
|
|
def _parse_combination_info(self, msg_bytes, l, port_info): |
|
157
|
|
|
port_info['mode_combinations'] = [] |
|
158
|
|
|
|
|
159
|
|
|
mode_combo = msg_bytes.pop(0) + msg_bytes.pop(0)*256 |
|
160
|
|
|
l.append('Combinations:') |
|
161
|
|
|
while mode_combo != 0: |
|
162
|
|
|
cmodes = [] |
|
163
|
|
|
for i in range(16): |
|
164
|
|
|
if mode_combo & (1<<i): |
|
165
|
|
|
cmodes.append(i) |
|
166
|
|
|
l.append('+'.join([f'Mode {m}' for m in cmodes])) |
|
167
|
|
|
port_info['mode_combinations'].append(cmodes) |
|
168
|
|
|
if len(msg_bytes) == 0: |
|
169
|
|
|
mode_combo = 0 |
|
170
|
|
|
else: |
|
171
|
|
|
mode_combo = msg_bytes.pop(0) + msg_bytes.pop(0)*256 |
|
172
|
|
|
l.append(', ') |
|
173
|
|
|
|
|
174
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
175
|
|
|
port = msg_bytes.pop(0) |
|
176
|
|
|
mode = msg_bytes.pop(0) |
|
177
|
|
|
l.append(f'Port {port} Mode {mode}:') |
|
178
|
|
|
|
|
179
|
|
|
port_info = dispatcher.port_info.setdefault(port, {}) |
|
180
|
|
|
modes_info = port_info.setdefault('modes', {}) |
|
181
|
|
|
if mode == 0x01: # MODE INFO |
|
182
|
|
|
self._parse_mode_info(msg_bytes, l, port_info) |
|
183
|
|
|
nModes = msg_bytes.pop(0) |
|
184
|
|
|
l.append(f'nModes:{nModes}, input:') |
|
185
|
|
|
self._parse_mode_info_input_output(msg_bytes, l, modes_info) |
|
186
|
|
|
dispatcher.message_port_info_to_peripheral(port, 'port_info_received') |
|
187
|
|
|
elif mode == 0x02: # Combination info |
|
188
|
|
|
self._parse_combination_info(msg_bytes, l, port_info) |
|
189
|
|
|
dispatcher.message_port_info_to_peripheral(port, 'port_combination_info_received') |
|
190
|
|
|
else: |
|
191
|
|
|
raise UnknownMessageError |
|
192
|
|
|
|
|
193
|
|
|
class PortOutputFeedbackMessage(Message): |
|
194
|
|
|
"""Ack messages/error messages sent in response to a command being issued to the hub |
|
195
|
|
|
""" |
|
196
|
|
|
msg_type = 0x82 |
|
197
|
|
|
|
|
198
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
199
|
|
|
port = msg_bytes.pop(0) |
|
200
|
|
|
l.append(f'Command feedback: Port {port}') |
|
201
|
|
|
feedback = msg_bytes.pop(0) |
|
202
|
|
|
if feedback & 1: |
|
203
|
|
|
l.append('Buffer empty, Command in progess') |
|
204
|
|
|
if feedback & 2: |
|
205
|
|
|
l.append('Buffer empty, Command completed') |
|
206
|
|
|
if feedback & 4: |
|
207
|
|
|
l.append('Command discarded') |
|
208
|
|
|
if feedback & 8: |
|
209
|
|
|
l.append('Idle') |
|
210
|
|
|
if feedback & 16: |
|
211
|
|
|
l.append('Busy/Full') |
|
212
|
|
|
|
|
213
|
|
|
class PortModeInformationMessage(Message): |
|
214
|
|
|
"""Information on a specific mode |
|
215
|
|
|
|
|
216
|
|
|
This tells us a mode's name, what numeric format it uses, and it's range. |
|
217
|
|
|
""" |
|
218
|
|
|
msg_type = 0x44 |
|
219
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
220
|
|
|
port = msg_bytes.pop(0) |
|
221
|
|
|
mode = msg_bytes.pop(0) |
|
222
|
|
|
mode_type = msg_bytes.pop(0) |
|
223
|
|
|
|
|
224
|
|
|
port_info = dispatcher.port_info.setdefault(port, {}) |
|
225
|
|
|
modes_info = port_info.setdefault('modes', {}) |
|
226
|
|
|
mode_info = modes_info.setdefault(mode, {}) |
|
227
|
|
|
|
|
228
|
|
|
l.append(f'MODE INFO Port:{port} Mode:{mode}') |
|
229
|
|
|
mode_types = { 0: self._parse_name, |
|
230
|
|
|
0x1: self._parse_raw_range, |
|
231
|
|
|
0x2: self._parse_pct_range, |
|
232
|
|
|
0x3: self._parse_si_range, |
|
233
|
|
|
0x4: self._parse_symbol, |
|
234
|
|
|
0x5: self._parse_mapping, |
|
235
|
|
|
0x82: self._parse_format, |
|
236
|
|
|
} |
|
237
|
|
|
if mode_type in mode_types: |
|
238
|
|
|
mode_types[mode_type](msg_bytes, l, mode_info) |
|
239
|
|
|
else: |
|
240
|
|
|
raise UnknownMessageError |
|
241
|
|
|
dispatcher.message_port_info_to_peripheral(port, 'port_mode_info_received') |
|
242
|
|
|
|
|
243
|
|
|
|
|
244
|
|
|
def _parse_format(self, msg_bytes, l, mode_info): |
|
245
|
|
|
# 4 bytes |
|
246
|
|
|
# [0] = Number of datasets (e.g. RBG has 3 for each color) |
|
247
|
|
|
# [1] = Dataset type. 00-byte, 01=16b, 10=32b, 11=float |
|
248
|
|
|
# [2] = Total figures |
|
249
|
|
|
# [3] = Decimals if any |
|
250
|
|
|
mode_info['datasets'] = msg_bytes.pop(0) |
|
251
|
|
|
dataset_types = ['8b', '16b', '32b', 'float'] |
|
252
|
|
|
mode_info['dataset_type'] = dataset_types[msg_bytes.pop(0)] |
|
253
|
|
|
mode_info['dataset_total_figures'] = msg_bytes.pop(0) |
|
254
|
|
|
mode_info['dataset_decimals'] = msg_bytes.pop(0) |
|
255
|
|
|
|
|
256
|
|
|
def _parse_mapping(self, msg_bytes, l, mode_info): |
|
257
|
|
|
l.append('Input Mapping:') |
|
258
|
|
|
bits = ['NA', 'NA', 'Discrete', 'Relative', 'Absolute', 'NA', 'Supports Functional Mapping 2.0}', 'Supports NULL'] |
|
259
|
|
|
# First byte is bit-mask of input details |
|
260
|
|
|
mask = msg_bytes[0] |
|
261
|
|
|
maps = [ bits[i] for i in range(8) if (mask>>i) & 1] |
|
262
|
|
|
l.append(','.join(maps)) |
|
263
|
|
|
mode_info['input_mapping'] = maps |
|
264
|
|
|
|
|
265
|
|
|
l.append('Output Mapping:') |
|
266
|
|
|
mask = msg_bytes[1] |
|
267
|
|
|
maps = [ bits[i] for i in range(8) if (mask>>i)&1] |
|
268
|
|
|
l.append(','.join(maps)) |
|
269
|
|
|
mode_info['output_mapping'] = maps |
|
270
|
|
|
|
|
271
|
|
|
def _parse_symbol(self, msg_bytes, l, mode_info): |
|
272
|
|
|
l.append('Symbol:') |
|
273
|
|
|
symbol = ''.join( [chr(b) for b in msg_bytes if b!=0]) |
|
274
|
|
|
l.append(symbol) |
|
275
|
|
|
mode_info['symbol'] =symbol |
|
276
|
|
|
|
|
277
|
|
|
def _unpack_float(self, b): |
|
278
|
|
|
return struct.unpack('<f', bytearray(b[0:4])) |
|
279
|
|
|
|
|
280
|
|
|
def _parse_si_range(self, msg_bytes, l, mode_info): |
|
281
|
|
|
l.append('SI range:') |
|
282
|
|
|
mn = self._unpack_float(msg_bytes[0:4])[0] |
|
283
|
|
|
mx = self._unpack_float(msg_bytes[4:])[0] |
|
284
|
|
|
l.append(f'{mn} to {mx}') |
|
285
|
|
|
mode_info['si_range'] = (mn, mx) |
|
286
|
|
|
|
|
287
|
|
|
def _parse_pct_range(self, msg_bytes, l, mode_info): |
|
288
|
|
|
l.append('Pct range:') |
|
289
|
|
|
b_array = bytearray(msg_bytes) |
|
290
|
|
|
pct_min = struct.unpack('<f', b_array[0:4])[0] |
|
291
|
|
|
pct_max = struct.unpack('<f', b_array[4:])[0] |
|
292
|
|
|
l.append(f'{pct_min} to {pct_max}') |
|
293
|
|
|
mode_info['pct_range'] = (pct_min, pct_max) |
|
294
|
|
|
|
|
295
|
|
|
def _parse_raw_range(self, msg_bytes, l, mode_info): |
|
296
|
|
|
l.append('Raw range:') |
|
297
|
|
|
b_array = bytearray(msg_bytes) |
|
298
|
|
|
raw_min = struct.unpack('<f', b_array[0:4])[0] |
|
299
|
|
|
raw_max = struct.unpack('<f', b_array[4:])[0] |
|
300
|
|
|
l.append(f'{raw_min} to {raw_max}') |
|
301
|
|
|
mode_info['raw_range'] = (raw_min, raw_max) |
|
302
|
|
|
|
|
303
|
|
|
def _parse_name(self, msg_bytes, l, mode_info): |
|
304
|
|
|
l.append('Name:') |
|
305
|
|
|
name = ''.join( [chr(b) for b in msg_bytes if b!=0]) |
|
306
|
|
|
l.append(name) |
|
307
|
|
|
mode_info['name'] = name |
|
308
|
|
|
|
|
309
|
|
|
class AttachedIOMessage(Message): |
|
310
|
|
|
"""Peripheral attach and detach message |
|
311
|
|
|
""" |
|
312
|
|
|
msg_type = 0x04 |
|
313
|
|
|
|
|
314
|
|
|
def parse(self, msg_bytes, l, dispatcher): |
|
315
|
|
|
# 5-bytes = detached |
|
316
|
|
|
# 15 bytes = attached |
|
317
|
|
|
# 9 bytes = virtual attached |
|
318
|
|
|
# Subtract 3 bytes for what we've already popped off |
|
319
|
|
|
port = msg_bytes.pop(0) |
|
320
|
|
|
event = msg_bytes.pop(0) |
|
321
|
|
|
detach, attach, virtual_attach = [event==x for x in range(3)] |
|
322
|
|
|
if detach: |
|
323
|
|
|
l.append(f'Detached IO Port:{port}') |
|
324
|
|
|
return |
|
325
|
|
|
elif attach: |
|
326
|
|
|
l.append(f'Attached IO Port:{port}') |
|
327
|
|
|
elif virtual_attach: |
|
328
|
|
|
l.append(f'Attached VirtualIO Port:{port}') |
|
329
|
|
|
|
|
330
|
|
|
if attach or virtual_attach: |
|
331
|
|
|
# Next two bytes (little-endian) is the device number (MSB is not used) |
|
332
|
|
|
device_id = msg_bytes.pop(0) |
|
333
|
|
|
assert device_id in DEVICES, f'Unknown device with id {device_id} being attached (port {port}' |
|
334
|
|
|
device_name = DEVICES[device_id] |
|
335
|
|
|
self._add_port_info(dispatcher,port, 'id', device_id) |
|
336
|
|
|
self._add_port_info(dispatcher,port, 'name', device_name) |
|
337
|
|
|
|
|
338
|
|
|
msg_bytes.pop(0) # pop off MSB that's always 0 |
|
339
|
|
|
l.append(f'{device_name}') |
|
340
|
|
|
|
|
341
|
|
|
# register the handler for this IO |
|
342
|
|
|
dispatcher.message_attach_to_hub(device_name, port) |
|
343
|
|
|
|
|
344
|
|
|
if attach: |
|
345
|
|
|
for ver_type in ['HW', 'SW']: |
|
346
|
|
|
# NExt few bytes are fw versions |
|
347
|
|
|
build0 = hex(msg_bytes.pop(0)) |
|
348
|
|
|
build1 = hex(msg_bytes.pop(0)) |
|
349
|
|
|
bugfix = hex(msg_bytes.pop(0)) |
|
350
|
|
|
ver = hex(msg_bytes.pop(0)) |
|
351
|
|
|
l.append(f'{ver_type}:{ver}.{bugfix}.{build1}{build0}') |
|
352
|
|
|
|
|
353
|
|
|
if virtual_attach: |
|
354
|
|
|
assert len(msg_bytes) == 2 |
|
355
|
|
|
port0, port1 = msg_bytes |
|
356
|
|
|
l.append(f'Port A: {port0}, Port B: {port1}') |
|
357
|
|
|
self._add_port_info(dispatcher, port, 'virtual', (port0, port1)) |
|
358
|
|
|
|
|
359
|
|
|
def _add_port_info(self, dispatcher, port, info_key, info_val): |
|
360
|
|
|
port_info_item = dispatcher.port_info.get(port, {}) |
|
361
|
|
|
port_info_item[info_key] = info_val |
|
362
|
|
|
dispatcher.port_info[port] = port_info_item |
|
363
|
|
|
|
|
364
|
|
|
|
|
365
|
|
|
if __name__ == '__main__': |
|
366
|
|
|
from mock import MagicMock |
|
367
|
|
|
hub = MagicMock() |
|
368
|
|
|
dis = MessageDispatch(hub) |
|
|
|
|
|
|
369
|
|
|
dis.port_to_peripheral[1] = MagicMock() |
|
370
|
|
|
|
|
371
|
|
|
msg = bytearray([0,0,0x45,1,9,9,9]) |
|
372
|
|
|
l = dis.parse(msg) |
|
373
|
|
|
print(l) |
|
374
|
|
|
|
|
375
|
|
|
msg = bytearray([0,0,0x46,1,9,9,9]) |
|
376
|
|
|
l = dis.parse(msg) |
|
377
|
|
|
print(l) |
|
378
|
|
|
|