QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
SBFProtocol.cc
Go to the documentation of this file.
1#include <array>
2#include <chrono>
3#include <cmath>
4#include <cstdint>
5#include <numbers>
6#include <optional>
7#include <string_view>
8#include <variant>
9
10#include <QtCore/QByteArray>
11#include <QtCore/QByteArrayView>
12#include <QtCore/QString>
13
14#include "GPSCommandChannel.h"
15#include "GPSDeadline.h"
16#include "GPSFamilyProtocol.h"
17#include "GPSProtocolEvent.h"
18#include "GPSProtocolMath.h"
19#include "GPSReceiverFamilies.h"
20#include "GPSStreamDemux.h"
21#include "GPSSurveyClock.h"
22#include "GPSTime.h"
23#include "NMEASentence.h"
24#include "QGCLoggingCategory.h"
25#include "SBF/SBFBlocks.h"
26#include "SBF/SBFPlan.h"
27#include "WireFields.h"
28
29QGC_LOGGING_CATEGORY(SBFProtocolLog, "GPS.Protocols.SBF")
30
31namespace {
32
33constexpr double DNU = 100000.0;
34constexpr qsizetype DESCRIPTOR_SIZE = 4;
35
36GPSPositionReport::FixType fixType(uint8_t modeType)
37{
38 switch (modeType) {
39 case 0:
40 return GPSPositionReport::FixType::NoFix;
41 case 2:
42 case 6:
43 return GPSPositionReport::FixType::Differential;
44 case 5:
45 case 8:
46 return GPSPositionReport::FixType::RTKFloat;
47 case 4:
48 case 7:
49 return GPSPositionReport::FixType::RTKFixed;
50 default:
51 return GPSPositionReport::FixType::Fix3D;
52 }
53}
54
57std::optional<QByteArray> promptDescriptor(QByteArrayView text)
58{
59 for (qsizetype end = text.indexOf('>'); end >= 0; end = text.indexOf('>', end + 1)) {
60 if (end < DESCRIPTOR_SIZE) {
61 continue;
62 }
63 const QByteArrayView descriptor = text.sliced(end - DESCRIPTOR_SIZE, DESCRIPTOR_SIZE);
64 const char before = end > DESCRIPTOR_SIZE ? text[end - DESCRIPTOR_SIZE - 1] : ' ';
65 const bool separated = !(NMEA::isAsciiUpper(before) || NMEA::isAsciiDigit(before));
66 const bool tail = (NMEA::isAsciiUpper(descriptor[2]) || NMEA::isAsciiDigit(descriptor[2])) &&
67 NMEA::isAsciiDigit(descriptor[3]);
68 if (separated && NMEA::isAsciiUpper(descriptor[0]) && NMEA::isAsciiUpper(descriptor[1]) && tail) {
69 return descriptor.toByteArray();
70 }
71 }
72 return std::nullopt;
73}
74
77bool detectPort(GPSCommandChannel& channel, QByteArray& port)
78{
80 if (!channel.write(SBF::Plan::PROMPT)) {
81 return false;
82 }
83 QByteArray received;
84 std::array<uint8_t, GPSCommandChannel::READ_CHUNK_SIZE> chunk{};
85 while (channel.nowUs() < deadline.untilUs) {
86 const int count = channel.read(chunk, deadline.remaining(channel.nowUs()));
87 if (count < 0) {
88 return false;
89 }
90 received.append(reinterpret_cast<const char*>(chunk.data()), count);
91 if (const auto descriptor = promptDescriptor(received)) {
92 port = *descriptor;
93 qCDebug(SBFProtocolLog).noquote() << "Septentrio GNSS receiver COM port:" << port;
94 return true;
95 }
96 // Keep enough for a prompt split across reads.
97 if (received.size() > 2 * static_cast<qsizetype>(chunk.size())) {
98 received = received.last(DESCRIPTOR_SIZE + 1);
99 }
100 }
101 return false;
102}
103
111class Protocol final : public GPSFamilyProtocol
112{
113public:
114 bool configure(GPSCommandChannel& channel, GPSConfig config, unsigned& baud) override;
115
116 bool probe(GPSCommandChannel& channel) override;
117
118 GPSReceiveUpdates onFrame(const GPSFrame& frame, GPSDecodeContext& context) override;
119
121 bool receiverReady() const override { return _ready; }
122
123 bool armDecodeOnly(const GPSConfig& config, GPSDecodeContext& context) override
124 {
125 context.stream().setEnabled(GPSFrameKind::RTCM3, true);
126 _startBase(config.base, context.nowUs());
127 return true;
128 }
129
130 GPSReceiveUpdates receive(GPSCommandChannel& channel, std::chrono::milliseconds timeout) override
131 {
132 if (!_ready || channel.failed()) {
133 return GPSReceiveUpdates{};
134 }
135 return channel.receiveCycle(timeout);
136 }
137
138private:
139 bool _configureBase(GPSCommandChannel& channel, const GPSBaseStationConfig& base, unsigned baud);
141 void _startBase(const GPSBaseStationConfig& base, uint64_t nowUs);
142 GPSReceiveUpdates _decodeBlock(const GPSFrame& frame, GPSDecodeContext& context);
144 bool _applyPVTGeodetic(const SBF::PVTGeodetic& pvt, GPSDecodedPosition& position, GPSDecodeContext& context) const;
145 void _publishSurveyStatus(const SBF::PVTGeodetic& pvt, const GPSDecodedPosition& position, bool coordinatesValid,
146 GPSDecodeContext& context);
147
148 bool _ready = false;
150 GPSSurveyClock _surveyClock;
151 bool _surveyActive = false;
153 std::optional<uint64_t> _lastEpoch;
154};
155
156bool Protocol::configure(GPSCommandChannel& channel, GPSConfig config, unsigned& baud)
157{
158 // A new configuration stops reporting the base, forgets survey progress and accepts any receiver time.
159 _ready = false;
160 _surveyClock.reset();
161 _surveyActive = false;
162 _lastEpoch.reset();
163 channel.stream().setEnabled(GPSFrameKind::RTCM3, false);
164 channel.stream().reset(GPSFrameKind::SBF);
165 // The link keeps the rate selected or detected; otherwise it runs at Plan::BAUD_RATE.
166 baud = config.linkBaud(baud, SBF::Plan::BAUD_RATE);
167 if (!_configureBase(channel, config.base, baud)) {
168 return false;
169 }
170 _startBase(config.base, channel.nowUs());
171 return true;
172}
173
174bool Protocol::_configureBase(GPSCommandChannel& channel, const GPSBaseStationConfig& base, unsigned baud)
175{
176 (void) channel.setBaudrate(baud);
178
179 if (!channel.runSequence(SBF::Plan::silenceCorrectionOutput()).succeeded()) {
180 return false;
181 }
182 QByteArray port;
183 if (!detectPort(channel, port)) {
184 channel.failControl(QStringLiteral("No command prompt from the Septentrio receiver"));
185 return false;
186 }
187 if (!channel.runRequired(SBF::Plan::port(port, baud))) {
188 return false;
189 }
190 channel.stream().setEnabled(GPSFrameKind::RTCM3, true);
191 return channel.runRequired(SBF::Plan::base(port, base)) && !channel.failed();
192}
193
194void Protocol::_startBase(const GPSBaseStationConfig& base, uint64_t nowUs)
195{
196 _base = base;
197 if (!std::holds_alternative<GPSBaseStationConfig::Fixed>(_base.mode)) {
198 _surveyClock.start(nowUs);
199 }
200 _ready = true;
201}
202
203GPSReceiveUpdates Protocol::onFrame(const GPSFrame& frame, GPSDecodeContext& context)
204{
205 if (frame.kind == GPSFrameKind::RTCM3) {
206 context.publishRTCM(frame.bytes);
207 return {};
208 }
209 return _decodeBlock(frame, context);
210}
211
212GPSReceiveUpdates Protocol::_decodeBlock(const GPSFrame& frame, GPSDecodeContext& context)
213{
214 const auto header = SBF::checkedHeader(frame.bytes);
215 if (!header || header->number() != SBF::BlockId::PVT_GEODETIC || header->length < Wire::SIZE<SBF::PVTGeodetic>) {
216 return {};
217 }
218 const auto pvt = Wire::decode<SBF::PVTGeodetic>(frame.bytes);
219 if (pvt.tow >= GPSTime::WEEK_MS || pvt.wnc == UINT16_MAX) {
220 return {};
221 }
222 const uint64_t epoch = GPSTime::epochMs(pvt.wnc, pvt.tow);
223 if (_lastEpoch && epoch <= *_lastEpoch) {
225 }
226 _lastEpoch = epoch;
227
229 // A base reports in the WGS84/ITRS datum (0) it was configured for. Other receivers report in theirs, such as a
230 // rover in its correction provider's datum (19), and are decoded as reported.
231 if (_ready && pvt.datum != 0) {
232 position.navigation.fixType = GPSPositionReport::FixType::NoFix;
234 qCWarning(SBFProtocolLog) << "Unsupported Septentrio position datum:" << unsigned(pvt.datum);
235 context.failControl(QStringLiteral("Septentrio position datum is not WGS84/ITRS"));
236 _ready = false;
237 context.stream().setEnabled(GPSFrameKind::RTCM3, false);
238 context.publishSurvey(false, false, {});
239 } else {
240 const bool coordinatesValid = _applyPVTGeodetic(pvt, position, context);
241 if (_ready) {
242 _publishSurveyStatus(pvt, position, coordinatesValid, context);
243 }
244 }
245 context.publishPosition(position);
247}
248
249bool Protocol::_applyPVTGeodetic(const SBF::PVTGeodetic& pvt, GPSDecodedPosition& position,
250 GPSDecodeContext& context) const
251{
252 position.navigation.fixType = fixType(pvt.modeType());
253 if (pvt.error != 0) {
254 position.navigation.fixType = GPSPositionReport::FixType::NoFix;
255 } else if (pvt.mode2D() && position.navigation.fixType >= GPSPositionReport::FixType::Fix3D) {
256 position.navigation.fixType = GPSPositionReport::FixType::Fix2D;
257 }
258
259 // Any value beyond the specified maximum is invalid, not only the do-not-use value (-2*10^10).
260 position.velocityValid = position.navigation.fixType > GPSPositionReport::FixType::NoFix && pvt.error == 0 &&
261 !(fabsf(pvt.vn) > 600.0f || fabsf(pvt.ve) > 600.0f || fabsf(pvt.vu) > 600.0f);
262
263 const bool coordinatesValid = std::isfinite(pvt.latitude) && std::abs(pvt.latitude) <= std::numbers::pi / 2 &&
264 std::isfinite(pvt.longitude) && std::abs(pvt.longitude) <= std::numbers::pi &&
265 std::isfinite(pvt.height) && std::abs(pvt.height) <= DNU;
266 if (!coordinatesValid || !std::isfinite(pvt.undulation) || std::abs(pvt.undulation) > DNU) {
267 position.navigation.fixType = GPSPositionReport::FixType::NoFix;
268 }
269
270 const auto satellitesUsed = gpsSatellitesUsed(pvt.nrSV); // 255 = do not use value
271 position.navigation.satellitesUsed = satellitesUsed;
272 context.publishSatelliteUsage(satellitesUsed);
273
277 position.navigation.altitudeMslMeters = pvt.height - static_cast<double>(pvt.undulation);
278
279 // Accuracy is reported as 2DRMS in cm; halve it for the RMS convention used by the other drivers.
281 pvt.hAccuracy != UINT16_MAX ? static_cast<float>(pvt.hAccuracy) / 200.0f : NAN;
283 pvt.vAccuracy != UINT16_MAX ? static_cast<float>(pvt.vAccuracy) / 200.0f : NAN;
284
285 position.navigation.speedMetersPerSecond = sqrtf(pvt.vn * pvt.vn + pvt.ve * pvt.ve);
286 position.navigation.courseRadians = std::isfinite(pvt.cog) && pvt.cog >= 0.0f && pvt.cog <= 360.0f
287 ? static_cast<float>(pvt.cog * GPSProtocolMath::DEG_TO_RAD)
288 : NAN;
289
290 // WNc/TOW is GNSS system time, not UTC. Without receiver UTC/leap information,
291 // retain the epoch key internally and let the facade use reception UTC.
294 return coordinatesValid;
295}
296
297void Protocol::_publishSurveyStatus(const SBF::PVTGeodetic& pvt, const GPSDecodedPosition& position,
298 bool coordinatesValid, GPSDecodeContext& context)
299{
300 // Mode bit 6 means automatic base determination is still in progress, not completed.
301 // Septentrio PolaRx5TR 5.5.0 Reference Guide, SBF Mode definition (p. 382).
302 const bool active = !std::holds_alternative<GPSBaseStationConfig::Fixed>(_base.mode) && pvt.modeAutoSet();
303 const bool valid = !pvt.modeAutoSet() && pvt.modeType() == 3 && !pvt.mode2D() && !pvt.error && coordinatesValid;
304 // The final update on the active-to-inactive transition freezes the survey duration.
305 if (active || _surveyActive) {
306 (void) _surveyClock.update(context.nowUs());
307 }
308 _surveyActive = active;
309 // PVT accuracy describes the navigation solution, not the averaged base survey.
310 GPSEllipsoidPosition surveyPosition;
311 if (coordinatesValid && !pvt.error) {
313 .longitudeDegrees = position.navigation.longitudeDegrees,
315 }
316 context.publishSurvey(active, valid, _surveyClock.duration(), surveyPosition);
317}
318
319QLatin1StringView signature(const GPSFrame& frame)
320{
321 const std::string_view text = frame.text();
322 if (frame.kind == GPSFrameKind::ASCIILine) {
323 return text.starts_with("$R:") || text.starts_with("$R?") ? QLatin1StringView("Septentrio command replies")
324 : QLatin1StringView();
325 }
326 // A candidate the framer truncated at its capture limit cannot be checked, so only short blocks count.
327 if (frame.kind != GPSFrameKind::SBF) {
328 return {};
329 }
330 const auto header = SBF::checkedHeader(frame.bytes);
331 return header && header->length % 4 == 0 ? QLatin1StringView("SBF blocks") : QLatin1StringView();
332}
333
335bool Protocol::probe(GPSCommandChannel& channel)
336{
337 const auto scope = channel.deadlineScope(SBF::Plan::PROMPT_TIMEOUT);
338 const QByteArrayView prompt = SBF::Plan::PROMPT;
339 channel.beginCommand({prompt.toByteArray(), SBF::Plan::PROMPT_TIMEOUT});
340 QByteArray port;
341 const bool answered = detectPort(channel, port);
343 : channel.failed() ? channel.failureOutcome()
345 return answered;
346}
347
348} // namespace
349
350namespace SBF {
351
353 .type = GPSType::septentrio,
354 .logCategory = &SBFProtocolLog,
355 .stream = {.framers = GPSFrameKind::SBF | GPSFrameKind::RTCM3, .enabled = GPSFrameKind::SBF},
356 .baudCandidates = Plan::BAUD_RATES,
357 .create = &gpsCreateProtocol<Protocol>,
358 .signature = &signature,
359};
360
361} // namespace SBF
GPSCommandOutcome
Definition GPSCommand.h:33
@ Protocol
The receiver violated or rejected the control protocol.
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.
@ SBF
Septentrio "$@" block candidates, up to the first 110 bytes; the decoder checks the CRC.
@ ASCIILine
Printable lines up to CR/LF, without the terminator; the decoder checks checksums.
@ RTCM3
0xD3 frames with a valid CRC-24Q.
Config config
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void beginCommand(GPSConfigurationStep step)
uint64_t nowUs() const
GPSReceiveUpdates receiveCycle(std::chrono::milliseconds timeout)
int read(std::span< uint8_t > buffer, std::chrono::milliseconds timeout)
Reads raw bytes without decoding them.
bool setBaudrate(unsigned baudrate)
void failControl(GPSProtocolError failure=GPSProtocolError::Protocol)
GPSDeadlineScope deadlineScope(std::chrono::milliseconds timeout)
GPSStreamDemux & stream()
bool write(QByteArrayView bytes)
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 finishCommand(GPSCommandOutcome outcome)
void publishSatelliteUsage(std::optional< int > count)
void publishPosition(const GPSDecodedPosition &report)
void failControl(const QString &detail)
Records a protocol failure described by detail unless a failure is already recorded,...
void publishRTCM(std::span< const uint8_t > frame)
uint64_t nowUs() const
GPSStreamDemux & stream()
void publishSurvey(GPSSurveyReport &status)
Stamps status with the receipt time and publishes a copy. Survey progress is not an update flag.
virtual bool receiverReady() const
virtual bool probe(GPSCommandChannel &channel)
virtual GPSReceiveUpdates receive(GPSCommandChannel &channel, std::chrono::milliseconds timeout)
One streaming receive. The default runs GPSCommandChannel::receiveCycle() and then the streaming serv...
virtual GPSReceiveUpdates onFrame(const GPSFrame &frame, GPSDecodeContext &context)=0
virtual bool armDecodeOnly(const GPSConfig &config, GPSDecodeContext &context)
virtual bool configure(GPSCommandChannel &channel, GPSConfig config, unsigned &baud)=0
void setEnabled(GPSFrameKind kind, bool enabled)
Enables or disables an available framer; either way it restarts from an empty state.
Host-timed survey-in duration for receivers that report completion but not elapsed time.
constexpr std::array BAUD_RATES
Rates the port query probes, in order. Trimble receivers default to 115200.
Definition AshtechPlan.h:31
GPSPositionReport position(const GPSDecodedPosition &source, const GPSIntegrityReport &diagnostic, uint64_t nowUs)
constexpr double DEG_TO_RAD
constexpr double RAD_TO_DEG
constexpr uint64_t epochMs(uint32_t week, uint32_t towMs)
Milliseconds since the GPS epoch of towMs into week.
Definition GPSTime.h:12
constexpr uint32_t WEEK_MS
Milliseconds in a GPS week; every time of week is below it.
Definition GPSTime.h:9
constexpr bool isAsciiUpper(char ch)
constexpr bool isAsciiDigit(char ch)
Locale-independent ASCII character classes for receiver text.
std::optional< Frame > frame(std::string_view text)
constexpr uint16_t PVT_GEODETIC
Definition SBFBlocks.h:35
constexpr QByteArrayView PROMPT("\n\r")
Asks for the command prompt, which starts with the connection descriptor, such as "USB1>".
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
constexpr unsigned BAUD_RATE
The link rate unless one is selected or detected. It is set on the host side first.
Definition SBFPlan.h:18
constexpr std::chrono::milliseconds PROMPT_TIMEOUT
Definition SBFPlan.h:27
constexpr QByteArrayView FORCE_COMMAND_INPUT("SSSSSSSSSS\n")
Forces the connection back to command input, whatever it was set to accept.
GPSCommandSequence port(QByteArrayView port, unsigned baud)
Clears the SBF output of port, keeps a serial port at the link's baud, and selects SBF and the WGS84 ...
Definition SBFPlan.cc:34
GPSCommandSequence silenceCorrectionOutput()
Definition SBFPlan.cc:25
Septentrio receivers configured as RTK bases with SBF commands; they stream PVTGeodetic blocks and RT...
const GPSReceiverFamily FAMILY
std::optional< BlockHeader > checkedHeader(std::span< const uint8_t > block)
The header of a block candidate whose length fits the candidate and whose CRC matches; nullopt otherw...
Definition SBFBlocks.h:97
Configuration used only by the RTK base-station role.
The receiver configuration requested for one configure() attempt.
static GPSDeadline after(uint64_t nowUs, std::chrono::microseconds timeout)
A negative timeout is already due at nowUs.
Definition GPSDeadline.h:14
uint64_t untilUs
Definition GPSDeadline.h:11
std::chrono::milliseconds remaining(uint64_t nowUs) const
Rounded up to whole milliseconds and capped at INT32_MAX milliseconds.
Definition GPSDeadline.h:20
Decoded navigation plus whether the producer accepted its velocity solution.
WGS84 geodetic position with ellipsoid height, not mean-sea-level altitude.
std::optional< uint8_t > satellitesUsed
GPSNavigationValues navigation
constexpr bool modeAutoSet() const
Definition SBFBlocks.h:61
constexpr bool mode2D() const
Definition SBFBlocks.h:63
constexpr uint8_t modeType() const
Definition SBFBlocks.h:59
uint16_t vAccuracy
Definition SBFBlocks.h:57
uint16_t hAccuracy
Definition SBFBlocks.h:56