diff --git a/CMakeLists.txt b/CMakeLists.txt index 89e77d5d0..f94d445c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ option(ENABLE_CHANNELRX_DEMODDSD "Enable channelrx demoddsd plugin" ON) option(ENABLE_CHANNELRX_DEMODFT8 "Enable channelrx demodft8 plugin" ON) option(ENABLE_CHANNELRX_DEMODNAVTEX "Enable channelrx demodnavtex plugin" ON) option(ENABLE_CHANNELRX_DEMODRTTY "Enable channelrx demodrtty plugin" ON) +option(ENABLE_CHANNELRX_DEMODILS "Enable channelrx demodils plugin" ON) # Channel Tx enablers option(ENABLE_CHANNELTX "Enable channeltx plugins" ON) diff --git a/doc/img/ILSDemod_plugin.png b/doc/img/ILSDemod_plugin.png new file mode 100644 index 000000000..614dec533 Binary files /dev/null and b/doc/img/ILSDemod_plugin.png differ diff --git a/doc/img/ILSDemod_plugin_alignment.png b/doc/img/ILSDemod_plugin_alignment.png new file mode 100644 index 000000000..3bfb2f2ae Binary files /dev/null and b/doc/img/ILSDemod_plugin_alignment.png differ diff --git a/doc/img/ILSDemod_plugin_chart.png b/doc/img/ILSDemod_plugin_chart.png new file mode 100644 index 000000000..cc8e59a6e Binary files /dev/null and b/doc/img/ILSDemod_plugin_chart.png differ diff --git a/doc/img/ILSDemod_plugin_map.png b/doc/img/ILSDemod_plugin_map.png new file mode 100644 index 000000000..fb9b8df2f Binary files /dev/null and b/doc/img/ILSDemod_plugin_map.png differ diff --git a/doc/img/ILSDemod_plugin_thr_to_loc.png b/doc/img/ILSDemod_plugin_thr_to_loc.png new file mode 100644 index 000000000..6119dc3aa Binary files /dev/null and b/doc/img/ILSDemod_plugin_thr_to_loc.png differ diff --git a/doc/img/ILSDemod_plugin_threshold.png b/doc/img/ILSDemod_plugin_threshold.png new file mode 100644 index 000000000..331d9aed1 Binary files /dev/null and b/doc/img/ILSDemod_plugin_threshold.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 9270f0fee..47a6f077b 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -121,6 +121,10 @@ if (ENABLE_CHANNELRX_DEMODRTTY) add_subdirectory(demodrtty) endif() +if (ENABLE_CHANNELRX_DEMODILS) + add_subdirectory(demodils) +endif() + if(NOT SERVER_MODE) add_subdirectory(heatmap) diff --git a/plugins/channelrx/demodils/CMakeLists.txt b/plugins/channelrx/demodils/CMakeLists.txt new file mode 100644 index 000000000..77b6260aa --- /dev/null +++ b/plugins/channelrx/demodils/CMakeLists.txt @@ -0,0 +1,63 @@ +project(demodils) + +set(demodils_SOURCES + ilsdemod.cpp + ilsdemodsettings.cpp + ilsdemodbaseband.cpp + ilsdemodsink.cpp + ilsdemodplugin.cpp + ilsdemodwebapiadapter.cpp +) + +set(demodils_HEADERS + ilsdemod.h + ilsdemodsettings.h + ilsdemodbaseband.h + ilsdemodsink.h + ilsdemodplugin.h + ilsdemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(demodils_SOURCES + ${demodils_SOURCES} + ilsdemodgui.cpp + ilsdemodgui.ui + ) + set(demodils_HEADERS + ${demodils_HEADERS} + ilsdemodgui.h + ) + + set(TARGET_NAME demodils) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodilssrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demodils_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() diff --git a/plugins/channelrx/demodils/ilsdemod.cpp b/plugins/channelrx/demodils/ilsdemod.cpp new file mode 100644 index 000000000..a999a975c --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemod.cpp @@ -0,0 +1,921 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ilsdemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGILSDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGMapItem.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/morsedemod.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "settings/serializable.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(ILSDemod::MsgConfigureILSDemod, Message) +MESSAGE_CLASS_DEFINITION(ILSDemod::MsgAngleEstimate, Message) + +const char * const ILSDemod::m_channelIdURI = "sdrangel.channel.ilsdemod"; +const char * const ILSDemod::m_channelId = "ILSDemod"; + +ILSDemod::ILSDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_running(false), + m_spectrumVis(SDR_RX_SCALEF), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new ILSDemodBaseband(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, + &ILSDemod::networkManagerFinished + ); + QObject::connect( + this, + &ChannelAPI::indexInDeviceSetChanged, + this, + &ILSDemod::handleIndexInDeviceSetChanged + ); +} + +ILSDemod::~ILSDemod() +{ + qDebug("ILSDemod::~ILSDemod"); + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &ILSDemod::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +void ILSDemod::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 ILSDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void ILSDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + + if (m_running) { + m_basebandSink->feed(begin, end); + } +} + +void ILSDemod::start() +{ + if (m_running) { + return; + } + + qDebug("ILSDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_basebandSink->setSpectrumSink(&m_spectrumVis); + m_thread.start(); + // FIXME: Threading!! Compare to SSB + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + ILSDemodBaseband::MsgConfigureILSDemodBaseband *msg = ILSDemodBaseband::MsgConfigureILSDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); + + m_running = true; +} + +void ILSDemod::stop() +{ + if (!m_running) { + return; + } + + qDebug("ILSDemod::stop"); + m_running = false; + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool ILSDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureILSDemod::match(cmd)) + { + MsgConfigureILSDemod& cfg = (MsgConfigureILSDemod&) cmd; + qDebug() << "ILSDemod::handleMessage: MsgConfigureILSDemod"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + qDebug() << "ILSDemod::handleMessage: DSPSignalNotification"; + // Forward to the sink + if (m_running) + { + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + m_basebandSink->getInputMessageQueue()->push(rep); + } + // Forward to GUI if any + if (m_guiMessageQueue) { + m_guiMessageQueue->push(new DSPSignalNotification(notif)); + } + + return true; + } + else if (MorseDemod::MsgReportIdent::match(cmd)) + { + MorseDemod::MsgReportIdent& report = (MorseDemod::MsgReportIdent&) cmd; + + // Forward to GUI + if (m_guiMessageQueue) + { + MorseDemod::MsgReportIdent *msg = new MorseDemod::MsgReportIdent(report); + m_guiMessageQueue->push(msg); + } + + return true; + } + else if (ILSDemod::MsgAngleEstimate::match(cmd)) + { + // Forward to GUI + ILSDemod::MsgAngleEstimate& report = (ILSDemod::MsgAngleEstimate&)cmd; + if (getMessageQueueToGUI()) + { + ILSDemod::MsgAngleEstimate *msg = new ILSDemod::MsgAngleEstimate(report); + getMessageQueueToGUI()->push(msg); + } + + // Forward via UDP + if (m_settings.m_udpEnabled) + { + QString ddm = QString::number(report.getDDM(), 'f', 3); + QByteArray bytes = ddm.toUtf8(); + m_udpSocket.writeDatagram(bytes, bytes.size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + + // Write to log file + if (m_logFile.isOpen()) + { + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + float stationAltitude = MainCore::instance()->getSettings().getAltitude(); + + QDateTime dateTime = QDateTime::currentDateTime(); + m_logStream << dateTime.date().toString() + << "," << dateTime.time().toString() + << "," << stationLatitude + << "," << stationLongitude + << "," << stationAltitude + << "," << report.getModDepth90() + << "," << report.getModDepth150() + << "," << report.getSDM() + << "," << report.getDDM() + << "," << report.getAngle() + << "," << report.getPowerCarrier() + << "," << report.getPower90() + << "," << report.getPower150() + << "\n"; + } + + return true; + } + else if (MainCore::MsgChannelDemodQuery::match(cmd)) + { + qDebug() << "ILSDemod::handleMessage: MsgChannelDemodQuery"; + sendSampleRateToDemodAnalyzer(); + + return true; + } + else + { + return false; + } +} + +ScopeVis *ILSDemod::getScopeSink() +{ + return m_basebandSink->getScopeSink(); +} + +void ILSDemod::setCenterFrequency(qint64 frequency) +{ + ILSDemodSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureILSDemod *msgToGUI = MsgConfigureILSDemod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void ILSDemod::applySettings(const ILSDemodSettings& settings, bool force) +{ + qDebug() << "ILSDemod::applySettings:" + << " m_logEnabled: " << settings.m_logEnabled + << " m_logFilename: " << settings.m_logFilename + << " 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 ((settings.m_mode != m_settings.m_mode) || force) { + reverseAPIKeys.append("mode"); + } + if ((settings.m_frequencyIndex != m_settings.m_frequencyIndex) || force) { + reverseAPIKeys.append("frequencyIndex"); + } + if ((settings.m_squelch != m_settings.m_squelch) || force) { + reverseAPIKeys.append("squelch"); + } + if ((settings.m_volume != m_settings.m_volume) || force) { + reverseAPIKeys.append("volume"); + } + if ((settings.m_audioMute != m_settings.m_audioMute) || force) { + reverseAPIKeys.append("audioMute"); + } + if ((settings.m_average != m_settings.m_average) || force) { + reverseAPIKeys.append("average"); + } + if ((settings.m_ddmUnits != m_settings.m_ddmUnits) || force) { + reverseAPIKeys.append("ddmUnits"); + } + if ((settings.m_identThreshold != m_settings.m_identThreshold) || force) { + reverseAPIKeys.append("identThreshold"); + } + if ((settings.m_ident != m_settings.m_ident) || force) { + reverseAPIKeys.append("ident"); + } + if ((settings.m_runway != m_settings.m_runway) || force) { + reverseAPIKeys.append("runway"); + } + if ((settings.m_trueBearing != m_settings.m_trueBearing) || force) { + reverseAPIKeys.append("trueBearing"); + } + if ((settings.m_latitude != m_settings.m_latitude) || force) { + reverseAPIKeys.append("latitude"); + } + if ((settings.m_longitude != m_settings.m_longitude) || force) { + reverseAPIKeys.append("longitude"); + } + if ((settings.m_elevation != m_settings.m_elevation) || force) { + reverseAPIKeys.append("elevation"); + } + if ((settings.m_glidePath != m_settings.m_glidePath) || force) { + reverseAPIKeys.append("glidePath"); + } + if ((settings.m_refHeight != m_settings.m_refHeight) || force) { + reverseAPIKeys.append("refHeight"); + } + if ((settings.m_courseWidth != m_settings.m_courseWidth) || force) { + reverseAPIKeys.append("courseWidth"); + } + if ((settings.m_udpEnabled != m_settings.m_udpEnabled) || force) { + reverseAPIKeys.append("udpEnabled"); + } + if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) { + reverseAPIKeys.append("udpAddress"); + } + if ((settings.m_udpPort != m_settings.m_udpPort) || force) { + reverseAPIKeys.append("udpPort"); + } + if ((settings.m_logFilename != m_settings.m_logFilename) || force) { + reverseAPIKeys.append("logFilename"); + } + if ((settings.m_logEnabled != m_settings.m_logEnabled) || force) { + reverseAPIKeys.append("logEnabled"); + } + 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"); + } + + if (m_running) + { + ILSDemodBaseband::MsgConfigureILSDemodBaseband *msg = ILSDemodBaseband::MsgConfigureILSDemodBaseband::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); + } + + if ((settings.m_logEnabled != m_settings.m_logEnabled) + || (settings.m_logFilename != m_settings.m_logFilename) + || force) + { + if (m_logFile.isOpen()) + { + m_logStream.flush(); + m_logFile.close(); + } + if (settings.m_logEnabled && !settings.m_logFilename.isEmpty()) + { + m_logFile.setFileName(settings.m_logFilename); + if (m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) + { + qDebug() << "ILSDemod::applySettings - Logging to: " << settings.m_logFilename; + m_logStream.setDevice(&m_logFile); + bool newFile = m_logFile.size() == 0; + if (newFile) + { + // Write header + m_logStream << "Date,Time,Latitude,Longitude,Height,MD90,MD150,SDM,DDM,Angle,Carrier(dB),90Hz(dB),150Hz(dB)\n"; + } + } + else + { + qDebug() << "ILSDemod::applySettings - Unable to open log file: " << settings.m_logFilename; + } + } + } + + m_settings = settings; +} + +void ILSDemod::sendSampleRateToDemodAnalyzer() +{ + QList pipes; + MainCore::instance()->getMessagePipes().getMessagePipes(this, "reportdemod", pipes); + + if (pipes.size() > 0) + { + for (const auto& pipe : pipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + MainCore::MsgChannelDemodReport *msg = MainCore::MsgChannelDemodReport::create( + this, + ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE + ); + messageQueue->push(msg); + } + } +} + +QByteArray ILSDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool ILSDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureILSDemod *msg = MsgConfigureILSDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureILSDemod *msg = MsgConfigureILSDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int ILSDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIlsDemodSettings(new SWGSDRangel::SWGILSDemodSettings()); + response.getIlsDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int ILSDemod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int ILSDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + ILSDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureILSDemod *msg = MsgConfigureILSDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("ILSDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureILSDemod *msgToGUI = MsgConfigureILSDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +int ILSDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIlsDemodReport(new SWGSDRangel::SWGILSDemodReport()); + response.getIlsDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void ILSDemod::webapiUpdateChannelSettings( + ILSDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getIlsDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getIlsDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("mode")) { + settings.m_mode = (ILSDemodSettings::Mode) response.getIlsDemodSettings()->getMode(); + } + if (channelSettingsKeys.contains("frequencyIndex")) { + settings.m_frequencyIndex = response.getIlsDemodSettings()->getFrequencyIndex(); + } + if (channelSettingsKeys.contains("squelch")) { + settings.m_squelch = response.getIlsDemodSettings()->getSquelch(); + } + if (channelSettingsKeys.contains("volume")) { + settings.m_volume = response.getIlsDemodSettings()->getVolume(); + } + if (channelSettingsKeys.contains("audioMute")) { + settings.m_audioMute = response.getIlsDemodSettings()->getAudioMute(); + } + if (channelSettingsKeys.contains("average")) { + settings.m_average = response.getIlsDemodSettings()->getAverage(); + } + if (channelSettingsKeys.contains("ddmUnits")) { + settings.m_ddmUnits = (ILSDemodSettings::DDMUnits) response.getIlsDemodSettings()->getDdmUnits(); + } + if (channelSettingsKeys.contains("identThreshold")) { + settings.m_identThreshold = response.getIlsDemodSettings()->getIdentThreshold(); + } + if (channelSettingsKeys.contains("ident")) { + settings.m_ident = *response.getIlsDemodSettings()->getIdent(); + } + if (channelSettingsKeys.contains("runway")) { + settings.m_runway = *response.getIlsDemodSettings()->getRunway(); + } + if (channelSettingsKeys.contains("trueBearing")) { + settings.m_trueBearing = response.getIlsDemodSettings()->getTrueBearing(); + } + if (channelSettingsKeys.contains("latitude")) { + settings.m_latitude = *response.getIlsDemodSettings()->getLatitude(); + } + if (channelSettingsKeys.contains("longitude")) { + settings.m_longitude = *response.getIlsDemodSettings()->getLongitude(); + } + if (channelSettingsKeys.contains("elevation")) { + settings.m_elevation = response.getIlsDemodSettings()->getElevation(); + } + if (channelSettingsKeys.contains("glidePath")) { + settings.m_glidePath = response.getIlsDemodSettings()->getGlidePath(); + } + if (channelSettingsKeys.contains("refHeight")) { + settings.m_refHeight = response.getIlsDemodSettings()->getRefHeight(); + } + if (channelSettingsKeys.contains("courseWidth")) { + settings.m_courseWidth = response.getIlsDemodSettings()->getCourseWidth(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getIlsDemodSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getIlsDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getIlsDemodSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("logFilename")) { + settings.m_logFilename = *response.getAdsbDemodSettings()->getLogFilename(); + } + if (channelSettingsKeys.contains("logEnabled")) { + settings.m_logEnabled = response.getAdsbDemodSettings()->getLogEnabled(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getIlsDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getIlsDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getIlsDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getIlsDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getIlsDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getIlsDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getIlsDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getIlsDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) { + settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getIlsDemodSettings()->getScopeConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getIlsDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getIlsDemodSettings()->getRollupState()); + } +} + +void ILSDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const ILSDemodSettings& settings) +{ + response.getIlsDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getIlsDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getIlsDemodSettings()->setMode((int) settings.m_mode); + response.getIlsDemodSettings()->setFrequencyIndex(settings.m_frequencyIndex); + response.getIlsDemodSettings()->setSquelch(settings.m_squelch); + response.getIlsDemodSettings()->setVolume(settings.m_volume); + response.getIlsDemodSettings()->setAudioMute(settings.m_audioMute); + response.getIlsDemodSettings()->setAverage(settings.m_average); + response.getIlsDemodSettings()->setDdmUnits((int) settings.m_ddmUnits); + response.getIlsDemodSettings()->setIdentThreshold(settings.m_identThreshold); + response.getIlsDemodSettings()->setIdent(new QString(settings.m_ident)); + response.getIlsDemodSettings()->setRunway(new QString(settings.m_runway)); + response.getIlsDemodSettings()->setTrueBearing(settings.m_trueBearing); + response.getIlsDemodSettings()->setLatitude(new QString(settings.m_latitude)); + response.getIlsDemodSettings()->setLatitude(new QString(settings.m_latitude)); + response.getIlsDemodSettings()->setElevation(settings.m_elevation); + response.getIlsDemodSettings()->setGlidePath(settings.m_glidePath); + response.getIlsDemodSettings()->setRefHeight(settings.m_refHeight); + response.getIlsDemodSettings()->setCourseWidth(settings.m_courseWidth); + response.getIlsDemodSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getIlsDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getIlsDemodSettings()->setUdpPort(settings.m_udpPort); + response.getIlsDemodSettings()->setLogFilename(new QString(settings.m_logFilename)); + response.getIlsDemodSettings()->setLogEnabled(settings.m_logEnabled); + + response.getIlsDemodSettings()->setRgbColor(settings.m_rgbColor); + if (response.getIlsDemodSettings()->getTitle()) { + *response.getIlsDemodSettings()->getTitle() = settings.m_title; + } else { + response.getIlsDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getIlsDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getIlsDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getIlsDemodSettings()->getReverseApiAddress()) { + *response.getIlsDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getIlsDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getIlsDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getIlsDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getIlsDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_scopeGUI) + { + if (response.getIlsDemodSettings()->getScopeConfig()) + { + settings.m_scopeGUI->formatTo(response.getIlsDemodSettings()->getScopeConfig()); + } + else + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + response.getIlsDemodSettings()->setScopeConfig(swgGLScope); + } + } + if (settings.m_channelMarker) + { + if (response.getIlsDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getIlsDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getIlsDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getIlsDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getIlsDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getIlsDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void ILSDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + + response.getIlsDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg)); + response.getIlsDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate()); +} + +void ILSDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const ILSDemodSettings& 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 ILSDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const ILSDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("ILSDemod")); + swgChannelSettings->setIlsDemodSettings(new SWGSDRangel::SWGILSDemodSettings()); + SWGSDRangel::SWGILSDemodSettings *swgILSDemodSettings = swgChannelSettings->getIlsDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgILSDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgILSDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("mode") || force) { + swgILSDemodSettings->setMode((int) settings.m_mode); + } + if (channelSettingsKeys.contains("frequencyIndex") || force) { + swgILSDemodSettings->setFrequencyIndex(settings.m_frequencyIndex); + } + if (channelSettingsKeys.contains("squelch") || force) { + swgILSDemodSettings->setSquelch(settings.m_squelch); + } + if (channelSettingsKeys.contains("volume") || force) { + swgILSDemodSettings->setVolume(settings.m_volume); + } + if (channelSettingsKeys.contains("audioMute") || force) { + swgILSDemodSettings->setAudioMute(settings.m_audioMute); + } + if (channelSettingsKeys.contains("average") || force) { + swgILSDemodSettings->setAverage(settings.m_average); + } + if (channelSettingsKeys.contains("ddmUnits") || force) { + swgILSDemodSettings->setDdmUnits((int) settings.m_ddmUnits); + } + if (channelSettingsKeys.contains("identThreshold") || force) { + swgILSDemodSettings->setIdentThreshold(settings.m_identThreshold); + } + if (channelSettingsKeys.contains("ident") || force) { + swgILSDemodSettings->setIdent(new QString(settings.m_ident)); + } + if (channelSettingsKeys.contains("runway") || force) { + swgILSDemodSettings->setRunway(new QString(settings.m_runway)); + } + if (channelSettingsKeys.contains("trueBearing") || force) { + swgILSDemodSettings->setTrueBearing(settings.m_trueBearing); + } + if (channelSettingsKeys.contains("latitude") || force) { + swgILSDemodSettings->setLatitude(new QString(settings.m_latitude)); + } + if (channelSettingsKeys.contains("longitude") || force) { + swgILSDemodSettings->setLongitude(new QString(settings.m_longitude)); + } + if (channelSettingsKeys.contains("elevation") || force) { + swgILSDemodSettings->setElevation(settings.m_elevation); + } + if (channelSettingsKeys.contains("glidePath") || force) { + swgILSDemodSettings->setGlidePath(settings.m_glidePath); + } + if (channelSettingsKeys.contains("refHeight") || force) { + swgILSDemodSettings->setRefHeight(settings.m_refHeight); + } + if (channelSettingsKeys.contains("courseWidth") || force) { + swgILSDemodSettings->setCourseWidth(settings.m_courseWidth); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgILSDemodSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgILSDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgILSDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("logFilename") || force) { + swgILSDemodSettings->setLogFilename(new QString(settings.m_logFilename)); + } + if (channelSettingsKeys.contains("logEnabled") || force) { + swgILSDemodSettings->setLogEnabled(settings.m_logEnabled); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgILSDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgILSDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgILSDemodSettings->setStreamIndex(settings.m_streamIndex); + } + + if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force)) + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + swgILSDemodSettings->setScopeConfig(swgGLScope); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgILSDemodSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgILSDemodSettings->setRollupState(swgRollupState); + } +} + +void ILSDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "ILSDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("ILSDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void ILSDemod::handleIndexInDeviceSetChanged(int index) +{ + if (!m_running || (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/demodils/ilsdemod.h b/plugins/channelrx/demodils/ilsdemod.h new file mode 100644 index 000000000..a75984762 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemod.h @@ -0,0 +1,222 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMOD_H +#define INCLUDE_ILSDEMOD_H + +#include +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "dsp/spectrumvis.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "ilsdemodbaseband.h" +#include "ilsdemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ScopeVis; + +class ILSDemod : public BasebandSampleSink, public ChannelAPI { +public: + class MsgConfigureILSDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ILSDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureILSDemod* create(const ILSDemodSettings& settings, bool force) + { + return new MsgConfigureILSDemod(settings, force); + } + + private: + ILSDemodSettings m_settings; + bool m_force; + + MsgConfigureILSDemod(const ILSDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + // Sent from Sink when an estimate is made of the angle + class MsgAngleEstimate : public Message { + MESSAGE_CLASS_DECLARATION + + public: + Real getPowerCarrier() const { return m_powerCarrier; } + Real getPower90() const { return m_power90; } + Real getPower150() const { return m_power150; } + Real getModDepth90() const { return m_modDepth90; } + Real getModDepth150() const { return m_modDepth150; } + Real getSDM() const { return m_sdm; } + Real getDDM() const { return m_ddm; } + Real getAngle() const { return m_angle; } + + static MsgAngleEstimate* create(Real powerCarrier, Real power90, Real power150, Real modDepth90, Real modDepth150, Real sdm, Real ddm, Real angle) + { + return new MsgAngleEstimate(powerCarrier, power90, power150, modDepth90, modDepth150, sdm, ddm, angle); + } + + private: + Real m_powerCarrier; + Real m_power90; + Real m_power150; + Real m_modDepth90; + Real m_modDepth150; + Real m_sdm; + Real m_ddm; + Real m_angle; + + MsgAngleEstimate(Real powerCarrier, Real power90, Real power150, Real modDepth90, Real modDepth150, Real sdm, Real ddm, Real angle) : + m_powerCarrier(powerCarrier), + m_power90(power90), + m_power150(power150), + m_modDepth90(modDepth90), + m_modDepth150(modDepth150), + m_sdm(sdm), + m_ddm(ddm), + m_angle(angle) + {} + }; + + ILSDemod(DeviceAPI *deviceAPI); + virtual ~ILSDemod(); + 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); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const ILSDemodSettings& settings); + + static void webapiUpdateChannelSettings( + ILSDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + SpectrumVis *getSpectrumVis() { return &m_spectrumVis; } + ScopeVis *getScopeSink(); + uint32_t getAudioSampleRate() const { return m_running ? m_basebandSink->getAudioSampleRate() : 0; } + bool getSquelchOpen() const { return m_running ? m_basebandSink->getSquelchOpen() : false; } + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } +/* 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; + ILSDemodBaseband* m_basebandSink; + bool m_running; + ILSDemodSettings m_settings; + SpectrumVis m_spectrumVis; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_centerFrequency; + QUdpSocket m_udpSocket; + QFile m_logFile; + QTextStream m_logStream; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + virtual bool handleMessage(const Message& cmd); + void applySettings(const ILSDemodSettings& settings, bool force = false); + void sendSampleRateToDemodAnalyzer(); + void webapiReverseSendSettings(QList& channelSettingsKeys, const ILSDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const ILSDemodSettings& settings, + bool force + ); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); + +}; + +#endif // INCLUDE_ILSDEMOD_H + diff --git a/plugins/channelrx/demodils/ilsdemodbaseband.cpp b/plugins/channelrx/demodils/ilsdemodbaseband.cpp new file mode 100644 index 000000000..2e31ba827 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodbaseband.cpp @@ -0,0 +1,212 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ilsdemodbaseband.h" + +MESSAGE_CLASS_DEFINITION(ILSDemodBaseband::MsgConfigureILSDemodBaseband, Message) + +ILSDemodBaseband::ILSDemodBaseband(ILSDemod *packetDemod) : + m_sink(packetDemod), + m_running(false) +{ + qDebug("ILSDemodBaseband::ILSDemodBaseband"); + + m_sink.setScopeSink(&m_scopeSink); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); + DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(m_sink.getAudioFifo(), getInputMessageQueue()); + m_sink.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate()); + m_channelSampleRate = 0; +} + +ILSDemodBaseband::~ILSDemodBaseband() +{ + m_inputMessageQueue.clear(); + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_sink.getAudioFifo()); + delete m_channelizer; +} + +void ILSDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); + m_channelSampleRate = 0; +} + +void ILSDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &ILSDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void ILSDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &ILSDemodBaseband::handleData + ); + m_running = false; +} + +void ILSDemodBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void ILSDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void ILSDemodBaseband::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 ILSDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool ILSDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureILSDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureILSDemodBaseband& cfg = (MsgConfigureILSDemodBaseband&) cmd; + qDebug() << "ILSDemodBaseband::handleMessage: MsgConfigureILSDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "ILSDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + if (m_channelSampleRate != m_channelizer->getChannelSampleRate()) + { + m_sink.applyAudioSampleRate(m_sink.getAudioSampleRate()); // reapply when channel sample rate changes + m_channelSampleRate = m_channelizer->getChannelSampleRate(); + } + + return true; + } + else + { + return false; + } +} + +void ILSDemodBaseband::applySettings(const ILSDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + if (m_channelSampleRate != m_channelizer->getChannelSampleRate()) + { + m_sink.applyAudioSampleRate(m_sink.getAudioSampleRate()); // reapply when channel sample rate changes + m_channelSampleRate = m_channelizer->getChannelSampleRate(); + } + } + + if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) + { + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + int audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_audioDeviceName); + //qDebug("AMDemod::applySettings: audioDeviceName: %s audioDeviceIndex: %d", qPrintable(settings.m_audioDeviceName), audioDeviceIndex); + audioDeviceManager->removeAudioSink(m_sink.getAudioFifo()); + audioDeviceManager->addAudioSink(m_sink.getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); + int audioSampleRate = audioDeviceManager->getOutputSampleRate(audioDeviceIndex); + + if (m_sink.getAudioSampleRate() != audioSampleRate) + { + m_channelizer->setChannelization(audioSampleRate, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + m_sink.applyAudioSampleRate(audioSampleRate); + } + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int ILSDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void ILSDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} + diff --git a/plugins/channelrx/demodils/ilsdemodbaseband.h b/plugins/channelrx/demodils/ilsdemodbaseband.h new file mode 100644 index 000000000..7f464944a --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodbaseband.h @@ -0,0 +1,110 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMODBASEBAND_H +#define INCLUDE_ILSDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/scopevis.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "ilsdemodsink.h" + +class DownChannelizer; +class ChannelAPI; +class ILSDemod; +class ScopeVis; +class SpectrumVis; + +class ILSDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureILSDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const ILSDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureILSDemodBaseband* create(const ILSDemodSettings& settings, bool force) + { + return new MsgConfigureILSDemodBaseband(settings, force); + } + + private: + ILSDemodSettings m_settings; + bool m_force; + + MsgConfigureILSDemodBaseband(const ILSDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + ILSDemodBaseband(ILSDemod *packetDemod); + ~ILSDemodBaseband(); + 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 setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } + void setBasebandSampleRate(int sampleRate); + int getChannelSampleRate() const; + void setSpectrumSink(SpectrumVis* spectrumSink) { m_spectrumVis = spectrumSink; m_sink.setSpectrumSink(spectrumSink); } + ScopeVis *getScopeSink() { return &m_scopeSink; } + void setChannel(ChannelAPI *channel); + bool getSquelchOpen() const { return m_sink.getSquelchOpen(); } + int getAudioSampleRate() const { return m_sink.getAudioSampleRate(); } + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); } + void setAudioFifoLabel(const QString& label) { m_sink.setAudioFifoLabel(label); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + int m_channelSampleRate; + ILSDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + ILSDemodSettings m_settings; + SpectrumVis *m_spectrumVis; + ScopeVis m_scopeSink; + bool m_running; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(ILSDemodSink *sink); + void applySettings(const ILSDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_ILSDEMODBASEBAND_H + diff --git a/plugins/channelrx/demodils/ilsdemodgui.cpp b/plugins/channelrx/demodils/ilsdemodgui.cpp new file mode 100644 index 000000000..5ffd4b062 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodgui.cpp @@ -0,0 +1,1531 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "SWGMapItem.h" + +#include "ilsdemodgui.h" + +#include "device/deviceset.h" +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/morsedemod.h" +#include "ui_ilsdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "util/units.h" +#include "util/morse.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "dsp/dspengine.h" +#include "dsp/glscopesettings.h" +#include "dsp/spectrumvis.h" +#include "gui/crightclickenabler.h" +#include "gui/tabletapandhold.h" +#include "gui/dialogpositioner.h" +#include "gui/audioselectdialog.h" +#include "channel/channelwebapiutils.h" +#include "feature/featurewebapiutils.h" +#include "feature/feature.h" +#include "feature/featureset.h" +#include "maincore.h" + +#include "ilsdemod.h" +#include "ilsdemodsink.h" + +MESSAGE_CLASS_DEFINITION(ILSDemodGUI::MsgGSAngle, Message) + +// 3.1.3.2 - ILS can use one or two carrier frequencies +// If one is used, frequency tolerance is +/- 0.005% -> 5.5kHz +// If two are used, frequency tolerance is +/- 0.002% and they should be symmetric about the assigned frequency +// Frequency separation should be within [5kHz,14kHz] +// Called "offset carrier" https://www.pprune.org/tech-log/108843-vhf-offset-carrier.html + +// ICAO Anntex 10 Volume 1 - 3.1.6.1 +const QStringList ILSDemodGUI::m_locFrequencies = { + "108.10", "108.15", "108.30", "108.35", "108.50", "108.55", "180.70", "108.75", + "108.90", "108.95", "109.10", "109.15", "109.30", "109.35", "109.50", "109.55", + "109.70", "109.75", "109.90", "109.95", "110.10", "110.15", "110.30", "110.35", + "110.50", "110.55", "110.70", "110.75", "110.90", "110.95", "111.10", "111.15", + "111.30", "111.35", "111.50", "111.55", "111.70", "111.75", "111.90", "111.95", +}; + +const QStringList ILSDemodGUI::m_gsFrequencies = { + "334.70", "334.55", "334.10", "333.95", "329.90", "329.75", "330.50", "330.35", + "329.30", "329.15", "331.40", "331.25", "332.00", "331.85", "332.60", "332.45", + "333.20", "333.05", "333.80", "333.65", "334.40", "334.25", "335.00", "334.85", + "329.60", "329.45", "330.20", "330.05", "330.80", "330.65", "331.70", "331.55", + "332.30", "332.15", "332.90", "332.75", "333.50", "333.35", "331.10", "330.95", +}; + +// UK data from NATS: https://nats-uk.ead-it.com/cms-nats/opencms/en/Publications/AIP/ +// Note that AIP data is more accurate in tables than on charts +// Some values tweaked from AIP to better align with 3D Map +const QList ILSDemodGUI::m_ils = { + {"EGKB", "IBGH", "21", 109350000, 205.71, 3.0, 51.338272, 0.038065, 569, 15.25, 1840, 1.13}, + {"EGKK", "IWW", "26L", 110900000, 257.59, 3.0, 51.150680, -0.171943, 212, 15.5, 3060, 0.06}, + {"EGKK", "IGG", "08R", 110900000, 77.63, 3.0, 51.145872, -0.206807, 212, 15.5, 3140, -0.06}, + {"EGLL", "ILL", "27L", 109500000, 269.72, 3.0, 51.464960, -0.434108, 93, 17.1, 3960, 0.0}, + {"EGLL", "IRR", "27R", 110300000, 269.71, 3.0, 51.477678, -0.433291, 99, 17.7, 4190, 0.0}, + {"EGLL", "IAA", "09L", 110300000, 89.67, 3.0, 51.477503, -0.484984, 99, 15.5, 4020, 0.0}, + {"EGLL", "IBB", "09R", 109500000, 89.68, 3.0, 51.464795, -0.482305, 93, 15.25, 3750, 0.0}, + {"EGLC", "ILST", "09", 111150000, 92.87, 5.5, 51.505531, 0.045766, 48, 10.7, 1510, 0.0}, + {"EGLC", "ILSR", "27", 111150000, 272.89, 5.5, 51.504927, 0.064960, 48, 10.7, 1580, 0.0}, + {"EGSS", "ISX", "22", 110500000, 222.78, 3.0, 51.895165, 0.250051, 352, 14.9, 3430, 0.0}, + {"EGSS", "ISED", "04", 110500000, 42.78, 3.0, 51.877054, 0.222887, 352, 16.2, 3130, 0.0}, +}; + +ILSDemodGUI* ILSDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + ILSDemodGUI* gui = new ILSDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void ILSDemodGUI::destroy() +{ + delete this; +} + +void ILSDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray ILSDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool ILSDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +QString ILSDemodGUI::formatAngleDirection(float angle) const +{ + QString text; + if (m_settings.m_mode == ILSDemodSettings::LOC) + { + if (angle == 0.0) { + text = "Centre"; + } else if (angle > 0.0) { + text = "Left"; + } else { + text = "Right"; + } + } + else + { + if (angle == 0.0) { + text = "On slope"; + } else if (angle > 0.0) { + text = "Above"; + } else { + text = "Below"; + } + } + return text; +} + +bool ILSDemodGUI::handleMessage(const Message& message) +{ + if (ILSDemod::MsgConfigureILSDemod::match(message)) + { + qDebug("ILSDemodGUI::handleMessage: ILSDemod::MsgConfigureILSDemod"); + const ILSDemod::MsgConfigureILSDemod& cfg = (ILSDemod::MsgConfigureILSDemod&) 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->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; + } + else if (MorseDemod::MsgReportIdent::match(message)) + { + MorseDemod::MsgReportIdent& report = (MorseDemod::MsgReportIdent&) message; + + QString ident = report.getIdent(); + QString identString = Morse::toString(ident); // Convert Morse to a string + + ui->morseIdent->setText(Morse::toSpacedUnicode(ident) + " " + identString); + + // Check it matches ident setting + if (identString == m_settings.m_ident) { + ui->morseIdent->setStyleSheet("QLabel { color: white }"); + } else { + ui->morseIdent->setStyleSheet("QLabel { color: red }"); + } + + return true; + } + else if (ILSDemod::MsgAngleEstimate::match(message)) + { + ILSDemod::MsgAngleEstimate& report = (ILSDemod::MsgAngleEstimate&) message; + ui->md90->setValue(report.getModDepth90()); + ui->md150->setValue(report.getModDepth150()); + float sdmPercent = report.getSDM() * 100.0f; + ui->sdm->setValue(sdmPercent); + // 3.1.3.5.3.6.1 + if ((sdmPercent < 30.0f) || (sdmPercent > 60.0f)) { + ui->sdm->setStyleSheet("QLineEdit { background: rgb(255, 0, 0); }"); + } else { + ui->sdm->setStyleSheet(""); + } + ui->ddm->setText(formatDDM(report.getDDM())); + float angle = report.getAngle(); + float absAngle = std::abs(angle); + ui->angle->setText(QString("%1").arg(absAngle, 0, 'f', 1)); + ui->angleDirection->setText(formatAngleDirection(angle)); + ui->pCarrier->setText(QString("%1").arg(report.getPowerCarrier(), 0, 'f', 1)); + ui->p90->setText(QString("%1").arg(report.getPower90(), 0, 'f', 1)); + ui->p150->setText(QString("%1").arg(report.getPower150(), 0, 'f', 1)); + if (m_settings.m_mode == ILSDemodSettings::LOC) { + ui->cdi->setLocalizerDDM(report.getDDM()); + } else { + ui->cdi->setGlideSlopeDDM(report.getDDM()); + } + if (m_settings.m_mode == ILSDemodSettings::GS) + { + m_gsAngle = angle; + if (!sendToLOCChannel(angle)) { + drawPath(); + } + } + else + { + m_locAngle = angle; + drawPath(); + } + return true; + } + else if (MsgGSAngle::match(message)) + { + MsgGSAngle& report = (MsgGSAngle&) message; + m_gsAngle = report.getAngle(); + drawPath(); + return true; + } + + return false; +} + +void ILSDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void ILSDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void ILSDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void ILSDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void ILSDemodGUI::on_rfBW_valueChanged(int value) +{ + float bw = value; + ui->rfBWText->setText(formatFrequency((int)bw)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void ILSDemodGUI::on_mode_currentIndexChanged(int index) +{ + ui->frequency->clear(); + m_settings.m_mode = (ILSDemodSettings::Mode)index; + if (m_settings.m_mode == ILSDemodSettings::LOC) + { + ui->cdi->setMode(CourseDeviationIndicator::LOC); + ui->frequency->setMinimumSize(60, 0); + for (const auto &freq : m_locFrequencies) { + ui->frequency->addItem(freq); + } + scanAvailableChannels(); + } + else + { + ui->cdi->setMode(CourseDeviationIndicator::GS); + ui->frequency->setMinimumSize(110, 0); + for (int i = 0; i < m_locFrequencies.size(); i++) + { + QString text = m_locFrequencies[i] + "/" + m_gsFrequencies[i]; + ui->frequency->addItem(text); + } + closePipes(); + } + applySettings(); +} + +void ILSDemodGUI::on_frequency_currentIndexChanged(int index) +{ + m_settings.m_frequencyIndex = index; + if ((index >= 0) && (index < m_locFrequencies.size())) + { + QString text; + if (m_settings.m_mode == ILSDemodSettings::LOC) { + text = m_locFrequencies[index]; + } else { + text = m_gsFrequencies[index]; + } + double frequency = text.toDouble() * 1e6; + ChannelWebAPIUtils::setCenterFrequency(m_ilsDemod->getDeviceSetIndex(), frequency); + } + applySettings(); +} + +void ILSDemodGUI::on_average_clicked(bool checked) +{ + m_settings.m_average = checked; + applySettings(); +} + +void ILSDemodGUI::on_volume_valueChanged(int value) +{ + ui->volumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_volume = value / 10.0; + applySettings(); +} + +void ILSDemodGUI::on_squelch_valueChanged(int value) +{ + ui->squelchText->setText(QString("%1 dB").arg(value)); + m_settings.m_squelch = value; + applySettings(); +} + +void ILSDemodGUI::on_audioMute_toggled(bool checked) +{ + m_settings.m_audioMute = checked; + applySettings(); +} + +void ILSDemodGUI::on_thresh_valueChanged(int value) +{ + ui->threshText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_identThreshold = value / 10.0; + applySettings(); +} + +void ILSDemodGUI::on_ddmUnits_currentIndexChanged(int index) +{ + m_settings.m_ddmUnits = (ILSDemodSettings::DDMUnits) index; + applySettings(); +} + +void ILSDemodGUI::on_ident_editingFinished() +{ + m_settings.m_ident = ui->ident->currentText(); + applySettings(); +} + +// 3.1.3.7.3 Note 1 +float ILSDemodGUI::calcCourseWidth(int m_thresholdToLocalizer) const +{ + float width = 2.0f * Units::radiansToDegrees(atan(105.0f/m_thresholdToLocalizer)); + width = std::min(6.0f, width); + return width; +} + +void ILSDemodGUI::on_ident_currentIndexChanged(int index) +{ + m_settings.m_ident = ui->ident->currentText(); + if ((index >= 0) && (index < m_ils.size())) + { + m_disableDrawILS = true; + ui->trueBearing->setValue(m_ils[index].m_trueBearing); + ui->latitude->setText(QString::number(m_ils[index].m_latitude, 'f', 8)); + on_latitude_editingFinished(); + ui->longitude->setText(QString::number(m_ils[index].m_longitude, 'f', 8)); + on_longitude_editingFinished(); + ui->elevation->setValue(m_ils[index].m_elevation); + ui->glidePath->setValue(m_ils[index].m_glidePath); + ui->height->setValue(m_ils[index].m_refHeight); + ui->courseWidth->setValue(calcCourseWidth(m_ils[index].m_thresholdToLocalizer)); + ui->slope->setValue(m_ils[index].m_slope); + ui->runway->setText(QString("%1 %2").arg(m_ils[index].m_airportICAO).arg(m_ils[index].m_runway)); + on_runway_editingFinished(); + int frequency = m_ils[index].m_frequency; + QString freqText = QString("%1").arg(frequency / 1000000.0f, 0, 'f', 2); + if (m_settings.m_mode == ILSDemodSettings::GS) + { + int index = m_locFrequencies.indexOf(freqText); + if (index >= 0) { + freqText = m_gsFrequencies[index]; + } + } + ui->frequency->setCurrentText(freqText); + m_disableDrawILS = false; + } + drawILSOnMap(); + applySettings(); +} + +void ILSDemodGUI::on_runway_editingFinished() +{ + m_settings.m_runway = ui->runway->text(); + drawILSOnMap(); + applySettings(); +} + +void ILSDemodGUI::on_findOnMap_clicked() +{ + QString pos = QString("%1,%2").arg(m_settings.m_latitude).arg(m_settings.m_longitude); + FeatureWebAPIUtils::mapFind(pos); +} + +void ILSDemodGUI::on_addMarker_clicked() +{ + float stationLatitude = MainCore::instance()->getSettings().getLatitude(); + float stationLongitude = MainCore::instance()->getSettings().getLongitude(); + float stationAltitude = MainCore::instance()->getSettings().getAltitude(); + + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "mapitems", mapPipes); + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + QString mode = m_settings.m_mode == ILSDemodSettings::LOC ? "LOC" : "GS"; + QString name = QString("%1 M%2").arg(mode).arg(m_markerNo); + swgMapItem->setName(new QString(name)); + + swgMapItem->setLatitude(stationLatitude); + swgMapItem->setLongitude(stationLongitude); + swgMapItem->setAltitude(stationAltitude); + swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND + swgMapItem->setFixedPosition(true); + swgMapItem->setPositionDateTime(new QString(QDateTime::currentDateTime().toString(Qt::ISODateWithMs))); + + swgMapItem->setImage(new QString(QString("qrc:///aprs/aprs/aprs-symbols-24-0-06.png"))); + QString text; + if (!ui->ddm->text().isEmpty()) + { + text = QString("ILS %7 Marker %1\n90Hz: %2%\n150Hz: %3%\nDDM: %4\nAngle: %5%6 %8") + .arg(m_markerNo) + .arg(ui->md90->value()) + .arg(ui->md150->value()) + .arg(ui->ddm->text()) + .arg(ui->angle->text()) + .arg(QChar(0xb0)) + .arg(mode) + .arg(ui->angleDirection->text()); + } + else + { + text = QString("ILS %1 Marker %2\nNo data").arg(mode).arg(m_markerNo); + } + swgMapItem->setText(new QString(text)); + + if (!m_mapMarkers.contains(name)) { + m_mapMarkers.insert(name, true); + } + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ilsDemod, swgMapItem); + messageQueue->push(msg); + + m_markerNo++; + } +} + +void ILSDemodGUI::removeFromMap(const QString& name) +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "mapitems", mapPipes); + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString("")); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ilsDemod, swgMapItem); + messageQueue->push(msg); + } +} + +void ILSDemodGUI::on_clearMarkers_clicked() +{ + QMutableHashIterator itr(m_mapMarkers); + while (itr.hasNext()) + { + itr.next(); + removeFromMap(itr.key()); + itr.remove(); + } + m_markerNo = 0; +} + +// Lats and longs in decimal degrees. Distance in metres. Bearing in degrees. +// https://www.movable-type.co.uk/scripts/latlong.html +static void calcRadialEndPoint(float startLatitude, float startLongitude, float distance, float bearing, float &endLatitude, float &endLongitude) +{ + double startLatRad = startLatitude*M_PI/180.0; + double startLongRad = startLongitude*M_PI/180.0; + double theta = bearing*M_PI/180.0; + double earthRadius = 6378137.0; // At equator + double delta = distance/earthRadius; + double endLatRad = std::asin(sin(startLatRad)*cos(delta) + cos(startLatRad)*sin(delta)*cos(theta)); + double endLongRad = startLongRad + std::atan2(sin(theta)*sin(delta)*cos(startLatRad), cos(delta) - sin(startLatRad)*sin(endLatRad)); + endLatitude = endLatRad*180.0/M_PI; + endLongitude = endLongRad*180.0/M_PI; +} + +void ILSDemodGUI::addPolygonToMap(const QString& name, const QString& label, const QList& coordinates, QRgb color) +{ + // Send to Map feature + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "mapitems", mapPipes); + + if (mapPipes.size() > 0) + { + if (!m_mapILS.contains(name)) { + m_mapILS.insert(name, true); + } + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + swgMapItem->setName(new QString(name)); + swgMapItem->setLabel(new QString(label)); + swgMapItem->setLatitude(coordinates[0].latitude()); + swgMapItem->setLongitude(coordinates[0].longitude()); + swgMapItem->setAltitude(coordinates[0].altitude()); + QString image = QString("none"); + swgMapItem->setImage(new QString(image)); + swgMapItem->setImageRotation(0); + swgMapItem->setFixedPosition(true); + swgMapItem->setAltitudeReference(3); // 1 - CLAMP_TO_GROUND, 3 - CLIP_TO_GROUND + swgMapItem->setColorValid(true); + swgMapItem->setColor(color); + QList *coords = new QList(); + + for (const auto& coord : coordinates) + { + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(coord.latitude()); + c->setLongitude(coord.longitude()); + c->setAltitude(coord.altitude()); + coords->append(c); + } + + swgMapItem->setCoordinates(coords); + swgMapItem->setType(2); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ilsDemod, swgMapItem); + messageQueue->push(msg); + } + } +} + +void ILSDemodGUI::addLineToMap(const QString& name, const QString& label, float startLatitude, float startLongitude, float startAltitude, float endLatitude, float endLongitude, float endAltitude) +{ + // Send to Map feature + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "mapitems", mapPipes); + + if (mapPipes.size() > 0) + { + if (!m_mapILS.contains(name)) { + m_mapILS.insert(name, true); + } + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + swgMapItem->setName(new QString(name)); + swgMapItem->setLabel(new QString(label)); + swgMapItem->setLatitude(startLatitude); + swgMapItem->setLongitude(startLongitude); + swgMapItem->setAltitude(startAltitude); + QString image = QString("none"); + swgMapItem->setImage(new QString(image)); + swgMapItem->setImageRotation(0); + swgMapItem->setFixedPosition(true); + swgMapItem->setAltitudeReference(3); // CLIP_TO_GROUND + QList *coords = new QList(); + + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(startLatitude); + c->setLongitude(startLongitude); + c->setAltitude(startAltitude); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(endLatitude); + c->setLongitude(endLongitude); + c->setAltitude(endAltitude); + coords->append(c); + + swgMapItem->setCoordinates(coords); + swgMapItem->setType(3); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ilsDemod, swgMapItem); + messageQueue->push(msg); + } + } +} + +void ILSDemodGUI::clearILSFromMap() +{ + QMutableHashIterator itr(m_mapILS); + while (itr.hasNext()) + { + itr.next(); + removeFromMap(itr.key()); + itr.remove(); + } +} + +void ILSDemodGUI::drawILSOnMap() +{ + m_ilsValid = false; + if (m_disableDrawILS) { + return; + } + + // Check and get all needed settings to display the ILS + float thresholdLatitude, thresholdLongitude; + if (!Units::stringToLatitudeAndLongitude(m_settings.m_latitude + " " + m_settings.m_longitude, thresholdLatitude, thresholdLongitude)) + { + qDebug() << "ILSDemodGUI::drawILSOnMap: Lat/lon invalid: " << (m_settings.m_latitude + " " + m_settings.m_longitude); + return; + } + // Can't set this to 0 and use CLIP_TO_GROUND or CLAMP_TO_GROUND, as ground at GARP is below runway at somewhere like EGKB + // Perhaps Map needs API so we can query altitude in current terrain + m_altitude = Units::feetToMetres(m_settings.m_elevation); + float slope = m_settings.m_slope; + + // Estimate touchdown point, which is roughly where glide-path should intersect runway + float thresholdToTouchDown = m_settings.m_refHeight / tan(Units::degreesToRadians(m_settings.m_glidePath)); + calcRadialEndPoint(thresholdLatitude, thresholdLongitude, thresholdToTouchDown, m_settings.m_trueBearing, m_tdLatitude, m_tdLongitude); + + // Estimate localizer reference point (can be localizer location, but may be behind - similar to GARP) + float thresholdToLoc = 105.0f / tan(Units::degreesToRadians(m_settings.m_courseWidth / 2.0f)); + calcRadialEndPoint(thresholdLatitude, thresholdLongitude, thresholdToLoc, m_settings.m_trueBearing, m_locLatitude, m_locLongitude); + m_locAltitude = m_altitude + thresholdToLoc * (slope / 100.0f); + + // Update GPS angle + updateGPSAngle(); + + // Check to see if there are any Maps open + QList pipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "mapitems", pipes); + if (pipes.size() == 0) { + return; + } + m_hasDrawnILS = true; + + // Check to see if we have a LOC Demod open, if so, G/S Demod doesn't need to draw + if (m_settings.m_mode == ILSDemodSettings::GS) + { + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "ilsdemod", pipes); + if (pipes.size() > 0) { + return; + } + } + + clearILSFromMap(); + + // Check to see if map is using Ellipsoid for terrain + int featureSetIndex = -1; + int featureIndex = -1; + QString terrain; + Feature *feature = FeatureWebAPIUtils::getFeature(featureSetIndex, featureIndex, "sdrangel.feature.map"); + if (feature && ChannelWebAPIUtils::getFeatureSetting(featureSetIndex, featureIndex, "terrain", terrain)) + { + if (terrain == "Ellipsoid") + { + m_altitude = 0.0f; + slope = 0.0f; + } + } + + m_locDistance = Units::nauticalMilesToMetres(18); // 3.1.3.3.1 (Can be 25NM) + m_gsDistance = Units::nauticalMilesToMetres(10); + m_ilsValid = true; + + // Runway true bearing points towards runway (direction aircraft land) - calculate bearing towards aircraft + float bearing = fmod(m_settings.m_trueBearing - 180.0f, 360.0f); + + // Calculate course line (actually true bearing (rather than magnetic course) so we can plot on map) + float lineEndLatitude, lineEndLongitude; + calcRadialEndPoint(m_locLatitude, m_locLongitude, m_locDistance, bearing, lineEndLatitude, lineEndLongitude); + + QList coords; + float midLatitude, midLongitude; + float endLatitude, endLongitude; + // Calculate sector lines + m_locToTouchdown = thresholdToLoc - thresholdToTouchDown; + float tdToEnd = (m_locDistance - m_locToTouchdown); + float locEndAltitude = m_altitude + sin(Units::degreesToRadians(m_settings.m_glidePath)) * tdToEnd; + // Coverage should be +/- 10 degrees + // We plot course sector (i.e. +/- 0.155 DDM) + float bearingR = fmod(bearing - m_settings.m_courseWidth / 2.0, 360.0f); + calcRadialEndPoint(m_locLatitude, m_locLongitude, m_locToTouchdown, bearingR, midLatitude, midLongitude); + calcRadialEndPoint(midLatitude, midLongitude, tdToEnd, bearingR, endLatitude, endLongitude); + + coords.clear(); + coords.append(QGeoCoordinate(m_locLatitude, m_locLongitude, m_locAltitude)); + coords.append(QGeoCoordinate(midLatitude, midLongitude, m_altitude)); + coords.append(QGeoCoordinate(m_tdLatitude, m_tdLongitude, m_altitude)); + addPolygonToMap("ILS Localizer Sector 150Hz Runway", "", coords, QColor(0, 200, 0, 125).rgba()); + + coords.clear(); + coords.append(QGeoCoordinate(midLatitude, midLongitude, m_altitude)); + coords.append(QGeoCoordinate(endLatitude, endLongitude, locEndAltitude)); + coords.append(QGeoCoordinate(lineEndLatitude, lineEndLongitude, locEndAltitude)); + coords.append(QGeoCoordinate(m_tdLatitude, m_tdLongitude, m_altitude)); + addPolygonToMap("ILS Localizer Sector 150Hz", "", coords, QColor(0, 200, 0, 125).rgba()); + + float bearingL = fmod(bearing + m_settings.m_courseWidth / 2.0, 360.0f); + calcRadialEndPoint(m_locLatitude, m_locLongitude, m_locToTouchdown, bearingL, midLatitude, midLongitude); + calcRadialEndPoint(midLatitude, midLongitude, tdToEnd, bearingL, endLatitude, endLongitude); + + coords.clear(); + coords.append(QGeoCoordinate(m_locLatitude, m_locLongitude, m_locAltitude)); + coords.append(QGeoCoordinate(midLatitude, midLongitude, m_altitude)); + coords.append(QGeoCoordinate(m_tdLatitude, m_tdLongitude, m_altitude)); + addPolygonToMap("ILS Localizer Sector 90Hz Runway", "", coords, QColor(0, 150, 0, 125).rgba()); + + coords.clear(); + coords.append(QGeoCoordinate(midLatitude, midLongitude, m_altitude)); + coords.append(QGeoCoordinate(endLatitude, endLongitude, locEndAltitude)); + coords.append(QGeoCoordinate(lineEndLatitude, lineEndLongitude, locEndAltitude)); + coords.append(QGeoCoordinate(m_tdLatitude, m_tdLongitude, m_altitude)); + addPolygonToMap("ILS Localizer Sector 90Hz", "", coords, QColor(0, 150, 0, 125).rgba()); + + // Calculate start of glide path + // 1.75*theta and 0.45*theta are the required limits of glidepath coverage (Figure 10) + // We plot full scale deflection (i.e. +/- 0.175 DDM) + float gpEndLatitude, gpEndLongitude; + calcRadialEndPoint(m_tdLatitude, m_tdLongitude, m_gsDistance, bearing, gpEndLatitude, gpEndLongitude); + float endAltitudeA = m_altitude + sin(Units::degreesToRadians(m_settings.m_glidePath + 0.36f)) * m_gsDistance; + float endAltitudeB = m_altitude + sin(Units::degreesToRadians(m_settings.m_glidePath - 0.36f)) * m_gsDistance; + float gpEndAltitude = m_altitude + sin(Units::degreesToRadians(m_settings.m_glidePath)) * m_gsDistance; + + coords.clear(); + coords.append(QGeoCoordinate(m_tdLatitude, m_tdLongitude, m_altitude)); + coords.append(QGeoCoordinate(gpEndLatitude, gpEndLongitude, endAltitudeA)); + coords.append(QGeoCoordinate(gpEndLatitude, gpEndLongitude, gpEndAltitude)); + addPolygonToMap("ILS Glide Path 90Hz", "", coords, QColor(150, 150, 0, 175).rgba()); + + coords.clear(); + coords.append(QGeoCoordinate(m_tdLatitude, m_tdLongitude, m_altitude)); + coords.append(QGeoCoordinate(gpEndLatitude, gpEndLongitude, endAltitudeB)); + coords.append(QGeoCoordinate(gpEndLatitude, gpEndLongitude, gpEndAltitude)); + addPolygonToMap("ILS Glide Path 150Hz", "", coords, QColor(200, 200, 0, 175).rgba()); + + drawPath(); +} + +void ILSDemodGUI::drawPath() +{ + if (!m_hasDrawnILS) { + drawILSOnMap(); + } + if (!m_ilsValid) { + return; + } + float locAngle = std::isnan(m_locAngle) ? 0.0f : m_locAngle; + float gsAngle = std::isnan(m_gsAngle) ? m_settings.m_glidePath : (m_settings.m_glidePath + m_gsAngle); + + // Plot line at current estimated loc & g/s angles + float bearing = fmod(m_settings.m_trueBearing - 180.0f + locAngle, 360.0f); + float tdLatitude, tdLongitude; + float endLatitude, endLongitude; + float distance = m_locDistance - m_locToTouchdown; + + calcRadialEndPoint(m_locLatitude, m_locLongitude, m_locToTouchdown, bearing, tdLatitude, tdLongitude); + calcRadialEndPoint(tdLatitude, tdLongitude, distance, bearing, endLatitude, endLongitude); + float endAltitude = m_altitude + sin(Units::degreesToRadians(gsAngle)) * distance; + + QStringList runwayStrings = m_settings.m_runway.split(" "); + QString label; + if (runwayStrings.size() == 2) { + label = QString("%1 %2").arg(runwayStrings[1]).arg(m_settings.m_ident); // Assume first string is airport ICAO + } else if (!runwayStrings[0].isEmpty()) { + label = QString("%1 %2").arg(runwayStrings[0]).arg(m_settings.m_ident); + } else { + label = QString("%2%3T %1").arg(m_settings.m_ident).arg((int)std::round(m_settings.m_trueBearing)).arg(QChar(0xb0)); + } + addLineToMap("ILS Radial Runway", label, m_locLatitude, m_locLongitude, m_locAltitude, tdLatitude, tdLongitude, m_altitude); + addLineToMap("ILS Radial", "", tdLatitude, tdLongitude, m_altitude, endLatitude, endLongitude, endAltitude); +} + +void ILSDemodGUI::updateGPSAngle() +{ + // Get GPS position + float gpsLatitude = MainCore::instance()->getSettings().getLatitude(); + float gpsLongitude = MainCore::instance()->getSettings().getLongitude(); + float gpsAltitude = MainCore::instance()->getSettings().getAltitude(); + QGeoCoordinate gpsPos(gpsLatitude, gpsLongitude, gpsAltitude); + + if (m_settings.m_mode == ILSDemodSettings::LOC) + { + // Calculate angle from localizer to GPS + QGeoCoordinate locPos(m_locLatitude, m_locLongitude, m_altitude); + qreal angle = gpsPos.azimuthTo(locPos); + angle = angle - m_settings.m_trueBearing; + ui->gpsAngle->setText(QString::number(std::abs(angle), 'f', 1)); + ui->gpsAngleDirection->setText(formatAngleDirection(angle)); + } + else + { + // Calculate angle from glide path runway intersection to GPS + QGeoCoordinate tdPos(m_tdLatitude, m_tdLongitude, m_altitude); + qreal d = tdPos.distanceTo(gpsPos); + float h = gpsAltitude - m_altitude; + float angle = Units::radiansToDegrees(atan(h/d)) - m_settings.m_glidePath; + ui->gpsAngle->setText(QString::number(std::abs(angle), 'f', 1)); + ui->gpsAngleDirection->setText(formatAngleDirection(angle)); + } +} + +void ILSDemodGUI::on_trueBearing_valueChanged(double value) +{ + m_settings.m_trueBearing = (float) value; + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_elevation_valueChanged(int value) +{ + m_settings.m_elevation = value; + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_latitude_editingFinished() +{ + m_settings.m_latitude = ui->latitude->text(); + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_longitude_editingFinished() +{ + m_settings.m_longitude = ui->longitude->text(); + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_glidePath_valueChanged(double value) +{ + m_settings.m_glidePath = value; + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_height_valueChanged(double value) +{ + m_settings.m_refHeight = (float)value; + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_courseWidth_valueChanged(double value) +{ + m_settings.m_courseWidth = (float)value; + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_slope_valueChanged(double value) +{ + m_settings.m_slope = (float)value; + applySettings(); + drawILSOnMap(); +} + +void ILSDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void ILSDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void ILSDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void ILSDemodGUI::on_channel1_currentIndexChanged(int index) +{ + m_settings.m_scopeCh1 = index; + applySettings(); +} + +void ILSDemodGUI::on_channel2_currentIndexChanged(int index) +{ + m_settings.m_scopeCh2 = index; + applySettings(); +} + +void ILSDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void ILSDemodGUI::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_ilsDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + } + + dialog.move(p); + new DialogPositioner(&dialog, false); + 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(); +} + +ILSDemodGUI::ILSDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::ILSDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_doApplySettings(true), + m_squelchOpen(false), + m_tickCount(0), + m_markerNo(0), + m_disableDrawILS(false), + m_hasDrawnILS(false), + m_locAngle(NAN), + m_gsAngle(NAN) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demodils/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_ilsDemod = reinterpret_cast(rxChannel); + m_ilsDemod->setMessageQueueToGUI(getInputMessageQueue()); + m_spectrumVis = m_ilsDemod->getSpectrumVis(); + m_spectrumVis->setGLSpectrum(ui->glSpectrum); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + CRightClickEnabler *audioMuteRightClickEnabler = new CRightClickEnabler(ui->audioMute); + connect(audioMuteRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(audioSelect(const QPoint &))); + + 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_scopeVis = m_ilsDemod->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 the IQ waveforms + ui->scopeGUI->setPreTrigger(1); + GLScopeSettings::TraceData traceDataI, traceDataQ; + traceDataI.m_projectionType = Projector::ProjectionReal; + traceDataI.m_amp = 1.0; // for -1 to +1 + traceDataI.m_ofs = 0.0; // vertical offset + traceDataQ.m_projectionType = Projector::ProjectionImag; + traceDataQ.m_amp = 1.0; + traceDataQ.m_ofs = 0.0; + ui->scopeGUI->changeTrace(0, traceDataI); + ui->scopeGUI->addTrace(traceDataQ); + ui->scopeGUI->setDisplayMode(GLScopeSettings::DisplayXYV); + 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(ILSDemodSettings::ILSDEMOD_SPECTRUM_SAMPLE_RATE); + m_scopeVis->configure(500, 1, 0, 0, true); // not working! + //m_scopeVis->setFreeRun(false); // FIXME: add method rather than call m_scopeVis->configure() + + // FIXME: Don't do this - SpectrumSettings will be inconsistant.. + ui->glSpectrum->setCenterFrequency(0); + ui->glSpectrum->setSampleRate(ILSDemodSettings::ILSDEMOD_SPECTRUM_SAMPLE_RATE); + ui->glSpectrum->setDisplayWaterfall(true); + ui->glSpectrum->setDisplayMaxHold(false); + ui->glSpectrum->setSsbSpectrum(false); + ui->glSpectrum->setDisplayHistogram(false); + ui->glSpectrum->setDisplayCurrent(true); + ui->glSpectrum->setSpectrumStyle(SpectrumSettings::Gradient); + ui->glSpectrum->setMeasurementParams(SpectrumSettings::MeasurementPeaks, 0, 1000, 90, 150, 1, 5, true, 1); + ui->glSpectrum->setMeasurementsVisible(true); + ui->spectrumGUI->setBuddies(m_spectrumVis, ui->glSpectrum); + + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("ILS Demodulator"); + 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.setSpectrumGUI(ui->spectrumGUI); + m_settings.setRollupState(&m_rollupState); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + on_mode_currentIndexChanged(ui->mode->currentIndex()); // Populate frequencies + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + for (const auto& ils : m_ils) { + ui->ident->addItem(ils.m_ident); + } + + // Get updated when position changes + connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &ILSDemodGUI::preferenceChanged); + + ui->scopeContainer->setVisible(false); + + displaySettings(); + makeUIConnections(); + applySettings(true); + drawILSOnMap(); + + bool devMode = false; + ui->pCarrierLabel->setVisible(devMode); + ui->pCarrier->setVisible(devMode); + ui->pCarrierUnits->setVisible(devMode); + ui->p90Label->setVisible(devMode); + ui->p90->setVisible(devMode); + ui->p90Units->setVisible(devMode); + ui->p150Label->setVisible(devMode); + ui->p150->setVisible(devMode); + ui->p150Units->setVisible(devMode); + + SpectrumSettings spectrumSettings = m_spectrumVis->getSettings(); + spectrumSettings.m_fftSize = 2048; + spectrumSettings.m_averagingMode = SpectrumSettings::AvgModeMoving; + spectrumSettings.m_averagingValue = 1; + // FLAT TOP? + SpectrumVis::MsgConfigureSpectrumVis *msg = SpectrumVis::MsgConfigureSpectrumVis::create(spectrumSettings, false); + m_spectrumVis->getInputMessageQueue()->push(msg); + + scanAvailableChannels(); + QObject::connect( + MainCore::instance(), + &MainCore::channelAdded, + this, + &ILSDemodGUI::handleChannelAdded + ); +} + +ILSDemodGUI::~ILSDemodGUI() +{ + QObject::disconnect(&MainCore::instance()->getMasterTimer(), &QTimer::timeout, this, &ILSDemodGUI::tick); + QObject::disconnect(MainCore::instance(), &MainCore::channelAdded, this, &ILSDemodGUI::handleChannelAdded); + on_clearMarkers_clicked(); + clearILSFromMap(); + delete ui; +} + +void ILSDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void ILSDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + ILSDemod::MsgConfigureILSDemod* message = ILSDemod::MsgConfigureILSDemod::create( m_settings, force); + m_ilsDemod->getInputMessageQueue()->push(message); + } +} + +QString ILSDemodGUI::formatDDM(float ddm) const +{ + switch (m_settings.m_ddmUnits) + { + case ILSDemodSettings::PERCENT: + return QString::number(ddm * 100.0f, 'f', 1); + case ILSDemodSettings::MICROAMPS: + if (m_settings.m_mode == ILSDemodSettings::LOC) { + return QString::number(ddm * 967.75f, 'f', 1); // 0.155 -> 150uA + } else { + return QString::number(ddm * 857.125f, 'f', 1); + } + default: + return QString::number(ddm, 'f', 3); + } +} + +QString ILSDemodGUI::formatFrequency(int frequency) const +{ + QString suffix = ""; + if (width() > 450) { + suffix = " Hz"; + } + return QString("%1%2").arg(frequency).arg(suffix); +} + +void ILSDemodGUI::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(formatFrequency((int)m_settings.m_rfBandwidth)); + ui->rfBW->setValue(m_settings.m_rfBandwidth); + + ui->volume->setValue(m_settings.m_volume * 10.0); + ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 0, 'f', 1)); + + ui->squelch->setValue(m_settings.m_squelch); + ui->squelchText->setText(QString("%1 dB").arg(m_settings.m_squelch)); + + ui->audioMute->setChecked(m_settings.m_audioMute); + ui->average->setChecked(m_settings.m_average); + + ui->thresh->setValue(m_settings.m_identThreshold * 10.0); + ui->threshText->setText(QString("%1").arg(m_settings.m_identThreshold, 0, 'f', 1)); + + m_disableDrawILS = true; + ui->mode->setCurrentIndex((int) m_settings.m_mode); + ui->frequency->setCurrentIndex(m_settings.m_frequencyIndex); + ui->ident->setCurrentText(m_settings.m_ident); + ui->runway->setText(m_settings.m_runway); + on_runway_editingFinished(); + ui->trueBearing->setValue(m_settings.m_trueBearing); + ui->height->setValue(m_settings.m_refHeight); + ui->courseWidth->setValue(m_settings.m_courseWidth); + ui->slope->setValue(m_settings.m_slope); + ui->latitude->setText(m_settings.m_latitude); + ui->longitude->setText(m_settings.m_longitude); + ui->elevation->setValue(m_settings.m_elevation); + ui->glidePath->setValue(m_settings.m_glidePath); + m_disableDrawILS = false; + + updateIndexLabel(); + + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + + ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); + ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); + + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + ui->logEnable->setChecked(m_settings.m_logEnabled); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); + drawILSOnMap(); +} + +void ILSDemodGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void ILSDemodGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void ILSDemodGUI::audioSelect(const QPoint& p) +{ + qDebug("ILSDemodGUI::audioSelect"); + AudioSelectDialog audioSelect(DSPEngine::instance()->getAudioDeviceManager(), m_settings.m_audioDeviceName); + audioSelect.move(p); + audioSelect.exec(); + + if (audioSelect.m_selected) + { + m_settings.m_audioDeviceName = audioSelect.m_audioDeviceName; + applySettings(); + } +} + +void ILSDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_ilsDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbPeak = CalcDb::dbPower(magsqPeak); + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + } + + int audioSampleRate = m_ilsDemod->getAudioSampleRate(); + bool squelchOpen = m_ilsDemod->getSquelchOpen(); + + if (squelchOpen != m_squelchOpen) + { + if (audioSampleRate < 0) { + ui->audioMute->setStyleSheet("QToolButton { background-color : red; }"); + } else if (squelchOpen) { + ui->audioMute->setStyleSheet("QToolButton { background-color : green; }"); + } else { + ui->audioMute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + } + + m_squelchOpen = squelchOpen; + } + + if (!m_hasDrawnILS && (m_tickCount % 25 == 0)) + { + // Check to see if there are any Maps open - Is there a signal we can use instead? + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "mapitems", mapPipes); + if (mapPipes.size() > 0) { + drawILSOnMap(); + } + } + + m_tickCount++; +} + +void ILSDemodGUI::on_logEnable_clicked(bool checked) +{ + m_settings.m_logEnabled = checked; + applySettings(); +} + +void ILSDemodGUI::on_logFilename_clicked() +{ + // Get filename to save to + QFileDialog fileDialog(nullptr, "Select CSV file to log data to", "", "*.csv"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + m_settings.m_logFilename = fileNames[0]; + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + applySettings(); + } + } +} + +void ILSDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &ILSDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &ILSDemodGUI::on_rfBW_valueChanged); + QObject::connect(ui->mode, QOverload::of(&QComboBox::currentIndexChanged), this, &ILSDemodGUI::on_mode_currentIndexChanged); + QObject::connect(ui->frequency, QOverload::of(&QComboBox::currentIndexChanged), this, &ILSDemodGUI::on_frequency_currentIndexChanged); + QObject::connect(ui->average, &QPushButton::clicked, this, &ILSDemodGUI::on_average_clicked); + QObject::connect(ui->volume, &QDial::valueChanged, this, &ILSDemodGUI::on_volume_valueChanged); + QObject::connect(ui->squelch, &QDial::valueChanged, this, &ILSDemodGUI::on_squelch_valueChanged); + QObject::connect(ui->audioMute, &QToolButton::toggled, this, &ILSDemodGUI::on_audioMute_toggled); + QObject::connect(ui->thresh, &QDial::valueChanged, this, &ILSDemodGUI::on_thresh_valueChanged); + QObject::connect(ui->ddmUnits, QOverload::of(&QComboBox::currentIndexChanged), this, &ILSDemodGUI::on_ddmUnits_currentIndexChanged); + QObject::connect(ui->ident, QOverload::of(&QComboBox::currentIndexChanged), this, &ILSDemodGUI::on_ident_currentIndexChanged); + QObject::connect(ui->ident->lineEdit(), &QLineEdit::editingFinished, this, &ILSDemodGUI::on_ident_editingFinished); + QObject::connect(ui->runway, &QLineEdit::editingFinished, this, &ILSDemodGUI::on_runway_editingFinished); + QObject::connect(ui->trueBearing, QOverload::of(&QDoubleSpinBox::valueChanged), this, &ILSDemodGUI::on_trueBearing_valueChanged); + QObject::connect(ui->elevation, QOverload::of(&QSpinBox::valueChanged), this, &ILSDemodGUI::on_elevation_valueChanged); + QObject::connect(ui->latitude, &QLineEdit::editingFinished, this, &ILSDemodGUI::on_latitude_editingFinished); + QObject::connect(ui->longitude, &QLineEdit::editingFinished, this, &ILSDemodGUI::on_longitude_editingFinished); + QObject::connect(ui->glidePath, QOverload::of(&QDoubleSpinBox::valueChanged), this, &ILSDemodGUI::on_glidePath_valueChanged); + QObject::connect(ui->height, QOverload::of(&QDoubleSpinBox::valueChanged), this, &ILSDemodGUI::on_height_valueChanged); + QObject::connect(ui->courseWidth, QOverload::of(&QDoubleSpinBox::valueChanged), this, &ILSDemodGUI::on_courseWidth_valueChanged); + QObject::connect(ui->slope, QOverload::of(&QDoubleSpinBox::valueChanged), this, &ILSDemodGUI::on_slope_valueChanged); + QObject::connect(ui->findOnMap, &QPushButton::clicked, this, &ILSDemodGUI::on_findOnMap_clicked); + QObject::connect(ui->addMarker, &QPushButton::clicked, this, &ILSDemodGUI::on_addMarker_clicked); + QObject::connect(ui->clearMarkers, &QPushButton::clicked, this, &ILSDemodGUI::on_clearMarkers_clicked); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &ILSDemodGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &ILSDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &ILSDemodGUI::on_udpPort_editingFinished); + QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &ILSDemodGUI::on_logEnable_clicked); + QObject::connect(ui->logFilename, &QToolButton::clicked, this, &ILSDemodGUI::on_logFilename_clicked); + QObject::connect(ui->channel1, QOverload::of(&QComboBox::currentIndexChanged), this, &ILSDemodGUI::on_channel1_currentIndexChanged); + QObject::connect(ui->channel2, QOverload::of(&QComboBox::currentIndexChanged), this, &ILSDemodGUI::on_channel2_currentIndexChanged); +} + +void ILSDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} + +void ILSDemodGUI::preferenceChanged(int elementType) +{ + Preferences::ElementType pref = (Preferences::ElementType)elementType; + if ((pref == Preferences::Latitude) || (pref == Preferences::Longitude) || (pref == Preferences::Altitude)) { + updateGPSAngle(); + } +} + +bool ILSDemodGUI::sendToLOCChannel(float angle) +{ + QList pipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_ilsDemod, "ilsdemod", pipes); + for (const auto& pipe : pipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + MsgGSAngle *msg = MsgGSAngle::create(angle); + messageQueue->push(msg); + } + return pipes.size() > 0; +} + +void ILSDemodGUI::closePipes() +{ + for (const auto channel : m_availableChannels) + { + ObjectPipe *pipe = MainCore::instance()->getMessagePipes().unregisterProducerToConsumer(channel, m_ilsDemod, "ilsdemod"); + if (pipe) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + + if (messageQueue) { + disconnect(messageQueue, &MessageQueue::messageEnqueued, this, nullptr); // Have to use nullptr, as slot is a lambda. + } + } + } +} + +void ILSDemodGUI::scanAvailableChannels() +{ + MainCore *mainCore = MainCore::instance(); + MessagePipes& messagePipes = mainCore->getMessagePipes(); + std::vector& deviceSets = mainCore->getDeviceSets(); + m_availableChannels.clear(); + int deviceSetIndex = 0; + + for (const auto& deviceSet : deviceSets) + { + DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine; + + if (deviceSourceEngine) + { + for (int chi = 0; chi < deviceSet->getNumberOfChannels(); chi++) + { + ChannelAPI *channel = deviceSet->getChannelAt(chi); + + if ((channel->getURI() == "sdrangel.channel.ilsdemod") && !m_availableChannels.contains(channel) && (m_ilsDemod != channel)) + { + ObjectPipe *pipe = messagePipes.registerProducerToConsumer(channel, m_ilsDemod, "ilsdemod"); + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + QObject::connect( + messageQueue, + &MessageQueue::messageEnqueued, + this, + [=](){ this->handleChannelMessageQueue(messageQueue); }, + Qt::QueuedConnection + ); + QObject::connect( + pipe, + &ObjectPipe::toBeDeleted, + this, + &ILSDemodGUI::handleMessagePipeToBeDeleted + ); + m_availableChannels.insert(channel); + } + } + } + deviceSetIndex++; + } +} + +void ILSDemodGUI::handleChannelAdded(int deviceSetIndex, ChannelAPI *channel) +{ + qDebug("ILSDemodGUI::handleChannelAdded: deviceSetIndex: %d:%d channel: %s (%p)", + deviceSetIndex, channel->getIndexInDeviceSet(), qPrintable(channel->getURI()), channel); + std::vector& deviceSets = MainCore::instance()->getDeviceSets(); + DeviceSet *deviceSet = deviceSets[deviceSetIndex]; + DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine; + + if (deviceSourceEngine && (channel->getURI() == "sdrangel.channel.ilsdemod")) + { + if (!m_availableChannels.contains(channel) && (m_ilsDemod != channel)) + { + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + ObjectPipe *pipe = messagePipes.registerProducerToConsumer(channel, m_ilsDemod, "ilsdemod"); + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + QObject::connect( + messageQueue, + &MessageQueue::messageEnqueued, + this, + [=](){ this->handleChannelMessageQueue(messageQueue); }, + Qt::QueuedConnection + ); + QObject::connect( + pipe, + &ObjectPipe::toBeDeleted, + this, + &ILSDemodGUI::handleMessagePipeToBeDeleted + ); + m_availableChannels.insert(channel); + } + } +} + +void ILSDemodGUI::handleMessagePipeToBeDeleted(int reason, QObject* object) +{ + if ((reason == 0) && m_availableChannels.contains((ChannelAPI*) object)) // producer (channel) + { + qDebug("ILSDemodGUI::handleMessagePipeToBeDeleted: removing channel at (%p)", object); + m_availableChannels.remove((ChannelAPI*) object); + } +} + +void ILSDemodGUI::handleChannelMessageQueue(MessageQueue* messageQueue) +{ + Message* message; + + while ((message = messageQueue->pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + diff --git a/plugins/channelrx/demodils/ilsdemodgui.h b/plugins/channelrx/demodils/ilsdemodgui.h new file mode 100644 index 000000000..3c26252f1 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodgui.h @@ -0,0 +1,224 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMODGUI_H +#define INCLUDE_ILSDEMODGUI_H + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include "ilsdemod.h" +#include "ilsdemodsettings.h" + +#include + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class SpectrumVis; +class ILSDemod; +class ILSDemodGUI; + +namespace Ui { + class ILSDemodGUI; +} +class ILSDemodGUI; + +class ILSDemodGUI : public ChannelGUI { + Q_OBJECT + + struct ILS { + QString m_airportICAO; + QString m_ident; // ILS identifier + QString m_runway; + int m_frequency; // In Hz + float m_trueBearing; // In degrees + float m_glidePath; // In degrees + double m_latitude; // Position of threshold + double m_longitude; + int m_elevation; // In feet as it is on most charts - FIXME: Meters + float m_refHeight; // ILS reference datum height above threshold + int m_thresholdToLocalizer; // Distance from localizer antenna (GARP) to threshold (LTP) + float m_slope; // In % + }; + + // Send from G/S channel to LOC channel + class MsgGSAngle : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getAngle() const { return m_angle; } + + static MsgGSAngle* create(float angle) + { + return new MsgGSAngle(angle); + } + + private: + float m_angle; + + MsgGSAngle(float angle) : + m_angle(angle) + {} + }; + + + +public: + static ILSDemodGUI* 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(); + +private: + Ui::ILSDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + ILSDemodSettings m_settings; + qint64 m_deviceCenterFrequency; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + SpectrumVis* m_spectrumVis; + + ILSDemod* m_ilsDemod; + bool m_squelchOpen; + int m_basebandSampleRate; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + int m_markerNo; + QHash m_mapMarkers; + QHash m_mapILS; + bool m_disableDrawILS; + bool m_hasDrawnILS; + + bool m_ilsValid; + float m_locLatitude; + float m_locLongitude; + float m_tdLatitude; + float m_tdLongitude; + float m_altitude; // Threshold, in metres + float m_locDistance; // Range of localizer in metres + float m_gsDistance; + float m_locToTouchdown; + float m_locAltitude; + + float m_locAngle; + float m_gsAngle; + + static const QStringList m_locFrequencies; + static const QStringList m_gsFrequencies; + static const QList m_ils; + + explicit ILSDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~ILSDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + qint64 getFrequency(); + QString formatFrequency(int frequency) const; + QString formatDDM(float ddm) const; + QString formatAngleDirection(float angle) const; + void removeFromMap(const QString& name); + void drawILSOnMap(); + void drawPath(); + void clearILSFromMap(); + void addLineToMap(const QString& name, const QString& label, float startLatitude, float startLongitude, float startAltitude, float endLatitude, float endLongitude, float endAltitude); + void addPolygonToMap(const QString& name, const QString& label, const QList& coordinates, QRgb color); + void updateGPSAngle(); + float calcCourseWidth(int m_thresholdToLocalizer) const; + + void leaveEvent(QEvent*); + void enterEvent(EnterEventType*); + + bool sendToLOCChannel(float angle); + void closePipes(); + void scanAvailableChannels(); + void handleChannelAdded(int deviceSetIndex, ChannelAPI *channel); + void handleMessagePipeToBeDeleted(int reason, QObject* object); + void handleChannelMessageQueue(MessageQueue* messageQueue); + QSet m_availableChannels; + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_mode_currentIndexChanged(int index); + void on_frequency_currentIndexChanged(int index); + void on_average_clicked(bool checked); + void on_thresh_valueChanged(int value); + void on_volume_valueChanged(int value); + void on_squelch_valueChanged(int value); + void on_audioMute_toggled(bool checked); + void on_ddmUnits_currentIndexChanged(int index); + void on_ident_editingFinished(); + void on_ident_currentIndexChanged(int index); + void on_runway_editingFinished(); + void on_trueBearing_valueChanged(double value); + void on_latitude_editingFinished(); + void on_longitude_editingFinished(); + void on_elevation_valueChanged(int value); + void on_glidePath_valueChanged(double value); + void on_height_valueChanged(double value); + void on_courseWidth_valueChanged(double value); + void on_slope_valueChanged(double value); + void on_findOnMap_clicked(); + void on_clearMarkers_clicked(); + void on_addMarker_clicked(); + void on_udpEnabled_clicked(bool checked); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + void on_logEnable_clicked(bool checked=false); + void on_logFilename_clicked(); + void on_channel1_currentIndexChanged(int index); + void on_channel2_currentIndexChanged(int index); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void audioSelect(const QPoint& p); + void tick(); + void preferenceChanged(int elementType); +}; + +#endif // INCLUDE_ILSDEMODGUI_H + diff --git a/plugins/channelrx/demodils/ilsdemodgui.ui b/plugins/channelrx/demodils/ilsdemodgui.ui new file mode 100644 index 000000000..358f12356 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodgui.ui @@ -0,0 +1,1790 @@ + + + ILSDemodGUI + + + + 0 + 0 + 417 + 1145 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + ILS Demodulator + + + + + 0 + 0 + 390 + 221 + + + + + 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 + + + + + + + + + Left: Mute/Unmute audio Right: view/select audio device + + + ... + + + + :/sound_on.png + :/sound_off.png:/sound_on.png + + + true + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + Mode + + + + + + + Whether to receive localizer or glideslope signal + + + + LOC + + + + + G/S + + + + + + + + Qt::Vertical + + + + + + + + 66 + 0 + + + + Frequency + + + + + + + + 70 + 0 + + + + ILS frequency in MHz + + + -1 + + + + + + + MHz + + + + + + + Qt::Vertical + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 2300 + + + 25000 + + + 1 + + + 2300 + + + Qt::Horizontal + + + + + + + + 40 + 0 + + + + 500Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + Ident + + + + + + + ILS Identifier + + + true + + + + + + + Qt::Vertical + + + + + + + Lat + + + + + + + Latitude of runway threshold in decimal degrees. North positive + + + + + + + ° + + + + + + + Qt::Vertical + + + + + + + Lon + + + + + + + Longitude of runway threshold in decimal degrees. East positive + + + + + + + ° + + + + + + + Qt::Vertical + + + + + + + Ele + + + + + + + Runway threshold elevation in feet + + + 20000 + + + + + + + ft + + + + + + + Find runway on map + + + + + + + :/gridpolar.png:/gridpolar.png + + + + + + + + + Qt::Horizontal + + + + + + + + + Rwy + + + + + + + Airport ICAO and runway + + + + + + + Qt::Vertical + + + + + + + Bearing + + + + + + + Runway true bearing (Course + magnetic deviation) + + + 360.000000000000000 + + + 0.010000000000000 + + + + + + + °T + + + + + + + Qt::Vertical + + + + + + + Slope + + + + + + + Slope of runway in degrees. Positive indicates runway slopes up from threshold. + + + -20.000000000000000 + + + 20.000000000000000 + + + 0.100000000000000 + + + + + + + % + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + GPA + + + + + + + Glide path angle in degrees + + + 1 + + + 1.000000000000000 + + + 20.000000000000000 + + + 3.000000000000000 + + + + + + + ° + + + + + + + Qt::Vertical + + + + + + + RD Height + + + + + + + ILS Reference Datum Height (RDH) above runway threshold + + + 1 + + + 100.000000000000000 + + + + + + + m + + + + + + + Qt::Vertical + + + + + + + Course Width + + + + + + + Course width in degrees + + + 1 + + + 2.000000000000000 + + + 6.000000000000000 + + + + + + + ° + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Send DDM via UDP + + + Qt::RightToLeft + + + UDP + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 9998 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + + 24 + 16777215 + + + + Enable averaging of measurements + + + AVG + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + MT + + + + + + + + 24 + 24 + + + + Morse code ident threshold (Linear SNR) + + + 100 + + + 1 + + + + + + + 2.0 + + + + + + + Qt::Vertical + + + + + + + Vol + + + + + + + + 24 + 24 + + + + Audio volume + + + 100 + + + 1 + + + + + + + + 25 + 0 + + + + 10.0 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Sq + + + + + + + + 24 + 24 + + + + Audio squelch threshold (dB) + + + -100 + + + 0 + + + 1 + + + -40 + + + + + + + + 40 + 0 + + + + -100dB + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 24 + 16777215 + + + + Start/stop logging of data to .csv file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Add marker to map at current position + + + + + + + :/create.png:/create.png + + + + + + + Clear markers from map + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 20 + 240 + 371 + 268 + + + + + 0 + 0 + + + + ILS Data + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 2 + + + 2 + + + 2 + + + 2 + + + 3 + + + + + DDM + + + + + + + Power<sub>Carrier</sub> + + + + + + + <html><head/><body><p>DM<span style=" vertical-align:sub;">150Hz</span></p></body></html> + + + + + + + + 60 + 16777215 + + + + Specifies units to display DDM in + + + + FS + + + + + % + + + + + uA + + + + + + + + + 50 + 16777215 + + + + Angle of deviation from ILS course line in degrees + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + + 50 + 16777215 + + + + Difference in depth of modulation + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + <html><head/><body><p>Deviation<span style=" vertical-align:sub;">ILS</span></p></body></html> + + + + + + + 150Hz depth of modulation + + + 0 + + + + + + + <html><head/><body><p>DM<span style=" vertical-align:sub;">90Hz</span></p></body></html> + + + + + + + + 50 + 16777215 + + + + 90Hz power in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + + 50 + 16777215 + + + + 150Hz power in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + + 50 + 16777215 + + + + Angle of deviation from ILS course line in degrees calculated from GPS position + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + dB + + + + + + + SDM + + + + + + + <html><head/><body><p>Deviation<span style=" vertical-align:sub;">GPS</span></p></body></html> + + + + + + + 90Hz depth of modulation + + + 0 + + + + + + + Power<sub>90Hz</sub> + + + + + + + + + ° + + + + + + + + 100 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 50 + 16777215 + + + + Carrier power in dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + + + + Power<sub>150Hz</sub> + + + + + + + dB + + + + + + + + + ° + + + + + + + + 100 + 16777215 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + dB + + + + + + + Sum of depth of modulation. Nominally 40% for LOC and 80% for G/S + + + 0 + + + + + + + + + + + + + 0 + 0 + + + + + 100 + 100 + + + + Course Deviation Indicator (CDI) + + + + + + + 0 + + + + + Received Morse code identifier + + + + + + Qt::AlignCenter + + + + + + + + + + + + + 10 + 500 + 400 + 284 + + + + + 0 + 0 + + + + + 400 + 0 + + + + Demodulated Spectrum + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + 10 + 800 + 716 + 341 + + + + + 714 + 0 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Real + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Demod + + + + + + + + + 0 + 0 + + + + Imag + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Demod + + + + + + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + GLSpectrum + QWidget +
gui/glspectrum.h
+ 1 +
+ + GLSpectrumGUI + QWidget +
gui/glspectrumgui.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + GLScope + QWidget +
gui/glscope.h
+ 1 +
+ + GLScopeGUI + QWidget +
gui/glscopegui.h
+ 1 +
+ + CourseDeviationIndicator + QWidget +
gui/coursedeviationindicator.h
+
+
+ + deltaFrequency + audioMute + mode + frequency + rfBW + ident + latitude + longitude + elevation + findOnMap + runway + trueBearing + slope + glidePath + height + courseWidth + udpEnabled + average + thresh + volume + squelch + logEnable + logFilename + addMarker + clearMarkers + pCarrier + p90 + p150 + ddm + ddmUnits + angle + angleDirection + gpsAngle + gpsAngleDirection + channel1 + channel2 + + + + + +
diff --git a/plugins/channelrx/demodils/ilsdemodplugin.cpp b/plugins/channelrx/demodils/ilsdemodplugin.cpp new file mode 100644 index 000000000..2951c89c4 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodplugin.cpp @@ -0,0 +1,93 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ilsdemodgui.h" +#endif +#include "ilsdemod.h" +#include "ilsdemodwebapiadapter.h" +#include "ilsdemodplugin.h" + +const PluginDescriptor ILSDemodPlugin::m_pluginDescriptor = { + ILSDemod::m_channelId, + QStringLiteral("ILS Demodulator"), + QStringLiteral("7.12.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +ILSDemodPlugin::ILSDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& ILSDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void ILSDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(ILSDemod::m_channelIdURI, ILSDemod::m_channelId, this); +} + +void ILSDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + ILSDemod *instance = new ILSDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* ILSDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* ILSDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return ILSDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* ILSDemodPlugin::createChannelWebAPIAdapter() const +{ + return new ILSDemodWebAPIAdapter(); +} + diff --git a/plugins/channelrx/demodils/ilsdemodplugin.h b/plugins/channelrx/demodils/ilsdemodplugin.h new file mode 100644 index 000000000..c6ab9c90c --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodplugin.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMODPLUGIN_H +#define INCLUDE_ILSDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class ILSDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.ilsdemod") + +public: + explicit ILSDemodPlugin(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_ILSDEMODPLUGIN_H + diff --git a/plugins/channelrx/demodils/ilsdemodsettings.cpp b/plugins/channelrx/demodils/ilsdemodsettings.cpp new file mode 100644 index 000000000..cdfb9f375 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodsettings.cpp @@ -0,0 +1,236 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ilsdemodsettings.h" + +ILSDemodSettings::ILSDemodSettings() : + m_channelMarker(nullptr), + m_spectrumGUI(nullptr), + m_scopeGUI(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void ILSDemodSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 15000.0f; // 15k to support offset carrier + m_mode = LOC; + m_frequencyIndex = 0; + m_squelch = -60.0; + m_volume = 2.0; + m_audioMute = false; + m_average = false; + m_ddmUnits = FULL_SCALE; + m_identThreshold = 4.0f; + m_ident = ""; + m_runway = ""; + m_trueBearing = 0.0f; + m_slope = 0.0f; + m_latitude = ""; + m_longitude = ""; + m_elevation = 0; + m_glidePath = 3.0f; + m_refHeight = 15.25; + m_courseWidth = 4.0f; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_logFilename = "ils_log.csv"; + m_logEnabled = false; + m_scopeCh1 = 0; + m_scopeCh2 = 1; + + m_rgbColor = QColor(0, 205, 200).rgb(); + m_title = "ILS Demodulator"; + m_audioDeviceName = AudioDeviceManager::m_defaultDeviceName; + 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 ILSDemodSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_inputFrequencyOffset); + s.writeFloat(2, m_rfBandwidth); + s.writeS32(3, (int) m_mode); + s.writeS32(4, m_frequencyIndex); + s.writeS32(5, m_squelch); + s.writeFloat(6, m_volume); + s.writeBool(7, m_audioMute); + s.writeBool(8, m_average); + s.writeS32(9, (int) m_ddmUnits); + s.writeFloat(10, m_identThreshold); + s.writeString(11, m_ident); + s.writeString(12, m_runway); + s.writeFloat(13, m_trueBearing); + s.writeFloat(14, m_slope); + s.writeString(15, m_latitude); + s.writeString(16, m_longitude); + s.writeS32(17, m_elevation); + s.writeFloat(18, m_glidePath); + s.writeFloat(19, m_refHeight); + s.writeFloat(20, m_courseWidth); + s.writeBool(21, m_udpEnabled); + s.writeString(22, m_udpAddress); + s.writeU32(23, m_udpPort); + s.writeString(24, m_logFilename); + s.writeBool(25, m_logEnabled); + s.writeS32(26, m_scopeCh1); + s.writeS32(27, m_scopeCh2); + + s.writeU32(40, m_rgbColor); + s.writeString(41, m_title); + if (m_channelMarker) { + s.writeBlob(42, m_channelMarker->serialize()); + } + s.writeString(43, m_audioDeviceName); + s.writeS32(44, m_streamIndex); + s.writeBool(45, m_useReverseAPI); + s.writeString(46, m_reverseAPIAddress); + s.writeU32(47, m_reverseAPIPort); + s.writeU32(48, m_reverseAPIDeviceIndex); + s.writeU32(49, m_reverseAPIChannelIndex); + if (m_spectrumGUI) { + s.writeBlob(50, m_spectrumGUI->serialize()); + } + if (m_scopeGUI) { + s.writeBlob(51, m_scopeGUI->serialize()); + } + if (m_rollupState) { + s.writeBlob(52, m_rollupState->serialize()); + } + s.writeS32(53, m_workspaceIndex); + s.writeBlob(54, m_geometryBytes); + s.writeBool(55, m_hidden); + + return s.final(); +} + +bool ILSDemodSettings::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, 15000.0f); + d.readS32(3, (int *) &m_mode, (int) LOC); + d.readS32(4, &m_frequencyIndex, 0); + d.readS32(5, &m_squelch, -40); + d.readFloat(6, &m_volume, 2.0f); + d.readBool(7, &m_audioMute, false); + d.readBool(8, &m_average, false); + d.readS32(9, (int *) &m_ddmUnits, (int) FULL_SCALE); + d.readFloat(10, &m_identThreshold, 4.0f); + d.readString(11, &m_ident, ""); + d.readString(12, &m_runway, ""); + d.readFloat(13, &m_trueBearing, 0.0f); + d.readFloat(14, &m_slope, 0.0f); + d.readString(15, &m_latitude, ""); + d.readString(16, &m_longitude, ""); + d.readS32(17, &m_elevation, 0); + d.readFloat(18, &m_glidePath, 30.f); + d.readFloat(19, &m_refHeight, 15.25f); + d.readFloat(20, &m_courseWidth, 4.0f); + + d.readBool(21, &m_udpEnabled); + d.readString(22, &m_udpAddress); + d.readU32(23, &utmp); + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + d.readString(24, &m_logFilename, "ils_log.csv"); + d.readBool(25, &m_logEnabled, false); + d.readS32(26, &m_scopeCh1, 0); + d.readS32(27, &m_scopeCh2, 0); + + d.readU32(40, &m_rgbColor, QColor(0, 205, 200).rgb()); + d.readString(41, &m_title, "ILS Demodulator"); + if (m_channelMarker) + { + d.readBlob(42, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + d.readString(43, &m_audioDeviceName, AudioDeviceManager::m_defaultDeviceName); + d.readS32(44, &m_streamIndex, 0); + d.readBool(45, &m_useReverseAPI, false); + d.readString(46, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(47, &utmp, 0); + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + d.readU32(48, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(49, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + if (m_spectrumGUI) + { + d.readBlob(50, &bytetmp); + m_spectrumGUI->deserialize(bytetmp); + } + if (m_scopeGUI) + { + d.readBlob(51, &bytetmp); + m_scopeGUI->deserialize(bytetmp); + } + if (m_rollupState) + { + d.readBlob(52, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + d.readS32(28, &m_workspaceIndex, 0); + d.readBlob(29, &m_geometryBytes); + d.readBool(30, &m_hidden, false); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + diff --git a/plugins/channelrx/demodils/ilsdemodsettings.h b/plugins/channelrx/demodils/ilsdemodsettings.h new file mode 100644 index 000000000..552c1fc78 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodsettings.h @@ -0,0 +1,100 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMODSETTINGS_H +#define INCLUDE_ILSDEMODSETTINGS_H + +#include + +#include "util/baudot.h" + +class Serializable; + +struct ILSDemodSettings +{ + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; + enum Mode { + LOC, + GS + } m_mode; + int m_frequencyIndex; + int m_squelch; + Real m_volume; + bool m_audioMute; + bool m_average; + enum DDMUnits { + FULL_SCALE, + PERCENT, + MICROAMPS + } m_ddmUnits; + float m_identThreshold; //!< Linear SNR threshold for Morse demodulator + + QString m_ident; + QString m_runway; + float m_trueBearing; + float m_slope; // In % + QString m_latitude; // Of threshold. String, so can support multiple formats + QString m_longitude; + int m_elevation; // Of threshold in feet + float m_glidePath; // In degrees + float m_refHeight; // In metres + float m_courseWidth; // In degrees + + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + QString m_logFilename; + bool m_logEnabled; + int m_scopeCh1; + int m_scopeCh2; + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + QString m_audioDeviceName; + 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_spectrumGUI; + Serializable *m_scopeGUI; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + static const int ILSDEMOD_CHANNEL_SAMPLE_RATE = 20480; // 2560*8 - Ident is at 1020. Voice 300/3k. SR chosen so 90/150Hz in middle of bin + 20k b/w for offset-carrier + static const int ILSDEMOD_SPECTRUM_DECIM_LOG2 = 5; + static const int ILSDEMOD_SPECTRUM_SAMPLE_RATE = ILSDEMOD_CHANNEL_SAMPLE_RATE / (1 << ILSDEMOD_SPECTRUM_DECIM_LOG2); + + ILSDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void setSpectrumGUI(Serializable *spectrumGUI) { m_spectrumGUI = spectrumGUI; } + void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_ILSDEMODSETTINGS_H */ + diff --git a/plugins/channelrx/demodils/ilsdemodsink.cpp b/plugins/channelrx/demodils/ilsdemodsink.cpp new file mode 100644 index 000000000..cbb9b629d --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodsink.cpp @@ -0,0 +1,473 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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/scopevis.h" +#include "util/stepfunctions.h" +#include "util/db.h" +#include "util/morse.h" +#include "util/units.h" +#include "maincore.h" + +#include "ilsdemod.h" +#include "ilsdemodsink.h" + +ILSDemodSink::ILSDemodSink(ILSDemod *ilsDemod) : + m_spectrumSink(nullptr), + m_scopeSink(nullptr), + m_ilsDemod(ilsDemod), + m_channel(nullptr), + m_channelSampleRate(ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_audioSampleRate(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_fftSequence(-1), + m_fft(nullptr), + m_fftCounter(0), + m_squelchLevel(0.001f), + m_squelchCount(0), + m_squelchOpen(false), + m_squelchDelayLine(9600), + m_volumeAGC(0.003), + m_audioFifo(48000), + m_sampleBufferIndex(0) +{ + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + + m_magsq = 0.0; + + m_sampleBuffer.resize(m_sampleBufferSize); + m_spectrumSampleBuffer.resize(m_sampleBufferSize); + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); + + FFTFactory *fftFactory = DSPEngine::instance()->getFFTFactory(); + if (m_fftSequence >= 0) { + fftFactory->releaseEngine(m_fftSize, false, m_fftSequence); + } + m_fftSequence = fftFactory->getEngine(m_fftSize, false, &m_fft); + m_fftCounter = 0; + m_fftWindow.create(FFTWindow::Flattop, m_fftSize); +} + +ILSDemodSink::~ILSDemodSink() +{ +} + +void ILSDemodSink::sampleToScope(Complex sample, Real demod) +{ + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + m_sampleBuffer[m_sampleBufferIndex] = Sample(r, i); + m_spectrumSampleBuffer[m_sampleBufferIndex] = Sample(demod * SDR_RX_SCALEF, 0); + m_sampleBufferIndex++; + + if (m_sampleBufferIndex == m_sampleBufferSize) + { + if (m_scopeSink) + { + std::vector vbegin; + vbegin.push_back(m_sampleBuffer.begin()); + m_scopeSink->feed(vbegin, m_sampleBufferSize); + } + if (m_spectrumSink) + { + m_spectrumSink->feed(m_spectrumSampleBuffer.begin(), m_spectrumSampleBuffer.end(), false); + } + m_sampleBufferIndex = 0; + } +} + +void ILSDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + 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 ILSDemodSink::processOneSample(Complex &ci) +{ + Complex ca; + + // Calculate average and peak levels for level meter + double magsqRaw = ci.real()*ci.real() + ci.imag()*ci.imag();; + Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + m_magsqCount++; + + ci /= SDR_RX_SCALEF; + + // AM demodulation + Complex demod = std::abs(ci); + + // Resample as audio + if (m_audioInterpolatorDistance < 1.0f) // interpolate + { + while (!m_audioInterpolator.interpolate(&m_audioInterpolatorDistanceRemain, demod, &ca)) + { + processOneAudioSample(ca); + m_audioInterpolatorDistanceRemain += m_audioInterpolatorDistance; + } + } + else // decimate + { + if (m_audioInterpolator.decimate(&m_audioInterpolatorDistanceRemain, demod, &ca)) + { + processOneAudioSample(ca); + m_audioInterpolatorDistanceRemain += m_audioInterpolatorDistance; + } + } + + // Decimate again for spectral analysis + Complex demodDecim; + if (m_decimator.decimate(demod, demodDecim)) + { + + // Use FFT to calculate sidebands modulation depth + m_fft->in()[m_fftCounter] = demodDecim; + m_fftCounter++; + if (m_fftCounter == m_fftSize) + { + calcDDM(); + m_fftCounter = 0; + + // Send results to GUI + if (getMessageQueueToChannel()) + { + Real modDepth90, modDepth150, sdm, ddm; + if (m_settings.m_average) + { + modDepth90 = m_modDepth90Average.instantAverage(); + modDepth150 = m_modDepth150Average.instantAverage(); + sdm = m_sdmAverage.instantAverage(); + ddm = m_ddmAverage.instantAverage(); + } + else + { + modDepth90 = m_modDepth90; + modDepth150 = m_modDepth150; + sdm = m_sdm; + ddm = m_ddm; + } + + Real angle; + if (m_settings.m_mode == ILSDemodSettings::LOC) + { + // For localiser, angle depends on runway length + // At ILS datum (over threshold) (or ILS point B for short runways (<=1200m), which is 1050m from threshold) + // the displacement sensitivity is 0.00145 DDM/metre (3.1.3.7) + // The points at which DDM is 0.155 (i.e a displacement of 0.155/0.00154=~105m) define the course sector (3.1.3.7.3 Note 1) + // And this must be <= 6 degrees (typically between 3-6degrees) (3.1.3.7.1) + // Localilzer to threshold distances (geometric angle) + // EGKK 3150m (3.8deg), EGKB 1840m (6.5deg), EGLL 3960m (3.0deg), EGLC 1570m(27) 1510m(09) (7.6/8deg) EGJJ 1710m (7deg) + // FAS data for EGJJ https://nats-uk.ead-it.com/cms-nats/export/sites/default/en/Publications/AIP/Current-AIRAC/graphics/196515.pdf + // LTP (Landing threshold point) 491231.8010N 0021105.6645W = 49.20883361 -2.18490681 + // FPAP 491224.8745N 0021228.7365W = 49.20690958 -2.20798236 + // Length offset 136m (distance from near threshold??) + // LTP-FPAP=1690m D=1690+305=1995 (GARP is 305m/1000ft from FPAP) + // EGJJ angle for 1995m = 6deg + angle = ddm / 0.155f * (m_settings.m_courseWidth / 2.0f); + } + else + { + // For glide slope, sector is 0.175 DDM = 0.7 degrees + // Displacement sensitivity 0.0875 at 0.12*theta (0.12*3=0.36deg) (3.1.5.6.2) + // GP coverage is from 0.45*theta to 1.75*theta (5.25-1.35=4.9deg for 3deg GP) + angle = 0.12f * m_settings.m_glidePath * ddm / 0.0875f; + } + + ILSDemod::MsgAngleEstimate *msg = ILSDemod::MsgAngleEstimate::create(m_powerCarrier, m_power90, m_power150, modDepth90, modDepth150, sdm, ddm, angle); + getMessageQueueToChannel()->push(msg); + } + } + + // Select signals to feed to scope + Complex scopeSample; + switch (m_settings.m_scopeCh1) + { + case 0: + scopeSample.real(ci.real()); + break; + case 1: + scopeSample.real(ci.imag()); + break; + case 2: + scopeSample.real(demod.real()); + break; + } + switch (m_settings.m_scopeCh2) + { + case 0: + scopeSample.imag(ci.real()); + break; + case 1: + scopeSample.imag(ci.imag()); + break; + case 2: + scopeSample.imag(demod.real()); + break; + } + sampleToScope(scopeSample, demod.real()); + } + +} + +void ILSDemodSink::processOneAudioSample(Complex &ci) +{ + Real re = ci.real(); + Real im = ci.imag(); + Real magsq = re*re + im*im; + m_audioMovingAverage(magsq); + double magsqAvg = m_movingAverage.asDouble(); + + m_squelchDelayLine.write(magsq); + + if (magsqAvg < m_squelchLevel) + { + if (m_squelchCount > 0) { + m_squelchCount--; + } + } + else + { + if (m_squelchCount < (unsigned int)m_audioSampleRate / 10) { + m_squelchCount++; + } + } + + qint16 sample; + + m_squelchOpen = (m_squelchCount >= (unsigned int)m_audioSampleRate / 20); + + if (m_squelchOpen && !m_settings.m_audioMute) + { + Real demod; + + { + demod = sqrt(m_squelchDelayLine.readBack(m_audioSampleRate/20)); + m_volumeAGC.feed(demod); + demod = (demod - m_volumeAGC.getValue()) / m_volumeAGC.getValue(); + } + + demod = m_bandpass.filter(demod); + + Real attack = (m_squelchCount - 0.05f * m_audioSampleRate) / (0.05f * m_audioSampleRate); + sample = demod * StepFunctions::smootherstep(attack) * (m_audioSampleRate/24) * m_settings.m_volume; + } + else + { + sample = 0; + } + + m_audioBuffer[m_audioBufferFill].l = sample; + m_audioBuffer[m_audioBufferFill].r = sample; + ++m_audioBufferFill; + + if (m_audioBufferFill >= m_audioBuffer.size()) + { + uint res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + + if (res != m_audioBufferFill) + { + qDebug("ILSDemodSink::processOneAudioSample: %u/%u audio samples written", res, m_audioBufferFill); + m_audioFifo.clear(); + } + + m_audioBufferFill = 0; + } + + m_morseDemod.processOneSample(ci); +} + +Real ILSDemodSink::magSq(int bin) const +{ + Complex c = m_fft->out()[bin]; + Real v = c.real() * c.real() + c.imag() * c.imag(); + Real magsq = v / (m_fftSize * m_fftSize); + return magsq; +} + +// Calculate the difference in the depth of modulation (DDM) +void ILSDemodSink::calcDDM() +{ + // 3.1.3.5.3 - the modulating tones shall be 90 Hz and 150 Hz within plus or minus 2.5 per cent + // At 88/92Hz, some energy is lost in adjacent bin, so we use flat top windowing for accurate + // amplitude measurement, which is what is needed for calculating depth of modulation + m_fftWindow.apply(m_fft->in()); + + // Perform FFT + m_fft->transform(); + + // Convert bin to frequency offset + double frequencyResolution = ILSDemodSettings::ILSDEMOD_SPECTRUM_SAMPLE_RATE / (double)m_fftSize; + int bin90 = 90.0 / frequencyResolution; + int bin150 = 150.0 / frequencyResolution; + + double mag90, mag150; + double magSqCarrier = magSq(0); + double magCarrier = sqrt(magSqCarrier); + + // Add both sidebands + mag90 = sqrt(magSq(bin90)) + sqrt(magSq(m_fftSize-bin90)); + mag150 = sqrt(magSq(bin150)) + sqrt(magSq(m_fftSize-bin150)); + + // Calculate power in dB + m_powerCarrier = CalcDb::dbPower(magSqCarrier); + m_power90 = CalcDb::dbPower(mag90 * mag90); + m_power150 = CalcDb::dbPower(mag150 * mag150); + + // Calculate modulation depth as % of carrier + m_modDepth90 = mag90 / magCarrier * 100.0; + m_modDepth150 = mag150 / magCarrier * 100.0; + + // Calculate modulation depth difference (https://www.youtube.com/watch?v=71iww_ERoYc) + m_ddm = (m_modDepth90 - m_modDepth150) / 100.0; + + // Calculate sum of difference of modulation + m_sdm = (m_modDepth90 + m_modDepth150) / 100.0; + + // Calculate moving averages + m_modDepth90Average(m_modDepth90); + m_modDepth150Average(m_modDepth150); + m_sdmAverage(m_sdm); + m_ddmAverage(m_ddm); +} + +void ILSDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "ILSDemodSink::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) ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; + +} + +void ILSDemodSink::applySettings(const ILSDemodSettings& settings, bool force) +{ + qDebug() << "ILSDemodSink::applySettings:" + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_volume: " << settings.m_volume + << " m_squelch: " << settings.m_squelch + << " m_audioMute: " << settings.m_audioMute + << " m_audioDeviceName: " << settings.m_audioDeviceName + << " force: " << force; + + if ((m_settings.m_squelch != settings.m_squelch) || force) { + m_squelchLevel = CalcDb::powerFromdB(settings.m_squelch); + } + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) + { + m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + if ((settings.m_identThreshold != m_settings.m_identThreshold) || force) { + m_morseDemod.applySettings(settings.m_identThreshold); + } + + if (force) + { + m_modDepth90Average.reset(); + m_modDepth150Average.reset(); + m_ddmAverage.reset(); + m_decimator.setLog2Decim(ILSDemodSettings::ILSDEMOD_SPECTRUM_DECIM_LOG2); + } + + m_settings = settings; +} + +void ILSDemodSink::applyAudioSampleRate(int sampleRate) +{ + if (sampleRate < 0) + { + qWarning("ILSDemodSink::applyAudioSampleRate: invalid sample rate: %d", sampleRate); + return; + } + + qDebug("ILSDemodSink::applyAudioSampleRate: sampleRate: %d channelSampleRate: %d", sampleRate, ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE); + + if (sampleRate != m_audioSampleRate) + { + m_audioInterpolator.create(16, ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE, 3500.0f); + m_audioInterpolatorDistanceRemain = 0; + m_audioInterpolatorDistance = (Real) ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE / (Real) sampleRate; + m_bandpass.create(301, sampleRate, 300.0f, 3000.0f); + //m_bandpass.printTaps("audio_bpf"); + m_audioFifo.setSize(sampleRate); + m_squelchDelayLine.resize(sampleRate/5); + + m_volumeAGC.resizeNew(sampleRate/10, 0.003f); + m_morseDemod.applyChannelSettings(sampleRate); + } + + m_audioSampleRate = sampleRate; +} + diff --git a/plugins/channelrx/demodils/ilsdemodsink.h b/plugins/channelrx/demodils/ilsdemodsink.h new file mode 100644 index 000000000..4b4e5973c --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodsink.h @@ -0,0 +1,170 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMODSINK_H +#define INCLUDE_ILSDEMODSINK_H + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/agc.h" +#include "dsp/firfilter.h" +#include "dsp/fftfactory.h" +#include "dsp/fftengine.h" +#include "dsp/fftwindow.h" +#include "dsp/decimatorc.h" +#include "dsp/morsedemod.h" +#include "audio/audiofifo.h" +#include "util/movingaverage.h" +#include "util/movingmaximum.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" + +#include "ilsdemodsettings.h" + +class ChannelAPI; +class ILSDemod; +class ScopeVis; +class SpectrumVis; + +class ILSDemodSink : public ChannelSampleSink { +public: + ILSDemodSink(ILSDemod *packetDemod); + ~ILSDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void setSpectrumSink(SpectrumVis* spectrumSink) { m_spectrumSink = spectrumSink; } + void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; } + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const ILSDemodSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; m_morseDemod.setMessageQueueToChannel(messageQueue); } + void setChannel(ChannelAPI *channel) { m_channel = channel; } + void applyAudioSampleRate(int sampleRate); + + int getAudioSampleRate() const { return m_audioSampleRate; } + bool getSquelchOpen() const { return m_squelchOpen; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + void setAudioFifoLabel(const QString& label) { m_audioFifo.setLabel(label); } + + 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.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + SpectrumVis* m_spectrumSink; + ScopeVis* m_scopeSink; // Scope GUI to display baseband waveform + ILSDemod *m_ilsDemod; + ILSDemodSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_audioSampleRate; + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + + MessageQueue *m_messageQueueToChannel; + + MovingAverageUtil m_movingAverage; + MovingAverageUtil m_audioMovingAverage; + + DecimatorC m_decimator; + + int m_fftSequence; + FFTEngine *m_fft; + int m_fftCounter; + FFTWindow m_fftWindow; + static const int m_fftSize = 256; // 2.5Hz res (so 90/150Hz are centered in bins - and FT isn't too wide) + Real m_powerCarrier; + Real m_power90; + Real m_power150; + Real m_modDepth90; + Real m_modDepth150; + Real m_sdm; + Real m_ddm; + MovingAverageUtil m_modDepth90Average; // ~0.5 sec + MovingAverageUtil m_modDepth150Average; + MovingAverageUtil m_sdmAverage; + MovingAverageUtil m_ddmAverage; + + Real m_squelchLevel; + uint32_t m_squelchCount; + bool m_squelchOpen; + DoubleBufferFIFO m_squelchDelayLine; + SimpleAGC<4800> m_volumeAGC; + Bandpass m_bandpass; + Interpolator m_audioInterpolator; + Real m_audioInterpolatorDistance; + Real m_audioInterpolatorDistanceRemain; + AudioVector m_audioBuffer; + AudioFifo m_audioFifo; + uint32_t m_audioBufferFill; + + SampleVector m_sampleBuffer; + static const int m_sampleBufferSize = ILSDemodSettings::ILSDEMOD_CHANNEL_SAMPLE_RATE / 20; + int m_sampleBufferIndex; + SampleVector m_spectrumSampleBuffer; + + MorseDemod m_morseDemod; + + void processOneSample(Complex &ci); + void processOneAudioSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void sampleToScope(Complex sample, Real demod); + void calcDDM(); + Real magSq(int bin) const; +}; + +#endif // INCLUDE_ILSDEMODSINK_H + diff --git a/plugins/channelrx/demodils/ilsdemodwebapiadapter.cpp b/plugins/channelrx/demodils/ilsdemodwebapiadapter.cpp new file mode 100644 index 000000000..67992c9df --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "ilsdemod.h" +#include "ilsdemodwebapiadapter.h" + +ILSDemodWebAPIAdapter::ILSDemodWebAPIAdapter() +{} + +ILSDemodWebAPIAdapter::~ILSDemodWebAPIAdapter() +{} + +int ILSDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIlsDemodSettings(new SWGSDRangel::SWGILSDemodSettings()); + response.getIlsDemodSettings()->init(); + ILSDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int ILSDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + ILSDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodils/ilsdemodwebapiadapter.h b/plugins/channelrx/demodils/ilsdemodwebapiadapter.h new file mode 100644 index 000000000..fd1ad9bd6 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_ILSDEMOD_WEBAPIADAPTER_H +#define INCLUDE_ILSDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "ilsdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class ILSDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + ILSDemodWebAPIAdapter(); + virtual ~ILSDemodWebAPIAdapter(); + + 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: + ILSDemodSettings m_settings; +}; + +#endif // INCLUDE_ILSDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodils/readme.md b/plugins/channelrx/demodils/readme.md new file mode 100644 index 000000000..8f0c8d77f --- /dev/null +++ b/plugins/channelrx/demodils/readme.md @@ -0,0 +1,247 @@ +

ILS Demodulator Plugin

+ +

Introduction

+ +This plugin can be used to demodulate ILS (Instrument Landing System) signals. These are the signals +used by aircraft to perform precision approaches and auto-lands. Details of the demodulated signal are displayed, +such as the DDM (Difference in Depth of Modulation), as well as a visual representation of course line & glide path deviation on a +CDI (Course Deviation Indicator), similar to that used in aircraft. + +The ILS localizer course and glide path can be displayed in 3D on the [Map](../../feature/map/readme.md) feature. + +![ILS Demodulator on Map](../../../doc/img/ILSDemod_plugin_map.png) + +Two independent signals are transmitted as part of ILS on different frequencies: The localizer (LOC) signal (at 108-112MHz) that gives guidance in the horizontal plane +and the glide slope (G/S) signal (at 329-335MHz) that gives guidance in the vertical plane. + +Each signal contains 90Hz and 150Hz tones. A phased antenna array is used so that the relative strength of the tones to the carrier varies throughout space. +The tones will be equal (more specifically, the difference in depth of modulation (DDM) will be 0), along the localizer course line or glide path. +When approaching the localizer, the 90Hz tone will be stronger to the left and the 150Hz tone will be stronger to the right. Similarly, the 90Hz tone +will be stronger above the glide path, with the 150Hz being stronger below. + +As the LOC and G/S signals are so far apart in frequency, in order to receive both simultaneously, two SDRs, each with their own ILS Demodulator, are required. +If you only have one SDR, you can demodulate either signal independently. + +Note: The G/S could do with additional testing. If you are able to capture a G/S signal as a IQ .wav/sdriq file, please get in touch. + +

Interface

+ +The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md) + +![ILS Demodulator plugin GUI](../../../doc/img/ILSDemod_plugin.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: Mode

+ +Specifies whether the ILS Localizer (LOC) or Glide Slope (G/S) signal is to be demodulated. The localizer provides horizontal guidance and the glide slope vertical. + +

5: Frequency

+ +Specifies the ILS frequency. This will be in the range 108-112 MHz. When a frequency is selected, the device will be tuned to the corresponding frequency. +Localizers use the same frequency as listed on aviation charts for the ILS. +Glide slopes using a paired frequency in the range 329-335MHz range, which is typically not displayed on charts. + +

6: RF Bandwidth

+ +This specifies the bandwidth of a filter that is applied to the input signal to limit the RF bandwidth. +This should be set wide enough to contain the ILS and audio signal. +In some countries, offset carrier can be used, where the same signal is transmitted at multiple offsets. In this case, the +bandwidth should be set wide enough to cover all signals (E.g. ~16kHz). + +

7: Ident

+ +Specifies the identifer for the ILS. This is typically 3 or 4 characters. The drop-down contains a number of identifiers for ILSs at +airports within the South East of the UK. Selecting one of these will automatically fill in the other fields with details of the ILS. +The ILS identifier is broadcast as Morse code at an offset of 1020Hz from the ILS carrier. This is demodulated and displayed below the CDI. + +

8: Latitude

+ +Specifies the latitude of the runway threshold, in decimal degrees (North positive). + +

9: Longitude

+ +Specifies the longitude of the runway threshold, in decimal degrees (East positive). + +

10: Elevation

+ +Specifies the runway threshold elevation in feet. The correct elevation value may differ from the terrain height in the 3D map, depending on which terrain model is used (as set in the 3D map settings), so you may wish +to adjust this until the localizer is nicely displayed on the runway in the 3D map. + +

11: Runway

+ +Specifies the airport ICAO and runway name. (E.g. EGKK 08R). + +

12: Bearing

+ +Specifies the runway bearing in degrees true. This can be calculated from the runway course given on charts by adding the magnetic declination. + +

13: Slope

+ +Specifies the runway slope in %. + +

14: Glide Path Angle

+ +Specifies the glide path angle in degrees. For most ILS approaches, this is 3.0 degrees, but there are some exceptions, such as EGLC which is 5.5 degrees. + +

15: RD Height

+ +Specifies the ILS Reference Datum Height (RDH) above the runway threshold in metres. (Also known as the Threshold Crossing Height (TCH)). + +This is typically 15m (50ft) +/-3m (10ft), or 12m (40m) for short runways (<1200m). + +

16: Course Width

+ +Specifies the localizer course width in degrees. This is typically between 3 and 6 degrees, with shorter runways having wider course widths. + +

17: UDP

+ +When checked, the calculated DDM value is forwarded to the specified UDP address (18) and port (19) as a UTF-8 string. + +

18: UDP address

+ +IP address of the host to forward DDM data to via UDP. + +

19: UDP port

+ +UDP port number to forward DDM data to. + +

20: Average

+ +When checked, a moving average filter is applied to the ILS data. + +

21: MT - Morse Threshold

+ +This is the Morse code ident threshold, expressed as a linear signal to noise (SNR) ratio. This is effectively the signal level required for the Morse demodulator to detect a dot or dash. Setting this to low values will allow the Morse demodulator to detect weak signals, but it also increases the likelihood that noise will incorrectly be interpreted as a signal, resulting in invalid ident being reported. + +

22: Volume

+ +This is the volume of the audio signal from 0.0 (mute) to 10.0 (maximum). It can be varied continuously in 0.1 steps using the dial button. + +

23: Squelch Threshold

+ +This is the squelch threshold in dB. The average total power received in the signal bandwidth before demodulation is compared to this value and the squelch input is open above this value. It can be varied continuously in 0.1 dB steps from 0.0 to -100.0 dB using the dial button. + +

24: Start/stop Logging Data to .csv File

+ +When checked, writes demodulated data to the .csv file specified by (25). + +

25: .csv Log Filename

+ +Click to specify the name of the .csv file which data will be logged to. + +

26: Add Marker

+ +Adds a marker to the map at the current GPS position, displaying current ILS data. + +

27: Clear Markers

+ +Clears all markers from the map. + +

28: ILS Data

+ +The ILS data area shows details of the demodulated signal. + +

29: DM90Hz

+ +Displays the depth of modulation of the 90Hz tone as a percentage of the carrier. + +

30: DM150Hz

+ +Displays the depth of modulation of the 150Hz tone as percentage of the carrier. + +

31: SDM

+ +Displays the Sum of the Depth of Modulation of the 90 and 150Hz tones. For LOC, this should be 40%. For G/S it should be 80%. + +

32: DDM

+ +Displays the Difference in the Depth of Modulation of the 90 and 150Hz tones (DDM=(DM90Hz-DM150Hz)/100). When the difference is 0, +the aircraft (or receiving antenna) will be aligned on the course line or glide path. For LOC, a positive DDM indicates the aircraft +is to the left of the course line, or for G/S, above the glide path. + +

33: Deviation ILS

+ +Displays an estimate of the deviation angle based on the calculated DDM. Note that this angle may be very inaccurate for |DDM|>0.155, as outside of this value DDM is not linear with angle. + +

34: Deviation GPS

+ +Displays a deviation angle calculated from GPS position, to be used as a reference in a comparison with the deviation angle computed from the ILS signals (33). + +

35: CDI

+ +The Course Deviation Indicator plots course / glide path deviation in a way similar to that displayed in aircraft. +Full scale deviation is 2.5 degrees (centre to edge) for LOC and 0.35 degrees for G/S. +"LOC" will be displayed in green above the CDI when the localizer is captured (|DDM| < 0.175). +"G/S" will be displayed in green above the CDI when the glide slope is capture (|DDM| < 0.175). + +Pilots would fly towards the diamond. So if the diamond is left-of-center, then the aircraft should turn to the left. + +The decoded Morse code identifier will be displayed underneath the CDI in both Morse and letters. +If will be displayed in white if it matches the specified identifer (7) or red if not. + +

36: Demodulated Spectrum

+ +The spectrum displays the demodulated AM spectrum, which should show the carrier, 90Hz and 150Hz sidebands. + +

Setting up an ILS

+ +First, find the approach charts (plates) for the runway/airport, or AIP (Aeronautical Information Publication) with the ILS of interest: + +* UK - [NATS AIP](https://nats-uk.ead-it.com/cms-nats/opencms/en/Publications/AIP/) +* Europe - [EUROCONTROL AIP](https://www.ead.eurocontrol.int/cms-eadbasic/opencms/en/login/ead-basic/) +* USA - [FAA DTPP](https://www.faa.gov/air_traffic/flight_info/aeronav/digital_products/dtpp/) +* Flight simmers may have [Navigraph Charts](https://navigraph.com/) + +This will contain the ILS identifier (green box), that should be entered in (7) and the frequency (red box) (5). It should also specify the glide path angle (blue box), to be entered in (14). That is typically 3 degrees. +The airport ICAO (purple box) and runway (yellow box) can be entered in (11). + +![ILS approach chart](../../../doc/img/ILSDemod_plugin_chart.png) + +Next, we need to enter the latitude (8), longitude (9) and elevation (10) of the runway threshold. This is available on some charts (orange box), but not usually accurately enough to line up perfectly on the 3D map. +For this, it's best to use the 3D map, and git statu click while holding shift at the start of the threshold to set a marker, which will display the coordinates. + +![Runway threshold coordinates](../../../doc/img/ILSDemod_plugin_threshold.png) + +The runway bearing should then be set (12) in degrees true. This is the runway course + magnetic declination. The easiest way to set this is just to enter the runway course from the chart (brown box), then visually +adjust the setting until the centre of the localizer (the course line) lines up with the runway centre line markings. Likewise, if necessary, the runway slope (13) can be set visually if needed. + +![Localizer runway alignment](../../../doc/img/ILSDemod_plugin_alignment.png) + +The ILS Reference Datum Height (RDH) to be set in (15) can often be found in the AIP, and is typically 15m (50ft). + +The course width (16) is ocassionaly specified in the AIP. + +If not in the AIP, it may be possible to calculate it from an SBAS FAS Data Block if available: +* Calculate the distance between LTP (Landing Threshold Point) and FPAP (Fight Path Alignment Point) from +the coordinates and add 305m to calculate the distance, D, between LTP and GARP (GNSS Azimuth Reference Point). +* With W as the Course Width at the LTP in metres (which is typically 105m), +* Calculate course width angle as 2 * atan(W/D). + +Alternatively, the course width angle can also be estimated by measuring the distance D above as the distance from +the the threshold to the localizer antenna, using a tool such as Google Maps (Right click on the map at the threshold and click Measure Distance +then left click on the localizer antenna). + +![Threshold to localizer measurement](../../../doc/img/ILSDemod_plugin_thr_to_loc.png) + +If D is less than 2000m, the calculated angle will be greater than 6 degrees. In this case, 6 degrees should be used as the +course angle, as this is the specified maximum angle. + +It should be noted that the GARP and localizer antenna aren't always coincident. + +Finally, you can measure the GPS Deviation angle at the point at which DDM is 0.155, and then the course width is twice that. + +(Please feel free to send me your settings so I can add them to the builtin database.) + diff --git a/plugins/channelrx/demodvor/vordemod.cpp b/plugins/channelrx/demodvor/vordemod.cpp index 38388bc4b..571deda73 100644 --- a/plugins/channelrx/demodvor/vordemod.cpp +++ b/plugins/channelrx/demodvor/vordemod.cpp @@ -34,6 +34,7 @@ #include "dsp/dspengine.h" #include "dsp/dspcommands.h" +#include "dsp/morsedemod.h" #include "device/deviceapi.h" #include "feature/feature.h" #include "settings/serializable.h" @@ -215,14 +216,14 @@ bool VORDemod::handleMessage(const Message& cmd) return true; } - else if (VORDemodReport::MsgReportIdent::match(cmd)) + else if (MorseDemod::MsgReportIdent::match(cmd)) { - VORDemodReport::MsgReportIdent& report = (VORDemodReport::MsgReportIdent&) cmd; + MorseDemod::MsgReportIdent& report = (MorseDemod::MsgReportIdent&) cmd; m_morseIdent = report.getIdent(); if (m_guiMessageQueue) { - VORDemodReport::MsgReportIdent *msg = new VORDemodReport::MsgReportIdent(report); + MorseDemod::MsgReportIdent *msg = new MorseDemod::MsgReportIdent(report); m_guiMessageQueue->push(msg); } diff --git a/plugins/channelrx/demodvor/vordemodgui.cpp b/plugins/channelrx/demodvor/vordemodgui.cpp index f4c23927b..e11f6e609 100644 --- a/plugins/channelrx/demodvor/vordemodgui.cpp +++ b/plugins/channelrx/demodvor/vordemodgui.cpp @@ -136,9 +136,9 @@ bool VORDemodGUI::handleMessage(const Message& message) return true; } - else if (VORDemodReport::MsgReportIdent::match(message)) + else if (MorseDemod::MsgReportIdent::match(message)) { - VORDemodReport::MsgReportIdent& report = (VORDemodReport::MsgReportIdent&) message; + MorseDemod::MsgReportIdent& report = (MorseDemod::MsgReportIdent&) message; QString ident = report.getIdent(); QString identString = Morse::toString(ident); // Convert Morse to a string diff --git a/plugins/channelrx/demodvor/vordemodreport.cpp b/plugins/channelrx/demodvor/vordemodreport.cpp index 96fcbc7f6..48169e9a6 100644 --- a/plugins/channelrx/demodvor/vordemodreport.cpp +++ b/plugins/channelrx/demodvor/vordemodreport.cpp @@ -18,4 +18,4 @@ #include "vordemodreport.h" MESSAGE_CLASS_DEFINITION(VORDemodReport::MsgReportRadial, Message) -MESSAGE_CLASS_DEFINITION(VORDemodReport::MsgReportIdent, Message) + diff --git a/plugins/channelrx/demodvor/vordemodreport.h b/plugins/channelrx/demodvor/vordemodreport.h index 8a053620d..1d07468d3 100644 --- a/plugins/channelrx/demodvor/vordemodreport.h +++ b/plugins/channelrx/demodvor/vordemodreport.h @@ -53,27 +53,6 @@ public: } }; - class MsgReportIdent : public Message { - MESSAGE_CLASS_DECLARATION - - public: - QString getIdent() const { return m_ident; } - - static MsgReportIdent* create(QString ident) - { - return new MsgReportIdent(ident); - } - - private: - QString m_ident; - - MsgReportIdent(QString ident) : - Message(), - m_ident(ident) - { - } - }; - public: VORDemodReport() {} ~VORDemodReport() {} diff --git a/plugins/channelrx/demodvor/vordemodsettings.cpp b/plugins/channelrx/demodvor/vordemodsettings.cpp index 9c12911ce..2107e0974 100644 --- a/plugins/channelrx/demodvor/vordemodsettings.cpp +++ b/plugins/channelrx/demodvor/vordemodsettings.cpp @@ -62,6 +62,7 @@ QByteArray VORDemodSettings::serialize() const s.writeS32(3, m_streamIndex); s.writeS32(4, m_volume*10); s.writeS32(5, m_squelch); + s.writeBool(10, m_audioMute); if (m_channelMarker) { s.writeBlob(6, m_channelMarker->serialize()); @@ -114,6 +115,7 @@ bool VORDemodSettings::deserialize(const QByteArray& data) m_volume = tmp * 0.1; d.readS32(5, &tmp, -40); m_squelch = tmp; + d.readBool(10, &m_audioMute, false); if (m_channelMarker) { diff --git a/plugins/channelrx/demodvor/vordemodsink.cpp b/plugins/channelrx/demodvor/vordemodsink.cpp index a346bea85..18b9e2153 100644 --- a/plugins/channelrx/demodvor/vordemodsink.cpp +++ b/plugins/channelrx/demodvor/vordemodsink.cpp @@ -44,9 +44,6 @@ VORDemodSCSink::VORDemodSCSink() : m_volumeAGC(0.003), m_audioFifo(48000), m_refPrev(0.0f), - m_movingAverageIdent(5000), - m_prevBit(0), - m_bitTime(0), m_varGoertzel(30, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE), m_refGoertzel(30, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE) { @@ -249,104 +246,14 @@ void VORDemodSCSink::processOneSample(Complex &ci) else m_refGoertzel.filter(phi); - // Ident demod - // Filter to remove voice - Complex c1 = m_bandpassIdent.filter(magc); - // Remove ident sub-carrier offset - c1 *= m_ncoIdent.nextIQ(); - // Filter other signals - Complex c2 = std::abs(m_lowpassIdent.filter(c1)); + // Decode Morse ident + m_morseDemod.processOneSample(magc); +} - // Filter noise with moving average (moving average preserves edges) - m_movingAverageIdent(c2.real()); - Real mav = m_movingAverageIdent.asFloat(); - - // Caclulate noise floor - if (mav > m_identMaxs[m_binCnt]) - m_identMaxs[m_binCnt] = mav; - m_binSampleCnt++; - if (m_binSampleCnt >= m_samplesPerDot10wpm/4) - { - // Calc minimum of maximums - m_identNoise = 1.0f; - for (int i = 0; i < m_identBins; i++) - { - m_identNoise = std::min(m_identNoise, m_identMaxs[i]); - } - m_binSampleCnt = 0; - m_binCnt++; - if (m_binCnt == m_identBins) - m_binCnt = 0; - m_identMaxs[m_binCnt] = 0.0f; - - // Prevent divide by zero - if (m_identNoise == 0.0f) - m_identNoise = 1e-20f; - } - - // CW demod - int bit = (mav / m_identNoise) >= m_settings.m_identThreshold; - //m_stream << mav << "," << m_identNoise << "," << bit << "," << (mav / m_identNoise) << "\n"; - if ((m_prevBit == 0) && (bit == 1)) - { - if (m_bitTime > 7*m_samplesPerDot10wpm) - { - if (m_ident.trimmed().size() > 2) // Filter out noise that may appear as one or two characters - { - qDebug() << "VORDemodSCSink::processOneSample:" << m_ident << " " << Morse::toString(m_ident); - - if (getMessageQueueToChannel()) - { - VORDemodReport::MsgReportIdent *msg = VORDemodReport::MsgReportIdent::create(m_ident); - getMessageQueueToChannel()->push(msg); - } - } - m_ident = ""; - } - else if (m_bitTime > 2.5*m_samplesPerDot10wpm) - { - m_ident.append(" "); - } - m_bitTime = 0; - } - else if (bit == 1) - { - m_bitTime++; - } - else if ((m_prevBit == 1) && (bit == 0)) - { - if (m_bitTime > 2*m_samplesPerDot10wpm) - { - m_ident.append("-"); - } - else if (m_bitTime > 0.2*m_samplesPerDot10wpm) - { - m_ident.append("."); - } - m_bitTime = 0; - } - else - { - m_bitTime++; - if (m_bitTime > 10*m_samplesPerDot7wpm) - { - m_ident = m_ident.simplified(); - if (m_ident.trimmed().size() > 2) // Filter out noise that may appear as one or two characters - { - qDebug() << "VORDemodSCSink::processOneSample:" << m_ident << " " << Morse::toString(m_ident); - - if (getMessageQueueToChannel()) - { - VORDemodReport::MsgReportIdent *msg = VORDemodReport::MsgReportIdent::create(m_ident); - getMessageQueueToChannel()->push(msg); - } - - } - m_ident = ""; - m_bitTime = 0; - } - } - m_prevBit = bit; +void VORDemodSCSink::setMessageQueueToChannel(MessageQueue *messageQueue) +{ + m_messageQueueToChannel = messageQueue; + m_morseDemod.setMessageQueueToChannel(messageQueue); } void VORDemodSCSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) @@ -367,30 +274,10 @@ void VORDemodSCSink::applyChannelSettings(int channelSampleRate, int channelFreq m_interpolatorDistanceRemain = 0; m_interpolatorDistance = (Real) channelSampleRate / (Real) VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE; - m_samplesPerDot7wpm = VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE*60/(50*7); - m_samplesPerDot10wpm = VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE*60/(50*10); - - m_ncoIdent.setFreq(-1020, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE); // +-50Hz source offset allowed m_ncoRef.setFreq(-9960, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE); - m_bandpassIdent.create(1001, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE, 970.0f, 1070.0f); // Ident at 1020 - //m_bandpassIdent.printTaps("bpf"); - m_highpassIdent.create(1001, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE, 900.0f); - //m_highpassIdent.printTaps("hpf"); - //m_file.setFileName("morse.txt"); - //m_file.open(QIODevice::WriteOnly); - //m_stream.setDevice(&m_file); - - m_lowpassIdent.create(301, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE, 100.0f); m_lowpassRef.create(301, VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE, 600.0f); // Max deviation is 480Hz - m_movingAverageIdent.resize(m_samplesPerDot10wpm/5); // Needs to be short enough for noise floor calculation - m_binSampleCnt = 0; - m_binCnt = 0; - m_identNoise = 0.0001f; - for (int i = 0; i < m_identBins; i++) - { - m_identMaxs[i] = 0.0f; - } + m_morseDemod.applyChannelSettings(VORDemodSettings::VORDEMOD_CHANNEL_SAMPLE_RATE); } m_channelSampleRate = channelSampleRate; @@ -414,14 +301,7 @@ void VORDemodSCSink::applySettings(const VORDemodSettings& settings, bool force) if (m_settings.m_navId != settings.m_navId) { // Reset state when navId changes, so we don't report old ident for new navId - m_binSampleCnt = 0; - m_binCnt = 0; - m_identNoise = 0.0001f; - for (int i = 0; i < m_identBins; i++) - { - m_identMaxs[i] = 0.0f; - } - m_ident = ""; + m_morseDemod.reset(); m_refGoertzel.reset(); m_varGoertzel.reset(); } @@ -437,6 +317,7 @@ void VORDemodSCSink::applySettings(const VORDemodSettings& settings, bool force) } m_settings = settings; + m_morseDemod.applySettings(m_settings.m_identThreshold); } void VORDemodSCSink::applyAudioSampleRate(int sampleRate) diff --git a/plugins/channelrx/demodvor/vordemodsink.h b/plugins/channelrx/demodvor/vordemodsink.h index 2266dffe0..782c87fe2 100644 --- a/plugins/channelrx/demodvor/vordemodsink.h +++ b/plugins/channelrx/demodvor/vordemodsink.h @@ -25,6 +25,7 @@ #include "dsp/agc.h" #include "dsp/firfilter.h" #include "dsp/goertzel.h" +#include "dsp/morsedemod.h" #include "audio/audiofifo.h" #include "util/movingaverage.h" #include "util/doublebufferfifo.h" @@ -43,7 +44,7 @@ public: void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); void applySettings(const VORDemodSettings& settings, bool force = false); - void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setMessageQueueToChannel(MessageQueue *messageQueue); void applyAudioSampleRate(int sampleRate); int getAudioSampleRate() const { return m_audioSampleRate; } @@ -116,28 +117,12 @@ private: AudioFifo m_audioFifo; uint32_t m_audioBufferFill; - NCO m_ncoIdent; NCO m_ncoRef; Lowpass m_lowpassRef; - Bandpass m_bandpassIdent; - Lowpass m_lowpassIdent; - Highpass m_highpassIdent; Complex m_refPrev; - MovingAverageUtilVar m_movingAverageIdent; - static const int m_identBins = 20; - Real m_identMaxs[m_identBins]; - Real m_identNoise; - int m_binSampleCnt; - int m_binCnt; - int m_samplesPerDot7wpm; - int m_samplesPerDot10wpm; - int m_prevBit; - int m_bitTime; - QString m_ident; Goertzel m_varGoertzel; Goertzel m_refGoertzel; - //QFile m_file; - //QTextStream m_stream; + MorseDemod m_morseDemod; void processOneSample(Complex &ci); void processOneAudioSample(Complex &ci); diff --git a/plugins/feature/map/czml.cpp b/plugins/feature/map/czml.cpp index b807a9081..de7874222 100644 --- a/plugins/feature/map/czml.cpp +++ b/plugins/feature/map/czml.cpp @@ -23,6 +23,8 @@ #include "util/coordinates.h" +const QStringList CZML::m_heightReferences = {"NONE", "CLAMP_TO_GROUND", "RELATIVE_TO_GROUND", "CLIP_TO_GROUND"}; + CZML::CZML(const MapSettings *settings) : m_settings(settings) { @@ -82,6 +84,9 @@ QJsonObject CZML::update(PolygonMapItem *mapItem) return obj; } + // Need to use perPositionHeight for vertical polygons + bool perPosition = mapItem->m_extrudedHeight == 0; + QJsonArray positions; for (const auto c : mapItem->m_points) { @@ -94,7 +99,12 @@ QJsonObject CZML::update(PolygonMapItem *mapItem) {"cartographicDegrees", positions}, }; - QColor color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + QColor color; + if (mapItem->m_colorValid) { + color = QColor::fromRgba(mapItem->m_color); + } else { + color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + } QJsonArray colorRGBA { color.red(), color.green(), color.blue(), color.alpha() }; @@ -119,12 +129,23 @@ QJsonObject CZML::update(PolygonMapItem *mapItem) QJsonObject polygon { {"positions", positionList}, - {"height", mapItem->m_altitude}, - {"extrudedHeight", mapItem->m_extrudedHeight}, {"material", material}, {"outline", true}, - {"outlineColor", outlineColor} + {"outlineColor", outlineColor}, }; + if (perPosition) + { + polygon.insert("perPositionHeight", true); + if (mapItem->m_altitudeReference != 0) { + polygon.insert("altitudeReference", m_heightReferences[mapItem->m_altitudeReference]); // Custom code in map3d.html + } + } + else + { + polygon.insert("height", mapItem->m_altitude); + polygon.insert("heightReference", m_heightReferences[mapItem->m_altitudeReference]); + polygon.insert("extrudedHeight", mapItem->m_extrudedHeight); + } obj.insert("polygon", polygon); obj.insert("description", mapItem->m_label); @@ -163,7 +184,12 @@ QJsonObject CZML::update(PolylineMapItem *mapItem) {"cartographicDegrees", positions}, }; - QColor color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + QColor color; + if (mapItem->m_colorValid) { + color = QColor::fromRgba(mapItem->m_color); + } else { + color = QColor::fromRgba(mapItem->m_itemSettings->m_3DTrackColor); + } QJsonArray colorRGBA { color.red(), color.green(), color.blue(), color.alpha() }; @@ -183,6 +209,10 @@ QJsonObject CZML::update(PolylineMapItem *mapItem) {"positions", positionList}, {"material", material} }; + polyline.insert("clampToGround", mapItem->m_altitudeReference == 1); + if (mapItem->m_altitudeReference == 3) { + polyline.insert("altitudeReference", m_heightReferences[mapItem->m_altitudeReference]); // Custom code in map3d.html + } obj.insert("polyline", polyline); obj.insert("description", mapItem->m_label); diff --git a/plugins/feature/map/czml.h b/plugins/feature/map/czml.h index ad737a1b0..cb2c90967 100644 --- a/plugins/feature/map/czml.h +++ b/plugins/feature/map/czml.h @@ -37,6 +37,7 @@ private: QHash m_lastPosition; QHash m_hasMoved; QGeoCoordinate m_position; + static const QStringList m_heightReferences; public: CZML(const MapSettings *settings); diff --git a/plugins/feature/map/data/README.txt b/plugins/feature/map/data/README.txt new file mode 100644 index 000000000..f7d3d7163 --- /dev/null +++ b/plugins/feature/map/data/README.txt @@ -0,0 +1,10 @@ +UK transmitter data: + +TxParamsAM.csv. TxParamsDAB.csv, TxParamsVHF.csv - https://www.ofcom.org.uk/spectrum/information/radio-tech-parameters + +700-plan-clearance.xlsx - https://www.ofcom.org.uk/spectrum/information/transmitter-frequency + +French transmitter data: + +sites-DAB-TII-v0.10.csv - https://extranet.arcom.fr/radio/index.php + diff --git a/plugins/feature/map/map.cpp b/plugins/feature/map/map.cpp index 5fc040bbf..f29692062 100644 --- a/plugins/feature/map/map.cpp +++ b/plugins/feature/map/map.cpp @@ -260,6 +260,7 @@ void Map::webapiFormatFeatureSettings( const MapSettings& settings) { response.getMapSettings()->setDisplayNames(settings.m_displayNames ? 1 : 0); + response.getMapSettings()->setTerrain(new QString(settings.m_terrain)); if (response.getMapSettings()->getTitle()) { *response.getMapSettings()->getTitle() = settings.m_title; @@ -303,6 +304,9 @@ void Map::webapiUpdateFeatureSettings( if (featureSettingsKeys.contains("displayNames")) { settings.m_displayNames = response.getMapSettings()->getDisplayNames(); } + if (featureSettingsKeys.contains("terrain")) { + settings.m_terrain = *response.getMapSettings()->getTerrain(); + } if (featureSettingsKeys.contains("title")) { settings.m_title = *response.getMapSettings()->getTitle(); } @@ -343,6 +347,9 @@ void Map::webapiReverseSendSettings(const QList& featureSettingsKeys, c if (featureSettingsKeys.contains("displayNames") || force) { swgMapSettings->setDisplayNames(settings.m_displayNames); } + if (featureSettingsKeys.contains("terrain") || force) { + swgMapSettings->setTerrain(new QString(settings.m_terrain)); + } if (featureSettingsKeys.contains("title") || force) { swgMapSettings->setTitle(new QString(settings.m_title)); } diff --git a/plugins/feature/map/map/map3d.html b/plugins/feature/map/map/map3d.html index 0f0aca7f5..62ff61adc 100644 --- a/plugins/feature/map/map/map3d.html +++ b/plugins/feature/map/map/map3d.html @@ -171,6 +171,47 @@ viewer.selectedEntity = pickEntityPrioritized(e); } + function showCoords(e) { + if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) { + var cartesian = viewer.camera.pickEllipsoid(e.position); + var cartographic = Cesium.Cartographic.fromCartesian(cartesian); + longitudeString = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6); + latitudeString = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6); + positionMarker.position = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 1); + positionMarker.point.show = true; + positionMarker.label.show = true; + positionMarker.label.text = + `Lon: ${` ${longitudeString}`}\u00B0` + + `\nLat: ${` ${latitudeString}`}\u00B0`; + } else { + // https://github.com/CesiumGS/cesium/issues/4368 + // viewer.scene.pickPosition doesn't work because we have viewer.scene.globe.depthTestAgainstTerrain = false + const ray = viewer.camera.getPickRay(e.position); + const cartesian = viewer.scene.globe.pick(ray, viewer.scene); + var cartographic = Cesium.Cartographic.fromCartesian(cartesian); + var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [cartographic]); + Cesium.when(promise, function(updatedPositions) { + longitudeString = Cesium.Math.toDegrees(cartographic.longitude).toFixed(6); + latitudeString = Cesium.Math.toDegrees(cartographic.latitude).toFixed(6); + heightString = updatedPositions[0].height.toFixed(1); + positionMarker.position = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 1); // Height relative to ground + positionMarker.point.show = true; + positionMarker.label.show = true; + positionMarker.label.text = + `Lon: ${` ${longitudeString}`}\u00B0` + + `\nLat: ${` ${latitudeString}`}\u00B0` + + `\nAlt: ${` ${heightString}`}m`; + }, function() { + console.log(`Terrain doesn't support sampleTerrainMostDetailed`); + }); + } + } + + function hideCoords() { + positionMarker.point.show = false; + positionMarker.label.show = false; + } + Cesium.Ion.defaultAccessToken = '$CESIUM_ION_API_KEY$'; const viewer = new Cesium.Viewer('cesiumContainer', { @@ -184,14 +225,37 @@ navigationInstructionsInitiallyVisible: false, terrainProviderViewModels: [] // User should adjust terrain via dialog, so depthTestAgainstTerrain doesn't get set }); - viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain + viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain (this prevents pickPosition from working) viewer.screenSpaceEventHandler.setInputAction(pickEntity, Cesium.ScreenSpaceEventType.LEFT_CLICK); + viewer.screenSpaceEventHandler.setInputAction(showCoords, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK, Cesium.KeyboardEventModifier.SHIFT); + viewer.screenSpaceEventHandler.setInputAction(hideCoords, Cesium.ScreenSpaceEventType.RIGHT_CLICK); var buildings = undefined; const images = new Map(); var mufGeoJSONStream = null; var foF2GeoJSONStream = null; + const positionMarker = viewer.entities.add({ + id: 'Position marker', + point : { + show: false, + pixelSize : 8, + color : Cesium.Color.RED, + heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND + }, + label: { + show: false, + showBackground: true, + font: "12px monospace", + fillColor: Cesium.Color.WHITE, + outlineColor: Cesium.Color.RED, + horizontalOrigin: Cesium.HorizontalOrigin.LEFT, + verticalOrigin: Cesium.VerticalOrigin.TOP, + pixelOffset: new Cesium.Cartesian2(0, 9), + heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND + }, + }); + // Generate HTML for MUF contour info box from properties in GeoJSON function describeMUF(properties, nameProperty) { let html = ""; @@ -446,6 +510,51 @@ console.log(`Can't currently use altitudeReference when more than one position`); czmlStream.process(command); } + } else if ( (command.hasOwnProperty('polygon') && command.polygon.hasOwnProperty('altitudeReference')) + || (command.hasOwnProperty('polyline') && command.polyline.hasOwnProperty('altitudeReference'))) { + // Support per vertex height reference in polygons and CLIP_TO_GROUND in polylines + var prim = command.hasOwnProperty('polygon') ? command.polygon : command.polyline; + var clipToGround = prim.altitudeReference == "CLIP_TO_GROUND"; + var clampToGround = prim.altitudeReference == "CLAMP_TO_GROUND"; + var size = prim.positions.cartographicDegrees.length; + var positionCount = size/3; + var positions = new Array(positionCount); + if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) { + if (clampToGround) { + for (let i = 0; i < positionCount; i++) { + prim.positions.cartographicDegrees[i*3+2] = 0; + } + } else if (clipToGround) { + for (let i = 0; i < positionCount; i++) { + if (prim.positions.cartographicDegrees[i*3+2] < 0) { + prim.positions.cartographicDegrees[i*3+2] = 0; + } + } + } + czmlStream.process(command); + } else { + for (let i = 0; i < positionCount; i++) { + positions[i] = Cesium.Cartographic.fromDegrees(prim.positions.cartographicDegrees[i*3+0], prim.positions.cartographicDegrees[i*3+1]); + } + var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, positions); + Cesium.when(promise, function(updatedPositions) { + if (clampToGround) { + for (let i = 0; i < positionCount; i++) { + prim.positions.cartographicDegrees[i*3+2] = updatedPositions[i].height; + } + } else if (clipToGround) { + for (let i = 0; i < positionCount; i++) { + if (prim.positions.cartographicDegrees[i*3+2] < updatedPositions[i].height) { + prim.positions.cartographicDegrees[i*3+2] = updatedPositions[i].height; + } + } + } + czmlStream.process(command); + }, function() { + console.log(`Terrain doesn't support sampleTerrainMostDetailed`); + czmlStream.process(command); + }); + } } else { czmlStream.process(command); } @@ -511,3 +620,4 @@ + diff --git a/plugins/feature/map/mapfmlistdialog.ui b/plugins/feature/map/mapfmlistdialog.ui new file mode 100644 index 000000000..4b5e55f0e --- /dev/null +++ b/plugins/feature/map/mapfmlistdialog.ui @@ -0,0 +1,437 @@ + + + MapBeaconDialog + + + + 0 + 0 + 1027 + 349 + + + + + Liberation Sans + 9 + + + + Beacons + + + + + + + 0 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Download FM/DAB list + + + + + + + :/recycle.png:/recycle.png + + + + + + + + + 2 + + + + FM + + + + + 0 + 20 + 989 + 192 + + + + + + + + Station + + + + + Frequency + + + + + Location + + + + + Power + + + + + Azimuth + + + + + Elevation + + + + + Distance (km) + + + + + + + DAB + + + + + 10 + 20 + 989 + 192 + + + + + + + + Station + + + + + Frequency + + + + + Location + + + + + Power + + + + + Azimuth + + + + + Elevation + + + + + Distance (km) + + + + + + + Settings + + + + + + Countries + + + + + + ETH + + + + + + + CRO + + + + + + + LCA + + + + + + + NIU + + + + + + + AZE + + + + + + + TKM + + + + + + + GTB + + + + + + + BRB + + + + + + + MLD + + + + + + + IRQ + + + + + + + PTC + + + + + + + ABW + + + + + + + SNG + + + + + + + VIR + + + + + + + AFG + + + + + + + AZR + + + + + + + BRU + + + + + + + CTI + + + + + + + F + + + + + + + GTM + + + + + + + ISL + + + + + + + LHW + + + + + + + MLI + + + + + + + NMB + + + + + + + PTR + + + + + + + SOM + + + + + + + TMP + + + + + + + VRG + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + + + buttonBox + accepted() + MapBeaconDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MapBeaconDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index 7f8601922..47285af89 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -1643,6 +1643,7 @@ void MapGUI::on_displaySettings_clicked() } applyMap2DSettings(dialog.m_map2DSettingsChanged); applyMap3DSettings(dialog.m_map3DSettingsChanged); + m_settingsKeys.append(dialog.m_settingsKeysChanged); applySettings(); m_objectMapModel.allUpdated(); m_imageMapModel.allUpdated(); diff --git a/plugins/feature/map/mapitem.cpp b/plugins/feature/map/mapitem.cpp index 9f1466aec..5a3c27af2 100644 --- a/plugins/feature/map/mapitem.cpp +++ b/plugins/feature/map/mapitem.cpp @@ -113,6 +113,9 @@ void PolygonMapItem::update(SWGSDRangel::SWGMapItem *mapItem) { MapItem::update(mapItem); m_extrudedHeight = mapItem->getExtrudedHeight(); + m_colorValid = mapItem->getColorValid(); + m_color = mapItem->getColor(); + m_altitudeReference = mapItem->getAltitudeReference(); qDeleteAll(m_points); m_points.clear(); @@ -145,6 +148,9 @@ void PolygonMapItem::update(SWGSDRangel::SWGMapItem *mapItem) void PolylineMapItem::update(SWGSDRangel::SWGMapItem *mapItem) { MapItem::update(mapItem); + m_colorValid = mapItem->getColorValid(); + m_color = mapItem->getColor(); + m_altitudeReference = mapItem->getAltitudeReference(); qDeleteAll(m_points); m_points.clear(); diff --git a/plugins/feature/map/mapitem.h b/plugins/feature/map/mapitem.h index 182511c50..02b47e55e 100644 --- a/plugins/feature/map/mapitem.h +++ b/plugins/feature/map/mapitem.h @@ -21,6 +21,7 @@ #include #include #include +#include #include "mapsettings.h" #include "cesiuminterface.h" @@ -139,6 +140,9 @@ protected: float m_extrudedHeight; // In metres QVariantList m_polygon; QGeoRectangle m_bounds; // Bounding boxes for the polygons, for view clipping + bool m_colorValid; + QRgb m_color; + int m_altitudeReference; }; class PolylineMapItem : public MapItem { @@ -158,6 +162,9 @@ protected: QList m_points; // FIXME: Remove? QVariantList m_polyline; QGeoRectangle m_bounds; // Bounding boxes for the polyline, for view clipping + bool m_colorValid; + QRgb m_color; + int m_altitudeReference; }; class ImageMapItem : public MapItem { diff --git a/plugins/feature/map/mapmodel.cpp b/plugins/feature/map/mapmodel.cpp index d7d8146e9..7b110604d 100644 --- a/plugins/feature/map/mapmodel.cpp +++ b/plugins/feature/map/mapmodel.cpp @@ -271,9 +271,16 @@ QVariant PolygonMapModel::data(const QModelIndex &index, int role) const case borderColorRole: return QVariant::fromValue(QColor(0x00, 0x00, 0x00, 0x00)); // Transparent case fillColorRole: - if (m_items[row]->m_itemSettings->m_display2DTrack) { - return QVariant::fromValue(QColor::fromRgba(m_items[row]->m_itemSettings->m_2DTrackColor)); - } else { + if (polygonItem->m_itemSettings->m_display2DTrack) + { + if (polygonItem->m_colorValid) { + return QVariant::fromValue(QColor::fromRgba(polygonItem->m_color)); + } else { + return QVariant::fromValue(QColor::fromRgba(polygonItem->m_itemSettings->m_2DTrackColor)); + } + } + else + { return QVariant::fromValue(QColor(0x00, 0x00, 0x00, 0x00)); // Transparent } case polygonRole: @@ -308,7 +315,11 @@ QVariant PolylineMapModel::data(const QModelIndex &index, int role) const switch (role) { case lineColorRole: - return QVariant::fromValue(QColor::fromRgba(m_items[row]->m_itemSettings->m_2DTrackColor)); + if (polylineItem->m_colorValid) { + return QVariant::fromValue(QColor::fromRgba(polylineItem->m_color)); + } else { + return QVariant::fromValue(QColor::fromRgba(polylineItem->m_itemSettings->m_2DTrackColor)); + } case coordinatesRole: return QVariant::fromValue(polylineItem->m_polyline); case boundsRole: diff --git a/plugins/feature/map/mapplugin.cpp b/plugins/feature/map/mapplugin.cpp index de6d2d7a0..c4b2597ea 100644 --- a/plugins/feature/map/mapplugin.cpp +++ b/plugins/feature/map/mapplugin.cpp @@ -30,7 +30,7 @@ const PluginDescriptor MapPlugin::m_pluginDescriptor = { Map::m_featureId, QStringLiteral("Map"), - QStringLiteral("7.11.0"), + QStringLiteral("7.12.0"), QStringLiteral("(c) Jon Beniston, M7RCE"), QStringLiteral("https://github.com/f4exb/sdrangel"), true, diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index c8e8e4bec..de8b90f92 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -33,6 +33,7 @@ const QStringList MapSettings::m_pipeTypes = { QStringLiteral("APTDemod"), QStringLiteral("FT8Demod"), QStringLiteral("HeatMap"), + QStringLiteral("ILSDemod"), QStringLiteral("Radiosonde"), QStringLiteral("StarTracker"), QStringLiteral("SatelliteTracker"), @@ -47,6 +48,7 @@ const QStringList MapSettings::m_pipeURIs = { QStringLiteral("sdrangel.channel.aptdemod"), QStringLiteral("sdrangel.channel.ft8demod"), QStringLiteral("sdrangel.channel.heatmap"), + QStringLiteral("sdrangel.channel.ilsdemod"), QStringLiteral("sdrangel.feature.radiosonde"), QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.satellitetracker"), @@ -95,6 +97,7 @@ MapSettings::MapSettings() : m_itemSettings.insert("DAB", dabSettings); m_itemSettings.insert("Navtex", new MapItemSettings("Navtex", false, QColor(255, 0, 255), false, true, 8)); + m_itemSettings.insert("ILSDemod", new MapItemSettings("ILSDemod", true, QColor(0, 205, 200), true, true, 10)); MapItemSettings *navAidSettings = new MapItemSettings("NavAid", false, QColor(255, 0, 255), false, true, 11); navAidSettings->m_filterDistance = 500000; diff --git a/plugins/feature/map/mapsettingsdialog.cpp b/plugins/feature/map/mapsettingsdialog.cpp index 9ce212513..cec8ae03d 100644 --- a/plugins/feature/map/mapsettingsdialog.cpp +++ b/plugins/feature/map/mapsettingsdialog.cpp @@ -249,13 +249,41 @@ void MapSettingsDialog::accept() m_map3DSettingsChanged = false; } - m_settings->m_map2DEnabled = ui->map2DEnabled->isChecked(); - m_settings->m_map3DEnabled = ui->map3DEnabled->isChecked(); - m_settings->m_terrain = ui->terrain->currentText(); - m_settings->m_buildings = ui->buildings->currentText(); - m_settings->m_sunLightEnabled = ui->sunLightEnabled->currentIndex() == 1; - m_settings->m_eciCamera = ui->eciCamera->currentIndex() == 1; - m_settings->m_antiAliasing = ui->antiAliasing->currentText(); + if (m_settings->m_map2DEnabled != ui->map2DEnabled->isChecked()) + { + m_settings->m_map2DEnabled = ui->map2DEnabled->isChecked(); + m_settingsKeysChanged.append("map2DEnabled"); + } + if (m_settings->m_map3DEnabled != ui->map3DEnabled->isChecked()) + { + m_settings->m_map3DEnabled = ui->map3DEnabled->isChecked(); + m_settingsKeysChanged.append("map3DEnabled"); + } + if (m_settings->m_terrain != ui->terrain->currentText()) + { + m_settings->m_terrain = ui->terrain->currentText(); + m_settingsKeysChanged.append("terrain"); + } + if (m_settings->m_buildings != ui->buildings->currentText()) + { + m_settings->m_buildings = ui->buildings->currentText(); + m_settingsKeysChanged.append("buildings"); + } + if (m_settings->m_sunLightEnabled != (ui->sunLightEnabled->currentIndex() == 1)) + { + m_settings->m_sunLightEnabled = ui->sunLightEnabled->currentIndex() == 1; + m_settingsKeysChanged.append("sunLightEnabled"); + } + if (m_settings->m_eciCamera != (ui->eciCamera->currentIndex() == 1)) + { + m_settings->m_eciCamera = ui->eciCamera->currentIndex() == 1; + m_settingsKeysChanged.append("eciCamera"); + } + if (m_settings->m_antiAliasing != ui->antiAliasing->currentText()) + { + m_settings->m_antiAliasing = ui->antiAliasing->currentText(); + m_settingsKeysChanged.append("antiAliasing"); + } for (int row = 0; row < ui->mapItemSettings->rowCount(); row++) { diff --git a/plugins/feature/map/mapsettingsdialog.h b/plugins/feature/map/mapsettingsdialog.h index 6eade0e69..433fe8eab 100644 --- a/plugins/feature/map/mapsettingsdialog.h +++ b/plugins/feature/map/mapsettingsdialog.h @@ -90,6 +90,7 @@ public: bool m_map2DSettingsChanged; // 2D map needs to be reloaded bool m_map3DSettingsChanged; // 3D map needs to be reloaded bool m_osmURLChanged; + QStringList m_settingsKeysChanged; // List of setting keys that have been changed private: MapSettings *m_settings; diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 4a0dcfcd1..9e578b6b4 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -13,7 +13,8 @@ On top of this, it can plot data from other plugins, such as: * The Sun, Moon and Stars from the Star Tracker, * Weather ballons from the RadioSonde feature, * RF Heat Maps from the Heap Map channel, -* Radials and estimated position from the VOR localizer feature. +* Radials and estimated position from the VOR localizer feature, +* ILS course line and glide path from the ILS Demodulator. As well as internet data sources: @@ -168,6 +169,7 @@ The map feature displays a 2D and a 3D map overlaid with objects reported by oth * Setting the Device center frequency to the first frequency found in the text bubble for the object. * Changing the order in which the objects are drawn, which can help to cycle through multiple objects that are at the same location on the map. * Setting the object as the tracking target on the 3D map. +* Left double clicking while holding shift on the 3D map will place a marker showing the position. Right clicking will clear it. The 2D map will only display the last reported positions for objects. The 3D map, however, has a timeline that allows replaying how objects have moved over time. diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 50c2efdde..c82864707 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -118,6 +118,7 @@ set(sdrbase_SOURCES dsp/hbfilterchainconverter.cpp dsp/hbfiltertraits.cpp dsp/mimochannel.cpp + dsp/morsedemod.cpp dsp/nco.cpp dsp/ncof.cpp dsp/phaselock.cpp @@ -336,6 +337,7 @@ set(sdrbase_HEADERS dsp/mimochannel.h dsp/misc.h dsp/movingaverage.h + dsp/morsedemod.h dsp/nco.h dsp/ncof.h dsp/phasediscri.h diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index 5643629c1..bfdc3e257 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -1177,6 +1177,38 @@ bool ChannelWebAPIUtils::patchFeatureSetting(unsigned int featureSetIndex, unsig } } +bool ChannelWebAPIUtils::getFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, int &value) +{ + SWGSDRangel::SWGFeatureSettings featureSettingsResponse; + Feature *feature; + + if (getFeatureSettings(featureSetIndex, featureIndex, featureSettingsResponse, feature)) + { + QJsonObject *jsonObj = featureSettingsResponse.asJsonObject(); + return WebAPIUtils::getSubObjectInt(*jsonObj, setting, value); + } + else + { + return false; + } +} + +bool ChannelWebAPIUtils::getFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, QString &value) +{ + SWGSDRangel::SWGFeatureSettings featureSettingsResponse; + Feature *feature; + + if (getFeatureSettings(featureSetIndex, featureIndex, featureSettingsResponse, feature)) + { + QJsonObject *jsonObj = featureSettingsResponse.asJsonObject(); + return WebAPIUtils::getSubObjectString(*jsonObj, setting, value); + } + else + { + return false; + } +} + bool ChannelWebAPIUtils::getFeatureReportValue(unsigned int featureSetIndex, unsigned int featureIndex, const QString &key, int &value) { SWGSDRangel::SWGFeatureReport featureReport; diff --git a/sdrbase/channel/channelwebapiutils.h b/sdrbase/channel/channelwebapiutils.h index aae92b34b..ee95f21d4 100644 --- a/sdrbase/channel/channelwebapiutils.h +++ b/sdrbase/channel/channelwebapiutils.h @@ -66,6 +66,8 @@ public: static bool patchDeviceSetting(unsigned int deviceIndex, const QString &setting, int value); static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, const QString &value); static bool patchFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, double value); + static bool getFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, int &value); + static bool getFeatureSetting(unsigned int featureSetIndex, unsigned int featureIndex, const QString &setting, QString &value); static bool getFeatureReportValue(unsigned int featureSetIndex, unsigned int featureIndex, const QString &key, int &value); static bool getFeatureReportValue(unsigned int featureSetIndex, unsigned int featureIndex, const QString &key, QString &value); protected: diff --git a/sdrbase/dsp/morsedemod.cpp b/sdrbase/dsp/morsedemod.cpp new file mode 100644 index 000000000..cd4511b16 --- /dev/null +++ b/sdrbase/dsp/morsedemod.cpp @@ -0,0 +1,168 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "util/morse.h" + +#include "morsedemod.h" + +MESSAGE_CLASS_DEFINITION(MorseDemod::MsgReportIdent, Message) + +MorseDemod::MorseDemod() : + m_movingAverageIdent(5000), + m_prevBit(0), + m_bitTime(0) +{ +} + +void MorseDemod::reset() +{ + m_binSampleCnt = 0; + m_binCnt = 0; + m_identNoise = 0.0001f; + for (int i = 0; i < m_identBins; i++) + { + m_identMaxs[i] = 0.0f; + } + m_ident = ""; +} + +void MorseDemod::applyChannelSettings(int channelSampleRate) +{ + if (channelSampleRate <= 0) { + return; + } + m_samplesPerDot7wpm = channelSampleRate*60/(50*7); + m_samplesPerDot10wpm = channelSampleRate*60/(50*10); + + m_ncoIdent.setFreq(-1020, channelSampleRate); // +-50Hz source offset allowed + m_bandpassIdent.create(1001, channelSampleRate, 970.0f, 1070.0f); // Ident at 1020 + + m_lowpassIdent.create(301, channelSampleRate, 100.0f); + m_movingAverageIdent.resize(m_samplesPerDot10wpm/5); // Needs to be short enough for noise floor calculation + + reset(); +} + +void MorseDemod::applySettings(int identThreshold) +{ + m_identThreshold = identThreshold; + reset(); +} + +void MorseDemod::processOneSample(const Complex &magc) +{ + // Filter to remove voice + Complex c1 = m_bandpassIdent.filter(magc); + // Remove ident sub-carrier offset + c1 *= m_ncoIdent.nextIQ(); + // Filter other signals + Complex c2 = std::abs(m_lowpassIdent.filter(c1)); + + // Filter noise with moving average (moving average preserves edges) + m_movingAverageIdent(c2.real()); + Real mav = m_movingAverageIdent.asFloat(); + + // Caclulate noise floor + if (mav > m_identMaxs[m_binCnt]) + m_identMaxs[m_binCnt] = mav; + m_binSampleCnt++; + if (m_binSampleCnt >= m_samplesPerDot10wpm/4) + { + // Calc minimum of maximums + m_identNoise = 1.0f; + for (int i = 0; i < m_identBins; i++) + { + m_identNoise = std::min(m_identNoise, m_identMaxs[i]); + } + m_binSampleCnt = 0; + m_binCnt++; + if (m_binCnt == m_identBins) + m_binCnt = 0; + m_identMaxs[m_binCnt] = 0.0f; + + // Prevent divide by zero + if (m_identNoise == 0.0f) + m_identNoise = 1e-20f; + } + + // CW demod + int bit = (mav / m_identNoise) >= m_identThreshold; + //m_stream << mav << "," << m_identNoise << "," << bit << "," << (mav / m_identNoise) << "\n"; + if ((m_prevBit == 0) && (bit == 1)) + { + if (m_bitTime > 7*m_samplesPerDot10wpm) + { + if (m_ident.trimmed().size() > 2) // Filter out noise that may appear as one or two characters + { + qDebug() << "MorseDemod::processOneSample:" << m_ident << " " << Morse::toString(m_ident); + + if (getMessageQueueToChannel()) + { + MorseDemod::MsgReportIdent *msg = MorseDemod::MsgReportIdent::create(m_ident); + getMessageQueueToChannel()->push(msg); + } + } + m_ident = ""; + } + else if (m_bitTime > 2.5*m_samplesPerDot10wpm) + { + m_ident.append(" "); + } + m_bitTime = 0; + } + else if (bit == 1) + { + m_bitTime++; + } + else if ((m_prevBit == 1) && (bit == 0)) + { + if (m_bitTime > 2*m_samplesPerDot10wpm) + { + m_ident.append("-"); + } + else if (m_bitTime > 0.2*m_samplesPerDot10wpm) + { + m_ident.append("."); + } + m_bitTime = 0; + } + else + { + m_bitTime++; + if (m_bitTime > 10*m_samplesPerDot7wpm) + { + m_ident = m_ident.simplified(); + if (m_ident.trimmed().size() > 2) // Filter out noise that may appear as one or two characters + { + qDebug() << "MorseDemod::processOneSample:" << m_ident << " " << Morse::toString(m_ident); + + if (getMessageQueueToChannel()) + { + MorseDemod::MsgReportIdent *msg = MorseDemod::MsgReportIdent::create(m_ident); + getMessageQueueToChannel()->push(msg); + } + + } + m_ident = ""; + m_bitTime = 0; + } + } + m_prevBit = bit; +} + diff --git a/sdrbase/dsp/morsedemod.h b/sdrbase/dsp/morsedemod.h new file mode 100644 index 000000000..b0f9552f4 --- /dev/null +++ b/sdrbase/dsp/morsedemod.h @@ -0,0 +1,87 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 SDRBASE_DSP_MORSEDEMOD_H_ +#define SDRBASE_DSP_MORSEDEMOD_H_ + +#include + +#include "dsp/nco.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "export.h" + +// Morse code demodulator for use with VOR and ILS +class SDRBASE_API MorseDemod { + +public: + class SDRBASE_API MsgReportIdent : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getIdent() const { return m_ident; } + + static MsgReportIdent* create(QString ident) + { + return new MsgReportIdent(ident); + } + + private: + QString m_ident; + + MsgReportIdent(QString ident) : + Message(), + m_ident(ident) + { + } + }; + + MorseDemod(); + void processOneSample(const Complex &magc); + void applyChannelSettings(int channelSampleRate); + void applySettings(int identThreshold); + void reset(); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + MessageQueue *getMessageQueueToChannel() const { return m_messageQueueToChannel; } + +private: + + MessageQueue *m_messageQueueToChannel; + NCO m_ncoIdent; + Bandpass m_bandpassIdent; + Lowpass m_lowpassIdent; + MovingAverageUtilVar m_movingAverageIdent; + static const int m_identBins = 20; + Real m_identMaxs[m_identBins]; + Real m_identNoise; + int m_binSampleCnt; + int m_binCnt; + int m_samplesPerDot7wpm; + int m_samplesPerDot10wpm; + int m_prevBit; + int m_bitTime; + QString m_ident; + + int m_identThreshold; + +}; + +#endif /* SDRBASE_DSP_MORSEDEMOD_H_ */ + diff --git a/sdrbase/feature/featurewebapiutils.cpp b/sdrbase/feature/featurewebapiutils.cpp index 56141692d..f2d4f72fa 100644 --- a/sdrbase/feature/featurewebapiutils.cpp +++ b/sdrbase/feature/featurewebapiutils.cpp @@ -87,7 +87,7 @@ bool FeatureWebAPIUtils::mapSetDateTime(const QDateTime& dateTime, int featureSe } // Get first feature with the given URI -Feature* FeatureWebAPIUtils::getFeature(int featureSetIndex, int featureIndex, const QString& uri) +Feature* FeatureWebAPIUtils::getFeature(int& featureSetIndex, int& featureIndex, const QString& uri) { FeatureSet *featureSet; Feature *feature; @@ -116,13 +116,16 @@ Feature* FeatureWebAPIUtils::getFeature(int featureSetIndex, int featureIndex, c else { // Find first feature matching URI - for (std::vector::const_iterator it = featureSets.begin(); it != featureSets.end(); ++it, featureIndex++) + int fsi = 0; + for (std::vector::const_iterator it = featureSets.begin(); it != featureSets.end(); ++it, ++fsi) { for (int fi = 0; fi < (*it)->getNumberOfFeatures(); fi++) { feature = (*it)->getFeatureAt(fi); if (feature->getURI() == uri) { + featureSetIndex = fsi; + featureIndex = fi; return feature; } } diff --git a/sdrbase/feature/featurewebapiutils.h b/sdrbase/feature/featurewebapiutils.h index c72e6342b..6acb745a4 100644 --- a/sdrbase/feature/featurewebapiutils.h +++ b/sdrbase/feature/featurewebapiutils.h @@ -29,7 +29,7 @@ class SDRBASE_API FeatureWebAPIUtils public: static bool mapFind(const QString& target, int featureSetIndex=-1, int featureIndex=-1); static bool mapSetDateTime(const QDateTime& dateTime, int featureSetIndex=-1, int featureIndex=-1); - static Feature *getFeature(int featureSetIndex, int featureIndex, const QString& uri); + static Feature *getFeature(int& featureSetIndex, int& featureIndex, const QString& uri); static bool satelliteAOS(const QString name, const QDateTime aos, const QDateTime los); static bool satelliteLOS(const QString name); }; diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h index 62c1a745f..69d1deb95 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -268,11 +268,11 @@ public: // We support both decimal and DMS formats static bool stringToLatitudeAndLongitude(const QString& string, float& latitude, float& longitude) { - QRegExp decimal("(-?[0-9]+\\.[0-9]+) *,? *(-?[0-9]+\\.[0-9]+)"); + QRegExp decimal("(-?[0-9]+(\\.[0-9]+)?) *,? *(-?[0-9]+(\\.[0-9]+)?)"); if (decimal.exactMatch(string)) { latitude = decimal.capturedTexts()[1].toFloat(); - longitude = decimal.capturedTexts()[2].toFloat(); + longitude = decimal.capturedTexts()[3].toFloat(); return true; } QRegExp dms(QString("([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([NS]) *,? *([0-9]+)[%1d]([0-9]+)['m]([0-9]+(\\.[0-9]+)?)[\"s]([EW])").arg(QChar(0xb0))); @@ -313,6 +313,26 @@ public: longitude = -longitude; return true; } + // 512255.5900N 0024400.6105W as used on aviation charts + QRegExp dms3(QString("(\\d{2})(\\d{2})((\\d{2})(\\.\\d+)?)([NS]) *,?(\\d{3})(\\d{2})((\\d{2})(\\.\\d+)?)([EW])")); + if (dms3.exactMatch(string)) + { + float latD = dms3.capturedTexts()[1].toFloat(); + float latM = dms3.capturedTexts()[2].toFloat(); + float latS = dms3.capturedTexts()[3].toFloat(); + bool north = dms3.capturedTexts()[6] == "N"; + float lonD = dms3.capturedTexts()[7].toFloat(); + float lonM = dms3.capturedTexts()[8].toFloat(); + float lonS = dms3.capturedTexts()[9].toFloat(); + bool east = dms3.capturedTexts()[12] == "E"; + latitude = latD + latM/60.0 + latS/(60.0*60.0); + if (!north) + latitude = -latitude; + longitude = lonD + lonM/60.0 + lonS/(60.0*60.0); + if (!east) + longitude = -longitude; + return true; + } return false; } diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 99712a4f3..94d15364d 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -4538,6 +4538,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setIeee802154ModSettings(new SWGSDRangel::SWGIEEE_802_15_4_ModSettings()); channelSettings->getIeee802154ModSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "ILSDemodSettings") + { + channelSettings->setIlsDemodSettings(new SWGSDRangel::SWGILSDemodSettings()); + channelSettings->getIlsDemodSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "InterferometerSettings") { channelSettings->setInterferometerSettings(new SWGSDRangel::SWGInterferometerSettings()); @@ -5392,6 +5397,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.setDsdDemodSettings(nullptr); channelSettings.setHeatMapSettings(nullptr); channelSettings.setIeee802154ModSettings(nullptr); + channelSettings.setIlsDemodSettings(nullptr); channelSettings.setNavtexDemodSettings(nullptr); channelSettings.setNfmDemodSettings(nullptr); channelSettings.setNfmModSettings(nullptr); @@ -5430,6 +5436,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.setDatvModReport(nullptr); channelReport.setDsdDemodReport(nullptr); channelReport.setHeatMapReport(nullptr); + channelReport.setIlsDemodReport(nullptr); channelReport.setNavtexDemodReport(nullptr); channelReport.setNfmDemodReport(nullptr); channelReport.setNfmModReport(nullptr); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 68a9e9008..500713f76 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -48,6 +48,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channeltx.freedvmod", "FreeDVModSettings"}, {"sdrangel.channel.freqtracker", "FreqTrackerSettings"}, {"sdrangel.channel.heatmap", "HeatMapSettings"}, + {"sdrangel.channel.ilsdemod", "ILSDemodSettings"}, {"sdrangel.channel.navtexemod", "NavtexDemodSettings"}, {"sdrangel.channel.m17demod", "M17DemodSettings"}, {"sdrangel.channeltx.modm17", "M17ModSettings"}, diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 3cf13c2f6..3f9cc5e60 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -21,6 +21,7 @@ set(sdrgui_SOURCES gui/commandsdialog.cpp gui/commandoutputdialog.cpp gui/configurationsdialog.cpp + gui/coursedeviationindicator.cpp gui/crightclickenabler.cpp gui/customtextedit.cpp gui/cwkeyergui.cpp @@ -130,6 +131,7 @@ set(sdrgui_HEADERS gui/commandsdialog.h gui/commandoutputdialog.h gui/configurationsdialog.h + gui/coursedeviationindicator.h gui/crightclickenabler.h gui/customtextedit.h gui/cwkeyergui.h diff --git a/sdrgui/gui/coursedeviationindicator.cpp b/sdrgui/gui/coursedeviationindicator.cpp new file mode 100644 index 000000000..6b3742122 --- /dev/null +++ b/sdrgui/gui/coursedeviationindicator.cpp @@ -0,0 +1,179 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "coursedeviationindicator.h" + +CourseDeviationIndicator::CourseDeviationIndicator(QWidget *parent) : + QWidget(parent), + m_localizerDDM(0.0f), + m_glideSlopeDDM(0.0f) +{ +} + +void CourseDeviationIndicator::setMode(Mode mode) +{ + m_mode = mode; + update(); +} + +void CourseDeviationIndicator::setLocalizerDDM(float ddm) +{ + m_localizerDDM = ddm; + update(); +} + +void CourseDeviationIndicator::setGlideSlopeDDM(float ddm) +{ + m_glideSlopeDDM = ddm; + update(); +} + +void CourseDeviationIndicator::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + + QRect r = rect(); + int midW = r.width() / 2; + int midH = r.height() / 2; + int spacing; + + // A320 like CDI + + // Black background + int bgw, bgh; + if (m_mode == LOC) + { + bgw = r.width(); + bgh = 20; + painter.fillRect(0, midH - bgh, bgw, bgh*2, QColor(0, 0, 0)); + } + else + { + bgw = 20; + bgh = r.height(); + painter.fillRect(midW - bgw, 0, bgw*2, bgh, QColor(0, 0, 0)); + } + + const int dots = 5; + + // Circles + painter.setPen(QColor(255, 255, 255)); + const int radius = 4; + int x, y; + + if (m_mode == LOC) + { + spacing = r.width() / 5; + x = spacing / 2; + y = midH; + } + else + { + spacing = r.height() / 5; + x = midW; + y = spacing / 2; + } + for (int i = 0; i < dots; i++) + { + if (i != 2) { + painter.drawEllipse(QPointF(x, y), radius, radius); + } + if (m_mode == LOC) { + x += spacing; + } else { + y += spacing; + } + } + + // Diamond (index) - only draw half of symbol if out of range + // Shouldn't draw the symbol if signal not vaiid + // Typically, LOC full scale deflection 0.155 DDM (Which is ~2.5deg, so 1 deg per dot, but can be 3 degrees. A320 is 0.8 deg per dot) + // For GS, full deflection is 0.0875 DDM (0.7deg, so 0.14 deg per dot) + painter.setPen(QColor(255, 150, 250)); + float dev; + if (m_mode == LOC) { + dev = m_localizerDDM / 0.155; + } else { + dev = m_glideSlopeDDM / 0.0875; + } + dev = std::min(dev, 1.0f); + dev = std::max(dev, -1.0f); + if (m_mode == LOC) + { + x = midW + dev * r.width() / 2; // Positive DDM means we're to left of course line + y = midH; + } + else + { + x = midW; + y = midH + dev * r.height() / 2; // Positive DDM means we're above glide path + } + int dw = 10; + int dh = 8; + painter.drawLine(x, y + dh, x - dw, y); + painter.drawLine(x - dw, y, x, y - dh); + painter.drawLine(x + dw, y, x, y - dh); + painter.drawLine(x, y + dh, x + dw, y); + + // Centre line + painter.setPen(QColor(255, 255, 70)); + if (m_mode == LOC) + { + int lh = 14; + painter.drawLine(midW, midH + lh, midW, midH - lh); + painter.drawLine(midW-1, midH + lh, midW-1, midH - lh); + painter.drawLine(midW+1, midH + lh, midW+1, midH - lh); + } + else + { + int lw = 14; + painter.drawLine(midW + lw, midH, midW - lw, midH); + painter.drawLine(midW + lw, midH - 1, midW - lw, midH - 1); + painter.drawLine(midW + lw, midH + 1, midW - lw, midH + 1); + } + + if (m_mode == LOC) + { + // Indicate localizer capture + if (std::abs(m_localizerDDM) < 0.175) // See 3.1.3.7.4 + { + QFontMetrics fm(painter.font()); + QString text = "LOC"; + int tw = fm.horizontalAdvance(text); + int th = fm.descent(); + painter.setPen(QColor(0, 255, 0)); + painter.drawText(midW - tw/2, midH - bgh - th, text); + } + } + else + { + // Indicate glideslope capture + if (std::abs(m_glideSlopeDDM) < 0.175) // Can't see a spec for this + { + QFontMetrics fm(painter.font()); + QString text = "G/S"; + int tw = fm.horizontalAdvance(text); + int th = fm.ascent() / 2; + painter.setPen(QColor(0, 255, 0)); + painter.drawText(midW + bgw + 2, midH + th, text); + } + } + +} + diff --git a/sdrgui/gui/coursedeviationindicator.h b/sdrgui/gui/coursedeviationindicator.h new file mode 100644 index 000000000..addcb277f --- /dev/null +++ b/sdrgui/gui/coursedeviationindicator.h @@ -0,0 +1,59 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 SDRGUI_GUI_COURSEDEVIATIONINDICATOR_H +#define SDRGUI_GUI_COURSEDEVIATIONINDICATOR_H + +#include + +#include "export.h" + +// Aircraft Course Deviation Indicator (CDI) +class SDRGUI_API CourseDeviationIndicator : public QWidget { + Q_OBJECT + +public: + + enum Mode { + LOC, + GS, + // TODO: BOTH + }; + + explicit CourseDeviationIndicator(QWidget *parent = nullptr); + void setLocalizerDDM(float ddm); + float getLocazlierDDM() const { return m_localizerDDM; } + void setGlideSlopeDDM(float ddm); + float getGlideSlopeDDM() const { return m_glideSlopeDDM; } + void setMode(Mode mode); + Mode getMode() const { return m_mode; } + + void paintEvent(QPaintEvent *event); +protected: + +private: + float m_localizerDDM; + float m_glideSlopeDDM; + Mode m_mode; + +private slots: + + +}; + +#endif // SDRGUI_GUI_COURSEDEVIATIONINDICATOR_H + diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 23cff09c5..5c55e0b53 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -55,6 +55,8 @@ ChannelReport: $ref: "http://swgserver:8081/api/swagger/include/RTTYDemod.yaml#/RTTYDemodReport" HeatMapReport: $ref: "http://swgserver:8081/api/swagger/include/HeatMap.yaml#/HeatMapReport" + ILSDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/ILSDemod.yaml#/ILSDemodReport" 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 3938261df..daa36f511 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -69,6 +69,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/RTTYDemod.yaml#/RTTYDemodSettings" HeatMapSettings: $ref: "http://swgserver:8081/api/swagger/include/HeatMap.yaml#/HeatMapSettings" + ILSDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/ILSDemod.yaml#/ILSDemodSettings" InterferometerSettings: $ref: "http://swgserver:8081/api/swagger/include/Interferometer.yaml#/InterferometerSettings" IEEE_802_15_4_ModSettings: diff --git a/swagger/sdrangel/api/swagger/include/ILSDemod.yaml b/swagger/sdrangel/api/swagger/include/ILSDemod.yaml new file mode 100644 index 000000000..7b98f7b9a --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/ILSDemod.yaml @@ -0,0 +1,98 @@ +ILSDemodSettings: + description: ILSDemod + properties: + inputFrequencyOffset: + type: integer + format: int64 + rfBandwidth: + type: number + format: float + mode: + description: "(0 for LOC, 1 for G/S)" + type: integer + frequencyIndex: + type: integer + squelch: + type: integer + volume: + type: number + format: float + audioMute: + type: integer + average: + type: integer + ddmUnits: + type: integer + identThreshold: + type: number + format: float + ident: + type: string + runway: + type: string + trueBearing: + type: number + format: float + latitude: + type: string + longitude: + type: string + elevation: + type: integer + glidePath: + type: number + format: float + refHeight: + type: number + format: float + courseWidth: + type: number + format: float + udpEnabled: + description: "Whether to forward DDM to specified UDP port" + type: integer + udpAddress: + description: "UDP address to forward DDM to" + type: string + udpPort: + description: "UDP port to forward DDM to" + type: integer + logFilename: + type: string + logEnabled: + 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" + +ILSDemodReport: + description: ILSDemod + properties: + channelPowerDB: + description: power received in channel (dB) + type: number + format: float + channelSampleRate: + type: integer + diff --git a/swagger/sdrangel/api/swagger/include/Map.yaml b/swagger/sdrangel/api/swagger/include/Map.yaml index e8c9b306c..e89ab20ec 100644 --- a/swagger/sdrangel/api/swagger/include/Map.yaml +++ b/swagger/sdrangel/api/swagger/include/Map.yaml @@ -4,6 +4,9 @@ MapSettings: displayNames: description: Display object names on the map (1 for yes, 0 for no) type: integer + terrain: + description: "Terrain used for 3D map (E.g: 'Ellipsoid' or 'Cesium World Terrain')" + type: string title: type: string rgbColor: @@ -118,7 +121,7 @@ MapItem: type: number format: float altitudeReference: - description: "0 - NONE (Absolute), 1 - CLAMP_TO_GROUND, 2 - RELATIVE_TO_GROUND, 3 - CLIP_TO_GROUND" + description: "0 - NONE (Absolute), 1 - CLAMP_TO_GROUND, 2 - RELATIVE_TO_GROUND, 3 - CLIP_TO_GROUND." type: integer animations: description: "Animations to play" @@ -156,6 +159,12 @@ MapItem: availableUntil: description: "Date and time until after which this item should no longer appear on 3D map" type: string + colorValid: + description: "0 - Use default color, 1 - Use specified color" + type: integer + color: + description: "RGBA for polygon and polyline" + type: integer MapAnimation: description: "Animation to play in the model on the 3D map" diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index b6eb84f09..e47906200 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -76,6 +76,8 @@ SWGChannelReport::SWGChannelReport() { m_rtty_demod_report_isSet = false; heat_map_report = nullptr; m_heat_map_report_isSet = false; + ils_demod_report = nullptr; + m_ils_demod_report_isSet = false; m17_demod_report = nullptr; m_m17_demod_report_isSet = false; m17_mod_report = nullptr; @@ -174,6 +176,8 @@ SWGChannelReport::init() { m_rtty_demod_report_isSet = false; heat_map_report = new SWGHeatMapReport(); m_heat_map_report_isSet = false; + ils_demod_report = new SWGILSDemodReport(); + m_ils_demod_report_isSet = false; m17_demod_report = new SWGM17DemodReport(); m_m17_demod_report_isSet = false; m17_mod_report = new SWGM17ModReport(); @@ -290,6 +294,9 @@ SWGChannelReport::cleanup() { if(heat_map_report != nullptr) { delete heat_map_report; } + if(ils_demod_report != nullptr) { + delete ils_demod_report; + } if(m17_demod_report != nullptr) { delete m17_demod_report; } @@ -414,6 +421,8 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&heat_map_report, pJson["HeatMapReport"], "SWGHeatMapReport", "SWGHeatMapReport"); + ::SWGSDRangel::setValue(&ils_demod_report, pJson["ILSDemodReport"], "SWGILSDemodReport", "SWGILSDemodReport"); + ::SWGSDRangel::setValue(&m17_demod_report, pJson["M17DemodReport"], "SWGM17DemodReport", "SWGM17DemodReport"); ::SWGSDRangel::setValue(&m17_mod_report, pJson["M17ModReport"], "SWGM17ModReport", "SWGM17ModReport"); @@ -544,6 +553,9 @@ SWGChannelReport::asJsonObject() { if((heat_map_report != nullptr) && (heat_map_report->isSet())){ toJsonValue(QString("HeatMapReport"), heat_map_report, obj, QString("SWGHeatMapReport")); } + if((ils_demod_report != nullptr) && (ils_demod_report->isSet())){ + toJsonValue(QString("ILSDemodReport"), ils_demod_report, obj, QString("SWGILSDemodReport")); + } if((m17_demod_report != nullptr) && (m17_demod_report->isSet())){ toJsonValue(QString("M17DemodReport"), m17_demod_report, obj, QString("SWGM17DemodReport")); } @@ -851,6 +863,16 @@ SWGChannelReport::setHeatMapReport(SWGHeatMapReport* heat_map_report) { this->m_heat_map_report_isSet = true; } +SWGILSDemodReport* +SWGChannelReport::getIlsDemodReport() { + return ils_demod_report; +} +void +SWGChannelReport::setIlsDemodReport(SWGILSDemodReport* ils_demod_report) { + this->ils_demod_report = ils_demod_report; + this->m_ils_demod_report_isSet = true; +} + SWGM17DemodReport* SWGChannelReport::getM17DemodReport() { return m17_demod_report; @@ -1138,6 +1160,9 @@ SWGChannelReport::isSet(){ if(heat_map_report && heat_map_report->isSet()){ isObjectUpdated = true; break; } + if(ils_demod_report && ils_demod_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 4d7abbd88..863e38004 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -43,6 +43,7 @@ #include "SWGFreqTrackerReport.h" #include "SWGHeatMapReport.h" #include "SWGIEEE_802_15_4_ModReport.h" +#include "SWGILSDemodReport.h" #include "SWGM17DemodReport.h" #include "SWGM17ModReport.h" #include "SWGNFMDemodReport.h" @@ -157,6 +158,9 @@ public: SWGHeatMapReport* getHeatMapReport(); void setHeatMapReport(SWGHeatMapReport* heat_map_report); + SWGILSDemodReport* getIlsDemodReport(); + void setIlsDemodReport(SWGILSDemodReport* ils_demod_report); + SWGM17DemodReport* getM17DemodReport(); void setM17DemodReport(SWGM17DemodReport* m17_demod_report); @@ -296,6 +300,9 @@ private: SWGHeatMapReport* heat_map_report; bool m_heat_map_report_isSet; + SWGILSDemodReport* ils_demod_report; + bool m_ils_demod_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 476da4f74..849d66b0a 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -88,6 +88,8 @@ SWGChannelSettings::SWGChannelSettings() { m_rtty_demod_settings_isSet = false; heat_map_settings = nullptr; m_heat_map_settings_isSet = false; + ils_demod_settings = nullptr; + m_ils_demod_settings_isSet = false; interferometer_settings = nullptr; m_interferometer_settings_isSet = false; ieee_802_15_4_mod_settings = nullptr; @@ -210,6 +212,8 @@ SWGChannelSettings::init() { m_rtty_demod_settings_isSet = false; heat_map_settings = new SWGHeatMapSettings(); m_heat_map_settings_isSet = false; + ils_demod_settings = new SWGILSDemodSettings(); + m_ils_demod_settings_isSet = false; interferometer_settings = new SWGInterferometerSettings(); m_interferometer_settings_isSet = false; ieee_802_15_4_mod_settings = new SWGIEEE_802_15_4_ModSettings(); @@ -352,6 +356,9 @@ SWGChannelSettings::cleanup() { if(heat_map_settings != nullptr) { delete heat_map_settings; } + if(ils_demod_settings != nullptr) { + delete ils_demod_settings; + } if(interferometer_settings != nullptr) { delete interferometer_settings; } @@ -506,6 +513,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&heat_map_settings, pJson["HeatMapSettings"], "SWGHeatMapSettings", "SWGHeatMapSettings"); + ::SWGSDRangel::setValue(&ils_demod_settings, pJson["ILSDemodSettings"], "SWGILSDemodSettings", "SWGILSDemodSettings"); + ::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"); @@ -666,6 +675,9 @@ SWGChannelSettings::asJsonObject() { if((heat_map_settings != nullptr) && (heat_map_settings->isSet())){ toJsonValue(QString("HeatMapSettings"), heat_map_settings, obj, QString("SWGHeatMapSettings")); } + if((ils_demod_settings != nullptr) && (ils_demod_settings->isSet())){ + toJsonValue(QString("ILSDemodSettings"), ils_demod_settings, obj, QString("SWGILSDemodSettings")); + } if((interferometer_settings != nullptr) && (interferometer_settings->isSet())){ toJsonValue(QString("InterferometerSettings"), interferometer_settings, obj, QString("SWGInterferometerSettings")); } @@ -1051,6 +1063,16 @@ SWGChannelSettings::setHeatMapSettings(SWGHeatMapSettings* heat_map_settings) { this->m_heat_map_settings_isSet = true; } +SWGILSDemodSettings* +SWGChannelSettings::getIlsDemodSettings() { + return ils_demod_settings; +} +void +SWGChannelSettings::setIlsDemodSettings(SWGILSDemodSettings* ils_demod_settings) { + this->ils_demod_settings = ils_demod_settings; + this->m_ils_demod_settings_isSet = true; +} + SWGInterferometerSettings* SWGChannelSettings::getInterferometerSettings() { return interferometer_settings; @@ -1416,6 +1438,9 @@ SWGChannelSettings::isSet(){ if(heat_map_settings && heat_map_settings->isSet()){ isObjectUpdated = true; break; } + if(ils_demod_settings && ils_demod_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 a3c00926b..f0ddcab8d 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -48,6 +48,7 @@ #include "SWGFreqTrackerSettings.h" #include "SWGHeatMapSettings.h" #include "SWGIEEE_802_15_4_ModSettings.h" +#include "SWGILSDemodSettings.h" #include "SWGInterferometerSettings.h" #include "SWGLocalSinkSettings.h" #include "SWGLocalSourceSettings.h" @@ -185,6 +186,9 @@ public: SWGHeatMapSettings* getHeatMapSettings(); void setHeatMapSettings(SWGHeatMapSettings* heat_map_settings); + SWGILSDemodSettings* getIlsDemodSettings(); + void setIlsDemodSettings(SWGILSDemodSettings* ils_demod_settings); + SWGInterferometerSettings* getInterferometerSettings(); void setInterferometerSettings(SWGInterferometerSettings* interferometer_settings); @@ -360,6 +364,9 @@ private: SWGHeatMapSettings* heat_map_settings; bool m_heat_map_settings_isSet; + SWGILSDemodSettings* ils_demod_settings; + bool m_ils_demod_settings_isSet; + SWGInterferometerSettings* interferometer_settings; bool m_interferometer_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGILSDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGILSDemodReport.cpp new file mode 100644 index 000000000..4fb7a9c16 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGILSDemodReport.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 "SWGILSDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGILSDemodReport::SWGILSDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGILSDemodReport::SWGILSDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGILSDemodReport::~SWGILSDemodReport() { + this->cleanup(); +} + +void +SWGILSDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGILSDemodReport::cleanup() { + + +} + +SWGILSDemodReport* +SWGILSDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGILSDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGILSDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGILSDemodReport::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 +SWGILSDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGILSDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGILSDemodReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGILSDemodReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGILSDemodReport::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/SWGILSDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGILSDemodReport.h new file mode 100644 index 000000000..a8ac86315 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGILSDemodReport.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. + */ + +/* + * SWGILSDemodReport.h + * + * ILSDemod + */ + +#ifndef SWGILSDemodReport_H_ +#define SWGILSDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGILSDemodReport: public SWGObject { +public: + SWGILSDemodReport(); + SWGILSDemodReport(QString* json); + virtual ~SWGILSDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGILSDemodReport* 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 /* SWGILSDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGILSDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGILSDemodSettings.cpp new file mode 100644 index 000000000..69aa10b2f --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGILSDemodSettings.cpp @@ -0,0 +1,912 @@ +/** + * 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 "SWGILSDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGILSDemodSettings::SWGILSDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGILSDemodSettings::SWGILSDemodSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + mode = 0; + m_mode_isSet = false; + frequency_index = 0; + m_frequency_index_isSet = false; + squelch = 0; + m_squelch_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + audio_mute = 0; + m_audio_mute_isSet = false; + average = 0; + m_average_isSet = false; + ddm_units = 0; + m_ddm_units_isSet = false; + ident_threshold = 0.0f; + m_ident_threshold_isSet = false; + ident = nullptr; + m_ident_isSet = false; + runway = nullptr; + m_runway_isSet = false; + true_bearing = 0.0f; + m_true_bearing_isSet = false; + latitude = nullptr; + m_latitude_isSet = false; + longitude = nullptr; + m_longitude_isSet = false; + elevation = 0; + m_elevation_isSet = false; + glide_path = 0.0f; + m_glide_path_isSet = false; + ref_height = 0.0f; + m_ref_height_isSet = false; + course_width = 0.0f; + m_course_width_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = nullptr; + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + log_filename = nullptr; + m_log_filename_isSet = false; + log_enabled = 0; + m_log_enabled_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; +} + +SWGILSDemodSettings::~SWGILSDemodSettings() { + this->cleanup(); +} + +void +SWGILSDemodSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + mode = 0; + m_mode_isSet = false; + frequency_index = 0; + m_frequency_index_isSet = false; + squelch = 0; + m_squelch_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + audio_mute = 0; + m_audio_mute_isSet = false; + average = 0; + m_average_isSet = false; + ddm_units = 0; + m_ddm_units_isSet = false; + ident_threshold = 0.0f; + m_ident_threshold_isSet = false; + ident = new QString(""); + m_ident_isSet = false; + runway = new QString(""); + m_runway_isSet = false; + true_bearing = 0.0f; + m_true_bearing_isSet = false; + latitude = new QString(""); + m_latitude_isSet = false; + longitude = new QString(""); + m_longitude_isSet = false; + elevation = 0; + m_elevation_isSet = false; + glide_path = 0.0f; + m_glide_path_isSet = false; + ref_height = 0.0f; + m_ref_height_isSet = false; + course_width = 0.0f; + m_course_width_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = new QString(""); + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + log_filename = new QString(""); + m_log_filename_isSet = false; + log_enabled = 0; + m_log_enabled_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 +SWGILSDemodSettings::cleanup() { + + + + + + + + + + + if(ident != nullptr) { + delete ident; + } + if(runway != nullptr) { + delete runway; + } + + if(latitude != nullptr) { + delete latitude; + } + if(longitude != nullptr) { + delete longitude; + } + + + + + + if(udp_address != nullptr) { + delete udp_address; + } + + if(log_filename != nullptr) { + delete log_filename; + } + + + 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; + } +} + +SWGILSDemodSettings* +SWGILSDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGILSDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&mode, pJson["mode"], "qint32", ""); + + ::SWGSDRangel::setValue(&frequency_index, pJson["frequencyIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&squelch, pJson["squelch"], "qint32", ""); + + ::SWGSDRangel::setValue(&volume, pJson["volume"], "float", ""); + + ::SWGSDRangel::setValue(&audio_mute, pJson["audioMute"], "qint32", ""); + + ::SWGSDRangel::setValue(&average, pJson["average"], "qint32", ""); + + ::SWGSDRangel::setValue(&ddm_units, pJson["ddmUnits"], "qint32", ""); + + ::SWGSDRangel::setValue(&ident_threshold, pJson["identThreshold"], "float", ""); + + ::SWGSDRangel::setValue(&ident, pJson["ident"], "QString", "QString"); + + ::SWGSDRangel::setValue(&runway, pJson["runway"], "QString", "QString"); + + ::SWGSDRangel::setValue(&true_bearing, pJson["trueBearing"], "float", ""); + + ::SWGSDRangel::setValue(&latitude, pJson["latitude"], "QString", "QString"); + + ::SWGSDRangel::setValue(&longitude, pJson["longitude"], "QString", "QString"); + + ::SWGSDRangel::setValue(&elevation, pJson["elevation"], "qint32", ""); + + ::SWGSDRangel::setValue(&glide_path, pJson["glidePath"], "float", ""); + + ::SWGSDRangel::setValue(&ref_height, pJson["refHeight"], "float", ""); + + ::SWGSDRangel::setValue(&course_width, pJson["courseWidth"], "float", ""); + + ::SWGSDRangel::setValue(&udp_enabled, pJson["udpEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_address, pJson["udpAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&log_filename, pJson["logFilename"], "QString", "QString"); + + ::SWGSDRangel::setValue(&log_enabled, pJson["logEnabled"], "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 +SWGILSDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGILSDemodSettings::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_mode_isSet){ + obj->insert("mode", QJsonValue(mode)); + } + if(m_frequency_index_isSet){ + obj->insert("frequencyIndex", QJsonValue(frequency_index)); + } + if(m_squelch_isSet){ + obj->insert("squelch", QJsonValue(squelch)); + } + if(m_volume_isSet){ + obj->insert("volume", QJsonValue(volume)); + } + if(m_audio_mute_isSet){ + obj->insert("audioMute", QJsonValue(audio_mute)); + } + if(m_average_isSet){ + obj->insert("average", QJsonValue(average)); + } + if(m_ddm_units_isSet){ + obj->insert("ddmUnits", QJsonValue(ddm_units)); + } + if(m_ident_threshold_isSet){ + obj->insert("identThreshold", QJsonValue(ident_threshold)); + } + if(ident != nullptr && *ident != QString("")){ + toJsonValue(QString("ident"), ident, obj, QString("QString")); + } + if(runway != nullptr && *runway != QString("")){ + toJsonValue(QString("runway"), runway, obj, QString("QString")); + } + if(m_true_bearing_isSet){ + obj->insert("trueBearing", QJsonValue(true_bearing)); + } + if(latitude != nullptr && *latitude != QString("")){ + toJsonValue(QString("latitude"), latitude, obj, QString("QString")); + } + if(longitude != nullptr && *longitude != QString("")){ + toJsonValue(QString("longitude"), longitude, obj, QString("QString")); + } + if(m_elevation_isSet){ + obj->insert("elevation", QJsonValue(elevation)); + } + if(m_glide_path_isSet){ + obj->insert("glidePath", QJsonValue(glide_path)); + } + if(m_ref_height_isSet){ + obj->insert("refHeight", QJsonValue(ref_height)); + } + if(m_course_width_isSet){ + obj->insert("courseWidth", QJsonValue(course_width)); + } + if(m_udp_enabled_isSet){ + obj->insert("udpEnabled", QJsonValue(udp_enabled)); + } + if(udp_address != nullptr && *udp_address != QString("")){ + toJsonValue(QString("udpAddress"), udp_address, obj, QString("QString")); + } + if(m_udp_port_isSet){ + obj->insert("udpPort", QJsonValue(udp_port)); + } + if(log_filename != nullptr && *log_filename != QString("")){ + toJsonValue(QString("logFilename"), log_filename, obj, QString("QString")); + } + if(m_log_enabled_isSet){ + obj->insert("logEnabled", QJsonValue(log_enabled)); + } + 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 +SWGILSDemodSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGILSDemodSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGILSDemodSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGILSDemodSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +qint32 +SWGILSDemodSettings::getMode() { + return mode; +} +void +SWGILSDemodSettings::setMode(qint32 mode) { + this->mode = mode; + this->m_mode_isSet = true; +} + +qint32 +SWGILSDemodSettings::getFrequencyIndex() { + return frequency_index; +} +void +SWGILSDemodSettings::setFrequencyIndex(qint32 frequency_index) { + this->frequency_index = frequency_index; + this->m_frequency_index_isSet = true; +} + +qint32 +SWGILSDemodSettings::getSquelch() { + return squelch; +} +void +SWGILSDemodSettings::setSquelch(qint32 squelch) { + this->squelch = squelch; + this->m_squelch_isSet = true; +} + +float +SWGILSDemodSettings::getVolume() { + return volume; +} +void +SWGILSDemodSettings::setVolume(float volume) { + this->volume = volume; + this->m_volume_isSet = true; +} + +qint32 +SWGILSDemodSettings::getAudioMute() { + return audio_mute; +} +void +SWGILSDemodSettings::setAudioMute(qint32 audio_mute) { + this->audio_mute = audio_mute; + this->m_audio_mute_isSet = true; +} + +qint32 +SWGILSDemodSettings::getAverage() { + return average; +} +void +SWGILSDemodSettings::setAverage(qint32 average) { + this->average = average; + this->m_average_isSet = true; +} + +qint32 +SWGILSDemodSettings::getDdmUnits() { + return ddm_units; +} +void +SWGILSDemodSettings::setDdmUnits(qint32 ddm_units) { + this->ddm_units = ddm_units; + this->m_ddm_units_isSet = true; +} + +float +SWGILSDemodSettings::getIdentThreshold() { + return ident_threshold; +} +void +SWGILSDemodSettings::setIdentThreshold(float ident_threshold) { + this->ident_threshold = ident_threshold; + this->m_ident_threshold_isSet = true; +} + +QString* +SWGILSDemodSettings::getIdent() { + return ident; +} +void +SWGILSDemodSettings::setIdent(QString* ident) { + this->ident = ident; + this->m_ident_isSet = true; +} + +QString* +SWGILSDemodSettings::getRunway() { + return runway; +} +void +SWGILSDemodSettings::setRunway(QString* runway) { + this->runway = runway; + this->m_runway_isSet = true; +} + +float +SWGILSDemodSettings::getTrueBearing() { + return true_bearing; +} +void +SWGILSDemodSettings::setTrueBearing(float true_bearing) { + this->true_bearing = true_bearing; + this->m_true_bearing_isSet = true; +} + +QString* +SWGILSDemodSettings::getLatitude() { + return latitude; +} +void +SWGILSDemodSettings::setLatitude(QString* latitude) { + this->latitude = latitude; + this->m_latitude_isSet = true; +} + +QString* +SWGILSDemodSettings::getLongitude() { + return longitude; +} +void +SWGILSDemodSettings::setLongitude(QString* longitude) { + this->longitude = longitude; + this->m_longitude_isSet = true; +} + +qint32 +SWGILSDemodSettings::getElevation() { + return elevation; +} +void +SWGILSDemodSettings::setElevation(qint32 elevation) { + this->elevation = elevation; + this->m_elevation_isSet = true; +} + +float +SWGILSDemodSettings::getGlidePath() { + return glide_path; +} +void +SWGILSDemodSettings::setGlidePath(float glide_path) { + this->glide_path = glide_path; + this->m_glide_path_isSet = true; +} + +float +SWGILSDemodSettings::getRefHeight() { + return ref_height; +} +void +SWGILSDemodSettings::setRefHeight(float ref_height) { + this->ref_height = ref_height; + this->m_ref_height_isSet = true; +} + +float +SWGILSDemodSettings::getCourseWidth() { + return course_width; +} +void +SWGILSDemodSettings::setCourseWidth(float course_width) { + this->course_width = course_width; + this->m_course_width_isSet = true; +} + +qint32 +SWGILSDemodSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGILSDemodSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGILSDemodSettings::getUdpAddress() { + return udp_address; +} +void +SWGILSDemodSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGILSDemodSettings::getUdpPort() { + return udp_port; +} +void +SWGILSDemodSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + +QString* +SWGILSDemodSettings::getLogFilename() { + return log_filename; +} +void +SWGILSDemodSettings::setLogFilename(QString* log_filename) { + this->log_filename = log_filename; + this->m_log_filename_isSet = true; +} + +qint32 +SWGILSDemodSettings::getLogEnabled() { + return log_enabled; +} +void +SWGILSDemodSettings::setLogEnabled(qint32 log_enabled) { + this->log_enabled = log_enabled; + this->m_log_enabled_isSet = true; +} + +qint32 +SWGILSDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGILSDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGILSDemodSettings::getTitle() { + return title; +} +void +SWGILSDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGILSDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGILSDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGILSDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGILSDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGILSDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGILSDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGILSDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGILSDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGILSDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGILSDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGILSDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGILSDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +SWGGLScope* +SWGILSDemodSettings::getScopeConfig() { + return scope_config; +} +void +SWGILSDemodSettings::setScopeConfig(SWGGLScope* scope_config) { + this->scope_config = scope_config; + this->m_scope_config_isSet = true; +} + +SWGChannelMarker* +SWGILSDemodSettings::getChannelMarker() { + return channel_marker; +} +void +SWGILSDemodSettings::setChannelMarker(SWGChannelMarker* channel_marker) { + this->channel_marker = channel_marker; + this->m_channel_marker_isSet = true; +} + +SWGRollupState* +SWGILSDemodSettings::getRollupState() { + return rollup_state; +} +void +SWGILSDemodSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGILSDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_mode_isSet){ + isObjectUpdated = true; break; + } + if(m_frequency_index_isSet){ + isObjectUpdated = true; break; + } + if(m_squelch_isSet){ + isObjectUpdated = true; break; + } + if(m_volume_isSet){ + isObjectUpdated = true; break; + } + if(m_audio_mute_isSet){ + isObjectUpdated = true; break; + } + if(m_average_isSet){ + isObjectUpdated = true; break; + } + if(m_ddm_units_isSet){ + isObjectUpdated = true; break; + } + if(m_ident_threshold_isSet){ + isObjectUpdated = true; break; + } + if(ident && *ident != QString("")){ + isObjectUpdated = true; break; + } + if(runway && *runway != QString("")){ + isObjectUpdated = true; break; + } + if(m_true_bearing_isSet){ + isObjectUpdated = true; break; + } + if(latitude && *latitude != QString("")){ + isObjectUpdated = true; break; + } + if(longitude && *longitude != QString("")){ + isObjectUpdated = true; break; + } + if(m_elevation_isSet){ + isObjectUpdated = true; break; + } + if(m_glide_path_isSet){ + isObjectUpdated = true; break; + } + if(m_ref_height_isSet){ + isObjectUpdated = true; break; + } + if(m_course_width_isSet){ + isObjectUpdated = true; break; + } + if(m_udp_enabled_isSet){ + isObjectUpdated = true; break; + } + if(udp_address && *udp_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_udp_port_isSet){ + isObjectUpdated = true; break; + } + if(log_filename && *log_filename != QString("")){ + isObjectUpdated = true; break; + } + if(m_log_enabled_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/SWGILSDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGILSDemodSettings.h new file mode 100644 index 000000000..dda600b17 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGILSDemodSettings.h @@ -0,0 +1,266 @@ +/** + * 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. + */ + +/* + * SWGILSDemodSettings.h + * + * ILSDemod + */ + +#ifndef SWGILSDemodSettings_H_ +#define SWGILSDemodSettings_H_ + +#include + + +#include "SWGChannelMarker.h" +#include "SWGGLScope.h" +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGILSDemodSettings: public SWGObject { +public: + SWGILSDemodSettings(); + SWGILSDemodSettings(QString* json); + virtual ~SWGILSDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGILSDemodSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + qint32 getMode(); + void setMode(qint32 mode); + + qint32 getFrequencyIndex(); + void setFrequencyIndex(qint32 frequency_index); + + qint32 getSquelch(); + void setSquelch(qint32 squelch); + + float getVolume(); + void setVolume(float volume); + + qint32 getAudioMute(); + void setAudioMute(qint32 audio_mute); + + qint32 getAverage(); + void setAverage(qint32 average); + + qint32 getDdmUnits(); + void setDdmUnits(qint32 ddm_units); + + float getIdentThreshold(); + void setIdentThreshold(float ident_threshold); + + QString* getIdent(); + void setIdent(QString* ident); + + QString* getRunway(); + void setRunway(QString* runway); + + float getTrueBearing(); + void setTrueBearing(float true_bearing); + + QString* getLatitude(); + void setLatitude(QString* latitude); + + QString* getLongitude(); + void setLongitude(QString* longitude); + + qint32 getElevation(); + void setElevation(qint32 elevation); + + float getGlidePath(); + void setGlidePath(float glide_path); + + float getRefHeight(); + void setRefHeight(float ref_height); + + float getCourseWidth(); + void setCourseWidth(float course_width); + + qint32 getUdpEnabled(); + void setUdpEnabled(qint32 udp_enabled); + + QString* getUdpAddress(); + void setUdpAddress(QString* udp_address); + + qint32 getUdpPort(); + void setUdpPort(qint32 udp_port); + + QString* getLogFilename(); + void setLogFilename(QString* log_filename); + + qint32 getLogEnabled(); + void setLogEnabled(qint32 log_enabled); + + 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; + + qint32 mode; + bool m_mode_isSet; + + qint32 frequency_index; + bool m_frequency_index_isSet; + + qint32 squelch; + bool m_squelch_isSet; + + float volume; + bool m_volume_isSet; + + qint32 audio_mute; + bool m_audio_mute_isSet; + + qint32 average; + bool m_average_isSet; + + qint32 ddm_units; + bool m_ddm_units_isSet; + + float ident_threshold; + bool m_ident_threshold_isSet; + + QString* ident; + bool m_ident_isSet; + + QString* runway; + bool m_runway_isSet; + + float true_bearing; + bool m_true_bearing_isSet; + + QString* latitude; + bool m_latitude_isSet; + + QString* longitude; + bool m_longitude_isSet; + + qint32 elevation; + bool m_elevation_isSet; + + float glide_path; + bool m_glide_path_isSet; + + float ref_height; + bool m_ref_height_isSet; + + float course_width; + bool m_course_width_isSet; + + qint32 udp_enabled; + bool m_udp_enabled_isSet; + + QString* udp_address; + bool m_udp_address_isSet; + + qint32 udp_port; + bool m_udp_port_isSet; + + QString* log_filename; + bool m_log_filename_isSet; + + qint32 log_enabled; + bool m_log_enabled_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 /* SWGILSDemodSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp b/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp index 0b00c0769..439be987c 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem.cpp @@ -90,6 +90,10 @@ SWGMapItem::SWGMapItem() { m_extruded_height_isSet = false; available_until = nullptr; m_available_until_isSet = false; + color_valid = 0; + m_color_valid_isSet = false; + color = 0; + m_color_isSet = false; } SWGMapItem::~SWGMapItem() { @@ -160,6 +164,10 @@ SWGMapItem::init() { m_extruded_height_isSet = false; available_until = new QString(""); m_available_until_isSet = false; + color_valid = 0; + m_color_valid_isSet = false; + color = 0; + m_color_isSet = false; } void @@ -235,6 +243,8 @@ SWGMapItem::cleanup() { if(available_until != nullptr) { delete available_until; } + + } SWGMapItem* @@ -310,6 +320,10 @@ SWGMapItem::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&available_until, pJson["availableUntil"], "QString", "QString"); + ::SWGSDRangel::setValue(&color_valid, pJson["colorValid"], "qint32", ""); + + ::SWGSDRangel::setValue(&color, pJson["color"], "qint32", ""); + } QString @@ -419,6 +433,12 @@ SWGMapItem::asJsonObject() { if(available_until != nullptr && *available_until != QString("")){ toJsonValue(QString("availableUntil"), available_until, obj, QString("QString")); } + if(m_color_valid_isSet){ + obj->insert("colorValid", QJsonValue(color_valid)); + } + if(m_color_isSet){ + obj->insert("color", QJsonValue(color)); + } return obj; } @@ -733,6 +753,26 @@ SWGMapItem::setAvailableUntil(QString* available_until) { this->m_available_until_isSet = true; } +qint32 +SWGMapItem::getColorValid() { + return color_valid; +} +void +SWGMapItem::setColorValid(qint32 color_valid) { + this->color_valid = color_valid; + this->m_color_valid_isSet = true; +} + +qint32 +SWGMapItem::getColor() { + return color; +} +void +SWGMapItem::setColor(qint32 color) { + this->color = color; + this->m_color_isSet = true; +} + bool SWGMapItem::isSet(){ @@ -831,6 +871,12 @@ SWGMapItem::isSet(){ if(available_until && *available_until != QString("")){ isObjectUpdated = true; break; } + if(m_color_valid_isSet){ + isObjectUpdated = true; break; + } + if(m_color_isSet){ + isObjectUpdated = true; break; + } }while(false); return isObjectUpdated; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem.h b/swagger/sdrangel/code/qt5/client/SWGMapItem.h index b807fcaa5..99e36836d 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem.h @@ -138,6 +138,12 @@ public: QString* getAvailableUntil(); void setAvailableUntil(QString* available_until); + qint32 getColorValid(); + void setColorValid(qint32 color_valid); + + qint32 getColor(); + void setColor(qint32 color); + virtual bool isSet() override; @@ -235,6 +241,12 @@ private: QString* available_until; bool m_available_until_isSet; + qint32 color_valid; + bool m_color_valid_isSet; + + qint32 color; + bool m_color_isSet; + }; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp index 5d357c82b..5dfa91130 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.cpp @@ -90,6 +90,10 @@ SWGMapItem_2::SWGMapItem_2() { m_extruded_height_isSet = false; available_until = nullptr; m_available_until_isSet = false; + color_valid = 0; + m_color_valid_isSet = false; + color = 0; + m_color_isSet = false; } SWGMapItem_2::~SWGMapItem_2() { @@ -160,6 +164,10 @@ SWGMapItem_2::init() { m_extruded_height_isSet = false; available_until = new QString(""); m_available_until_isSet = false; + color_valid = 0; + m_color_valid_isSet = false; + color = 0; + m_color_isSet = false; } void @@ -235,6 +243,8 @@ SWGMapItem_2::cleanup() { if(available_until != nullptr) { delete available_until; } + + } SWGMapItem_2* @@ -310,6 +320,10 @@ SWGMapItem_2::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&available_until, pJson["availableUntil"], "QString", "QString"); + ::SWGSDRangel::setValue(&color_valid, pJson["colorValid"], "qint32", ""); + + ::SWGSDRangel::setValue(&color, pJson["color"], "qint32", ""); + } QString @@ -419,6 +433,12 @@ SWGMapItem_2::asJsonObject() { if(available_until != nullptr && *available_until != QString("")){ toJsonValue(QString("availableUntil"), available_until, obj, QString("QString")); } + if(m_color_valid_isSet){ + obj->insert("colorValid", QJsonValue(color_valid)); + } + if(m_color_isSet){ + obj->insert("color", QJsonValue(color)); + } return obj; } @@ -733,6 +753,26 @@ SWGMapItem_2::setAvailableUntil(QString* available_until) { this->m_available_until_isSet = true; } +qint32 +SWGMapItem_2::getColorValid() { + return color_valid; +} +void +SWGMapItem_2::setColorValid(qint32 color_valid) { + this->color_valid = color_valid; + this->m_color_valid_isSet = true; +} + +qint32 +SWGMapItem_2::getColor() { + return color; +} +void +SWGMapItem_2::setColor(qint32 color) { + this->color = color; + this->m_color_isSet = true; +} + bool SWGMapItem_2::isSet(){ @@ -831,6 +871,12 @@ SWGMapItem_2::isSet(){ if(available_until && *available_until != QString("")){ isObjectUpdated = true; break; } + if(m_color_valid_isSet){ + isObjectUpdated = true; break; + } + if(m_color_isSet){ + isObjectUpdated = true; break; + } }while(false); return isObjectUpdated; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h index fdf07802f..e5c7a6722 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapItem_2.h @@ -138,6 +138,12 @@ public: QString* getAvailableUntil(); void setAvailableUntil(QString* available_until); + qint32 getColorValid(); + void setColorValid(qint32 color_valid); + + qint32 getColor(); + void setColor(qint32 color); + virtual bool isSet() override; @@ -235,6 +241,12 @@ private: QString* available_until; bool m_available_until_isSet; + qint32 color_valid; + bool m_color_valid_isSet; + + qint32 color; + bool m_color_isSet; + }; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGMapSettings.cpp index 7dc1b36d5..336f11aa6 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGMapSettings.cpp @@ -30,6 +30,8 @@ SWGMapSettings::SWGMapSettings(QString* json) { SWGMapSettings::SWGMapSettings() { display_names = 0; m_display_names_isSet = false; + terrain = nullptr; + m_terrain_isSet = false; title = nullptr; m_title_isSet = false; rgb_color = 0; @@ -56,6 +58,8 @@ void SWGMapSettings::init() { display_names = 0; m_display_names_isSet = false; + terrain = new QString(""); + m_terrain_isSet = false; title = new QString(""); m_title_isSet = false; rgb_color = 0; @@ -77,6 +81,9 @@ SWGMapSettings::init() { void SWGMapSettings::cleanup() { + if(terrain != nullptr) { + delete terrain; + } if(title != nullptr) { delete title; } @@ -106,6 +113,8 @@ void SWGMapSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&display_names, pJson["displayNames"], "qint32", ""); + ::SWGSDRangel::setValue(&terrain, pJson["terrain"], "QString", "QString"); + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); @@ -141,6 +150,9 @@ SWGMapSettings::asJsonObject() { if(m_display_names_isSet){ obj->insert("displayNames", QJsonValue(display_names)); } + if(terrain != nullptr && *terrain != QString("")){ + toJsonValue(QString("terrain"), terrain, obj, QString("QString")); + } if(title != nullptr && *title != QString("")){ toJsonValue(QString("title"), title, obj, QString("QString")); } @@ -179,6 +191,16 @@ SWGMapSettings::setDisplayNames(qint32 display_names) { this->m_display_names_isSet = true; } +QString* +SWGMapSettings::getTerrain() { + return terrain; +} +void +SWGMapSettings::setTerrain(QString* terrain) { + this->terrain = terrain; + this->m_terrain_isSet = true; +} + QString* SWGMapSettings::getTitle() { return title; @@ -267,6 +289,9 @@ SWGMapSettings::isSet(){ if(m_display_names_isSet){ isObjectUpdated = true; break; } + if(terrain && *terrain != QString("")){ + isObjectUpdated = true; break; + } if(title && *title != QString("")){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGMapSettings.h b/swagger/sdrangel/code/qt5/client/SWGMapSettings.h index 6a6fb0282..df3cde740 100644 --- a/swagger/sdrangel/code/qt5/client/SWGMapSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGMapSettings.h @@ -46,6 +46,9 @@ public: qint32 getDisplayNames(); void setDisplayNames(qint32 display_names); + QString* getTerrain(); + void setTerrain(QString* terrain); + QString* getTitle(); void setTitle(QString* title); @@ -77,6 +80,9 @@ private: qint32 display_names; bool m_display_names_isSet; + QString* terrain; + bool m_terrain_isSet; + QString* title; bool m_title_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index 0d5a400c5..560d7894b 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -160,6 +160,8 @@ #include "SWGIEEE_802_15_4_ModActions.h" #include "SWGIEEE_802_15_4_ModReport.h" #include "SWGIEEE_802_15_4_ModSettings.h" +#include "SWGILSDemodReport.h" +#include "SWGILSDemodSettings.h" #include "SWGInstanceChannelsResponse.h" #include "SWGInstanceConfigResponse.h" #include "SWGInstanceDevicesResponse.h" @@ -1081,6 +1083,16 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGILSDemodReport").compare(type) == 0) { + SWGILSDemodReport *obj = new SWGILSDemodReport(); + obj->init(); + return obj; + } + if(QString("SWGILSDemodSettings").compare(type) == 0) { + SWGILSDemodSettings *obj = new SWGILSDemodSettings(); + obj->init(); + return obj; + } if(QString("SWGInstanceChannelsResponse").compare(type) == 0) { SWGInstanceChannelsResponse *obj = new SWGInstanceChannelsResponse(); obj->init();