QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NTRIPHttpTransport.cc
Go to the documentation of this file.
2
3#include <chrono>
4#include <utility>
5
6#include "NTRIPError.h"
7#include "NTRIPHttpSession.h"
9#include "RuntimeScheduler.h"
10
11QGC_LOGGING_CATEGORY(NTRIPHttpTransportLog, "GPS.NTRIPHttpTransport")
12
13NTRIPHttpTransport::NTRIPHttpTransport(const NTRIPConnectionConfig& config, const QVector<int>& rtcmWhitelist,
14 QObject* parent, RuntimeScheduler* scheduler)
15 : NTRIPTransport(parent)
16 , _config(config)
17 , _scheduler(RuntimeScheduler::orDefault(scheduler, this))
18 , _connectTimeoutTask(_scheduler, this)
19 , _dataWatchdogTask(_scheduler, this)
20{
21 _rtcmDecoder.setWhitelist(rtcmWhitelist);
22 qCDebug(NTRIPHttpTransportLog) << "RTCM message filter:" << rtcmWhitelist;
23 if (rtcmWhitelist.empty()) {
24 qCDebug(NTRIPHttpTransportLog) << "Message filter empty; all RTCM message IDs will be forwarded.";
25 }
26}
27
32
34{
35 _stopTimers();
36 _retireSession();
37 _stopped = false;
38 NTRIPHttpSession* const session = _attachSession(new NTRIPHttpSession(this, _scheduler));
39 if (const QString error = session->open(_config); !error.isEmpty()) {
40 _retireSession();
41 _fail({NTRIPError::InvalidConfig, error});
42 return;
43 }
44 if (_config.sendsCredentialsInClear()) {
45 qCWarning(NTRIPHttpTransportLog) << "Sending credentials without TLS — data is not encrypted";
46 }
47 qCDebug(NTRIPHttpTransportLog) << "Connecting to mount" << _config.mountpoint;
48 _startConnectTimeout();
49}
50
52{
53 _stopped = true;
54 _stopTimers();
55 _retireSession();
56}
57
58NTRIPHttpSession* NTRIPHttpTransport::_attachSession(NTRIPHttpSession* session)
59{
60 _rtcmDecoder.reset();
61 _session = session;
62 // A retired session is disconnected, and a failed attempt sets _stopped, so these run only for the live attempt.
63 connect(session, &NTRIPHttpSession::certificatePinned, this, [this](const QString& pin) {
64 if (!_stopped) {
65 emit certificatePinned(pin);
66 }
67 });
68 connect(session, &NTRIPHttpSession::responseStarted, this, &NTRIPHttpTransport::_onResponseStarted);
69 connect(session, &NTRIPHttpSession::bodyReceived, this, [this](const QByteArray& body, qint64 receivedAtMs) {
70 _dataSinceValidFrame = true;
71 _parseRtcm(body, receivedAtMs);
72 });
73 connect(session, &NTRIPHttpSession::failed, this, &NTRIPHttpTransport::_fail);
74 connect(session, &NTRIPHttpSession::finished, this,
75 [this]() { _fail({NTRIPError::ServerDisconnected, tr("NTRIP correction stream ended")}); });
76 return session;
77}
78
79void NTRIPHttpTransport::_stopTimers()
80{
81 _connectTimeoutTask.cancel();
82 _dataWatchdogTask.cancel();
83}
84
85qint64 NTRIPHttpTransport::_nowMs() const
86{
87 return _scheduler->nowMs();
88}
89
90void NTRIPHttpTransport::_startConnectTimeout()
91{
92 _connectTimeoutTask.schedule(CONNECT_TIMEOUT, [this]() {
93 // A refusal the caster is still explaining ends on its own deadline.
94 if (_session && _session->awaitingErrorBody()) {
95 return;
96 }
97 qCWarning(NTRIPHttpTransportLog) << "Connection timeout";
98 _fail({NTRIPError::ConnectionTimeout, tr("Connection timeout")});
99 });
100}
101
102void NTRIPHttpTransport::_startDataWatchdog(std::chrono::milliseconds delay)
103{
104 // Valid frames only record their time; the single timer re-arms for the remainder when it expires early.
105 _dataWatchdogTask.schedule(delay, [this]() {
106 const std::chrono::milliseconds idle{_nowMs() - _lastValidFrameMs};
107 if (idle < DATA_WATCHDOG) {
108 _startDataWatchdog(DATA_WATCHDOG - idle);
109 } else if (_dataSinceValidFrame) {
110 _fail({NTRIPError::DataWatchdog, tr("No valid RTCM corrections received")});
111 } else {
112 const auto secs = std::chrono::duration_cast<std::chrono::seconds>(DATA_WATCHDOG).count();
113 qCWarning(NTRIPHttpTransportLog) << "No data received for" << secs << "seconds";
114 _fail({NTRIPError::DataWatchdog, tr("No data received for %1 seconds").arg(secs)});
115 }
116 });
117}
118
119void NTRIPHttpTransport::_retireSession()
120{
121 // retire() disconnects the session first, so its abort cannot call back into this transport.
122 if (const auto session = std::exchange(_session, {})) {
123 session->retire();
124 }
125}
126
127bool NTRIPHttpTransport::_isCurrent(const NTRIPHttpSession* session) const
128{
129 return !_stopped && _session == session;
130}
131
132void NTRIPHttpTransport::_fail(const NTRIPFailure& failure)
133{
134 if (_stopped) {
135 return;
136 }
137 _stopped = true;
138 _stopTimers();
139 // The aborted session emits nothing further, and observers of error() may stop, restart or retire this transport.
140 if (_session) {
141 _session->abort();
142 }
143 emit error(failure);
144}
145
146void NTRIPHttpTransport::_onResponseStarted()
147{
148 // Observers of connected() may stop or restart this transport.
149 const NTRIPHttpSession* const session = _session;
150 _connectTimeoutTask.cancel();
151 emit connected();
152 if (!_isCurrent(session)) {
153 return;
154 }
155 _lastValidFrameMs = _nowMs();
156 _dataSinceValidFrame = false;
157 _startDataWatchdog(DATA_WATCHDOG);
158}
159
160void NTRIPHttpTransport::_parseRtcm(const QByteArray& buffer, qint64 receivedAtMs)
161{
162 if (_stopped) {
163 return;
164 }
165 const NTRIPHttpSession* const session = _session;
166 _rtcmDecoder.feed(buffer, receivedAtMs, [this, session](const RTCMDecodedFrame& frame) {
167 if (!frame.valid) {
168 qCWarning(NTRIPHttpTransportLog) << "Invalid RTCM frame, dropping message id" << frame.messageId;
169 return true;
170 }
171 // Whitelisting is a routing policy, not evidence of a broken caster stream.
172 _lastValidFrameMs = _nowMs();
173 _dataSinceValidFrame = false;
174 if (frame.filtered) {
175 qCDebug(NTRIPHttpTransportLog) << "Ignoring RTCM" << frame.messageId;
176 return true;
177 }
178 qCDebug(NTRIPHttpTransportLog) << "RTCM packet id" << frame.messageId << "len" << frame.data.size();
179 emit correctionFrameReceived(frame);
180 return _isCurrent(session);
181 });
182}
183
184void NTRIPHttpTransport::sendNMEA(const QByteArray& nmea)
185{
186 if (_stopped || nmea.isEmpty() || !_session || !_session->isConnected()) {
187 return;
188 }
189
190 if (!_session->write(nmea)) {
191 _fail({NTRIPError::SocketError, tr("Socket did not accept the complete NTRIP write")});
192 return;
193 }
194 qCDebug(NTRIPHttpTransportLog) << "Queued NMEA bytes:" << nmea.size();
195}
Config config
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
void certificatePinned(const QString &pin)
A newly trusted self-signed certificate, as NTRIPConnectionConfig::pinnedCertificate; precedes the re...
QString open(const NTRIPConnectionConfig &config)
void finished()
The response ended where its HTTP framing allows.
void bodyReceived(const QByteArray &body, qint64 receivedAtMs)
Body bytes, with the scheduler time at which the bytes carrying them arrived.
void responseStarted()
The caster accepted the request; its body follows.
void failed(const NTRIPFailure &failure)
A socket, TLS or HTTP failure, including an error status with the caster's explanation.
void sendNMEA(const QByteArray &nmea) override
static constexpr std::chrono::milliseconds CONNECT_TIMEOUT
static constexpr std::chrono::milliseconds DATA_WATCHDOG
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.
bool feed(QByteArrayView bytes, qint64 receivedAtMs, Deliver &&deliver)
Definition RTCMFramer.h:115
qint64 nowMs() const
bool schedule(std::chrono::microseconds delay, RuntimeScheduler::Callback callback)
std::optional< Frame > frame(std::string_view text)
bool sendsCredentialsInClear() const
Credentials go to the caster without TLS.