From a2cfe07dee9ed534d1d51c0fab88af926871c5c6 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:14:09 +0000 Subject: [PATCH 01/12] Add RTTY demodulator --- doc/img/RTTYDemod_plugin.png | Bin 0 -> 32734 bytes plugins/channelrx/demodrtty/CMakeLists.txt | 63 + plugins/channelrx/demodrtty/readme.md | 103 ++ plugins/channelrx/demodrtty/rttydemod.cpp | 780 ++++++++++ plugins/channelrx/demodrtty/rttydemod.h | 218 +++ .../channelrx/demodrtty/rttydemodbaseband.cpp | 181 +++ .../channelrx/demodrtty/rttydemodbaseband.h | 103 ++ plugins/channelrx/demodrtty/rttydemodgui.cpp | 669 +++++++++ plugins/channelrx/demodrtty/rttydemodgui.h | 130 ++ plugins/channelrx/demodrtty/rttydemodgui.ui | 1323 +++++++++++++++++ .../channelrx/demodrtty/rttydemodplugin.cpp | 93 ++ plugins/channelrx/demodrtty/rttydemodplugin.h | 50 + .../channelrx/demodrtty/rttydemodsettings.cpp | 213 +++ .../channelrx/demodrtty/rttydemodsettings.h | 88 ++ plugins/channelrx/demodrtty/rttydemodsink.cpp | 670 +++++++++ plugins/channelrx/demodrtty/rttydemodsink.h | 170 +++ .../demodrtty/rttydemodwebapiadapter.cpp | 52 + .../demodrtty/rttydemodwebapiadapter.h | 50 + sdrbase/util/baudot.cpp | 192 +++ sdrbase/util/baudot.h | 77 + sdrbase/util/movingmaximum.h | 99 ++ sdrbase/webapi/webapirequestmapper.cpp | 14 + sdrbase/webapi/webapiutils.cpp | 4 + 23 files changed, 5342 insertions(+) create mode 100644 doc/img/RTTYDemod_plugin.png create mode 100644 plugins/channelrx/demodrtty/CMakeLists.txt create mode 100644 plugins/channelrx/demodrtty/readme.md create mode 100644 plugins/channelrx/demodrtty/rttydemod.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemod.h create mode 100644 plugins/channelrx/demodrtty/rttydemodbaseband.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemodbaseband.h create mode 100644 plugins/channelrx/demodrtty/rttydemodgui.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemodgui.h create mode 100644 plugins/channelrx/demodrtty/rttydemodgui.ui create mode 100644 plugins/channelrx/demodrtty/rttydemodplugin.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemodplugin.h create mode 100644 plugins/channelrx/demodrtty/rttydemodsettings.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemodsettings.h create mode 100644 plugins/channelrx/demodrtty/rttydemodsink.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemodsink.h create mode 100644 plugins/channelrx/demodrtty/rttydemodwebapiadapter.cpp create mode 100644 plugins/channelrx/demodrtty/rttydemodwebapiadapter.h create mode 100644 sdrbase/util/baudot.cpp create mode 100644 sdrbase/util/baudot.h create mode 100644 sdrbase/util/movingmaximum.h diff --git a/doc/img/RTTYDemod_plugin.png b/doc/img/RTTYDemod_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..58acef1fd5864ba3ac1732b4f7979b64170c6557 GIT binary patch literal 32734 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 literal 0 HcmV?d00001 diff --git a/plugins/channelrx/demodrtty/CMakeLists.txt b/plugins/channelrx/demodrtty/CMakeLists.txt new file mode 100644 index 000000000..87d5c3a7f --- /dev/null +++ b/plugins/channelrx/demodrtty/CMakeLists.txt @@ -0,0 +1,63 @@ +project(demodrtty) + +set(demodrtty_SOURCES + rttydemod.cpp + rttydemodsettings.cpp + rttydemodbaseband.cpp + rttydemodsink.cpp + rttydemodplugin.cpp + rttydemodwebapiadapter.cpp +) + +set(demodrtty_HEADERS + rttydemod.h + rttydemodsettings.h + rttydemodbaseband.h + rttydemodsink.h + rttydemodplugin.h + rttydemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(demodrtty_SOURCES + ${demodrtty_SOURCES} + rttydemodgui.cpp + rttydemodgui.ui + ) + set(demodrtty_HEADERS + ${demodrtty_HEADERS} + rttydemodgui.h + ) + + set(TARGET_NAME demodrtty) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodrttysrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demodrtty_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/demodrtty/readme.md b/plugins/channelrx/demodrtty/readme.md new file mode 100644 index 000000000..34f55893a --- /dev/null +++ b/plugins/channelrx/demodrtty/readme.md @@ -0,0 +1,103 @@ +

RTTY demodulator plugin

+ +

Introduction

+ +This plugin can be used to demodulate RTTY (Radioteletype) transmissions. +RTTY using BFSK (Binary Frequency Shift Keying), where transmission of data alternates between two frequencies, +the mark frequency and the space frequency. The RTTY Demodulor should be centered in between these frequencies. +The baud rate, frequency shift (difference between mark and space frequencies), bandwidth and baudot character set are configurable. + +

Interface

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

+ +From the presets dropdown, you can select common baud rate and frequency shift settings, or choose Custom to set these individually. + +

5: Baud rate

+ +Specifies the baud rate, in symbols per second. +The tooltip will display an estimate of the received baud rate (Which will be accurate to around 5 baud), providing that the frequency shift has been set correctly. + +

6: Frequency shift

+ +Specifies the frequency shift in Hertz between the mark frequency and the space frequency. +The tooltip will display an estimate of the frequency shift (Which will be accurate to around 10-20Hz), assuming that the bandwidth has been set wide enough to contain the signal. + +

7: 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 mark and space frequencies and sidebands, +but not so wide to accept noise or adjacent signals. + +

8: UDP

+ +When checked, received characters are forwarded to the specified UDP address (9) and port (10). + +

9: UDP address

+ +IP address of the host to forward received characters to via UDP. + +

10: UDP port

+ +UDP port number to forward received characters to. + +

11: Squelch

+ +Sets the squelch power. Characters received with average power lower than this setting will be discarded. + +

12: Baudot Character Set

+ +The baudot character set dropdown determines how the received Baudot encodings will be mapped to Unicode characters. The following character sets are supported: + +* ITA 2 +* UK +* European +* US +* Russian +* Murray + +

13: Bit ordering

+ +Specifies whether bits are transmitted least-significant-bit first (LSB) or most-significant-bit first (MSB). + +

14: Mark/Space Frequency

+ +When unchecked, the mark frequency is the higher frequency, when checked space frequency is higher. + +

15: Suppress CR LF

+ +When checked the CR CR LF sequence is just displayed as CR. + +

16: Unshift on Space

+ +When checked, the Baudot character set will shift to letters when a space character (' ') is received. + +

17: Start/stop Logging Messages to .txt File

+ +When checked, writes all received characters to the .txt file specified by (16). + +

18: .txt Log Filename

+ +Click to specify the name of the .txt file which received characters are logged to. + +

19: Received Text

+ +The received text area shows characters as they are received. + diff --git a/plugins/channelrx/demodrtty/rttydemod.cpp b/plugins/channelrx/demodrtty/rttydemod.cpp new file mode 100644 index 000000000..a36cf3414 --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemod.cpp @@ -0,0 +1,780 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "rttydemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGRTTYDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGMapItem.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "settings/serializable.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(RttyDemod::MsgConfigureRttyDemod, Message) +MESSAGE_CLASS_DEFINITION(RttyDemod::MsgCharacter, Message) +MESSAGE_CLASS_DEFINITION(RttyDemod::MsgModeEstimate, Message) + +const char * const RttyDemod::m_channelIdURI = "sdrangel.channel.rttydemod"; +const char * const RttyDemod::m_channelId = "RTTYDemod"; + +RttyDemod::RttyDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new RttyDemodBaseband(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, + &RttyDemod::networkManagerFinished + ); + QObject::connect( + this, + &ChannelAPI::indexInDeviceSetChanged, + this, + &RttyDemod::handleIndexInDeviceSetChanged + ); +} + +RttyDemod::~RttyDemod() +{ + qDebug("RttyDemod::~RttyDemod"); + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &RttyDemod::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +void RttyDemod::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 RttyDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void RttyDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void RttyDemod::start() +{ + qDebug("RttyDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + RttyDemodBaseband::MsgConfigureRttyDemodBaseband *msg = RttyDemodBaseband::MsgConfigureRttyDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void RttyDemod::stop() +{ + qDebug("RttyDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool RttyDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureRttyDemod::match(cmd)) + { + MsgConfigureRttyDemod& cfg = (MsgConfigureRttyDemod&) cmd; + qDebug() << "RttyDemod::handleMessage: MsgConfigureRttyDemod"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "RttyDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + // Forward to GUI if any + if (m_guiMessageQueue) { + m_guiMessageQueue->push(new DSPSignalNotification(notif)); + } + + return true; + } + else if (RttyDemod::MsgCharacter::match(cmd)) + { + // Forward to GUI + RttyDemod::MsgCharacter& report = (RttyDemod::MsgCharacter&)cmd; + if (getMessageQueueToGUI()) + { + RttyDemod::MsgCharacter *msg = new RttyDemod::MsgCharacter(report); + getMessageQueueToGUI()->push(msg); + } + + // Forward via UDP + if (m_settings.m_udpEnabled) + { + QByteArray bytes = report.getCharacter().toUtf8(); + m_udpSocket.writeDatagram(bytes, bytes.size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + + // Write to log file + if (m_logFile.isOpen()) { + m_logStream << report.getCharacter(); + } + + return true; + } + else if (RttyDemod::MsgModeEstimate::match(cmd)) + { + // Forward to GUI + RttyDemod::MsgModeEstimate& report = (RttyDemod::MsgModeEstimate&)cmd; + if (getMessageQueueToGUI()) + { + RttyDemod::MsgModeEstimate *msg = new RttyDemod::MsgModeEstimate(report); + getMessageQueueToGUI()->push(msg); + } + + return true; + } + else if (MainCore::MsgChannelDemodQuery::match(cmd)) + { + qDebug() << "RttyDemod::handleMessage: MsgChannelDemodQuery"; + sendSampleRateToDemodAnalyzer(); + + return true; + } + else + { + return false; + } +} + +ScopeVis *RttyDemod::getScopeSink() +{ + return m_basebandSink->getScopeSink(); +} + +void RttyDemod::setCenterFrequency(qint64 frequency) +{ + RttyDemodSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureRttyDemod *msgToGUI = MsgConfigureRttyDemod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void RttyDemod::applySettings(const RttyDemodSettings& settings, bool force) +{ + qDebug() << "RttyDemod::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_baudRate != m_settings.m_baudRate) || force) { + reverseAPIKeys.append("baudRate"); + } + if ((settings.m_frequencyShift != m_settings.m_frequencyShift) || force) { + reverseAPIKeys.append("frequencyShift"); + } + 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_characterSet != m_settings.m_characterSet) || force) { + reverseAPIKeys.append("characterSet"); + } + if ((settings.m_suppressCRLF != m_settings.m_suppressCRLF) || force) { + reverseAPIKeys.append("suppressCRLF"); + } + if ((settings.m_unshiftOnSpace != m_settings.m_unshiftOnSpace) || force) { + reverseAPIKeys.append("unshiftOnSpace"); + } + if ((settings.m_msbFirst != m_settings.m_msbFirst) || force) { + reverseAPIKeys.append("msbFirst"); + } + if ((settings.m_spaceHigh != m_settings.m_spaceHigh) || force) { + reverseAPIKeys.append("spaceHigh"); + } + if ((settings.m_squelch != m_settings.m_squelch) || force) { + reverseAPIKeys.append("squelch"); + } + 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"); + } + + RttyDemodBaseband::MsgConfigureRttyDemodBaseband *msg = RttyDemodBaseband::MsgConfigureRttyDemodBaseband::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() << "RttyDemod::applySettings - Logging to: " << settings.m_logFilename; + m_logStream.setDevice(&m_logFile); + } + else + { + qDebug() << "RttyDemod::applySettings - Unable to open log file: " << settings.m_logFilename; + } + } + } + + m_settings = settings; +} + +void RttyDemod::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, + RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE + ); + messageQueue->push(msg); + } + } +} + +QByteArray RttyDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool RttyDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureRttyDemod *msg = MsgConfigureRttyDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureRttyDemod *msg = MsgConfigureRttyDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int RttyDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRttyDemodSettings(new SWGSDRangel::SWGRTTYDemodSettings()); + response.getRttyDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int RttyDemod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int RttyDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + RttyDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureRttyDemod *msg = MsgConfigureRttyDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("RttyDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureRttyDemod *msgToGUI = MsgConfigureRttyDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +int RttyDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRttyDemodReport(new SWGSDRangel::SWGRTTYDemodReport()); + response.getRttyDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void RttyDemod::webapiUpdateChannelSettings( + RttyDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getRttyDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getRttyDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("baudRate")) { + settings.m_baudRate = response.getRttyDemodSettings()->getBaudRate(); + } + if (channelSettingsKeys.contains("frequencyShift")) { + settings.m_frequencyShift = response.getRttyDemodSettings()->getFrequencyShift(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getRttyDemodSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getRttyDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getRttyDemodSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("characterSet")) { + settings.m_characterSet = (Baudot::CharacterSet)response.getRttyDemodSettings()->getCharacterSet(); + } + if (channelSettingsKeys.contains("suppressCRLF")) { + settings.m_suppressCRLF = response.getRttyDemodSettings()->getSuppressCrlf(); + } + if (channelSettingsKeys.contains("unshiftOnSpace")) { + settings.m_unshiftOnSpace = response.getRttyDemodSettings()->getUnshiftOnSpace(); + } + if (channelSettingsKeys.contains("msbFirst")) { + settings.m_msbFirst = response.getRttyDemodSettings()->getMsbFirst(); + } + if (channelSettingsKeys.contains("spaceHigh")) { + settings.m_spaceHigh = response.getRttyDemodSettings()->getSpaceHigh(); + } + if (channelSettingsKeys.contains("squelch")) { + settings.m_squelch = response.getRttyDemodSettings()->getSquelch(); + } + 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.getRttyDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getRttyDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getRttyDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getRttyDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getRttyDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getRttyDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getRttyDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getRttyDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) { + settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getRttyDemodSettings()->getScopeConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getRttyDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getRttyDemodSettings()->getRollupState()); + } +} + +void RttyDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const RttyDemodSettings& settings) +{ + response.getRttyDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getRttyDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getRttyDemodSettings()->setBaudRate(settings.m_baudRate); + response.getRttyDemodSettings()->setFrequencyShift(settings.m_frequencyShift); + response.getRttyDemodSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getRttyDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getRttyDemodSettings()->setUdpPort(settings.m_udpPort); + response.getRttyDemodSettings()->setCharacterSet(settings.m_characterSet); + response.getRttyDemodSettings()->setSuppressCrlf(settings.m_suppressCRLF); + response.getRttyDemodSettings()->setUnshiftOnSpace(settings.m_unshiftOnSpace); + response.getRttyDemodSettings()->setMsbFirst(settings.m_msbFirst); + response.getRttyDemodSettings()->setSpaceHigh(settings.m_spaceHigh); + response.getRttyDemodSettings()->setSquelch(settings.m_squelch); + response.getRttyDemodSettings()->setLogFilename(new QString(settings.m_logFilename)); + response.getRttyDemodSettings()->setLogEnabled(settings.m_logEnabled); + + response.getRttyDemodSettings()->setRgbColor(settings.m_rgbColor); + if (response.getRttyDemodSettings()->getTitle()) { + *response.getRttyDemodSettings()->getTitle() = settings.m_title; + } else { + response.getRttyDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getRttyDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getRttyDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getRttyDemodSettings()->getReverseApiAddress()) { + *response.getRttyDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getRttyDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getRttyDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getRttyDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getRttyDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_scopeGUI) + { + if (response.getRttyDemodSettings()->getScopeConfig()) + { + settings.m_scopeGUI->formatTo(response.getRttyDemodSettings()->getScopeConfig()); + } + else + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + response.getRttyDemodSettings()->setScopeConfig(swgGLScope); + } + } + if (settings.m_channelMarker) + { + if (response.getRttyDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getRttyDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getRttyDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getRttyDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getRttyDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getRttyDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void RttyDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + + response.getRttyDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg)); + response.getRttyDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate()); +} + +void RttyDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const RttyDemodSettings& 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 RttyDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const RttyDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("RttyDemod")); + swgChannelSettings->setRttyDemodSettings(new SWGSDRangel::SWGRTTYDemodSettings()); + SWGSDRangel::SWGRTTYDemodSettings *swgRttyDemodSettings = swgChannelSettings->getRttyDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgRttyDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgRttyDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("baudRate") || force) { + swgRttyDemodSettings->setBaudRate(settings.m_baudRate); + } + if (channelSettingsKeys.contains("frequencyShift") || force) { + swgRttyDemodSettings->setFrequencyShift(settings.m_frequencyShift); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgRttyDemodSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgRttyDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgRttyDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("characterSet") || force) { + swgRttyDemodSettings->setCharacterSet(settings.m_characterSet); + } + if (channelSettingsKeys.contains("suppressCRLF") || force) { + swgRttyDemodSettings->setSuppressCrlf(settings.m_suppressCRLF); + } + if (channelSettingsKeys.contains("unshiftOnSpace") || force) { + swgRttyDemodSettings->setUnshiftOnSpace(settings.m_unshiftOnSpace); + } + if (channelSettingsKeys.contains("msbFirst") || force) { + swgRttyDemodSettings->setMsbFirst(settings.m_msbFirst); + } + if (channelSettingsKeys.contains("spaceHigh") || force) { + swgRttyDemodSettings->setSpaceHigh(settings.m_spaceHigh); + } + if (channelSettingsKeys.contains("squelch") || force) { + swgRttyDemodSettings->setSquelch(settings.m_squelch); + } + if (channelSettingsKeys.contains("logFilename") || force) { + swgRttyDemodSettings->setLogFilename(new QString(settings.m_logFilename)); + } + if (channelSettingsKeys.contains("logEnabled") || force) { + swgRttyDemodSettings->setLogEnabled(settings.m_logEnabled); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgRttyDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgRttyDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgRttyDemodSettings->setStreamIndex(settings.m_streamIndex); + } + + if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force)) + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + swgRttyDemodSettings->setScopeConfig(swgGLScope); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgRttyDemodSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgRttyDemodSettings->setRollupState(swgRollupState); + } +} + +void RttyDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "RttyDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("RttyDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void RttyDemod::handleIndexInDeviceSetChanged(int index) +{ + if (index < 0) { + return; + } + + QString fifoLabel = QString("%1 [%2:%3]") + .arg(m_channelId) + .arg(m_deviceAPI->getDeviceSetIndex()) + .arg(index); + m_basebandSink->setFifoLabel(fifoLabel); +} + diff --git a/plugins/channelrx/demodrtty/rttydemod.h b/plugins/channelrx/demodrtty/rttydemod.h new file mode 100644 index 000000000..926b1d5b8 --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemod.h @@ -0,0 +1,218 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_RTTYDEMOD_H +#define INCLUDE_RTTYDEMOD_H + +#include +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "rttydemodbaseband.h" +#include "rttydemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ScopeVis; + +class RttyDemod : public BasebandSampleSink, public ChannelAPI { +public: + class MsgConfigureRttyDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RttyDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRttyDemod* create(const RttyDemodSettings& settings, bool force) + { + return new MsgConfigureRttyDemod(settings, force); + } + + private: + RttyDemodSettings m_settings; + bool m_force; + + MsgConfigureRttyDemod(const RttyDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + // Sent from Sink when character is decoded + class MsgCharacter : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getCharacter() const { return m_character; } + + static MsgCharacter* create(const QString& character) + { + return new MsgCharacter(character); + } + + private: + QString m_character; + + MsgCharacter(const QString& character) : + m_character(character) + {} + }; + + // Sent from Sink when an estimate is made of the baud rate + class MsgModeEstimate : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getBaudRate() const { return m_baudRate; } + int getFrequencyShift() const { return m_frequencyShift; } + + static MsgModeEstimate* create(int baudRate, int frequencyShift) + { + return new MsgModeEstimate(baudRate, frequencyShift); + } + + private: + int m_baudRate; + int m_frequencyShift; + + MsgModeEstimate(int baudRate, int frequencyShift) : + m_baudRate(baudRate), + m_frequencyShift(frequencyShift) + {} + }; + + RttyDemod(DeviceAPI *deviceAPI); + virtual ~RttyDemod(); + 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 RttyDemodSettings& settings); + + static void webapiUpdateChannelSettings( + RttyDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + ScopeVis *getScopeSink(); + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } +/* void 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; + RttyDemodBaseband* m_basebandSink; + RttyDemodSettings m_settings; + 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 RttyDemodSettings& settings, bool force = false); + void sendSampleRateToDemodAnalyzer(); + void webapiReverseSendSettings(QList& channelSettingsKeys, const RttyDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const RttyDemodSettings& settings, + bool force + ); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); + +}; + +#endif // INCLUDE_RTTYDEMOD_H + diff --git a/plugins/channelrx/demodrtty/rttydemodbaseband.cpp b/plugins/channelrx/demodrtty/rttydemodbaseband.cpp new file mode 100644 index 000000000..e54076203 --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodbaseband.cpp @@ -0,0 +1,181 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "rttydemodbaseband.h" + +MESSAGE_CLASS_DEFINITION(RttyDemodBaseband::MsgConfigureRttyDemodBaseband, Message) + +RttyDemodBaseband::RttyDemodBaseband(RttyDemod *packetDemod) : + m_sink(packetDemod), + m_running(false) +{ + qDebug("RttyDemodBaseband::RttyDemodBaseband"); + + m_sink.setScopeSink(&m_scopeSink); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +RttyDemodBaseband::~RttyDemodBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void RttyDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void RttyDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &RttyDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void RttyDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &RttyDemodBaseband::handleData + ); + m_running = false; +} + +void RttyDemodBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void RttyDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void RttyDemodBaseband::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 RttyDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool RttyDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureRttyDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureRttyDemodBaseband& cfg = (MsgConfigureRttyDemodBaseband&) cmd; + qDebug() << "RttyDemodBaseband::handleMessage: MsgConfigureRttyDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "RttyDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + + return true; + } + else + { + return false; + } +} + +void RttyDemodBaseband::applySettings(const RttyDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int RttyDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void RttyDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} + diff --git a/plugins/channelrx/demodrtty/rttydemodbaseband.h b/plugins/channelrx/demodrtty/rttydemodbaseband.h new file mode 100644 index 000000000..d45b1eb5d --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodbaseband.h @@ -0,0 +1,103 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_RTTYDEMODBASEBAND_H +#define INCLUDE_RTTYDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/scopevis.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "rttydemodsink.h" + +class DownChannelizer; +class ChannelAPI; +class RttyDemod; +class ScopeVis; + +class RttyDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureRttyDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RttyDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRttyDemodBaseband* create(const RttyDemodSettings& settings, bool force) + { + return new MsgConfigureRttyDemodBaseband(settings, force); + } + + private: + RttyDemodSettings m_settings; + bool m_force; + + MsgConfigureRttyDemodBaseband(const RttyDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + RttyDemodBaseband(RttyDemod *packetDemod); + ~RttyDemodBaseband(); + 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; + ScopeVis *getScopeSink() { return &m_scopeSink; } + void setChannel(ChannelAPI *channel); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + RttyDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + RttyDemodSettings m_settings; + ScopeVis m_scopeSink; + bool m_running; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(RttyDemodSink *sink); + void applySettings(const RttyDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_RTTYDEMODBASEBAND_H + diff --git a/plugins/channelrx/demodrtty/rttydemodgui.cpp b/plugins/channelrx/demodrtty/rttydemodgui.cpp new file mode 100644 index 000000000..8785f3a9d --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodgui.cpp @@ -0,0 +1,669 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "rttydemodgui.h" + +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_rttydemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "dsp/dspengine.h" +#include "dsp/glscopesettings.h" +#include "gui/crightclickenabler.h" +#include "gui/tabletapandhold.h" +#include "gui/dialogpositioner.h" +#include "channel/channelwebapiutils.h" +#include "feature/featurewebapiutils.h" +#include "maincore.h" + +#include "rttydemod.h" +#include "rttydemodsink.h" + +RttyDemodGUI* RttyDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + RttyDemodGUI* gui = new RttyDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void RttyDemodGUI::destroy() +{ + delete this; +} + +void RttyDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray RttyDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool RttyDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +void RttyDemodGUI::characterReceived(QString c) +{ + // Is the scroll bar at the bottom? + int scrollPos = ui->text->verticalScrollBar()->value(); + bool atBottom = scrollPos >= ui->text->verticalScrollBar()->maximum(); + + // Move cursor to end where we want to append new text + // (user may have moved it by clicking / highlighting text) + ui->text->moveCursor(QTextCursor::End); + + // Restore scroll position + ui->text->verticalScrollBar()->setValue(scrollPos); + + if ((c == '\r') && (m_previousChar[1] == '\r') && m_settings.m_suppressCRLF) + { + // Don't insert yet + } + else if ((c == '\n') && (m_previousChar[0] == '\r') && (m_previousChar[1] == '\r') && m_settings.m_suppressCRLF) + { + // Change \r\r\n to \r + } + else if ((c != '\n') && (m_previousChar[0] == '\r') && (m_previousChar[1] == '\r') && m_settings.m_suppressCRLF) + { + ui->text->insertPlainText("\r"); // Insert \r we skipped + ui->text->insertPlainText(c); + } + else if (c == '\b') + { + ui->text->textCursor().deletePreviousChar(); + } + else + { + ui->text->insertPlainText(c); + } + + // Scroll to bottom, if we we're previously at the bottom + if (atBottom) { + ui->text->verticalScrollBar()->setValue(ui->text->verticalScrollBar()->maximum()); + } + + // Save last 2 previous characters + m_previousChar[0] = m_previousChar[1]; + m_previousChar[1] = c; +} + +bool RttyDemodGUI::handleMessage(const Message& message) +{ + if (RttyDemod::MsgConfigureRttyDemod::match(message)) + { + qDebug("RttyDemodGUI::handleMessage: RttyDemod::MsgConfigureRttyDemod"); + const RttyDemod::MsgConfigureRttyDemod& cfg = (RttyDemod::MsgConfigureRttyDemod&) 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 (RttyDemod::MsgCharacter::match(message)) + { + RttyDemod::MsgCharacter& report = (RttyDemod::MsgCharacter&) message; + QString c = report.getCharacter(); + characterReceived(c); + return true; + } + else if (RttyDemod::MsgModeEstimate::match(message)) + { + RttyDemod::MsgModeEstimate& report = (RttyDemod::MsgModeEstimate&) message; + ui->baudRate->setToolTip(QString("Baud rate (symbols per second)\n\nEstimate: %1 baud").arg(report.getBaudRate())); + ui->frequencyShift->setToolTip(QString("Frequency shift in Hz (Difference between mark and space frequency)\n\nEstimate: %1 Hz").arg(report.getFrequencyShift())); + ui->modeEst->setText(QString("%1/%2").arg(report.getBaudRate()).arg(report.getFrequencyShift())); + return true; + } + + return false; +} + +void RttyDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void RttyDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void RttyDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void RttyDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void RttyDemodGUI::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 RttyDemodGUI::on_baudRate_currentIndexChanged(int index) +{ + (void) index; + m_settings.m_baudRate = ui->baudRate->currentText().toFloat(); + applySettings(); +} + +void RttyDemodGUI::on_frequencyShift_valueChanged(int value) +{ + ui->frequencyShiftText->setText(formatFrequency(value)); + m_settings.m_frequencyShift = value; + applySettings(); +} + +void RttyDemodGUI::on_squelch_valueChanged(int value) +{ + ui->squelchText->setText(QString("%1 dB").arg(value)); + m_settings.m_squelch = value; + applySettings(); +} + +void RttyDemodGUI::on_characterSet_currentIndexChanged(int index) +{ + m_settings.m_characterSet = (Baudot::CharacterSet) index; + applySettings(); +} + +void RttyDemodGUI::on_suppressCRLF_clicked(bool checked) +{ + m_settings.m_suppressCRLF = checked; + applySettings(); +} + +void RttyDemodGUI::on_mode_currentIndexChanged(int index) +{ + QString mode = ui->mode->currentText(); + + bool custom = mode == "Custom"; + if (!custom) + { + QStringList settings = mode.split("/"); + int baudRate = settings[0].toInt(); + int frequencyShift = settings[1].toInt(); + int bandwidth = frequencyShift * 2 + baudRate; + ui->baudRate->setCurrentText(settings[0]); + ui->frequencyShift->setValue(frequencyShift); + ui->rfBW->setValue(bandwidth); + } + + ui->baudRateLabel->setEnabled(custom); + ui->baudRate->setEnabled(custom); + ui->frequencyShiftLabel->setEnabled(custom); + ui->frequencyShift->setEnabled(custom); + ui->frequencyShiftText->setEnabled(custom); + ui->rfBWLabel->setEnabled(custom); + ui->rfBW->setEnabled(custom); + ui->rfBWText->setEnabled(custom); + + //m_settings.m_mode = index; + applySettings(); +} + + +void RttyDemodGUI::on_filter_currentIndexChanged(int index) +{ + m_settings.m_filter = (RttyDemodSettings::FilterType)index; + applySettings(); +} + +void RttyDemodGUI::on_atc_clicked(bool checked) +{ + m_settings.m_atc = checked; + applySettings(); +} + +void RttyDemodGUI::on_endian_clicked(bool checked) +{ + m_settings.m_msbFirst = checked; + if (checked) { + ui->endian->setText("MSB"); + } else { + ui->endian->setText("LSB"); + } + applySettings(); +} + +void RttyDemodGUI::on_spaceHigh_clicked(bool checked) +{ + m_settings.m_spaceHigh = checked; + if (checked) { + ui->spaceHigh->setText("M-S"); + } else { + ui->spaceHigh->setText("S-M"); + } + applySettings(); +} + +void RttyDemodGUI::on_clearTable_clicked() +{ + ui->text->clear(); +} + +void RttyDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void RttyDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void RttyDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void RttyDemodGUI::on_channel1_currentIndexChanged(int index) +{ + m_settings.m_scopeCh1 = index; + applySettings(); +} + +void RttyDemodGUI::on_channel2_currentIndexChanged(int index) +{ + m_settings.m_scopeCh2 = index; + applySettings(); +} + +void RttyDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void RttyDemodGUI::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_rttyDemod->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(); +} + +RttyDemodGUI::RttyDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::RttyDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_doApplySettings(true), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demodrtty/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_rttyDemod = reinterpret_cast(rxChannel); + m_rttyDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + 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_rttyDemod->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(RttyDemodSettings::RTTYDEMOD_CHANNEL_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() + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("RTTY 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.setRollupState(&m_rollupState); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + ui->scopeContainer->setVisible(false); + + // Hide developer only settings + ui->filterSettingsWidget->setVisible(false); + ui->filterLine->setVisible(false); + + displaySettings(); + makeUIConnections(); + applySettings(true); +} + +RttyDemodGUI::~RttyDemodGUI() +{ + delete ui; +} + +void RttyDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void RttyDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + RttyDemod::MsgConfigureRttyDemod* message = RttyDemod::MsgConfigureRttyDemod::create( m_settings, force); + m_rttyDemod->getInputMessageQueue()->push(message); + } +} + +QString RttyDemodGUI::formatFrequency(int frequency) const +{ + QString suffix = ""; + if (width() > 450) { + suffix = " Hz"; + } + return QString("%1%2").arg(frequency).arg(suffix); +} + +void RttyDemodGUI::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->mode->setCurrentText("Custom"); + ui->rfBWText->setText(formatFrequency((int)m_settings.m_rfBandwidth)); + ui->rfBW->setValue(m_settings.m_rfBandwidth); + QString baudRate; + if (m_settings.m_baudRate < 46.0f && m_settings.m_baudRate > 45.0f) { + baudRate = "45.45"; + } else { + baudRate = QString("%1").arg(m_settings.m_baudRate); + } + ui->baudRate->setCurrentIndex(ui->baudRate->findText(baudRate)); + ui->frequencyShiftText->setText(formatFrequency(m_settings.m_frequencyShift)); + ui->frequencyShift->setValue(m_settings.m_frequencyShift); + ui->squelchText->setText(QString("%1 dB").arg(m_settings.m_squelch)); + ui->squelch->setValue(m_settings.m_squelch); + ui->characterSet->setCurrentIndex((int)m_settings.m_characterSet); + ui->suppressCRLF->setChecked(m_settings.m_suppressCRLF); + ui->filter->setCurrentIndex((int)m_settings.m_filter); + ui->atc->setChecked(m_settings.m_atc); + ui->endian->setChecked(m_settings.m_msbFirst); + if (m_settings.m_msbFirst) { + ui->endian->setText("MSB"); + } else { + ui->endian->setText("LSB"); + } + ui->spaceHigh->setChecked(m_settings.m_spaceHigh); + if (m_settings.m_spaceHigh) { + ui->spaceHigh->setText("M-S"); + } else { + ui->spaceHigh->setText("S-M"); + } + + 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(".txt log filename: %1").arg(m_settings.m_logFilename)); + ui->logEnable->setChecked(m_settings.m_logEnabled); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); +} + +void RttyDemodGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void RttyDemodGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void RttyDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_rttyDemod->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)); + } + + m_tickCount++; +} + +void RttyDemodGUI::on_logEnable_clicked(bool checked) +{ + m_settings.m_logEnabled = checked; + applySettings(); +} + +void RttyDemodGUI::on_logFilename_clicked() +{ + // Get filename to save to + QFileDialog fileDialog(nullptr, "Select file to log received text to", "", "*.txt"); + 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(".txt log filename: %1").arg(m_settings.m_logFilename)); + applySettings(); + } + } +} + +void RttyDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &RttyDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &RttyDemodGUI::on_rfBW_valueChanged); + QObject::connect(ui->baudRate, QOverload::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_baudRate_currentIndexChanged); + QObject::connect(ui->frequencyShift, &QSlider::valueChanged, this, &RttyDemodGUI::on_frequencyShift_valueChanged); + QObject::connect(ui->squelch, &QDial::valueChanged, this, &RttyDemodGUI::on_squelch_valueChanged); + QObject::connect(ui->characterSet, QOverload::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_characterSet_currentIndexChanged); + QObject::connect(ui->suppressCRLF, &ButtonSwitch::clicked, this, &RttyDemodGUI::on_suppressCRLF_clicked); + QObject::connect(ui->mode, QOverload::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_mode_currentIndexChanged); + QObject::connect(ui->filter, QOverload::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_filter_currentIndexChanged); + QObject::connect(ui->atc, &QCheckBox::clicked, this, &RttyDemodGUI::on_atc_clicked); + QObject::connect(ui->endian, &QCheckBox::clicked, this, &RttyDemodGUI::on_endian_clicked); + QObject::connect(ui->spaceHigh, &QCheckBox::clicked, this, &RttyDemodGUI::on_spaceHigh_clicked); + QObject::connect(ui->clearTable, &QPushButton::clicked, this, &RttyDemodGUI::on_clearTable_clicked); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &RttyDemodGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &RttyDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &RttyDemodGUI::on_udpPort_editingFinished); + QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &RttyDemodGUI::on_logEnable_clicked); + QObject::connect(ui->logFilename, &QToolButton::clicked, this, &RttyDemodGUI::on_logFilename_clicked); + QObject::connect(ui->channel1, QOverload::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_channel1_currentIndexChanged); + QObject::connect(ui->channel2, QOverload::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_channel2_currentIndexChanged); +} + +void RttyDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} + diff --git a/plugins/channelrx/demodrtty/rttydemodgui.h b/plugins/channelrx/demodrtty/rttydemodgui.h new file mode 100644 index 000000000..7d5723a2e --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodgui.h @@ -0,0 +1,130 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_RTTYDEMODGUI_H +#define INCLUDE_RTTYDEMODGUI_H + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include "rttydemod.h" +#include "rttydemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class RttyDemod; +class RttyDemodGUI; + +namespace Ui { + class RttyDemodGUI; +} +class RttyDemodGUI; + +class RttyDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static RttyDemodGUI* 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::RttyDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + RttyDemodSettings m_settings; + qint64 m_deviceCenterFrequency; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + + RttyDemod* m_rttyDemod; + int m_basebandSampleRate; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + QString m_previousChar[2]; + + explicit RttyDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~RttyDemodGUI(); + + 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; + void characterReceived(QString c); + + void leaveEvent(QEvent*); + void enterEvent(EnterEventType*); + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_baudRate_currentIndexChanged(int index); + void on_frequencyShift_valueChanged(int value); + void on_squelch_valueChanged(int value); + void on_characterSet_currentIndexChanged(int index); + void on_suppressCRLF_clicked(bool checked=false); + void on_mode_currentIndexChanged(int index); + void on_filter_currentIndexChanged(int index); + void on_atc_clicked(bool checked); + void on_endian_clicked(bool checked); + void on_spaceHigh_clicked(bool checked); + void on_clearTable_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 tick(); +}; + +#endif // INCLUDE_RTTYDEMODGUI_H + diff --git a/plugins/channelrx/demodrtty/rttydemodgui.ui b/plugins/channelrx/demodrtty/rttydemodgui.ui new file mode 100644 index 000000000..f94ac1453 --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodgui.ui @@ -0,0 +1,1323 @@ + + + RttyDemodGUI + + + + 0 + 0 + 411 + 814 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + Packet Demodulator + + + + + 0 + 0 + 390 + 181 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + + 86 + 0 + + + + RTTY baud rate and frequency shift + + + + 45.45/170 + + + + + 50/170 + + + + + 50/450 + + + + + 75/170 + + + + + 75/850 + + + + + Custom + + + + + + + + Qt::Vertical + + + + + + + Baud + + + + + + + + 60 + 0 + + + + Baud rate in symbols per second + + + 1 + + + + 45 + + + + + 45.45 + + + + + 50 + + + + + 75 + + + + + 100 + + + + + 110 + + + + + 150 + + + + + 200 + + + + + + + + Qt::Vertical + + + + + + + Shift + + + + + + + Frequency shift in Hz (Difference between mark and space frequency) + + + 10 + + + 1000 + + + 1 + + + Qt::Horizontal + + + + + + + 850Hz + + + + + + + Qt::Vertical + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 100 + + + 2000 + + + 1 + + + 250 + + + Qt::Horizontal + + + + + + + + 40 + 0 + + + + 500Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + + Filter + + + + + + + + LPF + + + + + Raised Cosine b=1 + + + + + Raised Cosine b=0.75 + + + + + Raised Cosine b=0.5 + + + + + Rasied Cosine b=1 BW=0.75 + + + + + Raised Cosine b=1 BW=1.25 + + + + + MAV + + + + + Filtered MAV + + + + + + + + + 24 + 16777215 + + + + Automatic threshold correction + + + ATC + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Est. + + + + + + + Estimated baud rate and frequency shift + + + 50/170 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Send messages 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::Vertical + + + + + + + Squelch + + + + + + + + 24 + 24 + + + + Squelch. Characters received with average power below this setting will be discarded. + + + -120 + + + 0 + + + + + + + + 46 + 0 + + + + -100 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + Baudot + + + + + + + + 80 + 0 + + + + Baudot character set + + + 0 + + + + ITA 2 + + + + + UK + + + + + European + + + + + US + + + + + Russian + + + + + Murray + + + + + + + + + 30 + 0 + + + + + 24 + 16777215 + + + + Whether LSB (Least significant bit) or MSB (Most significant bit) is transmitted first + + + LSB + + + + + + + + 30 + 0 + + + + + 24 + 16777215 + + + + Whether mark is high frequency (unchecked) or low frequency (checked) + + + S-M + + + + + + + + 24 + 16777215 + + + + When checked the CR CR LF sequence is just displayed as CR + + + CR + + + + + + + + 24 + 16777215 + + + + Unshift on space - Set character set to letter when a space character is received + + + US + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 24 + 16777215 + + + + Start/stop logging of received characters to .txt file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Clear messages + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 0 + 190 + 391 + 251 + + + + + 0 + 0 + + + + Received Messages + + + + + + Received text + + + true + + + + + + + + + 0 + 440 + 716 + 341 + + + + + 714 + 0 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Real + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Mag Sq + + + + + Sample Idx + + + + + abs(Sum1) + + + + + abs(Sum2) + + + + + Bit + + + + + Bit Cnt + + + + + Got SOP + + + + + Real(exp) + + + + + Imag(exp) + + + + + abs(sum1)Filt + + + + + abs(sum2)Filt + + + + + Diff + + + + + DiffFilt + + + + + data + + + + + clock + + + + + Env1 + + + + + Env2 + + + + + Bias1 + + + + + Bias2 + + + + + Unbiased data + + + + + Biased data + + + + + + + + + 0 + 0 + + + + Imag + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Mag Sq + + + + + Sample Idx + + + + + abs(Sum1) + + + + + abs(Sum2) + + + + + Bit + + + + + Bit Cnt + + + + + Got SOP + + + + + Real(exp) + + + + + imag(exp) + + + + + abs(sum1)Filt + + + + + abs(sum2)Filt + + + + + Diff + + + + + DiffFilt + + + + + data + + + + + clock + + + + + Env1 + + + + + Env2 + + + + + Bias1 + + + + + Bias2 + + + + + Unbiased data + + + + + Biased data + + + + + + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.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 +
+
+ + deltaFrequency + mode + baudRate + frequencyShift + rfBW + filter + atc + udpEnabled + squelch + characterSet + endian + spaceHigh + suppressCRLF + unshiftOnSpace + logEnable + logFilename + clearTable + text + channel1 + channel2 + + + + + +
diff --git a/plugins/channelrx/demodrtty/rttydemodplugin.cpp b/plugins/channelrx/demodrtty/rttydemodplugin.cpp new file mode 100644 index 000000000..dcfb9e4d5 --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodplugin.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 "rttydemodgui.h" +#endif +#include "rttydemod.h" +#include "rttydemodwebapiadapter.h" +#include "rttydemodplugin.h" + +const PluginDescriptor RttyDemodPlugin::m_pluginDescriptor = { + RttyDemod::m_channelId, + QStringLiteral("RTTY Demodulator"), + QStringLiteral("7.11.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +RttyDemodPlugin::RttyDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& RttyDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void RttyDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(RttyDemod::m_channelIdURI, RttyDemod::m_channelId, this); +} + +void RttyDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + RttyDemod *instance = new RttyDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* RttyDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* RttyDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return RttyDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* RttyDemodPlugin::createChannelWebAPIAdapter() const +{ + return new RttyDemodWebAPIAdapter(); +} + diff --git a/plugins/channelrx/demodrtty/rttydemodplugin.h b/plugins/channelrx/demodrtty/rttydemodplugin.h new file mode 100644 index 000000000..dd61061cb --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodplugin.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_RTTYDEMODPLUGIN_H +#define INCLUDE_RTTYDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class RttyDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.rttydemod") + +public: + explicit RttyDemodPlugin(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_RTTYDEMODPLUGIN_H + diff --git a/plugins/channelrx/demodrtty/rttydemodsettings.cpp b/plugins/channelrx/demodrtty/rttydemodsettings.cpp new file mode 100644 index 000000000..ab55bfeef --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodsettings.cpp @@ -0,0 +1,213 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "rttydemodsettings.h" + +RttyDemodSettings::RttyDemodSettings() : + m_channelMarker(nullptr), + m_scopeGUI(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void RttyDemodSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 400.0f; // OBW for 2FSK = 2 * deviation + data rate. Then add a bit for carrier frequency offset + m_baudRate = 45.45; + m_frequencyShift = 170; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_characterSet = Baudot::ITA2; + m_suppressCRLF = false; + m_filter = LOWPASS; + m_atc = true; + m_msbFirst = false; + m_spaceHigh = false; + m_squelch = -70; + m_logFilename = "rtty_log.csv"; + m_logEnabled = false; + m_scopeCh1 = 0; + m_scopeCh2 = 1; + + m_rgbColor = QColor(180, 205, 130).rgb(); + m_title = "RTTY Demodulator"; + 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 RttyDemodSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_streamIndex); + + s.writeFloat(3, m_rfBandwidth); + s.writeFloat(4, m_baudRate); + s.writeS32(5, m_frequencyShift); + s.writeS32(6, (int)m_characterSet); + s.writeBool(7, m_suppressCRLF); + s.writeBool(8, m_unshiftOnSpace); + s.writeS32(9, (int)m_filter); + s.writeBool(10, m_atc); + s.writeBool(34, m_msbFirst); + s.writeBool(35, m_spaceHigh); + s.writeS32(36, m_squelch); + + if (m_channelMarker) { + s.writeBlob(11, m_channelMarker->serialize()); + } + + s.writeU32(12, m_rgbColor); + s.writeString(13, m_title); + s.writeBool(14, m_useReverseAPI); + s.writeString(15, m_reverseAPIAddress); + s.writeU32(16, m_reverseAPIPort); + s.writeU32(17, m_reverseAPIDeviceIndex); + s.writeU32(18, m_reverseAPIChannelIndex); + + s.writeBool(22, m_udpEnabled); + s.writeString(23, m_udpAddress); + s.writeU32(24, m_udpPort); + + s.writeS32(31, m_scopeCh1); + s.writeS32(32, m_scopeCh2); + s.writeBlob(33, m_scopeGUI->serialize()); + + s.writeString(25, m_logFilename); + s.writeBool(26, m_logEnabled); + + if (m_rollupState) { + s.writeBlob(27, m_rollupState->serialize()); + } + + s.writeS32(28, m_workspaceIndex); + s.writeBlob(29, m_geometryBytes); + s.writeBool(30, m_hidden); + + return s.final(); +} + +bool RttyDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_streamIndex, 0); + + d.readFloat(3, &m_rfBandwidth, 450.0f); + d.readFloat(4, &m_baudRate, 45.45f); + d.readS32(5, &m_frequencyShift, 170); + d.readS32(6, (int *)&m_characterSet, (int)Baudot::ITA2); + d.readBool(7, &m_suppressCRLF, false); + d.readBool(8, &m_unshiftOnSpace, false); + d.readS32(9, (int *)&m_filter, (int) LOWPASS); + d.readBool(10, &m_atc, true); + d.readBool(34, &m_msbFirst, false); + d.readBool(35, &m_spaceHigh, false); + d.readS32(36, &m_squelch, -70); + + if (m_channelMarker) + { + d.readBlob(11, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + + d.readU32(12, &m_rgbColor, QColor(180, 205, 130).rgb()); + d.readString(13, &m_title, "RTTY Demodulator"); + d.readBool(14, &m_useReverseAPI, false); + d.readString(15, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(16, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(17, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(18, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + + d.readBool(22, &m_udpEnabled); + d.readString(23, &m_udpAddress); + d.readU32(24, &utmp); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + + d.readS32(31, &m_scopeCh1, 0); + d.readS32(32, &m_scopeCh2, 0); + if (m_scopeGUI) + { + d.readBlob(33, &bytetmp); + m_scopeGUI->deserialize(bytetmp); + } + + d.readString(25, &m_logFilename, "rtty_log.csv"); + d.readBool(26, &m_logEnabled, false); + + if (m_rollupState) + { + d.readBlob(27, &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/demodrtty/rttydemodsettings.h b/plugins/channelrx/demodrtty/rttydemodsettings.h new file mode 100644 index 000000000..6cd37a1af --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodsettings.h @@ -0,0 +1,88 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_RTTYDEMODSETTINGS_H +#define INCLUDE_RTTYDEMODSETTINGS_H + +#include + +#include "util/baudot.h" + +class Serializable; + +struct RttyDemodSettings +{ + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; + Real m_baudRate; + int m_frequencyShift; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + Baudot::CharacterSet m_characterSet; + bool m_suppressCRLF; + bool m_unshiftOnSpace; + enum FilterType { + LOWPASS, + COSINE_B_1, + COSINE_B_0_75, + COSINE_B_0_5, + COSINE_B_1_BW_0_75, + COSINE_B_1_BW_1_25, + MAV, + FILTERED_MAV + } m_filter; + bool m_atc; + bool m_msbFirst; // false = LSB first, true = MSB first + bool m_spaceHigh; // false = mark high frequency, true = space high frequency + int m_squelch; // In dB + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + int m_scopeCh1; + int m_scopeCh2; + + QString m_logFilename; + bool m_logEnabled; + Serializable *m_scopeGUI; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + static const int RTTYDEMOD_CHANNEL_SAMPLE_RATE = 1000; + + RttyDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_RTTYDEMODSETTINGS_H */ + diff --git a/plugins/channelrx/demodrtty/rttydemodsink.cpp b/plugins/channelrx/demodrtty/rttydemodsink.cpp new file mode 100644 index 000000000..d4792f0f8 --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodsink.cpp @@ -0,0 +1,670 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 + +#include "dsp/dspengine.h" +#include "dsp/scopevis.h" +#include "util/db.h" +#include "maincore.h" + +#include "rttydemod.h" +#include "rttydemodsink.h" + +RttyDemodSink::RttyDemodSink(RttyDemod *packetDemod) : + m_rttyDemod(packetDemod), + m_channelSampleRate(RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_expLength(600), + m_prods1(nullptr), + m_prods2(nullptr), + m_exp(nullptr), + m_clockHistogram(100), + m_shiftEstMag(m_fftSize), + m_fftSequence(-1), + m_fft(nullptr), + m_fftCounter(0), + m_sampleIdx(0), + m_sampleBufferIndex(0) +{ + m_magsq = 0.0; + + m_sampleBuffer.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; +} + +RttyDemodSink::~RttyDemodSink() +{ + delete[] m_exp; + delete[] m_prods1; + delete[] m_prods2; +} + +void RttyDemodSink::sampleToScope(Complex sample) +{ + if (m_scopeSink) + { + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); + + if (m_sampleBufferIndex == m_sampleBufferSize) + { + std::vector vbegin; + vbegin.push_back(m_sampleBuffer.begin()); + m_scopeSink->feed(vbegin, m_sampleBufferSize); + m_sampleBufferIndex = 0; + } + } +} + +void RttyDemodSink::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 RttyDemodSink::processOneSample(Complex &ci) +{ + // 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++; + + // Sum power while data is being received + if (m_gotSOP) + { + m_rssiMagSqSum += magsq; + m_rssiMagSqCount++; + } + + ci /= SDR_RX_SCALEF; + + // Use FFT to estimate frequency shift + m_fft->in()[m_fftCounter] = ci; + m_fftCounter++; + if (m_fftCounter == m_fftSize) + { + estimateFrequencyShift(); + m_fftCounter = 0; + } + + // Correlate with expected mark and space frequencies + Complex exp = m_exp[m_expIdx]; + m_expIdx = (m_expIdx + 1) % m_expLength; + //Complex exp = m_exp[m_sampleIdx]; + //qDebug() << "IQ " << real(ci) << imag(ci); + Complex corr1 = ci * exp; + Complex corr2 = ci * std::conj(exp); + + // Filter + Real abs1, abs2; + Real abs1Filt, abs2Filt; + if (m_settings.m_filter == RttyDemodSettings::LOWPASS) + { + // Low pass filter + abs1Filt = abs1 = std::abs(m_lowpassComplex1.filter(corr1)); + abs2Filt = abs2 = std::abs(m_lowpassComplex2.filter(corr2)); + } + else if ( (m_settings.m_filter == RttyDemodSettings::COSINE_B_1) + || (m_settings.m_filter == RttyDemodSettings::COSINE_B_0_75) + || (m_settings.m_filter == RttyDemodSettings::COSINE_B_0_5) + ) + { + // Rasised cosine filter + abs1Filt = abs1 = std::abs(m_raisedCosine1.filter(corr1)); + abs2Filt = abs2 = std::abs(m_raisedCosine2.filter(corr2)); + } + else + { + // Moving average + + // Calculating moving average (well windowed sum) + Complex old1 = m_prods1[m_sampleIdx]; + Complex old2 = m_prods2[m_sampleIdx]; + m_prods1[m_sampleIdx] = corr1; + m_prods2[m_sampleIdx] = corr2; + m_sum1 += m_prods1[m_sampleIdx] - old1; + m_sum2 += m_prods2[m_sampleIdx] - old2; + m_sampleIdx = (m_sampleIdx + 1) % m_samplesPerBit; + + // Square signals (by calculating absolute value of complex signal) + abs1 = std::abs(m_sum1); + abs2 = std::abs(m_sum2); + + // Apply optional low-pass filter to try to avoid extra zero-crassings above the baud rate + if (m_settings.m_filter == RttyDemodSettings::FILTERED_MAV) + { + abs1Filt = m_lowpass1.filter(abs1); + abs2Filt = m_lowpass2.filter(abs2); + } + else + { + abs1Filt = abs1; + abs2Filt = abs2; + } + } + + // Envelope calculation + m_movMax1(abs1Filt); + m_movMax2(abs2Filt); + Real env1 = m_movMax1.getMaximum(); + Real env2 = m_movMax2.getMaximum(); + + // Automatic threshold correction to compensate for frequency selective fading + // http://www.w7ay.net/site/Technical/ATC/index.html + Real bias1 = abs1Filt - 0.5 * env1; + Real bias2 = abs2Filt - 0.5 * env2; + Real unbiasedData = abs1Filt - abs2Filt; + Real biasedData = bias1 - bias2; + + // Save current data for edge detection + m_dataPrev = m_data; + // Set data according to stongest correlation + if (m_settings.m_spaceHigh) { + m_data = m_settings.m_atc ? biasedData < 0 : unbiasedData < 0; + } else { + m_data = m_settings.m_atc ? biasedData > 0 : unbiasedData > 0; + } + + if (!m_gotSOP) + { + // Look for falling edge which indicates start bit + if (!m_data && m_dataPrev) + { + m_gotSOP = true; + m_bits = 0; + m_bitCount = 0; + m_clockCount = 0; + m_clock = false; + m_cycleCount = 0; + } + } + else + { + // Sample in middle of symbol + if (m_clockCount == m_samplesPerBit/2) + { + receiveBit(m_data); + m_clock = true; + } + m_clockCount = (m_clockCount + 1) % m_samplesPerBit; + if (m_clockCount == 0) { + m_clock = false; + } + + // Count cycles between edges, to estimate baud rate + m_cycleCount++; + if (m_data != m_dataPrev) + { + if (m_cycleCount < m_clockHistogram.size()) + { + m_clockHistogram[m_cycleCount]++; + m_edgeCount++; + + // Every 100 edges, calculate estimate + if (m_edgeCount == 100) { + estimateBaudRate(); + } + } + m_cycleCount = 0; + } + } + + // 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(magsq); + break; + case 3: + scopeSample.real(m_sampleIdx); + break; + case 4: + scopeSample.real(abs(m_sum1)); + break; + case 5: + scopeSample.real(abs(m_sum2)); + break; + case 6: + scopeSample.real(m_bit); + break; + case 7: + scopeSample.real(m_bitCount); + break; + case 8: + scopeSample.real(m_gotSOP); + break; + case 9: + scopeSample.real(real(exp)); + break; + case 10: + scopeSample.real(imag(exp)); + break; + case 11: + scopeSample.real(abs1Filt); + break; + case 12: + scopeSample.real(abs2Filt); + break; + case 13: + scopeSample.real(abs2 - abs1); + break; + case 14: + scopeSample.real(abs2Filt - abs1Filt); + break; + case 15: + scopeSample.real(m_data); + break; + case 16: + scopeSample.real(m_clock); + break; + case 17: + scopeSample.real(env1); + break; + case 18: + scopeSample.real(env2); + break; + case 19: + scopeSample.real(bias1); + break; + case 20: + scopeSample.real(bias2); + break; + case 21: + scopeSample.real(unbiasedData); + break; + case 22: + scopeSample.real(biasedData); + break; + } + switch (m_settings.m_scopeCh2) + { + case 0: + scopeSample.imag(ci.real()); + break; + case 1: + scopeSample.imag(ci.imag()); + break; + case 2: + scopeSample.imag(magsq); + break; + case 3: + scopeSample.imag(m_sampleIdx); + break; + case 4: + scopeSample.imag(abs(m_sum1)); + break; + case 5: + scopeSample.imag(abs(m_sum2)); + break; + case 6: + scopeSample.imag(m_bit); + break; + case 7: + scopeSample.imag(m_bitCount); + break; + case 8: + scopeSample.imag(m_gotSOP); + break; + case 9: + scopeSample.imag(real(exp)); + break; + case 10: + scopeSample.imag(imag(exp)); + break; + case 11: + scopeSample.imag(abs1Filt); + break; + case 12: + scopeSample.imag(abs2Filt); + break; + case 13: + scopeSample.imag(abs2 - abs1); + break; + case 14: + scopeSample.imag(abs2Filt - abs1Filt); + break; + case 15: + scopeSample.imag(m_data); + break; + case 16: + scopeSample.imag(m_clock); + break; + case 17: + scopeSample.imag(env1); + break; + case 18: + scopeSample.imag(env2); + break; + case 19: + scopeSample.imag(bias1); + break; + case 20: + scopeSample.imag(bias2); + break; + case 21: + scopeSample.imag(unbiasedData); + break; + case 22: + scopeSample.imag(biasedData); + break; + } + sampleToScope(scopeSample); +} + +void RttyDemodSink::estimateFrequencyShift() +{ + // Perform FFT + m_fft->transform(); + // Calculate magnitude + for (int i = 0; i < m_fftSize; i++) + { + Complex c = m_fft->out()[i]; + Real v = c.real() * c.real() + c.imag() * c.imag(); + Real magsq = v / (m_fftSize * m_fftSize); + m_shiftEstMag[i] = magsq; + } + // Fink peaks in each half + Real peak1 = m_shiftEstMag[0]; + int peak1Bin = 0; + for (int i = 1; i < m_fftSize/2; i++) + { + if (m_shiftEstMag[i] > peak1) + { + peak1 = m_shiftEstMag[i]; + peak1Bin = i; + } + } + Real peak2 = m_shiftEstMag[m_fftSize/2]; + int peak2Bin = m_fftSize/2; + for (int i = m_fftSize/2+1; i < m_fftSize; i++) + { + if (m_shiftEstMag[i] > peak2) + { + peak2 = m_shiftEstMag[i]; + peak2Bin = i; + } + } + // Convert bin to frequency offset + double frequencyResolution = RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE / (double)m_fftSize; + double freq1 = frequencyResolution * peak1Bin; + double freq2 = -frequencyResolution * (m_fftSize - peak2Bin); + m_freq1Average(freq1); + m_freq2Average(freq2); + //int shift = m_freq1Average.instantAverage() - m_freq2Average.instantAverage(); + //qDebug() << "Freq est " << freq1 << freq2 << shift; +} + +int RttyDemodSink::estimateBaudRate() +{ + // Find most frequent entry in histogram + auto histMax = max_element(m_clockHistogram.begin(), m_clockHistogram.end()); + int index = std::distance(m_clockHistogram.begin(), histMax); + + // Calculate baud rate as weighted average + Real baud1 = RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE / (Real)(index-1); + int count1 = m_clockHistogram[index-1]; + Real baud2 = RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE / (Real)(index); + int count2 = m_clockHistogram[index]; + Real baud3 = RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE / (Real)(index+1); + int count3 = m_clockHistogram[index+1]; + Real total = count1 + count2 + count3; + Real estBaud = count1/total*baud1 + count2/total*baud2 + count3/total*baud3; + m_baudRateAverage(estBaud); + + // Send estimate to GUI + if (getMessageQueueToChannel()) + { + int estFrequencyShift = m_freq1Average.instantAverage() - m_freq2Average.instantAverage(); + RttyDemod::MsgModeEstimate *msg = RttyDemod::MsgModeEstimate::create(m_baudRateAverage.instantAverage(), estFrequencyShift); + getMessageQueueToChannel()->push(msg); + } + + // Restart estimation + std::fill(m_clockHistogram.begin(), m_clockHistogram.end(), 0); + m_edgeCount = 0; + + return estBaud; +} + +void RttyDemodSink::receiveBit(bool bit) +{ + m_bit = bit; + + // Store in shift reg. + if (m_settings.m_msbFirst) { + m_bits = (m_bit & 0x1) | (m_bits << 1); + } else { + m_bits = (m_bit << 6) | (m_bits >> 1); + } + m_bitCount++; + + if (m_bitCount == 7) + { + if ( (!m_settings.m_msbFirst && ((m_bits & 0x40) != 0x40)) + || (m_settings.m_msbFirst && ((m_bits & 0x01) != 0x01))) + { + //qDebug() << "No stop bit"; + } + else + { + QString c = m_rttyDecoder.decode((m_bits >> 1) & 0x1f); + if ((c != '\0') && (c != '<') && (c != '>') && (c != '^')) + { + // Calculate average power over received byte + float rssi = CalcDb::dbPower(m_rssiMagSqSum / m_rssiMagSqCount); + if (rssi > m_settings.m_squelch) + { + // Slow enough to send individually to be displayed + if (getMessageQueueToChannel()) + { + RttyDemod::MsgCharacter *msg = RttyDemod::MsgCharacter::create(c); + getMessageQueueToChannel()->push(msg); + } + } + } + } + m_gotSOP = false; + } +} + +void RttyDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "RttyDemodSink::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) RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void RttyDemodSink::init() +{ + m_sampleIdx = 0; + m_expIdx = 0; + m_sum1 = 0.0; + m_sum2 = 0.0; + for (int i = 0; i < m_samplesPerBit; i++) + { + m_prods1[i] = 0.0f; + m_prods2[i] = 0.0f; + } + m_bit = 0; + m_bits = 0; + m_bitCount = 0; + m_gotSOP = false; + m_clockCount = 0; + m_clock = 0; + m_rssiMagSqSum = 0.0; + m_rssiMagSqCount = 0; + m_rttyDecoder.init(); +} + +void RttyDemodSink::applySettings(const RttyDemodSettings& settings, bool force) +{ + qDebug() << "RttyDemodSink::applySettings:" + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_baudRate: " << settings.m_baudRate + << " m_frequencyShift: " << settings.m_frequencyShift + << " m_characterSet: " << settings.m_characterSet + << " m_unshiftOnSpace: " << settings.m_unshiftOnSpace + << " force: " << force; + + 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) RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + if ((settings.m_baudRate != m_settings.m_baudRate) || (settings.m_filter != m_settings.m_filter) || force) + { + m_envelope1.create(301, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, 2); + m_envelope2.create(301, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, 2); + m_lowpass1.create(301, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, m_settings.m_baudRate * 1.1); + m_lowpass2.create(301, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, m_settings.m_baudRate * 1.1); + //m_lowpass1.printTaps("lpf"); + + m_lowpassComplex1.create(301, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, m_settings.m_baudRate * 1.1); + m_lowpassComplex2.create(301, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE, m_settings.m_baudRate * 1.1); + //m_lowpass1.printTaps("lpfc"); + + // http://w7ay.net/site/Technical/Extended%20Nyquist%20Filters/index.html + // http://w7ay.net/site/Technical/EqualizedRaisedCosine/index.html + float beta = 1.0f; + float bw = 1.0f; + if (settings.m_filter == RttyDemodSettings::COSINE_B_0_5) { + beta = 0.5f; + } else if (settings.m_filter == RttyDemodSettings::COSINE_B_0_75) { + beta = 0.75f; + } else if (settings.m_filter == RttyDemodSettings::COSINE_B_1_BW_0_75) { + bw = 0.75f; + } else if (settings.m_filter == RttyDemodSettings::COSINE_B_1_BW_1_25) { + bw = 1.25f; + } + m_raisedCosine1.create(beta, 7, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE/(m_settings.m_baudRate/bw), false); + m_raisedCosine2.create(beta, 7, RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE/(m_settings.m_baudRate/bw), false); + //m_raisedCosine1.printTaps("rcos"); + } + + if ((settings.m_characterSet != m_settings.m_characterSet) || force) { + m_rttyDecoder.setCharacterSet(settings.m_characterSet); + } + if ((settings.m_unshiftOnSpace != m_settings.m_unshiftOnSpace) || force) { + m_rttyDecoder.setUnshiftOnSpace(settings.m_unshiftOnSpace); + } + + if ((settings.m_baudRate != m_settings.m_baudRate) || (settings.m_frequencyShift != m_settings.m_frequencyShift) || force) + { + delete[] m_exp; + delete[] m_prods1; + delete[] m_prods2; + m_samplesPerBit = RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE / settings.m_baudRate; + m_exp = new Complex[m_expLength]; + m_prods1 = new Complex[m_samplesPerBit]; + m_prods2 = new Complex[m_samplesPerBit]; + Real f0 = 0.0f; + for (int i = 0; i < m_expLength; i++) + { + m_exp[i] = Complex(cos(f0), sin(f0)); + f0 += 2.0f * (Real)M_PI * (settings.m_frequencyShift/2.0f) / RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE; + } + init(); + + // Due to start and stop bits, we should get mark and space at least every 8 bits + // while something is being transmitted + m_movMax1.setSize(m_samplesPerBit * 8); + m_movMax2.setSize(m_samplesPerBit * 8); + + m_edgeCount = 0; + std::fill(m_clockHistogram.begin(), m_clockHistogram.end(), 0); + + m_baudRateAverage.reset(); + m_freq1Average.reset(); + m_freq2Average.reset(); + } + + m_settings = settings; +} + diff --git a/plugins/channelrx/demodrtty/rttydemodsink.h b/plugins/channelrx/demodrtty/rttydemodsink.h new file mode 100644 index 000000000..e99e0397a --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodsink.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_RTTYDEMODSINK_H +#define INCLUDE_RTTYDEMODSINK_H + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "dsp/raisedcosine.h" +#include "dsp/fftfactory.h" +#include "dsp/fftengine.h" +#include "util/movingaverage.h" +#include "util/movingmaximum.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" + +#include "rttydemodsettings.h" + +class ChannelAPI; +class RttyDemod; +class ScopeVis; + + +class RttyDemodSink : public ChannelSampleSink { +public: + RttyDemodSink(RttyDemod *packetDemod); + ~RttyDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; } + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const RttyDemodSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setChannel(ChannelAPI *channel) { m_channel = channel; } + + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.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; + }; + + ScopeVis* m_scopeSink; // Scope GUI to display baseband waveform + RttyDemod *m_rttyDemod; + RttyDemodSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + + 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; + Lowpass m_envelope1; + Lowpass m_envelope2; + Lowpass m_lowpass1; + Lowpass m_lowpass2; + Lowpass m_lowpassComplex1; + Lowpass m_lowpassComplex2; + RaisedCosine m_raisedCosine1; + RaisedCosine m_raisedCosine2; + + MovingMaximum m_movMax1; + MovingMaximum m_movMax2; + + int m_expLength; + int m_samplesPerBit; + Complex *m_prods1; + Complex *m_prods2; + Complex *m_exp; + Complex m_sum1; + Complex m_sum2; + int m_sampleIdx; + int m_expIdx; + int m_bit; + bool m_data; + bool m_dataPrev; + int m_clockCount; + bool m_clock; + double m_rssiMagSqSum; + int m_rssiMagSqCount; + + unsigned short m_bits; + int m_bitCount; + bool m_gotSOP; + BaudotDecoder m_rttyDecoder; + + // For baud rate estimation + int m_cycleCount; + std::vector m_clockHistogram; + int m_edgeCount; + MovingAverageUtil m_baudRateAverage; + + // For frequency shift estimation + std::vector m_shiftEstMag; + int m_fftSequence; + FFTEngine *m_fft; + int m_fftCounter; + static const int m_fftSize = 128; // ~7Hz res + MovingAverageUtil m_freq1Average; + MovingAverageUtil m_freq2Average; + + SampleVector m_sampleBuffer; + static const int m_sampleBufferSize = RttyDemodSettings::RTTYDEMOD_CHANNEL_SAMPLE_RATE / 20; + int m_sampleBufferIndex; + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void sampleToScope(Complex sample); + void init(); + void receiveBit(bool bit); + int estimateBaudRate(); + void estimateFrequencyShift(); +}; + +#endif // INCLUDE_RTTYDEMODSINK_H + diff --git a/plugins/channelrx/demodrtty/rttydemodwebapiadapter.cpp b/plugins/channelrx/demodrtty/rttydemodwebapiadapter.cpp new file mode 100644 index 000000000..e809ab01d --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodwebapiadapter.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 "rttydemod.h" +#include "rttydemodwebapiadapter.h" + +RttyDemodWebAPIAdapter::RttyDemodWebAPIAdapter() +{} + +RttyDemodWebAPIAdapter::~RttyDemodWebAPIAdapter() +{} + +int RttyDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRttyDemodSettings(new SWGSDRangel::SWGRTTYDemodSettings()); + response.getRttyDemodSettings()->init(); + RttyDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int RttyDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + RttyDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodrtty/rttydemodwebapiadapter.h b/plugins/channelrx/demodrtty/rttydemodwebapiadapter.h new file mode 100644 index 000000000..25746811d --- /dev/null +++ b/plugins/channelrx/demodrtty/rttydemodwebapiadapter.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_RTTYDEMOD_WEBAPIADAPTER_H +#define INCLUDE_RTTYDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "rttydemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class RttyDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + RttyDemodWebAPIAdapter(); + virtual ~RttyDemodWebAPIAdapter(); + + 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: + RttyDemodSettings m_settings; +}; + +#endif // INCLUDE_RTTYDEMOD_WEBAPIADAPTER_H diff --git a/sdrbase/util/baudot.cpp b/sdrbase/util/baudot.cpp new file mode 100644 index 000000000..29bd0938d --- /dev/null +++ b/sdrbase/util/baudot.cpp @@ -0,0 +1,192 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "baudot.h" + +// https://en.wikipedia.org/wiki/Baudot_code +// We use < for FIGS and > for LTRS and ^ for Cyrillic +// Unicode used for source file encoding + +const QString Baudot::m_ita2Letter[] = { + "\0", "E", "\n", "A", " ", "S", "I", "U", + "\r", "D", "R", "J", "N", "F", "C", "K", + "T", "Z", "L", "W", "H", "Y", "P", "Q", + "O", "B", "G", "<", "M", "X", "V", ">" +}; + +const QString Baudot::m_ita2Figure[] = { + "\0", "3", "\n", "-", " ", "\'", "8", "7", + "\r", "\x5", "4", "\a", ",", "!", ":", "(", + "5", "+", ")", "2", "£", "6", "0", "1", + "9", "?", "&", "<", ".", "/", "=", ">" +}; + +const QString Baudot::m_ukLetter[] = { + "\0", "A", "E", "/", "Y", "U", "I", "O", + "<", "J", "G", "H", "B", "C", "F", "D", + " ", "-", "X", "Z", "S", "T", "W", "V", + "\b", "K", "M", "L", "R", "Q", "N", "P" +}; + +const QString Baudot::m_ukFigure[] = { + "\0", "1", "2", "⅟", "3", "4", "³⁄", "5", + " ", "6", "7", "¹", "8", "9", "⁵⁄", "0", + ">", ".", "⁹⁄", ":", "⁷⁄", "²", "?", "\'", + "\b", "(", ")", "=", "-", "/", "£", "+" +}; + +const QString Baudot::m_europeanLetter[] = { + "\0", "A", "E", "É", "Y", "U", "I", "O", + "<", "J", "G", "H", "B", "C", "F", "D", + " ", "t", "X", "Z", "S", "T", "W", "V", + "\b", "K", "M", "L", "R", "Q", "N", "P" +}; + +const QString Baudot::m_europeanFigure[] = { + "\0", "1", "2", "&", "3", "4", "º", "5", + " ", "6", "7", "H̱", "8", "9", "F̱", "0", + ">", ".", ",", ":", ";", "!", "?", "\'", + "\b", "(", ")", "=", "-", "/", "№", "%" +}; + +const QString Baudot::m_usLetter[] = { + "\0", "E", "\n", "A", " ", "S", "I", "U", + "\r", "D", "R", "J", "N", "F", "C", "K", + "T", "Z", "L", "W", "H", "Y", "P", "Q", + "O", "B", "G", "<", "M", "X", "V", ">" +}; + +const QString Baudot::m_usFigure[] = { + "\0", "3", "\n", "-", " ", "\a", "8", "7", + "\r", "\x5", "4", "\'", ",", "!", ":", "(", + "5", "\"", ")", "2", "#", "6", "0", "1", + "9", "?", "&", "<", ".", "/", ";", ">" +}; + +const QString Baudot::m_russianLetter[] = { + "\0", "Е", "\n", "А", " ", "С", "И", "У", + "\r", "Д", "П", "Й", "Н", "Ф", "Ц", "К", + "Т", "З", "Л", "В", "Х", "Ы", "P", "Я", + "О", "Б", "Г", "<", "М", "Ь", "Ж", ">" +}; + +const QString Baudot::m_russianFigure[] = { + "\0", "3", "\n", "-", " ", "\'", "8", "7", + "\r", "Ч", "4", "Ю", ",", "Э", ":", "(", + "5", "+", ")", "2", "Щ", "6", "0", "1", + "9", "?", "Ш", "<", ".", "/", ";", ">" +}; + +const QString Baudot::m_murrayLetter[] = { + " ", "E", "?", "A", ">", "S", "I", "U", + "\n", "D", "R", "J", "N", "F", "C", "K", + "T", "Z", "L", "W", "H", "Y", "P", "Q", + "O", "B", "G", "<", "M", "X", "V", "\b" +}; + +const QString Baudot::m_murrayFigure[] = { + " ", "3", "?", " ", ">", "'", "8", "7", + "\n", "²", "4", "⁷⁄", "-", "⅟", "(", "⁹⁄", + "5", ".", "/", "2", "⁵⁄", "6", "0", "1", + "9", "?", "³⁄", "<", ",", "£", ")", "\b" +}; + +BaudotDecoder::BaudotDecoder() +{ + setCharacterSet(Baudot::ITA2); + setUnshiftOnSpace(false); + init(); +} + +void BaudotDecoder::setCharacterSet(Baudot::CharacterSet characterSet) +{ + m_characterSet = characterSet; + switch (m_characterSet) + { + case Baudot::ITA2: + m_letters = Baudot::m_ita2Letter; + m_figures = Baudot::m_ita2Figure; + break; + case Baudot::UK: + m_letters = Baudot::m_ukLetter; + m_figures = Baudot::m_ukFigure; + break; + case Baudot::EUROPEAN: + m_letters = Baudot::m_europeanLetter; + m_figures = Baudot::m_europeanFigure; + break; + case Baudot::US: + m_letters = Baudot::m_usLetter; + m_figures = Baudot::m_usFigure; + break; + case Baudot::RUSSIAN: + m_letters = Baudot::m_russianLetter; + m_figures = Baudot::m_russianFigure; + break; + case Baudot::MURRAY: + m_letters = Baudot::m_murrayLetter; + m_figures = Baudot::m_murrayFigure; + break; + default: + qDebug() << "BaudotDecoder::BaudotDecoder: Unsupported character set " << m_characterSet; + m_letters = Baudot::m_ita2Letter; + m_figures = Baudot::m_ita2Figure; + m_characterSet = Baudot::ITA2; + break; + } +} + +void BaudotDecoder::setUnshiftOnSpace(bool unshiftOnSpace) +{ + m_unshiftOnSpace = unshiftOnSpace; +} + +void BaudotDecoder::init() +{ + m_figure = false; +} + +QString BaudotDecoder::decode(char bits) +{ + QString c = m_figure ? m_figures[bits] : m_letters[bits]; + + if ((c == '>') || (m_unshiftOnSpace && (c == " "))) + { + // Switch to letters + m_figure = false; + if (m_characterSet == Baudot::RUSSIAN) { + m_letters = Baudot::m_ita2Letter; + } + } + if (c == '<') + { + // Switch to figures + m_figure = true; + } + if ((m_characterSet == Baudot::RUSSIAN) && (c == '\0')) + { + // Switch to Cyrillic + m_figure = false; + m_letters = Baudot::m_russianLetter; + c = '^'; + } + + return c; +} + diff --git a/sdrbase/util/baudot.h b/sdrbase/util/baudot.h new file mode 100644 index 000000000..3f714c9be --- /dev/null +++ b/sdrbase/util/baudot.h @@ -0,0 +1,77 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_UTIL_BAUDOT_H +#define INCLUDE_UTIL_BAUDOT_H + +#include +#include +#include + +#include "export.h" + +class SDRBASE_API Baudot { + +public: + + enum CharacterSet { + ITA2, + UK, + EUROPEAN, + US, + RUSSIAN, // MTK-2 + MURRAY + }; + + // QString used for fractions in figure set + static const QString m_ita2Letter[]; + static const QString m_ita2Figure[]; + static const QString m_ukLetter[]; + static const QString m_ukFigure[]; + static const QString m_europeanLetter[]; + static const QString m_europeanFigure[]; + static const QString m_usLetter[]; + static const QString m_usFigure[]; + static const QString m_russianLetter[]; + static const QString m_russianFigure[]; + static const QString m_murrayLetter[]; + static const QString m_murrayFigure[]; + +}; + +class SDRBASE_API BaudotDecoder { + +public: + + BaudotDecoder(); + void setCharacterSet(Baudot::CharacterSet characterSet=Baudot::ITA2); + void setUnshiftOnSpace(bool unshiftOnSpace); + void init(); + QString decode(char bits); + +private: + + Baudot::CharacterSet m_characterSet; + bool m_unshiftOnSpace; + const QString *m_letters; + const QString *m_figures; + bool m_figure; + +}; + +#endif // INCLUDE_UTIL_BAUDOT_H + diff --git a/sdrbase/util/movingmaximum.h b/sdrbase/util/movingmaximum.h new file mode 100644 index 000000000..9d18bb5a3 --- /dev/null +++ b/sdrbase/util/movingmaximum.h @@ -0,0 +1,99 @@ +/////////////////////////////////////////////////////////////////////////////////////// +// // +// 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_UTIL_MOVINGMAXIMUM_H +#define INCLUDE_UTIL_MOVINGMAXIMUM_H + +#include +#include + +// Calculates moving maximum over a number of samples +template +class MovingMaximum +{ +public: + + MovingMaximum() : + m_samples(nullptr), + m_size(0) + { + reset(); + } + + ~MovingMaximum() + { + delete[] m_samples; + } + + void reset() + { + m_count = 0; + m_index = 0; + m_max = NAN; + } + + void setSize(int size) + { + delete[] m_samples; + m_samples = new T[size](); + m_size = size; + reset(); + } + + void operator()(T sample) + { + if (m_count < m_size) + { + m_samples[m_count++] = sample; + if (m_count == 1) { + m_max = sample; + } else { + m_max = std::max(m_max, sample); + } + } + else + { + T oldest = m_samples[m_index]; + m_samples[m_index] = sample; + m_index = (m_index + 1) % m_size; + m_max = std::max(m_max, sample); + if (oldest >= m_max) + { + // Find new maximum, that will be lower than the oldest sample + m_max = m_samples[0]; + for (unsigned int i = 1; i < m_size; i++) { + m_max = std::max(m_max, m_samples[i]); + } + } + } + } + + T getMaximum() const { + return m_max; + } + +private: + T *m_samples; + unsigned int m_size; // Max number of samples + unsigned int m_count; // Number of samples used + unsigned int m_index; // Current index + T m_max; +}; + +#endif /* INCLUDE_UTIL_MOVINGMAXIMUM_H */ + diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 383a49822..99712a4f3 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -4543,6 +4543,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setInterferometerSettings(new SWGSDRangel::SWGInterferometerSettings()); channelSettings->getInterferometerSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "NavtexDemodSettings") + { + channelSettings->setNavtexDemodSettings(new SWGSDRangel::SWGNavtexDemodSettings()); + channelSettings->getNavtexDemodSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "M17DemodSettings") { channelSettings->setM17DemodSettings(new SWGSDRangel::SWGM17DemodSettings()); @@ -4624,6 +4629,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setRemoteTcpSinkSettings(new SWGSDRangel::SWGRemoteTCPSinkSettings()); channelSettings->getRemoteTcpSinkSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "RTTYDemodSettings") + { + channelSettings->setRttyDemodSettings(new SWGSDRangel::SWGRTTYDemodSettings()); + channelSettings->getRttyDemodSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "SigMFFileSinkSettings") { channelSettings->setSigMfFileSinkSettings(new SWGSDRangel::SWGSigMFFileSinkSettings()); @@ -5382,6 +5392,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.setDsdDemodSettings(nullptr); channelSettings.setHeatMapSettings(nullptr); channelSettings.setIeee802154ModSettings(nullptr); + channelSettings.setNavtexDemodSettings(nullptr); channelSettings.setNfmDemodSettings(nullptr); channelSettings.setNfmModSettings(nullptr); channelSettings.setNoiseFigureSettings(nullptr); @@ -5394,6 +5405,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.setRemoteSinkSettings(nullptr); channelSettings.setRemoteSourceSettings(nullptr); channelSettings.setRemoteTcpSinkSettings(nullptr); + channelSettings.setRttyDemodSettings(nullptr); channelSettings.setSsbDemodSettings(nullptr); channelSettings.setSsbModSettings(nullptr); channelSettings.setUdpSourceSettings(nullptr); @@ -5418,6 +5430,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.setDatvModReport(nullptr); channelReport.setDsdDemodReport(nullptr); channelReport.setHeatMapReport(nullptr); + channelReport.setNavtexDemodReport(nullptr); channelReport.setNfmDemodReport(nullptr); channelReport.setNfmModReport(nullptr); channelReport.setNoiseFigureReport(nullptr); @@ -5427,6 +5440,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.setRadioClockReport(nullptr); channelReport.setRadiosondeDemodReport(nullptr); channelReport.setRemoteSourceReport(nullptr); + channelReport.setRttyDemodReport(nullptr); channelReport.setSsbDemodReport(nullptr); channelReport.setSsbModReport(nullptr); channelReport.setUdpSourceReport(nullptr); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 0d0f5ef3b..68a9e9008 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.navtexemod", "NavtexDemodSettings"}, {"sdrangel.channel.m17demod", "M17DemodSettings"}, {"sdrangel.channeltx.modm17", "M17ModSettings"}, {"sdrangel.channel.nfmdemod", "NFMDemodSettings"}, @@ -66,6 +67,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.demod.remotesink", "RemoteSinkSettings"}, {"sdrangel.demod.remotetcpsink", "RemoteTCPSinkSettings"}, {"sdrangel.channeltx.remotesource", "RemoteSourceSettings"}, + {"sdrangel.channel.rttydemod", "RTTYDemodSettings"}, {"sdrangel.channeltx.modssb", "SSBModSettings"}, {"sdrangel.channel.ssbdemod", "SSBDemodSettings"}, {"sdrangel.channel.ft8demod", "FT8DemodSettings"}, @@ -162,6 +164,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"IEEE_802_15_4_Mod", "IEEE_802_15_4_ModSettings"}, {"M17Demod", "M17DemodSettings"}, {"M17Mod", "M17ModSettings"}, + {"NavtexDemod", "NavtexDemodSettings"}, {"NFMDemod", "NFMDemodSettings"}, {"NFMMod", "NFMModSettings"}, {"NoiseFigure", "NoiseFigureSettings"}, @@ -176,6 +179,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"RemoteSink", "RemoteSinkSettings"}, {"RemoteSource", "RemoteSourceSettings"}, {"RemoteTCPSink", "RemoteTCPSinkSettings"}, + {"RTTYDemodSettings", "RTTYDemodSettings"}, {"SSBMod", "SSBModSettings"}, {"SSBDemod", "SSBDemodSettings"}, {"FT8Demod", "FT8DemodSettings"}, From 95b46937a7883d39edcf1b07c1c5358b027a2180 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:24:37 +0000 Subject: [PATCH 02/12] Add Navtex demodulator --- doc/img/NavtexDemod_plugin.png | Bin 0 -> 116590 bytes plugins/channelrx/demodnavtex/CMakeLists.txt | 63 + plugins/channelrx/demodnavtex/navtexdemod.cpp | 751 +++++++++++ plugins/channelrx/demodnavtex/navtexdemod.h | 224 ++++ .../demodnavtex/navtexdemodbaseband.cpp | 181 +++ .../demodnavtex/navtexdemodbaseband.h | 103 ++ .../channelrx/demodnavtex/navtexdemodgui.cpp | 878 +++++++++++++ .../channelrx/demodnavtex/navtexdemodgui.h | 150 +++ .../channelrx/demodnavtex/navtexdemodgui.ui | 1140 +++++++++++++++++ .../demodnavtex/navtexdemodplugin.cpp | 93 ++ .../channelrx/demodnavtex/navtexdemodplugin.h | 50 + .../demodnavtex/navtexdemodsettings.cpp | 209 +++ .../demodnavtex/navtexdemodsettings.h | 79 ++ .../channelrx/demodnavtex/navtexdemodsink.cpp | 493 +++++++ .../channelrx/demodnavtex/navtexdemodsink.h | 145 +++ .../demodnavtex/navtexdemodwebapiadapter.cpp | 52 + .../demodnavtex/navtexdemodwebapiadapter.h | 50 + plugins/channelrx/demodnavtex/readme.md | 101 ++ sdrbase/util/navtex.cpp | 729 +++++++++++ sdrbase/util/navtex.h | 111 ++ .../api/swagger/include/ChannelReport.yaml | 4 + .../api/swagger/include/ChannelSettings.yaml | 4 + .../api/swagger/include/NavtexDemod.yaml | 62 + .../api/swagger/include/RTTYDemod.yaml | 73 ++ .../code/qt5/client/SWGChannelReport.cpp | 50 + .../code/qt5/client/SWGChannelReport.h | 14 + .../code/qt5/client/SWGChannelSettings.cpp | 50 + .../code/qt5/client/SWGChannelSettings.h | 14 + .../code/qt5/client/SWGModelFactory.h | 24 + .../code/qt5/client/SWGNavtexDemodReport.cpp | 131 ++ .../code/qt5/client/SWGNavtexDemodReport.h | 64 + .../qt5/client/SWGNavtexDemodSettings.cpp | 586 +++++++++ .../code/qt5/client/SWGNavtexDemodSettings.h | 182 +++ .../code/qt5/client/SWGRTTYDemodReport.h | 64 + .../code/qt5/client/SWGRTTYDemodSettings.cpp | 697 ++++++++++ 35 files changed, 7621 insertions(+) create mode 100644 doc/img/NavtexDemod_plugin.png create mode 100644 plugins/channelrx/demodnavtex/CMakeLists.txt create mode 100644 plugins/channelrx/demodnavtex/navtexdemod.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemod.h create mode 100644 plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemodbaseband.h create mode 100644 plugins/channelrx/demodnavtex/navtexdemodgui.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemodgui.h create mode 100644 plugins/channelrx/demodnavtex/navtexdemodgui.ui create mode 100644 plugins/channelrx/demodnavtex/navtexdemodplugin.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemodplugin.h create mode 100644 plugins/channelrx/demodnavtex/navtexdemodsettings.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemodsettings.h create mode 100644 plugins/channelrx/demodnavtex/navtexdemodsink.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemodsink.h create mode 100644 plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.cpp create mode 100644 plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.h create mode 100644 plugins/channelrx/demodnavtex/readme.md create mode 100644 sdrbase/util/navtex.cpp create mode 100644 sdrbase/util/navtex.h create mode 100644 swagger/sdrangel/api/swagger/include/NavtexDemod.yaml create mode 100644 swagger/sdrangel/api/swagger/include/RTTYDemod.yaml create mode 100644 swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.cpp diff --git a/doc/img/NavtexDemod_plugin.png b/doc/img/NavtexDemod_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..6813577532d6fcef944698234f0262f05ba0eb1c GIT binary patch literal 116590 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 z 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 LCahTT7UKT^ecXl& literal 0 HcmV?d00001 diff --git a/plugins/channelrx/demodnavtex/CMakeLists.txt b/plugins/channelrx/demodnavtex/CMakeLists.txt new file mode 100644 index 000000000..fbb93f63b --- /dev/null +++ b/plugins/channelrx/demodnavtex/CMakeLists.txt @@ -0,0 +1,63 @@ +project(demodnavtex) + +set(demodnavtex_SOURCES + navtexdemod.cpp + navtexdemodsettings.cpp + navtexdemodbaseband.cpp + navtexdemodsink.cpp + navtexdemodplugin.cpp + navtexdemodwebapiadapter.cpp +) + +set(demodnavtex_HEADERS + navtexdemod.h + navtexdemodsettings.h + navtexdemodbaseband.h + navtexdemodsink.h + navtexdemodplugin.h + navtexdemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(demodnavtex_SOURCES + ${demodnavtex_SOURCES} + navtexdemodgui.cpp + navtexdemodgui.ui + ) + set(demodnavtex_HEADERS + ${demodnavtex_HEADERS} + navtexdemodgui.h + ) + + set(TARGET_NAME demodnavtex) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodnavtexsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demodnavtex_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/demodnavtex/navtexdemod.cpp b/plugins/channelrx/demodnavtex/navtexdemod.cpp new file mode 100644 index 000000000..5ec7cbad5 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemod.cpp @@ -0,0 +1,751 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "navtexdemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGNavtexDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGMapItem.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "settings/serializable.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(NavtexDemod::MsgConfigureNavtexDemod, Message) +MESSAGE_CLASS_DEFINITION(NavtexDemod::MsgCharacter, Message) +MESSAGE_CLASS_DEFINITION(NavtexDemod::MsgMessage, Message) + +const char * const NavtexDemod::m_channelIdURI = "sdrangel.channel.navtexdemod"; +const char * const NavtexDemod::m_channelId = "NavtexDemod"; + +NavtexDemod::NavtexDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new NavtexDemodBaseband(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, + &NavtexDemod::networkManagerFinished + ); + QObject::connect( + this, + &ChannelAPI::indexInDeviceSetChanged, + this, + &NavtexDemod::handleIndexInDeviceSetChanged + ); +} + +NavtexDemod::~NavtexDemod() +{ + qDebug("NavtexDemod::~NavtexDemod"); + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &NavtexDemod::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +void NavtexDemod::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 NavtexDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void NavtexDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void NavtexDemod::start() +{ + qDebug("NavtexDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + NavtexDemodBaseband::MsgConfigureNavtexDemodBaseband *msg = NavtexDemodBaseband::MsgConfigureNavtexDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void NavtexDemod::stop() +{ + qDebug("NavtexDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool NavtexDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureNavtexDemod::match(cmd)) + { + MsgConfigureNavtexDemod& cfg = (MsgConfigureNavtexDemod&) cmd; + qDebug() << "NavtexDemod::handleMessage: MsgConfigureNavtexDemod"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "NavtexDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + // Forward to GUI if any + if (m_guiMessageQueue) { + m_guiMessageQueue->push(new DSPSignalNotification(notif)); + } + + return true; + } + else if (NavtexDemod::MsgCharacter::match(cmd)) + { + // Forward to GUI + NavtexDemod::MsgCharacter& report = (NavtexDemod::MsgCharacter&)cmd; + if (getMessageQueueToGUI()) + { + NavtexDemod::MsgCharacter *msg = new NavtexDemod::MsgCharacter(report); + getMessageQueueToGUI()->push(msg); + } + + return true; + } + else if (NavtexDemod::MsgMessage::match(cmd)) + { + // Forward to GUI + NavtexDemod::MsgMessage& report = (NavtexDemod::MsgMessage&)cmd; + if (getMessageQueueToGUI()) + { + NavtexDemod::MsgMessage *msg = new NavtexDemod::MsgMessage(report); + getMessageQueueToGUI()->push(msg); + } + + // Forward via UDP + if (m_settings.m_udpEnabled) + { + qDebug() << "Forwarding to " << m_settings.m_udpAddress << ":" << m_settings.m_udpPort; + QByteArray bytes = report.getMessage().m_message.toUtf8(); + m_udpSocket.writeDatagram(bytes, bytes.size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + + // Write to log file + if (m_logFile.isOpen()) + { + const NavtexMessage &navtexMsg = report.getMessage(); + + if (navtexMsg.m_valid) + { + m_logStream << navtexMsg.m_dateTime.date().toString() << "," + << navtexMsg.m_dateTime.time().toString() << "," + << navtexMsg.m_stationId << "," + << navtexMsg.m_typeId << "," + << navtexMsg.m_id << "," + << "\"" << navtexMsg.m_message << "\"," + << report.getErrors() << "," + << report.getRSSI() + << "\n"; + } + } + + return true; + } + else if (MainCore::MsgChannelDemodQuery::match(cmd)) + { + qDebug() << "NavtexDemod::handleMessage: MsgChannelDemodQuery"; + sendSampleRateToDemodAnalyzer(); + + return true; + } + else + { + return false; + } +} + +ScopeVis *NavtexDemod::getScopeSink() +{ + return m_basebandSink->getScopeSink(); +} + +void NavtexDemod::setCenterFrequency(qint64 frequency) +{ + NavtexDemodSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureNavtexDemod *msgToGUI = MsgConfigureNavtexDemod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void NavtexDemod::applySettings(const NavtexDemodSettings& settings, bool force) +{ + qDebug() << "NavtexDemod::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_navArea != m_settings.m_navArea) || force) { + reverseAPIKeys.append("navArea"); + } + if ((settings.m_filterStation != m_settings.m_filterStation) || force) { + reverseAPIKeys.append("filterStation"); + } + if ((settings.m_filterType != m_settings.m_filterType) || force) { + reverseAPIKeys.append("filterType"); + } + 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"); + } + + NavtexDemodBaseband::MsgConfigureNavtexDemodBaseband *msg = NavtexDemodBaseband::MsgConfigureNavtexDemodBaseband::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() << "NavtexDemod::applySettings - Logging to: " << settings.m_logFilename; + bool newFile = m_logFile.size() == 0; + m_logStream.setDevice(&m_logFile); + if (newFile) + { + // Write header + m_logStream << "Date,Time,SID,TID,MID,Message,Errors,RSSI\n"; + } + } + else + { + qDebug() << "NavtexDemod::applySettings - Unable to open log file: " << settings.m_logFilename; + } + } + } + + m_settings = settings; +} + +void NavtexDemod::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, + NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE + ); + messageQueue->push(msg); + } + } +} + +QByteArray NavtexDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool NavtexDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureNavtexDemod *msg = MsgConfigureNavtexDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureNavtexDemod *msg = MsgConfigureNavtexDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int NavtexDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setNavtexDemodSettings(new SWGSDRangel::SWGNavtexDemodSettings()); + response.getNavtexDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int NavtexDemod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int NavtexDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + NavtexDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureNavtexDemod *msg = MsgConfigureNavtexDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("NavtexDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureNavtexDemod *msgToGUI = MsgConfigureNavtexDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +int NavtexDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setNavtexDemodReport(new SWGSDRangel::SWGNavtexDemodReport()); + response.getNavtexDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void NavtexDemod::webapiUpdateChannelSettings( + NavtexDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getNavtexDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getNavtexDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("navArea")) { + settings.m_navArea = response.getNavtexDemodSettings()->getNavArea(); + } + if (channelSettingsKeys.contains("filterStation")) { + settings.m_filterStation = *response.getNavtexDemodSettings()->getFilterStation(); + } + if (channelSettingsKeys.contains("filterType")) { + settings.m_filterType = *response.getNavtexDemodSettings()->getFilterType(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getNavtexDemodSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getNavtexDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getNavtexDemodSettings()->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.getNavtexDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getNavtexDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getNavtexDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getNavtexDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getNavtexDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getNavtexDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getNavtexDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getNavtexDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) { + settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getNavtexDemodSettings()->getScopeConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getNavtexDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getNavtexDemodSettings()->getRollupState()); + } +} + +void NavtexDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const NavtexDemodSettings& settings) +{ + response.getNavtexDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getNavtexDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getNavtexDemodSettings()->setNavArea(settings.m_navArea); + response.getNavtexDemodSettings()->setFilterStation(new QString(settings.m_filterStation)); + response.getNavtexDemodSettings()->setFilterType(new QString(settings.m_filterType)); + response.getNavtexDemodSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getNavtexDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getNavtexDemodSettings()->setUdpPort(settings.m_udpPort); + response.getNavtexDemodSettings()->setLogFilename(new QString(settings.m_logFilename)); + response.getNavtexDemodSettings()->setLogEnabled(settings.m_logEnabled); + + response.getNavtexDemodSettings()->setRgbColor(settings.m_rgbColor); + if (response.getNavtexDemodSettings()->getTitle()) { + *response.getNavtexDemodSettings()->getTitle() = settings.m_title; + } else { + response.getNavtexDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getNavtexDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getNavtexDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getNavtexDemodSettings()->getReverseApiAddress()) { + *response.getNavtexDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getNavtexDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getNavtexDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getNavtexDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getNavtexDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_scopeGUI) + { + if (response.getNavtexDemodSettings()->getScopeConfig()) + { + settings.m_scopeGUI->formatTo(response.getNavtexDemodSettings()->getScopeConfig()); + } + else + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + response.getNavtexDemodSettings()->setScopeConfig(swgGLScope); + } + } + if (settings.m_channelMarker) + { + if (response.getNavtexDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getNavtexDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getNavtexDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getNavtexDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getNavtexDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getNavtexDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void NavtexDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + + response.getNavtexDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg)); + response.getNavtexDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate()); +} + +void NavtexDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const NavtexDemodSettings& 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 NavtexDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const NavtexDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("NavtexDemod")); + swgChannelSettings->setNavtexDemodSettings(new SWGSDRangel::SWGNavtexDemodSettings()); + SWGSDRangel::SWGNavtexDemodSettings *swgNavtexDemodSettings = swgChannelSettings->getNavtexDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgNavtexDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgNavtexDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("navArea") || force) { + swgNavtexDemodSettings->setNavArea(settings.m_navArea); + } + if (channelSettingsKeys.contains("filterStation") || force) { + swgNavtexDemodSettings->setFilterStation(new QString(settings.m_filterStation)); + } + if (channelSettingsKeys.contains("filterType") || force) { + swgNavtexDemodSettings->setFilterType(new QString(settings.m_filterType)); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgNavtexDemodSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgNavtexDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgNavtexDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("logFilename") || force) { + swgNavtexDemodSettings->setLogFilename(new QString(settings.m_logFilename)); + } + if (channelSettingsKeys.contains("logEnabled") || force) { + swgNavtexDemodSettings->setLogEnabled(settings.m_logEnabled); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgNavtexDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgNavtexDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgNavtexDemodSettings->setStreamIndex(settings.m_streamIndex); + } + + if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force)) + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + swgNavtexDemodSettings->setScopeConfig(swgGLScope); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgNavtexDemodSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgNavtexDemodSettings->setRollupState(swgRollupState); + } +} + +void NavtexDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "NavtexDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("NavtexDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void NavtexDemod::handleIndexInDeviceSetChanged(int index) +{ + if (index < 0) { + return; + } + + QString fifoLabel = QString("%1 [%2:%3]") + .arg(m_channelId) + .arg(m_deviceAPI->getDeviceSetIndex()) + .arg(index); + m_basebandSink->setFifoLabel(fifoLabel); +} + diff --git a/plugins/channelrx/demodnavtex/navtexdemod.h b/plugins/channelrx/demodnavtex/navtexdemod.h new file mode 100644 index 000000000..9322471d9 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemod.h @@ -0,0 +1,224 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_NAVTEXDEMOD_H +#define INCLUDE_NAVTEXDEMOD_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" +#include "util/navtex.h" + +#include "navtexdemodbaseband.h" +#include "navtexdemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ScopeVis; + +class NavtexDemod : public BasebandSampleSink, public ChannelAPI { +public: + class MsgConfigureNavtexDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const NavtexDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureNavtexDemod* create(const NavtexDemodSettings& settings, bool force) + { + return new MsgConfigureNavtexDemod(settings, force); + } + + private: + NavtexDemodSettings m_settings; + bool m_force; + + MsgConfigureNavtexDemod(const NavtexDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgCharacter : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getCharacter() const { return m_character; } + + static MsgCharacter* create(const QString& character) + { + return new MsgCharacter(character); + } + + private: + QString m_character; + + MsgCharacter(const QString& character) : + m_character(character) + {} + }; + + class MsgMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const NavtexMessage& getMessage() const { return m_message; } + int getErrors() const { return m_errors; } + float getRSSI() const { return m_rssi; } + + static MsgMessage* create(const NavtexMessage& message, int errors, float rssi) + { + return new MsgMessage(message, errors, rssi); + } + + private: + NavtexMessage m_message; + int m_errors; + float m_rssi; + + MsgMessage(const NavtexMessage& message, int errors, float rssi) : + m_message(message), + m_errors(errors), + m_rssi(rssi) + {} + }; + + NavtexDemod(DeviceAPI *deviceAPI); + virtual ~NavtexDemod(); + 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 NavtexDemodSettings& settings); + + static void webapiUpdateChannelSettings( + NavtexDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + ScopeVis *getScopeSink(); + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } +/* void 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; + NavtexDemodBaseband* m_basebandSink; + NavtexDemodSettings m_settings; + 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 NavtexDemodSettings& settings, bool force = false); + void sendSampleRateToDemodAnalyzer(); + void webapiReverseSendSettings(QList& channelSettingsKeys, const NavtexDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const NavtexDemodSettings& settings, + bool force + ); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); + +}; + +#endif // INCLUDE_NAVTEXDEMOD_H + diff --git a/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp b/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp new file mode 100644 index 000000000..464942421 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp @@ -0,0 +1,181 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "navtexdemodbaseband.h" + +MESSAGE_CLASS_DEFINITION(NavtexDemodBaseband::MsgConfigureNavtexDemodBaseband, Message) + +NavtexDemodBaseband::NavtexDemodBaseband(NavtexDemod *packetDemod) : + m_sink(packetDemod), + m_running(false) +{ + qDebug("NavtexDemodBaseband::NavtexDemodBaseband"); + + m_sink.setScopeSink(&m_scopeSink); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +NavtexDemodBaseband::~NavtexDemodBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void NavtexDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void NavtexDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &NavtexDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void NavtexDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &NavtexDemodBaseband::handleData + ); + m_running = false; +} + +void NavtexDemodBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void NavtexDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void NavtexDemodBaseband::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 NavtexDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool NavtexDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureNavtexDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureNavtexDemodBaseband& cfg = (MsgConfigureNavtexDemodBaseband&) cmd; + qDebug() << "NavtexDemodBaseband::handleMessage: MsgConfigureNavtexDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "NavtexDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + + return true; + } + else + { + return false; + } +} + +void NavtexDemodBaseband::applySettings(const NavtexDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int NavtexDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void NavtexDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} + diff --git a/plugins/channelrx/demodnavtex/navtexdemodbaseband.h b/plugins/channelrx/demodnavtex/navtexdemodbaseband.h new file mode 100644 index 000000000..d8ab73ff9 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodbaseband.h @@ -0,0 +1,103 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_NAVTEXDEMODBASEBAND_H +#define INCLUDE_NAVTEXDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/scopevis.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "navtexdemodsink.h" + +class DownChannelizer; +class ChannelAPI; +class NavtexDemod; +class ScopeVis; + +class NavtexDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureNavtexDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const NavtexDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureNavtexDemodBaseband* create(const NavtexDemodSettings& settings, bool force) + { + return new MsgConfigureNavtexDemodBaseband(settings, force); + } + + private: + NavtexDemodSettings m_settings; + bool m_force; + + MsgConfigureNavtexDemodBaseband(const NavtexDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + NavtexDemodBaseband(NavtexDemod *packetDemod); + ~NavtexDemodBaseband(); + 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; + ScopeVis *getScopeSink() { return &m_scopeSink; } + void setChannel(ChannelAPI *channel); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + NavtexDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + NavtexDemodSettings m_settings; + ScopeVis m_scopeSink; + bool m_running; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(NavtexDemodSink *sink); + void applySettings(const NavtexDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_NAVTEXDEMODBASEBAND_H + diff --git a/plugins/channelrx/demodnavtex/navtexdemodgui.cpp b/plugins/channelrx/demodnavtex/navtexdemodgui.cpp new file mode 100644 index 000000000..42ecf5e17 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodgui.cpp @@ -0,0 +1,878 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "navtexdemodgui.h" + +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_navtexdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/csv.h" +#include "util/db.h" +#include "util/morse.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "gui/decimaldelegate.h" +#include "dsp/dspengine.h" +#include "dsp/glscopesettings.h" +#include "gui/crightclickenabler.h" +#include "gui/tabletapandhold.h" +#include "gui/dialogpositioner.h" +#include "channel/channelwebapiutils.h" +#include "feature/featurewebapiutils.h" +#include "maincore.h" + +#include "navtexdemod.h" +#include "navtexdemodsink.h" + +void NavtexDemodGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + // Trailing spaces are for sort arrow + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("15/04/2016-")); + ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("10:17")); + ui->messages->setItem(row, MESSAGE_COL_STATION_ID, new QTableWidgetItem("A")); + ui->messages->setItem(row, MESSAGE_COL_STATION, new QTableWidgetItem("Netherlands")); + ui->messages->setItem(row, MESSAGE_COL_TYPE_ID, new QTableWidgetItem("B")); + ui->messages->setItem(row, MESSAGE_COL_TYPE, new QTableWidgetItem("Meteorological\nwarning")); + ui->messages->setItem(row, MESSAGE_COL_MESSAGE_ID, new QTableWidgetItem("12")); + ui->messages->setItem(row, MESSAGE_COL_MESSAGE, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ\n123456789")); + ui->messages->setItem(row, MESSAGE_COL_ERRORS, new QTableWidgetItem("100")); + ui->messages->setItem(row, MESSAGE_COL_ERROR_PERCENT, new QTableWidgetItem("10.0%")); + ui->messages->resizeColumnsToContents(); + ui->messages->removeRow(row); +} + +// Columns in table reordered +void NavtexDemodGUI::messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_columnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void NavtexDemodGUI::messages_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_columnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void NavtexDemodGUI::columnSelectMenu(QPoint pos) +{ + menu->popup(ui->messages->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void NavtexDemodGUI::columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->messages->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *NavtexDemodGUI::createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked())); + return action; +} + +NavtexDemodGUI* NavtexDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + NavtexDemodGUI* gui = new NavtexDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void NavtexDemodGUI::destroy() +{ + delete this; +} + +void NavtexDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray NavtexDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool NavtexDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +qint64 NavtexDemodGUI::getFrequency() +{ + qint64 frequency; + + // m_deviceCenterFrequency may sometimes be 0 if using file source + frequency = (m_deviceCenterFrequency ? m_deviceCenterFrequency : 518000) + m_settings.m_inputFrequencyOffset; + + return ((frequency + 500) / 1000) * 1000; // Round to nearest kHz +} + +// Add row to table +void NavtexDemodGUI::messageReceived(const NavtexMessage& message, int errors, float rssi) +{ + // Is scroll bar at bottom + QScrollBar *sb = ui->messages->verticalScrollBar(); + bool scrollToBottom = sb->value() == sb->maximum(); + + ui->messages->setSortingEnabled(false); + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + + QTableWidgetItem *dateItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *stationIdItem = new QTableWidgetItem(); + QTableWidgetItem *stationItem = new QTableWidgetItem(); + QTableWidgetItem *typeIdItem = new QTableWidgetItem(); + QTableWidgetItem *typeItem = new QTableWidgetItem(); + QTableWidgetItem *messageIdItem = new QTableWidgetItem(); + QTableWidgetItem *messageItem = new QTableWidgetItem(); + QTableWidgetItem *errorsItem = new QTableWidgetItem(); + QTableWidgetItem *errorPercentItem = new QTableWidgetItem(); + QTableWidgetItem *rssiItem = new QTableWidgetItem(); + ui->messages->setItem(row, MESSAGE_COL_DATE, dateItem); + ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); + ui->messages->setItem(row, MESSAGE_COL_STATION_ID, stationIdItem); + ui->messages->setItem(row, MESSAGE_COL_STATION, stationItem); + ui->messages->setItem(row, MESSAGE_COL_TYPE_ID, typeIdItem); + ui->messages->setItem(row, MESSAGE_COL_TYPE, typeItem); + ui->messages->setItem(row, MESSAGE_COL_MESSAGE_ID, messageIdItem); + ui->messages->setItem(row, MESSAGE_COL_MESSAGE, messageItem); + ui->messages->setItem(row, MESSAGE_COL_ERRORS, errorsItem); + ui->messages->setItem(row, MESSAGE_COL_ERROR_PERCENT, errorPercentItem); + ui->messages->setItem(row, MESSAGE_COL_RSSI, rssiItem); + + dateItem->setData(Qt::DisplayRole, message.m_dateTime.date()); + timeItem->setData(Qt::DisplayRole, message.m_dateTime.time()); + if (message.m_valid) + { + QString station = message.getStation(m_settings.m_navArea, getFrequency()); + QString type = message.getType(); + stationIdItem->setText(message.m_stationId); + stationItem->setText(station); + typeIdItem->setText(message.m_typeId); + typeItem->setText(type); + messageIdItem->setText(message.m_id); + // Add to filter comboboxes + if (!station.isEmpty() && (ui->filterStation->findText(station) == -1)) { + ui->filterStation->addItem(station); + } + if (!type.isEmpty() && (ui->filterType->findText(type) == -1)) { + ui->filterType->addItem(type); + } + errorsItem->setData(Qt::DisplayRole, errors); + float errorPC = 100.0f * errors / (message.m_message.size() * 2.0f); // SITOR-B sends each character twice + errorPercentItem->setData(Qt::DisplayRole, errorPC); + rssiItem->setData(Qt::DisplayRole, rssi); + } + messageItem->setText(message.m_message); + ui->messages->setSortingEnabled(true); + ui->messages->resizeRowToContents(row); + if (scrollToBottom) { + ui->messages->scrollToBottom(); + } + filterRow(row); +} + +bool NavtexDemodGUI::handleMessage(const Message& message) +{ + if (NavtexDemod::MsgConfigureNavtexDemod::match(message)) + { + qDebug("NavtexDemodGUI::handleMessage: NavtexDemod::MsgConfigureNavtexDemod"); + const NavtexDemod::MsgConfigureNavtexDemod& cfg = (NavtexDemod::MsgConfigureNavtexDemod&) 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 (NavtexDemod::MsgCharacter::match(message)) + { + NavtexDemod::MsgCharacter& report = (NavtexDemod::MsgCharacter&) message; + QString c = report.getCharacter(); + + // Is the scroll bar at the bottom? + int scrollPos = ui->text->verticalScrollBar()->value(); + bool atBottom = scrollPos >= ui->text->verticalScrollBar()->maximum(); + + // Move cursor to end where we want to append new text + // (user may have moved it by clicking / highlighting text) + ui->text->moveCursor(QTextCursor::End); + + // Restore scroll position + ui->text->verticalScrollBar()->setValue(scrollPos); + + if (c == '\b') { + ui->text->textCursor().deletePreviousChar(); + } else { + ui->text->insertPlainText(c); + } + + // Scroll to bottom, if we we're previously at the bottom + if (atBottom) { + ui->text->verticalScrollBar()->setValue(ui->text->verticalScrollBar()->maximum()); + } + + return true; + } + else if (NavtexDemod::MsgMessage::match(message)) + { + NavtexDemod::MsgMessage& textMsg = (NavtexDemod::MsgMessage&) message; + messageReceived(textMsg.getMessage(), textMsg.getErrors(), textMsg.getRSSI()); + return true; + } + + return false; +} + +void NavtexDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void NavtexDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void NavtexDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void NavtexDemodGUI::updateTxStation() +{ + const NavtexTransmitter *transmitter = NavtexTransmitter::getTransmitter(QDateTime::currentDateTimeUtc().time(), + m_settings.m_navArea, + getFrequency()); + if (transmitter) + { + ui->txStation->setText(transmitter->m_station); + } + else + { + ui->txStation->setText(""); + } +} + +void NavtexDemodGUI::on_findOnMapFeature_clicked() +{ + QString station = ui->txStation->text(); + if (!station.isEmpty()) { + FeatureWebAPIUtils::mapFind(station); + } +} + +void NavtexDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void NavtexDemodGUI::on_rfBW_valueChanged(int value) +{ + float bw = value; + ui->rfBWText->setText(QString("%1 Hz").arg((int)bw)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void NavtexDemodGUI::on_filterStation_currentIndexChanged(int index) +{ + m_settings.m_filterStation = ui->filterStation->currentText(); + filter(); + applySettings(); +} + +void NavtexDemodGUI::on_filterType_currentIndexChanged(int index) +{ + m_settings.m_filterType = ui->filterType->currentText(); + filter(); + applySettings(); +} + +void NavtexDemodGUI::on_clearTable_clicked() +{ + ui->messages->setRowCount(0); + ui->text->clear(); +} + +void NavtexDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void NavtexDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void NavtexDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void NavtexDemodGUI::filterRow(int row) +{ + bool hidden = false; + if ((m_settings.m_filterStation != "") && (m_settings.m_filterStation != "All")) + { + QTableWidgetItem *stationItem = ui->messages->item(row, MESSAGE_COL_STATION); + if (m_settings.m_filterStation != stationItem->text()) { + hidden = true; + } + } + if ((m_settings.m_filterType != "") && (m_settings.m_filterType != "All")) + { + QTableWidgetItem *typeItem = ui->messages->item(row, MESSAGE_COL_TYPE); + if (m_settings.m_filterType != typeItem->text()) { + hidden = true; + } + } + ui->messages->setRowHidden(row, hidden); +} + +void NavtexDemodGUI::filter() +{ + for (int i = 0; i < ui->messages->rowCount(); i++) { + filterRow(i); + } +} + +void NavtexDemodGUI::on_navArea_currentIndexChanged(int index) +{ + m_settings.m_navArea = index + 1; + updateTxStation(); + applySettings(); +} + +void NavtexDemodGUI::on_channel1_currentIndexChanged(int index) +{ + m_settings.m_scopeCh1 = index; + applySettings(); +} + +void NavtexDemodGUI::on_channel2_currentIndexChanged(int index) +{ + m_settings.m_scopeCh2 = index; + applySettings(); +} + +void NavtexDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void NavtexDemodGUI::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_navtexDemod->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(); +} + +NavtexDemodGUI::NavtexDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::NavtexDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_doApplySettings(true), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demodnavtex/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_navtexDemod = reinterpret_cast(rxChannel); + m_navtexDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + 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); + + ui->messages->setItemDelegateForColumn(MESSAGE_COL_ERROR_PERCENT, new DecimalDelegate(1)); + ui->messages->setItemDelegateForColumn(MESSAGE_COL_RSSI, new DecimalDelegate(1)); + + m_scopeVis = m_navtexDemod->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(NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_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() + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("Navtex 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.setRollupState(&m_rollupState); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->messages->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->messages->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + menu = new QMenu(ui->messages); + for (int i = 0; i < ui->messages->horizontalHeader()->count(); i++) + { + QString text = ui->messages->horizontalHeaderItem(i)->text(); + menu->addAction(createCheckableItem(text, i, true)); + } + ui->messages->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->messages->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(messages_sectionMoved(int, int, int))); + connect(ui->messages->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(messages_sectionResized(int, int, int))); + + ui->messages->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint))); + TableTapAndHold *tableTapAndHold = new TableTapAndHold(ui->messages); + connect(tableTapAndHold, &TableTapAndHold::tapAndHold, this, &NavtexDemodGUI::customContextMenuRequested); + + ui->scopeContainer->setVisible(false); + + displaySettings(); + makeUIConnections(); + applySettings(true); +} + +void NavtexDemodGUI::customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->messages->itemAt(pos); + if (item) + { + int row = item->row(); + QString station = ui->messages->item(row, MESSAGE_COL_STATION)->text(); + + QMenu* tableContextMenu = new QMenu(ui->messages); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + QAction* copyAction = new QAction("Copy", tableContextMenu); + const QString text = item->text(); + connect(copyAction, &QAction::triggered, this, [text]()->void { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); + }); + tableContextMenu->addAction(copyAction); + + if (!station.isEmpty()) + { + tableContextMenu->addSeparator(); + QAction* findOnMapAction = new QAction(QString("Find %1 on map").arg(station), tableContextMenu); + connect(findOnMapAction, &QAction::triggered, this, [station]()->void { + FeatureWebAPIUtils::mapFind(station); + }); + tableContextMenu->addAction(findOnMapAction); + } + + tableContextMenu->popup(ui->messages->viewport()->mapToGlobal(pos)); + } +} + +NavtexDemodGUI::~NavtexDemodGUI() +{ + delete ui; +} + +void NavtexDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void NavtexDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + NavtexDemod::MsgConfigureNavtexDemod* message = NavtexDemod::MsgConfigureNavtexDemod::create( m_settings, force); + m_navtexDemod->getInputMessageQueue()->push(message); + } +} + +void NavtexDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + setTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + ui->rfBWText->setText(QString("%1 Hz").arg((int)m_settings.m_rfBandwidth)); + ui->rfBW->setValue(m_settings.m_rfBandwidth); + + ui->navArea->setCurrentIndex(m_settings.m_navArea - 1); + + updateIndexLabel(); + + ui->filterStation->setCurrentText(m_settings.m_filterStation); + ui->filterType->setCurrentText(m_settings.m_filterType); + + 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); + + // Order and size columns + QHeaderView *header = ui->messages->horizontalHeader(); + for (int i = 0; i < NAVTEXDEMOD_COLUMNS; i++) + { + bool hidden = m_settings.m_columnSizes[i] == 0; + header->setSectionHidden(i, hidden); + menu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_columnSizes[i] > 0) + ui->messages->setColumnWidth(i, m_settings.m_columnSizes[i]); + header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]); + } + + filter(); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); +} + +void NavtexDemodGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void NavtexDemodGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void NavtexDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_navtexDemod->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)); + } + if (m_tickCount % (50*10) == 0) { + updateTxStation(); + } + + m_tickCount++; +} + +void NavtexDemodGUI::on_logEnable_clicked(bool checked) +{ + m_settings.m_logEnabled = checked; + applySettings(); +} + +void NavtexDemodGUI::on_logFilename_clicked() +{ + // Get filename to save to + QFileDialog fileDialog(nullptr, "Select file to log received messages 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(); + } + } +} + +// Read .csv log and process as received messages +void NavtexDemodGUI::on_logOpen_clicked() +{ + QFileDialog fileDialog(nullptr, "Select .csv log file to read", "", "*.csv"); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + QFile file(fileNames[0]); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + QString error; + QHash colIndexes = CSV::readHeader(in, {"Date", "Time", "SID", "TID", "MID", "Message"}, error); + if (error.isEmpty()) + { + int dateCol = colIndexes.value("Date"); + int timeCol = colIndexes.value("Time"); + int sidCol = colIndexes.value("SID"); + int tidCol = colIndexes.value("TID"); + int midCol = colIndexes.value("MID"); + int messageCol = colIndexes.value("Message"); + int errorsCol = colIndexes.value("Errors"); + int rssiCol = colIndexes.value("RSSI"); + int maxCol = std::max({dateCol, timeCol, sidCol, tidCol, midCol, messageCol}); + + QMessageBox dialog(this); + dialog.setText("Reading message data"); + dialog.addButton(QMessageBox::Cancel); + dialog.show(); + QApplication::processEvents(); + int count = 0; + bool cancelled = false; + QStringList cols; + while (!cancelled && CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + QDate date = QDate::fromString(cols[dateCol]); + QTime time = QTime::fromString(cols[timeCol]); + QDateTime dateTime(date, time); + NavtexMessage message(dateTime, + cols[sidCol], + cols[tidCol], + cols[midCol], + cols[messageCol]); + int errors = cols[errorsCol].toInt(); + float rssi = cols[rssiCol].toFloat(); + messageReceived(message, errors, rssi); + if (count % 1000 == 0) + { + QApplication::processEvents(); + if (dialog.clickedButton()) { + cancelled = true; + } + } + count++; + } + } + dialog.close(); + } + else + { + QMessageBox::critical(this, "Navtex Demod", error); + } + } + else + { + QMessageBox::critical(this, "Navtex Demod", QString("Failed to open file %1").arg(fileNames[0])); + } + } + } +} + +void NavtexDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &NavtexDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &NavtexDemodGUI::on_rfBW_valueChanged); + QObject::connect(ui->navArea, QOverload::of(&QComboBox::currentIndexChanged), this, &NavtexDemodGUI::on_navArea_currentIndexChanged); + QObject::connect(ui->findOnMapFeature, &QPushButton::clicked, this, &NavtexDemodGUI::on_findOnMapFeature_clicked); + QObject::connect(ui->filterStation, QOverload::of(&QComboBox::currentIndexChanged), this, &NavtexDemodGUI::on_filterStation_currentIndexChanged); + QObject::connect(ui->filterType, QOverload::of(&QComboBox::currentIndexChanged), this, &NavtexDemodGUI::on_filterType_currentIndexChanged); + QObject::connect(ui->clearTable, &QPushButton::clicked, this, &NavtexDemodGUI::on_clearTable_clicked); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &NavtexDemodGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &NavtexDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &NavtexDemodGUI::on_udpPort_editingFinished); + QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &NavtexDemodGUI::on_logEnable_clicked); + QObject::connect(ui->logFilename, &QToolButton::clicked, this, &NavtexDemodGUI::on_logFilename_clicked); + QObject::connect(ui->logOpen, &QToolButton::clicked, this, &NavtexDemodGUI::on_logOpen_clicked); + QObject::connect(ui->channel1, QOverload::of(&QComboBox::currentIndexChanged), this, &NavtexDemodGUI::on_channel1_currentIndexChanged); + QObject::connect(ui->channel2, QOverload::of(&QComboBox::currentIndexChanged), this, &NavtexDemodGUI::on_channel2_currentIndexChanged); +} + +void NavtexDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); + updateTxStation(); +} + diff --git a/plugins/channelrx/demodnavtex/navtexdemodgui.h b/plugins/channelrx/demodnavtex/navtexdemodgui.h new file mode 100644 index 000000000..071aa4dd6 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodgui.h @@ -0,0 +1,150 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_NAVTEXDEMODGUI_H +#define INCLUDE_NAVTEXDEMODGUI_H + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include "navtexdemod.h" +#include "navtexdemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class NavtexDemod; +class NavtexDemodGUI; + +namespace Ui { + class NavtexDemodGUI; +} +class NavtexDemodGUI; + +class NavtexDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static NavtexDemodGUI* 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::NavtexDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + NavtexDemodSettings m_settings; + qint64 m_deviceCenterFrequency; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + + NavtexDemod* m_navtexDemod; + int m_basebandSampleRate; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QMenu *menu; // Column select context menu + + enum MessageCol { + MESSAGE_COL_DATE, + MESSAGE_COL_TIME, + MESSAGE_COL_STATION_ID, + MESSAGE_COL_STATION, + MESSAGE_COL_TYPE_ID, + MESSAGE_COL_TYPE, + MESSAGE_COL_MESSAGE_ID, + MESSAGE_COL_MESSAGE, + MESSAGE_COL_ERRORS, + MESSAGE_COL_ERROR_PERCENT, + MESSAGE_COL_RSSI + }; + + explicit NavtexDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~NavtexDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void messageReceived(const NavtexMessage& message, int errors, float rssi); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + void updateTxStation(); + qint64 getFrequency(); + + void leaveEvent(QEvent*); + void enterEvent(EnterEventType*); + + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked); + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_navArea_currentIndexChanged(int index); + void on_findOnMapFeature_clicked(); + void on_filterStation_currentIndexChanged(int index); + void on_filterType_currentIndexChanged(int index); + void on_clearTable_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_logOpen_clicked(); + void on_channel1_currentIndexChanged(int index); + void on_channel2_currentIndexChanged(int index); + void filterRow(int row); + void filter(); + void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void messages_sectionResized(int logicalIndex, int oldSize, int newSize); + void columnSelectMenu(QPoint pos); + void columnSelectMenuChecked(bool checked = false); + void customContextMenuRequested(QPoint point); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); +}; + +#endif // INCLUDE_NAVTEXDEMODGUI_H + diff --git a/plugins/channelrx/demodnavtex/navtexdemodgui.ui b/plugins/channelrx/demodnavtex/navtexdemodgui.ui new file mode 100644 index 000000000..e9b960b5a --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodgui.ui @@ -0,0 +1,1140 @@ + + + NavtexDemodGUI + + + + 0 + 0 + 411 + 814 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + Packet Demodulator + + + + + 0 + 0 + 390 + 151 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + Navarea + + + + + + + Geographic area in which the receiver is in + + + + 1 - North Atlantic, North Sea, Baltic Sea + + + + + 2 - East Atlantic + + + + + 3 - Mediterranean Sea + + + + + 4 - West Atlantic + + + + + 5- Brasil + + + + + 6 - Argentina, Uruguay + + + + + 7 - South Africa + + + + + 8 - India + + + + + 9 - Arabia + + + + + 10 - Australia + + + + + 11- East Asia + + + + + 12 - Eastern Pacific + + + + + 13 - Russia + + + + + 14 - New Zealand, Southern Pacific + + + + + 15 - Chile + + + + + 16 - Peru + + + + + + + + Qt::Vertical + + + + + + + TX + + + + + + + Transmitting station assigned to current time slot + + + true + + + + + + + Find transmitter on map + + + + + + + :/gridpolar.png:/gridpolar.png + + + + + + + Qt::Vertical + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 250 + + + 600 + + + 1 + + + 250 + + + Qt::Horizontal + + + + + + + + 40 + 0 + + + + 500 Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + Send messages 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 + + + + + + + + + Station + + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + Display messages only from the specified station + + + + All + + + + + + + + Type + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Display messages only with the specified type + + + + All + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 24 + 16777215 + + + + Start/stop logging of received messages to .csv file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Read data from .csv log file + + + ... + + + + :/load.png:/load.png + + + false + + + + + + + Clear messages + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 10 + 170 + 381 + 251 + + + + + 0 + 0 + + + + Received Messages + + + + + + Qt::Vertical + + + + Received text + + + true + + + + + Received messages + + + QAbstractItemView::NoEditTriggers + + + + Date + + + Date message was received + + + + + Time + + + Time message was received + + + + + SID + + + Transmitting station ID + + + + + Station + + + Transmitting station + + + + + TID + + + Message type ID + + + + + Type + + + Message type + + + + + MID + + + Message ID + + + + + Message + + + Message text + + + + + Errors + + + The number of errors detected while receiving this message + + + + + Error % + + + The percentage of errors + + + + + RSSI (dB) + + + Average channel power during reception + + + + + + + + + + + 0 + 440 + 716 + 341 + + + + + 714 + 0 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Real + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + real(exp) + + + + + imag(exp) + + + + + real(corr1) + + + + + imag(corr1) + + + + + real(corr2) + + + + + imag(corr2) + + + + + abs1Filt + + + + + abs2Filt + + + + + env1 + + + + + env2 + + + + + unbiasedData + + + + + biasedData + + + + + data + + + + + clock + + + + + bit + + + + + gotSOP + + + + + + + + + 0 + 0 + + + + Imag + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + real(exp) + + + + + imag(exp) + + + + + real(corr1) + + + + + imag(corr1) + + + + + real(corr2) + + + + + imag(corr2) + + + + + abs1Filt + + + + + abs2Filt + + + + + env1 + + + + + env2 + + + + + unbiasedData + + + + + biasedData + + + + + data + + + + + clock + + + + + bit + + + + + gotSOP + + + + + + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + ButtonSwitch + QToolButton +

gui/buttonswitch.h
+ + + RollupContents + QWidget +
gui/rollupcontents.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 +
+ + + deltaFrequency + navArea + txStation + findOnMapFeature + rfBW + udpEnabled + filterStation + filterType + logEnable + logFilename + logOpen + clearTable + text + messages + channel1 + channel2 + + + + + + diff --git a/plugins/channelrx/demodnavtex/navtexdemodplugin.cpp b/plugins/channelrx/demodnavtex/navtexdemodplugin.cpp new file mode 100644 index 000000000..51d6fff6d --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodplugin.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 "navtexdemodgui.h" +#endif +#include "navtexdemod.h" +#include "navtexdemodwebapiadapter.h" +#include "navtexdemodplugin.h" + +const PluginDescriptor NavtexDemodPlugin::m_pluginDescriptor = { + NavtexDemod::m_channelId, + QStringLiteral("Navtex Demodulator"), + QStringLiteral("7.11.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +NavtexDemodPlugin::NavtexDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& NavtexDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void NavtexDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(NavtexDemod::m_channelIdURI, NavtexDemod::m_channelId, this); +} + +void NavtexDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + NavtexDemod *instance = new NavtexDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* NavtexDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* NavtexDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return NavtexDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* NavtexDemodPlugin::createChannelWebAPIAdapter() const +{ + return new NavtexDemodWebAPIAdapter(); +} + diff --git a/plugins/channelrx/demodnavtex/navtexdemodplugin.h b/plugins/channelrx/demodnavtex/navtexdemodplugin.h new file mode 100644 index 000000000..e81496fdc --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodplugin.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_NAVTEXDEMODPLUGIN_H +#define INCLUDE_NAVTEXDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class NavtexDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.navtexdemod") + +public: + explicit NavtexDemodPlugin(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_NAVTEXDEMODPLUGIN_H + diff --git a/plugins/channelrx/demodnavtex/navtexdemodsettings.cpp b/plugins/channelrx/demodnavtex/navtexdemodsettings.cpp new file mode 100644 index 000000000..2f64e3b4e --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodsettings.cpp @@ -0,0 +1,209 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "navtexdemodsettings.h" + +NavtexDemodSettings::NavtexDemodSettings() : + m_channelMarker(nullptr), + m_scopeGUI(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void NavtexDemodSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 450.0f; // OBW for 2FSK = 2 * deviation + data rate. Then add a bit for carrier frequency offset + m_navArea = 1; + m_filterStation = "All"; + m_filterType = "All"; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_logFilename = "navtex_log.csv"; + m_logEnabled = false; + m_scopeCh1 = 0; + m_scopeCh2 = 1; + + m_rgbColor = QColor(100, 25, 207).rgb(); + m_title = "Navtex Demodulator"; + 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; + + for (int i = 0; i < NAVTEXDEMOD_COLUMNS; i++) + { + m_columnIndexes[i] = i; + m_columnSizes[i] = -1; // Autosize + } +} + +QByteArray NavtexDemodSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_streamIndex); + s.writeS32(3, m_navArea); + s.writeString(4, m_filterStation); + s.writeString(5, m_filterType); + + if (m_channelMarker) { + s.writeBlob(6, m_channelMarker->serialize()); + } + s.writeFloat(7, m_rfBandwidth); + + s.writeBool(9, m_udpEnabled); + s.writeString(10, m_udpAddress); + s.writeU32(11, m_udpPort); + s.writeString(12, m_logFilename); + s.writeBool(13, m_logEnabled); + s.writeS32(14, m_scopeCh1); + s.writeS32(15, m_scopeCh2); + + s.writeU32(20, m_rgbColor); + s.writeString(21, m_title); + s.writeBool(22, m_useReverseAPI); + s.writeString(23, m_reverseAPIAddress); + s.writeU32(24, m_reverseAPIPort); + s.writeU32(25, m_reverseAPIDeviceIndex); + s.writeU32(26, m_reverseAPIChannelIndex); + + if (m_rollupState) { + s.writeBlob(27, m_rollupState->serialize()); + } + + s.writeS32(28, m_workspaceIndex); + s.writeBlob(29, m_geometryBytes); + s.writeBool(30, m_hidden); + s.writeBlob(31, m_scopeGUI->serialize()); + + for (int i = 0; i < NAVTEXDEMOD_COLUMNS; i++) { + s.writeS32(100 + i, m_columnIndexes[i]); + } + + for (int i = 0; i < NAVTEXDEMOD_COLUMNS; i++) { + s.writeS32(200 + i, m_columnSizes[i]); + } + + return s.final(); +} + +bool NavtexDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_streamIndex, 0); + d.readS32(3, &m_navArea, 1); + d.readString(4, &m_filterStation, "All"); + d.readString(5, &m_filterType, "All"); + + if (m_channelMarker) + { + d.readBlob(6, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + d.readFloat(7, &m_rfBandwidth, 450.0f); + + d.readBool(9, &m_udpEnabled); + d.readString(10, &m_udpAddress); + d.readU32(11, &utmp); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + d.readString(12, &m_logFilename, "navtex_log.csv"); + d.readBool(13, &m_logEnabled, false); + d.readS32(14, &m_scopeCh1, 0); + d.readS32(15, &m_scopeCh2, 0); + + d.readU32(20, &m_rgbColor, QColor(100, 25, 207).rgb()); + d.readString(21, &m_title, "Navtex Demodulator"); + d.readBool(22, &m_useReverseAPI, false); + d.readString(23, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(24, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(25, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(26, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(27, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(28, &m_workspaceIndex, 0); + d.readBlob(29, &m_geometryBytes); + d.readBool(30, &m_hidden, false); + + if (m_scopeGUI) + { + d.readBlob(31, &bytetmp); + m_scopeGUI->deserialize(bytetmp); + } + + for (int i = 0; i < NAVTEXDEMOD_COLUMNS; i++) { + d.readS32(100 + i, &m_columnIndexes[i], i); + } + + for (int i = 0; i < NAVTEXDEMOD_COLUMNS; i++) { + d.readS32(200 + i, &m_columnSizes[i], -1); + } + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + diff --git a/plugins/channelrx/demodnavtex/navtexdemodsettings.h b/plugins/channelrx/demodnavtex/navtexdemodsettings.h new file mode 100644 index 000000000..1ea163870 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodsettings.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_NAVTEXDEMODSETTINGS_H +#define INCLUDE_NAVTEXDEMODSETTINGS_H + +#include + +class Serializable; + +// Number of columns in the table +#define NAVTEXDEMOD_COLUMNS 11 + +struct NavtexDemodSettings +{ + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; + int m_navArea; + QString m_filterStation; + QString m_filterType; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + int m_scopeCh1; + int m_scopeCh2; + + QString m_logFilename; + bool m_logEnabled; + Serializable *m_scopeGUI; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + int m_columnIndexes[NAVTEXDEMOD_COLUMNS];//!< How the columns are ordered in the table + int m_columnSizes[NAVTEXDEMOD_COLUMNS]; //!< Size of the columns in the table + + static const int NAVTEXDEMOD_CHANNEL_SAMPLE_RATE = 1000; // Must be integer multiple of baud rate (x10) + static const int NAVTEXDEMOD_BAUD_RATE = 100; + static const int NAVTEXDEMOD_SAMPLES_PER_BIT = NAVTEXDEMOD_CHANNEL_SAMPLE_RATE / NAVTEXDEMOD_BAUD_RATE; + static const int NAVTEXDEMOD_FREQUENCY_SHIFT = 170; + + NavtexDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_NAVTEXDEMODSETTINGS_H */ + diff --git a/plugins/channelrx/demodnavtex/navtexdemodsink.cpp b/plugins/channelrx/demodnavtex/navtexdemodsink.cpp new file mode 100644 index 000000000..0ed543ed2 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodsink.cpp @@ -0,0 +1,493 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 + +#include "dsp/dspengine.h" +#include "dsp/scopevis.h" +#include "util/db.h" +#include "maincore.h" + +#include "navtexdemod.h" +#include "navtexdemodsink.h" + +NavtexDemodSink::NavtexDemodSink(NavtexDemod *packetDemod) : + m_navtexDemod(packetDemod), + m_channelSampleRate(NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_exp(nullptr), + m_sampleBufferIndex(0) +{ + m_magsq = 0.0; + + m_sampleBuffer.resize(m_sampleBufferSize); + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); + + m_lowpassComplex1.create(301, NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE, NavtexDemodSettings::NAVTEXDEMOD_BAUD_RATE * 1.1); + m_lowpassComplex2.create(301, NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE, NavtexDemodSettings::NAVTEXDEMOD_BAUD_RATE * 1.1); +} + +NavtexDemodSink::~NavtexDemodSink() +{ + delete[] m_exp; +} + +void NavtexDemodSink::sampleToScope(Complex sample) +{ + if (m_scopeSink) + { + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); + + if (m_sampleBufferIndex == m_sampleBufferSize) + { + std::vector vbegin; + vbegin.push_back(m_sampleBuffer.begin()); + m_scopeSink->feed(vbegin, m_sampleBufferSize); + m_sampleBufferIndex = 0; + } + } +} + +void NavtexDemodSink::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 NavtexDemodSink::eraseChars(int n) +{ + if (getMessageQueueToChannel()) + { + QString msg = QString("%1").arg(QChar(0x8)); // Backspace + for (int i = 0; i < n; i++) + { + NavtexDemod::MsgCharacter *msg = NavtexDemod::MsgCharacter::create(QChar(0x8)); + getMessageQueueToChannel()->push(msg); + } + } +} + +void NavtexDemodSink::processOneSample(Complex &ci) +{ + // 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++; + + // Sum power while data is being received + if (m_gotSOP) + { + m_rssiMagSqSum += magsq; + m_rssiMagSqCount++; + } + + ci /= SDR_RX_SCALEF; + + // Correlate with expected frequencies + Complex exp = m_exp[m_expIdx]; + m_expIdx = (m_expIdx + 1) % m_expLength; + Complex corr1 = ci * exp; + Complex corr2 = ci * std::conj(exp); + + // Low pass filter + Real abs1Filt = std::abs(m_lowpassComplex1.filter(corr1)); + Real abs2Filt = std::abs(m_lowpassComplex2.filter(corr2)); + + // Envelope calculation + m_movMax1(abs1Filt); + m_movMax2(abs2Filt); + Real env1 = m_movMax1.getMaximum(); + Real env2 = m_movMax2.getMaximum(); + + // Automatic threshold correction to compensate for frequency selective fading + // http://www.w7ay.net/site/Technical/ATC/index.html + Real bias1 = abs1Filt - 0.5 * env1; + Real bias2 = abs2Filt - 0.5 * env2; + Real unbiasedData = abs1Filt - abs2Filt; + Real biasedData = bias1 - bias2; + + // Save current data for edge detection + m_dataPrev = m_data; + // Set data according to stongest correlation + m_data = biasedData < 0; + + // Generate sampling clock by aligning to correlator zero-crossing + if (m_data && !m_dataPrev) + { + if ((m_clockCount > 2) && (m_clockCount < m_samplesPerBit*3/4) && m_gotSOP) + { + //qDebug() << "Clock toggle ignored at " << m_clockCount; + } + else + { + m_clockCount = 0; + m_clock = false; + } + } + else + { + // Sample in middle of symbol + if (m_clockCount == m_samplesPerBit/2) + { + receiveBit(m_data); + m_clock = true; + } + m_clockCount = (m_clockCount + 1) % m_samplesPerBit; + if (m_clockCount == 0) { + m_clock = false; + } + } + + // 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(real(exp)); + break; + case 3: + scopeSample.real(imag(exp)); + break; + case 4: + scopeSample.real(real(corr1)); + break; + case 5: + scopeSample.real(imag(corr1)); + break; + case 6: + scopeSample.real(real(corr2)); + break; + case 7: + scopeSample.real(imag(corr2)); + break; + case 8: + scopeSample.real(abs1Filt); + break; + case 9: + scopeSample.real(abs2Filt); + break; + case 10: + scopeSample.real(env1); + break; + case 11: + scopeSample.real(env2); + break; + case 12: + scopeSample.real(unbiasedData); + break; + case 13: + scopeSample.real(biasedData); + break; + case 14: + scopeSample.real(m_data); + break; + case 15: + scopeSample.real(m_clock); + break; + case 16: + scopeSample.real(m_bit); + break; + case 17: + scopeSample.real(m_gotSOP); + break; + } + switch (m_settings.m_scopeCh2) + { + case 0: + scopeSample.imag(ci.real()); + break; + case 1: + scopeSample.imag(ci.imag()); + break; + case 2: + scopeSample.imag(real(exp)); + break; + case 3: + scopeSample.imag(imag(exp)); + break; + case 4: + scopeSample.imag(real(corr1)); + break; + case 5: + scopeSample.imag(imag(corr1)); + break; + case 6: + scopeSample.imag(real(corr2)); + break; + case 7: + scopeSample.imag(imag(corr2)); + break; + case 8: + scopeSample.imag(abs1Filt); + break; + case 9: + scopeSample.imag(abs2Filt); + break; + case 10: + scopeSample.imag(env1); + break; + case 11: + scopeSample.imag(env2); + break; + case 12: + scopeSample.imag(unbiasedData); + break; + case 13: + scopeSample.imag(biasedData); + break; + case 14: + scopeSample.imag(m_data); + break; + case 15: + scopeSample.imag(m_clock); + break; + case 16: + scopeSample.imag(m_bit); + break; + case 17: + scopeSample.imag(m_gotSOP); + break; + } + sampleToScope(scopeSample); +} + +void NavtexDemodSink::receiveBit(bool bit) +{ + m_bit = bit; + + // Store in shift reg + m_bits = (m_bits << 1) | m_bit; + m_bitCount++; + + if (!m_gotSOP) + { + if (m_bitCount == 14) + { + if ((m_bits & 0x3fff) == 0x19f8) // phase 2 followed by phase 1 + { + m_gotSOP = true; + m_bitCount = 0; + m_sitorBDecoder.init(); + } + else + { + m_bitCount--; + } + } + } + else + { + if (m_bitCount == 7) + { + char c = m_sitorBDecoder.decode(m_bits & 0x7f); + if (c != -1) + { + //qDebug() << "Out: " << SitorBDecoder::printable(c); + m_consecutiveErrors = 0; + + if ((c != '<') && (c != '>') && (c != 0x2)) + { + // 7 bytes per second, so may as well send individually to be displayed + if (getMessageQueueToChannel()) + { + NavtexDemod::MsgCharacter *msg = NavtexDemod::MsgCharacter::create(SitorBDecoder::printable(c)); + getMessageQueueToChannel()->push(msg); + } + // Add character to message buffer + m_messageBuffer.append(c); + } + else + { + if (m_messageBuffer.size() > 0) + { + QRegularExpression re("[Z*][C*][Z*][C*](.|\n|\r)*[N*][N*][N*][N*]"); + QRegularExpressionMatch match = re.match(m_messageBuffer); + if (match.hasMatch()) + { + if (getMessageQueueToChannel()) + { + NavtexMessage navtexMsg = NavtexMessage(match.captured(0)); + + float rssi = CalcDb::dbPower(m_rssiMagSqSum / m_rssiMagSqCount); + NavtexDemod::MsgMessage *msg = NavtexDemod::MsgMessage::create(navtexMsg, m_sitorBDecoder.getErrors(), rssi); + getMessageQueueToChannel()->push(msg); + } + // Navtex messages can span multiple blocks? + m_messageBuffer = ""; + } + } + if (c == 0x2) // End of text + { + // Reset demod + init(); + } + } + + } + if (c == '*') + { + m_errorCount++; + m_consecutiveErrors++; + // ITU 476-5 just says return to standby after the percentage of + // mutilated signals received has reached a predetermined value + // without saying what that value is + if (m_messageBuffer.size() >= 12) + { + float errorPC = m_errorCount / (float)(m_messageBuffer.size() + m_errorCount); + if (errorPC >= 0.2f) + { + //qDebug() << "Too many errors" << m_errorCount << m_messageBuffer.size(); + init(); + } + } + else if (m_errorCount >= 3) + { + //qDebug() << "Too many errors" << m_errorCount << m_messageBuffer.size(); + eraseChars(m_messageBuffer.size()); + init(); + } + if (m_consecutiveErrors >= 5) + { + //qDebug() << "Too many consequtive errors"; + init(); + } + } + m_bitCount = 0; + } + } +} + +void NavtexDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "NavtexDemodSink::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) NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void NavtexDemodSink::init() +{ + m_expIdx = 0; + m_bit = 0; + m_bits = 0; + m_bitCount = 0; + m_gotSOP = false; + m_errorCount = 0; + m_clockCount = 0; + m_clock = 0; + m_rssiMagSqSum = 0.0; + m_rssiMagSqCount = 0; + m_consecutiveErrors = 0; + m_sitorBDecoder.init(); + m_messageBuffer = ""; +} + +void NavtexDemodSink::applySettings(const NavtexDemodSettings& settings, bool force) +{ + qDebug() << "NavtexDemodSink::applySettings:" + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " force: " << force; + + 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) NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + if (force) + { + delete[] m_exp; + m_exp = new Complex[m_expLength]; + Real f0 = 0.0f; + for (int i = 0; i < m_expLength; i++) + { + m_exp[i] = Complex(cos(f0), sin(f0)); + f0 += 2.0f * (Real)M_PI * (NavtexDemodSettings::NAVTEXDEMOD_FREQUENCY_SHIFT/2.0f) / NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE; + } + init(); + // Due to start and stop bits, we should get mark and space at least every 8 bits + // while something is being transmitted + m_movMax1.setSize(m_samplesPerBit * 8); + m_movMax2.setSize(m_samplesPerBit * 8); + } + + m_settings = settings; +} + diff --git a/plugins/channelrx/demodnavtex/navtexdemodsink.h b/plugins/channelrx/demodnavtex/navtexdemodsink.h new file mode 100644 index 000000000..56b596dad --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodsink.h @@ -0,0 +1,145 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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_NAVTEXDEMODSINK_H +#define INCLUDE_NAVTEXDEMODSINK_H + +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" +#include "util/movingmaximum.h" +#include "util/messagequeue.h" +#include "util/navtex.h" + +#include "navtexdemodsettings.h" + +class ChannelAPI; +class NavtexDemod; +class ScopeVis; + +class NavtexDemodSink : public ChannelSampleSink { +public: + NavtexDemodSink(NavtexDemod *packetDemod); + ~NavtexDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; } + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const NavtexDemodSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setChannel(ChannelAPI *channel) { m_channel = channel; } + + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.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; + }; + + ScopeVis* m_scopeSink; // Scope GUI to display baseband waveform + NavtexDemod *m_navtexDemod; + NavtexDemodSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + + 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; + + Lowpass m_lowpassComplex1; + Lowpass m_lowpassComplex2; + MovingMaximum m_movMax1; + MovingMaximum m_movMax2; + + static const int m_expLength = 600; + static const int m_samplesPerBit = NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE / NavtexDemodSettings::NAVTEXDEMOD_BAUD_RATE; + Complex *m_exp; + int m_expIdx; + int m_bit; + bool m_data; + bool m_dataPrev; + int m_clockCount; + bool m_clock; + double m_rssiMagSqSum; + int m_rssiMagSqCount; + + unsigned short m_bits; + int m_bitCount; + bool m_gotSOP; + int m_errorCount; + int m_consecutiveErrors; + QString m_messageBuffer; + + SitorBDecoder m_sitorBDecoder; + + SampleVector m_sampleBuffer; + static const int m_sampleBufferSize = NavtexDemodSettings::NAVTEXDEMOD_CHANNEL_SAMPLE_RATE / 20; + int m_sampleBufferIndex; + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void sampleToScope(Complex sample); + void eraseChars(int n); + void init(); + void receiveBit(bool bit); +}; + +#endif // INCLUDE_NAVTEXDEMODSINK_H + diff --git a/plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.cpp b/plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.cpp new file mode 100644 index 000000000..3195274ef --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.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 "navtexdemod.h" +#include "navtexdemodwebapiadapter.h" + +NavtexDemodWebAPIAdapter::NavtexDemodWebAPIAdapter() +{} + +NavtexDemodWebAPIAdapter::~NavtexDemodWebAPIAdapter() +{} + +int NavtexDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setNavtexDemodSettings(new SWGSDRangel::SWGNavtexDemodSettings()); + response.getNavtexDemodSettings()->init(); + NavtexDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int NavtexDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + NavtexDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.h b/plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.h new file mode 100644 index 000000000..5d481ff97 --- /dev/null +++ b/plugins/channelrx/demodnavtex/navtexdemodwebapiadapter.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_NAVTEXDEMOD_WEBAPIADAPTER_H +#define INCLUDE_NAVTEXDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "navtexdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class NavtexDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + NavtexDemodWebAPIAdapter(); + virtual ~NavtexDemodWebAPIAdapter(); + + 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: + NavtexDemodSettings m_settings; +}; + +#endif // INCLUDE_NAVTEXDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodnavtex/readme.md b/plugins/channelrx/demodnavtex/readme.md new file mode 100644 index 000000000..7f80d0e5c --- /dev/null +++ b/plugins/channelrx/demodnavtex/readme.md @@ -0,0 +1,101 @@ +

Navtex demodulator plugin

+ +

Introduction

+ +This plugin can be used to demodulate Navtex (Navigational Telex) transmissions, which contain marine navigational and meteorological warnings and forecasts. +These are broadcast worldwide on 518kHz (in English for international messages), 490kHz (often in the local language for national messages) and 4209.5kHz. +Navtex messages are transmitted using FSK with 170Hz separation at 100 baud, using [SITOR-B](https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.625-4-201203-I!!PDF-E.pdf]) encoding. +The [Map](../../feature/map/readme.md) feature can display the location of Navtex transmitters, along with their transmission times and frequencies. + +

Interface

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

+ +Specifies the geographical area in which the receiver is in. This enables the plugin to decode transmitter station identifiers, and display which transmitter the current transmission timeslot is assigned to. +Note that with good propagation conditions, it is possible to receive messages from another area, so the station indicated in the message table (17) should be checked against the location given in the recevied message text. + +

5: TX

+ +Displays which transmitter is assigned the current 10 minute timeslot. + +

6: Find TX On Map

+ +If the [Map](../../feature/map/readme.md) feature is open, when clicked, the Map will be centered on the current transmitter (5). + +

7: RF Bandwidth

+ +This specifies the bandwidth of a filter that is applied to the input signal to limit the RF bandwidth. + +

8: UDP

+ +When checked, received packets are forwarded to the specified UDP address (9) and port (10). + +

9: UDP address

+ +IP address of the host to forward received packets to via UDP. + +

10: UDP port

+ +UDP port number to forward received packets to. + +

11: Station Filter

+ +This drop down displays a list of all stations which messages have been received from. When a station other than "All" is selected, only messages from that station will be displayed in the table. + +

12: Message Type Filter

+ +This drop down displays a list of all message types that have been received. When a type other than "All" is selected, only messages with that type will be displayed in the table. + +

13: Start/stop Logging Messages to .csv File

+ +When checked, writes all received messages to a .csv file. + +

14: .csv Log Filename

+ +Click to specify the name of the .csv file which received messasges are logged to. + +

15: Read Data from .csv File

+ +Click to specify a previously written .csv log file, which is read and used to update the table. + +

16: Received Text

+ +The received text area shows text as it is received. + +

17: Received Messages Table

+ +The received messages table displays the contents of the messages that have been received. + +* Date - Date the message was received. +* Time - Time the message was received. +* SID - Station identifer of the transmitting station. +* Station - SID decoded according to the currently selected navarea. +* TID - Message type identifier. +* MID - Message identifier. +* Message - The message text. +* Errors - The number of characters that were received with detected errors. +* Error % - The percentage of characters that were received with errors. +* RSSI - Average channel power in dB, while receiving the message. + +Right clicking on the header will open a menu allowing you to select which columns are visible, or locate the station in the selected row on the [Map](../../feature/map/readme.md). +To reorder the columns, left click and drag left or right a column header. +Left click on a header to sort the table by the data in that column. + diff --git a/sdrbase/util/navtex.cpp b/sdrbase/util/navtex.cpp new file mode 100644 index 000000000..3d605a9e2 --- /dev/null +++ b/sdrbase/util/navtex.cpp @@ -0,0 +1,729 @@ +/////////////////////////////////////////////////////////////////////////////////// +// 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 "navtex.h" + +// From https://en.wikipedia.org/wiki/List_of_Navtex_stations +const QList NavtexTransmitter::m_navtexTransmitters = { + {1, "Svalbard", 78.056944, 13.609722, {Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {1, "Bodo", 67.266667, 14.383333, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {1, "Vardo", 70.370889, 31.097389, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, + {1, "Torshavn", 62.014944, -6.800056, {NavtexTransmitter::Schedule('D', 518000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(12, 30), QTime(16, 30), QTime(20, 30)})}}, + {1, "Niton", 50.586297, -1.254756, {NavtexTransmitter::Schedule('E', 518000, {QTime(0, 40), QTime(4, 40), QTime(8, 40), QTime(12, 40), QTime(16, 40), QTime(20, 40)}), + NavtexTransmitter::Schedule('K', 518000, {QTime(1, 40), QTime(5, 40), QTime(6, 40), QTime(13, 40), QTime(17, 40), QTime(21, 40)}), + NavtexTransmitter::Schedule('I', 490000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)})}}, // Have seen this broadcast at 9:10 + {1, "Talinn", 59.4644, 24.357294, {NavtexTransmitter::Schedule('F', 518000, {QTime(3, 20), QTime(7, 20), QTime(11, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)})}}, + {1, "Cullercoats", 55.0732, -1.463233, {NavtexTransmitter::Schedule('G', 518000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(17, 0), QTime(21, 0)}), + NavtexTransmitter::Schedule('U', 490000, {QTime(3, 20), QTime(7, 20), QTime(11, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)})}}, + {1, "Bjuroklubb", 64.461639, 21.591833, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)})}}, + {1, "Grimeton", 57.103056, 12.385556, {NavtexTransmitter::Schedule('I', 518000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)})}}, + {1, "Gislovshammer", 55.488917, 14.314222, {NavtexTransmitter::Schedule('J', 518000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(17, 30), QTime(21, 30)})}}, + {1, "Rogaland", 58.658817, 5.603778, {NavtexTransmitter::Schedule('L', 518000, {QTime(1, 50), QTime(5, 50), QTime(9, 50), QTime(13, 50), QTime(17, 50), QTime(21, 50)})}}, + {1, "Jeloy", 59.435833, 10.589444, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {1, "Orlandet", 63.661194, 9.5455, {NavtexTransmitter::Schedule('N', 518000, {QTime(2, 10), QTime(6, 10), QTime(10, 10), QTime(14, 10), QTime(18, 10), QTime(22, 10)})}}, + {1, "Portpatrick", 54.844044, -5.124478, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)}), + NavtexTransmitter::Schedule('C', 490000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(16, 20), QTime(20, 20)})}}, + {1, "Netherlands Coastguard", 52.095128, 4.257975, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {1, "Malin Head", 55.363278, -7.33925, {NavtexTransmitter::Schedule('Q', 518000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(18, 40), QTime(22, 40)})}}, + {1, "Saudanes", 66.18625, -18.951867, {NavtexTransmitter::Schedule('R', 518000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(18, 50), QTime(22, 50)}), + NavtexTransmitter::Schedule('E', 490000, {QTime(0, 40), QTime(4, 40), QTime(8, 40), QTime(16, 40), QTime(20, 40)})}}, + {1, "Hamburg", 53.673333, 9.808611, {NavtexTransmitter::Schedule('S', 518000, {QTime(3, 0), QTime(7, 0), QTime(12, 0), QTime(15, 0), QTime(19, 0), QTime(23, 0)}), + NavtexTransmitter::Schedule('L', 490000, {QTime(1, 50), QTime(5, 50), QTime(9, 50), QTime(17, 50), QTime(21, 50)})}}, // Transmitter is at Pinneberg (used on wiki), but messages give location as Hamburg + {1, "Oostende", 51.182278, 2.806539, {NavtexTransmitter::Schedule('T', 518000, {QTime(3, 10), QTime(7, 10), QTime(11, 10), QTime(15, 10), QTime(19, 10), QTime(23, 10)}), + NavtexTransmitter::Schedule('V', 518000, {QTime(3, 30), QTime(7, 30), QTime(11, 30), QTime(15, 30), QTime(19, 30), QTime(23, 30)}), + NavtexTransmitter::Schedule('B', 490000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(16, 10), QTime(20, 10)})}}, + {1, "Valentia", 51.929756, -10.349028, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)})}}, + {1, "Grindavik", 63.833208, -22.450786, {NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 50), QTime(12, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)}), + NavtexTransmitter::Schedule('K', 490000, {QTime(1, 40), QTime(5, 40), QTime(9, 40), QTime(17, 40), QTime(21, 40)})}}, + + {2, "Cross Corsen", 48.476031, -5.053697, {NavtexTransmitter::Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)}), + NavtexTransmitter::Schedule('E', 490000, {QTime(0, 40), QTime(4, 40), QTime(8, 40), QTime(12, 40), QTime(16, 40), QTime(20, 40)})}}, + {2, "Coruna", 43.367028, -8.451861, {NavtexTransmitter::Schedule('D', 518000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(12, 30), QTime(16, 30), QTime(20, 30)}), + NavtexTransmitter::Schedule('W', 490000, {QTime(3, 40), QTime(7, 40), QTime(11, 40), QTime(15, 40), QTime(17, 40), QTime(23, 40)})}}, + {2, "Horta", 38.529872, -28.628922, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 50), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)}), + NavtexTransmitter::Schedule('J', 490000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(15, 30), QTime(21, 30)})}}, + {2, "Tarifa", 36.042, -5.556606, {NavtexTransmitter::Schedule('G', 518000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(17, 0), QTime(21, 0)})}}, + {2, "Las Palmas", 27.758522, -15.605361, {NavtexTransmitter::Schedule('I', 518000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)}), + NavtexTransmitter::Schedule('A', 490000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {2, "Casablanca", 33.6, -7.633333, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {2, "Porto Santo", 33.066278, -16.355417, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {2, "Monsanto", 38.731611, -9.190611, {NavtexTransmitter::Schedule('R', 518000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(18, 50), QTime(22, 50)}), + NavtexTransmitter::Schedule('G', 490000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(15, 0), QTime(21, 0)})}}, + {2, "Ribeira de Vinha", 16.853228, -25.003197, {NavtexTransmitter::Schedule('U', 518000, {QTime(3, 20), QTime(7, 20), QTime(11, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)})}}, + {2, "Cabo La Nao", 38.723258, 0.161367, {NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 520), QTime(11, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)}), + NavtexTransmitter::Schedule('M', 490000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(16, 0), QTime(22, 0)})}}, + {2, "Sao Vicente", 16.853228, -25.003197, {NavtexTransmitter::Schedule('P', 490000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(16, 30), QTime(22, 30)})}}, +// {2, "Niton", 50.586297, -1.254756, {NavtexTransmitter::Schedule('T', 490000, {QTime(3, 10), QTime(7, 10), QTime(11, 10), QTime(15, 10), QTime(17, 10), QTime(23, 10)})}}, + {2, "Tarifa", 36.042, -5.556606, {NavtexTransmitter::Schedule('T', 490000, {QTime(3, 10), QTime(7, 10), QTime(11, 10), QTime(15, 10), QTime(17, 10), QTime(23, 10)})}}, + + {3, "Novorossijsk", 44.599111, 37.951442, {NavtexTransmitter::Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {3, "Algier", 36.733333, 3.18, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {3, "Odessa", 46.377611, 30.748222, {NavtexTransmitter::Schedule('C', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {3, "Istanbul", 41.066667, 28.95, {NavtexTransmitter::Schedule('D', 518000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(12, 30), QTime(16, 30), QTime(20, 30)}), + NavtexTransmitter::Schedule('B', 490000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(16, 10), QTime(20, 10)}), + NavtexTransmitter::Schedule('M', 4209500, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(16, 0), QTime(22, 0)})}}, + {3, "Samsun", 41.386667, 36.188333, {NavtexTransmitter::Schedule('E', 518000, {QTime(0, 40), QTime(4, 40), QTime(8, 40), QTime(12, 40), QTime(16, 40), QTime(20, 40)}), + NavtexTransmitter::Schedule('A', 490000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {3, "Antalya", 36.1525, 32.44, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 50), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)}), + NavtexTransmitter::Schedule('D', 490000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(16, 30), QTime(20, 30)})}}, + {3, "Iraklio", 35.322861, 25.748986, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)}), + NavtexTransmitter::Schedule('Q', 490000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(16, 40), QTime(22, 40)}), + NavtexTransmitter::Schedule('S', 4209500, {QTime(3, 0), QTime(7, 0), QTime(11, 0), QTime(15, 0), QTime(19, 0), QTime(3, 20)})}}, + {3, "Izmir", 38.275833, 26.2675, {NavtexTransmitter::Schedule('I', 518000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)}), + NavtexTransmitter::Schedule('C', 490000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(16, 20), QTime(20, 20)})}}, + {3, "Varna", 43.068056, 27.786111, {NavtexTransmitter::Schedule('J', 518000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(17, 30), QTime(21, 30)})}}, + {3, "Kerkyra", 39.607222, 19.890833, {NavtexTransmitter::Schedule('K', 518000, {QTime(1, 40), QTime(5, 40), QTime(9, 40), QTime(13, 40), QTime(17, 40), QTime(21, 40)}), + NavtexTransmitter::Schedule('P', 490000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(16, 30), QTime(22, 30)})}}, + {3, "Limnos", 39.906389, 25.181389, {NavtexTransmitter::Schedule('L', 518000, {QTime(1, 50), QTime(5, 50), QTime(9, 50), QTime(13, 50), QTime(17, 50), QTime(21, 50)}), + NavtexTransmitter::Schedule('R', 490000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(16, 50), QTime(22, 50)})}}, + {3, "Cyprus", 35.048278, 33.283628, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {3, "Alexandria", 31.198089, 29.864494, {NavtexTransmitter::Schedule('N', 518000, {QTime(2, 10), QTime(6, 10), QTime(10, 10), QTime(14, 10), QTime(18, 10), QTime(22, 10)})}}, + {3, "Malta", 35.815211, 14.526911, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)})}}, + {3, "Haifa", 32.827806, 34.969306, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {3, "Split", 43.181861, 16.422333, {NavtexTransmitter::Schedule('Q', 518000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(18, 40), QTime(22, 40)})}}, + {3, "La Maddalena", 41.222778, 9.398889, {NavtexTransmitter::Schedule('R', 518000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(18, 50), QTime(22, 50)}), + NavtexTransmitter::Schedule('I', 490000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)})}}, + {3, "Kelibia", 36.801819, 11.037372, {NavtexTransmitter::Schedule('T', 518000, {QTime(3, 10), QTime(7, 10), QTime(12, 10), QTime(15, 10), QTime(19, 10), QTime(23, 10)})}}, + {3, "Mondolfo", 43.747778, 13.141667, {NavtexTransmitter::Schedule('U', 518000, {QTime(3, 20), QTime(7, 20), QTime(12, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)}), + NavtexTransmitter::Schedule('E', 490000, {QTime(0, 40), QTime(4, 40), QTime(8, 40), QTime(12, 40), QTime(16, 40), QTime(20, 40)})}}, + {3, "Sellia Marina", 38.873056, 16.719722, {NavtexTransmitter::Schedule('V', 518000, {QTime(3, 30), QTime(7, 30), QTime(12, 30), QTime(15, 30), QTime(19, 30), QTime(23, 30)}), + NavtexTransmitter::Schedule('W', 490000, {QTime(3, 40), QTime(7, 40), QTime(11, 40), QTime(15, 40), QTime(17, 40), QTime(23, 40)})}}, + {3, "Cross La Garde", 43.104306, 5.991389, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)}), + NavtexTransmitter::Schedule('S', 490000, {QTime(3, 0), QTime(7, 0), QTime(11, 0), QTime(15, 0), QTime(19, 0), QTime(3, 20)})}}, + {3, "Cabo de la Nao", 38.723258, 0.161367, {NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 50), QTime(12, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)}), + NavtexTransmitter::Schedule('M', 490000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(16, 0), QTime(22, 0)})}}, + + {4, "Miami", 25.626225, -80.383411, {NavtexTransmitter::Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {4, "Bermuda Harbour", 32.380389, -64.682778, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {4, "Riviere-au-Renard", 50.195, -66.109889, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)}), + NavtexTransmitter::Schedule('D', 490000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(16, 30), QTime(20, 30)})}}, + {4, "Boston", 41.709833, -70.498353, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 20), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)})}}, + {4, "New Orleans", 29.884625, -89.945611, {NavtexTransmitter::Schedule('G', 518000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(17, 0), QTime(21, 0)}), + NavtexTransmitter::Schedule('G', 4209500, {QTime(3, 0), QTime(7, 0), QTime(11, 0), QTime(15, 0), QTime(19, 0), QTime(23, 0)})}}, + {4, "Wiarton", 44.937111, -81.233467, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)})}}, + {4, "Curacao", 12.173197, -68.864919, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)})}}, // Duplicate Id + {4, "Portsmouth", 36.726342, -76.007894, {NavtexTransmitter::Schedule('N', 518000, {QTime(2, 10), QTime(6, 10), QTime(10, 10), QTime(14, 10), QTime(18, 10), QTime(22, 10)})}}, + {4, "St. John's", 47.611111, -52.666944, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)})}}, + {4, "Thunder Bay", 48.563514, -88.656311, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {4, "Sydney", 46.185556, -59.893611, {NavtexTransmitter::Schedule('Q', 518000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(18, 40), QTime(22, 40)}), + NavtexTransmitter::Schedule('J', 490000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(15, 30), QTime(21, 30)})}}, + + {4, "Isabela", 18.466683, -67.071819, {NavtexTransmitter::Schedule('R', 518000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(18, 50), QTime(22, 50)})}}, + {4, "Iqaluit", 63.731389, -68.543167, {NavtexTransmitter::Schedule('T', 518000, {QTime(3, 10), QTime(7, 10), QTime(11, 10), QTime(15, 10), QTime(19, 10), QTime(23, 10)}), + NavtexTransmitter::Schedule('S', 490000, {QTime(3, 0), QTime(7, 0), QTime(11, 0), QTime(15, 0), QTime(19, 0), QTime(3, 20)})}}, + {4, "Saint John", 43.744256, -66.121786, {NavtexTransmitter::Schedule('U', 518000, {QTime(3, 20), QTime(7, 20), QTime(11, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)}), + NavtexTransmitter::Schedule('V', 490000, {QTime(3, 30), QTime(7, 30), QTime(11, 30), QTime(15, 30), QTime(19, 30), QTime(23, 30)})}}, + {4, "Kook Island", 64.067017, -52.012611, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)})}}, + {4, "Labrador", 53.708611, -57.021667, {NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 50), QTime(12, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)})}}, + + {6, "La Paloma", -34.666667, -54.15, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 50), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)})}}, + {6, "Ushuaia", -54.8, -68.3, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {6, "Rio Gallegos", -51.616667, -69.216667, {NavtexTransmitter::Schedule('N', 518000, {QTime(2, 10), QTime(6, 10), QTime(10, 10), QTime(14, 10), QTime(18, 10), QTime(22, 10)})}}, + {6, "Comodoro Rivadavia", -45.85, -67.416667, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)})}}, + {6, "Bahía Blanca", -38.716667, -62.1, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {6, "Mar del Plata", -38.05, -57.533333, {NavtexTransmitter::Schedule('Q', 518000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(18, 40), QTime(22, 40)})}}, + {6, "Buenos Aires", -34.6, -58.366667, {NavtexTransmitter::Schedule('R', 518000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(18, 50), QTime(22, 50)})}}, + + {7, "Walvis Bay", -23.05665, 14.624333, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {7, "Cape Town", -33.685128, 18.712961, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, + {7, "Port Elizabeth", -34.036722, 25.555833, {NavtexTransmitter::Schedule('I', 518000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)})}}, + {7, "Durban", -29.804833, 30.815633, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)})}}, + + {8, "Mauritius", -20.167089, 57.478161, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, + {8, "Bombay", 19.083239, 72.834033, {NavtexTransmitter::Schedule('G', 518000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(17, 0), QTime(21, 0)})}}, + {8, "Madras", 13.082778, 80.287222, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + + {9, "Bushehr", 28.962225, 50.822794, {NavtexTransmitter::Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {9, "Hamala", 26.157167, 50.47665, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {9, "Bandar Abbas", 27.161022, 56.225378, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 50), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)})}}, + {9, "Jeddah", 21.342222, 39.155833, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)})}}, + {9, "Muscat", 23.6, 58.5, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {9, "Karachi", 24.851944, 67.0425, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {9, "Quseir", 26.110889, 34.280083, {NavtexTransmitter::Schedule('V', 518000, {QTime(3, 30), QTime(7, 30), QTime(11, 30), QTime(15, 30), QTime(19, 30), QTime(23, 30)})}}, + {9, "Serapeum ", 30.470311, 32.36675, {NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 50), QTime(12, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)})}}, + + {11, "Jayapura", -2.516667, 140.716667, {NavtexTransmitter::Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {11, "Ambon", -3.7, 128.2, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {11, "Singapore", 1.333333, 103.7, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, + {11, "Makassar", -5.1, 119.433333, {NavtexTransmitter::Schedule('D', 518000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(12, 30), QTime(16, 30), QTime(20, 30)})}}, + {11, "Jakarta", -6.116667, 106.866667, {NavtexTransmitter::Schedule('E', 518000, {QTime(0, 40), QTime(4, 40), QTime(8, 40), QTime(12, 40), QTime(16, 40), QTime(20, 40)})}}, + {11, "Bangkok", 13.024444, 100.019733, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 50), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)})}}, + {11, "Naha", 26.15, 127.766667, {NavtexTransmitter::Schedule('G', 518000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(17, 0), QTime(21, 0)})}}, + {11, "Moji", 33.95, 130.966667, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)})}}, + {11, "Puerto Princesa", 9.733333, 118.716667, {NavtexTransmitter::Schedule('I', 518000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)})}}, + {11, "Yokohama", 35.433333, 139.633333, {NavtexTransmitter::Schedule('I', 518000, {QTime(1, 20), QTime(5, 20), QTime(9, 20), QTime(13, 20), QTime(17, 20), QTime(21, 20)})}}, // Duplicate Id + {11, "Manila", 14.583333, 121.05, {NavtexTransmitter::Schedule('J', 518000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(17, 30), QTime(21, 30)})}}, + {11, "Otaru", 43.2, 141, {NavtexTransmitter::Schedule('J', 518000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(17, 30), QTime(21, 30)})}}, // Duplicate Id + {11, "Davao City", 7.066667, 125.6, {NavtexTransmitter::Schedule('K', 518000, {QTime(1, 40), QTime(5, 40), QTime(9, 40), QTime(13, 40), QTime(17, 40), QTime(21, 40)})}}, + {11, "Kushiro", 42.983333, 144.383333, {NavtexTransmitter::Schedule('K', 518000, {QTime(1, 40), QTime(5, 40), QTime(9, 40), QTime(13, 40), QTime(17, 40), QTime(21, 40)})}}, // Duplicate Id + {11, "Hongkong", 22.209167, 114.256111, {NavtexTransmitter::Schedule('L', 518000, {QTime(1, 50), QTime(5, 50), QTime(9, 50), QTime(13, 50), QTime(17, 50), QTime(21, 50)})}}, + {11, "Sanya", 18.232222, 109.495833, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {11, "Guangzhou", 23.15, 113.483333, {NavtexTransmitter::Schedule('N', 518000, {QTime(2, 10), QTime(6, 10), QTime(10, 10), QTime(14, 10), QTime(18, 10), QTime(22, 10)})}}, + {11, "Fuzhou", 26.028544, 119.305444, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)})}}, + {11, "Da Nang", 16.083333, 108.233333, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, + {11, "Chilung", 25.15, 121.733333, {NavtexTransmitter::Schedule('P', 518000, {QTime(2, 30), QTime(6, 30), QTime(10, 30), QTime(14, 30), QTime(18, 30), QTime(22, 30)})}}, // Duplicate Id + {11, "Shanghai", 31.108889, 121.544167, {NavtexTransmitter::Schedule('Q', 518000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(18, 40), QTime(22, 40)})}}, + {11, "Dalian", 38.845244, 121.518056, {NavtexTransmitter::Schedule('R', 518000, {QTime(2, 50), QTime(6, 50), QTime(10, 50), QTime(14, 50), QTime(18, 50), QTime(22, 50)})}}, + {11, "Sandakan", 5.895886, 118.00305, {NavtexTransmitter::Schedule('S', 518000, {QTime(3, 0), QTime(7, 0), QTime(12, 0), QTime(15, 0), QTime(19, 0), QTime(23, 0)})}}, + {11, "Miri", 4.438, 114.020889, {NavtexTransmitter::Schedule('T', 518000, {QTime(3, 10), QTime(7, 10), QTime(11, 10), QTime(15, 10), QTime(19, 10), QTime(23, 10)})}}, + {11, "Penang", 5.425, 100.403056, {NavtexTransmitter::Schedule('U', 518000, {QTime(3, 20), QTime(7, 20), QTime(11, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)})}}, + {11, "Guam", 13.47445, 144.844389, {NavtexTransmitter::Schedule('V', 518000, {QTime(3, 30), QTime(7, 30), QTime(11, 30), QTime(15, 30), QTime(19, 30), QTime(23, 30)})}}, + {11, "Jukbyeon", 37.05, 129.416667, {NavtexTransmitter::Schedule('V', 518000, {QTime(3, 30), QTime(7, 30), QTime(11, 30), QTime(15, 30), QTime(19, 30), QTime(23, 30)}), // Duplicate Id + NavtexTransmitter::Schedule('J', 490000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(15, 30), QTime(21, 30)})}}, + + {11, "Byeonsan", 35.6, 126.483333, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)}), + NavtexTransmitter::Schedule('K', 490000, {QTime(1, 40), QTime(5, 40), QTime(9, 40), QTime(17, 40), QTime(21, 40)})}}, + {11, "Ho-Chi-Minh City", 10.703317, 106.729139, {NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 50), QTime(12, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)})}}, + + {12, "San Francisco", 37.925739, -122.734056, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, + {12, "Prince Rupert", 54.298519, -130.417669, {NavtexTransmitter::Schedule('D', 518000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(12, 30), QTime(16, 30), QTime(20, 30)})}}, + {12, "Tofino", 48.925478, -125.540306, {NavtexTransmitter::Schedule('H', 518000, {QTime(1, 10), QTime(5, 10), QTime(9, 10), QTime(13, 10), QTime(17, 10), QTime(21, 10)})}}, + {12, "Kodiak", 57.781606, -152.537583, {NavtexTransmitter::Schedule('J', 518000, {QTime(1, 30), QTime(5, 30), QTime(9, 30), QTime(13, 30), QTime(17, 30), QTime(21, 30)}), + NavtexTransmitter::Schedule('X', 518000, {QTime(3, 50), QTime(7, 50), QTime(12, 50), QTime(15, 50), QTime(19, 50), QTime(23, 50)})}}, + {12, "Ayora", -0.75, -90.316667, {NavtexTransmitter::Schedule('L', 518000, {QTime(1, 50), QTime(5, 50), QTime(9, 50), QTime(13, 50), QTime(17, 50), QTime(21, 50)}), + NavtexTransmitter::Schedule('A', 490000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {12, "Guayaquil", -2.283333, -80.016667, {NavtexTransmitter::Schedule('M', 518000, {QTime(2, 0), QTime(6, 0), QTime(10, 0), QTime(14, 0), QTime(18, 0), QTime(22, 0)})}}, + {12, "Honolulu", 21.437019, -158.143239, {NavtexTransmitter::Schedule('O', 518000, {QTime(2, 20), QTime(6, 20), QTime(10, 20), QTime(14, 20), QTime(18, 20), QTime(22, 20)})}}, + {12, "Cambria", 35.524297, -121.061922, {NavtexTransmitter::Schedule('Q', 518000, {QTime(2, 40), QTime(6, 40), QTime(10, 40), QTime(14, 40), QTime(18, 40), QTime(22, 40)})}}, + {12, "Astoria", 46.203989, -123.955639, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)})}}, + + {13, "Vladivostok", 43.381472, 131.899861, {NavtexTransmitter::Schedule('A', 518000, {QTime(0, 0), QTime(4, 0), QTime(8, 0), QTime(12, 0), QTime(16, 0), QTime(20, 0)})}}, + {13, "Kholmsk", 47.023556, 142.045056, {NavtexTransmitter::Schedule('B', 518000, {QTime(0, 10), QTime(4, 10), QTime(8, 10), QTime(12, 10), QTime(16, 10), QTime(20, 10)})}}, + {13, "Murmansk", 68.865803, 33.070761, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, + {13, "Petropavlosk", 53.247778, 158.419472, {NavtexTransmitter::Schedule('C', 518000, {QTime(0, 20), QTime(4, 20), QTime(8, 20), QTime(12, 20), QTime(16, 20), QTime(20, 20)})}}, // Duplicate + {13, "Magadan", 59.683333, 150.15, {NavtexTransmitter::Schedule('D', 518000, {QTime(0, 30), QTime(4, 30), QTime(8, 30), QTime(12, 30), QTime(16, 30), QTime(20, 30)})}}, + {13, "Archangelsk", 64.556278, 40.550028, {NavtexTransmitter::Schedule('F', 518000, {QTime(0, 50), QTime(4, 50), QTime(8, 50), QTime(12, 50), QTime(16, 50), QTime(20, 50)})}}, + {13, "Okhotsk", 59.366667, 143.2, {NavtexTransmitter::Schedule('G', 518000, {QTime(1, 0), QTime(5, 0), QTime(9, 0), QTime(13, 0), QTime(17, 0), QTime(21, 0)})}}, + {13, "Astrakhan", 46.296694, 47.997778, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)})}}, + + {15, "Antofagasta", -23.491333, -70.424778, {NavtexTransmitter::Schedule('A', 518000, {QTime(4, 0), QTime(12, 0), QTime(20, 0)})}}, + {15, "Valparaíso", -32.802222, -71.485, {NavtexTransmitter::Schedule('B', 518000, {QTime(4, 10), QTime(12, 10), QTime(20, 10)})}}, + {15, "Talcahuano", -36.715056, -73.108, {NavtexTransmitter::Schedule('C', 518000, {QTime(4, 20), QTime(12, 20), QTime(20, 20)})}}, + {15, "Puerto Montt", -41.489983, -72.957744, {NavtexTransmitter::Schedule('D', 518000, {QTime(4, 30), QTime(12, 30), QTime(20, 30)})}}, + {15, "Punta Arenas", -52.948111, -71.056944, {NavtexTransmitter::Schedule('E', 518000, {QTime(4, 40), QTime(12, 40), QTime(20, 40)})}}, + {15, "Easter Island", -27.15, -109.416667, {NavtexTransmitter::Schedule('F', 518000, {QTime(4, 50), QTime(12, 50), QTime(20, 50)})}}, + + {16, "Paita", -5.083333, -81.116667, {NavtexTransmitter::Schedule('S', 518000, {QTime(3, 0), QTime(7, 0), QTime(12, 0), QTime(15, 0), QTime(19, 0), QTime(23, 0)})}}, + {16, "Callao", -12.5, -77.15, {NavtexTransmitter::Schedule('U', 518000, {QTime(3, 20), QTime(7, 20), QTime(11, 20), QTime(15, 20), QTime(19, 20), QTime(23, 20)})}}, + {16, "Mollendo", -17.016667, -72.016667, {NavtexTransmitter::Schedule('W', 518000, {QTime(3, 40), QTime(7, 40), QTime(12, 40), QTime(15, 40), QTime(19, 40), QTime(23, 40)})}}, + +}; + +const QMap NavtexMessage::m_types = { + {"A", "Navigational warning"}, + {"B", "Meteorological warning"}, + {"C", "Ice reports"}, + {"D", "Search and rescue"}, + {"E", "Meteorological forecasts"}, + {"F", "Pilot service messages"}, + {"G", "AIS"}, + {"H", "LORAN"}, + {"J", "SATNAV"}, + {"K", "Navaid messages"}, + {"L", "Navigational warning"}, + {"T", "Test transmissions"}, + {"X", "Special services"}, + {"Y", "Special services"}, + {"Z", "No message"} +}; + +const NavtexTransmitter* NavtexTransmitter::getTransmitter(QTime time, int area, qint64 frequency) +{ + for (const auto& transmitter : NavtexTransmitter::m_navtexTransmitters) + { + if (transmitter.m_area == area) + { + for (const auto& schedule : transmitter.m_schedules) + { + if (schedule.m_frequency == frequency) + { + for (const auto& txStartTime : schedule.m_times) + { + // Transmitters have 10 minute windows for transmission + int secs = txStartTime.secsTo(time); + if ((secs >= 0) && (secs < 10*60)) { + return &transmitter; + } + } + } + } + } + } + return nullptr; +} + +NavtexMessage::NavtexMessage(QDateTime dateTime, const QString& stationId, const QString& typeId, const QString& id, const QString& message) : + m_dateTime(dateTime), + m_stationId(stationId), + m_typeId(typeId), + m_id(id), + m_message(message), + m_valid(true) +{ +} + + +NavtexMessage::NavtexMessage(const QString& text) +{ + m_dateTime = QDateTime::currentDateTime(); + QRegularExpression re("[Z*][C*][Z*][C*][ *]([A-Z])([A-Z])(\\d\\d)((.|\n|\r)*)[N*][N*][N*][N*]"); + + QRegularExpressionMatch match = re.match(text); + if (match.hasMatch()) + { + m_stationId = match.captured(1); + m_typeId = match.captured(2); + m_id = match.captured(3); + m_message = match.captured(4).trimmed(); + m_valid = true; + } + else + { + m_message = text; + m_valid = false; + } +} + +QString NavtexMessage::getStation(int area, qint64 frequency) const +{ + for (const auto& transmitter : NavtexTransmitter::m_navtexTransmitters) + { + if (transmitter.m_area == area) + { + for (const auto& schedule : transmitter.m_schedules) + { + if ((schedule.m_id == m_stationId) && (schedule.m_frequency == frequency)) { + return transmitter.m_station; + } + } + } + + } + return ""; +} + +QString NavtexMessage::getType() const +{ + if (m_valid && m_types.contains(m_typeId)) { + return m_types.value(m_typeId); + } + return ""; +} + +void SitorBDecoder::init() +{ + m_state = PHASING; + m_idx = 0; + m_figureSet = false; + m_errors = 0; +} + +// In: +// Received 7-bit CCIR476 sequence +// Returns: +// Decoded ASCII character +// ETX end of text +// '*' both chars invalid +// -1 no character available yet +char SitorBDecoder::decode(char c) +{ + char ret = -1; + + //qDebug() << "In: " << printable(ccir476Decode(c)); + + switch (m_state) + { + case PHASING: + // Wait until we get a valid non-phasing character + if ((c != PHASING_1) && (c != PHASING_2) && (ccir476Decode(c) != -1)) + { + m_buf[m_idx++] = c; + m_state = FILL_RX; + } + break; + + case FILL_DX: + // Fill up buffer + m_buf[m_idx++] = c; + if (m_idx == BUFFER_SIZE) + { + m_state = RX; + m_idx = 0; + } + else + { + m_state = FILL_RX; + } + break; + + case FILL_RX: + // Should be phasing 1 + if (c != PHASING_1) { + m_errors++; + } + m_state = FILL_DX; + break; + + + case RX: + { + // Try to decode a character + char dx = ccir476Decode(m_buf[m_idx]); + char rx = ccir476Decode(c); + char a; + + // Idle alpha (phasing 1) in both dx and rx means end of signal + if ((dx == '<') && (rx == '<')) + { + a = 0x2; // ETX - End of text + } + else if (dx != -1) + { + a = dx; // First received character has no detectable error + if ((dx != rx) && !((dx == '<') && (rx == '>')) && !((dx == '>') && (rx == '<'))) { + m_errors++; + } + } + else if (rx != -1) + { + a = rx; // Second received character has no detectable error + m_errors++; + } + else + { + a = '*'; // Both received characters have errors + m_errors += 2; + } + if (a == 0xf) { + m_figureSet = false; + } else if (a == 0xe) { + m_figureSet = true; + } else { + ret = a; + } + m_state = DX; + } + break; + + case DX: + // Save received character in buffer + m_buf[m_idx] = c; + m_idx = (m_idx + 1) % BUFFER_SIZE; + m_state = RX; + break; + } + + return ret; +} + +QString SitorBDecoder::printable(char c) +{ + if (c == -1) { + return "Unknown"; + } else if (c == 0x2) { + return "End of transmission"; + } else if (c == 0xf) { + return "Letter"; + } else if (c == 0xe) { + return "Figure"; + } else if (c == 0x5) { + return "Cross"; + } else if (c == 0x7) { + return "Bell"; + } else { + return c; + } +} + +// https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.476-5-199510-I!!PDF-E.pdf - Table 1 + +// https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.625-4-201203-I!!PDF-E.pdf + +const char SitorBDecoder::m_ccir476LetterSetDecode[128] = { + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 0x0d, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 'T', + -1, + -1, + -1, + 0x0a, + -1, + ' ', + 'V', + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 'B', + -1, + -1, + -1, + -1, + -1, + 0x0f, + 'X', + -1, + -1, + -1, + -1, + '>', + -1, + 'E', + 0x0e, + -1, + -1, + 'U', + 'Q', + -1, + 'K', + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 'O', + -1, + -1, + -1, + 'H', + -1, + 'N', + 'M', + -1, + -1, + -1, + -1, + 'L', + -1, + 'R', + 'G', + -1, + -1, + 'I', + 'P', + -1, + 'C', + -1, + -1, + -1, + -1, + -1, + -1, + 'Z', + -1, + 'D', + -1, + -1, + -1, + 'S', + 'Y', + -1, + 'F', + -1, + -1, + -1, + -1, + 'A', + 'W', + -1, + 'J', + -1, + -1, + -1, + '<', + -1, + -1, + -1, + -1, + -1, + -1, + -1, +}; + +const char SitorBDecoder::m_ccir476FigureSetDecode[128] = { + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 0x0d, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + '5', + -1, + -1, + -1, + 0x0a, + -1, + ' ', + '=', + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + '?', + -1, + -1, + -1, + -1, + -1, + 0x0f, + '/', + -1, + -1, + -1, + -1, + '>', + -1, + '3', + 0x0e, + -1, + -1, + '7', + '1', + -1, + '(', + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + '9', + -1, + -1, + -1, + -93, + -1, + ',', + '.', + -1, + -1, + -1, + -1, + ')', + -1, + '4', + '&', + -1, + -1, + '8', + '0', + -1, + ':', + -1, + -1, + -1, + -1, + -1, + -1, + '+', + -1, + 0x05, + -1, + -1, + -1, + '\'', + '6', + -1, + '!', + -1, + -1, + -1, + -1, + '-', + '2', + -1, + 0x07, + -1, + -1, + -1, + '<', + -1, + -1, + -1, + -1, + -1, + -1, + -1, +}; + +char SitorBDecoder::ccir476Decode(char c) +{ + if (m_figureSet) { + return m_ccir476FigureSetDecode[c]; + } else { + return m_ccir476LetterSetDecode[c]; + } +} + diff --git a/sdrbase/util/navtex.h b/sdrbase/util/navtex.h new file mode 100644 index 000000000..e1086240b --- /dev/null +++ b/sdrbase/util/navtex.h @@ -0,0 +1,111 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_UTIL_NAVTEX_H +#define INCLUDE_UTIL_NAVTEX_H + +#include +#include +#include + +#include "export.h" + +class SDRBASE_API NavtexTransmitter { + +public: + + struct Schedule { + char m_id; + qint64 m_frequency; + QList m_times; + Schedule(char id, qint64 frequency) : + m_id(id), + m_frequency(frequency) + { + } + Schedule(char id, qint64 frequency, QList times) : + m_id(id), + m_frequency(frequency), + m_times(times) + { + } + }; + + int m_area; + QString m_station; + float m_latitude; + float m_longitude; + QList m_schedules; + + static const QList m_navtexTransmitters; + static const NavtexTransmitter* getTransmitter(QTime time, int area, qint64 frequency); +}; + +class SDRBASE_API NavtexMessage { + +public: + + QString m_stationId; + QString m_typeId; + QString m_id; + QString m_message; + QDateTime m_dateTime; + bool m_valid; + + static const QMap m_types; + + NavtexMessage(const QString& text); + NavtexMessage(QDateTime dataTime, const QString& stationId, const QString& typeId, const QString& id, const QString& message); + QString getStation(int area, qint64 frequency) const; + QString getType() const; + +}; + +class SDRBASE_API SitorBDecoder { + +public: + + void init(); + char decode(char c); + int getErrors() const { return m_errors; } + static QString printable(char c); + +private: + static const char PHASING_1 = 0x78; + static const char PHASING_2 = 0x33; + static const int BUFFER_SIZE = 3; + char m_buf[3]; + bool m_figureSet; + enum State { + PHASING, + FILL_DX, + FILL_RX, + DX, + RX + } m_state; + int m_idx; + int m_errors; + + static const char m_ccir476LetterSetDecode[128]; + static const char m_ccir476FigureSetDecode[128]; + + char ccir476Decode(char c); + +}; + +#endif // INCLUDE_UTIL_NAVTEX_H + diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 65020ee91..23cff09c5 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -51,12 +51,16 @@ ChannelReport: $ref: "http://swgserver:8081/api/swagger/include/FreqTracker.yaml#/FreqTrackerReport" FT8DemodReport: $ref: "http://swgserver:8081/api/swagger/include/FT8Demod.yaml#/FT8DemodReport" + RTTYDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/RTTYDemod.yaml#/RTTYDemodReport" HeatMapReport: $ref: "http://swgserver:8081/api/swagger/include/HeatMap.yaml#/HeatMapReport" M17DemodReport: $ref: "http://swgserver:8081/api/swagger/include/M17Demod.yaml#/M17DemodReport" M17ModReport: $ref: "http://swgserver:8081/api/swagger/include/M17Mod.yaml#/M17ModReport" + NavtexDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/NavtexDemod.yaml#/NavtexDemodReport" NFMDemodReport: $ref: "http://swgserver:8081/api/swagger/include/NFMDemod.yaml#/NFMDemodReport" NFMModReport: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index 3222b6590..3938261df 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -65,6 +65,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/FreqTracker.yaml#/FreqTrackerSettings" FT8DemodSettings: $ref: "http://swgserver:8081/api/swagger/include/FT8Demod.yaml#/FT8DemodSettings" + RTTYDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/RTTYDemod.yaml#/RTTYDemodSettings" HeatMapSettings: $ref: "http://swgserver:8081/api/swagger/include/HeatMap.yaml#/HeatMapSettings" InterferometerSettings: @@ -75,6 +77,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/M17Demod.yaml#/M17DemodSettings" M17ModSettings: $ref: "http://swgserver:8081/api/swagger/include/M17Mod.yaml#/M17ModSettings" + NavtexDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/NavtexDemod.yaml#/NavtexDemodSettings" NFMDemodSettings: $ref: "http://swgserver:8081/api/swagger/include/NFMDemod.yaml#/NFMDemodSettings" NFMModSettings: diff --git a/swagger/sdrangel/api/swagger/include/NavtexDemod.yaml b/swagger/sdrangel/api/swagger/include/NavtexDemod.yaml new file mode 100644 index 000000000..af2417eb7 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/NavtexDemod.yaml @@ -0,0 +1,62 @@ +NavtexDemodSettings: + description: ACARSDemod + properties: + inputFrequencyOffset: + type: integer + format: int64 + rfBandwidth: + type: number + format: float + navArea: + type: integer + filterStation: + type: string + filterType: + type: string + udpEnabled: + description: "Whether to forward received messages to specified UDP port" + type: integer + udpAddress: + description: "UDP address to forward received messages to" + type: string + udpPort: + description: "UDP port to forward received messages 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" + +NavtexDemodReport: + description: ACARSDemod + properties: + channelPowerDB: + description: power received in channel (dB) + type: number + format: float + channelSampleRate: + type: integer diff --git a/swagger/sdrangel/api/swagger/include/RTTYDemod.yaml b/swagger/sdrangel/api/swagger/include/RTTYDemod.yaml new file mode 100644 index 000000000..b4ff78c66 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/RTTYDemod.yaml @@ -0,0 +1,73 @@ +RTTYDemodSettings: + description: ACARSDemod + properties: + inputFrequencyOffset: + type: integer + format: int64 + rfBandwidth: + type: number + format: float + baudRate: + type: number + format: float + frequencyShift: + type: integer + udpEnabled: + description: "Whether to forward received messages to specified UDP port" + type: integer + udpAddress: + description: "UDP address to forward received messages to" + type: string + udpPort: + description: "UDP port to forward received messages to" + type: integer + characterSet: + type: integer + suppressCRLF: + type: integer + unshiftOnSpace: + type: integer + msbFirst: + type: integer + spaceHigh: + type: integer + squelch: + 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" + +RTTYDemodReport: + description: ACARSDemod + 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 8f4f6d22d..b6eb84f09 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -72,12 +72,16 @@ SWGChannelReport::SWGChannelReport() { m_freq_tracker_report_isSet = false; ft8_demod_report = nullptr; m_ft8_demod_report_isSet = false; + rtty_demod_report = nullptr; + m_rtty_demod_report_isSet = false; heat_map_report = nullptr; m_heat_map_report_isSet = false; m17_demod_report = nullptr; m_m17_demod_report_isSet = false; m17_mod_report = nullptr; m_m17_mod_report_isSet = false; + navtex_demod_report = nullptr; + m_navtex_demod_report_isSet = false; nfm_demod_report = nullptr; m_nfm_demod_report_isSet = false; nfm_mod_report = nullptr; @@ -166,12 +170,16 @@ SWGChannelReport::init() { m_freq_tracker_report_isSet = false; ft8_demod_report = new SWGFT8DemodReport(); m_ft8_demod_report_isSet = false; + rtty_demod_report = new SWGRTTYDemodReport(); + m_rtty_demod_report_isSet = false; heat_map_report = new SWGHeatMapReport(); m_heat_map_report_isSet = false; m17_demod_report = new SWGM17DemodReport(); m_m17_demod_report_isSet = false; m17_mod_report = new SWGM17ModReport(); m_m17_mod_report_isSet = false; + navtex_demod_report = new SWGNavtexDemodReport(); + m_navtex_demod_report_isSet = false; nfm_demod_report = new SWGNFMDemodReport(); m_nfm_demod_report_isSet = false; nfm_mod_report = new SWGNFMModReport(); @@ -276,6 +284,9 @@ SWGChannelReport::cleanup() { if(ft8_demod_report != nullptr) { delete ft8_demod_report; } + if(rtty_demod_report != nullptr) { + delete rtty_demod_report; + } if(heat_map_report != nullptr) { delete heat_map_report; } @@ -285,6 +296,9 @@ SWGChannelReport::cleanup() { if(m17_mod_report != nullptr) { delete m17_mod_report; } + if(navtex_demod_report != nullptr) { + delete navtex_demod_report; + } if(nfm_demod_report != nullptr) { delete nfm_demod_report; } @@ -396,12 +410,16 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&ft8_demod_report, pJson["FT8DemodReport"], "SWGFT8DemodReport", "SWGFT8DemodReport"); + ::SWGSDRangel::setValue(&rtty_demod_report, pJson["RTTYDemodReport"], "SWGRTTYDemodReport", "SWGRTTYDemodReport"); + ::SWGSDRangel::setValue(&heat_map_report, pJson["HeatMapReport"], "SWGHeatMapReport", "SWGHeatMapReport"); ::SWGSDRangel::setValue(&m17_demod_report, pJson["M17DemodReport"], "SWGM17DemodReport", "SWGM17DemodReport"); ::SWGSDRangel::setValue(&m17_mod_report, pJson["M17ModReport"], "SWGM17ModReport", "SWGM17ModReport"); + ::SWGSDRangel::setValue(&navtex_demod_report, pJson["NavtexDemodReport"], "SWGNavtexDemodReport", "SWGNavtexDemodReport"); + ::SWGSDRangel::setValue(&nfm_demod_report, pJson["NFMDemodReport"], "SWGNFMDemodReport", "SWGNFMDemodReport"); ::SWGSDRangel::setValue(&nfm_mod_report, pJson["NFMModReport"], "SWGNFMModReport", "SWGNFMModReport"); @@ -520,6 +538,9 @@ SWGChannelReport::asJsonObject() { if((ft8_demod_report != nullptr) && (ft8_demod_report->isSet())){ toJsonValue(QString("FT8DemodReport"), ft8_demod_report, obj, QString("SWGFT8DemodReport")); } + if((rtty_demod_report != nullptr) && (rtty_demod_report->isSet())){ + toJsonValue(QString("RTTYDemodReport"), rtty_demod_report, obj, QString("SWGRTTYDemodReport")); + } if((heat_map_report != nullptr) && (heat_map_report->isSet())){ toJsonValue(QString("HeatMapReport"), heat_map_report, obj, QString("SWGHeatMapReport")); } @@ -529,6 +550,9 @@ SWGChannelReport::asJsonObject() { if((m17_mod_report != nullptr) && (m17_mod_report->isSet())){ toJsonValue(QString("M17ModReport"), m17_mod_report, obj, QString("SWGM17ModReport")); } + if((navtex_demod_report != nullptr) && (navtex_demod_report->isSet())){ + toJsonValue(QString("NavtexDemodReport"), navtex_demod_report, obj, QString("SWGNavtexDemodReport")); + } if((nfm_demod_report != nullptr) && (nfm_demod_report->isSet())){ toJsonValue(QString("NFMDemodReport"), nfm_demod_report, obj, QString("SWGNFMDemodReport")); } @@ -807,6 +831,16 @@ SWGChannelReport::setFt8DemodReport(SWGFT8DemodReport* ft8_demod_report) { this->m_ft8_demod_report_isSet = true; } +SWGRTTYDemodReport* +SWGChannelReport::getRttyDemodReport() { + return rtty_demod_report; +} +void +SWGChannelReport::setRttyDemodReport(SWGRTTYDemodReport* rtty_demod_report) { + this->rtty_demod_report = rtty_demod_report; + this->m_rtty_demod_report_isSet = true; +} + SWGHeatMapReport* SWGChannelReport::getHeatMapReport() { return heat_map_report; @@ -837,6 +871,16 @@ SWGChannelReport::setM17ModReport(SWGM17ModReport* m17_mod_report) { this->m_m17_mod_report_isSet = true; } +SWGNavtexDemodReport* +SWGChannelReport::getNavtexDemodReport() { + return navtex_demod_report; +} +void +SWGChannelReport::setNavtexDemodReport(SWGNavtexDemodReport* navtex_demod_report) { + this->navtex_demod_report = navtex_demod_report; + this->m_navtex_demod_report_isSet = true; +} + SWGNFMDemodReport* SWGChannelReport::getNfmDemodReport() { return nfm_demod_report; @@ -1088,6 +1132,9 @@ SWGChannelReport::isSet(){ if(ft8_demod_report && ft8_demod_report->isSet()){ isObjectUpdated = true; break; } + if(rtty_demod_report && rtty_demod_report->isSet()){ + isObjectUpdated = true; break; + } if(heat_map_report && heat_map_report->isSet()){ isObjectUpdated = true; break; } @@ -1097,6 +1144,9 @@ SWGChannelReport::isSet(){ if(m17_mod_report && m17_mod_report->isSet()){ isObjectUpdated = true; break; } + if(navtex_demod_report && navtex_demod_report->isSet()){ + isObjectUpdated = true; break; + } if(nfm_demod_report && nfm_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 fa45747a9..4d7abbd88 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -47,10 +47,12 @@ #include "SWGM17ModReport.h" #include "SWGNFMDemodReport.h" #include "SWGNFMModReport.h" +#include "SWGNavtexDemodReport.h" #include "SWGNoiseFigureReport.h" #include "SWGPacketDemodReport.h" #include "SWGPacketModReport.h" #include "SWGPagerDemodReport.h" +#include "SWGRTTYDemodReport.h" #include "SWGRadioAstronomyReport.h" #include "SWGRadioClockReport.h" #include "SWGRadiosondeDemodReport.h" @@ -149,6 +151,9 @@ public: SWGFT8DemodReport* getFt8DemodReport(); void setFt8DemodReport(SWGFT8DemodReport* ft8_demod_report); + SWGRTTYDemodReport* getRttyDemodReport(); + void setRttyDemodReport(SWGRTTYDemodReport* rtty_demod_report); + SWGHeatMapReport* getHeatMapReport(); void setHeatMapReport(SWGHeatMapReport* heat_map_report); @@ -158,6 +163,9 @@ public: SWGM17ModReport* getM17ModReport(); void setM17ModReport(SWGM17ModReport* m17_mod_report); + SWGNavtexDemodReport* getNavtexDemodReport(); + void setNavtexDemodReport(SWGNavtexDemodReport* navtex_demod_report); + SWGNFMDemodReport* getNfmDemodReport(); void setNfmDemodReport(SWGNFMDemodReport* nfm_demod_report); @@ -282,6 +290,9 @@ private: SWGFT8DemodReport* ft8_demod_report; bool m_ft8_demod_report_isSet; + SWGRTTYDemodReport* rtty_demod_report; + bool m_rtty_demod_report_isSet; + SWGHeatMapReport* heat_map_report; bool m_heat_map_report_isSet; @@ -291,6 +302,9 @@ private: SWGM17ModReport* m17_mod_report; bool m_m17_mod_report_isSet; + SWGNavtexDemodReport* navtex_demod_report; + bool m_navtex_demod_report_isSet; + SWGNFMDemodReport* nfm_demod_report; bool m_nfm_demod_report_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index 557b412eb..476da4f74 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -84,6 +84,8 @@ SWGChannelSettings::SWGChannelSettings() { m_freq_tracker_settings_isSet = false; ft8_demod_settings = nullptr; m_ft8_demod_settings_isSet = false; + rtty_demod_settings = nullptr; + m_rtty_demod_settings_isSet = false; heat_map_settings = nullptr; m_heat_map_settings_isSet = false; interferometer_settings = nullptr; @@ -94,6 +96,8 @@ SWGChannelSettings::SWGChannelSettings() { m_m17_demod_settings_isSet = false; m17_mod_settings = nullptr; m_m17_mod_settings_isSet = false; + navtex_demod_settings = nullptr; + m_navtex_demod_settings_isSet = false; nfm_demod_settings = nullptr; m_nfm_demod_settings_isSet = false; nfm_mod_settings = nullptr; @@ -202,6 +206,8 @@ SWGChannelSettings::init() { m_freq_tracker_settings_isSet = false; ft8_demod_settings = new SWGFT8DemodSettings(); m_ft8_demod_settings_isSet = false; + rtty_demod_settings = new SWGRTTYDemodSettings(); + m_rtty_demod_settings_isSet = false; heat_map_settings = new SWGHeatMapSettings(); m_heat_map_settings_isSet = false; interferometer_settings = new SWGInterferometerSettings(); @@ -212,6 +218,8 @@ SWGChannelSettings::init() { m_m17_demod_settings_isSet = false; m17_mod_settings = new SWGM17ModSettings(); m_m17_mod_settings_isSet = false; + navtex_demod_settings = new SWGNavtexDemodSettings(); + m_navtex_demod_settings_isSet = false; nfm_demod_settings = new SWGNFMDemodSettings(); m_nfm_demod_settings_isSet = false; nfm_mod_settings = new SWGNFMModSettings(); @@ -338,6 +346,9 @@ SWGChannelSettings::cleanup() { if(ft8_demod_settings != nullptr) { delete ft8_demod_settings; } + if(rtty_demod_settings != nullptr) { + delete rtty_demod_settings; + } if(heat_map_settings != nullptr) { delete heat_map_settings; } @@ -353,6 +364,9 @@ SWGChannelSettings::cleanup() { if(m17_mod_settings != nullptr) { delete m17_mod_settings; } + if(navtex_demod_settings != nullptr) { + delete navtex_demod_settings; + } if(nfm_demod_settings != nullptr) { delete nfm_demod_settings; } @@ -488,6 +502,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&ft8_demod_settings, pJson["FT8DemodSettings"], "SWGFT8DemodSettings", "SWGFT8DemodSettings"); + ::SWGSDRangel::setValue(&rtty_demod_settings, pJson["RTTYDemodSettings"], "SWGRTTYDemodSettings", "SWGRTTYDemodSettings"); + ::SWGSDRangel::setValue(&heat_map_settings, pJson["HeatMapSettings"], "SWGHeatMapSettings", "SWGHeatMapSettings"); ::SWGSDRangel::setValue(&interferometer_settings, pJson["InterferometerSettings"], "SWGInterferometerSettings", "SWGInterferometerSettings"); @@ -498,6 +514,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&m17_mod_settings, pJson["M17ModSettings"], "SWGM17ModSettings", "SWGM17ModSettings"); + ::SWGSDRangel::setValue(&navtex_demod_settings, pJson["NavtexDemodSettings"], "SWGNavtexDemodSettings", "SWGNavtexDemodSettings"); + ::SWGSDRangel::setValue(&nfm_demod_settings, pJson["NFMDemodSettings"], "SWGNFMDemodSettings", "SWGNFMDemodSettings"); ::SWGSDRangel::setValue(&nfm_mod_settings, pJson["NFMModSettings"], "SWGNFMModSettings", "SWGNFMModSettings"); @@ -642,6 +660,9 @@ SWGChannelSettings::asJsonObject() { if((ft8_demod_settings != nullptr) && (ft8_demod_settings->isSet())){ toJsonValue(QString("FT8DemodSettings"), ft8_demod_settings, obj, QString("SWGFT8DemodSettings")); } + if((rtty_demod_settings != nullptr) && (rtty_demod_settings->isSet())){ + toJsonValue(QString("RTTYDemodSettings"), rtty_demod_settings, obj, QString("SWGRTTYDemodSettings")); + } if((heat_map_settings != nullptr) && (heat_map_settings->isSet())){ toJsonValue(QString("HeatMapSettings"), heat_map_settings, obj, QString("SWGHeatMapSettings")); } @@ -657,6 +678,9 @@ SWGChannelSettings::asJsonObject() { if((m17_mod_settings != nullptr) && (m17_mod_settings->isSet())){ toJsonValue(QString("M17ModSettings"), m17_mod_settings, obj, QString("SWGM17ModSettings")); } + if((navtex_demod_settings != nullptr) && (navtex_demod_settings->isSet())){ + toJsonValue(QString("NavtexDemodSettings"), navtex_demod_settings, obj, QString("SWGNavtexDemodSettings")); + } if((nfm_demod_settings != nullptr) && (nfm_demod_settings->isSet())){ toJsonValue(QString("NFMDemodSettings"), nfm_demod_settings, obj, QString("SWGNFMDemodSettings")); } @@ -1007,6 +1031,16 @@ SWGChannelSettings::setFt8DemodSettings(SWGFT8DemodSettings* ft8_demod_settings) this->m_ft8_demod_settings_isSet = true; } +SWGRTTYDemodSettings* +SWGChannelSettings::getRttyDemodSettings() { + return rtty_demod_settings; +} +void +SWGChannelSettings::setRttyDemodSettings(SWGRTTYDemodSettings* rtty_demod_settings) { + this->rtty_demod_settings = rtty_demod_settings; + this->m_rtty_demod_settings_isSet = true; +} + SWGHeatMapSettings* SWGChannelSettings::getHeatMapSettings() { return heat_map_settings; @@ -1057,6 +1091,16 @@ SWGChannelSettings::setM17ModSettings(SWGM17ModSettings* m17_mod_settings) { this->m_m17_mod_settings_isSet = true; } +SWGNavtexDemodSettings* +SWGChannelSettings::getNavtexDemodSettings() { + return navtex_demod_settings; +} +void +SWGChannelSettings::setNavtexDemodSettings(SWGNavtexDemodSettings* navtex_demod_settings) { + this->navtex_demod_settings = navtex_demod_settings; + this->m_navtex_demod_settings_isSet = true; +} + SWGNFMDemodSettings* SWGChannelSettings::getNfmDemodSettings() { return nfm_demod_settings; @@ -1366,6 +1410,9 @@ SWGChannelSettings::isSet(){ if(ft8_demod_settings && ft8_demod_settings->isSet()){ isObjectUpdated = true; break; } + if(rtty_demod_settings && rtty_demod_settings->isSet()){ + isObjectUpdated = true; break; + } if(heat_map_settings && heat_map_settings->isSet()){ isObjectUpdated = true; break; } @@ -1381,6 +1428,9 @@ SWGChannelSettings::isSet(){ if(m17_mod_settings && m17_mod_settings->isSet()){ isObjectUpdated = true; break; } + if(navtex_demod_settings && navtex_demod_settings->isSet()){ + isObjectUpdated = true; break; + } if(nfm_demod_settings && nfm_demod_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h index b30d51860..a3c00926b 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -55,10 +55,12 @@ #include "SWGM17ModSettings.h" #include "SWGNFMDemodSettings.h" #include "SWGNFMModSettings.h" +#include "SWGNavtexDemodSettings.h" #include "SWGNoiseFigureSettings.h" #include "SWGPacketDemodSettings.h" #include "SWGPacketModSettings.h" #include "SWGPagerDemodSettings.h" +#include "SWGRTTYDemodSettings.h" #include "SWGRadioAstronomySettings.h" #include "SWGRadioClockSettings.h" #include "SWGRadiosondeDemodSettings.h" @@ -177,6 +179,9 @@ public: SWGFT8DemodSettings* getFt8DemodSettings(); void setFt8DemodSettings(SWGFT8DemodSettings* ft8_demod_settings); + SWGRTTYDemodSettings* getRttyDemodSettings(); + void setRttyDemodSettings(SWGRTTYDemodSettings* rtty_demod_settings); + SWGHeatMapSettings* getHeatMapSettings(); void setHeatMapSettings(SWGHeatMapSettings* heat_map_settings); @@ -192,6 +197,9 @@ public: SWGM17ModSettings* getM17ModSettings(); void setM17ModSettings(SWGM17ModSettings* m17_mod_settings); + SWGNavtexDemodSettings* getNavtexDemodSettings(); + void setNavtexDemodSettings(SWGNavtexDemodSettings* navtex_demod_settings); + SWGNFMDemodSettings* getNfmDemodSettings(); void setNfmDemodSettings(SWGNFMDemodSettings* nfm_demod_settings); @@ -346,6 +354,9 @@ private: SWGFT8DemodSettings* ft8_demod_settings; bool m_ft8_demod_settings_isSet; + SWGRTTYDemodSettings* rtty_demod_settings; + bool m_rtty_demod_settings_isSet; + SWGHeatMapSettings* heat_map_settings; bool m_heat_map_settings_isSet; @@ -361,6 +372,9 @@ private: SWGM17ModSettings* m17_mod_settings; bool m_m17_mod_settings_isSet; + SWGNavtexDemodSettings* navtex_demod_settings; + bool m_navtex_demod_settings_isSet; + SWGNFMDemodSettings* nfm_demod_settings; bool m_nfm_demod_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index a6def3911..0d5a400c5 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -205,6 +205,8 @@ #include "SWGNFMModReport.h" #include "SWGNFMModSettings.h" #include "SWGNamedEnum.h" +#include "SWGNavtexDemodReport.h" +#include "SWGNavtexDemodSettings.h" #include "SWGNoiseFigureReport.h" #include "SWGNoiseFigureSettings.h" #include "SWGPERTesterActions.h" @@ -238,6 +240,8 @@ #include "SWGPresets.h" #include "SWGRDSReport.h" #include "SWGRDSReport_altFrequencies.h" +#include "SWGRTTYDemodReport.h" +#include "SWGRTTYDemodSettings.h" #include "SWGRadioAstronomyActions.h" #include "SWGRadioAstronomyReport.h" #include "SWGRadioAstronomySettings.h" @@ -1302,6 +1306,16 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGNavtexDemodReport").compare(type) == 0) { + SWGNavtexDemodReport *obj = new SWGNavtexDemodReport(); + obj->init(); + return obj; + } + if(QString("SWGNavtexDemodSettings").compare(type) == 0) { + SWGNavtexDemodSettings *obj = new SWGNavtexDemodSettings(); + obj->init(); + return obj; + } if(QString("SWGNoiseFigureReport").compare(type) == 0) { SWGNoiseFigureReport *obj = new SWGNoiseFigureReport(); obj->init(); @@ -1467,6 +1481,16 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGRTTYDemodReport").compare(type) == 0) { + SWGRTTYDemodReport *obj = new SWGRTTYDemodReport(); + obj->init(); + return obj; + } + if(QString("SWGRTTYDemodSettings").compare(type) == 0) { + SWGRTTYDemodSettings *obj = new SWGRTTYDemodSettings(); + obj->init(); + return obj; + } if(QString("SWGRadioAstronomyActions").compare(type) == 0) { SWGRadioAstronomyActions *obj = new SWGRadioAstronomyActions(); obj->init(); diff --git a/swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.cpp new file mode 100644 index 000000000..0a9e03f20 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.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 "SWGNavtexDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGNavtexDemodReport::SWGNavtexDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGNavtexDemodReport::SWGNavtexDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGNavtexDemodReport::~SWGNavtexDemodReport() { + this->cleanup(); +} + +void +SWGNavtexDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGNavtexDemodReport::cleanup() { + + +} + +SWGNavtexDemodReport* +SWGNavtexDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGNavtexDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGNavtexDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGNavtexDemodReport::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 +SWGNavtexDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGNavtexDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGNavtexDemodReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGNavtexDemodReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGNavtexDemodReport::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/SWGNavtexDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.h new file mode 100644 index 000000000..6265e9c6f --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodReport.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. + */ + +/* + * SWGNavtexDemodReport.h + * + * ACARSDemod + */ + +#ifndef SWGNavtexDemodReport_H_ +#define SWGNavtexDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGNavtexDemodReport: public SWGObject { +public: + SWGNavtexDemodReport(); + SWGNavtexDemodReport(QString* json); + virtual ~SWGNavtexDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGNavtexDemodReport* 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 /* SWGNavtexDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.cpp new file mode 100644 index 000000000..dd66a0189 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.cpp @@ -0,0 +1,586 @@ +/** + * 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 "SWGNavtexDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGNavtexDemodSettings::SWGNavtexDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGNavtexDemodSettings::SWGNavtexDemodSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + nav_area = 0; + m_nav_area_isSet = false; + filter_station = nullptr; + m_filter_station_isSet = false; + filter_type = nullptr; + m_filter_type_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; +} + +SWGNavtexDemodSettings::~SWGNavtexDemodSettings() { + this->cleanup(); +} + +void +SWGNavtexDemodSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + nav_area = 0; + m_nav_area_isSet = false; + filter_station = new QString(""); + m_filter_station_isSet = false; + filter_type = new QString(""); + m_filter_type_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 +SWGNavtexDemodSettings::cleanup() { + + + + if(filter_station != nullptr) { + delete filter_station; + } + if(filter_type != nullptr) { + delete filter_type; + } + + 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; + } +} + +SWGNavtexDemodSettings* +SWGNavtexDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGNavtexDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&nav_area, pJson["navArea"], "qint32", ""); + + ::SWGSDRangel::setValue(&filter_station, pJson["filterStation"], "QString", "QString"); + + ::SWGSDRangel::setValue(&filter_type, pJson["filterType"], "QString", "QString"); + + ::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 +SWGNavtexDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGNavtexDemodSettings::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_nav_area_isSet){ + obj->insert("navArea", QJsonValue(nav_area)); + } + if(filter_station != nullptr && *filter_station != QString("")){ + toJsonValue(QString("filterStation"), filter_station, obj, QString("QString")); + } + if(filter_type != nullptr && *filter_type != QString("")){ + toJsonValue(QString("filterType"), filter_type, obj, QString("QString")); + } + 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 +SWGNavtexDemodSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGNavtexDemodSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGNavtexDemodSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGNavtexDemodSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getNavArea() { + return nav_area; +} +void +SWGNavtexDemodSettings::setNavArea(qint32 nav_area) { + this->nav_area = nav_area; + this->m_nav_area_isSet = true; +} + +QString* +SWGNavtexDemodSettings::getFilterStation() { + return filter_station; +} +void +SWGNavtexDemodSettings::setFilterStation(QString* filter_station) { + this->filter_station = filter_station; + this->m_filter_station_isSet = true; +} + +QString* +SWGNavtexDemodSettings::getFilterType() { + return filter_type; +} +void +SWGNavtexDemodSettings::setFilterType(QString* filter_type) { + this->filter_type = filter_type; + this->m_filter_type_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGNavtexDemodSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGNavtexDemodSettings::getUdpAddress() { + return udp_address; +} +void +SWGNavtexDemodSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getUdpPort() { + return udp_port; +} +void +SWGNavtexDemodSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + +QString* +SWGNavtexDemodSettings::getLogFilename() { + return log_filename; +} +void +SWGNavtexDemodSettings::setLogFilename(QString* log_filename) { + this->log_filename = log_filename; + this->m_log_filename_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getLogEnabled() { + return log_enabled; +} +void +SWGNavtexDemodSettings::setLogEnabled(qint32 log_enabled) { + this->log_enabled = log_enabled; + this->m_log_enabled_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGNavtexDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGNavtexDemodSettings::getTitle() { + return title; +} +void +SWGNavtexDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGNavtexDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGNavtexDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGNavtexDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGNavtexDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGNavtexDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGNavtexDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGNavtexDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGNavtexDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +SWGGLScope* +SWGNavtexDemodSettings::getScopeConfig() { + return scope_config; +} +void +SWGNavtexDemodSettings::setScopeConfig(SWGGLScope* scope_config) { + this->scope_config = scope_config; + this->m_scope_config_isSet = true; +} + +SWGChannelMarker* +SWGNavtexDemodSettings::getChannelMarker() { + return channel_marker; +} +void +SWGNavtexDemodSettings::setChannelMarker(SWGChannelMarker* channel_marker) { + this->channel_marker = channel_marker; + this->m_channel_marker_isSet = true; +} + +SWGRollupState* +SWGNavtexDemodSettings::getRollupState() { + return rollup_state; +} +void +SWGNavtexDemodSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGNavtexDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_nav_area_isSet){ + isObjectUpdated = true; break; + } + if(filter_station && *filter_station != QString("")){ + isObjectUpdated = true; break; + } + if(filter_type && *filter_type != QString("")){ + 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/SWGNavtexDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.h new file mode 100644 index 000000000..ab870a93d --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGNavtexDemodSettings.h @@ -0,0 +1,182 @@ +/** + * 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. + */ + +/* + * SWGNavtexDemodSettings.h + * + * ACARSDemod + */ + +#ifndef SWGNavtexDemodSettings_H_ +#define SWGNavtexDemodSettings_H_ + +#include + + +#include "SWGChannelMarker.h" +#include "SWGGLScope.h" +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGNavtexDemodSettings: public SWGObject { +public: + SWGNavtexDemodSettings(); + SWGNavtexDemodSettings(QString* json); + virtual ~SWGNavtexDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGNavtexDemodSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + qint32 getNavArea(); + void setNavArea(qint32 nav_area); + + QString* getFilterStation(); + void setFilterStation(QString* filter_station); + + QString* getFilterType(); + void setFilterType(QString* filter_type); + + 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 nav_area; + bool m_nav_area_isSet; + + QString* filter_station; + bool m_filter_station_isSet; + + QString* filter_type; + bool m_filter_type_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 /* SWGNavtexDemodSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.h new file mode 100644 index 000000000..66e25b9fc --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.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. + */ + +/* + * SWGRTTYDemodReport.h + * + * ACARSDemod + */ + +#ifndef SWGRTTYDemodReport_H_ +#define SWGRTTYDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGRTTYDemodReport: public SWGObject { +public: + SWGRTTYDemodReport(); + SWGRTTYDemodReport(QString* json); + virtual ~SWGRTTYDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGRTTYDemodReport* 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 /* SWGRTTYDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.cpp new file mode 100644 index 000000000..34281bf47 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.cpp @@ -0,0 +1,697 @@ +/** + * 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 "SWGRTTYDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGRTTYDemodSettings::SWGRTTYDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGRTTYDemodSettings::SWGRTTYDemodSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + baud_rate = 0.0f; + m_baud_rate_isSet = false; + frequency_shift = 0; + m_frequency_shift_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; + character_set = 0; + m_character_set_isSet = false; + suppress_crlf = 0; + m_suppress_crlf_isSet = false; + unshift_on_space = 0; + m_unshift_on_space_isSet = false; + msb_first = 0; + m_msb_first_isSet = false; + space_high = 0; + m_space_high_isSet = false; + squelch = 0; + m_squelch_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; +} + +SWGRTTYDemodSettings::~SWGRTTYDemodSettings() { + this->cleanup(); +} + +void +SWGRTTYDemodSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + baud_rate = 0.0f; + m_baud_rate_isSet = false; + frequency_shift = 0; + m_frequency_shift_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; + character_set = 0; + m_character_set_isSet = false; + suppress_crlf = 0; + m_suppress_crlf_isSet = false; + unshift_on_space = 0; + m_unshift_on_space_isSet = false; + msb_first = 0; + m_msb_first_isSet = false; + space_high = 0; + m_space_high_isSet = false; + squelch = 0; + m_squelch_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 +SWGRTTYDemodSettings::cleanup() { + + + + + + 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; + } +} + +SWGRTTYDemodSettings* +SWGRTTYDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGRTTYDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&baud_rate, pJson["baudRate"], "float", ""); + + ::SWGSDRangel::setValue(&frequency_shift, pJson["frequencyShift"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_enabled, pJson["udpEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_address, pJson["udpAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&character_set, pJson["characterSet"], "qint32", ""); + + ::SWGSDRangel::setValue(&suppress_crlf, pJson["suppressCRLF"], "qint32", ""); + + ::SWGSDRangel::setValue(&unshift_on_space, pJson["unshiftOnSpace"], "qint32", ""); + + ::SWGSDRangel::setValue(&msb_first, pJson["msbFirst"], "qint32", ""); + + ::SWGSDRangel::setValue(&space_high, pJson["spaceHigh"], "qint32", ""); + + ::SWGSDRangel::setValue(&squelch, pJson["squelch"], "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 +SWGRTTYDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGRTTYDemodSettings::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_baud_rate_isSet){ + obj->insert("baudRate", QJsonValue(baud_rate)); + } + if(m_frequency_shift_isSet){ + obj->insert("frequencyShift", QJsonValue(frequency_shift)); + } + 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(m_character_set_isSet){ + obj->insert("characterSet", QJsonValue(character_set)); + } + if(m_suppress_crlf_isSet){ + obj->insert("suppressCRLF", QJsonValue(suppress_crlf)); + } + if(m_unshift_on_space_isSet){ + obj->insert("unshiftOnSpace", QJsonValue(unshift_on_space)); + } + if(m_msb_first_isSet){ + obj->insert("msbFirst", QJsonValue(msb_first)); + } + if(m_space_high_isSet){ + obj->insert("spaceHigh", QJsonValue(space_high)); + } + if(m_squelch_isSet){ + obj->insert("squelch", QJsonValue(squelch)); + } + 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 +SWGRTTYDemodSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGRTTYDemodSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGRTTYDemodSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGRTTYDemodSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +float +SWGRTTYDemodSettings::getBaudRate() { + return baud_rate; +} +void +SWGRTTYDemodSettings::setBaudRate(float baud_rate) { + this->baud_rate = baud_rate; + this->m_baud_rate_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getFrequencyShift() { + return frequency_shift; +} +void +SWGRTTYDemodSettings::setFrequencyShift(qint32 frequency_shift) { + this->frequency_shift = frequency_shift; + this->m_frequency_shift_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGRTTYDemodSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGRTTYDemodSettings::getUdpAddress() { + return udp_address; +} +void +SWGRTTYDemodSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getUdpPort() { + return udp_port; +} +void +SWGRTTYDemodSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getCharacterSet() { + return character_set; +} +void +SWGRTTYDemodSettings::setCharacterSet(qint32 character_set) { + this->character_set = character_set; + this->m_character_set_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getSuppressCrlf() { + return suppress_crlf; +} +void +SWGRTTYDemodSettings::setSuppressCrlf(qint32 suppress_crlf) { + this->suppress_crlf = suppress_crlf; + this->m_suppress_crlf_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getUnshiftOnSpace() { + return unshift_on_space; +} +void +SWGRTTYDemodSettings::setUnshiftOnSpace(qint32 unshift_on_space) { + this->unshift_on_space = unshift_on_space; + this->m_unshift_on_space_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getMsbFirst() { + return msb_first; +} +void +SWGRTTYDemodSettings::setMsbFirst(qint32 msb_first) { + this->msb_first = msb_first; + this->m_msb_first_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getSpaceHigh() { + return space_high; +} +void +SWGRTTYDemodSettings::setSpaceHigh(qint32 space_high) { + this->space_high = space_high; + this->m_space_high_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getSquelch() { + return squelch; +} +void +SWGRTTYDemodSettings::setSquelch(qint32 squelch) { + this->squelch = squelch; + this->m_squelch_isSet = true; +} + +QString* +SWGRTTYDemodSettings::getLogFilename() { + return log_filename; +} +void +SWGRTTYDemodSettings::setLogFilename(QString* log_filename) { + this->log_filename = log_filename; + this->m_log_filename_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getLogEnabled() { + return log_enabled; +} +void +SWGRTTYDemodSettings::setLogEnabled(qint32 log_enabled) { + this->log_enabled = log_enabled; + this->m_log_enabled_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGRTTYDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGRTTYDemodSettings::getTitle() { + return title; +} +void +SWGRTTYDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGRTTYDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGRTTYDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGRTTYDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGRTTYDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGRTTYDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGRTTYDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGRTTYDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGRTTYDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +SWGGLScope* +SWGRTTYDemodSettings::getScopeConfig() { + return scope_config; +} +void +SWGRTTYDemodSettings::setScopeConfig(SWGGLScope* scope_config) { + this->scope_config = scope_config; + this->m_scope_config_isSet = true; +} + +SWGChannelMarker* +SWGRTTYDemodSettings::getChannelMarker() { + return channel_marker; +} +void +SWGRTTYDemodSettings::setChannelMarker(SWGChannelMarker* channel_marker) { + this->channel_marker = channel_marker; + this->m_channel_marker_isSet = true; +} + +SWGRollupState* +SWGRTTYDemodSettings::getRollupState() { + return rollup_state; +} +void +SWGRTTYDemodSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGRTTYDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_baud_rate_isSet){ + isObjectUpdated = true; break; + } + if(m_frequency_shift_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(m_character_set_isSet){ + isObjectUpdated = true; break; + } + if(m_suppress_crlf_isSet){ + isObjectUpdated = true; break; + } + if(m_unshift_on_space_isSet){ + isObjectUpdated = true; break; + } + if(m_msb_first_isSet){ + isObjectUpdated = true; break; + } + if(m_space_high_isSet){ + isObjectUpdated = true; break; + } + if(m_squelch_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; +} +} + From 0c05e6dee28cbe5c4c7cfd3cee9e36dc80485a9c Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:29:22 +0000 Subject: [PATCH 03/12] Add methods to print filter taps as Matlab vectors --- sdrbase/dsp/firfilter.h | 14 ++++++++++++++ sdrbase/dsp/raisedcosine.h | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/sdrbase/dsp/firfilter.h b/sdrbase/dsp/firfilter.h index 662a838fa..9db921579 100644 --- a/sdrbase/dsp/firfilter.h +++ b/sdrbase/dsp/firfilter.h @@ -19,6 +19,7 @@ #pragma once #include +#include #include "dsp/dsptypes.h" #include "dsp/misc.h" #include "export.h" @@ -57,6 +58,19 @@ public: return acc; } + // Print taps as a Matlab vector + void printTaps(const char *name) + { + printf("%s = [", name); + for (int i = 0; i <= m_taps.size() - 1; ++i) { + printf("%g ", m_taps[i]); + } + for (int i = m_taps.size() - 2; i >= 0; --i) { + printf("%g ", m_taps[i]); + } + printf("];\n"); + } + protected: void init(int nTaps) { diff --git a/sdrbase/dsp/raisedcosine.h b/sdrbase/dsp/raisedcosine.h index 8d2d63088..630ef2ebb 100644 --- a/sdrbase/dsp/raisedcosine.h +++ b/sdrbase/dsp/raisedcosine.h @@ -20,6 +20,7 @@ #define INCLUDE_RAISEDCOSINE_H #include +#include #include "dsp/dsptypes.h" // Raised-cosine low-pass filter for pulse shaping, without intersymbol interference (ISI) @@ -131,6 +132,19 @@ public: return acc; } + // Print taps as a Matlab vector + void printTaps(const char *name) + { + printf("%s = [", name); + for (int i = 0; i <= m_taps.size() - 1; ++i) { + printf("%g ", m_taps[i]); + } + for (int i = m_taps.size() - 2; i >= 0; --i) { + printf("%g ", m_taps[i]); + } + printf("];\n"); + } + private: std::vector m_taps; std::vector m_samples; From b3224e3aa85388d18bf260aadd449d2c3ccb8ced Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:37:38 +0000 Subject: [PATCH 04/12] Update CMakeLists for RTTY and Navtex demods --- CMakeLists.txt | 2 ++ plugins/channelrx/CMakeLists.txt | 8 ++++++++ sdrbase/CMakeLists.txt | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index d0c525d6a..06a5e838d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,8 @@ option(ENABLE_CHANNELRX_DEMODPACKET "Enable channelrx demodpacket plugin" ON) option(ENABLE_CHANNELRX_DEMODAPT "Enable channelrx demodapt plugin" ON) 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) # Channel Tx enablers option(ENABLE_CHANNELTX "Enable channeltx plugins" ON) diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index e0350abc5..9270f0fee 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -113,6 +113,14 @@ if (ENABLE_CHANNELRX_DEMODFT8 AND FT8_SUPPORT) add_subdirectory(demodft8) endif() +if (ENABLE_CHANNELRX_DEMODNAVTEX) + add_subdirectory(demodnavtex) +endif() + +if (ENABLE_CHANNELRX_DEMODRTTY) + add_subdirectory(demodrtty) +endif() + if(NOT SERVER_MODE) add_subdirectory(heatmap) diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 12fd1f08f..50c2efdde 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -176,6 +176,7 @@ set(sdrbase_SOURCES util/aprs.cpp util/astronomy.cpp util/azel.cpp + util/baudot.cpp util/colormap.cpp util/coordinates.cpp util/crc.cpp @@ -195,6 +196,7 @@ set(sdrbase_SOURCES util/message.cpp util/messagequeue.cpp util/morse.cpp + util/navtex.cpp util/openaip.cpp util/osndb.cpp util/ourairportsdb.cpp @@ -399,6 +401,7 @@ set(sdrbase_HEADERS util/aprs.h util/astronomy.h util/azel.h + util/baudot.h util/colormap.h util/coordinates.h util/CRC64.h @@ -423,6 +426,8 @@ set(sdrbase_HEADERS util/messagequeue.h util/morse.h util/movingaverage.h + util/movingmaximum.h + util/navtex.h util/openaip.h util/osndb.h util/outairportsdb.h From 6a0a7e0bd90a15263f22979b6eb338b0a78a6baf Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:39:19 +0000 Subject: [PATCH 05/12] Add RTTY Swagger files --- .../code/qt5/client/SWGRTTYDemodReport.cpp | 131 +++++++++++ .../code/qt5/client/SWGRTTYDemodSettings.h | 212 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.h diff --git a/swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.cpp new file mode 100644 index 000000000..f4d0e0862 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodReport.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 "SWGRTTYDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGRTTYDemodReport::SWGRTTYDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGRTTYDemodReport::SWGRTTYDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGRTTYDemodReport::~SWGRTTYDemodReport() { + this->cleanup(); +} + +void +SWGRTTYDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGRTTYDemodReport::cleanup() { + + +} + +SWGRTTYDemodReport* +SWGRTTYDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGRTTYDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGRTTYDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGRTTYDemodReport::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 +SWGRTTYDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGRTTYDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGRTTYDemodReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGRTTYDemodReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGRTTYDemodReport::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/SWGRTTYDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.h new file mode 100644 index 000000000..8d1f4e92f --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGRTTYDemodSettings.h @@ -0,0 +1,212 @@ +/** + * 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. + */ + +/* + * SWGRTTYDemodSettings.h + * + * ACARSDemod + */ + +#ifndef SWGRTTYDemodSettings_H_ +#define SWGRTTYDemodSettings_H_ + +#include + + +#include "SWGChannelMarker.h" +#include "SWGGLScope.h" +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGRTTYDemodSettings: public SWGObject { +public: + SWGRTTYDemodSettings(); + SWGRTTYDemodSettings(QString* json); + virtual ~SWGRTTYDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGRTTYDemodSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + float getBaudRate(); + void setBaudRate(float baud_rate); + + qint32 getFrequencyShift(); + void setFrequencyShift(qint32 frequency_shift); + + qint32 getUdpEnabled(); + void setUdpEnabled(qint32 udp_enabled); + + QString* getUdpAddress(); + void setUdpAddress(QString* udp_address); + + qint32 getUdpPort(); + void setUdpPort(qint32 udp_port); + + qint32 getCharacterSet(); + void setCharacterSet(qint32 character_set); + + qint32 getSuppressCrlf(); + void setSuppressCrlf(qint32 suppress_crlf); + + qint32 getUnshiftOnSpace(); + void setUnshiftOnSpace(qint32 unshift_on_space); + + qint32 getMsbFirst(); + void setMsbFirst(qint32 msb_first); + + qint32 getSpaceHigh(); + void setSpaceHigh(qint32 space_high); + + qint32 getSquelch(); + void setSquelch(qint32 squelch); + + 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; + + float baud_rate; + bool m_baud_rate_isSet; + + qint32 frequency_shift; + bool m_frequency_shift_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; + + qint32 character_set; + bool m_character_set_isSet; + + qint32 suppress_crlf; + bool m_suppress_crlf_isSet; + + qint32 unshift_on_space; + bool m_unshift_on_space_isSet; + + qint32 msb_first; + bool m_msb_first_isSet; + + qint32 space_high; + bool m_space_high_isSet; + + qint32 squelch; + bool m_squelch_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 /* SWGRTTYDemodSettings_H_ */ From bc29c5105d5936caac6ca56bab7f79e1600ef322 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:46:03 +0000 Subject: [PATCH 06/12] Fix gcc warnings --- sdrbase/util/baudot.cpp | 2 +- sdrbase/util/navtex.cpp | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sdrbase/util/baudot.cpp b/sdrbase/util/baudot.cpp index 29bd0938d..ed7b95eb2 100644 --- a/sdrbase/util/baudot.cpp +++ b/sdrbase/util/baudot.cpp @@ -164,7 +164,7 @@ void BaudotDecoder::init() QString BaudotDecoder::decode(char bits) { - QString c = m_figure ? m_figures[bits] : m_letters[bits]; + QString c = m_figure ? m_figures[(int)bits] : m_letters[(int)bits]; if ((c == '>') || (m_unshiftOnSpace && (c == " "))) { diff --git a/sdrbase/util/navtex.cpp b/sdrbase/util/navtex.cpp index 3d605a9e2..37dd9600a 100644 --- a/sdrbase/util/navtex.cpp +++ b/sdrbase/util/navtex.cpp @@ -270,16 +270,15 @@ const NavtexTransmitter* NavtexTransmitter::getTransmitter(QTime time, int area, } NavtexMessage::NavtexMessage(QDateTime dateTime, const QString& stationId, const QString& typeId, const QString& id, const QString& message) : - m_dateTime(dateTime), m_stationId(stationId), m_typeId(typeId), m_id(id), m_message(message), + m_dateTime(dateTime), m_valid(true) { } - NavtexMessage::NavtexMessage(const QString& text) { m_dateTime = QDateTime::currentDateTime(); @@ -448,7 +447,7 @@ QString SitorBDecoder::printable(char c) } else if (c == 0x7) { return "Bell"; } else { - return c; + return QString("%1").arg(c); } } @@ -721,9 +720,9 @@ const char SitorBDecoder::m_ccir476FigureSetDecode[128] = { char SitorBDecoder::ccir476Decode(char c) { if (m_figureSet) { - return m_ccir476FigureSetDecode[c]; + return m_ccir476FigureSetDecode[(int)c]; } else { - return m_ccir476LetterSetDecode[c]; + return m_ccir476LetterSetDecode[(int)c]; } } From 3ca1d1cb1120544f9f7f8eddf19ee5c8a593cd0e Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 16:54:09 +0000 Subject: [PATCH 07/12] Fix gcc warnings --- plugins/channelrx/demodrtty/rttydemodsink.cpp | 2 +- plugins/channelrx/demodrtty/rttydemodsink.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/channelrx/demodrtty/rttydemodsink.cpp b/plugins/channelrx/demodrtty/rttydemodsink.cpp index d4792f0f8..b9d094620 100644 --- a/plugins/channelrx/demodrtty/rttydemodsink.cpp +++ b/plugins/channelrx/demodrtty/rttydemodsink.cpp @@ -41,12 +41,12 @@ RttyDemodSink::RttyDemodSink(RttyDemod *packetDemod) : m_prods1(nullptr), m_prods2(nullptr), m_exp(nullptr), + m_sampleIdx(0), m_clockHistogram(100), m_shiftEstMag(m_fftSize), m_fftSequence(-1), m_fft(nullptr), m_fftCounter(0), - m_sampleIdx(0), m_sampleBufferIndex(0) { m_magsq = 0.0; diff --git a/plugins/channelrx/demodrtty/rttydemodsink.h b/plugins/channelrx/demodrtty/rttydemodsink.h index e99e0397a..15e49d769 100644 --- a/plugins/channelrx/demodrtty/rttydemodsink.h +++ b/plugins/channelrx/demodrtty/rttydemodsink.h @@ -139,7 +139,7 @@ private: BaudotDecoder m_rttyDecoder; // For baud rate estimation - int m_cycleCount; + unsigned int m_cycleCount; std::vector m_clockHistogram; int m_edgeCount; MovingAverageUtil m_baudRateAverage; From 84e15c587fad9d46f7b3dba76c8848ab7ba86dd6 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 17:06:19 +0000 Subject: [PATCH 08/12] Fix gcc warnings --- plugins/channelrx/demodnavtex/navtexdemodgui.cpp | 4 ++++ plugins/channelrx/demodrtty/rttydemodgui.cpp | 2 ++ 2 files changed, 6 insertions(+) diff --git a/plugins/channelrx/demodnavtex/navtexdemodgui.cpp b/plugins/channelrx/demodnavtex/navtexdemodgui.cpp index 42ecf5e17..de33b0ba2 100644 --- a/plugins/channelrx/demodnavtex/navtexdemodgui.cpp +++ b/plugins/channelrx/demodnavtex/navtexdemodgui.cpp @@ -360,6 +360,8 @@ void NavtexDemodGUI::on_rfBW_valueChanged(int value) void NavtexDemodGUI::on_filterStation_currentIndexChanged(int index) { + (void) index; + m_settings.m_filterStation = ui->filterStation->currentText(); filter(); applySettings(); @@ -367,6 +369,8 @@ void NavtexDemodGUI::on_filterStation_currentIndexChanged(int index) void NavtexDemodGUI::on_filterType_currentIndexChanged(int index) { + (void) index; + m_settings.m_filterType = ui->filterType->currentText(); filter(); applySettings(); diff --git a/plugins/channelrx/demodrtty/rttydemodgui.cpp b/plugins/channelrx/demodrtty/rttydemodgui.cpp index 8785f3a9d..57fda37c2 100644 --- a/plugins/channelrx/demodrtty/rttydemodgui.cpp +++ b/plugins/channelrx/demodrtty/rttydemodgui.cpp @@ -248,6 +248,8 @@ void RttyDemodGUI::on_suppressCRLF_clicked(bool checked) void RttyDemodGUI::on_mode_currentIndexChanged(int index) { + (void) index; + QString mode = ui->mode->currentText(); bool custom = mode == "Custom"; From d9f27485c31377fc646ffd95441c47133e7a16ea Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 17:14:56 +0000 Subject: [PATCH 09/12] Add Navtex transmitters and French DAB transmitters to Map --- plugins/feature/map/data/transmitters.csv | 185 ++++++++++++++++++++++ plugins/feature/map/mapgui.cpp | 46 ++++++ plugins/feature/map/mapgui.h | 1 + plugins/feature/map/mapsettings.cpp | 2 + plugins/feature/map/readme.md | 8 +- sdrbase/util/csv.cpp | 8 +- sdrbase/util/csv.h | 4 +- sdrbase/util/units.h | 19 +++ 8 files changed, 264 insertions(+), 9 deletions(-) diff --git a/plugins/feature/map/data/transmitters.csv b/plugins/feature/map/data/transmitters.csv index 1640a2a93..fb5425b19 100644 --- a/plugins/feature/map/data/transmitters.csv +++ b/plugins/feature/map/data/transmitters.csv @@ -3476,3 +3476,188 @@ DAB,Wolv'ton & Shrop 3702,Wolv'ton & Shrop,218640000,52.878444,-3.091941,305,0.6 DAB,Wolv'ton & Shrop 3708,Wolv'ton & Shrop,218640000,52.547739,-2.115424,225,0.6,37,08 DAB,Wolv'ton & Shrop 3709,Wolv'ton & Shrop,218640000,52.670935,-2.550228,396,1.95447,37,09 DAB,Wolv'ton & Shrop 3711,Wolv'ton & Shrop,218640000,52.496364,-2.048417,267,4,37,11 +DAB,Marseille intermédiaire 1 2310,Marseille intermédiaire 1,195936000,43.3878,5.41167,66,10,17,0a +DAB,Marseille intermédiaire 1 2302,Marseille intermédiaire 1,195936000,43.5272,5.40194,25,3.5,17,02 +DAB,Marseille intermédiaire 2 2410,Marseille intermédiaire 2,188928000,43.3839,5.42556,148,10,18,0a +DAB,Nice intermédiaire 2 1704,Nice intermédiaire 2,220352000,43.7217,7.32083,56,6,11,04 +DAB,Nice local 2 0404,Nice local 2,208064000,43.7675,7.29556,30,12,04,04 +DAB,Paris intermédiaire 1 1210,Paris intermédiaire 1,181936000,48.8853,2.42222,144,8,0c,0a +DAB,Paris local 2 2610,Paris local 2,204640000,48.8853,2.42222,144,8,1a,0a +DAB,Paris local 1 2210,Paris local 1,202928000,48.8872,2.34167,48,4,16,0a +DAB,Marseille local 3 0710,Marseille local 3,201072000,43.3878,5.41167,66,10,07,0a +DAB,Marseille local 2 1110,Marseille local 2,199360000,43.3892,5.4125,45,4,0b,0a +DAB,Paris local 3 2710,Paris local 3,216928000,48.8853,2.42222,144,4,1b,0a +DAB,Nice étendu 1 3104,Nice étendu 1,216928000,43.7142,7.30722,18,10,1f,04 +DAB,Nice local 3 0604,Nice local 3,201072000,43.7217,7.32083,38,2,06,04 +DAB,Lille local 1 0510,Lille local 1,192352000,50.6556,3.03056,70,7,05,0a +DAB,Douai-Lens local 1 2701,Douai-Lens local 1,222064000,50.4336,2.6075,30,4,1b,01 +DAB,Lille étendu 1 0010,Lille étendu 1,195936000,50.6425,3.12389,143,4,00,0a +DAB,Calais-Boulogne-sur-Mer local 1 0102,Calais-Boulogne-sur-Mer local 1,176640000,50.9506,1.89083,63,3,01,02 +DAB,Dunkerque local 1 0303,Dunkerque local 1,201072000,51.0331,2.35889,55,3,03,03 +DAB,Valenciennes local 1 2204,Valenciennes local 1,188928000,50.3394,3.57389,46,8,16,04 +DAB,Lille local 2 0610,Lille local 2,194064000,50.6425,3.12389,143,4,06,0a +DAB,Marseille local 2 1101,Marseille local 2,199360000,43.3586,5.57444,22,2,0b,01 +DAB,Lyon étendu 1 1710,Lyon étendu 1,181936000,45.8225,4.82083,32,13,11,0a +DAB,Lyon étendu 1 1704,Lyon étendu 1,181936000,45.965,4.70361,19,2.1,11,04 +DAB,Strasbourg local 2 0910,Strasbourg local 2,192352000,48.5867,7.74111,90,2.5,09,0a +DAB,Strasbourg étendu 1 6710,Strasbourg étendu 1,187072000,48.5867,7.74111,90,4,43,0a +DAB,Strasbourg étendu 1 6701,Strasbourg étendu 1,187072000,48.0767,7.32667,40,1.2,43,01 +DAB,Strasbourg étendu 1 6702,Strasbourg étendu 1,187072000,47.7344,7.34556,15,2.5,43,02 +DAB,Colmar local 1 1801,Colmar local 1,220352000,48.0786,7.33444,56,1,12,01 +DAB,Mulhouse local 1 1302,Mulhouse local 1,222064000,47.7503,7.31472,55,4,0d,02 +DAB,Bourg-en-Bresse local 1 1601,Bourg-en-Bresse local 1,187072000,46.1917,5.33833,32,1.5,10,01 +DAB,Bourgoin-Jallieu local 1 2802,Bourgoin-Jallieu local 1,178352000,45.5911,5.36583,35,3,1c,02 +DAB,Strasbourg local 1 0510,Strasbourg local 1,185360000,48.5806,7.76583,54,4,05,0a +DAB,Lyon local 2 2504,Lyon local 2,176640000,45.965,4.70361,19,1.8,19,04 +DAB,Lyon local 2 2510,Lyon local 2,176640000,45.8225,4.82083,32,6,19,0a +DAB,Nantes local 1 1710,Nantes local 1,190640000,47.2447,-1.60833,125,6,11,0a +DAB,La Roche-sur-Yon local 1 1902,La Roche-sur-Yon local 1,202928000,46.6892,-1.43722,47,6,13,02 +DAB,Saint-Nazaire local 1 1401,Saint-Nazaire local 1,201072000,47.3253,-2.40639,63,3,0e,01 +DAB,Saint-Nazaire local 1 1403,Saint-Nazaire local 1,201072000,47.1914,-2.07667,32,3,0e,03 +DAB,Nantes étendu 1 0801,Nantes étendu 1,180064000,47.3253,-2.40639,63,2,08,01 +DAB,Nantes étendu 1 0810,Nantes étendu 1,180064000,47.2447,-1.60833,125,9,08,0a +DAB,Nantes local 2 2010,Nantes local 2,223936000,47.2236,-1.615,98,4,14,0a +DAB,Rouen étendu 1 1610,Rouen étendu 1,209936000,49.4428,1.03306,66,10,10,0a +DAB,Rouen étendu 1 1601,Rouen étendu 1,209936000,49.5047,0.139722,49,10,10,01 +DAB,Rouen local 1 0410,Rouen local 1,206352000,49.4472,1.13667,63,6,04,0a +DAB,Rouen local 2 2310,Rouen local 2,213360000,49.4472,1.13667,63,6,17,0a +DAB,Le Havre local 1 2901,Le Havre local 1,215072000,49.5072,0.0894444,30,4,1d,01 +DAB,Paris intermédiaire 2 1310,Paris intermédiaire 2,187072000,48.8853,2.42222,144,8,0d,0a +DAB,Mâcon local 1 1401,Mâcon local 1,195936000,46.3517,4.78333,32,1.8,0e,01 +DAB,Bordeaux étendu 1 1310,Bordeaux étendu 1,199360000,44.8203,-0.505556,163,6,0d,0a +DAB,Bordeaux étendu 1 1301,Bordeaux étendu 1,199360000,44.4328,0.07,37,4,0d,01 +DAB,Bordeaux local 1 3010,Bordeaux local 1,188928000,44.8719,-0.515278,78,10,1e,0a +DAB,Bordeaux local 1 3010,Bordeaux local 1,188928000,44.8614,-0.558611,35,2,1e,0a +DAB,Bordeaux local 2 3110,Bordeaux local 2,197648000,44.8203,-0.505556,163,4,1f,0a +DAB,Arcachon local 1 2501,Arcachon local 1,220352000,44.6469,-1.16361,91,4,19,01 +DAB,Toulouse étendu 1 1810,Toulouse étendu 1,183648000,43.5589,1.44639,27,10,12,0a +DAB,Toulouse étendu 1 0301,Toulouse étendu 1,183648000,43.9875,1.35917,34,5,03,01 +DAB,Toulouse local 1 3010,Toulouse local 1,192352000,43.6058,1.46722,20,6,1e,0a +DAB,Toulouse local 2 3110,Toulouse local 2,199360000,43.5589,1.44639,27,5,1f,0a +DAB,métropole métropolitain 1 6708,métropole métropolitain 1,188928000,45.3156,4.74611,15,2.6,43,08 +DAB,métropole métropolitain 1 6706,métropole métropolitain 1,188928000,44.9642,4.80083,25,10,43,06 +DAB,métropole métropolitain 1 2701,métropole métropolitain 1,192352000,43.3586,5.57444,27,2.6,1b,01 +DAB,métropole métropolitain 1 0604,métropole métropolitain 1,208064000,47.1456,4.66167,52,2.6,06,04 +DAB,métropole métropolitain 1 0601,métropole métropolitain 1,208064000,47.0347,4.81389,42,4,06,01 +DAB,métropole métropolitain 1 0610,métropole métropolitain 1,208064000,47.3003,4.99056,29,5,06,0a +DAB,métropole métropolitain 1 0605,métropole métropolitain 1,208064000,47.485,4.1575,32,1.7,06,05 +DAB,métropole métropolitain 1 0606,métropole métropolitain 1,208064000,47.335,4.81083,21,2.6,06,06 +DAB,métropole métropolitain 1 0607,métropole métropolitain 1,208064000,47.3469,4.45889,45,1.7,06,07 +DAB,métropole métropolitain 1 0407,métropole métropolitain 1,211648000,44.6131,4.77917,26,3.6,04,07 +DAB,métropole métropolitain 1 0402,métropole métropolitain 1,211648000,44.1814,4.66222,47,4,04,02 +DAB,métropole métropolitain 1 1404,métropole métropolitain 1,185360000,48.0333,3.00278,78,1.5,0e,04 +DAB,métropole métropolitain 1 1910,métropole métropolitain 1,188928000,45.8225,4.82083,34,13,13,0a +DAB,métropole métropolitain 1 6705,métropole métropolitain 1,188928000,45.5325,4.80861,45,3.4,43,05 +DAB,métropole métropolitain 1 0602,métropole métropolitain 1,208064000,46.7364,4.665,27,6.5,06,02 +DAB,métropole métropolitain 1 1903,métropole métropolitain 1,188928000,46.2817,4.68056,76,10,13,03 +DAB,métropole métropolitain 1 1101,métropole métropolitain 1,185360000,48.88,2.28389,167,10,0b,01 +DAB,métropole métropolitain 1 1110,métropole métropolitain 1,185360000,48.8675,2.41528,139,14,0b,0a +DAB,métropole métropolitain 1 1403,métropole métropolitain 1,185360000,48.1647,2.88139,38,1.8,0e,03 +DAB,métropole métropolitain 1 1410,métropole métropolitain 1,185360000,48.4267,2.71083,46,5.6,0e,0a +DAB,métropole métropolitain 1 6702,métropole métropolitain 1,194064000,48.2911,2.68194,28,1.7,43,02 +DAB,métropole métropolitain 1 0405,métropole métropolitain 1,211648000,43.97,4.85972,88,4,04,05 +DAB,métropole métropolitain 1 0608,métropole métropolitain 1,208064000,47.5394,3.89389,20,5,06,08 +DAB,métropole métropolitain 1 1010,métropole métropolitain 1,208064000,47.8406,3.66639,101,7,0a,0a +DAB,métropole métropolitain 1 1401,métropole métropolitain 1,185360000,48.6,2.44278,86,3,0e,01 +DAB,métropole métropolitain 1 1402,métropole métropolitain 1,185360000,48.4783,2.42417,93,6,0e,02 +DAB,métropole métropolitain 2 0808,métropole métropolitain 2,190640000,45.3156,4.74611,15,2.6,08,08 +DAB,métropole métropolitain 2 0806,métropole métropolitain 2,190640000,44.9642,4.80083,25,5,08,06 +DAB,métropole métropolitain 2 2807,métropole métropolitain 2,197648000,43.6319,5.09611,23,1.9,1c,07 +DAB,métropole métropolitain 2 2808,métropole métropolitain 2,197648000,43.5403,5.23833,14,2,1c,08 +DAB,métropole métropolitain 2 0010,métropole métropolitain 2,206352000,47.1456,4.66167,52,2.6,00,0a +DAB,métropole métropolitain 2 1310,métropole métropolitain 2,206352000,47.0347,4.81389,42,4,0d,0a +DAB,métropole métropolitain 2 0001,métropole métropolitain 2,206352000,47.485,4.1575,32,1.7,00,01 +DAB,métropole métropolitain 2 0002,métropole métropolitain 2,206352000,47.3469,4.45889,45,1.7,00,02 +DAB,métropole métropolitain 2 0607,métropole métropolitain 2,213360000,44.6131,4.77917,26,4,06,07 +DAB,métropole métropolitain 2 0602,métropole métropolitain 2,213360000,44.1814,4.66222,47,4,06,02 +DAB,métropole métropolitain 2 0504,métropole métropolitain 2,199360000,48.0333,3.00278,78,4,05,04 +DAB,métropole métropolitain 2 2010,métropole métropolitain 2,190640000,45.8225,4.82083,34,10,14,0a +DAB,métropole métropolitain 2 0805,métropole métropolitain 2,190640000,45.5325,4.80861,45,10,08,05 +DAB,métropole métropolitain 2 2004,métropole métropolitain 2,190640000,45.965,4.70361,19,10,14,04 +DAB,métropole métropolitain 2 1302,métropole métropolitain 2,206352000,46.7364,4.665,27,6.5,0d,02 +DAB,métropole métropolitain 2 0101,métropole métropolitain 2,199360000,46.3517,4.78333,32,5,01,01 +DAB,métropole métropolitain 2 1301,métropole métropolitain 2,206352000,46.5656,4.87639,29,5,0d,01 +DAB,métropole métropolitain 2 0501,métropole métropolitain 2,199360000,48.88,2.28389,167,10,05,01 +DAB,métropole métropolitain 2 0510,métropole métropolitain 2,199360000,48.8675,2.41528,139,14,05,0a +DAB,métropole métropolitain 2 0505,métropole métropolitain 2,199360000,48.1647,2.88139,38,14,05,05 +DAB,métropole métropolitain 2 0506,métropole métropolitain 2,199360000,48.4286,2.54028,36,1.7,05,06 +DAB,métropole métropolitain 2 0507,métropole métropolitain 2,199360000,48.2911,2.68194,28,1.7,05,07 +DAB,métropole métropolitain 2 0605,métropole métropolitain 2,213360000,43.97,4.85972,88,4.2,06,05 +DAB,métropole métropolitain 2 0606,métropole métropolitain 2,213360000,43.8036,5.04694,39,4.2,06,06 +DAB,métropole métropolitain 2 0003,métropole métropolitain 2,206352000,47.5394,3.89389,20,2.6,00,03 +DAB,métropole métropolitain 2 0004,métropole métropolitain 2,206352000,47.8406,3.66639,101,7,00,04 +DAB,métropole métropolitain 2 0503,métropole métropolitain 2,199360000,48.6286,2.42806,52,7,05,03 +DAB,Dijon étendu 1 0410,Dijon étendu 1,204640000,47.3003,4.99056,29,7.5,04,0a +DAB,Dijon étendu 1 0402,Dijon étendu 1,204640000,46.7364,4.665,27,6.5,04,02 +DAB,Avignon étendu 1 2505,Avignon étendu 1,208064000,43.97,4.85972,88,7.7,19,05 +DAB,Avignon étendu 1 2502,Avignon étendu 1,208064000,44.1814,4.66222,47,6,19,02 +DAB,Paris étendu 1 0701,Paris étendu 1,218640000,48.88,2.28389,167,10,07,01 +DAB,Paris étendu 1 0710,Paris étendu 1,218640000,48.8675,2.41528,139,14,07,0a +DAB,Avignon local 1 3605,Avignon local 1,178352000,43.97,4.85972,88,4.6,24,05 +DAB,Dijon local 1 6710,Dijon local 1,218640000,47.315,4.98667,27,7.7,43,0a +DAB,Lille étendu 1 0001,Lille étendu 1,195936000,50.4186,2.65944,74,10.5,00,01 +DAB,Lyon local 1 2410,Lyon local 1,218640000,45.8181,4.90667,56,7,18,0a +DAB,Marseille étendu 1 3210,Marseille étendu 1,176640000,43.3839,5.42556,106,10,20,0a +DAB,Marseille étendu 1 3201,Marseille étendu 1,176640000,43.2747,5.30833,27,4,20,01 +DAB,Paris intermédiaire 2 1301,Paris intermédiaire 2,187072000,48.8025,2.20444,103,10,0d,01 +DAB,Nice intermédiaire 1 1004,Nice intermédiaire 1,218640000,43.7217,7.32083,53,8,0a,04 +DAB,Nice intermédiaire 1 1005,Nice intermédiaire 1,218640000,43.5783,7.03556,45,4,0a,05 +DAB,métropole métropolitain 1 2710,métropole métropolitain 1,192352000,43.3892,5.4125,71,20,1b,0a +DAB,Toulon local 1 0010,Toulon local 1,220352000,43.0531,5.84556,72,4,00,0a +DAB,Toulon étendu 1 2610,Toulon étendu 1,202928000,43.0531,5.84556,72,10,1a,0a +DAB,Toulon étendu 1 2601,Toulon étendu 1,202928000,43.3686,5.99194,49,4.8,1a,01 +DAB,Toulon étendu 1 2602,Toulon étendu 1,202928000,43.2814,6.29417,70,4.8,1a,02 +DAB,Toulon étendu 1 2603,Toulon étendu 1,202928000,43.1033,6.34889,22,5,1a,03 +DAB,métropole métropolitain 2 2810,métropole métropolitain 2,197648000,43.3892,5.4125,71,20,1c,0a +DAB,Orléans local 1 2310,Orléans local 1,178352000,47.9433,1.9275,89,6.4,17,0a +DAB,Poitiers local 1 1410,Poitiers local 1,192352000,46.5628,0.348889,74,8.2,0e,0a +DAB,Tours local 1 2610,Tours local 1,204640000,47.4144,0.684722,52,9.4,1a,0a +DAB,Saint-Étienne étendu 1 0510,Saint-Étienne étendu 1,185360000,45.4006,4.38778,62,10,05,0a +DAB,Saint-Étienne étendu 1 0501,Saint-Étienne étendu 1,185360000,45.9844,3.91944,21,5,05,01 +DAB,Nice intermédiaire 2 1705,Nice intermédiaire 2,220352000,43.5783,7.03556,46,4,11,05 +DAB,Nice intermédiaire 2 1706,Nice intermédiaire 2,220352000,43.6611,6.9175,17,3.5,11,06 +DAB,Nice local 2 0405,Nice local 2,208064000,43.5783,7.03556,46,4,04,05 +DAB,Nice local 2 0404,Nice local 2,208064000,43.7217,7.32083,54,5,04,04 +DAB,Paris intermédiaire 1 1201,Paris intermédiaire 1,181936000,48.8025,2.20444,103,10,0c,01 +DAB,Lille local 1 0505,Lille local 1,192352000,50.6897,3.1825,78,0.4,05,05 +DAB,Annecy étendu 1 1801,Annecy étendu 1,192352000,45.9164,6.17222,32,4,12,01 +DAB,Annecy étendu 1 1002,Annecy étendu 1,192352000,46.1453,6.18861,24,4,0a,02 +DAB,Annecy étendu 1 1803,Annecy étendu 1,192352000,45.66,5.82139,32,4,12,03 +DAB,Annemasse local 1 3402,Annemasse local 1,194064000,46.1681,6.21806,23,3,22,02 +DAB,Chambéry local 1 2103,Chambéry local 1,187072000,45.6633,5.82222,18,5,15,03 +DAB,Grenoble local 1 2601,Grenoble local 1,206352000,45.1508,5.665,37,6.7,1a,01 +DAB,Grenoble local 1 2602,Grenoble local 1,206352000,45.2603,5.54611,33,3.5,1a,02 +DAB,Saint-Étienne local 1 3610,Saint-Étienne local 1,195936000,45.4,4.39389,22,8.6,24,0a +DAB,Orléans étendu 1 0810,Orléans étendu 1,209936000,47.9433,1.9275,88,9,08,0a +DAB,Orléans étendu 1 0801,Orléans étendu 1,209936000,47.6083,1.30306,58,3,08,01 +DAB,Orléans étendu 1 0802,Orléans étendu 1,209936000,47.9983,2.74194,49,4,08,02 +DAB,Poitiers étendu 1 0110,Poitiers étendu 1,206352000,46.5914,0.3475,61,6,01,0a +DAB,Poitiers étendu 1 0101,Poitiers étendu 1,206352000,46.9047,0.526389,70,5.5,01,01 +DAB,Poitiers étendu 1 0102,Poitiers étendu 1,206352000,46.3489,-0.430556,82,4,01,02 +DAB,Tours étendu 1 2710,Tours étendu 1,185360000,47.4058,0.724167,36,10,1b,0a +DAB,Tours étendu 1 2701,Tours étendu 1,185360000,46.9644,0.684444,27,1.7,1b,01 +DAB,Tours étendu 1 2702,Tours étendu 1,185360000,47.1361,0.228333,20,2.6,1b,02 +DAB,Annecy local 1 3101,Annecy local 1,222064000,45.9161,6.16944,25,8,1f,01 +DAB,Grenoble étendu 1 0301,Grenoble étendu 1,180064000,45.1508,5.665,37,6.7,03,01 +DAB,Grenoble étendu 1 0302,Grenoble étendu 1,180064000,45.2603,5.54611,33,3.5,03,02 +DAB,Grenoble étendu 1 0303,Grenoble étendu 1,180064000,45.5578,5.45361,12,2.6,03,03 +DAB,Valenciennes local 1 2206,Valenciennes local 1,188928000,50.2675,3.92194,46,9.5,16,06 +DAB,Nice étendu 1 3105,Nice étendu 1,216928000,43.5694,7.035,48,3.5,1f,05 +DAB,Nice étendu 1 3106,Nice étendu 1,216928000,43.4328,6.81167,24,4,1f,06 +DAB,Besançon local 1 2410,Besançon local 1,202928000,47.2422,6.08361,79,3,18,0a +DAB,métropole métropolitain 1 2702,métropole métropolitain 1,192352000,43.8386,5.03028,13,1.7,1b,02 +DAB,métropole métropolitain 1 0410,métropole métropolitain 1,211648000,43.8247,4.34083,73,5,04,0a +DAB,métropole métropolitain 1 1904,métropole métropolitain 1,188928000,45.7983,4.70361,20,0.1,13,04 +DAB,Montpellier étendu 1 0802,Montpellier étendu 1,220352000,43.1642,2.97306,24,3.3,08,02 +DAB,Montpellier étendu 1 0801,Montpellier étendu 1,220352000,43.3631,3.22917,27,3.3,08,01 +DAB,Montpellier étendu 1 0810,Montpellier étendu 1,220352000,43.5242,3.64444,65,3.5,08,0a +DAB,Nîmes étendu 1 1701,Nîmes étendu 1,209936000,44.1164,4.05944,14,5,11,01 +DAB,Perpignan local 1 0310,Perpignan local 1,216928000,42.7681,2.77917,26,5,03,0a +DAB,métropole métropolitain 2 2809,métropole métropolitain 2,197648000,43.3586,5.57444,27,2.6,1c,09 +DAB,métropole métropolitain 2 1303,métropole métropolitain 2,206352000,47.3003,4.99056,29,5,0d,03 +DAB,métropole métropolitain 2 0005,métropole métropolitain 2,206352000,47.335,4.81083,22,2.6,00,05 +DAB,Amiens étendu 1 0810,Amiens étendu 1,0,49.8597,2.28222,,0,08,0a +DAB,Amiens local 1 3010,Amiens local 1,0,49.8597,2.28222,,0,1e,0a +DAB,Besançon étendu 1 1202,Besançon étendu 1,180064000,46.6519,5.58222,36,2.6,0c,02 +DAB,Besançon étendu 1 1203,Besançon étendu 1,180064000,47.1169,5.47,35,2.6,0c,03 +DAB,Annemasse local 1 3404,Annemasse local 1,194064000,46.2908,5.98528,11,8.7,22,04 diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index bb936c93d..7f8601922 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -38,6 +38,7 @@ #include "util/units.h" #include "util/maidenhead.h" #include "util/morse.h" +#include "util/navtex.h" #include "maplocationdialog.h" #include "mapmaidenheaddialog.h" #include "mapsettingsdialog.h" @@ -307,6 +308,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur addNavAids(); addAirspace(); addAirports(); + addNavtex(); displaySettings(); applySettings(true); @@ -985,6 +987,50 @@ void MapGUI::airportsUpdated() addAirports(); } + +void MapGUI::addNavtex() +{ + for (int i = 0; i < NavtexTransmitter::m_navtexTransmitters.size(); i++) + { + SWGSDRangel::SWGMapItem navtexMapItem; + QString name = QString("%1").arg(NavtexTransmitter::m_navtexTransmitters[i].m_station); + navtexMapItem.setName(new QString(name)); + navtexMapItem.setLatitude(NavtexTransmitter::m_navtexTransmitters[i].m_latitude); + navtexMapItem.setLongitude(NavtexTransmitter::m_navtexTransmitters[i].m_longitude); + navtexMapItem.setAltitude(0.0); + navtexMapItem.setImage(new QString("antenna.png")); + navtexMapItem.setImageRotation(0); + QString text = QString("Navtex Transmitter\nStation: %1\nArea: %2") + .arg(NavtexTransmitter::m_navtexTransmitters[i].m_station) + .arg(NavtexTransmitter::m_navtexTransmitters[i].m_area); + QStringList schedules; + for (const auto& schedule : NavtexTransmitter::m_navtexTransmitters[i].m_schedules) + { + QString scheduleText = QString("\nFrequency: %1 kHz\nID: %2").arg(schedule.m_frequency / 1000).arg(schedule.m_id); + if (schedule.m_times.size() > 0) + { + QStringList times; + for (const auto& time : schedule.m_times) { + times.append(time.toString("hh:mm")); + } + scheduleText.append("\nTimes: "); + scheduleText.append(times.join(" ")); + scheduleText.append(" UTC"); + } + schedules.append(scheduleText); + } + text.append(schedules.join("")); + navtexMapItem.setText(new QString(text)); + navtexMapItem.setModel(new QString("antenna.glb")); + navtexMapItem.setFixedPosition(true); + navtexMapItem.setOrientation(0); + navtexMapItem.setLabel(new QString(name)); + navtexMapItem.setLabelAltitudeOffset(4.5); + navtexMapItem.setAltitudeReference(1); + update(m_map, &navtexMapItem, "Navtex"); + } +} + void MapGUI::blockApplySettings(bool block) { m_doApplySettings = !block; diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index bf41dd065..c90a27b72 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -160,6 +160,7 @@ public: void addAirspace(const Airspace *airspace, const QString& group, int cnt); void addAirspace(); void addAirports(); + void addNavtex(); void find(const QString& target); void track3D(const QString& target); Q_INVOKABLE void supportedMapsChanged(); diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index 93d972c39..c8e8e4bec 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -94,6 +94,8 @@ MapSettings::MapSettings() : dabSettings->m_filterDistance = 75000; m_itemSettings.insert("DAB", dabSettings); + m_itemSettings.insert("Navtex", new MapItemSettings("Navtex", false, QColor(255, 0, 255), false, true, 8)); + MapItemSettings *navAidSettings = new MapItemSettings("NavAid", false, QColor(255, 0, 255), false, true, 11); navAidSettings->m_filterDistance = 500000; m_itemSettings.insert("NavAid", navAidSettings); diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 1a65ddf80..4a0dcfcd1 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -12,16 +12,18 @@ On top of this, it can plot data from other plugins, such as: * Weather imagery from APT Demodulator, * The Sun, Moon and Stars from the Star Tracker, * Weather ballons from the RadioSonde feature, +* RF Heat Maps from the Heap Map channel, * Radials and estimated position from the VOR localizer feature. -As well as other data sources: +As well as internet data sources: -* AM, FM and DAB transmitters in the UK, +* AM, FM and DAB transmitters in the UK and DAB transmitters in France, * Airports, NavAids and airspaces, * Beacons based on the IARU Region 1 beacon database and International Beacon Project, * Radio time transmitters, * GRAVES radar, -* Ionosonde station data. +* Ionosonde station data, +* Navtex transmitters. It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites. diff --git a/sdrbase/util/csv.cpp b/sdrbase/util/csv.cpp index 715551955..42dbfe24f 100644 --- a/sdrbase/util/csv.cpp +++ b/sdrbase/util/csv.cpp @@ -70,7 +70,7 @@ QHash *CSV::hash(const QString& filename, int reserve) // Read a row from a CSV file (handling quotes) // https://stackoverflow.com/questions/27318631/parsing-through-a-csv-file-in-qt -bool CSV::readRow(QTextStream &in, QStringList *row) +bool CSV::readRow(QTextStream &in, QStringList *row, char separator) { static const int delta[][5] = { // , " \n ? eof @@ -101,7 +101,7 @@ bool CSV::readRow(QTextStream &in, QStringList *row) else { in >> ch; - if (ch == ',') { + if (ch == separator) { t = 0; } else if (ch == '\"') { t = 1; @@ -137,13 +137,13 @@ bool CSV::readRow(QTextStream &in, QStringList *row) // Read header row from CSV file and return a hash mapping names to column numbers // Returns error if header row can't be read, or if all of requiredColumns aren't found -QHash CSV::readHeader(QTextStream &in, QStringList requiredColumns, QString &error) +QHash CSV::readHeader(QTextStream &in, QStringList requiredColumns, QString &error, char separator) { QHash colNumbers; QStringList row; // Read column names - if (CSV::readRow(in, &row)) + if (CSV::readRow(in, &row, separator)) { // Create hash mapping column names to indices for (int i = 0; i < row.size(); i++) { diff --git a/sdrbase/util/csv.h b/sdrbase/util/csv.h index 99692e60a..5e3743014 100644 --- a/sdrbase/util/csv.h +++ b/sdrbase/util/csv.h @@ -46,8 +46,8 @@ struct SDRBASE_API CSV { static QHash *hash(const QString& filename, int reserve=0); - static bool readRow(QTextStream &in, QStringList *row); - static QHash readHeader(QTextStream &in, QStringList requiredColumns, QString &error); + static bool readRow(QTextStream &in, QStringList *row, char seperator=','); + static QHash readHeader(QTextStream &in, QStringList requiredColumns, QString &error, char seperator=','); }; diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h index a813651d8..62c1a745f 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -294,6 +294,25 @@ public: longitude = -longitude; return true; } + QRegExp dms2(QString("([0-9]+)([NS])([0-9]{2})([0-9]{2}) *,?([0-9]+)([EW])([0-9]{2})([0-9]{2})")); + if (dms2.exactMatch(string)) + { + float latD = dms2.capturedTexts()[1].toFloat(); + bool north = dms2.capturedTexts()[2] == "N"; + float latM = dms2.capturedTexts()[3].toFloat(); + float latS = dms2.capturedTexts()[4].toFloat(); + float lonD = dms2.capturedTexts()[5].toFloat(); + bool east = dms2.capturedTexts()[6] == "E"; + float lonM = dms2.capturedTexts()[7].toFloat(); + float lonS = dms2.capturedTexts()[8].toFloat(); + 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; } From cd59307806990d175408fce0d3570bcb4d235161 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 18:00:20 +0000 Subject: [PATCH 10/12] Update docs --- plugins/channelrx/demodnavtex/readme.md | 14 +++++++------- plugins/channelrx/demodrtty/readme.md | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/channelrx/demodnavtex/readme.md b/plugins/channelrx/demodnavtex/readme.md index 7f80d0e5c..a36e6639b 100644 --- a/plugins/channelrx/demodnavtex/readme.md +++ b/plugins/channelrx/demodnavtex/readme.md @@ -1,4 +1,4 @@ -

Navtex demodulator plugin

+

Navtex Demodulator Plugin

Introduction

@@ -29,7 +29,7 @@ Average total power in dB relative to a +/- 1.0 amplitude signal received in the

4: Navarea

-Specifies the geographical area in which the receiver is in. This enables the plugin to decode transmitter station identifiers, and display which transmitter the current transmission timeslot is assigned to. +Specifies the geographical area in which the receiver is in. This enables the plugin to decode transmitter station identifiers, and display which transmitter the current transmission timeslot is assigned to (5). Note that with good propagation conditions, it is possible to receive messages from another area, so the station indicated in the message table (17) should be checked against the location given in the recevied message text.

5: TX

@@ -46,15 +46,15 @@ This specifies the bandwidth of a filter that is applied to the input signal to

8: UDP

-When checked, received packets are forwarded to the specified UDP address (9) and port (10). +When checked, received messages are forwarded to the specified UDP address (9) and port (10).

9: UDP address

-IP address of the host to forward received packets to via UDP. +IP address of the host to forward received messages to via UDP.

10: UDP port

-UDP port number to forward received packets to. +UDP port number to forward received messages to.

11: Station Filter

@@ -66,7 +66,7 @@ This drop down displays a list of all message types that have been received. Whe

13: Start/stop Logging Messages to .csv File

-When checked, writes all received messages to a .csv file. +When checked, writes all received messages to a .csv file, specified by (14).

14: .csv Log Filename

@@ -87,7 +87,7 @@ The received messages table displays the contents of the messages that have been * Date - Date the message was received. * Time - Time the message was received. * SID - Station identifer of the transmitting station. -* Station - SID decoded according to the currently selected navarea. +* Station - SID decoded according to the currently selected navarea (4). * TID - Message type identifier. * MID - Message identifier. * Message - The message text. diff --git a/plugins/channelrx/demodrtty/readme.md b/plugins/channelrx/demodrtty/readme.md index 34f55893a..f6ae6de52 100644 --- a/plugins/channelrx/demodrtty/readme.md +++ b/plugins/channelrx/demodrtty/readme.md @@ -1,9 +1,9 @@ -

RTTY demodulator plugin

+

RTTY Demodulator Plugin

Introduction

This plugin can be used to demodulate RTTY (Radioteletype) transmissions. -RTTY using BFSK (Binary Frequency Shift Keying), where transmission of data alternates between two frequencies, +RTTY uses BFSK (Binary Frequency Shift Keying), where transmission of data alternates between two frequencies, the mark frequency and the space frequency. The RTTY Demodulor should be centered in between these frequencies. The baud rate, frequency shift (difference between mark and space frequencies), bandwidth and baudot character set are configurable. @@ -60,7 +60,7 @@ UDP port number to forward received characters to.

11: Squelch

-Sets the squelch power. Characters received with average power lower than this setting will be discarded. +Sets the squelch power level in dB. Characters received with average power lower than this setting will be discarded.

12: Baudot Character Set

@@ -79,11 +79,11 @@ Specifies whether bits are transmitted least-significant-bit first (LSB) or most

14: Mark/Space Frequency

-When unchecked, the mark frequency is the higher frequency, when checked space frequency is higher. +When unchecked, the mark frequency is the higher frequency, when checked the space frequency is higher.

15: Suppress CR LF

-When checked the CR CR LF sequence is just displayed as CR. +When checked the CR CR LF sequence is just displayed as CR. This can help make more received text visible in (19).

16: Unshift on Space

From d97d1f7ed093936f4ff75c1be69864720ccdf3b7 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 18:02:18 +0000 Subject: [PATCH 11/12] Connect signals to slots --- plugins/channelrx/demodais/aisdemodgui.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/channelrx/demodais/aisdemodgui.cpp b/plugins/channelrx/demodais/aisdemodgui.cpp index 1614b19dc..f9516aead 100644 --- a/plugins/channelrx/demodais/aisdemodgui.cpp +++ b/plugins/channelrx/demodais/aisdemodgui.cpp @@ -783,6 +783,8 @@ void AISDemodGUI::makeUIConnections() QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &AISDemodGUI::on_logEnable_clicked); QObject::connect(ui->logFilename, &QToolButton::clicked, this, &AISDemodGUI::on_logFilename_clicked); QObject::connect(ui->logOpen, &QToolButton::clicked, this, &AISDemodGUI::on_logOpen_clicked); + QObject::connect(ui->channel1, QOverload::of(&QComboBox::currentIndexChanged), this, &AISDemodGUI::on_channel1_currentIndexChanged); + QObject::connect(ui->channel2, QOverload::of(&QComboBox::currentIndexChanged), this, &AISDemodGUI::on_channel2_currentIndexChanged); } void AISDemodGUI::updateAbsoluteCenterFrequency() From 42bee4e3fa597a5b4b61e109c1372526fc357932 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 3 Mar 2023 18:03:38 +0000 Subject: [PATCH 12/12] Remove unused filter. Fix default log file name. --- plugins/channelrx/demodpacket/packetdemodsettings.cpp | 2 +- plugins/channelrx/demodpacket/packetdemodsink.cpp | 1 - plugins/channelrx/demodpacket/packetdemodsink.h | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/channelrx/demodpacket/packetdemodsettings.cpp b/plugins/channelrx/demodpacket/packetdemodsettings.cpp index 93abb748e..ae46e7853 100644 --- a/plugins/channelrx/demodpacket/packetdemodsettings.cpp +++ b/plugins/channelrx/demodpacket/packetdemodsettings.cpp @@ -170,7 +170,7 @@ bool PacketDemodSettings::deserialize(const QByteArray& data) m_udpPort = 9999; } - d.readString(25, &m_logFilename, "pager_log.csv"); + d.readString(25, &m_logFilename, "packet_log.csv"); d.readBool(26, &m_logEnabled, false); if (m_rollupState) diff --git a/plugins/channelrx/demodpacket/packetdemodsink.cpp b/plugins/channelrx/demodpacket/packetdemodsink.cpp index 98928638e..67e98c99c 100644 --- a/plugins/channelrx/demodpacket/packetdemodsink.cpp +++ b/plugins/channelrx/demodpacket/packetdemodsink.cpp @@ -302,7 +302,6 @@ void PacketDemodSink::applySettings(const PacketDemodSettings& settings, bool fo m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); m_interpolatorDistance = (Real) m_channelSampleRate / (Real) PacketDemodSettings::PACKETDEMOD_CHANNEL_SAMPLE_RATE; m_interpolatorDistanceRemain = m_interpolatorDistance; - m_lowpass.create(301, PacketDemodSettings::PACKETDEMOD_CHANNEL_SAMPLE_RATE, settings.m_rfBandwidth / 2.0f); } if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) { diff --git a/plugins/channelrx/demodpacket/packetdemodsink.h b/plugins/channelrx/demodpacket/packetdemodsink.h index 9db2d722f..ad4c36ba5 100644 --- a/plugins/channelrx/demodpacket/packetdemodsink.h +++ b/plugins/channelrx/demodpacket/packetdemodsink.h @@ -105,7 +105,6 @@ private: MovingAverageUtil m_movingAverage; - Lowpass m_lowpass; PhaseDiscriminators m_phaseDiscri; int m_correlationLength;