diff --git a/docs/development.md b/docs/development.md index 99bb2b4..41b64b2 100644 --- a/docs/development.md +++ b/docs/development.md @@ -143,6 +143,7 @@ The simulator supports multiple test scenarios, configurable via the `config.sce | `model_mismatch` | TX + RX connected with Model ID mismatch flag. Triggers warning dialog. | | `armed` | TX + RX connected with "is Armed" warning flag. `ANT` is pinned to 0. | | `single_antenna` | TX + RX connected on a receiver with one RF path. `2RSS` is pinned to 0, so the telemetry widget reports no diversity. | +| `flrc` | TX + RX connected at F1000. FLRC carries no SNR, so `RSNR` is a permanent 0 and the full-screen SNR row reads "n/a". | | `slow_loading` | Parameter reads delayed by ~2 seconds each. Tests loading UI states. | | `no_module` | No CRSF module found. Triggers "No Module Found" error dialog. | | `critical_error` | TX + RX connected with a critical baud-rate error flag. Triggers the warning screen; the suppress write clears it. | diff --git a/screenshots/widget_telemetry_fullscren.png b/screenshots/widget_telemetry_fullscren.png index b7d58df..6db88d0 100644 Binary files a/screenshots/widget_telemetry_fullscren.png and b/screenshots/widget_telemetry_fullscren.png differ diff --git a/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua b/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua index 26fbd91..b72751c 100644 --- a/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua +++ b/src/SCRIPTS/CRSFSimulator/csrfsimulator.lua @@ -44,6 +44,9 @@ local shim = loadScript("/SCRIPTS/CRSFSimulator/shim.lua")() -- Shows armed warning in subtitle. -- "single_antenna" RX with a single RF path: 2RSS pinned to 0, so the -- widgets report no diversity. +-- "flrc" TX + RX connected at F1000. FLRC carries no SNR, so +-- RSNR is a permanent 0 and an SNR reading must say so +-- rather than print 0 dB. -- "unrated_rate" TX + RX connected on a packet rate ExpressLRS publishes -- no receiver sensitivity for. There is no floor to -- measure RSSI against, so anything drawn against one has @@ -1210,6 +1213,7 @@ local function getElrsFlags() or config.scenario == "single_antenna" or config.scenario == "weak_link" or config.scenario == "unrated_rate" + or config.scenario == "flrc" then flags = 0x01 -- connected else @@ -1685,8 +1689,6 @@ local moduleFound = (config.scenario ~= "no_module") -- no_module scenario has no entry -> mockTelemetry returns nil. -- ============================================================================ -local txModuleTelemetry = { TPWR = 50 } - -- TQly/TRSS are the downlink pair: the RX->TX telemetry path, reported by the -- handset's own receiver. They run a few dB behind the uplink in every -- scenario because the receiver transmits at a fraction of the module's power, @@ -1698,6 +1700,7 @@ local scenarioTelemetry = { ["1RSS"] = -87, ["2RSS"] = -93, RQly = 99, + RSNR = 8, ANT = 1, TQly = 100, TRSS = -95, @@ -1717,6 +1720,7 @@ local scenarioTelemetry = { ["1RSS"] = -84, ["2RSS"] = 0, RQly = 97, + RSNR = 8, ANT = 0, TQly = 100, TRSS = -91, @@ -1734,6 +1738,7 @@ local scenarioTelemetry = { ["1RSS"] = -78, ["2RSS"] = -82, RQly = 100, + RSNR = 8, ANT = 0, TQly = 100, TRSS = -83, @@ -1745,6 +1750,26 @@ local scenarioTelemetry = { Alt = 85, GPS = { lat = 54.7050, lon = 25.3100 }, }, + -- TX + RX connected at F1000 (v3 index 13). FLRC carries no SNR, so the + -- receiver reports 0 for as long as the rate runs; pinned in sensorToggle. + flrc = { + TPWR = 250, + RFMD = 13, + ["1RSS"] = -70, + ["2RSS"] = -74, + RQly = 100, + RSNR = 0, + ANT = 0, + TQly = 100, + TRSS = -80, + RxBt = 16.4, + Curr = 18.0, + FM = "ACRO", + Sats = 10, + GSpd = 45.0, + Alt = 30, + GPS = { lat = 54.6872, lon = 25.2797 }, + }, -- A packet rate ExpressLRS publishes no sensitivity figure for (v3 index 18, -- "9K1000", carried in the tables as 0). There is no floor to measure -- against, so everything derived from one has to fall back rather than draw @@ -1755,6 +1780,7 @@ local scenarioTelemetry = { ["1RSS"] = -79, ["2RSS"] = -84, RQly = 98, + RSNR = 8, ANT = 0, TQly = 100, TRSS = -88, @@ -1774,6 +1800,7 @@ local scenarioTelemetry = { ["1RSS"] = -55, ["2RSS"] = -58, RQly = 95, + RSNR = 12, ANT = 1, TQly = 100, TRSS = -61, @@ -1788,11 +1815,17 @@ local scenarioTelemetry = { ["1RSS"] = -55, ["2RSS"] = -58, RQly = 95, + RSNR = 12, ANT = 1, TQly = 100, TRSS = -61, RxBt = 15.8, Curr = 0.5, + FM = "ACRO", + Sats = 12, + GSpd = 25.3, + Alt = 142, + GPS = { lat = 54.6872, lon = 25.2797 }, }, -- Same signal as model_mismatch, and deliberately no sensorToggle entry: the -- mismatch bit is the only thing that may move, so RQly stays at 95 and the @@ -1803,6 +1836,7 @@ local scenarioTelemetry = { ["1RSS"] = -55, ["2RSS"] = -58, RQly = 95, + RSNR = 12, ANT = 1, TQly = 100, TRSS = -61, @@ -1817,6 +1851,7 @@ local scenarioTelemetry = { ["1RSS"] = -85, ["2RSS"] = -88, RQly = 60, + RSNR = 2, ANT = 0, TQly = 62, TRSS = -97, @@ -1828,13 +1863,35 @@ local scenarioTelemetry = { Alt = 210, GPS = { lat = 54.6600, lon = 25.2400 }, }, + no_telemetry = { + -- The sensor list a previous flight left on the model. The RX never + -- answers, so every value is served as 0 the way EdgeTX does; only the + -- key set matters. + TPWR = 50, + RFMD = 7, + ["1RSS"] = -87, + ["2RSS"] = -93, + RQly = 99, + RSNR = 8, + ANT = 1, + TQly = 100, + TRSS = -95, + RxBt = 15.2, + Curr = 12.5, + FM = "ACRO", + Sats = 12, + GSpd = 25.3, + Alt = 142, + GPS = { lat = 54.6872, lon = 25.2797 }, + }, reconnect = { - -- Same as normal; only served when isRxAvailable() is true + -- Same as normal; zeroed until isRxAvailable() turns true TPWR = 50, RFMD = 7, ["1RSS"] = -87, ["2RSS"] = -93, RQly = 99, + RSNR = 8, ANT = 1, TQly = 100, TRSS = -95, @@ -1848,6 +1905,7 @@ local scenarioTelemetry = { ["1RSS"] = -87, ["2RSS"] = -93, RQly = 99, + RSNR = 8, ANT = 1, TQly = 100, TRSS = -95, @@ -1866,6 +1924,7 @@ local scenarioTelemetry = { ["1RSS"] = -87, ["2RSS"] = -93, RQly = 99, + RSNR = 8, ANT = 1, TQly = 100, TRSS = -95, @@ -1887,6 +1946,7 @@ local sensorJitter = { TRSS = 3, RQly = 2, -- +/- 2% TQly = 2, + RSNR = 2, -- +/- 2 dB RxBt = 0.05, -- +/- 0.05V Curr = 2.0, -- +/- 2A GSpd = 3.0, @@ -1914,6 +1974,10 @@ local sensorToggle = { -- Must stay exactly 0: that is what marks the second RF path as absent. ["2RSS"] = { 0 }, }, + flrc = { + -- FLRC reports no SNR: exactly 0, never jittered. + RSNR = { 0 }, + }, mismatch_cycle = { -- ~10 s connected, ~5 s down, repeating. Exactly 95 or 0 so the -- RQly-derived connection state flips cleanly on each phase change. @@ -1935,46 +1999,44 @@ local function updateTelemetryCache() end lastTelemetryUpdate = now - if not isRxAvailable() then - -- TX module still reports RFMD/TPWR via link stats even without RX. - -- Only provide these when a module is present (not no_module). - telemetryCache = {} - if moduleFound then - for k, v in pairs(txModuleTelemetry) do - telemetryCache[k] = v - end - end - return - end local t = scenarioTelemetry[config.scenario] + telemetryCache = {} if not t then - telemetryCache = {} return end - telemetryCache = {} - toggleStep = toggleStep + 1 - local toggles = sensorToggle[config.scenario] - for sensorId, base in pairs(t) do - local seq = toggles and toggles[sensorId] - local jit = sensorJitter[sensorId] - if seq then - telemetryCache[sensorId] = seq[(toggleStep % #seq) + 1] - elseif jit then - local val = base + (math.random() * 2 - 1) * jit - if jit == math.floor(jit) then - val = math.floor(val + 0.5) - end - -- A link quality is a percentage of packets received, so it cannot - -- exceed 100. Jittering a base of 99 was handing the widgets 101, which - -- is not a reading any receiver can produce. - local ceiling = sensorCeiling[sensorId] - if ceiling and val > ceiling then - val = ceiling + if isRxAvailable() then + toggleStep = toggleStep + 1 + local toggles = sensorToggle[config.scenario] + for sensorId, base in pairs(t) do + local seq = toggles and toggles[sensorId] + local jit = sensorJitter[sensorId] + if seq then + telemetryCache[sensorId] = seq[(toggleStep % #seq) + 1] + elseif jit then + local val = base + (math.random() * 2 - 1) * jit + if jit == math.floor(jit) then + val = math.floor(val + 0.5) + end + -- A link quality is a percentage of packets received, so it cannot + -- exceed 100. Jittering a base of 99 was handing the widgets 101, which + -- is not a reading any receiver can produce. + local ceiling = sensorCeiling[sensorId] + if ceiling and val > ceiling then + val = ceiling + end + telemetryCache[sensorId] = val + else + telemetryCache[sensorId] = base end - telemetryCache[sensorId] = val - else - telemetryCache[sensorId] = base + end + end + + -- EdgeTX serves 0 for every sensor while RQly is 0 (luaGetValueAndPush), + -- the module's own TPWR/RFMD included: Lua never sees the link stats frame. + if (telemetryCache.RQly or 0) == 0 then + for sensorId in pairs(t) do + telemetryCache[sensorId] = 0 end end end @@ -1982,7 +2044,8 @@ end --- Return a mock telemetry sensor value for the current scenario. -- Called at ~10 Hz by the widget via crsf.getSensorValue(). Values are -- regenerated only once per second; intermediate calls return cached data. --- Returns nil when disconnected or the sensor is not defined. +-- Returns 0 for every sensor while the link is down, as EdgeTX does, and nil +-- for a sensor the scenario does not define. local function mockGetSensorValue(sensorId) updateTelemetryCache() return telemetryCache[sensorId] diff --git a/src/WIDGETS/ELRSTelemetry/rf_modes.lua b/src/WIDGETS/ELRSTelemetry/rf_modes.lua index 6e8f71f..7080ce2 100644 --- a/src/WIDGETS/ELRSTelemetry/rf_modes.lua +++ b/src/WIDGETS/ELRSTelemetry/rf_modes.lua @@ -195,6 +195,18 @@ function RfModes.floor(rfmd) return dbm end +-- The FLRC rates. The radio reports no SNR for FLRC packets, so RSNR sits at +-- 0 for as long as one of these runs. Keyed by name: both majors spell them +-- the same, and the name is what a reader can check against the RF Mode row. +local NO_SNR = { D250 = true, D500 = true, F500 = true, F1000 = true } + +--- Whether an RFMD sensor value names a rate that carries an SNR reading. +function RfModes.hasSnr(rfmd) + local names = RfModes._names + local name = names and names[rfmd + 1] + return not (name and NO_SNR[name]) +end + -- ============================================================================ -- Return module -- ============================================================================ diff --git a/src/WIDGETS/ELRSTelemetry/telemetry.lua b/src/WIDGETS/ELRSTelemetry/telemetry.lua index cb8a04e..95bf99d 100644 --- a/src/WIDGETS/ELRSTelemetry/telemetry.lua +++ b/src/WIDGETS/ELRSTelemetry/telemetry.lua @@ -188,6 +188,13 @@ function Telemetry.rfModeName() return RfModes.name(rfmd) end +--- Whether the current rate carries an SNR reading at all. FLRC rates do +--- not, so their RSNR is a placeholder 0 rather than a measurement. +function Telemetry.hasSnr() + local rfmd = Telemetry.link.rfmd + return rfmd == nil or RfModes.hasSnr(rfmd) +end + -- ============================================================================ -- Derived state -- ============================================================================ @@ -410,6 +417,7 @@ function Telemetry.update() link.rssi1 = crsf.getSensorValue("1RSS") link.rssi2 = crsf.getSensorValue("2RSS") link.rqly = crsf.getSensorValue("RQly") + link.rsnr = crsf.getSensorValue("RSNR") link.ant = crsf.getSensorValue("ANT") link.tqly = crsf.getSensorValue("TQly") link.trss = crsf.getSensorValue("TRSS") diff --git a/src/WIDGETS/ELRSTelemetry/ui/display.lua b/src/WIDGETS/ELRSTelemetry/ui/display.lua index 932deeb..bc5122e 100644 --- a/src/WIDGETS/ELRSTelemetry/ui/display.lua +++ b/src/WIDGETS/ELRSTelemetry/ui/display.lua @@ -118,10 +118,10 @@ function Display.trssText() return table.concat({ tostring(trss), " dBm" }) end ---- TX power on its own, e.g. "100 mW", or "--" while unknown. +--- TX power on its own, e.g. "100 mW", or "--" while disconnected. function Display.powerText() local tpwr = Telemetry.link.tpwr - if tpwr == nil then + if not Telemetry.isConnected() or tpwr == nil then return "--" end return table.concat({ tostring(tpwr), " mW" }) diff --git a/src/WIDGETS/ELRSTelemetry/ui/fullscreen.lua b/src/WIDGETS/ELRSTelemetry/ui/fullscreen.lua index 67935d3..58fb38f 100644 --- a/src/WIDGETS/ELRSTelemetry/ui/fullscreen.lua +++ b/src/WIDGETS/ELRSTelemetry/ui/fullscreen.lua @@ -30,8 +30,8 @@ local FullScreenUI = {} local LABEL_PCT = (LCD_W < LCD_H) and 42 or 50 --- Wrap a value formatter so a row reads "--" while the link is down. ---- Only the link and power rows take this. The flight controller and GPS rows ---- have their own fallbacks, and latitude/longitude deliberately keep showing +--- The link, power, satellite, speed and altitude rows take this. The flight +--- controller rows have their own fallbacks, and latitude/longitude keep showing --- the last known position after the link drops -- that is what you read when --- you are looking for a model that stopped answering. local function whenConnected(fn) @@ -147,34 +147,37 @@ function FullScreenUI.build() createDisplayRow(fields, "Link Quality", Display.lqValueText) + -- Both antennas on one row in fixed 1 / 2 order, as the module's own screen + -- and the compact tiers print them, so neither number jumps when the + -- receiver switches paths. A single-path receiver gets the bare reading. createDisplayRow( fields, - "RSSI 1", + "RSSI", whenConnected(function() local rssi1 = Telemetry.link.rssi1 if rssi1 == nil then return "--" end + if Telemetry.hasDiversity() then + return table.concat({ tostring(rssi1), " / ", tostring(Telemetry.link.rssi2), " dBm" }) + end return table.concat({ tostring(rssi1), " dBm" }) end) ) createDisplayRow( fields, - "RSSI 2", + "SNR", whenConnected(function() - local rssi2 = Telemetry.link.rssi2 - if rssi2 == nil then + local rsnr = Telemetry.link.rsnr + if rsnr == nil then return "--" end - return table.concat({ tostring(rssi2), " dBm" }) - end), - function() - if not Telemetry.hasDiversity() then - return COLOR_THEME_DISABLED + if not Telemetry.hasSnr() then + return "n/a" -- FLRC: the sensor is a permanent 0, not a reading end - return COLOR_THEME_SECONDARY1 - end + return table.concat({ tostring(rsnr), " dB" }) + end) ) createDisplayRow( @@ -186,7 +189,7 @@ function FullScreenUI.build() end -- EdgeTX's telemetry list prints the raw ANT enum (0/1) and so does the TX -- module's own screen. The "Ant " prefix keeps this row from reading as that - -- same number, and 1/2 matches the RSSI 1 / RSSI 2 rows above. + -- same number, and 1/2 matches the order of the RSSI pair above. if Telemetry.link.ant == 0 then return "Ant 1" end @@ -264,29 +267,41 @@ function FullScreenUI.build() -- GPS section createSectionHeader(fields, "GPS") - createDisplayRow(fields, "Satellites", function() - local sats = Telemetry.link.sats - if sats == nil then - return "--" - end - return tostring(sats) - end) + createDisplayRow( + fields, + "Satellites", + whenConnected(function() + local sats = Telemetry.link.sats + if sats == nil then + return "--" + end + return tostring(sats) + end) + ) - createDisplayRow(fields, "Speed", function() - local gspd = Telemetry.link.gspd - if gspd == nil then - return "--" - end - return string.format("%.1f", gspd) - end) + createDisplayRow( + fields, + "Speed", + whenConnected(function() + local gspd = Telemetry.link.gspd + if gspd == nil then + return "--" + end + return string.format("%.1f", gspd) + end) + ) - createDisplayRow(fields, "Altitude", function() - local alt = Telemetry.link.alt - if alt == nil then - return "--" - end - return tostring(alt) - end) + createDisplayRow( + fields, + "Altitude", + whenConnected(function() + local alt = Telemetry.link.alt + if alt == nil then + return "--" + end + return tostring(alt) + end) + ) createDisplayRow(fields, "Latitude", function() if Telemetry.gps == nil then