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.
Objects to parse and hold a simple reply
The second post shows a slightly more involved (for me, not for the library user) topic: building objects for a command that has multiple repeating sets of data. It's not that complex: the number of repetitions is predefined and fixed. In the example that I use, the GSV reply, 4 different satellite attributes are in the reply message. Repeated 4 times. If there are less than 4 sets of data, the fields are empty.
edit: the parser also supports GSA, where the message reports up to 12 satellite's PRNs.
This example line has data for 4 satellites.
GSV: $GPGSV,3,1,11,13,79,310,,14,53,113,,05,51,214,,30,47,067,*72
The first attribute for the first satellite is the value 13. It's the PRN.
I wrote the class gsv to hold generic attributes of the reply. And a class gsv_sat to hold per-satellite data. gsv has a container member to hold 4 of those gsv_sat objects. I use a std::array<gsv_sat,4> as container type. I could have made gsv_sat an embedded type of gsv. But I erred on the keep-it-simple side.
Why I selected std::array as container in this lib: https://embeddedartistry.com/blog/2017/08/23/choosing-the-right-stl-container-general-rules-of-thumb/ |
For convenience, I created a typedef:
typedef std::array<gsv_sat, 4> gsv_sat_array;
edit: also the gsa class - available in the library but not discussed in the blog - uses an array, to store up to 12 satellite PRN numbers.
gsv_sat class
This class can hold a set of 4 attributes that the GSV message returns for each satellite. It doesn't have any parsing logic. That's managed by the gsv class.
class gsv_sat { public: unsigned int prn; unsigned int elev; unsigned int azim; unsigned int snr; };
gsv class
This is the one that can parse the GSV message, retrieve its individual data points and fill up the individual satellite information.
class gsv { public: static bool from_data(const std::string& data, gsv& gsv); nmea::talker_id source; gsv_sat_array sats; };
Appreciate that this is all very little code, to retrieve all that info. The from_data() method does all the groundwork.
// $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 4: case 8: case 12: case 16: if (std::string_view(word).length() == 0) { gsv.sats[(field - 4) / 4 ].prn = 0; } else { gsv.sats[(field - 4) / 4 ].prn = std::stoi(std::string(std::string_view(word))); } break; case 5: case 9: case 13: case 17: if (std::string_view(word).length() == 0) { gsv.sats[(field - 5) / 4 ].elev = 0; } else { gsv.sats[(field - 5) / 4 ].elev = std::stoi(std::string(std::string_view(word))); } break; case 6: case 10: case 14: case 18: if (std::string_view(word).length() == 0) { gsv.sats[(field - 6) / 4 ].azim = 0; } else { gsv.sats[(field - 6) / 4 ].azim = std::stoi(std::string(std::string_view(word))); } break; case 7: case 11: case 15: case 19: if (std::string_view(word).length() == 0) { gsv.sats[(field - 7) / 4 ].snr = 0; } else { gsv.sats[(field - 7) / 4 ].snr = std::stoi(std::string(std::string_view(word))); } break; default: // skip 1, 2, 3 break; } field++; } return (field == 20); // everything parsed }
Because the satellite is repeated a fixed amount of times, I was able to reuse the case conditions for each field. This code also shows an example that shows the use of std::string_view. It's used here to hide the trailing checksum data from the string tokeniser.
Typical use:
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 << "source: " << o.source << ". " << std::endl; for(auto s : o.sats) { std::cout << "sat prn: " << s.prn << ", elev: " << s.elev << ", azim: " << s.azim << ", snr: " << s.snr << "." << std::endl; } }
This example shows a typical use for the GSV scenario, where the GPS returns multiple lines. And each line can contain telemetry of 1 .. 4 satellites.
Result:
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.
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.
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.
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.
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.
Next posts
In the follow up blog, I'll finish with the utility classes and data type helpers.
next post: C++ parser library for NMEA GPS data - pt. 3: utility class, example use and wrap-up
visit the github repository
Link to all posts.