SmartCAD p2

This commit is contained in:
liquidraver
2026-07-12 14:18:07 +02:00
parent dc1239537e
commit a320a7646d
8 changed files with 146 additions and 83 deletions
+69 -56
View File
@@ -26,7 +26,7 @@ feature measures CAD behaviour directly instead of deriving it from RSSI.
## How it works
Every `cad.probe.interval` seconds (default 60), when the radio is idle
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
@@ -61,9 +61,10 @@ 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)
### Auto mode (staircase) — ON by default
With `cad.auto on`, a one-sided staircase controller acts on the stats:
`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
@@ -73,86 +74,98 @@ With `cad.auto on`, a one-sided staircase controller acts on the stats:
- **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 clamped to 4…+4 around the family base and persisted to
flash whenever it steps (steps are hours apart).
- The offset is persisted to flash whenever it steps.
Quiet sites converge *below* the standard value — more sensitive LBT than
any fixed-config firmware, meaning fewer TX-over-RX stomps. Noisy sites
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.
## Recommended workflow (dry-run first)
To watch or hand-tune instead of letting it self-adjust, set
`cad.auto off` (see below).
Everything ships **observing but not acting**: probes run and counters
accumulate from first boot, but the operating detPeak stays at the
family default until you enable auto mode or set an offset by hand.
## Reading the status
1. **Let it observe.** Optionally speed up data collection during the
observation window:
Out of the box the node calibrates itself — you don't have to do
anything. To see what it's doing:
```
set cad.probe.interval 15
```
```
get cad
```
2. **Read the curve.** After some hours:
Compact output (kept short so it survives a truncated LoRa reply):
```
get cad
```
```
> 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%
```
Example output:
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.
```
> auto:off offset:0 peak:21 (base 21, 4 sym) probe:15s
lvl -3 (peak 18): 240 probes, 31 busy, 28 fp, 3 tp (fp 11.6%)
lvl -2 (peak 19): 241 probes, 9 busy, 7 fp, 2 tp (fp 2.9%)
lvl -1 (peak 20): 240 probes, 3 busy, 1 fp, 2 tp (fp 0.4%)
lvl +0 (peak 21): 241 probes, 2 busy, 0 fp, 2 tp (fp 0.0%)
lvl +1 (peak 22): 240 probes, 2 busy, 0 fp, 2 tp (fp 0.0%)
lvl +2 (peak 23): 240 probes, 1 busy, 0 fp, 1 tp (fp 0.0%)
```
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.
Read it bottom-up: the false-positive rate jumps somewhere (here
between 1 and 2). The lowest clean level (1) is your site's knee.
In dry-run mode the sweep covers levels 3…+2 evenly.
### Dry-run / manual tuning
How long to wait (at the default 60 s interval; divide by 4 at 15 s):
Prefer to observe or pin the value yourself? Turn the staircase off:
| After | You get |
|---|---|
| 1224 h | Knee location, coarsely — enough for a manual offset |
| 23 days | Per-level rates at ~1% resolution, incl. day/night variation |
| 1 week | Solid, weekday/weekend-proof picture |
```
set cad.auto off
```
3. **Act.** Either pin it manually:
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
```
```
set cad.offset -1
```
or let the staircase manage it from here on:
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`.
```
set cad.auto on
set cad.probe.interval 60
```
With auto on, `get cad` keeps showing the live state; offset changes
appear in the log as `cad: step down/up -> offset N` and are saved to
flash automatically.
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>` | off | Staircase controller acts on the stats. |
| `set cad.offset <n>` | 0 | Operating offset, 4…4. Negative = more sensitive. Applied live. |
| `set cad.probe.interval <sec>` | 60 | Probe cadence; 0 disables probing (and freezes auto), 10255 otherwise. |
| `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).
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
+3 -3
View File
@@ -253,9 +253,9 @@ Changes are persisted immediately unless noted. Some require a reboot.
| `set rxduty <0\|1\|on\|off>` | | RX duty cycle mode *(reboot required)*. Window timing auto-sized per SF/BW/preamble from the SX126x datasheet constraints (boot log line `rxduty:` shows the result). Zero-loss guarantee assumes senders on preamble-32 firmware (current MeshCore at SF≤8); legacy preamble-16 senders are only caught ~50% worst-phase — keep off until the local mesh has converted. Presets with 16-symbol preambles (SF≥9) fall back to continuous RX automatically. |
| `set adc.multiplier <mult>` | (0 = use board default) | Battery voltage ADC calibration multiplier |
| `set meshtimesync <on\|off>` | default **off** | Mesh time sync: automatically correct this node's clock from the consensus of Ed25519-signed advert timestamps heard on the mesh. Steps at most ±1 h per step, one step per 6 h; abstains without a quorum (default 6) of tenured agreeing senders; never overrides a clock set in the last 7 days, whether from GPS (re-armed on every fix) or a manual set. See `MESHTIMESYNC.md`. |
| `set cad.auto <on\|off>` | default **off** | Adaptive CAD: let the staircase controller move the operating detPeak offset based on probe statistics. Leave off (dry-run) until `get cad` has accumulated a few days of per-level data. See `ADAPTIVE_CAD.md`. |
| `set cad.offset <n>` | 4 to 4, default 0 | Operating detPeak offset from the chip family's per-SF base (SX126x: SF+13; LR11xx/LR20xx: 5668 table). Negative = more sensitive LBT (catches weaker signals, risks false busy), positive = less sensitive. Applied live; the auto staircase may move it later if `cad.auto` is on. |
| `set cad.probe.interval <sec>` | 0 (off) or 10255, default **60** | Seconds between calibration CAD probes. Lower (1520 s) during a dry-run observation window to accumulate statistics faster; 0 disables probing entirely (also freezes auto adaptation). |
| `set cad.auto <on\|off>` | default **on** | Adaptive CAD: let the staircase controller move the operating detPeak offset based on probe statistics. On by default (repeaters and companions); at the default 15 s probe interval it responds to environment change in ~12 h. Turn off to observe/hand-tune via `get cad` + `set cad.offset`. See `ADAPTIVE_CAD.md`. |
| `set cad.offset <n>` | 8 to 12, default 0 | Operating detPeak offset from the chip family's per-SF base (SX126x: SF+13; LR11xx/LR20xx: 5668 table). Negative = more sensitive LBT (catches weaker signals, risks false busy), positive = less sensitive. Wide range so dense hilltops / quiet valleys can settle far from base. The per-family absolute clamp in the driver (SX126x 1540, LR 4890) is a firmware guardrail against a CAD that never/always fires, not a chip limit (`cadDetPeak` is a full `uint8_t`). Applied live; the auto staircase may move it later if `cad.auto` is on. |
| `set cad.probe.interval <sec>` | 0 (off) or 10255, default **15** | Seconds between calibration CAD probes. Default 15 s → ~12 h staircase response. 0 disables probing entirely (also freezes auto adaptation). |
| `set cad.reset` | | Clear the accumulated per-level CAD probe statistics (RAM only; also cleared automatically on any radio parameter change). |
| `set prv.key <hex>` | 64-char hex (32-byte key) | Replace private key; derive new identity *(reboot to apply)* |
@@ -702,10 +702,10 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
}
}
/* Offset 156: cad_offset (ZephCore extension, signed -4..4, default 0) */
/* Offset 156: cad_offset (ZephCore extension, signed, default 0) */
if (off < len) {
prefs.cad_offset = (int8_t)buf[off++];
if (prefs.cad_offset < -4 || prefs.cad_offset > 4) {
if (prefs.cad_offset < CAD_OFFSET_MIN || prefs.cad_offset > CAD_OFFSET_MAX) {
prefs.cad_offset = 0;
}
}
+16 -11
View File
@@ -1140,15 +1140,20 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
int n = 0;
if (base == 0) {
return snprintf(buf, cap, "CAD: not supported by this radio");
return snprintf(buf, cap, "cad n/a");
}
int peak = (int)base + _cad_offset;
/* Terse on purpose — remote replies are capped at ~160 B over LoRa.
* Header: a:on o:1 pk:22(b21/4s) iv:15s
* a auto on/off o offset pk operating peak
* b family base 4s symbols iv probe interval
* Level: -3(18) 22p 18b 16f 2t 72%
* level(peak) probes busy fp tp fp-rate%% (integer). */
n += snprintf(buf + n, cap > n ? cap - n : 0,
"auto:%s offset:%d peak:%d (base %u, 4 sym) probe:%us",
_cad_auto ? "on" : "off", (int)_cad_offset, peak,
base, (unsigned)_cad_probe_interval_s);
"a:%s o:%d pk:%d(b%u/4s) iv:%us",
_cad_auto ? "on" : "off", (int)_cad_offset,
(int)base + _cad_offset, base,
(unsigned)_cad_probe_interval_s);
for (int i = 0; i < CAD_NUM_LEVELS; i++) {
CadLevelStats &s = _cad_stats[i];
@@ -1156,14 +1161,14 @@ int LoRaRadioBase::formatCadStatus(char *buf, int cap)
if (s.probes == 0) {
continue;
}
/* FP rate in tenths of a percent */
uint32_t fp_pm = ((uint32_t)s.fp * 1000U) / s.probes;
/* Integer FP rate, rounded to nearest percent. */
unsigned fp_pct = (unsigned)(((uint32_t)s.fp * 100U + s.probes / 2)
/ s.probes);
n += snprintf(buf + n, cap > n ? cap - n : 0,
"\nlvl %+d (peak %d): %u probes, %u busy, %u fp, %u tp (fp %u.%u%%)",
"\n%+d(%d) %up %ub %uf %ut %u%%",
i + CAD_LEVEL_MIN, (int)base + i + CAD_LEVEL_MIN,
s.probes, s.busy, s.fp, s.tp,
(unsigned)(fp_pm / 10), (unsigned)(fp_pm % 10));
s.probes, s.busy, s.fp, s.tp, fp_pct);
}
return n;
+10 -4
View File
@@ -26,11 +26,17 @@
* false-positive rate stays under target. Levels are signed offsets from
* the chip family's per-SF base detPeak (SX126x: SF+13; LR11xx/LR20xx:
* 56-68 table) so the C++ layer stays scale-independent. */
#define CAD_LEVEL_MIN (-4) /* most sensitive probe level */
#define CAD_LEVEL_MAX 4 /* least sensitive probe level */
/* Operating-offset range (levels from the per-SF family base detPeak). Wide
* on purpose a dense hilltop may need a much higher detPeak than a quiet
* valley node; the per-family absolute clamp in the driver (SX126x 15-40,
* LR 48-90) is a firmware guardrail against "CAD never/always fires", NOT a
* chip limit (cadDetPeak is a full uint8_t).
* MUST match CAD_OFFSET_MIN/MAX in helpers/NodePrefs.h. */
#define CAD_LEVEL_MIN (-8) /* most sensitive probe level */
#define CAD_LEVEL_MAX 12 /* least sensitive probe level */
#define CAD_NUM_LEVELS (CAD_LEVEL_MAX - CAD_LEVEL_MIN + 1)
#define CAD_SWEEP_MIN (-3) /* dry-run sweep window */
#define CAD_SWEEP_MAX 2
#define CAD_SWEEP_MIN (-4) /* dry-run sweep window (get cad with auto off) */
#define CAD_SWEEP_MAX 4
#define CAD_FP_TARGET_PERMILLE 10 /* step-down needs FP rate <= 1% */
#define CAD_STEP_DOWN_MIN_PROBES 300 /* samples before a down-step call */
#define CAD_STEP_UP_MIN_PROBES 50 /* samples before an up-step call */
+1 -1
View File
@@ -240,7 +240,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
if (prefs.apc_margin < 6 || prefs.apc_margin > 30) prefs.apc_margin = 16;
if (prefs.meshtimesync > 1) prefs.meshtimesync = 0;
if (prefs.cad_auto > 1) prefs.cad_auto = 0;
if (prefs.cad_offset < -4 || prefs.cad_offset > 4) prefs.cad_offset = 0;
if (prefs.cad_offset < CAD_OFFSET_MIN || prefs.cad_offset > CAD_OFFSET_MAX) prefs.cad_offset = 0;
if (prefs.cad_probe_interval != 0 && prefs.cad_probe_interval < 10) prefs.cad_probe_interval = 10;
/* One-time format upgrade: old files (< 294 bytes) never saved the ZephCore
+30 -3
View File
@@ -171,7 +171,7 @@ void CommonCLI::loadPrefs(const char* path) {
_prefs->flood_max_advert = constrain(_prefs->flood_max_advert, (uint8_t)0, (uint8_t)64);
_prefs->meshtimesync = constrain(_prefs->meshtimesync, (uint8_t)0, (uint8_t)1);
_prefs->cad_auto = constrain(_prefs->cad_auto, (uint8_t)0, (uint8_t)1);
_prefs->cad_offset = constrain(_prefs->cad_offset, (int8_t)-4, (int8_t)4);
_prefs->cad_offset = constrain(_prefs->cad_offset, (int8_t)CAD_OFFSET_MIN, (int8_t)CAD_OFFSET_MAX);
if (_prefs->cad_probe_interval != 0 && _prefs->cad_probe_interval < 10) {
_prefs->cad_probe_interval = 10;
}
@@ -304,6 +304,32 @@ void CommonCLI::scheduleReboot(uint8_t type)
}
void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, char* reply) {
/* Case-insensitive command keywords. Lowercase only the first two
* whitespace-delimited tokens (the verb and the config key) the value
* (3rd token onward) is preserved verbatim, so case-sensitive arguments
* like passwords, node names, owner info, and keys are never mangled.
* Fixes e.g. "Get cad" / "SET Cad.Auto on" not being recognized. */
char norm[CLI_REPLY_SIZE];
{
int tok = 0; /* completed tokens so far */
bool in_tok = false;
size_t j = 0;
for (size_t i = 0; command[i] != '\0' && j < sizeof(norm) - 1; i++) {
char c = command[i];
if (c == ' ' || c == '\t') {
if (in_tok) { in_tok = false; tok++; }
} else {
in_tok = true;
if (tok < 2 && c >= 'A' && c <= 'Z') {
c = (char)(c - 'A' + 'a');
}
}
norm[j++] = c;
}
norm[j] = '\0';
command = norm;
}
if (strcmp(command, "start dfu") == 0) {
/* Reboot into UF2 bootloader for firmware update */
strcpy(reply, "OK - rebooting to UF2 DFU");
@@ -637,8 +663,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
}
} else if (memcmp(config, "cad.offset ", 11) == 0) {
int val = atoi(&config[11]);
if (val < -4 || val > 4) {
strcpy(reply, "Error: offset range is -4..4");
if (val < CAD_OFFSET_MIN || val > CAD_OFFSET_MAX) {
snprintf(reply, CLI_REPLY_SIZE, "Error: offset range is %d..%d",
CAD_OFFSET_MIN, CAD_OFFSET_MAX);
} else {
_prefs->cad_offset = (int8_t)val;
_callbacks->applyCadPrefs();
+15 -3
View File
@@ -24,6 +24,18 @@
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
/* Adaptive-CAD operating detPeak offset range (levels from the family base).
* Wide on purpose: a dense hilltop can need a much higher detPeak than a quiet
* valley node. The per-family absolute clamp inside the driver (SX126x 15-40,
* LR11xx/LR20xx 48-90) is a firmware guardrail, NOT a chip limit cadDetPeak
* is a full uint8_t (0-255). It just keeps the staircase from wandering into
* "CAD never fires" (too high) or "CAD always busy" (too low) territory. This
* offset range limits how far the staircase / manual offset may roam; MUST
* match CAD_LEVEL_MIN/MAX in adapters/radio/radio_common.h (they index the
* per-level stats array). */
#define CAD_OFFSET_MIN (-8)
#define CAD_OFFSET_MAX 12
struct NodePrefs {
/* ---- Common fields (both roles) ---- */
float airtime_factor;
@@ -139,9 +151,9 @@ static inline void initNodePrefs(NodePrefs* prefs) {
prefs->rx_duty_cycle = 0; // Default OFF — continuous RX for best reliability
prefs->apc_enabled = 0; // Default OFF — fixed TX power
prefs->apc_margin = 16; // Default 16 dB target link margin
prefs->cad_auto = 0; // Default OFFdry-run: probes + counters only
prefs->cad_offset = 0; // Family base detPeak (SF+13 on SX126x)
prefs->cad_probe_interval = 60; // One CAD probe per minute
prefs->cad_auto = 1; // Default ONadaptive staircase acts on probe stats
prefs->cad_offset = 0; // Start at family base detPeak (SF+13 on SX126x)
prefs->cad_probe_interval = 15; // 15 s → staircase responds to change in ~1-2 h
prefs->wake_on_msg = 1; // Default ON — wake display when message arrives
prefs->v_contact_enabled = 1; // Default ON — v-contact loopback admin chat (companion)
prefs->v_battery_alert_mv = 0xFFFF; // Sentinel: derive from board auto-shutdown threshold