I wrote an OO driver for Teseo-LIV3 GPS module (as used in shabaz ' GPS / Galileo / BeiDou / GLONASS receiver). It knows how to retrieve info from the GPS, in NMEA format. In this series, I'm designing an OO lib to parse the payloads and return the data as objects.
There are 10s of NMEA parsers available on GitHub. They will work with the Teseo lib just fine, as long as they can parse NMEA payloads. I'm writing a fresh one to flex the C++ muscles. An excuse to try out OO design in an embedded context.
Objectives for the NMEA parser
The library builds an object representation, based on data from NMEA replies. For the first posts, I selected a few of the ones that are most used, and that pose different scenarios:
- NMEA replies that contain a single list of values, like GLL, GGA and RMC. I used these two, to test out common things across different reply types.
- NMEA replies that repeat some values, like GSV and GSA. In a GSV reply, attribute values for up to 4 satellites are listed. GSA lists the PRN for up to 12 satellites.
Each object will parse a single NMEA reply line. For commands that return multiple reply lines, such as GSV, you will use one object for each of those reply lines.
The library is a line parser, not a NMEA full conversation parser. It expects that you know what type of reply you are dealing with. But it can be the basis of a full conversation parser, if you 'd need that. The first token in every reply is the message type, so that exercise would be trivial.
It's also not smarter than a parser. It presents objects with NMEA data. And tries to cast each value to a meaningful type for use in algorithms. But no higher abstraction than that.
Overall, you 'll find these types of classes in the library:
- objects that hold values. They may represent the attributes of a reply, or the attributes of a particular satellite,
- utility class that defines common types, and knows how to do common activities, e.g. date, time, and coordinates parsing,
- typecasts to bind C++ STL containers to a particular content type (e.g. a fixed size array to hold satellite objects).
- objects to hold timestamps (up to ms) and dates. Most likely to be replaced later by something more standard
example code:
reply = "$GPGLL,5051.83778,N,00422.55809,S,185427.150,V,N*4F"; nmea::gll o; nmea::gll::from_data(reply, o); std::cout << "source: " << o.source << ". " << "lat: " << o.lat << " lon: " << o.lon << ". " << o.t << ". " << "valid: " << o.valid << ". " << std::endl;
In the real world, the reply string would be retrieved from a GPS API, such as the teseo lib I referred to above.
For modern C++ fans, I'll use the time and date types from std::chrono. And some string manipulation constructs. The one I use the most is a std::string_view. It can represent part of a string, without copying the string data. Example uses:
- NMEA replies are a comma separated string of values. But they have a checksum at the end, that is not using the same character separation schema. In places where it simplifies further processing, I use a std::string_view to represent the full string, without the checksum.
- I use std::views::split() to tokenise the NMEA replies into their individual parts. Each part is represented by a std::string_view. again without copying the string data, but by maintaining pointers that you can iterate over.
Your toolchain must support - and needs to be set minimal to - the C++ standard -std=c++23. For the GCC family, version must be v11 or higher.
Here's how you can get a compatible toolchain for the Raspberry Pico: Raspberry Pico C/C++ SDK - Set up C++23 capable toolchain .
The code excerpt below shows both substring and tokenise in action:
// $GPGSV,3,1,11,13,79,310,,14,53,113,,05,51,214,,30,47,067,*72 bool gsv::from_data(const std::string& data, gsv& gsv) { unsigned int field = 0; std::string_view v = std::string_view(data).substr(0, data.find('*')); for (const auto word : std::views::split(v, delim)) { switch (field) { case 0: // talker id gsv.source = nmea::get_talker_id(std::string_view(word)); break; case // ...
Objects to parse and hold a simple reply
Let's start with the simplest scenario: NMEA replies that just hold a predefined set of values. For example:
GLL: $GPGLL,5051.83778,N,00422.55809,S,185427.150,V,N*4F
RMC: $GPRMC,185427.150,V,5051.83778,N,00422.55809,E,,,240724,,,N*7F
For this pattern, I built classes that can parse the line, and hold the attributes.
This initial version has a gll and rmc class. Both have as purpose to hold the data of the reply to the NMEA command with the same name.
class gll
class gll { public: static bool from_data(const std::string& data, gll& gll); nmea::talker_id source; float lat; float lon; time_t t; bool valid; };
class rmc
class rmc { public: static bool from_data(const std::string& data, rmc& rmc); nmea::talker_id source; float lat; float lon; float speed; time_t t; std::chrono::year_month_day d; bool valid; };
typical use
std::string reply = "$GPGLL,5051.83778,N,00422.55809,S,185427.150,V,N*4F"; nmea::gll o; nmea::gll::from_data(reply, o); std::cout << "GLL " << std::endl << "source: " << o.source << ". " << "lat: " << o.lat << " lon: " << o.lon << ". " << o.t << ". " << "valid: " << o.valid << ". " << std::endl; return; // ... std::string reply = "$GPRMC,185427.150,V,5051.83778,N,00422.55809,E,,,240724,,,N*7F"; nmea::rmc o; nmea::rmc::from_data(reply, o); std::cout << "RMC " << std::endl << "source: " << o.source << ". " << "lat: " << o.lat << " lon: " << o.lon << ". " << o.t.get_h() << ":" << o.t.get_m() << ":" << o.t.get_s() << ":" << o.t.get_msec() << ". " << o.d.day() << ":" << o.d.month() << ":" << o.d.year() << ". " << "valid: " << o.valid << ". " << std::endl; return;
Results:
GLL
source: 0. lat: 50.864 lon: 7.04263. 18:54:27.150. valid: 0.
RMC
source: 0. lat: 50.864 lon: 7.04263. 18:54:27.150. 2024-07-24. valid: 0.
The from_data() methods are static. You pass the object you want to fill as a parameter.
Comment below if you 'd implement this as a non-static method. Or if you would have designed a superclass for all reply handlers.
Just for insight, here is the implementation of the from_data() parser for the gll reply type:
// $GPGLL,<Lat>,<N/S>,<Long>,<E/W>,<Timestamp>,<Status>,<mode indicator>*<checksum><cr><lf> bool gll::from_data(const std::string& data, gll& gll) { unsigned int field = 0; for (const auto word : std::views::split(data, delim)) { switch (field) { case 0: // talker id gll.source = nmea::get_talker_id(std::string_view(word)); break; case 1: // latitude gll.lat = nmea::get_coord(std::string_view(word)); break; case 2: // latitude direction if (nmea::get_dir(std::string_view(word)) == nmea::s) { gll.lat = gll.lat * -1; } break; case 3: // longitude gll.lon = nmea::get_coord(std::string_view(word)); break; case 4: // longitude direction if (nmea::get_dir(std::string_view(word)) == nmea::w) { gll.lon = gll.lon * -1; } break; case 5: // timestamp nmea::get_time(std::string_view(word), gll.t); break; case 6: // valid gll.valid = nmea::get_valid(std::string_view(word)); break; default: // skip 7 break; } field++; } return (field == 8); // everything parsed }
It uses some methods and types that will be discussed in the next blogs. The flow and approach isn't that hard to follow, though.
Next posts
In the follow up blogs, I'll continue with the somewhat more involved parsing scenarios:
- Objects to parse and hold a reply with repeating info
- Utility classes and data type helpers
Here is the UML preview of the helper and utility classes. It may help to understand some of the function calls that I haven't explained in this post:
edit: the date and time_ms class are removed. I refactored to use std::chrono::year_month_day. and std::chrono::hh_mm_ss.
ST also provides a good NMEA parser. But that's licensed for use on, or in combination with, ST controllers and processors. The Teseo IC is an ST ARM controller, so I believe it would be valid to use the ST parser on (say) a Pico, in combination with the Teseo chip. But I'm not your lawyer . I used ST's Teseo software guide to write this code. But it is not derived from, or based on, the ST parser code. I got inspiration from shabaz' Arduino library. |
Eclipse project with test bed: cpp_nmea_parse_20240801.zip.
The lib will run on any device with a C++ 2x compatible toolchain (e.g.: Raspberry Pico). The test code (main.cpp) sends some data to stdout for showcasing. Not very portable, but not expected to be a part of your embedded design. It's a test bed after all.
next: C++ parser library for NMEA GPS data - pt. 2: parse replies with repeating info
visit the github repository
Link to all posts.