/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2022-2023 Jon Beniston, M7RCE // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include "feature/featureuiset.h" #include "gui/basicfeaturesettingsdialog.h" #include "gui/flowlayout.h" #include "gui/scidoublespinbox.h" #include "gui/dialogpositioner.h" #include "ui_remotecontrolgui.h" #include "remotecontrol.h" #include "remotecontrolgui.h" #include "remotecontrolsettingsdialog.h" RemoteControlGUI* RemoteControlGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) { RemoteControlGUI* gui = new RemoteControlGUI(pluginAPI, featureUISet, feature); return gui; } void RemoteControlGUI::destroy() { delete this; } void RemoteControlGUI::resetToDefaults() { m_settings.resetToDefaults(); displaySettings(); applySettings(true); } QByteArray RemoteControlGUI::serialize() const { return m_settings.serialize(); } bool RemoteControlGUI::deserialize(const QByteArray& data) { if (m_settings.deserialize(data)) { m_feature->setWorkspaceIndex(m_settings.m_workspaceIndex); displaySettings(); applySettings(true); on_update_clicked(); return true; } else { resetToDefaults(); return false; } } bool RemoteControlGUI::handleMessage(const Message& message) { if (RemoteControl::MsgConfigureRemoteControl::match(message)) { qDebug("RemoteControlGUI::handleMessage: RemoteControl::MsgConfigureRemoteControl"); const RemoteControl::MsgConfigureRemoteControl& cfg = (RemoteControl::MsgConfigureRemoteControl&) message; m_settings = cfg.getSettings(); blockApplySettings(true); displaySettings(); blockApplySettings(false); return true; } else if (RemoteControl::MsgDeviceStatus::match(message)) { const RemoteControl::MsgDeviceStatus& msg = (RemoteControl::MsgDeviceStatus&) message; deviceUpdated(msg.getProtocol(), msg.getDeviceId(), msg.getStatus()); return true; } else if (RemoteControl::MsgDeviceError::match(message)) { const RemoteControl::MsgDeviceError& msg = (RemoteControl::MsgDeviceError&) message; QMessageBox::critical(this, "Remote Control Error", msg.getErrorMessage()); return true; } else if (RemoteControl::MsgDeviceUnavailable::match(message)) { const RemoteControl::MsgDeviceUnavailable& msg = (RemoteControl::MsgDeviceUnavailable&) message; deviceUnavailable(msg.getProtocol(), msg.getDeviceId()); return true; } return false; } void RemoteControlGUI::handleInputMessages() { Message* message; while ((message = getInputMessageQueue()->pop())) { if (handleMessage(*message)) { delete message; } } } void RemoteControlGUI::onWidgetRolled(QWidget* widget, bool rollDown) { (void) widget; (void) rollDown; getRollupContents()->saveState(m_rollupState); applySettings(); } RemoteControlGUI::RemoteControlGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : FeatureGUI(parent), ui(new Ui::RemoteControlGUI), m_pluginAPI(pluginAPI), m_featureUISet(featureUISet), m_doApplySettings(true) { m_feature = feature; setAttribute(Qt::WA_DeleteOnClose, true); m_helpURL = "plugins/feature/remotecontrol/readme.md"; RollupContents *rollupContents = getRollupContents(); ui->setupUi(rollupContents); rollupContents->arrangeRollups(); connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); ui->startStop->setStyleSheet("QToolButton { background-color : blue; }" "QToolButton:checked { background-color : green; }" "QToolButton:disabled { background-color : gray; }"); m_startStopIcon.addFile(":/play.png", QSize(16, 16), QIcon::Normal, QIcon::Off); m_startStopIcon.addFile(":/stop.png", QSize(16, 16), QIcon::Normal, QIcon::On); m_remoteControl = reinterpret_cast(feature); m_remoteControl->setMessageQueueToGUI(&m_inputMessageQueue); m_settings.setRollupState(&m_rollupState); connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); displaySettings(); applySettings(true); makeUIConnections(); m_resizer.enableChildMouseTracking(); } RemoteControlGUI::~RemoteControlGUI() { qDeleteAll(m_deviceGUIs); m_deviceGUIs.clear(); delete ui; } void RemoteControlGUI::setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; m_feature->setWorkspaceIndex(index); } void RemoteControlGUI::blockApplySettings(bool block) { m_doApplySettings = !block; } void RemoteControlGUI::displaySettings() { setTitleColor(m_settings.m_rgbColor); setWindowTitle(m_settings.m_title); setTitle(m_settings.m_title); createGUI(); blockApplySettings(true); getRollupContents()->restoreState(m_rollupState); blockApplySettings(false); getRollupContents()->arrangeRollups(); } void RemoteControlGUI::onMenuDialogCalled(const QPoint &p) { if (m_contextMenuType == ContextMenuType::ContextMenuChannelSettings) { BasicFeatureSettingsDialog dialog(this); dialog.setTitle(m_settings.m_title); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); dialog.setDefaultTitle(m_displayedName); dialog.move(p); new DialogPositioner(&dialog, false); dialog.exec(); m_settings.m_title = dialog.getTitle(); m_settings.m_useReverseAPI = dialog.useReverseAPI(); m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); setTitle(m_settings.m_title); setTitleColor(m_settings.m_rgbColor); applySettings(); } resetContextMenuType(); } void RemoteControlGUI::createControls(RemoteControlDeviceGUI *gui, QBoxLayout *vBox, FlowLayout *flow, int &widgetCnt) { // Create buttons to control the device QGridLayout *controlsGrid = nullptr; if (gui->m_rcDevice->m_verticalControls) { controlsGrid = new QGridLayout(); vBox->addLayout(controlsGrid); } else if (!flow) { flow = new FlowLayout(2, 6, 6); vBox->addItem(flow); } int row = 0; for (auto const &control : gui->m_rcDevice->m_controls) { if (!gui->m_rcDevice->m_verticalControls && (widgetCnt > 0)) { QFrame *line = new QFrame(); line->setFrameShape(QFrame::VLine); line->setFrameShadow(QFrame::Sunken); flow->addWidget(line); } DeviceDiscoverer::ControlInfo *info = gui->m_rcDevice->m_info.getControl(control.m_id); if (!info) { qDebug() << "RemoteControlGUI::createControls: Info missing for " << control.m_id; continue; } if (!control.m_labelLeft.isEmpty()) { QLabel *controlLabelLeft = new QLabel(control.m_labelLeft); if (gui->m_rcDevice->m_verticalControls) { controlsGrid->addWidget(controlLabelLeft, row, 0); controlsGrid->setColumnStretch(row, 0); } else { flow->addWidget(controlLabelLeft); } } QList widgets; QWidget *widget = nullptr; switch (info->m_type) { case DeviceDiscoverer::BOOL: { ButtonSwitch *button = new ButtonSwitch(); button->setToolTip("Start/stop " + info->m_name); button->setIcon(m_startStopIcon); button->setStyleSheet("QToolButton { background-color : blue; }" "QToolButton:checked { background-color : green; }" "QToolButton:disabled { background-color : gray; }"); connect(button, &ButtonSwitch::toggled, [=] (bool toggled) { RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, toggled); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(button); widget = button; } break; case DeviceDiscoverer::INT: { QSpinBox *spinBox = new QSpinBox(); spinBox->setToolTip("Set value for " + info->m_name); spinBox->setMinimum((int)info->m_min); spinBox->setMaximum((int)info->m_max); connect(spinBox, static_cast(&QSpinBox::valueChanged), [=] (int value) { RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, value); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(spinBox); widget = spinBox; } break; case DeviceDiscoverer::FLOAT: { switch (info->m_widgetType) { case DeviceDiscoverer::SPIN_BOX: { QDoubleSpinBox *spinBox = new SciDoubleSpinBox(); spinBox->setToolTip("Set value for " + info->m_name); spinBox->setMinimum(info->m_min); spinBox->setMaximum(info->m_max); spinBox->setDecimals(info->m_precision); connect(spinBox, static_cast(&QDoubleSpinBox::valueChanged), [=] (double value) { RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, (float)value * info->m_scale); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(spinBox); widget = spinBox; } break; case DeviceDiscoverer::DIAL: { widget = new QWidget(); QHBoxLayout *layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); widget->setLayout(layout); QDial *dial = new QDial(); dial->setMaximumSize(24, 24); dial->setToolTip("Set value for " + info->m_name); dial->setMinimum(info->m_min); dial->setMaximum(info->m_max); connect(dial, static_cast(&QDial::valueChanged), [=] (int value) { RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, ((float)value) * info->m_scale); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(dial); layout->addWidget(dial); QLabel *label = new QLabel(QString::number(dial->value())); label->setToolTip("Value for " + info->m_name); widgets.append(label); layout->addWidget(label); } break; case DeviceDiscoverer::SLIDER: { widget = new QWidget(); QHBoxLayout *layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); widget->setLayout(layout); QSlider *slider = new QSlider(Qt::Horizontal); slider->setToolTip("Set value for " + info->m_name); slider->setMinimum(info->m_min); slider->setMaximum(info->m_max); connect(slider, static_cast(&QSlider::valueChanged), [=] (int value) { RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, ((float)value) * info->m_scale); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(slider); layout->addWidget(slider); QLabel *label = new QLabel(QString::number(slider->value())); label->setToolTip("Value for " + info->m_name); widgets.append(label); layout->addWidget(label); } break; } } break; case DeviceDiscoverer::STRING: { QLineEdit *lineEdit = new QLineEdit(); lineEdit->setToolTip("Set value for " + info->m_name); connect(lineEdit, &QLineEdit::editingFinished, [=] () { QString text = lineEdit->text(); RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, text); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(lineEdit); widget = lineEdit; } break; case DeviceDiscoverer::LIST: { QComboBox *combo = new QComboBox(); combo->setToolTip("Set value for " + info->m_name); combo->insertItems(0, info->m_values); connect(combo, &QComboBox::currentTextChanged, [=] (const QString &text) { RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, text); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(combo); widget = combo; } break; case DeviceDiscoverer::BUTTON: { QString label = info->m_name; if (info->m_values.size() > 0) { label = info->m_values[0]; } QToolButton *button = new QToolButton(); button->setText(label); button->setToolTip("Trigger " + info->m_name); connect(button, &QToolButton::clicked, [=] (bool checked) { (void) checked; RemoteControl::MsgDeviceSetState *message = RemoteControl::MsgDeviceSetState::create(gui->m_rcDevice->m_protocol, gui->m_rcDevice->m_info.m_id, control.m_id, 1); m_remoteControl->getInputMessageQueue()->push(message); } ); widgets.append(button); widget = button; } break; default: qDebug() << "RemoteControlGUI::createControls: Unexpected type for control."; break; } gui->m_controls.insert(control.m_id, widgets); if (gui->m_rcDevice->m_verticalControls) { controlsGrid->addWidget(widget, row, 1); } else { flow->addWidget(widget); } if (!control.m_labelRight.isEmpty()) { QLabel *controlLabelRight = new QLabel(control.m_labelRight); if (gui->m_rcDevice->m_verticalControls) { controlsGrid->addWidget(controlLabelRight, row, 2); controlsGrid->setColumnStretch(row, 2); } else { flow->addWidget(controlLabelRight); } } widgetCnt++; row++; } } void RemoteControlGUI::createChart(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, const QString &id, const QString &units) { if (gui->m_chart == nullptr) { // Create a chart to plot the sensor data gui->m_chart = new QChart(); gui->m_chart->setTitle(""); gui->m_chart->legend()->hide(); gui->m_chart->layout()->setContentsMargins(0, 0, 0, 0); gui->m_chart->setMargins(QMargins(1, 1, 1, 1)); gui->m_chart->setTheme(QChart::ChartThemeDark); QLineSeries *series = new QLineSeries(); gui->m_series.insert(id, series); QLineSeries *onePointSeries = new QLineSeries(); gui->m_onePointSeries.insert(id, onePointSeries); gui->m_chart->addSeries(series); QValueAxis *yAxis = new QValueAxis(); QDateTimeAxis *xAxis = new QDateTimeAxis(); xAxis->setFormat(QString("hh:mm:ss")); yAxis->setTitleText(units); gui->m_chart->addAxis(xAxis, Qt::AlignBottom); gui->m_chart->addAxis(yAxis, Qt::AlignLeft); series->attachAxis(xAxis); series->attachAxis(yAxis); gui->m_chartView = new QChartView(); gui->m_chartView->setChart(gui->m_chart); if (m_settings.m_chartHeightFixed) { gui->m_chartView->setMinimumSize(300, m_settings.m_chartHeightPixels); gui->m_chartView->setMaximumSize(16777215, m_settings.m_chartHeightPixels); gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } else { gui->m_chartView->setMinimumSize(300, 130); // 130 is enough to display axis labels gui->m_chartView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); gui->m_chartView->setSceneRect(0, 0, 300, 130); // This determines m_chartView->sizeHint() - default is 640x480, which is a bit big } QBoxLayout *chartLayout = new QVBoxLayout(); gui->m_chartView->setLayout(chartLayout); vBox->addWidget(gui->m_chartView); } else { // Add new series QLineSeries *series = new QLineSeries(); gui->m_series.insert(id, series); QLineSeries *onePointSeries = new QLineSeries(); gui->m_onePointSeries.insert(id, onePointSeries); gui->m_chart->addSeries(series); if (!gui->m_rcDevice->m_commonYAxis) { // Use per series Y axis QValueAxis *yAxis = new QValueAxis(); yAxis->setTitleText(units); gui->m_chart->addAxis(yAxis, Qt::AlignRight); series->attachAxis(yAxis); } else { // Use common y axis QAbstractAxis *yAxis = gui->m_chart->axes(Qt::Vertical)[0]; // Only display units if all the same if (yAxis->titleText() != units) { yAxis->setTitleText(""); } series->attachAxis(yAxis); } series->attachAxis(gui->m_chart->axes(Qt::Horizontal)[0]); } } void RemoteControlGUI::createSensors(RemoteControlDeviceGUI *gui, QVBoxLayout *vBox, FlowLayout *flow, int &widgetCnt, bool &hasCharts) { // Table doesn't seem to expand in a QHBoxLayout, so we have to use a GridLayout QGridLayout *grid = nullptr; QTableWidget *table = nullptr; if (gui->m_rcDevice->m_verticalSensors) { grid = new QGridLayout(); grid->setColumnStretch(0, 1); vBox->addLayout(grid); table = new QTableWidget(gui->m_rcDevice->m_sensors.size(), 3); table->verticalHeader()->setVisible(false); table->horizontalHeader()->setVisible(false); table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); table->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); // Needed so table->sizeHint matches minimumSize set below } else if (!flow) { flow = new FlowLayout(2, 6, 6); vBox->addItem(flow); } int row = 0; bool hasUnits = false; for (auto const &sensor : gui->m_rcDevice->m_sensors) { // For vertical layout, we use a table // For horizontal, we use HBox of labels separated with bars if (gui->m_rcDevice->m_verticalSensors) { if (!sensor.m_labelLeft.isEmpty()) { QTableWidgetItem *sensorLabel = new QTableWidgetItem(sensor.m_labelLeft); sensorLabel->setFlags(Qt::ItemIsEnabled); table->setItem(row, COL_LABEL, sensorLabel); } QTableWidgetItem *valueItem = new QTableWidgetItem("-"); table->setItem(row, COL_VALUE, valueItem); valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); valueItem->setFlags(Qt::ItemIsEnabled); if (!sensor.m_labelRight.isEmpty()) { QTableWidgetItem *unitsItem = new QTableWidgetItem(sensor.m_labelRight); unitsItem->setFlags(Qt::ItemIsEnabled); table->setItem(row, COL_UNITS, unitsItem); hasUnits = true; } gui->m_sensorValueItems.insert(sensor.m_id, valueItem); grid->addWidget(table, 0, 0); } else { if (widgetCnt > 0) { QFrame *line = new QFrame(); line->setFrameShape(QFrame::VLine); line->setFrameShadow(QFrame::Sunken); flow->addWidget(line); } if (!sensor.m_labelLeft.isEmpty()) { QLabel *sensorLabel = new QLabel(sensor.m_labelLeft); flow->addWidget(sensorLabel); } QLabel *sensorValue = new QLabel("-"); flow->addWidget(sensorValue); if (!sensor.m_labelRight.isEmpty()) { QLabel *sensorUnits = new QLabel(sensor.m_labelRight); flow->addWidget(sensorUnits); } gui->m_sensorValueLabels.insert(sensor.m_id, sensorValue); } if (sensor.m_plot) { createChart(gui, vBox, sensor.m_id, sensor.m_labelRight); hasCharts = true; } widgetCnt++; row++; } if (table) { table->resizeColumnToContents(COL_LABEL); if (hasUnits) { table->resizeColumnToContents(COL_UNITS); } else { table->hideColumn(COL_UNITS); } int tableWidth = 0; for (int i = 0; i < table->columnCount(); i++){ tableWidth += table->columnWidth(i); } int tableHeight = 0; for (int i = 0; i < table->rowCount(); i++){ tableHeight += table->rowHeight(i); } table->setMinimumWidth(tableWidth); table->setMinimumHeight(tableHeight+2); } } RemoteControlGUI::RemoteControlDeviceGUI *RemoteControlGUI::createDeviceGUI(RemoteControlDevice *rcDevice) { // Create the UI for the device RemoteControlDeviceGUI *gui = new RemoteControlDeviceGUI(rcDevice); bool hasCharts = false; gui->m_container = new QWidget(getRollupContents()); gui->m_container->setWindowTitle(gui->m_rcDevice->m_label); bool vertical = gui->m_rcDevice->m_verticalControls || gui->m_rcDevice->m_verticalSensors; QVBoxLayout *vBox = new QVBoxLayout(); vBox->setContentsMargins(2, 2, 2, 2); FlowLayout *flow = nullptr; if (!vertical) { flow = new FlowLayout(2, 6, 6); vBox->addItem(flow); } int widgetCnt = 0; // Create buttons to control the device createControls(gui, vBox, flow, widgetCnt); if (gui->m_rcDevice->m_verticalControls) { widgetCnt = 0; } // Create widgets to display the sensor label and its value createSensors(gui, vBox, flow, widgetCnt, hasCharts); gui->m_container->setLayout(vBox); if (hasCharts && !m_settings.m_chartHeightFixed) { gui->m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } gui->m_container->show(); return gui; } void RemoteControlGUI::createGUI() { // Delete existing elements for (auto gui : m_deviceGUIs) { delete gui->m_container; gui->m_container = nullptr; } qDeleteAll(m_deviceGUIs); m_deviceGUIs.clear(); // Create new GUIs for each device bool expanding = false; for (auto device : m_settings.m_devices) { RemoteControlDeviceGUI *gui = createDeviceGUI(device); m_deviceGUIs.append(gui); if (gui->m_container->sizePolicy().verticalPolicy() == QSizePolicy::Expanding) { expanding = true; } } if (expanding) { getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } else { getRollupContents()->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); } // FIXME: Why are these three steps needed to get the window // to resize to the newly added widgets? getRollupContents()->arrangeRollups(); // Recalc rollup size layout()->activate(); // Get QMdiSubWindow to recalc its sizeHint resize(sizeHint()); // Need to do it twice when FlowLayout is used! getRollupContents()->arrangeRollups(); layout()->activate(); resize(sizeHint()); } void RemoteControlGUI::on_startStop_toggled(bool checked) { if (m_doApplySettings) { RemoteControl::MsgStartStop *message = RemoteControl::MsgStartStop::create(checked); m_remoteControl->getInputMessageQueue()->push(message); } } void RemoteControlGUI::on_update_clicked() { RemoteControl::MsgDeviceGetState *message = RemoteControl::MsgDeviceGetState::create(); m_remoteControl->getInputMessageQueue()->push(message); } void RemoteControlGUI::on_settings_clicked() { // Display settings dialog RemoteControlSettingsDialog dialog(&m_settings); if (dialog.exec() == QDialog::Accepted) { createGUI(); applySettings(); on_update_clicked(); } } void RemoteControlGUI::on_clearData_clicked() { // Clear data in all charts for (auto deviceGUI : m_deviceGUIs) { for (auto series : deviceGUI->m_series) { series->clear(); } for (auto series : deviceGUI->m_onePointSeries) { series->clear(); } } } // Update a control widget with latest state value void RemoteControlGUI::updateControl(QWidget *widget, const DeviceDiscoverer::ControlInfo *controlInfo, const QString &key, const QVariant &value) { if (ButtonSwitch *button = qobject_cast(widget)) { if ((QMetaType::Type)value.type() == QMetaType::QString) { if (value.toString() == "unavailable") { button->setStyleSheet("QToolButton { background-color : gray; }" "QToolButton:checked { background-color : gray; }" "QToolButton:disabled { background-color : gray; }"); } else if (value.toString() == "error") { button->setStyleSheet("QToolButton { background-color : red; }" "QToolButton:checked { background-color : red; }" "QToolButton:disabled { background-color : red; }"); } else { qDebug() << "RemoteControlGUI::updateControl: String value for button " << key << value; } } else { int state = value.toInt(); int prev = button->blockSignals(true); button->setChecked(state != 0); button->blockSignals(prev); button->setStyleSheet("QToolButton { background-color : blue; }" "QToolButton:checked { background-color : green; }" "QToolButton:disabled { background-color : gray; }"); } } else if (QSpinBox *spinBox = qobject_cast(widget)) { int prev = spinBox->blockSignals(true); if (value.toString() == "unavailable") { spinBox->setStyleSheet("QSpinBox { background-color : gray; }"); } else if (value.toString() == "error") { spinBox->setStyleSheet("QSpinBox { background-color : red; }"); } else { int state = value.toInt(); bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum()); spinBox->setValue(state); if (outOfRange) { spinBox->setStyleSheet("QSpinBox { background-color : red; }"); } else { spinBox->setStyleSheet(""); } } spinBox->blockSignals(prev); } else if (QDoubleSpinBox *spinBox = qobject_cast(widget)) { int prev = spinBox->blockSignals(true); if (value.toString() == "unavailable") { spinBox->setStyleSheet("QDoubleSpinBox { background-color : gray; }"); } else if (value.toString() == "error") { spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }"); } else { double state = value.toDouble(); if (controlInfo) { state = state / controlInfo->m_scale; } bool outOfRange = (state < spinBox->minimum()) || (state > spinBox->maximum()); spinBox->setValue(state); if (outOfRange) { spinBox->setStyleSheet("QDoubleSpinBox { background-color : red; }"); } else { spinBox->setStyleSheet(""); } } spinBox->blockSignals(prev); } else if (QDial *dial = qobject_cast(widget)) { int prev = dial->blockSignals(true); if (value.toString() == "unavailable") { dial->setStyleSheet("QDial { background-color : gray; }"); } else if (value.toString() == "error") { dial->setStyleSheet("QDial { background-color : red; }"); } else { double state = value.toDouble(); if (controlInfo) { state = state / controlInfo->m_scale; } bool outOfRange = (state < dial->minimum()) || (state > dial->maximum()); dial->setValue(state); if (outOfRange) { dial->setStyleSheet("QDial { background-color : red; }"); } else { dial->setStyleSheet(""); } } dial->blockSignals(prev); } else if (QSlider *slider = qobject_cast(widget)) { int prev = slider->blockSignals(true); if (value.toString() == "unavailable") { slider->setStyleSheet("QSlider { background-color : gray; }"); } else if (value.toString() == "error") { slider->setStyleSheet("QSlider { background-color : red; }"); } else { double state = value.toDouble(); if (controlInfo) { state = state / controlInfo->m_scale; } bool outOfRange = (state < slider->minimum()) || (state > slider->maximum()); slider->setValue(state); if (outOfRange) { slider->setStyleSheet("QSlider { background-color : red; }"); } else { slider->setStyleSheet(""); } } slider->blockSignals(prev); } else if (QComboBox *comboBox = qobject_cast(widget)) { int prev = comboBox->blockSignals(true); QString string = value.toString(); int index = comboBox->findText(string); if (index != -1) { comboBox->setCurrentIndex(index); comboBox->setStyleSheet(""); } else { comboBox->setStyleSheet("QComboBox { background-color : red; }"); } comboBox->blockSignals(prev); } else if (QLineEdit *lineEdit = qobject_cast(widget)) { lineEdit->setText(value.toString()); } else if (QLabel *label = qobject_cast(widget)) { label->setText(value.toString()); } else { qDebug() << "RemoteControlGUI::updateControl: Unexpected widget type"; } } void RemoteControlGUI::updateChart(RemoteControlDeviceGUI *deviceGUI, const QString &key, const QVariant &value) { // Format the value for display bool ok = false; double d = value.toDouble(&ok); bool iOk = false; int iValue = value.toInt(&iOk); QString formattedValue; RemoteControlSensor *sensor = deviceGUI->m_rcDevice->getSensor(key); QString format = sensor->m_format.trimmed(); if (format.contains("%s")) { formattedValue = QString::asprintf(format.toUtf8(), value.toString().toUtf8().data()); } else if (format.contains("%d") || format.contains("%u") || format.contains("%x") || format.contains("%X")) { formattedValue = QString::asprintf(format.toUtf8(), value.toInt()); } else if (((QMetaType::Type)value.type() == QMetaType::Double) || ((QMetaType::Type)value.type() == QMetaType::Float)) { if (format.isEmpty()) { format = "%.1f"; } formattedValue = QString::asprintf(format.toUtf8(), value.toDouble()); } else if (iOk) { formattedValue = QString::asprintf("%d", iValue); } else { formattedValue = value.toString(); } // Update sensor value widget to display the latest value if (deviceGUI->m_sensorValueLabels.contains(key)) { deviceGUI->m_sensorValueLabels.value(key)->setText(formattedValue); } else { deviceGUI->m_sensorValueItems.value(key)->setText(formattedValue); } // Plot value on chart if (deviceGUI->m_series.contains(key)) { QLineSeries *onePointSeries = deviceGUI->m_onePointSeries.value(key); QLineSeries *series = deviceGUI->m_series.value(key); QDateTime dt = QDateTime::currentDateTime(); if (ok) { // Charts aren't displayed properly if series has only one point, // so we save the first point in an additional series: onePointSeries if (onePointSeries->count() == 0) { onePointSeries->append(dt.toMSecsSinceEpoch(), d); } else { if (series->count() == 0) { series->append(onePointSeries->at(0)); } series->append(dt.toMSecsSinceEpoch(), d); QList axes = deviceGUI->m_chart->axes(Qt::Horizontal, series); QDateTimeAxis *dtAxis = (QDateTimeAxis *)axes[0]; QDateTime start = QDateTime::fromMSecsSinceEpoch(series->at(0).x()); QDateTime end = QDateTime::fromMSecsSinceEpoch(series->at(series->count() - 1).x()); if (start.date() == end.date()) { if (start.secsTo(end) < 60*5) { dtAxis->setFormat(QString("hh:mm:ss")); } else { dtAxis->setFormat(QString("hh:mm")); } } else { dtAxis->setFormat(QString("%1 hh:mm").arg(QLocale::system().dateFormat(QLocale::ShortFormat))); } dtAxis->setRange(start, end); axes = deviceGUI->m_chart->axes(Qt::Vertical, series); QValueAxis *yAxis = (QValueAxis *)axes[0]; if (series->count() == 2) { double y1 = series->at(0).y(); double y2 = series->at(1).y(); double yMin = std::min(y1, y2); double yMax = std::max(y1, y2); double min = (yMin >= 0.0) ? yMin * 0.9 : yMin * 1.1; double max = (yMax >= 0.0) ? yMax * 1.1 : yMax * 0.9; yAxis->setRange(min, max); } else { double min = (d >= 0.0) ? d * 0.9 : d * 1.1; double max = (d >= 0.0) ? d * 1.1 : d * 0.9; if (min < yAxis->min()) { yAxis->setMin(min); } if (max > yAxis->max()) { yAxis->setMax(max); } } } } else { qDebug() << "RemoteControlGUI::deviceUpdated: Error converting " << key << value; } } } void RemoteControlGUI::deviceUpdated(const QString &protocol, const QString &deviceId, const QHash &status) { for (auto deviceGUI : m_deviceGUIs) { if ( (protocol == deviceGUI->m_rcDevice->m_protocol) && (deviceId == deviceGUI->m_rcDevice->m_info.m_id)) { deviceGUI->m_container->setEnabled(true); QHashIterator itr(status); while (itr.hasNext()) { itr.next(); QString key = itr.key(); QVariant value = itr.value(); if (deviceGUI->m_controls.contains(key)) { // Update control(s) to display latest state QList widgets = deviceGUI->m_controls.value(key); DeviceDiscoverer::ControlInfo *control = deviceGUI->m_rcDevice->m_info.getControl(key); for (auto widget : widgets) { updateControl(widget, control, key, value); } } else if (deviceGUI->m_sensorValueLabels.contains(key) || deviceGUI->m_sensorValueItems.contains(key)) { // Plot on chart updateChart(deviceGUI, key, value); } else { qDebug() << "RemoteControlGUI::deviceUpdated: Unexpected status key " << key << value; } } } } } void RemoteControlGUI::deviceUnavailable(const QString &protocol, const QString &deviceId) { for (auto deviceGUI : m_deviceGUIs) { if ( (protocol == deviceGUI->m_rcDevice->m_protocol) && (deviceId == deviceGUI->m_rcDevice->m_info.m_id)) { deviceGUI->m_container->setEnabled(false); } } } void RemoteControlGUI::applySettings(bool force) { if (m_doApplySettings) { RemoteControl::MsgConfigureRemoteControl* message = RemoteControl::MsgConfigureRemoteControl::create(m_settings, force); m_remoteControl->getInputMessageQueue()->push(message); } } void RemoteControlGUI::makeUIConnections() { QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &RemoteControlGUI::on_startStop_toggled); QObject::connect(ui->update, &QToolButton::clicked, this, &RemoteControlGUI::on_update_clicked); QObject::connect(ui->settings, &QToolButton::clicked, this, &RemoteControlGUI::on_settings_clicked); QObject::connect(ui->clearData, &QToolButton::clicked, this, &RemoteControlGUI::on_clearData_clicked); }