refactor(cpn): introduce BoundedString<N> for ModelData string fields (#7438)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
68873e2955
commit
9326a202b2
@@ -119,6 +119,10 @@ jobs:
|
||||
with:
|
||||
os: 'linux'
|
||||
|
||||
- name: Run companion unit tests
|
||||
shell: bash
|
||||
run: cmake --build build/native --target tests-companion --parallel
|
||||
|
||||
build-macos:
|
||||
name: macOS Companion
|
||||
runs-on: macos-15
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) EdgeTX
|
||||
*
|
||||
* Based on code named
|
||||
* opentx - https://github.com/opentx/opentx
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <QString>
|
||||
|
||||
// A self-limiting string for Companion's data-model fields.
|
||||
//
|
||||
// N is the maximum number of bytes the value may hold, EXCLUDING the implicit
|
||||
// NUL of the legacy `char[]` fields it replaces. So a legacy `char foo[16+1]`
|
||||
// (16 usable chars) maps to `BoundedString<16>`.
|
||||
//
|
||||
// Assignment always truncates to N bytes, mirroring the byte-level truncation
|
||||
// the YAML char-array reader does today (yaml_ops.h: `str.copy(value, N-1)`).
|
||||
// Truncation is byte-based on purpose: it must match the radio firmware (and the
|
||||
// legacy code path) exactly, including the case where a multi-byte UTF-8
|
||||
// sequence is split at the boundary. Do not "improve" it to be codepoint-aware.
|
||||
template <size_t N>
|
||||
class BoundedString
|
||||
{
|
||||
static_assert(N > 0, "BoundedString capacity must be greater than zero");
|
||||
|
||||
std::string s_;
|
||||
|
||||
public:
|
||||
static constexpr size_t capacity() { return N; }
|
||||
|
||||
BoundedString() = default;
|
||||
BoundedString(const BoundedString&) = default;
|
||||
BoundedString& operator=(const BoundedString&) = default;
|
||||
|
||||
BoundedString(std::string_view v) { assign(v); }
|
||||
BoundedString(const char* v) { assign(v ? std::string_view(v) : std::string_view()); }
|
||||
BoundedString(const QString& v) { assign(v); }
|
||||
|
||||
BoundedString& operator=(std::string_view v) { assign(v); return *this; }
|
||||
BoundedString& operator=(const char* v) { assign(v ? std::string_view(v) : std::string_view()); return *this; }
|
||||
BoundedString& operator=(const QString& v) { assign(v); return *this; }
|
||||
|
||||
void assign(std::string_view v) { s_.assign(v.substr(0, N)); }
|
||||
// NOTE: QString is serialised as UTF-8 here, matching the YAML layer. Some
|
||||
// legacy UI call sites stored Latin-1 via QString::toLatin1(); Phase 1 must
|
||||
// confirm the encoding at each migrated boundary before relying on this.
|
||||
void assign(const QString& v) { assign(v.toStdString()); }
|
||||
|
||||
void clear() { s_.clear(); }
|
||||
bool empty() const { return s_.empty(); }
|
||||
size_t size() const { return s_.size(); }
|
||||
size_t length() const { return s_.size(); }
|
||||
|
||||
const std::string& str() const { return s_; }
|
||||
const char* c_str() const { return s_.c_str(); }
|
||||
QString toQString() const { return QString::fromStdString(s_); }
|
||||
|
||||
// Implicit conversion so a migrated field still drops into APIs taking a
|
||||
// std::string (and, via QString's std::string ctor, much of the Qt UI).
|
||||
operator const std::string&() const { return s_; }
|
||||
|
||||
// Explicit comparison overloads (std::string's own operator== is a
|
||||
// non-member template, so the implicit conversion above can't drive it).
|
||||
// The const char* overload binds legacy char[]/literals via array-to-pointer
|
||||
// (a standard conversion), so it wins cleanly over the others rather than
|
||||
// tying with them and producing an ambiguity.
|
||||
bool operator==(const BoundedString& o) const { return s_ == o.s_; }
|
||||
bool operator!=(const BoundedString& o) const { return s_ != o.s_; }
|
||||
bool operator==(const std::string& o) const { return s_ == o; }
|
||||
bool operator!=(const std::string& o) const { return s_ != o; }
|
||||
bool operator==(const char* o) const { return o ? s_ == o : s_.empty(); }
|
||||
bool operator!=(const char* o) const { return o ? s_ != o : !s_.empty(); }
|
||||
};
|
||||
@@ -46,9 +46,9 @@
|
||||
|
||||
void YamlValidateLabelsNames(ModelData& model, Board::Type board)
|
||||
{
|
||||
YamlValidateName(model.name, board);
|
||||
model.name = YamlValidateName(model.name.toQString(), board).toLatin1().constData();
|
||||
|
||||
QStringList lst = QString(model.labels).split(',', Qt::SkipEmptyParts);
|
||||
QStringList lst = model.labels.toQString().split(',', Qt::SkipEmptyParts);
|
||||
|
||||
for (int i = lst.count() - 1; i >= 0; i--) {
|
||||
YamlValidateLabel(lst[i]);
|
||||
@@ -56,7 +56,7 @@ void YamlValidateLabelsNames(ModelData& model, Board::Type board)
|
||||
lst.removeAt(i);
|
||||
}
|
||||
|
||||
strcpy(model.labels, QString(lst.join(',')).toLatin1().data());
|
||||
model.labels = QString(lst.join(',')).toLatin1().constData();
|
||||
|
||||
for (int i = 0; i < CPN_MAX_CURVES; i++) {
|
||||
YamlValidateName(model.curves[i].name, board);
|
||||
@@ -418,7 +418,7 @@ struct YamlSwitchWarningState {
|
||||
std::stringstream ss(src_str);
|
||||
while (!ss.eof()) {
|
||||
auto c = ss.get();
|
||||
if (c < 'A' && c > 'Z') {
|
||||
if (c < 'A' || c > 'Z') {
|
||||
ss.ignore();
|
||||
continue;
|
||||
}
|
||||
@@ -1324,12 +1324,12 @@ bool convert<ModelData>::decode(const Node& node, ModelData& rhs)
|
||||
|
||||
if (node["semver"]) {
|
||||
node["semver"] >> rhs.semver;
|
||||
if (SemanticVersion().isValid(rhs.semver)) {
|
||||
modelSettingsVersion = SemanticVersion(QString(rhs.semver));
|
||||
if (SemanticVersion().isValid(rhs.semver.toQString())) {
|
||||
modelSettingsVersion = SemanticVersion(rhs.semver.toQString());
|
||||
}
|
||||
else {
|
||||
qDebug() << "Invalid settings version:" << rhs.semver;
|
||||
memset(rhs.semver, 0, sizeof(rhs.semver));
|
||||
qDebug() << "Invalid settings version:" << rhs.semver.c_str();
|
||||
rhs.semver.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1355,7 +1355,7 @@ bool convert<ModelData>::decode(const Node& node, ModelData& rhs)
|
||||
<< "Companion" << SemanticVersion(VERSION).toString();
|
||||
} else {
|
||||
QString prmpt = QCoreApplication::translate("YamlModelSettings", "Warning: '%1' has settings version %2 that is not supported by Companion %3!\n\nModel settings may be corrupted if you continue.");
|
||||
prmpt = prmpt.arg(rhs.name).arg(modelSettingsVersion.toString()).arg(SemanticVersion(VERSION).toString());
|
||||
prmpt = prmpt.arg(rhs.name.toQString()).arg(modelSettingsVersion.toString()).arg(SemanticVersion(VERSION).toString());
|
||||
QMessageBox msgBox;
|
||||
msgBox.setWindowTitle(QCoreApplication::translate("YamlModelSettings", "Read Model Settings"));
|
||||
msgBox.setText(prmpt);
|
||||
|
||||
@@ -72,18 +72,22 @@ void YamlValidateLabel(QString &input)
|
||||
delete lv;
|
||||
}
|
||||
|
||||
void YamlValidateName(char *input, Board::Type board)
|
||||
QString YamlValidateName(const QString &input, Board::Type board)
|
||||
{
|
||||
NameValidator *nv = new NameValidator(board);
|
||||
NameValidator nv(board);
|
||||
QString in(input);
|
||||
|
||||
if (!nv->isValid(in)) {
|
||||
nv->fixup(in);
|
||||
if (!nv.isValid(in)) {
|
||||
nv.fixup(in);
|
||||
in = in.trimmed();
|
||||
}
|
||||
|
||||
strcpy(input, in.toLatin1().data());
|
||||
delete nv;
|
||||
return in;
|
||||
}
|
||||
|
||||
void YamlValidateName(char *input, Board::Type board)
|
||||
{
|
||||
strcpy(input, YamlValidateName(QString(input), board).toLatin1().data());
|
||||
}
|
||||
|
||||
namespace YAML {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "helpers.h"
|
||||
#include "boards.h"
|
||||
#include "semanticversion.h"
|
||||
#include "boundedstring.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <QString>
|
||||
@@ -97,6 +98,17 @@ void operator >> (const YAML::Node& node, char (&value)[N])
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the char[] reader above: a scalar is assigned and truncated to the
|
||||
// field capacity. BoundedString<N> truncates to N bytes, matching the legacy
|
||||
// char[N+1] field (which keeps N usable bytes plus the NUL).
|
||||
template <size_t N>
|
||||
void operator >> (const YAML::Node& node, BoundedString<N>& value)
|
||||
{
|
||||
if (node && node.IsScalar()) {
|
||||
value = node.as<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, size_t N>
|
||||
void operator>>(const YAML::Node& node, T (&value)[N])
|
||||
{
|
||||
@@ -127,10 +139,24 @@ void operator>>(const YAML::Node& node, T (&value)[N])
|
||||
}
|
||||
|
||||
void YamlValidateName(char *input, Board::Type board);
|
||||
QString YamlValidateName(const QString &input, Board::Type board);
|
||||
void YamlValidateLabel(QString &input);
|
||||
|
||||
namespace YAML {
|
||||
|
||||
// Encode/decode for BoundedString<N> so `node["x"] = field` and `node.as<>()`
|
||||
// behave like the legacy char[] fields. Decode truncates to the capacity.
|
||||
template <size_t N>
|
||||
struct convert<BoundedString<N>> {
|
||||
static Node encode(const BoundedString<N>& rhs) { return Node(rhs.str()); }
|
||||
static bool decode(const Node& node, BoundedString<N>& rhs)
|
||||
{
|
||||
if (node && node.IsScalar())
|
||||
rhs = node.as<std::string>();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
std::string LookupValue(const YamlLookupTable& lut, const int& value);
|
||||
Node operator << (const YamlLookupTable& lut, const int& value);
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ ModelData & ModelData::operator=(const ModelData & src)
|
||||
|
||||
void ModelData::copy(const ModelData & src)
|
||||
{
|
||||
memcpy(&semver, &src.semver, sizeof(semver));
|
||||
semver = src.semver;
|
||||
used = src.used;
|
||||
memcpy(&name, &src.name, sizeof(name));
|
||||
memcpy(&filename, &src.filename, sizeof(filename));
|
||||
memcpy(&labels, &src.labels, sizeof(labels));
|
||||
name = src.name;
|
||||
filename = src.filename;
|
||||
labels = src.labels;
|
||||
modelIndex = src.modelIndex;
|
||||
modelUpdated = src.modelUpdated;
|
||||
modelErrors = src.modelErrors;
|
||||
@@ -230,11 +230,11 @@ void ModelData::clear()
|
||||
// memset(reinterpret_cast<void *>(this), 0, sizeof(ModelData));
|
||||
// as struct contains complex data types eg std::string
|
||||
|
||||
memset(&semver, 0, sizeof(semver));
|
||||
semver.clear();
|
||||
used = false;
|
||||
memset(&name, 0, sizeof(name));
|
||||
memset(&filename, 0, sizeof(filename));
|
||||
memset(&labels, 0, sizeof(labels));
|
||||
name.clear();
|
||||
filename.clear();
|
||||
labels.clear();
|
||||
modelIndex = -1; // an invalid index, this is managed by the TreeView data model
|
||||
modelUpdated = false;
|
||||
modelErrors = false;
|
||||
@@ -419,7 +419,7 @@ void ModelData::setDefaultValues(unsigned int id, const GeneralSettings & settin
|
||||
{
|
||||
clear();
|
||||
used = true;
|
||||
sprintf(name, "MODEL%02d", id + 1);
|
||||
name = QString::asprintf("MODEL%02d", id + 1);
|
||||
|
||||
for (int i = 0; i < CPN_MAX_MODULES; i++) {
|
||||
moduleData[i].modelId = id + 1;
|
||||
@@ -592,7 +592,7 @@ void ModelData::convert(RadioDataConversionState & cstate)
|
||||
{
|
||||
// Here we can add explicit conversions when moving from one board to another
|
||||
|
||||
QString origin = QString(name);
|
||||
QString origin = name.toQString();
|
||||
if (origin.isEmpty())
|
||||
origin = QString::number(cstate.modelIdx+1);
|
||||
cstate.setOrigin(tr("Model: ") % origin);
|
||||
@@ -2180,7 +2180,7 @@ const Board::SwitchType ModelData::getSwitchType(int sw, const GeneralSettings &
|
||||
|
||||
QString ModelData::getChecklistFilename() const
|
||||
{
|
||||
return QString(name).replace(" ", "_").append(".txt").toLower();
|
||||
return name.toQString().replace(" ", "_").append(".txt").toLower();
|
||||
}
|
||||
|
||||
void ModelData::gvarClear(const int index, bool updateRefs)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "boundedstring.h"
|
||||
#include "constants.h"
|
||||
#include "curvedata.h"
|
||||
#include "customfunctiondata.h"
|
||||
@@ -89,6 +90,9 @@ enum TrainerMode {
|
||||
};
|
||||
|
||||
#define MODEL_NAME_LEN 15
|
||||
#define MODEL_FILENAME_LEN 16 // must match radio LEN_MODEL_FILENAME (dataconstants.h)
|
||||
#define MODEL_SEMVER_LEN 8
|
||||
#define MODEL_LABELS_LEN 99 // CSV of labels; was char labels[100]
|
||||
#define INPUT_NAME_LEN 4
|
||||
#define CPN_MAX_BITMAP_LEN 14
|
||||
|
||||
@@ -129,11 +133,11 @@ class ModelData {
|
||||
ModelData();
|
||||
ModelData(const ModelData & src);
|
||||
|
||||
char semver[8 + 1];
|
||||
BoundedString<MODEL_SEMVER_LEN> semver;
|
||||
bool used;
|
||||
char name[MODEL_NAME_LEN + 1];
|
||||
char filename[16+1];
|
||||
char labels[100];
|
||||
BoundedString<MODEL_NAME_LEN> name;
|
||||
BoundedString<MODEL_FILENAME_LEN> filename;
|
||||
BoundedString<MODEL_LABELS_LEN> labels;
|
||||
int modelIndex; // Companion only, temporary index position managed by data model.
|
||||
bool modelUpdated; // Companion only, used to highlight if changed in models list
|
||||
bool modelErrors; // Companion only, used to highlight if data errors in models list
|
||||
|
||||
@@ -34,14 +34,14 @@ void RadioData::setCurrentModel(unsigned int index)
|
||||
{
|
||||
generalSettings.currModelIndex = index;
|
||||
if (index < models.size()) {
|
||||
strcpy(generalSettings.currModelFilename, models[index].filename);
|
||||
strcpy(generalSettings.currModelFilename, models[index].filename.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void RadioData::fixModelFilename(unsigned int index)
|
||||
{
|
||||
ModelData & model = models[index];
|
||||
QString filename(model.filename);
|
||||
QString filename = model.filename.toQString();
|
||||
const bool hasSDCard = Boards::getCapability(getCurrentFirmware()->getBoard(), Board::HasSDCard);
|
||||
bool ok = hasSDCard ? filename.endsWith(".yml") : filename.endsWith(".bin");
|
||||
if (ok) {
|
||||
@@ -51,14 +51,14 @@ void RadioData::fixModelFilename(unsigned int index)
|
||||
}
|
||||
if (ok) {
|
||||
for (unsigned i=0; i<index; i++) {
|
||||
if (strcmp(models[i].filename, model.filename) == 0) {
|
||||
if (models[i].filename == model.filename) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
sprintf(model.filename, "model%d.%s", index + 1, hasSDCard ? "yml" : "bin");
|
||||
model.filename = QString::asprintf("model%d.%s", index + 1, hasSDCard ? "yml" : "bin");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,14 +72,14 @@ void RadioData::fixModelFilenames()
|
||||
|
||||
QString RadioData::getNextModelFilename()
|
||||
{
|
||||
char filename[sizeof(ModelData::filename)];
|
||||
int8_t index = 0;
|
||||
bool found = true;
|
||||
QString filename;
|
||||
while (found) {
|
||||
sprintf(filename, "model%d.yml", ++index);
|
||||
filename = QString::asprintf("model%d.yml", ++index);
|
||||
found = false;
|
||||
for (unsigned int i = 0; i < models.size(); i++) {
|
||||
if (strcmp(filename, models[i].filename) == 0) {
|
||||
if (models[i].filename == filename.toStdString()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -135,12 +135,12 @@ bool RadioData::deleteLabel(QString label)
|
||||
|
||||
// Remove labels in the models
|
||||
for(auto& model : models) {
|
||||
QStringList modelLabels = fromCSV(QString::fromLatin1(model.labels));
|
||||
QStringList modelLabels = fromCSV(QString::fromLatin1(model.labels.c_str()));
|
||||
if (modelLabels.indexOf(label) >= 0) {
|
||||
deleted = true;
|
||||
modelLabels.removeAll(label);
|
||||
}
|
||||
strcpy(model.labels, toCSV(modelLabels).toLatin1().data());
|
||||
model.labels = toCSV(modelLabels).toLatin1().constData();
|
||||
}
|
||||
|
||||
// Remove the label from the global list
|
||||
@@ -171,8 +171,8 @@ bool RadioData::renameLabel(QString from, QString to)
|
||||
|
||||
// Check that rename is possible (Rename won't cause too long of a string)
|
||||
for(auto& model : models) {
|
||||
if (RadioData::fromCSV(model.labels).indexOf(from) != -1) {
|
||||
if ((int)strlen(model.labels) + lengthdiff > (int)sizeof(model.labels) - 1) {
|
||||
if (RadioData::fromCSV(model.labels.toQString()).indexOf(from) != -1) {
|
||||
if ((int)model.labels.size() + lengthdiff > (int)model.labels.capacity()) {
|
||||
success = false;
|
||||
throw std::length_error(model.name);
|
||||
break;
|
||||
@@ -181,13 +181,13 @@ bool RadioData::renameLabel(QString from, QString to)
|
||||
}
|
||||
if (success) {
|
||||
for(auto& model : models) {
|
||||
QStringList modelLabels = QString(model.labels).split(',', Qt::SkipEmptyParts);
|
||||
QStringList modelLabels = model.labels.toQString().split(',', Qt::SkipEmptyParts);
|
||||
int ind = modelLabels.indexOf(csvFrom);
|
||||
if (ind != -1) {
|
||||
modelLabels.replace(ind, csvTo);
|
||||
QString outputcsv = QString(modelLabels.join(','));
|
||||
if (outputcsv.toLatin1().size() < (int)sizeof(model.labels)) {
|
||||
strcpy(model.labels, outputcsv.toLatin1().data());
|
||||
if (outputcsv.toLatin1().size() <= (int)model.labels.capacity()) {
|
||||
model.labels = outputcsv.toLatin1().constData();
|
||||
} else { // Shouldn't ever get here, from check above
|
||||
success = false;
|
||||
throw std::length_error(model.name);
|
||||
@@ -228,13 +228,13 @@ bool RadioData::addLabelToModel(int index, QString label)
|
||||
if ((unsigned int)index >= models.size()) return false;
|
||||
label = escapeCSV(label);
|
||||
|
||||
char *modelLabelCsv = models[index].labels;
|
||||
auto & modelLabelCsv = models[index].labels;
|
||||
// Make sure it will fit
|
||||
if (strlen(modelLabelCsv) + label.size() + 1 < sizeof(models[index].labels)-1) {
|
||||
QStringList modelLabels = QString::fromLatin1(modelLabelCsv).split(',', Qt::SkipEmptyParts);
|
||||
if (modelLabelCsv.size() + (size_t)label.size() + 1 < modelLabelCsv.capacity()) {
|
||||
QStringList modelLabels = QString::fromLatin1(modelLabelCsv.c_str()).split(',', Qt::SkipEmptyParts);
|
||||
if (modelLabels.indexOf(label) == -1) {
|
||||
modelLabels.append(label);
|
||||
strcpy(models[index].labels, QString(modelLabels.join(',')).toLatin1().data());
|
||||
modelLabelCsv = QString(modelLabels.join(',')).toLatin1().constData();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -246,10 +246,10 @@ bool RadioData::removeLabelFromModel(int index, QString label)
|
||||
{
|
||||
if ((unsigned int)index >= models.size()) return false;
|
||||
|
||||
QStringList lbls = fromCSV(QString::fromLatin1(models[index].labels));
|
||||
QStringList lbls = fromCSV(QString::fromLatin1(models[index].labels.c_str()));
|
||||
if (lbls.indexOf(label) >= 0) {
|
||||
lbls.removeAll(label);
|
||||
strcpy(models[index].labels, toCSV(lbls).toLatin1().data());
|
||||
models[index].labels = toCSV(lbls).toLatin1().constData();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -258,7 +258,7 @@ bool RadioData::removeLabelFromModel(int index, QString label)
|
||||
void RadioData::addLabelsFromModels()
|
||||
{
|
||||
for(const auto &model: models) {
|
||||
QStringList labels = QString(model.labels).split(',', Qt::SkipEmptyParts);
|
||||
QStringList labels = model.labels.toQString().split(',', Qt::SkipEmptyParts);
|
||||
foreach(QString label, labels) {
|
||||
addLabel(label);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ QVariant LabelsModel::data(const QModelIndex &index, int role) const
|
||||
} else if (role == Qt::CheckStateRole) {
|
||||
if (index.column() == 0 && selectedModel >= 0 &&
|
||||
selectedModel < (int)radioData->models.size()) {
|
||||
QStringList modelLabels = QString(radioData->models.at(selectedModel).labels).split(',',Qt::SkipEmptyParts);
|
||||
QStringList modelLabels = radioData->models.at(selectedModel).labels.toQString().split(',',Qt::SkipEmptyParts);
|
||||
label = RadioData::escapeCSV(label);
|
||||
return modelLabels.indexOf(label)==-1?Qt::Unchecked:Qt::Checked;
|
||||
} else if (index.column() == 0 && selectedModel == -1) {
|
||||
|
||||
@@ -1033,7 +1033,7 @@ void MdiChild::pasteModelData(const QMimeData * mimeData, const QModelIndex row,
|
||||
// We don't want to create an index value conflict so use an invalid one (it will get updated after we're done here)
|
||||
// this is esp. important because otherwise we may delete this model during a move operation (eg. after a cut)
|
||||
radioData.models[modelIdx].modelIndex = -modelIdx;
|
||||
strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str());
|
||||
radioData.models[modelIdx].filename = radioData.getNextModelFilename();
|
||||
lastSelectedModel = modelIdx; // after refresh the last pasted model will be selected
|
||||
modified = true;
|
||||
setModelModified(modelIdx, false); // avoid unnecessary refreshes
|
||||
@@ -1259,7 +1259,7 @@ void MdiChild::openModelEditWindow(int row)
|
||||
gStopwatch.report("ModelEdit creation");
|
||||
ModelEdit * t = new ModelEdit(this, radioData, (row), firmware);
|
||||
gStopwatch.report("ModelEdit created");
|
||||
t->setWindowTitle(tr("Editing model %1: ").arg(row+1) + QString(model.name) + QString(" (%1)").arg(userFriendlyCurrentFile()));
|
||||
t->setWindowTitle(tr("Editing model %1: ").arg(row+1) + model.name.toQString() + QString(" (%1)").arg(userFriendlyCurrentFile()));
|
||||
connect(t, &ModelEdit::modified, this, &MdiChild::setCurrentModelModified);
|
||||
connect(t, &ModelEdit::closed, this, &MdiChild::onModelEditClosed);
|
||||
gStopwatch.report("STARTING MODEL EDIT");
|
||||
@@ -1922,7 +1922,7 @@ unsigned MdiChild::exportModels(const QVector<int> modelIndices)
|
||||
if (idx < 0 || idx >= (int)radioData.models.size())
|
||||
continue;
|
||||
|
||||
const QString path(QDir::toNativeSeparators(g.profile[g.id()].sdPath() + "/TEMPLATES/" + QString(radioData.models[idx].name) + ".yml"));
|
||||
const QString path(QDir::toNativeSeparators(g.profile[g.id()].sdPath() + "/TEMPLATES/" + radioData.models[idx].name.toQString() + ".yml"));
|
||||
qDebug() << path;
|
||||
|
||||
QString filename;
|
||||
@@ -2000,7 +2000,7 @@ QStringList MdiChild::modelErrorsList()
|
||||
void MdiChild::modelShowErrors()
|
||||
{
|
||||
ModelData &mdl = radioData.models[getCurrentModel()];
|
||||
QMessageBox::critical(this, QString("%1").arg(mdl.name), mdl.errorsList().join("\n"));
|
||||
QMessageBox::critical(this, QString("%1").arg(mdl.name.toQString()), mdl.errorsList().join("\n"));
|
||||
}
|
||||
|
||||
void MdiChild::onModelEditClosed(int id)
|
||||
@@ -2111,7 +2111,7 @@ void MdiChild::modelImport()
|
||||
if (ok) {
|
||||
// We don't want to create an index value conflict so use an invalid one (it will get updated after we're done here)
|
||||
radioData.models[modelIdx].modelIndex = -modelIdx;
|
||||
strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str());
|
||||
radioData.models[modelIdx].filename = radioData.getNextModelFilename();
|
||||
lastSelectedModel = modelIdx; // after refresh the last pasted model will be selected
|
||||
setModelModified(modelIdx, false); // avoid unnecessary refreshes
|
||||
radioData.addLabelsFromModels();
|
||||
|
||||
@@ -357,9 +357,9 @@ void SetupPanel::on_throttleTrimSwitch_currentIndexChanged(int index)
|
||||
|
||||
void SetupPanel::on_name_editingFinished()
|
||||
{
|
||||
if (QString(model->name) != ui->name->text()) {
|
||||
if (model->name.toQString() != ui->name->text()) {
|
||||
int length = ui->name->maxLength();
|
||||
strncpy(model->name, ui->name->text().toLatin1(), length);
|
||||
model->name = ui->name->text().left(length).toLatin1().constData();
|
||||
emit modified();
|
||||
}
|
||||
}
|
||||
@@ -410,7 +410,7 @@ void SetupPanel::populateThrottleTrimSwitchCB()
|
||||
|
||||
void SetupPanel::update()
|
||||
{
|
||||
ui->name->setText(model->name);
|
||||
ui->name->setText(model->name.toQString());
|
||||
ui->throttleReverse->setChecked(model->throttleReversed);
|
||||
ui->throttleSource->updateValue();
|
||||
populateThrottleTrimSwitchCB();
|
||||
|
||||
@@ -715,8 +715,8 @@ void ModelsListModel::refresh()
|
||||
|
||||
if (!model.isEmpty() && current) {
|
||||
QString modelName;
|
||||
if (strlen(model.name) > 0) {
|
||||
modelName = model.name;
|
||||
if (!model.name.empty()) {
|
||||
modelName = model.name.toQString();
|
||||
}
|
||||
else {
|
||||
/*: Translators: do NOT use accents here, this is a default model name. */
|
||||
@@ -746,7 +746,7 @@ void ModelsListModel::refresh()
|
||||
current->setData(currentColumn++, rxs);
|
||||
}
|
||||
if (hasLabels) {
|
||||
QStringList labels = RadioData::fromCSV(QString::fromUtf8(model.labels));
|
||||
QStringList labels = RadioData::fromCSV(QString::fromUtf8(model.labels.c_str()));
|
||||
current->setData(currentColumn++, labels.join(QChar(0x2022)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ void CompareDialog::compare()
|
||||
|
||||
for (int i=0; i < modelsList.size(); ++i) {
|
||||
multimodelprinter->setModel(i, &modelsList[i].model, &modelsList[i].gs);
|
||||
QString name(modelsList.at(i).model.name);
|
||||
QString name(modelsList.at(i).model.name.toQString());
|
||||
if (name.isEmpty())
|
||||
name = tr("Unnamed Model %1").arg(i+1);
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ QString MultiModelPrinter::printSetup()
|
||||
|
||||
MultiColumns columns(modelPrinterMap.size());
|
||||
columns.appendSectionTableStart();
|
||||
ROWLABELCOMPARECELL(tr("Name"), 20, model->name, 80);
|
||||
ROWLABELCOMPARECELL(tr("Name"), 20, model->name.toQString(), 80);
|
||||
if (firmware->getCapability(HasModelImage)) {
|
||||
ROWLABELCOMPARECELL(tr("Model Image"), 0, model->bitmap, 0);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ PrintDialog::PrintDialog(QWidget *parent, Firmware * firmware, GeneralSettings &
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowIcon(CompanionIcon("print.png"));
|
||||
setWindowTitle(model.name);
|
||||
setWindowTitle(model.name.toQString());
|
||||
multiModelPrinter.setModel(0, &model, &generalSettings);
|
||||
ui->textEdit->setHtml(multiModelPrinter.print(ui->textEdit->document()));
|
||||
if (!printfilename.isEmpty()) {
|
||||
|
||||
@@ -178,9 +178,9 @@ bool LabelsStorageFormat::load(RadioData & radioData)
|
||||
}
|
||||
|
||||
model.modelIndex = modelIdx;
|
||||
strncpy(model.filename, mc.filename.c_str(), sizeof(model.filename)-1);
|
||||
model.filename = mc.filename;
|
||||
|
||||
if (hasLabels && !strncmp(radioData.generalSettings.currModelFilename, model.filename, sizeof(model.filename)))
|
||||
if (hasLabels && model.filename == radioData.generalSettings.currModelFilename)
|
||||
radioData.generalSettings.currModelIndex = modelIdx;
|
||||
|
||||
model.used = true;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (C) EdgeTX
|
||||
*
|
||||
* Based on code named
|
||||
* opentx - https://github.com/opentx/opentx
|
||||
* 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 "gtests.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "firmwares/boundedstring.h"
|
||||
#include "firmwares/edgetx/yaml_ops.h"
|
||||
|
||||
// A representative spread of inputs, including boundary and over-length cases
|
||||
// and a multi-byte UTF-8 sequence that straddles the truncation point.
|
||||
static const std::vector<std::string> kInputs = {
|
||||
"",
|
||||
"model1.yml",
|
||||
"0123456789012345", // 16 bytes: exactly the filename capacity
|
||||
"01234567890123456789", // over the filename capacity
|
||||
std::string(160, 'X'), // far over any capacity
|
||||
"caf\xC3\xA9", // "café" (UTF-8), 5 bytes
|
||||
};
|
||||
|
||||
// --- Construction & assignment -------------------------------------------
|
||||
|
||||
TEST(BoundedString, AssignTruncatesToCapacity)
|
||||
{
|
||||
BoundedString<16> bs;
|
||||
bs = std::string(40, 'A');
|
||||
EXPECT_EQ(16u, bs.size());
|
||||
EXPECT_EQ(std::string(16, 'A'), bs.str());
|
||||
}
|
||||
|
||||
TEST(BoundedString, ShortValueIsUntouched)
|
||||
{
|
||||
BoundedString<16> bs("model1.yml");
|
||||
EXPECT_EQ("model1.yml", bs.str());
|
||||
EXPECT_FALSE(bs.empty());
|
||||
}
|
||||
|
||||
TEST(BoundedString, AssignFromVariousSources)
|
||||
{
|
||||
BoundedString<16> a("c-string");
|
||||
BoundedString<16> b(std::string("std-string"));
|
||||
BoundedString<16> c(QString("qstring"));
|
||||
EXPECT_EQ("c-string", a.str());
|
||||
EXPECT_EQ("std-string", b.str());
|
||||
EXPECT_EQ("qstring", c.str());
|
||||
EXPECT_EQ(QString("qstring"), c.toQString());
|
||||
}
|
||||
|
||||
TEST(BoundedString, NullCStringIsEmpty)
|
||||
{
|
||||
const char* p = nullptr;
|
||||
BoundedString<16> bs(p);
|
||||
EXPECT_TRUE(bs.empty());
|
||||
}
|
||||
|
||||
TEST(BoundedString, Comparison)
|
||||
{
|
||||
BoundedString<16> a("abc");
|
||||
BoundedString<16> b("abc");
|
||||
BoundedString<16> c("xyz");
|
||||
EXPECT_TRUE(a == b);
|
||||
EXPECT_TRUE(a != c);
|
||||
EXPECT_TRUE(a == std::string("abc"));
|
||||
EXPECT_TRUE(a != std::string("abcd"));
|
||||
EXPECT_TRUE(a == "abc"); // const char*
|
||||
}
|
||||
|
||||
// --- YAML round-trip ------------------------------------------------------
|
||||
|
||||
TEST(BoundedString, YamlRoundTrip)
|
||||
{
|
||||
for (const auto& in : kInputs) {
|
||||
BoundedString<16> src(in); // truncates on construction
|
||||
YAML::Node node;
|
||||
node["v"] = src;
|
||||
|
||||
BoundedString<16> dst;
|
||||
node["v"] >> dst;
|
||||
|
||||
EXPECT_EQ(src.str(), dst.str()) << "round-trip mismatch for input: " << in;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BoundedString, YamlDecodeTruncatesOverlongScalar)
|
||||
{
|
||||
// A malicious/oversized scalar must be clamped on decode, not overflow.
|
||||
YAML::Node node;
|
||||
node["v"] = std::string(200, 'Z');
|
||||
|
||||
BoundedString<16> dst;
|
||||
node["v"] >> dst;
|
||||
EXPECT_EQ(16u, dst.size());
|
||||
}
|
||||
|
||||
// --- Byte-for-byte equivalence with the legacy char[N+1] path -------------
|
||||
// This is the gate that protects radio YAML compatibility: BoundedString<N>
|
||||
// must produce identical bytes to a legacy `char[N+1]` field on both encode
|
||||
// and decode, for every input.
|
||||
|
||||
TEST(BoundedString, MatchesLegacyCharArrayOnDecode)
|
||||
{
|
||||
constexpr size_t CAP = 16;
|
||||
for (const auto& in : kInputs) {
|
||||
YAML::Node node;
|
||||
node["v"] = in;
|
||||
|
||||
char legacy[CAP + 1] = {0};
|
||||
node["v"] >> legacy;
|
||||
|
||||
BoundedString<CAP> modern;
|
||||
node["v"] >> modern;
|
||||
|
||||
EXPECT_EQ(std::string(legacy), modern.str())
|
||||
<< "decode diverged from legacy char[] for input: " << in;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BoundedString, MatchesLegacyCharArrayOnEncode)
|
||||
{
|
||||
constexpr size_t CAP = 16;
|
||||
for (const auto& in : kInputs) {
|
||||
char legacy[CAP + 1] = {0};
|
||||
{
|
||||
// seed the legacy buffer the same way decode would
|
||||
YAML::Node seed;
|
||||
seed["v"] = in;
|
||||
seed["v"] >> legacy;
|
||||
}
|
||||
BoundedString<CAP> modern(in);
|
||||
|
||||
YAML::Node a, b;
|
||||
a["v"] = legacy;
|
||||
b["v"] = modern;
|
||||
|
||||
YAML::Emitter ea, eb;
|
||||
ea << a;
|
||||
eb << b;
|
||||
EXPECT_EQ(std::string(ea.c_str()), std::string(eb.c_str()))
|
||||
<< "encode diverged from legacy char[] for input: " << in;
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
#include "gtests.h"
|
||||
#include "location.h"
|
||||
#include "storage/etx.h"
|
||||
#include "storage/storage.h"
|
||||
#include "firmwares/opentx/opentxinterface.h"
|
||||
#include "firmwares/customfunctiondata.h"
|
||||
|
||||
TEST(Conversions, ConversionX9DPFrom22)
|
||||
{
|
||||
RadioData radioData;
|
||||
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_22_x9d+.bin");
|
||||
|
||||
ASSERT_EQ(true, store.load(radioData));
|
||||
|
||||
const GeneralSettings& settings = radioData.generalSettings;
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
|
||||
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
|
||||
EXPECT_EQ(20, settings.customFn[0].param);
|
||||
|
||||
EXPECT_STREQ("Tes", settings.switchName[0]);
|
||||
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
|
||||
|
||||
const ModelData& model = radioData.models[0];
|
||||
EXPECT_STREQ("Test", model.name);
|
||||
EXPECT_EQ(TimerData::TIMERMODE_ON, model.timers[0].mode);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_SWITCH, SWITCH_SA0), model.timers[0].swtch);
|
||||
EXPECT_EQ(80, model.mixData[0].weight);
|
||||
EXPECT_EQ(900, model.limitData[0].max); // -100
|
||||
EXPECT_EQ(80, model.expoData[0].weight);
|
||||
|
||||
EXPECT_EQ(CurveReference::CURVE_REF_CUSTOM, model.expoData[0].curve.type);
|
||||
EXPECT_EQ(1, model.expoData[0].curve.value);
|
||||
EXPECT_EQ(CurveReference::CURVE_REF_EXPO, model.expoData[1].curve.type);
|
||||
EXPECT_EQ(20, model.expoData[1].curve.value);
|
||||
|
||||
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
|
||||
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
|
||||
EXPECT_STREQ("Tes", model.gvarData[0].name);
|
||||
EXPECT_EQ(PULSES_PXX_XJT_X16, model.moduleData[0].protocol);
|
||||
EXPECT_EQ(0, model.moduleData[0].subType);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_FCC, model.moduleData[1].subType);
|
||||
EXPECT_STREQ("Thr", model.inputNames[0]);
|
||||
EXPECT_STREQ("Tes", model.sensorData[0].label);
|
||||
EXPECT_EQ(10, model.sensorData[0].id);
|
||||
EXPECT_EQ(10, model.sensorData[0].instance);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY,0).toValue(), model.logicalSw[0].val1);
|
||||
}
|
||||
|
||||
TEST(Conversions, ConversionX9DPFrom23)
|
||||
{
|
||||
RadioData radioData;
|
||||
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_23_x9d+.bin");
|
||||
|
||||
ASSERT_EQ(true, store.load(radioData));
|
||||
|
||||
const GeneralSettings& settings = radioData.generalSettings;
|
||||
EXPECT_EQ(20, settings.speakerVolume);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
|
||||
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
|
||||
EXPECT_EQ(20, settings.customFn[0].param);
|
||||
|
||||
EXPECT_STREQ("Tes", settings.switchName[0]);
|
||||
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
|
||||
|
||||
const ModelData & model1 = radioData.models[0];
|
||||
EXPECT_STREQ("Test", model1.name);
|
||||
EXPECT_EQ(TimerData::TIMERMODE_ON, model1.timers[0].mode);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_SWITCH, SWITCH_SA0), model1.timers[0].swtch);
|
||||
EXPECT_EQ(80, model1.mixData[0].weight);
|
||||
EXPECT_EQ(900, model1.limitData[0].max); // -100
|
||||
EXPECT_EQ(80, model1.expoData[0].weight);
|
||||
// EXPECT_EQ(HELI_SWASH_TYPE_120X, model1.swashRingData.type);
|
||||
EXPECT_EQ(10, model1.flightModeData[0].gvars[0]);
|
||||
EXPECT_STREQ("Tes", model1.gvarData[0].name);
|
||||
EXPECT_EQ(PULSES_ACCST_ISRM_D16, model1.moduleData[0].protocol);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model1.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_FCC, model1.moduleData[1].subType);
|
||||
EXPECT_STREQ("Rud", model1.inputNames[0]);
|
||||
EXPECT_STREQ("Tes", model1.sensorData[0].label);
|
||||
EXPECT_EQ(10, model1.sensorData[0].id);
|
||||
EXPECT_EQ(11, model1.sensorData[0].instance);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY,0).toValue(), model1.logicalSw[0].val1);
|
||||
|
||||
const ModelData & model2 = radioData.models[1];
|
||||
EXPECT_STREQ("Test", model2.name);
|
||||
EXPECT_EQ(TimerData::TIMERMODE_ON, model2.timers[0].mode);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_SWITCH, SWITCH_SA0), model2.timers[0].swtch);
|
||||
EXPECT_EQ(80, model2.mixData[0].weight);
|
||||
EXPECT_EQ(900, model2.limitData[0].max); // -100
|
||||
EXPECT_EQ(80, model2.expoData[0].weight);
|
||||
// EXPECT_EQ(HELI_SWASH_TYPE_120X, model2.swashRingData.type);
|
||||
EXPECT_EQ(10, model2.flightModeData[0].gvars[0]);
|
||||
EXPECT_STREQ("Tes", model2.gvarData[0].name);
|
||||
EXPECT_EQ(PULSES_ACCESS_ISRM, model2.moduleData[0].protocol);
|
||||
EXPECT_EQ(0, model2.moduleData[0].subType);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model2.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_FCC, model2.moduleData[1].subType);
|
||||
EXPECT_STREQ("Rud", model2.inputNames[0]);
|
||||
EXPECT_STREQ("Tes", model2.sensorData[0].label);
|
||||
EXPECT_EQ(10, model2.sensorData[0].id);
|
||||
EXPECT_EQ(11, model2.sensorData[0].instance);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY,0).toValue(), model2.logicalSw[0].val1);
|
||||
}
|
||||
|
||||
TEST(Conversions, ConversionX7From22)
|
||||
{
|
||||
RadioData radioData;
|
||||
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_22_x7.bin");
|
||||
|
||||
ASSERT_EQ(true, store.load(radioData));
|
||||
|
||||
const GeneralSettings& settings = radioData.generalSettings;
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
|
||||
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
|
||||
EXPECT_EQ(20, settings.customFn[0].param);
|
||||
|
||||
EXPECT_STREQ("Tes", settings.switchName[0]);
|
||||
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
|
||||
|
||||
const ModelData& model = radioData.models[0];
|
||||
EXPECT_STREQ("Test", model.name);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
|
||||
|
||||
EXPECT_EQ(80, model.mixData[0].weight);
|
||||
EXPECT_EQ(80, model.expoData[0].weight);
|
||||
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), model.mixData[4].srcRaw);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[4].swtch);
|
||||
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
|
||||
EXPECT_STREQ("Thr", model.inputNames[0]);
|
||||
|
||||
EXPECT_STREQ("Tes", model.sensorData[0].label);
|
||||
EXPECT_EQ(10, model.sensorData[0].id);
|
||||
EXPECT_EQ(10, model.sensorData[0].instance);
|
||||
EXPECT_EQ(900, model.limitData[0].max); // -100
|
||||
|
||||
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
|
||||
EXPECT_STREQ("FMtest", model.flightModeData[1].name);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_VIRTUAL,1), model.flightModeData[1].swtch);
|
||||
EXPECT_STREQ("Tes", model.gvarData[0].name);
|
||||
|
||||
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1);
|
||||
EXPECT_EQ(0, model.logicalSw[0].val2);
|
||||
|
||||
const FrSkyScreenData& screen = model.frsky.screens[0];
|
||||
EXPECT_EQ(TELEMETRY_SCREEN_NUMBERS, screen.type);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), screen.body.lines[0].source[0]);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_SPECIAL,4), screen.body.lines[0].source[1]);
|
||||
}
|
||||
|
||||
TEST(Conversions, ConversionXLiteFrom22)
|
||||
{
|
||||
RadioData radioData;
|
||||
Storage store = Storage(RADIO_TESTS_PATH "/eeprom_22_xlite.bin");
|
||||
|
||||
ASSERT_EQ(true, store.load(radioData));
|
||||
|
||||
const GeneralSettings& settings = radioData.generalSettings;
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
|
||||
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
|
||||
EXPECT_EQ(20, settings.customFn[0].param);
|
||||
|
||||
EXPECT_STREQ("Tes", settings.switchName[0]);
|
||||
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
|
||||
|
||||
const ModelData& model = radioData.models[0];
|
||||
EXPECT_STREQ("Test", model.name);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
|
||||
|
||||
EXPECT_EQ(80, model.mixData[0].weight);
|
||||
EXPECT_EQ(80, model.expoData[0].weight);
|
||||
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), model.mixData[4].srcRaw);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[4].swtch);
|
||||
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
|
||||
EXPECT_STREQ("Thr", model.inputNames[0]);
|
||||
|
||||
EXPECT_STREQ("Tes", model.sensorData[0].label);
|
||||
EXPECT_EQ(10, model.sensorData[0].id);
|
||||
EXPECT_EQ(9, model.sensorData[0].instance);
|
||||
EXPECT_EQ(900, model.limitData[0].max); // -100
|
||||
|
||||
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1);
|
||||
EXPECT_EQ(0, model.logicalSw[0].val2);
|
||||
|
||||
const FrSkyScreenData& screen = model.frsky.screens[0];
|
||||
EXPECT_EQ(TELEMETRY_SCREEN_NUMBERS, screen.type);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0), screen.body.lines[0].source[0]);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_SPECIAL,4), screen.body.lines[0].source[1]);
|
||||
}
|
||||
|
||||
bool loadFile(QByteArray & filedata, const QString & filename)
|
||||
{
|
||||
QFile file(filename);
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
qDebug() << QString("Error opening file %1:\n%2.").arg(filename).arg(file.errorString());
|
||||
return false;
|
||||
}
|
||||
filedata = file.readAll();
|
||||
qDebug() << "File" << filename << "read, size:" << filedata.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(Conversions, ConversionX10From22)
|
||||
{
|
||||
QByteArray byteBuffer;
|
||||
|
||||
#define USE_ETX
|
||||
|
||||
#if defined(USE_ETX)
|
||||
EtxFormat etx(RADIO_TESTS_PATH "/model_22_x10.etx");
|
||||
RadioData radio;
|
||||
EXPECT_EQ(true, etx.load(radio));
|
||||
|
||||
const GeneralSettings& settings = radio.generalSettings;
|
||||
const ModelData& model = radio.models[0];
|
||||
#else
|
||||
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x10/RADIO/radio.bin"));
|
||||
|
||||
GeneralSettings settings;
|
||||
EXPECT_NE(nullptr, loadRadioSettingsFromByteArray(settings, byteBuffer));
|
||||
#endif
|
||||
|
||||
EXPECT_EQ(100, settings.calibSpanNeg[9]);
|
||||
EXPECT_EQ(500, settings.calibMid[9]);
|
||||
EXPECT_EQ(900, settings.calibSpanPos[9]);
|
||||
|
||||
EXPECT_EQ(200, settings.calibSpanNeg[10]);
|
||||
EXPECT_EQ(400, settings.calibMid[10]);
|
||||
EXPECT_EQ(600, settings.calibSpanPos[10]);
|
||||
|
||||
EXPECT_EQ(-23, settings.vBatMin);
|
||||
EXPECT_EQ(20, settings.speakerVolume);
|
||||
EXPECT_STREQ("en", settings.ttsLanguage);
|
||||
EXPECT_STREQ("model1.bin", settings.currModelFilename);
|
||||
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
|
||||
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
|
||||
EXPECT_EQ(20, settings.customFn[0].param);
|
||||
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_ON), settings.customFn[1].swtch);
|
||||
EXPECT_EQ(FuncVolume, settings.customFn[1].func);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5+1).toValue(), settings.customFn[1].param); // RS
|
||||
|
||||
EXPECT_STREQ("Tes", settings.switchName[0]);
|
||||
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
|
||||
|
||||
EXPECT_STREQ("BT_X10", settings.bluetoothName);
|
||||
|
||||
#if !defined(USE_ETX)
|
||||
byteBuffer.clear();
|
||||
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x10/MODELS/model1.bin"));
|
||||
|
||||
ModelData model;
|
||||
ASSERT_NE(nullptr, loadModelFromByteArray(model, byteBuffer));
|
||||
#endif
|
||||
|
||||
EXPECT_STREQ("Test", model.name);
|
||||
EXPECT_EQ(0, model.noGlobalFunctions);
|
||||
EXPECT_EQ(0, model.beepANACenter);
|
||||
EXPECT_EQ(80, model.mixData[0].weight);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_MAX), model.mixData[2].srcRaw); // MAX
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5), model.mixData[3].srcRaw); // LS
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM, 0), model.mixData[5].srcRaw);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[5].swtch);
|
||||
EXPECT_EQ(900, model.limitData[0].max); // -100
|
||||
EXPECT_EQ(80, model.expoData[0].weight);
|
||||
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1); // TR1
|
||||
EXPECT_EQ(0, model.logicalSw[0].val2);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY, 19*3).toValue(), model.logicalSw[1].val1); // TELE:20
|
||||
EXPECT_EQ(20, model.logicalSw[1].val2);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_VIRTUAL,1).toValue(), model.logicalSw[1].andsw);
|
||||
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
|
||||
EXPECT_STREQ("Tes", model.flightModeData[0].name);
|
||||
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
|
||||
EXPECT_STREQ("Tes", model.gvarData[0].name);
|
||||
EXPECT_EQ(PULSES_PXX_XJT_D8, model.moduleData[0].protocol);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
|
||||
EXPECT_STREQ("Rud", model.inputNames[0]);
|
||||
EXPECT_STREQ("Tes", model.sensorData[0].label);
|
||||
EXPECT_EQ(10, model.sensorData[0].id);
|
||||
EXPECT_EQ(10, model.sensorData[0].instance);
|
||||
EXPECT_EQ(5 + 2 + 3, model.thrTraceSrc); // CH3
|
||||
}
|
||||
|
||||
TEST(Conversions, ConversionX12SFrom22)
|
||||
{
|
||||
QByteArray byteBuffer;
|
||||
|
||||
#define USE_ETX
|
||||
|
||||
#if defined(USE_ETX)
|
||||
EtxFormat etx(RADIO_TESTS_PATH "/model_22_x12s.etx");
|
||||
RadioData radio;
|
||||
EXPECT_EQ(true, etx.load(radio));
|
||||
|
||||
const GeneralSettings& settings = radio.generalSettings;
|
||||
const ModelData& model = radio.models[0];
|
||||
#else
|
||||
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x12s/RADIO/radio.bin"));
|
||||
|
||||
GeneralSettings settings;
|
||||
EXPECT_NE(nullptr, loadRadioSettingsFromByteArray(settings, byteBuffer));
|
||||
#endif
|
||||
|
||||
EXPECT_EQ(-30, settings.vBatMin);
|
||||
EXPECT_EQ(20, settings.speakerVolume);
|
||||
EXPECT_STREQ("en", settings.ttsLanguage);
|
||||
EXPECT_STREQ("model1.bin", settings.currModelFilename);
|
||||
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), settings.customFn[0].swtch);
|
||||
EXPECT_EQ(FuncLogs, settings.customFn[0].func);
|
||||
EXPECT_EQ(20, settings.customFn[0].param);
|
||||
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_ON), settings.customFn[1].swtch);
|
||||
EXPECT_EQ(FuncVolume, settings.customFn[1].func);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5+1).toValue(), settings.customFn[1].param); // RS
|
||||
|
||||
EXPECT_STREQ("Tes", settings.switchName[0]);
|
||||
EXPECT_EQ(Board::SWITCH_3POS, settings.switchConfig[0]);
|
||||
|
||||
EXPECT_STREQ("BT", settings.bluetoothName);
|
||||
|
||||
#if !defined(USE_ETX)
|
||||
byteBuffer.clear();
|
||||
ASSERT_EQ(true, loadFile(byteBuffer, RADIO_TESTS_PATH "/model_22_x10/MODELS/model1.bin"));
|
||||
|
||||
ModelData model;
|
||||
ASSERT_NE(nullptr, loadModelFromByteArray(model, byteBuffer));
|
||||
#endif
|
||||
|
||||
EXPECT_STREQ("Test", model.name);
|
||||
EXPECT_EQ(0, model.noGlobalFunctions);
|
||||
EXPECT_EQ(0, model.beepANACenter);
|
||||
EXPECT_EQ(80, model.mixData[0].weight);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_MAX), model.mixData[2].srcRaw); // MAX
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_INPUT, 4+5), model.mixData[3].srcRaw); // LS
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM, 0), model.mixData[5].srcRaw);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_TELEMETRY, 1), model.mixData[5].swtch);
|
||||
EXPECT_EQ(900, model.limitData[0].max); // -100
|
||||
EXPECT_EQ(80, model.expoData[0].weight);
|
||||
EXPECT_EQ(LS_FN_VPOS, model.logicalSw[0].func);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_PPM,0).toValue(), model.logicalSw[0].val1); // TR1
|
||||
EXPECT_EQ(0, model.logicalSw[0].val2);
|
||||
EXPECT_EQ(RawSource(SOURCE_TYPE_TELEMETRY, 19*3).toValue(), model.logicalSw[1].val1); // TELE:20
|
||||
EXPECT_EQ(20, model.logicalSw[1].val2);
|
||||
EXPECT_EQ(RawSwitch(SWITCH_TYPE_VIRTUAL,1).toValue(), model.logicalSw[1].andsw);
|
||||
EXPECT_EQ(HELI_SWASH_TYPE_120X, model.swashRingData.type);
|
||||
EXPECT_STREQ("Test", model.flightModeData[0].name);
|
||||
EXPECT_EQ(10, model.flightModeData[0].gvars[0]);
|
||||
EXPECT_STREQ("Tes", model.gvarData[0].name);
|
||||
EXPECT_EQ(PULSES_PXX_R9M, model.moduleData[1].protocol);
|
||||
EXPECT_EQ(MODULE_SUBTYPE_R9M_EU, model.moduleData[1].subType);
|
||||
EXPECT_STREQ("Rud", model.inputNames[0]);
|
||||
EXPECT_STREQ("Tes", model.sensorData[0].label);
|
||||
EXPECT_EQ(10, model.sensorData[0].id);
|
||||
EXPECT_EQ(10, model.sensorData[0].instance);
|
||||
EXPECT_EQ(5 + 2 + 3, model.thrTraceSrc); // CH3
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "storage.h"
|
||||
#include "opentxinterface.h"
|
||||
#include "customdebug.h"
|
||||
#include "firmwares/boardfactories.h"
|
||||
|
||||
|
||||
using ::testing::TestEventListener;
|
||||
@@ -130,6 +131,7 @@ int main(int argc, char **argv)
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
CustomDebug::setFilterRules();
|
||||
gBoardFactories = new BoardFactories();
|
||||
registerStorageFactories();
|
||||
registerOpenTxFirmwares();
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) EdgeTX
|
||||
*
|
||||
* Based on code named
|
||||
* opentx - https://github.com/opentx/opentx
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Model-level YAML round-trip corpus test.
|
||||
//
|
||||
// Gates the byte-compatibility of the BoundedString<N> migration: the
|
||||
// migrated ModelData header fields (name, labels) must survive Companion's
|
||||
// real YAML (de)serialisation unchanged, and the serialisation must be a
|
||||
// byte-stable fixed point. Truncation to the field capacity must match the
|
||||
// radio (and the legacy char[] behaviour) exactly.
|
||||
//
|
||||
// Note: ModelData::filename is the on-storage file name and is NOT part of a
|
||||
// single model's YAML (the ScriptData "file" key is unrelated), so it is not
|
||||
// exercised here -- its byte equivalence is covered by boundedstring_test.
|
||||
|
||||
#include "gtests.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "firmwares/eeprominterface.h"
|
||||
#include "firmwares/edgetx/edgetxinterface.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Round-trip a model through the real Companion YAML serialisation and return
|
||||
// the reloaded copy; hands the serialised bytes back to the caller.
|
||||
ModelData roundTrip(const ModelData& in, QByteArray& yamlOut)
|
||||
{
|
||||
writeModelToYaml(in, yamlOut);
|
||||
ModelData out;
|
||||
out.clear();
|
||||
loadModelFromYaml(out, yamlOut);
|
||||
return out;
|
||||
}
|
||||
|
||||
class ModelYamlRoundTrip : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
Firmware::setCurrentVariant(Firmware::getFirmwareForFlavour("tx16s"));
|
||||
ASSERT_NE(getCurrentFirmware(), nullptr)
|
||||
<< "tx16s firmware must be registered for this test";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Valid, in-limit header fields survive a round-trip unchanged, and writing
|
||||
// the reloaded model reproduces identical bytes (a deterministic fixed point).
|
||||
TEST_F(ModelYamlRoundTrip, HeaderFieldsSurviveAndAreStable)
|
||||
{
|
||||
struct Case { const char* name; const char* labels; };
|
||||
const std::vector<Case> corpus = {
|
||||
{"MyModel", "fav,race"},
|
||||
{"ABCDEFGHIJKLMNO", "alpha,bravo"}, // name exactly at the 15-char limit
|
||||
{"", ""}, // empty model
|
||||
{"Plane01", "fav"},
|
||||
{"Heli3D", "race,fav,park,3d,heli,test"},
|
||||
};
|
||||
|
||||
for (const auto& c : corpus) {
|
||||
ModelData m;
|
||||
m.clear();
|
||||
m.used = true;
|
||||
m.name = c.name;
|
||||
m.labels = c.labels;
|
||||
|
||||
QByteArray y1;
|
||||
ModelData m2 = roundTrip(m, y1);
|
||||
|
||||
EXPECT_EQ(m.name.str(), m2.name.str()) << "name lost for: " << c.name;
|
||||
EXPECT_EQ(m.labels.str(), m2.labels.str()) << "labels lost for: " << c.name;
|
||||
|
||||
// Fixed point at the field level: reloading the re-serialised model
|
||||
// yields the same field bytes. (We deliberately do NOT compare the whole
|
||||
// YAML document -- it round-trips unrelated non-string fields that have
|
||||
// pre-existing non-idempotencies, e.g. varioData.centerSilent.)
|
||||
QByteArray y2;
|
||||
ModelData m3 = roundTrip(m2, y2);
|
||||
|
||||
EXPECT_EQ(m2.name.str(), m3.name.str());
|
||||
EXPECT_EQ(m2.labels.str(), m3.labels.str());
|
||||
}
|
||||
}
|
||||
|
||||
// An over-length name is truncated to the field capacity on assignment (no
|
||||
// overflow), and that truncated value is exactly what round-trips.
|
||||
TEST_F(ModelYamlRoundTrip, OverlongNameTruncatesAndRoundTrips)
|
||||
{
|
||||
ModelData m;
|
||||
m.clear();
|
||||
m.used = true;
|
||||
m.name = std::string(40, 'N'); // capacity is MODEL_NAME_LEN (15)
|
||||
|
||||
ASSERT_EQ(m.name.size(), (size_t)MODEL_NAME_LEN);
|
||||
|
||||
QByteArray y1;
|
||||
ModelData m2 = roundTrip(m, y1);
|
||||
EXPECT_EQ(m.name.str(), m2.name.str());
|
||||
|
||||
// The truncated value is a fixed point: no further loss or growth.
|
||||
QByteArray y2;
|
||||
ModelData m3 = roundTrip(m2, y2);
|
||||
EXPECT_EQ(m2.name.str(), m3.name.str());
|
||||
}
|
||||
@@ -46,8 +46,7 @@ WizMix::WizMix(const GeneralSettings & settings, unsigned int modelId, const Mod
|
||||
vehicle(NOVEHICLE)
|
||||
{
|
||||
memset(name, 0, sizeof(name));
|
||||
memcpy(name, originalModelData.name, sizeof(name) - 1);
|
||||
name[sizeof(name) - 1] = '\0';
|
||||
strncpy(name, originalModelData.name.c_str(), sizeof(name) - 1);
|
||||
}
|
||||
|
||||
void WizMix::maxMixSwitch(char *name, MixData &mix, int channel, int sw, int weight)
|
||||
@@ -87,7 +86,7 @@ WizMix::operator ModelData()
|
||||
int throttleChannel = -1;
|
||||
|
||||
ModelData model;
|
||||
model.labels[0] = '\0';
|
||||
model.labels.clear();
|
||||
model.used = true;
|
||||
model.moduleData[0].modelId = modelId;
|
||||
model.setDefaultInputs(settings);
|
||||
@@ -96,9 +95,7 @@ WizMix::operator ModelData()
|
||||
int mixIndex = 0;
|
||||
int timerIndex = 0;
|
||||
|
||||
memset(model.name, 0, sizeof(model.name));
|
||||
memcpy(model.name, name, sizeof(model.name) - 1);
|
||||
model.name[sizeof(model.name) - 1] = '\0';
|
||||
model.name = name;
|
||||
|
||||
// Add the channel mixes
|
||||
for (int i=0; i<WIZ_MAX_CHANNELS; i++ )
|
||||
|
||||
Reference in New Issue
Block a user