QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NMEASatellites.cc
Go to the documentation of this file.
1#include "NMEASatellites.h"
2
3#include <algorithm>
4#include <iterator>
5#include <utility>
6
7#include "NMEASentence.h"
8
9namespace {
10constexpr size_t GSV_PAGE_COUNT_FIELD = 1;
11constexpr size_t GSV_PAGE_NUMBER_FIELD = 2;
12constexpr size_t GSV_SATELLITE_COUNT_FIELD = 3;
13constexpr size_t GSV_FIRST_SATELLITE_FIELD = 4;
14constexpr size_t GSV_SATELLITE_FIELDS = 4;
15constexpr int GSV_SATELLITES_PER_PAGE = 4;
16constexpr int MAX_GSV_PAGES = 64;
17constexpr int MAX_GSV_SATELLITES = MAX_GSV_PAGES * GSV_SATELLITES_PER_PAGE;
18constexpr int MAX_GSV_SIGNAL_ID = 15;
19constexpr int MAX_GSV_SATELLITE_ID = 999;
20
21bool validNavigation(const NMEA::Sentence& input)
22{
23 const auto status = NMEA::navigationStatus(input);
24 if (!status) {
25 return false;
26 }
27 if (input.type() == "GGA") {
28 return NMEA::gga(input).has_value() || (input.count >= NMEA::Field::GGA_MIN_FIELDS && !status->valid);
29 }
30 if (input.type() != "RMC" || input.count < NMEA::Field::RMC_MIN_FIELDS ||
32 return false;
33 }
34 return !status->valid || NMEA::rmc(input).has_value();
35}
36} // namespace
37
38namespace NMEA {
39GPSConstellation satelliteConstellation(std::string_view talker, std::optional<int> systemId,
40 std::optional<int> satelliteId)
41{
42 using Constellation = GPSConstellation;
43 namespace Id = SatelliteIds;
44 constexpr int GPS_SYSTEM_ID = 1;
45 constexpr int GLONASS_SYSTEM_ID = 2;
46 constexpr int GALILEO_SYSTEM_ID = 3;
47 constexpr int BEIDOU_SYSTEM_ID = 4;
48 constexpr int QZSS_SYSTEM_ID = 5;
49 constexpr int NAVIC_SYSTEM_ID = 6;
50 constexpr int SBAS_LAST_PRN = 158;
51 constexpr int QZSS_LEGACY_LAST_NMEA_ID = 200;
52 constexpr int BEIDOU_UNAMBIGUOUS_FIRST_NMEA_ID = Id::QZSS_LAST_NMEA_ID + 1;
53
54 if ((talker == "GP" || (talker == "GN" && (!systemId || *systemId == GPS_SYSTEM_ID))) && satelliteId &&
55 ((*satelliteId >= Id::SBAS_FIRST_NMEA_ID && *satelliteId <= Id::SBAS_LAST_NMEA_ID) ||
56 (*satelliteId >= Id::SBAS_FIRST_PRN && *satelliteId <= SBAS_LAST_PRN))) {
57 return Constellation::SBAS;
58 }
59 if (talker == "GP") {
60 return Constellation::GPS;
61 }
62 if (talker == "GL") {
63 return Constellation::GLONASS;
64 }
65 if (talker == "GA") {
66 return Constellation::Galileo;
67 }
68 if (talker == "GB" || talker == "BD") {
69 return Constellation::BeiDou;
70 }
71 if (talker == "GQ" || talker == "PQ" || talker == "QZ") {
72 return Constellation::QZSS;
73 }
74 if (talker == "GI") {
75 return Constellation::NavIC;
76 }
77 if (talker != "GN") {
78 return Constellation::Unknown;
79 }
80 if (systemId) {
81 switch (*systemId) {
82 case GPS_SYSTEM_ID:
83 return Constellation::GPS;
84 case GLONASS_SYSTEM_ID:
85 return Constellation::GLONASS;
86 case GALILEO_SYSTEM_ID:
87 return Constellation::Galileo;
88 case BEIDOU_SYSTEM_ID:
89 return Constellation::BeiDou;
90 case QZSS_SYSTEM_ID:
91 return Constellation::QZSS;
92 case NAVIC_SYSTEM_ID:
93 return Constellation::NavIC;
94 default:
95 return Constellation::Unknown;
96 }
97 }
98 if (!satelliteId) {
99 return Constellation::Unknown;
100 }
101 const int id = *satelliteId;
102 // SBAS identifiers were classified above, so the remaining low range is GPS.
103 if (id >= Id::FIRST_LOCAL_ID && id < Id::SBAS_FIRST_NMEA_ID) {
104 return Constellation::GPS;
105 }
106 if (id >= Id::GLONASS_FIRST_NMEA_ID && id <= Id::GLONASS_LAST_NMEA_ID) {
107 return Constellation::GLONASS;
108 }
109 if (id >= Id::QZSS_FIRST_NMEA_ID && id <= QZSS_LEGACY_LAST_NMEA_ID) {
110 return Constellation::QZSS;
111 }
112 // Qt's legacy BeiDou range starts at 201; u-blox extended QZSS ends at 202.
113 // Without explicit context, neither interpretation is safe for 201/202.
114 if ((id >= BEIDOU_UNAMBIGUOUS_FIRST_NMEA_ID && id <= Id::BEIDOU_LEGACY_LAST_NMEA_ID) ||
115 (id >= Id::BEIDOU_EXTENDED_FIRST_NMEA_ID && id <= Id::BEIDOU_EXTENDED_LAST_NMEA_ID)) {
116 return Constellation::BeiDou;
117 }
118 if (id >= Id::GALILEO_FIRST_NMEA_ID && id <= Id::GALILEO_LAST_NMEA_ID) {
119 return Constellation::Galileo;
120 }
121 return Constellation::Unknown;
122}
123
124std::optional<GSV> gsv(const Sentence& input)
125{
126 if (input.type() != "GSV" || input.count < GSV_FIRST_SATELLITE_FIELD) {
127 return {};
128 }
129 const auto& f = input.fields;
130 const auto total = number<int>(f[GSV_PAGE_COUNT_FIELD]);
131 const auto message = number<int>(f[GSV_PAGE_NUMBER_FIELD]);
132 const auto count = number<int>(f[GSV_SATELLITE_COUNT_FIELD]);
133 const auto system = satelliteConstellation(input.talker(), {}, {});
134 if (!total || !message || !count || (system == GPSConstellation::Unknown && input.talker() != "GN") || *total < 1 ||
135 *total > MAX_GSV_PAGES || *message < 1 || *message > *total || *count < 0 || *count > MAX_GSV_SATELLITES ||
136 *total != std::max(1, (*count + GSV_SATELLITES_PER_PAGE - 1) / GSV_SATELLITES_PER_PAGE)) {
137 return {};
138 }
139 const int entries = std::min(GSV_SATELLITES_PER_PAGE, *count - (*message - 1) * GSV_SATELLITES_PER_PAGE);
140 const size_t end = GSV_FIRST_SATELLITE_FIELD + entries * GSV_SATELLITE_FIELDS;
141 const size_t fields = input.count - GSV_FIRST_SATELLITE_FIELD;
142 const size_t satelliteSlots = fields / GSV_SATELLITE_FIELDS;
143 const size_t trailingFields = fields % GSV_SATELLITE_FIELDS;
144 if (satelliteSlots < static_cast<size_t>(entries) || satelliteSlots > GSV_SATELLITES_PER_PAGE ||
145 trailingFields > 1) {
146 return {};
147 }
148 const size_t signalIndex = GSV_FIRST_SATELLITE_FIELD + satelliteSlots * GSV_SATELLITE_FIELDS;
149 // Some receivers retain all four satellite slots on short or empty pages.
150 for (size_t index = end; index < signalIndex; ++index) {
151 if (!f[index].empty()) {
152 return {};
153 }
154 }
155 GSV result{system, *total, *message, *count};
156 if (trailingFields == 1 && !f[signalIndex].empty()) {
157 const auto signal = number<int>(f[signalIndex], HEX_BASE);
158 if (!signal || *signal < 0 || *signal > MAX_GSV_SIGNAL_ID) {
159 return {};
160 }
161 result.signal = *signal;
162 }
163 for (size_t index = GSV_FIRST_SATELLITE_FIELD; index < end; index += GSV_SATELLITE_FIELDS) {
164 const auto id = number<int>(f[index]);
165 if (!id || *id < 1 || *id > MAX_GSV_SATELLITE_ID) {
166 return {};
167 }
168 SatelliteData satellite;
169 satellite.constellation = satelliteConstellation(input.talker(), {}, *id);
170 if (satellite.constellation == GPSConstellation::Unknown) {
171 return {};
172 }
173 satellite.id = satelliteId(satellite.constellation, *id);
174 result.satellites.push_back(satellite);
175 }
176 return result;
177}
178
180{
181 _views.clear();
182 _used.clear();
183 _time.reset();
184 _batchStartedUs.reset();
185 _lastAcceptedUs = 0;
186}
187
189{
190 Update update{false, flushDue(receivedAtUs)};
191 const auto complete = [&]() {
192 auto epoch = flush();
193 update.completed.insert(update.completed.end(), std::make_move_iterator(epoch.begin()),
194 std::make_move_iterator(epoch.end()));
195 };
196 if (validNavigation(input)) {
197 const auto time = utcMilliseconds(input.fields[Field::UTC_TIME]);
198 if (time && time != _time) {
199 complete();
200 _time = time;
201 }
202 return update;
203 }
204 if (input.type() == "GSV") {
205 const auto parsed = gsv(input);
206 if (!parsed) {
207 return update;
208 }
209 const auto& page = *parsed;
210 auto& signalReports = _views[page.constellation];
211 if (page.message == 1 && signalReports[page.signal].complete()) {
212 complete();
213 }
214 auto& report = _views[page.constellation][page.signal];
215 if (page.message == 1) {
216 report = {page.messages, page.satelliteCount, 1, receivedAtUs, {}};
217 }
218 if (report.messages != page.messages || report.count != page.satelliteCount || report.next != page.message) {
219 _views[page.constellation].erase(page.signal);
220 return update;
221 }
222 report.satellites.insert(report.satellites.end(), page.satellites.begin(), page.satellites.end());
223 report.timestamp = std::min(report.timestamp, receivedAtUs);
224 ++report.next;
225 update.accepted = true;
226 } else if (input.type() == "GSA" && input.count >= Field::GSA_MIN_FIELDS) {
227 const auto& f = input.fields;
228 const auto fix = number<unsigned>(f[Field::GSA_DIMENSION]);
229 if (!fix || *fix < FixDimension::NO_FIX || *fix > FixDimension::THREE_D) {
230 return update;
231 }
232 std::optional<int> system;
233 if (input.talker() == "GN" && input.count > Field::GSA_SYSTEM_ID && !f[Field::GSA_SYSTEM_ID].empty()) {
234 system = number<int>(f[Field::GSA_SYSTEM_ID], HEX_BASE);
235 if (!system) {
236 return update;
237 }
238 }
239 std::map<GPSConstellation, Used> reports;
240 const auto explicitSystem = satelliteConstellation(input.talker(), system, {});
241 if (explicitSystem != GPSConstellation::Unknown) {
242 reports[explicitSystem].timestamp = receivedAtUs;
243 }
244 if (explicitSystem == GPSConstellation::GPS) {
245 reports[GPSConstellation::SBAS].timestamp = receivedAtUs;
246 }
248 ++index) {
249 if (f[index].empty()) {
250 continue;
251 }
252 const auto id = number<int>(f[index]);
253 if (!id || *id <= 0) {
254 return update;
255 }
256 const auto constellation = satelliteConstellation(input.talker(), system, id);
257 if (constellation == GPSConstellation::Unknown) {
258 return update;
259 }
260 auto& report = reports[constellation];
261 report.timestamp = receivedAtUs;
262 if (*fix != FixDimension::NO_FIX) {
263 report.ids.insert(satelliteId(constellation, *id));
264 }
265 }
266 if (reports.empty()) {
267 return update;
268 }
269 if (!_views.empty() && std::any_of(reports.begin(), reports.end(),
270 [this](const auto& report) { return _used.contains(report.first); })) {
271 complete();
272 }
273 for (auto& [constellation, report] : reports) {
274 _used[constellation] = std::move(report);
275 }
276 update.accepted = true;
277 }
278 if (update.accepted) {
279 if (!_batchStartedUs) {
280 _batchStartedUs = receivedAtUs;
281 }
282 _lastAcceptedUs = receivedAtUs;
283 }
284 return update;
285}
286
287std::optional<uint64_t> SatelliteAssembler::deadlineUs() const
288{
289 if (!_batchStartedUs) {
290 return std::nullopt;
291 }
292 return std::min(*_batchStartedUs + static_cast<uint64_t>(BATCH_TIMEOUT.count()),
293 _lastAcceptedUs + static_cast<uint64_t>(IDLE_TIMEOUT.count()));
294}
295
297{
298 const auto deadline = deadlineUs();
299 return deadline && nowUs >= *deadline ? flush() : SatelliteEpoch{};
300}
301
303{
304 _batchStartedUs.reset();
305 std::map<GPSConstellation, SatelliteSystem> systems;
306 std::map<GPSConstellation, std::set<int>> satellites;
307 const auto includeView = [&systems](GPSConstellation constellation, uint64_t timestamp) {
308 auto& out = systems[constellation];
309 out.constellation = constellation;
310 out.inViewTimestampUs = out.inViewTimestampUs ? std::min(out.inViewTimestampUs, timestamp) : timestamp;
311 };
312 for (const auto& [system, signalReports] : _views) {
313 for (const auto& [signal, report] : signalReports) {
314 if (!report.complete()) {
315 continue;
316 }
317 // GN identifies coverage only through its entries, including across pages and signals.
318 if (system != GPSConstellation::Unknown) {
319 includeView(system, report.timestamp);
320 if (system == GPSConstellation::GPS) {
321 includeView(GPSConstellation::SBAS, report.timestamp);
322 }
323 }
324 for (const auto& satellite : report.satellites) {
325 includeView(satellite.constellation, report.timestamp);
326 satellites[satellite.constellation].insert(satellite.id);
327 }
328 }
329 }
330 for (auto& [system, view] : satellites) {
331 systems[system].inView = static_cast<int>(view.size());
332 }
333 for (const auto& [system, used] : _used) {
334 auto& out = systems[system];
335 out.constellation = system;
336 out.inUseTimestampUs = used.timestamp;
337 out.inUse = static_cast<int>(used.ids.size());
338 }
339 SatelliteEpoch result;
340 for (auto& [system, report] : systems) {
341 result.push_back(std::move(report));
342 }
343 _views.clear();
344 _used.clear();
345 return result;
346}
347} // namespace NMEA
GPSConstellation
std::optional< uint64_t > deadlineUs() const
Update ingest(const Sentence &input, uint64_t receivedAtUs)
static constexpr std::chrono::microseconds BATCH_TIMEOUT
SatelliteEpoch flushDue(uint64_t nowUs)
static constexpr std::chrono::microseconds IDLE_TIMEOUT
constexpr size_t GSA_DIMENSION
constexpr size_t GSA_SATELLITE_SLOTS
constexpr size_t RMC_MIN_FIELDS
constexpr size_t RMC_DATE
constexpr size_t GSA_MIN_FIELDS
constexpr size_t GSA_SYSTEM_ID
constexpr size_t GSA_FIRST_SATELLITE
constexpr size_t GGA_MIN_FIELDS
constexpr size_t UTC_TIME
constexpr unsigned THREE_D
constexpr unsigned NO_FIX
std::optional< GSV > gsv(const Sentence &input)
GPSConstellation satelliteConstellation(std::string_view talker, std::optional< int > systemId, std::optional< int > satelliteId)
constexpr int HEX_BASE
std::optional< UtcDate > rmcDate(std::string_view field)
std::optional< int > utcMilliseconds(std::string_view field)
std::vector< SatelliteSystem > SatelliteEpoch
std::optional< RMC > rmc(const Sentence &input)
int satelliteId(GPSConstellation constellation, int wireId)
Canonical constellation-local identifier; preserve unknown/vendor ranges unchanged.
std::optional< GGA > gga(const Sentence &input)
Quality zero is a valid fix-loss observation with empty coordinates and optional hemisphere fields.
std::optional< NavigationStatus > navigationStatus(const Sentence &input)
GPSConstellation constellation
std::string_view type() const
std::array< std::string_view, MAX_FIELDS > fields
std::string_view talker() const