From c27ea6d5d7e4ddb00c4c4f6ef76c2a2259cf836b Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 11:45:37 +0100 Subject: [PATCH 01/14] VOR localizer: Get it working on Qt6. --- plugins/feature/vorlocalizer/map.qrc | 2 + .../vorlocalizer/map/ModifiedMapView.qml | 178 ++++++++++++++++++ plugins/feature/vorlocalizer/map/map_6.qml | 156 +++++++++++++++ .../feature/vorlocalizer/vorlocalizergui.cpp | 9 + 4 files changed, 345 insertions(+) create mode 100644 plugins/feature/vorlocalizer/map/ModifiedMapView.qml create mode 100644 plugins/feature/vorlocalizer/map/map_6.qml diff --git a/plugins/feature/vorlocalizer/map.qrc b/plugins/feature/vorlocalizer/map.qrc index f5985ce53..1fec0ef98 100644 --- a/plugins/feature/vorlocalizer/map.qrc +++ b/plugins/feature/vorlocalizer/map.qrc @@ -1,7 +1,9 @@ map/map.qml + map/map_6.qml map/MapStation.qml + map/ModifiedMapView.qml map/antenna.png map/VOR.png map/VOR-DME.png diff --git a/plugins/feature/vorlocalizer/map/ModifiedMapView.qml b/plugins/feature/vorlocalizer/map/ModifiedMapView.qml new file mode 100644 index 000000000..2aa266940 --- /dev/null +++ b/plugins/feature/vorlocalizer/map/ModifiedMapView.qml @@ -0,0 +1,178 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import QtQuick +import QtLocation as QL +import QtPositioning as QP +import Qt.labs.animation +/*! + \qmltype MapView + \inqmlmodule QtLocation + \brief An interactive map viewer component. + + MapView wraps a Map and adds the typical interactive features: + changing the zoom level, panning and tilting the map. + + The implementation is a QML assembly of smaller building blocks that are + available separately. In case you want to make changes in your own version + of this component, you can copy the QML, which is installed into the + \c qml/QtLocation module directory, and modify it as needed. + + \sa Map +*/ +Item { + /*! + \qmlproperty Map MapView::map + + This property provides access to the underlying Map instance. + */ + property alias map: map + + /*! + \qmlproperty real minimumZoomLevel + + The minimum zoom level according to the size of the view. + + \sa Map::minimumZoomLevel + */ + property real minimumZoomLevel: map.minimumZoomLevel + + /*! + \qmlproperty real maximumZoomLevel + + The maximum valid zoom level for the map. + + \sa Map::maximumZoomLevel + */ + property real maximumZoomLevel: map.maximumZoomLevel + + // -------------------------------- + // implementation + id: root + Component.onCompleted: map.resetPinchMinMax() + + QL.Map { + id: map + width: parent.width + height: parent.height + tilt: tiltHandler.persistentTranslation.y / -5 + property bool pinchAdjustingZoom: false + + BoundaryRule on zoomLevel { + id: br + minimum: map.minimumZoomLevel + maximum: map.maximumZoomLevel + } + + onZoomLevelChanged: { + br.returnToBounds(); + if (!pinchAdjustingZoom) resetPinchMinMax() + } + + function resetPinchMinMax() { + pinch.persistentScale = 1 + pinch.scaleAxis.minimum = Math.pow(2, root.minimumZoomLevel - map.zoomLevel + 1) + pinch.scaleAxis.maximum = Math.pow(2, root.maximumZoomLevel - map.zoomLevel - 1) + } + + PinchHandler { + id: pinch + target: null + property real rawBearing: 0 + property QP.geoCoordinate startCentroid + onActiveChanged: if (active) { + flickAnimation.stop() + pinch.startCentroid = map.toCoordinate(pinch.centroid.position, false) + } else { + flickAnimation.restart(centroid.velocity) + map.resetPinchMinMax() + } + onScaleChanged: (delta) => { + map.pinchAdjustingZoom = true + map.zoomLevel += Math.log2(delta) + map.alignCoordinateToPoint(pinch.startCentroid, pinch.centroid.position) + map.pinchAdjustingZoom = false + } + onRotationChanged: (delta) => { + pinch.rawBearing -= delta + // snap to 0° if we're close enough + map.bearing = (Math.abs(pinch.rawBearing) < 5) ? 0 : pinch.rawBearing + map.alignCoordinateToPoint(pinch.startCentroid, pinch.centroid.position) + } + grabPermissions: PointerHandler.TakeOverForbidden + } + WheelHandler { + id: wheel + // workaround for QTBUG-87646 / QTBUG-112394 / QTBUG-112432: + // Magic Mouse pretends to be a trackpad but doesn't work with PinchHandler + // and we don't yet distinguish mice and trackpads on Wayland either + acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" + ? PointerDevice.Mouse | PointerDevice.TouchPad + : PointerDevice.Mouse + onWheel: (event) => { + const loc = map.toCoordinate(wheel.point.position) + switch (event.modifiers) { + case Qt.NoModifier: + // jonb - Changed to make more like Qt5 + //map.zoomLevel += event.angleDelta.y / 120 + map.zoomLevel += event.angleDelta.y / 1000 + break + case Qt.ShiftModifier: + map.bearing += event.angleDelta.y / 15 + break + case Qt.ControlModifier: + map.tilt += event.angleDelta.y / 15 + break + } + map.alignCoordinateToPoint(loc, wheel.point.position) + } + } + DragHandler { + id: drag + signal flickStarted // for autotests only + signal flickEnded + target: null + onTranslationChanged: (delta) => map.pan(-delta.x, -delta.y) + onActiveChanged: if (active) { + flickAnimation.stop() + } else { + flickAnimation.restart(centroid.velocity) + } + } + + property vector3d animDest + onAnimDestChanged: if (flickAnimation.running) { + const delta = Qt.vector2d(animDest.x - flickAnimation.animDestLast.x, animDest.y - flickAnimation.animDestLast.y) + map.pan(-delta.x, -delta.y) + flickAnimation.animDestLast = animDest + } + + Vector3dAnimation on animDest { + id: flickAnimation + property vector3d animDestLast + from: Qt.vector3d(0, 0, 0) + duration: 500 + easing.type: Easing.OutQuad + onStarted: drag.flickStarted() + onStopped: drag.flickEnded() + + function restart(vel) { + stop() + map.animDest = Qt.vector3d(0, 0, 0) + animDestLast = Qt.vector3d(0, 0, 0) + to = Qt.vector3d(vel.x / duration * 100, vel.y / duration * 100, 0) + start() + } + } + + DragHandler { + id: tiltHandler + minimumPointCount: 2 + maximumPointCount: 2 + target: null + xAxis.enabled: false + grabPermissions: PointerHandler.TakeOverForbidden + onActiveChanged: if (active) flickAnimation.stop() + } + } +} diff --git a/plugins/feature/vorlocalizer/map/map_6.qml b/plugins/feature/vorlocalizer/map/map_6.qml new file mode 100644 index 000000000..a0e60095e --- /dev/null +++ b/plugins/feature/vorlocalizer/map/map_6.qml @@ -0,0 +1,156 @@ +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtLocation 6.5 +import QtPositioning 6.5 + +Item { + id: qmlMap + property int vorZoomLevel: 11 + property string mapProvider: "osm" + property variant mapPtr + property string requestedMapType + property variant guiPtr + + function createMap(pluginParameters, requestedMap, gui) { + requestedMapType = requestedMap + guiPtr = gui + + var paramString = "" + for (var prop in pluginParameters) { + var parameter = 'PluginParameter { name: "' + prop + '"; value: "' + pluginParameters[prop] + '"}' + paramString = paramString + parameter + } + var pluginString = 'import QtLocation 6.5; Plugin{ name:"' + mapProvider + '"; ' + paramString + '}' + var plugin = Qt.createQmlObject (pluginString, qmlMap) + + if (mapPtr) { + // Objects aren't destroyed immediately, so don't call findChild("map") + mapPtr.destroy() + mapPtr = null + } + mapPtr = actualMapComponent.createObject(page) + mapPtr.map.plugin = plugin + mapPtr.map.forceActiveFocus() + return mapPtr + } + + Item { + id: page + anchors.fill: parent + } + + Component { + id: actualMapComponent + + ModifiedMapView { + id: mapView + objectName: "mapView" + anchors.fill: parent + map.center: QtPositioning.coordinate(51.5, 0.125) // London + map.zoomLevel: 10 + map.objectName: "map" + // not in 6 + //gesture.enabled: true + //gesture.acceptedGestures: MapGestureArea.PinchGesture | MapGestureArea.PanGesture + + MapItemView { + model: vorModel + delegate: vorRadialComponent + parent: mapView.map + } + + MapStation { + id: station + objectName: "station" + stationName: "Home" + } + + MapItemView { + model: vorModel + delegate: vorComponent + parent: mapView.map + } + + map.onZoomLevelChanged: { + if (map.zoomLevel > 11) { + station.zoomLevel = map.zoomLevel + vorZoomLevel = map.zoomLevel + } else { + station.zoomLevel = 11 + vorZoomLevel = 11 + } + } + + map.onSupportedMapTypesChanged : { + for (var i = 0; i < map.supportedMapTypes.length; i++) { + if (requestedMapType == map.supportedMapTypes[i].name) { + map.activeMapType = map.supportedMapTypes[i] + } + } + } + } + } + + Component { + id: vorRadialComponent + MapPolyline { + line.width: 2 + line.color: 'gray' + path: vorRadial + } + } + + Component { + id: vorComponent + MapQuickItem { + id: vor + anchorPoint.x: image.width/2 + anchorPoint.y: bubble.height/2 + coordinate: position + zoomLevel: vorZoomLevel + + sourceItem: Grid { + columns: 1 + Grid { + horizontalItemAlignment: Grid.AlignHCenter + verticalItemAlignment: Grid.AlignVCenter + columnSpacing: 5 + layer.enabled: true + layer.smooth: true + Image { + id: image + source: vorImage + MouseArea { + anchors.fill: parent + hoverEnabled: true + onDoubleClicked: (mouse) => { + selected = !selected + } + } + } + Rectangle { + id: bubble + color: bubbleColour + border.width: 1 + width: text.width + 5 + height: text.height + 5 + radius: 5 + Text { + id: text + anchors.centerIn: parent + text: vorData + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onDoubleClicked: (mouse) => { + selected = !selected + } + } + } + } + } + } + } + +} diff --git a/plugins/feature/vorlocalizer/vorlocalizergui.cpp b/plugins/feature/vorlocalizer/vorlocalizergui.cpp index bae749c47..45aa3a76b 100644 --- a/plugins/feature/vorlocalizer/vorlocalizergui.cpp +++ b/plugins/feature/vorlocalizer/vorlocalizergui.cpp @@ -1041,6 +1041,11 @@ void VORLocalizerGUI::applyMapSettings() m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); QQuickItem *item = ui->map->rootObject(); + if (!item) + { + qCritical("VORLocalizerGUI::applyMapSettings: Map not found. Are all required Qt plugins installed?"); + return; + } QObject *object = item->findChild("map"); QGeoCoordinate coords; @@ -1146,7 +1151,11 @@ VORLocalizerGUI::VORLocalizerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISe ui->map->setAttribute(Qt::WA_AcceptTouchEvents, true); ui->map->rootContext()->setContextProperty("vorModel", &m_vorModel); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) ui->map->setSource(QUrl(QStringLiteral("qrc:/demodvor/map/map.qml"))); +#else + ui->map->setSource(QUrl(QStringLiteral("qrc:/demodvor/map/map_6.qml"))); +#endif m_muteIcon.addPixmap(QPixmap("://sound_off.png"), QIcon::Normal, QIcon::On); m_muteIcon.addPixmap(QPixmap("://sound_on.png"), QIcon::Normal, QIcon::Off); From 9c31f0066a4137fb0b74a9240c3a93139cc91ce9 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 11:46:09 +0100 Subject: [PATCH 02/14] Remove debug. --- plugins/channelrx/demodadsb/map/map_6.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/channelrx/demodadsb/map/map_6.qml b/plugins/channelrx/demodadsb/map/map_6.qml index 259eeb05b..86b48538f 100644 --- a/plugins/channelrx/demodadsb/map/map_6.qml +++ b/plugins/channelrx/demodadsb/map/map_6.qml @@ -431,7 +431,6 @@ Item { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: (mouse) => { - console.log("AIRPORT CLICKED ************************* "); if (mouse.button === Qt.RightButton) { showRangeItem.visible = !rangeGroup.groupVisible hideRangeItem.visible = rangeGroup.groupVisible @@ -508,7 +507,6 @@ Item { MouseArea { anchors.fill: parent onClicked: (mouse) => { - console.log("AIRPORT 2 CLICKED ************************* "); if (showFreq) { var freqIdx = Math.floor((mouse.y-5)/((height-10)/airportDataRows)) if (freqIdx == 0) { From 7b6c9e23ec6c5b8a970b41a9608dc4e6fd3ec922 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 19:01:18 +0100 Subject: [PATCH 03/14] Update Channel Power API --- swagger/sdrangel/api/swagger/include/ChannelPower.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/swagger/sdrangel/api/swagger/include/ChannelPower.yaml b/swagger/sdrangel/api/swagger/include/ChannelPower.yaml index 303276da3..6289f76c1 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelPower.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelPower.yaml @@ -4,6 +4,13 @@ ChannelPowerSettings: inputFrequencyOffset: type: integer format: int64 + frequencyMode: + description: (0 for Offset, 1 for Absolute) + type: integer + frequency: + description: Channel center frequency + type: integer + format: int64 rfBandwidth: type: number format: float From 83498f848b089a6f975945232e401d1cf37cfc28 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 19:04:52 +0100 Subject: [PATCH 04/14] Regnerate swagger files --- sdrbase/resources/webapi/doc/html2/index.html | 11 ++++- .../doc/swagger/include/ChannelPower.yaml | 7 +++ swagger/sdrangel/code/html2/index.html | 11 ++++- .../qt5/client/SWGChannelPowerSettings.cpp | 46 +++++++++++++++++++ .../code/qt5/client/SWGChannelPowerSettings.h | 12 +++++ 5 files changed, 85 insertions(+), 2 deletions(-) diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 038c51178..aa5313fcf 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -3819,6 +3819,15 @@ margin-bottom: 20px; "type" : "integer", "format" : "int64" }, + "frequencyMode" : { + "type" : "integer", + "description" : "(0 for Offset, 1 for Absolute)" + }, + "frequency" : { + "type" : "integer", + "format" : "int64", + "description" : "Channel center frequency" + }, "rfBandwidth" : { "type" : "number", "format" : "float" @@ -58934,7 +58943,7 @@ except ApiException as e:
- Generated 2024-04-04T16:23:36.765+02:00 + Generated 2024-04-06T20:04:32.029+02:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/ChannelPower.yaml b/sdrbase/resources/webapi/doc/swagger/include/ChannelPower.yaml index 068d62151..ce8c6cdde 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/ChannelPower.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/ChannelPower.yaml @@ -4,6 +4,13 @@ ChannelPowerSettings: inputFrequencyOffset: type: integer format: int64 + frequencyMode: + description: (0 for Offset, 1 for Absolute) + type: integer + frequency: + description: Channel center frequency + type: integer + format: int64 rfBandwidth: type: number format: float diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index 038c51178..aa5313fcf 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -3819,6 +3819,15 @@ margin-bottom: 20px; "type" : "integer", "format" : "int64" }, + "frequencyMode" : { + "type" : "integer", + "description" : "(0 for Offset, 1 for Absolute)" + }, + "frequency" : { + "type" : "integer", + "format" : "int64", + "description" : "Channel center frequency" + }, "rfBandwidth" : { "type" : "number", "format" : "float" @@ -58934,7 +58943,7 @@ except ApiException as e:
- Generated 2024-04-04T16:23:36.765+02:00 + Generated 2024-04-06T20:04:32.029+02:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.cpp index e2d31e2aa..5a1d0b1f3 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.cpp @@ -30,6 +30,10 @@ SWGChannelPowerSettings::SWGChannelPowerSettings(QString* json) { SWGChannelPowerSettings::SWGChannelPowerSettings() { input_frequency_offset = 0L; m_input_frequency_offset_isSet = false; + frequency_mode = 0; + m_frequency_mode_isSet = false; + frequency = 0L; + m_frequency_isSet = false; rf_bandwidth = 0.0f; m_rf_bandwidth_isSet = false; pulse_threshold = 0.0f; @@ -66,6 +70,10 @@ void SWGChannelPowerSettings::init() { input_frequency_offset = 0L; m_input_frequency_offset_isSet = false; + frequency_mode = 0; + m_frequency_mode_isSet = false; + frequency = 0L; + m_frequency_isSet = false; rf_bandwidth = 0.0f; m_rf_bandwidth_isSet = false; pulse_threshold = 0.0f; @@ -101,6 +109,8 @@ SWGChannelPowerSettings::cleanup() { + + if(title != nullptr) { delete title; } @@ -133,6 +143,10 @@ void SWGChannelPowerSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + ::SWGSDRangel::setValue(&frequency_mode, pJson["frequencyMode"], "qint32", ""); + + ::SWGSDRangel::setValue(&frequency, pJson["frequency"], "qint64", ""); + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); ::SWGSDRangel::setValue(&pulse_threshold, pJson["pulseThreshold"], "float", ""); @@ -178,6 +192,12 @@ SWGChannelPowerSettings::asJsonObject() { if(m_input_frequency_offset_isSet){ obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); } + if(m_frequency_mode_isSet){ + obj->insert("frequencyMode", QJsonValue(frequency_mode)); + } + if(m_frequency_isSet){ + obj->insert("frequency", QJsonValue(frequency)); + } if(m_rf_bandwidth_isSet){ obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); } @@ -231,6 +251,26 @@ SWGChannelPowerSettings::setInputFrequencyOffset(qint64 input_frequency_offset) this->m_input_frequency_offset_isSet = true; } +qint32 +SWGChannelPowerSettings::getFrequencyMode() { + return frequency_mode; +} +void +SWGChannelPowerSettings::setFrequencyMode(qint32 frequency_mode) { + this->frequency_mode = frequency_mode; + this->m_frequency_mode_isSet = true; +} + +qint64 +SWGChannelPowerSettings::getFrequency() { + return frequency; +} +void +SWGChannelPowerSettings::setFrequency(qint64 frequency) { + this->frequency = frequency; + this->m_frequency_isSet = true; +} + float SWGChannelPowerSettings::getRfBandwidth() { return rf_bandwidth; @@ -369,6 +409,12 @@ SWGChannelPowerSettings::isSet(){ if(m_input_frequency_offset_isSet){ isObjectUpdated = true; break; } + if(m_frequency_mode_isSet){ + isObjectUpdated = true; break; + } + if(m_frequency_isSet){ + isObjectUpdated = true; break; + } if(m_rf_bandwidth_isSet){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.h index 7db5edcc5..4f01a5dc5 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelPowerSettings.h @@ -47,6 +47,12 @@ public: qint64 getInputFrequencyOffset(); void setInputFrequencyOffset(qint64 input_frequency_offset); + qint32 getFrequencyMode(); + void setFrequencyMode(qint32 frequency_mode); + + qint64 getFrequency(); + void setFrequency(qint64 frequency); + float getRfBandwidth(); void setRfBandwidth(float rf_bandwidth); @@ -93,6 +99,12 @@ private: qint64 input_frequency_offset; bool m_input_frequency_offset_isSet; + qint32 frequency_mode; + bool m_frequency_mode_isSet; + + qint64 frequency; + bool m_frequency_isSet; + float rf_bandwidth; bool m_rf_bandwidth_isSet; From 116d6674bd8ae8c4eb4b6bda3391754c217adce4 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 22:16:46 +0100 Subject: [PATCH 05/14] Add frequency and frequencyMode to API --- .../channelrx/channelpower/channelpower.cpp | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/channelrx/channelpower/channelpower.cpp b/plugins/channelrx/channelpower/channelpower.cpp index b57c70efb..151b43673 100644 --- a/plugins/channelrx/channelpower/channelpower.cpp +++ b/plugins/channelrx/channelpower/channelpower.cpp @@ -289,13 +289,26 @@ int ChannelPower::webapiSettingsPutPatch( ChannelPowerSettings settings = m_settings; webapiUpdateChannelSettings(settings, channelSettingsKeys, response); - MsgConfigureChannelPower *msg = MsgConfigureChannelPower::create(settings, channelSettingsKeys, force); + // Ensure inputFrequencyOffset and frequency are consistent + QStringList settingsKeys = channelSettingsKeys; + if (settingsKeys.contains("frequency") && (settings.m_frequencyMode == ChannelPowerSettings::Absolute)) + { + settings.m_inputFrequencyOffset = settings.m_frequency - m_centerFrequency; + settingsKeys.append("inputFrequencyOffset"); + } + else if (settingsKeys.contains("inputFrequencyOffset") && (settings.m_frequencyMode == ChannelPowerSettings::Offset)) + { + settings.m_frequency = m_centerFrequency + settings.m_inputFrequencyOffset; + settingsKeys.append("frequency"); + } + + MsgConfigureChannelPower *msg = MsgConfigureChannelPower::create(settings, settingsKeys, force); m_inputMessageQueue.push(msg); qDebug("ChannelPower::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); if (m_guiMessageQueue) // forward to GUI if any { - MsgConfigureChannelPower *msgToGUI = MsgConfigureChannelPower::create(settings, channelSettingsKeys, force); + MsgConfigureChannelPower *msgToGUI = MsgConfigureChannelPower::create(settings, settingsKeys, force); m_guiMessageQueue->push(msgToGUI); } @@ -312,6 +325,12 @@ void ChannelPower::webapiUpdateChannelSettings( if (channelSettingsKeys.contains("inputFrequencyOffset")) { settings.m_inputFrequencyOffset = response.getChannelPowerSettings()->getInputFrequencyOffset(); } + if (channelSettingsKeys.contains("frequencyMode")) { + settings.m_frequencyMode = (ChannelPowerSettings::FrequencyMode) response.getChannelPowerSettings()->getFrequencyMode(); + } + if (channelSettingsKeys.contains("frequency")) { + settings.m_frequency = response.getChannelPowerSettings()->getFrequency(); + } if (channelSettingsKeys.contains("rfBandwidth")) { settings.m_rfBandwidth = response.getChannelPowerSettings()->getRfBandwidth(); } @@ -356,6 +375,8 @@ void ChannelPower::webapiUpdateChannelSettings( void ChannelPower::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const ChannelPowerSettings& settings) { response.getChannelPowerSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getChannelPowerSettings()->setFrequencyMode(settings.m_frequencyMode); + response.getChannelPowerSettings()->setFrequency(settings.m_frequency); response.getChannelPowerSettings()->setRfBandwidth(settings.m_rfBandwidth); response.getChannelPowerSettings()->setPulseThreshold(settings.m_pulseThreshold); response.getChannelPowerSettings()->setAveragePeriodUs(settings.m_averagePeriodUS); @@ -469,6 +490,12 @@ void ChannelPower::webapiFormatChannelSettings( if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { swgChannelPowerSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); } + if (channelSettingsKeys.contains("frequencyMode") || force) { + swgChannelPowerSettings->setFrequencyMode(settings.m_frequencyMode); + } + if (channelSettingsKeys.contains("inputFrequency") || force) { + swgChannelPowerSettings->setFrequency(settings.m_frequency); + } if (channelSettingsKeys.contains("rfBandwidth") || force) { swgChannelPowerSettings->setRfBandwidth(settings.m_rfBandwidth); } From be7199531d1da5a4782508e077204739b09e3da6 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 22:17:56 +0100 Subject: [PATCH 06/14] Add Add Channels dialog, to easily add Channel Powe channels. --- doc/img/SID_plugin_addchannels.png | Bin 0 -> 19437 bytes doc/img/SID_plugin_settings.png | Bin 16092 -> 16416 bytes plugins/feature/sid/CMakeLists.txt | 3 + plugins/feature/sid/readme.md | 46 +++--- plugins/feature/sid/sidaddchannelsdialog.cpp | 147 +++++++++++++++++++ plugins/feature/sid/sidaddchannelsdialog.h | 58 ++++++++ plugins/feature/sid/sidaddchannelsdialog.ui | 92 ++++++++++++ plugins/feature/sid/sidgui.cpp | 14 ++ plugins/feature/sid/sidgui.h | 1 + plugins/feature/sid/sidgui.ui | 14 ++ 10 files changed, 357 insertions(+), 18 deletions(-) create mode 100644 doc/img/SID_plugin_addchannels.png create mode 100644 plugins/feature/sid/sidaddchannelsdialog.cpp create mode 100644 plugins/feature/sid/sidaddchannelsdialog.h create mode 100644 plugins/feature/sid/sidaddchannelsdialog.ui diff --git a/doc/img/SID_plugin_addchannels.png b/doc/img/SID_plugin_addchannels.png new file mode 100644 index 0000000000000000000000000000000000000000..09ff7878fe7b95ad1f02d37ab625d031afb0327f GIT binary patch literal 19437 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 GKl~palv6eU literal 0 HcmV?d00001 diff --git a/doc/img/SID_plugin_settings.png b/doc/img/SID_plugin_settings.png index fa30c699b6dc4c80ac8fad9e6e66b7c3d94d49eb..986ab271a07c6adc0074a0404cdcbd348d77df2d 100644 GIT binary patch literal 16416 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 M{~YB`;n6?l6@`eq*iR{# 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 literal 16092 zcmZ{rWmFtnx2_2gT!Xs@cXxMpcXxLS5Zr=8aCdiiN#pMBL4#{^JA0q+o^!^y_Xk~7 z-PGu=C3CI$KJScFQjkQ1!-WF_14ERS5>o*KgIEHcbHYG@UQco6U_ggYt}2ouU^P?t zr=SZ+OJR9oFtGY~_&4LvpletsDQ#CUFr@y!f1lbCY%jpT1e~SCgw?zZF0!Hh(8XUz zbHQNq@~MdBhtLyY`lCX(qJ_mE@70wc$+d#ZV1ieGSCwK)*Pv^}vf8pTEg_|#2r3v4 z9`f%DGFKG&hrPY+*`4j(tKHq5@-A3M|VASfcY4t0SqC{Uf5WwI9IT(F`xK$<~HwIOZR>7mIfUTd02Z}+^4%l={wr)ObJ9T z|B@C^UxJlCAnW8@+Y-{4L~ei1C5AN1bIUsp)8%_86KYEYGsBz1Ox~yBqM{YdeHZ>t zXKvv#=l)5GG`)c$m13EWa{R`&D7*#kMu=BeD#g=PVq1#IB4{7#s4pN0X-y>yD)c z-9EF`ty?nI*4C-uoR3?+jKd=%Q21%8CZQAERKI@xva^768OG3*+Kecu3#Yr7IN0eA zu``30l>`^Y)Sv?Wm;e9cg&z`b@!Kbelhe{%3zvC$PVZGir~c0iE%v&MtP#{5E*9Z$ zgTALjB}6B42DM5lJUzZ}n~*49CA&16P*G;-l!t!E29qsN$uOwV{oH2C#uBfEj#2Johh>VP!KJCTv z&eT<{CA=@o0}Jwn-!7cQI23{m5PUypl!-AsfD?m|HJN?xOEg4vd%4cj`WimE9 ztnPm=-W{eHsvH7RPUmD9&1I+uj>w2S_C$_tuXp;Z$fAn}J&M#TH5QtzRdsp;_%oY7 zd+hBQd7y`&j?tZmj3&V~LV;N3NfF{t$#{2u@w!kTMG`ws^0+5Lj>Do?f0JVlV zP_6Qamo~gB*lq9~uODdBd1~|EAWHGW&kTCvi2rFPWR4zOVbnZy-Z|gF>*g2b>Em|b zBTkANPmRaL+U(I}<~^+=cr5(m`xse#-D5C-$h=D-o9Av?@MC7H+czV#$N#mV!+g0$ z|NJLe^jTt=JUUUXRH*{~to(sml}?hvap&c6-vc|kyBXf1g1zjb4pRQWJKo6<$yJ*P zjYh#_=8Y8|#mU#8l+O!>UL%Nj9H4VkO8eTb8=2IwN{wohW&EelWrY=*wU-!BLU<%` zKgRbOUc0nLQgd>qZluqasxz9b=SS%G*jvMe`nOvhi7vLfGegNbp+iM>`>T3nF9d=s z7Nmy0r@n;oLcH07fzs9SlPR)@FdAzqQBB{? zm?`gX+{1-X($fFN4=gZbU@^NqkLXR|0v(Cs4>*^X;)JsB1C|%#KE>SmZFy<}JI# zY0q>l&zaOKTXfHo9p*^|q?>I!9~AI50Yh^2Eu_rM2{pYypNDG{H>A;6JjrI8MUyCF zb?UB$JFQvdr~Q zom?ZSQ7)9I*EXe!QD|^GRnb6Vs%-IlcF`c5`4;$k(Mop(7W3eLJIQ(ONu}q1zsTnI zQWIl?iotQDG zK2APym=5+McwC-iovzF+mzhkW+bp=l&?s##X9(S``LCwSz?`>|J3Z7ot5Pe5`{a?!H0 z$K^QD*^zt!M6`j0uKsVmtsI84N2JBhcf_wxh^#C9h#O%d%J&$`$G7$r249xb!eR*J zr<+hG*Ofjro4MXP(8w9$e)0q1%q;{&{P(*q_SX0Y-+;%0AI1*`##&FTfW$x3vqbci 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 diff --git a/plugins/feature/sid/CMakeLists.txt b/plugins/feature/sid/CMakeLists.txt index e24703036..6e0d57f95 100644 --- a/plugins/feature/sid/CMakeLists.txt +++ b/plugins/feature/sid/CMakeLists.txt @@ -27,12 +27,15 @@ if(NOT SERVER_MODE) sidgui.ui sidsettingsdialog.cpp sidsettingsdialog.ui + sidaddchannelsdialog.cpp + sidaddchannelsdialog.ui icons.qrc ) set(sid_HEADERS ${sid_HEADERS} sidgui.h sidsettingsdialog.h + sidaddchannelsdialog.h ) set(TARGET_NAME featuresid) diff --git a/plugins/feature/sid/readme.md b/plugins/feature/sid/readme.md index 3796d8920..caea10b17 100644 --- a/plugins/feature/sid/readme.md +++ b/plugins/feature/sid/readme.md @@ -119,7 +119,17 @@ When checked, all data is displayed on a single combined chart. Check to display a legend on the chart. When unchecked the legend will be hidden. You can click on items in the legend to temporarily hide and then show the corresponding series on the chart. The position of the legend can be set in the Settings Dialog. -

16: Open Settings Dialog

+

16: Add Channels

+ +Click to open the Add Channels Dialog. This allows you to easily add the Channel Power channels for each VLF transmitter on each RX device. +This dialog shows a table with one row for each VLF Transmitter, and a column for each RX device. +When OK is pressed, a corresponding Channel Power channel will be created for every cell that is checked. + +For example, in the image below, Channel Power channels will be added for the GDQ transmitter on both devices R0 and R1, with one for FTA on device R0 only. + +![SID Add Channels dialog](../../../doc/img/SID_plugin_addchannels.png) + +

17: Open Settings Dialog

Click to open the Settings Dialog. The settings dialog allows a user to: @@ -134,7 +144,7 @@ Click to open the Settings Dialog. The settings dialog allows a user to: ![SID settings dialog](../../../doc/img/SID_plugin_settings_dialog.png) -

17: Display SDO/SOHO Imagery

+

18: Display SDO/SOHO Imagery

When checked, displays imagary from NASA's SDO (Solar Dynamic Observatory) and ESA/NASA's SOHO (Solar and Heliospheric Observatory) satellites. @@ -142,11 +152,11 @@ SDOs images the Sun in a variety of UV and EUV wavelengths. SOHO shows images of Solar flares are particularly visible in the AIA 131 Å images. -

18: Image or Video Selection

+

19: Image or Video Selection

Selects whether to display images (unchecked) or video (checked). -

19: Image/Wavelength Selection

+

20: Image/Wavelength Selection

Selects which image / wavelength to view. @@ -172,63 +182,63 @@ Selects which image / wavelength to view. * LASCO (Large Angle Spectrometric Coronagraph) shows solar corona. C2 shows corona up to 8.4Mkm. C3 shows corona up to 23Mkm. -

20: Show GOES 16, 18 and SDO

+

21: Show GOES 16, 18 and SDO

When checked, opens a [Satellite Tracker](../../feature/satellitetracker/readme.md) feature and sets it to display data for the GOES 16, GOES 18 and SDO satellites. The position and tracks of the satellites will then be visible on a [Map](../../feature/map/readme.md) feature. -

21: Autoscale X

+

22: Autoscale X

When clicked, the chart X-axis is automatically scaled so that all power data is visible. When right-clicked, autoscaling of the X-axis will occur whenever new data is added to the chart. -

22: Autoscale Y

+

23: Autoscale Y

When clicked, the chart Y-axis is automatically scaled so that all power data is visible. When right-clicked, autoscaling of the Y-axis will occur whenever new data is added to the chart. -

23: Set X-axis to Today

+

24: Set X-axis to Today

When clicked, the X-axis is set to show today, from midnight to midnight. When right-clicked, the X-axis is set to show sunrise to sunset. This uses latitude and longitude from Preferences > My position. -

24: Set X-axis to -1 day

+

25: Set X-axis to -1 day

When clicked, the X-axis is set 1 day earlier than the current setting, at the same time. -

25: Set X-axis to +1 day

+

26: Set X-axis to +1 day

When clicked, the X-axis is set 1 day later than the current setting, at the same time. -

26: Start Time

+

27: Start Time

Displays/sets the current start time of the chart (X-axis minimum). It's possible to scroll through hours/days/months by clicking on the relevent segment and using the mouse scroll wheel. -

27: End Time

+

28: End Time

Displays/sets the current end time of the chart (X-axis maximum). It's possible to scroll through hours/days/months by clicking on the relevent segment and using the mouse scroll wheel. -

28: Min

+

29: Min

Displays/sets the minimum Y-axis value. -

29: Max

+

30: Max

Displays/sets the maximum Y-axis value. -

30: Now

+

31: Now

When checked, the latest SDO imagery is displayed. When unchecked, you can enter a date and time for which imagery should be displayed. -

31: Date Time

+

32: Date Time

Specifies the date and time for which SDO imagery should be displayed. Images are updated every 15 minutes. The date and time can also be set by clicking on the chart. -

32: Map

+

33: Map

Select a Map to link to the SID feature. When a time is selected on the SID charts, the [Map](../../feature/map/readme.md) feature will have it's time set accordingly. This allows you, for example, to see the corresponding impact on MUF/foF2 displayed on the 3D map. -

33: Show Paths on Map

+

34: Show Paths on Map

When clicked, shows the great circle paths between transmitters and receivers on a [Map](../../feature/map/readme.md). diff --git a/plugins/feature/sid/sidaddchannelsdialog.cpp b/plugins/feature/sid/sidaddchannelsdialog.cpp new file mode 100644 index 000000000..0e4dcfba7 --- /dev/null +++ b/plugins/feature/sid/sidaddchannelsdialog.cpp @@ -0,0 +1,147 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "util/vlftransmitters.h" +#include "channel/channelwebapiutils.h" +#include "device/deviceapi.h" +#include "device/deviceset.h" +#include "dsp/dspdevicesourceengine.h" +#include "maincore.h" + +#include "sidaddchannelsdialog.h" + +SIDAddChannelsDialog::SIDAddChannelsDialog(SIDSettings *settings, QWidget* parent) : + QDialog(parent), + ui(new Ui::SIDAddChannelsDialog), + m_settings(settings) +{ + ui->setupUi(this); + + MainCore *mainCore = MainCore::instance(); + std::vector& deviceSets = mainCore->getDeviceSets(); + + ui->channels->setColumnCount(deviceSets.size() + 2); + + // Create column header + ui->channels->setHorizontalHeaderItem(COL_TX_NAME, new QTableWidgetItem("Callsign")); + ui->channels->setHorizontalHeaderItem(COL_TX_FREQUENCY, new QTableWidgetItem("Frequency (Hz)")); + for (int i = 0; i < deviceSets.size(); i++) + { + if (deviceSets[i]->m_deviceSourceEngine || deviceSets[i]->m_deviceMIMOEngine) + { + QTableWidgetItem *item = new QTableWidgetItem(mainCore->getDeviceSetId(deviceSets[i])); + + ui->channels->setHorizontalHeaderItem(COL_DEVICE + i, item); + } + } + + // Add row for each transmitter, with checkbox for each device + for (int j = 0; j < VLFTransmitters::m_transmitters.size(); j++) + { + int row = ui->channels->rowCount(); + ui->channels->setRowCount(row+1); + + ui->channels->setItem(row, COL_TX_NAME, new QTableWidgetItem(VLFTransmitters::m_transmitters[j].m_callsign)); + ui->channels->setItem(row, COL_TX_FREQUENCY, new QTableWidgetItem(QString::number(VLFTransmitters::m_transmitters[j].m_frequency))); + + for (int i = 0; i < deviceSets.size(); i++) + { + if (deviceSets[i]->m_deviceSourceEngine || deviceSets[i]->m_deviceMIMOEngine) + { + QTableWidgetItem *enableItem = new QTableWidgetItem(); + enableItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + enableItem->setCheckState(Qt::Unchecked); + ui->channels->setItem(row, COL_DEVICE + i, enableItem); + } + } + } + + ui->channels->resizeColumnsToContents(); +} + +SIDAddChannelsDialog::~SIDAddChannelsDialog() +{ + delete ui; +} + +void SIDAddChannelsDialog::accept() +{ + MainCore *mainCore = MainCore::instance(); + connect(mainCore, &MainCore::channelAdded, this, &SIDAddChannelsDialog::channelAdded); + + m_count = m_settings->m_channelSettings.size(); + m_row = 0; + m_col = COL_DEVICE; + addNextChannel(); +} + +void SIDAddChannelsDialog::addNextChannel() +{ + if (m_row < ui->channels->rowCount()) + { + QString id = ui->channels->horizontalHeaderItem(m_col)->text(); + unsigned int deviceSetIndex; + + if (ui->channels->item(m_row, m_col)->checkState() == Qt::Checked) + { + MainCore::getDeviceSetIndexFromId(id, deviceSetIndex); + ChannelWebAPIUtils::addChannel(deviceSetIndex, "sdrangel.channel.channelpower", 0); + } + else + { + nextChannel(); // can recursively call this method + } + } + else + { + QDialog::accept(); + } +} + +void SIDAddChannelsDialog::nextChannel() +{ + m_col++; + if (m_col >= ui->channels->columnCount()) + { + m_col = COL_DEVICE; + m_row++; + } + + addNextChannel(); +} + +void SIDAddChannelsDialog::channelAdded(int deviceSetIndex, ChannelAPI *channel) +{ + const VLFTransmitters::Transmitter *transmitter = VLFTransmitters::m_callsignHash.value(ui->channels->item(m_row, COL_TX_NAME)->text()); + + ChannelWebAPIUtils::patchChannelSetting(channel, "title", transmitter->m_callsign); + ChannelWebAPIUtils::patchChannelSetting(channel, "frequency", transmitter->m_frequency); + ChannelWebAPIUtils::patchChannelSetting(channel, "frequencyMode", 1); + ChannelWebAPIUtils::patchChannelSetting(channel, "rfBandwidth", 300); + ChannelWebAPIUtils::patchChannelSetting(channel, "averagePeriodUS", 10000000); + + // Update setings if they are created by SIDGUI before this slot is called + if (m_count < m_settings->m_channelSettings.size()) { + m_settings->m_channelSettings[m_count].m_label = transmitter->m_callsign; + } + + m_count++; + nextChannel(); +} diff --git a/plugins/feature/sid/sidaddchannelsdialog.h b/plugins/feature/sid/sidaddchannelsdialog.h new file mode 100644 index 000000000..f19592ea7 --- /dev/null +++ b/plugins/feature/sid/sidaddchannelsdialog.h @@ -0,0 +1,58 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Jon Beniston, M7RCE // +// // +// 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 as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_SIDADDCHANNELSDIALOG_H +#define INCLUDE_SIDADDCHANNELSDIALOG_H + +#include "ui_sidaddchannelsdialog.h" + +#include "sidsettings.h" + +class SIDAddChannelsDialog : public QDialog { + Q_OBJECT + +public: + explicit SIDAddChannelsDialog(SIDSettings *settings, QWidget* parent = 0); + ~SIDAddChannelsDialog(); + +private: + +private slots: + void accept(); + + void channelAdded(int deviceSetIndex, ChannelAPI *channel); + +private: + Ui::SIDAddChannelsDialog* ui; + + enum Column { + COL_TX_NAME, + COL_TX_FREQUENCY, + COL_DEVICE + }; + + SIDSettings *m_settings; + + int m_row; // Row and column in table, when adding channels + int m_col; + int m_count; // How many channels we've added + + void addNextChannel(); + void nextChannel(); +}; + +#endif // INCLUDE_SIDADDCHANNELSDIALOG_H diff --git a/plugins/feature/sid/sidaddchannelsdialog.ui b/plugins/feature/sid/sidaddchannelsdialog.ui new file mode 100644 index 000000000..a5b0468c6 --- /dev/null +++ b/plugins/feature/sid/sidaddchannelsdialog.ui @@ -0,0 +1,92 @@ + + + SIDAddChannelsDialog + + + + 0 + 0 + 441 + 463 + + + + + Liberation Sans + 9 + + + + Add channels + + + + + + Select devices and VLF transmitters to add channels for + + + + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + true + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SIDAddChannelsDialog + accept() + + + 257 + 31 + + + 157 + 274 + + + + + buttonBox + rejected() + SIDAddChannelsDialog + reject() + + + 325 + 31 + + + 286 + 274 + + + + + diff --git a/plugins/feature/sid/sidgui.cpp b/plugins/feature/sid/sidgui.cpp index a8a5a0102..dce08bab2 100644 --- a/plugins/feature/sid/sidgui.cpp +++ b/plugins/feature/sid/sidgui.cpp @@ -39,6 +39,7 @@ #include "sid.h" #include "sidgui.h" #include "sidsettingsdialog.h" +#include "sidaddchannelsdialog.h" #include "SWGMapItem.h" @@ -1482,6 +1483,16 @@ void SIDGUI::on_deleteAll_clicked() getData(); } +void SIDGUI::on_addChannels_clicked() +{ + SIDAddChannelsDialog dialog(&m_settings); + + new DialogPositioner(&dialog, true); + + dialog.exec(); +} + + void SIDGUI::on_settings_clicked() { SIDSettingsDialog dialog(&m_settings); @@ -1493,6 +1504,8 @@ void SIDGUI::on_settings_clicked() &SIDGUI::removeChannels ); + new DialogPositioner(&dialog, true); + if (dialog.exec() == QDialog::Accepted) { setAutosaveTimer(); @@ -1587,6 +1600,7 @@ void SIDGUI::makeUIConnections() QObject::connect(ui->saveData, &QToolButton::clicked, this, &SIDGUI::on_saveData_clicked); QObject::connect(ui->loadData, &QToolButton::clicked, this, &SIDGUI::on_loadData_clicked); QObject::connect(ui->saveChartImage, &QToolButton::clicked, this, &SIDGUI::on_saveChartImage_clicked); + QObject::connect(ui->addChannels, &QToolButton::clicked, this, &SIDGUI::on_addChannels_clicked); QObject::connect(ui->settings, &QToolButton::clicked, this, &SIDGUI::on_settings_clicked); } diff --git a/plugins/feature/sid/sidgui.h b/plugins/feature/sid/sidgui.h index 7807450d9..ae1ab9c3f 100644 --- a/plugins/feature/sid/sidgui.h +++ b/plugins/feature/sid/sidgui.h @@ -284,6 +284,7 @@ private slots: void updateStatus(); void autosave(); void on_settings_clicked(); + void on_addChannels_clicked(); void xRayDataUpdated(const QList& data, bool primary); void protonDataUpdated(const QList& data, bool primary); void grbDataUpdated(const QList& data); diff --git a/plugins/feature/sid/sidgui.ui b/plugins/feature/sid/sidgui.ui index 71a17c413..59b7d4b05 100644 --- a/plugins/feature/sid/sidgui.ui +++ b/plugins/feature/sid/sidgui.ui @@ -327,6 +327,20 @@ + + + + Add channels + + + + + + + :/channels_add.png:/channels_add.png + + + From 035e6f59be00315d9cfdd90cfcedd8602941a317 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 22:22:28 +0100 Subject: [PATCH 07/14] Add additional patchChannelSettings variants and addChannel. --- sdrbase/channel/channelwebapiutils.cpp | 98 ++++++++++++++++++++++---- sdrbase/channel/channelwebapiutils.h | 7 +- sdrbase/webapi/webapiutils.cpp | 48 +++++++++++++ sdrbase/webapi/webapiutils.h | 2 + 4 files changed, 142 insertions(+), 13 deletions(-) diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index 42292d54c..141964aa6 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2020-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // // Copyright (C) 2020, 2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -267,6 +267,23 @@ bool ChannelWebAPIUtils::getChannelSettings(unsigned int deviceIndex, unsigned i return true; } +bool ChannelWebAPIUtils::getChannelSettings(ChannelAPI *channel, SWGSDRangel::SWGChannelSettings &channelSettingsResponse) +{ + QString errorResponse; + int httpRC; + + httpRC = channel->webapiSettingsGet(channelSettingsResponse, errorResponse); + + if (httpRC/100 != 2) + { + qWarning("ChannelWebAPIUtils::getChannelSettings: get channel settings error %d: %s", + httpRC, qPrintable(errorResponse)); + return false; + } + + return true; +} + bool ChannelWebAPIUtils::getChannelReport(unsigned int deviceIndex, unsigned int channelIndex, SWGSDRangel::SWGChannelReport &channelReport) { QString errorResponse; @@ -1485,22 +1502,19 @@ bool ChannelWebAPIUtils::patchFeatureSetting(unsigned int featureSetIndex, unsig } } - -bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsigned int channelIndex, const QString &setting, double value) +bool ChannelWebAPIUtils::patchChannelSetting(ChannelAPI *channel, const QString &setting, const QVariant& value) { SWGSDRangel::SWGChannelSettings channelSettingsResponse; QString errorResponse; int httpRC; - ChannelAPI *channel; - if (getChannelSettings(deviceSetIndex, channelIndex, channelSettingsResponse, channel)) + if (getChannelSettings(channel, channelSettingsResponse)) { // Patch settings QJsonObject *jsonObj = channelSettingsResponse.asJsonObject(); - double oldValue; - if (WebAPIUtils::getSubObjectDouble(*jsonObj, setting, oldValue)) + if (WebAPIUtils::hasSubObject(*jsonObj, setting)) { - WebAPIUtils::setSubObjectDouble(*jsonObj, setting, value); + WebAPIUtils::setSubObject(*jsonObj, setting, value); QStringList channelSettingsKeys; channelSettingsKeys.append(setting); channelSettingsResponse.init(); @@ -1511,19 +1525,19 @@ bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsign if (httpRC/100 == 2) { - qDebug("ChannelWebAPIUtils::patchChannelSetting: set feature setting %s to %f OK", qPrintable(setting), value); + qDebug("ChannelWebAPIUtils::patchChannelSetting: set feature setting %s to %s OK", qPrintable(setting), qPrintable(value.toString())); return true; } else { - qWarning("ChannelWebAPIUtils::patchChannelSetting: set feature setting %s to %f error %d: %s", - qPrintable(setting), value, httpRC, qPrintable(*errorResponse2.getMessage())); + qWarning("ChannelWebAPIUtils::patchChannelSetting: set feature setting %s to %s error %d: %s", + qPrintable(setting), qPrintable(value.toString()), httpRC, qPrintable(*errorResponse2.getMessage())); return false; } } else { - qWarning("ChannelWebAPIUtils::patchChannelSetting: no key %s in feature settings", qPrintable(setting)); + qWarning("ChannelWebAPIUtils::patchChannelSetting: no key %s in channel settings", qPrintable(setting)); return false; } } @@ -1533,6 +1547,39 @@ bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsign } } +bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsigned int channelIndex, const QString &setting, const QString& value) +{ + ChannelAPI *channel = MainCore::instance()->getChannel(deviceSetIndex, channelIndex); + + if (channel) { + return patchChannelSetting(channel, setting, value); + } else { + return false; + } +} + +bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsigned int channelIndex, const QString &setting, int value) +{ + ChannelAPI *channel = MainCore::instance()->getChannel(deviceSetIndex, channelIndex); + + if (channel) { + return patchChannelSetting(channel, setting, value); + } else { + return false; + } +} + +bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsigned int channelIndex, const QString &setting, double value) +{ + ChannelAPI *channel = MainCore::instance()->getChannel(deviceSetIndex, channelIndex); + + if (channel) { + return patchChannelSetting(channel, setting, value); + } else { + return false; + } +} + bool ChannelWebAPIUtils::patchChannelSetting(unsigned int deviceSetIndex, unsigned int channelIndex, const QString &setting, const QJsonArray& value) { SWGSDRangel::SWGChannelSettings channelSettingsResponse; @@ -1835,3 +1882,30 @@ bool ChannelWebAPIUtils::getChannelReportValue(unsigned int deviceIndex, unsigne return false; } +bool ChannelWebAPIUtils::addChannel(unsigned int deviceSetIndex, const QString& uri, int direction) +{ + MainCore *mainCore = MainCore::instance(); + PluginAPI::ChannelRegistrations *channelRegistrations = mainCore->getPluginManager()->getRxChannelRegistrations(); + int nbRegistrations = channelRegistrations->size(); + int index = 0; + + for (; index < nbRegistrations; index++) + { + if (channelRegistrations->at(index).m_channelIdURI == uri) { + break; + } + } + + if (index < nbRegistrations) + { + MainCore::MsgAddChannel *msg = MainCore::MsgAddChannel::create(deviceSetIndex, index, direction); + mainCore->getMainMessageQueue()->push(msg); + + return true; + } + else + { + qWarning() << "ChannelWebAPIUtils::addChannel:" << uri << "plugin not available"; + return false; + } +} diff --git a/sdrbase/channel/channelwebapiutils.h b/sdrbase/channel/channelwebapiutils.h index 8a9d25c4c..6e65cfcef 100644 --- a/sdrbase/channel/channelwebapiutils.h +++ b/sdrbase/channel/channelwebapiutils.h @@ -2,7 +2,7 @@ // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2020 Edouard Griffiths, F4EXB // -// Copyright (C) 2020-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // // // // 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 // @@ -79,6 +79,9 @@ public: static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, const QString &value); static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, double value); static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, const QJsonArray& value); + static bool patchChannelSetting(ChannelAPI *channel, const QString &setting, const QVariant &value); + static bool patchChannelSetting(unsigned int deviceSetIndex, unsigned int channeIndex, const QString &setting, const QString &value); + static bool patchChannelSetting(unsigned int deviceSetIndex, unsigned int channeIndex, const QString &setting, int value); static bool patchChannelSetting(unsigned int deviceSetIndex, unsigned int channeIndex, const QString &setting, double value); static bool patchChannelSetting(unsigned int deviceSetIndex, unsigned int channeIndex, const QString &setting, const QJsonArray& value); static bool getFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, int &value); @@ -98,7 +101,9 @@ public: static bool getFeatureSettings(unsigned int featureSetIndex, unsigned int featureIndex, SWGSDRangel::SWGFeatureSettings &featureSettingsResponse, Feature *&feature); static bool getFeatureReport(unsigned int featureSetIndex, unsigned int featureIndex, SWGSDRangel::SWGFeatureReport &featureReport); static bool getChannelSettings(unsigned int deviceIndex, unsigned int channelIndex, SWGSDRangel::SWGChannelSettings &channelSettingsResponse, ChannelAPI *&channel); + static bool getChannelSettings(ChannelAPI *channel, SWGSDRangel::SWGChannelSettings &channelSettingsResponse); static bool getChannelReport(unsigned int deviceIndex, unsigned int channelIndex, SWGSDRangel::SWGChannelReport &channelReport); + static bool addChannel(unsigned int deviceSetIndex, const QString& uri, int direction); protected: static QString getDeviceHardwareId(unsigned int deviceIndex); }; diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 3ceb398b4..5ed235b3d 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -584,6 +584,54 @@ bool WebAPIUtils::getSubObjectIntList(const QJsonObject &json, const QString &ke return false; } +bool WebAPIUtils::hasSubObject(const QJsonObject &json, const QString &key) +{ + for (QJsonObject::const_iterator it = json.begin(); it != json.end(); it++) + { + QJsonValue jsonValue = it.value(); + + if (jsonValue.isObject()) + { + QJsonObject subObject = jsonValue.toObject(); + + if (subObject.contains(key)) { + return true; + } + } + } + + return false; +} + +// Set value withing nested JSON object +bool WebAPIUtils::setSubObject(QJsonObject &json, const QString &key, const QVariant &value) +{ + for (QJsonObject::iterator it = json.begin(); it != json.end(); it++) + { + QJsonValue jsonValue = it.value(); + + if (jsonValue.isObject()) + { + QJsonObject subObject = jsonValue.toObject(); + + if (subObject.contains(key)) + { + if (subObject[key].isString()) { + subObject[key] = value.toString(); + } else if (subObject[key].isDouble()) { + subObject[key] = value.toDouble(); + } else { + qDebug() << "WebAPIUtils::setSubObject: Unsupported type"; + } + it.value() = subObject; + return true; + } + } + } + + return false; +} + // look for value in key=value bool WebAPIUtils::extractValue(const QJsonObject &json, const QString &key, QJsonValue &value) { diff --git a/sdrbase/webapi/webapiutils.h b/sdrbase/webapi/webapiutils.h index 752996217..243053362 100644 --- a/sdrbase/webapi/webapiutils.h +++ b/sdrbase/webapi/webapiutils.h @@ -54,6 +54,8 @@ public: static bool getSubObjectString(const QJsonObject &json, const QString &key, QString &value); static bool setSubObjectString(QJsonObject &json, const QString &key, const QString &value); static bool getSubObjectIntList(const QJsonObject &json, const QString &key, const QString &subKey, QList &values); + static bool setSubObject(QJsonObject &json, const QString &key, const QVariant &value); + static bool hasSubObject(const QJsonObject &json, const QString &key); static bool extractValue(const QJsonObject &json, const QString &key, QJsonValue &value); static bool extractArray(const QJsonObject &json, const QString &key, QJsonArray &value); static bool extractObject(const QJsonObject &json, const QString &key, QJsonObject &value); From 44385832c34159195cd3b411cb3841e199c9d969 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 22:33:54 +0100 Subject: [PATCH 08/14] Fix crash if no devices. --- plugins/feature/sid/sidaddchannelsdialog.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/feature/sid/sidaddchannelsdialog.cpp b/plugins/feature/sid/sidaddchannelsdialog.cpp index 0e4dcfba7..ae953fadc 100644 --- a/plugins/feature/sid/sidaddchannelsdialog.cpp +++ b/plugins/feature/sid/sidaddchannelsdialog.cpp @@ -83,13 +83,20 @@ SIDAddChannelsDialog::~SIDAddChannelsDialog() void SIDAddChannelsDialog::accept() { - MainCore *mainCore = MainCore::instance(); - connect(mainCore, &MainCore::channelAdded, this, &SIDAddChannelsDialog::channelAdded); + if (ui->channels->columnCount() > 2) + { + MainCore *mainCore = MainCore::instance(); + connect(mainCore, &MainCore::channelAdded, this, &SIDAddChannelsDialog::channelAdded); - m_count = m_settings->m_channelSettings.size(); - m_row = 0; - m_col = COL_DEVICE; - addNextChannel(); + m_count = m_settings->m_channelSettings.size(); + m_row = 0; + m_col = COL_DEVICE; + addNextChannel(); + } + else + { + QDialog::accept(); + } } void SIDAddChannelsDialog::addNextChannel() From 1d8d297565b441f8ef09f5269d2c16425154d46c Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 22:35:13 +0100 Subject: [PATCH 09/14] Fix gcc warnings. --- plugins/feature/sid/sidaddchannelsdialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/feature/sid/sidaddchannelsdialog.cpp b/plugins/feature/sid/sidaddchannelsdialog.cpp index ae953fadc..416511f07 100644 --- a/plugins/feature/sid/sidaddchannelsdialog.cpp +++ b/plugins/feature/sid/sidaddchannelsdialog.cpp @@ -42,7 +42,7 @@ SIDAddChannelsDialog::SIDAddChannelsDialog(SIDSettings *settings, QWidget* paren // Create column header ui->channels->setHorizontalHeaderItem(COL_TX_NAME, new QTableWidgetItem("Callsign")); ui->channels->setHorizontalHeaderItem(COL_TX_FREQUENCY, new QTableWidgetItem("Frequency (Hz)")); - for (int i = 0; i < deviceSets.size(); i++) + for (unsigned int i = 0; i < deviceSets.size(); i++) { if (deviceSets[i]->m_deviceSourceEngine || deviceSets[i]->m_deviceMIMOEngine) { @@ -136,6 +136,8 @@ void SIDAddChannelsDialog::nextChannel() void SIDAddChannelsDialog::channelAdded(int deviceSetIndex, ChannelAPI *channel) { + (void) deviceSetIndex; + const VLFTransmitters::Transmitter *transmitter = VLFTransmitters::m_callsignHash.value(ui->channels->item(m_row, COL_TX_NAME)->text()); ChannelWebAPIUtils::patchChannelSetting(channel, "title", transmitter->m_callsign); From 5b0d5b9efb087f36b7988824a6d9858d1f2ed9e3 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sat, 6 Apr 2024 22:36:33 +0100 Subject: [PATCH 10/14] Fix gcc warning --- plugins/feature/sid/sidaddchannelsdialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/feature/sid/sidaddchannelsdialog.cpp b/plugins/feature/sid/sidaddchannelsdialog.cpp index 416511f07..cad94cf91 100644 --- a/plugins/feature/sid/sidaddchannelsdialog.cpp +++ b/plugins/feature/sid/sidaddchannelsdialog.cpp @@ -61,7 +61,7 @@ SIDAddChannelsDialog::SIDAddChannelsDialog(SIDSettings *settings, QWidget* paren ui->channels->setItem(row, COL_TX_NAME, new QTableWidgetItem(VLFTransmitters::m_transmitters[j].m_callsign)); ui->channels->setItem(row, COL_TX_FREQUENCY, new QTableWidgetItem(QString::number(VLFTransmitters::m_transmitters[j].m_frequency))); - for (int i = 0; i < deviceSets.size(); i++) + for (unsigned int i = 0; i < deviceSets.size(); i++) { if (deviceSets[i]->m_deviceSourceEngine || deviceSets[i]->m_deviceMIMOEngine) { From a76262f0b79a47bb7b334830c0df7a00b56699d5 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sun, 7 Apr 2024 15:00:50 +0100 Subject: [PATCH 11/14] Ensure frequency and inputFrequencyOffset are consistent. --- plugins/channelrx/channelpower/channelpower.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/channelrx/channelpower/channelpower.cpp b/plugins/channelrx/channelpower/channelpower.cpp index 151b43673..42bca59e3 100644 --- a/plugins/channelrx/channelpower/channelpower.cpp +++ b/plugins/channelrx/channelpower/channelpower.cpp @@ -291,12 +291,12 @@ int ChannelPower::webapiSettingsPutPatch( // Ensure inputFrequencyOffset and frequency are consistent QStringList settingsKeys = channelSettingsKeys; - if (settingsKeys.contains("frequency") && (settings.m_frequencyMode == ChannelPowerSettings::Absolute)) + if (settingsKeys.contains("frequency") && !settingsKeys.contains("inputFrequencyOffset")) { settings.m_inputFrequencyOffset = settings.m_frequency - m_centerFrequency; settingsKeys.append("inputFrequencyOffset"); } - else if (settingsKeys.contains("inputFrequencyOffset") && (settings.m_frequencyMode == ChannelPowerSettings::Offset)) + else if (settingsKeys.contains("inputFrequencyOffset") && !settingsKeys.contains("frequency")) { settings.m_frequency = m_centerFrequency + settings.m_inputFrequencyOffset; settingsKeys.append("frequency"); From 304f0ea89ab28e96c1a74db59e5b666b0d418ee1 Mon Sep 17 00:00:00 2001 From: srcejon Date: Sun, 7 Apr 2024 16:51:55 +0100 Subject: [PATCH 12/14] Radio Clock: Add JJY. --- plugins/channelrx/channelpower/readme.md | 32 ++-- .../channelrx/radioclock/radioclockgui.cpp | 89 +++++++++- plugins/channelrx/radioclock/radioclockgui.h | 4 +- plugins/channelrx/radioclock/radioclockgui.ui | 29 +++- .../radioclock/radioclocksettings.cpp | 8 +- .../channelrx/radioclock/radioclocksettings.h | 10 +- .../channelrx/radioclock/radioclocksink.cpp | 156 +++++++++++++++++- plugins/channelrx/radioclock/radioclocksink.h | 3 +- plugins/channelrx/radioclock/readme.md | 44 +++-- .../api/swagger/include/RadioClock.yaml | 9 +- 10 files changed, 339 insertions(+), 45 deletions(-) diff --git a/plugins/channelrx/channelpower/readme.md b/plugins/channelrx/channelpower/readme.md index a176b3d43..cc8375ab8 100644 --- a/plugins/channelrx/channelpower/readme.md +++ b/plugins/channelrx/channelpower/readme.md @@ -1,4 +1,4 @@ -

Channel Power Plugin

+

Channel Power Plugin

Introduction

@@ -10,38 +10,48 @@ The top and bottom bars of the channel window are described [here](../../../sdrg ![Channel power plugin GUI](../../../doc/img/ChannelPower_plugin_settings.png) -

1: Frequency shift from center frequency of reception

+

1: Channel frequency entry mode

-Use the wheels to adjust the channel center frequency as a shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. +Select from one of the following modes to determine how the channel center frequency is calculated: -

2: BW - Channel Bandwidth

+* Δf - Specify an offset in Hz from device center frequency. +* f - Specific a frequency in Hz. + +

2: Channel Frequency

+ +Specifies channel center frequency according to frequency entry mode (1): + +* Δf - Offset in Hz from device center frequency. +* f - Absolute frequency in Hz. + +

3: BW - Channel Bandwidth

Bandwidth in Hz of the channel for which power is to be measured. -

3: Tavg - Average Time

+

4: Tavg - Average Time

Time period overwhich the channel power is averaged. Values range from 10us to 10s in powers of 10. The available values depend upon the sample rate. -

4: THp - Pulse Threshold

+

5: THp - Pulse Threshold

The pulse threshold sets the power in dB for which the channel power needs to exceed, in order to be included in the pulse average power measurement. -

5: Avg - Average Power

+

6: Avg - Average Power

Displays the most recent average power measurement in dB. -

6: Max - Max Peak Power

+

7: Max - Max Peak Power

Displays the maximum instantaneous peak power measurement in dB. -

7: Min - Min Peak Power

+

8: Min - Min Peak Power

Displays the minimum instantaneous peak power measurement in dB. -

8: Pulse - Pulse Average Power

+

9: Pulse - Pulse Average Power

Displays the most recent pulse average power measurement in dB. -

9: Clear Data

+

10: Clear Data

Clears current measurements (min and max values are reset). diff --git a/plugins/channelrx/radioclock/radioclockgui.cpp b/plugins/channelrx/radioclock/radioclockgui.cpp index 1b07ec698..8860c2233 100644 --- a/plugins/channelrx/radioclock/radioclockgui.cpp +++ b/plugins/channelrx/radioclock/radioclockgui.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2020-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // // Copyright (C) 2020-2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -139,8 +139,7 @@ bool RadioClockGUI::handleMessage(const Message& message) const DSPSignalNotification& notif = (const DSPSignalNotification&) message; m_deviceCenterFrequency = notif.getCenterFrequency(); m_basebandSampleRate = notif.getSampleRate(); - ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); - ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2)); + calcOffset(); updateAbsoluteCenterFrequency(); return true; } @@ -148,6 +147,23 @@ bool RadioClockGUI::handleMessage(const Message& message) return false; } +// Calculate input frequency offset, when device center frequency changes +void RadioClockGUI::calcOffset() +{ + if (m_settings.m_frequencyMode == RadioClockSettings::Offset) + { + ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); + } + else + { + qint64 offset = m_settings.m_frequency - m_deviceCenterFrequency; + m_channelMarker.setCenterFrequency(offset); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); + } +} + void RadioClockGUI::handleInputMessages() { Message* message; @@ -163,8 +179,22 @@ void RadioClockGUI::handleInputMessages() void RadioClockGUI::channelMarkerChangedByCursor() { - ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + m_settings.m_frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset; + + qint64 value = 0; + + if (m_settings.m_frequencyMode == RadioClockSettings::Offset) { + value = m_settings.m_inputFrequencyOffset; + } else if (m_settings.m_frequencyMode == RadioClockSettings::Absolute) { + value = m_settings.m_frequency; + } + + ui->deltaFrequency->blockSignals(true); + ui->deltaFrequency->setValue(value); + ui->deltaFrequency->blockSignals(false); + + updateAbsoluteCenterFrequency(); applySettings(); } @@ -173,9 +203,46 @@ void RadioClockGUI::channelMarkerHighlightedByCursor() setHighlighted(m_channelMarker.getHighlighted()); } +void RadioClockGUI::on_frequencyMode_currentIndexChanged(int index) +{ + m_settings.m_frequencyMode = (RadioClockSettings::FrequencyMode) index; + ui->deltaFrequency->blockSignals(true); + + if (m_settings.m_frequencyMode == RadioClockSettings::Offset) + { + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->deltaFrequency->setValue(m_settings.m_inputFrequencyOffset); + ui->deltaUnits->setText("Hz"); + } + else if (m_settings.m_frequencyMode == RadioClockSettings::Absolute) + { + ui->deltaFrequency->setValueRange(true, 11, 0, 99999999999, 0); + ui->deltaFrequency->setValue(m_settings.m_frequency); + ui->deltaUnits->setText("Hz"); + } + + ui->deltaFrequency->blockSignals(false); + + updateAbsoluteCenterFrequency(); + applySettings(); +} + void RadioClockGUI::on_deltaFrequency_changed(qint64 value) { - m_channelMarker.setCenterFrequency(value); + qint64 offset = 0; + + if (m_settings.m_frequencyMode == RadioClockSettings::Offset) + { + offset = value; + m_settings.m_frequency = m_deviceCenterFrequency + offset; + } + else if (m_settings.m_frequencyMode == RadioClockSettings::Absolute) + { + m_settings.m_frequency = value; + offset = m_settings.m_frequency - m_deviceCenterFrequency; + } + + m_channelMarker.setCenterFrequency(offset); m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); updateAbsoluteCenterFrequency(); applySettings(); @@ -300,7 +367,6 @@ RadioClockGUI::RadioClockGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Bas ui->status->setText("Looking for minute marker"); - ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); @@ -368,7 +434,8 @@ void RadioClockGUI::displaySettings() blockApplySettings(true); - ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + ui->frequencyMode->setCurrentIndex((int) m_settings.m_frequencyMode); + on_frequencyMode_currentIndexChanged((int) m_settings.m_frequencyMode); ui->rfBWText->setText(QString("%1 Hz").arg((int)m_settings.m_rfBandwidth)); ui->rfBW->setValue(m_settings.m_rfBandwidth); @@ -420,6 +487,7 @@ void RadioClockGUI::tick() void RadioClockGUI::makeUIConnections() { + QObject::connect(ui->frequencyMode, QOverload::of(&QComboBox::currentIndexChanged), this, &RadioClockGUI::on_frequencyMode_currentIndexChanged); QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &RadioClockGUI::on_deltaFrequency_changed); QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &RadioClockGUI::on_rfBW_valueChanged); QObject::connect(ui->threshold, &QDial::valueChanged, this, &RadioClockGUI::on_threshold_valueChanged); @@ -430,4 +498,11 @@ void RadioClockGUI::makeUIConnections() void RadioClockGUI::updateAbsoluteCenterFrequency() { setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); + if ( (m_basebandSampleRate > 1) + && ( (m_settings.m_inputFrequencyOffset >= m_basebandSampleRate / 2) + || (m_settings.m_inputFrequencyOffset < -m_basebandSampleRate / 2))) { + setStatusText("Frequency out of band"); + } else { + setStatusText(""); + } } diff --git a/plugins/channelrx/radioclock/radioclockgui.h b/plugins/channelrx/radioclock/radioclockgui.h index ec4ed36c3..a17d01ff7 100644 --- a/plugins/channelrx/radioclock/radioclockgui.h +++ b/plugins/channelrx/radioclock/radioclockgui.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2020-2022 Jon Beniston, M7RCE // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // // Copyright (C) 2020, 2022 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // @@ -93,6 +93,7 @@ private: bool handleMessage(const Message& message); void makeUIConnections(); void updateAbsoluteCenterFrequency(); + void calcOffset(); void displayDateTime(); @@ -100,6 +101,7 @@ private: void enterEvent(EnterEventType*); private slots: + void on_frequencyMode_currentIndexChanged(int index); void on_deltaFrequency_changed(qint64 value); void on_rfBW_valueChanged(int index); void on_threshold_valueChanged(int value); diff --git a/plugins/channelrx/radioclock/radioclockgui.ui b/plugins/channelrx/radioclock/radioclockgui.ui index ab7bf35ba..c9129783f 100644 --- a/plugins/channelrx/radioclock/radioclockgui.ui +++ b/plugins/channelrx/radioclock/radioclockgui.ui @@ -74,16 +74,32 @@ 2
- + - 16 + 40 0 - - Df + + + 40 + 16777215 + + + Select frequency entry mode. + + + + Δf + + + + + f + + @@ -370,6 +386,11 @@ WWVB + + + JJY + +
diff --git a/plugins/channelrx/radioclock/radioclocksettings.cpp b/plugins/channelrx/radioclock/radioclocksettings.cpp index 6b7efe2d2..2a0825c48 100644 --- a/plugins/channelrx/radioclock/radioclocksettings.cpp +++ b/plugins/channelrx/radioclock/radioclocksettings.cpp @@ -2,7 +2,7 @@ // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2020, 2022 Edouard Griffiths, F4EXB // -// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // // // 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 // @@ -35,7 +35,9 @@ RadioClockSettings::RadioClockSettings() : void RadioClockSettings::resetToDefaults() { + m_frequencyMode = Offset; m_inputFrequencyOffset = 0; + m_frequency = 0; m_rfBandwidth = 50.0f; m_threshold = 5; m_modulation = MSF; @@ -58,9 +60,11 @@ QByteArray RadioClockSettings::serialize() const s.writeS32(1, m_inputFrequencyOffset); s.writeFloat(2, m_rfBandwidth); + s.writeS64(3, m_frequency); s.writeFloat(4, m_threshold); s.writeS32(5, (int)m_modulation); s.writeS32(6, (int)m_timezone); + s.writeS32(7, (int)m_frequencyMode); s.writeU32(12, m_rgbColor); s.writeString(13, m_title); @@ -108,9 +112,11 @@ bool RadioClockSettings::deserialize(const QByteArray& data) d.readS32(1, &m_inputFrequencyOffset, 0); d.readFloat(2, &m_rfBandwidth, 50.0f); + d.readS64(3, &m_frequency, 0); d.readFloat(4, &m_threshold, 30); d.readS32(5, (int *)&m_modulation, DCF77); d.readS32(6, (int *)&m_timezone, BROADCAST); + d.readS32(7, (int *)&m_frequencyMode, Offset); d.readU32(12, &m_rgbColor, QColor(102, 0, 0).rgb()); d.readString(13, &m_title, "Radio Clock"); diff --git a/plugins/channelrx/radioclock/radioclocksettings.h b/plugins/channelrx/radioclock/radioclocksettings.h index 5a518fc9a..aa944aa53 100644 --- a/plugins/channelrx/radioclock/radioclocksettings.h +++ b/plugins/channelrx/radioclock/radioclocksettings.h @@ -2,7 +2,7 @@ // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2019, 2021-2022 Edouard Griffiths, F4EXB // -// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // // // 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 // @@ -30,14 +30,20 @@ class Serializable; struct RadioClockSettings { + enum FrequencyMode { + Offset, + Absolute + } m_frequencyMode; qint32 m_inputFrequencyOffset; + qint64 m_frequency; Real m_rfBandwidth; Real m_threshold; //!< For MSF and DCF in dB enum Modulation { MSF, DCF77, TDF, - WWVB + WWVB, + JJY } m_modulation; enum DisplayTZ { BROADCAST, diff --git a/plugins/channelrx/radioclock/radioclocksink.cpp b/plugins/channelrx/radioclock/radioclocksink.cpp index 4f3853c3e..21e00b6a4 100644 --- a/plugins/channelrx/radioclock/radioclocksink.cpp +++ b/plugins/channelrx/radioclock/radioclocksink.cpp @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2021-2022 Edouard Griffiths, F4EXB // -// Copyright (C) 2021 Jon Beniston, M7RCE // +// Copyright (C) 2021-2024 Jon Beniston, M7RCE // // // // 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 // @@ -794,6 +794,158 @@ void RadioClockSink::wwvb() m_prevData = m_data; } +// Japan JJY 40kHz +// https://en.wikipedia.org/wiki/JJY +void RadioClockSink::jjy() +{ + // JJY reduces carrier by -10dB + // Full power, then reduced power, which is the opposite of WWVB + // 0.8s full power is is zero bit, 0.5s full power is one bit + // 0.2s full power is a marker. Seven markers per minute (0, 9, 19, 29, 39, 49, and 59s) and for leap second + m_threshold = m_thresholdMovingAverage.asDouble() * m_linearThreshold; // xdB below average + m_data = m_magsq > m_threshold; + + // Look for minute marker - two consequtive markers + if ((m_data == 1) && (m_prevData == 0)) + { + if ( (m_highCount <= RadioClockSettings::RADIOCLOCK_CHANNEL_SAMPLE_RATE * 0.3) + && (m_lowCount >= RadioClockSettings::RADIOCLOCK_CHANNEL_SAMPLE_RATE * 0.7) + ) + { + if (m_gotMarker && !m_gotMinuteMarker) + { + qDebug() << "RadioClockSink::jjy - Minute marker: (low " << m_lowCount << " high " << m_highCount << ") prev period " << m_periodCount; + m_gotMinuteMarker = true; + m_second = 1; + m_secondMarkers = 1; + if (getMessageQueueToChannel()) { + getMessageQueueToChannel()->push(RadioClock::MsgStatus::create("Got minute marker")); + } + } + else + { + qDebug() << "RadioClockSink::jjy - Marker: (low " << m_lowCount << " high " << m_highCount << ") prev period " << m_periodCount << " second " << m_second; + } + m_gotMarker = true; + m_periodCount = 0; + } + else + { + m_gotMarker = false; + } + m_highCount = 0; + } + else if ((m_data == 0) && (m_prevData == 1)) + { + m_lowCount = 0; + } + else if (m_data == 1) + { + m_highCount++; + } + else if (m_data == 0) + { + m_lowCount++; + } + + m_sample = false; + if (m_gotMinuteMarker) + { + m_periodCount++; + if (m_periodCount == 100) + { + // Check we get second marker + m_secondMarkers += m_data == 1; + // If we see too many 0s instead of 1s, assume we've lost the signal + if ((m_second > 10) && (m_secondMarkers / m_second < 0.7)) + { + qDebug() << "RadioClockSink::jjy - Lost lock: " << m_secondMarkers << m_second; + m_gotMinuteMarker = false; + if (getMessageQueueToChannel()) { + getMessageQueueToChannel()->push(RadioClock::MsgStatus::create("Looking for minute marker")); + } + } + m_sample = true; + } + else if (m_periodCount == 650) + { + // Get data bit A for timecode + m_timeCode[m_second] = !m_data; // No carrier = 1, carrier = 0 + m_sample = true; + } + else if (m_periodCount == 950) + { + if (m_second == 59) + { + // Check markers are decoded as 1s + const QList markerBits = {9, 19, 29, 39, 49, 59}; + int missingMarkers = 0; + for (int i = 0; i < markerBits.size(); i++) + { + if (m_timeCode[markerBits[i]] != 1) + { + missingMarkers++; + qDebug() << "RadioClockSink::jjy - Missing marker at bit " << markerBits[i]; + } + } + if (missingMarkers >= 3) + { + m_gotMinuteMarker = false; + qDebug() << "RadioClockSink::jjy - Lost lock: Missing markers: " << missingMarkers; + if (getMessageQueueToChannel()) { + getMessageQueueToChannel()->push(RadioClock::MsgStatus::create("Looking for minute marker")); + } + } + + // Check 0s where expected + const QList zeroBits = {4, 10, 11, 14, 20, 21, 24, 34, 35, 44, 55, 56, 57, 58}; + for (int i = 0; i < zeroBits.size(); i++) + { + if (m_timeCode[zeroBits[i]] != 0) { + qDebug() << "RadioClockSink::jjy - Unexpected 1 at bit " << zeroBits[i]; + } + } + + // Decode timecode to time and date + int minute = bcdMSB(1, 8, 4); + int hour = bcdMSB(12, 18, 14); + int dayOfYear = bcdMSB(22, 33, 24, 29); + int year = 2000 + bcdMSB(41, 48); + + // Japan doesn't have daylight savings + m_dst = RadioClockSettings::NOT_IN_EFFECT; + + // Time is UTC + QDate date(year, 1, 1); + date = date.addDays(dayOfYear - 1); + m_dateTime = QDateTime(date, QTime(hour, minute), Qt::OffsetFromUTC, 0); + if (getMessageQueueToChannel()) { + getMessageQueueToChannel()->push(RadioClock::MsgStatus::create("OK")); + } + + m_second = 0; + } + else + { + m_second++; + m_dateTime = m_dateTime.addSecs(1); + } + + if (getMessageQueueToChannel()) + { + RadioClock::MsgDateTime *msg = RadioClock::MsgDateTime::create(m_dateTime, m_dst); + getMessageQueueToChannel()->push(msg); + } + } + else if (m_periodCount == 1000) + { + m_periodCount = 0; + } + } + + m_prevData = m_data; +} + void RadioClockSink::processOneSample(Complex &ci) { // Calculate average and peak levels for level meter @@ -817,6 +969,8 @@ void RadioClockSink::processOneSample(Complex &ci) tdf(ci); } else if (m_settings.m_modulation == RadioClockSettings::WWVB) { wwvb(); + } else if (m_settings.m_modulation == RadioClockSettings::JJY) { + jjy(); } else { msf60(); } diff --git a/plugins/channelrx/radioclock/radioclocksink.h b/plugins/channelrx/radioclock/radioclocksink.h index e8a9a3ebc..b79a6956a 100644 --- a/plugins/channelrx/radioclock/radioclocksink.h +++ b/plugins/channelrx/radioclock/radioclocksink.h @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2019-2021 Edouard Griffiths, F4EXB // -// Copyright (C) 2020-2021 Jon Beniston, M7RCE // +// Copyright (C) 2020-2024 Jon Beniston, M7RCE // // Copyright (C) 2020 Kacper Michajłow // // // // This program is free software; you can redistribute it and/or modify // @@ -160,6 +160,7 @@ private: void tdf(Complex &ci); void msf60(); void wwvb(); + void jjy(); }; #endif // INCLUDE_RADIOCLOCKSINK_H diff --git a/plugins/channelrx/radioclock/readme.md b/plugins/channelrx/radioclock/readme.md index ca1ab245e..716a560c2 100644 --- a/plugins/channelrx/radioclock/readme.md +++ b/plugins/channelrx/radioclock/readme.md @@ -1,4 +1,4 @@ -

Radio clock plugin

+

Radio Clock Plugin

Introduction

@@ -8,6 +8,7 @@ This plugin can be used to receive the time and date as broadcast on Low Frequen * [DCF77](https://en.wikipedia.org/wiki/DCF77) - Germany - 77.5kHz * [TDF](https://en.wikipedia.org/wiki/TDF_time_signal) - France - 162kHz * [WWVB](https://en.wikipedia.org/wiki/WWVB) - USA - 60kHz +* [JJY](https://en.wikipedia.org/wiki/JJY) - Japan - 40kHz If you'd like other transmitters to be supported, please upload a .sdriq file to SDRangel's [github issue tracker](https://github.com/f4exb/sdrangel/issues). @@ -21,29 +22,39 @@ The top and bottom bars of the channel window are described [here](../../../sdrg ![Radio clock plugin GUI](../../../doc/img/RadioClock_plugin.png) -

1: Frequency shift from center frequency of reception

+

1: Channel frequency entry mode

-Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. +Select from one of the following modes to determine how the channel center frequency is calculated: -

2: Channel power

+* Δf - Specify an offset in Hz from device center frequency. +* f - Specific a frequency in Hz. + +

2: Channel Frequency

+ +Specifies channel center frequency according to frequency entry mode (1): + +* Δf - Offset in Hz from device center frequency. +* f - Absolute frequency in Hz. + +

3: Channel power

Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. -

3: Level meter in dB

+

4: Level meter in dB

- top bar (green): average value - bottom bar (blue green): instantaneous peak value - tip vertical bar (bright green): peak hold value -

4: BW - RF Bandwidth

+

5: BW - RF Bandwidth

This specifies the bandwidth of a LPF that is applied to the input signal to limit the RF bandwidth. -

5: TH - Threshold

+

6: TH - Threshold

-For MSF, DCF77 and WWVB, specifies the threshold in dB below the average carrier power level that determines a binary 0 or 1. +For MSF, DCF77, WWVB and JJY, specifies the threshold in dB below the average carrier power level that determines a binary 0 or 1. -

6: Modulation

+

7: Modulation

Specifies the modulation and timecode encoding used: @@ -51,8 +62,9 @@ Specifies the modulation and timecode encoding used: * DCF77 - OOK (On-off keying) * TDF - PM (Phase modulation) * WWVB - OOK (On-off keying) +* JJY - OOK (On-off keying) -

7: Display Time Zone

+

8: Display Time Zone

Specifies the time zone used to display the received time. This can be: @@ -60,15 +72,15 @@ Specifies the time zone used to display the received time. This can be: * Local - the time is converted to the local time (as determined by your operating system's time zone). * UTC - the time is converted to Coordinated Universal Time. -

8: Date

+

9: Date

Displays the decoded date. -

9: Time

+

10: Time

-Displays the decoded time, adjusted for the time zone set by (7). +Displays the decoded time, adjusted for the time zone set by (8). -

10: Status

+

11: Status

Displays the demodulator status. This can be: @@ -81,7 +93,7 @@ The date and time fields are only valid when the status indicates OK. If while in the OK state several second markers are not detected, the status will return to Looking for minute marker. -

11: Daylight Savings

+

12: Daylight Savings

Displays the daylight savings state: @@ -90,7 +102,7 @@ Displays the daylight savings state: * Starting * Ending -For MSF, DCF77 and TDF, starting/ending is indicated one hour before the change. For WWVB it is set for the whole day. +For MSF, DCF77 and TDF, starting/ending is indicated one hour before the change. For WWVB it is set for the whole day. Japan does not use daylight savings.

Waveforms

diff --git a/swagger/sdrangel/api/swagger/include/RadioClock.yaml b/swagger/sdrangel/api/swagger/include/RadioClock.yaml index f7502ae5c..50b771452 100644 --- a/swagger/sdrangel/api/swagger/include/RadioClock.yaml +++ b/swagger/sdrangel/api/swagger/include/RadioClock.yaml @@ -5,6 +5,13 @@ RadioClockSettings: description: channel center frequency shift from baseband center in Hz type: integer format: int64 + frequencyMode: + description: (0 for Offset, 1 for Absolute) + type: integer + frequency: + description: Channel center frequency + type: integer + format: int64 rfBandwidth: description: channel RF bandwidth in Hz type: number @@ -13,7 +20,7 @@ RadioClockSettings: type: number format: float modulation: - description: 0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB + description: 0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB, 4 - JJY type: integer timezone: description: 0 - Broadcast, 1 - Local, 2 - UTC From 7b6bbe88f3a91738103eda92ad668c0e52233a6e Mon Sep 17 00:00:00 2001 From: srcejon Date: Sun, 7 Apr 2024 16:53:16 +0100 Subject: [PATCH 13/14] Regenerate swagger files --- sdrbase/resources/webapi/doc/html2/index.html | 13 +++++- .../doc/swagger/include/RadioClock.yaml | 9 +++- swagger/sdrangel/code/html2/index.html | 13 +++++- .../code/qt5/client/SWGRadioClockSettings.cpp | 46 +++++++++++++++++++ .../code/qt5/client/SWGRadioClockSettings.h | 12 +++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index aa5313fcf..304040e07 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -12701,6 +12701,15 @@ margin-bottom: 20px; "format" : "int64", "description" : "channel center frequency shift from baseband center in Hz" }, + "frequencyMode" : { + "type" : "integer", + "description" : "(0 for Offset, 1 for Absolute)" + }, + "frequency" : { + "type" : "integer", + "format" : "int64", + "description" : "Channel center frequency" + }, "rfBandwidth" : { "type" : "number", "format" : "float", @@ -12712,7 +12721,7 @@ margin-bottom: 20px; }, "modulation" : { "type" : "integer", - "description" : "0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB" + "description" : "0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB, 4 - JJY" }, "timezone" : { "type" : "integer", @@ -58943,7 +58952,7 @@ except ApiException as e:
- Generated 2024-04-06T20:04:32.029+02:00 + Generated 2024-04-07T17:52:28.367+02:00
diff --git a/sdrbase/resources/webapi/doc/swagger/include/RadioClock.yaml b/sdrbase/resources/webapi/doc/swagger/include/RadioClock.yaml index 8acd20869..600ee71bb 100644 --- a/sdrbase/resources/webapi/doc/swagger/include/RadioClock.yaml +++ b/sdrbase/resources/webapi/doc/swagger/include/RadioClock.yaml @@ -5,6 +5,13 @@ RadioClockSettings: description: channel center frequency shift from baseband center in Hz type: integer format: int64 + frequencyMode: + description: (0 for Offset, 1 for Absolute) + type: integer + frequency: + description: Channel center frequency + type: integer + format: int64 rfBandwidth: description: channel RF bandwidth in Hz type: number @@ -13,7 +20,7 @@ RadioClockSettings: type: number format: float modulation: - description: 0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB + description: 0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB, 4 - JJY type: integer timezone: description: 0 - Broadcast, 1 - Local, 2 - UTC diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index aa5313fcf..304040e07 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -12701,6 +12701,15 @@ margin-bottom: 20px; "format" : "int64", "description" : "channel center frequency shift from baseband center in Hz" }, + "frequencyMode" : { + "type" : "integer", + "description" : "(0 for Offset, 1 for Absolute)" + }, + "frequency" : { + "type" : "integer", + "format" : "int64", + "description" : "Channel center frequency" + }, "rfBandwidth" : { "type" : "number", "format" : "float", @@ -12712,7 +12721,7 @@ margin-bottom: 20px; }, "modulation" : { "type" : "integer", - "description" : "0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB" + "description" : "0 - MSF, 1 - DCF77, 2 - TDF, 3 - WWVB, 4 - JJY" }, "timezone" : { "type" : "integer", @@ -58943,7 +58952,7 @@ except ApiException as e:
- Generated 2024-04-06T20:04:32.029+02:00 + Generated 2024-04-07T17:52:28.367+02:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.cpp index 5f85a1499..3d6a1e14c 100644 --- a/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.cpp @@ -30,6 +30,10 @@ SWGRadioClockSettings::SWGRadioClockSettings(QString* json) { SWGRadioClockSettings::SWGRadioClockSettings() { input_frequency_offset = 0L; m_input_frequency_offset_isSet = false; + frequency_mode = 0; + m_frequency_mode_isSet = false; + frequency = 0L; + m_frequency_isSet = false; rf_bandwidth = 0.0f; m_rf_bandwidth_isSet = false; threshold = 0.0f; @@ -70,6 +74,10 @@ void SWGRadioClockSettings::init() { input_frequency_offset = 0L; m_input_frequency_offset_isSet = false; + frequency_mode = 0; + m_frequency_mode_isSet = false; + frequency = 0L; + m_frequency_isSet = false; rf_bandwidth = 0.0f; m_rf_bandwidth_isSet = false; threshold = 0.0f; @@ -110,6 +118,8 @@ SWGRadioClockSettings::cleanup() { + + if(title != nullptr) { delete title; } @@ -145,6 +155,10 @@ void SWGRadioClockSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + ::SWGSDRangel::setValue(&frequency_mode, pJson["frequencyMode"], "qint32", ""); + + ::SWGSDRangel::setValue(&frequency, pJson["frequency"], "qint64", ""); + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); ::SWGSDRangel::setValue(&threshold, pJson["threshold"], "float", ""); @@ -194,6 +208,12 @@ SWGRadioClockSettings::asJsonObject() { if(m_input_frequency_offset_isSet){ obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); } + if(m_frequency_mode_isSet){ + obj->insert("frequencyMode", QJsonValue(frequency_mode)); + } + if(m_frequency_isSet){ + obj->insert("frequency", QJsonValue(frequency)); + } if(m_rf_bandwidth_isSet){ obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); } @@ -253,6 +273,26 @@ SWGRadioClockSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { this->m_input_frequency_offset_isSet = true; } +qint32 +SWGRadioClockSettings::getFrequencyMode() { + return frequency_mode; +} +void +SWGRadioClockSettings::setFrequencyMode(qint32 frequency_mode) { + this->frequency_mode = frequency_mode; + this->m_frequency_mode_isSet = true; +} + +qint64 +SWGRadioClockSettings::getFrequency() { + return frequency; +} +void +SWGRadioClockSettings::setFrequency(qint64 frequency) { + this->frequency = frequency; + this->m_frequency_isSet = true; +} + float SWGRadioClockSettings::getRfBandwidth() { return rf_bandwidth; @@ -411,6 +451,12 @@ SWGRadioClockSettings::isSet(){ if(m_input_frequency_offset_isSet){ isObjectUpdated = true; break; } + if(m_frequency_mode_isSet){ + isObjectUpdated = true; break; + } + if(m_frequency_isSet){ + isObjectUpdated = true; break; + } if(m_rf_bandwidth_isSet){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.h b/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.h index 2f651135a..11983fad6 100644 --- a/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGRadioClockSettings.h @@ -48,6 +48,12 @@ public: qint64 getInputFrequencyOffset(); void setInputFrequencyOffset(qint64 input_frequency_offset); + qint32 getFrequencyMode(); + void setFrequencyMode(qint32 frequency_mode); + + qint64 getFrequency(); + void setFrequency(qint64 frequency); + float getRfBandwidth(); void setRfBandwidth(float rf_bandwidth); @@ -100,6 +106,12 @@ private: qint64 input_frequency_offset; bool m_input_frequency_offset_isSet; + qint32 frequency_mode; + bool m_frequency_mode_isSet; + + qint64 frequency; + bool m_frequency_isSet; + float rf_bandwidth; bool m_rf_bandwidth_isSet; From c9632b9fe30c718ba452af3bf38739ae2276194c Mon Sep 17 00:00:00 2001 From: srcejon Date: Sun, 7 Apr 2024 17:02:43 +0100 Subject: [PATCH 14/14] Add new settings to API. --- plugins/channelrx/radioclock/radioclock.cpp | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/plugins/channelrx/radioclock/radioclock.cpp b/plugins/channelrx/radioclock/radioclock.cpp index aa42cde5f..8b6a2291e 100644 --- a/plugins/channelrx/radioclock/radioclock.cpp +++ b/plugins/channelrx/radioclock/radioclock.cpp @@ -233,9 +233,15 @@ void RadioClock::applySettings(const RadioClockSettings& settings, bool force) QList reverseAPIKeys; + if ((settings.m_frequencyMode != m_settings.m_frequencyMode) || force) { + reverseAPIKeys.append("frequencyMode"); + } if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { reverseAPIKeys.append("inputFrequencyOffset"); } + if ((settings.m_frequency != m_settings.m_frequency) || force) { + reverseAPIKeys.append("frequency"); + } if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { reverseAPIKeys.append("rfBandwidth"); } @@ -331,6 +337,13 @@ int RadioClock::webapiSettingsPutPatch( RadioClockSettings settings = m_settings; webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + // Ensure inputFrequencyOffset and frequency are consistent + if (channelSettingsKeys.contains("frequency") && !channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = settings.m_frequency - m_centerFrequency; + } else if (channelSettingsKeys.contains("inputFrequencyOffset") && !channelSettingsKeys.contains("frequency")) { + settings.m_frequency = m_centerFrequency + settings.m_inputFrequencyOffset; + } + MsgConfigureRadioClock *msg = MsgConfigureRadioClock::create(settings, force); m_inputMessageQueue.push(msg); @@ -351,9 +364,15 @@ void RadioClock::webapiUpdateChannelSettings( const QStringList& channelSettingsKeys, SWGSDRangel::SWGChannelSettings& response) { + if (channelSettingsKeys.contains("frequencyMode")) { + settings.m_frequencyMode = (RadioClockSettings::FrequencyMode) response.getRadioClockSettings()->getFrequencyMode(); + } if (channelSettingsKeys.contains("inputFrequencyOffset")) { settings.m_inputFrequencyOffset = response.getRadioClockSettings()->getInputFrequencyOffset(); } + if (channelSettingsKeys.contains("frequency")) { + settings.m_frequency = response.getRadioClockSettings()->getFrequency(); + } if (channelSettingsKeys.contains("rfBandwidth")) { settings.m_rfBandwidth = response.getRadioClockSettings()->getRfBandwidth(); } @@ -403,7 +422,9 @@ void RadioClock::webapiUpdateChannelSettings( void RadioClock::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const RadioClockSettings& settings) { + response.getRadioClockSettings()->setFrequencyMode((int)settings.m_frequencyMode); response.getRadioClockSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getRadioClockSettings()->setFrequency(settings.m_frequency); response.getRadioClockSettings()->setRfBandwidth(settings.m_rfBandwidth); response.getRadioClockSettings()->setThreshold(settings.m_threshold); response.getRadioClockSettings()->setModulation((int)settings.m_modulation); @@ -536,9 +557,15 @@ void RadioClock::webapiFormatChannelSettings( // transfer data that has been modified. When force is on transfer all data except reverse API data + if (channelSettingsKeys.contains("frequencyMode") || force) { + swgRadioClockSettings->setFrequencyMode(settings.m_frequencyMode); + } if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { swgRadioClockSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); } + if (channelSettingsKeys.contains("frequency") || force) { + swgRadioClockSettings->setFrequency(settings.m_frequency); + } if (channelSettingsKeys.contains("rfBandwidth") || force) { swgRadioClockSettings->setRfBandwidth(settings.m_rfBandwidth); }