DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
gnss_survey.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 gnss_survey.h
6 * @brief GNSS survey-in: WGS84 geodetic <-> ECEF + fixed-position averaging (DETWS_ENABLE_NTRIP_CASTER).
7 *
8 * An RTK / DGPS base must advertise its own antenna position precisely. A stationary receiver's
9 * single-epoch fixes scatter by meters, so the base "surveys in": it averages many fixes into one mean
10 * position and only trusts it once enough observations have landed and their spread is small enough. That
11 * mean position, in ECEF, is what the caster puts in an RTCM3 1005/1006 message (see rtcm3.h).
12 *
13 * This is the pure, zero-heap, host-tested core:
14 * - the exact closed-form WGS84 geodetic (lat/lon/ellipsoidal-height) -> ECEF transform and an iterative
15 * inverse (for read-back), matched against pyproj (EPSG:4979 -> EPSG:4978);
16 * - an incremental survey accumulator that keeps a running mean ECEF and a 3-D spread (standard
17 * deviation) using a shifted-origin sum / sum-of-squares, so it is numerically stable at the
18 * ~6.4e6 m magnitude of ECEF coordinates and needs no history buffer;
19 * - a helper to fold a GGA fix straight into the survey (built only when DETWS_ENABLE_NMEA0183 is on).
20 *
21 * @author Douglas Quigg (dstroy0)
22 * @date 2026
23 */
24
25#ifndef DETERMINISTICESPASYNCWEBSERVER_GNSS_SURVEY_H
26#define DETERMINISTICESPASYNCWEBSERVER_GNSS_SURVEY_H
27
28#include "ServerConfig.h"
29
30#if DETWS_ENABLE_NTRIP_CASTER
31
32#include <stddef.h>
33#include <stdint.h>
34
35/** @brief A WGS84 geodetic position: latitude/longitude in degrees, ellipsoidal height in metres. */
36struct GnssGeodetic
37{
38 double lat_deg;
39 double lon_deg;
40 double height_m; ///< height above the WGS84 ellipsoid (NOT mean sea level)
41};
42
43/** @brief An Earth-Centred, Earth-Fixed position in metres (WGS84 / EPSG:4978). */
44struct GnssEcef
45{
46 double x;
47 double y;
48 double z;
49};
50
51/** @brief WGS84 geodetic -> ECEF (exact closed form). */
52void gnss_geodetic_to_ecef(const GnssGeodetic *g, GnssEcef *e);
53
54/** @brief ECEF -> WGS84 geodetic (iterative; converges to sub-millimetre in a few passes). */
55void gnss_ecef_to_geodetic(const GnssEcef *e, GnssGeodetic *g);
56
57/** @brief Round metres to RTCM's 0.1 mm integer units (half away from zero) for rtcm3_build_1005/1006. */
58int64_t gnss_ecef_m_to_01mm(double metres);
59
60/**
61 * @brief Survey-in accumulator: a running mean ECEF and 3-D spread over the fixes fed to it.
62 *
63 * Coordinates are accumulated relative to the first fix (the origin) so the running sums stay small and
64 * the variance is free of the catastrophic cancellation a raw sum-of-squares at ~6.4e6 m would suffer.
65 * Zero heap; carries no per-fix history.
66 */
67struct GnssSurvey
68{
69 bool has_origin; ///< the first fix has been recorded as the shift origin
70 double ox; ///< origin ECEF (metres) - the first fix
71 double oy;
72 double oz;
73 double sdx; ///< sum of (x - origin) over all fixes
74 double sdy;
75 double sdz;
76 double sdx2; ///< sum of (x - origin)^2 over all fixes
77 double sdy2;
78 double sdz2;
79 uint32_t count; ///< number of fixes accumulated
80};
81
82/** @brief Reset a survey to empty (no fixes). */
83void gnss_survey_reset(GnssSurvey *s);
84
85/** @brief Fold one ECEF fix into the survey. */
86void gnss_survey_add_ecef(GnssSurvey *s, const GnssEcef *e);
87
88/** @brief Fold one geodetic fix into the survey (converts to ECEF first). */
89void gnss_survey_add_geodetic(GnssSurvey *s, const GnssGeodetic *g);
90
91/** @brief Number of fixes accumulated so far. */
92uint32_t gnss_survey_count(const GnssSurvey *s);
93
94/** @brief Mean ECEF position. @return false (and leaves @p out untouched) if no fixes yet. */
95bool gnss_survey_mean(const GnssSurvey *s, GnssEcef *out);
96
97/**
98 * @brief 3-D standard deviation of the accumulated fixes, in metres (sqrt of the summed per-axis
99 * population variances). @return 0 with fewer than two fixes.
100 */
101double gnss_survey_accuracy_m(const GnssSurvey *s);
102
103/**
104 * @brief Whether the survey has converged: at least @p min_obs fixes and a 3-D spread within
105 * @p acc_limit_m metres. Mirrors a u-blox TMODE3 survey-in's min-observations / accuracy-limit gate.
106 */
107bool gnss_survey_complete(const GnssSurvey *s, uint32_t min_obs, double acc_limit_m);
108
109#if DETWS_ENABLE_NMEA0183
110struct Nmea0183;
111
112/**
113 * @brief Decode a parsed GGA sentence into a geodetic fix (ellipsoidal height = MSL altitude + geoid
114 * separation). @return false if @p m is not a GGA, the fix quality is 0 (no fix), or a field is
115 * missing / malformed.
116 */
117bool gnss_gga_to_geodetic(const Nmea0183 *m, GnssGeodetic *out);
118
119/** @brief Parse a GGA and, if it carries a valid fix, fold it into the survey. @return true if added. */
120bool gnss_survey_add_gga(GnssSurvey *s, const Nmea0183 *m);
121#endif
122
123#endif // DETWS_ENABLE_NTRIP_CASTER
124
125#endif // DETERMINISTICESPASYNCWEBSERVER_GNSS_SURVEY_H
User-facing configuration for DeterministicESPAsyncWebServer.