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.
The third post wraps up the library design. Topics:
- utility class
- example with output
Utility class
nmea
This is a pure utility class, that offers functionality to parse particular nmea data types. It's also the one that holds the different NMEA-wide enumeration definitions. The class doesn't have any data member, and all methods are static. You can call each of them right away, without constructing an object (actually: I blocked possibility of instantiating an object).
example:
// retrieve the talker ID from the current reply token rmc.source = nmea::get_talker_id(std::string_view(word));
class definition:
typedef std::chrono::hh_mm_ss<std::chrono::duration<int, std::ratio<1, 1000>>> time_t; class nmea { public: enum talker_id { gps, // If system works in GPS only mode glonass, // If system works in GLONASS only mode galileo, // If system works in GALILEO only mode beidou, // If system works in BEIDOU only mode qzss, // If system works in QZSS only mode multiconstellation // If system works in multi-constellation mode }; enum dir { n, s, e, w }; enum qual : unsigned int { q0 = 0, q1 = 1, q2 = 2, q6 = 6 }; nmea() = delete; // prevent creation of objects of this utility class static talker_id get_talker_id(const std::string_view& sv); static talker_id get_system_id(const std::string_view& sv); static float get_coord(const std::string_view& sv); static dir get_dir(const std::string_view& sv); static void get_time(const std::string_view& sv, time_t& t); static void get_date(const std::string_view& sv, std::chrono::year_month_day& d); static bool get_valid(const std::string_view& sv); static qual get_qual(const std::string_view& sv); };
implementation:
nmea::talker_id nmea::get_talker_id(const std::string_view& sv) { talker_id id = gps; if (sv.starts_with("$GP")) { id = gps; } else if (sv.starts_with("$GL")) { id = glonass; } else if (sv.starts_with("$GA")) { id = galileo; } else if (sv.starts_with("$BD")) { id = beidou; } else if (sv.starts_with("$QZ")) { id = qzss; } else if (sv.starts_with("$GN")) { id = multiconstellation; } return id; } nmea::talker_id nmea::get_system_id(const std::string_view& sv) { talker_id id = gps; if (sv.starts_with("1")) { id = gps; } else if (sv.starts_with("2")) { id = glonass; } else if (sv.starts_with("3")) { id = galileo; } else if (sv.starts_with("4")) { id = beidou; } else if (sv.starts_with("5")) { id = qzss; } return id; } float nmea::get_coord(const std::string_view& sv) { std::string s(sv); float coord = std::stof(s.substr(0, 2), nullptr); coord += std::stof(s.substr(2), nullptr) / 60.0; return coord; } nmea::dir nmea::get_dir(const std::string_view& sv) { assert(sv.starts_with('N') || sv.starts_with('S') || sv.starts_with('E') || sv.starts_with('W')); dir d = n; if (sv.starts_with('N')) { d = n; } else if (sv.starts_with('S')) { d = s; } else if (sv.starts_with('E')) { d = e; } else if (sv.starts_with('W')) { d = w; } return d; } void nmea::get_time(const std::string_view& sv, time_t& t) { std::string s(sv); t = time_t{ std::chrono::hours(std::stoi(s.substr(0, 2))) + std::chrono::minutes(std::stoi(s.substr(2, 2))) + std::chrono::seconds(std::stoi(s.substr(4, 2))) + std::chrono::milliseconds(std::stoi(s.substr(7))) }; /* t = std::chrono::hh_mm_ss(10.5h + 98min + 2020s + 0.5s); t.set_h(std::stoi(s.substr(0, 2))); t.set_m(std::stoi(s.substr(2, 2))); t.set_s(std::stoi(s.substr(4, 2))); t.set_ms(std::stoi(s.substr(7)));*/ } void nmea::get_date(const std::string_view& sv, std::chrono::year_month_day& d) { std::string s(sv); d = std::chrono::year_month_day{ std::chrono::year(std::stoi(s.substr(4)) + 2000), // TODO Y2.1K warning std::chrono::month(std::stoi(s.substr(2, 2))), std::chrono::day(std::stoi(s.substr(0, 2))) }; } bool nmea::get_valid(const std::string_view& sv) { assert(sv.starts_with('A') || sv.starts_with('V')); return sv.starts_with('A'); } nmea::qual nmea::get_qual(const std::string_view& sv) { // the typecast from int to enem<unsigned int> below is confirmed // to be safe in this case // https://www.modernescpp.com/index.php/strongly-typed-enums/ return static_cast<nmea::qual>(std::stoi(std::string(sv))); }
Like most of this library code, it's all reasonably straightforward. Splitting off different parts of the exercise to specialised classes, helps. That's also a reason why I created different libraries for GPS interaction and NMEA data parsing. Although they can (and in a later project will) work together, they solve different aspects of API integration. Trying to integrate parsing logic in the GPS interaction can resolve in logic that's hard to untangle. It also makes it harder to use either the GPS communication part, or the parsing part, in isolation.
Example and Testbed
The library's main file serves 2 purposes: Showcase how the parser can be used, and act as a testbed for the code. I hope it's self-explanatory:
#include <string> #include <vector> #include <iostream> #include "nmea.h" void test_gll() { 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; } void test_gga() { std::string reply = "$GPGGA,191237.000,5051.78066,N,00422.57079,E,1,05,3.7,027.26,M,47.3,M,,*65"; nmea::gga o; nmea::gga::from_data(reply, o); std::cout << "GGA " << std::endl << "source: " << o.source << ". " << "lat: " << o.lat << " lon: " << o.lon << ". " << o.t << ". " << "qual: " << o.qual << ", " << "sats: " << o.sats << ". " << std::endl; return; } void test_gsa() { std::vector<std::string> replies { "$GNGSA,A,3,15,18,,,,,,,,,,,4.7,3.7,2.9*2D", "$GNGSA,A,3,73,65,81,,,,,,,,,,4.7,3.7,2.9*2E" }; for(auto r : replies) { nmea::gsa o; nmea::gsa::from_data(r, o); std::cout << "GSA " << std::endl << "source: " << o.source << ". " << std::endl << "system: " << o.system_id << ". " << std::endl; for(const auto s : o.sats) { std::cout << "sat prn: " << s << "." << std::endl; } } return; } void test_gsv() { std::vector<std::string> replies { "$GPGSV,3,1,11,13,79,310,,14,53,113,,05,51,214,,30,47,067,*72", "$GPGSV,3,2,11,15,45,295,24,22,44,145,,20,27,192,,07,16,064,*7A", "$GPGSV,3,3,11,18,16,298,25,24,08,249,,08,08,029,18,,,,*40", "$GLGSV,2,1,08,72,79,113,,74,77,084,,75,38,202,,65,37,317,28*68", "$GLGSV,2,2,08,73,34,040,35,71,28,130,,81,13,333,24,82,08,017,*68" }; for(auto r : replies) { nmea::gsv o; nmea::gsv::from_data(r, o); std::cout << "GSV " << std::endl << "source: " << o.source << ". " << std::endl; for(const auto s : o.sats) { std::cout << "sat prn: " << s.prn << ", elev: " << s.elev << ", azim: " << s.azim << ", snr: " << s.snr << "." << std::endl; } } return; } void test_rmc() { 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 << ". " << o.d << ". " << "valid: " << o.valid << ". " << std::endl; return; } int main() { test_gll(); test_gga(); test_gsa(); test_gsv(); test_rmc(); return 0; }
All test string come from this investigative post.
Output:
GLL
source: 0. lat: 50.864 lon: 7.04263. 18:54:27.150. valid: 0.
GGA
source: 0. lat: 50.863 lon: 7.04285. 19:12:37.000. qual: 1, sats: 5.
GSA
source: 5.
system: 3.
sat prn: 15.
sat prn: 18.
sat prn: 0.
...
sat prn: 0.
GSA
source: 5.
system: 3.
sat prn: 73.
sat prn: 65.
sat prn: 81.
sat prn: 0.
...
sat prn: 0.
GSV
source: 0.
sat prn: 13, elev: 79, azim: 310, snr: 0.
sat prn: 14, elev: 53, azim: 113, snr: 0.
sat prn: 5, elev: 51, azim: 214, snr: 0.
sat prn: 30, elev: 47, azim: 67, snr: 0.
GSV
source: 0.
sat prn: 15, elev: 45, azim: 295, snr: 24.
sat prn: 22, elev: 44, azim: 145, snr: 0.
sat prn: 20, elev: 27, azim: 192, snr: 0.
sat prn: 7, elev: 16, azim: 64, snr: 0.
GSV
source: 0.
sat prn: 18, elev: 16, azim: 298, snr: 25.
sat prn: 24, elev: 8, azim: 249, snr: 0.
sat prn: 8, elev: 8, azim: 29, snr: 18.
sat prn: 0, elev: 0, azim: 0, snr: 0.
GSV
source: 1.
sat prn: 72, elev: 79, azim: 113, snr: 0.
sat prn: 74, elev: 77, azim: 84, snr: 0.
sat prn: 75, elev: 38, azim: 202, snr: 0.
sat prn: 65, elev: 37, azim: 317, snr: 28.
GSV
source: 1.
sat prn: 73, elev: 34, azim: 40, snr: 35.
sat prn: 71, elev: 28, azim: 130, snr: 0.
sat prn: 81, elev: 13, azim: 333, snr: 24.
sat prn: 82, elev: 8, azim: 17, snr: 0.
RMC
source: 0. lat: 50.864 lon: 7.04263. 18:54:27.150. 2024-07-24. valid: 0.
In an embedded design, you'd use a similar approach. Get data from the GPS in NMEA format. Then use this parser's object to get hold of individual attributes.
Next post: C++ parser library for NMEA GPS data - pt. 4: We have C++ objects and containers: So what?
visit the github repository
Link to all posts.