QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QuectelProtocol.cc
Go to the documentation of this file.
1#include <algorithm>
2#include <chrono>
3#include <cmath>
4#include <cstdint>
5#include <limits>
6#include <optional>
7#include <string_view>
8#include <utility>
9#include <variant>
10
11#include <QtCore/QByteArray>
12#include <QtCore/QByteArrayView>
13#include <QtCore/QString>
14
15#include "GPSCommandChannel.h"
17#include "GPSProtocolEvent.h"
18#include "GPSProtocolMath.h"
19#include "GPSReceiverConfig.h"
20#include "GPSReceiverFamilies.h"
21#include "GPSStreamDemux.h"
22#include "GPSTime.h"
23#include "MonotonicClock.h"
24#include "NMEASentence.h"
25#include "QGCLoggingCategory.h"
27#include "Quectel/QuectelPlan.h"
28
29QGC_LOGGING_CATEGORY(QuectelProtocolLog, "GPS.Protocols.Quectel")
30
31namespace {
32
33namespace Plan = Quectel::Plan;
38
51class Protocol final : public GPSNMEAFamilyProtocol
52{
53public:
54 bool configure(GPSCommandChannel& channel, GPSConfig config, unsigned& baud) override;
55
56 bool probe(GPSCommandChannel& channel) override;
57
59 QString identity() const override { return QString::fromUtf8(_firmware); }
60
61 bool armDecodeOnly(const GPSConfig& config, GPSDecodeContext& context) override
62 {
63 if (std::holds_alternative<GPSBaseStationConfig::ReceiverAveraging>(config.base.mode)) {
64 return false;
65 }
66 _setBase(config.base.mode);
67 _monitorSurvey(context);
68 _finishAttempt(context);
69 return true;
70 }
71
72 std::chrono::milliseconds nextReadSlice(std::chrono::milliseconds timeout) const override
73 {
74 return (std::min) (timeout, STATUS_POLL);
75 }
76
77private:
78 class Attempt;
79
80 enum class SurveyPhase
81 {
82 Off,
83 AwaitingBoot,
84 Verifying,
85 Monitoring,
86 };
87
89 void _stop(GPSDecodeContext& context);
91 void _startAttempt(GPSStreamDemux& stream);
93 void _setBase(const GPSBaseStationConfig::Mode& mode);
95 void _beginRestart(GPSDecodeContext& context);
97 void _watchBoot(GPSStreamDemux& stream);
98
100 void _endBootWatch() { _expectingBoot = false; }
101
103 void _restartStream(GPSStreamDemux& stream) { _nmea.reset(stream); }
104
106 void _setFirmware(std::string_view firmware) { _firmware = QByteArrayView(firmware).toByteArray(); }
107
109 void _monitorSurvey(GPSDecodeContext& context);
111 void _finishAttempt(GPSDecodeContext& context);
113 void _expireSurvey(GPSDecodeContext& context);
115 void _expire(GPSDecodeContext& context) override;
116 GPSReceiveUpdates _decodeLine(std::string_view line, GPSDecodeContext& context) override;
117 bool _handleSurvey(std::string_view body, GPSDecodeContext& context);
118 void _revokeSurvey(GPSDecodeContext& context);
119 void _publishSurvey(GPSDecodeContext& context);
120
123 GPSProtocolMath::Ecef _fixedECEF;
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;
133};
134
136class Protocol::Attempt
137{
138public:
139 Attempt(GPSCommandChannel& channel, Protocol& protocol)
140 : _channel(channel)
141 , _protocol(protocol)
142 {}
143
144 bool run(GPSConfig config, unsigned& baud);
145
146private:
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);
152 bool _changeRole();
153 bool _changeBase(QByteArrayView body, std::string_view label);
154 bool _save();
155 bool _restart(bool requireRoleMatch = true);
156 bool _restartWithBase();
157 bool _verifySaved(bool restarted, bool baseVerified);
159 bool _fail(const QString& reason, GPSProtocolError failure = GPSProtocolError::Protocol);
160
161 GPSCommandChannel& _channel;
162 Protocol& _protocol;
164 QByteArray _surveyRestart;
165 unsigned _role = 0;
166 bool _baseMatches = false;
167 bool _baseHasDistance = false;
168 bool _saveAcknowledged = false;
169 bool _saveUncertain = false;
170};
171
172bool Protocol::configure(GPSCommandChannel& channel, GPSConfig config, unsigned& baud)
173{
174 Attempt attempt(channel, *this);
175 return attempt.run(std::move(config), baud);
176}
177
178bool Protocol::Attempt::run(GPSConfig config, unsigned& baud)
179{
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))) {
187 return _fail(
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"));
190 }
191 const auto scope = _channel.deadlineScope(Plan::CONFIGURATION_TIMEOUT);
192 _protocol._setBase(config.base.mode);
193 const auto detection = _channel.detectBaud(Plan::BAUD_RATES, baud, [this]() -> GPSBaudProbe {
194 _protocol._restartStream(_channel.stream());
195 if (_identify()) {
196 return GPSBaudProbe::Found;
197 }
198 // Firmware that answered but is not an LG290P(03) answers the same at every rate.
199 return _protocol._firmware.isEmpty() ? GPSBaudProbe::TryNext : GPSBaudProbe::Stop;
200 });
201 if (!detection.found) {
202 if (_protocol._firmware.isEmpty()) {
203 _channel.failNoAnswer(detection.probed);
204 return _fail({});
205 }
206 return _fail(QStringLiteral("No verified LG290P(03) identity; receiver configuration was not changed"));
207 }
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"));
212 }
213
214 bool restarted = false;
215 if (config.allowPersistentChanges) {
216 // Work from saved settings, not another client's uncommitted changes. A role
217 // readback alone cannot distinguish a pending role from the active one.
218 if (!_restart(false)) {
219 return _fail(
220 QStringLiteral("LG290P saved role query after restart failed; no persistent change was attempted"));
221 }
222 restarted = true;
223 }
224 if (_role != Plan::BASE_ROLE) {
225 if (!config.allowPersistentChanges) {
226 return _fail(
227 QStringLiteral("LG290P role mismatch: save the requested base role externally, reboot and reconnect. "
228 "Alternatively, explicitly allow persistent changes for this connection"),
230 }
231 if (!_changeRole()) {
232 return _fail(QStringLiteral("LG290P role change was rejected or its readback did not match"));
233 }
234 if (!_save()) {
235 return _fail(QStringLiteral("LG290P role save failed; receiver activation is not verified"));
236 }
237 if (!_restart()) {
238 return _fail(QStringLiteral("LG290P saved role could not be verified after restart"));
239 }
240 restarted = true;
241 }
242
243 bool baseChanged = false;
244 if (!_verifyBase(false)) {
245 return _fail(QStringLiteral("LG290P base settings query failed; no base change was attempted"));
246 }
247 if (!_baseMatches && !config.allowPersistentChanges) {
248 return _fail(
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 "
251 "connection"),
253 }
254 if (!_baseMatches) {
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"));
258 }
259 if (!_save()) {
260 return _fail(QStringLiteral("LG290P base save failed; receiver activation is not verified"));
261 }
262 if (!_restartWithBase()) {
263 return _fail(QStringLiteral("LG290P saved base settings could not be verified after restart"));
264 }
265 baseChanged = true;
266 }
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"));
273 }
274 restarted = false;
275 }
276 if (!_verifySaved(restarted, baseChanged)) {
277 return _fail(
278 QStringLiteral("LG290P restart/readback failed; saved role or base settings do not match the request"));
279 }
280
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"));
285 }
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"));
289 }
290 _protocol._finishAttempt(_channel.context());
291 return true;
292}
293
294bool Protocol::Attempt::_acknowledge(QByteArrayView body, std::string_view label)
295{
296 auto command = Plan::command(body, Plan::acknowledgement(body));
297 if (!label.empty()) {
298 command.step.command = QByteArrayView(label).toByteArray();
299 }
300 const auto result = _channel.transact(command);
301 return result.outcome == GPSCommandOutcome::Acknowledged;
302}
303
304bool Protocol::Attempt::_identify(std::chrono::milliseconds timeout)
305{
306 const auto result = _channel.transact(Plan::command(
307 Plan::IDENTIFY,
308 [this](std::string_view body) {
309 const Fields reply(body);
310 if (rejected(reply, Plan::IDENTIFY)) {
312 }
313 if (reply.size() != 4 || reply[0] != Plan::IDENTIFY) {
315 }
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()
321 },
322 timeout));
323 return result.outcome == GPSCommandOutcome::ReadbackVerified;
324}
325
326bool Protocol::Attempt::_verifyRole(bool requireMatch)
327{
328 const auto result = _channel.transact(Plan::command(Plan::READ_ROLE, [this, requireMatch](std::string_view body) {
329 const Fields reply(body);
330 unsigned role = 0;
331 const bool valid = reply.size() == 3 && reply[0] == "PQTMCFGRCVRMODE" && reply[1] == "OK" &&
332 number(reply[2], role) && role <= 2;
333 if (valid) {
334 _role = role;
335 }
336 return readback(reply, "PQTMCFGRCVRMODE", valid && (!requireMatch || role == Plan::BASE_ROLE));
337 }));
338 return result.outcome == GPSCommandOutcome::ReadbackVerified;
339}
340
341bool Protocol::Attempt::_verifyBase(bool requireMatch)
342{
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);
349 }));
350 if (rate.outcome != GPSCommandOutcome::ReadbackVerified) {
351 return false;
352 }
353 const auto base = _channel.transact(Plan::command(Plan::READ_BASE, [this, requireMatch](std::string_view body) {
354 const Fields reply(body);
355 unsigned mode = 0;
356 unsigned count = 0;
357 double accuracy = 0;
358 double distance = 0;
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) &&
362 number(reply[4], accuracy) && number(reply[5], ecef.x) && number(reply[6], ecef.y) &&
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;
367 if (valid) {
368 _baseHasDistance = reply.size() == 9;
369 }
370 const auto& requested = _protocol._baseMode;
371 if (matches && std::holds_alternative<GPSBaseStationConfig::Fixed>(requested)) {
372 const auto& fixed = _protocol._fixedECEF;
373 matches = mode == 2 && count == 0 && accuracy == 0 && GPSProtocolMath::distance(ecef, fixed) <= 0.00011;
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;
378 if (matches) {
379 // Re-execute precisely the read configuration, including otherwise ignored ECEF fields.
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]));
384 }
385 }
386 }
387 _baseMatches = matches;
388 return readback(reply, "PQTMCFGSVIN", valid && (!requireMatch || matches));
389 }));
391}
392
393bool Protocol::Attempt::_changeRole()
394{
395 if (!_acknowledge(QByteArrayView(Plan::WRITE_BASE_ROLE))) {
396 return false;
397 }
398 return _verifyRole();
399}
400
401bool Protocol::Attempt::_changeBase(QByteArrayView body, std::string_view label)
402{
403 if (!_acknowledge(body, label)) {
404 return false;
405 }
406 return _verifyBase();
407}
408
409bool Protocol::Attempt::_restartWithBase()
410{
411 if (!_restart()) {
412 return false;
413 }
414 return _verifyBase();
415}
416
417bool Protocol::Attempt::_verifySaved(bool restarted, bool baseVerified)
418{
419 // A role readback can reflect an unsaved, not-yet-active change. Reboot and read back
420 // the saved role before claiming that the receiver actually operates in that role.
421 if (!restarted) {
422 if (!_restart()) {
423 return false;
424 }
425 }
426 if (baseVerified) {
427 return true;
428 }
429 return _verifyBase();
430}
431
432bool Protocol::Attempt::_save()
433{
434 const auto result =
435 _channel.transact(Plan::command(Plan::SAVE, Plan::acknowledgement(Plan::SAVE), Plan::SAVE_TIMEOUT));
436 const bool saved = result.outcome == GPSCommandOutcome::Acknowledged;
437 if (saved) {
438 _saveAcknowledged = true;
439 _saveUncertain = false;
440 qCDebug(QuectelProtocolLog) << "LG290P acknowledged saving configuration to nonvolatile memory";
441 } else {
442 _saveUncertain = result.outcome != GPSCommandOutcome::Rejected && result.acceptedBytes > 0;
443 }
444 return saved;
445}
446
447bool Protocol::Attempt::_restart(bool requireRoleMatch)
448{
449 _protocol._beginRestart(_channel.context());
450 qCDebug(QuectelProtocolLog) << "Restarting LG290P and verifying its saved configuration";
451 const auto scope = _channel.deadlineScope(Plan::RESTART_TIMEOUT);
452 const QByteArray restart = NMEAUtils::frame(Plan::RESTART);
453 if (!_channel.writeCommand({QByteArrayView(Plan::RESTART).toByteArray(), Plan::RESTART_TIMEOUT}, restart)) {
454 return false;
455 }
456 // PQTMSRR has no documented ACK. Do not inflate successful transport completion to acknowledgment.
457 _channel.finishCommand(GPSCommandOutcome::Written);
458 _protocol._watchBoot(_channel.stream());
459 do {
460 _channel.wait(Plan::RESTART_POLL);
461 if (_channel.failed()) {
462 break;
463 }
464 const bool identified = _identify(Plan::RESTART_IDENTIFY_TIMEOUT);
465 if (_protocol._restartRejected) {
466 _channel.setErrorDetail(QStringLiteral("LG290P rejected PQTMSRR."));
467 break;
468 }
469 if (identified && _protocol._sawBoot) {
470 _protocol._endBootWatch();
471 return _verifyRole(requireRoleMatch);
472 }
473 } while (!_channel.failed() && _channel.nowUs() < _channel.operationDeadline().untilUs);
474 _protocol._endBootWatch();
475 return false;
476}
477
478bool Protocol::Attempt::_fail(const QString& reason, GPSProtocolError failure)
479{
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();
485 }
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."));
494 } else if (_channel.error() != GPSProtocolError::Cancelled) {
495 _channel.setErrorDetail(detail);
496 }
497 return false;
498}
499
500void Protocol::_stop(GPSDecodeContext& context)
501{
502 _ready = false;
503 _phase = SurveyPhase::Off;
504 _revokeSurvey(context);
505}
506
507void Protocol::_startAttempt(GPSStreamDemux& stream)
508{
509 _lastTow.reset();
510 _survey.reset();
511 _phase = SurveyPhase::Off;
512 _firmware.clear();
513 _nmea.reset(stream);
514 _nmea.setRTCMEnabled(false);
515}
516
517void Protocol::_setBase(const GPSBaseStationConfig::Mode& mode)
518{
519 _baseMode = mode;
520 if (const auto* fixed = std::get_if<GPSBaseStationConfig::Fixed>(&mode)) {
521 _fixedECEF = GPSProtocolMath::toEcef(fixed->position);
522 }
523}
524
525void Protocol::_beginRestart(GPSDecodeContext& context)
526{
527 _revokeSurvey(context);
528 _phase = SurveyPhase::AwaitingBoot;
529}
530
531void Protocol::_watchBoot(GPSStreamDemux& stream)
532{
533 _nmea.reset(stream);
534 _expectingBoot = true;
535 _sawBoot = false;
536 _restartRejected = false;
537}
538
539void Protocol::_monitorSurvey(GPSDecodeContext& context)
540{
541 _phase = SurveyPhase::Monitoring;
542 _publishSurvey(context);
543}
544
545void Protocol::_finishAttempt(GPSDecodeContext& context)
546{
547 _ready = true;
548 _expireSurvey(context);
549 _nmea.setRTCMEnabled(_survey && _survey->valid);
550}
551
552void Protocol::_expire(GPSDecodeContext& context)
553{
554 if (context.failed()) {
555 _stop(context);
556 } else {
557 _expireSurvey(context);
558 }
559}
560
561GPSReceiveUpdates Protocol::_decodeLine(std::string_view line, GPSDecodeContext& context)
562{
563 const auto body = QuectelCodec::checkedBody(line);
564 if (body.empty()) {
565 return {};
566 }
567 context.offerReply(body);
568 if (_expectingBoot && body.starts_with("PQTMSRR,") && QuectelCodec::rejected(Fields(body), "PQTMSRR")) {
569 _restartRejected = true;
570 }
571 if (body.starts_with("PQTMSVINSTATUS,")) {
572 return _handleSurvey(body, context) ? GPSReceiveUpdates(GPSReceiveUpdate::Activity) : GPSReceiveUpdates{};
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) {
577 // ยง2.3.1: this is the first output upon each successful startup.
578 if (_expectingBoot && !_sawBoot) {
579 _sawBoot = true;
580 if (_phase == SurveyPhase::AwaitingBoot) {
581 _phase = SurveyPhase::Verifying;
582 }
583 } else if (!_expectingBoot && (_ready || _phase != SurveyPhase::Off)) {
584 _ready = false;
585 _phase = SurveyPhase::Off;
586 _revokeSurvey(context);
587 context.failControl(QStringLiteral("LG290P restarted; its base configuration is no longer verified"));
588 }
590 }
591 }
592 return {};
593}
594
595void Protocol::_revokeSurvey(GPSDecodeContext& context)
596{
597 _nmea.setRTCMEnabled(false);
598 if (_survey) {
599 _survey.reset();
600 context.publishSurvey(false, false, {});
601 }
602}
603
604void Protocol::_expireSurvey(GPSDecodeContext& context)
605{
606 if (_survey &&
607 (context.failed() || !MonotonicClock::withinAge(_survey->timestampUs, context.nowUs(), STATUS_MAX_AGE))) {
608 _revokeSurvey(context);
609 }
610}
611
612void Protocol::_publishSurvey(GPSDecodeContext& context)
613{
614 _expireSurvey(context);
615 if (_phase == SurveyPhase::Monitoring && _survey) {
616 // A status buffered during boot verification keeps its original receipt time.
617 context.publishAsReceived(*_survey);
618 _nmea.setRTCMEnabled(_ready && _survey->valid);
619 }
620}
621
622bool Protocol::_handleSurvey(std::string_view body, GPSDecodeContext& context)
623{
624 const Fields reply(body);
625 unsigned version = 0;
626 unsigned tow = 0;
627 unsigned validity = 0;
628 unsigned observations = 0;
629 unsigned configuredCount = 0;
630 double accuracy = 0;
632 if (reply.size() != 12 || !number(reply[1], version) || version != 1 || !number(reply[2], tow) ||
633 tow >= GPSTime::WEEK_MS) {
634 return false;
635 }
636 // TOW is a modular measurement clock, not a receipt timestamp. A late packet from before Sunday's rollover must
637 // not look newer than the current week.
638 if (_lastTow && !GPSTime::towAdvances(tow, *_lastTow)) {
639 return false;
640 }
641 _lastTow = tow;
642 if (_phase != SurveyPhase::Verifying && _phase != SurveyPhase::Monitoring) {
643 return false;
644 }
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);
651 return false;
652 }
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();
657 const bool coordinatesKnown = GPSProtocolMath::nearEarthSurface(ecef);
658 if (!matches || (!coordinatesKnown && (validity == 2 || (validity == 1 && observations != 0)))) {
659 _revokeSurvey(context);
660 return false;
661 }
662 if (fixed && validity == 2 && GPSProtocolMath::distance(ecef, _fixedECEF) > 0.001) {
663 _revokeSurvey(context);
664 return false;
665 }
666 // BOOT + identity + saved-role/base readback establish this survey's session.
667 // A one-observation survey can finish before any progress notification is sent.
668 const bool valid = validity == 2 && (fixed || observations >= configuredCount);
669 GPSSurveyReport report{};
670 // Fixed mode's MeanAcc=0 describes supplied coordinates, not measured position uncertainty.
671 if (!fixed && validity != 0 && observations != 0 && coordinatesKnown) {
673 }
674 // Base positioning is fixed at 1 Hz. Gaps do not count as accepted observation seconds.
675 report.duration = std::chrono::seconds(observations);
676 report.valid = valid;
677 report.active = validity == 1 && !valid;
678 if (coordinatesKnown && validity != 0) {
679 report.position = GPSProtocolMath::fromEcef(ecef);
680 }
681 report.timestampUs = context.nowUs();
682 _survey = report;
683 _publishSurvey(context);
684 return true;
685}
686
687QLatin1StringView signature(const GPSFrame& frame)
688{
689 const std::string_view text = frame.text();
690 const bool checked = text.starts_with("$PQTM") && !QuectelCodec::checkedBody(text).empty();
691 return frame.kind == GPSFrameKind::ASCIILine && checked ? QLatin1StringView("$PQTM sentences")
692 : QLatin1StringView();
693}
694
696bool Protocol::probe(GPSCommandChannel& channel)
697{
698 const auto query = Plan::command(Plan::IDENTIFY, [](std::string_view body) {
699 const Fields reply(body);
700 return reply.size() == 4 && reply[0] == Plan::IDENTIFY ? GPSCommandOutcome::Acknowledged
702 });
703 return channel.transact(query).succeeded();
704}
705
706} // namespace
707
708namespace Quectel {
709
711 .type = GPSType::quectel,
712 .logCategory = &QuectelProtocolLog,
713 .stream = GPSNMEAStream::STREAM,
714 .baudCandidates = Plan::BAUD_RATES,
715 .create = &gpsCreateProtocol<Protocol>,
716 .signature = &signature,
717};
718
719} // namespace Quectel
GPSBaudProbe
@ 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().
Definition GPSCommand.h:23
@ 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.
Config config
#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,...
uint64_t nowUs() const
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.
Definition AshtechPlan.h:31
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.
Definition GPSTime.h:18
constexpr uint32_t WEEK_MS
Milliseconds in a GPS week; every time of week is below it.
Definition GPSTime.h:9
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.
Definition SBFPlan.cc:49
std::variant< SurveyIn, Fixed, ReceiverAveraging > Mode
GPSConfigurationStep step
Evidence label and per-attempt timeout. A required command that fails ends the sequence.
Definition GPSCommand.h:133
The receiver configuration requested for one configure() attempt.
std::optional< double > meanAccuracyMeters