New LUA scripts & widgets - #3
Open
jurgelenas wants to merge 229 commits into
Open
Conversation
…ns for mk3 and 1 column for small screen radios
|
Just tested the B&W bind phrase script on RM Pocket, worked great. Using it to switch between trainer bindphrase and plane/quad bindphrase. |
The module recomputes the flag as it answers and never sends an ELRS_STATUS unasked, so the one-answer-per-connection latch only reset on a hasTelemetry edge -- and every way out of a mismatch is applied over a link the handset never sees drop. The warning stayed on screen for the rest of the session. The request rules now read as one predicate, canRequestStatus(): connected, ELRS module, no answer yet or the last one was a mismatch, RSSI above -70, at most once per second. Only the third rule changed. The RSSI gate still covers the retry, so a mismatch fixed out at range keeps its banner until the link drops. ELRSTelemetry create() drops the verdict too: instances are rebuilt per model, but the singleton they share is not. The new dev-only mismatch_recovery scenario clears the flag with RQly held at 95, which no existing scenario did. Over 45 s with 5 telemetry + 12 VTX instances: normal and weak_link still write zero status requests, mismatch_recovery 11 at 1 Hz and then silence.
crsf_elrsinfo.lua was written on the assumption that TX-module state would be useful to more than one consumer. It never was: every one of its five API members has exactly one reader, and that reader is the telemetry widget. Keeping it in SCRIPTS/ELRS taxed every reader of the shared library with a module that is not shared, and the library table already had to say "loaded only by the telemetry widget" to explain it. Pure path move. It stays a singleton behind the _elrsInfoSingleton global in main.lua, because its poll rate limits are one set of counters: loaded per instance it would ping and ask for model match once per second per placed widget, and every push costs an RC-channels frame.
The Latitude and Longitude rows on the telemetry widget's full-screen page have never rendered anything but "--" under the simulator: no scenario defined a GPS sensor, so Telemetry.gps stayed nil and the one piece of state the widget deliberately caches across dropouts was the one piece nothing exercised. Added to the six scenarios that already carry the rest of the GPS group (Sats/GSpd/Alt), each with a distinct position so which scenario is running is readable off the screen. GPS stays out of sensorJitter, like the other non-moving sensors, so screenshots remain stable.
The packet-rate names and their rated sensitivity floors are 140 lines of literals that sat between update() and the frame handlers, so neither the state machine nor the data could be read top to bottom. They also version on ExpressLRS's release clock, where everything around them versions on the link-etiquette clock. RFMOD/RFRSSI stop being public fields of deviceInfo. The +1 that turns the 0-based RFMD sensor into a 1-based Lua index now lives next to the literals that define the convention, behind RfModes.name() and RfModes.floor(), and the "RFMD<n>" fallback with it. Not a memory saving -- both literal sets stay in select()'s constant pool. It stays eagerly loaded: a loadScript inside the DEVICE_INFO handler would put a file open and a compile in a frame handler at the moment the link comes up.
loadable.lua was four layers in one file and the state it drove was split across the library boundary, which had duplicated it: the same tick sampled ANT/1RSS/2RSS twice, picked the active antenna through two functions that did the same thing, and tracked the connection edge in two latches. Three restatements of the same status ladder had drifted apart, and each of the five screen files carried its own byte-identical detailColor() and heroTextLq() -- so a colour rule could be changed on four screens out of five. telemetry.lua now owns everything that describes the link: the drain, the device cache, the model-match policy, the sensor snapshot and every value derived from it. It is one owner because all of it describes the radio, not a widget instance -- two widgets on one screen were two independently sampled copies that could disagree within a frame. ui/display.lua is the read model the layouts consume, so the view no longer reaches past it: crsf is now a vararg of exactly one file in the widget, which makes the one-drainer rule something grep can check. Sharing the snapshot needs two guarantees, both in the code and both stated in the headers. update() samples at most once per tick, or the range smoother would step once per instance per frame and settle N times faster on a screen with N widgets; drain() stays per instance and ungated, because each instance owns a firmware pop queue only it can empty. It also needs a reset contract, which replaces one that used to be half explicit and half accidental. The singleton outlives a model change where the old per-instance table did not, so resetModel() clears what belongs to the model -- the GPS position above all, which nothing ever cleared and which reads as a live fix. It is keyed on the model name rather than on create() alone, because create() also runs when a widget is placed, and adding a second telemetry widget must not throw away the cell count the first one locked on to. The full-screen page now reads the snapshot instead of sampling five sensors at paint time, so every row on it is from the same instant.
The page was ~260 lines inline in loadable.lua, which shared nothing with the wiring around it. loadable.lua is 110 lines now and does what its name says: pick the layout, wire the components, drive them from the widget callbacks. The eight repeated "-- while the link is down" guards collapse into one whenConnected() wrapper. Which rows take it is deliberate and unchanged: only the link and power rows. The flight controller and GPS rows keep their own fallbacks, and latitude/longitude keep showing the last known position after the link drops. Loaded on first entry rather than with the widget, since only one widget can be full screen at a time and update() is not a hot path. The header describes when EdgeTX actually calls update() -- on construction, on entering and leaving full screen, on an options edit -- rather than claiming a rebuild policy the code does not have.
Only one widget can be full screen at a time, so a page builder loaded with the widget is resident in every instance that never shows one -- and VTX Admin is the widget users place many of. update() is not a hot path, so waiting until it is asked for costs nothing. Matches the telemetry widget, so the two keep the same shape.
It sat directly under TX Power showing the same fact in another unit, and it was not the module's own index: a linear scan over a hardcoded power table that answered 7 for anything it did not recognise, so a module with a power level outside the list reported the top index. TX Power in milliwatts is the number that means something to a pilot.
getFieldInfo returns an ID that encodes a slot in the current model's sensor list -- api_general.cpp resolves it as g_model.telemetrySensors[(id - MIXSRC_FIRST_TELEM) / 3] -- but the cache keyed on the sensor name lives in the CRSF singleton, which is a global in the widget Lua state and so outlives every model change. So "RQly" resolved at slot 3 under one model kept addressing slot 3 under the next, where it may be a different sensor entirely, for the rest of the session. Nothing invalidated it, because until now nothing in the widget observed the model-change edge. The telemetry widget now has that edge, in resetModel(), so the cache is dropped there. Pre-existing; only visible after switching models with differently ordered sensor lists.
Four things that read as copy-paste divergence rather than intent: borderPad = 0 on the 1/4 tier's rows was present on sd and hd and absent on small and sd_tall, with nothing explaining why the same tier should be padded differently on two screens out of four. Now on all four. portrait keeps one, because its second row is a bare label rather than a box. small.lua read WidgetUI.fonts.<tier>.hero directly where its siblings go through heroFont(), so a heroStatus added to its font table would have been silently ignored -- the shrink-while-a-status-shows contract would not have held on that screen alone. hd.lua wrote a bare 0 for a font where every sibling writes the name. It is STDSIZE; VTX Admin's hd.lua had the same wart next to a correct STDSIZE in the same table, which is what makes it a slip rather than a choice. portrait.lua's appendDataRows said it was shared by the 1/2 and 1/1 tiers, but portrait has no halfH breakpoint and no buildHalf. Verified: sd on TX16S and sd_tall on Jumper T15 both still render.
The architecture section tabled both tools and every library module but said nothing about the widgets at all, and the README elided their ui/ trees behind a "...". The layering the widgets actually use -- entry point, domain, wiring, read model, full-screen page, per-screen layouts -- was only discoverable by reading the loadScript calls. Records the two rules that hold across both widgets: the per-screen files are self-contained so shared presentation belongs in ui/display.lua, and no ui/ file receives the CRSF transport. Also records what sharing the telemetry state costs, so the next person to touch drain(), update() or a frame handler knows which of the three invariants they are standing on.
The rate tables carry a literal 0 for the rates ExpressLRS publishes no sensitivity figure for, and RfModes.floor returned it unchanged. Its own docstring says otherwise, and so does every caller: telemetry.lua guards with `or -128`, which 0 -- a truthy number -- walks straight past. The range percentage then divided by (0 + 50) on nine of the v4 rates and two of the v3 ones.
Range % answered a question nobody asks: it counted *down* from a full bar as the link got better, and it was a percentage of a distance that was never measured. What a pilot wants is how much signal is left before the receiver stops hearing, and ExpressLRS publishes exactly that -- a rated sensitivity floor per packet rate, already in rf_modes.lua. So the smoother now produces headroomPct with the polarity the right way up, the detail line prints the pair the comparison needs (-93 / -108 dBm) instead of an invented percentage, and its warning colour is driven by link margin in dB -- the unit the number is actually measured in -- on the same thresholds range % used. The full-screen page gains Sensitivity and Link Margin rows directly under the RSSI ones, sharing their unit so the arithmetic between the three is visible. Also samples TQly and TRSS, the downlink pair, and promotes the status ladder out of Display.statusText() into Telemetry.statusLevel(), so the words and the colours to come index one value rather than two copies of the same if-chain.
TQly and TRSS are the RX->TX telemetry path, and nothing could exercise them because no scenario served them. They sit a few dB behind the uplink everywhere, which is the asymmetry worth showing: the receiver answers at a fraction of the module's power. The unrated_rate scenario reaches the other untested branch. Every connected scenario ran RFMD 7, which is rated, so the fallback for a packet rate ExpressLRS publishes no sensitivity figure for was unreachable in the simulator -- which is how RfModes.floor came to return 0 for it unnoticed.
The minimized widget was four left-aligned labels on a flat rectangle, with no bar, gauge, rule or indicator anywhere in src/WIDGETS. This gives the 1/1 tier the instruments the data has always deserved: a status LED, the uplink as a headline LQ over two full-width bars, a stepped TX power meter, antenna cells, and a captioned row for the downlink and battery. The uplink is deliberately unheadered. It is what you fly on -- a degraded uplink loses control where a degraded downlink only makes the numbers stale -- so it takes the upper body in a register nothing else competes with, and the downlink is one captioned row underneath. ui/components.lua carries the drawing and the composition, so each screen file's buildFull is the four lines that differ: the hero font and the width gate. Everything is label, rectangle and circle, the only types lvgl.build() will accept in a widget zone, and nothing calls build() until the tree is complete. Two things measured rather than assumed. Font line heights come from lcd.sizeText, which still works under LVGL, because the 800x480 target ships different fonts and a scaled 480x272 number would be wrong there. And whether the downlink and battery groups share a row is decided by their measured widths, not a breakpoint -- on the same TX16S the answer differs between a 396px zone and a 198px one, and the ladder takes the dBm unit off first, then the pack total, then splits the rows, and only then drops the DOWNLINK header, which it can do at all because TQly and TRSS name themselves.
GREEN and RED are raw primaries -- RGB(0,255,0) and RGB(255,0,0), colors.cpp:51-54 -- so a full-width LQ bar in GREEN was the loudest thing on the screen, and it stayed that way whatever theme the pilot had chosen. The theme carries the same three meanings in colours picked to sit together: EDIT, ACTIVE and WARNING. Bars, the status LED and the LQ headline now take those and repaint with the theme. Text keeps a separate ramp. COLOR_THEME_ACTIVE is a bright yellow, which is fine as a bar fill and illegible as a word on the light themes' near-white panel, so warnings in text stay ORANGE. Bar tracks are the disabled grey at part opacity. Solid it read as a second bar competing with its own fill; the theme's soft secondary disappeared into the panel entirely, which left the fill a floating stub with nothing to measure it against. The 1/2 tier keeps both bars, which is the point of the layout -- it is the size the widget is usually placed at, and the mirrored-blocks draft this replaced could not keep even one there. The battery row drops before either bar is squeezed below the height at which it stops being a bar.
TQly sits beside TRSS on the same row in percent, so a bare -94 there invites reading it as one too. The unit is not something the width ladder gets to trade away. It was the first thing to go, which left the narrowest rung showing "TRSS -94". Now the pack total goes first, then the row splits, and the DOWNLINK header is what drops last -- the right order, because TQly and TRSS name themselves and a bare number does not name its unit. Costs nothing at the sizes that matter: the 396px zone still fits the row under its header, and the 198px one reaches the headerless rung it was already using.
Both were COLOR_THEME_PRIMARY1, which is the text colour -- RGB(0,0,0) on the light themes. As a glyph that is correct and as a filled block it is a black bar dropped onto a blue panel, which is what they looked like. COLOR_THEME_FOCUS is what EdgeTX uses everywhere else to mean "this one is active", which is exactly what a lit power cell and the live antenna cell are saying, and it follows the theme. Power deliberately stays off the green/amber/red ramp: 500 mW is not worse than 50, so a health colour there would be claiming something the number does not mean. The mismatch banners move from RED to COLOR_THEME_WARNING with them, so the whole widget now draws from the theme. The single exception is ORANGE for warning text, because COLOR_THEME_ACTIVE is a yellow that cannot be read on a near-white panel.
Four adjustments from looking at it on the radio rather than in the plan. The status LED was centred on the strip height, which is the text's line box plus padding, so it sat below the RF mode beside it -- near enough to read as a mistake rather than a choice. It now centres on the line box, landing on the same centre as the antenna cells, and it is a third smaller: at half the row height an indicator stops being a dot and starts competing with the text. It also gets a fixed lane, so its radius can be tuned without the RF mode moving sideways. TX POWER's meter was absorbing the whole gap between the RF mode and the antenna cells, which dragged its own reading to the far side of the strip with nothing tying the two together. The meter now has a natural width measured off the row, the reading sits a fixed gap after it, and the slack stays to their right. Whether the meter fits at all is now measured against the antenna cells' position instead of assumed from the width gate. The panel is inset from the zone rather than filling it, so two widgets side by side no longer share one edge, and the content is inset again from the panel's own border -- full-width bars starting and ending on the border read as spilling out of the panel. And at half width TX POWER moves above the uplink panel instead of below the group rows. It is link configuration, like the RF mode and the antenna it now sits with; last place filed it under the flight controller's readings, which is not what it is.
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.