diff --git a/doc/img/HeatMap_plugin_map.png b/doc/img/HeatMap_plugin_map.png new file mode 100644 index 000000000..42b1f3488 Binary files /dev/null and b/doc/img/HeatMap_plugin_map.png differ diff --git a/doc/img/HeatMap_plugin_power.png b/doc/img/HeatMap_plugin_power.png new file mode 100644 index 000000000..2c920daea Binary files /dev/null and b/doc/img/HeatMap_plugin_power.png differ diff --git a/doc/img/HeatMap_plugin_settings.png b/doc/img/HeatMap_plugin_settings.png new file mode 100644 index 000000000..75b8490f0 Binary files /dev/null and b/doc/img/HeatMap_plugin_settings.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index e00a561fe..e0350abc5 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -114,6 +114,8 @@ if (ENABLE_CHANNELRX_DEMODFT8 AND FT8_SUPPORT) endif() if(NOT SERVER_MODE) + add_subdirectory(heatmap) + if (ENABLE_CHANNELRX_CHANALYZER) add_subdirectory(chanalyzer) endif() diff --git a/plugins/channelrx/heatmap/CMakeLists.txt b/plugins/channelrx/heatmap/CMakeLists.txt new file mode 100644 index 000000000..0d86be5bb --- /dev/null +++ b/plugins/channelrx/heatmap/CMakeLists.txt @@ -0,0 +1,68 @@ +project(heatmap) + +set(heatmap_SOURCES + heatmap.cpp + heatmapsettings.cpp + heatmapbaseband.cpp + heatmapsink.cpp + heatmapplugin.cpp + heatmapwebapiadapter.cpp +) + +set(heatmap_HEADERS + heatmap.h + heatmapsettings.h + heatmapbaseband.h + heatmapsink.h + heatmapplugin.h + heatmapwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(heatmap_SOURCES + ${heatmap_SOURCES} + heatmapgui.cpp + heatmapgui.ui + ) + set(heatmap_HEADERS + ${heatmap_HEADERS} + heatmapgui.h + ) + + set(TARGET_NAME heatmap) + set(TARGET_LIB "Qt::Widgets" Qt::Charts) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME heatmapsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${heatmap_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/heatmap/heatmap.cpp b/plugins/channelrx/heatmap/heatmap.cpp new file mode 100644 index 000000000..8af7d837c --- /dev/null +++ b/plugins/channelrx/heatmap/heatmap.cpp @@ -0,0 +1,578 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 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 "heatmap.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGChannelReport.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(HeatMap::MsgConfigureHeatMap, Message) + +const char * const HeatMap::m_channelIdURI = "sdrangel.channel.heatmap"; +const char * const HeatMap::m_channelId = "HeatMap"; + +HeatMap::HeatMap(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new HeatMapBaseband(this); + m_basebandSink->setMessageQueueToChannel(getInputMessageQueue()); + m_basebandSink->setChannel(this); + m_basebandSink->moveToThread(&m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &HeatMap::networkManagerFinished + ); + QObject::connect( + this, + &ChannelAPI::indexInDeviceSetChanged, + this, + &HeatMap::handleIndexInDeviceSetChanged + ); +} + +HeatMap::~HeatMap() +{ + qDebug("HeatMap::~HeatMap"); + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &HeatMap::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +void HeatMap::setDeviceAPI(DeviceAPI *deviceAPI) +{ + if (deviceAPI != m_deviceAPI) + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + m_deviceAPI = deviceAPI; + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + } +} + +uint32_t HeatMap::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void HeatMap::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void HeatMap::start() +{ + qDebug("HeatMap::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + HeatMapBaseband::MsgConfigureHeatMapBaseband *msg = HeatMapBaseband::MsgConfigureHeatMapBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void HeatMap::stop() +{ + qDebug("HeatMap::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool HeatMap::handleMessage(const Message& cmd) +{ + if (MsgConfigureHeatMap::match(cmd)) + { + MsgConfigureHeatMap& cfg = (MsgConfigureHeatMap&) cmd; + qDebug() << "HeatMap::handleMessage: MsgConfigureHeatMap"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "HeatMap::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(new DSPSignalNotification(notif)); + } + + return true; + } + else + { + return false; + } +} + +ScopeVis *HeatMap::getScopeSink() +{ + return m_basebandSink->getScopeSink(); +} + +void HeatMap::setCenterFrequency(qint64 frequency) +{ + HeatMapSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureHeatMap *msgToGUI = MsgConfigureHeatMap::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void HeatMap::applySettings(const HeatMapSettings& settings, bool force) +{ + qDebug() << "HeatMap::applySettings:" + << " m_streamIndex: " << settings.m_streamIndex + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSink(this, settings.m_streamIndex); + m_deviceAPI->addChannelSinkAPI(this); + } + + reverseAPIKeys.append("streamIndex"); + } + + HeatMapBaseband::MsgConfigureHeatMapBaseband *msg = HeatMapBaseband::MsgConfigureHeatMapBaseband::create(settings, force); + m_basebandSink->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +QByteArray HeatMap::serialize() const +{ + return m_settings.serialize(); +} + +bool HeatMap::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureHeatMap *msg = MsgConfigureHeatMap::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureHeatMap *msg = MsgConfigureHeatMap::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int HeatMap::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings()); + response.getHeatMapSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int HeatMap::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int HeatMap::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + HeatMapSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureHeatMap *msg = MsgConfigureHeatMap::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("HeatMap::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureHeatMap *msgToGUI = MsgConfigureHeatMap::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void HeatMap::webapiUpdateChannelSettings( + HeatMapSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getHeatMapSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getHeatMapSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("minPower")) { + settings.m_minPower = response.getHeatMapSettings()->getMinPower(); + } + if (channelSettingsKeys.contains("maxPower")) { + settings.m_maxPower = response.getHeatMapSettings()->getMaxPower(); + } + if (channelSettingsKeys.contains("colorMapName")) { + settings.m_colorMapName = *response.getHeatMapSettings()->getColorMapName(); + } + if (channelSettingsKeys.contains("mode")) { + settings.m_mode = (HeatMapSettings::Mode)response.getHeatMapSettings()->getMode(); + } + if (channelSettingsKeys.contains("pulseThreshold")) { + settings.m_pulseThreshold = response.getHeatMapSettings()->getPulseThreshold(); + } + if (channelSettingsKeys.contains("averagePeriodUS")) { + settings.m_averagePeriodUS = response.getHeatMapSettings()->getAveragePeriodUs(); + } + if (channelSettingsKeys.contains("sampleRate")) { + settings.m_sampleRate = response.getHeatMapSettings()->getSampleRate(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getHeatMapSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getHeatMapSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getHeatMapSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getHeatMapSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getHeatMapSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getHeatMapSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getHeatMapSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getHeatMapSettings()->getReverseApiChannelIndex(); + } + if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) { + settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getHeatMapSettings()->getScopeConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getHeatMapSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getHeatMapSettings()->getRollupState()); + } +} + +void HeatMap::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const HeatMapSettings& settings) +{ + response.getHeatMapSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getHeatMapSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getHeatMapSettings()->setMinPower(settings.m_minPower); + response.getHeatMapSettings()->setMaxPower(settings.m_maxPower); + response.getHeatMapSettings()->setColorMapName(new QString(settings.m_colorMapName)); + response.getHeatMapSettings()->setMode((int) settings.m_mode); + response.getHeatMapSettings()->setPulseThreshold(settings.m_pulseThreshold); + response.getHeatMapSettings()->setAveragePeriodUs(settings.m_averagePeriodUS); + response.getHeatMapSettings()->setSampleRate(settings.m_sampleRate); + + response.getHeatMapSettings()->setRgbColor(settings.m_rgbColor); + if (response.getHeatMapSettings()->getTitle()) { + *response.getHeatMapSettings()->getTitle() = settings.m_title; + } else { + response.getHeatMapSettings()->setTitle(new QString(settings.m_title)); + } + + response.getHeatMapSettings()->setStreamIndex(settings.m_streamIndex); + response.getHeatMapSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getHeatMapSettings()->getReverseApiAddress()) { + *response.getHeatMapSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getHeatMapSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getHeatMapSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getHeatMapSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getHeatMapSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_scopeGUI) + { + if (response.getHeatMapSettings()->getScopeConfig()) + { + settings.m_scopeGUI->formatTo(response.getHeatMapSettings()->getScopeConfig()); + } + else + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + response.getHeatMapSettings()->setScopeConfig(swgGLScope); + } + } + + if (settings.m_channelMarker) + { + if (response.getHeatMapSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getHeatMapSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getHeatMapSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getHeatMapSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getHeatMapSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getHeatMapSettings()->setRollupState(swgRollupState); + } + } +} + +void HeatMap::webapiReverseSendSettings(QList& channelSettingsKeys, const HeatMapSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgChannelSettings; +} + +void HeatMap::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const HeatMapSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("HeatMap")); + swgChannelSettings->setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings()); + SWGSDRangel::SWGHeatMapSettings *swgHeatMapSettings = swgChannelSettings->getHeatMapSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgHeatMapSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgHeatMapSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("minPower") || force) { + swgHeatMapSettings->setMinPower(settings.m_minPower); + } + if (channelSettingsKeys.contains("maxPower") || force) { + swgHeatMapSettings->setMaxPower(settings.m_maxPower); + } + if (channelSettingsKeys.contains("colorMapName") || force) { + swgHeatMapSettings->setColorMapName(new QString(settings.m_colorMapName)); + } + if (channelSettingsKeys.contains("mode") || force) { + swgHeatMapSettings->setMode((int) settings.m_mode); + } + if (channelSettingsKeys.contains("pulseThreshold") || force) { + swgHeatMapSettings->setPulseThreshold(settings.m_pulseThreshold); + } + if (channelSettingsKeys.contains("averagePeriodUS") || force) { + swgHeatMapSettings->setAveragePeriodUs(settings.m_averagePeriodUS); + } + if (channelSettingsKeys.contains("sampleRate") || force) { + swgHeatMapSettings->setSampleRate(settings.m_sampleRate); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgHeatMapSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgHeatMapSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgHeatMapSettings->setStreamIndex(settings.m_streamIndex); + } + + if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force)) + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + swgHeatMapSettings->setScopeConfig(swgGLScope); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgHeatMapSettings->setChannelMarker(swgChannelMarker); + } +} + +void HeatMap::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "HeatMap::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("HeatMap::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void HeatMap::handleIndexInDeviceSetChanged(int index) +{ + if (index < 0) { + return; + } + + QString fifoLabel = QString("%1 [%2:%3]") + .arg(m_channelId) + .arg(m_deviceAPI->getDeviceSetIndex()) + .arg(index); + m_basebandSink->setFifoLabel(fifoLabel); +} + diff --git a/plugins/channelrx/heatmap/heatmap.h b/plugins/channelrx/heatmap/heatmap.h new file mode 100644 index 000000000..b9df675cd --- /dev/null +++ b/plugins/channelrx/heatmap/heatmap.h @@ -0,0 +1,178 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 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_HEATMAP_H +#define INCLUDE_HEATMAP_H + +#include + +#include +#include +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "heatmapbaseband.h" +#include "heatmapsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ScopeVis; + +class HeatMap : public BasebandSampleSink, public ChannelAPI { +public: + class MsgConfigureHeatMap : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const HeatMapSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureHeatMap* create(const HeatMapSettings& settings, bool force) + { + return new MsgConfigureHeatMap(settings, force); + } + + private: + HeatMapSettings m_settings; + bool m_force; + + MsgConfigureHeatMap(const HeatMapSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + HeatMap(DeviceAPI *deviceAPI); + virtual ~HeatMap(); + virtual void destroy() { delete this; } + virtual void setDeviceAPI(DeviceAPI *deviceAPI); + virtual DeviceAPI *getDeviceAPI() { return m_deviceAPI; } + + using BasebandSampleSink::feed; + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); } + virtual QString getSinkName() { return objectName(); } + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual const QString& getURI() const { return getName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + virtual void setCenterFrequency(qint64 frequency); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return 0; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const HeatMapSettings& settings); + + static void webapiUpdateChannelSettings( + HeatMapSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + ScopeVis *getScopeSink(); + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } + + void getMagLevels(double& avg, double& pulseAvg, double &maxPeak, double &minPeak) + { + m_basebandSink->getMagLevels(avg, pulseAvg, maxPeak, minPeak); + } + + void resetMagLevels() { + m_basebandSink->resetMagLevels(); + } + +/* void setMessageQueueToGUI(MessageQueue* queue) override { + ChannelAPI::setMessageQueueToGUI(queue); + m_basebandSink->setMessageQueueToGUI(queue); + }*/ + + uint32_t getNumberOfDeviceStreams() const; + + static const char * const m_channelIdURI; + static const char * const m_channelId; + +private: + DeviceAPI *m_deviceAPI; + QThread m_thread; + HeatMapBaseband* m_basebandSink; + HeatMapSettings m_settings; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_centerFrequency; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + virtual bool handleMessage(const Message& cmd); + void applySettings(const HeatMapSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& channelSettingsKeys, const HeatMapSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const HeatMapSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); +}; + +#endif // INCLUDE_HEATMAP_H + diff --git a/plugins/channelrx/heatmap/heatmapbaseband.cpp b/plugins/channelrx/heatmap/heatmapbaseband.cpp new file mode 100644 index 000000000..2241ab7e2 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapbaseband.cpp @@ -0,0 +1,178 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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 "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "heatmapbaseband.h" + +MESSAGE_CLASS_DEFINITION(HeatMapBaseband::MsgConfigureHeatMapBaseband, Message) + +HeatMapBaseband::HeatMapBaseband(HeatMap *heatDemod) : + m_sink(heatDemod), + m_running(false) +{ + qDebug("HeatMapBaseband::HeatMapBaseband"); + + m_sink.setScopeSink(&m_scopeSink); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +HeatMapBaseband::~HeatMapBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void HeatMapBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void HeatMapBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &HeatMapBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void HeatMapBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &HeatMapBaseband::handleData + ); + m_running = false; +} + +void HeatMapBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void HeatMapBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void HeatMapBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0)) + { + SampleVector::iterator part1begin; + SampleVector::iterator part1end; + SampleVector::iterator part2begin; + SampleVector::iterator part2end; + + std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end); + + // first part of FIFO data + if (part1begin != part1end) { + m_channelizer->feed(part1begin, part1end); + } + + // second part of FIFO data (used when block wraps around) + if(part2begin != part2end) { + m_channelizer->feed(part2begin, part2end); + } + + m_sampleFifo.readCommit((unsigned int) count); + } +} + +void HeatMapBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool HeatMapBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureHeatMapBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureHeatMapBaseband& cfg = (MsgConfigureHeatMapBaseband&) cmd; + qDebug() << "HeatMapBaseband::handleMessage: MsgConfigureHeatMapBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "HeatMapBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + + return true; + } + else + { + return false; + } +} + +void HeatMapBaseband::applySettings(const HeatMapSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) + || (settings.m_sampleRate != m_settings.m_sampleRate) + || force) + { + m_channelizer->setChannelization(settings.m_sampleRate, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +void HeatMapBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} + diff --git a/plugins/channelrx/heatmap/heatmapbaseband.h b/plugins/channelrx/heatmap/heatmapbaseband.h new file mode 100644 index 000000000..34ef62301 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapbaseband.h @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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_HEATMAPBASEBAND_H +#define INCLUDE_HEATMAPBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/scopevis.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "heatmapsink.h" + +class DownChannelizer; +class ChannelAPI; +class HeatMap; +class ScopeVis; + +class HeatMapBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureHeatMapBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const HeatMapSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureHeatMapBaseband* create(const HeatMapSettings& settings, bool force) + { + return new MsgConfigureHeatMapBaseband(settings, force); + } + + private: + HeatMapSettings m_settings; + bool m_force; + + MsgConfigureHeatMapBaseband(const HeatMapSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + HeatMapBaseband(HeatMap *heatDemod); + ~HeatMapBaseband(); + void reset(); + void startWork(); + void stopWork(); + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void getMagSqLevels(double& avg, double &peak, int& nbSamples) { + m_sink.getMagSqLevels(avg, peak, nbSamples); + } + void getMagLevels(double& avg, double& pulseAvg, double &maxPeak, double &minPeak) { + m_sink.getMagLevels(avg, pulseAvg, maxPeak, minPeak); + } + void resetMagLevels() { + m_sink.resetMagLevels(); + } + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } + void setBasebandSampleRate(int sampleRate); + ScopeVis *getScopeSink() { return &m_scopeSink; } + void setChannel(ChannelAPI *channel); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + HeatMapSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + HeatMapSettings m_settings; + ScopeVis m_scopeSink; + bool m_running; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(HeatMapSink *sink); + void applySettings(const HeatMapSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_HEATMAPBASEBAND_H + diff --git a/plugins/channelrx/heatmap/heatmapgui.cpp b/plugins/channelrx/heatmap/heatmapgui.cpp new file mode 100644 index 000000000..bcdedadde --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapgui.cpp @@ -0,0 +1,1469 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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 +#include +#include +#include +#include +#include +#include + +#include "heatmapgui.h" + +#include "device/deviceuiset.h" +#include "device/deviceapi.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_heatmapgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/astronomy.h" +#include "util/csv.h" +#include "util/db.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "gui/dialpopup.h" +#include "gui/dialogpositioner.h" +#include "dsp/dspengine.h" +#include "gui/crightclickenabler.h" +#include "gui/tabletapandhold.h" +#include "channel/channelwebapiutils.h" +#include "maincore.h" +#include "feature/featurewebapiutils.h" + +#include "heatmap.h" +#include "heatmapsink.h" + +#include "SWGMapItem.h" + +HeatMapGUI* HeatMapGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + HeatMapGUI* gui = new HeatMapGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void HeatMapGUI::destroy() +{ + delete this; +} + +void HeatMapGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray HeatMapGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool HeatMapGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +bool HeatMapGUI::handleMessage(const Message& message) +{ + if (HeatMap::MsgConfigureHeatMap::match(message)) + { + qDebug("HeatMapGUI::handleMessage: HeatMap::MsgConfigureHeatMap"); + const HeatMap::MsgConfigureHeatMap& cfg = (HeatMap::MsgConfigureHeatMap&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + ui->scopeGUI->updateSettings(); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_deviceCenterFrequency = notif.getCenterFrequency(); + m_basebandSampleRate = notif.getSampleRate(); + ui->rfBW->setMaximum(m_basebandSampleRate/100); + 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)); + updateAbsoluteCenterFrequency(); + return true; + } + + return false; +} + +void HeatMapGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void HeatMapGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void HeatMapGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void HeatMapGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void HeatMapGUI::on_rfBW_valueChanged(int value) +{ + float bw = value * 100.0f; + ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void HeatMapGUI::on_minPower_valueChanged(double value) +{ + m_settings.m_minPower = (float)value; + plotMap(); + if (m_powerYAxis) { + m_powerYAxis->setMin(m_settings.m_minPower); + } + applySettings(); +} + +void HeatMapGUI::on_maxPower_valueChanged(double value) +{ + m_settings.m_maxPower = (float)value; + plotMap(); + if (m_powerYAxis) { + m_powerYAxis->setMax(m_settings.m_maxPower); + } + applySettings(); +} + +void HeatMapGUI::on_colorMap_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_colorMapName = ui->colorMap->currentText(); + m_colorMap = ColorMap::getColorMap(m_settings.m_colorMapName); + } + plotMap(); + applySettings(); +} + +void HeatMapGUI::on_pulseTH_valueChanged(int value) +{ + m_settings.m_pulseThreshold = (float)value; + ui->pulseTHText->setText(QString::number(value)); + applySettings(); +} + +const QStringList HeatMapGUI::m_averagePeriodTexts = { + "10us", "100us", "1ms", "10ms", "100ms", "1s", "10s" +}; + +void HeatMapGUI::on_averagePeriod_valueChanged(int value) +{ + m_settings.m_averagePeriodUS = (int)std::powf(10.0f, (float)value); + ui->averagePeriodText->setText(m_averagePeriodTexts[value-1]); + applySettings(); +} + +const QStringList HeatMapGUI::m_sampleRateTexts = { + "10", "100", "1k", "10k", "100k", "1M" +}; + +void HeatMapGUI::on_sampleRate_valueChanged(int value) +{ + m_settings.m_sampleRate = (int)std::powf(10.0f, (float)value); + ui->sampleRateText->setText(m_sampleRateTexts[value-1]); + ui->averagePeriod->setMinimum(std::max(1, m_averagePeriodTexts.size() - value)); + m_scopeVis->setLiveRate(m_settings.m_sampleRate); + applySettings(); +} + +void HeatMapGUI::on_mode_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_mode = (HeatMapSettings::Mode)index; + bool none = m_settings.m_mode == HeatMapSettings::None; + ui->writeImage->setEnabled(!none); + ui->writeCSV->setEnabled(!none); + ui->readCSV->setEnabled(!none); + if (none) { + deleteFromMap(); + } else { + plotMap(); + } + applySettings(); + } +} + +void HeatMapGUI::displayPowerChart() +{ + if (m_settings.m_displayChart) + { + ui->chartContainer->setVisible(true); + plotPowerVsTimeChart(); + } + else + { + ui->chartContainer->setVisible(false); + QChart *emptyChart = new QChart(); + emptyChart->setTheme(QChart::ChartThemeDark); + ui->powerChart->setChart(emptyChart); // Can't set to nullptr + delete m_powerChart; + m_powerChart = emptyChart; + m_powerAverageSeries = nullptr; // Deleted when chart deleted? + } +} + +void HeatMapGUI::on_displayChart_clicked(bool checked) +{ + m_settings.m_displayChart = checked; + displayPowerChart(); + applySettings(); +} + +void HeatMapGUI::on_clearHeatMap_clicked() +{ + // Clear heat map + m_heatMap->resetMagLevels(); + clearPower(); + plotMap(); + if (m_powerAverageSeries) + { + m_powerAverageSeries->clear(); + m_powerMaxPeakSeries->clear(); + m_powerMinPeakSeries->clear(); + m_powerPulseAverageSeries->clear(); + m_powerPathLossSeries->clear(); + } +} + +bool HeatMapGUI::pixelValid(int x, int y) const +{ + return (y >= 0) && (x >= 0) && (y < m_height) && (x < m_width); +} + +void HeatMapGUI::coordsToPixel(double latitude, double longitude, int& x, int& y) const +{ + y = m_height - (latitude - m_south) / m_degreesLatPerPixel; + x = (longitude - m_west) / m_degreesLonPerPixel; +} + +void HeatMapGUI::pixelToCoords(int x, int y, double& latitude, double& longitude) const +{ + latitude = m_north - (y /*+ 0.5*/) * m_degreesLatPerPixel; + longitude = m_west + (x /*+ 0.5*/) * m_degreesLonPerPixel; +} + +void HeatMapGUI::on_writeCSV_clicked() +{ + m_csvFileDialog.setAcceptMode(QFileDialog::AcceptSave); + m_csvFileDialog.setNameFilter("*.csv"); + if (m_csvFileDialog.exec()) + { + QStringList fileNames = m_csvFileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + QFile file(fileNames[0]); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QMessageBox::critical(this, "Heat Map", QString("Failed to open file %1").arg(fileNames[0])); + return; + } + QTextStream out(&file); + out.setRealNumberPrecision(9); // https://bugreports.qt.io/browse/QTBUG-110775 - set to 9, so we get at least 6 fractional digits + + out << "Latitude,Longitude," << ui->mode->currentText() << " Power (dB)\n"; + float *power = getCurrentModePowerData(); + for (int y = 0; y < m_height; y++) + { + for (int x = 0; x < m_width; x++) + { + float pow = power[y*m_width+x]; + if (!std::isnan(pow)) + { + double latitude, longitude; + pixelToCoords(x, y, latitude, longitude); + out << latitude << "," << longitude << "," << pow << "\n"; + } + } + } + } + } +} + +void HeatMapGUI::on_readCSV_clicked() +{ + m_csvFileDialog.setAcceptMode(QFileDialog::AcceptOpen); + m_csvFileDialog.setNameFilter("*.csv"); + if (m_csvFileDialog.exec()) + { + QStringList fileNames = m_csvFileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + QFile file(fileNames[0]); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QMessageBox::critical(this, "Heat Map", QString("Failed to open file %1").arg(fileNames[0])); + return; + } + QTextStream in(&file); + QString powerColName = ui->mode->currentText() + " Power (dB)"; + QString error; + QStringList colNames = {"Latitude", "Longitude", powerColName}; + QHash colIndexes = CSV::readHeader(in, colNames, error); + if (error.isEmpty()) + { + clearPower(getCurrentModePowerData()); + clearImage(); + int latitudeCol = colIndexes.value("Latitude"); + int longitudeCol = colIndexes.value("Longitude"); + int powerCol = colIndexes.value(powerColName); + QStringList cols; + while (CSV::readRow(in, &cols)) + { + if (cols.size() >= 3) + { + double latitude = cols[latitudeCol].toDouble(); + double longitude = cols[longitudeCol].toDouble(); + float power = cols[powerCol].toFloat(); + updatePower(latitude, longitude, power); + } + } + } + else + { + QString actualColNames = colIndexes.keys().join(" "); + QString expectedColNames = colNames.join(" "); + QMessageBox::critical(this, "Heat Map", QString("Failed to read expected header in CSV file. %1 != %2").arg(actualColNames).arg(expectedColNames)); + return; + } + } + } +} + +void HeatMapGUI::on_writeImage_clicked() +{ + m_imageFileDialog.setAcceptMode(QFileDialog::AcceptSave); + m_imageFileDialog.setNameFilter("*.png;*.jpg;*.jpeg;*.bmp;*.ppm;*.xbm;*.xpm"); + if (m_imageFileDialog.exec()) + { + QStringList fileNames = m_imageFileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + if (!m_image.save(fileNames[0])) + { + QMessageBox::critical(this, "Heat Map", QString("Failed to save image to %1").arg(fileNames[0])); + return; + } + } + } +} + +void HeatMapGUI::displayTXPosition(bool enabled) +{ + ui->txLatitudeLabel->setEnabled(enabled); + ui->txLatitude->setEnabled(enabled); + ui->txLongitude->setEnabled(enabled); + ui->txLongitudeLabel->setEnabled(enabled); + ui->txPower->setEnabled(enabled); + ui->txPowerLabel->setEnabled(enabled); + ui->txPowerUnits->setEnabled(enabled); + ui->txPositionSet->setEnabled(enabled); + ui->rangeLabel->setEnabled(enabled); + ui->range->setEnabled(enabled); + ui->rangeUnits->setEnabled(enabled); + ui->pathLossLabel->setEnabled(enabled); + ui->pathLoss->setEnabled(enabled); + ui->pathLossUnits->setEnabled(enabled); + if (enabled) { + sendTxToMap(); + } else { + deleteTxFromMap(); + } +} + +void HeatMapGUI::on_txPosition_clicked(bool checked) +{ + m_settings.m_txPosValid = checked; + displayTXPosition(checked); + applySettings(); +} + +void HeatMapGUI::on_txLatitude_editingFinished() +{ + m_settings.m_txLatitude = ui->txLatitude->text().toFloat(); + updateRange(); + sendTxToMap(); + applySettings(); +} + +void HeatMapGUI::on_txLongitude_editingFinished() +{ + m_settings.m_txLongitude = ui->txLongitude->text().toFloat(); + updateRange(); + sendTxToMap(); + applySettings(); +} + +void HeatMapGUI::on_txPower_valueChanged(double value) +{ + m_settings.m_txPower = (float)value; + sendTxToMap(); + applySettings(); +} + +void HeatMapGUI::on_txPositionSet_clicked(bool checked) +{ + ui->txLatitude->setText(QString::number(m_latitude)); + ui->txLongitude->setText(QString::number(m_longitude)); + m_settings.m_txLatitude = m_latitude; + m_settings.m_txLongitude = m_longitude; + updateRange(); + sendTxToMap(); + applySettings(); +} + +void HeatMapGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void HeatMapGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.setDefaultTitle(m_displayedName); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + dialog.setNumberOfStreams(m_heatMap->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + } + + dialog.move(p); + new DialogPositioner(&dialog, true); + dialog.exec(); + + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitle(m_channelMarker.getTitle()); + setTitleColor(m_settings.m_rgbColor); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + updateIndexLabel(); + } + + applySettings(); + } + + resetContextMenuType(); +} + +HeatMapGUI::HeatMapGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::HeatMapGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_basebandSampleRate(1), + m_doApplySettings(true), + m_tickCount(0), + m_brush(Qt::SolidPattern), + m_powerChart(nullptr), + m_powerAverageSeries(nullptr), + m_powerMaxPeakSeries(nullptr), + m_powerMinPeakSeries(nullptr), + m_powerPulseAverageSeries(nullptr), + m_powerPathLossSeries(nullptr) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/heatmap/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setSizePolicy(rollupContents->sizePolicy()); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_heatMap = reinterpret_cast(rxChannel); + m_heatMap->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + m_scopeVis = m_heatMap->getScopeSink(); + m_scopeVis->setGLScope(ui->glScope); + ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); + ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + + // Scope settings to display magdB + ui->scopeGUI->setPreTrigger(1); + GLScopeSettings::TraceData traceDataI, traceDataQ; + traceDataI.m_projectionType = Projector::ProjectionMagDB; + traceDataI.m_amp = 1.0; // for 0 -100 dB + traceDataI.m_ofs = 0.0; + ui->scopeGUI->changeTrace(0, traceDataI); + ui->scopeGUI->setDisplayMode(GLScopeSettings::DisplayX); + ui->scopeGUI->focusOnTrace(0); // re-focus to take changes into account in the GUI + + GLScopeSettings::TriggerData triggerData; + triggerData.m_triggerLevel = 0.1; + triggerData.m_triggerLevelCoarse = 10; + triggerData.m_triggerPositiveEdge = true; + ui->scopeGUI->changeTrigger(0, triggerData); + ui->scopeGUI->focusOnTrigger(0); // re-focus to take changes into account in the GUI + + m_scopeVis->setLiveRate(m_settings.m_sampleRate); + + 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); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("Heat Map"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + setTitleColor(m_channelMarker.getColor()); + m_settings.setChannelMarker(&m_channelMarker); + m_settings.setScopeGUI(ui->scopeGUI); + m_settings.setRollupState(&m_rollupState); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + m_latitude = MainCore::instance()->getSettings().getLatitude(); + m_longitude = MainCore::instance()->getSettings().getLongitude(); + m_altitude = MainCore::instance()->getSettings().getAltitude(); + ui->latitude->setText(QString::number(m_latitude)); + ui->longitude->setText(QString::number(m_longitude)); + + QStringList colorMapNames = ColorMap::getColorMapNames(); + for (const auto color : colorMapNames) { + ui->colorMap->addItem(color); + } + m_colorMap = ColorMap::getColorMap(m_settings.m_colorMapName); + + m_pen.setColor(Qt::black); + m_painter.setPen(m_pen); + //m_painter.setBrush(m_brush); + + createMap(); + + // Get updated when position changes + connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &HeatMapGUI::preferenceChanged); + + ui->scopeContainer->setVisible(false); + + displaySettings(); + makeUIConnections(); + applySettings(true); + DialPopup::addPopupsToChildDials(this); + + plotPowerVsTimeChart(); +} + +HeatMapGUI::~HeatMapGUI() +{ + disconnect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + deleteFromMap(); + deleteTxFromMap(); + deleteMap(); + delete ui; +} + +void HeatMapGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void HeatMapGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + HeatMap::MsgConfigureHeatMap* message = HeatMap::MsgConfigureHeatMap::create(m_settings, force); + m_heatMap->getInputMessageQueue()->push(message); + } +} + +void HeatMapGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + setTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); + + ui->minPower->setValue(m_settings.m_minPower); + ui->maxPower->setValue(m_settings.m_maxPower); + ui->colorMap->setCurrentText(m_settings.m_colorMapName); + m_colorMap = ColorMap::getColorMap(m_settings.m_colorMapName); + + ui->mode->setCurrentIndex((int)m_settings.m_mode); + ui->pulseTH->setValue(m_settings.m_pulseThreshold); + ui->pulseTHText->setText(QString::number((int)m_settings.m_pulseThreshold)); + + int value = (int)std::log10(m_settings.m_averagePeriodUS); + ui->averagePeriod->setValue(value); + ui->averagePeriodText->setText(m_averagePeriodTexts[value-1]); + + value = (int)std::log10(m_settings.m_sampleRate); + ui->sampleRate->setValue(value); + ui->sampleRateText->setText(m_sampleRateTexts[value-1]); + ui->averagePeriod->setMinimum(std::max(1, m_averagePeriodTexts.size() - value)); + + ui->txPosition->setChecked(m_settings.m_txPosValid); + displayTXPosition(m_settings.m_txPosValid); + ui->txLatitude->setText(QString::number(m_settings.m_txLatitude)); + ui->txLongitude->setText(QString::number(m_settings.m_txLongitude)); + ui->txPower->setValue(m_settings.m_txPower); + + ui->displayChart->setChecked(m_settings.m_displayChart); + displayPowerChart(); + ui->displayAverage->setChecked(m_settings.m_displayAverage); + ui->displayMax->setChecked(m_settings.m_displayMax); + ui->displayMin->setChecked(m_settings.m_displayMin); + ui->displayPulseAverage->setChecked(m_settings.m_displayPulseAverage); + ui->displayPathLoss->setChecked(m_settings.m_displayPathLoss); + ui->displayMins->setValue(m_settings.m_displayMins); + + m_scopeVis->setLiveRate(m_settings.m_sampleRate); + + updateIndexLabel(); + updateRange(); + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); +} + +void HeatMapGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void HeatMapGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void HeatMapGUI::tick() +{ + // Update power meter widget + double magsqAvg, magsqpeak; + int nbMagsqSamples; + m_heatMap->getMagSqLevels(magsqAvg, magsqpeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbMaxPeak = CalcDb::dbPower(magsqpeak); + + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbMaxPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + } + + // Get power measurements and plot on map + if ((m_y >= 0) && (m_x >= 0) && (m_y < m_height) && (m_x < m_width)) + { + double magAvg, magPulseAvg, magMaxPeak, magMinPeak; + m_heatMap->getMagLevels(magAvg, magPulseAvg, magMaxPeak, magMinPeak); + + double powDbPulseAvg, powDbMinPeak, powDbPathLoss; + powDbAvg = std::numeric_limits::quiet_NaN(); + powDbPulseAvg = std::numeric_limits::quiet_NaN(); + powDbMaxPeak = std::numeric_limits::quiet_NaN(); + powDbMinPeak = std::numeric_limits::quiet_NaN(); + powDbPathLoss = std::numeric_limits::quiet_NaN(); + + int idx = m_y * m_width + m_x; + if (!std::isnan(magAvg)) + { + powDbAvg = CalcDb::dbPower(magAvg * magAvg); + m_powerAverage[idx] = powDbAvg; + if (m_tickCount % 4 == 0) { + ui->average->setText(QString::number(powDbAvg, 'f', 1)); + } + } + else + { + ui->average->setText(""); + } + if (!std::isnan(magPulseAvg)) + { + powDbPulseAvg = CalcDb::dbPower(magPulseAvg * magPulseAvg); + m_powerPulseAverage[idx] = powDbPulseAvg; + if (m_tickCount % 4 == 0) { + ui->pulseAverage->setText(QString::number(powDbPulseAvg, 'f', 1)); + } + } + else + { + ui->pulseAverage->setText(""); + } + if (magMaxPeak != -std::numeric_limits::max()) + { + powDbMaxPeak = CalcDb::dbPower(magMaxPeak * magMaxPeak); + m_powerMaxPeak[idx] = powDbMaxPeak; + if (m_tickCount % 4 == 0) { + ui->maxPeak->setText(QString::number(powDbMaxPeak, 'f', 1)); + } + } + else + { + ui->maxPeak->setText(""); + } + if (magMinPeak != std::numeric_limits::max()) + { + powDbMinPeak = CalcDb::dbPower(magMinPeak * magMinPeak); + m_powerMinPeak[idx] = powDbMinPeak; + if (m_tickCount % 4 == 0) { + ui->minPeak->setText(QString::number(powDbMinPeak, 'f', 1)); + } + } + else + { + ui->minPeak->setText(""); + } + + double range = calcRange(m_latitude, m_longitude); + double frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset; + powDbPathLoss = m_settings.m_txPower - calcFreeSpacePathLoss(range, frequency); + m_powerPathLoss[idx] = powDbPathLoss; + + if (m_heatMap->getDeviceAPI()->state(0) == DeviceAPI::StRunning) + { + addToPowerSeries(QDateTime::currentDateTime(), powDbAvg, powDbPulseAvg, powDbMaxPeak, powDbMinPeak, powDbPathLoss); + if (m_settings.m_mode != HeatMapSettings::None) + { + // Plot newest measurement on map + float *power = getCurrentModePowerData(); + double powDb = power[idx]; + if (!std::isnan(powDb)) { + plotPixel(m_x, m_y, powDb); + } + } + + if (m_tickCount % 15 == 0) + { + trimPowerSeries(QDateTime::currentDateTime().addSecs(-10*60)); + // Updating axis range causes chart to be redrawn, so don't call for every sample + updateAxis(); + } + + } + } + + if (m_tickCount % 25 == 0) { + sendToMap(); + } + + m_tickCount++; +} + +float *HeatMapGUI::getCurrentModePowerData() +{ + switch (m_settings.m_mode) + { + case HeatMapSettings::None: + case HeatMapSettings::Average: + return m_powerAverage; + case HeatMapSettings::PulseAverage: + return m_powerPulseAverage; + case HeatMapSettings::Max: + return m_powerMaxPeak; + case HeatMapSettings::Min: + return m_powerMinPeak; + case HeatMapSettings::PathLoss: + return m_powerPathLoss; + default: + return nullptr; + } +} + +void HeatMapGUI::updatePower(double latitude, double longitude, float power) +{ + int x, y; + coordsToPixel(latitude, longitude, x, y); + if (!pixelValid(x, y)) + { + resizeMap(x, y); + coordsToPixel(latitude, longitude, x, y); + } + float *powerArray = getCurrentModePowerData(); + powerArray[y*m_width+x] = power; + plotPixel(x, y, power); +} + +void HeatMapGUI::plotMap() +{ + clearImage(); + plotMap(getCurrentModePowerData()); + sendToMap(); +} + +void HeatMapGUI::clearPower() +{ + clearPower(m_powerAverage); + clearPower(m_powerPulseAverage); + clearPower(m_powerMaxPeak); + clearPower(m_powerMinPeak); + clearPower(m_powerPathLoss); +} + +void HeatMapGUI::clearPower(float *power) +{ + clearPower(power, m_width * m_height); +} + +void HeatMapGUI::clearPower(float *power, int size) +{ + std::fill_n(power, size, std::numeric_limits::quiet_NaN()); +} + +void HeatMapGUI::clearImage() +{ + m_image.fill(Qt::transparent); + //m_pen.setColor(Qt::black); + //m_painter.drawEllipse(QPoint(m_width/2, m_height/2), m_width/2-1, m_height/2-1); +} + +void HeatMapGUI::plotMap(float *power) +{ + for (int y = 0; y < m_height; y++) + { + for (int x = 0; x < m_width; x++) + { + float pow = power[y * m_width + x]; + if (!std::isnan(pow)) { + plotPixel(x, y, pow); + } + } + } +} + +void HeatMapGUI::plotPixel(int x, int y, double power) +{ + // Normalise to [0,1] + float powNorm = (power - m_settings.m_minPower) / (m_settings.m_maxPower - m_settings.m_minPower); + if (powNorm < 0) { + return; // Don't plot below min, so we can easily see where we're below the min limit + } + powNorm = std::max(0.0f, powNorm); + powNorm = std::min(1.0f, powNorm); + + int index = std::round(powNorm * 255); + + QColor color = QColor::fromRgbF(m_colorMap[index*3], m_colorMap[index*3+1], m_colorMap[index*3+2]); + m_pen.setColor(color); + m_painter.setPen(m_pen); + //m_painter.setBrush(m_brush); + + m_painter.drawPoint(QPoint(x, y)); +} + +void HeatMapGUI::updateRange() +{ + if (m_settings.m_txPosValid) + { + // Calculate range + qreal range = calcRange(m_latitude, m_longitude); + if (range < 1000) + { + ui->range->setText(QString::number(std::round(range))); + ui->rangeUnits->setText("m"); + } + else + { + ui->range->setText(QString::number(range / 1000.0, 'f', 1)); + ui->rangeUnits->setText("km"); + } + double frequency = m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset; + double loss = calcFreeSpacePathLoss(range, frequency); + ui->pathLoss->setText(QString::number(loss, 'f', 1)); + } + else + { + ui->range->setText(""); + ui->pathLoss->setText(""); + } +} + +qreal HeatMapGUI::calcRange(double latitude, double longitude) +{ + QGeoCoordinate pos(latitude, longitude); + QGeoCoordinate tx(m_settings.m_txLatitude, m_settings.m_txLongitude); + return tx.distanceTo(pos); +} + +double HeatMapGUI::calcFreeSpacePathLoss(double range, double frequency) +{ + // In dB + if ((range == 0.0) || (frequency == 0.0)) { + return 0.0; + } else { + return 20.0 * log10(range) + 20 * log10(frequency) + 20 * log10(4.0 * M_PI / Astronomy::m_speedOfLight); + } +} + +void HeatMapGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &HeatMapGUI::on_deltaFrequency_changed); + QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &HeatMapGUI::on_rfBW_valueChanged); + QObject::connect(ui->minPower, QOverload::of(&QDoubleSpinBox::valueChanged), this, &HeatMapGUI::on_minPower_valueChanged); + QObject::connect(ui->maxPower, QOverload::of(&QDoubleSpinBox::valueChanged), this, &HeatMapGUI::on_maxPower_valueChanged); + QObject::connect(ui->colorMap, QOverload::of(&QComboBox::currentIndexChanged), this, &HeatMapGUI::on_colorMap_currentIndexChanged); + QObject::connect(ui->pulseTH, QOverload::of(&QDial::valueChanged), this, &HeatMapGUI::on_pulseTH_valueChanged); + QObject::connect(ui->averagePeriod, QOverload::of(&QDial::valueChanged), this, &HeatMapGUI::on_averagePeriod_valueChanged); + QObject::connect(ui->sampleRate, QOverload::of(&QDial::valueChanged), this, &HeatMapGUI::on_sampleRate_valueChanged); + QObject::connect(ui->mode, QOverload::of(&QComboBox::currentIndexChanged), this, &HeatMapGUI::on_mode_currentIndexChanged); + QObject::connect(ui->displayChart, &QPushButton::clicked, this, &HeatMapGUI::on_displayChart_clicked); + QObject::connect(ui->clearHeatMap, &QPushButton::clicked, this, &HeatMapGUI::on_clearHeatMap_clicked); + QObject::connect(ui->writeCSV, &QPushButton::clicked, this, &HeatMapGUI::on_writeCSV_clicked); + QObject::connect(ui->readCSV, &QPushButton::clicked, this, &HeatMapGUI::on_readCSV_clicked); + QObject::connect(ui->writeImage, &QPushButton::clicked, this, &HeatMapGUI::on_writeImage_clicked); + QObject::connect(ui->txPosition, &QPushButton::clicked, this, &HeatMapGUI::on_txPosition_clicked); + QObject::connect(ui->txLatitude, &QLineEdit::editingFinished, this, &HeatMapGUI::on_txLatitude_editingFinished); + QObject::connect(ui->txLongitude, &QLineEdit::editingFinished, this, &HeatMapGUI::on_txLongitude_editingFinished); + QObject::connect(ui->txPower, QOverload::of(&QDoubleSpinBox::valueChanged), this, &HeatMapGUI::on_txPower_valueChanged); + QObject::connect(ui->txPositionSet, &QPushButton::clicked, this, &HeatMapGUI::on_txPositionSet_clicked); + QObject::connect(ui->displayAverage, &QCheckBox::clicked, this, &HeatMapGUI::on_displayAverage_clicked); + QObject::connect(ui->displayMax, &QCheckBox::clicked, this, &HeatMapGUI::on_displayMax_clicked); + QObject::connect(ui->displayMin, &QCheckBox::clicked, this, &HeatMapGUI::on_displayMin_clicked); + QObject::connect(ui->displayPulseAverage, &QCheckBox::clicked, this, &HeatMapGUI::on_displayPulseAverage_clicked); + QObject::connect(ui->displayPathLoss, &QCheckBox::clicked, this, &HeatMapGUI::on_displayPathLoss_clicked); + QObject::connect(ui->displayMins, QOverload::of(&QSpinBox::valueChanged), this, &HeatMapGUI::on_displayMins_valueChanged); +} + +void HeatMapGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} + +void HeatMapGUI::sendToMap() +{ + if (m_settings.m_mode != HeatMapSettings::None) + { + // Send to Map feature + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes); + + if (mapPipes.size() > 0) + { + // Encode image as base64 PNG + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + m_image.save(&buffer, "PNG"); + QByteArray data = ba.toBase64(); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString("Heat Map")); + swgMapItem->setImage(new QString(data)); + swgMapItem->setAltitude(0); + swgMapItem->setType(1); + swgMapItem->setImageTileEast(m_east); + swgMapItem->setImageTileWest(m_west); + swgMapItem->setImageTileNorth(m_north); + swgMapItem->setImageTileSouth(m_south); + swgMapItem->setImageZoomLevel((float)m_zoomLevel); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem); + messageQueue->push(msg); + } + } + } +} + +void HeatMapGUI::deleteFromMap() +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString("Heat Map")); + swgMapItem->setImage(new QString()); // Set image to "" to delete it + swgMapItem->setType(1); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem); + messageQueue->push(msg); + } +} + +void HeatMapGUI::sendTxToMap() +{ + // Send to Map feature + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes); + + if (mapPipes.size() > 0) + { + QString text = QString("Heat Map Transmitter\nPower: %1 dB").arg(m_settings.m_txPower); + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString("TX")); + swgMapItem->setLatitude(m_settings.m_txLatitude); + swgMapItem->setLongitude(m_settings.m_txLongitude); + swgMapItem->setAltitude(0); + swgMapItem->setImage(new QString("antenna.png")); + swgMapItem->setText(new QString(text)); + swgMapItem->setModel(new QString("antenna.glb")); + swgMapItem->setFixedPosition(true); + swgMapItem->setLabel(new QString("TX")); + swgMapItem->setLabelAltitudeOffset(4.5); + swgMapItem->setAltitudeReference(1); + swgMapItem->setType(0); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem); + messageQueue->push(msg); + } + } +} + +void HeatMapGUI::deleteTxFromMap() +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_heatMap, "mapitems", mapPipes); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString("TX")); + swgMapItem->setImage(new QString()); // Set image to "" to delete it + swgMapItem->setType(0); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_heatMap, swgMapItem); + messageQueue->push(msg); + } +} + +void HeatMapGUI::preferenceChanged(int elementType) +{ + Preferences::ElementType pref = (Preferences::ElementType)elementType; + if ((pref == Preferences::Latitude) || (pref == Preferences::Longitude) || (pref == Preferences::Altitude)) + { + // Get new position + m_latitude = MainCore::instance()->getSettings().getLatitude(); + m_longitude = MainCore::instance()->getSettings().getLongitude(); + m_altitude = MainCore::instance()->getSettings().getAltitude(); + + // Display new position in GUI + ui->latitude->setText(QString::number(m_latitude)); + ui->longitude->setText(QString::number(m_longitude)); + updateRange(); + + // Map position to pixel + int x, y; + coordsToPixel(m_latitude, m_longitude, x, y); + if ((x != m_x) || (y != m_y)) + { + m_x = x; + m_y = y; + m_heatMap->resetMagLevels(); + if (!pixelValid(x, y)) { + resizeMap(x, y); + } + } + } +} + +void HeatMapGUI::createMap() +{ + // https://wiki.openstreetmap.org/wiki/Zoom_levels + double earthCircumference = 40075016.686; + double scale = cos(Units::degreesToRadians(m_latitude)); + m_resolution = earthCircumference * scale / pow(2.0, m_zoomLevel+8.0); // metres per pixel + m_width = m_blockSize; + m_height = m_blockSize; + m_degreesLonPerPixel = m_resolution / scale / (earthCircumference / 360.0); + m_degreesLatPerPixel = m_resolution / (earthCircumference / 360.0); + int size = m_width * m_height; + m_powerAverage = new float[size]; + m_powerPulseAverage = new float[size]; + m_powerMaxPeak = new float[size]; + m_powerMinPeak = new float[size]; + m_powerPathLoss = new float[size]; + + m_north = m_latitude + m_degreesLatPerPixel * m_height / 2; + m_south = m_latitude - m_degreesLatPerPixel * m_height / 2; + m_east = m_longitude + m_degreesLonPerPixel * m_width / 2; + m_west = m_longitude - m_degreesLonPerPixel * m_width / 2; + m_x = m_width / 2; + m_y = m_height / 2; + + m_image = QImage(m_width, m_height, QImage::Format_ARGB32); + m_painter.begin(&m_image); + + on_clearHeatMap_clicked(); +} + +void HeatMapGUI::deleteMap() +{ + deleteFromMap(); + delete[] m_powerAverage; + m_powerAverage = nullptr; + delete[] m_powerPulseAverage; + m_powerPulseAverage = nullptr; + delete[] m_powerMaxPeak; + m_powerMaxPeak = nullptr; + delete[] m_powerMinPeak; + m_powerMinPeak = nullptr; + delete[] m_powerPathLoss; + m_powerPathLoss = nullptr; + m_painter.end(); +} + +void HeatMapGUI::resizeMap(int x, int y) +{ + if ((x <= -m_blockSize) || (x >= m_width+m_blockSize) || (y <= -m_blockSize) || (y >= m_height+m_blockSize)) + { + // Position has moved a long way - restart from scratch to avoid map being too big + qDebug() << "HeatMapGUI::resizeMap: Position has moved significantly. Recreating map"; + deleteMap(); + createMap(); + } + else + { + // Expand map + int newWidth = m_width; + int newHeight = m_height; + int xOffset = 0; + int yOffset = 0; + if (x < 0) + { + newWidth += m_blockSize; + xOffset = m_blockSize; + m_west -= m_blockSize * m_degreesLonPerPixel; + } + if (x >= m_width) + { + newWidth += m_blockSize; + m_east += m_blockSize * m_degreesLonPerPixel; + } + if (y < 0) + { + newHeight += m_blockSize; + yOffset = m_blockSize * newWidth; + m_north += m_blockSize * m_degreesLatPerPixel; + } + if (y >= m_height) + { + newHeight += m_blockSize; + m_south -= m_blockSize * m_degreesLatPerPixel; + } + + int newSize = newWidth * newHeight; + float *powerAverage = new float[newSize]; + float *powerPulseAverage = new float[newSize]; + float *powerMaxPeak = new float[newSize]; + float *powerMinPeak = new float[newSize]; + float *powerPathLoss = new float[newSize]; + clearPower(powerAverage, newSize); + clearPower(powerPulseAverage, newSize); + clearPower(powerMaxPeak, newSize); + clearPower(powerMinPeak, newSize); + clearPower(powerPathLoss, newSize); + + // Copy across old data + for (int j = 0; j < m_height; j++) + { + int srcStart = j * m_width; + int srcEnd = (j + 1) * m_width; + int destStart = j * newWidth + yOffset + xOffset; + //qDebug() << srcStart << srcEnd << destStart; + std::copy(m_powerAverage + srcStart, m_powerAverage + srcEnd, powerAverage + destStart); + std::copy(m_powerPulseAverage + srcStart, m_powerPulseAverage + srcEnd, powerPulseAverage + destStart); + std::copy(m_powerMaxPeak + srcStart, m_powerMaxPeak + srcEnd, powerMaxPeak + destStart); + std::copy(m_powerMinPeak + srcStart, m_powerMinPeak + srcEnd, powerMinPeak + destStart); + std::copy(m_powerPathLoss + srcStart, m_powerPathLoss + srcEnd, powerPathLoss + destStart); + } + + delete[] m_powerAverage; + delete[] m_powerPulseAverage; + delete[] m_powerMaxPeak; + delete[] m_powerMinPeak; + m_powerAverage = powerAverage; + m_powerPulseAverage = powerPulseAverage; + m_powerMaxPeak = powerMaxPeak; + m_powerMinPeak = powerMinPeak; + m_powerPathLoss = powerPathLoss; + m_width = newWidth; + m_height = newHeight; + m_painter.end(); + m_image = QImage(m_width, m_height, QImage::Format_ARGB32); + m_painter.begin(&m_image); + plotMap(); + } +} + +void HeatMapGUI::plotPowerVsTimeChart() +{ + QChart *oldChart = m_powerChart; + + m_powerChart = new QChart(); + + m_powerChart->layout()->setContentsMargins(0, 0, 0, 0); + m_powerChart->setMargins(QMargins(1, 1, 1, 1)); + m_powerChart->setTheme(QChart::ChartThemeDark); + + m_powerChart->legend()->setAlignment(Qt::AlignBottom); + m_powerChart->legend()->setVisible(true); + + // Create measurement data series + m_powerAverageSeries = new QLineSeries(); + m_powerAverageSeries->setVisible(m_settings.m_displayAverage); + m_powerAverageSeries->setName("Average"); + m_powerMaxPeakSeries = new QLineSeries(); + m_powerMaxPeakSeries->setVisible(m_settings.m_displayMax); + m_powerMaxPeakSeries->setName("Max"); + m_powerMinPeakSeries = new QLineSeries(); + m_powerMinPeakSeries->setVisible(m_settings.m_displayMin); + m_powerMinPeakSeries->setName("Min"); + m_powerPulseAverageSeries = new QLineSeries(); + m_powerPulseAverageSeries->setVisible(m_settings.m_displayPulseAverage); + m_powerPulseAverageSeries->setName("Pulse Average"); + m_powerPathLossSeries = new QLineSeries(); + m_powerPathLossSeries->setVisible(m_settings.m_displayPathLoss); + m_powerPathLossSeries->setName("Path Loss"); + + // Create X axis + m_powerXAxis = new QDateTimeAxis(); + QString dateTimeFormat = "hh:mm:ss"; + m_powerXAxis->setFormat(dateTimeFormat); + m_powerXAxis->setTitleText("Time"); + + // Create Y axis + m_powerYAxis = new QValueAxis(); + m_powerYAxis->setRange(m_settings.m_minPower, m_settings.m_maxPower); + m_powerYAxis->setTitleText("Power (dB)"); + + m_powerChart->addAxis(m_powerXAxis, Qt::AlignBottom); + m_powerChart->addAxis(m_powerYAxis, Qt::AlignLeft); + + m_powerChart->addSeries(m_powerAverageSeries); + m_powerAverageSeries->attachAxis(m_powerXAxis); + m_powerAverageSeries->attachAxis(m_powerYAxis); + m_powerChart->addSeries(m_powerMaxPeakSeries); + m_powerMaxPeakSeries->attachAxis(m_powerXAxis); + m_powerMaxPeakSeries->attachAxis(m_powerYAxis); + m_powerChart->addSeries(m_powerMinPeakSeries); + m_powerMinPeakSeries->attachAxis(m_powerXAxis); + m_powerMinPeakSeries->attachAxis(m_powerYAxis); + m_powerChart->addSeries(m_powerPulseAverageSeries); + m_powerPulseAverageSeries->attachAxis(m_powerXAxis); + m_powerPulseAverageSeries->attachAxis(m_powerYAxis); + m_powerChart->addSeries(m_powerPathLossSeries); + m_powerPathLossSeries->attachAxis(m_powerXAxis); + m_powerPathLossSeries->attachAxis(m_powerYAxis); + + ui->powerChart->setChart(m_powerChart); + + delete oldChart; +} + +void HeatMapGUI::addToPowerSeries(QDateTime dateTime, double average, double pulseAverage, double max, double min, double pathLoss) +{ + if (m_powerAverageSeries) + { + qint64 msecs = dateTime.toMSecsSinceEpoch(); + if (!std::isnan(average)) { + m_powerAverageSeries->append(msecs, average); + } + if (!std::isnan(pulseAverage)) { + m_powerPulseAverageSeries->append(msecs, pulseAverage); + } + if (!std::isnan(max)) { + m_powerMaxPeakSeries->append(msecs, max); + } + if (!std::isnan(min)) { + m_powerMinPeakSeries->append(msecs, min); + } + if (!std::isnan(pathLoss)) { + m_powerPathLossSeries->append(msecs, pathLoss); + } + } +} + +void HeatMapGUI::updateAxis() +{ + if (!m_powerPathLossSeries || (m_powerPathLossSeries->count() <= 1)) { + return; + } + QDateTime current = QDateTime::currentDateTime(); + QDateTime first = QDateTime::fromMSecsSinceEpoch(m_powerPathLossSeries->at(0).x()); + QDateTime min = current.addSecs(-60 * m_settings.m_displayMins); + if (first > min) { + min = first; + } + m_powerXAxis->setRange(min, current); +} + +void HeatMapGUI::trimPowerSeries(QLineSeries *series, QDateTime dateTime) +{ + qint64 msecs = dateTime.toMSecsSinceEpoch(); + for (int i = 0; i < series->count(); i++) + { + const QPointF& p = series->at(i); + if (p.x() >= msecs) + { + if (i > 0) { + series->removePoints(0, i); + } + break; + } + } +} + +// Remove all points in series before the specified date and time +void HeatMapGUI::trimPowerSeries(QDateTime dateTime) +{ + if (m_powerAverageSeries) + { + trimPowerSeries(m_powerAverageSeries, dateTime); + trimPowerSeries(m_powerMaxPeakSeries, dateTime); + trimPowerSeries(m_powerMinPeakSeries, dateTime); + trimPowerSeries(m_powerPulseAverageSeries, dateTime); + trimPowerSeries(m_powerPathLossSeries, dateTime); + } +} + +void HeatMapGUI::on_displayAverage_clicked(bool checked) +{ + m_settings.m_displayAverage = checked; + m_powerAverageSeries->setVisible(checked); + applySettings(); +} + +void HeatMapGUI::on_displayMax_clicked(bool checked) +{ + m_settings.m_displayMax = checked; + m_powerMaxPeakSeries->setVisible(checked); + applySettings(); +} + +void HeatMapGUI::on_displayMin_clicked(bool checked) +{ + m_settings.m_displayMin = checked; + m_powerMinPeakSeries->setVisible(checked); + applySettings(); +} + +void HeatMapGUI::on_displayPulseAverage_clicked(bool checked) +{ + m_settings.m_displayPulseAverage = checked; + m_powerPulseAverageSeries->setVisible(checked); + applySettings(); +} + +void HeatMapGUI::on_displayPathLoss_clicked(bool checked) +{ + m_settings.m_displayPathLoss = checked; + m_powerPathLossSeries->setVisible(checked); + applySettings(); +} + +void HeatMapGUI::on_displayMins_valueChanged(int value) +{ + m_settings.m_displayMins = value; + updateAxis(); + applySettings(); +} + diff --git a/plugins/channelrx/heatmap/heatmapgui.h b/plugins/channelrx/heatmap/heatmapgui.h new file mode 100644 index 000000000..ca2ee4f6e --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapgui.h @@ -0,0 +1,222 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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_HEATMAPGUI_H +#define INCLUDE_HEATMAPGUI_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/messagequeue.h" +#include "util/colormap.h" +#include "settings/rollupstate.h" + +#include "heatmapsettings.h" +#include "heatmap.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class HeatMap; +class HeatMapGUI; + +namespace Ui { + class HeatMapGUI; +} + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +using namespace QtCharts; +#endif + +class HeatMapGUI : public ChannelGUI { + Q_OBJECT + +public: + static HeatMapGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual void setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; }; + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; }; + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; }; + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; }; + virtual QString getTitle() const { return m_settings.m_title; }; + virtual QColor getTitleColor() const { return m_settings.m_rgbColor; }; + virtual void zetHidden(bool hidden) { m_settings.m_hidden = hidden; } + virtual bool getHidden() const { return m_settings.m_hidden; } + virtual ChannelMarker& getChannelMarker() { return m_channelMarker; } + virtual int getStreamIndex() const { return m_settings.m_streamIndex; } + virtual void setStreamIndex(int streamIndex) { m_settings.m_streamIndex = streamIndex; } + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + void preferenceChanged(int elementType); + +private: + Ui::HeatMapGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + HeatMapSettings m_settings; + qint64 m_deviceCenterFrequency; + int m_basebandSampleRate; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + + HeatMap* m_heatMap; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QMenu *messagesMenu; // Column select context menu + QMenu *copyMenu; + + int m_width; // In pixels + int m_height; + double m_resolution; + double m_degreesLonPerPixel; + double m_degreesLatPerPixel; + float *m_powerAverage; + float *m_powerPulseAverage; + float *m_powerMaxPeak; + float *m_powerMinPeak; + float *m_powerPathLoss; + QImage m_image; + double m_east, m_west, m_north, m_south; + double m_latitude; + double m_longitude; + double m_altitude; + QPainter m_painter; + QPen m_pen; + QBrush m_brush; + const float *m_colorMap; + int m_x; // Current position + int m_y; + + QChart *m_powerChart; + QLineSeries *m_powerAverageSeries; + QLineSeries *m_powerMaxPeakSeries; + QLineSeries *m_powerMinPeakSeries; + QLineSeries *m_powerPulseAverageSeries; + QLineSeries *m_powerPathLossSeries; + QDateTimeAxis *m_powerXAxis; + QValueAxis *m_powerYAxis; + + const static QStringList m_averagePeriodTexts; + const static QStringList m_sampleRateTexts; + + const static int m_blockSize = 512; // osm uses powers of 2 + const static int m_zoomLevel = 15; // 3m/pixel at lat=51 GPS accuracy ~5m at 1Hz + + QFileDialog m_csvFileDialog; + QFileDialog m_imageFileDialog; + + explicit HeatMapGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~HeatMapGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void messageReceived(const QByteArray& message, const QDateTime& dateTime); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + + float *getCurrentModePowerData(); + void coordsToPixel(double latitude, double longitude, int& x, int& y) const; + void pixelToCoords(int x, int y, double& latitude, double& longitude) const; + bool pixelValid(int x, int y) const; + + void createMap(); + void deleteMap(); + void resizeMap(int x, int y); + void clearPower(); + void clearPower(float *power); + void clearPower(float *power, int size); + void clearImage(); + void updatePower(double latitude, double longitude, float power); + void plotMap(); + void plotMap(float *power); + void plotPixel(int x, int y, double power); + qreal calcRange(double latitude, double longitude); + void updateRange(); + void displayTXPosition(bool enabled); + double calcFreeSpacePathLoss(double range, double frequency); + + void displayPowerChart(); + void plotPowerVsTimeChart(); + void addToPowerSeries(QDateTime dateTime, double average, double pulseAverage, double max, double min, double pathLoss); + void trimPowerSeries(QDateTime dateTime); + void trimPowerSeries(QLineSeries *series, QDateTime dateTime); + void updateAxis(); + + void deleteFromMap(); + void sendToMap(); + void sendTxToMap(); + void deleteTxFromMap(); + + void leaveEvent(QEvent*); + void enterEvent(EnterEventType*); + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_displayChart_clicked(bool checked=false); + void on_clearHeatMap_clicked(); + void on_writeCSV_clicked(); + void on_readCSV_clicked(); + void on_writeImage_clicked(); + void on_minPower_valueChanged(double value); + void on_maxPower_valueChanged(double value); + void on_colorMap_currentIndexChanged(int index); + void on_pulseTH_valueChanged(int value); + void on_averagePeriod_valueChanged(int value); + void on_sampleRate_valueChanged(int value); + void on_mode_currentIndexChanged(int index); + void on_txPosition_clicked(bool checked=false); + void on_txLatitude_editingFinished(); + void on_txLongitude_editingFinished(); + void on_txPower_valueChanged(double value); + void on_txPositionSet_clicked(bool checked=false); + void on_displayAverage_clicked(bool checked=false); + void on_displayMax_clicked(bool checked=false); + void on_displayMin_clicked(bool checked=false); + void on_displayPulseAverage_clicked(bool checked=false); + void on_displayPathLoss_clicked(bool checked=false); + void on_displayMins_valueChanged(int value); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); +}; + +#endif // INCLUDE_HEATMAPGUI_H + diff --git a/plugins/channelrx/heatmap/heatmapgui.ui b/plugins/channelrx/heatmap/heatmapgui.ui new file mode 100644 index 000000000..d402a3801 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapgui.ui @@ -0,0 +1,1620 @@ + + + HeatMapGUI + + + + 0 + 0 + 395 + 1018 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + Heat Map + + + + + 0 + 0 + 390 + 191 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 1 + + + 100000 + + + 1 + + + 100 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 10.0k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Res + + + + + + + + 76 + 0 + + + + Pixel resolution + + + + 3m/pixel + + + + + + + + + + Qt::Horizontal + + + + + + + + + SR + + + + + + + + 24 + 24 + + + + Sample rate + + + 1 + + + 6 + + + 1 + + + + + + + + 30 + 0 + + + + 100k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + S/s + + + + + + + + + + + + + + Qt::Vertical + + + + + + + T<sub>AVG</sub> + + + + + + + + 0 + 0 + + + + + 24 + 24 + + + + Time period to average measurement over + + + 1 + + + 7 + + + 1 + + + + + + + + 44 + 0 + + + + 100ms + + + + + + + Qt::Vertical + + + + + + + true + + + TH<sub>P</sub> + + + + + + + true + + + + 24 + 24 + + + + Pulse threshold power in dB + + + -120 + + + 0 + + + + + + + true + + + + 28 + 0 + + + + -120 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + dB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Check to set transmitter position, which enables range and path loss calculation + + + TX + + + + + + + Qt::Vertical + + + + + + + false + + + Latitude + + + + + + + false + + + + 100 + 16777215 + + + + Latitude of transmitter. Decimal degrees, north positive. + + + + + + + Qt::Vertical + + + + + + + false + + + Longitude + + + + + + + false + + + + 100 + 16777215 + + + + Longitude of transmitter. Decimal degrees, East positive. + + + + + + + Qt::Vertical + + + + + + + false + + + Power + + + + + + + false + + + Transmitter power in dB + + + 1 + + + -150.000000000000000 + + + + + + + false + + + dB + + + + + + + Qt::Vertical + + + + + + + false + + + Set transmitter coordinates to current position + + + + + + + :/gps.png:/gps.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Min + + + + + + + Power level to map to first colour in colour map + + + 0 + + + -150.000000000000000 + + + 100.000000000000000 + + + -120.000000000000000 + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + Max + + + + + + + Power level to map to last colour in colour map + + + 0 + + + -150.000000000000000 + + + 100.000000000000000 + + + -120.000000000000000 + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + Colour Map + + + + + + + Colour map + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 0 + 200 + 391 + 91 + + + + + 0 + 0 + + + + + 350 + 0 + + + + Data + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Latitude + + + + + + + + 100 + 16777215 + + + + Current latitude. Decimal degrees, North positive + + + + + + true + + + + + + + Qt::Vertical + + + + + + + Longitude + + + + + + + + 100 + 16777215 + + + + Current longitude. Decimal degrees, East positive + + + + + + true + + + + + + + Qt::Vertical + + + + + + + false + + + Range + + + + + + + false + + + Range (distance) to transmitter + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + false + + + km + + + + + + + Qt::Vertical + + + + + + + false + + + Loss + + + + + + + false + + + Free space path loss from current position to transmitter in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + dB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Avg + + + + + + + + 0 + 0 + + + + + 60 + 16777215 + + + + Average power in dB + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + Max + + + + + + + + 60 + 16777215 + + + + Maximum peak power in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + Min + + + + + + + + 60 + 16777215 + + + + Minimum peak power in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + Pulse + + + + + + + + 60 + 16777215 + + + + Pulse average power in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + dB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Data + + + + + + + + 70 + 0 + + + + Power measurement to plot on map + + + + None + + + + + Average + + + + + Max + + + + + Min + + + + + Pulse average + + + + + Path loss + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Display chart + + + + + + + :/carrier.png:/carrier.png + + + true + + + false + + + + + + + Save heat map as an image file + + + + + + + :/picture.png:/picture.png + + + + + + + Save data to a .csv file + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Read data from a .csv file + + + ... + + + + :/load.png:/load.png + + + false + + + + + + + Clear heat map + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 0 + 300 + 381 + 121 + + + + + 0 + 0 + + + + Chart + + + + + + + + Display average power series on chart + + + Average + + + true + + + + + + + Display maximum peak power series on chart + + + Max + + + true + + + + + + + Display minimum peak power series on chart + + + Min + + + true + + + + + + + Display pulse average power series on chart + + + Pulse + + + true + + + + + + + Display path loss series on chart + + + Path Loss + + + true + + + + + + + Qt::Vertical + + + + + + + Display the specified last number of minutes worth of power measurements + + + 1 + + + 10 + + + + + + + mins + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 200 + 0 + + + + Power vs time + + + + + + + + + 0 + 510 + 716 + 341 + + + + + 714 + 0 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + GLScope + QWidget +
gui/glscope.h
+ 1 +
+ + GLScopeGUI + QWidget +
gui/glscopegui.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + QChartView + QGraphicsView +
QtCharts
+
+
+ + deltaFrequency + rfBW + resolution + sampleRate + averagePeriod + pulseTH + txPosition + txLatitude + txLongitude + txPower + txPositionSet + minPower + maxPower + colorMap + latitude + longitude + range + pathLoss + average + maxPeak + minPeak + pulseAverage + mode + writeImage + writeCSV + readCSV + clearHeatMap + + + + + + +
diff --git a/plugins/channelrx/heatmap/heatmapplugin.cpp b/plugins/channelrx/heatmap/heatmapplugin.cpp new file mode 100644 index 000000000..fb7429542 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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 "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "heatmapgui.h" +#endif +#include "heatmap.h" +#include "heatmapwebapiadapter.h" +#include "heatmapplugin.h" + +const PluginDescriptor HeatMapPlugin::m_pluginDescriptor = { + HeatMap::m_channelId, + QStringLiteral("Heat Map"), + QStringLiteral("7.10.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +HeatMapPlugin::HeatMapPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& HeatMapPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void HeatMapPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(HeatMap::m_channelIdURI, HeatMap::m_channelId, this); +} + +void HeatMapPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + HeatMap *instance = new HeatMap(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* HeatMapPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* HeatMapPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return HeatMapGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* HeatMapPlugin::createChannelWebAPIAdapter() const +{ + return new HeatMapWebAPIAdapter(); +} diff --git a/plugins/channelrx/heatmap/heatmapplugin.h b/plugins/channelrx/heatmap/heatmapplugin.h new file mode 100644 index 000000000..7df4a5ac1 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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_HEATMAPPLUGIN_H +#define INCLUDE_HEATMAPPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class HeatMapPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.heatmap") + +public: + explicit HeatMapPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_HEATMAPPLUGIN_H diff --git a/plugins/channelrx/heatmap/heatmapsettings.cpp b/plugins/channelrx/heatmap/heatmapsettings.cpp new file mode 100644 index 000000000..f269bfaf8 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapsettings.cpp @@ -0,0 +1,197 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 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 "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" +#include "heatmapsettings.h" + +HeatMapSettings::HeatMapSettings() : + m_channelMarker(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void HeatMapSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 16000.0f; + m_minPower = -100.0f; + m_maxPower = 0.0f; + m_colorMapName = "Jet"; + m_mode = Average; + m_pulseThreshold= -50.0f; + m_averagePeriodUS = 100000; + m_sampleRate = 100; + m_txPosValid = false; + m_txLatitude = 0.0f; + m_txLongitude = 0.0f; + m_txPower = 0.0f; + m_displayChart = true; + m_displayAverage = true; + m_displayMax = true; + m_displayMin = true; + m_displayPulseAverage = true; + m_displayPathLoss = true; + m_displayMins = 2; + m_rgbColor = QColor(102, 40, 220).rgb(); + m_title = "Heat Map"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + m_workspaceIndex = 0; + m_hidden = false; +} + +QByteArray HeatMapSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_inputFrequencyOffset); + s.writeFloat(2, m_rfBandwidth); + s.writeFloat(3, m_minPower); + s.writeFloat(4, m_maxPower); + s.writeString(5, m_colorMapName); + s.writeS32(6, (int)m_mode); + s.writeFloat(7, m_pulseThreshold); + s.writeS32(8, m_averagePeriodUS); + s.writeS32(9, m_sampleRate); + s.writeBool(10, m_txPosValid); + s.writeFloat(11, m_txLatitude); + s.writeFloat(12, m_txLongitude); + s.writeFloat(13, m_txPower); + s.writeBool(14, m_displayChart); + s.writeBool(15, m_displayAverage); + s.writeBool(16, m_displayMax); + s.writeBool(17, m_displayMin); + s.writeBool(18, m_displayPulseAverage); + s.writeBool(19, m_displayPathLoss); + s.writeS32(20, m_displayMins); + + s.writeU32(21, m_rgbColor); + s.writeString(22, m_title); + + if (m_channelMarker) { + s.writeBlob(23, m_channelMarker->serialize()); + } + + s.writeS32(24, m_streamIndex); + s.writeBool(25, m_useReverseAPI); + s.writeString(26, m_reverseAPIAddress); + s.writeU32(27, m_reverseAPIPort); + s.writeU32(28, m_reverseAPIDeviceIndex); + s.writeU32(29, m_reverseAPIChannelIndex); + + if (m_rollupState) { + s.writeBlob(30, m_rollupState->serialize()); + } + + s.writeS32(32, m_workspaceIndex); + s.writeBlob(33, m_geometryBytes); + s.writeBool(34, m_hidden); + + return s.final(); +} + +bool HeatMapSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readFloat(2, &m_rfBandwidth, 16000.0f); + d.readFloat(3, &m_minPower, -100.0f); + d.readFloat(4, &m_maxPower, 0.0f); + d.readString(5, &m_colorMapName, "Jet"); + d.readS32(6, (int*)&m_mode, (int)Average); + d.readFloat(7, &m_pulseThreshold, 50.0f); + d.readS32(8, &m_averagePeriodUS, 100000); + d.readS32(9, &m_sampleRate, 100); + d.readBool(10, &m_txPosValid, false); + d.readFloat(11, &m_txLatitude); + d.readFloat(12, &m_txLongitude); + d.readFloat(13, &m_txPower); + d.readBool(14, &m_displayChart, true); + d.readBool(15, &m_displayAverage, true); + d.readBool(16, &m_displayMax, true); + d.readBool(17, &m_displayMin, true); + d.readBool(18, &m_displayPulseAverage, true); + d.readBool(19, &m_displayPathLoss, true); + d.readS32(20, &m_displayMins, 2); + + d.readU32(21, &m_rgbColor, QColor(102, 40, 220).rgb()); + d.readString(22, &m_title, "Heat Map"); + + if (m_channelMarker) + { + d.readBlob(23, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readS32(24, &m_streamIndex, 0); + d.readBool(25, &m_useReverseAPI, false); + d.readString(26, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(27, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(28, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(29, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(30, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(31, &m_workspaceIndex, 0); + d.readBlob(32, &m_geometryBytes); + d.readBool(33, &m_hidden, false); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + diff --git a/plugins/channelrx/heatmap/heatmapsettings.h b/plugins/channelrx/heatmap/heatmapsettings.h new file mode 100644 index 000000000..3b6a4c39b --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapsettings.h @@ -0,0 +1,84 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 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_HEATMAPSETTINGS_H +#define INCLUDE_HEATMAPSETTINGS_H + +#include +#include + +#include "dsp/dsptypes.h" + +class Serializable; + +struct HeatMapSettings +{ + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; + float m_minPower; + float m_maxPower; + QString m_colorMapName; + enum Mode { + None, + Average, + Max, + Min, + PulseAverage, + PathLoss, + } m_mode; + float m_pulseThreshold; + int m_averagePeriodUS; + int m_sampleRate; + bool m_txPosValid; + float m_txLatitude; + float m_txLongitude; + float m_txPower; + bool m_displayChart; + bool m_displayAverage; + bool m_displayMax; + bool m_displayMin; + bool m_displayPulseAverage; + bool m_displayPathLoss; + int m_displayMins; + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + Serializable *m_scopeGUI; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + HeatMapSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_HEATMAPSETTINGS_H */ + diff --git a/plugins/channelrx/heatmap/heatmapsink.cpp b/plugins/channelrx/heatmap/heatmapsink.cpp new file mode 100644 index 000000000..49ad7080c --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapsink.cpp @@ -0,0 +1,229 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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 "dsp/dspengine.h" +#include "dsp/datafifo.h" +#include "dsp/scopevis.h" +#include "util/db.h" +#include "util/stepfunctions.h" +#include "maincore.h" + +#include "heatmap.h" +#include "heatmapsink.h" + +HeatMapSink::HeatMapSink(HeatMap *heatMap) : + m_scopeSink(nullptr), + m_heatMap(heatMap), + m_channelSampleRate(10000), + m_channelFrequencyOffset(0), + m_magsq(0.0), + m_magsqSum(0.0), + m_magsqPeak(0.0), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_sampleBufferSize(1000), + m_sampleBufferIndex(0) +{ + resetMagLevels(); + m_sampleBuffer.resize(m_sampleBufferSize); + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +HeatMapSink::~HeatMapSink() +{ +} + +void HeatMapSink::sampleToScope(Complex sample) +{ + if (m_scopeSink) + { + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); + + if (m_sampleBufferIndex >= m_sampleBufferSize) + { + std::vector vbegin; + vbegin.push_back(m_sampleBuffer.begin()); + m_scopeSink->feed(vbegin, m_sampleBufferSize); + m_sampleBufferIndex = 0; + } + } +} + +void HeatMapSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + QMutexLocker mutexLocker(&m_mutex); // Is this too coarse, depending on size of sample vector? + Complex ci; + + for (SampleVector::const_iterator it = begin; it != end; ++it) + { + Complex c(it->real(), it->imag()); + c *= m_nco.nextIQ(); + + if (m_interpolatorDistance < 1.0f) // interpolate + { + while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + else // decimate + { + if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + } +} + +void HeatMapSink::processOneSample(Complex &ci) +{ + Real re = ci.real() / SDR_RX_SCALEF; + Real im = ci.imag() / SDR_RX_SCALEF; + Real magsq = re*re + im*im; + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + if (magsq > m_magsqPeak) { + m_magsqPeak = magsq; + } + m_magsqCount++; + + // Although computationally more expensive to take the square root here, + // it possibly reduces problems of accumulating numbers + // that may differ significantly in magnitude, for long averages + double mag = sqrt((double)magsq); + + m_magSum += mag; + if (mag > m_pulseThresholdLinear) + { + m_magPulseSum += mag; + m_magPulseCount++; + if (m_magPulseCount >= m_averageCnt) + { + m_magPulseAvg = m_magPulseSum / m_magPulseCount; + m_magPulseSum = 0.0; + m_magPulseCount = 0; + } + } + + if (mag > m_magMaxPeak) { + m_magMaxPeak = mag; + } + if (mag < m_magMinPeak) { + m_magMinPeak = mag; + } + + m_magCount++; + if (m_magCount >= m_averageCnt) + { + m_magAvg = m_magSum / m_magCount; + m_magSum = 0.0; + m_magCount = 0; + } + + // Sample to feed to scope (so we can see power at channel sample rate) + Complex scopeSample; + scopeSample.real(ci.real() / SDR_RX_SCALEF); + scopeSample.imag(ci.imag() / SDR_RX_SCALEF); + sampleToScope(scopeSample); +} + +void HeatMapSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "HeatMapSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((m_channelFrequencyOffset != channelFrequencyOffset) || + (m_channelSampleRate != channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((m_channelSampleRate != channelSampleRate) || force) + { + m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) channelSampleRate / (Real) m_settings.m_sampleRate; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void HeatMapSink::applySettings(const HeatMapSettings& settings, bool force) +{ + qDebug() << "HeatMapSink::applySettings:" + << " sampleRate: " << settings.m_sampleRate + << " averagePeriodUS: " << settings.m_averagePeriodUS + << " pulseThreshold: " << settings.m_pulseThreshold + << " force: " << force; + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) + || (settings.m_averagePeriodUS != m_settings.m_averagePeriodUS) + || (settings.m_sampleRate != m_settings.m_sampleRate) + || force) + { + m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) settings.m_sampleRate; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + if ((settings.m_averagePeriodUS != m_settings.m_averagePeriodUS) + || (settings.m_sampleRate != m_settings.m_sampleRate) + || force) + { + m_averageCnt = (int)((settings.m_averagePeriodUS * settings.m_sampleRate / 1e6)); + // For low sample rates, we want a small buffer, so scope update isn't too slow + if (settings.m_sampleRate < 100) { + m_sampleBufferSize = 1; + } else if (settings.m_sampleRate <= 100) { + m_sampleBufferSize = 10; + } else if (settings.m_sampleRate <= 10000) { + m_sampleBufferSize = 100; + } else { + m_sampleBufferSize = 1000; + } + qDebug() << "m_averageCnt" << m_averageCnt; + qDebug() << "m_sampleBufferSize" << m_sampleBufferSize; + m_sampleBuffer.resize(m_sampleBufferSize); + if (m_sampleBufferIndex >= m_sampleBufferSize) { + m_sampleBufferIndex = 0; + } + } + + if ((settings.m_pulseThreshold != m_settings.m_pulseThreshold) || force) + { + m_pulseThresholdLinear = std::pow(10.0, settings.m_pulseThreshold / 20.0); + qDebug() << "m_pulseThresholdLinear" << m_pulseThresholdLinear; + } + + m_settings = settings; +} + diff --git a/plugins/channelrx/heatmap/heatmapsink.h b/plugins/channelrx/heatmap/heatmapsink.h new file mode 100644 index 000000000..df8335e94 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapsink.h @@ -0,0 +1,159 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 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_HEATMAPSINK_H +#define INCLUDE_HEATMAPSINK_H + +#include +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/phasediscri.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "dsp/gaussian.h" +#include "util/movingaverage.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" +#include "util/crc.h" + +#include "heatmapsettings.h" + +#include +#include +#include + +class ChannelAPI; +class HeatMap; +class ScopeVis; + +class HeatMapSink : public ChannelSampleSink { +public: + HeatMapSink(HeatMap *heatMap); + ~HeatMapSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; } + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const HeatMapSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setChannel(ChannelAPI *channel) { m_channel = channel; } + + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double &peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0; + m_magsqPeak = 0.0; + m_magsqCount = 0; + } + + void getMagLevels(double& avg, double& pulseAvg, double &maxPeak, double &minPeak) + { + QMutexLocker mutexLocker(&m_mutex); + avg = m_magAvg; + pulseAvg = m_magPulseAvg; + maxPeak = m_magMaxPeak; + minPeak = m_magMinPeak; + } + + void resetMagLevels() + { + QMutexLocker mutexLocker(&m_mutex); + m_magSum = 0.0; + m_magCount = 0; + m_magAvg = std::numeric_limits::quiet_NaN(); + m_magPulseSum = 0.0; + m_magPulseCount = 0; + m_magPulseAvg = std::numeric_limits::quiet_NaN(); + m_magMinPeak = std::numeric_limits::max(); + m_magMaxPeak = -std::numeric_limits::max(); + } + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + ScopeVis* m_scopeSink; // Scope GUI to display filtered power + HeatMap *m_heatMap; + HeatMapSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_sinkSampleRate; + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + // For power meter in GUI (same as other channels) + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + MovingAverageUtil m_movingAverage; + + // For heat map + double m_magSum; + double m_magCount; + double m_magAvg; + double m_magPulseSum; + double m_magPulseCount; + double m_magPulseAvg; + double m_magMaxPeak; + double m_magMinPeak; + + int m_averageCnt; + double m_pulseThresholdLinear; + + MessageQueue *m_messageQueueToChannel; + QMutex m_mutex; + + SampleVector m_sampleBuffer; + int m_sampleBufferSize; + int m_sampleBufferIndex; + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void sampleToScope(Complex sample); +}; + +#endif // INCLUDE_HEATMAPSINK_H + diff --git a/plugins/channelrx/heatmap/heatmapwebapiadapter.cpp b/plugins/channelrx/heatmap/heatmapwebapiadapter.cpp new file mode 100644 index 000000000..87965f9d6 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapwebapiadapter.cpp @@ -0,0 +1,53 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 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 "SWGChannelSettings.h" +#include "heatmap.h" +#include "heatmapwebapiadapter.h" + +HeatMapWebAPIAdapter::HeatMapWebAPIAdapter() +{} + +HeatMapWebAPIAdapter::~HeatMapWebAPIAdapter() +{} + +int HeatMapWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings()); + response.getHeatMapSettings()->init(); + HeatMap::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int HeatMapWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + HeatMap::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} + diff --git a/plugins/channelrx/heatmap/heatmapwebapiadapter.h b/plugins/channelrx/heatmap/heatmapwebapiadapter.h new file mode 100644 index 000000000..0b2816c35 --- /dev/null +++ b/plugins/channelrx/heatmap/heatmapwebapiadapter.h @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 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_HEATMAP_WEBAPIADAPTER_H +#define INCLUDE_HEATMAP_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "heatmapsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class HeatMapWebAPIAdapter : public ChannelWebAPIAdapter { +public: + HeatMapWebAPIAdapter(); + virtual ~HeatMapWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + HeatMapSettings m_settings; +}; + +#endif // INCLUDE_HEATMAP_WEBAPIADAPTER_H + diff --git a/plugins/channelrx/heatmap/readme.md b/plugins/channelrx/heatmap/readme.md new file mode 100644 index 000000000..4af4fcb6c --- /dev/null +++ b/plugins/channelrx/heatmap/readme.md @@ -0,0 +1,159 @@ +

Heat Map plugin

+ +

Introduction

+ +This plugin can be used to generate a heat map based on RF channel power. Channel power is measured as average power, maximum peak power, minimum peak power as well as pulse average power (i.e. average power above a threshold). + +To view the Heat Map visually, the [Map Feature](../../feature/map/readme.md) should be opened. If using the 3D map, it is recommended to set the Terrain to Ellisoid (as the heat map is 2D). + +To record data for a heat map, a GPS is required, and Preferences > My Position should have "Auto-update from GPS" enabled. + +On Android, GPS setup should be automatic. On Windows/Linux/Mac, a GPS supporting NMEA via a serial port at 4800 baud is required. +The COM port / serial device should be specfied via the QT_NMEA_SERIAL_PORT environment variable before SDRangel is started. + +![Heat Map plugin GUI](../../../doc/img/HeatMap_plugin.png) + +

Interface

+ +The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md) + +![Heat Map plugin GUI](../../../doc/img/HeatMap_plugin_settings.png) + +

1: Frequency shift from center frequency of reception

+ +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. + +

2: Channel power

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

3: 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

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

5: Res - Resolution

+ +Displays the heat map resolution in metres per pixel. Currently this is fixed at ~3m. + +

6: SR - Sample Rate

+ +Sets the sample rate at which channel power is sampled and measured. Values range from 1MS/s to 100S/s in powers of 10. + +

7: 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. + +

8: 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. + +

9: TX - Enable Transmitter

+ +When checked, enables the position of the transmitter of the signal being mapped to be set and its transmit power level. This enables range and path loss calculations. + +

10: Transmitter Latitude

+ +Specifies the latitude of the transmitter in decimal degrees, North positive. + +

11: Transmitter Longitude

+ +Specifies the longitude of the transmitter in decimal degrees, East postive. + +

12: Transmitter Power

+ +Specifies the power of the transmitter in dB. + +

13: Set Transmitter Position

+ +When clicked, sets the transmitter position (10, 11) to the current position. + +

14: Min - Colour Map Mininimum Power

+ +The colour map minimum power field, specifies the power in dB, below which, power measurements are not plotted on the heat map, and above which are plotted using the colour map (16). + +

15: Max - Colour Map Maximum Power

+ +The colour map maximum power field, specifies the power in dB, above which, all power measurements are mapped to the highest entry in the colour map. + +

16: Colour Map

+ +Selects the colour map used to linearly map power measurements between the minimum (14) and maximum (15) fields to the colour of pixels that will be plotted on the map. + +

17: Latitude

+ +Displays current latitude in decimal degrees. North positive. + +

18: Longitude

+ +Displays current longitude in decimal degrees. East positive. + +

19: Range

+ +Displays the range from the current position to the transmitter position in metres or kilometres. + +

20: Loss

+ +Displays the free space path loss from the current position to the transmitter, based on the channel centre frequency, in dB + +

21: Avg - Average Power

+ +Displays the most recent average power measurement in dB. + +

22: Max - Max Peak Power

+ +Displays the current maximum peak power measurement in dB. + +

23: Min - Min Peak Power

+ +Displays the current minimum peak power measurement in dB. + +

24: Pulse - Pulse Average Power

+ +Displays the most recent pulse average power in dB. + +

25: Data

+ +Selects which power data should be plotted on the map, and also which data will be saved (26, 27) or read (28). + +

26: Display Chart

+ +Toggles whether the chart (31) is displayed. + +

27: Save to image

+ +Saves the heat map power data selected by (25) to an image file. + +

28: Save to CSV file

+ +Saves the heat map power data selected by (25) to a CSV file. + +

29: Read from CSV file

+ +Reads a heat map from a CSV file. + +

30: Clear Heat Map

+ +Clears all heat map power data. + +

31: Chart

+ +The chart displays the most recent average, max peak, min peak and pulse average measurements as well as an estimated +received signal level from the transmitter (9), taking in to account free space path loss. + +Each series can be individually enabled or disabled. +The last number of minutes worth of data displayed can be specified from 1 to 60 minutes. + +

Power Measurements

+ +The following figure shows the difference between average power, max peak power, min peak power and pulse average power, when the averaging time is over the full time of the graph. + +![Power Measurements](../../../doc/img/HeatMap_plugin_power.png) + +The 'Path loss' map displays free space path loss based on the range to the transmitter (10, 11) and centre frequency. + diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index 0a3930c0e..93d972c39 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -84,7 +84,7 @@ MapSettings::MapSettings() : m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", true, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", true, QColor(0, 192, 255), true, true, 8)); - m_itemSettings.insert("HeatMap", new MapItemSettings("HeatMap", true, QColor(102, 40, 220), true, false, 11)); + m_itemSettings.insert("HeatMap", new MapItemSettings("HeatMap", true, QColor(102, 40, 220), true, true, 11)); m_itemSettings.insert("AM", new MapItemSettings("AM", false, QColor(255, 0, 0), false, true, 10)); MapItemSettings *fmSettings = new MapItemSettings("FM", false, QColor(255, 0, 0), false, true, 12); diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 4bc3bc9c4..383a49822 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -4528,6 +4528,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setFt8DemodSettings(new SWGSDRangel::SWGFT8DemodSettings()); channelSettings->getFt8DemodSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "HeatMapSettings") + { + channelSettings->setHeatMapSettings(new SWGSDRangel::SWGHeatMapSettings()); + channelSettings->getHeatMapSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "IEEE_802_15_4_ModSettings") { channelSettings->setIeee802154ModSettings(new SWGSDRangel::SWGIEEE_802_15_4_ModSettings()); @@ -5375,6 +5380,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.setDatvModSettings(nullptr); channelSettings.setDabDemodSettings(nullptr); channelSettings.setDsdDemodSettings(nullptr); + channelSettings.setHeatMapSettings(nullptr); channelSettings.setIeee802154ModSettings(nullptr); channelSettings.setNfmDemodSettings(nullptr); channelSettings.setNfmModSettings(nullptr); @@ -5411,6 +5417,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.setBfmDemodReport(nullptr); channelReport.setDatvModReport(nullptr); channelReport.setDsdDemodReport(nullptr); + channelReport.setHeatMapReport(nullptr); channelReport.setNfmDemodReport(nullptr); channelReport.setNfmModReport(nullptr); channelReport.setNoiseFigureReport(nullptr); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 5dd3043e4..0d0f5ef3b 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -47,6 +47,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channel.freedvdemod", "FreeDVDemodSettings"}, {"sdrangel.channeltx.freedvmod", "FreeDVModSettings"}, {"sdrangel.channel.freqtracker", "FreqTrackerSettings"}, + {"sdrangel.channel.heatmap", "HeatMapSettings"}, {"sdrangel.channel.m17demod", "M17DemodSettings"}, {"sdrangel.channeltx.modm17", "M17ModSettings"}, {"sdrangel.channel.nfmdemod", "NFMDemodSettings"}, @@ -157,6 +158,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"FreeDVDemod", "FreeDVDemodSettings"}, {"FreeDVMod", "FreeDVModSettings"}, {"FreqTracker", "FreqTrackerSettings"}, + {"HeatMap", "HeatMapSettings"}, {"IEEE_802_15_4_Mod", "IEEE_802_15_4_ModSettings"}, {"M17Demod", "M17DemodSettings"}, {"M17Mod", "M17ModSettings"}, diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 09a261a3c..65020ee91 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -51,6 +51,8 @@ ChannelReport: $ref: "http://swgserver:8081/api/swagger/include/FreqTracker.yaml#/FreqTrackerReport" FT8DemodReport: $ref: "http://swgserver:8081/api/swagger/include/FT8Demod.yaml#/FT8DemodReport" + HeatMapReport: + $ref: "http://swgserver:8081/api/swagger/include/HeatMap.yaml#/HeatMapReport" M17DemodReport: $ref: "http://swgserver:8081/api/swagger/include/M17Demod.yaml#/M17DemodReport" M17ModReport: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index 6d1268600..3222b6590 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -65,6 +65,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/FreqTracker.yaml#/FreqTrackerSettings" FT8DemodSettings: $ref: "http://swgserver:8081/api/swagger/include/FT8Demod.yaml#/FT8DemodSettings" + HeatMapSettings: + $ref: "http://swgserver:8081/api/swagger/include/HeatMap.yaml#/HeatMapSettings" InterferometerSettings: $ref: "http://swgserver:8081/api/swagger/include/Interferometer.yaml#/InterferometerSettings" IEEE_802_15_4_ModSettings: diff --git a/swagger/sdrangel/api/swagger/include/HeatMap.yaml b/swagger/sdrangel/api/swagger/include/HeatMap.yaml new file mode 100644 index 000000000..7c8fa4851 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/HeatMap.yaml @@ -0,0 +1,63 @@ +HeatMapSettings: + description: HeatMap + properties: + inputFrequencyOffset: + type: integer + format: int64 + rfBandwidth: + description: channel RF bandwidth in Hz + type: number + format: float + minPower: + type: number + format: float + maxPower: + type: number + format: float + colorMapName: + type: string + mode: + description: "(0 - None, 1 - Average, 2 - Max, 3 - Min, 4 - Pulse Average)" + type: integer + pulseThreshold: + type: number + format: float + averagePeriodUS: + description: "Averaging period in microseconds" + type: integer + sampleRate: + type: integer + rgbColor: + type: integer + title: + type: string + streamIndex: + description: MIMO channel. Not relevant when connected to SI (single Rx). + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer + reverseAPIChannelIndex: + type: integer + scopeConfig: + $ref: "http://swgserver:8081/api/swagger/include/GLScope.yaml#/GLScope" + channelMarker: + $ref: "http://swgserver:8081/api/swagger/include/ChannelMarker.yaml#/ChannelMarker" + rollupState: + $ref: "http://swgserver:8081/api/swagger/include/RollupState.yaml#/RollupState" + +HeatMapReport: + description: HeatMap + properties: + channelPowerDB: + description: power received in channel (dB) + type: number + format: float + channelSampleRate: + type: integer diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index 1f324731c..8f4f6d22d 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -72,6 +72,8 @@ SWGChannelReport::SWGChannelReport() { m_freq_tracker_report_isSet = false; ft8_demod_report = nullptr; m_ft8_demod_report_isSet = false; + heat_map_report = nullptr; + m_heat_map_report_isSet = false; m17_demod_report = nullptr; m_m17_demod_report_isSet = false; m17_mod_report = nullptr; @@ -164,6 +166,8 @@ SWGChannelReport::init() { m_freq_tracker_report_isSet = false; ft8_demod_report = new SWGFT8DemodReport(); m_ft8_demod_report_isSet = false; + heat_map_report = new SWGHeatMapReport(); + m_heat_map_report_isSet = false; m17_demod_report = new SWGM17DemodReport(); m_m17_demod_report_isSet = false; m17_mod_report = new SWGM17ModReport(); @@ -272,6 +276,9 @@ SWGChannelReport::cleanup() { if(ft8_demod_report != nullptr) { delete ft8_demod_report; } + if(heat_map_report != nullptr) { + delete heat_map_report; + } if(m17_demod_report != nullptr) { delete m17_demod_report; } @@ -389,6 +396,8 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&ft8_demod_report, pJson["FT8DemodReport"], "SWGFT8DemodReport", "SWGFT8DemodReport"); + ::SWGSDRangel::setValue(&heat_map_report, pJson["HeatMapReport"], "SWGHeatMapReport", "SWGHeatMapReport"); + ::SWGSDRangel::setValue(&m17_demod_report, pJson["M17DemodReport"], "SWGM17DemodReport", "SWGM17DemodReport"); ::SWGSDRangel::setValue(&m17_mod_report, pJson["M17ModReport"], "SWGM17ModReport", "SWGM17ModReport"); @@ -511,6 +520,9 @@ SWGChannelReport::asJsonObject() { if((ft8_demod_report != nullptr) && (ft8_demod_report->isSet())){ toJsonValue(QString("FT8DemodReport"), ft8_demod_report, obj, QString("SWGFT8DemodReport")); } + if((heat_map_report != nullptr) && (heat_map_report->isSet())){ + toJsonValue(QString("HeatMapReport"), heat_map_report, obj, QString("SWGHeatMapReport")); + } if((m17_demod_report != nullptr) && (m17_demod_report->isSet())){ toJsonValue(QString("M17DemodReport"), m17_demod_report, obj, QString("SWGM17DemodReport")); } @@ -795,6 +807,16 @@ SWGChannelReport::setFt8DemodReport(SWGFT8DemodReport* ft8_demod_report) { this->m_ft8_demod_report_isSet = true; } +SWGHeatMapReport* +SWGChannelReport::getHeatMapReport() { + return heat_map_report; +} +void +SWGChannelReport::setHeatMapReport(SWGHeatMapReport* heat_map_report) { + this->heat_map_report = heat_map_report; + this->m_heat_map_report_isSet = true; +} + SWGM17DemodReport* SWGChannelReport::getM17DemodReport() { return m17_demod_report; @@ -1066,6 +1088,9 @@ SWGChannelReport::isSet(){ if(ft8_demod_report && ft8_demod_report->isSet()){ isObjectUpdated = true; break; } + if(heat_map_report && heat_map_report->isSet()){ + isObjectUpdated = true; break; + } if(m17_demod_report && m17_demod_report->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h index cce351d05..fa45747a9 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -41,6 +41,7 @@ #include "SWGFreeDVDemodReport.h" #include "SWGFreeDVModReport.h" #include "SWGFreqTrackerReport.h" +#include "SWGHeatMapReport.h" #include "SWGIEEE_802_15_4_ModReport.h" #include "SWGM17DemodReport.h" #include "SWGM17ModReport.h" @@ -148,6 +149,9 @@ public: SWGFT8DemodReport* getFt8DemodReport(); void setFt8DemodReport(SWGFT8DemodReport* ft8_demod_report); + SWGHeatMapReport* getHeatMapReport(); + void setHeatMapReport(SWGHeatMapReport* heat_map_report); + SWGM17DemodReport* getM17DemodReport(); void setM17DemodReport(SWGM17DemodReport* m17_demod_report); @@ -278,6 +282,9 @@ private: SWGFT8DemodReport* ft8_demod_report; bool m_ft8_demod_report_isSet; + SWGHeatMapReport* heat_map_report; + bool m_heat_map_report_isSet; + SWGM17DemodReport* m17_demod_report; bool m_m17_demod_report_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index dc035435a..557b412eb 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -84,6 +84,8 @@ SWGChannelSettings::SWGChannelSettings() { m_freq_tracker_settings_isSet = false; ft8_demod_settings = nullptr; m_ft8_demod_settings_isSet = false; + heat_map_settings = nullptr; + m_heat_map_settings_isSet = false; interferometer_settings = nullptr; m_interferometer_settings_isSet = false; ieee_802_15_4_mod_settings = nullptr; @@ -200,6 +202,8 @@ SWGChannelSettings::init() { m_freq_tracker_settings_isSet = false; ft8_demod_settings = new SWGFT8DemodSettings(); m_ft8_demod_settings_isSet = false; + heat_map_settings = new SWGHeatMapSettings(); + m_heat_map_settings_isSet = false; interferometer_settings = new SWGInterferometerSettings(); m_interferometer_settings_isSet = false; ieee_802_15_4_mod_settings = new SWGIEEE_802_15_4_ModSettings(); @@ -334,6 +338,9 @@ SWGChannelSettings::cleanup() { if(ft8_demod_settings != nullptr) { delete ft8_demod_settings; } + if(heat_map_settings != nullptr) { + delete heat_map_settings; + } if(interferometer_settings != nullptr) { delete interferometer_settings; } @@ -481,6 +488,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&ft8_demod_settings, pJson["FT8DemodSettings"], "SWGFT8DemodSettings", "SWGFT8DemodSettings"); + ::SWGSDRangel::setValue(&heat_map_settings, pJson["HeatMapSettings"], "SWGHeatMapSettings", "SWGHeatMapSettings"); + ::SWGSDRangel::setValue(&interferometer_settings, pJson["InterferometerSettings"], "SWGInterferometerSettings", "SWGInterferometerSettings"); ::SWGSDRangel::setValue(&ieee_802_15_4_mod_settings, pJson["IEEE_802_15_4_ModSettings"], "SWGIEEE_802_15_4_ModSettings", "SWGIEEE_802_15_4_ModSettings"); @@ -633,6 +642,9 @@ SWGChannelSettings::asJsonObject() { if((ft8_demod_settings != nullptr) && (ft8_demod_settings->isSet())){ toJsonValue(QString("FT8DemodSettings"), ft8_demod_settings, obj, QString("SWGFT8DemodSettings")); } + if((heat_map_settings != nullptr) && (heat_map_settings->isSet())){ + toJsonValue(QString("HeatMapSettings"), heat_map_settings, obj, QString("SWGHeatMapSettings")); + } if((interferometer_settings != nullptr) && (interferometer_settings->isSet())){ toJsonValue(QString("InterferometerSettings"), interferometer_settings, obj, QString("SWGInterferometerSettings")); } @@ -995,6 +1007,16 @@ SWGChannelSettings::setFt8DemodSettings(SWGFT8DemodSettings* ft8_demod_settings) this->m_ft8_demod_settings_isSet = true; } +SWGHeatMapSettings* +SWGChannelSettings::getHeatMapSettings() { + return heat_map_settings; +} +void +SWGChannelSettings::setHeatMapSettings(SWGHeatMapSettings* heat_map_settings) { + this->heat_map_settings = heat_map_settings; + this->m_heat_map_settings_isSet = true; +} + SWGInterferometerSettings* SWGChannelSettings::getInterferometerSettings() { return interferometer_settings; @@ -1344,6 +1366,9 @@ SWGChannelSettings::isSet(){ if(ft8_demod_settings && ft8_demod_settings->isSet()){ isObjectUpdated = true; break; } + if(heat_map_settings && heat_map_settings->isSet()){ + isObjectUpdated = true; break; + } if(interferometer_settings && interferometer_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h index ff58153bb..b30d51860 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -46,6 +46,7 @@ #include "SWGFreeDVDemodSettings.h" #include "SWGFreeDVModSettings.h" #include "SWGFreqTrackerSettings.h" +#include "SWGHeatMapSettings.h" #include "SWGIEEE_802_15_4_ModSettings.h" #include "SWGInterferometerSettings.h" #include "SWGLocalSinkSettings.h" @@ -176,6 +177,9 @@ public: SWGFT8DemodSettings* getFt8DemodSettings(); void setFt8DemodSettings(SWGFT8DemodSettings* ft8_demod_settings); + SWGHeatMapSettings* getHeatMapSettings(); + void setHeatMapSettings(SWGHeatMapSettings* heat_map_settings); + SWGInterferometerSettings* getInterferometerSettings(); void setInterferometerSettings(SWGInterferometerSettings* interferometer_settings); @@ -342,6 +346,9 @@ private: SWGFT8DemodSettings* ft8_demod_settings; bool m_ft8_demod_settings_isSet; + SWGHeatMapSettings* heat_map_settings; + bool m_heat_map_settings_isSet; + SWGInterferometerSettings* interferometer_settings; bool m_interferometer_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGHeatMapReport.cpp b/swagger/sdrangel/code/qt5/client/SWGHeatMapReport.cpp new file mode 100644 index 000000000..8ec8598d3 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGHeatMapReport.cpp @@ -0,0 +1,131 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGHeatMapReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGHeatMapReport::SWGHeatMapReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGHeatMapReport::SWGHeatMapReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGHeatMapReport::~SWGHeatMapReport() { + this->cleanup(); +} + +void +SWGHeatMapReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGHeatMapReport::cleanup() { + + +} + +SWGHeatMapReport* +SWGHeatMapReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGHeatMapReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGHeatMapReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGHeatMapReport::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_channel_power_db_isSet){ + obj->insert("channelPowerDB", QJsonValue(channel_power_db)); + } + if(m_channel_sample_rate_isSet){ + obj->insert("channelSampleRate", QJsonValue(channel_sample_rate)); + } + + return obj; +} + +float +SWGHeatMapReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGHeatMapReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGHeatMapReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGHeatMapReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGHeatMapReport::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_channel_power_db_isSet){ + isObjectUpdated = true; break; + } + if(m_channel_sample_rate_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGHeatMapReport.h b/swagger/sdrangel/code/qt5/client/SWGHeatMapReport.h new file mode 100644 index 000000000..5dc830817 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGHeatMapReport.h @@ -0,0 +1,64 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGHeatMapReport.h + * + * HeatMap + */ + +#ifndef SWGHeatMapReport_H_ +#define SWGHeatMapReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGHeatMapReport: public SWGObject { +public: + SWGHeatMapReport(); + SWGHeatMapReport(QString* json); + virtual ~SWGHeatMapReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGHeatMapReport* fromJson(QString &jsonString) override; + + float getChannelPowerDb(); + void setChannelPowerDb(float channel_power_db); + + qint32 getChannelSampleRate(); + void setChannelSampleRate(qint32 channel_sample_rate); + + + virtual bool isSet() override; + +private: + float channel_power_db; + bool m_channel_power_db_isSet; + + qint32 channel_sample_rate; + bool m_channel_sample_rate_isSet; + +}; + +} + +#endif /* SWGHeatMapReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGHeatMapSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGHeatMapSettings.cpp new file mode 100644 index 000000000..b579a2819 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGHeatMapSettings.cpp @@ -0,0 +1,557 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGHeatMapSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGHeatMapSettings::SWGHeatMapSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGHeatMapSettings::SWGHeatMapSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + min_power = 0.0f; + m_min_power_isSet = false; + max_power = 0.0f; + m_max_power_isSet = false; + color_map_name = nullptr; + m_color_map_name_isSet = false; + mode = 0; + m_mode_isSet = false; + pulse_threshold = 0.0f; + m_pulse_threshold_isSet = false; + average_period_us = 0; + m_average_period_us_isSet = false; + sample_rate = 0; + m_sample_rate_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = nullptr; + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + scope_config = nullptr; + m_scope_config_isSet = false; + channel_marker = nullptr; + m_channel_marker_isSet = false; + rollup_state = nullptr; + m_rollup_state_isSet = false; +} + +SWGHeatMapSettings::~SWGHeatMapSettings() { + this->cleanup(); +} + +void +SWGHeatMapSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + min_power = 0.0f; + m_min_power_isSet = false; + max_power = 0.0f; + m_max_power_isSet = false; + color_map_name = new QString(""); + m_color_map_name_isSet = false; + mode = 0; + m_mode_isSet = false; + pulse_threshold = 0.0f; + m_pulse_threshold_isSet = false; + average_period_us = 0; + m_average_period_us_isSet = false; + sample_rate = 0; + m_sample_rate_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = new QString(""); + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + scope_config = new SWGGLScope(); + m_scope_config_isSet = false; + channel_marker = new SWGChannelMarker(); + m_channel_marker_isSet = false; + rollup_state = new SWGRollupState(); + m_rollup_state_isSet = false; +} + +void +SWGHeatMapSettings::cleanup() { + + + + + if(color_map_name != nullptr) { + delete color_map_name; + } + + + + + + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + if(scope_config != nullptr) { + delete scope_config; + } + if(channel_marker != nullptr) { + delete channel_marker; + } + if(rollup_state != nullptr) { + delete rollup_state; + } +} + +SWGHeatMapSettings* +SWGHeatMapSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGHeatMapSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&min_power, pJson["minPower"], "float", ""); + + ::SWGSDRangel::setValue(&max_power, pJson["maxPower"], "float", ""); + + ::SWGSDRangel::setValue(&color_map_name, pJson["colorMapName"], "QString", "QString"); + + ::SWGSDRangel::setValue(&mode, pJson["mode"], "qint32", ""); + + ::SWGSDRangel::setValue(&pulse_threshold, pJson["pulseThreshold"], "float", ""); + + ::SWGSDRangel::setValue(&average_period_us, pJson["averagePeriodUS"], "qint32", ""); + + ::SWGSDRangel::setValue(&sample_rate, pJson["sampleRate"], "qint32", ""); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&stream_index, pJson["streamIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_channel_index, pJson["reverseAPIChannelIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&scope_config, pJson["scopeConfig"], "SWGGLScope", "SWGGLScope"); + + ::SWGSDRangel::setValue(&channel_marker, pJson["channelMarker"], "SWGChannelMarker", "SWGChannelMarker"); + + ::SWGSDRangel::setValue(&rollup_state, pJson["rollupState"], "SWGRollupState", "SWGRollupState"); + +} + +QString +SWGHeatMapSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGHeatMapSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_input_frequency_offset_isSet){ + obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); + } + if(m_rf_bandwidth_isSet){ + obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); + } + if(m_min_power_isSet){ + obj->insert("minPower", QJsonValue(min_power)); + } + if(m_max_power_isSet){ + obj->insert("maxPower", QJsonValue(max_power)); + } + if(color_map_name != nullptr && *color_map_name != QString("")){ + toJsonValue(QString("colorMapName"), color_map_name, obj, QString("QString")); + } + if(m_mode_isSet){ + obj->insert("mode", QJsonValue(mode)); + } + if(m_pulse_threshold_isSet){ + obj->insert("pulseThreshold", QJsonValue(pulse_threshold)); + } + if(m_average_period_us_isSet){ + obj->insert("averagePeriodUS", QJsonValue(average_period_us)); + } + if(m_sample_rate_isSet){ + obj->insert("sampleRate", QJsonValue(sample_rate)); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_stream_index_isSet){ + obj->insert("streamIndex", QJsonValue(stream_index)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + if(m_reverse_api_channel_index_isSet){ + obj->insert("reverseAPIChannelIndex", QJsonValue(reverse_api_channel_index)); + } + if((scope_config != nullptr) && (scope_config->isSet())){ + toJsonValue(QString("scopeConfig"), scope_config, obj, QString("SWGGLScope")); + } + if((channel_marker != nullptr) && (channel_marker->isSet())){ + toJsonValue(QString("channelMarker"), channel_marker, obj, QString("SWGChannelMarker")); + } + if((rollup_state != nullptr) && (rollup_state->isSet())){ + toJsonValue(QString("rollupState"), rollup_state, obj, QString("SWGRollupState")); + } + + return obj; +} + +qint64 +SWGHeatMapSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGHeatMapSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGHeatMapSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGHeatMapSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +float +SWGHeatMapSettings::getMinPower() { + return min_power; +} +void +SWGHeatMapSettings::setMinPower(float min_power) { + this->min_power = min_power; + this->m_min_power_isSet = true; +} + +float +SWGHeatMapSettings::getMaxPower() { + return max_power; +} +void +SWGHeatMapSettings::setMaxPower(float max_power) { + this->max_power = max_power; + this->m_max_power_isSet = true; +} + +QString* +SWGHeatMapSettings::getColorMapName() { + return color_map_name; +} +void +SWGHeatMapSettings::setColorMapName(QString* color_map_name) { + this->color_map_name = color_map_name; + this->m_color_map_name_isSet = true; +} + +qint32 +SWGHeatMapSettings::getMode() { + return mode; +} +void +SWGHeatMapSettings::setMode(qint32 mode) { + this->mode = mode; + this->m_mode_isSet = true; +} + +float +SWGHeatMapSettings::getPulseThreshold() { + return pulse_threshold; +} +void +SWGHeatMapSettings::setPulseThreshold(float pulse_threshold) { + this->pulse_threshold = pulse_threshold; + this->m_pulse_threshold_isSet = true; +} + +qint32 +SWGHeatMapSettings::getAveragePeriodUs() { + return average_period_us; +} +void +SWGHeatMapSettings::setAveragePeriodUs(qint32 average_period_us) { + this->average_period_us = average_period_us; + this->m_average_period_us_isSet = true; +} + +qint32 +SWGHeatMapSettings::getSampleRate() { + return sample_rate; +} +void +SWGHeatMapSettings::setSampleRate(qint32 sample_rate) { + this->sample_rate = sample_rate; + this->m_sample_rate_isSet = true; +} + +qint32 +SWGHeatMapSettings::getRgbColor() { + return rgb_color; +} +void +SWGHeatMapSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGHeatMapSettings::getTitle() { + return title; +} +void +SWGHeatMapSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGHeatMapSettings::getStreamIndex() { + return stream_index; +} +void +SWGHeatMapSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGHeatMapSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGHeatMapSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGHeatMapSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGHeatMapSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGHeatMapSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGHeatMapSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGHeatMapSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGHeatMapSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGHeatMapSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGHeatMapSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +SWGGLScope* +SWGHeatMapSettings::getScopeConfig() { + return scope_config; +} +void +SWGHeatMapSettings::setScopeConfig(SWGGLScope* scope_config) { + this->scope_config = scope_config; + this->m_scope_config_isSet = true; +} + +SWGChannelMarker* +SWGHeatMapSettings::getChannelMarker() { + return channel_marker; +} +void +SWGHeatMapSettings::setChannelMarker(SWGChannelMarker* channel_marker) { + this->channel_marker = channel_marker; + this->m_channel_marker_isSet = true; +} + +SWGRollupState* +SWGHeatMapSettings::getRollupState() { + return rollup_state; +} +void +SWGHeatMapSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGHeatMapSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_min_power_isSet){ + isObjectUpdated = true; break; + } + if(m_max_power_isSet){ + isObjectUpdated = true; break; + } + if(color_map_name && *color_map_name != QString("")){ + isObjectUpdated = true; break; + } + if(m_mode_isSet){ + isObjectUpdated = true; break; + } + if(m_pulse_threshold_isSet){ + isObjectUpdated = true; break; + } + if(m_average_period_us_isSet){ + isObjectUpdated = true; break; + } + if(m_sample_rate_isSet){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_stream_index_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_channel_index_isSet){ + isObjectUpdated = true; break; + } + if(scope_config && scope_config->isSet()){ + isObjectUpdated = true; break; + } + if(channel_marker && channel_marker->isSet()){ + isObjectUpdated = true; break; + } + if(rollup_state && rollup_state->isSet()){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGHeatMapSettings.h b/swagger/sdrangel/code/qt5/client/SWGHeatMapSettings.h new file mode 100644 index 000000000..dab75f031 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGHeatMapSettings.h @@ -0,0 +1,176 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGHeatMapSettings.h + * + * HeatMap + */ + +#ifndef SWGHeatMapSettings_H_ +#define SWGHeatMapSettings_H_ + +#include + + +#include "SWGChannelMarker.h" +#include "SWGGLScope.h" +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGHeatMapSettings: public SWGObject { +public: + SWGHeatMapSettings(); + SWGHeatMapSettings(QString* json); + virtual ~SWGHeatMapSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGHeatMapSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + float getMinPower(); + void setMinPower(float min_power); + + float getMaxPower(); + void setMaxPower(float max_power); + + QString* getColorMapName(); + void setColorMapName(QString* color_map_name); + + qint32 getMode(); + void setMode(qint32 mode); + + float getPulseThreshold(); + void setPulseThreshold(float pulse_threshold); + + qint32 getAveragePeriodUs(); + void setAveragePeriodUs(qint32 average_period_us); + + qint32 getSampleRate(); + void setSampleRate(qint32 sample_rate); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getStreamIndex(); + void setStreamIndex(qint32 stream_index); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + qint32 getReverseApiChannelIndex(); + void setReverseApiChannelIndex(qint32 reverse_api_channel_index); + + SWGGLScope* getScopeConfig(); + void setScopeConfig(SWGGLScope* scope_config); + + SWGChannelMarker* getChannelMarker(); + void setChannelMarker(SWGChannelMarker* channel_marker); + + SWGRollupState* getRollupState(); + void setRollupState(SWGRollupState* rollup_state); + + + virtual bool isSet() override; + +private: + qint64 input_frequency_offset; + bool m_input_frequency_offset_isSet; + + float rf_bandwidth; + bool m_rf_bandwidth_isSet; + + float min_power; + bool m_min_power_isSet; + + float max_power; + bool m_max_power_isSet; + + QString* color_map_name; + bool m_color_map_name_isSet; + + qint32 mode; + bool m_mode_isSet; + + float pulse_threshold; + bool m_pulse_threshold_isSet; + + qint32 average_period_us; + bool m_average_period_us_isSet; + + qint32 sample_rate; + bool m_sample_rate_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + QString* title; + bool m_title_isSet; + + qint32 stream_index; + bool m_stream_index_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + + qint32 reverse_api_channel_index; + bool m_reverse_api_channel_index_isSet; + + SWGGLScope* scope_config; + bool m_scope_config_isSet; + + SWGChannelMarker* channel_marker; + bool m_channel_marker_isSet; + + SWGRollupState* rollup_state; + bool m_rollup_state_isSet; + +}; + +} + +#endif /* SWGHeatMapSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index 0f350497c..a6def3911 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -155,6 +155,8 @@ #include "SWGGain.h" #include "SWGHackRFInputSettings.h" #include "SWGHackRFOutputSettings.h" +#include "SWGHeatMapReport.h" +#include "SWGHeatMapSettings.h" #include "SWGIEEE_802_15_4_ModActions.h" #include "SWGIEEE_802_15_4_ModReport.h" #include "SWGIEEE_802_15_4_ModSettings.h" @@ -1050,6 +1052,16 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGHeatMapReport").compare(type) == 0) { + SWGHeatMapReport *obj = new SWGHeatMapReport(); + obj->init(); + return obj; + } + if(QString("SWGHeatMapSettings").compare(type) == 0) { + SWGHeatMapSettings *obj = new SWGHeatMapSettings(); + obj->init(); + return obj; + } if(QString("SWGIEEE_802_15_4_ModActions").compare(type) == 0) { SWGIEEE_802_15_4_ModActions *obj = new SWGIEEE_802_15_4_ModActions(); obj->init();