16#include <QtPositioning/QGeoCoordinate>
17#include <QtCore/QDateTime>
19#include <QtCore/QApplicationStatic>
20#include <QtCore/QCoreApplication>
21#include <QtCore/QDebug>
22#include <QtCore/QtGlobal>
30 return _ntripManagerInstance();
33NTRIPManager::NTRIPManager(QObject* parent)
36 qCDebug(NTRIPManagerLog) <<
"NTRIPManager created";
40 QObject* parentObj =
qgcApp() ?
static_cast<QObject*
>(
qgcApp()) : static_cast<QObject*>(this);
42 _rtcmMavlink->setObjectName(QStringLiteral(
"RTCMMavlink"));
45 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
47 auto connectSetting = [
this](
Fact* fact) {
66 QTimer::singleShot(0,
this, &NTRIPManager::_onSettingChanged);
68 _ggaTimer =
new QTimer(
this);
69 _ggaTimer->setInterval(5000);
70 connect(_ggaTimer, &QTimer::timeout,
this, &NTRIPManager::_sendGGA);
72 _dataRateTimer =
new QTimer(
this);
73 _dataRateTimer->setInterval(1000);
74 connect(_dataRateTimer, &QTimer::timeout,
this, [
this]() {
75 const double rate =
static_cast<double>(_bytesReceived - _dataRatePrevBytes);
76 _dataRatePrevBytes = _bytesReceived;
77 if (!qFuzzyCompare(_dataRateBytesPerSec + 1.0, rate + 1.0)) {
78 _dataRateBytesPerSec = rate;
79 emit dataRateChanged();
81 emit bytesReceivedChanged();
82 emit messagesReceivedChanged();
85 connect(
qApp, &QCoreApplication::aboutToQuit,
this, &NTRIPManager::stopNTRIP, Qt::QueuedConnection);
88NTRIPManager::~NTRIPManager()
90 qCDebug(NTRIPManagerLog) <<
"NTRIPManager destroyed";
94void NTRIPManager::_setStatus(ConnectionStatus status,
const QString& msg)
98 if (_connectionStatus != status) {
99 _connectionStatus = status;
104 if (_statusMessage != msg) {
105 _statusMessage = msg;
111 qCDebug(NTRIPManagerLog) <<
"NTRIP status:" <<
static_cast<int>(status) << msg;
118 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
134void NTRIPManager::startNTRIP()
136 if (_startStopBusy || _transport) {
137 if (_startStopBusy) {
138 qCWarning(NTRIPManagerLog) <<
"startNTRIP called while start/stop in progress";
142 _startStopBusy =
true;
144 if (_reconnectTimer) {
145 _reconnectTimer->stop();
150 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
155 if (_udpForwardEnabled && settings) {
157 const quint16 udpPort =
static_cast<quint16
>(settings->
ntripUdpTargetPort()->rawValue().toUInt());
158 const QHostAddress parsedAddr(udpAddr);
159 if (!udpAddr.isEmpty() && !parsedAddr.isNull() && udpPort > 0) {
160 _udpTargetAddress = parsedAddr;
161 _udpTargetPort = udpPort;
163 _udpSocket =
new QUdpSocket(
this);
165 qCDebug(NTRIPManagerLog) <<
"NTRIP UDP forwarding enabled:" << udpAddr <<
":" << udpPort;
167 qCWarning(NTRIPManagerLog) <<
"NTRIP UDP forwarding enabled but address/port invalid";
168 _udpForwardEnabled =
false;
172 if (config.
host.isEmpty()) {
173 qCWarning(NTRIPManagerLog) <<
"NTRIP host address is empty";
174 _setStatus(ConnectionStatus::Error, tr(
"No host address"));
175 _startStopBusy =
false;
179 if (config.
port <= 0 || config.
port > 65535) {
180 qCWarning(NTRIPManagerLog) <<
"NTRIP port is invalid:" << config.
port;
181 _setStatus(ConnectionStatus::Error, tr(
"Invalid port"));
182 _startStopBusy =
false;
186 qCDebug(NTRIPManagerLog) <<
"startNTRIP: host=" << config.
host <<
" port=" << config.
port
189 _setStatus(ConnectionStatus::Connecting, tr(
"Connecting to %1:%2...").arg(config.
host).arg(config.
port));
192 _messagesReceived = 0;
193 _dataRateBytesPerSec = 0.0;
194 _dataRatePrevBytes = 0;
199 _runningConfig = config;
200 _runningUdpForward = _udpForwardEnabled;
201 _runningUdpAddr = _udpForwardEnabled ? _udpTargetAddress.toString() : QString();
202 _runningUdpPort = _udpForwardEnabled ? _udpTargetPort : 0;
207 this, &NTRIPManager::_tcpError);
210 _reconnectAttempts = 0;
211 _casterStatus = CasterStatus::CasterConnected;
214 _setStatus(ConnectionStatus::Connected, tr(
"Connected"));
216 if (_ggaTimer && !_ggaTimer->isActive()) {
217 _ggaTimer->setInterval(1000);
221 _dataRateTimer->start();
227 qCDebug(NTRIPManagerLog) <<
"NTRIP started";
229 _startStopBusy =
false;
232void NTRIPManager::stopNTRIP()
234 if (_startStopBusy) {
235 qCWarning(NTRIPManagerLog) <<
"stopNTRIP called while start/stop in progress";
238 _startStopBusy =
true;
240 if (_reconnectTimer) {
241 _reconnectTimer->stop();
247 _transport->deleteLater();
248 _transport =
nullptr;
251 _runningUdpForward =
false;
252 _runningUdpAddr.clear();
255 _setStatus(ConnectionStatus::Disconnected, tr(
"Disconnected"));
256 qCDebug(NTRIPManagerLog) <<
"NTRIP stopped";
259 if (_ggaTimer && _ggaTimer->isActive()) {
262 _dataRateTimer->stop();
263 if (_dataRateBytesPerSec != 0.0) {
264 _dataRateBytesPerSec = 0.0;
265 _dataRatePrevBytes = 0;
272 _udpSocket =
nullptr;
274 _udpForwardEnabled =
false;
276 _startStopBusy =
false;
279void NTRIPManager::_tcpError(
const QString& errorMsg)
281 qCWarning(NTRIPManagerLog) <<
"NTRIP error:" << errorMsg;
282 _setStatus(ConnectionStatus::Error, errorMsg);
284 if (errorMsg.contains(
"NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) {
285 _casterStatus = CasterStatus::CasterNoLocation;
287 _casterStatus = CasterStatus::CasterError;
291 _scheduleReconnect();
294void NTRIPManager::_scheduleReconnect()
296 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
305 int backoffMs = qMin(kMinReconnectMs * (1 << qMin(_reconnectAttempts, 4)), kMaxReconnectMs);
306 _reconnectAttempts = qMin(_reconnectAttempts + 1, kMaxReconnectAttempts);
308 qCDebug(NTRIPManagerLog) <<
"NTRIP reconnecting in" << backoffMs <<
"ms (attempt" << _reconnectAttempts <<
")";
310 _setStatus(ConnectionStatus::Reconnecting, tr(
"Reconnecting in %1s...").arg(backoffMs / 1000));
312 if (!_reconnectTimer) {
313 _reconnectTimer =
new QTimer(
this);
314 _reconnectTimer->setSingleShot(
true);
315 connect(_reconnectTimer, &QTimer::timeout,
this, [
this]() {
322 _reconnectTimer->start(backoffMs);
325void NTRIPManager::_rtcmDataReceived(
const QByteArray& data)
327 _bytesReceived +=
static_cast<quint64
>(data.size());
330 qCDebug(NTRIPManagerLog) <<
"NTRIP Forwarding RTCM to vehicle:" << data.size() <<
"bytes";
332 if (!_rtcmMavlink &&
qgcApp()) {
339 if (_connectionStatus != ConnectionStatus::Connected) {
340 _setStatus(ConnectionStatus::Connected, tr(
"Connected"));
343 qCWarning(NTRIPManagerLog) <<
"RTCMMavlink not ready; dropping" << data.size() <<
"bytes";
346 if (_udpForwardEnabled && _udpSocket && _udpTargetPort > 0) {
347 const qint64 sent = _udpSocket->writeDatagram(data, _udpTargetAddress, _udpTargetPort);
349 qCWarning(NTRIPManagerLog) <<
"NTRIP UDP forward failed:" << _udpSocket->errorString();
354QByteArray NTRIPManager::makeGGA(
const QGeoCoordinate& coord,
double altitude_msl)
356 const QTime utc = QDateTime::currentDateTimeUtc().time();
357 const QString hhmmss = QString(
"%1%2%3")
358 .arg(utc.hour(), 2, 10, QChar(
'0'))
359 .arg(utc.minute(), 2, 10, QChar(
'0'))
360 .arg(utc.second(), 2, 10, QChar(
'0'));
362 auto dmm = [](
double deg,
bool lat) -> QString {
363 double a = qFabs(deg);
365 double m = (a - d) * 60.0;
367 int m10000 = int(m * 10000.0 + 0.5);
368 double m_rounded = m10000 / 10000.0;
369 if (m_rounded >= 60.0) {
374 QString mm = QString::number(m_rounded,
'f', 4);
375 if (m_rounded < 10.0) {
380 return QString(
"%1%2").arg(d, 2, 10, QChar(
'0')).arg(mm);
382 return QString(
"%1%2").arg(d, 3, 10, QChar(
'0')).arg(mm);
386 const bool latNorth = coord.latitude() >= 0.0;
387 const bool lonEast = coord.longitude() >= 0.0;
389 const QString latField = dmm(coord.latitude(),
true);
390 const QString lonField = dmm(coord.longitude(),
false);
392 const QString core = QString(
"GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,")
395 .arg(latNorth ?
"N" :
"S")
397 .arg(lonEast ?
"E" :
"W")
398 .arg(QString::number(altitude_msl,
'f', 1));
401 const QByteArray coreBytes = core.toUtf8();
402 for (
char ch : coreBytes) {
403 cksum ^=
static_cast<quint8
>(ch);
406 const QString cks = QString(
"%1").arg(cksum, 2, 16, QChar(
'0')).toUpper();
407 const QByteArray sentence = QByteArray(
"$") + coreBytes + QByteArray(
"*") + cks.toUtf8();
411QPair<QGeoCoordinate, QString> NTRIPManager::_getBestPosition()
const
415 if (
Vehicle* veh = mvm->activeVehicle()) {
418 Fact* latF = gps->getFact(QStringLiteral(
"lat"));
419 Fact* lonF = gps->getFact(QStringLiteral(
"lon"));
422 const double lat = latF->rawValue().toDouble();
423 const double lon = lonF->rawValue().toDouble();
425 if (qIsFinite(lat) && qIsFinite(lon) &&
426 !(lat == 0.0 && lon == 0.0) &&
427 qAbs(lat) <= 90.0 && qAbs(lon) <= 180.0) {
429 return {QGeoCoordinate(lat, lon, veh->coordinate().altitude()),
430 QStringLiteral(
"GPS Raw")};
435 QGeoCoordinate coord = veh->coordinate();
436 if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) {
437 return {coord, QStringLiteral(
"Vehicle EKF")};
444 QGeoCoordinate coord = posMgr->gcsPosition();
445 if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) {
446 return {coord, QStringLiteral(
"GCS Position")};
450 return {QGeoCoordinate(), QString()};
453void NTRIPManager::_sendGGA()
459 const auto [coord, srcUsed] = _getBestPosition();
461 if (!coord.isValid()) {
462 qCDebug(NTRIPManagerLog) <<
"NTRIP: No valid position, skipping GGA";
466 if (_ggaTimer && _ggaTimer->interval() != 5000) {
467 _ggaTimer->setInterval(5000);
468 qCDebug(NTRIPManagerLog) <<
"NTRIP: Position acquired, reducing GGA to 5s";
471 double alt_msl = coord.altitude();
472 if (!qIsFinite(alt_msl)) {
476 const QByteArray gga = makeGGA(coord, alt_msl);
479 if (!srcUsed.isEmpty() && srcUsed != _ggaSource) {
480 _ggaSource = srcUsed;
485void NTRIPManager::_onSettingChanged()
487 if (_startStopBusy) {
491 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
494 bool isRunning = (_transport !=
nullptr);
496 if (shouldRun && isRunning && settings) {
500 const quint16 udpPort =
static_cast<quint16
>(settings->
ntripUdpTargetPort()->rawValue().toUInt());
502 if (config != _runningConfig ||
503 udpFwd != _runningUdpForward || udpAddr != _runningUdpAddr || udpPort != _runningUdpPort) {
504 qCDebug(NTRIPManagerLog) <<
"NTRIP settings changed while running, restarting";
511 const bool reconnectPending = _reconnectTimer && _reconnectTimer->isActive();
513 if (shouldRun && !isRunning && !reconnectPending) {
515 }
else if (!shouldRun) {
516 if (_reconnectTimer) {
517 _reconnectTimer->stop();
521 }
else if (reconnectPending) {
522 _reconnectAttempts = 0;
523 _setStatus(ConnectionStatus::Disconnected, tr(
"Disconnected"));
530 if (_sourceTableModel) {
531 return _sourceTableModel->mountpoints();
536void NTRIPManager::fetchMountpoints()
538 if (_mountpointFetchStatus == MountpointFetchStatus::FetchInProgress) {
542 if (_sourceTableModel && _sourceTableModel->count() > 0 && _sourceTableFetchedAtMs > 0) {
543 const qint64 age = QDateTime::currentMSecsSinceEpoch() - _sourceTableFetchedAtMs;
544 if (age < kSourceTableCacheTtlMs) {
545 qCDebug(NTRIPManagerLog) <<
"Source table cache hit, age:" << age <<
"ms";
546 _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess;
552 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
559 const QString user = settings->
ntripUsername()->rawValue().toString();
560 const QString pass = settings->
ntripPassword()->rawValue().toString();
563 if (host.isEmpty()) {
564 _mountpointFetchError = tr(
"Host address is empty");
565 _mountpointFetchStatus = MountpointFetchStatus::FetchError;
571 if (!_sourceTableModel) {
576 if (_sourceTableFetcher) {
577 _sourceTableFetcher->deleteLater();
578 _sourceTableFetcher =
nullptr;
581 _mountpointFetchStatus = MountpointFetchStatus::FetchInProgress;
582 _mountpointFetchError.clear();
589 _sourceTableModel->parseSourceTable(table);
590 _sourceTableFetchedAtMs = QDateTime::currentMSecsSinceEpoch();
594 if (
Vehicle* veh = mvm->activeVehicle()) {
595 QGeoCoordinate coord = veh->coordinate();
596 if (coord.isValid()) {
597 _sourceTableModel->updateDistances(coord);
602 _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess;
604 qCDebug(NTRIPManagerLog) <<
"NTRIP source table fetched:" << _sourceTableModel->count() <<
"mountpoints";
608 _mountpointFetchError = errorMsg;
609 _mountpointFetchStatus = MountpointFetchStatus::FetchError;
612 qCWarning(NTRIPManagerLog) <<
"NTRIP source table fetch error:" << errorMsg;
616 _sourceTableFetcher->deleteLater();
617 _sourceTableFetcher =
nullptr;
620 _sourceTableFetcher->
fetch();
623void NTRIPManager::selectMountpoint(
const QString& mountpoint)
625 NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
628 qCDebug(NTRIPManagerLog) <<
"NTRIP mountpoint selected:" << mountpoint;
Q_APPLICATION_STATIC(NTRIPManager, _ntripManagerInstance)
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Used to group Facts together into an object hierarachy.
A Fact is used to hold a single value within the system.
void rawValueChanged(const QVariant &value)
void error(const QString &errorMsg)
void sendNMEA(const QByteArray &nmea)
void RTCMDataUpdate(const QByteArray &message)
void statusMessageChanged()
void messagesReceivedChanged()
void bytesReceivedChanged()
void mountpointFetchStatusChanged()
void casterStatusChanged(CasterStatus status)
void connectionStatusChanged()
void mountpointFetchErrorChanged()
void mountpointModelChanged()
Fact *ntripUsername READ ntripUsername CONSTANT Fact * ntripUsername()
Fact *ntripServerHostAddress READ ntripServerHostAddress CONSTANT Fact * ntripServerHostAddress()
Fact *ntripUseTls READ ntripUseTls CONSTANT Fact * ntripUseTls()
Fact *ntripServerConnectEnabled READ ntripServerConnectEnabled CONSTANT Fact * ntripServerConnectEnabled()
Fact *ntripServerPort READ ntripServerPort CONSTANT Fact * ntripServerPort()
Fact *ntripUdpTargetAddress READ ntripUdpTargetAddress CONSTANT Fact * ntripUdpTargetAddress()
Fact *ntripWhitelist READ ntripWhitelist CONSTANT Fact * ntripWhitelist()
Fact *ntripMountpoint READ ntripMountpoint CONSTANT Fact * ntripMountpoint()
Fact *ntripPassword READ ntripPassword CONSTANT Fact * ntripPassword()
Fact *ntripUdpForwardEnabled READ ntripUdpForwardEnabled CONSTANT Fact * ntripUdpForwardEnabled()
Fact *ntripUdpTargetPort READ ntripUdpTargetPort CONSTANT Fact * ntripUdpTargetPort()
void error(const QString &errorMsg)
void sourceTableReceived(const QString &table)
void RTCMDataUpdate(QByteArrayView data)