From 9543f3a117b621797e56296b209cfaf1d88a0398 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Wed, 23 Sep 2020 13:42:29 +0100 Subject: [PATCH] Add 9600 FSK modem with scrambler and raised-cosine pulse-shaping. Add baseband BPF for AFSK. --- doc/img/PacketMod_plugin.png | Bin 17517 -> 22719 bytes plugins/channeltx/modpacket/CMakeLists.txt | 3 + plugins/channeltx/modpacket/packetmod.cpp | 40 ++ .../modpacket/packetmodbpfdialog.cpp | 45 ++ .../channeltx/modpacket/packetmodbpfdialog.h | 45 ++ .../channeltx/modpacket/packetmodbpfdialog.ui | 146 +++++ plugins/channeltx/modpacket/packetmodgui.cpp | 67 +++ plugins/channeltx/modpacket/packetmodgui.h | 3 + plugins/channeltx/modpacket/packetmodgui.ui | 19 + .../channeltx/modpacket/packetmodsettings.cpp | 80 ++- .../channeltx/modpacket/packetmodsettings.h | 16 +- .../channeltx/modpacket/packetmodsource.cpp | 138 ++++- plugins/channeltx/modpacket/packetmodsource.h | 15 +- .../modpacket/packetmodtxsettingsdialog.cpp | 18 +- .../modpacket/packetmodtxsettingsdialog.h | 11 +- .../modpacket/packetmodtxsettingsdialog.ui | 519 ++++++++++++------ plugins/channeltx/modpacket/readme.md | 26 +- sdrbase/CMakeLists.txt | 3 + sdrbase/dsp/raisedcosine.h | 136 +++++ sdrbase/resources/webapi/doc/html2/index.html | 17 +- sdrbase/util/lfsr.cpp | 106 ++++ sdrbase/util/lfsr.h | 105 ++++ sdrbase/util/popcount.h | 40 ++ .../api/swagger/include/PacketMod.yaml | 11 + swagger/sdrangel/code/html2/index.html | 17 +- .../code/qt5/client/SWGPacketModSettings.cpp | 94 ++++ .../code/qt5/client/SWGPacketModSettings.h | 24 + 27 files changed, 1523 insertions(+), 221 deletions(-) create mode 100644 plugins/channeltx/modpacket/packetmodbpfdialog.cpp create mode 100644 plugins/channeltx/modpacket/packetmodbpfdialog.h create mode 100644 plugins/channeltx/modpacket/packetmodbpfdialog.ui create mode 100644 sdrbase/dsp/raisedcosine.h create mode 100644 sdrbase/util/lfsr.cpp create mode 100644 sdrbase/util/lfsr.h create mode 100644 sdrbase/util/popcount.h diff --git a/doc/img/PacketMod_plugin.png b/doc/img/PacketMod_plugin.png index 72899037bb8cd6956bab23538f399f1e8477124d..db6e2f4dd4004dc5db68c4902991334d982542d9 100644 GIT binary patch literal 22719 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 // change of stream is possible for MIMO devices only @@ -309,6 +324,9 @@ void PacketMod::webapiUpdateChannelSettings( if (channelSettingsKeys.contains("inputFrequencyOffset")) { settings.m_inputFrequencyOffset = response.getPacketModSettings()->getInputFrequencyOffset(); } + if (channelSettingsKeys.contains("mode")) { + settings.setMode(*response.getPacketModSettings()->getMode()); + } if (channelSettingsKeys.contains("rfBandwidth")) { settings.m_rfBandwidth = response.getPacketModSettings()->getRfBandwidth(); } @@ -345,6 +363,15 @@ void PacketMod::webapiUpdateChannelSettings( if (channelSettingsKeys.contains("preEmphasisHighFreq")) { settings.m_preEmphasisHighFreq = response.getPacketModSettings()->getPreEmphasisHighFreq(); } + if (channelSettingsKeys.contains("bpf")) { + settings.m_bpf = response.getPacketModSettings()->getBpf() != 0; + } + if (channelSettingsKeys.contains("bpfLowCutoff")) { + settings.m_bpfLowCutoff = response.getPacketModSettings()->getBpfLowCutoff(); + } + if (channelSettingsKeys.contains("bpfHighCutoff")) { + settings.m_bpfHighCutoff = response.getPacketModSettings()->getBpfHighCutoff(); + } if (channelSettingsKeys.contains("rgbColor")) { settings.m_rgbColor = response.getPacketModSettings()->getRgbColor(); } @@ -415,6 +442,7 @@ int PacketMod::webapiActionsPost( void PacketMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const PacketModSettings& settings) { response.getPacketModSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getPacketModSettings()->setMode(new QString(settings.getMode())); response.getPacketModSettings()->setRfBandwidth(settings.m_rfBandwidth); response.getPacketModSettings()->setFmDeviation(settings.m_fmDeviation); response.getPacketModSettings()->setGain(settings.m_gain); @@ -427,6 +455,9 @@ void PacketMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& res response.getPacketModSettings()->setPreEmphasis(settings.m_preEmphasis ? 1 : 0); response.getPacketModSettings()->setPreEmphasisTau(settings.m_preEmphasisTau); response.getPacketModSettings()->setPreEmphasisHighFreq(settings.m_preEmphasisHighFreq); + response.getPacketModSettings()->setBpf(settings.m_bpf ? 1 : 0); + response.getPacketModSettings()->setBpfLowCutoff(settings.m_bpfLowCutoff); + response.getPacketModSettings()->setBpfHighCutoff(settings.m_bpfHighCutoff); response.getPacketModSettings()->setRgbColor(settings.m_rgbColor); if (response.getPacketModSettings()->getTitle()) { @@ -505,6 +536,15 @@ void PacketMod::webapiReverseSendSettings(QList& channelSettingsKeys, c if (channelSettingsKeys.contains("preEmphasisHighFreq") || force) { swgPacketModSettings->setPreEmphasisHighFreq(settings.m_preEmphasisHighFreq); } + if (channelSettingsKeys.contains("bpf") || force) { + swgPacketModSettings->setBpf(settings.m_preEmphasis); + } + if (channelSettingsKeys.contains("bpfLowCutoff") || force) { + swgPacketModSettings->setBpfLowCutoff(settings.m_bpfLowCutoff); + } + if (channelSettingsKeys.contains("bpfHighCutoff") || force) { + swgPacketModSettings->setBpfHighCutoff(settings.m_bpfHighCutoff); + } if (channelSettingsKeys.contains("rgbColor") || force) { swgPacketModSettings->setRgbColor(settings.m_rgbColor); } diff --git a/plugins/channeltx/modpacket/packetmodbpfdialog.cpp b/plugins/channeltx/modpacket/packetmodbpfdialog.cpp new file mode 100644 index 000000000..c318d0658 --- /dev/null +++ b/plugins/channeltx/modpacket/packetmodbpfdialog.cpp @@ -0,0 +1,45 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#define _USE_MATH_DEFINES +#include +#include "packetmodbpfdialog.h" +#include "ui_packetmodbpfdialog.h" + +PacketModBPFDialog::PacketModBPFDialog(float lowFreq, float highFreq, int taps, QWidget* parent) : + QDialog(parent), + ui(new Ui::PacketModBPFDialog) +{ + ui->setupUi(this); + ui->lowFreq->setValue(lowFreq); + ui->highFreq->setValue(highFreq); + ui->taps->setValue(taps); +} + +PacketModBPFDialog::~PacketModBPFDialog() +{ + delete ui; +} + +void PacketModBPFDialog::accept() +{ + m_lowFreq = ui->lowFreq->value(); + m_highFreq = ui->highFreq->value(); + m_taps = ui->taps->value(); + + QDialog::accept(); +} diff --git a/plugins/channeltx/modpacket/packetmodbpfdialog.h b/plugins/channeltx/modpacket/packetmodbpfdialog.h new file mode 100644 index 000000000..7746228b3 --- /dev/null +++ b/plugins/channeltx/modpacket/packetmodbpfdialog.h @@ -0,0 +1,45 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_PACKETMODBPFDIALOG_H +#define INCLUDE_PACKETMODBPFDIALOG_H + +#include + +namespace Ui { + class PacketModBPFDialog; +} + +class PacketModBPFDialog : public QDialog { + Q_OBJECT + +public: + explicit PacketModBPFDialog(float lowFreq, float highFreq, int taps, QWidget* parent = 0); + ~PacketModBPFDialog(); + + float m_lowFreq; + float m_highFreq; + int m_taps; + +private slots: + void accept(); + +private: + Ui::PacketModBPFDialog* ui; +}; + +#endif // INCLUDE_PACKETMODBPFDIALOG_H diff --git a/plugins/channeltx/modpacket/packetmodbpfdialog.ui b/plugins/channeltx/modpacket/packetmodbpfdialog.ui new file mode 100644 index 000000000..9c864668d --- /dev/null +++ b/plugins/channeltx/modpacket/packetmodbpfdialog.ui @@ -0,0 +1,146 @@ + + + PacketModBPFDialog + + + + 0 + 0 + 351 + 152 + + + + + Liberation Sans + 9 + + + + Bandpass Filter Settings + + + + + + + + + Low frequency corner (Hz) + + + + + + + Low frequency corner. + + + 10000.000000000000000 + + + 1000.000000000000000 + + + + + + + High frequency corner (Hz) + + + + + + + High frequency corner. + + + 1000000.000000000000000 + + + 1000.000000000000000 + + + + + + + Number of taps in the filter. More taps gives sharper roll off. Must be odd. + + + 5 + + + 100001 + + + 301 + + + + + + + + + + Filter taps + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + lowFreq + highFreq + + + + + buttonBox + accepted() + PacketModBPFDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PacketModBPFDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channeltx/modpacket/packetmodgui.cpp b/plugins/channeltx/modpacket/packetmodgui.cpp index ed7edce42..2ed69fb3e 100644 --- a/plugins/channeltx/modpacket/packetmodgui.cpp +++ b/plugins/channeltx/modpacket/packetmodgui.cpp @@ -39,6 +39,7 @@ #include "packetmodgui.h" #include "packetmodrepeatdialog.h" #include "packetmodtxsettingsdialog.h" +#include "packetmodbpfdialog.h" PacketModGUI* PacketModGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx) @@ -140,6 +141,28 @@ void PacketModGUI::on_deltaFrequency_changed(qint64 value) applySettings(); } +void PacketModGUI::on_mode_currentIndexChanged(int value) +{ + QString mode = ui->mode->currentText(); + + // If m_doApplySettings is set, we are here from a call to displaySettings, + // so we only want to display the current settings, not update them + // as though a user had selected a new mode + if (m_doApplySettings) + m_settings.setMode(mode); + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); + ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + ui->glSpectrum->setCenterFrequency(m_settings.m_spectrumRate/4); + ui->glSpectrum->setSampleRate(m_settings.m_spectrumRate/2); + applySettings(); + + // Remove custom mode when deselected, as we no longer know how to set it + if (value < 2) + ui->mode->removeItem(2); +} + void PacketModGUI::on_rfBW_valueChanged(int value) { float bw = value * 100.0f; @@ -254,6 +277,12 @@ void PacketModGUI::on_preEmphasis_toggled(bool checked) applySettings(); } +void PacketModGUI::on_bpf_toggled(bool checked) +{ + m_settings.m_bpf = checked; + applySettings(); +} + void PacketModGUI::preEmphasisSelect() { FMPreemphasisDialog dialog(m_settings.m_preEmphasisTau, m_settings.m_preEmphasisHighFreq); @@ -265,6 +294,18 @@ void PacketModGUI::preEmphasisSelect() } } +void PacketModGUI::bpfSelect() +{ + PacketModBPFDialog dialog(m_settings.m_bpfLowCutoff, m_settings.m_bpfHighCutoff, m_settings.m_bpfTaps); + if (dialog.exec() == QDialog::Accepted) + { + m_settings.m_bpfLowCutoff = dialog.m_lowFreq; + m_settings.m_bpfHighCutoff = dialog.m_highFreq; + m_settings.m_bpfTaps = dialog.m_taps; + applySettings(); + } +} + void PacketModGUI::repeatSelect() { PacketModRepeatDialog dialog(m_settings.m_repeatDelay, m_settings.m_repeatCount); @@ -280,7 +321,10 @@ void PacketModGUI::txSettingsSelect() { PacketModTXSettingsDialog dialog(m_settings.m_rampUpBits, m_settings.m_rampDownBits, m_settings.m_rampRange, m_settings.m_modulateWhileRamping, + m_settings.m_modulation, m_settings.m_baud, m_settings.m_markFrequency, m_settings.m_spaceFrequency, + m_settings.m_pulseShaping, m_settings.m_beta, m_settings.m_symbolSpan, + m_settings.m_scramble, m_settings.m_polynomial, m_settings.m_ax25PreFlags, m_settings.m_ax25PostFlags, m_settings.m_ax25Control, m_settings.m_ax25PID, m_settings.m_lpfTaps, @@ -292,8 +336,15 @@ void PacketModGUI::txSettingsSelect() m_settings.m_rampDownBits = dialog.m_rampDownBits; m_settings.m_rampRange = dialog.m_rampRange; m_settings.m_modulateWhileRamping = dialog.m_modulateWhileRamping; + m_settings.m_modulation = static_cast(dialog.m_modulation); + m_settings.m_baud = dialog.m_baud; m_settings.m_markFrequency = dialog.m_markFrequency; m_settings.m_spaceFrequency = dialog.m_spaceFrequency; + m_settings.m_pulseShaping = dialog.m_pulseShaping; + m_settings.m_beta = dialog.m_beta; + m_settings.m_symbolSpan = dialog.m_symbolSpan; + m_settings.m_scramble = dialog.m_scramble; + m_settings.m_polynomial = dialog.m_polynomial; m_settings.m_ax25PreFlags = dialog.m_ax25PreFlags; m_settings.m_ax25PostFlags = dialog.m_ax25PostFlags; m_settings.m_ax25Control = dialog.m_ax25Control; @@ -302,6 +353,7 @@ void PacketModGUI::txSettingsSelect() m_settings.m_bbNoise = dialog.m_bbNoise; m_settings.m_rfNoise = dialog.m_rfNoise; m_settings.m_writeToFile = dialog.m_writeToFile; + displaySettings(); applySettings(); } } @@ -399,6 +451,9 @@ PacketModGUI::PacketModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseb CRightClickEnabler *preempRightClickEnabler = new CRightClickEnabler(ui->preEmphasis); connect(preempRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(preEmphasisSelect())); + CRightClickEnabler *bpfRightClickEnabler = new CRightClickEnabler(ui->bpf); + connect(bpfRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(bpfSelect())); + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); @@ -479,6 +534,18 @@ void PacketModGUI::displaySettings() blockApplySettings(true); ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + if ((m_settings.m_baud == 1200) && (m_settings.m_modulation == PacketModSettings::AFSK)) + ui->mode->setCurrentIndex(0); + else if ((m_settings.m_baud == 9600) && (m_settings.m_modulation == PacketModSettings::FSK)) + ui->mode->setCurrentIndex(1); + else + { + ui->mode->removeItem(2); + ui->mode->addItem(m_settings.getMode()); + ui->mode->setCurrentIndex(2); + } + ui->glSpectrum->setCenterFrequency(m_settings.m_spectrumRate/4); + ui->glSpectrum->setSampleRate(m_settings.m_spectrumRate/2); ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); diff --git a/plugins/channeltx/modpacket/packetmodgui.h b/plugins/channeltx/modpacket/packetmodgui.h index 0c0cd4813..bab47c8a8 100644 --- a/plugins/channeltx/modpacket/packetmodgui.h +++ b/plugins/channeltx/modpacket/packetmodgui.h @@ -88,6 +88,7 @@ private slots: void handleSourceMessages(); void on_deltaFrequency_changed(qint64 value); + void on_mode_currentIndexChanged(int value); void on_rfBW_valueChanged(int index); void on_fmDev_valueChanged(int value); void on_gain_valueChanged(int value); @@ -101,7 +102,9 @@ private slots: void on_packet_returnPressed(); void on_repeat_toggled(bool checked); void on_preEmphasis_toggled(bool checked); + void on_bpf_toggled(bool checked); void preEmphasisSelect(); + void bpfSelect(); void repeatSelect(); void txSettingsSelect(); diff --git a/plugins/channeltx/modpacket/packetmodgui.ui b/plugins/channeltx/modpacket/packetmodgui.ui index 6ae31b79b..8506f45e0 100644 --- a/plugins/channeltx/modpacket/packetmodgui.ui +++ b/plugins/channeltx/modpacket/packetmodgui.ui @@ -210,6 +210,11 @@ 1200 AFSK + + + 9600 FSK + + @@ -472,6 +477,20 @@ + + + + Baseband bandpass filter. Right click for additional settings. + + + ... + + + + :/filter_bandpass.png:/filter_bandpass.png + + + diff --git a/plugins/channeltx/modpacket/packetmodsettings.cpp b/plugins/channeltx/modpacket/packetmodsettings.cpp index 68f0ae8dc..8f0f00faa 100644 --- a/plugins/channeltx/modpacket/packetmodsettings.cpp +++ b/plugins/channeltx/modpacket/packetmodsettings.cpp @@ -33,7 +33,7 @@ void PacketModSettings::resetToDefaults() { m_inputFrequencyOffset = 0; m_baud = 1200; - m_rfBandwidth = 10000.0f; + m_rfBandwidth = 12500.0f; m_fmDeviation = 2500.0f; m_gain = -2.0f; // To avoid overflow, which results in out-of-band RF m_channelMute = false; @@ -47,7 +47,7 @@ void PacketModSettings::resetToDefaults() m_markFrequency = 2200; m_spaceFrequency = 1200; m_ax25PreFlags = 5; - m_ax25PostFlags = 2; + m_ax25PostFlags = 4; // Extra seemingly needed for 9600. m_ax25Control = 3; m_ax25PID = 0xf0; m_preEmphasis = false; @@ -70,6 +70,56 @@ void PacketModSettings::resetToDefaults() m_reverseAPIPort = 8888; m_reverseAPIDeviceIndex = 0; m_reverseAPIChannelIndex = 0; + m_bpf = false; + m_bpfLowCutoff = m_spaceFrequency - 400.0f; + m_bpfHighCutoff = m_markFrequency + 400.0f; + m_bpfTaps = 301; + m_scramble = false; + m_polynomial = 0x10800; + m_pulseShaping = true; + m_beta = 0.5f; + m_symbolSpan = 6; +} + +bool PacketModSettings::setMode(QString mode) +{ + int baud; + bool valid; + + // First part of mode string should give baud rate + baud = mode.split(" ")[0].toInt(&valid); + if (!valid) + return false; + + if (mode.endsWith("AFSK")) + { + // UK channels - https://rsgb.org/main/blog/news/gb2rs/headlines/2015/12/04/check-your-aprs-deviation/ + m_baud = baud; + m_scramble = false; + m_rfBandwidth = 12500.0f; + m_fmDeviation = 2500.0f; + m_spectrumRate = 8000; + m_modulation = PacketModSettings::AFSK; + } + else if (mode.endsWith("FSK")) + { + // G3RUH - http://www.jrmiller.demon.co.uk/products/figs/man9k6.pdf + m_baud = baud; + m_scramble = true; + m_rfBandwidth = 20000.0f; + m_fmDeviation = 3000.0f; + m_spectrumRate = 24000; + m_bpf = false; + m_modulation = PacketModSettings::FSK; + } + else + return false; + return true; +} + +QString PacketModSettings::getMode() const +{ + return QString("%1 %2").arg(m_baud).arg(m_modulation == PacketModSettings::AFSK ? "AFSK" : "FSK"); } QByteArray PacketModSettings::serialize() const @@ -119,6 +169,18 @@ QByteArray PacketModSettings::serialize() const s.writeU32(38, m_reverseAPIDeviceIndex); s.writeU32(39, m_reverseAPIChannelIndex); + s.writeBool(40, m_bpf); + s.writeReal(41, m_bpfLowCutoff); + s.writeReal(42, m_bpfHighCutoff); + s.writeS32(43, m_bpfTaps); + s.writeBool(44, m_scramble); + s.writeS32(45, m_polynomial); + s.writeBool(46, m_pulseShaping); + s.writeReal(47, m_beta); + s.writeS32(48, m_symbolSpan); + s.writeS32(49, m_spectrumRate); + s.writeS32(50, m_modulation); + return s.final(); } @@ -155,7 +217,7 @@ bool PacketModSettings::deserialize(const QByteArray& data) d.readS32(14, &m_markFrequency, 5); d.readS32(15, &m_spaceFrequency, 5); d.readS32(16, &m_ax25PreFlags, 5); - d.readS32(17, &m_ax25PostFlags, 2); + d.readS32(17, &m_ax25PostFlags, 4); d.readS32(18, &m_ax25Control, 3); d.readS32(19, &m_ax25PID, 0xf0); d.readBool(20, &m_preEmphasis, false); @@ -193,6 +255,18 @@ bool PacketModSettings::deserialize(const QByteArray& data) d.readU32(39, &utmp, 0); m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readBool(40, &m_bpf, false); + d.readReal(41, &m_bpfLowCutoff, 1200.0 - 400.0f); + d.readReal(42, &m_bpfHighCutoff, 2200.0 + 400.0f); + d.readS32(43, &m_bpfTaps, 301); + d.readBool(44, &m_scramble, m_baud == 9600); + d.readS32(45, &m_polynomial, 0x10800); + d.readBool(46, &m_pulseShaping, true); + d.readReal(47, &m_beta, 0.5f); + d.readS32(48, &m_symbolSpan, 6); + d.readS32(49, &m_spectrumRate, m_baud == 1200 ? 8000 : 24000); + d.readS32(50, (qint32 *)&m_modulation, m_baud == 1200 ? PacketModSettings::AFSK : PacketModSettings::FSK); + return true; } else diff --git a/plugins/channeltx/modpacket/packetmodsettings.h b/plugins/channeltx/modpacket/packetmodsettings.h index 2514bd2c0..a7db6ee62 100644 --- a/plugins/channeltx/modpacket/packetmodsettings.h +++ b/plugins/channeltx/modpacket/packetmodsettings.h @@ -30,6 +30,7 @@ struct PacketModSettings static const int infinitePackets = -1; qint64 m_inputFrequencyOffset; + enum Modulation {AFSK, FSK} m_modulation; int m_baud; Real m_rfBandwidth; Real m_fmDeviation; @@ -49,8 +50,8 @@ struct PacketModSettings int m_ax25Control; int m_ax25PID; bool m_preEmphasis; - float m_preEmphasisTau; - float m_preEmphasisHighFreq; + Real m_preEmphasisTau; + Real m_preEmphasisHighFreq; int m_lpfTaps; bool m_bbNoise; bool m_rfNoise; @@ -69,12 +70,23 @@ struct PacketModSettings uint16_t m_reverseAPIPort; uint16_t m_reverseAPIDeviceIndex; uint16_t m_reverseAPIChannelIndex; + bool m_bpf; + Real m_bpfLowCutoff; + Real m_bpfHighCutoff; + int m_bpfTaps; + bool m_scramble; + int m_polynomial; + bool m_pulseShaping; + float m_beta; + int m_symbolSpan; PacketModSettings(); void resetToDefaults(); void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } QByteArray serialize() const; bool deserialize(const QByteArray& data); + bool setMode(QString mode); + QString getMode() const; }; #endif /* PLUGINS_CHANNELTX_MODPACKET_PACKETMODSETTINGS_H */ diff --git a/plugins/channeltx/modpacket/packetmodsource.cpp b/plugins/channeltx/modpacket/packetmodsource.cpp index 2e0d9f43b..d849e4ec0 100644 --- a/plugins/channeltx/modpacket/packetmodsource.cpp +++ b/plugins/channeltx/modpacket/packetmodsource.cpp @@ -25,11 +25,12 @@ PacketModSource::PacketModSource() : m_channelSampleRate(48000), + m_spectrumRate(0), m_preemphasisFilter(48000, FMPREEMPHASIS_TAU_US), m_channelFrequencyOffset(0), m_magsq(0.0), m_audioPhase(0.0f), - m_fmPhase(0.0f), + m_fmPhase(0.0), m_levelCalcCount(0), m_peakLevel(0.0f), m_levelSum(0.0f), @@ -37,9 +38,13 @@ PacketModSource::PacketModSource() : m_byteIdx(0), m_bitIdx(0), m_last5Bits(0), - m_state(idle) + m_state(idle), + m_scrambler(0x10800, 0x0) { m_lowpass.create(301, m_channelSampleRate, 22000.0 / 2.0); + qDebug() << "PacketModSource::PacketModSource creating BPF : " << m_channelSampleRate; + m_bandpass.create(301, m_channelSampleRate, 800.0, 2600.0); + m_pulseShape.create(0.5, 6, m_channelSampleRate/9600); applySettings(m_settings, true); applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); } @@ -111,9 +116,6 @@ void PacketModSource::sampleToSpectrum(Real sample) void PacketModSource::modulateSample() { Real audioMod; - Real theta; - Real f_delta; - Real linearGain; Real linearRampGain; Real emphasis; @@ -133,14 +135,19 @@ void PacketModSource::modulateSample() } else { - // Bell 202 AFSK + if (m_sampleIdx == 0) { if (bitsValid()) { // NRZI encoding - encode 0 as change of freq, 1 no change if (getBit() == 0) - m_f = m_f == m_settings.m_markFrequency ? m_settings.m_spaceFrequency : m_settings.m_markFrequency; + m_nrziBit = m_nrziBit == 1 ? 0 : 1; + // Scramble to ensure lots of transitions + if (m_settings.m_scramble) + m_scrambledBit = m_scrambler.scramble(m_nrziBit); + else + m_scrambledBit = m_nrziBit; } // Should we start ramping down power? if ((m_bitCount < m_settings.m_rampDownBits) || ((m_bitCount == 0) && !m_settings.m_rampDownBits)) @@ -151,17 +158,40 @@ void PacketModSource::modulateSample() } } m_sampleIdx++; - if (m_sampleIdx > m_samplesPerSymbol) + if (m_sampleIdx >= m_samplesPerSymbol) m_sampleIdx = 0; if (!m_settings.m_bbNoise) - audioMod = sin(m_audioPhase); + { + if (m_settings.m_modulation == PacketModSettings::AFSK) + { + // Bell 202 AFSK + audioMod = sin(m_audioPhase); + if ((m_state == tx) || m_settings.m_modulateWhileRamping) + m_audioPhase += (M_PI * 2.0f * (m_scrambledBit ? m_settings.m_markFrequency : m_settings.m_spaceFrequency)) / (m_channelSampleRate); + if (m_audioPhase > M_PI) + m_audioPhase -= (2.0f * M_PI); + } + else + { + // FSK + if (m_settings.m_pulseShaping) + { + if ((m_sampleIdx == 1) && (m_state != ramp_down)) + audioMod = m_pulseShape.filter(m_scrambledBit ? 1.0f : -1.0f); + else + audioMod = m_pulseShape.filter(0.0f); + } + else + audioMod = m_scrambledBit ? 1.0f : -1.0f; + } + } else audioMod = (Real)rand()/((Real)RAND_MAX)-0.5; // Noise to test filter frequency response - if ((m_state == tx) || m_settings.m_modulateWhileRamping) - m_audioPhase += (M_PI * 2.0f * m_f) / (m_channelSampleRate); - if (m_audioPhase > M_PI) - m_audioPhase -= (2.0f * M_PI); + + // Baseband bandpass filter + if (m_settings.m_bpf) + audioMod = m_bandpass.filter(audioMod); // Preemphasis filter if (m_settings.m_preEmphasis) @@ -174,25 +204,25 @@ void PacketModSource::modulateSample() sampleToSpectrum(audioMod); // FM - m_fmPhase += audioMod; + m_fmPhase += m_phaseSensitivity * audioMod; + // Keep phase in range -pi,pi if (m_fmPhase > M_PI) - m_fmPhase -= (2.0f * M_PI); - f_delta = m_settings.m_fmDeviation / m_channelSampleRate; - theta = 2.0f * M_PI * f_delta * m_fmPhase; + m_fmPhase -= 2.0f * M_PI; + else if (m_fmPhase < -M_PI) + m_fmPhase += 2.0f * M_PI; linearRampGain = powf(10.0f, m_pow/20.0f); - linearGain = powf(10.0f, m_settings.m_gain/20.0f); if (!m_settings.m_rfNoise) { - m_modSample.real(linearGain * linearRampGain * cos(theta)); - m_modSample.imag(linearGain * linearRampGain * sin(theta)); + m_modSample.real(m_linearGain * linearRampGain * cos(m_fmPhase)); + m_modSample.imag(m_linearGain * linearRampGain * sin(m_fmPhase)); } else { // Noise to test filter frequency response - m_modSample.real(linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); - m_modSample.imag(linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); + m_modSample.real(m_linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); + m_modSample.imag(m_linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); } // Apply low pass filter to limit RF BW @@ -262,6 +292,7 @@ void PacketModSource::calculateLevel(Real& sample) void PacketModSource::applySettings(const PacketModSettings& settings, bool force) { + // Only recreate filters if settings have changed if ((settings.m_lpfTaps != m_settings.m_lpfTaps) || (settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { qDebug() << "PacketModSource::applySettings: Creating new lpf with taps " << settings.m_lpfTaps << " rfBW " << settings.m_rfBandwidth; @@ -272,8 +303,40 @@ void PacketModSource::applySettings(const PacketModSettings& settings, bool forc qDebug() << "PacketModSource::applySettings: Creating new preemphasis filter with tau " << settings.m_preEmphasisTau << " highFreq " << settings.m_preEmphasisHighFreq << " sampleRate " << m_channelSampleRate; m_preemphasisFilter.configure(m_channelSampleRate, settings.m_preEmphasisTau, settings.m_preEmphasisHighFreq); } + if ((settings.m_bpfLowCutoff != m_settings.m_bpfLowCutoff) || (settings.m_bpfHighCutoff != m_settings.m_bpfHighCutoff) + || (settings.m_bpfTaps != m_settings.m_bpfTaps)|| force) + { + qDebug() << "PacketModSource::applySettings: Recreating bandpass filter: " + << " m_bpfTaps: " << settings.m_bpfTaps + << " m_channelSampleRate:" << m_channelSampleRate + << " m_bpfLowCutoff: " << settings.m_bpfLowCutoff + << " m_bpfHighCutoff: " << settings.m_bpfHighCutoff; + m_bandpass.create(settings.m_bpfTaps, m_channelSampleRate, settings.m_bpfLowCutoff, settings.m_bpfHighCutoff); + } + if ((settings.m_beta != m_settings.m_beta) || (settings.m_symbolSpan != m_settings.m_symbolSpan) || (settings.m_baud != m_settings.m_baud) || force) + { + qDebug() << "PacketModSource::applySettings: Recreating pulse shaping filter: " + << " beta: " << settings.m_beta + << " symbolSpan: " << settings.m_symbolSpan + << " channelSampleRate:" << m_channelSampleRate + << " baud:" << settings.m_baud; + m_pulseShape.create(settings.m_beta, m_settings.m_symbolSpan, m_channelSampleRate/settings.m_baud); + } + if ((settings.m_polynomial != m_settings.m_polynomial) || force) + m_scrambler.setPolynomial(settings.m_polynomial); + if ((settings.m_spectrumRate != m_settings.m_spectrumRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorConsumed = false; + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) settings.m_spectrumRate; + m_interpolator.create(48, settings.m_spectrumRate, settings.m_spectrumRate / 2.2, 3.0); + } m_settings = settings; + + // Precalculate FM sensensity and linear gain to save doing it in the loop + m_phaseSensitivity = 2.0f * M_PI * m_settings.m_fmDeviation / (double)m_channelSampleRate; + m_linearGain = powf(10.0f, m_settings.m_gain/20.0f); } void PacketModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) @@ -292,8 +355,25 @@ void PacketModSource::applyChannelSettings(int channelSampleRate, int channelFre if ((m_channelSampleRate != channelSampleRate) || force) { - m_preemphasisFilter.configure(channelSampleRate, m_settings.m_preEmphasisTau); + qDebug() << "PacketModSource::applyChannelSettings: Recreating filters"; m_lowpass.create(m_settings.m_lpfTaps, channelSampleRate, m_settings.m_rfBandwidth / 2.0); + qDebug() << "PacketModSource::applyChannelSettings: Recreating bandpass filter: " + << " bpfTaps: " << m_settings.m_bpfTaps + << " channelSampleRate:" << channelSampleRate + << " bpfLowCutoff: " << m_settings.m_bpfLowCutoff + << " bpfHighCutoff: " << m_settings.m_bpfHighCutoff; + m_bandpass.create(m_settings.m_bpfTaps, channelSampleRate, m_settings.m_bpfLowCutoff, m_settings.m_bpfHighCutoff); + m_preemphasisFilter.configure(channelSampleRate, m_settings.m_preEmphasisTau); + qDebug() << "PacketModSource::applyChannelSettings: Recreating pulse shaping filter: " + << " beta: " << m_settings.m_beta + << " symbolSpan: " << m_settings.m_symbolSpan + << " channelSampleRate:" << m_channelSampleRate + << " baud:" << m_settings.m_baud; + m_pulseShape.create(m_settings.m_beta, m_settings.m_symbolSpan, channelSampleRate/m_settings.m_baud); + } + + if ((m_channelSampleRate != channelSampleRate) || (m_spectrumRate != m_settings.m_spectrumRate) || force) + { m_interpolatorDistanceRemain = 0; m_interpolatorConsumed = false; m_interpolatorDistance = (Real) channelSampleRate / (Real) m_settings.m_spectrumRate; @@ -302,6 +382,11 @@ void PacketModSource::applyChannelSettings(int channelSampleRate, int channelFre m_channelSampleRate = channelSampleRate; m_channelFrequencyOffset = channelFrequencyOffset; + m_spectrumRate = m_settings.m_spectrumRate; + m_samplesPerSymbol = m_channelSampleRate / m_settings.m_baud; + qDebug() << "m_samplesPerSymbol: " << m_samplesPerSymbol << " (" << m_channelSampleRate << "/" << m_settings.m_baud << ")"; + // Precalculate FM sensensity to save doing it in the loop + m_phaseSensitivity = 2.0f * M_PI * m_settings.m_fmDeviation / (double)m_channelSampleRate; } static uint8_t *ax25_address(uint8_t *p, QString address, uint8_t crrl) @@ -395,7 +480,7 @@ void PacketModSource::initTX() m_byteIdx = 0; m_bitIdx = 0; m_bitCount = m_bitCountTotal; // Reset to allow retransmission - m_f = m_settings.m_spaceFrequency; + m_nrziBit = 0; if (m_settings.m_rampUpBits == 0) { m_state = tx; @@ -407,6 +492,7 @@ void PacketModSource::initTX() m_pow = -(Real)m_settings.m_rampRange; m_powRamp = m_settings.m_rampRange/(m_settings.m_rampUpBits * (Real)m_samplesPerSymbol); } + m_scrambler.init(); } void PacketModSource::addTXPacket(QString callsign, QString to, QString via, QString data) @@ -475,8 +561,10 @@ void PacketModSource::addTXPacket(QString callsign, QString to, QString via, QSt // single tone m_sampleIdx = 0; m_audioPhase = 0.0f; - m_fmPhase = 0.0f; + m_fmPhase = 0.0; if (m_settings.m_writeToFile) m_audioFile.open("packetmod.csv", std::ofstream::out); + else if (m_audioFile.is_open()) + m_audioFile.close(); } diff --git a/plugins/channeltx/modpacket/packetmodsource.h b/plugins/channeltx/modpacket/packetmodsource.h index d48cd36a0..6f23ab803 100644 --- a/plugins/channeltx/modpacket/packetmodsource.h +++ b/plugins/channeltx/modpacket/packetmodsource.h @@ -31,7 +31,10 @@ #include "dsp/interpolator.h" #include "dsp/lowpass.h" #include "dsp/bandpass.h" +#include "dsp/highpass.h" +#include "dsp/raisedcosine.h" #include "dsp/fmpreemphasis.h" +#include "util/lfsr.h" #include "util/movingaverage.h" #include "packetmodsettings.h" @@ -69,13 +72,20 @@ public: private: int m_channelSampleRate; int m_channelFrequencyOffset; + int m_spectrumRate; PacketModSettings m_settings; NCO m_carrierNco; Real m_audioPhase; - Real m_fmPhase; + double m_fmPhase; // Double gives cleaner spectrum than Real + double m_phaseSensitivity; + Real m_linearGain; Complex m_modSample; + int m_nrziBit; // Output of NRZI coder + int m_scrambledBit; // Output from scrambler to be pulse shaped + RaisedCosine m_pulseShape; // Pulse shaping filter + Bandpass m_bandpass; // Baseband bandpass filter for AFSK Lowpass m_lowpass; // Low pass filter to limit RF bandwidth FMPreemphasis m_preemphasisFilter; // FM preemphasis filter to amplify high frequencies @@ -97,7 +107,6 @@ private: static const int m_levelNbSamples = 480; // every 10ms assuming 48k Sa/s - int m_f; // Current frequency int m_sampleIdx; // Sample index in to symbol int m_samplesPerSymbol; // Number of samples per symbol Real m_pow; // In dB @@ -115,6 +124,8 @@ private: int m_bitCount; // Count of number of valid bits in m_bits int m_bitCountTotal; + LFSR m_scrambler; // Scrambler + std::ofstream m_audioFile; // For debug output of baseband waveform bool bitsValid(); // Are there and bits to transmit diff --git a/plugins/channeltx/modpacket/packetmodtxsettingsdialog.cpp b/plugins/channeltx/modpacket/packetmodtxsettingsdialog.cpp index d3a09193a..b4564e5c0 100644 --- a/plugins/channeltx/modpacket/packetmodtxsettingsdialog.cpp +++ b/plugins/channeltx/modpacket/packetmodtxsettingsdialog.cpp @@ -18,8 +18,10 @@ #include "packetmodtxsettingsdialog.h" PacketModTXSettingsDialog::PacketModTXSettingsDialog(int rampUpBits, int rampDownBits, - int rampRange, bool modulateWhileRamping, + int rampRange, bool modulateWhileRamping, int modulation, int baud, int markFrequency, int spaceFrequency, + bool pulseShaping, float beta, int symbolSpan, + bool scramble, int polynomial, int ax25PreFlags, int ax25PostFlags, int ax25Control, int ax25PID, int lpfTaps, bool bbNoise, bool rfNoise, bool writeToFile, @@ -32,8 +34,15 @@ PacketModTXSettingsDialog::PacketModTXSettingsDialog(int rampUpBits, int rampDow ui->rampDown->setValue(rampDownBits); ui->rampRange->setValue(rampRange); ui->modulateWhileRamping->setChecked(modulateWhileRamping); + ui->modulation->setCurrentIndex(modulation); + ui->baud->setValue(baud); ui->markFrequency->setValue(markFrequency); + ui->pulseShaping->setChecked(pulseShaping); + ui->beta->setValue(beta); + ui->symbolSpan->setValue(symbolSpan); ui->spaceFrequency->setValue(spaceFrequency); + ui->scramble->setChecked(scramble); + ui->polynomial->setValue(polynomial); ui->ax25PreFlags->setValue(ax25PreFlags); ui->ax25PostFlags->setValue(ax25PostFlags); ui->ax25Control->setValue(ax25Control); @@ -55,8 +64,15 @@ void PacketModTXSettingsDialog::accept() m_rampDownBits = ui->rampDown->value(); m_rampRange = ui->rampRange->value(); m_modulateWhileRamping = ui->modulateWhileRamping->isChecked(); + m_modulation = ui->modulation->currentIndex(); + m_baud = ui->baud->value(); m_markFrequency = ui->markFrequency->value(); m_spaceFrequency = ui->spaceFrequency->value(); + m_pulseShaping = ui->pulseShaping->isChecked(); + m_beta = ui->beta->value(); + m_symbolSpan = ui->symbolSpan->value(); + m_scramble = ui->scramble->isChecked(); + m_polynomial = ui->polynomial->value(); m_ax25PreFlags = ui->ax25PreFlags->value(); m_ax25PostFlags = ui->ax25PostFlags->value(); m_ax25Control = ui->ax25Control->value(); diff --git a/plugins/channeltx/modpacket/packetmodtxsettingsdialog.h b/plugins/channeltx/modpacket/packetmodtxsettingsdialog.h index 7889ba625..9ef800408 100644 --- a/plugins/channeltx/modpacket/packetmodtxsettingsdialog.h +++ b/plugins/channeltx/modpacket/packetmodtxsettingsdialog.h @@ -25,8 +25,10 @@ class PacketModTXSettingsDialog : public QDialog { public: explicit PacketModTXSettingsDialog(int rampUpBits, int rampDownBits, int rampRange, - bool modulateWhileRamping, + bool modulateWhileRamping, int modulation, int baud, int markFrequency, int spaceFrequency, + bool pulseShaping, float beta, int symbolSpan, + bool scramble, int polynomial, int ax25PreFlags, int ax25PostFlags, int ax25Control, int ax25PID, int lpfTaps, bool bbNoise, bool rfNoise, bool writeToFile, @@ -37,8 +39,15 @@ public: int m_rampDownBits; int m_rampRange; bool m_modulateWhileRamping; + int m_modulation; + int m_baud; int m_markFrequency; int m_spaceFrequency; + bool m_pulseShaping; + float m_beta; + int m_symbolSpan; + bool m_scramble; + int m_polynomial; int m_ax25PreFlags; int m_ax25PostFlags; int m_ax25Control; diff --git a/plugins/channeltx/modpacket/packetmodtxsettingsdialog.ui b/plugins/channeltx/modpacket/packetmodtxsettingsdialog.ui index f9a28efb7..592c8be52 100644 --- a/plugins/channeltx/modpacket/packetmodtxsettingsdialog.ui +++ b/plugins/channeltx/modpacket/packetmodtxsettingsdialog.ui @@ -7,7 +7,7 @@ 0 0 351 - 449 + 849 @@ -21,8 +21,335 @@ - + + + AX.25 Protocol Settings + + + + + + AX.25 preamble flags + + + + + + + Number of flags to be transmitted before the frame. This gives more time for a receiver to unmute. + + + 1 + + + 1024 + + + + + + + AX.25 postamble flags + + + + + + + Number of flags to be transmitted after the frame. + + + 1 + + + 1024 + + + + + + + AX.25 control + + + + + + + Value of control field in AX.25 frame. + + + 0x + + + 255 + + + 3 + + + 16 + + + + + + + AX.25 PID + + + + + + + Value of PID field in AX.25 frame. Use 0xf0 for no L3. + + + 0x + + + 255 + + + 240 + + + 16 + + + + + + + + + + Modulation + + + + + + Baud rate + + + + + + + Baud rate (symbols per second). + + + 100000 + + + 1200 + + + + + + + Modulaton type. + + + + AFSK + + + + + FSK + + + + + + + + Modulation + + + + + + + RF BW limit LPF taps + + + + + + + Number of taps in LPF for RF BW filter. + + + 10000 + + + + + + + + + + AFSK Modulation + + + + + + Frequency of tone to generate for a mark (1). + + + 24000 + + + 100 + + + + + + + Mark frequency (Hz) + + + + + + + Space frequency (Hz) + + + + + + + Frequency of tone to generate for a space (0). + + + 24000 + + + 100 + + + + + + + + + + FSK Modulation + + + + + + Enable raised cosine pulse shaping filter + + + Raised cosine pulse shaping + + + + + + + Roll-off of the filter + + + 1.000000000000000 + + + 0.250000000000000 + + + + + + + Number of symbols over which filter is applied + + + 1 + + + 20 + + + + + + + Filter rolloff (beta) + + + + + + + Filter symbol span + + + + + + + + + + Scrambing + + + + + Enabling scrambling. + + + Scramble + + + + + + + Polynomial + + + + + + + Polynomial of the scrambler. The +1 is implicit. + + + 0x + + + 2147483647 + + + 67584 + + + 16 + + + + + + + + + + Power Ramping + + @@ -68,7 +395,7 @@ - + Modulate during ramping. @@ -78,156 +405,26 @@ - - - - Mark frequency (Hz) - - - - - + + + + + + + Debug + + + + - Frequency of tone to generate for a mark (1). + Generate white noise as baseband signal. - - 24000 - - - 100 - - - - - - Space frequency (Hz) + Generate BB noise - - - - Frequency of tone to generate for a space (0). - - - 24000 - - - 100 - - - - - - - AX.25 preamble flags - - - - - - - Number of flags to be transmitted before the frame. This gives more time for a receiver to unmute. - - - 1 - - - 1024 - - - - - - - AX.25 postamble flags - - - - - - - Number of flags to be transmitted after the frame. - - - 1 - - - 1024 - - - - - - - AX.25 control - - - - - - - Value of control field in AX.25 frame. - - - 0x - - - 255 - - - 3 - - - 16 - - - - - - - AX.25 PID - - - - - - - Value of PID field in AX.25 frame. Use 0xf0 for no L3. - - - 0x - - - 255 - - - 240 - - - 16 - - - - - - - Lowpass taps - - - - - - - Number of taps in LPF for RF BW filter. - - - 10000 - - - - + Generate white noise as RF signal. @@ -237,7 +434,7 @@ - + @@ -253,16 +450,6 @@ - - - - Generate white noise as baseband signal. - - - Generate BB noise - - - @@ -278,22 +465,6 @@ - - rampUp - rampDown - rampRange - modulateWhileRamping - markFrequency - spaceFrequency - ax25PreFlags - ax25PostFlags - ax25Control - ax25PID - lpfTaps - bbNoise - rfNoise - writeToFile - diff --git a/plugins/channeltx/modpacket/readme.md b/plugins/channeltx/modpacket/readme.md index bc5b5c080..3b4dcbdd1 100644 --- a/plugins/channeltx/modpacket/readme.md +++ b/plugins/channeltx/modpacket/readme.md @@ -22,7 +22,7 @@ Use this button to toggle mute for this channel.

4: Modulation

-This specifies the baud rate and modulation that is used for the packet transmission. Currently 1200 baud AFSK is supported. +This specifies the baud rate and modulation that is used for the packet transmission. Currently 1200 baud AFSK and 9600 baud FSK are supported.

5: RF Bandwidth

@@ -50,29 +50,33 @@ Enter your amateur radio callsign and optionally a sub-station ID (SSID). E.g. M Check this button to enable a FM preemphasis filter, which amplifiers higher frequencies. Right click to open the dialog to adjust settings for the filter. -

11: Repeat

+

11: Bandpass Filter

+ +Check this button to enable a baseband bandpass filter. Right click to open the dialog to adjust settings for the filter. + +

12: Repeat

Check this button to repeated transmit a packet. Right click to open the dialog to adjust the delay between retransmission and number of times the packet should be repeated. -

12: Insertion position

+

13: Insertion position

Inserts position as APRS formatted latitude and longitude in to the current cursor position within the data field. Lattitude and longitude can be specified under Preferences > My position. -

13: To

+

14: To

Enter the destination for the packet. To send the packet to the APRS network, use APRS or APZ. -

14: Via

+

15: Via

Enter the routing for the packet. To have the packet repeated by digipeaters, use WIDE2-2. To have the packet repeated by the International Space Station (ISS), use ARISS. -

15: Data

+

16: Data

-The packet of data to send. To send an APRS status message, use the format >Status. APRS messages can be tracked on https://aprs.fi +The packet of data to send. To send an APRS status message, use the format >Status. The APRS specification can be found at: http://www.aprs.org/doc/APRS101.PDF. APRS messages can be tracked on https://aprs.fi -

16: TX

+

17: TX

-Transmits a packet based on the current values in callsign, to, via and data. +Transmits a packet containing the current values in callsign, to, via and data fields.

API

@@ -80,6 +84,6 @@ Full details of the API can be found in the Swagger documentation. Here is a qui curl -X POST "http://127.0.0.1:8091/sdrangel/deviceset/1/channel/0/actions" -d '{"channelType": "PacketMod", "direction": 1, "PacketModActions": { "tx": { "callsign": "MYCALL", "to": "APRS", "via": "WIDE2-2", "data": ">Using SDRangel API to transmit" }}}' -Or to set the frequency deviation: +Or to set the mode to 9600 FSK: - curl -X PATCH "http://127.0.0.1:8091/sdrangel/deviceset/1/channel/0/settings" -d '{"channelType": "PacketMod", "direction": 1, "PacketModSettings": {"fmDeviation": 5000}}' + curl -X PATCH "http://127.0.0.1:8091/sdrangel/deviceset/1/channel/0/settings" -d '{"channelType": "PacketMod", "direction": 1, "PacketModSettings": {"mode": "9600 FSK"}}' diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 1dc6811ee..c80c15a2a 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -147,6 +147,7 @@ set(sdrbase_SOURCES util/CRC64.cpp util/db.cpp util/fixedtraits.cpp + util/lfsr.cpp util/message.cpp util/messagequeue.cpp util/prettyprint.cpp @@ -265,6 +266,7 @@ set(sdrbase_HEADERS dsp/phaselock.h dsp/phaselockcomplex.h dsp/projector.h + dsp/raisedcosine.h dsp/recursivefilters.h dsp/samplemififo.h dsp/samplemofifo.h @@ -305,6 +307,7 @@ set(sdrbase_HEADERS util/fixedtraits.h util/incrementalarray.h util/incrementalvector.h + util/lfsr.h util/message.h util/messagequeue.h util/movingaverage.h diff --git a/sdrbase/dsp/raisedcosine.h b/sdrbase/dsp/raisedcosine.h new file mode 100644 index 000000000..0816a912f --- /dev/null +++ b/sdrbase/dsp/raisedcosine.h @@ -0,0 +1,136 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RAISEDCOSINE_H +#define INCLUDE_RAISEDCOSINE_H + +#define _USE_MATH_DEFINES +#include +#include "dsp/dsptypes.h" + +// Raised-cosine low-pass filter for pulse shaping, without intersymbol interference (ISI) +// https://en.wikipedia.org/wiki/Raised-cosine_filter +// This could be optimised in to a polyphase filter, as samplesPerSymbol-1 inputs +// to filter() should be zero, as the data is upsampled to the sample rate +template class RaisedCosine { +public: + RaisedCosine() : m_ptr(0) { } + + // beta - roll-off factor + // symbolSpan - number of symbols over which the filter is spread + // samplesPerSymbol - number of samples per symbol + void create(double beta, int symbolSpan, int samplesPerSymbol) + { + int nTaps = symbolSpan * samplesPerSymbol + 1; + int i; + + // check constraints + if(!(nTaps & 1)) { + qDebug("Raised cosine filter has to have an odd number of taps"); + nTaps++; + } + + // make room + m_samples.resize(nTaps); + for(int i = 0; i < nTaps; i++) + m_samples[i] = 0; + m_ptr = 0; + m_taps.resize(nTaps / 2 + 1); + + // calculate filter taps + for(i = 0; i < nTaps / 2 + 1; i++) + { + double t = (i - (nTaps / 2)) / (double)samplesPerSymbol; + double denominator = 1.0 - std::pow(2.0 * beta * t, 2.0); + double sinc; + + if (denominator != 0.0) + { + if (t == 0) + sinc = 1.0; + else + sinc = sin(M_PI*t)/(M_PI*t); + m_taps[i] = sinc * (cos(M_PI*beta*t) / denominator) / (double)samplesPerSymbol; + } + else + m_taps[i] = beta * sin(M_PI/(2.0*beta)) / (2.0*samplesPerSymbol); + } + + // normalize + double sum = 0; + for(i = 0; i < (int)m_taps.size() - 1; i++) + sum += std::pow(m_taps[i], 2.0) * 2; + sum += std::pow(m_taps[i], 2.0); + sum = std::sqrt(sum); + for(i = 0; i < (int)m_taps.size(); i++) + m_taps[i] /= sum; + } + + Type filter(Type sample) + { + Type acc = 0; + int a = m_ptr; + int b = a - 1; + int i, n_taps, size; + + m_samples[m_ptr] = sample; + size = m_samples.size(); // Valgrind optim (2) + + while (b < 0) + { + b += size; + } + + n_taps = m_taps.size() - 1; // Valgrind optim + + for (i = 0; i < n_taps; i++) + { + acc += (m_samples[a] + m_samples[b]) * m_taps[i]; + a++; + + while (a >= size) + { + a -= size; + } + + b--; + + while(b < 0) + { + b += size; + } + } + + acc += m_samples[a] * m_taps[i]; + m_ptr++; + + while(m_ptr >= size) + { + m_ptr -= size; + } + + return acc; + } + +private: + std::vector m_taps; + std::vector m_samples; + int m_ptr; +}; + +#endif // INCLUDE_RAISEDCOSINE_H diff --git a/sdrbase/resources/webapi/doc/html2/index.html b/sdrbase/resources/webapi/doc/html2/index.html index 994b0eb4f..bbb0fb7bf 100644 --- a/sdrbase/resources/webapi/doc/html2/index.html +++ b/sdrbase/resources/webapi/doc/html2/index.html @@ -4932,6 +4932,10 @@ margin-bottom: 20px; "type" : "integer", "format" : "int64" }, + "mode" : { + "type" : "string", + "description" : "Transmission mode. \"1200 AFSK\" or \"9600 FSK\"." + }, "rfBandwidth" : { "type" : "number", "format" : "float" @@ -4974,6 +4978,17 @@ margin-bottom: 20px; "type" : "number", "format" : "float" }, + "bpf" : { + "type" : "integer" + }, + "bpfLowCutoff" : { + "type" : "number", + "format" : "float" + }, + "bpfHighCutoff" : { + "type" : "number", + "format" : "float" + }, "rgbColor" : { "type" : "integer" }, @@ -33454,7 +33469,7 @@ except ApiException as e:
- Generated 2020-09-18T15:59:26.503+02:00 + Generated 2020-09-23T09:56:01.490+02:00
diff --git a/sdrbase/util/lfsr.cpp b/sdrbase/util/lfsr.cpp new file mode 100644 index 000000000..459a43dac --- /dev/null +++ b/sdrbase/util/lfsr.cpp @@ -0,0 +1,106 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "lfsr.h" +#include "popcount.h" + +// Shift LFSR one bit +int LFSR::shift() +{ + int bit; + + bit = (popcount(m_sr & m_polynomial) & 1) ^ 1; + m_sr = (m_sr << 1) | bit; + return bit; +} + +// Scramble a single bit +int LFSR::scramble(int bit_in) +{ + int bit_out; + + bit_out = (popcount(m_sr & m_polynomial) & 1) ^ bit_in; + m_sr = (m_sr << 1) | bit_out; + return bit_out; +} + #include + +// Scramble data using LFSR - LSB first +void LFSR::scramble(uint8_t *data, int length) +{ + uint8_t byte_in, byte_out; + int bit_in, bit_out; + + for(int i = 0; i < length; i++) + { + byte_in = data[i]; + byte_out = 0; + for (int j = 0; j < 8; j++) + { + bit_in = (byte_in >> j) & 1; + bit_out = (popcount(m_sr & m_polynomial) & 1) ^ bit_in; + m_sr = (m_sr << 1) | bit_out; + byte_out = byte_out | (bit_out << j); + } + data[i] = byte_out; + } +} + +// Descramble data using LFSR - LSB first +void LFSR::descramble(uint8_t *data, int length) +{ + uint8_t byte_in, byte_out; + int bit_in, bit_out; + + for(int i = 0; i < length; i++) + { + byte_in = data[i]; + byte_out = 0; + for (int j = 0; j < 8; j++) + { + bit_in = (byte_in >> j) & 1; + bit_out = (popcount(m_sr & m_polynomial) & 1) ^ bit_in; + m_sr = (m_sr << 1) | bit_in; + byte_out = byte_out | (bit_out << j); + } + data[i] = byte_out; + } +} + +// XOR data with rand_bit of LFSR - LSB first +void LFSR::randomize(uint8_t *data, int length) +{ + uint8_t byte_in, byte_out; + int bit_in, bit_out, bit; + + for(int i = 0; i < length; i++) + { + byte_in = data[i]; + byte_out = 0; + for (int j = 0; j < 8; j++) + { + // XOR input bit with specified bit from SR + bit_in = (byte_in >> j) & 1; + bit_out = ((m_sr >> m_rand_bit) & 1) ^ bit_in; + byte_out = byte_out | (bit_out << j); + // Update LFSR + bit = popcount(m_sr & m_polynomial) & 1; + m_sr = (m_sr << 1) | bit; + } + data[i] = byte_out; + } +} diff --git a/sdrbase/util/lfsr.h b/sdrbase/util/lfsr.h new file mode 100644 index 000000000..b487fbbac --- /dev/null +++ b/sdrbase/util/lfsr.h @@ -0,0 +1,105 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_LFSR_H +#define INCLUDE_LFSR_H + +#include + +#include "export.h" + +// Linear feedback shift register that can be used for scrambling or generating +// PN (Pseudo Noise) random sequence. +class SDRBASE_API LFSR +{ +public: + // Create and initialise LFSR with specified number of bits, polynomial and + // initial state (which must be non-zero, unless a multiplicative scrambler). + // The +1 is implicit in the polynomial so x^1 + 1 should be passed as 0x01 + LFSR(uint32_t polynomial, uint32_t init_value = ~0U, int rand_bit = -1) : + m_rand_bit(rand_bit), + m_polynomial(polynomial), + m_init_value(init_value) + { + init(); + } + + // Initialise LFSR state + void init() + { + m_sr = m_init_value; + } + + // Shift the LFSR one bit and return output of XOR + int shift(); + + // Multiplicative scramble a single bit using LFSR + int scramble(int bit_in); + + // Multiplicative scramble of data using LFSR - LSB first + void scramble(uint8_t *data, int length); + // Descramble data using LFSR - LSB first + void descramble(uint8_t *data, int length); + + // XOR data with rand_bit from LFSR generating pseudo noise (PN) sequence - LSB first + void randomize(uint8_t *data, int length); + + // Get current shift-register value + uint32_t getSR() + { + return m_sr; + } + + // Set the polynomial + void setPolynomial(uint32_t polynomial) + { + m_polynomial = polynomial; + } + + // Get the polynomial + uint32_t getPolynomial() + { + return m_polynomial; + } + +private: + int m_rand_bit; // Which bit from the SR to use in randomize() + uint32_t m_polynomial; // Polynomial coefficients (+1 is implicit) + uint32_t m_init_value; // Value to initialise SR to when init() is called + uint32_t m_sr; // Shift register +}; + +// http://www.jrmiller.demon.co.uk/products/figs/man9k6.pdf +// In Matlab: comm.Scrambler(2, '1 + z^-12 + z^-17', 0) +// Call scramble() +class SDRBASE_API ScramblerG3RUG : public LFSR +{ +public: + ScramblerG3RUG() : LFSR(0x10800, 0x0) {} +}; + +// https://public.ccsds.org/Pubs/131x0b3e1.pdf +// x^8+x^7+x^5+x^3+1 +// In Matlab: comm.PNSequence('Polynomial', 'x^8+x^7+x^5+x^3+1', 'InitialConditions', [1 1 1 1 1 1 1 1]) +// Call randomize() +class SDRBASE_API RandomizeCCSDS : public LFSR +{ +public: + RandomizeCCSDS() : LFSR(0x95, 0xff, 7) {} +}; + +#endif diff --git a/sdrbase/util/popcount.h b/sdrbase/util/popcount.h new file mode 100644 index 000000000..cd7c5ac17 --- /dev/null +++ b/sdrbase/util/popcount.h @@ -0,0 +1,40 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_POPCOUNT_H +#define INCLUDE_POPCOUNT_H + +// Population count - count number of bits +#if defined(__cplusplus) && (__cplusplus >= 202002L) +#include +#define popcount std::popcount +#elif defined (__GNUC__) +#define popcount __builtin_popcount +#elif defined(_MSC_VER) +#include +#define popcount __popcnt +#else +static int popcount(int in) +{ + int cnt = 0; + for(int i = 0; i < 32; i++) + cnt += (in >> i) & 1; + return cnt; +} +#endif + +#endif /* INCLUDE_POPCOUNT_H */ diff --git a/swagger/sdrangel/api/swagger/include/PacketMod.yaml b/swagger/sdrangel/api/swagger/include/PacketMod.yaml index 06e3236fe..99e4ce4c8 100644 --- a/swagger/sdrangel/api/swagger/include/PacketMod.yaml +++ b/swagger/sdrangel/api/swagger/include/PacketMod.yaml @@ -4,6 +4,9 @@ PacketModSettings: inputFrequencyOffset: type: integer format: int64 + mode: + description: Transmission mode. "1200 AFSK" or "9600 FSK". + type: string rfBandwidth: type: number format: float @@ -34,6 +37,14 @@ PacketModSettings: preEmphasisHighFreq: type: number format: float + bpf: + type: integer + bpfLowCutoff: + type: number + format: float + bpfHighCutoff: + type: number + format: float rgbColor: type: integer title: diff --git a/swagger/sdrangel/code/html2/index.html b/swagger/sdrangel/code/html2/index.html index 994b0eb4f..bbb0fb7bf 100644 --- a/swagger/sdrangel/code/html2/index.html +++ b/swagger/sdrangel/code/html2/index.html @@ -4932,6 +4932,10 @@ margin-bottom: 20px; "type" : "integer", "format" : "int64" }, + "mode" : { + "type" : "string", + "description" : "Transmission mode. \"1200 AFSK\" or \"9600 FSK\"." + }, "rfBandwidth" : { "type" : "number", "format" : "float" @@ -4974,6 +4978,17 @@ margin-bottom: 20px; "type" : "number", "format" : "float" }, + "bpf" : { + "type" : "integer" + }, + "bpfLowCutoff" : { + "type" : "number", + "format" : "float" + }, + "bpfHighCutoff" : { + "type" : "number", + "format" : "float" + }, "rgbColor" : { "type" : "integer" }, @@ -33454,7 +33469,7 @@ except ApiException as e:
- Generated 2020-09-18T15:59:26.503+02:00 + Generated 2020-09-23T09:56:01.490+02:00
diff --git a/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.cpp index ea40165fe..51360b8d7 100644 --- a/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.cpp @@ -30,6 +30,8 @@ SWGPacketModSettings::SWGPacketModSettings(QString* json) { SWGPacketModSettings::SWGPacketModSettings() { input_frequency_offset = 0L; m_input_frequency_offset_isSet = false; + mode = nullptr; + m_mode_isSet = false; rf_bandwidth = 0.0f; m_rf_bandwidth_isSet = false; fm_deviation = 0.0f; @@ -54,6 +56,12 @@ SWGPacketModSettings::SWGPacketModSettings() { m_pre_emphasis_tau_isSet = false; pre_emphasis_high_freq = 0.0f; m_pre_emphasis_high_freq_isSet = false; + bpf = 0; + m_bpf_isSet = false; + bpf_low_cutoff = 0.0f; + m_bpf_low_cutoff_isSet = false; + bpf_high_cutoff = 0.0f; + m_bpf_high_cutoff_isSet = false; rgb_color = 0; m_rgb_color_isSet = false; title = nullptr; @@ -80,6 +88,8 @@ void SWGPacketModSettings::init() { input_frequency_offset = 0L; m_input_frequency_offset_isSet = false; + mode = new QString(""); + m_mode_isSet = false; rf_bandwidth = 0.0f; m_rf_bandwidth_isSet = false; fm_deviation = 0.0f; @@ -104,6 +114,12 @@ SWGPacketModSettings::init() { m_pre_emphasis_tau_isSet = false; pre_emphasis_high_freq = 0.0f; m_pre_emphasis_high_freq_isSet = false; + bpf = 0; + m_bpf_isSet = false; + bpf_low_cutoff = 0.0f; + m_bpf_low_cutoff_isSet = false; + bpf_high_cutoff = 0.0f; + m_bpf_high_cutoff_isSet = false; rgb_color = 0; m_rgb_color_isSet = false; title = new QString(""); @@ -125,6 +141,12 @@ SWGPacketModSettings::init() { void SWGPacketModSettings::cleanup() { + if(mode != nullptr) { + delete mode; + } + + + @@ -164,6 +186,8 @@ void SWGPacketModSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + ::SWGSDRangel::setValue(&mode, pJson["mode"], "QString", "QString"); + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); ::SWGSDRangel::setValue(&fm_deviation, pJson["fmDeviation"], "float", ""); @@ -188,6 +212,12 @@ SWGPacketModSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&pre_emphasis_high_freq, pJson["preEmphasisHighFreq"], "float", ""); + ::SWGSDRangel::setValue(&bpf, pJson["bpf"], "qint32", ""); + + ::SWGSDRangel::setValue(&bpf_low_cutoff, pJson["bpfLowCutoff"], "float", ""); + + ::SWGSDRangel::setValue(&bpf_high_cutoff, pJson["bpfHighCutoff"], "float", ""); + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); @@ -223,6 +253,9 @@ SWGPacketModSettings::asJsonObject() { if(m_input_frequency_offset_isSet){ obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); } + if(mode != nullptr && *mode != QString("")){ + toJsonValue(QString("mode"), mode, obj, QString("QString")); + } if(m_rf_bandwidth_isSet){ obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); } @@ -259,6 +292,15 @@ SWGPacketModSettings::asJsonObject() { if(m_pre_emphasis_high_freq_isSet){ obj->insert("preEmphasisHighFreq", QJsonValue(pre_emphasis_high_freq)); } + if(m_bpf_isSet){ + obj->insert("bpf", QJsonValue(bpf)); + } + if(m_bpf_low_cutoff_isSet){ + obj->insert("bpfLowCutoff", QJsonValue(bpf_low_cutoff)); + } + if(m_bpf_high_cutoff_isSet){ + obj->insert("bpfHighCutoff", QJsonValue(bpf_high_cutoff)); + } if(m_rgb_color_isSet){ obj->insert("rgbColor", QJsonValue(rgb_color)); } @@ -297,6 +339,16 @@ SWGPacketModSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { this->m_input_frequency_offset_isSet = true; } +QString* +SWGPacketModSettings::getMode() { + return mode; +} +void +SWGPacketModSettings::setMode(QString* mode) { + this->mode = mode; + this->m_mode_isSet = true; +} + float SWGPacketModSettings::getRfBandwidth() { return rf_bandwidth; @@ -417,6 +469,36 @@ SWGPacketModSettings::setPreEmphasisHighFreq(float pre_emphasis_high_freq) { this->m_pre_emphasis_high_freq_isSet = true; } +qint32 +SWGPacketModSettings::getBpf() { + return bpf; +} +void +SWGPacketModSettings::setBpf(qint32 bpf) { + this->bpf = bpf; + this->m_bpf_isSet = true; +} + +float +SWGPacketModSettings::getBpfLowCutoff() { + return bpf_low_cutoff; +} +void +SWGPacketModSettings::setBpfLowCutoff(float bpf_low_cutoff) { + this->bpf_low_cutoff = bpf_low_cutoff; + this->m_bpf_low_cutoff_isSet = true; +} + +float +SWGPacketModSettings::getBpfHighCutoff() { + return bpf_high_cutoff; +} +void +SWGPacketModSettings::setBpfHighCutoff(float bpf_high_cutoff) { + this->bpf_high_cutoff = bpf_high_cutoff; + this->m_bpf_high_cutoff_isSet = true; +} + qint32 SWGPacketModSettings::getRgbColor() { return rgb_color; @@ -505,6 +587,9 @@ SWGPacketModSettings::isSet(){ if(m_input_frequency_offset_isSet){ isObjectUpdated = true; break; } + if(mode && *mode != QString("")){ + isObjectUpdated = true; break; + } if(m_rf_bandwidth_isSet){ isObjectUpdated = true; break; } @@ -541,6 +626,15 @@ SWGPacketModSettings::isSet(){ if(m_pre_emphasis_high_freq_isSet){ isObjectUpdated = true; break; } + if(m_bpf_isSet){ + isObjectUpdated = true; break; + } + if(m_bpf_low_cutoff_isSet){ + isObjectUpdated = true; break; + } + if(m_bpf_high_cutoff_isSet){ + isObjectUpdated = true; break; + } if(m_rgb_color_isSet){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.h b/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.h index 15b1afae8..35d0fb34f 100644 --- a/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGPacketModSettings.h @@ -45,6 +45,9 @@ public: qint64 getInputFrequencyOffset(); void setInputFrequencyOffset(qint64 input_frequency_offset); + QString* getMode(); + void setMode(QString* mode); + float getRfBandwidth(); void setRfBandwidth(float rf_bandwidth); @@ -81,6 +84,15 @@ public: float getPreEmphasisHighFreq(); void setPreEmphasisHighFreq(float pre_emphasis_high_freq); + qint32 getBpf(); + void setBpf(qint32 bpf); + + float getBpfLowCutoff(); + void setBpfLowCutoff(float bpf_low_cutoff); + + float getBpfHighCutoff(); + void setBpfHighCutoff(float bpf_high_cutoff); + qint32 getRgbColor(); void setRgbColor(qint32 rgb_color); @@ -112,6 +124,9 @@ private: qint64 input_frequency_offset; bool m_input_frequency_offset_isSet; + QString* mode; + bool m_mode_isSet; + float rf_bandwidth; bool m_rf_bandwidth_isSet; @@ -148,6 +163,15 @@ private: float pre_emphasis_high_freq; bool m_pre_emphasis_high_freq_isSet; + qint32 bpf; + bool m_bpf_isSet; + + float bpf_low_cutoff; + bool m_bpf_low_cutoff_isSet; + + float bpf_high_cutoff; + bool m_bpf_high_cutoff_isSet; + qint32 rgb_color; bool m_rgb_color_isSet;