New LUA scripts & widgets - #3
Open
jurgelenas wants to merge 163 commits into
Open
Conversation
…ns for mk3 and 1 column for small screen radios
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.