diff --git a/doc/img/APTDemod_plugin.png b/doc/img/APTDemod_plugin.png new file mode 100644 index 000000000..24d98125b Binary files /dev/null and b/doc/img/APTDemod_plugin.png differ diff --git a/doc/img/APTDemod_plugin_settings.png b/doc/img/APTDemod_plugin_settings.png new file mode 100644 index 000000000..5934b4e54 Binary files /dev/null and b/doc/img/APTDemod_plugin_settings.png differ diff --git a/doc/img/APTDemod_plugin_settingsdialog.png b/doc/img/APTDemod_plugin_settingsdialog.png new file mode 100644 index 000000000..c98d10d0e Binary files /dev/null and b/doc/img/APTDemod_plugin_settingsdialog.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index fc20e0df6..90a06fa44 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -18,6 +18,10 @@ add_subdirectory(demodchirpchat) add_subdirectory(demodvorsc) add_subdirectory(demodpacket) +if(APT_FOUND) + add_subdirectory(demodapt) +endif() + if(LIBDSDCC_FOUND AND LIBMBE_FOUND) add_subdirectory(demoddsd) endif(LIBDSDCC_FOUND AND LIBMBE_FOUND) diff --git a/plugins/channelrx/demodapt/CMakeLists.txt b/plugins/channelrx/demodapt/CMakeLists.txt new file mode 100644 index 000000000..2eeacf6de --- /dev/null +++ b/plugins/channelrx/demodapt/CMakeLists.txt @@ -0,0 +1,68 @@ +project(demodapt) + +set(demodapt_SOURCES + aptdemod.cpp + aptdemodsettings.cpp + aptdemodbaseband.cpp + aptdemodsink.cpp + aptdemodplugin.cpp + aptdemodwebapiadapter.cpp +) + +set(demodapt_HEADERS + aptdemod.h + aptdemodsettings.h + aptdemodbaseband.h + aptdemodsink.h + aptdemodplugin.h + aptdemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${APT_INCLUDE_DIR} +) + +if(NOT SERVER_MODE) + set(demodapt_SOURCES + ${demodapt_SOURCES} + aptdemodgui.cpp + aptdemodgui.ui + aptdemodsettingsdialog.cpp + aptdemodsettingsdialog.ui + icons.qrc + ) + set(demodapt_HEADERS + ${demodapt_HEADERS} + aptdemodgui.h + aptdemodsettingsdialog.h + ) + + set(TARGET_NAME demodapt) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodaptsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demodapt_SOURCES} +) + +if(APT_EXTERNAL) + add_dependencies(${TARGET_NAME} apt) +endif() + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + ${APT_LIBRARIES} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/channelrx/demodapt/aptdemod.cpp b/plugins/channelrx/demodapt/aptdemod.cpp new file mode 100644 index 000000000..86f9f65c2 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemod.cpp @@ -0,0 +1,851 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aptdemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGAPTDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGChannelActions.h" +#include "SWGMapItem.h" +#include "SWGAPTDemodActions.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(APTDemod::MsgConfigureAPTDemod, Message) +MESSAGE_CLASS_DEFINITION(APTDemod::MsgPixels, Message) +MESSAGE_CLASS_DEFINITION(APTDemod::MsgImage, Message) +MESSAGE_CLASS_DEFINITION(APTDemod::MsgResetDecoder, Message) + +const char * const APTDemod::m_channelIdURI = "sdrangel.channel.aptdemod"; +const char * const APTDemod::m_channelId = "APTDemod"; + +APTDemod::APTDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new APTDemodBaseband(this); + m_basebandSink->setMessageQueueToChannel(getInputMessageQueue()); + 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*))); + + for (int y = 0; y < APT_MAX_HEIGHT; y++) + { + m_image.prow[y] = new float[APT_PROW_WIDTH]; + m_tempImage.prow[y] = new float[APT_PROW_WIDTH]; + } + resetDecoder(); +} + +APTDemod::~APTDemod() +{ + qDebug("APTDemod::~APTDemod"); + 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; + + for (int y = 0; y < APT_MAX_HEIGHT; y++) + { + delete m_image.prow[y]; + delete m_tempImage.prow[y]; + } +} + +uint32_t APTDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void APTDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void APTDemod::start() +{ + qDebug("APTDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + APTDemodBaseband::MsgConfigureAPTDemodBaseband *msg = APTDemodBaseband::MsgConfigureAPTDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void APTDemod::stop() +{ + qDebug("APTDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool APTDemod::matchSatellite(const QString satelliteName) +{ + return m_settings.m_satelliteTrackerControl + && ( (satelliteName == m_settings.m_satelliteName) + || ( (m_settings.m_satelliteName == "All") + && ( (satelliteName == "NOAA 15") + || (satelliteName == "NOAA 18") + || (satelliteName == "NOAA 19")))); +} + +bool APTDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureAPTDemod::match(cmd)) + { + MsgConfigureAPTDemod& cfg = (MsgConfigureAPTDemod&) cmd; + qDebug() << "APTDemod::handleMessage: MsgConfigureAPTDemod"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "APTDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + // Forward to GUI if any + if (m_guiMessageQueue) + m_guiMessageQueue->push(new DSPSignalNotification(notif)); + + return true; + } + else if (APTDemod::MsgPixels::match(cmd)) + { + const APTDemod::MsgPixels& pixelsMsg = (APTDemod::MsgPixels&) cmd; + const float *pixels = pixelsMsg.getPixels(); + processPixels(pixels); + return true; + } + else if (APTDemod::MsgResetDecoder::match(cmd)) + { + resetDecoder(); + // Forward to sink + m_basebandSink->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); + return true; + } + else + { + return false; + } +} + +void APTDemod::applySettings(const APTDemodSettings& settings, bool force) +{ + bool callProcessImage = false; + + qDebug() << "APTDemod::applySettings:" + << " m_cropNoise: " << settings.m_cropNoise + << " m_denoise: " << settings.m_denoise + << " m_linearEqualise: " << settings.m_linearEqualise + << " m_histogramEqualise: " << settings.m_histogramEqualise + << " m_precipitationOverlay: " << settings.m_precipitationOverlay + << " m_flip: " << settings.m_flip + << " m_channels: " << settings.m_channels + << " m_decodeEnabled: " << settings.m_decodeEnabled + << " m_autoSave: " << settings.m_autoSave + << " m_autoSavePath: " << settings.m_autoSavePath + << " m_autoSaveMinScanLines: " << settings.m_autoSaveMinScanLines + << " m_streamIndex: " << settings.m_streamIndex + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) { + reverseAPIKeys.append("fmDeviation"); + } + if ((settings.m_denoise != m_settings.m_denoise) || force) { + reverseAPIKeys.append("denoise"); + } + if ((settings.m_linearEqualise != m_settings.m_linearEqualise) || force) { + reverseAPIKeys.append("linearEqualise"); + } + if ((settings.m_histogramEqualise != m_settings.m_histogramEqualise) || force) { + reverseAPIKeys.append("histogramEqualise"); + } + if ((settings.m_precipitationOverlay != m_settings.m_precipitationOverlay) || force) { + reverseAPIKeys.append("precipitationOverlay"); + } + if ((settings.m_flip != m_settings.m_flip) || force) { + reverseAPIKeys.append("flip"); + } + if ((settings.m_channels != m_settings.m_channels) || force) { + reverseAPIKeys.append("channels"); + } + if ((settings.m_decodeEnabled != m_settings.m_decodeEnabled) || force) { + reverseAPIKeys.append("decodeEnabled"); + } + if ((settings.m_autoSave != m_settings.m_autoSave) || force) { + reverseAPIKeys.append("autoSave"); + } + if ((settings.m_autoSavePath != m_settings.m_autoSavePath) || force) { + reverseAPIKeys.append("autoSavePath"); + } + if ((settings.m_autoSaveMinScanLines != m_settings.m_autoSaveMinScanLines) || force) { + reverseAPIKeys.append("autoSaveMinScanLines"); + } + + 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"); + } + + APTDemodBaseband::MsgConfigureAPTDemodBaseband *msg = APTDemodBaseband::MsgConfigureAPTDemodBaseband::create(settings, force); + m_basebandSink->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + if ((settings.m_cropNoise != m_settings.m_cropNoise) || + (settings.m_denoise != m_settings.m_denoise) || + (settings.m_linearEqualise != m_settings.m_linearEqualise) || + (settings.m_histogramEqualise != m_settings.m_histogramEqualise) || + (settings.m_precipitationOverlay != m_settings.m_precipitationOverlay) || + (settings.m_flip != m_settings.m_flip) || + (settings.m_channels != m_settings.m_channels)) + { + // Call after settings have been applied + callProcessImage = true; + } + + m_settings = settings; + + if (callProcessImage) + sendImageToGUI(); +} + +QByteArray APTDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool APTDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureAPTDemod *msg = MsgConfigureAPTDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureAPTDemod *msg = MsgConfigureAPTDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int APTDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAptDemodSettings(new SWGSDRangel::SWGAPTDemodSettings()); + response.getAptDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int APTDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + APTDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureAPTDemod *msg = MsgConfigureAPTDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("APTDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAPTDemod *msgToGUI = MsgConfigureAPTDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void APTDemod::webapiUpdateChannelSettings( + APTDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getAptDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("fmDeviation")) { + settings.m_fmDeviation = response.getAptDemodSettings()->getFmDeviation(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getAptDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("cropNoise")) { + settings.m_cropNoise = response.getAptDemodSettings()->getCropNoise(); + } + if (channelSettingsKeys.contains("denoise")) { + settings.m_denoise = response.getAptDemodSettings()->getDenoise(); + } + if (channelSettingsKeys.contains("linearEqualise")) { + settings.m_linearEqualise = response.getAptDemodSettings()->getLinearEqualise(); + } + if (channelSettingsKeys.contains("histogramEqualise")) { + settings.m_histogramEqualise = response.getAptDemodSettings()->getHistogramEqualise(); + } + if (channelSettingsKeys.contains("precipitationOverlay")) { + settings.m_precipitationOverlay = response.getAptDemodSettings()->getPrecipitationOverlay(); + } + if (channelSettingsKeys.contains("flip")) { + settings.m_flip = response.getAptDemodSettings()->getFlip(); + } + if (channelSettingsKeys.contains("channels")) { + settings.m_channels = (APTDemodSettings::ChannelSelection)response.getAptDemodSettings()->getChannels(); + } + if (channelSettingsKeys.contains("decodeEnabled")) { + settings.m_decodeEnabled = response.getAptDemodSettings()->getDecodeEnabled(); + } + if (channelSettingsKeys.contains("autoSave")) { + settings.m_autoSave = response.getAptDemodSettings()->getAutoSave(); + } + if (channelSettingsKeys.contains("autoSavePath")) { + settings.m_autoSavePath = *response.getAptDemodSettings()->getAutoSavePath(); + } + if (channelSettingsKeys.contains("autoSaveMinScanLines")) { + settings.m_autoSaveMinScanLines = response.getAptDemodSettings()->getAutoSaveMinScanLines(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getAptDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getAptDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getAptDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAptDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAptDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAptDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getAptDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getAptDemodSettings()->getReverseApiChannelIndex(); + } +} + +void APTDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const APTDemodSettings& settings) +{ + response.getAptDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getAptDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getAptDemodSettings()->setFmDeviation(settings.m_fmDeviation); + response.getAptDemodSettings()->setCropNoise(settings.m_cropNoise); + response.getAptDemodSettings()->setCropNoise(settings.m_denoise); + response.getAptDemodSettings()->setLinearEqualise(settings.m_linearEqualise); + response.getAptDemodSettings()->setHistogramEqualise(settings.m_histogramEqualise); + response.getAptDemodSettings()->setPrecipitationOverlay(settings.m_precipitationOverlay); + response.getAptDemodSettings()->setFlip(settings.m_flip); + response.getAptDemodSettings()->setChannels((int)settings.m_channels); + response.getAptDemodSettings()->setDecodeEnabled(settings.m_decodeEnabled); + response.getAptDemodSettings()->setAutoSave(settings.m_autoSave); + response.getAptDemodSettings()->setAutoSavePath(new QString(settings.m_autoSavePath)); + response.getAptDemodSettings()->setAutoSaveMinScanLines(settings.m_autoSaveMinScanLines); + + response.getAptDemodSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getAptDemodSettings()->getTitle()) { + *response.getAptDemodSettings()->getTitle() = settings.m_title; + } else { + response.getAptDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getAptDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getAptDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAptDemodSettings()->getReverseApiAddress()) { + *response.getAptDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAptDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAptDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAptDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getAptDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); +} + +void APTDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const APTDemodSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgChannelSettings; +} + +void APTDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const APTDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("APTDemod")); + swgChannelSettings->setAptDemodSettings(new SWGSDRangel::SWGAPTDemodSettings()); + SWGSDRangel::SWGAPTDemodSettings *swgAPTDemodSettings = swgChannelSettings->getAptDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgAPTDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgAPTDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("fmDeviation") || force) { + swgAPTDemodSettings->setFmDeviation(settings.m_fmDeviation); + } + if (channelSettingsKeys.contains("cropNoise") || force) { + swgAPTDemodSettings->setCropNoise(settings.m_cropNoise); + } + if (channelSettingsKeys.contains("denoise") || force) { + swgAPTDemodSettings->setDenoise(settings.m_denoise); + } + if (channelSettingsKeys.contains("linearEqualise") || force) { + swgAPTDemodSettings->setLinearEqualise(settings.m_linearEqualise); + } + if (channelSettingsKeys.contains("histogramEqualise") || force) { + swgAPTDemodSettings->setHistogramEqualise(settings.m_histogramEqualise); + } + if (channelSettingsKeys.contains("precipitationOverlay") || force) { + swgAPTDemodSettings->setPrecipitationOverlay(settings.m_precipitationOverlay); + } + if (channelSettingsKeys.contains("flip") || force) { + swgAPTDemodSettings->setFlip(settings.m_flip); + } + if (channelSettingsKeys.contains("channels") || force) { + swgAPTDemodSettings->setChannels((int)settings.m_channels); + } + if (channelSettingsKeys.contains("decodeEnabled") || force) { + swgAPTDemodSettings->setDecodeEnabled(settings.m_decodeEnabled); + } + if (channelSettingsKeys.contains("m_autoSave") || force) { + swgAPTDemodSettings->setAutoSave(settings.m_autoSave); + } + if (channelSettingsKeys.contains("m_autoSavePath") || force) { + swgAPTDemodSettings->setAutoSavePath(new QString(settings.m_autoSavePath)); + } + if (channelSettingsKeys.contains("m_autoSaveMinScanLines") || force) { + swgAPTDemodSettings->setAutoSaveMinScanLines(settings.m_autoSaveMinScanLines); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgAPTDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgAPTDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgAPTDemodSettings->setStreamIndex(settings.m_streamIndex); + } +} + +int APTDemod::webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage) +{ + SWGSDRangel::SWGAPTDemodActions *swgAPTDemodActions = query.getAptDemodActions(); + + if (swgAPTDemodActions) + { + if (channelActionsKeys.contains("aos")) + { + qDebug() << "Aos action"; + SWGSDRangel::SWGAPTDemodActions_aos* aos = swgAPTDemodActions->getAos(); + QString *satelliteName = aos->getSatelliteName(); + if (satelliteName != nullptr) + { + qDebug() << "sat " << *satelliteName; + if (matchSatellite(*satelliteName)) + { + qDebug() << "Matched sat"; + // Reset for new pass + resetDecoder(); + m_basebandSink->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); + + // Save satellite name + m_satelliteName = *satelliteName; + + // Enable decoder and set direction of pass + APTDemodSettings settings = m_settings; + settings.m_decodeEnabled = true; + settings.m_flip = !aos->getNorthToSouthPass(); + qDebug() << "Sending settings"; + m_inputMessageQueue.push(MsgConfigureAPTDemod::create(settings, false)); + if (m_guiMessageQueue) + m_guiMessageQueue->push(MsgConfigureAPTDemod::create(settings, false)); + } + + return 202; + } + else + { + errorMessage = "Missing satellite name"; + return 400; + } + } + else if (channelActionsKeys.contains("los")) + { + SWGSDRangel::SWGAPTDemodActions_los* los = swgAPTDemodActions->getLos(); + QString *satelliteName = los->getSatelliteName(); + if (satelliteName != nullptr) + { + if (matchSatellite(*satelliteName)) + { + // Save image + if (m_settings.m_autoSave) + saveImageToDisk(); + // Disable decoder + APTDemodSettings settings = m_settings; + settings.m_decodeEnabled = false; + m_inputMessageQueue.push(MsgConfigureAPTDemod::create(settings, false)); + if (m_guiMessageQueue) + m_guiMessageQueue->push(MsgConfigureAPTDemod::create(settings, false)); + } + + return 202; + } + else + { + errorMessage = "Missing satellite name"; + return 400; + } + } + else + { + errorMessage = "Unknown action"; + return 400; + } + } + else + { + errorMessage = "Missing APTDemodActions in query"; + return 400; + } +} + +void APTDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "APTDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("APTDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void APTDemod::resetDecoder() +{ + m_image.nrow = 0; + m_tempImage.nrow = 0; + m_greyImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_Grayscale8); + m_greyImage.fill(0); + m_colourImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_RGB888); + m_colourImage.fill(0); + m_satelliteName = ""; +} + +void APTDemod::processPixels(const float *pixels) +{ + memcpy(m_image.prow[m_image.nrow], pixels, sizeof(float) * APT_PROW_WIDTH); + m_image.nrow++; + sendImageToGUI(); +} + +static void copyImage(apt_image_t *dst, apt_image_t *src) +{ + dst->nrow = src->nrow; + dst->zenith = src->zenith; + dst->chA = src->chA; + dst->chB = src->chB; + for (int i = 0; i < src->nrow; i++) + memcpy(dst->prow[i], src->prow[i], sizeof(float) * APT_PROW_WIDTH); +} + +static uchar roundAndClip(float p) +{ + int q = (int)round(p); + if (q > 255) + q = 255; + else if (q < 0) + q = 0; + return q; +} + +QImage APTDemod::extractImage(QImage image) +{ + if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) + return image.copy(0, 0, APT_IMG_WIDTH, m_tempImage.nrow); + else if (m_settings.m_channels == APTDemodSettings::CHANNEL_A) + return image.copy(APT_CHA_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow); + else + return image.copy(APT_CHB_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow); +} + +QImage APTDemod::processImage(QStringList& imageTypes) +{ + copyImage(&m_tempImage, &m_image); + + // Calibrate channels according to wavelength + if (m_tempImage.nrow >= APT_CALIBRATION_ROWS) + { + m_tempImage.chA = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + m_tempImage.chB = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + QStringList channelTypes({ + "", // Unknown + "Visible (0.58-0.68 um)", + "Near-IR (0.725-1.0 um)", + "Near-IR (1.58-1.64 um)", + "Mid-infrared (3.55-3.93 um)", + "Thermal-infrared (10.3-11.3 um)", + "Thermal-infrared (11.5-12.5 um)" + }); + + imageTypes.append(channelTypes[m_tempImage.chA]); + imageTypes.append(channelTypes[m_tempImage.chB]); + } + + // Crop noise due to low elevation at top and bottom of image + if (m_settings.m_cropNoise) + m_tempImage.zenith -= apt_cropNoise(&m_tempImage); + + // Denoise filter + if (m_settings.m_denoise) + { + apt_denoise(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + apt_denoise(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + } + + // Flip image if satellite pass is North to South + if (m_settings.m_flip) + { + apt_flipImage(&m_tempImage, APT_CH_WIDTH, APT_CHA_OFFSET); + apt_flipImage(&m_tempImage, APT_CH_WIDTH, APT_CHB_OFFSET); + } + + // Linear equalise to improve contrast + if (m_settings.m_linearEqualise) + { + apt_linearEnhance(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + apt_linearEnhance(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + } + + // Histogram equalise to improve contrast + if (m_settings.m_histogramEqualise) + { + apt_histogramEqualise(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); + apt_histogramEqualise(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); + } + + if (m_settings.m_precipitationOverlay) + { + // Overlay precipitation + for (int r = 0; r < m_tempImage.nrow; r++) + { + uchar *l = m_colourImage.scanLine(r); + for (int i = 0; i < APT_IMG_WIDTH; i++) + { + float p = m_tempImage.prow[r][i]; + + if ((i >= APT_CHB_OFFSET) && (i < APT_CHB_OFFSET + APT_CH_WIDTH) && (p >= 198)) + { + apt_rgb_t rgb = apt_applyPalette(apt_PrecipPalette, p - 198); + // Negative float values get converted to positive uchars here + l[i*3] = (uchar)rgb.r; + l[i*3+1] = (uchar)rgb.g; + l[i*3+2] = (uchar)rgb.b; + int a = i - APT_CHB_OFFSET + APT_CHA_OFFSET; + l[a*3] = (uchar)rgb.r; + l[a*3+1] = (uchar)rgb.g; + l[a*3+2] = (uchar)rgb.b; + } + else + { + uchar q = roundAndClip(p); + l[i*3] = q; + l[i*3+1] = q; + l[i*3+2] = q; + } + } + } + return extractImage(m_colourImage); + } + else + { + for (int r = 0; r < m_tempImage.nrow; r++) + { + uchar *l = m_greyImage.scanLine(r); + for (int i = 0; i < APT_IMG_WIDTH; i++) + { + float p = m_tempImage.prow[r][i]; + l[i] = roundAndClip(p); + } + } + return extractImage(m_greyImage); + } +} + +void APTDemod::sendImageToGUI() +{ + // Send image to GUI + if (getMessageQueueToGUI()) + { + QStringList imageTypes; + QImage image = processImage(imageTypes); + getMessageQueueToGUI()->push(APTDemod::MsgImage::create(image, imageTypes, m_satelliteName)); + } +} + +void APTDemod::saveImageToDisk() +{ + QStringList imageTypes; + QImage image = processImage(imageTypes); + if (image.height() >= m_settings.m_autoSaveMinScanLines) + { + QString filename; + QDateTime datetime = QDateTime::currentDateTime(); + filename = QString("apt_%1_%2.png").arg(m_satelliteName).arg(datetime.toString("yyyyMMdd_hhmm")); + if (!m_settings.m_autoSavePath.isEmpty()) + { + if (m_settings.m_autoSavePath.endsWith('/')) + filename = m_settings.m_autoSavePath + filename; + else + filename = m_settings.m_autoSavePath + '/' + filename; + } + if (!image.save(filename)) + qCritical() << "Failed to save APT image to: " << filename; + } +} diff --git a/plugins/channelrx/demodapt/aptdemod.h b/plugins/channelrx/demodapt/aptdemod.h new file mode 100644 index 000000000..f38341e6a --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemod.h @@ -0,0 +1,242 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMOD_H +#define INCLUDE_APTDEMOD_H + +#include + +#include +#include +#include + +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "aptdemodbaseband.h" +#include "aptdemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; + +class APTDemod : public BasebandSampleSink, public ChannelAPI { + Q_OBJECT +public: + class MsgConfigureAPTDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const APTDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAPTDemod* create(const APTDemodSettings& settings, bool force) + { + return new MsgConfigureAPTDemod(settings, force); + } + + private: + APTDemodSettings m_settings; + bool m_force; + + MsgConfigureAPTDemod(const APTDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + // One row of pixels from sink + class MsgPixels : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const float *getPixels() const { return m_pixels; } + int getZenith() const { return m_zenith; } + + static MsgPixels* create(const float *pixels, int zenith) + { + return new MsgPixels(pixels, zenith); + } + + private: + float m_pixels[APT_PROW_WIDTH]; + int m_zenith; + + MsgPixels(const float *pixels, int zenith) : + Message(), + m_zenith(zenith) + { + memcpy(m_pixels, pixels, sizeof(m_pixels)); + } + }; + + // Processed image to be sent to GUI + class MsgImage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QImage getImage() const { return m_image; } + const QStringList getImageTypes() const { return m_imageTypes; } + const QString getSatelliteName() const { return m_satelliteName; } + + static MsgImage* create(const QImage image, const QStringList imageTypes, const QString satelliteName) + { + return new MsgImage(image, imageTypes, satelliteName); + } + + private: + QImage m_image; + QStringList m_imageTypes; + QString m_satelliteName; + + MsgImage(const QImage image, const QStringList imageTypes, const QString satelliteName) : + Message(), + m_image(image), + m_imageTypes(imageTypes), + m_satelliteName(satelliteName) + { + } + }; + + // Sent from GUI to reset decoder + class MsgResetDecoder : public Message { + MESSAGE_CLASS_DECLARATION + + public: + static MsgResetDecoder* create() + { + return new MsgResetDecoder(); + } + + private: + + MsgResetDecoder() : + Message() + { + } + }; + + APTDemod(DeviceAPI *deviceAPI); + virtual ~APTDemod(); + virtual void destroy() { delete this; } + + using BasebandSampleSink::feed; + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual const QString& getURI() const { return getName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return 0; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return 0; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const APTDemodSettings& settings); + + static void webapiUpdateChannelSettings( + APTDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } + + uint32_t getNumberOfDeviceStreams() const; + + static const char * const m_channelIdURI; + static const char * const m_channelId; + +private: + DeviceAPI *m_deviceAPI; + QThread m_thread; + APTDemodBaseband* m_basebandSink; + APTDemodSettings m_settings; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_centerFrequency; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + // Image buffers + apt_image_t m_image; // Received image + apt_image_t m_tempImage; // Processed image + QImage m_greyImage; + QImage m_colourImage; + QString m_satelliteName; + + void applySettings(const APTDemodSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& channelSettingsKeys, const APTDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const APTDemodSettings& settings, + bool force + ); + + bool matchSatellite(const QString satelliteName); + void resetDecoder(); + void processPixels(const float *pixels); + QImage extractImage(QImage image); + QImage processImage(QStringList& imageTypes); + void sendImageToGUI(); + void saveImageToDisk(); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + +}; + +#endif // INCLUDE_APTDEMOD_H diff --git a/plugins/channelrx/demodapt/aptdemodbaseband.cpp b/plugins/channelrx/demodapt/aptdemodbaseband.cpp new file mode 100644 index 000000000..9a700a1b7 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodbaseband.cpp @@ -0,0 +1,177 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "aptdemodbaseband.h" +#include "aptdemod.h" + +MESSAGE_CLASS_DEFINITION(APTDemodBaseband::MsgConfigureAPTDemodBaseband, Message) + +APTDemodBaseband::APTDemodBaseband(APTDemod *packetDemod) : + m_sink(packetDemod), + m_running(false), + m_mutex(QMutex::Recursive) +{ + qDebug("APTDemodBaseband::APTDemodBaseband"); + + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +APTDemodBaseband::~APTDemodBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void APTDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void APTDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &APTDemodBaseband::handleData, + Qt::QueuedConnection + ); + m_running = true; +} + +void APTDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &APTDemodBaseband::handleData + ); + m_running = false; +} + +void APTDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void APTDemodBaseband::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 APTDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool APTDemodBaseband::handleMessage(const Message& cmd) +{ + qDebug() << "APTDemodBaseband::handleMessage"; + if (MsgConfigureAPTDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureAPTDemodBaseband& cfg = (MsgConfigureAPTDemodBaseband&) cmd; + qDebug() << "APTDemodBaseband::handleMessage: MsgConfigureAPTDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "APTDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + + return true; + } + else if (APTDemod::MsgResetDecoder::match(cmd)) + { + m_sink.resetDecoder(); + return true; + } + else + { + return false; + } +} + +void APTDemodBaseband::applySettings(const APTDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(APTDEMOD_AUDIO_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +void APTDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} diff --git a/plugins/channelrx/demodapt/aptdemodbaseband.h b/plugins/channelrx/demodapt/aptdemodbaseband.h new file mode 100644 index 000000000..8acbf26fe --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodbaseband.h @@ -0,0 +1,94 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODBASEBAND_H +#define INCLUDE_APTDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "aptdemodsink.h" + +class DownChannelizer; +class APTDemod; + +class APTDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureAPTDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const APTDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAPTDemodBaseband* create(const APTDemodSettings& settings, bool force) + { + return new MsgConfigureAPTDemodBaseband(settings, force); + } + + private: + APTDemodSettings m_settings; + bool m_force; + + MsgConfigureAPTDemodBaseband(const APTDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + APTDemodBaseband(APTDemod *packetDemod); + ~APTDemodBaseband(); + void reset(); + void startWork(); + void stopWork(); + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_sink.getMagSqLevels(avg, peak, nbSamples); + } + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } + void setBasebandSampleRate(int sampleRate); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + APTDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + APTDemodSettings m_settings; + bool m_running; + QMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(APTDemodSink *sink); + void applySettings(const APTDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_APTDEMODBASEBAND_H diff --git a/plugins/channelrx/demodapt/aptdemodgui.cpp b/plugins/channelrx/demodapt/aptdemodgui.cpp new file mode 100644 index 000000000..06e724482 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodgui.cpp @@ -0,0 +1,479 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aptdemodgui.h" +#include "util/ax25.h" + +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_aptdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "util/morse.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "dsp/dspengine.h" +#include "gui/crightclickenabler.h" +#include "channel/channelwebapiutils.h" +#include "maincore.h" + +#include "aptdemod.h" +#include "aptdemodsink.h" +#include "aptdemodsettingsdialog.h" + +APTDemodGUI* APTDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + APTDemodGUI* gui = new APTDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void APTDemodGUI::destroy() +{ + delete this; +} + +void APTDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray APTDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool APTDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +bool APTDemodGUI::handleMessage(const Message& message) +{ + if (APTDemod::MsgConfigureAPTDemod::match(message)) + { + qDebug("APTDemodGUI::handleMessage: APTDemod::MsgConfigureAPTDemod"); + const APTDemod::MsgConfigureAPTDemod& cfg = (APTDemod::MsgConfigureAPTDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (APTDemod::MsgImage::match(message)) + { + const APTDemod::MsgImage& imageMsg = (APTDemod::MsgImage&) message; + m_image = imageMsg.getImage(); + m_pixmap.convertFromImage(m_image); + ui->image->setPixmap(m_pixmap); + QStringList imageTypes = imageMsg.getImageTypes(); + if (imageTypes.size() == 0) + { + ui->channelALabel->setText("Channel A"); + ui->channelBLabel->setText("Channel B"); + } + else + { + if (imageTypes[0].isEmpty()) + ui->channelALabel->setText("Channel A"); + else + ui->channelALabel->setText(imageTypes[0]); + if (imageTypes[1].isEmpty()) + ui->channelBLabel->setText("Channel B"); + else + ui->channelBLabel->setText(imageTypes[1]); + } + QString satelliteName = imageMsg.getSatelliteName(); + if (!satelliteName.isEmpty()) + ui->imageContainer->setWindowTitle("Received image from " + satelliteName); + else + ui->imageContainer->setWindowTitle("Received image"); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_basebandSampleRate = notif.getSampleRate(); + return true; + } + + return false; +} + +void APTDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void APTDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void APTDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void APTDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void APTDemodGUI::on_rfBW_valueChanged(int value) +{ + float bw = value * 100.0f; + ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void APTDemodGUI::on_fmDev_valueChanged(int value) +{ + ui->fmDevText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_fmDeviation = value * 100.0; + applySettings(); +} + +void APTDemodGUI::on_channels_currentIndexChanged(int index) +{ + m_settings.m_channels = (APTDemodSettings::ChannelSelection)index; + if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) + { + ui->channelALabel->setVisible(true); + ui->channelBLabel->setVisible(true); + } + else if (m_settings.m_channels == APTDemodSettings::CHANNEL_A) + { + ui->channelALabel->setVisible(true); + ui->channelBLabel->setVisible(false); + } + else + { + ui->channelALabel->setVisible(false); + ui->channelBLabel->setVisible(true); + } + applySettings(); +} + +void APTDemodGUI::on_cropNoise_clicked(bool checked) +{ + m_settings.m_cropNoise = checked; + applySettings(); +} + +void APTDemodGUI::on_denoise_clicked(bool checked) +{ + m_settings.m_denoise = checked; + applySettings(); +} + +void APTDemodGUI::on_linear_clicked(bool checked) +{ + m_settings.m_linearEqualise = checked; + applySettings(); +} + +void APTDemodGUI::on_histogram_clicked(bool checked) +{ + m_settings.m_histogramEqualise = checked; + applySettings(); +} + +void APTDemodGUI::on_precipitation_clicked(bool checked) +{ + m_settings.m_precipitationOverlay = checked; + applySettings(); +} + +void APTDemodGUI::on_flip_clicked(bool checked) +{ + m_settings.m_flip = checked; + if (m_settings.m_flip) + ui->image->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); + else + ui->image->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + applySettings(); +} + +void APTDemodGUI::on_startStop_clicked(bool checked) +{ + m_settings.m_decodeEnabled = checked; + applySettings(); +} + +void APTDemodGUI::on_resetDecoder_clicked() +{ + ui->image->setPixmap(QPixmap()); + ui->imageContainer->setWindowTitle("Received image"); + // Send message to reset decoder + m_aptDemod->getInputMessageQueue()->push(APTDemod::MsgResetDecoder::create()); +} + +void APTDemodGUI::on_showSettings_clicked() +{ + APTDemodSettingsDialog dialog(&m_settings); + if (dialog.exec() == QDialog::Accepted) + applySettings(); +} + +// Save image to disk +void APTDemodGUI::on_saveImage_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save image to", "", "*.png;*.jpg;*.jpeg;*.bmp;*.ppm;*.xbm;*.xpm"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + qDebug() << "APT: Saving image to " << fileNames; + if (!m_image.save(fileNames[0])) + QMessageBox::critical(this, "APT Demodulator", QString("Failed to save image to %1").arg(fileNames[0])); + } + } +} + +void APTDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void APTDemodGUI::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_aptDemod->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(); +} + +APTDemodGUI::APTDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::APTDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_doApplySettings(true), + 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_aptDemod = reinterpret_cast(rxChannel); + m_aptDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("APT Demodulator"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + setTitleColor(m_channelMarker.getColor()); + m_settings.setChannelMarker(&m_channelMarker); + + m_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(handleInputMessages())); + + displaySettings(); + applySettings(true); +} + +APTDemodGUI::~APTDemodGUI() +{ + delete ui; +} + +void APTDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void APTDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + APTDemod::MsgConfigureAPTDemod* message = APTDemod::MsgConfigureAPTDemod::create( m_settings, force); + m_aptDemod->getInputMessageQueue()->push(message); + } +} + +void APTDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); + + ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); + ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + + ui->startStop->setChecked(m_settings.m_decodeEnabled); + ui->cropNoise->setChecked(m_settings.m_cropNoise); + ui->denoise->setChecked(m_settings.m_denoise); + ui->linear->setChecked(m_settings.m_linearEqualise); + ui->histogram->setChecked(m_settings.m_histogramEqualise); + ui->precipitation->setChecked(m_settings.m_precipitationOverlay); + ui->flip->setChecked(m_settings.m_flip); + if (m_settings.m_flip) + ui->image->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); + else + ui->image->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + ui->channels->setCurrentIndex((int)m_settings.m_channels); + + displayStreamIndex(); + + blockApplySettings(false); +} + +void APTDemodGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void APTDemodGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void APTDemodGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void APTDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_aptDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbPeak = CalcDb::dbPower(magsqPeak); + + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + } + + m_tickCount++; +} diff --git a/plugins/channelrx/demodapt/aptdemodgui.h b/plugins/channelrx/demodapt/aptdemodgui.h new file mode 100644 index 000000000..7c324af48 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodgui.h @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODGUI_H +#define INCLUDE_APTDEMODGUI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/messagequeue.h" +#include "aptdemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class APTDemod; +class APTDemodGUI; + +namespace Ui { + class APTDemodGUI; +} +class APTDemodGUI; + +class APTDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static APTDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + Ui::APTDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + APTDemodSettings m_settings; + bool m_doApplySettings; + + APTDemod* m_aptDemod; + int m_basebandSampleRate; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QImage m_image; + QPixmap m_pixmap; + + explicit APTDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~APTDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_fmDev_valueChanged(int value); + void on_channels_currentIndexChanged(int index); + void on_cropNoise_clicked(bool checked=false); + void on_denoise_clicked(bool checked=false); + void on_linear_clicked(bool checked=false); + void on_histogram_clicked(bool checked=false); + void on_precipitation_clicked(bool checked=false); + void on_flip_clicked(bool checked=false); + void on_startStop_clicked(bool checked=false); + void on_showSettings_clicked(); + void on_resetDecoder_clicked(); + void on_saveImage_clicked(); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); +}; + +#endif // INCLUDE_APTDEMODGUI_H diff --git a/plugins/channelrx/demodapt/aptdemodgui.ui b/plugins/channelrx/demodapt/aptdemodgui.ui new file mode 100644 index 000000000..2579d8c26 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodgui.ui @@ -0,0 +1,723 @@ + + + APTDemodGUI + + + + 0 + 0 + 451 + 569 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + APT Demodulator + + + APT Demodulator + + + + + 0 + 0 + 431 + 121 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 300 + + + 600 + + + 1 + + + 400 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 40.0k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + + + + Dev + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Frequency deviation + + + 100 + + + 250 + + + 1 + + + 170 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 17.0k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Start/stop decoding + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Show settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + Reset decoder (clears current image) + + + + + + + :/bin.png:/bin.png + + + + + + + Save image to disk + + + + + + + :/save.png:/save.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Channels + + + + + + + + 55 + 0 + + + + Which channels from the image to display + + + + Both + + + + + A + + + + + B + + + + + + + + Crop noise from top and bottom of image + + + ^ + + + + :/apt/icons/cropnoise.png:/apt/icons/cropnoise.png + + + true + + + true + + + + + + + Apply denoise filter to the image + + + ^ + + + + :/apt/icons/denoise.png:/apt/icons/denoise.png + + + true + + + true + + + + + + + Apply linear equalisation to the image + + + ^ + + + + :/linear.png:/linear.png + + + true + + + true + + + + + + + Apply histogram equalisation to the image + + + ^ + + + + :/dsb.png:/dsb.png + + + true + + + true + + + + + + + Overlay precipitation + + + ^ + + + + :/apt/icons/precipitation.png:/apt/icons/precipitation.png + + + true + + + true + + + + + + + Satellite pass direction (flips image) + + + ^ + + + + :/arrow_down.png + :/arrow_up.png:/arrow_down.png + + + true + + + true + + + + + + + + + + + 0 + 150 + 431 + 381 + + + + + 0 + 0 + + + + Received Image + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Channel A + + + Qt::AlignCenter + + + + + + + Channel B + + + Qt::AlignCenter + + + + + + + + + + 0 + 0 + + + + + 300 + 350 + + + + + + + Qt::AlignCenter + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + ScaledImage + QLabel +
gui/scaledimage.h
+
+
+ + deltaFrequency + rfBW + fmDev + startStop + showSettings + resetDecoder + saveImage + channels + cropNoise + denoise + linear + histogram + precipitation + flip + + + + + + +
diff --git a/plugins/channelrx/demodapt/aptdemodplugin.cpp b/plugins/channelrx/demodapt/aptdemodplugin.cpp new file mode 100644 index 000000000..cde064db4 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "aptdemodgui.h" +#endif +#include "aptdemod.h" +#include "aptdemodwebapiadapter.h" +#include "aptdemodplugin.h" + +const PluginDescriptor APTDemodPlugin::m_pluginDescriptor = { + APTDemod::m_channelId, + QStringLiteral("APT Demodulator"), + QStringLiteral("6.5.5"), + QStringLiteral("(c) Jon Beniston, M7RCE and Aptdec authors"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +APTDemodPlugin::APTDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& APTDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void APTDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(APTDemod::m_channelIdURI, APTDemod::m_channelId, this); +} + +void APTDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + APTDemod *instance = new APTDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* APTDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* APTDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return APTDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* APTDemodPlugin::createChannelWebAPIAdapter() const +{ + return new APTDemodWebAPIAdapter(); +} diff --git a/plugins/channelrx/demodapt/aptdemodplugin.h b/plugins/channelrx/demodapt/aptdemodplugin.h new file mode 100644 index 000000000..6fce4ea67 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODPLUGIN_H +#define INCLUDE_APTDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class APTDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.aptdemod") + +public: + explicit APTDemodPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_APTDEMODPLUGIN_H diff --git a/plugins/channelrx/demodapt/aptdemodsettings.cpp b/plugins/channelrx/demodapt/aptdemodsettings.cpp new file mode 100644 index 000000000..a53e681ce --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsettings.cpp @@ -0,0 +1,163 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" +#include "aptdemodsettings.h" + +APTDemodSettings::APTDemodSettings() : + m_channelMarker(0) +{ + resetToDefaults(); +} + +void APTDemodSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 40000.0f; + m_fmDeviation = 17000.0f; + m_cropNoise = false; + m_denoise = true; + m_linearEqualise = false; + m_histogramEqualise = false; + m_precipitationOverlay = false; + m_flip = false; + m_channels = BOTH_CHANNELS; + m_decodeEnabled = true; + m_satelliteTrackerControl = true; + m_satelliteName = "All"; + m_autoSave = false; + m_autoSavePath = ""; + m_autoSaveMinScanLines = 200; + + m_rgbColor = QColor(216, 112, 169).rgb(); + m_title = "APT Demodulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; +} + +QByteArray APTDemodSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_streamIndex); + s.writeReal(3, m_rfBandwidth); + s.writeReal(4, m_fmDeviation); + s.writeBool(5, m_cropNoise); + s.writeBool(6, m_denoise); + s.writeBool(7, m_linearEqualise); + s.writeBool(8, m_histogramEqualise); + s.writeBool(9, m_precipitationOverlay); + s.writeBool(10, m_flip); + s.writeS32(11, (int)m_channels); + s.writeBool(12, m_decodeEnabled); + s.writeBool(13, m_satelliteTrackerControl); + s.writeString(14, m_satelliteName); + s.writeBool(15, m_autoSave); + s.writeString(16, m_autoSavePath); + s.writeS32(17, m_autoSaveMinScanLines); + + if (m_channelMarker) { + s.writeBlob(20, m_channelMarker->serialize()); + } + + s.writeU32(21, m_rgbColor); + s.writeString(22, m_title); + s.writeBool(23, m_useReverseAPI); + s.writeString(24, m_reverseAPIAddress); + s.writeU32(25, m_reverseAPIPort); + s.writeU32(26, m_reverseAPIDeviceIndex); + s.writeU32(27, m_reverseAPIChannelIndex); + + return s.final(); +} + +bool APTDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_streamIndex, 0); + d.readReal(3, &m_rfBandwidth, 40000.0f); + d.readReal(4, &m_fmDeviation, 17000.0f); + d.readBool(5, &m_cropNoise, false); + d.readBool(6, &m_denoise, true); + d.readBool(7, &m_linearEqualise, false); + d.readBool(8, &m_histogramEqualise, false); + d.readBool(9, &m_precipitationOverlay, false); + d.readBool(10, &m_flip, false); + d.readS32(11, (int *)&m_channels, (int)BOTH_CHANNELS); + d.readBool(12, &m_decodeEnabled, true); + d.readBool(13, &m_satelliteTrackerControl, true); + d.readString(14, &m_satelliteName, "All"); + d.readBool(15, &m_autoSave, false); + d.readString(16, &m_autoSavePath, ""); + d.readS32(17, &m_autoSaveMinScanLines, 200); + + d.readBlob(20, &bytetmp); + + if (m_channelMarker) { + m_channelMarker->deserialize(bytetmp); + } + + d.readU32(21, &m_rgbColor, QColor(216, 112, 169).rgb()); + d.readString(22, &m_title, "APT Demodulator"); + d.readBool(23, &m_useReverseAPI, false); + d.readString(24, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(25, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(26, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(27, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + + diff --git a/plugins/channelrx/demodapt/aptdemodsettings.h b/plugins/channelrx/demodapt/aptdemodsettings.h new file mode 100644 index 000000000..bcd805562 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsettings.h @@ -0,0 +1,64 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODSETTINGS_H +#define INCLUDE_APTDEMODSETTINGS_H + +#include +#include + +class Serializable; + +struct APTDemodSettings +{ + qint32 m_inputFrequencyOffset; + float m_rfBandwidth; + float m_fmDeviation; + bool m_cropNoise; + bool m_denoise; + bool m_linearEqualise; + bool m_histogramEqualise; + bool m_precipitationOverlay; + bool m_flip; + enum ChannelSelection {BOTH_CHANNELS, CHANNEL_A, CHANNEL_B} m_channels; + bool m_decodeEnabled; + bool m_satelliteTrackerControl; //! Whether Sat Tracker can set direction of pass + QString m_satelliteName; //!< All, NOAA 15, NOAA 18 or NOAA 19 + bool m_autoSave; + QString m_autoSavePath; + int m_autoSaveMinScanLines; + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + QString m_audioDeviceName; + 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; + + APTDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_APTDEMODSETTINGS_H */ diff --git a/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp b/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp new file mode 100644 index 000000000..67b160826 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsettingsdialog.cpp @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "aptdemodsettingsdialog.h" + +APTDemodSettingsDialog::APTDemodSettingsDialog(APTDemodSettings *settings, QWidget* parent) : + QDialog(parent), + m_settings(settings), + ui(new Ui::APTDemodSettingsDialog) +{ + ui->setupUi(this); + ui->satelliteTrackerControl->setChecked(settings->m_satelliteTrackerControl); + ui->satellite->setCurrentText(settings->m_satelliteName); + ui->autoSave->setChecked(settings->m_autoSave); + ui->autoSavePath->setText(settings->m_autoSavePath); + ui->minScanlines->setValue(settings->m_autoSaveMinScanLines); +} + +APTDemodSettingsDialog::~APTDemodSettingsDialog() +{ + delete ui; +} + +void APTDemodSettingsDialog::accept() +{ + m_settings->m_satelliteTrackerControl = ui->satelliteTrackerControl->isChecked(); + m_settings->m_satelliteName = ui->satellite->currentText(); + m_settings->m_autoSave = ui->autoSave->isChecked(); + m_settings->m_autoSavePath = ui->autoSavePath->text(); + m_settings->m_autoSaveMinScanLines = ui->minScanlines->value(); + QDialog::accept(); +} + +void APTDemodSettingsDialog::on_autoSavePathBrowse_clicked() +{ + QString dir = QFileDialog::getExistingDirectory(this, "Select directory to save images to", "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + ui->autoSavePath->setText(dir); +} diff --git a/plugins/channelrx/demodapt/aptdemodsettingsdialog.h b/plugins/channelrx/demodapt/aptdemodsettingsdialog.h new file mode 100644 index 000000000..f43aaddd1 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsettingsdialog.h @@ -0,0 +1,41 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODSETTINGSDIALOG_H +#define INCLUDE_APTDEMODSETTINGSDIALOG_H + +#include "ui_aptdemodsettingsdialog.h" +#include "aptdemodsettings.h" + +class APTDemodSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit APTDemodSettingsDialog(APTDemodSettings *settings, QWidget* parent = 0); + ~APTDemodSettingsDialog(); + + APTDemodSettings *m_settings; + +private slots: + void accept(); + void on_autoSavePathBrowse_clicked(); + +private: + Ui::APTDemodSettingsDialog* ui; +}; + +#endif // INCLUDE_APTDEMODSETTINGSDIALOG_H diff --git a/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui b/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui new file mode 100644 index 000000000..4f146269f --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsettingsdialog.ui @@ -0,0 +1,205 @@ + + + APTDemodSettingsDialog + + + + 0 + 0 + 385 + 212 + + + + + Liberation Sans + 9 + + + + APT Demodulator Settings + + + + + + + 0 + 0 + + + + + + + Path to save image + + + + + + + + + Path to save images to + + + + + + + + + + + :/load.png:/load.png + + + + + + + + + Minimum scanlines + + + + + + + Enter the minimum number of scanlines in an image (after cropping) for it to be automatically saved + + + 1 + + + 30000 + + + 100 + + + 200 + + + + + + + Satellite + + + + + + + Select which satellite this channel will be used for + + + true + + + + All + + + + + NOAA 15 + + + + + NOAA 18 + + + + + NOAA 19 + + + + + + + + Check to enable control by Satellite Tracker feature + + + Enable Satellite Tracker control + + + + + + + Check to automatically save images when acquisition is stopped or LOS + + + Auto save image + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + satelliteTrackerControl + satellite + autoSave + autoSavePath + autoSavePathBrowse + minScanlines + + + + + + + + buttonBox + accepted() + APTDemodSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + APTDemodSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channelrx/demodapt/aptdemodsink.cpp b/plugins/channelrx/demodapt/aptdemodsink.cpp new file mode 100644 index 000000000..38538dc2f --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsink.cpp @@ -0,0 +1,205 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "dsp/dspengine.h" +#include "dsp/dspengine.h" +#include "util/db.h" +#include "util/stepfunctions.h" +#include "pipes/pipeendpoint.h" +#include "maincore.h" + +#include "aptdemod.h" +#include "aptdemodsink.h" + +APTDemodSink::APTDemodSink(APTDemod *packetDemod) : + m_aptDemod(packetDemod), + m_channelSampleRate(APTDEMOD_AUDIO_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_samples(nullptr) +{ + m_magsq = 0.0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); + + m_samplesLength = APTDEMOD_AUDIO_SAMPLE_RATE * APT_MAX_HEIGHT / 2; // APT broadcasts at 2 lines per second + m_samples = new float[m_samplesLength]; + + resetDecoder(); +} + +void APTDemodSink::resetDecoder() +{ + m_sampleCount = 0; + m_writeIdx = 0; + m_readIdx = 0; + + apt_init(APTDEMOD_AUDIO_SAMPLE_RATE); + + m_row = 0; + m_zenith = 0; +} + +APTDemodSink::~APTDemodSink() +{ + delete m_samples; +} + +// callback from APT library to get audio samples +static int getsamples(void *context, float *samples, int count) +{ + APTDemodSink *sink = (APTDemodSink *)context; + return sink->getSamples(samples, count); +} + +int APTDemodSink::getSamples(float *samples, int count) +{ + for (int i = 0; i < count; i++) + { + if ((m_sampleCount > 0) && (m_readIdx < m_samplesLength)) + { + *samples++ = m_samples[m_readIdx++]; + m_sampleCount--; + } + else + return i; + } + + return count; +} + +void APTDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + + for (SampleVector::const_iterator it = begin; it != end; ++it) + { + Complex c(it->real(), it->imag()); + c *= m_nco.nextIQ(); + + if (m_interpolatorDistance < 1.0f) // interpolate + { + while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + else // decimate + { + if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + } + + // Have we enough samples to decode one line? + // 2 lines per second + if (m_sampleCount >= APTDEMOD_AUDIO_SAMPLE_RATE) + { + float pixels[APT_PROW_WIDTH]; + apt_getpixelrow(pixels, m_row, &m_zenith, m_row == 0, getsamples, this); + getMessageQueueToChannel()->push(APTDemod::MsgPixels::create(pixels, m_zenith)); + m_row++; + } +} + + +void APTDemodSink::processOneSample(Complex &ci) +{ + Complex ca; + + // FM demodulation + double magsqRaw; + Real deviation; + Real fmDemod = m_phaseDiscri.phaseDiscriminatorDelta(ci, magsqRaw, deviation); + + // Add to sample buffer, if there's space and decoding is enabled + if ((m_writeIdx < m_samplesLength) && m_settings.m_decodeEnabled) + { + m_samples[m_writeIdx++] = fmDemod; + m_sampleCount++; + } + + // Calculate average and peak levels for level meter + Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + m_magsqCount++; +} + +void APTDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "APTDemodSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((m_channelFrequencyOffset != channelFrequencyOffset) || + (m_channelSampleRate != channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((m_channelSampleRate != channelSampleRate) || force) + { + m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth, 2.2); + m_interpolatorDistance = (Real) channelSampleRate / (Real) APTDEMOD_AUDIO_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void APTDemodSink::applySettings(const APTDemodSettings& settings, bool force) +{ + qDebug() << "APTDemodSink::applySettings:" + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_fmDeviation: " << settings.m_fmDeviation + << " m_decodeEnabled: " << settings.m_decodeEnabled + << " force: " << force; + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) + { + m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth, 2.2); + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) APTDEMOD_AUDIO_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) + { + m_phaseDiscri.setFMScaling(APTDEMOD_AUDIO_SAMPLE_RATE / (2.0f * settings.m_fmDeviation)); + } + + m_settings = settings; +} diff --git a/plugins/channelrx/demodapt/aptdemodsink.h b/plugins/channelrx/demodapt/aptdemodsink.h new file mode 100644 index 000000000..9a8a14284 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodsink.h @@ -0,0 +1,126 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMODSINK_H +#define INCLUDE_APTDEMODSINK_H + +#include "dsp/channelsamplesink.h" +#include "dsp/phasediscri.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" + +#include "aptdemodsettings.h" +#include + +#include +#include +#include + +// FIXME: Use lower sample rate for better SNR? +// Do we want an audio filter? Subcarrier at 2800Hz. Does libaptdec have one? +#define APTDEMOD_AUDIO_SAMPLE_RATE 48000 +// Lines are 2 per second -> 4160 words per second + +class APTDemod; + +class APTDemodSink : public ChannelSampleSink { +public: + APTDemodSink(APTDemod *packetDemod); + ~APTDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const APTDemodSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + + int getSamples(float *samples, int count); + void resetDecoder(); + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + APTDemod *m_aptDemod; + APTDemodSettings m_settings; + int m_channelSampleRate; + int m_channelFrequencyOffset; + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + + MessageQueue *m_messageQueueToChannel; + + MovingAverageUtil m_movingAverage; + + PhaseDiscriminators m_phaseDiscri; + + // Audio buffer - should probably use a FIFO + float *m_samples; + int m_sampleCount; + int m_samplesLength; + int m_readIdx; + int m_writeIdx; + + int m_row; // Row of image currently being received + int m_zenith; // Row number of Zenith + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } +}; + +#endif // INCLUDE_APTDEMODSINK_H diff --git a/plugins/channelrx/demodapt/aptdemodwebapiadapter.cpp b/plugins/channelrx/demodapt/aptdemodwebapiadapter.cpp new file mode 100644 index 000000000..b6cc87b0a --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "aptdemod.h" +#include "aptdemodwebapiadapter.h" + +APTDemodWebAPIAdapter::APTDemodWebAPIAdapter() +{} + +APTDemodWebAPIAdapter::~APTDemodWebAPIAdapter() +{} + +int APTDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAptDemodSettings(new SWGSDRangel::SWGAPTDemodSettings()); + response.getAptDemodSettings()->init(); + APTDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int APTDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + APTDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodapt/aptdemodwebapiadapter.h b/plugins/channelrx/demodapt/aptdemodwebapiadapter.h new file mode 100644 index 000000000..32027f238 --- /dev/null +++ b/plugins/channelrx/demodapt/aptdemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APTDEMOD_WEBAPIADAPTER_H +#define INCLUDE_APTDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "aptdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class APTDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + APTDemodWebAPIAdapter(); + virtual ~APTDemodWebAPIAdapter(); + + 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: + APTDemodSettings m_settings; +}; + +#endif // INCLUDE_APTDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodapt/icons.qrc b/plugins/channelrx/demodapt/icons.qrc new file mode 100644 index 000000000..0806ccca5 --- /dev/null +++ b/plugins/channelrx/demodapt/icons.qrc @@ -0,0 +1,7 @@ + + + icons/cropnoise.png + icons/denoise.png + icons/precipitation.png + + diff --git a/plugins/channelrx/demodapt/icons/cropnoise.png b/plugins/channelrx/demodapt/icons/cropnoise.png new file mode 100644 index 000000000..e6d4f6944 Binary files /dev/null and b/plugins/channelrx/demodapt/icons/cropnoise.png differ diff --git a/plugins/channelrx/demodapt/icons/denoise.png b/plugins/channelrx/demodapt/icons/denoise.png new file mode 100644 index 000000000..4084374ad Binary files /dev/null and b/plugins/channelrx/demodapt/icons/denoise.png differ diff --git a/plugins/channelrx/demodapt/icons/precipitation.png b/plugins/channelrx/demodapt/icons/precipitation.png new file mode 100644 index 000000000..0f2a8a6a0 Binary files /dev/null and b/plugins/channelrx/demodapt/icons/precipitation.png differ diff --git a/plugins/channelrx/demodapt/readme.md b/plugins/channelrx/demodapt/readme.md new file mode 100644 index 000000000..a81ec7b7a --- /dev/null +++ b/plugins/channelrx/demodapt/readme.md @@ -0,0 +1,108 @@ +

APT Demodulator Plugin

+ +

Introduction

+ +This plugin can be used to demodulate APT (Automatic Picture Transmission) signals transmitted by NOAA weather satellites. These images are at a 4km/pixel resolution in either the visible, near-IR, mid-IR or thermal-IR bands. + +![APT Demodulator plugin GUI](../../../doc/img/APTDemod_plugin.png) + +* NOAA 15 transmits on 137.620 MHz. +* NOAA 18 transmits on 137.912 MHz. +* NOAA 19 transmits on 137.100 MHz. + +

Interface

+ +![APT Demodulator plugin GUI](../../../doc/img/APTDemod_plugin_settings.png) + +

1: Frequency shift from center frequency of reception

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

2: Channel power

+ +Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. + +

3: Level meter in dB

+ + - top bar (green): average value + - bottom bar (blue green): instantaneous peak value + - tip vertical bar (bright green): peak hold value + +

4: RF Bandwidth

+ +This specifies the bandwidth of a LPF that is applied to the input signal to limit the RF bandwidth. APT signals are nominally 34kHz wide, however, this defaults to 40kHz to allow for some Doppler shift. + +

5: Frequency deviation

+ +Adjusts the expected frequency deviation in 0.1 kHz steps from 10 to 25 kHz. The typical value for APT is 17 kHz. + +

6: Start/stop decoding

+ +Starts or stops decoding. A maximum of 3000 scanlines can be decoded, after which, the Reset Decoder (7) button needs to be pressed, to start a new image. + +

7: Show settings dialog

+ +When clicked, shows additional APT Demodulator settings. + +![APT Demodulator settings dialog](../../../doc/img/APTDemod_plugin_settingsdialog.png) + +This includes: + + - Whether the APT demodulator can be controlled by the Satellite Tracker feature. When checked, the image decoder will be enabled and reset on AOS and the satellite pass direction will be used to control image rotation. The decoder will be stopped on LOS. + - Which satellites the APT demodulator will respond to AOS and LOS indications from the Satellite Tracker. This can be used to simulataneously decode images from multiple satellites, by having multiple instances of the APT Demodulator and setting a unique satellite name for each demodulator. + - Whether to automatically save the image on LOS. + - Path to save automatically saved images in. + - The minimum number of scanlines required to be in an image, after noise cropping, for it to be automatically saved. + +

8: Reset decoder

+ +Clears the current image and restarts the decoder. The decoder must be reset between passes of different satellites. + +

9: Save image to disk

+ +Saves the current image to disk. Images can be saved in PNG, JPEG, BMP, PPM, XBM or XPM formats. + +

10: Channel selection

+ +Selects whether: + + - both channels are displayed + - only channel A is displayed + - only channel B is displayed + +

11: Crop noise

+ +When checked, noise is cropped from the top and bottom of the image. This is noise that is typically the result of the satellite being at a low elevation. + +

12: Apply denoise filter

+ +When checked, a denoise filter is applied to the received image. + +

13: Apply linear equalisation

+ +When checked, linear equalisation is performed, which can enhance the contrast. The equalisation is performed separately on each channel. + +

14: Apply histogram equalisation

+ +When checked, histogram equalisation is performed, which can enhance the contrast. The equalisation is performed separately on each channel. + +

15: Overlay precipitation

+ +When checked, precipitation is detected from the IR channel and overlayed on both channels using a colour palette. + +This option will not work if linear or histogram equalisation has been applied. + +

16: Pass direction

+ +The pass direction check button should be set to match the direction of the satellite pass. +i.e. select down arrow for satellite passing from the North to the South and the up arrow for the satellite passing from the South to the North. +This will ensure the image has the Northern latitudes at the top of the image. +This can be set automatically by the Satellite Tracker feature. + +

Attribution

+ +This plugin uses libapt, part of Aptdec by Thierry Leconte and Xerbo, to perform image decoding and processing: https://github.com/Xerbo/aptdec + +Icons are by Freepik from Flaticon https://www.flaticon.com/ + +Icons are by Hare Krishna from the Noun Project Noun Project: https://thenounproject.com/