|
1 | 1 | # Development |
2 | 2 |
|
3 | | -This document covers the internal architecture of the ExpressLRS configuration tool and the CRSF simulator used for testing inside the EdgeTX simulator without real hardware. |
| 3 | +This document covers the internal architecture of the ExpressLRS tools and the CRSF simulator used for testing inside the EdgeTX simulator without real hardware. |
4 | 4 |
|
5 | 5 | ## Make targets |
6 | 6 |
|
@@ -32,30 +32,61 @@ Run `make help` to list all targets. The Makefile groups them into three categor |
32 | 32 |
|
33 | 33 | ## Architecture |
34 | 34 |
|
| 35 | +The configuration tool, `SCRIPTS/TOOLS/ExpressLRS/`: |
| 36 | + |
35 | 37 | | Module | Purpose | |
36 | 38 | |--------|---------| |
37 | 39 | | `main.lua` | Entry point, run-loop orchestrator, and the App policy layer (device switching, folder-ready edges, the synthetic "Other Devices" row types) over a `crsf_session.lua` instance | |
38 | 40 | | `navigation.lua` | Folder and device navigation stack | |
39 | 41 | | `ui/lvgl.lua` | Color LCD interface (LVGL dialogs, command pages, warnings) | |
40 | 42 | | `ui/lcd.lua` | BW LCD interface (text cursor, popups) | |
41 | 43 |
|
42 | | -The tool builds on the shared `SCRIPTS/ELRS/` library, which the widgets use too: |
| 44 | +The bind phrase manager, `SCRIPTS/TOOLS/ExpressLRSBind/` (sets the bind phrase / UID over MSP; |
| 45 | +the device side requires ExpressLRS 4.1+, and a pre-4.1 device simply never answers -- the tool's |
| 46 | +bounded UID retry reports that instead of polling forever). |
| 47 | + |
| 48 | +Only the transmitter's UID is read, and only on the events that can change it: the tool opening, a |
| 49 | +write completing, and the target selector moving. The receiver is never asked. It answers over the |
| 50 | +link, a link only exists between devices already sharing a UID, so its answer is the transmitter's |
| 51 | +number by construction; the link's presence carries the whole of what asking would have told us, and |
| 52 | +costs no over-air traffic. That is also why there is no manual refresh -- every event a "read UID" |
| 53 | +button existed to cover is one the App observes for itself: |
| 54 | + |
| 55 | +| Module | Purpose | |
| 56 | +|--------|---------| |
| 57 | +| `main.lua` | Entry point and the App layer: target selection (TX/RX/Both), phrase-vs-raw-UID parsing, the two-step Both sequence (RX first -- writing its phrase drops it off the link -- then TX), the bounded transmitter UID probe, and the frame router over `crsf.drain` | |
| 58 | +| `history_storage.lua` | The last five phrases, newest first, persisted through `file_storage.lua` as indexed keys `h1`..`h5` | |
| 59 | +| `ui/lvgl.lua` | Color LCD interface | |
| 60 | +| `ui/lcd.lua` | BW LCD interface (line list, phrase editing through `ui/lcd/text_edit.lua`). Note `popupConfirmation`'s message argument never reaches the screen on BW: EdgeTX's Lua binding sets `warningInfoText` without `warningInfoLength` (`api_general.cpp` `luaPopupConfirmation`), so a confirmation has only its 24-char title (`WARNING_LINE_LEN`) to say what it needs | |
| 61 | + |
| 62 | +Both tools pick their UI chunk at runtime -- `local useLvgl = (lvgl ~= nil)` -- and load exactly one |
| 63 | +of `ui/lvgl.lua` or `ui/lcd.lua`; there are no per-radio builds. |
| 64 | + |
| 65 | +The tools build on the shared `SCRIPTS/ELRS/` library, which the widgets use too: |
43 | 66 |
|
44 | 67 | | Module | Purpose | |
45 | 68 | |--------|---------| |
46 | 69 | | `SCRIPTS/ELRS/crsf.lua` | CRSF constants, telemetry transport (`pop`/`drain`/`push`), module detection, derived link state (`hasTelemetry`, refreshed as a drain empties the queue), stateless frame decoders (`decodeDeviceInfo`, `decodeElrsStatus`, `isElrsV1Frame`) | |
47 | 70 | | `SCRIPTS/ELRS/crsf_params.lua` | Opt-in parameter codec: `PARAMETER_SETTINGS_ENTRY` chunk reassembly over a caller-owned rx table and per-type decode, plus encoders that return `PARAMETER_READ`/`WRITE`, command-step and suppress-critical-errors frames for the caller to push. Loaded by the tool and the VTX Admin widget | |
48 | 71 | | `SCRIPTS/ELRS/crsf_session.lua` | Opt-in stateful parameter client (`CRSFSession.new`, multi-instance): field store, load queue and retry scheduler, paced write queue, command state machine, and optional device discovery, link status and ELRS 1.x detection. Loaded by the tool and the VTX Admin widget | |
49 | 72 | | `SCRIPTS/ELRS/crsf_elrsinfo.lua` | Opt-in TX-module state: DEVICE_INFO cache, version-keyed RFMOD/RFRSSI tables, per-connection model-match latch. Loaded only by the telemetry widget | |
| 73 | +| `SCRIPTS/ELRS/msp.lua` | Opt-in MSP-over-CRSF codec: stateless encoders returning `(frameType, payload)` for `MSP_REQ`/`MSP_WRITE` and decoders for single-frame v1 `MSP_RESP`, plus the ELRS `RXTX_CONFIG` UID/phrase helpers. Loaded only by the bind tool | |
| 74 | +| `SCRIPTS/ELRS/defer.lua` | Single-slot `setTimeout`/`poll` timer; scheduling replaces the pending callback, which is what cancels a stale retry when a new action starts. Loaded only by the bind tool | |
| 75 | +| `SCRIPTS/ELRS/ui/lcd/text_edit.lua` | BW text editor replicating the firmware's `editName()` model-name semantics (rotary cycles the char, ENTER advances, long ENTER toggles case or commits on a space). Loaded only by the bind tool's BW UI. `ui/<display>/` is the library's home for shared UI components, mirroring the tools' own `ui/` split | |
| 76 | +| `SCRIPTS/ELRS/ui/lcd/alert.lua` | BW full-screen alert (MIDSIZE title, body lines, optional bottom action labels). Loaded by both tools' BW UIs | |
| 77 | +| `SCRIPTS/ELRS/ui/lvgl/dialogs.lua` | The color-LCD startup dialogs a tool can raise before it has a page -- the version gate and the missing-module notice. Both are terminal, so each takes the caller's `onExit` for the close box and the Exit button. Loaded by both tools' LVGL UIs | |
| 78 | +| `SCRIPTS/ELRS/loader.lua` | The tools' GC-guarded script loader: a full collection before each `loadScript` keeps fresh-install compile peaks from stacking. The one part consumers bootstrap with a bare `loadScript` | |
| 79 | +| `SCRIPTS/ELRS/edgetx_version.lua` | The one home of the minimum EdgeTX requirement (2.11.6 / 2.12.1 / 3.0). Each tool's `main.lua` checks it once and hands `deps.versionOk` to its UI chunk, whose `preCheck` owns the presentation. Keep `min_edgetx_version` in `edgetx.yml` in step | |
50 | 80 | | `SCRIPTS/ELRS/sensors.lua` | Generic EdgeTX telemetry reader (`getSensorValue` with a cached name-to-ID lookup), not CRSF-specific. Loaded by `crsf.lua`, which exposes it to every consumer as `crsf.getSensorValue` | |
51 | | -| `SCRIPTS/ELRS/file_storage.lua` | Generic key=value file persistence (`read`/`write`), schema-free. Loaded only by the VTX Admin widget | |
| 81 | +| `SCRIPTS/ELRS/file_storage.lua` | Generic key=value file persistence (`read`/`write`), schema-free. Loaded by the VTX Admin widget and the bind tool | |
52 | 82 | | `SCRIPTS/ELRS/shim.lua` | `table.concat` polyfill for BW radios | |
53 | 83 |
|
54 | 84 | Frames are consumed pull-style. `crossfireTelemetryPop()` is destructive per script instance, and |
55 | 85 | the firmware delivers every widget instance its own copy of each incoming frame, so **each script |
56 | | -instance has exactly one draining consumer**: the tool and the VTX Admin widget drain through |
57 | | -`session:drain()`, the telemetry widget through `elrsinfo:drain()`. A future widget needing two |
58 | | -consumers must pop once and route the frames itself. `reassemble()` callers pass the field id they |
| 86 | +instance has exactly one draining consumer**: the config tool and the VTX Admin widget drain through |
| 87 | +`session:drain()`, the telemetry widget through `elrsinfo:drain()`, and the bind tool through |
| 88 | +`crsf.drain(App, App.onFrame)`. A future widget needing two consumers must pop once and route the |
| 89 | +frames itself. `reassemble()` callers pass the field id they |
59 | 90 | are waiting for (strict), or `data[3]` to accept any field from their device (`acceptUnsolicited`, |
60 | 91 | used by VTX Admin so sibling instances stay in sync from each other's answers). |
61 | 92 |
|
@@ -96,6 +127,18 @@ The simulator supports multiple test scenarios, configurable via the `config.sce |
96 | 127 | | `no_module` | No CRSF module found. Triggers "No Module Found" error dialog. | |
97 | 128 | | `critical_error` | TX + RX connected with a critical baud-rate error flag. Triggers the warning screen; the suppress write clears it. | |
98 | 129 |
|
| 130 | +### MSP bind traffic |
| 131 | + |
| 132 | +The mock answers the bind tool's MSP `RXTX_CONFIG` traffic: a UID read (`MSP_REQ`) is served from a |
| 133 | +per-device `mspUid` table, and a phrase write (`MSP_WRITE`) rederives the target's UID through a |
| 134 | +deterministic pseudo-hash, so equal phrases give equal UIDs (the property the Both flow |
| 135 | +demonstrates; the bytes need not match the firmware's MD5). Both devices start on the same UID, |
| 136 | +because a reachable receiver whose UID differs from the transmitter's is a state the radios cannot |
| 137 | +be in. The RX answers only while the scenario keeps it reachable, and writes are unacknowledged just |
| 138 | +like the real firmware. `FRAMETYPE_COMMAND` bind requests are log-only -- note that the firmware |
| 139 | +handles that command identically at either address (`EnterBindingModeSafely`), so the "unbind" |
| 140 | +button puts the receiver into bind mode rather than erasing its binding. |
| 141 | + |
99 | 142 | `config.maxPacketBytes` (default 64, `CRSF_MAX_PACKET_LEN`) is the largest frame the mock handset |
100 | 143 | link carries. Parameter entries longer than `maxPacketBytes - 8` are chunked exactly as |
101 | 144 | `CRSFEndpoint::sendParameter` does, so lowering it -- real firmware shrinks it on slow baud rates in |
|
0 commit comments