From cc694226b1618e27bf76162df0357ec743268cb9 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 11 Jun 2026 00:20:57 +0200 Subject: [PATCH 1/7] Forbid duplicate network provider --- app/position/providers/positionprovidersmodel.cpp | 8 ++++++++ app/position/providers/positionprovidersmodel.h | 1 + app/qml/gps/MMNetworkProviderDrawer.qml | 9 +++++---- app/qml/gps/MMPositionProviderPage.qml | 7 ++++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index 5c7365e7a..4e7f81993 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -164,6 +164,14 @@ void PositionProvidersModel::addProvider( const QString &name, const QString &pr } } +bool PositionProvidersModel::providerExists( const QString &providerId ) +{ + return std::any_of( mProviders.begin(), mProviders.end(), [providerId]( const PositionProvider & provider ) + { + return provider.providerId == providerId; + } ); +} + AppSettings *PositionProvidersModel::appSettings() const { return mAppSettings; diff --git a/app/position/providers/positionprovidersmodel.h b/app/position/providers/positionprovidersmodel.h index 0f86831fc..866e1fddb 100644 --- a/app/position/providers/positionprovidersmodel.h +++ b/app/position/providers/positionprovidersmodel.h @@ -69,6 +69,7 @@ class PositionProvidersModel : public QAbstractListModel Q_INVOKABLE void removeProvider( const QString &providerId ); Q_INVOKABLE void addProvider( const QString &providerName, const QString &providerId, const QString &providerType ); + Q_INVOKABLE bool providerExists( const QString &providerId ); AppSettings *appSettings() const; void setAppSettings( AppSettings * ); diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index 670e4e811..55a448a9f 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -110,14 +110,15 @@ MMComponents.MMDrawer { const deviceAddress = ip + ":" + port root.confirmed( aliasInput.text.trim(), deviceAddress ) - root.close() - ipAddressInput.textField.clear() - portInput.textField.clear() - aliasInput.textField.clear() } } } + function showDuplicateProviderError() { + ipAddressInput.errorMsg = qsTr( "Network position provider with this IP address & port already exists" ) + portInput.errorMsg = qsTr( "Network position provider with this IP address & port already exists" ) + } + onClosed: { ipAddressInput.textField.clear() portInput.textField.clear() diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 4c9c59b38..39657d28a 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -154,7 +154,12 @@ MMComponents.MMPage { id: networkProviderDrawer onConfirmed: function( alias, deviceAddress ) { - root.activateProvider( "external_ip", deviceAddress, alias ) + if ( providersModel.providerExists( deviceAddress ) ) { + showDuplicateProviderError() + } else { + close() + root.activateProvider( "external_ip", deviceAddress, alias ) + } } } From ec4cfdfba69e4b102c5e1c6a44bd7177d75d2597 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 11 Jun 2026 00:36:18 +0200 Subject: [PATCH 2/7] Fix default naming for position providers --- app/qml/gps/MMPositionProviderPage.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 39657d28a..ecaab89ce 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -69,6 +69,7 @@ MMComponents.MMPage { if ( listdelegate.providerName ) return listdelegate.providerName return qsTr( "Unknown device" ) } + secondaryText: { if ( listdelegate.isActive ) { if ( listdelegate.providerType === "external_ip" ) @@ -273,8 +274,8 @@ MMComponents.MMPage { return // do not construct the same provider again } - providersModel.addProvider( name, id, type ) PositionKit.positionProvider = PositionKit.constructProvider( type, id, name ) + providersModel.addProvider( PositionKit.positionProvider.name(), id, type ) if ( type === "external_bt" ) { connectingDialogLoader.open( "bluetooth" ) From 4882d393e81f6916974550578d24af8f2897440a Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 11 Jun 2026 09:41:52 +0200 Subject: [PATCH 3/7] Fix connecting status drawer visuals --- app/qml/gps/MMExternalProviderConnectionDrawer.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index e4db97a9b..bdcdb3d6f 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -67,7 +67,7 @@ MMComponents.MMDrawer { : "" ) message.description: root.providerType === "bluetooth" ? qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) - : qsTr( "We were not able to connect to the specified IP address. Please try again later." ) + : qsTr( "We were not able to connect to the specified IP address." ) message.linkText: qsTr( "Learn more" ) } }, @@ -75,10 +75,10 @@ MMComponents.MMDrawer { name: "waitingToReconnect" when: root.positionProvider && root.positionProvider.state === PositionProvider.WaitingToReconnect PropertyChanges { - message.image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage + message.image: __style.externalGpsRedImage message.title: root.providerType === "bluetooth" ? qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) - : qsTr( "We were not able to connect to the specified IP address. Please try again later." ) + : qsTr( "We were not able to connect to the specified IP address." ) message.description: root.positionProvider.stateMessage + "

" + qsTr( "You can close this message, we will try to repeatedly connect to your device." ) message.linkText: qsTr( "Learn more" ) } From 110377fb681d5bba2af37e76bc8a7cf028646c8b Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 11 Jun 2026 10:18:57 +0200 Subject: [PATCH 4/7] Fix sections in position providers list --- .../providers/positionprovidersmodel.cpp | 16 +++++++++++++--- app/position/providers/positionprovidersmodel.h | 3 ++- app/qml/gps/MMPositionProviderPage.qml | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index 4e7f81993..32345d658 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -19,7 +19,7 @@ PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractLis { if ( !InputUtils::isMobilePlatform() ) { - const PositionProvider simulated( "Simulated provider", "Simulated position around point", "internal", "simulated" ); + const PositionProvider simulated( tr( "Simulated provider" ), tr( "Simulated position around point" ), QStringLiteral( "internal" ), QStringLiteral( "simulated" ) ); mProviders.push_front( simulated ); } @@ -30,8 +30,8 @@ PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractLis PositionProvider internal; internal.name = tr( "Internal" ); internal.description = tr( "GPS receiver of this device" ); - internal.providerType = "internal"; - internal.providerId = "devicegps"; + internal.providerType = QStringLiteral( "internal" ); + internal.providerId = QStringLiteral( "devicegps" ); mProviders.push_front( internal ); @@ -68,6 +68,7 @@ QHash PositionProvidersModel::roleNames() const roles.insert( DataRoles::ProviderName, QByteArray( "providerName" ) ); roles.insert( DataRoles::ProviderDescription, QByteArray( "providerDescription" ) ); roles.insert( DataRoles::ProviderType, QByteArray( "providerType" ) ); + roles.insert( DataRoles::ProviderGroup, QByteArray( "providerGroup" ) ); roles.insert( DataRoles::ProviderId, QByteArray( "providerId" ) ); return roles; } @@ -103,6 +104,15 @@ QVariant PositionProvidersModel::data( const QModelIndex &index, const int role case DataRoles::ProviderType: return provider.providerType; + case DataRoles::ProviderGroup: + { + if ( provider.providerType == QStringLiteral( "internal" ) ) + { + return QStringLiteral( "internal" ); + } + return QStringLiteral( "external" ); + } + default: return {}; } diff --git a/app/position/providers/positionprovidersmodel.h b/app/position/providers/positionprovidersmodel.h index 866e1fddb..8b26b8e6c 100644 --- a/app/position/providers/positionprovidersmodel.h +++ b/app/position/providers/positionprovidersmodel.h @@ -58,7 +58,8 @@ class PositionProvidersModel : public QAbstractListModel ProviderName = Qt::UserRole + 1, // name of bluetooth device or custom name for network device ProviderDescription, // device address (IP/BT) + device type ProviderId, // device address (IP/BT) - ProviderType // external_ip (connected) / external_bt (connected) / internal (device) / simulated (device) + ProviderType, // external_ip (connected) / external_bt (connected) / internal (device) / simulated (device) + ProviderGroup // internal / external }; Q_ENUM( DataRoles ) diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index ecaab89ce..34ceb63ff 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -100,12 +100,12 @@ MMComponents.MMPage { } section { - property: "providerType" + property: "providerGroup" delegate: MMComponents.MMText { required property string section width: ListView.view.width - text: section === "internal" ? qsTr( "Internal receivers" ) : qsTr( "External receivers" ) + text: qsTr( "%1 receivers" ).arg( section === "internal" ? qsTr( "Internal" ) : qsTr( "External" ) ) font: __style.p6 color: __style.nightColor From 2584ba5d4e71b3434183ce3c08e327bcc12a1cc0 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 11 Jun 2026 10:31:37 +0200 Subject: [PATCH 5/7] Use silence timer for UDP connection in network provider --- app/position/providers/networkpositionprovider.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 5f59a1def..1cc9bc16c 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -126,14 +126,16 @@ void NetworkPositionProvider::positionUpdateReceived() { mUdpSocket->connectToHost( peerAddress.toString(), peerPort ); } + + // restart UDP silence timer + mUdpReconnectTimer.start(); 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 + // restart the UDP silence timer, we just received data if ( socket->socketType() == QAbstractSocket::UdpSocket ) { - mUdpReconnectTimer.stop(); + mUdpReconnectTimer.start(); } const QByteArray rawNmeaData = socket->readAll(); From e71176f981b3a049230c292ee3fdda5bb9d6e5a9 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Thu, 11 Jun 2026 10:44:28 +0200 Subject: [PATCH 6/7] Slightly refactor code --- app/position/providers/networkpositionprovider.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 1cc9bc16c..8f16307c4 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -51,7 +51,7 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt void NetworkPositionProvider::startUpdates() { - // TODO: QHostAddress doesn't support hostname lookup (QHostInfo does) + // NOTE: QHostAddress doesn't support hostname lookup (QHostInfo does) mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); mUdpSocket->bind( QHostAddress::LocalHost, mTargetPort ); mUdpReconnectTimer.start( ReconnectDelay::ExtraLongDelay ); @@ -172,14 +172,9 @@ 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 ) - { - 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 ) + const bool isTcpSocketAndUdpNotListening = socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive; + const bool isUdpSocket = socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive; + if ( isTcpSocketAndUdpNotListening || isUdpSocket ) { setState( tr( "No connection" ), State::NoConnection ); startReconnectTimer(); From 022ab39ea1bfc6403fcd92e4f63a38b7af137f20 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 17 Jun 2026 18:40:06 +0200 Subject: [PATCH 7/7] Add silence treshold for tcp connection too --- .../providers/networkpositionprovider.cpp | 34 ++++++++----------- .../providers/networkpositionprovider.h | 2 +- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 8f16307c4..d42fac6da 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -34,16 +34,13 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt mReconnectTimer.setSingleShot( false ); mReconnectTimer.setInterval( ONE_SECOND_MS ); connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); - mUdpReconnectTimer.setSingleShot( true ); - connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] + mHeartBeatTimer.setSingleShot( true ); + connect( &mHeartBeatTimer, &QTimer::timeout, this, [this] { - 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(); @@ -54,7 +51,7 @@ void NetworkPositionProvider::startUpdates() // NOTE: QHostAddress doesn't support hostname lookup (QHostInfo does) mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); mUdpSocket->bind( QHostAddress::LocalHost, mTargetPort ); - mUdpReconnectTimer.start( ReconnectDelay::ExtraLongDelay ); + mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); } void NetworkPositionProvider::stopUpdates() @@ -76,7 +73,7 @@ NetworkPositionProvider::~NetworkPositionProvider() void NetworkPositionProvider::closeProvider() { - mUdpReconnectTimer.stop(); + mHeartBeatTimer.stop(); mReconnectTimer.stop(); if ( mTcpSocket ) @@ -99,7 +96,7 @@ void NetworkPositionProvider::positionUpdateReceived() // this approach will let us use QIODevice functions for both sockets if ( socket->socketType() == QAbstractSocket::UdpSocket && mUdpSocket->state() != QAbstractSocket::ConnectedState ) { - mUdpReconnectTimer.stop(); + mHeartBeatTimer.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 @@ -127,16 +124,13 @@ void NetworkPositionProvider::positionUpdateReceived() mUdpSocket->connectToHost( peerAddress.toString(), peerPort ); } - // restart UDP silence timer - mUdpReconnectTimer.start(); + // restart silence timer + mHeartBeatTimer.start(); return; } - // restart the UDP silence timer, we just received data - if ( socket->socketType() == QAbstractSocket::UdpSocket ) - { - mUdpReconnectTimer.start(); - } + // restart the silence timer, we just received data + mHeartBeatTimer.start(); const QByteArray rawNmeaData = socket->readAll(); @@ -171,7 +165,7 @@ void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketS } else if ( state == QAbstractSocket::UnconnectedState ) { - const bool isUdpSocketListening = mUdpSocket->state() == QAbstractSocket::ConnectedState || mUdpSocket->state() == QAbstractSocket::BoundState || mUdpReconnectTimer.isActive(); + const bool isUdpSocketListening = mUdpSocket->state() == QAbstractSocket::ConnectedState || mUdpSocket->state() == QAbstractSocket::BoundState || mHeartBeatTimer.isActive(); const bool isTcpSocketAndUdpNotListening = socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive; const bool isUdpSocket = socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive; if ( isTcpSocketAndUdpNotListening || isUdpSocket ) diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index b3b57d136..cbbd1b0a5 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -59,7 +59,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 + QTimer mHeartBeatTimer; // 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