adaptive cad docs update

This commit is contained in:
liquidraver
2026-07-13 21:42:03 +02:00
parent 5c68aaea8e
commit 1d2e812faf
2 changed files with 125 additions and 31 deletions
+92 -15
View File
@@ -24,6 +24,70 @@ threshold inside the radio's despreader — **not** a dBm level. The RSSI
noise floor cannot be converted into a detPeak value, which is why this
feature measures CAD behaviour directly instead of deriving it from RSSI.
## The mental model: strong signals vs. faint ones
This is the picture that makes every knob below make sense. Skip it and
the numbers look arbitrary; read it and "SmartCAD" is one idea seen from
three angles.
**detPeak gates on correlation strength, which tracks link budget — and it
is blind to distance.** The despreader's correlation peak is essentially
the signal's SNR after processing gain, so a strong arrival produces a big
peak and a weak one a small peak, *regardless of how far it travelled*. A
90 km line-of-sight repeater that arrives strong and a 1 km neighbour
arriving strong both trip CAD; a faint straggler at the edge of
decodability, a multipath echo, or a distant node buried in noise all
produce small peaks. **Raising detPeak raises the bar to "strong only";
lowering it means "hear even the faint stuff."** There is no knob that
separates *near* from *far* — only *strong* from *faint* — because the
radio only ever knew signal strength, never distance.
**Semtech's recommended values (AN1200.48: 2129 across all SF/BW for the
SX126x; our base is `SF+13`, which is 21 at SF8) are tuned for a
receiver** that wants to hear everything down to its sensitivity limit —
i.e. to *catch the faint*. Listen-before-talk on a busy backbone often
wants the opposite: deliberately sit *above* that band so it ignores faint
contention it would win on capture anyway. So "operating above 29" is not
a misconfiguration here; it is the point.
**Scale sanity, because the register is deceptive.** `cadDetPeak` is a
full 8-bit field (0255), but the *useful* range is only ~1832. The
driver's 40 ceiling is ~11 above the highest value Semtech recommends
anywhere — a near-blind guardrail, not an operating point. And the
LR11xx/LR2021 family's 5668 numbers are a **different chip's correlation
scale**; porting them onto an SX126x makes CAD deaf. If a value feels like
it should be "mid-scale," that instinct is the trap: this scale is
compressed, not linear over 0255.
**Why the probes only ever measure the faint side.** A calibration probe
is *skipped* whenever RSSI is more than 7 dB above the noise floor (a
strong signal is present — see the prefilter below). So every probe that
runs does so in a quiet moment, and the only thing CAD can trip on there
is a **sub-floor, faint, correlation-only-detectable** signal. That makes
the entire adaptive loop a **faint-rejection tuner**: the `busy%` you read
in `get cad` is "how often a quiet moment still held a faint signal", not
total channel occupancy. Strong signals never enter the statistics — they
were filtered out before the probe, and in real LBT they trip on their own
merits.
**So the busy cap is really a faint-tolerance dial.** `set cad.busycap`
says "keep raising detPeak until fewer than *N*% of quiet moments trip on
faint signals." A **lower** cap rejects faint traffic more aggressively
(ignore echoes and stragglers); a higher cap tolerates more of it. On a
saturated hilltop that should react only to strong contention, set the cap
low (e.g. `10`); the default `25` is a moderate setting suited to mixed
and leaf nodes. This is the same mechanism as "airtime protection" below —
two names for one lever, because the airtime a busy node wastes *is* the
airtime spent deferring for faint traffic.
**The one thing you cannot see locally.** Because strong signals are
prefiltered out of the measurement, a node can never confirm from its own
stats that it *still catches strong* — the numbers only cover the faint
side it is rejecting. The only check is behavioural: does the node still
defer for its known strong peers? If it climbs so high that even they stop
tripping, back the cap up. (This is the same "miss side is invisible"
caveat noted at the end, viewed through the strong/faint lens.)
## How it works
Every `cad.probe.interval` seconds (default 15), when the radio is idle
@@ -50,11 +114,20 @@ below-noise-floor packet (detecting those is CAD's whole purpose):
1. Probes are skipped entirely when the channel is visibly busy (RSSI
more than 7 dB above the learned noise floor) or a packet is being
received — those teach nothing about false positives.
2. After a busy verdict, RX is restarted and the firmware waits ~8 symbol
times: a real transmitter is still on the air and trips the receive
path. If nothing shows up, the verdict counts as a suspected false
positive. Residual contamination by real traffic biases the estimate
received — those teach nothing about false positives. (This is also the
prefilter that limits probing to the faint regime; see the mental model
above.)
2. After a busy verdict, RX is restarted and the firmware watches a
~12-symbol window for either signal of a real transmitter: (a) the
restarted RX syncs on it, **or** (b) instantaneous RSSI climbs above the
noise floor. If neither appears, the verdict counts as a suspected false
positive. Path (b) matters: tearing RX down to run the probe routinely
eats a real packet's preamble, so RX often fails to re-sync on a signal
that is genuinely there — an earlier `isReceiving()`-only check booked
those as false positives, a detPeak-independent floor that flattened the
FP curve and drove the staircase into the ceiling. Channel energy does
not depend on winning the preamble race, so it recovers them. Residual
contamination by real below-floor traffic biases the estimate
*conservative* (higher detPeak), which is the safe direction.
Statistics decay (halve) every 6 hours so the picture stays fresh, and
@@ -131,22 +204,25 @@ get cad
Compact output (kept short so it survives a truncated LoRa reply):
```
> a:on o:-1 pk:20(b21/4s) iv:15s
-2(19) 241p 9b 7f 2t 3%
-1(20) 900p 5b 3f 2t 0%
+0(21) 300p 2b 0f 2t 0%
> a:on o:-1 pk:20(b21/4s) iv:15s bc:25%
-2(19) 241p 9b 7f 2t 3%
*-1(20) 900p 5b 3f 2t 0%
+0(21) 300p 2b 0f 2t 0%
```
Header: `a` auto on/off · `o` operating offset · `pk` operating detPeak ·
`b` family base · `4s` 4 symbols · `iv` probe interval.
Per level: `level(peak) probes busy fp tp fp%%``fp` = suspected false
positives, `tp` = busy verdicts confirmed by a real packet, `fp%%` =
integer false-positive rate. Only levels that have been probed print, so
in auto mode you'll usually see the operating level and its frontier.
`b` family base · `4s` 4 symbols · `iv` probe interval · `bc` busy cap.
Then a **three-rung window** centred on the operating offset (`*` marks
it): the frontier (one step more sensitive), the operating level, and one
step less sensitive — exactly the three rungs the staircase reads to judge
the local slope. Per level: `level(peak) probes busy fp tp fp%%``fp` =
suspected false positives, `tp` = busy verdicts confirmed by a real
signal, `fp%%` = integer false-positive rate.
Here the staircase has already stepped to offset 1 (peak 20): the 1
level is clean (0%) over 900 probes while 2 shows a 3% jump, so 1 is
this site's knee.
this site's knee. Remember (mental model above) these numbers describe the
*faint* regime only — strong signals were prefiltered out before probing.
### Dry-run / manual tuning
@@ -183,6 +259,7 @@ a knee to resolve clearly, longer to capture day/night variation.
| `set cad.auto <on\|off>` | **on** | Staircase controller acts on the stats. Off = observe/hand-tune. |
| `set cad.offset <n>` | 0 | Operating offset, 8…12. Negative = more sensitive. Applied live. |
| `set cad.probe.interval <sec>` | 15 | Probe cadence; 0 disables probing (and freezes auto), 10255 otherwise. |
| `set cad.busycap <pct>` | 25 | Faint-tolerance / airtime cap: raise detPeak once more than this % of (quiet-moment) probes trip on faint signals. Lower = reject faint/echo harder (busy backbones); 0 = off. 1090 otherwise. |
| `set cad.reset` | | Clear accumulated statistics (RAM only). |
All settings persist in prefs and apply to every role — repeater, room
+33 -16
View File
@@ -428,22 +428,39 @@ Algorithm in `triggerNoiseFloorCalibrate()`:
### 5.3.1 Adaptive CAD (LBT detPeak calibration)
`cadDetPeak` is a correlation peak-to-noise threshold in the despreader (not
dBm), so the right LBT sensitivity is site-dependent (chirp-like interference
varies) and cannot be derived from the RSSI floor. `LoRaRadioBase::cadMaintenance()`
(housekeeping tick) runs one calibration CAD probe per `cad.probe.interval`
(default 60 s) at a signed **level** relative to the family's per-SF base
detPeak, restarts RX, and classifies busy verdicts via a ground-truth filter
(pre-probe RSSI near floor; post-busy ~8-symbol wait for real RX activity →
`tp`, else suspected `fp`). Per-level counters decay 6-hourly and reset on any
RF param change. With `cad.auto on`, a one-sided staircase steps the operating
offset down when the frontier level shows FP ≤ 1% over ≥300 probes, up quickly
when the operating level exceeds 2× target over ≥50 probes; offset clamped
4…+4, persisted via `Dispatcher::onCadOffsetChanged()`. Probe + offset plumbing
is per-driver extension API (`*_cad_probe`, `*_cad_set_peak_offset`,
`*_cad_base_peak`); LBT CAD runs 4 symbols (set in `buildModemConfig`), and the
drivers scale their blocking-CAD timeout to `nSym·Tsym + margin`. CLI: `get cad`,
`set cad.auto/offset/probe.interval/reset`. SX127x: unsupported (no HW CAD).
User doc: `ADAPTIVE_CAD.md`.
dBm): it gates on signal *strength* ≈ link budget, blind to distance, so
raising it means "react to strong signals only, ignore faint/echo". The right
LBT sensitivity is site-dependent and cannot be derived from the RSSI floor.
`LoRaRadioBase::cadMaintenance()` (housekeeping tick) runs one calibration CAD
probe per `cad.probe.interval` (default **15 s**) at a signed **level** relative
to the family's per-SF base detPeak, restarts RX, and classifies busy verdicts
with a ground-truth filter. **Key property:** the probe is *skipped* when RSSI >
floor+7 dB, so probes only ever sample the quiet/faint regime — the whole loop
is a faint-rejection tuner and `busy%` is faint-regime, not total occupancy.
Post-busy classification watches a ~12-symbol window for RX re-sync **or** an
RSSI climb above floor+guard (the energy path recovers real packets whose
preamble the probe's RX-restart ate — the fix for the FP over-count that used to
drive the staircase to the ceiling) → `tp`, else `fp`. Counters decay 6-hourly,
reset on any RF param change.
With `cad.auto on` the staircase is **knee-seeking**: probes sample op / op1 /
op+1 (½/¼/¼); it steps **up** when the level above is ≥`CAD_KNEE_SLOPE_PERMILLE`
(5%) cleaner (steep side, below knee), **down** only on a clean flat plateau
(`≤CAD_PLATEAU_CLEAN_PERMILLE`), else holds — slope-based so convergence is
independent of a site's FP floor. Highest-priority override: **airtime / faint
cap** — step up when the operating busy rate exceeds `cad_busycap` (percent,
`set cad.busycap`, default 25, 0=off); self-targeting since only busy nodes
reach it, and effectively a faint-tolerance dial (lower = reject faint harder).
Each step needs ≥`CAD_STEP_MIN_PROBES` (120); offset clamped **8…+12**,
persisted via `Dispatcher::onCadOffsetChanged()`. Driver absolute clamp (SX126x
1540, LR 4890) is a guardrail; AN1200.48 recommends 2129 for SX126x (base
`SF+13`), tuned to catch faint — LBT may deliberately sit above it. Probe +
offset plumbing is per-driver extension API (`*_cad_probe`,
`*_cad_set_peak_offset`, `*_cad_base_peak`); LBT CAD runs 4 symbols (set in
`buildModemConfig`), drivers scale their blocking-CAD timeout to
`nSym·Tsym + margin`. CLI: `get cad` (3-rung window, `*`=operating, `bc:`=cap),
`set cad.auto/offset/probe.interval/busycap/reset`. SX127x: unsupported (no HW
CAD). Full mental model + tuning: `ADAPTIVE_CAD.md`.
### 5.4 LR1110 Driver Errata Workarounds