Skip to content

Commit 55a29a6

Browse files
committed
Added support for packing and sending messages. Also fixed a few typing errors, links and typos.
1 parent 0bec30d commit 55a29a6

File tree

5 files changed

+337
-37
lines changed

5 files changed

+337
-37
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Install the package with pip<br>
2424
Import the core module<br>
2525
`from ubxtranslator import core`
2626

27-
If the message class you want has already been defined simply import it.
27+
If the message class you want has already been defined, just import it.
2828
If not you will need to construct the messages and classes yourself, see the examples for more information.<br>
2929
`from ubxtranslator import predefined`
3030

@@ -39,16 +39,17 @@ parser = core.Parser([
3939
Then you can use the parser to decode messages from any byte stream.<br>
4040
`cls_name, msg_name, payload = parser.receive_from(port)`
4141

42-
The result is a tuple which can be unpacked as shown above.<br>
43-
The variables `cls_name` and `msg_name` are strings, ie. `'NAV'`, `'PVT'`.<br>
42+
The result is a tuple that can be unpacked as shown above.<br>
43+
The variables `cls_name` and `msg_name` are strings, i.e. `'NAV'`, `'PVT'`.<br>
4444

4545
The payload is the namedtuple of the message and can be accessed like an object. The attributes share the names of the fields.<br>
4646
`print(cls_name, msg_name, payload.lat, payload.lng)`
4747

4848
Bitfields are also returned as namedtuples and can be accessed the same way.<br>
4949
`print(payload.flags.channel)`
5050

51-
Repeated Blocks are returned as a list of blocks, the fields within each block are also named tuples. All of the repeated blocks in the predefined messages are name `RB`.<br>
51+
Repeated Blocks are returned as a list of blocks, the fields within each block are also named tuples.
52+
All of the repeated blocks in the predefined messages are name `RB`.<br>
5253
```
5354
for i in range(len(payload.RB)):
5455
print(payload.RB[i].gnssId, payload.RB[i].flags.health)
@@ -64,6 +65,5 @@ For full examples see the examples directory.
6465
Want to contribute? Please feel free to submit issues or pull requests.
6566
Nothing in this package is very complicated, please have a crack and help me to improve this.
6667

67-
- Add the ability to pack messages into packets for two way communications
6868
- Add more and better tests
6969
- Add Field type RU1_3

ubxtranslator/core.py

Lines changed: 169 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import struct
44
from collections import namedtuple
5-
from typing import List, Iterator, Union
5+
from typing import List, Iterator, Union, Tuple, Any
6+
7+
__all__ = ['PadByte', 'Field', 'Flag', 'BitField', 'RepeatedBlock', 'Message', 'Cls', 'Parser']
8+
69

7-
__all__ = ['PadByte', 'Field', 'Flag', 'BitField', 'RepeatedBlock', 'Message', 'Cls', 'Parser', ]
810

911

1012
class PadByte:
@@ -39,7 +41,7 @@ class Field:
3941
"""A field type that is used to describe most `normal` fields.
4042
4143
The descriptor code is as per the uBlox data sheet available;
42-
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_%28UBX-13003221%29_Public.pdf
44+
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_UBX-13003221.pdf
4345
4446
Field types that are variable length are not supported at this stage.
4547
@@ -118,16 +120,20 @@ def __init__(self, name: str, start: int, stop: int):
118120
for i in range(start, stop):
119121
self._mask |= 0x01 << i
120122

121-
def parse(self, value) -> tuple:
123+
def parse(self, value) -> Tuple[str, int,]:
122124
"""Return a tuple representing the provided value"""
123125
return self.name, (value & self._mask) >> self._start
124126

127+
def pack(self, value: int) -> int:
128+
"""Return the shifted value to be ORed into the bitfield"""
129+
return (value << self._start) & self._mask
130+
125131

126132
class BitField:
127133
"""A bit field type made up of flags.
128134
129135
The bit field uses the types described within the uBlox data sheet:
130-
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_%28UBX-13003221%29_Public.pdf
136+
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_UBX-13003221.pdf
131137
132138
Flags should be passed within the constructor and should not be added after
133139
the class has been created. The constructor does check whether the flag field
@@ -165,19 +171,30 @@ def __init__(self, name: str, type_: str, subfields: List[Flag]):
165171
self._nt = namedtuple(self.name, [f.name for f in self._subfields])
166172

167173
@property
168-
def repeated_block(self):
174+
def repeated_block(self) -> bool:
169175
return False
170176

171177
@property
172178
def fmt(self) -> str:
173179
"""Return the format char for use with the struct package"""
174180
return BitField.__types__[self._type]
175181

176-
def parse(self, it: Iterator) -> namedtuple:
182+
def parse(self, it: Iterator) -> Tuple[str, Any]:
177183
"""Return a named tuple representing the provided value"""
178184
value = next(it)
179185
return self.name, self._nt(**{k: v for k, v in [x.parse(value) for x in self._subfields]})
180186

187+
def pack(self, values: Any) -> int:
188+
"""Return the combined integer value of all flags"""
189+
res = 0
190+
if isinstance(values, dict):
191+
for sf in self._subfields:
192+
res |= sf.pack(values.get(sf.name, 0))
193+
else:
194+
for sf in self._subfields:
195+
res |= sf.pack(getattr(values, sf.name, 0))
196+
return res
197+
181198

182199
class RepeatedBlock:
183200
"""Defines a repeated block of Fields within a UBX Message
@@ -192,28 +209,50 @@ def __init__(self, name: str, fields: List[Union[Field, BitField, PadByte]]):
192209
self._nt = namedtuple(self.name, [f.name for f in self._fields if hasattr(f, 'name')])
193210

194211
@property
195-
def repeated_block(self):
212+
def repeated_block(self) -> bool:
196213
return True
197214

198215
@property
199-
def fmt(self):
216+
def fmt(self) -> str:
200217
"""Return the format string for use with the struct package."""
201218
return ''.join([field.fmt for field in self._fields]) * (self.repeat + 1)
202219

203-
def parse(self, it: Iterator) -> tuple:
220+
def parse(self, it: Iterator) -> Tuple[str, Any]:
204221
"""Return a tuple representing the provided value/s"""
205222
resp = []
206223
for i in range(self.repeat + 1):
207224
resp.append(self._nt(**{k: v for k, v in [f.parse(it) for f in self._fields] if k is not None}))
208225

209226
return self.name, resp
210227

228+
def pack(self, values: List[Any]) -> List[Any]:
229+
"""Flatten values of repeated block items into a list for struct.pack"""
230+
res = []
231+
for item in values:
232+
if isinstance(item, dict):
233+
for f in self._fields:
234+
if isinstance(f, BitField):
235+
res.append(f.pack(item.get(f.name, {})))
236+
elif isinstance(f, Field):
237+
res.append(item.get(f.name, 0 if f._type != 'C' else b'\x00'))
238+
elif isinstance(f, PadByte):
239+
pass
240+
else:
241+
for f in self._fields:
242+
if isinstance(f, BitField):
243+
res.append(f.pack(getattr(item, f.name, {})))
244+
elif isinstance(f, Field):
245+
res.append(getattr(item, f.name, 0 if f._type != 'C' else b'\x00'))
246+
elif isinstance(f, PadByte):
247+
pass
248+
return res
249+
211250

212251
class Message:
213252
"""Defines a UBX message.
214253
215254
The Messages are described in the data sheet:
216-
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_%28UBX-13003221%29_Public.pdf
255+
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_UBX-13003221.pdf
217256
218257
The supplied name should be upper case. eg. PVT
219258
@@ -247,16 +286,16 @@ def __init__(self, id_: int, name: str, fields: list):
247286
self._repeated_block = field
248287

249288
@property
250-
def id_(self):
289+
def id_(self) -> int:
251290
"""Public read only access to the message id"""
252291
return self._id
253292

254293
@property
255-
def fmt(self):
294+
def fmt(self) -> str:
256295
"""Return the format string for use with the struct package."""
257296
return ''.join([field.fmt for field in self._fields])
258297

259-
def parse(self, payload: bytes) -> namedtuple:
298+
def parse(self, payload: bytes) -> Tuple[str, Any]:
260299
"""Return a named tuple parsed from the provided payload.
261300
262301
If the provided payload is not the same length as what is implied by the format string
@@ -269,6 +308,40 @@ def parse(self, payload: bytes) -> namedtuple:
269308

270309
return self.name, self._nt(**{k: v for k, v in [f.parse(it) for f in self._fields] if k is not None})
271310

311+
def pack(self, values: Any) -> bytes:
312+
"""Return the bytes of the payload for this message from provided values."""
313+
flat_values = []
314+
if self._repeated_block:
315+
if isinstance(values, dict):
316+
repeated_list = values.get(self._repeated_block.name, [])
317+
else:
318+
repeated_list = getattr(values, self._repeated_block.name, [])
319+
if not repeated_list:
320+
raise ValueError("Repeated block {} cannot be empty".format(self._repeated_block.name))
321+
self._repeated_block.repeat = len(repeated_list) - 1
322+
323+
for f in self._fields:
324+
if isinstance(f, (BitField, RepeatedBlock)):
325+
if isinstance(values, dict):
326+
val = values.get(f.name)
327+
else:
328+
val = getattr(values, f.name)
329+
if isinstance(f, BitField):
330+
flat_values.append(f.pack(val if val is not None else {}))
331+
else:
332+
flat_values.extend(f.pack(val if val is not None else []))
333+
elif isinstance(f, Field):
334+
if isinstance(values, dict):
335+
val = values.get(f.name)
336+
else:
337+
val = getattr(values, f.name)
338+
flat_values.append(val if val is not None else (0 if f._type != 'C' else b'\x00'))
339+
elif isinstance(f, PadByte):
340+
pass
341+
# PadByte doesn't take values
342+
343+
return struct.pack(self.fmt, *flat_values)
344+
272345
def check_payload_length(self, payload_len: int):
273346
"""Check whether payload_len is a valid length for this type of message.
274347
@@ -304,7 +377,7 @@ class Cls:
304377
"""Defines a UBX message class.
305378
306379
The Classes are described in the data sheet:
307-
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_%28UBX-13003221%29_Public.pdf
380+
https://www.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_UBX-13003221.pdf
308381
309382
The messages within the class can be provided via the constructor or via the `register_msg` method.
310383
@@ -328,7 +401,7 @@ def __init__(self, id_: int, name: str, messages: List[Message]):
328401
self._messages[msg.id_] = msg
329402

330403
@property
331-
def id_(self):
404+
def id_(self) -> int:
332405
"""Public read only access to the class id"""
333406
return self._id
334407

@@ -348,7 +421,7 @@ def register_msg(self, msg: Message):
348421
# noinspection PyProtectedMember
349422
self._messages[msg._id] = msg
350423

351-
def parse(self, msg_id, payload):
424+
def parse(self, msg_id, payload) -> Tuple[str, str, Any]:
352425
"""Return a named tuple parsed from the provided payload.
353426
354427
If the provided payload is not the same length as what is implied by the format string
@@ -377,7 +450,7 @@ class Parser:
377450
UBX packets from the device. The typical way to do this would be a serial package like pyserial, see the
378451
examples file.
379452
380-
# TODO add info about the message sending methods.
453+
The parser now also includes methods to pack messages into packets for two-way communications.
381454
"""
382455
PREFIX = bytes((0xB5, 0x62))
383456

@@ -392,10 +465,82 @@ def register_cls(self, cls: Cls):
392465
"""Register a message class."""
393466
self.classes[cls.id_] = cls
394467

395-
def receive_from(self, stream) -> namedtuple:
468+
def get_cls_by_name(self, name: str) -> Cls:
469+
"""Find a registered class by name."""
470+
for cls in self.classes.values():
471+
if cls.name == name:
472+
return cls
473+
raise ValueError("Unknown class name {}".format(name))
474+
475+
def get_msg_by_name(self, cls: Cls, msg_name: str) -> Message:
476+
"""Find a message by name within a class."""
477+
for msg in cls._messages.values():
478+
if msg.name == msg_name:
479+
return msg
480+
raise ValueError("Unknown message name {} in class {}".format(msg_name, cls.name))
481+
482+
def prepare_msg(self, cls_name: str, msg_name: str) -> dict:
483+
"""Prepare a mutable structure prefilled with defaults for packing."""
484+
cls = self.get_cls_by_name(cls_name)
485+
msg = self.get_msg_by_name(cls, msg_name)
486+
487+
def get_defaults(fields):
488+
d = {}
489+
for f in fields:
490+
if isinstance(f, BitField):
491+
d[f.name] = {sf.name: 0 for sf in f._subfields}
492+
elif isinstance(f, RepeatedBlock):
493+
d[f.name] = [get_defaults(f._fields)]
494+
elif isinstance(f, Field):
495+
d[f.name] = 0 if f._type != 'C' else b'\x00'
496+
return d
497+
498+
res = get_defaults(msg._fields)
499+
# Include resolved IDs and names for later use in transfer_to
500+
res['_cls_id'] = cls.id_
501+
res['_msg_id'] = msg.id_
502+
return res
503+
504+
def _pack_for_transfer(self, msg_dict: dict) -> bytes:
505+
"""Build a UBX packet from a message dictionary.
506+
Raise ValueError in case of errors due to an invalid message dictionary."""
507+
try:
508+
cls_id = msg_dict['_cls_id']
509+
except KeyError:
510+
raise ValueError("Message dictionary must contain a `_cls_id` key, did you forget to call prepare_msg?")
511+
try:
512+
msg_id = msg_dict['_msg_id']
513+
except KeyError:
514+
raise ValueError("Message dictionary must contain a `_msg_id` key, did you forget to call prepare_msg?")
515+
516+
cls = self.classes[cls_id]
517+
msg_obj = cls[msg_id]
518+
payload = msg_obj.pack(msg_dict)
519+
520+
header = struct.pack('BBH', cls_id, msg_id, len(payload))
521+
packet = self.PREFIX + header + payload
522+
ck_a, ck_b = self._generate_fletcher_checksum(header + payload)
523+
packet += struct.pack('BB', ck_a, ck_b)
524+
return packet
525+
526+
def transfer_to(self, msg_dict: dict, stream):
527+
"""Build and write a UBX packet to a stream from a message dictionary."""
528+
packet = self._pack_for_transfer(msg_dict)
529+
stream.write(packet)
530+
if hasattr(stream, 'flush'):
531+
stream.flush()
532+
533+
async def transfer_to_async(self, msg_dict: dict, stream):
534+
"""Async version of transfer_to."""
535+
packet = self._pack_for_transfer(msg_dict)
536+
stream.write(packet)
537+
if hasattr(stream, 'drain'):
538+
await stream.drain()
539+
540+
def receive_from(self, stream) -> Tuple[str, str, Any]:
396541
"""Receive a message from a stream and return as a namedtuple.
397-
raise IOError in case of errors due to insufficient data.
398-
raise ValueError in case of errors due to sufficient but invalid data.
542+
Raise IOError in case of errors due to insufficient data.
543+
Raise ValueError in case of errors due to sufficient but invalid data.
399544
"""
400545
while True:
401546
# Search for the prefix
@@ -441,7 +586,7 @@ def receive_from(self, stream) -> namedtuple:
441586

442587
return self.classes[msg_cls].parse(msg_id, buff[4:])
443588

444-
async def receive_from_async(self, stream) -> namedtuple:
589+
async def receive_from_async(self, stream) -> Tuple[str, str, Any]:
445590
"""Async version of receive_from."""
446591
while True:
447592
# Search for the prefix
@@ -480,7 +625,7 @@ async def receive_from_async(self, stream) -> namedtuple:
480625
return self.classes[msg_cls].parse(msg_id, payload)
481626

482627
@staticmethod
483-
def _read_until(stream, terminator: bytes, size=None):
628+
def _read_until(stream, terminator: bytes, size=None) -> bytes:
484629
"""Read from the stream until the terminator byte/s are read.
485630
Return the bytes read including the termination bytes.
486631
"""
@@ -500,7 +645,7 @@ def _read_until(stream, terminator: bytes, size=None):
500645
return bytes(line)
501646

502647
@staticmethod
503-
async def _read_until_async(stream, terminator: bytes, size=None):
648+
async def _read_until_async(stream, terminator: bytes, size=None) -> bytes:
504649
"""Async version of Parser._read_until."""
505650
term_len = len(terminator)
506651
line = bytearray()
@@ -532,10 +677,3 @@ def _generate_fletcher_checksum(payload: bytes) -> bytes:
532677

533678
return bytes((check_a, check_b))
534679

535-
def prepare_msg(self, cls_name, msg_name):
536-
# TODO find the class and message and return a blank message ready for filling
537-
raise NotImplementedError('Sorry this has not been implemented yet!')
538-
539-
def transfer_to(self, msg, stream):
540-
# TODO pack the message into bytes and transfer to the byte stream
541-
raise NotImplementedError('Sorry this has not been implemented yet!')

0 commit comments

Comments
 (0)