Skip to content

New LUA scripts & widgets - #3

Open
jurgelenas wants to merge 163 commits into
masterfrom
unified-lua-lsp
Open

New LUA scripts & widgets#3
jurgelenas wants to merge 163 commits into
masterfrom
unified-lua-lsp

Conversation

@jurgelenas

Copy link
Copy Markdown
Member

No description provided.

The getScreenId() comments and the ui/sd*.lua file headers had the two
480-wide tiers' radios the wrong way round: sd_tall (480x320) claimed
the TX16S family and sd (480x272) claimed the T15 Pro, TX15, ST16 and
PL18. It is the other way round.

radio/src/targets/horus/hal.h selects 480x320 only under RADIO_T15 and
480x272 for everything else in the family, so a TX16S takes the sd
branch. The 480x320 radios live in their own targets - t15pro, tx15,
st16 and pl18 - and are the ones that reach sd_tall.

Comments only, no behaviour change: getScreenId() already routed on
LCD_W/LCD_H and was correct. But the comments are what you read when
deciding which radio to launch to exercise a tier, and they sent you to
the wrong one.
crsf.lua carried two null-terminated string parsers 170 lines apart,
and they were not equivalent: onElrsStatus guarded its loop on
`data[off] and data[off] ~= 0` while the shared CRSF:fieldGetString
guarded only on `~= 0`, so a frame with no terminator hit
string.char(nil). Swapping the call in as-is would have traded a
duplicated loop for a Lua error inside CRSF:poll(). fieldGetString now
carries the nil guard, which also fixes onDeviceInfo - its other
caller, and the one that could actually reach the fault.

onDeviceInfo also committed to the cache before it knew the frame was
long enough: a partial parse set info.name and then errored, and since
requestDeviceInfo() stops pinging once info.name is set, the retry
latched off for good. The version triplet is now checked first.
onElrsStatus gains the source-address check onDeviceInfo and the
tool's parseElrsInfoMessage already had; ExpressLRS hardcodes
CRSF_ADDRESS_CRSF_TRANSMITTER as the 0x2E origin.

Verified headless on TX16S by driving poll() with scripted frames: 21
assertions over both handlers. Reverting just the nil guard failed
exactly four of them and nothing else. The telemetry widget still
renders against the CRSF simulator, so the address check does not
reject the live path.
luaExecStandalone scans the table a TOOLS script returns and then never
pops it, so it sits on the standalone Lua stack - a GC root - for the
whole tool session (colorlcd/standalone_lua.cpp:89-120). The luaL_unref
at line 252 drops the registry reference to init after its single call,
but the lingering table still holds it, and init holds setMock as an
upvalue. Both stayed resident until the tool was closed.

main.lua now returns a named module table so init can clear its own
entry, which subsumes the setMock clear: init uniquely captures
VERSION, useLvgl and setMock, so releasing it releases those too. B&W
and the widget loader both pop the table and are unaffected.

crsf.lua clears setMock for consistency, though nothing captures it
there - it is chunk-scope, so the closure is already unreachable once
the chunk returns CRSF.

Verified headless on TX16S: the tool loads with the mock installed,
re-enters cleanly with no Lua errors, and both widgets still take live
mock telemetry through the shared _crsfSingleton.
Four of the five comment blocks from upstream elrs.lua:437-494 were lost
in the port, leaving a chunk-reassembly state machine over module-level
state with nothing explaining why it does what it does. Restored verbatim
at their original positions, and commented the port-only logic upstream
never had: the device/field mismatch discard, the length sanity check,
the hidden-bit detection that drives fieldHiddenChanged, and the
nameStale/reloading guard on the name cache.

The fifth block described the return value, which is now gone. Nothing
could read it: poll() called the function as a bare statement and
ui/lcd.lua never referenced the parser. Redraw is driven from protocol
state instead -- #loadQueue in lcd.lua:211 and the isFolderLoaded
transition in main.lua:184.
csrfsimulator.lua called table.concat and table.unpack unguarded, and
B&W EdgeTX ships without the table library, so the chunk died at load
with "attempt to index a nil value (global 'table')" and the ExpressLRS
tool could not be exercised on those radios at all. Simulator-only:
setMock() is gated on getVersion() ending in "-simu", so real radios
never loaded the mock.

SCRIPTS/ELRS/shim.lua already carried tableConcat and tableRemove for
exactly this and had no consumer until now; the mock's hand-rolled
tableRemove goes away in favour of it. The unpack site gets a new
charsToString() rather than a tableUnpack polyfill, because a pure-Lua
unpack has to recurse once per element and `return t[i], f(...)` is not
a tail call, so it cannot be optimised away.
The tool script carried ~90 lines of verbatim or near-verbatim copies of
SCRIPTS/ELRS/crsf.lua and shim.lua: the address/frame-type/field-type
constants down to identical comments, pop/push, hasCrsfModule, setMock,
and a byte-identical tableConcat polyfill. The copies had already
drifted into a bug: crsf.lua declared CMD_CONFIRMED = 3 where the
firmware's commandStep_e has lcsAskConfirm = 3, lcsConfirmed = 4 --
latent only because the widgets never send anything past CMD_CLICK.

The tool now loads /SCRIPTS/ELRS/crsf.lua and shim.lua and references
crsf.CONST directly at every call site; its own shim.lua is deleted and
protocol.lua drops its constant table, pop/push and hasCrsfModule.
main.lua's setMock goes too -- crsf.lua already self-mocks at load, and
the tool talks to CRSF only through the library. The library gains what
the tool needs (FIELD_UINT32..INT64, ELRS_SERIAL_ID, ELRS_FLAGS_*) plus
the corrected command steps. B&W radios ship without the table library
(linit.c gates it on COLORLCD), so crsf.lua's two raw table.* calls now
go through the shim -- a prerequisite, since the tool is the first
consumer of this code on B&W hardware.

Checking the internal pseudo field types against the CRSF spec showed
DEVICE = 15 shadowed a real wire type: 0x0F is CRSF_VTX in both the
spec and the firmware enum, so a TBS VTX browsed via Other Devices
would have rendered its VTX parameters through the device-row handler.
The synthetic markers are now 128/129 -- provably unreachable from the
wire, whose type byte is masked with 0x7f -- FIELD_VTX = 15 documents
the wire truth, and the unreferenced BACK_EXIT (legacy elrs.lua's
synthetic EXIT row) is gone. shim.getSensorValue is deleted as a third
copy of crsf.getSensorValue with no consumers, and edgetx.yml marks the
tool as depending on the ELRS library, matching the widgets.

Verified headless on GX12 (B&W) and TX16S: the tool loads and populates
the root folder on both, switches to the mock RX through Other Devices,
and drives Enable WiFi through ASKCONFIRM -> CONFIRMED -> Executing --
the path the drifted constant would have wedged, since the mock only
advances on 4. no_module, slow_loading and model_mismatch scenarios,
and both widgets running live alongside the tool, all render correctly.
hasTelemetry now derives from RQly instead of a 1 Hz status poll, and
the ELRS status request is sent once per connection (connected + ELRS
serial + active-antenna RSSI above -70 dBm), just for model match. The
device ping goes to the TX module instead of broadcast -- answered on
the UART, never over the air -- via the shared CRSF:pingDevices(dest).
VTX Admin loses its periodic folder poll entirely: reads happen at
discovery, after our own writes (retried up to 3x), and once on resume
from a tool session. Net widget traffic is ~2 frames per session where
it was 1-2 per second forever.

The CRSF mock honors ping destinations, logs every push, and gains
weak_link and mismatch_cycle scenarios; model_mismatch RSSI is raised
to a bench-realistic -55/-58 so the status-request gate can pass.
App.reset() and Protocol.reset() had no callers and never did -- both
arrived orphaned in the BW/colour unification commit. CRSF's
unregisterHandler() goes with them, along with registerHandler's
duplicate-registration guard: every registration site is a top-level
once-per-chunk statement, and the one repeat path (both widgets sharing
_crsfSingleton) deliberately registers a fresh closure per instance, so
the guard could never match. onDeviceInfo's info.vStr was a
string.format on every DEVICE_INFO frame that nothing read.

Protocol.handlers loses its constructor placeholder and its save column,
which no caller ever dereferenced -- both UIs call fieldIntSave,
fieldStringSave and handleCommandSave directly, from code that has
already branched on the field type. Entries are plain load functions
now; the four typed nil entries stay, documenting the wire types with no
handler. The data[i] = nil writes in parseParameterInfoMessage went too:
the frame table is fresh per pop and the tool is its only consumer.

VERSION_CHECK_ENABLED was constant true in both UIs, so init() runs the
version check unconditionally and preCheck tests the result. lvgl's
buildFieldWidget drops its folderWidth parameter and FIELD_FOLDER
branch, unreachable because the build loop consumes every folder run
before reaching it. lcd's init() stops re-assigning its declared
defaults, and the count == 0 guards in selectField and handleEvent are
gone -- getSelectableCount() is getFieldCount() + 1 for the
always-present BACK/EXIT row.

warningDismissed folds into warningDismissedAt in both UIs; the boolean
equalled "timestamp is set" at every observable point. The lvgl Exit
callback's flag-only write is dropped rather than timestamped, since
handleWarning early-returns on App.shouldExit and never read it.

ELRSVTXAdmin stops passing crsf, Presets and Protocol to the screen
files, which never used them, and the orphaned Protocol and VTX locals
go with them -- portrait keeps VTX, being the one tier that renders
power and pit state. writeConfig's boolean pitmode coercion is
unreachable: d.pitmode is a number on every path.

Verified in the simulator on TX16S and GX12 -- the tool loads and
renders, folder navigation and a field edit work, and the model-mismatch
warning shows, dismisses, stays suppressed at 30 s and returns after
60 s.
crsf.lua is a transport core again: constants, the pop/push fan-out and
sensor reads. The DEVICE_INFO cache, the RFMOD/RFRSSI tables and the
per-connection model-match latch move to WIDGETS/ELRSTelemetry/txinfo.lua,
loaded only by the widget that reads them, so the tool and VTX Admin no
longer pay for them.
decodeDeviceInfo(), decodeElrsStatus() and isElrsV1Frame() own the wire
layout of DEVICE_INFO, ELRS_STATUS and the 1.x signature; callers gate on
the decoded source id. The status decode keeps the connected bit alongside
modelMismatch and criticalError.

The tool's protocol.lua consumes them instead of carrying its own parsers
and bit masks, stores the decoded flags, gains a short-frame guard its
DEVICE_INFO path lacked, and sends the link-status request through
requestElrsStatus(), whose hardcoded addressing the deviceIsELRS_TX gate
already guarantees.
elrsinfo.lua is the opt-in stateful companion to the CRSF singleton --
DEVICE_INFO cache, version-keyed RFMOD/RFRSSI tables, per-connection
model-match latch -- registering its handlers on the shared singleton
and consuming the shared decoders instead of parsing frames itself.
Loaded only by the telemetry widget, so the tool and VTX Admin never
pay for it.

The RF tables now build once per detected major version instead of on
every DEVICE_INFO frame, and the highest known version at or below vMaj
wins, so ELRS 5 firmware keeps the newest rate names instead of losing
them.
encodeParameterEntry now slices entry payloads at maxPacketBytes - 8
exactly as CRSFEndpoint::sendParameter does (6 bytes CRSF header/CRC
plus the FieldId/ChunksRemain pair repeated per frame), honouring the
requested chunk index it previously ignored. config.maxPacketBytes
defaults to CRSF_MAX_PACKET_LEN (64); lowering it emulates a slow-baud
handset (CRSFHandset::adjustMaxPacketSize) for deeper chunking.

Also adds the coverage the mock was missing:
- an INT8 "RF Gain" field (value -3, min -10, max 10) on the TX device,
  plus INT8 two's-complement decode on write, so sign extension is
  exercised end to end
- a "critical_error" scenario (connected + baud-rate error, flags 0x41)
  whose critical bits clear on the suppress-critical-errors write
  (pseudo-field FIELD_ID_SUPPRESS_CRITICAL_ERRORS = 0x2E, the bare
  literal TXModuleEndpoint.cpp matches on)

Verified on TX16S under scenario "normal": a full TX+RX parameter walk
through the shipping tool protocol decodes identically to the layout,
with Pitmode (3 chunks) and Packet Rate (2 chunks) reassembled complete
and RF Gain read back as -3/-10/10.
src/SCRIPTS/ELRS/crsf_fields.lua now owns the stateless parameter-field
codecs: the byte getters (readValue, readStringOrOpts) and decodeEntry,
which masks the type byte, reads the hidden bit and name, and dispatches
the per-type loaders (int with sign/width arithmetic, float, text
selection with the dirty/identity cache, string, folder, command). Like
elrsinfo.lua it is opt-in: telemetry-only widgets never load it. Unlike
crsf.lua's fieldGetString, nothing in it mutates the frame data table,
so it is safe under the poll() handler fan-out.

protocol.lua keeps the policy that was interleaved with the decode:
the load-queue pop, the fieldHiddenChanged notification, the CMD_IDLE
popup-clear + related-fields reload (formerly inside fieldCommandLoad),
and the root/background child auto-queue. Its chunk reassembly is
unchanged. The tool no longer loads shim.lua -- its only uses moved
with the codecs.

Verified: full TX+RX field dumps through the tool protocol are
byte-identical to the pre-move baseline on TX16S and on GX12 (whose Lua
state has no table library, proving the shim discipline holds).
crsf_fields.lua gains the session-addressed frame senders: sendWriteInt
(two's-complement re-encode at the field's width), sendWriteString
(maxlen clamp, NUL-strip, terminator), sendCommandStep (the one-byte
commandStep_e write behind CLICK/CONFIRMED/CANCEL/QUERY) and
sendSuppressCriticalErrors (pseudo-field 0x2E, the bare literal
TXModuleEndpoint.cpp matches on). Each reads deviceId/handsetId from a
caller-owned session table -- the tool passes its Protocol table.

The UIs receive the codec via deps and call the two pure save
operations directly; everything with policy stays behind protocol.lua:
handleCommandSave, commandConfirm, commandCancel (send + dismiss), the
new commandRequestCancel (send + keep the popup for the device's
CMD_IDLE echo) and suppressCriticalErrors (optimistic local flag clear
+ the suppress write).

lcd.lua no longer builds frames: the warning screen calls
suppressCriticalErrors(), and drawPopup's EXIT handling is routed per
status -- ASKCONFIRM/EXECUTING through commandCancel() exactly like the
colour UI, everything else through commandRequestCancel(). The one
deliberate wire change: EXIT during an executing command sends a single
CMD_CANCEL where the old code pushed it twice (once at the top of
drawPopup, again via popupConfirmation's CANCEL result).

Verified on TX16S: RF Gain -3 -> -4 edit round-trips (negative encode,
mock decode, sign-extended read-back), a Packet Rate change reloads
siblings (Telem Ratio units recompute), Bind runs CLICK -> 4 QUERYs ->
natural completion with the popup dismissed by the IDLE echo, and
Enable WiFi runs ASKCONFIRM -> CONFIRMED -> EXECUTING -> one CANCEL.
On GX12: the critical-error banner clears with the 0x2E write on the
wire, and EXIT during Bind's executing popup sends CLICK + 2 QUERYs +
exactly one CANCEL with no error.
crsf_fields.lua gains reassemble()/resetChunks(), a line-for-line port
of the tool's PARAMETER_SETTINGS_ENTRY chunk handling operating on
session-table state (fieldChunk, fieldData, expectChunksRemain), and
sendRead(), which carries session.fieldChunk so follow-up reads of a
chunked entry continue where reassembly left off.

Two deliberate deltas from the old inline code, both inert for the
tool: reassemble() takes an explicit expectedFieldId so a passive
consumer under the crsf:poll() fan-out can pass data[3] and accept any
field from its device, and a new fieldDataId gate keeps a
sibling-elicited entry for another field out of an in-flight buffer --
required for that fan-out mode, unreachable when the expected id is
pinned. The unallocated-field guard moved from before-buffering to the
caller's decode gate (unreachable either way: the expected id always
comes from loadQueue/fieldPopup, both within the allocated range).

protocol.lua keeps the policy wrapper: expected-id selection from
popup/queue, queue pop on completion, decode via decodeEntry, and the
reload/auto-queue rules. Its manual chunk-state clears became
resetChunks().

Verified: field dumps byte-identical to the pre-refactor baseline on
TX16S (maxPacketBytes 64 and 22 -- 14-byte chunks splitting names
mid-string) and GX12; slow_loading shows the loading gauge progressing
through the 0.5 s retry cadence against 2 s responses with no errors.
The widget's private parse helpers are gone: parseChildIds and
parseFieldName read at hardcoded offsets 6/7 and parseFieldType dropped
the hidden bit, all on the assumption that an entry always arrives in
one frame. Discovery now runs Protocol (its session table, addressed at
ADDRESS_TX/ADDRESS_HANDSET_ELRS) through crsf_fields reassemble +
decodeEntry, and reads go out via sendRead, which carries the chunk
index the old sendParameterRead pinned to 0 -- so chunked entries work
without a new loop: an incomplete entry leaves the item queued (or the
folder read armed) and the existing tick retry sends the follow-up.
discoveredFields, previously write-only, is now the decode-target cache
that gives decodeEntry its per-field identity.

Reassembly gained a guard the tool never needed. Under the poll()
fan-out every instance sees every answer, so when several instances
each request the same field, the extra copies of a multi-chunk entry's
final chunk arrive after a session has already completed it -- and
their header is indistinguishable from a fresh single-frame entry, so
they decoded as garbage. Reproduced with two widget zones at
maxPacketBytes 22: both read "VTX Off" instead of "R3". reassemble()
now records the field whose chunked entry just completed and swallows
those trailing finals until the next request cycle.

VTX.parseFolderName strips the pit-mode aux arrow by suffix: the shared
decoder translates the firmware's one-byte 0xC0/0xC1 into the
multi-byte CHAR_UP/CHAR_DOWN glyphs, which the old fixed -2 slice would
have cut mid-glyph. Write-queue entries carry {id, value} so
sendWriteInt consumes them directly; "Send VTx" queues CMD_CLICK the
same way, byte-identical on the wire.

Verified on TX16S with two widget instances: discovery to READY, a Band
R->A change writing field 11 and both instances picking up the folder
read-back, Send VTx sending one CMD_CLICK, and the resume-from-tool
path issuing its single folder re-read. At maxPacketBytes 22 both
instances now show R3, and the tool's field dump stays byte-identical
to the baseline.
elrsinfo.lua becomes crsf_elrsinfo.lua, so SCRIPTS/ELRS holds crsf.lua,
crsf_fields.lua and crsf_elrsinfo.lua under one prefix, with shim.lua
as the only non-CRSF file. The singleton global (_elrsInfoSingleton)
and the module table are unchanged.

Verified: the telemetry widget loads /SCRIPTS/ELRS/crsf_elrsinfo.lua
and renders link stats on TX16S.
hasTelemetry(), isModelMismatch() and hasCriticalError() only returned
Protocol.connected / .modelMismatch / .criticalError, which the UIs and
main.lua already read as plain fields elsewhere. The call sites now use
those fields; a caller that needs a function-valued property (an LVGL
text/visible callback) can close over them at the point of use.

Verified on TX16S: the model_mismatch scenario still raises its dialog
and the subtitle still reports telemetry state.
Adds crsf_fields.lua to the shared-library table with the non-mutating
rule and the two receive models reassemble() serves, records the
protocol.lua row as policy over that engine, and lists the new
critical_error scenario and the maxPacketBytes chunking knob. README's
SD card tree picks up both renamed and new library files.
Commit f04caf2 replaced the per-type save wrappers with the codec's
sendWriteInt, but the B&W edit path kept calling Protocol.fieldIntSave,
so committing any edited value crashed the tool with "attempt to call
a nil value". Both numeric and text-selection edits go over
sendWriteInt, matching the LVGL UI.
poll() hands one data table to every handler registered for a frame
type, so the decoders must not write into it. The string reader now
collects chars into its own buffer instead of converting frame bytes
in place, which lets any handler decode the same frame independently
of registration order.
crsf_params.lua is a pure codec: reassembly runs over an explicit
caller-owned rx table ({ chunk, data, dataId, expect, done }) with the
device address passed in, and the frame builders return
(frameType, payload) for the caller to push instead of pushing
themselves. Callers no longer lend the codec a slice of their own
table -- the six flat reassembly keys and the addressing reads are
gone, and every send site is crsf.push(encode*(...)).
crsf_session.lua is a stateful CRSF parameter client (CRSFSession.new,
multi-instance): it owns the field store, the load queue, the paced
write queue, the command state machine, and optionally device
discovery, link status and ELRS 1.x detection. The overloaded
fieldTimeout becomes named per-purpose deadlines, and tick() documents
the send ladder it always implied: at most one parameter frame per
tick, command keep-alive > write drain > link-status > reads.

protocol.lua dissolves: the session absorbs its machinery, main.lua's
App keeps the policy residue (device switching, folder-ready edges,
the synthetic "Other Devices" row types), and both UIs talk to the
session surface -- writes go through writeField, which encodes by
field type and re-reads the related fields itself, so the codec no
longer appears in any UI.

Two deliberate hardenings over the old engine: the load-queue head
pops only when the decoded entry answers it, and CMD_IDLE dismisses
the popup only for the active command.
Each widget instance owns a CRSFSession in passive fan-out mode
(acceptUnsolicited): discovery re-derives from the session's field
store through the onFieldUpdate callback, writes go through the paced
write queue (STATE_SENDING exits when isWriting() clears), and the
bounded folder read-back is the session's refresh slot. The widget
keeps only its state machine, statusText and write policy.

Two session rules surfaced by running many instances on one bus: a
fan-out session must decode folder names fresh (no staleness flag can
cover a sibling's write rewriting the name -- non-folder names stay
cached, they are static), and the root-children auto-queue must only
trigger off the session's own read, or every sibling's root answer
multiplies the load traffic by the instance count.
Merge the widget's VTX and Protocol tables into a single VTXAdmin
component: the client of the ELRS "VTX Administrator" service, owning
the discovery state machine, current/desired VTX state, write policy,
and the 6POS quick-change and push-trigger automation behind one
tick(). The machine state is now "phase" -- VTXAdmin.state is the
parsed VTX state.

Around it, loadable.lua wires focused components: presets_storage.lua
is a pure settings store (preset slots, sources, flags, and their
key=value persistence), ui/display.lua carries the VTXDisplay
read-model and WidgetLayout builders, ui/fullscreen.lua the
full-screen editor, and the schema-free key=value file I/O joins the
shared ELRS library as file_storage.lua. Dependencies run one way:
FileStorage <- PresetsStorage <- VTXAdmin <- UI.

Behavior is unchanged: log lines, the hold-until-ready 6POS latch, the
push-trigger first-sample adoption and the presets.txt format all
survive verbatim, verified in the simulator against a pre-refactor
baseline (discovery, 6POS sweep, trigger edges, save/load roundtrip,
no-module, 11 instances).

The CRSF simulator mock rides along: it now builds folder-name
summaries only for the TX module and mimics the firmware's Fan Thresh
visibility rule (hidden while Dynamic power is off).
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.

3 participants