DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 gnss_survey.cpp
6 * @brief GNSS survey-in: WGS84 geodetic <-> ECEF + fixed-position averaging. See gnss_survey.h.
7 */
8
10
11#if DETWS_ENABLE_NTRIP_CASTER
12
13#include <math.h>
14#include <string.h>
15
16#if DETWS_ENABLE_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 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 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 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 gnss_survey_reset(GnssSurvey *s)
80{
81 memset(s, 0, sizeof(*s));
82}
83
84void 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 gnss_survey_add_geodetic(GnssSurvey *s, const GnssGeodetic *g)
106{
107 GnssEcef e;
108 gnss_geodetic_to_ecef(g, &e);
109 gnss_survey_add_ecef(s, &e);
110}
111
112uint32_t gnss_survey_count(const GnssSurvey *s)
113{
114 return s->count;
115}
116
117bool gnss_survey_mean(const GnssSurvey *s, GnssEcef *out)
118{
119 if (s->count == 0)
120 return false;
121 double n = (double)s->count;
122 out->x = s->ox + s->sdx / n;
123 out->y = s->oy + s->sdy / n;
124 out->z = s->oz + s->sdz / n;
125 return true;
126}
127
128double gnss_survey_accuracy_m(const GnssSurvey *s)
129{
130 if (s->count < 2)
131 return 0.0;
132 double n = (double)s->count;
133 double mx = s->sdx / n;
134 double my = s->sdy / n;
135 double mz = s->sdz / n;
136 double vx = s->sdx2 / n - mx * mx; // population variance of the shifted coordinates
137 double vy = s->sdy2 / n - my * my;
138 double vz = s->sdz2 / n - mz * mz;
139 if (vx < 0.0) // clamp tiny negatives from floating-point rounding
140 vx = 0.0;
141 if (vy < 0.0)
142 vy = 0.0;
143 if (vz < 0.0)
144 vz = 0.0;
145 return sqrt(vx + vy + vz);
146}
147
148bool gnss_survey_complete(const GnssSurvey *s, uint32_t min_obs, double acc_limit_m)
149{
150 return s->count >= min_obs && s->count >= 2 && gnss_survey_accuracy_m(s) <= acc_limit_m;
151}
152
153// ---------------------------------------------------------------------------------------------
154// GGA -> geodetic (only when the NMEA 0183 codec is available).
155// ---------------------------------------------------------------------------------------------
156
157#if DETWS_ENABLE_NMEA0183
158
159namespace
160{
161// A GGA lat/lon field is ddmm.mmmm / dddmm.mmmm; split into whole degrees + decimal minutes.
162bool dm_to_deg(const char *field, uint8_t len, double *out)
163{
164 if (!field || len == 0)
165 return false;
166 const char *end = field;
167 double dm = det_strtod(field, &end);
168 if (end == field)
169 return false;
170 double deg = (double)(long)(dm / 100.0); // whole degrees (field is non-negative; sign is N/S,E/W)
171 double minutes = dm - deg * 100.0;
172 *out = deg + minutes / 60.0;
173 return true;
174}
175} // namespace
176
177bool gnss_gga_to_geodetic(const Nmea0183 *m, GnssGeodetic *out)
178{
179 if (!m || !out || m->type[0] != 'G' || m->type[1] != 'G' || m->type[2] != 'A')
180 return false;
181 if (m->field_count < 10) // need through the altitude field (index 9)
182 return false;
183
184 long quality = 0;
185 if (!nmea0183_field_int(m, 6, &quality) || quality <= 0) // 0 = no fix
186 return false;
187
188 double lat = 0.0;
189 double lon = 0.0;
190 if (!dm_to_deg(m->fields[2], m->field_len[2], &lat))
191 return false;
192 if (!dm_to_deg(m->fields[4], m->field_len[4], &lon))
193 return false;
194 if (m->field_len[3] == 1 && (m->fields[3][0] == 'S' || m->fields[3][0] == 's'))
195 lat = -lat;
196 if (m->field_len[5] == 1 && (m->fields[5][0] == 'W' || m->fields[5][0] == 'w'))
197 lon = -lon;
198
199 float msl = 0.0f;
200 if (!nmea0183_field_float(m, 9, &msl)) // orthometric (mean-sea-level) altitude
201 return false;
202 float geoid = 0.0f;
203 nmea0183_field_float(m, 11, &geoid); // geoid separation; absent -> 0 (ellipsoidal height = MSL)
204
205 out->lat_deg = lat;
206 out->lon_deg = lon;
207 out->height_m = (double)msl + (double)geoid; // ellipsoidal height above WGS84
208 return true;
209}
210
211bool gnss_survey_add_gga(GnssSurvey *s, const Nmea0183 *m)
212{
213 GnssGeodetic g;
214 if (!gnss_gga_to_geodetic(m, &g))
215 return false;
216 gnss_survey_add_geodetic(s, &g);
217 return true;
218}
219
220#endif // DETWS_ENABLE_NMEA0183
221
222#endif // DETWS_ENABLE_NTRIP_CASTER
GNSS survey-in: WGS84 geodetic <-> ECEF + fixed-position averaging (DETWS_ENABLE_NTRIP_CASTER).
NMEA 0183 sentence codec (DETWS_ENABLE_NMEA0183) - the marine / GPS ASCII protocol.
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
double det_strtod(const char *s, const char **end)
Parse a double (integer[.frac][e[+/-]exp]); sets end (or to s if none).
Definition numparse.h:99