Skip to content

Add Sync Backpack Time button (real timestamps on goggle DVR) + lua memory optimisations#18

Open
bob9 wants to merge 2 commits into
ExpressLRS:masterfrom
bob9:sync-backpack-time
Open

Add Sync Backpack Time button (real timestamps on goggle DVR) + lua memory optimisations#18
bob9 wants to merge 2 commits into
ExpressLRS:masterfrom
bob9:sync-backpack-time

Conversation

@bob9

@bob9 bob9 commented Jul 14, 2026

Copy link
Copy Markdown

Adds a "Sync Backpack Time" item at the bottom of the root menu (only shown for an ELRS TX module). It sends the handset date/time to the module as a param write on fieldId 0x3C, which the firmware uses to set the backpack RTC. Beeps so you know it fired.

The point of it: the backpack can pass time on to HDZero goggles, so DVR recordings get real timestamps instead of defaulting to epoch. Handy for finding the right clip after a session, and for anyone using DVR footage to match up logs. Radios don't have GPS but they do have a clock the pilot already sets, so the handset is the natural source.

After the review feedback about script size I went and measured things properly, and ended up splitting this into two commits. The first is a set of optimisations to the existing script, the second is the button. The optimisations save more than the button costs, so the script comes out slightly smaller than master overall:

master this PR
worst-case heap allocated while parsing one string 7365 B 1893 B
string parse CPU 38.0 ms 22.5 ms
resident heap after load 37467 B 37006 B
resident heap after a full session 34235 B 34018 B
compiled size (luac -s) 14342 B 14338 B

The big one is the first row. fieldGetStrOrOpts builds the CHAR_UP/CHAR_DOWN substitution table from scratch for every single character it parses, so every field name / option list / unit costs 2-3 heap allocations per byte, hundreds of strings per load. Hoisting that table out to a local kills it completely. I suspect this burst pattern is behind some of the "Script panic: not enough memory" reports on the B&W radios (ExpressLRS/ExpressLRS#2052) - a radio whose free headroom sits between those two numbers should stop panicking, though I can't promise that for every setup.

The rest is smaller stuff: the four INT entries (and two STRING entries) in the functions table were identical tables so they're shared now, the seven copies of crossfireTelemetryPush(0x2D, {deviceId, handsetId, x, y}) became one helper, removed the explicit nils from table constructors, folded fieldUnsignedToSigned into its only caller, deduped the getVersion/lcd.RGB checks in setLCDvar, and hoisted a couple of repeated expressions out of the draw loop.

On correctness: I built a small harness that stubs the radio API and drives both versions through the same scripted session - loading with chunked params, folders, editing with clamping, command popups (confirm and cancel), switching to another device and back, warning flags, the works, on both colour and B&W paths - and diffs everything observable (every push, every drawText with coordinates, popups, tones, return values). Output is byte-identical to master except for the new menu row. Can attach the harness if anyone wants to rerun it. Heap numbers above are from desktop lua with stripped chunks, so on the radio's 32-bit lua the absolute values are roughly half, but the deltas scale the same way.

@pkendall64

Copy link
Copy Markdown
Contributor

We will NOT be adding this much code to the existing Lua script. It already has problems running on low memory radios.

bob9 added 2 commits July 15, 2026 11:03
- Hoist the CHAR_UP/CHAR_DOWN substitution table out of fieldGetStrOrOpts:
  it was rebuilt for every character parsed (2-3 heap allocations per byte
  of every name/option/unit string). Worst-case heap growth within a single
  string parse drops from ~7.4KB to ~1.9KB (-74%); parse CPU -40%.
- Share one table between the four identical INT entries and the two
  identical STRING entries in the functions dispatch table.
- Drop explicit nil fields from table constructors (each costs a VM
  instruction and hash-slot padding).
- Replace seven duplicated crossfireTelemetryPush(cmd, {deviceId,
  handsetId, x, y}) sites with a shared crsfPush helper.
- Inline single-caller fieldUnsignedToSigned into fieldSignedLoad.
- setLCDvar: one getVersion() call and one lcd.RGB branch instead of two;
  drop the unused textWidth global.
- Hoist repeated subexpressions in the draw loop and popup handler.

Net vs master: -384B stripped bytecode (13,958 vs 14,342), -28 VM
instructions, ~-1.3KB resident heap (64-bit desktop measure; ~half on the
radio's 32-bit Lua). Behavior verified byte-identical to master across a
scripted session covering chunked parameters, folders, device switching,
editing with clamping, command popups, warning flags, and touch events on
both color and B&W paths.
…handset

Adds a Sync Backpack Time entry at the bottom of the root menu, shown only
when the selected device is an ELRS TX module. Pressing it sends the
handset date/time as a parameter write to fieldId 0x3C (handled by the
module firmware to set the backpack RTC) and plays a confirmation tone.

Combined with the preceding perf commit, the script remains smaller than
master with the feature included: 14,338 vs 14,342 bytes stripped
bytecode, 2,596 vs 2,624 VM instructions, and lower resident heap at
every measured checkpoint.
@bob9 bob9 force-pushed the sync-backpack-time branch from 3a0d54d to 925f332 Compare July 15, 2026 07:48
@bob9 bob9 changed the title Add Sync Backpack Time button to set the backpack RTC from the handset Add Sync Backpack Time button (real timestamps on goggle DVR) + lua memory optimisations Jul 15, 2026
@bob9

bob9 commented Jul 15, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look - that's a fair concern, so I spent some time measuring where the script's memory actually goes. (For context on why the button's worth having: it gets real timestamps onto HDZero DVR files via the backpack, instead of every recording claiming to be from 1970.)

Reworked into two commits: optimisations to the existing script first, then the button on top. Net result is the script is now slightly smaller than master with the feature included (14338 vs 14342 bytes compiled), and heap use is lower everywhere I measured. Details and numbers in the updated description.

The more interesting find along the way: the string parser allocates 2-3 temporary objects per character for everything it reads, which I suspect feeds the "not enough memory" panics on the small radios. Fixing that cut the parser's worst-case allocation burst by about 74%.

Verified the optimisations against master with a stubbed-radio harness - output is byte-identical across a full session apart from the new row. The two commits are independent, so if you want the optimisations without the button that works too. Or if you'd rather have no menu item at all I can make the sync fire automatically on connect, which is even smaller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants