ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
gnss_survey.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 pc_gnss_survey.cpp
6 * @brief GNSS survey-in: WGS84 geodetic <-> ECEF + fixed-position averaging. See pc_gnss_survey.h.
7 */
8
10
11#if PC_ENABLE_NTRIP_CASTER
12
13#include <math.h>
14#include <string.h>
15
16#if PC_NEED_NMEA0183
19#endif
20
21namespace
22{
23// WGS84 ellipsoid.
24const double WGS84_A = 6378137.0; // semi-major axis (m)
25const double WGS84_F = 1.0 / 298.257223563; // flattening
26const double WGS84_E2 = WGS84_F * (2.0 - WGS84_F); // first eccentricity squared
27const double DEG2RAD = 0.017453292519943295; // pi / 180
28const double RAD2DEG = 57.29577951308232; // 180 / pi
29} // namespace
30
31void pc_gnss_geodetic_to_ecef(const GnssGeodetic *g, GnssEcef *e)
32{
33 double lat = g->lat_deg * DEG2RAD;
34 double lon = g->lon_deg * DEG2RAD;
35 double slat = sin(lat);
36 double clat = cos(lat);
37 double slon = sin(lon);
38 double clon = cos(lon);
39 double n = WGS84_A / sqrt(1.0 - WGS84_E2 * slat * slat); // prime vertical radius of curvature
40 e->x = (n + g->height_m) * clat * clon;
41 e->y = (n + g->height_m) * clat * slon;
42 e->z = (n * (1.0 - WGS84_E2) + g->height_m) * slat;
43}
44
45void pc_gnss_ecef_to_geodetic(const GnssEcef *e, GnssGeodetic *g)
46{
47 double p = sqrt(e->x * e->x + e->y * e->y);
48 g->lon_deg = atan2(e->y, e->x) * RAD2DEG;
49 if (p < 1e-9) // on the polar axis: latitude is +/-90, height is measured along z
50 {
51 double b = WGS84_A * (1.0 - WGS84_F);
52 g->lat_deg = (e->z >= 0.0) ? 90.0 : -90.0;
53 g->height_m = fabs(e->z) - b;
54 return;
55 }
56 double lat = atan2(e->z, p * (1.0 - WGS84_E2)); // Bowring-style seed
57 double n = WGS84_A;
58 for (int i = 0; i < 6; i++) // fixed-point iteration; sub-mm after a few passes
59 {
60 double slat = sin(lat);
61 n = WGS84_A / sqrt(1.0 - WGS84_E2 * slat * slat);
62 double h = p / cos(lat) - n;
63 lat = atan2(e->z, p * (1.0 - WGS84_E2 * n / (n + h)));
64 }
65 g->lat_deg = lat * RAD2DEG;
66 g->height_m = p / cos(lat) - n;
67}
68
69int64_t pc_gnss_ecef_m_to_01mm(double metres)
70{
71 double v = metres * 10000.0; // metres -> 0.1 mm
72 return (int64_t)(v >= 0.0 ? v + 0.5 : v - 0.5);
73}
74
75// ---------------------------------------------------------------------------------------------
76// Survey-in accumulator.
77// ---------------------------------------------------------------------------------------------
78
79void pc_gnss_survey_reset(GnssSurvey *s)
80{
81 memset(s, 0, sizeof(*s));
82}
83
84void pc_gnss_survey_add_ecef(GnssSurvey *s, const GnssEcef *e)
85{
86 if (!s->has_origin)
87 {
88 s->has_origin = true;
89 s->ox = e->x;
90 s->oy = e->y;
91 s->oz = e->z;
92 }
93 double dx = e->x - s->ox;
94 double dy = e->y - s->oy;
95 double dz = e->z - s->oz;
96 s->sdx += dx;
97 s->sdy += dy;
98 s->sdz += dz;
99 s->sdx2 += dx * dx;
100 s->sdy2 += dy * dy;
101 s->sdz2 += dz * dz;
102 s->count++;
103}
104
105void pc_gnss_survey_add_geodetic(GnssSurvey *s, const GnssGeodetic *g)
106{
107 GnssEcef e;
108 pc_gnss_geodetic_to_ecef(g, &e);
109 pc_gnss_survey_add_ecef(s, &e);
110}
111
112uint32_t pc_gnss_survey_count(const GnssSurvey *s)
113{
114 return s->count;
115}
116
117bool pc_gnss_survey_mean(const GnssSurvey *s, GnssEcef *out)
118{
119 if (s->count == 0)
120 {
121 return false;
122 }
123 double n = (double)s->count;
124 out->x = s->ox + s->sdx / n;
125 out->y = s->oy + s->sdy / n;
126 out->z = s->oz + s->sdz / n;
127 return true;
128}
129
130double pc_gnss_survey_accuracy_m(const GnssSurvey *s)
131{
132 if (s->count < 2)
133 {
134 return 0.0;
135 }
136 double n = (double)s->count;
137 double mx = s->sdx / n;
138 double my = s->sdy / n;
139 double mz = s->sdz / n;
140 double vx = s->sdx2 / n - mx * mx; // population variance of the shifted coordinates
141 double vy = s->sdy2 / n - my * my;
142 double vz = s->sdz2 / n - mz * mz;
143 if (vx < 0.0) // clamp tiny negatives from floating-point rounding
144 {
145 vx = 0.0;
146 }
147 if (vy < 0.0)
148 {
149 vy = 0.0;
150 }
151 if (vz < 0.0)
152 {
153 vz = 0.0;
154 }
155 return sqrt(vx + vy + vz);
156}
157
158bool pc_gnss_survey_complete(const GnssSurvey *s, uint32_t min_obs, double acc_limit_m)
159{
160 return s->count >= min_obs && s->count >= 2 && pc_gnss_survey_accuracy_m(s) <= acc_limit_m;
161}
162
163// ---------------------------------------------------------------------------------------------
164// GGA -> geodetic (only when the NMEA 0183 codec is available).
165// ---------------------------------------------------------------------------------------------
166
167#if PC_NEED_NMEA0183
168
169namespace
170{
171// A GGA lat/lon field is ddmm.mmmm / dddmm.mmmm; split into whole degrees + decimal minutes.
172bool dm_to_deg(const char *field, uint8_t len, double *out)
173{
174 if (!field || len == 0)
175 {
176 return false;
177 }
178 const char *end = field;
179 double dm = pc_strtod(field, &end);
180 if (end == field)
181 {
182 return false;
183 }
184 double deg = (double)(long)(dm / 100.0); // whole degrees (field is non-negative; sign is N/S,E/W)
185 double minutes = dm - deg * 100.0;
186 *out = deg + minutes / 60.0;
187 return true;
188}
189} // namespace
190
191bool pc_gnss_gga_to_geodetic(const Nmea0183 *m, GnssGeodetic *out)
192{
193 if (!m || !out || m->type[0] != 'G' || m->type[1] != 'G' || m->type[2] != 'A')
194 {
195 return false;
196 }
197 if (m->field_count < 10) // need through the altitude field (index 9)
198 {
199 return false;
200 }
201
202 long quality = 0;
203 if (!pc_nmea0183_field_int(m, 6, &quality) || quality <= 0) // 0 = no fix
204 {
205 return false;
206 }
207
208 double lat = 0.0;
209 double lon = 0.0;
210 if (!dm_to_deg(m->fields[2], m->field_len[2], &lat))
211 {
212 return false;
213 }
214 if (!dm_to_deg(m->fields[4], m->field_len[4], &lon))
215 {
216 return false;
217 }
218 if (m->field_len[3] == 1 && (m->fields[3][0] == 'S' || m->fields[3][0] == 's'))
219 {
220 lat = -lat;
221 }
222 if (m->field_len[5] == 1 && (m->fields[5][0] == 'W' || m->fields[5][0] == 'w'))
223 {
224 lon = -lon;
225 }
226
227 float msl = 0.0f;
228 if (!pc_nmea0183_field_float(m, 9, &msl)) // orthometric (mean-sea-level) altitude
229 {
230 return false;
231 }
232 float geoid = 0.0f;
233 pc_nmea0183_field_float(m, 11, &geoid); // geoid separation; absent -> 0 (ellipsoidal height = MSL)
234
235 out->lat_deg = lat;
236 out->lon_deg = lon;
237 out->height_m = (double)msl + (double)geoid; // ellipsoidal height above WGS84
238 return true;
239}
240
241bool pc_gnss_survey_add_gga(GnssSurvey *s, const Nmea0183 *m)
242{
243 GnssGeodetic g;
244 if (!pc_gnss_gga_to_geodetic(m, &g))
245 {
246 return false;
247 }
248 pc_gnss_survey_add_geodetic(s, &g);
249 return true;
250}
251
252#endif // PC_NEED_NMEA0183
253
254#endif // PC_ENABLE_NTRIP_CASTER
NMEA 0183 sentence codec (PC_ENABLE_NMEA0183) - the marine / GPS ASCII protocol.
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
double pc_strtod(const char *s, const char **end)
Parse a double (integer[.frac][e[+/-]exp]); sets end (or to s if none).
Definition numparse.h:119