QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NTRIPSourceTableController.cc
Go to the documentation of this file.
2
3#include <QtCore/QDateTime>
4#include <QtNetwork/QNetworkAccessManager>
5#include <QtNetwork/QNetworkReply>
6#include <QtNetwork/QNetworkRequest>
7#include <QtNetwork/QSslError>
8
9#include "NTRIPSourceTable.h"
10#include "QGCLoggingCategory.h"
11#include "QGCNetworkHelper.h"
12
13QGC_LOGGING_CATEGORY(NTRIPSourceTableControllerLog, "GPS.NTRIPSourceTableController")
14
15namespace {
16bool isSelfSignedOnly(const QList<QSslError>& errors)
17{
18 if (errors.isEmpty()) {
19 return false;
20 }
21 for (const QSslError& error : errors) {
22 switch (error.error()) {
23 case QSslError::SelfSignedCertificate:
24 case QSslError::SelfSignedCertificateInChain:
25 break;
26 case QSslError::UnableToGetLocalIssuerCertificate:
27 case QSslError::UnableToVerifyFirstCertificate:
28 case QSslError::CertificateUntrusted:
29 if (error.certificate().isNull() || !error.certificate().isSelfSigned()) {
30 return false;
31 }
32 break;
33 default:
34 return false;
35 }
36 }
37 return true;
38}
39} // namespace
40
42 : QObject(parent),
43 _model(new NTRIPSourceTableModel(this)),
44 _networkManager(QGCNetworkHelper::createNetworkManager(this))
45{}
46
48{
49 // Abort any in-flight reply before the shared QNAM is destroyed by ~QObject's
50 // child cleanup, otherwise the reply outlives its manager.
51 _abortReply();
52}
53
55{
56 return _model;
57}
58
59void NTRIPSourceTableController::fetch(const NTRIPTransportConfig& config, const QGeoCoordinate& sortCoord)
60{
61 const QString cacheKey = config.casterIdentity();
62
63 // Debounce repeat clicks for the same caster, but let a request for a
64 // different caster supersede an in-flight one (_abortReply below cancels it).
65 if (_fetchStatus == FetchStatus::InProgress && cacheKey == _lastFetchKey) {
66 return;
67 }
68
69 if (_model->count() > 0 && _fetchedAtMs > 0 && cacheKey == _lastFetchKey) {
70 const qint64 age = QDateTime::currentMSecsSinceEpoch() - _fetchedAtMs;
71 if (age < kCacheTtlMs) {
72 qCDebug(NTRIPSourceTableControllerLog) << "Source table cache hit, age:" << age << "ms";
73 _fetchStatus = FetchStatus::Success;
74 emit fetchStatusChanged();
75 return;
76 }
77 }
78
79 // Same validation the streaming transport runs (host/port/control chars,
80 // RFC 7617 username) so the fetch can't reach a caster the stream rejects.
81 if (const QString invalid = config.validationError(); !invalid.isEmpty()) {
82 _onFetchError(invalid);
83 return;
84 }
85
86 _abortReply();
87
88 _sortCoord = sortCoord;
89 _lastFetchKey = cacheKey;
90 _fetchStatus = FetchStatus::InProgress;
91 _fetchError.clear();
92 emit fetchStatusChanged();
93
94 QUrl url;
95 url.setScheme(config.useTls ? QStringLiteral("https") : QStringLiteral("http"));
96 url.setHost(config.host);
97 url.setPort(config.port);
98 url.setPath(QStringLiteral("/"));
99
101 reqCfg.timeoutMs = kFetchTimeoutMs;
102 reqCfg.userAgent = QStringLiteral("QGC-NTRIP");
103 reqCfg.http2Allowed = false;
104 reqCfg.cacheEnabled = false;
105
106 QNetworkRequest request = QGCNetworkHelper::createRequest(url, reqCfg);
107 request.setRawHeader("Ntrip-Version", "Ntrip/2.0");
108 if (!config.username.isEmpty() || !config.password.isEmpty()) {
109 QGCNetworkHelper::setBasicAuth(request, config.username, config.password);
110 }
111
112 _replyTooLarge = false;
113 _reply = _networkManager->get(request);
114 QNetworkReply* const reply = _reply;
115 connect(reply, &QNetworkReply::sslErrors, this,
116 [reply, allowSelfSigned = config.allowSelfSignedCerts](const QList<QSslError>& errors) {
117 if (allowSelfSigned && isSelfSignedOnly(errors)) {
118 reply->ignoreSslErrors(errors);
119 }
120 });
121 // Bound memory: abort mid-download if the caster streams an oversized body.
122 connect(_reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64) {
123 if (received >= kMaxSourceTableBytes && _reply) {
124 _replyTooLarge = true;
125 _reply->abort();
126 }
127 });
128 connect(_reply, &QNetworkReply::finished, this, &NTRIPSourceTableController::_onReplyFinished);
129}
130
131void NTRIPSourceTableController::_onReplyFinished()
132{
133 if (_replyTooLarge) {
134 _abortReply();
135 _onFetchError(tr("Source table too large (exceeds %1 MB)").arg(kMaxSourceTableBytes / (1024 * 1024)));
136 return;
137 }
138
139 const bool networkError = _reply->error() != QNetworkReply::NoError;
140 const QString networkErrorMsg = networkError ? QGCNetworkHelper::errorMessage(_reply) : QString();
141 const QString body = networkError ? QString() : QString::fromUtf8(_reply->readAll());
142 _abortReply();
143
144 if (networkError) {
145 _onFetchError(networkErrorMsg);
146 return;
147 }
148
149 if (!body.contains(QStringLiteral("ENDSOURCETABLE"))) {
150 _onFetchError(tr("Response does not contain a valid source table"));
151 return;
152 }
153
154 _onSourceTableReceived(body);
155}
156
157void NTRIPSourceTableController::_onSourceTableReceived(const QString& table)
158{
159 _model->parseSourceTable(table);
160 _fetchedAtMs = QDateTime::currentMSecsSinceEpoch();
161
162 if (_sortCoord.isValid()) {
163 _model->updateDistances(_sortCoord);
164 }
165
166 _fetchStatus = FetchStatus::Success;
167 emit fetchStatusChanged();
169}
170
171void NTRIPSourceTableController::_onFetchError(const QString& error)
172{
173 // Invalidate the cache: a failed fetch must not let a later call serve the
174 // previous caster's table as this caster's "Success".
175 _fetchedAtMs = 0;
176 _model->clear();
177 _fetchError = error;
178 _fetchStatus = FetchStatus::Error;
179 emit fetchErrorChanged();
180 emit fetchStatusChanged();
181}
182
183void NTRIPSourceTableController::_abortReply()
184{
185 if (_reply) {
186 disconnect(_reply, nullptr, this, nullptr);
187 if (_reply->isRunning()) {
188 _reply->abort();
189 }
190 _reply->deleteLater();
191 _reply = nullptr;
192 }
193}
194
195void NTRIPSourceTableController::injectSourceTableForTest(const QString& table)
196{
197 _abortReply();
198 _onSourceTableReceived(table);
199}
200
201void NTRIPSourceTableController::injectFetchErrorForTest(const QString& error)
202{
203 _abortReply();
204 _onFetchError(error);
205}
206
207void NTRIPSourceTableController::selectMountpoint(const QString& mountpoint)
208{
209 emit mountpointSelected(mountpoint);
210}
Config config
Error error
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Q_INVOKABLE void selectMountpoint(const QString &mountpoint)
NTRIPSourceTableController(QObject *parent=nullptr)
void fetch(const NTRIPTransportConfig &config, const QGeoCoordinate &sortCoord={})
void mountpointSelected(const QString &mountpoint)
QAbstractListModel * mountpointModel() const
static constexpr qint64 kMaxSourceTableBytes
void updateDistances(const QGeoCoordinate &from)
void parseSourceTable(const QString &raw)
QNetworkRequest createRequest(const QUrl &url, const RequestConfig &config)
void setBasicAuth(QNetworkRequest &request, const QString &credentials)
QString errorMessage(const QNetworkReply *reply)
Common request configuration options.