/pyUBX-Cpp

C++ Parser/Generator for UBX protocol messages - generated by pyUBX

Primary LanguageC++

pyUBX/lang/cpp

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.

Overview

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++

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++.

Message structs

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;

Class Structure

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

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) {};
};

UBX Parsing

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;
Loading

Todo: Derive ParseNMEA from ParseNMEABase and Parse from ParseNMEA.

Serialization

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);

Testing

Googletest

Build googletest

cd googletest
cmake .
make -j

Footnotes

  1. E.g. Texas Instrument's MSP430 compiler only supports C++03.