diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9827c6f9ae..7c7895f34e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -37,6 +37,8 @@ set(QFIELD_CORE_SRCS positioning/udpreceiver.cpp positioning/positioning.cpp positioning/positioningsource.cpp + positioning/ntripclient.cpp + positioning/ntripsocketclient.cpp positioning/positioningdevicemodel.cpp positioning/geofencer.cpp positioning/positioninginformationmodel.cpp diff --git a/src/core/positioning/bluetoothreceiver.cpp b/src/core/positioning/bluetoothreceiver.cpp index ae3c32057e..fe73ca695d 100644 --- a/src/core/positioning/bluetoothreceiver.cpp +++ b/src/core/positioning/bluetoothreceiver.cpp @@ -89,6 +89,26 @@ BluetoothReceiver::~BluetoothReceiver() mSocket = nullptr; } +void BluetoothReceiver::onCorrectionDataReceived( const QByteArray &data ) +{ + if ( !mSocket || !mSocket->isOpen() ) + { + qWarning() << "Bluetooth socket not open—cannot forward corrections."; + return; + } + + qint64 bytesWritten = mSocket->write( data ); + if ( bytesWritten == -1 ) + { + qWarning() << "Failed to write corrections to Bluetooth socket:" << mSocket->errorString(); + } + else + { + qDebug() << "Forwarded" << bytesWritten << "bytes of correction data to Bluetooth."; + } +} + + void BluetoothReceiver::handleDisconnectDevice() { if ( mSocket->state() != QBluetoothSocket::SocketState::UnconnectedState ) @@ -137,6 +157,36 @@ void BluetoothReceiver::handleError( QBluetoothSocket::SocketError error ) } qInfo() << QStringLiteral( "BluetoothReceiver: Error: %1" ).arg( mLastError ); + 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; if ( mSocket->isOpen() ) { mSocket->close(); @@ -184,7 +234,7 @@ void BluetoothReceiver::repairDevice( const QBluetoothAddress &address ) case QBluetoothLocalDevice::Paired: case QBluetoothLocalDevice::AuthorizedPaired: { - mSocket->connectToService( address, QBluetoothUuid( QBluetoothUuid::ServiceClassUuid::SerialPort ), QBluetoothSocket::ReadOnly ); + mSocket->connectToService( address, QBluetoothUuid( QBluetoothUuid::ServiceClassUuid::SerialPort ), QBluetoothSocket::ReadWrite ); break; } @@ -206,7 +256,7 @@ void BluetoothReceiver::pairingFinished( const QBluetoothAddress &address, QBlue case QBluetoothLocalDevice::Paired: case QBluetoothLocalDevice::AuthorizedPaired: { - mSocket->connectToService( address, QBluetoothUuid( QBluetoothUuid::ServiceClassUuid::SerialPort ), QBluetoothSocket::ReadOnly ); + mSocket->connectToService( address, QBluetoothUuid( QBluetoothUuid::ServiceClassUuid::SerialPort ), QBluetoothSocket::ReadWrite ); break; } diff --git a/src/core/positioning/bluetoothreceiver.h b/src/core/positioning/bluetoothreceiver.h index 2ef38647e8..fd0ef4bcd6 100644 --- a/src/core/positioning/bluetoothreceiver.h +++ b/src/core/positioning/bluetoothreceiver.h @@ -37,6 +37,7 @@ class BluetoothReceiver : public NmeaGnssReceiver public slots: QString socketStateString() override; + void onCorrectionDataReceived( const QByteArray &data ); private slots: /** diff --git a/src/core/positioning/ntripclient.cpp b/src/core/positioning/ntripclient.cpp new file mode 100644 index 0000000000..0e3fb7d73c --- /dev/null +++ b/src/core/positioning/ntripclient.cpp @@ -0,0 +1,128 @@ +#include "ntripclient.h" +#include "ntripsocketclient.h" + +#include + +NtripClient::NtripClient( QObject *parent ) + : QObject( parent ) +{ +} + +NtripClient::~NtripClient() +{ + stop(); +} + +void NtripClient::start( const QString &ntripHost, const quint16 &port, const QString &mountpoint, const QString &username, const QString &password ) +{ + if ( mReply ) + { + qWarning() << "NtripClient already running"; + return; + } + + mBytesSent = 0; + mBytesReceived = 0; + + NtripSocketClient *client = new NtripSocketClient( this ); + + connect( client, &NtripSocketClient::correctionDataReceived, [this]( const QByteArray &data ) { + mBytesReceived += data.size(); + + quint8 firstByte = quint8( data.at( 0 ) ); + if ( firstByte == 0xD3 ) + { + qDebug() << "RTCM chunk:"; + } + else if ( firstByte == 0x73 ) + { + qDebug() << "SPARTN chunk:"; + } + else + { + qDebug() << "UNKNOWN chunk:"; + } + + qDebug() << data.size() << "bytes"; + // send to your GNSS device + emit correctionDataReceived( data ); + emit bytesCountersChanged(); + } ); + + connect( client, &NtripSocketClient::errorOccurred, [this]( const QString &msg ) { + qWarning() << msg; + emit errorOccurred( msg ); + } ); + + connect( client, &NtripSocketClient::streamConnected, [this]() { + emit streamConnected(); + } ); + + mBytesSent = client->start( + ntripHost, + port, + "/" + mountpoint, + username, + password ); + + // Emit immediately to show sent bytes + emit bytesCountersChanged(); + + + //connect(mReply, &QNetworkReply::readyRead, this, &NtripClient::onReadyRead); + //connect(mReply, &QNetworkReply::finished, this, &NtripClient::onFinished); + //connect(mReply, xxxQOverload::of(&QNetworkReply::errorOccurred), + // this, &NtripClient::onError); +} + +void NtripClient::stop() +{ + if ( mReply ) + { + disconnect( mReply, nullptr, this, nullptr ); // ✅ Disconnect all signals + + if ( mReply->isRunning() ) + { + mReply->abort(); // ✅ Cancel the request + } + mReply->deleteLater(); + mReply = nullptr; + } +} + +/* +void NtripClient::onReadyRead() +{ + QByteArray data = mReply->readAll(); + qInfo() << data + "\n"; + emit correctionDataReceived(data); +} +*/ + +void NtripClient::onFinished() +{ + if ( mReply ) + { + emit errorOccurred( "NTRIP connection closed" ); + } + // Schedule cleanup after Qt finishes emitting signals + QMetaObject::invokeMethod( this, "stop", Qt::QueuedConnection ); +} + +void NtripClient::onError( QNetworkReply::NetworkError code ) +{ + int status = mReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + QString reason = mReply->attribute( QNetworkRequest::HttpReasonPhraseAttribute ).toString(); + + qWarning() << "HTTP status during error:" << status << reason; + qWarning() << "Network error code:" << code; + + emit errorOccurred( + QStringLiteral( "Network error %1, HTTP %2 %3" ) + .arg( code ) + .arg( status ) + .arg( reason ) ); + //emit errorOccurred(QStringLiteral("NTRIP error: %1").arg(code)); + // Schedule cleanup after Qt finishes emitting signals + QMetaObject::invokeMethod( this, "stop", Qt::QueuedConnection ); +} diff --git a/src/core/positioning/ntripclient.h b/src/core/positioning/ntripclient.h new file mode 100644 index 0000000000..f80ad85924 --- /dev/null +++ b/src/core/positioning/ntripclient.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +class NtripClient : public QObject +{ + Q_OBJECT + public: + explicit NtripClient( QObject *parent = nullptr ); + ~NtripClient(); + + void start( const QString &ntripHost, const quint16 &port, const QString &mountpoint, const QString &username, const QString &password ); + + qint64 bytesSent() const { return mBytesSent; } + qint64 bytesReceived() const { return mBytesReceived; } + + public slots: + void stop(); + + signals: + void correctionDataReceived( const QByteArray &rtcmData ); + void errorOccurred( const QString &message ); + void bytesCountersChanged(); + void streamConnected(); + + private slots: + //void onReadyRead(); + void onFinished(); + void onError( QNetworkReply::NetworkError code ); + + private: + QNetworkAccessManager mNetworkManager; + QNetworkReply *mReply = nullptr; + qint64 mBytesSent = 0; + qint64 mBytesReceived = 0; +}; diff --git a/src/core/positioning/ntripsocketclient.cpp b/src/core/positioning/ntripsocketclient.cpp new file mode 100644 index 0000000000..dffe30e6b7 --- /dev/null +++ b/src/core/positioning/ntripsocketclient.cpp @@ -0,0 +1,103 @@ +#include "ntripsocketclient.h" + +#include + +NtripSocketClient::NtripSocketClient( QObject *parent ) + : QObject( parent ) +{ + connect( &mSocket, &QTcpSocket::connected, this, &NtripSocketClient::onConnected ); + connect( &mSocket, &QTcpSocket::readyRead, this, &NtripSocketClient::onReadyRead ); + connect( &mSocket, &QTcpSocket::disconnected, this, &NtripSocketClient::onDisconnected ); + connect( &mSocket, QOverload::of( &QTcpSocket::errorOccurred ), + this, &NtripSocketClient::onSocketError ); +} + +NtripSocketClient::~NtripSocketClient() +{ + stop(); +} + +qint64 NtripSocketClient::start( + const QString &host, + quint16 port, + const QString &mountpoint, + const QString &username, + const QString &password ) +{ + mHeadersSent = false; + mSocket.connectToHost( host, port ); + + QString credentials = username + ":" + password; + QByteArray base64 = credentials.toUtf8().toBase64(); + + QByteArray request; + request.append( "GET " + mountpoint.toUtf8() + " HTTP/1.0\r\n" ); + request.append( "Host: " + host.toUtf8() + ":" + QByteArray::number( port ) + "\r\n" ); + request.append( "User-Agent: QField NTRIP QtSocketClient/1.0\r\n" ); + request.append( "Accept: */*\r\n" ); + request.append( "Authorization: Basic " + base64 + "\r\n" ); + request.append( "Connection: close\r\n" ); + //request.append("Ntrip-Version: Ntrip/2.0\r\n"); + request.append( "\r\n" ); + + connect( &mSocket, &QTcpSocket::connected, [this, request]() { + mSocket.write( request ); + mSocket.flush(); + } ); + + return request.size(); +} + +void NtripSocketClient::stop() +{ + if ( mSocket.isOpen() ) + { + mSocket.disconnectFromHost(); + mSocket.close(); + } +} + +void NtripSocketClient::onConnected() +{ + qDebug() << "Connected to NTRIP caster."; +} + +void NtripSocketClient::onReadyRead() +{ + QByteArray data = mSocket.readAll(); + + // If headers not processed yet, discard them + if ( !mHeadersSent ) + { + int headerEnd = data.indexOf( "\r\n\r\n" ); + if ( headerEnd != -1 ) + { + QByteArray headerData = data.left( headerEnd ); + qDebug() << "Received HTTP headers:\n" + << headerData; + data = data.mid( headerEnd + 4 ); + mHeadersSent = true; + emit streamConnected(); + } + else + { + // Wait for more data + return; + } + } + + if ( !data.isEmpty() ) + { + emit correctionDataReceived( data ); + } +} + +void NtripSocketClient::onDisconnected() +{ + emit errorOccurred( "Disconnected from NTRIP caster." ); +} + +void NtripSocketClient::onSocketError( QAbstractSocket::SocketError error ) +{ + emit errorOccurred( "Socket error: " + QString::number( error ) + " (" + mSocket.errorString() + ")" ); +} diff --git a/src/core/positioning/ntripsocketclient.h b/src/core/positioning/ntripsocketclient.h new file mode 100644 index 0000000000..3764b1fcb2 --- /dev/null +++ b/src/core/positioning/ntripsocketclient.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +class NtripSocketClient : public QObject +{ + Q_OBJECT + public: + explicit NtripSocketClient( QObject *parent = nullptr ); + ~NtripSocketClient(); + + qint64 start( + const QString &host, + quint16 port, + const QString &mountpoint, + const QString &username, + const QString &password ); + + void stop(); + + signals: + void correctionDataReceived( const QByteArray &data ); + void errorOccurred( const QString &message ); + void streamConnected(); + + private slots: + void onConnected(); + void onReadyRead(); + void onDisconnected(); + void onSocketError( QAbstractSocket::SocketError error ); + + private: + QTcpSocket mSocket; + bool mHeadersSent = false; +}; diff --git a/src/core/positioning/positioning.cpp b/src/core/positioning/positioning.cpp index 785af6eef3..81e3f856d0 100644 --- a/src/core/positioning/positioning.cpp +++ b/src/core/positioning/positioning.cpp @@ -86,6 +86,15 @@ void Positioning::setupSource() connect( mPositioningSourceReplica.data(), SIGNAL( orientationChanged() ), this, SIGNAL( orientationChanged() ) ); connect( mPositioningSourceReplica.data(), SIGNAL( loggingChanged() ), this, SIGNAL( loggingChanged() ) ); connect( mPositioningSourceReplica.data(), SIGNAL( loggingPathChanged() ), this, SIGNAL( loggingPathChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( enableNtripClientChanged() ), this, SIGNAL( enableNtripClientChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripHostChanged() ), this, SIGNAL( ntripHostChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripPortChanged() ), this, SIGNAL( ntripPortChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripMountpointChanged() ), this, SIGNAL( ntripMountpointChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripUsernameChanged() ), this, SIGNAL( ntripUsernameChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripPasswordChanged() ), this, SIGNAL( ntripPasswordChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripStatusChanged() ), this, SIGNAL( ntripStatusChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripBytesSentChanged() ), this, SIGNAL( ntripBytesSentChanged() ) ); + connect( mPositioningSourceReplica.data(), SIGNAL( ntripBytesReceivedChanged() ), this, SIGNAL( ntripBytesReceivedChanged() ) ); connect( mPositioningSourceReplica.data(), SIGNAL( positionInformationChanged() ), this, SLOT( processGnssPositionInformation() ) ); @@ -408,6 +417,114 @@ void Positioning::setBackgroundMode( bool backgroundMode ) emit backgroundModeChanged(); } +bool Positioning::enableNtripClient() const +{ + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "enableNtripClient" ) : mPropertiesToSync.value( "enableNtripClient", false ) ).toBool(); +} + +void Positioning::setEnableNtripClient( bool enableNtripClient ) +{ + if ( isSourceAvailable() ) + { + mPositioningSourceReplica->setProperty( "enableNtripClient", enableNtripClient ); + } + else + { + mPropertiesToSync["enableNtripClient"] = enableNtripClient; + emit enableNtripClientChanged(); + } +} + +QString Positioning::ntripHost() const +{ + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "ntripHost" ) : mPropertiesToSync.value( "ntripHost", "" ) ).toString(); +} + +void Positioning::setNtripHost( const QString &ntripHost ) +{ + if ( isSourceAvailable() ) + { + mPositioningSourceReplica->setProperty( "ntripHost", ntripHost ); + } + else + { + mPropertiesToSync["ntripHost"] = ntripHost; + emit ntripHostChanged(); + } +} + +int Positioning::ntripPort() const +{ + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "ntripPort" ) : mPropertiesToSync.value( "ntripPort", 2101 ) ).toInt(); +} + +void Positioning::setNtripPort( int ntripPort ) +{ + if ( isSourceAvailable() ) + { + mPositioningSourceReplica->setProperty( "ntripPort", ntripPort ); + } + else + { + mPropertiesToSync["ntripPort"] = ntripPort; + emit ntripPortChanged(); + } +} + +QString Positioning::ntripMountpoint() const +{ + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "ntripMountpoint" ) : mPropertiesToSync.value( "ntripMountpoint", "" ) ).toString(); +} + +void Positioning::setNtripMountpoint( const QString &ntripMountpoint ) +{ + if ( isSourceAvailable() ) + { + mPositioningSourceReplica->setProperty( "ntripMountpoint", ntripMountpoint ); + } + else + { + mPropertiesToSync["ntripMountpoint"] = ntripMountpoint; + emit ntripMountpointChanged(); + } +} + +QString Positioning::ntripUsername() const +{ + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "ntripUsername" ) : mPropertiesToSync.value( "ntripUsername", "" ) ).toString(); +} + +void Positioning::setNtripUsername( const QString &ntripUsername ) +{ + if ( isSourceAvailable() ) + { + mPositioningSourceReplica->setProperty( "ntripUsername", ntripUsername ); + } + else + { + mPropertiesToSync["ntripUsername"] = ntripUsername; + emit ntripUsernameChanged(); + } +} + +QString Positioning::ntripPassword() const +{ + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "ntripPassword" ) : mPropertiesToSync.value( "ntripPassword", "" ) ).toString(); +} + +void Positioning::setNtripPassword( const QString &ntripPassword ) +{ + if ( isSourceAvailable() ) + { + mPositioningSourceReplica->setProperty( "ntripPassword", ntripPassword ); + } + else + { + mPropertiesToSync["ntripPassword"] = ntripPassword; + emit ntripPasswordChanged(); + } +} + QList Positioning::getBackgroundPositionInformation() const { QList positionInformationList; @@ -423,6 +540,21 @@ QList Positioning::getBackgroundPositionInformation() c return positionInformationList; } +QString Positioning::ntripStatus() const +{ + return isSourceAvailable() ? mPositioningSourceReplica->property( "ntripStatus" ).toString() : QString(); +} + +qint64 Positioning::ntripBytesSent() const +{ + return isSourceAvailable() ? mPositioningSourceReplica->property( "ntripBytesSent" ).toLongLong() : 0; +} + +qint64 Positioning::ntripBytesReceived() const +{ + return isSourceAvailable() ? mPositioningSourceReplica->property( "ntripBytesReceived" ).toLongLong() : 0; +} + PositioningSource::ElevationCorrectionMode Positioning::elevationCorrectionMode() const { return static_cast( ( isSourceAvailable() ? mPositioningSourceReplica->property( "elevationCorrectionMode" ) : mPropertiesToSync.value( "elevationCorrectionMode", static_cast( PositioningSource::ElevationCorrectionMode::None ) ) ).toInt() ); diff --git a/src/core/positioning/positioning.h b/src/core/positioning/positioning.h index 280d19dbd7..8225516302 100644 --- a/src/core/positioning/positioning.h +++ b/src/core/positioning/positioning.h @@ -66,6 +66,15 @@ class Positioning : public QObject Q_PROPERTY( QString loggingPath READ loggingPath WRITE setLoggingPath NOTIFY loggingPathChanged ) Q_PROPERTY( bool backgroundMode READ backgroundMode WRITE setBackgroundMode NOTIFY backgroundModeChanged ) + Q_PROPERTY( bool enableNtripClient READ enableNtripClient WRITE setEnableNtripClient NOTIFY enableNtripClientChanged ) + Q_PROPERTY( QString ntripHost READ ntripHost WRITE setNtripHost NOTIFY ntripHostChanged ) + Q_PROPERTY( int ntripPort READ ntripPort WRITE setNtripPort NOTIFY ntripPortChanged ) + 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 ) + Q_PROPERTY( QString ntripStatus READ ntripStatus NOTIFY ntripStatusChanged ) + Q_PROPERTY( qint64 ntripBytesSent READ ntripBytesSent NOTIFY ntripBytesSentChanged ) + Q_PROPERTY( qint64 ntripBytesReceived READ ntripBytesReceived NOTIFY ntripBytesReceivedChanged ) public: explicit Positioning( QObject *parent = nullptr ); @@ -244,6 +253,81 @@ class Positioning : public QObject */ void setBackgroundMode( bool backgroundMode ); + /** + * Returns TRUE if the NTRIP client is enabled. + */ + bool enableNtripClient() const; + + /** + * Sets whether the NTRIP client is enabled. + */ + void setEnableNtripClient( bool enableNtripClient ); + + /** + * Returns the NTRIP host server address. + */ + QString ntripHost() const; + + /** + * Sets the NTRIP host server address. + */ + void setNtripHost( const QString &ntripHost ); + + /** + * Returns the NTRIP server port. + */ + int ntripPort() const; + + /** + * Sets the NTRIP server port. + */ + void setNtripPort( int ntripPort ); + + /** + * Returns the NTRIP mountpoint. + */ + QString ntripMountpoint() const; + + /** + * Sets the NTRIP mountpoint. + */ + void setNtripMountpoint( const QString &ntripMountpoint ); + + /** + * Returns the NTRIP username. + */ + QString ntripUsername() const; + + /** + * Sets the NTRIP username. + */ + void setNtripUsername( const QString &ntripUsername ); + + /** + * Returns the NTRIP password. + */ + QString ntripPassword() const; + + /** + * Sets the NTRIP password. + */ + void setNtripPassword( const QString &ntripPassword ); + + /** + * Returns the current NTRIP connection status. + */ + QString ntripStatus() const; + + /** + * Returns the number of bytes sent via NTRIP. + */ + qint64 ntripBytesSent() const; + + /** + * Returns the number of bytes received via NTRIP. + */ + qint64 ntripBytesReceived() const; + /** * Returns a list of position information collected while background mode is active. * \see backgroundMode() @@ -269,6 +353,15 @@ class Positioning : public QObject void loggingChanged(); void loggingPathChanged(); void backgroundModeChanged(); + void enableNtripClientChanged(); + void ntripHostChanged(); + void ntripPortChanged(); + void ntripMountpointChanged(); + void ntripUsernameChanged(); + void ntripPasswordChanged(); + void ntripStatusChanged(); + void ntripBytesSentChanged(); + void ntripBytesReceivedChanged(); void triggerConnectDevice(); void triggerDisconnectDevice(); diff --git a/src/core/positioning/positioningsource.cpp b/src/core/positioning/positioningsource.cpp index a4f3e660c6..8858de3101 100644 --- a/src/core/positioning/positioningsource.cpp +++ b/src/core/positioning/positioningsource.cpp @@ -23,6 +23,7 @@ #include "egenioussreceiver.h" #include "filereceiver.h" #include "internalgnssreceiver.h" +#include "ntripclient.h" #include "positioningsource.h" #include "positioningutils.h" #include "tcpreceiver.h" @@ -178,6 +179,111 @@ void PositioningSource::setBackgroundMode( bool backgroundMode ) emit backgroundModeChanged(); } +void PositioningSource::setEnableNtripClient( bool enableNtripClient ) +{ + if ( mEnableNtripClient == enableNtripClient ) + return; + + mEnableNtripClient = enableNtripClient; + + // Start or stop NTRIP client based on the setting + if ( mEnableNtripClient ) + { + startNtripClient(); + } + else + { + stopNtripClient(); + } + + emit enableNtripClientChanged(); +} + +void PositioningSource::setNtripHost( const QString &ntripHost ) +{ + if ( mNtripHost == ntripHost ) + return; + + mNtripHost = ntripHost; + + // Restart NTRIP client if enabled and parameters changed + if ( mEnableNtripClient && mNtripClient ) + { + stopNtripClient(); + startNtripClient(); + } + + emit ntripHostChanged(); +} + +void PositioningSource::setNtripPort( int ntripPort ) +{ + if ( mNtripPort == ntripPort ) + return; + + mNtripPort = ntripPort; + + // Restart NTRIP client if enabled and parameters changed + if ( mEnableNtripClient && mNtripClient ) + { + stopNtripClient(); + startNtripClient(); + } + + emit ntripPortChanged(); +} + +void PositioningSource::setNtripMountpoint( const QString &ntripMountpoint ) +{ + if ( mNtripMountpoint == ntripMountpoint ) + return; + + mNtripMountpoint = ntripMountpoint; + + // Restart NTRIP client if enabled and parameters changed + if ( mEnableNtripClient && mNtripClient ) + { + stopNtripClient(); + startNtripClient(); + } + + emit ntripMountpointChanged(); +} + +void PositioningSource::setNtripUsername( const QString &ntripUsername ) +{ + if ( mNtripUsername == ntripUsername ) + return; + + mNtripUsername = ntripUsername; + + // Restart NTRIP client if enabled and parameters changed + if ( mEnableNtripClient && mNtripClient ) + { + stopNtripClient(); + startNtripClient(); + } + + emit ntripUsernameChanged(); +} + +void PositioningSource::setNtripPassword( const QString &ntripPassword ) +{ + if ( mNtripPassword == ntripPassword ) + return; + + mNtripPassword = ntripPassword; + + // Restart NTRIP client if enabled and parameters changed + if ( mEnableNtripClient && mNtripClient ) + { + stopNtripClient(); + startNtripClient(); + } + + emit ntripPasswordChanged(); +} + QList PositioningSource::getBackgroundPositionInformation() const { QList positionInformationList; @@ -228,13 +334,18 @@ void PositioningSource::setupDevice() disconnect( mReceiver, &AbstractGnssReceiver::lastGnssPositionInformationChanged, this, &PositioningSource::lastGnssPositionInformationChanged ); disconnect( mReceiver, &AbstractGnssReceiver::lastErrorChanged, this, &PositioningSource::deviceLastErrorChanged ); disconnect( mReceiver, &AbstractGnssReceiver::socketStateChanged, this, &PositioningSource::deviceSocketStateChanged ); + disconnect( mReceiver, &AbstractGnssReceiver::socketStateChanged, this, &PositioningSource::onDeviceSocketStateChanged ); disconnect( mReceiver, &AbstractGnssReceiver::socketStateStringChanged, this, &PositioningSource::deviceSocketStateStringChanged ); mReceiver->deleteLater(); mReceiver = nullptr; + + // Stop NTRIP client when receiver is being replaced + stopNtripClient(); } if ( mDeviceId.isEmpty() ) { + // Using internal receiver - NTRIP client not needed mReceiver = new InternalGnssReceiver( this ); } else @@ -283,6 +394,12 @@ void PositioningSource::setupDevice() { #ifdef WITH_BLUETOOTH mReceiver = new BluetoothReceiver( mDeviceId, this ); + + // Start NTRIP client if enabled for Bluetooth receivers + if ( mEnableNtripClient ) + { + startNtripClient(); + } #endif } } @@ -292,6 +409,7 @@ void PositioningSource::setupDevice() connect( mReceiver, &AbstractGnssReceiver::lastGnssPositionInformationChanged, this, &PositioningSource::lastGnssPositionInformationChanged ); connect( mReceiver, &AbstractGnssReceiver::lastErrorChanged, this, &PositioningSource::deviceLastErrorChanged ); connect( mReceiver, &AbstractGnssReceiver::socketStateChanged, this, &PositioningSource::deviceSocketStateChanged ); + connect( mReceiver, &AbstractGnssReceiver::socketStateChanged, this, &PositioningSource::onDeviceSocketStateChanged ); connect( mReceiver, &AbstractGnssReceiver::socketStateStringChanged, this, &PositioningSource::deviceSocketStateStringChanged ); setValid( mReceiver->valid() ); @@ -395,6 +513,25 @@ void PositioningSource::processCompassReading() } } +void PositioningSource::onDeviceSocketStateChanged() +{ + if ( mReceiver ) + { + QAbstractSocket::SocketState state = mReceiver->socketState(); + + // Stop NTRIP client when receiver is disconnected or has connection error + if ( mNtripClient && ( state == QAbstractSocket::UnconnectedState || state == QAbstractSocket::ClosingState ) ) + { + stopNtripClient(); + } + // Start NTRIP client when external receiver connects and setting is enabled + else if ( !mNtripClient && mEnableNtripClient && !mDeviceId.isEmpty() && state == QAbstractSocket::ConnectedState ) + { + startNtripClient(); + } + } +} + void PositioningSource::triggerConnectDevice() { if ( mReceiver ) @@ -410,3 +547,82 @@ void PositioningSource::triggerDisconnectDevice() mReceiver->disconnectDevice(); } } + +void PositioningSource::startNtripClient() +{ + // Only start NTRIP client if we have an external receiver that can use RTK corrections + if ( mReceiver && !mDeviceId.isEmpty() ) + { + // Check that all required NTRIP parameters are configured + if ( mNtripHost.isEmpty() || mNtripMountpoint.isEmpty() || mNtripUsername.isEmpty() || mNtripPassword.isEmpty() ) + { + setNtripStatus( QStringLiteral( "Missing parameters" ) ); + qWarning() << "NTRIP Client: Missing required connection parameters (host, mountpoint, username, or password)"; + return; + } + + if ( !mNtripClient ) + mNtripClient = std::make_unique( this ); + + // Reset byte counters + mNtripBytesSent = 0; + mNtripBytesReceived = 0; + emit ntripBytesSentChanged(); + emit ntripBytesReceivedChanged(); + + setNtripStatus( QStringLiteral( "Connecting..." ) ); + mNtripClient->start( mNtripHost, static_cast( mNtripPort ), mNtripMountpoint, mNtripUsername, mNtripPassword ); + + // Connect to receiver if it supports RTK corrections +#ifdef WITH_BLUETOOTH + if ( auto bluetoothReceiver = dynamic_cast( mReceiver ) ) + { + connect( mNtripClient.get(), &NtripClient::correctionDataReceived, bluetoothReceiver, &BluetoothReceiver::onCorrectionDataReceived ); + } +#endif + + // Track connection status through signals + connect( mNtripClient.get(), &NtripClient::streamConnected, + this, [this]() { + setNtripStatus( QStringLiteral( "Connected" ) ); + } ); + + connect( mNtripClient.get(), &NtripClient::errorOccurred, + this, [this]( const QString &msg ) { + setNtripStatus( QStringLiteral( "Error: %1" ).arg( msg ) ); + qWarning() << "NTRIP Client Error:" << msg; + } ); + + // Track byte counters + connect( mNtripClient.get(), &NtripClient::bytesCountersChanged, + this, [this]() { + mNtripBytesSent = mNtripClient->bytesSent(); + mNtripBytesReceived = mNtripClient->bytesReceived(); + emit ntripBytesSentChanged(); + emit ntripBytesReceivedChanged(); + } ); + } + else + { + setNtripStatus( QStringLiteral( "No external receiver" ) ); + } +} + +void PositioningSource::stopNtripClient() +{ + if ( mNtripClient ) + { + mNtripClient->stop(); + mNtripClient.reset(); + } + setNtripStatus( QStringLiteral( "Disconnected" ) ); +} + +void PositioningSource::setNtripStatus( const QString &status ) +{ + if ( mNtripStatus == status ) + return; + + mNtripStatus = status; + emit ntripStatusChanged(); +} diff --git a/src/core/positioning/positioningsource.h b/src/core/positioning/positioningsource.h index b1be3f9173..d5ecf75aa0 100644 --- a/src/core/positioning/positioningsource.h +++ b/src/core/positioning/positioningsource.h @@ -19,6 +19,7 @@ #include "abstractgnssreceiver.h" #include "gnsspositioninformation.h" +#include "ntripclient.h" #include #include @@ -56,6 +57,15 @@ class PositioningSource : public QObject Q_PROPERTY( QString loggingPath READ loggingPath WRITE setLoggingPath NOTIFY loggingPathChanged ) Q_PROPERTY( bool backgroundMode READ backgroundMode WRITE setBackgroundMode NOTIFY backgroundModeChanged ) + Q_PROPERTY( bool enableNtripClient READ enableNtripClient WRITE setEnableNtripClient NOTIFY enableNtripClientChanged ) + Q_PROPERTY( QString ntripHost READ ntripHost WRITE setNtripHost NOTIFY ntripHostChanged ) + Q_PROPERTY( int ntripPort READ ntripPort WRITE setNtripPort NOTIFY ntripPortChanged ) + 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 ) + Q_PROPERTY( QString ntripStatus READ ntripStatus NOTIFY ntripStatusChanged ) + Q_PROPERTY( qint64 ntripBytesSent READ ntripBytesSent NOTIFY ntripBytesSentChanged ) + Q_PROPERTY( qint64 ntripBytesReceived READ ntripBytesReceived NOTIFY ntripBytesReceivedChanged ) public: /** @@ -223,6 +233,81 @@ class PositioningSource : public QObject */ void setBackgroundMode( bool backgroundMode ); + /** + * Returns TRUE if the NTRIP client is enabled. + */ + bool enableNtripClient() const { return mEnableNtripClient; } + + /** + * Sets whether the NTRIP client is enabled. + */ + void setEnableNtripClient( bool enableNtripClient ); + + /** + * Returns the NTRIP host server address. + */ + QString ntripHost() const { return mNtripHost; } + + /** + * Sets the NTRIP host server address. + */ + void setNtripHost( const QString &ntripHost ); + + /** + * Returns the NTRIP server port. + */ + int ntripPort() const { return mNtripPort; } + + /** + * Sets the NTRIP server port. + */ + void setNtripPort( int ntripPort ); + + /** + * Returns the NTRIP mountpoint. + */ + QString ntripMountpoint() const { return mNtripMountpoint; } + + /** + * Sets the NTRIP mountpoint. + */ + void setNtripMountpoint( const QString &ntripMountpoint ); + + /** + * Returns the NTRIP username. + */ + QString ntripUsername() const { return mNtripUsername; } + + /** + * Sets the NTRIP username. + */ + void setNtripUsername( const QString &ntripUsername ); + + /** + * Returns the NTRIP password. + */ + QString ntripPassword() const { return mNtripPassword; } + + /** + * Sets the NTRIP password. + */ + void setNtripPassword( const QString &ntripPassword ); + + /** + * Returns the current NTRIP connection status. + */ + QString ntripStatus() const { return mNtripStatus; } + + /** + * Returns the number of bytes sent via NTRIP. + */ + qint64 ntripBytesSent() const { return mNtripBytesSent; } + + /** + * Returns the number of bytes received via NTRIP. + */ + qint64 ntripBytesReceived() const { return mNtripBytesReceived; } + /** * Returns a list of position information collected while background mode is active. * \see backgroundMode() @@ -249,6 +334,15 @@ class PositioningSource : public QObject void loggingChanged(); void loggingPathChanged(); void backgroundModeChanged(); + void enableNtripClientChanged(); + void ntripHostChanged(); + void ntripPortChanged(); + void ntripMountpointChanged(); + void ntripUsernameChanged(); + void ntripPasswordChanged(); + void ntripStatusChanged(); + void ntripBytesSentChanged(); + void ntripBytesReceivedChanged(); public slots: @@ -259,9 +353,13 @@ class PositioningSource : public QObject void lastGnssPositionInformationChanged( const GnssPositionInformation &lastGnssPositionInformation ); void processCompassReading(); + void onDeviceSocketStateChanged(); private: void setupDevice(); + void startNtripClient(); + void stopNtripClient(); + void setNtripStatus( const QString &status ); bool mActive = false; @@ -280,8 +378,18 @@ class PositioningSource : public QObject QString mLoggingPath; bool mBackgroundMode = false; + bool mEnableNtripClient = false; + QString mNtripHost; + int mNtripPort = 2101; + QString mNtripMountpoint; + QString mNtripUsername; + QString mNtripPassword; + QString mNtripStatus; + qint64 mNtripBytesSent = 0; + qint64 mNtripBytesReceived = 0; AbstractGnssReceiver *mReceiver = nullptr; + std::unique_ptr mNtripClient; QCompass mCompass; QTimer mCompassTimer; diff --git a/src/qml/PositioningSettings.qml b/src/qml/PositioningSettings.qml index a2d78bd372..2bb53fa365 100644 --- a/src/qml/PositioningSettings.qml +++ b/src/qml/PositioningSettings.qml @@ -15,6 +15,16 @@ Settings { property bool showPositionInformation: false + property bool enableNtripClient: false + property string ntripHost: "" + property int ntripPort: 2101 + property string ntripMountpoint: "" + property string ntripUsername: "" + property string ntripPassword: "" + property string ntripStatus: "" + property int ntripBytesSent: 0 + property int ntripBytesReceived: 0 + property bool alwaysShowPreciseView: false property real preciseViewPrecision: 2.5 property bool preciseViewProximityAlarm: true diff --git a/src/qml/QFieldSettings.qml b/src/qml/QFieldSettings.qml index 936f5e57ae..6a5fe12c18 100644 --- a/src/qml/QFieldSettings.qml +++ b/src/qml/QFieldSettings.qml @@ -1622,6 +1622,186 @@ Page { positioningSettings.logging = checked; } } + + Label { + Layout.fillWidth: true + Layout.columnSpan: 2 + Layout.topMargin: 20 + Layout.bottomMargin: 10 + text: qsTr("NTRIP Client") + font: Theme.strongFont + color: Theme.mainColor + } + + Rectangle { + Layout.fillWidth: true + Layout.columnSpan: 2 + Layout.bottomMargin: 10 + height: 1 + color: Theme.mainColor + } + + RowLayout { + Layout.fillWidth: true + Layout.columnSpan: 2 + spacing: 10 + + Label { + text: qsTr("Enable NTRIP client") + font: Theme.defaultFont + color: Theme.mainTextColor + wrapMode: Text.WordWrap + Layout.preferredWidth: 120 + + MouseArea { + anchors.fill: parent + onClicked: enableNtripClient.toggle() + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + Label { + text: positioningSettings.ntripStatus || "" + font: Theme.tipFont + color: Theme.secondaryTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Label { + text: positioningSettings.enableNtripClient ? qsTr("↑%1 ↓%2").arg(positioningSettings.ntripBytesSent).arg(positioningSettings.ntripBytesReceived) : "" + font: Theme.tipFont + color: Theme.secondaryTextColor + } + } + + QfSwitch { + id: enableNtripClient + Layout.preferredWidth: implicitContentWidth + Layout.alignment: Qt.AlignTop + checked: positioningSettings.enableNtripClient + onCheckedChanged: { + positioningSettings.enableNtripClient = checked; + } + } + } + + // NTRIP Host + RowLayout { + Layout.fillWidth: true + Layout.columnSpan: 2 + spacing: 10 + + Label { + text: qsTr("NTRIP Host") + font: Theme.defaultFont + color: Theme.mainTextColor + wrapMode: Text.WordWrap + Layout.preferredWidth: 120 + } + + QfTextField { + id: ntripHost + Layout.fillWidth: true + text: positioningSettings.ntripHost + placeholderText: qsTr("e.g. ntrip.example.com") + onTextChanged: { + positioningSettings.ntripHost = text; + } + } + } + + // NTRIP Port + Label { + text: qsTr("NTRIP Port") + font: Theme.defaultFont + color: Theme.mainTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + QfTextField { + id: ntripPort + Layout.fillWidth: true + text: positioningSettings.ntripPort || "2101" + validator: IntValidator { + bottom: 1 + top: 65535 + } + onTextChanged: { + positioningSettings.ntripPort = parseInt(text) || 2101; + } + } + + // NTRIP Username + RowLayout { + Layout.fillWidth: true + Layout.columnSpan: 2 + spacing: 10 + + Label { + text: qsTr("NTRIP Username") + font: Theme.defaultFont + color: Theme.mainTextColor + wrapMode: Text.WordWrap + Layout.preferredWidth: 120 + } + + QfTextField { + id: ntripUsername + Layout.fillWidth: true + text: positioningSettings.ntripUsername + onTextChanged: { + positioningSettings.ntripUsername = text; + } + } + } + + // NTRIP Password + RowLayout { + Layout.fillWidth: true + Layout.columnSpan: 2 + spacing: 10 + + Label { + text: qsTr("NTRIP Password") + font: Theme.defaultFont + color: Theme.mainTextColor + wrapMode: Text.WordWrap + Layout.preferredWidth: 120 + } + + QfTextField { + id: ntripPassword + Layout.fillWidth: true + text: positioningSettings.ntripPassword + echoMode: TextInput.Password + onTextChanged: { + positioningSettings.ntripPassword = text; + } + } + } + + // NTRIP Mountpoint + Label { + text: qsTr("NTRIP Mountpoint") + font: Theme.defaultFont + color: Theme.mainTextColor + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + QfTextField { + id: ntripMountpoint + Layout.fillWidth: true + text: positioningSettings.ntripMountpoint + onTextChanged: { + positioningSettings.ntripMountpoint = text; + } + } } Item { diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index a7c0ae82d0..459fb1ee04 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -351,6 +351,24 @@ ApplicationWindow { loggingPath: platformUtilities.appDataDirs()[0] + "/logs" logging: positioningSettings.logging + enableNtripClient: positioningSettings.enableNtripClient + ntripHost: positioningSettings.ntripHost + ntripPort: positioningSettings.ntripPort + ntripMountpoint: positioningSettings.ntripMountpoint + ntripUsername: positioningSettings.ntripUsername + ntripPassword: positioningSettings.ntripPassword + + onNtripStatusChanged: { + positioningSettings.ntripStatus = ntripStatus; + } + + onNtripBytesSentChanged: { + positioningSettings.ntripBytesSent = ntripBytesSent; + } + + onNtripBytesReceivedChanged: { + positioningSettings.ntripBytesReceived = ntripBytesReceived; + } onPositionInformationChanged: { if (active) { @@ -3595,6 +3613,21 @@ ApplicationWindow { onCheckedChanged: positioningSettings.showPositionInformation = checked } + MenuItem { + text: qsTr("Enable NTRIP Client") + height: 48 + leftPadding: Theme.menuItemCheckLeftPadding + font: Theme.defaultFont + + checkable: true + checked: positioningSettings.enableNtripClient + indicator.height: 20 + indicator.width: 20 + indicator.implicitHeight: 24 + indicator.implicitWidth: 24 + onCheckedChanged: positioningSettings.enableNtripClient = checked + } + MenuItem { text: qsTr("Positioning Settings") height: 48