QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NMEANavigationEpoch.cc
Go to the documentation of this file.
2
3#include <algorithm>
4
5#include "MonotonicClock.h"
6
7namespace {
8std::optional<double> finiteValue(double value)
9{
10 return std::isfinite(value) ? std::optional<double>(value) : std::nullopt;
11}
12
13std::optional<double> nonnegativeNumber(std::string_view field)
14{
15 const auto value = NMEA::number<double>(field);
16 return value && *value >= 0.0 ? value : std::nullopt;
17}
18
19void markReceipt(NMEA::NavigationEpoch& epoch, uint64_t receivedAtUs)
20{
21 epoch.receivedAtUs = epoch.receivedAtUs == 0 ? receivedAtUs : std::min(epoch.receivedAtUs, receivedAtUs);
22}
23} // namespace
24
25namespace NMEA {
26
27std::optional<double> NavigationEpoch::altitudeEllipsoidMeters() const
28{
30 ? std::optional<double>(*altitudeMslMeters + *geoidSeparationMeters)
31 : std::nullopt;
32}
33
35{
36 *this = {};
37}
38
39std::optional<NavigationUpdate> NavigationEpochAssembler::ingest(const Sentence& sentence, uint64_t receivedAtUs)
40{
41 const auto sequence = ++_sequence;
42
43 if (const auto navigation = navigationStatus(sentence)) {
44 if (!navigation->valid) {
45 _navigationValid = false;
46 _invalidThroughSequence = sequence;
47 NavigationEpoch loss;
48 loss.timeMs = navigation->utcMilliseconds;
49 loss.receivedAtUs = receivedAtUs;
50 loss.positionReceivedAtUs = receivedAtUs;
51 loss.sequence = sequence;
52 loss.fixQuality = GPSFixQuality::NoFix;
53 if (sentence.type() == "GGA" && sentence.count > Field::GGA_SATELLITES_USED) {
54 loss.satellitesUsed = number<unsigned>(sentence.fields[Field::GGA_SATELLITES_USED]);
55 }
56 return NavigationUpdate{
58 }
59 _navigationValid = true;
60 }
61
62 if (auto update = _handlePositionSentence(sentence, receivedAtUs)) {
63 return update;
64 }
65 if (auto update = _handleAccuracy(sentence, receivedAtUs)) {
66 return update;
67 }
68 return _handleUntimedMetadata(sentence, receivedAtUs);
69}
70
71std::optional<NavigationEpoch> NavigationEpochAssembler::expireUntimedMetadata(uint64_t nowUs)
72{
73 auto* stored = _current();
74 if (!stored || !stored->epoch.verticalDopReceivedAtUs ||
75 MonotonicClock::withinAge(stored->epoch.verticalDopReceivedAtUs, nowUs, METADATA_MAX_AGE)) {
76 return std::nullopt;
77 }
78 stored->epoch.verticalDop.reset();
79 stored->epoch.verticalDopReceivedAtUs = 0;
80 return stored->epoch;
81}
82
83NavigationEpochAssembler::StoredEpoch& NavigationEpochAssembler::_find(std::optional<int> timeMs)
84{
85 if (auto* existing = _findExisting(timeMs)) {
86 return *existing;
87 }
88
89 auto oldest = _epochs.begin();
90 for (auto epoch = _epochs.begin(); epoch != _epochs.end(); ++epoch) {
91 if (!epoch->active) {
92 oldest = epoch;
93 break;
94 }
95 if (epoch->epoch.receivedAtUs < oldest->epoch.receivedAtUs) {
96 oldest = epoch;
97 }
98 }
99 _resetEpoch(*oldest, timeMs);
100 return *oldest;
101}
102
103NavigationEpochAssembler::StoredEpoch* NavigationEpochAssembler::_findExisting(std::optional<int> timeMs)
104{
105 const int key = timeMs.value_or(NO_TIME_KEY);
106 for (auto& epoch : _epochs) {
107 if (epoch.active && epoch.key == key) {
108 return &epoch;
109 }
110 }
111 return nullptr;
112}
113
114NavigationEpochAssembler::StoredEpoch* NavigationEpochAssembler::_current()
115{
116 if (!_hasCurrent) {
117 return nullptr;
118 }
119 for (auto& epoch : _epochs) {
120 if (epoch.active && epoch.key == _currentKey) {
121 return &epoch;
122 }
123 }
124 return nullptr;
125}
126
127void NavigationEpochAssembler::_resetEpoch(StoredEpoch& stored, std::optional<int> timeMs)
128{
129 stored = {};
130 stored.active = true;
131 stored.key = timeMs.value_or(NO_TIME_KEY);
132 stored.epoch.timeMs = timeMs;
133}
134
135void NavigationEpochAssembler::_resetExpiredEpoch(StoredEpoch& stored, uint64_t receivedAtUs)
136{
137 if (stored.epoch.receivedAtUs && receivedAtUs > stored.epoch.receivedAtUs &&
138 !MonotonicClock::withinAge(stored.epoch.receivedAtUs, receivedAtUs, METADATA_MAX_AGE)) {
139 _resetEpoch(stored, stored.epoch.timeMs);
140 }
141}
142
143NavigationEpoch& NavigationEpochAssembler::_positionEpoch(std::optional<int> timeMs, uint64_t receivedAtUs)
144{
145 auto& stored = _find(timeMs);
146 _resetExpiredEpoch(stored, receivedAtUs);
147 _currentKey = stored.key;
148 _hasCurrent = true;
149 return stored.epoch;
150}
151
152bool NavigationEpochAssembler::_hiddenByFixLoss(const NavigationEpoch& epoch) const
153{
154 return !_navigationValid || epoch.sequence <= _invalidThroughSequence;
155}
156
157std::optional<NavigationUpdate> NavigationEpochAssembler::_handlePositionSentence(const Sentence& sentence,
158 uint64_t receivedAtUs)
159{
160 // Position sentences reach here only with a receiver-declared valid fix; ingest() reports fix loss first.
161 NavigationEpoch* epoch = nullptr;
162 if (const auto rmcFix = rmc(sentence)) {
163 if (!rmcFix->utcMilliseconds) {
164 return std::nullopt;
165 }
166 epoch = &_positionEpoch(rmcFix->utcMilliseconds, receivedAtUs);
167 epoch->latitude = rmcFix->latitude;
168 epoch->longitude = rmcFix->longitude;
169 epoch->speedMetersPerSecond = finiteValue(rmcFix->speedMetersPerSecond);
170 epoch->courseDegrees = finiteValue(rmcFix->courseDegrees);
171 } else if (const auto ggaFix = gga(sentence)) {
172 epoch = &_positionEpoch(utcMilliseconds(sentence.fields[Field::UTC_TIME]), receivedAtUs);
173 epoch->ggaQuality = ggaFix->quality;
174 epoch->latitude = ggaFix->latitude;
175 epoch->longitude = ggaFix->longitude;
176 epoch->altitudeMslMeters = finiteValue(ggaFix->altitude);
177 epoch->geoidSeparationMeters = finiteValue(ggaFix->geoidSeparation);
178 epoch->satellitesUsed = ggaFix->satellitesUsed;
179 epoch->horizontalDop = finiteValue(ggaFix->hdop);
180 if (!epoch->accuracyReceivedAtUs ||
181 !MonotonicClock::withinAge(epoch->accuracyReceivedAtUs, receivedAtUs, METADATA_MAX_AGE)) {
182 epoch->horizontalAccuracyMeters.reset();
183 epoch->verticalAccuracyMeters.reset();
184 epoch->accuracyReceivedAtUs = 0;
185 }
186 } else if (const auto gllFix = gll(sentence)) {
187 if (!gllFix->utcMilliseconds) {
188 return std::nullopt;
189 }
190 epoch = &_positionEpoch(gllFix->utcMilliseconds, receivedAtUs);
191 epoch->latitude = gllFix->latitude;
192 epoch->longitude = gllFix->longitude;
193 } else {
194 return std::nullopt;
195 }
196
197 epoch->fixQuality = fixQuality(epoch->ggaQuality.value_or(GgaQuality::GPS), GPSFixQuality::Fix3D);
198 markReceipt(*epoch, receivedAtUs);
199 epoch->positionReceivedAtUs = receivedAtUs;
200 epoch->sequence = _sequence;
201 return NavigationUpdate{
202 .type = NavigationUpdate::Type::Epoch, .trigger = NavigationUpdate::Trigger::Position, .epoch = *epoch};
203}
204
205std::optional<NavigationUpdate> NavigationEpochAssembler::_handleAccuracy(const Sentence& sentence,
206 uint64_t receivedAtUs)
207{
208 const auto accuracy = gst(sentence);
209 if (!accuracy) {
210 return std::nullopt;
211 }
212 const auto timeMs = utcMilliseconds(sentence.fields[Field::UTC_TIME]);
213 if (!timeMs) {
214 return std::nullopt;
215 }
216 auto& stored = _find(timeMs);
217 _resetExpiredEpoch(stored, receivedAtUs);
218 auto& epoch = stored.epoch;
219 epoch.horizontalAccuracyMeters = finiteValue(accuracy->horizontalAccuracy);
220 epoch.verticalAccuracyMeters = finiteValue(accuracy->verticalAccuracy);
221 epoch.accuracyReceivedAtUs = receivedAtUs;
222 markReceipt(epoch, receivedAtUs);
223 if (!_hasCurrent || _currentKey != stored.key || _hiddenByFixLoss(epoch)) {
224 return std::nullopt;
225 }
226 return NavigationUpdate{
228}
229
230std::optional<NavigationUpdate> NavigationEpochAssembler::_handleUntimedMetadata(const Sentence& sentence,
231 uint64_t receivedAtUs)
232{
233 auto* stored = _current();
234 if (!stored || !stored->epoch.positionReceivedAtUs ||
235 !MonotonicClock::withinAge(stored->epoch.positionReceivedAtUs, receivedAtUs, METADATA_MAX_AGE)) {
236 return std::nullopt;
237 }
238
239 auto& epoch = stored->epoch;
240 if (sentence.type() == "GSA") {
241 if (sentence.count < Field::GSA_MIN_FIELDS) {
242 return std::nullopt;
243 }
244 if (!epoch.horizontalDop) {
245 epoch.horizontalDop = nonnegativeNumber(sentence.fields[Field::GSA_HDOP]);
246 }
247 const auto vdop = nonnegativeNumber(sentence.fields[Field::GSA_VDOP]);
248 epoch.verticalDop = vdop;
249 epoch.verticalDopReceivedAtUs = vdop ? receivedAtUs : 0;
250 } else if (const auto velocity = vtg(sentence)) {
251 epoch.speedMetersPerSecond = finiteValue(velocity->speedMetersPerSecond);
252 epoch.courseDegrees = finiteValue(velocity->courseDegrees);
253 } else {
254 return std::nullopt;
255 }
256 if (_hiddenByFixLoss(epoch)) {
257 return std::nullopt;
258 }
259 return NavigationUpdate{
261}
262
263} // namespace NMEA
static constexpr std::chrono::microseconds METADATA_MAX_AGE
Receipt age beyond which retained metadata no longer joins a new sentence of its epoch.
std::optional< NavigationEpoch > expireUntimedMetadata(uint64_t nowUs)
std::optional< NavigationUpdate > ingest(const Sentence &sentence, uint64_t receivedAtUs)
constexpr bool withinAge(uint64_t timestampUs, uint64_t nowUs, std::chrono::microseconds maximumAge)
Whether a receipt is at most maximumAge old, its limit included.
constexpr size_t GSA_VDOP
constexpr size_t GSA_MIN_FIELDS
constexpr size_t GGA_SATELLITES_USED
constexpr size_t GSA_HDOP
constexpr size_t UTC_TIME
constexpr unsigned GPS
std::optional< Sentence > sentence(std::string_view text)
GPSFixQuality fixQuality(unsigned quality, GPSFixQuality autonomous)
Autonomous GGA does not distinguish 2D/3D; the caller supplies its independently resolved quality.
std::optional< int > utcMilliseconds(std::string_view field)
std::optional< RMC > rmc(const Sentence &input)
std::optional< GLL > gll(const Sentence &input)
std::optional< GGA > gga(const Sentence &input)
Quality zero is a valid fix-loss observation with empty coordinates and optional hemisphere fields.
std::optional< VTG > vtg(const Sentence &input)
std::optional< NavigationStatus > navigationStatus(const Sentence &input)
std::optional< GST > gst(const Sentence &input)
std::optional< double > altitudeEllipsoidMeters() const
std::optional< double > geoidSeparationMeters
std::optional< double > altitudeMslMeters
std::optional< int > timeMs
std::optional< unsigned > satellitesUsed