diff --git a/README.md b/README.md
index e45e133..3539d27 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 a85edaf..f2afc33 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,11 +87,13 @@ 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
-singleton decodes each one N times on a radio and once in the simulator).
+singleton decodes each one N times on a radio and once in the simulator). ELRTLM doesn't use the
+singleton: it loads its own copy of `state.lua`, which the firmware drops with the script on every
+model load, so only the frame-handler rule applies there.
The tools build on the shared `SCRIPTS/ELRS/` library, which the widgets use too:
@@ -106,6 +111,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.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. Shared by every telemetry widget instance through `_elrsTelemetrySingleton`; `ELRTLM` loads its own copy |
+| `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 +124,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 ea65d06..156801e 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 282e0c1..9cd4c9a 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 94%
rename from src/WIDGETS/ELRSTelemetry/telemetry.lua
rename to src/SCRIPTS/ELRS/telemetry/state.lua
index d84fc70..178d5d0 100644
--- a/src/WIDGETS/ELRSTelemetry/telemetry.lua
+++ b/src/SCRIPTS/ELRS/telemetry/state.lua
@@ -1,15 +1,14 @@
---------------------------------------------------------------------------
-- 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. --
+-- The colour widget's instances share one copy through --
+-- _elrsTelemetrySingleton, and each instance gets its own copy of every --
+-- frame, so frame handlers must only assign, never count. The B&W --
+-- ELRTLM script loads its own copy, fresh on every model load. --
---------------------------------------------------------------------------
local crsf = ...
-local RfModes = loadScript("/WIDGETS/ELRSTelemetry/rf_modes.lua")()
+local RfModes = loadScript("/SCRIPTS/ELRS/telemetry/rf_modes.lua")()
local Telemetry = {}
@@ -297,8 +296,8 @@ end
-- Per-tick pump
-- ============================================================================
--- 30 ms: dedupes instances within one 50 ms widget loop; else smoothing
--- speeds up per widget
+-- 30 ms: dedupes the colour widget's instances within one 50 ms loop;
+-- else smoothing speeds up per widget
local SAMPLE_INTERVAL = 3
-- 1 or 2; nil while the base is unknown
@@ -400,13 +399,14 @@ function Telemetry:_onFrame(command, data)
end
end
--- Every instance, every frame: each has its own queue that would overflow
+-- Every widget instance, every frame: each has its own queue that would
+-- overflow. On B&W this reads the shared log through ELRTLM's cursor.
function Telemetry.drain()
crsf.drain(Telemetry, Telemetry._onFrame)
end
-- ============================================================================
--- Return singleton
+-- Return module
-- ============================================================================
return 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