QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
GPSCommandChannel.cc
Go to the documentation of this file.
1#include "GPSCommandChannel.h"
2
3#include <algorithm>
4#include <array>
5#include <cstdint>
6#include <utility>
7#include <vector>
8
9#include <QtCore/QScopeGuard>
10#include <QtCore/QScopedValueRollback>
11#include <QtCore/QStringList>
12
13#include "GPSFamilyProtocol.h"
14#include "GPSProtocolRuntime.h"
16#include "QGCLoggingCategory.h"
17
18QString gpsRateList(std::span<const unsigned> rates)
19{
20 QStringList names;
21 for (const unsigned rate : rates) {
22 names.append(QString::number(rate));
23 }
24 return names.join(QStringLiteral(", "));
25}
26
27GPSDeadlineScope::GPSDeadlineScope(GPSCommandChannel& channel, std::chrono::milliseconds timeout)
28 : GPSDeadlineScope(channel, GPSDeadline::after(channel.nowUs(), timeout))
29{}
30
32 : _channel(channel)
33 , _previous(channel._operationDeadline)
34{
35 _limitUntil(deadline.untilUs);
36}
37
39{
40 _channel._operationDeadline = _previous;
41}
42
43void GPSDeadlineScope::_limitUntil(uint64_t untilUs)
44{
45 _channel._operationDeadline.untilUs = std::min(_channel._operationDeadline.untilUs, untilUs);
46}
47
48GPSCommandChannel::GPSCommandChannel(GPSProtocolRuntime& runtime)
49 : _runtime(runtime)
50{}
51
53{
54 return _runtime.nowUs();
55}
56
58{
59 if (!failed()) {
60 _error = failure;
61 _errorDetail.clear();
62 }
63}
64
65void GPSCommandChannel::failControl(const QString& detail)
66{
67 if (!failed()) {
69 _errorDetail = detail;
70 }
71}
72
73QString GPSCommandChannel::_commandFailure(const QByteArray& label, GPSCommandOutcome outcome) const
74{
75 const QString verb =
76 outcome == GPSCommandOutcome::Rejected ? QStringLiteral("rejected") : QStringLiteral("did not acknowledge");
77 return QStringLiteral("%1 receiver %2 '%3'")
78 .arg(gpsReceiverName(_runtime.family().type), verb, QString::fromUtf8(label));
79}
80
82{
83 failControl(_commandFailure(result.failedLabel, result.outcome));
84}
85
86void GPSCommandChannel::_describeFailure()
87{
88 if (!_errorDetail.isEmpty() || _error == GPSProtocolError::Cancelled || _command.command.isEmpty() ||
89 _command.succeeded()) {
90 return;
91 }
92 _errorDetail = _commandFailure(_command.command, _command.outcome);
93}
94
95void GPSCommandChannel::failNoAnswer(std::span<const unsigned> rates)
96{
97 const QString receiver = gpsReceiverName(_runtime.family().type);
98 failControl(rates.empty()
99 ? QStringLiteral("The %1 receiver link cannot run at the requested baud rate").arg(receiver)
100 : QStringLiteral("No %1 receiver answered at %2 baud").arg(receiver, gpsRateList(rates)));
101}
102
118
119void GPSCommandChannel::_resetError()
120{
121 _error = GPSProtocolError::None;
122 _errorDetail.clear();
123}
124
125GPSConfigurationEvidence GPSCommandChannel::_failedAttempt(GPSConfigurationStep step) const
126{
128 failure.command = std::move(step.command);
129 failure.required = step.required;
130 failure.outcome = failureOutcome();
131 return failure;
132}
133
135{
136 if (failed()) {
137 // A pending attempt keeps its evidence for whatever ends it.
138 if (_commandCompleted) {
139 _command = _failedAttempt(std::move(step));
140 }
141 return;
142 }
144 _command = {};
145 _command.startedAtUs = nowUs();
146 _command.command = std::move(step.command);
147 _command.required = step.required;
148 _commandDeadline.untilUs =
149 std::min(_operationDeadline.untilUs, GPSDeadline::after(_command.startedAtUs, step.timeout).untilUs);
150 _commandCompleted = false;
151}
152
154{
155 if (_commandCompleted) {
156 return _command;
157 }
158 _command.outcome = outcome;
159 _commandCompleted = true;
160 // Completion observers may finish evidence again or begin another attempt.
161 const auto result = _command;
162 _runtime._commandFinished(result);
163 return result;
164}
165
166int GPSCommandChannel::read(std::span<uint8_t> buffer, std::chrono::milliseconds timeout)
167{
168 if (failed() || !buffer.data() || buffer.empty()) {
169 return failed() ? -1 : 0;
170 }
171 const auto result =
172 _runtime._io.read(buffer, {std::min(_operationDeadline.untilUs, GPSDeadline::after(nowUs(), timeout).untilUs)});
173 _errorDetail = result.detail;
174 if (result.status == GPSReadStatus::Data && result.bytesRead >= 0 &&
175 static_cast<size_t>(result.bytesRead) <= buffer.size()) {
176 return result.bytesRead;
177 }
178 if (result.status == GPSReadStatus::TimedOut && result.bytesRead == 0) {
179 return 0;
180 }
182 if (_error != GPSProtocolError::Cancelled) {
183 qCWarning(logCategory()).noquote() << QStringLiteral("Receiver read failed (status %1): %2")
184 .arg(static_cast<int>(result.status))
185 .arg(_errorDetail);
186 }
187 return -1;
188}
189
190GPSReceiveUpdates GPSCommandChannel::_readAndDecode(std::chrono::milliseconds timeout)
191{
192 std::array<uint8_t, READ_CHUNK_SIZE> buffer{};
193 const int count = read(buffer, timeout);
194 if (count < 0) {
195 return {};
196 }
197 return _runtime.consume(std::span<const uint8_t>(buffer).first(static_cast<size_t>(count)));
198}
199
200bool GPSCommandChannel::write(QByteArrayView bytes)
201{
202 if (failed()) {
203 return false;
204 }
205 if (!bytes.data()) {
206 _errorDetail.clear();
208 return false;
209 }
210 const uint64_t deadline =
211 std::min(_operationDeadline.untilUs, _commandCompleted ? UINT64_MAX : _commandDeadline.untilUs);
212 // A late command that wrote nothing just times out. A late later part goes to the transport, whose sticky failure
213 // stops the session rather than leave a torn frame on the link.
214 if (nowUs() >= deadline && (_commandCompleted || _command.acceptedBytes == 0)) {
216 return false;
217 }
218 const auto result =
219 _runtime._io.cancelToken.isCancelled()
221 : _runtime._io.write({reinterpret_cast<const uint8_t*>(bytes.data()), static_cast<size_t>(bytes.size())},
222 {deadline});
223 const auto length = bytes.size();
224 _errorDetail = result.detail;
225 // A write outside an attempt belongs to no reported evidence.
226 if (!_commandCompleted) {
227 _command.acceptedBytes += result.acceptedBytes;
228 _command.writtenBytes += result.writtenBytes;
229 }
230 if (result.status == GPSWriteStatus::Completed && result.acceptedBytes == length && result.writtenBytes == length &&
231 result.uncertainBytes() == 0) {
232 return true;
233 }
234 if (result.status == GPSWriteStatus::Unsupported && result.acceptedBytes == 0 && result.writtenBytes == 0) {
236 return false;
237 }
238 // The transport found the command late before taking any of its bytes, as the check above can just miss.
239 if (result.status == GPSWriteStatus::TimedOut && result.acceptedBytes == 0 && result.writtenBytes == 0 &&
240 (_commandCompleted || _command.acceptedBytes == 0)) {
242 return false;
243 }
245 if (_error == GPSProtocolError::Transport && _errorDetail.isEmpty()) {
246 const QString receiver = gpsReceiverName(_runtime.family().type);
247 const QString command = _commandCompleted ? QString() : QString::fromUtf8(_command.command);
248 if (command.isEmpty()) {
249 _errorDetail = QStringLiteral("Could not write to the %1 receiver").arg(receiver);
250 } else if (result.acceptedBytes > 0) {
251 _errorDetail = QStringLiteral("%1 receiver did not accept the full '%2' command").arg(receiver, command);
252 } else {
253 _errorDetail = QStringLiteral("Could not write '%2' to the %1 receiver").arg(receiver, command);
254 }
255 }
258 return false;
259}
260
261bool GPSCommandChannel::setBaudrate(unsigned baudrate)
262{
263 return _setBaudrate(baudrate) == GPSBaudStatus::Configured;
264}
265
266GPSBaudStatus GPSCommandChannel::_setBaudrate(unsigned baudrate)
267{
268 if (failed()) {
270 }
271 _errorDetail.clear();
272 const auto status =
273 _runtime._io.cancelToken.isCancelled() ? GPSBaudStatus::Cancelled : _runtime._io.setBaudrate(baudrate);
274 if (status == GPSBaudStatus::Cancelled) {
276 } else if (status == GPSBaudStatus::Error) {
278 _errorDetail = QStringLiteral("Cannot set the receiver link to %1 baud").arg(baudrate);
279 }
280 return status;
281}
282
283void GPSCommandChannel::wait(std::chrono::microseconds duration)
284{
285 if (failed()) {
286 return;
287 }
288 const auto now = nowUs();
289 const auto remaining = _operationDeadline.untilUs > now ? _operationDeadline.untilUs - now : 0;
290 const auto untilDeadline = std::chrono::microseconds(std::min<uint64_t>(remaining, INT64_MAX));
291 if (!_runtime._io.clock.wait(std::min(duration, untilDeadline))) {
293 _errorDetail.clear();
294 }
295}
296
298{
299 const auto scope = deadlineScope(step.timeout);
300 beginCommand(std::move(step));
301 return write(bytes);
302}
303
305{
306 if (_commandCompleted) {
307 return _command;
308 }
309 const auto scope = deadlineScope(_commandDeadline);
310 const uint64_t deadline = _operationDeadline.untilUs;
312 for (;;) {
313 if (const auto failure = failureOutcome(); failure != GPSCommandOutcome::Pending) {
314 outcome = failure;
315 break;
316 }
317 if (const auto response = reply(); response != GPSCommandOutcome::Pending) {
318 outcome = response;
319 break;
320 }
321 if (nowUs() >= deadline) {
323 break;
324 }
325 (void) _readAndDecode(_commandDeadline.remaining(nowUs()));
326 }
327 return completeCommand(outcome);
328}
329
330void GPSCommandChannel::_clearReply()
331{
332 _reply.reset();
333 _replyMatcher = {};
334}
335
336GPSConfigurationEvidence GPSCommandChannel::_transact(GPSConfigurationStep step, QByteArrayView bytes,
337 ReplyMatcher matcher)
338{
339 const auto guard = qScopeGuard([this] { _clearReply(); });
340 _replyMatcher = std::move(matcher);
341 if (failed()) {
342 return _failedAttempt(std::move(step));
343 }
345 if (!writeCommand(std::move(step), bytes)) {
347 }
348 return awaitReply([this] { return _reply.value_or(GPSCommandOutcome::Pending); });
349}
350
351void GPSCommandChannel::_offerText(std::string_view reply)
352{
353 const auto* text = std::get_if<GPSTextMatcher>(&_replyMatcher);
354 if (text && *text && _replyPending()) {
355 _resolveReply((*text)(reply));
356 }
357}
358
359void GPSCommandChannel::_offerFrame(const GPSFrame& frame)
360{
361 const auto* frameReply = std::get_if<GPSFrameMatcher>(&_replyMatcher);
362 if (frameReply && *frameReply && _replyPending()) {
363 _resolveReply((*frameReply)(frame));
364 }
365}
366
367void GPSCommandChannel::_offerRaw(std::span<const uint8_t> bytes)
368{
369 if (auto* const* raw = std::get_if<GPSRawAckMatcher*>(&_replyMatcher)) {
370 (*raw)->append(bytes);
371 _resolveReply((*raw)->outcome());
372 }
373}
374
376{
377 if (const auto* raw = std::get_if<GPSCommandSequence::RawReply>(&command.reply)) {
378 GPSRawAckMatcher matcher(raw->accepted, raw->rejected);
379 return _transact(command.step, command.wire, &matcher);
380 }
381 return transact(command.step, command.wire, std::get<GPSTextMatcher>(command.reply));
382}
383
385{
386 for (size_t index = 0; index < sequence.steps.size(); ++index) {
387 const auto& command = sequence.steps[index];
389 for (unsigned attempt = 0; attempt < (std::max) (command.attempts, 1U); ++attempt) {
390 result = transact(command);
391 if (result.succeeded() || failed()) {
392 break;
393 }
394 }
395 if (!result.succeeded() && (command.step.required || failed())) {
397 .failedStep = index, .failedLabel = command.step.command, .outcome = result.outcome};
398 }
399 }
401}
402
404{
405 const auto result = runSequence(sequence);
406 if (!result.succeeded()) {
407 failSequence(result);
408 }
409 return result.succeeded();
410}
411
412GPSBaudDetection GPSCommandChannel::detectBaud(std::span<const unsigned> candidates, unsigned baud,
414{
415 std::vector<unsigned> order;
416 if (baud) {
417 order.push_back(baud);
418 } else {
419 order.assign(candidates.begin(), candidates.end());
420 if (_detectedBaud != 0) {
421 // Detection may recognise a receiver by its traffic at a rate the family does not list.
422 std::erase(order, _detectedBaud);
423 order.insert(order.begin(), _detectedBaud);
424 }
425 }
426 GPSBaudDetection result{.baud = baud};
427 for (const unsigned candidate : order) {
428 result.baud = candidate;
429 const GPSBaudStatus status = _setBaudrate(candidate);
430 if (status == GPSBaudStatus::Unsupported && !baud) {
431 continue;
432 }
433 if (status != GPSBaudStatus::Configured) {
434 result.linkFailed = true;
435 return result;
436 }
437 result.probed.push_back(candidate);
438 const GPSBaudProbe outcome = probe();
439 if (outcome == GPSBaudProbe::Found) {
440 result.found = true;
441 return result;
442 }
443 if (outcome == GPSBaudProbe::Stop || failed()) {
444 return result;
445 }
446 }
447 return result;
448}
449
450GPSReceiveUpdates GPSCommandChannel::receiveCycle(std::chrono::milliseconds timeout)
451{
452 const auto scope = deadlineScope(timeout);
453 if (failed()) {
454 return GPSReceiveUpdates{};
455 }
456 GPSFamilyProtocol& protocol = _runtime.protocol();
457 GPSReceiveUpdates handled;
458 std::array<uint8_t, READ_CHUNK_SIZE> buffer{};
459 for (;;) {
460 if (protocol.completeReceiveCycle(handled)) {
461 return handled;
462 }
463 const int count = read(buffer, protocol.nextReadSlice(timeout));
464 if (count < 0) {
465 return handled;
466 }
467 handled |= _runtime.consume(std::span<const uint8_t>(buffer).first(static_cast<size_t>(count)));
468 if (nowUs() >= _operationDeadline.untilUs) {
469 return handled;
470 }
471 }
472}
473
474void GPSCommandChannel::receiveFor(std::chrono::milliseconds duration)
475{
476 const GPSDeadline until = GPSDeadline::after(nowUs(), duration);
477 while (nowUs() < until.untilUs) {
478 (void) _runtime.protocol().receive(*this, until.remaining(nowUs()));
479 if (failed()) {
480 return;
481 }
482 }
483}
484
485bool GPSCommandChannel::receiveUntil(std::function<bool()> done, std::chrono::milliseconds duration)
486{
487 const GPSDeadline until = GPSDeadline::after(nowUs(), duration);
488 while (!done() && nowUs() < until.untilUs) {
489 (void) receiveCycle(until.remaining(nowUs()));
490 if (failed()) {
491 return false;
492 }
493 }
494 return done();
495}
496
498{
499 if (_servicingControls || failed()) {
500 return;
501 }
502 {
503 const QScopedValueRollback servicing(_servicingControls, true);
504 // This budget belongs to pending receiver commands, independently of the completed read slice.
505 const auto scope = deadlineScope(SERVICE_TIMEOUT);
506 _runtime.protocol().serviceStreaming(*this);
507 }
508 (void) flush();
509}
510
511GPSReceiveUpdates GPSCommandChannel::flush()
512{
513 return _runtime.consume({});
514}
515
517{
518 return _runtime.stream();
519}
520
522{
523 return _runtime._context;
524}
525
527{
528 return _runtime.logCategory();
529}
QString gpsRateList(std::span< const unsigned > rates)
rates as a diagnostic names them, such as "38400, 9600".
GPSBaudProbe
@ Stop
The receiver answered but is not usable, so no other rate is tried.
QString gpsRateList(std::span< const unsigned > rates)
rates as a diagnostic names them, such as "38400, 9600".
GPSBaudStatus
How the link answered a baud rate change.
std::function< GPSBaudProbe()> GPSBaudProbeFunction
Probes the receiver at the rate the link was just set to.
std::function< GPSCommandOutcome()> GPSReplyPoll
Polled after each decoded chunk, for replies a family tracks in its own decoder state; Pending keeps ...
Definition GPSCommand.h:73
GPSCommandOutcome
Definition GPSCommand.h:33
GPSProtocolError
Sticky failure of the current receiver session; I/O and control stop until the next configure().
Definition GPSCommand.h:23
@ InvalidArgument
The driver requested an invalid write, or received before configuring.
@ 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.
@ Transport
The link could not read, write, or change its baud rate.
const QLoggingCategory &(*)() GPSLogCategory
A family's logging category function, as declared by Q_DECLARE_LOGGING_CATEGORY; usable as qCWarning(...
QString gpsReceiverName(GPSType type)
The name of type's receiver family as shown to users; empty when no descriptor exists.
bool isCancelled() const
Safe to call from any thread.
void wait(std::chrono::microseconds duration)
Waits for duration, capped by the operation deadline.
void beginCommand(GPSConfigurationStep step)
void serviceControls()
Runs the family's streaming services once under SERVICE_TIMEOUT, then flushes. Nested calls do nothin...
GPSLogCategory logCategory() const
GPSBaudDetection detectBaud(std::span< const unsigned > candidates, unsigned baud, GPSBaudProbeFunction probe)
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.
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)
GPSDeadlineScope deadlineScope(std::chrono::milliseconds timeout)
bool receiveUntil(std::function< bool()> done, std::chrono::milliseconds duration)
static constexpr std::chrono::milliseconds SERVICE_TIMEOUT
Budget of the streaming services that run after each receive.
GPSConfigurationEvidence completeCommand(GPSCommandOutcome outcome)
Retires the current attempt and reports it; repeated completion returns the retained evidence unchang...
GPSReceiveUpdates flush()
Flushes decoder state and delivers the resulting batch.
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...
GPSConfigurationEvidence awaitReply(GPSReplyPoll reply)
Decodes received traffic until reply resolves the current command, its deadline expires,...
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...
GPSConfigurationEvidence transact(GPSConfigurationStep step, QByteArrayView bytes)
GPSCommandOutcome failureOutcome() const
The command outcome that reports the sticky failure; Pending while healthy.
void finishCommand(GPSCommandOutcome outcome)
GPSDeadlineScope(GPSCommandChannel &channel, std::chrono::milliseconds timeout)
virtual void serviceStreaming(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 bool completeReceiveCycle(GPSReceiveUpdates handled)
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.
GPSFamilyProtocol & protocol()
const GPSReceiverFamily & family() const
GPSReceiveUpdates consume(std::span< const uint8_t > bytes)
Decodes bytes and delivers their batch to the observer. An empty span flushes.
GPSStreamDemux & stream()
GPSLogCategory logCategory() const
The family's logging category, or the runtime's when the family has none.
Bounded raw-byte matching for receivers whose replies need not be complete ASCII lines.
Definition GPSCommand.h:77
unsigned baud
The rate that was found, or the last rate attempted.
std::function< bool(std::chrono::microseconds)> wait
Waits duration on this clock; false when the wait was cancelled. Empty means a real wait.
Definition GPSClock.h:14
GPSConfigurationStep step
Evidence label and per-attempt timeout. A required command that fails ends the sequence.
Definition GPSCommand.h:133
std::variant< GPSTextMatcher, RawReply > reply
Definition GPSCommand.h:135
GPSCommandOutcome outcome
The failed command's last outcome.
Definition GPSCommand.h:145
std::optional< size_t > failedStep
The failed required step, or the step during which a sticky failure ended the sequence.
Definition GPSCommand.h:142
std::vector< Command > steps
Definition GPSCommand.h:150
Evidence from one configuration command, not a claim of physical receiver operation.
Definition GPSCommand.h:46
GPSCommandOutcome outcome
Definition GPSCommand.h:48
uint64_t startedAtUs
When the attempt began; its deadline counts from here.
Definition GPSCommand.h:50
std::chrono::milliseconds timeout
Definition GPSCommand.h:64
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
std::function< GPSReadResult(std::span< uint8_t >, GPSDeadline)> read
GPSCancelToken cancelToken
Checked before each write and baud change; reads observe it in the transport.
std::function< GPSWriteResult(std::span< const uint8_t >, GPSDeadline)> write
GPSClock clock
Without a wait, the runtime sleeps in short slices until the duration elapses or cancelToken is cance...
std::function< GPSBaudStatus(unsigned)> setBaudrate