Files
edgetx-gx12/companion/src/radiodata.cpp
T
Max PapernoandBertrand Songis 0dbad182e8 [Companion] Radio conversion improvements (#5534)
* Move MultiposPots & MultiposPotsPositions capability lookups and getAnalogInputName() to Boards class.

* Add RawSource::isAvailable() and RawSwitch::isAvailable(); Add board type param to various RawSource lookup functions.

* Add CustomFunctionData::toString(), FlightModeData::toString(), FactoryInstalledPots board capability, MAX_SWITCH_TYPE, & break out GeneralSettings source/switch defaults to own function.

* Add RadioDataConversionState class for tracking conversion actions.

* Implement RadioDataConversionState handler, add conversion process report, and improve/fix conversion issues:
    * Make sure all switches/pots/knobs have default configs for destination board type;
    * Try to move more custom control names, intelligently;
    * Convert model timers and throttle source;
    * Adjust for Horus 6P switch in place of Taranis S2 knob;
    * Adjust for extra sliders on X12 and X9E to move LS and RS to proper slots as needed;
    * Make sure ALL controls are validated, eg. to always account for extra trims/analogs/switches/etc which don't exist on destination;
    * Properly indicate invalid RawSource::toString() items with "???".

* Allow user to cancel out of file conversions & provide ample warnings if switching radio type/profile with unsaved file(s) still open;
Also: Fix bug with some orphaned MdiChild windows never being deleted; Create static bool Storage::isBoardCompatible(b1, b2).

* Add maxLibQt "stub" (will not compile).

* Add ExportableTableView for better presentation of conversion results table.

* Move getFourCC() and isBoardCompatible() to Boards class.
2017-12-18 22:56:39 +01:00

450 lines
15 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 "radiodata.h"
#include "radiodataconversionstate.h"
#include "eeprominterface.h"
// TODO here we will move a lot of functions from eeprominterface.cpp when no merge risk
RawSource RawSource::convert(RadioDataConversionState & cstate)
{
cstate.setItemType("SRC", 1);
RadioDataConversionState::EventType evt = RadioDataConversionState::EVT_NONE;
RadioDataConversionState::LogField oldData(index, toString(cstate.fromModel(), cstate.fromGS(), cstate.fromType));
if (type == SOURCE_TYPE_STICK) {
if (cstate.toBoard.getCapability(Board::Sliders)) {
if (index >= cstate.fromBoard.getCapability(Board::Sticks) + cstate.fromBoard.getCapability(Board::Pots)) {
// 1st slider alignment
index += cstate.toBoard.getCapability(Board::Pots) - cstate.fromBoard.getCapability(Board::Pots);
}
if (isSlider(0, cstate.fromType)) {
// LS and RS sliders are after 2 aux sliders on X12 and X9E
if ((IS_HORUS_X12S(cstate.toType) || IS_TARANIS_X9E(cstate.toType)) && !IS_HORUS_X12S(cstate.fromType) && !IS_TARANIS_X9E(cstate.fromType)) {
if (index >= 7) {
index += 2; // LS/RS to LS/RS
}
}
else if (!IS_TARANIS_X9E(cstate.toType) && !IS_HORUS_X12S(cstate.toType) && (IS_HORUS_X12S(cstate.fromType) || IS_TARANIS_X9E(cstate.fromType))) {
if (index >= 7 && index <= 8) {
index += 2; // aux sliders to spare analogs (which may not exist, this is validated later)
evt = RadioDataConversionState::EVT_CVRT;
}
else if (index >= 9 && index <= 10) {
index -= 2; // LS/RS to LS/RS
}
}
}
}
if (IS_TARANIS(cstate.toType) && IS_HORUS(cstate.fromType)) {
if (index == 6)
index = 5; // pot S2 to S2
else if (index == 5)
index = -1; // 6P on Horus doesn't exist on Taranis
}
else if (IS_HORUS(cstate.toType) && IS_TARANIS(cstate.fromType) && index == 5)
{
index = 6; // pot S2 to S2
}
} // SOURCE_TYPE_STICK
if (type == SOURCE_TYPE_SWITCH) {
// SWI to SWR don't exist on !X9E board
if (!IS_TARANIS_X9E(cstate.toType) && IS_TARANIS_X9E(cstate.fromType)) {
if (index >= 8) {
index = index % 8;
evt = RadioDataConversionState::EVT_CVRT;
}
}
if (IS_TARANIS_X7(cstate.toType) && (IS_TARANIS_X9(cstate.fromType) || IS_HORUS(cstate.fromType))) {
// No SE and SG on X7 board
if (index == 4 || index == 6) {
index = 3; // SG and SE to SD
evt = RadioDataConversionState::EVT_CVRT;
}
else if (index == 5) {
index = 4; // SF to SF
}
else if (index == 7) {
index = 5; // SH to SH
}
}
// Compensate for SE and SG on X9/Horus board if converting from X7
else if ((IS_TARANIS_X9(cstate.toType) || IS_HORUS(cstate.toType)) && IS_TARANIS_X7(cstate.fromType)) {
if (index == 4) {
index = 5; // SF to SF
}
else if (index == 5) {
index = 7; // SH to SH
}
}
} // SOURCE_TYPE_SWITCH
// final validation (we do not pass model to isAvailable() because we don't know what has or hasn't been converted)
if (!isAvailable(NULL, cstate.toGS(), cstate.toType)) {
cstate.setInvalid(oldData);
index = -1; // TODO: better way to flag invalid sources?
type = MAX_SOURCE_TYPE;
}
else if (evt == RadioDataConversionState::EVT_CVRT) {
cstate.setConverted(oldData, RadioDataConversionState::LogField(index, toString(cstate.toModel(), cstate.toGS(), cstate.toType)));
}
else if (oldData.id != index) {
// provide info by default if anything changed
cstate.setMoved(oldData, RadioDataConversionState::LogField(index, toString(cstate.toModel(), cstate.toGS(), cstate.toType)));
}
return *this;
}
RawSwitch RawSwitch::convert(RadioDataConversionState & cstate)
{
if (!index)
return *this;
cstate.setItemType("SW", 2);
RadioDataConversionState::EventType evt = RadioDataConversionState::EVT_NONE;
RadioDataConversionState::LogField oldData(index, toString(cstate.fromType, cstate.fromGS(), cstate.fromModel()));
if (type == SWITCH_TYPE_SWITCH) {
int srcIdx = div(abs(index)-1, 3).quot; // raw source index
int delta = 0;
// SWI to SWR don't exist on !X9E board
if (!IS_TARANIS_X9E(cstate.toType) && IS_TARANIS_X9E(cstate.fromType)) {
if (srcIdx > 7) {
index %= 24;
evt = RadioDataConversionState::EVT_CVRT;
}
}
// No SE and SG on X7 board
if (IS_TARANIS_X7(cstate.toType) && (IS_TARANIS_X9(cstate.fromType) || IS_HORUS(cstate.fromType))) {
if (srcIdx == 4 || srcIdx == 5) {
delta = 3; // SE to SD & SF to SF
if (srcIdx == 4)
evt = RadioDataConversionState::EVT_CVRT;
}
else if (srcIdx == 6) {
delta = 9; // SG to SD
evt = RadioDataConversionState::EVT_CVRT;
}
else if (srcIdx == 7) {
delta = 6; // SH to SH
}
}
// Compensate for SE and SG on X9/Horus board if converting from X7
else if ((IS_TARANIS_X9(cstate.toType) || IS_HORUS(cstate.toType)) && IS_TARANIS_X7(cstate.fromType)) {
if (srcIdx == 4) {
delta = -3; // SF to SF
}
else if (srcIdx == 5) {
delta = -6; // SH to SH
}
}
if (index < 0) {
delta = -delta; // invert for !switch
}
index -= delta;
} // SWITCH_TYPE_SWITCH
// final validation (we do not pass model to isAvailable() because we don't know what has or hasn't been converted)
if (!isAvailable(NULL, cstate.toGS(), cstate.toType)) {
cstate.setInvalid(oldData);
type = MAX_SWITCH_TYPE; // TODO: better way to flag invalid switches?
}
else if (evt == RadioDataConversionState::EVT_CVRT) {
cstate.setConverted(oldData, RadioDataConversionState::LogField(index, toString(cstate.toType, cstate.toGS(), cstate.toModel())));
}
else if (oldData.id != index) {
// provide info by default if anything changed
cstate.setMoved(oldData, RadioDataConversionState::LogField(index, toString(cstate.toType, cstate.toGS(), cstate.toModel())));
}
return *this;
}
void ExpoData::convert(RadioDataConversionState & cstate)
{
cstate.setComponent(QObject::tr("INP"), 3);
cstate.setSubComp(RawSource(SOURCE_TYPE_VIRTUAL_INPUT, chn).toString(cstate.fromModel(), cstate.fromGS(), cstate.fromType) % QObject::tr(" (@%1)").arg(cstate.subCompIdx));
srcRaw.convert(cstate);
swtch.convert(cstate);
}
void MixData::convert(RadioDataConversionState & cstate)
{
cstate.setComponent(QObject::tr("MIX"), 4);
cstate.setSubComp(RawSource(SOURCE_TYPE_CH, destCh-1).toString(cstate.fromModel(), cstate.fromGS(), cstate.fromType) % QObject::tr(" (@%1)").arg(cstate.subCompIdx));
srcRaw.convert(cstate);
swtch.convert(cstate);
}
void LogicalSwitchData::convert(RadioDataConversionState & cstate)
{
cstate.setComponent("LSW", 7);
cstate.setSubComp(RawSwitch(SWITCH_TYPE_VIRTUAL, cstate.subCompIdx + 1).toString(cstate.fromType, cstate.fromGS(), cstate.fromModel()));
CSFunctionFamily family = getFunctionFamily();
switch(family) {
case LS_FAMILY_VOFS:
val1 = RawSource(val1).convert(cstate.withComponentField("V1")).toValue();
break;
case LS_FAMILY_STICKY:
case LS_FAMILY_VBOOL:
val1 = RawSwitch(val1).convert(cstate.withComponentField("V1")).toValue();
val2 = RawSwitch(val2).convert(cstate.withComponentField("V2")).toValue();
break;
case LS_FAMILY_EDGE:
val1 = RawSwitch(val1).convert(cstate.withComponentField("V1")).toValue();
break;
case LS_FAMILY_VCOMP:
val1 = RawSource(val1).convert(cstate.withComponentField("V1")).toValue();
val2 = RawSource(val2).convert(cstate.withComponentField("V2")).toValue();
break;
default:
break;
}
andsw = RawSwitch(andsw).convert(cstate.withComponentField("AND")).toValue();
}
void CustomFunctionData::convert(RadioDataConversionState & cstate)
{
cstate.setComponent("CFN", 8);
cstate.setSubComp(toString(cstate.subCompIdx, (cstate.toModel() ? false : true)));
swtch.convert(cstate);
if (func == FuncVolume || func == FuncPlayValue || (func >= FuncAdjustGV1 && func <= FuncAdjustGVLast && adjustMode == 1)) {
param = RawSource(param).convert(cstate.withComponentField("PARAM")).toValue();
}
}
void FlightModeData::convert(RadioDataConversionState & cstate)
{
cstate.setComponent("FMD", 2);
cstate.setSubComp(toString(cstate.subCompIdx));
swtch.convert(cstate);
}
void TimerData::convert(RadioDataConversionState & cstate)
{
cstate.setComponent("TMR", 1);
cstate.setSubComp(QObject::tr("Timer %1").arg(cstate.subCompIdx + 1));
mode.convert(cstate);
}
void ModelData::convert(RadioDataConversionState & cstate)
{
// Here we can add explicit conversions when moving from one board to another
QString origin = QString(name);
if (origin.isEmpty())
origin = QString::number(cstate.modelIdx+1);
cstate.setOrigin(QObject::tr("Model: ") % origin);
cstate.setComponent("SET", 0);
if (thrTraceSrc && (int)thrTraceSrc < cstate.fromBoard.getCapability(Board::Pots) + cstate.fromBoard.getCapability(Board::Sliders)) {
cstate.setSubComp(QObject::tr("Throttle Source"));
thrTraceSrc = RawSource(SOURCE_TYPE_STICK, (int)thrTraceSrc + 3).convert(cstate).index - 3;
}
for (int i=0; i<CPN_MAX_TIMERS; i++) {
timers[i].convert(cstate.withComponentIndex(i));
}
for (int i=0; i<CPN_MAX_MIXERS; i++) {
mixData[i].convert(cstate.withComponentIndex(i));
}
for (int i=0; i<CPN_MAX_EXPOS; i++) {
expoData[i].convert(cstate.withComponentIndex(i));
}
for (int i=0; i<CPN_MAX_LOGICAL_SWITCHES; i++) {
logicalSw[i].convert(cstate.withComponentIndex(i));
}
for (int i=0; i<CPN_MAX_SPECIAL_FUNCTIONS; i++) {
customFn[i].convert(cstate.withComponentIndex(i));
}
for (int i=0; i<CPN_MAX_FLIGHT_MODES; i++) {
flightModeData[i].convert(cstate.withComponentIndex(i));
}
}
void GeneralSettings::convert(RadioDataConversionState & cstate)
{
// Here we can add explicit conversions when moving from one board to another
cstate.setOrigin(QObject::tr("Radio Settings"));
setDefaultControlTypes(cstate.toType); // start with default switches/pots/sliders
// Try to intelligently copy any custom control names
// SE and SG are skipped on X7 board
if (IS_TARANIS_X7(cstate.toType)) {
if (IS_TARANIS_X9(cstate.fromType) || IS_HORUS(cstate.fromType)) {
strncpy(switchName[4], switchName[5], sizeof(switchName[0]));
strncpy(switchName[5], switchName[7], sizeof(switchName[0]));
}
}
else if (IS_TARANIS_X7(cstate.fromType)) {
if (IS_TARANIS_X9(cstate.toType) || IS_HORUS(cstate.toType)) {
strncpy(switchName[5], switchName[4], sizeof(switchName[0]));
strncpy(switchName[7], switchName[5], sizeof(switchName[0]));
}
}
// LS and RS sliders are after 2 aux sliders on X12 and X9E
if ((IS_HORUS_X12S(cstate.toType) || IS_TARANIS_X9E(cstate.toType)) && !IS_HORUS_X12S(cstate.fromType) && !IS_TARANIS_X9E(cstate.fromType)) {
strncpy(sliderName[0], sliderName[2], sizeof(sliderName[0]));
strncpy(sliderName[1], sliderName[3], sizeof(sliderName[0]));
}
else if (!IS_TARANIS_X9E(cstate.toType) && !IS_HORUS_X12S(cstate.toType) && (IS_HORUS_X12S(cstate.fromType) || IS_TARANIS_X9E(cstate.fromType))) {
strncpy(sliderName[2], sliderName[0], sizeof(sliderName[0]));
strncpy(sliderName[3], sliderName[1], sizeof(sliderName[0]));
}
if (IS_HORUS(cstate.toType)) {
// 6P switch is only on Horus (by default)
if (cstate.fromBoard.getCapability(Board::FactoryInstalledPots) == 2) {
strncpy(potName[2], potName[1], sizeof(potName[0]));
potName[1][0] = '\0';
}
}
if (IS_TARANIS(cstate.toType)) {
// No S3 pot on Taranis boards by default
if (cstate.fromBoard.getCapability(Board::FactoryInstalledPots) > 2)
strncpy(potName[1], potName[2], sizeof(potName[0]));
contrast = qBound<int>(getCurrentFirmware()->getCapability(MinContrast), contrast, getCurrentFirmware()->getCapability(MaxContrast));
}
// TODO: Would be nice at this point to have GUI pause and ask the user to set up any custom hardware they have on the destination radio.
// Convert all global functions (do this after HW adjustments)
for (int i=0; i<CPN_MAX_SPECIAL_FUNCTIONS; i++) {
customFn[i].convert(cstate.withComponentIndex(i));
}
}
/*
* RadioData
*/
RadioData::RadioData()
{
models.resize(getCurrentFirmware()->getCapability(Models));
}
void RadioData::setCurrentModel(unsigned int index)
{
generalSettings.currModelIndex = index;
if (index < models.size()) {
strcpy(generalSettings.currModelFilename, models[index].filename);
}
}
void RadioData::fixModelFilename(unsigned int index)
{
ModelData & model = models[index];
QString filename(model.filename);
bool ok = filename.endsWith(".bin");
if (ok) {
if (filename.startsWith("model") && filename.mid(5, filename.length()-9).toInt() > 0) {
ok = false;
}
}
if (ok) {
for (unsigned i=0; i<index; i++) {
if (strcmp(models[i].filename, model.filename) == 0) {
ok = false;
break;
}
}
}
if (!ok) {
sprintf(model.filename, "model%d.bin", index+1);
}
}
void RadioData::fixModelFilenames()
{
for (unsigned int i=0; i<models.size(); i++) {
fixModelFilename(i);
}
setCurrentModel(generalSettings.currModelIndex);
}
QString RadioData::getNextModelFilename()
{
char filename[sizeof(ModelData::filename)];
int index = 0;
bool found = true;
while (found) {
sprintf(filename, "model%d.bin", ++index);
found = false;
for (unsigned int i=0; i<models.size(); i++) {
if (strcmp(filename, models[i].filename) == 0) {
found = true;
break;
}
}
}
return filename;
}
void RadioData::convert(RadioDataConversionState & cstate)
{
generalSettings.convert(cstate.withModelIndex(-1));
for (unsigned i=0; i<models.size(); i++) {
models[i].convert(cstate.withModelIndex(i));
}
if (categories.size() == 0) {
categories.push_back(CategoryData(qPrintable(QObject::tr("Models"))));
for (unsigned i=0; i<models.size(); i++) {
models[i].category = 0;
}
}
if (IS_HORUS(cstate.toType)) {
fixModelFilenames();
}
// ensure proper number of model slots
if (getCurrentFirmware()->getCapability(Models) && getCurrentFirmware()->getCapability(Models) != (int)models.size()) {
models.resize(getCurrentFirmware()->getCapability(Models));
}
}