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 <QtCore/QApplicationStatic>
4#include <QtCore/QCoreApplication>
5#include <QtCore/QtMath>
6#include <chrono>
7
8#include "Fact.h"
10#include "NTRIPError.h"
11#include "NTRIPHttpTransport.h"
12#include "NTRIPSettings.h"
13#include "QGCApplication.h"
14#include "QGCLoggingCategory.h"
15#include "RTCMMavlink.h"
16#include "RTCMUdpInput.h"
17#include "SettingsManager.h"
18#include "Vehicle.h"
19
20QGC_LOGGING_CATEGORY(NTRIPManagerLog, "GPS.NTRIPManager")
21
22Q_APPLICATION_STATIC(NTRIPManager, _ntripManagerInstance);
23
25{
26 return _ntripManagerInstance();
27}
28
29// -----------------------------------------------------------------------------
30// Transition table
31// -----------------------------------------------------------------------------
32// Single source of truth for all legal state transitions. Events not listed for
33// the current state are silently ignored (logged at debug). Entry actions are
34// implemented in _onEnterState() below.
35namespace {
36
38using Ev = NTRIPManager::Event;
39
40struct TransitionRow
41{
42 CS from;
43 Ev event;
44 CS to;
45};
46
47constexpr TransitionRow kTransitions[] = {
48 // Disconnected: user or settings can kick us into Connecting.
49 {CS::Disconnected, Ev::StartRequested, CS::Connecting},
50
51 // Connecting: transport handshake outcome.
52 {CS::Connecting, Ev::TransportConnected, CS::Connected},
53 {CS::Connecting, Ev::RTCMBeforeConnected, CS::Connected},
54 {CS::Connecting, Ev::TransportError, CS::Reconnecting},
55 {CS::Connecting, Ev::TransportFatalError, CS::Error},
56 {CS::Connecting, Ev::ConfigInvalid, CS::Error},
57 {CS::Connecting, Ev::StopRequested, CS::Disconnected},
58
59 // Connected: streaming; may lose link or be stopped.
60 {CS::Connected, Ev::TransportError, CS::Reconnecting},
61 {CS::Connected, Ev::TransportFatalError, CS::Error},
62 {CS::Connected, Ev::StopRequested, CS::Disconnected},
63 {CS::Connected, Ev::HotReconfigure, CS::Connecting},
64
65 // Self-transition: a transport-affecting setting changed mid-handshake.
66 // Re-runs the Connecting entry action (teardown + restart) without a state
67 // change so the in-flight attempt picks up the new config.
68 {CS::Connecting, Ev::HotReconfigure, CS::Connecting},
69
70 // Reconnecting: backoff timer owns when we try again, but user/settings
71 // can short-circuit it.
72 {CS::Reconnecting, Ev::ReconnectDue, CS::Connecting},
73 {CS::Reconnecting, Ev::ReconnectGaveUp, CS::Error},
74 {CS::Reconnecting, Ev::StopRequested, CS::Disconnected},
75 {CS::Reconnecting, Ev::StartRequested, CS::Connecting},
76
77 // Error: user-visible fault. User retry or stop both resolve it.
78 {CS::Error, Ev::StartRequested, CS::Connecting},
79 {CS::Error, Ev::StopRequested, CS::Disconnected},
80};
81
82bool isRetryable(NTRIPError error)
83{
84 switch (error) {
85 case NTRIPError::AuthFailed:
86 case NTRIPError::InvalidConfig:
87 return false;
88 default:
89 return true;
90 }
91}
92
93} // namespace
94
95// -----------------------------------------------------------------------------
96// Lifecycle
97// -----------------------------------------------------------------------------
98
99NTRIPManager::NTRIPManager(QObject* parent) : QObject(parent)
100{
101 qCDebug(NTRIPManagerLog) << "NTRIPManager created";
102
103 _settingsDebounceTimer.setSingleShot(true);
104 _settingsDebounceTimer.setInterval(kSettingsDebounceMs);
105 connect(&_settingsDebounceTimer, &QChronoTimer::timeout, this, &NTRIPManager::_onSettingChanged);
106
108
109 connect(&_sourceTableController, &NTRIPSourceTableController::mountpointSelected, this,
110 [this](const QString& mountpoint) {
111 if (_settings && _settings->ntripMountpoint()) {
112 _settings->ntripMountpoint()->setRawValue(mountpoint);
113 }
114 });
115
116 _reconnectTimer.setSingleShot(true);
117 _reconnectTimer.callOnTimeout(this, [this]() { _dispatch(Event::ReconnectDue); });
118
119 // DirectConnection: queued slot may not dispatch before destruction during quit.
120 connect(qApp, &QCoreApplication::aboutToQuit, this, &NTRIPManager::stopNTRIP, Qt::DirectConnection);
121}
122
124{
125 qCDebug(NTRIPManagerLog) << "NTRIPManager destroyed";
126 stopNTRIP();
127}
128
130{
131 return _rtcmMavlink;
132}
133
135{
136 _rtcmMavlink = mavlink;
137}
138
140{
141 if (_initialized) {
142 qCWarning(NTRIPManagerLog) << "NTRIPManager::init() called more than once";
143 return;
144 }
145 _initialized = true;
146
148 if (!_settings) {
149 qCCritical(NTRIPManagerLog) << "init: NTRIPSettings unavailable — SettingsManager not ready?";
150 } else {
151 const Fact* facts[] = {
152 _settings->ntripServerConnectEnabled(),
153 _settings->ntripServerHostAddress(),
154 _settings->ntripServerPort(),
155 _settings->ntripUsername(),
156 _settings->ntripPassword(),
157 _settings->ntripMountpoint(),
158 _settings->ntripWhitelist(),
159 _settings->ntripUseTls(),
160 _settings->ntripAllowSelfSignedCerts(),
161 _settings->ntripUdpForwardEnabled(),
162 _settings->ntripUdpTargetAddress(),
163 _settings->ntripUdpTargetPort(),
164 };
165 for (const auto* fact : facts) {
166 if (fact) {
167 connect(fact, &Fact::rawValueChanged, this, [this]() { _settingsDebounceTimer.start(); });
168 }
169 }
170 }
171
172 _ggaProvider.init(_settings);
173
174 // RTCMMavlink may be injected for tests; otherwise own one so RTCM corrections
175 // reach connected vehicles. Mirrors the legacy self-wiring behavior.
176 if (!_rtcmMavlink) {
177 QObject* parentObj = qgcApp() ? static_cast<QObject*>(qgcApp()) : static_cast<QObject*>(this);
178 _rtcmMavlink = new RTCMMavlink(parentObj);
179 _rtcmMavlink->setObjectName(QStringLiteral("RTCMMavlink"));
180 }
181
182 _setupRtcmUdpInput();
183
184 if (_settings) {
185 _onSettingChanged();
186 }
187}
188
189void NTRIPManager::_setupRtcmUdpInput()
190{
191 if (!_settings || !_settings->rtcmUdpInputPort()) {
192 return;
193 }
194
195 const quint16 port = static_cast<quint16>(_settings->rtcmUdpInputPort()->rawValue().toUInt());
196 _rtcmUdpInput = new RTCMUdpInput(port, this);
197 connect(_rtcmUdpInput, &RTCMUdpInput::rtcmDataReceived, _rtcmMavlink, &RTCMMavlink::RTCMDataUpdate);
198
199 auto applyUdpInputSettings = [this]() {
200 const quint16 inPort = static_cast<quint16>(_settings->rtcmUdpInputPort()->rawValue().toUInt());
201 _rtcmUdpInput->setPort(inPort);
202 _rtcmUdpInput->setValidation(_settings->rtcmUdpValidate()->rawValue().toBool());
203 if (_settings->rtcmUdpInputEnabled()->rawValue().toBool()) {
204 _rtcmUdpInput->start();
205 } else {
206 _rtcmUdpInput->stop();
207 }
208 };
209 connect(_settings->rtcmUdpInputEnabled(), &Fact::rawValueChanged, this, applyUdpInputSettings);
210 connect(_settings->rtcmUdpInputPort(), &Fact::rawValueChanged, this, applyUdpInputSettings);
211 connect(_settings->rtcmUdpValidate(), &Fact::rawValueChanged, this, applyUdpInputSettings);
212 applyUdpInputSettings();
213}
214
215// -----------------------------------------------------------------------------
216// Public control surface
217// -----------------------------------------------------------------------------
218
220{
221 _dispatch(Event::StartRequested);
222}
223
225{
226 _dispatch(Event::StopRequested);
227}
228
230{
231 if (!_settings) {
232 return;
233 }
234 QGeoCoordinate sortCoord;
236 sortCoord = mvm->activeVehicle()->coordinate();
237 }
238 _sourceTableController.fetch(NTRIPTransportConfig::fromSettings(*_settings), sortCoord);
239}
240
241// -----------------------------------------------------------------------------
242// State machine
243// -----------------------------------------------------------------------------
244
245bool NTRIPManager::_dispatch(Event ev, const QString& detail)
246{
247 for (const auto& row : kTransitions) {
248 if (row.from == _connectionStatus && row.event == ev) {
249 _enterState(row.to, detail);
250 return true;
251 }
252 }
253 qCDebug(NTRIPManagerLog) << "NTRIP event" << static_cast<int>(ev) << "ignored in state"
254 << static_cast<int>(_connectionStatus);
255 return false;
256}
257
258void NTRIPManager::_enterState(ConnectionStatus to, const QString& detail)
259{
260 const ConnectionStatus from = _connectionStatus;
261 const bool stateChanged = (from != to);
262 const QString msg = detail.isEmpty() ? _defaultMessageFor(to) : detail;
263
264 // Commit state + message before running entry actions so that a recursive
265 // _dispatch() from inside an entry action (e.g. _startTransport → ConfigInvalid)
266 // observes the already-committed state, not the stale caller value.
267 _connectionStatus = to;
268 const bool msgChanged = (_statusMessage != msg);
269 if (msgChanged) {
270 _statusMessage = msg;
271 }
272
273 if (stateChanged) {
274 qCDebug(NTRIPManagerLog) << "NTRIP state" << static_cast<int>(from) << "→" << static_cast<int>(to) << msg;
276 }
277 if (msgChanged) {
279 }
280
281 // Entry action runs on every dispatched transition, including self-transitions
282 // (e.g. Connecting→Connecting on HotReconfigure). Signals stay gated above.
283 _onEnterState(from, to);
284}
285
286QString NTRIPManager::_defaultMessageFor(ConnectionStatus state)
287{
288 switch (state) {
290 return tr("Disconnected");
292 return tr("Connecting...");
294 return tr("Connected");
296 return tr("Reconnecting...");
298 return {}; // Error always carries a detail from the caller.
299 }
300 return {};
301}
302
303void NTRIPManager::_onEnterState(ConnectionStatus /*from*/, ConnectionStatus to)
304{
305 // Per-state side effects. Teardown helpers are idempotent — safe to call
306 // from any state where the resource may or may not be active.
307 switch (to) {
309 _cancelReconnect();
310 _teardownTransport();
311 _ggaProvider.stop();
312 _stats.stop();
313 _udpForwarder.stop();
314 _setSecurityWarning({});
315 _runningConfig = {};
316 break;
317
319 _cancelReconnect(); // may have arrived here via Reconnecting → StartRequested
320 _setSecurityWarning({});
321 _teardownTransport(); // clear any lingering transport from a prior attempt
322 _startTransport(); // may recursively dispatch ConfigInvalid → Error
323 break;
324
326 _resetReconnectAttempts();
327 _casterStatus = CasterStatus::CasterConnected;
328 emit casterStatusChanged(_casterStatus);
329 _ggaProvider.start(_transport);
330 _stats.start();
331 break;
332
334 _teardownTransport();
335 _ggaProvider.stop();
336 _stats.stop();
337 _scheduleReconnect();
338 break;
339
341 _cancelReconnect();
342 _teardownTransport();
343 _ggaProvider.stop();
344 _stats.stop();
345 _udpForwarder.stop();
346 _setSecurityWarning({});
347 _runningConfig = {};
348 break;
349 }
350}
351
352// -----------------------------------------------------------------------------
353// Entry-action helpers
354// -----------------------------------------------------------------------------
355
356void NTRIPManager::_teardownTransport()
357{
358 if (!_transport) {
359 return;
360 }
361 _transport->disconnect(this);
362 _transport->stop();
363 _transport->deleteLater();
364 _transport = nullptr;
365}
366
367int NTRIPManager::_reconnectBackoffMs() const
368{
369 return qMin(kMinReconnectMs * (1 << qMin(_reconnectAttempts, 5)), kMaxReconnectMs);
370}
371
372void NTRIPManager::_scheduleReconnect()
373{
374 // Backoff uses the pre-increment attempt count: attempt #1 waits kMinReconnectMs,
375 // #2 waits 2x, etc. Increment, then check the ceiling.
376 const auto backoff = std::chrono::milliseconds{_reconnectBackoffMs()};
377 ++_reconnectAttempts;
378 if (_reconnectExhausted()) {
379 _dispatch(Event::ReconnectGaveUp, tr("Gave up after %1 reconnect attempts").arg(kMaxReconnectAttempts));
380 return;
381 }
382 _reconnectTimer.setInterval(backoff);
383 _reconnectTimer.start();
384}
385
386void NTRIPManager::_startTransport()
387{
388 if (!_settings) {
389 _dispatch(Event::ConfigInvalid, tr("Settings unavailable"));
390 return;
391 }
392
394
395 _applyUdpForwarderConfig(config);
396
397 if (const QString err = config.validationError(); !err.isEmpty()) {
398 qCWarning(NTRIPManagerLog) << "NTRIP config invalid:" << err << "host=" << config.host
399 << " port=" << config.port;
400 _dispatch(Event::ConfigInvalid, err);
401 return;
402 }
403
404 qCDebug(NTRIPManagerLog) << "startTransport: host=" << config.host << " port=" << config.port
405 << " mount=" << config.mountpoint;
406
407 // Replace the generic "Connecting..." with a host-specific message.
408 const QString msg = tr("Connecting to %1:%2...").arg(config.host).arg(config.port);
409 if (_statusMessage != msg) {
410 _statusMessage = msg;
412 }
413
414 _stats.reset();
415 _runningConfig = config;
416
417 if (_injectedTransport) {
418 _transport = _injectedTransport;
419 _injectedTransport = nullptr;
420 } else {
421 _transport = new NTRIPHttpTransport(config, this);
422 }
423
424 // QueuedConnection: _onTransportError may tear the transport down — Direct
425 // would destroy it while still inside its own signal emission (use-after-free).
426 connect(_transport, &NTRIPTransport::error, this, &NTRIPManager::_onTransportError, Qt::QueuedConnection);
427
428 // Must stay non-queued: TransportConnected is dispatched synchronously so a
429 // queued `error` that tore the transport down cannot interleave a stale
430 // TransportConnected into the Disconnected state. Do not make this queued.
431 connect(_transport, &NTRIPTransport::connected, this, [this]() { _dispatch(Event::TransportConnected); });
432
433 connect(_transport, &NTRIPTransport::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived);
434
435 connect(_transport, &NTRIPTransport::plaintextCredentialsWarning, this, &NTRIPManager::_onPlaintextCredentialsWarning);
436
437 _transport->start();
438 qCDebug(NTRIPManagerLog) << "NTRIP transport started";
439}
440
441// -----------------------------------------------------------------------------
442// Signal handlers
443// -----------------------------------------------------------------------------
444
445void NTRIPManager::_onTransportError(NTRIPError code, const QString& detail)
446{
447 qCWarning(NTRIPManagerLog) << "NTRIP error:" << static_cast<int>(code) << detail;
448
449 const CasterStatus caster =
450 (code == NTRIPError::NoLocation) ? CasterStatus::CasterNoLocation : CasterStatus::CasterError;
451 if (_casterStatus != caster) {
452 _casterStatus = caster;
453 emit casterStatusChanged(_casterStatus);
454 }
455
456 if (_isEnabled() && isRetryable(code)) {
457 const int backoffMs = _reconnectBackoffMs();
458 qCDebug(NTRIPManagerLog) << "NTRIP reconnecting in" << backoffMs << "ms (attempt" << (_reconnectAttempts + 1)
459 << ")";
460 _dispatch(Event::TransportError, tr("Reconnecting in %1s: %2").arg(backoffMs / 1000).arg(detail));
461 } else {
462 _dispatch(Event::TransportFatalError, detail);
463 }
464}
465
466void NTRIPManager::_onPlaintextCredentialsWarning()
467{
468 qCWarning(NTRIPManagerLog) << "Credentials sent without TLS encryption — enable TLS in NTRIP settings";
469 _setSecurityWarning(tr("Credentials are being sent without TLS encryption."));
470}
471
472void NTRIPManager::_setSecurityWarning(const QString& warning)
473{
474 if (_securityWarning == warning) {
475 return;
476 }
477 _securityWarning = warning;
479}
480
481void NTRIPManager::_rtcmDataReceived(const QByteArray& data, int messageId)
482{
483 _stats.recordMessage(data.size(), messageId);
484
485 qCDebug(NTRIPManagerLog) << "NTRIP forwarding RTCM:" << data.size() << "bytes";
486
487 RTCMMavlink* mavlink = _rtcmMavlink;
488 if (mavlink) {
489 mavlink->RTCMDataUpdate(data);
490
491 if (_connectionStatus != ConnectionStatus::Connected) {
492 // RTCM arrived before we processed the connected() signal — normalize
493 // through the state machine so this stays the single source of truth.
494 // No-op (logged) from any state without a matching transition row.
496 }
497 } else {
498 qCWarning(NTRIPManagerLog) << "RTCMMavlink not ready; dropping" << data.size() << "bytes";
499 }
500
501 _udpForwarder.forward(data);
502}
503
504bool NTRIPManager::_isEnabled() const
505{
506 return _settings && _settings->ntripServerConnectEnabled() &&
507 _settings->ntripServerConnectEnabled()->rawValue().toBool();
508}
509
510void NTRIPManager::_onSettingChanged()
511{
512 if (!_settings) {
513 return;
514 }
515
516 if (!_isEnabled()) {
517 // Match legacy: when disabled while Reconnecting, reset the attempt
518 // counter so a future re-enable starts with a clean backoff schedule.
519 if (_connectionStatus == ConnectionStatus::Reconnecting) {
520 _resetReconnectAttempts();
521 }
522 _dispatch(Event::StopRequested);
523 return;
524 }
525
526 const bool isActive =
527 (_connectionStatus == ConnectionStatus::Connecting || _connectionStatus == ConnectionStatus::Connected);
528
529 if (!isActive) {
530 // Disconnected / Error / Reconnecting: start fresh. The connecting
531 // path re-reads settings, so the new values take effect there.
532 _dispatch(Event::StartRequested);
533 return;
534 }
535
536 // Active — classify the diff to avoid unnecessary reconnects.
537 // Hot (host, port, creds, mountpoint, TLS) requires a new TCP handshake.
538 // Warm (UDP sink) reconfigures the sidecar in place.
539 // Cold (whitelist) is pushed to the live parser; the caster doesn't care.
540 const NTRIPTransportConfig newConfig = NTRIPTransportConfig::fromSettings(*_settings);
541
542 if (newConfig.transportDiffers(_runningConfig)) {
543 qCDebug(NTRIPManagerLog) << "NTRIP transport-affecting setting changed, reconnecting";
544 _dispatch(Event::HotReconfigure);
545 return;
546 }
547
548 if (newConfig.udpForwardDiffers(_runningConfig)) {
549 qCDebug(NTRIPManagerLog) << "NTRIP UDP forward settings changed, reconfiguring in place";
550 _applyUdpForwarderConfig(newConfig);
551 }
552
553 if (newConfig.whitelistDiffers(_runningConfig) && _transport) {
554 qCDebug(NTRIPManagerLog) << "NTRIP RTCM whitelist changed, applying to live parser";
555 _transport->setRtcmWhitelist(NTRIPTransportConfig::parseWhitelist(newConfig.whitelist));
556 }
557
558 _runningConfig = newConfig;
559}
560
561void NTRIPManager::_applyUdpForwarderConfig(const NTRIPTransportConfig& config)
562{
563 if (!config.udpForwardEnabled) {
564 _udpForwarder.stop();
565 return;
566 }
567 if (!_udpForwarder.configure(config.udpTargetAddress, config.udpTargetPort)) {
568 qCWarning(NTRIPManagerLog) << "UDP forward config invalid:" << config.udpTargetAddress << config.udpTargetPort;
569 }
570}
Config config
Q_APPLICATION_STATIC(NTRIPManager, _ntripManagerInstance)
#define qgcApp()
#define qApp
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
A Fact is used to hold a single value within the system.
Definition Fact.h:17
void rawValueChanged(const QVariant &value)
static MultiVehicleManager * instance()
Vehicle * activeVehicle() const
void recordMessage(int bytes, int messageId=0)
void sourceChanged(const QString &source)
void init(NTRIPSettings *settings)
void start(NTRIPTransport *transport)
void statusMessageChanged()
void setRtcmMavlink(RTCMMavlink *mavlink)
void casterStatusChanged(CasterStatus status)
ConnectionStatus
Public connection status. Numeric values are stable — QML binds against them.
Q_INVOKABLE void fetchMountpoints()
void ggaSourceChanged()
RTCMMavlink * rtcmMavlink() const
void securityWarningChanged()
NTRIPManager(QObject *parent=nullptr)
@ StartRequested
startNTRIP() called or settings enable went true.
@ ReconnectGaveUp
NTRIPReconnectPolicy fired gaveUp().
@ HotReconfigure
Transport-affecting setting changed while connected; reconnect in place.
@ TransportFatalError
NTRIPTransport emitted a non-retryable error.
@ RTCMBeforeConnected
RTCM data arrived before the connected() signal was processed.
@ ConfigInvalid
NTRIPTransportConfig::isValid() returned false.
@ TransportError
NTRIPTransport emitted a retryable error.
@ TransportConnected
NTRIPTransport emitted connected().
@ StopRequested
stopNTRIP() called or settings enable went false.
@ ReconnectDue
NTRIPReconnectPolicy fired reconnectRequested().
~NTRIPManager() override
void connectionStatusChanged()
void fetch(const NTRIPTransportConfig &config, const QGeoCoordinate &sortCoord={})
void mountpointSelected(const QString &mountpoint)
void plaintextCredentialsWarning()
void RTCMDataUpdate(const QByteArray &message, int messageId)
void error(NTRIPError code, const QString &detail)
Listens on a UDP port for raw RTCM3 correction data and emits it for forwarding to connected vehicles...
void stop()
Unbind the socket and stop accepting datagrams.
void rtcmDataReceived(const QByteArray &data)
void setPort(quint16 port)
Change the listen port. If already running, restarts automatically.
void setValidation(const bool validate)
static SettingsManager * instance()
NTRIPSettings * ntripSettings() const
bool configure(const QString &address, quint16 port)
void forward(const QByteArray &data)
bool whitelistDiffers(const NTRIPTransportConfig &other) const
bool transportDiffers(const NTRIPTransportConfig &other) const
static NTRIPTransportConfig fromSettings(NTRIPSettings &settings)
bool udpForwardDiffers(const NTRIPTransportConfig &other) const
static QVector< int > parseWhitelist(const QString &csv)