diff --git a/README.md b/README.md
index c6f4b25..9412ebc 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,10 @@ Widgets require a GUI screen. Both widgets running side-by-side on the home scre
The telemetry widget (`ExpressLRS Telemetry`) displays real-time link statistics on your home screen. Larger widget sizes show more information and long press to open a fullscreen view showing all the telemetry items in a list.
+Black & white radios get the same telemetry as a telemetry screen: select the `ELRTLM` script on a page in Model Setup > Telemetry. The screen shows link quality, RSSI, RF mode, power and battery; press ENTER for the full list.
+
+
+
## VTX Administrator Widget
diff --git a/docs/development.md b/docs/development.md
index 1ca6a8c..294ceb0 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -60,14 +60,17 @@ The telemetry widget, `WIDGETS/ELRSTelemetry/`:
| Module | Purpose |
|--------|---------|
| `main.lua` | Entry point and the EdgeTX widget descriptor. Bootstraps the singletons into globals and resets the model-scoped state when the model changes |
-| `telemetry.lua` | Everything the widget knows about the link, in one owner: the frame drain, the DEVICE_INFO cache, the model-match status and the rules for when it may be asked for, the per-tick sensor snapshot, and every value derived from it. The only file in the widget that sees `crsf` |
-| `rf_modes.lua` | Packet-rate names and rated RSSI floors, keyed by the module's firmware major version. Pure data plus its selector, apart from the policy because it versions on ExpressLRS's release clock |
| `loadable.lua` | One per placed widget: picks the layout for the screen, wires the components, drives them from the widget callbacks. Owns no state |
| `ui/display.lua` | The read model: zero-argument formatters the layouts pass to LVGL as `text`/`color` callbacks, plus `WidgetLayout`. The whole vocabulary the view has |
| `ui/fullscreen.lua` | The full-screen page, one layout for every screen size. Loaded on first entry |
| `ui/.lua` | Minimized layout per screen size, each with its own breakpoints, fonts and height tiers |
| `ui/topbar.lua` | Top-bar layout, shared by every screen file |
+Its domain lives in the library so the B&W telemetry screen can share it: `SCRIPTS/ELRS/telemetry/state.lua`
+is the link state and `telemetry/rf_modes.lua` its rate tables. The B&W counterpart is the telemetry
+script `SCRIPTS/TELEMETRY/ELRTLM.lua`: it drains and samples from `background()`, `run()` draws
+`telemetry/lcd/dashboard.lua`, and ENTER loads the `telemetry/lcd/details.lua` list, released on exit.
+
`WIDGETS/ELRSVTXAdmin/` has the same shape. Its domain lives in the library so the B&W telemetry
screen can share it: `SCRIPTS/ELRS/vtx/admin.lua` is loaded once per instance (a config client, not
shared link state), and `SCRIPTS/ELRS/vtx/presets.lua` holds the 6POS presets, shared by every instance.
@@ -84,7 +87,7 @@ another; shared *presentation* goes in `ui/display.lua` instead. And no `ui/` fi
transport: the view asks the read model, which asks the domain.
The telemetry widget's state is shared by every instance of it, because it describes the radio's link
-rather than a widget. That costs three things, all stated in `telemetry.lua`'s header: `drain()` runs
+rather than a widget. That costs three things, all stated in `state.lua`'s header: `drain()` runs
per instance and ungated (each instance owns a firmware pop queue only it can empty), `update()`
samples at most once per tick (or the range smoother steps once per instance per frame), and the frame
handlers stay pure assignment (every instance is delivered its own copy of each frame, so the
@@ -106,6 +109,9 @@ The tools build on the shared `SCRIPTS/ELRS/` library, which the widgets use too
| `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` and the `REQUIRED_VERSIONS` dialog lines (`deps.requiredVersions`) to its UI chunk, whose `preCheck` owns the presentation. Keep `min_edgetx_version` in `edgetx.yml` in step |
| `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`. A cached ID addresses a slot in the model that was loaded when it was resolved, so a consumer that survives a model change must call `crsf.resetSensorCache()` on that edge |
| `SCRIPTS/ELRS/file_storage.lua` | Generic key=value file persistence (`read`/`write`), schema-free. Loaded by VTX Admin and the bind tool |
+| `SCRIPTS/ELRS/telemetry/state.lua` | Everything known about the link, in one owner: the frame drain, the DEVICE_INFO cache, the model-match status and the rules for when it may be asked for, the per-tick sensor snapshot, and every value derived from it. A singleton shared by every telemetry widget instance; loaded once by `ELRTLM` |
+| `SCRIPTS/ELRS/telemetry/rf_modes.lua` | Packet-rate names and rated RSSI floors, keyed by the module's firmware major version. Pure data plus its selector, apart from the policy because it versions on ExpressLRS's release clock |
+| `SCRIPTS/ELRS/telemetry/lcd/dashboard.lua`, `details.lua` | The telemetry screen's dashboard (LQ, bars, signal, power, battery) and its scrolling list of every reading |
| `SCRIPTS/ELRS/vtx/admin.lua` | VTX Admin domain: discovers the module's VTX Admin folder, parses its name into state, stages writes (`writeConfig`) and sends them (`pushToVtx`), and runs the 6POS and push-trigger automation. One per widget instance or telemetry script |
| `SCRIPTS/ELRS/vtx/presets.lua` | 6POS preset collections over `file_storage.lua`, persisted to `/SCRIPTS/ELRS/vtx/presets.txt` |
| `SCRIPTS/ELRS/vtx/lcd/dashboard.lua`, `menu.lua` | The VTX Admin telemetry screen's dashboard and its menu (VTX settings, 6POS Quick Change, Presets). Sources are picked by rotary or by moving the control |
@@ -116,7 +122,9 @@ the firmware delivers every widget instance its own copy of each incoming frame,
instance has exactly one draining consumer**: the config tool and the VTX Admin widget drain through
`session:drain()`, the telemetry widget through `Telemetry.drain()`, and the bind tool through
`crsf.drain(App, App.onFrame)`. A future widget needing two consumers must pop once and route the
-frames itself. `reassemble()` callers pass the field id they
+frames itself. B&W firmware has a single queue for every script instead, so there `crsf.lua` pops
+into a short shared log and hands each consumer its own copy through a per-consumer cursor; the
+telemetry scripts share one `crsf` through the `_crsfSingleton` global for that. `reassemble()` callers pass the field id they
are waiting for (strict), or `data[3]` to accept any field from their device (`acceptUnsolicited`,
used by VTX Admin so sibling instances stay in sync from each other's answers).
diff --git a/edgetx.yml b/edgetx.yml
index 37a71a1..7df26dd 100644
--- a/edgetx.yml
+++ b/edgetx.yml
@@ -16,6 +16,7 @@ package:
- screenshots/tool_bind.png
- screenshots/tool_bind_bw.png
- screenshots/widget_telemetry_fullscren.png
+ - screenshots/telemetry_bw.png
- screenshots/widget_vtxadmin_fullscreen.png
- screenshots/telemetry_vtxadmin_bw.png
- screenshots/widgets.png
@@ -62,6 +63,11 @@ widgets:
- ELRS
telemetry:
+ - name: ELRTLM
+ path: SCRIPTS/TELEMETRY/ELRTLM.lua
+ depends:
+ - ELRS
+
- name: ELRVTX
path: SCRIPTS/TELEMETRY/ELRVTX.lua
depends:
diff --git a/screenshots/telemetry_bw.png b/screenshots/telemetry_bw.png
new file mode 100644
index 0000000..65e3058
Binary files /dev/null and b/screenshots/telemetry_bw.png differ
diff --git a/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua b/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua
index f192d0e..aa0d011 100644
--- a/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua
+++ b/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua
@@ -95,8 +95,9 @@ local pwmChannelConfig = {
[4] = { inputChannel = 4, mode = 0, inverted = 0 },
}
--- Firmware gives each Lua state its own frame queue: a shared log with
--- per-consumer cursors, new consumers start at the tail.
+-- Colour firmware gives each widget its own frame queue: a shared log with
+-- per-consumer cursors, new consumers start at the tail. On B&W crsf.lua
+-- pops with one key, as there the firmware has a single queue.
local frameLog = {}
local logTotal = 0
diff --git a/src/SCRIPTS/ELRS/crsf.lua b/src/SCRIPTS/ELRS/crsf.lua
index a1401f8..703e918 100644
--- a/src/SCRIPTS/ELRS/crsf.lua
+++ b/src/SCRIPTS/ELRS/crsf.lua
@@ -3,7 +3,8 @@
-- --
-- Protocol constants, pop/push (with the simulator mock seam) and --
-- link-layer decoders. Each script instance has its own frame queue on --
--- colour radios; consumers drain theirs with CRSF.drain(). --
+-- colour radios; B&W radios have one, shared through a log here. --
+-- Consumers drain with CRSF.drain(). --
---------------------------------------------------------------------------
local shim = loadScript("/SCRIPTS/ELRS/shim.lua")()
@@ -87,11 +88,61 @@ CRSF.hasTelemetry = false
-- Transport (setMock() swaps these in the simulator)
-- ============================================================================
---- Pop one frame. consumer is only used by the simulator mock.
+-- B&W firmware has one queue for every script: consumers read a log of it
+-- through their own cursor, new ones from the tail
+local SHARED_QUEUE = lvgl == nil
+local LOG_MAX = 8 -- frames kept for a consumer that stopped draining
+local frameLog = {}
+local logHead = 0
+local logTail = 1
+local cursors = setmetatable({}, { __mode = "k" })
+
+local function pruneLog()
+ local oldest = logHead + 1
+ for _, nextIdx in pairs(cursors) do
+ if nextIdx < oldest then
+ oldest = nextIdx
+ end
+ end
+ oldest = math.max(oldest, logHead - LOG_MAX + 1)
+ for i = logTail, oldest - 1 do
+ frameLog[i] = nil
+ end
+ logTail = math.max(logTail, oldest)
+end
+
+local function sharedPop(consumer)
+ local nextIdx = math.max(cursors[consumer] or (logHead + 1), logTail)
+ if nextIdx > logHead then
+ local command, data = CRSF._popImpl(frameLog)
+ if command == nil then
+ cursors[consumer] = nextIdx
+ return nil
+ end
+ logHead = logHead + 1
+ frameLog[logHead] = { command, data }
+ end
+ cursors[consumer] = nextIdx + 1
+ local frame = frameLog[nextIdx]
+ pruneLog()
+ -- Copy: decoders convert strings in place
+ local data = {}
+ for i = 1, #frame[2] do
+ data[i] = frame[2][i]
+ end
+ return frame[1], data
+end
+
+--- Pop one frame for consumer.
-- Link state refreshes only on an empty pop, so frames land before the flip.
-- The TX zeroes RQly on disconnect.
function CRSF.pop(consumer)
- local command, data = CRSF._popImpl(consumer)
+ local command, data
+ if SHARED_QUEUE then
+ command, data = sharedPop(consumer)
+ else
+ command, data = CRSF._popImpl(consumer)
+ end
if command == nil then
CRSF.hasTelemetry = (CRSF.getSensorValue("RQly") or 0) > 0
end
diff --git a/src/SCRIPTS/ELRS/telemetry/lcd/dashboard.lua b/src/SCRIPTS/ELRS/telemetry/lcd/dashboard.lua
new file mode 100644
index 0000000..08ac25f
--- /dev/null
+++ b/src/SCRIPTS/ELRS/telemetry/lcd/dashboard.lua
@@ -0,0 +1,123 @@
+---------------------------------------------------------------------------
+-- B&W Telemetry Dashboard --
+---------------------------------------------------------------------------
+
+local Telemetry = ...
+
+local Dashboard = {}
+
+local Dialogs
+
+local TITLE_H = 9
+local BAR_H = 5
+local HERO_Y = 11
+local LQ_BAR_Y = 27
+local RSSI_Y = 34
+local RSSI_BAR_Y = 41
+local GRID_Y = 48
+local GRID_LINE = 8
+local MID = math.floor(LCD_W / 2)
+
+local function dash(v, text)
+ if v == nil or not Telemetry.isConnected() then
+ return "--"
+ end
+ return text
+end
+
+function Dashboard.drawTitle()
+ lcd.drawFilledRectangle(0, 0, LCD_W, TITLE_H)
+ lcd.drawText(1, 1, "ExpressLRS", INVERS)
+ local level = Telemetry.statusLevel()
+ local STATUS = Telemetry.STATUS
+ if level == STATUS.NO_TELEMETRY then
+ lcd.drawText(LCD_W - 1, 2, "No telemetry", SMLSIZE + INVERS + RIGHT)
+ elseif level == STATUS.MISMATCH then
+ lcd.drawText(LCD_W - 1, 2, "MISMATCH!", SMLSIZE + INVERS + RIGHT + BLINK)
+ elseif level == STATUS.OK then
+ lcd.drawText(LCD_W - 1, 2, Telemetry.rfModeName() or "", SMLSIZE + INVERS + RIGHT)
+ end
+end
+
+local function drawBar(y, pct)
+ lcd.drawRectangle(0, y, LCD_W, BAR_H)
+ if pct then
+ local w = math.floor((LCD_W - 2) * math.max(0, math.min(100, pct)) / 100)
+ if w > 0 then
+ lcd.drawFilledRectangle(1, y + 1, w, BAR_H - 2)
+ end
+ end
+end
+
+local function drawHero()
+ local link = Telemetry.link
+ local connected = Telemetry.isConnected()
+ local lq = connected and tostring(link.rqly or 0) or "--"
+ lcd.drawText(1, HERO_Y, lq, DBLSIZE)
+ lcd.drawText(36, HERO_Y + 8, "LQ %", SMLSIZE)
+ if not connected or not Telemetry.hasDiversity() then
+ return
+ end
+ local ant = Telemetry.activeAnt()
+ if Telemetry.isXband() then
+ lcd.drawText(LCD_W - 1, HERO_Y + 8, ant == 1 and "SubG" or "2.4", SMLSIZE + RIGHT)
+ else
+ lcd.drawText(LCD_W - 1, HERO_Y + 8, "Ant " .. ant, SMLSIZE + RIGHT)
+ end
+end
+
+local function signalText()
+ local link = Telemetry.link
+ local rssi = Telemetry.activeRssi()
+ if rssi == nil or not Telemetry.isConnected() then
+ return "--"
+ end
+ local text = tostring(rssi)
+ if Telemetry.hasDiversity() then
+ text = (link.rssi1 or "--") .. " " .. link.rssi2
+ end
+ if link.sens then
+ text = text .. " / " .. link.sens
+ end
+ return text .. "dBm"
+end
+
+local function cellText()
+ local vbat = Telemetry.link.vbat
+ local cells = Telemetry.cellCnt
+ if vbat == nil or vbat <= 0 or cells == nil then
+ return "--"
+ end
+ return string.format("%dS %.2fV", cells, vbat / cells)
+end
+
+local function drawCell(x, right, y, caption, text)
+ lcd.drawText(x, y, caption, SMLSIZE)
+ lcd.drawText(right, y, text, SMLSIZE + RIGHT)
+end
+
+local function drawGrid()
+ local link = Telemetry.link
+ local y2 = GRID_Y + GRID_LINE
+ drawCell(1, MID - 3, GRID_Y, "PWR", dash(link.tpwr, (link.tpwr or "") .. "mW"))
+ drawCell(1, MID - 3, y2, "BATT", cellText())
+ drawCell(MID + 2, LCD_W - 1, GRID_Y, "TQly", dash(link.tqly, (link.tqly or "") .. "%"))
+ drawCell(MID + 2, LCD_W - 1, y2, "TRSS", dash(link.trss, (link.trss or "") .. "dBm"))
+end
+
+function Dashboard.draw()
+ if not Telemetry.hasModule() then
+ Dialogs = Dialogs or loadScript("/SCRIPTS/ELRS/ui/lcd/dialogs.lua")()
+ Dialogs.drawNoModule()
+ return
+ end
+ lcd.clear()
+ Dashboard.drawTitle()
+ drawHero()
+ drawBar(LQ_BAR_Y, Telemetry.isConnected() and Telemetry.link.rqly)
+ drawCell(1, LCD_W - 1, RSSI_Y, "RSSI", signalText())
+ drawBar(RSSI_BAR_Y, Telemetry.headroomPct)
+ drawGrid()
+end
+
+return Dashboard
diff --git a/src/SCRIPTS/ELRS/telemetry/lcd/details.lua b/src/SCRIPTS/ELRS/telemetry/lcd/details.lua
new file mode 100644
index 0000000..8b455bc
--- /dev/null
+++ b/src/SCRIPTS/ELRS/telemetry/lcd/details.lua
@@ -0,0 +1,171 @@
+---------------------------------------------------------------------------
+-- B&W Telemetry Details --
+---------------------------------------------------------------------------
+
+local Telemetry, Dashboard = ...
+
+local ROW_H = 8
+local ROW_Y = 11
+local ROW_COUNT = math.floor((LCD_H - ROW_Y) / ROW_H)
+
+local link = Telemetry.link
+
+local function connected(v)
+ return v ~= nil and Telemetry.isConnected()
+end
+
+local function rfMode()
+ return Telemetry.isConnected() and Telemetry.rfModeName() or nil
+end
+
+local function lq()
+ return Telemetry.isConnected() and (link.rqly or 0) .. "%" or nil
+end
+
+local function rssi()
+ if not connected(link.rssi1) then
+ return nil
+ end
+ if Telemetry.hasDiversity() then
+ return link.rssi1 .. " / " .. link.rssi2 .. "dBm"
+ end
+ return link.rssi1 .. "dBm"
+end
+
+local function snr()
+ if not connected(link.rsnr) then
+ return nil
+ end
+ if not Telemetry.hasSnr() then
+ return "n/a" -- FLRC: the sensor is a permanent 0, not a reading
+ end
+ return link.rsnr .. "dB"
+end
+
+local function antLabel()
+ return Telemetry.isXband() and "Active Band" or "Active Antenna"
+end
+
+local function ant()
+ if not Telemetry.isConnected() then
+ return nil
+ end
+ if not Telemetry.hasDiversity() then
+ return "N/A"
+ end
+ if Telemetry.isXband() then
+ return Telemetry.activeAnt() == 1 and "SubG" or "2.4"
+ end
+ return "Ant " .. Telemetry.activeAnt()
+end
+
+local function sens()
+ return connected(link.sens) and link.sens .. "dBm" or nil
+end
+
+local function margin()
+ local db = Telemetry.marginDb()
+ return connected(db) and string.format("%+ddB", db) or nil
+end
+
+local function power()
+ return connected(link.tpwr) and link.tpwr .. "mW" or nil
+end
+
+local function pack()
+ local vbat = link.vbat
+ return vbat and vbat > 0 and string.format("%.2fV", vbat) or nil
+end
+
+local function cells()
+ local vbat, n = link.vbat, Telemetry.cellCnt
+ return vbat and vbat > 0 and n and string.format("%dS %.2fV", n, vbat / n) or nil
+end
+
+local function current()
+ local curr = link.curr
+ return curr and curr > 0 and string.format("%.2fA", curr) or nil
+end
+
+local function flightMode()
+ local fm = link.fm
+ return fm ~= nil and fm ~= 0 and tostring(fm) or nil
+end
+
+local function sats()
+ return connected(link.sats) and tostring(link.sats) or nil
+end
+
+local function speed()
+ return connected(link.gspd) and string.format("%.1f", link.gspd) or nil
+end
+
+local function alt()
+ return connected(link.alt) and tostring(link.alt) or nil
+end
+
+-- Last known position survives link loss
+local function lat()
+ return Telemetry.gps and tostring(Telemetry.gps.lat)
+end
+
+local function lon()
+ return Telemetry.gps and tostring(Telemetry.gps.lon)
+end
+
+local rows = {
+ { "Link Status" },
+ { "RF Mode", rfMode },
+ { "Link Quality", lq },
+ { "RSSI", rssi },
+ { "SNR", snr },
+ { antLabel, ant },
+ { "Sensitivity", sens },
+ { "Link Margin", margin },
+ { "Power" },
+ { "TX Power", power },
+ { "Flight Controller" },
+ { "Battery", pack },
+ { "Cells", cells },
+ { "Current", current },
+ { "Flight Mode", flightMode },
+ { "GPS" },
+ { "Satellites", sats },
+ { "Speed", speed },
+ { "Altitude", alt },
+ { "Latitude", lat },
+ { "Longitude", lon },
+}
+
+local MAX_OFFSET = math.max(0, #rows - ROW_COUNT)
+
+local offset = 0
+
+local function run(event)
+ if event == EVT_VIRTUAL_EXIT then
+ return true
+ elseif event == EVT_VIRTUAL_NEXT then
+ offset = math.min(offset + 1, MAX_OFFSET)
+ elseif event == EVT_VIRTUAL_PREV then
+ offset = math.max(offset - 1, 0)
+ end
+
+ lcd.clear()
+ Dashboard.drawTitle()
+ for i = 1, math.min(ROW_COUNT, #rows - offset) do
+ local row = rows[offset + i]
+ local y = ROW_Y + (i - 1) * ROW_H
+ local label = row[1]
+ if type(label) == "function" then
+ label = label()
+ end
+ if row[2] then
+ lcd.drawText(1, y, label)
+ lcd.drawText(LCD_W - 1, y, row[2]() or "--", RIGHT)
+ else
+ lcd.drawText(1, y, label, BOLD)
+ end
+ end
+end
+
+return { run = run }
diff --git a/src/WIDGETS/ELRSTelemetry/rf_modes.lua b/src/SCRIPTS/ELRS/telemetry/rf_modes.lua
similarity index 92%
rename from src/WIDGETS/ELRSTelemetry/rf_modes.lua
rename to src/SCRIPTS/ELRS/telemetry/rf_modes.lua
index 10852f2..8e18a67 100644
--- a/src/WIDGETS/ELRSTelemetry/rf_modes.lua
+++ b/src/SCRIPTS/ELRS/telemetry/rf_modes.lua
@@ -1,8 +1,5 @@
---------------------------------------------------------------------------
-- ELRS RF Mode Tables --
--- Loaded via loadScript() from ELRSTelemetry/telemetry.lua with no --
--- arguments; returns the RfModes table. --
--- --
-- Packet-rate names and sensitivity floors per ELRS major version. --
-- RFMD is 0-based; callers pass the raw sensor value. --
---------------------------------------------------------------------------
@@ -161,7 +158,7 @@ end
function RfModes.name(rfmd)
local names = RfModes._names
- return (names and names[rfmd + 1]) or table.concat({ "RFMD", tostring(rfmd) })
+ return (names and names[rfmd + 1]) or ("RFMD" .. rfmd)
end
function RfModes.floor(rfmd)
diff --git a/src/WIDGETS/ELRSTelemetry/telemetry.lua b/src/SCRIPTS/ELRS/telemetry/state.lua
similarity index 97%
rename from src/WIDGETS/ELRSTelemetry/telemetry.lua
rename to src/SCRIPTS/ELRS/telemetry/state.lua
index d84fc70..340a34f 100644
--- a/src/WIDGETS/ELRSTelemetry/telemetry.lua
+++ b/src/SCRIPTS/ELRS/telemetry/state.lua
@@ -1,15 +1,12 @@
---------------------------------------------------------------------------
-- ELRS Telemetry State --
--- Loaded via loadScript() from ELRSTelemetry/main.lua with (crsf); --
--- returns the Telemetry singleton. --
--- --
-- Shared by all widget instances. Each instance gets its own copy of --
-- every frame, so frame handlers must only assign, never count. --
---------------------------------------------------------------------------
local crsf = ...
-local RfModes = loadScript("/WIDGETS/ELRSTelemetry/rf_modes.lua")()
+local RfModes = loadScript("/SCRIPTS/ELRS/telemetry/rf_modes.lua")()
local Telemetry = {}
diff --git a/src/SCRIPTS/TELEMETRY/ELRTLM.lua b/src/SCRIPTS/TELEMETRY/ELRTLM.lua
new file mode 100644
index 0000000..0d3cd73
--- /dev/null
+++ b/src/SCRIPTS/TELEMETRY/ELRTLM.lua
@@ -0,0 +1,40 @@
+---------------------------------------------------------------------------
+-- ExpressLRS Telemetry - B&W telemetry screen --
+-- Requires /SCRIPTS/ELRS on the SD card. --
+---------------------------------------------------------------------------
+
+---@diagnostic disable-next-line: need-check-nil
+local loader = loadScript("/SCRIPTS/ELRS/loader.lua")()
+
+-- One per Lua state: B&W scripts share the frame queue through it
+_crsfSingleton = _crsfSingleton or loader("/SCRIPTS/ELRS/crsf.lua")
+local crsf = _crsfSingleton
+crsf.resetSensorCache() -- outlives model changes
+local Telemetry = loader("/SCRIPTS/ELRS/telemetry/state.lua", crsf)
+local Dashboard = loader("/SCRIPTS/ELRS/telemetry/lcd/dashboard.lua", Telemetry)
+local Details
+
+local function background()
+ Telemetry.drain()
+ Telemetry.update()
+end
+
+local function run(event)
+ if Details then
+ if Details.run(event) then
+ Details = nil
+ collectgarbage("collect")
+ Dashboard.draw()
+ end
+ return 0
+ end
+ if event == EVT_VIRTUAL_ENTER and Telemetry.hasModule() then
+ Details = loader("/SCRIPTS/ELRS/telemetry/lcd/details.lua", Telemetry, Dashboard)
+ Details.run(0)
+ return 0
+ end
+ Dashboard.draw()
+ return 0
+end
+
+return { run = run, background = background }
diff --git a/src/SCRIPTS/TELEMETRY/ELRVTX.lua b/src/SCRIPTS/TELEMETRY/ELRVTX.lua
index 4d68805..9bd4035 100644
--- a/src/SCRIPTS/TELEMETRY/ELRVTX.lua
+++ b/src/SCRIPTS/TELEMETRY/ELRVTX.lua
@@ -6,7 +6,10 @@
---@diagnostic disable-next-line: need-check-nil
local loader = loadScript("/SCRIPTS/ELRS/loader.lua")()
-local crsf = loader("/SCRIPTS/ELRS/crsf.lua")
+-- One per Lua state: B&W scripts share the frame queue through it
+_crsfSingleton = _crsfSingleton or loader("/SCRIPTS/ELRS/crsf.lua")
+local crsf = _crsfSingleton
+crsf.resetSensorCache() -- outlives model changes
local params = loader("/SCRIPTS/ELRS/crsf_params.lua", crsf)
local CRSFSession = loader("/SCRIPTS/ELRS/crsf_session.lua", crsf, params)
local FileStorage = loader("/SCRIPTS/ELRS/file_storage.lua")
diff --git a/src/WIDGETS/ELRSTelemetry/main.lua b/src/WIDGETS/ELRSTelemetry/main.lua
index 14bdf45..5e05554 100644
--- a/src/WIDGETS/ELRSTelemetry/main.lua
+++ b/src/WIDGETS/ELRSTelemetry/main.lua
@@ -17,7 +17,7 @@ local function create(zone, options)
_crsfSingleton = getCRSF()
end
if not _elrsTelemetrySingleton then
- local getTelemetry = loadScript(table.concat({ "/WIDGETS/", name, "/telemetry.lua" }))
+ local getTelemetry = loadScript("/SCRIPTS/ELRS/telemetry/state.lua")
---@diagnostic disable-next-line: need-check-nil
_elrsTelemetrySingleton = getTelemetry(_crsfSingleton)
end