From 108b96d36c205b11ee6984e12303ef6ec9b1fcf1 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:35:28 +0200 Subject: [PATCH] rename cli commands and fix it --- README.md | 8 +- docs/ARCHITECTURE.md | 2 +- ...peater_CLI_commands.md => CLI_commands.md} | 172 ++++++++++++------ docs/DESIGN.md | 2 +- docs/LINUX_NATIVE.md | 2 +- 5 files changed, 128 insertions(+), 58 deletions(-) rename docs/{Repeater_CLI_commands.md => CLI_commands.md} (60%) diff --git a/README.md b/README.md index 2161ef3..68b1e23 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@
Flash it · Boards · - CLI · + CLI · Design · Release notes
@@ -62,9 +62,9 @@ the [supported boards list](docs/supported_boards.md). | Role | What it does | How you talk to it | |---|---|---| | **Companion** | Your personal node: contacts, channels, offline message queue | MeshCore app over BLE or USB (WiFi on some ESP32 boards) | -| **Repeater** | Relays packets for everyone else | [Text CLI](docs/Repeater_CLI_commands.md) over USB, or remote admin over the mesh | +| **Repeater** | Relays packets for everyone else | [Text CLI](docs/CLI_commands.md) over USB, or remote admin over the mesh | | **Room server** | A shared message board that keeps posts for clients who were away | Same CLI as the repeater; users log in from the app | -| **Observer** | Listens only and publishes what it hears to MQTT over WiFi (ESP32) | Same CLI | +| **Observer** | Listens only and publishes what it hears to MQTT over WiFi (ESP32) | [Its own small CLI](docs/CLI_commands.md#observer-zephcore-only) over USB | One role per firmware image; you choose it when you flash. @@ -168,7 +168,7 @@ ZephCore is a plain west workspace application, and it tries to be a well-behave | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Component reference | | [adr/](docs/adr/README.md) | Decisions and why they were made | | [BUILDING.md](docs/BUILDING.md) | Every build variant, platform notes, Kconfig options | -| [Repeater_CLI_commands.md](docs/Repeater_CLI_commands.md) | Every CLI command | +| [CLI_commands.md](docs/CLI_commands.md) | Every CLI command | | [supported_boards.md](docs/supported_boards.md) | Boards and build strings | | [LINUX_NATIVE.md](docs/LINUX_NATIVE.md) | Running on a Pi or Femtofox | | [releasenotes/](releasenotes/) | What changed in each release | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d42a8f7..c6aa673 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -744,7 +744,7 @@ Companion extras: `get|set autoshutdown`, `get|set v.contact`, `get|set v.batter Stats: `stats-core/stats-radio/stats-packets`, `clear stats` Time: `clock`, `clock sync`, `time` | Location code used in the MQTT topics (e.g. `BUD`); rebuilds the topics and reconnects MQTT |
+
+---
+
## `get` — Read Configuration
| Command | Returns |
@@ -269,7 +338,7 @@ Companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: tr
| `get repeat` | Forwarding enabled: `on` or `off` |
| `get radio` | Radio params as `freq,bw,sf,cr` — the same comma-separated form `set radio` takes, so a reply can be edited and sent straight back |
| `get freq` | Frequency in MHz |
-| `get freqerr` | Carrier frequency error measured on received packets: `mean N Hz, min A, max B, K pkts`. **LR2021 only** — other radios answer `not available`. Purely diagnostic; nothing acts on it. **The mean only approximates *this* node's reference error once it is averaged over many different peers** — their individual errors cancel, ours does not — so read `K` and the min/max spread before believing it: a tight spread over a handful of packets is one chatty neighbour, not a population. Small values are the expected answer and mean there is nothing to do; LoRa tolerates carrier error up to roughly a quarter of the bandwidth before sensitivity suffers, so at BW 62.5 kHz a few hundred Hz is noise. If it is kHz-scale the correction is board-dependent: XTAL parts have `SetXoscCpTrim`, but **TCXO parts have no chip-side trim at all** (DS §6.11.4: "If a TCXO is configured, this command has no effect"), leaving only a software offset to the programmed frequency. Values beyond ±200 kHz are discarded by the driver and warn once — the field is decoded from three `GetLoraPacketStatus` bytes that DS rev 2.1 does not document, so implausible readings are evidence the field is not real on that firmware rather than a genuine measurement. Reset by `clear stats`. |
+| `get freqerr` | *(ZephCore only)* Carrier frequency error measured on received packets: `mean N Hz, min A, max B, K pkts`. **LR2021 only** — other radios answer `not available`. Purely diagnostic; nothing acts on it. **The mean only approximates *this* node's reference error once it is averaged over many different peers** — their individual errors cancel, ours does not — so read `K` and the min/max spread before believing it: a tight spread over a handful of packets is one chatty neighbour, not a population. Small values are the expected answer and mean there is nothing to do; LoRa tolerates carrier error up to roughly a quarter of the bandwidth before sensitivity suffers, so at BW 62.5 kHz a few hundred Hz is noise. If it is kHz-scale the correction is board-dependent: XTAL parts have `SetXoscCpTrim`, but **TCXO parts have no chip-side trim at all** (DS §6.11.4: "If a TCXO is configured, this command has no effect"), leaving only a software offset to the programmed frequency. Values beyond ±200 kHz are discarded by the driver and warn once — the field is decoded from three `GetLoraPacketStatus` bytes that DS rev 2.1 does not document, so implausible readings are evidence the field is not real on that firmware rather than a genuine measurement. Reset by `clear stats`. |
| `get tx` | TX power in dBm |
| `get lat` | Stored latitude |
| `get lon` | Stored longitude |
@@ -278,7 +347,7 @@ Companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: tr
| `get txdelay` | Adaptive TX delay status: contention estimate and flood delay factor |
| `get rxdelay` | *(deprecated)* Always returns "adaptive (rxdelay deprecated)" |
| `get direct.txdelay` | *(deprecated)* Always returns "adaptive (direct.txdelay deprecated)" |
-| `get backoff.multiplier` | Per-dupe reactive backoff multiplier |
+| `get backoff.multiplier` | *(ZephCore only)* Per-dupe reactive backoff multiplier |
| `get flood.max` | Max flood retransmit hops |
| `get flood.max.unscoped` | Max retransmit hops for un-scoped floods |
| `get flood.max.advert` | Max retransmit hops for ADVERT floods |
@@ -288,36 +357,37 @@ Companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: tr
| `get guest.password` | Guest access password |
| `get owner.info` | Owner/contact info (pipes `\|` display as newlines) |
| `get int.thresh` | Interference threshold |
-| `get leds` | LED master switch: `on` or `off` |
-| `get leds.radio` | Activity-LED mode: `tx`, `rx`, `all` or `off`. Appends `(no radio LED on this board)` where the board has no `lora-tx-led` alias |
-| `get leds.hb` | Heartbeat-LED mode: `all`, `hb`, `unread` or `off`. Appends `(no heartbeat LED on this board)` where the board has neither `led0` nor `led1` |
-| `get buzzer` | *(room server only)* Buzzer/vibration mode as ` ()`: `0 (silent)`, `1 (sound+vib)`, `2 (vibrate)`, `3 (sound)`. Compiled out on repeater builds (`#ifndef ZEPHCORE_REPEATER`) — a repeater answers `unknown config: buzzer`. |
+| `get leds` | *(ZephCore only)* LED master switch: `on` or `off` |
+| `get leds.radio` | *(ZephCore only)* Activity-LED mode: `tx`, `rx`, `all` or `off`. Appends `(no radio LED on this board)` where the board has no `lora-tx-led` alias |
+| `get leds.hb` | *(ZephCore only)* Heartbeat-LED mode: `all`, `hb`, `unread` or `off`. Appends `(no heartbeat LED on this board)` where the board has neither `led0` nor `led1` |
+| `get buzzer` | *(ZephCore only; room server only)* Buzzer/vibration mode as ` ()`: `0 (silent)`, `1 (sound+vib)`, `2 (vibrate)`, `3 (sound)`. Compiled out on repeater builds (`#ifndef ZEPHCORE_REPEATER`) — a repeater answers `unknown config: buzzer`. |
| `get agc.reset.interval` | Removed — replies `Removed - Automatic AGC reset is on`. Periodic AGC recalibration was deleted (it reset the noise floor to its unseeded sentinel on every fire). Use `set rxduty` to cut RX current. |
| `get multi.acks` | Extra ACK transmit count (`0` or `1`) |
| `get path.hash.mode` | Path hashing algorithm: `0`, `1`, or `2` |
| `get loop.detect` | Loop detection level: `off`, `minimal`, `moderate`, or `strict` |
| `get radio.rxgain` | RX gain boost: `on` or `off` |
| `get radio.fem.rxgain` | External FEM's LNA in the RX path: `on` (through the LNA) or `off` (bypassed). Default `on` |
-| `get rxduty` | RX duty cycle mode: `0` or `1` |
-| `get display.rotate` | Panel 180-degree rotation: `0` or `1`. Reports the **live panel state**, not the stored byte — the two differ only when a rotation was refused, which is the case worth seeing. Boards whose panel cannot rotate reply `unsupported (panel cannot rotate)` |
-| `get input.rotate` | Joystick/D-pad axis swap: `0` or `1` |
+| `get rxduty` | *(ZephCore only)* RX duty cycle mode: `0` or `1` |
+| `get display.rotate` | *(ZephCore only)* Panel 180-degree rotation: `0` or `1`. Reports the **live panel state**, not the stored byte — the two differ only when a rotation was refused, which is the case worth seeing. Boards whose panel cannot rotate reply `unsupported (panel cannot rotate)` |
+| `get input.rotate` | *(ZephCore only)* Joystick/D-pad axis swap: `0` or `1` |
| `get tz.offset` | Display timezone: whole hours from UTC, `-12`..`14` (`0` = UTC) |
-| `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
-| `get gps standby` | GPS standby threshold in seconds (see `set gps standby`) |
-| `get gps diag` | What the last GPS module-configuration attempt did — which path ran, bytes sent, and tracked satellites per constellation. See **GPS configuration diagnostics** in the GPS section for the field reference |
-| `get meshtimesync` | Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
-| `get probe.interval` | Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off |
-| `get loop.wakes` | Debug builds only (`CONFIG_ZEPHCORE_LOOP_WAKE_STATS`, on in `debug.conf`). Event-loop wakes since boot: `wakes=N up=Ss` then a count per event bit (`bN=`, the role's `MESH_EVENT_*` bit numbers; one wake can carry several bits). Wakes per second = wakes / up. Read it twice and take the difference to measure a window. |
-| `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 gps duty` | *(ZephCore only)* Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
+| `get gps standby` | *(ZephCore only)* GPS standby threshold in seconds (see `set gps standby`) |
+| `get gps diag` | *(ZephCore only)* What the last GPS module-configuration attempt did — which path ran, bytes sent, and tracked satellites per constellation. See **GPS configuration diagnostics** in the GPS section for the field reference |
+| `get meshtimesync` | *(ZephCore only)* Mesh time-sync state + live dry-run: on/off, eligible voter count, votes for/against, consensus skew and radius, would-be verdict (`ok`/`in-band`/`step±N`/`abstain (reason)`/`hold (reason)`; a recent clock set — manual or GPS — shows as `hold (suppressed)`, and a backward step a forward-only role would refuse is annotated `(skipped: forward-only)`), step counters, suppression countdown, and a per-sender evidence table (`prefix hops count skew E`, `E` = counted toward the verdict above). Entries that count print first, so a size-capped reply never hides the ones that explain the summary; if the table doesn't fully fit, a trailing `+N more` shows how many were left out. Sensing runs even while off, so this works as a dry-run before enabling. Over remote admin the reply is truncated to the packet size (summary always fits); the full table needs the USB CLI. |
+| `get probe.interval` | *(ZephCore only)* Seconds between periodic radio measurements (noise-floor sample + CAD probe). 0 = CAD probing off |
+| `get loop.wakes` | *(ZephCore only)* Debug builds only (`CONFIG_ZEPHCORE_LOOP_WAKE_STATS`, on in `debug.conf`). Event-loop wakes since boot: `wakes=N up=Ss` then a count per event bit (`bN=`, the role's `MESH_EVENT_*` bit numbers; one wake can carry several bits). Wakes per second = wakes / up. Read it twice and take the difference to measure a window. |
+| `get mem []` | *(ZephCore only)* Builds with `CONFIG_ZEPHCORE_MEM_STATS` only (off by default, also in `debug.conf`). One line per call, `n` counting from 0 (the default): first the threads, `t prio= size= peak= free=`, then the kernel heaps, `h size= alloc= peak=`, then `end`. For sizing stacks and heaps against measured use; resolve a heap address with `nm` on `zephyr.elf`. |
+| `get dc.restarts` | *(ZephCore only)* 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.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 cad.auto` | *(ZephCore only)* Whether the adaptive-CAD staircase is acting on probe statistics (`on`/`off`). Set with `set cad.auto`. |
+| `get cad.offset` | *(ZephCore only)* 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` | *(ZephCore only)* Faint-tolerance / airtime cap in percent, or `0 (off)`. Also shown as `bc` in `get cad.stats`. |
+| `get cad.stats` | *(ZephCore only)* 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 |
-| `get pm` | ESP32 light-sleep builds: `> on asleep P% sleeps S/E wake tT gG oO radioR btnB [win Ns]` (`off` when powersaving is off): powersaving state, share of uptime asleep, sleeps taken out of light-sleep entries, wake causes (RTC timer, GPIO, other), sleeps that ended with the radio IRQ / user button active, and the console window left. Radio wakes should track the packets the node hears. Other builds: `Error: no light sleep on this build` |
+| `get pm` | *(ZephCore only)* ESP32 light-sleep builds: `> on asleep P% sleeps S/E wake tT gG oO radioR btnB [win Ns]` (`off` when powersaving is off): powersaving state, share of uptime asleep, sleeps taken out of light-sleep entries, wake causes (RTC timer, GPIO, other), sleeps that ended with the radio IRQ / user button active, and the console window left. Radio wakes should track the packets the node hears. Other builds: `Error: no light sleep on this build` |
| `get pwrmgt.support` | `> supported` on nRF52 (VBUS detection, boot voltage), else `> unsupported` — upstream's reply |
| `get pwrmgt.source` | nRF52: `> external` (VBUS present) or `> battery`; elsewhere `ERROR: Power management not supported` |
| `get pwrmgt.bootreason` | `> Reset: ; Shutdown: `. The cause is this boot's hardware reset cause as labels (`PIN`, `SOFTWARE`, `BROWNOUT`, `POR`, `WATCHDOG`, `LOWPOWER` for a wake from System OFF, ...), followed by `(crash in , pc 0x...)` when the previous run ended in a fatal error and rebooted — resolve the pc with `addr2line` against the same build. The shutdown reason is `User Request`, `Low Voltage` or `None` |
@@ -331,7 +401,7 @@ Companion builds of boards whose `zephcore.yml` declares `capabilities: wifi: tr
Changes are persisted immediately unless noted. Some require a reboot.
-**The literal `default` is accepted by every `set` that takes a number or an
+*(ZephCore only)* **The literal `default` is accepted by every `set` that takes a number or an
on/off value**, and restores that setting to what a factory-fresh node boots
with — e.g. `set probe.interval default`, `set tx default`, `set cad.auto
default`. The values come from `initNodePrefs()` (`helpers/NodePrefs.h`), so the
@@ -359,7 +429,7 @@ four radio parameters together, since they are one interop-critical set.
| `set txdelay ` | | Accepted for prefs compatibility — **ignored** (txdelay is adaptive) |
| `set rxdelay ` | | Accepted for prefs compatibility — **ignored** (rxdelay is adaptive) |
| `set direct.txdelay ` | | Accepted for prefs compatibility — **ignored** (direct.txdelay is adaptive) |
-| `set backoff.multiplier ` | 0.0–2.0 | Per-dupe reactive backoff multiplier (0 = disable reactive backoff) |
+| `set backoff.multiplier ` | 0.0–2.0 | *(ZephCore only)* Per-dupe reactive backoff multiplier (0 = disable reactive backoff) |
| `set flood.max ` | 0–64 | Maximum flood retransmit hops |
| `set flood.max.unscoped ` | 0–64 | Hop limit for un-scoped floods only (default 64 = same as flood.max); scoped/transport floods still use flood.max |
| `set flood.max.advert ` | 0–64 | Hop limit for ADVERT floods only (default 8); curbs advert churn independent of flood.max |
@@ -369,29 +439,29 @@ four radio parameters together, since they are one interop-critical set.
| `set guest.password ` | | Set guest access password |
| `set owner.info ` | Use `\|` for newlines | Owner/contact information |
| `set int.thresh ` | | Interference detection threshold |
-| `set buzzer <0\|1\|2\|3>` | or `off` / `on` / `vibrate` / `sound` | *(room server only)* `0`/`off` silent, `1`/`on` sound + vibration, `2`/`vibrate` vibration only, `3`/`sound` sound only. Modes 2 and 3 need a vibration motor; without one the node replies `Error: no vibration motor on this board - use 0 or 1`. Applied live and persisted. Compiled out on repeater builds. |
-| `set leds ` | default **on** | Master switch for every LED on the node, applied live and persisted: heartbeat, unread-message and LoRa TX-activity LEDs, plus the message and shutdown flashes. Works on every role, including headless repeaters where the TX LED is the only one that ever lights. Does **not** cover the display backlight, which is a separate UI brightness setting. |
-| `set leds.radio ` | default **tx** | What the LoRa activity LED reacts to, applied live and persisted. `tx` lights it for the duration of each transmit (the behaviour before this setting existed), `rx` gives a 30 ms blink per valid packet received, `all` does both, `off` keeps it dark. Sits **below** `set leds` — the master switch off keeps it dark whatever this says. Only boards defining the `lora-tx-led` alias have this LED; elsewhere the value is stored but does nothing, and the reply says so. |
-| `set leds.hb ` | default **all** | What the heartbeat LED reacts to, applied live and persisted. `all` is the 4 s liveness tick that widens from 20 ms to 200 ms while messages are unread (the behaviour before this setting existed), `hb` never widens, `unread` stays dark until there are unread messages, `off` keeps it dark. Also sits below `set leds`. See the LED-topology notes below for what this does on single-LED boards. |
+| `set buzzer <0\|1\|2\|3>` | or `off` / `on` / `vibrate` / `sound` | *(ZephCore only; room server only)* `0`/`off` silent, `1`/`on` sound + vibration, `2`/`vibrate` vibration only, `3`/`sound` sound only. Modes 2 and 3 need a vibration motor; without one the node replies `Error: no vibration motor on this board - use 0 or 1`. Applied live and persisted. Compiled out on repeater builds. |
+| `set leds ` | default **on** | *(ZephCore only)* Master switch for every LED on the node, applied live and persisted: heartbeat, unread-message and LoRa TX-activity LEDs, plus the message and shutdown flashes. Works on every role, including headless repeaters where the TX LED is the only one that ever lights. Does **not** cover the display backlight, which is a separate UI brightness setting. |
+| `set leds.radio ` | default **tx** | *(ZephCore only)* What the LoRa activity LED reacts to, applied live and persisted. `tx` lights it for the duration of each transmit (the behaviour before this setting existed), `rx` gives a 30 ms blink per valid packet received, `all` does both, `off` keeps it dark. Sits **below** `set leds` — the master switch off keeps it dark whatever this says. Only boards defining the `lora-tx-led` alias have this LED; elsewhere the value is stored but does nothing, and the reply says so. |
+| `set leds.hb ` | default **all** | *(ZephCore only)* What the heartbeat LED reacts to, applied live and persisted. `all` is the 4 s liveness tick that widens from 20 ms to 200 ms while messages are unread (the behaviour before this setting existed), `hb` never widens, `unread` stays dark until there are unread messages, `off` keeps it dark. Also sits below `set leds`. See the LED-topology notes below for what this does on single-LED boards. |
| `set agc.reset.interval ` | Accepted, ignored | Removed — replies `Removed - Automatic AGC reset is on`. The prefs byte is still read and written so the on-flash layout stays byte-exact, but nothing acts on it. |
| `set multi.acks <0\|1>` | | Enable extra ACK transmits |
| `set path.hash.mode ` | 0, 1, or 2 | Path hashing algorithm |
| `set loop.detect ` | `off`, `minimal`, `moderate`, `strict` | Loop detection sensitivity |
| `set radio.rxgain <0\|1\|on\|off>` | | RX gain boost, applied live. Replies `Error: unsupported` on radios without RX boost (SX127x); the pref is still saved. |
| `set radio.fem.rxgain <0\|1\|on\|off>` | default **1** | Routes receive through the external FEM's LNA (`1`) or around it via the FEM's bypass path (`0`), applied live. Sensitivity for battery life — `0` costs roughly 17 dB and saves the LNA's supply current. Transmit, and the driver's idle/sleep gating of the FEM, are unaffected either way. Supported only where the FEM's receive path is software-selectable and that select line is wired to the radio node as `lna-bypass-gpios` — today the KCT8103L boards, `heltec_t096`, `heltec_wireless_tracker_v2`, `heltec_wifi_lora32_v43` and `heltec_wifi_lora32_v4_r8`. Every other board reports `Error: unsupported`: `heltec_wifi_lora32_v4`'s GC1109 has no receive-path select (its CPS is don't-care in RX, same as MeshCore); `station_g2`, `gat562_30s`, `ikoka_nano_30dbm` and `promicro_sx1262` have only the DIO2/TXEN/RXEN transmit-receive switch; `rak3401_1watt`'s SKY66122 is enabled by a standalone always-on regulator outside the radio node; and non-SX126x radios (LR1110, LR2021, SX127x) never implement it. The pref is still saved when unsupported. **Do not expect the FEM's chip-enable to be the knob** — deasserting `antenna-enable-gpios` in RX shuts the part down and takes the through path with it (~69 dB measured on a V4.3), which is what 1.17.2 did before this moved to `lna-bypass-gpios`. |
-| `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 display.rotate <0\|1\|on\|off>` | default **0** | Rotate the display 180 degrees, for cases and upgrade kits that mount the screen upside down (e.g. the Meshnology N37E for the Wio Tracker L1). Applied live — the driver flips the panel's `SEGMENT_MAP` and `COM_OUTPUT_SCAN`, two bytes on the wire, and the next frame comes out rotated with no redraw and no per-frame cost. **Only full-height SSD1306 and SH1106 panels support this** (`rak4631`, `gat562_30s`, `heltec_wifi_lora32_v4`/`v43`/`v4_r8`, `lilygo_t3s3`, `station_g2`, `wio_tracker_l1`); every other panel replies `Error: this panel cannot rotate` and the pref is **not** saved, so a stored value can never disagree with what the screen shows. `lilygo_timpulse_plus` is excluded despite being an SSD1306: its 64x32 glass is windowed into a 128x64 controller at `page-offset 4`, and the COM-scan reversal flips the controller's whole range, which would move the image off the bonded region. E-paper (SSD16xx) is excluded on purpose: its driver accepts a 180-degree orientation but implements it by flipping the RAM entry mode only, which reverses byte order without reversing bit order inside each byte — it would report success and render wrong. |
-| `set input.rotate <0\|1\|on\|off>` | default **0** | Swap the joystick/D-pad axes — up/down and left/right — to match an upside-down mount. Applied live. Deliberately **separate** from `display.rotate`: a case can flip the screen without moving the stick, and boards whose panel cannot rotate can still need the axis swap. Works on every board with directional input, in both the joystick UI and the button UI (where it swaps page-prev/page-next). Non-directional keys, tap codes and long-press gestures are unaffected. |
+| `set rxduty <0\|1\|on\|off>` | | *(ZephCore only)* 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 display.rotate <0\|1\|on\|off>` | default **0** | *(ZephCore only)* Rotate the display 180 degrees, for cases and upgrade kits that mount the screen upside down (e.g. the Meshnology N37E for the Wio Tracker L1). Applied live — the driver flips the panel's `SEGMENT_MAP` and `COM_OUTPUT_SCAN`, two bytes on the wire, and the next frame comes out rotated with no redraw and no per-frame cost. **Only full-height SSD1306 and SH1106 panels support this** (`rak4631`, `gat562_30s`, `heltec_wifi_lora32_v4`/`v43`/`v4_r8`, `lilygo_t3s3`, `station_g2`, `wio_tracker_l1`); every other panel replies `Error: this panel cannot rotate` and the pref is **not** saved, so a stored value can never disagree with what the screen shows. `lilygo_timpulse_plus` is excluded despite being an SSD1306: its 64x32 glass is windowed into a 128x64 controller at `page-offset 4`, and the COM-scan reversal flips the controller's whole range, which would move the image off the bonded region. E-paper (SSD16xx) is excluded on purpose: its driver accepts a 180-degree orientation but implements it by flipping the RAM entry mode only, which reverses byte order without reversing bit order inside each byte — it would report success and render wrong. |
+| `set input.rotate <0\|1\|on\|off>` | default **0** | *(ZephCore only)* Swap the joystick/D-pad axes — up/down and left/right — to match an upside-down mount. Applied live. Deliberately **separate** from `display.rotate`: a case can flip the screen without moving the stick, and boards whose panel cannot rotate can still need the axis swap. Works on every board with directional input, in both the joystick UI and the button UI (where it swaps page-prev/page-next). Non-directional keys, tap codes and long-press gestures are unaffected. |
| `set tz.offset <-12..14>` | default **0** (UTC) | Whole-hour offset from UTC applied when formatting the **on-device clock display** — the top bar, the status page and the joystick System -> Time screen, which show e.g. `UTC+2` instead of `UTC`. Accepts `default`. **Display only, by design.** The RTC itself, `clock`, `clock sync` and `time ` all stay UTC: they round-trip with each other, apps parse them, and an offset that reached the clock would read as a jump to every timestamp consumer on the node (advert timestamps, the ACL's monotonic `sender_timestamp` gate, MeshTimeSync) — a backward clock silently mutes a node on the mesh. Whole hours only, matching upstream MeshCore's command of the same name, so half-hour zones (+5:30, -3:30) cannot be expressed. Accepted on headless nodes, where it is simply inert. |
| `set adc.multiplier ` | `0` (use board default) or 100–30000 | Battery voltage ADC calibration multiplier, set directly. Rejects non-numeric input, NaN/inf and negatives. |
-| `set adc.multiplier target ` | 3000–4400 mV | Calibrate against a voltage you measured with a multimeter: rescales the current multiplier so the ADC reads ``. Replies with the old and new multiplier plus the before/after reading. `Error: no ADC reading on this board` if the board has no battery ADC. |
-| `set adc.multiplier full` | board must be fully charged | Same calibration, but against the board's battery-curve 100% point instead of a hand-measured value. Only meaningful on a full charge. |
-| `set meshtimesync ` | 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 ` | 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 ~1–2 h. Turn off to observe/hand-tune via `get cad.stats` + `set cad.offset`. See `ADAPTIVE_CAD.md`. |
-| `set cad.offset ` | −8 to 12, default 0 | Operating detPeak offset from the chip family's base for the current SF, bandwidth and CAD symbol count (Semtech LoRa Basics Modem reference tables; SX126x ~18–34, LR11xx ~50–85, LR20xx its own symbol-indexed 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 12–48, LR11xx 40–100, LR20xx 48–90) is a firmware guardrail against a CAD that never/always fires, not a chip limit (`cadDetPeak` is a full `uint8_t`); the driver reports it so the controller narrows this range to match rather than exploring offsets that collapse onto one peak. Applied live; the auto staircase may move it later if `cad.auto` is on. |
-| `set probe.interval ` | 0 (off) or 10–255, default **15** | Seconds between periodic radio measurements. ONE reading serves both: the noise-floor RSSI sample (median of 8) and the CAD calibration probe, which consumes that same reading rather than measuring separately — so this is also the noise-floor sampling rate, and it sets how often an idle repeater wakes. Default 15 s → ~1–2 h CAD staircase response; the floor EMA warms up over 8 samples (~2 min) and its unguarded bypass runs every 16th (~4 min). Longer = fewer wakes, slower to track a changing RF environment. 0 disables CAD probing entirely (also freezes auto adaptation); the floor sampler then falls back to its build-time default. |
-| `set cad.busycap ` | 0 (off) or 10–90, default **15** | Faint-tolerance / airtime cap: the max percentage of **quiet-moment probes** that may trip on a faint signal before the staircase backs off to a less sensitive detPeak. Not a percentage of TX attempts — probes are prefiltered to quiet moments, so strong traffic never enters the statistic. On a congested hilltop most busy verdicts are distant traffic won on capture anyway, so deferring for all of it starves the node's own airtime. Self-targeting: a quiet node's busy rate never reaches the cap. Shown as `bc:` in `get cad.stats`. 0 disables the cap (pure knee-seeking). |
-| `set cad.reset` | | Full CAD reset: clears the accumulated per-level probe statistics **and** returns the operating detPeak offset to the family base (`cad.offset` = 0), applying it to the radio live and persisting it. Use after a change to the base tables, or to undo a staircase that has walked somewhere unhelpful. Before 1.17.4 this cleared only the statistics, leaving the node to re-converge *from* the walked offset with no evidence for why it was there. `set radio`, `set freq` and the companion app's set-radio-params now run this automatically when they move freq, bandwidth or SF, so it is only needed by hand after a firmware base-table change. |
+| `set adc.multiplier target ` | 3000–4400 mV | *(ZephCore only)* Calibrate against a voltage you measured with a multimeter: rescales the current multiplier so the ADC reads ``. Replies with the old and new multiplier plus the before/after reading. `Error: no ADC reading on this board` if the board has no battery ADC. |
+| `set adc.multiplier full` | board must be fully charged | *(ZephCore only)* Same calibration, but against the board's battery-curve 100% point instead of a hand-measured value. Only meaningful on a full charge. |
+| `set meshtimesync ` | default **off** | *(ZephCore only)* 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 ` | default **on** | *(ZephCore only)* 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 ~1–2 h. Turn off to observe/hand-tune via `get cad.stats` + `set cad.offset`. See `ADAPTIVE_CAD.md`. |
+| `set cad.offset ` | −8 to 12, default 0 | *(ZephCore only)* Operating detPeak offset from the chip family's base for the current SF, bandwidth and CAD symbol count (Semtech LoRa Basics Modem reference tables; SX126x ~18–34, LR11xx ~50–85, LR20xx its own symbol-indexed 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 12–48, LR11xx 40–100, LR20xx 48–90) is a firmware guardrail against a CAD that never/always fires, not a chip limit (`cadDetPeak` is a full `uint8_t`); the driver reports it so the controller narrows this range to match rather than exploring offsets that collapse onto one peak. Applied live; the auto staircase may move it later if `cad.auto` is on. |
+| `set probe.interval ` | 0 (off) or 10–255, default **15** | *(ZephCore only)* Seconds between periodic radio measurements. ONE reading serves both: the noise-floor RSSI sample (median of 8) and the CAD calibration probe, which consumes that same reading rather than measuring separately — so this is also the noise-floor sampling rate, and it sets how often an idle repeater wakes. Default 15 s → ~1–2 h CAD staircase response; the floor EMA warms up over 8 samples (~2 min) and its unguarded bypass runs every 16th (~4 min). Longer = fewer wakes, slower to track a changing RF environment. 0 disables CAD probing entirely (also freezes auto adaptation); the floor sampler then falls back to its build-time default. |
+| `set cad.busycap ` | 0 (off) or 10–90, default **15** | *(ZephCore only)* Faint-tolerance / airtime cap: the max percentage of **quiet-moment probes** that may trip on a faint signal before the staircase backs off to a less sensitive detPeak. Not a percentage of TX attempts — probes are prefiltered to quiet moments, so strong traffic never enters the statistic. On a congested hilltop most busy verdicts are distant traffic won on capture anyway, so deferring for all of it starves the node's own airtime. Self-targeting: a quiet node's busy rate never reaches the cap. Shown as `bc:` in `get cad.stats`. 0 disables the cap (pure knee-seeking). |
+| `set cad.reset` | | *(ZephCore only)* Full CAD reset: clears the accumulated per-level probe statistics **and** returns the operating detPeak offset to the family base (`cad.offset` = 0), applying it to the radio live and persisting it. Use after a change to the base tables, or to undo a staircase that has walked somewhere unhelpful. Before 1.17.4 this cleared only the statistics, leaving the node to re-converge *from* the walked offset with no evidence for why it was there. `set radio`, `set freq` and the companion app's set-radio-params now run this automatically when they move freq, bandwidth or SF, so it is only needed by hand after a firmware base-table change. |
| `set extra.sf [sf] [sf]` | up to 3 SFs, `0`/`off` clears | **LR2021 only** (`Error: unsupported` elsewhere) — LoRa *side detectors*: demodulate up to three extra spreading factors concurrently with `sf`, on the same bandwidth, so one repeater can serve several SF communities. Which SF a packet arrived on is a chip-side readout, not a guess. Chip constraints, enforced in the driver and reported as `Error: unsupported or invalid extra SF config`: every extra SF must be **greater** than `sf`, all distinct, highest−lowest ≤ 4, and at BW ≥ 500 kHz at most 2 (only 1 when `sf` ≥ 10). **Receive only, and the bridge it creates is one-way.** TX always uses the single configured `sf`, and all detectors share one bandwidth, so this is multi-SF, not multi-channel. A node with `sf 7` + `extra.sf 8` hears SF8 traffic and *does* forward it — but the forward goes out at SF7, so traffic moves SF8 -> SF7 only and nothing comes back. An SF8 node's direct messages are delivered while its ACKs never arrive, so it retries to its limit every time; adverts and one-way flood traffic propagate fine. Because every extra SF must be **greater** than `sf`, the main SF is always the lowest in the set and TX always uses it — so the bridge direction is fixed at high-SF-in / low-SF-out and **cannot be reversed**. Two nodes back to back both point the same way; there is no configuration that carries SF7 -> SF8. Treat it as a collector for slower-SF stragglers, not as a link between two SF islands. Applied live and restored on every RX entry. **Interaction with CAD:** the chip's SF constraint for CAD is the inverse of the one for RX, so the driver switches side detectors off for each LBT CAD and back on when RX re-arms — two extra SPI commands per TX, no configuration required. Persisted; a set that no longer fits after an `sf`/`bw` change is refused at boot and logged. |
| `set prv.key ` | **128-char hex** (64-byte expanded Ed25519 key) | Replace private key; derive new identity *(reboot to apply)*. The length must be exact — `fromHex` rejects anything else with `Error, bad key`. `get prv.key` returns the same 128-char form. Not USB-gated. |
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 78da1b6..9484b07 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -4,7 +4,7 @@
> companion, server roles, event loops, connectivity, storage, peripherals, UI and the repository).
> This is the system view: what ZephCore is made of and the rules that keep it coherent. Component detail is in
> [ARCHITECTURE.md](ARCHITECTURE.md); decisions and their history are in [adr/](adr/README.md); the CLI is in
-> [Repeater_CLI_commands.md](Repeater_CLI_commands.md). Where a **rule** is stated, known exceptions are listed
+> [CLI_commands.md](CLI_commands.md). Where a **rule** is stated, known exceptions are listed
> in §5.3.
---
diff --git a/docs/LINUX_NATIVE.md b/docs/LINUX_NATIVE.md
index f2282fb..d886d6c 100644
--- a/docs/LINUX_NATIVE.md
+++ b/docs/LINUX_NATIVE.md
@@ -264,7 +264,7 @@ Only one client connects at a time.
### Repeater CLI
The repeater role exposes the same configuration CLI as an MCU repeater's USB-CDC
-serial console — see [Repeater_CLI_commands.md](Repeater_CLI_commands.md) for the
+serial console — see [CLI_commands.md](CLI_commands.md) for the
full command list (`get`/`set`, `password`, `reboot`, `clock`, …). On native Linux
that console maps onto Zephyr's native-PTY UART (`CONFIG_SERIAL` +
`CONFIG_UART_NATIVE_PTY`). At boot the binary prints the pseudo-terminal it created: