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;
}
}
+1
View File
@@ -91,6 +91,7 @@
#warning "Unknown board!"
#endif
#define MAX_USER_DATA 50
#define MAX_TIMERS 3
#define NUM_CAL_PPM 4
+126
View File
@@ -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> 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();
}
+45
View File
@@ -39,6 +39,10 @@
#include "quick_menu_def.h"
#endif
#if !defined(BACKUP)
#include <string>
#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<uint8_t>(cfsGroupOn, n, 1); }
void cfsSetGroupAlwaysOn(uint8_t n, bool v) { cfsGroupOn = bfSet<uint8_t>(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();
});
/*
+185
View File
@@ -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)
}
+1
View File
@@ -152,6 +152,7 @@ void applyDefaultTemplate()
LayoutFactory::deleteCustomScreens();
LayoutFactory::deleteTopBarWidgets();
#endif
g_model.clearUserData();
// enable switch warnings
bool sw1found = false;
+1
View File
@@ -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<ModelData*>(buffer);
#if defined(FLIGHT_MODES) && defined(GVARS)
// reset GVars to default values
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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<YamlTreeWalker*>(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<YamlTreeWalker*>(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<YamlTreeWalker*>(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<YamlTreeWalker*>(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<YamlTreeWalker*>(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<YamlTreeWalker*>(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<YamlTreeWalker*>(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);
}
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
@@ -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[] = {
+17 -5
View File
@@ -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;
}
+4 -4
View File
@@ -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--;
}
@@ -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);
+1
View File
@@ -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;
+57
View File
@@ -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)
+210
View File
@@ -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<std::string*>(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);
}
+7 -7
View File
@@ -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"