mirror of
https://github.com/EdgeTX/edgetx.git
synced 2025-07-21 23:35:12 +03:00
* [simulation] Hardening: Fix some potential crashes & 26 memory leaks, improve LCD performance. * [simpgmspace] Init trims; Make sure `REa` is really defined (to match board files). * [simpgmspace][opentxsimulator] Verify current running state before start/stop; Move rotary enc. init. * [simpgmspace][LcdWidget] Improve performance by moving LCD content change check to lcdRefresh() & limiting LcdWidget refresh time to 60 fps max. * [simueeprom] Ensure thread could be started, set default running state to false. * [simufatfs] Fix paths report trace. * [eepromimportexport] Fix memory leaks resulting from import debugging scheme being used. * [customdebug] Introduce new scheme for custom debug output in compliance with Qt recommendations (see docs for QLoggingCategory). * [opentxeeprom] Fix extra conversion table cache elements being created and also not properly deleted (and hence leaking). * [opentxinterface] Unregister EEpromInterfaces in unregisterOpenTxFirmwares(); * [storage] Unregister storage factories on exit (fixes leak); create virtual StorageFormat/StorageFactory destructors (prevents warnings). * [helpers] GVarGroup now emits own signal, no need to pass ModelPanel pointer (removes dependency on modeledit.h) * [DebugOutput] Clear simulator trace hook before exiting (prevent possible issues); Fix leak and possible bad QString allocations when reading from buffer; Fix leak with combo box event filter. * [TelemetrySimulator] Fix leak by deleting LogPlaybackController object on exit; Only set up data fields once; convert timers to static. * [build] Consolidate all Companion/Simulator shared items in `common` library to reduce build time/etc (node & edge still remain awkward). * [simulatorwidget] Delete removed spacer object (previously-forgotten "26th" leak). * [DebugOutput] Fix stray trailing characters issue with new text buffer allocation (from previous commit); Increase maximum buffer sizes to better accommodate slower systems. * Cosmetics.
1125 lines
31 KiB
C++
1125 lines
31 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 <QtGui>
|
|
#if defined _MSC_VER
|
|
#include <io.h>
|
|
#include <stdio.h>
|
|
#elif defined __GNUC__
|
|
#include <unistd.h>
|
|
#endif
|
|
#if defined(WIN32) && defined(WIN_USE_CONSOLE_STDIO)
|
|
#include "windows.h"
|
|
#endif
|
|
|
|
#include "appdata.h"
|
|
#include "helpers.h"
|
|
#include "simulatormainwindow.h"
|
|
#include "storage/sdcard.h"
|
|
|
|
#include <QLabel>
|
|
#include <QMessageBox>
|
|
|
|
Stopwatch gStopwatch("global");
|
|
|
|
const QColor colors[CPN_MAX_CURVES] = {
|
|
QColor(0,0,127),
|
|
QColor(0,127,0),
|
|
QColor(127,0,0),
|
|
QColor(0,127,127),
|
|
QColor(127,0,127),
|
|
QColor(127,127,0),
|
|
QColor(127,127,127),
|
|
QColor(0,0,255),
|
|
QColor(0,127,255),
|
|
QColor(127,0,255),
|
|
QColor(0,255,0),
|
|
QColor(0,255,127),
|
|
QColor(127,255,0),
|
|
QColor(255,0,0),
|
|
QColor(255,0,127),
|
|
QColor(255,127,0),
|
|
QColor(0,0,127),
|
|
QColor(0,127,0),
|
|
QColor(127,0,0),
|
|
QColor(0,127,127),
|
|
QColor(127,0,127),
|
|
QColor(127,127,0),
|
|
QColor(127,127,127),
|
|
QColor(0,0,255),
|
|
QColor(0,127,255),
|
|
QColor(127,0,255),
|
|
QColor(0,255,0),
|
|
QColor(0,255,127),
|
|
QColor(127,255,0),
|
|
QColor(255,0,0),
|
|
QColor(255,0,127),
|
|
QColor(255,127,0),
|
|
};
|
|
|
|
void populateGvSourceCB(QComboBox *b, int value)
|
|
{
|
|
QString strings[] = { QObject::tr("---"), QObject::tr("Rud Trim"), QObject::tr("Ele Trim"), QObject::tr("Thr Trim"), QObject::tr("Ail Trim"), QObject::tr("Rot Enc"), QObject::tr("Rud"), QObject::tr("Ele"), QObject::tr("Thr"), QObject::tr("Ail"), QObject::tr("P1"), QObject::tr("P2"), QObject::tr("P3")};
|
|
b->clear();
|
|
for (int i=0; i<= 12; i++) {
|
|
b->addItem(strings[i]);
|
|
}
|
|
b->setCurrentIndex(value);
|
|
}
|
|
|
|
void populateFileComboBox(QComboBox * b, const QSet<QString> & set, const QString & current)
|
|
{
|
|
b->clear();
|
|
b->addItem("----");
|
|
|
|
bool added = false;
|
|
// Convert set into list and sort it alphabetically case insensitive
|
|
QStringList list = QStringList::fromSet(set);
|
|
qSort(list.begin(), list.end(), caseInsensitiveLessThan);
|
|
foreach (QString entry, list) {
|
|
b->addItem(entry);
|
|
if (entry == current) {
|
|
b->setCurrentIndex(b->count()-1);
|
|
added = true;
|
|
}
|
|
}
|
|
|
|
if (!added && !current.isEmpty()) {
|
|
b->addItem(current);
|
|
b->setCurrentIndex(b->count()-1);
|
|
}
|
|
}
|
|
|
|
void getFileComboBoxValue(QComboBox * b, char * dest, int length)
|
|
{
|
|
memset(dest, 0, length+1);
|
|
if (b->currentText() != "----") {
|
|
strncpy(dest, b->currentText().toLatin1(), length);
|
|
}
|
|
}
|
|
|
|
void populatePhasesCB(QComboBox *b, int value)
|
|
{
|
|
for (int i=-getCurrentFirmware()->getCapability(FlightModes); i<=getCurrentFirmware()->getCapability(FlightModes); i++) {
|
|
if (i < 0)
|
|
b->addItem(QObject::tr("!Flight mode %1").arg(-i-1), i);
|
|
else if (i > 0)
|
|
b->addItem(QObject::tr("Flight mode %1").arg(i-1), i);
|
|
else
|
|
b->addItem(QObject::tr("----"), 0);
|
|
}
|
|
b->setCurrentIndex(value + getCurrentFirmware()->getCapability(FlightModes));
|
|
}
|
|
|
|
/*
|
|
* GVarGroup
|
|
*/
|
|
|
|
GVarGroup::GVarGroup(QCheckBox * weightGV, QAbstractSpinBox * weightSB, QComboBox * weightCB, int & weight, const ModelData & model, const int deflt, const int mini, const int maxi, const double step, bool allowGvars):
|
|
QObject(),
|
|
weightGV(weightGV),
|
|
weightSB(weightSB),
|
|
sb(dynamic_cast<QSpinBox *>(weightSB)),
|
|
dsb(dynamic_cast<QDoubleSpinBox *>(weightSB)),
|
|
weightCB(weightCB),
|
|
weight(weight),
|
|
step(step),
|
|
lock(true)
|
|
{
|
|
if (allowGvars && getCurrentFirmware()->getCapability(Gvars)) {
|
|
populateGVCB(*weightCB, weight, model);
|
|
connect(weightGV, SIGNAL(stateChanged(int)), this, SLOT(gvarCBChanged(int)));
|
|
connect(weightCB, SIGNAL(currentIndexChanged(int)), this, SLOT(valuesChanged()));
|
|
}
|
|
else {
|
|
weightGV->hide();
|
|
if (weight > maxi || weight < mini) {
|
|
weight = deflt;
|
|
}
|
|
}
|
|
|
|
int val;
|
|
|
|
if (weight>maxi || weight<mini) {
|
|
val = deflt;
|
|
weightGV->setChecked(true);
|
|
weightSB->hide();
|
|
weightCB->show();
|
|
}
|
|
else {
|
|
val = weight;
|
|
weightGV->setChecked(false);
|
|
weightSB->show();
|
|
weightCB->hide();
|
|
}
|
|
|
|
if (sb) {
|
|
sb->setMinimum(mini);
|
|
sb->setMaximum(maxi);
|
|
sb->setValue(val);
|
|
}
|
|
else {
|
|
dsb->setMinimum(mini*step);
|
|
dsb->setMaximum(maxi*step);
|
|
dsb->setValue(val*step);
|
|
}
|
|
|
|
connect(weightSB, SIGNAL(editingFinished()), this, SLOT(valuesChanged()));
|
|
|
|
lock = false;
|
|
}
|
|
|
|
void GVarGroup::gvarCBChanged(int state)
|
|
{
|
|
weightCB->setVisible(state);
|
|
if (weightSB)
|
|
weightSB->setVisible(!state);
|
|
else
|
|
weightSB->setVisible(!state);
|
|
valuesChanged();
|
|
}
|
|
|
|
void GVarGroup::valuesChanged()
|
|
{
|
|
if (!lock) {
|
|
if (weightGV->isChecked())
|
|
weight = weightCB->itemData(weightCB->currentIndex()).toInt();
|
|
else if (sb)
|
|
weight = sb->value();
|
|
else
|
|
weight = round(dsb->value()/step);
|
|
|
|
emit valueChanged();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* CurveGroup
|
|
*/
|
|
|
|
CurveGroup::CurveGroup(QComboBox * curveTypeCB, QCheckBox * curveGVarCB, QComboBox * curveValueCB, QSpinBox * curveValueSB, CurveReference & curve, const ModelData & model, unsigned int flags):
|
|
QObject(),
|
|
curveTypeCB(curveTypeCB),
|
|
curveGVarCB(curveGVarCB),
|
|
curveValueCB(curveValueCB),
|
|
curveValueSB(curveValueSB),
|
|
curve(curve),
|
|
model(model),
|
|
flags(flags),
|
|
lock(false),
|
|
lastType(-1)
|
|
{
|
|
if (!(flags & HIDE_DIFF)) curveTypeCB->addItem(tr("Diff"), 0);
|
|
if (!(flags & HIDE_EXPO)) curveTypeCB->addItem(tr("Expo"), 1);
|
|
curveTypeCB->addItem(tr("Func"), 2);
|
|
curveTypeCB->addItem(tr("Curve"), 3);
|
|
|
|
curveValueCB->setMaxVisibleItems(10);
|
|
|
|
connect(curveTypeCB, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged(int)));
|
|
connect(curveGVarCB, SIGNAL(stateChanged(int)), this, SLOT(gvarCBChanged(int)));
|
|
connect(curveValueCB, SIGNAL(currentIndexChanged(int)), this, SLOT(valuesChanged()));
|
|
connect(curveValueSB, SIGNAL(editingFinished()), this, SLOT(valuesChanged()));
|
|
|
|
update();
|
|
}
|
|
|
|
void CurveGroup::update()
|
|
{
|
|
lock = true;
|
|
|
|
int found = curveTypeCB->findData(curve.type);
|
|
if (found < 0) found = 0;
|
|
curveTypeCB->setCurrentIndex(found);
|
|
|
|
if (curve.type == CurveReference::CURVE_REF_DIFF || curve.type == CurveReference::CURVE_REF_EXPO) {
|
|
curveGVarCB->setVisible(getCurrentFirmware()->getCapability(Gvars));
|
|
if (curve.value > 100 || curve.value < -100) {
|
|
curveGVarCB->setChecked(true);
|
|
if (lastType != CurveReference::CURVE_REF_DIFF && lastType != CurveReference::CURVE_REF_EXPO) {
|
|
lastType = curve.type;
|
|
populateGVCB(*curveValueCB, curve.value, model);
|
|
}
|
|
curveValueCB->show();
|
|
curveValueSB->hide();
|
|
}
|
|
else {
|
|
curveGVarCB->setChecked(false);
|
|
curveValueSB->setMinimum(-100);
|
|
curveValueSB->setMaximum(100);
|
|
curveValueSB->setValue(curve.value);
|
|
curveValueSB->show();
|
|
curveValueCB->hide();
|
|
}
|
|
}
|
|
else {
|
|
curveGVarCB->hide();
|
|
curveValueSB->hide();
|
|
curveValueCB->show();
|
|
switch (curve.type) {
|
|
case CurveReference::CURVE_REF_FUNC:
|
|
if (lastType != curve.type) {
|
|
lastType = curve.type;
|
|
curveValueCB->clear();
|
|
for (int i=0; i<=6/*TODO constant*/; i++) {
|
|
curveValueCB->addItem(CurveReference(CurveReference::CURVE_REF_FUNC, i).toString());
|
|
}
|
|
}
|
|
curveValueCB->setCurrentIndex(curve.value);
|
|
break;
|
|
case CurveReference::CURVE_REF_CUSTOM:
|
|
{
|
|
int numcurves = getCurrentFirmware()->getCapability(NumCurves);
|
|
if (lastType != curve.type) {
|
|
lastType = curve.type;
|
|
curveValueCB->clear();
|
|
for (int i= ((flags & HIDE_NEGATIVE_CURVES) ? 0 : -numcurves); i<=numcurves; i++) {
|
|
curveValueCB->addItem(CurveReference(CurveReference::CURVE_REF_CUSTOM, i).toString(), i);
|
|
if (i == curve.value) {
|
|
curveValueCB->setCurrentIndex(curveValueCB->count() - 1);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
lock = false;
|
|
}
|
|
|
|
void CurveGroup::gvarCBChanged(int state)
|
|
{
|
|
if (!lock) {
|
|
if (state) {
|
|
curve.value = 10000+1; // TODO constant in EEpromInterface ...
|
|
lastType = -1; // quickfix for issue #3518: force refresh of curveValueCB at next update() to set current index to GV1
|
|
}
|
|
else {
|
|
curve.value = 0; // TODO could be better
|
|
}
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
void CurveGroup::typeChanged(int value)
|
|
{
|
|
if (!lock) {
|
|
int type = curveTypeCB->itemData(curveTypeCB->currentIndex()).toInt();
|
|
switch (type) {
|
|
case 0:
|
|
curve = CurveReference(CurveReference::CURVE_REF_DIFF, 0);
|
|
break;
|
|
case 1:
|
|
curve = CurveReference(CurveReference::CURVE_REF_EXPO, 0);
|
|
break;
|
|
case 2:
|
|
curve = CurveReference(CurveReference::CURVE_REF_FUNC, 0);
|
|
break;
|
|
case 3:
|
|
curve = CurveReference(CurveReference::CURVE_REF_CUSTOM, 0);
|
|
break;
|
|
}
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
void CurveGroup::valuesChanged()
|
|
{
|
|
if (!lock) {
|
|
switch (curveTypeCB->itemData(curveTypeCB->currentIndex()).toInt()) {
|
|
case 0:
|
|
case 1:
|
|
{
|
|
int value;
|
|
if (curveGVarCB->isChecked())
|
|
value = curveValueCB->itemData(curveValueCB->currentIndex()).toInt();
|
|
else
|
|
value = curveValueSB->value();
|
|
curve = CurveReference(curveTypeCB->itemData(curveTypeCB->currentIndex()).toInt() == 0 ? CurveReference::CURVE_REF_DIFF : CurveReference::CURVE_REF_EXPO, value);
|
|
break;
|
|
}
|
|
case 2:
|
|
curve = CurveReference(CurveReference::CURVE_REF_FUNC, curveValueCB->currentIndex());
|
|
break;
|
|
case 3:
|
|
curve = CurveReference(CurveReference::CURVE_REF_CUSTOM, curveValueCB->itemData(curveValueCB->currentIndex()).toInt());
|
|
break;
|
|
}
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
void populateGvarUseCB(QComboBox *b, unsigned int phase)
|
|
{
|
|
b->addItem(QObject::tr("Own value"));
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(FlightModes); i++) {
|
|
if (i != (int)phase) {
|
|
b->addItem(QObject::tr("Flight mode %1 value").arg(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
void populateSwitchCB(QComboBox * b, const RawSwitch & value, const GeneralSettings & generalSettings, SwitchContext context)
|
|
{
|
|
Board::Type board = getCurrentBoard();
|
|
RawSwitch item;
|
|
|
|
b->clear();
|
|
|
|
if (context != MixesContext && context != GlobalFunctionsContext) {
|
|
// !FMx
|
|
if (IS_ARM(board)) {
|
|
for (int i=-getCurrentFirmware()->getCapability(FlightModes); i<0; i++) {
|
|
item = RawSwitch(SWITCH_TYPE_FLIGHT_MODE, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (context != GlobalFunctionsContext) {
|
|
for (int i=-getCurrentFirmware()->getCapability(LogicalSwitches); i<0; i++) {
|
|
item = RawSwitch(SWITCH_TYPE_VIRTUAL, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
|
|
for (int i=-getCurrentFirmware()->getCapability(RotaryEncoders); i<0; i++) {
|
|
item = RawSwitch(SWITCH_TYPE_ROTARY_ENCODER, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
for (int i = -getBoardCapability(board, Board::NumTrimSwitches); i < 0; i++) {
|
|
item = RawSwitch(SWITCH_TYPE_TRIM, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
for (int i=getCurrentFirmware()->getCapability(MultiposPots)-1; i>=0; i--) {
|
|
if (generalSettings.potConfig[i] == Board::POT_MULTIPOS_SWITCH) {
|
|
for (int j=-getCurrentFirmware()->getCapability(MultiposPotsPositions); j<0; j++) {
|
|
item = RawSwitch(SWITCH_TYPE_MULTIPOS_POT, -i*getCurrentFirmware()->getCapability(MultiposPotsPositions)+j);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i=-getCurrentFirmware()->getCapability(SwitchesPositions); i<0; i++) {
|
|
item = RawSwitch(SWITCH_TYPE_SWITCH, i);
|
|
if (IS_HORUS_OR_TARANIS(board) && !generalSettings.switchPositionAllowedTaranis(i)) {
|
|
continue;
|
|
}
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
if (context == TimersContext) {
|
|
for (int i=0; i<5; i++) {
|
|
item = RawSwitch(SWITCH_TYPE_TIMER_MODE, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
else {
|
|
item = RawSwitch(SWITCH_TYPE_NONE);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
for (int i=1; i<=getCurrentFirmware()->getCapability(SwitchesPositions); i++) {
|
|
item = RawSwitch(SWITCH_TYPE_SWITCH, i);
|
|
if (IS_HORUS_OR_TARANIS(board) && !generalSettings.switchPositionAllowedTaranis(i)) {
|
|
continue;
|
|
}
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(MultiposPots); i++) {
|
|
if (generalSettings.potConfig[i] == Board::POT_MULTIPOS_SWITCH) {
|
|
for (int j=1; j<=getCurrentFirmware()->getCapability(MultiposPotsPositions); j++) {
|
|
item = RawSwitch(SWITCH_TYPE_MULTIPOS_POT, i*getCurrentFirmware()->getCapability(MultiposPotsPositions)+j);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i=1; i <= getBoardCapability(board, Board::NumTrimSwitches); i++) {
|
|
item = RawSwitch(SWITCH_TYPE_TRIM, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
for (int i=1; i<=getCurrentFirmware()->getCapability(RotaryEncoders); i++) {
|
|
item = RawSwitch(SWITCH_TYPE_ROTARY_ENCODER, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
if (context != GlobalFunctionsContext) {
|
|
for (int i=1; i<=getCurrentFirmware()->getCapability(LogicalSwitches); i++) {
|
|
item = RawSwitch(SWITCH_TYPE_VIRTUAL, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
|
|
if (context == SpecialFunctionsContext || context == GlobalFunctionsContext) {
|
|
// ON
|
|
item = RawSwitch(SWITCH_TYPE_ON);
|
|
b->addItem(item.toString(), item.toValue());
|
|
// One
|
|
item = RawSwitch(SWITCH_TYPE_ONE, 1);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
|
|
// FMx
|
|
if (context != MixesContext && context != GlobalFunctionsContext) {
|
|
if (IS_ARM(board)) {
|
|
for (int i=1; i<=getCurrentFirmware()->getCapability(FlightModes); i++) {
|
|
item = RawSwitch(SWITCH_TYPE_FLIGHT_MODE, i);
|
|
b->addItem(item.toString(), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i=0; i < b->count(); ++i) {
|
|
if (RawSwitch(b->itemData(i).toInt()) == value) {
|
|
b->setCurrentIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
b->setMaxVisibleItems(10);
|
|
}
|
|
|
|
void populateGVCB(QComboBox & b, int value, const ModelData & model)
|
|
{
|
|
bool selected = false;
|
|
|
|
b.clear();
|
|
|
|
int count = getCurrentFirmware()->getCapability(Gvars);
|
|
for (int i=-count; i<=-1; i++) {
|
|
int16_t gval = (int16_t)(-10000+i);
|
|
if (strlen(model.gvars_names[-i-1]) > 0)
|
|
b.addItem(QObject::tr("-GV%1 (%2)").arg(-i).arg(model.gvars_names[-i-1]), gval);
|
|
else
|
|
b.addItem(QObject::tr("-GV%1").arg(-i), gval);
|
|
if (value == gval) {
|
|
b.setCurrentIndex(b.count()-1);
|
|
selected = true;
|
|
}
|
|
}
|
|
|
|
for (int i=1; i<=count; i++) {
|
|
int16_t gval = (int16_t)(10000+i);
|
|
if (strlen(model.gvars_names[i-1]) > 0)
|
|
b.addItem(QObject::tr("GV%1 (%2)").arg(i).arg(model.gvars_names[i-1]), gval);
|
|
else
|
|
b.addItem(QObject::tr("GV%1").arg(i), gval);
|
|
if (value == gval) {
|
|
b.setCurrentIndex(b.count()-1);
|
|
selected = true;
|
|
}
|
|
}
|
|
|
|
if (!selected) {
|
|
b.setCurrentIndex(count);
|
|
}
|
|
}
|
|
|
|
void populateSourceCB(QComboBox * b, const RawSource & source, const GeneralSettings generalSettings, const ModelData * model, unsigned int flags)
|
|
{
|
|
Board::Type board = getCurrentBoard();
|
|
RawSource item;
|
|
|
|
b->clear();
|
|
|
|
if (flags & POPULATE_NONE) {
|
|
item = RawSource(SOURCE_TYPE_NONE);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
|
|
if (flags & POPULATE_SCRIPT_OUTPUTS) {
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(LuaScripts); i++) {
|
|
for (int j=0; j<getCurrentFirmware()->getCapability(LuaOutputsPerScript); j++) {
|
|
item = RawSource(SOURCE_TYPE_LUA_OUTPUT, i*16+j);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (model && (flags & POPULATE_VIRTUAL_INPUTS)) {
|
|
int virtualInputs = getCurrentFirmware()->getCapability(VirtualInputs);
|
|
for (int i=0; i<virtualInputs; i++) {
|
|
if (model->isInputValid(i)) {
|
|
item = RawSource(SOURCE_TYPE_VIRTUAL_INPUT, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (flags & POPULATE_SOURCES) {
|
|
int totalSources = CPN_MAX_STICKS + getBoardCapability(board, Board::Pots) + getBoardCapability(board, Board::Sliders) + getBoardCapability(board, Board::MouseAnalogs);
|
|
for (int i=0; i < totalSources; i++) {
|
|
item = RawSource(SOURCE_TYPE_STICK, i);
|
|
// skip unavailable pots and sliders
|
|
if (item.isPot() && !generalSettings.isPotAvailable(i-CPN_MAX_STICKS))
|
|
continue;
|
|
if (item.isSlider() && !generalSettings.isSliderAvailable(i-CPN_MAX_STICKS-getBoardCapability(board, Board::Pots)))
|
|
continue;
|
|
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(RotaryEncoders); i++) {
|
|
item = RawSource(SOURCE_TYPE_ROTARY_ENCODER, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
|
|
if (flags & POPULATE_TRIMS) {
|
|
for (int i=0; i < getBoardCapability(board, Board::NumTrims); i++) {
|
|
item = RawSource(SOURCE_TYPE_TRIM, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
|
|
if (flags & POPULATE_SOURCES) {
|
|
item = RawSource(SOURCE_TYPE_MAX);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
|
|
if (flags & POPULATE_SWITCHES) {
|
|
for (int i=0; i<getBoardCapability(board, Board::Switches); i++) {
|
|
item = RawSource(SOURCE_TYPE_SWITCH, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
if (IS_HORUS_OR_TARANIS(board) && !generalSettings.switchSourceAllowedTaranis(i)) {
|
|
QModelIndex index = b->model()->index(b->count()-1, 0);
|
|
QVariant v(0);
|
|
b->model()->setData(index, v, Qt::UserRole - 1);
|
|
}
|
|
}
|
|
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(LogicalSwitches); i++) {
|
|
item = RawSource(SOURCE_TYPE_CUSTOM_SWITCH, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
|
|
if (flags & POPULATE_SOURCES) {
|
|
for (int i=0; i<CPN_MAX_CYC; i++) {
|
|
item = RawSource(SOURCE_TYPE_CYC, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(TrainerInputs); i++) {
|
|
item = RawSource(SOURCE_TYPE_PPM, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(Outputs); i++) {
|
|
item = RawSource(SOURCE_TYPE_CH, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
|
|
if (flags & POPULATE_TELEMETRY) {
|
|
if (IS_ARM(board)) {
|
|
for (int i=0; i<5; ++i) {
|
|
item = RawSource(SOURCE_TYPE_SPECIAL, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
for (int i=0; i<CPN_MAX_SENSORS; ++i) {
|
|
if (model && model->sensorData[i].isAvailable()) { //this conditon must be false if we populate Global Functions where model = 0
|
|
for (int j=0; j<3; ++j) {
|
|
item = RawSource(SOURCE_TYPE_TELEMETRY, 3*i+j);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
// qDebug() << item.toString(model) << source.toString(model);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (int i=0; i<(flags & POPULATE_TELEMETRYEXT ? TELEMETRY_SOURCES_STATUS_COUNT : TELEMETRY_SOURCES_COUNT); i++) {
|
|
if (i==TELEMETRY_SOURCE_TX_TIME && !getCurrentFirmware()->getCapability(RtcTime))
|
|
continue;
|
|
if (i==TELEMETRY_SOURCE_SWR && !getCurrentFirmware()->getCapability(SportTelemetry))
|
|
continue;
|
|
if (i==TELEMETRY_SOURCE_TIMER3 && !IS_ARM(board))
|
|
continue;
|
|
item = RawSource(SOURCE_TYPE_TELEMETRY, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (flags & POPULATE_GVARS) {
|
|
for (int i=0; i<getCurrentFirmware()->getCapability(Gvars); i++) {
|
|
item = RawSource(SOURCE_TYPE_GVAR, i);
|
|
b->addItem(item.toString(model), item.toValue());
|
|
}
|
|
}
|
|
|
|
for (int i=0; i < b->count(); ++i) {
|
|
if (RawSource(b->itemData(i).toInt()) == source) {
|
|
b->setCurrentIndex(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
b->setMaxVisibleItems(10);
|
|
}
|
|
|
|
QString image2qstring(QImage image)
|
|
{
|
|
if (image.isNull())
|
|
return "";
|
|
QBuffer buffer;
|
|
image.save(&buffer, "PNG");
|
|
QString ImageStr;
|
|
int b=0;
|
|
int size=buffer.data().size();
|
|
for (int j = 0; j < size; j++) {
|
|
b=buffer.data().at(j);
|
|
ImageStr += QString("%1").arg(b&0xff, 2, 16, QChar('0'));
|
|
}
|
|
return ImageStr;
|
|
}
|
|
|
|
int findmult(float value, float base)
|
|
{
|
|
int vvalue = value*10;
|
|
int vbase = base*10;
|
|
vvalue--;
|
|
|
|
int mult = 0;
|
|
for (int i=8; i>=0; i--) {
|
|
if (vvalue/vbase >= (1<<i)) {
|
|
mult = i+1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return mult;
|
|
}
|
|
|
|
QString getFrSkyAlarmType(int alarm)
|
|
{
|
|
switch (alarm) {
|
|
case 1:
|
|
return QObject::tr("Yellow");
|
|
case 2:
|
|
return QObject::tr("Orange");
|
|
case 3:
|
|
return QObject::tr("Red");
|
|
default:
|
|
return "----";
|
|
}
|
|
}
|
|
|
|
QString getFrSkyUnits(int units)
|
|
{
|
|
switch(units) {
|
|
case 1:
|
|
return QObject::tr("---");
|
|
default:
|
|
return "V";
|
|
}
|
|
}
|
|
|
|
QString getFrSkyProtocol(int protocol)
|
|
{
|
|
switch(protocol) {
|
|
case 2:
|
|
if ((getCurrentFirmware()->getCapability(Telemetry) & TM_HASWSHH))
|
|
return QObject::tr("Winged Shadow How High");
|
|
else
|
|
return QObject::tr("Winged Shadow How High (not supported)");
|
|
case 1:
|
|
return QObject::tr("FrSky Sensor Hub");
|
|
default:
|
|
return QObject::tr("None");
|
|
}
|
|
}
|
|
|
|
QString getFrSkyMeasure(int units)
|
|
{
|
|
switch(units) {
|
|
case 1:
|
|
return QObject::tr("Imperial");
|
|
default:
|
|
return QObject::tr("Metric");
|
|
}
|
|
}
|
|
|
|
QString getFrSkySrc(int index)
|
|
{
|
|
return RawSource(SOURCE_TYPE_TELEMETRY, index-1).toString();
|
|
}
|
|
|
|
QString getTheme()
|
|
{
|
|
int theme_set = g.theme();
|
|
QString Theme;
|
|
switch(theme_set) {
|
|
case 0:
|
|
Theme="classic";
|
|
break;
|
|
case 2:
|
|
Theme="monowhite";
|
|
break;
|
|
case 3:
|
|
Theme="monochrome";
|
|
break;
|
|
case 4:
|
|
Theme="monoblue";
|
|
break;
|
|
default:
|
|
Theme="yerico";
|
|
break;
|
|
}
|
|
return Theme;
|
|
}
|
|
|
|
CompanionIcon::CompanionIcon(const QString &baseimage)
|
|
{
|
|
static QString theme = getTheme();
|
|
addFile(":/themes/"+theme+"/16/"+baseimage, QSize(16,16));
|
|
addFile(":/themes/"+theme+"/24/"+baseimage, QSize(24,24));
|
|
addFile(":/themes/"+theme+"/32/"+baseimage, QSize(32,32));
|
|
addFile(":/themes/"+theme+"/48/"+baseimage, QSize(48,48));
|
|
}
|
|
|
|
void startSimulation(QWidget * parent, RadioData & radioData, int modelIdx)
|
|
{
|
|
SimulatorInterface * simulator = getCurrentSimulator();
|
|
if (simulator) {
|
|
RadioData * simuData = new RadioData(radioData);
|
|
unsigned int flags = 0;
|
|
|
|
if (modelIdx >= 0) {
|
|
flags |= SIMULATOR_FLAGS_NOTX;
|
|
simuData->setCurrentModel(modelIdx);
|
|
}
|
|
|
|
SimulatorMainWindow * dialog = new SimulatorMainWindow(parent, simulator, flags);
|
|
if (!dialog->setRadioData(simuData)) {
|
|
QMessageBox::critical(NULL, QObject::tr("Data Load Error"), QObject::tr("Error occurred while starting simulator."));
|
|
delete dialog;
|
|
delete simuData;
|
|
return;
|
|
}
|
|
|
|
dialog->setWindowModality(Qt::ApplicationModal);
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
dialog->start();
|
|
|
|
QObject::connect(dialog, &SimulatorMainWindow::destroyed, [simuData] (void) {
|
|
// TODO simuData and Horus tmp directory is deleted on simulator close OR we could use it to get back data from the simulation
|
|
delete simuData;
|
|
});
|
|
|
|
dialog->show();
|
|
}
|
|
else {
|
|
QMessageBox::warning(NULL,
|
|
QObject::tr("Warning"),
|
|
QObject::tr("Simulator for this firmware is not yet available"));
|
|
}
|
|
}
|
|
|
|
QPixmap makePixMap(const QImage & image)
|
|
{
|
|
Firmware * firmware = getCurrentFirmware();
|
|
QImage result = image.scaled(firmware->getCapability(LcdWidth), firmware->getCapability(LcdHeight));
|
|
if (firmware->getCapability(LcdDepth) == 4) {
|
|
result = result.convertToFormat(QImage::Format_RGB32);
|
|
for (int i = 0; i < result.width(); ++i) {
|
|
for (int j = 0; j < result.height(); ++j) {
|
|
QRgb col = result.pixel(i, j);
|
|
int gray = qGray(col);
|
|
result.setPixel(i, j, qRgb(gray, gray, gray));
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
result = result.convertToFormat(QImage::Format_Mono);
|
|
}
|
|
|
|
return QPixmap::fromImage(result);
|
|
}
|
|
|
|
int version2index(const QString & version)
|
|
{
|
|
int result = 999;
|
|
QStringList parts = version.split("N");
|
|
if (parts.size() > 1)
|
|
result = parts[1].toInt(); // nightly build
|
|
parts = parts[0].split('.');
|
|
if (parts.size() > 2)
|
|
result += 1000 * parts[2].toInt();
|
|
if (parts.size() > 1)
|
|
result += 100000 * parts[1].toInt();
|
|
if (parts.size() > 0)
|
|
result += 10000000 * parts[0].toInt();
|
|
return result;
|
|
}
|
|
|
|
QString index2version(int index)
|
|
{
|
|
if (index >= 19900000) {
|
|
int nightly = index % 1000;
|
|
index /= 1000;
|
|
int revision = index % 100;
|
|
index /= 100;
|
|
int minor = index % 100;
|
|
int major = index / 100;
|
|
QString result = QString("%1.%2.%3").arg(major).arg(minor).arg(revision);
|
|
if (nightly > 0 && nightly < 999) {
|
|
result += QString("N%1").arg(nightly);
|
|
}
|
|
return result;
|
|
}
|
|
else if (index >= 19900) {
|
|
int revision = index % 100;
|
|
index /= 100;
|
|
int minor = index % 100;
|
|
int major = index / 100;
|
|
return QString("%1.%2.%3").arg(major).arg(minor).arg(revision);
|
|
}
|
|
else {
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
bool qunlink(const QString & fileName)
|
|
{
|
|
return QFile::remove(fileName);
|
|
}
|
|
|
|
QString generateProcessUniqueTempFileName(const QString & fileName)
|
|
{
|
|
QString sanitizedFileName = fileName;
|
|
sanitizedFileName.remove('/');
|
|
return QDir::tempPath() + QString("/%1-").arg(QCoreApplication::applicationPid()) + sanitizedFileName;
|
|
}
|
|
|
|
bool isTempFileName(const QString & fileName)
|
|
{
|
|
return fileName.startsWith(QDir::tempPath());
|
|
}
|
|
|
|
QString getSoundsPath(const GeneralSettings &generalSettings)
|
|
{
|
|
QString path = g.profile[g.id()].sdPath() + "/SOUNDS/";
|
|
QString lang = generalSettings.ttsLanguage;
|
|
if (lang.isEmpty())
|
|
lang = "en";
|
|
path.append(lang);
|
|
return path;
|
|
}
|
|
|
|
QSet<QString> getFilesSet(const QString &path, const QStringList &filter, int maxLen)
|
|
{
|
|
QSet<QString> result;
|
|
QDir dir(path);
|
|
if (dir.exists()) {
|
|
foreach (QString filename, dir.entryList(filter, QDir::Files)) {
|
|
QFileInfo file(filename);
|
|
QString name = file.completeBaseName();
|
|
if (name.length() <= maxLen) {
|
|
result.insert(name);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool caseInsensitiveLessThan(const QString &s1, const QString &s2)
|
|
{
|
|
return s1.toLower() < s2.toLower();
|
|
}
|
|
|
|
bool GpsGlitchFilter::isGlitch(GpsCoord coord)
|
|
{
|
|
if ((fabs(coord.latitude) < 0.1) && (fabs(coord.longitude) < 0.1)) {
|
|
return true;
|
|
}
|
|
|
|
if (lastValid) {
|
|
if (fabs(coord.latitude - lastLat) > 0.01) {
|
|
// qDebug() << "GpsGlitchFilter(): latitude glitch " << coord.latitude << lastLat;
|
|
if ( ++glitchCount < 10) {
|
|
return true;
|
|
}
|
|
}
|
|
if (fabs(coord.longitude - lastLon) > 0.01) {
|
|
// qDebug() << "GpsGlitchFilter(): longitude glitch " << coord.longitude << lastLon;
|
|
if ( ++glitchCount < 10) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
lastLat = coord.latitude;
|
|
lastLon = coord.longitude;
|
|
lastValid = true;
|
|
glitchCount = 0;
|
|
return false;
|
|
}
|
|
|
|
bool GpsLatLonFilter::isValid(GpsCoord coord)
|
|
{
|
|
if (lastLat == coord.latitude) {
|
|
return false;
|
|
}
|
|
if (lastLon == coord.longitude) {
|
|
return false;
|
|
}
|
|
lastLat = coord.latitude;
|
|
lastLon = coord.longitude;
|
|
return true;
|
|
}
|
|
|
|
double toDecimalCoordinate(const QString & value)
|
|
{
|
|
if (value.isEmpty()) return 0.0;
|
|
double temp = int(value.left(value.length()-1).toDouble() / 100);
|
|
double result = temp + (value.left(value.length() - 1).toDouble() - temp * 100) / 60.0;
|
|
QChar direction = value.at(value.size()-1);
|
|
if ((direction == 'S') || (direction == 'W')) {
|
|
result = -result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
GpsCoord extractGpsCoordinates(const QString & position)
|
|
{
|
|
GpsCoord result;
|
|
QStringList parts = position.split(' ');
|
|
if (parts.size() == 2) {
|
|
QString value = parts.at(0).trimmed();
|
|
QChar direction = value.at(value.size()-1);
|
|
if (direction == 'E' || direction == 'W') {
|
|
// OpenTX 2.1 format: "NNN.MMM[E|W] NNN.MMM[N|S]" <longitude> <latitude>
|
|
result.latitude = toDecimalCoordinate(parts.at(1).trimmed());
|
|
result.longitude = toDecimalCoordinate(parts.at(0).trimmed());
|
|
}
|
|
else {
|
|
// OpenTX 2.2 format: "DD.DDDDDD DD.DDDDDD" <latitude> <longitude> both in Signed degrees format (DDD.dddd)
|
|
// Precede South latitudes and West longitudes with a minus sign.
|
|
// Latitudes range from -90 to 90.
|
|
// Longitudes range from -180 to 180.
|
|
result.latitude = parts.at(0).trimmed().toDouble();
|
|
result.longitude = parts.at(1).trimmed().toDouble();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TableLayout::TableLayout(QWidget * parent, int rowCount, const QStringList & headerLabels)
|
|
{
|
|
#if defined(TABLE_LAYOUT)
|
|
tableWidget = new QTableWidget(parent);
|
|
QVBoxLayout * layout = new QVBoxLayout();
|
|
layout->addWidget(tableWidget);
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
|
parent->setLayout(layout);
|
|
|
|
tableWidget->setRowCount(rowCount);
|
|
tableWidget->setColumnCount(headerLabels.size());
|
|
tableWidget->setShowGrid(false);
|
|
tableWidget->verticalHeader()->setVisible(false);
|
|
tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
tableWidget->setSelectionMode(QAbstractItemView::NoSelection);
|
|
tableWidget->setFrameStyle(QFrame::NoFrame | QFrame::Plain);
|
|
tableWidget->setStyleSheet("QTableWidget {background-color: transparent;}");
|
|
tableWidget->setHorizontalHeaderLabels(headerLabels);
|
|
#else
|
|
gridWidget = new QGridLayout(parent);
|
|
|
|
int col = 0;
|
|
foreach(QString text, headerLabels) {
|
|
QLabel *label = new QLabel();
|
|
label->setFrameShape(QFrame::Panel);
|
|
label->setFrameShadow(QFrame::Raised);
|
|
label->setMidLineWidth(0);
|
|
label->setAlignment(Qt::AlignCenter);
|
|
label->setMargin(5);
|
|
label->setText(text);
|
|
// if (!minimize)
|
|
// label->setMinimumWidth(100);
|
|
gridWidget->addWidget(label, 0, col++);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void TableLayout::addWidget(int row, int column, QWidget * widget)
|
|
{
|
|
#if defined(TABLE_LAYOUT)
|
|
QHBoxLayout * layout = new QHBoxLayout(tableWidget);
|
|
layout->addWidget(widget);
|
|
addLayout(row, column, layout);
|
|
#else
|
|
gridWidget->addWidget(widget, row + 1, column);
|
|
#endif
|
|
}
|
|
|
|
void TableLayout::addLayout(int row, int column, QLayout * layout)
|
|
{
|
|
#if defined(TABLE_LAYOUT)
|
|
layout->setContentsMargins(1, 3, 1, 3);
|
|
QWidget * containerWidget = new QWidget(tableWidget);
|
|
containerWidget->setLayout(layout);
|
|
tableWidget->setCellWidget(row, column, containerWidget);
|
|
#else
|
|
gridWidget->addLayout(layout, row + 1, column);
|
|
#endif
|
|
}
|
|
|
|
void TableLayout::resizeColumnsToContents()
|
|
{
|
|
#if defined(TABLE_LAYOUT)
|
|
tableWidget->resizeColumnsToContents();
|
|
#else
|
|
#endif
|
|
}
|
|
|
|
void TableLayout::setColumnWidth(int col, int width)
|
|
{
|
|
#if defined(TABLE_LAYOUT)
|
|
tableWidget->setColumnWidth(col, width);
|
|
#else
|
|
#endif
|
|
}
|
|
|
|
void TableLayout::pushRowsUp(int row)
|
|
{
|
|
#if defined(TABLE_LAYOUT)
|
|
#else
|
|
// Push the rows up
|
|
QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding );
|
|
gridWidget->addItem(spacer, row, 0);
|
|
#endif
|
|
// Push rows upward
|
|
// addDoubleSpring(gridLayout, 5, num_fsw+1);
|
|
|
|
}
|