feat(firmware): add 'user data' section to model yaml for Lua scripts (#7562)

Co-authored-by: Peter Feerick <peter.feerick@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
philmoz
2026-09-18 09:04:48 +10:00
committed by GitHub
co-authored by Peter Feerick Claude Sonnet 5
parent abeed9f23b
commit ed71a38726
45 changed files with 1105 additions and 16 deletions
+1
View File
@@ -51,6 +51,7 @@
#define CPN_MAX_SCRIPT_OUTPUTS 6
#define CPN_MAX_SPACEMOUSE 6
#define CPN_MAX_INPUTS 32 // v2.10 replaces CPN_MAX_ANALOGS - the value is abitary as radio ADC refactor is still a WIP
#define CPN_MAX_USER_DATA 50
// pre v2.10
#define CPN_MAX_POTS 8
@@ -170,6 +170,12 @@ static const YamlLookupTable cfsSwitchLuaOverride = {
{ 1, "ON" },
};
static const YamlLookupTable udTypeLut = {
{ 0, "INT" },
{ 1, "FLOAT" },
{ 2, "STRING" },
};
struct YamlTrim {
int mode = 0;
int ref = 0;
@@ -1307,6 +1313,14 @@ Node convert<ModelData>::encode(const ModelData& rhs)
node["modelCustomScriptsDisabled"] = globalOnOffFilterLut << rhs.modelCustomScriptsDisabled;
node["modelTelemetryDisabled"] = globalOnOffFilterLut << rhs.modelTelemetryDisabled;
for (int i=0; i<CPN_MAX_USER_DATA; i++) {
if (!rhs.userData[i].key.empty()) {
node["userData"][std::to_string(i)]["key"] = rhs.userData[i].key;
node["userData"][std::to_string(i)]["type"] = udTypeLut << rhs.userData[i].type;
node["userData"][std::to_string(i)]["value"] = rhs.userData[i].value;
}
}
return node;
}
@@ -1650,6 +1664,16 @@ bool convert<ModelData>::decode(const Node& node, ModelData& rhs)
node["modelCustomScriptsDisabled"] >> globalOnOffFilterLut >> rhs.modelCustomScriptsDisabled;
node["modelTelemetryDisabled"] >> globalOnOffFilterLut >> rhs.modelTelemetryDisabled;
if (node["userData"]) {
for (int i=0; i<CPN_MAX_USER_DATA; i++) {
if (node["userData"][std::to_string(i)]) {
node["userData"][std::to_string(i)]["key"] >> rhs.userData[i].key;
node["userData"][std::to_string(i)]["type"] >> udTypeLut >> rhs.userData[i].type;
node["userData"][std::to_string(i)]["value"] >> rhs.userData[i].value;
}
}
}
// preferably perform conversions here to avoid cluttering the field decodes
if (modelSettingsVersion < SemanticVersion("2.8.0"))
+8
View File
@@ -127,6 +127,11 @@ void ModelData::copy(const ModelData & src)
checklistData = src.checklistData;
updRefList = nullptr;
memset(&updRefInfo, 0, sizeof(updRefInfo));
for (int i = 0; i < CPN_MAX_USER_DATA; i += 1) {
userData[i].type = src.userData[i].type;
userData[i].key = src.userData[i].key;
userData[i].value = src.userData[i].value;
}
}
ExpoData * ModelData::insertInput(const int idx)
@@ -356,6 +361,9 @@ void ModelData::clear()
updRefList = nullptr;
memset(&updRefInfo, 0, sizeof(updRefInfo));
for (int i = 0; i < CPN_MAX_USER_DATA; i += 1)
userData[i].clear();
}
bool ModelData::isEmpty() const
+12
View File
@@ -126,6 +126,16 @@ class CustomSwitchData {
void clear() { memset(reinterpret_cast<void *>(this), 0, sizeof(CustomSwitchData)); }
};
// Key/Value data for Lua scripts
class UserData {
public:
UserData() { clear(); }
unsigned int type;
std::string key;
std::string value;
void clear() { key.clear(); value.clear(); type = 0; }
};
class ModelData {
Q_DECLARE_TR_FUNCTIONS(ModelData)
@@ -239,6 +249,8 @@ class ModelData {
QByteArray checklistData;
UserData userData[CPN_MAX_USER_DATA];
ModelData & operator=(const ModelData & src);
void convert(RadioDataConversionState & cstate);
@@ -176,3 +176,107 @@ TEST_F(ModelYamlRoundTrip, AntennaModeNotWrittenWithoutExternalAntenna)
ASSERT_TRUE(y.contains("moduleData")) << "module was not serialised";
EXPECT_FALSE(y.contains("antennaMode"));
}
// userData string values may contain embedded NUL bytes.
TEST_F(ModelYamlRoundTrip, UserDataEmbeddedNulSurvivesRoundTrip)
{
ModelData m;
m.clear();
m.used = true;
m.userData[0].key = "App|Key";
m.userData[0].type = 2; // STRING
m.userData[0].value = std::string("emb") + '\0' + "X";
ASSERT_EQ(m.userData[0].value.size(), (size_t)5);
QByteArray y;
ModelData m2 = roundTrip(m, y);
EXPECT_EQ(m2.userData[0].key, m.userData[0].key);
ASSERT_EQ(m2.userData[0].value.size(), m.userData[0].value.size());
EXPECT_EQ(m2.userData[0].value, m.userData[0].value);
}
// Companion must also read a value written by the firmware's own writer,
// which escapes non-printable bytes as \xHH - not just its own output.
TEST_F(ModelYamlRoundTrip, UserDataParsesFirmwareEscapedEmbeddedNul)
{
const QByteArray yaml =
"header:\n"
" name: Tst\n"
"userData:\n"
" \"0\":\n"
" key: \"App|Key\"\n"
" type: STRING\n"
" value: \"emb\\x00X\"\n";
ModelData m;
m.clear();
ASSERT_TRUE(loadModelFromYaml(m, yaml));
EXPECT_EQ(m.userData[0].key, "App|Key");
const std::string expected = std::string("emb") + '\0' + "X";
ASSERT_EQ(m.userData[0].value.size(), expected.size());
EXPECT_EQ(m.userData[0].value, expected);
}
// Quotes and backslashes are both special-cased by the firmware's writer
// (radio/src/storage/yaml/yaml_tree_walker.cpp), more likely to show up in
// real user data than an embedded NUL (quoted text, Windows-style paths).
TEST_F(ModelYamlRoundTrip, UserDataEscapesQuotesAndBackslashesRoundTrip)
{
ModelData m;
m.clear();
m.used = true;
m.userData[0].key = "App|Key";
m.userData[0].type = 2; // STRING
m.userData[0].value = "a\"b\\c";
QByteArray y;
ModelData m2 = roundTrip(m, y);
EXPECT_EQ(m2.userData[0].value, m.userData[0].value);
}
TEST_F(ModelYamlRoundTrip, UserDataParsesFirmwareEscapedQuotesAndBackslashes)
{
const QByteArray yaml =
"header:\n"
" name: Tst\n"
"userData:\n"
" \"0\":\n"
" key: \"App|Key\"\n"
" type: STRING\n"
" value: \"a\\x22b\\x5Cc\"\n";
ModelData m;
m.clear();
ASSERT_TRUE(loadModelFromYaml(m, yaml));
EXPECT_EQ(m.userData[0].value, "a\"b\\c");
}
// Multiple entries of different types survive a round trip together.
TEST_F(ModelYamlRoundTrip, UserDataMixedTypesRoundTrip)
{
ModelData m;
m.clear();
m.used = true;
m.userData[0].key = "App|Str";
m.userData[0].type = 2; // STRING
m.userData[0].value = "hello";
m.userData[1].key = "App|Int";
m.userData[1].type = 0; // INT
m.userData[1].value = "12345";
m.userData[2].key = "App|Float";
m.userData[2].type = 1; // FLOAT
m.userData[2].value = "1.23456789";
QByteArray y;
ModelData m2 = roundTrip(m, y);
for (int i = 0; i < 3; i++) {
EXPECT_EQ(m2.userData[i].key, m.userData[i].key) << "entry " << i;
EXPECT_EQ(m2.userData[i].type, m.userData[i].type) << "entry " << i;
EXPECT_EQ(m2.userData[i].value, m.userData[i].value) << "entry " << i;
}
}