From ed71a38726c8691cc415091e333ea46209fdb0f2 Mon Sep 17 00:00:00 2001 From: philmoz Date: Fri, 18 Sep 2026 09:04:48 +1000 Subject: [PATCH] feat(firmware): add 'user data' section to model yaml for Lua scripts (#7562) Co-authored-by: Peter Feerick Co-authored-by: Claude Sonnet 5 --- companion/src/constants.h | 1 + .../src/firmwares/edgetx/yaml_modeldata.cpp | 24 ++ companion/src/firmwares/modeldata.cpp | 8 + companion/src/firmwares/modeldata.h | 12 + .../src/tests/model_yaml_roundtrip_test.cpp | 104 +++++++++ radio/src/dataconstants.h | 1 + radio/src/datastructs_model.cpp | 126 +++++++++++ radio/src/datastructs_private.h | 45 ++++ radio/src/lua/api_model.cpp | 185 +++++++++++++++ radio/src/model_init.cpp | 1 + radio/src/storage/sdcard_yaml.cpp | 1 + .../storage/yaml/yaml_datastructs_128x64.cpp | 8 + .../src/storage/yaml/yaml_datastructs_c14.cpp | 8 + .../src/storage/yaml/yaml_datastructs_f16.cpp | 8 + .../storage/yaml/yaml_datastructs_funcs.cpp | 85 +++++++ .../storage/yaml/yaml_datastructs_gx12.cpp | 8 + .../storage/yaml/yaml_datastructs_gx15.cpp | 8 + .../src/storage/yaml/yaml_datastructs_h17.cpp | 8 + .../storage/yaml/yaml_datastructs_nb4p.cpp | 8 + .../storage/yaml/yaml_datastructs_nv14.cpp | 8 + .../storage/yaml/yaml_datastructs_pa01.cpp | 8 + .../storage/yaml/yaml_datastructs_pl18.cpp | 8 + .../storage/yaml/yaml_datastructs_pl18u.cpp | 8 + .../storage/yaml/yaml_datastructs_st16.cpp | 8 + .../src/storage/yaml/yaml_datastructs_t15.cpp | 8 + .../storage/yaml/yaml_datastructs_t15pro.cpp | 8 + .../src/storage/yaml/yaml_datastructs_t20.cpp | 8 + .../src/storage/yaml/yaml_datastructs_t22.cpp | 8 + .../storage/yaml/yaml_datastructs_tpro.cpp | 8 + .../storage/yaml/yaml_datastructs_tx15.cpp | 8 + .../yaml/yaml_datastructs_tx16smk3.cpp | 8 + .../src/storage/yaml/yaml_datastructs_v12.cpp | 8 + .../src/storage/yaml/yaml_datastructs_x10.cpp | 8 + .../src/storage/yaml/yaml_datastructs_x9d.cpp | 8 + .../yaml/yaml_datastructs_x9dp2019.cpp | 8 + .../src/storage/yaml/yaml_datastructs_x9e.cpp | 8 + .../storage/yaml/yaml_datastructs_xlite.cpp | 8 + .../storage/yaml/yaml_datastructs_xlites.cpp | 8 + radio/src/storage/yaml/yaml_parser.cpp | 22 +- radio/src/storage/yaml/yaml_tree_walker.cpp | 8 +- radio/src/storage/yaml/yaml_tree_walker.h | 8 + radio/src/tests/gtests.h | 1 + radio/src/tests/lua.cpp | 57 +++++ radio/src/tests/userdata.cpp | 210 ++++++++++++++++++ tools/build-common.sh | 14 +- 45 files changed, 1105 insertions(+), 16 deletions(-) create mode 100644 radio/src/tests/userdata.cpp diff --git a/companion/src/constants.h b/companion/src/constants.h index eeff0ae0c..7ed18f77a 100644 --- a/companion/src/constants.h +++ b/companion/src/constants.h @@ -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 diff --git a/companion/src/firmwares/edgetx/yaml_modeldata.cpp b/companion/src/firmwares/edgetx/yaml_modeldata.cpp index 7208968e2..7dfba1458 100644 --- a/companion/src/firmwares/edgetx/yaml_modeldata.cpp +++ b/companion/src/firmwares/edgetx/yaml_modeldata.cpp @@ -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::encode(const ModelData& rhs) node["modelCustomScriptsDisabled"] = globalOnOffFilterLut << rhs.modelCustomScriptsDisabled; node["modelTelemetryDisabled"] = globalOnOffFilterLut << rhs.modelTelemetryDisabled; + for (int i=0; i::decode(const Node& node, ModelData& rhs) node["modelCustomScriptsDisabled"] >> globalOnOffFilterLut >> rhs.modelCustomScriptsDisabled; node["modelTelemetryDisabled"] >> globalOnOffFilterLut >> rhs.modelTelemetryDisabled; + if (node["userData"]) { + for (int i=0; i> 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")) diff --git a/companion/src/firmwares/modeldata.cpp b/companion/src/firmwares/modeldata.cpp index b2dd6721a..1c76eea97 100644 --- a/companion/src/firmwares/modeldata.cpp +++ b/companion/src/firmwares/modeldata.cpp @@ -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 diff --git a/companion/src/firmwares/modeldata.h b/companion/src/firmwares/modeldata.h index 9c3525ad0..e8f8eff14 100644 --- a/companion/src/firmwares/modeldata.h +++ b/companion/src/firmwares/modeldata.h @@ -126,6 +126,16 @@ class CustomSwitchData { void clear() { memset(reinterpret_cast(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); diff --git a/companion/src/tests/model_yaml_roundtrip_test.cpp b/companion/src/tests/model_yaml_roundtrip_test.cpp index 87234b8a5..8178a53e5 100644 --- a/companion/src/tests/model_yaml_roundtrip_test.cpp +++ b/companion/src/tests/model_yaml_roundtrip_test.cpp @@ -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; + } +} diff --git a/radio/src/dataconstants.h b/radio/src/dataconstants.h index 9afdba99b..df632949c 100644 --- a/radio/src/dataconstants.h +++ b/radio/src/dataconstants.h @@ -91,6 +91,7 @@ #warning "Unknown board!" #endif +#define MAX_USER_DATA 50 #define MAX_TIMERS 3 #define NUM_CAL_PPM 4 diff --git a/radio/src/datastructs_model.cpp b/radio/src/datastructs_model.cpp index 284227047..0753d98e2 100644 --- a/radio/src/datastructs_model.cpp +++ b/radio/src/datastructs_model.cpp @@ -260,3 +260,129 @@ void ModelData::resetScreenData() } } #endif + +// User Data store. Each entry is a key/value pair. +// Can be used by Lua scripts to save configuration data. +std::vector userData; + +// Check if userData[n] exists - controls YAML writing +bool ModelData::hasUserData(int n) +{ + return (size_t)n < userData.size() && !userData[n].key.empty(); +} + +// Get User Data item at position 'n' +// Used when parsing YAML files +UserData* ModelData::getUserData(int n) +{ + if ((size_t)n >= userData.size()) + return nullptr; + return &userData[n]; +} + +// Get User Data item with specified key +UserData* ModelData::getUserData(const char* key) +{ + if (key[0]) { + for (int i = 0; (size_t)i < userData.size(); i += 1) + if (userData[i].key == key) + return &userData[i]; + } + return nullptr; +} + +// Get User Data item at position 'n', resizing the store if needed +// (handles gaps in a sparse/hand-edited YAML index sequence). No +// storageDirty() here - this also runs on plain loads, not just mutations. +UserData* ModelData::getOrCreateUserData(int n) +{ + if ((size_t)n >= MAX_USER_DATA) return nullptr; + if ((size_t)n >= userData.size()) { + userData.resize(n + 1); + } + return &userData[n]; +} + +// val/val_len are used verbatim (not NUL-terminated semantics), so a +// string value may contain embedded NUL bytes. +static bool setUD(const char* key, const char* val, size_t val_len, UDType typ) +{ + if (key[0] == 0) return false; + + auto ud = g_model.getUserData(key); + if (ud == nullptr) { + // Reuse a gap slot (see getOrCreateUserData()) before growing the store. + for (auto& slot : userData) { + if (slot.key.empty()) { + slot.key = key; + slot.value.assign(val, val_len); + slot.type = typ; + storageDirty(EE_MODEL); + return true; + } + } + if (userData.size() >= MAX_USER_DATA) return false; + userData.emplace_back(); + userData.back().key = key; + userData.back().value.assign(val, val_len); + userData.back().type = typ; + storageDirty(EE_MODEL); + } else if (ud->type != typ || ud->value.compare(0, std::string::npos, val, val_len) != 0) { + ud->value.assign(val, val_len); + ud->type = typ; + storageDirty(EE_MODEL); + } + return true; +} + +// Update or add User Data item +bool ModelData::setUserData(const char* key, const char* str) +{ + return setUD(key, str, strlen(str), UD_STRING); +} + +// Update or add User Data item - str may contain embedded NUL bytes. +bool ModelData::setUserData(const char* key, const char* str, size_t len) +{ + return setUD(key, str, len, UD_STRING); +} + +// Update or add User Data item +bool ModelData::setUserData(const char* key, int32_t num) +{ + std::string s = std::to_string(num); + return setUD(key, s.c_str(), s.size(), UD_INT); +} + +// Update or add User Data item +bool ModelData::setUserData(const char* key, float num) +{ + // %.9g gives enough significant digits to round-trip a 32-bit float, + // unlike std::to_string() which fixes 6 decimal places and silently + // loses precision (e.g. very small or very large values). + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%.9g", (double)num); + return setUD(key, buf, len, UD_FLOAT); +} + +void ModelData::deleteUserData(const char* key) +{ + for (auto ud = userData.cbegin(); ud != userData.cend(); ud++) { + if (ud->key == key) { + userData.erase(ud); + storageDirty(EE_MODEL); + return; + } + } +} + +// Clear all User Data +void ModelData::clearUserData() +{ + userData.clear(); +} + +int ModelData::getUserDataCount() +{ + return userData.size(); +} diff --git a/radio/src/datastructs_private.h b/radio/src/datastructs_private.h index 85fce1e7e..2190779c9 100644 --- a/radio/src/datastructs_private.h +++ b/radio/src/datastructs_private.h @@ -39,6 +39,10 @@ #include "quick_menu_def.h" #endif +#if !defined(BACKUP) +#include +#endif + #if defined(PCBTARANIS) #define N_TARANIS_FIELD(x) #define TARANIS_FIELD(x) x; @@ -754,6 +758,28 @@ PACK(struct USBJoystickChData { #endif }); +// Key/Value data for Lua scripts +enum UDType { + UD_INT, + UD_FLOAT, + UD_STRING, +}; +struct UserData { +#if defined(YAML_GENERATOR) + CUST_ATTR(key, r_userdata_key, w_userdata_key); + CUST_ATTR(type, r_userdata_type, w_userdata_type); + CUST_ATTR(value, r_userdata_value, w_userdata_value); +#else +#if !defined(BACKUP) + std::string key; + UDType type = UD_STRING; + std::string value; + UserData() = default; + UserData(const char* k, const char* v, UDType t) { key = k; value = v; type = t; } +#endif +#endif +}; + PACK(struct ModelData { // Must match start of PartialModel CUST_ATTR(semver,nullptr,w_semver); @@ -943,6 +969,25 @@ PACK(struct ModelData { bool cfsGroupAlwaysOn(uint8_t n) { return bfGet(cfsGroupOn, n, 1); } void cfsSetGroupAlwaysOn(uint8_t n, bool v) { cfsGroupOn = bfSet(cfsGroupOn, v, n, 1); } #endif + +#if defined(YAML_GENERATOR) + NOBACKUP(UserData userData[MAX_USER_DATA]) FUNC(userdata_is_active); +#endif + bool hasUserData(int n); + UserData* getUserData(int n); + UserData* getUserData(const char* key); + // Used by the YAML reader: get (creating/resizing as needed) the entry + // at parsed array index n, in case of a sparse/non-contiguous index + // sequence (e.g. a hand-edited or Companion-generated model file). + UserData* getOrCreateUserData(int n); + bool setUserData(const char* key, const char* str); + // str may contain embedded NUL bytes. + bool setUserData(const char* key, const char* str, size_t len); + bool setUserData(const char* key, int32_t num); + bool setUserData(const char* key, float num); + void deleteUserData(const char* key); + void clearUserData(); + int getUserDataCount(); }); /* diff --git a/radio/src/lua/api_model.cpp b/radio/src/lua/api_model.cpp index 24085968c..b23414871 100644 --- a/radio/src/lua/api_model.cpp +++ b/radio/src/lua/api_model.cpp @@ -1963,6 +1963,187 @@ static int luaModelSetSwashRing(lua_State *L) } #endif // HELI +static std::string getUDKey(lua_State *L) +{ + // generate user data key from supplied 'app' and 'key' values + // UD key = "app|key" + + const char* a = luaL_checkstring(L, 1); + const char* k = luaL_checkstring(L, 2); + + // App name and key are mandatory, and app name cannot contain '|' + // (used internally as the app/key separator) + if (a[0] == 0 || k[0] == 0 || strchr(a, '|') != nullptr) { + return ""; + } + + std::string s(a); + s += "|"; + s += k; + + return s; +} + +/*luadoc +@function model.getUserData(app, key) + +Get User Data value for given app + key + +@param app (string) name of Lua app / widget / script. App name cannot contain the '|' character. + +@param key (string) name of User Data entry + +@retval nil no User Data value defined + +@retval User Data value (string or number) + +@status current Introduced in 3.0.0 +*/ +static int luaGetUserData(lua_State *L) +{ + std::string s = getUDKey(L); + + auto ud = g_model.getUserData(s.c_str()); + if (ud) { + if (ud->type == UD_INT) { + lua_pushinteger(L, strtol(ud->value.c_str(), nullptr, 10)); + } else if (ud->type == UD_FLOAT) { + lua_pushnumber(L, strtof(ud->value.c_str(), nullptr)); + } else { + lua_pushlstring(L, ud->value.data(), ud->value.size()); + } + } + else { + lua_pushnil(L); + } + return 1; +} + +/*luadoc +@function model.getAllUserData() + +Get a table of all User Data entries + +@param app (string) name of Lua app / widget / script. App name cannot contain the '|' character. + +@retval table of all User Data entries for the named app. If the app name is not supplied returns all entries. + +@status current Introduced in 3.0.0 +*/ +static int luaGetAllUserData(lua_State *L) +{ + // Increases stack by pushing an empty table so lua_gettop(L) checks below must check for >= 2 (not 1). + lua_newtable(L); + + if (g_model.getUserDataCount() == 0) return 1; + if (lua_gettop(L) >= 2 && lua_type(L, 1) != LUA_TSTRING) return 1; + + bool matchApp = false; + std::string s; + if (lua_gettop(L) >= 2) { + s = luaL_checkstring(L, 1); + // App name cannot contain '|' - no entry can match, since no app + // name stored via setUserData()/getUDKey() ever contains one either. + if (s.find('|') != std::string::npos) return 1; + s += "|"; + matchApp = true; + } + + for (int i = 0; i < g_model.getUserDataCount(); i += 1) { + auto ud = g_model.getUserData(i); + if (ud && !ud->key.empty()) { + std::string k = ud->key; + if (matchApp) { + if (k.rfind(s, 0) != 0) + continue; + k = k.substr(s.size(), k.size()-s.size()); + } + if (ud->type == UD_INT) { + lua_pushtableinteger(L, k.c_str(), strtol(ud->value.c_str(), nullptr, 10)); + } else if (ud->type == UD_FLOAT) { + lua_pushtablenumber(L, k.c_str(), strtof(ud->value.c_str(), nullptr)); + } else { + // lua_pushtablestring() would truncate an embedded NUL byte. + lua_pushstring(L, k.c_str()); + lua_pushlstring(L, ud->value.data(), ud->value.size()); + lua_settable(L, -3); + } + } + } + + return 1; +} + +/*luadoc +@function model.setUserData(app, key, value) + +Update User Data string for given app and key with new value. +A new User Data entry will be created if the app + key is not found. + +@param app (string) name of Lua app / widget / script. App name cannot contain the '|' character. +App and key names are stored as NUL-terminated C strings, so any embedded +'\0' character - and everything after it - will be silently truncated. +String values may contain embedded '\0' characters, except when the value +is both longer than 255 bytes and contains one - such a value is +truncated at the first '\0'. + +@param key (string) name of User Data entry + +@param value (string or number) value to save to User Data entry + +@retval boolean - true if user data was saved, false if save failed + +@status current Introduced in 3.0.0 +*/ +static int luaSetUserData(lua_State *L) +{ + if (lua_type(L, 3) != LUA_TSTRING && !lua_isnumber(L, 3)) { + // Fallback to trigger the standard Lua type error before allocating std::string + luaL_checkstring(L, 3); + return 0; + } + + std::string s = getUDKey(L); + + // App name and key are mandatory + if (s.empty()) { + lua_pushboolean(L, false); + return 1; + } + + if (lua_type(L, 3) == LUA_TSTRING) { + size_t len; + const char* v = lua_tolstring(L, 3, &len); + lua_pushboolean(L, g_model.setUserData(s.c_str(), v, len)); + } else if (lua_isinteger(L, 3)) { + int32_t n = luaL_checkinteger(L, 3); + lua_pushboolean(L, g_model.setUserData(s.c_str(), n)); + } else { + float f = luaL_checknumber(L, 3); + lua_pushboolean(L, g_model.setUserData(s.c_str(), f)); + } + return 1; +} + +/*luadoc +@function model.deleteUserData(app, key) + +Delete User Data entry for given app + key + +@param app (string) name of Lua app / widget / script. App name cannot contain the '|' character. + +@param key (string) name of User Data entry + +@status current Introduced in 3.0.0 +*/ +static int luaDeleteUserData(lua_State *L) +{ + std::string s = getUDKey(L); + if (!s.empty()) + g_model.deleteUserData(s.c_str()); + return 0; +} + extern "C" { LROT_BEGIN(modellib, NULL, 0) LROT_FUNCENTRY( getInfo, luaModelGetInfo ) @@ -2008,5 +2189,9 @@ LROT_BEGIN(modellib, NULL, 0) LROT_FUNCENTRY( getSwashRing, luaModelGetSwashRing ) LROT_FUNCENTRY( setSwashRing, luaModelSetSwashRing ) #endif + LROT_FUNCENTRY( getUserData, luaGetUserData ) + LROT_FUNCENTRY( getAllUserData, luaGetAllUserData ) + LROT_FUNCENTRY( setUserData, luaSetUserData ) + LROT_FUNCENTRY( deleteUserData, luaDeleteUserData ) LROT_END(modellib, NULL, 0) } diff --git a/radio/src/model_init.cpp b/radio/src/model_init.cpp index 68c85cc81..8a21b4b90 100644 --- a/radio/src/model_init.cpp +++ b/radio/src/model_init.cpp @@ -152,6 +152,7 @@ void applyDefaultTemplate() LayoutFactory::deleteCustomScreens(); LayoutFactory::deleteTopBarWidgets(); #endif + g_model.clearUserData(); // enable switch warnings bool sw1found = false; diff --git a/radio/src/storage/sdcard_yaml.cpp b/radio/src/storage/sdcard_yaml.cpp index d2b1837fd..e37f5508f 100644 --- a/radio/src/storage/sdcard_yaml.cpp +++ b/radio/src/storage/sdcard_yaml.cpp @@ -350,6 +350,7 @@ const char * readModelYaml(const char * filename, uint8_t * buffer, uint32_t siz #if defined(COLORLCD) g_model.resetScreenData(); #endif + g_model.clearUserData(); auto md = reinterpret_cast(buffer); #if defined(FLIGHT_MODES) && defined(GVARS) // reset GVars to default values diff --git a/radio/src/storage/yaml/yaml_datastructs_128x64.cpp b/radio/src/storage/yaml/yaml_datastructs_128x64.cpp index 60c9239a9..3963229f5 100644 --- a/radio/src/storage/yaml/yaml_datastructs_128x64.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_128x64.cpp @@ -810,6 +810,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 96, struct_ModelHeader, NULL), @@ -879,6 +886,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_c14.cpp b/radio/src/storage/yaml/yaml_datastructs_c14.cpp index 8f254d4b5..2123a4488 100644 --- a/radio/src/storage/yaml/yaml_datastructs_c14.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_c14.cpp @@ -904,6 +904,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -975,6 +982,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_f16.cpp b/radio/src/storage/yaml/yaml_datastructs_f16.cpp index a5cacdad2..72d1d9ffb 100644 --- a/radio/src/storage/yaml/yaml_datastructs_f16.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_f16.cpp @@ -905,6 +905,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -976,6 +983,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_funcs.cpp b/radio/src/storage/yaml/yaml_datastructs_funcs.cpp index 4862d99cc..ba41d8139 100644 --- a/radio/src/storage/yaml/yaml_datastructs_funcs.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_funcs.cpp @@ -2880,3 +2880,88 @@ bool isAlwaysActive(void* user, uint8_t* data, uint32_t bitoffs) { return true; } + +// Handle User Data save / load + +bool userdata_is_active(void* user, uint8_t* data, uint32_t bitoffs) +{ + auto tw = reinterpret_cast(user); + return g_model.hasUserData(tw->getElmts()); +} + +// val_len wraps for a true length > 255, but val is always NUL-terminated +// at its real end, so strlen(val) recovers the true length whenever it's +// >= val_len. Only a true length > 255 with an embedded NUL can still +// truncate - a pre-existing limit of the shared uint8_t val_len param. +static size_t userdata_val_len(const char* val, uint8_t val_len) +{ + size_t real_len = strlen(val); + return real_len > val_len ? real_len : val_len; +} + +void r_userdata_key(void* user, uint8_t* data, uint32_t bitoffs, + const char* val, uint8_t val_len) +{ + auto tw = reinterpret_cast(user); + auto ud = g_model.getOrCreateUserData(tw->getElmts(1)); + if (ud) ud->key.assign(val, userdata_val_len(val, val_len)); +} + +bool w_userdata_key(void* user, uint8_t* data, uint32_t bitoffs, + yaml_writer_func wf, void* opaque) +{ + auto tw = reinterpret_cast(user); + auto ud = g_model.getUserData(tw->getElmts(1)); + if (!ud) return false; + return yaml_output_string(ud->key.c_str(), ud->key.size(), wf, opaque, true); +} + +const struct YamlIdStr enum_UDType[] = { + { UD_INT, "INT" }, + { UD_FLOAT, "FLOAT" }, + { UD_STRING, "STRING" }, + { 0, NULL } +}; + +void r_userdata_type(void* user, uint8_t* data, uint32_t bitoffs, + const char* val, uint8_t val_len) +{ + auto tw = reinterpret_cast(user); + auto ud = g_model.getUserData(tw->getElmts(1)); + if (ud) { + ud->type = (UDType)yaml_parse_enum(enum_UDType, val, val_len); + } else { + TRACE("Bad model YAML - userData 'type' defined before 'key'"); + } +} + +bool w_userdata_type(void* user, uint8_t* data, uint32_t bitoffs, + yaml_writer_func wf, void* opaque) +{ + auto tw = reinterpret_cast(user); + auto ud = g_model.getUserData(tw->getElmts(1)); + if (!ud) return false; + const char* s = yaml_output_enum(ud->type, enum_UDType); + return wf(opaque, s, strlen(s)); +} + +void r_userdata_value(void* user, uint8_t* data, uint32_t bitoffs, + const char* val, uint8_t val_len) +{ + auto tw = reinterpret_cast(user); + auto ud = g_model.getUserData(tw->getElmts(1)); + if (ud) { + ud->value.assign(val, userdata_val_len(val, val_len)); + } else { + TRACE("Bad model YAML - userData 'value' defined before 'key'"); + } +} + +bool w_userdata_value(void* user, uint8_t* data, uint32_t bitoffs, + yaml_writer_func wf, void* opaque) +{ + auto tw = reinterpret_cast(user); + auto ud = g_model.getUserData(tw->getElmts(1)); + if (!ud) return false; + return yaml_output_string(ud->value.c_str(), ud->value.size(), wf, opaque, true); +} diff --git a/radio/src/storage/yaml/yaml_datastructs_gx12.cpp b/radio/src/storage/yaml/yaml_datastructs_gx12.cpp index 770e5385e..fb483b643 100644 --- a/radio/src/storage/yaml/yaml_datastructs_gx12.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_gx12.cpp @@ -848,6 +848,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 96, struct_ModelHeader, NULL), @@ -927,6 +934,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_gx15.cpp b/radio/src/storage/yaml/yaml_datastructs_gx15.cpp index d65ed4ff9..c5e507edd 100644 --- a/radio/src/storage/yaml/yaml_datastructs_gx15.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_gx15.cpp @@ -943,6 +943,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1024,6 +1031,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_h17.cpp b/radio/src/storage/yaml/yaml_datastructs_h17.cpp index 970891e27..f98fed4ef 100644 --- a/radio/src/storage/yaml/yaml_datastructs_h17.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_h17.cpp @@ -899,6 +899,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -970,6 +977,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_nb4p.cpp b/radio/src/storage/yaml/yaml_datastructs_nb4p.cpp index e2a9f71cc..994dc2b94 100644 --- a/radio/src/storage/yaml/yaml_datastructs_nb4p.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_nb4p.cpp @@ -895,6 +895,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -966,6 +973,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_nv14.cpp b/radio/src/storage/yaml/yaml_datastructs_nv14.cpp index 5898226bd..d0126b193 100644 --- a/radio/src/storage/yaml/yaml_datastructs_nv14.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_nv14.cpp @@ -902,6 +902,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -974,6 +981,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_pa01.cpp b/radio/src/storage/yaml/yaml_datastructs_pa01.cpp index 24448d1a6..837d83313 100644 --- a/radio/src/storage/yaml/yaml_datastructs_pa01.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_pa01.cpp @@ -942,6 +942,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1023,6 +1030,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_pl18.cpp b/radio/src/storage/yaml/yaml_datastructs_pl18.cpp index eb78350b4..1e321cfd4 100644 --- a/radio/src/storage/yaml/yaml_datastructs_pl18.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_pl18.cpp @@ -902,6 +902,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -974,6 +981,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_pl18u.cpp b/radio/src/storage/yaml/yaml_datastructs_pl18u.cpp index 21caf352b..b8aafa1f4 100644 --- a/radio/src/storage/yaml/yaml_datastructs_pl18u.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_pl18u.cpp @@ -895,6 +895,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -966,6 +973,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_st16.cpp b/radio/src/storage/yaml/yaml_datastructs_st16.cpp index ee4452565..f4434c022 100644 --- a/radio/src/storage/yaml/yaml_datastructs_st16.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_st16.cpp @@ -942,6 +942,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1023,6 +1030,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_t15.cpp b/radio/src/storage/yaml/yaml_datastructs_t15.cpp index 0d28102e7..22f29517a 100644 --- a/radio/src/storage/yaml/yaml_datastructs_t15.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_t15.cpp @@ -922,6 +922,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1001,6 +1008,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_t15pro.cpp b/radio/src/storage/yaml/yaml_datastructs_t15pro.cpp index 39846e84b..2ae138415 100644 --- a/radio/src/storage/yaml/yaml_datastructs_t15pro.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_t15pro.cpp @@ -942,6 +942,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1023,6 +1030,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_t20.cpp b/radio/src/storage/yaml/yaml_datastructs_t20.cpp index b98db8de5..673957338 100644 --- a/radio/src/storage/yaml/yaml_datastructs_t20.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_t20.cpp @@ -828,6 +828,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 96, struct_ModelHeader, NULL), @@ -905,6 +912,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_t22.cpp b/radio/src/storage/yaml/yaml_datastructs_t22.cpp index 39846e84b..2ae138415 100644 --- a/radio/src/storage/yaml/yaml_datastructs_t22.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_t22.cpp @@ -942,6 +942,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1023,6 +1030,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_tpro.cpp b/radio/src/storage/yaml/yaml_datastructs_tpro.cpp index 96d9ec647..8cd9015c3 100644 --- a/radio/src/storage/yaml/yaml_datastructs_tpro.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_tpro.cpp @@ -828,6 +828,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 96, struct_ModelHeader, NULL), @@ -905,6 +912,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_tx15.cpp b/radio/src/storage/yaml/yaml_datastructs_tx15.cpp index d65ed4ff9..c5e507edd 100644 --- a/radio/src/storage/yaml/yaml_datastructs_tx15.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_tx15.cpp @@ -943,6 +943,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1024,6 +1031,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp b/radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp index e972cd504..636c8ebb6 100644 --- a/radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp @@ -944,6 +944,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1025,6 +1032,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_v12.cpp b/radio/src/storage/yaml/yaml_datastructs_v12.cpp index 506ade98c..b4313e3b6 100644 --- a/radio/src/storage/yaml/yaml_datastructs_v12.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_v12.cpp @@ -937,6 +937,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -1018,6 +1025,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_x10.cpp b/radio/src/storage/yaml/yaml_datastructs_x10.cpp index d1ba656a6..2b1c17446 100644 --- a/radio/src/storage/yaml/yaml_datastructs_x10.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_x10.cpp @@ -904,6 +904,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), @@ -975,6 +982,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_x9d.cpp b/radio/src/storage/yaml/yaml_datastructs_x9d.cpp index 6fa673905..fe91c2c6b 100644 --- a/radio/src/storage/yaml/yaml_datastructs_x9d.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_x9d.cpp @@ -810,6 +810,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 192, struct_ModelHeader, NULL), @@ -881,6 +888,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp b/radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp index 6fa673905..fe91c2c6b 100644 --- a/radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp @@ -810,6 +810,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 192, struct_ModelHeader, NULL), @@ -881,6 +888,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_x9e.cpp b/radio/src/storage/yaml/yaml_datastructs_x9e.cpp index b29b5c3a7..49b8a31a7 100644 --- a/radio/src/storage/yaml/yaml_datastructs_x9e.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_x9e.cpp @@ -810,6 +810,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 192, struct_ModelHeader, NULL), @@ -882,6 +889,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_xlite.cpp b/radio/src/storage/yaml/yaml_datastructs_xlite.cpp index 60c9239a9..3963229f5 100644 --- a/radio/src/storage/yaml/yaml_datastructs_xlite.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_xlite.cpp @@ -810,6 +810,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 96, struct_ModelHeader, NULL), @@ -879,6 +886,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_datastructs_xlites.cpp b/radio/src/storage/yaml/yaml_datastructs_xlites.cpp index 4e95395f3..d684deb60 100644 --- a/radio/src/storage/yaml/yaml_datastructs_xlites.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_xlites.cpp @@ -815,6 +815,13 @@ static const struct YamlNode struct_USBJoystickChData[] = { YAML_UNSIGNED( "switch_npos", 3 ), YAML_END }; +static const struct YamlNode struct_UserData[] = { + YAML_IDX, + YAML_CUSTOM("key",r_userdata_key,w_userdata_key), + YAML_CUSTOM("type",r_userdata_type,w_userdata_type), + YAML_CUSTOM("value",r_userdata_value,w_userdata_value), + YAML_END +}; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), YAML_STRUCT("header", 96, struct_ModelHeader, NULL), @@ -884,6 +891,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_ENUM("modelSFDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelCustomScriptsDisabled", 2, enum_ModelOverridableEnable, NULL), YAML_ENUM("modelTelemetryDisabled", 2, enum_ModelOverridableEnable, NULL), + YAML_ARRAY("userData", 0, 50, struct_UserData, userdata_is_active), YAML_END }; static const struct YamlNode struct_PartialModel[] = { diff --git a/radio/src/storage/yaml/yaml_parser.cpp b/radio/src/storage/yaml/yaml_parser.cpp index c7ea842e9..567a6a454 100644 --- a/radio/src/storage/yaml/yaml_parser.cpp +++ b/radio/src/storage/yaml/yaml_parser.cpp @@ -222,6 +222,12 @@ YamlParser::parse(const char* buffer, unsigned int size) break; case ps_ValEsc1: + // Match encodings generated by Companion YAML parser + if (*c == 'b') { scratch_buf += '\b'; state = ps_ValQuo; break; } + if (*c == 't') { scratch_buf += '\t'; state = ps_ValQuo; break; } + if (*c == 'n') { scratch_buf += '\n'; state = ps_ValQuo; break; } + if (*c == 'f') { scratch_buf += '\f'; state = ps_ValQuo; break; } + if (*c == 'r') { scratch_buf += '\r'; state = ps_ValQuo; break; } if (*c == 'x') { state = ps_ValEsc2; break; @@ -236,10 +242,13 @@ YamlParser::parse(const char* buffer, unsigned int size) state = ps_ValEsc3; break; } - else if (*c >= 'A' && *c <= 'F') { - escHexVal = (*c - 'A' + 10) << 4; + else { + char ch = toupper((uint8_t)*c); + if (ch >= 'A' && ch <= 'F') { + escHexVal = (ch - 'A' + 10) << 4; state = ps_ValEsc3; break; + } } TRACE_YAML("wrong hex digit '%c'",*c); return DONE_PARSING; @@ -251,11 +260,14 @@ YamlParser::parse(const char* buffer, unsigned int size) state = ps_ValQuo; break; } - else if (*c >= 'A' && *c <= 'F') { - escHexVal |= (*c - 'A' + 10); + else { + char ch = toupper((uint8_t)*c); + if (ch >= 'A' && ch <= 'F') { + escHexVal |= (ch - 'A' + 10); scratch_buf += escHexVal; state = ps_ValQuo; break; + } } TRACE_YAML("wrong hex digit '%c'",*c); return DONE_PARSING; @@ -298,7 +310,7 @@ YamlParser::parse(const char* buffer, unsigned int size) if ((state == ps_Val) && eof && node_found) { calls->set_attr(ctx, scratch_buf.c_str(), scratch_buf.size()); } - + return CONTINUE_PARSING; } diff --git a/radio/src/storage/yaml/yaml_tree_walker.cpp b/radio/src/storage/yaml/yaml_tree_walker.cpp index 7b468dd76..0fb49c6c4 100644 --- a/radio/src/storage/yaml/yaml_tree_walker.cpp +++ b/radio/src/storage/yaml/yaml_tree_walker.cpp @@ -112,14 +112,14 @@ static const char hex_digits[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; -static bool yaml_output_string(const char* str, uint32_t max_len, - yaml_writer_func wf, void* opaque) +bool yaml_output_string(const char* str, uint32_t max_len, + yaml_writer_func wf, void* opaque, bool exact) { if (!wf(opaque, "\"", 1)) return false; - while(max_len > 0 && *str) { - if (*str >= 0x20 && *str <= 0x7E && *str != '"') { + while(max_len > 0 && (exact || *str)) { + if (*str >= 0x20 && *str <= 0x7E && *str != '"' && *str != '\\') { if (!wf(opaque, str++, 1)) return false; max_len--; } diff --git a/radio/src/storage/yaml/yaml_tree_walker.h b/radio/src/storage/yaml/yaml_tree_walker.h index 1c18db498..e8b0f3260 100644 --- a/radio/src/storage/yaml/yaml_tree_walker.h +++ b/radio/src/storage/yaml/yaml_tree_walker.h @@ -167,3 +167,11 @@ public: // utils uint32_t yaml_parse_enum(const struct YamlIdStr* choices, const char* val, uint8_t val_len); const char* yaml_output_enum(int32_t i, const struct YamlIdStr* choices); +// exact=false (default) stops at the first NUL, matching the semantics +// needed for a fixed-capacity char[] field whose content is shorter than +// its declared max_len - the field is not necessarily NUL-padded so +// anything past the terminator has no meaning. exact=true writes the full +// max_len regardless of embedded NULs - use it when max_len already IS +// the true content length (e.g. a std::string's size()). +bool yaml_output_string(const char* str, uint32_t max_len, yaml_writer_func wf, + void* opaque, bool exact = false); diff --git a/radio/src/tests/gtests.h b/radio/src/tests/gtests.h index dd116c223..f8cc06bcb 100644 --- a/radio/src/tests/gtests.h +++ b/radio/src/tests/gtests.h @@ -82,6 +82,7 @@ inline void SYSTEM_RESET() inline void MODEL_RESET() { memset(&g_model, 0, sizeof(g_model)); + g_model.clearUserData(); anaResetFiltered(); extern uint8_t s_mixer_first_run_done; s_mixer_first_run_done = false; diff --git a/radio/src/tests/lua.cpp b/radio/src/tests/lua.cpp index a90408168..c15c6b923 100644 --- a/radio/src/tests/lua.cpp +++ b/radio/src/tests/lua.cpp @@ -283,4 +283,61 @@ TEST(Lua, ioSeek) std::filesystem::remove(simuFatfsGetRealPath("seek-test.txt")); } +// Adapted from the sample script in PR #7562 (minus the LVGL UI part): +// populates the same app/key/value set and exercises the full Lua API +// through the real binding layer, not just the underlying C++ store. +TEST(Lua, testUserData) +{ + MODEL_RESET(); + + const char userdata_tst[] = + "model.setUserData('App', 'K1', 'String')\n" + "model.setUserData('App', 'K2', 12345)\n" + "model.setUserData('App', 'K3', 12.345)\n" + + "assert(model.getUserData('App', 'K1') == 'String')\n" + "assert(model.getUserData('App', 'K2') == 12345)\n" + "assert(math.abs(model.getUserData('App', 'K3') - 12.345) < 0.001)\n" + "assert(model.getUserData('Other', 'K1') == nil)\n" + + "local ud = model.getAllUserData('App')\n" + "local n = 0\n" + "for k in pairs(ud) do n = n + 1 end\n" + "assert(n == 3)\n" + "assert(ud.K1 == 'String')\n" + + "local all = model.getAllUserData()\n" + "assert(all['App|K1'] == 'String')\n" + + "model.deleteUserData('App', 'K1')\n" + "assert(model.getUserData('App', 'K1') == nil)\n" + "assert(model.getUserData('App', 'K2') == 12345)\n"; + + luaExecStr(userdata_tst); +} + +// string.char() builds the value at runtime, sidestepping the fact that +// luaL_loadstring() (used to load this test's own Lua source) is itself +// strlen()-based and couldn't carry a literal embedded NUL through. +TEST(Lua, testUserDataEmbeddedNul) +{ + MODEL_RESET(); + + const char userdata_nul_tst[] = + "local v = string.char(65, 0, 66)\n" + "assert(#v == 3)\n" + "model.setUserData('App', 'NulKey', v)\n" + + "local got = model.getUserData('App', 'NulKey')\n" + "assert(#got == 3)\n" + "assert(got == v)\n" + "assert(string.byte(got, 2) == 0)\n" + + "local all = model.getAllUserData('App')\n" + "assert(#all.NulKey == 3)\n" + "assert(all.NulKey == v)\n"; + + luaExecStr(userdata_nul_tst); +} + #endif // #if defined(LUA) diff --git a/radio/src/tests/userdata.cpp b/radio/src/tests/userdata.cpp new file mode 100644 index 000000000..abde4f3f7 --- /dev/null +++ b/radio/src/tests/userdata.cpp @@ -0,0 +1,210 @@ +/* + * 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 "storage/yaml/yaml_tree_walker.h" +#include "storage/yaml/yaml_parser.h" +#include "storage/yaml/yaml_datastructs.h" + +class UserDataTest : public EdgeTxTest {}; + +TEST_F(UserDataTest, SetGetString) +{ + EXPECT_TRUE(g_model.setUserData("key1", "hello")); + auto ud = g_model.getUserData("key1"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->type, UD_STRING); + EXPECT_EQ(ud->value, "hello"); +} + +TEST_F(UserDataTest, SetGetInt) +{ + EXPECT_TRUE(g_model.setUserData("key1", (int32_t)12345)); + auto ud = g_model.getUserData("key1"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->type, UD_INT); + EXPECT_EQ(strtol(ud->value.c_str(), nullptr, 10), 12345); +} + +TEST_F(UserDataTest, FloatRoundTrip) +{ + // std::to_string() fixes 6 decimal places and would truncate this to + // "0.000000" - %.9g must preserve enough precision to round-trip. + float f = 1.234567e-5f; + EXPECT_TRUE(g_model.setUserData("key1", f)); + auto ud = g_model.getUserData("key1"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->type, UD_FLOAT); + EXPECT_NEAR(strtof(ud->value.c_str(), nullptr), f, f * 1e-5f); +} + +TEST_F(UserDataTest, UpdateExistingChangesType) +{ + // A value/type change on an existing key must persist even when the + // string representation happens to be identical either side. + EXPECT_TRUE(g_model.setUserData("key1", (int32_t)123)); + EXPECT_TRUE(g_model.setUserData("key1", "123")); + auto ud = g_model.getUserData("key1"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->type, UD_STRING); +} + +TEST_F(UserDataTest, DeleteRemovesEntry) +{ + g_model.setUserData("key1", "a"); + g_model.deleteUserData("key1"); + EXPECT_EQ(g_model.getUserData("key1"), nullptr); + EXPECT_EQ(g_model.getUserDataCount(), 0); +} + +TEST_F(UserDataTest, MaxEntriesRespected) +{ + char key[16]; + for (int i = 0; i < MAX_USER_DATA; i += 1) { + snprintf(key, sizeof(key), "k%d", i); + EXPECT_TRUE(g_model.setUserData(key, i)); + } + EXPECT_FALSE(g_model.setUserData("overflow", 0)); + EXPECT_EQ(g_model.getUserDataCount(), MAX_USER_DATA); +} + +TEST_F(UserDataTest, GetOrCreateSparseIndex) +{ + // Simulates the YAML reader encountering a non-contiguous index + // (e.g. a hand-edited or Companion-generated model file): the entry + // must land at the requested index, not be appended elsewhere. + auto ud = g_model.getOrCreateUserData(3); + ASSERT_NE(ud, nullptr); + ud->key = "k3"; + EXPECT_EQ(g_model.getUserDataCount(), 4); + EXPECT_EQ(g_model.getUserData(3)->key, "k3"); + // Gap slots exist but are inactive (empty key) + EXPECT_FALSE(g_model.hasUserData(0)); + EXPECT_TRUE(g_model.hasUserData(3)); +} + +TEST_F(UserDataTest, GetOrCreateRespectsMax) +{ + EXPECT_EQ(g_model.getOrCreateUserData(MAX_USER_DATA), nullptr); +} + +TEST_F(UserDataTest, SetUserDataReusesGapSlot) +{ + // A sparse YAML load can leave the store full of inactive gap slots + // (empty key) well before MAX_USER_DATA active entries exist; setUserData + // must reuse a gap rather than spuriously refusing the new entry. + auto ud = g_model.getOrCreateUserData(MAX_USER_DATA - 1); + ASSERT_NE(ud, nullptr); + ud->key = "last"; + EXPECT_EQ(g_model.getUserDataCount(), MAX_USER_DATA); + + EXPECT_TRUE(g_model.setUserData("newkey", "value")); + EXPECT_EQ(g_model.getUserDataCount(), MAX_USER_DATA); + auto found = g_model.getUserData("newkey"); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->value, "value"); +} + +TEST_F(UserDataTest, SetGetEmbeddedNulString) +{ + const char raw[] = {'a', '\0', 'b', 'c'}; + EXPECT_TRUE(g_model.setUserData("key1", raw, sizeof(raw))); + auto ud = g_model.getUserData("key1"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->value.size(), sizeof(raw)); + EXPECT_EQ(memcmp(ud->value.data(), raw, sizeof(raw)), 0); +} + +static bool yaml_string_writer(void* opaque, const char* str, size_t len) +{ + static_cast(opaque)->append(str, len); + return true; +} + +TEST_F(UserDataTest, YamlRoundTripPreservesEmbeddedNul) +{ + const char raw[] = {'e', 'm', 'b', '\0', 'X'}; + ASSERT_TRUE(g_model.setUserData("nulkey", raw, sizeof(raw))); + + // Pin the no-regression case alongside it: a value over 255 bytes with + // no embedded NUL must still round-trip in full. + std::string longVal(300, 'Y'); + ASSERT_TRUE(g_model.setUserData("longkey", longVal.c_str(), longVal.size())); + + std::string yaml; + { + YamlTreeWalker tree; + tree.reset(get_modeldata_nodes(), (uint8_t*)&g_model); + ASSERT_TRUE(tree.generate(yaml_string_writer, &yaml)); + } + + g_model.clearUserData(); + ASSERT_EQ(g_model.getUserDataCount(), 0); + + { + YamlTreeWalker tree; + tree.reset(get_modeldata_nodes(), (uint8_t*)&g_model); + YamlParser yp; + yp.init(YamlTreeWalker::get_parser_calls(), &tree); + yp.parse(yaml.c_str(), yaml.size()); + } + + auto ud = g_model.getUserData("nulkey"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->value.size(), sizeof(raw)); + EXPECT_EQ(memcmp(ud->value.data(), raw, sizeof(raw)), 0); + + auto udLong = g_model.getUserData("longkey"); + ASSERT_NE(udLong, nullptr); + EXPECT_EQ(udLong->value, longVal); +} + +// Characterization test: 255 bytes is the largest value for which the +// embedded-NUL fix is guaranteed exact (see userdata_val_len() in +// yaml_datastructs_funcs.cpp) - pin the boundary explicitly. +TEST_F(UserDataTest, YamlRoundTripPreserves255ByteEmbeddedNul) +{ + std::string raw(255, 'Z'); + raw[100] = '\0'; + ASSERT_TRUE(g_model.setUserData("boundary", raw.data(), raw.size())); + + std::string yaml; + { + YamlTreeWalker tree; + tree.reset(get_modeldata_nodes(), (uint8_t*)&g_model); + ASSERT_TRUE(tree.generate(yaml_string_writer, &yaml)); + } + + g_model.clearUserData(); + + { + YamlTreeWalker tree; + tree.reset(get_modeldata_nodes(), (uint8_t*)&g_model); + YamlParser yp; + yp.init(YamlTreeWalker::get_parser_calls(), &tree); + yp.parse(yaml.c_str(), yaml.size()); + } + + auto ud = g_model.getUserData("boundary"); + ASSERT_NE(ud, nullptr); + EXPECT_EQ(ud->value, raw); +} diff --git a/tools/build-common.sh b/tools/build-common.sh index d82793522..7dbe0d2d6 100644 --- a/tools/build-common.sh +++ b/tools/build-common.sh @@ -12,7 +12,7 @@ get_target_build_options() { BUILD_OPTIONS+="-DPCB=X7" ;; x7access) - BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=ACCESS -DPXX1=YES" + BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=ACCESS -DPXX1=YES -DUSE_FW_LTO=y" ;; t12) BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T12 -DINTERNAL_MODULE_MULTI=ON" @@ -21,7 +21,7 @@ get_target_build_options() { BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TX12" ;; tx12mk2) - BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TX12MK2" + BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TX12MK2 -DUSE_FW_LTO=y" ;; gx12) BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=GX12" @@ -33,10 +33,10 @@ get_target_build_options() { BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=T8" ;; zorro) - BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=ZORRO" + BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=ZORRO -DUSE_FW_LTO=y" ;; pocket) - BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=POCKET" + BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=POCKET -DUSE_FW_LTO=y" ;; mt12) BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=MT12" @@ -48,7 +48,7 @@ get_target_build_options() { BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPRO" ;; tprov2) - BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPROV2" + BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPROV2 -DUSE_FW_LTO=y" ;; tpros) BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=TPROS" @@ -72,7 +72,7 @@ get_target_build_options() { BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=LR3PRO" ;; commando8) - BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=COMMANDO8" + BUILD_OPTIONS+="-DPCB=X7 -DPCBREV=COMMANDO8 -DUSE_FW_LTO=y" ;; xlite) BUILD_OPTIONS+="-DPCB=XLITE" @@ -90,7 +90,7 @@ get_target_build_options() { BUILD_OPTIONS+="-DPCB=X9D+ -DPCBREV=2019 -DUSE_FW_LTO=y" ;; x9e) - BUILD_OPTIONS+="-DPCB=X9E" + BUILD_OPTIONS+="-DPCB=X9E -DUSE_FW_LTO=y" ;; x10) BUILD_OPTIONS+="-DPCB=X10"