QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NTRIPManager.cc
Go to the documentation of this file.
1#include "NTRIPManager.h"
2
3#include <chrono>
4#include <utility>
5
6#include <QtCore/QUrl>
7#include <QtNetwork/QHostAddress>
8
10#include "NTRIPError.h"
11#include "NTRIPHttpTransport.h"
12#include "QGCLoggingCategory.h"
14#include "RuntimeScheduler.h"
15
16QGC_LOGGING_CATEGORY(NTRIPManagerLog, "GPS.NTRIPManager")
17
18namespace {
19
20bool isRetryable(NTRIPError error)
21{
22 switch (error) {
23 case NTRIPError::AuthFailed:
24 case NTRIPError::InvalidConfig:
25 case NTRIPError::RequestRejected:
26 // TLS failures recur the same way until the user changes the TLS settings.
27 case NTRIPError::SslError:
28 return false;
29 default:
30 return true;
31 }
32}
33
34QString correctionSourceInstance(const NTRIPConnectionConfig& connection)
35{
36 QUrl endpoint = connection.url();
37 endpoint.setScheme(connection.useTls ? QStringLiteral("ntrips") : QStringLiteral("ntrip"));
38 return endpoint.toString(QUrl::FullyEncoded);
39}
40
41} // namespace
42
44{
45 QVector<int> ids;
46 for (const auto& token : whitelist.split(QLatin1Char(','), Qt::SkipEmptyParts)) {
47 bool ok = false;
48 const int id = token.trimmed().toInt(&ok);
49 if (ok && id > 0) {
50 ids.append(id);
51 }
52 }
53 return ids;
54}
55
56// -----------------------------------------------------------------------------
57// Lifecycle
58// -----------------------------------------------------------------------------
59
60NTRIPManager::NTRIPManager(QObject* parent, RuntimeScheduler* scheduler, const Dependencies& dependencies)
61 : QObject(parent)
62 , _scheduler(RuntimeScheduler::orDefault(scheduler, this))
63 , _settingsDebounceTask(_scheduler, this)
64 , _reconnectTask(_scheduler, this)
65 , _ggaReporter(this, _scheduler)
66 // The display agrees with routing, which stops using a stream after the same age.
67 , _stats(GPSCorrectionSelector::FRESHNESS_TIMEOUT, this, _scheduler)
68 , _correctionManager(dependencies.corrections)
69 , _networkMonitor(dependencies.network)
70 , _sourceTableController(this, _scheduler)
71{
72 qCDebug(NTRIPManagerLog) << this;
73
75
76 connect(&_sourceTableController, &NTRIPSourceTableController::certificatePinned, this,
77 &NTRIPManager::_setPinnedCertificate);
78
79 if (_networkMonitor) {
80 connect(_networkMonitor, &QGCNetworkAvailabilityMonitor::availableChanged, this, [this](bool available) {
81 if (!available || _shutdown || _connectionStatus != ConnectionStatus::Reconnecting || !_waitingForNetwork) {
82 return;
83 }
84 _waitingForNetwork = false;
85 _dispatch(Event::ReconnectDue);
86 _publish();
87 });
88 }
89}
90
92{
93 qCDebug(NTRIPManagerLog) << this;
94 // Observers must not see the teardown of an object that is being destroyed.
95 blockSignals(true);
96 shutdown();
97}
98
100{
101 return _connectionStatus == ConnectionStatus::Connected ? _running.connection.credentialsInClearWarning()
102 : QString();
103}
104
106{
107 switch (_connectionStatus) {
109 return tr("Disconnected");
111 return tr("Connecting");
113 return tr("Connected");
115 return tr("Reconnecting");
117 break;
118 }
119 return tr("Error");
120}
121
123{
124 if (_initialized || _shutdown) {
125 qCWarning(NTRIPManagerLog) << "Already initialized or shut down";
126 return;
127 }
128 _initialized = true;
129 _ggaReporter.configure(_configuration.gga);
130 _applyConfiguration();
131}
132
134{
135 if (_shutdown || configuration == _configuration) {
136 return;
137 }
138 const bool ggaChanged = configuration.gga != _configuration.gga;
139 const bool streamChanged = configuration.enabled != _configuration.enabled ||
140 configuration.connection != _configuration.connection ||
141 configuration.filter != _configuration.filter;
142 _configuration = configuration;
143 // Opting out forgets the pinned certificate, so opting in again trusts the caster's current certificate.
144 QString& pin = _configuration.connection.pinnedCertificate;
145 const bool pinForgotten = !_configuration.connection.allowSelfSignedCerts && !pin.isEmpty();
146 if (pinForgotten) {
147 pin.clear();
148 }
149 if (_initialized && ggaChanged) {
150 _ggaReporter.configure(configuration.gga);
151 }
152 if (_initialized && streamChanged) {
153 _settingsDebounceTask.schedule(SETTINGS_DEBOUNCE, [this]() { _applyConfiguration(); });
154 }
155 if (pinForgotten) {
156 emit certificatePinChanged(QString());
157 }
158}
159
162{
163 if (_initialized || _transport) {
164 qCWarning(NTRIPManagerLog) << "Inject GGA position providers before initializing NTRIP";
165 return;
166 }
167 _ggaReporter.setPositionProvider(source, std::move(provider));
168}
169
170// -----------------------------------------------------------------------------
171// Public control surface
172// -----------------------------------------------------------------------------
173
175{
176 if (_shutdown || _connectionStatus != ConnectionStatus::Error) {
177 return;
178 }
179 _configuration.enabled = true;
180 _start();
181 emit enableRequested();
182}
183
185{
186 if (_shutdown) {
187 return;
188 }
189 _shutdown = true;
190 _sourceTableController.cancel();
191 _stop();
192}
193
194void NTRIPManager::fetchMountpoints(const QGeoCoordinate& sortCoordinate)
195{
196 if (_shutdown) {
197 return;
198 }
199 _sourceTableController.fetch(_configuration.connection, sortCoordinate);
200}
201
202// -----------------------------------------------------------------------------
203// State machine
204// -----------------------------------------------------------------------------
205
206void NTRIPManager::_start()
207{
208 if (_shutdown || _connectionStatus == ConnectionStatus::Connecting ||
209 _connectionStatus == ConnectionStatus::Connected) {
210 return;
211 }
212 _settingsDebounceTask.cancel();
213 _cancelReconnect();
214 _reconnectBackoff.reset();
215 _dispatch(Event::StartRequested);
216 _publish();
217}
218
219void NTRIPManager::_stop()
220{
221 _settingsDebounceTask.cancel();
222 _cancelReconnect();
223 _dispatch(Event::StopRequested);
224 _publish();
225}
226
227void NTRIPManager::_publish()
228{
229 // Each value is recorded before its signal, so an observer that re-enters the manager cannot cause a repeat.
230 if (std::exchange(_notified.status, _connectionStatus) != _connectionStatus) {
232 }
233 if (std::exchange(_notified.message, _statusMessage) != _statusMessage) {
235 }
236 if (const QString warning = securityWarning(); std::exchange(_notified.securityWarning, warning) != warning) {
238 }
239}
240
241bool NTRIPManager::_dispatch(Event ev, const QString& detail, std::chrono::milliseconds retryAfter)
242{
243 using CS = ConnectionStatus;
244
245 struct Transition
246 {
247 CS from;
248 Event event;
249 CS to;
250 };
251
252 // Every legal transition; events not listed for the current state are ignored. Entry actions are in
253 // _onEnterState().
254 static constexpr Transition kTransitions[] = {
255 // Disconnected: user or settings can kick us into Connecting.
256 {CS::Disconnected, Event::StartRequested, CS::Connecting},
257
258 // Connecting: transport handshake outcome.
259 {CS::Connecting, Event::TransportConnected, CS::Connected},
260 {CS::Connecting, Event::TransportError, CS::Reconnecting},
261 {CS::Connecting, Event::TransportFatalError, CS::Error},
262 {CS::Connecting, Event::ConfigInvalid, CS::Error},
263 {CS::Connecting, Event::StopRequested, CS::Disconnected},
264
265 // Connected: streaming; may lose link or be stopped.
266 {CS::Connected, Event::TransportError, CS::Reconnecting},
267 {CS::Connected, Event::TransportFatalError, CS::Error},
268 {CS::Connected, Event::StopRequested, CS::Disconnected},
269 {CS::Connected, Event::HotReconfigure, CS::Connecting},
270
271 // Self-transition: a transport-affecting setting changed mid-handshake. Re-runs the Connecting entry action
272 // (teardown + restart) without a state change so the in-flight attempt picks up the new config.
273 {CS::Connecting, Event::HotReconfigure, CS::Connecting},
274
275 // Reconnecting: backoff timer owns when we try again, but user/settings can short-circuit it.
276 {CS::Reconnecting, Event::ReconnectDue, CS::Connecting},
277 {CS::Reconnecting, Event::ReconnectGaveUp, CS::Error},
278 {CS::Reconnecting, Event::StopRequested, CS::Disconnected},
279 {CS::Reconnecting, Event::StartRequested, CS::Connecting},
280
281 // Error: user-visible fault. User retry or stop both resolve it.
282 {CS::Error, Event::StartRequested, CS::Connecting},
283 {CS::Error, Event::StopRequested, CS::Disconnected},
284 };
285
286 for (const auto& row : kTransitions) {
287 if (row.from == _connectionStatus && row.event == ev) {
288 _enterState(row.to, detail, retryAfter);
289 return true;
290 }
291 }
292 qCDebug(NTRIPManagerLog) << "NTRIP event" << ev << "ignored in state" << _connectionStatus;
293 return false;
294}
295
296void NTRIPManager::_enterState(ConnectionStatus to, const QString& detail, std::chrono::milliseconds retryAfter)
297{
298 const ConnectionStatus from = _connectionStatus;
299 const QString msg = detail.isEmpty() ? _defaultMessageFor(to) : detail;
300 if (from != to) {
301 _waitingForNetwork = false;
302 qCDebug(NTRIPManagerLog) << "NTRIP state" << from << "→" << to << msg;
303 }
304
305 // Commit state + message before running entry actions so that a recursive
306 // _dispatch() from inside an entry action (e.g. _openSession → ConfigInvalid)
307 // observes the already-committed state, not the stale caller value.
308 _connectionStatus = to;
309 _statusMessage = msg;
310
311 // Entry action runs on every dispatched transition, including self-transitions
312 // (e.g. Connecting→Connecting on HotReconfigure).
313 _onEnterState(to, retryAfter);
314}
315
316QString NTRIPManager::_defaultMessageFor(ConnectionStatus state)
317{
318 switch (state) {
320 return tr("Disconnected");
322 return tr("Connecting...");
324 return tr("Connected");
326 return tr("Reconnecting...");
328 return {}; // Error always carries a detail from the caller.
329 }
330 return {};
331}
332
333void NTRIPManager::_onEnterState(ConnectionStatus to, std::chrono::milliseconds retryAfter)
334{
335 switch (to) {
338 _cancelReconnect();
339 _stopStreaming();
340 _running = {};
342 _correctionSource.reset();
343 }
344 break;
345
347 _cancelReconnect();
348 _openSession();
349 break;
350
352 _ggaReporter.start(_transport);
353 _stats.start();
354 break;
355
357 _stopStreaming();
358 _scheduleReconnect(retryAfter);
359 break;
360 }
361}
362
363// -----------------------------------------------------------------------------
364// Entry-action helpers
365// -----------------------------------------------------------------------------
366
367void NTRIPManager::_stopStreaming()
368{
369 const QPointer<NTRIPTransport> transport = std::exchange(_transport, nullptr);
370 if (transport) {
371 transport->disconnect(this);
372 transport->stop();
373 transport->deleteLater();
374 }
375 _ggaReporter.stop();
376 _stats.stop();
377}
378
379void NTRIPManager::_scheduleReconnect(std::chrono::milliseconds retryAfter)
380{
381 if (_shouldWaitForNetwork()) {
382 _waitForNetwork();
383 return;
384 }
385
386 if (_reconnectExhausted()) {
387 _dispatch(Event::ReconnectGaveUp,
388 tr("Gave up after %1 reconnect attempts: %2").arg(MAX_RECONNECT_ATTEMPTS).arg(_lastFailureDetail));
389 return;
390 }
391 const ExponentialBackoff beforeAttempt = _reconnectBackoff;
392 const auto backoff = _reconnectBackoff.next(retryAfter);
393 _reconnectTask.schedule(backoff, [this, beforeAttempt]() {
394 if (_shouldWaitForNetwork()) {
395 // The attempt never ran, so the next one keeps its delay.
396 _reconnectBackoff = beforeAttempt;
397 _waitForNetwork();
398 } else {
399 _dispatch(Event::ReconnectDue);
400 }
401 _publish();
402 });
403}
404
405void NTRIPManager::_cancelReconnect()
406{
407 _reconnectTask.cancel();
408 _waitingForNetwork = false;
409}
410
411bool NTRIPManager::_shouldWaitForNetwork() const
412{
413 return _networkMonitor && !_networkMonitor->available() && !_casterIsLoopback();
414}
415
416bool NTRIPManager::_casterIsLoopback() const
417{
418 const QString host = _configuration.connection.host.trimmed();
419 if (host.compare(QStringLiteral("localhost"), Qt::CaseInsensitive) == 0) {
420 return true;
421 }
422
423 QHostAddress address;
424 return address.setAddress(host) && address.isLoopback();
425}
426
427void NTRIPManager::_waitForNetwork()
428{
429 _waitingForNetwork = true;
430 _statusMessage = tr("Waiting for network");
431}
432
433void NTRIPManager::_openSession()
434{
435 _stopStreaming();
436
437 const Configuration config = _configuration;
438 const auto& connection = config.connection;
439
440 _correctionSource.reset();
441 if (_correctionManager) {
442 _correctionSource =
443 _correctionManager->openSource(GPSCorrectionSettings::Ntrip, correctionSourceInstance(connection));
444 }
445
446 if (const QString err = connection.streamValidationError(); !err.isEmpty()) {
447 qCWarning(NTRIPManagerLog) << "NTRIP config invalid:" << err << "host=" << connection.host
448 << " port=" << connection.port;
449 _dispatch(Event::ConfigInvalid, err);
450 return;
451 }
452
453 qCDebug(NTRIPManagerLog) << "Starting NTRIP transport: host=" << connection.host << " port=" << connection.port
454 << " mount=" << connection.mountpoint;
455
456 // Replace the generic "Connecting..." with a host-specific message.
457 _statusMessage = tr("Connecting to %1:%2...").arg(connection.host).arg(connection.port);
458
459 _running = config;
460 _stats.reset();
461
462 NTRIPTransport* transport = _transportFactory ? _transportFactory(_configuration, this) : nullptr;
463 if (!transport) {
464 transport = new NTRIPHttpTransport(connection, config.filter.messageIds(), this, _scheduler);
465 }
466 _transport = transport;
467
468 // Qt still delivers events queued before a transport was retired; they belong to the ended stream.
469 const auto isCurrent = [this, guard = QPointer<NTRIPTransport>(transport)]() {
470 return guard && guard == _transport;
471 };
472
473 // Queued: error handling stops and deletes the transport, which must not happen inside the transport's (or its
474 // socket's) own signal emission.
475 connect(
476 transport, &NTRIPTransport::error, this,
477 [this, isCurrent](const NTRIPFailure& failure) {
478 if (isCurrent()) {
479 _onTransportError(failure);
480 }
481 },
482 Qt::QueuedConnection);
483
484 // Direct, so the handshake state precedes any error or frame queued after it. _stopStreaming() disconnects a
485 // retired transport before stopping it, so direct signals only ever come from the current one.
486 connect(transport, &NTRIPTransport::connected, this, &NTRIPManager::_onTransportConnected);
487
488 // Queued, like errors, so routing and statistics run outside the transport's parse loop and keep their order
489 // relative to a queued failure.
490 connect(
492 [this, isCurrent](const RTCMDecodedFrame& frame) {
493 if (isCurrent()) {
494 _onCorrectionFrame(frame);
495 }
496 },
497 Qt::QueuedConnection);
498
499 connect(transport, &NTRIPTransport::certificatePinned, this, &NTRIPManager::_onCertificatePinned);
500
501 transport->start();
502 qCDebug(NTRIPManagerLog) << "NTRIP transport started";
503}
504
505// -----------------------------------------------------------------------------
506// Signal handlers
507// -----------------------------------------------------------------------------
508
509void NTRIPManager::_onTransportConnected()
510{
511 _dispatch(Event::TransportConnected);
512 _publish();
513}
514
515void NTRIPManager::_onTransportError(const NTRIPFailure& failure)
516{
517 const auto code = failure.code;
518 const auto& detail = failure.detail;
519 if (_connectionStatus != ConnectionStatus::Connecting && _connectionStatus != ConnectionStatus::Connected) {
520 return;
521 }
522 qCWarning(NTRIPManagerLog) << "NTRIP error:" << code << detail;
523 _lastFailureDetail = detail;
524
525 if (_isEnabled() && isRetryable(code)) {
526 const auto backoffMs = _reconnectBackoff.peek(failure.retryAfter).count();
527 qCDebug(NTRIPManagerLog) << "NTRIP reconnecting in" << backoffMs << "ms (attempt"
528 << (_reconnectBackoff.attempts() + 1) << ")";
529 _dispatch(Event::TransportError, tr("Reconnecting in %1s: %2").arg(backoffMs / 1000).arg(detail),
530 failure.retryAfter);
531 } else {
532 _dispatch(Event::TransportFatalError, detail);
533 }
534 _publish();
535}
536
537void NTRIPManager::_onCertificatePinned(const QString& pin)
538{
539 // The running transport already trusts the certificate, so storing its pin must not reconnect.
540 _running.connection.pinnedCertificate = pin;
541 _setPinnedCertificate(pin);
542}
543
544void NTRIPManager::_setPinnedCertificate(const QString& pin)
545{
546 QString& pinned = _configuration.connection.pinnedCertificate;
547 if (pinned == pin) {
548 return;
549 }
550 pinned = pin;
551 emit certificatePinChanged(pin);
552}
553
554void NTRIPManager::_onCorrectionFrame(const RTCMDecodedFrame& frame)
555{
556 if (_correctionSource) {
557 _correctionSource->submit(frame.data, frame.receivedAtMs);
558 }
559 _stats.recordMessage(frame.data.size(), frame.messageId, frame.receivedAtMs);
560
561 // Only corrections show the caster serves this stream; one that accepts and then drops must keep backing off.
562 _reconnectBackoff.reset();
563}
564
565void NTRIPManager::_applyConfiguration()
566{
567 if (_shutdown) {
568 return;
569 }
570 const Configuration& newConfig = _configuration;
571 const bool isActive =
572 (_connectionStatus == ConnectionStatus::Connecting || _connectionStatus == ConnectionStatus::Connected);
573
574 if (!_isEnabled()) {
575 _reconnectBackoff.reset();
576 _stop();
577 } else if (!isActive) {
578 // Disconnected / Error / Reconnecting: start fresh. The connecting
579 // path re-reads settings, so the new values take effect there.
580 _start();
581 } else if (newConfig.connection != _running.connection) {
582 qCDebug(NTRIPManagerLog) << "NTRIP transport-affecting setting changed, reconnecting";
583 _dispatch(Event::HotReconfigure);
584 } else {
585 if (newConfig.filter != _running.filter && _transport) {
586 qCDebug(NTRIPManagerLog) << "NTRIP RTCM whitelist changed, applying to live parser";
587 _transport->setRtcmWhitelist(newConfig.filter.messageIds());
588 }
589 _running = newConfig;
590 }
591 _publish();
592}
Config config
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Retry delays that start at an initial value and grow by a factor per attempt, up to a cap.
constexpr int attempts() const
Attempts counted since construction or the last reset().
constexpr Delay peek(Delay retryAfter=Delay::zero()) const
constexpr Delay next(Delay retryAfter=Delay::zero())
Returns peek(retryAfter) and counts the attempt.
constexpr void reset()
void recordMessage(int bytes, int messageId, qint64 receivedAtMs)
Count every message; health uses the newest valid receipt on the scheduler's clock.
std::function< std::optional< GPSObservation >()> PositionProvider
void start(NTRIPTransport *transport)
void sourceChanged(const QString &source)
void setPositionProvider(PositionSource source, PositionProvider provider)
void configure(const Configuration &configuration)
NTRIPManager(QObject *parent=nullptr, RuntimeScheduler *scheduler=nullptr, const Dependencies &dependencies={})
void statusMessageChanged()
void setConfiguration(const Configuration &configuration)
void certificatePinChanged(const QString &pin)
The trusted self-signed caster certificate changed, or was forgotten (empty); the settings should sto...
ConnectionStatus
Public connection status. Numeric values are stable — QML binds against them.
QString securityWarning() const
Set while a connected stream sends its credentials without TLS.
void setGgaPositionProvider(NTRIPGgaReporter::PositionSource source, NTRIPGgaReporter::PositionProvider provider)
void init()
Applies the configuration; call once the position providers are installed.
void ggaSourceChanged()
void enableRequested()
Retrying after an error turns the saved connection on.
QString connectionStatusText() const
Q_INVOKABLE void retryNTRIP()
Retry a user-visible error, preserving the enabled setting for subsequent automatic retries.
void securityWarningChanged()
@ StartRequested
Settings enable went true, or the user retried an error.
@ ReconnectGaveUp
The reconnect attempt ceiling was reached.
@ HotReconfigure
Transport-affecting setting changed while connected; reconnect in place.
@ TransportFatalError
NTRIPTransport emitted a non-retryable error.
@ ConfigInvalid
Connection configuration failed validation.
@ TransportError
NTRIPTransport emitted a retryable error.
@ TransportConnected
NTRIPTransport emitted connected().
@ StopRequested
Settings enable went false, or the manager shut down.
@ ReconnectDue
The reconnect delay elapsed or the network returned.
~NTRIPManager() override
void shutdown()
Permanently retire this manager, including deferred settings work.
void connectionStatusChanged()
Q_INVOKABLE void fetchMountpoints(const QGeoCoordinate &sortCoordinate=QGeoCoordinate())
const Configuration & configuration() const
void fetch(const NTRIPConnectionConfig &config, const QGeoCoordinate &sortCoord={})
void certificatePinned(const QString &pin)
A self-signed caster certificate was trusted on first use; the owner persists the pin.
void correctionFrameReceived(const RTCMDecodedFrame &frame)
A valid frame of a whitelisted message.
void certificatePinned(const QString &pin)
A self-signed caster certificate was trusted on first use; the owner persists the pin.
virtual void start()=0
void error(const NTRIPFailure &failure)
void availableChanged(bool available)
bool schedule(std::chrono::microseconds delay, RuntimeScheduler::Callback callback)
std::optional< Frame > frame(std::string_view text)
QString streamValidationError() const
QString credentialsInClearWarning() const
The user-visible warning when credentials go to the caster without TLS; empty otherwise.
QString pinnedCertificate
"host:port|sha256-hex" of the self-signed certificate trusted on first use; other endpoints ignore it...
QUrl url(bool withMountpoint=true) const
The caster's HTTP(S) URL, with the mountpoint as its path when withMountpoint is set,...
NTRIPError code
Definition NTRIPError.h:39
QString detail
Definition NTRIPError.h:40
std::chrono::milliseconds retryAfter
Definition NTRIPError.h:41
Services an NTRIPManager uses, which must outlive it.
The NTRIP settings as values; the application's settings binding supplies them.
NTRIPRTCMFilterConfig filter
NTRIPGgaReporter::Configuration gga
NTRIPConnectionConfig connection
QVector< int > messageIds() const
The positive message IDs of the whitelist; entries that are not one are skipped.