From 081e81e9d943b4220ee9dab5bb5fc38f670bc3fc Mon Sep 17 00:00:00 2001 From: f4exb Date: Thu, 6 Aug 2020 10:46:49 +0200 Subject: [PATCH] New FileSink channel plugin --- doc/img/FileSink_plugin.png | Bin 0 -> 48870 bytes doc/img/FileSink_plugin.xcf | Bin 0 -> 244773 bytes plugins/channelrx/filesink/CMakeLists.txt | 61 ++ plugins/channelrx/filesink/filesink.cpp | 607 ++++++++++++++++++ plugins/channelrx/filesink/filesink.h | 155 +++++ .../channelrx/filesink/filesinkbaseband.cpp | 248 +++++++ plugins/channelrx/filesink/filesinkbaseband.h | 131 ++++ plugins/channelrx/filesink/filesinkgui.cpp | 590 +++++++++++++++++ plugins/channelrx/filesink/filesinkgui.h | 118 ++++ plugins/channelrx/filesink/filesinkgui.ui | 584 +++++++++++++++++ .../channelrx/filesink/filesinkmessages.cpp | 23 + plugins/channelrx/filesink/filesinkmessages.h | 108 ++++ plugins/channelrx/filesink/filesinkplugin.cpp | 85 +++ plugins/channelrx/filesink/filesinkplugin.h | 50 ++ .../channelrx/filesink/filesinksettings.cpp | 173 +++++ plugins/channelrx/filesink/filesinksettings.h | 60 ++ plugins/channelrx/filesink/filesinksink.cpp | 305 +++++++++ plugins/channelrx/filesink/filesinksink.h | 86 +++ .../filesink/filesinkwebapiadapter.cpp | 51 ++ .../filesink/filesinkwebapiadapter.h | 49 ++ plugins/channelrx/filesink/readme.md | 99 +++ sdrbase/dsp/filerecord.cpp | 41 +- sdrbase/dsp/filerecord.h | 11 +- sdrbase/dsp/filerecordinterface.cpp | 8 +- 24 files changed, 3618 insertions(+), 25 deletions(-) create mode 100644 doc/img/FileSink_plugin.png create mode 100644 doc/img/FileSink_plugin.xcf create mode 100644 plugins/channelrx/filesink/CMakeLists.txt create mode 100644 plugins/channelrx/filesink/filesink.cpp create mode 100644 plugins/channelrx/filesink/filesink.h create mode 100644 plugins/channelrx/filesink/filesinkbaseband.cpp create mode 100644 plugins/channelrx/filesink/filesinkbaseband.h create mode 100644 plugins/channelrx/filesink/filesinkgui.cpp create mode 100644 plugins/channelrx/filesink/filesinkgui.h create mode 100644 plugins/channelrx/filesink/filesinkgui.ui create mode 100644 plugins/channelrx/filesink/filesinkmessages.cpp create mode 100644 plugins/channelrx/filesink/filesinkmessages.h create mode 100644 plugins/channelrx/filesink/filesinkplugin.cpp create mode 100644 plugins/channelrx/filesink/filesinkplugin.h create mode 100644 plugins/channelrx/filesink/filesinksettings.cpp create mode 100644 plugins/channelrx/filesink/filesinksettings.h create mode 100644 plugins/channelrx/filesink/filesinksink.cpp create mode 100644 plugins/channelrx/filesink/filesinksink.h create mode 100644 plugins/channelrx/filesink/filesinkwebapiadapter.cpp create mode 100644 plugins/channelrx/filesink/filesinkwebapiadapter.h create mode 100644 plugins/channelrx/filesink/readme.md diff --git a/doc/img/FileSink_plugin.png b/doc/img/FileSink_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..837c8001b83614748f1258c2547187fe039a5d49 GIT binary patch literal 48870 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 literal 0 HcmV?d00001 diff --git a/doc/img/FileSink_plugin.xcf b/doc/img/FileSink_plugin.xcf new file mode 100644 index 0000000000000000000000000000000000000000..4c644ad9144ec449a9bc9bd2226bc4676e898f23 GIT binary patch literal 244773 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�VFm6rFjeX9O{xIb52 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. // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#include + +#include +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGChannelReport.h" +#include "SWGChannelActions.h" + +#include "util/simpleserializer.h" +#include "dsp/dspcommands.h" +#include "dsp/dspdevicesourceengine.h" +#include "dsp/dspengine.h" +#include "dsp/devicesamplesource.h" +#include "dsp/hbfilterchainconverter.h" +#include "dsp/devicesamplemimo.h" +#include "device/deviceapi.h" + +#include "filesinkmessages.h" +#include "filesinkbaseband.h" +#include "filesink.h" + +MESSAGE_CLASS_DEFINITION(FileSink::MsgConfigureFileSink, Message) + +const QString FileSink::m_channelIdURI = "sdrangel.channel.filesink"; +const QString FileSink::m_channelId = "FileSink"; + +FileSink::FileSink(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_centerFrequency(0), + m_frequencyOffset(0), + m_spectrumVis(SDR_RX_SCALEF), + m_basebandSampleRate(48000) +{ + setObjectName(m_channelId); + + m_basebandSink = new FileSinkBaseband(); + m_basebandSink->setSpectrumSink(&m_spectrumVis); + m_basebandSink->moveToThread(&m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +FileSink::~FileSink() +{ + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +uint32_t FileSink::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void FileSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void FileSink::start() +{ + qDebug("FileSink::start"); + m_basebandSink->reset(); + m_basebandSink->setMessageQueueToGUI(getMessageQueueToGUI()); + m_basebandSink->setDeviceHwId(m_deviceAPI->getHardwareId()); + m_basebandSink->setDeviceUId(m_deviceAPI->getDeviceUID()); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + FileSinkBaseband::MsgConfigureFileSinkBaseband *msg = FileSinkBaseband::MsgConfigureFileSinkBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void FileSink::stop() +{ + qDebug("FileSink::stop"); + m_basebandSink->stopWork(); + m_thread.exit(); + m_thread.wait(); +} + +bool FileSink::handleMessage(const Message& cmd) +{ + if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& cfg = (DSPSignalNotification&) cmd; + + qDebug() << "FileSink::handleMessage: DSPSignalNotification:" + << " inputSampleRate: " << cfg.getSampleRate() + << " centerFrequency: " << cfg.getCenterFrequency(); + + m_basebandSampleRate = cfg.getSampleRate(); + m_centerFrequency = cfg.getCenterFrequency(); + DSPSignalNotification *notif = new DSPSignalNotification(cfg); + m_basebandSink->getInputMessageQueue()->push(notif); + + if (getMessageQueueToGUI()) + { + DSPSignalNotification *notifToGUI = new DSPSignalNotification(cfg); + getMessageQueueToGUI()->push(notifToGUI); + } + + return true; + } + else if (MsgConfigureFileSink::match(cmd)) + { + MsgConfigureFileSink& cfg = (MsgConfigureFileSink&) cmd; + qDebug() << "FileSink::handleMessage: MsgConfigureFileSink"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else + { + return false; + } +} + +QByteArray FileSink::serialize() const +{ + return m_settings.serialize(); +} + +bool FileSink::deserialize(const QByteArray& data) +{ + (void) data; + if (m_settings.deserialize(data)) + { + MsgConfigureFileSink *msg = MsgConfigureFileSink::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureFileSink *msg = MsgConfigureFileSink::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void FileSink::getLocalDevices(std::vector& indexes) +{ + indexes.clear(); + DSPEngine *dspEngine = DSPEngine::instance(); + + for (uint32_t i = 0; i < dspEngine->getDeviceSourceEnginesNumber(); i++) + { + DSPDeviceSourceEngine *deviceSourceEngine = dspEngine->getDeviceSourceEngineByIndex(i); + DeviceSampleSource *deviceSource = deviceSourceEngine->getSource(); + + if (deviceSource->getDeviceDescription() == "LocalInput") { + indexes.push_back(i); + } + } +} + +DeviceSampleSource *FileSink::getLocalDevice(uint32_t index) +{ + DSPEngine *dspEngine = DSPEngine::instance(); + + if (index < dspEngine->getDeviceSourceEnginesNumber()) + { + DSPDeviceSourceEngine *deviceSourceEngine = dspEngine->getDeviceSourceEngineByIndex(index); + DeviceSampleSource *deviceSource = deviceSourceEngine->getSource(); + + if (deviceSource->getDeviceDescription() == "LocalInput") + { + if (!getDeviceAPI()) { + qDebug("FileSink::getLocalDevice: the parent device is unset"); + } else if (getDeviceAPI()->getDeviceUID() == deviceSourceEngine->getUID()) { + qDebug("FileSink::getLocalDevice: source device at index %u is the parent device", index); + } else { + return deviceSource; + } + } + else + { + qDebug("FileSink::getLocalDevice: source device at index %u is not a SigMF File sink", index); + } + } + else + { + qDebug("FileSink::getLocalDevice: non existent source device index: %u", index); + } + + return nullptr; +} + +void FileSink::applySettings(const FileSinkSettings& settings, bool force) +{ + qDebug() << "FileSink::applySettings:" + << "m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << "m_log2Decim: " << settings.m_log2Decim + << "m_fileRecordName: " << settings.m_fileRecordName + << "force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_fileRecordName != m_settings.m_fileRecordName) || force) { + reverseAPIKeys.append("fileRecordName"); + } + if ((settings.m_rgbColor != m_settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + if ((settings.m_title != m_settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((settings.m_log2Decim != m_settings.m_log2Decim) || force) { + reverseAPIKeys.append("log2Decim"); + } + if ((settings.m_spectrumSquelchMode != m_settings.m_spectrumSquelchMode) || force) { + reverseAPIKeys.append("spectrumSquelchMode"); + } + if ((settings.m_spectrumSquelch != m_settings.m_spectrumSquelch) || force) { + reverseAPIKeys.append("spectrumSquelch"); + } + if ((settings.m_preRecordTime != m_settings.m_preRecordTime) || force) { + reverseAPIKeys.append("preRecordTime"); + } + if ((settings.m_squelchPostRecordTime != m_settings.m_squelchPostRecordTime) || force) { + reverseAPIKeys.append("squelchPostRecordTime"); + } + if ((settings.m_squelchRecordingEnable != m_settings.m_squelchRecordingEnable) || force) { + reverseAPIKeys.append("squelchRecordingEnable"); + } + + 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"); + } + + FileSinkBaseband::MsgConfigureFileSinkBaseband *msg = FileSinkBaseband::MsgConfigureFileSinkBaseband::create(settings, force); + m_basebandSink->getInputMessageQueue()->push(msg); + + if ((settings.m_useReverseAPI) && (reverseAPIKeys.size() != 0)) + { + 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); + } + + m_settings = settings; +} + +void FileSink::record(bool record) +{ + FileSinkBaseband::MsgConfigureFileSinkWork *msg = FileSinkBaseband::MsgConfigureFileSinkWork::create(record); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +uint64_t FileSink::getMsCount() const +{ + if (m_basebandSink) { + return m_basebandSink->getMsCount(); + } else { + return 0; + } +} + +uint64_t FileSink::getByteCount() const +{ + if (m_basebandSink) { + return m_basebandSink->getByteCount(); + } else { + return 0; + } +} + +unsigned int FileSink::getNbTracks() const +{ + if (m_basebandSink) { + return m_basebandSink->getNbTracks(); + } else { + return 0; + } +} + +int FileSink::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSigMfFileSinkSettings(new SWGSDRangel::SWGSigMFFileSinkSettings()); + response.getSigMfFileSinkSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int FileSink::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + FileSinkSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureFileSink *msg = MsgConfigureFileSink::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("FileSink::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureFileSink *msgToGUI = MsgConfigureFileSink::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +int FileSink::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSigMfFileSinkReport(new SWGSDRangel::SWGSigMFFileSinkReport()); + response.getSigMfFileSinkReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +int FileSink::webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage) +{ + SWGSDRangel::SWGSigMFFileSinkActions *swgSigMFFileSinkActions = query.getSigMfFileSinkActions(); + + if (swgSigMFFileSinkActions) + { + if (channelActionsKeys.contains("record")) + { + bool record = swgSigMFFileSinkActions->getRecord() != 0; + + if (!m_settings.m_squelchRecordingEnable) + { + FileSinkBaseband::MsgConfigureFileSinkWork *msg = FileSinkBaseband::MsgConfigureFileSinkWork::create(record); + m_basebandSink->getInputMessageQueue()->push(msg); + + if (getMessageQueueToGUI()) + { + FileSinkMessages::MsgReportRecording *msgToGUI = FileSinkMessages::MsgReportRecording::create(record); + getMessageQueueToGUI()->push(msgToGUI); + } + } + } + + return 202; + } + else + { + errorMessage = "Missing SigMFFileSinkActions in query"; + return 400; + } +} + +void FileSink::webapiUpdateChannelSettings( + FileSinkSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getSigMfFileSinkSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("fileRecordName")) { + settings.m_fileRecordName = *response.getSigMfFileSinkSettings()->getFileRecordName(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getSigMfFileSinkSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getSigMfFileSinkSettings()->getTitle(); + } + if (channelSettingsKeys.contains("log2Decim")) { + settings.m_log2Decim = response.getSigMfFileSinkSettings()->getLog2Decim(); + } + if (channelSettingsKeys.contains("spectrumSquelchMode")) { + settings.m_spectrumSquelchMode = response.getSigMfFileSinkSettings()->getSpectrumSquelchMode() != 0; + } + if (channelSettingsKeys.contains("spectrumSquelch")) { + settings.m_spectrumSquelch = response.getSigMfFileSinkSettings()->getSpectrumSquelch(); + } + if (channelSettingsKeys.contains("preRecordTime")) { + settings.m_preRecordTime = response.getSigMfFileSinkSettings()->getPreRecordTime(); + } + if (channelSettingsKeys.contains("squelchPostRecordTime")) { + settings.m_squelchPostRecordTime = response.getSigMfFileSinkSettings()->getSquelchPostRecordTime(); + } + if (channelSettingsKeys.contains("squelchRecordingEnable")) { + settings.m_squelchRecordingEnable = response.getSigMfFileSinkSettings()->getSquelchRecordingEnable() != 0; + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getSigMfFileSinkSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getSigMfFileSinkSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getSigMfFileSinkSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getSigMfFileSinkSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getSigMfFileSinkSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getSigMfFileSinkSettings()->getReverseApiChannelIndex(); + } + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_reverseAPIChannelIndex = response.getSigMfFileSinkSettings()->getInputFrequencyOffset(); + } +} + +void FileSink::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const FileSinkSettings& settings) +{ + response.getSigMfFileSinkSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + + if (response.getSigMfFileSinkSettings()->getFileRecordName()) { + *response.getSigMfFileSinkSettings()->getFileRecordName() = settings.m_fileRecordName; + } else { + response.getSigMfFileSinkSettings()->setFileRecordName(new QString(settings.m_fileRecordName)); + } + + response.getSigMfFileSinkSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getSigMfFileSinkSettings()->getTitle()) { + *response.getSigMfFileSinkSettings()->getTitle() = settings.m_title; + } else { + response.getSigMfFileSinkSettings()->setTitle(new QString(settings.m_title)); + } + + response.getSigMfFileSinkSettings()->setLog2Decim(settings.m_log2Decim); + response.getSigMfFileSinkSettings()->setSpectrumSquelchMode(settings.m_spectrumSquelchMode ? 1 : 0); + response.getSigMfFileSinkSettings()->setSpectrumSquelch(settings.m_spectrumSquelch); + response.getSigMfFileSinkSettings()->setPreRecordTime(settings.m_preRecordTime); + response.getSigMfFileSinkSettings()->setSquelchPostRecordTime(settings.m_squelchPostRecordTime); + response.getSigMfFileSinkSettings()->setSquelchRecordingEnable(settings.m_squelchRecordingEnable ? 1 : 0); + response.getSigMfFileSinkSettings()->setStreamIndex(settings.m_streamIndex); + response.getSigMfFileSinkSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getSigMfFileSinkSettings()->getReverseApiAddress()) { + *response.getSigMfFileSinkSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getSigMfFileSinkSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getSigMfFileSinkSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getSigMfFileSinkSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getSigMfFileSinkSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); +} + +void FileSink::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + response.getSigMfFileSinkReport()->setSpectrumSquelch(m_basebandSink->isSquelchOpen() ? 1 : 0); + response.getSigMfFileSinkReport()->setSpectrumMax(m_basebandSink->getSpecMax()); + response.getSigMfFileSinkReport()->setSinkSampleRate(m_basebandSink->getSinkSampleRate()); + response.getSigMfFileSinkReport()->setRecordTimeMs(getMsCount()); + response.getSigMfFileSinkReport()->setRecordSize(getByteCount()); + response.getSigMfFileSinkReport()->setRecording(m_basebandSink->isRecording() ? 1 : 0); + response.getSigMfFileSinkReport()->setRecordCaptures(getNbTracks()); + response.getSigMfFileSinkReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate()); +} + +void FileSink::webapiReverseSendSettings(QList& channelSettingsKeys, const FileSinkSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + swgChannelSettings->setDirection(0); // single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("FileSink")); + swgChannelSettings->setSigMfFileSinkSettings(new SWGSDRangel::SWGSigMFFileSinkSettings()); + SWGSDRangel::SWGSigMFFileSinkSettings *swgSigMFFileSinkSettings = swgChannelSettings->getSigMfFileSinkSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + swgSigMFFileSinkSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("fileRecordName")) { + swgSigMFFileSinkSettings->setTitle(new QString(settings.m_fileRecordName)); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgSigMFFileSinkSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgSigMFFileSinkSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("log2Decim") || force) { + swgSigMFFileSinkSettings->setLog2Decim(settings.m_log2Decim); + } + if (channelSettingsKeys.contains("spectrumSquelchMode")) { + swgSigMFFileSinkSettings->setSpectrumSquelchMode(settings.m_spectrumSquelchMode ? 1 : 0); + } + if (channelSettingsKeys.contains("spectrumSquelch")) { + swgSigMFFileSinkSettings->setSpectrumSquelch(settings.m_spectrumSquelch); + } + if (channelSettingsKeys.contains("preRecordTime")) { + swgSigMFFileSinkSettings->setPreRecordTime(settings.m_preRecordTime); + } + if (channelSettingsKeys.contains("squelchPostRecordTime")) { + swgSigMFFileSinkSettings->setSquelchPostRecordTime(settings.m_squelchPostRecordTime); + } + if (channelSettingsKeys.contains("squelchRecordingEnable")) { + swgSigMFFileSinkSettings->setSquelchRecordingEnable(settings.m_squelchRecordingEnable ? 1 : 0); + } + if (channelSettingsKeys.contains("streamIndex")) { + swgSigMFFileSinkSettings->setStreamIndex(settings.m_streamIndex); + } + + 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 FileSink::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "FileSink::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("FileSink::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/channelrx/filesink/filesink.h b/plugins/channelrx/filesink/filesink.h new file mode 100644 index 000000000..0f14044ee --- /dev/null +++ b/plugins/channelrx/filesink/filesink.h @@ -0,0 +1,155 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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_FILESINK_H_ +#define INCLUDE_FILESINK_H_ + +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "dsp/spectrumvis.h" +#include "channel/channelapi.h" + +#include "filesinksettings.h" + +class QNetworkAccessManager; +class QNetworkReply; + +class DeviceAPI; +class DeviceSampleSource; +class FileSinkBaseband; + +class FileSink : public BasebandSampleSink, public ChannelAPI { + Q_OBJECT +public: + class MsgConfigureFileSink : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const FileSinkSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureFileSink* create(const FileSinkSettings& settings, bool force) + { + return new MsgConfigureFileSink(settings, force); + } + + private: + FileSinkSettings m_settings; + bool m_force; + + MsgConfigureFileSink(const FileSinkSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + FileSink(DeviceAPI *deviceAPI); + virtual ~FileSink(); + virtual void destroy() { delete this; } + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual void getTitle(QString& title) { title = "File Sink"; } + virtual qint64 getCenterFrequency() const { return m_frequencyOffset; } + + 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 m_frequencyOffset; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + virtual int webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const FileSinkSettings& settings); + + static void webapiUpdateChannelSettings( + FileSinkSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + void getLocalDevices(std::vector& indexes); + uint32_t getNumberOfDeviceStreams() const; + SpectrumVis *getSpectrumVis() { return &m_spectrumVis; } + void record(bool record); + uint64_t getMsCount() const; + uint64_t getByteCount() const; + unsigned int getNbTracks() const; + + static const QString m_channelIdURI; + static const QString m_channelId; + +private: + DeviceAPI *m_deviceAPI; + QThread m_thread; + FileSinkBaseband *m_basebandSink; + FileSinkSettings m_settings; + SpectrumVis m_spectrumVis; + + uint64_t m_centerFrequency; + int64_t m_frequencyOffset; + uint32_t m_basebandSampleRate; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const FileSinkSettings& settings, bool force = false); + void propagateSampleRateAndFrequency(uint32_t index, uint32_t log2Decim); + DeviceSampleSource *getLocalDevice(uint32_t index); + + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const FileSinkSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif /* INCLUDE_FILESINK_H_ */ diff --git a/plugins/channelrx/filesink/filesinkbaseband.cpp b/plugins/channelrx/filesink/filesinkbaseband.cpp new file mode 100644 index 000000000..92f10a8ff --- /dev/null +++ b/plugins/channelrx/filesink/filesinkbaseband.cpp @@ -0,0 +1,248 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/downchannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/spectrumvis.h" +#include "util/db.h" + +#include "filesinkmessages.h" +#include "filesinkbaseband.h" + +MESSAGE_CLASS_DEFINITION(FileSinkBaseband::MsgConfigureFileSinkBaseband, Message) +MESSAGE_CLASS_DEFINITION(FileSinkBaseband::MsgConfigureFileSinkWork, Message) + +FileSinkBaseband::FileSinkBaseband() : + m_running(false), + m_specMax(0), + m_squelchLevel(0), + m_squelchOpen(false), + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); + + qDebug("FileSinkBaseband::FileSinkBaseband"); + connect(&m_timer, SIGNAL(timeout()), this, SLOT(tick())); + m_timer.start(200); +} + +FileSinkBaseband::~FileSinkBaseband() +{ + m_inputMessageQueue.clear(); + delete m_channelizer; +} + +void FileSinkBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void FileSinkBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &FileSinkBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void FileSinkBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &FileSinkBaseband::handleData + ); + m_running = false; +} + +void FileSinkBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void FileSinkBaseband::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 FileSinkBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool FileSinkBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureFileSinkBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFileSinkBaseband& cfg = (MsgConfigureFileSinkBaseband&) cmd; + qDebug() << "FileSinkBaseband::handleMessage: MsgConfigureFileSinkBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "FileSinkBaseband::handleMessage: DSPSignalNotification:" + << " basebandSampleRate: " << notif.getSampleRate() + << " cnterFrequency: " << notif.getCenterFrequency(); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + m_centerFrequency = notif.getCenterFrequency(); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + int desiredSampleRate = m_channelizer->getBasebandSampleRate() / (1<setChannelization(desiredSampleRate, m_settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + desiredSampleRate, + m_channelizer->getChannelFrequencyOffset(), + m_centerFrequency + m_settings.m_inputFrequencyOffset); + + return true; + } + else if (MsgConfigureFileSinkWork::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureFileSinkWork& conf = (MsgConfigureFileSinkWork&) cmd; + qDebug() << "FileSinkBaseband::handleMessage: MsgConfigureFileSinkWork: " << conf.isWorking(); + + if (!m_settings.m_squelchRecordingEnable) + { + if (conf.isWorking()) { + m_sink.startRecording(); + } else { + m_sink.stopRecording(); + } + } + + return true; + } + else + { + return false; + } +} + +void FileSinkBaseband::applySettings(const FileSinkSettings& settings, bool force) +{ + qDebug() << "FileSinkBaseband::applySettings:" + << "m_log2Decim:" << settings.m_log2Decim + << "m_inputFrequencyOffset:" << settings.m_inputFrequencyOffset + << "m_fileRecordName: " << settings.m_fileRecordName + << "m_centerFrequency: " << m_centerFrequency + << "force: " << force; + + if ((settings.m_log2Decim != m_settings.m_log2Decim) + || (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + int desiredSampleRate = m_channelizer->getBasebandSampleRate() / (1<setChannelization(desiredSampleRate, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings( + m_channelizer->getChannelSampleRate(), + desiredSampleRate, + m_channelizer->getChannelFrequencyOffset(), + m_centerFrequency + settings.m_inputFrequencyOffset); + } + + if ((settings.m_spectrumSquelchMode != m_settings.m_spectrumSquelchMode) || force) { + if (!settings.m_spectrumSquelchMode) { + m_squelchOpen = false; + } + } + + if ((settings.m_spectrumSquelch != m_settings.m_spectrumSquelch) || force) { + m_squelchLevel = CalcDb::powerFromdB(settings.m_spectrumSquelch); + } + + m_sink.applySettings(settings, force); + m_settings = settings; +} + +int FileSinkBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void FileSinkBaseband::tick() +{ + if (m_spectrumSink && m_settings.m_spectrumSquelchMode) + { + m_specMax = m_spectrumSink->getSpecMax(); + bool squelchOpen = m_specMax > m_squelchLevel; + + if (squelchOpen != m_squelchOpen) + { + if (m_messageQueueToGUI) + { + FileSinkMessages::MsgReportSquelch *msg = FileSinkMessages::MsgReportSquelch::create(squelchOpen); + m_messageQueueToGUI->push(msg); + } + + if (m_settings.m_squelchRecordingEnable) { + m_sink.squelchRecording(squelchOpen); + } + } + + m_squelchOpen = squelchOpen; + } +} diff --git a/plugins/channelrx/filesink/filesinkbaseband.h b/plugins/channelrx/filesink/filesinkbaseband.h new file mode 100644 index 000000000..c9e541ba9 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkbaseband.h @@ -0,0 +1,131 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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_FILESINKBASEBAND_H_ +#define INCLUDE_FILESINKBASEBAND_H_ + +#include +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "filesinksink.h" +#include "filesinksettings.h" + +class DownChannelizer; +class SpectrumVis; + +class FileSinkBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureFileSinkBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const FileSinkSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureFileSinkBaseband* create(const FileSinkSettings& settings, bool force) + { + return new MsgConfigureFileSinkBaseband(settings, force); + } + + private: + FileSinkSettings m_settings; + bool m_force; + + MsgConfigureFileSinkBaseband(const FileSinkSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgConfigureFileSinkWork : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool isWorking() const { return m_working; } + + static MsgConfigureFileSinkWork* create(bool working) + { + return new MsgConfigureFileSinkWork(working); + } + + private: + bool m_working; + + MsgConfigureFileSinkWork(bool working) : + Message(), + m_working(working) + { } + }; + + FileSinkBaseband(); + ~FileSinkBaseband(); + + 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 + int getChannelSampleRate() const; + void setBasebandSampleRate(int sampleRate); + bool isRunning() const { return m_running; } + void setSpectrumSink(SpectrumVis* spectrumSink) { m_spectrumSink = spectrumSink; m_sink.setSpectrumSink(spectrumSink); } + uint64_t getMsCount() const { return m_sink.getMsCount(); } + uint64_t getByteCount() const { return m_sink.getByteCount(); } + unsigned int getNbTracks() const { return m_sink.getNbTracks(); } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_messageQueueToGUI = messageQueue; m_sink.setMessageQueueToGUI(messageQueue); } + void setDeviceHwId(const QString& hwId) { m_sink.setDeviceHwId(hwId); } + void setDeviceUId(int uid) { m_sink.setDeviceUId(uid); } + bool isSquelchOpen() const { return m_squelchOpen; } + bool isRecording() const { return m_sink.isRecording(); } + float getSpecMax() const { return m_specMax; } + int getSinkSampleRate() const { return m_sink.getSampleRate(); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + FileSinkSink m_sink; + SpectrumVis *m_spectrumSink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_messageQueueToGUI; + FileSinkSettings m_settings; + float m_specMax; //!< Last max used for comparison + float m_squelchLevel; + bool m_squelchOpen; + int64_t m_centerFrequency; + bool m_running; + QMutex m_mutex; + QTimer m_timer; + + bool handleMessage(const Message& cmd); + void applySettings(const FileSinkSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed + void tick(); +}; + + +#endif // INCLUDE_FILESINKBASEBAND_H_ diff --git a/plugins/channelrx/filesink/filesinkgui.cpp b/plugins/channelrx/filesink/filesinkgui.cpp new file mode 100644 index 000000000..779ee2709 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkgui.cpp @@ -0,0 +1,590 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#include "device/deviceuiset.h" +#include "device/deviceapi.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "dsp/hbfilterchainconverter.h" +#include "dsp/dspcommands.h" +#include "mainwindow.h" + +#include "filesinkmessages.h" +#include "filesink.h" +#include "filesinkgui.h" +#include "ui_filesinkgui.h" + +FileSinkGUI* FileSinkGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *channelRx) +{ + FileSinkGUI* gui = new FileSinkGUI(pluginAPI, deviceUISet, channelRx); + return gui; +} + +void FileSinkGUI::destroy() +{ + delete this; +} + +void FileSinkGUI::setName(const QString& name) +{ + setObjectName(name); +} + +QString FileSinkGUI::getName() const +{ + return objectName(); +} + +qint64 FileSinkGUI::getCenterFrequency() const { + return 0; +} + +void FileSinkGUI::setCenterFrequency(qint64 centerFrequency) +{ + (void) centerFrequency; +} + +void FileSinkGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray FileSinkGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool FileSinkGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool FileSinkGUI::handleMessage(const Message& message) +{ + if (DSPSignalNotification::match(message)) + { + DSPSignalNotification notif = (const DSPSignalNotification&) message; + m_basebandSampleRate = notif.getSampleRate(); + displayRate(); + + if (m_fixedPosition) + { + setFrequencyFromPos(); + applySettings(); + } + else + { + setPosFromFrequency(); + } + + return true; + } + else if (FileSink::MsgConfigureFileSink::match(message)) + { + const FileSink::MsgConfigureFileSink& cfg = (FileSink::MsgConfigureFileSink&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (FileSinkMessages::MsgConfigureSpectrum::match(message)) + { + const FileSinkMessages::MsgConfigureSpectrum& cfg = (FileSinkMessages::MsgConfigureSpectrum&) message; + ui->glSpectrum->setSampleRate(cfg.getSampleRate()); + ui->glSpectrum->setCenterFrequency(cfg.getCenterFrequency()); + return true; + } + else if (FileSinkMessages::MsgReportSquelch::match(message)) + { + const FileSinkMessages::MsgReportSquelch& report = (FileSinkMessages::MsgReportSquelch&) message; + + if (report.getOpen()) { + ui->squelchLevel->setStyleSheet("QDial { background-color : green; }"); + } else { + ui->squelchLevel->setStyleSheet("QDial { background:rgb(79,79,79); }"); + } + + return true; + } + else if (FileSinkMessages::MsgReportRecording::match(message)) + { + const FileSinkMessages::MsgReportSquelch& report = (FileSinkMessages::MsgReportSquelch&) message; + + if (report.getOpen()) + { + ui->record->setStyleSheet("QToolButton { background-color : red; }"); + ui->squelchedRecording->setEnabled(false); + } + else + { + ui->record->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + ui->squelchedRecording->setEnabled(true); + } + + return true; + } + else if (FileSinkMessages::MsgReportRecordFileName::match(message)) + { + const FileSinkMessages::MsgReportRecordFileName& report = (FileSinkMessages::MsgReportRecordFileName&) message; + ui->fileNameText->setText(report.getFileName()); + return true; + } + else + { + return false; + } +} + +FileSinkGUI::FileSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *channelrx, QWidget* parent) : + RollupWidget(parent), + ui(new Ui::FileSinkGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_fixedShiftIndex(0), + m_basebandSampleRate(0), + m_fixedPosition(false), + m_tickCount(0) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_fileSink = (FileSink*) channelrx; + m_spectrumVis = m_fileSink->getSpectrumVis(); + m_spectrumVis->setGLSpectrum(ui->glSpectrum); + m_fileSink->setMessageQueueToGUI(getInputMessageQueue()); + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 8, -99999999, 99999999); + ui->position->setEnabled(m_fixedPosition); + ui->glSpectrumGUI->setBuddies(m_spectrumVis, ui->glSpectrum); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(m_settings.m_rgbColor); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setBandwidth(m_basebandSampleRate); + m_channelMarker.setTitle("File Sink"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + m_settings.setSpectrumGUI(ui->glSpectrumGUI); + + m_deviceUISet->registerRxChannelInstance(FileSink::m_channelIdURI, this); + m_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + connect(&(m_deviceUISet->m_deviceAPI->getMasterTimer()), SIGNAL(timeout()), this, SLOT(tick())); + + displaySettings(); + applySettings(true); +} + +FileSinkGUI::~FileSinkGUI() +{ + m_deviceUISet->removeRxChannelInstance(this); + delete m_fileSink; // TODO: check this: when the GUI closes it has to delete the demodulator + delete ui; +} + +void FileSinkGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void FileSinkGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + setTitleColor(m_channelMarker.getColor()); + + FileSink::MsgConfigureFileSink* message = FileSink::MsgConfigureFileSink::create(m_settings, force); + m_fileSink->getInputMessageQueue()->push(message); + } +} + +void FileSinkGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setBandwidth(m_basebandSampleRate / (1<deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + ui->fileNameText->setText(m_settings.m_fileRecordName); + ui->decimationFactor->setCurrentIndex(m_settings.m_log2Decim); + ui->spectrumSquelch->setChecked(m_settings.m_spectrumSquelchMode); + ui->squelchLevel->setValue(m_settings.m_spectrumSquelch); + ui->squelchLevelText->setText(tr("%1").arg(m_settings.m_spectrumSquelch)); + ui->preRecordTime->setValue(m_settings.m_preRecordTime); + ui->preRecordTimeText->setText(tr("%1").arg(m_settings.m_preRecordTime)); + ui->postSquelchTime->setValue(m_settings.m_squelchPostRecordTime); + ui->postSquelchTimeText->setText(tr("%1").arg(m_settings.m_squelchPostRecordTime)); + ui->squelchedRecording->setChecked(m_settings.m_squelchRecordingEnable); + ui->record->setEnabled(!m_settings.m_squelchRecordingEnable); + + if (!m_settings.m_spectrumSquelchMode) { + ui->squelchLevel->setStyleSheet("QDial { background:rgb(79,79,79); }"); + } + + displayStreamIndex(); + setPosFromFrequency(); + + blockApplySettings(false); +} + +void FileSinkGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void FileSinkGUI::displayRate() +{ + double channelSampleRate = ((double) m_basebandSampleRate) / (1<channelRateText->setText(tr("%1k").arg(QString::number(channelSampleRate / 1000.0, 'g', 5))); + m_channelMarker.setBandwidth(channelSampleRate); +} + +void FileSinkGUI::displayPos() +{ + ui->position->setValue(m_fixedShiftIndex); + ui->filterChainIndex->setText(tr("%1").arg(m_fixedShiftIndex)); +} + +void FileSinkGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void FileSinkGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void FileSinkGUI::channelMarkerChangedByCursor() +{ + if (m_fixedPosition) { + return; + } + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + setPosFromFrequency(); + applySettings(); +} + +void FileSinkGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void FileSinkGUI::handleSourceMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void FileSinkGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void FileSinkGUI::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.move(p); + 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); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_fileSink->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + +void FileSinkGUI::on_deltaFrequency_changed(qint64 value) +{ + if (!m_fixedPosition) + { + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + setPosFromFrequency(); + applySettings(); + } +} + +void FileSinkGUI::on_decimationFactor_currentIndexChanged(int index) +{ + m_settings.m_log2Decim = index; + applyDecimation(); + displayRate(); + displayPos(); + applySettings(); + + if (m_fixedPosition) { + setFrequencyFromPos(); + } else { + setPosFromFrequency(); + } +} + +void FileSinkGUI::on_fixedPosition_toggled(bool checked) +{ + m_fixedPosition = checked; + m_channelMarker.setMovable(!checked); + ui->deltaFrequency->setEnabled(!checked); + ui->position->setEnabled(checked); + + if (m_fixedPosition) + { + setFrequencyFromPos(); + applySettings(); + } +} + +void FileSinkGUI::on_position_valueChanged(int value) +{ + m_fixedShiftIndex = value; + displayPos(); + + if (m_fixedPosition) + { + setFrequencyFromPos(); + applySettings(); + } +} + +void FileSinkGUI::on_spectrumSquelch_toggled(bool checked) +{ + m_settings.m_spectrumSquelchMode = checked; + + if (!m_settings.m_spectrumSquelchMode) + { + m_settings.m_squelchRecordingEnable = false; + ui->squelchLevel->setStyleSheet("QDial { background:rgb(79,79,79); }"); + ui->squelchedRecording->blockSignals(true); + ui->squelchedRecording->setChecked(false); + ui->squelchedRecording->blockSignals(false); + ui->record->setEnabled(true); + } + + ui->squelchedRecording->setEnabled(checked); + + applySettings(); +} + +void FileSinkGUI::on_squelchLevel_valueChanged(int value) +{ + m_settings.m_spectrumSquelch = value; + ui->squelchLevelText->setText(tr("%1").arg(m_settings.m_spectrumSquelch)); + applySettings(); +} + +void FileSinkGUI::on_preRecordTime_valueChanged(int value) +{ + m_settings.m_preRecordTime = value; + ui->preRecordTimeText->setText(tr("%1").arg(m_settings.m_preRecordTime)); + applySettings(); +} + +void FileSinkGUI::on_postSquelchTime_valueChanged(int value) +{ + m_settings.m_squelchPostRecordTime = value; + ui->postSquelchTimeText->setText(tr("%1").arg(m_settings.m_squelchPostRecordTime)); + applySettings(); +} + +void FileSinkGUI::on_squelchedRecording_toggled(bool checked) +{ + ui->record->setEnabled(!checked); + m_settings.m_squelchRecordingEnable = checked; + applySettings(); +} + +void FileSinkGUI::on_record_toggled(bool checked) +{ + ui->squelchedRecording->setEnabled(!checked); + + if (checked) { + ui->record->setStyleSheet("QToolButton { background-color : red; }"); + } else { + ui->record->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + } + + m_fileSink->record(checked); +} + +void FileSinkGUI::on_showFileDialog_clicked(bool checked) +{ + (void) checked; + QFileDialog fileDialog( + this, + tr("Save record file"), + m_settings.m_fileRecordName, + tr("SDR I/Q Files (*.sdriq)") + ); + + fileDialog.setOptions(QFileDialog::DontUseNativeDialog); + fileDialog.setFileMode(QFileDialog::AnyFile); + QStringList fileNames; + + if (fileDialog.exec()) + { + fileNames = fileDialog.selectedFiles(); + + if (fileNames.size() > 0) + { + m_settings.m_fileRecordName = fileNames.at(0); + ui->fileNameText->setText(m_settings.m_fileRecordName); + applySettings(); + } + } +} + +void FileSinkGUI::setFrequencyFromPos() +{ + int inputFrequencyOffset = FileSinkSettings::getOffsetFromFixedShiftIndex( + m_basebandSampleRate, + m_settings.m_log2Decim, + m_fixedShiftIndex); + m_channelMarker.setCenterFrequency(inputFrequencyOffset); + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); +} + +void FileSinkGUI::setPosFromFrequency() +{ + int fshift = FileSinkSettings::getHalfBand(m_basebandSampleRate, m_settings.m_log2Decim + 1); + m_fixedShiftIndex = FileSinkSettings::getFixedShiftIndexFromOffset( + m_basebandSampleRate, + m_settings.m_log2Decim, + m_settings.m_inputFrequencyOffset + (m_settings.m_inputFrequencyOffset < 0 ? -fshift : fshift) + ); + displayPos(); +} + +void FileSinkGUI::applyDecimation() +{ + ui->position->setMaximum(FileSinkSettings::getNbFixedShiftIndexes(m_settings.m_log2Decim)-1); + ui->position->setValue(m_fixedShiftIndex); + m_fixedShiftIndex = ui->position->value(); +} + +void FileSinkGUI::tick() +{ + if (++m_tickCount == 20) // once per second + { + uint64_t msTime = m_fileSink->getMsCount(); + uint64_t bytes = m_fileSink->getByteCount(); + unsigned int nbTracks = m_fileSink->getNbTracks(); + QTime recordLength(0, 0, 0, 0); + recordLength = recordLength.addSecs(msTime / 1000); + recordLength = recordLength.addMSecs(msTime % 1000); + QString s_time = recordLength.toString("HH:mm:ss"); + ui->recordTimeText->setText(s_time); + ui->recordSizeText->setText(displayScaled(bytes, 2)); + ui->recordNbTracks->setText(tr("#%1").arg(nbTracks)); + m_tickCount = 0; + } +} + +QString FileSinkGUI::displayScaled(uint64_t value, int precision) +{ + if (value < 1000) { + return tr("%1").arg(QString::number(value, 'f', precision)); + } else if (value < 1000000) { + return tr("%1k").arg(QString::number(value / 1000.0, 'f', precision)); + } else if (value < 1000000000) { + return tr("%1M").arg(QString::number(value / 1000000.0, 'f', precision)); + } else if (value < 1000000000000) { + return tr("%1G").arg(QString::number(value / 1000000000.0, 'f', precision)); + } +} + diff --git a/plugins/channelrx/filesink/filesinkgui.h b/plugins/channelrx/filesink/filesinkgui.h new file mode 100644 index 000000000..1e1da56dc --- /dev/null +++ b/plugins/channelrx/filesink/filesinkgui.h @@ -0,0 +1,118 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 PLUGINS_CHANNELRX_FILESINK_FILESINKGUI_H_ +#define PLUGINS_CHANNELRX_FILESINK_FILESINKGUI_H_ + +#include + +#include + +#include "plugin/plugininstancegui.h" +#include "dsp/channelmarker.h" +#include "gui/rollupwidget.h" +#include "util/messagequeue.h" + +#include "filesinksettings.h" + +class PluginAPI; +class DeviceUISet; +class FileSink; +class BasebandSampleSink; +class SpectrumVis; + +namespace Ui { + class FileSinkGUI; +} + +class FileSinkGUI : public RollupWidget, public PluginInstanceGUI { + Q_OBJECT +public: + static FileSinkGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void setName(const QString& name); + QString getName() const; + virtual qint64 getCenterFrequency() const; + virtual void setCenterFrequency(qint64 centerFrequency); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual bool handleMessage(const Message& message); + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + Ui::FileSinkGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + FileSinkSettings m_settings; + int m_fixedShiftIndex; + int m_basebandSampleRate; + double m_shiftFrequencyFactor; //!< Channel frequency shift factor + bool m_fixedPosition; + bool m_doApplySettings; + + FileSink* m_fileSink; + SpectrumVis* m_spectrumVis; + MessageQueue m_inputMessageQueue; + + uint32_t m_tickCount; + + explicit FileSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = nullptr); + virtual ~FileSinkGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void applyDecimation(); + void displaySettings(); + void displayStreamIndex(); + void displayRate(); + void displayPos(); + void setFrequencyFromPos(); + void setPosFromFrequency(); + QString displayScaled(uint64_t value, int precision); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void handleSourceMessages(); + void on_deltaFrequency_changed(qint64 value); + void on_decimationFactor_currentIndexChanged(int index); + void on_fixedPosition_toggled(bool checked); + void on_position_valueChanged(int value); + void on_spectrumSquelch_toggled(bool checked); + void on_squelchLevel_valueChanged(int value); + void on_preRecordTime_valueChanged(int value); + void on_postSquelchTime_valueChanged(int value); + void on_squelchedRecording_toggled(bool checked); + void on_record_toggled(bool checked); + void on_showFileDialog_clicked(bool checked); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void tick(); +}; + + + +#endif /* PLUGINS_CHANNELRX_FILESINK_FILESINKGUI_H_ */ diff --git a/plugins/channelrx/filesink/filesinkgui.ui b/plugins/channelrx/filesink/filesinkgui.ui new file mode 100644 index 000000000..814bf79a0 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkgui.ui @@ -0,0 +1,584 @@ + + + FileSinkGUI + + + + 0 + 0 + 552 + 458 + + + + + 552 + 102 + + + + + Liberation Sans + 9 + + + + File Sink + + + File Sink + + + + + 0 + 0 + 550 + 100 + + + + + 550 + 100 + + + + Settings + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + true + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Dec + + + + + + + + 55 + 16777215 + + + + Decimation factor + + + + 1 + + + + + 2 + + + + + 4 + + + + + 8 + + + + + 16 + + + + + 32 + + + + + 64 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + 0 + + + + Sink rate (kS/s) + + + 0000k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 32 + 0 + + + + Number of captures (files) in recording session updated at end of file + + + #000 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 52 + 0 + + + + Total recording time (HH:MM:SS) + + + 00:00:00 + + + + + + + + 52 + 0 + + + + Total recording size (k: kB, M: MB, G: GB) + + + 999.99M + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + 10 + + + + + Use fixed frequency shift positions for little performance improvement + + + Pos + + + + + + + Center frequency position + + + 2 + + + 1 + + + Qt::Horizontal + + + + + + + + 24 + 0 + + + + Filter chain hash code + + + 000 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Toggle spectrum squelch recording + + + SQ + + + true + + + + + + + + 24 + 24 + + + + Spectrum squelch level (dB) + + + -120 + + + 0 + + + 1 + + + -50 + + + + + + + Spectrum squelch level (dB) + + + -100 + + + + + + + + 24 + 24 + + + + Pre-recording time (s) + + + 10 + + + 1 + + + + + + + Squelched recoding pre-recording time (s) + + + 10 + + + + + + + + 24 + 24 + + + + Squelched recording post-recording time (s) + + + 10 + + + 1 + + + + + + + Squelched recording post-recording time (s) + + + 10 + + + + + + + Squelched recording enable + + + REC + + + + + + + + + + + + 24 + 16777215 + + + + + + + + :/record_off.png:/record_off.png + + + + + + + + 24 + 24 + + + + + 24 + 24 + + + + Open file + + + + + + + :/preset-load.png:/preset-load.png + + + + + + + true + + + Current recording file + + + ... + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + + 0 + 100 + 541 + 351 + + + + Channel Spectrum + + + + 10 + + + + + + 0 + 300 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + GLSpectrum + QWidget +
gui/glspectrum.h
+ 1 +
+ + GLSpectrumGUI + QWidget +
gui/glspectrumgui.h
+ 1 +
+
+ + + + +
diff --git a/plugins/channelrx/filesink/filesinkmessages.cpp b/plugins/channelrx/filesink/filesinkmessages.cpp new file mode 100644 index 000000000..020291d05 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkmessages.cpp @@ -0,0 +1,23 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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 "filesinkmessages.h" + +MESSAGE_CLASS_DEFINITION(FileSinkMessages::MsgConfigureSpectrum, Message) +MESSAGE_CLASS_DEFINITION(FileSinkMessages::MsgReportSquelch, Message) +MESSAGE_CLASS_DEFINITION(FileSinkMessages::MsgReportRecording, Message) +MESSAGE_CLASS_DEFINITION(FileSinkMessages::MsgReportRecordFileName, Message) diff --git a/plugins/channelrx/filesink/filesinkmessages.h b/plugins/channelrx/filesink/filesinkmessages.h new file mode 100644 index 000000000..b6914a72a --- /dev/null +++ b/plugins/channelrx/filesink/filesinkmessages.h @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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_FILESINKMESSAGES_H_ +#define INCLUDE_FILESINKMESSAGES_H_ + +#include + +#include "util/message.h" + +class FileSinkMessages : public QObject { + Q_OBJECT +public: + class MsgConfigureSpectrum : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int64_t getCenterFrequency() const { return m_centerFrequency; } + int getSampleRate() const { return m_sampleRate; } + + static MsgConfigureSpectrum* create(int64_t centerFrequency, int sampleRate) { + return new MsgConfigureSpectrum(centerFrequency, sampleRate); + } + + private: + int64_t m_centerFrequency; + int m_sampleRate; + + MsgConfigureSpectrum(int64_t centerFrequency, int sampleRate) : + Message(), + m_centerFrequency(centerFrequency), + m_sampleRate(sampleRate) + { } + }; + + class MsgReportSquelch : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getOpen() const { return m_open; } + + static MsgReportSquelch* create(bool open) { + return new MsgReportSquelch(open); + } + + private: + bool m_open; + + MsgReportSquelch(bool open) : + Message(), + m_open(open) + { } + }; + + class MsgReportRecording : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getRecording() const { return m_recording; } + + static MsgReportRecording* create(bool recording) { + return new MsgReportRecording(recording); + } + + private: + bool m_recording; + + MsgReportRecording(bool recording) : + Message(), + m_recording(recording) + { } + }; + + class MsgReportRecordFileName : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getFileName() const { return m_fileName; } + + static MsgReportRecordFileName* create(const QString& fileName) { + return new MsgReportRecordFileName(fileName); + } + + private: + QString m_fileName; + + MsgReportRecordFileName(const QString& fileName) : + Message(), + m_fileName(fileName) + { } + }; +}; + +#endif // INCLUDE_FILESINKMESSAGES_H_ diff --git a/plugins/channelrx/filesink/filesinkplugin.cpp b/plugins/channelrx/filesink/filesinkplugin.cpp new file mode 100644 index 000000000..ab05de670 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkplugin.cpp @@ -0,0 +1,85 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "filesinkgui.h" +#endif +#include "filesink.h" +#include "filesinkwebapiadapter.h" +#include "filesinkplugin.h" + +const PluginDescriptor FileSinkPlugin::m_pluginDescriptor = { + FileSink::m_channelId, + QString("File Sink"), + QString("4.15.0"), + QString("(c) Edouard Griffiths, F4EXB"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +FileSinkPlugin::FileSinkPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& FileSinkPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void FileSinkPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register channel Source + m_pluginAPI->registerRxChannel(FileSink::m_channelIdURI, FileSink::m_channelId, this); +} + +#ifdef SERVER_MODE +PluginInstanceGUI* FileSinkPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + return nullptr; +} +#else +PluginInstanceGUI* FileSinkPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return FileSinkGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +BasebandSampleSink* FileSinkPlugin::createRxChannelBS(DeviceAPI *deviceAPI) const +{ + return new FileSink(deviceAPI); +} + +ChannelAPI* FileSinkPlugin::createRxChannelCS(DeviceAPI *deviceAPI) const +{ + return new FileSink(deviceAPI); +} + +ChannelWebAPIAdapter* FileSinkPlugin::createChannelWebAPIAdapter() const +{ + return new FileSinkWebAPIAdapter(); +} diff --git a/plugins/channelrx/filesink/filesinkplugin.h b/plugins/channelrx/filesink/filesinkplugin.h new file mode 100644 index 000000000..39202ee83 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkplugin.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// 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 PLUGINS_CHANNELRX_FILESINK_FILESINKPLUGIN_H_ +#define PLUGINS_CHANNELRX_FILESINK_FILESINKPLUGIN_H_ + + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class FileSinkPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.demod.filesink") + +public: + explicit FileSinkPlugin(QObject* parent = 0); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual PluginInstanceGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + virtual BasebandSampleSink* createRxChannelBS(DeviceAPI *deviceAPI) const; + virtual ChannelAPI* createRxChannelCS(DeviceAPI *deviceAPI) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif /* PLUGINS_CHANNELRX_FILESINK_FILESINKPLUGIN_H_ */ diff --git a/plugins/channelrx/filesink/filesinksettings.cpp b/plugins/channelrx/filesink/filesinksettings.cpp new file mode 100644 index 000000000..b2146e11d --- /dev/null +++ b/plugins/channelrx/filesink/filesinksettings.cpp @@ -0,0 +1,173 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "filesinksettings.h" + +FileSinkSettings::FileSinkSettings() +{ + resetToDefaults(); +} + +void FileSinkSettings::resetToDefaults() +{ + m_ncoMode = false; + m_inputFrequencyOffset = 0; + m_fileRecordName = ""; + m_rgbColor = QColor(140, 4, 4).rgb(); + m_title = "File Sink"; + m_log2Decim = 0; + m_spectrumGUI = nullptr; + m_spectrumSquelchMode = false; + m_spectrumSquelch = -50; + m_preRecordTime = 0; + m_squelchPostRecordTime = 0; + m_squelchRecordingEnable = false; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; +} + +QByteArray FileSinkSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeBool(2, m_ncoMode); + s.writeString(3, m_fileRecordName); + s.writeS32(4, m_streamIndex); + s.writeU32(5, m_rgbColor); + s.writeString(6, m_title); + s.writeBool(7, m_useReverseAPI); + s.writeString(8, m_reverseAPIAddress); + s.writeU32(9, m_reverseAPIPort); + s.writeU32(10, m_reverseAPIDeviceIndex); + s.writeU32(11, m_reverseAPIChannelIndex); + s.writeU32(12, m_log2Decim); + + if (m_spectrumGUI) { + s.writeBlob(13, m_spectrumGUI->serialize()); + } + + s.writeBool(14, m_spectrumSquelchMode); + s.writeS32(15, m_spectrumSquelch); + s.writeS32(16, m_preRecordTime); + s.writeS32(17, m_squelchPostRecordTime); + s.writeBool(18, m_squelchRecordingEnable); + + return s.final(); +} + +bool FileSinkSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + uint32_t tmp; + int stmp; + QString strtmp; + QByteArray bytetmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readBool(2, &m_ncoMode, false); + d.readString(3, &m_fileRecordName, ""); + d.readS32(4, &m_streamIndex, 0); + d.readU32(5, &m_rgbColor, QColor(0, 255, 255).rgb()); + d.readString(6, &m_title, "File Sink"); + d.readBool(7, &m_useReverseAPI, false); + d.readString(8, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(9, &tmp, 0); + + if ((tmp > 1023) && (tmp < 65535)) { + m_reverseAPIPort = tmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(10, &tmp, 0); + m_reverseAPIDeviceIndex = tmp > 99 ? 99 : tmp; + d.readU32(11, &tmp, 0); + m_reverseAPIChannelIndex = tmp > 99 ? 99 : tmp; + d.readU32(12, &tmp, 0); + m_log2Decim = tmp > 6 ? 6 : tmp; + + if (m_spectrumGUI) + { + d.readBlob(13, &bytetmp); + m_spectrumGUI->deserialize(bytetmp); + } + + d.readBool(14, &m_spectrumSquelchMode, false); + d.readS32(15, &stmp, -50); + m_spectrumSquelch = stmp; + d.readS32(16, &m_preRecordTime, 0); + d.readS32(17, &m_squelchPostRecordTime, 0); + d.readBool(18, &m_squelchRecordingEnable, false); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +unsigned int FileSinkSettings::getNbFixedShiftIndexes(int log2Decim) +{ + int decim = (1<. // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FILESINKSETTINGS_H_ +#define INCLUDE_FILESINKSETTINGS_H_ + +#include +#include + +class Serializable; + +struct FileSinkSettings +{ + bool m_ncoMode; + qint32 m_inputFrequencyOffset; + QString m_fileRecordName; + quint32 m_rgbColor; + QString m_title; + uint32_t m_log2Decim; + bool m_spectrumSquelchMode; + float m_spectrumSquelch; + int m_preRecordTime; + int m_squelchPostRecordTime; + bool m_squelchRecordingEnable; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + Serializable *m_spectrumGUI; + + FileSinkSettings(); + void resetToDefaults(); + void setSpectrumGUI(Serializable *spectrumGUI) { m_spectrumGUI = spectrumGUI; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + + static unsigned int getNbFixedShiftIndexes(int log2Decim); + static int getHalfBand(int sampleRate, int log2Decim); + static unsigned int getFixedShiftIndexFromOffset(int sampleRate, int log2Decim, int frequencyOffset); + static int getOffsetFromFixedShiftIndex(int sampleRate, int log2Decim, int shiftIndex); +}; + +#endif /* INCLUDE_FILESINKSETTINGS_H_ */ diff --git a/plugins/channelrx/filesink/filesinksink.cpp b/plugins/channelrx/filesink/filesinksink.cpp new file mode 100644 index 000000000..a68d3a05e --- /dev/null +++ b/plugins/channelrx/filesink/filesinksink.cpp @@ -0,0 +1,305 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspcommands.h" +#include "dsp/sigmffilerecord.h" +#include "dsp/spectrumvis.h" + +#include "filesinkmessages.h" +#include "filesinksink.h" + +FileSinkSink::FileSinkSink() : + m_spectrumSink(nullptr), + m_msgQueueToGUI(nullptr), + m_nbCaptures(0), + m_preRecordBuffer(48000), + m_preRecordFill(0), + m_recordEnabled(false), + m_record(false), + m_squelchOpen(false), + m_postSquelchCounter(0), + m_msCount(0), + m_byteCount(0) +{} + +FileSinkSink::~FileSinkSink() +{} + +void FileSinkSink::startRecording() +{ + if (m_recordEnabled) // File is open for writing and valid + { + // set the length of pre record time + qint64 mSShift = (m_preRecordFill * 1000) / m_sinkSampleRate; + m_fileSink.setMsShift(-mSShift); + + // notify capture start + m_fileSink.startRecording(); + m_record = true; + m_nbCaptures++; + + if (m_msgQueueToGUI) + { + FileSinkMessages::MsgReportRecordFileName *msg + = FileSinkMessages::MsgReportRecordFileName::create(m_fileSink.getCurrentFileName()); + m_msgQueueToGUI->push(msg); + } + + // copy pre record samples + SampleVector::iterator p1Begin, p1End, p2Begin, p2End; + m_preRecordBuffer.readBegin(m_preRecordFill, &p1Begin, &p1End, &p2Begin, &p2End); + + if (p1Begin != p1End) { + m_fileSink.feed(p1Begin, p1End, false); + } + if (p2Begin != p2End) { + m_fileSink.feed(p2Begin, p2End, false); + } + + m_byteCount += m_preRecordFill * sizeof(Sample); + + if (m_sinkSampleRate > 0) { + m_msCount += (m_preRecordFill * 1000) / m_sinkSampleRate; + } + } +} + +void FileSinkSink::stopRecording() +{ + if (m_record) + { + m_preRecordBuffer.reset(); + m_fileSink.stopRecording(); + m_record = false; + } +} + +void FileSinkSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + SampleVector::const_iterator beginw = begin; + SampleVector::const_iterator endw = end; + + if (m_decimator.getDecim() != 1) + { + for (SampleVector::const_iterator it = begin; it < end; ++it) + { + Complex c(it->real(), it->imag()); + c *= m_nco.nextIQ(); + Complex ci; + + if (m_decimator.decimate(c, ci)) { + m_sampleBuffer.push_back(Sample(ci.real(), ci.imag())); + } + } + + beginw = m_sampleBuffer.begin(); + endw = m_sampleBuffer.end(); + } + + + if (!m_record && (m_settings.m_preRecordTime != 0)) { + m_preRecordFill = m_preRecordBuffer.write(beginw, endw); + } + + if (m_settings.m_squelchRecordingEnable) + { + int nbToWrite = endw - beginw; + + if (m_squelchOpen) + { + m_fileSink.feed(beginw, endw, true); + } + else + { + if (nbToWrite < m_postSquelchCounter) + { + m_fileSink.feed(beginw, endw, true); + m_postSquelchCounter -= nbToWrite; + } + else + { + if (m_msgQueueToGUI) + { + FileSinkMessages::MsgReportRecording *msg = FileSinkMessages::MsgReportRecording::create(false); + m_msgQueueToGUI->push(msg); + } + + m_fileSink.feed(beginw, endw + m_postSquelchCounter, true); + nbToWrite = m_postSquelchCounter; + m_postSquelchCounter = 0; + + stopRecording(); + } + } + + m_byteCount += nbToWrite * sizeof(Sample); + + if (m_sinkSampleRate > 0) { + m_msCount += (nbToWrite * 1000) / m_sinkSampleRate; + } + } + else if (m_record) + { + m_fileSink.feed(beginw, endw, true); + int nbSamples = endw - beginw; + m_byteCount += nbSamples * sizeof(Sample); + + if (m_sinkSampleRate > 0) { + m_msCount += (nbSamples * 1000) / m_sinkSampleRate; + } + } + + if (m_spectrumSink) { + m_spectrumSink->feed(beginw, endw, false); + } + + if (m_decimator.getDecim() != 1) { + m_sampleBuffer.clear(); + } +} + +void FileSinkSink::applyChannelSettings( + int channelSampleRate, + int sinkSampleRate, + int channelFrequencyOffset, + int64_t centerFrequency, + bool force) +{ + qDebug() << "FileSinkSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " sinkSampleRate: " << sinkSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset + << " centerFrequency: " << centerFrequency + << " force: " << force; + + if ((m_channelFrequencyOffset != channelFrequencyOffset) || + (m_channelSampleRate != channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((m_channelSampleRate != channelSampleRate) + || (m_sinkSampleRate != sinkSampleRate) || force) + { + int decim = channelSampleRate / sinkSampleRate; + + for (int i = 0; i < 7; i++) // find log2 beween 0 and 6 + { + if (decim & 1 == 1) + { + qDebug() << "FileSinkSink::applyChannelSettings: log2decim: " << i; + m_decimator.setLog2Decim(i); + break; + } + + decim >>= 1; + } + } + + if ((m_centerFrequency != centerFrequency) + || (m_channelFrequencyOffset != channelFrequencyOffset) + || (m_sinkSampleRate != sinkSampleRate) || force) + { + DSPSignalNotification *notif = new DSPSignalNotification(sinkSampleRate, centerFrequency); + DSPSignalNotification *notifToSpectrum = new DSPSignalNotification(*notif); + m_fileSink.getInputMessageQueue()->push(notif); + m_spectrumSink->getInputMessageQueue()->push(notifToSpectrum); + + if (m_msgQueueToGUI) + { + FileSinkMessages::MsgConfigureSpectrum *msg = FileSinkMessages::MsgConfigureSpectrum::create( + centerFrequency, sinkSampleRate); + m_msgQueueToGUI->push(msg); + } + } + + if ((m_sinkSampleRate != sinkSampleRate) || force) { + m_preRecordBuffer.setSize(m_settings.m_preRecordTime * sinkSampleRate); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; + m_sinkSampleRate = sinkSampleRate; + m_centerFrequency = centerFrequency; + m_preRecordBuffer.reset(); +} + +void FileSinkSink::applySettings(const FileSinkSettings& settings, bool force) +{ + qDebug() << "FileSinkSink::applySettings:" + << "m_fileRecordName: " << settings.m_fileRecordName + << "force: " << force; + + if ((settings.m_fileRecordName != m_settings.m_fileRecordName) || force) + { + QString fileBase; + FileRecordInterface::RecordType recordType = FileRecordInterface::guessTypeFromFileName(settings.m_fileRecordName, fileBase); + + if (recordType == FileRecordInterface::RecordTypeSdrIQ) + { + m_fileSink.setFileName(fileBase); + m_msCount = 0; + m_byteCount = 0; + m_nbCaptures = 0; + m_recordEnabled = true; + } + else + { + m_recordEnabled = false; + } + } + + if ((settings.m_preRecordTime != m_settings.m_squelchPostRecordTime) || force) + { + m_preRecordBuffer.setSize(settings.m_preRecordTime * m_sinkSampleRate); + + if (settings.m_preRecordTime == 0) { + m_preRecordFill = 0; + } + } + + m_settings = settings; +} + +void FileSinkSink::squelchRecording(bool squelchOpen) +{ + if (!m_recordEnabled || !m_settings.m_squelchRecordingEnable) { + return; + } + + if (squelchOpen) + { + if (!m_record) + { + startRecording(); + + if (m_msgQueueToGUI) + { + FileSinkMessages::MsgReportRecording *msg = FileSinkMessages::MsgReportRecording::create(true); + m_msgQueueToGUI->push(msg); + } + } + } + else + { + m_postSquelchCounter = m_settings.m_squelchPostRecordTime * m_sinkSampleRate; + } + + m_squelchOpen = squelchOpen; +} \ No newline at end of file diff --git a/plugins/channelrx/filesink/filesinksink.h b/plugins/channelrx/filesink/filesinksink.h new file mode 100644 index 000000000..caca68ff4 --- /dev/null +++ b/plugins/channelrx/filesink/filesinksink.h @@ -0,0 +1,86 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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_FILESINKSINK_H_ +#define INCLUDE_FILESINKSINK_H_ + +#include "dsp/channelsamplesink.h" +#include "dsp/filerecord.h" +#include "dsp/decimatorc.h" +#include "dsp/samplesimplefifo.h" +#include "dsp/ncof.h" + +#include "filesinksettings.h" + +class FileRecordInterface; +class SpectrumVis; + +class FileSinkSink : public ChannelSampleSink { +public: + FileSinkSink(); + ~FileSinkSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + FileRecord *getFileSink() { return &m_fileSink; } + void setSpectrumSink(SpectrumVis* spectrumSink) { m_spectrumSink = spectrumSink; } + void startRecording(); + void stopRecording(); + void setDeviceHwId(const QString& hwId) { m_deviceHwId = hwId; } + void setDeviceUId(int uid) { m_deviceUId = uid; } + void applyChannelSettings( + int channelSampleRate, + int sinkSampleRate, + int channelFrequencyOffset, + int64_t centerFrequency, + bool force = false); + void applySettings(const FileSinkSettings& settings, bool force = false); + uint64_t getMsCount() const { return m_msCount; } + uint64_t getByteCount() const { return m_byteCount; } + unsigned int getNbTracks() const { return m_nbCaptures; } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_msgQueueToGUI = messageQueue; } + void squelchRecording(bool squelchOpen); + int getSampleRate() const { return m_sinkSampleRate; } + bool isRecording() const { return m_record; } + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_sinkSampleRate; + int64_t m_centerFrequency; + NCOF m_nco; + DecimatorC m_decimator; + SampleVector m_sampleBuffer; + FileSinkSettings m_settings; + FileRecord m_fileSink; + unsigned int m_nbCaptures; + SampleSimpleFifo m_preRecordBuffer; + unsigned int m_preRecordFill; + float m_squelchLevel; + SpectrumVis* m_spectrumSink; + MessageQueue *m_msgQueueToGUI; + bool m_recordEnabled; + bool m_record; + bool m_squelchOpen; + int m_postSquelchCounter; + QString m_deviceHwId; + int m_deviceUId; + uint64_t m_msCount; + uint64_t m_byteCount; +}; + +#endif // INCLUDE_FILESINKSINK_H_ diff --git a/plugins/channelrx/filesink/filesinkwebapiadapter.cpp b/plugins/channelrx/filesink/filesinkwebapiadapter.cpp new file mode 100644 index 000000000..9b7f85747 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkwebapiadapter.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "filesink.h" +#include "filesinkwebapiadapter.h" + +FileSinkWebAPIAdapter::FileSinkWebAPIAdapter() +{} + +FileSinkWebAPIAdapter::~FileSinkWebAPIAdapter() +{} + +int FileSinkWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + (void) response; + response.setSigMfFileSinkSettings(new SWGSDRangel::SWGSigMFFileSinkSettings()); + response.getSigMfFileSinkSettings()->init(); + FileSink::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int FileSinkWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + FileSink::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/filesink/filesinkwebapiadapter.h b/plugins/channelrx/filesink/filesinkwebapiadapter.h new file mode 100644 index 000000000..33a1b0f95 --- /dev/null +++ b/plugins/channelrx/filesink/filesinkwebapiadapter.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// 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_FILESINK_WEBAPIADAPTER_H +#define INCLUDE_FILESINK_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "filesinksettings.h" + +/** + * Standalone API adapter only for the settings + */ +class FileSinkWebAPIAdapter : public ChannelWebAPIAdapter { +public: + FileSinkWebAPIAdapter(); + virtual ~FileSinkWebAPIAdapter(); + + 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: + FileSinkSettings m_settings; +}; + +#endif // INCLUDE_FILESINK_WEBAPIADAPTER_H diff --git a/plugins/channelrx/filesink/readme.md b/plugins/channelrx/filesink/readme.md new file mode 100644 index 000000000..9b450c9c0 --- /dev/null +++ b/plugins/channelrx/filesink/readme.md @@ -0,0 +1,99 @@ +

File Recorder

+ +

Introduction

+ +Use this plugin to record its channel IQ data in [sdriq](../../samplesource/fileinput/readme.md#introduction) format. The baseband sample rate can be decimated by a factor of two and its center shifted to accomodate different requirements than recording the full baseband. More than one such plugin can be used in the same baseband to record different parts of the baseband spectrum. Of course in this case file output collision should be avoided. + +Such files can be read in SDRangel using the [File input plugin](../../samplesource/fileinput/readme.md). + +Each recording is written in a new file with the starting timestamp before the `.sdriq` extension in `yyyy-MM-ddTHH_mm_ss_zzz` format. It keeps the first dot limted groups of the filename before the `.sdriq` extension if there are two such groups or before the two last groups if there are more than two groups. Examples: + + - Given file name: `test.sdriq` then a recording file will be like: `test.2020-08-05T21_39_07_974.sdriq` + - Given file name: `test.2020-08-05T20_36_15_974.sdriq` then a recording file will be like (with timestamp updated): `test.2020-08-05T21_41_21_173.sdriq` + - Given file name: `test.first.sdriq` then a recording file will be like: `test.2020-08-05T22_00_07_974.sdriq` + - Given file name: `record.test.first.sdriq` then a recording file will be like: `reocrd.test.2020-08-05T21_39_52_974.sdriq` + +

Interface

+ +![File Sink plugin GUI](../../../doc/img/FileSink_plugin.png) + +

1: Frequency shift from center frequency of reception

+ +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +If the fixed position slider is engaged (7) this control is disabled. + +

2: Decimation factor

+ +Use this control to decimate the baseband samples by a power of two. Consequently the baseband sample rate is reduced by this factor in the channel. + +

3: Channel (sink) sample rate

+ +Shows the channel sink sample rate in kS/s. The recod capture is effectively recorded at this rate. + +

4: Number of recordings in session

+ +Adding a new File Sink plugin instance or changing the file name starts a new session. This is the number of files recorded in the session (not counting the current one if recording). + +

5: Recording time

+ +This is the total recording time for the session. + +

6: Record size

+ +This is the total number of bytes recorded for the session.The number is possibly suffixed by a multiplier character: + - **k**: _kilo_ for kilobytes + - **M**: _mega_ for meabytes + - **G**: _giga_ for gigabytes + +

7: Fixed frequency shift positions

+ +Use the checkbox to move the shift frequency at definite positions where the chain of half band decimation filters match an exact bandwidth and shift. The effect is to bypass the last interpolator and NCO and thus can save CPU cycles. This may be useful at high sample rates at the expense of not getting exactly on the desired spot. + +Use the slider to move position from lower to higher frequency. The position index appears on the right. The higher the decimation factor the more positions are available as the decimated bandwidth gets smaller. + +When this is engaged the frequency control (1) is disabled. + +This is a GUI only feature when using API it is up to the API client to calculate the desired position. Starting from the center any position lower or higher at the bandwidth divided by two times the decimation factor yields the desired property. + +

8: Spectrum squelch

+ +Recording can be triggered by specifying a power level. If any peak in the spectrum exceeds this level then recording is triggered. This button only activates the detection system. When squelch is open the level button (9) lits up in green (as on the screenshot). To activate triggered recording you have to use (12). + +You can try to see for which squelch level you obtain the desired triggering before actually applying it to control the record process with button (12). + +Note that spectrum polling is done every 200 ms. If the signal of interest is shorter you may want to tweak the spectrum controls using the "Maximum" averaging type and a number of averaging samples making the averaging period (appearing in the tooltip) larger than 200 ms. Please refer to spectrum controls documentation in the main window readme (link in 15) for more details. + +

9: Squelch level

+ +This is the squelch level as discussed above. To try to find the correct value you can use the spectrum display (15). + +

10: Pre recording period

+ +This is the number of seconds of data that will be prepended before the start of recording point. Thus you can make sure that the signal of interest will be fully recorded. Works in both spectrum squelch triggered and manual mode. + +

11: Post recording period

+ +This applies to spectrum squelch triggered recording only. This is the number of seconds recorded after the squelch closes. If the squelch opens again during this period then the counter is reset and recording will stop only after this period of time is elapsed without the squelch re-opening. + +This is useful if you want to record a bunch of transient bursts or just make sure that the recording does not stop too abruptly. + +

12: Enable/disble spectrum squelch triggered recording

+ +Use this button to effectively apply spectrum squelch to recording. In this mode recording on and off will be under the control of the squelch system. Thus when active the normal record button (13) is disabled. However its color changes to reflect the recording status as described next. + +

13: Record button

+ +Use this button to start/stop recording unconditionnaly. Note that start/stop recording is opening/closing a new file in the same session. Until the file is changed with (14) the same file root will be used until the device is stopped or channel plugin is dismissed. + +The button turns red if recording is active. + +

14: Select output file

+ +Use this button to open a file dialog that lets you specify the location and name of the output files. Please refer to the indtroduction at the top of this page for details on how the recording file name is composed from the given file name. + +The file path currently being written (or last closed) appears at the right of the button. + +

15: Channel spectrum

+ +This is the spectrum display of the IQ stream seen by the channel. It is the same as all spectrum displays in the program and is identical to the [main window](../../../sdrgui/readme.md#) spectrum display. diff --git a/sdrbase/dsp/filerecord.cpp b/sdrbase/dsp/filerecord.cpp index 59630c260..4a7d59ea0 100644 --- a/sdrbase/dsp/filerecord.cpp +++ b/sdrbase/dsp/filerecord.cpp @@ -19,6 +19,7 @@ #include #include +#include #include "dsp/dspcommands.h" #include "util/simpleserializer.h" @@ -28,19 +29,20 @@ FileRecord::FileRecord() : FileRecordInterface(), - m_fileName("test.sdriq"), + m_fileBase("test"), m_sampleRate(0), m_centerFrequency(0), m_recordOn(false), m_recordStart(false), - m_byteCount(0) + m_byteCount(0), + m_msShift(0) { setObjectName("FileRecord"); } -FileRecord::FileRecord(const QString& filename) : +FileRecord::FileRecord(const QString& fileBase) : FileRecordInterface(), - m_fileName(filename), + m_fileBase(fileBase), m_sampleRate(0), m_centerFrequency(0), m_recordOn(false), @@ -55,11 +57,11 @@ FileRecord::~FileRecord() stopRecording(); } -void FileRecord::setFileName(const QString& filename) +void FileRecord::setFileName(const QString& fileBase) { if (!m_recordOn) { - m_fileName = filename; + m_fileBase = fileBase; } } @@ -94,13 +96,19 @@ void FileRecord::stop() void FileRecord::startRecording() { + if (m_recordOn) { + stopRecording(); + } + if (!m_sampleFile.is_open()) { qDebug() << "FileRecord::startRecording"; - m_sampleFile.open(m_fileName.toStdString().c_str(), std::ios::binary); + m_curentFileName = QString("%1.%2.sdriq").arg(m_fileBase).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz")); + m_sampleFile.open(m_curentFileName.toStdString().c_str(), std::ios::binary); m_recordOn = true; m_recordStart = true; m_byteCount = 0; + writeHeader(); } } @@ -124,7 +132,12 @@ bool FileRecord::handleMessage(const Message& message) m_centerFrequency = notif.getCenterFrequency(); qDebug() << "FileRecord::handleMessage: DSPSignalNotification: m_inputSampleRate: " << m_sampleRate << " m_centerFrequency: " << m_centerFrequency; - return true; + + if (m_recordOn) { + startRecording(); + } + + return true; } else { @@ -132,23 +145,13 @@ bool FileRecord::handleMessage(const Message& message) } } -void FileRecord::handleConfigure(const QString& fileName) -{ - if (fileName != m_fileName) - { - stopRecording(); - } - - m_fileName = fileName; -} - void FileRecord::writeHeader() { Header header; header.sampleRate = m_sampleRate; header.centerFrequency = m_centerFrequency; std::time_t ts = time(0); - header.startTimeStamp = ts; + header.startTimeStamp = ts + (m_msShift / 1000); header.sampleSize = SDR_RX_SAMP_SZ; header.filler = 0; diff --git a/sdrbase/dsp/filerecord.h b/sdrbase/dsp/filerecord.h index ec0bd5ee6..a41afa68f 100644 --- a/sdrbase/dsp/filerecord.h +++ b/sdrbase/dsp/filerecord.h @@ -44,17 +44,19 @@ public: #pragma pack(pop) FileRecord(); - FileRecord(const QString& filename); + FileRecord(const QString& fileBase); virtual ~FileRecord(); quint64 getByteCount() const { return m_byteCount; } + void setMsShift(int shift) { m_msShift = shift; } + const QString& getCurrentFileName() { return m_curentFileName; } virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly); virtual void start(); virtual void stop(); virtual bool handleMessage(const Message& message); - virtual void setFileName(const QString& filename); + virtual void setFileName(const QString& fileBase); virtual void startRecording(); virtual void stopRecording(); virtual bool isRecording() const { return m_recordOn; } @@ -63,15 +65,16 @@ public: static void writeHeader(std::ofstream& samplefile, Header& header); private: - QString m_fileName; + QString m_fileBase; quint32 m_sampleRate; quint64 m_centerFrequency; bool m_recordOn; bool m_recordStart; std::ofstream m_sampleFile; + QString m_curentFileName; quint64 m_byteCount; + int m_msShift; - void handleConfigure(const QString& fileName); void writeHeader(); }; diff --git a/sdrbase/dsp/filerecordinterface.cpp b/sdrbase/dsp/filerecordinterface.cpp index 4dde783a8..0ee8ed118 100644 --- a/sdrbase/dsp/filerecordinterface.cpp +++ b/sdrbase/dsp/filerecordinterface.cpp @@ -31,9 +31,9 @@ FileRecordInterface::~FileRecordInterface() QString FileRecordInterface::genUniqueFileName(unsigned int deviceUID, int istream) { if (istream < 0) { - return QString("rec%1_%2.sdriq").arg(deviceUID).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz")); + return QString("rec%1.%2.sdriq").arg(deviceUID).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz")); } else { - return QString("rec%1_%2_%3.sdriq").arg(deviceUID).arg(istream).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz")); + return QString("rec%1_%2.%3.sdriq").arg(deviceUID).arg(istream).arg(QDateTime::currentDateTimeUtc().toString("yyyy-MM-ddTHH_mm_ss_zzz")); } } @@ -48,6 +48,10 @@ FileRecordInterface::RecordType FileRecordInterface::guessTypeFromFileName(const if (extension == "sdriq") { + if (dotBreakout.length() > 1) { + dotBreakout.removeLast(); + } + fileBase = dotBreakout.join(QLatin1Char('.')); return RecordTypeSdrIQ; }