This is the C++ source code, partially generated by pyUBX/generateCpp.py
, located in https://github.com/mayeranalytics/pyUBX/tree/master/lang/cpp. Please see the top-level repo https://github.com/mayeranalytics/pyUBX for details.
The lang/cpp
subtree is also published under https://github.com/mayeranalytics/pyUBX-Cpp.
PyUBX is a small but functional Python3 wrapper for the u-blox M8 UBX protocol (see UBX-13003221 - R13, §31).
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
The pyUBX python classes in folder UBX/
are translated into C++ classes by generateCpp.py
. They can be found in lang/cpp/src
. Although C++11 has many useful features it is not supported by all vendors 1 so we rely on C++98 features only. We largely stick to the recommendations made in the excellent technical note The Inefficiency of C++, Fact or Fiction? by Anders Lundgren of IAR Ssytems (presentation slides are here, an updated version is here). In particular:
- Use templates carefully
- Don't use STL
- Don't use exceptions
Lundgren's closing remarks capture the fundamental issue with C++:
Perhaps even more than some other languages, the power and options available within C++ can lead to one implementer producing tight, “cheap” code, while leading another astray to produce a much more expensive result.
Along the same lines, in starker language, see Linus Torvald's comments about C++.
It is easiest to look at an example. This is the struct for the MON-VER
message:
// File lang/cpp/src/MON.h
// Auto-generated by generateCpp.py v0.1 on 2017-10-30T16:56:07.208773
#include <stdint.h>
#include "UBX.h" // defines iterator
struct MON
{
struct VER;
};
struct MON::VER
{
char swVersion[30];
char hwVersion[10];
struct Repeated {
char extension[30];
};
typedef _iterator<MON::VER::Repeated> iterator;
static _iterator<Repeated> iter(char*data, size_t size);
static _iterator<Repeated> iter(MON::VER& msg, size_t size);
};
The UBX types map directly onto C types. The only complication comes from the repeated blocks that give rise to variable length messages. We don't want to use vector
or other STL containers because they are not suited for microcontrollers, usually. Instead, iterators are used. Note that they do not have the STL compatiblebegin()
and end
functions.
The data structure corresponding to each UBX message is just the sequence of non-repeated fields packed into a struct. The repeated fields are accessed by an iterator.
The iterator is instantiated with a pointer to the data and the length of the data.
MON::VER::iterator i=MON::VER:iter(data, size);
The data needs to be allocated in advance (no range checking is performed!).
Data is accessed with the *
and ->
operators
char* ext = iter->extension;
Both NMEA and UBX messages have a layered structure, i.e. the payload is wrapped inside a control structure that includes a checksum and, for UBX, message iedntification and length. The C++ classes follow this nested structure. Both ParseNMEABase
and ParseUBXBase
run a small FSM.
NMEA parsing isn't really the main goal of this library, but for getting started and for debugging it's good to be able to parse at least some of the NMEA messages. The modular design allows slicing in of more complete NMEA parsing libraries, if necessary.
Class ParseNMEABase
iteratively parses each character and when a complete NMEA message is received and the checksum is validated the onNMEA
callback is called.
// parseNMEABase.h
class ParseNMEABase
{
public:
// Constructor.
ParseNMEABase(char* const buf, const size_t BUFLEN);
// Parse one new byte.
bool parse(uint8_t);
// NMEA callback. You must implement this in a derived class.
// buf is guaranteed to be a null-terminated string.
virtual void onNMEA(char buf[], size_t len) = 0;
// NMEA error callback
// Override this function if needed.
virtual void onNMEAerr() {};
};
Class ParseNMEA
then parses the NMEA payload and calls the appropriate callback such as onGGA
. ParseNMEA
and ParseNMEABase
are composed by inheriting from both and implementing the onNMEA
callback which then calls ParseNMEA.parse
.
// parseNMEA.h
class ParseNMEA
{
public:
// Parse one new byte.
void parse(char buf[], size_t max_len);
// GGA callback
virtual void onGGA(
uint32_t utc,
float lat,
float lon,
uint8_t qual,
uint8_t n_satellites,
float hdil,
float alt,
float height
);
virtual void onError(char buf[], size_t len) {};
};
Class ParseUBXBase
iteratively parses each character and when a complete UBX message is received and the checksum is validated the onUBX
callback is called.
// parseUBXBase.h
class ParseUBXBase
{
public:
// Constructor.
ParseUBXBase(char* const buf, const size_t BUFLEN);
/* Parse one byte */
bool parse(uint8_t);
// UBX callback
// buf is guaranteed to be a null-terminated string.
virtual void onUBX(uint8_t cls, uint8_t id, size_t len, char buf[]) = 0;
enum Error {BuflenExceeded, BadChksum, NotImplemented, Other};
// UBX error callback
// Override this function.
virtual void onUBXerr(uint8_t cls, uint8_t id, uint16_t len, Error err) {};
};
Class ParseUBX
parses the UBX payload and calls the appropriate callback such as onACK_NAK
.
ParseUBX
is implemented by the C++-generator generateCpp.py
. It automatically creates the
callbacks for each UBX message and has the correct (lengthy) switch statements in onUBX
.
// parseUBX.h
// auto-generated by generateCpp.py
class ParseUBX : public ParseUBXBase
{
public:
// constructor
ParseUBX(char* const buf, const size_t BUFLEN) : ParseUBXBase(buf, BUFLEN) {};
// callback for ACK::ACK_ messages
virtual void onACK_ACK_(ACK::ACK_& msg) {}
// callback for ACK::NAK messages
virtual void onACK_NAK(ACK::NAK& msg) {}
// etc...
private:
void onUBX(uint8_t cls, uint8_t id, size_t len, char buf[]);
};
The whole machinery is brought together in class Parse
(implemented in parse.h
).
The resulting structure looks like this:
graph TD;
ParseUBX-->Parse;
ParseUBXBase-->ParseUBX;
ParseNMEA-->Parse;
ParseNMEABase-->Parse;
Todo: Derive ParseNMEA from ParseNMEABase and Parse from ParseNMEA.
Class SerializeUBX
defined in serializeUBX.h
generates UBX messages.
class SerializeUBX
{
public:
/* Write one byte, you must implement this function in a derived class.
*/
virtual void writeByte(uint8_t byte) = 0;
/* Serialize message T from char*
*/
template<class T>
serialize(uint8_t* payload, uint16_t payload_len);
/* Serialize message T from T&
*/
template<class T>
void serialize(T& message, uint16_t payload_len=sizeof(T));
/* Serialize message T with zero length payload
*/
template<class T>
void serialize();
/* Serialize Get message (zero length payload message) corresponding to message T
*/
template<class T>
void serializeGet();
};
You must implement the writeByte
function in a derived class. The serialize
functions have to be called in different ways, depending on wheter the message has Repeated
fields or not.
Message has no repeated fields
CGF::PRT msg;
msg.portID=1; // UART
// etc
class MySerializer : public SerializeUBX {
void writeByte(uint8_t byte) {
// write to UART
}
};
MySerializer serializer;
serializer.serialize(msg);
Build googletest
cd googletest
cmake .
make -j
Footnotes
-
E.g. Texas Instrument's MSP430 compiler only supports C++03. ↩