diff --git a/doc/img/RTTYDemod_plugin.png b/doc/img/RTTYDemod_plugin.png new file mode 100644 index 000000000..58acef1fd Binary files /dev/null and b/doc/img/RTTYDemod_plugin.png differ 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"},