Feature: Add live channel-health metrics to device displays - #3332
Draft
usrflo wants to merge 14 commits into
Draft
Feature: Add live channel-health metrics to device displays#3332usrflo wants to merge 14 commits into
usrflo wants to merge 14 commits into
Conversation
…or rate) Three ~5s windowed metrics sampled in RadioLibWrapper::loop(): - channel utilization: own TX, in-progress reception, or RSSI above noise floor + fixed 15dB margin (rate-limited to one poll / 50ms) - RX deafness: fraction of time the radio is not in RX mode - RX error rate: share of reception attempts with CRC errors Exposed via mesh::Radio virtuals (default 0 = good/unavailable), appended to the stats-radio JSON, and approximated in the simulator by SimRadio (airtime-counter deltas + recv_mode_ wall time). Co-Authored-By: Claude <noreply@anthropic.com>
RADIO/status pages gain three windowed indicators in positive framing (full bar = good, warning colour below threshold): 'CH frei' (100 - utilization, warn <50%), 'RX-bereit' (100 - deafness, warn <80%) and 'RX-Guete' (100 - RX error rate, warn <90%), drawn with the battery-indicator bar pattern. On the 128x64 companion RADIO page the static TX-dBm label makes room; noise floor moves beside BW/CR. Co-Authored-By: Claude <noreply@anthropic.com>
…label - 'CH frei'/'RX-bereit'/'RX-Guete' -> 'CH free'/'RX ready'/'RX quality' (rest of the UI is English; 'coverage' avoided - it means network reach in mesh terms, not RX availability) - pin the bar to the display's right edge and right-align the percent value just before it, so neither shifts when the value width changes (100% vs 9%) - rename the cryptic 'Q:' companion label to 'RXQ' Co-Authored-By: Claude <noreply@anthropic.com>
The old percent divided CRC failures by good decodes only, so a window with at least one good packet and as many failures showed a permanent 0% (and wrapped arbitrarily beyond 100). Feed the window with ALL attempts (decodes + failures) instead and surface counts: - displays now show 'RX quality good/total' (e.g. 23/24) - the corruption share AND the traffic heard in the ~5s window (0/0 = quiet) - new mesh::Radio::getRxQualityCounts(good,total) replaces getRxErrorRatePct(); stats-radio JSON derives rx_err_pct from the counts and adds rx_good/rx_total - initialize n_recv_errors in the RadioLibWrapper ctor (was read as a garbage snapshot baseline in begin(), injecting one bogus delta) Co-Authored-By: Claude <noreply@anthropic.com>
advance() only rolled a bucket when a single inter-call gap reached 1000ms. Both callers tick far faster (RadioLibWrapper::loop / SimRadio::loop, every main-loop pass), so elapsed sub-second time was silently dropped, cur_ev accumulated forever and counts() reported a lifetime total instead of the ~5s window (HW: RX quality counts never reset; JSON rx_err_pct denominator grew unbounded). Accumulate elapsed ms into cur_ms and roll per completed second, like WindowedPercent's cur_total. WindowedPercent itself was correct. Co-Authored-By: Claude <noreply@anthropic.com>
WindowedCountedRatio moves from 5x1s to 60x10s buckets (~10 min): packet counts on a quiet mesh need minutes to become statistically meaningful (5 s often showed only 0/0 or 1/2). The time-based utilization/deafness metrics stay at ~5 s. - counts() extrapolates to the full window while it fills after construction/clear (events per observed time x window length), so the number has 10-min scale immediately and converges as the window fills. - dt cap raised to the full window: long stalls/deep sleep age the window in wall time; a stall spanning the whole window drops the stale partial bucket instead of baking it into the oldest surviving bucket. - clear() added; resetStats() now re-stamps the delta bases and clears the window (zeroing counters alone underflowed the next loop() delta and injected a garbage spike into one bucket - 5 s visible before, up to 10 min now). Co-Authored-By: Claude <noreply@anthropic.com>
…tions) A node with a well-placed antenna always hears distant stations whose signals cannot decode - those CRC failures are physics, not channel health. A failure now enters the RX-quality window only if its SNR was at/above the per-SF decode threshold + 3 dB guard (snr_threshold table, hoisted above recvRaw): 'should have decoded, but didn't' = collision/ interference verdict on this channel. Weak failures drop out of both numerator and denominator of the window. The packet-status SNR stays latched after a failed readData (readData clears IRQ/FIFO state, not packet status - verified across SX126x/ SX127x/LR11x0/LR2021); gating is limited to CRC/header errors (-7/-24), whose SNR is meaningful. Raw n_recv_errors keeps counting every failure (JSON recv_errors, binary stats); a new n_recv_errors_strong feeds the window deltas in loop(). Optional per-failure decision logging behind MESH_DEBUG_RXQ. Co-Authored-By: Claude <noreply@anthropic.com>
…ood/total") The percentage scans like the bar rows above it; the counts keep showing sample size and traffic level. Quiet stays plain "0/0" (no data, no verdict); if very large counts would collide with the label on a narrow display the percentage is dropped, keeping the counts. Value turns warning-coloured below 80%, like the RX-ready row. Co-Authored-By: Claude <noreply@anthropic.com>
All three channel-health lines now follow the same pattern: label, "NN%" value, progress bar (warn colour below 50%/80%/80%). The good/total counts leave the display - they remain available via the stats-radio JSON (rx_good/rx_total). A quiet window (no decodes yet) renders "--%" with an empty, dimmed bar instead of a verdict; drawHealthBar gains a no_data flag for that (default off, so the other rows are unchanged). Co-Authored-By: Claude <noreply@anthropic.com>
…safety Correctness: - stats-radio JSON overflowed the fixed 160-byte CLI reply buffers once counters grew (worst case ~198 bytes). New keys shortened (util/deaf/ good/tot), legacy fields untouched; worst case now ~151 bytes. The derivable err-pct is no longer printed separately. - loop()'s per-iteration isReceivingPacket() call erased HEADER_ERR (CustomSX1262::isReceiving hdrErr branch) before readData() classified the packet, so header-damaged receptions were counted as good decodes and garbage entered the mesh parse path. The busy verdict is now sampled on the 50 ms tick and never while STATE_INT_READY is set. - RXQ relevance test trusted getLastSNR() before any packet status was latched (reads the 0 dB reset value, which passes every threshold): gated on _rx_snr_latched. - _cur_busy no longer holds a stale verdict while the radio is out of RX. - CAD dwells (blocking, radio in standby) are attributed to the deafness window in isChannelActive(), where they happen, instead of vanishing. - resetStats() now clears all three windows (WindowedPercent::clear() added), so a stats reset yields a consistent snapshot. - New metric members get in-class initializers (ctor/begin never set them; only .bss zero-init of globals saved them); dead _last_err_cnt removed (written twice, never read). - ESPNOW companions inherited the 0-defaults as full "all healthy" bars: hasChannelHealth() + getRxQualityPct() added to mesh::Radio; UIs render real no-data bars on radios that measure nothing. - ui-new RADIO page shows TX power again (folded into the FQ/SF row). Efficiency: busy sampling costs 2 SPI transactions per 50 ms tick (~0.1% CPU) instead of one per kHz main-loop iteration; warm-up extrapolation (two 64-bit divides per render, ratio-invariant) dropped. Simplification: drawHealthBar unified into DisplayDriver next to the other shared draw helpers (3 drifted copies removed); RX-quality percentage math lives in getRxQualityPct() instead of 4 call sites; WindowedCountedRatio no longer restates its default template args. Co-Authored-By: Claude <noreply@anthropic.com>
drawHealthBar(..., rxq_pct, ..., !radio->getRxQualityPct(rxq_pct)) read
rxq_pct by value (3rd argument) while getRxQualityPct() wrote it through
its reference (5th argument) in the same call. The two accesses are
unsequenced - undefined behavior - and with -O2 the compiler copied the
by-value argument before running the virtual call, so the bar rendered
the pre-call 0 whenever the window held data ('--%' only while empty).
HW symptom matched exactly: RX quality stuck at 0% while packets decoded
normally; the pre-bar version (4111e11), which computed the percentage
before the draw call, showed ~90%. Sequence the fetch explicitly in all
three UIs.
Co-Authored-By: Claude <noreply@anthropic.com>
HW testing at a site with a real broadband interferer (~-76 dBm ambient vs -113 quiet, zero decodes while it is active) showed both health metrics lying exactly when reception was dead: CH free stayed 100% (energy measured against the adapted floor, which rises with the jam) and RX quality froze at its last healthy ratio (a jammed channel produces zero header-valid IRQs = zero window events). - CH free now measures against busyRefFloor(): the P10 of recently published noise-floor values (ring of 64 blocks), absolutely capped at -100 dBm. An interferer drives utilization toward 100% for as long as it lasts - multi-hour jammers included (the cap) - and the reference recovers on its own once quiet blocks return. - RX quality reports 0% instead of a stale ratio while ambient energy sits far above the quiet floor (>=80% of the last ~5 s) AND no decode attempt happened for 2 min. - Display-only: LBT / interference_threshold semantics unchanged (they keep using the adapted _noise_floor). Co-Authored-By: Claude <noreply@anthropic.com>
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.
Motivation
The statistics a MeshCore device reports to the app are cumulative lifetime totals - they say nothing about the current situation. Channel stats over a short time window let a user quickly check the health of the mesh at their location.
This adds three small, windowed channel-health metrics rendered directly on the device display (companion
ui-new/ui-tinyandsimple_repeater) and exposed via thestats-radioJSON.The three metrics
isInRecvMode()sampling in the radio loop - catches duty-cycle deafness, blocking code, sleep misconfigAll three render as uniform health bars. A radio that cannot measure them (e.g. ESP-NOW builds) reports no data, and the UI shows "-- %" with a dimmed bar instead of a false "all healthy".
Sustained interference
The channel-busy verdict needs a reference for "quiet". Using the adapted noise floor directly has a failure mode: a floor that tracks a sustained interferer hides that interferer behind its own margin, so a jammed channel reads as free. CH free therefore measures against a quiet-channel reference: the 10th percentile of the last 64 published noise-floor values (covering several minutes), absolutely capped at −100 dBm. A sustained broadband interferer keeps CH free near 0 % for as long as it is present — even for hours — and the reference recovers on its own within minutes once the channel is quiet again. During warm-up (first 8 blocks after boot) the current noise floor is used as reference.
RX quality cannot drop on its own when reception dies: a jammed channel produces no decode attempts at all, so the windowed ratio would freeze at its last healthy value. It therefore reports 0 % when ambient energy stayed above the quiet reference for ≥ 80 % of the last 5 s and no decode attempt occurred for 2 minutes — meaning reception is dead, not merely "no traffic". While packets still decode through the interference, the honest good/total ratio is shown; genuine quiet stays "-- %".
This is display-only: LBT (
interference_threshold, CAD, resend deferral) keeps using the adapted noise floor. The verdicts reuse the existing ≥ 50 ms RSSI poll — no extra SPI traffic; the percentile is recomputed once per ~3.2 s calibration block.Field observation (site with an intermittent broadband interferer ~37 dB above the −113 dBm ambient): while present, CH free reads 0 % and RX quality falls to 0 % within ~2 min, with the noise-floor row showing −76; once it disappears, all three rows recover within minutes.
stats-radioJSONShort keys
util/deaf/good/totalongside the existingchan_util_pct/rx_deaf_pct/rx_good/rx_total(the reply buffer is 160 bytes).Testing
good == totin clean runs, JSON reply ~106 B