diff --git a/CMakeLists.txt b/CMakeLists.txt index cd74fddff..89491cf0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,6 +146,7 @@ option(ENABLE_FEATURE_GS232CONTROLLER "Enable feature gs232controller plugin" ON option(ENABLE_FEATURE_REMOTECONTROL "Enable feature remote control plugin" ON) option(ENABLE_FEATURE_SKYMAP "Enable feature sky map plugin" ON) option(ENABLE_FEATURE_SID "Enable feature sid plugin" ON) +option(ENABLE_FEATURE_MORSEDECODER "Enable feature morsedecoder plugin" ON) # on windows always build external libraries if(WIN32) @@ -779,6 +780,7 @@ if (NOT ENABLE_EXTERNAL_LIBRARIES OR (ENABLE_EXTERNAL_LIBRARIES STREQUAL "AUTO") find_package(LibDAB) find_package(HIDAPI) find_package(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE) + find_package(GGMorse) # Devices if(ENABLE_AIRSPY) diff --git a/CMakePresets.json b/CMakePresets.json index 892f549ba..01dc1c0ad 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -28,6 +28,7 @@ "SOAPYSDR_DIR": "/opt/install/SoapySDR", "UHD_DIR": "/opt/install/uhd", "XTRX_DIR": "/opt/install/xtrx-images", + "GGMORSE_DIR": "/opt/install/ggmorse", "CMAKE_INSTALL_PREFIX": "/opt/install/sdrangel" }, "warnings": { diff --git a/cmake/Modules/FindGGMorse.cmake b/cmake/Modules/FindGGMorse.cmake new file mode 100644 index 000000000..08c158264 --- /dev/null +++ b/cmake/Modules/FindGGMorse.cmake @@ -0,0 +1,38 @@ +if (NOT GGMORSE_FOUND) + INCLUDE(FindPkgConfig) + PKG_CHECK_MODULES(PC_GGMorse "libggmorse") + + FIND_PATH(GGMORSE_INCLUDE_DIR + NAMES ggmorse/ggmorse.h + HINTS ${GGMORSE_DIR}/include + ${PC_GGMORSE_INCLUDE_DIR} + ${CMAKE_INSTALL_PREFIX}/include + PATHS /usr/local/include + /usr/include + ) + + FIND_LIBRARY(GGMORSE_LIBRARIES + NAMES ggmorse libggmorse + HINTS ${GGMORSE_DIR}/lib + ${GGMORSE_DIR}/lib64 + ${PC_GGMORSE_LIBDIR} + ${CMAKE_INSTALL_PREFIX}/lib + ${CMAKE_INSTALL_PREFIX}/lib64 + PATHS /usr/local/lib + /usr/local/lib64 + /usr/lib + /usr/lib64 + ) + + if (GGMORSE_INCLUDE_DIR AND GGMORSE_LIBRARIES) + set(GGMORSE_FOUND TRUE CACHE INTERNAL "GGMorse found") + message(STATUS "Found GGMorse: ${GGMORSE_INCLUDE_DIR}, ${GGMORSE_LIBRARIES}") + else (GGMORSE_INCLUDE_DIR AND GGMORSE_LIBRARIES) + set(GGMORSE_FOUND FALSE CACHE INTERNAL "GGMorse found") + message(STATUS "GGMorse not found") + endif (GGMORSE_INCLUDE_DIR AND GGMORSE_LIBRARIES) + + INCLUDE(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(GGMORSE DEFAULT_MSG GGMORSE_LIBRARIES GGMORSE_INCLUDE_DIR) + MARK_AS_ADVANCED(GGMORSE_LIBRARIES GGMORSE_INCLUDE_DIR) +endif (NOT GGMORSE_FOUND) diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index ed2cf43c4..1baf410cb 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -124,5 +124,11 @@ endif() if (ENABLE_FEATURE_SID) add_subdirectory(sid) else() - message(STATUS "Not building SID (ENABLED_FEATURE_SID=${ENABLED_FEATURE_SID})") + message(STATUS "Not building SID (ENABLE_FEATURE_SID=${ENABLE_FEATURE_SID})") +endif() + +if (ENABLE_FEATURE_MORSEDECODER AND GGMORSE_FOUND) + add_subdirectory(morsedecoder) +else() + message(STATUS "Not building morsedecoder (ENABLE_FEATURE_MORSEDECODER=${ENABLE_FEATURE_MORSEDECODER} GGMODSE_FOUND=${GGMORSE_FOUND})") endif() diff --git a/plugins/feature/morsedecoder/CMakeLists.txt b/plugins/feature/morsedecoder/CMakeLists.txt new file mode 100644 index 000000000..8eefe5a54 --- /dev/null +++ b/plugins/feature/morsedecoder/CMakeLists.txt @@ -0,0 +1,61 @@ +project(morsedecoder) + +set(morsedecoder_SOURCES + morsedecoder.cpp + morsedecodersettings.cpp + morsedecoderplugin.cpp + morsedecoderworker.cpp + morsedecoderwebapiadapter.cpp +) + +set(morsedecoder_HEADERS + morsedecoder.h + morsedecodersettings.h + morsedecoderplugin.h + morsedecoderworker.h + morsedecoderwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(morsedecoder_SOURCES + ${morsedecoder_SOURCES} + morsedecodergui.cpp + morsedecodergui.ui + ) + set(morsedecoder_HEADERS + ${morsedecoder_HEADERS} + morsedecodergui.h + ) + + set(TARGET_NAME featuremorsedecoder) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featuremorsedecodersrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${morsedecoder_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/feature/morsedecoder/morsedecoder.cpp b/plugins/feature/morsedecoder/morsedecoder.cpp new file mode 100644 index 000000000..913904bdf --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoder.cpp @@ -0,0 +1,611 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureActions.h" +#include "SWGDeviceState.h" + +#include "dsp/dspcommands.h" +#include "dsp/datafifo.h" +#include "channel/channelapi.h" +#include "maincore.h" +#include "settings/serializable.h" + +#include "morsedecoderworker.h" +#include "morsedecoder.h" + +MESSAGE_CLASS_DEFINITION(MorseDecoder::MsgConfigureMorseDecoder, Message) +MESSAGE_CLASS_DEFINITION(MorseDecoder::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(MorseDecoder::MsgReportChannels, Message) +MESSAGE_CLASS_DEFINITION(MorseDecoder::MsgSelectChannel, Message) +MESSAGE_CLASS_DEFINITION(MorseDecoder::MsgReportSampleRate, Message) + +const char* const MorseDecoder::m_featureIdURI = "sdrangel.feature.morsedecoder"; +const char* const MorseDecoder::m_featureId = "MorseDecoder"; + +MorseDecoder::MorseDecoder(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_thread(nullptr), + m_running(false), + m_worker(nullptr), + m_availableChannelOrFeatureHandler(MorseDecoderSettings::m_channelURIs), + m_selectedChannel(nullptr), + m_dataPipe(nullptr) +{ + qDebug("DemodAnalyzer::DemodAnalyzer: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_state = StIdle; + m_errorMessage = "DemodAnalyzer error"; + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &MorseDecoder::networkManagerFinished + ); + QObject::connect( + &m_availableChannelOrFeatureHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &MorseDecoder::channelsOrFeaturesChanged + ); + m_availableChannelOrFeatureHandler.scanAvailableChannelsAndFeatures(); +} + +MorseDecoder::~MorseDecoder() +{ + QObject::disconnect( + &m_availableChannelOrFeatureHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &MorseDecoder::channelsOrFeaturesChanged + ); + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &MorseDecoder::networkManagerFinished + ); + delete m_networkManager; + stop(); +} + +void MorseDecoder::start() +{ + QMutexLocker m_lock(&m_mutex); + + if (m_running) { + return; + } + + qDebug("MorseDecoder::start"); + m_thread = new QThread(); + m_worker = new MorseDecoderWorker(); + m_worker->moveToThread(m_thread); + + QObject::connect( + m_thread, + &QThread::started, + m_worker, + &MorseDecoderWorker::startWork + ); + QObject::connect( + m_thread, + &QThread::finished, + m_worker, + &QObject::deleteLater + ); + QObject::connect( + m_thread, + &QThread::finished, + m_thread, + &QThread::deleteLater + ); + + m_worker->setMessageQueueToFeature(getInputMessageQueue()); + m_worker->startWork(); + m_state = StRunning; + m_thread->start(); + + MorseDecoderWorker::MsgConfigureMorseDecoderWorker *msg + = MorseDecoderWorker::MsgConfigureMorseDecoderWorker::create(m_settings, QList(), true); + m_worker->getInputMessageQueue()->push(msg); + + if (m_dataPipe) + { + DataFifo *fifo = qobject_cast(m_dataPipe->m_element); + + if (fifo) + { + MorseDecoderWorker::MsgConnectFifo *msg = MorseDecoderWorker::MsgConnectFifo::create(fifo, true); + m_worker->getInputMessageQueue()->push(msg); + } + } + + m_running = true; +} + +void MorseDecoder::stop() +{ + QMutexLocker m_lock(&m_mutex); + + if (!m_running) { + return; + } + + qDebug("MorseDecoder::stop"); + m_running = false; + + if (m_dataPipe) + { + DataFifo *fifo = qobject_cast(m_dataPipe->m_element); + + if (fifo) + { + MorseDecoderWorker::MsgConnectFifo *msg = MorseDecoderWorker::MsgConnectFifo::create(fifo, false); + m_worker->getInputMessageQueue()->push(msg); + } + } + + m_worker->stopWork(); + m_state = StIdle; + m_thread->quit(); + m_thread->wait(); +} + +bool MorseDecoder::handleMessage(const Message& cmd) +{ + if (MsgConfigureMorseDecoder::match(cmd)) + { + MsgConfigureMorseDecoder& cfg = (MsgConfigureMorseDecoder&) cmd; + qDebug() << "MorseDecoder::handleMessage: MsgConfigureDemodAnalyzer"; + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce()); + + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + qDebug() << "MorseDecoder::handleMessage: MsgStartStop: start:" << cfg.getStartStop(); + + if (cfg.getStartStop()) { + start(); + } else { + stop(); + } + + return true; + } + else if (MsgSelectChannel::match(cmd)) + { + MsgSelectChannel& cfg = (MsgSelectChannel&) cmd; + ChannelAPI *selectedChannel = cfg.getChannel(); + qDebug("MorseDecoder::handleMessage: MsgSelectChannel: %p %s", + selectedChannel, qPrintable(selectedChannel->objectName())); + setChannel(selectedChannel); + MainCore::MsgChannelDemodQuery *msg = MainCore::MsgChannelDemodQuery::create(); + selectedChannel->getInputMessageQueue()->push(msg); + + return true; + } + else if (MainCore::MsgChannelDemodReport::match(cmd)) + { + qDebug() << "MorseDecoder::handleMessage: MainCore::MsgChannelDemodReport"; + MainCore::MsgChannelDemodReport& report = (MainCore::MsgChannelDemodReport&) cmd; + + if (report.getChannelAPI() == m_selectedChannel) + { + m_sampleRate = report.getSampleRate(); + + if (m_running) { + m_worker->applySampleRate(m_sampleRate); + } + + if (m_dataPipe) + { + DataFifo *fifo = qobject_cast(m_dataPipe->m_element); + + if (fifo) { + fifo->setSize(2*m_sampleRate); + } + } + + if (getMessageQueueToGUI()) + { + MsgReportSampleRate *msg = MsgReportSampleRate::create(m_sampleRate); + getMessageQueueToGUI()->push(msg); + } + } + + return true; + } + else + { + return false; + } +} + +QByteArray MorseDecoder::serialize() const +{ + return m_settings.serialize(); +} + +bool MorseDecoder::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureMorseDecoder *msg = MsgConfigureMorseDecoder::create(m_settings, QList(), true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureMorseDecoder *msg = MsgConfigureMorseDecoder::create(m_settings, QList(), true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void MorseDecoder::applySettings(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force) +{ + qDebug() << "MorseDecoder::applySettings:" << settings.getDebugString(settingsKeys, force) << " force: " << force; + + if (m_running) + { + MorseDecoderWorker::MsgConfigureMorseDecoderWorker *msg = MorseDecoderWorker::MsgConfigureMorseDecoderWorker::create( + settings, settingsKeys, force + ); + m_worker->getInputMessageQueue()->push(msg); + } + + + if (settings.m_useReverseAPI) + { + bool fullUpdate = (settingsKeys.contains("useReverseAPI") && settings.m_useReverseAPI) || + settingsKeys.contains("reverseAPIAddress") || + settingsKeys.contains("reverseAPIPort") || + settingsKeys.contains("reverseAPIFeatureSetIndex") || + settingsKeys.contains("m_reverseAPIFeatureIndex"); + webapiReverseSendSettings(settingsKeys, settings, fullUpdate || force); + } + + if (force) { + m_settings = settings; + } else { + m_settings.applySettings(settingsKeys, settings); + } +} + +void MorseDecoder::channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo) +{ + m_availableChannels = m_availableChannelOrFeatureHandler.getAvailableChannelOrFeatureList(); + notifyUpdate(renameFrom, renameTo); +} + +void MorseDecoder::notifyUpdate(const QStringList& renameFrom, const QStringList& renameTo) +{ + if (getMessageQueueToGUI()) + { + MsgReportChannels *msg = MsgReportChannels::create(renameFrom, renameTo); + msg->getAvailableChannels() = m_availableChannels; + getMessageQueueToGUI()->push(msg); + } +} + +void MorseDecoder::setChannel(ChannelAPI *selectedChannel) +{ + if ((selectedChannel == m_selectedChannel) || (m_availableChannels.indexOfObject(selectedChannel) == -1)) { + return; + } + + MainCore *mainCore = MainCore::instance(); + + if (m_selectedChannel) + { + ObjectPipe *pipe = mainCore->getDataPipes().unregisterProducerToConsumer(m_selectedChannel, this, "demod"); + DataFifo *fifo = qobject_cast(pipe->m_element); + + if ((fifo) && m_running) + { + MorseDecoderWorker::MsgConnectFifo *msg = MorseDecoderWorker::MsgConnectFifo::create(fifo, false); + m_worker->getInputMessageQueue()->push(msg); + } + + ObjectPipe *messagePipe = mainCore->getMessagePipes().unregisterProducerToConsumer(m_selectedChannel, this, "reportdemod"); + + if (messagePipe) + { + MessageQueue *messageQueue = qobject_cast(messagePipe->m_element); + + if (messageQueue) { + disconnect(messageQueue, &MessageQueue::messageEnqueued, this, nullptr); // Have to use nullptr, as slot is a lambda. + } + } + } + + m_dataPipe = mainCore->getDataPipes().registerProducerToConsumer(selectedChannel, this, "demod"); + connect(m_dataPipe, SIGNAL(toBeDeleted(int, QObject*)), this, SLOT(handleDataPipeToBeDeleted(int, QObject*))); + DataFifo *fifo = qobject_cast(m_dataPipe->m_element); + + if (fifo) + { + fifo->setSize(96000); + + if (m_running) + { + MorseDecoderWorker::MsgConnectFifo *msg = MorseDecoderWorker::MsgConnectFifo::create(fifo, true); + m_worker->getInputMessageQueue()->push(msg); + } + } + + ObjectPipe *messagePipe = mainCore->getMessagePipes().registerProducerToConsumer(selectedChannel, this, "reportdemod"); + + if (messagePipe) + { + MessageQueue *messageQueue = qobject_cast(messagePipe->m_element); + + if (messageQueue) + { + QObject::connect( + messageQueue, + &MessageQueue::messageEnqueued, + this, + [=](){ this->handleChannelMessageQueue(messageQueue); }, + Qt::QueuedConnection + ); + } + } + + m_selectedChannel = selectedChannel; +} + +int MorseDecoder::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + (void) errorMessage; + getFeatureStateStr(*response.getState()); + MsgStartStop *msg = MsgStartStop::create(run); + getInputMessageQueue()->push(msg); + return 202; +} + +int MorseDecoder::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setDemodAnalyzerSettings(new SWGSDRangel::SWGDemodAnalyzerSettings()); + response.getDemodAnalyzerSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int MorseDecoder::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + MorseDecoderSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureMorseDecoder *msg = MsgConfigureMorseDecoder::create(settings, featureSettingsKeys, force); + m_inputMessageQueue.push(msg); + + qDebug("DemodAnalyzer::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureMorseDecoder *msgToGUI = MsgConfigureMorseDecoder::create(settings, featureSettingsKeys, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void MorseDecoder::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const MorseDecoderSettings& settings) +{ + if (response.getDemodAnalyzerSettings()->getTitle()) { + *response.getDemodAnalyzerSettings()->getTitle() = settings.m_title; + } else { + response.getDemodAnalyzerSettings()->setTitle(new QString(settings.m_title)); + } + + response.getDemodAnalyzerSettings()->setRgbColor(settings.m_rgbColor); + response.getDemodAnalyzerSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getDemodAnalyzerSettings()->getReverseApiAddress()) { + *response.getDemodAnalyzerSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getDemodAnalyzerSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getDemodAnalyzerSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getDemodAnalyzerSettings()->setReverseApiFeatureSetIndex(settings.m_reverseAPIFeatureSetIndex); + response.getDemodAnalyzerSettings()->setReverseApiFeatureIndex(settings.m_reverseAPIFeatureIndex); + + if (settings.m_rollupState) + { + if (response.getDemodAnalyzerSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getDemodAnalyzerSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getDemodAnalyzerSettings()->setRollupState(swgRollupState); + } + } +} + +void MorseDecoder::webapiUpdateFeatureSettings( + MorseDecoderSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getDemodAnalyzerSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getDemodAnalyzerSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getDemodAnalyzerSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getDemodAnalyzerSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getDemodAnalyzerSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureSetIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getDemodAnalyzerSettings()->getReverseApiFeatureSetIndex(); + } + if (featureSettingsKeys.contains("reverseAPIFeatureIndex")) { + settings.m_reverseAPIFeatureIndex = response.getDemodAnalyzerSettings()->getReverseApiFeatureIndex(); + } + if (settings.m_rollupState && featureSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(featureSettingsKeys, response.getDemodAnalyzerSettings()->getRollupState()); + } +} + +void MorseDecoder::webapiReverseSendSettings(const QList& featureSettingsKeys, const MorseDecoderSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("DemodAnalyzer")); + swgFeatureSettings->setDemodAnalyzerSettings(new SWGSDRangel::SWGDemodAnalyzerSettings()); + SWGSDRangel::SWGDemodAnalyzerSettings *swgDemodAnalyzerSettings = swgFeatureSettings->getDemodAnalyzerSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("title") || force) { + swgDemodAnalyzerSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgDemodAnalyzerSettings->setRgbColor(settings.m_rgbColor); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIFeatureSetIndex) + .arg(settings.m_reverseAPIFeatureIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgFeatureSettings->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 swgFeatureSettings; +} + +void MorseDecoder::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "MorseDecoder::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("MorseDecoder::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void MorseDecoder::handleChannelMessageQueue(MessageQueue* messageQueue) +{ + Message* message; + + while ((message = messageQueue->pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void MorseDecoder::handleDataPipeToBeDeleted(int reason, QObject *object) +{ + qDebug("MorseDecoder::handleDataPipeToBeDeleted: %d %p", reason, object); + + if ((reason == 0) && (m_selectedChannel == object)) + { + DataFifo *fifo = qobject_cast(m_dataPipe->m_element); + + if ((fifo) && m_running) + { + MorseDecoderWorker::MsgConnectFifo *msg = MorseDecoderWorker::MsgConnectFifo::create(fifo, false); + m_worker->getInputMessageQueue()->push(msg); + } + + m_selectedChannel = nullptr; + } +} + +int MorseDecoder::webapiActionsPost( + const QStringList&, + SWGSDRangel::SWGFeatureActions& query, + QString& errorMessage) { + + MainCore* m_core = MainCore::instance(); + auto action = query.getDemodAnalyzerActions(); + if (action == nullptr) { + errorMessage = QString("missing MorseDecoderActions in request"); + return 404; + } + + auto deviceId = action->getDeviceId(); + auto chanId = action->getChannelId(); + + ChannelAPI * chan = m_core->getChannel(deviceId, chanId); + if (chan == nullptr) { + errorMessage = QString("device(%1) or channel (%2) on the device does not exist").arg(deviceId).arg(chanId); + return 404; + } + + MsgSelectChannel *msg = MsgSelectChannel::create(chan); + getInputMessageQueue()->push(msg); + return 200; +} diff --git a/plugins/feature/morsedecoder/morsedecoder.h b/plugins/feature/morsedecoder/morsedecoder.h new file mode 100644 index 000000000..e302c8293 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoder.h @@ -0,0 +1,223 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MORSEDECODER_H_ +#define INCLUDE_FEATURE_MORSEDECODER_H_ + +#include +#include +#include + +#include "feature/feature.h" +#include "util/message.h" +#include "availablechannelorfeaturehandler.h" + +#include "morsedecodersettings.h" + +class WebAPIAdapterInterface; +class MorseDecoderWorker; +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class ObjectPipe; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class MorseDecoder : public Feature +{ + Q_OBJECT +public: + class MsgConfigureMorseDecoder : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MorseDecoderSettings& getSettings() const { return m_settings; } + const QList& getSettingsKeys() const { return m_settingsKeys; } + bool getForce() const { return m_force; } + + static MsgConfigureMorseDecoder* create(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force) { + return new MsgConfigureMorseDecoder(settings, settingsKeys, force); + } + + private: + MorseDecoderSettings m_settings; + QList m_settingsKeys; + bool m_force; + + MsgConfigureMorseDecoder(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force) : + Message(), + m_settings(settings), + m_settingsKeys(settingsKeys), + m_force(force) + { } + }; + + class MsgStartStop : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getStartStop() const { return m_startStop; } + + static MsgStartStop* create(bool startStop) { + return new MsgStartStop(startStop); + } + + protected: + bool m_startStop; + + MsgStartStop(bool startStop) : + Message(), + m_startStop(startStop) + { } + }; + + class MsgReportChannels : public Message { + MESSAGE_CLASS_DECLARATION + + public: + AvailableChannelOrFeatureList& getAvailableChannels() { return m_availableChannels; } + const QStringList& getRenameFrom() const { return m_renameFrom; } + const QStringList& getRenameTo() const { return m_renameTo; } + + static MsgReportChannels* create(const QStringList& renameFrom, const QStringList& renameTo) { + return new MsgReportChannels(renameFrom, renameTo); + } + + private: + AvailableChannelOrFeatureList m_availableChannels; + QStringList m_renameFrom; + QStringList m_renameTo; + + MsgReportChannels(const QStringList& renameFrom, const QStringList& renameTo) : + Message(), + m_renameFrom(renameFrom), + m_renameTo(renameTo) + {} + }; + + class MsgSelectChannel : public Message { + MESSAGE_CLASS_DECLARATION + + public: + ChannelAPI *getChannel() { return m_channel; } + static MsgSelectChannel* create(ChannelAPI *channel) { + return new MsgSelectChannel(channel); + } + + protected: + ChannelAPI *m_channel; + + MsgSelectChannel(ChannelAPI *channel) : + Message(), + m_channel(channel) + { } + }; + + class MsgReportSampleRate : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSampleRate() const { return m_sampleRate; } + + static MsgReportSampleRate* create(int sampleRate) { + return new MsgReportSampleRate(sampleRate); + } + + private: + int m_sampleRate; + + MsgReportSampleRate(int sampleRate) : + Message(), + m_sampleRate(sampleRate) + {} + }; + + MorseDecoder(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~MorseDecoder(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiActionsPost( + const QStringList& featureActionsKeys, + SWGSDRangel::SWGFeatureActions& query, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const MorseDecoderSettings& settings); + + static void webapiUpdateFeatureSettings( + MorseDecoderSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + QThread *m_thread; + QRecursiveMutex m_mutex; + bool m_running; + MorseDecoderWorker *m_worker; + MorseDecoderSettings m_settings; + AvailableChannelOrFeatureList m_availableChannels; + AvailableChannelOrFeatureHandler m_availableChannelOrFeatureHandler; + ChannelAPI *m_selectedChannel; + ObjectPipe *m_dataPipe; + int m_sampleRate; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force = false); + void notifyUpdate(const QStringList& renameFrom, const QStringList& renameTo); + void setChannel(ChannelAPI *selectedChannel); + void webapiReverseSendSettings(const QList& featureSettingsKeys, const MorseDecoderSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleChannelMessageQueue(MessageQueue *messageQueues); + void channelsOrFeaturesChanged(const QStringList& renameFrom, const QStringList& renameTo); + void handleDataPipeToBeDeleted(int reason, QObject *object); +}; + +#endif // INCLUDE_FEATURE_MORSEDECODER_H_ diff --git a/plugins/feature/morsedecoder/morsedecodergui.cpp b/plugins/feature/morsedecoder/morsedecodergui.cpp new file mode 100644 index 000000000..5c74b22d9 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecodergui.cpp @@ -0,0 +1,356 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "feature/featureuiset.h" +#include "dsp/spectrumvis.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "gui/glspectrum.h" +#include "gui/glscope.h" +#include "gui/dialpopup.h" +#include "gui/dialogpositioner.h" +#include "util/db.h" +#include "maincore.h" + +#include "ui_morsedecodergui.h" +#include "morsedecoder.h" +#include "morsedecodergui.h" + +MorseDecoderGUI* MorseDecoderGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + MorseDecoderGUI* gui = new MorseDecoderGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void MorseDecoderGUI::destroy() +{ + delete this; +} + +void MorseDecoderGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray MorseDecoderGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool MorseDecoderGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex); + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool MorseDecoderGUI::handleMessage(const Message& message) +{ + if (MorseDecoder::MsgConfigureMorseDecoder::match(message)) + { + qDebug("MorseDecoderGUI::handleMessage: MorseDecoder::MsgConfigureMorseDecoder"); + const MorseDecoder::MsgConfigureMorseDecoder& cfg = (MorseDecoder::MsgConfigureMorseDecoder&) message; + + if (cfg.getForce()) { + m_settings = cfg.getSettings(); + } else { + m_settings.applySettings(cfg.getSettingsKeys(), cfg.getSettings()); + } + + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (MorseDecoder::MsgReportChannels::match(message)) + { + qDebug("MorseDecoderGUI::handleMessage: MorseDecoder::MsgReportChannels"); + MorseDecoder::MsgReportChannels& report = (MorseDecoder::MsgReportChannels&) message; + m_availableChannels = report.getAvailableChannels(); + updateChannelList(); + + return true; + } + else if (MorseDecoder::MsgReportSampleRate::match(message)) + { + MorseDecoder::MsgReportSampleRate& report = (MorseDecoder::MsgReportSampleRate&) message; + int sampleRate = report.getSampleRate(); + qDebug("MorseDecoderGUI::handleMessage: MorseDecoder::MsgReportSampleRate: %d", sampleRate); + displaySampleRate(sampleRate); + m_sampleRate = sampleRate; + + return true; + } + + return false; +} + +void MorseDecoderGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void MorseDecoderGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + RollupContents *rollupContents = getRollupContents(); + + rollupContents->saveState(m_rollupState); + applySettings(); +} + +MorseDecoderGUI::MorseDecoderGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::MorseDecoderGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_sampleRate(48000), + m_doApplySettings(true), + m_lastFeatureState(0), + m_selectedChannel(nullptr) +{ + m_feature = feature; + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/feature/morsedecoder/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + + m_morseDecoder = reinterpret_cast(feature); + m_morseDecoder->setMessageQueueToGUI(&m_inputMessageQueue); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); + m_statusTimer.start(1000); + + displaySampleRate(m_sampleRate); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + m_settings.setRollupState(&m_rollupState); + + displaySettings(); + applySettings(true); + makeUIConnections(); + DialPopup::addPopupsToChildDials(this); + m_resizer.enableChildMouseTracking(); +} + +MorseDecoderGUI::~MorseDecoderGUI() +{ + delete ui; +} + +void MorseDecoderGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void MorseDecoderGUI::setWorkspaceIndex(int index) +{ + m_settings.m_workspaceIndex = index; + m_feature->setWorkspaceIndex(index); +} + +void MorseDecoderGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + setTitle(m_settings.m_title); + blockApplySettings(true); + getRollupContents()->restoreState(m_rollupState); + blockApplySettings(false); +} + +void MorseDecoderGUI::displaySampleRate(int sampleRate) +{ + QString s = QString::number(sampleRate/1000.0, 'f', 1); + ui->sinkSampleRateText->setText(tr("%1 kS/s").arg(s)); +} + +void MorseDecoderGUI::updateChannelList() +{ + ui->channels->blockSignals(true); + ui->channels->clear(); + + AvailableChannelOrFeatureList::const_iterator it = m_availableChannels.begin(); + int selectedItem = -1; + + for (int i = 0; it != m_availableChannels.end(); ++it, i++) + { + ui->channels->addItem(it->getLongId()); + + if (it->m_object == m_selectedChannel) { + selectedItem = i; + } + } + + ui->channels->blockSignals(false); + + if (m_availableChannels.size() > 0) + { + if (selectedItem >= 0) { + ui->channels->setCurrentIndex(selectedItem); + } else { + ui->channels->setCurrentIndex(0); + } + } +} + +void MorseDecoderGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); + dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); + dialog.setDefaultTitle(m_displayedName); + + dialog.move(p); + new DialogPositioner(&dialog, false); + dialog.exec(); + + m_settings.m_title = dialog.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); + m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); + + setTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + m_settingsKeys.append("title"); + m_settingsKeys.append("rgbColor"); + m_settingsKeys.append("useReverseAPI"); + m_settingsKeys.append("reverseAPIAddress"); + m_settingsKeys.append("reverseAPIPort"); + m_settingsKeys.append("reverseAPIFeatureSetIndex"); + m_settingsKeys.append("reverseAPIFeatureIndex"); + + applySettings(); + } + + resetContextMenuType(); +} + +void MorseDecoderGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + MorseDecoder::MsgStartStop *message = MorseDecoder::MsgStartStop::create(checked); + m_morseDecoder->getInputMessageQueue()->push(message); + } +} + +void MorseDecoderGUI::on_channels_currentIndexChanged(int index) +{ + if ((index >= 0) && (index < m_availableChannels.size())) + { + m_selectedChannel = qobject_cast(m_availableChannels[index].m_object); + MorseDecoder::MsgSelectChannel *msg = MorseDecoder::MsgSelectChannel::create(m_selectedChannel); + m_morseDecoder->getInputMessageQueue()->push(msg); + } +} + +void MorseDecoderGUI::on_channelApply_clicked() +{ + if (ui->channels->count() > 0) { + on_channels_currentIndexChanged(ui->channels->currentIndex()); + } +} + +void MorseDecoderGUI::tick() +{ +} + +void MorseDecoderGUI::updateStatus() +{ + int state = m_morseDecoder->getState(); + + if (m_lastFeatureState != state) + { + switch (state) + { + case Feature::StNotStarted: + ui->startStop->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + break; + case Feature::StIdle: + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }"); + break; + case Feature::StRunning: + ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + break; + case Feature::StError: + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + QMessageBox::information(this, tr("Message"), m_morseDecoder->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } +} + +void MorseDecoderGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + MorseDecoder::MsgConfigureMorseDecoder* message = MorseDecoder::MsgConfigureMorseDecoder::create( m_settings, m_settingsKeys, force); + m_morseDecoder->getInputMessageQueue()->push(message); + } + + m_settingsKeys.clear(); +} + +void MorseDecoderGUI::makeUIConnections() +{ + QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &MorseDecoderGUI::on_startStop_toggled); + QObject::connect(ui->channels, qOverload(&QComboBox::currentIndexChanged), this, &MorseDecoderGUI::on_channels_currentIndexChanged); + QObject::connect(ui->channelApply, &QPushButton::clicked, this, &MorseDecoderGUI::on_channelApply_clicked); +} diff --git a/plugins/feature/morsedecoder/morsedecodergui.h b/plugins/feature/morsedecoder/morsedecodergui.h new file mode 100644 index 000000000..4de50ec28 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecodergui.h @@ -0,0 +1,95 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MORSEDECODERGUI_H_ +#define INCLUDE_FEATURE_MORSEDECODERGUI_H_ + +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "availablechannelorfeaturehandler.h" +#include "settings/rollupstate.h" + +#include "morsedecodersettings.h" + +class PluginAPI; +class FeatureUISet; +class MorseDecoder; +class Feature; + +namespace Ui { + class MorseDecoderGUI; +} + +class MorseDecoderGUI : public FeatureGUI { + Q_OBJECT +public: + static MorseDecoderGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual void setWorkspaceIndex(int index); + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; } + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; } + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; } + +private: + Ui::MorseDecoderGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + MorseDecoderSettings m_settings; + QList m_settingsKeys; + RollupState m_rollupState; + int m_sampleRate; + bool m_doApplySettings; + + MorseDecoder* m_morseDecoder; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + AvailableChannelOrFeatureList m_availableChannels; + ChannelAPI *m_selectedChannel; + + explicit MorseDecoderGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~MorseDecoderGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displaySampleRate(int sampleRate); + void updateChannelList(); + bool handleMessage(const Message& message); + void makeUIConnections(); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_startStop_toggled(bool checked); + void on_channels_currentIndexChanged(int index); + void on_channelApply_clicked(); + void updateStatus(); + void tick(); + +}; + +#endif // INCLUDE_FEATURE_MORSEDECODERGUI_H_ diff --git a/plugins/feature/morsedecoder/morsedecodergui.ui b/plugins/feature/morsedecoder/morsedecodergui.ui new file mode 100644 index 000000000..58e57b76f --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecodergui.ui @@ -0,0 +1,468 @@ + + + MorseDecoderGUI + + + + 0 + 0 + 407 + 407 + + + + + 0 + 0 + + + + + 407 + 407 + + + + + Liberation Sans + 9 + + + + Morse Decoder + + + + + 0 + 10 + 401 + 121 + + + + + 401 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 0 + 22 + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Chan + + + + + + + + 200 + 0 + + + + Channel index + + + + + + + + 24 + 24 + + + + (Re) apply channel selection + + + + + + + :/checkmark.png:/checkmark.png + + + + + + + Channel sample rate + + + 00000.0 kS/s + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Send messages via UDP + + + Qt::RightToLeft + + + UDP + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 9998 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Pitch + + + + + + + + 60 + 0 + + + + CW pitch (Hz) + + + 2000.0 Hz + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Speed + + + + + + + + 50 + 0 + + + + CW speed (WPM) + + + 25 WPM + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Lock pitch and speed + + + + + + + :/unlocked.png + :/locked.png:/unlocked.png + + + true + + + + + + + + 24 + 16777215 + + + + Start/stop logging of received characters to .txt file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Clear messages + + + + + + + :/bin.png:/bin.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + C + + + + + + + + 55 + 0 + + + + 3000.000 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 130 + 401 + 271 + + + + Morse Text + + + + 0 + 0 + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+
+ + + + +
diff --git a/plugins/feature/morsedecoder/morsedecoderplugin.cpp b/plugins/feature/morsedecoder/morsedecoderplugin.cpp new file mode 100644 index 000000000..40e82916e --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoderplugin.cpp @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "morsedecodergui.h" +#endif +#include "morsedecoder.h" +#include "morsedecoderplugin.h" +#include "morsedecoderwebapiadapter.h" + +const PluginDescriptor MorseDecoderPlugin::m_pluginDescriptor = { + MorseDecoder::m_featureId, + QStringLiteral("Morse Decoder"), + QStringLiteral("7.21.0"), + QStringLiteral("(c) Edouard Griffiths, F4EXB"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +MorseDecoderPlugin::MorseDecoderPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& MorseDecoderPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void MorseDecoderPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register RigCtl Server feature + m_pluginAPI->registerFeature(MorseDecoder::m_featureIdURI, MorseDecoder::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* MorseDecoderPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* MorseDecoderPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return MorseDecoderGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* MorseDecoderPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new MorseDecoder(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* MorseDecoderPlugin::createFeatureWebAPIAdapter() const +{ + return new MorseDecoderWebAPIAdapter(); +} diff --git a/plugins/feature/morsedecoder/morsedecoderplugin.h b/plugins/feature/morsedecoder/morsedecoderplugin.h new file mode 100644 index 000000000..86ba749f5 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoderplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MORSEDECODERPLUGIN_H_ +#define INCLUDE_FEATURE_MORSEDECODERPLUGIN_H_ + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class MorseDecoderPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.morsedecoder") + +public: + explicit MorseDecoderPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const; + virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const; + virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + + +#endif // INCLUDE_FEATURE_MORSEDECODERPLUGIN_H_ diff --git a/plugins/feature/morsedecoder/morsedecodersettings.cpp b/plugins/feature/morsedecoder/morsedecodersettings.cpp new file mode 100644 index 000000000..d379cab79 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecodersettings.cpp @@ -0,0 +1,236 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "morsedecodersettings.h" + +const QStringList MorseDecoderSettings::m_channelURIs = { + QStringLiteral("sdrangel.channel.amdemod"), + QStringLiteral("sdrangel.channel.nfmdemod"), + QStringLiteral("sdrangel.channel.ssbdemod"), + QStringLiteral("sdrangel.channel.wfmdemod"), +}; + +MorseDecoderSettings::MorseDecoderSettings() : + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void MorseDecoderSettings::resetToDefaults() +{ + m_title = "Morse Decoder"; + m_rgbColor = QColor(0, 255, 0).rgb(); + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; + m_workspaceIndex = 0; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_logFilename = "cw_log.txt"; + m_logEnabled = false; +} + +QByteArray MorseDecoderSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(5, m_title); + s.writeU32(6, m_rgbColor); + s.writeBool(7, m_useReverseAPI); + s.writeString(8, m_reverseAPIAddress); + s.writeU32(9, m_reverseAPIPort); + s.writeU32(10, m_reverseAPIFeatureSetIndex); + s.writeU32(11, m_reverseAPIFeatureIndex); + + if (m_rollupState) { + s.writeBlob(12, m_rollupState->serialize()); + } + + s.writeS32(13, m_workspaceIndex); + s.writeBlob(14, m_geometryBytes); + + s.writeBool(22, m_udpEnabled); + s.writeString(23, m_udpAddress); + s.writeU32(24, m_udpPort); + s.writeString(25, m_logFilename); + s.writeBool(26, m_logEnabled); + + return s.final(); +} + +bool MorseDecoderSettings::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.readString(5, &m_title, "Demod Analyzer"); + d.readU32(6, &m_rgbColor, QColor(0, 255, 0).rgb()); + d.readBool(7, &m_useReverseAPI, false); + d.readString(8, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(9, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(10, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(11, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(12, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(13, &m_workspaceIndex, 0); + d.readBlob(14, &m_geometryBytes); + + d.readBool(22, &m_udpEnabled); + d.readString(23, &m_udpAddress); + d.readU32(24, &utmp); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + + d.readString(25, &m_logFilename, "cw_log.txt"); + d.readBool(26, &m_logEnabled, false); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +void MorseDecoderSettings::applySettings(const QStringList& settingsKeys, const MorseDecoderSettings& settings) +{ + if (settingsKeys.contains("title")) { + m_title = settings.m_title; + } + if (settingsKeys.contains("rgbColor")) { + m_rgbColor = settings.m_rgbColor; + } + if (settingsKeys.contains("useReverseAPI")) { + m_useReverseAPI = settings.m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress")) { + m_reverseAPIAddress = settings.m_reverseAPIAddress; + } + if (settingsKeys.contains("reverseAPIPort")) { + m_reverseAPIPort = settings.m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIFeatureSetIndex")) { + m_reverseAPIFeatureSetIndex = settings.m_reverseAPIFeatureSetIndex; + } + if (settingsKeys.contains("reverseAPIFeatureIndex")) { + m_reverseAPIFeatureIndex = settings.m_reverseAPIFeatureIndex; + } + if (settingsKeys.contains("workspaceIndex")) { + m_workspaceIndex = settings.m_workspaceIndex; + } + if (settingsKeys.contains("udpEnabled")) { + m_udpEnabled = settings.m_udpEnabled; + } + if (settingsKeys.contains("udpAddress")) { + m_udpAddress = settings.m_udpAddress; + } + if (settingsKeys.contains("udpPort")) { + m_udpPort = settings.m_udpPort; + } + if (settingsKeys.contains("logEnabled")) { + m_logEnabled = settings.m_logEnabled; + } + if (settingsKeys.contains("logFilename")) { + m_logFilename = settings.m_logFilename; + } +} + +QString MorseDecoderSettings::getDebugString(const QStringList& settingsKeys, bool force) const +{ + std::ostringstream ostr; + + if (settingsKeys.contains("title") || force) { + ostr << " m_title: " << m_title.toStdString(); + } + if (settingsKeys.contains("rgbColor") || force) { + ostr << " m_rgbColor: " << m_rgbColor; + } + if (settingsKeys.contains("useReverseAPI") || force) { + ostr << " m_useReverseAPI: " << m_useReverseAPI; + } + if (settingsKeys.contains("reverseAPIAddress") || force) { + ostr << " m_reverseAPIAddress: " << m_reverseAPIAddress.toStdString(); + } + if (settingsKeys.contains("reverseAPIPort") || force) { + ostr << " m_reverseAPIPort: " << m_reverseAPIPort; + } + if (settingsKeys.contains("reverseAPIFeatureSetIndex") || force) { + ostr << " m_reverseAPIFeatureSetIndex: " << m_reverseAPIFeatureSetIndex; + } + if (settingsKeys.contains("reverseAPIFeatureIndex") || force) { + ostr << " m_reverseAPIFeatureIndex: " << m_reverseAPIFeatureIndex; + } + if (settingsKeys.contains("workspaceIndex") || force) { + ostr << " m_workspaceIndex: " << m_workspaceIndex; + } + if (settingsKeys.contains("udpEnabled") || force) { + ostr << " m_udpEnabled: " << m_udpEnabled; + } + if (settingsKeys.contains("udpAddress") || force) { + ostr << " m_udpAddress: " << m_udpAddress.toStdString(); + } + if (settingsKeys.contains("udpPort") || force) { + ostr << " m_udpPort: " << m_udpPort; + } + if (settingsKeys.contains("logEnabled") || force) { + ostr << " m_logEnabled: " << m_logEnabled; + } + if (settingsKeys.contains("logFilename") || force) { + ostr << " m_logFilename: " << m_logFilename.toStdString(); + } + + return QString(ostr.str().c_str()); +} diff --git a/plugins/feature/morsedecoder/morsedecodersettings.h b/plugins/feature/morsedecoder/morsedecodersettings.h new file mode 100644 index 000000000..51df14ae3 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecodersettings.h @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MORSEDECODERSETTINGS_H_ +#define INCLUDE_FEATURE_MORSEDECODERSETTINGS_H_ + +#include +#include + +class Serializable; + +struct MorseDecoderSettings +{ + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + QString m_logFilename; + bool m_logEnabled; + + MorseDecoderSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void applySettings(const QStringList& settingsKeys, const MorseDecoderSettings& settings); + QString getDebugString(const QStringList& settingsKeys, bool force=false) const; + + static const QStringList m_channelURIs; +}; + +#endif // INCLUDE_FEATURE_MORSEDECODERSETTINGS_H_ diff --git a/plugins/feature/morsedecoder/morsedecoderwebapiadapter.cpp b/plugins/feature/morsedecoder/morsedecoderwebapiadapter.cpp new file mode 100644 index 000000000..cb085f659 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoderwebapiadapter.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGFeatureSettings.h" +#include "morsedecoder.h" +#include "morsedecoderwebapiadapter.h" + +MorseDecoderWebAPIAdapter::MorseDecoderWebAPIAdapter() +{} + +MorseDecoderWebAPIAdapter::~MorseDecoderWebAPIAdapter() +{} + +int MorseDecoderWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setDemodAnalyzerSettings(new SWGSDRangel::SWGDemodAnalyzerSettings()); + response.getDemodAnalyzerSettings()->init(); + MorseDecoder::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int MorseDecoderWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + MorseDecoder::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/morsedecoder/morsedecoderwebapiadapter.h b/plugins/feature/morsedecoder/morsedecoderwebapiadapter.h new file mode 100644 index 000000000..bcdcedbc6 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoderwebapiadapter.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MORSEDECODERWEBAPIADAPTER_H_ +#define INCLUDE_FEATURE_MORSEDECODERWEBAPIADAPTER_H_ + +#include "feature/featurewebapiadapter.h" +#include "morsedecodersettings.h" + +/** + * Standalone API adapter only for the settings + */ +class MorseDecoderWebAPIAdapter : public FeatureWebAPIAdapter { +public: + MorseDecoderWebAPIAdapter(); + virtual ~MorseDecoderWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + +private: + MorseDecoderSettings m_settings; +}; + +#endif // INCLUDE_FEATURE_MORSEDECODERWEBAPIADAPTER_H_ diff --git a/plugins/feature/morsedecoder/morsedecoderworker.cpp b/plugins/feature/morsedecoder/morsedecoderworker.cpp new file mode 100644 index 000000000..06f8e7186 --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoderworker.cpp @@ -0,0 +1,195 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/scopevis.h" +#include "dsp/datafifo.h" + +#include "morsedecoderworker.h" + +MESSAGE_CLASS_DEFINITION(MorseDecoderWorker::MsgConfigureMorseDecoderWorker, Message) +MESSAGE_CLASS_DEFINITION(MorseDecoderWorker::MsgConnectFifo, Message) + +MorseDecoderWorker::MorseDecoderWorker() : + m_dataFifo(nullptr), + m_msgQueueToFeature(nullptr), + m_sampleBufferSize(0), + m_nbBytes(0) +{ + qDebug("MorseDecoderWorker::MorseDecoderWorker"); +} + +MorseDecoderWorker::~MorseDecoderWorker() +{ + m_inputMessageQueue.clear(); +} + +void MorseDecoderWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +void MorseDecoderWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +void MorseDecoderWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +void MorseDecoderWorker::feedPart( + const QByteArray::const_iterator& begin, + const QByteArray::const_iterator& end, + DataFifo::DataType dataType +) +{ + int nbBytes; + + switch(dataType) + { + case DataFifo::DataTypeCI16: + nbBytes = 4; + break; + case DataFifo::DataTypeI16: + default: + nbBytes = 2; + } + + m_nbBytes = nbBytes; + int countSamples = (end - begin) / nbBytes; + + if (countSamples > m_sampleBufferSize) + { + m_sampleBuffer.resize(countSamples); + m_sampleBufferSize = countSamples; + } + + // TODO + // for (int i = 0; i < countSamples; i++) { + // processSample(dataType, begin, countSamples, i); + // } +} + +void MorseDecoderWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool MorseDecoderWorker::handleMessage(const Message& cmd) +{ + if (MsgConfigureMorseDecoderWorker::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureMorseDecoderWorker& cfg = (MsgConfigureMorseDecoderWorker&) cmd; + qDebug("MorseDecoderWorker::handleMessage: MsgConfigureMorseDecoderWorker"); + + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce()); + + return true; + } + else if (MsgConnectFifo::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConnectFifo& msg = (MsgConnectFifo&) cmd; + m_dataFifo = msg.getFifo(); + bool doConnect = msg.getConnect(); + qDebug("DemodAnalyzerWorker::handleMessage: MsgConnectFifo: %s", (doConnect ? "connect" : "disconnect")); + + if (doConnect) { + QObject::connect( + m_dataFifo, + &DataFifo::dataReady, + this, + &MorseDecoderWorker::handleData, + Qt::QueuedConnection + ); + } + else + { + QObject::disconnect( + m_dataFifo, + &DataFifo::dataReady, + this, + &MorseDecoderWorker::handleData + ); + } + + return true; + } + else + { + return false; + } +} + +void MorseDecoderWorker::applySettings(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force) +{ + qDebug() << "MorseDecoderWorker::applySettings:" << settings.getDebugString(settingsKeys, force) << force; + + if (force) { + m_settings = settings; + } else { + m_settings.applySettings(settingsKeys, settings); + } + +} + +void MorseDecoderWorker::applySampleRate(int sampleRate) +{ + m_sinkSampleRate = sampleRate; +} + +void MorseDecoderWorker::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + while ((m_dataFifo->fill() > 0) && (m_inputMessageQueue.size() == 0)) + { + QByteArray::iterator part1begin; + QByteArray::iterator part1end; + QByteArray::iterator part2begin; + QByteArray::iterator part2end; + DataFifo::DataType dataType; + + std::size_t count = m_dataFifo->readBegin(m_dataFifo->fill(), &part1begin, &part1end, &part2begin, &part2end, dataType); + + // first part of FIFO data + if (part1begin != part1end) { + feedPart(part1begin, part1end, dataType); + } + + // second part of FIFO data (used when block wraps around) + if (part2begin != part2end) { + feedPart(part2begin, part2end, dataType); + } + + m_dataFifo->readCommit((unsigned int) count); + } +} diff --git a/plugins/feature/morsedecoder/morsedecoderworker.h b/plugins/feature/morsedecoder/morsedecoderworker.h new file mode 100644 index 000000000..67cb614eb --- /dev/null +++ b/plugins/feature/morsedecoder/morsedecoderworker.h @@ -0,0 +1,121 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MORSEDECODERWORKER_H_ +#define INCLUDE_FEATURE_MORSEDECODERWORKER_H_ + +#include + +#include +#include +#include + +#include "dsp/dsptypes.h" +#include "dsp/datafifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "morsedecodersettings.h" + + +class MorseDecoderWorker : public QObject { + Q_OBJECT +public: + class MsgConfigureMorseDecoderWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const MorseDecoderSettings& getSettings() const { return m_settings; } + const QList& getSettingsKeys() const { return m_settingsKeys; } + bool getForce() const { return m_force; } + + static MsgConfigureMorseDecoderWorker* create(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force) + { + return new MsgConfigureMorseDecoderWorker(settings, settingsKeys, force); + } + + private: + MorseDecoderSettings m_settings; + QList m_settingsKeys; + bool m_force; + + MsgConfigureMorseDecoderWorker(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force) : + Message(), + m_settings(settings), + m_settingsKeys(settingsKeys), + m_force(force) + { } + }; + + class MsgConnectFifo : public Message { + MESSAGE_CLASS_DECLARATION + + public: + DataFifo *getFifo() { return m_fifo; } + bool getConnect() const { return m_connect; } + + static MsgConnectFifo* create(DataFifo *fifo, bool connect) { + return new MsgConnectFifo(fifo, connect); + } + private: + DataFifo *m_fifo; + bool m_connect; + MsgConnectFifo(DataFifo *fifo, bool connect) : + Message(), + m_fifo(fifo), + m_connect(connect) + { } + }; + + MorseDecoderWorker(); + ~MorseDecoderWorker(); + void reset(); + void startWork(); + void stopWork(); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; } + + void applySampleRate(int sampleRate); + void applySettings(const MorseDecoderSettings& settings, const QList& settingsKeys, bool force = false); + +private: + DataFifo *m_dataFifo; + int m_channelSampleRate; + int m_sinkSampleRate; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_msgQueueToFeature; //!< Queue to report channel change to main feature object + MorseDecoderSettings m_settings; + double m_magsq; + SampleVector m_sampleBuffer; + int m_sampleBufferSize; + int m_nbBytes; + QRecursiveMutex m_mutex; + + void feedPart( + const QByteArray::const_iterator& begin, + const QByteArray::const_iterator& end, + DataFifo::DataType dataType + ); + + bool handleMessage(const Message& cmd); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_FEATURE_MORSEDECODERWORKER_H_