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:
co-authored by
Peter Feerick
Claude Sonnet 5
parent
abeed9f23b
commit
ed71a38726
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user