diff --git a/qgcimages.qrc b/qgcimages.qrc
index 94220a6b98d7..16a639eaf92a 100644
--- a/qgcimages.qrc
+++ b/qgcimages.qrc
@@ -100,9 +100,6 @@
src/AutoPilotPlugins/PX4/Images/GeoFence.svg
src/AutoPilotPlugins/PX4/Images/GeoFenceLight.svg
src/AnalyzeView/GeoTag/GeoTagIcon.svg
- src/UI/toolbar/Images/Gps.svg
- src/UI/toolbar/Images/GpsAuthentication.svg
- src/UI/toolbar/Images/GpsInterference.svg
src/UI/toolbar/Images/Hamburger.svg
src/UI/toolbar/Images/HamburgerThin.svg
src/FlightMap/Images/Help.svg
@@ -172,7 +169,6 @@
src/UI/toolbar/Images/roi.svg
src/FlightMap/Images/rollDialWhite.svg
src/FlightMap/Images/rollPointerWhite.svg
- src/UI/toolbar/Images/RTK.svg
src/AutoPilotPlugins/Common/Images/SafetyComponentIcon.png
src/FlightMap/Images/scale.png
src/FlightMap/Images/scale_end.png
diff --git a/src/API/QGCCorePlugin.cc b/src/API/QGCCorePlugin.cc
index 90347d290b48..ada388f5e413 100644
--- a/src/API/QGCCorePlugin.cc
+++ b/src/API/QGCCorePlugin.cc
@@ -302,7 +302,7 @@ const QVariantList &QGCCorePlugin::toolBarIndicators()
{
static const QVariantList toolBarIndicatorList = QVariantList(
{
- QVariant::fromValue(QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/Toolbar/RTKGPSIndicator.qml"))),
+ QVariant::fromValue(QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/GPS/RTK/RTKGPSIndicator.qml"))),
}
);
diff --git a/src/AnalyzeView/GeoTag/GeoTagPage.qml b/src/AnalyzeView/GeoTag/GeoTagPage.qml
index 709973af61bc..b6ed33e81d8b 100644
--- a/src/AnalyzeView/GeoTag/GeoTagPage.qml
+++ b/src/AnalyzeView/GeoTag/GeoTagPage.qml
@@ -5,6 +5,7 @@ import QtQuick.Layouts
import QGroundControl
import QGroundControl.AnalyzeView
import QGroundControl.Controls
+import "qrc:/qml/QGroundControl/GPS/GPSFormatHelpers.js" as GPSHelpers
AnalyzePage {
id: geoTagPage
@@ -530,7 +531,7 @@ AnalyzePage {
Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 20
text: {
if (model.coordinate && model.coordinate.isValid) {
- return model.coordinate.latitude.toFixed(6) + ", " + model.coordinate.longitude.toFixed(6)
+ return GPSHelpers.formatCoordinate(model.coordinate.latitude, model.coordinate.longitude, 6)
}
return ""
}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6a6bd52091de..b77ef5c96796 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -39,6 +39,7 @@ add_subdirectory(FirmwarePlugin)
add_subdirectory(FlyView)
add_subdirectory(FlightMap)
add_subdirectory(FollowMe)
+add_subdirectory(GCSGeofence)
add_subdirectory(Gimbal)
add_subdirectory(GPS)
add_subdirectory(Utilities)
@@ -131,7 +132,12 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
FirstRunPromptDialogsModule
FlyViewModule
FlightMapModule
+ GCSGeofenceModule
+ GPSModule
+ GPSNTRIPModule
+ GPSRTKModule
PlanViewModule
+ PositionManagerModule
QGroundControlControlsModule
QGroundControlModule
ToolbarModule
diff --git a/src/Comms/LinkManager.cc b/src/Comms/LinkManager.cc
index c0b7ca22d569..cc21e5cb7034 100644
--- a/src/Comms/LinkManager.cc
+++ b/src/Comms/LinkManager.cc
@@ -17,9 +17,6 @@
#ifndef QGC_NO_SERIAL_LINK
#include "SerialLink.h"
#include "GPSManager.h"
-#include "PositionManager.h"
-#include "UdpIODevice.h"
-#include "GPSRtk.h"
#endif
#ifdef QT_DEBUG
@@ -38,9 +35,6 @@ LinkManager::LinkManager(QObject *parent)
: QObject(parent)
, _portListTimer(new QTimer(this))
, _qmlConfigurations(new QmlObjectListModel(this))
-#ifndef QGC_NO_SERIAL_LINK
- , _nmeaSocket(new UdpIODevice(this))
-#endif
{
qCDebug(LinkManagerLog) << this;
@@ -432,26 +426,6 @@ void LinkManager::_updateAutoConnectLinks()
_addUDPAutoConnectLink();
_addMAVLinkForwardingLink();
- // check to see if nmea gps is configured for UDP input, if so, set it up to connect
- if (_autoConnectSettings->autoConnectNmeaPort()->cookedValueString() == "UDP Port") {
- if ((_nmeaSocket->localPort() != _autoConnectSettings->nmeaUdpPort()->rawValue().toUInt()) || (_nmeaSocket->state() != UdpIODevice::BoundState)) {
- qCDebug(LinkManagerLog) << "Changing port for UDP NMEA stream";
- _nmeaSocket->close();
- _nmeaSocket->bind(QHostAddress::AnyIPv4, _autoConnectSettings->nmeaUdpPort()->rawValue().toUInt());
- QGCPositionManager::instance()->setNmeaSourceDevice(_nmeaSocket);
- }
-#ifndef QGC_NO_SERIAL_LINK
- if (_nmeaPort) {
- _nmeaPort->close();
- delete _nmeaPort;
- _nmeaPort = nullptr;
- _nmeaDeviceName = "";
- }
-#endif
- } else {
- _nmeaSocket->close();
- }
-
#ifndef QGC_NO_SERIAL_LINK
_addSerialAutoConnectLink();
#endif
@@ -758,27 +732,7 @@ void LinkManager::_addSerialAutoConnectLink()
QGCSerialPortInfo::BoardType_t boardType;
QString boardName;
- // check to see if nmea gps is configured for current Serial port, if so, set it up to connect
- if (portInfo.systemLocation().trimmed() == _autoConnectSettings->autoConnectNmeaPort()->cookedValueString()) {
- if (portInfo.systemLocation().trimmed() != _nmeaDeviceName) {
- _nmeaDeviceName = portInfo.systemLocation().trimmed();
- qCDebug(LinkManagerLog) << "Configuring nmea port" << _nmeaDeviceName;
- QSerialPort* newPort = new QSerialPort(portInfo, this);
- _nmeaBaud = _autoConnectSettings->autoConnectNmeaBaud()->cookedValue().toUInt();
- newPort->setBaudRate(static_cast(_nmeaBaud));
- qCDebug(LinkManagerLog) << "Configuring nmea baudrate" << _nmeaBaud;
- // This will stop polling old device if previously set
- QGCPositionManager::instance()->setNmeaSourceDevice(newPort);
- if (_nmeaPort) {
- delete _nmeaPort;
- }
- _nmeaPort = newPort;
- } else if (_autoConnectSettings->autoConnectNmeaBaud()->cookedValue().toUInt() != _nmeaBaud) {
- _nmeaBaud = _autoConnectSettings->autoConnectNmeaBaud()->cookedValue().toUInt();
- _nmeaPort->setBaudRate(static_cast(_nmeaBaud));
- qCDebug(LinkManagerLog) << "Configuring nmea baudrate" << _nmeaBaud;
- }
- } else if (portInfo.getBoardInfo(boardType, boardName)) {
+ if (portInfo.getBoardInfo(boardType, boardName)) {
// Should we be auto-connecting to this board type?
if (!_allowAutoConnectToBoard(boardType)) {
continue;
@@ -789,7 +743,7 @@ void LinkManager::_addSerialAutoConnectLink()
qCDebug(LinkManagerLog) << "Waiting for bootloader to finish" << portInfo.systemLocation();
continue;
}
- if (_portAlreadyConnected(portInfo.systemLocation()) || (_autoConnectRTKPort == portInfo.systemLocation())) {
+ if (_portAlreadyConnected(portInfo.systemLocation()) || _autoConnectRTKPorts.contains(portInfo.systemLocation())) {
qCDebug(LinkManagerVerboseLog) << "Skipping existing autoconnect" << portInfo.systemLocation();
} else if (!_autoconnectPortWaitList.contains(portInfo.systemLocation())) {
// We don't connect to the port the first time we see it. The ability to correctly detect whether we
@@ -812,9 +766,9 @@ void LinkManager::_addSerialAutoConnectLink()
pSerialConfig = new SerialConfiguration(tr("%1 on %2 (AutoConnect)").arg(boardName, portInfo.portName().trimmed()));
break;
case QGCSerialPortInfo::BoardTypeRTKGPS:
- qCDebug(LinkManagerLog) << "RTK GPS auto-connected" << portInfo.portName().trimmed();
- _autoConnectRTKPort = portInfo.systemLocation();
- GPSManager::instance()->gpsRtk()->connectGPS(portInfo.systemLocation(), boardName);
+ qCDebug(LinkManagerLog) << "RTK GPS auto-connected" << portInfo.portName().trimmed() << boardName;
+ _autoConnectRTKPorts.append(portInfo.systemLocation());
+ GPSManager::instance()->connectGPS(portInfo.systemLocation(), boardName);
break;
default:
qCWarning(LinkManagerLog) << "Internal error: Unknown board type" << boardType;
@@ -835,11 +789,14 @@ void LinkManager::_addSerialAutoConnectLink()
}
}
- // Check for RTK GPS connection gone
- if (!_autoConnectRTKPort.isEmpty() && !currentPorts.contains(_autoConnectRTKPort)) {
- qCDebug(LinkManagerLog) << "RTK GPS disconnected" << _autoConnectRTKPort;
- GPSManager::instance()->gpsRtk()->disconnectGPS();
- _autoConnectRTKPort.clear();
+ // Check for RTK GPS connections gone
+ for (int i = _autoConnectRTKPorts.size() - 1; i >= 0; --i) {
+ const QString &port = _autoConnectRTKPorts.at(i);
+ if (!currentPorts.contains(port)) {
+ qCDebug(LinkManagerLog) << "RTK GPS disconnected" << port;
+ GPSManager::instance()->disconnectGPS(port);
+ _autoConnectRTKPorts.removeAt(i);
+ }
}
}
@@ -862,7 +819,7 @@ bool LinkManager::_allowAutoConnectToBoard(QGCSerialPortInfo::BoardType_t boardT
}
break;
case QGCSerialPortInfo::BoardTypeRTKGPS:
- if (_autoConnectSettings->autoConnectRTKGPS()->rawValue().toBool() && !GPSManager::instance()->gpsRtk()->connected()) {
+ if (_autoConnectSettings->autoConnectRTKGPS()->rawValue().toBool() && !GPSManager::instance()->connected()) {
return true;
}
break;
diff --git a/src/Comms/LinkManager.h b/src/Comms/LinkManager.h
index 1eb8d8871fcb..5fdfec4836f5 100644
--- a/src/Comms/LinkManager.h
+++ b/src/Comms/LinkManager.h
@@ -25,7 +25,6 @@ class QmlObjectListModel;
class QTimer;
class SerialLink;
class UDPConfiguration;
-class UdpIODevice;
/// @brief Manage communication links
/// The Link Manager organizes the physical Links. It can manage arbitrary
@@ -179,14 +178,10 @@ private slots:
bool _portAlreadyConnected(const QString &portName);
void _filterCompositePorts(QList &portList);
- UdpIODevice *_nmeaSocket = nullptr;
QMap _autoconnectPortWaitList; ///< key: QGCSerialPortInfo::systemLocation, value: wait count
QList _activeLinkCheckList; ///< List of links we are waiting for a vehicle to show up on
QStringList _commPortList;
QStringList _commPortDisplayList;
- QString _autoConnectRTKPort;
- QString _nmeaDeviceName;
- uint32_t _nmeaBaud = 0;
- QSerialPort *_nmeaPort = nullptr;
+ QStringList _autoConnectRTKPorts;
#endif // QGC_NO_SERIAL_LINK
};
diff --git a/src/FirmwarePlugin/FirmwarePlugin.cc b/src/FirmwarePlugin/FirmwarePlugin.cc
index e0f9cd78ca5d..6984fafe45d3 100644
--- a/src/FirmwarePlugin/FirmwarePlugin.cc
+++ b/src/FirmwarePlugin/FirmwarePlugin.cc
@@ -195,8 +195,7 @@ const QVariantList &FirmwarePlugin::toolIndicators(const Vehicle*)
//-- Default list of indicators for all vehicles.
if (_toolIndicatorList.isEmpty()) {
_toolIndicatorList = QVariantList({
- QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/VehicleGPSIndicator.qml")),
- QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/GPSResilienceIndicator.qml")),
+ QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/GPS/RTK/VehicleGPSIndicator.qml")),
QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/TelemetryRSSIIndicator.qml")),
QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/RCRSSIIndicator.qml")),
QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/BatteryIndicator.qml")),
diff --git a/src/FlightMap/FlightMap.qml b/src/FlightMap/FlightMap.qml
index 58c9ed2a792b..aed0a5a20370 100644
--- a/src/FlightMap/FlightMap.qml
+++ b/src/FlightMap/FlightMap.qml
@@ -8,6 +8,7 @@ import Qt.labs.animation
import QGroundControl
import QGroundControl.Controls
import QGroundControl.FlightMap
+import QGroundControl.GPS.RTK
Map {
id: _map
@@ -225,12 +226,30 @@ Map {
}
}
+ RTKBaseStationMapItem {
+ activeVehicle: _activeVehicle
+ planView: _map.planView
+ }
+
+ /// GCS position accuracy circle
+ MapCircle {
+ center: gcsPosition
+ radius: QGroundControl.qgcPositionManger.gcsPositionHorizontalAccuracy
+ color: Qt.rgba(0.2, 0.4, 0.8, 0.15)
+ border.color: Qt.rgba(0.2, 0.4, 0.8, 0.4)
+ border.width: 1
+ visible: gcsPosition.isValid && !planView
+ && isFinite(QGroundControl.qgcPositionManger.gcsPositionHorizontalAccuracy)
+ && QGroundControl.qgcPositionManger.gcsPositionHorizontalAccuracy < 100
+ }
+
/// Ground Station location
MapQuickItem {
anchorPoint.x: sourceItem.width / 2
anchorPoint.y: sourceItem.height / 2
visible: gcsPosition.isValid && !planView
coordinate: gcsPosition
+ opacity: QGroundControl.qgcPositionManger.positionStale ? 0.4 : 1.0
sourceItem: Image {
id: mapItemImage
diff --git a/src/FlyView/FlyViewMap.qml b/src/FlyView/FlyViewMap.qml
index d6eacc939377..104cf1d78b54 100644
--- a/src/FlyView/FlyViewMap.qml
+++ b/src/FlyView/FlyViewMap.qml
@@ -10,6 +10,7 @@ import QGroundControl.Controls
import QGroundControl.FlyView
import QGroundControl.FlightMap
import QGroundControl.PlanView
+import "qrc:/qml/QGroundControl/GPS/GPSFormatHelpers.js" as GPSHelpers
FlightMap {
id: _root
@@ -745,8 +746,8 @@ FlightMap {
ColumnLayout {
spacing: 0
- QGCLabel { text: qsTr("Lat: %1").arg(mapClickCoord.latitude.toFixed(6)) }
- QGCLabel { text: qsTr("Lon: %1").arg(mapClickCoord.longitude.toFixed(6)) }
+ QGCLabel { text: qsTr("Lat: %1").arg(GPSHelpers.formatLatitude(mapClickCoord.latitude, 6)) }
+ QGCLabel { text: qsTr("Lon: %1").arg(GPSHelpers.formatLongitude(mapClickCoord.longitude, 6)) }
}
}
}
diff --git a/src/FlyView/FlyViewWidgetLayer.qml b/src/FlyView/FlyViewWidgetLayer.qml
index f79499a123d7..9e06664aac1b 100644
--- a/src/FlyView/FlyViewWidgetLayer.qml
+++ b/src/FlyView/FlyViewWidgetLayer.qml
@@ -12,6 +12,8 @@ import QGroundControl
import QGroundControl.Controls
import QGroundControl.FlyView
import QGroundControl.FlightMap
+import QGroundControl.GCSGeofence
+import QGroundControl.GPS.RTK
import QGroundControl.Viewer3D
// This is the ui overlay layer for the widgets/tools for Fly View
@@ -163,6 +165,20 @@ Item {
z: QGroundControl.zOrderTopMost
}
+ GCSGeofenceAlert {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: ScreenTools.defaultFontPixelHeight
+ z: QGroundControl.zOrderTopMost
+ }
+
+ GNSSIntegrityAlert {
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: ScreenTools.defaultFontPixelHeight * 4
+ z: QGroundControl.zOrderTopMost
+ }
+
MapScale {
id: mapScale
anchors.left: toolStrip.right
diff --git a/src/FlyView/PreFlightGPSCheck.qml b/src/FlyView/PreFlightGPSCheck.qml
index 696581ca2c79..af98a95e7d30 100644
--- a/src/FlyView/PreFlightGPSCheck.qml
+++ b/src/FlyView/PreFlightGPSCheck.qml
@@ -5,18 +5,23 @@ import QGroundControl.Controls
PreFlightCheckButton {
name: qsTr("GPS")
- telemetryFailure: _3dLockFailure || _satCountFailure
+ telemetryFailure: _3dLockFailure || _satCountFailure || _hdopFailure
telemetryTextFailure: _3dLockFailure ?
qsTr("Waiting for 3D lock.") :
- (_satCountFailure ? _satCountFailureText : "")
- allowTelemetryFailureOverride: !_3dLockFailure && _satCountFailure && allowOverrideSatCount
+ (_hdopFailure ? _hdopFailureText :
+ (_satCountFailure ? _satCountFailureText : ""))
+ allowTelemetryFailureOverride: !_3dLockFailure && (_satCountFailure && allowOverrideSatCount || _hdopFailure)
property bool allowOverrideSatCount: false ///< true: sat count above failureSatCount reguired to pass, false: user can click past satCount <= failureSetCount
property int failureSatCount: -1 ///< -1 indicates no sat count check
+ property real failureHdop: -1 ///< -1 indicates no HDOP check
- property bool _3dLock: globals.activeVehicle ? globals.activeVehicle.gps.lock.rawValue >= 3 : false
+ property bool _3dLock: globals.activeVehicle ? globals.activeVehicle.gps.lock.rawValue >= VehicleGPSFactGroup.Fix3D : false
property int _satCount: globals.activeVehicle ? globals.activeVehicle.gps.count.rawValue : 0
+ property real _hdop: globals.activeVehicle && !isNaN(globals.activeVehicle.gps.hdop.rawValue) ? globals.activeVehicle.gps.hdop.rawValue : -1
property bool _3dLockFailure: !_3dLock
property bool _satCountFailure: failureSatCount !== -1 && _satCount <= failureSatCount
+ property bool _hdopFailure: failureHdop > 0 && _hdop > 0 && _hdop > failureHdop
property string _satCountFailureText: allowOverrideSatCount ? qsTr("Warning - Sat count below %1.").arg(failureSatCount + 1) : qsTr("Waiting for sat count above %1.").arg(failureSatCount)
+ property string _hdopFailureText: qsTr("Warning - HDOP %1 exceeds limit %2.").arg(_hdop.toFixed(1)).arg(failureHdop.toFixed(1))
}
diff --git a/src/FollowMe/FollowMe.cc b/src/FollowMe/FollowMe.cc
index 2de62bd2b1e4..fedf351f5af5 100644
--- a/src/FollowMe/FollowMe.cc
+++ b/src/FollowMe/FollowMe.cc
@@ -3,6 +3,7 @@
#include "FirmwarePlugin.h"
#include "Vehicle.h"
#include "PositionManager.h"
+#include "QGCMAVLink.h"
#include "SettingsManager.h"
#include "AppSettings.h"
#include "QGCLoggingCategory.h"
@@ -116,8 +117,8 @@ void FollowMe::_sendGCSMotionReport()
// Get the current location coordinates
// Important note: QGC only supports sending the constant GCS home position altitude for follow me.
- motionReport.lat_int = static_cast(gcsCoordinate.latitude() * 1e7);
- motionReport.lon_int = static_cast(gcsCoordinate.longitude() * 1e7);
+ motionReport.lat_int = QGCMAVLink::doubleToMavlinkLatLon(gcsCoordinate.latitude());
+ motionReport.lon_int = QGCMAVLink::doubleToMavlinkLatLon(gcsCoordinate.longitude());
motionReport.altMetersAMSL = gcsCoordinate.altitude();
estimationCapabilities |= (1 << POS);
diff --git a/src/GCSGeofence/CMakeLists.txt b/src/GCSGeofence/CMakeLists.txt
new file mode 100644
index 000000000000..27e0a38861fe
--- /dev/null
+++ b/src/GCSGeofence/CMakeLists.txt
@@ -0,0 +1,28 @@
+# ============================================================================
+# GCS Geofence Module
+# Client-side geofence monitoring and vehicle fence synchronization
+# ============================================================================
+
+target_sources(${CMAKE_PROJECT_NAME}
+ PRIVATE
+ GCSGeofenceMonitor.cc
+ GCSGeofenceMonitor.h
+ GCSGeofenceSyncer.cc
+ GCSGeofenceSyncer.h
+)
+
+target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+
+# ----------------------------------------------------------------------------
+# GCSGeofence QML Module
+# ----------------------------------------------------------------------------
+qt_add_library(GCSGeofenceModule STATIC)
+
+qt_add_qml_module(GCSGeofenceModule
+ URI QGroundControl.GCSGeofence
+ VERSION 1.0
+ RESOURCE_PREFIX /qml
+ QML_FILES
+ GCSGeofenceAlert.qml
+ NO_PLUGIN
+)
diff --git a/src/GCSGeofence/GCSGeofenceAlert.qml b/src/GCSGeofence/GCSGeofenceAlert.qml
new file mode 100644
index 000000000000..4aa5410a058d
--- /dev/null
+++ b/src/GCSGeofence/GCSGeofenceAlert.qml
@@ -0,0 +1,43 @@
+import QtQuick
+
+import QGroundControl
+import QGroundControl.Controls
+
+Rectangle {
+ id: root
+
+ anchors.margins: -ScreenTools.defaultFontPixelHeight
+ height: alertColumn.height + ScreenTools.defaultFontPixelHeight
+ width: alertColumn.width + ScreenTools.defaultFontPixelWidth * 2
+ color: Qt.rgba(qgcPal.colorRed.r, qgcPal.colorRed.g, qgcPal.colorRed.b, 0.85)
+ radius: ScreenTools.defaultFontPixelWidth / 2
+ visible: _monitor ? _monitor.breached : false
+
+ property var _monitor: QGroundControl.gcsGeofenceMonitor
+
+ QGCPalette { id: qgcPal; colorGroupEnabled: true }
+
+ Column {
+ id: alertColumn
+ anchors.centerIn: parent
+ spacing: ScreenTools.defaultFontPixelHeight * 0.5
+
+ QGCLabel {
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "white"
+ font.pointSize: ScreenTools.largeFontPointSize
+ font.bold: true
+ text: qsTr("GCS Geofence Breach")
+ }
+
+ QGCLabel {
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "white"
+ font.pointSize: ScreenTools.mediumFontPointSize
+ width: ScreenTools.defaultFontPixelWidth * 40
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ text: _monitor ? _monitor.breachMessage : ""
+ }
+ }
+}
diff --git a/src/GCSGeofence/GCSGeofenceMonitor.cc b/src/GCSGeofence/GCSGeofenceMonitor.cc
new file mode 100644
index 000000000000..49bd6075bcfe
--- /dev/null
+++ b/src/GCSGeofence/GCSGeofenceMonitor.cc
@@ -0,0 +1,167 @@
+#include "GCSGeofenceMonitor.h"
+#include "PositionManager.h"
+#include "QGCLoggingCategory.h"
+
+#include
+#include
+
+QGC_LOGGING_CATEGORY(GCSGeofenceMonitorLog, "PositionManager.GCSGeofenceMonitor")
+
+Q_APPLICATION_STATIC(GCSGeofenceMonitor, _geofenceMonitor);
+
+GCSGeofenceMonitor::GCSGeofenceMonitor(QObject *parent)
+ : QObject(parent)
+{
+ qCDebug(GCSGeofenceMonitorLog) << this;
+}
+
+GCSGeofenceMonitor::~GCSGeofenceMonitor()
+{
+ qCDebug(GCSGeofenceMonitorLog) << this;
+}
+
+GCSGeofenceMonitor *GCSGeofenceMonitor::instance()
+{
+ return _geofenceMonitor();
+}
+
+void GCSGeofenceMonitor::init()
+{
+ (void) connect(QGCPositionManager::instance(), &QGCPositionManager::positionInfoUpdated,
+ this, &GCSGeofenceMonitor::_positionUpdated);
+}
+
+bool GCSGeofenceMonitor::startMonitoring(const QGeoAreaMonitorInfo &monitor, FenceType type)
+{
+ if (!monitor.isValid()) {
+ qCWarning(GCSGeofenceMonitorLog) << "Invalid monitor:" << monitor.name();
+ return false;
+ }
+
+ const QString id = monitor.identifier();
+ if (_monitors.contains(id)) {
+ qCDebug(GCSGeofenceMonitorLog) << "Updating monitor:" << monitor.name();
+ } else {
+ qCDebug(GCSGeofenceMonitorLog) << "Adding monitor:" << monitor.name()
+ << (type == FenceType::Inclusion ? "inclusion" : "exclusion")
+ << "area:" << monitor.area();
+ }
+
+ _monitors[id] = {monitor, type, false};
+ emit activeMonitorCountChanged();
+
+ const QGeoPositionInfo current = QGCPositionManager::instance()->geoPositionInfo();
+ if (current.isValid()) {
+ _positionUpdated(current);
+ }
+
+ return true;
+}
+
+bool GCSGeofenceMonitor::stopMonitoring(const QString &identifier)
+{
+ if (_monitors.remove(identifier)) {
+ qCDebug(GCSGeofenceMonitorLog) << "Removed monitor:" << identifier;
+ emit activeMonitorCountChanged();
+ _updateBreachState();
+ return true;
+ }
+ return false;
+}
+
+void GCSGeofenceMonitor::stopAllMonitoring()
+{
+ if (!_monitors.isEmpty()) {
+ _monitors.clear();
+ emit activeMonitorCountChanged();
+ _updateBreachState();
+ qCDebug(GCSGeofenceMonitorLog) << "All monitors cleared";
+ }
+}
+
+QList GCSGeofenceMonitor::activeMonitors() const
+{
+ QList result;
+ result.reserve(_monitors.size());
+ for (const auto &state : _monitors) {
+ result.append(state.info);
+ }
+ return result;
+}
+
+GCSGeofenceMonitor::FenceType GCSGeofenceMonitor::fenceType(const QString &identifier) const
+{
+ const auto it = _monitors.constFind(identifier);
+ if (it != _monitors.constEnd()) {
+ return it->type;
+ }
+ return FenceType::Inclusion;
+}
+
+void GCSGeofenceMonitor::_positionUpdated(const QGeoPositionInfo &update)
+{
+ if (!update.isValid() || _monitors.isEmpty()) {
+ return;
+ }
+
+ const QGeoCoordinate coord = update.coordinate();
+
+ for (auto it = _monitors.begin(); it != _monitors.end(); ++it) {
+ MonitorState &state = it.value();
+ const bool nowInside = state.info.area().contains(coord);
+
+ if (nowInside && !state.inside) {
+ state.inside = true;
+ emit areaEntered(state.info, update);
+ } else if (!nowInside && state.inside) {
+ state.inside = false;
+ emit areaExited(state.info, update);
+ }
+ }
+
+ _updateBreachState();
+}
+
+void GCSGeofenceMonitor::_updateBreachState()
+{
+ bool anyBreach = false;
+ QString message;
+
+ for (const auto &state : _monitors) {
+ const bool violated = (state.type == FenceType::Inclusion && !state.inside)
+ || (state.type == FenceType::Exclusion && state.inside);
+ if (violated) {
+ anyBreach = true;
+ if (!message.isEmpty()) {
+ message += QStringLiteral(", ");
+ }
+ if (state.type == FenceType::Inclusion) {
+ message += tr("Outside %1").arg(state.info.name());
+ } else {
+ message += tr("Inside %1").arg(state.info.name());
+ }
+ }
+ }
+
+ if (_breached != anyBreach) {
+ _breached = anyBreach;
+ emit breachedChanged();
+
+ if (anyBreach) {
+ for (const auto &state : _monitors) {
+ const bool violated = (state.type == FenceType::Inclusion && !state.inside)
+ || (state.type == FenceType::Exclusion && state.inside);
+ if (violated) {
+ emit breachDetected(state.info.name(), state.type);
+ }
+ }
+ } else {
+ emit breachCleared(QString());
+ }
+ }
+
+ if (_breachMessage != message) {
+ _breachMessage = message;
+ emit breachMessageChanged();
+ }
+}
diff --git a/src/GCSGeofence/GCSGeofenceMonitor.h b/src/GCSGeofence/GCSGeofenceMonitor.h
new file mode 100644
index 000000000000..8a10846f9ebe
--- /dev/null
+++ b/src/GCSGeofence/GCSGeofenceMonitor.h
@@ -0,0 +1,73 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(GCSGeofenceMonitorLog)
+
+class QGCPositionManager;
+
+class GCSGeofenceMonitor : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+ Q_PROPERTY(int activeMonitorCount READ activeMonitorCount NOTIFY activeMonitorCountChanged)
+ Q_PROPERTY(bool breached READ breached NOTIFY breachedChanged)
+ Q_PROPERTY(QString breachMessage READ breachMessage NOTIFY breachMessageChanged)
+
+public:
+ explicit GCSGeofenceMonitor(QObject *parent = nullptr);
+ ~GCSGeofenceMonitor() override;
+
+ static GCSGeofenceMonitor *instance();
+
+ void init();
+
+ enum class FenceType {
+ Inclusion,
+ Exclusion
+ };
+ Q_ENUM(FenceType)
+
+ Q_INVOKABLE bool startMonitoring(const QGeoAreaMonitorInfo &monitor, FenceType type = FenceType::Inclusion);
+ Q_INVOKABLE bool stopMonitoring(const QString &identifier);
+ Q_INVOKABLE void stopAllMonitoring();
+
+ QList activeMonitors() const;
+ int activeMonitorCount() const { return _monitors.size(); }
+ bool breached() const { return _breached; }
+ QString breachMessage() const { return _breachMessage; }
+ FenceType fenceType(const QString &identifier) const;
+
+signals:
+ void areaEntered(const QGeoAreaMonitorInfo &monitor, const QGeoPositionInfo &update);
+ void areaExited(const QGeoAreaMonitorInfo &monitor, const QGeoPositionInfo &update);
+ void breachDetected(const QString &monitorName, FenceType type);
+ void breachCleared(const QString &monitorName);
+ void activeMonitorCountChanged();
+ void breachedChanged();
+ void breachMessageChanged();
+
+private slots:
+ void _positionUpdated(const QGeoPositionInfo &update);
+
+private:
+ void _updateBreachState();
+
+ struct MonitorState {
+ QGeoAreaMonitorInfo info;
+ FenceType type = FenceType::Inclusion;
+ bool inside = false;
+ };
+
+ QHash _monitors;
+ bool _breached = false;
+ QString _breachMessage;
+};
diff --git a/src/GCSGeofence/GCSGeofenceSyncer.cc b/src/GCSGeofence/GCSGeofenceSyncer.cc
new file mode 100644
index 000000000000..2dff8bbece8a
--- /dev/null
+++ b/src/GCSGeofence/GCSGeofenceSyncer.cc
@@ -0,0 +1,199 @@
+#include "GCSGeofenceSyncer.h"
+#include "AudioOutput.h"
+#include "GCSGeofenceMonitor.h"
+#include "GeoFenceManager.h"
+#include "MultiVehicleManager.h"
+#include "ParameterManager.h"
+#include "QGCFenceCircle.h"
+#include "QGCFencePolygon.h"
+#include "QGCLoggingCategory.h"
+#include "Vehicle.h"
+
+#include
+#include
+#include
+#include
+
+QGC_LOGGING_CATEGORY(GCSGeofenceSyncerLog, "PositionManager.GCSGeofenceSyncer")
+
+Q_APPLICATION_STATIC(GCSGeofenceSyncer, _geofenceSyncer);
+
+GCSGeofenceSyncer::GCSGeofenceSyncer(QObject *parent)
+ : QObject(parent)
+{
+}
+
+GCSGeofenceSyncer::~GCSGeofenceSyncer()
+{
+}
+
+GCSGeofenceSyncer *GCSGeofenceSyncer::instance()
+{
+ return _geofenceSyncer();
+}
+
+void GCSGeofenceSyncer::init()
+{
+ auto *mvm = MultiVehicleManager::instance();
+ if (!mvm) {
+ qCWarning(GCSGeofenceSyncerLog) << "MultiVehicleManager not available";
+ return;
+ }
+ connect(mvm, &MultiVehicleManager::activeVehicleChanged,
+ this, &GCSGeofenceSyncer::_activeVehicleChanged);
+
+ auto *monitor = GCSGeofenceMonitor::instance();
+ if (!monitor) {
+ qCWarning(GCSGeofenceSyncerLog) << "GCSGeofenceMonitor not available";
+ return;
+ }
+ connect(monitor, &GCSGeofenceMonitor::breachDetected, this, [](const QString &name, GCSGeofenceMonitor::FenceType type) {
+ Q_UNUSED(type);
+ const QString msg = tr("warning, ground station geofence breach, %1").arg(name.toLower());
+ qCWarning(GCSGeofenceSyncerLog) << msg;
+ if (auto *audio = AudioOutput::instance()) {
+ audio->say(msg);
+ }
+ });
+}
+
+void GCSGeofenceSyncer::_activeVehicleChanged(Vehicle *vehicle)
+{
+ if (_vehicle) {
+ if (_fenceManager) {
+ disconnect(_fenceManager, nullptr, this, nullptr);
+ }
+ disconnect(_vehicle, nullptr, this, nullptr);
+ }
+
+ _clearMonitors();
+ _vehicle = vehicle;
+ _fenceManager = nullptr;
+
+ if (!_vehicle) {
+ return;
+ }
+
+ _fenceManager = _vehicle->geoFenceManager();
+
+ connect(_fenceManager, &GeoFenceManager::loadComplete,
+ this, &GCSGeofenceSyncer::_fenceLoadComplete);
+ connect(_vehicle, &Vehicle::homePositionChanged,
+ this, &GCSGeofenceSyncer::_homePositionChanged);
+
+ qCDebug(GCSGeofenceSyncerLog) << "Active vehicle changed, fence manager connected";
+}
+
+void GCSGeofenceSyncer::_fenceLoadComplete()
+{
+ qCDebug(GCSGeofenceSyncerLog) << "Fence load complete, syncing";
+ _syncFences();
+}
+
+void GCSGeofenceSyncer::_paramCircularFenceChanged()
+{
+ qCDebug(GCSGeofenceSyncerLog) << "Param circular fence changed, re-syncing";
+ _syncFences();
+}
+
+void GCSGeofenceSyncer::_homePositionChanged()
+{
+ _syncFences();
+}
+
+void GCSGeofenceSyncer::_syncFences()
+{
+ _clearMonitors();
+
+ if (!_vehicle || !_fenceManager) {
+ return;
+ }
+
+ auto *monitor = GCSGeofenceMonitor::instance();
+ if (!monitor) {
+ return;
+ }
+ int count = 0;
+
+ // Sync fence circles
+ const auto &circles = _fenceManager->circles();
+ for (int i = 0; i < circles.size(); ++i) {
+ QGCFenceCircle circle = circles.at(i);
+ const QGeoCoordinate center = circle.center();
+ const double radius = circle.radius()->rawValue().toDouble();
+
+ if (!center.isValid() || radius <= 0) {
+ continue;
+ }
+
+ QGeoCircle geoCircle(center, radius);
+ QGeoAreaMonitorInfo info;
+ info.setName(QStringLiteral("Fence Circle %1").arg(i));
+ info.setArea(geoCircle);
+
+ const auto type = circle.inclusion()
+ ? GCSGeofenceMonitor::FenceType::Inclusion
+ : GCSGeofenceMonitor::FenceType::Exclusion;
+
+ if (monitor->startMonitoring(info, type)) {
+ ++count;
+ }
+ }
+
+ // Sync fence polygons
+ const auto &polygons = _fenceManager->polygons();
+ for (int i = 0; i < polygons.size(); ++i) {
+ const QGCFencePolygon &polygon = polygons.at(i);
+ const QList coords = polygon.coordinateList();
+
+ if (coords.size() < 3) {
+ continue;
+ }
+
+ QGeoPolygon geoPoly(coords);
+ QGeoAreaMonitorInfo info;
+ info.setName(QStringLiteral("Fence Polygon %1").arg(i));
+ info.setArea(geoPoly);
+
+ const auto type = polygon.inclusion()
+ ? GCSGeofenceMonitor::FenceType::Inclusion
+ : GCSGeofenceMonitor::FenceType::Exclusion;
+
+ if (monitor->startMonitoring(info, type)) {
+ ++count;
+ }
+ }
+
+ // Sync parametric circular fence (GF_MAX_HOR_DIST / FENCE_RADIUS) around home
+ const QGeoCoordinate home = _vehicle->homePosition();
+ if (home.isValid()) {
+ auto *pm = _vehicle->parameterManager();
+ double paramRadius = 0;
+
+ if (pm->parameterExists(-1, QStringLiteral("GF_MAX_HOR_DIST"))) {
+ paramRadius = pm->getParameter(-1, QStringLiteral("GF_MAX_HOR_DIST"))->rawValue().toDouble();
+ } else if (pm->parameterExists(-1, QStringLiteral("FENCE_RADIUS"))) {
+ paramRadius = pm->getParameter(-1, QStringLiteral("FENCE_RADIUS"))->rawValue().toDouble();
+ }
+
+ if (paramRadius > 0) {
+ QGeoCircle geoCircle(home, paramRadius);
+ QGeoAreaMonitorInfo info;
+ info.setName(QStringLiteral("Circular Geofence"));
+ info.setArea(geoCircle);
+
+ if (monitor->startMonitoring(info, GCSGeofenceMonitor::FenceType::Inclusion)) {
+ ++count;
+ }
+ }
+ }
+
+ qCDebug(GCSGeofenceSyncerLog) << "Synced" << count << "fence monitors";
+}
+
+void GCSGeofenceSyncer::_clearMonitors()
+{
+ if (auto *monitor = GCSGeofenceMonitor::instance()) {
+ monitor->stopAllMonitoring();
+ }
+}
diff --git a/src/GCSGeofence/GCSGeofenceSyncer.h b/src/GCSGeofence/GCSGeofenceSyncer.h
new file mode 100644
index 000000000000..39fb1dd88bde
--- /dev/null
+++ b/src/GCSGeofence/GCSGeofenceSyncer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(GCSGeofenceSyncerLog)
+
+class GeoFenceManager;
+class Vehicle;
+
+class GCSGeofenceSyncer : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit GCSGeofenceSyncer(QObject *parent = nullptr);
+ ~GCSGeofenceSyncer() override;
+
+ static GCSGeofenceSyncer *instance();
+
+ void init();
+
+private slots:
+ void _activeVehicleChanged(Vehicle *vehicle);
+ void _fenceLoadComplete();
+ void _paramCircularFenceChanged();
+ void _homePositionChanged();
+
+private:
+ void _syncFences();
+ void _clearMonitors();
+
+ Vehicle *_vehicle = nullptr;
+ GeoFenceManager *_fenceManager = nullptr;
+};
diff --git a/src/GPS/CMakeLists.txt b/src/GPS/CMakeLists.txt
index 902a62e6e255..25dd5ccdcc7b 100644
--- a/src/GPS/CMakeLists.txt
+++ b/src/GPS/CMakeLists.txt
@@ -1,28 +1,15 @@
# ============================================================================
# GPS Module
-# GPS/GNSS positioning and RTK support
+# GPS/GNSS positioning, RTK support, and NTRIP corrections
# ============================================================================
-# GPS requires serial link support
-if(QGC_NO_SERIAL_LINK)
- return()
-endif()
-
target_sources(${CMAKE_PROJECT_NAME}
PRIVATE
+ GPSEvent.h
+ GPSEventModel.cc
+ GPSEventModel.h
GPSManager.cc
GPSManager.h
- GPSProvider.cc
- GPSProvider.h
- GPSRtk.cc
- GPSRtk.h
- GPSRTKFactGroup.cc
- GPSRTKFactGroup.h
- RTCMMavlink.cc
- RTCMMavlink.h
- satellite_info.h
- sensor_gnss_relative.h
- sensor_gps.h
)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::Core)
@@ -30,50 +17,22 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::Core)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# ----------------------------------------------------------------------------
-# NTRIP Subsystem
+# GPS QML Module
# ----------------------------------------------------------------------------
-add_subdirectory(NTRIP)
-
-# ============================================================================
-# PX4 GPS Drivers Integration
-# ============================================================================
-
-CPMAddPackage(
- NAME px4-gpsdrivers
- GITHUB_REPOSITORY PX4/PX4-GPSDrivers
- GIT_TAG caf5158061bd10e79c9f042abb62c86bc6f3e7a7
- SOURCE_SUBDIR src
-)
-
-# NOTE: Using file(GLOB) for external dependency sources
-# CONFIGURE_DEPENDS ensures CMake re-runs if files are added/removed
-file(GLOB GPS_DRIVERS_SOURCES
- CONFIGURE_DEPENDS
- "${px4-gpsdrivers_SOURCE_DIR}/src/*.c"
- "${px4-gpsdrivers_SOURCE_DIR}/src/*.cpp"
- "${px4-gpsdrivers_SOURCE_DIR}/src/*.h"
-)
-
-add_library(px4-gpsdrivers OBJECT ${GPS_DRIVERS_SOURCES})
-target_include_directories(px4-gpsdrivers SYSTEM PRIVATE ${px4-gpsdrivers_SOURCE_DIR}/src)
-target_compile_definitions(px4-gpsdrivers PRIVATE GPS_DEFINITIONS_HEADER=<${CMAKE_CURRENT_SOURCE_DIR}/definitions.h>)
-target_link_libraries(px4-gpsdrivers PRIVATE Qt6::Core)
-qgc_disable_dependency_warnings(px4-gpsdrivers)
-
-target_sources(${CMAKE_PROJECT_NAME}
- PRIVATE
- definitions.h
- $
+qt_add_library(GPSModule STATIC)
+
+qt_add_qml_module(GPSModule
+ URI QGroundControl.GPS
+ VERSION 1.0
+ RESOURCE_PREFIX /qml
+ QML_FILES
+ GPSFormatHelpers.js
+ NO_PLUGIN
)
# ----------------------------------------------------------------------------
-# GPS/RTK Configuration Resources
+# Subsystems
# ----------------------------------------------------------------------------
-qt_add_resources(${CMAKE_PROJECT_NAME} json_gps_drivers
- PREFIX "/json/Vehicle"
- FILES GPSRTKFact.json
-)
-
-target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE GPS_DEFINITIONS_HEADER=<${CMAKE_CURRENT_SOURCE_DIR}/definitions.h>)
-
-target_include_directories(${CMAKE_PROJECT_NAME} SYSTEM PRIVATE ${px4-gpsdrivers_SOURCE_DIR}/src)
+add_subdirectory(Driver)
+add_subdirectory(RTK)
+add_subdirectory(NTRIP)
diff --git a/src/GPS/Driver/CMakeLists.txt b/src/GPS/Driver/CMakeLists.txt
new file mode 100644
index 000000000000..5e6c8465f101
--- /dev/null
+++ b/src/GPS/Driver/CMakeLists.txt
@@ -0,0 +1,61 @@
+# ============================================================================
+# GPS Driver Module
+# PX4 GPS driver interface, transport layer, and sensor data types
+# ============================================================================
+
+target_sources(${CMAKE_PROJECT_NAME}
+ PRIVATE
+ GPSProvider.cc
+ GPSProvider.h
+ GPSTransport.h
+ GPSTypes.h
+ satellite_info.h
+ sensor_gnss_relative.h
+ sensor_gps.h
+)
+
+if(NOT QGC_NO_SERIAL_LINK)
+ target_sources(${CMAKE_PROJECT_NAME}
+ PRIVATE
+ SerialGPSTransport.cc
+ SerialGPSTransport.h
+ )
+endif()
+
+target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+
+# ============================================================================
+# PX4 GPS Drivers Integration
+# ============================================================================
+
+CPMAddPackage(
+ NAME px4-gpsdrivers
+ GITHUB_REPOSITORY PX4/PX4-GPSDrivers
+ GIT_TAG 5ed844c374646bfa2b41461c909a3b4df1a17233
+ SOURCE_SUBDIR src
+)
+
+# NOTE: Using file(GLOB) for external dependency sources
+# CONFIGURE_DEPENDS ensures CMake re-runs if files are added/removed
+file(GLOB GPS_DRIVERS_SOURCES
+ CONFIGURE_DEPENDS
+ "${px4-gpsdrivers_SOURCE_DIR}/src/*.c"
+ "${px4-gpsdrivers_SOURCE_DIR}/src/*.cpp"
+ "${px4-gpsdrivers_SOURCE_DIR}/src/*.h"
+)
+
+add_library(px4-gpsdrivers OBJECT ${GPS_DRIVERS_SOURCES})
+target_include_directories(px4-gpsdrivers SYSTEM PRIVATE ${px4-gpsdrivers_SOURCE_DIR}/src)
+target_compile_definitions(px4-gpsdrivers PRIVATE GPS_DEFINITIONS_HEADER=<${CMAKE_CURRENT_SOURCE_DIR}/definitions.h>)
+target_link_libraries(px4-gpsdrivers PRIVATE Qt6::Core)
+qgc_disable_dependency_warnings(px4-gpsdrivers)
+
+target_sources(${CMAKE_PROJECT_NAME}
+ PRIVATE
+ definitions.h
+ $
+)
+
+target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE GPS_DEFINITIONS_HEADER=<${CMAKE_CURRENT_SOURCE_DIR}/definitions.h>)
+
+target_include_directories(${CMAKE_PROJECT_NAME} SYSTEM PRIVATE ${px4-gpsdrivers_SOURCE_DIR}/src)
diff --git a/src/GPS/Driver/GPSProvider.cc b/src/GPS/Driver/GPSProvider.cc
new file mode 100644
index 000000000000..3ed76e1298f0
--- /dev/null
+++ b/src/GPS/Driver/GPSProvider.cc
@@ -0,0 +1,354 @@
+#include "GPSProvider.h"
+#include "GPSTransport.h"
+#include "QGCLoggingCategory.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+QGC_LOGGING_CATEGORY(GPSTypesLog, "GPS.GPSTypes")
+QGC_LOGGING_CATEGORY(GPSProviderLog, "GPS.GPSProvider")
+QGC_LOGGING_CATEGORY(GPSDriversLog, "GPS.Drivers")
+
+GPSProvider::GPSProvider(GPSTransport *transport, GPSType type, const rtk_data_s &rtkData, const std::atomic_bool &requestStop, QObject *parent)
+ : QObject(parent)
+ , _transport(transport)
+ , _type(type)
+ , _requestStop(requestStop)
+ , _rtkData(rtkData)
+{
+ qCDebug(GPSProviderLog) << this;
+
+ qCDebug(GPSProviderLog) << "Survey in accuracy:" << _rtkData.surveyInAccMeters
+ << "| duration:" << _rtkData.surveyInDurationSecs;
+}
+
+GPSProvider::rtk_data_s GPSProvider::rtkDataFromSettings(RTKSettings &settings)
+{
+ rtk_data_s d{};
+ d.surveyInAccMeters = settings.surveyInAccuracyLimit()->rawValue().toDouble();
+ d.surveyInDurationSecs = settings.surveyInMinObservationDuration()->rawValue().toInt();
+ d.useFixedBaseLocation = static_cast(settings.useFixedBasePosition()->rawValue().toInt());
+ d.fixedBaseLatitude = settings.fixedBasePositionLatitude()->rawValue().toDouble();
+ d.fixedBaseLongitude = settings.fixedBasePositionLongitude()->rawValue().toDouble();
+ d.fixedBaseAltitudeMeters = settings.fixedBasePositionAltitude()->rawValue().toFloat();
+ d.fixedBaseAccuracyMeters = settings.fixedBasePositionAccuracy()->rawValue().toFloat();
+ d.outputMode = static_cast(qBound(0u, settings.gpsOutputMode()->rawValue().toUInt(), 255u));
+ d.ubxMode = static_cast(qBound(0u, settings.ubxMode()->rawValue().toUInt(), 255u));
+ d.ubxDynamicModel = static_cast(qBound(0u, settings.ubxDynamicModel()->rawValue().toUInt(), 255u));
+ d.ubxUart2Baudrate = settings.ubxUart2Baudrate()->rawValue().toInt();
+ d.ubxPpkOutput = settings.ubxPpkOutput()->rawValue().toBool();
+ d.ubxDgnssTimeout = static_cast(qBound(0u, settings.ubxDgnssTimeout()->rawValue().toUInt(), 65535u));
+ d.ubxMinCno = static_cast(qBound(0u, settings.ubxMinCno()->rawValue().toUInt(), 255u));
+ d.ubxMinElevation = static_cast(qBound(0u, settings.ubxMinElevation()->rawValue().toUInt(), 255u));
+ d.ubxOutputRate = static_cast(qBound(0u, settings.ubxOutputRate()->rawValue().toUInt(), 65535u));
+ d.ubxJamDetSensitivityHi = settings.ubxJamDetSensitivityHi()->rawValue().toBool();
+ d.gnssSystems = settings.gnssSystems()->rawValue().toInt();
+ d.headingOffset = settings.headingOffset()->rawValue().toFloat();
+ return d;
+}
+
+GPSProvider::~GPSProvider()
+{
+ qCDebug(GPSProviderLog) << this;
+}
+
+void GPSProvider::wakeFromReconnectWait()
+{
+ QMutexLocker locker(&_stopMutex);
+ _stopCondition.wakeAll();
+}
+
+
+void GPSProvider::_publishSatelliteInfo()
+{
+ emit satelliteInfoUpdate(_satelliteInfo);
+}
+
+void GPSProvider::_publishSensorGNSSRelative()
+{
+ emit sensorGnssRelativeUpdate(_sensorGnssRelative);
+}
+
+void GPSProvider::_publishSensorGPS()
+{
+ emit sensorGpsUpdate(_sensorGps);
+}
+
+void GPSProvider::_gotRTCMData(const uint8_t *data, size_t len)
+{
+ const QByteArray message(reinterpret_cast(data), len);
+ emit RTCMDataUpdate(message);
+}
+
+void GPSProvider::injectRTCMData(const QByteArray &data)
+{
+ if (data.isEmpty()) {
+ return;
+ }
+
+ QMutexLocker locker(&_rtcmMutex);
+ _rtcmBuffer.append(data);
+}
+
+void GPSProvider::_drainRTCMBuffer()
+{
+ QByteArray data;
+ {
+ QMutexLocker locker(&_rtcmMutex);
+ if (_rtcmBuffer.isEmpty()) {
+ return;
+ }
+ data.swap(_rtcmBuffer);
+ }
+
+ if (_transport && _transport->isOpen() && _transport->write(data.constData(), data.size()) < 0) {
+ qCWarning(GPSProviderLog) << "Failed to inject RTCM data:" << data.size() << "bytes";
+ }
+}
+
+int GPSProvider::_callbackEntry(GPSCallbackType type, void *data1, int data2, void *user)
+{
+ GPSProvider* const gps = reinterpret_cast(user);
+ return gps->_callback(type, data1, data2);
+}
+
+int GPSProvider::_callback(GPSCallbackType type, void *data1, int data2)
+{
+ switch (type) {
+ case GPSCallbackType::readDeviceData: {
+ // PX4 convention: timeout is smuggled in first 4 bytes of data1 buffer
+ // data1 = buffer (with timeout in first bytes), data2 = buffer length
+ if (!_transport || !_transport->isOpen()) {
+ return -1;
+ }
+
+ _drainRTCMBuffer();
+
+ int timeout = 0;
+ memcpy(&timeout, data1, sizeof(timeout));
+
+ if (_transport->bytesAvailable() == 0) {
+ if (!_transport->waitForReadyRead(timeout)) {
+ return 0;
+ }
+ }
+ return static_cast(_transport->read(reinterpret_cast(data1), data2));
+ }
+ case GPSCallbackType::writeDeviceData:
+ if (!_transport || !_transport->isOpen()) {
+ return -1;
+ }
+ if (_transport->write(reinterpret_cast(data1), data2) >= 0) {
+ if (_transport->waitForBytesWritten(kGPSReceiveTimeout)) {
+ return data2;
+ }
+ }
+ return -1;
+ case GPSCallbackType::setBaudrate:
+ return (_transport && _transport->setBaudRate(data2)) ? 0 : -1;
+ case GPSCallbackType::gotRTCMMessage:
+ _gotRTCMData(reinterpret_cast(data1), data2);
+ break;
+ case GPSCallbackType::surveyInStatus:
+ {
+ const SurveyInStatus* const raw = reinterpret_cast(data1);
+
+ SurveyInStatusData status;
+ status.duration = raw->duration;
+ status.accuracyMM = raw->mean_accuracy;
+ status.latitude = raw->latitude;
+ status.longitude = raw->longitude;
+ status.altitude = raw->altitude;
+ status.valid = raw->flags & 1;
+ status.active = (raw->flags >> 1) & 1;
+
+ qCDebug(GPSProviderLog) << "Survey-in:" << status.duration << "s accuracy="
+ << status.accuracyMM << "mm valid=" << status.valid << "active=" << status.active;
+ emit surveyInStatus(status);
+ break;
+ }
+ case GPSCallbackType::gotRelativePositionMessage:
+ _publishSensorGNSSRelative();
+ break;
+ case GPSCallbackType::setClock:
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+void GPSProvider::process()
+{
+ if (!_transport || !_transport->open()) {
+ qCWarning(GPSProviderLog) << "Transport open failed, exiting GPS thread";
+ emit error(QStringLiteral("Failed to open GPS transport"));
+ emit finished();
+ return;
+ }
+ qCDebug(GPSProviderLog) << "Transport opened";
+
+ std::unique_ptr gpsDriver;
+
+ while (!_requestStop && !QThread::currentThread()->isInterruptionRequested()) {
+ if (gpsDriver) {
+ QMutexLocker locker(&_stopMutex);
+ gpsDriver.reset();
+ if (_requestStop) break;
+ _stopCondition.wait(&_stopMutex, kReconnectDelayMs);
+ if (_requestStop) break;
+ }
+
+ gpsDriver.reset(_connectGPS());
+ if (!gpsDriver) {
+ qCWarning(GPSProviderLog) << "Driver configuration failed for type" << static_cast(_type);
+ emit error(QStringLiteral("GPS driver configuration failed"));
+ continue;
+ }
+ {
+ qCDebug(GPSProviderLog) << "Driver connected, type:" << static_cast(_type);
+ emit connectionEstablished();
+ (void) memset(&_sensorGps, 0, sizeof(_sensorGps));
+
+ uint8_t numTries = 0;
+ static constexpr uint8_t kMaxReceiveTries = 10;
+ while (!_requestStop && !QThread::currentThread()->isInterruptionRequested() && (numTries < kMaxReceiveTries)) {
+ _drainRTCMBuffer();
+
+ const int helperRet = gpsDriver->receive(kGPSReceiveTimeout);
+
+ if (helperRet > 0) {
+ numTries = 0;
+
+ if (helperRet & GPSReceiveType::Position) {
+ _publishSensorGPS();
+ }
+
+ if (helperRet & GPSReceiveType::Satellite) {
+ _publishSatelliteInfo();
+ }
+ } else {
+ // Some drivers (NMEA) return -1 on timeout even though they've
+ // partially updated _sensorGps (e.g. GGA parsed but no VTG/RMC for velocity).
+ // Publish position if the driver wrote valid coordinates.
+ if (_sensorGps.fix_type >= sensor_gps_s::FIX_TYPE_2D
+ && (_sensorGps.latitude_deg != 0. || _sensorGps.longitude_deg != 0.)) {
+ qCDebug(GPSProviderLog) << "Publishing partial position from driver: fix="
+ << _sensorGps.fix_type << "lat=" << _sensorGps.latitude_deg;
+ _publishSensorGPS();
+ numTries = 0;
+ } else {
+ ++numTries;
+ qCDebug(GPSProviderLog) << "receive() returned" << helperRet
+ << "fix=" << _sensorGps.fix_type << "try" << numTries;
+ }
+ }
+ }
+
+ if (!_transport->isOpen()) {
+ emit error(QStringLiteral("GPS transport closed unexpectedly"));
+ break;
+ }
+ }
+ }
+
+ gpsDriver.reset();
+
+ _transport->close();
+
+ qCDebug(GPSProviderLog) << Q_FUNC_INFO << "Exiting GPS thread";
+ emit finished();
+}
+
+
+GPSHelper *GPSProvider::_connectGPS()
+{
+ GPSHelper *gpsDriver = nullptr;
+ GPSBaseStationSupport *baseStationDriver = nullptr;
+ uint32_t baudrate = 0;
+
+ switch (_type) {
+ case GPSType::Trimble:
+ baseStationDriver = new GPSDriverAshtech(&_callbackEntry, this, &_sensorGps, &_satelliteInfo);
+ gpsDriver = baseStationDriver;
+ baudrate = 115200;
+ break;
+ case GPSType::Septentrio:
+ baseStationDriver = new GPSDriverSBF(&_callbackEntry, this, &_sensorGps, &_satelliteInfo, _rtkData.headingOffset);
+ gpsDriver = baseStationDriver;
+ baudrate = 0;
+ break;
+ case GPSType::UBlox:
+ {
+ GPSDriverUBX::Settings ubxSettings{};
+ ubxSettings.dynamic_model = _rtkData.ubxDynamicModel;
+ ubxSettings.heading_offset = _rtkData.headingOffset;
+ ubxSettings.uart2_baudrate = _rtkData.ubxUart2Baudrate;
+ ubxSettings.ppk_output = _rtkData.ubxPpkOutput;
+ ubxSettings.mode = static_cast(_rtkData.ubxMode);
+ ubxSettings.dgnss_timeout = _rtkData.ubxDgnssTimeout;
+ ubxSettings.min_cno = _rtkData.ubxMinCno;
+ ubxSettings.min_elev = _rtkData.ubxMinElevation;
+ ubxSettings.output_rate = _rtkData.ubxOutputRate;
+ ubxSettings.jam_det_sensitivity_hi = _rtkData.ubxJamDetSensitivityHi;
+ baseStationDriver = new GPSDriverUBX(GPSDriverUBX::Interface::UART, &_callbackEntry, this, &_sensorGps, &_satelliteInfo, ubxSettings);
+ gpsDriver = baseStationDriver;
+ baudrate = 0;
+ break;
+ }
+ case GPSType::Femto:
+ baseStationDriver = new GPSDriverFemto(&_callbackEntry, this, &_sensorGps, &_satelliteInfo);
+ gpsDriver = baseStationDriver;
+ baudrate = 0;
+ break;
+ case GPSType::NMEA:
+ gpsDriver = new GPSDriverNMEA(&_callbackEntry, this, &_sensorGps, &_satelliteInfo, _rtkData.headingOffset);
+ baudrate = NMEA_DEFAULT_BAUDRATE;
+ break;
+ case GPSType::MTK:
+ gpsDriver = new GPSDriverMTK(&_callbackEntry, this, &_sensorGps);
+ baudrate = 0;
+ break;
+ case GPSType::EmlidReach:
+ gpsDriver = new GPSDriverEmlidReach(&_callbackEntry, this, &_sensorGps, &_satelliteInfo);
+ baudrate = 0;
+ break;
+ default:
+ qCWarning(GPSProviderLog) << "Unsupported GPS Type:" << static_cast(_type);
+ return nullptr;
+ }
+
+ if (baseStationDriver) {
+ switch (_rtkData.useFixedBaseLocation) {
+ case BaseModeDefinition::Mode::BaseFixed:
+ baseStationDriver->setBasePosition(_rtkData.fixedBaseLatitude, _rtkData.fixedBaseLongitude, _rtkData.fixedBaseAltitudeMeters, _rtkData.fixedBaseAccuracyMeters * 1000.0f);
+ break;
+ case BaseModeDefinition::Mode::BaseSurveyIn:
+ default:
+ baseStationDriver->setSurveyInSpecs(_rtkData.surveyInAccMeters * 10000.f, _rtkData.surveyInDurationSecs);
+ break;
+ }
+ _gpsConfig.output_mode = static_cast(_rtkData.outputMode);
+ } else {
+ _gpsConfig.output_mode = GPSHelper::OutputMode::GPS;
+ }
+
+ _gpsConfig.gnss_systems = static_cast(_rtkData.gnssSystems);
+
+ if (gpsDriver->configure(baudrate, _gpsConfig) != 0) {
+ delete gpsDriver;
+ return nullptr;
+ }
+
+ return gpsDriver;
+}
diff --git a/src/GPS/Driver/GPSProvider.h b/src/GPS/Driver/GPSProvider.h
new file mode 100644
index 000000000000..a0567b48f6f6
--- /dev/null
+++ b/src/GPS/Driver/GPSProvider.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include "GPSTypes.h"
+#include "Settings/RTKSettings.h"
+
+#include "satellite_info.h"
+#include "sensor_gnss_relative.h"
+#include "sensor_gps.h"
+
+Q_DECLARE_LOGGING_CATEGORY(GPSProviderLog)
+
+class GPSTransport;
+
+class GPSProvider : public QObject
+{
+ Q_OBJECT
+
+public:
+ struct rtk_data_s {
+ double surveyInAccMeters = 0;
+ int surveyInDurationSecs = 0;
+ BaseModeDefinition::Mode useFixedBaseLocation = BaseModeDefinition::Mode::BaseSurveyIn;
+ double fixedBaseLatitude = 0.;
+ double fixedBaseLongitude = 0.;
+ float fixedBaseAltitudeMeters = 0.f;
+ float fixedBaseAccuracyMeters = 0.f;
+
+ uint8_t outputMode = 1; // GPSHelper::OutputMode: 0=GPS, 1=GPSAndRTCM, 2=RTCM
+ uint8_t ubxMode = 0; // GPSDriverUBX::UBXMode: 0=Normal, 1-6 see ubx.h
+ uint8_t ubxDynamicModel = 7;
+ int32_t ubxUart2Baudrate = 57600;
+ bool ubxPpkOutput = false;
+ uint16_t ubxDgnssTimeout = 0;
+ uint8_t ubxMinCno = 0;
+ uint8_t ubxMinElevation = 0;
+ uint16_t ubxOutputRate = 0;
+ bool ubxJamDetSensitivityHi = false;
+ int32_t gnssSystems = 0; // GPSHelper::GNSSSystemsMask bitmask, 0 = receiver defaults
+ float headingOffset = 5.f; // Dual-antenna heading offset (degrees)
+ };
+
+ struct SurveyInStatusData {
+ float duration = 0.f;
+ float accuracyMM = 0.f;
+ double latitude = 0.;
+ double longitude = 0.;
+ float altitude = 0.f;
+ bool valid = false;
+ bool active = false;
+ };
+
+ GPSProvider(GPSTransport *transport, GPSType type, const rtk_data_s &rtkData, const std::atomic_bool &requestStop, QObject *parent = nullptr);
+ ~GPSProvider();
+
+ void injectRTCMData(const QByteArray &data);
+ void wakeFromReconnectWait();
+
+ static rtk_data_s rtkDataFromSettings(RTKSettings &settings);
+
+public slots:
+ void process();
+
+signals:
+ void connectionEstablished();
+ void satelliteInfoUpdate(const satellite_info_s &message);
+ void sensorGnssRelativeUpdate(const sensor_gnss_relative_s &message);
+ void sensorGpsUpdate(const sensor_gps_s &message);
+ void RTCMDataUpdate(const QByteArray &message);
+ void surveyInStatus(const GPSProvider::SurveyInStatusData &status);
+ void error(const QString &errorMessage);
+ void finished();
+
+private:
+ int _callback(GPSCallbackType type, void *data1, int data2);
+
+ GPSHelper *_connectGPS();
+ void _publishSensorGPS();
+ void _publishSatelliteInfo();
+ void _publishSensorGNSSRelative();
+
+ void _gotRTCMData(const uint8_t *data, size_t len);
+ void _drainRTCMBuffer();
+
+ static int _callbackEntry(GPSCallbackType type, void *data1, int data2, void *user);
+
+ GPSTransport *_transport = nullptr;
+ GPSType _type;
+ const std::atomic_bool &_requestStop; // must outlive this object
+ rtk_data_s _rtkData{};
+ GPSHelper::GPSConfig _gpsConfig{};
+
+ // Worker-thread-only: written in _callback(), emitted by-value via queued signals
+ struct satellite_info_s _satelliteInfo{};
+ struct sensor_gnss_relative_s _sensorGnssRelative{};
+ struct sensor_gps_s _sensorGps{};
+
+ QMutex _rtcmMutex;
+ QByteArray _rtcmBuffer;
+
+ QMutex _stopMutex;
+ QWaitCondition _stopCondition;
+
+ // Unscoped enum: used as bitmask with GPS library's int return from receive()
+ enum GPSReceiveType : int {
+ Position = 1,
+ Satellite = 2
+ };
+
+ static constexpr uint32_t kGPSReceiveTimeout = 1200;
+ static constexpr uint32_t kReconnectDelayMs = 1000;
+ static constexpr float kDefaultHeadingOffset = 5.f;
+};
+
+Q_DECLARE_METATYPE(GPSProvider::SurveyInStatusData)
diff --git a/src/GPS/Driver/GPSTransport.h b/src/GPS/Driver/GPSTransport.h
new file mode 100644
index 000000000000..a247142c0617
--- /dev/null
+++ b/src/GPS/Driver/GPSTransport.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include
+
+class GPSTransport : public QObject
+{
+ Q_OBJECT
+
+public:
+ using QObject::QObject;
+ ~GPSTransport() override = default;
+
+ virtual bool open() = 0;
+ virtual void close() = 0;
+ virtual bool isOpen() const = 0;
+
+ virtual qint64 read(char *data, qint64 maxSize) = 0;
+ virtual qint64 write(const char *data, qint64 size) = 0;
+ virtual bool waitForReadyRead(int msecs) = 0;
+ virtual bool waitForBytesWritten(int msecs) = 0;
+ virtual qint64 bytesAvailable() const = 0;
+ virtual bool setBaudRate(qint32 baudRate) = 0;
+};
diff --git a/src/GPS/Driver/GPSTypes.h b/src/GPS/Driver/GPSTypes.h
new file mode 100644
index 000000000000..84742586dfb7
--- /dev/null
+++ b/src/GPS/Driver/GPSTypes.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(GPSTypesLog)
+
+enum class GPSType : int {
+ UBlox = 0,
+ Trimble = 1,
+ Septentrio = 2,
+ Femto = 3,
+ NMEA = 4,
+ MTK = 5,
+ EmlidReach = 6
+};
+Q_DECLARE_METATYPE(GPSType)
+
+struct GPSTypeInfo {
+ GPSType type;
+ const char* matchString;
+ const char* displayName;
+ int manufacturerId;
+};
+
+inline constexpr std::array kGPSTypeRegistry {{
+ {GPSType::Trimble, "trimble", "Trimble", 1},
+ {GPSType::Septentrio, "septentrio", "Septentrio", 2},
+ {GPSType::Femto, "femtomes", "Femtomes", 3},
+ {GPSType::UBlox, "blox", "U-blox", 4},
+ {GPSType::NMEA, "nmea", "NMEA", 5},
+ {GPSType::MTK, "mtk", "MTK", 6},
+ {GPSType::EmlidReach, "emlid", "Emlid Reach", 7},
+}};
+
+inline GPSType gpsTypeFromString(QStringView str, int* manufacturerId = nullptr)
+{
+ for (const auto& entry : kGPSTypeRegistry) {
+ if (str.contains(QLatin1String(entry.matchString), Qt::CaseInsensitive)) {
+ if (manufacturerId) *manufacturerId = entry.manufacturerId;
+ return entry.type;
+ }
+ }
+ qCWarning(GPSTypesLog) << "Unrecognized GPS type string:" << str << ", defaulting to U-blox";
+ if (manufacturerId) *manufacturerId = 4;
+ return GPSType::UBlox;
+}
+
+inline const char* gpsTypeDisplayName(GPSType type)
+{
+ for (const auto& entry : kGPSTypeRegistry) {
+ if (entry.type == type) return entry.displayName;
+ }
+ return "Unknown";
+}
diff --git a/src/GPS/Driver/SerialGPSTransport.cc b/src/GPS/Driver/SerialGPSTransport.cc
new file mode 100644
index 000000000000..c81e2e260731
--- /dev/null
+++ b/src/GPS/Driver/SerialGPSTransport.cc
@@ -0,0 +1,106 @@
+#include "SerialGPSTransport.h"
+#include "QGCLoggingCategory.h"
+
+#ifdef Q_OS_ANDROID
+#include "qserialport.h"
+#else
+#include
+#endif
+
+#include
+
+QGC_LOGGING_CATEGORY(SerialGPSTransportLog, "GPS.SerialGPSTransport")
+
+SerialGPSTransport::SerialGPSTransport(const QString &device, QObject *parent)
+ : GPSTransport(parent)
+ , _device(device)
+{
+}
+
+SerialGPSTransport::~SerialGPSTransport()
+{
+ close();
+}
+
+bool SerialGPSTransport::open()
+{
+ if (_serial) {
+ close();
+ }
+
+ _serial = new QSerialPort(this);
+ _serial->setPortName(_device);
+ if (!_serial->open(QIODevice::ReadWrite)) {
+ uint32_t retries = 60;
+ while ((retries-- > 0) && (_serial->error() == QSerialPort::PermissionError)) {
+ if (QThread::currentThread()->isInterruptionRequested()) {
+ break;
+ }
+ qCDebug(SerialGPSTransportLog) << "Cannot open device... retrying";
+ QThread::msleep(500);
+ if (_serial->open(QIODevice::ReadWrite)) {
+ _serial->clearError();
+ break;
+ }
+ }
+
+ if (_serial->error() != QSerialPort::NoError) {
+ qCWarning(SerialGPSTransportLog) << "Failed to open Serial Device" << _device << _serial->errorString();
+ delete _serial;
+ _serial = nullptr;
+ return false;
+ }
+ }
+
+ (void) _serial->setBaudRate(QSerialPort::Baud9600);
+ (void) _serial->setDataBits(QSerialPort::Data8);
+ (void) _serial->setParity(QSerialPort::NoParity);
+ (void) _serial->setStopBits(QSerialPort::OneStop);
+ (void) _serial->setFlowControl(QSerialPort::NoFlowControl);
+
+ return true;
+}
+
+void SerialGPSTransport::close()
+{
+ if (_serial) {
+ _serial->close();
+ delete _serial;
+ _serial = nullptr;
+ }
+}
+
+bool SerialGPSTransport::isOpen() const
+{
+ return _serial && _serial->isOpen();
+}
+
+qint64 SerialGPSTransport::read(char *data, qint64 maxSize)
+{
+ return _serial ? _serial->read(data, maxSize) : -1;
+}
+
+qint64 SerialGPSTransport::write(const char *data, qint64 size)
+{
+ return _serial ? _serial->write(data, size) : -1;
+}
+
+bool SerialGPSTransport::waitForReadyRead(int msecs)
+{
+ return _serial && _serial->waitForReadyRead(msecs);
+}
+
+bool SerialGPSTransport::waitForBytesWritten(int msecs)
+{
+ return _serial && _serial->waitForBytesWritten(msecs);
+}
+
+qint64 SerialGPSTransport::bytesAvailable() const
+{
+ return _serial ? _serial->bytesAvailable() : 0;
+}
+
+bool SerialGPSTransport::setBaudRate(qint32 baudRate)
+{
+ return _serial && _serial->setBaudRate(baudRate);
+}
diff --git a/src/GPS/Driver/SerialGPSTransport.h b/src/GPS/Driver/SerialGPSTransport.h
new file mode 100644
index 000000000000..6e7e50fb45bd
--- /dev/null
+++ b/src/GPS/Driver/SerialGPSTransport.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "GPSTransport.h"
+
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(SerialGPSTransportLog)
+
+class QSerialPort;
+
+class SerialGPSTransport : public GPSTransport
+{
+ Q_OBJECT
+
+public:
+ explicit SerialGPSTransport(const QString &device, QObject *parent = nullptr);
+ ~SerialGPSTransport() override;
+
+ bool open() override;
+ void close() override;
+ bool isOpen() const override;
+
+ qint64 read(char *data, qint64 maxSize) override;
+ qint64 write(const char *data, qint64 size) override;
+ bool waitForReadyRead(int msecs) override;
+ bool waitForBytesWritten(int msecs) override;
+ qint64 bytesAvailable() const override;
+ bool setBaudRate(qint32 baudRate) override;
+
+private:
+ QString _device;
+ QSerialPort *_serial = nullptr;
+};
diff --git a/src/GPS/definitions.h b/src/GPS/Driver/definitions.h
similarity index 80%
rename from src/GPS/definitions.h
rename to src/GPS/Driver/definitions.h
index 676dfc7c08a6..4fdac02c0af0 100644
--- a/src/GPS/definitions.h
+++ b/src/GPS/Driver/definitions.h
@@ -11,7 +11,7 @@
Q_DECLARE_LOGGING_CATEGORY(GPSDriversLog)
-#define GPS_INFO(...) qCInfo(GPSDriversLog, __VA_ARGS__)
+#define GPS_INFO(...) qCDebug(GPSDriversLog, __VA_ARGS__)
#define GPS_WARN(...) qCWarning(GPSDriversLog, __VA_ARGS__)
#define GPS_ERR(...) qCCritical(GPSDriversLog, __VA_ARGS__)
@@ -20,19 +20,11 @@ Q_DECLARE_LOGGING_CATEGORY(GPSDriversLog)
#define M_DEG_TO_RAD_F 0.0174532925f
#define M_RAD_TO_DEG_F 57.2957795f
-#define M_PI_2_F 0.63661977f
+#define M_2_PI_F 0.63661977f
-#ifdef _WIN32
-#if (_MSC_VER < 1900)
-struct timespec
-{
- time_t tv_sec;
- long tv_nsec;
-};
-#else
+#ifdef Q_OS_WIN
#include
#endif
-#endif
static inline void gps_usleep(unsigned long usecs)
{
diff --git a/src/GPS/Driver/satellite_info.h b/src/GPS/Driver/satellite_info.h
new file mode 100644
index 000000000000..08d5f98ff929
--- /dev/null
+++ b/src/GPS/Driver/satellite_info.h
@@ -0,0 +1,66 @@
+/* https://github.com/PX4/PX4-Autopilot/blob/main/msg/SatelliteInfo.msg */
+
+#pragma once
+
+#include
+
+#include
+
+struct satellite_info_s
+{
+ uint64_t timestamp;
+ static constexpr uint8_t SAT_INFO_MAX_SATELLITES = 40;
+
+ uint8_t count;
+ uint8_t svid[SAT_INFO_MAX_SATELLITES];
+ uint8_t used[SAT_INFO_MAX_SATELLITES];
+ uint8_t elevation[SAT_INFO_MAX_SATELLITES];
+ uint16_t azimuth[SAT_INFO_MAX_SATELLITES];
+ uint8_t snr[SAT_INFO_MAX_SATELLITES];
+ uint8_t prn[SAT_INFO_MAX_SATELLITES];
+};
+Q_DECLARE_METATYPE(satellite_info_s);
+
+enum class SatelliteSystem : uint8_t {
+ Undefined,
+ GPS,
+ SBAS,
+ GLONASS,
+ BeiDou,
+ Galileo,
+ QZSS,
+};
+
+inline SatelliteSystem satelliteSystemFromSvid(uint8_t svid, uint8_t prn)
+{
+ if (svid >= 1 && svid <= 32) return SatelliteSystem::GPS;
+ if (svid >= 33 && svid <= 64) return SatelliteSystem::SBAS;
+ if (svid >= 65 && svid <= 96) return SatelliteSystem::GLONASS;
+ if (svid >= 120 && svid <= 158) return SatelliteSystem::SBAS;
+ if (svid >= 159 && svid <= 163) return SatelliteSystem::BeiDou;
+ if (svid >= 193 && svid <= 202) return SatelliteSystem::QZSS;
+ if (svid >= 211 && svid <= 246) return SatelliteSystem::Galileo;
+ if (svid >= 247) return SatelliteSystem::BeiDou;
+
+ if (prn >= 1 && prn <= 32) return SatelliteSystem::GPS;
+ if (prn >= 65 && prn <= 96) return SatelliteSystem::GLONASS;
+ if (prn >= 120 && prn <= 158) return SatelliteSystem::SBAS;
+ if (prn >= 159 && prn <= 195) return SatelliteSystem::BeiDou;
+ if (prn >= 211 && prn <= 246) return SatelliteSystem::Galileo;
+
+ return SatelliteSystem::Undefined;
+}
+
+inline const char* satelliteSystemName(SatelliteSystem sys)
+{
+ switch (sys) {
+ case SatelliteSystem::GPS: return "GPS";
+ case SatelliteSystem::SBAS: return "SBAS";
+ case SatelliteSystem::GLONASS: return "GLONASS";
+ case SatelliteSystem::BeiDou: return "BeiDou";
+ case SatelliteSystem::Galileo: return "Galileo";
+ case SatelliteSystem::QZSS: return "QZSS";
+ case SatelliteSystem::Undefined: return "Other";
+ }
+ return "Other";
+}
diff --git a/src/GPS/sensor_gnss_relative.h b/src/GPS/Driver/sensor_gnss_relative.h
similarity index 89%
rename from src/GPS/sensor_gnss_relative.h
rename to src/GPS/Driver/sensor_gnss_relative.h
index c142f5544dc2..811dbc4d7ea1 100644
--- a/src/GPS/sensor_gnss_relative.h
+++ b/src/GPS/Driver/sensor_gnss_relative.h
@@ -1,4 +1,4 @@
-/* https://github.com/PX4/Firmware/blob/master/msg/SensorGnssRelative.msg */
+/* https://github.com/PX4/PX4-Autopilot/blob/main/msg/SensorGnssRelative.msg */
#pragma once
diff --git a/src/GPS/sensor_gps.h b/src/GPS/Driver/sensor_gps.h
similarity index 57%
rename from src/GPS/sensor_gps.h
rename to src/GPS/Driver/sensor_gps.h
index d5a1e0ff0e1a..90dbb5cde00e 100644
--- a/src/GPS/sensor_gps.h
+++ b/src/GPS/Driver/sensor_gps.h
@@ -1,4 +1,4 @@
-/* https://github.com/PX4/Firmware/blob/master/msg/SensorGps.msg */
+/* https://github.com/PX4/PX4-Autopilot/blob/main/msg/SensorGps.msg */
#pragma once
@@ -41,17 +41,24 @@ struct sensor_gps_s
static constexpr uint8_t JAMMING_STATE_UNKNOWN = 0;
static constexpr uint8_t JAMMING_STATE_OK = 1;
- static constexpr uint8_t JAMMING_STATE_WARNING = 2;
- static constexpr uint8_t JAMMING_STATE_CRITICAL = 3;
+ static constexpr uint8_t JAMMING_STATE_MITIGATED = 2;
+ static constexpr uint8_t JAMMING_STATE_DETECTED = 3;
uint8_t jamming_state;
int32_t jamming_indicator;
static constexpr uint8_t SPOOFING_STATE_UNKNOWN = 0;
- static constexpr uint8_t SPOOFING_STATE_NONE = 1;
- static constexpr uint8_t SPOOFING_STATE_INDICATED = 2;
- static constexpr uint8_t SPOOFING_STATE_MULTIPLE = 3;
+ static constexpr uint8_t SPOOFING_STATE_OK = 1;
+ static constexpr uint8_t SPOOFING_STATE_MITIGATED = 2;
+ static constexpr uint8_t SPOOFING_STATE_DETECTED = 3;
uint8_t spoofing_state;
+ static constexpr uint8_t AUTHENTICATION_STATE_UNKNOWN = 0;
+ static constexpr uint8_t AUTHENTICATION_STATE_INITIALIZING = 1;
+ static constexpr uint8_t AUTHENTICATION_STATE_ERROR = 2;
+ static constexpr uint8_t AUTHENTICATION_STATE_OK = 3;
+ static constexpr uint8_t AUTHENTICATION_STATE_DISABLED = 4;
+ uint8_t authentication_state;
+
float vel_m_s;
float vel_n_m_s;
float vel_e_m_s;
@@ -64,6 +71,16 @@ struct sensor_gps_s
uint8_t satellites_used;
+ static constexpr uint32_t SYSTEM_ERROR_OK = 0;
+ static constexpr uint32_t SYSTEM_ERROR_INCOMING_CORRECTIONS = 1;
+ static constexpr uint32_t SYSTEM_ERROR_CONFIGURATION = 2;
+ static constexpr uint32_t SYSTEM_ERROR_SOFTWARE = 4;
+ static constexpr uint32_t SYSTEM_ERROR_ANTENNA = 8;
+ static constexpr uint32_t SYSTEM_ERROR_EVENT_CONGESTION = 16;
+ static constexpr uint32_t SYSTEM_ERROR_CPU_OVERLOAD = 32;
+ static constexpr uint32_t SYSTEM_ERROR_OUTPUT_CONGESTION = 64;
+ uint32_t system_error;
+
float heading;
float heading_offset;
float heading_accuracy;
diff --git a/src/GPS/GPSEvent.h b/src/GPS/GPSEvent.h
new file mode 100644
index 000000000000..7ee50a6af7b7
--- /dev/null
+++ b/src/GPS/GPSEvent.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+#include
+#include
+
+struct GPSEvent {
+ Q_GADGET
+
+public:
+ enum class Severity { Info, Warning, Error };
+ Q_ENUM(Severity)
+
+ enum class Source { RTKBase, NTRIP, Transport, GPS, GCS };
+ Q_ENUM(Source)
+
+ // Uses system clock UTC; assumes host clock is reasonably synchronized
+ QDateTime timestamp = QDateTime::currentDateTimeUtc();
+ Severity severity = Severity::Info;
+ Source source = Source::GPS;
+ QString message;
+
+ static GPSEvent info(Source src, const QString& msg) { return {QDateTime::currentDateTimeUtc(), Severity::Info, src, msg}; }
+ static GPSEvent warning(Source src, const QString& msg) { return {QDateTime::currentDateTimeUtc(), Severity::Warning, src, msg}; }
+ static GPSEvent error(Source src, const QString& msg) { return {QDateTime::currentDateTimeUtc(), Severity::Error, src, msg}; }
+};
+Q_DECLARE_METATYPE(GPSEvent)
diff --git a/src/GPS/GPSEventModel.cc b/src/GPS/GPSEventModel.cc
new file mode 100644
index 000000000000..7ebf7491b70c
--- /dev/null
+++ b/src/GPS/GPSEventModel.cc
@@ -0,0 +1,86 @@
+#include "GPSEventModel.h"
+
+GPSEventModel::GPSEventModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+int GPSEventModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : _events.size();
+}
+
+QVariant GPSEventModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() >= _events.size()) {
+ return {};
+ }
+ const auto &e = _events.at(index.row());
+ switch (role) {
+ case TimestampRole: return e.timestamp.toString(Qt::ISODate);
+ case SeverityRole: return severityString(e.severity);
+ case SourceRole: return sourceString(e.source);
+ case MessageRole: return e.message;
+ default: return {};
+ }
+}
+
+QHash GPSEventModel::roleNames() const
+{
+ return {
+ {TimestampRole, "timestamp"},
+ {SeverityRole, "severity"},
+ {SourceRole, "source"},
+ {MessageRole, "message"},
+ };
+}
+
+void GPSEventModel::append(const GPSEvent &event, int maxSize)
+{
+ if (maxSize <= 0) {
+ maxSize = 100;
+ }
+ if (_events.size() >= maxSize) {
+ beginRemoveRows(QModelIndex(), 0, 0);
+ _events.removeFirst();
+ endRemoveRows();
+ }
+ const int row = _events.size();
+ beginInsertRows(QModelIndex(), row, row);
+ _events.append(event);
+ endInsertRows();
+ emit countChanged();
+}
+
+void GPSEventModel::clear()
+{
+ if (_events.isEmpty()) {
+ return;
+ }
+ beginResetModel();
+ _events.clear();
+ endResetModel();
+ emit countChanged();
+}
+
+QString GPSEventModel::severityString(GPSEvent::Severity s)
+{
+ switch (s) {
+ case GPSEvent::Severity::Info: return QStringLiteral("Info");
+ case GPSEvent::Severity::Warning: return QStringLiteral("Warning");
+ case GPSEvent::Severity::Error: return QStringLiteral("Error");
+ }
+ return QStringLiteral("Unknown");
+}
+
+QString GPSEventModel::sourceString(GPSEvent::Source s)
+{
+ switch (s) {
+ case GPSEvent::Source::RTKBase: return QStringLiteral("RTK Base");
+ case GPSEvent::Source::NTRIP: return QStringLiteral("NTRIP");
+ case GPSEvent::Source::Transport: return QStringLiteral("Transport");
+ case GPSEvent::Source::GPS: return QStringLiteral("GPS");
+ case GPSEvent::Source::GCS: return QStringLiteral("GCS");
+ }
+ return QStringLiteral("Unknown");
+}
diff --git a/src/GPS/GPSEventModel.h b/src/GPS/GPSEventModel.h
new file mode 100644
index 000000000000..993b15412a1d
--- /dev/null
+++ b/src/GPS/GPSEventModel.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "GPSEvent.h"
+
+#include
+#include
+
+class GPSManager;
+
+class GPSEventModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(int count READ count NOTIFY countChanged)
+
+public:
+ enum Role {
+ TimestampRole = Qt::UserRole + 1,
+ SeverityRole,
+ SourceRole,
+ MessageRole
+ };
+
+ explicit GPSEventModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QHash roleNames() const override;
+
+ int count() const { return rowCount(); }
+
+ void append(const GPSEvent &event, int maxSize);
+ void clear();
+
+signals:
+ void countChanged();
+
+private:
+ static QString severityString(GPSEvent::Severity s);
+ static QString sourceString(GPSEvent::Source s);
+
+ QList _events;
+};
diff --git a/src/GPS/GPSFormatHelpers.js b/src/GPS/GPSFormatHelpers.js
new file mode 100644
index 000000000000..6a3c3c5bba83
--- /dev/null
+++ b/src/GPS/GPSFormatHelpers.js
@@ -0,0 +1,55 @@
+.pragma library
+
+function formatDuration(secs) {
+ if (secs < 60) return secs + "s"
+ if (secs < 3600) return Math.floor(secs / 60) + "m " + (secs % 60) + "s"
+ return Math.floor(secs / 3600) + "h " + Math.floor((secs % 3600) / 60) + "m"
+}
+
+function formatDataSize(bytes) {
+ if (bytes < 1024) return bytes + " B"
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB"
+ return (bytes / 1048576).toFixed(1) + " MB"
+}
+
+function formatDataRate(bytesPerSec) {
+ if (bytesPerSec < 1024) return bytesPerSec.toFixed(0) + " B/s"
+ return (bytesPerSec / 1024).toFixed(1) + " KB/s"
+}
+
+function formatAccuracy(rawVal, naText) {
+ if (isNaN(rawVal)) return naText || "-.--"
+ if (rawVal < 1.0) return (rawVal * 100).toFixed(1) + " cm"
+ return rawVal.toFixed(3) + " m"
+}
+
+var defaultCoordPrecision = 7
+
+function formatLatitude(lat, precision) {
+ return lat.toFixed(precision !== undefined ? precision : defaultCoordPrecision)
+}
+
+function formatLongitude(lon, precision) {
+ return lon.toFixed(precision !== undefined ? precision : defaultCoordPrecision)
+}
+
+function formatCoordinate(lat, lon, precision) {
+ var p = precision !== undefined ? precision : defaultCoordPrecision
+ return lat.toFixed(p) + ", " + lon.toFixed(p)
+}
+
+function formatHeading(degrees) {
+ if (isNaN(degrees)) return "-.--"
+ return degrees.toFixed(1) + "\u00B0"
+}
+
+// lockVal uses VehicleGPSFactGroup::GPSFixType enum values:
+// 0=None, 1=NoFix, 2=2D, 3=3D, 4=DGPS, 5=RTKFloat, 6=RTKFixed, 7=Static
+function fixTypeColor(lockVal, palette) {
+ if (lockVal >= 6) return palette.colorGreen // RTK Fixed / Static
+ if (lockVal >= 5) return palette.colorOrange // RTK Float
+ if (lockVal >= 3) return palette.colorGreen // 3D / DGPS
+ if (lockVal >= 2) return palette.colorOrange // 2D
+ if (lockVal >= 1) return palette.colorRed // No Fix
+ return palette.colorGrey // None
+}
diff --git a/src/GPS/GPSManager.cc b/src/GPS/GPSManager.cc
index 9c53966131a3..d85acc849e7a 100644
--- a/src/GPS/GPSManager.cc
+++ b/src/GPS/GPSManager.cc
@@ -1,26 +1,222 @@
#include "GPSManager.h"
+#include "FactGroup.h"
+#include "GPSPositionSource.h"
+#include "GPSProvider.h"
+#include "GPSRTKFactGroup.h"
+#include "GPSSatelliteInfoSource.h"
#include "GPSRtk.h"
+#include "GPSTypes.h"
#include "QGCLoggingCategory.h"
+#include "QmlObjectListModel.h"
+#include "RTCMMavlink.h"
+#include "RTCMRouter.h"
+#include "RTKSatelliteModel.h"
+#include "RTKSettings.h"
+#include "SettingsManager.h"
+
+#ifndef QGC_NO_SERIAL_LINK
+#include "QGCSerialPortInfo.h"
+#endif
#include
QGC_LOGGING_CATEGORY(GPSManagerLog, "GPS.GPSManager")
+// Initialization order: GPSManager → NTRIPManager → QGCPositionManager
+// NTRIPManager deps are injected via setRtcmRouter/setEventLogger in QGroundControlQmlGlobal ctor
Q_APPLICATION_STATIC(GPSManager, _gpsManager);
GPSManager::GPSManager(QObject *parent)
: QObject(parent)
- , _gpsRtk(new GPSRtk(this))
+ , _devices(new QmlObjectListModel(this))
+ , _rtcmMavlink(new RTCMMavlink(this))
+ , _rtcmRouter(new RTCMRouter(_rtcmMavlink, this))
{
- // qCDebug(GPSManagerLog) << Q_FUNC_INFO << this;
+ qCDebug(GPSManagerLog) << this;
+
+ (void) qRegisterMetaType("satellite_info_s");
+ (void) qRegisterMetaType("sensor_gnss_relative_s");
+ (void) qRegisterMetaType("sensor_gps_s");
+ (void) qRegisterMetaType("GPSProvider::SurveyInStatusData");
}
GPSManager::~GPSManager()
{
- // qCDebug(GPSManagerLog) << Q_FUNC_INFO << this;
+ disconnectAll();
+ qCDebug(GPSManagerLog) << this;
}
GPSManager *GPSManager::instance()
{
return _gpsManager();
}
+
+void GPSManager::connectGPS(const QString &device, const QString &gpsType)
+{
+ qCDebug(GPSManagerLog) << "connectGPS:" << device << "type:" << gpsType;
+ if (_deviceMap.contains(device)) {
+ qCDebug(GPSManagerLog) << "Device already connected:" << device;
+ return;
+ }
+
+ // Resolve GPS type: user setting > caller-supplied > USB board detection
+ RTKSettings *rtkSettings = SettingsManager::instance()->rtkSettings();
+ const int settingMfg = rtkSettings->baseReceiverManufacturers()->rawValue().toInt();
+ QString resolvedType = gpsType;
+
+ // If user has explicitly set a receiver type in settings, use that
+ if (settingMfg > 0) {
+ for (const auto &entry : kGPSTypeRegistry) {
+ if (entry.manufacturerId == settingMfg) {
+ resolvedType = QString::fromLatin1(entry.matchString);
+ qCDebug(GPSManagerLog) << "Using receiver type from settings:" << resolvedType;
+ break;
+ }
+ }
+ }
+#ifndef QGC_NO_SERIAL_LINK
+ // Only use USB board detection as fallback when no setting is configured
+ else {
+ const auto ports = QGCSerialPortInfo::availablePorts();
+ for (const QGCSerialPortInfo &port : ports) {
+ if (port.systemLocation() == device) {
+ QGCSerialPortInfo::BoardType_t boardType;
+ QString boardName;
+ if (port.getBoardInfo(boardType, boardName)
+ && boardType == QGCSerialPortInfo::BoardTypeRTKGPS
+ && !boardName.isEmpty()) {
+ qCDebug(GPSManagerLog) << "USB board detected:" << boardName;
+ resolvedType = boardName;
+ }
+ break;
+ }
+ }
+ }
+#endif
+
+ _lastError.clear();
+ emit lastErrorChanged();
+
+ auto *gpsRtk = new GPSRtk(this);
+ gpsRtk->setRtcmRouter(_rtcmRouter);
+ _rtcmRouter->addGpsRtk(gpsRtk);
+
+ (void) connect(gpsRtk, &GPSRtk::errorOccurred, this, [this](const QString &msg) {
+ _lastError = msg;
+ emit lastErrorChanged();
+ });
+
+ _deviceMap.insert(device, gpsRtk);
+ _devices->append(gpsRtk);
+ _mostRecentDevice = device;
+
+ gpsRtk->connectGPS(device, resolvedType);
+
+ emit deviceCountChanged();
+ emit deviceConnected(device);
+ emit positionSourceChanged();
+
+ qCDebug(GPSManagerLog) << "GPS device connected:" << device << "total:" << _deviceMap.size();
+}
+
+void GPSManager::disconnectGPS(const QString &device)
+{
+ GPSRtk *gpsRtk = _deviceMap.take(device);
+ if (!gpsRtk) {
+ return;
+ }
+
+ _rtcmRouter->removeGpsRtk(gpsRtk);
+ gpsRtk->disconnectGPS();
+ _devices->removeOne(gpsRtk);
+ gpsRtk->deleteLater();
+
+ emit deviceCountChanged();
+ emit deviceDisconnected(device);
+ emit positionSourceChanged();
+
+ qCDebug(GPSManagerLog) << "GPS device disconnected:" << device << "remaining:" << _deviceMap.size();
+}
+
+void GPSManager::disconnectAll()
+{
+ const QStringList devices = _deviceMap.keys();
+ for (const QString &device : devices) {
+ disconnectGPS(device);
+ }
+}
+
+bool GPSManager::connected() const
+{
+ for (auto it = _deviceMap.cbegin(); it != _deviceMap.cend(); ++it) {
+ if (it.value()->connected()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool GPSManager::isDeviceRegistered(const QString &device) const
+{
+ return _deviceMap.contains(device);
+}
+
+int GPSManager::deviceCount() const
+{
+ return _deviceMap.size();
+}
+
+GPSRtk *GPSManager::gpsRtk(const QString &device) const
+{
+ if (device.isEmpty()) {
+ return _primaryDevice();
+ }
+ return _deviceMap.value(device, nullptr);
+}
+
+GPSRtk *GPSManager::_primaryDevice() const
+{
+ if (_deviceMap.isEmpty()) {
+ return nullptr;
+ }
+ // Prefer the most recently connected device
+ if (!_mostRecentDevice.isEmpty() && _deviceMap.contains(_mostRecentDevice)) {
+ return _deviceMap.value(_mostRecentDevice);
+ }
+ return _deviceMap.cbegin().value();
+}
+
+FactGroup *GPSManager::gpsRtkFactGroup() const
+{
+ GPSRtk *primary = _primaryDevice();
+ if (primary) {
+ return primary->gpsRtkFactGroup();
+ }
+ // Return a static "disconnected" FactGroup so QML never binds to nullptr
+ static GPSRTKFactGroup sDisconnected;
+ return &sDisconnected;
+}
+
+RTKSatelliteModel *GPSManager::rtkSatelliteModel() const
+{
+ GPSRtk *primary = _primaryDevice();
+ return primary ? primary->satelliteModel() : nullptr;
+}
+
+QGeoPositionInfoSource *GPSManager::gpsPositionSource() const
+{
+ GPSRtk *primary = _primaryDevice();
+ return primary ? primary->positionSource() : nullptr;
+}
+
+QGeoSatelliteInfoSource *GPSManager::gpsSatelliteInfoSource() const
+{
+ GPSRtk *primary = _primaryDevice();
+ return primary ? primary->satelliteInfoSource() : nullptr;
+}
+
+void GPSManager::logEvent(const GPSEvent &event)
+{
+ _eventModel.append(event, kMaxEventLogSize);
+ emit eventLogged(event);
+}
diff --git a/src/GPS/GPSManager.h b/src/GPS/GPSManager.h
index bb4b556c15b1..b2ab511d694f 100644
--- a/src/GPS/GPSManager.h
+++ b/src/GPS/GPSManager.h
@@ -1,24 +1,83 @@
#pragma once
+#include "GPSEvent.h"
+#include "GPSEventModel.h"
+
+#include
#include
#include
+#include
+#include
Q_DECLARE_LOGGING_CATEGORY(GPSManagerLog)
+class FactGroup;
+class GPSPositionSource;
class GPSRtk;
+class QGeoPositionInfoSource;
+class QGeoSatelliteInfoSource;
+class QmlObjectListModel;
+class RTCMMavlink;
+class RTCMRouter;
+class RTKSatelliteModel;
class GPSManager : public QObject
{
Q_OBJECT
+ Q_MOC_INCLUDE("QmlObjectListModel.h")
+ Q_MOC_INCLUDE("RTCMMavlink.h")
+ Q_PROPERTY(int deviceCount READ deviceCount NOTIFY deviceCountChanged)
+ Q_PROPERTY(QmlObjectListModel* devices READ devices CONSTANT)
+ Q_PROPERTY(RTCMMavlink* rtcmMavlink READ rtcmMavlink CONSTANT)
+ Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
public:
- GPSManager(QObject *parent = nullptr);
- ~GPSManager();
+ static constexpr int kMaxEventLogSize = 50;
+
+ explicit GPSManager(QObject *parent = nullptr);
+ ~GPSManager() override;
static GPSManager *instance();
- GPSRtk *gpsRtk() { return _gpsRtk; }
+ Q_INVOKABLE void connectGPS(const QString &device, const QString &gpsType);
+ Q_INVOKABLE void disconnectGPS(const QString &device);
+ Q_INVOKABLE void disconnectAll();
+
+ bool connected() const;
+ bool isDeviceRegistered(const QString &device) const;
+ int deviceCount() const;
+ QmlObjectListModel *devices() { return _devices; }
+ QString lastError() const { return _lastError; }
+
+ GPSRtk *gpsRtk(const QString &device = {}) const;
+
+ FactGroup *gpsRtkFactGroup() const;
+ RTKSatelliteModel *rtkSatelliteModel() const;
+ QGeoPositionInfoSource *gpsPositionSource() const;
+ QGeoSatelliteInfoSource *gpsSatelliteInfoSource() const;
+ RTCMRouter *rtcmRouter() const { return _rtcmRouter; }
+ RTCMMavlink *rtcmMavlink() const { return _rtcmMavlink; }
+ GPSEventModel *eventModel() { return &_eventModel; }
+
+ void logEvent(const GPSEvent &event);
+
+signals:
+ void eventLogged(const GPSEvent &event);
+ void deviceCountChanged();
+ void lastErrorChanged();
+ void deviceConnected(const QString &device);
+ void deviceDisconnected(const QString &device);
+ void positionSourceChanged();
private:
- GPSRtk *_gpsRtk = nullptr;
+ GPSRtk *_primaryDevice() const;
+
+ QmlObjectListModel *_devices = nullptr;
+ QHash> _deviceMap;
+ QString _mostRecentDevice;
+
+ RTCMMavlink *_rtcmMavlink = nullptr;
+ RTCMRouter *_rtcmRouter = nullptr;
+ GPSEventModel _eventModel{this};
+ QString _lastError;
};
diff --git a/src/GPS/GPSProvider.cc b/src/GPS/GPSProvider.cc
deleted file mode 100644
index 7bb04f682340..000000000000
--- a/src/GPS/GPSProvider.cc
+++ /dev/null
@@ -1,262 +0,0 @@
-#include "GPSProvider.h"
-#include "QGCLoggingCategory.h"
-#include "RTCMMavlink.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#ifdef Q_OS_ANDROID
-#include "qserialport.h"
-#else
-#include
-#endif
-
-QGC_LOGGING_CATEGORY(GPSProviderLog, "GPS.GPSProvider")
-QGC_LOGGING_CATEGORY(GPSDriversLog, "GPS.Drivers")
-
-GPSProvider::GPSProvider(const QString &device, GPSType type, const rtk_data_s &rtkData, const std::atomic_bool &requestStop, QObject *parent)
- : QThread(parent)
- , _device(device)
- , _type(type)
- , _requestStop(requestStop)
- , _rtkData(rtkData)
-{
- // qCDebug(GPSProviderLog) << Q_FUNC_INFO << this;
-
- qCDebug(GPSProviderLog) << QString("Survey in accuracy: %1 | duration: %2").arg(_rtkData.surveyInAccMeters).arg(_rtkData.surveyInDurationSecs);
-}
-
-GPSProvider::~GPSProvider()
-{
- delete _serial;
-
- // qCDebug(GPSProviderLog) << Q_FUNC_INFO << this;
-}
-
-void GPSProvider::_publishSatelliteInfo()
-{
- emit satelliteInfoUpdate(_satelliteInfo);
-}
-
-void GPSProvider::_publishSensorGNSSRelative()
-{
- emit sensorGnssRelativeUpdate(_sensorGnssRelative);
-}
-
-void GPSProvider::_publishSensorGPS()
-{
- emit sensorGpsUpdate(_sensorGps);
-}
-
-void GPSProvider::_gotRTCMData(const uint8_t *data, size_t len)
-{
- const QByteArray message(reinterpret_cast(data), len);
- emit RTCMDataUpdate(message);
-}
-
-int GPSProvider::_callbackEntry(GPSCallbackType type, void *data1, int data2, void *user)
-{
- GPSProvider* const gps = reinterpret_cast(user);
- return gps->callback(type, data1, data2);
-}
-
-int GPSProvider::callback(GPSCallbackType type, void *data1, int data2)
-{
- switch (type) {
- case GPSCallbackType::readDeviceData:
- if (_serial->bytesAvailable() == 0) {
- const int timeout = *(reinterpret_cast(data1));
- if (!_serial->waitForReadyRead(timeout)) {
- return 0;
- }
- }
- return _serial->read(reinterpret_cast(data1), data2);
- case GPSCallbackType::writeDeviceData:
- if (_serial->write(reinterpret_cast(data1), data2) >= 0) {
- if (_serial->waitForBytesWritten(-1)) {
- return data2;
- }
- }
- return -1;
- case GPSCallbackType::setBaudrate:
- return (_serial->setBaudRate(data2) ? 0 : -1);
- case GPSCallbackType::gotRTCMMessage:
- _gotRTCMData(reinterpret_cast(data1), data2);
- break;
- case GPSCallbackType::surveyInStatus:
- {
- const SurveyInStatus* const status = reinterpret_cast(data1);
- qCDebug(GPSProviderLog) << "Position:" << status->latitude << status->longitude << status->altitude;
-
- const bool valid = status->flags & 1;
- const bool active = (status->flags >> 1) & 1;
-
- qCDebug(GPSProviderLog) << QString("Survey-in status: %1s cur accuracy: %2mm valid: %3 active: %4").arg(status->duration).arg(status->mean_accuracy).arg(valid).arg(active);
- emit surveyInStatus(status->duration, status->mean_accuracy, status->latitude, status->longitude, status->altitude, valid, active);
- break;
- }
- case GPSCallbackType::setClock:
- case GPSCallbackType::gotRelativePositionMessage:
- // _sensorGnssRelative
- default:
- break;
- }
-
- return 0;
-}
-
-void GPSProvider::run()
-{
-#ifdef SIMULATE_RTCM_OUTPUT
- _sendRTCMData();
-#endif
-
- _connectSerial();
-
- GPSBaseStationSupport *gpsDriver = nullptr;
-
- while (!_requestStop) {
- if (gpsDriver) {
- delete gpsDriver;
- gpsDriver = nullptr;
- }
-
- gpsDriver = _connectGPS();
- if (gpsDriver) {
- (void) memset(&_sensorGps, 0, sizeof(_sensorGps));
-
- uint8_t numTries = 0;
- while (!_requestStop && (numTries < 3)) {
- const int helperRet = gpsDriver->receive(kGPSReceiveTimeout);
-
- if (helperRet > 0) {
- numTries = 0;
-
- if (helperRet & GPSReceiveType::Position) {
- _publishSensorGPS();
- numTries = 0;
- }
-
- if (helperRet & GPSReceiveType::Satellite) {
- _publishSatelliteInfo();
- numTries = 0;
- }
- } else {
- ++numTries;
- }
- }
-
- if ((_serial->error() != QSerialPort::NoError) && (_serial->error() != QSerialPort::TimeoutError)) {
- break;
- }
- }
- }
-
- delete gpsDriver;
- gpsDriver = nullptr;
-
- delete _serial;
- _serial = nullptr;
-
- qCDebug(GPSProviderLog) << Q_FUNC_INFO << "Exiting GPS thread";
-}
-
-bool GPSProvider::_connectSerial()
-{
- _serial = new QSerialPort();
- _serial->setPortName(_device);
- if (!_serial->open(QIODevice::ReadWrite)) {
- // Give the device some time to come up. In some cases the device is not
- // immediately accessible right after startup for some reason. This can take 10-20s.
- uint32_t retries = 60;
- while ((retries-- > 0) && (_serial->error() == QSerialPort::PermissionError)) {
- qCDebug(GPSProviderLog) << "Cannot open device... retrying";
- msleep(500);
- if (_serial->open(QIODevice::ReadWrite)) {
- _serial->clearError();
- break;
- }
- }
-
- if (_serial->error() != QSerialPort::NoError) {
- qCWarning(GPSProviderLog) << "GPS: Failed to open Serial Device" << _device << _serial->errorString();
- return false;
- }
- }
-
- (void) _serial->setBaudRate(QSerialPort::Baud9600);
- (void) _serial->setDataBits(QSerialPort::Data8);
- (void) _serial->setParity(QSerialPort::NoParity);
- (void) _serial->setStopBits(QSerialPort::OneStop);
- (void) _serial->setFlowControl(QSerialPort::NoFlowControl);
-
- return true;
-}
-
-GPSBaseStationSupport *GPSProvider::_connectGPS()
-{
- GPSBaseStationSupport *gpsDriver = nullptr;
- uint32_t baudrate = 0;
- switch(_type) {
- case GPSType::trimble:
- gpsDriver = new GPSDriverAshtech(&_callbackEntry, this, &_sensorGps, &_satelliteInfo);
- baudrate = 115200;
- break;
- case GPSType::septentrio:
- gpsDriver = new GPSDriverSBF(&_callbackEntry, this, &_sensorGps, &_satelliteInfo, kGPSHeadingOffset);
- baudrate = 0;
- break;
- case GPSType::u_blox:
- gpsDriver = new GPSDriverUBX(GPSDriverUBX::Interface::UART, &_callbackEntry, this, &_sensorGps, &_satelliteInfo);
- baudrate = 0;
- break;
- case GPSType::femto:
- gpsDriver = new GPSDriverFemto(&_callbackEntry, this, &_sensorGps, &_satelliteInfo);
- baudrate = 0;
- break;
- default:
- // GPSDriverEmlidReach, GPSDriverMTK, GPSDriverNMEA
- qCWarning(GPSProviderLog) << "Unsupported GPS Type:" << static_cast(_type);
- return nullptr;
- }
-
- switch(_rtkData.useFixedBaseLocation){
- case BaseModeDefinition::Mode::BaseFixed:
- gpsDriver->setBasePosition(_rtkData.fixedBaseLatitude, _rtkData.fixedBaseLongitude, _rtkData.fixedBaseAltitudeMeters, _rtkData.fixedBaseAccuracyMeters * 1000.0f);
- break;
-
- case BaseModeDefinition::Mode::BaseSurveyIn:
- default:
- gpsDriver->setSurveyInSpecs(_rtkData.surveyInAccMeters * 10000.f, _rtkData.surveyInDurationSecs);
- break;
- }
-
- _gpsConfig.output_mode = GPSHelper::OutputMode::RTCM;
-
- if (gpsDriver->configure(baudrate, _gpsConfig) != 0) {
- return nullptr;
- }
-
- return gpsDriver;
-}
-
-void GPSProvider::_sendRTCMData()
-{
- RTCMMavlink *const rtcm = new RTCMMavlink(this);
-
- const int fakeMsgLengths[3] = { 30, 170, 240 };
- const uint8_t* const fakeData = new uint8_t[fakeMsgLengths[2]];
- while (!_requestStop) {
- for (int i = 0; i < 3; ++i) {
- const QByteArray message(reinterpret_cast(fakeData), fakeMsgLengths[i]);
- rtcm->RTCMDataUpdate(message);
- msleep(4);
- }
- msleep(100);
- }
- delete[] fakeData;
-}
diff --git a/src/GPS/GPSProvider.h b/src/GPS/GPSProvider.h
deleted file mode 100644
index 697a63dba576..000000000000
--- a/src/GPS/GPSProvider.h
+++ /dev/null
@@ -1,89 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include "Settings/RTKSettings.h"
-
-#include "satellite_info.h"
-#include "sensor_gnss_relative.h"
-#include "sensor_gps.h"
-
-Q_DECLARE_LOGGING_CATEGORY(GPSProviderLog)
-
-class QSerialPort;
-class GPSBaseStationSupport;
-
-class GPSProvider : public QThread
-{
- Q_OBJECT
-
-public:
- enum class GPSType {
- u_blox,
- trimble,
- septentrio,
- femto
- };
-
- struct rtk_data_s {
- double surveyInAccMeters = 0;
- int surveyInDurationSecs = 0;
- BaseModeDefinition::Mode useFixedBaseLocation = BaseModeDefinition::Mode::BaseSurveyIn;
- double fixedBaseLatitude = 0.;
- double fixedBaseLongitude = 0.;
- float fixedBaseAltitudeMeters = 0.f;
- float fixedBaseAccuracyMeters = 0.f;
- };
-
- GPSProvider(const QString &device, GPSType type, const rtk_data_s &rtkData, const std::atomic_bool &requestStop, QObject *parent = nullptr);
- ~GPSProvider();
-
- int callback(GPSCallbackType type, void *data1, int data2);
-
-signals:
- void satelliteInfoUpdate(const satellite_info_s &message);
- void sensorGnssRelativeUpdate(const sensor_gnss_relative_s &message);
- void sensorGpsUpdate(const sensor_gps_s &message);
- void RTCMDataUpdate(const QByteArray &message);
- void surveyInStatus(float duration, float accuracyMM, double latitude, double longitude, float altitude, bool valid, bool active);
-
-private:
- void run() final;
-
- bool _connectSerial();
- GPSBaseStationSupport *_connectGPS();
- void _publishSensorGPS();
- void _publishSatelliteInfo();
- void _publishSensorGNSSRelative();
-
- void _gotRTCMData(const uint8_t *data, size_t len);
- void _sendRTCMData();
-
- static int _callbackEntry(GPSCallbackType type, void *data1, int data2, void *user);
-
- QString _device;
- GPSType _type;
- const std::atomic_bool &_requestStop;
- rtk_data_s _rtkData{};
- GPSHelper::GPSConfig _gpsConfig{};
-
- struct satellite_info_s _satelliteInfo{};
- struct sensor_gnss_relative_s _sensorGnssRelative{};
- struct sensor_gps_s _sensorGps{};
-
- QSerialPort *_serial = nullptr;
-
- enum GPSReceiveType {
- Position = 1,
- Satellite = 2
- };
-
- static constexpr uint32_t kGPSReceiveTimeout = 1200;
- static constexpr float kGPSHeadingOffset = 5.f;
-};
diff --git a/src/GPS/GPSRTKFactGroup.cc b/src/GPS/GPSRTKFactGroup.cc
deleted file mode 100644
index 70c12e0824b0..000000000000
--- a/src/GPS/GPSRTKFactGroup.cc
+++ /dev/null
@@ -1,25 +0,0 @@
-#include "GPSRTKFactGroup.h"
-#include "QGCLoggingCategory.h"
-
-QGC_LOGGING_CATEGORY(GPSRTKFactGroupLog, "GPS.GPSRTKFactGroup")
-
-GPSRTKFactGroup::GPSRTKFactGroup(QObject *parent)
- : FactGroup(1000, QStringLiteral(":/json/Vehicle/GPSRTKFact.json"), parent)
-{
- // qCDebug(GPSRTKFactGroupLog) << Q_FUNC_INFO << this;
-
- _addFact(&_connectedFact);
- _addFact(&_currentDurationFact);
- _addFact(&_currentAccuracyFact);
- _addFact(&_currentLatitudeFact);
- _addFact(&_currentLongitudeFact);
- _addFact(&_currentAltitudeFact);
- _addFact(&_validFact);
- _addFact(&_activeFact);
- _addFact(&_numSatellitesFact);
-}
-
-GPSRTKFactGroup::~GPSRTKFactGroup()
-{
- // qCDebug(GPSRTKFactGroupLog) << Q_FUNC_INFO << this;
-}
diff --git a/src/GPS/GPSRTKFactGroup.h b/src/GPS/GPSRTKFactGroup.h
deleted file mode 100644
index d3bbd687cbe5..000000000000
--- a/src/GPS/GPSRTKFactGroup.h
+++ /dev/null
@@ -1,46 +0,0 @@
-#pragma once
-
-#include
-
-#include "FactGroup.h"
-
-Q_DECLARE_LOGGING_CATEGORY(GPSRTKFactGroupLog)
-
-class GPSRTKFactGroup : public FactGroup
-{
- Q_OBJECT
- Q_PROPERTY(Fact *connected READ connected CONSTANT)
- Q_PROPERTY(Fact *currentDuration READ currentDuration CONSTANT)
- Q_PROPERTY(Fact *currentAccuracy READ currentAccuracy CONSTANT)
- Q_PROPERTY(Fact *currentLatitude READ currentLatitude CONSTANT)
- Q_PROPERTY(Fact *currentLongitude READ currentLongitude CONSTANT)
- Q_PROPERTY(Fact *currentAltitude READ currentAltitude CONSTANT)
- Q_PROPERTY(Fact *valid READ valid CONSTANT)
- Q_PROPERTY(Fact *active READ active CONSTANT)
- Q_PROPERTY(Fact *numSatellites READ numSatellites CONSTANT)
-
-public:
- explicit GPSRTKFactGroup(QObject *parent = nullptr);
- ~GPSRTKFactGroup();
-
- Fact *connected() { return &_connectedFact; }
- Fact *currentDuration() { return &_currentDurationFact; }
- Fact *currentAccuracy() { return &_currentAccuracyFact; }
- Fact *currentLatitude() { return &_currentLatitudeFact; }
- Fact *currentLongitude() { return &_currentLongitudeFact; }
- Fact *currentAltitude() { return &_currentAltitudeFact; }
- Fact *valid() { return &_validFact; }
- Fact *active() { return &_activeFact; }
- Fact *numSatellites() { return &_numSatellitesFact; }
-
-private:
- Fact _connectedFact = Fact(0, QStringLiteral("connected"), FactMetaData::valueTypeBool);
- Fact _currentDurationFact = Fact(0, QStringLiteral("currentDuration"), FactMetaData::valueTypeDouble);
- Fact _currentAccuracyFact = Fact(0, QStringLiteral("currentAccuracy"), FactMetaData::valueTypeDouble);
- Fact _currentLatitudeFact = Fact(0, QStringLiteral("currentLatitude"), FactMetaData::valueTypeDouble);
- Fact _currentLongitudeFact = Fact(0, QStringLiteral("currentLongitude"), FactMetaData::valueTypeDouble);
- Fact _currentAltitudeFact = Fact(0, QStringLiteral("currentAltitude"), FactMetaData::valueTypeFloat);
- Fact _validFact = Fact(0, QStringLiteral("valid"), FactMetaData::valueTypeBool);
- Fact _activeFact = Fact(0, QStringLiteral("active"), FactMetaData::valueTypeBool);
- Fact _numSatellitesFact = Fact(0, QStringLiteral("numSatellites"), FactMetaData::valueTypeInt32);
-};
diff --git a/src/GPS/GPSRtk.cc b/src/GPS/GPSRtk.cc
deleted file mode 100644
index c963b03451c6..000000000000
--- a/src/GPS/GPSRtk.cc
+++ /dev/null
@@ -1,154 +0,0 @@
-#include "GPSRtk.h"
-#include "GPSProvider.h"
-#include "GPSRTKFactGroup.h"
-#include "QGCLoggingCategory.h"
-#include "RTCMMavlink.h"
-#include "RTKSettings.h"
-#include "SettingsManager.h"
-
-QGC_LOGGING_CATEGORY(GPSRtkLog, "GPS.GPSRtk")
-
-GPSRtk::GPSRtk(QObject *parent)
- : QObject(parent)
- , _gpsRtkFactGroup(new GPSRTKFactGroup(this))
-{
- qCDebug(GPSRtkLog) << this;
-
- (void) qRegisterMetaType("satellite_info_s");
- (void) qRegisterMetaType("sensor_gnss_relative_s");
- (void) qRegisterMetaType("sensor_gps_s");
-}
-
-GPSRtk::~GPSRtk()
-{
- disconnectGPS();
-
- qCDebug(GPSRtkLog) << this;
-}
-
-void GPSRtk::_onGPSConnect()
-{
- _gpsRtkFactGroup->connected()->setRawValue(true);
-}
-
-void GPSRtk::_onGPSDisconnect()
-{
- _gpsRtkFactGroup->connected()->setRawValue(false);
-}
-
-void GPSRtk::_onGPSSurveyInStatus(float duration, float accuracyMM, double latitude, double longitude, float altitude, bool valid, bool active)
-{
- _gpsRtkFactGroup->currentDuration()->setRawValue(duration);
- _gpsRtkFactGroup->currentAccuracy()->setRawValue(static_cast(accuracyMM) / 1000.0);
- _gpsRtkFactGroup->currentLatitude()->setRawValue(latitude);
- _gpsRtkFactGroup->currentLongitude()->setRawValue(longitude);
- _gpsRtkFactGroup->currentAltitude()->setRawValue(altitude);
- _gpsRtkFactGroup->valid()->setRawValue(valid);
- _gpsRtkFactGroup->active()->setRawValue(active);
-}
-
-void GPSRtk::connectGPS(const QString &device, QStringView gps_type)
-{
- GPSProvider::GPSType type;
- RTKSettings* rtkSettings = SettingsManager::instance()->rtkSettings();
-
- if (gps_type.contains(QStringLiteral("trimble"), Qt::CaseInsensitive)) {
- type = GPSProvider::GPSType::trimble;
- rtkSettings->baseReceiverManufacturers()->setRawValue(1); // Trimble
- qCDebug(GPSRtkLog) << "Connecting Trimble device";
-
- } else if (gps_type.contains(QStringLiteral("septentrio"), Qt::CaseInsensitive)) {
- type = GPSProvider::GPSType::septentrio;
- rtkSettings->baseReceiverManufacturers()->setRawValue(2); // Septentrio
- qCDebug(GPSRtkLog) << "Connecting Septentrio device";
-
- } else if (gps_type.contains(QStringLiteral("femtomes"), Qt::CaseInsensitive)) {
- type = GPSProvider::GPSType::femto;
- rtkSettings->baseReceiverManufacturers()->setRawValue(3); // Femto
- qCDebug(GPSRtkLog) << "Connecting Femtomes device";
-
- } else if(gps_type.contains(QStringLiteral("blox"), Qt::CaseInsensitive)) {
- type = GPSProvider::GPSType::u_blox;
- rtkSettings->baseReceiverManufacturers()->setRawValue(4); // Ublox
- qCDebug(GPSRtkLog) << "Connecting U-blox device";
- }else{
- type = GPSProvider::GPSType::u_blox;
- rtkSettings->baseReceiverManufacturers()->setRawValue(4); // Ublox
- qCDebug(GPSRtkLog) << "Connecting device has U-blox by default";
- }
-
- disconnectGPS();
-
- _requestGpsStop = false;
- const GPSProvider::rtk_data_s rtkData = {
- rtkSettings->surveyInAccuracyLimit()->rawValue().toDouble(),
- rtkSettings->surveyInMinObservationDuration()->rawValue().toInt(),
- static_cast(rtkSettings->useFixedBasePosition()->rawValue().toInt()),
- rtkSettings->fixedBasePositionLatitude()->rawValue().toDouble(),
- rtkSettings->fixedBasePositionLongitude()->rawValue().toDouble(),
- rtkSettings->fixedBasePositionAltitude()->rawValue().toFloat(),
- rtkSettings->fixedBasePositionAccuracy()->rawValue().toFloat()
- };
- _gpsProvider = new GPSProvider(
- device,
- type,
- rtkData,
- _requestGpsStop,
- this
- );
- (void) QMetaObject::invokeMethod(_gpsProvider, "start", Qt::AutoConnection);
-
- _rtcmMavlink = new RTCMMavlink(this);
- (void) connect(_gpsProvider, &GPSProvider::RTCMDataUpdate, _rtcmMavlink, &RTCMMavlink::RTCMDataUpdate);
-
- (void) connect(_gpsProvider, &GPSProvider::satelliteInfoUpdate, this, &GPSRtk::_satelliteInfoUpdate);
- (void) connect(_gpsProvider, &GPSProvider::sensorGpsUpdate, this, &GPSRtk::_sensorGpsUpdate);
- (void) connect(_gpsProvider, &GPSProvider::surveyInStatus, this, &GPSRtk::_onGPSSurveyInStatus);
- (void) connect(_gpsProvider, &GPSProvider::finished, this, &GPSRtk::_onGPSDisconnect);
-
- (void) QMetaObject::invokeMethod(this, "_onGPSConnect", Qt::AutoConnection);
-}
-
-void GPSRtk::disconnectGPS()
-{
- if (_gpsProvider) {
- _requestGpsStop = true;
- if (!_gpsProvider->wait(kGPSThreadDisconnectTimeout)) {
- qCWarning(GPSRtkLog) << "Failed to wait for GPS thread exit. Consider increasing the timeout";
- }
-
- _gpsProvider->deleteLater();
- _gpsProvider = nullptr;
- }
-
- if (_rtcmMavlink) {
- _rtcmMavlink->deleteLater();
- _rtcmMavlink = nullptr;
- }
-}
-
-bool GPSRtk::connected() const
-{
- return (_gpsProvider ? _gpsProvider->isRunning() : false);
-}
-
-FactGroup *GPSRtk::gpsRtkFactGroup()
-{
- return _gpsRtkFactGroup;
-}
-
-void GPSRtk::_satelliteInfoUpdate(const satellite_info_s &msg)
-{
- qCDebug(GPSRtkLog) << Q_FUNC_INFO << QStringLiteral("%1 satellites").arg(msg.count);
- _gpsRtkFactGroup->numSatellites()->setRawValue(msg.count);
-}
-
-void GPSRtk::_sensorGnssRelativeUpdate(const sensor_gnss_relative_s &/*msg*/)
-{
- qCDebug(GPSRtkLog) << Q_FUNC_INFO;
-}
-
-void GPSRtk::_sensorGpsUpdate(const sensor_gps_s &msg)
-{
- qCDebug(GPSRtkLog) << Q_FUNC_INFO << QStringLiteral("alt=%1, long=%2, lat=%3").arg(msg.altitude_msl_m).arg(msg.longitude_deg).arg(msg.latitude_deg);
-}
diff --git a/src/GPS/GPSRtk.h b/src/GPS/GPSRtk.h
deleted file mode 100644
index 3206718248c5..000000000000
--- a/src/GPS/GPSRtk.h
+++ /dev/null
@@ -1,47 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-
-#include "satellite_info.h"
-#include "sensor_gnss_relative.h"
-#include "sensor_gps.h"
-
-Q_DECLARE_LOGGING_CATEGORY(GPSRtkLog)
-
-class GPSRTKFactGroup;
-class FactGroup;
-class RTCMMavlink;
-class GPSProvider;
-
-class GPSRtk : public QObject
-{
- Q_OBJECT
-
-public:
- explicit GPSRtk(QObject *parent = nullptr);
- ~GPSRtk();
-
- void connectGPS(const QString &device, QStringView gps_type);
- void disconnectGPS();
- bool connected() const;
- FactGroup *gpsRtkFactGroup();
-
-private slots:
- void _satelliteInfoUpdate(const satellite_info_s &msg);
- void _sensorGnssRelativeUpdate(const sensor_gnss_relative_s &msg);
- void _sensorGpsUpdate(const sensor_gps_s &msg);
- void _onGPSConnect();
- void _onGPSDisconnect();
- void _onGPSSurveyInStatus(float duration, float accuracyMM, double latitude, double longitude, float altitude, bool valid, bool active);
-
-private:
- GPSProvider *_gpsProvider = nullptr;
- RTCMMavlink *_rtcmMavlink = nullptr;
- GPSRTKFactGroup *_gpsRtkFactGroup = nullptr;
-
- std::atomic_bool _requestGpsStop = false;
-
- static constexpr uint32_t kGPSThreadDisconnectTimeout = 2000;
-};
diff --git a/src/GPS/NTRIP/CMakeLists.txt b/src/GPS/NTRIP/CMakeLists.txt
index e8a891d846f5..6d07f44b2ca0 100644
--- a/src/GPS/NTRIP/CMakeLists.txt
+++ b/src/GPS/NTRIP/CMakeLists.txt
@@ -5,10 +5,24 @@
target_sources(${CMAKE_PROJECT_NAME}
PRIVATE
+ NTRIPError.h
+ NTRIPTransport.h
+ NTRIPConnectionStats.cc
+ NTRIPConnectionStats.h
+ NTRIPGgaProvider.cc
+ NTRIPGgaProvider.h
NTRIPHttpTransport.cc
NTRIPHttpTransport.h
+ NTRIPTransportConfig.cc
+ NTRIPTransportConfig.h
NTRIPManager.cc
NTRIPManager.h
+ NTRIPReconnectPolicy.cc
+ NTRIPReconnectPolicy.h
+ NTRIPSourceTableController.cc
+ NTRIPSourceTableController.h
+ NTRIPUdpForwarder.cc
+ NTRIPUdpForwarder.h
NTRIPSourceTable.cc
NTRIPSourceTable.h
RTCMParser.cc
@@ -16,3 +30,18 @@ target_sources(${CMAKE_PROJECT_NAME}
)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+
+# ----------------------------------------------------------------------------
+# NTRIP QML Module
+# ----------------------------------------------------------------------------
+qt_add_library(GPSNTRIPModule STATIC)
+
+qt_add_qml_module(GPSNTRIPModule
+ URI QGroundControl.GPS.NTRIP
+ VERSION 1.0
+ RESOURCE_PREFIX /qml
+ QML_FILES
+ NTRIPConnectionStatus.qml
+ NTRIPSettings.qml
+ NO_PLUGIN
+)
diff --git a/src/GPS/NTRIP/NTRIPConnectionStats.cc b/src/GPS/NTRIP/NTRIPConnectionStats.cc
new file mode 100644
index 000000000000..585b2822cdb4
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPConnectionStats.cc
@@ -0,0 +1,65 @@
+#include "NTRIPConnectionStats.h"
+
+#include
+
+NTRIPConnectionStats::NTRIPConnectionStats(QObject* parent)
+ : QObject(parent)
+{
+ _rateTimer.setInterval(1000);
+ connect(&_rateTimer, &QTimer::timeout, this, [this]() {
+ const quint64 prevBytes = _dataRatePrevBytes;
+ const double rate = static_cast(_bytesReceived - prevBytes);
+ _dataRatePrevBytes = _bytesReceived;
+ if (qAbs(_dataRateBytesPerSec - rate) > 1.0) {
+ _dataRateBytesPerSec = rate;
+ emit dataRateChanged();
+ }
+ if (_bytesReceived != prevBytes) {
+ emit bytesReceivedChanged();
+ }
+ if (_prevMessagesReceived != _messagesReceived) {
+ _prevMessagesReceived = _messagesReceived;
+ emit messagesReceivedChanged();
+ }
+ if (_lastMessageTime.isValid()) {
+ emit correctionAgeChanged();
+ }
+ });
+}
+
+void NTRIPConnectionStats::start()
+{
+ _rateTimer.start();
+}
+
+void NTRIPConnectionStats::stop()
+{
+ _rateTimer.stop();
+ if (_dataRateBytesPerSec != 0.0) {
+ _dataRateBytesPerSec = 0.0;
+ _dataRatePrevBytes = 0;
+ emit dataRateChanged();
+ }
+}
+
+void NTRIPConnectionStats::recordMessage(int bytes)
+{
+ if (bytes > 0) {
+ _bytesReceived += static_cast(bytes);
+ _messagesReceived++;
+ _lastMessageTime.restart();
+ }
+}
+
+void NTRIPConnectionStats::reset()
+{
+ _bytesReceived = 0;
+ _messagesReceived = 0;
+ _dataRateBytesPerSec = 0.0;
+ _dataRatePrevBytes = 0;
+ _lastMessageTime.invalidate();
+ emit bytesReceivedChanged();
+ emit messagesReceivedChanged();
+ emit dataRateChanged();
+ emit correctionAgeChanged();
+}
diff --git a/src/GPS/NTRIP/NTRIPConnectionStats.h b/src/GPS/NTRIP/NTRIPConnectionStats.h
new file mode 100644
index 000000000000..7d05520998cf
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPConnectionStats.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+class NTRIPConnectionStats : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+ Q_PROPERTY(quint64 bytesReceived READ bytesReceived NOTIFY bytesReceivedChanged)
+ Q_PROPERTY(quint32 messagesReceived READ messagesReceived NOTIFY messagesReceivedChanged)
+ Q_PROPERTY(double dataRateBytesPerSec READ dataRateBytesPerSec NOTIFY dataRateChanged)
+ Q_PROPERTY(double correctionAgeSec READ correctionAgeSec NOTIFY correctionAgeChanged)
+
+public:
+ explicit NTRIPConnectionStats(QObject* parent = nullptr);
+
+ void start();
+ void stop();
+ void recordMessage(int bytes);
+ void reset();
+
+ quint64 bytesReceived() const { return _bytesReceived; }
+ quint32 messagesReceived() const { return _messagesReceived; }
+ double dataRateBytesPerSec() const { return _dataRateBytesPerSec; }
+ double correctionAgeSec() const { return _lastMessageTime.isValid() ? _lastMessageTime.elapsed() / 1000.0 : -1.0; }
+
+signals:
+ void bytesReceivedChanged();
+ void messagesReceivedChanged();
+ void dataRateChanged();
+ void correctionAgeChanged();
+
+private:
+ quint64 _bytesReceived = 0;
+ quint32 _messagesReceived = 0;
+ quint32 _prevMessagesReceived = 0;
+ double _dataRateBytesPerSec = 0.0;
+ quint64 _dataRatePrevBytes = 0;
+ QElapsedTimer _lastMessageTime;
+ QTimer _rateTimer;
+};
diff --git a/src/GPS/NTRIP/NTRIPConnectionStatus.qml b/src/GPS/NTRIP/NTRIPConnectionStatus.qml
new file mode 100644
index 000000000000..64cd69b47f3f
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPConnectionStatus.qml
@@ -0,0 +1,134 @@
+import QtQuick
+import QtQuick.Layouts
+
+import QGroundControl
+import QGroundControl.Controls
+import QGroundControl.FactControls
+import "qrc:/qml/QGroundControl/GPS/GPSFormatHelpers.js" as GPSHelpers
+
+SettingsGroupLayout {
+ id: root
+
+ property var rtcmMavlink: null
+
+ property var _ntripMgr: QGroundControl.ntripManager
+ property int _lastMsgCount: 0
+ property bool _dataStale: false
+ property string _valueNA: qsTr("-.--")
+
+ heading: qsTr("NTRIP Corrections")
+ showDividers: true
+ visible: _ntripMgr ? _ntripMgr.connectionStatus !== NTRIPManager.Disconnected : false
+
+ function _formatDataSize(bytes) { return GPSHelpers.formatDataSize(bytes) }
+ function _formatDataRate(bytesPerSec) { return GPSHelpers.formatDataRate(bytesPerSec) }
+
+ QGCPalette { id: qgcPal }
+
+ Timer {
+ interval: 5000
+ running: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ repeat: true
+ onTriggered: {
+ if (_ntripMgr.connectionStats.messagesReceived === _lastMsgCount && _ntripMgr.connectionStats.messagesReceived > 0) {
+ _dataStale = true
+ } else {
+ _dataStale = false
+ }
+ _lastMsgCount = _ntripMgr.connectionStats.messagesReceived
+ }
+ }
+
+ RowLayout {
+ spacing: ScreenTools.defaultFontPixelWidth
+
+ Rectangle {
+ width: ScreenTools.defaultFontPixelHeight * 0.6
+ height: width
+ radius: width / 2
+ color: {
+ if (_dataStale) return qgcPal.colorOrange
+ switch (_ntripMgr.connectionStatus) {
+ case NTRIPManager.Connected: return qgcPal.colorGreen
+ case NTRIPManager.Connecting:
+ case NTRIPManager.Reconnecting: return qgcPal.colorOrange
+ case NTRIPManager.Error: return qgcPal.colorRed
+ default: return qgcPal.colorGrey
+ }
+ }
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ QGCLabel {
+ text: {
+ if (_dataStale && _ntripMgr.connectionStatus === NTRIPManager.Connected)
+ return qsTr("No Data")
+ switch (_ntripMgr.connectionStatus) {
+ case NTRIPManager.Connected: return qsTr("Connected")
+ case NTRIPManager.Connecting: return qsTr("Connecting")
+ case NTRIPManager.Reconnecting: return qsTr("Reconnecting")
+ case NTRIPManager.Error: return qsTr("Error")
+ default: return qsTr("Disconnected")
+ }
+ }
+ }
+ }
+
+ LabelledLabel {
+ label: qsTr("Mountpoint")
+ labelText: QGroundControl.settingsManager.ntripSettings.ntripMountpoint.valueString
+ visible: QGroundControl.settingsManager.ntripSettings.ntripMountpoint.valueString !== ""
+ && _ntripMgr.connectionStatus === NTRIPManager.Connected
+ }
+
+ LabelledLabel {
+ label: qsTr("Messages")
+ labelText: _ntripMgr.connectionStats.messagesReceived
+ visible: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ && _ntripMgr.connectionStats.messagesReceived > 0
+ }
+
+ LabelledLabel {
+ label: qsTr("Data Received")
+ labelText: _formatDataSize(_ntripMgr.connectionStats.bytesReceived) + " ("
+ + _formatDataRate(_ntripMgr.connectionStats.dataRateBytesPerSec) + ")"
+ visible: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ && _ntripMgr.connectionStats.bytesReceived > 0
+ }
+
+ QGCLabel {
+ readonly property real _dataLimitBytes: 50 * 1024 * 1024 // 50 MB
+ text: qsTr("⚠ Data usage: %1 — consider connection costs").arg(_formatDataSize(_ntripMgr.connectionStats.bytesReceived))
+ wrapMode: Text.WordWrap
+ color: qgcPal.colorOrange
+ font.pointSize: ScreenTools.smallFontPointSize
+ visible: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ && _ntripMgr.connectionStats.bytesReceived > _dataLimitBytes
+ Layout.fillWidth: true
+ }
+
+ LabelledLabel {
+ label: qsTr("To Vehicle")
+ labelText: rtcmMavlink ? (_formatDataSize(rtcmMavlink.totalBytesSent) + " ("
+ + rtcmMavlink.bandwidthKBps.toFixed(1) + " KB/s)") : _valueNA
+ visible: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ && rtcmMavlink && rtcmMavlink.totalBytesSent > 0
+ }
+
+ LabelledLabel {
+ label: qsTr("GGA Source")
+ labelText: _ntripMgr.ggaSource
+ visible: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ && _ntripMgr.ggaSource
+ }
+
+ QGCLabel {
+ text: _ntripMgr.statusMessage
+ wrapMode: Text.WordWrap
+ color: qgcPal.colorRed
+ font.pointSize: ScreenTools.smallFontPointSize
+ visible: _ntripMgr.connectionStatus === NTRIPManager.Error
+ && _ntripMgr.statusMessage !== ""
+ Layout.fillWidth: true
+ }
+}
diff --git a/src/GPS/NTRIP/NTRIPError.h b/src/GPS/NTRIP/NTRIPError.h
new file mode 100644
index 000000000000..b71d697063aa
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPError.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include
+
+enum class NTRIPError {
+ ConnectionTimeout,
+ DataWatchdog,
+ AuthFailed,
+ SocketError,
+ SslError,
+ ServerDisconnected,
+ InvalidHttpResponse,
+ HttpError,
+ HeaderTooLarge,
+ InvalidMountpoint,
+ NoLocation,
+ InvalidConfig,
+ Unknown
+};
+Q_DECLARE_METATYPE(NTRIPError)
diff --git a/src/GPS/NTRIP/NTRIPGgaProvider.cc b/src/GPS/NTRIP/NTRIPGgaProvider.cc
new file mode 100644
index 000000000000..adc131695611
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPGgaProvider.cc
@@ -0,0 +1,263 @@
+#include "NTRIPGgaProvider.h"
+#include "NTRIPTransport.h"
+#include "NTRIPSettings.h"
+#include "Fact.h"
+#include "FactGroup.h"
+#include "GPSManager.h"
+#include "MultiVehicleManager.h"
+#include "PositionManager.h"
+#include "SettingsManager.h"
+#include "Vehicle.h"
+
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(NTRIPManagerLog)
+
+namespace {
+
+QPair getVehicleGPSPosition()
+{
+ MultiVehicleManager* mvm = MultiVehicleManager::instance();
+ if (!mvm) return {};
+ Vehicle* veh = mvm->activeVehicle();
+ if (!veh) return {};
+
+ FactGroup* gps = veh->gpsFactGroup();
+ if (!gps) return {};
+
+ Fact* latF = gps->getFact(QStringLiteral("lat"));
+ Fact* lonF = gps->getFact(QStringLiteral("lon"));
+ if (!latF || !lonF) return {};
+
+ const double lat = latF->rawValue().toDouble();
+ const double lon = lonF->rawValue().toDouble();
+
+ if (qIsFinite(lat) && qIsFinite(lon) &&
+ !(lat == 0.0 && lon == 0.0) &&
+ qAbs(lat) <= 90.0 && qAbs(lon) <= 180.0) {
+ return {QGeoCoordinate(lat, lon, veh->coordinate().altitude()), QStringLiteral("Vehicle GPS")};
+ }
+ return {};
+}
+
+QPair getVehicleEKFPosition()
+{
+ MultiVehicleManager* mvm = MultiVehicleManager::instance();
+ if (!mvm) return {};
+ Vehicle* veh = mvm->activeVehicle();
+ if (!veh) return {};
+
+ QGeoCoordinate coord = veh->coordinate();
+ if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) {
+ return {coord, QStringLiteral("Vehicle EKF")};
+ }
+ return {};
+}
+
+QPair getRTKBasePosition()
+{
+ if (!GPSManager::instance()->connected()) {
+ return {};
+ }
+
+ FactGroup* rtkFg = GPSManager::instance()->gpsRtkFactGroup();
+ if (!rtkFg) return {};
+
+ Fact* latF = rtkFg->getFact(QStringLiteral("baseLatitude"));
+ Fact* lonF = rtkFg->getFact(QStringLiteral("baseLongitude"));
+ Fact* altF = rtkFg->getFact(QStringLiteral("baseAltitude"));
+ Fact* fixF = rtkFg->getFact(QStringLiteral("baseFixType"));
+ if (!latF || !lonF || !fixF) return {};
+
+ const double lat = latF->rawValue().toDouble();
+ const double lon = lonF->rawValue().toDouble();
+ const double alt = altF ? altF->rawValue().toDouble() : 0.0;
+ const int fixType = fixF->rawValue().toInt();
+
+ if (qIsFinite(lat) && qIsFinite(lon) &&
+ (fixType > 0) &&
+ !(lat == 0.0 && lon == 0.0) &&
+ qAbs(lat) <= 90.0 && qAbs(lon) <= 180.0) {
+ return {QGeoCoordinate(lat, lon, alt), QStringLiteral("RTK Base")};
+ }
+ return {};
+}
+
+QPair getGCSPosition()
+{
+ QGCPositionManager* posMgr = QGCPositionManager::instance();
+ if (!posMgr) return {};
+
+ QGeoCoordinate coord = posMgr->gcsPosition();
+ if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) {
+ return {coord, QStringLiteral("GCS Position")};
+ }
+ return {};
+}
+
+} // anonymous namespace
+
+NTRIPGgaProvider::NTRIPGgaProvider(QObject* parent)
+ : QObject(parent)
+{
+ _timer.setInterval(5000);
+ connect(&_timer, &QTimer::timeout, this, &NTRIPGgaProvider::_sendGGA);
+
+ // Register default providers (can be overridden via setPositionProvider)
+ _providers[PositionSource::VehicleGPS] = &getVehicleGPSPosition;
+ _providers[PositionSource::VehicleEKF] = &getVehicleEKFPosition;
+ _providers[PositionSource::RTKBase] = &getRTKBasePosition;
+ _providers[PositionSource::GCSPosition] = &getGCSPosition;
+}
+
+void NTRIPGgaProvider::setPositionProvider(PositionSource source, PositionProvider provider)
+{
+ _providers[source] = std::move(provider);
+}
+
+void NTRIPGgaProvider::start(NTRIPTransport* transport)
+{
+ _transport = transport;
+ _fastRetryCount = 0;
+ _timer.setInterval(1000);
+ _sendGGA();
+ _timer.start();
+}
+
+void NTRIPGgaProvider::stop()
+{
+ _timer.stop();
+ _transport = nullptr;
+}
+
+void NTRIPGgaProvider::_sendGGA()
+{
+ if (!_transport) {
+ return;
+ }
+
+ const auto [coord, srcUsed] = _getBestPosition();
+
+ if (!coord.isValid()) {
+ if (++_fastRetryCount >= 5 && _timer.interval() != 5000) {
+ _timer.setInterval(5000);
+ }
+ return;
+ }
+
+ _fastRetryCount = 0;
+ if (_timer.interval() != 5000) {
+ _timer.setInterval(5000);
+ }
+
+ double alt_msl = coord.altitude();
+ if (!qIsFinite(alt_msl)) {
+ alt_msl = 0.0;
+ }
+
+ const QByteArray gga = makeGGA(coord, alt_msl);
+ _transport->sendNMEA(gga);
+
+ if (!srcUsed.isEmpty() && srcUsed != _source) {
+ _source = srcUsed;
+ emit sourceChanged(_source);
+ }
+}
+
+QByteArray NTRIPGgaProvider::makeGGA(const QGeoCoordinate& coord, double altitude_msl, int fixQuality, int numSatellites)
+{
+ const QTime utc = QDateTime::currentDateTimeUtc().time();
+ QByteArray hhmmss;
+ hhmmss += QByteArray::number(utc.hour()).rightJustified(2, '0');
+ hhmmss += QByteArray::number(utc.minute()).rightJustified(2, '0');
+ hhmmss += QByteArray::number(utc.second()).rightJustified(2, '0');
+
+ auto dmm = [](double deg, bool lat) -> QByteArray {
+ const double a = qFabs(deg);
+ int d = static_cast(a);
+ double m = (a - d) * 60.0;
+
+ const int m10000 = static_cast(m * 10000.0 + 0.5);
+ double m_rounded = m10000 / 10000.0;
+ if (m_rounded >= 60.0) {
+ m_rounded -= 60.0;
+ d += 1;
+ }
+
+ QByteArray mm = QByteArray::number(m_rounded, 'f', 4);
+ if (m_rounded < 10.0) {
+ mm.prepend('0');
+ }
+
+ const int dWidth = lat ? 2 : 3;
+ return QByteArray::number(d).rightJustified(dWidth, '0') + mm;
+ };
+
+ const bool latNorth = coord.latitude() >= 0.0;
+ const bool lonEast = coord.longitude() >= 0.0;
+
+ const QByteArray latField = dmm(coord.latitude(), true);
+ const QByteArray lonField = dmm(coord.longitude(), false);
+
+ QByteArray core;
+ core += "GPGGA,";
+ core += hhmmss + ',';
+ core += latField + ',';
+ core += (latNorth ? "N" : "S");
+ core += ',';
+ core += lonField + ',';
+ core += (lonEast ? "E" : "W");
+ core += ',' + QByteArray::number(fixQuality) + ',' + QByteArray::number(numSatellites) + ",1.0,";
+ core += QByteArray::number(altitude_msl, 'f', 1);
+ core += ",M,0.0,M,,";
+
+ quint8 cksum = 0;
+ for (char ch : core) {
+ cksum ^= static_cast(ch);
+ }
+
+ QByteArray sentence;
+ sentence += '$';
+ sentence += core;
+ sentence += '*';
+ sentence += QByteArray::number(cksum, 16).rightJustified(2, '0').toUpper();
+ return sentence;
+}
+
+QPair NTRIPGgaProvider::_getBestPosition() const
+{
+ NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
+ const auto source = settings
+ ? static_cast(settings->ntripGgaPositionSource()->rawValue().toUInt())
+ : PositionSource::Auto;
+
+ // If a specific source is requested, try only that one
+ if (source != PositionSource::Auto) {
+ auto it = _providers.find(source);
+ if (it != _providers.end()) {
+ return it.value()();
+ }
+ return {};
+ }
+
+ // Auto: try each provider in priority order
+ static constexpr PositionSource kPriority[] = {
+ PositionSource::VehicleGPS,
+ PositionSource::VehicleEKF,
+ PositionSource::RTKBase,
+ PositionSource::GCSPosition,
+ };
+
+ for (PositionSource s : kPriority) {
+ auto it = _providers.find(s);
+ if (it != _providers.end()) {
+ auto result = it.value()();
+ if (result.first.isValid()) {
+ return result;
+ }
+ }
+ }
+
+ return {};
+}
diff --git a/src/GPS/NTRIP/NTRIPGgaProvider.h b/src/GPS/NTRIP/NTRIPGgaProvider.h
new file mode 100644
index 000000000000..7bb8d7682c7c
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPGgaProvider.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class Fact;
+class FactGroup;
+class NTRIPTransport;
+
+class NTRIPGgaProvider : public QObject
+{
+ Q_OBJECT
+
+public:
+ enum class PositionSource {
+ Auto = 0,
+ VehicleGPS = 1,
+ VehicleEKF = 2,
+ RTKBase = 3,
+ GCSPosition = 4
+ };
+ Q_ENUM(PositionSource)
+
+ using PositionProvider = std::function()>;
+
+ explicit NTRIPGgaProvider(QObject* parent = nullptr);
+
+ void start(NTRIPTransport* transport);
+ void stop();
+
+ QString currentSource() const { return _source; }
+
+ void setPositionProvider(PositionSource source, PositionProvider provider);
+
+ static QByteArray makeGGA(const QGeoCoordinate& coord, double altitude_msl, int fixQuality = 1, int numSatellites = 12);
+
+signals:
+ void sourceChanged(const QString& source);
+
+private:
+ void _sendGGA();
+
+ QPair _getBestPosition() const;
+
+ QPointer _transport;
+ QTimer _timer;
+ QString _source;
+ QHash _providers;
+ int _fastRetryCount = 0;
+};
diff --git a/src/GPS/NTRIP/NTRIPHttpTransport.cc b/src/GPS/NTRIP/NTRIPHttpTransport.cc
index 5a202dba1542..12a2405c797b 100644
--- a/src/GPS/NTRIP/NTRIPHttpTransport.cc
+++ b/src/GPS/NTRIP/NTRIPHttpTransport.cc
@@ -1,4 +1,7 @@
#include "NTRIPHttpTransport.h"
+#include "NTRIPError.h"
+#include "NTRIPTransportConfig.h"
+#include "NTRIPSettings.h"
#include "QGCLoggingCategory.h"
#include
@@ -9,21 +12,18 @@
QGC_LOGGING_CATEGORY(NTRIPHttpTransportLog, "GPS.NTRIPHttpTransport")
NTRIPHttpTransport::NTRIPHttpTransport(const NTRIPTransportConfig& config, QObject* parent)
- : QObject(parent)
- , _hostAddress(config.host)
- , _port(config.port)
- , _username(config.username)
- , _password(config.password)
- , _mountpoint(config.mountpoint)
- , _useTls(config.useTls)
+ : NTRIPTransport(parent)
+ , _config(config)
{
- for (const auto& msg : config.whitelist.split(',')) {
+ QVector whitelist;
+ for (const auto& msg : _config.whitelist.split(',')) {
int msg_int = msg.toInt();
if (msg_int)
- _whitelist.append(msg_int);
+ whitelist.append(msg_int);
}
- qCDebug(NTRIPHttpTransportLog) << "RTCM message filter:" << _whitelist;
- if (_whitelist.empty()) {
+ _rtcmParser.setWhitelist(whitelist);
+ qCDebug(NTRIPHttpTransportLog) << "RTCM message filter:" << whitelist;
+ if (whitelist.empty()) {
qCDebug(NTRIPHttpTransportLog) << "Message filter empty; all RTCM message IDs will be forwarded.";
}
@@ -31,7 +31,7 @@ NTRIPHttpTransport::NTRIPHttpTransport(const NTRIPTransportConfig& config, QObje
_connectTimeoutTimer->setSingleShot(true);
connect(_connectTimeoutTimer, &QTimer::timeout, this, [this]() {
qCWarning(NTRIPHttpTransportLog) << "Connection timeout";
- emit error(QStringLiteral("Connection timeout"));
+ emit error(NTRIPError::ConnectionTimeout, QStringLiteral("Connection timeout"));
});
_dataWatchdogTimer = new QTimer(this);
@@ -39,7 +39,7 @@ NTRIPHttpTransport::NTRIPHttpTransport(const NTRIPTransportConfig& config, QObje
_dataWatchdogTimer->setInterval(kDataWatchdogMs);
connect(_dataWatchdogTimer, &QTimer::timeout, this, [this]() {
qCWarning(NTRIPHttpTransportLog) << "No data received for" << kDataWatchdogMs / 1000 << "seconds";
- emit error(tr("No data received for %1 seconds").arg(kDataWatchdogMs / 1000));
+ emit error(NTRIPError::DataWatchdog, tr("No data received for %1 seconds").arg(kDataWatchdogMs / 1000));
});
}
@@ -77,23 +77,31 @@ void NTRIPHttpTransport::_sendHttpRequest()
return;
}
- if (!_mountpoint.isEmpty()) {
+ if (!_config.mountpoint.isEmpty()) {
static const QRegularExpression controlChars(QStringLiteral("[\\r\\n\\x00-\\x1f]"));
- if (_mountpoint.contains(controlChars)) {
+ if (_config.mountpoint.contains(controlChars)) {
qCWarning(NTRIPHttpTransportLog) << "Mountpoint contains control characters, rejecting";
- emit error(tr("Invalid mountpoint name (contains control characters)"));
+ emit error(NTRIPError::InvalidMountpoint, tr("Invalid mountpoint name (contains control characters)"));
+ return;
+ }
+ if (_config.host.contains(controlChars)) {
+ qCWarning(NTRIPHttpTransportLog) << "Host contains control characters, rejecting";
+ emit error(NTRIPError::InvalidConfig, tr("Invalid host (contains control characters)"));
return;
}
qCDebug(NTRIPHttpTransportLog) << "Sending HTTP request";
QByteArray req;
- req += "GET /" + _mountpoint.toUtf8() + " HTTP/1.0\r\n";
- req += "Host: " + _hostAddress.toUtf8() + "\r\n";
+ req += "GET /" + _config.mountpoint.toUtf8() + " HTTP/1.1\r\n";
+ req += "Host: " + _config.host.toUtf8() + "\r\n";
req += "Ntrip-Version: Ntrip/2.0\r\n";
req += "User-Agent: NTRIP QGroundControl/1.0\r\n";
- if (!_username.isEmpty() || !_password.isEmpty()) {
- const QByteArray authB64 = (_username + ":" + _password).toUtf8().toBase64();
+ if (!_config.username.isEmpty() || !_config.password.isEmpty()) {
+ if (!_config.useTls) {
+ qCWarning(NTRIPHttpTransportLog) << "Sending credentials without TLS — data is not encrypted";
+ }
+ const QByteArray authB64 = (_config.username + ":" + _config.password).toUtf8().toBase64();
req += "Authorization: Basic " + authB64 + "\r\n";
}
@@ -101,11 +109,11 @@ void NTRIPHttpTransport::_sendHttpRequest()
_socket->write(req);
_socket->flush();
- qCDebug(NTRIPHttpTransportLog) << "HTTP request sent for mount:" << _mountpoint;
+ qCDebug(NTRIPHttpTransportLog) << "HTTP request sent for mount:" << _config.mountpoint;
} else {
+ qCWarning(NTRIPHttpTransportLog) << "No mountpoint configured, connecting without RTCM stream request";
_httpHandshakeDone = true;
emit connected();
- _dataWatchdogTimer->start();
}
qCDebug(NTRIPHttpTransportLog) << "Socket connected"
@@ -124,20 +132,25 @@ void NTRIPHttpTransport::_connect()
return;
}
- qCDebug(NTRIPHttpTransportLog) << "connectToHost" << _hostAddress << ":" << _port << " mount=" << _mountpoint;
+ qCDebug(NTRIPHttpTransportLog) << "connectToHost" << _config.host << ":" << _config.port << " mount=" << _config.mountpoint;
_httpHandshakeDone = false;
_httpResponseBuf.clear();
_rtcmParser.reset();
- if (_useTls) {
+ if (_config.useTls) {
QSslSocket* sslSocket = new QSslSocket(this);
_socket = sslSocket;
- connect(sslSocket, QOverload&>::of(&QSslSocket::sslErrors),
- this, [](const QList& errors) {
+ connect(sslSocket, &QSslSocket::sslErrors,
+ this, [this](const QList& errors) {
+ QStringList msgs;
for (const QSslError& e : errors) {
qCWarning(NTRIPHttpTransportLog) << "TLS error:" << e.errorString();
+ msgs.append(e.errorString());
}
+ // All errors in the sslErrors signal are actual errors; emit and abort
+ emit error(NTRIPError::SslError, msgs.join(QStringLiteral("; ")));
+ _socket->abort();
});
} else {
_socket = new QTcpSocket(this);
@@ -154,13 +167,13 @@ void NTRIPHttpTransport::_connect()
QString msg = _socket->errorString();
if (code == QAbstractSocket::RemoteHostClosedError && !_httpHandshakeDone) {
- if (!_mountpoint.isEmpty()) {
+ if (!_config.mountpoint.isEmpty()) {
msg += " (peer closed before HTTP response; check mountpoint and credentials)";
}
}
qCWarning(NTRIPHttpTransportLog) << "Socket error code:" << int(code) << " msg:" << msg;
- emit error(msg);
+ emit error(NTRIPError::SocketError, msg);
});
connect(_socket, &QTcpSocket::disconnected, this, [this]() {
@@ -180,24 +193,24 @@ void NTRIPHttpTransport::_connect()
qCWarning(NTRIPHttpTransportLog) << "Disconnected:"
<< "reason=" << reason
<< "ms_since_200=" << (_postOkTimestampMs > 0 ? QDateTime::currentMSecsSinceEpoch() - _postOkTimestampMs : -1);
- emit error(reason);
+ emit error(NTRIPError::ServerDisconnected, reason);
});
connect(_socket, &QTcpSocket::readyRead, this, &NTRIPHttpTransport::_readBytes);
- if (_useTls) {
+ if (_config.useTls) {
QSslSocket* sslSocket = qobject_cast(_socket);
connect(sslSocket, &QSslSocket::encrypted, this, [this]() {
_connectTimeoutTimer->stop();
_sendHttpRequest();
});
- sslSocket->connectToHostEncrypted(_hostAddress, static_cast(_port));
+ sslSocket->connectToHostEncrypted(_config.host, static_cast(_config.port));
} else {
connect(_socket, &QTcpSocket::connected, this, [this]() {
_connectTimeoutTimer->stop();
_sendHttpRequest();
});
- _socket->connectToHost(_hostAddress, static_cast(_port));
+ _socket->connectToHost(_config.host, static_cast(_config.port));
}
_connectTimeoutTimer->start(kConnectTimeoutMs);
}
@@ -231,7 +244,7 @@ void NTRIPHttpTransport::_parseRtcm(const QByteArray& buffer)
const uint16_t id = _rtcmParser.messageId();
- if (_whitelist.empty() || _whitelist.contains(id)) {
+ if (_rtcmParser.isWhitelisted(id)) {
qCDebug(NTRIPHttpTransportLog) << "RTCM packet id" << id << "len" << message.length();
emit RTCMDataUpdate(message);
} else {
@@ -259,7 +272,7 @@ void NTRIPHttpTransport::_readBytes()
if (_httpResponseBuf.size() > kMaxHttpHeaderSize) {
qCWarning(NTRIPHttpTransportLog) << "HTTP response header too large, dropping";
_httpResponseBuf.clear();
- emit error(tr("HTTP response header too large"));
+ emit error(NTRIPError::HeaderTooLarge, tr("HTTP response header too large"));
}
return;
}
@@ -299,7 +312,7 @@ void NTRIPHttpTransport::_readBytes()
if (status.code == 401) {
qCWarning(NTRIPHttpTransportLog) << "Authentication failed:" << status.reason;
- emit error(tr("Authentication failed (401): check username and password"));
+ emit error(NTRIPError::AuthFailed, tr("Authentication failed (401): check username and password"));
return;
}
@@ -317,14 +330,14 @@ void NTRIPHttpTransport::_readBytes()
msg += QStringLiteral(" — ") + cleanBody;
}
}
- emit error(msg);
+ emit error(NTRIPError::HttpError, msg);
return;
}
qCWarning(NTRIPHttpTransportLog) << "No HTTP status line found in response. First line:"
<< (lines.isEmpty() ? QStringLiteral("(empty)") : lines.first().left(120));
_httpResponseBuf.clear();
- emit error(tr("Invalid HTTP response from caster"));
+ emit error(NTRIPError::InvalidHttpResponse, tr("Invalid HTTP response from caster"));
return;
}
diff --git a/src/GPS/NTRIP/NTRIPHttpTransport.h b/src/GPS/NTRIP/NTRIPHttpTransport.h
index 522d387c9467..e9971d87782f 100644
--- a/src/GPS/NTRIP/NTRIPHttpTransport.h
+++ b/src/GPS/NTRIP/NTRIPHttpTransport.h
@@ -1,34 +1,17 @@
#pragma once
+#include "NTRIPTransportConfig.h"
+#include "NTRIPTransport.h"
+#include "RTCMParser.h"
+
#include
-#include
#include
#include
#include
-#include "RTCMParser.h"
-
Q_DECLARE_LOGGING_CATEGORY(NTRIPHttpTransportLog)
-struct NTRIPTransportConfig {
- QString host;
- int port = 2101;
- QString username;
- QString password;
- QString mountpoint;
- QString whitelist;
- bool useTls = false;
-
- bool operator==(const NTRIPTransportConfig& other) const {
- return host == other.host && port == other.port &&
- username == other.username && password == other.password &&
- mountpoint == other.mountpoint && whitelist == other.whitelist &&
- useTls == other.useTls;
- }
- bool operator!=(const NTRIPTransportConfig& other) const { return !(*this == other); }
-};
-
-class NTRIPHttpTransport : public QObject
+class NTRIPHttpTransport : public NTRIPTransport
{
Q_OBJECT
friend class NTRIPHttpTransportTest;
@@ -41,15 +24,11 @@ class NTRIPHttpTransport : public QObject
explicit NTRIPHttpTransport(const NTRIPTransportConfig& config, QObject* parent = nullptr);
~NTRIPHttpTransport() override;
- void start();
- void stop();
- void sendNMEA(const QByteArray& nmea);
+ void start() override;
+ void stop() override;
+ void sendNMEA(const QByteArray& nmea) override;
-signals:
- void connected();
- void error(const QString& errorMsg);
- void RTCMDataUpdate(const QByteArray& message);
- void finished();
+ const NTRIPTransportConfig& config() const { return _config; }
protected:
struct HttpStatus {
@@ -68,20 +47,14 @@ class NTRIPHttpTransport : public QObject
void _readBytes();
void _parseRtcm(const QByteArray& buffer);
+ NTRIPTransportConfig _config;
+
QTcpSocket* _socket = nullptr;
QTimer* _connectTimeoutTimer = nullptr;
QTimer* _dataWatchdogTimer = nullptr;
- QString _hostAddress;
- int _port;
- QString _username;
- QString _password;
- QString _mountpoint;
- QVector _whitelist;
-
RTCMParser _rtcmParser;
bool _httpHandshakeDone = false;
- bool _useTls = false;
bool _stopped = false;
qint64 _postOkTimestampMs = 0;
diff --git a/src/GPS/NTRIP/NTRIPManager.cc b/src/GPS/NTRIP/NTRIPManager.cc
index 081d22cb1236..e86c9674261c 100644
--- a/src/GPS/NTRIP/NTRIPManager.cc
+++ b/src/GPS/NTRIP/NTRIPManager.cc
@@ -1,25 +1,15 @@
#include "NTRIPManager.h"
+#include "NTRIPError.h"
#include "NTRIPHttpTransport.h"
-#include "NTRIPSourceTable.h"
#include "NTRIPSettings.h"
-#include "QmlObjectListModel.h"
#include "Fact.h"
-#include "FactGroup.h"
-#include "QGCApplication.h"
+#include "GPSEvent.h"
#include "QGCLoggingCategory.h"
-#include "RTCMMavlink.h"
+#include "RTCMRouter.h"
#include "SettingsManager.h"
-#include "MultiVehicleManager.h"
-#include "PositionManager.h"
-#include "Vehicle.h"
-#include
-#include
-
#include
#include
-#include
-#include
QGC_LOGGING_CATEGORY(NTRIPManagerLog, "GPS.NTRIPManager")
@@ -35,18 +25,13 @@ NTRIPManager::NTRIPManager(QObject* parent)
{
qCDebug(NTRIPManagerLog) << "NTRIPManager created";
- _rtcmMavlink = qgcApp() ? qgcApp()->findChild() : nullptr;
- if (!_rtcmMavlink) {
- QObject* parentObj = qgcApp() ? static_cast(qgcApp()) : static_cast(this);
- _rtcmMavlink = new RTCMMavlink(parentObj);
- _rtcmMavlink->setObjectName(QStringLiteral("RTCMMavlink"));
- }
-
NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
if (settings) {
auto connectSetting = [this](Fact* fact) {
if (fact) {
- connect(fact, &Fact::rawValueChanged, this, &NTRIPManager::_onSettingChanged);
+ connect(fact, &Fact::rawValueChanged, this, [this]() {
+ _settingsDebounceTimer.start();
+ });
}
};
connectSetting(settings->ntripServerConnectEnabled());
@@ -62,24 +47,22 @@ NTRIPManager::NTRIPManager(QObject* parent)
connectSetting(settings->ntripUdpTargetPort());
}
- // Check initial state (handles connect-on-start)
+ _settingsDebounceTimer.setSingleShot(true);
+ _settingsDebounceTimer.setInterval(kSettingsDebounceMs);
+ connect(&_settingsDebounceTimer, &QTimer::timeout, this, &NTRIPManager::_onSettingChanged);
+
QTimer::singleShot(0, this, &NTRIPManager::_onSettingChanged);
- _ggaTimer = new QTimer(this);
- _ggaTimer->setInterval(5000);
- connect(_ggaTimer, &QTimer::timeout, this, &NTRIPManager::_sendGGA);
-
- _dataRateTimer = new QTimer(this);
- _dataRateTimer->setInterval(1000);
- connect(_dataRateTimer, &QTimer::timeout, this, [this]() {
- const double rate = static_cast(_bytesReceived - _dataRatePrevBytes);
- _dataRatePrevBytes = _bytesReceived;
- if (!qFuzzyCompare(_dataRateBytesPerSec + 1.0, rate + 1.0)) {
- _dataRateBytesPerSec = rate;
- emit dataRateChanged();
+ connect(&_ggaProvider, &NTRIPGgaProvider::sourceChanged, this, &NTRIPManager::ggaSourceChanged);
+ connect(&_reconnectPolicy, &NTRIPReconnectPolicy::reconnectRequested, this, [this]() {
+ if (!_transport) {
+ startNTRIP();
}
- emit bytesReceivedChanged();
- emit messagesReceivedChanged();
+ });
+
+ connect(&_reconnectPolicy, &NTRIPReconnectPolicy::gaveUp, this, [this]() {
+ _setStatus(ConnectionStatus::Error, tr("Gave up after %1 reconnect attempts").arg(NTRIPReconnectPolicy::kMaxReconnectAttempts));
+ _transition(OperationState::Starting, OperationState::Idle);
});
connect(qApp, &QCoreApplication::aboutToQuit, this, &NTRIPManager::stopNTRIP, Qt::QueuedConnection);
@@ -112,74 +95,76 @@ void NTRIPManager::_setStatus(ConnectionStatus status, const QString& msg)
}
}
-NTRIPTransportConfig NTRIPManager::_configFromSettings() const
+bool NTRIPManager::_transition(OperationState from, OperationState to)
{
- NTRIPTransportConfig config;
- NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
- if (!settings) {
- return config;
+ // Valid transitions: Idle→Starting, Starting→Running, Starting→Idle (config fail),
+ // Running→Stopping, Starting→Stopping, Stopping→Idle
+ static constexpr struct { OperationState from, to; } kValidTransitions[] = {
+ {OperationState::Idle, OperationState::Starting},
+ {OperationState::Starting, OperationState::Running},
+ {OperationState::Starting, OperationState::Idle},
+ {OperationState::Starting, OperationState::Stopping},
+ {OperationState::Running, OperationState::Stopping},
+ {OperationState::Stopping, OperationState::Idle},
+ };
+
+ if (_opState != from) {
+ qCCritical(NTRIPManagerLog) << "Invalid state transition: expected"
+ << static_cast(from) << "actual" << static_cast(_opState)
+ << "target" << static_cast(to);
+ return false;
}
- if (settings->ntripServerHostAddress()) config.host = settings->ntripServerHostAddress()->rawValue().toString();
- if (settings->ntripServerPort()) config.port = settings->ntripServerPort()->rawValue().toInt();
- if (settings->ntripUsername()) config.username = settings->ntripUsername()->rawValue().toString();
- if (settings->ntripPassword()) config.password = settings->ntripPassword()->rawValue().toString();
- if (settings->ntripMountpoint()) config.mountpoint = settings->ntripMountpoint()->rawValue().toString();
- if (settings->ntripWhitelist()) config.whitelist = settings->ntripWhitelist()->rawValue().toString();
- if (settings->ntripUseTls()) config.useTls = settings->ntripUseTls()->rawValue().toBool();
+ for (const auto &t : kValidTransitions) {
+ if (t.from == from && t.to == to) {
+ _opState = to;
+ return true;
+ }
+ }
- return config;
+ qCCritical(NTRIPManagerLog) << "Illegal state transition:"
+ << static_cast(from) << "→" << static_cast(to);
+ return false;
}
-void NTRIPManager::startNTRIP()
+void NTRIPManager::_logEvent(const GPSEvent &event)
{
- if (_startStopBusy || _transport) {
- if (_startStopBusy) {
- qCWarning(NTRIPManagerLog) << "startNTRIP called while start/stop in progress";
- }
- return;
+ if (_eventLogger) {
+ _eventLogger(event);
}
- _startStopBusy = true;
+}
- if (_reconnectTimer) {
- _reconnectTimer->stop();
+void NTRIPManager::startNTRIP()
+{
+ if (!_transition(OperationState::Idle, OperationState::Starting)) {
+ return;
}
- NTRIPTransportConfig config = _configFromSettings();
+ _reconnectPolicy.cancel();
NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
- if (settings && settings->ntripUdpForwardEnabled()) {
- _udpForwardEnabled = settings->ntripUdpForwardEnabled()->rawValue().toBool();
+ if (!settings) {
+ _transition(OperationState::Starting, OperationState::Idle);
+ return;
}
- if (_udpForwardEnabled && settings) {
+ NTRIPTransportConfig config = NTRIPTransportConfig::fromSettings(*settings);
+
+ if (settings->ntripUdpForwardEnabled() && settings->ntripUdpForwardEnabled()->rawValue().toBool()) {
const QString udpAddr = settings->ntripUdpTargetAddress()->rawValue().toString();
const quint16 udpPort = static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt());
- const QHostAddress parsedAddr(udpAddr);
- if (!udpAddr.isEmpty() && !parsedAddr.isNull() && udpPort > 0) {
- _udpTargetAddress = parsedAddr;
- _udpTargetPort = udpPort;
- if (!_udpSocket) {
- _udpSocket = new QUdpSocket(this);
- }
- qCDebug(NTRIPManagerLog) << "NTRIP UDP forwarding enabled:" << udpAddr << ":" << udpPort;
- } else {
- qCWarning(NTRIPManagerLog) << "NTRIP UDP forwarding enabled but address/port invalid";
- _udpForwardEnabled = false;
+ if (!_udpForwarder.configure(udpAddr, udpPort)) {
+ qCWarning(NTRIPManagerLog) << "UDP forward config invalid:" << udpAddr << udpPort;
+ _logEvent(GPSEvent::warning(GPSEvent::Source::NTRIP,
+ tr("UDP forwarding disabled: invalid address or port")));
}
}
- if (config.host.isEmpty()) {
- qCWarning(NTRIPManagerLog) << "NTRIP host address is empty";
- _setStatus(ConnectionStatus::Error, tr("No host address"));
- _startStopBusy = false;
- return;
- }
-
- if (config.port <= 0 || config.port > 65535) {
- qCWarning(NTRIPManagerLog) << "NTRIP port is invalid:" << config.port;
- _setStatus(ConnectionStatus::Error, tr("Invalid port"));
- _startStopBusy = false;
+ if (!config.isValid()) {
+ qCWarning(NTRIPManagerLog) << "NTRIP config invalid: host=" << config.host << " port=" << config.port;
+ _setStatus(ConnectionStatus::Error,
+ config.host.isEmpty() ? tr("No host address") : tr("Invalid port"));
+ _transition(OperationState::Starting, OperationState::Idle);
return;
}
@@ -188,319 +173,157 @@ void NTRIPManager::startNTRIP()
_setStatus(ConnectionStatus::Connecting, tr("Connecting to %1:%2...").arg(config.host).arg(config.port));
- _bytesReceived = 0;
- _messagesReceived = 0;
- _dataRateBytesPerSec = 0.0;
- _dataRatePrevBytes = 0;
- emit bytesReceivedChanged();
- emit messagesReceivedChanged();
- emit dataRateChanged();
+ _stats.reset();
_runningConfig = config;
- _runningUdpForward = _udpForwardEnabled;
- _runningUdpAddr = _udpForwardEnabled ? _udpTargetAddress.toString() : QString();
- _runningUdpPort = _udpForwardEnabled ? _udpTargetPort : 0;
- _transport = new NTRIPHttpTransport(config, this);
+ if (_transportFactory) {
+ _transport = _transportFactory(config, this);
+ } else {
+ _transport = new NTRIPHttpTransport(config, this);
+ }
+
+ if (!_transport) {
+ qCCritical(NTRIPManagerLog) << "Transport factory returned null";
+ _setStatus(ConnectionStatus::Error, tr("Internal error: transport creation failed"));
+ _transition(OperationState::Starting, OperationState::Idle);
+ return;
+ }
- connect(_transport, &NTRIPHttpTransport::error,
- this, &NTRIPManager::_tcpError);
+ connect(_transport, &NTRIPTransport::error,
+ this, &NTRIPManager::_transportError);
- connect(_transport, &NTRIPHttpTransport::connected, this, [this]() {
- _reconnectAttempts = 0;
+ connect(_transport, &NTRIPTransport::connected, this, [this]() {
+ _transition(OperationState::Starting, OperationState::Running);
+ _reconnectPolicy.resetAttempts();
_casterStatus = CasterStatus::CasterConnected;
emit casterStatusChanged(_casterStatus);
_setStatus(ConnectionStatus::Connected, tr("Connected"));
+ _logEvent(GPSEvent::info(GPSEvent::Source::NTRIP, tr("Connected to caster")));
- if (_ggaTimer && !_ggaTimer->isActive()) {
- _ggaTimer->setInterval(1000);
- _sendGGA();
- _ggaTimer->start();
- }
- _dataRateTimer->start();
+ _ggaProvider.start(_transport);
+ _stats.start();
});
- connect(_transport, &NTRIPHttpTransport::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived);
+ connect(_transport, &NTRIPTransport::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived);
_transport->start();
qCDebug(NTRIPManagerLog) << "NTRIP started";
-
- _startStopBusy = false;
+ // State stays Starting until the connected signal fires (see lambda above)
}
void NTRIPManager::stopNTRIP()
{
- if (_startStopBusy) {
- qCWarning(NTRIPManagerLog) << "stopNTRIP called while start/stop in progress";
+ if (_opState == OperationState::Stopping) {
return;
}
- _startStopBusy = true;
-
- if (_reconnectTimer) {
- _reconnectTimer->stop();
+ if (_opState == OperationState::Idle) {
+ if (_reconnectPolicy.isPending()) {
+ _reconnectPolicy.cancel();
+ _setStatus(ConnectionStatus::Disconnected, tr("Disconnected"));
+ }
+ return;
}
+ _opState = OperationState::Stopping; // direct assign: validated by guards above
+
+ _reconnectPolicy.cancel();
if (_transport) {
- disconnect(_transport, &NTRIPHttpTransport::error, this, &NTRIPManager::_tcpError);
+ _transport->disconnect(this);
_transport->stop();
_transport->deleteLater();
_transport = nullptr;
_runningConfig = {};
- _runningUdpForward = false;
- _runningUdpAddr.clear();
- _runningUdpPort = 0;
_setStatus(ConnectionStatus::Disconnected, tr("Disconnected"));
qCDebug(NTRIPManagerLog) << "NTRIP stopped";
}
- if (_ggaTimer && _ggaTimer->isActive()) {
- _ggaTimer->stop();
- }
- _dataRateTimer->stop();
- if (_dataRateBytesPerSec != 0.0) {
- _dataRateBytesPerSec = 0.0;
- _dataRatePrevBytes = 0;
- emit dataRateChanged();
- }
-
- if (_udpSocket) {
- _udpSocket->close();
- delete _udpSocket;
- _udpSocket = nullptr;
- }
- _udpForwardEnabled = false;
+ _ggaProvider.stop();
+ _stats.stop();
+ _udpForwarder.stop();
- _startStopBusy = false;
+ _transition(OperationState::Stopping, OperationState::Idle);
}
-void NTRIPManager::_tcpError(const QString& errorMsg)
+void NTRIPManager::_transportError(NTRIPError code, const QString& detail)
{
- qCWarning(NTRIPManagerLog) << "NTRIP error:" << errorMsg;
- _setStatus(ConnectionStatus::Error, errorMsg);
+ qCWarning(NTRIPManagerLog) << "NTRIP error:" << static_cast(code) << detail;
+ _logEvent(GPSEvent::error(GPSEvent::Source::NTRIP, detail));
+ _setStatus(ConnectionStatus::Error, detail);
- if (errorMsg.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) {
+ if (code == NTRIPError::NoLocation) {
_casterStatus = CasterStatus::CasterNoLocation;
} else {
_casterStatus = CasterStatus::CasterError;
}
emit casterStatusChanged(_casterStatus);
- _scheduleReconnect();
-}
-
-void NTRIPManager::_scheduleReconnect()
-{
NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
- bool shouldRun = settings && settings->ntripServerConnectEnabled() &&
- settings->ntripServerConnectEnabled()->rawValue().toBool();
- if (!shouldRun) {
- return;
- }
+ const bool shouldRun = settings && settings->ntripServerConnectEnabled() &&
+ settings->ntripServerConnectEnabled()->rawValue().toBool();
+ // Tear down the current transport before deciding whether to reconnect
stopNTRIP();
- int backoffMs = qMin(kMinReconnectMs * (1 << qMin(_reconnectAttempts, 4)), kMaxReconnectMs);
- _reconnectAttempts = qMin(_reconnectAttempts + 1, kMaxReconnectAttempts);
-
- qCDebug(NTRIPManagerLog) << "NTRIP reconnecting in" << backoffMs << "ms (attempt" << _reconnectAttempts << ")";
-
- _setStatus(ConnectionStatus::Reconnecting, tr("Reconnecting in %1s...").arg(backoffMs / 1000));
-
- if (!_reconnectTimer) {
- _reconnectTimer = new QTimer(this);
- _reconnectTimer->setSingleShot(true);
- connect(_reconnectTimer, &QTimer::timeout, this, [this]() {
- if (!_transport) {
- startNTRIP();
- }
- });
+ if (!shouldRun) {
+ return;
}
- _reconnectTimer->start(backoffMs);
+ const int backoffMs = _reconnectPolicy.nextBackoffMs();
+ qCDebug(NTRIPManagerLog) << "NTRIP reconnecting in" << backoffMs << "ms (attempt"
+ << (_reconnectPolicy.attempts() + 1) << ")";
+ _setStatus(ConnectionStatus::Reconnecting, tr("Reconnecting in %1s...").arg(backoffMs / 1000));
+ _reconnectPolicy.scheduleReconnect();
}
void NTRIPManager::_rtcmDataReceived(const QByteArray& data)
{
- _bytesReceived += static_cast(data.size());
- _messagesReceived++;
+ _stats.recordMessage(data.size());
- qCDebug(NTRIPManagerLog) << "NTRIP Forwarding RTCM to vehicle:" << data.size() << "bytes";
+ qCDebug(NTRIPManagerLog) << "NTRIP Forwarding RTCM:" << data.size() << "bytes";
- if (!_rtcmMavlink && qgcApp()) {
- _rtcmMavlink = qgcApp()->findChild();
- }
-
- if (_rtcmMavlink) {
- _rtcmMavlink->RTCMDataUpdate(data);
+ RTCMRouter *router = _rtcmRouter;
+ if (router) {
+ router->routeAll(data);
if (_connectionStatus != ConnectionStatus::Connected) {
_setStatus(ConnectionStatus::Connected, tr("Connected"));
}
} else {
- qCWarning(NTRIPManagerLog) << "RTCMMavlink not ready; dropping" << data.size() << "bytes";
+ qCWarning(NTRIPManagerLog) << "RTCMRouter not ready; dropping" << data.size() << "bytes";
}
- if (_udpForwardEnabled && _udpSocket && _udpTargetPort > 0) {
- const qint64 sent = _udpSocket->writeDatagram(data, _udpTargetAddress, _udpTargetPort);
- if (sent < 0) {
- qCWarning(NTRIPManagerLog) << "NTRIP UDP forward failed:" << _udpSocket->errorString();
- }
- }
-}
-
-QByteArray NTRIPManager::makeGGA(const QGeoCoordinate& coord, double altitude_msl)
-{
- const QTime utc = QDateTime::currentDateTimeUtc().time();
- const QString hhmmss = QString("%1%2%3")
- .arg(utc.hour(), 2, 10, QChar('0'))
- .arg(utc.minute(), 2, 10, QChar('0'))
- .arg(utc.second(), 2, 10, QChar('0'));
-
- auto dmm = [](double deg, bool lat) -> QString {
- double a = qFabs(deg);
- int d = int(a);
- double m = (a - d) * 60.0;
-
- int m10000 = int(m * 10000.0 + 0.5);
- double m_rounded = m10000 / 10000.0;
- if (m_rounded >= 60.0) {
- m_rounded -= 60.0;
- d += 1;
- }
-
- QString mm = QString::number(m_rounded, 'f', 4);
- if (m_rounded < 10.0) {
- mm.prepend("0");
- }
-
- if (lat) {
- return QString("%1%2").arg(d, 2, 10, QChar('0')).arg(mm);
- } else {
- return QString("%1%2").arg(d, 3, 10, QChar('0')).arg(mm);
- }
- };
-
- const bool latNorth = coord.latitude() >= 0.0;
- const bool lonEast = coord.longitude() >= 0.0;
-
- const QString latField = dmm(coord.latitude(), true);
- const QString lonField = dmm(coord.longitude(), false);
-
- const QString core = QString("GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,")
- .arg(hhmmss)
- .arg(latField)
- .arg(latNorth ? "N" : "S")
- .arg(lonField)
- .arg(lonEast ? "E" : "W")
- .arg(QString::number(altitude_msl, 'f', 1));
-
- quint8 cksum = 0;
- const QByteArray coreBytes = core.toUtf8();
- for (char ch : coreBytes) {
- cksum ^= static_cast(ch);
- }
-
- const QString cks = QString("%1").arg(cksum, 2, 16, QChar('0')).toUpper();
- const QByteArray sentence = QByteArray("$") + coreBytes + QByteArray("*") + cks.toUtf8();
- return sentence;
-}
-
-QPair NTRIPManager::_getBestPosition() const
-{
- MultiVehicleManager* mvm = MultiVehicleManager::instance();
- if (mvm) {
- if (Vehicle* veh = mvm->activeVehicle()) {
- FactGroup* gps = veh->gpsFactGroup();
- if (gps) {
- Fact* latF = gps->getFact(QStringLiteral("lat"));
- Fact* lonF = gps->getFact(QStringLiteral("lon"));
-
- if (latF && lonF) {
- const double lat = latF->rawValue().toDouble();
- const double lon = lonF->rawValue().toDouble();
-
- if (qIsFinite(lat) && qIsFinite(lon) &&
- !(lat == 0.0 && lon == 0.0) &&
- qAbs(lat) <= 90.0 && qAbs(lon) <= 180.0) {
-
- return {QGeoCoordinate(lat, lon, veh->coordinate().altitude()),
- QStringLiteral("GPS Raw")};
- }
- }
- }
-
- QGeoCoordinate coord = veh->coordinate();
- if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) {
- return {coord, QStringLiteral("Vehicle EKF")};
- }
- }
- }
-
- QGCPositionManager* posMgr = QGCPositionManager::instance();
- if (posMgr) {
- QGeoCoordinate coord = posMgr->gcsPosition();
- if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) {
- return {coord, QStringLiteral("GCS Position")};
- }
- }
-
- return {QGeoCoordinate(), QString()};
-}
-
-void NTRIPManager::_sendGGA()
-{
- if (!_transport) {
- return;
- }
-
- const auto [coord, srcUsed] = _getBestPosition();
-
- if (!coord.isValid()) {
- qCDebug(NTRIPManagerLog) << "NTRIP: No valid position, skipping GGA";
- return;
- }
-
- if (_ggaTimer && _ggaTimer->interval() != 5000) {
- _ggaTimer->setInterval(5000);
- qCDebug(NTRIPManagerLog) << "NTRIP: Position acquired, reducing GGA to 5s";
- }
-
- double alt_msl = coord.altitude();
- if (!qIsFinite(alt_msl)) {
- alt_msl = 0.0;
- }
-
- const QByteArray gga = makeGGA(coord, alt_msl);
- _transport->sendNMEA(gga);
-
- if (!srcUsed.isEmpty() && srcUsed != _ggaSource) {
- _ggaSource = srcUsed;
- emit ggaSourceChanged();
- }
+ _udpForwarder.forward(data);
}
void NTRIPManager::_onSettingChanged()
{
- if (_startStopBusy) {
+ if (_opState == OperationState::Starting || _opState == OperationState::Stopping) {
return;
}
NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
- bool shouldRun = settings && settings->ntripServerConnectEnabled() &&
- settings->ntripServerConnectEnabled()->rawValue().toBool();
- bool isRunning = (_transport != nullptr);
+ const bool shouldRun = settings && settings->ntripServerConnectEnabled() &&
+ settings->ntripServerConnectEnabled()->rawValue().toBool();
+ const bool isRunning = (_transport != nullptr);
if (shouldRun && isRunning && settings) {
- NTRIPTransportConfig config = _configFromSettings();
- const bool udpFwd = settings->ntripUdpForwardEnabled()->rawValue().toBool();
- const QString udpAddr = settings->ntripUdpTargetAddress()->rawValue().toString();
- const quint16 udpPort = static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt());
-
- if (config != _runningConfig ||
- udpFwd != _runningUdpForward || udpAddr != _runningUdpAddr || udpPort != _runningUdpPort) {
+ NTRIPTransportConfig config = NTRIPTransportConfig::fromSettings(*settings);
+ const bool udpFwd = settings->ntripUdpForwardEnabled()
+ ? settings->ntripUdpForwardEnabled()->rawValue().toBool() : false;
+ const QString udpAddr = settings->ntripUdpTargetAddress()
+ ? settings->ntripUdpTargetAddress()->rawValue().toString() : QString();
+ const quint16 udpPort = settings->ntripUdpTargetPort()
+ ? static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt()) : 0;
+
+ const bool udpChanged = udpFwd != _udpForwarder.isEnabled()
+ || udpAddr != _udpForwarder.address()
+ || udpPort != _udpForwarder.port();
+
+ if (config != _runningConfig || udpChanged) {
qCDebug(NTRIPManagerLog) << "NTRIP settings changed while running, restarting";
stopNTRIP();
startNTRIP();
@@ -508,123 +331,26 @@ void NTRIPManager::_onSettingChanged()
}
}
- const bool reconnectPending = _reconnectTimer && _reconnectTimer->isActive();
+ const bool reconnectPending = _reconnectPolicy.isPending();
if (shouldRun && !isRunning && !reconnectPending) {
startNTRIP();
} else if (!shouldRun) {
- if (_reconnectTimer) {
- _reconnectTimer->stop();
- }
+ _reconnectPolicy.cancel();
if (isRunning) {
stopNTRIP();
} else if (reconnectPending) {
- _reconnectAttempts = 0;
+ _reconnectPolicy.resetAttempts();
_setStatus(ConnectionStatus::Disconnected, tr("Disconnected"));
}
}
}
-QmlObjectListModel* NTRIPManager::mountpointModel() const
-{
- if (_sourceTableModel) {
- return _sourceTableModel->mountpoints();
- }
- return nullptr;
-}
-
void NTRIPManager::fetchMountpoints()
{
- if (_mountpointFetchStatus == MountpointFetchStatus::FetchInProgress) {
- return;
- }
-
- if (_sourceTableModel && _sourceTableModel->count() > 0 && _sourceTableFetchedAtMs > 0) {
- const qint64 age = QDateTime::currentMSecsSinceEpoch() - _sourceTableFetchedAtMs;
- if (age < kSourceTableCacheTtlMs) {
- qCDebug(NTRIPManagerLog) << "Source table cache hit, age:" << age << "ms";
- _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess;
- emit mountpointFetchStatusChanged();
- return;
- }
- }
-
- NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
+ NTRIPSettings *settings = SettingsManager::instance()->ntripSettings();
if (!settings) {
return;
}
-
- const QString host = settings->ntripServerHostAddress()->rawValue().toString();
- const int port = settings->ntripServerPort()->rawValue().toInt();
- const QString user = settings->ntripUsername()->rawValue().toString();
- const QString pass = settings->ntripPassword()->rawValue().toString();
- const bool useTls = settings->ntripUseTls() ? settings->ntripUseTls()->rawValue().toBool() : false;
-
- if (host.isEmpty()) {
- _mountpointFetchError = tr("Host address is empty");
- _mountpointFetchStatus = MountpointFetchStatus::FetchError;
- emit mountpointFetchErrorChanged();
- emit mountpointFetchStatusChanged();
- return;
- }
-
- if (!_sourceTableModel) {
- _sourceTableModel = new NTRIPSourceTableModel(this);
- emit mountpointModelChanged();
- }
-
- if (_sourceTableFetcher) {
- _sourceTableFetcher->deleteLater();
- _sourceTableFetcher = nullptr;
- }
-
- _mountpointFetchStatus = MountpointFetchStatus::FetchInProgress;
- _mountpointFetchError.clear();
- emit mountpointFetchStatusChanged();
- emit mountpointFetchErrorChanged();
-
- _sourceTableFetcher = new NTRIPSourceTableFetcher(host, port, user, pass, useTls, this);
-
- connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::sourceTableReceived, this, [this](const QString& table) {
- _sourceTableModel->parseSourceTable(table);
- _sourceTableFetchedAtMs = QDateTime::currentMSecsSinceEpoch();
-
- MultiVehicleManager* mvm = MultiVehicleManager::instance();
- if (mvm) {
- if (Vehicle* veh = mvm->activeVehicle()) {
- QGeoCoordinate coord = veh->coordinate();
- if (coord.isValid()) {
- _sourceTableModel->updateDistances(coord);
- }
- }
- }
-
- _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess;
- emit mountpointFetchStatusChanged();
- qCDebug(NTRIPManagerLog) << "NTRIP source table fetched:" << _sourceTableModel->count() << "mountpoints";
- });
-
- connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::error, this, [this](const QString& errorMsg) {
- _mountpointFetchError = errorMsg;
- _mountpointFetchStatus = MountpointFetchStatus::FetchError;
- emit mountpointFetchErrorChanged();
- emit mountpointFetchStatusChanged();
- qCWarning(NTRIPManagerLog) << "NTRIP source table fetch error:" << errorMsg;
- });
-
- connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::finished, this, [this]() {
- _sourceTableFetcher->deleteLater();
- _sourceTableFetcher = nullptr;
- });
-
- _sourceTableFetcher->fetch();
-}
-
-void NTRIPManager::selectMountpoint(const QString& mountpoint)
-{
- NTRIPSettings* settings = SettingsManager::instance()->ntripSettings();
- if (settings && settings->ntripMountpoint()) {
- settings->ntripMountpoint()->setRawValue(mountpoint);
- qCDebug(NTRIPManagerLog) << "NTRIP mountpoint selected:" << mountpoint;
- }
+ _sourceTableController.fetch(NTRIPTransportConfig::fromSettings(*settings));
}
diff --git a/src/GPS/NTRIP/NTRIPManager.h b/src/GPS/NTRIP/NTRIPManager.h
index 0ed8bc2b5ffc..5416e3d0ad8e 100644
--- a/src/GPS/NTRIP/NTRIPManager.h
+++ b/src/GPS/NTRIP/NTRIPManager.h
@@ -1,43 +1,45 @@
#pragma once
-#include "NTRIPHttpTransport.h"
+#include "NTRIPConnectionStats.h"
+#include "NTRIPGgaProvider.h"
+#include "NTRIPReconnectPolicy.h"
+#include "NTRIPSourceTableController.h"
+#include "NTRIPTransport.h"
+#include "NTRIPTransportConfig.h"
+#include "NTRIPUdpForwarder.h"
+
+#include
#include
#include
+#include
#include
-#include
-#include
-#include
#include
Q_DECLARE_LOGGING_CATEGORY(NTRIPManagerLog)
-class RTCMMavlink;
+#include "RTCMRouter.h"
+
+enum class NTRIPError;
class NTRIPSettings;
-class Vehicle;
-class MultiVehicleManager;
-class NTRIPSourceTableModel;
-class NTRIPSourceTableFetcher;
-class QmlObjectListModel;
+struct GPSEvent;
class NTRIPManager : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
- Q_MOC_INCLUDE("QmlObjectListModel.h")
+ Q_MOC_INCLUDE("NTRIPConnectionStats.h")
+ Q_MOC_INCLUDE("NTRIPSourceTableController.h")
Q_PROPERTY(ConnectionStatus connectionStatus READ connectionStatus NOTIFY connectionStatusChanged)
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
Q_PROPERTY(CasterStatus casterStatus READ casterStatus NOTIFY casterStatusChanged)
Q_PROPERTY(QString ggaSource READ ggaSource NOTIFY ggaSourceChanged)
- Q_PROPERTY(QmlObjectListModel* mountpointModel READ mountpointModel NOTIFY mountpointModelChanged)
- Q_PROPERTY(MountpointFetchStatus mountpointFetchStatus READ mountpointFetchStatus NOTIFY mountpointFetchStatusChanged)
- Q_PROPERTY(QString mountpointFetchError READ mountpointFetchError NOTIFY mountpointFetchErrorChanged)
- Q_PROPERTY(quint64 bytesReceived READ bytesReceived NOTIFY bytesReceivedChanged)
- Q_PROPERTY(quint32 messagesReceived READ messagesReceived NOTIFY messagesReceivedChanged)
- Q_PROPERTY(double dataRateBytesPerSec READ dataRateBytesPerSec NOTIFY dataRateChanged)
+ Q_PROPERTY(NTRIPSourceTableController* sourceTableController READ sourceTableController CONSTANT)
+ Q_PROPERTY(NTRIPConnectionStats* connectionStats READ connectionStats CONSTANT)
public:
+ using TransportFactory = std::function;
enum class ConnectionStatus {
Disconnected,
Connecting,
@@ -50,19 +52,6 @@ class NTRIPManager : public QObject
enum class CasterStatus { CasterConnected, CasterNoLocation, CasterError };
Q_ENUM(CasterStatus)
- enum class MountpointFetchStatus {
- FetchIdle,
- FetchInProgress,
- FetchSuccess,
- FetchError
- };
- Q_ENUM(MountpointFetchStatus)
-
- static constexpr int kMinReconnectMs = 1000;
- static constexpr int kMaxReconnectMs = 30000;
- static constexpr int kMaxReconnectAttempts = 100;
- static constexpr int kSourceTableCacheTtlMs = 60000;
-
explicit NTRIPManager(QObject* parent = nullptr);
~NTRIPManager() override;
@@ -71,79 +60,56 @@ class NTRIPManager : public QObject
ConnectionStatus connectionStatus() const { return _connectionStatus; }
QString statusMessage() const { return _statusMessage; }
CasterStatus casterStatus() const { return _casterStatus; }
- QString ggaSource() const { return _ggaSource; }
- QmlObjectListModel* mountpointModel() const;
- MountpointFetchStatus mountpointFetchStatus() const { return _mountpointFetchStatus; }
- QString mountpointFetchError() const { return _mountpointFetchError; }
- quint64 bytesReceived() const { return _bytesReceived; }
- quint32 messagesReceived() const { return _messagesReceived; }
- double dataRateBytesPerSec() const { return _dataRateBytesPerSec; }
+ QString ggaSource() const { return _ggaProvider.currentSource(); }
+ NTRIPSourceTableController* sourceTableController() { return &_sourceTableController; }
+ NTRIPConnectionStats* connectionStats() { return &_stats; }
Q_INVOKABLE void fetchMountpoints();
- Q_INVOKABLE void selectMountpoint(const QString& mountpoint);
+ Q_INVOKABLE void selectMountpoint(const QString& mountpoint) { _sourceTableController.selectMountpoint(mountpoint); }
+
+ void setTransportFactory(TransportFactory factory) { _transportFactory = std::move(factory); }
+ void setRtcmRouter(RTCMRouter *router) { _rtcmRouter = router; }
+ void setEventLogger(std::function logger) { _eventLogger = std::move(logger); }
void startNTRIP();
void stopNTRIP();
- static QByteArray makeGGA(const QGeoCoordinate& coord, double altitude_msl);
-
signals:
void connectionStatusChanged();
void statusMessageChanged();
void casterStatusChanged(CasterStatus status);
void ggaSourceChanged();
- void mountpointFetchStatusChanged();
- void mountpointModelChanged();
- void mountpointFetchErrorChanged();
- void bytesReceivedChanged();
- void messagesReceivedChanged();
- void dataRateChanged();
private:
- void _tcpError(const QString& errorMsg);
+ enum class OperationState { Idle, Starting, Running, Stopping };
+
+ void _transportError(NTRIPError code, const QString& detail);
void _rtcmDataReceived(const QByteArray& data);
void _onSettingChanged();
- void _sendGGA();
- void _scheduleReconnect();
void _setStatus(ConnectionStatus status, const QString& msg = {});
+ void _logEvent(const GPSEvent &event);
+ bool _transition(OperationState from, OperationState to);
- NTRIPTransportConfig _configFromSettings() const;
- QPair _getBestPosition() const;
-
- QTimer* _ggaTimer = nullptr;
+ NTRIPGgaProvider _ggaProvider{this};
+ NTRIPConnectionStats _stats{this};
+ NTRIPReconnectPolicy _reconnectPolicy{this};
+ NTRIPUdpForwarder _udpForwarder{this};
ConnectionStatus _connectionStatus = ConnectionStatus::Disconnected;
QString _statusMessage;
- QString _ggaSource;
-
- NTRIPHttpTransport* _transport = nullptr;
- RTCMMavlink* _rtcmMavlink = nullptr;
- QTimer* _reconnectTimer = nullptr;
- int _reconnectAttempts = 0;
- bool _startStopBusy = false;
-
CasterStatus _casterStatus = CasterStatus::CasterError;
- QUdpSocket* _udpSocket = nullptr;
- QHostAddress _udpTargetAddress;
- quint16 _udpTargetPort = 0;
- bool _udpForwardEnabled = false;
+ TransportFactory _transportFactory;
+ QPointer _transport;
+ OperationState _opState = OperationState::Idle;
+
+ QPointer _rtcmRouter;
+ std::function _eventLogger;
NTRIPTransportConfig _runningConfig;
- bool _runningUdpForward = false;
- QString _runningUdpAddr;
- quint16 _runningUdpPort = 0;
-
- NTRIPSourceTableModel* _sourceTableModel = nullptr;
- NTRIPSourceTableFetcher* _sourceTableFetcher = nullptr;
- MountpointFetchStatus _mountpointFetchStatus = MountpointFetchStatus::FetchIdle;
- QString _mountpointFetchError;
-
- quint64 _bytesReceived = 0;
- quint32 _messagesReceived = 0;
- double _dataRateBytesPerSec = 0.0;
- quint64 _dataRatePrevBytes = 0;
- QTimer* _dataRateTimer = nullptr;
-
- qint64 _sourceTableFetchedAtMs = 0;
+
+ NTRIPSourceTableController _sourceTableController{this};
+
+ static constexpr int kSettingsDebounceMs = 250;
+ QTimer _settingsDebounceTimer;
};
diff --git a/src/GPS/NTRIP/NTRIPReconnectPolicy.cc b/src/GPS/NTRIP/NTRIPReconnectPolicy.cc
new file mode 100644
index 000000000000..26a925dca1ab
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPReconnectPolicy.cc
@@ -0,0 +1,41 @@
+#include "NTRIPReconnectPolicy.h"
+
+#include
+
+NTRIPReconnectPolicy::NTRIPReconnectPolicy(QObject* parent)
+ : QObject(parent)
+{
+ _timer.setSingleShot(true);
+ connect(&_timer, &QTimer::timeout, this, &NTRIPReconnectPolicy::reconnectRequested);
+}
+
+void NTRIPReconnectPolicy::scheduleReconnect()
+{
+ if (_attempts >= kMaxReconnectAttempts) {
+ emit gaveUp();
+ return;
+ }
+ const int backoffMs = nextBackoffMs();
+ ++_attempts;
+ _timer.start(backoffMs);
+}
+
+void NTRIPReconnectPolicy::cancel()
+{
+ _timer.stop();
+}
+
+bool NTRIPReconnectPolicy::isPending() const
+{
+ return _timer.isActive();
+}
+
+void NTRIPReconnectPolicy::resetAttempts()
+{
+ _attempts = 0;
+}
+
+int NTRIPReconnectPolicy::nextBackoffMs() const
+{
+ return qMin(kMinReconnectMs * (1 << qMin(_attempts, 5)), kMaxReconnectMs);
+}
diff --git a/src/GPS/NTRIP/NTRIPReconnectPolicy.h b/src/GPS/NTRIP/NTRIPReconnectPolicy.h
new file mode 100644
index 000000000000..fafdae332aac
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPReconnectPolicy.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include
+#include
+
+class NTRIPReconnectPolicy : public QObject
+{
+ Q_OBJECT
+
+public:
+ static constexpr int kMinReconnectMs = 1000;
+ static constexpr int kMaxReconnectMs = 30000;
+ static constexpr int kMaxReconnectAttempts = 100;
+
+ explicit NTRIPReconnectPolicy(QObject* parent = nullptr);
+
+ void scheduleReconnect();
+ void cancel();
+ bool isPending() const;
+ void resetAttempts();
+ int attempts() const { return _attempts; }
+ int nextBackoffMs() const;
+
+ bool hasGivenUp() const { return _attempts >= kMaxReconnectAttempts; }
+
+signals:
+ void reconnectRequested();
+ void gaveUp();
+
+private:
+ QTimer _timer;
+ int _attempts = 0;
+};
diff --git a/src/GPS/NTRIP/NTRIPSettings.qml b/src/GPS/NTRIP/NTRIPSettings.qml
new file mode 100644
index 000000000000..0da0df666c79
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPSettings.qml
@@ -0,0 +1,349 @@
+import QtQuick
+import QtQuick.Layouts
+
+import QGroundControl
+import QGroundControl.Controls
+import QGroundControl.FactControls
+import "qrc:/qml/QGroundControl/GPS/GPSFormatHelpers.js" as GPSHelpers
+
+ColumnLayout {
+ id: root
+ spacing: ScreenTools.defaultFontPixelHeight / 2
+
+ property var _ntrip: QGroundControl.settingsManager.ntripSettings
+ property Fact _ntripEnabled: _ntrip.ntripServerConnectEnabled
+ property var _ntripMgr: QGroundControl.ntripManager
+ property bool _isConnected: _ntripMgr.connectionStatus === NTRIPManager.Connected
+ property bool _isConnecting: _ntripMgr.connectionStatus === NTRIPManager.Connecting
+ property bool _isReconnecting: _ntripMgr.connectionStatus === NTRIPManager.Reconnecting
+ property bool _isError: _ntripMgr.connectionStatus === NTRIPManager.Error
+ property bool _isActive: _ntripEnabled.rawValue
+ property bool _hasHost: _ntrip.ntripServerHostAddress.rawValue !== ""
+ property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 30
+
+ function formatBytes(bytes) { return GPSHelpers.formatDataSize(bytes) }
+ function formatRate(bytesPerSec) { return GPSHelpers.formatDataRate(bytesPerSec) }
+
+ QGCPalette { id: qgcPal }
+
+ // -- Connection Status ---------------------------------------------------
+
+ SettingsGroupLayout {
+ Layout.fillWidth: true
+ heading: qsTr("NTRIP Connection")
+ headingDescription: qsTr("Receive RTK corrections from an NTRIP caster")
+ showDividers: true
+ visible: _ntrip.visible
+
+ ConnectionStatusRow {
+ statusColor: {
+ switch (_ntripMgr.connectionStatus) {
+ case NTRIPManager.Connected: return qgcPal.colorGreen
+ case NTRIPManager.Connecting:
+ case NTRIPManager.Reconnecting: return qgcPal.colorOrange
+ case NTRIPManager.Error: return qgcPal.colorRed
+ default: return qgcPal.colorGrey
+ }
+ }
+ statusText: _ntripMgr.statusMessage || qsTr("Disconnected")
+ buttonText: {
+ if (_isConnecting) return qsTr("Connecting...")
+ if (_isReconnecting) return qsTr("Reconnecting...")
+ if (_isActive) return qsTr("Disconnect")
+ return qsTr("Connect")
+ }
+ buttonEnabled: _isActive ? !_isConnecting : _hasHost
+ onButtonClicked: _ntripEnabled.rawValue = !_ntripEnabled.rawValue
+ }
+
+ QGCLabel {
+ Layout.fillWidth: true
+ visible: _isConnected && _ntripMgr.connectionStats.messagesReceived > 0
+ text: {
+ var parts = [qsTr("%1 messages").arg(_ntripMgr.connectionStats.messagesReceived),
+ formatBytes(_ntripMgr.connectionStats.bytesReceived)]
+ if (_ntripMgr.connectionStats.dataRateBytesPerSec > 0)
+ parts.push(formatRate(_ntripMgr.connectionStats.dataRateBytesPerSec))
+ if (_ntripMgr.ggaSource)
+ parts.push(qsTr("GGA: %1").arg(_ntripMgr.ggaSource))
+ return parts.join(" \u00B7 ")
+ }
+ font.pointSize: ScreenTools.smallFontPointSize
+ color: qgcPal.colorGrey
+ }
+
+ // Connected summary: show server/mountpoint inline
+ LabelledLabel {
+ label: qsTr("Server")
+ labelText: {
+ var host = _ntrip.ntripServerHostAddress.rawValue
+ var port = _ntrip.ntripServerPort.rawValue
+ var mp = _ntrip.ntripMountpoint.rawValue
+ var s = host + ":" + port
+ if (mp) s += "/" + mp
+ return s
+ }
+ visible: _isActive
+ }
+
+ QGCLabel {
+ Layout.fillWidth: true
+ visible: _isError && _ntripMgr.statusMessage !== ""
+ text: _ntripMgr.statusMessage
+ wrapMode: Text.WordWrap
+ color: qgcPal.colorRed
+ font.pointSize: ScreenTools.smallFontPointSize
+ }
+ }
+
+ // -- Server Settings -----------------------------------------------------
+
+ SettingsGroupLayout {
+ Layout.fillWidth: true
+ heading: qsTr("NTRIP Server")
+ showDividers: true
+ visible: !_isActive
+ && (_ntrip.ntripServerHostAddress.visible || _ntrip.ntripServerPort.visible ||
+ _ntrip.ntripUsername.visible || _ntrip.ntripPassword.visible)
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ fact: _ntrip.ntripServerHostAddress
+ visible: fact.visible
+ }
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ fact: _ntrip.ntripServerPort
+ visible: fact.visible
+ }
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ label: fact.shortDescription
+ fact: _ntrip.ntripUsername
+ visible: fact.visible
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ visible: _ntrip.ntripPassword.visible
+ spacing: ScreenTools.defaultFontPixelWidth * 0.5
+
+ LabelledFactTextField {
+ id: passwordField
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ label: fact.shortDescription
+ fact: _ntrip.ntripPassword
+ textField.echoMode: _showPassword ? TextInput.Normal : TextInput.Password
+
+ property bool _showPassword: false
+ }
+
+ QGCButton {
+ text: passwordField._showPassword ? qsTr("Hide") : qsTr("Show")
+ onClicked: passwordField._showPassword = !passwordField._showPassword
+ Layout.alignment: Qt.AlignBottom
+ }
+ }
+
+ FactCheckBoxSlider {
+ Layout.fillWidth: true
+ text: fact.shortDescription
+ fact: _ntrip.ntripUseTls
+ visible: fact.visible
+ }
+
+ QGCLabel {
+ Layout.fillWidth: true
+ visible: !_ntrip.ntripUseTls.rawValue
+ && (_ntrip.ntripUsername.rawValue !== "" || _ntrip.ntripPassword.rawValue !== "")
+ text: qsTr("⚠ Credentials will be sent unencrypted. Enable TLS for secure connections.")
+ wrapMode: Text.WordWrap
+ color: qgcPal.colorOrange
+ font.pointSize: ScreenTools.smallFontPointSize
+ }
+ }
+
+ // -- Mountpoint ----------------------------------------------------------
+
+ SettingsGroupLayout {
+ Layout.fillWidth: true
+ heading: qsTr("NTRIP Mountpoint")
+ showDividers: true
+ visible: !_isActive && _ntrip.ntripMountpoint.visible
+
+ property bool _showBrowseList: false
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: ScreenTools.defaultFontPixelWidth
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ label: fact.shortDescription
+ fact: _ntrip.ntripMountpoint
+ }
+
+ QGCButton {
+ text: qsTr("Browse")
+ enabled: _hasHost &&
+ _ntripMgr.sourceTableController.fetchStatus !== NTRIPSourceTableController.InProgress
+ onClicked: {
+ parent.parent._showBrowseList = true
+ _ntripMgr.fetchMountpoints()
+ }
+ }
+ }
+
+ QGCLabel {
+ Layout.fillWidth: true
+ visible: _ntripMgr.sourceTableController.fetchStatus === NTRIPSourceTableController.InProgress
+ text: qsTr("Fetching mountpoints...")
+ color: qgcPal.colorOrange
+ }
+
+ QGCLabel {
+ Layout.fillWidth: true
+ visible: _ntripMgr.sourceTableController.fetchStatus === NTRIPSourceTableController.Error
+ text: _ntripMgr.sourceTableController.fetchError
+ color: qgcPal.colorRed
+ wrapMode: Text.WordWrap
+ }
+
+ QGCListView {
+ id: mountpointList
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.min(contentHeight, ScreenTools.defaultFontPixelHeight * 20)
+ visible: !!parent._showBrowseList
+ && !!_ntripMgr.sourceTableController.mountpointModel
+ && _ntripMgr.sourceTableController.mountpointModel.count > 0
+ model: _ntripMgr.sourceTableController.mountpointModel
+ spacing: ScreenTools.defaultFontPixelHeight * 0.25
+
+ delegate: Rectangle {
+ required property int index
+ required property var object
+
+ width: ListView.view.width
+ height: mountRow.height + ScreenTools.defaultFontPixelHeight * 0.5
+ radius: ScreenTools.defaultFontPixelHeight * 0.25
+ color: {
+ if (object.mountpoint === _ntrip.ntripMountpoint.rawValue)
+ return qgcPal.buttonHighlight
+ return index % 2 === 0 ? qgcPal.windowShade : qgcPal.window
+ }
+
+ RowLayout {
+ id: mountRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: ScreenTools.defaultFontPixelWidth
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ RowLayout {
+ spacing: ScreenTools.defaultFontPixelWidth
+
+ QGCLabel {
+ text: object.mountpoint
+ font.bold: true
+ color: object.mountpoint === _ntrip.ntripMountpoint.rawValue
+ ? qgcPal.buttonHighlightText : qgcPal.text
+ }
+ QGCLabel {
+ visible: object.mountpoint === _ntrip.ntripMountpoint.rawValue
+ text: qsTr("(selected)")
+ font.pointSize: ScreenTools.smallFontPointSize
+ color: qgcPal.buttonHighlightText
+ }
+ }
+
+ QGCLabel {
+ text: {
+ var parts = []
+ if (object.format) parts.push(object.format)
+ if (object.navSystem) parts.push(object.navSystem)
+ if (object.country) parts.push(object.country)
+ if (object.bitrate > 0) parts.push(object.bitrate + " bps")
+ if (object.distanceKm >= 0) parts.push(object.distanceKm.toFixed(1) + " km")
+ return parts.join(" \u00B7 ")
+ }
+ font.pointSize: ScreenTools.smallFontPointSize
+ color: object.mountpoint === _ntrip.ntripMountpoint.rawValue
+ ? qgcPal.buttonHighlightText : qgcPal.colorGrey
+ }
+ }
+
+ QGCButton {
+ text: object.mountpoint === _ntrip.ntripMountpoint.rawValue
+ ? qsTr("Selected") : qsTr("Select")
+ enabled: object.mountpoint !== _ntrip.ntripMountpoint.rawValue
+ onClicked: {
+ _ntripMgr.sourceTableController.selectMountpoint(object.mountpoint)
+ mountpointList.parent._showBrowseList = false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // -- Options & Forwarding ------------------------------------------------
+
+ SettingsGroupLayout {
+ Layout.fillWidth: true
+ heading: qsTr("NTRIP Options")
+ headingDescription: qsTr("Message filtering, GGA position source, and UDP forwarding")
+ showDividers: true
+
+ FactComboBox {
+ Layout.fillWidth: true
+ fact: _ntrip.ntripGgaPositionSource
+ visible: fact.visible
+ indexModel: false
+ }
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ label: fact.shortDescription
+ fact: _ntrip.ntripWhitelist
+ visible: fact.visible
+ textField.placeholderText: qsTr("e.g. 1005,1077,1087")
+ }
+
+ FactCheckBoxSlider {
+ Layout.fillWidth: true
+ text: fact.shortDescription
+ fact: _ntrip.ntripUdpForwardEnabled
+ visible: fact.visible
+ }
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ label: fact.shortDescription
+ fact: _ntrip.ntripUdpTargetAddress
+ visible: fact.visible
+ enabled: _ntrip.ntripUdpForwardEnabled.rawValue
+ }
+
+ LabelledFactTextField {
+ Layout.fillWidth: true
+ textFieldPreferredWidth: _textFieldWidth
+ label: fact.shortDescription
+ fact: _ntrip.ntripUdpTargetPort
+ visible: fact.visible
+ enabled: _ntrip.ntripUdpForwardEnabled.rawValue
+ }
+ }
+}
diff --git a/src/GPS/NTRIP/NTRIPSourceTable.h b/src/GPS/NTRIP/NTRIPSourceTable.h
index 1804a1dd5849..532ba0626260 100644
--- a/src/GPS/NTRIP/NTRIPSourceTable.h
+++ b/src/GPS/NTRIP/NTRIPSourceTable.h
@@ -13,45 +13,33 @@ class QNetworkAccessManager;
class NTRIPMountpointModel : public QObject
{
Q_OBJECT
- Q_PROPERTY(QString mountpoint READ mountpoint CONSTANT)
- Q_PROPERTY(QString identifier READ identifier CONSTANT)
- Q_PROPERTY(QString format READ format CONSTANT)
- Q_PROPERTY(QString formatDetails READ formatDetails CONSTANT)
- Q_PROPERTY(int carrier READ carrier CONSTANT)
- Q_PROPERTY(QString navSystem READ navSystem CONSTANT)
- Q_PROPERTY(QString network READ network CONSTANT)
- Q_PROPERTY(QString country READ country CONSTANT)
- Q_PROPERTY(double latitude READ latitude CONSTANT)
- Q_PROPERTY(double longitude READ longitude CONSTANT)
- Q_PROPERTY(bool nmea READ nmea CONSTANT)
- Q_PROPERTY(bool solution READ solution CONSTANT)
- Q_PROPERTY(QString generator READ generator CONSTANT)
- Q_PROPERTY(QString compression READ compression CONSTANT)
- Q_PROPERTY(QString authentication READ authentication CONSTANT)
- Q_PROPERTY(bool fee READ fee CONSTANT)
- Q_PROPERTY(int bitrate READ bitrate CONSTANT)
- Q_PROPERTY(double distanceKm READ distanceKm NOTIFY distanceKmChanged)
+ Q_PROPERTY(QString mountpoint MEMBER _mountpoint CONSTANT)
+ Q_PROPERTY(QString identifier MEMBER _identifier CONSTANT)
+ Q_PROPERTY(QString format MEMBER _format CONSTANT)
+ Q_PROPERTY(QString formatDetails MEMBER _formatDetails CONSTANT)
+ Q_PROPERTY(int carrier MEMBER _carrier CONSTANT)
+ Q_PROPERTY(QString navSystem MEMBER _navSystem CONSTANT)
+ Q_PROPERTY(QString network MEMBER _network CONSTANT)
+ Q_PROPERTY(QString country MEMBER _country CONSTANT)
+ Q_PROPERTY(double latitude MEMBER _latitude CONSTANT)
+ Q_PROPERTY(double longitude MEMBER _longitude CONSTANT)
+ Q_PROPERTY(bool nmea MEMBER _nmea CONSTANT)
+ Q_PROPERTY(bool solution MEMBER _solution CONSTANT)
+ Q_PROPERTY(QString generator MEMBER _generator CONSTANT)
+ Q_PROPERTY(QString compression MEMBER _compression CONSTANT)
+ Q_PROPERTY(QString authentication MEMBER _authentication CONSTANT)
+ Q_PROPERTY(bool fee MEMBER _fee CONSTANT)
+ Q_PROPERTY(int bitrate MEMBER _bitrate CONSTANT)
+ Q_PROPERTY(double distanceKm READ distanceKm NOTIFY distanceKmChanged)
public:
explicit NTRIPMountpointModel(QObject* parent = nullptr) : QObject(parent) {}
QString mountpoint() const { return _mountpoint; }
QString identifier() const { return _identifier; }
- QString format() const { return _format; }
- QString formatDetails() const { return _formatDetails; }
int carrier() const { return _carrier; }
- QString navSystem() const { return _navSystem; }
- QString network() const { return _network; }
- QString country() const { return _country; }
double latitude() const { return _latitude; }
double longitude() const { return _longitude; }
- bool nmea() const { return _nmea; }
- bool solution() const { return _solution; }
- QString generator() const { return _generator; }
- QString compression() const { return _compression; }
- QString authentication() const { return _authentication; }
- bool fee() const { return _fee; }
- int bitrate() const { return _bitrate; }
double distanceKm() const { return _distanceKm; }
static NTRIPMountpointModel* fromSourceTableLine(const QString& line, QObject* parent = nullptr);
diff --git a/src/GPS/NTRIP/NTRIPSourceTableController.cc b/src/GPS/NTRIP/NTRIPSourceTableController.cc
new file mode 100644
index 000000000000..851970d28558
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPSourceTableController.cc
@@ -0,0 +1,119 @@
+#include "NTRIPSourceTableController.h"
+#include "NTRIPSettings.h"
+#include "NTRIPSourceTable.h"
+#include "MultiVehicleManager.h"
+#include "QGCLoggingCategory.h"
+#include "QmlObjectListModel.h"
+#include "SettingsManager.h"
+#include "Vehicle.h"
+
+#include
+#include
+
+QGC_LOGGING_CATEGORY(NTRIPSourceTableControllerLog, "GPS.NTRIPSourceTableController")
+
+NTRIPSourceTableController::NTRIPSourceTableController(QObject *parent)
+ : QObject(parent)
+{
+}
+
+QmlObjectListModel *NTRIPSourceTableController::mountpointModel() const
+{
+ return _model ? _model->mountpoints() : nullptr;
+}
+
+void NTRIPSourceTableController::fetch(const NTRIPTransportConfig &config)
+{
+ fetch(config.host, config.port, config.username, config.password, config.useTls);
+}
+
+void NTRIPSourceTableController::fetch(const QString &host, int port,
+ const QString &username, const QString &password,
+ bool useTls)
+{
+ if (_fetchStatus == FetchStatus::InProgress) {
+ return;
+ }
+
+ if (_model && _model->count() > 0 && _fetchedAtMs > 0
+ && host == _lastFetchHost && port == _lastFetchPort
+ && username == _lastFetchUsername
+ && QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256) == _lastFetchPasswordHash) {
+ const qint64 age = QDateTime::currentMSecsSinceEpoch() - _fetchedAtMs;
+ if (age < kCacheTtlMs) {
+ qCDebug(NTRIPSourceTableControllerLog) << "Source table cache hit, age:" << age << "ms";
+ _fetchStatus = FetchStatus::Success;
+ emit fetchStatusChanged();
+ return;
+ }
+ }
+
+ if (host.isEmpty()) {
+ _fetchError = tr("Host address is empty");
+ _fetchStatus = FetchStatus::Error;
+ emit fetchErrorChanged();
+ emit fetchStatusChanged();
+ return;
+ }
+
+ if (!_model) {
+ _model = new NTRIPSourceTableModel(this);
+ emit mountpointModelChanged();
+ }
+
+ if (_fetcher) {
+ _fetcher->deleteLater();
+ _fetcher = nullptr;
+ }
+
+ _fetchStatus = FetchStatus::InProgress;
+ _fetchError.clear();
+ emit fetchStatusChanged();
+
+ _fetcher = new NTRIPSourceTableFetcher(host, port, username, password, useTls, this);
+
+ _lastFetchHost = host;
+ _lastFetchPort = port;
+ _lastFetchUsername = username;
+ _lastFetchPasswordHash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256);
+
+ connect(_fetcher, &NTRIPSourceTableFetcher::sourceTableReceived, this, [this](const QString &table) {
+ _model->parseSourceTable(table);
+ _fetchedAtMs = QDateTime::currentMSecsSinceEpoch();
+
+ MultiVehicleManager *mvm = MultiVehicleManager::instance();
+ if (mvm && mvm->activeVehicle()) {
+ QGeoCoordinate coord = mvm->activeVehicle()->coordinate();
+ if (coord.isValid()) {
+ _model->updateDistances(coord);
+ _model->sortByDistance();
+ }
+ }
+
+ _fetchStatus = FetchStatus::Success;
+ emit fetchStatusChanged();
+ emit mountpointModelChanged();
+ });
+
+ connect(_fetcher, &NTRIPSourceTableFetcher::error, this, [this](const QString &err) {
+ _fetchError = err;
+ _fetchStatus = FetchStatus::Error;
+ emit fetchErrorChanged();
+ emit fetchStatusChanged();
+ });
+
+ connect(_fetcher, &NTRIPSourceTableFetcher::finished, this, [this]() {
+ _fetcher->deleteLater();
+ _fetcher = nullptr;
+ });
+
+ _fetcher->fetch();
+}
+
+void NTRIPSourceTableController::selectMountpoint(const QString &mountpoint)
+{
+ NTRIPSettings *settings = SettingsManager::instance()->ntripSettings();
+ if (settings && settings->ntripMountpoint()) {
+ settings->ntripMountpoint()->setRawValue(mountpoint);
+ }
+}
diff --git a/src/GPS/NTRIP/NTRIPSourceTableController.h b/src/GPS/NTRIP/NTRIPSourceTableController.h
new file mode 100644
index 000000000000..2873515ea35a
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPSourceTableController.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "NTRIPTransportConfig.h"
+
+#include
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(NTRIPSourceTableControllerLog)
+
+class NTRIPSourceTableFetcher;
+class NTRIPSourceTableModel;
+class QmlObjectListModel;
+
+class NTRIPSourceTableController : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+ Q_MOC_INCLUDE("QmlObjectListModel.h")
+ Q_PROPERTY(FetchStatus fetchStatus READ fetchStatus NOTIFY fetchStatusChanged)
+ Q_PROPERTY(QString fetchError READ fetchError NOTIFY fetchErrorChanged)
+ Q_PROPERTY(QmlObjectListModel* mountpointModel READ mountpointModel NOTIFY mountpointModelChanged)
+
+public:
+ enum class FetchStatus { Idle, InProgress, Success, Error };
+ Q_ENUM(FetchStatus)
+
+ static constexpr int kCacheTtlMs = 60000;
+
+ explicit NTRIPSourceTableController(QObject *parent = nullptr);
+
+ FetchStatus fetchStatus() const { return _fetchStatus; }
+ QString fetchError() const { return _fetchError; }
+ QmlObjectListModel *mountpointModel() const;
+
+ void fetch(const NTRIPTransportConfig &config);
+ void fetch(const QString &host, int port, const QString &username,
+ const QString &password, bool useTls);
+ Q_INVOKABLE void selectMountpoint(const QString &mountpoint);
+
+signals:
+ void fetchStatusChanged();
+ void fetchErrorChanged();
+ void mountpointModelChanged();
+
+private:
+ NTRIPSourceTableModel *_model = nullptr;
+ NTRIPSourceTableFetcher *_fetcher = nullptr;
+ FetchStatus _fetchStatus = FetchStatus::Idle;
+ QString _fetchError;
+ qint64 _fetchedAtMs = 0;
+ QString _lastFetchHost;
+ int _lastFetchPort = 0;
+ QString _lastFetchUsername;
+ QByteArray _lastFetchPasswordHash;
+};
diff --git a/src/GPS/NTRIP/NTRIPTransport.h b/src/GPS/NTRIP/NTRIPTransport.h
new file mode 100644
index 000000000000..f17678c7edf4
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPTransport.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "NTRIPError.h"
+
+#include
+
+class NTRIPTransport : public QObject
+{
+ Q_OBJECT
+
+public:
+ using QObject::QObject;
+
+ virtual void start() = 0;
+ virtual void stop() = 0;
+ virtual void sendNMEA(const QByteArray& nmea) = 0;
+
+signals:
+ void connected();
+ void error(NTRIPError code, const QString& detail);
+ void RTCMDataUpdate(const QByteArray& message);
+ void finished();
+};
diff --git a/src/GPS/NTRIP/NTRIPTransportConfig.cc b/src/GPS/NTRIP/NTRIPTransportConfig.cc
new file mode 100644
index 000000000000..a7636678b11e
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPTransportConfig.cc
@@ -0,0 +1,16 @@
+#include "NTRIPTransportConfig.h"
+#include "NTRIPSettings.h"
+#include "Fact.h"
+
+NTRIPTransportConfig NTRIPTransportConfig::fromSettings(NTRIPSettings &settings)
+{
+ NTRIPTransportConfig config;
+ if (settings.ntripServerHostAddress()) config.host = settings.ntripServerHostAddress()->rawValue().toString();
+ if (settings.ntripServerPort()) config.port = settings.ntripServerPort()->rawValue().toInt();
+ if (settings.ntripUsername()) config.username = settings.ntripUsername()->rawValue().toString();
+ if (settings.ntripPassword()) config.password = settings.ntripPassword()->rawValue().toString();
+ if (settings.ntripMountpoint()) config.mountpoint = settings.ntripMountpoint()->rawValue().toString();
+ if (settings.ntripWhitelist()) config.whitelist = settings.ntripWhitelist()->rawValue().toString();
+ if (settings.ntripUseTls()) config.useTls = settings.ntripUseTls()->rawValue().toBool();
+ return config;
+}
diff --git a/src/GPS/NTRIP/NTRIPTransportConfig.h b/src/GPS/NTRIP/NTRIPTransportConfig.h
new file mode 100644
index 000000000000..1d0054f266d0
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPTransportConfig.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include
+
+class NTRIPSettings;
+
+struct NTRIPTransportConfig {
+ QString host;
+ int port = 2101;
+ QString username;
+ QString password;
+ QString mountpoint;
+ QString whitelist;
+ bool useTls = false;
+
+ bool operator==(const NTRIPTransportConfig&) const = default;
+
+ bool isValid() const { return !host.isEmpty() && port > 0 && port <= 65535; }
+
+ static NTRIPTransportConfig fromSettings(NTRIPSettings &settings);
+};
diff --git a/src/GPS/NTRIP/NTRIPUdpForwarder.cc b/src/GPS/NTRIP/NTRIPUdpForwarder.cc
new file mode 100644
index 000000000000..a96cdca6a6da
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPUdpForwarder.cc
@@ -0,0 +1,59 @@
+#include "NTRIPUdpForwarder.h"
+#include "QGCLoggingCategory.h"
+
+#include
+
+QGC_LOGGING_CATEGORY(NTRIPUdpForwarderLog, "GPS.NTRIPUdpForwarder")
+
+NTRIPUdpForwarder::NTRIPUdpForwarder(QObject *parent)
+ : QObject(parent)
+{
+}
+
+NTRIPUdpForwarder::~NTRIPUdpForwarder()
+{
+ stop();
+}
+
+bool NTRIPUdpForwarder::configure(const QString &address, quint16 port)
+{
+ stop();
+
+ const QHostAddress parsed(address);
+ if (address.isEmpty() || parsed.isNull() || port == 0) {
+ qCWarning(NTRIPUdpForwarderLog) << "Invalid UDP forward config:" << address << port;
+ return false;
+ }
+
+ _address = parsed;
+ _port = port;
+ _socket = new QUdpSocket(this);
+ _enabled = true;
+
+ qCDebug(NTRIPUdpForwarderLog) << "UDP forwarding configured:" << address << ":" << port;
+ return true;
+}
+
+void NTRIPUdpForwarder::forward(const QByteArray &data)
+{
+ if (!_enabled || !_socket || _port == 0) {
+ return;
+ }
+
+ const qint64 sent = _socket->writeDatagram(data, _address, _port);
+ if (sent < 0) {
+ qCWarning(NTRIPUdpForwarderLog) << "UDP forward failed:" << _socket->errorString();
+ }
+}
+
+void NTRIPUdpForwarder::stop()
+{
+ if (_socket) {
+ _socket->close();
+ _socket->deleteLater();
+ _socket = nullptr;
+ }
+ _enabled = false;
+ _port = 0;
+ _address.clear();
+}
diff --git a/src/GPS/NTRIP/NTRIPUdpForwarder.h b/src/GPS/NTRIP/NTRIPUdpForwarder.h
new file mode 100644
index 000000000000..ba04088a1648
--- /dev/null
+++ b/src/GPS/NTRIP/NTRIPUdpForwarder.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include
+#include
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(NTRIPUdpForwarderLog)
+
+class QUdpSocket;
+
+class NTRIPUdpForwarder : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit NTRIPUdpForwarder(QObject *parent = nullptr);
+ ~NTRIPUdpForwarder() override;
+
+ bool configure(const QString &address, quint16 port);
+ void forward(const QByteArray &data);
+ void stop();
+
+ bool isEnabled() const { return _enabled; }
+ QString address() const { return _address.toString(); }
+ quint16 port() const { return _port; }
+
+private:
+ QUdpSocket *_socket = nullptr;
+ QHostAddress _address;
+ quint16 _port = 0;
+ bool _enabled = false;
+};
diff --git a/src/GPS/NTRIP/RTCMParser.cc b/src/GPS/NTRIP/RTCMParser.cc
index b0eaef98b1e8..79c0ad97212d 100644
--- a/src/GPS/NTRIP/RTCMParser.cc
+++ b/src/GPS/NTRIP/RTCMParser.cc
@@ -7,7 +7,7 @@ RTCMParser::RTCMParser()
void RTCMParser::reset()
{
- _state = WaitingForPreamble;
+ _state = State::WaitingForPreamble;
_messageLength = 0;
_bytesRead = 0;
_lengthBytesRead = 0;
@@ -17,39 +17,43 @@ void RTCMParser::reset()
bool RTCMParser::addByte(uint8_t byte)
{
switch (_state) {
- case WaitingForPreamble:
- if (byte == RTCM3_PREAMBLE) {
+ case State::WaitingForPreamble:
+ if (byte == kPreamble) {
_buffer[0] = byte;
_bytesRead = 1;
- _state = ReadingLength;
+ _state = State::ReadingLength;
_lengthBytesRead = 0;
}
break;
- case ReadingLength:
+ case State::ReadingLength:
_lengthBytes[_lengthBytesRead++] = byte;
_buffer[_bytesRead++] = byte;
if (_lengthBytesRead == 2) {
_messageLength = ((_lengthBytes[0] & 0x03) << 8) | _lengthBytes[1];
if (_messageLength > 0 && _messageLength <= kMaxPayloadLength) {
- _state = ReadingMessage;
+ _state = State::ReadingMessage;
} else {
reset();
}
}
break;
- case ReadingMessage:
+ case State::ReadingMessage:
if (_bytesRead < kHeaderSize + kMaxPayloadLength) {
_buffer[_bytesRead++] = byte;
+ } else {
+ // Buffer overflow — corrupt or oversized message; discard
+ reset();
+ break;
}
if (_bytesRead >= _messageLength + kHeaderSize) {
- _state = ReadingCRC;
+ _state = State::ReadingCRC;
_crcBytesRead = 0;
}
break;
- case ReadingCRC:
+ case State::ReadingCRC:
_crcBytes[_crcBytesRead++] = byte;
if (_crcBytesRead == kCrcSize) {
return true;
diff --git a/src/GPS/NTRIP/RTCMParser.h b/src/GPS/NTRIP/RTCMParser.h
index 23dd6df05f1e..0c1e0f5870b0 100644
--- a/src/GPS/NTRIP/RTCMParser.h
+++ b/src/GPS/NTRIP/RTCMParser.h
@@ -3,13 +3,18 @@
#include
#include
-static constexpr uint8_t RTCM3_PREAMBLE = 0xD3;
+#include
+#include
class RTCMParser
{
public:
+ static constexpr uint8_t kPreamble = 0xD3;
+
RTCMParser();
void reset();
+ void setWhitelist(const QVector& ids) { _whitelist = QSet(ids.begin(), ids.end()); }
+ bool isWhitelisted(uint16_t id) const { return _whitelist.isEmpty() || _whitelist.contains(id); }
bool addByte(uint8_t byte);
uint8_t* message() { return _buffer; }
uint16_t messageLength() const { return _messageLength; }
@@ -21,7 +26,7 @@ class RTCMParser
static uint32_t crc24q(const uint8_t* data, size_t len);
private:
- enum State {
+ enum class State {
WaitingForPreamble,
ReadingLength,
ReadingMessage,
@@ -31,6 +36,7 @@ class RTCMParser
static constexpr uint16_t kMaxPayloadLength = 1023;
static constexpr int kHeaderSize = 3;
+ QSet _whitelist;
State _state;
uint8_t _buffer[kHeaderSize + kMaxPayloadLength];
uint16_t _messageLength;
diff --git a/src/GPS/RTCMMavlink.h b/src/GPS/RTCMMavlink.h
deleted file mode 100644
index b9b7af6aad2b..000000000000
--- a/src/GPS/RTCMMavlink.h
+++ /dev/null
@@ -1,29 +0,0 @@
-#pragma once
-
-#include
-#include
-#include
-
-typedef struct __mavlink_gps_rtcm_data_t mavlink_gps_rtcm_data_t;
-
-Q_DECLARE_LOGGING_CATEGORY(RTCMMavlinkLog)
-
-class RTCMMavlink : public QObject
-{
- Q_OBJECT
-
-public:
- RTCMMavlink(QObject *parent = nullptr);
- ~RTCMMavlink();
-
-public slots:
- void RTCMDataUpdate(QByteArrayView data);
-
-private:
- void _calculateBandwith(qsizetype bytes);
- static void _sendMessageToVehicle(const mavlink_gps_rtcm_data_t &data);
-
- uint8_t _sequenceId = 0;
- qsizetype _bandwidthByteCounter = 0;
- QElapsedTimer _bandwidthTimer;
-};
diff --git a/src/GPS/RTK/CMakeLists.txt b/src/GPS/RTK/CMakeLists.txt
new file mode 100644
index 000000000000..6f41691d1efc
--- /dev/null
+++ b/src/GPS/RTK/CMakeLists.txt
@@ -0,0 +1,62 @@
+# ============================================================================
+# RTK Module
+# RTK base station, RTCM correction forwarding, and survey-in
+# ============================================================================
+
+target_sources(${CMAKE_PROJECT_NAME}
+ PRIVATE
+ GPSPositionSource.cc
+ GPSPositionSource.h
+ GPSRtk.cc
+ GPSRtk.h
+ GPSSatelliteInfoSource.cc
+ GPSSatelliteInfoSource.h
+ GPSRTKFactGroup.cc
+ GPSRTKFactGroup.h
+ RTCMMavlink.cc
+ RTCMMavlink.h
+ RTCMRouter.cc
+ RTCMRouter.h
+ RTKSatelliteModel.cc
+ RTKSatelliteModel.h
+)
+
+target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+
+# ----------------------------------------------------------------------------
+# RTK QML Module
+# ----------------------------------------------------------------------------
+qt_add_library(GPSRTKModule STATIC)
+
+qt_add_qml_module(GPSRTKModule
+ URI QGroundControl.GPS.RTK
+ VERSION 1.0
+ RESOURCE_PREFIX /qml
+ QML_FILES
+ FixStatusDot.qml
+ GNSSIntegrityAlert.qml
+ GPSIndicator.qml
+ GPSIndicatorPage.qml
+ GPSStatusBlock.qml
+ RTKBaseStationMapItem.qml
+ RTKBaseStationStatus.qml
+ RTKGPSIndicator.qml
+ RTKSettings.qml
+ SatelliteSkyPlot.qml
+ SignalStrengthBar.qml
+ VehicleGPSIndicator.qml
+ RESOURCES
+ Images/Gps.svg
+ Images/GpsAuthentication.svg
+ Images/GpsInterference.svg
+ Images/RTK.svg
+ NO_PLUGIN
+)
+
+# ----------------------------------------------------------------------------
+# RTK Fact Metadata
+# ----------------------------------------------------------------------------
+qt_add_resources(${CMAKE_PROJECT_NAME} json_gps_rtk
+ PREFIX "/json/Vehicle"
+ FILES GPSRTKFact.json
+)
diff --git a/src/GPS/RTK/FixStatusDot.qml b/src/GPS/RTK/FixStatusDot.qml
new file mode 100644
index 000000000000..393dd85246e9
--- /dev/null
+++ b/src/GPS/RTK/FixStatusDot.qml
@@ -0,0 +1,40 @@
+import QtQuick
+import QtQuick.Layouts
+
+import QGroundControl
+import QGroundControl.Controls
+
+/// Colored circle indicating GPS fix or general status.
+/// Set either `lockValue` (uses fixTypeColor mapping) or `color` directly.
+Item {
+ id: root
+
+ property int lockValue: -1
+ property color statusColor: _resolvedColor
+ property real dotSize: ScreenTools.defaultFontPixelHeight * 0.6
+
+ Layout.preferredWidth: dotSize
+ Layout.preferredHeight: dotSize
+ Layout.alignment: Qt.AlignVCenter
+
+ implicitWidth: dotSize
+ implicitHeight: dotSize
+
+ QGCPalette { id: _pal }
+
+ property color _resolvedColor: {
+ if (lockValue < 0) return _pal.colorGrey
+ if (lockValue >= VehicleGPSFactGroup.FixRTKFixed) return _pal.colorGreen
+ if (lockValue >= VehicleGPSFactGroup.FixRTKFloat) return _pal.colorOrange
+ if (lockValue >= VehicleGPSFactGroup.Fix3D) return _pal.colorGreen
+ if (lockValue >= VehicleGPSFactGroup.Fix2D) return _pal.colorOrange
+ if (lockValue >= VehicleGPSFactGroup.FixNoFix) return _pal.colorRed
+ return _pal.colorGrey
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: width / 2
+ color: root.statusColor
+ }
+}
diff --git a/src/GPS/RTK/GNSSIntegrityAlert.qml b/src/GPS/RTK/GNSSIntegrityAlert.qml
new file mode 100644
index 000000000000..4a9a94475b57
--- /dev/null
+++ b/src/GPS/RTK/GNSSIntegrityAlert.qml
@@ -0,0 +1,48 @@
+import QtQuick
+
+import QGroundControl
+import QGroundControl.Controls
+
+Rectangle {
+ id: root
+
+ anchors.margins: -ScreenTools.defaultFontPixelHeight
+ height: alertColumn.height + ScreenTools.defaultFontPixelHeight
+ width: alertColumn.width + ScreenTools.defaultFontPixelWidth * 2
+ color: Qt.rgba(qgcPal.colorRed.r, qgcPal.colorRed.g, qgcPal.colorRed.b, 0.85)
+ radius: ScreenTools.defaultFontPixelWidth / 2
+ visible: _jamming || _spoofing
+
+ property var _gps: QGroundControl.multiVehicleManager.activeVehicle
+ ? QGroundControl.multiVehicleManager.activeVehicle.gps : null
+ property bool _jamming: _gps ? _gps.jammingState.value === VehicleGPSFactGroup.JammingDetected : false
+ property bool _spoofing: _gps ? _gps.spoofingState.value === VehicleGPSFactGroup.SpoofingDetected : false
+
+ QGCPalette { id: qgcPal; colorGroupEnabled: true }
+
+ Column {
+ id: alertColumn
+ anchors.centerIn: parent
+ spacing: ScreenTools.defaultFontPixelHeight * 0.5
+
+ QGCLabel {
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "white"
+ font.pointSize: ScreenTools.largeFontPointSize
+ font.bold: true
+ text: _jamming && _spoofing ? qsTr("GPS Jamming & Spoofing")
+ : _jamming ? qsTr("GPS Jamming Detected")
+ : qsTr("GPS Spoofing Detected")
+ }
+
+ QGCLabel {
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: "white"
+ font.pointSize: ScreenTools.mediumFontPointSize
+ width: ScreenTools.defaultFontPixelWidth * 40
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ text: qsTr("Vehicle GNSS integrity compromised. Position data may be unreliable.")
+ }
+ }
+}
diff --git a/src/GPS/RTK/GPSIndicator.qml b/src/GPS/RTK/GPSIndicator.qml
new file mode 100644
index 000000000000..2881b629a204
--- /dev/null
+++ b/src/GPS/RTK/GPSIndicator.qml
@@ -0,0 +1,154 @@
+import QtQuick
+import QtQuick.Layouts
+
+import QGroundControl
+import QGroundControl.Controls
+import "qrc:/qml/QGroundControl/GPS/GPSFormatHelpers.js" as GPSHelpers
+
+Item {
+ id: control
+ width: gpsIndicatorRow.width
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+
+ property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
+ property bool _rtkConnected: QGroundControl.gpsManager ? QGroundControl.gpsManager.deviceCount > 0 : false
+ property int _lockType: _activeVehicle ? _activeVehicle.gps.lock.rawValue : 0
+ property bool _ntripActive: QGroundControl.ntripManager.connectionStatus === NTRIPManager.Connected
+ property bool _correctionsActive: _ntripActive || _rtkConnected
+ property bool _vehicleHasRtk: _lockType >= VehicleGPSFactGroup.FixRTKFloat
+
+ property var _gpsAggregate: _activeVehicle ? _activeVehicle.gpsAggregate : null
+ property bool _hasInterference: {
+ if (!_gpsAggregate) return false
+ var jam = _gpsAggregate.jammingState.value
+ var spf = _gpsAggregate.spoofingState.value
+ return (jam >= VehicleGPSFactGroup.JammingMitigated && jam !== VehicleGPSFactGroup.JammingInvalid)
+ || (spf >= VehicleGPSFactGroup.SpoofingMitigated && spf !== VehicleGPSFactGroup.SpoofingInvalid)
+ }
+
+ QGCPalette { id: qgcPal }
+
+ property string _fixLabel: {
+ if (!_activeVehicle) return ""
+ if (_lockType >= VehicleGPSFactGroup.FixRTKFixed) return "RTK Fixed"
+ if (_lockType >= VehicleGPSFactGroup.FixRTKFloat) return "RTK Float"
+ if (_lockType >= VehicleGPSFactGroup.Fix3D) return "3D"
+ if (_lockType >= VehicleGPSFactGroup.Fix2D) return "2D"
+ return ""
+ }
+
+ Row {
+ id: gpsIndicatorRow
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ spacing: ScreenTools.defaultFontPixelWidth / 2
+
+ Row {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ spacing: -ScreenTools.defaultFontPixelWidth / 2
+
+ QGCLabel {
+ rotation: 90
+ text: qsTr("RTK")
+ color: qgcPal.text
+ anchors.verticalCenter: parent.verticalCenter
+ visible: _rtkConnected
+ }
+
+ Item {
+ width: gpsIcon.width
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+
+ QGCColoredImage {
+ id: gpsIcon
+ width: height
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ source: "qrc:/qml/QGroundControl/GPS/RTK/Images/Gps.svg"
+ fillMode: Image.PreserveAspectFit
+ sourceSize.height: height
+ opacity: (_activeVehicle && _activeVehicle.gps.count.value > 0) ? 1 : 0.5
+ color: !_activeVehicle ? (_correctionsActive ? qgcPal.colorGrey : qgcPal.text) : GPSHelpers.fixTypeColor(_lockType, qgcPal)
+ }
+
+ // Corrections active dot (bottom-right)
+ Rectangle {
+ width: ScreenTools.defaultFontPixelHeight * 0.45
+ height: width
+ radius: width / 2
+ color: _vehicleHasRtk ? qgcPal.colorGreen : qgcPal.colorOrange
+ border.color: qgcPal.window
+ border.width: 1
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: -width * 0.15
+ anchors.bottomMargin: parent.height * 0.1
+ visible: _correctionsActive
+ }
+
+ // Interference warning badge (top-right)
+ Rectangle {
+ width: ScreenTools.defaultFontPixelHeight * 0.5
+ height: width
+ radius: width / 2
+ color: qgcPal.colorRed
+ border.color: qgcPal.window
+ border.width: 1
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.rightMargin: -width * 0.15
+ anchors.topMargin: parent.height * 0.1
+ visible: _hasInterference
+
+ QGCLabel {
+ anchors.centerIn: parent
+ text: "!"
+ color: qgcPal.window
+ font.bold: true
+ font.pixelSize: parent.height * 0.7
+ }
+ }
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: _activeVehicle && _activeVehicle.gps.count.value > 0
+ spacing: 0
+
+ QGCLabel {
+ anchors.horizontalCenter: hdopValue.horizontalCenter
+ color: qgcPal.text
+ text: _activeVehicle ? _activeVehicle.gps.count.valueString : ""
+ }
+
+ QGCLabel {
+ id: hdopValue
+ color: qgcPal.text
+ text: _activeVehicle && !isNaN(_activeVehicle.gps.hdop.value) ? _activeVehicle.gps.hdop.value.toFixed(1) : ""
+ }
+ }
+
+ QGCLabel {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: _fixLabel !== ""
+ text: _fixLabel
+ font.pointSize: ScreenTools.smallFontPointSize
+ font.bold: true
+ color: !_activeVehicle ? (_correctionsActive ? qgcPal.colorGrey : qgcPal.text) : GPSHelpers.fixTypeColor(_lockType, qgcPal)
+ }
+ }
+
+ QGCMouseArea {
+ fillItem: parent
+ onClicked: mainWindow.showIndicatorDrawer(gpsIndicatorPage, control)
+ }
+
+ Component {
+ id: gpsIndicatorPage
+ GPSIndicatorPage { }
+ }
+}
diff --git a/src/GPS/RTK/GPSIndicatorPage.qml b/src/GPS/RTK/GPSIndicatorPage.qml
new file mode 100644
index 000000000000..384af5d2d13c
--- /dev/null
+++ b/src/GPS/RTK/GPSIndicatorPage.qml
@@ -0,0 +1,485 @@
+import QtQuick
+import QtQuick.Layouts
+
+import QGroundControl
+import QGroundControl.Controls
+import QGroundControl.FactControls
+import QGroundControl.GPS.NTRIP
+import "qrc:/qml/QGroundControl/GPS/GPSFormatHelpers.js" as GPSHelpers
+
+ToolIndicatorPage {
+ showExpand: true
+
+ property var activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
+ property var _ntripMgr: QGroundControl.ntripManager
+ property var _rtcmMavlink: QGroundControl.gpsManager ? QGroundControl.gpsManager.rtcmMavlink : null
+ property string na: qsTr("N/A", "No data to display")
+ property string valueNA: qsTr("-.--", "No data to display")
+
+ property bool correctionsActive: {
+ var ntripOn = _ntripMgr && _ntripMgr.connectionStatus === NTRIPManager.Connected
+ var rtkOn = QGroundControl.gpsManager && QGroundControl.gpsManager.deviceCount > 0
+ return ntripOn || rtkOn
+ }
+ property int _vehicleLock: activeVehicle ? activeVehicle.gps.lock.rawValue : 0
+ property bool vehicleHasRtk: _vehicleLock >= VehicleGPSFactGroup.FixRTKFloat
+ property int _correctionsSentSec: 0
+ property var _posMgr: QGroundControl.qgcPositionManger
+ property var _autoConnectSettings: QGroundControl.settingsManager.autoConnectSettings
+ property bool _gcsNmeaActive: _autoConnectSettings.autoConnectNmeaPort.valueString !== ""
+ && _autoConnectSettings.autoConnectNmeaPort.valueString !== "Disabled"
+
+ // RTK convergence tracking
+ property real convergenceTimeSec: -1
+ property real convergenceStart: 0
+
+ onCorrectionsActiveChanged: {
+ if (correctionsActive && !vehicleHasRtk) {
+ convergenceStart = Date.now() / 1000.0
+ convergenceTimeSec = -1
+ }
+ }
+ onVehicleHasRtkChanged: {
+ if (vehicleHasRtk && convergenceStart > 0) {
+ convergenceTimeSec = (Date.now() / 1000.0) - convergenceStart
+ convergenceStart = 0
+ }
+ }
+
+ function _hasResilienceData(gps) {
+ if (!gps) return false
+ var jam = gps.jammingState.value
+ var spf = gps.spoofingState.value
+ var auth = gps.authenticationState.value
+ return (jam > VehicleGPSFactGroup.JammingUnknown && jam !== VehicleGPSFactGroup.JammingInvalid)
+ || (spf > VehicleGPSFactGroup.SpoofingUnknown && spf !== VehicleGPSFactGroup.SpoofingInvalid)
+ || (auth > VehicleGPSFactGroup.AuthUnknown && auth !== VehicleGPSFactGroup.AuthInvalid)
+ }
+
+ function _isJammingVisible(gps) {
+ if (!gps) return false
+ var v = gps.jammingState.value
+ return v > VehicleGPSFactGroup.JammingUnknown && v !== VehicleGPSFactGroup.JammingInvalid
+ }
+
+ function _isSpoofingVisible(gps) {
+ if (!gps) return false
+ var v = gps.spoofingState.value
+ return v > VehicleGPSFactGroup.SpoofingUnknown && v !== VehicleGPSFactGroup.SpoofingInvalid
+ }
+
+ function _isAuthVisible(gps) {
+ if (!gps) return false
+ var v = gps.authenticationState.value
+ return v > VehicleGPSFactGroup.AuthUnknown && v !== VehicleGPSFactGroup.AuthInvalid
+ }
+
+ function _formatAccuracy(rawVal) { return GPSHelpers.formatAccuracy(rawVal, valueNA) }
+
+ // ====================================================================
+ // Content: essential status at a glance
+ // ====================================================================
+ contentComponent: Component {
+ ColumnLayout {
+ spacing: ScreenTools.defaultFontPixelHeight / 2
+
+ // -- Vehicle GPS Status ------------------------------------------
+ GPSStatusBlock {
+ heading: qsTr("Vehicle GPS")
+ gps: activeVehicle ? activeVehicle.gps : null
+ visible: !!activeVehicle
+ }
+
+ // -- RTK Corrections Status --------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("RTK Corrections")
+ showDividers: true
+ visible: activeVehicle && correctionsActive
+
+ Timer {
+ id: correctionTimer
+ interval: 1000
+ running: correctionsActive && !vehicleHasRtk
+ repeat: true
+ onTriggered: _correctionsSentSec++
+ onRunningChanged: _correctionsSentSec = 0
+ }
+
+ RowLayout {
+ spacing: ScreenTools.defaultFontPixelWidth
+
+ FixStatusDot {
+ statusColor: {
+ if (_vehicleLock >= VehicleGPSFactGroup.FixRTKFixed) return qgcPal.colorGreen
+ if (_vehicleLock >= VehicleGPSFactGroup.FixRTKFloat) return qgcPal.colorOrange
+ if (correctionsActive && _correctionsSentSec > 30) return qgcPal.colorRed
+ if (correctionsActive) return qgcPal.colorOrange
+ return qgcPal.colorGrey
+ }
+ }
+
+ QGCLabel {
+ text: {
+ if (_vehicleLock >= VehicleGPSFactGroup.FixRTKFixed) return qsTr("RTK Fixed")
+ if (_vehicleLock >= VehicleGPSFactGroup.FixRTKFloat) return qsTr("RTK Float")
+ if (correctionsActive && _correctionsSentSec > 30) return qsTr("No RTK Fix")
+ if (correctionsActive) return qsTr("Waiting for RTK...")
+ return qsTr("No Corrections")
+ }
+ }
+ }
+
+ LabelledLabel {
+ label: qsTr("Source")
+ labelText: {
+ var sources = []
+ if (_ntripMgr && _ntripMgr.connectionStatus === NTRIPManager.Connected)
+ sources.push(qsTr("NTRIP"))
+ if (QGroundControl.gpsManager && QGroundControl.gpsManager.deviceCount > 0)
+ sources.push(qsTr("RTK Base"))
+ return sources.join(" + ")
+ }
+ }
+
+ LabelledLabel {
+ label: qsTr("Correction Age")
+ labelText: {
+ var stats = _ntripMgr ? _ntripMgr.connectionStats : null
+ if (!stats || stats.correctionAgeSec < 0) return valueNA
+ var age = stats.correctionAgeSec
+ if (age < 1.0) return qsTr("< 1 s")
+ return age.toFixed(0) + " s"
+ }
+ visible: _ntripMgr && _ntripMgr.connectionStatus === NTRIPManager.Connected
+ }
+
+ QGCLabel {
+ text: qsTr("Corrections are being sent but vehicle has not achieved RTK fix. Check base station accuracy, satellite count, and signal quality.")
+ wrapMode: Text.WordWrap
+ color: qgcPal.colorOrange
+ font.pointSize: ScreenTools.smallFontPointSize
+ visible: correctionsActive && !vehicleHasRtk && _correctionsSentSec > 30
+ Layout.fillWidth: true
+ }
+ }
+
+ // -- Hints -------------------------------------------------------
+ QGCLabel {
+ Layout.fillWidth: true
+ visible: activeVehicle && !correctionsActive && !vehicleHasRtk
+ text: qsTr("Connect an RTK base station or NTRIP caster for centimeter-level accuracy.")
+ wrapMode: Text.WordWrap
+ font.pointSize: ScreenTools.smallFontPointSize
+ color: qgcPal.colorGrey
+ }
+
+ // -- No vehicle placeholder --------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("Vehicle GPS")
+ visible: !activeVehicle
+
+ QGCLabel {
+ text: qsTr("No vehicle connected")
+ color: qgcPal.colorGrey
+ }
+ }
+ }
+ }
+
+ // ====================================================================
+ // Expanded: detailed diagnostics
+ // ====================================================================
+ expandedComponent: Component {
+ ColumnLayout {
+ spacing: ScreenTools.defaultFontPixelHeight / 2
+
+ // -- Vehicle GPS Details -----------------------------------------
+ GPSStatusBlock {
+ heading: qsTr("Vehicle GPS Details")
+ gps: activeVehicle ? activeVehicle.gps : null
+ showDetails: true
+ visible: !!activeVehicle
+ }
+
+ // -- Vehicle RTK Status -------------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("Vehicle RTK Status")
+ showDividers: true
+ visible: activeVehicle && activeVehicle.gps.rtkRate.rawValue > 0
+
+ LabelledLabel {
+ label: qsTr("Baseline")
+ labelText: activeVehicle ? _formatAccuracy(activeVehicle.gps.rtkBaseline.rawValue) : valueNA
+ visible: activeVehicle && !isNaN(activeVehicle.gps.rtkBaseline.rawValue)
+ }
+
+ LabelledLabel {
+ label: qsTr("Accuracy")
+ labelText: activeVehicle ? _formatAccuracy(activeVehicle.gps.rtkAccuracy.rawValue) : valueNA
+ visible: activeVehicle && !isNaN(activeVehicle.gps.rtkAccuracy.rawValue)
+ }
+
+ LabelledLabel {
+ label: qsTr("RTK Satellites")
+ labelText: activeVehicle ? activeVehicle.gps.rtkNumSats.valueString : na
+ }
+
+ LabelledLabel {
+ label: qsTr("Baseline Rate")
+ labelText: activeVehicle ? (activeVehicle.gps.rtkRate.valueString + " Hz") : valueNA
+ }
+
+ LabelledLabel {
+ label: qsTr("Ambiguity (IAR)")
+ labelText: activeVehicle ? activeVehicle.gps.rtkIAR.valueString : na
+ visible: activeVehicle && activeVehicle.gps.rtkIAR.rawValue > 0
+ }
+
+ LabelledLabel {
+ label: qsTr("Convergence Time")
+ labelText: convergenceTimeSec >= 0 ? GPSHelpers.formatDuration(Math.round(convergenceTimeSec)) : qsTr("Converging...")
+ visible: vehicleHasRtk || (correctionsActive && convergenceStart > 0)
+ }
+ }
+
+ // -- RTCM Bandwidth ----------------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("RTCM Data")
+ showDividers: true
+ visible: _rtcmMavlink && _rtcmMavlink.totalBytesSent > 0
+
+ LabelledLabel {
+ label: qsTr("Bandwidth")
+ labelText: _rtcmMavlink ? (_rtcmMavlink.bandwidthKBps.toFixed(1) + " KB/s") : valueNA
+ }
+
+ LabelledLabel {
+ label: qsTr("Total Sent")
+ labelText: _rtcmMavlink ? GPSHelpers.formatDataSize(_rtcmMavlink.totalBytesSent) : valueNA
+ }
+ }
+
+ // -- Vehicle GPS 2 -----------------------------------------------
+ GPSStatusBlock {
+ heading: qsTr("Vehicle GPS 2")
+ gps: activeVehicle ? activeVehicle.gps2 : null
+ showDetails: true
+ visible: !!activeVehicle && !!activeVehicle.gps2 && activeVehicle.gps2.telemetryAvailable
+ }
+
+ // -- RTK Base Station Status (per-device) ------------------------
+ Repeater {
+ model: QGroundControl.gpsManager ? QGroundControl.gpsManager.devices : null
+
+ RTKBaseStationStatus {
+ required property var object
+ device: object
+ }
+ }
+
+ // -- NTRIP Status ------------------------------------------------
+ NTRIPConnectionStatus {
+ rtcmMavlink: _rtcmMavlink
+ }
+
+ // -- Ground Station Position -------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("Ground Station")
+ showDividers: true
+ property bool gcsRtkActive: Boolean(QGroundControl.gpsManager) && QGroundControl.gpsManager.deviceCount > 0
+ visible: Boolean(_posMgr) && (_posMgr.gcsPosition.isValid || !!_gcsNmeaActive || gcsRtkActive)
+
+ property bool posValid: !!_posMgr && _posMgr.gcsPosition.isValid
+
+ RowLayout {
+ spacing: ScreenTools.defaultFontPixelWidth
+ visible: Boolean(!parent.posValid && (!!_gcsNmeaActive || parent.gcsRtkActive))
+
+ FixStatusDot { statusColor: qgcPal.colorOrange }
+
+ QGCLabel {
+ text: parent.gcsRtkActive ? qsTr("Waiting for RTK GPS position...") : qsTr("Waiting for position data...")
+ }
+ }
+
+ LabelledLabel {
+ label: qsTr("Source")
+ labelText: _posMgr && _posMgr.sourceDescription ? _posMgr.sourceDescription : na
+ visible: _posMgr && _posMgr.sourceDescription !== ""
+ }
+
+ LabelledLabel {
+ label: qsTr("Position")
+ labelText: {
+ var mgr = _posMgr
+ if (!mgr || !mgr.gcsPosition.isValid) return valueNA
+ return GPSHelpers.formatCoordinate(mgr.gcsPosition.latitude, mgr.gcsPosition.longitude)
+ }
+ visible: !!parent.posValid
+ }
+
+ LabelledLabel {
+ label: qsTr("Accuracy")
+ labelText: {
+ var mgr = _posMgr
+ if (!mgr || !isFinite(mgr.gcsPositionHorizontalAccuracy)) return valueNA
+ return _formatAccuracy(mgr.gcsPositionHorizontalAccuracy)
+ }
+ visible: !!parent.posValid && !!_posMgr && isFinite(_posMgr.gcsPositionHorizontalAccuracy)
+ }
+ }
+
+ // -- GCS Receiver Satellites --------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("GCS Receiver Satellites")
+ showDividers: true
+ visible: _posMgr && _posMgr.gcsSatellitesInView > 0
+
+ SatelliteSkyPlot {
+ satelliteModel: _posMgr ? _posMgr.gcsSatelliteModel : null
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 14
+ Layout.preferredHeight: Layout.preferredWidth
+ visible: _posMgr && _posMgr.gcsSatelliteModel
+ && _posMgr.gcsSatelliteModel.rowCount() > 0
+ }
+
+ LabelledLabel {
+ label: qsTr("In View / In Use")
+ labelText: _posMgr ? (_posMgr.gcsSatellitesInView + " / " + _posMgr.gcsSatellitesInUse) : na
+ }
+
+ LabelledLabel {
+ label: qsTr("Constellations")
+ labelText: _posMgr && _posMgr.gcsSatelliteModel
+ ? _posMgr.gcsSatelliteModel.constellationSummary
+ : na
+ visible: _posMgr && _posMgr.gcsSatelliteModel
+ && _posMgr.gcsSatelliteModel.constellationSummary !== ""
+ }
+
+ Repeater {
+ model: _posMgr ? _posMgr.gcsSatelliteModel : null
+
+ RowLayout {
+ spacing: ScreenTools.defaultFontPixelWidth
+
+ QGCLabel {
+ text: model.systemName
+ font.pointSize: ScreenTools.smallFontPointSize
+ Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 8
+ }
+
+ QGCLabel {
+ text: "PRN " + model.identifier
+ font.pointSize: ScreenTools.smallFontPointSize
+ Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 8
+ }
+
+ Rectangle {
+ width: Math.max(2, Math.min(model.signalStrength, 50) * ScreenTools.defaultFontPixelWidth * 0.12)
+ height: ScreenTools.defaultFontPixelHeight * 0.5
+ color: model.inUse ? qgcPal.colorGreen : qgcPal.colorGrey
+ radius: 2
+ Layout.alignment: Qt.AlignVCenter
+ }
+
+ QGCLabel {
+ text: model.signalStrength + " dB"
+ font.pointSize: ScreenTools.smallFontPointSize
+ Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 6
+ }
+
+ QGCLabel {
+ text: model.inUse ? qsTr("In Use") : ""
+ font.pointSize: ScreenTools.smallFontPointSize
+ font.bold: true
+ color: qgcPal.colorGreen
+ }
+ }
+ }
+ }
+
+ // -- GPS Event Log -----------------------------------------------
+ SettingsGroupLayout {
+ heading: qsTr("Event Log")
+ showDividers: true
+ visible: QGroundControl.gpsEventModel && QGroundControl.gpsEventModel.count > 0
+
+ QGCListView {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.min(contentHeight, ScreenTools.defaultFontPixelHeight * 12)
+ clip: true
+ model: QGroundControl.gpsEventModel
+ spacing: ScreenTools.defaultFontPixelHeight * 0.15
+
+ delegate: RowLayout {
+ width: ListView.view.width
+ spacing: ScreenTools.defaultFontPixelWidth
+
+ FixStatusDot {
+ dotSize: ScreenTools.defaultFontPixelHeight * 0.5
+ statusColor: model.severity === "Error" ? qgcPal.colorRed
+ : model.severity === "Warning" ? qgcPal.colorOrange
+ : qgcPal.colorGreen
+ }
+
+ QGCLabel {
+ text: "[%1] %2".arg(model.source).arg(model.message)
+ font.pointSize: ScreenTools.smallFontPointSize
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
+ }
+ }
+
+ // -- Signal Integrity --------------------------------------------
+ SettingsGroupLayout {
+ property var _gps: activeVehicle ? activeVehicle.gps : null
+ visible: _gps && _hasResilienceData(_gps)
+ heading: qsTr("GPS 1 Signal Integrity")
+ showDividers: true
+
+ LabelledLabel {
+ label: qsTr("Jamming")
+ labelText: parent._gps ? (parent._gps.jammingState.enumStringValue || na) : na
+ visible: _isJammingVisible(parent._gps)
+ }
+ LabelledLabel {
+ label: qsTr("Spoofing")
+ labelText: parent._gps ? (parent._gps.spoofingState.enumStringValue || na) : na
+ visible: _isSpoofingVisible(parent._gps)
+ }
+ LabelledLabel {
+ label: qsTr("Authentication")
+ labelText: parent._gps ? (parent._gps.authenticationState.enumStringValue || na) : na
+ visible: _isAuthVisible(parent._gps)
+ }
+ }
+
+ SettingsGroupLayout {
+ property var _gps: activeVehicle ? activeVehicle.gps2 : null
+ visible: _gps && _hasResilienceData(_gps)
+ heading: qsTr("GPS 2 Signal Integrity")
+ showDividers: true
+
+ LabelledLabel {
+ label: qsTr("Jamming")
+ labelText: parent._gps ? (parent._gps.jammingState.enumStringValue || na) : na
+ visible: _isJammingVisible(parent._gps)
+ }
+ LabelledLabel {
+ label: qsTr("Spoofing")
+ labelText: parent._gps ? (parent._gps.spoofingState.enumStringValue || na) : na
+ visible: _isSpoofingVisible(parent._gps)
+ }
+ LabelledLabel {
+ label: qsTr("Authentication")
+ labelText: parent._gps ? (parent._gps.authenticationState.enumStringValue || na) : na
+ visible: _isAuthVisible(parent._gps)
+ }
+ }
+ }
+ }
+}
diff --git a/src/GPS/RTK/GPSPositionSource.cc b/src/GPS/RTK/GPSPositionSource.cc
new file mode 100644
index 000000000000..80e1d3ad21ff
--- /dev/null
+++ b/src/GPS/RTK/GPSPositionSource.cc
@@ -0,0 +1,114 @@
+#include "GPSPositionSource.h"
+#include "QGCLoggingCategory.h"
+
+#include
+#include
+
+#include
+
+QGC_LOGGING_CATEGORY(GPSPositionSourceLog, "GPS.GPSPositionSource")
+
+GPSPositionSource::GPSPositionSource(QObject *parent)
+ : QGeoPositionInfoSource(parent)
+{
+ qCDebug(GPSPositionSourceLog) << this;
+}
+
+GPSPositionSource::~GPSPositionSource()
+{
+ qCDebug(GPSPositionSourceLog) << this;
+}
+
+QGeoPositionInfo GPSPositionSource::lastKnownPosition(bool) const
+{
+ return _lastPosition;
+}
+
+QGeoPositionInfoSource::PositioningMethods GPSPositionSource::supportedPositioningMethods() const
+{
+ return SatellitePositioningMethods;
+}
+
+int GPSPositionSource::minimumUpdateInterval() const
+{
+ return kMinUpdateIntervalMs;
+}
+
+QGeoPositionInfoSource::Error GPSPositionSource::error() const
+{
+ return QGeoPositionInfoSource::NoError;
+}
+
+void GPSPositionSource::startUpdates()
+{
+ _running = true;
+}
+
+void GPSPositionSource::stopUpdates()
+{
+ _running = false;
+}
+
+void GPSPositionSource::requestUpdate(int)
+{
+ if (_lastPosition.isValid()) {
+ emit positionUpdated(_lastPosition);
+ }
+}
+
+void GPSPositionSource::updateFromSensorGps(const sensor_gps_s &gps)
+{
+ if (!_running) {
+ return;
+ }
+
+ if (gps.fix_type < sensor_gps_s::FIX_TYPE_2D) {
+ return;
+ }
+
+ QGeoCoordinate coord(gps.latitude_deg, gps.longitude_deg);
+ if (gps.fix_type >= sensor_gps_s::FIX_TYPE_3D) {
+ coord.setAltitude(gps.altitude_msl_m);
+ }
+
+ QDateTime timestamp;
+ if (gps.time_utc_usec > 0) {
+ timestamp = QDateTime::fromMSecsSinceEpoch(
+ static_cast(gps.time_utc_usec / 1000), QTimeZone::UTC);
+ } else {
+ timestamp = QDateTime::currentDateTimeUtc();
+ }
+
+ QGeoPositionInfo info(coord, timestamp);
+
+ if (gps.eph > 0 && std::isfinite(gps.eph)) {
+ info.setAttribute(QGeoPositionInfo::HorizontalAccuracy, static_cast(gps.eph));
+ }
+
+ if (gps.epv > 0 && std::isfinite(gps.epv)) {
+ info.setAttribute(QGeoPositionInfo::VerticalAccuracy, static_cast(gps.epv));
+ }
+
+ if (gps.vel_m_s >= 0 && std::isfinite(gps.vel_m_s)) {
+ info.setAttribute(QGeoPositionInfo::GroundSpeed, static_cast(gps.vel_m_s));
+ }
+
+ if (std::isfinite(gps.vel_d_m_s)) {
+ // NED convention: vel_d_m_s is positive downward, Qt expects positive upward
+ info.setAttribute(QGeoPositionInfo::VerticalSpeed, static_cast(-gps.vel_d_m_s));
+ }
+
+ if (std::isfinite(gps.cog_rad)) {
+ double deg = static_cast(gps.cog_rad) * 180.0 / M_PI;
+ if (deg < 0.0) deg += 360.0;
+ info.setAttribute(QGeoPositionInfo::Direction, deg);
+ }
+
+ if (gps.heading_accuracy > 0 && std::isfinite(gps.heading_accuracy)) {
+ info.setAttribute(QGeoPositionInfo::DirectionAccuracy,
+ static_cast