Pico GPS Teseo
Loading...
Searching...
No Matches
nmea.cpp
Go to the documentation of this file.
1/*
2 * nmea.cpp
3 *
4 * Created on: 29 jul. 2024
5 * Author: jancu
6 */
7
8module;
9
10#include <cassert>
11#include <ranges>
12#include <cmath>
13#include <chrono>
14
15module nmea;
16
17using std::operator""sv;
18constexpr auto delim{","sv};
19
20namespace nmea {
21
22talker_id nmea::talker(const std::string_view& sv) {
24 if (sv.starts_with("$GP")) {
25 id = talker_id::gps;
26 } else if (sv.starts_with("$GL")) {
28 } else if (sv.starts_with("$GA")) {
30 } else if (sv.starts_with("$BD")) {
32 } else if (sv.starts_with("$QZ")) {
33 id = talker_id::qzss;
34 } else if (sv.starts_with("$GN")) {
36 }
37 return id;
38}
39
40talker_id nmea::system(const std::string_view& sv) {
42 if (sv.starts_with("1")) {
43 id = talker_id::gps;
44 } else if (sv.starts_with("2")) {
46 } else if (sv.starts_with("3")) {
48 } else if (sv.starts_with("4")) {
50 } else if (sv.starts_with("5")) {
51 id = talker_id::qzss;
52 }
53 return id;
54}
55
56float nmea::coord(const unsigned int degrees_chars, const std::string_view& sv) {
57 std::string s(sv);
58 // a float (and a double) may not precisely represent the value in the message
59 float coord = std::stof(s.substr(0, degrees_chars), nullptr);
60 coord += std::stof(s.substr(degrees_chars), nullptr) / 60.0;
61 return coord;
62}
63
64direction nmea::dir(const std::string_view& sv) {
65 assert(sv.starts_with('N') ||
66 sv.starts_with('S') ||
67 sv.starts_with('E') ||
68 sv.starts_with('W'));
70 if (sv.starts_with('N')) {
71 d = direction::n;
72 } else if (sv.starts_with('S')) {
73 d = direction::s;
74 } else if (sv.starts_with('E')) {
75 d = direction::e;
76 } else if (sv.starts_with('W')) {
77 d = direction::w;
78 }
79 return d;
80}
81
82void nmea::time(const std::string_view& sv, time_t& t) {
83 std::string s(sv);
84 t = time_t{
85 std::chrono::hours(std::stoi(s.substr(0, 2))) +
86 std::chrono::minutes(std::stoi(s.substr(2, 2))) +
87 std::chrono::seconds(std::stoi(s.substr(4, 2))) +
88 std::chrono::milliseconds(std::stoi(s.substr(7)))
89 };
90}
91
92void nmea::date(const std::string_view& sv, std::chrono::year_month_day& d) {
93 std::string s(sv);
94// NMEA returns two digits, and not all systems know what century we are in.
95// Current solution: use the century defined in source code.
96// alternative could be to convert to stream and use formatter to parse 2 digit year
97// but that's a bit much for our use case
98#define CENTURY (2000)
99 d = std::chrono::year_month_day{
100 std::chrono::year(std::stoi(s.substr(4)) + CENTURY),
101 std::chrono::month(std::stoi(s.substr(2, 2))),
102 std::chrono::day(std::stoi(s.substr(0, 2)))
103 };
104#undef CENTURY
105}
106
107bool nmea::valid(const std::string_view& sv) {
108 assert(sv.starts_with('A') ||
109 sv.starts_with('V'));
110 return sv.starts_with('A');
111}
112
113quality nmea::qual(const std::string_view& sv) {
114 // the typecast from int to enem<unsigned int> below is confirmed
115 // to be safe in this case
116 // https://www.modernescpp.com/index.php/strongly-typed-enums/
117 return static_cast<quality>(std::stoi(std::string(sv)));
118}
119
120// $GPGLL,<Lat>,<N/S>,<Long>,<E/W>,<Timestamp>,<Status>,<mode indicator>*<checksum><cr><lf>
121
122gll::gll_result gll::from_data(const std::string& data) {
123 unsigned int field = 0;
125 for (const auto word : std::views::split(data, delim)) {
126 switch (field) {
127 case 0: // talker id
128 gll.result.source = nmea::talker(std::string_view(word));
129 break;
130 case 1: // latitude
131 gll.result.lat = nmea::coord(2, std::string_view(word));
132 break;
133 case 2: // latitude direction
134 if (nmea::dir(std::string_view(word)) == direction::s) {
135 gll.result.lat = gll.result.lat * -1;
136 }
137 break;
138 case 3: // longitude
139 gll.result.lon = nmea::coord(3, std::string_view(word));
140 break;
141 case 4: // longitude direction
142 if (nmea::dir(std::string_view(word)) == direction::w) {
143 gll.result.lon = gll.result.lon * -1;
144 }
145 break;
146 case 5: // timestamp
147 nmea::time(std::string_view(word), gll.result.t);
148 break;
149 case 6: // valid
150 gll.result.valid = nmea::valid(std::string_view(word));
151 break;
152 default: // skip 7
153 break;
154 }
155 field++;
156 }
157 gll.result.success = (field == 8); // everything parsed
158 return gll;
159}
160
161// $GPGGA,<Timestamp>,<Lat>,<N/S>,<Long>,<E/W>,<GPSQual>,<Sats>,<HDOP>,<Alt>,<AltVal>,<GeoSep>,
162// <GeoVal>,<DGPSAge>,<DGPSRef>*<checksum><cr><lf>
163gga::gga_result gga::from_data(const std::string& data) {
164 unsigned int field = 0;
166 for (const auto word : std::views::split(data, delim)) {
167 switch (field) {
168 case 0: // talker id
169 gga.result.source = nmea::talker(std::string_view(word));
170 break;
171 case 1: // timestamp
172 nmea::time(std::string_view(word), gga.result.t);
173 break;
174 case 2: // latitude
175 gga.result.lat = nmea::coord(2, std::string_view(word));
176 break;
177 case 3: // latitude direction
178 if (nmea::dir(std::string_view(word)) == direction::s) {
179 gga.result.lat = gga.result.lat * -1;
180 }
181 break;
182 case 4: // longitude
183 gga.result.lon = nmea::coord(3, std::string_view(word));
184 break;
185 case 5: // longitude direction
186 if (nmea::dir(std::string_view(word)) == direction::w) {
187 gga.result.lon = gga.result.lon * -1;
188 }
189 break;
190 case 6: // qual
191 gga.result.qual = nmea::qual(std::string_view(word));
192 break;
193 case 7: // sats
194 gga.result.sats = std::stoi(std::string(std::string_view(word)));
195 break;
196 case 9: // altitude (in meters)
197 // a float (and a double) may not precisely represent the value in the message
198 gga.result.alt = std::stof(std::string(std::string_view(word)), nullptr);
199 break;
200 case 11: // geoid separation (in meters)
201 // a float (and a double) may not precisely represent the value in the message
202 gga.result.geosep = std::stof(std::string(std::string_view(word)), nullptr);
203 break;
204 default: // skip 8, 10, 12 .. 15
205 break;
206 }
207 field++;
208 }
209 gga.result.success = (field == 15); // everything parsed
210 return gga;
211}
212
213// $GNGSA,A,3,15,18,,,,,,,,,,,4.7,3.7,2.9*2D
214// $GNGSA,A,3,73,65,81,,,,,,,,,,4.7,3.7,2.9*2E
215gsa::gsa_result gsa::from_data(const std::string& data) {
216 unsigned int field = 0;
218 std::string_view v = std::string_view(data).substr(0, data.find('*'));
219 for (const auto word : std::views::split(v, delim)) {
220 switch (field) {
221 case 0: // talker id
222 gsa.result.source = nmea::talker(std::string_view(word));
223 break;
224 case 3:
225 case 4:
226 case 5:
227 case 6:
228 case 7:
229 case 8:
230 case 9:
231 case 10:
232 case 11:
233 case 12:
234 case 13:
235 case 14:
236 if (std::string_view(word).length() == 0) {
237 gsa.result.sats[field - 3] = 0;
238 } else {
239 gsa.result.sats[field - 3] =
240 std::stoi(std::string(std::string_view(word)));
241 }
242 break;
243 case 15:
244 gsa.result.system_id = nmea::system(std::string_view(word));
245 break;
246 default: // skip 1,2, 16, 17
247 break;
248 }
249 field++;
250 }
251 gsa.result.success = (field == 18); // everything parsed
252 return gsa;
253}
254
255// $GPGSV,3,1,11,13,79,310,,14,53,113,,05,51,214,,30,47,067,*72
256// $GPGSV,3,2,11,15,45,295,24,22,44,145,,20,27,192,,07,16,064,*7A
257// $GPGSV,3,3,11,18,16,298,25,24,08,249,,08,08,029,18,,,,*40
258// $GLGSV,2,1,08,72,79,113,,74,77,084,,75,38,202,,65,37,317,28*68
259// $GLGSV,2,2,08,73,34,040,35,71,28,130,,81,13,333,24,82,08,017,*68
260gsv::gsv_result gsv::from_data(const std::string& data) {
261 unsigned int field = 0;
263 std::string_view v = std::string_view(data).substr(0, data.find('*'));
264 for (const auto word : std::views::split(v, delim)) {
265 switch (field) {
266 case 0: // talker id
267 gsv.result.source = nmea::talker(std::string_view(word));
268 break;
269 case 4:
270 case 8:
271 case 12:
272 case 16:
273 if (std::string_view(word).length() == 0) {
274 gsv.result.sats[(field - 4) / 4 ].prn = 0;
275 } else {
276 gsv.result.sats[(field - 4) / 4 ].prn =
277 std::stoi(std::string(std::string_view(word)));
278 }
279 break;
280 case 5:
281 case 9:
282 case 13:
283 case 17:
284 if (std::string_view(word).length() == 0) {
285 gsv.result.sats[(field - 5) / 4 ].elev = 0;
286 } else {
287 gsv.result.sats[(field - 5) / 4 ].elev =
288 std::stoi(std::string(std::string_view(word)));
289 }
290 break;
291 case 6:
292 case 10:
293 case 14:
294 case 18:
295 if (std::string_view(word).length() == 0) {
296 gsv.result.sats[(field - 6) / 4 ].azim = 0;
297 } else {
298 gsv.result.sats[(field - 6) / 4 ].azim =
299 std::stoi(std::string(std::string_view(word)));
300 }
301 break;
302 case 7:
303 case 11:
304 case 15:
305 case 19:
306 if (std::string_view(word).length() == 0) {
307 gsv.result.sats[(field - 7) / 4 ].snr = 0;
308 } else {
309 gsv.result.sats[(field - 7) / 4 ].snr =
310 std::stoi(std::string(std::string_view(word)));
311 }
312 break;
313 default: // skip 1, 2, 3
314 break;
315 }
316 field++;
317 }
318 gsv.result.success = (field == 20); // everything parsed
319 return gsv;
320}
321
322// $GPRMC,185427.150,V,5051.83778,N,00422.55809,E,,,240724,,,N*7F
323rmc::rmc_result rmc::from_data(const std::string& data) {
324 unsigned int field = 0;
326 for (const auto word : std::views::split(data, delim)) {
327 switch (field) {
328 case 0: // talker id
329 rmc.result.source = nmea::talker(std::string_view(word));
330 break;
331 case 1: // timestamp
332 nmea::time(std::string_view(word), rmc.result.t);
333 break;
334 case 2: // valid
335 rmc.result.valid = nmea::valid(std::string_view(word));
336 break;
337 case 3: // latitude
338 rmc.result.lat = nmea::coord(2, std::string_view(word));
339 break;
340 case 4: // latitude direction
341 if (nmea::dir(std::string_view(word)) == direction::s) {
342 rmc.result.lat = rmc.result.lat * -1;
343 }
344 break;
345 case 5: // longitude
346 rmc.result.lon = nmea::coord(3, std::string_view(word));
347 break;
348 case 6: // longitude direction
349 if (nmea::dir(std::string_view(word)) == direction::w) {
350 rmc.result.lon = rmc.result.lon * -1;
351 }
352 break;
353 case 9: // dqtestamp
354 nmea::date(std::string_view(word), rmc.result.d);
355 break;
356 default: // skip 7, 8, 10, 11, 12
357 break;
358 }
359 field++;
360 }
361 rmc.result.success = (field == 13); // everything parsed
362 return rmc;
363}
364
365} // namespace nmea
static quality qual(const std::string_view &sv)
Definition nmea.cpp:113
static bool valid(const std::string_view &sv)
Definition nmea.cpp:107
static talker_id system(const std::string_view &sv)
Definition nmea.cpp:40
static direction dir(const std::string_view &sv)
Definition nmea.cpp:64
static void time(const std::string_view &sv, time_t &t)
Definition nmea.cpp:82
static float coord(const unsigned int degrees_chars, const std::string_view &sv)
Definition nmea.cpp:56
static talker_id talker(const std::string_view &sv)
Definition nmea.cpp:22
static void date(const std::string_view &sv, std::chrono::year_month_day &d)
Definition nmea.cpp:92
Definition nmea.cpp:20
std::chrono::hh_mm_ss< std::chrono::duration< long long, std::ratio< 1, 1000 > > > time_t
constexpr auto delim
Definition nmea.cpp:18
#define CENTURY
float geosep
quality qual
talker_id source
unsigned int sats
static gga_result from_data(const std::string &data)
Definition nmea.cpp:163
nmea_result< gga > gga_result
talker_id source
nmea_result< gll > gll_result
static gll_result from_data(const std::string &data)
Definition nmea.cpp:122
talker_id source
nmea_result< gsa > gsa_result
static gsa_result from_data(const std::string &data)
Definition nmea.cpp:215
talker_id system_id
gsa_sat_array sats
talker_id source
nmea_result< gsv > gsv_result
gsv_sat_array sats
static gsv_result from_data(const std::string &data)
Definition nmea.cpp:260
std::chrono::year_month_day d
static rmc_result from_data(const std::string &data)
Definition nmea.cpp:323
talker_id source
nmea_result< rmc > rmc_result