11#include <QtCore/QByteArray>
12#include <QtCore/QByteArrayView>
13#include <QtCore/QList>
14#include <QtCore/QString>
40std::string_view unquote(std::string_view field)
42 if (field.size() < 2 || field.front() !=
'"' || field.back() !=
'"') {
45 field.remove_prefix(1);
46 field.remove_suffix(1);
47 return field.find(
'"') == std::string_view::npos ? field : std::string_view{};
50bool supportedFirmware(std::string_view model, std::string_view firmware)
52 constexpr std::string_view PREFIX =
"R4.10Build";
53 if (!firmware.starts_with(PREFIX)) {
56 const auto build = NMEA::number<unsigned>(firmware.substr(PREFIX.size()));
58 return build && ((model ==
"UM980" && *build >= 7923) || (model ==
"UM982" && *build >= 7650));
63bool validXorChecksum(std::string_view line)
65 const auto star =
line.rfind(
'*');
66 return star != std::string_view::npos && star + 3 ==
line.size() &&
71bool modeMatches(std::string_view mode, Plan::Reply reply)
73 const auto starts = [mode](std::string_view name) {
74 return mode == name || (mode.starts_with(name) && mode.size() > name.size() && mode[name.size()] ==
' ');
77 case Plan::Reply::RoverMode:
78 return starts(
"MODE ROVER");
79 case Plan::Reply::AveragingMode:
80 return starts(
"MODE BASE TIME");
82 return mode ==
"MODE BASE";
112 if (std::holds_alternative<GPSBaseStationConfig::SurveyIn>(
config.base.mode)) {
115 _setBase(
config.base.mode);
121 std::chrono::milliseconds
nextReadSlice(std::chrono::milliseconds timeout)
const override
128 bool probe(
GPSCommandChannel& channel)
override {
return _identify(channel).succeeded() || !_model.isEmpty(); }
156 Plan::Reply _expectedMode = Plan::Reply::AveragingMode;
158 QByteArray _firmware;
162 QByteArray _reportedMode;
164 bool _reportedFixed =
false;
166 bool _acknowledgementMatched =
false;
170 uint64_t _lastBaseStatus = 0;
171 std::optional<uint64_t> _lastBaseEpoch;
172 bool _monitorBase =
false;
173 bool _baseValid =
false;
174 bool _averaging =
false;
180 (void) channel.
flush();
181 const auto* fixed = std::get_if<GPSBaseStationConfig::Fixed>(&
config.base.mode);
182 const auto* averaging = std::get_if<GPSBaseStationConfig::ReceiverAveraging>(&
config.base.mode);
183 if (!fixed && !averaging) {
185 QStringLiteral(
"Unicore requires receiver-managed averaging with a duration between 1 and 3600 seconds"));
186 return _failAttempt(channel);
188 _setBase(
config.base.mode);
189 const auto scope = channel.
deadlineScope(Plan::CONFIGURATION_TIMEOUT);
192 _nmea.reset(channel.
stream());
193 identity = _identify(channel);
200 if (!detection.found) {
201 if (!_rejection.isEmpty()) {
208 return _failAttempt(channel);
210 baud = detection.baud;
211 qCDebug(UnicoreProtocolLog).noquote() <<
"Unicore" << _model <<
"firmware" << _firmware;
212 const auto plan = fixed ? Plan::fixedBase(_fixedECEF) :
Plan::
averagingBase(averaging->maximumDuration);
214 return _failAttempt(channel);
216 _monitorBaseStatus(channel.
context());
217 if (!channel.
runRequired(_sequence(plan.output))) {
218 return _failAttempt(channel);
222 _nmea.setRTCMEnabled(_baseValid);
229 _monitorBase =
false;
231 _lastBaseEpoch.reset();
232 _nmea.setRTCMEnabled(
false);
237 _publishBase(context,
false,
false);
242 const auto* fixed = std::get_if<GPSBaseStationConfig::Fixed>(&mode);
244 _expectedMode = fixed ? Plan::Reply::FixedMode : Plan::Reply::AveragingMode;
253 return channel.
runSequence(_sequence({Plan::identify()}));
259 for (
const Plan::Command& command : commands) {
261 [
this, command](std::string_view line) { return _match(command, line); },
262 {.label = command.label}));
267GPSCommandOutcome Protocol::_match(
const Plan::Command& command, std::string_view line)
269 if (
line.starts_with(
"$command,")) {
270 const auto response =
line.find(
",response: ", 9);
271 if (response == std::string_view::npos ||
272 QByteArrayView(
line.substr(9, response - 9)).compare(
command.text, Qt::CaseInsensitive) != 0) {
275 _acknowledgementMatched =
true;
276 if (
line.substr(response + 11,
line.rfind(
'*') - response - 11) !=
"OK") {
283 case Plan::Reply::Acknowledgement:
285 case Plan::Reply::Version:
286 if (!
line.starts_with(
"#VERSIONA,")) {
289 if (supportedFirmware(_model.toStdString(), _firmware.toStdString())) {
292 _rejection = QStringLiteral(
293 "Unsupported Unicore receiver '%1' firmware '%2'; requires UM980 R4.10Build7923+ "
294 "or UM982 R4.10Build7650+")
295 .arg(QString::fromUtf8(_model), QString::fromUtf8(_firmware));
297 case Plan::Reply::RoverMode:
298 case Plan::Reply::AveragingMode:
299 case Plan::Reply::FixedMode:
300 if (!
line.starts_with(
"#MODE,")) {
305 case Plan::Reply::FixedPosition:
306 if (!
line.starts_with(
"#BESTNAVXYZA,") || !_reportedFixed) {
318 _publishBase(context, _averaging,
false);
324 _publishBase(channel.
context(),
false,
false);
327 _monitorBase =
false;
329 _nmea.setRTCMEnabled(
false);
330 (void) channel.
flush();
334GPSReceiveUpdates Protocol::_decodeLine(std::string_view line,
GPSDecodeContext& context)
336 if (
line.starts_with(
"$command,")) {
337 if (!context.
replyPending() || !validXorChecksum(line)) {
340 _acknowledgementMatched =
false;
344 if (!
line.starts_with(
'#')) {
347 const auto comma =
line.find(
',');
348 const auto name =
line.substr(1, comma - 1);
349 if (name !=
"VERSIONA" && name !=
"MODE" && name !=
"BESTNAVXYZA") {
356 const auto star =
line.rfind(
'*');
357 const auto semicolon =
line.find(
';');
358 if (semicolon == std::string_view::npos || semicolon >= star) {
361 std::array<std::string_view, 10> header{};
365 const auto body =
line.substr(semicolon + 1, star - semicolon - 1);
367 if (name ==
"VERSIONA") {
368 valid = _handleVersion(body, context);
369 }
else if (name ==
"MODE") {
370 valid = _handleMode(body, context);
375 const auto week = NMEA::number<uint16_t>(header[4]);
376 const auto milliseconds = NMEA::number<uint32_t>(header[5]);
377 if (header[2] !=
"GPS" || header[3] !=
"FINE" || !week || !milliseconds || *milliseconds >=
GPSTime::WEEK_MS) {
381 if (_lastBaseEpoch && epoch < *_lastBaseEpoch) {
382 _invalidateBase(context, QStringLiteral(
"Unicore base status went back in time; the receiver may have "
386 if (_lastBaseEpoch && epoch == *_lastBaseEpoch) {
389 _lastBaseEpoch = epoch;
390 valid = _handlePosition(body, context);
398bool Protocol::_handleVersion(std::string_view body,
GPSDecodeContext& context)
400 std::array<std::string_view, 6> fields{};
404 const auto model = unquote(fields[0]);
405 const auto firmware = unquote(fields[1]);
406 if (model.empty() || model.size() > 32 || firmware.empty() || firmware.size() > 32) {
410 _model = QByteArrayView(model).toByteArray();
411 _firmware = QByteArrayView(firmware).toByteArray();
414 _invalidateBase(context,
415 QStringLiteral(
"Unicore receiver reported its identity unasked; it may have restarted"));
422 const auto mode = body.substr(0, body.find(
','));
423 _reportedMode = QByteArrayView(mode).toByteArray();
424 if (_ready && !modeMatches(mode, _expectedMode)) {
425 _invalidateBase(context, QStringLiteral(
"Unicore receiver left the configured base mode"));
430bool Protocol::_handlePosition(std::string_view body,
GPSDecodeContext& context)
432 std::array<std::string_view, 28> fields{};
436 const auto x = NMEA::number<double>(fields[2]);
437 const auto y = NMEA::number<double>(fields[3]);
438 const auto z = NMEA::number<double>(fields[4]);
439 if (!x || !y || !z) {
445 _reportedPosition = coordinates;
446 _reportedFixed = fixed;
447 const bool matches = _averaging || samePosition(coordinates, _fixedECEF);
448 if (_ready && _baseValid && (!fixed || !matches || !samePosition(coordinates, _baseECEF))) {
449 _invalidateBase(context, QStringLiteral(
"Unicore base position was lost or changed"));
452 _lastBaseStatus = context.
nowUs();
453 _baseValid = fixed && matches;
455 _baseECEF = coordinates;
457 _nmea.setRTCMEnabled(_ready && _baseValid);
458 _publishBase(context, _averaging && !_baseValid, _baseValid);
462void Protocol::_publishBase(
GPSDecodeContext& context,
bool active,
bool valid)
468void Protocol::_invalidateBase(
GPSDecodeContext& context,
const QString& reason)
471 _monitorBase =
false;
473 _nmea.setRTCMEnabled(
false);
474 _publishBase(context,
false,
false);
480 if (_ready && (context.
failed() ||
482 _invalidateBase(context, QStringLiteral(
"Unicore base status stopped arriving"));
487QLatin1StringView signature(
const GPSFrame& frame)
489 const std::string_view text =
frame.text();
493 if (text.starts_with(
"$command,") && text.find(
",response: ") != std::string_view::npos && validXorChecksum(text)) {
494 return QLatin1StringView(
"Unicore command replies");
498 return unicore ? QLatin1StringView(
"Unicore ASCII logs") : QLatin1StringView();
506 .
type = GPSType::unicore,
507 .logCategory = &UnicoreProtocolLog,
510 .create = &gpsCreateProtocol<Protocol>,
511 .signature = &signature,
std::optional< GPSASCIILog > gpsASCIILog(std::string_view line)
@ Stop
The receiver answered but is not usable, so no other rate is tried.
@ 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={})
QString gpsReceiverIdentity(const QByteArray &model, const QByteArray &firmware)
A receiver identity as GPSFamilyProtocol::identity() reports it: "<model> <firmware>",...
@ 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)
GPSDecodeContext & context()
void failSequence(const GPSCommandSequence::Result &result)
void failControl(GPSProtocolError failure=GPSProtocolError::Protocol)
GPSDeadlineScope deadlineScope(std::chrono::milliseconds timeout)
GPSReceiveUpdates flush()
Flushes decoder state and delivers the resulting batch.
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)
void offerReply(std::string_view reply)
Passes one decoded text reply to the outstanding text matcher, if any.
void failControl(const QString &detail)
Records a protocol failure described by detail unless a failure is already recorded,...
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 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.
constexpr std::array BAUD_RATES
Rates the port query probes, in order. Trimble receivers default to 115200.
QByteArray line(std::string_view command)
command as one CR/LF-terminated line.
Ecef toEcef(const GPSEllipsoidPosition &position)
bool nearEarthSurface(const Ecef &position)
double distance(const Ecef &left, const Ecef &right)
Straight-line distance between left and right, in metres.
GPSEllipsoidPosition fromEcef(const Ecef &position)
constexpr uint64_t epochMs(uint32_t week, uint32_t towMs)
Milliseconds since the GPS epoch of towMs into week.
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.
std::optional< uint32_t > hexNumber(std::string_view digits)
The value of digits when every one is a hexadecimal digit, with no sign or prefix; nullopt otherwise.
unsigned char checksum(std::string_view body)
constexpr bool isAsciiDigit(char ch)
Locale-independent ASCII character classes for receiver text.
size_t splitFields(std::string_view text, std::span< std::string_view > fields)
Preserve empty fields, including the final field. Return zero on storage overflow.
std::optional< Frame > frame(std::string_view text)
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.
BaseStation averagingBase(std::chrono::seconds maximum)
Receiver-managed averaging for at most maximum.
const GPSReceiverFamily FAMILY
std::variant< SurveyIn, Fixed, ReceiverAveraging > Mode
std::variant< GPSTextMatcher, RawReply > reply
GPSCommandOutcome outcome
The failed command's last outcome.
std::vector< Command > steps
The receiver configuration requested for one configure() attempt.
WGS84 geodetic position with ellipsoid height, not mean-sea-level altitude.