ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
nmea0183.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file nmea0183.cpp
6 * @brief NMEA 0183 sentence codec (pure, host-tested).
7 */
8
10
11#if PC_NEED_NMEA0183
12
14#include <string.h>
15
16uint8_t pc_nmea0183_checksum(const char *s, size_t len)
17{
18 uint8_t cs = 0;
19 for (size_t i = 0; i < len; i++)
20 {
21 cs ^= (uint8_t)s[i];
22 }
23 return cs;
24}
25
26static char hex_digit(uint8_t v)
27{
28 v &= 0x0Fu;
29 return (char)(v < 10 ? ('0' + v) : ('A' + v - 10));
30}
31
32static int hex_val(char c)
33{
34 if (c >= '0' && c <= '9')
35 {
36 return c - '0';
37 }
38 if (c >= 'A' && c <= 'F')
39 {
40 return c - 'A' + 10;
41 }
42 if (c >= 'a' && c <= 'f')
43 {
44 return c - 'a' + 10;
45 }
46 return -1;
47}
48
49size_t pc_nmea0183_build(char *buf, size_t cap, const char *body)
50{
51 if (!buf || !body)
52 {
53 return 0;
54 }
55 size_t blen = strnlen(body, cap);
56 size_t total = 1 + blen + 1 + 2 + 2; // '$' + body + '*' + HH + CRLF
57 if (cap < total + 1) // + NUL
58 {
59 return 0;
60 }
61 uint8_t cs = pc_nmea0183_checksum(body, blen);
62 size_t p = 0;
63 buf[p++] = '$';
64 memcpy(buf + p, body, blen);
65 p += blen;
66 buf[p++] = '*';
67 buf[p++] = hex_digit((uint8_t)(cs >> 4));
68 buf[p++] = hex_digit(cs);
69 buf[p++] = '\r';
70 buf[p++] = '\n';
71 buf[p] = '\0';
72 return p;
73}
74
75bool pc_nmea0183_parse(const char *s, size_t len, Nmea0183 *out)
76{
77 if (!s || !out || len < 4 || (s[0] != '$' && s[0] != '!'))
78 {
79 return false;
80 }
81
82 // Find the '*' that introduces the checksum, stopping at any CR/LF.
83 size_t star = 0;
84 bool found = false;
85 for (size_t i = 1; i < len; i++)
86 {
87 if (s[i] == '*')
88 {
89 star = i;
90 found = true;
91 break;
92 }
93 if (s[i] == '\r' || s[i] == '\n')
94 {
95 break;
96 }
97 }
98 if (!found || star + 2 >= len) // need two checksum hex digits after '*'
99 {
100 return false;
101 }
102
103 int hi = hex_val(s[star + 1]);
104 int lo = hex_val(s[star + 2]);
105 if (hi < 0 || lo < 0)
106 {
107 return false;
108 }
109 uint8_t expect = (uint8_t)((hi << 4) | lo);
110 if (pc_nmea0183_checksum(s + 1, star - 1) != expect)
111 {
112 return false;
113 }
114
115 // Split the payload s[1..star-1] on commas (field 0 is the address).
116 uint8_t fc = 0;
117 size_t fstart = 1;
118 for (size_t i = 1; i <= star; i++)
119 {
120 if (i == star || s[i] == ',')
121 {
122 if (fc < PC_NMEA0183_MAX_FIELDS)
123 {
124 out->fields[fc] = s + fstart;
125 out->field_len[fc] = (uint8_t)(i - fstart);
126 fc++;
127 }
128 fstart = i + 1;
129 }
130 }
131 out->field_count = fc;
132
133 // Derive talker / type from the address field (field 0).
134 memset(out->talker, 0, sizeof(out->talker));
135 memset(out->type, 0, sizeof(out->type));
136 if (fc > 0) // GCOVR_EXCL_BR_LINE fc == 0 is unreachable: `found` (checked above, before this
137 // point) guarantees star >= 1 (it is only ever set inside the search loop, which
138 // starts at i = 1), so the split loop's `i <= star` bound guarantees an i == star
139 // iteration, whose `i == star || ...` test is true unconditionally. PC_NMEA0183_
140 // MAX_FIELDS is a positive compile-time constant (26, not overridden by any build
141 // in this tree), so the *first* field-boundary event the split loop reaches --
142 // whether an earlier comma or i == star itself -- always satisfies
143 // `fc < PC_NMEA0183_MAX_FIELDS` and increments fc from 0. Hence fc >= 1 whenever
144 // this line runs.
145 {
146 uint8_t al = out->field_len[0];
147 const char *a = out->fields[0];
148 for (uint8_t i = 0; i < 2 && i < al; i++)
149 {
150 out->talker[i] = a[i];
151 }
152 for (uint8_t i = 0; i < 3 && (uint8_t)(2 + i) < al; i++)
153 {
154 out->type[i] = a[2 + i];
155 }
156 }
157 return true;
158}
159
160bool pc_nmea0183_field_float(const Nmea0183 *m, uint8_t idx, float *out)
161{
162 if (!m || !out || idx >= m->field_count || m->field_len[idx] == 0)
163 {
164 return false;
165 }
166 const char *end = m->fields[idx];
167 // The field is delimited by a ',' or '*' in the source, so pc_strtof stops at the field end.
168 float v = pc_strtof(m->fields[idx], &end);
169 if (end == m->fields[idx])
170 {
171 return false;
172 }
173 *out = v;
174 return true;
175}
176
177bool pc_nmea0183_field_int(const Nmea0183 *m, uint8_t idx, long *out)
178{
179 if (!m || !out || idx >= m->field_count || m->field_len[idx] == 0)
180 {
181 return false;
182 }
183 const char *end = m->fields[idx];
184 long v = pc_strtol(m->fields[idx], &end);
185 if (end == m->fields[idx])
186 {
187 return false;
188 }
189 *out = v;
190 return true;
191}
192
193// ddmm.mmmm + a hemisphere char -> signed decimal degrees (float precision on the raw field).
194static double nmea_coord(float ddmm, char hemi)
195{
196 int deg = (int)(ddmm / 100.0f);
197 double dec = (double)deg + ((double)ddmm - (double)deg * 100.0) / 60.0;
198 if (hemi == 'S' || hemi == 's' || hemi == 'W' || hemi == 'w')
199 {
200 dec = -dec;
201 }
202 return dec;
203}
204
205// Split an hhmmss.ss time field into hour / minute / second (all 0 on a too-short field).
206static void nmea_time(const Nmea0183 *m, uint8_t idx, uint8_t *h, uint8_t *mi, float *s)
207{
208 *h = 0;
209 *mi = 0;
210 *s = 0.0f;
211 if (idx >= m->field_count || m->field_len[idx] < 6)
212 {
213 return;
214 }
215 const char *f = m->fields[idx];
216 *h = (uint8_t)((f[0] - '0') * 10 + (f[1] - '0'));
217 *mi = (uint8_t)((f[2] - '0') * 10 + (f[3] - '0'));
218 const char *end = f + 4;
219 *s = pc_strtof(f + 4, &end); // ss.ss, stopped at the ',' / '*' delimiter
220}
221
222// Split a ddmmyy date field into day / month / (2-digit) year.
223static void nmea_date(const Nmea0183 *m, uint8_t idx, uint8_t *d, uint8_t *mo, uint8_t *y)
224{
225 *d = 0;
226 *mo = 0;
227 *y = 0;
228 if (idx >= m->field_count || m->field_len[idx] < 6)
229 {
230 return;
231 }
232 const char *f = m->fields[idx];
233 *d = (uint8_t)((f[0] - '0') * 10 + (f[1] - '0'));
234 *mo = (uint8_t)((f[2] - '0') * 10 + (f[3] - '0'));
235 *y = (uint8_t)((f[4] - '0') * 10 + (f[5] - '0'));
236}
237
238bool pc_nmea0183_parse_gga(const Nmea0183 *m, pc_nmea_gga *out)
239{
240 if (!m || !out || strcmp(m->type, "GGA") != 0 || m->field_count < 10) // need through altitude (field 9)
241 {
242 return false;
243 }
244 memset(out, 0, sizeof(*out));
245 nmea_time(m, 1, &out->hour, &out->minute, &out->second);
246 float lat = 0.0f, lon = 0.0f;
247 if (pc_nmea0183_field_float(m, 2, &lat) && m->field_len[3] >= 1)
248 {
249 out->lat_deg = nmea_coord(lat, m->fields[3][0]);
250 }
251 if (pc_nmea0183_field_float(m, 4, &lon) && m->field_len[5] >= 1)
252 {
253 out->lon_deg = nmea_coord(lon, m->fields[5][0]);
254 }
255 long q = 0;
256 if (pc_nmea0183_field_int(m, 6, &q))
257 {
258 out->fix_quality = (uint8_t)q;
259 }
260 long ns = 0;
261 if (pc_nmea0183_field_int(m, 7, &ns))
262 {
263 out->num_sats = (uint8_t)ns;
264 }
265 pc_nmea0183_field_float(m, 8, &out->hdop); // stays 0 if empty (out was zeroed)
266 pc_nmea0183_field_float(m, 9, &out->alt_m); // altitude MSL, field 10 is the 'M' unit
267 return true;
268}
269
270bool pc_nmea0183_parse_rmc(const Nmea0183 *m, pc_nmea_rmc *out)
271{
272 if (!m || !out || strcmp(m->type, "RMC") != 0 || m->field_count < 10) // need through date (field 9)
273 {
274 return false;
275 }
276 memset(out, 0, sizeof(*out));
277 nmea_time(m, 1, &out->hour, &out->minute, &out->second);
278 out->valid = (m->field_len[2] >= 1 && (m->fields[2][0] == 'A' || m->fields[2][0] == 'a'));
279 float lat = 0.0f, lon = 0.0f;
280 if (pc_nmea0183_field_float(m, 3, &lat) && m->field_len[4] >= 1)
281 {
282 out->lat_deg = nmea_coord(lat, m->fields[4][0]);
283 }
284 if (pc_nmea0183_field_float(m, 5, &lon) && m->field_len[6] >= 1)
285 {
286 out->lon_deg = nmea_coord(lon, m->fields[6][0]);
287 }
288 pc_nmea0183_field_float(m, 7, &out->speed_knots);
289 pc_nmea0183_field_float(m, 8, &out->course_deg);
290 nmea_date(m, 9, &out->day, &out->month, &out->year);
291 return true;
292}
293
294bool pc_nmea0183_parse_gsv(const Nmea0183 *m, pc_nmea_gsv *out)
295{
296 if (!m || !out || strcmp(m->type, "GSV") != 0 || m->field_count < 4) // header: totalMsgs, msgNum, satsInView
297 {
298 return false;
299 }
300 memset(out, 0, sizeof(*out));
301 long v = 0;
302 if (pc_nmea0183_field_int(m, 1, &v))
303 {
304 out->total_msgs = (uint8_t)v;
305 }
306 if (pc_nmea0183_field_int(m, 2, &v))
307 {
308 out->msg_num = (uint8_t)v;
309 }
310 if (pc_nmea0183_field_int(m, 3, &v))
311 {
312 out->sats_in_view = (uint8_t)v;
313 }
314
315 // Satellite records begin at field 4, four fields each (PRN, elevation, azimuth, SNR).
316 uint8_t records = (uint8_t)((m->field_count - 4) / 4);
317 if (records > 4)
318 {
319 records = 4;
320 }
321 out->sat_count = records;
322 for (uint8_t i = 0; i < records; i++)
323 {
324 uint8_t base = (uint8_t)(4 + i * 4);
325 pc_nmea_gsv_sat *s = &out->sats[i];
326 if (pc_nmea0183_field_int(m, base, &v))
327 {
328 s->prn = (uint8_t)v;
329 }
330 if (pc_nmea0183_field_int(m, (uint8_t)(base + 1), &v))
331 {
332 s->elev_deg = (int16_t)v;
333 }
334 if (pc_nmea0183_field_int(m, (uint8_t)(base + 2), &v))
335 {
336 s->azim_deg = (int16_t)v;
337 }
338 // SNR is blank for a satellite in view but not tracked.
339 s->snr_valid = pc_nmea0183_field_int(m, (uint8_t)(base + 3), &v);
340 if (s->snr_valid)
341 {
342 s->snr_db = (uint8_t)v;
343 }
344 }
345 return true;
346}
347
348bool pc_nmea0183_parse_zda(const Nmea0183 *m, pc_nmea_zda *out)
349{
350 if (!m || !out || strcmp(m->type, "ZDA") != 0 || m->field_count < 5) // need through year (field 4)
351 {
352 return false;
353 }
354 memset(out, 0, sizeof(*out));
355 nmea_time(m, 1, &out->hour, &out->minute, &out->second);
356 long v = 0;
357 if (pc_nmea0183_field_int(m, 2, &v))
358 {
359 out->day = (uint8_t)v;
360 }
361 if (pc_nmea0183_field_int(m, 3, &v))
362 {
363 out->month = (uint8_t)v;
364 }
365 if (pc_nmea0183_field_int(m, 4, &v))
366 {
367 out->year = (uint16_t)v;
368 }
369 // The local-zone offset is optional; a receiver commonly leaves both fields blank.
370 if (pc_nmea0183_field_int(m, 5, &v))
371 {
372 out->zone_hours = (int8_t)v;
373 }
374 if (pc_nmea0183_field_int(m, 6, &v))
375 {
376 out->zone_minutes = (uint8_t)v;
377 }
378 return true;
379}
380
381bool pc_nmea0183_parse_vtg(const Nmea0183 *m, pc_nmea_vtg *out)
382{
383 if (!m || !out || strcmp(m->type, "VTG") != 0 || m->field_count < 9) // need through the km/h unit (field 8)
384 {
385 return false;
386 }
387 memset(out, 0, sizeof(*out));
388 pc_nmea0183_field_float(m, 1, &out->course_true_deg); // stays 0 if empty (out was zeroed)
389 pc_nmea0183_field_float(m, 3, &out->course_mag_deg);
390 pc_nmea0183_field_float(m, 5, &out->speed_knots);
391 pc_nmea0183_field_float(m, 7, &out->speed_kmh);
392 // The mode indicator (field 9) is NMEA 2.3+; older receivers omit it.
393 if (m->field_count > 9 && m->field_len[9] >= 1)
394 {
395 out->mode = m->fields[9][0];
396 }
397 return true;
398}
399
400bool pc_nmea0183_parse_gsa(const Nmea0183 *m, pc_nmea_gsa *out)
401{
402 if (!m || !out || strcmp(m->type, "GSA") != 0 || m->field_count < 18) // need through VDOP (field 17)
403 {
404 return false;
405 }
406 memset(out, 0, sizeof(*out));
407 if (m->field_len[1] >= 1)
408 {
409 out->mode = m->fields[1][0];
410 }
411 long v = 0;
412 if (pc_nmea0183_field_int(m, 2, &v))
413 {
414 out->fix_type = (uint8_t)v;
415 }
416 // Fields 3..14 are the (up to 12) PRNs used; unused slots are blank and skipped.
417 uint8_t cnt = 0;
418 for (uint8_t f = 3; f <= 14; f++)
419 {
420 if (pc_nmea0183_field_int(m, f, &v))
421 {
422 out->sats[cnt++] = (uint8_t)v;
423 }
424 }
425 out->sat_count = cnt;
426 pc_nmea0183_field_float(m, 15, &out->pdop);
427 pc_nmea0183_field_float(m, 16, &out->hdop);
428 pc_nmea0183_field_float(m, 17, &out->vdop);
429 return true;
430}
431
432bool pc_nmea0183_parse_mwv(const Nmea0183 *m, pc_nmea_mwv *out)
433{
434 if (!m || !out || strcmp(m->type, "MWV") != 0 || m->field_count < 6) // need through status (field 5)
435 {
436 return false;
437 }
438 memset(out, 0, sizeof(*out));
439 pc_nmea0183_field_float(m, 1, &out->wind_angle_deg); // stays 0 if empty (out was zeroed)
440 if (m->field_len[2] >= 1)
441 {
442 out->reference = m->fields[2][0];
443 }
444 pc_nmea0183_field_float(m, 3, &out->wind_speed);
445 if (m->field_len[4] >= 1)
446 {
447 out->speed_units = m->fields[4][0];
448 }
449 out->valid = (m->field_len[5] >= 1 && (m->fields[5][0] == 'A' || m->fields[5][0] == 'a'));
450 return true;
451}
452
453bool pc_nmea0183_parse_dpt(const Nmea0183 *m, pc_nmea_dpt *out)
454{
455 if (!m || !out || strcmp(m->type, "DPT") != 0 || m->field_count < 3) // need depth (field 1) + offset (field 2)
456 {
457 return false;
458 }
459 memset(out, 0, sizeof(*out));
460 pc_nmea0183_field_float(m, 1, &out->depth_m); // stays 0 if empty (out was zeroed)
461 pc_nmea0183_field_float(m, 2, &out->offset_m);
462 out->has_range = pc_nmea0183_field_float(m, 3, &out->range_m); // the range scale is optional
463 return true;
464}
465
466bool pc_nmea0183_parse_hdg(const Nmea0183 *m, pc_nmea_hdg *out)
467{
468 if (!m || !out || strcmp(m->type, "HDG") != 0 || m->field_count < 6) // heading + dev + dir + var + dir
469 {
470 return false;
471 }
472 memset(out, 0, sizeof(*out));
473 pc_nmea0183_field_float(m, 1, &out->heading_deg);
474 float dev = 0.0f;
475 if (pc_nmea0183_field_float(m, 2, &dev)) // field 3 is the E/W direction (West -> negative)
476 {
477 out->deviation_deg = (m->field_len[3] >= 1 && (m->fields[3][0] == 'W' || m->fields[3][0] == 'w')) ? -dev : dev;
478 }
479 float var = 0.0f;
480 if (pc_nmea0183_field_float(m, 4, &var)) // field 5 is the E/W direction
481 {
482 out->variation_deg = (m->field_len[5] >= 1 && (m->fields[5][0] == 'W' || m->fields[5][0] == 'w')) ? -var : var;
483 }
484 return true;
485}
486
487bool pc_nmea0183_parse_gll(const Nmea0183 *m, pc_nmea_gll *out)
488{
489 if (!m || !out || strcmp(m->type, "GLL") != 0 || m->field_count < 7) // need through status (field 6)
490 {
491 return false;
492 }
493 memset(out, 0, sizeof(*out));
494 float lat = 0.0f, lon = 0.0f;
495 if (pc_nmea0183_field_float(m, 1, &lat) && m->field_len[2] >= 1)
496 {
497 out->lat_deg = nmea_coord(lat, m->fields[2][0]);
498 }
499 if (pc_nmea0183_field_float(m, 3, &lon) && m->field_len[4] >= 1)
500 {
501 out->lon_deg = nmea_coord(lon, m->fields[4][0]);
502 }
503 nmea_time(m, 5, &out->hour, &out->minute, &out->second);
504 out->valid = (m->field_len[6] >= 1 && (m->fields[6][0] == 'A' || m->fields[6][0] == 'a'));
505 // The FAA mode indicator (field 7) is NMEA 2.3+; older receivers omit it.
506 if (m->field_count > 7 && m->field_len[7] >= 1)
507 {
508 out->mode = m->fields[7][0];
509 }
510 return true;
511}
512
513bool pc_nmea0183_parse_vhw(const Nmea0183 *m, pc_nmea_vhw *out)
514{
515 if (!m || !out || strcmp(m->type, "VHW") != 0 || m->field_count < 8) // need through speed km/h (field 7)
516 {
517 return false;
518 }
519 memset(out, 0, sizeof(*out));
520 pc_nmea0183_field_float(m, 1, &out->heading_true_deg); // heading true; stays 0 if empty (out was zeroed)
521 pc_nmea0183_field_float(m, 3, &out->heading_mag_deg); // heading magnetic
522 pc_nmea0183_field_float(m, 5, &out->speed_knots); // speed through water, knots
523 pc_nmea0183_field_float(m, 7, &out->speed_kmh); // speed through water, km/h
524 return true;
525}
526
527bool pc_nmea0183_parse_vlw(const Nmea0183 *m, pc_nmea_vlw *out)
528{
529 if (!m || !out || strcmp(m->type, "VLW") != 0 || m->field_count < 4) // need through the trip distance (field 3)
530 {
531 return false;
532 }
533 memset(out, 0, sizeof(*out));
534 pc_nmea0183_field_float(m, 1, &out->total_water_nm); // total cumulative; stays 0 if empty (out was zeroed)
535 pc_nmea0183_field_float(m, 3, &out->trip_water_nm); // since the last reset
536 return true;
537}
538
539#endif // PC_NEED_NMEA0183
NMEA 0183 sentence codec (PC_ENABLE_NMEA0183) - the marine / GPS ASCII protocol.
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
long pc_strtol(const char *s, const char **end)
Parse a base-10 long; sets end past the digits (or to s if none).
Definition numparse.h:32
float pc_strtof(const char *s, const char **end)
Parse a float (integer[.frac][e[+/-]exp]); sets end (or to s if none).
Definition numparse.h:154