mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 20:19:56 +00:00
readme housekeeping
This commit is contained in:
@@ -1,238 +1,192 @@
|
||||
<h1>ZephCore — MeshCore for Zephyr RTOS <img src="img/kite-network-logo-thick-bright.svg" alt="ZephCore logo" width="85" align="middle"></h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="img/kite-network-logo-thick-bright.svg">
|
||||
<img src="img/kite-network-logo-thick-dark.svg" alt="ZephCore logo" width="140">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
A port of [MeshCore](https://github.com/meshcore-dev/MeshCore/) LoRa mesh firmware from Arduino to [Zephyr RTOS](https://zephyrproject.org/). Aiming for full protocol compatibility with the original Arduino firmware and the MeshCore mobile apps.
|
||||
<h1 align="center">ZephCore</h1>
|
||||
|
||||
## Why Zephyr?
|
||||
<p align="center"><b>MeshCore, but it runs on Zephyr.</b></p>
|
||||
|
||||
The Arduino version uses a `loop()`. This port replaces that with Zephyr's event-driven primitives (`k_event_wait`, `k_poll`, `k_msgq`), so the CPU sleeps in WFI (Wait For Interrupt) between events.
|
||||
<p align="center">
|
||||
<a href="https://github.com/liquidraver/ZephCore/actions/workflows/build.yml"><img src="https://github.com/liquidraver/ZephCore/actions/workflows/build.yml/badge.svg" alt="build"></a>
|
||||
<a href="https://github.com/liquidraver/ZephCore/actions/workflows/tests.yml"><img src="https://github.com/liquidraver/ZephCore/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
|
||||
<a href="https://github.com/liquidraver/ZephCore/releases/latest"><img src="https://img.shields.io/github/v/release/liquidraver/ZephCore" alt="latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT"></a>
|
||||
</p>
|
||||
|
||||
Other benefits:
|
||||
<p align="center">
|
||||
<a href="https://zephcore.meshcore.dev">Flash it</a> ·
|
||||
<a href="docs/supported_boards.md">Boards</a> ·
|
||||
<a href="docs/Repeater_CLI_commands.md">CLI</a> ·
|
||||
<a href="docs/DESIGN.md">Design</a> ·
|
||||
<a href="releasenotes/">Release notes</a>
|
||||
</p>
|
||||
|
||||
- **Proper driver model** -- LoRa, GNSS, display, sensors, and BLE all use Zephyr subsystem drivers rather than Arduino libraries
|
||||
- **Hierarchical build configuration** -- board-specific settings compose cleanly via Kconfig and devicetree overlays
|
||||
- **DFU support** -- generates Arduino compatible zip packages for OTA updates and UF2 binaries for drag-and-drop flashing
|
||||
- **Back and forth compatible** -- Adapted to softdevice and adafruit's bootloader, so no bootloader re-flashing required.
|
||||
---
|
||||
|
||||
## Supported Boards
|
||||
ZephCore is firmware for [MeshCore](https://github.com/meshcore-dev/MeshCore/) LoRa mesh nodes. It talks to
|
||||
the same mesh, pairs with the same MeshCore apps and answers the same CLI commands as the original firmware.
|
||||
What is different is underneath: instead of Arduino, it is built on [Zephyr RTOS](https://zephyrproject.org/).
|
||||
|
||||
nRF52840, ESP32, nRF54L15, MG24, and STM32WL boards, covering SX126x, LR1110,
|
||||
LR2021, and SX127x radios. ZephCore also runs as a **native Linux process** on
|
||||
SBCs (Femtofox / Luckfox Pico Mini, Raspberry Pi + RAK6421 HAT) with a real
|
||||
SX1262 on SPI/GPIO and the companion app connecting over TCP — see
|
||||
[LINUX_NATIVE.md](docs/LINUX_NATIVE.md).
|
||||
If you already run MeshCore, a ZephCore node is just another node on your mesh. Your neighbours will not
|
||||
notice, and neither will your phone.
|
||||
|
||||
For the full board list with exact `west build -b` strings, radios, and
|
||||
hardware notes, see the [supported boards list](docs/supported_boards.md) and
|
||||
the [Board Porting Guide](zephcore/boards/example_board/README.md).
|
||||
It is a community port, not the official firmware. The protocol and the apps belong to the MeshCore
|
||||
project; ZephCore follows them and does not change what goes over the air.
|
||||
|
||||
## Device Roles
|
||||
> [!NOTE]
|
||||
> About 99% of this code was written by AI. A human decides what gets built and tests it on real radios.
|
||||
> More on that [at the bottom](#who-wrote-this).
|
||||
|
||||
- **Companion** (default) -- connects to MeshCore mobile apps via BLE/USB. Contacts, channels, offline message queue.
|
||||
- **Repeater** -- forwards packets, configured via USB serial CLI. See the [Repeater CLI Command Reference](docs/Repeater_CLI_commands.md) for all available commands.
|
||||
- **Room Server** -- store-and-forward shared message room (a "BBS"). Clients log in with an admin or guest password and post messages; the server pushes each new post to every other logged-in client. No BLE; configured via the same USB serial CLI as the repeater.
|
||||
- **Observer** (ESP32 only) -- listen-only node that publishes received LoRa packets to MQTT over WiFi STA. Configured at runtime via serial CLI.
|
||||
## Get it on your device
|
||||
|
||||
## Building
|
||||
1. **Web flasher** (easiest): open [zephcore.meshcore.dev](https://zephcore.meshcore.dev) in Chrome or
|
||||
Edge, pick your board and role, plug in, flash.
|
||||
2. **Mesh America Device Configurator**: [apps.meshamerica.com](https://apps.meshamerica.com) carries the
|
||||
same builds.
|
||||
3. **By hand**: download from [Releases](https://github.com/liquidraver/ZephCore/releases). nRF52 boards
|
||||
take the `.uf2` by drag and drop; ESP32 boards take the `-merged.bin` at offset 0 with esptool.
|
||||
|
||||
Prerequisites: [Zephyr SDK >=1.0.1 (!)](https://docs.zephyrproject.org/latest/develop/getting_started/index.html) and `west` installed (required by Zephyr 4.4.0, which is pinned in `west.yml`).
|
||||
> [!WARNING]
|
||||
> Coming from stock MeshCore (or going back)? On nRF52 the bootloader stays the same, so there is nothing
|
||||
> extra to flash. But settings do not carry over between the two firmwares in either direction: expect to
|
||||
> set the node up again.
|
||||
|
||||
Optional: [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil) to allow DFU zip generation for OTA updates on nRF52.
|
||||
Then pair with the MeshCore app as usual (default BLE PIN `123456`). Not sure your board is covered? Check
|
||||
the [supported boards list](docs/supported_boards.md).
|
||||
|
||||
Or skip the prerequisites entirely and use the [dev container](.devcontainer/README.md) -- SDK, toolchains, west and the vendor blobs, preconfigured to match CI. Open the repo in VS Code and run **Dev Containers: Reopen in Container**.
|
||||
## What a node can be
|
||||
|
||||
| Role | What it does | How you talk to it |
|
||||
|---|---|---|
|
||||
| **Companion** | Your personal node: contacts, channels, offline message queue | MeshCore app over BLE or USB (WiFi on some ESP32 boards) |
|
||||
| **Repeater** | Relays packets for everyone else | [Text CLI](docs/Repeater_CLI_commands.md) over USB, or remote admin over the mesh |
|
||||
| **Room server** | A shared message board that keeps posts for clients who were away | Same CLI as the repeater; users log in from the app |
|
||||
| **Observer** | Listens only and publishes what it hears to MQTT over WiFi (ESP32) | Same CLI |
|
||||
|
||||
One role per firmware image; you choose it when you flash.
|
||||
|
||||
## What you get on top of stock MeshCore
|
||||
|
||||
All of this is local behaviour. Nothing here changes the wire protocol, so mixed meshes work fine.
|
||||
|
||||
- **A CPU that sleeps.** Everything is event-driven; there is no `loop()` spinning between packets.
|
||||
- **Self-tuning retransmit timing.** Repeaters measure how crowded their neighbourhood is and space their
|
||||
retransmits to match, instead of using fixed `txdelay` values. ([ADR 0007](docs/adr/0007-adaptive-contention-window.md))
|
||||
- **Adaptive listen-before-talk.** Channel-activity detection calibrates itself to the local noise.
|
||||
([ADAPTIVE_CAD.md](docs/ADAPTIVE_CAD.md))
|
||||
- **Optional RX duty cycle.** The radio naps between preamble checks to cut receive current, on radios
|
||||
that support it. ([ADR 0006](docs/adr/0006-sx126x-rx-duty-cycle-and-busy-gating.md))
|
||||
- **Mesh time sync.** Nodes without GPS can agree on the time. Off by default.
|
||||
([MESHTIMESYNC.md](docs/MESHTIMESYNC.md))
|
||||
- **WiFi firmware updates** for ESP32 repeaters, so the node on the roof can stay on the roof.
|
||||
- **Runs on Linux too.** The same code runs as a normal process on a Raspberry Pi or Femtofox with a real
|
||||
SX1262 attached. ([LINUX_NATIVE.md](docs/LINUX_NATIVE.md))
|
||||
- **Newer radios.** LR1110 and LR2021 alongside SX126x and SX127x.
|
||||
|
||||
## Hardware
|
||||
|
||||
<!-- Generated by `python zephcore/scripts/board_manifest.py docs`; edit the board manifests, not this table. -->
|
||||
<!-- boards:summary -->
|
||||
| Platform | Boards | With published firmware |
|
||||
|---|---|---|
|
||||
| nRF52840 | 22 | 21 |
|
||||
| ESP32 family | 16 | 14 |
|
||||
| nRF54L | 4 | 3 |
|
||||
| EFR32MG24 | 1 | 1 |
|
||||
| STM32WL | 1 | 1 |
|
||||
| Native Linux (presets) | 3 | 3 |
|
||||
<!-- /boards -->
|
||||
|
||||
RAK4631, SenseCAP T1000-E, Wio Tracker L1, Heltec T114 and V3/V4, XIAO ESP32-S3, nRF54L15 and MG24, Seeed
|
||||
LoRa-E5, Femtofox, Raspberry Pi with a RAK6421 HAT, and more. The full list with exact build strings is in
|
||||
[docs/supported_boards.md](docs/supported_boards.md). Want to add one? See the
|
||||
[board porting guide](zephcore/boards/example_board/README.md).
|
||||
|
||||
## Build it yourself
|
||||
|
||||
The quickest start is the [dev container](.devcontainer/README.md): open the repo in VS Code and choose
|
||||
**Reopen in Container**. Otherwise install the
|
||||
[Zephyr SDK and west](https://docs.zephyrproject.org/latest/develop/getting_started/index.html), then:
|
||||
|
||||
```bash
|
||||
# Initialize workspace (first time only)
|
||||
cd <cloned folder>
|
||||
# once
|
||||
west init -l zephcore
|
||||
west update
|
||||
|
||||
# Companion (production — default, no extra conf needed)
|
||||
# a companion, then a repeater
|
||||
west build -b wio_tracker_l1 zephcore --pristine
|
||||
west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
|
||||
|
||||
# Companion (debug logging)
|
||||
west build -b wio_tracker_l1 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/debug.conf"
|
||||
|
||||
# Repeater
|
||||
west build -b rak4631 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/repeater.conf"
|
||||
|
||||
# Repeater (debug logging)
|
||||
west build -b rak4631 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/debug.conf"
|
||||
|
||||
# Repeater with packet logging (clean RAW/RX/TX lines only, no debug spam)
|
||||
west build -b rak4631 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/packet_logging.conf"
|
||||
|
||||
# ESP32 repeater + WiFi AP HTTP OTA (requires sysbuild for MCUboot)
|
||||
west build -b xiao_esp32s3/esp32s3/procpu zephcore --pristine --sysbuild -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/wifi_ota.conf"
|
||||
|
||||
# Room Server (store-and-forward BBS, USB CLI)
|
||||
west build -b rak4631 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/room_server.conf"
|
||||
|
||||
# Observer (ESP32, listen-only WiFi+MQTT)
|
||||
west build -b xiao_esp32c3 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/observer.conf"
|
||||
|
||||
# Formatter (factory-reset utility)
|
||||
west build -b wio_tracker_l1 zephcore/tools/formatter --pristine
|
||||
|
||||
# BLE debug logging (debug.conf enables logging; the flag raises the BLE adapter to DBG)
|
||||
west build -b rak4631 zephcore --pristine -- \
|
||||
-DEXTRA_CONF_FILE="boards/common/debug.conf" -DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y
|
||||
west flash
|
||||
```
|
||||
|
||||
Output binaries are in `build/zephyr/` -- `.hex`, `.uf2`, and DFU `.zip` as applicable.
|
||||
Roles and extras are config fragments in `zephcore/boards/common/`, added through `EXTRA_CONF_FILE`
|
||||
(separate several with `;`):
|
||||
|
||||
**Platform notes:**
|
||||
- ESP32 boards require `west blobs fetch hal_espressif` once before the first build.
|
||||
- ESP32 default builds use `CONFIG_ESP_SIMPLE_BOOT` (no MCUboot) -- `west flash` writes a self-contained `zephyr.bin`. The `wifi_ota.conf` overlay is the exception: it requires MCUboot, so add `--sysbuild`.
|
||||
- MG24 requires `west blobs fetch hal_silabs` and `pyocd` (`west flash --runner pyocd`).
|
||||
- nRF54L15 requires `--no-sysbuild` (no MCUboot support yet).
|
||||
- Heltec V3 routes console/shell to `uart0` -- use the UART serial port for logs/CLI.
|
||||
| Fragment | Gives you |
|
||||
|---|---|
|
||||
| *(none)* | Companion |
|
||||
| `repeater.conf` | Repeater |
|
||||
| `room_server.conf` | Room server |
|
||||
| `observer.conf` | Observer (ESP32) |
|
||||
| `debug.conf` | Logging and asserts |
|
||||
| `packet_logging.conf` | One clean RAW/RX/TX line per packet, nothing else |
|
||||
|
||||
Always use `--pristine` when switching boards or roles.
|
||||
Every other variant, flashing per platform (ESP32 blobs and `--sysbuild`, nRF54L, MG24) and the Kconfig
|
||||
options are in [docs/BUILDING.md](docs/BUILDING.md).
|
||||
|
||||
## Architecture Overview
|
||||
## For Zephyr people
|
||||
|
||||
```
|
||||
Mobile App <--BLE (NUS)--> [ Companion ] <--LoRa--> Mesh Network
|
||||
|
|
||||
k_event_wait()
|
||||
/ | \
|
||||
LORA_RX LORA_TX_DONE BLE_RX
|
||||
```
|
||||
ZephCore is a plain west workspace application, and it tries to be a well-behaved one:
|
||||
|
||||
All code paths are event-driven. The CPU sleeps in WFI between events.
|
||||
- **Zephyr is pinned to a `main` commit**, bumped deliberately. ([ADR 0005](docs/adr/0005-zephyr-main-pin.md))
|
||||
- **A small patch set** in `zephcore/patches/zephyr` is applied at configure time and fails the build
|
||||
loudly if it stops applying. One patch owns each upstream file. If you maintain one of those files,
|
||||
that directory is the list of things we would love to see fixed upstream.
|
||||
- **Radios**: the in-tree SX126x and SX127x drivers, plus out-of-tree LR11xx and LR20xx drivers, behind
|
||||
one adapter.
|
||||
- **Boards are devicetree and Kconfig only.** A board directory declares hardware, never role policy;
|
||||
config is layered from `prj.conf` down to `board.conf`.
|
||||
- **ESP32**: simple boot for companions, MCUboot with sysbuild where WiFi OTA is involved
|
||||
([ADR 0001](docs/adr/0001-esp32-boot-and-flash-layout.md)); native USB on S3 companions
|
||||
([ADR 0002](docs/adr/0002-esp32s3-native-usb-companion.md)).
|
||||
- **Tests**: the mesh core builds and runs on the host under ASan and UBSan in CI.
|
||||
|
||||
- **LoRa RX**: Zephyr driver callback enqueues to a ring buffer and signals the mesh event loop
|
||||
- **LoRa TX**: A dedicated thread blocks on `k_poll()`, restarts RX on completion, then notifies the mesh loop
|
||||
- **BLE**: NUS write handler enqueues to `k_msgq` and signals the mesh loop; TX uses `bt_gatt_notify_cb()` chaining
|
||||
- **USB**: CDC-ACM with V3 binary framing protocol, frame timeout recovery
|
||||
- **Main loop**: `k_event_wait()` blocks until work arrives; housekeeping runs every 5s
|
||||
## Documentation
|
||||
|
||||
### Key Differences from Arduino
|
||||
| | |
|
||||
|---|---|
|
||||
| [DESIGN.md](docs/DESIGN.md) | The system view: roles, layers, rules |
|
||||
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Component reference |
|
||||
| [adr/](docs/adr/README.md) | Decisions and why they were made |
|
||||
| [BUILDING.md](docs/BUILDING.md) | Every build variant, platform notes, Kconfig options |
|
||||
| [Repeater_CLI_commands.md](docs/Repeater_CLI_commands.md) | Every CLI command |
|
||||
| [supported_boards.md](docs/supported_boards.md) | Boards and build strings |
|
||||
| [LINUX_NATIVE.md](docs/LINUX_NATIVE.md) | Running on a Pi or Femtofox |
|
||||
| [releasenotes/](releasenotes/) | What changed in each release |
|
||||
|
||||
| | Arduino | Zephyr |
|
||||
|---|---------|--------|
|
||||
| Idle behavior | Cooperative loop; CPU busy-waits unless `board.sleep()` called explicitly | `k_event_wait(K_FOREVER)` yields to idle thread → WFI between events |
|
||||
| LoRa TX completion | ISR sets flag, polled in `loop()` via `isSendComplete()` | ISR signals `k_poll_signal`, dedicated thread blocks on `k_poll()` |
|
||||
| BLE transport | Platform-specific (ESP-IDF BLE, Adafruit nRF52 lib) | Unified `bt_gatt` API across all SoCs |
|
||||
| LoRa driver | RadioLib (userspace SPI bit-bang) | Zephyr subsystem driver (DTS-configured, kernel-managed SPI) |
|
||||
| Configuration | `platformio.ini` + `variant.h` per board | Kconfig + devicetree overlays, hierarchical config inheritance |
|
||||
| Threading | Single `loop()` + ISRs | Explicit threads (main mesh, TX wait) + system work queue |
|
||||
## Who wrote this?
|
||||
|
||||
### Adaptive Contention Window (ZephCore-only)
|
||||
Mostly robots. Around 99% of the code in this repository was written by AI (Claude and Cursor). One human
|
||||
points, argues, says "no, not like that", and flashes the result onto a bench full of real radios to see
|
||||
whether it actually works.
|
||||
|
||||
Arduino MeshCore uses three static delay knobs (`txdelay`, `rxdelay`, `direct.txdelay`) that add the same retransmit jitter regardless of local conditions. In a linear chain of repeaters where each only hears its neighbor, this adds latency for zero benefit. In dense areas with 50+ neighbors, the same value may be too low to avoid collisions.
|
||||
The ideas that matter are not ours either: the mesh protocol, the apps and most new features come from the
|
||||
[MeshCore project](https://github.com/meshcore-dev/MeshCore/) and the people doing the hard thinking there.
|
||||
|
||||
ZephCore replaces all three with a self-tuning system based on **observed retransmit contention**:
|
||||
What keeps it honest: changes are tested on real hardware before release (mostly nRF boards on our own
|
||||
bench; the ESP32 boards are largely tested by the community, so reports are very welcome), the mesh core
|
||||
has host tests in CI, and the decisions are written down in [ADRs](docs/adr/README.md) so the next robot
|
||||
does not undo them.
|
||||
|
||||
1. **Dupe counting**: When a node retransmits a flood packet, it counts how many times it hears that same packet retransmitted by neighbors within a 10-second window. This is a direct measurement of local contention -- 0 dupes means a quiet linear chain, 15+ means a dense cluster.
|
||||
[](https://www.twonks.co.uk/)
|
||||
|
||||
2. **EMA-based delay sizing**: Dupe counts feed into a rolling exponential moving average. This drives a sqrt-curve delay factor for future retransmits: near-zero delay in sparse areas, scaling up in dense ones. At ~15 dupes (moderate density), the factor matches the old Arduino default of 0.5. Jitter is double-capped at `min(2000ms, 6·airtime)` for repeaters.
|
||||
|
||||
3. **Reactive per-packet backoff**: When a node is waiting to retransmit and hears a neighbor retransmit the same packet, it pushes its own TX back by a random amount (up to `backoff.multiplier × airtime`). This is real-time CSMA -- you hear the channel being used for your packet, so you defer. Capped at `min(2000ms, 12·airtime)` total reactive extension per packet.
|
||||
|
||||
**Companion-originated floods** use a smaller spread (up to `min(1000ms, 3·airtime)`) so user messages feel responsive while still avoiding collisions with nearby repeaters that may be in TX/RX.
|
||||
|
||||
**Direct packets** (routed, single next-hop) use minimal fixed jitter instead of adaptive delay, since only the next hop retransmits them.
|
||||
|
||||
The old `txdelay`, `rxdelay`, and `direct.txdelay` commands are still accepted for binary compatibility with Arduino prefs but are ignored -- the system is fully adaptive.
|
||||
|
||||
**CLI commands:**
|
||||
- `get txdelay` -- shows adaptive status: contention estimate and current flood delay factor
|
||||
- `get/set backoff.multiplier` -- per-dupe reactive backoff multiplier (default 0.2, range 0.0-2.0). Set to 0 to disable reactive backoff (EMA window still works). Higher values allow more per-packet deferral in dense areas.
|
||||
|
||||
**Compatibility**: Purely local behavior, no wire protocol changes. Works alongside Arduino MeshCore repeaters -- their retransmits are counted as dupes just the same.
|
||||
|
||||
## Power Saving
|
||||
|
||||
- **LoRa RX duty cycle**: chip-autonomous receive windowing (sniff mode) reduces LoRa RX current from ~10-15mA to ~3-5mA. Off by default; toggle at runtime with `set rxduty on/off` (SX126x only — unsupported on LR1110 due to a mid-preamble lock issue, and on SX127x). Window timing is auto-sized per SF/BW/preamble from the SX126x datasheet constraints.
|
||||
- **Production by default**: No logging, no asserts, reboot-on-fatal. Add `debug.conf` to enable logging.
|
||||
- **GPIO-gated GPS**: Powered on only during fix acquisition
|
||||
|
||||
## Configuration
|
||||
|
||||
Key Kconfig options (set in board configs or via `-D` flags):
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `CONFIG_ZEPHCORE_ROLE_COMPANION` | y | BLE companion mode |
|
||||
| `CONFIG_ZEPHCORE_ROLE_REPEATER` | n | USB CLI repeater mode |
|
||||
| `CONFIG_ZEPHCORE_ROLE_OBSERVER` | n | Listen-only WiFi+MQTT mode (ESP32) |
|
||||
| `CONFIG_ZEPHCORE_RADIO_NATIVE` | y | SX1261/SX1262/SX1268, LLCC68, STM32WL (Zephyr native sx126x driver) |
|
||||
| `CONFIG_ZEPHCORE_RADIO_LR1110` | n | LR1110/LR1120/LR1121 (custom driver) |
|
||||
| `CONFIG_ZEPHCORE_RADIO_LR2021` | n | LR2021 (custom driver) |
|
||||
| `CONFIG_ZEPHCORE_RADIO_SX127X` | n | SX1272/SX1276/SX1278 (loramac-node backend) |
|
||||
| `CONFIG_ZEPHCORE_LORA_RX_DUTY_CYCLE` | n | RX duty cycle (sniff mode) boot default; runtime toggle via `set rxduty on/off` (SX126x only) |
|
||||
| `CONFIG_ZEPHCORE_APC` | y (compiled in, runtime OFF) | Adaptive Power Control — enable at runtime via CLI |
|
||||
| `CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM` | 22 | Initial TX power; lower for boards with external PA |
|
||||
| `CONFIG_ZEPHCORE_MAX_TX_POWER_DBM` | 22 | Hard cap (radio adapter clamps above this) |
|
||||
| `CONFIG_ZEPHCORE_MAX_CONTACTS` | 350 | Contact storage slots (companion) |
|
||||
| `CONFIG_ZEPHCORE_MAX_CHANNELS` | 40 | Channel slots (companion) |
|
||||
| `CONFIG_ZEPHCORE_BLE_PASSKEY` | 123456 | BLE pairing PIN |
|
||||
| `CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC` | 300 | Companion GPS duty interval between fixes (seconds, 10–86400); always-on is a runtime setting (`set gps duty 0`) |
|
||||
| `CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC` | 300 | Cold-start window for the very first fix (longer to allow almanac download) |
|
||||
| `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC` | 172800 | Repeater/room-server GPS duty interval boot default (48 h); 0 = always-on |
|
||||
| `CONFIG_ZEPHCORE_WIFI_OTA` | n | WiFi AP + HTTP OTA updates (ESP32 repeaters, requires `--sysbuild`) |
|
||||
| `CONFIG_ZEPHCORE_REPEATER_UPLINK` | n | Repeater WiFi+MQTT uplink (ESP32) |
|
||||
| `CONFIG_ZEPHCORE_PACKET_LOGGING` | n | Arduino-compatible mesh packet logging |
|
||||
| `CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS` | 5000 | Periodic maintenance interval |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
zephcore/
|
||||
src/ Core mesh engine (Mesh, Dispatcher, Packet, Identity, ContentionTracker)
|
||||
app/ Companion / Repeater / Room Server / Observer role implementations
|
||||
adapters/
|
||||
ble/ BLE NUS transport
|
||||
board/ GPIO, LED, power management
|
||||
clock/ Millisecond and RTC clocks
|
||||
datastore/ LittleFS filesystem wrapper
|
||||
gps/ GPS/GNSS drivers
|
||||
mqtt/ MQTT publisher (observer / repeater uplink)
|
||||
ota/ WiFi AP + HTTP firmware update server
|
||||
radio/ LoRa radio drivers (SX126x, LR1110, LR2021, SX127x)
|
||||
rng/ Random number generator
|
||||
sensors/ I2C sensor auto-detection
|
||||
transport/ TCP companion (native Linux) + serial companion (STM32WL)
|
||||
usb/ USB serial transport (CDC-ACM, V3 framing)
|
||||
wifi/ WiFi station client
|
||||
helpers/
|
||||
ui/ Shared UI plumbing (display, buzzer, multi-tap input)
|
||||
ui-button/ Single-button page UI
|
||||
ui-joystick/ 5-way joystick UI (Wio Tracker L1)
|
||||
boards/
|
||||
nrf52840/ nRF52840 board overlays and configs
|
||||
esp32/ ESP32 (classic + C3/C6/S3) board overlays and configs
|
||||
nrf54l/ nRF54L15 board overlay and config
|
||||
mg24/ EFR32MG24 board overlay and config
|
||||
stm32wl/ Seeed LoRa-E5 board overlay and config
|
||||
linux_native/ native_sim presets (Femtofox, RAK6421)
|
||||
common/ Shared Kconfig fragments and devicetree includes
|
||||
lib/ Monocypher crypto library (Ed25519/X25519)
|
||||
patches/ Auto-applied patches to the Zephyr tree
|
||||
```
|
||||
*Comic by [TWONKS](https://www.twonks.co.uk/). Borrowed with love; go read the rest.*
|
||||
|
||||
## License
|
||||
|
||||
MIT License — see [`LICENSE`](LICENSE). Same license as the
|
||||
upstream MeshCore project, which this work relies heavily on (see the
|
||||
[official meshcore repo](https://github.com/meshcore-dev/MeshCore/)).
|
||||
|
||||
A few vendored dependencies carry their own (compatible) licenses — see the
|
||||
notice at the bottom of `LICENSE` for details (Monocypher, Zephyr patches).
|
||||
|
||||
Logo by [recrof](https://github.com/recrof), licensed under the
|
||||
[WTFPL](https://en.wikipedia.org/wiki/WTFPL).
|
||||
|
||||
The comic below was stolen from https://www.twonks.co.uk/
|
||||
|
||||
[](https://www.twonks.co.uk/)
|
||||
|
||||
(FYI the whole project is 99,9% claude and cursor backed, relying heavily on the [official meshcore repo](https://github.com/meshcore-dev/MeshCore/) and the work they do in it)
|
||||
MIT, same as upstream MeshCore; see [LICENSE](LICENSE), which also lists the vendored pieces (Monocypher,
|
||||
Zephyr patches). Logo by [recrof](https://github.com/recrof), [WTFPL](https://en.wikipedia.org/wiki/WTFPL).
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Building ZephCore
|
||||
|
||||
Everything about building from source: setup, every build variant, flashing, per-platform notes and the
|
||||
Kconfig options worth knowing. The short version is in the [README](../README.md#build-it-yourself).
|
||||
|
||||
## Setup
|
||||
|
||||
**Dev container (easiest).** Open the repo in VS Code and run **Dev Containers: Reopen in Container**. It
|
||||
carries the SDK, toolchains, west and the vendor blobs, and matches CI. See
|
||||
[.devcontainer/README.md](../.devcontainer/README.md).
|
||||
|
||||
**By hand.** Install the
|
||||
[Zephyr SDK >= 1.0.1 and west](https://docs.zephyrproject.org/latest/develop/getting_started/index.html)
|
||||
(the SDK version matters: Zephyr is pinned to a `main` commit in `zephcore/west.yml`, see
|
||||
[ADR 0005](adr/0005-zephyr-main-pin.md)). Then, in the cloned folder:
|
||||
|
||||
```bash
|
||||
west init -l zephcore
|
||||
west update
|
||||
```
|
||||
|
||||
Optional: [adafruit-nrfutil](https://github.com/adafruit/Adafruit_nRF52_nrfutil) on the path makes nRF52
|
||||
builds also produce the DFU `.zip`. ESP32 and MG24 need their vendor blobs once:
|
||||
|
||||
```bash
|
||||
west blobs fetch hal_espressif
|
||||
west blobs fetch hal_silabs
|
||||
```
|
||||
|
||||
## Build variants
|
||||
|
||||
All builds have the same shape; the role and the extras are config fragments from
|
||||
`zephcore/boards/common/`, passed in `EXTRA_CONF_FILE` and separated with `;`.
|
||||
|
||||
```bash
|
||||
west build -b <board> zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/<fragment>.conf"
|
||||
```
|
||||
|
||||
| Fragment | Gives you | Notes |
|
||||
|---|---|---|
|
||||
| *(none)* | Companion | Production build: no logging, no asserts, reboot on fatal error |
|
||||
| `repeater.conf` | Repeater | On ESP32-S3/C-series this pulls in `wifi_ota.conf`, so add `--sysbuild` |
|
||||
| `room_server.conf` | Room server | |
|
||||
| `observer.conf` | Observer | ESP32 only (WiFi + MQTT) |
|
||||
| `debug.conf` | Logging and asserts | Combine with any role |
|
||||
| `packet_logging.conf` | One RAW/RX/TX line per packet | Logging subsystem stays off, so the console carries only these lines and the CLI |
|
||||
| `repeater_uplink.conf` | Repeater WiFi + MQTT uplink | ESP32, with `repeater.conf` |
|
||||
| `wifi_ota.conf` | WiFi AP + HTTP firmware update | ESP32 repeaters; needs MCUboot, so `--sysbuild` |
|
||||
| `wifi_companion.conf` | App over TCP on your WiFi | Added automatically for boards whose manifest declares `wifi: true` |
|
||||
|
||||
Board strings are in [supported_boards.md](supported_boards.md). Examples:
|
||||
|
||||
```bash
|
||||
# Companion
|
||||
west build -b wio_tracker_l1 zephcore --pristine
|
||||
|
||||
# Companion with debug logging
|
||||
west build -b wio_tracker_l1 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/debug.conf"
|
||||
|
||||
# Repeater
|
||||
west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
|
||||
|
||||
# Repeater with packet logging
|
||||
west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/packet_logging.conf"
|
||||
|
||||
# ESP32-S3 repeater (WiFi OTA comes with it, hence sysbuild)
|
||||
west build -b xiao_esp32s3/esp32s3/procpu zephcore --pristine --sysbuild -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
|
||||
|
||||
# Room server
|
||||
west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/room_server.conf"
|
||||
|
||||
# Observer
|
||||
west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/observer.conf"
|
||||
|
||||
# BLE adapter at debug level (debug.conf turns logging on, the flag raises BLE to DBG)
|
||||
west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/debug.conf" -DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y
|
||||
|
||||
# Formatter (factory-reset utility)
|
||||
west build -b wio_tracker_l1 zephcore/tools/formatter --pristine
|
||||
```
|
||||
|
||||
**Always use `--pristine` when you switch board or role.** Outputs land in `build/zephyr/`: `.uf2`, `.hex`,
|
||||
`.bin` and the DFU `.zip`, as the platform allows.
|
||||
|
||||
The release matrix is `build.sh` (`./build.sh nrf`, `./build.sh esp32 companions`, ...). Which boards it
|
||||
builds comes from the board manifests (`zephcore/boards/**/zephcore.yml`), and it adds no `-D` flags of its
|
||||
own, so a local build of the same board and role is the same firmware.
|
||||
|
||||
## Flashing and platform notes
|
||||
|
||||
| Platform | Flash | Worth knowing |
|
||||
|---|---|---|
|
||||
| nRF52840 | Copy `zephyr.uf2` to the UF2 drive, or `west flash` | Keeps the Adafruit bootloader and SoftDevice layout of stock MeshCore |
|
||||
| ESP32 companion | `west flash` (image at 0x0) | Simple boot, no MCUboot ([ADR 0001](adr/0001-esp32-boot-and-flash-layout.md)) |
|
||||
| ESP32-S3 companion | esptool on the ROM's USB-Serial-JTAG port | Native USB: esptool auto-reset does not work, enter the bootloader with `start dfu` or a 1200-baud touch ([ADR 0002](adr/0002-esp32s3-native-usb-companion.md)) |
|
||||
| ESP32 repeater (S3, C-series) | `west flash` after a `--sysbuild` build | MCUboot image; without `--sysbuild` the board silently stops enumerating USB |
|
||||
| nRF54L | `zephyr.hex` over SWD | Build with `--no-sysbuild`; no USB peripheral, so no UF2 |
|
||||
| EFR32MG24 | `west flash --runner pyocd` | Needs the `hal_silabs` blobs |
|
||||
| STM32WL | `west flash` over SWD / ST-Link | No BLE or USB device: companion protocol and CLI run on USART1 |
|
||||
| Native Linux | Copy the binary to the SBC | See [LINUX_NATIVE.md](LINUX_NATIVE.md) |
|
||||
|
||||
Released ESP32 images are always MCUboot `-merged.bin`, written at offset 0. Heltec V3 and the classic
|
||||
ESP32 boards have their console and CLI on `uart0` (the USB-UART port).
|
||||
|
||||
## How the configuration is put together
|
||||
|
||||
`zephcore/CMakeLists.txt` derives the platform from the board string and layers the Kconfig fragments; a
|
||||
later layer overrides an earlier one:
|
||||
|
||||
```
|
||||
prj.conf → boards/common/zephcore_common.conf → boards/common/<platform>_common.conf
|
||||
→ boards/common/esp32c3s3_common.conf (S3/C3 only) → boards/<platform>/<board>/board.conf
|
||||
→ your EXTRA_CONF_FILE
|
||||
```
|
||||
|
||||
Devicetree follows the same rule: `board.overlay` first, overlays paired with a conf fragment after it,
|
||||
`partitions.overlay` last ([ADR 0003](adr/0003-devicetree-overlay-precedence.md)). Most hardware features
|
||||
(display, buttons, buzzer, GPS, PSRAM) are detected from devicetree and need no Kconfig. Zephyr patches
|
||||
from `zephcore/patches/` are applied at configure time and abort the build if they do not apply.
|
||||
|
||||
## Kconfig options worth knowing
|
||||
|
||||
Set them in a board conf or with `-DCONFIG_...=`. The full list with help texts is `zephcore/Kconfig`.
|
||||
|
||||
| Option | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `ZEPHCORE_ROLE_*` | `COMPANION` | Role choice: `COMPANION`, `REPEATER`, `ROOM_SERVER`, `OBSERVER`. Normally set by the role fragment |
|
||||
| `ZEPHCORE_RADIO_*` | `NATIVE` | Radio choice: `NATIVE` (Zephyr SX126x driver, LLCC68, STM32WL), `LR1110` (LR1110/1120/1121), `LR2021`, `SX127X`. Set by the board |
|
||||
| `ZEPHCORE_DEFAULT_TX_POWER_DBM` | 22 | Initial TX power; lower on boards with an external PA |
|
||||
| `ZEPHCORE_MAX_TX_POWER_DBM` | 22 | Hard cap, enforced by the radio adapter |
|
||||
| `ZEPHCORE_LORA_RX_DUTY_CYCLE` | n | Boot default for the RX duty cycle; the runtime setting is `set rxduty` |
|
||||
| `ZEPHCORE_MAX_CONTACTS` | 350 | Companion contact slots (boards short on RAM lower it) |
|
||||
| `ZEPHCORE_MAX_CHANNELS` | 40 | Companion channel slots |
|
||||
| `ZEPHCORE_OFFLINE_QUEUE_SIZE` | 256 | Companion offline message queue |
|
||||
| `ZEPHCORE_BLE_PASSKEY` | 123456 | BLE pairing PIN |
|
||||
| `ZEPHCORE_GPS_POLL_INTERVAL_SEC` | 300 | Companion GPS interval between fixes (10 to 86400); the runtime setting is `set gps duty` |
|
||||
| `ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC` | 300 | Window for the very first fix after a cold start |
|
||||
| `ZEPHCORE_REPEATER_GPS_INTERVAL_SEC` | 172800 | Repeater and room server GPS interval (48 h); 0 = always on |
|
||||
| `ZEPHCORE_PACKET_LOGGING` | n | Upstream-format packet log lines (`packet_logging.conf`) |
|
||||
| `ZEPHCORE_WIFI_OTA` | n | WiFi AP + HTTP firmware update (ESP32 repeaters) |
|
||||
| `ZEPHCORE_REPEATER_UPLINK` | n | Repeater WiFi + MQTT uplink (ESP32) |
|
||||
| `ZEPHCORE_HOUSEKEEPING_INTERVAL_MS` | 5000 | Companion housekeeping tick |
|
||||
|
||||
## Tests
|
||||
|
||||
Host regression tests live in `tests/` and run in CI under ASan and UBSan. Locally (Linux or WSL):
|
||||
|
||||
```bash
|
||||
python3 tests/run.py run --profile quick
|
||||
```
|
||||
@@ -33,7 +33,8 @@ Readers:
|
||||
two-space-indented ` <capability>: true|false` form.
|
||||
- build.sh: board_manifest.py matrix <nrf|nrf54l|mg24|stm32wl|esp32|linux> [companions|repeaters]
|
||||
- gen_provider_catalog.py imports load_boards().
|
||||
- docs: board_manifest.py docs (rewrites the generated lists in docs/supported_boards.md)
|
||||
- docs: board_manifest.py docs (rewrites the generated lists in docs/supported_boards.md
|
||||
and the board-count table in README.md)
|
||||
|
||||
`board_manifest.py check` validates every manifest; CI should run it.
|
||||
"""
|
||||
@@ -196,9 +197,25 @@ DOCS_GROUPS = [("nrf52", "nRF52840"), ("esp32", "ESP32"), ("stm32wl", "STM32WL")
|
||||
("mg24", "MG24"), ("nrf54l", "nRF54L")]
|
||||
|
||||
|
||||
SUMMARY_GROUPS = [("nrf52", "nRF52840"), ("esp32", "ESP32 family"), ("nrf54l", "nRF54L"),
|
||||
("mg24", "EFR32MG24"), ("stm32wl", "STM32WL"), ("linux", "Native Linux (presets)")]
|
||||
|
||||
|
||||
def summary(boards):
|
||||
"""The '<!-- boards:summary -->' table: boards per platform, and how many are published."""
|
||||
rows = ["| Platform | Boards | With published firmware |", "|---|---|---|"]
|
||||
for platform, label in SUMMARY_GROUPS:
|
||||
group = [b for b in boards if b.platform == platform]
|
||||
rows.append("| %s | %d | %d |" % (label, len(group),
|
||||
sum(b.release is not None for b in group)))
|
||||
return "<!-- boards:summary -->\n%s\n<!-- /boards -->" % "\n".join(rows)
|
||||
|
||||
|
||||
def docs(boards, path):
|
||||
"""Rewrite each '<!-- boards:<platform> -->' ... '<!-- /boards -->' block."""
|
||||
text = open(path, encoding="utf-8").read()
|
||||
text = re.sub(r"<!-- boards:summary -->.*?<!-- /boards -->", lambda _m: summary(boards),
|
||||
text, flags=re.S)
|
||||
for platform, _ in DOCS_GROUPS:
|
||||
group = [b for b in boards if b.platform == platform]
|
||||
width = max((len(b.target) for b in group), default=0)
|
||||
@@ -225,7 +242,10 @@ def main(argv):
|
||||
print(row)
|
||||
return 0
|
||||
if cmd == "docs":
|
||||
docs(boards, argv[2] if len(argv) > 2 else os.path.join(APP_DIR, "..", "docs", "supported_boards.md"))
|
||||
root = os.path.join(APP_DIR, "..")
|
||||
for path in argv[2:] or [os.path.join(root, "docs", "supported_boards.md"),
|
||||
os.path.join(root, "README.md")]:
|
||||
docs(boards, path)
|
||||
return 0
|
||||
print(__doc__)
|
||||
return 2
|
||||
|
||||
Reference in New Issue
Block a user