11#include <QtCore/QByteArray>
12#include <QtCore/QByteArrayView>
13#include <QtCore/QString>
59 QString
identity()
const override {
return QString::fromUtf8(_firmware); }
63 if (std::holds_alternative<GPSBaseStationConfig::ReceiverAveraging>(
config.base.mode)) {
66 _setBase(
config.base.mode);
67 _monitorSurvey(context);
68 _finishAttempt(context);
72 std::chrono::milliseconds
nextReadSlice(std::chrono::milliseconds timeout)
const override
80 enum class SurveyPhase
100 void _endBootWatch() { _expectingBoot =
false; }
103 void _restartStream(
GPSStreamDemux& stream) { _nmea.reset(stream); }
106 void _setFirmware(std::string_view firmware) { _firmware = QByteArrayView(firmware).toByteArray(); }
125 QByteArray _firmware;
126 std::optional<unsigned> _lastTow;
127 std::optional<GPSSurveyReport> _survey;
128 SurveyPhase _phase = SurveyPhase::Off;
130 bool _expectingBoot =
false;
131 bool _sawBoot =
false;
132 bool _restartRejected =
false;
136class Protocol::Attempt
141 , _protocol(protocol)
148 bool _acknowledge(QByteArrayView body, std::string_view label = {});
149 bool _identify(std::chrono::milliseconds timeout = Plan::COMMAND_TIMEOUT);
150 bool _verifyRole(
bool requireMatch =
true);
151 bool _verifyBase(
bool requireMatch =
true);
153 bool _changeBase(QByteArrayView body, std::string_view label);
155 bool _restart(
bool requireRoleMatch =
true);
156 bool _restartWithBase();
157 bool _verifySaved(
bool restarted,
bool baseVerified);
164 QByteArray _surveyRestart;
166 bool _baseMatches =
false;
167 bool _baseHasDistance =
false;
168 bool _saveAcknowledged =
false;
169 bool _saveUncertain =
false;
174 Attempt attempt(channel, *
this);
175 return attempt.run(std::move(
config), baud);
180 _protocol._stop(_channel.context());
181 (void) _channel.flush();
182 _protocol._startAttempt(_channel.stream());
183 const auto* survey = std::get_if<GPSBaseStationConfig::SurveyIn>(&
config.base.mode);
184 if (std::holds_alternative<GPSBaseStationConfig::ReceiverAveraging>(
config.base.mode) ||
185 (survey && (survey->duration > Plan::MAXIMUM_SURVEY_DURATION ||
186 survey->accuracyMeters > Plan::MAXIMUM_SURVEY_ACCURACY_METERS))) {
188 QStringLiteral(
"Unsupported LG290P configuration: use native 1 Hz observation count (maximum 86400) and "
189 "3D position accuracy threshold (maximum 1000 m), not receiver-managed survey"));
191 const auto scope = _channel.deadlineScope(Plan::CONFIGURATION_TIMEOUT);
192 _protocol._setBase(
config.base.mode);
194 _protocol._restartStream(_channel.stream());
201 if (!detection.found) {
202 if (_protocol._firmware.isEmpty()) {
203 _channel.failNoAnswer(detection.probed);
206 return _fail(QStringLiteral(
"No verified LG290P(03) identity; receiver configuration was not changed"));
208 baud = detection.baud;
209 qCDebug(QuectelProtocolLog).noquote() <<
"Quectel receiver firmware:" << _protocol._firmware;
210 if (!_verifyRole(
false)) {
211 return _fail(QStringLiteral(
"LG290P receiver role query failed; no role change was attempted"));
214 bool restarted =
false;
215 if (
config.allowPersistentChanges) {
218 if (!_restart(
false)) {
220 QStringLiteral(
"LG290P saved role query after restart failed; no persistent change was attempted"));
224 if (_role != Plan::BASE_ROLE) {
225 if (!
config.allowPersistentChanges) {
227 QStringLiteral(
"LG290P role mismatch: save the requested base role externally, reboot and reconnect. "
228 "Alternatively, explicitly allow persistent changes for this connection"),
231 if (!_changeRole()) {
232 return _fail(QStringLiteral(
"LG290P role change was rejected or its readback did not match"));
235 return _fail(QStringLiteral(
"LG290P role save failed; receiver activation is not verified"));
238 return _fail(QStringLiteral(
"LG290P saved role could not be verified after restart"));
243 bool baseChanged =
false;
244 if (!_verifyBase(
false)) {
245 return _fail(QStringLiteral(
"LG290P base settings query failed; no base change was attempted"));
247 if (!_baseMatches && !
config.allowPersistentChanges) {
249 QStringLiteral(
"LG290P base settings mismatch: provision/save the requested ECEF coordinates or survey "
250 "observation count/accuracy externally, or explicitly allow persistent changes for this "
255 const QByteArray
base = Plan::writeBase(
config.base.mode, _protocol._fixedECEF, _baseHasDistance);
256 if (!_changeBase(base, survey ? std::string_view{} : Plan::FIXED_BASE_LABEL)) {
257 return _fail(QStringLiteral(
"LG290P base change was rejected or its readback did not match"));
260 return _fail(QStringLiteral(
"LG290P base save failed; receiver activation is not verified"));
262 if (!_restartWithBase()) {
263 return _fail(QStringLiteral(
"LG290P saved base settings could not be verified after restart"));
267 if (survey && !baseChanged) {
268 qCDebug(QuectelProtocolLog)
269 <<
"LG290P survey uses accepted 1 Hz observations, not wall time; accuracy filters individual 3D fixes. "
270 "The receiver itself stores converged coordinates";
271 if (!_acknowledge(_surveyRestart, Plan::RESTART_SURVEY_LABEL)) {
272 return _fail(QStringLiteral(
"LG290P rejected restarting the unchanged, externally saved survey"));
276 if (!_verifySaved(restarted, baseChanged)) {
278 QStringLiteral(
"LG290P restart/readback failed; saved role or base settings do not match the request"));
281 _protocol._monitorSurvey(_channel.context());
282 const auto baseOutput = Plan::messageRates(Plan::BASE_OUTPUT);
283 if (!_channel.runSequence(baseOutput).succeeded()) {
284 return _fail(QStringLiteral(
"LG290P base message output configuration/readback failed"));
286 const auto nmeaOutput = Plan::messageRates(Plan::NMEA_OUTPUT);
287 if (!_channel.runSequence(nmeaOutput).succeeded()) {
288 return _fail(QStringLiteral(
"LG290P NMEA output configuration/readback failed"));
290 _protocol._finishAttempt(_channel.context());
294bool Protocol::Attempt::_acknowledge(QByteArrayView body, std::string_view label)
297 if (!label.empty()) {
300 const auto result = _channel.transact(command);
304bool Protocol::Attempt::_identify(std::chrono::milliseconds timeout)
306 const auto result = _channel.transact(Plan::command(
308 [
this](std::string_view body) {
309 const Fields reply(body);
310 if (
rejected(reply, Plan::IDENTIFY)) {
313 if (reply.size() != 4 || reply[0] != Plan::IDENTIFY) {
316 const std::string_view firmware = reply[1];
317 _protocol._setFirmware(firmware);
318 return firmware.starts_with(Plan::QUALIFIED_FIRMWARE) && firmware.size() > Plan::QUALIFIED_FIRMWARE.size()
326bool Protocol::Attempt::_verifyRole(
bool requireMatch)
328 const auto result = _channel.transact(Plan::command(Plan::READ_ROLE, [
this, requireMatch](std::string_view body) {
329 const Fields reply(body);
331 const bool valid = reply.size() == 3 && reply[0] ==
"PQTMCFGRCVRMODE" && reply[1] ==
"OK" &&
332 number(reply[2], role) && role <= 2;
336 return readback(reply,
"PQTMCFGRCVRMODE", valid && (!requireMatch || role == Plan::BASE_ROLE));
341bool Protocol::Attempt::_verifyBase(
bool requireMatch)
343 _baseMatches =
false;
344 const auto rate = _channel.transact(Plan::command(Plan::READ_FIX_RATE, [](std::string_view body) {
345 const Fields reply(body);
346 unsigned interval = 0;
347 return readback(reply,
"PQTMCFGFIXRATE",
348 reply.size() == 3 &&
number(reply[2], interval) && interval == Plan::FIX_INTERVAL_MS);
353 const auto base = _channel.transact(Plan::command(Plan::READ_BASE, [
this, requireMatch](std::string_view body) {
354 const Fields reply(body);
360 const bool valid = (reply.size() == 8 || reply.size() == 9) && reply[0] ==
"PQTMCFGSVIN" && reply[1] ==
"OK" &&
361 number(reply[2], mode) && mode <= 2 &&
number(reply[3], count) &&
363 number(reply[7], ecef.
z) && (reply.size() == 8 ||
number(reply[8], distance)) &&
364 std::chrono::seconds(count) <= Plan::MAXIMUM_SURVEY_DURATION && accuracy >= 0 &&
365 accuracy <= Plan::MAXIMUM_SURVEY_ACCURACY_METERS && distance >= 0 &&
distance <= 10;
366 bool matches = valid;
368 _baseHasDistance = reply.size() == 9;
370 const auto& requested = _protocol._baseMode;
371 if (matches && std::holds_alternative<GPSBaseStationConfig::Fixed>(requested)) {
372 const auto& fixed = _protocol._fixedECEF;
374 }
else if (matches) {
375 const auto& survey = std::get<GPSBaseStationConfig::SurveyIn>(requested);
376 matches = mode == 1 && count == survey.duration.count() &&
377 std::abs(accuracy - survey.accuracyMeters) <= 0.000000001 &&
distance == 0;
380 _surveyRestart = QByteArrayView(Plan::RESTART_SURVEY).toByteArray();
381 for (
size_t index = 2; index < reply.size(); ++index) {
382 _surveyRestart +=
',';
383 _surveyRestart.append(QByteArrayView(reply[index]));
387 _baseMatches = matches;
388 return readback(reply,
"PQTMCFGSVIN", valid && (!requireMatch || matches));
393bool Protocol::Attempt::_changeRole()
395 if (!_acknowledge(QByteArrayView(Plan::WRITE_BASE_ROLE))) {
398 return _verifyRole();
401bool Protocol::Attempt::_changeBase(QByteArrayView body, std::string_view label)
403 if (!_acknowledge(body, label)) {
406 return _verifyBase();
409bool Protocol::Attempt::_restartWithBase()
414 return _verifyBase();
417bool Protocol::Attempt::_verifySaved(
bool restarted,
bool baseVerified)
429 return _verifyBase();
432bool Protocol::Attempt::_save()
438 _saveAcknowledged =
true;
439 _saveUncertain =
false;
440 qCDebug(QuectelProtocolLog) <<
"LG290P acknowledged saving configuration to nonvolatile memory";
447bool Protocol::Attempt::_restart(
bool requireRoleMatch)
449 _protocol._beginRestart(_channel.context());
450 qCDebug(QuectelProtocolLog) <<
"Restarting LG290P and verifying its saved configuration";
451 const auto scope = _channel.deadlineScope(Plan::RESTART_TIMEOUT);
453 if (!_channel.writeCommand({QByteArrayView(Plan::RESTART).toByteArray(), Plan::RESTART_TIMEOUT}, restart)) {
458 _protocol._watchBoot(_channel.stream());
460 _channel.wait(Plan::RESTART_POLL);
461 if (_channel.failed()) {
464 const bool identified = _identify(Plan::RESTART_IDENTIFY_TIMEOUT);
465 if (_protocol._restartRejected) {
466 _channel.setErrorDetail(QStringLiteral(
"LG290P rejected PQTMSRR."));
469 if (identified && _protocol._sawBoot) {
470 _protocol._endBootWatch();
471 return _verifyRole(requireRoleMatch);
473 }
while (!_channel.failed() && _channel.nowUs() < _channel.operationDeadline().untilUs);
474 _protocol._endBootWatch();
478bool Protocol::Attempt::_fail(
const QString& reason,
GPSProtocolError failure)
480 _protocol._stop(_channel.context());
481 (void) _channel.flush();
482 QString detail = reason;
483 if (!_channel.errorDetail().isEmpty()) {
484 detail += (detail.isEmpty() ? QString() : QStringLiteral(
" ")) + _channel.errorDetail();
486 _channel.failControl(failure);
487 if (_saveAcknowledged || _saveUncertain) {
488 const QString persistence =
489 _saveAcknowledged ? QStringLiteral(
"An LG290P flash save was acknowledged. ") : QString();
490 const QString uncertain =
491 _saveUncertain ? QStringLiteral(
"The latest flash save may have taken effect. ") : QString();
492 _channel.setErrorDetail(persistence + uncertain + detail +
493 QStringLiteral(
" Receiver settings may have changed; no rollback was attempted."));
495 _channel.setErrorDetail(detail);
503 _phase = SurveyPhase::Off;
504 _revokeSurvey(context);
511 _phase = SurveyPhase::Off;
514 _nmea.setRTCMEnabled(
false);
520 if (
const auto* fixed = std::get_if<GPSBaseStationConfig::Fixed>(&mode)) {
527 _revokeSurvey(context);
528 _phase = SurveyPhase::AwaitingBoot;
534 _expectingBoot =
true;
536 _restartRejected =
false;
541 _phase = SurveyPhase::Monitoring;
542 _publishSurvey(context);
548 _expireSurvey(context);
549 _nmea.setRTCMEnabled(_survey && _survey->valid);
557 _expireSurvey(context);
561GPSReceiveUpdates Protocol::_decodeLine(std::string_view line,
GPSDecodeContext& context)
569 _restartRejected =
true;
571 if (body.starts_with(
"PQTMSVINSTATUS,")) {
573 }
else if (body.starts_with(
"PQTMVER,")) {
574 const Fields reply(body);
575 const std::string_view firmware(_firmware.constData(),
static_cast<size_t>(_firmware.size()));
576 if (reply.size() == 6 && reply[1] ==
"1" && reply[2] ==
"MODULE" && reply[3] == firmware) {
578 if (_expectingBoot && !_sawBoot) {
580 if (_phase == SurveyPhase::AwaitingBoot) {
581 _phase = SurveyPhase::Verifying;
583 }
else if (!_expectingBoot && (_ready || _phase != SurveyPhase::Off)) {
585 _phase = SurveyPhase::Off;
586 _revokeSurvey(context);
587 context.
failControl(QStringLiteral(
"LG290P restarted; its base configuration is no longer verified"));
597 _nmea.setRTCMEnabled(
false);
608 _revokeSurvey(context);
614 _expireSurvey(context);
615 if (_phase == SurveyPhase::Monitoring && _survey) {
618 _nmea.setRTCMEnabled(_ready && _survey->valid);
622bool Protocol::_handleSurvey(std::string_view body,
GPSDecodeContext& context)
624 const Fields reply(body);
625 unsigned version = 0;
627 unsigned validity = 0;
628 unsigned observations = 0;
629 unsigned configuredCount = 0;
632 if (reply.size() != 12 || !
number(reply[1], version) || version != 1 || !
number(reply[2], tow) ||
642 if (_phase != SurveyPhase::Verifying && _phase != SurveyPhase::Monitoring) {
645 if (!
number(reply[3], validity) || validity > 2 || !reply[4].empty() || !
number(reply[6], observations) ||
646 std::chrono::seconds(observations) > Plan::MAXIMUM_SURVEY_DURATION || !
number(reply[7], configuredCount) ||
647 std::chrono::seconds(configuredCount) > Plan::MAXIMUM_SURVEY_DURATION || !
number(reply[8], ecef.
x) ||
648 !
number(reply[9], ecef.
y) || !
number(reply[10], ecef.
z) || !
number(reply[11], accuracy) || accuracy < 0 ||
649 accuracy > std::numeric_limits<uint32_t>::max() / 1000.0) {
650 _revokeSurvey(context);
653 const bool fixed = std::holds_alternative<GPSBaseStationConfig::Fixed>(_baseMode);
654 const bool matches = fixed
655 ? configuredCount == 0 && observations == 0
656 : configuredCount == std::get<GPSBaseStationConfig::SurveyIn>(_baseMode).duration.count();
658 if (!matches || (!coordinatesKnown && (validity == 2 || (validity == 1 && observations != 0)))) {
659 _revokeSurvey(context);
663 _revokeSurvey(context);
668 const bool valid = validity == 2 && (fixed || observations >= configuredCount);
671 if (!fixed && validity != 0 && observations != 0 && coordinatesKnown) {
675 report.duration = std::chrono::seconds(observations);
676 report.valid = valid;
677 report.active = validity == 1 && !valid;
678 if (coordinatesKnown && validity != 0) {
681 report.timestampUs = context.
nowUs();
683 _publishSurvey(context);
687QLatin1StringView signature(
const GPSFrame& frame)
689 const std::string_view text =
frame.text();
692 : QLatin1StringView();
698 const auto query = Plan::command(Plan::IDENTIFY, [](std::string_view body) {
699 const Fields reply(body);
711 .
type = GPSType::quectel,
712 .logCategory = &QuectelProtocolLog,
715 .create = &gpsCreateProtocol<Protocol>,
716 .signature = &signature,
@ Stop
The receiver answered but is not usable, so no other rate is tried.
GPSProtocolError
Sticky failure of the current receiver session; I/O and control stop until the next configure().
@ Protocol
The receiver violated or rejected the control protocol.
@ Cancelled
A stop was requested; not a receiver or link fault.
@ ConsentRequired
Configuration needs a persistent receiver change the caller did not allow.
@ ASCIILine
Printable lines up to CR/LF, without the terminator; the decoder checks checksums.
#define QGC_LOGGING_CATEGORY(name, categoryStr)
GPSConfigurationEvidence transact(GPSConfigurationStep step, QByteArrayView bytes)
void offerReply(std::string_view reply)
Passes one decoded text reply to the outstanding text matcher, if any.
void publishAsReceived(GPSProtocolEvent event)
void failControl(const QString &detail)
Records a protocol failure described by detail unless a failure is already recorded,...
void publishSurvey(GPSSurveyReport &status)
Stamps status with the receipt time and publishes a copy. Survey progress is not an update flag.
virtual bool probe(GPSCommandChannel &channel)
virtual bool armDecodeOnly(const GPSConfig &config, GPSDecodeContext &context)
virtual bool configure(GPSCommandChannel &channel, GPSConfig config, unsigned &baud)=0
virtual QString identity() const
Model and firmware reported during configuration; empty when unknown.
virtual std::chrono::milliseconds nextReadSlice(std::chrono::milliseconds timeout) const
The longest single read in a receive cycle of timeout; the cycle deadline still bounds it.
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 std::chrono::milliseconds STATUS_POLL
The longest read of a family that expires its status, so expiry runs on a silent link too.
static constexpr GPSStreamConfig STREAM
Framers an ASCII family's descriptor declares.
Borrowed fields for the largest supported PQTM reply, including empty trailing fields.
constexpr std::array BAUD_RATES
Rates the port query probes, in order. Trimble receivers default to 115200.
GPSCommandOutcome acknowledgement(std::string_view reply)
A $PASHS setting is acknowledged by an ACK.
Ecef toEcef(const GPSEllipsoidPosition &position)
bool nearEarthSurface(const Ecef &position)
double roundedToMillimeters(double meters)
meters rounded to whole millimetres.
double distance(const Ecef &left, const Ecef &right)
Straight-line distance between left and right, in metres.
GPSEllipsoidPosition fromEcef(const Ecef &position)
constexpr bool towAdvances(uint32_t later, uint32_t earlier)
Whether time of week later follows earlier by less than half a week, across the week rollover.
constexpr uint32_t WEEK_MS
Milliseconds in a GPS week; every time of week is below it.
constexpr bool withinAge(uint64_t timestampUs, uint64_t nowUs, std::chrono::microseconds maximumAge)
Whether a receipt is at most maximumAge old, its limit included.
QByteArray frame(QByteArrayView body)
body as one "$<body>*hh\r\n" sentence, with uppercase checksum digits.
std::optional< T > number(std::string_view field, int base=DECIMAL_BASE)
std::optional< Frame > frame(std::string_view text)
std::string_view checkedBody(std::string_view line)
The body of a checksum-valid "$<body>*hh" line; empty otherwise.
bool rejected(const Fields &reply, std::string_view command)
GPSCommandOutcome readback(const Fields &reply, std::string_view command, bool matches)
bool number(std::string_view text, T &result)
GPSCommandSequence::Command command(QByteArrayView body, GPSTextMatcher reply, std::chrono::milliseconds timeout)
body labelled by its text, framed as a sentence, and completed by reply within timeout.
const GPSReceiverFamily FAMILY
GPSCommandSequence base(QByteArrayView port, const GPSBaseStationConfig &base)
Streams RTCM3 corrections and the PVTGeodetic survey status over port in the mode base selects.
std::variant< SurveyIn, Fixed, ReceiverAveraging > Mode
GPSConfigurationStep step
Evidence label and per-attempt timeout. A required command that fails ends the sequence.
The receiver configuration requested for one configure() attempt.
std::optional< double > meanAccuracyMeters