Fix/noise floor ratchet, adapt noise to the real floor with recovery in both directions#2933
Fix/noise floor ratchet, adapt noise to the real floor with recovery in both directions#2933usrflo wants to merge 3 commits into
Conversation
The noise-floor calibration sampled only RSSI values below the current floor + threshold, a one-way ratchet: it accepted ever-lower samples but never recovered upward, so _noise_floor drifted to the -120 clamp and stayed there. That left the RSSI-margin LBT (isChannelActive with interference_threshold, plus isResendChannelActive / isChannelNoisy on the feature branches that consume _noise_floor) permanently over-sensitive — resends and dwell-gated TX deferred even on a quiet channel. Replace the ratcheted block mean with the median of the 64-sample block: - accepts every idle (!isReceivingPacket) sample — no downward bias; - median rejects transient interference spikes (high and low outliers) and recovers in BOTH directions; - _noise_floor is written only after a full block, so the previous value stays valid while the next block is sampled — no reset-to-0 and thus no permissive LBT window (margin = RSSI - 0) during reconvergence. resetAGC no longer forces _noise_floor = 0 (the stuck-ratchet workaround); it only discards the in-progress block so a fresh one is measured after the analog frontend reset. Verified: Heltec_v3_repeater firmware build (compiles RadioLibWrappers.cpp against real RadioLib). Co-Authored-By: Claude <noreply@anthropic.com>
Documents the ratchet-to-median fix on fix/noise-floor-ratchet: symptom, root cause (one-way ratchet drift to -120), the median-of-64 replacement, files touched, build verification note (sim does not compile RadioLibWrappers.cpp; verified via Heltec_v3_repeater), and merge intent. Co-Authored-By: Claude <noreply@anthropic.com>
Problem Proof on dev Branch, _noise_floor = -120 trap, Case CPatch noise-floor-provocation-C.patch demonstrates on the dev branch that if |
Problem Proof on dev Branch, Drifting Floor, Case BPatch noise-floor-provocation-B.patch demonstrates the one-way drift of the noise floor on the dev branch, provoked by a reduced threshold. From 20 s — Drift (threshold = 2): cutoff = floor + 2 slips into the noise; each block censors the upper portion → floor slides monotonically downward, rejected count rises, cutoff follows. What B demonstrates (vs. C) Together, they cover: cause (B) and symptom (C). |
Proof of fix/noise-floor-ratchet (PR 2933), _noise_floor = -120 trap, Case CPatch fix-noise-floor-ratchet-C.patch demonstrates the noise floor doesn't drop permanently if once lowered using the changes from PR 2933. |
Proof of fix/noise-floor-ratchet (PR 2933), Drifting Floor, Case BPatch fix-noise-floor-ratchet-B.patch demonstrates the noise floor doesn't drift one-sided when using the changes from PR 2933. |
Symptom
On long-running nodes (I saw it with an ufo integration branch, which combines
feature/repeated-sending-2+feature/quiet-dwell), direct-packet resends and dwell-gated TX get deferred or suppressed even on a quiet channel.The radio's reported
noise_floordrifts to the-120clamp and never recovers, so the RSSI-margin LBT checks (isResendChannelActive,isChannelNoisy,isChannelActivewithinterference_threshold) stay permanently over-sensitive — every send looks like it collides with noise.Root cause
RadioLibWrapper::loop()calibrated_noise_floorfrom a 64-sample block, but only accepted samples that satisfiedThat filter is a one-way ratchet: it admits ever-lower samples but rejects anything above the current floor, so the block mean can only move down. Over time it walks to the
-120lower clamp and sticks there. The only thing that reset it wasresetAGCsetting_noise_floor = 0— butresetAGCis gated onagc_reset_interval, which defaults to0(off), and forcing0would anyway open a brief permissive LBT window (margin = RSSI − 0) until the next block completes.The fix
Replace the ratcheted block mean with the median of the 64-sample block:
!isReceivingPacket()) — no downward bias._noise_flooronly after a full block is collected. The previous value stays valid while the next block is sampled — no reset-to-0, hence no permissive LBT window during reconvergence.-120(lower bound of the radio's RSSI range).resetAGC()no longer touches_noise_floor; it only discards the in-progress block (the analog frontend was just reset, so queued samples are stale)._noise_flooritself is left in place because the median estimator no longer needs the hard reset that the ratchet did.SAMPLING_THRESHOLDis removed (it only fed the ratchet filter).NUM_NOISE_FLOOR_SAMPLES(64) moves to the header so the sample buffer can be a member array.