22
33import struct
44from 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
1012class 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
126132class 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
182199class 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
212251class 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