1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2025-04-13 06:58:35 -04:00

Merge pull request from srcejon/remote_control

Remote Control Feature
This commit is contained in:
Edouard Griffiths 2022-09-17 11:21:24 +02:00 committed by GitHub
commit 262e932544
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 8866 additions and 51 deletions

View File

@ -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)

Binary file not shown.

After

(image error) Size: 42 KiB

Binary file not shown.

After

(image error) Size: 34 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

After

(image error) Size: 3.0 KiB

Binary file not shown.

After

(image error) Size: 24 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 11 KiB

View File

@ -73,3 +73,7 @@ endif()
if (ENABLE_FEATURE_AMBE AND LIBSERIALDV_FOUND)
add_subdirectory(ambe)
endif()
if (ENABLE_FEATURE_REMOTECONTROL)
add_subdirectory(remotecontrol)
endif()

View File

@ -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 $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
endif()

View File

@ -0,0 +1,161 @@
<h1>Remote Control Feature Plugin</h1>
<h2>Introduction</h2>
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)
<h2>Interface</h2>
![Remote Control feature settings](../../../doc/img/RemoteControl_plugin_settings.png)
<h3>1: Start/Stop</h3>
Starts or stops periodic reading of the state of all devices. The update period can be set in the Settings dialog (3).
<h3>2: Update State</h3>
Press to manually read the state of all devices. This can be used regardless of whether the plugin is started or stopped (1).
<h3>3: Display Settings Dialog</h3>
Pressing this button opens the Settings dialog.
<h3>4: Clear Chart Data</h3>
Pressing this button will clear all data from all charts.
<h3>5: Device GUIs</h3>
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.
<h2>Settings Dialog</h2>
<h3>Devices Tab</h3>
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.
<h3>Settings Tab</h3>
![Remote Control settings tab](../../../doc/img/RemoteControl_plugin_settings_tab.png)
<h4>TP-Link Settings</h4>
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 TP-Link Kasa Smart Home app.
<h4>Home Assistant Settings</h4>
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
<h4>VISA Settings</h4>
* 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.
<h4>Devices Settings</h4>
* Update period - Period in seconds between automatic updates of device control and sensor state.
<h4>Chart Settings</h4>
* 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 of each chart.
<h2>Device Dialog</h2>
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 with the units.
These fields can be changed by double clicking in the cell.
The Format column in the sensor table allows custom formatting of sensor values in the GUI.
printf format specifiers can be used:
* %i for integers,
* %e, %f and %g for real numbers (floats) and
* %s for strings.
By default, real numbers are displayed to 1 decimal place. To increase this to 3, you can use %.3f.
Checking the Plot column will result in a chart being drawn that plots sensor data versus 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 for most instruments. Some basic controls are included for Rigol DP832 and Siglent SSA3032X.
<h3>VISA Control Dialog</h3>
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 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 in the device. This is used to update the control in the GUI. If multiple queries are specified, the value is taken from the last response.
<h3>VISA Sensor Dialog</h3>
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.

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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<QString, QVariant> getStatus() const { return m_status; }
static MsgDeviceStatus* create(const QString &protocol, const QString &deviceId, const QHash<QString, QVariant> status) {
return new MsgDeviceStatus(protocol, deviceId, status);
}
protected:
QString m_protocol;
QString m_deviceId;
QHash<QString, QVariant> m_status;
MsgDeviceStatus(const QString &protocol, const QString &deviceId, const QHash<QString, QVariant> 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_

View File

@ -0,0 +1,636 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "remotecontroldevicedialog.h"
#include "remotecontrolvisasensordialog.h"
#include "remotecontrolvisacontroldialog.h"
#include <QDebug>
#include <QUrl>
#include <QMessageBox>
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)
{
(void)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)
{
(void)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<QString, QVariant> 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<DeviceDiscoverer::DeviceInfo> &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<DeviceDiscoverer::ControlInfo *>(control));
addControlRow(control->m_name, control->m_id, control->m_units);
}
else
{
delete control;
}
}
void RemoteControlDeviceDialog::on_controlEdit_clicked()
{
QList<QTableWidgetItem *> 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<VISADevice::VISAControl *>(info->getControl(id));
RemoteControlVISAControlDialog dialog(m_settings, m_rcDevice, control, false);
if (dialog.exec() == QDialog::Accepted)
{
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)
{
(void)row;
if ((ui->protocol->currentText() == "VISA") && (column <= COL_ID)) {
on_controlEdit_clicked();
}
}
void RemoteControlDeviceDialog::on_controlRemove_clicked()
{
QList<QTableWidgetItem *> 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<QTableWidgetItem *> 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()];
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
info->m_controls.swap(row, row - 1);
#else
info->m_controls.swapItemsAt(row, row - 1);
#endif
}
}
}
}
void RemoteControlDeviceDialog::on_controlDown_clicked()
{
QList<QTableWidgetItem *> 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()];
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
info->m_controls.swap(row, row + 1);
#else
info->m_controls.swapItemsAt(row, row + 1);
#endif
}
}
}
}
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<DeviceDiscoverer::SensorInfo *>(sensor));
addSensorRow(sensor->m_name, sensor->m_id, sensor->m_units);
}
else
{
delete sensor;
}
}
void RemoteControlDeviceDialog::on_sensorRemove_clicked()
{
QList<QTableWidgetItem *> 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<QTableWidgetItem *> 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<VISADevice::VISASensor *>(info->getSensor(id));
RemoteControlVISASensorDialog dialog(m_settings, m_rcDevice, sensor, false);
if (dialog.exec() == QDialog::Accepted)
{
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)
{
(void)row;
if ((ui->protocol->currentText() == "VISA") && (column <= COL_ID)) {
on_sensorEdit_clicked();
}
}
void RemoteControlDeviceDialog::on_sensorUp_clicked()
{
QList<QTableWidgetItem *> 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()];
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
info->m_sensors.swap(row, row - 1);
#else
info->m_sensors.swapItemsAt(row, row - 1);
#endif
}
}
}
}
void RemoteControlDeviceDialog::on_sensorDown_clicked()
{
QList<QTableWidgetItem *> 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()];
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
info->m_sensors.swap(row, row + 1);
#else
info->m_sensors.swapItemsAt(row, row + 1);
#endif
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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<DeviceDiscoverer::DeviceInfo> &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<DeviceDiscoverer::DeviceInfo> 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

View File

@ -0,0 +1,592 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RemoteControlDeviceDialog</class>
<widget class="QDialog" name="RemoteControlDeviceDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>700</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>Remote Control Device</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="flat">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="serviceLayout"/>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="protocolLabel">
<property name="toolTip">
<string>Protocol to connect to the device</string>
</property>
<property name="text">
<string>Protocol:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="protocol">
<item>
<property name="text">
<string>Select a protocol...</string>
</property>
</item>
<item>
<property name="text">
<string>TPLink</string>
</property>
</item>
<item>
<property name="text">
<string>HomeAssistant</string>
</property>
</item>
<item>
<property name="text">
<string>VISA</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="deviceLabel">
<property name="text">
<string>Device:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="modelLabel">
<property name="text">
<string>Model:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="device"/>
</item>
<item row="2" column="1">
<widget class="QLabel" name="model">
<property name="toolTip">
<string>Device model name</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelLabel">
<property name="text">
<string>Label:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="label">
<property name="toolTip">
<string>Label to display for this device in the UI</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="controlsGroup">
<property name="title">
<string>Controls</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="controlsHorizontalLayout">
<item>
<widget class="QTableWidget" name="controls">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Enable</string>
</property>
<property name="toolTip">
<string>Enable display of control in GUI</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
<property name="toolTip">
<string>Name of the control</string>
</property>
</column>
<column>
<property name="text">
<string>Units</string>
</property>
</column>
<column>
<property name="text">
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Left Label</string>
</property>
<property name="toolTip">
<string>Label to display to the left of this control in the UI</string>
</property>
</column>
<column>
<property name="text">
<string>Right Label</string>
</property>
<property name="toolTip">
<string>Label to display to the right of this control in the UI</string>
</property>
</column>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="controlAdd">
<property name="toolTip">
<string>Add a new control</string>
</property>
<property name="text">
<string>Add...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="controlRemove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Remove selected control</string>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="controlEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Edit selected control</string>
</property>
<property name="text">
<string>Edit...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="controlDown">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move down</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/arrow_down.png</normaloff>:/arrow_down.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="controlUp">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move up</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/arrow_up.png</normaloff>:/arrow_up.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="controlsForm">
<item row="0" column="0">
<widget class="QLabel" name="controlsLayoutLabel">
<property name="text">
<string>Layout:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="controlsLayout">
<property name="toolTip">
<string>How controls are laid out in the UI</string>
</property>
<item>
<property name="text">
<string>Horizontal</string>
</property>
</item>
<item>
<property name="text">
<string>Vertical</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="sensorsGroup">
<property name="title">
<string>Sensors</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="sensorsHorizontalLayout">
<item>
<widget class="QTableWidget" name="sensors">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Enable</string>
</property>
<property name="toolTip">
<string>Enable display of this sensor's value</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
<property name="toolTip">
<string>Sensor name</string>
</property>
</column>
<column>
<property name="text">
<string>Units</string>
</property>
<property name="toolTip">
<string>Units to display for this sensor in the UI</string>
</property>
</column>
<column>
<property name="text">
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Left Label</string>
</property>
<property name="toolTip">
<string>Label to display to the left of this sensor in the UI</string>
</property>
</column>
<column>
<property name="text">
<string>Right Label</string>
</property>
<property name="toolTip">
<string>Label to display to the right of this sensor in the UI</string>
</property>
</column>
<column>
<property name="text">
<string>Format</string>
</property>
<property name="toolTip">
<string>printf format string for formatting the sensor value as a decimal floating point value (E.g. %f %.1f %.3e) or as a string (%s)</string>
</property>
</column>
<column>
<property name="text">
<string>Plot</string>
</property>
<property name="toolTip">
<string>Plot sensor data on chart</string>
</property>
</column>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="sensorAdd">
<property name="text">
<string>Add...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sensorRemove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sensorEdit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sensorDown">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move down</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/arrow_down.png</normaloff>:/arrow_down.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="sensorUp">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move up</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/arrow_up.png</normaloff>:/arrow_up.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="sensorsForm">
<item row="1" column="0">
<widget class="QLabel" name="sensorsLayoutLabel">
<property name="text">
<string>Layout</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="sensorsLayout">
<property name="toolTip">
<string>How sensors are laid out in the UI</string>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Horizontal</string>
</property>
</item>
<item>
<property name="text">
<string>Vertical</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="yAxisLabel">
<property name="text">
<string>Y Axis</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="yAxis">
<property name="toolTip">
<string>Set whether each series of sensor data is plotted on a common Y axis or with individual axes</string>
</property>
<item>
<property name="text">
<string>Per-sensor</string>
</property>
</item>
<item>
<property name="text">
<string>Common</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>protocol</tabstop>
<tabstop>device</tabstop>
<tabstop>label</tabstop>
<tabstop>controls</tabstop>
<tabstop>controlAdd</tabstop>
<tabstop>controlRemove</tabstop>
<tabstop>controlEdit</tabstop>
<tabstop>controlDown</tabstop>
<tabstop>controlUp</tabstop>
<tabstop>controlsLayout</tabstop>
<tabstop>sensors</tabstop>
<tabstop>sensorAdd</tabstop>
<tabstop>sensorRemove</tabstop>
<tabstop>sensorEdit</tabstop>
<tabstop>sensorDown</tabstop>
<tabstop>sensorUp</tabstop>
<tabstop>sensorsLayout</tabstop>
<tabstop>yAxis</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RemoteControlDeviceDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>619</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>319</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RemoteControlDeviceDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>619</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>319</y>
</hint>
</hints>
</connection>
</connections>
</ui>

File diff suppressed because it is too large Load Diff

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_REMOTECONTROLGUI_H_
#define INCLUDE_FEATURE_REMOTECONTROLGUI_H_
#include <QTimer>
#include <QHash>
#include <QIcon>
#include <QtCharts>
#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<QString, QList<QWidget *>> m_controls;
QHash<QString, QLabel *> m_sensorValueLabels;
QHash<QString, QTableWidgetItem *> m_sensorValueItems;
QChartView *m_chartView;
QChart *m_chart;
QHash<QString, QLineSeries *> m_series;
QHash<QString, QLineSeries *> 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<RemoteControlDeviceGUI *> 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<QString, QVariant> &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_

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RemoteControlGUI</class>
<widget class="RollupContents" name="RemoteControlGUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>360</width>
<height>60</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>360</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>Packet Error Rate Tester</string>
</property>
<widget class="QWidget" name="settingsContainer" native="true">
<property name="geometry">
<rect>
<x>2</x>
<y>2</y>
<width>351</width>
<height>51</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="controlLayout">
<item>
<widget class="ButtonSwitch" name="startStop">
<property name="toolTip">
<string>Start/stop periodic updating of device state</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/play.png</normaloff>
<normalon>:/stop.png</normalon>:/play.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="update">
<property name="toolTip">
<string>Update state of all devices</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/recycle.png</normaloff>:/recycle.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="toolTip">
<string>Open settings dialog</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/listing.png</normaloff>:/listing.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clearData">
<property name="toolTip">
<string>Clear data in charts</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/bin.png</normaloff>:/bin.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="buttonsHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>RollupContents</class>
<extends>QWidget</extends>
<header>gui/rollupcontents.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>startStop</tabstop>
<tabstop>update</tabstop>
<tabstop>settings</tabstop>
<tabstop>clearData</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QtPlugin>
#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);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_REMOTECONTROLPLUGIN_H
#define INCLUDE_FEATURE_REMOTECONTROLPLUGIN_H
#include <QObject>
#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

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#include <QDebug>
#include <QDataStream>
#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<RemoteControlDevice *>& devices) const
{
QByteArray data;
QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly);
(*stream) << devices;
delete stream;
return data;
}
void RemoteControlSettings::deserializeDeviceList(const QByteArray& data, QList<RemoteControlDevice *>& devices)
{
QDataStream *stream = new QDataStream(data);
(*stream) >> devices;
delete stream;
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_REMOTECONTROLSETTINGS_H_
#define INCLUDE_FEATURE_REMOTECONTROLSETTINGS_H_
#include <QByteArray>
#include <QString>
#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<RemoteControlControl> m_controls; // Enabled controls
QList<RemoteControlSensor> 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<RemoteControlDevice *> 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<RemoteControlDevice *>& devices) const;
void deserializeDeviceList(const QByteArray& data, QList<RemoteControlDevice *>& devices);
};
#endif // INCLUDE_FEATURE_REMOTECONTROLSETTINGS_H_

View File

@ -0,0 +1,248 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include "remotecontrolsettingsdialog.h"
#include "channel/channelwebapiutils.h"
RemoteControlSettingsDialog::RemoteControlSettingsDialog(RemoteControlSettings *settings, QWidget* parent) :
QDialog(parent),
ui(new Ui::RemoteControlSettingsDialog),
m_settings(settings)
{
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)
{
(void)row;
(void)column;
on_edit_clicked();
}
void RemoteControlSettingsDialog::devicesSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
(void)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<QTableWidgetItem *> 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<QTableWidgetItem *> 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<QTableWidgetItem *> 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]);
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
m_devices.swap(row, row - 1);
#else
m_devices.swapItemsAt(row, row - 1);
#endif
}
}
}
}
void RemoteControlSettingsDialog::on_deviceDown_clicked()
{
QList<QTableWidgetItem *> 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]);
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
m_devices.swap(row, row + 1);
#else
m_devices.swapItemsAt(row, row + 1);
#endif
}
}
}
}
void RemoteControlSettingsDialog::on_chartVerticalPolicy_currentIndexChanged(int index)
{
bool enabled = index == 1;
ui->chartHeightLabel->setEnabled(enabled);
ui->chartHeight->setEnabled(enabled);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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<RemoteControlDevice *> m_devices;
enum DeviceCol {
COL_LABEL,
COL_NAME,
COL_MODEL,
COL_SERVICE,
};
};
#endif // INCLUDE_FEATURE_REMOTECONTROLSETTINGSDIALOG_H

View File

@ -0,0 +1,474 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RemoteControlSettingsDialog</class>
<widget class="QDialog" name="RemoteControlSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>592</width>
<height>640</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>Remote Control Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="devicesTab">
<attribute name="title">
<string>Devices</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableWidget" name="devices">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Label</string>
</property>
<property name="toolTip">
<string>Label used for the device in the GUI</string>
</property>
</column>
<column>
<property name="text">
<string>Device</string>
</property>
<property name="toolTip">
<string>Device name</string>
</property>
</column>
<column>
<property name="text">
<string>Model</string>
</property>
<property name="toolTip">
<string>Device model</string>
</property>
</column>
<column>
<property name="text">
<string>Protocol</string>
</property>
<property name="toolTip">
<string>Protocol used to communicate with the device</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="add">
<property name="text">
<string>Add...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="edit">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deviceUp">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move up</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/arrow_up.png</normaloff>:/arrow_up.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deviceDown">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move down</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/arrow_down.png</normaloff>:/arrow_down.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="settingsTab">
<attribute name="title">
<string>Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="tpLinkGroup">
<property name="title">
<string>TP-Link</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="tpLinkUsernameLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="tpLinkUsername">
<property name="toolTip">
<string>Username. Typically your email address</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="tpLinkPasswordLabel">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="tpLinkPassword">
<property name="toolTip">
<string>Password</string>
</property>
<property name="echoMode">
<enum>QLineEdit::PasswordEchoOnEdit</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="homeAssistantGroup">
<property name="title">
<string>Home Assistant</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="homeAssistantTokenLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Access Token</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="homeAssistantHostLabel">
<property name="text">
<string>Host</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="homeAssistantToken">
<property name="toolTip">
<string>API access token. Can be generated on your profile page: http://homeassistant.local:8123/profile</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="homeAssistantHost">
<property name="toolTip">
<string>Hostname of computer running Home Assistant. Typically http://homeassistant.local:8123/profile</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="visaGroup">
<property name="title">
<string>VISA</string>
</property>
<layout class="QFormLayout" name="formLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="visaResourceFilterLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Resource filter</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="visaLogIOLabel">
<property name="text">
<string>Log IO</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="visaResourceFilter">
<property name="toolTip">
<string>Regular expression of VISA resources not to connect to</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="visaLogIO">
<property name="toolTip">
<string>When checked, VISA input and output is written to SDRangel log</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="deviceGroup">
<property name="title">
<string>Devices</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="updatePeriodLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Update period (s)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="updatePeriod">
<property name="toolTip">
<string>Period in seconds between requests to update device state</string>
</property>
<property name="minimum">
<double>0.010000000000000</double>
</property>
<property name="maximum">
<double>1000000.000000000000000</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="chartsGroup">
<property name="title">
<string>Charts</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="chartVerticalPolicyLabel">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Height</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="chartVerticalPolicy">
<item>
<property name="text">
<string>Expanding</string>
</property>
</item>
<item>
<property name="text">
<string>Fixed</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="chartHeightLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Height (pixels)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="chartHeight">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimum">
<number>50</number>
</property>
<property name="maximum">
<number>2000</number>
</property>
<property name="value">
<number>130</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RemoteControlSettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>619</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>319</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RemoteControlSettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>619</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>319</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,195 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "remotecontrolvisacontroldialog.h"
#include <QDebug>
#include <QPushButton>
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)
{
(void)text;
validate();
}
void RemoteControlVISAControlDialog::on_id_textEdited(const QString &text)
{
(void)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);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

View File

@ -0,0 +1,374 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RemoteControlVISAControlDialog</class>
<widget class="QDialog" name="RemoteControlVISAControlDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>821</width>
<height>540</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>VISA Control</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="controlsGroup">
<property name="title">
<string>Controls</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout_3">
<item row="1" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="name">
<property name="toolTip">
<string>Name for this control</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="idLabel">
<property name="text">
<string>ID</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="id">
<property name="toolTip">
<string>Unique identifier for this control</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="typeLayout">
<item>
<widget class="QComboBox" name="type">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<item>
<property name="text">
<string>Boolean</string>
</property>
</item>
<item>
<property name="text">
<string>Integer</string>
</property>
</item>
<item>
<property name="text">
<string>Float</string>
</property>
</item>
<item>
<property name="text">
<string>String</string>
</property>
</item>
<item>
<property name="text">
<string>List</string>
</property>
</item>
<item>
<property name="text">
<string>Button</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="widgetType">
<property name="toolTip">
<string>Type of widget to display value</string>
</property>
<item>
<property name="text">
<string>Spin box</string>
</property>
</item>
<item>
<property name="text">
<string>Dial</string>
</property>
</item>
<item>
<property name="text">
<string>Slider</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="minLabel">
<property name="text">
<string>Min</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="min">
<property name="toolTip">
<string>Minimum value</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-9999999999999999932209486743616279764617084419440640.000000000000000</double>
</property>
<property name="maximum">
<double>10000000000000000735758738477112498397576062152177456799245857901351759143802190202050679656153088.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="maxLabel">
<property name="text">
<string>Max</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="max">
<property name="toolTip">
<string>Maximum value</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-999999999999999983222784.000000000000000</double>
</property>
<property name="maximum">
<double>100000000000000007629769841091887003294964970946560.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="scaleLabel">
<property name="text">
<string>Scale</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="scale">
<property name="toolTip">
<string>Scale factor applied to value before sending to instrument</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="maximum">
<double>1000000000000000.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="precisionLabel">
<property name="text">
<string>Precision</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="precision">
<property name="toolTip">
<string>Precision (number of decimals)</string>
</property>
<property name="maximum">
<number>323</number>
</property>
<property name="value">
<number>3</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelLabel">
<property name="text">
<string>Label</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="label">
<property name="toolTip">
<string>Button label</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="values">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>List of allowable values</string>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove">
<property name="toolTip">
<string>Remove current entry from list</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/bin.png</normaloff>:/bin.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="unitsLabel">
<property name="text">
<string>Units</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="units">
<property name="toolTip">
<string>Units</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="setStateLabel">
<property name="text">
<string>Set state</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QTextEdit" name="setState"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="getStateLabel">
<property name="text">
<string>Get state</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QTextEdit" name="getState"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>name</tabstop>
<tabstop>id</tabstop>
<tabstop>type</tabstop>
<tabstop>min</tabstop>
<tabstop>max</tabstop>
<tabstop>precision</tabstop>
<tabstop>values</tabstop>
<tabstop>units</tabstop>
<tabstop>setState</tabstop>
<tabstop>getState</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RemoteControlVISAControlDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>619</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>319</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RemoteControlVISAControlDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>295</x>
<y>619</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>319</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,100 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "remotecontrolvisasensordialog.h"
#include <QDebug>
#include <QPushButton>
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)
{
(void)text;
validate();
}
void RemoteControlVISASensorDialog::on_id_textEdited(const QString &text)
{
(void)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);
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#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

View File

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RemoteControlVISASensorDialog</class>
<widget class="QDialog" name="RemoteControlVISASensorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>350</height>
</rect>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>VISA Sensor</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="flat">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="topMargin">
<number>0</number>
</property>
<item row="2" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="getStateLabel">
<property name="text">
<string>Get state</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QTextEdit" name="getState">
<property name="toolTip">
<string>VISA/SCPI command to get the state for this sensor</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name">
<property name="toolTip">
<string>Name of this sensor</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="type">
<item>
<property name="text">
<string>Boolean</string>
</property>
</item>
<item>
<property name="text">
<string>Float</string>
</property>
</item>
<item>
<property name="text">
<string>String</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="idLabel">
<property name="text">
<string>ID</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="id">
<property name="toolTip">
<string>Unique (per device) identifier for this sensor</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Units</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="units">
<property name="toolTip">
<string>The units of the sensor state</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>name</tabstop>
<tabstop>id</tabstop>
<tabstop>type</tabstop>
<tabstop>units</tabstop>
<tabstop>getState</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RemoteControlVISASensorDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>356</x>
<y>330</y>
</hint>
<hint type="destinationlabel">
<x>356</x>
<y>175</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RemoteControlVISASensorDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>356</x>
<y>330</y>
</hint>
<hint type="destinationlabel">
<x>356</x>
<y>175</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#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 ((QMetaType::Type)variant.type() == QMetaType::Bool)
{
bool b = variant.toBool();
device->setState(id, b);
}
else if ((QMetaType::Type)variant.type() == QMetaType::Int)
{
int i = variant.toInt();
device->setState(id, i);
}
else if ((QMetaType::Type)variant.type() == QMetaType::Float)
{
float f = variant.toFloat();
device->setState(id, f);
}
else if ((QMetaType::Type)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<QString, QVariant> 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<QString, QVariant> 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<Device *>(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));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_PERTESTERWORKER_H_
#define INCLUDE_FEATURE_REMOTECONTROLWORKER_H_
#include <QObject>
#include <QTimer>
#include <QUdpSocket>
#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<Device *> 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<QString, QVariant> status);
void deviceUnavailable();
void deviceError(const QString &error);
};
#endif // INCLUDE_FEATURE_REMOTECONTROLWORKER_H_

View File

@ -207,6 +207,10 @@ set(sdrbase_SOURCES
util/timeutil.cpp
util/visa.cpp
util/weather.cpp
util/iot/device.cpp
util/iot/homeassistant.cpp
util/iot/tplink.cpp
util/iot/visa.cpp
plugin/plugininterface.cpp
plugin/pluginapi.cpp
@ -426,6 +430,10 @@ set(sdrbase_HEADERS
util/timeutil.h
util/visa.h
util/weather.h
util/iot/device.h
util/iot/homeassistant.h
util/iot/tplink.h
util/iot/visa.h
webapi/webapiadapter.h
webapi/webapiadapterbase.h

577
sdrbase/util/iot/device.cpp Normal file
View File

@ -0,0 +1,577 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include "util/simpleserializer.h"
#include "util/iot/device.h"
#include "util/iot/tplink.h"
#include "util/iot/homeassistant.h"
#include "util/iot/visa.h"
Device::Device(DeviceDiscoverer::DeviceInfo *info)
{
if (info) {
m_info = *info;
}
}
Device* Device::create(const QHash<QString, QVariant>& settings, const QString& protocol, DeviceDiscoverer::DeviceInfo *info)
{
if (checkSettings(settings, protocol))
{
if (protocol == "TPLink")
{
if (settings.contains("deviceId"))
{
return new TPLinkDevice(settings.value("username").toString(),
settings.value("password").toString(),
settings.value("deviceId").toString(),
info);
}
else
{
qDebug() << "Device::create: A deviceId is required for: " << protocol;
}
}
else if (protocol == "HomeAssistant")
{
if (settings.contains("deviceId"))
{
return new HomeAssistantDevice(settings.value("apiKey").toString(),
settings.value("url").toString(),
settings.value("deviceId").toString(),
settings.value("controlIds").toStringList(),
settings.value("sensorIds").toStringList(),
info);
}
else
{
qDebug() << "Device::create: A deviceId is required for: " << protocol;
}
}
else if (protocol == "VISA")
{
if (settings.contains("deviceId"))
{
return new VISADevice(settings,
settings.value("deviceId").toString(),
settings.value("controlIds").toStringList(),
settings.value("sensorIds").toStringList(),
info);
}
else
{
qDebug() << "Device::create: A deviceId is required for: " << protocol;
}
}
}
return nullptr;
}
bool Device::checkSettings(const QHash<QString, QVariant>& settings, const QString& protocol)
{
if (protocol == "TPLink")
{
if (settings.contains("username") && settings.contains("password"))
{
return true;
}
else
{
qDebug() << "Device::checkSettings: A username and password are required for: " << protocol;
return false;
}
}
else if (protocol == "HomeAssistant")
{
if (settings.contains("apiKey"))
{
if (settings.contains("url"))
{
return true;
}
else
{
qDebug() << "Device::checkSettings: A host url is required for: " << protocol;
return false;
}
}
else
{
qDebug() << "Device::checkSettings: An apiKey is required for: " << protocol;
return false;
}
}
else if (protocol == "VISA")
{
return true;
}
else
{
qDebug() << "Device::checkSettings: Unsupported protocol: " << protocol;
return false;
}
}
void Device::setState(const QString &controlId, bool state)
{
qDebug() << "Device::setState: " << getProtocol() << " doesn't support bool. Can't set " << controlId << " to " << state;
}
void Device::setState(const QString &controlId, int state)
{
qDebug() << "Device::setState: " << getProtocol() << " doesn't support int. Can't set " << controlId << " to " << state;
}
void Device::setState(const QString &controlId, float state)
{
qDebug() << "Device::setState: " << getProtocol() << " doesn't support float. Can't set " << controlId << " to " << state;
}
void Device::setState(const QString &controlId, const QString &state)
{
qDebug() << "Device::setState: " << getProtocol() << " doesn't support QString. Can't set " << controlId << " to " << state;
}
void Device::recordGetRequest(void *ptr)
{
m_getRequests.insert(ptr, QDateTime::currentDateTime());
}
void Device::removeGetRequest(void *ptr)
{
m_getRequests.remove(ptr);
}
void Device::recordSetRequest(const QString &id, int guardMS)
{
m_setRequests.insert(id, QDateTime::currentDateTime().addMSecs(guardMS));
}
bool Device::getAfterSet(void *ptr, const QString &id)
{
if (m_getRequests.contains(ptr) && m_setRequests.contains(id))
{
QDateTime getTime = m_getRequests.value(ptr);
QDateTime setTime = m_setRequests.value(id);
return getTime > setTime;
}
else
{
return true;
}
}
const QStringList DeviceDiscoverer::m_typeStrings = {
"Auto",
"Boolean",
"Integer",
"Float",
"String",
"List",
"Button"
};
const QStringList DeviceDiscoverer::m_widgetTypeStrings = {
"Spin box",
"Dial",
"Slider"
};
DeviceDiscoverer::DeviceDiscoverer()
{
}
DeviceDiscoverer *DeviceDiscoverer::getDiscoverer(const QHash<QString, QVariant>& settings, const QString& protocol)
{
if (Device::checkSettings(settings, protocol))
{
if (protocol == "TPLink")
{
return new TPLinkDeviceDiscoverer(settings.value("username").toString(), settings.value("password").toString());
}
else if (protocol == "HomeAssistant")
{
return new HomeAssistantDeviceDiscoverer(settings.value("apiKey").toString(), settings.value("url").toString());
}
else if (protocol == "VISA")
{
return new VISADeviceDiscoverer(settings.value("resourceFilter").toString());
}
}
return nullptr;
}
DeviceDiscoverer::DeviceInfo::DeviceInfo()
{
}
DeviceDiscoverer::DeviceInfo::DeviceInfo(const DeviceInfo &info)
{
m_name = info.m_name;
m_id = info.m_id;
m_model = info.m_model;
// Take deep-copy of controls and sensors
for (auto const control : info.m_controls) {
ControlInfo *ci = control->clone();
m_controls.append(ci);
}
for (auto const sensor : info.m_sensors) {
m_sensors.append(sensor->clone());
}
}
DeviceDiscoverer::DeviceInfo& DeviceDiscoverer::DeviceInfo::operator=(const DeviceInfo &info)
{
m_name = info.m_name;
m_id = info.m_id;
m_model = info.m_model;
qDeleteAll(m_controls);
m_controls.clear();
qDeleteAll(m_sensors);
m_sensors.clear();
// Take deep-copy of controls and sensors
for (auto const control : info.m_controls) {
m_controls.append(control->clone());
}
for (auto const sensor : info.m_sensors) {
m_sensors.append(sensor->clone());
}
return *this;
}
DeviceDiscoverer::DeviceInfo::~DeviceInfo()
{
qDeleteAll(m_controls);
m_controls.clear();
qDeleteAll(m_sensors);
m_sensors.clear();
}
DeviceDiscoverer::DeviceInfo::operator QString() const
{
QString controls;
QString sensors;
for (auto control : m_controls) {
controls.append((QString)*control);
}
for (auto sensor : m_sensors) {
sensors.append((QString)*sensor);
}
return QString("DeviceInfo: m_name: %1 m_id: %2 m_model: %3 m_controls: %4 m_sensors: %5")
.arg(m_name)
.arg(m_id)
.arg(m_model)
.arg(controls)
.arg(sensors);
}
DeviceDiscoverer::ControlInfo::ControlInfo() :
m_type(AUTO),
m_min(-1000000),
m_max(1000000),
m_scale(1.0f),
m_precision(3),
m_widgetType(SPIN_BOX)
{
}
DeviceDiscoverer::ControlInfo::operator QString() const
{
return QString("ControlInfo: m_name: %1 m_id: %2 m_type: %3")
.arg(m_name)
.arg(m_id)
.arg(DeviceDiscoverer::m_typeStrings[m_type]);
}
DeviceDiscoverer::ControlInfo *DeviceDiscoverer::ControlInfo::clone() const
{
return new ControlInfo(*this);
}
QByteArray DeviceDiscoverer::ControlInfo::serialize() const
{
SimpleSerializer s(1);
s.writeString(1, m_name);
s.writeString(2, m_id);
s.writeS32(3, (int)m_type);
s.writeFloat(4, m_min);
s.writeFloat(5, m_max);
s.writeFloat(6, m_scale);
s.writeS32(7, m_precision);
s.writeList(8, m_values);
s.writeS32(9, (int)m_widgetType);
s.writeString(10, m_units);
return s.final();
}
bool DeviceDiscoverer::ControlInfo::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
d.readString(1, &m_name);
d.readString(2, &m_id);
d.readS32(3, (int*)&m_type);
d.readFloat(4, &m_min);
d.readFloat(5, &m_max);
d.readFloat(6, &m_scale, 1.0f);
d.readS32(7, &m_precision, 3);
d.readList(8, &m_values);
d.readS32(9, (int *)&m_widgetType);
d.readString(10, &m_units);
return true;
}
else
{
return false;
}
}
DeviceDiscoverer::SensorInfo::operator QString() const
{
return QString("SensorInfo: m_name: %1 m_id: %2 m_type: %3")
.arg(m_name)
.arg(m_id)
.arg(DeviceDiscoverer::m_typeStrings[m_type]);
}
DeviceDiscoverer::SensorInfo *DeviceDiscoverer::SensorInfo::clone() const
{
return new SensorInfo(*this);
}
QByteArray DeviceDiscoverer::SensorInfo::serialize() const
{
SimpleSerializer s(1);
s.writeString(1, m_name);
s.writeString(2, m_id);
s.writeS32(3, (int)m_type);
s.writeString(4, m_units);
return s.final();
}
bool DeviceDiscoverer::SensorInfo::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
d.readString(1, &m_name);
d.readString(2, &m_id);
d.readS32(3, (int*)&m_type);
d.readString(4, &m_units);
return true;
}
else
{
return false;
}
}
QByteArray DeviceDiscoverer::DeviceInfo::serialize() const
{
SimpleSerializer s(1);
s.writeString(1, m_name);
s.writeString(2, m_id);
s.writeString(3, m_model);
s.writeList(10, m_controls);
s.writeList(11, m_sensors);
return s.final();
}
bool DeviceDiscoverer::DeviceInfo::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
QByteArray blob;
d.readString(1, &m_name);
d.readString(2, &m_id);
d.readString(3, &m_model);
d.readList(10, &m_controls);
d.readList(11, &m_sensors);
return true;
}
else
{
return false;
}
}
DeviceDiscoverer::ControlInfo *DeviceDiscoverer::DeviceInfo::getControl(const QString &id) const
{
for (auto c : m_controls)
{
if (c->m_id == id) {
return c;
}
}
return nullptr;
}
DeviceDiscoverer::SensorInfo *DeviceDiscoverer::DeviceInfo::getSensor(const QString &id) const
{
for (auto s : m_sensors)
{
if (s->m_id == id) {
return s;
}
}
return nullptr;
}
void DeviceDiscoverer::DeviceInfo::deleteControl(const QString &id)
{
for (int i = 0; i < m_controls.size(); i++)
{
if (m_controls[i]->m_id == id)
{
delete m_controls.takeAt(i);
return;
}
}
}
void DeviceDiscoverer::DeviceInfo::deleteSensor(const QString &id)
{
for (int i = 0; i < m_sensors.size(); i++)
{
if (m_sensors[i]->m_id == id)
{
delete m_sensors.takeAt(i);
return;
}
}
}
QDataStream& operator<<(QDataStream& out, const DeviceDiscoverer::ControlInfo* control)
{
int typeId;
if (dynamic_cast<const VISADevice::VISAControl *>(control)) {
typeId = 1;
} else {
typeId = 0;
}
out << typeId;
out << control->serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, DeviceDiscoverer::ControlInfo*& control)
{
QByteArray data;
int typeId;
in >> typeId;
if (typeId == 1) {
control = new VISADevice::VISAControl();
} else {
control = new DeviceDiscoverer::ControlInfo();
}
in >> data;
control->deserialize(data);
return in;
}
QDataStream& operator<<(QDataStream& out, const DeviceDiscoverer::SensorInfo* sensor)
{
int typeId;
if (dynamic_cast<const VISADevice::VISASensor *>(sensor)) {
typeId = 1;
} else {
typeId = 0;
}
out << typeId;
out << sensor->serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, DeviceDiscoverer::SensorInfo*& sensor)
{
QByteArray data;
int typeId;
in >> typeId;
if (typeId == 1) {
sensor = new VISADevice::VISASensor();
} else {
sensor = new DeviceDiscoverer::SensorInfo();
}
in >> data;
sensor->deserialize(data);
return in;
}
QDataStream& operator<<(QDataStream& out, const VISADevice::VISASensor &sensor)
{
out << sensor.serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, VISADevice::VISASensor& sensor)
{
QByteArray data;
in >> data;
sensor.deserialize(data);
return in;
}
QDataStream& operator<<(QDataStream& out, const VISADevice::VISAControl &control)
{
out << control.serialize();
return out;
}
QDataStream& operator>>(QDataStream& in, VISADevice::VISAControl& control)
{
QByteArray data;
in >> data;
control.deserialize(data);
return in;
}

152
sdrbase/util/iot/device.h Normal file
View File

@ -0,0 +1,152 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_DEVICE_H
#define INCLUDE_DEVICE_H
#include <QtCore>
#include "export.h"
class QNetworkAccessManager;
class QNetworkReply;
class SDRBASE_API DeviceDiscoverer : public QObject
{
Q_OBJECT
public:
enum Type {
AUTO,
BOOL,
INT,
FLOAT,
STRING,
LIST,
BUTTON
};
enum WidgetType {
SPIN_BOX,
DIAL,
SLIDER
};
struct SDRBASE_API ControlInfo {
QString m_name;
QString m_id;
Type m_type; // Data type
float m_min; // Min/max when m_type=INT/FLOAT
float m_max;
float m_scale;
int m_precision;
QStringList m_values; // Allowed values when m_type==LIST or label for button when m_type==BUTTON
WidgetType m_widgetType;// For m_type==FLOAT
QString m_units;
ControlInfo();
virtual ~ControlInfo() {}
operator QString() const;
virtual ControlInfo *clone() const;
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
};
struct SDRBASE_API SensorInfo {
QString m_name;
QString m_id;
Type m_type;
QString m_units; // W/Watts etc
virtual ~SensorInfo() {}
operator QString() const;
virtual SensorInfo *clone() const;
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
};
struct SDRBASE_API DeviceInfo {
QString m_name; // User friendly name
QString m_id; // ID for the device used by the API
QString m_model; // Model name
QList<ControlInfo *> m_controls;
QList<SensorInfo *> m_sensors;
DeviceInfo();
DeviceInfo(const DeviceInfo &info);
~DeviceInfo();
DeviceInfo& operator=(const DeviceInfo &info);
operator QString() const;
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
ControlInfo *getControl(const QString &id) const;
SensorInfo *getSensor(const QString &id) const;
void deleteControl(const QString &id);
void deleteSensor(const QString &id);
};
protected:
DeviceDiscoverer();
public:
static DeviceDiscoverer *getDiscoverer(const QHash<QString, QVariant>& settings, const QString& protocol="TPLink");
static const QStringList m_typeStrings;
static const QStringList m_widgetTypeStrings;
virtual void getDevices() = 0;
signals:
void deviceList(const QList<DeviceInfo> &devices);
void error(const QString &msg);
};
class SDRBASE_API Device : public QObject
{
Q_OBJECT
protected:
Device(DeviceDiscoverer::DeviceInfo *info=nullptr);
public:
static Device* create(const QHash<QString, QVariant>& settings, const QString& protocol="TPLink", DeviceDiscoverer::DeviceInfo *info=nullptr);
static bool checkSettings(const QHash<QString, QVariant>& settings, const QString& protocol);
virtual void getState() = 0;
virtual void setState(const QString &controlId, bool state);
virtual void setState(const QString &controlId, int state);
virtual void setState(const QString &controlId, float state);
virtual void setState(const QString &controlId, const QString &state);
virtual QString getProtocol() const = 0;
virtual QString getDeviceId() const = 0;
signals:
void deviceUpdated(QHash<QString, QVariant>); // Called when new state available. Hash keys are control and sensor IDs
void deviceUnavailable(); // Called when device is unavailable. error() isn't signalled, as we expect devices to come and go
void error(const QString &msg); // Called on terminal error, such as invalid authentication details
protected:
DeviceDiscoverer::DeviceInfo m_info;
QHash<void *, QDateTime> m_getRequests; // These data and functions help prevent using stale data from slow getStates
QHash<QString, QDateTime> m_setRequests;
void recordGetRequest(void *ptr);
void removeGetRequest(void *ptr);
void recordSetRequest(const QString &id, int guardMS=0);
bool getAfterSet(void *ptr, const QString &id);
};
#endif /* INCLUDE_DEVICE_H */

View File

@ -0,0 +1,332 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include "util/iot/homeassistant.h"
#include "util/simpleserializer.h"
HomeAssistantDevice::HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info) :
Device(info),
m_deviceId(deviceId),
m_apiKey(apiKey),
m_url(url)
{
m_entities = controls;
m_entities.append(sensors);
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDevice::handleReply
);
}
HomeAssistantDevice::~HomeAssistantDevice()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDevice::handleReply
);
delete m_networkManager;
}
void HomeAssistantDevice::getState()
{
// Get state for all entities of the device
for (auto entity : m_entities)
{
QUrl url(m_url + "/api/states/" + entity);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->get(request);
recordGetRequest(reply);
}
}
void HomeAssistantDevice::setState(const QString &controlId, bool state)
{
QString domain = controlId.left(controlId.indexOf("."));
QUrl url(m_url + "/api/services/" + domain + "/turn_" + (state ? "on" : "off"));
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject object {
{"entity_id", controlId}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
// 750ms guard, to try to avoid toggling of widget, while state updates on server
// Perhaps should be a setting
recordSetRequest(controlId, 750);
}
void HomeAssistantDevice::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(data, &error);
if (!document.isNull())
{
//qDebug() << "Received " << document;
// POSTs to /api/services return an array, GETs from /api/states return an object
if (document.isObject())
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("entity_id")) && obj.contains(QStringLiteral("state")))
{
QString entityId = obj.value(QStringLiteral("entity_id")).toString();
if (getAfterSet(reply, entityId))
{
QHash<QString, QVariant> status;
QString state = obj.value(QStringLiteral("state")).toString();
bool dOk;
bool iOk;
int i = state.toInt(&iOk);
double d = state.toDouble(&dOk);
if ((state == "on") || (state == "playing")) {
status.insert(entityId, 1);
} else if ((state == "off") || (state == "paused")) {
status.insert(entityId, 0);
} else if (iOk) {
status.insert(entityId, i);
} else if (dOk) {
status.insert(entityId, d);
} else {
status.insert(entityId, state);
}
emit deviceUpdated(status);
}
}
}
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
}
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: error: " << reply->error();
}
removeGetRequest(reply);
reply->deleteLater();
}
else
{
qDebug() << "HomeAssistantDevice::handleReply: reply is null";
}
}
HomeAssistantDeviceDiscoverer::HomeAssistantDeviceDiscoverer(const QString& apiKey, const QString& url) :
m_apiKey(apiKey),
m_url(url)
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDeviceDiscoverer::handleReply
);
}
HomeAssistantDeviceDiscoverer::~HomeAssistantDeviceDiscoverer()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&HomeAssistantDeviceDiscoverer::handleReply
);
delete m_networkManager;
}
void HomeAssistantDeviceDiscoverer::getDevices()
{
QUrl url(m_url+ "/api/template");
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer " + m_apiKey.toLocal8Bit());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// Use templates to get a list of devices and associated entities
QString tpl =
"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq',None)| list %}\n"
"{%- set ns = namespace(devices = []) %}\n"
"{%- for device in devices %}\n"
" {%- set entities = device_entities(device) | list %}\n"
" {%- if entities %}\n"
" {%- set ens = namespace(entityobjs = []) %}\n"
" {%- for entity in entities %}\n"
" {%- set entityobj = {'entity_id': entity, 'name': state_attr(entity,'friendly_name'), 'unit_of_measurement': state_attr(entity,'unit_of_measurement')} %}\n"
" {%- set ens.entityobjs = ens.entityobjs + [ entityobj ] %}\n"
" {%- endfor %}\n"
" {%- set obj = {'device_id': device, 'name': device_attr(device,'name'), 'name_by_user': device_attr(device,'name_by_user'), 'model': device_attr(device,'model'), 'entities': ens.entityobjs } %}\n"
" {%- set ns.devices = ns.devices + [ obj ] %}\n"
" {%- endif %}\n"
"{%- endfor %}\n"
"{{ ns.devices | tojson }}";
QJsonObject object {
{"template", tpl}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void HomeAssistantDeviceDiscoverer::handleReply(QNetworkReply* reply)
{
if (reply)
{
if (!reply->error())
{
QList<DeviceInfo> devices;
QByteArray data = reply->readAll();
//qDebug() << "Received " << data;
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(data, &error);
if (!document.isNull())
{
if (document.isArray())
{
for (auto deviceRef : document.array())
{
QJsonObject deviceObj = deviceRef.toObject();
if (deviceObj.contains(QStringLiteral("device_id")) && deviceObj.contains(QStringLiteral("entities")))
{
QJsonArray entitiesArray = deviceObj.value(QStringLiteral("entities")).toArray();
if (entitiesArray.size() > 0)
{
DeviceInfo info;
info.m_id = deviceObj.value(QStringLiteral("device_id")).toString();
if (deviceObj.contains(QStringLiteral("name_by_user"))) {
info.m_name = deviceObj.value(QStringLiteral("name_by_user")).toString();
}
if (info.m_name.isEmpty() && deviceObj.contains(QStringLiteral("name"))) {
info.m_name = deviceObj.value(QStringLiteral("name")).toString();
}
if (deviceObj.contains(QStringLiteral("model"))) {
info.m_model = deviceObj.value(QStringLiteral("model")).toString();
}
for (auto entityRef : entitiesArray)
{
QJsonObject entityObj = entityRef.toObject();
QString entity = entityObj.value(QStringLiteral("entity_id")).toString();
QString name = entityObj.value(QStringLiteral("name")).toString();
QString domain = entity.left(entity.indexOf('.'));
if (domain == "binary_sensor")
{
SensorInfo *sensorInfo = new SensorInfo();
sensorInfo->m_name = name;
sensorInfo->m_id = entity;
sensorInfo->m_type = DeviceDiscoverer::BOOL;
sensorInfo->m_units = entityObj.value(QStringLiteral("unit_of_measurement")).toString();
info.m_sensors.append(sensorInfo);
}
else if (domain == "sensor")
{
SensorInfo *sensorInfo = new SensorInfo();
sensorInfo->m_name = name;
sensorInfo->m_id = entity;
sensorInfo->m_type = DeviceDiscoverer::FLOAT; // FIXME: Auto?
sensorInfo->m_units = entityObj.value(QStringLiteral("unit_of_measurement")).toString();
info.m_sensors.append(sensorInfo);
}
else if ((domain == "switch") || (domain == "light") || (domain == "media_player")) // Entities that support turn_on/turn_off
{
ControlInfo *controlInfo = new ControlInfo();
controlInfo->m_name = name;
controlInfo->m_id = entity;
controlInfo->m_type = DeviceDiscoverer::BOOL;
info.m_controls.append(controlInfo);
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Unsupported domain: " << domain;
}
}
if ((info.m_controls.size() > 0) || (info.m_sensors.size() > 0))
{
devices.append(info);
}
}
else
{
//qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: No entities " << deviceObj.value(QStringLiteral("device_id")).toString();
}
}
else
{
//qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: device_id or entities missing";
}
}
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Document is not an array: " << document;
}
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
}
emit deviceList(devices);
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: error: " << reply->error() << ":" << reply->errorString();
// Get QNetworkReply::AuthenticationRequiredError if token is invalid
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
emit error("Home Assistant: Authentication failed. Check access token is valid.");
} else {
emit error(QString("Home Assistant: Network error. %1").arg(reply->errorString()));
}
}
reply->deleteLater();
}
else
{
qDebug() << "HomeAssistantDeviceDiscoverer::handleReply: reply is null";
}
}

View File

@ -0,0 +1,71 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_IOT_HOMEASSISTANT_H
#define INCLUDE_IOT_HOMEASSISTANT_H
#include "util/iot/device.h"
// Supports Home Assistant devices - https://www.home-assistant.io/
class SDRBASE_API HomeAssistantDevice : public Device {
Q_OBJECT
public:
HomeAssistantDevice(const QString& apiKey, const QString& url, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info=nullptr);
~HomeAssistantDevice();
virtual void getState() override;
using Device::setState;
virtual void setState(const QString &controlId, bool state) override;
virtual QString getProtocol() const override { return "HomeAssistant"; }
virtual QString getDeviceId() const override { return m_deviceId; }
private:
QString m_deviceId;
QStringList m_entities; // List of entities that are part of the device, to get state for (controls and sensors)
QString m_apiKey; // Bearer token
QString m_url; // Typically http://homeassistant.local:8123
QNetworkAccessManager *m_networkManager;
public slots:
void handleReply(QNetworkReply* reply);
};
class SDRBASE_API HomeAssistantDeviceDiscoverer : public DeviceDiscoverer {
Q_OBJECT
public:
HomeAssistantDeviceDiscoverer(const QString& apiKey, const QString& url);
~HomeAssistantDeviceDiscoverer();
virtual void getDevices() override;
private:
QString m_deviceId;
QString m_apiKey; // Bearer token
QString m_url; // Typically http://homeassistant.local:8123
QNetworkAccessManager *m_networkManager;
public slots:
void handleReply(QNetworkReply* reply);
};
#endif /* INCLUDE_IOT_HOMEASSISTANT_H */

661
sdrbase/util/iot/tplink.cpp Normal file
View File

@ -0,0 +1,661 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include "util/iot/tplink.h"
#include "util/simpleserializer.h"
const QString TPLinkCommon::m_url = "https://wap.tplinkcloud.com";
TPLinkCommon::TPLinkCommon(const QString& username, const QString &password) :
m_loggedIn(false),
m_outstandingRequest(false),
m_username(username),
m_password(password),
m_networkManager(nullptr)
{
}
void TPLinkCommon::login()
{
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject params {
{"appType", "Kasa_Android"},
{"cloudUserName", m_username},
{"cloudPassword", m_password},
{"terminalUUID", "9cc4653e-338f-48e4-b8ca-6ed3f67631e4"}
};
QJsonObject object {
{"method", "login"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void TPLinkCommon::handleLoginReply(QNetworkReply* reply, QString &errorMessage)
{
if (reply)
{
if (!reply->error())
{
QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
if (document.isObject())
{
//qDebug() << "Received " << document;
if (!m_loggedIn)
{
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("error_code")))
{
int errorCode = obj.value(QStringLiteral("error_code")).toInt();
if (!errorCode)
{
if (obj.contains(QStringLiteral("result")))
{
QJsonObject result = obj.value(QStringLiteral("result")).toObject();
if (result.contains(QStringLiteral("token")))
{
m_loggedIn = true;
m_token = result.value(QStringLiteral("token")).toString();
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a token: " << result;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result object: " << obj;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Non-zero error_code while logging in: " << errorCode;
if (obj.contains(QStringLiteral("msg")))
{
QString msg = obj.value(QStringLiteral("msg")).toString();
qDebug() << "TPLinkDevice::handleReply: Error message: " << msg;
// Typical msg is "Incorrect email or password"
errorMessage = QString("TP-Link: Failed to log in. %1").arg(msg);
}
else
{
errorMessage = QString("TP-Link: Failed to log in. Error code: %1").arg(errorCode);
}
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain an error_code: " << obj;
}
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "TPLinkDevice::handleReply: reply is null";
}
if (!m_loggedIn && errorMessage.isEmpty()) {
errorMessage = "TP-Link: Failed to log in.";
}
}
TPLinkDevice::TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info) :
Device(info),
TPLinkCommon(username, password),
m_deviceId(deviceId)
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDevice::handleReply
);
login();
}
TPLinkDevice::~TPLinkDevice()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDevice::handleReply
);
delete m_networkManager;
}
void TPLinkDevice::getState()
{
if (!m_loggedIn)
{
m_outstandingRequest = true;
return;
}
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject system;
system.insert("get_sysinfo", QJsonValue());
QJsonObject emeter;
emeter.insert("get_realtime", QJsonValue());
QJsonObject requestData {
{"system", system},
{"emeter", emeter}
};
QJsonObject params {
{"deviceId", m_deviceId},
{"requestData", requestData},
{"token", m_token}
};
QJsonObject object {
{"method", "passthrough"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
QNetworkReply *reply = m_networkManager->post(request, document.toJson());
recordGetRequest(reply);
}
void TPLinkDevice::setState(const QString &controlId, bool state)
{
if (!m_loggedIn)
{
// Should we queue these and apply after logged in?
qDebug() << "TPLinkDevice::setState: Unable to set state for " << controlId << " to " << state << " as not yet logged in";
return;
}
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject stateObj {
{"state", (int)state}
};
QJsonObject system {
{"set_relay_state", stateObj}
};
QJsonObject requestData {
{"system", system}
};
if (controlId != "switch") {
QJsonArray childIds {
controlId
};
QJsonObject context {
{"child_ids", childIds}
};
requestData.insert("context", QJsonValue(context));
}
QJsonObject params {
{"deviceId", m_deviceId},
{"requestData", requestData},
{"token", m_token}
};
QJsonObject object {
{"method", "passthrough"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
recordSetRequest(controlId);
}
void TPLinkDevice::handleReply(QNetworkReply* reply)
{
if (!m_loggedIn)
{
QString errorMessage;
TPLinkCommon::handleLoginReply(reply, errorMessage);
if (!errorMessage.isEmpty())
{
emit error(errorMessage);
}
else if (m_outstandingRequest)
{
m_outstandingRequest = true;
getState();
}
}
else if (reply)
{
if (!reply->error())
{
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(data, &error);
if (!document.isNull())
{
if (document.isObject())
{
//qDebug() << "Received " << document;
QJsonObject obj = document.object();
if (obj.contains(QStringLiteral("result")))
{
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
QHash<QString, QVariant> status;
if (resultObj.contains(QStringLiteral("responseData")))
{
QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject();
if (responseDataObj.contains(QStringLiteral("system")))
{
QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject();
if (systemObj.contains(QStringLiteral("get_sysinfo")))
{
QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject();
if (sysInfoObj.contains(QStringLiteral("child_num")))
{
QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray();
for (auto childRef : children)
{
QJsonObject childObj = childRef.toObject();
if (childObj.contains(QStringLiteral("state")) && childObj.contains(QStringLiteral("id")))
{
QString id = childObj.value(QStringLiteral("id")).toString();
if (getAfterSet(reply, id))
{
int state = childObj.value(QStringLiteral("state")).toInt();
status.insert(id, state); // key should match id in discoverer
}
}
}
}
else if (sysInfoObj.contains(QStringLiteral("relay_state")))
{
if (getAfterSet(reply, "switch"))
{
int state = sysInfoObj.value(QStringLiteral("relay_state")).toInt();
status.insert("switch", state); // key should match id in discoverer
}
}
}
}
// KP115 has emeter, but KP105 doesn't
if (responseDataObj.contains(QStringLiteral("emeter")))
{
QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject();
if (emeterObj.contains(QStringLiteral("get_realtime")))
{
QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject();
if (realtimeObj.contains(QStringLiteral("current_ma")))
{
double current = realtimeObj.value(QStringLiteral("current_ma")).toDouble();
status.insert("current", current / 1000.0);
}
if (realtimeObj.contains(QStringLiteral("voltage_mv")))
{
double voltage = realtimeObj.value(QStringLiteral("voltage_mv")).toDouble();
status.insert("voltage", voltage / 1000.0);
}
if (realtimeObj.contains(QStringLiteral("power_mw")))
{
double power = realtimeObj.value(QStringLiteral("power_mw")).toDouble();
status.insert("power", power / 1000.0);
}
}
}
}
emit deviceUpdated(status);
}
else if (obj.contains(QStringLiteral("error_code")))
{
// If a device isn't available, we can get:
// {"error_code":-20002,"msg":"Request timeout"}
// {"error_code":-20571,"msg":"Device is offline"}
int errorCode = obj.value(QStringLiteral("error_code")).toInt();
QString msg = obj.value(QStringLiteral("msg")).toString();
qDebug() << "TPLinkDevice::handleReply: Error code: " << errorCode << " " << msg;
emit deviceUnavailable();
}
else
{
qDebug() << "TPLinkDevice::handleReply: Object doesn't contain a result or error_code: " << obj;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
}
}
else
{
qDebug() << "TPLinkDevice::handleReply: error: " << reply->error();
}
removeGetRequest(reply);
reply->deleteLater();
}
else
{
qDebug() << "TPLinkDevice::handleReply: reply is null";
}
}
TPLinkDeviceDiscoverer::TPLinkDeviceDiscoverer(const QString& username, const QString &password) :
TPLinkCommon(username, password)
{
m_networkManager = new QNetworkAccessManager();
QObject::connect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDeviceDiscoverer::handleReply
);
login();
}
TPLinkDeviceDiscoverer::~TPLinkDeviceDiscoverer()
{
QObject::disconnect(
m_networkManager,
&QNetworkAccessManager::finished,
this,
&TPLinkDeviceDiscoverer::handleReply
);
delete m_networkManager;
}
void TPLinkDeviceDiscoverer::getDevices()
{
if (!m_loggedIn)
{
m_outstandingRequest = true;
return;
}
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject params {
{"token", m_token}
};
QJsonObject object {
{"method", "getDeviceList"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_networkManager->post(request, document.toJson());
}
void TPLinkDeviceDiscoverer::getState(const QString &deviceId)
{
QUrl url(m_url);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QJsonObject system;
system.insert("get_sysinfo", QJsonValue());
QJsonObject emeter;
emeter.insert("get_realtime", QJsonValue());
QJsonObject requestData {
{"system", system},
{"emeter", emeter}
};
QJsonObject params {
{"deviceId", deviceId},
{"requestData", requestData},
{"token", m_token}
};
QJsonObject object {
{"method", "passthrough"},
{"params", params}
};
QJsonDocument document;
document.setObject(object);
m_getStateReplies.insert(m_networkManager->post(request, document.toJson()), deviceId);
}
void TPLinkDeviceDiscoverer::handleReply(QNetworkReply* reply)
{
if (!m_loggedIn)
{
QString errorMessage;
TPLinkCommon::handleLoginReply(reply, errorMessage);
if (!errorMessage.isEmpty())
{
emit error(errorMessage);
}
else if (m_outstandingRequest)
{
m_outstandingRequest = false;
getDevices();
}
}
else if (reply)
{
if (!reply->error())
{
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(data, &error);
if (!document.isNull())
{
if (document.isObject())
{
//qDebug() << "Received " << document;
QJsonObject obj = document.object();
if (m_getStateReplies.contains(reply))
{
// Reply for getState
m_getStateReplies.remove(reply);
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
if (resultObj.contains(QStringLiteral("responseData")))
{
QJsonObject responseDataObj = resultObj.value(QStringLiteral("responseData")).toObject();
if (responseDataObj.contains(QStringLiteral("system")))
{
DeviceInfo info;
QJsonObject systemObj = responseDataObj.value(QStringLiteral("system")).toObject();
if (systemObj.contains(QStringLiteral("get_sysinfo")))
{
QJsonObject sysInfoObj = systemObj.value(QStringLiteral("get_sysinfo")).toObject();
if (sysInfoObj.contains(QStringLiteral("alias"))) {
info.m_name = sysInfoObj.value(QStringLiteral("alias")).toString();
}
if (sysInfoObj.contains(QStringLiteral("model"))) {
info.m_model = sysInfoObj.value(QStringLiteral("model")).toString();
}
if (sysInfoObj.contains(QStringLiteral("deviceId"))) {
info.m_id = sysInfoObj.value(QStringLiteral("deviceId")).toString();
}
if (sysInfoObj.contains(QStringLiteral("child_num")))
{
QJsonArray children = sysInfoObj.value(QStringLiteral("children")).toArray();
int child = 1;
for (auto childRef : children)
{
QJsonObject childObj = childRef.toObject();
ControlInfo *controlInfo = new ControlInfo();
controlInfo->m_id = childObj.value(QStringLiteral("id")).toString();
if (childObj.contains(QStringLiteral("alias"))) {
controlInfo->m_name = childObj.value(QStringLiteral("alias")).toString();
}
controlInfo->m_type = DeviceDiscoverer::BOOL;
info.m_controls.append(controlInfo);
child++;
}
}
else if (sysInfoObj.contains(QStringLiteral("relay_state")))
{
ControlInfo *controlInfo = new ControlInfo();
controlInfo->m_id = "switch";
if (sysInfoObj.contains(QStringLiteral("alias"))) {
controlInfo->m_name = sysInfoObj.value(QStringLiteral("alias")).toString();
}
controlInfo->m_type = DeviceDiscoverer::BOOL;
info.m_controls.append(controlInfo);
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: get_sysinfo missing";
}
// KP115 has energy meter, but KP105 doesn't. KP105 will have emeter object, but without get_realtime sub-object
if (responseDataObj.contains(QStringLiteral("emeter")))
{
QJsonObject emeterObj = responseDataObj.value(QStringLiteral("emeter")).toObject();
if (emeterObj.contains(QStringLiteral("get_realtime")))
{
QJsonObject realtimeObj = emeterObj.value(QStringLiteral("get_realtime")).toObject();
if (realtimeObj.contains(QStringLiteral("current_ma")))
{
SensorInfo *currentSensorInfo = new SensorInfo();
currentSensorInfo->m_name = "Current";
currentSensorInfo->m_id = "current";
currentSensorInfo->m_type = DeviceDiscoverer::FLOAT;
currentSensorInfo->m_units = "A";
info.m_sensors.append(currentSensorInfo);
}
if (realtimeObj.contains(QStringLiteral("voltage_mv")))
{
SensorInfo *voltageSensorInfo = new SensorInfo();
voltageSensorInfo->m_name = "Voltage";
voltageSensorInfo->m_id = "voltage";
voltageSensorInfo->m_type = DeviceDiscoverer::FLOAT;
voltageSensorInfo->m_units = "V";
info.m_sensors.append(voltageSensorInfo);
}
if (realtimeObj.contains(QStringLiteral("power_mw")))
{
SensorInfo *powerSensorInfo = new SensorInfo();
powerSensorInfo->m_name = "Power";
powerSensorInfo->m_id = "power";
powerSensorInfo->m_type = DeviceDiscoverer::FLOAT;
powerSensorInfo->m_units = "W";
info.m_sensors.append(powerSensorInfo);
}
}
}
if (info.m_controls.size() > 0) {
m_devices.append(info);
} else {
qDebug() << "TPLinkDeviceDiscoverer::handleReply: No controls in info";
}
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: No responseData";
}
if (m_getStateReplies.size() == 0)
{
emit deviceList(m_devices);
m_devices.clear();
}
}
else
{
// Reply for getDevice
if (obj.contains(QStringLiteral("result")))
{
QJsonObject resultObj = obj.value(QStringLiteral("result")).toObject();
if (resultObj.contains(QStringLiteral("deviceList")))
{
QJsonArray deviceArray = resultObj.value(QStringLiteral("deviceList")).toArray();
for (auto deviceRef : deviceArray)
{
QJsonObject deviceObj = deviceRef.toObject();
if (deviceObj.contains(QStringLiteral("deviceId")) && deviceObj.contains(QStringLiteral("deviceType")))
{
// In order to discover what controls and sensors a device has, we need to get sysinfo
getState(deviceObj.value(QStringLiteral("deviceId")).toString());
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: deviceList element doesn't contain a deviceId: " << deviceObj;
}
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: result doesn't contain a deviceList: " << resultObj;
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Object doesn't contain a result: " << obj;
}
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Document is not an object: " << document;
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: Error parsing JSON: " << error.errorString() << " at offset " << error.offset;
}
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: error: " << reply->error();
}
reply->deleteLater();
}
else
{
qDebug() << "TPLinkDeviceDiscoverer::handleReply: reply is null";
}
}

78
sdrbase/util/iot/tplink.h Normal file
View File

@ -0,0 +1,78 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_IOT_TPLINK_H
#define INCLUDE_IOT_TPLINK_H
#include "util/iot/device.h"
class SDRBASE_API TPLinkCommon {
protected:
TPLinkCommon(const QString& username, const QString &password);
void login();
void handleLoginReply(QNetworkReply* reply, QString &errorMessage);
bool m_loggedIn;
bool m_outstandingRequest; // Issue getState / getDevices after logged in
QString m_username;
QString m_password;
QString m_token;
QNetworkAccessManager *m_networkManager;
static const QString m_url;
};
// Supports TPLink's Kasa plugs - https://www.tp-link.com/uk/smarthome/
class SDRBASE_API TPLinkDevice : public Device, TPLinkCommon {
Q_OBJECT
public:
TPLinkDevice(const QString& username, const QString &password, const QString &deviceId, DeviceDiscoverer::DeviceInfo *info=nullptr);
~TPLinkDevice();
virtual void getState() override;
using Device::setState;
virtual void setState(const QString &controlId, bool state) override;
virtual QString getProtocol() const override { return "TPLink"; }
virtual QString getDeviceId() const override { return m_deviceId; }
private:
QString m_deviceId;
public slots:
void handleReply(QNetworkReply* reply);
};
class SDRBASE_API TPLinkDeviceDiscoverer : public DeviceDiscoverer, TPLinkCommon {
Q_OBJECT
public:
TPLinkDeviceDiscoverer(const QString& username, const QString &password);
~TPLinkDeviceDiscoverer();
virtual void getDevices() override;
private:
void getState(const QString &deviceId);
QHash<QNetworkReply *, QString> m_getStateReplies;
QList<DeviceInfo> m_devices;
public slots:
void handleReply(QNetworkReply* reply);
};
#endif /* INCLUDE_IOT_TPLINK_H */

506
sdrbase/util/iot/visa.cpp Normal file
View File

@ -0,0 +1,506 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include "util/iot/visa.h"
#include "util/visa.h"
#include "util/simpleserializer.h"
VISADevice::VISADevice(const QHash<QString, QVariant> settings, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info) :
Device(info),
m_deviceId(deviceId),
m_session(0),
m_controls(controls),
m_sensors(sensors)
{
m_visa.openDefault();
QHashIterator<QString, QVariant> itr(settings);
while (itr.hasNext())
{
itr.next();
QString key = itr.key();
QVariant value = itr.value();
if ((key == "deviceId") || (key == "controlIds") || (key == "sensorIds"))
{
// Nothing to do here
}
else if (key == "logIO")
{
m_visa.setDebugIO(value.toBool());
}
else
{
qDebug() << "VISADevice::VISADevice: Unsupported setting key: " << key << " value: " << value;
}
}
open();
}
VISADevice::~VISADevice()
{
m_visa.close(m_session);
m_visa.closeDefault();
}
bool VISADevice::open()
{
if (!m_session) {
m_session = m_visa.open(m_deviceId);
}
if (!m_session) {
emit deviceUnavailable();
}
return m_session != 0;
}
bool VISADevice::convertToBool(const QString &string, bool &ok)
{
QString lower = string.trimmed().toLower();
if ((lower == "0") || (lower == "false") || (lower == "off"))
{
ok = true;
return false;
}
else if ((lower == "1") || (lower == "true") || (lower == "on"))
{
ok = true;
return true;
}
else
{
ok = false;
return false;
}
}
void VISADevice::convert(QHash<QString, QVariant> &status, const QString &id, DeviceDiscoverer::Type type, const QString &state)
{
if (type == DeviceDiscoverer::BOOL)
{
bool ok;
bool value = convertToBool(state, ok);
if (ok) {
status.insert(id, value);
} else {
status.insert(id, "error");
}
}
else if (type == DeviceDiscoverer::INT)
{
bool ok;
int value = state.toInt(&ok);
if (ok) {
status.insert(id, value);
} else {
status.insert(id, "error");
}
}
else if (type == DeviceDiscoverer::FLOAT)
{
bool ok;
float value = state.toFloat(&ok);
if (ok) {
status.insert(id, value);
} else {
status.insert(id, "error");
}
}
else
{
status.insert(id, state);
}
}
void VISADevice::getState()
{
if (open())
{
QHash<QString, QVariant> status;
for (auto c : m_info.m_controls)
{
if (m_controls.contains(c->m_id))
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
QString cmds = control->m_getState.trimmed();
if (!cmds.isEmpty())
{
bool error;
QStringList results = m_visa.processCommands(m_session, cmds, &error);
if (!error && (results.size() > 0))
{
// Take last returned value as the state
QString state = results[results.size()-1].trimmed();
convert(status, control->m_id, control->m_type, state);
}
else
{
status.insert(control->m_id, "error");
}
}
}
}
for (auto s : m_info.m_sensors)
{
if (m_sensors.contains(s->m_id))
{
VISASensor *sensor = reinterpret_cast<VISASensor *>(s);
QString cmds = sensor->m_getState.trimmed();
if (!cmds.isEmpty())
{
bool error;
QStringList results = m_visa.processCommands(m_session, cmds, &error);
if (!error && (results.size() > 0))
{
// Take last returned value as the state
QString state = results[results.size()-1].trimmed();
convert(status, sensor->m_id, sensor->m_type, state);
}
else
{
status.insert(sensor->m_id, "error");
}
}
}
}
emit deviceUpdated(status);
}
}
void VISADevice::setState(const QString &controlId, bool state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), (int)state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
void VISADevice::setState(const QString &controlId, int state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
void VISADevice::setState(const QString &controlId, float state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
void VISADevice::setState(const QString &controlId, const QString &state)
{
if (open())
{
for (auto c : m_info.m_controls)
{
VISAControl *control = reinterpret_cast<VISAControl *>(c);
if (control->m_id == controlId)
{
QString commands = QString::asprintf(control->m_setState.toUtf8(), state);
bool error;
m_visa.processCommands(m_session, commands, &error);
if (error) {
qDebug() << "VISADevice::setState: Failed to set state of " << controlId;
}
}
}
}
}
VISADeviceDiscoverer::VISADeviceDiscoverer(const QString& resourceFilter) :
m_resourceFilter(resourceFilter)
{
m_session = m_visa.openDefault();
}
VISADeviceDiscoverer::~VISADeviceDiscoverer()
{
m_visa.closeDefault();
}
void VISADeviceDiscoverer::getDevices()
{
QRegularExpression *filterP = nullptr;
QRegularExpression filter(m_resourceFilter);
if (!m_resourceFilter.trimmed().isEmpty()) {
filterP = &filter;
}
// Get list of VISA instruments
QList<VISA::Instrument> instruments = m_visa.instruments(filterP);
// Convert to list of devices
QList<DeviceInfo> devices;
for (auto const &instrument : instruments)
{
DeviceInfo info;
info.m_name = instrument.m_model;
info.m_id = instrument.m_resource;
info.m_model = instrument.m_model;
if ((info.m_name == "DP832") || (info.m_name == "DP832A"))
{
for (int i = 1; i <= 3; i++)
{
VISADevice::VISAControl *output = new VISADevice::VISAControl();
output->m_name = QString("CH%1").arg(i);
output->m_id = QString("control.ch%1").arg(i);
output->m_type = BOOL;
output->m_getState = QString(":OUTPUT? CH%1").arg(i);
output->m_setState = QString(":OUTPUT CH%1,%d").arg(i);
info.m_controls.append(output);
VISADevice::VISAControl *setVoltage = new VISADevice::VISAControl();
setVoltage->m_name = QString("V%1").arg(i);
setVoltage->m_id = QString("control.voltage%1").arg(i);
setVoltage->m_type = FLOAT;
setVoltage->m_min = 0.0f;
setVoltage->m_max = i == 3 ? 5.0f : 30.0f;
setVoltage->m_scale = 1.0f;
setVoltage->m_precision = 3;
setVoltage->m_widgetType = SPIN_BOX;
setVoltage->m_units = "V";
setVoltage->m_getState = QString(":SOURCE%1:VOLTage?").arg(i);
setVoltage->m_setState = QString(":SOURCE%1:VOLTage %f").arg(i);
info.m_controls.append(setVoltage);
VISADevice::VISAControl *setCurrent = new VISADevice::VISAControl();
setCurrent->m_name = QString("i%1").arg(i);
setCurrent->m_id = QString("control.current%1").arg(i);
setCurrent->m_type = FLOAT;
setCurrent->m_min = 0.0f;
setCurrent->m_max = 3.0f;
setCurrent->m_scale = 1.0f;
setCurrent->m_precision = 3;
setCurrent->m_widgetType = SPIN_BOX;
setCurrent->m_units = "A";
setCurrent->m_getState = QString(":SOURCE%1:CURRent?").arg(i);
setCurrent->m_setState = QString(":SOURCE%1:CURRent %f").arg(i);
info.m_controls.append(setCurrent);
VISADevice::VISASensor *voltage = new VISADevice::VISASensor();
voltage->m_name = QString("V%1").arg(i);
voltage->m_id = QString("sensor.voltage%1").arg(i);
voltage->m_type = FLOAT;
voltage->m_units = "V";
voltage->m_getState = QString(":MEASure:VOLTage? CH%1").arg(i);
info.m_sensors.append(voltage);
VISADevice::VISASensor *current = new VISADevice::VISASensor();
current->m_name = QString("i%1").arg(i);
current->m_id = QString("sensor.current%1").arg(i);
current->m_type = FLOAT;
current->m_units = "A";
current->m_getState = QString(":MEASure:CURRent? CH%1").arg(i);
info.m_sensors.append(current);
VISADevice::VISASensor *power = new VISADevice::VISASensor();
power->m_name = QString("P%1").arg(i);
power->m_id = QString("sensor.power%1").arg(i);
power->m_type = FLOAT;
power->m_units = "W";
power->m_getState = QString(":MEASure:POWEr? CH%1").arg(i);
info.m_sensors.append(power);
}
}
else if (info.m_name == "SSA3032X")
{
VISADevice::VISAControl *frequency = new VISADevice::VISAControl();
frequency->m_name = "Frequency";
frequency->m_id = "control.frequency";
frequency->m_type = FLOAT;
frequency->m_min = 0.0f;
frequency->m_max = 3.2e3f;
frequency->m_scale = 1e6f;
frequency->m_precision = 6;
frequency->m_widgetType = SPIN_BOX;
frequency->m_units = "MHz";
frequency->m_getState = ":FREQuency:CENTer?";
frequency->m_setState = ":FREQuency:CENTer %f";
info.m_controls.append(frequency);
VISADevice::VISAControl *span = new VISADevice::VISAControl();
span->m_name = "Span";
span->m_id = "control.span";
span->m_type = FLOAT;
span->m_min = 0.0f;
span->m_max = 3.2e3f;
span->m_scale = 1e6;
span->m_precision = 3;
span->m_widgetType = SPIN_BOX;
span->m_units = "MHz";
span->m_getState = ":FREQuency:SPAN?";
span->m_setState = ":FREQuency:SPAN %f";
info.m_controls.append(span);
VISADevice::VISAControl *markerX = new VISADevice::VISAControl();
markerX->m_name = "Marker X";
markerX->m_id = "control.markerx";
markerX->m_type = FLOAT;
markerX->m_min = 0.0f;
markerX->m_max = 3.2e3f;
markerX->m_scale = 1e6;
markerX->m_precision = 6;
markerX->m_widgetType = SPIN_BOX;
markerX->m_units = "MHz";
markerX->m_getState = ":CALCulate:MARKer1:X?";
markerX->m_setState = ":CALCulate:MARKer1:X %f";
info.m_controls.append(markerX);
VISADevice::VISASensor *markerY = new VISADevice::VISASensor();
markerY->m_name = "Marker Y";
markerY->m_id = "sensor.markery";
markerY->m_type = FLOAT;
markerY->m_units = "dBm";
markerY->m_getState = ":CALCulate:MARKer1:Y?";
info.m_sensors.append(markerY);
}
devices.append(info);
}
emit deviceList(devices);
}
DeviceDiscoverer::ControlInfo *VISADevice::VISAControl::clone() const
{
return new VISAControl(*this);
}
QByteArray VISADevice::VISAControl::serialize() const
{
SimpleSerializer s(1);
s.writeBlob(1, ControlInfo::serialize());
s.writeString(2, m_getState);
s.writeString(3, m_setState);
return s.final();
}
bool VISADevice::VISAControl::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
QByteArray blob;
d.readBlob(1, &blob);
ControlInfo::deserialize(blob);
d.readString(2, &m_getState);
d.readString(3, &m_setState);
return true;
}
else
{
return false;
}
}
DeviceDiscoverer::SensorInfo *VISADevice::VISASensor::clone() const
{
return new VISASensor(*this);
}
QByteArray VISADevice::VISASensor::serialize() const
{
SimpleSerializer s(1);
s.writeBlob(1, SensorInfo::serialize());
s.writeString(2, m_getState);
return s.final();
}
bool VISADevice::VISASensor::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid()) {
return false;
}
if (d.getVersion() == 1)
{
QByteArray blob;
d.readBlob(1, &blob);
SensorInfo::deserialize(blob);
d.readString(2, &m_getState);
return true;
}
else
{
return false;
}
}

85
sdrbase/util/iot/visa.h Normal file
View File

@ -0,0 +1,85 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_IOT_VISA_H
#define INCLUDE_IOT_VISA_H
#include "util/iot/device.h"
#include "util/visa.h"
class SDRBASE_API VISADevice : public Device {
Q_OBJECT
public:
struct SDRBASE_API VISAControl : public DeviceDiscoverer::ControlInfo {
QString m_getState;
QString m_setState;
virtual DeviceDiscoverer::ControlInfo *clone() const override;
virtual QByteArray serialize() const override;
virtual bool deserialize(const QByteArray& data) override;
};
struct SDRBASE_API VISASensor : public DeviceDiscoverer::SensorInfo {
QString m_getState;
virtual DeviceDiscoverer::SensorInfo *clone() const override;
virtual QByteArray serialize() const override;
virtual bool deserialize(const QByteArray& data) override;
};
VISADevice(const QHash<QString, QVariant> settings, const QString &deviceId,
const QStringList &controls, const QStringList &sensors,
DeviceDiscoverer::DeviceInfo *info=nullptr);
~VISADevice();
virtual void getState() override;
using Device::setState;
virtual void setState(const QString &controlId, bool state) override;
virtual void setState(const QString &controlId, int state) override;
virtual void setState(const QString &controlId, float state) override;
virtual void setState(const QString &controlId, const QString &state) override;
virtual QString getProtocol() const override { return "VISA"; }
virtual QString getDeviceId() const override { return m_deviceId; }
private:
QString m_deviceId; // VISA resource (E.g. USB0::0xcafe...)
VISA m_visa;
ViSession m_session;
QStringList m_controls; // Control IDs for getState
QStringList m_sensors; // Sensor IDs for getState
bool m_debugIO;
bool open();
bool convertToBool(const QString &string, bool &ok);
void convert(QHash<QString, QVariant> &status, const QString &id, DeviceDiscoverer::Type type, const QString &state);
};
class SDRBASE_API VISADeviceDiscoverer : public DeviceDiscoverer {
Q_OBJECT
public:
VISADeviceDiscoverer(const QString &resourceFilter = "");
~VISADeviceDiscoverer();
virtual void getDevices() override;
private:
VISA m_visa;
ViSession m_session;
QString m_resourceFilter;
};
#endif /* INCLUDE_IOT_VISA_H */

View File

@ -3,6 +3,7 @@
#include <QString>
#include <QMap>
#include <QDataStream>
#include "dsp/dsptypes.h"
#include "export.h"
@ -26,9 +27,29 @@ public:
void writeString(quint32 id, const QString& value);
void writeBlob(quint32 id, const QByteArray& value);
template<typename T>
void writeList(quint32 id, const QList<T>& value)
{
QByteArray data;
QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly);
(*stream) << value;
delete stream;
writeBlob(id, data);
}
template<typename TK, typename TV>
void writeHash(quint32 id, const QHash<TK,TV>& value)
{
QByteArray data;
QDataStream *stream = new QDataStream(&data, QIODevice::WriteOnly);
(*stream) << value;
delete stream;
writeBlob(id, data);
}
const QByteArray& final();
protected:
// Lists and hashes are written as TBlob
enum Type {
TSigned32 = 0,
TUnsigned32 = 1,
@ -63,6 +84,33 @@ public:
bool readString(quint32 id, QString* result, const QString& def = QString()) const;
bool readBlob(quint32 id, QByteArray* result, const QByteArray& def = QByteArray()) const;
template<typename T>
bool readList(quint32 id, QList<T>* result)
{
QByteArray data;
bool ok = readBlob(id, &data);
if (ok)
{
QDataStream *stream = new QDataStream(data);
(*stream) >> *result;
delete stream;
}
return ok;
}
template<typename TK, typename TV>
bool readHash(quint32 id, QHash<TK,TV>* result)
{
QByteArray data;
bool ok = readBlob(id, &data);
if (ok)
{
QDataStream *stream = new QDataStream(data);
(*stream) >> *result;
delete stream;
}
return ok;
}
bool isValid() const { return m_valid; }
quint32 getVersion() const { return m_version; }
void dump() const;

View File

@ -16,6 +16,7 @@
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QRegularExpression>
#include "visa.h"
@ -48,8 +49,10 @@ VISA::VISA() :
viClose = (ViStatus (*)(ViObject vi)) libraryFunc(visaLibrary, "viClose");
viPrintf = (ViStatus (*) (ViSession vi, ViString writeFmt, ...)) libraryFunc(visaLibrary, "viPrintf");
viScanf = (ViStatus (*) (ViSession vi, ViString writeFmt, ...)) libraryFunc(visaLibrary, "viScanf");
viFindRsrc = (ViStatus (*) (ViSession vi, ViString expr, ViPFindList li, ViPUInt32 retCnt, ViChar desc[])) libraryFunc(visaLibrary, "viFindRsrc");
viFindNext = (ViStatus (*) (ViSession vi, ViChar desc[])) libraryFunc(visaLibrary, "viFindNext");
if (viOpenDefaultRM && viOpen && viClose && viPrintf) {
if (viOpenDefaultRM && viOpen && viClose && viPrintf && viFindRsrc && viFindNext) {
m_available = true;
}
}
@ -106,32 +109,149 @@ void VISA::close(ViSession session)
}
}
QStringList VISA::processCommands(ViSession session, const QString& commands)
QStringList VISA::processCommands(ViSession session, const QString& commands, bool *error)
{
QStringList list = commands.split("\n");
QStringList results;
for (int i = 0; i < list.size(); i++)
if (isAvailable())
{
QString command = list[i].trimmed();
if (!command.isEmpty() && !command.startsWith("#")) // Allow # to comment out lines
QStringList list = commands.split("\n");
ViStatus status;
if (error) {
*error = false;
}
for (int i = 0; i < list.size(); i++)
{
qDebug() << "VISA ->: " << command;
QByteArray bytes = QString("%1\n").arg(command).toLatin1();
char *cmd = bytes.data();
viPrintf(session, cmd);
if (command.endsWith("?"))
QString command = list[i].trimmed();
if (!command.isEmpty() && !command.startsWith("#")) // Allow # to comment out lines
{
char buf[1024] = "";
char format[] = "%t";
viScanf(session, format, buf);
results.append(buf);
qDebug() << "VISA <-: " << QString(buf).trimmed();
if (m_debugIO) {
qDebug() << "VISA ->: " << command;
}
QByteArray bytes = QString("%1\n").arg(command).toLatin1();
char *cmd = bytes.data();
status = viPrintf(session, cmd);
if (error && status) {
*error = true;
}
if (command.contains("?"))
{
char buf[1024] = "";
char format[] = "%t";
status = viScanf(session, format, buf);
if (error && status) {
*error = true;
}
results.append(buf);
if (m_debugIO) {
qDebug() << "VISA <-: " << QString(buf).trimmed();
}
}
}
}
}
else if (error)
{
*error = true;
}
return results;
}
QStringList VISA::findResources()
{
QStringList resources;
if (isAvailable())
{
ViChar rsrc[VI_FIND_BUFLEN];
ViFindList list;
ViRsrc matches = rsrc;
ViUInt32 nMatches = 0;
ViChar expr[] = "?*INSTR";
if (VI_SUCCESS == viFindRsrc(m_defaultRM, expr, &list, &nMatches, matches))
{
if (nMatches > 0)
{
resources.append(QString(rsrc));
while (VI_SUCCESS == viFindNext(list, matches))
{
resources.append(QString(rsrc));
}
}
}
}
return resources;
}
bool VISA::identification(ViSession session, QString &manufacturer, QString &model, QString &serialNumber, QString &revision)
{
if (isAvailable())
{
QStringList result = processCommands(session, "*IDN?");
if ((result.size() == 1) && (!result[0].isEmpty()))
{
QStringList details = result[0].trimmed().split(',');
manufacturer = details[0];
// Some serial devices (ASRLn) loop back the the command if not connected
if (manufacturer == "*IDN?") {
return false;
}
if (details.size() >= 2) {
model = details[1];
}
if (details.size() >= 3) {
serialNumber = details[2];
}
if (details.size() >= 4) {
revision = details[3];
}
qDebug() << "VISA::identification: "
<< "manufacturer: " << manufacturer
<< "model: " << model
<< "serialNumber: " << serialNumber
<< "revision: " << revision;
return true;
}
}
return false;
}
// Filter is a list of resources not to try to open
QList<VISA::Instrument> VISA::instruments(QRegularExpression *filter)
{
QList<VISA::Instrument> instruments;
if (isAvailable())
{
QStringList resourceList = findResources();
for (auto const &resource : resourceList)
{
if (filter)
{
if (filter->match(resource).hasMatch()) {
continue;
}
}
ViSession session = open(resource);
if (session)
{
Instrument instrument;
QString manufacturer, model, serialNumber, revision;
if (identification(session, instrument.m_manufacturer, instrument.m_model, instrument.m_serial, instrument.m_revision))
{
instrument.m_resource = resource;
instruments.append(instrument);
}
close(session);
}
}
}
return instruments;
}
#ifdef _MSC_VER
void *VISA::libraryOpen(const char *filename)

View File

@ -23,6 +23,10 @@
#include "export.h"
#include <QDebug>
class QRegularExpression;
typedef char ViChar;
typedef ViChar * ViPChar;
typedef signed long ViInt32;
@ -35,17 +39,29 @@ typedef ViObject ViSession;
typedef ViSession * ViPSession;
typedef ViString ViRsrc;
typedef ViUInt32 ViAccessMode;
typedef ViUInt32 * ViPUInt32;
typedef ViObject ViFindList;
typedef ViFindList * ViPFindList;
#define VI_SUCCESS 0
#define VI_TRUE 1
#define VI_FALSE 0
#define VI_NULL 0
#define VI_FIND_BUFLEN 256
// We dynamically load the visa dll, as most users probably do not have it
// Note: Can't seem to call viOpenDefaultRM/viClose in constructor / destructor of global instance
class SDRBASE_API VISA {
public:
struct Instrument {
QString m_resource;
QString m_manufacturer;
QString m_model;
QString m_serial;
QString m_revision;
};
// Default session
ViSession m_defaultRM;
// Function pointers to VISA API for direct calls
@ -54,6 +70,8 @@ public:
ViStatus (*viClose) (ViObject vi);
ViStatus (*viPrintf) (ViSession vi, ViString writeFmt, ...);
ViStatus (*viScanf) (ViSession vi, ViString readFmt, ...);
ViStatus (*viFindRsrc) (ViSession vi, ViString expr, ViPFindList li, ViPUInt32 retCnt, ViChar desc[]);
ViStatus (*viFindNext) (ViSession vi, ViChar desc[]);
VISA();
@ -61,7 +79,11 @@ public:
void closeDefault();
ViSession open(const QString& device);
void close(ViSession session);
QStringList processCommands(ViSession session, const QString& commands);
QStringList processCommands(ViSession session, const QString& commands, bool *error=nullptr);
QStringList findResources();
bool identification(ViSession session, QString &manufacturer, QString &model, QString &serialNumber, QString &revision);
QList<Instrument> instruments(QRegularExpression *filter);
void setDebugIO(bool debugIO) { m_debugIO = debugIO; }
// Is the VISA library available
bool isAvailable() const
@ -71,6 +93,7 @@ public:
private:
bool m_available;
bool m_debugIO;
protected:
void *visaLibrary;

View File

@ -65,6 +65,7 @@ set(sdrgui_SOURCES
gui/samplingdevicedialog.cpp
gui/scaleengine.cpp
gui/scaledimage.cpp
gui/scidoublespinbox.cpp
gui/sdrangelsplash.cpp
gui/spectrumcalibrationpointsdialog.cpp
gui/spectrummarkersdialog.cpp
@ -168,6 +169,7 @@ set(sdrgui_HEADERS
gui/samplingdevicedialog.h
gui/scaleengine.h
gui/scaledimage.h
gui/scidoublespinbox.h
gui/sdrangelsplash.h
gui/spectrumcalibrationpointsdialog.h
gui/spectrummarkersdialog.h

View File

@ -51,7 +51,7 @@
#include <QtWidgets>
#include "flowlayout.h"
//! [1]
FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing)
: QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing)
{
@ -63,25 +63,19 @@ FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing)
{
setContentsMargins(margin, margin, margin, margin);
}
//! [1]
//! [2]
FlowLayout::~FlowLayout()
{
QLayoutItem *item;
while ((item = takeAt(0)))
delete item;
}
//! [2]
//! [3]
void FlowLayout::addItem(QLayoutItem *item)
{
itemList.append(item);
}
//! [3]
//! [4]
int FlowLayout::horizontalSpacing() const
{
if (m_hSpace >= 0) {
@ -99,9 +93,7 @@ int FlowLayout::verticalSpacing() const
return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
}
}
//! [4]
//! [5]
int FlowLayout::count() const
{
return itemList.size();
@ -118,16 +110,12 @@ QLayoutItem *FlowLayout::takeAt(int index)
return itemList.takeAt(index);
return nullptr;
}
//! [5]
//! [6]
Qt::Orientations FlowLayout::expandingDirections() const
{
return { };
}
//! [6]
//! [7]
bool FlowLayout::hasHeightForWidth() const
{
return true;
@ -138,9 +126,7 @@ int FlowLayout::heightForWidth(int width) const
int height = doLayout(QRect(0, 0, width, 0), true);
return height;
}
//! [7]
//! [8]
void FlowLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect);
@ -162,9 +148,7 @@ QSize FlowLayout::minimumSize() const
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom());
return size;
}
//! [8]
//! [9]
int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
{
int left, top, right, bottom;
@ -173,39 +157,72 @@ int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
int x = effectiveRect.x();
int y = effectiveRect.y();
int lineHeight = 0;
//! [9]
QList<int> maxLineHeights;
int maxLineHeight = 0;
//! [10]
// In order to support vertical alignment and expanding widgets, we need to first calculate maxLineHeight
// for each row of widgets
if (!testOnly) {
for (QLayoutItem *item : qAsConst(itemList)) {
const QWidget *wid = item->widget();
int spaceX = horizontalSpacing();
if (spaceX == -1)
spaceX = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
int nextX = x + item->sizeHint().width() + spaceX;
if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
x = effectiveRect.x();
nextX = x + item->sizeHint().width() + spaceX;
maxLineHeights.append(lineHeight);
lineHeight = 0;
}
x = nextX;
lineHeight = qMax(lineHeight, item->sizeHint().height());
}
maxLineHeights.append(lineHeight);
maxLineHeight = maxLineHeights.takeFirst();
x = effectiveRect.x();
y = effectiveRect.y();
}
// Now do layout
for (QLayoutItem *item : qAsConst(itemList)) {
const QWidget *wid = item->widget();
int spaceX = horizontalSpacing();
if (spaceX == -1)
spaceX = wid->style()->layoutSpacing(
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
spaceX = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
int spaceY = verticalSpacing();
if (spaceY == -1)
spaceY = wid->style()->layoutSpacing(
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
//! [10]
//! [11]
spaceY = wid->style()->layoutSpacing(QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
int nextX = x + item->sizeHint().width() + spaceX;
if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
x = effectiveRect.x();
y = y + lineHeight + spaceY;
nextX = x + item->sizeHint().width() + spaceX;
if (maxLineHeights.size() > 0) {
maxLineHeight = maxLineHeights.takeFirst();
}
lineHeight = 0;
}
if (!testOnly)
item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
{
QSize size = item->sizeHint();
if (wid && (wid->sizePolicy().verticalPolicy() != QSizePolicy::Fixed)) {
size.setHeight(maxLineHeight);
}
QRect r(QPoint(x, y), size);
item->setGeometry(r);
}
x = nextX;
lineHeight = qMax(lineHeight, item->sizeHint().height());
}
return y + lineHeight - rect.y() + bottom;
}
//! [11]
//! [12]
int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const
{
QObject *parent = this->parent();
@ -218,4 +235,3 @@ int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const
return static_cast<QLayout *>(parent)->spacing();
}
}
//! [12]

View File

@ -51,11 +51,13 @@
#ifndef FLOWLAYOUT_H
#define FLOWLAYOUT_H
#include "export.h"
#include <QLayout>
#include <QRect>
#include <QStyle>
//! [0]
class FlowLayout : public QLayout
class SDRGUI_API FlowLayout : public QLayout
{
public:
explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);

View File

@ -170,14 +170,15 @@ void FramelessWindowResizer::mouseMoveEvent(QMouseEvent* event)
if (m_widget->layout())
{
minSize = m_widget->layout()->minimumSize();
//minSize = m_widget->layout()->minimumSize();
maxSize = m_widget->layout()->maximumSize();
}
else
{
minSize = m_widget->minimumSize();
//minSize = m_widget->minimumSize();
maxSize = m_widget->maximumSize();
}
minSize = m_widget->minimumSizeHint(); // Need to use minimumSizeHint for FlowLayout to work
// Limit requested to size to allowed min/max
QSize size = reqSize;
@ -186,12 +187,12 @@ void FramelessWindowResizer::mouseMoveEvent(QMouseEvent* event)
// Prevent vertical expansion of vertically fixed widgets
if (m_widget->sizePolicy().verticalPolicy() == QSizePolicy::Fixed) {
size.setHeight(m_widget->height());
size.setHeight(m_widget->sizeHint().height());
}
// Prevent horizontal expansion of horizontal fixed widgets
if (m_widget->sizePolicy().horizontalPolicy() == QSizePolicy::Fixed) {
size.setWidth(m_widget->width());
size.setWidth(m_widget->sizeHint().width());
}
// Move

View File

@ -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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "scidoublespinbox.h"
SciDoubleSpinBox::SciDoubleSpinBox(QWidget *parent) :
QDoubleSpinBox(parent)
{
}
double SciDoubleSpinBox::valueFromText(const QString &text) const
{
return text.toDouble();
}
QValidator::State SciDoubleSpinBox::validate(QString &input, int &pos) const
{
bool ok;
input.toDouble(&ok);
if (ok) {
return QValidator::Acceptable;
}
QString validChars = "0123456789+-.e"; // 'C' locale, so no , separator
for (int i = 0; i < input.size(); i++)
{
if (validChars.indexOf(input[i]) == -1) {
return QValidator::Invalid;
}
}
if (input.count('e') > 1) {
return QValidator::Invalid;
}
if (input.count('.') > 1) {
return QValidator::Invalid;
}
if (input.count('+') > 2) {
return QValidator::Invalid;
}
if (input.count('-') > 2) {
return QValidator::Invalid;
}
return QValidator::Intermediate;
}

View File

@ -0,0 +1,39 @@
///////////////////////////////////////////////////////////////////////////////////
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef SDRGUI_GUI_SCIDOUBLESPINBOX_H
#define SDRGUI_GUI_SCIDOUBLESPINBOX_H
#include <QDoubleSpinBox>
#include "export.h"
// Scientific double spin box.
// Like a double spin box, except that we additionally scientific E notation for entry
// (E.g. 4.5e-4)
class SDRGUI_API SciDoubleSpinBox : public QDoubleSpinBox {
Q_OBJECT
public:
explicit SciDoubleSpinBox(QWidget *parent = nullptr);
virtual double valueFromText(const QString &text) const;
virtual QValidator::State validate(QString &input, int &pos) const;
};
#endif // SDRGUI_GUI_SCIDOUBLESPINBOX_H