9#include <QtCore/QByteArray>
10#include <QtCore/QString>
33std::optional<T> numberOr(std::string_view field, T fallback)
38 return NMEA::number<T>(field);
41uint64_t utcUs(std::chrono::year_month_day date,
int milliseconds)
43 return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::sys_days(date).time_since_epoch() +
44 std::chrono::milliseconds(milliseconds))
49uint64_t utcAtTimeOfDay(uint64_t referenceUtcUs, uint64_t referenceReceiptUs, std::optional<int> timeMs, uint64_t nowUs,
50 std::chrono::microseconds maximumAge)
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) {
60 }
else if (reference - result > DAY_US / 2) {
63 return result > 0 ?
static_cast<uint64_t
>(result) : 0;
79std::optional<uint64_t> receiptUtc(std::string_view date, std::string_view time)
81 if (date.size() != 10 || date[2] !=
'.' || date[5] !=
'.') {
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));
88 if (!day || !month || !year || *year < 1980 || *year > 9999 || !milliseconds) {
91 const std::chrono::year_month_day calendar{std::chrono::year{*year}, std::chrono::month{*month},
92 std::chrono::day{*day}};
96 return utcUs(calendar, *milliseconds);
118 QString
identity()
const override {
return _board; }
130 std::optional<int> time;
131 uint64_t receivedAtUs = 0;
133 [[nodiscard]]
bool matches(
const EpochReceipt& position, std::chrono::microseconds maximumAge)
const
135 return time && time ==
position.time &&
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);
163 void _expireMetadata(uint64_t nowUs);
164 void _applyMetadata(std::optional<int> time, uint64_t nowUs);
174 bool _baseSetupDone =
false;
176 bool _baseSetupPending =
false;
178 bool _rtcmActivationPending =
false;
180 bool _awaitingSurveyReceipt =
false;
183 bool _receiverPositions =
false;
184 uint64_t _utcReceivedUs = 0;
185 uint64_t _utcReference = 0;
186 EpochReceipt _positionEpoch;
187 EpochReceipt _accuracyReceipt;
189 bool _surveyReceiptRequested =
false;
190 std::optional<uint64_t> _surveyReceiptStartUtc;
201 if (!detection.found) {
210 baud = detection.baud;
215 _nmea.reset(channel.
stream());
217 _nmea.reset(channel.
stream());
226 [
this](std::string_view reply) {
return _boardReply(reply); })}};
232 QStringLiteral(
"Trimble %1 cannot run as an RTK base station; only the MB-Two can").arg(_board));
237 _nmea.setRTCMEnabled(
true);
245 if (_baseSetupPending) {
246 _baseSetupPending =
false;
249 if (_rtcmActivationPending) {
250 _rtcmActivationPending =
false;
251 _activateRTCMOutput(channel);
258 [
this](std::string_view reply) {
return _portReply(reply); });
267 if (_baseSetupDone) {
271 if (
const auto* fixed = std::get_if<GPSBaseStationConfig::Fixed>(&_base.mode)) {
272 if (!channel.
runRequired({{Plan::fixedPositionCommand(fixed->position)}})) {
275 _activateRTCMOutput(channel);
280 }
else if (
const auto* survey = std::get_if<GPSBaseStationConfig::SurveyIn>(&_base.mode)) {
284 _requestSurveyReceipts(
true);
285 _awaitingSurveyReceipt =
true;
287 _awaitingSurveyReceipt =
false;
289 _requestSurveyReceipts(
false);
290 channel.
failControl(QStringLiteral(
"No matching Ashtech survey-start receipt"));
297 if (!_rtcmActivationPending) {
298 _surveyClock.start(context);
301 _baseSetupDone =
true;
311 if (!reply.starts_with(
"$PASHR,PRT,") || std::count(reply.begin(), reply.end(),
',') != 3) {
320 constexpr std::string_view PREFIX =
"$PASHR,RID,";
321 if (!reply.starts_with(PREFIX)) {
324 const std::string_view board = reply.substr(PREFIX.size());
325 _board = QString::fromLatin1(board.substr(0, board.find_first_of(
",*")));
334 _baseSetupDone =
false;
335 _baseSetupPending =
false;
336 _rtcmActivationPending =
false;
337 _awaitingSurveyReceipt =
false;
339 _receiverPositions =
false;
343 _accuracyReceipt = {};
345 _requestSurveyReceipts(
false);
346 _nmea.setRTCMEnabled(
false);
350GPSReceiveUpdates Protocol::_decodeLine(std::string_view line,
GPSDecodeContext& context)
352 const auto vendor = _decodeVendor(line, context);
354 _surveyClock.publishProgress(context);
356 return vendor.value_or(GPSReceiveUpdates{});
359void Protocol::_requestSurveyReceipts(
bool requested)
361 _surveyReceiptRequested = requested;
362 _surveyReceiptStartUtc.reset();
365std::optional<GPSReceiveUpdates> Protocol::_decodeVendor(std::string_view message,
GPSDecodeContext& context)
368 if (!sentence || message.size() < 7) {
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);
378 if (type ==
"GGA" && commas == 14 && !_receiverPositions) {
379 return _handleGGA(*sentence, now);
381 if (message.starts_with(
"$PASHR,POS,") && commas == 18) {
382 return _handlePosition(*sentence, now);
384 if (type ==
"GST" && commas == 8) {
385 return _handleAccuracy(*sentence, now);
387 if (message.starts_with(
"$PASHR,RECEIPT,")) {
388 return _handleSurveyReceipt(*sentence, context);
391 return GPSReceiveUpdates{};
394std::optional<GPSReceiveUpdates> Protocol::_handleTime(
const NMEA::Sentence& sentence, uint64_t nowUs)
397 if (!zda || !
zda->utcMilliseconds ||
zda->date.year < 1980 ||
zda->date.year > 9999) {
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);
405 _nmea.position().navigation.utcTimeUs = _utcReference;
406 _utcReceivedUs =
nowUs;
407 return GPSReceiveUpdates{};
410std::optional<GPSReceiveUpdates> Protocol::_handleGGA(
const NMEA::Sentence& sentence, uint64_t nowUs)
416 applyGGA(_nmea.position(), *fix, nowUs);
421std::optional<GPSReceiveUpdates> Protocol::_handlePosition(
const NMEA::Sentence& sentence, uint64_t nowUs)
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);
466 for (
const size_t unused : {4, 10, 13, 14, 17}) {
467 if (!numberOr(fields[unused], 0.0)) {
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")) {
479 const bool coordinatesFound = !fields[5].empty() && !fields[7].empty() && !fields[9].empty();
481 auto& navigation = _nmea.position().navigation;
484 navigation.altitudeEllipsoidMeters = *altitude;
485 navigation.altitudeMslMeters = NAN;
486 navigation.horizontalDop =
static_cast<float>(*horizontalDop);
487 navigation.verticalDop =
static_cast<float>(*verticalDop);
489 if (!coordinatesFound) {
490 navigation.fixType = GPSPositionReport::FixType::NoFix;
493 if (*quality == 9 || *quality == 10) {
494 navigation.fixType = GPSPositionReport::FixType::Differential;
496 }
else if (*quality == 12 || *quality == 22) {
497 navigation.fixType = GPSPositionReport::FixType::RTKFloat;
499 }
else if (*quality == 13 || *quality == 23) {
500 navigation.fixType = GPSPositionReport::FixType::RTKFixed;
506 _receiverPositions =
true;
508 if (_ready && !_baseSetupDone) {
509 _baseSetupPending =
true;
513 navigation.timestampUs =
nowUs;
515 navigation.satellitesUsed =
gpsSatellitesUsed(
static_cast<unsigned>(*satellites));
517 navigation.speedMetersPerSecond =
static_cast<float>(*groundSpeedKnots) / 1.9438445f;
520 _nmea.position().velocityValid =
true;
524std::optional<GPSReceiveUpdates> Protocol::_handleAccuracy(
const NMEA::Sentence& sentence, uint64_t 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;
538 return GPSReceiveUpdates{};
541std::optional<GPSReceiveUpdates> Protocol::_handleSurveyReceipt(
const NMEA::Sentence& sentence,
547 const auto& fields =
sentence.fields;
548 if (fields[2] !=
"POS" || fields[3] !=
"AVG") {
551 const bool started = fields[4] ==
"STARTED";
552 const bool failed = !started && fields[8] ==
"ERR";
555 std::optional<uint64_t> receiptTime;
556 std::optional<uint32_t> interval;
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) {
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)) {
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) {
584 const auto* survey = std::get_if<GPSBaseStationConfig::SurveyIn>(&_base.mode);
585 if (!_ready || !survey || !_surveyReceiptRequested) {
588 if (*interval != survey->duration.count()) {
591 const uint64_t now = context.
nowUs();
592 const bool awaitingReceipt = _awaitingSurveyReceipt && context.
replyPending();
594 if (!awaitingReceipt || _surveyReceiptStartUtc) {
598 (*receiptTime < _utcReference ||
599 std::chrono::microseconds(*receiptTime - _utcReference) > STATUS_MAX_AGE)) {
602 _surveyReceiptStartUtc = receiptTime;
604 if (!_surveyReceiptStartUtc || *receiptTime < *_surveyReceiptStartUtc ||
605 (!failed && *receiptTime - *_surveyReceiptStartUtc < uint64_t(*interval) * 1000000)) {
608 _requestSurveyReceipts(
false);
610 if (awaitingReceipt) {
615 _surveyClock.finish(context, !failed, {.latitudeDegrees =
latitude, .longitudeDegrees =
longitude});
616 _rtcmActivationPending = !failed;
618 return GPSReceiveUpdates{};
621void Protocol::_expireMetadata(uint64_t nowUs)
623 auto& navigation = _nmea.position().navigation;
625 navigation.horizontalAccuracyMeters = NAN;
626 navigation.verticalAccuracyMeters = NAN;
629 navigation.utcTimeUs = 0;
633void Protocol::_applyMetadata(std::optional<int> time, uint64_t nowUs)
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);
644QLatin1StringView signature(
const GPSFrame& frame)
646 const std::string_view text =
frame.text();
648 ? QLatin1StringView(
"$PASHR sentences")
649 : QLatin1StringView();
657 .
type = GPSType::trimble,
658 .logCategory = &AshtechProtocolLog,
663 .create = &gpsCreateProtocol<Protocol>,
664 .signature = &signature,
@ 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={})
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.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
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.
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),...
constexpr std::array BAUD_RATES
Rates the port query probes, in order. Trimble receivers default to 115200.
constexpr std::string_view BOARD_QUERY
Board identification; only an MB-Two (BASE_BOARD) can run as a base station.
constexpr std::string_view LINK_SPEED
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.
constexpr unsigned PORT_QUERY_ATTEMPTS_AT_LINK_BAUD
constexpr std::chrono::milliseconds LINK_SPEED_SETTLE
constexpr std::string_view PORT_QUERY
QByteArray line(std::string_view command)
command as one CR/LF-terminated line.
constexpr unsigned LINK_BAUD
constexpr unsigned PORT_QUERY_ATTEMPTS
constexpr std::array STATION
Station description once averaging has started (see surveyStart()).
constexpr std::string_view BASE_BOARD
constexpr std::array RTCM_OUTPUTS
RTCM3 output once the base position is known.
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.
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 altitudeEllipsoidMeters
std::optional< unsigned > satellitesUsed