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..ee8932517 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodgui.cpp @@ -0,0 +1,1533 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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; + bool ellipsoid = false; + 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; + qDebug() << Units::radiansToDegrees(atan(h/d)); + 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..ccc203fa8 --- /dev/null +++ b/plugins/channelrx/demodils/ilsdemodsettings.cpp @@ -0,0 +1,237 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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; + int tmp; + 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..a6679641b --- /dev/null +++ b/plugins/channelrx/demodils/readme.md @@ -0,0 +1,243 @@ +

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

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 or Glideslope signal is to be demodulated. The localizer provides horizontal guidance and the glideslope 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. +Glideslopes 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 Sun 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). + +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 double click 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/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/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/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();