diff --git a/doc/img/RigCtlServer_plugin.png b/doc/img/RigCtlServer_plugin.png new file mode 100644 index 000000000..6e07b69fc Binary files /dev/null and b/doc/img/RigCtlServer_plugin.png differ diff --git a/doc/img/RigCtlServer_plugin.xcf b/doc/img/RigCtlServer_plugin.xcf new file mode 100644 index 000000000..a9e1aecd2 Binary files /dev/null and b/doc/img/RigCtlServer_plugin.xcf differ diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 40d228b3a..58a75ed42 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -1,3 +1,4 @@ project(feature) +add_subdirectory(rigctlserver) add_subdirectory(simpleptt) diff --git a/plugins/feature/rigctlserver/CMakeLists.txt b/plugins/feature/rigctlserver/CMakeLists.txt new file mode 100644 index 000000000..c1a0223ee --- /dev/null +++ b/plugins/feature/rigctlserver/CMakeLists.txt @@ -0,0 +1,56 @@ +project(rigctlserver) + +set(rigctlserver_SOURCES + rigctlserver.cpp + rigctlserversettings.cpp + rigctlserverplugin.cpp + rigctlserverworker.cpp + rigctlserverwebapiadapter.cpp +) + +set(rigctlserver_HEADERS + rigctlserver.h + rigctlserversettings.h + rigctlserverplugin.h + rigctlserverworker.h + rigctlserverwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(rigctlserver_SOURCES + ${rigctlserver_SOURCES} + rigctlservergui.cpp + rigctlservergui.ui + ) + set(rigctlserver_HEADERS + ${rigctlserver_HEADERS} + rigctlservergui.h + ) + + set(TARGET_NAME featurerigctlserver) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featurerigctlserversrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${rigctlserver_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/feature/rigctlserver/readme.md b/plugins/feature/rigctlserver/readme.md new file mode 100644 index 000000000..b60e46ef2 --- /dev/null +++ b/plugins/feature/rigctlserver/readme.md @@ -0,0 +1,66 @@ +

RigCtl server plugin

+ +

Introduction

+ +The rigctl server plugin allows SDRangel to be controlled via [Hamlib](http://hamlib.sourceforge.net/manuals/hamlib.html)'s rigctld protocol. This allows other software that implements the rigctld protocol, such at the satellite tracking software GPredict, to control SDRangel, to adjust for doppler or to automatically switch between different satellite frequencies and modes. + +

Interface

+ +![File source channel plugin GUI](../../../doc/img/RigCtlServer_plugin.png) + +

1: Start/Stop plugin

+ +This button starts or stops the plugin + +

2: Enable rigctrl server

+ +Checking this option will enable the rigctrl server in SDRangel. The default is disabled. + +

3: Refresh list of devices and channels

+ +Use this button to refresh the list of devices (4) and channels (5) + +

4: Select device set

+ +Specify the SDRangel device set that will be controlled by received rigctl commands. Defaults to R0. + +

5: Select channel

+ +The channel index specifies the SDRangel channel that will be controlled by received rigctl commands. Defaults to 0. + +

6: Port

+ +The rigctl server plugin opens a TCP port to receive commands from a rigctl client. Please specify a free TCP port number. The default rigctld port is 4532. + +

7: Max Frequency Offset in Hz

+ +The maximum frequency offset controls whether the center frequency or frequency offset is adjusted when a new frequency is received by a rigctl command. +If the difference between the new frequency and the current center frequency is less than this value, the input offset (in the demodulator) will be adjusted. +If the difference is greater than this value, the center frequency will be set to the received frequency. +To only ever set the center frequency, set this value to 0. The default value is 10000. + +

Supported rigctrl Commands

+ +The following rigctrl commands are supported: + + + +

Example rigctrl Session

+ +Run SDRangel and from the Preferences menu select rigctrl. Check "Enable rigctrl server" and press OK. + +In a terminal window, run: + +
+telnet localhost 4532
+set_mode AM 1000
+set_freq 100000000
+set_powerstat 1
+
diff --git a/plugins/feature/rigctlserver/rigctlserver.cpp b/plugins/feature/rigctlserver/rigctlserver.cpp new file mode 100644 index 000000000..0587f7eab --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserver.cpp @@ -0,0 +1,402 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureReport.h" +#include "SWGFeatureActions.h" +#include "SWGSimplePTTReport.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" + +#include "rigctlserverworker.h" +#include "rigctlserver.h" + +MESSAGE_CLASS_DEFINITION(RigCtlServer::MsgConfigureRigCtlServer, Message) +MESSAGE_CLASS_DEFINITION(RigCtlServer::MsgStartStop, Message) + +const QString RigCtlServer::m_featureIdURI = "sdrangel.feature.rigctlserver"; +const QString RigCtlServer::m_featureId = "RigCtlServer"; + +RigCtlServer::RigCtlServer(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface), + m_ptt(false) +{ + qDebug("RigCtlServer::RigCtlServer: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_worker = new RigCtlServerWorker(webAPIAdapterInterface); + m_state = StIdle; + m_errorMessage = "RigCtlServer error"; +} + +RigCtlServer::~RigCtlServer() +{ + if (m_worker->isRunning()) { + stop(); + } + + delete m_worker; +} + +void RigCtlServer::start() +{ + qDebug("RigCtlServer::start"); + + m_worker->reset(); + m_worker->setMessageQueueToFeature(getInputMessageQueue()); + bool ok = m_worker->startWork(); + m_state = ok ? StRunning : StError; + m_thread.start(); + + RigCtlServerWorker::MsgConfigureRigCtlServerWorker *msg = RigCtlServerWorker::MsgConfigureRigCtlServerWorker::create(m_settings, true); + m_worker->getInputMessageQueue()->push(msg); +} + +void RigCtlServer::stop() +{ + qDebug("RigCtlServer::stop"); + m_worker->stopWork(); + m_state = StIdle; + m_thread.quit(); + m_thread.wait(); +} + +bool RigCtlServer::handleMessage(const Message& cmd) +{ + if (MsgConfigureRigCtlServer::match(cmd)) + { + MsgConfigureRigCtlServer& cfg = (MsgConfigureRigCtlServer&) cmd; + qDebug() << "RigCtlServer::handleMessage: MsgConfigureRigCtlServer"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + qDebug() << "RigCtlServer::handleMessage: MsgStartStop: start:" << cfg.getStartStop(); + + if (cfg.getStartStop()) { + start(); + } else { + stop(); + } + + return true; + } + else if (RigCtlServerSettings::MsgChannelIndexChange::match(cmd)) + { + RigCtlServerSettings::MsgChannelIndexChange& cfg = (RigCtlServerSettings::MsgChannelIndexChange&) cmd; + int newChannelIndex = cfg.getIndex(); + qDebug() << "RigCtlServer::handleMessage: MsgChannelIndexChange: " << newChannelIndex; + RigCtlServerSettings settings = m_settings; + settings.m_channelIndex = newChannelIndex; + applySettings(settings, false); + + if (getMessageQueueToGUI()) + { + RigCtlServerSettings::MsgChannelIndexChange *msg = new RigCtlServerSettings::MsgChannelIndexChange(cfg); + getMessageQueueToGUI()->push(msg); + } + + return true; + } + else + { + return false; + } +} + +QByteArray RigCtlServer::serialize() const +{ + return m_settings.serialize(); +} + +bool RigCtlServer::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureRigCtlServer *msg = MsgConfigureRigCtlServer::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureRigCtlServer *msg = MsgConfigureRigCtlServer::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void RigCtlServer::applySettings(const RigCtlServerSettings& settings, bool force) +{ + qDebug() << "RigCtlServer::applySettings:" + << " m_enabled: " << settings.m_enabled + << " m_deviceIndex: " << settings.m_deviceIndex + << " m_channelIndex: " << settings.m_channelIndex + << " m_rigCtlPort: " << settings.m_rigCtlPort + << " m_maxFrequencyOffset: " << settings.m_maxFrequencyOffset + << " m_title: " << settings.m_title + << " m_rgbColor: " << settings.m_rgbColor + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex + << " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((m_settings.m_enabled != settings.m_enabled) || force) { + reverseAPIKeys.append("enabled"); + } + if ((m_settings.m_deviceIndex != settings.m_deviceIndex) || force) { + reverseAPIKeys.append("deviceIndex"); + } + if ((m_settings.m_channelIndex != settings.m_channelIndex) || force) { + reverseAPIKeys.append("channelIndex"); + } + if ((m_settings.m_rigCtlPort != settings.m_rigCtlPort) || force) { + reverseAPIKeys.append("rigCtlPort"); + } + if ((m_settings.m_maxFrequencyOffset != settings.m_maxFrequencyOffset) || force) { + reverseAPIKeys.append("maxFrequencyOffset"); + } + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + RigCtlServerWorker::MsgConfigureRigCtlServerWorker *msg = RigCtlServerWorker::MsgConfigureRigCtlServerWorker::create( + settings, force + ); + m_worker->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_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) || + (m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +int RigCtlServer::webapiRun(bool run, + SWGSDRangel::SWGDeviceState& response, + QString& errorMessage) +{ + getFeatureStateStr(*response.getState()); + MsgStartStop *msg = MsgStartStop::create(run); + getInputMessageQueue()->push(msg); + return 202; +} + +int RigCtlServer::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setRigCtlServerSettings(new SWGSDRangel::SWGRigCtlServerSettings()); + response.getRigCtlServerSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int RigCtlServer::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + RigCtlServerSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureRigCtlServer *msg = MsgConfigureRigCtlServer::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("RigCtlServer::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureRigCtlServer *msgToGUI = MsgConfigureRigCtlServer::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void RigCtlServer::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const RigCtlServerSettings& settings) +{ + response.getRigCtlServerSettings()->setEnabled(settings.m_enabled ? 1 : 0); + response.getRigCtlServerSettings()->setDeviceIndex(settings.m_deviceIndex); + response.getRigCtlServerSettings()->setChannelIndex(settings.m_channelIndex); + response.getRigCtlServerSettings()->setRigCtlPort(settings.m_rigCtlPort); + response.getRigCtlServerSettings()->setMaxFrequencyOffset(settings.m_maxFrequencyOffset); + + if (response.getRigCtlServerSettings()->getTitle()) { + *response.getRigCtlServerSettings()->getTitle() = settings.m_title; + } else { + response.getRigCtlServerSettings()->setTitle(new QString(settings.m_title)); + } + + response.getRigCtlServerSettings()->setRgbColor(settings.m_rgbColor); + response.getRigCtlServerSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getRigCtlServerSettings()->getReverseApiAddress()) { + *response.getRigCtlServerSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getRigCtlServerSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getRigCtlServerSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getRigCtlServerSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex); + response.getRigCtlServerSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex); +} + +void RigCtlServer::webapiUpdateFeatureSettings( + RigCtlServerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("enabled")) { + settings.m_enabled = response.getRigCtlServerSettings()->getEnabled(); + } + if (featureSettingsKeys.contains("deviceIndex")) { + settings.m_deviceIndex = response.getRigCtlServerSettings()->getDeviceIndex(); + } + if (featureSettingsKeys.contains("channelIndex")) { + settings.m_channelIndex = response.getRigCtlServerSettings()->getChannelIndex(); + } + if (featureSettingsKeys.contains("rigCtlPort")) { + settings.m_rigCtlPort = response.getRigCtlServerSettings()->getRigCtlPort(); + } + if (featureSettingsKeys.contains("maxFrequencyOffset")) { + settings.m_maxFrequencyOffset = response.getRigCtlServerSettings()->getMaxFrequencyOffset(); + } + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getRigCtlServerSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getRigCtlServerSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getRigCtlServerSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getRigCtlServerSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getRigCtlServerSettings()->getReverseApiPort(); + } + if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIFeatureSetIndex = response.getRigCtlServerSettings()->getReverseApiDeviceIndex(); + } + if (featureSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIFeatureIndex = response.getRigCtlServerSettings()->getReverseApiChannelIndex(); + } +} + +void RigCtlServer::webapiReverseSendSettings(QList& featureSettingsKeys, const RigCtlServerSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("RigCtlServer")); + swgFeatureSettings->setRigCtlServerSettings(new SWGSDRangel::SWGRigCtlServerSettings()); + SWGSDRangel::SWGRigCtlServerSettings *swgRigCtlServerSettings = swgFeatureSettings->getRigCtlServerSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("enabled") || force) { + swgRigCtlServerSettings->setEnabled(settings.m_enabled ? 1 : 0); + } + if (featureSettingsKeys.contains("deviceIndex") || force) { + swgRigCtlServerSettings->setDeviceIndex(settings.m_deviceIndex); + } + if (featureSettingsKeys.contains("channelIndex") || force) { + swgRigCtlServerSettings->setChannelIndex(settings.m_channelIndex); + } + if (featureSettingsKeys.contains("rigCtlPort") || force) { + swgRigCtlServerSettings->setRigCtlPort(settings.m_rigCtlPort); + } + if (featureSettingsKeys.contains("maxFrequencyOffset") || force) { + swgRigCtlServerSettings->setMaxFrequencyOffset(settings.m_maxFrequencyOffset); + } + if (featureSettingsKeys.contains("title") || force) { + swgRigCtlServerSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgRigCtlServerSettings->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 RigCtlServer::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "RigCtlServer::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("RigCtlServer::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/rigctlserver/rigctlserver.h b/plugins/feature/rigctlserver/rigctlserver.h new file mode 100644 index 000000000..03e813108 --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserver.h @@ -0,0 +1,139 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RIGCTLSERVER_H_ +#define INCLUDE_FEATURE_RIGCTLSERVER_H_ + +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "rigctlserversettings.h" + +class WebAPIAdapterInterface; +class RigCtlServerWorker; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class RigCtlServer : public Feature +{ + Q_OBJECT +public: + class MsgConfigureRigCtlServer : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RigCtlServerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRigCtlServer* create(const RigCtlServerSettings& settings, bool force) { + return new MsgConfigureRigCtlServer(settings, force); + } + + private: + RigCtlServerSettings m_settings; + bool m_force; + + MsgConfigureRigCtlServer(const RigCtlServerSettings& settings, bool force) : + Message(), + m_settings(settings), + 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) + { } + }; + + RigCtlServer(WebAPIAdapterInterface *webAPIAdapterInterface); + ~RigCtlServer(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = 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); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const RigCtlServerSettings& settings); + + static void webapiUpdateFeatureSettings( + RigCtlServerSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const QString m_featureIdURI; + static const QString m_featureId; + +private: + QThread m_thread; + RigCtlServerWorker *m_worker; + RigCtlServerSettings m_settings; + bool m_ptt; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const RigCtlServerSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const RigCtlServerSettings& settings, bool force); + +private slots: + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_RIGCTLSERVER_H_ diff --git a/plugins/feature/rigctlserver/rigctlservergui.cpp b/plugins/feature/rigctlserver/rigctlservergui.cpp new file mode 100644 index 000000000..9b16689aa --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlservergui.cpp @@ -0,0 +1,409 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "feature/featureuiset.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "mainwindow.h" +#include "device/deviceuiset.h" + +#include "ui_rigctlservergui.h" +#include "rigctlserver.h" +#include "rigctlservergui.h" + +RigCtlServerGUI* RigCtlServerGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + RigCtlServerGUI* gui = new RigCtlServerGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void RigCtlServerGUI::destroy() +{ + delete this; +} + +void RigCtlServerGUI::setName(const QString& name) +{ + setObjectName(name); +} + +QString RigCtlServerGUI::getName() const +{ + return objectName(); +} + +void RigCtlServerGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray RigCtlServerGUI::serialize() const +{ + qDebug("RigCtlServerGUI::serialize: %d", m_settings.m_channelIndex); + return m_settings.serialize(); +} + +bool RigCtlServerGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + qDebug("RigCtlServerGUI::deserialize: %d", m_settings.m_channelIndex); + updateDeviceSetList(); + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool RigCtlServerGUI::handleMessage(const Message& message) +{ + if (RigCtlServer::MsgConfigureRigCtlServer::match(message)) + { + qDebug("RigCtlServerGUI::handleMessage: RigCtlServer::MsgConfigureRigCtlServer"); + const RigCtlServer::MsgConfigureRigCtlServer& cfg = (RigCtlServer::MsgConfigureRigCtlServer&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (RigCtlServerSettings::MsgChannelIndexChange::match(message)) + { + const RigCtlServerSettings::MsgChannelIndexChange& cfg = (RigCtlServerSettings::MsgChannelIndexChange&) message; + int newChannelIndex = cfg.getIndex(); + qDebug("RigCtlServerGUI::handleMessage: RigCtlServerSettings::MsgChannelIndexChange: %d", newChannelIndex); + ui->channel->blockSignals(true); + ui->channel->setCurrentIndex(newChannelIndex); + m_settings.m_channelIndex = newChannelIndex; + ui->channel->blockSignals(false); + + return true; + } + + return false; +} + +void RigCtlServerGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void RigCtlServerGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +RigCtlServerGUI::RigCtlServerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + RollupWidget(parent), + ui(new Ui::RigCtlServerGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + setChannelWidget(false); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + m_rigCtlServer = reinterpret_cast(feature); + m_rigCtlServer->setMessageQueueToGUI(&m_inputMessageQueue); + + m_featureUISet->registerFeatureInstance(RigCtlServer::m_featureIdURI, this, m_rigCtlServer); + m_featureUISet->addRollupWidget(this); + + 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); + + updateDeviceSetList(); + displaySettings(); + applySettings(true); +} + +RigCtlServerGUI::~RigCtlServerGUI() +{ + m_featureUISet->removeFeatureInstance(this); + delete m_rigCtlServer; // When the GUI closes it has to delete the demodulator because it can be done with (x) + delete ui; +} + +void RigCtlServerGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void RigCtlServerGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + blockApplySettings(true); + ui->rigCtrlPort->setValue(m_settings.m_rigCtlPort); + ui->maxFrequencyOffset->setValue(m_settings.m_maxFrequencyOffset); + blockApplySettings(false); +} + +void RigCtlServerGUI::updateDeviceSetList() +{ + MainWindow *mainWindow = MainWindow::getInstance(); + std::vector& deviceUISets = mainWindow->getDeviceUISets(); + std::vector::const_iterator it = deviceUISets.begin(); + + ui->device->blockSignals(true); + + ui->device->clear(); + unsigned int deviceIndex = 0; + + for (; it != deviceUISets.end(); ++it, deviceIndex++) + { + DSPDeviceSourceEngine *deviceSourceEngine = (*it)->m_deviceSourceEngine; + DSPDeviceSinkEngine *deviceSinkEngine = (*it)->m_deviceSinkEngine; + DSPDeviceMIMOEngine *deviceMIMOEngine = (*it)->m_deviceMIMOEngine; + + if (deviceSourceEngine) { + ui->device->addItem(QString("R%1").arg(deviceIndex), deviceIndex); + } + } + + int newDeviceIndex; + + if (it != deviceUISets.begin()) + { + if (m_settings.m_deviceIndex < 0) { + ui->device->setCurrentIndex(0); + } else { + ui->device->setCurrentIndex(m_settings.m_deviceIndex); + } + + newDeviceIndex = ui->device->currentData().toInt(); + } + else + { + newDeviceIndex = -1; + } + + + if (newDeviceIndex != m_settings.m_deviceIndex) + { + qDebug("RigCtlServerGUI::updateDeviceSetLists: device index changed: %d", newDeviceIndex); + m_settings.m_deviceIndex = newDeviceIndex; + } + + updateChannelList(); + + ui->device->blockSignals(false); +} + +bool RigCtlServerGUI::updateChannelList() +{ + int newChannelIndex; + ui->channel->blockSignals(true); + ui->channel->clear(); + + if (m_settings.m_deviceIndex < 0) + { + newChannelIndex = -1; + } + else + { + MainWindow *mainWindow = MainWindow::getInstance(); + std::vector& deviceUISets = mainWindow->getDeviceUISets(); + DeviceUISet *deviceUISet = deviceUISets[m_settings.m_deviceIndex]; + int nbChannels = deviceUISet->getNumberOfChannels(); + + for (int ch = 0; ch < nbChannels; ch++) { + ui->channel->addItem(QString("%1").arg(ch), ch); + } + + if (nbChannels > 0) + { + if (m_settings.m_channelIndex < 0) { + ui->channel->setCurrentIndex(0); + } else { + ui->channel->setCurrentIndex(m_settings.m_channelIndex); + } + + newChannelIndex = ui->channel->currentIndex(); + } + else + { + newChannelIndex = -1; + } + } + + ui->channel->blockSignals(false); + + if (newChannelIndex != m_settings.m_channelIndex) + { + qDebug("RigCtlServerGUI::updateChannelList: channel index changed: %d", newChannelIndex); + m_settings.m_channelIndex = newChannelIndex; + return true; + } + + return false; +} + +void RigCtlServerGUI::leaveEvent(QEvent*) +{ +} + +void RigCtlServerGUI::enterEvent(QEvent*) +{ +} + +void RigCtlServerGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setColor(m_settings.m_rgbColor); + 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.move(p); + dialog.exec(); + + m_settings.m_rgbColor = dialog.getColor().rgb(); + 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(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + + resetContextMenuType(); +} + +void RigCtlServerGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + RigCtlServer::MsgStartStop *message = RigCtlServer::MsgStartStop::create(checked); + m_rigCtlServer->getInputMessageQueue()->push(message); + } +} + +void RigCtlServerGUI::on_enable_toggled(bool checked) +{ + m_settings.m_enabled = checked; + applySettings(); +} + +void RigCtlServerGUI::on_devicesRefresh_clicked() +{ + updateDeviceSetList(); + displaySettings(); + applySettings(); +} + +void RigCtlServerGUI::on_device_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_deviceIndex = ui->device->currentData().toInt(); + updateChannelList(); + applySettings(); + } +} + +void RigCtlServerGUI::on_channel_currentIndexChanged(int index) +{ + if (index >= 0) + { + m_settings.m_channelIndex = index; + applySettings(); + } +} + +void RigCtlServerGUI::on_rigCtrlPort_valueChanged(int value) +{ + m_settings.m_rigCtlPort = value; + applySettings(); +} + +void RigCtlServerGUI::on_maxFrequencyOffset_valueChanged(int value) +{ + m_settings.m_maxFrequencyOffset = value; + applySettings(); +} + +void RigCtlServerGUI::updateStatus() +{ + int state = m_rigCtlServer->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_rigCtlServer->getErrorMessage()); + break; + default: + break; + } + + m_lastFeatureState = state; + } +} + +void RigCtlServerGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + RigCtlServer::MsgConfigureRigCtlServer* message = RigCtlServer::MsgConfigureRigCtlServer::create( m_settings, force); + m_rigCtlServer->getInputMessageQueue()->push(message); + } +} diff --git a/plugins/feature/rigctlserver/rigctlservergui.h b/plugins/feature/rigctlserver/rigctlservergui.h new file mode 100644 index 000000000..78adb9287 --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlservergui.h @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RIGCTLSERVERGUI_H_ +#define INCLUDE_FEATURE_RIGCTLSERVERGUI_H_ + +#include + +#include "plugin/plugininstancegui.h" +#include "gui/rollupwidget.h" +#include "util/messagequeue.h" +#include "rigctlserversettings.h" + +class PluginAPI; +class FeatureUISet; +class RigCtlServer; + +namespace Ui { + class RigCtlServerGUI; +} + +class RigCtlServerGUI : public RollupWidget, public PluginInstanceGUI { + Q_OBJECT +public: + static RigCtlServerGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + void setName(const QString& name); + QString getName() const; + virtual qint64 getCenterFrequency() const { return 0; } + virtual void setCenterFrequency(qint64 centerFrequency) {} + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual bool handleMessage(const Message& message); + +private: + Ui::RigCtlServerGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + RigCtlServerSettings m_settings; + bool m_doApplySettings; + + RigCtlServer* m_rigCtlServer; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + + explicit RigCtlServerGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~RigCtlServerGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void updateDeviceSetList(); + bool updateChannelList(); //!< true if channel index has changed + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_startStop_toggled(bool checked); + void on_enable_toggled(bool checked); + void on_devicesRefresh_clicked(); + void on_device_currentIndexChanged(int index); + void on_channel_currentIndexChanged(int index); + void on_rigCtrlPort_valueChanged(int value); + void on_maxFrequencyOffset_valueChanged(int value); + void updateStatus(); +}; + + +#endif // INCLUDE_FEATURE_RIGCTLSERVERGUI_H_ diff --git a/plugins/feature/rigctlserver/rigctlservergui.ui b/plugins/feature/rigctlserver/rigctlservergui.ui new file mode 100644 index 000000000..3e3873dbd --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlservergui.ui @@ -0,0 +1,282 @@ + + + RigCtlServerGUI + + + + 0 + 0 + 320 + 189 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 320 + 16777215 + + + + + Liberation Sans + 9 + + + + RigCtl Server + + + + + 10 + 10 + 301 + 171 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + start/stop acquisition + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Select to enable rigctrl server. + + + Enable rigctl server + + + + + + + + + + + + 24 + 16777215 + + + + Refresh indexes of available device sets + + + + + + + :/recycle.png:/recycle.png + + + + + + + Device + + + + + + + + 55 + 0 + + + + Receiver deviceset index + + + + + + + Channel + + + + + + + + 55 + 0 + + + + Channel index + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + RigCtl Port + + + + + + + TCP port to listen for rigctrl commands on. +Default is 4532. + + + 1024 + + + 65536 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Max Frequency Offset + + + + + + + Controls whether the center frequency or frequency offset is adjusted when a new frequency is received via a rigctrl command. +If the difference between the new frequency and the current center frequency is less than this value, the offset will be adjusted. If it is greater than this value, the center frequency will be set to the new frequency. +To only ever set the center frequency, set this value to 0. +Default is 10000. + + + 9999999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+
+ + + + +
diff --git a/plugins/feature/rigctlserver/rigctlserverplugin.cpp b/plugins/feature/rigctlserver/rigctlserverplugin.cpp new file mode 100644 index 000000000..4f30ec491 --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserverplugin.cpp @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "rigctlservergui.h" +#endif +#include "rigctlserver.h" +#include "rigctlserverplugin.h" +#include "rigctlserverwebapiadapter.h" + +const PluginDescriptor RigCtlServerPlugin::m_pluginDescriptor = { + RigCtlServer::m_featureId, + QString("RigCtl Server"), + QString("5.12.0"), + QString("(c) Edouard Griffiths, F4EXB"), + QString("https://github.com/f4exb/sdrangel"), + true, + QString("https://github.com/f4exb/sdrangel") +}; + +RigCtlServerPlugin::RigCtlServerPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& RigCtlServerPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void RigCtlServerPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + // register RigCtl Server feature + m_pluginAPI->registerFeature(RigCtlServer::m_featureIdURI, RigCtlServer::m_featureId, this); +} + +#ifdef SERVER_MODE +PluginInstanceGUI* RigCtlServerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +PluginInstanceGUI* RigCtlServerPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return RigCtlServerGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* RigCtlServerPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new RigCtlServer(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* RigCtlServerPlugin::createFeatureWebAPIAdapter() const +{ + return new RigCtlServerWebAPIAdapter(); +} diff --git a/plugins/feature/rigctlserver/rigctlserverplugin.h b/plugins/feature/rigctlserver/rigctlserverplugin.h new file mode 100644 index 000000000..455eb38da --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserverplugin.h @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RIGCTLSERVERPLUGIN_H +#define INCLUDE_FEATURE_RIGCTLSERVERPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class WebAPIAdapterInterface; + +class RigCtlServerPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.rigctlserver") + +public: + explicit RigCtlServerPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual PluginInstanceGUI* 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_RIGCTLSERVERPLUGIN_H diff --git a/plugins/feature/rigctlserver/rigctlserversettings.cpp b/plugins/feature/rigctlserver/rigctlserversettings.cpp new file mode 100644 index 000000000..a4038f55a --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserversettings.cpp @@ -0,0 +1,119 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "rigctlserversettings.h" + +MESSAGE_CLASS_DEFINITION(RigCtlServerSettings::MsgChannelIndexChange, Message) + +RigCtlServerSettings::RigCtlServerSettings() +{ + resetToDefaults(); +} + +void RigCtlServerSettings::resetToDefaults() +{ + m_rigCtlPort = 4532; + m_maxFrequencyOffset = 10000; + m_deviceIndex = -1; + m_channelIndex = -1; + m_title = "RigCtl Server"; + m_rgbColor = QColor(225, 25, 99).rgb(); + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; +} + +QByteArray RigCtlServerSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeU32(1, m_rigCtlPort); + s.writeS32(2, m_maxFrequencyOffset); + s.writeS32(3, m_deviceIndex); + s.writeS32(4, m_channelIndex); + 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); + + return s.final(); +} + +bool RigCtlServerSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + qint32 tmp; + uint32_t utmp; + QString strtmp; + + d.readU32(2, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_rigCtlPort = utmp; + } else { + m_rigCtlPort = 4532; + } + + d.readS32(2, &m_maxFrequencyOffset, 10000); + d.readS32(3, &m_deviceIndex, -1); + d.readS32(4, &m_channelIndex, -1); + d.readString(5, &m_title, "RigCtl Server"); + d.readU32(6, &m_rgbColor, QColor(225, 25, 99).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; + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/feature/rigctlserver/rigctlserversettings.h b/plugins/feature/rigctlserver/rigctlserversettings.h new file mode 100644 index 000000000..9c8de1db3 --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserversettings.h @@ -0,0 +1,69 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RIGCTLSERVERSETTINGS_H_ +#define INCLUDE_FEATURE_RIGCTLSERVERSETTINGS_H_ + +#include +#include + +#include "util/message.h" + +class Serializable; + +struct RigCtlServerSettings +{ + class MsgChannelIndexChange : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getIndex() const { return m_index; } + + static MsgChannelIndexChange* create(int index) { + return new MsgChannelIndexChange(index); + } + + protected: + int m_index; + + MsgChannelIndexChange(int index) : + Message(), + m_index(index) + { } + }; + + bool m_enabled; + quint32 m_rigCtlPort; + int m_maxFrequencyOffset; + int m_deviceIndex; + int m_channelIndex; + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + + RigCtlServerSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif // INCLUDE_FEATURE_RIGCTLSERVERSETTINGS_H_ diff --git a/plugins/feature/rigctlserver/rigctlserverwebapiadapter.cpp b/plugins/feature/rigctlserver/rigctlserverwebapiadapter.cpp new file mode 100644 index 000000000..a24244aa6 --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserverwebapiadapter.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGFeatureSettings.h" +#include "rigctlserver.h" +#include "rigctlserverwebapiadapter.h" + +RigCtlServerWebAPIAdapter::RigCtlServerWebAPIAdapter() +{} + +RigCtlServerWebAPIAdapter::~RigCtlServerWebAPIAdapter() +{} + +int RigCtlServerWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings()); + response.getSimplePttSettings()->init(); + RigCtlServer::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int RigCtlServerWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + RigCtlServer::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/rigctlserver/rigctlserverwebapiadapter.h b/plugins/feature/rigctlserver/rigctlserverwebapiadapter.h new file mode 100644 index 000000000..5747f8ffa --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserverwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_RIGCTLSERVER_WEBAPIADAPTER_H +#define INCLUDE_RIGCTLSERVER_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "rigctlserversettings.h" + +/** + * Standalone API adapter only for the settings + */ +class RigCtlServerWebAPIAdapter : public FeatureWebAPIAdapter { +public: + RigCtlServerWebAPIAdapter(); + virtual ~RigCtlServerWebAPIAdapter(); + + 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: + RigCtlServerSettings m_settings; +}; + +#endif // INCLUDE_RIGCTLSERVER_WEBAPIADAPTER_H diff --git a/plugins/feature/rigctlserver/rigctlserverworker.cpp b/plugins/feature/rigctlserver/rigctlserverworker.cpp new file mode 100644 index 000000000..7cfb52019 --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserverworker.cpp @@ -0,0 +1,850 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "SWGDeviceState.h" +#include "SWGSuccessResponse.h" +#include "SWGErrorResponse.h" +#include "SWGDeviceSettings.h" +#include "SWGChannelSettings.h" +#include "SWGDeviceSet.h" + +#include "webapi/webapiadapterinterface.h" +#include "webapi/webapiutils.h" + +#include "rigctlserverworker.h" + +MESSAGE_CLASS_DEFINITION(RigCtlServerWorker::MsgConfigureRigCtlServerWorker, Message) + +const unsigned int RigCtlServerWorker::m_CmdLength = 1024; +const unsigned int RigCtlServerWorker::m_ResponseLength = 1024; +const struct RigCtlServerWorker::ModeDemod RigCtlServerWorker::m_modeMap[] = { + {"FM", "NFMDemod"}, + {"WFM", "WFMDemod"}, + {"AM", "AMDemod"}, + {"LSB", "SSBDemod"}, + {"USB", "SSBDemod"}, + {nullptr, nullptr} +}; + +RigCtlServerWorker::RigCtlServerWorker(WebAPIAdapterInterface *webAPIAdapterInterface) : + m_state(idle), + m_tcpServer(nullptr), + m_clientConnection(nullptr), + m_webAPIAdapterInterface(webAPIAdapterInterface), + m_msgQueueToFeature(nullptr), + m_running(false), + m_mutex(QMutex::Recursive) +{ + qDebug("RigCtlServerWorker::RigCtlServerWorker"); +} + +RigCtlServerWorker::~RigCtlServerWorker() +{ + m_inputMessageQueue.clear(); +} + +void RigCtlServerWorker::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); +} + +bool RigCtlServerWorker::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; + return m_running; +} + +void RigCtlServerWorker::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + restartServer(false, 0); + m_running = false; +} + +void RigCtlServerWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool RigCtlServerWorker::handleMessage(const Message& cmd) +{ + if (MsgConfigureRigCtlServerWorker::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureRigCtlServerWorker& cfg = (MsgConfigureRigCtlServerWorker&) cmd; + qDebug() << "RigCtlServerWorker::handleMessage: MsgConfigureRigCtlServerWorker"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else + { + return false; + } +} + +void RigCtlServerWorker::applySettings(const RigCtlServerSettings& settings, bool force) +{ + qDebug() << "RigCtlServerWorker::applySettings:" + << " m_title: " << settings.m_title + << " m_rgbColor: " << settings.m_rgbColor + << " m_enabled: " << settings.m_enabled + << " m_deviceIndex: " << settings.m_deviceIndex + << " m_channelIndex: " << settings.m_channelIndex + << " m_rigCtlPort: " << settings.m_rigCtlPort + << " m_maxFrequencyOffset: " << settings.m_maxFrequencyOffset + << " force: " << force; + + if ((settings.m_rigCtlPort != m_settings.m_rigCtlPort) || + (settings.m_enabled != m_settings.m_enabled) || force) + { + restartServer(settings.m_enabled, settings.m_rigCtlPort); + } + + m_settings = settings; +} + +void RigCtlServerWorker::restartServer(bool enabled, uint32_t port) +{ + if (m_tcpServer) + { + if (m_clientConnection) + { + m_clientConnection->close(); + delete m_clientConnection; + m_clientConnection = nullptr; + } + + disconnect(m_tcpServer, &QTcpServer::newConnection, this, &RigCtlServerWorker::acceptConnection); + m_tcpServer->close(); + delete m_tcpServer; + m_tcpServer = nullptr; + } + + if (enabled) + { + qDebug() << "RigCtlServerWorker::restartServer: server enabled on port " << port; + m_tcpServer = new QTcpServer(this); + + if (!m_tcpServer->listen(QHostAddress::Any, port)) { + qWarning("RigCtrl failed to listen on port %u. Check it is not already in use.", port); + } else { + connect(m_tcpServer, &QTcpServer::newConnection, this, &RigCtlServerWorker::acceptConnection); + } + } + else + { + qDebug() << "RigCtlServerWorker::restartServer: server disabled"; + } +} + +void RigCtlServerWorker::acceptConnection() +{ + QMutexLocker mutexLocker(&m_mutex); + m_clientConnection = m_tcpServer->nextPendingConnection(); + + if (!m_clientConnection) { + return; + } + + connect(m_clientConnection, &QIODevice::readyRead, this, &RigCtlServerWorker::getCommand); + connect(m_clientConnection, &QAbstractSocket::disconnected, m_clientConnection, &QObject::deleteLater); +} + +// Get rigctl command and start processing it +void RigCtlServerWorker::getCommand() +{ + QMutexLocker mutexLocker(&m_mutex); + char cmd[m_CmdLength]; + qint64 len; + rig_errcode_e rigCtlRC; + char response[m_ResponseLength]; + std::fill(response, response + m_ResponseLength, '\0'); + + // Get rigctld command from client + len = m_clientConnection->readLine(cmd, sizeof(cmd)); + + if (len != -1) + { + //qDebug() << "RigCtrl::getCommand - " << cmd; + if (!strncmp(cmd, "F ", 2) || !strncmp(cmd, "set_freq ", 9)) + { + // Set frequency + double targetFrequency = atof(cmd[0] == 'F' ? &cmd[2] : &cmd[9]); + setFrequency(targetFrequency, rigCtlRC); + sprintf(response, "RPRT %d\n", rigCtlRC); + } + else if (!strncmp(cmd, "f", 1) || !strncmp(cmd, "get_freq", 8)) + { + // Get frequency - need to add centerFrequency and inputFrequencyOffset + double frequency; + + if (getFrequency(frequency, rigCtlRC)) { + sprintf(response, "%u\n", (unsigned int) frequency); + } else { + sprintf(response, "RPRT %d\n", rigCtlRC); + } + } + else if (!strncmp(cmd, "M ?", 3) || !(strncmp(cmd, "set_mode ?", 10))) + { + // Return list of modes supported + char *p = response; + + for (int i = 0; m_modeMap[i].mode != nullptr; i++) { + p += sprintf(p, "%s ", m_modeMap[i].mode); + } + + p += sprintf(p, "\n"); + } + else if (!strncmp(cmd, "M ", 2) || !(strncmp(cmd, "set_mode ", 9))) + { + // Set mode + // Map rigctrl mode name to SDRangel modem name + const char *targetModem = nullptr; + const char *targetMode = nullptr; + int targetBW = -1; + char *p = (cmd[0] == 'M' ? &cmd[2] : &cmd[9]); + int i, l; + + for (i = 0; m_modeMap[i].mode != nullptr; i++) + { + l = strlen(m_modeMap[i].mode); + + if (!strncmp(p, m_modeMap[i].mode, l)) + { + targetMode = m_modeMap[i].mode; + targetModem = m_modeMap[i].modem; + p += l; + break; + } + } + + // Save bandwidth, if given + while(isspace(*p)) { + p++; + } + + if (isdigit(*p)) + { + targetBW = atoi(p); + } + + if (m_modeMap[i].modem != nullptr) + { + changeModem(targetMode, targetModem, targetBW, rigCtlRC); + sprintf(response, "RPRT %d\n", rigCtlRC); + } + else + { + sprintf(response, "RPRT %d\n", RIG_EINVAL); + m_clientConnection->write(response, strlen(response)); + } + } + else if (!strncmp(cmd, "m", 1) || !(strncmp(cmd, "get_mode", 8))) + { + // Get mode + const char *mode; + double passband; + + if (getMode(&mode, passband, rigCtlRC)) + { + if (passband < 0) { + sprintf(response, "%s\n", mode); + } else { + sprintf(response, "%s %u\n", mode, (unsigned int) passband); + } + } + else + { + sprintf(response, "RPRT %d\n", rigCtlRC); + } + } + else if (!strncmp(cmd, "set_powerstat 0", 15)) + { + // Power off radio + setPowerOff(rigCtlRC); + sprintf(response, "RPRT %d\n", rigCtlRC); + } + else if (!strncmp(cmd, "set_powerstat 1", 15)) + { + // Power on radio + setPowerOn(rigCtlRC); + sprintf(response, "RPRT %d\n", rigCtlRC); + } + else if (!strncmp(cmd, "get_powerstat", 13)) + { + // Return if powered on or off + bool power; + if (getPower(power, rigCtlRC)) + { + if (power) { + sprintf(response, "1\n"); + } else { + sprintf(response, "0\n"); + } + } + else + { + sprintf(response, "RPRT %d\n", rigCtlRC); + } + } + else + { + // Unimplemented command + sprintf(response, "RPRT %d\n", RIG_ENIMPL); + m_clientConnection->write(response, strlen(response)); + } + } + + m_clientConnection->write(response, strlen(response)); +} + +bool RigCtlServerWorker::setFrequency(double targetFrequency, rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGDeviceSettings deviceSettingsResponse; + SWGSDRangel::SWGErrorResponse errorResponse; + int httpRC; + + // Get current device center frequency + httpRC = m_webAPIAdapterInterface->devicesetDeviceSettingsGet( + m_settings.m_deviceIndex, + deviceSettingsResponse, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::setFrequency: get device frequency error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + QJsonObject *jsonObj = deviceSettingsResponse.asJsonObject(); + double freq; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "centerFrequency", freq)) + { + bool outOfRange = std::abs(freq - targetFrequency) > m_settings.m_maxFrequencyOffset; + + if (outOfRange) + { + // Update centerFrequency + WebAPIUtils::setSubObjectDouble(*jsonObj, "centerFrequency", targetFrequency); + QStringList deviceSettingsKeys; + deviceSettingsKeys.append("centerFrequency"); + deviceSettingsResponse.init(); + deviceSettingsResponse.fromJsonObject(*jsonObj); + SWGSDRangel::SWGErrorResponse errorResponse2; + + httpRC = m_webAPIAdapterInterface->devicesetDeviceSettingsPutPatch( + m_settings.m_deviceIndex, + false, // PATCH + deviceSettingsKeys, + deviceSettingsResponse, + errorResponse2 + ); + + if (httpRC/100 == 2) + { + qDebug("RigCtlServerWorker::setFrequency: set device frequency %f OK", targetFrequency); + } + else + { + qWarning("RigCtlServerWorker::setFrequency: set device frequency error %d: %s", + httpRC, qPrintable(*errorResponse2.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + } + + // Update inputFrequencyOffset (offet if in range else zero) + float targetOffset = outOfRange ? 0 : targetFrequency - freq; + SWGSDRangel::SWGChannelSettings channelSettingsResponse; + // Get channel settings containing inputFrequencyOffset, so we can patch it + httpRC = m_webAPIAdapterInterface->devicesetChannelSettingsGet( + m_settings.m_deviceIndex, + m_settings.m_channelIndex, + channelSettingsResponse, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::setFrequency: get channel offset frequency error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + jsonObj = channelSettingsResponse.asJsonObject(); + + if (WebAPIUtils::setSubObjectDouble(*jsonObj, "inputFrequencyOffset", targetOffset)) + { + QStringList channelSettingsKeys; + channelSettingsKeys.append("inputFrequencyOffset"); + channelSettingsResponse.init(); + channelSettingsResponse.fromJsonObject(*jsonObj); + + httpRC = m_webAPIAdapterInterface->devicesetChannelSettingsPutPatch( + m_settings.m_deviceIndex, + m_settings.m_channelIndex, + false, // PATCH + channelSettingsKeys, + channelSettingsResponse, + errorResponse + ); + + if (httpRC/100 == 2) + { + qDebug("RigCtlServerWorker::setFrequency: set channel offset frequency %f OK", targetOffset); + } + else + { + qWarning("RigCtlServerWorker::setFrequency: set channel frequency offset error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + } + else + { + qWarning("RigCtlServerWorker::setFrequency: No inputFrequencyOffset key in channel settings"); + rigCtlRC = RIG_ENIMPL; + return false; + } + } + else + { + qWarning("RigCtlServerWorker::setFrequency: no centerFrequency key in device settings"); + rigCtlRC = RIG_ENIMPL; + return false; + } + + rigCtlRC = RIG_OK; // return OK + return true; +} + +bool RigCtlServerWorker::getFrequency(double& frequency, rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGDeviceSettings deviceSettingsResponse; + SWGSDRangel::SWGErrorResponse errorResponse; + int httpRC; + + // Get current device center frequency + httpRC = m_webAPIAdapterInterface->devicesetDeviceSettingsGet( + m_settings.m_deviceIndex, + deviceSettingsResponse, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::getFrequency: get device frequency error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + QJsonObject *jsonObj = deviceSettingsResponse.asJsonObject(); + double deviceFreq; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "centerFrequency", deviceFreq)) + { + SWGSDRangel::SWGChannelSettings channelSettingsResponse; + // Get channel settings containg inputFrequencyOffset, so we can patch them + httpRC = m_webAPIAdapterInterface->devicesetChannelSettingsGet( + m_settings.m_deviceIndex, + m_settings.m_channelIndex, + channelSettingsResponse, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::setFrequency: get channel offset frequency error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + QJsonObject *jsonObj2 = channelSettingsResponse.asJsonObject(); + double channelOffset; + + if (WebAPIUtils::getSubObjectDouble(*jsonObj2, "inputFrequencyOffset", channelOffset)) + { + frequency = deviceFreq + channelOffset; + } + else + { + qWarning("RigCtlServerWorker::setFrequency: No inputFrequencyOffset key in channel settings"); + rigCtlRC = RIG_ENIMPL; + return false; + } + } + else + { + qWarning("RigCtlServerWorker::setFrequency: no centerFrequency key in device settings"); + rigCtlRC = RIG_ENIMPL; + return false; + } + + rigCtlRC = RIG_OK; + return true; +} + +bool RigCtlServerWorker::changeModem(const char *newMode, const char *newModemId, int newModemBw, rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGDeviceSet deviceSetResponse; + SWGSDRangel::SWGSuccessResponse successResponse; + SWGSDRangel::SWGErrorResponse errorResponse; + int httpRC; + int nbChannels; + int currentOffset; + + // get deviceset details to get the number of channel modems + httpRC = m_webAPIAdapterInterface->devicesetGet( + m_settings.m_deviceIndex, + deviceSetResponse, + errorResponse + ); + + if (httpRC/100 != 2) // error + { + qWarning("RigCtlServerWorker::changeModem: deevice set get information error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + if (!WebAPIUtils::getObjectInt(*deviceSetResponse.asJsonObject(), "channelcount", nbChannels)) + { + qWarning("RigCtlServerWorker::changeModem: no channelcount key in device set information"); + rigCtlRC = RIG_ENIMPL; + return false; + } + + QList channelObjects; + + if (!WebAPIUtils::getObjectObjects(*deviceSetResponse.asJsonObject(), "channels", channelObjects)) + { + qWarning("RigCtlServerWorker::changeModem: no channels key in device set information"); + rigCtlRC = RIG_ENIMPL; + return false; + } + + if (m_settings.m_channelIndex < channelObjects.size()) + { + const QJsonObject& channelInfo = channelObjects.at(m_settings.m_channelIndex); + + if (!WebAPIUtils::getObjectInt(channelInfo, "deltaFrequency", currentOffset)) + { + qWarning("RigCtlServerWorker::changeModem: no deltaFrequency key in device set channel information"); + rigCtlRC = RIG_ENIMPL; + return false; + } + } + else + { + qWarning("RigCtlServerWorker::changeModem: channel not found in device set channels information"); + rigCtlRC = RIG_ENIMPL; + return false; + } + + // delete current modem + httpRC = m_webAPIAdapterInterface->devicesetChannelDelete( + m_settings.m_deviceIndex, + m_settings.m_channelIndex, + successResponse, + errorResponse + ); + + if (httpRC/100 != 2) // error + { + qWarning("RigCtlServerWorker::changeModem: delete channel error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + // create new modem + SWGSDRangel::SWGChannelSettings query; + QString newModemIdStr(newModemId); + bool lsb = !strncmp(newMode, "LSB", 3); + query.init(); + query.setChannelType(new QString(newModemIdStr)); + query.setDirection(0); + + httpRC = m_webAPIAdapterInterface->devicesetChannelPost( + m_settings.m_deviceIndex, + query, + successResponse, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::changeModem: create channel error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + // wait for channel switchover + QEventLoop loop; + QTimer *timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), &loop, SLOT(quit())); + timer->start(200); + loop.exec(); + delete timer; + + // when a new channel is created it is put last in the list + qDebug("RigCtlServerWorker::changeModem: created %s at %d", newModemId, nbChannels-1); + + if (m_msgQueueToFeature) + { + RigCtlServerSettings::MsgChannelIndexChange *msg = RigCtlServerSettings::MsgChannelIndexChange::create(nbChannels-1); + m_msgQueueToFeature->push(msg); + } + + SWGSDRangel::SWGChannelSettings swgChannelSettings; + QStringList channelSettingsKeys; + channelSettingsKeys.append("inputFrequencyOffset"); + QString jsonSettingsStr = tr("\"inputFrequencyOffset\":%1").arg(currentOffset); + + if (!strncmp(newMode, "LSB", 3)) + { + int bw = newModemBw < 0 ? -3000 : -newModemBw; + } + + if (lsb || (newModemBw >= 0)) + { + int bw = lsb ? (newModemBw < 0 ? -3000 : -newModemBw) : newModemBw; + channelSettingsKeys.append("rfBandwidth"); + jsonSettingsStr.append(tr(",\"rfBandwidth\":%2").arg(bw)); + } + + QString jsonStr = tr("{ \"channelType\": \"%1\", \"%2Settings\": {%3}}") + .arg(QString(newModemId)) + .arg(QString(newModemId)) + .arg(jsonSettingsStr); + swgChannelSettings.fromJson(jsonStr); + + httpRC = m_webAPIAdapterInterface->devicesetChannelSettingsPutPatch( + m_settings.m_deviceIndex, + nbChannels-1, // new index + false, // PATCH + channelSettingsKeys, + swgChannelSettings, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::changeModem: set channel settings error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + rigCtlRC = RIG_OK; + return true; +} + +bool RigCtlServerWorker::getMode(const char **mode, double& passband, rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGChannelSettings channelSettingsResponse; + SWGSDRangel::SWGErrorResponse errorResponse; + int httpRC; + + // Get channel settings containg inputFrequencyOffset, so we can patch them + httpRC = m_webAPIAdapterInterface->devicesetChannelSettingsGet( + m_settings.m_deviceIndex, + m_settings.m_channelIndex, + channelSettingsResponse, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::getModem: get channel settings error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + QJsonObject *jsonObj = channelSettingsResponse.asJsonObject(); + QString channelType; + int i; + + if (!WebAPIUtils::getObjectString(*jsonObj, "channelType", channelType)) + { + qWarning("RigCtlServerWorker::getModem: no channelType key in channel settings"); + rigCtlRC = RIG_ENIMPL; + return false; + } + + for (i = 0; m_modeMap[i].mode != nullptr; i++) + { + if (!channelType.compare(m_modeMap[i].modem)) + { + *mode = m_modeMap[i].mode; + break; + } + } + + if (!m_modeMap[i].mode) + { + qWarning("RigCtlServerWorker::getModem: channel type not implemented: %s", qPrintable(channelType)); + rigCtlRC = RIG_ENIMPL; + return false; + } + + + if (WebAPIUtils::getSubObjectDouble(*jsonObj, "rfBandwidth", passband)) + { + if (!channelType.compare("SSBDemod")) + { + if (passband < 0) { // LSB + passband = -passband; + } else { // USB + *mode = m_modeMap[i+1].mode; + } + } + } + else + { + passband = -1; + } + + rigCtlRC = RIG_OK; + return true; +} + +bool RigCtlServerWorker::setPowerOn(rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGDeviceState response; + SWGSDRangel::SWGErrorResponse errorResponse; + + int httpRC = m_webAPIAdapterInterface->devicesetDeviceRunPost( + m_settings.m_deviceIndex, + response, + errorResponse + ); + + if (httpRC/100 == 2) + { + qDebug("RigCtlServerWorker::setPowerOn: set device start OK"); + rigCtlRC = RIG_OK; + return true; + } + else + { + qWarning("RigCtlServerWorker::setPowerOn: set device start error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } +} + +bool RigCtlServerWorker::setPowerOff(rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGDeviceState response; + SWGSDRangel::SWGErrorResponse errorResponse; + + int httpRC = m_webAPIAdapterInterface->devicesetDeviceRunDelete( + m_settings.m_deviceIndex, + response, + errorResponse + ); + + if (httpRC/100 == 2) + { + qDebug("RigCtlServerWorker::setPowerOff: set device stop OK"); + rigCtlRC = RIG_OK; + return true; + } + else + { + qWarning("RigCtlServerWorker::setPowerOff: set device stop error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } +} + +bool RigCtlServerWorker::getPower(bool& power, rig_errcode_e& rigCtlRC) +{ + SWGSDRangel::SWGDeviceState response; + SWGSDRangel::SWGErrorResponse errorResponse; + + int httpRC = m_webAPIAdapterInterface->devicesetDeviceRunGet( + m_settings.m_deviceIndex, + response, + errorResponse + ); + + if (httpRC/100 != 2) + { + qWarning("RigCtlServerWorker::getPower: get device run state error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + QJsonObject *jsonObj = response.asJsonObject(); + QString state; + + if (WebAPIUtils::getObjectString(*jsonObj, "state", state)) + { + qDebug("RigCtlServerWorker::getPower: run state is %s", qPrintable(state)); + + if (state.compare("running")) { + power = false; // not equal + } else { + power = true; // equal + } + } + else + { + qWarning("RigCtlServerWorker::getPower: get device run state error %d: %s", + httpRC, qPrintable(*errorResponse.getMessage())); + rigCtlRC = RIG_EINVAL; + return false; + } + + return true; +} diff --git a/plugins/feature/rigctlserver/rigctlserverworker.h b/plugins/feature/rigctlserver/rigctlserverworker.h new file mode 100644 index 000000000..bd57495ef --- /dev/null +++ b/plugins/feature/rigctlserver/rigctlserverworker.h @@ -0,0 +1,145 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_RIGCTLSERVERWORKER_H_ +#define INCLUDE_FEATURE_RIGCTLSERVERWORKER_H_ + +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" + +#include "rigctlserversettings.h" + +class WebAPIAdapterInterface; +class QTcpServer; +class QTcpSocket; + +class RigCtlServerWorker : public QObject +{ + Q_OBJECT +public: + class MsgConfigureRigCtlServerWorker : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RigCtlServerSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRigCtlServerWorker* create(const RigCtlServerSettings& settings, bool force) + { + return new MsgConfigureRigCtlServerWorker(settings, force); + } + + private: + RigCtlServerSettings m_settings; + bool m_force; + + MsgConfigureRigCtlServerWorker(const RigCtlServerSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + struct ModeDemod { + const char *mode; + const char *modem; + }; + + RigCtlServerWorker(WebAPIAdapterInterface *webAPIAdapterInterface); + ~RigCtlServerWorker(); + void reset(); + bool startWork(); + void stopWork(); + bool isRunning() const { return m_running; } + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; } + +private: + enum RigCtrlState + { + idle, + set_freq, set_freq_no_offset, set_freq_center, set_freq_center_no_offset, set_freq_set_offset, set_freq_offset, + get_freq_center, get_freq_offset, + set_mode_mod, set_mode_settings, set_mode_reply, + get_power, + set_power_on, set_power_off + }; + + // Hamlib rigctrl error codes + enum rig_errcode_e { + RIG_OK = 0, /*!< No error, operation completed successfully */ + RIG_EINVAL = -1, /*!< invalid parameter */ + RIG_ECONF = -2, /*!< invalid configuration (serial,..) */ + RIG_ENOMEM = -3, /*!< memory shortage */ + RIG_ENIMPL = -4, /*!< function not implemented, but will be */ + RIG_ETIMEOUT = -5, /*!< communication timed out */ + RIG_EIO = -6, /*!< IO error, including open failed */ + RIG_EINTERNAL = -7, /*!< Internal Hamlib error, huh! */ + RIG_EPROTO = -8, /*!< Protocol error */ + RIG_ERJCTED = -9, /*!< Command rejected by the rig */ + RIG_ETRUNC = -10, /*!< Command performed, but arg truncated */ + RIG_ENAVAIL = -11, /*!< function not available */ + RIG_ENTARGET = -12, /*!< VFO not targetable */ + RIG_BUSERROR = -13, /*!< Error talking on the bus */ + RIG_BUSBUSY = -14, /*!< Collision on the bus */ + RIG_EARG = -15, /*!< NULL RIG handle or any invalid pointer parameter in get arg */ + RIG_EVFO = -16, /*!< Invalid VFO */ + RIG_EDOM = -17 /*!< Argument out of domain of func */ + }; + + RigCtrlState m_state; + double m_targetFrequency; + double m_targetOffset; + QString m_targetModem; + int m_targetBW; + + QTcpServer *m_tcpServer; + QTcpSocket *m_clientConnection; + + WebAPIAdapterInterface *m_webAPIAdapterInterface; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + MessageQueue *m_msgQueueToFeature; //!< Queue to report channel change to main feature object + RigCtlServerSettings m_settings; + bool m_running; + QMutex m_mutex; + + static const unsigned int m_CmdLength; + static const unsigned int m_ResponseLength; + static const ModeDemod m_modeMap[]; + + bool handleMessage(const Message& cmd); + void applySettings(const RigCtlServerSettings& settings, bool force = false); + void restartServer(bool enabled, uint32_t port); + bool setFrequency(double frequency, rig_errcode_e& rigCtlRC); + bool getFrequency(double& frequency, rig_errcode_e& rigCtlRC); + bool changeModem(const char *newMode, const char *newModemId, int newModemBw, rig_errcode_e& rigCtlRC); + bool getMode(const char **mode, double& frequency, rig_errcode_e& rigCtlRC); + bool setPowerOn(rig_errcode_e& rigCtlRC); + bool setPowerOff(rig_errcode_e& rigCtlRC); + bool getPower(bool& power, rig_errcode_e& rigCtlRC); + +private slots: + void handleInputMessages(); + void acceptConnection(); + void getCommand(); +}; + +#endif // INCLUDE_FEATURE_RIGCTLSERVERWORKER_H_