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.
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
UBX
is a "u-blox proprietary protocol to communicate with a host computer". There are
- 9 message classes
UPD
MON
AID
TIM
ESF
MGA
LOG
SEC
HNR
, and - 155 individual messages, many of which have multiple versions
- there are different types of messages:
Command
Get
Set
Input
Output
Periodic
Poll Request
Polled
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.
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.
UBX class ID and message ID are defined by using member variables _class
and _id
.
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.
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.)
- class
ACK
:ACK
NAK
- 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
The two main classes are UBXManager
and UBXMessage
.
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
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'
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 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
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 are printed, including the ACK-ACK
.
usage: UBX.py [-h] [--VER-GET] [--GNSS-GET] [--PMS-GET] [--PM2-GET]
[--RATE-GET] [--RXM RXM] [--NMEA] [-d]
Send UBX commands to u-blox M8 device.
optional arguments:
-h, --help show this help message and exit
--VER-GET Get the version string
--GNSS-GET Get CFG-GNSS
--PMS-GET Get CFG-PMS
--PM2-GET Get CFG-PM2
--RATE-GET Get CFG-RATE
--RXM RXM Set the power mode (0=cont, 1=save)
--NMEA Dump NMEA messages.
-d, --debug Turn on debug mode
UBX.py
uses finite state machines defined in FSM.py
. The Manager
class derives from UBXManager
and overrides the onUBX
, etc., callbacks.
See Lang C++, also separately published as https://github.com/mayeranalytics/pyUBX-Cpp.
It is unusual to interface with a chip using Python. Usually this is done in C/C++ using a microcontroller, maybe an Arduino.
However, it is desirable to use a high-level language in the early stages of a project where it is key to quickly
- learn the interface/protocol,
- test new products (chips, modules, antenna configurations),
- cobble together prototypes or some mobile test-kit
The standard serial interfaces, such as UART, I2C, SPI, are easy to use with the appropriate USB-to-XYZ adapters.
The Python shell (or even better: IPython) provides a REPL that allows to interactively explore the protocol and the behaviour of the chip.
For field tests single board computers (SBCs) can be used. Some draw less than 200mA, so a 5Ah USB power bank is more than enough for a day's work. A default choice is the ubiquitous Rasperry Pi. The community is huge and libraries are plentiful. But with only one each of UART, SPI, I2C it's not necessarliy the best choice for prototyping. Some popular alternatives are
C.H.I.P: Very cheap, with on-board flash, and with slightly more I/O than the RaspberryNextThing Co. went bust in March 2018- Beaglebone Black: Well equipped with on-board flash, 2 x SPI, 2 x I2C, 4 x UART, etc., but not so cheap (around 50$).
- Pine64: Rather large and power hungry (300-800mA current draw), but cheap yet powerful with an Allwinner R18 quad-core A64 processor and generous I/O.
- UDOO Neo: i.MX 6SoloX-based with 3 x UART, 3 x I2C, but only 1 x SPI. The basic version is about 50$.
Although this has not been tested, a microcontroller running MicroPython
or CircuitPython should be capable of using this library with modifications.
(In particular, CircuitPython does not support threading, which is used by UBXManager
.)
For testing we used:
- SBC: Raspberry Pi
- GNSS: CAM-M8Q module on carrier board (80x40mm ground plane), connected via UART
- Power consumption measurement: INA219 module, connected via I2C
- Manipulators/accessors
Also have a look at the logged enhancement requests on github.
The UBX protocol takes up about 220 pages of the Receiver Description, so is rather extensive and it would have been nice to rely on prior work. Almost all libraries we could find are hand-crafted C libraries. It's hard to imagine that this manual approach resulted in bug-free code.
Kaitai is a parser generator for binary structures. It looks very promising and would have been a great fit. Unfortunately, Kaitai can only create parsers but not generators (serializers). We'll look again at Kaitai when this limitation is lifted. Since the pyUBX
message definitions are written in Python it shouldn't be difficult to quickly generate the necessay .ksy
yaml at a later stage.
This quite comprehensive C++11 library is available on github. It relies on the Qt framework - not an option for most microcontrollers.
U-blox own library called libMGA can be obtained, according to the forum, by contacting u-blox.
The pyUBX
software is GPL 3.0 licensed. The software is provided "as-is". Use it carefully or you might brick your device!!!
U-blox' documentation has a very peculiar copyright note that strictly prohibits the use of the documents without the express permission of u-blox. U-blox reacted quickly when confronted with it on the forum. It's probably safe to dismiss parts of this disclaimer as unreasonably broad.
This is the full text of the disclaimer:
u-blox reserves all rights to this document and the information contained herein. Products, names, logos and designs described herein mayin whole or in part be subject to intellectual property rights. Reproduction, use, modification or disclosure to third parties of this document or any part thereof without the express permission of u-blox is strictly prohibited.
The information contained herein is provided “as is” and u-blox assumes no liability for the use of the information. No warranty, either express or implied, is given, including but not limited, with respect to the accuracy, correctness, reliability and fitness for a particularpurpose of the information. This document may be revised by u-blox at any time. For most recent documents, please visit www.u-blox.com.
Copyright © 2017, u-blox AG.
u-blox® is a registered trademark of u-blox Holding AG in the EU and other countries. ARM® is the registered trademark of ARM Limitedin the EU and other countries.