ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
nmea0183.h
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.h
6 * @brief NMEA 0183 sentence codec (PC_ENABLE_NMEA0183) - the marine / GPS ASCII protocol.
7 *
8 * NMEA 0183 sentences look like `$GPGGA,123519,4807.038,N,...*47<CR><LF>`: a `$` (or `!` for
9 * AIS-encapsulated), a comma-separated field list whose first field is the 5-char address
10 * (2-char talker id + 3-char sentence type), a `*`, and a two-hex-digit XOR checksum. This
11 * codec builds a sentence (adding the `$`, checksum, and CR/LF) and parses one (validating the
12 * checksum and splitting the fields), with `pc_strtof` / `pc_strtol` field-value helpers.
13 *
14 * GPS / marine receivers are cheap UART breakouts, so on an ESP32 this is a plain
15 * `HardwareSerial` link (commonly 4800 or 9600 baud); the UART is the application's. Pure
16 * codec, host-tested. Bridge position / wind / depth data onto Wi-Fi.
17 *
18 * @author Douglas Quigg (dstroy0)
19 * @date 2026
20 */
21
22#ifndef PROTOCORE_NMEA0183_H
23#define PROTOCORE_NMEA0183_H
24
25#include "protocore_config.h"
26
27#if PC_NEED_NMEA0183
28
29#include <stddef.h>
30#include <stdint.h>
31
32/** @brief A parsed NMEA 0183 sentence. Field pointers reference the caller's buffer. */
33struct Nmea0183
34{
35 char talker[3]; ///< 2-char talker id (e.g. "GP") + NUL
36 char type[4]; ///< 3-char sentence type (e.g. "GGA") + NUL
37 uint8_t field_count; ///< number of fields, including field 0 (the address)
38 const char *fields[PC_NMEA0183_MAX_FIELDS]; ///< field 0 is the address; data is 1..n
39 uint8_t field_len[PC_NMEA0183_MAX_FIELDS]; ///< each field's length (0 for an empty field)
40};
41
42/** @brief XOR checksum over @p len octets (the sentence body between `$` and `*`). */
43uint8_t pc_nmea0183_checksum(const char *s, size_t len);
44
45/**
46 * @brief Build a sentence from @p body (e.g. "GPGGA,123519,..."): writes `$<body>*HH\r\n` and
47 * NUL-terminates. Returns the length (excluding the NUL) or 0 on overflow.
48 */
49size_t pc_nmea0183_build(char *buf, size_t cap, const char *body);
50
51/**
52 * @brief Parse a sentence: requires a leading `$`/`!`, validates the `*HH` XOR checksum, and
53 * splits the comma-separated fields. Fills @p out (field 0 is the address; talker / type are
54 * derived from it). Returns false on a bad frame or checksum.
55 */
56bool pc_nmea0183_parse(const char *s, size_t len, Nmea0183 *out);
57
58/** @brief Decode field @p idx as a float (false if absent / empty / non-numeric). */
59bool pc_nmea0183_field_float(const Nmea0183 *m, uint8_t idx, float *out);
60
61/** @brief Decode field @p idx as a long integer (false if absent / empty / non-numeric). */
62bool pc_nmea0183_field_int(const Nmea0183 *m, uint8_t idx, long *out);
63
64// -- typed decoders for the two common GPS position sentences --
65//
66// These lift a parsed sentence (pc_nmea0183_parse) into a position struct: the ddmm.mmmm coordinates
67// become signed decimal degrees (hemisphere-adjusted), and the hhmmss.ss / ddmmyy fields split into their
68// components. Coordinates are read through the float field helper, so their precision is float's (~1 m).
69
70/** @brief Decoded GGA (fix data). */
71struct pc_nmea_gga
72{
73 uint8_t hour, minute; ///< UTC time of the fix
74 float second;
75 double lat_deg; ///< latitude in signed decimal degrees (+ N, - S); 0 if absent
76 double lon_deg; ///< longitude in signed decimal degrees (+ E, - W); 0 if absent
77 uint8_t fix_quality; ///< 0 = no fix, 1 = GPS, 2 = DGPS, ... (field 6)
78 uint8_t num_sats; ///< satellites used in the fix
79 float hdop; ///< horizontal dilution of precision
80 float alt_m; ///< altitude above mean sea level (metres)
81};
82
83/** @brief Decoded RMC (recommended minimum: position + velocity + date). */
84struct pc_nmea_rmc
85{
86 bool valid; ///< true when the status field is 'A' (data valid), false for 'V' (warning)
87 uint8_t hour, minute; ///< UTC time
88 float second;
89 uint8_t day, month, year; ///< UTC date (year is the 2-digit field value, 0..99)
90 double lat_deg; ///< latitude in signed decimal degrees
91 double lon_deg; ///< longitude in signed decimal degrees
92 float speed_knots; ///< speed over ground (knots)
93 float course_deg; ///< course over ground (degrees true)
94};
95
96/**
97 * @brief Decode a parsed GGA sentence into @p out. @return true iff @p m is a GGA sentence with enough
98 * fields; false (and @p out untouched) otherwise. Empty optional fields read back as 0.
99 */
100bool pc_nmea0183_parse_gga(const Nmea0183 *m, pc_nmea_gga *out);
101
102/**
103 * @brief Decode a parsed RMC sentence into @p out. @return true iff @p m is an RMC sentence with enough
104 * fields; false otherwise. @c valid reflects the A/V status field (a 'V' sentence still decodes).
105 */
106bool pc_nmea0183_parse_rmc(const Nmea0183 *m, pc_nmea_rmc *out);
107
108/** @brief One satellite record from a GSV sentence. */
109struct pc_nmea_gsv_sat
110{
111 uint8_t prn; ///< satellite PRN / id
112 int16_t elev_deg; ///< elevation (degrees, 0..90)
113 int16_t azim_deg; ///< azimuth (degrees true, 0..359)
114 uint8_t snr_db; ///< signal-to-noise ratio (dB-Hz), valid only when @ref snr_valid
115 bool snr_valid; ///< false when the SNR field is blank (the satellite is not being tracked)
116};
117
118/** @brief Decoded GSV (satellites in view). One sentence carries up to four satellite records; a full sky
119 * view spans @ref total_msgs sentences. */
120struct pc_nmea_gsv
121{
122 uint8_t total_msgs; ///< total GSV sentences in this cycle
123 uint8_t msg_num; ///< this sentence's number (1-based)
124 uint8_t sats_in_view; ///< total satellites in view across the cycle
125 uint8_t sat_count; ///< satellite records present in THIS sentence (0..4)
126 pc_nmea_gsv_sat sats[4];
127};
128
129/**
130 * @brief Decode a parsed GSV sentence into @p out. @return true iff @p m is a GSV sentence with at least
131 * the 3-field header; the per-satellite records present in this sentence are filled (0..4).
132 */
133bool pc_nmea0183_parse_gsv(const Nmea0183 *m, pc_nmea_gsv *out);
134
135/** @brief Decoded ZDA (UTC time + calendar date + local zone offset). Unlike RMC this carries the full
136 * 4-digit year, so it is the sentence to read for wall-clock time sync. */
137struct pc_nmea_zda
138{
139 uint8_t hour, minute; ///< UTC time
140 float second;
141 uint8_t day, month; ///< UTC date
142 uint16_t year; ///< UTC year (4-digit)
143 int8_t zone_hours; ///< local zone offset hours (-13..+13); 0 if the field is absent
144 uint8_t zone_minutes; ///< local zone offset minutes (0..59); 0 if the field is absent
145};
146
147/**
148 * @brief Decode a parsed ZDA sentence into @p out. @return true iff @p m is a ZDA sentence with at least
149 * the time / day / month / year fields; false otherwise. The zone offset reads back 0 when absent.
150 */
151bool pc_nmea0183_parse_zda(const Nmea0183 *m, pc_nmea_zda *out);
152
153/** @brief Decoded VTG (course over ground + ground speed). The course-over-ground vector complements the
154 * RMC/GGA position - it is the sentence to read for heading and speed. */
155struct pc_nmea_vtg
156{
157 float course_true_deg; ///< course over ground, degrees true (0 if the field is absent)
158 float course_mag_deg; ///< course over ground, degrees magnetic (0 if absent)
159 float speed_knots; ///< speed over ground in knots (0 if absent)
160 float speed_kmh; ///< speed over ground in km/h (0 if absent)
161 char mode; ///< NMEA 2.3+ mode indicator ('A'/'D'/'E'/'N'), or '\0' when the field is absent
162};
163
164/**
165 * @brief Decode a parsed VTG sentence into @p out. @return true iff @p m is a VTG sentence with at least the
166 * course / speed fields (through the km/h unit); false otherwise. The mode reads back '\0' when absent.
167 */
168bool pc_nmea0183_parse_vtg(const Nmea0183 *m, pc_nmea_vtg *out);
169
170/** @brief Decoded GSA (GPS DOP + the satellites active in the fix). Where GSV lists satellites in view and
171 * GGA gives the fix quality, GSA gives the 2D/3D fix mode, which satellites were used, and all three DOPs. */
172struct pc_nmea_gsa
173{
174 char mode; ///< selection mode: 'M' manual, 'A' automatic 2D/3D
175 uint8_t fix_type; ///< 1 = no fix, 2 = 2D, 3 = 3D
176 uint8_t sat_count; ///< number of satellite PRNs present (0..12)
177 uint8_t sats[12]; ///< PRNs of the satellites used in the fix
178 float pdop; ///< position (3D) dilution of precision
179 float hdop; ///< horizontal dilution of precision
180 float vdop; ///< vertical dilution of precision
181};
182
183/**
184 * @brief Decode a parsed GSA sentence into @p out. @return true iff @p m is a GSA sentence with the full
185 * field set (through VDOP); false otherwise. Empty PRN slots are skipped (not counted).
186 */
187bool pc_nmea0183_parse_gsa(const Nmea0183 *m, pc_nmea_gsa *out);
188
189/** @brief Decoded MWV (wind speed + angle): the standard wind-instrument sentence. */
190struct pc_nmea_mwv
191{
192 float wind_angle_deg; ///< wind angle (0..359 degrees)
193 char reference; ///< 'R' relative (apparent) or 'T' true, or '\0' if the field is absent
194 float wind_speed; ///< wind speed, in the units given by @ref speed_units
195 char speed_units; ///< 'K' km/h, 'M' m/s, 'N' knots, or '\0' if absent
196 bool valid; ///< status field: true for 'A' (valid), false for 'V'
197};
198
199/**
200 * @brief Decode a parsed MWV sentence into @p out. @return true iff @p m is an MWV sentence with the angle /
201 * reference / speed / units / status fields; false otherwise.
202 */
203bool pc_nmea0183_parse_mwv(const Nmea0183 *m, pc_nmea_mwv *out);
204
205/** @brief Decoded DPT (depth of water): depth relative to the transducer plus the transducer offset. */
206struct pc_nmea_dpt
207{
208 float depth_m; ///< water depth relative to the transducer (meters)
209 float offset_m; ///< transducer offset (meters): + = transducer to waterline, - = transducer to keel
210 bool has_range; ///< true when the optional maximum-range-scale field is present
211 float range_m; ///< maximum range scale in use (meters), valid only when @ref has_range
212};
213
214/**
215 * @brief Decode a parsed DPT sentence into @p out. @return true iff @p m is a DPT sentence with the depth
216 * and offset fields; false otherwise. The optional range scale sets @ref pc_nmea_dpt::has_range.
217 */
218bool pc_nmea0183_parse_dpt(const Nmea0183 *m, pc_nmea_dpt *out);
219
220/** @brief Decoded HDG (magnetic heading + deviation + variation): the compass sentence. The deviation and
221 * variation are signed with East positive / West negative, so true heading = heading + deviation + variation. */
222struct pc_nmea_hdg
223{
224 float heading_deg; ///< magnetic sensor heading (degrees)
225 float deviation_deg; ///< magnetic deviation (degrees), + East / - West; 0 if absent
226 float variation_deg; ///< magnetic variation (degrees), + East / - West; 0 if absent
227};
228
229/**
230 * @brief Decode a parsed HDG sentence into @p out. @return true iff @p m is an HDG sentence with the heading
231 * / deviation / variation fields; false otherwise. The E/W direction fields are folded into the sign.
232 */
233bool pc_nmea0183_parse_hdg(const Nmea0183 *m, pc_nmea_hdg *out);
234
235/** @brief Decoded GLL (geographic position - latitude / longitude): position + UTC time + validity. Where
236 * GGA carries the full fix quality and RMC adds velocity, GLL is the minimal position report. */
237struct pc_nmea_gll
238{
239 double lat_deg; ///< latitude in signed decimal degrees (+ N, - S); 0 if absent
240 double lon_deg; ///< longitude in signed decimal degrees (+ E, - W); 0 if absent
241 uint8_t hour, minute; ///< UTC time of the position
242 float second;
243 bool valid; ///< true when the status field is 'A' (data valid), false for 'V' (warning)
244 char mode; ///< FAA mode indicator (NMEA 2.3+), or '\0' if the field is absent
245};
246
247/**
248 * @brief Decode a parsed GLL sentence into @p out. @return true iff @p m is a GLL sentence through the
249 * status field; false otherwise. @c valid reflects the A/V status (a 'V' sentence still decodes);
250 * the FAA mode indicator is set only when the (optional, NMEA 2.3+) field is present.
251 */
252bool pc_nmea0183_parse_gll(const Nmea0183 *m, pc_nmea_gll *out);
253
254/** @brief Decoded VHW (water speed + heading): the vessel's heading and its speed through the water. Where VTG
255 * reports GPS course + speed over ground, VHW reports the heading the vessel points and its speed relative to
256 * the water (from a paddlewheel / pitot log). */
257struct pc_nmea_vhw
258{
259 float heading_true_deg; ///< vessel heading, degrees true (0 if the field is absent)
260 float heading_mag_deg; ///< vessel heading, degrees magnetic (0 if absent)
261 float speed_knots; ///< speed through the water in knots (0 if absent)
262 float speed_kmh; ///< speed through the water in km/h (0 if absent)
263};
264
265/**
266 * @brief Decode a parsed VHW sentence into @p out. @return true iff @p m is a VHW sentence with at least the
267 * heading / speed fields (through the km/h value); false otherwise. Empty optional fields read back 0.
268 */
269bool pc_nmea0183_parse_vhw(const Nmea0183 *m, pc_nmea_vhw *out);
270
271/** @brief Decoded VLW (distance traveled through the water): the cumulative and trip water-distance log from a
272 * paddlewheel / pitot log - the marine odometer, the through-water companion to VHW's speed. */
273struct pc_nmea_vlw
274{
275 float total_water_nm; ///< total cumulative water distance (nautical miles; 0 if absent)
276 float trip_water_nm; ///< water distance since the last reset (nautical miles; 0 if absent)
277};
278
279/**
280 * @brief Decode a parsed VLW sentence into @p out. @return true iff @p m is a VLW sentence with at least the
281 * total + trip water-distance fields; false otherwise. Empty optional fields read back 0.
282 */
283bool pc_nmea0183_parse_vlw(const Nmea0183 *m, pc_nmea_vlw *out);
284
285#endif // PC_NEED_NMEA0183
286#endif // PROTOCORE_NMEA0183_H
User-facing configuration for ProtoCore.