From 1db0a14c4ba49d83eed68c0c4c72074130054aec Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:42:51 +0200 Subject: [PATCH] get cad fix --- docs/Repeater_CLI_commands.md | 5 ++++- zephcore/helpers/CommonCLI.cpp | 26 ++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/Repeater_CLI_commands.md b/docs/Repeater_CLI_commands.md index fe8d2bb..d8d6a5e 100644 --- a/docs/Repeater_CLI_commands.md +++ b/docs/Repeater_CLI_commands.md @@ -280,7 +280,10 @@ All `set uplink.*` changes are saved immediately and only applied after reboot. | `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off | | `get dc.restarts` | Duty-cycle re-arm counter — RxTimeout re-arms **plus** parked-RX watchdog recoveries, sharing one total. **Read it as a rate: divide by uptime.** A bare count is not interpretable, and the two sources it merges cost very differently. An RxTimeout re-arm is ~7 ms of deaf time (the `Calibrate(ALL)` gap in the driver's `restart_rx`) after which the chip returns to duty cycle immediately — packets, not power. A watchdog recovery means the chip sat parked in *full RX* for one to two watchdog periods (`2·(preamble+8)` symbols, floored at 250 ms) — power, not packets, since parked RX still receives. The counter cannot tell you which, so read the worst case. **Measured normal: ~250/hr on a high site at SF8/BW 62.5** (one every ~14 s), where the worst case — every event a park — costs about 3.5% of the duty cycle's savings. Nothing to act on below roughly **2000/hr**; above that the parked-RX share starts eating a meaningful fraction of the saving and it becomes worth splitting the counter to find out. A high rate means the preamble detector is tripping without a decodable packet following, which on an elevated site is usually distant marginal traffic rather than interference — cross-check `get cad.stats`, whose adaptive detPeak offset rises independently in a genuinely busy RF environment. Reset by `clear stats`. | | `get cad` | Always `on` — ZephCore performs CAD/LBT unconditionally and has no enable knob. Kept as a boolean reply for Arduino MeshCore app compatibility; the real status lives in `get cad.stats`. | -| `get cad.stats` | Adaptive-CAD status: header (`a` auto on/off, `o` operating detPeak offset, `pk` absolute peak with family base, `sp` noise-floor RSSI burst quality as `mean-spread-dB/zero-spread-%` (plus `(burst-count rN/bN/aN)` on the local USB console, omitted over the air to protect the 161 B reply budget, where `r` is completed RSSI reads, `b` reads the chip refused as busy, and `a` bursts abandoned because of one — on a healthy radio `b`/`a` stay at 0, and a large `a` against a near-zero burst count is the signature of a sampler being refused rather than one losing the odd read) — a non-zero mean proves the 8 reads are independent however high the share climbs; only mean `0.0` with a high share indicts the sampler. See `ADAPTIVE_CAD.md`. `bc` busy cap), then a 3-rung window around the operating offset (`*` marks it) with probe/busy/fp/tp counts and false-positive rate — the three levels the knee controller reads. Probing runs even while `cad.auto` is off (dry-run), so this is the observation tool for picking a site-appropriate detPeak. See `ADAPTIVE_CAD.md`. Not available on SX127x boards (no hardware CAD). | +| `get cad.auto` | Whether the adaptive-CAD staircase is acting on probe statistics (`on`/`off`). Set with `set cad.auto`. | +| `get cad.offset` | Operating detPeak offset from the chip family's per-SF/per-bandwidth base, signed. Also shown as `o` in `get cad.stats`, alongside the absolute peak it resolves to. | +| `get cad.busycap` | Faint-tolerance / airtime cap in percent, or `0 (off)`. Also shown as `bc` in `get cad.stats`. | +| `get cad.stats` | Adaptive-CAD status: header (`a` auto on/off, `o` operating detPeak offset, `pk` absolute peak with family base, `sp` noise-floor RSSI burst quality as `mean-spread-dB/zero-spread-%` (plus `(burst-count rN/bN/aN/dN)` on the local USB console, omitted over the air to protect the 161 B reply budget, where `r` is completed RSSI reads, `b` reads the chip refused as busy, `a` bursts abandoned because of one, and `d` attempts turned away before a burst even started by the duty-cycle sleep window — on a healthy radio `b`/`a` stay at 0, and a large `a` against a near-zero burst count is the signature of a sampler being refused rather than one losing the odd read) — a non-zero mean proves the 8 reads are independent however high the share climbs; only mean `0.0` with a high share indicts the sampler. See `ADAPTIVE_CAD.md`. `bc` busy cap), then a 3-rung window around the operating offset (`*` marks it) with probe/busy/fp/tp counts and false-positive rate — the three levels the knee controller reads. Probing runs even while `cad.auto` is off (dry-run), so this is the observation tool for picking a site-appropriate detPeak. See `ADAPTIVE_CAD.md`. Not available on SX127x boards (no hardware CAD). | | `get extra.sf` | LR2021 side detectors: the extra spreading factors currently received alongside `sf`, comma-separated (bare, no `> ` prefix), or `No extra SF configured`. Reflects the saved prefs, not what the chip accepted — if the set became invalid after an `sf`/`bw` change it is reported here but was refused at boot (a `WRN` line says so). | | `get adc.multiplier` | Battery voltage ADC calibration multiplier | | `get bootloader.ver` | Bootloader version string | diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 866c4c4..f527f3f 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -537,9 +537,31 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch if (_callbacks->formatCadStatus(reply + n, (int)cap - n) == 0) { strcpy(reply, "not available"); } - } else if (memcmp(config, "cad", 3) == 0) { + } else if (memcmp(config, "cad.auto", 8) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %s", + _prefs->cad_auto ? "on" : "off"); + } else if (memcmp(config, "cad.offset", 10) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->cad_offset); + } else if (memcmp(config, "cad.busycap", 11) == 0) { + if (_prefs->cad_busycap == 0) { + strcpy(reply, "> 0 (off)"); + } else { + snprintf(reply, CLI_REPLY_SIZE, "> %u", + (unsigned)_prefs->cad_busycap); + } + } else if (strcmp(config, "cad") == 0) { /* Arduino exposes this as a boolean knob; ZephCore always does CAD, - * so the answer is a constant "on". Apps parse the word. */ + * so the answer is a constant "on". Apps parse the word. + * + * EXACT match, not the 3-byte prefix this used to be. Every branch + * in this chain is a memcmp prefix test, so "cad" matched every + * `get cad.*` that had no branch of its own and answered "on" for + * all of them -- a percentage, a signed offset and an action all + * came back as a boolean. cad.stats escaped only by being ordered + * above it, which is a fragile way to stay correct. Anchoring this + * one fixes the whole family at once: anything under cad. without a + * getter now falls through to the honest "??:" reply instead of + * being answered wrongly. */ strcpy(reply, "> on"); } else if (memcmp(config, "extra.sf", 8) == 0) { /* No "> " prefix, and the empty case is a sentence -- both match