From 136ccfec83952f31a23ce2fde2b3f3e78d02b655 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 27 Jul 2026 11:12:46 +0200 Subject: [PATCH 01/21] Squash and merge trimble prototype branch --- .github/workflows/android.yml | 13 + .github/workflows/ios.yml | 13 + .gitignore | 4 +- CMakeLists.txt | 21 + CMakePresets.json | 100 +++++ app/CMakeLists.txt | 24 ++ app/main.cpp | 6 + app/maptools/recordingmaptool.cpp | 6 +- app/position/geoposition.h | 7 + app/position/positionkit.cpp | 67 +++- app/position/positionkit.h | 4 + .../providers/abstractpositionprovider.cpp | 5 + .../providers/abstractpositionprovider.h | 8 + .../providers/trimblepositionprovider.cpp | 363 ++++++++++++++++++ .../providers/trimblepositionprovider.h | 72 ++++ app/position/providers/trimbleregistration.h | 52 +++ .../providers/trimbleregistration_android.cpp | 105 +++++ .../providers/trimbleregistration_ios.mm | 122 ++++++ app/position/providers/trimblesecrets.cpp.enc | Bin 0 -> 512 bytes app/qml/gps/MMGpsDataDrawer.qml | 2 +- app/qml/gps/MMPositionProviderPage.qml | 7 +- app/qml/gps/MMProviderTypeDrawer.qml | 9 + app/qml/settings/MMSettingsPage.qml | 56 ++- app/test/testposition.cpp | 96 +++++ app/test/testposition.h | 6 + cmake_templates/iOSInfo.plist.in | 21 + vcpkg.json | 1 + 27 files changed, 1171 insertions(+), 19 deletions(-) create mode 100644 CMakePresets.json create mode 100644 app/position/providers/trimblepositionprovider.cpp create mode 100644 app/position/providers/trimblepositionprovider.h create mode 100644 app/position/providers/trimbleregistration.h create mode 100644 app/position/providers/trimbleregistration_android.cpp create mode 100644 app/position/providers/trimbleregistration_ios.mm create mode 100644 app/position/providers/trimblesecrets.cpp.enc diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4d859f3ec..78c2fac96 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -112,6 +112,18 @@ jobs: -k "$MERGINSECRETS_DECRYPT_KEY" \ -md md5 + - name: Extract Trimble App ID + env: + TRIMBLE_SECRETS_DECRYPT_KEY: ${{ secrets.TRIMBLE_SECRETS_DECRYPT_KEY }} + run: | + cd mm/app/position/providers/ + $HOMEBREW_PREFIX/bin/openssl \ + aes-256-cbc -d \ + -in trimblesecrets.cpp.enc \ + -out trimblesecrets.cpp \ + -k "$TRIMBLE_SECRETS_DECRYPT_KEY" \ + -md md5 + - name: ccache uses: hendrikmuhs/ccache-action@v1.2 with: @@ -237,6 +249,7 @@ jobs: -DQT_ANDROID_SIGN_APK=Yes \ -DQT_ANDROID_SIGN_AAB=Yes \ -DUSE_MM_SERVER_API_KEY=Yes \ + -DWITH_TRIMBLE_PROVIDERS=TRUE \ -DUSE_KEYCHAIN=No \ -DCMAKE_TOOLCHAIN_FILE:PATH="${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" \ -GNinja \ diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 5eb881dd3..5e5359387 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -107,6 +107,18 @@ jobs: -k "$MERGINSECRETS_DECRYPT_KEY" \ -md md5 + - name: Extract Trimble App ID + env: + TRIMBLE_SECRETS_DECRYPT_KEY: ${{ secrets.TRIMBLE_SECRETS_DECRYPT_KEY }} + run: | + cd mm/app/position/providers/ + $HOMEBREW_PREFIX/bin/openssl \ + aes-256-cbc -d \ + -in trimblesecrets.cpp.enc \ + -out trimblesecrets.cpp \ + -k "$TRIMBLE_SECRETS_DECRYPT_KEY" \ + -md md5 + - name: Configure Keychain run: | security create-keychain -p "" "$KEYCHAIN" @@ -164,6 +176,7 @@ jobs: -D CMAKE_SYSTEM_PROCESSOR=aarch64 \ -DIOS=TRUE \ -DUSE_MM_SERVER_API_KEY=TRUE \ + -DWITH_TRIMBLE_PROVIDERS=TRUE \ -DUSE_KEYCHAIN=No \ -DCMAKE_INSTALL_PREFIX:PATH=../install-mm \ -G "Xcode" \ diff --git a/.gitignore b/.gitignore index 272a0a1b4..a9d29a033 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *build*/ app/config.pri core/merginsecrets.cpp +app/position/providers/trimblesecrets.cpp *.idea *.vscode *.cache @@ -22,4 +23,5 @@ CMakeLists.txt.user .github/secrets/ios/LutraConsulting*.mobileprovision google_play_key.json fastlane/report.xml -CMakeUserPresets.json \ No newline at end of file +CMakeUserPresets.json +CLAUDE.md diff --git a/CMakeLists.txt b/CMakeLists.txt index fd59bfd6e..d385c39c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,13 @@ set(HAVE_BLUETOOTH CACHE BOOL "Building with bluetooth position provider" ) +set(WITH_TRIMBLE_PROVIDERS + FALSE + CACHE + BOOL + "Building with Trimble GNSS position provider via Trimble Mobile Manager (mobile only)" +) + set(USE_KEYCHAIN FALSE CACHE @@ -265,6 +272,20 @@ if (HAVE_BLUETOOTH) ) endif () +if (WITH_TRIMBLE_PROVIDERS) + if (NOT ANDROID AND NOT IOS) + message( + FATAL_ERROR + "WITH_TRIMBLE_PROVIDERS is only supported on Android and iOS. Set WITH_TRIMBLE_PROVIDERS=FALSE for desktop builds." + ) + endif () + find_package( + Qt6 + COMPONENTS WebSockets + REQUIRED + ) +endif () + if (ENABLE_TESTS) find_package( Qt6 diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..714f5a94c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,100 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 0 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", + "USE_MM_SERVER_API_KEY": "FALSE", + "ENABLE_TESTS": "FALSE" + } + }, + { + "name": "macos-base", + "displayName": "macOS arm64 Debug", + "hidden": true, + "inherits": "base", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/macos", + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "arm64-osx", + "CMAKE_BUILD_TYPE": "Debug", + "ENABLE_TESTS": "TRUE" + } + }, + { + "name": "ios-base", + "displayName": "iOS arm64 RelWithDebInfo", + "hidden": true, + "inherits": "base", + "generator": "Xcode", + "binaryDir": "${sourceDir}/build/ios", + "cacheVariables": { + "VCPKG_HOST_TRIPLET": "arm64-osx", + "VCPKG_TARGET_TRIPLET": "arm64-ios", + "CMAKE_SYSTEM_NAME": "iOS", + "CMAKE_SYSTEM_PROCESSOR": "aarch64", + "IOS": "TRUE", + "ENABLE_BITCODE": "OFF", + "ENABLE_ARC": "ON", + "CMAKE_CXX_VISIBILITY_PRESET": "hidden", + "CMAKE_OSX_DEPLOYMENT_TARGET": "14.0", + "WITH_TRIMBLE_PROVIDERS": "TRUE" + } + }, + { + "name": "android-arm64-base", + "displayName": "Android arm64-v8a Release", + "hidden": true, + "inherits": "base", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/android-arm64", + "cacheVariables": { + "VCPKG_HOST_TRIPLET": "arm64-osx", + "VCPKG_TARGET_TRIPLET": "arm64-android", + "VCPKG_INSTALL_OPTIONS": "--allow-unsupported", + "CMAKE_BUILD_TYPE": "Release", + "ANDROID_ABI": "arm64-v8a", + "QT_ANDROID_ABIS": "arm64-v8a", + "USE_KEYCHAIN": "FALSE", + "ANDROID_SDK_ROOT": "$env{ANDROID_SDK_ROOT}", + "WITH_TRIMBLE_PROVIDERS": "TRUE" + } + }, + { + "name": "android-arm-base", + "displayName": "Android armeabi-v7a Release", + "hidden": true, + "inherits": "base", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/android-arm", + "cacheVariables": { + "VCPKG_HOST_TRIPLET": "arm64-osx", + "VCPKG_TARGET_TRIPLET": "arm-android", + "VCPKG_INSTALL_OPTIONS": "--allow-unsupported", + "CMAKE_BUILD_TYPE": "Release", + "ANDROID_ABI": "armeabi-v7a", + "QT_ANDROID_ABIS": "armeabi-v7a", + "USE_KEYCHAIN": "FALSE", + "ANDROID_SDK_ROOT": "$env{ANDROID_SDK_ROOT}" + } + }, + { + "name": "android-arm64-signed-base", + "displayName": "Android arm64-v8a Release (signed)", + "hidden": true, + "inherits": "android-arm64-base", + "cacheVariables": { + "QT_ANDROID_SIGN_APK": "YES", + "QT_ANDROID_SIGN_AAB": "YES" + } + } + ], + "buildPresets": [] +} diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index a4ec70413..e4691dba2 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -209,6 +209,25 @@ if (HAVE_BLUETOOTH) set(MM_HDRS ${MM_HDRS} position/providers/bluetoothpositionprovider.h) endif () +if (WITH_TRIMBLE_PROVIDERS) + if (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/position/providers/trimblesecrets.cpp) + message( + FATAL_ERROR + "app/position/providers/trimblesecrets.cpp doesn't exist. Decrypt trimblesecrets.cpp.enc or set WITH_TRIMBLE_PROVIDERS=FALSE" + ) + endif () + + set(MM_SRCS ${MM_SRCS} position/providers/trimblepositionprovider.cpp) + set(MM_HDRS ${MM_HDRS} position/providers/trimbleregistration.h + position/providers/trimblepositionprovider.h + ) + if (ANDROID) + set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistration_android.cpp) + elseif (IOS) + set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistration_ios.mm) + endif () +endif () + if (ENABLE_TESTS) set(MM_SRCS ${MM_SRCS} @@ -517,6 +536,11 @@ if (HAVE_BLUETOOTH) target_link_libraries(MerginMaps PUBLIC Qt6::Bluetooth) endif () +if (WITH_TRIMBLE_PROVIDERS) + target_compile_definitions(MerginMaps PRIVATE WITH_TRIMBLE_PROVIDERS) + target_link_libraries(MerginMaps PUBLIC Qt6::WebSockets) +endif () + if (NOT IOS) target_link_libraries(MerginMaps PUBLIC Qt6::PrintSupport) endif () diff --git a/app/main.cpp b/app/main.cpp index 3057bd5da..8d250b0e3 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -765,6 +765,12 @@ int main( int argc, char *argv[] ) engine.rootContext()->setContextProperty( "__haveBluetooth", false ); #endif +#ifdef WITH_TRIMBLE_PROVIDERS + engine.rootContext()->setContextProperty( "__withTrimbleProviders", true ); +#else + engine.rootContext()->setContextProperty( "__withTrimbleProviders", false ); +#endif + // Even though enabling QT's HighDPI scaling removes the need to multiply pixel values with dp, // there are screens that need a "little help", because system DPR has different value than the // one we calculated. In these scenarios we use a ratio between real (our) DPR and DPR reported by QT. diff --git a/app/maptools/recordingmaptool.cpp b/app/maptools/recordingmaptool.cpp index 8bd2b7071..44ff96a13 100644 --- a/app/maptools/recordingmaptool.cpp +++ b/app/maptools/recordingmaptool.cpp @@ -79,10 +79,10 @@ void RecordingMapTool::addPoint( const QgsPoint &point ) fixZM( pointToAdd ); - // apply gps antenna height - if ( QgsWkbTypes::hasZ( pointToAdd.wkbType() ) && mPositionKit && mPositionKit->antennaHeight() > 0 ) + // apply gps antenna height (zero when provider already reduced to ground point, e.g. Trimble via TMM) + if ( QgsWkbTypes::hasZ( pointToAdd.wkbType() ) && mPositionKit && mPositionKit->antennaHeightToApply() > 0 ) { - pointToAdd.setZ( pointToAdd.z() - mPositionKit->antennaHeight() ); + pointToAdd.setZ( pointToAdd.z() - mPositionKit->antennaHeightToApply() ); } QgsVertexId id( mActivePart, mActiveRing, 0 ); diff --git a/app/position/geoposition.h b/app/position/geoposition.h index 5dd413c8b..fa4e9e292 100644 --- a/app/position/geoposition.h +++ b/app/position/geoposition.h @@ -31,6 +31,13 @@ class GeoPosition : public QgsGpsInformation bool isMock = false; + // Set by providers that already account for antenna height (e.g. Trimble via TMM). + // antennaHeight >= 0 means the provider reported it; -1 = not provided. + // antennaHeightApplied = true means the provider already reduced to the ground point + // and the app must NOT subtract it again. + double antennaHeight = -1; + bool antennaHeightApplied = false; + // copies all data from QgsGpsInformation other and updates satellitesVisible static GeoPosition fromQgsGpsInformation( const QgsGpsInformation &other ); diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 04c086b14..7dc499c06 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -22,6 +22,9 @@ #include "providers/internalpositionprovider.h" #include "providers/simulatedpositionprovider.h" #include "providers/networkpositionprovider.h" +#ifdef WITH_TRIMBLE_PROVIDERS +#include "providers/trimblepositionprovider.h" +#endif #ifdef ANDROID #include "providers/androidpositionprovider.h" #include @@ -39,6 +42,10 @@ QgsCoordinateReferenceSystem PositionKit::positionCrs3D() { return mPositionCrs3D; } + if ( mProviderCrs.isValid() ) + { + return mProviderCrs; + } return QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); } @@ -96,9 +103,21 @@ void PositionKit::setPositionProvider( AbstractPositionProvider *provider ) mPositionProvider.reset( provider ); + mProviderCrs = QgsCoordinateReferenceSystem(); // reset to WGS84 default + if ( mPositionProvider ) { connect( mPositionProvider.get(), &AbstractPositionProvider::positionChanged, this, &PositionKit::parsePositionUpdate ); + connect( mPositionProvider.get(), &AbstractPositionProvider::sourceCrsChanged, this, [this]( const QgsCoordinateReferenceSystem & crs ) + { + mProviderCrs = crs.isValid() ? crs : QgsCoordinateReferenceSystem(); + refreshPositionTransformer( QgsCoordinateTransformContext() ); + } ); + + // Pick up provider CRS if it's already known (e.g. re-activation) + const QgsCoordinateReferenceSystem initial = mPositionProvider->sourceCrs(); + if ( initial.isValid() ) + mProviderCrs = initial; CoreUtils::log( QStringLiteral( "PositionKit" ), QStringLiteral( "Changed position provider to: %1" ).arg( provider->id() ) ); } @@ -135,6 +154,15 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c } #endif +#ifdef WITH_TRIMBLE_PROVIDERS + if ( type == QStringLiteral( "external_trimble" ) ) + { + AbstractPositionProvider *provider = new TrimblePositionProvider( id, name, *mPositionTransformer ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } +#endif + if ( type == QStringLiteral( "external_ip" ) ) { QString providerName( name ); @@ -263,9 +291,9 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) hasAnythingChanged = true; } - if ( !qgsDoubleNear( newPosition.elevation - antennaHeight(), mPosition.elevation ) ) + if ( !qgsDoubleNear( newPosition.elevation - ( newPosition.antennaHeightApplied ? 0 : antennaHeight() ), mPosition.elevation ) ) { - mPosition.elevation = newPosition.elevation - antennaHeight(); + mPosition.elevation = newPosition.elevation - ( newPosition.antennaHeightApplied ? 0 : antennaHeight() ); emit altitudeChanged( mPosition.elevation ); hasAnythingChanged = true; } @@ -390,6 +418,16 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) emit positionProviderNameChanged(); } + const bool antennaDataChanged = !qgsDoubleNear( newPosition.antennaHeight, mPosition.antennaHeight ) + || newPosition.antennaHeightApplied != mPosition.antennaHeightApplied; + if ( antennaDataChanged ) + { + mPosition.antennaHeight = newPosition.antennaHeight; + mPosition.antennaHeightApplied = newPosition.antennaHeightApplied; + emit antennaHeightChanged(); + hasAnythingChanged = true; + } + if ( hasAnythingChanged ) { emit positionChanged( mPosition ); @@ -434,7 +472,7 @@ void PositionKit::appStateChanged( const Qt::ApplicationState state ) void PositionKit::refreshPositionTransformer( const QgsCoordinateTransformContext &transformContext ) { - const QgsCoordinateReferenceSystem srcCrs = positionCrs3DEllipsoidHeight(); + const QgsCoordinateReferenceSystem srcCrs = mProviderCrs.isValid() ? mProviderCrs : positionCrs3DEllipsoidHeight(); const QgsCoordinateReferenceSystem destCrs = positionCrs3D(); QgsCoordinateTransformContext context = transformContext; @@ -604,10 +642,29 @@ void PositionKit::setAppSettings( AppSettings *appSettings ) double PositionKit::antennaHeight() const { + // Display value: provider-streamed height takes precedence; never overwrites AppSettings. + if ( mPosition.antennaHeight >= 0 ) + return mPosition.antennaHeight; + if ( mAppSettings ) - { return mAppSettings->gpsAntennaHeight(); - } return 0; } + +double PositionKit::antennaHeightToApply() const +{ + // If the provider already subtracted antenna height, the app must apply zero. + if ( mPosition.antennaHeightApplied ) + return 0; + + if ( mAppSettings ) + return mAppSettings->gpsAntennaHeight(); + + return 0; +} + +bool PositionKit::antennaHeightApplied() const +{ + return mPosition.antennaHeightApplied; +} diff --git a/app/position/positionkit.h b/app/position/positionkit.h index 4feee113c..dcd71db4c 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -82,6 +82,7 @@ class PositionKit : public QObject Q_PROPERTY( AppSettings *appSettings READ appSettings WRITE setAppSettings NOTIFY appSettingsChanged ) Q_PROPERTY( double antennaHeight READ antennaHeight NOTIFY antennaHeightChanged ) + Q_PROPERTY( bool antennaHeightApplied READ antennaHeightApplied NOTIFY antennaHeightChanged ) public: //! Creates new position kit @@ -143,6 +144,8 @@ class PositionKit : public QObject void setAppSettings( AppSettings *appSettings ); double antennaHeight() const; + double antennaHeightToApply() const; + bool antennaHeightApplied() const; void setVerticalCrs( const QgsCoordinateReferenceSystem &verticalCrs ); void setElevationTransformationEnabled( bool elevationTransformationEnabled ); @@ -201,6 +204,7 @@ class PositionKit : public QObject QgsCoordinateReferenceSystem mPositionCrs3D; QgsCoordinateReferenceSystem mVerticalCrs; + QgsCoordinateReferenceSystem mProviderCrs; // updated from provider's sourceCrsChanged; invalid = WGS84 bool mElevationTransformationEnabled = false; friend class TestPosition; diff --git a/app/position/providers/abstractpositionprovider.cpp b/app/position/providers/abstractpositionprovider.cpp index 30ae52983..8be3cf020 100644 --- a/app/position/providers/abstractpositionprovider.cpp +++ b/app/position/providers/abstractpositionprovider.cpp @@ -22,6 +22,11 @@ void AbstractPositionProvider::setPosition( QgsPoint ) { } +QgsCoordinateReferenceSystem AbstractPositionProvider::sourceCrs() const +{ + return QgsCoordinateReferenceSystem(); // invalid → caller treats as WGS84 +} + QString AbstractPositionProvider::name() const { return mProviderName; diff --git a/app/position/providers/abstractpositionprovider.h b/app/position/providers/abstractpositionprovider.h index 03424eae6..db32ce33b 100644 --- a/app/position/providers/abstractpositionprovider.h +++ b/app/position/providers/abstractpositionprovider.h @@ -10,6 +10,7 @@ #ifndef ABSTRACTPOSITIONPROVIDER_H #define ABSTRACTPOSITIONPROVIDER_H +#include #include #include @@ -53,12 +54,19 @@ class AbstractPositionProvider : public QObject Q_INVOKABLE QString name() const; Q_INVOKABLE QString type() const; + // The geodetic CRS the provider's coordinates are in. + // Default: invalid (means WGS84). Override in providers that report their datum. + virtual QgsCoordinateReferenceSystem sourceCrs() const; + signals: void positionChanged( const GeoPosition &position ); void stateMessageChanged( const QString &message ); void stateChanged( AbstractPositionProvider::State state ); + // Emitted when the provider's reported source CRS changes. + void sourceCrsChanged( const QgsCoordinateReferenceSystem &crs ); + protected: void setState( const QString &message ); // keeps state enum the same and only changes the message void setState( const QString &message, State state ); diff --git a/app/position/providers/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp new file mode 100644 index 000000000..d3f38aae7 --- /dev/null +++ b/app/position/providers/trimblepositionprovider.cpp @@ -0,0 +1,363 @@ +/*************************************************************************** + * * + * 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 "trimblepositionprovider.h" + +#include +#include +#include +#include + +#include +#include +#include + +#ifdef ANDROID +#include +#include +#endif + +// trimblesecrets.cpp is generated by decrypting trimblesecrets.cpp.enc. +// It defines static QString __getTrimbleAppId(). +#include "trimblesecrets.cpp" + +constexpr int ONE_SECOND_MS = 1000; +constexpr int TMM_WS_V2_DEFAULT_PORT = 9639; + +// --------------------------------------------------------------------------- +// Frame resolver +// --------------------------------------------------------------------------- + +struct FrameEntry +{ + const char *name; // prefix match against targetReferenceFrameName + const char *authId; // EPSG or user authority identifier +}; + +static const FrameEntry KNOWN_FRAMES[] = +{ + { "WGS 84", "EPSG:4979" }, + { "WGS84", "EPSG:4979" }, + { "ITRF2020", "EPSG:9990" }, + { "ITRF2014", "EPSG:7912" }, + { "ITRF2008", "EPSG:8999" }, + { "ITRF2005", "EPSG:8998" }, + { "ITRF2000", "EPSG:8997" }, + { "ITRF97", "EPSG:8996" }, + { "ITRF96", "EPSG:8995" }, + { "ITRF94", "EPSG:8994" }, + { "ITRF93", "EPSG:8993" }, + { "ITRF92", "EPSG:8992" }, + { "ITRF91", "EPSG:8991" }, + { "ITRF90", "EPSG:8990" }, + { "ITRF89", "EPSG:8989" }, + { "ITRF88", "EPSG:8988" }, + { "NAD83(2011)", "EPSG:6319" }, + { "NAD83", "EPSG:4269" }, + { "ETRS89", "EPSG:4936" }, + { "GDA2020", "EPSG:7843" }, + { "GDA94", "EPSG:4939" }, + { nullptr, nullptr } +}; + +QgsCoordinateReferenceSystem TrimblePositionProvider::resolveFrame( const QString &frameName, double epoch ) +{ + if ( frameName.isEmpty() ) + return QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ); // WGS84 default + + for ( const FrameEntry *e = KNOWN_FRAMES; e->name; ++e ) + { + if ( frameName.startsWith( QLatin1String( e->name ), Qt::CaseInsensitive ) ) + { + QgsCoordinateReferenceSystem crs( QString::fromLatin1( e->authId ) ); + if ( epoch > 0 ) + crs.setCoordinateEpoch( epoch ); + return crs; + } + } + + QgsDebugMsgLevel( QStringLiteral( "TrimblePositionProvider: unknown frame '%1', falling back to WGS84" ).arg( frameName ), 2 ); + return QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ); +} + +// --------------------------------------------------------------------------- +// Message parser +// --------------------------------------------------------------------------- + +GeoPosition TrimblePositionProvider::parseLocationMessage( const QString &json ) +{ + GeoPosition pos; + + const QJsonDocument doc = QJsonDocument::fromJson( json.toUtf8() ); + if ( doc.isNull() || !doc.isObject() ) + return pos; + + const QJsonObject obj = doc.object(); + + if ( obj.contains( QStringLiteral( "latitude" ) ) && !obj.value( QStringLiteral( "latitude" ) ).isNull() ) + pos.latitude = obj.value( QStringLiteral( "latitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + + if ( obj.contains( QStringLiteral( "longitude" ) ) && !obj.value( QStringLiteral( "longitude" ) ).isNull() ) + pos.longitude = obj.value( QStringLiteral( "longitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + + if ( obj.contains( QStringLiteral( "altitude" ) ) && !obj.value( QStringLiteral( "altitude" ) ).isNull() ) + pos.elevation = obj.value( QStringLiteral( "altitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + + if ( obj.contains( QStringLiteral( "hdop" ) ) && !obj.value( QStringLiteral( "hdop" ) ).isNull() ) + pos.hdop = obj.value( QStringLiteral( "hdop" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "totalSatInUse" ) ) && !obj.value( QStringLiteral( "totalSatInUse" ) ).isNull() ) + pos.satellitesUsed = obj.value( QStringLiteral( "totalSatInUse" ) ).toInt( -1 ); + + if ( obj.contains( QStringLiteral( "satellites" ) ) && !obj.value( QStringLiteral( "satellites" ) ).isNull() ) + pos.satellitesVisible = obj.value( QStringLiteral( "satellites" ) ).toInt( -1 ); + + // diffStatus: 1=Autonomous, 2=DGPS, 4=Fixed, 5=Float + const int diffStatus = obj.value( QStringLiteral( "diffStatus" ) ).toInt( 1 ); + switch ( diffStatus ) + { + case 4: + pos.qualityIndicator = Qgis::GpsQualityIndicator::RTK; + break; + case 5: + pos.qualityIndicator = Qgis::GpsQualityIndicator::FloatRTK; + break; + case 2: + pos.qualityIndicator = Qgis::GpsQualityIndicator::DGPS; + break; + default: + pos.qualityIndicator = Qgis::GpsQualityIndicator::GPS; + break; + } + + // TMM has already reduced its output to the ground point. + // antennaHeight here is the value TMM applied (metadata only); the app must NOT subtract it again. + if ( obj.contains( QStringLiteral( "antennaHeight" ) ) && !obj.value( QStringLiteral( "antennaHeight" ) ).isNull() ) + { + pos.antennaHeight = obj.value( QStringLiteral( "antennaHeight" ) ).toDouble( -1 ); + pos.antennaHeightApplied = true; + } + + return pos; +} + +// --------------------------------------------------------------------------- +// Constructor / destructor +// --------------------------------------------------------------------------- + +TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( id, QStringLiteral( "external_trimble" ), name, positionTransformer, parent ) + , mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) +{ + mRegistration = new TrimbleRegistration( this ); + connect( mRegistration, &TrimbleRegistration::registered, this, &TrimblePositionProvider::onRegistered ); + connect( mRegistration, &TrimbleRegistration::failed, this, &TrimblePositionProvider::onRegistrationFailed ); + + mReconnectTimer.setSingleShot( false ); + mReconnectTimer.setInterval( ONE_SECOND_MS ); + connect( &mReconnectTimer, &QTimer::timeout, this, &TrimblePositionProvider::onReconnectTimeout ); + + mHeartBeatTimer.setSingleShot( true ); + connect( &mHeartBeatTimer, &QTimer::timeout, this, [this] + { + setState( tr( "No data from Trimble Mobile Manager" ), State::NoConnection ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); + } ); + + TrimblePositionProvider::startUpdates(); +} + +TrimblePositionProvider::~TrimblePositionProvider() +{ + TrimblePositionProvider::closeProvider(); +} + +void TrimblePositionProvider::startUpdates() +{ + if ( mRegistrationInProgress ) + { + qDebug() << "TrimblePositionProvider: startUpdates called while registration already in progress, ignoring"; + return; + } + if ( mCachedPort > 0 ) + { + qDebug() << "TrimblePositionProvider: startUpdates - reconnecting with cached port" << mCachedPort; + connectWebSocket( mCachedPort ); + return; + } + mRegistrationInProgress = true; + qDebug() << "TrimblePositionProvider: startUpdates - requesting registration"; + setState( tr( "Connecting to Trimble Mobile Manager" ), State::Connecting ); + mRegistration->requestRegistration( __getTrimbleAppId() ); +} + +void TrimblePositionProvider::stopUpdates() +{ + mHeartBeatTimer.stop(); + mReconnectTimer.stop(); + if ( mSocket && mSocket->state() == QAbstractSocket::ConnectedState ) + mSocket->close(); +} + +void TrimblePositionProvider::closeProvider() +{ + mHeartBeatTimer.stop(); + mReconnectTimer.stop(); + if ( mSocket ) + { + mSocket->disconnect(); + mSocket->abort(); + } +} + +QgsCoordinateReferenceSystem TrimblePositionProvider::sourceCrs() const +{ + return mSourceCrs; +} + +// --------------------------------------------------------------------------- +// Private slots +// --------------------------------------------------------------------------- + +void TrimblePositionProvider::onRegistered( int port ) +{ + qDebug() << "TrimblePositionProvider: registration succeeded, port:" << port; + mRegistrationInProgress = false; + mCachedPort = port; + connectWebSocket( port ); +} + +void TrimblePositionProvider::onRegistrationFailed( const QString &reason ) +{ + qDebug() << "TrimblePositionProvider: registration failed:" << reason; + mRegistrationInProgress = false; + setState( reason, State::NoConnection ); + emit positionChanged( GeoPosition() ); +} + +void TrimblePositionProvider::connectWebSocket( int port ) +{ + mSocket = std::make_unique(); + + connect( mSocket.get(), &QWebSocket::textMessageReceived, this, &TrimblePositionProvider::onTextMessageReceived ); + connect( mSocket.get(), &QWebSocket::disconnected, this, &TrimblePositionProvider::onSocketDisconnected ); + connect( mSocket.get(), QOverload::of( &QWebSocket::error ), + this, &TrimblePositionProvider::onSocketError ); + connect( mSocket.get(), &QWebSocket::connected, this, [this] + { + setState( tr( "Connected to Trimble Mobile Manager" ), State::Connected ); + mReconnectDelay = ReconnectDelay::ShortDelay; + mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); + } ); + + const QUrl url( QStringLiteral( "ws://127.0.0.1:%1/" ).arg( port ) ); + setState( tr( "Connecting to Trimble Mobile Manager" ), State::Connecting ); + mSocket->open( url ); +} + +void TrimblePositionProvider::onTextMessageReceived( const QString &message ) +{ + mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); + + // Parse once; keep antenna-height fields through the position transformer + GeoPosition raw = parseLocationMessage( message ); + const double antennaHeight = raw.antennaHeight; + const bool antennaHeightApplied = raw.antennaHeightApplied; + + GeoPosition pos = mPositionTransformer->processNetworkPosition( raw ); + pos.antennaHeight = antennaHeight; + pos.antennaHeightApplied = antennaHeightApplied; + + setState( tr( "Connected to Trimble Mobile Manager" ), State::Connected ); + emit positionChanged( pos ); + + // Update source CRS from reported frame (re-resolve only on change) + const QJsonDocument doc = QJsonDocument::fromJson( message.toUtf8() ); + if ( !doc.isNull() && doc.isObject() ) + { + const QJsonObject obj = doc.object(); + const QString frameName = obj.value( QStringLiteral( "targetReferenceFrameName" ) ).toString(); + const double epoch = obj.value( QStringLiteral( "targetReferenceFrameEpoch" ) ).toDouble( 0 ); + const QgsCoordinateReferenceSystem newCrs = resolveFrame( frameName, epoch ); + + if ( newCrs != mSourceCrs ) + { + mSourceCrs = newCrs; + emit sourceCrsChanged( mSourceCrs ); + } + } +} + +void TrimblePositionProvider::onSocketError( QAbstractSocket::SocketError error ) +{ + Q_UNUSED( error ) + setState( tr( "Connection error: %1" ).arg( mSocket ? mSocket->errorString() : QString() ), State::NoConnection ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); +} + +void TrimblePositionProvider::onSocketDisconnected() +{ + mHeartBeatTimer.stop(); + setState( tr( "Disconnected from Trimble Mobile Manager" ), State::NoConnection ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); +} + +void TrimblePositionProvider::startReconnectTimer() +{ + mSecondsLeftToReconnect = mReconnectDelay / ONE_SECOND_MS; + setState( tr( "Reconnecting in %1 s" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + mReconnectTimer.start(); + + if ( mReconnectDelay == ReconnectDelay::ShortDelay ) + mReconnectDelay = ReconnectDelay::LongDelay; +} + +void TrimblePositionProvider::onReconnectTimeout() +{ + if ( mSecondsLeftToReconnect <= 1 ) + { + reconnect(); + } + else + { + mSecondsLeftToReconnect--; + setState( tr( "Reconnecting in %1 s" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + } +} + +void TrimblePositionProvider::reconnect() +{ + mReconnectTimer.stop(); + if ( mCachedPort > 0 ) + { + connectWebSocket( mCachedPort ); + } + else + { + startUpdates(); // re-register + } +} + +void TrimblePositionProvider::openAntennaHeightPage() +{ +#ifdef ANDROID + QJniObject intentAction = QJniObject::fromString( QStringLiteral( "com.trimble.tmm.OPENANTENNAHEIGHT" ) ); + QJniObject intent( "android/content/Intent", "(Ljava/lang/String;)V", intentAction.object() ); + QJniObject activity = QJniObject::callStaticObjectMethod( "org/qtproject/qt/android/QtNative", + "activity", + "()Landroid/app/Activity;" ); + if ( activity.isValid() ) + activity.callMethod( "startActivity", "(Landroid/content/Intent;)V", intent.object() ); +#elif defined(Q_OS_IOS) + QDesktopServices::openUrl( QUrl( QStringLiteral( "TmmOpenToAntennaHeight://trimble.tmm.iOS" ) ) ); +#endif +} diff --git a/app/position/providers/trimblepositionprovider.h b/app/position/providers/trimblepositionprovider.h new file mode 100644 index 000000000..33c365a64 --- /dev/null +++ b/app/position/providers/trimblepositionprovider.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * * + * 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 TRIMBLEPOSITIONPROVIDER_H +#define TRIMBLEPOSITIONPROVIDER_H + +#include +#include + +#include "abstractpositionprovider.h" +#include "trimbleregistration.h" + +class TrimblePositionProvider : public AbstractPositionProvider +{ + Q_OBJECT + + enum ReconnectDelay + { + ShortDelay = 3000, + LongDelay = 5000, + ExtraLongDelay = 10000 + }; + + public: + TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); + ~TrimblePositionProvider() override; + + void startUpdates() override; + void stopUpdates() override; + void closeProvider() override; + + QgsCoordinateReferenceSystem sourceCrs() const override; + + Q_INVOKABLE void openAntennaHeightPage(); + + // Visible for unit testing + static GeoPosition parseLocationMessage( const QString &json ); + static QgsCoordinateReferenceSystem resolveFrame( const QString &frameName, double epoch ); + + private slots: + void onRegistered( int port ); + void onRegistrationFailed( const QString &reason ); + void onTextMessageReceived( const QString &message ); + void onSocketError( QAbstractSocket::SocketError error ); + void onSocketDisconnected(); + void onReconnectTimeout(); + + private: + void connectWebSocket( int port ); + void startReconnectTimer(); + void reconnect(); + + TrimbleRegistration *mRegistration = nullptr; + std::unique_ptr mSocket; + QTimer mReconnectTimer; + QTimer mHeartBeatTimer; + + int mCachedPort = 0; + int mReconnectDelay = ReconnectDelay::ShortDelay; + int mSecondsLeftToReconnect = 0; + bool mRegistrationInProgress = false; + + QgsCoordinateReferenceSystem mSourceCrs; +}; + +#endif // TRIMBLEPOSITIONPROVIDER_H diff --git a/app/position/providers/trimbleregistration.h b/app/position/providers/trimbleregistration.h new file mode 100644 index 000000000..e4070aa46 --- /dev/null +++ b/app/position/providers/trimbleregistration.h @@ -0,0 +1,52 @@ +/*************************************************************************** + * * + * 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 TRIMBLEREGISTRATION_H +#define TRIMBLEREGISTRATION_H + +#include +#include +#include +#include + +#ifdef ANDROID +class TmmResultReceiver; // defined in trimbleregistration_android.cpp +#endif + +/** + * Platform-agnostic async contract for registering with Trimble Mobile Manager. + * + * Call requestRegistration() once; listen for registered() or failed(). + * Concrete implementations are in trimbleregistration_android.cpp (Android) + * and trimbleregistration_ios.mm (iOS). + */ +class TrimbleRegistration : public QObject +{ + Q_OBJECT + + public: + explicit TrimbleRegistration( QObject *parent = nullptr ); + ~TrimbleRegistration() override; + + void requestRegistration( const QString &appId ); + + // iOS only: called by QDesktopServices URL handler when TMM calls back + Q_INVOKABLE void handleCallback( const QUrl &url ); + + signals: + void registered( int locationV2Port ); + void failed( const QString &reason ); + + private: +#ifdef ANDROID + std::unique_ptr mResultReceiver; +#endif +}; + +#endif // TRIMBLEREGISTRATION_H diff --git a/app/position/providers/trimbleregistration_android.cpp b/app/position/providers/trimbleregistration_android.cpp new file mode 100644 index 000000000..45ad63694 --- /dev/null +++ b/app/position/providers/trimbleregistration_android.cpp @@ -0,0 +1,105 @@ +/*************************************************************************** + * * + * 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 "trimbleregistration.h" + +#ifdef ANDROID + +#include +#include +#include + +static constexpr int TMM_REGISTER_REQUEST_CODE = 0x544D4D52; // "TMMR" + +static constexpr char TMM_ACTION_REGISTER[] = "com.trimble.tmm.REGISTER"; + +static constexpr char TMM_EXTRA_APP_ID[] = "applicationID"; +static constexpr char TMM_EXTRA_RESULT[] = "registrationResult"; +static constexpr char TMM_EXTRA_LOCATION_V2_PORT[] = "locationV2Port"; + +static constexpr char TMM_RESULT_OK[] = "OK"; + +// Concrete subclass of the abstract QAndroidActivityResultReceiver. +// Forwards the result to TrimbleRegistration via a stored pointer. +class TmmResultReceiver : public QAndroidActivityResultReceiver +{ + public: + explicit TmmResultReceiver( TrimbleRegistration *reg ) : mReg( reg ) {} + + void handleActivityResult( int receiverRequestCode, int resultCode, const QJniObject &data ) override + { + Q_UNUSED( resultCode ) + if ( receiverRequestCode != TMM_REGISTER_REQUEST_CODE || !mReg ) + return; + + if ( !data.isValid() ) + { + emit mReg->failed( TrimbleRegistration::tr( "No response from Trimble Mobile Manager" ) ); + return; + } + + QJniObject resultKey = QJniObject::fromString( QString::fromLatin1( TMM_EXTRA_RESULT ) ); + QJniObject resultObj = data.callObjectMethod( "getStringExtra", + "(Ljava/lang/String;)Ljava/lang/String;", + resultKey.object() ); + const QString result = resultObj.isValid() ? resultObj.toString() : QString(); + + if ( result != QLatin1String( TMM_RESULT_OK ) ) + { + emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager registration failed: %1" ).arg( result ) ); + return; + } + + QJniObject portKey = QJniObject::fromString( QString::fromLatin1( TMM_EXTRA_LOCATION_V2_PORT ) ); + const int port = data.callMethod( "getIntExtra", + "(Ljava/lang/String;I)I", + portKey.object(), + static_cast( 0 ) ); + if ( port <= 0 ) + { + emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager returned invalid port" ) ); + return; + } + + emit mReg->registered( port ); + } + + private: + TrimbleRegistration *mReg = nullptr; +}; + +TrimbleRegistration::TrimbleRegistration( QObject *parent ) + : QObject( parent ) + , mResultReceiver( std::make_unique( this ) ) +{ +} + +TrimbleRegistration::~TrimbleRegistration() = default; + +void TrimbleRegistration::requestRegistration( const QString &appId ) +{ + QJniObject intentAction = QJniObject::fromString( QString::fromLatin1( TMM_ACTION_REGISTER ) ); + QJniObject intent( "android/content/Intent", "(Ljava/lang/String;)V", intentAction.object() ); + QJniObject appIdStr = QJniObject::fromString( appId ); + QJniObject extraKey = QJniObject::fromString( QString::fromLatin1( TMM_EXTRA_APP_ID ) ); + intent.callObjectMethod( "putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + extraKey.object(), + appIdStr.object() ); + + QtAndroidPrivate::startActivity( intent, TMM_REGISTER_REQUEST_CODE, mResultReceiver.get() ); +} + +void TrimbleRegistration::handleCallback( const QUrl &url ) +{ + Q_UNUSED( url ) + // Android registration is intent-based; this callback is iOS-only +} + +#endif // ANDROID diff --git a/app/position/providers/trimbleregistration_ios.mm b/app/position/providers/trimbleregistration_ios.mm new file mode 100644 index 000000000..382f1016e --- /dev/null +++ b/app/position/providers/trimbleregistration_ios.mm @@ -0,0 +1,122 @@ +/*************************************************************************** + * * + * 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 "trimbleregistration.h" + +#ifdef Q_OS_IOS + +#include +#include +#include +#include +#include +#include +#include + +#import + +static constexpr char TMM_CALLBACK_SCHEME[] = "merginmaps-tmm"; +static constexpr char TMM_REGISTER_SCHEME[] = "tmmregister"; + +TrimbleRegistration::TrimbleRegistration( QObject *parent ) + : QObject( parent ) +{ +} + +TrimbleRegistration::~TrimbleRegistration() = default; + +void TrimbleRegistration::requestRegistration( const QString &appId ) +{ + QDesktopServices::setUrlHandler( QString::fromLatin1( TMM_CALLBACK_SCHEME ), this, "handleCallback" ); + + QJsonObject payload; + payload[QStringLiteral( "application_id" )] = appId; + payload[QStringLiteral( "returl" )] = QStringLiteral( "%1://registration" ).arg( QLatin1String( TMM_CALLBACK_SCHEME ) ); + + const QByteArray jsonBytes = QJsonDocument( payload ).toJson( QJsonDocument::Compact ); + const QString base64 = QString::fromLatin1( jsonBytes.toBase64() ); + + // Build the raw URL string and open via UIKit directly. + // QDesktopServices::openUrl() routes through QUrl which percent-encodes or rejects + // the base64 payload ('+', '/', '=' are special in URLs), breaking the scheme. + const QString rawUrlString = QStringLiteral( "%1://?%2" ) + .arg( QLatin1String( TMM_REGISTER_SCHEME ), base64 ); + NSString *nsUrlString = rawUrlString.toNSString(); + NSURL *nsUrl = [NSURL URLWithString:nsUrlString]; + + qDebug() << "TrimbleRegistration: opening URL:" << rawUrlString; + qDebug() << "TrimbleRegistration: NSURL valid:" << ( nsUrl != nil ); + + if ( !nsUrl ) + { + emit failed( tr( "Could not open Trimble Mobile Manager. Is it installed?" ) ); + return; + } + + QPointer self( this ); + [[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:^( BOOL success ) + { + qDebug() << "TrimbleRegistration: openURL completionHandler success:" << success; + if ( !success && self ) + { + emit self->failed( tr( "Could not open Trimble Mobile Manager. Is it installed?" ) ); + } + }]; +} + +Q_INVOKABLE void TrimbleRegistration::handleCallback( const QUrl &url ) +{ + qDebug() << "TrimbleRegistration: handleCallback called with URL:" << url.toString(); + + if ( url.scheme() != QLatin1String( TMM_CALLBACK_SCHEME ) ) + { + qDebug() << "TrimbleRegistration: unexpected scheme, ignoring:" << url.scheme(); + return; + } + + const QString fragment = url.fragment(); + const QString query = url.query(); + const QString base64 = fragment.isEmpty() ? query : fragment; + qDebug() << "TrimbleRegistration: raw callback payload:" << base64; + + const QByteArray jsonBytes = QByteArray::fromBase64( base64.toLatin1() ); + const QJsonDocument doc = QJsonDocument::fromJson( jsonBytes ); + if ( doc.isNull() || !doc.isObject() ) + { + qDebug() << "TrimbleRegistration: failed to parse callback JSON:" << jsonBytes; + emit failed( tr( "Invalid response from Trimble Mobile Manager" ) ); + return; + } + + const QJsonObject obj = doc.object(); + const QString result = obj.value( QStringLiteral( "registrationResult" ) ).toString(); + const QJsonValue dbgPort = obj.value( QStringLiteral( "locationV2Port" ) ); + qDebug() << "TrimbleRegistration: registrationResult:" << result + << "locationV2Port:" << ( dbgPort.isString() ? dbgPort.toString() : QString::number( dbgPort.toInt( 0 ) ) ); + + if ( result != QLatin1String( "OK" ) ) + { + emit failed( tr( "Trimble Mobile Manager registration failed: %1" ).arg( result ) ); + return; + } + + // TMM returns port values as JSON strings, not numbers — use toString().toInt() + const QJsonValue portVal = obj.value( QStringLiteral( "locationV2Port" ) ); + const int port = portVal.isString() ? portVal.toString().toInt() : portVal.toInt( 0 ); + if ( port <= 0 ) + { + emit failed( tr( "Trimble Mobile Manager returned invalid port" ) ); + return; + } + + QDesktopServices::unsetUrlHandler( QString::fromLatin1( TMM_CALLBACK_SCHEME ) ); + emit registered( port ); +} + +#endif // Q_OS_IOS diff --git a/app/position/providers/trimblesecrets.cpp.enc b/app/position/providers/trimblesecrets.cpp.enc new file mode 100644 index 0000000000000000000000000000000000000000..868dae3a7e10ac5f4bb613e61b15bc1a0c6c6b4d GIT binary patch literal 512 zcmV+b0{{I}VQh3|WM5x0&8W#CbiOI$02h{8jxl7@ejV=hmC+bL*eq7E+k4lzbQ*36 zd#pDeE~)XHSIVK)_9KBH^@7DwCtQ%5-b0`UJ>3-GCl-cqW;rS%Z@7>6HU5h0Q@S(w z@x|R@?0Cw*{TJgW>JSLh#H7DPU%0BXNF-M*Y_x0iZCXEt14Ce(?#kkU3YW1r+J+-# z77A3bD<&!eCy9r1kX!YKLr)?2bJwI^TOd{bAHfpmAjatlLh^;7aB&SLRY%x<8Ar66 z_LvKFK7SUBM>PnU8BxPJW!h(!(q{>H>9~KHeI+H{!Lftt`fPo7#@8A4@k?MJ`o5wEC<7m!pKpt;Atc`;chov}(r>gr*-=12}Bbt^x-m^}9}QFfnO( zN|7Rx%BeREeC%|jXWFa+ch{k(o?#^@3Lh8&GqvLc>?8{TOb?+O>s{cdJb{7{2#Sk> z91zKaW+2WdS_opRj%g5Q)3Cu@tYF#%x$oqZoKo-|!XQGU-|P{Ej$B50B03JA_ 0 ? __inputUtils.formatNumber(AppSettings.gpsAntennaHeight, 3) + " m" : qsTr( "Not set" ) + value: PositionKit.antennaHeight > 0 ? __inputUtils.formatNumber(PositionKit.antennaHeight, 3) + " m" : qsTr( "Not set" ) alignmentRight: Positioner.index % 2 === 1 } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 34ceb63ff..a5c9b65fb 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -133,7 +133,7 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) onClicked: { - if ( __haveBluetooth ) { + if ( __haveBluetooth || __withTrimbleProviders ) { providerTypeDrawer.open() } else { @@ -148,6 +148,11 @@ MMComponents.MMPage { onProviderSelected: function( providerType ) { if ( providerType === "bluetooth" ) bluetoothDiscoveryLoader.active = true else if ( providerType === "network" ) networkProviderDrawer.open() + else if ( providerType === "trimble" ) { + root.activateProvider( "external_trimble", "trimble_tmm", qsTr( "Trimble Mobile Manager" ) ) + connectingDialogLoader.providerType = "trimble" + connectingDialogLoader.active = true + } } } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index 745b0db56..a5898abed 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -16,6 +16,8 @@ import "../components" as MMComponents MMComponents.MMListDrawer { id: root + property bool withTrimbleProviders: __withTrimbleProviders + signal providerSelected( string providerType ) drawerHeader.title: qsTr( "Connect new receiver" ) @@ -31,6 +33,10 @@ MMComponents.MMListDrawer { { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon }, { name: qsTr( "Network (TCP, UDP)" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } ] ) + + if ( root.withTrimbleProviders ) { + providerTypeModel.append( { name: qsTr( "Trimble" ), description: qsTr( "Trimble receivers via Trimble Mobile Manager" ), type: "trimble", icon: __style.gpsIcon } ) + } } } @@ -104,6 +110,9 @@ MMComponents.MMListDrawer { else if ( providerType === "network" ) { root.providerSelected("network") } + else if ( providerType === "trimble" ) { + root.providerSelected("trimble") + } } } } diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index 26435fddc..2594c4696 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -85,16 +85,56 @@ MMPage { MMLine {} - MMSettingsComponents.MMSettingsInput { + Loader { width: parent.width - title: qsTr("GPS antenna height") - description: qsTr("Includes pole height and GPS receiver’s antenna height") - valueDescription: qsTr("GPS antenna height, in meters") - value: AppSettings.gpsAntennaHeight - suffix: " m" + sourceComponent: PositionKit.antennaHeightApplied ? trimbleAntennaHeightComponent : editableAntennaHeightComponent + } - onValueWasChanged: function( newValue ) { - AppSettings.gpsAntennaHeight = newValue + Component { + id: editableAntennaHeightComponent + + MMSettingsComponents.MMSettingsInput { + width: parent ? parent.width : 0 + title: qsTr("GPS antenna height") + description: qsTr("Includes pole height and GPS receiver’s antenna height") + valueDescription: qsTr("GPS antenna height, in meters") + value: AppSettings.gpsAntennaHeight + suffix: " m" + + onValueWasChanged: function( newValue ) { + AppSettings.gpsAntennaHeight = newValue + } + } + } + + Component { + id: trimbleAntennaHeightComponent + + Column { + width: parent ? parent.width : 0 + spacing: __style.spacing4 + + MMSettingsComponents.MMSettingsItem { + width: parent.width + title: qsTr("GPS antenna height") + value: PositionKit.antennaHeight.toFixed( 3 ) + " m" + } + + MMText { + width: parent.width + text: qsTr("Antenna height is managed by Trimble Mobile Manager") + color: __style.greyColor + font: __style.t4 + } + + MMButton { + text: qsTr("Open in Trimble Mobile Manager") + size: MMButton.Sizes.Small + onClicked: { + if ( PositionKit.positionProvider ) + PositionKit.positionProvider.openAntennaHeightPage() + } + } } } diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 8bbaa05c8..3713efdf1 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -874,3 +874,99 @@ void TestPosition::testPositionTransformerSimulatedPosition() QVERIFY( qgsDoubleNear( newPosition.elevation, 127.53574931171875 ) ); QVERIFY( qgsDoubleNear( newPosition.elevation_diff, 43.764250688281265 ) ); } + +#ifdef WITH_TRIMBLE_PROVIDERS +#include "position/providers/trimblepositionprovider.h" +#include "appsettings.h" + +void TestPosition::testTrimbleMessageParser() +{ + // Full LocationV2DataMessage sample + const QString json = QStringLiteral( + R"({ + "latitude": 48.123456, + "longitude": 17.654321, + "altitude": 150.5, + "hdop": 0.8, + "satellites": 12, + "totalSatInUse": 10, + "diffStatus": 4, + "antennaHeight": 1.234, + "targetReferenceFrameName": "ITRF2014", + "targetReferenceFrameEpoch": 2010.0 + })" ); + + GeoPosition pos = TrimblePositionProvider::parseLocationMessage( json ); + + QVERIFY( qgsDoubleNear( pos.latitude, 48.123456 ) ); + QVERIFY( qgsDoubleNear( pos.longitude, 17.654321 ) ); + QVERIFY( qgsDoubleNear( pos.elevation, 150.5 ) ); + QVERIFY( qgsDoubleNear( pos.hdop, 0.8 ) ); + QCOMPARE( pos.satellitesVisible, 12 ); + QCOMPARE( pos.satellitesUsed, 10 ); + QCOMPARE( pos.qualityIndicator, QgsGpsInformation::QualityIndicator::RTK ); + QVERIFY( qgsDoubleNear( pos.antennaHeight, 1.234 ) ); + QVERIFY( pos.antennaHeightApplied ); + + // Null / missing fields should not crash + const QString emptyJson = QStringLiteral( "{}" ); + GeoPosition emptyPos = TrimblePositionProvider::parseLocationMessage( emptyJson ); + QVERIFY( !emptyPos.hasValidPosition() ); + + // Malformed JSON + const QString badJson = QStringLiteral( "not json at all" ); + GeoPosition badPos = TrimblePositionProvider::parseLocationMessage( badJson ); + QVERIFY( !badPos.hasValidPosition() ); +} + +void TestPosition::testTrimbleFrameResolver() +{ + // Known frames should resolve to valid CRS + QgsCoordinateReferenceSystem wgs84 = TrimblePositionProvider::resolveFrame( QStringLiteral( "WGS84" ), 0 ); + QVERIFY( wgs84.isValid() ); + QCOMPARE( wgs84.authid(), QStringLiteral( "EPSG:4979" ) ); + + QgsCoordinateReferenceSystem itrf2014 = TrimblePositionProvider::resolveFrame( QStringLiteral( "ITRF2014" ), 2010.0 ); + QVERIFY( itrf2014.isValid() ); + QCOMPARE( itrf2014.authid(), QStringLiteral( "EPSG:7912" ) ); + QVERIFY( qgsDoubleNear( itrf2014.coordinateEpoch(), 2010.0 ) ); + + QgsCoordinateReferenceSystem etrs89 = TrimblePositionProvider::resolveFrame( QStringLiteral( "ETRS89" ), 0 ); + QVERIFY( etrs89.isValid() ); + QCOMPARE( etrs89.authid(), QStringLiteral( "EPSG:4936" ) ); + + // Unknown frame → WGS84 fallback + QgsCoordinateReferenceSystem unknown = TrimblePositionProvider::resolveFrame( QStringLiteral( "SomeUnknownDatum2099" ), 0 ); + QVERIFY( unknown.isValid() ); + QCOMPARE( unknown.authid(), QStringLiteral( "EPSG:4979" ) ); + + // Empty frame → WGS84 default + QgsCoordinateReferenceSystem empty = TrimblePositionProvider::resolveFrame( QString(), 0 ); + QVERIFY( empty.isValid() ); + QCOMPARE( empty.authid(), QStringLiteral( "EPSG:4979" ) ); +} + +void TestPosition::testTrimbleAntennaHeight() +{ + // Provider-supplied antenna height: display from GeoPosition, apply 0 + GeoPosition trimblePos; + trimblePos.antennaHeight = 1.5; + trimblePos.antennaHeightApplied = true; + + positionKit->parsePositionUpdate( trimblePos ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeight(), 1.5 ) ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeightToApply(), 0.0 ) ); + QVERIFY( positionKit->antennaHeightApplied() ); + + // Normal provider (no antennaHeight from stream): display from AppSettings, apply AppSettings value + GeoPosition normalPos; + normalPos.antennaHeight = -1; // not provided + normalPos.antennaHeightApplied = false; + + const double savedHeight = positionKit->appSettings() ? positionKit->appSettings()->gpsAntennaHeight() : 0.0; + positionKit->parsePositionUpdate( normalPos ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeight(), savedHeight ) ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeightToApply(), savedHeight ) ); + QVERIFY( !positionKit->antennaHeightApplied() ); +} +#endif // WITH_TRIMBLE_PROVIDERS diff --git a/app/test/testposition.h b/app/test/testposition.h index 40ffd45c6..b3f28e5a5 100644 --- a/app/test/testposition.h +++ b/app/test/testposition.h @@ -47,6 +47,12 @@ class TestPosition: public QObject void testPositionTransformerNetworkPosition(); void testPositionTransformerSimulatedPosition(); +#ifdef WITH_TRIMBLE_PROVIDERS + void testTrimbleMessageParser(); + void testTrimbleFrameResolver(); + void testTrimbleAntennaHeight(); +#endif + private: PositionKit *positionKit; }; diff --git a/cmake_templates/iOSInfo.plist.in b/cmake_templates/iOSInfo.plist.in index 058f409f1..6e0a2ab14 100644 --- a/cmake_templates/iOSInfo.plist.in +++ b/cmake_templates/iOSInfo.plist.in @@ -26,6 +26,27 @@ MinimumOSVersion ${IPHONEOS_DEPLOYMENT_TARGET} + CFBundleURLTypes + + + CFBundleURLName + com.lutraconsulting.merginmaps.trimble + CFBundleURLSchemes + + merginmaps-tmm + + + + LSApplicationQueriesSchemes + + tmmregister + trimblelink + trimbleMobileManager + com.trimble.mobilemanager + trimble-mobile-manager + trimble + TrimbleMobileManager + NSCameraUsageDescription Program requires access to camera to take pictures for features on map NSLocationAlwaysAndWhenInUseUsageDescription diff --git a/vcpkg.json b/vcpkg.json index 3c13c7e36..01e4ee1f3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -111,6 +111,7 @@ ] }, "qtremoteobjects", + "qtwebsockets", { "name": "qtsensors", "features": [ From 70a46ed66a48917ac90edab35012a8325add6fa6 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 27 Jul 2026 14:43:03 +0200 Subject: [PATCH 02/21] Update gitignore --- .gitignore | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index a9d29a033..18ddc4b27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,30 @@ +# broad file type filters *.autosave *.orig *.DS_Store *.user -*build*/ -app/config.pri -core/merginsecrets.cpp -app/position/providers/trimblesecrets.cpp *.idea *.vscode *.cache -test/temp_projects/ -test/temp_extra_projects/ +*.gpkg-wal +*.gpkg-shm +# secrets +core/merginsecrets.cpp +app/position/providers/trimblesecrets.cpp +.github/secrets/ios/LutraConsulting*.mobileprovision +Input_keystore.keystore +google_play_key.json +# generated files +app/config.pri +*build*/ input.pro.user* app/android/assets app/android/AndroidManifest.xml app/android/build.gradle app/android/.gradle -*.gpkg-wal -*.gpkg-shm -Input_keystore.keystore -CMakeLists.txt.user -.github/secrets/ios/LutraConsulting*.mobileprovision -google_play_key.json +test/temp_projects/ +test/temp_extra_projects/ fastlane/report.xml +CMakeLists.txt.user CMakeUserPresets.json CLAUDE.md From 8153aaf69abc4c240f4ac3d8eb7acb8fd6fb616f Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 27 Jul 2026 15:33:20 +0200 Subject: [PATCH 03/21] Refactor build system setup --- CMakeLists.txt | 11 +- CMakePresets.json | 100 ------------------ app/CMakeLists.txt | 7 +- app/position/providers/trimbleregistration.h | 6 +- ...oid.cpp => trimbleregistrationandroid.cpp} | 0 ...ation_ios.mm => trimbleregistrationios.mm} | 2 +- vcpkg.json | 5 +- 7 files changed, 18 insertions(+), 113 deletions(-) delete mode 100644 CMakePresets.json rename app/position/providers/{trimbleregistration_android.cpp => trimbleregistrationandroid.cpp} (100%) rename app/position/providers/{trimbleregistration_ios.mm => trimbleregistrationios.mm} (97%) diff --git a/CMakeLists.txt b/CMakeLists.txt index d385c39c6..c05be88a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -278,12 +278,13 @@ if (WITH_TRIMBLE_PROVIDERS) FATAL_ERROR "WITH_TRIMBLE_PROVIDERS is only supported on Android and iOS. Set WITH_TRIMBLE_PROVIDERS=FALSE for desktop builds." ) + else () + find_package( + Qt6 + COMPONENTS WebSockets + REQUIRED + ) endif () - find_package( - Qt6 - COMPONENTS WebSockets - REQUIRED - ) endif () if (ENABLE_TESTS) diff --git a/CMakePresets.json b/CMakePresets.json deleted file mode 100644 index 714f5a94c..000000000 --- a/CMakePresets.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "version": 6, - "cmakeMinimumRequired": { - "major": 3, - "minor": 25, - "patch": 0 - }, - "configurePresets": [ - { - "name": "base", - "hidden": true, - "cacheVariables": { - "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", - "USE_MM_SERVER_API_KEY": "FALSE", - "ENABLE_TESTS": "FALSE" - } - }, - { - "name": "macos-base", - "displayName": "macOS arm64 Debug", - "hidden": true, - "inherits": "base", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/macos", - "cacheVariables": { - "VCPKG_TARGET_TRIPLET": "arm64-osx", - "CMAKE_BUILD_TYPE": "Debug", - "ENABLE_TESTS": "TRUE" - } - }, - { - "name": "ios-base", - "displayName": "iOS arm64 RelWithDebInfo", - "hidden": true, - "inherits": "base", - "generator": "Xcode", - "binaryDir": "${sourceDir}/build/ios", - "cacheVariables": { - "VCPKG_HOST_TRIPLET": "arm64-osx", - "VCPKG_TARGET_TRIPLET": "arm64-ios", - "CMAKE_SYSTEM_NAME": "iOS", - "CMAKE_SYSTEM_PROCESSOR": "aarch64", - "IOS": "TRUE", - "ENABLE_BITCODE": "OFF", - "ENABLE_ARC": "ON", - "CMAKE_CXX_VISIBILITY_PRESET": "hidden", - "CMAKE_OSX_DEPLOYMENT_TARGET": "14.0", - "WITH_TRIMBLE_PROVIDERS": "TRUE" - } - }, - { - "name": "android-arm64-base", - "displayName": "Android arm64-v8a Release", - "hidden": true, - "inherits": "base", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/android-arm64", - "cacheVariables": { - "VCPKG_HOST_TRIPLET": "arm64-osx", - "VCPKG_TARGET_TRIPLET": "arm64-android", - "VCPKG_INSTALL_OPTIONS": "--allow-unsupported", - "CMAKE_BUILD_TYPE": "Release", - "ANDROID_ABI": "arm64-v8a", - "QT_ANDROID_ABIS": "arm64-v8a", - "USE_KEYCHAIN": "FALSE", - "ANDROID_SDK_ROOT": "$env{ANDROID_SDK_ROOT}", - "WITH_TRIMBLE_PROVIDERS": "TRUE" - } - }, - { - "name": "android-arm-base", - "displayName": "Android armeabi-v7a Release", - "hidden": true, - "inherits": "base", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/android-arm", - "cacheVariables": { - "VCPKG_HOST_TRIPLET": "arm64-osx", - "VCPKG_TARGET_TRIPLET": "arm-android", - "VCPKG_INSTALL_OPTIONS": "--allow-unsupported", - "CMAKE_BUILD_TYPE": "Release", - "ANDROID_ABI": "armeabi-v7a", - "QT_ANDROID_ABIS": "armeabi-v7a", - "USE_KEYCHAIN": "FALSE", - "ANDROID_SDK_ROOT": "$env{ANDROID_SDK_ROOT}" - } - }, - { - "name": "android-arm64-signed-base", - "displayName": "Android arm64-v8a Release (signed)", - "hidden": true, - "inherits": "android-arm64-base", - "cacheVariables": { - "QT_ANDROID_SIGN_APK": "YES", - "QT_ANDROID_SIGN_AAB": "YES" - } - } - ], - "buildPresets": [] -} diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e4691dba2..6e2eb89b0 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -217,14 +217,15 @@ if (WITH_TRIMBLE_PROVIDERS) ) endif () - set(MM_SRCS ${MM_SRCS} position/providers/trimblepositionprovider.cpp) set(MM_HDRS ${MM_HDRS} position/providers/trimbleregistration.h position/providers/trimblepositionprovider.h ) + set(MM_SRCS ${MM_SRCS} position/providers/trimblepositionprovider.cpp) + if (ANDROID) - set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistration_android.cpp) + set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistrationandroid.cpp) elseif (IOS) - set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistration_ios.mm) + set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistrationios.mm) endif () endif () diff --git a/app/position/providers/trimbleregistration.h b/app/position/providers/trimbleregistration.h index e4070aa46..013ba1660 100644 --- a/app/position/providers/trimbleregistration.h +++ b/app/position/providers/trimbleregistration.h @@ -16,15 +16,15 @@ #include #ifdef ANDROID -class TmmResultReceiver; // defined in trimbleregistration_android.cpp +class TmmResultReceiver; // defined in trimbleregistrationandroid.cpp #endif /** * Platform-agnostic async contract for registering with Trimble Mobile Manager. * * Call requestRegistration() once; listen for registered() or failed(). - * Concrete implementations are in trimbleregistration_android.cpp (Android) - * and trimbleregistration_ios.mm (iOS). + * Concrete implementations are in trimbleregistrationandroid.cpp (Android) + * and trimbleregistrationios.mm (iOS). */ class TrimbleRegistration : public QObject { diff --git a/app/position/providers/trimbleregistration_android.cpp b/app/position/providers/trimbleregistrationandroid.cpp similarity index 100% rename from app/position/providers/trimbleregistration_android.cpp rename to app/position/providers/trimbleregistrationandroid.cpp diff --git a/app/position/providers/trimbleregistration_ios.mm b/app/position/providers/trimbleregistrationios.mm similarity index 97% rename from app/position/providers/trimbleregistration_ios.mm rename to app/position/providers/trimbleregistrationios.mm index 382f1016e..504d61b05 100644 --- a/app/position/providers/trimbleregistration_ios.mm +++ b/app/position/providers/trimbleregistrationios.mm @@ -60,7 +60,7 @@ } QPointer self( this ); - [[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:^( BOOL success ) + [[UIApplication sharedApplication] openURL:nsUrl options:@ {} completionHandler: ^ ( BOOL success ) { qDebug() << "TrimbleRegistration: openURL completionHandler success:" << success; if ( !success && self ) diff --git a/vcpkg.json b/vcpkg.json index 01e4ee1f3..90727bbcc 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -111,7 +111,10 @@ ] }, "qtremoteobjects", - "qtwebsockets", + { + "name": "qtwebsockets", + "platform": "android | ios" + }, { "name": "qtsensors", "features": [ From fe3adca959b3599a5b229fe0840392f5427ad5fb Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Jul 2026 11:13:34 +0200 Subject: [PATCH 04/21] Refactor trimble support context property in QML --- app/main.cpp | 6 ------ app/position/positionkit.cpp | 9 +++++++++ app/position/positionkit.h | 3 +++ app/qml/gps/MMPositionProviderPage.qml | 2 +- app/qml/gps/MMProviderTypeDrawer.qml | 13 ++++++------- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 8d250b0e3..3057bd5da 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -765,12 +765,6 @@ int main( int argc, char *argv[] ) engine.rootContext()->setContextProperty( "__haveBluetooth", false ); #endif -#ifdef WITH_TRIMBLE_PROVIDERS - engine.rootContext()->setContextProperty( "__withTrimbleProviders", true ); -#else - engine.rootContext()->setContextProperty( "__withTrimbleProviders", false ); -#endif - // Even though enabling QT's HighDPI scaling removes the need to multiply pixel values with dp, // there are screens that need a "little help", because system DPR has different value than the // one we calculated. In these scenarios we use a ratio between real (our) DPR and DPR reported by QT. diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 7dc499c06..2859a0f58 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -271,6 +271,15 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( const AppSetting return constructProvider( providerType, providerId, providerName ); } +bool PositionKit::hasTrimbleSupport() +{ +#ifdef WITH_TRIMBLE_PROVIDERS + return true; +#else + return false; +#endif +} + void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) { bool hasAnythingChanged = false; diff --git a/app/position/positionkit.h b/app/position/positionkit.h index dcd71db4c..c2f4dbd14 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -79,6 +79,7 @@ class PositionKit : public QObject Q_PROPERTY( AbstractPositionProvider *positionProvider READ positionProvider WRITE setPositionProvider NOTIFY positionProviderChanged ) Q_PROPERTY( QString positionProviderName READ positionProviderName NOTIFY positionProviderNameChanged ) Q_PROPERTY( bool isMockPosition READ isMockPosition NOTIFY isMockPositionChanged ) + Q_PROPERTY( bool hasTrimbleSupport READ hasTrimbleSupport NOTIFY hasTrimbleSupportChanged ) Q_PROPERTY( AppSettings *appSettings READ appSettings WRITE setAppSettings NOTIFY appSettingsChanged ) Q_PROPERTY( double antennaHeight READ antennaHeight NOTIFY antennaHeightChanged ) @@ -139,6 +140,7 @@ class PositionKit : public QObject Q_INVOKABLE AbstractPositionProvider *constructProvider( const QString &type, const QString &id, const QString &name = QString() ); Q_INVOKABLE AbstractPositionProvider *constructActiveProvider( const AppSettings *appsettings ); + static bool hasTrimbleSupport(); AppSettings *appSettings() const; void setAppSettings( AppSettings *appSettings ); @@ -181,6 +183,7 @@ class PositionKit : public QObject void positionProviderChanged( AbstractPositionProvider *provider ); void positionProviderNameChanged(); + void hasTrimbleSupportChanged(); void positionChanged( const GeoPosition & ); void isMockPositionChanged( bool ); diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index a5c9b65fb..1ad0570fe 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -133,7 +133,7 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) onClicked: { - if ( __haveBluetooth || __withTrimbleProviders ) { + if ( __haveBluetooth || PositionKit.hasTrimbleSupport ) { providerTypeDrawer.open() } else { diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index a5898abed..8a0540b3f 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -16,8 +16,6 @@ import "../components" as MMComponents MMComponents.MMListDrawer { id: root - property bool withTrimbleProviders: __withTrimbleProviders - signal providerSelected( string providerType ) drawerHeader.title: qsTr( "Connect new receiver" ) @@ -29,12 +27,13 @@ MMComponents.MMListDrawer { id: providerTypeModel Component.onCompleted: { - providerTypeModel.append( [ - { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon }, - { name: qsTr( "Network (TCP, UDP)" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } - ] ) + if ( __haveBluetooth ) { + providerTypeModel.append( { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon } ) + } + + providerTypeModel.append( { name: qsTr( "Network (TCP, UDP)" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } ) - if ( root.withTrimbleProviders ) { + if ( PositionKit.hasTrimbleSupport ) { providerTypeModel.append( { name: qsTr( "Trimble" ), description: qsTr( "Trimble receivers via Trimble Mobile Manager" ), type: "trimble", icon: __style.gpsIcon } ) } } From 887abd778d979f48411a4e0f8a3c35f7a311750d Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Jul 2026 11:55:58 +0200 Subject: [PATCH 05/21] Move trimble flag definition to config file --- app/CMakeLists.txt | 1 - cmake_templates/mmconfig.h.in | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 6e2eb89b0..1f51bac7b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -538,7 +538,6 @@ if (HAVE_BLUETOOTH) endif () if (WITH_TRIMBLE_PROVIDERS) - target_compile_definitions(MerginMaps PRIVATE WITH_TRIMBLE_PROVIDERS) target_link_libraries(MerginMaps PUBLIC Qt6::WebSockets) endif () diff --git a/cmake_templates/mmconfig.h.in b/cmake_templates/mmconfig.h.in index 181bfc603..2d4eb5c0c 100644 --- a/cmake_templates/mmconfig.h.in +++ b/cmake_templates/mmconfig.h.in @@ -12,6 +12,7 @@ #cmakedefine TEST_DATA_DIR "@TEST_DATA_DIR@" #cmakedefine HAVE_BLUETOOTH +#cmakedefine WITH_TRIMBLE_PROVIDERS #endif From b4ad426bf05775206bfb10ca3264d31b24c2b01d Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Jul 2026 16:00:31 +0200 Subject: [PATCH 06/21] Refactor bluetooth provider guard --- .github/workflows/macos.yml | 2 +- CMakeLists.txt | 10 +++++----- app/CMakeLists.txt | 4 ++-- app/bluetoothdiscoverymodel.cpp | 14 +++++++------- app/bluetoothdiscoverymodel.h | 6 +++--- app/main.cpp | 6 ------ app/position/positionkit.cpp | 13 +++++++++++-- app/position/positionkit.h | 3 +++ app/qml/gps/MMPositionProviderPage.qml | 2 +- app/qml/gps/MMProviderTypeDrawer.qml | 2 +- app/test/testmaptools.cpp | 8 +++++--- app/test/testposition.cpp | 20 ++++++++++---------- app/test/testposition.h | 2 +- app/test/testvariablesmanager.cpp | 22 ++++++++++++---------- cmake_templates/mmconfig.h.in | 2 +- 15 files changed, 63 insertions(+), 53 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3417ab130..1f6053911 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -119,7 +119,7 @@ jobs: -DVCPKG_TARGET_TRIPLET=${{ env.TRIPLET }} \ -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake \ -DUSE_MM_SERVER_API_KEY=TRUE \ - -DHAVE_BLUETOOTH=FALSE \ + -DWITH_BLUETOOTH_PROVIDERS=FALSE \ -DUSE_KEYCHAIN=No \ -DCOVERAGE=TRUE \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ diff --git a/CMakeLists.txt b/CMakeLists.txt index c05be88a7..58a119fa3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,9 +81,9 @@ else () endif () if (IOS) - set(HAVE_BLUETOOTH_DEFAULT FALSE) + set(WITH_BLUETOOTH_PROVIDERS_DEFAULT FALSE) else () - set(HAVE_BLUETOOTH_DEFAULT TRUE) + set(WITH_BLUETOOTH_PROVIDERS_DEFAULT TRUE) endif () if (DEFINED ENV{MM_VERSION_CODE}) @@ -138,8 +138,8 @@ set(ENABLE_TESTS ${ENABLE_TESTS_DEFAULT} CACHE BOOL "Whether to build tests" ) -set(HAVE_BLUETOOTH - ${HAVE_BLUETOOTH_DEFAULT} +set(WITH_BLUETOOTH_PROVIDERS + ${WITH_BLUETOOTH_PROVIDERS_DEFAULT} CACHE BOOL "Building with bluetooth position provider" ) @@ -264,7 +264,7 @@ if (IOS OR MACOS) find_package(Tasn1 REQUIRED) endif () -if (HAVE_BLUETOOTH) +if (WITH_BLUETOOTH_PROVIDERS) find_package( Qt6 COMPONENTS Bluetooth diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 1f51bac7b..3a91caae9 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -203,7 +203,7 @@ if (NOT WIN32) set(MM_SRCS ${MM_SRCS} static_plugins.cpp) endif () -if (HAVE_BLUETOOTH) +if (WITH_BLUETOOTH_PROVIDERS) set(MM_SRCS ${MM_SRCS} position/providers/bluetoothpositionprovider.cpp) set(MM_HDRS ${MM_HDRS} position/providers/bluetoothpositionprovider.h) @@ -533,7 +533,7 @@ if (ANDROID) target_link_libraries(MerginMaps PRIVATE Qt6::CorePrivate) endif () -if (HAVE_BLUETOOTH) +if (WITH_BLUETOOTH_PROVIDERS) target_link_libraries(MerginMaps PUBLIC Qt6::Bluetooth) endif () diff --git a/app/bluetoothdiscoverymodel.cpp b/app/bluetoothdiscoverymodel.cpp index 2a3107be4..f77f418f3 100644 --- a/app/bluetoothdiscoverymodel.cpp +++ b/app/bluetoothdiscoverymodel.cpp @@ -10,14 +10,14 @@ #include "bluetoothdiscoverymodel.h" #include "coreutils.h" -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS #include #endif BluetoothDiscoveryModel::BluetoothDiscoveryModel( QObject *parent ) : QAbstractListModel( parent ) { -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS mDiscoveryAgent = std::unique_ptr( new QBluetoothDeviceDiscoveryAgent() ); connect( mDiscoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscoveryModel::deviceDiscovered ); @@ -39,7 +39,7 @@ QHash BluetoothDiscoveryModel::roleNames() const { QHash roles; -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS roles.insert( DataRoles::DeviceAddress, "deviceAddress" ); roles.insert( DataRoles::DeviceName, "deviceName" ); roles.insert( DataRoles::SignalStrength, "signalStrength" ); @@ -50,7 +50,7 @@ QHash BluetoothDiscoveryModel::roleNames() const int BluetoothDiscoveryModel::rowCount( const QModelIndex & ) const { -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS return mFoundDevices.count(); #else return 0; @@ -59,7 +59,7 @@ int BluetoothDiscoveryModel::rowCount( const QModelIndex & ) const QVariant BluetoothDiscoveryModel::data( const QModelIndex &index, int role ) const { -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS if ( !index.isValid() ) return QVariant(); @@ -104,7 +104,7 @@ void BluetoothDiscoveryModel::setDiscovering( bool discovering ) if ( mDiscovering == discovering ) return; -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS if ( discovering ) { mDiscoveryAgent->start(); @@ -120,7 +120,7 @@ void BluetoothDiscoveryModel::setDiscovering( bool discovering ) emit discoveringChanged( mDiscovering ); } -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS void BluetoothDiscoveryModel::deviceDiscovered( const QBluetoothDeviceInfo &device ) { for ( int i = 0; i < mFoundDevices.count(); i++ ) diff --git a/app/bluetoothdiscoverymodel.h b/app/bluetoothdiscoverymodel.h index c9b35dd28..15e9cee66 100644 --- a/app/bluetoothdiscoverymodel.h +++ b/app/bluetoothdiscoverymodel.h @@ -17,7 +17,7 @@ #include "mmconfig.h" -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS #include #include #endif @@ -50,7 +50,7 @@ class BluetoothDiscoveryModel : public QAbstractListModel public slots: -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS void deviceDiscovered( const QBluetoothDeviceInfo &info ); void deviceUpdated( const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields ); #endif @@ -62,7 +62,7 @@ class BluetoothDiscoveryModel : public QAbstractListModel private: bool mDiscovering = false; -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS QList mFoundDevices; std::unique_ptr mDiscoveryAgent; #endif diff --git a/app/main.cpp b/app/main.cpp index 3057bd5da..d3aa6ded1 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -759,12 +759,6 @@ int main( int argc, char *argv[] ) engine.rootContext()->setContextProperty( "__layerDetailLegendImageProvider", layerDetailLegendImageProvider ); engine.addImageProvider( QLatin1String( "LayerDetailLegendImageProvider" ), layerDetailLegendImageProvider ); -#ifdef HAVE_BLUETOOTH - engine.rootContext()->setContextProperty( "__haveBluetooth", true ); -#else - engine.rootContext()->setContextProperty( "__haveBluetooth", false ); -#endif - // Even though enabling QT's HighDPI scaling removes the need to multiply pixel values with dp, // there are screens that need a "little help", because system DPR has different value than the // one we calculated. In these scenarios we use a ratio between real (our) DPR and DPR reported by QT. diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 2859a0f58..61e192d82 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -16,7 +16,7 @@ #include "appsettings.h" #include "inpututils.h" -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS #include "providers/bluetoothpositionprovider.h" #endif #include "providers/internalpositionprovider.h" @@ -145,7 +145,7 @@ QString PositionKit::positionProviderName() const AbstractPositionProvider *PositionKit::constructProvider( const QString &type, const QString &id, const QString &name ) { -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS if ( type == QStringLiteral( "external_bt" ) ) { AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name, *mPositionTransformer ); @@ -280,6 +280,15 @@ bool PositionKit::hasTrimbleSupport() #endif } +bool PositionKit::hasBluetoothSupport() +{ +#ifdef WITH_BLUETOOTH_PROVIDERS + return true; +#else + return false; +#endif +} + void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) { bool hasAnythingChanged = false; diff --git a/app/position/positionkit.h b/app/position/positionkit.h index c2f4dbd14..ee4178310 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -80,6 +80,7 @@ class PositionKit : public QObject Q_PROPERTY( QString positionProviderName READ positionProviderName NOTIFY positionProviderNameChanged ) Q_PROPERTY( bool isMockPosition READ isMockPosition NOTIFY isMockPositionChanged ) Q_PROPERTY( bool hasTrimbleSupport READ hasTrimbleSupport NOTIFY hasTrimbleSupportChanged ) + Q_PROPERTY( bool hasBluetoothSupport READ hasBluetoothSupport NOTIFY hasBluetoothSupportChanged ) Q_PROPERTY( AppSettings *appSettings READ appSettings WRITE setAppSettings NOTIFY appSettingsChanged ) Q_PROPERTY( double antennaHeight READ antennaHeight NOTIFY antennaHeightChanged ) @@ -141,6 +142,7 @@ class PositionKit : public QObject Q_INVOKABLE AbstractPositionProvider *constructProvider( const QString &type, const QString &id, const QString &name = QString() ); Q_INVOKABLE AbstractPositionProvider *constructActiveProvider( const AppSettings *appsettings ); static bool hasTrimbleSupport(); + static bool hasBluetoothSupport(); AppSettings *appSettings() const; void setAppSettings( AppSettings *appSettings ); @@ -184,6 +186,7 @@ class PositionKit : public QObject void positionProviderChanged( AbstractPositionProvider *provider ); void positionProviderNameChanged(); void hasTrimbleSupportChanged(); + void hasBluetoothSupportChanged(); void positionChanged( const GeoPosition & ); void isMockPositionChanged( bool ); diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 1ad0570fe..3f1fc74e3 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -133,7 +133,7 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) onClicked: { - if ( __haveBluetooth || PositionKit.hasTrimbleSupport ) { + if ( PositionKit.hasBluetoothSupport || PositionKit.hasTrimbleSupport ) { providerTypeDrawer.open() } else { diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index 8a0540b3f..b826eaf0d 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -27,7 +27,7 @@ MMComponents.MMListDrawer { id: providerTypeModel Component.onCompleted: { - if ( __haveBluetooth ) { + if ( PositionKit.hasBluetoothSupport ) { providerTypeModel.append( { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon } ) } diff --git a/app/test/testmaptools.cpp b/app/test/testmaptools.cpp index 2b7176a5c..229712d1c 100644 --- a/app/test/testmaptools.cpp +++ b/app/test/testmaptools.cpp @@ -13,6 +13,8 @@ #include #include +#include "mmconfig.h" + #include "qgspoint.h" #include "qgslinestring.h" #include "qgspolygon.h" @@ -35,7 +37,7 @@ #include "featurelayerpair.h" #include "streamingintervaltype.h" -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -3053,7 +3055,7 @@ void TestMapTools::testAntennaHeight() void TestMapTools::testSmallTracking() { -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS QgsVectorLayer *pointLayer = new QgsVectorLayer( QStringLiteral( "PointZ?crs=epsg:4326" ), QString(), QStringLiteral( "memory" ) ); RecordingMapTool mapTool; @@ -3078,7 +3080,7 @@ void TestMapTools::testSmallTracking() mapTool.setActiveLayer( pointLayer ); mapTool.setActiveFeature( QgsFeature() ); - BluetoothPositionProvider *btProvider = new BluetoothPositionProvider( "AA:AA:FF:AA:00:10", "testBluetoothProvider" ); + AbstractPositionProvider *btProvider = mPositionKit->constructProvider( QStringLiteral( "external_bt" ), QStringLiteral( "AA:AA:FF:AA:00:10" ), QStringLiteral( "testBluetoothProvider" ) ); mPositionKit->setPositionProvider( btProvider ); NmeaParser parser; diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 3713efdf1..dbd98a81e 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -19,7 +19,7 @@ #include "position/positionkit.h" #include "position/providers/simulatedpositionprovider.h" -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -91,7 +91,7 @@ void TestPosition::simulatedPosition() QVERIFY( !positionKit->positionProvider() ); } -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS void TestPosition::testBluetoothProviderConnection() { @@ -256,7 +256,7 @@ void TestPosition::testPositionProviderKeysInSettings() QSettings rawSettings; rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); @@ -507,7 +507,7 @@ void TestPosition::testPositionTransformerAndroidPosition() PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -566,7 +566,7 @@ void TestPosition::testPositionTransformerBluetoothPosition() PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -632,7 +632,7 @@ void TestPosition::testPositionTransformerInternalAndroidPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -668,7 +668,7 @@ void TestPosition::testPositionTransformerInternalIosPosition() PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -745,7 +745,7 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -780,7 +780,7 @@ void TestPosition::testPositionTransformerNetworkPosition() PositionTransformer passThroughTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -846,7 +846,7 @@ void TestPosition::testPositionTransformerSimulatedPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); diff --git a/app/test/testposition.h b/app/test/testposition.h index b3f28e5a5..6a609ccd2 100644 --- a/app/test/testposition.h +++ b/app/test/testposition.h @@ -28,7 +28,7 @@ class TestPosition: public QObject void simulatedPosition(); -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS void testBluetoothProviderConnection(); void testBluetoothProviderPosition(); #endif diff --git a/app/test/testvariablesmanager.cpp b/app/test/testvariablesmanager.cpp index cd191c431..7ae298680 100644 --- a/app/test/testvariablesmanager.cpp +++ b/app/test/testvariablesmanager.cpp @@ -7,6 +7,9 @@ * * ***************************************************************************/ #include "testvariablesmanager.h" + +#include "mmconfig.h" + #include "qgsexpression.h" #include "qgsexpressioncontext.h" #include "qgsexpressioncontextutils.h" @@ -16,7 +19,7 @@ #include "merginapi.h" -#ifdef HAVE_BLUETOOTH +#ifdef WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -45,11 +48,11 @@ void TestVariablesManager::cleanup() void TestVariablesManager::testPositionVariables() { -#ifdef HAVE_BLUETOOTH mAppSettings->setGpsAntennaHeight( 0 ); - BluetoothPositionProvider *btProvider = new BluetoothPositionProvider( "AA:AA:FF:AA:00:10", "testBluetoothProvider" ); - mPositionKit->setPositionProvider( btProvider ); + AbstractPositionProvider *testProvider = mPositionKit->constructProvider( QStringLiteral( "internal" ), + QStringLiteral( "devicegps" ) ); + mPositionKit->setPositionProvider( testProvider ); NmeaParser parser; QString fullNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_full.txt"; @@ -60,7 +63,7 @@ void TestVariablesManager::testPositionVariables() GeoPosition pos = GeoPosition::fromQgsGpsInformation( position ); pos.verticalSpeed = 0; pos.magneticVariation = 0; - emit btProvider->positionChanged( pos ); + emit testProvider->positionChanged( pos ); QgsExpressionContext context; context << mVariablesManager->positionScope(); @@ -82,14 +85,14 @@ void TestVariablesManager::testPositionVariables() evaluateExpression( QStringLiteral( "@position_pdop" ), QStringLiteral( "5.90" ), &context ); evaluateExpression( QStringLiteral( "@position_gps_fix" ), QStringLiteral( "RTK float" ), &context ); 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_bt" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_address" ), QStringLiteral( "devicegps" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_name" ), QStringLiteral( "Internal" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_type" ), QStringLiteral( "internal" ), &context ); mAppSettings->setGpsAntennaHeight( 1.6784 ); pos.verticalSpeed = 1.345; pos.magneticVariation = 14.34; - emit btProvider->positionChanged( pos ); + emit testProvider->positionChanged( pos ); context << mVariablesManager->positionScope(); evaluateExpression( QStringLiteral( "@position_vertical_speed" ), QStringLiteral( "1.34" ), &context ); @@ -97,7 +100,6 @@ void TestVariablesManager::testPositionVariables() evaluateExpression( QStringLiteral( "@position_gps_antenna_height" ), QStringLiteral( "1.678" ), &context ); mAppSettings->setGpsAntennaHeight( 0 ); -#endif } void TestVariablesManager::testUserVariables() diff --git a/cmake_templates/mmconfig.h.in b/cmake_templates/mmconfig.h.in index 2d4eb5c0c..8ca1fa395 100644 --- a/cmake_templates/mmconfig.h.in +++ b/cmake_templates/mmconfig.h.in @@ -11,7 +11,7 @@ #cmakedefine MM_TEST #cmakedefine TEST_DATA_DIR "@TEST_DATA_DIR@" -#cmakedefine HAVE_BLUETOOTH +#cmakedefine WITH_BLUETOOTH_PROVIDERS #cmakedefine WITH_TRIMBLE_PROVIDERS #endif From 98224a42e4c25e3532b3caad3e71fb850223a5d3 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Jul 2026 16:24:24 +0200 Subject: [PATCH 07/21] Improve preprocessor guard resiliency Some test cases with bluetooth preprocessor guard were not run, because there was missing import for mmconfig.h --- app/CMakeLists.txt | 6 ++++++ app/bluetoothdiscoverymodel.cpp | 14 +++++++------- app/bluetoothdiscoverymodel.h | 6 +++--- app/position/positionkit.cpp | 12 ++++++------ app/test/testmaptools.cpp | 4 ++-- app/test/testposition.cpp | 22 +++++++++++----------- app/test/testposition.h | 4 ++-- app/test/testvariablesmanager.cpp | 2 +- cmake_templates/mmconfig.h.in | 4 ++-- 9 files changed, 40 insertions(+), 34 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 3a91caae9..ed910b431 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -349,6 +349,12 @@ qt_add_executable( main.cpp ) +# throw error when #include is missing, guards against not imported preprocessor +# definitions +if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$") + target_compile_options(MerginMaps PRIVATE -Werror=undef) +endif () + set_target_properties(MerginMaps PROPERTIES WIN32_EXECUTABLE TRUE) target_include_directories( diff --git a/app/bluetoothdiscoverymodel.cpp b/app/bluetoothdiscoverymodel.cpp index f77f418f3..808f68c22 100644 --- a/app/bluetoothdiscoverymodel.cpp +++ b/app/bluetoothdiscoverymodel.cpp @@ -10,14 +10,14 @@ #include "bluetoothdiscoverymodel.h" #include "coreutils.h" -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS #include #endif BluetoothDiscoveryModel::BluetoothDiscoveryModel( QObject *parent ) : QAbstractListModel( parent ) { -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS mDiscoveryAgent = std::unique_ptr( new QBluetoothDeviceDiscoveryAgent() ); connect( mDiscoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscoveryModel::deviceDiscovered ); @@ -39,7 +39,7 @@ QHash BluetoothDiscoveryModel::roleNames() const { QHash roles; -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS roles.insert( DataRoles::DeviceAddress, "deviceAddress" ); roles.insert( DataRoles::DeviceName, "deviceName" ); roles.insert( DataRoles::SignalStrength, "signalStrength" ); @@ -50,7 +50,7 @@ QHash BluetoothDiscoveryModel::roleNames() const int BluetoothDiscoveryModel::rowCount( const QModelIndex & ) const { -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS return mFoundDevices.count(); #else return 0; @@ -59,7 +59,7 @@ int BluetoothDiscoveryModel::rowCount( const QModelIndex & ) const QVariant BluetoothDiscoveryModel::data( const QModelIndex &index, int role ) const { -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS if ( !index.isValid() ) return QVariant(); @@ -104,7 +104,7 @@ void BluetoothDiscoveryModel::setDiscovering( bool discovering ) if ( mDiscovering == discovering ) return; -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS if ( discovering ) { mDiscoveryAgent->start(); @@ -120,7 +120,7 @@ void BluetoothDiscoveryModel::setDiscovering( bool discovering ) emit discoveringChanged( mDiscovering ); } -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS void BluetoothDiscoveryModel::deviceDiscovered( const QBluetoothDeviceInfo &device ) { for ( int i = 0; i < mFoundDevices.count(); i++ ) diff --git a/app/bluetoothdiscoverymodel.h b/app/bluetoothdiscoverymodel.h index 15e9cee66..90f01502c 100644 --- a/app/bluetoothdiscoverymodel.h +++ b/app/bluetoothdiscoverymodel.h @@ -17,7 +17,7 @@ #include "mmconfig.h" -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS #include #include #endif @@ -50,7 +50,7 @@ class BluetoothDiscoveryModel : public QAbstractListModel public slots: -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS void deviceDiscovered( const QBluetoothDeviceInfo &info ); void deviceUpdated( const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields ); #endif @@ -62,7 +62,7 @@ class BluetoothDiscoveryModel : public QAbstractListModel private: bool mDiscovering = false; -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS QList mFoundDevices; std::unique_ptr mDiscoveryAgent; #endif diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 61e192d82..c63436e99 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -16,13 +16,13 @@ #include "appsettings.h" #include "inpututils.h" -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS #include "providers/bluetoothpositionprovider.h" #endif #include "providers/internalpositionprovider.h" #include "providers/simulatedpositionprovider.h" #include "providers/networkpositionprovider.h" -#ifdef WITH_TRIMBLE_PROVIDERS +#if WITH_TRIMBLE_PROVIDERS #include "providers/trimblepositionprovider.h" #endif #ifdef ANDROID @@ -145,7 +145,7 @@ QString PositionKit::positionProviderName() const AbstractPositionProvider *PositionKit::constructProvider( const QString &type, const QString &id, const QString &name ) { -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS if ( type == QStringLiteral( "external_bt" ) ) { AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name, *mPositionTransformer ); @@ -154,7 +154,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c } #endif -#ifdef WITH_TRIMBLE_PROVIDERS +#if WITH_TRIMBLE_PROVIDERS if ( type == QStringLiteral( "external_trimble" ) ) { AbstractPositionProvider *provider = new TrimblePositionProvider( id, name, *mPositionTransformer ); @@ -273,7 +273,7 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( const AppSetting bool PositionKit::hasTrimbleSupport() { -#ifdef WITH_TRIMBLE_PROVIDERS +#if WITH_TRIMBLE_PROVIDERS return true; #else return false; @@ -282,7 +282,7 @@ bool PositionKit::hasTrimbleSupport() bool PositionKit::hasBluetoothSupport() { -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS return true; #else return false; diff --git a/app/test/testmaptools.cpp b/app/test/testmaptools.cpp index 229712d1c..ee47b7a82 100644 --- a/app/test/testmaptools.cpp +++ b/app/test/testmaptools.cpp @@ -37,7 +37,7 @@ #include "featurelayerpair.h" #include "streamingintervaltype.h" -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -3055,7 +3055,7 @@ void TestMapTools::testAntennaHeight() void TestMapTools::testSmallTracking() { -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS QgsVectorLayer *pointLayer = new QgsVectorLayer( QStringLiteral( "PointZ?crs=epsg:4326" ), QString(), QStringLiteral( "memory" ) ); RecordingMapTool mapTool; diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index dbd98a81e..4180d89b3 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -19,7 +19,7 @@ #include "position/positionkit.h" #include "position/providers/simulatedpositionprovider.h" -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -91,7 +91,7 @@ void TestPosition::simulatedPosition() QVERIFY( !positionKit->positionProvider() ); } -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS void TestPosition::testBluetoothProviderConnection() { @@ -256,7 +256,7 @@ void TestPosition::testPositionProviderKeysInSettings() QSettings rawSettings; rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); @@ -507,7 +507,7 @@ void TestPosition::testPositionTransformerAndroidPosition() PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -566,7 +566,7 @@ void TestPosition::testPositionTransformerBluetoothPosition() PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -632,7 +632,7 @@ void TestPosition::testPositionTransformerInternalAndroidPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -668,7 +668,7 @@ void TestPosition::testPositionTransformerInternalIosPosition() PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -745,7 +745,7 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -780,7 +780,7 @@ void TestPosition::testPositionTransformerNetworkPosition() PositionTransformer passThroughTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -846,7 +846,7 @@ void TestPosition::testPositionTransformerSimulatedPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -875,7 +875,7 @@ void TestPosition::testPositionTransformerSimulatedPosition() QVERIFY( qgsDoubleNear( newPosition.elevation_diff, 43.764250688281265 ) ); } -#ifdef WITH_TRIMBLE_PROVIDERS +#if WITH_TRIMBLE_PROVIDERS #include "position/providers/trimblepositionprovider.h" #include "appsettings.h" diff --git a/app/test/testposition.h b/app/test/testposition.h index 6a609ccd2..8426afa70 100644 --- a/app/test/testposition.h +++ b/app/test/testposition.h @@ -28,7 +28,7 @@ class TestPosition: public QObject void simulatedPosition(); -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS void testBluetoothProviderConnection(); void testBluetoothProviderPosition(); #endif @@ -47,7 +47,7 @@ class TestPosition: public QObject void testPositionTransformerNetworkPosition(); void testPositionTransformerSimulatedPosition(); -#ifdef WITH_TRIMBLE_PROVIDERS +#if WITH_TRIMBLE_PROVIDERS void testTrimbleMessageParser(); void testTrimbleFrameResolver(); void testTrimbleAntennaHeight(); diff --git a/app/test/testvariablesmanager.cpp b/app/test/testvariablesmanager.cpp index 7ae298680..e3b2e74b5 100644 --- a/app/test/testvariablesmanager.cpp +++ b/app/test/testvariablesmanager.cpp @@ -19,7 +19,7 @@ #include "merginapi.h" -#ifdef WITH_BLUETOOTH_PROVIDERS +#if WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif diff --git a/cmake_templates/mmconfig.h.in b/cmake_templates/mmconfig.h.in index 8ca1fa395..3255029e5 100644 --- a/cmake_templates/mmconfig.h.in +++ b/cmake_templates/mmconfig.h.in @@ -11,8 +11,8 @@ #cmakedefine MM_TEST #cmakedefine TEST_DATA_DIR "@TEST_DATA_DIR@" -#cmakedefine WITH_BLUETOOTH_PROVIDERS -#cmakedefine WITH_TRIMBLE_PROVIDERS +#cmakedefine01 WITH_BLUETOOTH_PROVIDERS +#cmakedefine01 WITH_TRIMBLE_PROVIDERS #endif From 1924083e12a8fa97a68fd61ecf07469d0e694d9a Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Jul 2026 17:11:03 +0200 Subject: [PATCH 08/21] Update INSTALL.md --- INSTALL.md | 76 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 8095cdee3..76259ca11 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -60,6 +60,7 @@ Open workflow file for your platform/target and see the version of libraries use ## 2.1 Secrets +### Mergin API To communicate with MerginAPI, some endpoints need to attach `api_key`. To not leak API_KEY, the source code that returns the API_KEYS is encrypted. @@ -76,17 +77,34 @@ manifest files. encrypt -``` +```bash cd core/ openssl aes-256-cbc -in merginsecrets.cpp -out merginsecrets.cpp.enc -md md5 ``` decrypt -``` +```bash cd core/ openssl aes-256-cbc -d -in merginsecrets.cpp.enc -out merginsecrets.cpp -md md5 ``` +### Trimble +Similar approach has been used with APP ID for Trimble Mobile Manager. Having access to this file shouldn't be necessary. +You can always defer the build to CI. + +encrypt + +```bash +cd app/position/providers +openssl aes-256-cbc -in trimblesecrets.cpp -out trimblesecrets.cpp.enc -md md5 +``` + +decrypt + +```bash +cd app/position/providers +openssl aes-256-cbc -d -in trimblesecrets.cpp.enc -out trimblesecrets.cpp -md md5 +``` ## 2.2 Code formatting @@ -109,7 +127,7 @@ to install QtCreator and Qt on your host to be able to release translations. Dependencies are build with vcpkg. To fix the version of libraries, you need to download vcpkg and checkout to git commit specified in the file `VCPKG_BASELINE` in the repository. The VCPKG repository **HAS TO BE** outside the mobile repository. -``` +```bash git clone https://github.com/microsoft/vcpkg.git VCPKG_TAG=`cat mobile/VCPKG_BASELINE` git checkout ${VCPKG_TAG} @@ -130,7 +148,7 @@ Steps to build and run mobile app: 1. Install some dependencies, critically bison and flex. See "Install Build Dependencies" step in `.github/workflows/linux.yml` - ``` + ```bash sudo apt-get install -y \ gperf autopoint '^libxcb.*-dev' libx11-xcb-dev libegl1-mesa-dev \ libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev \ @@ -163,7 +181,7 @@ Steps to build and run mobile app: To use USE_MM_SERVER_API_KEY read [Secrets](#secrets) section. - ``` + ```bash mkdir -p build cd build cmake \ @@ -182,13 +200,13 @@ Steps to build and run mobile app: 4. Build application - ``` + ```bash ninja ``` 5. Run mobile app - ``` + ```bash ./app/MerginMaps ``` @@ -227,7 +245,7 @@ For building ABIs see https://www.qt.io/blog/android-multi-abi-builds-are-back can take considerable time (e.g. an hour). Subsequent runs will be faster as the libraries without change will be taken from local binary vcpkg cache. - ``` + ```bash export ANDROID_NDK_HOME=/home//android/ndk/ export ANDROID_SDK_ROOT=/home//android export QT_ANDROID_KEYSTORE_ALIAS= @@ -287,7 +305,7 @@ To use USE_MM_SERVER_API_KEY read [Secrets](#secrets) section. To build the project, go to the build folder and run the following command: -``` +```bash ninja ``` @@ -343,7 +361,7 @@ build_folder/ can take considerable time (e.g. an hour). Subsequent runs will be faster as the libraries without change will be taken from local binary vcpkg cache. - ``` + ```bash export ANDROID_NDK_HOME=/Users//android/ndk/ export ANDROID_SDK_ROOT=/Users//android export QT_ANDROID_KEYSTORE_ALIAS= @@ -406,13 +424,13 @@ build_folder/ To build the project, go to the build folder and run the following command: - ``` + ```bash ninja ``` Once built, navigate to the path and run MerginMaps: - ``` + ```bash build_folder/ app/ MerginMaps @@ -457,7 +475,7 @@ mobile app for Android on Windows, please help us to update this section. Note: make sure you adjust VCPKG_HOST_TRIPLET and CMAKE_SYSTEM_PROCESSOR if you use x64-osx host machine. - ``` + ```bash cd build export PATH=$(brew --prefix flex)/bin:$(brew --prefix bison)/bin:$(brew --prefix gettext)/bin:$PATH;\ @@ -469,14 +487,14 @@ mobile app for Android on Windows, please help us to update this section. -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ -DVCPKG_TARGET_TRIPLET=arm64-ios \ -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake \ - -D ENABLE_BITCODE=OFF \ - -D ENABLE_ARC=ON \ - -D CMAKE_CXX_VISIBILITY_PRESET=hidden \ - -D CMAKE_SYSTEM_NAME=iOS \ + -DENABLE_BITCODE=OFF \ + -DENABLE_ARC=ON \ + -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ + -DCMAKE_SYSTEM_NAME=iOS \ -DIOS=TRUE \ -DUSE_MM_SERVER_API_KEY=FALSE \ - -G "Xcode" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -G "Xcode" \ -S ../mobile \ -B ./ ``` @@ -485,7 +503,7 @@ mobile app for Android on Windows, please help us to update this section. Now you can create a build (either on command line or by setting these variables in Qt Creator) -``` +```bash cd build xcodebuild \ @@ -509,7 +527,7 @@ Once the project is opened, build it from Xcode. 1. Install some dependencies, critically XCode, bison and flex. See "Install Build Dependencies" step in `.github/workflows/macos.yml` -``` +```bash brew install cmake automake bison flex gnu-sed autoconf-archive libtool ninja pkg-config ``` @@ -534,7 +552,7 @@ Once the project is opened, build it from Xcode. Note: for **x64-osx** (intel laptops) build use **x64-osx** VCPKG_TARGET_TRIPLET instead of **arm64-osx** (Mx laptops) - ``` + ```bash cd build export PATH=$(brew --prefix flex)/bin:$(brew --prefix bison)/bin:$(brew --prefix gettext)/bin:$PATH;\ @@ -555,12 +573,12 @@ Once the project is opened, build it from Xcode. 4. Build application - ``` + ```bash ninja ``` 5. Run the mobile app - ``` + ```bash ./app/MerginMaps.app/Contents/MacOS/MerginMaps ``` @@ -569,7 +587,7 @@ Once the project is opened, build it from Xcode. 1. Install some dependencies. See `.github/workflows/win.yml` Critically Visual Studio, cmake, bison and flex. Setup build VS environment (adjust to your version) -``` +```shell "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" -arch=x64 ``` @@ -595,31 +613,31 @@ Once the project is opened, build it from Xcode. To use USE_MM_SERVER_API_KEY read [Secrets](#secrets) section. - ``` + ```shell mkdir build cd build cmake ^ -DCMAKE_BUILD_TYPE=Debug ^ -DCMAKE_TOOLCHAIN_FILE:PATH="/vcpkg/scripts/buildsystems/vcpkg.cmake" ^ - -G "Visual Studio 17 2022" ^ - -A x64 ^ -DVCPKG_TARGET_TRIPLET=x64-windows ^ -DUSE_MM_SERVER_API_KEY=FALSE ^ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ^ + -A x64 ^ + -G "Visual Studio 17 2022" ^ -S ../mobile ^ -B . ``` 4. Build application - ``` + ```shell cd build cmake --build . --config Release --verbose ``` 5. Run the mobile app - ``` + ```shell ./app/MerginMaps.exe ``` From 340a1aac0180119832790546a1eba55d4c5659a0 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 29 Jul 2026 16:27:24 +0200 Subject: [PATCH 09/21] Refactor provider UI flow --- .../MMExternalProviderConnectionDrawer.qml | 110 ++++++++++++++---- app/qml/gps/MMPositionProviderPage.qml | 7 +- app/qml/gps/MMProviderTypeDrawer.qml | 22 ++-- 3 files changed, 98 insertions(+), 41 deletions(-) diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index bdcdb3d6f..4044d445a 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -30,20 +30,12 @@ MMComponents.MMDrawer { states: [ State { - name: "working" + name: "connecting" when: root.positionProvider && root.positionProvider.state === PositionProvider.Connecting PropertyChanges { - message.image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage - message.title: root.providerType === "network" - ? qsTr( "Connecting to external receiver" ) - : ( root.positionProvider.name() - ? qsTr( "Connecting to" ) + " " + root.positionProvider.name() - : qsTr( "Connecting" ) + root.connectingSuffixAnimation ) - message.description: root.providerType === "bluetooth" - ? qsTr( "You might be asked to pair your device during this process." ) - : ( root.positionProvider.id() - ? qsTr( "Connecting to" ) + " " + root.positionProvider.id() + qsTr( ". You can close this panel, the app will continue in the background." ) - : qsTr( "Connecting" ) + root.connectingSuffixAnimation ) + message.image: root.getExternalProviderImage() + message.title: root.getConnectingTitle() + message.description: root.getConnectingDescription() message.linkText: "" } }, @@ -51,7 +43,7 @@ MMComponents.MMDrawer { name: "success" when: root.positionProvider && root.positionProvider.state === PositionProvider.Connected PropertyChanges { - message.image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage + message.image: root.getExternalProviderImage() message.title: qsTr( "Connected" ) message.description: "" message.linkText: "" @@ -62,12 +54,8 @@ MMComponents.MMDrawer { when: !root.positionProvider || root.positionProvider.state === PositionProvider.NoConnection PropertyChanges { message.image: __style.externalGpsRedImage - message.title: qsTr( "Failed to connect to" ) + " " + ( root.positionProvider - ? ( root.providerType === "network" ? root.positionProvider.id() : root.positionProvider.name() ) - : "" ) - 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." ) + message.title: root.getFailTitle() + message.description: root.getFailDescription() message.linkText: qsTr( "Learn more" ) } }, @@ -76,16 +64,14 @@ MMComponents.MMDrawer { when: root.positionProvider && root.positionProvider.state === PositionProvider.WaitingToReconnect PropertyChanges { 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." ) - message.description: root.positionProvider.stateMessage + "

" + qsTr( "You can close this message, we will try to repeatedly connect to your device." ) + message.title: root.getWaitingToReconnectTitle() + message.description: qsTr( "%1%2You can close this message, we will try to repeatedly connect to your device." ).arg( root.positionProvider.stateMessage ).arg( "

" ) message.linkText: qsTr( "Learn more" ) } } ] - state: "working" + state: "connecting" } drawerBottomMargin: __style.margin40 @@ -116,7 +102,7 @@ MMComponents.MMDrawer { interval: 400 repeat: true - running: rootstate.state === "working" + running: rootstate.state === "connecting" onTriggered: { if ( root.connectingSuffixAnimation.length > 2 ) { @@ -127,4 +113,78 @@ MMComponents.MMDrawer { } } } + + function getExternalProviderImage() { + switch ( root.providerType ) { + case "bluetooth": + return __style.externalBluetoothGreenImage + case "network": + return __style.externalNetworkGreenImage + case "trimble": + return __style.externalNetworkGreenImage + default: + return "" + } + } + + function getConnectingTitle() { + if ( root.providerType === "network" ) { + return qsTr( "Connecting to external receiver" ) + } + else { + if ( root.positionProvider.name() ) { + return qsTr( "Connecting to %1" ).arg( root.positionProvider.name() ) + } + else { + return qsTr( "Connecting%1" ).arg( root.connectingSuffixAnimation ) + } + } + } + + function getConnectingDescription() { + if ( root.providerType === "bluetooth" ) { + return qsTr( "You might be asked to pair your device during this process." ) + } + else { + if ( root.positionProvider.id() && root.providerType !== "trimble" ) { + return qsTr( "Connecting to %1. You can close this panel, the app will continue in the background." ).arg( root.positionProvider.id() ) + } + else { + return qsTr( "Connecting%1" ).arg( root.connectingSuffixAnimation ) + } + } + } + + function getFailTitle() { + if ( root.providerType === "trimble" || !root.positionProvider ) { + return qsTr( "Failed to connect" ) + } + else { + let providerName + if ( root.providerType === "network" ) { + providerName = root.positionProvider.id() + } + else { + providerName = root.positionProvider.name() + } + + return qsTr( "Failed to connect to %1" ).arg( providerName ) + } + } + + function getFailDescription() { + if ( root.providerType === "bluetooth" ) { + return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) + } + else if ( root.providerType === "network" ) { + return qsTr( "We were not able to connect to the specified IP address." ) + } + else if ( root.providerType === "trimble" ) { + return qsTr( "We were not able to connect to Trimble Mobile Manager." ) + } + } + + function getWaitingToReconnectTitle() { + return getFailDescription() + } } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 3f1fc74e3..52732a707 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -150,8 +150,6 @@ MMComponents.MMPage { else if ( providerType === "network" ) networkProviderDrawer.open() else if ( providerType === "trimble" ) { root.activateProvider( "external_trimble", "trimble_tmm", qsTr( "Trimble Mobile Manager" ) ) - connectingDialogLoader.providerType = "trimble" - connectingDialogLoader.active = true } } } @@ -240,7 +238,7 @@ MMComponents.MMPage { asynchronous: true sourceComponent: Component { MMExternalProviderConnectionDrawer{} } - onLoaded: { + onLoaded: () => { item.providerType = connectingDialogLoader.providerType item.open() } @@ -288,5 +286,8 @@ MMComponents.MMPage { else if ( type === "external_ip" ) { connectingDialogLoader.open( "network" ) } + else if ( type === "external_trimble" ) { + connectingDialogLoader.open( "trimble" ) + } } } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index b826eaf0d..d4464ab7a 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -21,12 +21,16 @@ MMComponents.MMListDrawer { drawerHeader.title: qsTr( "Connect new receiver" ) drawerHeader.titleFont: __style.t2 - onOpened: root.list.currentIndex = -1 + onOpened: list.currentIndex = -1 + + Component.onCompleted: () => { + root.list.currentIndex = -1 + } list.model: ListModel { id: providerTypeModel - Component.onCompleted: { + Component.onCompleted: () => { if ( PositionKit.hasBluetoothSupport ) { providerTypeModel.append( { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon } ) } @@ -82,7 +86,7 @@ MMComponents.MMListDrawer { description: parent.description checked: parent.ListView.isCurrentItem - onClicked: { + onClicked: () => { if ( parent.ListView.isCurrentItem ) { parent.ListView.view.currentIndex = -1 } @@ -99,19 +103,11 @@ MMComponents.MMListDrawer { text: qsTr( "Continue" ) enabled: root.list.currentIndex !== -1 - onClicked: { + onClicked: () => { const providerType = providerTypeModel.get( root.list.currentIndex ).type root.close() - if ( providerType === "bluetooth" ) { - root.providerSelected("bluetooth") - } - else if ( providerType === "network" ) { - root.providerSelected("network") - } - else if ( providerType === "trimble" ) { - root.providerSelected("trimble") - } + root.providerSelected( providerType ) } } } From d4596fda0535aa2c23e3ec20892fafbde9db97f9 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 31 Jul 2026 12:55:22 +0200 Subject: [PATCH 10/21] Refactor registration flow --- app/position/providers/trimbleregistration.h | 11 ++- .../providers/trimbleregistrationandroid.cpp | 62 +++++--------- .../providers/trimbleregistrationios.mm | 83 +++++++++---------- .../MMExternalProviderConnectionDrawer.qml | 2 +- cmake_templates/iOSInfo.plist.in | 4 +- 5 files changed, 68 insertions(+), 94 deletions(-) diff --git a/app/position/providers/trimbleregistration.h b/app/position/providers/trimbleregistration.h index 013ba1660..ebb88055b 100644 --- a/app/position/providers/trimbleregistration.h +++ b/app/position/providers/trimbleregistration.h @@ -12,7 +12,6 @@ #include #include -#include #include #ifdef ANDROID @@ -25,6 +24,8 @@ class TmmResultReceiver; // defined in trimbleregistrationandroid.cpp * Call requestRegistration() once; listen for registered() or failed(). * Concrete implementations are in trimbleregistrationandroid.cpp (Android) * and trimbleregistrationios.mm (iOS). + * + * TODO: move to native utils when refactoring native utils */ class TrimbleRegistration : public QObject { @@ -32,12 +33,14 @@ class TrimbleRegistration : public QObject public: explicit TrimbleRegistration( QObject *parent = nullptr ); - ~TrimbleRegistration() override; + ~TrimbleRegistration() override = default; void requestRegistration( const QString &appId ); - // iOS only: called by QDesktopServices URL handler when TMM calls back - Q_INVOKABLE void handleCallback( const QUrl &url ); +#ifdef Q_OS_IOS + slots: + void handleCallback( const QUrl &url ); +#endif signals: void registered( int locationV2Port ); diff --git a/app/position/providers/trimbleregistrationandroid.cpp b/app/position/providers/trimbleregistrationandroid.cpp index 45ad63694..7534ac434 100644 --- a/app/position/providers/trimbleregistrationandroid.cpp +++ b/app/position/providers/trimbleregistrationandroid.cpp @@ -11,28 +11,19 @@ #ifdef ANDROID -#include -#include #include -static constexpr int TMM_REGISTER_REQUEST_CODE = 0x544D4D52; // "TMMR" +#include "coreutils.h" -static constexpr char TMM_ACTION_REGISTER[] = "com.trimble.tmm.REGISTER"; +static constexpr int TMM_REGISTER_REQUEST_CODE = 0x544D4D52; // "TMMR" - custom value to pair intent result +static QString temp = QStringLiteral( "hello" ); -static constexpr char TMM_EXTRA_APP_ID[] = "applicationID"; -static constexpr char TMM_EXTRA_RESULT[] = "registrationResult"; -static constexpr char TMM_EXTRA_LOCATION_V2_PORT[] = "locationV2Port"; - -static constexpr char TMM_RESULT_OK[] = "OK"; - -// Concrete subclass of the abstract QAndroidActivityResultReceiver. -// Forwards the result to TrimbleRegistration via a stored pointer. class TmmResultReceiver : public QAndroidActivityResultReceiver { public: explicit TmmResultReceiver( TrimbleRegistration *reg ) : mReg( reg ) {} - void handleActivityResult( int receiverRequestCode, int resultCode, const QJniObject &data ) override + void handleActivityResult( const int receiverRequestCode, const int resultCode, const QJniObject &data ) override { Q_UNUSED( resultCode ) if ( receiverRequestCode != TMM_REGISTER_REQUEST_CODE || !mReg ) @@ -41,33 +32,31 @@ class TmmResultReceiver : public QAndroidActivityResultReceiver if ( !data.isValid() ) { emit mReg->failed( TrimbleRegistration::tr( "No response from Trimble Mobile Manager" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); return; } - QJniObject resultKey = QJniObject::fromString( QString::fromLatin1( TMM_EXTRA_RESULT ) ); - QJniObject resultObj = data.callObjectMethod( "getStringExtra", - "(Ljava/lang/String;)Ljava/lang/String;", - resultKey.object() ); - const QString result = resultObj.isValid() ? resultObj.toString() : QString(); + const QAndroidIntent intent( data ); + // we can't use intent.extraVariant() function here as it throws errors in Qt code + const QString registerResult = intent.handle().callObjectMethod( "getStringExtra", QJniObject::fromString( QStringLiteral( "registrationResult" ) ).object() ).toString(); - if ( result != QLatin1String( TMM_RESULT_OK ) ) + if ( registerResult != QStringLiteral( "OK" ) ) { - emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager registration failed: %1" ).arg( result ) ); + emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager registration failed: %1" ).arg( registerResult ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, registration has been refused by Trimble Mobile Manager. Reason: %1." ).arg( registerResult ) ); return; } + // we can't use intent.extraVariant() function here as it doesn't parse the data correctly + const int locationPortResult = intent.handle().callMethod( "getIntExtra", QJniObject::fromString( QStringLiteral( "locationV2Port" ) ).object(), 0 ); - QJniObject portKey = QJniObject::fromString( QString::fromLatin1( TMM_EXTRA_LOCATION_V2_PORT ) ); - const int port = data.callMethod( "getIntExtra", - "(Ljava/lang/String;I)I", - portKey.object(), - static_cast( 0 ) ); - if ( port <= 0 ) + if ( !locationPortResult ) { emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager returned invalid port" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager returned malformed location data port." ) ); return; } - emit mReg->registered( port ); + emit mReg->registered( locationPortResult ); } private: @@ -80,26 +69,13 @@ TrimbleRegistration::TrimbleRegistration( QObject *parent ) { } -TrimbleRegistration::~TrimbleRegistration() = default; - void TrimbleRegistration::requestRegistration( const QString &appId ) { - QJniObject intentAction = QJniObject::fromString( QString::fromLatin1( TMM_ACTION_REGISTER ) ); - QJniObject intent( "android/content/Intent", "(Ljava/lang/String;)V", intentAction.object() ); - QJniObject appIdStr = QJniObject::fromString( appId ); - QJniObject extraKey = QJniObject::fromString( QString::fromLatin1( TMM_EXTRA_APP_ID ) ); - intent.callObjectMethod( "putExtra", - "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", - extraKey.object(), - appIdStr.object() ); + const QAndroidIntent intent( QStringLiteral( "com.trimble.tmm.REGISTER" ) ); + // we can't use intent.extraVariant() function here as it writes the string as byte array instead of strings + intent.handle().callObjectMethod( "putExtra", QJniObject::fromString( QStringLiteral( "applicationID" ) ).object(), QJniObject::fromString( appId ).object() ); QtAndroidPrivate::startActivity( intent, TMM_REGISTER_REQUEST_CODE, mResultReceiver.get() ); } -void TrimbleRegistration::handleCallback( const QUrl &url ) -{ - Q_UNUSED( url ) - // Android registration is intent-based; this callback is iOS-only -} - #endif // ANDROID diff --git a/app/position/providers/trimbleregistrationios.mm b/app/position/providers/trimbleregistrationios.mm index 504d61b05..1455203a1 100644 --- a/app/position/providers/trimbleregistrationios.mm +++ b/app/position/providers/trimbleregistrationios.mm @@ -21,102 +21,97 @@ #import -static constexpr char TMM_CALLBACK_SCHEME[] = "merginmaps-tmm"; -static constexpr char TMM_REGISTER_SCHEME[] = "tmmregister"; +static QString MM_CALLBACK_SCHEME = QStringLiteral( "merginmaps" ); +static QString MM_CALLBACK_URL = QStringLiteral( "tmm-registration" ); TrimbleRegistration::TrimbleRegistration( QObject *parent ) : QObject( parent ) { } -TrimbleRegistration::~TrimbleRegistration() = default; - void TrimbleRegistration::requestRegistration( const QString &appId ) { - QDesktopServices::setUrlHandler( QString::fromLatin1( TMM_CALLBACK_SCHEME ), this, "handleCallback" ); + QDesktopServices::setUrlHandler( MM_CALLBACK_SCHEME, this, "handleCallback" ); QJsonObject payload; payload[QStringLiteral( "application_id" )] = appId; - payload[QStringLiteral( "returl" )] = QStringLiteral( "%1://registration" ).arg( QLatin1String( TMM_CALLBACK_SCHEME ) ); + payload[QStringLiteral( "returl" )] = QStringLiteral( "%1://%2" ).arg( MM_CALLBACK_SCHEME, MM_CALLBACK_URL ); const QByteArray jsonBytes = QJsonDocument( payload ).toJson( QJsonDocument::Compact ); const QString base64 = QString::fromLatin1( jsonBytes.toBase64() ); - // Build the raw URL string and open via UIKit directly. // QDesktopServices::openUrl() routes through QUrl which percent-encodes or rejects - // the base64 payload ('+', '/', '=' are special in URLs), breaking the scheme. - const QString rawUrlString = QStringLiteral( "%1://?%2" ) - .arg( QLatin1String( TMM_REGISTER_SCHEME ), base64 ); - NSString *nsUrlString = rawUrlString.toNSString(); - NSURL *nsUrl = [NSURL URLWithString:nsUrlString]; - - qDebug() << "TrimbleRegistration: opening URL:" << rawUrlString; - qDebug() << "TrimbleRegistration: NSURL valid:" << ( nsUrl != nil ); + // the base64 payload, breaking the scheme. + NSString *nsUrlString = QStringLiteral( "%1://?%2" ).arg( QStringLiteral( "tmmregister" ), base64 ).toNSString(); + NSURL *trimbleUrl = [NSURL URLWithString:nsUrlString]; - if ( !nsUrl ) + if ( !trimbleUrl ) { - emit failed( tr( "Could not open Trimble Mobile Manager. Is it installed?" ) ); + emit failed( tr( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, trimble URL is malformed." ) ); return; } QPointer self( this ); - [[UIApplication sharedApplication] openURL:nsUrl options:@ {} completionHandler: ^ ( BOOL success ) + [[UIApplication sharedApplication] openURL:trimbleUrl options:@ {} completionHandler: ^ ( BOOL success ) { - qDebug() << "TrimbleRegistration: openURL completionHandler success:" << success; if ( !success && self ) { - emit self->failed( tr( "Could not open Trimble Mobile Manager. Is it installed?" ) ); + emit self->failed( tr( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, failed to open Trimble Mobile Manager, probably it's missing." ) ); } }]; } -Q_INVOKABLE void TrimbleRegistration::handleCallback( const QUrl &url ) +void TrimbleRegistration::handleCallback( const QUrl &url ) { - qDebug() << "TrimbleRegistration: handleCallback called with URL:" << url.toString(); - - if ( url.scheme() != QLatin1String( TMM_CALLBACK_SCHEME ) ) + if ( url.scheme() != MM_CALLBACK_SCHEME ) { - qDebug() << "TrimbleRegistration: unexpected scheme, ignoring:" << url.scheme(); + emit self->failed( tr( "Registration failed, wrong response from Trimble Mobile Manager." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager responded with wrong scheme." ) ); return; } - const QString fragment = url.fragment(); - const QString query = url.query(); - const QString base64 = fragment.isEmpty() ? query : fragment; - qDebug() << "TrimbleRegistration: raw callback payload:" << base64; + if ( url.host() != MM_CALLBACK_URL ) + { + emit self->failed( tr( "Registration failed, wrong response from Trimble Mobile Manager." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager responded to wrong url." ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + return; + } - const QByteArray jsonBytes = QByteArray::fromBase64( base64.toLatin1() ); + const QByteArray jsonBytes = QByteArray::fromBase64( url.query().toLatin1() ); const QJsonDocument doc = QJsonDocument::fromJson( jsonBytes ); if ( doc.isNull() || !doc.isObject() ) { - qDebug() << "TrimbleRegistration: failed to parse callback JSON:" << jsonBytes; - emit failed( tr( "Invalid response from Trimble Mobile Manager" ) ); + emit failed( tr( "Registration failed, wrong response from Trimble Mobile Manager." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager responded with malformed JSON." ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); return; } const QJsonObject obj = doc.object(); - const QString result = obj.value( QStringLiteral( "registrationResult" ) ).toString(); - const QJsonValue dbgPort = obj.value( QStringLiteral( "locationV2Port" ) ); - qDebug() << "TrimbleRegistration: registrationResult:" << result - << "locationV2Port:" << ( dbgPort.isString() ? dbgPort.toString() : QString::number( dbgPort.toInt( 0 ) ) ); + const QString registerResult = obj.value( QStringLiteral( "registrationResult" ) ).toString(); - if ( result != QLatin1String( "OK" ) ) + if ( registerResult != QLatin1String( "OK" ) ) { - emit failed( tr( "Trimble Mobile Manager registration failed: %1" ).arg( result ) ); + emit failed( tr( "Trimble Mobile Manager registration failed: %1" ).arg( registerResult ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, registration has been refused by Trimble Mobile Manager. Reason: %1." ).arg( registerResult ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); return; } - // TMM returns port values as JSON strings, not numbers — use toString().toInt() - const QJsonValue portVal = obj.value( QStringLiteral( "locationV2Port" ) ); - const int port = portVal.isString() ? portVal.toString().toInt() : portVal.toInt( 0 ); - if ( port <= 0 ) + const int locationPort = obj.value( QStringLiteral( "locationV2Port" ) ).toInt( -1 ); + if ( locationPort <= 0 ) { emit failed( tr( "Trimble Mobile Manager returned invalid port" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager returned malformed location data port." ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); return; } - QDesktopServices::unsetUrlHandler( QString::fromLatin1( TMM_CALLBACK_SCHEME ) ); - emit registered( port ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + emit registered( locationPort ); } #endif // Q_OS_IOS diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index 4044d445a..93153868a 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -180,7 +180,7 @@ MMComponents.MMDrawer { return qsTr( "We were not able to connect to the specified IP address." ) } else if ( root.providerType === "trimble" ) { - return qsTr( "We were not able to connect to Trimble Mobile Manager." ) + return qsTr( "We were not able to connect to Trimble Mobile Manager. Please make sure it's installed." ) } } diff --git a/cmake_templates/iOSInfo.plist.in b/cmake_templates/iOSInfo.plist.in index 6e0a2ab14..9edbae0e0 100644 --- a/cmake_templates/iOSInfo.plist.in +++ b/cmake_templates/iOSInfo.plist.in @@ -30,10 +30,10 @@ CFBundleURLName - com.lutraconsulting.merginmaps.trimble + uk.co.lutraconsulting.merginmaps CFBundleURLSchemes - merginmaps-tmm + merginmaps From 8edfd98ced192fcb0cf402c54b8e7816140ab215 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 31 Jul 2026 21:19:45 +0200 Subject: [PATCH 11/21] Refactor ReconnectDelay to abstractPositionProvider --- app/position/providers/abstractpositionprovider.h | 8 ++++++++ app/position/providers/bluetoothpositionprovider.h | 7 ------- app/position/providers/networkpositionprovider.h | 8 -------- app/position/providers/trimblepositionprovider.h | 7 ------- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/app/position/providers/abstractpositionprovider.h b/app/position/providers/abstractpositionprovider.h index db32ce33b..ad7210c5d 100644 --- a/app/position/providers/abstractpositionprovider.h +++ b/app/position/providers/abstractpositionprovider.h @@ -71,6 +71,14 @@ class AbstractPositionProvider : public QObject void setState( const QString &message ); // keeps state enum the same and only changes the message void setState( const QString &message, State state ); + // signalizes in how many [ms] we will try to reconnect to GPS again + enum ReconnectDelay + { + ShortDelay = 3000, // 3 secs + LongDelay = 5000, // 5 secs + ExtraLongDelay = 10000 // 10 secs + }; + // ProviderId - unique id of this provider. // For external receiver it holds mac address of a bluetooth device. // Internal providers (internal gps and simulated provider) has constant values of "devicegps" and "simulated" diff --git a/app/position/providers/bluetoothpositionprovider.h b/app/position/providers/bluetoothpositionprovider.h index 8797c8ec1..87db1b407 100644 --- a/app/position/providers/bluetoothpositionprovider.h +++ b/app/position/providers/bluetoothpositionprovider.h @@ -26,13 +26,6 @@ class BluetoothPositionProvider : public AbstractPositionProvider { Q_OBJECT - // signalizes in how many [ms] we will try to reconnect to GPS again - enum ReconnectDelay - { - ShortDelay = 3000, - LongDelay = 5000 - }; - public: BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); ~BluetoothPositionProvider() override; diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index cbbd1b0a5..7c5b6384d 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -23,14 +23,6 @@ class NetworkPositionProvider : public AbstractPositionProvider { Q_OBJECT - // signalizes in how many [ms] we will try to reconnect to GPS again - enum ReconnectDelay - { - ShortDelay = 3000, // 3 secs - LongDelay = 5000, // 5 secs - ExtraLongDelay = 10000 // 10 secs - }; - public: NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); ~NetworkPositionProvider() override; diff --git a/app/position/providers/trimblepositionprovider.h b/app/position/providers/trimblepositionprovider.h index 33c365a64..ffdffff99 100644 --- a/app/position/providers/trimblepositionprovider.h +++ b/app/position/providers/trimblepositionprovider.h @@ -20,13 +20,6 @@ class TrimblePositionProvider : public AbstractPositionProvider { Q_OBJECT - enum ReconnectDelay - { - ShortDelay = 3000, - LongDelay = 5000, - ExtraLongDelay = 10000 - }; - public: TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); ~TrimblePositionProvider() override; From 2f2d03b9ec4918128cc4af1b956778234cb222c7 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 4 Aug 2026 00:47:18 +0200 Subject: [PATCH 12/21] Refactor Trimble position provider Improve position data parsing, antenna height handling. --- app/maptools/recordingmaptool.cpp | 5 +- app/position/geoposition.cpp | 20 +- app/position/geoposition.h | 7 - app/position/positionkit.cpp | 48 ++-- app/position/positionkit.h | 8 +- app/position/positiontransformer.cpp | 5 + app/position/positiontransformer.h | 8 + .../providers/trimblepositionprovider.cpp | 242 ++++++++++-------- .../providers/trimblepositionprovider.h | 13 +- app/qml/settings/MMSettingsPage.qml | 21 +- 10 files changed, 193 insertions(+), 184 deletions(-) diff --git a/app/maptools/recordingmaptool.cpp b/app/maptools/recordingmaptool.cpp index 44ff96a13..a0ac08fa7 100644 --- a/app/maptools/recordingmaptool.cpp +++ b/app/maptools/recordingmaptool.cpp @@ -79,10 +79,9 @@ void RecordingMapTool::addPoint( const QgsPoint &point ) fixZM( pointToAdd ); - // apply gps antenna height (zero when provider already reduced to ground point, e.g. Trimble via TMM) - if ( QgsWkbTypes::hasZ( pointToAdd.wkbType() ) && mPositionKit && mPositionKit->antennaHeightToApply() > 0 ) + if ( QgsWkbTypes::hasZ( pointToAdd.wkbType() ) && mPositionKit && mPositionKit->requireAntennaHeightTransform() ) { - pointToAdd.setZ( pointToAdd.z() - mPositionKit->antennaHeightToApply() ); + pointToAdd.setZ( pointToAdd.z() - mPositionKit->antennaHeight() ); } QgsVertexId id( mActivePart, mActiveRing, 0 ); diff --git a/app/position/geoposition.cpp b/app/position/geoposition.cpp index c955cb669..f96518626 100644 --- a/app/position/geoposition.cpp +++ b/app/position/geoposition.cpp @@ -131,32 +131,30 @@ QString GeoPosition::parseFixStatus() const // 9 = WAAS fix (not NMEA standard, but NovAtel receivers report this instead of a 2). // - switch ( quality ) + switch ( qualityIndicator ) { - case -1: + case Qgis::GpsQualityIndicator::Unknown: return QObject::tr( "No data" ); - case 0: + case Qgis::GpsQualityIndicator::Invalid: return QObject::tr( "No fix" ); - case 1: + case Qgis::GpsQualityIndicator::GPS: return QObject::tr( "GPS fix, no correction data" ); - case 2: - // fall through - case 9: + case Qgis::GpsQualityIndicator::DGPS: return QObject::tr( "DGPS fix" ); - case 3: + case Qgis::GpsQualityIndicator::PPS: return QObject::tr( "PPS fix" ); - case 4: + case Qgis::GpsQualityIndicator::RTK: return QObject::tr( "RTK fix" ); - case 5: + case Qgis::GpsQualityIndicator::FloatRTK: return QObject::tr( "RTK float" ); - case 6: + case Qgis::GpsQualityIndicator::Estimated: return QObject::tr( "Estimated fix (dead reckoning)" ); default: diff --git a/app/position/geoposition.h b/app/position/geoposition.h index fa4e9e292..5dd413c8b 100644 --- a/app/position/geoposition.h +++ b/app/position/geoposition.h @@ -31,13 +31,6 @@ class GeoPosition : public QgsGpsInformation bool isMock = false; - // Set by providers that already account for antenna height (e.g. Trimble via TMM). - // antennaHeight >= 0 means the provider reported it; -1 = not provided. - // antennaHeightApplied = true means the provider already reduced to the ground point - // and the app must NOT subtract it again. - double antennaHeight = -1; - bool antennaHeightApplied = false; - // copies all data from QgsGpsInformation other and updates satellitesVisible static GeoPosition fromQgsGpsInformation( const QgsGpsInformation &other ); diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index c63436e99..46f2845f7 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -309,9 +309,9 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) hasAnythingChanged = true; } - if ( !qgsDoubleNear( newPosition.elevation - ( newPosition.antennaHeightApplied ? 0 : antennaHeight() ), mPosition.elevation ) ) + if ( !qgsDoubleNear( newPosition.elevation - ( requireAntennaHeightTransform() ? 0 : antennaHeight() ), mPosition.elevation ) ) { - mPosition.elevation = newPosition.elevation - ( newPosition.antennaHeightApplied ? 0 : antennaHeight() ); + mPosition.elevation = newPosition.elevation - ( requireAntennaHeightTransform() ? 0 : antennaHeight() ); emit altitudeChanged( mPosition.elevation ); hasAnythingChanged = true; } @@ -414,9 +414,9 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) hasAnythingChanged = true; } - if ( newPosition.quality != mPosition.quality ) + if ( newPosition.qualityIndicator != mPosition.qualityIndicator ) { - mPosition.quality = newPosition.quality; + mPosition.qualityIndicator = newPosition.qualityIndicator; hasAnythingChanged = true; } @@ -436,16 +436,6 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) emit positionProviderNameChanged(); } - const bool antennaDataChanged = !qgsDoubleNear( newPosition.antennaHeight, mPosition.antennaHeight ) - || newPosition.antennaHeightApplied != mPosition.antennaHeightApplied; - if ( antennaDataChanged ) - { - mPosition.antennaHeight = newPosition.antennaHeight; - mPosition.antennaHeightApplied = newPosition.antennaHeightApplied; - emit antennaHeightChanged(); - hasAnythingChanged = true; - } - if ( hasAnythingChanged ) { emit positionChanged( mPosition ); @@ -660,21 +650,15 @@ void PositionKit::setAppSettings( AppSettings *appSettings ) double PositionKit::antennaHeight() const { - // Display value: provider-streamed height takes precedence; never overwrites AppSettings. - if ( mPosition.antennaHeight >= 0 ) - return mPosition.antennaHeight; - - if ( mAppSettings ) - return mAppSettings->gpsAntennaHeight(); - - return 0; -} - -double PositionKit::antennaHeightToApply() const -{ - // If the provider already subtracted antenna height, the app must apply zero. - if ( mPosition.antennaHeightApplied ) + // trimble provider has antenna height defined in TMM, so we "block" application setup + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { +#if WITH_TRIMBLE_PROVIDERS + return dynamic_cast( mPositionProvider.get() )->antennaHeight(); +#else return 0; +#endif + } if ( mAppSettings ) return mAppSettings->gpsAntennaHeight(); @@ -682,7 +666,11 @@ double PositionKit::antennaHeightToApply() const return 0; } -bool PositionKit::antennaHeightApplied() const +bool PositionKit::requireAntennaHeightTransform() const { - return mPosition.antennaHeightApplied; + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + return false; + } + return true; } diff --git a/app/position/positionkit.h b/app/position/positionkit.h index ee4178310..5fffaa112 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -84,7 +84,6 @@ class PositionKit : public QObject Q_PROPERTY( AppSettings *appSettings READ appSettings WRITE setAppSettings NOTIFY appSettingsChanged ) Q_PROPERTY( double antennaHeight READ antennaHeight NOTIFY antennaHeightChanged ) - Q_PROPERTY( bool antennaHeightApplied READ antennaHeightApplied NOTIFY antennaHeightChanged ) public: //! Creates new position kit @@ -148,8 +147,11 @@ class PositionKit : public QObject void setAppSettings( AppSettings *appSettings ); double antennaHeight() const; - double antennaHeightToApply() const; - bool antennaHeightApplied() const; + /* + * Trimble provider subtracts antenna height before providing the elevation for MM. For every other provider + * subtract antenna height in MM. + */ + Q_INVOKABLE bool requireAntennaHeightTransform() const; void setVerticalCrs( const QgsCoordinateReferenceSystem &verticalCrs ); void setElevationTransformationEnabled( bool elevationTransformationEnabled ); diff --git a/app/position/positiontransformer.cpp b/app/position/positiontransformer.cpp index 1b108d996..d44d07be5 100644 --- a/app/position/positiontransformer.cpp +++ b/app/position/positiontransformer.cpp @@ -55,6 +55,11 @@ GeoPosition PositionTransformer::processNetworkPosition( const GeoPosition &geoP return processBluetoothPosition( geoPosition ); } +GeoPosition PositionTransformer::processTrimblePosition( 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 1608dba28..4b7f3fcae 100644 --- a/app/position/positiontransformer.h +++ b/app/position/positiontransformer.h @@ -57,6 +57,14 @@ class PositionTransformer : QObject */ GeoPosition processNetworkPosition( const 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 TrimblePositionProvider to mitigate unnecessary transformations + * \return Copy of passed geoPosition with processed elevation and elevation separation. + */ + GeoPosition processTrimblePosition( 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/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp index d3f38aae7..9270ccc53 100644 --- a/app/position/providers/trimblepositionprovider.cpp +++ b/app/position/providers/trimblepositionprovider.cpp @@ -23,13 +23,80 @@ #include #endif +#include "coreutils.h" // trimblesecrets.cpp is generated by decrypting trimblesecrets.cpp.enc. // It defines static QString __getTrimbleAppId(). #include "trimblesecrets.cpp" constexpr int ONE_SECOND_MS = 1000; constexpr int TMM_WS_V2_DEFAULT_PORT = 9639; +const QString WEBSOCKET_LOCALHOST_ADDRESS = QStringLiteral( "ws://localhost" ); +TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( id, QStringLiteral( "external_trimble" ), name, positionTransformer, parent ) + , mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) +{ + mRegistration = new TrimbleRegistration( this ); + connect( mRegistration, &TrimbleRegistration::registered, this, &TrimblePositionProvider::onRegistered ); + connect( mRegistration, &TrimbleRegistration::failed, this, &TrimblePositionProvider::onRegistrationFailed ); + + mReconnectTimer.setSingleShot( false ); + mReconnectTimer.setInterval( ONE_SECOND_MS ); + connect( &mReconnectTimer, &QTimer::timeout, this, &TrimblePositionProvider::onReconnectTimeout ); + + mHeartBeatTimer.setSingleShot( true ); + connect( &mHeartBeatTimer, &QTimer::timeout, this, [this] + { + setState( tr( "No data" ), State::NoConnection ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); + } ); + + TrimblePositionProvider::startUpdates(); +} + +TrimblePositionProvider::~TrimblePositionProvider() +{ + TrimblePositionProvider::closeProvider(); +} + +void TrimblePositionProvider::startUpdates() +{ + if ( mRegistrationInProgress ) + { + return; + } + + if ( mCachedPort > 0 ) + { + connectWebSocket( mCachedPort ); + return; + } + + mRegistrationInProgress = true; + setState( tr( "Connecting" ), State::Connecting ); + mRegistration->requestRegistration( __getTrimbleAppId() ); +} + + +void TrimblePositionProvider::stopUpdates() +{ + mHeartBeatTimer.stop(); + mReconnectTimer.stop(); + if ( mSocket && mSocket->state() == QAbstractSocket::ConnectedState ) + mSocket->close(); +} + +void TrimblePositionProvider::closeProvider() +{ + mHeartBeatTimer.stop(); + mReconnectTimer.stop(); + if ( mSocket ) + { + mSocket->disconnect(); + mSocket->abort(); + } +} // --------------------------------------------------------------------------- // Frame resolver // --------------------------------------------------------------------------- @@ -86,10 +153,6 @@ QgsCoordinateReferenceSystem TrimblePositionProvider::resolveFrame( const QStrin return QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ); } -// --------------------------------------------------------------------------- -// Message parser -// --------------------------------------------------------------------------- - GeoPosition TrimblePositionProvider::parseLocationMessage( const QString &json ) { GeoPosition pos; @@ -106,129 +169,95 @@ GeoPosition TrimblePositionProvider::parseLocationMessage( const QString &json ) if ( obj.contains( QStringLiteral( "longitude" ) ) && !obj.value( QStringLiteral( "longitude" ) ).isNull() ) pos.longitude = obj.value( QStringLiteral( "longitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); - if ( obj.contains( QStringLiteral( "altitude" ) ) && !obj.value( QStringLiteral( "altitude" ) ).isNull() ) + if ( obj.contains( QStringLiteral( "undulation" ) ) && !obj.value( QStringLiteral( "undulation" ) ).isNull() ) + { + pos.elevation_diff = obj.value( QStringLiteral( "undulation" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + // prefer msl height if available + if ( obj.contains( QStringLiteral( "mslHeight" ) ) && !obj.value( QStringLiteral( "mslHeight" ) ).isNull() ) + { + pos.elevation = obj.value( QStringLiteral( "mslHeight" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + } + } + else if ( obj.contains( QStringLiteral( "altitude" ) ) && !obj.value( QStringLiteral( "altitude" ) ).isNull() ) + { pos.elevation = obj.value( QStringLiteral( "altitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + } + + if ( obj.contains( QStringLiteral( "speed" ) ) && !obj.value( QStringLiteral( "speed" ) ).isNull() ) + { + pos.speed = obj.value( QStringLiteral( "speed" ) ).toDouble( -1 ); + // trimble reports speed in m/s, and speed in QgsGpsInformation is in km/h + if ( pos.speed >= 0 ) pos.speed = pos.speed * 3.6; + } + + if ( obj.contains( QStringLiteral( "bearing" ) ) && !obj.value( QStringLiteral( "bearing" ) ).isNull() ) + pos.direction = obj.value( QStringLiteral( "bearing" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "pdop" ) ) && !obj.value( QStringLiteral( "pdop" ) ).isNull() ) + pos.pdop = obj.value( QStringLiteral( "pdop" ) ).toDouble( -1 ); if ( obj.contains( QStringLiteral( "hdop" ) ) && !obj.value( QStringLiteral( "hdop" ) ).isNull() ) pos.hdop = obj.value( QStringLiteral( "hdop" ) ).toDouble( -1 ); + if ( obj.contains( QStringLiteral( "vdop" ) ) && !obj.value( QStringLiteral( "vdop" ) ).isNull() ) + pos.vdop = obj.value( QStringLiteral( "vdop" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "hrms" ) ) && !obj.value( QStringLiteral( "hrms" ) ).isNull() ) + pos.hacc = obj.value( QStringLiteral( "hrms" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "vrms" ) ) && !obj.value( QStringLiteral( "vrms" ) ).isNull() ) + pos.vacc = obj.value( QStringLiteral( "vrms" ) ).toDouble( -1 ); + if ( obj.contains( QStringLiteral( "totalSatInUse" ) ) && !obj.value( QStringLiteral( "totalSatInUse" ) ).isNull() ) pos.satellitesUsed = obj.value( QStringLiteral( "totalSatInUse" ) ).toInt( -1 ); if ( obj.contains( QStringLiteral( "satellites" ) ) && !obj.value( QStringLiteral( "satellites" ) ).isNull() ) pos.satellitesVisible = obj.value( QStringLiteral( "satellites" ) ).toInt( -1 ); - // diffStatus: 1=Autonomous, 2=DGPS, 4=Fixed, 5=Float - const int diffStatus = obj.value( QStringLiteral( "diffStatus" ) ).toInt( 1 ); + if ( obj.contains( QStringLiteral( "utcTimeStamp" ) ) && !obj.value( QStringLiteral( "utcTimeStamp" ) ).isNull() ) + pos.utcDateTime = obj.value( QStringLiteral( "utcTimeStamp" ) ).toVariant().toDateTime(); + + const int diffStatus = obj.value( QStringLiteral( "diffStatus" ) ).toInt( -1 ); switch ( diffStatus ) { + case 1: + pos.qualityIndicator = Qgis::GpsQualityIndicator::GPS; + break; + case 2: + pos.qualityIndicator = Qgis::GpsQualityIndicator::DGPS; + break; case 4: pos.qualityIndicator = Qgis::GpsQualityIndicator::RTK; break; case 5: pos.qualityIndicator = Qgis::GpsQualityIndicator::FloatRTK; break; - case 2: - pos.qualityIndicator = Qgis::GpsQualityIndicator::DGPS; - break; default: - pos.qualityIndicator = Qgis::GpsQualityIndicator::GPS; + pos.qualityIndicator = Qgis::GpsQualityIndicator::Unknown; break; } + pos.fixStatusString = pos.parseFixStatus(); - // TMM has already reduced its output to the ground point. - // antennaHeight here is the value TMM applied (metadata only); the app must NOT subtract it again. if ( obj.contains( QStringLiteral( "antennaHeight" ) ) && !obj.value( QStringLiteral( "antennaHeight" ) ).isNull() ) { - pos.antennaHeight = obj.value( QStringLiteral( "antennaHeight" ) ).toDouble( -1 ); - pos.antennaHeightApplied = true; + mAntennaHeight = obj.value( QStringLiteral( "antennaHeight" ) ).toDouble( std::numeric_limits::quiet_NaN() ); } return pos; } -// --------------------------------------------------------------------------- -// Constructor / destructor -// --------------------------------------------------------------------------- - -TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) - : AbstractPositionProvider( id, QStringLiteral( "external_trimble" ), name, positionTransformer, parent ) - , mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) -{ - mRegistration = new TrimbleRegistration( this ); - connect( mRegistration, &TrimbleRegistration::registered, this, &TrimblePositionProvider::onRegistered ); - connect( mRegistration, &TrimbleRegistration::failed, this, &TrimblePositionProvider::onRegistrationFailed ); - - mReconnectTimer.setSingleShot( false ); - mReconnectTimer.setInterval( ONE_SECOND_MS ); - connect( &mReconnectTimer, &QTimer::timeout, this, &TrimblePositionProvider::onReconnectTimeout ); - - mHeartBeatTimer.setSingleShot( true ); - connect( &mHeartBeatTimer, &QTimer::timeout, this, [this] - { - setState( tr( "No data from Trimble Mobile Manager" ), State::NoConnection ); - emit positionChanged( GeoPosition() ); - startReconnectTimer(); - } ); - - TrimblePositionProvider::startUpdates(); -} - -TrimblePositionProvider::~TrimblePositionProvider() -{ - TrimblePositionProvider::closeProvider(); -} - -void TrimblePositionProvider::startUpdates() -{ - if ( mRegistrationInProgress ) - { - qDebug() << "TrimblePositionProvider: startUpdates called while registration already in progress, ignoring"; - return; - } - if ( mCachedPort > 0 ) - { - qDebug() << "TrimblePositionProvider: startUpdates - reconnecting with cached port" << mCachedPort; - connectWebSocket( mCachedPort ); - return; - } - mRegistrationInProgress = true; - qDebug() << "TrimblePositionProvider: startUpdates - requesting registration"; - setState( tr( "Connecting to Trimble Mobile Manager" ), State::Connecting ); - mRegistration->requestRegistration( __getTrimbleAppId() ); -} - -void TrimblePositionProvider::stopUpdates() -{ - mHeartBeatTimer.stop(); - mReconnectTimer.stop(); - if ( mSocket && mSocket->state() == QAbstractSocket::ConnectedState ) - mSocket->close(); -} - -void TrimblePositionProvider::closeProvider() -{ - mHeartBeatTimer.stop(); - mReconnectTimer.stop(); - if ( mSocket ) - { - mSocket->disconnect(); - mSocket->abort(); - } -} - QgsCoordinateReferenceSystem TrimblePositionProvider::sourceCrs() const { return mSourceCrs; } -// --------------------------------------------------------------------------- -// Private slots -// --------------------------------------------------------------------------- +double TrimblePositionProvider::antennaHeight() const +{ + return mAntennaHeight; +} -void TrimblePositionProvider::onRegistered( int port ) +void TrimblePositionProvider::onRegistered( const int port ) { - qDebug() << "TrimblePositionProvider: registration succeeded, port:" << port; mRegistrationInProgress = false; mCachedPort = port; connectWebSocket( port ); @@ -236,29 +265,27 @@ void TrimblePositionProvider::onRegistered( int port ) void TrimblePositionProvider::onRegistrationFailed( const QString &reason ) { - qDebug() << "TrimblePositionProvider: registration failed:" << reason; mRegistrationInProgress = false; setState( reason, State::NoConnection ); emit positionChanged( GeoPosition() ); } -void TrimblePositionProvider::connectWebSocket( int port ) +void TrimblePositionProvider::connectWebSocket( const int port ) { mSocket = std::make_unique(); connect( mSocket.get(), &QWebSocket::textMessageReceived, this, &TrimblePositionProvider::onTextMessageReceived ); connect( mSocket.get(), &QWebSocket::disconnected, this, &TrimblePositionProvider::onSocketDisconnected ); - connect( mSocket.get(), QOverload::of( &QWebSocket::error ), - this, &TrimblePositionProvider::onSocketError ); + connect( mSocket.get(), &QWebSocket::errorOccurred, this, &TrimblePositionProvider::onSocketError ); connect( mSocket.get(), &QWebSocket::connected, this, [this] { - setState( tr( "Connected to Trimble Mobile Manager" ), State::Connected ); + setState( tr( "Connected" ), State::Connected ); mReconnectDelay = ReconnectDelay::ShortDelay; mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); } ); - const QUrl url( QStringLiteral( "ws://127.0.0.1:%1/" ).arg( port ) ); - setState( tr( "Connecting to Trimble Mobile Manager" ), State::Connecting ); + const QUrl url( QStringLiteral( "%1:%2" ).arg( WEBSOCKET_LOCALHOST_ADDRESS, port ) ); + setState( tr( "Connecting" ), State::Connecting ); mSocket->open( url ); } @@ -266,17 +293,11 @@ void TrimblePositionProvider::onTextMessageReceived( const QString &message ) { mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); - // Parse once; keep antenna-height fields through the position transformer - GeoPosition raw = parseLocationMessage( message ); - const double antennaHeight = raw.antennaHeight; - const bool antennaHeightApplied = raw.antennaHeightApplied; - - GeoPosition pos = mPositionTransformer->processNetworkPosition( raw ); - pos.antennaHeight = antennaHeight; - pos.antennaHeightApplied = antennaHeightApplied; + GeoPosition parsedPosition = parseLocationMessage( message ); + GeoPosition newPosition = mPositionTransformer->processTrimblePosition( parsedPosition ); - setState( tr( "Connected to Trimble Mobile Manager" ), State::Connected ); - emit positionChanged( pos ); + setState( tr( "Connected" ), State::Connected ); + emit positionChanged( newPosition ); // Update source CRS from reported frame (re-resolve only on change) const QJsonDocument doc = QJsonDocument::fromJson( message.toUtf8() ); @@ -295,10 +316,11 @@ void TrimblePositionProvider::onTextMessageReceived( const QString &message ) } } -void TrimblePositionProvider::onSocketError( QAbstractSocket::SocketError error ) +void TrimblePositionProvider::onSocketError( const QAbstractSocket::SocketError error ) { Q_UNUSED( error ) - setState( tr( "Connection error: %1" ).arg( mSocket ? mSocket->errorString() : QString() ), State::NoConnection ); + setState( tr( "Disconnected" ), State::NoConnection ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Socket error occurred: %1" ).arg( mSocket->errorString() ) ); emit positionChanged( GeoPosition() ); startReconnectTimer(); } @@ -306,7 +328,7 @@ void TrimblePositionProvider::onSocketError( QAbstractSocket::SocketError error void TrimblePositionProvider::onSocketDisconnected() { mHeartBeatTimer.stop(); - setState( tr( "Disconnected from Trimble Mobile Manager" ), State::NoConnection ); + setState( tr( "Disconnected" ), State::NoConnection ); emit positionChanged( GeoPosition() ); startReconnectTimer(); } diff --git a/app/position/providers/trimblepositionprovider.h b/app/position/providers/trimblepositionprovider.h index ffdffff99..7944f3f9d 100644 --- a/app/position/providers/trimblepositionprovider.h +++ b/app/position/providers/trimblepositionprovider.h @@ -30,10 +30,11 @@ class TrimblePositionProvider : public AbstractPositionProvider QgsCoordinateReferenceSystem sourceCrs() const override; + // trimble provider provides antenna height from Trimble Mobile Manager + double antennaHeight() const; + // TODO: move to native utils Q_INVOKABLE void openAntennaHeightPage(); - // Visible for unit testing - static GeoPosition parseLocationMessage( const QString &json ); static QgsCoordinateReferenceSystem resolveFrame( const QString &frameName, double epoch ); private slots: @@ -49,17 +50,21 @@ class TrimblePositionProvider : public AbstractPositionProvider void startReconnectTimer(); void reconnect(); + GeoPosition parseLocationMessage( const QString &json ); + TrimbleRegistration *mRegistration = nullptr; std::unique_ptr mSocket; + QTimer mReconnectTimer; QTimer mHeartBeatTimer; - - int mCachedPort = 0; int mReconnectDelay = ReconnectDelay::ShortDelay; int mSecondsLeftToReconnect = 0; + + int mCachedPort = 0; bool mRegistrationInProgress = false; QgsCoordinateReferenceSystem mSourceCrs; + double mAntennaHeight = std::numeric_limits::quiet_NaN(); }; #endif // TRIMBLEPOSITIONPROVIDER_H diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index 2594c4696..7545acd9f 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -87,7 +87,7 @@ MMPage { Loader { width: parent.width - sourceComponent: PositionKit.antennaHeightApplied ? trimbleAntennaHeightComponent : editableAntennaHeightComponent + sourceComponent: PositionKit.requireAntennaHeightTransform() ? externalAntennaHeightComponent : editableAntennaHeightComponent } Component { @@ -108,7 +108,7 @@ MMPage { } Component { - id: trimbleAntennaHeightComponent + id: externalAntennaHeightComponent Column { width: parent ? parent.width : 0 @@ -117,20 +117,9 @@ MMPage { MMSettingsComponents.MMSettingsItem { width: parent.width title: qsTr("GPS antenna height") - value: PositionKit.antennaHeight.toFixed( 3 ) + " m" - } - - MMText { - width: parent.width - text: qsTr("Antenna height is managed by Trimble Mobile Manager") - color: __style.greyColor - font: __style.t4 - } - - MMButton { - text: qsTr("Open in Trimble Mobile Manager") - size: MMButton.Sizes.Small - onClicked: { + description: qsTr("Antenna height is managed by Trimble Mobile Manager") + value: __inputUtils.formatNumber( PositionKit.antennaHeight, 2 ) + " m" + onClicked: () => { if ( PositionKit.positionProvider ) PositionKit.positionProvider.openAntennaHeightPage() } From 8c01256340428b83484a8ee8a0fb889b32684207 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 4 Aug 2026 16:05:13 +0200 Subject: [PATCH 13/21] Fix Android trimble registration --- app/position/providers/trimbleregistration.h | 25 +++++- .../providers/trimbleregistrationandroid.cpp | 84 +++++++++---------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/app/position/providers/trimbleregistration.h b/app/position/providers/trimbleregistration.h index ebb88055b..64286826e 100644 --- a/app/position/providers/trimbleregistration.h +++ b/app/position/providers/trimbleregistration.h @@ -14,10 +14,27 @@ #include #include -#ifdef ANDROID -class TmmResultReceiver; // defined in trimbleregistrationandroid.cpp +#ifdef Q_OS_ANDROID +#include +class TrimbleRegistration; #endif +#ifdef Q_OS_ANDROID + +class TrimbleResultReceiver : public QObject, public QAndroidActivityResultReceiver +{ + Q_OBJECT + + public: + void handleActivityResult( int receiverRequestCode, int resultCode, const QJniObject &data ) override; + + signals: + void registrationFailed( QString reason ); + void registrationSucceeded( int locationDataPort ); +}; +#endif + + /** * Platform-agnostic async contract for registering with Trimble Mobile Manager. * @@ -47,8 +64,8 @@ class TrimbleRegistration : public QObject void failed( const QString &reason ); private: -#ifdef ANDROID - std::unique_ptr mResultReceiver; +#ifdef Q_OS_ANDROID + std::unique_ptr mResultReceiver; #endif }; diff --git a/app/position/providers/trimbleregistrationandroid.cpp b/app/position/providers/trimbleregistrationandroid.cpp index 7534ac434..8966457d3 100644 --- a/app/position/providers/trimbleregistrationandroid.cpp +++ b/app/position/providers/trimbleregistrationandroid.cpp @@ -9,64 +9,56 @@ #include "trimbleregistration.h" -#ifdef ANDROID +#ifdef Q_OS_ANDROID #include #include "coreutils.h" static constexpr int TMM_REGISTER_REQUEST_CODE = 0x544D4D52; // "TMMR" - custom value to pair intent result -static QString temp = QStringLiteral( "hello" ); -class TmmResultReceiver : public QAndroidActivityResultReceiver +void TrimbleResultReceiver::handleActivityResult( const int receiverRequestCode, const int resultCode, const QJniObject &data ) { - public: - explicit TmmResultReceiver( TrimbleRegistration *reg ) : mReg( reg ) {} - - void handleActivityResult( const int receiverRequestCode, const int resultCode, const QJniObject &data ) override - { - Q_UNUSED( resultCode ) - if ( receiverRequestCode != TMM_REGISTER_REQUEST_CODE || !mReg ) - return; - - if ( !data.isValid() ) - { - emit mReg->failed( TrimbleRegistration::tr( "No response from Trimble Mobile Manager" ) ); - CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); - return; - } - - const QAndroidIntent intent( data ); - // we can't use intent.extraVariant() function here as it throws errors in Qt code - const QString registerResult = intent.handle().callObjectMethod( "getStringExtra", QJniObject::fromString( QStringLiteral( "registrationResult" ) ).object() ).toString(); - - if ( registerResult != QStringLiteral( "OK" ) ) - { - emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager registration failed: %1" ).arg( registerResult ) ); - CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, registration has been refused by Trimble Mobile Manager. Reason: %1." ).arg( registerResult ) ); - return; - } - // we can't use intent.extraVariant() function here as it doesn't parse the data correctly - const int locationPortResult = intent.handle().callMethod( "getIntExtra", QJniObject::fromString( QStringLiteral( "locationV2Port" ) ).object(), 0 ); - - if ( !locationPortResult ) - { - emit mReg->failed( TrimbleRegistration::tr( "Trimble Mobile Manager returned invalid port" ) ); - CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager returned malformed location data port." ) ); - return; - } - - emit mReg->registered( locationPortResult ); - } - - private: - TrimbleRegistration *mReg = nullptr; -}; + Q_UNUSED( resultCode ) + if ( receiverRequestCode != TMM_REGISTER_REQUEST_CODE ) + return; + + if ( !data.isValid() ) + { + emit registrationFailed( TrimbleRegistration::tr( "No response from Trimble Mobile Manager" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); + return; + } + + const QAndroidIntent intent( data ); + // we can't use intent.extraVariant() function here as it throws errors in Qt code + const QString registerResult = intent.handle().callObjectMethod( "getStringExtra", QJniObject::fromString( QStringLiteral( "registrationResult" ) ).object() ).toString(); + + if ( registerResult != QStringLiteral( "OK" ) ) + { + emit registrationFailed( TrimbleRegistration::tr( "Trimble Mobile Manager registration failed: %1" ).arg( registerResult ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, registration has been refused by Trimble Mobile Manager. Reason: %1." ).arg( registerResult ) ); + return; + } + // we can't use intent.extraVariant() function here as it doesn't parse the data correctly + const int locationPortResult = intent.handle().callMethod( "getIntExtra", QJniObject::fromString( QStringLiteral( "locationV2Port" ) ).object(), 0 ); + + if ( !locationPortResult ) + { + emit registrationFailed( TrimbleRegistration::tr( "Trimble Mobile Manager returned invalid port" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager returned malformed location data port." ) ); + return; + } + + emit registrationSucceeded( locationPortResult ); +} TrimbleRegistration::TrimbleRegistration( QObject *parent ) : QObject( parent ) - , mResultReceiver( std::make_unique( this ) ) + , mResultReceiver( std::make_unique() ) { + connect( mResultReceiver.get(), &TrimbleResultReceiver::registrationFailed, this, &TrimbleRegistration::failed ); + connect( mResultReceiver.get(), &TrimbleResultReceiver::registrationSucceeded, this, &TrimbleRegistration::registered ); } void TrimbleRegistration::requestRegistration( const QString &appId ) From 818435048ea66cb69793e7afc117bfe5746ccac0 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 4 Aug 2026 16:27:44 +0200 Subject: [PATCH 14/21] Fix websocket URL --- app/position/providers/trimblepositionprovider.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/position/providers/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp index 9270ccc53..48b16ae0e 100644 --- a/app/position/providers/trimblepositionprovider.cpp +++ b/app/position/providers/trimblepositionprovider.cpp @@ -30,7 +30,6 @@ constexpr int ONE_SECOND_MS = 1000; constexpr int TMM_WS_V2_DEFAULT_PORT = 9639; -const QString WEBSOCKET_LOCALHOST_ADDRESS = QStringLiteral( "ws://localhost" ); TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) : AbstractPositionProvider( id, QStringLiteral( "external_trimble" ), name, positionTransformer, parent ) @@ -284,7 +283,7 @@ void TrimblePositionProvider::connectWebSocket( const int port ) mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); } ); - const QUrl url( QStringLiteral( "%1:%2" ).arg( WEBSOCKET_LOCALHOST_ADDRESS, port ) ); + const QUrl url( QStringLiteral( "ws://localhost:%2" ).arg( port ) ); setState( tr( "Connecting" ), State::Connecting ); mSocket->open( url ); } From a8260414d3841383570db3360a47428ef96b16a3 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 4 Aug 2026 17:13:01 +0200 Subject: [PATCH 15/21] Fix antenna height setup --- app/position/positionkit.cpp | 10 ++++++++++ app/position/positionkit.h | 4 ++++ app/qml/settings/MMSettingsPage.qml | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 46f2845f7..0c0eebc0d 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -674,3 +674,13 @@ bool PositionKit::requireAntennaHeightTransform() const } return true; } + +void PositionKit::openAntennaHeightPage() const +{ +#if WITH_TRIMBLE_PROVIDERS + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + dynamic_cast( mPositionProvider.get() )->openAntennaHeightPage(); + } +#endif +} diff --git a/app/position/positionkit.h b/app/position/positionkit.h index 5fffaa112..fbc7ae8ab 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -152,6 +152,10 @@ class PositionKit : public QObject * subtract antenna height in MM. */ Q_INVOKABLE bool requireAntennaHeightTransform() const; + /* + * Opens antenna height setting in Trimble Mobile Manager if using trimble position provider. + */ + Q_INVOKABLE void openAntennaHeightPage() const; void setVerticalCrs( const QgsCoordinateReferenceSystem &verticalCrs ); void setElevationTransformationEnabled( bool elevationTransformationEnabled ); diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index 7545acd9f..ac4995bfb 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -87,7 +87,7 @@ MMPage { Loader { width: parent.width - sourceComponent: PositionKit.requireAntennaHeightTransform() ? externalAntennaHeightComponent : editableAntennaHeightComponent + sourceComponent: !PositionKit.requireAntennaHeightTransform() ? externalAntennaHeightComponent : editableAntennaHeightComponent } Component { @@ -121,7 +121,7 @@ MMPage { value: __inputUtils.formatNumber( PositionKit.antennaHeight, 2 ) + " m" onClicked: () => { if ( PositionKit.positionProvider ) - PositionKit.positionProvider.openAntennaHeightPage() + PositionKit.openAntennaHeightPage() } } } From 9ce3313d5e72f9b5452d0ccc1372292812e44407 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 5 Aug 2026 13:18:34 +0200 Subject: [PATCH 16/21] Improve antenna height setting text --- app/qml/settings/MMSettingsPage.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index ac4995bfb..ac5377e9e 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -117,8 +117,8 @@ MMPage { MMSettingsComponents.MMSettingsItem { width: parent.width title: qsTr("GPS antenna height") - description: qsTr("Antenna height is managed by Trimble Mobile Manager") - value: __inputUtils.formatNumber( PositionKit.antennaHeight, 2 ) + " m" + description: qsTr("Click here to modify in Trimble Mobile Manager") + value: __inputUtils.formatNumber( PositionKit.antennaHeight, 3 ) + " m" onClicked: () => { if ( PositionKit.positionProvider ) PositionKit.openAntennaHeightPage() From 435f3ac991b622904003414d8a7e4d67d83abfca Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Sun, 9 Aug 2026 21:35:52 +0200 Subject: [PATCH 17/21] Refactor CRS from trimble provider & process geoid name --- app/position/positionkit.cpp | 32 ++-- app/position/positionkit.h | 1 - .../providers/abstractpositionprovider.cpp | 5 - .../providers/abstractpositionprovider.h | 8 - .../providers/trimblepositionprovider.cpp | 155 +++++++++++------- .../providers/trimblepositionprovider.h | 7 +- 6 files changed, 111 insertions(+), 97 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 0c0eebc0d..cdac5bf03 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -42,10 +42,6 @@ QgsCoordinateReferenceSystem PositionKit::positionCrs3D() { return mPositionCrs3D; } - if ( mProviderCrs.isValid() ) - { - return mProviderCrs; - } return QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); } @@ -62,6 +58,11 @@ QString PositionKit::positionCrs3DGeoidModelName() return QgsCoordinateReferenceSystem::fromEpsgId( 5773 ).description(); } + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + return dynamic_cast( mPositionProvider.get() )->geoidModelName(); + } + return {}; } @@ -103,22 +104,9 @@ void PositionKit::setPositionProvider( AbstractPositionProvider *provider ) mPositionProvider.reset( provider ); - mProviderCrs = QgsCoordinateReferenceSystem(); // reset to WGS84 default - if ( mPositionProvider ) { connect( mPositionProvider.get(), &AbstractPositionProvider::positionChanged, this, &PositionKit::parsePositionUpdate ); - connect( mPositionProvider.get(), &AbstractPositionProvider::sourceCrsChanged, this, [this]( const QgsCoordinateReferenceSystem & crs ) - { - mProviderCrs = crs.isValid() ? crs : QgsCoordinateReferenceSystem(); - refreshPositionTransformer( QgsCoordinateTransformContext() ); - } ); - - // Pick up provider CRS if it's already known (e.g. re-activation) - const QgsCoordinateReferenceSystem initial = mPositionProvider->sourceCrs(); - if ( initial.isValid() ) - mProviderCrs = initial; - CoreUtils::log( QStringLiteral( "PositionKit" ), QStringLiteral( "Changed position provider to: %1" ).arg( provider->id() ) ); } else // passed nullptr @@ -480,7 +468,15 @@ void PositionKit::appStateChanged( const Qt::ApplicationState state ) void PositionKit::refreshPositionTransformer( const QgsCoordinateTransformContext &transformContext ) { - const QgsCoordinateReferenceSystem srcCrs = mProviderCrs.isValid() ? mProviderCrs : positionCrs3DEllipsoidHeight(); + QgsCoordinateReferenceSystem srcCrs; + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + srcCrs = dynamic_cast( mPositionProvider.get() )->sourceCrs(); + } + else + { + srcCrs = positionCrs3DEllipsoidHeight(); + } const QgsCoordinateReferenceSystem destCrs = positionCrs3D(); QgsCoordinateTransformContext context = transformContext; diff --git a/app/position/positionkit.h b/app/position/positionkit.h index fbc7ae8ab..b5d8a9d97 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -216,7 +216,6 @@ class PositionKit : public QObject QgsCoordinateReferenceSystem mPositionCrs3D; QgsCoordinateReferenceSystem mVerticalCrs; - QgsCoordinateReferenceSystem mProviderCrs; // updated from provider's sourceCrsChanged; invalid = WGS84 bool mElevationTransformationEnabled = false; friend class TestPosition; diff --git a/app/position/providers/abstractpositionprovider.cpp b/app/position/providers/abstractpositionprovider.cpp index 8be3cf020..30ae52983 100644 --- a/app/position/providers/abstractpositionprovider.cpp +++ b/app/position/providers/abstractpositionprovider.cpp @@ -22,11 +22,6 @@ void AbstractPositionProvider::setPosition( QgsPoint ) { } -QgsCoordinateReferenceSystem AbstractPositionProvider::sourceCrs() const -{ - return QgsCoordinateReferenceSystem(); // invalid → caller treats as WGS84 -} - QString AbstractPositionProvider::name() const { return mProviderName; diff --git a/app/position/providers/abstractpositionprovider.h b/app/position/providers/abstractpositionprovider.h index ad7210c5d..22d106e8e 100644 --- a/app/position/providers/abstractpositionprovider.h +++ b/app/position/providers/abstractpositionprovider.h @@ -10,7 +10,6 @@ #ifndef ABSTRACTPOSITIONPROVIDER_H #define ABSTRACTPOSITIONPROVIDER_H -#include #include #include @@ -54,19 +53,12 @@ class AbstractPositionProvider : public QObject Q_INVOKABLE QString name() const; Q_INVOKABLE QString type() const; - // The geodetic CRS the provider's coordinates are in. - // Default: invalid (means WGS84). Override in providers that report their datum. - virtual QgsCoordinateReferenceSystem sourceCrs() const; - signals: void positionChanged( const GeoPosition &position ); void stateMessageChanged( const QString &message ); void stateChanged( AbstractPositionProvider::State state ); - // Emitted when the provider's reported source CRS changes. - void sourceCrsChanged( const QgsCoordinateReferenceSystem &crs ); - protected: void setState( const QString &message ); // keeps state enum the same and only changes the message void setState( const QString &message, State state ); diff --git a/app/position/providers/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp index 48b16ae0e..085ed642b 100644 --- a/app/position/providers/trimblepositionprovider.cpp +++ b/app/position/providers/trimblepositionprovider.cpp @@ -30,6 +30,58 @@ constexpr int ONE_SECOND_MS = 1000; constexpr int TMM_WS_V2_DEFAULT_PORT = 9639; +// Trimble reports just 2D CRS however we have here the 3D variant for further elevation processing +const QHash TRIMBLE_REFERENCE_FRAMES = +{ + { QStringLiteral( "POSGAR07" ), QStringLiteral( "5342" ) }, + { QStringLiteral( "GDA94" ), QStringLiteral( "4939" ) }, + { QStringLiteral( "GDA2020" ), QStringLiteral( "EPSG:7843" ) }, + { QStringLiteral( "ETRF2000" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "SIRGAS-CON" ), QStringLiteral( "EPSG:4989" ) }, + { QStringLiteral( "BGS2005" ), QStringLiteral( "EPSG:7797" ) }, + { QStringLiteral( "RGNC91-93" ), QStringLiteral( "EPSG:4907" ) }, + { QStringLiteral( "NAD83(CSRS)v7" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "SIRGAS-Chile 2021" ), QStringLiteral( "EPSG:20040" ) }, + { QStringLiteral( "SIRGAS-Chile 2016" ), QStringLiteral( "EPSG:9152" ) }, + { QStringLiteral( "CGCS2000" ), QStringLiteral( "EPSG:4480" ) }, + { QStringLiteral( "MAGNA-SIRGAS(2018)" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "MAGNA-SIRGAS" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "EUREF-DK15" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EUREF-DK94" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EST97" ), QStringLiteral( "EPSG:4935" ) }, + { QStringLiteral( "EUREF-FIN" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "RGF93v2b" ), QStringLiteral( "EPSG:9781" ) }, + { QStringLiteral( "RGAF09" ), QStringLiteral( "EPSG:5488" ) }, + { QStringLiteral( "RGFG95" ), QStringLiteral( "EPSG:4967" ) }, + { QStringLiteral( "RGTAAF07" ), QStringLiteral( "EPSG:7072" ) }, + { QStringLiteral( "ETRS89-DREF91(R16)" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ISN2016" ), QStringLiteral( "EPSG:8085" ) }, + { QStringLiteral( "ITRF2008-India-CORS" ), QStringLiteral( "EPSG:8999" ) }, + { QStringLiteral( "RDN2008" ), QStringLiteral( "EPSG:6705" ) }, + { QStringLiteral( "JGD2011" ), QStringLiteral( "EPSG:6667" ) }, + { QStringLiteral( "JGD2000" ), QStringLiteral( "EPSG:4947" ) }, + { QStringLiteral( "LKS-92" ), QStringLiteral( "EPSG:4949" ) }, + { QStringLiteral( "EUREF-NKG-2003" ), QStringLiteral( "EPSG:4951" ) }, + { QStringLiteral( "ITRF2008-Mexico" ), QStringLiteral( "EPSG:6364" ) }, + { QStringLiteral( "ETRF2000 (EPOCH:2010.5)" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "NZGD2000" ), QStringLiteral( "EPSG:4959" ) }, + { QStringLiteral( "EUREF89" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "RGR92" ), QStringLiteral( "EPSG:4971" ) }, + { QStringLiteral( "KSA-GRF17" ), QStringLiteral( "EPSG:9332" ) }, + { QStringLiteral( "ETRS89-D96-17" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "KGD2002" ), QStringLiteral( "EPSG:4927" ) }, + { QStringLiteral( "SWEREF99" ), QStringLiteral( "EPSG:4977" ) }, + { QStringLiteral( "OSNetv2009" ), QStringLiteral( "EPSG:4277" ) }, // TODO: create 3D CRS here + { QStringLiteral( "NAD83(2011) (EPOCH:2010)" ), QStringLiteral( "EPSG:6319" ) }, + { QStringLiteral( "NAD83(CORS96) (EPOCH:2002)" ), QStringLiteral( "EPSG:6782" ) }, + { QStringLiteral( "NAD83(PA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6321" ) }, + { QStringLiteral( "NAD83(MA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6324" ) }, + { QStringLiteral( "ITRF2000" ), QStringLiteral( "EPSG:7909" ) }, + { QStringLiteral( "ITRF2005" ), QStringLiteral( "EPSG:7910" ) }, + { QStringLiteral( "ITRF2008" ), QStringLiteral( "EPSG:7911" ) }, + { QStringLiteral( "ITRF2014" ), QStringLiteral( "EPSG:7912" ) }, + { QStringLiteral( "WGS84(G1762) current" ), QStringLiteral( "EPSG:4979" ) }, +}; TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) : AbstractPositionProvider( id, QStringLiteral( "external_trimble" ), name, positionTransformer, parent ) @@ -96,60 +148,25 @@ void TrimblePositionProvider::closeProvider() mSocket->abort(); } } -// --------------------------------------------------------------------------- -// Frame resolver -// --------------------------------------------------------------------------- - -struct FrameEntry -{ - const char *name; // prefix match against targetReferenceFrameName - const char *authId; // EPSG or user authority identifier -}; -static const FrameEntry KNOWN_FRAMES[] = +QgsCoordinateReferenceSystem TrimblePositionProvider::resolveFrame( const QString &frameName, const double epoch ) { - { "WGS 84", "EPSG:4979" }, - { "WGS84", "EPSG:4979" }, - { "ITRF2020", "EPSG:9990" }, - { "ITRF2014", "EPSG:7912" }, - { "ITRF2008", "EPSG:8999" }, - { "ITRF2005", "EPSG:8998" }, - { "ITRF2000", "EPSG:8997" }, - { "ITRF97", "EPSG:8996" }, - { "ITRF96", "EPSG:8995" }, - { "ITRF94", "EPSG:8994" }, - { "ITRF93", "EPSG:8993" }, - { "ITRF92", "EPSG:8992" }, - { "ITRF91", "EPSG:8991" }, - { "ITRF90", "EPSG:8990" }, - { "ITRF89", "EPSG:8989" }, - { "ITRF88", "EPSG:8988" }, - { "NAD83(2011)", "EPSG:6319" }, - { "NAD83", "EPSG:4269" }, - { "ETRS89", "EPSG:4936" }, - { "GDA2020", "EPSG:7843" }, - { "GDA94", "EPSG:4939" }, - { nullptr, nullptr } -}; - -QgsCoordinateReferenceSystem TrimblePositionProvider::resolveFrame( const QString &frameName, double epoch ) -{ - if ( frameName.isEmpty() ) - return QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ); // WGS84 default + // fallback to PositionKit::positionCrsXXX() + if ( frameName.isEmpty() ) return {}; - for ( const FrameEntry *e = KNOWN_FRAMES; e->name; ++e ) + const QString crsId = TRIMBLE_REFERENCE_FRAMES.value( frameName ); + if ( !crsId.isEmpty() ) { - if ( frameName.startsWith( QLatin1String( e->name ), Qt::CaseInsensitive ) ) + QgsCoordinateReferenceSystem newCrs( crsId ); + if ( epoch >= 0 ) { - QgsCoordinateReferenceSystem crs( QString::fromLatin1( e->authId ) ); - if ( epoch > 0 ) - crs.setCoordinateEpoch( epoch ); - return crs; + newCrs.setCoordinateEpoch( epoch ); } + return newCrs; } - QgsDebugMsgLevel( QStringLiteral( "TrimblePositionProvider: unknown frame '%1', falling back to WGS84" ).arg( frameName ), 2 ); - return QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4979" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "TrimblePositionProvider: unknown frame '%1', falling back to WGS84" ).arg( frameName ) ); + return {}; } GeoPosition TrimblePositionProvider::parseLocationMessage( const QString &json ) @@ -242,6 +259,31 @@ GeoPosition TrimblePositionProvider::parseLocationMessage( const QString &json ) mAntennaHeight = obj.value( QStringLiteral( "antennaHeight" ) ).toDouble( std::numeric_limits::quiet_NaN() ); } + const bool referenceFrameFieldExists = obj.contains( QStringLiteral( "targetReferenceFrameName" ) ) && !obj.value( QStringLiteral( "targetReferenceFrameName" ) ).isNull() ; + const bool referenceFrameEpochFieldExists = obj.contains( QStringLiteral( "targetReferenceFrameEpoch" ) ) && !obj.value( QStringLiteral( "targetReferenceFrameEpoch" ) ).isNull(); + + if ( referenceFrameFieldExists && referenceFrameEpochFieldExists ) + { + const QString frameName = obj.value( QStringLiteral( "targetReferenceFrameName" ) ).toString(); + const double epoch = obj.value( QStringLiteral( "targetReferenceFrameEpoch" ) ).toDouble( -1 ); + const QgsCoordinateReferenceSystem newCrs = resolveFrame( frameName, epoch ); + + if ( newCrs != mSourceCrs ) + { + mSourceCrs = newCrs; + mPositionTransformer->setSourceCrs( newCrs ); + } + } + + if ( obj.contains( QStringLiteral( "geoidModel" ) ) && !obj.value( QStringLiteral( "geoidModel" ) ).isNull() ) + { + mGeoidModelName = obj.value( QStringLiteral( "geoidModel" ) ).toString(); + } + else + { + mGeoidModelName = QString(); + } + return pos; } @@ -250,6 +292,11 @@ QgsCoordinateReferenceSystem TrimblePositionProvider::sourceCrs() const return mSourceCrs; } +QString TrimblePositionProvider::geoidModelName() const +{ + return mGeoidModelName; +} + double TrimblePositionProvider::antennaHeight() const { return mAntennaHeight; @@ -297,22 +344,6 @@ void TrimblePositionProvider::onTextMessageReceived( const QString &message ) setState( tr( "Connected" ), State::Connected ); emit positionChanged( newPosition ); - - // Update source CRS from reported frame (re-resolve only on change) - const QJsonDocument doc = QJsonDocument::fromJson( message.toUtf8() ); - if ( !doc.isNull() && doc.isObject() ) - { - const QJsonObject obj = doc.object(); - const QString frameName = obj.value( QStringLiteral( "targetReferenceFrameName" ) ).toString(); - const double epoch = obj.value( QStringLiteral( "targetReferenceFrameEpoch" ) ).toDouble( 0 ); - const QgsCoordinateReferenceSystem newCrs = resolveFrame( frameName, epoch ); - - if ( newCrs != mSourceCrs ) - { - mSourceCrs = newCrs; - emit sourceCrsChanged( mSourceCrs ); - } - } } void TrimblePositionProvider::onSocketError( const QAbstractSocket::SocketError error ) diff --git a/app/position/providers/trimblepositionprovider.h b/app/position/providers/trimblepositionprovider.h index 7944f3f9d..3ca93d86e 100644 --- a/app/position/providers/trimblepositionprovider.h +++ b/app/position/providers/trimblepositionprovider.h @@ -28,15 +28,14 @@ class TrimblePositionProvider : public AbstractPositionProvider void stopUpdates() override; void closeProvider() override; - QgsCoordinateReferenceSystem sourceCrs() const override; + QgsCoordinateReferenceSystem sourceCrs() const; + QString geoidModelName() const; // trimble provider provides antenna height from Trimble Mobile Manager double antennaHeight() const; // TODO: move to native utils Q_INVOKABLE void openAntennaHeightPage(); - static QgsCoordinateReferenceSystem resolveFrame( const QString &frameName, double epoch ); - private slots: void onRegistered( int port ); void onRegistrationFailed( const QString &reason ); @@ -51,6 +50,7 @@ class TrimblePositionProvider : public AbstractPositionProvider void reconnect(); GeoPosition parseLocationMessage( const QString &json ); + static QgsCoordinateReferenceSystem resolveFrame( const QString &frameName, double epoch ); TrimbleRegistration *mRegistration = nullptr; std::unique_ptr mSocket; @@ -65,6 +65,7 @@ class TrimblePositionProvider : public AbstractPositionProvider QgsCoordinateReferenceSystem mSourceCrs; double mAntennaHeight = std::numeric_limits::quiet_NaN(); + QString mGeoidModelName; }; #endif // TRIMBLEPOSITIONPROVIDER_H From 561d6f964494e591cd1aab61eb44de6d90fb7696 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Sun, 9 Aug 2026 21:41:34 +0200 Subject: [PATCH 18/21] Sort TRIMBLE_REFERENCE_FRAMES --- .../providers/trimblepositionprovider.cpp | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/app/position/providers/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp index 085ed642b..822884e53 100644 --- a/app/position/providers/trimblepositionprovider.cpp +++ b/app/position/providers/trimblepositionprovider.cpp @@ -33,53 +33,53 @@ constexpr int TMM_WS_V2_DEFAULT_PORT = 9639; // Trimble reports just 2D CRS however we have here the 3D variant for further elevation processing const QHash TRIMBLE_REFERENCE_FRAMES = { - { QStringLiteral( "POSGAR07" ), QStringLiteral( "5342" ) }, - { QStringLiteral( "GDA94" ), QStringLiteral( "4939" ) }, - { QStringLiteral( "GDA2020" ), QStringLiteral( "EPSG:7843" ) }, - { QStringLiteral( "ETRF2000" ), QStringLiteral( "EPSG:4937" ) }, - { QStringLiteral( "SIRGAS-CON" ), QStringLiteral( "EPSG:4989" ) }, { QStringLiteral( "BGS2005" ), QStringLiteral( "EPSG:7797" ) }, - { QStringLiteral( "RGNC91-93" ), QStringLiteral( "EPSG:4907" ) }, - { QStringLiteral( "NAD83(CSRS)v7" ), QStringLiteral( "EPSG:8254" ) }, - { QStringLiteral( "SIRGAS-Chile 2021" ), QStringLiteral( "EPSG:20040" ) }, - { QStringLiteral( "SIRGAS-Chile 2016" ), QStringLiteral( "EPSG:9152" ) }, { QStringLiteral( "CGCS2000" ), QStringLiteral( "EPSG:4480" ) }, - { QStringLiteral( "MAGNA-SIRGAS(2018)" ), QStringLiteral( "EPSG:4997" ) }, - { QStringLiteral( "MAGNA-SIRGAS" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "EST97" ), QStringLiteral( "EPSG:4935" ) }, + { QStringLiteral( "ETRF2000" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ETRF2000 (EPOCH:2010.5)" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ETRS89-D96-17" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ETRS89-DREF91(R16)" ), QStringLiteral( "EPSG:4937" ) }, { QStringLiteral( "EUREF-DK15" ), QStringLiteral( "EPSG:4937" ) }, { QStringLiteral( "EUREF-DK94" ), QStringLiteral( "EPSG:4937" ) }, - { QStringLiteral( "EST97" ), QStringLiteral( "EPSG:4935" ) }, { QStringLiteral( "EUREF-FIN" ), QStringLiteral( "EPSG:4937" ) }, - { QStringLiteral( "RGF93v2b" ), QStringLiteral( "EPSG:9781" ) }, - { QStringLiteral( "RGAF09" ), QStringLiteral( "EPSG:5488" ) }, - { QStringLiteral( "RGFG95" ), QStringLiteral( "EPSG:4967" ) }, - { QStringLiteral( "RGTAAF07" ), QStringLiteral( "EPSG:7072" ) }, - { QStringLiteral( "ETRS89-DREF91(R16)" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EUREF-NKG-2003" ), QStringLiteral( "EPSG:4951" ) }, + { QStringLiteral( "EUREF89" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "GDA2020" ), QStringLiteral( "EPSG:7843" ) }, + { QStringLiteral( "GDA94" ), QStringLiteral( "4939" ) }, { QStringLiteral( "ISN2016" ), QStringLiteral( "EPSG:8085" ) }, + { QStringLiteral( "ITRF2000" ), QStringLiteral( "EPSG:7909" ) }, + { QStringLiteral( "ITRF2005" ), QStringLiteral( "EPSG:7910" ) }, + { QStringLiteral( "ITRF2008" ), QStringLiteral( "EPSG:7911" ) }, { QStringLiteral( "ITRF2008-India-CORS" ), QStringLiteral( "EPSG:8999" ) }, - { QStringLiteral( "RDN2008" ), QStringLiteral( "EPSG:6705" ) }, - { QStringLiteral( "JGD2011" ), QStringLiteral( "EPSG:6667" ) }, - { QStringLiteral( "JGD2000" ), QStringLiteral( "EPSG:4947" ) }, - { QStringLiteral( "LKS-92" ), QStringLiteral( "EPSG:4949" ) }, - { QStringLiteral( "EUREF-NKG-2003" ), QStringLiteral( "EPSG:4951" ) }, { QStringLiteral( "ITRF2008-Mexico" ), QStringLiteral( "EPSG:6364" ) }, - { QStringLiteral( "ETRF2000 (EPOCH:2010.5)" ), QStringLiteral( "EPSG:4937" ) }, - { QStringLiteral( "NZGD2000" ), QStringLiteral( "EPSG:4959" ) }, - { QStringLiteral( "EUREF89" ), QStringLiteral( "EPSG:4937" ) }, - { QStringLiteral( "RGR92" ), QStringLiteral( "EPSG:4971" ) }, - { QStringLiteral( "KSA-GRF17" ), QStringLiteral( "EPSG:9332" ) }, - { QStringLiteral( "ETRS89-D96-17" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ITRF2014" ), QStringLiteral( "EPSG:7912" ) }, + { QStringLiteral( "JGD2000" ), QStringLiteral( "EPSG:4947" ) }, + { QStringLiteral( "JGD2011" ), QStringLiteral( "EPSG:6667" ) }, { QStringLiteral( "KGD2002" ), QStringLiteral( "EPSG:4927" ) }, - { QStringLiteral( "SWEREF99" ), QStringLiteral( "EPSG:4977" ) }, - { QStringLiteral( "OSNetv2009" ), QStringLiteral( "EPSG:4277" ) }, // TODO: create 3D CRS here + { QStringLiteral( "KSA-GRF17" ), QStringLiteral( "EPSG:9332" ) }, + { QStringLiteral( "LKS-92" ), QStringLiteral( "EPSG:4949" ) }, + { QStringLiteral( "MAGNA-SIRGAS" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "MAGNA-SIRGAS(2018)" ), QStringLiteral( "EPSG:4997" ) }, { QStringLiteral( "NAD83(2011) (EPOCH:2010)" ), QStringLiteral( "EPSG:6319" ) }, { QStringLiteral( "NAD83(CORS96) (EPOCH:2002)" ), QStringLiteral( "EPSG:6782" ) }, - { QStringLiteral( "NAD83(PA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6321" ) }, + { QStringLiteral( "NAD83(CSRS)v7" ), QStringLiteral( "EPSG:8254" ) }, { QStringLiteral( "NAD83(MA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6324" ) }, - { QStringLiteral( "ITRF2000" ), QStringLiteral( "EPSG:7909" ) }, - { QStringLiteral( "ITRF2005" ), QStringLiteral( "EPSG:7910" ) }, - { QStringLiteral( "ITRF2008" ), QStringLiteral( "EPSG:7911" ) }, - { QStringLiteral( "ITRF2014" ), QStringLiteral( "EPSG:7912" ) }, + { QStringLiteral( "NAD83(PA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6321" ) }, + { QStringLiteral( "NZGD2000" ), QStringLiteral( "EPSG:4959" ) }, + { QStringLiteral( "OSNetv2009" ), QStringLiteral( "EPSG:4277" ) }, // TODO: create 3D CRS here + { QStringLiteral( "POSGAR07" ), QStringLiteral( "5342" ) }, + { QStringLiteral( "RDN2008" ), QStringLiteral( "EPSG:6705" ) }, + { QStringLiteral( "RGAF09" ), QStringLiteral( "EPSG:5488" ) }, + { QStringLiteral( "RGF93v2b" ), QStringLiteral( "EPSG:9781" ) }, + { QStringLiteral( "RGFG95" ), QStringLiteral( "EPSG:4967" ) }, + { QStringLiteral( "RGNC91-93" ), QStringLiteral( "EPSG:4907" ) }, + { QStringLiteral( "RGR92" ), QStringLiteral( "EPSG:4971" ) }, + { QStringLiteral( "RGTAAF07" ), QStringLiteral( "EPSG:7072" ) }, + { QStringLiteral( "SIRGAS-Chile 2016" ), QStringLiteral( "EPSG:9152" ) }, + { QStringLiteral( "SIRGAS-Chile 2021" ), QStringLiteral( "EPSG:20040" ) }, + { QStringLiteral( "SIRGAS-CON" ), QStringLiteral( "EPSG:4989" ) }, + { QStringLiteral( "SWEREF99" ), QStringLiteral( "EPSG:4977" ) }, { QStringLiteral( "WGS84(G1762) current" ), QStringLiteral( "EPSG:4979" ) }, }; From 28eb51ea9f15013711b8ee70c34d2693714a8921 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Sun, 9 Aug 2026 23:51:46 +0200 Subject: [PATCH 19/21] Update TRIMBLE_REFERENCE_FRAMES --- .../providers/trimblepositionprovider.cpp | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/position/providers/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp index 822884e53..adcabeb2f 100644 --- a/app/position/providers/trimblepositionprovider.cpp +++ b/app/position/providers/trimblepositionprovider.cpp @@ -35,7 +35,9 @@ const QHash TRIMBLE_REFERENCE_FRAMES = { { QStringLiteral( "BGS2005" ), QStringLiteral( "EPSG:7797" ) }, { QStringLiteral( "CGCS2000" ), QStringLiteral( "EPSG:4480" ) }, + { QStringLiteral( "CR-SIRGAS" ), QStringLiteral( "EPSG:8906" ) }, { QStringLiteral( "EST97" ), QStringLiteral( "EPSG:4935" ) }, + { QStringLiteral( "ETRF1989" ), QStringLiteral( "EPSG:7914" ) }, { QStringLiteral( "ETRF2000" ), QStringLiteral( "EPSG:4937" ) }, { QStringLiteral( "ETRF2000 (EPOCH:2010.5)" ), QStringLiteral( "EPSG:4937" ) }, { QStringLiteral( "ETRS89-D96-17" ), QStringLiteral( "EPSG:4937" ) }, @@ -46,14 +48,16 @@ const QHash TRIMBLE_REFERENCE_FRAMES = { QStringLiteral( "EUREF-NKG-2003" ), QStringLiteral( "EPSG:4951" ) }, { QStringLiteral( "EUREF89" ), QStringLiteral( "EPSG:4937" ) }, { QStringLiteral( "GDA2020" ), QStringLiteral( "EPSG:7843" ) }, - { QStringLiteral( "GDA94" ), QStringLiteral( "4939" ) }, + { QStringLiteral( "GDA94" ), QStringLiteral( "EPSG:4939" ) }, { QStringLiteral( "ISN2016" ), QStringLiteral( "EPSG:8085" ) }, + { QStringLiteral( "ITRF1993" ), QStringLiteral( "EPSG:7905" ) }, { QStringLiteral( "ITRF2000" ), QStringLiteral( "EPSG:7909" ) }, { QStringLiteral( "ITRF2005" ), QStringLiteral( "EPSG:7910" ) }, { QStringLiteral( "ITRF2008" ), QStringLiteral( "EPSG:7911" ) }, { QStringLiteral( "ITRF2008-India-CORS" ), QStringLiteral( "EPSG:8999" ) }, { QStringLiteral( "ITRF2008-Mexico" ), QStringLiteral( "EPSG:6364" ) }, { QStringLiteral( "ITRF2014" ), QStringLiteral( "EPSG:7912" ) }, + { QStringLiteral( "ITRF2020" ), QStringLiteral( "EPSG:9989" ) }, { QStringLiteral( "JGD2000" ), QStringLiteral( "EPSG:4947" ) }, { QStringLiteral( "JGD2011" ), QStringLiteral( "EPSG:6667" ) }, { QStringLiteral( "KGD2002" ), QStringLiteral( "EPSG:4927" ) }, @@ -61,14 +65,22 @@ const QHash TRIMBLE_REFERENCE_FRAMES = { QStringLiteral( "LKS-92" ), QStringLiteral( "EPSG:4949" ) }, { QStringLiteral( "MAGNA-SIRGAS" ), QStringLiteral( "EPSG:4997" ) }, { QStringLiteral( "MAGNA-SIRGAS(2018)" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "MTRF-2000" ), QStringLiteral( "EPSG:8817" ) }, { QStringLiteral( "NAD83(2011) (EPOCH:2010)" ), QStringLiteral( "EPSG:6319" ) }, + { QStringLiteral( "NAD83(2011) (EPOCH:2017.5)" ), QStringLiteral( "EPSG:6319" ) }, { QStringLiteral( "NAD83(CORS96) (EPOCH:2002)" ), QStringLiteral( "EPSG:6782" ) }, - { QStringLiteral( "NAD83(CSRS)v7" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v7 (EPOCH:1997)" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v7 (EPOCH:2002)" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v7 (EPOCH:2010)" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v8 (EPOCH:1997)" ), QStringLiteral( "EPSG:10413" ) }, + { QStringLiteral( "NAD83(CSRS)v8 (EPOCH:2002)" ), QStringLiteral( "EPSG:10413" ) }, + { QStringLiteral( "NAD83(CSRS)v8 (EPOCH:2010)" ), QStringLiteral( "EPSG:10413" ) }, { QStringLiteral( "NAD83(MA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6324" ) }, { QStringLiteral( "NAD83(PA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6321" ) }, { QStringLiteral( "NZGD2000" ), QStringLiteral( "EPSG:4959" ) }, { QStringLiteral( "OSNetv2009" ), QStringLiteral( "EPSG:4277" ) }, // TODO: create 3D CRS here - { QStringLiteral( "POSGAR07" ), QStringLiteral( "5342" ) }, + { QStringLiteral( "POSGAR07" ), QStringLiteral( "EPSG:5342" ) }, + { QStringLiteral( "PZ-90.11" ), QStringLiteral( "EPSG:7680" ) }, { QStringLiteral( "RDN2008" ), QStringLiteral( "EPSG:6705" ) }, { QStringLiteral( "RGAF09" ), QStringLiteral( "EPSG:5488" ) }, { QStringLiteral( "RGF93v2b" ), QStringLiteral( "EPSG:9781" ) }, @@ -79,8 +91,11 @@ const QHash TRIMBLE_REFERENCE_FRAMES = { QStringLiteral( "SIRGAS-Chile 2016" ), QStringLiteral( "EPSG:9152" ) }, { QStringLiteral( "SIRGAS-Chile 2021" ), QStringLiteral( "EPSG:20040" ) }, { QStringLiteral( "SIRGAS-CON" ), QStringLiteral( "EPSG:4989" ) }, + { QStringLiteral( "SIRGAS-CON SIR17P01" ), QStringLiteral( "EPSG:8946" ) }, + { QStringLiteral( "SIRGAS-ROU98" ), QStringLiteral( "EPSG:5380" ) }, + { QStringLiteral( "SIRGAS2000" ), QStringLiteral( "EPSG:4989" ) }, { QStringLiteral( "SWEREF99" ), QStringLiteral( "EPSG:4977" ) }, - { QStringLiteral( "WGS84(G1762) current" ), QStringLiteral( "EPSG:4979" ) }, + { QStringLiteral( "WGS84 (current)" ), QStringLiteral( "EPSG:4979" ) }, }; TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) From e7a4b3c40e4ac423af983fc6e359a9239b44c8b2 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Sun, 9 Aug 2026 23:58:23 +0200 Subject: [PATCH 20/21] Remove LSApplicationQueriesSchemes as it seems unnecessary --- cmake_templates/iOSInfo.plist.in | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cmake_templates/iOSInfo.plist.in b/cmake_templates/iOSInfo.plist.in index 9edbae0e0..f85db7a9f 100644 --- a/cmake_templates/iOSInfo.plist.in +++ b/cmake_templates/iOSInfo.plist.in @@ -37,16 +37,6 @@ - LSApplicationQueriesSchemes - - tmmregister - trimblelink - trimbleMobileManager - com.trimble.mobilemanager - trimble-mobile-manager - trimble - TrimbleMobileManager - NSCameraUsageDescription Program requires access to camera to take pictures for features on map NSLocationAlwaysAndWhenInUseUsageDescription From 355f2e0080d843a12380af0a46b5709f0d3f0962 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 10 Aug 2026 00:21:40 +0200 Subject: [PATCH 21/21] Fix antenna height value formatting for null value --- app/qml/settings/MMSettingsPage.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index ac5377e9e..761b824a6 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -118,7 +118,7 @@ MMPage { width: parent.width title: qsTr("GPS antenna height") description: qsTr("Click here to modify in Trimble Mobile Manager") - value: __inputUtils.formatNumber( PositionKit.antennaHeight, 3 ) + " m" + value: Number.isNaN( PositionKit.antennaHeight ) ? qsTr( "N/A" ) : __inputUtils.formatNumber( PositionKit.antennaHeight, 3 ) + " m" onClicked: () => { if ( PositionKit.positionProvider ) PositionKit.openAntennaHeightPage()