Skip to content

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

Open
philmoz wants to merge 7 commits into
mainfrom
philmoz/model-user-data
Open

feat(firmware): add 'user data' section to model yaml for Lua scripts#7562
philmoz wants to merge 7 commits into
mainfrom
philmoz/model-user-data

Conversation

@philmoz

@philmoz philmoz commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #5760

Add a 'userData' section to the model YAML file to store key/value data entries.
Can be used by any Lua script to save miscellaneous data, or share data between scripts.

Notes:

  • Entries are indexed by a key string.
  • Values can be integer, float or string.
  • String values are encoded when written to YAML so can contain any ASCII character.
  • String values can be any length; however they will use RAM so take care especially on B&W radios.
  • Due to the requirements of the YAML code there is a limit of 50 entries per model
  • Minimal RAM increase if not used (<20 bytes).
  • FLASH size increase < 2K

Lua API:
Scripts must supply both an app name and key for each entry. These are combined internally to build the user data key. The app name cannot contain the '|' character.

  • model.getUserData(app, key) - returns the value for the given app & key, or nil if the entry is not found
  • model.getAllUserData(app) - returns a table of all saved user data entries for the given app
  • model.getAllUserData() - returns a table of all saved user data entries
  • model.setUserData(app, key, value) - adds or updates an entry, returns true if it succeeded or false if failed
  • model.deleteUserData(app, key) - deletes the entry for the given key

Sample script (stand alone tool):

-- TNS|Show User Data|TNE

local ud = {}
local ui = {}

function ui.close()
  lvgl.confirm({title="Exit", message="Really exit?",
    confirm=(function() exitTool = true end)
  })
end

function ui.build()
  lvgl.clear()

  local w = lvgl.PERCENT_SIZE + 90

  local pg = lvgl.page({title="User Data", back=close, backButton=true, menu=ui.close})

  local layout = pg:box({w=LCD_W, h=PAGE_BODY_HEIGHT, flexFlow=lvgl.FLOW_COLUMN, flexPad=lvgl.PAD_MEDIUM})

  for k, v in pairs(ud) do
    layout:label({text=k.." - "..v, w=w})
  end
end

local function init()
  -- add some dumy data
  model.setUserData("App", "K1", 'String')
  model.setUserData("App", "K2", 12345)
  model.setUserData("App", "K3", 12.345)

  ud = model.getAllUserData()

  ui.build()
end

local function run(event, touchState)
  if lvgl == nil then
    lcd.drawText(0, 0, "LVGL (EdgeTX 2.11+) required", COLOR_THEME_WARNING)
    return 0
  end

  if (exitTool) then return 2 end

  return 0
end

return {init = init, run = run, useLvgl=true}

TODO:

  • Companion read & write of user data in model file

Summary by CodeRabbit

Summary of changes

  • New Features

    • Added per-model User Data storage (string, integer, float), capped at 50 entries.
    • Added Lua APIs to get by key, list all entries, set values, and delete entries.
    • Included User Data in model save/load.
  • Bug Fixes

    • User Data is now cleared when applying the default template and when initializing models from YAML.
  • Enhancements

    • Improved YAML quoting/escape handling (control-character escapes, case-insensitive hex parsing, safer backslash output).
    • Improved label string escaping for CSV/YAML-safe transformations.

@philmoz philmoz added this to the 3.0 milestone Jul 15, 2026
@philmoz philmoz added enhancement ✨ New feature or request lua-api Lua API related labels Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds bounded typed model user-data storage with Lua APIs, Companion model copying and YAML conversion, YAML serialization across supported targets, parser escape handling, and a shared string replacement utility.

Changes

Model User Data

Layer / File(s) Summary
User-data storage contract and lifecycle
radio/src/dataconstants.h, radio/src/datastructs_*, radio/src/model_init.cpp, radio/src/storage/sdcard_yaml.cpp, companion/src/constants.h, companion/src/firmwares/modeldata.*
Defines typed user-data records with 50-entry limits, implements lookup and mutation operations, synchronizes Companion copy/clear behavior, and clears entries during model initialization.
Lua user-data API
radio/src/lua/api_model.cpp
Adds Lua functions to retrieve, enumerate, set, and delete typed user-data entries, then registers them in modellib.
YAML serialization and target schemas
radio/src/storage/yaml/*, companion/src/firmwares/edgetx/yaml_modeldata.cpp
Adds user-data YAML handlers and schema arrays across supported targets, extends Companion YAML conversion, exports string output support, and accepts control-character and lowercase hexadecimal escapes.
Shared string replacement utility
radio/src/strhelpers.*, radio/src/storage/modelslist.*, radio/src/gui/colorlcd/setup_menus/quick_menu.cpp
Adds strReplaceAll and replaces local string-replacement implementations in model-list and quick-menu code.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LuaScript
  participant modellib
  participant ModelData
  participant ModelYAML
  LuaScript->>modellib: setUserData(app, key, value)
  modellib->>ModelData: set typed user data
  ModelData-->>modellib: success boolean
  ModelData->>ModelYAML: persist dirty model data
  LuaScript->>modellib: getUserData(app, key)
  modellib->>ModelData: retrieve UserData
  ModelData-->>LuaScript: integer, number, or string
Loading

Suggested labels: companion

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements model YAML key/value storage and app-scoped access, but it does not provide the issue's appName-only delete API. Add an app-scoped delete API or equivalent bulk removal and align the Lua API names and behavior with #5760's appProperty* contract.
Out of Scope Changes check ⚠️ Warning Several unrelated changes were added, including YAML parser escape handling, string helpers, quick menu updates, and model list refactors. Remove or justify the unrelated parser, menu, and string-helper changes so the PR stays focused on user-data support.
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding user data support for Lua scripts in model YAML.
Description check ✅ Passed The description covers the fix, summary, Lua APIs, notes, and example usage; it is mostly complete despite not matching the template headings exactly.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch philmoz/model-user-data

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/datastructs_model.cpp`:
- Around line 266-272: Replace the file-scoped userData storage with
per-ModelData ownership, such as a mapping keyed by ModelData instances, without
changing the ModelData binary layout or memset behavior. Update
ModelData::hasUserData and all YAML/user-data helpers, including setUD, to read
and write the invoking instance’s storage rather than g_model. Ensure
per-instance storage is initialized and cleaned up with the corresponding
ModelData lifecycle.
- Around line 299-303: Update the existing user-data branch around ud->str and
ud->type so it also applies changes when typ differs, even if the string value
is unchanged. Preserve the assignments and storageDirty(EE_MODEL) behavior for
either a value or type change, including conversions such as UD_INT to
UD_STRING.

In `@radio/src/lua/api_model.cpp`:
- Around line 2038-2049: Update the user-data type dispatch around
g_model.setUserData so LUA_TSTRING values are detected explicitly before the
lua_isnumber check. Preserve numeric handling for actual integer and number
values, while ensuring numeric-looking Lua strings remain stored through the
string overload.
- Around line 1979-1985: Replace the throwing std::stoi and std::stof
conversions in luaGetUserData at radio/src/lua/api_model.cpp:1979-1985 and
luaGetAllUserData at radio/src/lua/api_model.cpp:2009-2015 with the non-throwing
strtol and strtof alternatives, preserving the existing Lua integer and number
results for valid values.

In `@radio/src/storage/yaml/yaml_datastructs_funcs.cpp`:
- Around line 2892-2909: Update r_userdata_key to construct the key from the
bounded buffer using val_len, including when initializing user data, instead of
treating val as null-terminated. Update w_userdata_key to emit ud->key through
yaml_output_string so special characters are YAML-escaped before writing.
- Around line 2918-2949: The userdata handlers must tolerate missing entries and
preserve the parser’s explicit value length. In r_userdata_type,
r_userdata_value, w_userdata_type, and w_userdata_value, check the result of
g_model.getUserData(tw->getElmts(1)) before dereferencing; return safely or emit
the appropriate empty/default output when it is null, consistent with
surrounding handlers. Update r_userdata_value to assign exactly val_len bytes
from val rather than treating it as NUL-terminated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9c97902-e811-4564-ae66-16a1ed05bf96

📥 Commits

Reviewing files that changed from the base of the PR and between c966bee and 5872b42.

📒 Files selected for processing (31)
  • radio/src/dataconstants.h
  • radio/src/datastructs_model.cpp
  • radio/src/datastructs_private.h
  • radio/src/lua/api_model.cpp
  • radio/src/model_init.cpp
  • radio/src/storage/sdcard_yaml.cpp
  • radio/src/storage/yaml/yaml_datastructs_128x64.cpp
  • radio/src/storage/yaml/yaml_datastructs_f16.cpp
  • radio/src/storage/yaml/yaml_datastructs_funcs.cpp
  • radio/src/storage/yaml/yaml_datastructs_gx12.cpp
  • radio/src/storage/yaml/yaml_datastructs_nb4p.cpp
  • radio/src/storage/yaml/yaml_datastructs_nv14.cpp
  • radio/src/storage/yaml/yaml_datastructs_pa01.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18u.cpp
  • radio/src/storage/yaml/yaml_datastructs_st16.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15pro.cpp
  • radio/src/storage/yaml/yaml_datastructs_t20.cpp
  • radio/src/storage/yaml/yaml_datastructs_t22.cpp
  • radio/src/storage/yaml/yaml_datastructs_tpro.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx15.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp
  • radio/src/storage/yaml/yaml_datastructs_x10.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9d.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9e.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlite.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlites.cpp
  • radio/src/storage/yaml/yaml_tree_walker.cpp
  • radio/src/storage/yaml/yaml_tree_walker.h

Comment thread radio/src/datastructs_model.cpp
Comment thread radio/src/datastructs_model.cpp Outdated
Comment thread radio/src/lua/api_model.cpp
Comment thread radio/src/lua/api_model.cpp Outdated
Comment on lines +2892 to +2909
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.getUserData(tw->getElmts(1));
if (ud == nullptr)
g_model.setUserData(val, "");
else
ud->key = val;
}

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));
return wf(opaque, ud->key.c_str(), ud->key.size());
}

@coderabbitai coderabbitai Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Prevent buffer over-read and fix YAML escaping for keys.

  • The buffer val may not be null-terminated; construct the string using std::string(val, val_len) to prevent reading out of bounds.
  • w_userdata_key writes the raw string directly via wf. If a user-data key contains special characters like a colon or a newline, it will break the YAML structure. Use yaml_output_string to properly escape the key.
🛠️ Proposed fix
 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.getUserData(tw->getElmts(1));
   if (ud == nullptr)
-    g_model.setUserData(val, "");
+    g_model.setUserData(std::string(val, val_len), "");
   else
-    ud->key = val;
+    ud->key = std::string(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 wf(opaque, ud->key.c_str(), ud->key.size());
+  return yaml_output_string(ud->key.c_str(), ud->key.size(), wf, opaque);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.getUserData(tw->getElmts(1));
if (ud == nullptr)
g_model.setUserData(val, "");
else
ud->key = val;
}
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));
return wf(opaque, ud->key.c_str(), ud->key.size());
}
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.getUserData(tw->getElmts(1));
if (ud == nullptr)
g_model.setUserData(std::string(val, val_len), "");
else
ud->key = std::string(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);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/storage/yaml/yaml_datastructs_funcs.cpp` around lines 2892 - 2909,
Update r_userdata_key to construct the key from the bounded buffer using
val_len, including when initializing user data, instead of treating val as
null-terminated. Update w_userdata_key to emit ud->key through
yaml_output_string so special characters are YAML-escaped before writing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The val buffer is always 0 terminated.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philmoz Good point, thanks for the clarification — that resolves the buffer over-read concern, since val follows the same null-terminated convention used elsewhere in this file (e.g. r_screen_id, r_wov_string).

The escaping concern for w_userdata_key still stands though: it writes ud->key raw via wf instead of yaml_output_string, so a key containing YAML special characters (:, #, newline, etc.) would produce malformed YAML on save. Worth using yaml_output_string(ud->key.c_str(), ud->key.size(), wf, opaque) there, consistent with how w_userdata_value already does it.

Comment thread radio/src/storage/yaml/yaml_datastructs_funcs.cpp
@philmoz
philmoz force-pushed the philmoz/model-user-data branch from 5872b42 to 7765bc9 Compare July 16, 2026 02:13

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/storage/yaml/yaml_datastructs_st16.cpp`:
- Around line 941-947: Guard the userData lookup in r_userdata_type and
r_userdata_value before dereferencing the result, ensuring reads are skipped or
handled safely when the key entry has not yet been processed. Apply the same fix
at the corresponding struct_UserData definitions in
radio/src/storage/yaml/yaml_datastructs_st16.cpp lines 941-947,
radio/src/storage/yaml/yaml_datastructs_t20.cpp lines 828-834, and
radio/src/storage/yaml/yaml_datastructs_x9e.cpp lines 810-816.

In `@radio/src/storage/yaml/yaml_parser.cpp`:
- Around line 245-251: In the YAML escape parsing logic, replace the raw-char
toupper calls at radio/src/storage/yaml/yaml_parser.cpp lines 245-251 (high
nibble) and 263-270 (low nibble) with explicit checks for uppercase and
lowercase hexadecimal ranges. Preserve the existing nibble conversion and parser
state transitions while avoiding undefined behavior for negative char values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7cd1b04e-ead0-4f44-a37f-a191d3a8bf6d

📥 Commits

Reviewing files that changed from the base of the PR and between 5872b42 and 7765bc9.

📒 Files selected for processing (36)
  • companion/src/constants.h
  • companion/src/firmwares/edgetx/yaml_modeldata.cpp
  • companion/src/firmwares/modeldata.cpp
  • companion/src/firmwares/modeldata.h
  • radio/src/dataconstants.h
  • radio/src/datastructs_model.cpp
  • radio/src/datastructs_private.h
  • radio/src/lua/api_model.cpp
  • radio/src/model_init.cpp
  • radio/src/storage/sdcard_yaml.cpp
  • radio/src/storage/yaml/yaml_datastructs_128x64.cpp
  • radio/src/storage/yaml/yaml_datastructs_f16.cpp
  • radio/src/storage/yaml/yaml_datastructs_funcs.cpp
  • radio/src/storage/yaml/yaml_datastructs_gx12.cpp
  • radio/src/storage/yaml/yaml_datastructs_nb4p.cpp
  • radio/src/storage/yaml/yaml_datastructs_nv14.cpp
  • radio/src/storage/yaml/yaml_datastructs_pa01.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18u.cpp
  • radio/src/storage/yaml/yaml_datastructs_st16.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15pro.cpp
  • radio/src/storage/yaml/yaml_datastructs_t20.cpp
  • radio/src/storage/yaml/yaml_datastructs_t22.cpp
  • radio/src/storage/yaml/yaml_datastructs_tpro.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx15.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp
  • radio/src/storage/yaml/yaml_datastructs_x10.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9d.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9e.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlite.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlites.cpp
  • radio/src/storage/yaml/yaml_parser.cpp
  • radio/src/storage/yaml/yaml_tree_walker.cpp
  • radio/src/storage/yaml/yaml_tree_walker.h
🚧 Files skipped from review as they are similar to previous changes (24)
  • radio/src/model_init.cpp
  • radio/src/storage/sdcard_yaml.cpp
  • radio/src/storage/yaml/yaml_datastructs_f16.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15pro.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9d.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlite.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx15.cpp
  • radio/src/storage/yaml/yaml_datastructs_pa01.cpp
  • radio/src/storage/yaml/yaml_datastructs_128x64.cpp
  • radio/src/storage/yaml/yaml_datastructs_gx12.cpp
  • radio/src/storage/yaml/yaml_datastructs_tpro.cpp
  • radio/src/datastructs_private.h
  • radio/src/storage/yaml/yaml_datastructs_x10.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp
  • radio/src/dataconstants.h
  • radio/src/storage/yaml/yaml_datastructs_xlites.cpp
  • radio/src/storage/yaml/yaml_datastructs_funcs.cpp
  • radio/src/storage/yaml/yaml_datastructs_t22.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18u.cpp
  • radio/src/lua/api_model.cpp
  • radio/src/datastructs_model.cpp
  • radio/src/storage/yaml/yaml_datastructs_nv14.cpp

Comment thread radio/src/storage/yaml/yaml_datastructs_st16.cpp
Comment on lines +245 to +251
else {
char ch = toupper(*c);
if (ch >= 'A' && ch <= 'F') {
escHexVal = (ch - 'A' + 10) << 4;
state = ps_ValEsc3;
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Undefined behavior when passing a raw char to toupper.

In C and C++, toupper expects an int representable as an unsigned char or EOF. Passing a negative value (which occurs if *c is a non-ASCII character from a malformed YAML sequence and char is signed) results in undefined behavior. Many standard library implementations use this value as an array index, leading to out-of-bounds memory reads and potential crashes.

  • radio/src/storage/yaml/yaml_parser.cpp#L245-L251: Replace toupper with explicit character range checks for the high nibble.
  • radio/src/storage/yaml/yaml_parser.cpp#L263-L270: Replace toupper with explicit character range checks for the low nibble.

You can safely handle both cases without toupper.

🛠️ Proposed fix to remove toupper

For radio/src/storage/yaml/yaml_parser.cpp#L245-L251 (High nibble):

-            else {
-              char ch = toupper(*c);
-              if (ch >= 'A' && ch <= 'F') {
-                escHexVal = (ch - 'A' + 10) << 4;
-                state = ps_ValEsc3;
-                break;
-              }
-            }
+            else if (*c >= 'A' && *c <= 'F') {
+                escHexVal = (*c - 'A' + 10) << 4;
+                state = ps_ValEsc3;
+                break;
+            }
+            else if (*c >= 'a' && *c <= 'f') {
+                escHexVal = (*c - 'a' + 10) << 4;
+                state = ps_ValEsc3;
+                break;
+            }

For radio/src/storage/yaml/yaml_parser.cpp#L263-L270 (Low nibble):

-            else {
-              char ch = toupper(*c);
-              if (ch >= 'A' && ch <= 'F') {
-                escHexVal |= (ch - 'A' + 10);
-                scratch_buf += escHexVal;
-                state = ps_ValQuo;
-                break;
-              }
-            }
+            else if (*c >= 'A' && *c <= 'F') {
+                escHexVal |= (*c - 'A' + 10);
+                scratch_buf += escHexVal;
+                state = ps_ValQuo;
+                break;
+            }
+            else if (*c >= 'a' && *c <= 'f') {
+                escHexVal |= (*c - 'a' + 10);
+                scratch_buf += escHexVal;
+                state = ps_ValQuo;
+                break;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else {
char ch = toupper(*c);
if (ch >= 'A' && ch <= 'F') {
escHexVal = (ch - 'A' + 10) << 4;
state = ps_ValEsc3;
break;
}
else if (*c >= 'A' && *c <= 'F') {
escHexVal = (*c - 'A' + 10) << 4;
state = ps_ValEsc3;
break;
}
else if (*c >= 'a' && *c <= 'f') {
escHexVal = (*c - 'a' + 10) << 4;
state = ps_ValEsc3;
break;
}
📍 Affects 1 file
  • radio/src/storage/yaml/yaml_parser.cpp#L245-L251 (this comment)
  • radio/src/storage/yaml/yaml_parser.cpp#L263-L270
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/storage/yaml/yaml_parser.cpp` around lines 245 - 251, In the YAML
escape parsing logic, replace the raw-char toupper calls at
radio/src/storage/yaml/yaml_parser.cpp lines 245-251 (high nibble) and 263-270
(low nibble) with explicit checks for uppercase and lowercase hexadecimal
ranges. Preserve the existing nibble conversion and parser state transitions
while avoiding undefined behavior for negative char values.

@philmoz
philmoz force-pushed the philmoz/model-user-data branch from 7765bc9 to 921e7da Compare July 16, 2026 03:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/model_init.cpp`:
- Line 152: Replace the raw memset-based ModelData reset in setModelDefaults()
with a lifetime-safe reset or reconstruction that preserves the
std::vector<UserData> object before calling clearUserData(); ensure full-model
initialization clears user data on every supported target. Apply the same
lifetime-safe reset after the model buffer is cleared in
radio/src/storage/sdcard_yaml.cpp at lines 352-352, with no separate direct
change required there if the shared reset logic fixes it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c4f67f9-de3d-4d75-87fd-dfd4be41c3e6

📥 Commits

Reviewing files that changed from the base of the PR and between 7765bc9 and 921e7da.

📒 Files selected for processing (36)
  • companion/src/constants.h
  • companion/src/firmwares/edgetx/yaml_modeldata.cpp
  • companion/src/firmwares/modeldata.cpp
  • companion/src/firmwares/modeldata.h
  • radio/src/dataconstants.h
  • radio/src/datastructs_model.cpp
  • radio/src/datastructs_private.h
  • radio/src/lua/api_model.cpp
  • radio/src/model_init.cpp
  • radio/src/storage/sdcard_yaml.cpp
  • radio/src/storage/yaml/yaml_datastructs_128x64.cpp
  • radio/src/storage/yaml/yaml_datastructs_f16.cpp
  • radio/src/storage/yaml/yaml_datastructs_funcs.cpp
  • radio/src/storage/yaml/yaml_datastructs_gx12.cpp
  • radio/src/storage/yaml/yaml_datastructs_nb4p.cpp
  • radio/src/storage/yaml/yaml_datastructs_nv14.cpp
  • radio/src/storage/yaml/yaml_datastructs_pa01.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18u.cpp
  • radio/src/storage/yaml/yaml_datastructs_st16.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15.cpp
  • radio/src/storage/yaml/yaml_datastructs_t15pro.cpp
  • radio/src/storage/yaml/yaml_datastructs_t20.cpp
  • radio/src/storage/yaml/yaml_datastructs_t22.cpp
  • radio/src/storage/yaml/yaml_datastructs_tpro.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx15.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp
  • radio/src/storage/yaml/yaml_datastructs_x10.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9d.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9e.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlite.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlites.cpp
  • radio/src/storage/yaml/yaml_parser.cpp
  • radio/src/storage/yaml/yaml_tree_walker.cpp
  • radio/src/storage/yaml/yaml_tree_walker.h
🚧 Files skipped from review as they are similar to previous changes (31)
  • radio/src/dataconstants.h
  • radio/src/storage/yaml/yaml_datastructs_t15.cpp
  • radio/src/storage/yaml/yaml_datastructs_x10.cpp
  • radio/src/storage/yaml/yaml_datastructs_tpro.cpp
  • radio/src/storage/yaml/yaml_datastructs_nb4p.cpp
  • radio/src/storage/yaml/yaml_datastructs_128x64.cpp
  • radio/src/storage/yaml/yaml_datastructs_t22.cpp
  • companion/src/firmwares/modeldata.h
  • radio/src/storage/yaml/yaml_datastructs_xlite.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx15.cpp
  • radio/src/storage/yaml/yaml_datastructs_pl18.cpp
  • radio/src/storage/yaml/yaml_datastructs_xlites.cpp
  • radio/src/storage/yaml/yaml_datastructs_f16.cpp
  • radio/src/storage/yaml/yaml_datastructs_st16.cpp
  • radio/src/storage/yaml/yaml_parser.cpp
  • companion/src/firmwares/modeldata.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9dp2019.cpp
  • radio/src/storage/yaml/yaml_datastructs_tx16smk3.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9d.cpp
  • radio/src/storage/yaml/yaml_datastructs_pa01.cpp
  • radio/src/storage/yaml/yaml_tree_walker.h
  • radio/src/storage/yaml/yaml_datastructs_nv14.cpp
  • radio/src/storage/yaml/yaml_datastructs_funcs.cpp
  • radio/src/lua/api_model.cpp
  • radio/src/storage/yaml/yaml_datastructs_t20.cpp
  • radio/src/datastructs_model.cpp
  • radio/src/storage/yaml/yaml_datastructs_x9e.cpp
  • companion/src/firmwares/edgetx/yaml_modeldata.cpp
  • radio/src/storage/yaml/yaml_tree_walker.cpp
  • radio/src/datastructs_private.h
  • radio/src/storage/yaml/yaml_datastructs_gx12.cpp

Comment thread radio/src/model_init.cpp
@3djc

3djc commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

I always have the concern with lua that users didn't write can fill those table, and he has no way to do housekeeping on this, since deleting the lua won't delete corresponding keys

@pfeerick

pfeerick commented Jul 16, 2026

Copy link
Copy Markdown
Member

True. However, with this, a lua tool should be able to see all the data (i.e. model.getAllUserData()) and then allow you to delete all the entires or just the entries you don't want (via model.deleteUserData(key)). But, given that is 50 entries per model, sure, you could have some entries left over over time, but you are unlikely to run out of "space". Not sure we can really do anything about that really, as sure, companion could cleanup if say a widget/tool was no longer on the SD card, but that could also result in loss of settings you want later. i.e. I think manual cleanup is the only valid option here, since we don't have a packaging system in place (yet).

@3djc

3djc commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Why is it that your guys always think users are developers, when only a ridiculously tiny proportion is. The average user struggle with copying data on their SD. They follow YT video, and guess what, they will have quickly tons of useless data after trying lua and seing it doesn't work for them. And no tool provided for them to be able to manage. And even if someone builds an lua to do so, no way to identify 'lua widget' owner to find which data belongs to whom

@pfeerick

Copy link
Copy Markdown
Member

they will have quickly tons of useless data after trying lua and seing it doesn't work for them.

Oi, stop looking at my 101 OpenTX and EdgeTX data folders... there is a reason I copy and ignore! I'm sure of it!

no way to identify 'lua widget' owner to find which data belongs to whom

This is the real problem... do you have any suggestions on how to implement this differently to mitigate this? Do we require a UUID for each script/tool or something (and how do we enforce it?) One way or another we really do need to have an API for widgets/lua to be able to have a proper way to store settings/freeform data. We can't just put our heads in the sand and ignore it.

@3djc

3djc commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Maybe set and get should ask for both a "script name" and "key name". That will also help 2 scripts setting values for some generic key name like "settings"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
radio/src/strhelpers.cpp (1)

1318-1336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add focused tests for the shared replacement helper.

Cover empty from, empty to, no matches, overlapping matches, and replacements where to contains from; this utility is now used by CSV/YAML serialization paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/strhelpers.cpp` around lines 1318 - 1336, Add focused tests for
strReplaceAll covering empty from, empty to, no matches, overlapping matches,
and cases where to contains from. Assert the resulting string for each scenario,
including the helper’s existing behavior of leaving the input unchanged when
from is empty.
radio/src/lua/api_model.cpp (3)

1969-1969: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix typographical error.

Both of these comments misspell "mandatory".

  • radio/src/lua/api_model.cpp#L1969-L1969: Change mandataory to mandatory.
  • radio/src/lua/api_model.cpp#L2066-L2066: Change mandataory to mandatory.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/lua/api_model.cpp` at line 1969, Correct the spelling of
“mandataory” to “mandatory” in the comments at radio/src/lua/api_model.cpp lines
1969-1969 and 2066-2066; no code behavior changes are needed.

2100-2104: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Prevent accidental deletion logic with empty keys.

If getUDKey returns an empty string, calling deleteUserData("") will perform a full scan of the user data vector and might inadvertently mutate state if empty slots exist. Guarding against an empty string prevents this edge case.

♻️ Proposed refactor
 static int luaDeleteUserData(lua_State *L)
 {
   std::string s = getUDKey(L);
-  g_model.deleteUserData(s.c_str());
+  if (!s.empty()) {
+    g_model.deleteUserData(s.c_str());
+  }
   return 0;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/lua/api_model.cpp` around lines 2100 - 2104, Update
luaDeleteUserData to check the string returned by getUDKey before calling
g_model.deleteUserData; only invoke deletion for a non-empty key, otherwise
return without mutating user data.

1999-2001: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Short-circuit on empty keys to avoid unnecessary lookups.

getUDKey returns an empty string if the app or key arguments are empty. It's safer and slightly more efficient to check for this and return nil immediately, rather than querying g_model.getUserData with an empty string.

♻️ Proposed refactor
   std::string s = getUDKey(L);
+
+  if (s.empty()) {
+    lua_pushnil(L);
+    return 1;
+  }
 
   auto ud = g_model.getUserData(s.c_str());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/lua/api_model.cpp` around lines 1999 - 2001, Update the code after
getUDKey in the surrounding API function to check whether the returned key
string is empty and immediately return Lua nil before calling
g_model.getUserData. Preserve the existing lookup and result handling for
non-empty keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/lua/api_model.cpp`:
- Around line 2062-2087: In luaSetUserData, validate argument 3’s supported Lua
type before calling getUDKey and allocating std::string s, while preserving the
existing string, integer, number, and fallback type-error behavior. Keep the
subsequent empty-key check and g_model.setUserData dispatch unchanged, and avoid
any luaL_checkstring call after the local std::string has been created.

---

Nitpick comments:
In `@radio/src/lua/api_model.cpp`:
- Line 1969: Correct the spelling of “mandataory” to “mandatory” in the comments
at radio/src/lua/api_model.cpp lines 1969-1969 and 2066-2066; no code behavior
changes are needed.
- Around line 2100-2104: Update luaDeleteUserData to check the string returned
by getUDKey before calling g_model.deleteUserData; only invoke deletion for a
non-empty key, otherwise return without mutating user data.
- Around line 1999-2001: Update the code after getUDKey in the surrounding API
function to check whether the returned key string is empty and immediately
return Lua nil before calling g_model.getUserData. Preserve the existing lookup
and result handling for non-empty keys.

In `@radio/src/strhelpers.cpp`:
- Around line 1318-1336: Add focused tests for strReplaceAll covering empty
from, empty to, no matches, overlapping matches, and cases where to contains
from. Assert the resulting string for each scenario, including the helper’s
existing behavior of leaving the input unchanged when from is empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0487aa61-ddd9-4d19-9bd3-252fbf430bd3

📥 Commits

Reviewing files that changed from the base of the PR and between 921e7da and cd04c3d.

📒 Files selected for processing (6)
  • radio/src/gui/colorlcd/setup_menus/quick_menu.cpp
  • radio/src/lua/api_model.cpp
  • radio/src/storage/modelslist.cpp
  • radio/src/storage/modelslist.h
  • radio/src/strhelpers.cpp
  • radio/src/strhelpers.h
💤 Files with no reviewable changes (1)
  • radio/src/storage/modelslist.h

Comment thread radio/src/lua/api_model.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@radio/src/lua/api_model.cpp`:
- Around line 2034-2041: Update the argument handling block to treat any call
with at least one argument as an app filter, changing the lua_gettop check from
an exact count to a lower-bound check. Validate argument 1 with luaL_checkstring
before constructing or assigning to std::string s, so Lua type errors occur
before a C++ string allocation can be bypassed by longjmp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fd157797-7201-4191-89d8-0aec34eeed3b

📥 Commits

Reviewing files that changed from the base of the PR and between cd04c3d and b09d5c6.

📒 Files selected for processing (2)
  • radio/src/datastructs_model.cpp
  • radio/src/lua/api_model.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • radio/src/datastructs_model.cpp

Comment on lines +2034 to +2041
bool matchApp = false;
std::string s;
if (lua_gettop(L) == 2) {
s = luaL_checkstring(L, 1);
strReplaceAll(s, "|", "_");
s += "|";
matchApp = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix argument count check and prevent std::string memory leak on Lua type errors.

There are two distinct issues in this block:

  1. Incorrect argument check (Functional correctness): lua_gettop(L) == 2 will only filter by app if exactly two arguments are passed. Since app is the first and only expected argument, the check should be >= 1.
  2. Memory leak on longjmp (Stability & availability): luaL_checkstring(L, 1) is called after std::string s is allocated on the stack. If the argument provided is not a string, Lua will throw a type error via longjmp, bypassing s's destructor and permanently leaking the string allocation.

Validate the argument type before allocating the std::string to safely trigger standard Lua type errors.

🔒️ Proposed fix
-  bool matchApp = false;
-  std::string s;
-  if (lua_gettop(L) == 2) {
-    s = luaL_checkstring(L, 1);
-    strReplaceAll(s, "|", "_");
-    s += "|";
-    matchApp = true;
-  }
+  const char* appName = nullptr;
+  if (lua_gettop(L) >= 1) {
+    appName = luaL_checkstring(L, 1);
+  }
+
+  bool matchApp = false;
+  std::string s;
+  if (appName) {
+    s = appName;
+    strReplaceAll(s, "|", "_");
+    s += "|";
+    matchApp = true;
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/lua/api_model.cpp` around lines 2034 - 2041, Update the argument
handling block to treat any call with at least one argument as an app filter,
changing the lua_gettop check from an exact count to a lower-bound check.
Validate argument 1 with luaL_checkstring before constructing or assigning to
std::string s, so Lua type errors occur before a C++ string allocation can be
bypassed by longjmp.

@offer-shmuely

Copy link
Copy Markdown
Contributor

Great feature, thanks 🙏

The api looks good (visually)

For the over populated data,
I can write a mini "preset" script for cleanup, the user will still need to run it🤷‍♂️ but it will give a chance to verify the outdated data is there.

One thing to wonder is how do validate the correctness of "app"?

Maybe app should be the path on disk? so future validation will be possible.

only the set operation should validate the data.
i.e.
For widget name "my widget 1.0" that located on /WIDGETS/my-widget
The set operation should validate app model.setUserData("/WIDGETS/my-widget", "key1", "data1")

@philmoz
philmoz force-pushed the philmoz/model-user-data branch from 162ac16 to ed7d162 Compare July 21, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement ✨ New feature or request lua-api Lua API related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

allow lua apps to keep key/value data in model yaml

4 participants