diff --git a/CMakeLists.txt b/CMakeLists.txt index 461e74c7b..bd47c9989 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,7 @@ option(ENABLE_FEATURE_STARTRACKER "Enable feature startracker plugin" ON) option(ENABLE_FEATURE_RIGCTLSERVER "Enable feature rigctlserver plugin" ON) option(ENABLE_FEATURE_PERTESTER "Enable feature pertester plugin" ON) option(ENABLE_FEATURE_GS232CONTROLLER "Enable feature gs232controller plugin" ON) +option(ENABLE_FEATURE_REMOTECONTROL "Enable feature remote control plugin" ON) # on windows always build external libraries if(WIN32) diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 19835dc5c..31cf277e7 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -73,3 +73,7 @@ endif() if (ENABLE_FEATURE_AMBE AND LIBSERIALDV_FOUND) add_subdirectory(ambe) endif() + +if (ENABLE_FEATURE_REMOTECONTROL) + add_subdirectory(remotecontrol) +endif() diff --git a/plugins/feature/remotecontrol/CMakeLists.txt b/plugins/feature/remotecontrol/CMakeLists.txt new file mode 100644 index 000000000..079e2844b --- /dev/null +++ b/plugins/feature/remotecontrol/CMakeLists.txt @@ -0,0 +1,71 @@ +project(emotecontrol) + +set(remotecontrol_SOURCES + remotecontrol.cpp + remotecontrolsettings.cpp + remotecontrolplugin.cpp + remotecontrolworker.cpp +) + +set(remotecontrol_HEADERS + remotecontrol.h + remotecontrolsettings.h + remotecontrolplugin.h + remotecontrolworker.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(remotecontrol_SOURCES + ${remotecontrol_SOURCES} + remotecontrolgui.cpp + remotecontrolgui.ui + remotecontrolsettingsdialog.cpp + remotecontrolsettingsdialog.ui + remotecontroldevicedialog.cpp + remotecontroldevicedialog.ui + remotecontrolvisasensordialog.cpp + remotecontrolvisasensordialog.ui + remotecontrolvisacontroldialog.cpp + remotecontrolvisacontroldialog.ui + ) + set(remotecontrol_HEADERS + ${remotecontrol_HEADERS} + remotecontrolgui.h + remotecontrolsettingsdialog.h + remotecontroldevicedialog.h + remotecontrolvisasensordialog.h + remotecontrolvisacontroldialog.h + ) + + set(TARGET_NAME featureremotecontrol) + set(TARGET_LIB Qt5::Widgets Qt5::Charts) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featureremotecontrolsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${remotecontrol_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::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/remotecontrol/readme.md b/plugins/feature/remotecontrol/readme.md new file mode 100644 index 000000000..b19ecd6cd --- /dev/null +++ b/plugins/feature/remotecontrol/readme.md @@ -0,0 +1,153 @@ +

Remote Control Feature Plugin

+ +

Introduction

+ +The Remote Control Feature can be used to control and view the status of Smart Home / IoT devices, +such as plugs and switches, and test equipment that supports the VISA API, such as benchtop power +supplies, multimeters and spectrum analyzers. + +In a SDR context, this can be useful for remotely controlling and monitoring power to SDRs, power +amplifiers and rotator controllers. Or it can be used to make and display signal power measurements +from a spectrum analyzer, in SDRangel. + +The Remote Control feature can interface to devices via the following APIs: + +* Home Assistant (https://www.home-assistant.io/) +* TP-Link's Kasa (https://www.tp-link.com/uk/home-networking/smart-plug/) +* VISA (https://www.ivifoundation.org/specifications/default.aspx) + +A user-configurable GUI is supported, that allows customization of which controls and sensors are displayed for each device. +Sensor values can be plotted versus time on charts. + +![Remote Control feature plugin GUI](../../../doc/img/RemoteControl_plugin.png) + +

Interface

+ +![Remote Control feature settings](../../../doc/img/RemoteControl_plugin_settings.png) + +

1: Start/Stop

+ +Starts or stops periodic reading of the state of all devices. The update period can be set in the Settings dialog (3). + +

2: Update State

+ +Press to manually read the state of all devices. This can be used regardless of whether the plugin is started or stopped (1). + +

3: Display Settings Dialog

+ +Pressing this button opens the Settings dialog. + +

4: Clear Chart Data

+ +Pressing this button will clear all data from all charts. + +

5: Device GUIs

+ +GUIs for the enabled controls and sensors within a device will be displayed below the settings. +If a device is not available, it will be greyed out. If an error occurs when getting the state for a specific +control or sensor, or an out of range value is received, the background of the corresponding widget will turn red. + +

Settings Dialog

+ +

Devices Tab

+ +The Devices tab displays a list of devices that have been added to this Remote Control. + +![Remote Control devices tab](../../../doc/img/RemoteControl_plugin_devices_tab.png) + +* Press Add... to add a new device. +* Press Remove to remove the selected device. +* Press Edit... to edit settings for the selected device. +* The up and down arrows move the selected device up or down in the list. +The order of devices in the list determines the display order of the device's controls and senors in the Remote Control's GUI. + +

Settings Tab

+ +![Remote Control settings tab](../../../doc/img/RemoteControl_plugin_settings_tab.png) + +

TP-Link Settings

+ +The TP-Link fields must be completed in order to discover TP-Link Kasa Smart Plugs using TP-Link's protocol. + +Enter the e-mail address and password used for the TPLink Kasa Smart Home app. + +

Home Assistant Settings

+ +The Home Assistant fields must be completed in order to discover devices connected to Home Assistant. + +* Access token - Access token required to use the Home Assistant API. Access tokens can be created on the user profile page, typically at: http://homeassistant.local:8123/profile +* Host - The hostname or IP address and port number of the computer running the Home Assistant server. This is typically http://homeassistant.local:8123 + +

VISA Settings

+ +* Resource filter - A regular expression of VISA resources not to attempt to open. This can be used to speed up VISA device discovery. As an example, devices using TCP and serial connections can be filted with: ^(TCPIP|ASRL). Leave the field empty to try to connect to all VISA devices. +* Log I/O - Check to log VISA commands and responses to the SDRangel log file. + +

Devices Settings

+ +* Update period - Period in seconds between automatic updates of device control and sensor state. + +

Chart Settings

+ +* Height - Specifies whether charts are a fixed height (Fixed), or can be expanded vertically (Expanding). This setting also determines where the 'Stack sub windows' button will place the GUI. +* Height (pixels) - When 'Height' is 'Fixed', this specifies the height in pixels. + +

Device Dialog

+ +The Device Dialog allows selecting devices to add to the Remote Control, as well as customing what controls and sensors are displayed for the device in the GUI. + +![Device dialog](../../../doc/img/RemoteControl_plugin_device.png) + +When the dialog first appears when adding a new device, all fields will be disabled except for Protocol. You should first select a protocol in order to discover all devices that are currently +available via the selected protocol. The available devices will be added to the Device field. Select from this field the device you wish to add. + +The device name is the name assigned by the selected protocol. If you wish to use a different label for the device in the GUI, this can be entered in the Label field. + +The Controls and Sensors tables allow you to customize which are contols and sensors are visible in the GUI, via the checkbox in the Enable column. + +The Left Label and Right Label fields hold the text that will be displayed either side of the control or sensor in the GUI. +The Left Label is initialised with the device name and the Right Label is initialised withthe units. +These fields can be changed by double clicking in the cell. + +Checking the Plot column will result in a chart being drawn that plots sensor data verses time. +All enabled sensors for a device will be plotted on the same chart. +The Y Axis field below the table determines whether each series will have it's own Y axis (Per-sensor) or whether a single Y axis will be used for all series (Common). + +The Layout fields control how the Controls and Sensors will be laid-out in the GUI. This can be set to be either Horizontally or Vertically. + +When the Protocol is set to VISA, additional buttons will be displayed under the tables that allow controls and sensors to be added or removed, as unlike when selecting +TP-Link and Home Assitant devices, these are not automatically defined. + +

VISA Control Dialog

+ +The VISA Control Dialog allows the specification of a control for a VISA device. Both the GUI element and the SCPI commands to set and get the state must be specified: + +![Device dialog](../../../doc/img/RemoteControl_plugin_visa_control.png) + +* Name - A name for the control. E.g. Voltage for a voltage control on a power supply. This field is used as the default value for the Left Label in the GUI. +* ID - A unique identifier for the control. This must be unique between all controls and sensors in a device. +* Type - The data type of the state being controlled. This can be: + * Boolean - For on/off controls. A toggle button is used in the GUI. + * Integer - For integers. Minimum and maximum limits can be specified. A spin box is used in the GUI. + * Float - For real numbers. Minimum and maximum limits can be specified, as well as the precision (number of decimals). The GUI can use either a spin box, dial or slider. The Scale field specifies a scale factor that is applied to the value from the GUI that is sent to the device. E.g. If you wish to have a value displayed in MHz, but the value in the SCPI command should be in Hz, then the Scale field should be set to 1000000. + * String - For a text string. + * List - For a list of text strings, selectable from a ComboBox in the GUI. + * Button - For a button that executes a specific command, but does not have any state to be displayed. E.g. for a Reset button that executes *RST. +* Units - The units of the control, if applicable. E.g V or Volts for a voltage control. This field is used as the default value for the Right Label in the GUI. +* Set state - SCPI commands that set the state of the setting in the device. The value of the control in the GUI can be substituted in to the command by using %d for boolean and integer, %f for float and %s for strings. +* Get state - SCPI commands that get the state of the setting in the device. This is used to update the control in the GUI. + +

VISA Sensor Dialog

+ +The VISA Sensor Dialog allows the specification of a sensor for a VISA device. Both the GUI element and the SCPI commands to get the state must be specified: + +![Device dialog](../../../doc/img/RemoteControl_plugin_visa_sensor.png) + +* Name - A name for the sensor. E.g. Current for a current measurement from a power supply. This field is used as the default value for the Left Label in the GUI. +* ID - A unique identifier for the sensor. This must be unique between all controls and sensors in a device. +* Type - The data type of the sensor. This can be: + * Boolean - For on/off, true/false and 1/0 values. + * Float - For real numbers. + * String - For text strings. +* Units - The units of the sensor, if applicable. E.g A or Amps for a current sensor. This field is used as the default value for the Right Label in the GUI and also for the Chart Y-axis label. +* Get state - SCPI commands that get the state of the sensor from the device. diff --git a/plugins/feature/remotecontrol/remotecontrol.cpp b/plugins/feature/remotecontrol/remotecontrol.cpp new file mode 100644 index 000000000..f16d33ec2 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrol.cpp @@ -0,0 +1,150 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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/featureset.h" +#include "settings/serializable.h" + +#include "remotecontrol.h" +#include "remotecontrolworker.h" + +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgStartStop, Message) +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgConfigureRemoteControl, Message) +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgDeviceGetState, Message) +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgDeviceSetState, Message) +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgDeviceStatus, Message) +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgDeviceError, Message) +MESSAGE_CLASS_DEFINITION(RemoteControl::MsgDeviceUnavailable, Message) + +const char* const RemoteControl::m_featureIdURI = "sdrangel.feature.remotecontrol"; +const char* const RemoteControl::m_featureId = "RemoteControl"; + +RemoteControl::RemoteControl(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface) +{ + qDebug("RemoteControl::RemoteControl: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_state = StIdle; + m_errorMessage = "RemoteControl error"; + start(); +} + +RemoteControl::~RemoteControl() +{ + stop(); +} + +void RemoteControl::start() +{ + qDebug() << "RemoteControl::start"; + + m_thread = new QThread(); + m_worker = new RemoteControlWorker(); + m_worker->moveToThread(m_thread); + + QObject::connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater); + QObject::connect(m_thread, &QThread::finished, m_thread, &QObject::deleteLater); + + m_worker->setMessageQueueToFeature(getInputMessageQueue()); + m_state = StRunning; + m_thread->start(); +} + +void RemoteControl::stop() +{ + qDebug() << "RemoteControl::stop"; + m_state = StIdle; + m_thread->quit(); + m_thread->wait(); +} + +bool RemoteControl::handleMessage(const Message& cmd) +{ + if (MsgConfigureRemoteControl::match(cmd)) + { + MsgConfigureRemoteControl& cfg = (MsgConfigureRemoteControl&) cmd; + applySettings(cfg.getSettings(), cfg.getForce()); + // Ensure GUI message queue is set. setMessageQueueToGUI() isn't virtual, so can't hook in there. + m_worker->setMessageQueueToGUI(getMessageQueueToGUI()); + // Forward to worker + MsgConfigureRemoteControl *msgToWorker = new MsgConfigureRemoteControl(cfg); + m_worker->getInputMessageQueue()->push(msgToWorker); + return true; + } + else if (MsgStartStop::match(cmd)) + { + MsgStartStop& cfg = (MsgStartStop&) cmd; + // Unlike most other plugins, our worker is always running. + // Start/stop is used just to control automatic updating of device state + // Forward to worker + MsgStartStop *msgToWorker = new MsgStartStop(cfg); + m_worker->getInputMessageQueue()->push(msgToWorker); + return true; + } + else if (MsgDeviceGetState::match(cmd)) + { + MsgDeviceGetState& get = (MsgDeviceGetState&)cmd; + // Forward to worker + MsgDeviceGetState *msgToWorker = new MsgDeviceGetState(get); + m_worker->getInputMessageQueue()->push(msgToWorker); + return true; + } + else if (MsgDeviceSetState::match(cmd)) + { + MsgDeviceSetState& set = (MsgDeviceSetState&)cmd; + // Forward to worker + MsgDeviceSetState *msgToWorker = new MsgDeviceSetState(set); + m_worker->getInputMessageQueue()->push(msgToWorker); + return true; + } + else + { + return false; + } +} + +QByteArray RemoteControl::serialize() const +{ + return m_settings.serialize(); +} + +bool RemoteControl::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureRemoteControl *msg = MsgConfigureRemoteControl::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureRemoteControl *msg = MsgConfigureRemoteControl::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void RemoteControl::applySettings(const RemoteControlSettings& settings, bool force) +{ + qDebug() << "RemoteControl::applySettings:" + << " force: " << force; + + m_settings = settings; +} diff --git a/plugins/feature/remotecontrol/remotecontrol.h b/plugins/feature/remotecontrol/remotecontrol.h new file mode 100644 index 000000000..e8de25845 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrol.h @@ -0,0 +1,215 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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_REMOTECONTROL_H_ +#define INCLUDE_FEATURE_REMOTECONTROL_H_ + +#include "feature/feature.h" +#include "util/message.h" + +#include "remotecontrolsettings.h" + +class WebAPIAdapterInterface; +class RemoteControlWorker; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class RemoteControl : public Feature +{ + Q_OBJECT +public: + 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 MsgConfigureRemoteControl : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const RemoteControlSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureRemoteControl* create(const RemoteControlSettings& settings, bool force) { + return new MsgConfigureRemoteControl(settings, force); + } + + private: + RemoteControlSettings m_settings; + bool m_force; + + MsgConfigureRemoteControl(const RemoteControlSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgDeviceGetState : public Message { + MESSAGE_CLASS_DECLARATION + + public: + + static MsgDeviceGetState* create() { + return new MsgDeviceGetState(); + } + + protected: + + MsgDeviceGetState() : + Message() + { } + }; + + class MsgDeviceSetState : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getProtocol() const { return m_protocol; } + QString getDeviceId() const { return m_deviceId; } + QString getId() const { return m_id; } + QVariant getValue() const { return m_value; } + + static MsgDeviceSetState* create(const QString &protocol, const QString &deviceId, const QString &id, QVariant value) { + return new MsgDeviceSetState(protocol, deviceId, id, value); + } + + protected: + QString m_protocol; + QString m_deviceId; + QString m_id; + QVariant m_value; + + MsgDeviceSetState(const QString &protocol, const QString &deviceId, const QString &id, QVariant value) : + Message(), + m_protocol(protocol), + m_deviceId(deviceId), + m_id(id), + m_value(value) + { } + }; + + class MsgDeviceStatus : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getProtocol() const { return m_protocol; } + QString getDeviceId() const { return m_deviceId; } + QHash getStatus() const { return m_status; } + + static MsgDeviceStatus* create(const QString &protocol, const QString &deviceId, const QHash status) { + return new MsgDeviceStatus(protocol, deviceId, status); + } + + protected: + QString m_protocol; + QString m_deviceId; + QHash m_status; + + MsgDeviceStatus(const QString &protocol, const QString &deviceId, const QHash status) : + Message(), + m_protocol(protocol), + m_deviceId(deviceId), + m_status(status) + { } + }; + + class MsgDeviceError : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getErrorMessage() const { return m_errorMessage; } + + static MsgDeviceError* create(const QString &errorMessage) { + return new MsgDeviceError(errorMessage); + } + + protected: + QString m_errorMessage; + + MsgDeviceError(const QString &errorMessage) : + Message(), + m_errorMessage(errorMessage) + { } + }; + + class MsgDeviceUnavailable : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getProtocol() const { return m_protocol; } + QString getDeviceId() const { return m_deviceId; } + + static MsgDeviceUnavailable* create(const QString &protocol, const QString &deviceId) { + return new MsgDeviceUnavailable(protocol, deviceId); + } + + protected: + QString m_protocol; + QString m_deviceId; + + MsgDeviceUnavailable(const QString &protocol, const QString &deviceId) : + Message(), + m_protocol(protocol), + m_deviceId(deviceId) + { } + }; + + RemoteControl(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~RemoteControl(); + 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); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + QThread *m_thread; + RemoteControlWorker *m_worker; + RemoteControlSettings m_settings; + + void start(); + void stop(); + void applySettings(const RemoteControlSettings& settings, bool force = false); + +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROL_H_ diff --git a/plugins/feature/remotecontrol/remotecontroldevicedialog.cpp b/plugins/feature/remotecontrol/remotecontroldevicedialog.cpp new file mode 100644 index 000000000..ee737ba51 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontroldevicedialog.cpp @@ -0,0 +1,620 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "remotecontroldevicedialog.h" +#include "remotecontrolvisasensordialog.h" +#include "remotecontrolvisacontroldialog.h" + +#include +#include +#include + +RemoteControlDeviceDialog::RemoteControlDeviceDialog(RemoteControlSettings *settings, RemoteControlDevice *rcDevice, QWidget* parent) : + QDialog(parent), + ui(new Ui::RemoteControlDeviceDialog), + m_settings(settings), + m_rcDevice(rcDevice), + m_discoverer(nullptr), + m_setDeviceWhenAvailable(false) +{ + ui->setupUi(this); + connect(ui->controls->selectionModel(), &QItemSelectionModel::selectionChanged, this, &RemoteControlDeviceDialog::controlSelectionChanged); + connect(ui->sensors->selectionModel(), &QItemSelectionModel::selectionChanged, this, &RemoteControlDeviceDialog::sensorSelectionChanged); + enableWidgets(); + resizeTables(); + if (!m_rcDevice->m_info.m_id.isEmpty()) + { + ui->controlsLayout->setCurrentIndex((int)m_rcDevice->m_verticalControls); + ui->sensorsLayout->setCurrentIndex((int)m_rcDevice->m_verticalSensors); + ui->yAxis->setCurrentIndex((int)m_rcDevice->m_commonYAxis); + m_setDeviceWhenAvailable = true; + // Set protocol last, as that triggers discovery + ui->protocol->setCurrentText(m_rcDevice->m_protocol); + } +} + +RemoteControlDeviceDialog::~RemoteControlDeviceDialog() +{ + delete ui; + delete m_discoverer; +} + +void RemoteControlDeviceDialog::resizeTables() +{ + // Fill table with a row of dummy data that will size the columns nicely + int row = ui->controls->rowCount(); + ui->controls->setRowCount(row + 1); + ui->controls->setItem(row, COL_ENABLE, new QTableWidgetItem("C")); + ui->controls->setItem(row, COL_UNITS, new QTableWidgetItem("Units")); + ui->controls->setItem(row, COL_NAME, new QTableWidgetItem("A reasonably long control name")); + ui->controls->setItem(row, COL_ID, new QTableWidgetItem("An identifier")); + ui->controls->setItem(row, COL_LABEL_LEFT, new QTableWidgetItem("A reasonably long control name")); + ui->controls->setItem(row, COL_LABEL_RIGHT, new QTableWidgetItem("Units")); + ui->controls->resizeColumnsToContents(); + ui->controls->removeRow(row); + row = ui->sensors->rowCount(); + ui->sensors->setRowCount(row + 1); + ui->sensors->setItem(row, COL_ENABLE, new QTableWidgetItem("C")); + ui->sensors->setItem(row, COL_NAME, new QTableWidgetItem("A reasonably long sensor name")); + ui->sensors->setItem(row, COL_UNITS, new QTableWidgetItem("Units")); + ui->sensors->setItem(row, COL_ID, new QTableWidgetItem("An identifier")); + ui->sensors->setItem(row, COL_LABEL_LEFT, new QTableWidgetItem("A reasonably long sensor name")); + ui->sensors->setItem(row, COL_LABEL_RIGHT, new QTableWidgetItem("Units")); + ui->sensors->setItem(row, COL_FORMAT, new QTableWidgetItem("Format")); + ui->sensors->setItem(row, COL_PLOT, new QTableWidgetItem("C")); + ui->sensors->resizeColumnsToContents(); + ui->sensors->removeRow(row); +} + +void RemoteControlDeviceDialog::accept() +{ + QDialog::accept(); + if ((ui->protocol->currentIndex() > 0) && (!ui->device->currentText().isEmpty())) + { + int deviceIndex = ui->device->currentIndex(); + m_rcDevice->m_protocol = ui->protocol->currentText(); + m_rcDevice->m_label = ui->label->text(); + m_rcDevice->m_verticalControls = ui->controlsLayout->currentIndex() == 1; + m_rcDevice->m_verticalSensors = ui->sensorsLayout->currentIndex() == 1; + m_rcDevice->m_commonYAxis = ui->yAxis->currentIndex() == 1; + m_rcDevice->m_info = m_deviceInfo[deviceIndex]; + m_rcDevice->m_controls.clear(); + for (int row = 0; row < ui->controls->rowCount(); row++) + { + if (ui->controls->item(row, COL_ENABLE)->checkState() == Qt::Checked) + { + RemoteControlControl control; + control.m_id = ui->controls->item(row, COL_ID)->text(); + control.m_labelLeft = ui->controls->item(row, COL_LABEL_LEFT)->text(); + control.m_labelRight = ui->controls->item(row, COL_LABEL_RIGHT)->text(); + m_rcDevice->m_controls.append(control); + } + } + m_rcDevice->m_sensors.clear(); + for (int row = 0; row < ui->sensors->rowCount(); row++) + { + if (ui->sensors->item(row, COL_ENABLE)->checkState() == Qt::Checked) + { + RemoteControlSensor sensor; + sensor.m_id = ui->sensors->item(row, COL_ID)->text(); + sensor.m_labelLeft = ui->sensors->item(row, COL_LABEL_LEFT)->text(); + sensor.m_labelRight = ui->sensors->item(row, COL_LABEL_RIGHT)->text(); + sensor.m_format = ui->sensors->item(row, COL_FORMAT)->text(); + sensor.m_plot = ui->sensors->item(row, COL_PLOT)->checkState() == Qt::Checked; + m_rcDevice->m_sensors.append(sensor); + } + } + } +} + +void RemoteControlDeviceDialog::enableWidgets() +{ + bool allEnabled = false; + bool visible = false; + bool editControlsEnabled = false; + bool editSensorsEnabled = false; + + QString protocol = ui->protocol->currentText(); + if (protocol != "Select a protocol...") { + allEnabled = true; + } + if (protocol == "VISA") + { + visible = true; + editControlsEnabled = ui->controls->selectedItems().size() > 0; + editSensorsEnabled = ui->sensors->selectedItems().size() > 0; + } + + ui->device->setEnabled(allEnabled); + ui->deviceLabel->setEnabled(allEnabled); + ui->label->setEnabled(allEnabled); + ui->labelLabel->setEnabled(allEnabled); + ui->model->setEnabled(allEnabled); + ui->modelLabel->setEnabled(allEnabled); + ui->controlsGroup->setEnabled(allEnabled); + ui->sensorsGroup->setEnabled(allEnabled); + + ui->controlAdd->setVisible(visible); + ui->controlRemove->setVisible(visible); + ui->controlEdit->setVisible(visible); + ui->controlRemove->setEnabled(editControlsEnabled); + ui->controlEdit->setEnabled(editControlsEnabled); + ui->sensorAdd->setVisible(visible); + ui->sensorRemove->setVisible(visible); + ui->sensorEdit->setVisible(visible); + ui->sensorRemove->setEnabled(editSensorsEnabled); + ui->sensorEdit->setEnabled(editSensorsEnabled); +} + +void RemoteControlDeviceDialog::controlSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + bool arrowsEnabled = (selected.indexes().size() > 0); + bool editEnabled = arrowsEnabled && (ui->protocol->currentText() == "VISA"); + + ui->controlRemove->setEnabled(editEnabled); + ui->controlEdit->setEnabled(editEnabled); + ui->controlUp->setEnabled(arrowsEnabled); + ui->controlDown->setEnabled(arrowsEnabled); +} + +void RemoteControlDeviceDialog::sensorSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + bool arrowsEnabled = (selected.indexes().size() > 0); + bool editEnabled = arrowsEnabled && (ui->protocol->currentText() == "VISA"); + + ui->sensorRemove->setEnabled(editEnabled); + ui->sensorEdit->setEnabled(editEnabled); + ui->sensorUp->setEnabled(arrowsEnabled); + ui->sensorDown->setEnabled(arrowsEnabled); +} + +void RemoteControlDeviceDialog::on_protocol_currentTextChanged(const QString &protocol) +{ + QHash settings; + + // Clear current values in all widgets + ui->device->setCurrentIndex(-1); + + if (protocol != "Select a protocol...") + { + if (protocol == "TPLink") + { + settings.insert("username", m_settings->m_tpLinkUsername); + settings.insert("password", m_settings->m_tpLinkPassword); + } + else if (protocol == "HomeAssistant") + { + settings.insert("apiKey", m_settings->m_homeAssistantToken); + settings.insert("url", m_settings->m_homeAssistantHost); + } + else if (protocol == "VISA") + { + settings.insert("resourceFilter", m_settings->m_visaResourceFilter); + } + + delete m_discoverer; + m_discoverer = DeviceDiscoverer::getDiscoverer(settings, protocol); + if (m_discoverer) + { + connect(m_discoverer, &DeviceDiscoverer::deviceList, this, &RemoteControlDeviceDialog::deviceList); + connect(m_discoverer, &DeviceDiscoverer::error, this, &RemoteControlDeviceDialog::deviceError); + m_discoverer->getDevices(); + } + else + { + QMessageBox::critical(this, "Remote Control Error", QString("Failed to discover %1 devices").arg(protocol)); + } + } + enableWidgets(); +} + +int RemoteControlDeviceDialog::addControlRow(const QString &name, const QString &id, const QString &units) +{ + QTableWidgetItem *item; + int row = ui->controls->rowCount(); + ui->controls->setRowCount(row + 1); + + item = new QTableWidgetItem(); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + item->setCheckState(Qt::Checked); + ui->controls->setItem(row, COL_ENABLE, item); + + item = new QTableWidgetItem(name); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->controls->setItem(row, COL_NAME, item); + + item = new QTableWidgetItem(units); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->controls->setItem(row, COL_UNITS, item); + + item = new QTableWidgetItem(id); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->controls->setItem(row, COL_ID, item); + + item = new QTableWidgetItem(name); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->controls->setItem(row, COL_LABEL_LEFT, item); + + item = new QTableWidgetItem(units); + item->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->controls->setItem(row, COL_LABEL_RIGHT, item); + + return row; +} + +int RemoteControlDeviceDialog::addSensorRow(const QString &name, const QString &id, const QString &units) +{ + QTableWidgetItem *item; + int row = ui->sensors->rowCount(); + ui->sensors->setRowCount(row + 1); + + item = new QTableWidgetItem(); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + item->setCheckState(Qt::Checked); + ui->sensors->setItem(row, COL_ENABLE, item); + + item = new QTableWidgetItem(name); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->sensors->setItem(row, COL_NAME, item); + + item = new QTableWidgetItem(units); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->sensors->setItem(row, COL_UNITS, item); + + item = new QTableWidgetItem(id); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->sensors->setItem(row, COL_ID, item); + + item = new QTableWidgetItem(name); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled); + ui->sensors->setItem(row, COL_LABEL_LEFT, item); + + item = new QTableWidgetItem(units); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled); + ui->sensors->setItem(row, COL_LABEL_RIGHT, item); + + item = new QTableWidgetItem(); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled); + ui->sensors->setItem(row, COL_FORMAT, item); + + item = new QTableWidgetItem(); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + item->setCheckState(Qt::Unchecked); + ui->sensors->setItem(row, COL_PLOT, item); + + return row; +} + +void RemoteControlDeviceDialog::on_device_currentIndexChanged(int index) +{ + ui->model->setText(""); + ui->label->setText(""); + ui->controls->setRowCount(0); + ui->sensors->setRowCount(0); + if ((index < m_deviceInfo.size()) && (index >= 0)) + { + DeviceDiscoverer::DeviceInfo *deviceInfo = &m_deviceInfo[index]; + + ui->model->setText(deviceInfo->m_model); + if (m_rcDevice->m_info.m_id == deviceInfo->m_id) { + ui->label->setText(m_rcDevice->m_label); + } else { + ui->label->setText(deviceInfo->m_name); + } + + for (auto c : deviceInfo->m_controls) { + addControlRow(c->m_name, c->m_id, c->m_units); + } + for (auto s : deviceInfo->m_sensors) { + addSensorRow(s->m_name, s->m_id, s->m_units); + } + } +} + +void RemoteControlDeviceDialog::updateTable() +{ + for (int row = 0; row < ui->controls->rowCount(); row++) + { + QString controlId = ui->controls->item(row, COL_ID)->text(); + RemoteControlControl *control = m_rcDevice->getControl(controlId); + if (control != nullptr) + { + ui->controls->item(row, COL_ENABLE)->setCheckState(Qt::Checked); + ui->controls->item(row, COL_LABEL_LEFT)->setText(control->m_labelLeft); + ui->controls->item(row, COL_LABEL_RIGHT)->setText(control->m_labelRight); + } + else + { + ui->controls->item(row, COL_ENABLE)->setCheckState(Qt::Unchecked); + } + } + for (int row = 0; row < ui->sensors->rowCount(); row++) + { + QString sensorId = ui->sensors->item(row, COL_ID)->text(); + RemoteControlSensor *sensor = m_rcDevice->getSensor(sensorId); + if (sensor != nullptr) + { + ui->sensors->item(row, COL_ENABLE)->setCheckState(Qt::Checked); + ui->sensors->item(row, COL_LABEL_LEFT)->setText(sensor->m_labelLeft); + ui->sensors->item(row, COL_LABEL_RIGHT)->setText(sensor->m_labelRight); + ui->sensors->item(row, COL_FORMAT)->setText(sensor->m_format); + ui->sensors->item(row, COL_PLOT)->setCheckState(sensor->m_plot ? Qt::Checked : Qt::Unchecked); + } + else + { + ui->sensors->item(row, COL_ENABLE)->setCheckState(Qt::Unchecked); + } + } +} + +void RemoteControlDeviceDialog::deviceList(const QList &devices) +{ + ui->device->clear(); + m_deviceInfo = devices; // Take a deep copy + int i = 0; + for (auto const &device : m_deviceInfo) + { + // Update default device info, with info for device we are editing + if (m_setDeviceWhenAvailable && (device.m_id == m_rcDevice->m_info.m_id)) { + m_deviceInfo[i] = m_rcDevice->m_info; + } + // Add device to list + ui->device->addItem(device.m_name); + i++; + } + if (m_setDeviceWhenAvailable) + { + ui->device->setCurrentText(m_rcDevice->m_info.m_name); + m_setDeviceWhenAvailable = false; + updateTable(); + } +} + +void RemoteControlDeviceDialog::deviceError(const QString &error) +{ + QMessageBox::critical(this, "Remote Control Error", error); +} + +void RemoteControlDeviceDialog::on_controlAdd_clicked() +{ + VISADevice::VISAControl *control = new VISADevice::VISAControl(); + RemoteControlVISAControlDialog dialog(m_settings, m_rcDevice, control, true); + if (dialog.exec() == QDialog::Accepted) + { + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->m_controls.append(reinterpret_cast(control)); + + addControlRow(control->m_name, control->m_id, control->m_units); + } + else + { + delete control; + } +} + +void RemoteControlDeviceDialog::on_controlEdit_clicked() +{ + QList items = ui->controls->selectedItems(); + if (items.size() > 0) + { + int row = items[0]->row(); + QString id = ui->controls->item(row, COL_ID)->text(); + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + VISADevice::VISAControl *control = reinterpret_cast(info->getControl(id)); + + RemoteControlVISAControlDialog dialog(m_settings, m_rcDevice, control, false); + if (dialog.exec() == QDialog::Accepted) + { + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + + ui->controls->item(row, COL_NAME)->setText(control->m_name); + ui->controls->item(row, COL_UNITS)->setText(control->m_units); + ui->controls->item(row, COL_ID)->setText(control->m_id); + } + } +} + +void RemoteControlDeviceDialog::on_controls_cellDoubleClicked(int row, int column) +{ + if ((ui->protocol->currentText() == "VISA") && (column <= COL_ID)) { + on_controlEdit_clicked(); + } +} + +void RemoteControlDeviceDialog::on_controlRemove_clicked() +{ + QList items = ui->controls->selectedItems(); + if (items.size() > 0) + { + int row = items[0]->row(); + QString id = ui->controls->item(row, COL_ID)->text(); + ui->controls->removeRow(row); + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->deleteControl(id); + } +} + +void RemoteControlDeviceDialog::on_controlUp_clicked() +{ + QList items = ui->controls->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = items[i]->row(); + int col = items[i]->column(); + if (row > 0) + { + // Swap rows in table + QTableWidgetItem *item1 = ui->controls->takeItem(row, col); + QTableWidgetItem *item2 = ui->controls->takeItem(row - 1, col); + ui->controls->setItem(row - 1, col, item1); + ui->controls->setItem(row, col, item2); + } + if (i == items.size() - 1) + { + ui->controls->setCurrentItem(items[i]); + if (row > 0) + { + // Swap device info + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->m_controls.swapItemsAt(row, row - 1); + } + } + } +} + +void RemoteControlDeviceDialog::on_controlDown_clicked() +{ + QList items = ui->controls->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = items[i]->row(); + int col = items[i]->column(); + if (row < ui->controls->rowCount() - 1) + { + // Swap rows in table + QTableWidgetItem *item1 = ui->controls->takeItem(row, col); + QTableWidgetItem *item2 = ui->controls->takeItem(row + 1, col); + ui->controls->setItem(row + 1, col, item1); + ui->controls->setItem(row, col, item2); + } + if (i == items.size() - 1) + { + ui->controls->setCurrentItem(items[i]); + if (row < ui->controls->rowCount() - 1) + { + // Swap device info + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->m_controls.swapItemsAt(row, row + 1); + } + } + } +} + +void RemoteControlDeviceDialog::on_sensorAdd_clicked() +{ + VISADevice::VISASensor *sensor = new VISADevice::VISASensor(); + RemoteControlVISASensorDialog dialog(m_settings, m_rcDevice, sensor, true); + if (dialog.exec() == QDialog::Accepted) + { + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->m_sensors.append(reinterpret_cast(sensor)); + + addSensorRow(sensor->m_name, sensor->m_id, sensor->m_units); + } + else + { + delete sensor; + } +} + +void RemoteControlDeviceDialog::on_sensorRemove_clicked() +{ + QList items = ui->sensors->selectedItems(); + if (items.size() > 0) + { + int row = items[0]->row(); + QString id = ui->sensors->item(row, COL_ID)->text(); + ui->sensors->removeRow(row); + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->deleteSensor(id); + } +} + +void RemoteControlDeviceDialog::on_sensorEdit_clicked() +{ + QList items = ui->sensors->selectedItems(); + if (items.size() > 0) + { + int row = items[0]->row(); + QString id = ui->sensors->item(row, COL_ID)->text(); + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + VISADevice::VISASensor *sensor = reinterpret_cast(info->getSensor(id)); + + RemoteControlVISASensorDialog dialog(m_settings, m_rcDevice, sensor, false); + if (dialog.exec() == QDialog::Accepted) + { + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + + ui->sensors->item(row, COL_NAME)->setText(sensor->m_name); + ui->sensors->item(row, COL_ID)->setText(sensor->m_id); + ui->sensors->item(row, COL_UNITS)->setText(sensor->m_units); + } + } +} + +void RemoteControlDeviceDialog::on_sensors_cellDoubleClicked(int row, int column) +{ + if ((ui->protocol->currentText() == "VISA") && (column <= COL_ID)) { + on_sensorEdit_clicked(); + } +} + +void RemoteControlDeviceDialog::on_sensorUp_clicked() +{ + QList items = ui->sensors->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = items[i]->row(); + int col = items[i]->column(); + if (row > 0) + { + // Swap rows in table + QTableWidgetItem *item1 = ui->sensors->takeItem(row, col); + QTableWidgetItem *item2 = ui->sensors->takeItem(row - 1, col); + ui->sensors->setItem(row - 1, col, item1); + ui->sensors->setItem(row, col, item2); + } + if (i == items.size() - 1) + { + ui->sensors->setCurrentItem(items[i]); + if (row > 0) + { + // Swap device info + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->m_sensors.swapItemsAt(row, row - 1); + } + } + } +} + +void RemoteControlDeviceDialog::on_sensorDown_clicked() +{ + QList items = ui->sensors->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = items[i]->row(); + int col = items[i]->column(); + if (row < ui->sensors->rowCount() - 1) + { + // Swap rows in table + QTableWidgetItem *item1 = ui->sensors->takeItem(row, col); + QTableWidgetItem *item2 = ui->sensors->takeItem(row + 1, col); + ui->sensors->setItem(row + 1, col, item1); + ui->sensors->setItem(row, col, item2); + } + if (i == items.size() - 1) + { + ui->sensors->setCurrentItem(items[i]); + if (row < ui->sensors->rowCount() - 1) + { + // Swap device info + DeviceDiscoverer::DeviceInfo *info = &m_deviceInfo[ui->device->currentIndex()]; + info->m_sensors.swapItemsAt(row, row + 1); + } + } + } +} diff --git a/plugins/feature/remotecontrol/remotecontroldevicedialog.h b/plugins/feature/remotecontrol/remotecontroldevicedialog.h new file mode 100644 index 000000000..1a84d2d76 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontroldevicedialog.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_REMOTECONTROLDEVICEDIALOG_H +#define INCLUDE_FEATURE_REMOTECONTROLDEVICEDIALOG_H + +#include "ui_remotecontroldevicedialog.h" +#include "remotecontrolsettings.h" +#include "util/iot/device.h" + +class RemoteControlDeviceDialog : public QDialog { + Q_OBJECT + +public: + explicit RemoteControlDeviceDialog(RemoteControlSettings *settings, RemoteControlDevice *device, QWidget* parent = 0); + ~RemoteControlDeviceDialog(); + +private slots: + void accept(); + void on_protocol_currentTextChanged(const QString &protocol); + void on_device_currentIndexChanged(int index); + void deviceList(const QList &devices); + void deviceError(const QString &error); + void on_controlAdd_clicked(); + void on_controlRemove_clicked(); + void on_controlEdit_clicked(); + void on_controlUp_clicked(); + void on_controlDown_clicked(); + void on_controls_cellDoubleClicked(int row, int column); + void on_sensorAdd_clicked(); + void on_sensorRemove_clicked(); + void on_sensorEdit_clicked(); + void on_sensorUp_clicked(); + void on_sensorDown_clicked(); + void on_sensors_cellDoubleClicked(int row, int column); + void controlSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void sensorSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +private: + void enableWidgets(); + void resizeTables(); + void updateTable(); + int addControlRow(const QString &name, const QString &id, const QString &units); + int addSensorRow(const QString &name, const QString &id, const QString &units); + + Ui::RemoteControlDeviceDialog* ui; + RemoteControlSettings *m_settings; + RemoteControlDevice *m_rcDevice; + DeviceDiscoverer *m_discoverer; + QList m_deviceInfo; + bool m_setDeviceWhenAvailable; + + enum SensorCol { + COL_ENABLE, + COL_NAME, + COL_UNITS, + COL_ID, + COL_LABEL_LEFT, + COL_LABEL_RIGHT, + COL_FORMAT, + COL_PLOT + }; +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLDEVICEDIALOG_H diff --git a/plugins/feature/remotecontrol/remotecontroldevicedialog.ui b/plugins/feature/remotecontrol/remotecontroldevicedialog.ui new file mode 100644 index 000000000..57772fdcf --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontroldevicedialog.ui @@ -0,0 +1,592 @@ + + + RemoteControlDeviceDialog + + + + 0 + 0 + 800 + 700 + + + + + Liberation Sans + 9 + + + + Remote Control Device + + + + + + false + + + + 0 + + + + + + + + + + Protocol to connect to the device + + + Protocol: + + + + + + + + Select a protocol... + + + + + TPLink + + + + + HomeAssistant + + + + + VISA + + + + + + + + Device: + + + + + + + Model: + + + + + + + + + + Device model name + + + + + + + + + + Label: + + + + + + + Label to display for this device in the UI + + + + + + + + + Controls + + + false + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + Enable + + + Enable display of control in GUI + + + + + Name + + + Name of the control + + + + + Units + + + + + ID + + + + + Left Label + + + Label to display to the left of this control in the UI + + + + + Right Label + + + Label to display to the right of this control in the UI + + + + + + + + + + + + Add a new control + + + Add... + + + + + + + false + + + Remove selected control + + + Remove + + + + + + + false + + + Edit selected control + + + Edit... + + + + + + + false + + + Move down + + + + + + + :/arrow_down.png:/arrow_down.png + + + + + + + false + + + Move up + + + + + + + :/arrow_up.png:/arrow_up.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Layout: + + + + + + + How controls are laid out in the UI + + + + Horizontal + + + + + Vertical + + + + + + + + + + + + + Sensors + + + false + + + false + + + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + Enable + + + Enable display of this sensor's value + + + + + Name + + + Sensor name + + + + + Units + + + Units to display for this sensor in the UI + + + + + ID + + + + + Left Label + + + Label to display to the left of this sensor in the UI + + + + + Right Label + + + Label to display to the right of this sensor in the UI + + + + + Format + + + printf format string for formatting the sensor value as a decimal floating point value (E.g. %f %.1f %.3e) or as a string (%s) + + + + + Plot + + + Plot sensor data on chart + + + + + + + + + + + + Add... + + + + + + + false + + + Remove + + + + + + + false + + + Edit... + + + + + + + false + + + Move down + + + + + + + :/arrow_down.png:/arrow_down.png + + + + + + + false + + + Move up + + + + + + + :/arrow_up.png:/arrow_up.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Layout + + + + + + + How sensors are laid out in the UI + + + 1 + + + + Horizontal + + + + + Vertical + + + + + + + + Y Axis + + + + + + + Set whether each series of sensor data is plotted on a common Y axis or with individual axes + + + + Per-sensor + + + + + Common + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + protocol + device + label + controls + controlAdd + controlRemove + controlEdit + controlDown + controlUp + controlsLayout + sensors + sensorAdd + sensorRemove + sensorEdit + sensorDown + sensorUp + sensorsLayout + yAxis + + + + + + + buttonBox + accepted() + RemoteControlDeviceDialog + accept() + + + 295 + 619 + + + 295 + 319 + + + + + buttonBox + rejected() + RemoteControlDeviceDialog + reject() + + + 295 + 619 + + + 295 + 319 + + + + + diff --git a/plugins/feature/remotecontrol/remotecontrolgui.cpp b/plugins/feature/remotecontrol/remotecontrolgui.cpp new file mode 100644 index 000000000..02ecdb314 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolgui.cpp @@ -0,0 +1,1170 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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 "feature/featureuiset.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "gui/flowlayout.h" +#include "gui/scidoublespinbox.h" + +#include "ui_remotecontrolgui.h" +#include "remotecontrol.h" +#include "remotecontrolgui.h" +#include "remotecontrolsettingsdialog.h" + +RemoteControlGUI* RemoteControlGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + RemoteControlGUI* gui = new RemoteControlGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void RemoteControlGUI::destroy() +{ + delete this; +} + +void RemoteControlGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray RemoteControlGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool RemoteControlGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex); + displaySettings(); + applySettings(true); + on_update_clicked(); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool RemoteControlGUI::handleMessage(const Message& message) +{ + if (RemoteControl::MsgConfigureRemoteControl::match(message)) + { + qDebug("RemoteControlGUI::handleMessage: RemoteControl::MsgConfigureRemoteControl"); + const RemoteControl::MsgConfigureRemoteControl& cfg = (RemoteControl::MsgConfigureRemoteControl&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (RemoteControl::MsgDeviceStatus::match(message)) + { + const RemoteControl::MsgDeviceStatus& msg = (RemoteControl::MsgDeviceStatus&) message; + deviceUpdated(msg.getProtocol(), msg.getDeviceId(), msg.getStatus()); + return true; + } + else if (RemoteControl::MsgDeviceError::match(message)) + { + const RemoteControl::MsgDeviceError& msg = (RemoteControl::MsgDeviceError&) message; + QMessageBox::critical(this, "Remote Control Error", msg.getErrorMessage()); + return true; + } + else if (RemoteControl::MsgDeviceUnavailable::match(message)) + { + const RemoteControl::MsgDeviceUnavailable& msg = (RemoteControl::MsgDeviceUnavailable&) message; + deviceUnavailable(msg.getProtocol(), msg.getDeviceId()); + return true; + } + + return false; +} + +void RemoteControlGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void RemoteControlGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +RemoteControlGUI::RemoteControlGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::RemoteControlGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true) +{ + m_feature = feature; + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/feature/remotecontrol/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setSizePolicy(rollupContents->sizePolicy()); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + + ui->startStop->setStyleSheet("QToolButton { background-color : blue; }" + "QToolButton:checked { background-color : green; }" + "QToolButton:disabled { background-color : gray; }"); + + m_startStopIcon.addFile(":/play.png", QSize(16, 16), QIcon::Normal, QIcon::Off); + m_startStopIcon.addFile(":/stop.png", QSize(16, 16), QIcon::Normal, QIcon::On); + + m_remoteControl = reinterpret_cast(feature); + m_remoteControl->setMessageQueueToGUI(&m_inputMessageQueue); + + m_settings.setRollupState(&m_rollupState); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + displaySettings(); + applySettings(true); + makeUIConnections(); +} + +RemoteControlGUI::~RemoteControlGUI() +{ + qDeleteAll(m_deviceGUIs); + m_deviceGUIs.clear(); + delete ui; +} + +void RemoteControlGUI::setWorkspaceIndex(int index) +{ + m_settings.m_workspaceIndex = index; + m_feature->setWorkspaceIndex(index); +} + +void RemoteControlGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void RemoteControlGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + setTitle(m_settings.m_title); + createGUI(); + blockApplySettings(true); + getRollupContents()->restoreState(m_rollupState); + blockApplySettings(false); + getRollupContents()->arrangeRollups(); +} + +void RemoteControlGUI::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); + 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); + + applySettings(); + } + + resetContextMenuType(); +} + +void RemoteControlGUI::createControls(RemoteControlDeviceGUI *gui, QBoxLayout *vBox, FlowLayout *flow, int &widgetCnt) +{ + // Create buttons to control the device + QGridLayout *controlsGrid = nullptr; + + if (gui->m_rcDevice->m_verticalControls) + { + controlsGrid = new QGridLayout(); + vBox->addLayout(controlsGrid); + } + else if (!flow) + { + flow = new FlowLayout(2, 6, 6); + vBox->addItem(flow); + } + + int row = 0; + for (auto const &control : gui->m_rcDevice->m_controls) + { + if (!gui->m_rcDevice->m_verticalControls && (widgetCnt > 0)) + { + QFrame *line = new QFrame(); + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Sunken); + flow->addWidget(line); + } + + DeviceDiscoverer::ControlInfo *info = gui->m_rcDevice->m_info.getControl(control.m_id); + if (!info) + { + qDebug() << "RemoteControlGUI::createControls: Info missing for " << control.m_id; + continue; + } + + if (!control.m_labelLeft.isEmpty()) + { + QLabel *controlLabelLeft = new QLabel(control.m_labelLeft); + if (gui->m_rcDevice->m_verticalControls) + { + controlsGrid->addWidget(controlLabelLeft, row, 0); + controlsGrid->setColumnStretch(row, 0); + } + else + { + flow->addWidget(controlLabelLeft); + } + } + + QList widgets; + QWidget *widget = nullptr; + + switch (info->m_type) + { + case DeviceDiscoverer::BOOL: + { + ButtonSwitch *button = new ButtonSwitch(); + button->setToolTip("Start/stop " + info->m_name); + button->setIcon(m_startStopIcon); + button->setStyleSheet("QToolButton { background-color : blue; }" + "QToolButton:checked { background-color : green; }" + "QToolButton:disabled { background-color : gray; }"); + connect(button, &ButtonSwitch::toggled, + [=] (bool toggled) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + toggled); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(button); + widget = button; + } + break; + + case DeviceDiscoverer::INT: + { + QSpinBox *spinBox = new QSpinBox(); + + spinBox->setToolTip("Set value for " + info->m_name); + spinBox->setMinimum((int)info->m_min); + spinBox->setMaximum((int)info->m_max); + connect(spinBox, static_cast(&QSpinBox::valueChanged), + [=] (int value) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + value); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(spinBox); + widget = spinBox; + } + break; + + case DeviceDiscoverer::FLOAT: + { + switch (info->m_widgetType) + { + case DeviceDiscoverer::SPIN_BOX: + { + QDoubleSpinBox *spinBox = new SciDoubleSpinBox(); + + spinBox->setToolTip("Set value for " + info->m_name); + spinBox->setMinimum(info->m_min); + spinBox->setMaximum(info->m_max); + spinBox->setDecimals(info->m_precision); + connect(spinBox, static_cast(&QDoubleSpinBox::valueChanged), + [=] (double value) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + (float)value * info->m_scale); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(spinBox); + widget = spinBox; + } + break; + + case DeviceDiscoverer::DIAL: + { + widget = new QWidget(); + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + widget->setLayout(layout); + + QDial *dial = new QDial(); + dial->setMaximumSize(24, 24); + dial->setToolTip("Set value for " + info->m_name); + dial->setMinimum(info->m_min); + dial->setMaximum(info->m_max); + + connect(dial, static_cast(&QDial::valueChanged), + [=] (int value) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + ((float)value) * info->m_scale); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(dial); + layout->addWidget(dial); + + QLabel *label = new QLabel(QString::number(dial->value())); + label->setToolTip("Value for " + info->m_name); + widgets.append(label); + layout->addWidget(label); + } + break; + + case DeviceDiscoverer::SLIDER: + { + widget = new QWidget(); + QHBoxLayout *layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + widget->setLayout(layout); + + QSlider *slider = new QSlider(Qt::Horizontal); + slider->setToolTip("Set value for " + info->m_name); + slider->setMinimum(info->m_min); + slider->setMaximum(info->m_max); + + connect(slider, static_cast(&QSlider::valueChanged), + [=] (int value) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + ((float)value) * info->m_scale); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(slider); + layout->addWidget(slider); + + QLabel *label = new QLabel(QString::number(slider->value())); + label->setToolTip("Value for " + info->m_name); + widgets.append(label); + layout->addWidget(label); + } + break; + + } + } + break; + + case DeviceDiscoverer::STRING: + { + QLineEdit *lineEdit = new QLineEdit(); + + lineEdit->setToolTip("Set value for " + info->m_name); + + connect(lineEdit, &QLineEdit::editingFinished, + [=] () + { + QString text = lineEdit->text(); + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + text); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(lineEdit); + widget = lineEdit; + } + break; + + case DeviceDiscoverer::LIST: + { + QComboBox *combo = new QComboBox(); + + combo->setToolTip("Set value for " + info->m_name); + combo->insertItems(0, info->m_values); + connect(combo, &QComboBox::textActivated, + [=] (const QString &text) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + text); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(combo); + widget = combo; + } + break; + + case DeviceDiscoverer::BUTTON: + { + QString label = info->m_name; + if (info->m_values.size() > 0) { + label = info->m_values[0]; + } + QToolButton *button = new QToolButton(); + button->setText(label); + button->setToolTip("Trigger " + info->m_name); + + connect(button, &QToolButton::clicked, + [=] (bool checked) + { + RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, + gui->m_rcDevice->m_info.m_id, + control.m_id, + 1); + m_remoteControl->getInputMessageQueue()->push(message); + } + ); + widgets.append(button); + widget = button; + } + break; + + + } + gui->m_controls.insert(control.m_id, widgets); + if (gui->m_rcDevice->m_verticalControls) { + controlsGrid->addWidget(widget, row, 1); + } else { + flow->addWidget(widget); + } + + if (!control.m_labelRight.isEmpty()) + { + QLabel *controlLabelRight = new QLabel(control.m_labelRight); + if (gui->m_rcDevice->m_verticalControls) + { + controlsGrid->addWidget(controlLabelRight, row, 2); + controlsGrid->setColumnStretch(row, 2); + } + else + { + flow->addWidget(controlLabelRight); + } + } + + widgetCnt++; + row++; + } +} + +void RemoteControlGUI::createChart(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, const QString &id, const QString &units) +{ + if (gui->m_chart == nullptr) + { + // Create a chart to plot the sensor data + gui->m_chart = new QChart(); + gui->m_chart->setTitle(""); + gui->m_chart->legend()->hide(); + gui->m_chart->layout()->setContentsMargins(0, 0, 0, 0); + gui->m_chart->setMargins(QMargins(1, 1, 1, 1)); + gui->m_chart->setTheme(QChart::ChartThemeDark); + QLineSeries *series = new QLineSeries(); + gui->m_series.insert(id, series); + QLineSeries *onePointSeries = new QLineSeries(); + gui->m_onePointSeries.insert(id, onePointSeries); + gui->m_chart->addSeries(series); + QValueAxis *yAxis = new QValueAxis(); + QDateTimeAxis *xAxis = new QDateTimeAxis(); + xAxis->setFormat(QString("hh:mm:ss")); + yAxis->setTitleText(units); + gui->m_chart->addAxis(xAxis, Qt::AlignBottom); + gui->m_chart->addAxis(yAxis, Qt::AlignLeft); + series->attachAxis(xAxis); + series->attachAxis(yAxis); + gui->m_chartView = new QChartView(); + gui->m_chartView->setChart(gui->m_chart); + if (m_settings.m_chartHeightFixed) + { + gui->m_chartView->setMinimumSize(300, m_settings.m_chartHeightPixels); + gui->m_chartView->setMaximumSize(16777215, m_settings.m_chartHeightPixels); + gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + } + else + { + gui->m_chartView->setMinimumSize(300, 130); // 130 is enough to display axis labels + gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + gui->m_chartView->setSceneRect(0, 0, 300, 130); // This determines m_chartView->sizeHint() - default is 640x480, which is a bit big + } + QBoxLayout *chartLayout = new QVBoxLayout(); + gui->m_chartView->setLayout(chartLayout); + + vBox->addWidget(gui->m_chartView); + } + else + { + // Add new series + QLineSeries *series = new QLineSeries(); + gui->m_series.insert(id, series); + QLineSeries *onePointSeries = new QLineSeries(); + gui->m_onePointSeries.insert(id, onePointSeries); + gui->m_chart->addSeries(series); + if (!gui->m_rcDevice->m_commonYAxis) + { + // Use per series Y axis + QValueAxis *yAxis = new QValueAxis(); + yAxis->setTitleText(units); + gui->m_chart->addAxis(yAxis, Qt::AlignRight); + series->attachAxis(yAxis); + } + else + { + // Use common y axis + QAbstractAxis *yAxis = gui->m_chart->axes(Qt::Vertical)[0]; + // Only display units if all the same + if (yAxis->titleText() != units) { + yAxis->setTitleText(""); + } + series->attachAxis(yAxis); + } + series->attachAxis(gui->m_chart->axes(Qt::Horizontal)[0]); + } +} + +void RemoteControlGUI::createSensors(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, FlowLayout *flow, int &widgetCnt, bool &hasCharts) +{ + // Table doesn't seem to expand in a QHBoxLayout, so we have to use a GridLayout + QGridLayout *grid = nullptr; + QTableWidget *table = nullptr; + if (gui->m_rcDevice->m_verticalSensors) + { + grid = new QGridLayout(); + grid->setColumnStretch(0, 1); + vBox->addLayout(grid); + table = new QTableWidget(gui->m_rcDevice->m_sensors.size(), 3); + table->verticalHeader()->setVisible(false); + table->horizontalHeader()->setVisible(false); + table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + table->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); // Needed so table->sizeHint matches minimumSize set below + } + else if (!flow) + { + flow = new FlowLayout(2, 6, 6); + vBox->addItem(flow); + } + + int row = 0; + bool hasUnits = false; + for (auto const &sensor : gui->m_rcDevice->m_sensors) + { + // For vertical layout, we use a table + // For horizontal, we use HBox of labels separated with bars + if (gui->m_rcDevice->m_verticalSensors) + { + if (!sensor.m_labelLeft.isEmpty()) + { + QTableWidgetItem *sensorLabel = new QTableWidgetItem(sensor.m_labelLeft); + sensorLabel->setFlags(Qt::ItemIsEnabled); + table->setItem(row, COL_LABEL, sensorLabel); + } + QTableWidgetItem *valueItem = new QTableWidgetItem("-"); + table->setItem(row, COL_VALUE, valueItem); + valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + valueItem->setFlags(Qt::ItemIsEnabled); + if (!sensor.m_labelRight.isEmpty()) + { + QTableWidgetItem *unitsItem = new QTableWidgetItem(sensor.m_labelRight); + unitsItem->setFlags(Qt::ItemIsEnabled); + table->setItem(row, COL_UNITS, unitsItem); + hasUnits = true; + } + gui->m_sensorValueItems.insert(sensor.m_id, valueItem); + grid->addWidget(table, 0, 0); + } + else + { + if (widgetCnt > 0) + { + QFrame *line = new QFrame(); + line->setFrameShape(QFrame::VLine); + line->setFrameShadow(QFrame::Sunken); + flow->addWidget(line); + } + if (!sensor.m_labelLeft.isEmpty()) + { + QLabel *sensorLabel = new QLabel(sensor.m_labelLeft); + flow->addWidget(sensorLabel); + } + QLabel *sensorValue = new QLabel("-"); + flow->addWidget(sensorValue); + if (!sensor.m_labelRight.isEmpty()) + { + QLabel *sensorUnits = new QLabel(sensor.m_labelRight); + flow->addWidget(sensorUnits); + } + gui->m_sensorValueLabels.insert(sensor.m_id, sensorValue); + } + + if (sensor.m_plot) + { + createChart(gui, vBox, sensor.m_id, sensor.m_labelRight); + hasCharts = true; + } + + widgetCnt++; + row++; + } + + if (table) + { + table->resizeColumnToContents(COL_LABEL); + if (hasUnits) { + table->resizeColumnToContents(COL_UNITS); + } else { + table->hideColumn(COL_UNITS); + } + + int tableWidth = 0; + for (int i = 0; i < table->columnCount(); i++){ + tableWidth += table->columnWidth(i); + } + int tableHeight = 0; + for (int i = 0; i < table->rowCount(); i++){ + tableHeight += table->rowHeight(i); + } + table->setMinimumWidth(tableWidth); + table->setMinimumHeight(tableHeight+2); + } +} + +RemoteControlGUI::RemoteControlDeviceGUI *RemoteControlGUI::createDeviceGUI(RemoteControlDevice *rcDevice) +{ + // Create the UI for the device + RemoteControlDeviceGUI *gui = new RemoteControlDeviceGUI(rcDevice); + + bool hasCharts = false; + + gui->m_container = new QWidget(getRollupContents()); + gui->m_container->setWindowTitle(gui->m_rcDevice->m_label); + bool vertical = gui->m_rcDevice->m_verticalControls || gui->m_rcDevice->m_verticalSensors; + QVBoxLayout *vBox = new QVBoxLayout(); + vBox->setContentsMargins(2, 2, 2, 2); + FlowLayout *flow = nullptr; + + if (!vertical) + { + flow = new FlowLayout(2, 6, 6); + vBox->addItem(flow); + } + int widgetCnt = 0; + + // Create buttons to control the device + createControls(gui, vBox, flow, widgetCnt); + + if (gui->m_rcDevice->m_verticalControls) { + widgetCnt = 0; + } + + // Create widgets to display the sensor label and its value + createSensors(gui, vBox, flow, widgetCnt, hasCharts); + + gui->m_container->setLayout(vBox); + + if (hasCharts && !m_settings.m_chartHeightFixed) { + gui->m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } + + gui->m_container->show(); + return gui; +} + +void RemoteControlGUI::createGUI() +{ + // Delete existing elements + for (auto gui : m_deviceGUIs) + { + delete gui->m_container; + gui->m_container = nullptr; + } + qDeleteAll(m_deviceGUIs); + m_deviceGUIs.clear(); + + // Create new GUIs for each device + bool expanding = false; + for (auto device : m_settings.m_devices) + { + RemoteControlDeviceGUI *gui = createDeviceGUI(device); + m_deviceGUIs.append(gui); + if (gui->m_container->sizePolicy().verticalPolicy() == QSizePolicy::Expanding) { + expanding = true; + } + } + if (expanding) + { + getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } + else + { + getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + } + + // FIXME: Why are these three steps needed to get the window + // to resize to the newly added widgets? + getRollupContents()->arrangeRollups(); // Recalc rollup size + layout()->activate(); // Get QMdiSubWindow to recalc its sizeHint + resize(sizeHint()); + + // Need to do it twice when FlowLayout is used! + getRollupContents()->arrangeRollups(); + layout()->activate(); + resize(sizeHint()); +} + +void RemoteControlGUI::on_startStop_toggled(bool checked) +{ + if (m_doApplySettings) + { + RemoteControl::MsgStartStop *message = RemoteControl::MsgStartStop::create(checked); + m_remoteControl->getInputMessageQueue()->push(message); + } +} + +void RemoteControlGUI::on_update_clicked() +{ + RemoteControl::MsgDeviceGetState *message = RemoteControl::MsgDeviceGetState::create(); + m_remoteControl->getInputMessageQueue()->push(message); +} + +void RemoteControlGUI::on_settings_clicked() +{ + // Display settings dialog + RemoteControlSettingsDialog dialog(&m_settings); + if (dialog.exec() == QDialog::Accepted) + { + createGUI(); + applySettings(); + on_update_clicked(); + } +} + +void RemoteControlGUI::on_clearData_clicked() +{ + // Clear data in all charts + for (auto deviceGUI : m_deviceGUIs) + { + for (auto series : deviceGUI->m_series) { + series->clear(); + } + for (auto series : deviceGUI->m_onePointSeries) { + series->clear(); + } + } +} + +// Update a control widget with latest state value +void RemoteControlGUI::updateControl(QWidget *widget, const DeviceDiscoverer::ControlInfo *controlInfo, const QString &key, const QVariant &value) +{ + if (ButtonSwitch *button = qobject_cast(widget)) + { + if (value.type() == QMetaType::QString) + { + if (value.toString() == "unavailable") + { + button->setStyleSheet("QToolButton { background-color : gray; }" + "QToolButton:checked { background-color : gray; }" + "QToolButton:disabled { background-color : gray; }"); + } + else if (value.toString() == "error") + { + button->setStyleSheet("QToolButton { background-color : red; }" + "QToolButton:checked { background-color : red; }" + "QToolButton:disabled { background-color : red; }"); + } + else + { + qDebug() << "RemoteControlGUI::updateControl: String value for button " << key << value; + } + } + else + { + int state = value.toInt(); + int prev = button->blockSignals(true); + button->setChecked(state != 0); + button->blockSignals(prev); + button->setStyleSheet("QToolButton { background-color : blue; }" + "QToolButton:checked { background-color : green; }" + "QToolButton:disabled { background-color : gray; }"); + } + } + else if (QSpinBox *spinBox = qobject_cast(widget)) + { + int prev = spinBox->blockSignals(true); + if (value.toString() == "unavailable") + { + spinBox->setStyleSheet("QSpinBox { background-color : gray; }"); + } + else if (value.toString() == "error") + { + spinBox->setStyleSheet("QSpinBox { background-color : red; }"); + } + else + { + int state = value.toInt(); + bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum()); + spinBox->setValue(state); + if (outOfRange) { + spinBox->setStyleSheet("QSpinBox { background-color : red; }"); + } else { + spinBox->setStyleSheet(""); + } + } + spinBox->blockSignals(prev); + } + else if (QDoubleSpinBox *spinBox = qobject_cast(widget)) + { + int prev = spinBox->blockSignals(true); + if (value.toString() == "unavailable") + { + spinBox->setStyleSheet("QDoubleSpinBox { background-color : gray; }"); + } + else if (value.toString() == "error") + { + spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }"); + } + else + { + double state = value.toDouble(); + if (controlInfo) { + state = state / controlInfo->m_scale; + } + bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum()); + spinBox->setValue(state); + if (outOfRange) { + spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }"); + } else { + spinBox->setStyleSheet(""); + } + } + spinBox->blockSignals(prev); + } + else if (QDial *dial = qobject_cast(widget)) + { + int prev = dial->blockSignals(true); + if (value.toString() == "unavailable") + { + dial->setStyleSheet("QDial { background-color : gray; }"); + } + else if (value.toString() == "error") + { + dial->setStyleSheet("QDial { background-color : red; }"); + } + else + { + double state = value.toDouble(); + if (controlInfo) { + state = state / controlInfo->m_scale; + } + bool outOfRange = (state < dial->minimum()) || (state > dial->maximum()); + dial->setValue(state); + if (outOfRange) { + dial->setStyleSheet("QDial { background-color : red; }"); + } else { + dial->setStyleSheet(""); + } + } + dial->blockSignals(prev); + } + else if (QSlider *slider = qobject_cast(widget)) + { + int prev = slider->blockSignals(true); + if (value.toString() == "unavailable") + { + slider->setStyleSheet("QSlider { background-color : gray; }"); + } + else if (value.toString() == "error") + { + slider->setStyleSheet("QSlider { background-color : red; }"); + } + else + { + double state = value.toDouble(); + if (controlInfo) { + state = state / controlInfo->m_scale; + } + bool outOfRange = (state < slider->minimum()) || (state > slider->maximum()); + slider->setValue(state); + if (outOfRange) { + slider->setStyleSheet("QSlider { background-color : red; }"); + } else { + slider->setStyleSheet(""); + } + } + slider->blockSignals(prev); + } + else if (QComboBox *comboBox = qobject_cast(widget)) + { + int prev = comboBox->blockSignals(true); + QString string = value.toString(); + int index = comboBox->findText(string); + if (index != -1) + { + comboBox->setCurrentIndex(index); + comboBox->setStyleSheet(""); + } + else + { + comboBox->setStyleSheet("QComboBox { background-color : red; }"); + } + comboBox->blockSignals(prev); + } + else if (QLineEdit *lineEdit = qobject_cast(widget)) + { + lineEdit->setText(value.toString()); + } + else if (QLabel *label = qobject_cast(widget)) + { + label->setText(value.toString()); + } + else + { + qDebug() << "RemoteControlGUI::updateControl: Unexpected widget type"; + } +} + +void RemoteControlGUI::updateChart(RemoteControlDeviceGUI *deviceGUI, const QString &key, const QVariant &value) +{ + // Format the value for display + bool ok = false; + double d = value.toDouble(&ok); + bool iOk = false; + int iValue = value.toInt(&iOk); + QString formattedValue; + RemoteControlSensor *sensor = deviceGUI->m_rcDevice->getSensor(key); + QString format = sensor->m_format.trimmed(); + if (format.contains("%s")) + { + formattedValue = QString::asprintf(format.toUtf8(), value.toString().toUtf8()); + } + else if (format.contains("%d") || format.contains("%u") || format.contains("%x") || format.contains("%X")) + { + formattedValue = QString::asprintf(format.toUtf8(), value.toInt()); + } + else if ((value.type() == QMetaType::Double) || (value.type() == QMetaType::Float)) + { + if (format.isEmpty()) { + format = "%.1f"; + } + formattedValue = QString::asprintf(format.toUtf8(), value.toDouble()); + } + else if (iOk) + { + formattedValue = QString::asprintf("%d", iValue); + } + else + { + formattedValue = value.toString(); + } + + // Update sensor value widget to display the latest value + if (deviceGUI->m_sensorValueLabels.contains(key)) { + deviceGUI->m_sensorValueLabels.value(key)->setText(formattedValue); + } else { + deviceGUI->m_sensorValueItems.value(key)->setText(formattedValue); + } + + // Plot value on chart + if (deviceGUI->m_series.contains(key)) + { + QLineSeries *onePointSeries = deviceGUI->m_onePointSeries.value(key); + QLineSeries *series = deviceGUI->m_series.value(key); + QDateTime dt = QDateTime::currentDateTime(); + if (ok) + { + // Charts aren't displayed properly if series has only one point, + // so we save the first point in an additional series: onePointSeries + if (onePointSeries->count() == 0) + { + onePointSeries->append(dt.toMSecsSinceEpoch(), d); + } + else + { + if (series->count() == 0) { + series->append(onePointSeries->at(0)); + } + series->append(dt.toMSecsSinceEpoch(), d); + QList axes = deviceGUI->m_chart->axes(Qt::Horizontal, series); + QDateTimeAxis *dtAxis = (QDateTimeAxis *)axes[0]; + QDateTime start = QDateTime::fromMSecsSinceEpoch(series->at(0).x()); + QDateTime end = QDateTime::fromMSecsSinceEpoch(series->at(series->count() - 1).x()); + if (start.date() == end.date()) + { + if (start.secsTo(end) < 60*5) { + dtAxis->setFormat(QString("hh:mm:ss")); + } else { + dtAxis->setFormat(QString("hh:mm")); + } + } + else + { + dtAxis->setFormat(QString("%1 hh:mm").arg(QLocale::system().dateFormat(QLocale::ShortFormat))); + } + dtAxis->setRange(start, end); + axes = deviceGUI->m_chart->axes(Qt::Vertical, series); + QValueAxis *yAxis = (QValueAxis *)axes[0]; + if (series->count() == 2) + { + double y1 = series->at(0).y(); + double y2 = series->at(1).y(); + double yMin = std::min(y1, y2); + double yMax = std::max(y1, y2); + double min = (yMin >= 0.0) ? yMin * 0.9 : yMin * 1.1; + double max = (yMax >= 0.0) ? yMax * 1.1 : yMax * 0.9; + yAxis->setRange(min, max); + } + else + { + double min = (d >= 0.0) ? d * 0.9 : d * 1.1; + double max = (d >= 0.0) ? d * 1.1 : d * 0.9; + if (min < yAxis->min()) { + yAxis->setMin(min); + } + if (max > yAxis->max()) { + yAxis->setMax(max); + } + } + } + } + else + { + qDebug() << "RemoteControlGUI::deviceUpdated: Error converting " << key << value; + } + } +} + +void RemoteControlGUI::deviceUpdated(const QString &protocol, const QString &deviceId, const QHash &status) +{ + for (auto deviceGUI : m_deviceGUIs) + { + if ( (protocol == deviceGUI->m_rcDevice->m_protocol) + && (deviceId == deviceGUI->m_rcDevice->m_info.m_id)) + { + deviceGUI->m_container->setEnabled(true); + + QHashIterator itr(status); + + while (itr.hasNext()) + { + itr.next(); + QString key = itr.key(); + QVariant value = itr.value(); + + if (deviceGUI->m_controls.contains(key)) + { + // Update control(s) to display latest state + QList widgets = deviceGUI->m_controls.value(key); + DeviceDiscoverer::ControlInfo *control = deviceGUI->m_rcDevice->m_info.getControl(key); + + for (auto widget : widgets) { + updateControl(widget, control, key, value); + } + } + else if (deviceGUI->m_sensorValueLabels.contains(key) || deviceGUI->m_sensorValueItems.contains(key)) + { + // Plot on chart + updateChart(deviceGUI, key, value); + } + else + { + qDebug() << "RemoteControlGUI::deviceUpdated: Unexpected status key " << key << value; + } + } + } + } +} + +void RemoteControlGUI::deviceUnavailable(const QString &protocol, const QString &deviceId) +{ + for (auto deviceGUI : m_deviceGUIs) + { + if ( (protocol == deviceGUI->m_rcDevice->m_protocol) + && (deviceId == deviceGUI->m_rcDevice->m_info.m_id)) + { + deviceGUI->m_container->setEnabled(false); + } + } +} + +void RemoteControlGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + RemoteControl::MsgConfigureRemoteControl* message = RemoteControl::MsgConfigureRemoteControl::create(m_settings, force); + m_remoteControl->getInputMessageQueue()->push(message); + } +} + +void RemoteControlGUI::makeUIConnections() +{ + QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &RemoteControlGUI::on_startStop_toggled); + QObject::connect(ui->update, &QToolButton::clicked, this, &RemoteControlGUI::on_update_clicked); + QObject::connect(ui->settings, &QToolButton::clicked, this, &RemoteControlGUI::on_settings_clicked); + QObject::connect(ui->clearData, &QToolButton::clicked, this, &RemoteControlGUI::on_clearData_clicked); +} diff --git a/plugins/feature/remotecontrol/remotecontrolgui.h b/plugins/feature/remotecontrol/remotecontrolgui.h new file mode 100644 index 000000000..1c12d0b5d --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolgui.h @@ -0,0 +1,137 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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_REMOTECONTROLGUI_H_ +#define INCLUDE_FEATURE_REMOTECONTROLGUI_H_ + +#include +#include +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "util/iot/device.h" +#include "gui/buttonswitch.h" +#include "settings/rollupstate.h" + +#include "remotecontrolsettings.h" + +class PluginAPI; +class FeatureUISet; +class RemoteControl; +class QGroupBox; +class QLabel; +class FlowLayout; + +namespace Ui { + class RemoteControlGUI; +} + +using namespace QtCharts; + +class RemoteControlGUI : public FeatureGUI { + Q_OBJECT + + struct RemoteControlDeviceGUI { + RemoteControlDevice *m_rcDevice; + QWidget *m_container; + QHash> m_controls; + QHash m_sensorValueLabels; + QHash m_sensorValueItems; + QChartView *m_chartView; + QChart *m_chart; + QHash m_series; + QHash m_onePointSeries; // Workaround for charts not drawing series with only one point properly + + RemoteControlDeviceGUI(RemoteControlDevice *rcDevice) : + m_rcDevice(rcDevice), + m_container(nullptr), + m_chartView(nullptr), + m_chart(nullptr) + { + } + + ~RemoteControlDeviceGUI() + { + } + }; + +public: + static RemoteControlGUI* 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::RemoteControlGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + RemoteControlSettings m_settings; + RollupState m_rollupState; + bool m_doApplySettings; + + RemoteControl* m_remoteControl; + MessageQueue m_inputMessageQueue; + QList m_deviceGUIs; + + QIcon m_startStopIcon; + + enum SensorCol { + COL_LABEL, + COL_VALUE, + COL_UNITS + }; + + explicit RemoteControlGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~RemoteControlGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + bool handleMessage(const Message& message); + void makeUIConnections(); + + void createControls(RemoteControlDeviceGUI *gui, QBoxLayout *vBox, FlowLayout *flow, int &widgetCnt); + void createChart(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, const QString &id, const QString &units); + void createSensors(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, FlowLayout *flow, int &widgetCnt, bool &hasCharts); + RemoteControlDeviceGUI *createDeviceGUI(RemoteControlDevice *device); + void createGUI(); + void updateControl(QWidget *widget, const DeviceDiscoverer::ControlInfo *controlInfo, const QString &key, const QVariant &value); + void updateChart(RemoteControlDeviceGUI *deviceGUI, const QString &key, const QVariant &value); + void deviceUpdated(const QString &protocol, const QString &deviceId, const QHash &status); + void deviceUnavailable(const QString &protocol, const QString &deviceId); + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void on_startStop_toggled(bool checked); + void on_update_clicked(); + void on_settings_clicked(); + void on_clearData_clicked(); +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLGUI_H_ diff --git a/plugins/feature/remotecontrol/remotecontrolgui.ui b/plugins/feature/remotecontrol/remotecontrolgui.ui new file mode 100644 index 000000000..67a760ade --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolgui.ui @@ -0,0 +1,174 @@ + + + RemoteControlGUI + + + + 0 + 0 + 360 + 60 + + + + + 0 + 0 + + + + + 360 + 0 + + + + + 16777215 + 16777215 + + + + + Liberation Sans + 9 + + + + Packet Error Rate Tester + + + + + 2 + 2 + 351 + 51 + + + + + 0 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Start/stop periodic updating of device state + + + + + + + :/play.png + :/stop.png:/play.png + + + + + + + Update state of all devices + + + + + + + :/recycle.png:/recycle.png + + + + + + + Open settings dialog + + + + + + + :/listing.png:/listing.png + + + + + + + Clear data in charts + + + + + + + :/bin.png:/bin.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+
+ + startStop + update + settings + clearData + + + + + +
diff --git a/plugins/feature/remotecontrol/remotecontrolplugin.cpp b/plugins/feature/remotecontrol/remotecontrolplugin.cpp new file mode 100644 index 000000000..d8f000602 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolplugin.cpp @@ -0,0 +1,74 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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 "remotecontrolgui.h" +#endif +#include "remotecontrol.h" +#include "remotecontrolplugin.h" + +const PluginDescriptor RemoteControlPlugin::m_pluginDescriptor = { + RemoteControl::m_featureId, + QStringLiteral("Remote Control"), + QStringLiteral("7.7.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +RemoteControlPlugin::RemoteControlPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& RemoteControlPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void RemoteControlPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(RemoteControl::m_featureIdURI, RemoteControl::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* RemoteControlPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* RemoteControlPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return RemoteControlGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* RemoteControlPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new RemoteControl(webAPIAdapterInterface); +} diff --git a/plugins/feature/remotecontrol/remotecontrolplugin.h b/plugins/feature/remotecontrol/remotecontrolplugin.h new file mode 100644 index 000000000..c291086ce --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolplugin.h @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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_REMOTECONTROLPLUGIN_H +#define INCLUDE_FEATURE_REMOTECONTROLPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class RemoteControlPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.remotecontrol") + +public: + explicit RemoteControlPlugin(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; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLPLUGIN_H diff --git a/plugins/feature/remotecontrol/remotecontrolsettings.cpp b/plugins/feature/remotecontrol/remotecontrolsettings.cpp new file mode 100644 index 000000000..37f1819fc --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolsettings.cpp @@ -0,0 +1,360 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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 "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "remotecontrolsettings.h" + +RemoteControlSettings::RemoteControlSettings() : + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void RemoteControlSettings::resetToDefaults() +{ + m_updatePeriod = 1.0f; + m_tpLinkUsername = ""; + m_tpLinkPassword = ""; + m_homeAssistantToken = ""; + m_homeAssistantHost = "http://homeassistant.local:8123"; + m_visaResourceFilter = ""; + m_visaLogIO = false; + m_chartHeightFixed = false; + m_chartHeightPixels = 130; + + m_title = "Remote Control"; + 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; + m_workspaceIndex = 0; +} + +QByteArray RemoteControlSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeFloat(1, m_updatePeriod); + s.writeString(2, m_tpLinkUsername); + s.writeString(3, m_tpLinkPassword); + s.writeString(4, m_homeAssistantToken); + s.writeString(5, m_homeAssistantHost); + s.writeString(6, m_visaResourceFilter); + s.writeBool(7, m_visaLogIO); + s.writeBool(10, m_chartHeightFixed); + s.writeS32(11, m_chartHeightPixels); + + s.writeBlob(19, serializeDeviceList(m_devices)); + + s.writeString(20, m_title); + s.writeU32(21, m_rgbColor); + s.writeBool(22, m_useReverseAPI); + s.writeString(23, m_reverseAPIAddress); + s.writeU32(24, m_reverseAPIPort); + s.writeU32(25, m_reverseAPIFeatureSetIndex); + s.writeU32(26, m_reverseAPIFeatureIndex); + + if (m_rollupState) { + s.writeBlob(27, m_rollupState->serialize()); + } + + s.writeS32(28, m_workspaceIndex); + + return s.final(); +} + +bool RemoteControlSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + QByteArray blob; + + d.readFloat(1, &m_updatePeriod, 1.0f); + d.readString(2, &m_tpLinkUsername, ""); + d.readString(3, &m_tpLinkPassword, ""); + d.readString(4, &m_homeAssistantToken, ""); + d.readString(5, &m_homeAssistantHost, "http://homeassistant.local:8123"); + d.readString(6, &m_visaResourceFilter, ""); + d.readBool(7, &m_visaLogIO, false); + + d.readBool(10, &m_chartHeightFixed, false); + d.readS32(11, &m_chartHeightPixels, 130); + + d.readBlob(19, &blob); + deserializeDeviceList(blob, m_devices); + + d.readString(20, &m_title, "Remote Control"); + d.readU32(21, &m_rgbColor, QColor(225, 25, 99).rgb()); + d.readBool(22, &m_useReverseAPI, false); + d.readString(23, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(24, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(25, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(26, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(27, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(28, &m_workspaceIndex, 0); + d.readBlob(29, &m_geometryBytes); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +QByteArray RemoteControlControl::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_id); + s.writeString(2, m_labelLeft); + s.writeString(3, m_labelRight); + + return s.final(); +} + +bool RemoteControlControl::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return false; + } + + if (d.getVersion() == 1) + { + d.readString(1, &m_id); + d.readString(2, &m_labelLeft); + d.readString(3, &m_labelRight); + return true; + } + else + { + return false; + } +} + +QByteArray RemoteControlSensor::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_id); + s.writeString(2, m_labelLeft); + s.writeString(3, m_labelRight); + s.writeString(4, m_format); + s.writeBool(5, m_plot); + + return s.final(); +} + +bool RemoteControlSensor::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return false; + } + + if (d.getVersion() == 1) + { + d.readString(1, &m_id); + d.readString(2, &m_labelLeft); + d.readString(3, &m_labelRight); + d.readString(4, &m_format); + d.readBool(5, &m_plot); + return true; + } + else + { + return false; + } +} + +QByteArray RemoteControlDevice::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(1, m_protocol); + s.writeString(2, m_label); + s.writeBlob(3, serializeControlList()); + s.writeBlob(4, serializeSensorList()); + s.writeBool(5, m_verticalControls); + s.writeBool(6, m_verticalSensors); + s.writeBool(7, m_commonYAxis); + s.writeBlob(8, m_info.serialize()); + + return s.final(); +} + +bool RemoteControlDevice::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return false; + } + + if (d.getVersion() == 1) + { + QByteArray blob; + + d.readString(1, &m_protocol); + d.readString(2, &m_label); + d.readBlob(3, &blob); + deserializeControlList(blob); + d.readBlob(4, &blob); + deserializeSensorList(blob); + d.readBool(5, &m_verticalControls, false); + d.readBool(6, &m_verticalSensors, true); + d.readBool(7, &m_commonYAxis); + d.readBlob(8, &blob); + m_info.deserialize(blob); + + return true; + } + else + { + return false; + } +} + +QDataStream& operator<<(QDataStream& out, const RemoteControlControl& control) +{ + out << control.serialize(); + return out; +} + +QDataStream& operator>>(QDataStream& in, RemoteControlControl& control) +{ + QByteArray data; + in >> data; + control.deserialize(data); + return in; +} + +QDataStream& operator<<(QDataStream& out, const RemoteControlSensor& sensor) +{ + out << sensor.serialize(); + return out; +} + +QDataStream& operator>>(QDataStream& in, RemoteControlSensor& sensor) +{ + QByteArray data; + in >> data; + sensor.deserialize(data); + return in; +} + +QDataStream& operator<<(QDataStream& out, const RemoteControlDevice* device) +{ + out << device->serialize(); + return out; +} + +QDataStream& operator>>(QDataStream& in, RemoteControlDevice*& device) +{ + device = new RemoteControlDevice(); + QByteArray data; + in >> data; + device->deserialize(data); + return in; +} + +QByteArray RemoteControlDevice::serializeControlList() const +{ + QByteArray data; + QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly); + (*stream) << m_controls; + delete stream; + return data; +} + +void RemoteControlDevice::deserializeControlList(const QByteArray& data) +{ + QDataStream *stream = new QDataStream(data); + (*stream) >> m_controls; + delete stream; +} + +QByteArray RemoteControlDevice::serializeSensorList() const +{ + QByteArray data; + QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly); + (*stream) << m_sensors; + delete stream; + return data; +} + +void RemoteControlDevice::deserializeSensorList(const QByteArray& data) +{ + QDataStream *stream = new QDataStream(data); + (*stream) >> m_sensors; + delete stream; +} + +QByteArray RemoteControlSettings::serializeDeviceList(const QList& devices) const +{ + QByteArray data; + QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly); + (*stream) << devices; + delete stream; + return data; +} + +void RemoteControlSettings::deserializeDeviceList(const QByteArray& data, QList& devices) +{ + QDataStream *stream = new QDataStream(data); + (*stream) >> devices; + delete stream; +} diff --git a/plugins/feature/remotecontrol/remotecontrolsettings.h b/plugins/feature/remotecontrol/remotecontrolsettings.h new file mode 100644 index 000000000..57c3ded38 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolsettings.h @@ -0,0 +1,129 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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_REMOTECONTROLSETTINGS_H_ +#define INCLUDE_FEATURE_REMOTECONTROLSETTINGS_H_ + +#include +#include + +#include "util/message.h" +#include "util/iot/device.h" + +class Serializable; + +struct RemoteControlControl { + QString m_id; + QString m_labelLeft; + QString m_labelRight; + + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +struct RemoteControlSensor { + QString m_id; + QString m_labelLeft; + QString m_labelRight; + QString m_format; + bool m_plot; + + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +struct RemoteControlDevice { + QString m_protocol; // TPLink, HomeAssistant, VISA + QString m_label; // Label to display as device name + QList m_controls; // Enabled controls + QList m_sensors; // Enabled sensors + bool m_verticalControls; + bool m_verticalSensors; + bool m_commonYAxis; // Whether multiple series on chart should share same axis + DeviceDiscoverer::DeviceInfo m_info; + RemoteControlDevice() : + m_verticalControls(false), + m_verticalSensors(true), + m_commonYAxis(false) + { + } + RemoteControlControl *getControl(const QString &id) + { + for (int i = 0; i < m_controls.size(); i++) + { + if (m_controls[i].m_id == id) { + return &m_controls[i]; + } + } + return nullptr; + } + RemoteControlSensor *getSensor(const QString &id) + { + for (int i = 0; i < m_sensors.size(); i++) + { + if (m_sensors[i].m_id == id) { + return &m_sensors[i]; + } + } + return nullptr; + } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +protected: + QByteArray serializeControlList() const; + void deserializeControlList(const QByteArray& data); + QByteArray serializeSensorList() const; + void deserializeSensorList(const QByteArray& data); +}; + +struct RemoteControlSettings +{ + float m_updatePeriod; //!< Period between device state updates + QString m_tpLinkUsername; + QString m_tpLinkPassword; + QString m_homeAssistantToken; + QString m_homeAssistantHost; + QString m_visaResourceFilter; + bool m_visaLogIO; + bool m_chartHeightFixed; //!< Whether chart heights should be fixed (to m_chartHeightPixels) or allowed to expand to use available space + int m_chartHeightPixels; //!< Chart height in pixels when fixed + + QList m_devices; + + 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; + + RemoteControlSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + + QByteArray serializeDeviceList(const QList& devices) const; + void deserializeDeviceList(const QByteArray& data, QList& devices); +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLSETTINGS_H_ diff --git a/plugins/feature/remotecontrol/remotecontrolsettingsdialog.cpp b/plugins/feature/remotecontrol/remotecontrolsettingsdialog.cpp new file mode 100644 index 000000000..68040e6f4 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolsettingsdialog.cpp @@ -0,0 +1,237 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "remotecontrolsettingsdialog.h" + +#include "channel/channelwebapiutils.h" + +RemoteControlSettingsDialog::RemoteControlSettingsDialog(RemoteControlSettings *settings, QWidget* parent) : + QDialog(parent), + m_settings(settings), + ui(new Ui::RemoteControlSettingsDialog) +{ + ui->setupUi(this); + resizeTable(); + ui->tpLinkUsername->setText(m_settings->m_tpLinkUsername); + ui->tpLinkPassword->setText(m_settings->m_tpLinkPassword); + ui->homeAssistantToken->setText(m_settings->m_homeAssistantToken); + ui->homeAssistantHost->setText(m_settings->m_homeAssistantHost); + ui->visaResourceFilter->setText(m_settings->m_visaResourceFilter); + ui->visaLogIO->setChecked(m_settings->m_visaLogIO); + ui->updatePeriod->setValue(m_settings->m_updatePeriod); + ui->chartVerticalPolicy->setCurrentIndex((int)m_settings->m_chartHeightFixed); + ui->chartHeight->setValue(m_settings->m_chartHeightPixels); + + connect(ui->devices->selectionModel(), &QItemSelectionModel::selectionChanged, this, &RemoteControlSettingsDialog::devicesSelectionChanged); + + updateTable(); + for (auto device : m_settings->m_devices) { + m_devices.append(new RemoteControlDevice(*device)); + } +} + +RemoteControlSettingsDialog::~RemoteControlSettingsDialog() +{ + qDeleteAll(m_devices); + m_devices.clear(); + delete ui; +} + +void RemoteControlSettingsDialog::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + int row = ui->devices->rowCount(); + ui->devices->setRowCount(row + 1); + ui->devices->setItem(row, COL_LABEL, new QTableWidgetItem("A short label")); + ui->devices->setItem(row, COL_NAME, new QTableWidgetItem("A reasonably long name")); + ui->devices->setItem(row, COL_MODEL, new QTableWidgetItem("A long model name to display")); + ui->devices->setItem(row, COL_SERVICE, new QTableWidgetItem("Home Assistant")); + ui->devices->resizeColumnsToContents(); + ui->devices->removeRow(row); +} + +void RemoteControlSettingsDialog::addToTable(int row, RemoteControlDevice *device) +{ + QTableWidgetItem *item; + + item = new QTableWidgetItem(device->m_label); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->devices->setItem(row, COL_LABEL, item); + + item = new QTableWidgetItem(device->m_info.m_name); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->devices->setItem(row, COL_NAME, item); + + item = new QTableWidgetItem(device->m_info.m_model); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->devices->setItem(row, COL_MODEL, item); + + item = new QTableWidgetItem(device->m_protocol); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ui->devices->setItem(row, COL_SERVICE, item); +} + +void RemoteControlSettingsDialog::updateTable() +{ + int row = 0; + ui->devices->setSortingEnabled(false); + ui->devices->setRowCount(m_settings->m_devices.size()); + for (auto device : m_settings->m_devices) + { + addToTable(row, device); + row++; + } + ui->devices->setSortingEnabled(true); +} + +void RemoteControlSettingsDialog::accept() +{ + QDialog::accept(); + m_settings->m_tpLinkUsername = ui->tpLinkUsername->text(); + m_settings->m_tpLinkPassword = ui->tpLinkPassword->text(); + m_settings->m_homeAssistantToken = ui->homeAssistantToken->text(); + m_settings->m_homeAssistantHost = ui->homeAssistantHost->text(); + m_settings->m_visaResourceFilter = ui->visaResourceFilter->text(); + m_settings->m_visaLogIO = ui->visaLogIO->isChecked(); + m_settings->m_updatePeriod = ui->updatePeriod->value(); + m_settings->m_chartHeightFixed = ui->chartVerticalPolicy->currentIndex() == 1; + m_settings->m_chartHeightPixels = ui->chartHeight->value(); + + qDeleteAll(m_settings->m_devices); + m_settings->m_devices.clear(); + m_settings->m_devices = m_devices; + m_devices.clear(); // So destructor doesn't delete them +} + +void RemoteControlSettingsDialog::on_devices_cellDoubleClicked(int row, int column) +{ + on_edit_clicked(); +} + +void RemoteControlSettingsDialog::devicesSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + bool enabled = selected.indexes().size() > 0; + ui->remove->setEnabled(enabled); + ui->edit->setEnabled(enabled); + ui->deviceUp->setEnabled(enabled); + ui->deviceDown->setEnabled(enabled); +} + +void RemoteControlSettingsDialog::on_add_clicked() +{ + RemoteControlDevice *device = new RemoteControlDevice(); + RemoteControlDeviceDialog dialog(m_settings, device); + if (dialog.exec() == QDialog::Accepted) + { + int row = ui->devices->rowCount(); + ui->devices->setRowCount(row + 1); + addToTable(row, device); + m_devices.append(device); + } + else + { + delete device; + } +} + +void RemoteControlSettingsDialog::on_remove_clicked() +{ + QList items = ui->devices->selectedItems(); + if (items.size() > 0) + { + int row = items[0]->row(); + if (row >= 0) + { + ui->devices->removeRow(row); + delete m_devices.takeAt(row); + } + } +} + +void RemoteControlSettingsDialog::on_edit_clicked() +{ + QList items = ui->devices->selectedItems(); + if (items.size() > 0) + { + int row = items[0]->row(); + if (row >= 0) + { + RemoteControlDevice *device = m_devices[row]; + RemoteControlDeviceDialog dialog(m_settings, device); + if (dialog.exec() == QDialog::Accepted) + { + ui->devices->item(row, COL_LABEL)->setText(device->m_label); + ui->devices->item(row, COL_NAME)->setText(device->m_info.m_name); + ui->devices->item(row, COL_MODEL)->setText(device->m_info.m_model); + ui->devices->item(row, COL_SERVICE)->setText(device->m_protocol); + } + } + } +} + +void RemoteControlSettingsDialog::on_deviceUp_clicked() +{ + QList items = ui->devices->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = items[i]->row(); + int col = items[i]->column(); + if (row > 0) + { + QTableWidgetItem *item1 = ui->devices->takeItem(row, col); + QTableWidgetItem *item2 = ui->devices->takeItem(row - 1, col); + ui->devices->setItem(row - 1, col, item1); + ui->devices->setItem(row, col, item2); + if (i == items.size() - 1) + { + ui->devices->setCurrentItem(items[i]); + m_devices.swapItemsAt(row, row - 1); + } + } + } +} + +void RemoteControlSettingsDialog::on_deviceDown_clicked() +{ + QList items = ui->devices->selectedItems(); + for (int i = 0; i < items.size(); i++) + { + int row = items[i]->row(); + int col = items[i]->column(); + if (row < ui->devices->rowCount() - 1) + { + QTableWidgetItem *item1 = ui->devices->takeItem(row, col); + QTableWidgetItem *item2 = ui->devices->takeItem(row + 1, col); + ui->devices->setItem(row + 1, col, item1); + ui->devices->setItem(row, col, item2); + if (i == items.size() - 1) + { + ui->devices->setCurrentItem(items[i]); + m_devices.swapItemsAt(row, row + 1); + } + } + } +} + +void RemoteControlSettingsDialog::on_chartVerticalPolicy_currentIndexChanged(int index) +{ + bool enabled = index == 1; + ui->chartHeightLabel->setEnabled(enabled); + ui->chartHeight->setEnabled(enabled); +} diff --git a/plugins/feature/remotecontrol/remotecontrolsettingsdialog.h b/plugins/feature/remotecontrol/remotecontrolsettingsdialog.h new file mode 100644 index 000000000..309d54b3d --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolsettingsdialog.h @@ -0,0 +1,60 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_REMOTECONTROLSETTINGSDIALOG_H +#define INCLUDE_FEATURE_REMOTECONTROLSETTINGSDIALOG_H + +#include "ui_remotecontrolsettingsdialog.h" +#include "remotecontrolsettings.h" +#include "remotecontroldevicedialog.h" + +class RemoteControlSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit RemoteControlSettingsDialog(RemoteControlSettings *settings, QWidget* parent = 0); + ~RemoteControlSettingsDialog(); + +private slots: + void accept(); + void on_devices_cellDoubleClicked(int row, int column); + void devicesSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + void on_add_clicked(); + void on_remove_clicked(); + void on_edit_clicked(); + void on_deviceUp_clicked(); + void on_deviceDown_clicked(); + void on_chartVerticalPolicy_currentIndexChanged(int index); + +private: + void resizeTable(); + void addToTable(int row, RemoteControlDevice *device); + void updateTable(); + + Ui::RemoteControlSettingsDialog* ui; + RemoteControlSettings *m_settings; + QList m_devices; + + enum DeviceCol { + COL_LABEL, + COL_NAME, + COL_MODEL, + COL_SERVICE, + }; +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLSETTINGSDIALOG_H diff --git a/plugins/feature/remotecontrol/remotecontrolsettingsdialog.ui b/plugins/feature/remotecontrol/remotecontrolsettingsdialog.ui new file mode 100644 index 000000000..04ec70e96 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolsettingsdialog.ui @@ -0,0 +1,474 @@ + + + RemoteControlSettingsDialog + + + + 0 + 0 + 592 + 640 + + + + + Liberation Sans + 9 + + + + Remote Control Settings + + + + + + + 0 + + + + + + + 0 + + + + Devices + + + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + Label + + + Label used for the device in the GUI + + + + + Device + + + Device name + + + + + Model + + + Device model + + + + + Protocol + + + Protocol used to communicate with the device + + + + + + + + + + Add... + + + + + + + false + + + Remove + + + + + + + false + + + Edit... + + + + + + + false + + + Move up + + + + + + + :/arrow_up.png:/arrow_up.png + + + + + + + false + + + Move down + + + + + + + :/arrow_down.png:/arrow_down.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Settings + + + + + + TP-Link + + + + + + + 100 + 0 + + + + Username + + + + + + + Username. Typically your email address + + + + + + + Password + + + + + + + Password + + + QLineEdit::PasswordEchoOnEdit + + + + + + + + + + Home Assistant + + + + + + + 100 + 0 + + + + Access Token + + + + + + + Host + + + + + + + API access token. Can be generated on your profile page: http://homeassistant.local:8123/profile + + + + + + + Hostname of computer running Home Assistant. Typically http://homeassistant.local:8123/profile + + + + + + + + + + VISA + + + + + + + 100 + 0 + + + + Resource filter + + + + + + + Log IO + + + + + + + Regular expression of VISA resources not to connect to + + + + + + + When checked, VISA input and output is written to SDRangel log + + + + + + + + + + + + + Devices + + + + + + + 100 + 0 + + + + Update period (s) + + + + + + + Period in seconds between requests to update device state + + + 0.010000000000000 + + + 1000000.000000000000000 + + + 1.000000000000000 + + + + + + + + + + Charts + + + + + + + 100 + 0 + + + + Height + + + + + + + + Expanding + + + + + Fixed + + + + + + + + false + + + Height (pixels) + + + + + + + false + + + 50 + + + 2000 + + + 130 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + rejected() + RemoteControlSettingsDialog + reject() + + + 295 + 619 + + + 295 + 319 + + + + + buttonBox + accepted() + RemoteControlSettingsDialog + accept() + + + 295 + 619 + + + 295 + 319 + + + + + diff --git a/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.cpp b/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.cpp new file mode 100644 index 000000000..289a7b511 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.cpp @@ -0,0 +1,193 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "remotecontrolvisacontroldialog.h" + +#include +#include + +RemoteControlVISAControlDialog::RemoteControlVISAControlDialog(RemoteControlSettings *settings, RemoteControlDevice *device, VISADevice::VISAControl *control, bool add, QWidget* parent) : + QDialog(parent), + ui(new Ui::RemoteControlVISAControlDialog), + m_settings(settings), + m_device(device), + m_control(control), + m_add(add), + m_userHasEditedId(false) +{ + ui->setupUi(this); + ui->name->setText(m_control->m_name); + ui->id->setText(m_control->m_id); + DeviceDiscoverer::Type type = m_control->m_type == DeviceDiscoverer::AUTO ? DeviceDiscoverer::BOOL : m_control->m_type; + ui->type->setCurrentText(DeviceDiscoverer::m_typeStrings[(int)type]); + ui->widgetType->setCurrentText(DeviceDiscoverer::m_widgetTypeStrings[(int)m_control->m_widgetType]); + ui->min->setValue(m_control->m_min); + ui->max->setValue(m_control->m_max); + ui->scale->setValue(m_control->m_scale); + ui->precision->setValue(m_control->m_precision); + ui->values->insertItems(0, m_control->m_values); + if (m_control->m_values.size() > 0) { + ui->label->setText(m_control->m_values[0]); + } + ui->units->setText(m_control->m_units); + ui->setState->setPlainText(m_control->m_setState); + ui->getState->setPlainText(m_control->m_getState); + on_type_currentIndexChanged(ui->type->currentIndex()); + validate(); +} + +RemoteControlVISAControlDialog::~RemoteControlVISAControlDialog() +{ + delete ui; +} + +void RemoteControlVISAControlDialog::accept() +{ + QDialog::accept(); + + m_control->m_name = ui->name->text(); + m_control->m_id = ui->id->text(); + m_control->m_type = (DeviceDiscoverer::Type)DeviceDiscoverer::m_typeStrings.indexOf(ui->type->currentText()); + m_control->m_widgetType = (DeviceDiscoverer::WidgetType)DeviceDiscoverer::m_widgetTypeStrings.indexOf(ui->widgetType->currentText()); + m_control->m_min = ui->min->value(); + m_control->m_max = ui->max->value(); + m_control->m_scale = ui->scale->value(); + m_control->m_precision = ui->precision->value(); + m_control->m_values.clear(); + if (m_control->m_type == DeviceDiscoverer::BUTTON) + { + m_control->m_values.append(ui->label->text()); + } + else + { + for (int i = 0; i < ui->values->count(); i++) { + m_control->m_values.append(ui->values->itemText(i)); + } + } + m_control->m_units = ui->units->text(); + m_control->m_setState = ui->setState->toPlainText(); + m_control->m_getState = ui->getState->toPlainText(); +} + +void RemoteControlVISAControlDialog::on_type_currentIndexChanged(int index) +{ + DeviceDiscoverer::Type type; + if (index < 0) { + type = DeviceDiscoverer::BOOL; + } else { + type = (DeviceDiscoverer::Type)DeviceDiscoverer::m_typeStrings.indexOf(ui->type->currentText()); + } + bool minMaxVisible = true; // Default to FLOAT + bool precisionVisible = true; + bool listVisible = false; + bool labelVisible = false; + int decimals = 3; + if (type == DeviceDiscoverer::BOOL) + { + minMaxVisible = false; + precisionVisible = false; + } + else if (type == DeviceDiscoverer::INT) + { + decimals = 0; + precisionVisible = false; + } + else if (type == DeviceDiscoverer::STRING) + { + minMaxVisible = false; + precisionVisible = false; + } + else if (type == DeviceDiscoverer::LIST) + { + minMaxVisible = false; + precisionVisible = false; + listVisible = true; + } + else if (type == DeviceDiscoverer::BUTTON) + { + minMaxVisible = false; + precisionVisible = false; + labelVisible = true; + } + ui->widgetType->setVisible(precisionVisible); + ui->minLabel->setVisible(minMaxVisible); + ui->min->setVisible(minMaxVisible); + ui->min->setDecimals(decimals); + ui->maxLabel->setVisible(minMaxVisible); + ui->max->setVisible(minMaxVisible); + ui->max->setDecimals(decimals); + ui->scaleLabel->setVisible(precisionVisible); + ui->scale->setVisible(precisionVisible); + ui->precisionLabel->setVisible(precisionVisible); + ui->precision->setVisible(precisionVisible); + ui->values->setVisible(listVisible); + ui->remove->setVisible(listVisible); + ui->labelLabel->setVisible(labelVisible); + ui->label->setVisible(labelVisible); + + bool getStateEnabled = type != DeviceDiscoverer::BUTTON; + ui->getStateLabel->setEnabled(getStateEnabled); + ui->getState->setEnabled(getStateEnabled); +} + +void RemoteControlVISAControlDialog::on_name_textChanged(const QString &text) +{ + if (m_add && !m_userHasEditedId) + { + // Set Id to lower case version of name + ui->id->setText(text.trimmed().toLower().replace(" ", "")); + } +} + +void RemoteControlVISAControlDialog::on_id_textChanged(const QString &text) +{ + validate(); +} + +void RemoteControlVISAControlDialog::on_id_textEdited(const QString &text) +{ + m_userHasEditedId = true; +} + +void RemoteControlVISAControlDialog::on_setState_textChanged() +{ + validate(); +} + +void RemoteControlVISAControlDialog::on_remove_clicked() +{ + ui->values->removeItem(ui->values->currentIndex()); +} + +void RemoteControlVISAControlDialog::validate() +{ + bool valid = true; + + QString id = ui->id->text().trimmed(); + if (id.isEmpty()) { + valid = false; + } else if (m_add) { + if (m_device->getControl(id)) { + valid = false; + } + } + if (ui->setState->toPlainText().trimmed().isEmpty()) { + valid = false; + } + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); +} diff --git a/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.h b/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.h new file mode 100644 index 000000000..0e28ad42a --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.h @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_REMOTECONTROLVISACONTROLDIALOG_H +#define INCLUDE_FEATURE_REMOTECONTROLVISACONTROLDIALOG_H + +#include "ui_remotecontrolvisacontroldialog.h" +#include "remotecontrolsettings.h" +#include "util/iot/visa.h" + +class RemoteControlVISAControlDialog : public QDialog { + Q_OBJECT + +public: + explicit RemoteControlVISAControlDialog(RemoteControlSettings *settings, RemoteControlDevice *device, VISADevice::VISAControl *control, bool add, QWidget* parent = 0); + ~RemoteControlVISAControlDialog(); + +private slots: + void accept(); + void on_name_textChanged(const QString &text); + void on_type_currentIndexChanged(int index); + void on_id_textChanged(const QString &text); + void on_id_textEdited(const QString &text); + void on_setState_textChanged(); + void on_remove_clicked(); + +private: + + Ui::RemoteControlVISAControlDialog* ui; + RemoteControlSettings *m_settings; + RemoteControlDevice *m_device; + VISADevice::VISAControl *m_control; + bool m_add; + bool m_userHasEditedId; + + void validate(); + +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLVISACONTROLDIALOG_H diff --git a/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.ui b/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.ui new file mode 100644 index 000000000..b92ec6b5a --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolvisacontroldialog.ui @@ -0,0 +1,374 @@ + + + RemoteControlVISAControlDialog + + + + 0 + 0 + 821 + 540 + + + + + Liberation Sans + 9 + + + + VISA Control + + + + + + Controls + + + false + + + + + + Name + + + + + + + Name for this control + + + + + + + ID + + + + + + + Unique identifier for this control + + + + + + + Type + + + + + + + + + + 100 + 0 + + + + + Boolean + + + + + Integer + + + + + Float + + + + + String + + + + + List + + + + + Button + + + + + + + + Type of widget to display value + + + + Spin box + + + + + Dial + + + + + Slider + + + + + + + + Min + + + + + + + Minimum value + + + 3 + + + -9999999999999999932209486743616279764617084419440640.000000000000000 + + + 10000000000000000735758738477112498397576062152177456799245857901351759143802190202050679656153088.000000000000000 + + + + + + + Max + + + + + + + Maximum value + + + 3 + + + -999999999999999983222784.000000000000000 + + + 100000000000000007629769841091887003294964970946560.000000000000000 + + + + + + + Scale + + + + + + + Scale factor applied to value before sending to instrument + + + 3 + + + 1000000000000000.000000000000000 + + + + + + + Precision + + + + + + + Precision (number of decimals) + + + 323 + + + 3 + + + + + + + Label + + + + + + + Button label + + + + + + + + 150 + 0 + + + + List of allowable values + + + true + + + + + + + Remove current entry from list + + + + + + + :/bin.png:/bin.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Units + + + + + + + Units + + + + + + + Set state + + + + + + + + + + Get state + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + name + id + type + min + max + precision + values + units + setState + getState + + + + + + + buttonBox + accepted() + RemoteControlVISAControlDialog + accept() + + + 295 + 619 + + + 295 + 319 + + + + + buttonBox + rejected() + RemoteControlVISAControlDialog + reject() + + + 295 + 619 + + + 295 + 319 + + + + + diff --git a/plugins/feature/remotecontrol/remotecontrolvisasensordialog.cpp b/plugins/feature/remotecontrol/remotecontrolvisasensordialog.cpp new file mode 100644 index 000000000..38792db11 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolvisasensordialog.cpp @@ -0,0 +1,98 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "remotecontrolvisasensordialog.h" + +#include +#include + +RemoteControlVISASensorDialog::RemoteControlVISASensorDialog(RemoteControlSettings *settings, RemoteControlDevice *device, VISADevice::VISASensor *sensor, bool add, QWidget* parent) : + QDialog(parent), + ui(new Ui::RemoteControlVISASensorDialog), + m_settings(settings), + m_device(device), + m_sensor(sensor), + m_add(add), + m_userHasEditedId(false) +{ + ui->setupUi(this); + ui->name->setText(m_sensor->m_name); + ui->id->setText(m_sensor->m_id); + ui->type->setCurrentText(DeviceDiscoverer::m_typeStrings[(int)m_sensor->m_type]); + ui->units->setText(m_sensor->m_units); + ui->getState->setPlainText(m_sensor->m_getState); + validate(); +} + +RemoteControlVISASensorDialog::~RemoteControlVISASensorDialog() +{ + delete ui; +} + +void RemoteControlVISASensorDialog::accept() +{ + QDialog::accept(); + + m_sensor->m_name = ui->name->text(); + m_sensor->m_id = ui->id->text(); + m_sensor->m_type = (DeviceDiscoverer::Type)DeviceDiscoverer::m_typeStrings.indexOf(ui->type->currentText()); + m_sensor->m_units = ui->units->text(); + m_sensor->m_getState = ui->getState->toPlainText(); +} + +void RemoteControlVISASensorDialog::on_name_textChanged(const QString &text) +{ + if (m_add && !m_userHasEditedId) + { + // Set Id to lower case version of name + ui->id->setText(text.trimmed().toLower().replace(" ", "")); + } +} + +void RemoteControlVISASensorDialog::on_id_textChanged(const QString &text) +{ + validate(); +} + +void RemoteControlVISASensorDialog::on_id_textEdited(const QString &text) +{ + m_userHasEditedId = true; +} + +void RemoteControlVISASensorDialog::on_getState_textChanged() +{ + validate(); +} + +void RemoteControlVISASensorDialog::validate() +{ + bool valid = true; + + QString id = ui->id->text().trimmed(); + if (id.isEmpty()) { + valid = false; + } else if (m_add) { + if (m_device->getSensor(id)) { + valid = false; + } + } + if (ui->getState->toPlainText().trimmed().isEmpty()) { + valid = false; + } + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); +} diff --git a/plugins/feature/remotecontrol/remotecontrolvisasensordialog.h b/plugins/feature/remotecontrol/remotecontrolvisasensordialog.h new file mode 100644 index 000000000..307514cb5 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolvisasensordialog.h @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_REMOTECONTROLVISASENSORDIALOG_H +#define INCLUDE_FEATURE_REMOTECONTROLVISASENSORDIALOG_H + +#include "ui_remotecontrolvisasensordialog.h" +#include "remotecontrolsettings.h" +#include "util/iot/visa.h" + +class RemoteControlVISASensorDialog : public QDialog { + Q_OBJECT + +public: + explicit RemoteControlVISASensorDialog(RemoteControlSettings *settings, RemoteControlDevice *device, VISADevice::VISASensor *sensor, bool add, QWidget* parent = 0); + ~RemoteControlVISASensorDialog(); + +private slots: + void accept(); + void on_name_textChanged(const QString &text); + void on_id_textChanged(const QString &text); + void on_id_textEdited(const QString &text); + void on_getState_textChanged(); + +private: + + Ui::RemoteControlVISASensorDialog* ui; + RemoteControlSettings *m_settings; + RemoteControlDevice *m_device; + VISADevice::VISASensor *m_sensor; + bool m_add; + bool m_userHasEditedId; + + void validate(); + +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLVISASENSORDIALOG_H diff --git a/plugins/feature/remotecontrol/remotecontrolvisasensordialog.ui b/plugins/feature/remotecontrol/remotecontrolvisasensordialog.ui new file mode 100644 index 000000000..7cb8e7112 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolvisasensordialog.ui @@ -0,0 +1,173 @@ + + + RemoteControlVISASensorDialog + + + + 0 + 0 + 800 + 350 + + + + + Liberation Sans + 9 + + + + VISA Sensor + + + + + + false + + + + 0 + + + + + Type + + + + + + + Get state + + + + + + + VISA/SCPI command to get the state for this sensor + + + + + + + Name of this sensor + + + + + + + Name + + + + + + + + Boolean + + + + + Float + + + + + String + + + + + + + + ID + + + + + + + Unique (per device) identifier for this sensor + + + + + + + Units + + + + + + + The units of the sensor state + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + name + id + type + units + getState + + + + + + + buttonBox + accepted() + RemoteControlVISASensorDialog + accept() + + + 356 + 330 + + + 356 + 175 + + + + + buttonBox + rejected() + RemoteControlVISASensorDialog + reject() + + + 356 + 330 + + + 356 + 175 + + + + + diff --git a/plugins/feature/remotecontrol/remotecontrolworker.cpp b/plugins/feature/remotecontrol/remotecontrolworker.cpp new file mode 100644 index 000000000..405f85bb9 --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolworker.cpp @@ -0,0 +1,234 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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/iot/device.h" + +#include "remotecontrol.h" +#include "remotecontrolworker.h" + +RemoteControlWorker::RemoteControlWorker() : + m_msgQueueToFeature(nullptr), + m_msgQueueToGUI(nullptr), + m_timer(this) +{ + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + connect(&m_timer, SIGNAL(timeout()), this, SLOT(update())); +} + +RemoteControlWorker::~RemoteControlWorker() +{ + m_timer.stop(); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_inputMessageQueue.clear(); + qDeleteAll(m_devices); + m_devices.clear(); +} + +void RemoteControlWorker::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +Device *RemoteControlWorker::getDevice(const QString &protocol, const QString deviceId) const +{ + for (auto device : m_devices) + { + if ((device->getProtocol() == protocol) && (device->getDeviceId() == deviceId)) { + return device; + } + } + return nullptr; +} + +bool RemoteControlWorker::handleMessage(const Message& cmd) +{ + if (RemoteControl::MsgConfigureRemoteControl::match(cmd)) + { + RemoteControl::MsgConfigureRemoteControl& cfg = (RemoteControl::MsgConfigureRemoteControl&) cmd; + + applySettings(cfg.getSettings(), cfg.getForce()); + return true; + } + else if (RemoteControl::MsgStartStop::match(cmd)) + { + RemoteControl::MsgStartStop& cfg = (RemoteControl::MsgStartStop&) cmd; + + // Start/stop automatic state updates + if (cfg.getStartStop()) { + m_timer.start(m_settings.m_updatePeriod * 1000.0); + } else { + m_timer.stop(); + } + return true; + } + else if (RemoteControl::MsgDeviceGetState::match(cmd)) + { + // Get state for all devices + update(); + return true; + } + else if (RemoteControl::MsgDeviceSetState::match(cmd)) + { + RemoteControl::MsgDeviceSetState& msg = (RemoteControl::MsgDeviceSetState&) cmd; + QString protocol = msg.getProtocol(); + QString deviceId = msg.getDeviceId(); + Device *device = getDevice(protocol, deviceId); + if (device) + { + QString id = msg.getId(); + QVariant variant = msg.getValue(); + + if (variant.type() == QMetaType::Bool) + { + bool b = variant.toBool(); + device->setState(id, b); + } + else if (variant.type() == QMetaType::Int) + { + int i = variant.toInt(); + device->setState(id, i); + } + else if (variant.type() == QMetaType::Float) + { + float f = variant.toFloat(); + device->setState(id, f); + } + else if (variant.type() == QMetaType::QString) + { + QString s = variant.toString(); + device->setState(id, s); + } + else + { + qDebug() << "RemoteControlWorker::handleMessage: Unsupported type: " << variant.typeName(); + } + } + else + { + qDebug() << "RemoteControlWorker::handleMessage: Device not found: " << protocol << " " << deviceId; + } + return true; + } + else + { + return false; + } +} + +void RemoteControlWorker::applySettings(const RemoteControlSettings& settings, bool force) +{ + qDebug() << "RemoteControlWorker::applySettings:" + << " m_updatePeriod: " << settings.m_updatePeriod + << " m_visaLogIO: " << settings.m_visaLogIO + << " force: " << force; + + if ((settings.m_updatePeriod != m_settings.m_updatePeriod) || force) { + m_timer.setInterval(settings.m_updatePeriod * 1000.0); + } + + // Always recreate all devices, as DeviceInfo may have changed + qDeleteAll(m_devices); + m_devices.clear(); + for (auto rcDevice : settings.m_devices) + { + QHash devSettings; + if (rcDevice->m_protocol == "TPLink") + { + devSettings.insert("username", settings.m_tpLinkUsername); + devSettings.insert("password", settings.m_tpLinkPassword); + } + else if (rcDevice->m_protocol == "HomeAssistant") + { + devSettings.insert("apiKey", settings.m_homeAssistantToken); + devSettings.insert("url", settings.m_homeAssistantHost); + } + else if (rcDevice->m_protocol == "VISA") + { + devSettings.insert("logIO", settings.m_visaLogIO); + } + devSettings.insert("deviceId", rcDevice->m_info.m_id); + QStringList controlIDs; + for (auto control : rcDevice->m_controls) { + controlIDs.append(control.m_id); + } + QStringList sensorIDs; + for (auto sensor : rcDevice->m_sensors) { + sensorIDs.append(sensor.m_id); + } + devSettings.insert("controlIds", controlIDs); + devSettings.insert("sensorIds", sensorIDs); + Device *device = Device::create(devSettings, rcDevice->m_protocol, &rcDevice->m_info); + m_devices.append(device); + connect(device, &Device::deviceUpdated, this, &RemoteControlWorker::deviceUpdated); + connect(device, &Device::deviceUnavailable, this, &RemoteControlWorker::deviceUnavailable); + connect(device, &Device::error, this, &RemoteControlWorker::deviceError); + } + + m_settings = settings; +} + +void RemoteControlWorker::update() +{ + for (auto device : m_devices) { + device->getState(); + } +} + +void RemoteControlWorker::deviceUpdated(QHash status) +{ + QObject *device = this->sender(); + + for (int i = 0; i < m_devices.size(); i++) + { + if (device == m_devices[i]) + { + if (getMessageQueueToGUI()) + { + getMessageQueueToGUI()->push(RemoteControl::MsgDeviceStatus::create(m_devices[i]->getProtocol(), + m_devices[i]->getDeviceId(), + status)); + } + } + } +} + +void RemoteControlWorker::deviceUnavailable() +{ + if (getMessageQueueToGUI()) + { + Device *device = qobject_cast(this->sender()); + + getMessageQueueToGUI()->push(RemoteControl::MsgDeviceUnavailable::create(device->getProtocol(), device->getDeviceId())); + } +} + +void RemoteControlWorker::deviceError(const QString &error) +{ + if (getMessageQueueToGUI()) { + getMessageQueueToGUI()->push(RemoteControl::MsgDeviceError::create(error)); + } +} diff --git a/plugins/feature/remotecontrol/remotecontrolworker.h b/plugins/feature/remotecontrol/remotecontrolworker.h new file mode 100644 index 000000000..34fabc98b --- /dev/null +++ b/plugins/feature/remotecontrol/remotecontrolworker.h @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2022 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_PERTESTERWORKER_H_ +#define INCLUDE_FEATURE_REMOTECONTROLWORKER_H_ + +#include +#include +#include + +#include "util/message.h" +#include "util/messagequeue.h" + +#include "remotecontrolsettings.h" + +class RemoteControlWorker : public QObject +{ + Q_OBJECT +public: + + RemoteControlWorker(); + ~RemoteControlWorker(); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + void setMessageQueueToFeature(MessageQueue *messageQueue) { m_msgQueueToFeature = messageQueue; } + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_msgQueueToGUI = messageQueue; } + +private: + + MessageQueue m_inputMessageQueue; + MessageQueue *m_msgQueueToFeature; + MessageQueue *m_msgQueueToGUI; + RemoteControlSettings m_settings; + bool m_running; + QTimer m_timer; + QList m_devices; + + bool handleMessage(const Message& cmd); + void applySettings(const RemoteControlSettings& settings, bool force = false); + MessageQueue *getMessageQueueToGUI() { return m_msgQueueToGUI; } + Device *getDevice(const QString &protocol, const QString deviceId) const; + +private slots: + void handleInputMessages(); + void update(); + void deviceUpdated(QHash status); + void deviceUnavailable(); + void deviceError(const QString &error); +}; + +#endif // INCLUDE_FEATURE_REMOTECONTROLWORKER_H_