Skip to content

Add Ntrip client to qfield#7025

Open
natuition wants to merge 48 commits intoopengisch:masterfrom
natuition:ntrip-client-finish
Open

Add Ntrip client to qfield#7025
natuition wants to merge 48 commits intoopengisch:masterfrom
natuition:ntrip-client-finish

Conversation

@natuition
Copy link

Add NTRIP client integration for RTK corrections

Add complete NTRIP client implementation to provide real-time kinematic
corrections to external GNSS receivers via Bluetooth.

Changes

Core Implementation

  • Add NtripSocketClient for low-level TCP connection and NTRIP protocol
  • Add NtripClient as high-level wrapper with byte counter tracking
  • Integrate NTRIP client into PositioningSource with configuration properties

Properties & State Management

  • Add NTRIP properties: host, port, mountpoint, username, password
  • Replace ntripStatus (QString) with ntripState (enum: Connected/Disconnected)
  • Add ntripLastError property for error reporting (similar to deviceLastError)
  • Add streamConnected and streamDisconnected signals
  • Track NTRIP bytes sent/received for monitoring

Bug Fixes

  • Fix TCP header fragmentation by buffering data until headers complete

Requirements

The NTRIP client is only enabled when an external receiver is active and
properly configured with all required connection parameters.

Testing

Tested on macOS with NavX GNSS receiver.

Screenshots

RTK positioning active with NTRIP corrections (using Centipede network) :
Capture d’écran 2026-02-05 à 16 05 52

NTRIP client configuration panel :
Capture d’écran 2026-02-05 à 16 06 06

edgecase14 and others added 19 commits July 29, 2025 22:25
Resolved conflicts:
- Updated mReceiver to use std::unique_ptr (from upstream)
- Preserved NTRIP client functionality (from ntrip-client branch)
- Added onDeviceSocketStateChanged connection for NTRIP support
@qfield-fairy
Copy link
Collaborator

qfield-fairy commented Feb 6, 2026

🍎 MacOS DMG universal builds

Download a MacOS DMG universal build of this PR for testing.
(Built from commit ba90d60)

🪟 Windows builds

Download a Windows build of this PR for testing.
(Built from commit ba90d60)

📱 Android builds

Download an Android arm64 build of this PR for testing.
(Built from commit ba90d60)

Other Android architectures

🐧 Linux AppImage builds

Download a Linux AppImage build of this PR for testing.
(Built from commit ba90d60)

@nirvn
Copy link
Member

nirvn commented Feb 6, 2026

@natuition , that's super nice -- are you cooperating with @edgecase14 or you took his initial submission and pushed it further? In any case, it's super nice, I'm just trying to understand how this came to be :)

@nirvn nirvn closed this Feb 6, 2026
@nirvn nirvn reopened this Feb 6, 2026
@natuition
Copy link
Author

Hi, I took his initial proposal and expanded on it :) Having a client ntrip feature in your application is quite requested, and we believe it can open up many new use cases. I work for NATUITION, and we sell an RTK rover (https://natuition.odoo.com/en_GB/shop/n2168-navx-2678); we would be delighted to use it with your software.

@natuition
Copy link
Author

  • 🐧 Linux / build (linux) (pull_request)

Don't hesitate to give me feedback if I need to change anything; I'm happy to help. I tried building the Android version on my macOS M4, but I couldn't get your Docker image to work, so I'm currently exploring using the NDK version available through Android Studio and compiling it. I'd really appreciate knowing how to compile so I can continue contributing to the project if needed.

@natuition
Copy link
Author

I'm working on it today :)

@natuition
Copy link
Author

natuition commented Mar 10, 2026

Pull request Summary

Generation date: 2026-03-10
Analyzed branch: ntrip-client-finish
Reference branch: origin/master

Compared to origin/master, this branch mainly adds a full NTRIP workflow to QField, from connection and data handling in the core positioning stack to user-facing settings in QML. The implementation introduces dedicated NTRIP components, extends GNSS and Bluetooth behavior for better stability, and adds UI/translation updates so the new capabilities are configurable and visible to users.

Validation note: commit 4b832b0 was tested on both Android and macOS with NavX, and it is functional on both platforms.

What this pull request adds over master

  • A complete NTRIP client architecture in core positioning with three new components:
    NtripClient, NtripSocketClient, and NtripSourceTableFetcher.
  • Support for NTRIP protocol version handling, including settings and integration points used by fetcher/client flows.
  • Improved network/socket behavior around NTRIP connections, including better handling of headers, HTTP-related conditions, disconnections, and busy socket states.
  • RTCM data logging support in the NTRIP client pipeline.
  • NMEA sentence sending support to caster endpoints, including a user toggle in positioning settings.
  • Integration updates in positioning source and positioning management layers so NTRIP is wired into existing GNSS workflows.
  • UI updates in QML settings screens (QFieldSettings, PositioningSettings, and app QML wiring) to expose new NTRIP options.
  • French translation additions for NTRIP-related settings and labels.
  • Build system updates (CMakeLists.txt files) to compile and link the new NTRIP sources.
  • General code quality/maintainability updates (modern signal-slot syntax, destructor/noexcept cleanups, and formatting refactors) accompanying the feature work.

@natuition natuition marked this pull request as ready for review March 10, 2026 13:33
Co-authored-by: Matthias Kuhn <matthias@opengis.ch>
@natuition natuition requested a review from nirvn March 12, 2026 16:13
Copy link
Member

@nirvn nirvn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a quick look through it, it's progressing super nicely. I've dropped some comments based on a local build (without a Bluetooth device yet, that'll be tomorrow).

Comment on lines +177 to +207
const char *stateStr = nullptr;
switch ( int( mSocket->state() ) )
{
case int( QAbstractSocket::UnconnectedState ):
stateStr = "UnconnectedState";
break;
case int( QAbstractSocket::HostLookupState ):
stateStr = "HostLookupState";
break;
case int( QAbstractSocket::ConnectingState ):
stateStr = "ConnectingState";
break;
case int( QAbstractSocket::ConnectedState ):
stateStr = "ConnectedState";
break;
case int( QAbstractSocket::BoundState ):
stateStr = "BoundState";
break;
case int( QAbstractSocket::ClosingState ):
stateStr = "ClosingState";
break;
case int( QAbstractSocket::ListeningState ):
stateStr = "ListeningState";
break;
default:
stateStr = "UnknownState";
break;
}

qInfo() << "Bluetooth Socket State: Error:" << stateStr;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? If it is, I'd rather incorporate that into the error message we already have a few lines above. Also, we don't use const char * in the code, we stick to QString unless a function parameter requires otherwise.

Comment on lines +123 to +126
case QBluetoothSocket::SocketState::ServiceLookupState:
// Service discovery is part of the connection handshake, do not treat it as disconnected.
currentState = QAbstractSocket::ConnectingState;
break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing some nice fixes including this one that would be nice to have submitted as separate PRs. It'd make this PR a little smaller, and fixes easier to merge / backport.

Comment on lines +237 to +243
if ( mSocket->state() == QBluetoothSocket::SocketState::ServiceLookupState
|| mSocket->state() == QBluetoothSocket::SocketState::ConnectingState
|| mSocket->state() == QBluetoothSocket::SocketState::ConnectedState )
{
qInfo() << "BluetoothReceiver: Skipping connect attempt, socket busy in state" << mSocket->state();
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We handle the ConnectedState state situation in BluetoothReceiver::handleConnectDevice , if there's a need to add the ConnectingState and ServiceLookupState, we should do that over there, where we gracefully then attempt to disconnect and auto-connecting afterwards.

Comment on lines +5 to +7
begin : 05.02.2026
copyright : (C) 2026 by Vincent LAMBERT
email :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have an email from you? :) valid for all 6 files introduced into the source tree.

Comment on lines +74 to +79
Q_PROPERTY( QString ntripHost READ ntripHost WRITE setNtripHost NOTIFY ntripHostChanged )
Q_PROPERTY( int ntripPort READ ntripPort WRITE setNtripPort NOTIFY ntripPortChanged )
Q_PROPERTY( int ntripVersion READ ntripVersion WRITE setNtripVersion NOTIFY ntripVersionChanged )
Q_PROPERTY( QString ntripMountpoint READ ntripMountpoint WRITE setNtripMountpoint NOTIFY ntripMountpointChanged )
Q_PROPERTY( QString ntripUsername READ ntripUsername WRITE setNtripUsername NOTIFY ntripUsernameChanged )
Q_PROPERTY( QString ntripPassword READ ntripPassword WRITE setNtripPassword NOTIFY ntripPasswordChanged )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here instead of adding 6 properties, I'd prefer to pass on a single ntrip configuration. The actual configuration would be a NtripConfiguration class flagged as Q_GADGET so remote object handles its properties smoothly (just like we do with GnssPositionInformation). That'll give us a nice way to grow configuration settings without having to expose countless properties attached to the Positioning item itself.

The other big, big advantage here is that ATM, we disconnect and reconnect every time a property is changed. So, it means that when first passing on the properties when QField launches, we call the ntrip client's start() functions 6 distinct times. By passing on a single configuration property, we will only connect once :)

Comment on lines +2158 to +2159
onClicked: {
ntripFetcher.fetch(positioningSettings.ntripHost, positioningSettings.ntripPort, positioningSettings.ntripUsername, positioningSettings.ntripPassword, positioningSettings.ntripVersion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should clear the text in ntripFetchErrorLabel when re-fetching, it's otherwise quite confusing for users to understand a new fetching operation is ongoing.

Comment on lines +178 to +179
const int headerEnd = data.indexOf( "\r\n\r\n" );
const QByteArray body = ( headerEnd >= 0 ) ? data.mid( headerEnd + 4 ) : data;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic needs tweaking here. With the NTRIP server I'm testing here, the \r\n\r\n chunk arrives at the very end of the buffer.

You could trim the data, that'd remove the last \r\n\r\n.

Comment on lines +567 to +572
#ifdef WITH_BLUETOOTH
if ( auto bluetoothReceiver = dynamic_cast<BluetoothReceiver *>( mReceiver.get() ) )
{
connect( mNtripClient.get(), &NtripClient::correctionDataReceived, bluetoothReceiver, &BluetoothReceiver::onCorrectionDataReceived );
}
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can expand that to tcp and serial port.

And here - as well as when setting up the receiver - instead of an #ifdef , we should rely on a new receiver capability flag (see AbstractGnssReceiver::Capability).

Comment on lines +402 to +408
bool mNtripSendNmea = true;
QString mNtripHost = "crtk.net";
int mNtripPort = 2101;
int mNtripVersion = 1;
QString mNtripMountpoint = "NEAR";
QString mNtripUsername = "QfieldNtripClient";
QString mNtripPassword = "QfieldNtripClient";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will become a NtripConfiguration object. When we finalize the PR, let's make sure the default host, mount point, username and password are empty.

For the time being I understand it makes it super easy for you to test things out :)

Comment on lines +1964 to +1973
QfSwitch {
id: enableNtripClient
Layout.preferredWidth: implicitContentWidth
Layout.alignment: Qt.AlignTop
checked: positioningSettings.enableNtripClient
onCheckedChanged: {
positioningSettings.enableNtripClient = checked;
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When NTRIP is disabled here, we should hide the NTRIP configuration settings (you can see the network proxy in the general tab as a good example of that UX).

@nirvn
Copy link
Member

nirvn commented Mar 15, 2026

@natuition , oh BTW, this branch failed to rebase against current master for me. I assume a manual rebase on your end will be needed.

For the time being, I manually inserted changes and made a test branch (https://github.com/opengisch/QField/tree/ntrip_review) to test against latest Qt 6.10.2.

@natuition
Copy link
Author

Hello, I have a week of vacation, so I'll look at it later. Thanks for the feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants