diff --git a/images/images.qrc b/images/images.qrc index 0cb5c552dc..41d98e4586 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -529,5 +529,6 @@ themes/qfield/nodpi/ic_arrow_drop_down_48dp.svg themes/qfield/nodpi/ic_arrow_drop_up_48dp.svg themes/qfield/nodpi/ic_shutdown_24dp.svg + themes/qfield/nodpi/ic_egeniouss_receiver_black_24dp.svg diff --git a/images/themes/qfield/nodpi/ic_egeniouss_receiver_black_24dp.svg b/images/themes/qfield/nodpi/ic_egeniouss_receiver_black_24dp.svg new file mode 100644 index 0000000000..f0cff592c2 --- /dev/null +++ b/images/themes/qfield/nodpi/ic_egeniouss_receiver_black_24dp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 2328f5d004..24fb44288e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -29,6 +29,7 @@ set(QFIELD_CORE_SRCS positioning/gnsspositioninformation.cpp positioning/internalgnssreceiver.cpp positioning/nmeagnssreceiver.cpp + positioning/egenioussreceiver.cpp positioning/tcpreceiver.cpp positioning/udpreceiver.cpp positioning/positioning.cpp @@ -150,6 +151,7 @@ set(QFIELD_CORE_HDRS positioning/positioningdevicemodel.h positioning/internalgnssreceiver.h positioning/nmeagnssreceiver.h + positioning/egenioussreceiver.h positioning/tcpreceiver.h positioning/udpreceiver.h positioning/geofencer.h diff --git a/src/core/pluginmanager.cpp b/src/core/pluginmanager.cpp index 9c2c48cd09..ab203f2b95 100644 --- a/src/core/pluginmanager.cpp +++ b/src/core/pluginmanager.cpp @@ -138,22 +138,28 @@ void PluginManager::handleWarnings( const QList &warnings ) void PluginManager::grantRequestedPluginPermission( bool permanent ) { + QSettings settings; + QString pluginKey = mPermissionRequestPluginPath; + pluginKey.replace( QChar( '/' ), QChar( '_' ) ); + settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( pluginKey ) ); + const QString uuid = settings.value( QStringLiteral( "uuid" ) ).toString(); + if ( permanent ) { - QSettings settings; - QString pluginKey = mPermissionRequestPluginPath; - pluginKey.replace( QChar( '/' ), QChar( '_' ) ); - settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( pluginKey ) ); settings.setValue( QStringLiteral( "permissionGranted" ), true ); - if ( !settings.value( QStringLiteral( "uuid" ) ).toString().isEmpty() ) + if ( !uuid.isEmpty() ) { settings.setValue( QStringLiteral( "userEnabled" ), true ); } - settings.endGroup(); } + settings.endGroup(); loadPlugin( mPermissionRequestPluginPath, QString(), true ); mPermissionRequestPluginPath.clear(); + if ( !uuid.isEmpty() ) + { + callPluginMethod( uuid, "appWideEnabled" ); + } } void PluginManager::denyRequestedPluginPermission( bool permanent ) @@ -272,10 +278,11 @@ void PluginManager::enableAppPlugin( const QString &uuid ) { if ( mAvailableAppPlugins.contains( uuid ) ) { - if ( !mLoadedPlugins.contains( mAvailableAppPlugins[uuid].path() ) ) + const QString pluginPath = mAvailableAppPlugins[uuid].path(); + if ( !mLoadedPlugins.contains( pluginPath ) ) { QSettings settings; - QString pluginKey = mAvailableAppPlugins[uuid].path(); + QString pluginKey = pluginPath; pluginKey.replace( QChar( '/' ), QChar( '_' ) ); settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( pluginKey ) ); settings.setValue( QStringLiteral( "uuid" ), uuid ); @@ -285,13 +292,19 @@ void PluginManager::enableAppPlugin( const QString &uuid ) } settings.endGroup(); - loadPlugin( mAvailableAppPlugins[uuid].path(), mAvailableAppPlugins[uuid].name() ); + loadPlugin( pluginPath, mAvailableAppPlugins[uuid].name() ); + + if ( mLoadedPlugins.contains( pluginPath ) ) + { + callPluginMethod( uuid, "appWideEnabled" ); + } } } } void PluginManager::disableAppPlugin( const QString &uuid ) { + callPluginMethod( uuid, "appWideDisabled" ); if ( mAvailableAppPlugins.contains( uuid ) ) { if ( mLoadedPlugins.contains( mAvailableAppPlugins[uuid].path() ) ) @@ -455,3 +468,27 @@ QString PluginManager::findProjectPlugin( const QString &projectPath ) } return QString(); } + +void PluginManager::callPluginMethod( const QString &uuid, const QString &methodName ) +{ + if ( !mAvailableAppPlugins.contains( uuid ) ) + { + return; + } + + const QString pluginPath = mAvailableAppPlugins[uuid].path(); + if ( !mLoadedPlugins.contains( pluginPath ) ) + { + return; + } + + const QPointer object = mLoadedPlugins[pluginPath]; + + const char *normalizedSignature = QMetaObject::normalizedSignature( ( methodName + "()" ).toStdString().c_str() ); + const int methodIndex = object->metaObject()->indexOfSlot( normalizedSignature ); + + if ( methodIndex != -1 ) + { + QMetaObject::invokeMethod( object.data(), methodName.toStdString().c_str() ); + } +} diff --git a/src/core/pluginmanager.h b/src/core/pluginmanager.h index 7be7e30b0d..2d6375ecfe 100644 --- a/src/core/pluginmanager.h +++ b/src/core/pluginmanager.h @@ -112,8 +112,10 @@ class PluginManager : public QObject void installProgress( double progress ); void installEnded( const QString &uuid = QString(), const QString &error = QString() ); + private slots: void handleWarnings( const QList &warnings ); + void callPluginMethod( const QString &uuid, const QString &methodName ); private: QQmlEngine *mEngine = nullptr; diff --git a/src/core/positioning/abstractgnssreceiver.h b/src/core/positioning/abstractgnssreceiver.h index 84cb6b2e20..0688ea1e03 100644 --- a/src/core/positioning/abstractgnssreceiver.h +++ b/src/core/positioning/abstractgnssreceiver.h @@ -72,6 +72,7 @@ class AbstractGnssReceiver : public QObject private: friend class InternalGnssReceiver; + friend class EgenioussReceiver; friend class NmeaGnssReceiver; friend class BluetoothReceiver; friend class TcpReceiver; diff --git a/src/core/positioning/egenioussreceiver.cpp b/src/core/positioning/egenioussreceiver.cpp new file mode 100644 index 0000000000..70266bb154 --- /dev/null +++ b/src/core/positioning/egenioussreceiver.cpp @@ -0,0 +1,141 @@ +#include "egenioussreceiver.h" + +#include +#include +#include + +EgenioussReceiver::EgenioussReceiver( QObject *parent ) + : AbstractGnssReceiver( parent ), mTcpSocket( new QTcpSocket() ) +{ + connect( mTcpSocket, &QTcpSocket::readyRead, this, &EgenioussReceiver::onReadyRead ); + connect( mTcpSocket, &QTcpSocket::errorOccurred, this, &EgenioussReceiver::handleError ); + connect( mTcpSocket, &QTcpSocket::connected, this, &EgenioussReceiver::connected ); + connect( mTcpSocket, &QTcpSocket::disconnected, this, &EgenioussReceiver::disconnected ); + setValid( true ); +} + +void EgenioussReceiver::handleConnectDevice() +{ + mTcpSocket->connectToHost( mAddress, mPort, QTcpSocket::ReadWrite ); +} + +void EgenioussReceiver::connected() +{ + mSocketState = QAbstractSocket::ConnectedState; + mSocketStateString = tr( "Successfully connected" ); + emit socketStateChanged( mSocketState ); + setValid( true ); +} + +void EgenioussReceiver::handleDisconnectDevice() +{ + mTcpSocket->disconnectFromHost(); +} + +void EgenioussReceiver::disconnected() +{ + if ( mTcpSocket->state() == QAbstractSocket::ConnectedState ) + { + mSocketState = QAbstractSocket::UnconnectedState; + mSocketStateString = tr( "Disconnected" ); + emit socketStateChanged( mSocketState ); + } +} + +QList> EgenioussReceiver::details() +{ + QList> detailsList; + + if ( mPayload.isEmpty() ) + { + return detailsList; + } + + detailsList.append( qMakePair( "q", mPayload.value( "q" ).toDouble() ) ); + + return detailsList; +} + +void EgenioussReceiver::onReadyRead() +{ + const int minimumDataSize = 9; + const uint8_t validStartByte = 0xFE; + const int payloadHeaderSize = 8; + + QByteArray mReceivedData = mTcpSocket->readAll(); + if ( mReceivedData.size() < minimumDataSize ) + { + mLastError = tr( "Received data is too short to process" ); + emit lastErrorChanged( mLastError ); + return; + } + if ( static_cast( mReceivedData[0] ) != validStartByte ) + { + mLastError = tr( "Invalid start byte" ); + emit lastErrorChanged( mLastError ); + return; + } + uint32_t payloadLength; + QDataStream dataStream( mReceivedData.mid( 4, 4 ) ); + dataStream.setByteOrder( QDataStream::LittleEndian ); + dataStream >> payloadLength; + if ( mReceivedData.size() < payloadHeaderSize + payloadLength ) + { + mLastError = tr( "Received data is too short to contain the payload" ); + emit lastErrorChanged( mLastError ); + return; + } + QJsonDocument jsonDoc = QJsonDocument::fromJson( mReceivedData.mid( payloadHeaderSize, payloadLength ) ); + if ( jsonDoc.isNull() || !jsonDoc.isObject() ) + { + mLastError = tr( "Failed to parse JSON" ); + emit lastErrorChanged( mLastError ); + return; + } + mPayload = jsonDoc.object(); + const double latitude = mPayload.value( "lat" ).toDouble() == 0 ? std::numeric_limits::quiet_NaN() : mPayload.value( "lat" ).toDouble(); + const double longitude = mPayload.value( "lon" ).toDouble() == 0 ? std::numeric_limits::quiet_NaN() : mPayload.value( "lon" ).toDouble(); + const double elevation = mPayload.value( "alt" ).toDouble() == 0 ? std::numeric_limits::quiet_NaN() : mPayload.value( "alt" ).toDouble(); + mLastGnssPositionInformation = GnssPositionInformation( + latitude, + longitude, + elevation, + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), + QList(), + 0, + 0, + 0, + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), + QDateTime::fromMSecsSinceEpoch( mPayload.value( "utc" ).toDouble() / 1e6, Qt::UTC ), + QChar(), + 0, + 1 ); + + emit lastGnssPositionInformationChanged( mLastGnssPositionInformation ); +} + +void EgenioussReceiver::handleError( QAbstractSocket::SocketError error ) +{ + switch ( error ) + { + case QAbstractSocket::HostNotFoundError: + mLastError = tr( "Could not find the remote host" ); + break; + case QAbstractSocket::NetworkError: + mLastError = tr( "Attempt to read or write from socket returned an error" ); + break; + case QAbstractSocket::ConnectionRefusedError: + mLastError = tr( "The connection was refused by the remote host" ); + break; + default: + mLastError = tr( "TCP receiver error (%1)" ).arg( QMetaEnum::fromType().valueToKey( error ) ); + break; + } + mSocketState = QAbstractSocket::UnconnectedState; + mSocketStateString = mLastError; + qInfo() << QStringLiteral( "EgenioussReceiver: Error: %1" ).arg( mLastError ); + + emit lastErrorChanged( mLastError ); +} diff --git a/src/core/positioning/egenioussreceiver.h b/src/core/positioning/egenioussreceiver.h new file mode 100644 index 0000000000..d4de3c10a7 --- /dev/null +++ b/src/core/positioning/egenioussreceiver.h @@ -0,0 +1,37 @@ +#ifndef EGENIOUSSRECEIVER_H +#define EGENIOUSSRECEIVER_H + +#include "abstractgnssreceiver.h" + +#include +#include + +class EgenioussReceiver : public AbstractGnssReceiver +{ + Q_OBJECT + + public: + explicit EgenioussReceiver( QObject *parent = nullptr ); + + private: + void handleConnectDevice() override; + void handleDisconnectDevice() override; + QList> details() override; + + private slots: + void onReadyRead(); + void handleError( QAbstractSocket::SocketError error ); + + private: + void processReceivedData(); + void connected(); + void disconnected(); + + private: + QTcpSocket *mTcpSocket = nullptr; + QJsonObject mPayload; + const QHostAddress::SpecialAddress mAddress = QHostAddress::LocalHost; + const int mPort = 1235; +}; + +#endif // EGENIOUSSRECEIVER_H diff --git a/src/core/positioning/internalgnssreceiver.cpp b/src/core/positioning/internalgnssreceiver.cpp index c7413af275..b99e372bb9 100644 --- a/src/core/positioning/internalgnssreceiver.cpp +++ b/src/core/positioning/internalgnssreceiver.cpp @@ -104,7 +104,7 @@ void InternalGnssReceiver::handleConnectDevice() Qt::PermissionStatus permissionStatus = qApp->checkPermission( locationPermission ); if ( permissionStatus == Qt::PermissionStatus::Undetermined ) { - qApp->requestPermission( locationPermission, [=]( const QPermission &permission ) { + qApp->requestPermission( locationPermission, this, [=]( const QPermission &permission ) { if ( permission.status() == Qt::PermissionStatus::Granted ) { mPermissionChecked = true; diff --git a/src/core/positioning/positioning.cpp b/src/core/positioning/positioning.cpp index 94e46a951d..49d010ad6e 100644 --- a/src/core/positioning/positioning.cpp +++ b/src/core/positioning/positioning.cpp @@ -20,6 +20,7 @@ #ifdef WITH_SERIALPORT #include "serialportreceiver.h" #endif +#include "egenioussreceiver.h" #include "internalgnssreceiver.h" #include "positioning.h" #include "positioningutils.h" @@ -205,6 +206,10 @@ void Positioning::setupDevice() const int port = mDeviceId.mid( portSeparator + 1 ).toInt(); mReceiver = new UdpReceiver( address, port, this ); } + else if ( mDeviceId.startsWith( QStringLiteral( "egeniouss:" ) ) ) + { + mReceiver = new EgenioussReceiver( this ); + } #ifdef WITH_SERIALPORT else if ( mDeviceId.startsWith( QStringLiteral( "serial:" ) ) ) { diff --git a/src/core/positioning/positioning.h b/src/core/positioning/positioning.h index 0c8a2ba322..11c4036fda 100644 --- a/src/core/positioning/positioning.h +++ b/src/core/positioning/positioning.h @@ -201,7 +201,6 @@ class Positioning : public QObject void setLogging( bool logging ); signals: - void activeChanged(); void deviceIdChanged(); void deviceChanged(); diff --git a/src/core/positioning/positioningdevicemodel.cpp b/src/core/positioning/positioningdevicemodel.cpp index 83ba568868..553f5e22b6 100644 --- a/src/core/positioning/positioningdevicemodel.cpp +++ b/src/core/positioning/positioningdevicemodel.cpp @@ -171,6 +171,9 @@ const QString PositioningDeviceModel::deviceId( const Device &device ) const case SerialPortDevice: return QStringLiteral( "serial:%1" ).arg( device.settings.value( QStringLiteral( "address" ) ).toString() ); + + case EgenioussDevice: + return QStringLiteral( "egeniouss:" ); } return QString(); diff --git a/src/core/positioning/positioningdevicemodel.h b/src/core/positioning/positioningdevicemodel.h index c358ca7fb2..4130c4da85 100644 --- a/src/core/positioning/positioningdevicemodel.h +++ b/src/core/positioning/positioningdevicemodel.h @@ -29,6 +29,7 @@ class PositioningDeviceModel : public QAbstractListModel BluetoothDevice, TcpDevice, UdpDevice, + EgenioussDevice, SerialPortDevice, }; Q_ENUM( Type ) diff --git a/src/core/positioning/positioninginformationmodel.h b/src/core/positioning/positioninginformationmodel.h index d574c0facd..570e8b8088 100644 --- a/src/core/positioning/positioninginformationmodel.h +++ b/src/core/positioning/positioninginformationmodel.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef POSITIONINGINFORMATIONMODEL_H +#define POSITIONINGINFORMATIONMODEL_H #include "positioning.h" @@ -118,3 +119,5 @@ class PositioningInformationModel : public QStandardItemModel QgsCoordinateReferenceSystem mCoordinateDisplayCrs; QMetaObject::Connection positioningSourceConnection; }; + +#endif // POSITIONINGINFORMATIONMODEL_H diff --git a/src/core/positioning/tcpreceiver.cpp b/src/core/positioning/tcpreceiver.cpp index 072e7de5b9..0c88b3df3b 100644 --- a/src/core/positioning/tcpreceiver.cpp +++ b/src/core/positioning/tcpreceiver.cpp @@ -119,7 +119,7 @@ void TcpReceiver::handleError( QAbstractSocket::SocketError error ) mLastError = tr( "The connection was refused by the remote host" ); break; default: - mLastError = tr( "UDP receiver error (%1)" ).arg( QMetaEnum::fromType().valueToKey( error ) ); + mLastError = tr( "TCP receiver error (%1)" ).arg( QMetaEnum::fromType().valueToKey( error ) ); break; } qInfo() << QStringLiteral( "TcpReceiver: Error: %1" ).arg( mLastError ); diff --git a/src/qml/EgenioussDeviceChooser.qml b/src/qml/EgenioussDeviceChooser.qml new file mode 100644 index 0000000000..74b2980a0f --- /dev/null +++ b/src/qml/EgenioussDeviceChooser.qml @@ -0,0 +1,21 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.qfield +import Theme + +Item { + width: parent.width + + function generateName() { + return "Egeniouss"; + } + + function setSettings(settings) { + // nothing to save! + } + + function getSettings() { + return {}; + } +} diff --git a/src/qml/PositioningDeviceSettings.qml b/src/qml/PositioningDeviceSettings.qml index a9b3d97e47..d6915faa7b 100644 --- a/src/qml/PositioningDeviceSettings.qml +++ b/src/qml/PositioningDeviceSettings.qml @@ -41,6 +41,20 @@ Popup { return positioningDeviceItem.item.getSettings(); } + function handleEgenioussChange() { + if (positioningSettings.egenioussEnabled) { + positioningDeviceTypeModel.insert(0, { + "name": qsTr('Egeniouss'), + "value": PositioningDeviceModel.EgenioussDevice + }); + } else { + positioningDeviceTypeModel.remove(0, 1); + positioningDeviceModel.removeDevice("Egeniouss"); + positioningDeviceComboBox.currentIndex = 0; + } + positioningDeviceType.model = positioningDeviceTypeModel; + } + Component.onCompleted: { if (withBluetooth) { positioningDeviceTypeModel.insert(0, { @@ -54,7 +68,14 @@ Popup { "value": PositioningDeviceModel.SerialPortDevice }); } + if (positioningSettings.egenioussEnabled) { + positioningDeviceTypeModel.insert(0, { + "name": qsTr('Egeniouss'), + "value": PositioningDeviceModel.EgenioussDevice + }); + } positioningDeviceType.model = positioningDeviceTypeModel; + positioningSettings.onEgenioussEnabledChanged.connect(handleEgenioussChange); } Page { @@ -95,7 +116,7 @@ Popup { id: positioningDeviceName Layout.fillWidth: true font: Theme.defaultFont - placeholderText: displayText == '' ? qsTr('Leave empty to auto-fill') : '' + placeholderText: displayText === '' ? qsTr('Leave empty to auto-fill') : '' } Label { @@ -130,6 +151,8 @@ Popup { return Theme.getThemeVectorIcon('ic_udp_receiver_black_24dp'); case PositioningDeviceModel.SerialPortDevice: return Theme.getThemeVectorIcon('ic_serial_port_receiver_black_24dp'); + case PositioningDeviceModel.EgenioussDevice: + return Theme.getThemeVectorIcon('ic_egeniouss_receiver_black_24dp'); } return ''; } @@ -154,6 +177,8 @@ Popup { return Theme.getThemeVectorIcon('ic_udp_receiver_black_24dp'); case PositioningDeviceModel.SerialPortDevice: return Theme.getThemeVectorIcon('ic_serial_port_receiver_black_24dp'); + case PositioningDeviceModel.EgenioussDevice: + return Theme.getThemeVectorIcon('ic_egeniouss_receiver_black_24dp'); } return ''; } @@ -193,6 +218,8 @@ Popup { return "qrc:/qml/UdpDeviceChooser.qml"; case PositioningDeviceModel.SerialPortDevice: return "qrc:/qml/SerialPortDeviceChooser.qml"; + case PositioningDeviceModel.EgenioussDevice: + return "qrc:/qml/EgenioussDeviceChooser.qml"; } return ''; } diff --git a/src/qml/PositioningInformationView.qml b/src/qml/PositioningInformationView.qml index d69454f7ae..5058e2b42a 100644 --- a/src/qml/PositioningInformationView.qml +++ b/src/qml/PositioningInformationView.qml @@ -35,7 +35,7 @@ Rectangle { id: grid readonly property real numberOfColumns: parent.width > 1000 ? 6 : parent.width > 620 ? 3 : 2 - readonly property real numberOfRows: grid.count / numberOfColumns + readonly property real numberOfRows: Math.ceil(grid.count / numberOfColumns) flow: GridView.FlowTopToBottom boundsBehavior: Flickable.StopAtBounds @@ -44,15 +44,15 @@ Rectangle { distanceUnits: projectInfo.distanceUnits coordinateDisplayCrs: projectInfo.coordinateDisplayCrs } - height: numberOfRows * cellHeight + height: grid.numberOfRows * cellHeight width: parent.width cellHeight: positioningInformationView.cellHeight cellWidth: parent.width / numberOfColumns clip: true delegate: Rectangle { - readonly property real currentColumn: parseInt(index / (grid.count / grid.numberOfColumns)) - readonly property real currentRow: index % (grid.count / grid.numberOfColumns) + readonly property real currentColumn: parseInt(index / grid.numberOfRows) + readonly property real currentRow: index % grid.numberOfRows width: grid.cellWidth height: grid.cellHeight diff --git a/src/qml/PositioningSettings.qml b/src/qml/PositioningSettings.qml index 87c0a5b4e2..614c988edd 100644 --- a/src/qml/PositioningSettings.qml +++ b/src/qml/PositioningSettings.qml @@ -45,4 +45,5 @@ Settings { property int digitizingMeasureType: 1 property bool geofencingPreventDigitizingDuringAlert: false + property bool egenioussEnabled: false } diff --git a/src/qml/QFieldSettings.qml b/src/qml/QFieldSettings.qml index 35c191704f..180a914336 100644 --- a/src/qml/QFieldSettings.qml +++ b/src/qml/QFieldSettings.qml @@ -871,6 +871,8 @@ Page { return Theme.getThemeVectorIcon('ic_udp_receiver_black_24dp'); case PositioningDeviceModel.SerialPortDevice: return Theme.getThemeVectorIcon('ic_serial_port_receiver_black_24dp'); + case PositioningDeviceModel.EgenioussDevice: + return Theme.getThemeVectorIcon('ic_egeniouss_receiver_black_24dp'); } return ''; } @@ -897,6 +899,8 @@ Page { return Theme.getThemeVectorIcon('ic_udp_receiver_black_24dp'); case PositioningDeviceModel.SerialPortDevice: return Theme.getThemeVectorIcon('ic_serial_port_receiver_black_24dp'); + case PositioningDeviceModel.EgenioussDevice: + return Theme.getThemeVectorIcon('ic_egeniouss_receiver_black_24dp'); } return ''; } diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 7626ec4903..2cece6ee17 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -243,6 +243,7 @@ ApplicationWindow { PositioningSettings { id: positioningSettings + objectName: "positioningSettings" onPositioningActivatedChanged: { if (positioningActivated) { diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc index 63836d9ba2..ddcafd53dc 100644 --- a/src/qml/qml.qrc +++ b/src/qml/qml.qrc @@ -43,6 +43,7 @@ ScaleBar.qml SensorInformationView.qml Toast.qml + EgenioussDeviceChooser.qml TcpDeviceChooser.qml UdpDeviceChooser.qml VariableEditor.qml