From 641239787969fbc768b2e2f131c78629bfa1cafa Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 26 Jan 2026 21:03:47 +0100 Subject: [PATCH 01/14] WIP Add network position provider This commit adds both UDP & TCP sockets under one network provider. We further differ external provider into bluetooth and network providers. --- app/CMakeLists.txt | 2 + app/appsettings.cpp | 8 +- app/position/positionkit.cpp | 74 ++++---- .../providers/bluetoothpositionprovider.cpp | 2 +- .../providers/networkpositionprovider.cpp | 168 ++++++++++++++++++ .../providers/networkpositionprovider.h | 64 +++++++ .../providers/positionprovidersmodel.cpp | 11 +- .../providers/positionprovidersmodel.h | 2 +- app/qml/gps/MMGpsDataDrawer.qml | 12 +- app/qml/gps/MMPositionProviderPage.qml | 97 +++++++++- app/qml/map/MMMapController.qml | 4 +- app/test/testposition.cpp | 10 +- app/test/testvariablesmanager.cpp | 2 +- gallery/positionkit.h | 2 +- 14 files changed, 397 insertions(+), 61 deletions(-) create mode 100644 app/position/providers/networkpositionprovider.cpp create mode 100644 app/position/providers/networkpositionprovider.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e49bfd223..52e1591de 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -33,6 +33,7 @@ set(MM_SRCS ios/iosutils.cpp position/providers/abstractpositionprovider.cpp position/providers/internalpositionprovider.cpp + position/providers/networkpositionprovider.cpp position/providers/positionprovidersmodel.cpp position/providers/simulatedpositionprovider.cpp position/tracking/abstracttrackingbackend.cpp @@ -123,6 +124,7 @@ set(MM_HDRS ios/iosutils.h position/providers/abstractpositionprovider.h position/providers/internalpositionprovider.h + position/providers/networkpositionprovider.h position/providers/positionprovidersmodel.h position/providers/simulatedpositionprovider.h position/tracking/abstracttrackingbackend.h diff --git a/app/appsettings.cpp b/app/appsettings.cpp index 58e340ec1..e781e108a 100644 --- a/app/appsettings.cpp +++ b/app/appsettings.cpp @@ -215,6 +215,7 @@ QVariantList AppSettings::savedPositionProviders() const QStringList provider; provider << settings.value( "providerName" ).toString(); provider << settings.value( "providerAddress" ).toString(); + provider << settings.value( "providerType" ).toString(); providers.push_back( provider ); } @@ -238,15 +239,16 @@ void AppSettings::savePositionProviders( const QVariantList &providers ) { QVariantList provider = providers[i].toList(); - if ( provider.length() < 2 ) + if ( provider.length() < 3 ) { CoreUtils::log( QStringLiteral( "AppSettings" ), QStringLiteral( "Tried to save provider without sufficient data" ) ); continue; } settings.setArrayIndex( i ); - settings.setValue( "providerName", providers[i].toList()[0] ); - settings.setValue( "providerAddress", providers[i].toList()[1] ); + settings.setValue( "providerName", provider[0] ); + settings.setValue( "providerAddress", provider[1] ); + settings.setValue( "providerType", provider[2] ); } settings.endArray(); } diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 92e3ddae0..f923f591b 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -18,6 +18,7 @@ #include "position/providers/internalpositionprovider.h" #include "position/providers/simulatedpositionprovider.h" +#include "providers/networkpositionprovider.h" #ifdef ANDROID #include "position/providers/androidpositionprovider.h" #include @@ -87,13 +88,12 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c { QString providerType( type ); - // currently the only external provider is bluetooth, so manually set internal provider for platforms that - // do not support reading bluetooth serial + // set internal provider for platforms that do not support reading bluetooth serial #ifndef HAVE_BLUETOOTH providerType = QStringLiteral( "internal" ); #endif - if ( providerType == QStringLiteral( "external" ) ) + if ( providerType == QStringLiteral( "external_bt" ) ) { #ifdef HAVE_BLUETOOTH AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name ); @@ -101,39 +101,47 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c return provider; #endif } - else // type == internal + + if ( providerType == QStringLiteral( "external_ip" ) ) { - if ( id == QStringLiteral( "simulated" ) ) - { - AbstractPositionProvider *provider = new SimulatedPositionProvider(); - QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); - return provider; - } + AbstractPositionProvider *provider = new NetworkPositionProvider( id, name ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } + + // type == internal + if ( id == QStringLiteral( "simulated" ) ) + { + AbstractPositionProvider *provider = new SimulatedPositionProvider(); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } + #ifdef ANDROID - else if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) + if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) + { + bool fused = ( id == QStringLiteral( "android_fused" ) ); + if ( fused && !AndroidPositionProvider::isFusedAvailable() ) { - bool fused = ( id == QStringLiteral( "android_fused" ) ); - if ( fused && !AndroidPositionProvider::isFusedAvailable() ) - { - // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? + // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? - // fallback to the default - at this point the Qt Positioning implementation - AbstractPositionProvider *provider = new InternalPositionProvider(); - QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); - return provider; - } - __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); - AbstractPositionProvider *provider = new AndroidPositionProvider( fused ); - QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); - return provider; - } -#endif - else // id == devicegps - { + // fallback to the default - at this point the Qt Positioning implementation AbstractPositionProvider *provider = new InternalPositionProvider(); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } + __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); + AbstractPositionProvider *provider = new AndroidPositionProvider( fused ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } +#endif + + // id == devicegps + { + AbstractPositionProvider *provider = new InternalPositionProvider(); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; } } @@ -162,8 +170,10 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app } else { - // find name of the active provider + // find name & type of the active provider QString providerName; + // Migration from single external provider to multiple currently, missing type == bluetooth provider + QString providerType = QStringLiteral( "external_bt" ); QVariantList providers = appsettings->savedPositionProviders(); for ( const auto &provider : providers ) @@ -178,10 +188,14 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app if ( providerData[1] == providerId ) { providerName = providerData[0].toString(); + if ( !providerData.at( 3 ).isNull() ) + { + providerType = providerData[3].toString(); + } } } - return constructProvider( QStringLiteral( "external" ), providerId, providerName ); + return constructProvider( providerType, providerId, providerName ); } } diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index 678455c11..7914ac381 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -28,7 +28,7 @@ QgsGpsInformation NmeaParser::parseNmeaString( const QString &nmeastring ) } BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external" ), name, parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, parent ) , mTargetAddress( addr ) { mSocket = std::unique_ptr( new QBluetoothSocket( QBluetoothServiceInfo::RfcommProtocol ) ); diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp new file mode 100644 index 000000000..d96b3bcc6 --- /dev/null +++ b/app/position/providers/networkpositionprovider.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** +* * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "networkpositionprovider.h" + +#include "coreutils.h" + +static int ONE_SECOND_MS = 1000; + +NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ) +{ + const QStringList targetAddress = addr.split( ":" ); + mTargetAddress = targetAddress.at( 0 ); + mTargetPort = targetAddress.at( 1 ).toInt(); + + mTcpSocket = std::make_unique(); + mUdpSocket = std::make_unique(); + +// only one socket will be active depending on, which will find host, the other will be closed +// connect(mTcpSocket.get(), &QAbstractSocket::hostFound, this, [this] +// { +// CoreUtils::log("NetworkPositionProvider", "TCP socket found host, aborting UDP socket"); +// mUdpSocket->abort(); +// }); +// connect(mUdpSocket.get(), &QAbstractSocket::hostFound, this, [this] +// { +// CoreUtils::log("NetworkPositionProvider", "UDP socket found host, aborting TCP socket"); +// mTcpSocket->abort(); +// }); + + connect( mTcpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + connect( mUdpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + + connect( mTcpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + connect( mUdpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + + mReconnectTimer.setSingleShot( false ); + mReconnectTimer.setInterval( ONE_SECOND_MS ); + connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); + + NetworkPositionProvider::startUpdates(); +} + +void NetworkPositionProvider::startUpdates() +{ + CoreUtils::log( "NetworkPositionProvider", "Connecting to host..." ); + mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); + mUdpSocket->connectToHost( mTargetAddress, mTargetPort ); +} + +void NetworkPositionProvider::stopUpdates() +{ + if ( mTcpSocket->state() != QAbstractSocket::UnconnectedState && mTcpSocket->state() != QAbstractSocket::ClosingState ) + { + mTcpSocket->disconnectFromHost(); + } + if ( mUdpSocket->state() != QAbstractSocket::UnconnectedState && mUdpSocket->state() != QAbstractSocket::ClosingState ) + { + mUdpSocket->disconnectFromHost(); + } +} + +void NetworkPositionProvider::closeProvider() +{ + mTcpSocket->close(); + mUdpSocket->close(); +} + +void NetworkPositionProvider::positionUpdateReceived() +{ + const QAbstractSocket *socket = dynamic_cast( sender() ); + const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); + CoreUtils::log( "NetworkPositionProvider", QStringLiteral( "%1 socket has received new data!" ).arg( socketTypeToString ) ); + if ( mTcpSocket->isValid() || mUdpSocket->isValid() ) + { + const QByteArray rawNmeaData = activeSocket()->readAll(); + const QString nmeaData( rawNmeaData ); + const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + + emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + } +} + +void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketState state ) +{ + if ( state == QAbstractSocket::SocketState::ConnectingState || state == QAbstractSocket::SocketState::HostLookupState ) + { + setState( tr( "Connecting to %1" ).arg( mProviderName ), State::Connecting ); + } + else if ( state == QAbstractSocket::SocketState::ConnectedState ) + { + setState( tr( "Connected" ), State::Connected ); + } + else if ( state == QAbstractSocket::SocketState::UnconnectedState ) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + const QAbstractSocket *socket = dynamic_cast( sender() ); + const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); + const QString stateToString = QMetaEnum::fromType().valueToKey( state ); + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "%1 Socket changed state, code: %2" ).arg( socketTypeToString, stateToString ) ); +} + +void NetworkPositionProvider::reconnectTimeout() +{ + if ( mSecondsLeftToReconnect <= 1 ) + { + reconnect(); + } + else + { + mSecondsLeftToReconnect--; + setState( tr( "No connection, reconnecting in (%1)" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + mReconnectTimer.start(); + } +} + +QAbstractSocket *NetworkPositionProvider::activeSocket() const +{ + if ( mTcpSocket->state() >= QAbstractSocket::ConnectingState && mTcpSocket->state() <= QAbstractSocket::BoundState ) + { + return mTcpSocket.get(); + } + + if ( mUdpSocket->state() >= QAbstractSocket::ConnectingState && mUdpSocket->state() <= QAbstractSocket::BoundState ) + { + return mUdpSocket.get(); + } + +//TODO: wouldn't it be better to guard against nullptr in caller and return nullptr here? + return new QAbstractSocket( QAbstractSocket::UnknownSocketType, nullptr ); +} + +void NetworkPositionProvider::reconnect() +{ + mReconnectTimer.stop(); + + setState( tr( "Reconnecting" ), State::Connecting ); + + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "Reconnecting to %1" ).arg( mProviderName ) ); + + stopUpdates(); + startUpdates(); +} + +void NetworkPositionProvider::startReconnectTimer() +{ + mSecondsLeftToReconnect = mReconnectDelay / ONE_SECOND_MS; + setState( tr( "No connection, reconnecting in (%1)" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + + mReconnectTimer.start(); + +// first time do reconnect in short time, then each other in long time + if ( mReconnectDelay == NetworkPositionProvider::ShortDelay ) + { + mReconnectDelay = NetworkPositionProvider::LongDelay; + } +} diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h new file mode 100644 index 000000000..5a8755417 --- /dev/null +++ b/app/position/providers/networkpositionprovider.h @@ -0,0 +1,64 @@ +/*************************************************************************** +* * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + + +#ifndef NETWORKPOSITIONPROVIDER_H +#define NETWORKPOSITIONPROVIDER_H + +#include +#include + +#include "abstractpositionprovider.h" +#include "bluetoothpositionprovider.h" + + +class NetworkPositionProvider : public AbstractPositionProvider +{ + Q_OBJECT + +// signalizes in how many [ms] we will try to reconnect to GPS again + enum ReconnectDelay + { + ShortDelay = 3000, + LongDelay = 5000 + }; + + public: + explicit NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); + + void startUpdates() override; + void stopUpdates() override; + void closeProvider() override; + + public slots: + void positionUpdateReceived(); + void socketStateChanged( QAbstractSocket::SocketState state ); + void reconnectTimeout(); + + private: +// utility function, which should return the socket in use + QAbstractSocket *activeSocket() const; +// trigger the reconnection flow for both sockets + void reconnect(); + // start the reconnection timeout + void startReconnectTimer(); + + std::unique_ptr mTcpSocket; + std::unique_ptr mUdpSocket; + + int mReconnectDelay = ReconnectDelay::ShortDelay; // in how many [ms] we will try to reconnect again + int mSecondsLeftToReconnect; // how many seconds are left to reconnect. Reconnects if less than or equal to one + QTimer mReconnectTimer; // timer that times out each second and lowers the mSecondsLeftToReconnect by one + + QString mTargetAddress; // IP address or hostname of the receiver + int mTargetPort; // active port of the receiver + + NmeaParser mNmeaParser; // parser to decode received NMEA strings +}; +#endif //NETWORKPOSITIONPROVIDER_H diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index e070e4576..b51373f2b 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -134,16 +134,17 @@ void PositionProvidersModel::removeProvider( const QString &providerId ) } } -void PositionProvidersModel::addProvider( const QString &name, const QString &providerId ) +void PositionProvidersModel::addProvider( const QString &name, const QString &providerId, const QString &providerType ) { if ( providerId.isEmpty() ) return; PositionProvider toAdd; + const QString deviceDesc = providerType == QStringLiteral( "external_bt" ) ? tr( " Bluetooth device" ) : tr( " Network device" ); toAdd.name = name; toAdd.providerId = providerId; - toAdd.description = providerId + " " + tr( " Bluetooth device" ); - toAdd.providerType = "external"; + toAdd.description = providerId + " " + deviceDesc; + toAdd.providerType = providerType; if ( mProviders.contains( toAdd ) ) return; @@ -218,8 +219,8 @@ QVariantList PositionProvidersModel::toVariantList() const if ( mProviders[i].providerType == QStringLiteral( "internal" ) ) continue; - QStringList a = { mProviders[i].name, mProviders[i].providerId }; - out.push_back( a ); + QStringList provider = { mProviders[i].name, mProviders[i].providerId, mProviders[i].providerType }; + out.push_back( provider ); } return out; diff --git a/app/position/providers/positionprovidersmodel.h b/app/position/providers/positionprovidersmodel.h index dc24171fe..a05589504 100644 --- a/app/position/providers/positionprovidersmodel.h +++ b/app/position/providers/positionprovidersmodel.h @@ -67,7 +67,7 @@ class PositionProvidersModel : public QAbstractListModel QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; Q_INVOKABLE void removeProvider( const QString &providerId ); - Q_INVOKABLE void addProvider( const QString &providerName, const QString &providerId ); + Q_INVOKABLE void addProvider( const QString &providerName, const QString &providerId, const QString &providerType ); AppSettings *appSettings() const; void setAppSettings( AppSettings * ); diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index b9b600c39..1574b5011 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -72,7 +72,7 @@ MMComponents.MMDrawer { width: parent.width / 2 - visible: __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") title: qsTr( "Status" ) value: __positionKit.positionProvider ? __positionKit.positionProvider.stateMessage : "" @@ -212,7 +212,7 @@ MMComponents.MMDrawer { __positionKit.fix } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -245,7 +245,7 @@ MMComponents.MMDrawer { __inputUtils.formatNumber( __positionKit.hdop, 2 ) } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -262,7 +262,7 @@ MMComponents.MMDrawer { __inputUtils.formatNumber( __positionKit.vdop, 2 ) } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -279,7 +279,7 @@ MMComponents.MMDrawer { __inputUtils.formatNumber( __positionKit.pdop, 2 ) } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -327,7 +327,7 @@ MMComponents.MMDrawer { } __inputUtils.formatNumber( __positionKit.geoidSeparation, 2 ) + " m" } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 9f3b501f4..6fbebbec3 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -8,6 +8,7 @@ ***************************************************************************/ import QtQuick +import QtQml.Models import QtQuick.Controls import QtQuick.Dialogs @@ -16,6 +17,7 @@ import MMInput import "../components" as MMComponents import "../dialogs" as MMDialogs +import "../inputs" as MMInputs MMComponents.MMPage { id: root @@ -94,7 +96,7 @@ MMComponents.MMPage { if ( ListView.section === "internal" ) { let ix = providersModel.index( index + 1, 0 ) let type = providersModel.data( ix, MM.PositionProvidersModel.ProviderType ) - if ( type === "external" ) return false + if ( type.includes("external") ) return false } return true @@ -135,7 +137,90 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) - onClicked: bluetoothDiscoveryLoader.active = true + onClicked: providerTypeDrawer.open() + } + + MMComponents.MMListDrawer { + id: providerTypeDrawer + + drawerHeader.title: qsTr("Pick the correct provider type") + drawerHeader.titleFont: __style.t2 + + list.model: ListModel { + ListElement { + name: qsTr( "Bluetooth provider" ) + type: "bluetooth" + } + ListElement { + name: qsTr( "Network provider" ) + type: "network" + } + } + + list.delegate: MMComponents.MMListDelegate { + required property string name + required property string type + + text: name + onClicked: { + providerTypeDrawer.close() + if (type === "bluetooth") { + bluetoothDiscoveryLoader.active = true + } + else if (type === "network"){ + networkProviderInfoDrawer.open() + } + } + } + } + + MMComponents.MMDrawerDialog { + id: networkProviderInfoDrawer + + signal confirmButtonClicked(string address, int port) + + title: qsTr("Network provider setup") + imageSource: __style.externalGpsGreenImage + description: qsTr( "To connect to the external device please specify the IP address and port below." ) + primaryButton.text: qsTr( "Confirm" ) + primaryButton.type: MMComponents.MMButton.Primary + + additionalContent: Column { + width: parent.width + spacing: __style.spacing20 + + MMInputs.MMTextInput { + id: ipAddressInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr("IP address") + placeholderText: qsTr("localhost") + } + + MMInputs.MMTextInput { + id: portInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr("Port") + placeholderText: qsTr("1234") + } + } + + onPrimaryButtonClicked: { + close() + confirmButtonClicked(ipAddressInput.text, portInput.text) + } + + onConfirmButtonClicked: function( address, port ) { + const deviceAddress = address + ":" + port + __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, "Network provider" ) + + providersModel.addProvider( "Network provider", deviceAddress, "external_ip" ) + } } MMComponents.MMMessage { @@ -205,9 +290,9 @@ MMComponents.MMPage { MMAddPositionProviderDrawer { onInitiatedConnectionTo: function ( deviceAddress, deviceName ) { - __positionKit.positionProvider = __positionKit.constructProvider( "external", deviceAddress, deviceName ) + __positionKit.positionProvider = __positionKit.constructProvider( "external_bt", deviceAddress, deviceName ) - providersModel.addProvider( deviceName, deviceAddress ) + providersModel.addProvider( deviceName, deviceAddress, "external_bt" ) list.model.discovering = false close() @@ -239,7 +324,7 @@ MMComponents.MMPage { } function constructProvider( type, id, name ) { - if ( type === "external" ) { + if ( type === "external_bt" ) { // Is bluetooth turned on? if ( !__inputUtils.isBluetoothTurnedOn() ) { __inputUtils.turnBluetoothOn() @@ -253,7 +338,7 @@ MMComponents.MMPage { __positionKit.positionProvider = __positionKit.constructProvider( type, id, name ) - if ( type === "external" ) { + if ( type === "external_bt" ) { connectingDialogLoader.open() } } diff --git a/app/qml/map/MMMapController.qml b/app/qml/map/MMMapController.qml index 01efc9958..981864f7b 100644 --- a/app/qml/map/MMMapController.qml +++ b/app/qml/map/MMMapController.qml @@ -679,7 +679,7 @@ Item { visible: { if ( root.mapExtentOffset > 0 && root.state !== "stakeout" ) return false - if ( __positionKit.positionProvider && __positionKit.positionProvider.type() === "external" ) { + if ( __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") ) { // for external receivers we want to show gps panel and accuracy button // even when the GPS receiver is not sending position data return true @@ -699,7 +699,7 @@ Item { { return "" } - else if ( __positionKit.positionProvider.type() === "external" ) + else if ( __positionKit.positionProvider.type().includes("external") ) { if ( __positionKit.positionProvider.state === MM.PositionProvider.Connecting ) { diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 2ca3ee608..55872fdfd 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -106,8 +106,8 @@ void TestPosition::testBluetoothProviderConnection() QCOMPARE( "testBluetoothProvider", pkProvider->name() ); QCOMPARE( "AA:AA:AA:AA:00:00", btProvider->id() ); QCOMPARE( "AA:AA:AA:AA:00:00", pkProvider->id() ); - QCOMPARE( "external", btProvider->type() ); - QCOMPARE( "external", pkProvider->type() ); + QCOMPARE( "external_bt", btProvider->type() ); + QCOMPARE( "external_bt", pkProvider->type() ); // // let's continue with BT instance, @@ -257,11 +257,11 @@ void TestPosition::testPositionProviderKeysInSettings() rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests #ifdef HAVE_BLUETOOTH - positionKit->setPositionProvider( positionKit->constructProvider( "external", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); + positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); QCOMPARE( positionKit->positionProvider()->name(), "testProviderA" ); - QCOMPARE( positionKit->positionProvider()->type(), "external" ); + QCOMPARE( positionKit->positionProvider()->type(), "external_bt" ); QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "AA:BB:CC:DD:EE:FF" ); #endif @@ -279,7 +279,7 @@ void TestPosition::testPositionProviderKeysInSettings() QCOMPARE( providersModel.data( providersModel.index( 1 ), PositionProvidersModel::ProviderId ), "simulated" ); providersModel.setAppSettings( &appSettings ); - providersModel.addProvider( "testProviderB", "AA:00:11:22:23:44" ); + providersModel.addProvider( "testProviderB", "AA:00:11:22:23:44", "external_bt" ); // app settings should have one saved provider - testProviderB QVariantList providers = appSettings.savedPositionProviders(); diff --git a/app/test/testvariablesmanager.cpp b/app/test/testvariablesmanager.cpp index e7bda0498..cd191c431 100644 --- a/app/test/testvariablesmanager.cpp +++ b/app/test/testvariablesmanager.cpp @@ -84,7 +84,7 @@ void TestVariablesManager::testPositionVariables() evaluateExpression( QStringLiteral( "@position_gps_antenna_height" ), QStringLiteral( "0.000" ), &context ); evaluateExpression( QStringLiteral( "@position_provider_address" ), QStringLiteral( "AA:AA:FF:AA:00:10" ), &context ); evaluateExpression( QStringLiteral( "@position_provider_name" ), QStringLiteral( "testBluetoothProvider" ), &context ); - evaluateExpression( QStringLiteral( "@position_provider_type" ), QStringLiteral( "external" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_type" ), QStringLiteral( "external_bt" ), &context ); mAppSettings->setGpsAntennaHeight( 1.6784 ); pos.verticalSpeed = 1.345; diff --git a/gallery/positionkit.h b/gallery/positionkit.h index e889f1a17..e23cd9593 100644 --- a/gallery/positionkit.h +++ b/gallery/positionkit.h @@ -58,7 +58,7 @@ class PositionKit : public QObject private: QString pProviderName = "Gps Source is ok!"; - QString pProviderType = "external"; + QString pProviderType = "external_bt"; QString pProviderMessage = "Connected"; QString pStateMessage = "Message"; QString pLastRead = "17:19:08 CEST"; From 18b58a415218fdf1f4dc8465e11f4aacab039b17 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 30 Jan 2026 15:39:50 +0100 Subject: [PATCH 02/14] Fix UDP issues --- .../providers/networkpositionprovider.cpp | 143 ++++++++++++------ .../providers/networkpositionprovider.h | 15 +- 2 files changed, 103 insertions(+), 55 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index d96b3bcc6..857bb63aa 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -9,12 +9,16 @@ #include "networkpositionprovider.h" +#include +#include + #include "coreutils.h" static int ONE_SECOND_MS = 1000; NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ), + mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) { const QStringList targetAddress = addr.split( ":" ); mTargetAddress = targetAddress.at( 0 ); @@ -23,36 +27,38 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt mTcpSocket = std::make_unique(); mUdpSocket = std::make_unique(); -// only one socket will be active depending on, which will find host, the other will be closed -// connect(mTcpSocket.get(), &QAbstractSocket::hostFound, this, [this] -// { -// CoreUtils::log("NetworkPositionProvider", "TCP socket found host, aborting UDP socket"); -// mUdpSocket->abort(); -// }); -// connect(mUdpSocket.get(), &QAbstractSocket::hostFound, this, [this] -// { -// CoreUtils::log("NetworkPositionProvider", "UDP socket found host, aborting TCP socket"); -// mTcpSocket->abort(); -// }); - - connect( mTcpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); - connect( mUdpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + connect( mTcpSocket.get(), &QTcpSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + connect( mUdpSocket.get(), &QUdpSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); - connect( mTcpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); - connect( mUdpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + connect( mTcpSocket.get(), &QTcpSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + connect( mUdpSocket.get(), &QUdpSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); mReconnectTimer.setSingleShot( false ); mReconnectTimer.setInterval( ONE_SECOND_MS ); connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); + mUdpReconnectTimer.setSingleShot( true ); + connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] + { + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); + if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + } ); NetworkPositionProvider::startUpdates(); } void NetworkPositionProvider::startUpdates() { + // TODO: QHostAddress doesn't support hostname lookup (QHostInfo does) CoreUtils::log( "NetworkPositionProvider", "Connecting to host..." ); mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); - mUdpSocket->connectToHost( mTargetAddress, mTargetPort ); + mUdpSocket->bind( QHostAddress::LocalHost, mTargetPort ); + mUdpReconnectTimer.start( ReconnectDelay::ExtraLongDelay ); } void NetworkPositionProvider::stopUpdates() @@ -75,37 +81,92 @@ void NetworkPositionProvider::closeProvider() void NetworkPositionProvider::positionUpdateReceived() { - const QAbstractSocket *socket = dynamic_cast( sender() ); + QAbstractSocket *socket = dynamic_cast( sender() ); const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); CoreUtils::log( "NetworkPositionProvider", QStringLiteral( "%1 socket has received new data!" ).arg( socketTypeToString ) ); - if ( mTcpSocket->isValid() || mUdpSocket->isValid() ) + + // if udp is not connected to the host yet, connect + // this approach will let us use QIODevice functions for both sockets + if ( socket->socketType() == QAbstractSocket::UdpSocket && mUdpSocket->state() != QAbstractSocket::ConnectedState ) { - const QByteArray rawNmeaData = activeSocket()->readAll(); - const QString nmeaData( rawNmeaData ); - const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + mUdpReconnectTimer.stop(); - emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + // if by any chance we showed wrong message in the status like "no connection", fix it here + // we know the connection is working because we just received data from the device + setState( tr( "Connected" ), State::Connected ); + + QHostAddress peerAddress; + int peerPort; + // process the incoming data as it will break the signal emitting if unprocessed + while ( mUdpSocket->hasPendingDatagrams() ) + { + QNetworkDatagram datagram = mUdpSocket->receiveDatagram(); + peerAddress = datagram.senderAddress(); + peerPort = datagram.senderPort(); + const QByteArray rawNmeaData = datagram.data(); + const QString nmeaData( rawNmeaData ); + const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + } + + // "connect" to peer if we are not already connecting + if (mUdpSocket->state() != QAbstractSocket::ConnectedState && mUdpSocket->state() != QAbstractSocket::ConnectingState) + { + mUdpSocket->connectToHost( peerAddress.toString(), peerPort ); + } + return; } + + // stop the UDP silence timer, we just received data + // kills the timer when the app was minimized, and we were able to reconnect in the meantime + if ( socket->socketType() == QAbstractSocket::UdpSocket ) + { + mUdpReconnectTimer.stop(); + } + + // if by any chance we showed wrong message in the status like "no connection", fix it here + // we know the connection is working because we just received data from the device + setState( tr( "Connected" ), State::Connected ); + + const QByteArray rawNmeaData = socket->readAll(); + const QString nmeaData( rawNmeaData ); + const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + + emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); } void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketState state ) { - if ( state == QAbstractSocket::SocketState::ConnectingState || state == QAbstractSocket::SocketState::HostLookupState ) + const QAbstractSocket *socket = dynamic_cast( sender() ); + + if ( state == QAbstractSocket::ConnectingState || state == QAbstractSocket::HostLookupState ) { setState( tr( "Connecting to %1" ).arg( mProviderName ), State::Connecting ); } - else if ( state == QAbstractSocket::SocketState::ConnectedState ) + // Only with TCP we can be sure in ConnectedState that we are connected, with UDP we wait until the first datagram arrives + else if ( state == QAbstractSocket::ConnectedState && socket->socketType() == QAbstractSocket::TcpSocket ) { setState( tr( "Connected" ), State::Connected ); } - else if ( state == QAbstractSocket::SocketState::UnconnectedState ) + else if ( state == QAbstractSocket::UnconnectedState ) { - setState( tr( "No connection" ), State::NoConnection ); - startReconnectTimer(); - // let's also invalidate current position since we no longer have connection - emit positionChanged( GeoPosition() ); + const bool isUdpSocketListening = mUdpSocket->state() == QAbstractSocket::ConnectedState || mUdpSocket->state() == QAbstractSocket::BoundState || mUdpReconnectTimer.isActive(); + if ( socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + else if (socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } } - const QAbstractSocket *socket = dynamic_cast( sender() ); + const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); const QString stateToString = QMetaEnum::fromType().valueToKey( state ); CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "%1 Socket changed state, code: %2" ).arg( socketTypeToString, stateToString ) ); @@ -125,22 +186,6 @@ void NetworkPositionProvider::reconnectTimeout() } } -QAbstractSocket *NetworkPositionProvider::activeSocket() const -{ - if ( mTcpSocket->state() >= QAbstractSocket::ConnectingState && mTcpSocket->state() <= QAbstractSocket::BoundState ) - { - return mTcpSocket.get(); - } - - if ( mUdpSocket->state() >= QAbstractSocket::ConnectingState && mUdpSocket->state() <= QAbstractSocket::BoundState ) - { - return mUdpSocket.get(); - } - -//TODO: wouldn't it be better to guard against nullptr in caller and return nullptr here? - return new QAbstractSocket( QAbstractSocket::UnknownSocketType, nullptr ); -} - void NetworkPositionProvider::reconnect() { mReconnectTimer.stop(); @@ -160,7 +205,7 @@ void NetworkPositionProvider::startReconnectTimer() mReconnectTimer.start(); -// first time do reconnect in short time, then each other in long time + // first time do reconnect in short time, then each other in long time if ( mReconnectDelay == NetworkPositionProvider::ShortDelay ) { mReconnectDelay = NetworkPositionProvider::LongDelay; diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index 5a8755417..d14b458a0 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -22,11 +22,12 @@ class NetworkPositionProvider : public AbstractPositionProvider { Q_OBJECT -// signalizes in how many [ms] we will try to reconnect to GPS again + // signalizes in how many [ms] we will try to reconnect to GPS again enum ReconnectDelay { - ShortDelay = 3000, - LongDelay = 5000 + ShortDelay = 3000, // 3 secs + LongDelay = 5000, // 5 secs + ExtraLongDelay = 10000 // 10 secs }; public: @@ -37,14 +38,15 @@ class NetworkPositionProvider : public AbstractPositionProvider void closeProvider() override; public slots: + // processes the received nmea data and emits new position void positionUpdateReceived(); + // changes the provider state depending on the socket states void socketStateChanged( QAbstractSocket::SocketState state ); + // checks if enough time passed since last reconnect and triggers it if necessary void reconnectTimeout(); private: -// utility function, which should return the socket in use - QAbstractSocket *activeSocket() const; -// trigger the reconnection flow for both sockets + // trigger the reconnection flow for both sockets void reconnect(); // start the reconnection timeout void startReconnectTimer(); @@ -55,6 +57,7 @@ class NetworkPositionProvider : public AbstractPositionProvider int mReconnectDelay = ReconnectDelay::ShortDelay; // in how many [ms] we will try to reconnect again int mSecondsLeftToReconnect; // how many seconds are left to reconnect. Reconnects if less than or equal to one QTimer mReconnectTimer; // timer that times out each second and lowers the mSecondsLeftToReconnect by one + QTimer mUdpReconnectTimer; // timer that times out after ExtraLongDelay and triggers reconnect QString mTargetAddress; // IP address or hostname of the receiver int mTargetPort; // active port of the receiver From d046d2fae5dc294898eda7f853adc0d01d400c4b Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 30 Jan 2026 22:22:43 +0100 Subject: [PATCH 03/14] Fix external provider recreation bug --- app/position/positionkit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index f923f591b..feafe89aa 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -188,9 +188,9 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app if ( providerData[1] == providerId ) { providerName = providerData[0].toString(); - if ( !providerData.at( 3 ).isNull() ) + if ( !providerData.at( 2 ).isNull() ) { - providerType = providerData[3].toString(); + providerType = providerData[2].toString(); } } } From 720e2c336f308a029920915a0791976e47ba9b08 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 30 Jan 2026 23:11:52 +0100 Subject: [PATCH 04/14] Fix another restart provider bug --- app/position/providers/positionprovidersmodel.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index b51373f2b..c9a91b6ea 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -197,10 +197,12 @@ void PositionProvidersModel::setAppSettings( AppSettings *as ) } PositionProvider provider; + const QString providerType = providerData[2].isNull() ? QStringLiteral( "external_bt" ) : QStringLiteral( "external_ip" ); + const QString deviceDesc = providerType == QStringLiteral( "external_bt" ) ? tr( " Bluetooth device" ) : tr( " Network device" ); provider.name = providerData[0].toString(); provider.providerId = providerData[1].toString(); - provider.description = provider.providerId + " " + tr( "Bluetooth device" ); - provider.providerType = "external"; + provider.description = provider.providerId + deviceDesc; + provider.providerType = providerType; mProviders.append( provider ); } From a99a18bf81361d5a6c5e881a9111410c22001efc Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 16 Feb 2026 18:58:16 +0100 Subject: [PATCH 05/14] Add Alias field for network provider --- app/qml/gps/MMPositionProviderPage.qml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 6fbebbec3..ae29b8c01 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -177,8 +177,6 @@ MMComponents.MMPage { MMComponents.MMDrawerDialog { id: networkProviderInfoDrawer - signal confirmButtonClicked(string address, int port) - title: qsTr("Network provider setup") imageSource: __style.externalGpsGreenImage description: qsTr( "To connect to the external device please specify the IP address and port below." ) @@ -189,6 +187,16 @@ MMComponents.MMPage { width: parent.width spacing: __style.spacing20 + MMInputs.MMTextInput { + id: aliasInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr("Device alias") + placeholderText: qsTr("Green device") + } + MMInputs.MMTextInput { id: ipAddressInput @@ -210,16 +218,13 @@ MMComponents.MMPage { } } - onPrimaryButtonClicked: { + onPrimaryButtonClicked: function() { close() - confirmButtonClicked(ipAddressInput.text, portInput.text) - } - onConfirmButtonClicked: function( address, port ) { - const deviceAddress = address + ":" + port - __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, "Network provider" ) + const deviceAddress = ipAddressInput.text + ":" + portInput.text + __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, aliasInput.text ) - providersModel.addProvider( "Network provider", deviceAddress, "external_ip" ) + providersModel.addProvider( aliasInput.text, deviceAddress, "external_ip" ) } } From 478b79890a8234c7fca951221e9012f46e8967c0 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 14:32:30 +0100 Subject: [PATCH 06/14] Refactor NmeaParser --- app/CMakeLists.txt | 2 ++ .../providers/bluetoothpositionprovider.cpp | 11 ------- .../providers/bluetoothpositionprovider.h | 29 ++++------------- .../providers/networkpositionprovider.cpp | 24 +++++++------- app/position/providers/nmeaparser.cpp | 23 +++++++++++++ app/position/providers/nmeaparser.h | 32 +++++++++++++++++++ 6 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 app/position/providers/nmeaparser.cpp create mode 100644 app/position/providers/nmeaparser.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 52e1591de..31b8cd534 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -36,6 +36,7 @@ set(MM_SRCS position/providers/networkpositionprovider.cpp position/providers/positionprovidersmodel.cpp position/providers/simulatedpositionprovider.cpp + position/providers/nmeaparser.cpp position/tracking/abstracttrackingbackend.cpp position/tracking/internaltrackingbackend.cpp position/tracking/positiontrackinghighlight.cpp @@ -127,6 +128,7 @@ set(MM_HDRS position/providers/networkpositionprovider.h position/providers/positionprovidersmodel.h position/providers/simulatedpositionprovider.h + position/providers/nmeaparser.h position/tracking/abstracttrackingbackend.h position/tracking/internaltrackingbackend.h position/tracking/positiontrackinghighlight.h diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index 7914ac381..ef4139011 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -16,17 +16,6 @@ #include #endif -NmeaParser::NmeaParser() : QgsNmeaConnection( new QBluetoothSocket() ) -{ -} - -QgsGpsInformation NmeaParser::parseNmeaString( const QString &nmeastring ) -{ - mStringBuffer = nmeastring; - processStringBuffer(); - return mLastGPSInformation; -} - BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent ) : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, parent ) , mTargetAddress( addr ) diff --git a/app/position/providers/bluetoothpositionprovider.h b/app/position/providers/bluetoothpositionprovider.h index b872b1c01..ad60d19a9 100644 --- a/app/position/providers/bluetoothpositionprovider.h +++ b/app/position/providers/bluetoothpositionprovider.h @@ -10,29 +10,12 @@ #ifndef BLUETOOTHPOSITIONPROVIDER_H #define BLUETOOTHPOSITIONPROVIDER_H -#include "abstractpositionprovider.h" - -#include "qgsnmeaconnection.h" - #include #include #include -/** - * NmeaParser is a big hack how to reuse QGIS NmeaConnection function in order to (a) keep ownership of bluetooth - * socket, (b) do not have multiple unique_ptrs holding the same pointer and to avoid some possible crashes. - * - * Note: This way of reusing makes the parser highly dependent on QgsNmeaConnection class and any change inside the class - * can lead to misbehaviors. See implementation of QgsNmeaConnection and QgsGpsConnection for more details. - */ -class NmeaParser : public QgsNmeaConnection -{ - public: - NmeaParser(); - - // Takes nmea string and returns gps position - QgsGpsInformation parseNmeaString( const QString &nmeastring ); -}; +#include "abstractpositionprovider.h" +#include "nmeaparser.h" /** * BluetoothPositionProvider initiates connection to bluetooth device @@ -52,11 +35,11 @@ class BluetoothPositionProvider : public AbstractPositionProvider public: BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); - virtual ~BluetoothPositionProvider() override; + ~BluetoothPositionProvider() override; - virtual void startUpdates() override; - virtual void stopUpdates() override; - virtual void closeProvider() override; + void startUpdates() override; + void stopUpdates() override; + void closeProvider() override; void handleLostConnection(); void startReconnectionTime(); diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 857bb63aa..b2d0af2fa 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -38,16 +38,16 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); mUdpReconnectTimer.setSingleShot( true ); connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] + { + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); + if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) { - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); - if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) - { - setState( tr( "No connection" ), State::NoConnection ); - startReconnectTimer(); - // let's also invalidate current position since we no longer have connection - emit positionChanged( GeoPosition() ); - } - } ); + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + } ); NetworkPositionProvider::startUpdates(); } @@ -110,7 +110,7 @@ void NetworkPositionProvider::positionUpdateReceived() } // "connect" to peer if we are not already connecting - if (mUdpSocket->state() != QAbstractSocket::ConnectedState && mUdpSocket->state() != QAbstractSocket::ConnectingState) + if ( mUdpSocket->state() != QAbstractSocket::ConnectedState && mUdpSocket->state() != QAbstractSocket::ConnectingState ) { mUdpSocket->connectToHost( peerAddress.toString(), peerPort ); } @@ -151,14 +151,14 @@ void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketS else if ( state == QAbstractSocket::UnconnectedState ) { const bool isUdpSocketListening = mUdpSocket->state() == QAbstractSocket::ConnectedState || mUdpSocket->state() == QAbstractSocket::BoundState || mUdpReconnectTimer.isActive(); - if ( socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive) + if ( socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive ) { setState( tr( "No connection" ), State::NoConnection ); startReconnectTimer(); // let's also invalidate current position since we no longer have connection emit positionChanged( GeoPosition() ); } - else if (socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive) + else if ( socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive ) { setState( tr( "No connection" ), State::NoConnection ); startReconnectTimer(); diff --git a/app/position/providers/nmeaparser.cpp b/app/position/providers/nmeaparser.cpp new file mode 100644 index 000000000..69a8f1015 --- /dev/null +++ b/app/position/providers/nmeaparser.cpp @@ -0,0 +1,23 @@ +/*************************************************************************** +* * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "nmeaparser.h" + +#include + +NmeaParser::NmeaParser() : QgsNmeaConnection( new QBuffer() ) +{ +} + +QgsGpsInformation NmeaParser::parseNmeaString( const QString &nmeaString ) +{ + mStringBuffer = nmeaString; + processStringBuffer(); + return mLastGPSInformation; +} \ No newline at end of file diff --git a/app/position/providers/nmeaparser.h b/app/position/providers/nmeaparser.h new file mode 100644 index 000000000..fc47d7e82 --- /dev/null +++ b/app/position/providers/nmeaparser.h @@ -0,0 +1,32 @@ +/*************************************************************************** +* * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef NMEAPARSER_H +#define NMEAPARSER_H + +#include + +/** + * NmeaParser is a big hack how to reuse QGIS NmeaConnection function in order to (a) keep ownership of bluetooth + * socket, (b) do not have multiple unique_ptrs holding the same pointer and to avoid some possible crashes. + * + * Note: This way of reusing makes the parser highly dependent on QgsNmeaConnection class and any change inside the class + * can lead to misbehavior's. See implementation of QgsNmeaConnection and QgsGpsConnection for more details. + */ +class NmeaParser : public QgsNmeaConnection +{ + public: + NmeaParser(); + +// Takes nmea string and returns gps position + QgsGpsInformation parseNmeaString( const QString &nmeaString ); +}; + + +#endif //NMEAPARSER_H \ No newline at end of file From b658cab72b00bbaae11b95cee4b1e725d7e4d4a1 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 14:48:12 +0100 Subject: [PATCH 07/14] Fix breaking bluetooth include --- app/position/providers/networkpositionprovider.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index d14b458a0..794f63458 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -13,9 +13,10 @@ #include #include +#include #include "abstractpositionprovider.h" -#include "bluetoothpositionprovider.h" +#include "nmeaparser.h" class NetworkPositionProvider : public AbstractPositionProvider From 263c1002b71f905ef397b68c53fd08a4b3f7c3aa Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 17:47:41 +0100 Subject: [PATCH 08/14] Update tests --- app/test/testposition.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 55872fdfd..c24c01cfc 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -257,7 +257,7 @@ void TestPosition::testPositionProviderKeysInSettings() rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests #ifdef HAVE_BLUETOOTH - positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); + positionKit->setPositionProvider( PositionKit::constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); QCOMPARE( positionKit->positionProvider()->name(), "testProviderA" ); @@ -266,7 +266,7 @@ void TestPosition::testPositionProviderKeysInSettings() QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "AA:BB:CC:DD:EE:FF" ); #endif - positionKit->setPositionProvider( positionKit->constructProvider( "internal", "devicegps" ) ); + positionKit->setPositionProvider( PositionKit::constructProvider( "internal", "devicegps" ) ); QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "devicegps" ); @@ -280,19 +280,27 @@ void TestPosition::testPositionProviderKeysInSettings() providersModel.setAppSettings( &appSettings ); providersModel.addProvider( "testProviderB", "AA:00:11:22:23:44", "external_bt" ); + providersModel.addProvider( "testProviderC", "localhost:9000", "external_ip" ); - // app settings should have one saved provider - testProviderB + // app settings should have two saved providers - testProviderB & testProviderC QVariantList providers = appSettings.savedPositionProviders(); - QCOMPARE( providers.count(), 1 ); // we have one (external) provider - QCOMPARE( providers.at( 0 ).toList().count(), 2 ); // the provider has two properties + QCOMPARE( providers.count(), 2 ); // we have two (external) providers + QCOMPARE( providers.at( 0 ).toList().count(), 3 ); // the provider has two properties QVariantList providerData = providers.at( 0 ).toList(); QCOMPARE( providerData.at( 0 ).toString(), "testProviderB" ); QCOMPARE( providerData.at( 1 ).toString(), "AA:00:11:22:23:44" ); + QCOMPARE( providerData.at( 2 ).toString(), "external_bt" ); + + providerData = providers.at( 1 ).toList(); + QCOMPARE( providerData.at( 0 ).toString(), "testProviderC" ); + QCOMPARE( providerData.at( 1 ).toString(), "localhost:9000" ); + QCOMPARE( providerData.at( 2 ).toString(), "external_ip" ); // remove that provider providersModel.removeProvider( "AA:00:11:22:23:44" ); + providersModel.removeProvider( "localhost:9000" ); providers = appSettings.savedPositionProviders(); From a1d90265502b22705b4a56a9f298462010716bed Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 18:17:33 +0100 Subject: [PATCH 09/14] Fix merge leftovers --- app/position/positionkit.cpp | 2 +- app/position/providers/bluetoothpositionprovider.cpp | 4 ++-- app/position/providers/networkpositionprovider.cpp | 4 ++-- app/position/providers/networkpositionprovider.h | 2 +- app/test/testposition.cpp | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 5d22056f1..87835805f 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -139,7 +139,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c if ( providerType == QStringLiteral( "external_ip" ) ) { - AbstractPositionProvider *provider = new NetworkPositionProvider( id, name ); + AbstractPositionProvider *provider = new NetworkPositionProvider( id, name, *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index 0876b0e03..61404e34f 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -19,8 +19,8 @@ #include #endif -BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, parent ) +BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, positionTransformer, parent ) , mTargetAddress( addr ) { mSocket = std::unique_ptr( new QBluetoothSocket( QBluetoothServiceInfo::RfcommProtocol ) ); diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index b2d0af2fa..93189d3ac 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -16,8 +16,8 @@ static int ONE_SECOND_MS = 1000; -NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ), +NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, positionTransformer, parent ), mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) { const QStringList targetAddress = addr.split( ":" ); diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index 794f63458..9d4bc080f 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -32,7 +32,7 @@ class NetworkPositionProvider : public AbstractPositionProvider }; public: - explicit NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); + NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); void startUpdates() override; void stopUpdates() override; diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 7239c05eb..0f8467c1d 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -257,7 +257,7 @@ void TestPosition::testPositionProviderKeysInSettings() rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests #ifdef HAVE_BLUETOOTH - positionKit->setPositionProvider( PositionKit::constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); + positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); QCOMPARE( positionKit->positionProvider()->name(), "testProviderA" ); @@ -266,7 +266,7 @@ void TestPosition::testPositionProviderKeysInSettings() QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "AA:BB:CC:DD:EE:FF" ); #endif - positionKit->setPositionProvider( PositionKit::constructProvider( "internal", "devicegps" ) ); + positionKit->setPositionProvider( positionKit->constructProvider( "internal", "devicegps" ) ); QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "devicegps" ); From c761edcbeb7fd892e0793a43a6f03de300f8a576 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 18:19:25 +0100 Subject: [PATCH 10/14] Remove debug logs --- app/position/providers/networkpositionprovider.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 93189d3ac..ef5b52654 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -12,8 +12,6 @@ #include #include -#include "coreutils.h" - static int ONE_SECOND_MS = 1000; NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) @@ -39,7 +37,6 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt mUdpReconnectTimer.setSingleShot( true ); connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] { - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) { setState( tr( "No connection" ), State::NoConnection ); @@ -55,7 +52,6 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt void NetworkPositionProvider::startUpdates() { // TODO: QHostAddress doesn't support hostname lookup (QHostInfo does) - CoreUtils::log( "NetworkPositionProvider", "Connecting to host..." ); mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); mUdpSocket->bind( QHostAddress::LocalHost, mTargetPort ); mUdpReconnectTimer.start( ReconnectDelay::ExtraLongDelay ); @@ -83,7 +79,6 @@ void NetworkPositionProvider::positionUpdateReceived() { QAbstractSocket *socket = dynamic_cast( sender() ); const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); - CoreUtils::log( "NetworkPositionProvider", QStringLiteral( "%1 socket has received new data!" ).arg( socketTypeToString ) ); // if udp is not connected to the host yet, connect // this approach will let us use QIODevice functions for both sockets @@ -169,7 +164,6 @@ void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketS const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); const QString stateToString = QMetaEnum::fromType().valueToKey( state ); - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "%1 Socket changed state, code: %2" ).arg( socketTypeToString, stateToString ) ); } void NetworkPositionProvider::reconnectTimeout() @@ -192,8 +186,6 @@ void NetworkPositionProvider::reconnect() setState( tr( "Reconnecting" ), State::Connecting ); - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "Reconnecting to %1" ).arg( mProviderName ) ); - stopUpdates(); startUpdates(); } From 2817eb9f0786909bc8e0e9ce98e5f3e00ce14e1a Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 09:02:33 +0100 Subject: [PATCH 11/14] Fix android merge error --- app/position/positionkit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 87835805f..51d8f207c 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -154,7 +154,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c #ifdef ANDROID if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) { - constbool fused = ( id == QStringLiteral( "android_fused" ) ); + const bool fused = ( id == QStringLiteral( "android_fused" ) ); if ( fused && !AndroidPositionProvider::isFusedAvailable() ) { // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? From 1ee402578957e7073ad3914220dcec43dc882fbb Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 10:49:32 +0100 Subject: [PATCH 12/14] Add position processing for network provider --- app/position/positiontransformer.cpp | 5 +++++ app/position/positiontransformer.h | 8 ++++++++ app/position/providers/bluetoothpositionprovider.cpp | 2 +- app/position/providers/networkpositionprovider.cpp | 5 ++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/position/positiontransformer.cpp b/app/position/positiontransformer.cpp index 2c4dc9b90..60b7b9470 100644 --- a/app/position/positiontransformer.cpp +++ b/app/position/positiontransformer.cpp @@ -45,6 +45,11 @@ GeoPosition PositionTransformer::processBluetoothPosition( GeoPosition geoPositi return geoPosition; } +GeoPosition PositionTransformer::processNetworkPosition( const GeoPosition &geoPosition ) +{ + return processBluetoothPosition( geoPosition ); +} + GeoPosition PositionTransformer::processAndroidPosition( GeoPosition geoPosition ) { if ( geoPosition.elevation != std::numeric_limits::quiet_NaN() ) diff --git a/app/position/positiontransformer.h b/app/position/positiontransformer.h index fb9a62250..e77519263 100644 --- a/app/position/positiontransformer.h +++ b/app/position/positiontransformer.h @@ -49,6 +49,14 @@ class PositionTransformer : QObject */ GeoPosition processBluetoothPosition( GeoPosition geoPosition ); + /** + * Transform the elevation if the user sets custom vertical CRS. The elevation gets recalculated to ellipsoid elevation + * and then back to orthometric based on specified CRS. + * \note This method should be used only with NetworkPositionProvider to mitigate unnecessary transformations + * \return Copy of passed geoPosition with processed elevation and elevation separation. + */ + GeoPosition processNetworkPosition( const GeoPosition &geoPosition ); + /** * Transform the elevation from EPSG:4979 (WGS84 (EPSG:4326) + ellipsoidal height) to specified geoid model * (by default EPSG:9707 (WGS84 + EGM96)) diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index 61404e34f..380c595fe 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -203,6 +203,6 @@ void BluetoothPositionProvider::positionUpdateReceived() GeoPosition positionData = GeoPosition::fromQgsGpsInformation( data ); GeoPosition transformedPosition = mPositionTransformer->processBluetoothPosition( positionData ); - emit positionChanged( positionData ); + emit positionChanged( transformedPosition ); } } diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index ef5b52654..fbe533b65 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -101,7 +101,9 @@ void NetworkPositionProvider::positionUpdateReceived() const QByteArray rawNmeaData = datagram.data(); const QString nmeaData( rawNmeaData ); const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); - emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + GeoPosition transformedPosition = mPositionTransformer->processNetworkPosition( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + + emit positionChanged( transformedPosition ); } // "connect" to peer if we are not already connecting @@ -126,6 +128,7 @@ void NetworkPositionProvider::positionUpdateReceived() const QByteArray rawNmeaData = socket->readAll(); const QString nmeaData( rawNmeaData ); const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + GeoPosition transformedPosition = mPositionTransformer->processNetworkPosition( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); } From 33f2ffd9b1c7357bbbc5ce88c3bbd7addce3bf24 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 11:12:47 +0100 Subject: [PATCH 13/14] Add processing tests --- app/test/testposition.cpp | 67 +++++++++++++++++++++++++++++++++++++++ app/test/testposition.h | 1 + 2 files changed, 68 insertions(+) diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 0f8467c1d..d7c1e320a 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -786,6 +786,73 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() QVERIFY( qgsDoubleNear( newPosition.elevation, 171.3 ) ); } +void TestPosition::testPositionTransformerNetworkPosition() +{ + // prepare position transformers + // WGS84 + ellipsoid + QgsCoordinateReferenceSystem ellipsoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 4979 ); + // WGS84 + EGM96 + QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); + PositionTransformer passThroughTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); + PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); + +#ifdef HAVE_BLUETOOTH + // mini file contains only minimal info like position and date + QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; + QFile miniNmeaFile( miniNmeaPositionFilePath ); + miniNmeaFile.open( QFile::ReadOnly ); + + QVERIFY( miniNmeaFile.isOpen() ); + + NmeaParser parser; + QgsGpsInformation position = parser.parseNmeaString( miniNmeaFile.readAll() ); + GeoPosition geoPosition = GeoPosition::fromQgsGpsInformation( position ); + + QVERIFY( qgsDoubleNear( geoPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( geoPosition.longitude, 17.1064 ) ); + QCOMPARE( geoPosition.elevation, 171.3 ); + QCOMPARE( geoPosition.elevation_diff, std::numeric_limits::quiet_NaN() ); +#else + GeoPosition geoPosition; + geoPosition.latitude = 48.10305; + geoPosition.longitude = 17.1064; + geoPosition.elevation = 171.3; +#endif + + // transform with pass through disabled and missing elevation separation + GeoPosition newPosition = positionTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QCOMPARE( newPosition.elevation, 171.3 ); + QCOMPARE( newPosition.elevation_diff, std::numeric_limits::quiet_NaN() ); + + // transform with pass through enabled and missing elevation separation + newPosition = passThroughTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QCOMPARE( newPosition.elevation, 171.3 ); + QCOMPARE( newPosition.elevation_diff, std::numeric_limits::quiet_NaN() ); + + // transform with pass through enabled and elevation separation + geoPosition.elevation_diff = 40; + newPosition = passThroughTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QCOMPARE( newPosition.elevation, 171.3 ); + QCOMPARE( newPosition.elevation_diff, 40 ); + + // transform with pass through disabled and elevation separation + newPosition = positionTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QVERIFY( qgsDoubleNear( newPosition.elevation, 167.53574931171875 ) ); + QVERIFY( qgsDoubleNear( newPosition.elevation_diff, 43.764250688281265 ) ); +} + void TestPosition::testPositionTransformerSimulatedPosition() { // prepare position transformers diff --git a/app/test/testposition.h b/app/test/testposition.h index 3636b89c3..40ffd45c6 100644 --- a/app/test/testposition.h +++ b/app/test/testposition.h @@ -44,6 +44,7 @@ class TestPosition: public QObject void testPositionTransformerInternalAndroidPosition(); void testPositionTransformerInternalIosPosition(); void testPositionTransformerInternalDesktopPosition(); + void testPositionTransformerNetworkPosition(); void testPositionTransformerSimulatedPosition(); private: From b11e613817e4d4366580324de099ffaf8573e1ce Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 11:17:27 +0100 Subject: [PATCH 14/14] Fix formatting --- app/test/testposition.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index d7c1e320a..94c7675e4 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -788,7 +788,7 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() void TestPosition::testPositionTransformerNetworkPosition() { - // prepare position transformers +// prepare position transformers // WGS84 + ellipsoid QgsCoordinateReferenceSystem ellipsoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 4979 ); // WGS84 + EGM96