PyUBX
Python library for parsing/generating u-blox UBX protocol messages, and for creating parsers/generators in other languages.
Install / Use
/learn @mayeranalytics/PyUBXREADME
pyUBX
This is a small but functional Python3 wrapper for the u-blox M8 UBX protocol, as defined in UBX-13003221 - R13, §31 and the u-blox F9 protocol as defined in UBX-18010854.
The focus is on getting the basics right, which first of all means correctly creating and parsing UBX messages with usable error messages on failure. The key features are:
- parse, generate and manipulate UBX messages
- message definitions are simple, uncluttered Python code (class definitions)
- decorators keep the boilerplate code at a minimum
- interact with a device using a REPL
- use as a parser generator for other languages or definition files for other parser generators, implemented are:
- C++, separately published as https://github.com/mayeranalytics/pyUBX-Cpp
Note: Currently only a subset of all UBX messages is implemented. See the progress status below.
Cloning
The python module, ubx, is installed with pip from the top level directory.
The C++ parser/generator depends on googletest. If you want to run the C++ tests then check out the repo using the —recursive option and build googletest:
# clone into ./pyUBX
git clone --recursive https://github.com/mayeranalytics/pyUBX.git
cd pyUBX
# Install library for use in python
pip install .
# build googletest
pushd lang/cpp/test/googletest
cmake .
make -j
popd
# run tests
pushd lang/cpp
make test
popd
The UBX protocol
UBX is a "u-blox proprietary protocol to communicate with a host computer". There are
- 9 message classes
UPDMONAIDTIMESFMGALOGSECHNR, and - 155 individual messages, many of which have multiple versions
- there are different types of messages:
CommandGetSetInputOutputPeriodicPoll RequestPolled
UBX Message Hierarchy
Messages are grouped in so-called classes. Each class is identified by a class ID. Within each class each message is identified by a message ID. In the message definitions in Section 39 of the documentation class IDs are titled Class and message IDs are, confusingly, titled ID. Here we stick to class ID and message ID.
Message definitions
For example, the ACK-ACK and ACK-NAK message format is defined in Python like this.
@initMessageClass
class ACK:
"""Message class ACK."""
_class = 0x05
class ACK:
_id = 0x01
class Fields:
clsID = U1(1) # Class ID of the Acknowledged Message
msgID = U1(2) # Message ID of the Acknowledged Message
class NAK:
_id = 0x00
class Fields:
clsID = U1(1) # Class ID of the Not-Acknowledged Message
msgID = U1(2) # Message ID of the Not-Acknowledged Message
The class structure mirrors the UBX message hierarchy. Python classes UBX.ACK, UBX.CGF, UBX.MON correspond to the respective messages classes. Python classes UBX.ACK.ACK, UBX.ACK.NAK, etc., correspond to the respective messages.
This design introduces some syntactic noise such as the frequent class keyword and abundant use of decorators. It is an acceptable tradeoff: As it is correct Python it can be used to parse and manipulate messages. For example, by introspection it becomes possible to use these UBX message definitions to generate parsers and generators for other languages, such as C/C++ (see below). The decorators add the boilerplate and keep the syntax as simple as possible.
Python class structure
Class ID and message ID
UBX class ID and message ID are defined by using member variables _class and _id.
Fields
Note that the Fields class variables have to be numbered, otherwise the exact order of the variables cannot be recovered (Python stores the various `things' belonging to a class in a dict). So the first argument of a type is always an ordering number. The actual numbers don't matter as long as the resulting ordering is correct.
Repeated blocks
UBX often uses repeated blocks. An example is the MON-VER message:
@initMessageClass
class MON:
"""Message class MON."""
_class = 0x0A
@addGet
class VER:
_id = 0x04
class Fields:
swVersion = CH(1, 30, nullTerminatedString=True)
hwVersion = CH(2, 10, nullTerminatedString=True)
class Repeated:
extension = CH(1, 10, nullTerminatedString=True)
Here, swVersion and hwVersion are fixed-length bytestrings that contain a null-terminated ASCII string. The repeated extension fields carry additional information.
When UBXManager receives a message from the GNSS receiver it tries to parse it. UBX messages are handled by the onUBX and onUBXError member functions. Here are the signatures of these two functions:
def onUBX(self, obj) # handle good UBX message
def onUBXError(self, msgClass, msgId, errMsg) # handle faulty UBX message
The argument obj contains a UBXMessage object with populated fields. A UBXMessage can be pretty-printed with the __str__ function. A repeated block is unrolled by appending _n to the variable names of the fields inside the repeated block. For example, the pretty-printed answer to UBX.MON.VER.Get is:
MON-VER
swVersion="ROM CORE 3.01 (107888)"
hwVersion="00080000"
extension_1="FWVER=SPG 3.01"
extension_2="PROTVER=18.00"
extension_3="GPS;GLO;GAL;BDS"
extension_4="SBAS;IMES;QZSS"
(This is from a CAM-M8Q module.)
Progress status
- class
ACK:ACKNAK
- class
CFG:GNSS: GNSS system channel sharing configurationPMS: Power mode setupPM2: Extended power management configurationPRT: Port configurationRATE: Navigation/Measurement rate settingsRXM: RXM configurationTP5: Time Pulse Parameters
- class
MON:VER: Receiver/Software VersionHW: Hardware Status
- class
NAV PVT: Position Velocity TimeRELPOSNED: Relative position, as used for differential GPS. Version 1 (uBlox-9) is implemented, which is incompatible with Version 0 (uBlox-8).DOP: Dilution of precisionSVINFO:- class
**ESF** MEAS
Usage
The two main classes are UBXManager and UBXMessage.
UBXManager
UBXManager runs a thread that reads and writes to/from a pyserial device. It is assumed that a u-blox M8 GNSS module is connected to the serial port.
import serial
from ubx import UBXManager
ser = serial.Serial('/dev/ttyAMA0', 9600, timeout=None)
manager = UBXManager(ser, debug=True)
The manager can be instantiated with any serial object that has a read(n) function that reads n bytes from the stream. Nothing more is required (in fact all it needs is read(1)).
If a file is used as the data source, it should be opened as binary.
An eofTimeout argument specifies how long the manager waits for more data after reaching the
end of the file. (Use None to wait indefinitely, use 0 to return when the end-of-file is reached.)
import serial
from ubx import UBXManager
infile = serial.Serial('testfile.dat', 'rb')
manager = UBXManager(infile, debug=True, eofTimeout=0)
The manager thread is then started like this:
manager.start()
By default UBXManager dumps all NMEA and UBX messages to stdout. By deriving and overriding the member functions onNMEA, onNMEAError, onUBX, onUBXError this behaviour can be changed.
An example is given as UBXQueue, where onUBX simply enqueues the data, allowing it to be read from a different thread.
UBXMessage
UBXMessage parses and generates UBX messages. The UBXMessage classes are organized in a hierarchy so that they can be accessed with a syntax that resembles u-blox' convention. For example, message CFG-PSM corresponds to Python class UBX.CFG.PSM and its subclasses.
The subclasses capture the message format variations that are used for requesting and receiving. So, the Get message of CFG-GNSS is
> UBX.CFG.PMS.Get().serialize()
b'\xb5b\n\x04\x00\x00\x0e4'
Get-modify-set
A typical usage pattern is get-modify-set:
rxm = UBX.CFG.RXM(b'\x48\x00') # create a message
rxm.lpMode = 1 # power save mode (see §31.11.27)
msg = rxm.serialize() # make new message
# send(msg)
Types
Types are defined in Types.h. Currently there are the following:
U1, I1, X1, U2, I2, X2, U4, I4, X4, R4, R8, CH, U
and they correspond exactly to the ones defined by u-blox in §31.3.5.
Simple types are defined like this:
@_InitType
class I4:
"""UBX Signed Int."""
fmt = "i" # used by the decorator _InitType
def ctype(): return "int32_t" # for future use
The decorator @_InitType does most of the work: It implements the __init__, __parse__and toString functions and adds the _size variable. The _InitType decorator needs the fmt class variable to be defined, the letter corresponds to the code used in the Python struct module.
CH and U are variable-length types and they are hand-coded. U is used for the many reserved fields.
UBX.py
UBX.py is a utlilty that allows to send UBX commands to the device. For example, to switch into power save mode and then start dumping NMEA messages, run
./UBX.py --RXM 1 --NMEA
The content of the CFG-RATE register can queried like so:
> ./UBX.py --RATE-GET
CFG-RATE:
measRate=0x03E8
navRate=0x0001
timeRef=0x0001
ACK-ACK:
clsID=0x06
msgID=0x08
Note that always all UBX messages ar
