1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-22 17:45:48 -05:00

Add RTTY demodulator

This commit is contained in:
Jon Beniston 2023-03-03 16:14:09 +00:00
parent 6b2705065b
commit a2cfe07dee
23 changed files with 5342 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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 $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()

View File

@ -0,0 +1,103 @@
<h1>RTTY demodulator plugin</h1>
<h2>Introduction</h2>
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.
<h2>Interface</h2>
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)
<h3>1: Frequency shift from center frequency of reception</h3>
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.
<h3>2: Channel power</h3>
Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band.
<h3>3: Level meter in dB</h3>
- top bar (green): average value
- bottom bar (blue green): instantaneous peak value
- tip vertical bar (bright green): peak hold value
<h3>4: RTTY Presets</h3>
From the presets dropdown, you can select common baud rate and frequency shift settings, or choose Custom to set these individually.
<h3>5: Baud rate</h3>
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.
<h3>6: Frequency shift</h3>
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.
<h3>7: RF Bandwidth</h3>
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.
<h3>8: UDP</h3>
When checked, received characters are forwarded to the specified UDP address (9) and port (10).
<h3>9: UDP address</h3>
IP address of the host to forward received characters to via UDP.
<h3>10: UDP port</h3>
UDP port number to forward received characters to.
<h3>11: Squelch</h3>
Sets the squelch power. Characters received with average power lower than this setting will be discarded.
<h3>12: Baudot Character Set</h3>
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
<h3>13: Bit ordering</h3>
Specifies whether bits are transmitted least-significant-bit first (LSB) or most-significant-bit first (MSB).
<h3>14: Mark/Space Frequency</h3>
When unchecked, the mark frequency is the higher frequency, when checked space frequency is higher.
<h3>15: Suppress CR LF</h3>
When checked the CR CR LF sequence is just displayed as CR.
<h3>16: Unshift on Space</h3>
When checked, the Baudot character set will shift to letters when a space character (' ') is received.
<h3>17: Start/stop Logging Messages to .txt File</h3>
When checked, writes all received characters to the .txt file specified by (16).
<h3>18: .txt Log Filename</h3>
Click to specify the name of the .txt file which received characters are logged to.
<h3>19: Received Text</h3>
The received text area shows characters as they are received.

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "rttydemod.h"
#include <QTime>
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QBuffer>
#include <QThread>
#include <stdio.h>
#include <complex.h>
#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<QString> 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<ObjectPipe*> pipes;
MainCore::instance()->getMessagePipes().getMessagePipes(this, "reportdemod", pipes);
if (pipes.size() > 0)
{
for (const auto& pipe : pipes)
{
MessageQueue *messageQueue = qobject_cast<MessageQueue*>(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<QString>& 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<QString>& 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);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_RTTYDEMOD_H
#define INCLUDE_RTTYDEMOD_H
#include <QNetworkRequest>
#include <QUdpSocket>
#include <QThread>
#include <QFile>
#include <QTextStream>
#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<QString>& channelSettingsKeys, const RttyDemodSettings& settings, bool force);
void webapiFormatChannelSettings(
QList<QString>& 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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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());
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_RTTYDEMODBASEBAND_H
#define INCLUDE_RTTYDEMODBASEBAND_H
#include <QObject>
#include <QRecursiveMutex>
#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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDockWidget>
#include <QMainWindow>
#include <QDebug>
#include <QAction>
#include <QClipboard>
#include <QFileDialog>
#include <QScrollBar>
#include <QMenu>
#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<const ChannelMarker*>(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<RttyDemod*>(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<int>::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<int>::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_characterSet_currentIndexChanged);
QObject::connect(ui->suppressCRLF, &ButtonSwitch::clicked, this, &RttyDemodGUI::on_suppressCRLF_clicked);
QObject::connect(ui->mode, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_mode_currentIndexChanged);
QObject::connect(ui->filter, QOverload<int>::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<int>::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_channel1_currentIndexChanged);
QObject::connect(ui->channel2, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RttyDemodGUI::on_channel2_currentIndexChanged);
}
void RttyDemodGUI::updateAbsoluteCenterFrequency()
{
setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

File diff suppressed because it is too large Load Diff

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QtPlugin>
#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();
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_RTTYDEMODPLUGIN_H
#define INCLUDE_RTTYDEMODPLUGIN_H
#include <QObject>
#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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_RTTYDEMODSETTINGS_H
#define INCLUDE_RTTYDEMODSETTINGS_H
#include <QByteArray>
#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 */

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QRegularExpression>
#include <complex.h>
#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<SampleVector::const_iterator> 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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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<Real, double, 16> m_movingAverage;
Lowpass<Real> m_envelope1;
Lowpass<Real> m_envelope2;
Lowpass<Real> m_lowpass1;
Lowpass<Real> m_lowpass2;
Lowpass<Complex> m_lowpassComplex1;
Lowpass<Complex> m_lowpassComplex2;
RaisedCosine<Complex> m_raisedCosine1;
RaisedCosine<Complex> m_raisedCosine2;
MovingMaximum<Real> m_movMax1;
MovingMaximum<Real> 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<int> m_clockHistogram;
int m_edgeCount;
MovingAverageUtil<Real, Real, 5> m_baudRateAverage;
// For frequency shift estimation
std::vector<Real> m_shiftEstMag;
int m_fftSequence;
FFTEngine *m_fft;
int m_fftCounter;
static const int m_fftSize = 128; // ~7Hz res
MovingAverageUtil<Real, Real, 16> m_freq1Average;
MovingAverageUtil<Real, Real, 16> 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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

192
sdrbase/util/baudot.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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", "", "8", "9", "", "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;
}

77
sdrbase/util/baudot.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_UTIL_BAUDOT_H
#define INCLUDE_UTIL_BAUDOT_H
#include <QString>
#include <QDateTime>
#include <QMap>
#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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_UTIL_MOVINGMAXIMUM_H
#define INCLUDE_UTIL_MOVINGMAXIMUM_H
#include <algorithm>
#include <QDebug>
// Calculates moving maximum over a number of samples
template <typename T>
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 */

View File

@ -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);

View File

@ -48,6 +48,7 @@ const QMap<QString, QString> 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<QString, QString> 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<QString, QString> 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<QString, QString> WebAPIUtils::m_channelTypeToSettingsKey = {
{"RemoteSink", "RemoteSinkSettings"},
{"RemoteSource", "RemoteSourceSettings"},
{"RemoteTCPSink", "RemoteTCPSinkSettings"},
{"RTTYDemodSettings", "RTTYDemodSettings"},
{"SSBMod", "SSBModSettings"},
{"SSBDemod", "SSBDemodSettings"},
{"FT8Demod", "FT8DemodSettings"},