mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-23 00:11:20 +00:00
logo thiccification and docs chores
This commit is contained in:
1 parent
7e73743579
commit
8c0464ea30
15 files changed
+33
-79
No files matched your search
@@ -1,333 +0,0 @@
|
||||
# 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
|
||||
100–200 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.
|
||||
|
||||
## 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: 21–29 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 (0–255), but the *useful* range is only ~18–32. 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 56–68 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 0–255.
|
||||
|
||||
**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.stats` 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 `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 56–68 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. (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
|
||||
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:
|
||||
|
||||
The controller is **knee-seeking**. The false-positive-vs-detPeak curve
|
||||
falls as detPeak rises (less sensitive → fewer false detects) and flattens
|
||||
past a knee; the sweet spot is that knee — the most sensitive detPeak whose
|
||||
FP has already bottomed out. Probes sample the operating level and **both
|
||||
neighbours** (op−1 more sensitive, op+1 less sensitive; op weighted half, each
|
||||
neighbour a quarter) so the staircase can read the local curve *slope*:
|
||||
|
||||
- **Airtime protection** (highest priority): step up when the operating
|
||||
level's *total* busy rate — real traffic plus false positives — exceeds
|
||||
the busy cap (`set cad.busycap`, percent; default **25%**, 0 = off). The
|
||||
knee logic only minimises *false* busy, but on a congested hilltop most
|
||||
busy verdicts are real distant traffic we'd win on capture anyway;
|
||||
deferring for all of it starves the node's own airtime. Self-targeting —
|
||||
a quiet node's busy rate never reaches the cap, so only a genuinely busy
|
||||
backbone backs off. Shown as `bc:` in `get cad.stats`.
|
||||
- **Step up** (less sensitive) when the level above is markedly cleaner —
|
||||
FP drops ≥5%/level (`CAD_KNEE_SLOPE_PERMILLE`). That means we're on the
|
||||
steep part below the knee; climb toward it.
|
||||
- **Step down** (more sensitive) only on a flat plateau (frontier no worse
|
||||
than operating, slope <5%/level) that is **already clean** (FP ≤5%,
|
||||
`CAD_PLATEAU_CLEAN_PERMILLE`) — reclaim sensitivity that costs nothing.
|
||||
- Otherwise **hold** — either at the knee (steep below, flat above) or on a
|
||||
noisy flat plateau.
|
||||
|
||||
Using slopes rather than an absolute FP target makes convergence independent
|
||||
of a site's FP *floor* (which varies with traffic and with the classifier's
|
||||
residual false-positive rate). The clean-plateau guard is what stops a
|
||||
flat-but-noisy curve from walking to the sensitive rail: there, holding is the
|
||||
least-bad move, while a genuinely quiet flat-low site correctly descends to
|
||||
the floor. Each involved rung needs ≥120 samples (`CAD_STEP_MIN_PROBES`)
|
||||
before a step, so a decision lands roughly every 1–2 hours at the default
|
||||
interval.
|
||||
|
||||
A step decision requires the operating rung (and, for the direction chosen,
|
||||
its neighbour) to be warm; `get cad.stats`'s three-rung window shows exactly those
|
||||
levels, so the slope the controller is acting on is visible directly. The
|
||||
offset is persisted to flash whenever it steps.
|
||||
|
||||
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 15–40, LR11xx/LR20xx 48–90). That absolute clamp
|
||||
is a *firmware guardrail*, not a chip limit — `cadDetPeak` is a full
|
||||
8-bit register (0–255) — 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) sp:0.9/84%(312) 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 · `sp` RSSI burst quality · `bc` busy cap.
|
||||
|
||||
**`sp` — are the noise-floor sampler's reads independent?** The floor
|
||||
sampler takes a median of 8 RSSI reads. The chip refreshes RSSI only once
|
||||
per averaging window (~16 µs at BW 62.5, ~134 µs at BW 7.8); reads issued
|
||||
faster than that return the same underlying value repeatedly and the median
|
||||
collapses to a single read. `sp` reports mean spread (max−min) across a
|
||||
burst, the share of bursts whose spread was 0, and the burst count.
|
||||
|
||||
Read it this way:
|
||||
|
||||
- **A non-zero mean proves the reads are independent**, however high the
|
||||
zero-spread share climbs. A quiet or steadily-occupied channel genuinely
|
||||
reads the same value 8 times at integer-dB resolution — that is correct,
|
||||
not broken.
|
||||
- **Mean 0.0 with a high share is the fault signature**: no burst ever
|
||||
spans anything, i.e. 8 copies of one sample.
|
||||
- The count matters. The burst rate is not derivable from uptime, because
|
||||
the sampler's guards (TX, mid-RX, duty-cycle sleep) block an unknown
|
||||
fraction of attempts, so a share without its denominator is unreadable.
|
||||
It appears on the local USB console only — a remote reply is capped at
|
||||
161 B and the three level rows have first claim on it. All three
|
||||
counters halve together at 8192 bursts, so the count stays four digits
|
||||
and the figures describe a recent window rather than everything since
|
||||
boot.
|
||||
|
||||
Measured on-air at BW 62.5: `0.6/90%` on a quiet channel, `0.9/84%` with
|
||||
the floor at −103 dBm. The share falls and the spread rises as ambient
|
||||
comes up — independent reads, responding the right way.
|
||||
|
||||
Note what this median does and does not do. The burst spans ~300 µs against
|
||||
a ~200 ms SF8/BW62.5 packet — about 0.15% of one transmission — so a
|
||||
neighbour's packet is either wholly inside the burst or wholly outside it,
|
||||
every read sees the same level, and the median returns it rather than
|
||||
rejecting it. The median rejects sub-300 µs glitches and bad SPI reads.
|
||||
Interference is handled by the mid-receive guard and the +14 dB threshold
|
||||
filter, not here.
|
||||
|
||||
The probe interval used to sit in this slot; it moved out because it is a
|
||||
pref you set, readable with `get probe.interval`, whereas burst spread is
|
||||
only observable from inside the sampler.
|
||||
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. Remember (mental model above) these numbers describe the
|
||||
*faint* regime only — strong signals were prefiltered out before probing.
|
||||
|
||||
### 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.stats` 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 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.stats` | | 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 probe.interval <sec>` | 15 | Shared cadence for the noise-floor sample and the CAD probe that consumes it; 0 disables probing (and freezes auto), 10–255 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. 10–90 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.stats` 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.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,36 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025-2026 ZephCore contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
This repository vendors third-party code and assets under separate,
|
||||
compatible licenses:
|
||||
|
||||
- `lib/monocypher/` — Monocypher, by Loup Vaillant. Dual-licensed
|
||||
BSD-2-Clause OR CC0-1.0 (see file headers).
|
||||
- `patches/zephyr/*.patch` — diffs against Zephyr RTOS (Apache-2.0). The
|
||||
patches are applied to, and remain part of, the separately-licensed Zephyr
|
||||
source tree fetched via `west update`; they are not distributed as
|
||||
standalone files under this project's MIT license.
|
||||
- `img/kite-network-logo-*.svg` (at the repository root, one level above this
|
||||
file) — ZephCore logo, by recrof (https://github.com/recrof). Licensed under
|
||||
the WTFPL.
|
||||
@@ -1,634 +0,0 @@
|
||||
# ZephCore Native Linux Port
|
||||
|
||||
Run ZephCore as a native Linux process on SBCs like the **Femtofox** (Luckfox Pico Mini + E22-900M30S) or a **Raspberry Pi + RAK6421 HAT**. The full mesh stack runs on top of Zephyr's `native_sim` board, talking to real SPI/GPIO via `/dev/spidev*` and the kernel's **GPIO V2 character-device uAPI** (direct ioctls — no libgpiod dependency).
|
||||
|
||||
The companion app connects via a TCP socket on port **5000**, using MeshCore's `SerialWifiInterface` framing (`['<'][len_LSB][len_MSB][payload]` app→node, `['>']…` node→app) — see [Companion app connection](#companion-app-connection).
|
||||
|
||||
> **Real-time clock is automatic.** The binary forces the `native_sim` simulated clock into real-time mode at boot (equivalent to always passing `--rt`), because it drives a *real* radio whose BUSY/DIO1 timing happens in wall-clock time. You do **not** need to pass `--rt`. See [adapters/transport/linux_native_setup.c](adapters/transport/linux_native_setup.c).
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
On the **build host** (where you run `west build`):
|
||||
|
||||
```bash
|
||||
# Zephyr SDK and west workspace as per the main CLAUDE.md.
|
||||
|
||||
# Cross-compile toolchains (for SBC targets) — install BOTH gcc and g++:
|
||||
# the mesh layer is C++, so the g++ cross-compiler is required (a build with
|
||||
# only gcc fails at CMake configure with "Tell CMake where to find the compiler").
|
||||
sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf # Femtofox (RV1103 ARMv7-A)
|
||||
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu # Raspberry Pi (aarch64)
|
||||
```
|
||||
|
||||
On the **target SBC** (where the binary runs): grant your user access to
|
||||
`/dev/spidev*` and `/dev/gpiochip*` so you don't need `sudo`/root — see
|
||||
[Running without root](#running-without-root) below. (You *can* just run as
|
||||
root with `sudo`, but it's not required.)
|
||||
|
||||
**Femtofox only:** the Femtofox image ships with `meshtasticd` pre-installed and it holds
|
||||
the SPI bus / GPIO lines. Uninstall it before running ZephCore or the radio will be
|
||||
unavailable. Use `foxbuntu-config` → uninstall meshtasticd, or:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop meshtasticd
|
||||
sudo apt remove meshtasticd
|
||||
```
|
||||
|
||||
No userspace library dependencies — the GPIO driver talks to `/dev/gpiochipN`
|
||||
directly via the kernel's GPIO V2 chardev uAPI (ioctl). Requires Linux ≥ 5.10
|
||||
(released Dec 2020 — every modern SBC distro has it). The SPI driver uses
|
||||
spidev the same way (no library either).
|
||||
|
||||
### Enabling spidev on the SBC
|
||||
|
||||
**Femtofox / Luckfox Pico Mini:** the official Femtofox image ships with SPI and GPIO
|
||||
already enabled — `/dev/spidev0.0` and `/dev/gpiochip0`–`3` are present out of the box.
|
||||
No extra configuration needed; skip straight to adding your user to the `spi`/`gpio` groups.
|
||||
|
||||
**Raspberry Pi:** add `dtparam=spi=on` to `/boot/firmware/config.txt` (RPi 5) or `/boot/config.txt` (RPi 4) and reboot. Verify `/dev/spidev0.0` exists.
|
||||
|
||||
---
|
||||
|
||||
## Running without root
|
||||
|
||||
By default `/dev/spidev*` and `/dev/gpiochip*` are owned `root:root`, so the
|
||||
binary only runs under `sudo`. The binary needs **no** elevated privileges of
|
||||
its own — it only needs read/write on those two device classes (the TCP port is
|
||||
5000, non-privileged; the flash file is written in your home dir). Grant access
|
||||
once with groups + a udev rule:
|
||||
|
||||
```bash
|
||||
# 1. Create the groups (the Femtofox/foxbuntu image ships without them).
|
||||
sudo groupadd -f spi
|
||||
sudo groupadd -f gpio
|
||||
|
||||
# 2. Add your login user (e.g. "femto") to both.
|
||||
sudo usermod -aG spi,gpio "$USER"
|
||||
|
||||
# 3. udev rule: give those groups access to the SPI + GPIO char devices.
|
||||
sudo tee /etc/udev/rules.d/90-zephcore.rules >/dev/null <<'EOF'
|
||||
KERNEL=="spidev*", GROUP="spi", MODE="0660"
|
||||
SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpio", MODE="0660"
|
||||
EOF
|
||||
|
||||
# 4. Reload udev and re-trigger so the rule applies to the existing nodes.
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --subsystem-match=spidev --subsystem-match=gpio
|
||||
|
||||
# 5. Log out and back in (group membership is only picked up at login).
|
||||
# Verify: id # should list "spi" and "gpio"
|
||||
# ls -l /dev/spidev0.0 /dev/gpiochip1 # group spi / gpio, mode 0660
|
||||
```
|
||||
|
||||
Then run the binary **without** `sudo`:
|
||||
|
||||
```bash
|
||||
./zephcore_native_linux.exe
|
||||
```
|
||||
|
||||
> **If the udev rule doesn't stick** (some minimal images use `mdev`/busybox
|
||||
> instead of systemd-udev, which ignores `/etc/udev/rules.d`), set the
|
||||
> permissions at boot instead — e.g. a tiny systemd unit or an `rc.local` line:
|
||||
> ```bash
|
||||
> chgrp gpio /dev/gpiochip* && chmod 660 /dev/gpiochip*
|
||||
> chgrp spi /dev/spidev* && chmod 660 /dev/spidev*
|
||||
> ```
|
||||
|
||||
> **Femtofox note:** you must still stop/uninstall `meshtasticd` first (it holds
|
||||
> the SPI bus) — see the Prerequisites section. Running as your user vs root
|
||||
> doesn't change that.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Host smoke build (x86-64 Linux, no real radio)
|
||||
|
||||
A device preset is required (it provides the SX1262 node); the Femtofox preset
|
||||
is fine for a host smoke build — the SPI/GPIO opens just fail gracefully with no
|
||||
hardware:
|
||||
|
||||
```bash
|
||||
rm -rf build && west build -b native_sim/native/64 zephcore -- \
|
||||
-DEXTRA_CONF_FILE="boards/linux_native/femtofox.conf"
|
||||
./build/zephyr/zephcore_native_linux.exe
|
||||
```
|
||||
|
||||
The binary prints something like:
|
||||
|
||||
```
|
||||
UART_0 connected to pseudotty: /dev/pts/3
|
||||
TCP companion transport listening on :5000
|
||||
```
|
||||
|
||||
`Ctrl-C` to stop.
|
||||
|
||||
### 2. Femtofox (Luckfox Pico Mini, ARMv7-A)
|
||||
|
||||
Note: use `native_sim` (32-bit), **not** `native_sim/native/64` — the Luckfox Pico Mini
|
||||
is ARMv7-A (32-bit) and the 64-bit variant will error at CMake configure time.
|
||||
|
||||
```bash
|
||||
west build -b native_sim zephcore -- \
|
||||
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
|
||||
-DNATIVE_TARGET_HOST=arm \
|
||||
-DCROSS_COMPILE=/usr/bin/arm-linux-gnueabihf- \
|
||||
-DEXTRA_CONF_FILE="boards/linux_native/femtofox.conf"
|
||||
|
||||
scp build/zephyr/zephcore_native_linux.exe femto@femtofox.local:~/
|
||||
ssh femto@femtofox.local ./zephcore_native_linux.exe # no sudo once groups are set up
|
||||
```
|
||||
|
||||
(No `--rt` flag needed — real-time mode is forced at boot. See the note at the top.)
|
||||
|
||||
### 3. Raspberry Pi + RAK6421 HAT (aarch64)
|
||||
|
||||
The `rak6421` preset is named after the **HAT**, not a Pi model — the wiring is
|
||||
identical on every Pi. The only per-model difference is the Linux gpiochip:
|
||||
|
||||
- **Pi 2 / 3 / 4 / Zero 2** → `gpiochip0` → `rak6421.conf`
|
||||
- **Pi 5** → `gpiochip4` (RP1) → `rak6421_pi5.conf`
|
||||
|
||||
The build command is unchanged by the native-Linux work this cycle — the
|
||||
GPIO-driver fixes, automatic `--rt`, and per-node persistence apply to every
|
||||
native build. For a 64-bit Pi OS (the usual case on Pi 3/4/5):
|
||||
|
||||
```bash
|
||||
rm -rf build && west build -b native_sim/native/64 zephcore -- \
|
||||
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
|
||||
-DNATIVE_TARGET_HOST=aarch64 \
|
||||
-DCROSS_COMPILE=/usr/bin/aarch64-linux-gnu- \
|
||||
-DEXTRA_CONF_FILE="boards/linux_native/rak6421.conf" # Pi 5: rak6421_pi5.conf
|
||||
scp build/zephyr/zephcore_native_linux.exe pi@raspberrypi.local:~/
|
||||
```
|
||||
|
||||
(On a **32-bit** Pi OS — e.g. Pi 2, or Pi 3/4 on 32-bit Raspberry Pi OS — build the
|
||||
armhf way instead: `-b native_sim -DNATIVE_TARGET_HOST=arm -DCROSS_COMPILE=/usr/bin/arm-linux-gnueabihf-`,
|
||||
same `-DEXTRA_CONF_FILE`.)
|
||||
|
||||
`rak6421_pi5` is just `rak6421` with `gpiochip4` — equivalently you can keep using
|
||||
`rak6421.conf` and override at runtime with `--lora-gpio-chip=/dev/gpiochip4`.
|
||||
|
||||
> **`--pristine` in this WSL workspace:** prefer `rm -rf build &&` over `west build --pristine`.
|
||||
> `--pristine` can feed Windows-style `D:/…` paths to CMake when `.west/config` was
|
||||
> initialised on Windows. `rm -rf build &&` is equivalent and works everywhere. (On a
|
||||
> native-Linux build host, plain `--pristine` is fine.)
|
||||
>
|
||||
> **Status:** the RAK6421 preset (`rak6421.conf`, Pi Zero 2 W) is **hardware-verified
|
||||
> end-to-end (2026-06-03)** — SX1262 init, SPI/CS, TCXO, radio-start, and over-the-air
|
||||
> RX/TX all confirmed on real hardware, alongside the Femtofox (2026-06-02). The Pi 5
|
||||
> preset (`rak6421_pi5.conf`) shares the same wiring (only the gpiochip differs) and is
|
||||
> build-verified but not separately hardware-tested. If the radio is dead, follow the
|
||||
> "Radio does nothing" troubleshooting below.
|
||||
>
|
||||
> The RAK13300/RAK13302 **does have a DIO3-powered TCXO and uses DIO2 as the RF
|
||||
> switch** — both overlays set `dio3-tcxo-voltage`/`dio2-tx-enable` accordingly, and
|
||||
> hold the module's two `Enable_Pins` (BCM 12 + 13) high via GPIO hogs
|
||||
> (`CONFIG_GPIO_HOGS=y`). Pins and clock follow the RAK6421 IO Slot 1 reference
|
||||
> wiring; see the wiring table below.
|
||||
|
||||
### 4. Repeater role (no companion)
|
||||
|
||||
Add `boards/common/repeater.conf` to the `EXTRA_CONF_FILE` list. The repeater CLI is accessible via the native PTY printed at boot (`/dev/pts/N`):
|
||||
|
||||
```bash
|
||||
screen /dev/pts/3 # local
|
||||
# or remotely:
|
||||
socat /dev/pts/3 TCP-LISTEN:6000,reuseaddr,fork &
|
||||
# then on a remote machine:
|
||||
nc <sbc-ip> 6000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hardware Wiring
|
||||
|
||||
### Femtofox / Luckfox Pico Mini + E22-900M30S (TCXO)
|
||||
|
||||
Pin offsets are computed as `(Rockchip GPIO# − 32)` because the kernel splits banks into separate `gpiochip` devices.
|
||||
|
||||
| Signal | gpiochip | Offset | Rockchip GPIO |
|
||||
|---|---|---|---|
|
||||
| SPI bus | `/dev/spidev0.0` @ 2 MHz | — | — |
|
||||
| CS | gpiochip1 | 16 | GPIO48 (1C0) |
|
||||
| DIO1/IRQ | gpiochip1 | 23 | GPIO55 (1C7) |
|
||||
| BUSY | gpiochip1 | 22 | GPIO54 (1C6) |
|
||||
| RESET | gpiochip1 | 25 | GPIO57 (1D1) |
|
||||
| RXEN | gpiochip1 | 24 | GPIO56 (1D0) |
|
||||
|
||||
SX1262 extras: DIO2 drives the RF switch (`dio2-tx-enable`), DIO3 powers the TCXO at 1.8V.
|
||||
|
||||
Source: `github.com/femtofox/femtofox` → `foxbuntu/.../femtofox_SX1262_TCXO.yaml`.
|
||||
|
||||
### Raspberry Pi + RAK6421 HAT + RAK13300/RAK13302 (IO Slot 1)
|
||||
|
||||
BCM GPIO numbers (Pi 2/3/4/Zero 2: `gpiochip0` → `rak6421.conf`; Pi 5: `gpiochip4` → `rak6421_pi5.conf`):
|
||||
|
||||
| Signal | BCM | WisBlock IO slot 1 |
|
||||
|---|---|---|
|
||||
| SPI bus | `/dev/spidev0.0` (CE0 = GPIO 8) | — |
|
||||
| DIO1/IRQ | 22 | IO6 |
|
||||
| RESET | 16 | IO4 |
|
||||
| BUSY | 24 | IO5 |
|
||||
| RX/TX enable | 12, 13 (`Enable_Pins`; 13 = IO3 antenna switch) | IO3 |
|
||||
|
||||
SX1262 extras: **DIO3 powers a 1.8 V TCXO** (`dio3-tcxo-voltage = <SX126X_DIO3_TCXO_1V8>`)
|
||||
and **DIO2 drives the RF switch** (`dio2-tx-enable` / `DIO2_AS_RF_SWITCH: true`) —
|
||||
same as the Femtofox. The two `Enable_Pins` (12, 13) are held high at boot via
|
||||
GPIO hogs in the overlay (`CONFIG_GPIO_HOGS=y` in the `.conf`). The RAK13302 is the
|
||||
1W SKY66122-boosted variant and additionally wants a per-step `TX_GAIN_LORA` PA
|
||||
gain table; ZephCore's SX126x driver does not expose that, so this overlay drives
|
||||
the 13300 fully and the 13302 at default (non-boosted) PA.
|
||||
|
||||
---
|
||||
|
||||
## Running the Binary
|
||||
|
||||
The binary prints its PTY path and TCP listen port at startup. Logs go to stderr; pipe them where you want them.
|
||||
|
||||
### Companion app connection
|
||||
|
||||
Use the MeshCore companion app's **"Connect via WiFi"** (TCP/Network) mode:
|
||||
|
||||
- **Host:** the SBC's IP address
|
||||
- **Port:** `5000` (default; configurable via `CONFIG_ZEPHCORE_LINUX_TCP_PORT`)
|
||||
|
||||
Wire format: **MeshCore SerialWifiInterface framing** (matches ESP32 Arduino reference in `src/helpers/esp32/SerialWifiInterface.cpp`):
|
||||
- App → Node: `['<' (0x3C)][length_LSB][length_MSB][NUS payload...]`
|
||||
- Node → App: `['>' (0x3E)][length_LSB][length_MSB][NUS payload...]`
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
```
|
||||
uart connected to pseudotty: /dev/pts/3
|
||||
```
|
||||
|
||||
That `/dev/pts/N` path **is** the serial console — point any serial terminal at it.
|
||||
|
||||
> **The PTY number changes on every run.** Grab it from the boot output each time
|
||||
> (or from the journal when running as a service — see below). It is not stable
|
||||
> across restarts.
|
||||
|
||||
#### Attach locally (same machine as the binary)
|
||||
|
||||
```bash
|
||||
screen /dev/pts/3 # detach: Ctrl-a then d ; quit: Ctrl-a then k
|
||||
# or
|
||||
picocom /dev/pts/3
|
||||
# or
|
||||
minicom -D /dev/pts/3
|
||||
```
|
||||
|
||||
You get the repeater prompt directly. Logs go to the binary's stderr, **not** the
|
||||
PTY, so the CLI stays clean (no log spam interleaved with your typing).
|
||||
|
||||
#### Attach remotely (configure an SBC over the network)
|
||||
|
||||
Bridge the PTY to a TCP port with `socat`, then connect from your workstation:
|
||||
|
||||
```bash
|
||||
# on the SBC:
|
||||
socat /dev/pts/3 TCP-LISTEN:6000,reuseaddr,fork &
|
||||
# from anywhere on the network:
|
||||
nc <sbc-ip> 6000
|
||||
```
|
||||
|
||||
#### When running as a systemd service
|
||||
|
||||
The binary runs in the background, so there's no terminal to attach to and the
|
||||
PTY path is only in the journal:
|
||||
|
||||
```bash
|
||||
journalctl -u zephcore | grep pseudotty # find the current /dev/pts/N
|
||||
```
|
||||
|
||||
Because the path changes every boot and you can't `screen` a backgrounded
|
||||
process, the practical options are:
|
||||
|
||||
- **Expose the CLI on a fixed TCP port** by adding the `socat` bridge as an
|
||||
`ExecStartPost=` in the unit (see [Run at boot](#run-at-boot-systemd-service)),
|
||||
so the console is always reachable at `<sbc-ip>:6000` regardless of the PTY
|
||||
number. Then `nc <sbc-ip> 6000` to configure.
|
||||
- **Or run the repeater in the foreground** (`sudo systemctl stop zephcore` then
|
||||
launch the binary by hand) just while you need to change settings, and restart
|
||||
the service afterward.
|
||||
|
||||
After changing settings, persist + apply them the same way as on an MCU repeater
|
||||
(most `set` commands take effect immediately; some require `reboot`). Settings
|
||||
survive restarts via the per-node flash file — see [Persistent storage](#persistent-storage).
|
||||
|
||||
### Run at boot (systemd service)
|
||||
|
||||
To start ZephCore automatically at boot and restart it if it ever exits, install
|
||||
a systemd service. This assumes you've already done the [Running without root](#running-without-root)
|
||||
setup (the `spi`/`gpio` groups + udev rule), so the service can run as your
|
||||
normal user — **no root**.
|
||||
|
||||
Create `/etc/systemd/system/zephcore.service` (adjust `User=` and the paths to
|
||||
your setup):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=ZephCore native LoRa mesh
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=femto
|
||||
SupplementaryGroups=spi gpio
|
||||
# WorkingDirectory pins where the persistent flash file lands
|
||||
# (meshcore_settings_<hostname>.bin) and must be writable by User=.
|
||||
WorkingDirectory=/home/femto
|
||||
ExecStart=/home/femto/zephcore_native_linux.exe
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Enable + start it (and have it come back on every reboot):
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now zephcore # start now + at boot
|
||||
sudo systemctl status zephcore # check it's running
|
||||
journalctl -u zephcore -f # follow logs (replaces stderr)
|
||||
```
|
||||
|
||||
Manage it:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart zephcore
|
||||
sudo systemctl stop zephcore
|
||||
sudo systemctl disable zephcore # stop auto-start at boot
|
||||
```
|
||||
|
||||
Notes:
|
||||
- **No `--rt` flag** is needed — real-time mode is baked into the binary.
|
||||
- **Stop `meshtasticd` first** (Femtofox): if it's installed it grabs the SPI bus.
|
||||
Disable it so it doesn't race your service: `sudo systemctl disable --now meshtasticd`.
|
||||
- **Companion (TCP) role** works perfectly as a service — connect the app to
|
||||
`<sbc-ip>:5000` as usual.
|
||||
- **Repeater role:** the CLI is a pseudo-terminal whose path (`/dev/pts/N`) is
|
||||
printed to the journal at boot (`journalctl -u zephcore | grep pseudotty`). To
|
||||
reach it remotely, add a bridge as `ExecStartPost`, e.g.
|
||||
`ExecStartPost=/bin/sh -c 'socat ... TCP-LISTEN:6000,fork &'` — or just run the
|
||||
repeater in the foreground when you need the CLI.
|
||||
- If you relocate the binary (e.g. to `/usr/local/bin/zephcore`), keep
|
||||
`WorkingDirectory=` pointed at a user-writable dir so the settings file persists.
|
||||
|
||||
---
|
||||
|
||||
## Persistent storage
|
||||
|
||||
Identity, prefs, contacts, channels and BLE-equivalent settings all live in
|
||||
LittleFS on `/lfs`, which is backed by Zephyr's flash simulator. On native Linux
|
||||
that simulator is **file-backed** (not RAM), so everything survives restarts.
|
||||
|
||||
The backing file is named **`meshcore_settings_<hostname>.bin`** and created in
|
||||
the binary's working directory on first run (`<hostname>` comes from the Linux
|
||||
`gethostname()`, so multiple nodes on one host each get their own file). For the
|
||||
Femtofox that's `meshcore_settings_femtofox.bin`.
|
||||
|
||||
```bash
|
||||
ls -l meshcore_settings_*.bin # appears after the first run
|
||||
```
|
||||
|
||||
- To move a node's identity to another machine, copy this file.
|
||||
- To reset a node to a fresh identity, delete it (a new one is created next run).
|
||||
- An explicit `--flash=/path/to/file.bin` overrides the default name/location.
|
||||
- **Migrating from an older build** that used the generic `flash.bin`:
|
||||
`cp flash.bin meshcore_settings_$(hostname).bin` before first run to keep your
|
||||
existing identity.
|
||||
|
||||
> Implementation: `patches/zephyr/0008-flash-sim-per-node-file.patch` makes the
|
||||
> flash simulator default to the per-node filename instead of `flash.bin`.
|
||||
|
||||
---
|
||||
|
||||
## Runtime Pin Override
|
||||
|
||||
For one-off hardware setups or custom wiring, override DT defaults at runtime:
|
||||
|
||||
```bash
|
||||
./zephcore_native_linux.exe \
|
||||
--lora-spidev=/dev/spidev0.0 \
|
||||
--lora-gpio-chip=/dev/gpiochip1
|
||||
```
|
||||
|
||||
Currently only the **paths** are runtime-overridable; pin offsets come from the DTS overlay you build with. To change pin numbers without rebuilding, create your own `boards/linux_native/<device>.conf` + `.overlay` (copy `femtofox.overlay` as a starting point) and pass it via `-DEXTRA_CONF_FILE`.
|
||||
|
||||
Run `./zephcore_native_linux.exe --help` to see all available command-line arguments registered by the native_sim infrastructure (Zephyr drivers, our SPI/GPIO drivers, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission denied opening /dev/spidev0.0 or /dev/gpiochip*
|
||||
|
||||
Set up group access — see [Running without root](#running-without-root). On the
|
||||
Femtofox image the `spi`/`gpio` groups don't exist yet, so you must `groupadd`
|
||||
them and add a udev rule (just `usermod` is not enough). Or run with `sudo`.
|
||||
|
||||
### `/dev/spidev0.0: No such file or directory`
|
||||
|
||||
SPI isn't enabled in the device tree. See "Enabling spidev on the SBC" above.
|
||||
|
||||
### `Linux kernel headers too old; GPIO V2 chardev uAPI required`
|
||||
|
||||
The host adapter requires the GPIO V2 uAPI (kernel ≥ 5.10, late 2020). All current SBC distros (Debian 12+, Ubuntu 22.04+, Raspberry Pi OS bookworm+) ship recent enough kernels. If you hit this, your distro is ancient — upgrade.
|
||||
|
||||
### TCP port 5000 already in use
|
||||
|
||||
Another service (Flask, Docker registry, AirPlay…) has port 5000. Override:
|
||||
|
||||
```bash
|
||||
west build … -- -DCONFIG_ZEPHCORE_LINUX_TCP_PORT=15000
|
||||
```
|
||||
|
||||
### "TCP companion client disconnected" loops
|
||||
|
||||
The transport expects raw NUS bytes with no length prefix. If the connection drops immediately, capture traffic with `tcpdump -i any -X port 5000` and verify the first bytes the app sends look like a MeshCore opcode (e.g. `0x01` = CMD_APP_START), not an HTTP request or other framed protocol.
|
||||
|
||||
### `Failed to request line 8` / `CS GPIO configure failed: -5` (Raspberry Pi)
|
||||
|
||||
```
|
||||
<err> gpio_native_linux: Failed to request line 8 on host chip
|
||||
<err> spi_native_linux: CS GPIO configure failed: -5
|
||||
<err> sx126x_hal: SPI bus not ready
|
||||
<err> sx126x: HAL init failed: -19
|
||||
```
|
||||
|
||||
On the Pi, `dtparam=spi=on` binds the spi0 controller to GPIO 7–11 in ALT0, so
|
||||
**CE0 = BCM GPIO 8 is owned by the SPI controller** and cannot also be requested
|
||||
as a plain GPIO for chip-select — the request fails with `-EIO`. The fix is to let
|
||||
spidev drive CE0 in **hardware** (it asserts CS on every transfer) and **not**
|
||||
declare `cs-gpios` in the overlay. The `rak6421` / `rak6421_pi5` overlays do this
|
||||
(fixed 2026-06-03). If you copy the Femtofox overlay for a Pi setup, remove its
|
||||
`cs-gpios` line — that path
|
||||
only works when CS is a free GPIO (as it is on the Luckfox, GPIO48 on gpiochip1).
|
||||
|
||||
### Radio does nothing (no RX, CAD always times out, TX never completes)
|
||||
|
||||
The interrupt path (DIO1) and BUSY readback are the spine of all radio ops. If
|
||||
they're broken at the GPIO layer, the chip stays wedged in standby and *every*
|
||||
operation fails (CAD `-ETIMEDOUT`, no RX, no TX-done).
|
||||
|
||||
> **Don't** try to verify DIO1/BUSY with `gpioget /dev/gpiochip1 23` while the
|
||||
> binary is running — the kernel grants GPIO lines **exclusively**, so the
|
||||
> binary holds them and `gpioget` just returns `-EBUSY`. Reading it with the
|
||||
> binary stopped tells you nothing (nothing is driving the radio). Instead,
|
||||
> build with LoRa debug logging and read the chip's own registers:
|
||||
> ```bash
|
||||
> west build … -- … -DCONFIG_LORA_LOG_LEVEL_DBG=y
|
||||
> ```
|
||||
> A healthy boot shows `sx126x_irq_work_handler: IRQ status: …` lines on RX/CAD/TX.
|
||||
|
||||
Things that previously broke this exact path (all fixed; listed so you recognize
|
||||
the symptoms if a regression appears):
|
||||
- Native GPIO driver requesting no lines (the `GPIO_DISCONNECTED == 0` trap) →
|
||||
BUSY stuck-low, commands dropped, chip frozen.
|
||||
- Double inversion on active-low pins → RESET driven backwards, chip held in reset.
|
||||
- Missing real-time clock mode → CAD/TX/RX time out before the real radio responds
|
||||
(now forced automatically; do not remove `linux_native_setup.c`).
|
||||
|
||||
If a regression appears, check `port_get_raw`/`pin_configure` in
|
||||
`gpio_native_linux.c` and that `linux_native_setup.c` is still compiled.
|
||||
|
||||
### `bind(5000) failed: 98` (EADDRINUSE)
|
||||
|
||||
Another instance of the binary is already running, or you killed the last one with `kill -9` and the socket is still in TIME_WAIT. `SO_REUSEADDR` is set so this should clear in <1 minute; or `pkill zephcore_native_linux.exe`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ ZephCore Mesh (C++) │
|
||||
│ CompanionMesh / RepeaterMesh │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ Zephyr APIs (k_event, k_msgq, k_thread)
|
||||
│
|
||||
┌──────────────▼──────────────────────────────┐
|
||||
│ Zephyr RTOS @ native_sim (running as Linux │
|
||||
│ process via pthreads) │
|
||||
│ │
|
||||
│ • SX1262 driver (unchanged) │
|
||||
│ • New spi_native_linux driver ──┐ │
|
||||
│ • New gpio_native_linux driver ──┤ │
|
||||
│ • LinuxTCPTransport (replaces BLE) ──┤ │
|
||||
└──────────────────────────────────────────┼──┘
|
||||
│
|
||||
host syscalls + zsock_*
|
||||
│
|
||||
┌──────────────────────────────────────────▼──┐
|
||||
│ Linux kernel │
|
||||
│ /dev/spidevX.Y /dev/gpiochipN │
|
||||
│ AF_INET TCP socket /dev/pts/N │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The mesh C++ layer and the patched SX1262 Zephyr driver are bit-identical to the MCU builds. Only the SPI/GPIO host bridges and the companion transport differ.
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Not BLE.** The companion app must support TCP/Network mode. No BlueZ integration.
|
||||
- **Not for production use** per Zephyr's `native_sim` documentation. Works fine for hobby/lab deployments.
|
||||
- **Single companion client.** One TCP connection at a time, mirroring `BT_MAX_CONN=1`.
|
||||
- **Persistent storage** is file-backed and survives restarts — see [Persistent storage](#persistent-storage).
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
All native-Linux board files live under **`boards/linux_native/`**:
|
||||
|
||||
- `boards/linux_native/linux_common.conf` + `.overlay` — platform base, auto-applied when `BOARD=native_sim` (no device wiring)
|
||||
- `boards/linux_native/femtofox.conf` + `.overlay` — Femtofox preset (SX1262 TCXO wiring)
|
||||
- `boards/linux_native/rak6421.conf` + `.overlay` — RAK6421 HAT, Pi 2/3/4/Zero 2 (gpiochip0)
|
||||
- `boards/linux_native/rak6421_pi5.conf` + `.overlay` — RAK6421 HAT, Pi 5 (gpiochip4)
|
||||
|
||||
> These are `EXTRA_CONF_FILE` **presets**, not Zephyr boards — you still build with
|
||||
> `-b native_sim … -DEXTRA_CONF_FILE="boards/linux_native/<device>.conf"`, not `-b <device>`.
|
||||
- `adapters/transport/LinuxTCPTransport.c` — TCP companion transport
|
||||
- `adapters/transport/linux_native_setup.c` — forces real-time clock mode at boot (bakes in `--rt`)
|
||||
- `patches/zephyr-new/drivers/spi/spi_native_linux*` — spidev SPI driver
|
||||
- `patches/zephyr-new/drivers/gpio/gpio_native_linux*` — GPIO driver (kernel GPIO V2 chardev ioctls, no libgpiod)
|
||||
- `patches/zephyr/0007-spi-gpio-native-linux.patch` — wires the new drivers into Zephyr's `drivers/spi/` and `drivers/gpio/` CMakeLists + Kconfig
|
||||
- `patches/zephyr/0008-flash-sim-per-node-file.patch` — flash simulator defaults to `meshcore_settings_<hostname>.bin` for persistent per-node storage
|
||||
|
||||
---
|
||||
|
||||
## Debugging Log
|
||||
|
||||
(Updated as issues are found during bringup.)
|
||||
|
||||
### Verification status
|
||||
|
||||
End-to-end verified under WSL Ubuntu 24.04 (gcc 13.3):
|
||||
|
||||
- ✅ All `patches/zephyr/*.patch` apply cleanly (including the new `0007-spi-gpio-native-linux.patch`).
|
||||
- ✅ All files in `patches/zephyr-new/` copy correctly into the Zephyr tree.
|
||||
- ✅ Platform detection routes `BOARD=native_sim` to `boards/linux_native/linux_common.conf`.
|
||||
- ✅ `native_sim/native/64` builds clean → `build/zephyr/zephcore_native_linux.exe` (~4.3 MB ELF).
|
||||
- ✅ Binary runs. Zephyr OS boots, mesh event loop starts.
|
||||
- ✅ `spi_native_linux` driver loads and attempts to open `/dev/spidev0.0` (fails in WSL: no SPI hardware).
|
||||
- ✅ `LinuxTCPTransport` listens on port 5000.
|
||||
- ✅ TCP client connect/disconnect works; `SerialWifiInterface` (`<`/`>` + 2-byte LE length) framing parses correctly.
|
||||
- ✅ **End-to-end on real Femtofox hardware (2026-06-02):** radio RX, CAD (LBT), and TX all working; companion app connects and meshes; settings persist across restarts. (Required fixing two bugs in the native GPIO driver — see "Known caveats" below.)
|
||||
- ✅ **End-to-end on real Raspberry Pi Zero 2 W + RAK6421/RAK13300 hardware (2026-06-03):** SX1262 init, SPI with spidev hardware CS (CE0), DIO3 TCXO, DIO2 RF switch, and over-the-air RX/TX all confirmed. (Required dropping the GPIO chip-select from the Pi overlays — CE0 is owned by the kernel SPI controller; see the `Failed to request line 8` troubleshooting entry.)
|
||||
|
||||
Build command verified (run from inside WSL with workspace at `/mnt/d/zephcore`):
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export ZEPHYR_BASE=/mnt/d/zephcore/zephyr
|
||||
cd /mnt/d/zephcore && west build -b native_sim zephcore
|
||||
```
|
||||
|
||||
Use `native_sim` (32-bit) for x86-64 host smoke builds and for ARM cross-compile targets
|
||||
(Femtofox). Use `native_sim/native/64` only if targeting a 64-bit host natively (no
|
||||
cross-compile). The 32-bit variant requires `libc6-dev-i386` on the build host.
|
||||
|
||||
### Known caveats found during implementation
|
||||
|
||||
- **Native GPIO driver had two bugs that killed the radio** (fixed 2026-06-02,
|
||||
`patches/zephyr-new/drivers/gpio/`):
|
||||
1. `rebuild_line()` tested `(flags & GPIO_DISCONNECTED) == GPIO_DISCONNECTED`,
|
||||
but `GPIO_DISCONNECTED == 0`, so the test was *always true* and **no GPIO
|
||||
line was ever actually requested** — every input read returned 0 and every
|
||||
output write was a no-op (SPI still worked only because spidev drove its own
|
||||
hardware CS). BUSY read stuck-low → driver never waited for the chip →
|
||||
commands issued while BUSY → silently dropped → chip frozen in STBY_RC. Fix:
|
||||
test `(flags & (GPIO_INPUT | GPIO_OUTPUT)) == GPIO_DISCONNECTED`.
|
||||
2. The driver passed `GPIO_V2_LINE_FLAG_ACTIVE_LOW` to the kernel while Zephyr's
|
||||
generic GPIO layer *already* inverts active-low pins (`data->invert`) →
|
||||
**double inversion** drove the active-low RESET line backwards, holding the
|
||||
chip in reset. Fix: driver `port_*_raw` are physical/raw; let Zephyr own
|
||||
inversion (don't set the kernel `ACTIVE_LOW` flag).
|
||||
Symptom of both: TCXO/XOSC/CAD all *look* like the culprit (XOSC_START_ERR,
|
||||
CAD `-ETIMEDOUT`) but are downstream — the chip is simply wedged.
|
||||
|
||||
- **`--rt` is mandatory and now automatic.** Without real-time clock mode the
|
||||
`native_sim` clock free-runs decoupled from wall time, so real-radio timeouts
|
||||
(200 ms CAD, TX/RX-done) fire before the hardware responds. Forced at boot via
|
||||
`linux_native_setup.c` (`hwtimer_set_real_time_mode(true)`).
|
||||
|
||||
- **Double-overlay listing** in `EXTRA_DTC_OVERLAY_FILE`: `linux_common.overlay` gets auto-paired twice — once when the platform conf is selected, once again when the EXTRA_CONF_FILE list is re-walked. Pre-existing CMakeLists.txt quirk affecting all platform confs; harmless (DTC merges idempotently) but cosmetic.
|
||||
|
||||
- **Pin paths are runtime-overridable, pin numbers are not.** Only `--lora-spidev=<path>` and `--lora-gpio-chip=<path>` are wired as command-line args. To change actual pin numbers without rebuilding, create a custom preset overlay and pass it via `-DEXTRA_CONF_FILE`. (A future iteration could expose pin offsets as cmdline args too.)
|
||||
@@ -1,336 +0,0 @@
|
||||
# Repeater CLI Commands
|
||||
|
||||
All commands are sent over USB serial (CDC-ACM). Commands sent remotely over the mesh (non-zero `sender_timestamp`) cannot access USB-only commands.
|
||||
|
||||
> The **Room Server** role shares this CLI — the common commands (radio, region, password, advert, gps, etc.) plus `setperm` / `get acl` all apply.
|
||||
|
||||
**Sources:**
|
||||
- `helpers/CommonCLI.cpp` — common commands shared by all roles
|
||||
- `app/RepeaterMesh.cpp` — repeater-specific commands (`setperm`, `get acl`, `region`, `discover.neighbors`)
|
||||
- `app/RepeaterRegionCLI.cpp` / `app/RoomServerRegionCLI.cpp` — the `region` sub-CLI
|
||||
- `app/RepeaterUplink.cpp` — `get`/`set uplink.*` (ESP32 uplink builds only)
|
||||
- `app/RoomServerMesh.cpp` — room-server-specific commands (`room.post`)
|
||||
|
||||
> **Commands are case-sensitive**, matching Arduino MeshCore. Nothing is lower-cased before matching.
|
||||
|
||||
> **Request-tag prefix.** If a command is longer than 4 characters and its **third** character is `|`
|
||||
> (e.g. `a7|reboot`), the first three characters are stripped before dispatch and echoed back at the
|
||||
> start of the reply. This is how the phone app correlates replies with requests. It means a command
|
||||
> whose third character is a literal `|` cannot be sent as-is.
|
||||
|
||||
---
|
||||
|
||||
## System
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ver` | Firmware version and build date |
|
||||
| `board` | Board manufacturer name |
|
||||
| `reboot` | Reboot immediately |
|
||||
| `start dfu` | Reboot into UF2 bootloader for drag-and-drop firmware update |
|
||||
| `start ota` | ESP32: start WiFi AP + HTTP OTA server. nRF52: reboot into BLE OTA DFU mode |
|
||||
| `stop ota` | Stop WiFi OTA server (ESP32 only) |
|
||||
| `clkreboot` | Set clock to a fixed reference time (15 May 2024 8:50pm UTC) then reboot |
|
||||
| `powersaving` | Not implemented |
|
||||
|
||||
---
|
||||
|
||||
## Clock
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `clock` | Display current UTC time |
|
||||
| `clock sync` | Sync clock from the sender's timestamp (only advances, cannot go backwards). Arms the 7-day mesh-time-sync suppression window. |
|
||||
| `time <unix_timestamp>` | Set RTC to a specific Unix timestamp (cannot go backwards). Arms the 7-day mesh-time-sync suppression window. |
|
||||
|
||||
---
|
||||
|
||||
## Advertisement
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `advert` | Send a flood-routed self-advertisement (1500 ms delay) |
|
||||
| `advert.zerohop` | Send a 0-hop (direct only) self-advertisement |
|
||||
|
||||
---
|
||||
|
||||
## Neighbors
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `neighbors` | Display current neighbor list |
|
||||
| `neighbor.remove <pubkey_hex>` | Remove a neighbor entry by its public key. A prefix is accepted — the hex is truncated to at most 32 bytes and matched at whatever length you give. **Repeater only in effect:** `RoomServerMesh` does not override `removeNeighbor`, so on a room server this replies `OK` and does nothing. |
|
||||
| `discover.neighbors` | *(repeater only)* Broadcast a node discovery request to find nearby nodes. Takes no arguments — anything after it replies `Err - discover.neighbors has no options`. Not implemented on room servers. |
|
||||
|
||||
---
|
||||
|
||||
## Security & Access Control
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `password <new_password>` | Set the admin password (**max 15 characters**) |
|
||||
| `setperm <perms_hex> <pubkey_hex>` | Set ACL permissions for a node (app format: 2-char hex perms first) |
|
||||
| `setperm <pubkey_hex> <perms_dec>` | Set ACL permissions for a node (Arduino format: pubkey first, decimal perms) |
|
||||
| `get acl` | *(USB only)* List all ACL entries with permissions and public keys |
|
||||
|
||||
> **Password length:** admin and guest passwords are capped at **15 characters** (16-byte storage incl. NUL; same limit as Arduino MeshCore). The login-send path silently truncates anything longer, so a password >15 chars will never authenticate. Applies to `set guest.password` as well.
|
||||
|
||||
> **`allow.read.only` is room-server only.** `RoomServerMesh.cpp` is its sole consumer; `RepeaterMesh.cpp` never reads it, so on a repeater the setting silently did nothing. The CLI now only exposes it on `CONFIG_ZEPHCORE_ROLE_ROOM_SERVER` builds — a deliberate divergence from Arduino MeshCore, whose shared CommonCLI offers the knob on every role. The pref itself is unchanged: it stays byte 114 of the on-flash prefs layout, identical to Arduino's, so existing prefs files are unaffected.
|
||||
|
||||
> **Guest access differs by role, matching Arduino MeshCore.** On a **repeater**, an empty `guest.password` (the default) means *open* guest access — a blank submitted password logs in as `PERM_ACL_GUEST`, which cannot run CLI commands or read the access list, so it gets login plus status/telemetry only. On a **room server**, an empty `guest.password` *disables* guest login, so a room is never accidentally left open; to run an open room use `set allow.read.only on`, which grants read-only (`PERM_ACL_GUEST`), not post rights. Set a non-empty `guest.password` to require one on either role.
|
||||
|
||||
---
|
||||
|
||||
## Room Server
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `room.post <message>` | Post a message to the shared room as the server itself (system post). Pushed to clients like any other post. |
|
||||
|
||||
---
|
||||
|
||||
## Region Filtering
|
||||
|
||||
Regions control which flood packets the repeater forwards. The region tree is hierarchical; the wildcard `*` region is the root.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `region` | Export the current region map (indented text tree) |
|
||||
| `region load` | Enter interactive region load mode. Paste indented region lines; send a blank line to commit |
|
||||
| `region save` | Save the current region map to persistent storage |
|
||||
| `region def <token> [...]` | Cursor-walk bulk region builder — define a hierarchy in one line (see below) |
|
||||
| `region put <name> [<parent>]` | Create a region; default parent is the wildcard root. Flood is **allowed** by default (use `region denyf` to deny) |
|
||||
| `region remove <name>` | Remove a region (must have no children) |
|
||||
| `region get <name>` | Show a region's parent and flood-allow flag |
|
||||
| `region home [<name>]` | Get (no arg) or set the home region |
|
||||
| `region default [<name>\|<null>]` | Get (no arg), set, or clear (`<null>`) the default flood scope. Originated floods (self-adverts, etc.) are scoped with this region's TransportKey. Auto-creates the region if it doesn't exist and persists immediately |
|
||||
| `region allowf <name>` | Allow flood packets in a region (clears deny-flood flag) |
|
||||
| `region denyf <name>` | Deny flood packets in a region (sets deny-flood flag) |
|
||||
| `region list allowed` | List all regions that allow floods |
|
||||
| `region list denied` | List all regions that deny floods |
|
||||
|
||||
**Region load format:** one region per line, indented with spaces to indicate depth. Append `F` after the name to mark flood-allowed (otherwise flood is denied by default).
|
||||
|
||||
**`region def` format:** space-separated tokens; a cursor starts at `*`. Each token is `name` (create child of cursor, advance cursor to it) or `name|jump` / `name,jump` (create child of cursor, then move cursor to the existing region `jump`). Does **not** auto-save — follow with `region save`. Reply is the updated region tree. Example — branched tree: `region def west pnw or pdx|pnw wa sw-wa`. Example — flat list: `region def west|* pnw|* or|* pdx|*`.
|
||||
|
||||
---
|
||||
|
||||
## Statistics & Logging
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `clear stats` | Reset all statistics counters |
|
||||
| `stats-core` | *(USB only)* Display core mesh statistics |
|
||||
| `stats-radio` | *(USB only)* Display radio statistics |
|
||||
| `stats-packets` | *(USB only)* Display packet statistics |
|
||||
| `log start` | Enable packet logging to file |
|
||||
| `log stop` | Disable packet logging |
|
||||
| `log erase` | Erase the log file |
|
||||
| `log` | *(USB only)* Dump the full log file to USB serial |
|
||||
| `erase` | *(USB only)* Format the entire filesystem |
|
||||
|
||||
---
|
||||
|
||||
## GPS
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `gps` | Show GPS status (`on` or `off`) |
|
||||
| `gps on` | Enable GPS module |
|
||||
| `gps off` | Disable GPS module |
|
||||
| `gps setloc` | Update stored latitude/longitude from current GPS fix |
|
||||
| `gps advert` | Show current location advertising policy |
|
||||
| `gps advert none` | Do not include location in advertisements |
|
||||
| `gps advert share` | Include live GPS location in advertisements |
|
||||
| `gps advert prefs` | Include stored lat/lon from prefs in advertisements |
|
||||
| `set gps duty <sec>` | GPS duty interval (standby seconds between fixes). `0` = always-on (continuous; streams fresh fixes, can download a full almanac). Floor 10s, cap 604800 (1 week). Persists to flash, applied live. |
|
||||
| `set gps duty default` | Reset GPS duty to the role default (repeater/room 48h, companion 300s) |
|
||||
| `set gps diag <0\|1\|on\|off>` | Arm GPS module-configuration diagnostics (see below). Not persisted — clears on reboot |
|
||||
|
||||
**GPS configuration diagnostics.** At boot the firmware configures the GNSS module — constellations, AssistNow/EASY, minimum elevation, fix rate — and on modules driven over raw NMEA those commands are sent **blind**: nothing reads the module's reply, so a silently rejected configuration is indistinguishable from a working one. These two commands make that visible.
|
||||
|
||||
```
|
||||
set gps diag 1 # arm it
|
||||
gps off # power-cycle the module...
|
||||
gps on # ...which re-runs configuration and records the result
|
||||
get gps diag # read it back
|
||||
```
|
||||
|
||||
Sample reply:
|
||||
|
||||
```
|
||||
> diag=on cfg=uart age=910s rx=120 mod=URANUS5 sent=12/336B sys=G3/R4/E0/B3/?0
|
||||
```
|
||||
|
||||
- `rx=` NMEA sentences the driver has parsed. **Check this first** — it is the only field that cannot be misread. Non-zero means the module is alive, at the right baud, and talking, so anything still wrong is signal or antenna. Zero means nothing is arriving at all, and no antenna work will help
|
||||
- `cfg=` which path ran — `uart` (raw PMTK+PCAS+UBX), `api` (driver GNSS API), `blind` (neither available), or `never-run`
|
||||
- `mod=` module identification, from a CASIC `$GPTXT` version reply or a u-blox `$PUBX` poll response, or `no-reply`. Only an explicit software-version token is accepted as an identity — TXT sentences also carry warnings, and a warning reported as an identity is worse than no answer
|
||||
- `sent=` commands/bytes written to the module (UART path), or `sys_ret=`/`rate_ret=` return codes (API path)
|
||||
- `sys=` tracked satellites per constellation from GSV talker IDs: **G**PS / GLONASS (**R**) / Galileo (**E**) / **B**eiDou / other. A constellation that stops reporting for 30 s decays to zero rather than showing a stale count
|
||||
|
||||
`sys=` totalling more than `sats=` in `get gps` is expected, not a discrepancy: GSV counts satellites **tracked**, GGA counts satellites **used in the fix solution**.
|
||||
|
||||
**`rx=` first, then `mod=`.** `rx=` is the only field that cannot be misread: non-zero means the module is alive, at the right baud and talking, so anything still wrong is signal or antenna; zero means nothing is arriving at all. `mod=` then tells you whether the module *heard* us — everything on this transport is written blind, so a module that hears nothing looks exactly like one that hears everything and ignores it. `mod=no-reply` with `rx=` climbing means the receive direction works but our transmit does not reach it: wiring or pin assignment, not configuration.
|
||||
|
||||
**`sent=` proves transmission, not acceptance.** Only `sys=` shows what the module actually did. A module still running its factory or previously saved configuration reports `G` non-zero with the rest at `0`. Note `B0` is expected on u-blox M8 (BeiDou is deliberately disabled — only three major constellations can run concurrently), and `?0` is normal outside Japan (QZSS is regional).
|
||||
|
||||
The generic-NMEA path sends three protocols — PMTK (MediaTek), PCAS (CASIC: Quectel L76K/L76KB, Air530Z) and UBX (u-blox) — because a WisBlock-style GPS slot can hold any of them and each family ignores what it does not understand. Related build option: `CONFIG_ZEPHCORE_GPS_NAV_MODE` sets the CASIC navigation dynamic model (`$PCAS11`), defaulting to stationary for repeaters and room servers and automotive otherwise. It is worth setting because that model is stored *in the module* and survives reflashing the host — a slot module that previously lived in another device can arrive stuck in an airborne model that quietly degrades fixes on a fixed site.
|
||||
|
||||
Caveats: the `sys=` tally needs `CONFIG_ZEPHCORE_GPS_SAT_DIAG` (default on for repeaters, off for companions to save RAM) — the reply says so when built without it. Only the raw-UART path is re-run on `gps on`; boards with a real GNSS driver (Air530Z, LC76G) keep reporting their boot-time result, because that path goes through `modem_chat_run_script()`, which is safe only at boot. On those boards `E0` is also expected — the Air530Z driver supports GPS/GLONASS/BeiDou but not Galileo, and the firmware falls back automatically.
|
||||
|
||||
---
|
||||
|
||||
## Sensor Settings
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `sensor list [<start_idx>]` | List custom sensor settings (paginated at 134 chars) |
|
||||
| `sensor get <key>` | Get a custom sensor setting value by key |
|
||||
| `sensor set <key> <value>` | Set a custom sensor setting value |
|
||||
|
||||
---
|
||||
|
||||
## Radio (Temporary Override)
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tempradio <freq>,<bw>,<sf>,<cr>,<timeout_mins>` | Apply temporary radio parameters; automatically reverts after `timeout_mins`. Constraints: freq 150–2500 MHz, bw 7–500 kHz, sf 5–12, cr 5–8. Saved prefs are never mutated — concurrent `set` commands and reboots both restore the real saved values. |
|
||||
|
||||
---
|
||||
|
||||
## Repeater Uplink (ESP32 + `CONFIG_ZEPHCORE_REPEATER_UPLINK`)
|
||||
|
||||
These commands configure observer-style WiFi+MQTT packet reporting from repeater role.
|
||||
All `set uplink.*` changes are saved immediately and only applied after reboot.
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `get uplink.status` | Uplink runtime state: enabled flag, WiFi state, MQTT state, reboot-required flag |
|
||||
| `get uplink.enable` | Uplink enable flag (`on`/`off`) |
|
||||
| `get uplink.wifi.ssid` | Configured WiFi SSID |
|
||||
| `get uplink.mqtt.host` | Configured MQTT broker host |
|
||||
| `get uplink.mqtt.port` | Configured MQTT broker port |
|
||||
| `get uplink.mqtt.tls` | MQTT TLS mode (`0`/`1`) |
|
||||
| `get uplink.mqtt.user` | Configured MQTT username |
|
||||
| `get uplink.mqtt.iata` | Configured IATA/site code used in MQTT topic |
|
||||
| `set uplink.enable <on\|off>` | Enable or disable repeater uplink *(reboot required)* |
|
||||
| `set uplink.wifi.ssid <ssid>` | Set WiFi SSID *(reboot required)* |
|
||||
| `set uplink.wifi.psk <psk>` | Set WiFi password *(reboot required)* |
|
||||
| `set uplink.mqtt.host <host>` | Set MQTT host *(reboot required)* |
|
||||
| `set uplink.mqtt.port <port>` | Set MQTT port 1–65535 *(reboot required)* |
|
||||
| `set uplink.mqtt.tls <0\|1>` | Set MQTT TLS mode *(reboot required)* |
|
||||
| `set uplink.mqtt.user <user>` | Set MQTT username *(reboot required)* |
|
||||
| `set uplink.mqtt.password <pass>` | Set MQTT password *(reboot required)* |
|
||||
| `set uplink.mqtt.iata <code>` | Set MQTT site code *(reboot required)* |
|
||||
|
||||
---
|
||||
|
||||
## `get` — Read Configuration
|
||||
|
||||
| Command | Returns |
|
||||
|---------|---------|
|
||||
| `get name` | Node name |
|
||||
| `get role` | Firmware role: `repeater` or `room_server` (companion builds report `companion`) |
|
||||
| `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 tx` | TX power in dBm |
|
||||
| `get lat` | Stored latitude |
|
||||
| `get lon` | Stored longitude |
|
||||
| `get dutycycle` | Duty cycle as percentage (e.g. "50.0%") |
|
||||
| `get af` | Raw airtime factor value |
|
||||
| `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 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 |
|
||||
| `get flood.advert.interval` | Flood advertisement interval in hours |
|
||||
| `get advert.interval` | Local advertisement interval in minutes |
|
||||
| `get allow.read.only` | *(room server only)* Whether read-only clients are allowed |
|
||||
| `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 buzzer` | *(room server only)* Buzzer/vibration mode as `<n> (<name>)`: `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/LNA active during RX: `on` or `off` (default `on`) |
|
||||
| `get rxduty` | RX duty cycle mode: `0` or `1` |
|
||||
| `get gps duty` | Now-effective GPS duty interval in seconds (`always on (0)` when continuous) |
|
||||
| `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 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 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 public.key` | Node's public key as hex. **Not** USB-only — it is answerable over remote admin, matching Arduino MeshCore. A public key is broadcast in every advert, so there is nothing to gate. |
|
||||
| `get prv.key` | *(USB only)* Node's private key as hex — the 128-char expanded form, the same one `set prv.key` takes |
|
||||
|
||||
---
|
||||
|
||||
## `set` — Write Configuration
|
||||
|
||||
Changes are persisted immediately unless noted. Some require a reboot.
|
||||
|
||||
| Command | Constraints | Description |
|
||||
|---------|-------------|-------------|
|
||||
| `set name <name>` | No `[ ] \ : , ? *` | Set node name |
|
||||
| `set repeat <on\|off>` | | Enable or disable packet forwarding |
|
||||
| `set radio <freq>,<bw>,<sf>,<cr>` | freq 150–2500, bw 7–500, sf 5–12, cr 5–8 | **Comma-separated**, not space-separated — spaces parse as a single argument and the command is rejected. Set radio params *(reboot required)* |
|
||||
| `set freq <mhz>` | 150–2500 *(USB only)* | Set frequency alone *(reboot required)* |
|
||||
| `set tx <dbm>` | −9 to board max (default 30) | Set TX power |
|
||||
| `set lat <latitude>` | | Set stored latitude |
|
||||
| `set lon <longitude>` | | Set stored longitude |
|
||||
| `set dutycycle <pct>` | 1–100 | Set duty cycle percentage (converted to airtime factor internally) |
|
||||
| `set af <value>` | float | Set raw airtime factor directly |
|
||||
| `set txdelay <value>` | | Accepted for prefs compatibility — **ignored** (txdelay is adaptive) |
|
||||
| `set rxdelay <value>` | | Accepted for prefs compatibility — **ignored** (rxdelay is adaptive) |
|
||||
| `set direct.txdelay <value>` | | Accepted for prefs compatibility — **ignored** (direct.txdelay is adaptive) |
|
||||
| `set backoff.multiplier <m>` | 0.0–2.0 | Per-dupe reactive backoff multiplier (0 = disable reactive backoff) |
|
||||
| `set flood.max <count>` | 0–64 | Maximum flood retransmit hops |
|
||||
| `set flood.max.unscoped <count>` | 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 <count>` | 0–64 | Hop limit for ADVERT floods only (default 8); curbs advert churn independent of flood.max |
|
||||
| `set flood.advert.interval <hours>` | `0` (off) or 3–168 | How often the repeater floods its own advertisement. `0` disables periodic flood adverts. |
|
||||
| `set advert.interval <mins>` | `0` (off) or min–240 | How often the repeater sends local (zero-hop) advertisements. `0` — the default — disables them. Stored halved (the pref holds minutes/2), so odd values round down. |
|
||||
| `set allow.read.only <on\|off>` | | *(room server only)* Allow or deny read-only client connections |
|
||||
| `set guest.password <pwd>` | | Set guest access password |
|
||||
| `set owner.info <text>` | Use `\|` for newlines | Owner/contact information |
|
||||
| `set int.thresh <value>` | | 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 <on\|off\|1\|0>` | 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 agc.reset.interval <ms>` | 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 <mode>` | 0, 1, or 2 | Path hashing algorithm |
|
||||
| `set loop.detect <mode>` | `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** | External FEM/LNA gating in the RX direction, applied live. Supported only where the FEM's chip-enable is wired to the radio node as `antenna-enable-gpios` — today `heltec_t096`, `heltec_wireless_tracker_v2`, `heltec_wifi_lora32_v4` and `heltec_wifi_lora32_v43`. Every other board reports `Error: unsupported`, for one of three reasons: it has no external amplifier at all; its amplifier has no separate receive-side enable, only the DIO2/TXEN/RXEN transmit-receive switch (`station_g2`, `gat562_30s`, `ikoka_nano_30dbm`, `promicro_sx1262`); or its amplifier enable is a standalone always-on regulator outside the radio node (`rak3401_1watt` — moving that pin onto the lora node as `antenna-enable-gpios` is what would enable it). Non-SX126x radios (LR1110, LR2021, SX127x) never support it. Setting `0` drops the FEM's RX gain (~16 dB on the SKY66122) and its supply current (~6.5 mA) — sensitivity for battery life, not a tuning knob. Transmit always uses the FEM, and the driver's idle/sleep gating is unchanged. Replies `Error: unsupported` on non-SX126x radios and on boards with no FEM wired; the pref is still saved. |
|
||||
| `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) or 100–30000 | Battery voltage ADC calibration multiplier, set directly. Rejects non-numeric input, NaN/inf and negatives. |
|
||||
| `set adc.multiplier target <mv>` | 3000–4400 mV | Calibrate against a voltage you measured with a multimeter: rescales the current multiplier so the ADC reads `<mv>`. 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 <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 **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 <n>` | −8 to 12, default 0 | Operating detPeak offset from the chip family's per-SF base (SX126x: SF+13; LR11xx/LR20xx: 56–68 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 15–40, LR 48–90) 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 probe.interval <sec>` | 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 <pct>` | 0 (off) or 10–90, default **25** | Airtime-protection cap: the max percentage of TX attempts the node will let CAD defer before the staircase backs off to a less sensitive detPeak — counting **real** traffic, not just false positives. 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` | | Clear the accumulated per-level CAD probe statistics (RAM only; also cleared automatically on any radio parameter change). |
|
||||
| `set extra.sf <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 <hex>` | **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. |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **USB-only commands** — `get acl`, `get prv.key`, `set freq`, `log` (dump), `stats-packets`, `stats-radio`, `stats-core`, `erase` — are blocked when the command arrives over the mesh (remote admin). These are the only ones gated on `sender_timestamp == 0`; `get public.key` and `set prv.key` are **not** among them.
|
||||
- **Adaptive contention window** — `txdelay`, `rxdelay`, and `direct.txdelay` are accepted and stored for Arduino prefs compatibility but have no effect. Use `get txdelay` to inspect the current adaptive state and `set backoff.multiplier` to tune reactive backoff.
|
||||
- **Region load mode** — after `region load`, every line received is parsed as a region entry until a blank line is sent. The loaded map is only committed to the live region tree at that point; use `region save` to persist it.
|
||||
- **Reboot delay** — `start dfu`, `start ota` (nRF52 BLE-DFU path only), `reboot`, `clkreboot` and `erase` defer the reset by **2 seconds** so the reply can be transmitted over LoRa first. On a companion the handler then keeps deferring in 20 ms steps until the BLE/USB transport has drained, up to a further 3 s grace. On ESP32 `start ota` starts a WiFi AP + HTTP server and does **not** reboot.
|
||||
@@ -1,132 +0,0 @@
|
||||
# Supported Boards
|
||||
|
||||
Board strings for use with `west build -b <board> zephcore`.
|
||||
|
||||
## nRF52840
|
||||
|
||||
```
|
||||
rak4631
|
||||
rak3401_1watt
|
||||
wio_tracker_l1
|
||||
t1000_e
|
||||
meshtracker_x1
|
||||
thinknode_m1
|
||||
thinknode_m3
|
||||
thinknode_m6
|
||||
rak_wismesh_tag
|
||||
ikoka_nano_30dbm
|
||||
sensecap_solar
|
||||
xiao_nrf52840
|
||||
lilygo_techo
|
||||
promicro_sx1262
|
||||
heltec_t114
|
||||
heltec_t096
|
||||
gat562_30s
|
||||
muziworks_r1neo
|
||||
```
|
||||
|
||||
> **RAK WisMesh Pocket** (WisBlock pocket): use `-b rak4631` — same board string and firmware as **RAK4631**.
|
||||
>
|
||||
> **Heltec T114** screenless build: append `boards/nrf52840/heltec_t114/no_display.conf` to `EXTRA_CONF_FILE` for units without the TFT module.
|
||||
>
|
||||
> **SenseCAP MeshTracker X1** (`meshtracker_x1`): nRF52840 + LR2021, AG3335M dual-band L1+L5 GNSS, SPA06 barometer, DRV2605L vibration, YSN8900 RTC, 8 MB QSPI flash (`/ext`), RGB LED, buzzer. Untested on hardware — first ZephCore board to use a real LR2021. The RTC is treated as an RX8900 second-source; boot-time discovery validates that before trusting it.
|
||||
>
|
||||
> **Heltec Mesh Node T096** (`heltec_t096`): nRF52840 with SX1262 + KCT8103L PA/FEM, UC6580 GNSS, and ST7735S 160x80 TFT companion display. The external SPI flash footprint is documented in the board notes but left disabled until the device parameters are confirmed.
|
||||
|
||||
## ESP32
|
||||
|
||||
```
|
||||
xiao_esp32c3
|
||||
xiao_esp32c6/esp32c6/hpcore
|
||||
xiao_esp32s3/esp32s3/procpu
|
||||
lilygo_tlora_c6/esp32c6/hpcore
|
||||
lilygo_t3s3/esp32s3/procpu
|
||||
station_g2/esp32s3/procpu
|
||||
heltec_wifi_lora32_v3/esp32s3/procpu
|
||||
heltec_wifi_lora32_v4/esp32s3/procpu
|
||||
heltec_wifi_lora32_v43/esp32s3/procpu
|
||||
heltec_wireless_tracker/esp32s3/procpu
|
||||
heltec_wireless_tracker_v2/esp32s3/procpu
|
||||
thinknode_m9/esp32s3/procpu
|
||||
ttgo_tbeam/esp32/procpu
|
||||
ttgo_lora32/esp32/procpu # source-only, no published firmware
|
||||
```
|
||||
|
||||
> ESP32 boards require `west blobs fetch hal_espressif` before first build.
|
||||
>
|
||||
> Heltec V3 console/shell use `uart0` (UART serial) in ZephCore.
|
||||
>
|
||||
> **Heltec Wireless Tracker** (`heltec_wireless_tracker/esp32s3/procpu`): V1.1
|
||||
> ESP32-S3-FN8 companion with SX1262, ST7735R 160x80 TFT, and UC6580 GPS.
|
||||
>
|
||||
> **Heltec Wireless Tracker V2** (`heltec_wireless_tracker_v2/esp32s3/procpu`):
|
||||
> ESP32-S3FN8 companion with SX1262 + KCT8103L PA/FEM, ST7735R 160x80 TFT,
|
||||
> UC6580 GNSS, battery ADC, and USB-C native serial/JTAG.
|
||||
>
|
||||
> **ThinkNode M9** (`thinknode_m9/esp32s3/procpu`): ESP32-S3 (16MB flash, 8MB
|
||||
> PSRAM) handheld with LR1110, ST7789 320x240 TFT, CC1167Q GPS, PCF8563 RTC,
|
||||
> and buzzer. Not yet validated on hardware — see the board README for the
|
||||
> bring-up list. The onboard STC8H I2C keyboard, IMU/magnetometer, and SD slot
|
||||
> are not ported. The LR1110 radio-firmware updater supports this board
|
||||
> (`west build -b thinknode_m9/esp32s3/procpu zephcore/tools/lr1110_updater`).
|
||||
>
|
||||
> **LilyGo T-Beam** (`ttgo_tbeam/esp32/procpu`): classic ESP32 (PICO-D4) with
|
||||
> SX1262, AXP2101 PMU, and GNSS. Use this for the **v1.2 SX1262** variant — the
|
||||
> upstream Zephyr DTS models the SX1276 radio, which ZephCore overrides to
|
||||
> SX1262 in `board.overlay`. Console/CLI are on `uart0` (onboard USB-UART).
|
||||
>
|
||||
> **TTGO LoRa32** (`ttgo_lora32/esp32/procpu`): classic ESP32 (PICO-D4) with
|
||||
> **SX1276**. Console/CLI on `uart0`.
|
||||
>
|
||||
> **Source-only — no published firmware, and not a supported configuration.**
|
||||
> It is deliberately absent from `build.sh`, the release workflow and the Mesh
|
||||
> America catalog, so no release ever carries a binary for it. It is kept in the
|
||||
> tree as the one exercise of the SX127x loramac-node radio path, which is old,
|
||||
> lacks RX duty cycle and RX gain boost, and gets none of the work the native
|
||||
> SX126x/LR11xx/LR20xx drivers do. Build it yourself if you want it; expect to
|
||||
> maintain it yourself too.
|
||||
>
|
||||
> If you do build the companion, note it is DRAM-bound (~97% of `dram0_0_seg`)
|
||||
> and capped at **50 contacts / 32 queued offline messages** — lower than any
|
||||
> other companion board. The repeater has no such limit. At the 150/128 this
|
||||
> board's config used to declare, it did not link at all. See `board.conf` for
|
||||
> the measured budget before raising either number.
|
||||
|
||||
## STM32WL
|
||||
|
||||
```
|
||||
lora_e5_mini
|
||||
```
|
||||
|
||||
> **Seeed LoRa-E5 mini** (`lora_e5_mini`): STM32WLE5JC with the integrated
|
||||
> SX1262-class sub-GHz radio. No BLE and no USB device — the companion
|
||||
> protocol and the CLI both run over USART1 (bridged to USB-C by the onboard
|
||||
> USB-UART chip). Flash over SWD/ST-Link with `west flash`.
|
||||
|
||||
## MG24 (Silicon Labs)
|
||||
|
||||
```
|
||||
xiao_mg24
|
||||
```
|
||||
|
||||
> Requires `west blobs fetch hal_silabs` and pyocd.
|
||||
|
||||
## nRF54L
|
||||
|
||||
```
|
||||
xiao_nrf54l15/nrf54l15/cpuapp
|
||||
me25ls02/nrf54l15/cpuapp
|
||||
```
|
||||
|
||||
> Requires `--no-sysbuild` flag: `west build -b xiao_nrf54l15/nrf54l15/cpuapp zephcore --no-sysbuild`
|
||||
>
|
||||
> The nRF54L15 has no USB peripheral, so there is no UF2 or DFU path on either board — flash
|
||||
> `zephyr.hex` over SWD. `me25ls02` is the MinewSemi ME25LS02 module on its MX25LE02 carrier;
|
||||
> its USB-C port is a CH340x UART bridge carrying the console, not a programming port.
|
||||
> Untested on hardware.
|
||||
|
||||
## Native Linux
|
||||
|
||||
Not `west build -b` boards — `EXTRA_CONF_FILE` presets on top of `native_sim`
|
||||
for SBCs (Femtofox / Luckfox Pico Mini, Raspberry Pi + RAK6421 HAT). See
|
||||
[LINUX_NATIVE.md](../LINUX_NATIVE.md) for build commands and wiring.
|
||||
Binary file not shown.
Reference in new issue
Block a user