Files
edgetx-gx12/companion/src/simulation/radiooutputswidget.cpp
T
Max PapernoandBertrand Songis 57dc0159d6 [Simulator] Asynchronous SimulatorInterface & a few new features. (#4738)
* [Simulator] Create RadioKeyWidget class for UI buttons & refactor ButtonsWidget; Refactor SimulatedUIWidget (and subtypes) to use new RadioKeyWidgets/ButtonsWidget; Centralize help text for key mappings and get creative with some icons; Simplify some radio UI setups with rectangular buttons.

* [Simulator] Convert all simulator data I/O to signals/slots mechanism:
    * SimulatorInterface/OpenTxSimulator:
       - Now inherits from QObject to allow signal/slot interface;
       - Allows data exchange on a per-item basis (eg. each I/O value is treated separately instead of sending whole arrays or structs of data);
       - Checks for data changes and only emits signals when change is detected (GUI can now assume only new values are being sent);
       - Manages its own 10ms timer (doesn't rely on GUI to do that);
       - Sends "heartbeat" signals @ 1Hz for status monitoring;
    * Simulator GUI:
       - All data is exchanged between GUI elements as well as SimulatorInterface via signals/slots using standardized methods;
       - Data is sent immediately, and only, when actually changed (eg. a control is moved) instead of in bulk at specific time intervals;
       - Similarly, an asynchronous method is used for reading incoming data, w/out timers or loops;
       - Improve VirtualJoystickWidget to be more encapsulated and configurable;
       - Pause telemetry simulator if window is hidden;

* [Simulator] Move SimulatorInterface instance to separate thread, ensure safe asynchronous operations & proper timer interactions; Protect/remove some functions, & reorganize the order (cosmetics).

* [Simulator] Traces are now delivered to OpenTxSimulator and one or more QIODevice(s) can be added as recipient(s); Add SimulatorInterface::getCapability() for compile-time settings; Remove reversed POT1/SLIDER1 mixer exception (Taranis) requirement for SIMU; Fix plus/minus key delay on wheel event w/out encoder.

* [Simulator] Add current knob/slider/trim input value in tool-tips (KnobWidget and SliderWidget).

* [Simulator] Fix trims widget internal value not properly updating, and remove trim influence on virtual joystick X/Y value display (closes #4671).

* [SimulatorInterface] Add handling of transmitter input voltage, including a rough conversion of volts to ADC value for different boards, and default battery volts lookup function; Clear analogs array before starting.

* [Simulator] Add SimulatorInterface::init() method to separate pre-startup tasks; Report actual trim range, not just extended on/off; Change how radio widget states are restored; VirtualJoystickWidget: Connect trim changes directly from simulator, connect joystick events directly, report stick mode directly instead of setting values/constraints externally.

* [Simulator] Calculate default Tx V input based on configured range in radio settings (or warning V+2 for radios which don't support a range).

* [Simulator] Add functional aux. trims for Horus (closes #4699).

* [Companion] Remove problematic QMessageLogContext from AppDebugMessageHandler::messageOutput().

* [Simulator] Prevent trim change via slider if disabled for flight mode (closes #4600).

* [OpenTxSimulator] Fixes for Qt < 5.4.

* [OpenTxSimulator] Fix slot name.
2017-04-02 12:17:37 +02:00

346 lines
10 KiB
C++

/*
* Copyright (C) OpenTX
*
* Based on code named
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* 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 for more details.
*/
#include "radiooutputswidget.h"
#include "ui_radiooutputswidget.h"
#include "appdata.h"
#include "constants.h"
#include "eeprominterface.h"
#include "radiodata.h"
#include "simulator.h"
extern AppData g; // ensure what "g" means
const quint16 RadioOutputsWidget::m_savedViewStateVersion = 1;
RadioOutputsWidget::RadioOutputsWidget(SimulatorInterface * simulator, Firmware * firmware, QWidget *parent) :
QWidget(parent),
m_simulator(simulator),
m_firmware(firmware),
m_radioProfileId(g.sessionId()),
ui(new Ui::RadioOutputsWidget)
{
ui->setupUi(this);
restoreState();
connect(m_simulator, &SimulatorInterface::channelOutValueChange, this, &RadioOutputsWidget::onChannelOutValueChange);
connect(m_simulator, &SimulatorInterface::virtualSwValueChange, this, &RadioOutputsWidget::onVirtSwValueChange);
connect(m_simulator, &SimulatorInterface::gVarValueChange, this, &RadioOutputsWidget::onGVarValueChange);
connect(m_simulator, &SimulatorInterface::phaseChanged, this, &RadioOutputsWidget::onPhaseChanged);
}
RadioOutputsWidget::~RadioOutputsWidget()
{
//stop();
saveState();
delete ui;
}
void RadioOutputsWidget::changeEvent(QEvent *e)
{
QWidget::changeEvent(e);
switch (e->type()) {
case QEvent::LanguageChange:
ui->retranslateUi(this);
break;
default:
break;
}
}
void RadioOutputsWidget::start()
{
setupChannelsDisplay();
setupGVarsDisplay();
setupLsDisplay();
}
//void RadioOutputsWidget::stop()
//{
//}
void RadioOutputsWidget::restart()
{
//stop();
start();
}
void RadioOutputsWidget::saveState()
{
QByteArray state;
QDataStream stream(&state, QIODevice::WriteOnly);
stream << m_savedViewStateVersion
<< ui->btnLogiSw->isChecked() << ui->btnGlobalVars->isChecked() << ui->btnChannels->isChecked()
<< ui->splitter->saveState();
SimulatorOptions opts = g.profile[m_radioProfileId].simulatorOptions();
opts.radioOutputsState = state;
g.profile[m_radioProfileId].simulatorOptions(opts);
}
void RadioOutputsWidget::restoreState()
{
quint16 ver = 0;
QByteArray splitterState;
bool ls = true, gv = true, ch = true;
QByteArray state = g.profile[m_radioProfileId].simulatorOptions().radioOutputsState;
QDataStream stream(state);
stream >> ver;
if (ver && ver <= m_savedViewStateVersion)
stream >> ls >> gv >> ch >> splitterState;
ui->btnLogiSw->setChecked(ls);
ui->btnGlobalVars->setChecked(gv);
ui->btnChannels->setChecked(ch);
if (!splitterState.isEmpty())
ui->splitter->restoreState(splitterState);
}
void RadioOutputsWidget::setupChannelsDisplay()
{
int outputs = std::min(32, m_firmware->getCapability(Capability(Outputs)));
// delete old widgets if already exist
m_channelsMap.clear();
QWidget * oldChanW = ui->channelsScroll->takeWidget();
if (oldChanW)
oldChanW->deleteLater();
if (!outputs)
return;
QWidget * channelsWidget = new QWidget();
QGridLayout * channelsLayout = new QGridLayout(channelsWidget);
channelsLayout->setHorizontalSpacing(4);
channelsLayout->setVerticalSpacing(3);
channelsLayout->setContentsMargins(5, 5, 5, 5);
ui->channelsScroll->setWidget(channelsWidget);
// populate outputs
int column = 0;
for (int i=0; i < outputs; i++) {
QLabel * label = new QLabel(channelsWidget);
label->setText(" " + RawSource(SOURCE_TYPE_CH, i).toString() + " ");
label->setAlignment(Qt::AlignCenter);
label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
channelsLayout->addWidget(label, 0, column, 1, 1);
QSlider * slider = new QSlider(channelsWidget);
slider->setEnabled(false);
slider->setMinimum(-1024);
slider->setMaximum(1024);
slider->setPageStep(128);
slider->setTracking(false);
slider->setOrientation(Qt::Vertical);
slider->setInvertedAppearance(false);
slider->setTickPosition(QSlider::TicksRight);
slider->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
channelsLayout->addWidget(slider, 2, column, 1, 1);
channelsLayout->setAlignment(slider, Qt::AlignHCenter);
QLabel * value = new QLabel(channelsWidget);
value->setMinimumSize(QSize(value->fontMetrics().size(Qt::TextSingleLine, "-100.0").width(), 0));
value->setAlignment(Qt::AlignCenter);
value->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
channelsLayout->addWidget(value, 1, column, 1, 1);
++column;
m_channelsMap.insert(i, QPair<QLabel *, QSlider *>(value, slider));
}
}
void RadioOutputsWidget::setupGVarsDisplay()
{
int gvars = m_firmware->getCapability(Capability(Gvars));
int fmodes = m_firmware->getCapability(Capability(FlightModes));
// delete old widgets if already exist
m_globalVarsMap.clear();
QWidget * oldGv = ui->globalVarsScroll->takeWidget();
if (oldGv)
oldGv->deleteLater();
if (!gvars)
return;
QWidget * gvarsWidget = new QWidget();
QGridLayout * gvarsLayout = new QGridLayout(gvarsWidget);
gvarsLayout->setContentsMargins(5, 5, 5, 5);
gvarsLayout->setHorizontalSpacing(6);
gvarsLayout->setVerticalSpacing(3);
ui->globalVarsScroll->setWidget(gvarsWidget);
QPalette::ColorRole bgrole = QPalette::AlternateBase;
for (int fm=0; fm < fmodes; fm++) {
QLabel * label = new QLabel(gvarsWidget);
label->setText(QString("FM%1").arg(fm));
label->setAlignment(Qt::AlignCenter);
label->setBackgroundRole(bgrole);
gvarsLayout->addWidget(label, 0, fm+1);
}
QHash<int, QLabel *> fmMap;
for (int gv=0; gv < gvars; gv++) {
bgrole = ((gv % 2) ? QPalette::Background : QPalette::AlternateBase);
QLabel * label = new QLabel(gvarsWidget);
label->setText(QString("GV%1").arg(gv+1));
label->setAutoFillBackground(true);
label->setBackgroundRole(bgrole);
gvarsLayout->addWidget(label, gv+1, 0);
for (int fm=0; fm < fmodes; fm++) {
QLabel * value = new QLabel(gvarsWidget);
value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
value->setAutoFillBackground(true);
value->setBackgroundRole(bgrole);
value->setText("0");
value->setStyleSheet("padding-right: .06em;");
gvarsLayout->addWidget(value, gv+1, fm+1);
fmMap.insert(fm, value);
}
m_globalVarsMap.insert(gv, fmMap);
}
}
void RadioOutputsWidget::setupLsDisplay()
{
int switches = m_firmware->getCapability(LogicalSwitches);
// delete old widgets if already exist
m_logicSwitchMap.clear();
QWidget * oldLsW = ui->logicalSwitchesScroll->takeWidget();
if (oldLsW)
oldLsW->deleteLater();
if (!switches)
return;
QWidget * logicalSwitches = new QWidget();
logicalSwitches->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
QGridLayout * logicalSwitchesLayout = new QGridLayout(logicalSwitches);
logicalSwitchesLayout->setHorizontalSpacing(3);
logicalSwitchesLayout->setVerticalSpacing(2);
logicalSwitchesLayout->setContentsMargins(5, 5, 5, 5);
ui->logicalSwitchesScroll->setWidget(logicalSwitches);
// populate logical switches
int rows = switches / (switches > 16 ? 4 : 2);
for (int i=0; i < switches; i++) {
logicalSwitchesLayout->addWidget(createLogicalSwitch(logicalSwitches, i), i / rows, i % rows, 1, 1);
}
}
QWidget * RadioOutputsWidget::createLogicalSwitch(QWidget * parent, int switchNo)
{
QLabel * swtch = new QLabel(parent);
swtch->setAutoFillBackground(true);
swtch->setFrameStyle(QFrame::Panel | QFrame::Raised);
swtch->setLineWidth(2);
swtch->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
QFont font = swtch->font();
font.setBold(true);
swtch->setFont(font);
swtch->setMinimumWidth(swtch->fontMetrics().width("99") + 10);
font.setBold(false);
swtch->setFont(font);
swtch->setText(QString("%1").arg(RawSwitch(SWITCH_TYPE_VIRTUAL, switchNo+1).toString().remove("L"), 2, QLatin1Char('0')));
swtch->setAlignment(Qt::AlignCenter);
m_logicSwitchMap.insert(switchNo, swtch);
return swtch;
}
void RadioOutputsWidget::onChannelOutValueChange(quint8 index, qint32 value)
{
if (m_channelsMap.contains(index)) {
QPair<QLabel *, QSlider *> ch = m_channelsMap.value(index);
ch.first->setText(QString("%1%").arg(calcRESXto100(value)));
ch.second->setValue(qMin(1024, qMax(-1024, value)));
}
//qDebug() << index << value;
}
void RadioOutputsWidget::onVirtSwValueChange(quint8 index, qint32 value)
{
if (!m_logicSwitchMap.contains(index))
return;
QLabel * ls = m_logicSwitchMap.value(index);
ls->setBackgroundRole(value ? QPalette::Dark : QPalette::Background);
ls->setForegroundRole(value ? QPalette::BrightText : QPalette::WindowText);
ls->setFrameShadow(value ? QFrame::Sunken : QFrame::Raised);
QFont font = ls->font();
font.setBold((bool)value);
ls->setFont(font);
//qDebug() << index << value;
}
void RadioOutputsWidget::onGVarValueChange(quint8 index, qint32 value)
{
if (!m_globalVarsMap.contains(index))
return;
QHash<int, QLabel *> fmMap = m_globalVarsMap.value(index);
SimulatorInterface::gVarMode_t gv(value);
if (fmMap.contains(gv.mode)) {
QLabel * lbl = fmMap.value(gv.mode);
if (lbl)
lbl->setText(QString::number(gv.value));
}
//qDebug() << index << value << gv.mode << gv.value;
}
void RadioOutputsWidget::onPhaseChanged(qint32 phase, const QString &)
{
QPalette::ColorRole fgrole, bgrole;
QLabel * lbl;
QFont font;
QHash<int, QHash<int, QLabel *> >::const_iterator gv;
QHash<int, QLabel *>::const_iterator fm;
for (gv = m_globalVarsMap.constBegin(); gv != m_globalVarsMap.constEnd(); ++gv) {
for (fm = gv.value().constBegin(); fm != gv.value().constEnd(); ++fm) {
lbl = fm.value();
font = lbl->font();
if (fm.key() == phase) {
fgrole = QPalette::BrightText;
bgrole = QPalette::Dark;
font.setBold(true);
}
else {
fgrole = QPalette::WindowText;
bgrole = ((gv.key() % 2) ? QPalette::Background : QPalette::AlternateBase);
font.setBold(false);
}
lbl->setForegroundRole(fgrole);
lbl->setBackgroundRole(bgrole);
lbl->setFont(font);
}
}
//qDebug() << phase;
}