QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
AshtechProtocol.cc
Go to the documentation of this file.
1#include <algorithm>
2#include <chrono>
3#include <cmath>
4#include <cstdint>
5#include <optional>
6#include <string_view>
7#include <variant>
8
9#include <QtCore/QByteArray>
10#include <QtCore/QString>
11
12#include "Ashtech/AshtechPlan.h"
13#include "GPSCommandChannel.h"
15#include "GPSProtocolMath.h"
16#include "GPSReceiverConfig.h"
17#include "GPSReceiverFamilies.h"
18#include "GPSReceiverReports.h"
19#include "GPSStreamDemux.h"
20#include "GPSSurveyClock.h"
21#include "MonotonicClock.h"
22#include "NMEASentence.h"
23#include "QGCLoggingCategory.h"
24
25QGC_LOGGING_CATEGORY(AshtechProtocolLog, "GPS.Protocols.Ashtech")
26
27namespace {
28
29namespace Plan = Ashtech::Plan;
30
32template <typename T>
33std::optional<T> numberOr(std::string_view field, T fallback)
34{
35 if (field.empty()) {
36 return fallback;
37 }
38 return NMEA::number<T>(field);
39}
40
41uint64_t utcUs(std::chrono::year_month_day date, int milliseconds)
42{
43 return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::sys_days(date).time_since_epoch() +
44 std::chrono::milliseconds(milliseconds))
45 .count();
46}
47
49uint64_t utcAtTimeOfDay(uint64_t referenceUtcUs, uint64_t referenceReceiptUs, std::optional<int> timeMs, uint64_t nowUs,
50 std::chrono::microseconds maximumAge)
51{
52 if (!referenceUtcUs || !timeMs || !MonotonicClock::withinAge(referenceReceiptUs, nowUs, maximumAge)) {
53 return 0;
54 }
55 constexpr int64_t DAY_US = 86400000000;
56 const auto reference = static_cast<int64_t>(referenceUtcUs);
57 int64_t result = reference - reference % DAY_US + int64_t(*timeMs) * 1000;
58 if (result - reference > DAY_US / 2) {
59 result -= DAY_US;
60 } else if (reference - result > DAY_US / 2) {
61 result += DAY_US;
62 }
63 return result > 0 ? static_cast<uint64_t>(result) : 0;
64}
65
66void applyGGA(GPSDecodedPosition& report, const NMEA::GGA& fix, uint64_t receivedAtUs)
67{
72 report.navigation.horizontalDop = fix.hdop;
74 report.navigation.fixType = NMEA::fixQuality(fix.quality, GPSFixQuality::Fix3D);
75 report.navigation.timestampUs = receivedAtUs;
76 report.velocityValid = false;
77}
78
79std::optional<uint64_t> receiptUtc(std::string_view date, std::string_view time)
80{
81 if (date.size() != 10 || date[2] != '.' || date[5] != '.') {
82 return std::nullopt;
83 }
84 const auto day = NMEA::number<unsigned>(date.substr(0, 2));
85 const auto month = NMEA::number<unsigned>(date.substr(3, 2));
86 const auto year = NMEA::number<int>(date.substr(6, 4));
87 const auto milliseconds = NMEA::utcMilliseconds(time);
88 if (!day || !month || !year || *year < 1980 || *year > 9999 || !milliseconds) {
89 return std::nullopt;
90 }
91 const std::chrono::year_month_day calendar{std::chrono::year{*year}, std::chrono::month{*month},
92 std::chrono::day{*day}};
93 if (!calendar.ok()) {
94 return std::nullopt;
95 }
96 return utcUs(calendar, *milliseconds);
97}
98
108class Protocol final : public GPSNMEAFamilyProtocol
109{
110public:
111 Protocol()
112 : GPSNMEAFamilyProtocol(GPSNMEAStream::Navigation::ReceiverSpecific)
113 {}
114
115 bool configure(GPSCommandChannel& channel, GPSConfig config, unsigned& baud) override;
116
118 QString identity() const override { return _board; }
119
121 void serviceStreaming(GPSCommandChannel& channel) override;
122
124 bool probe(GPSCommandChannel& channel) override { return _queryPort(channel, Plan::PORT_QUERY_ATTEMPTS); }
125
126private:
128 struct EpochReceipt
129 {
130 std::optional<int> time;
131 uint64_t receivedAtUs = 0;
132
133 [[nodiscard]] bool matches(const EpochReceipt& position, std::chrono::microseconds maximumAge) const
134 {
135 return time && time == position.time &&
136 MonotonicClock::withinAge(receivedAtUs, position.receivedAtUs, maximumAge);
137 }
138 };
139
141 void _reset(GPSStreamDemux& stream);
143 bool _queryPort(GPSCommandChannel& channel, unsigned attempts);
145 GPSReceiveUpdates _decodeLine(std::string_view line, GPSDecodeContext& context) override;
146
148 void _expire(GPSDecodeContext& context) override { _expireMetadata(context.nowUs()); }
149
150 void _setUpBase(GPSCommandChannel& channel);
151 void _activateRTCMOutput(GPSCommandChannel& channel);
152 GPSCommandOutcome _portReply(std::string_view reply);
153 GPSCommandOutcome _boardReply(std::string_view reply);
155 void _requestSurveyReceipts(bool requested);
157 std::optional<GPSReceiveUpdates> _decodeVendor(std::string_view line, GPSDecodeContext& context);
158 std::optional<GPSReceiveUpdates> _handleTime(const NMEA::Sentence& sentence, uint64_t nowUs);
159 std::optional<GPSReceiveUpdates> _handleGGA(const NMEA::Sentence& sentence, uint64_t nowUs);
160 std::optional<GPSReceiveUpdates> _handlePosition(const NMEA::Sentence& sentence, uint64_t nowUs);
161 std::optional<GPSReceiveUpdates> _handleAccuracy(const NMEA::Sentence& sentence, uint64_t nowUs);
162 std::optional<GPSReceiveUpdates> _handleSurveyReceipt(const NMEA::Sentence& sentence, GPSDecodeContext& context);
163 void _expireMetadata(uint64_t nowUs);
164 void _applyMetadata(std::optional<int> time, uint64_t nowUs);
165
167 char _port = 'A';
169 QString _board;
172 GPSBaseStationConfig _base{};
174 bool _baseSetupDone = false;
176 bool _baseSetupPending = false;
178 bool _rtcmActivationPending = false;
180 bool _awaitingSurveyReceipt = false;
181 GPSSurveyClock _surveyClock;
183 bool _receiverPositions = false;
184 uint64_t _utcReceivedUs = 0;
185 uint64_t _utcReference = 0;
186 EpochReceipt _positionEpoch;
187 EpochReceipt _accuracyReceipt;
188 NMEA::GST _accuracy;
189 bool _surveyReceiptRequested = false;
190 std::optional<uint64_t> _surveyReceiptStartUtc;
191};
192
193bool Protocol::configure(GPSCommandChannel& channel, GPSConfig config, unsigned& baud)
194{
195 _reset(channel.stream());
196 _base = config.base;
197 const auto detection = channel.detectBaud(Plan::BAUD_RATES, baud, [this, &channel]() -> GPSBaudProbe {
198 const bool answered = _queryPort(channel, Plan::PORT_QUERY_ATTEMPTS);
199 return answered ? GPSBaudProbe::Found : GPSBaudProbe::TryNext;
200 });
201 if (!detection.found) {
202 if (_portQuery.outcome == GPSCommandOutcome::Rejected) {
203 channel.failSequence(_portQuery);
204 } else {
205 channel.failNoAnswer(detection.probed);
206 }
207 return false;
208 }
209
210 baud = detection.baud;
211 if (baud != Plan::LINK_BAUD) {
212 baud = Plan::LINK_BAUD;
213 const QByteArray speed = Plan::line(Plan::LINK_SPEED, _port);
214 (void) channel.writeCommand({speed.trimmed(), Plan::RESPONSE_TIMEOUT}, speed);
215 _nmea.reset(channel.stream());
217 _nmea.reset(channel.stream());
218 (void) channel.setBaudrate(baud);
219 if (!_queryPort(channel, Plan::PORT_QUERY_ATTEMPTS_AT_LINK_BAUD)) {
220 channel.failNoAnswer({&baud, 1});
221 return false;
222 }
223 }
224
226 [this](std::string_view reply) { return _boardReply(reply); })}};
227 if (!channel.runRequired(board)) {
228 return false;
229 }
230 if (!_board.startsWith(QLatin1StringView(Plan::BASE_BOARD))) {
231 channel.failControl(
232 QStringLiteral("Trimble %1 cannot run as an RTK base station; only the MB-Two can").arg(_board));
233 return false;
234 }
235 (void) channel.runSequence(Plan::sequence(Plan::OUTPUTS, _port));
236
237 _nmea.setRTCMEnabled(true);
238 channel.context().publishSurvey(true, false, {});
239 _ready = true;
240 return !channel.failed();
241}
242
243void Protocol::serviceStreaming(GPSCommandChannel& channel)
244{
245 if (_baseSetupPending) {
246 _baseSetupPending = false;
247 _setUpBase(channel);
248 }
249 if (_rtcmActivationPending) {
250 _rtcmActivationPending = false;
251 _activateRTCMOutput(channel);
252 }
253}
254
255bool Protocol::_queryPort(GPSCommandChannel& channel, unsigned attempts)
256{
258 [this](std::string_view reply) { return _portReply(reply); });
259 query.attempts = attempts;
260 const GPSCommandSequence sequence{{std::move(query)}};
261 _portQuery = channel.runSequence(sequence);
262 return _portQuery.succeeded();
263}
264
265void Protocol::_setUpBase(GPSCommandChannel& channel)
266{
267 if (_baseSetupDone) {
268 return;
269 }
270 GPSDecodeContext& context = channel.context();
271 if (const auto* fixed = std::get_if<GPSBaseStationConfig::Fixed>(&_base.mode)) {
272 if (!channel.runRequired({{Plan::fixedPositionCommand(fixed->position)}})) {
273 return;
274 }
275 _activateRTCMOutput(channel);
276 if (channel.failed()) {
277 return;
278 }
279 context.publishSurvey(false, true, {}, fixed->position);
280 } else if (const auto* survey = std::get_if<GPSBaseStationConfig::SurveyIn>(&_base.mode)) {
281 // Decoding acknowledges the command from its start receipt; the matcher only sees a NAK.
282 const GPSCommandSequence start{
284 _requestSurveyReceipts(true);
285 _awaitingSurveyReceipt = true;
286 const bool started = channel.runSequence(start).succeeded();
287 _awaitingSurveyReceipt = false;
288 if (!started) {
289 _requestSurveyReceipts(false);
290 channel.failControl(QStringLiteral("No matching Ashtech survey-start receipt"));
291 return;
292 }
293 if (!channel.runRequired(Plan::sequence(Plan::STATION, _port))) {
294 return;
295 }
296 // A finish receipt may already have arrived with the station commands' replies.
297 if (!_rtcmActivationPending) {
298 _surveyClock.start(context);
299 }
300 }
301 _baseSetupDone = true;
302}
303
304void Protocol::_activateRTCMOutput(GPSCommandChannel& channel)
305{
306 (void) channel.runRequired(Plan::sequence(Plan::RTCM_OUTPUTS, _port));
307}
308
309GPSCommandOutcome Protocol::_portReply(std::string_view reply)
310{
311 if (!reply.starts_with("$PASHR,PRT,") || std::count(reply.begin(), reply.end(), ',') != 3) {
312 return Plan::rejection(reply);
313 }
314 _port = reply[11];
316}
317
318GPSCommandOutcome Protocol::_boardReply(std::string_view reply)
319{
320 constexpr std::string_view PREFIX = "$PASHR,RID,";
321 if (!reply.starts_with(PREFIX)) {
322 return Plan::rejection(reply);
323 }
324 const std::string_view board = reply.substr(PREFIX.size());
325 _board = QString::fromLatin1(board.substr(0, board.find_first_of(",*")));
327}
328
329void Protocol::_reset(GPSStreamDemux& stream)
330{
331 _base = {};
332 _board.clear();
333 _ready = false;
334 _baseSetupDone = false;
335 _baseSetupPending = false;
336 _rtcmActivationPending = false;
337 _awaitingSurveyReceipt = false;
338 _surveyClock = {};
339 _receiverPositions = false;
340 _utcReceivedUs = 0;
341 _utcReference = 0;
342 _positionEpoch = {};
343 _accuracyReceipt = {};
344 _accuracy = {};
345 _requestSurveyReceipts(false);
346 _nmea.setRTCMEnabled(false);
347 _nmea.reset(stream);
348}
349
350GPSReceiveUpdates Protocol::_decodeLine(std::string_view line, GPSDecodeContext& context)
351{
352 const auto vendor = _decodeVendor(line, context);
353 if (vendor) {
354 _surveyClock.publishProgress(context);
355 }
356 return vendor.value_or(GPSReceiveUpdates{});
357}
358
359void Protocol::_requestSurveyReceipts(bool requested)
360{
361 _surveyReceiptRequested = requested;
362 _surveyReceiptStartUtc.reset();
363}
364
365std::optional<GPSReceiveUpdates> Protocol::_decodeVendor(std::string_view message, GPSDecodeContext& context)
366{
367 const auto sentence = NMEA::sentence(message);
368 if (!sentence || message.size() < 7) {
369 return std::nullopt;
370 }
371
372 const uint64_t now = context.nowUs();
373 const auto commas = std::count(message.begin(), message.end(), ',');
374 const auto type = message.substr(3, 3);
375 if (type == "ZDA" && commas == 6) {
376 return _handleTime(*sentence, now);
377 }
378 if (type == "GGA" && commas == 14 && !_receiverPositions) {
379 return _handleGGA(*sentence, now);
380 }
381 if (message.starts_with("$PASHR,POS,") && commas == 18) {
382 return _handlePosition(*sentence, now);
383 }
384 if (type == "GST" && commas == 8) {
385 return _handleAccuracy(*sentence, now);
386 }
387 if (message.starts_with("$PASHR,RECEIPT,")) {
388 return _handleSurveyReceipt(*sentence, context);
389 }
390 context.offerReply(message);
391 return GPSReceiveUpdates{};
392}
393
394std::optional<GPSReceiveUpdates> Protocol::_handleTime(const NMEA::Sentence& sentence, uint64_t nowUs)
395{
396 const auto zda = NMEA::zda(sentence);
397 if (!zda || !zda->utcMilliseconds || zda->date.year < 1980 || zda->date.year > 9999) {
398 return std::nullopt;
399 }
400 const auto& date = zda->date;
401 const std::chrono::year_month_day calendar{std::chrono::year{date.year}, std::chrono::month{date.month},
402 std::chrono::day{date.day}};
403 const uint64_t utc = utcUs(calendar, *zda->utcMilliseconds);
404 _utcReference = utc / 1000000 > static_cast<uint64_t>(GPSProtocolMath::UTC_PLAUSIBILITY_FLOOR_SECS) ? utc : 0;
405 _nmea.position().navigation.utcTimeUs = _utcReference;
406 _utcReceivedUs = nowUs;
407 return GPSReceiveUpdates{};
408}
409
410std::optional<GPSReceiveUpdates> Protocol::_handleGGA(const NMEA::Sentence& sentence, uint64_t nowUs)
411{
412 const auto fix = NMEA::gga(sentence);
413 if (!fix) {
414 return std::nullopt;
415 }
416 applyGGA(_nmea.position(), *fix, nowUs);
417 _applyMetadata(NMEA::utcMilliseconds(sentence.fields[NMEA::Field::UTC_TIME]), nowUs);
419}
420
421std::optional<GPSReceiveUpdates> Protocol::_handlePosition(const NMEA::Sentence& sentence, uint64_t nowUs)
422{
423 /*
424 Example
425 $PASHR,POS,2,10,125410.00,5525.8138702,N,03833.9587380,E,131.555,1.0,0.0,0.007,-0.001,2.0,1.0,1.7,1.0,*34
426
427 $PASHR,POS,d1,d2,m3,m4,c5,m6,c7,f8,f9,f10,f11,f12,f13,f14,f15,f16,s17*cc
428 Parameter Description Range
429 d1 Position mode 0: standalone
430 1: differential
431 2: RTK float
432 3: RTK fixed
433 5: Dead reckoning
434 9: SBAS (see NPT setting)
435 d2 Number of satellite used in position fix 0-99
436 m3 Current UTC time of position fix (hhmmss.ss) 000000.00-235959.99
437 m4 Latitude of position (ddmm.mmmmmm) 0-90 degrees 00-59.9999999 minutes
438 c5 Latitude sector N, S
439 m6 Longitude of position (dddmm.mmmmmm) 0-180 degrees 00-59.9999999 minutes
440 c7 Longitude sector E,W
441 f8 Altitude above ellipsoid +9999.000
442 f9 Differential age (data link age), seconds 0.0-600.0
443 f10 True track/course over ground in degrees 0.0-359.9
444 f11 Speed over ground in knots 0.0-999.9
445 f12 Vertical velocity in decimeters per second +999.9
446 f13 PDOP 0-99.9
447 f14 HDOP 0-99.9
448 f15 VDOP 0-99.9
449 f16 TDOP 0-99.9
450 s17 Reserved no data
451 *cc Checksum
452 */
453 // d1 is the third field, after $PASHR and POS. An empty field keeps its default; a malformed one rejects the
454 // sentence. Ashtech reports an empty coordinate (latitude, longitude or altitude) until it has a fix.
455 const auto& fields = sentence.fields;
456 const auto quality = numberOr(fields[2], 0);
457 const auto satellites = numberOr(fields[3], 0);
458 const auto latitude = numberOr(fields[5], 0.0);
459 const auto longitude = numberOr(fields[7], 0.0);
460 const auto altitude = numberOr(fields[9], 0.0);
461 const auto trackDegrees = numberOr(fields[11], 0.0);
462 const auto groundSpeedKnots = numberOr(fields[12], 0.0);
463 const auto horizontalDop = numberOr(fields[15], 99.9);
464 const auto verticalDop = numberOr(fields[16], 99.9);
465 // Time, differential age, vertical velocity, PDOP and TDOP are validated but not reported.
466 for (const size_t unused : {4, 10, 13, 14, 17}) {
467 if (!numberOr(fields[unused], 0.0)) {
468 return std::nullopt;
469 }
470 }
471 const std::string_view northSouth = fields[6];
472 const std::string_view eastWest = fields[8];
473 if (!quality || *quality < 0 || *quality > 23 || !satellites || *satellites < 0 || *satellites > 255 || !latitude ||
474 *latitude < 0 || *latitude > 9000 || !longitude || *longitude < 0 || *longitude > 18000 || !altitude ||
475 !trackDegrees || !groundSpeedKnots || !horizontalDop || !verticalDop ||
476 (northSouth != "N" && northSouth != "S") || (eastWest != "E" && eastWest != "W")) {
477 return std::nullopt;
478 }
479 const bool coordinatesFound = !fields[5].empty() && !fields[7].empty() && !fields[9].empty();
480
481 auto& navigation = _nmea.position().navigation;
482 navigation.latitudeDegrees = (northSouth == "S" ? -1 : 1) * NMEA::degreesFromDegreesMinutes(*latitude);
483 navigation.longitudeDegrees = (eastWest == "W" ? -1 : 1) * NMEA::degreesFromDegreesMinutes(*longitude);
484 navigation.altitudeEllipsoidMeters = *altitude;
485 navigation.altitudeMslMeters = NAN;
486 navigation.horizontalDop = static_cast<float>(*horizontalDop);
487 navigation.verticalDop = static_cast<float>(*verticalDop);
488
489 if (!coordinatesFound) {
490 navigation.fixType = GPSPositionReport::FixType::NoFix;
491
492 } else {
493 if (*quality == 9 || *quality == 10) { // SBAS differential or BeiDou differential
494 navigation.fixType = GPSPositionReport::FixType::Differential;
495
496 } else if (*quality == 12 || *quality == 22) { // RTK float or RTK float dithered
497 navigation.fixType = GPSPositionReport::FixType::RTKFloat;
498
499 } else if (*quality == 13 || *quality == 23) { // RTK fixed or RTK fixed dithered
500 navigation.fixType = GPSPositionReport::FixType::RTKFixed;
501
502 } else {
503 navigation.fixType = gpsFixQualityFromValue(3 + *quality);
504 }
505
506 _receiverPositions = true;
507 // The first valid position sets up the configured receiver as a base station.
508 if (_ready && !_baseSetupDone) {
509 _baseSetupPending = true;
510 }
511 }
512
513 navigation.timestampUs = nowUs;
514 _applyMetadata(NMEA::utcMilliseconds(sentence.fields[4]), nowUs);
515 navigation.satellitesUsed = gpsSatellitesUsed(static_cast<unsigned>(*satellites));
516
517 navigation.speedMetersPerSecond = static_cast<float>(*groundSpeedKnots) / 1.9438445f;
518 // Course over ground (the direction of movement, not the heading).
519 navigation.courseRadians = static_cast<float>(*trackDegrees * GPSProtocolMath::DEG_TO_RAD);
520 _nmea.position().velocityValid = true;
522}
523
524std::optional<GPSReceiveUpdates> Protocol::_handleAccuracy(const NMEA::Sentence& sentence, uint64_t nowUs)
525{
526 const auto error = NMEA::gst(sentence);
527 if (!error) {
528 return std::nullopt;
529 }
530 _accuracy = *error;
531 _accuracyReceipt = {NMEA::utcMilliseconds(sentence.fields[NMEA::Field::UTC_TIME]), nowUs};
532 if (_positionEpoch.matches(_accuracyReceipt, STATUS_MAX_AGE)) {
533 _expireMetadata(nowUs);
534 _nmea.position().navigation.horizontalAccuracyMeters = _accuracy.horizontalAccuracy;
535 _nmea.position().navigation.verticalAccuracyMeters = _accuracy.verticalAccuracy;
537 }
538 return GPSReceiveUpdates{};
539}
540
541std::optional<GPSReceiveUpdates> Protocol::_handleSurveyReceipt(const NMEA::Sentence& sentence,
542 GPSDecodeContext& context)
543{
544 if (sentence.count < 9) {
545 return std::nullopt;
546 }
547 const auto& fields = sentence.fields;
548 if (fields[2] != "POS" || fields[3] != "AVG") {
549 return std::nullopt;
550 }
551 const bool started = fields[4] == "STARTED";
552 const bool failed = !started && fields[8] == "ERR";
553 double latitude = NAN;
554 double longitude = NAN;
555 std::optional<uint64_t> receiptTime;
556 std::optional<uint32_t> interval;
557
558 if (started) {
559 interval = NMEA::number<uint32_t>(fields[6]);
560 receiptTime = receiptUtc(fields[8], fields[7]);
561 if (sentence.count != 9 || fields[5] != "INTERVAL" || !interval || *interval == 0 || !receiptTime) {
562 return std::nullopt;
563 }
564 } else {
565 interval = NMEA::number<uint32_t>(fields[4]);
566 receiptTime = receiptUtc(fields[7], fields[6]);
567 if (!interval || *interval == 0 || fields[5] != "FINISHED" || !receiptTime ||
568 sentence.count != (failed ? 9 : 16)) {
569 return std::nullopt;
570 }
571 if (!failed) {
572 const auto lat = NMEA::coordinate(fields[8], fields[9], true);
573 const auto lon = NMEA::coordinate(fields[10], fields[11], false);
574 const auto alt = NMEA::number<float>(fields[12]);
575 const auto duration = NMEA::number<double>(fields[15]);
576 if (!lat || !lon || !alt || fields[13] != "OK" || fields[14].empty() || !duration || *duration < 0) {
577 return std::nullopt;
578 }
579 latitude = *lat;
580 longitude = *lon;
581 }
582 }
583
584 const auto* survey = std::get_if<GPSBaseStationConfig::SurveyIn>(&_base.mode);
585 if (!_ready || !survey || !_surveyReceiptRequested) {
586 return std::nullopt;
587 }
588 if (*interval != survey->duration.count()) {
589 return std::nullopt;
590 }
591 const uint64_t now = context.nowUs();
592 const bool awaitingReceipt = _awaitingSurveyReceipt && context.replyPending();
593 if (started) {
594 if (!awaitingReceipt || _surveyReceiptStartUtc) {
595 return std::nullopt;
596 }
597 if (_utcReference && MonotonicClock::withinAge(_utcReceivedUs, now, STATUS_MAX_AGE) &&
598 (*receiptTime < _utcReference ||
599 std::chrono::microseconds(*receiptTime - _utcReference) > STATUS_MAX_AGE)) {
600 return std::nullopt;
601 }
602 _surveyReceiptStartUtc = receiptTime;
603 } else {
604 if (!_surveyReceiptStartUtc || *receiptTime < *_surveyReceiptStartUtc ||
605 (!failed && *receiptTime - *_surveyReceiptStartUtc < uint64_t(*interval) * 1000000)) {
606 return std::nullopt;
607 }
608 _requestSurveyReceipts(false);
609 }
610 if (awaitingReceipt) {
612 }
613 if (!started) {
614 // The survey receipt's height is validated but not reported.
615 _surveyClock.finish(context, !failed, {.latitudeDegrees = latitude, .longitudeDegrees = longitude});
616 _rtcmActivationPending = !failed;
617 }
618 return GPSReceiveUpdates{};
619}
620
621void Protocol::_expireMetadata(uint64_t nowUs)
622{
623 auto& navigation = _nmea.position().navigation;
624 if (!_accuracyReceipt.time || !MonotonicClock::withinAge(_accuracyReceipt.receivedAtUs, nowUs, STATUS_MAX_AGE)) {
625 navigation.horizontalAccuracyMeters = NAN;
626 navigation.verticalAccuracyMeters = NAN;
627 }
628 if (!_utcReference || !MonotonicClock::withinAge(_utcReceivedUs, nowUs, STATUS_MAX_AGE)) {
629 navigation.utcTimeUs = 0;
630 }
631}
632
633void Protocol::_applyMetadata(std::optional<int> time, uint64_t nowUs)
634{
635 _expireMetadata(nowUs);
636 _positionEpoch = {time, nowUs};
637 const bool matches = _accuracyReceipt.matches(_positionEpoch, STATUS_MAX_AGE);
638 auto& navigation = _nmea.position().navigation;
639 navigation.horizontalAccuracyMeters = matches ? _accuracy.horizontalAccuracy : NAN;
640 navigation.verticalAccuracyMeters = matches ? _accuracy.verticalAccuracy : NAN;
641 navigation.utcTimeUs = utcAtTimeOfDay(_utcReference, _utcReceivedUs, time, nowUs, STATUS_MAX_AGE);
642}
643
644QLatin1StringView signature(const GPSFrame& frame)
645{
646 const std::string_view text = frame.text();
647 return frame.kind == GPSFrameKind::ASCIILine && text.starts_with("$PASHR,") && NMEA::sentence(text)
648 ? QLatin1StringView("$PASHR sentences")
649 : QLatin1StringView();
650}
651
652} // namespace
653
654namespace Ashtech {
655
657 .type = GPSType::trimble,
658 .logCategory = &AshtechProtocolLog,
659 .stream = GPSNMEAStream::STREAM,
660 .baudCandidates = Plan::BAUD_RATES,
661 // Unless a rate is selected, configure at the Trimble default rather than probing BAUD_RATES.
662 .autoBaudRate = Plan::LINK_BAUD,
663 .create = &gpsCreateProtocol<Protocol>,
664 .signature = &signature,
665};
666
667} // namespace Ashtech
GPSBaudProbe
GPSCommandOutcome
Definition GPSCommand.h:33
@ Protocol
The receiver violated or rejected the control protocol.
GPSCommandSequence::Command gpsCommand(QByteArray wire, std::chrono::milliseconds timeout, std::variant< GPSTextMatcher, GPSCommandSequence::RawReply > reply, GPSCommandOptions options={})
Definition GPSCommand.h:162
std::optional< uint8_t > gpsSatellitesUsed(std::optional< unsigned > count)
A reported used-satellite count; receivers report UINT8_MAX and above when the count is unknown.
constexpr GPSFixQuality gpsFixQualityFromValue(int value)
@ ASCIILine
Printable lines up to CR/LF, without the terminator; the decoder checks checksums.
Config config
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
int32_t latitude
Definition UBXPlan.cc:40
int32_t longitude
Definition UBXPlan.cc:42
GPSBaudDetection detectBaud(std::span< const unsigned > candidates, unsigned baud, GPSBaudProbeFunction probe)
void receiveFor(std::chrono::milliseconds duration)
Repeats the family's full streaming receive until duration has passed or I/O fails.
GPSDecodeContext & context()
bool setBaudrate(unsigned baudrate)
void failSequence(const GPSCommandSequence::Result &result)
void failControl(GPSProtocolError failure=GPSProtocolError::Protocol)
GPSStreamDemux & stream()
bool runRequired(const GPSCommandSequence &sequence)
Runs sequence and fails control over a failed step as failSequence() does.
GPSCommandSequence::Result runSequence(const GPSCommandSequence &sequence)
Runs sequence in order until a required step fails or I/O fails; failed attempts retry up to their co...
void failNoAnswer(std::span< const unsigned > rates)
bool writeCommand(GPSConfigurationStep step, QByteArrayView bytes)
Starts one command attempt under its own deadline scope and writes it; awaitReply() waits for its rep...
void resolveReply(GPSCommandOutcome outcome)
Resolves the outstanding reply; Pending and later resolutions are ignored.
void offerReply(std::string_view reply)
Passes one decoded text reply to the outstanding text matcher, if any.
uint64_t nowUs() const
bool replyPending() const
True while a transact() reply is outstanding and unresolved.
void publishSurvey(GPSSurveyReport &status)
Stamps status with the receipt time and publishes a copy. Survey progress is not an update flag.
virtual void serviceStreaming(GPSCommandChannel &channel)
virtual bool probe(GPSCommandChannel &channel)
virtual bool configure(GPSCommandChannel &channel, GPSConfig config, unsigned &baud)=0
virtual QString identity() const
Model and firmware reported during configuration; empty when unknown.
virtual GPSReceiveUpdates _decodeLine(std::string_view line, GPSDecodeContext &context)=0
Decodes one line after standard NMEA did.
virtual void _expire(GPSDecodeContext &context)
Drops state that is stale or that a recorded failure invalidated; runs before each decode.
static constexpr GPSStreamConfig STREAM
Framers an ASCII family's descriptor declares.
Host-timed survey-in duration for receivers that report completion but not elapsed time.
QByteArray surveyStart(std::chrono::seconds duration)
GPSCommandOutcome rejection(std::string_view reply)
A NAK rejects any command.
constexpr std::array OUTPUTS
Navigation output. Some receivers do not acknowledge some of these (GSV, for example),...
Definition AshtechPlan.h:47
constexpr std::array BAUD_RATES
Rates the port query probes, in order. Trimble receivers default to 115200.
Definition AshtechPlan.h:31
constexpr std::string_view BOARD_QUERY
Board identification; only an MB-Two (BASE_BOARD) can run as a base station.
Definition AshtechPlan.h:43
constexpr std::string_view LINK_SPEED
Definition AshtechPlan.h:38
GPSCommandSequence sequence(std::span< const Setting > settings, char port)
settings for port, each acknowledged by an ACK.
constexpr std::chrono::milliseconds RESPONSE_TIMEOUT
Per attempt, for every command.
Definition AshtechPlan.h:28
constexpr unsigned PORT_QUERY_ATTEMPTS_AT_LINK_BAUD
Definition AshtechPlan.h:40
constexpr std::chrono::milliseconds LINK_SPEED_SETTLE
Definition AshtechPlan.h:39
constexpr std::string_view PORT_QUERY
Definition AshtechPlan.h:32
QByteArray line(std::string_view command)
command as one CR/LF-terminated line.
constexpr unsigned LINK_BAUD
Definition AshtechPlan.h:37
constexpr unsigned PORT_QUERY_ATTEMPTS
Definition AshtechPlan.h:33
constexpr std::array STATION
Station description once averaging has started (see surveyStart()).
Definition AshtechPlan.h:60
constexpr std::string_view BASE_BOARD
Definition AshtechPlan.h:44
constexpr std::array RTCM_OUTPUTS
RTCM3 output once the base position is known.
Definition AshtechPlan.h:66
const GPSReceiverFamily FAMILY
GPSPositionReport position(const GPSDecodedPosition &source, const GPSIntegrityReport &diagnostic, uint64_t nowUs)
constexpr time_t UTC_PLAUSIBILITY_FLOOR_SECS
Receiver dates before this instant (2009-02-13) are implausible and reported as unknown.
constexpr double DEG_TO_RAD
constexpr bool withinAge(uint64_t timestampUs, uint64_t nowUs, std::chrono::microseconds maximumAge)
Whether a receipt is at most maximumAge old, its limit included.
uint64_t nowUs()
constexpr size_t UTC_TIME
std::optional< ZDA > zda(const Sentence &input)
std::optional< Sentence > sentence(std::string_view text)
std::optional< double > coordinate(std::string_view field, std::string_view hemisphere, bool latitude)
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)
double degreesFromDegreesMinutes(double ddmm)
Convert a signed ddmm.mmmm (or dddmm.mmmm) value; invalid values return NaN.
std::optional< GGA > gga(const Sentence &input)
Quality zero is a valid fix-loss observation with empty coordinates and optional hemisphere fields.
std::optional< GST > gst(const Sentence &input)
std::optional< Frame > frame(std::string_view text)
Configuration used only by the RTK base-station role.
The receiver configuration requested for one configure() attempt.
Decoded navigation plus whether the producer accepted its velocity solution.
GPSNavigationValues navigation
std::optional< uint8_t > satellitesUsed
double altitude
double hdop
unsigned quality
double geoidSeparation
double latitude
std::optional< unsigned > satellitesUsed
double longitude