Files
ZephCore/zephcore/ADAPTIVE_CAD.md
T
2026-07-12 14:18:07 +02:00

193 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Adaptive CAD — Site-Calibrated Listen-Before-Talk
ZephCore's listen-before-talk (LBT) runs a hardware Channel Activity
Detection (CAD) before every transmission: the radio's LoRa correlator
searches the channel for chirps, and TX is deferred while activity is
detected. How sensitive that search is comes down to one register value,
`cadDetPeak` — and the right value depends on where the node lives.
- **Too sensitive** (detPeak too low): chirp-like interference — other
LoRa networks, other spreading factors on the same frequency — triggers
*false positives*. The node keeps deferring TX for nothing, wasting
100200 ms retry cycles and adding latency; in bad cases it hits the
4-second CAD watchdog.
- **Not sensitive enough** (detPeak too high): real transmissions are
*missed* and the node transmits over ongoing packets, causing on-air
collisions.
A quiet valley node and a 50-network hilltop node need different values.
Adaptive CAD measures the site and finds the value, instead of shipping
one hardcoded number to everyone.
Important physics note: `cadDetPeak` is a correlation peak-to-noise
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.
## How it works
Every `cad.probe.interval` seconds (default 15), when the radio is idle
in receive mode, the firmware runs one **calibration CAD probe** and
immediately re-arms RX. A probe takes one CAD duration — about 4 ms at
SF7/250 kHz, up to ~130 ms at SF12/125 kHz — so the added radio deaf time
is roughly 0.1%. Probes also run in RX duty-cycle (sniff) mode: the
duty cycle is briefly interrupted and re-armed, within the same
preamble-catch budget philosophy the sniff-mode math already accepts.
Each probe tests one **level**: a signed offset from the chip family's
per-SF base detPeak (SX126x: `SF+13`; LR11xx/LR2021: the 5668 table).
Results accumulate per level:
- `probes` — how many CADs ran at this level
- `busy` — raw "activity detected" verdicts
- `fp` — suspected **false positives**: busy verdicts where no packet
materialised right after (see filtering below)
- `tp`**true positives**: busy verdicts confirmed by actual RX
activity immediately after the probe
Filtering, because a "busy" verdict during apparent silence may be a real
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
*conservative* (higher detPeak), which is the safe direction.
Statistics decay (halve) every 6 hours so the picture stays fresh, and
reset completely whenever the radio parameters change (frequency, SF, BW
— the data is only valid for one configuration).
### Auto mode (staircase) — ON by default
`cad.auto` ships **on**, on repeaters and companions alike. A one-sided
staircase controller acts on the probe stats:
- Probes concentrate on the **frontier** — one level more sensitive than
the current operating point (3 of every 4 probes), with the remainder
self-checking the operating level.
- **Step down** (more sensitive) when the frontier has ≥300 samples and
its false-positive rate is ≤1%.
- **Step up** (less sensitive) quickly when the operating level itself
shows a false-positive rate above 2% over ≥50 samples — false positives
at the operating point cost real transmissions.
- The offset is persisted to flash whenever it steps.
At the default 15-second probe interval a step decision lands roughly
every **12 hours**, so the node tracks a changing RF environment within
that window without thrashing.
The offset is clamped to **8…+12** levels around the family base — wide
enough that a dense hilltop can settle much less sensitive and a quiet
valley node much more sensitive. The driver additionally clamps the
absolute detPeak (SX126x 1540, LR11xx/LR20xx 4890). That absolute clamp
is a *firmware guardrail*, not a chip limit — `cadDetPeak` is a full
8-bit register (0255) — it simply stops the staircase from wandering
into "CAD never fires" (detPeak too high → LBT effectively off) or "CAD
always busy" (too low → node can't transmit) territory. Quiet sites
converge *below* the standard value — more sensitive LBT than any
fixed-config firmware, meaning fewer TX-over-RX stomps. Noisy sites
ratchet up until false positives vanish.
To watch or hand-tune instead of letting it self-adjust, set
`cad.auto off` (see below).
## Reading the status
Out of the box the node calibrates itself — you don't have to do
anything. To see what it's doing:
```
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%
```
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.
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.
### Dry-run / manual tuning
Prefer to observe or pin the value yourself? Turn the staircase off:
```
set cad.auto off
```
Probing continues (stats keep updating) but the operating offset no
longer moves. In dry-run the probe sweep covers levels 4…+4 evenly, so
`get cad` shows the whole false-positive-vs-detPeak curve. Read it
bottom-up: the level where `fp%%` jumps from ~0 is the knee; sit one step
above it. Then pin it:
```
set cad.offset -1
```
Negative offset = more sensitive LBT (catches weaker signals, risks false
busy); positive = less sensitive. Range 8…+12. Re-enable the staircase
any time with `set cad.auto on`. Offset changes (manual or automatic)
appear in the log as `cad: step down/up -> offset N`.
To collect dry-run data faster, drop the interval (probing is temporary
then): `set cad.probe.interval 10`. At the default 15 s, allow ~a day for
a knee to resolve clearly, longer to capture day/night variation.
## Command reference
| Command | Default | Description |
|---|---|---|
| `get cad` | | Status + per-level statistics (see above). |
| `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.reset` | | Clear accumulated statistics (RAM only). |
All settings persist in prefs and apply to every role — repeater, room
server, and companion (companions reach the CLI via the v-contact admin
chat or USB serial). Command keywords are case-insensitive (`Get cad`
works); command *values* like passwords and node names are not, so `on`
/`off` must be lowercase.
## Notes and limits
- **SX127x boards** (TTGO LoRa32, T-Beam classic) have no hardware CAD;
`get cad` reports `not available` and the settings are inert. Their LBT
remains the RSSI-based `int.thresh` gate.
- **The false-positive side is measured; the miss side mostly is not.**
A too-high detPeak shows up as on-air collisions, which a single node
cannot observe. That is why the workflow is "find the lowest clean
level and sit at it", never "raise it until problems stop being
visible". The `tp` column is the one local miss-side signal: probes
that detected real below-noise-floor traffic.
- **detPeak is the only adaptive knob.** `cadDetMin` stays at Semtech's
universal 10; the symbol count is fixed at 4 (better mid-payload
detection than 2 — most of a mesh packet's airtime is payload, and
that is where a pre-TX CAD usually lands). The drivers scale their
CAD timeout with symbol count and preset automatically.
- Statistics are RAM-only and restart after a reboot; the learned
*offset* is persisted. Mixed fleets are fine — this feature changes
only when *this* node decides the channel is busy, not anything on
the air.
- Related but separate: `int.thresh` (RSSI-above-floor software gate)
and `dc.restarts` (sniff-mode false-preamble counter) still work as
before; adaptive CAD complements them.