build: board manifests, shared board resolver, manifest-driven release matrix

- boards/**/zephcore.yml: one manifest per board/preset (release roles,
  variants, catalog metadata, capabilities); scripts/board_manifest.py
  validates and serves them to build.sh, gen_provider_catalog.py and
  docs/supported_boards.md
- cmake/zephcore_board.cmake: board dir -> platform, shared by app and
  sysbuild; removes the board-name regex lists and 7 separate globs
- conf chain: exact-name role/debug classification checked against
  Kconfig, idempotent across non-pristine reconfigures, version string
  single-sourced (BT DIS rev generated), day-granular build epoch
- Doom easter egg: Kconfig default for the wio_tracker_l1 companion
  instead of a build.sh-only flag
- catalog: maker for the LR2021 EVK (KeyError before); CI installs PyYAML
- user -DEXTRA_DTC_OVERLAY_FILE overlays now take precedence over derived overlays

Verified: identical .config, DTS and source lists for 97 board/role
configs; identical images for 4 reference builds; identical build.sh
command/artifact set and catalog content.
This commit is contained in:
liquidraver
2026-09-23 10:24:40 +02:00
parent a87fa81e07
commit 2e2d852f29
63 changed files with 1290 additions and 1063 deletions
+3
View File
@@ -526,6 +526,9 @@ jobs:
with:
python-version: '${{ env.python_version }}'
# gen_provider_catalog.py reads the board manifests (PyYAML).
- run: pip install -r requirements.txt
# Pull every board's firmware back into a single firmware/ directory. This
# branch is force-pushed, so an incomplete set here silently DELETES boards
# from the live configurator feed — which is exactly why this handoff must
+109 -363
View File
@@ -1,127 +1,123 @@
#!/usr/bin/env bash
#
# Release matrix.
#
# ./build.sh nrf | nrf54l | mg24 | stm32wl | linux
# ./build.sh esp32 companions | repeaters
#
# WHICH boards are built, in which roles and variants, lives in the board
# manifests (zephcore/boards/<platform>/<board>/zephcore.yml and
# zephcore/boards/linux_native/<preset>.zephcore.yml; schema and validator in
# zephcore/scripts/board_manifest.py). This script only knows how each platform
# packages its images. A board without a `release:` section is not published.
#
# Everything a release build puts in the firmware comes from the build config,
# never from extra -D flags here, so a local build of the same board + role is
# the same firmware. Release-only differences are layout and packaging.
set -euo pipefail
mkdir -p firmware
COMMIT_HASH=$(git rev-parse --short HEAD)
GROUP="${1:?usage: build.sh <nrf|nrf54l|mg24|stm32wl|linux> | build.sh esp32 <companions|repeaters>}"
ROLE_FILTER="${2:-}"
if [[ $GROUP == "esp32" && -z $ROLE_FILTER ]]; then
echo "build.sh esp32 needs 'companions' or 'repeaters'" >&2
exit 1
fi
nRF_boards=(
rak4631
rak3401_1watt
wio_tracker_l1
wio_tracker_l1_1w
t1000_e
meshtracker_x1
thinknode_m1
thinknode_m3
thinknode_m6
rak_wismesh_tag
ikoka_nano_30dbm
sensecap_solar
xiao_nrf52840
lilygo_techo
lilygo_timpulse_plus
promicro_sx1262
heltec_t114
heltec_t096
heltec_t1
gat562_30s
muziworks_r1neo
)
# Move the finished image(s) of one build into firmware/ under the published name.
package() {
local platform=$1 target=$2 stem=$3 tag=$4
local out="firmware/$stem-$tag-$COMMIT_HASH"
case $platform in
nrf)
# UF2 for drag-and-drop, DFU .zip for the configurator.
mv build/zephyr/zephyr.uf2 "$out.uf2"
mv build/zephyr/zephyr.zip "$out.zip"
;;
nrf54l | mg24 | stm32wl)
# No USB bootloader on these SoCs (nRF54L15, EFR32MG24 and STM32WL
# have no USB device peripheral): zephyr.hex links at the flash origin,
# IS the whole image, and is flashed over SWD. Download-only in the
# Mesh America catalog.
mv build/zephyr/zephyr.hex "$out.hex"
;;
linux)
# native_sim emits zephcore_native_linux.exe; ship it extension-less.
mv build/zephyr/zephcore_native_linux.exe "$out"
;;
esp32)
[[ $target =~ (esp32[^/]*) ]] || { echo "Unknown chip for: $target" >&2; exit 1; }
local chip=${BASH_REMATCH[1]}
if [[ $chip == "esp32" ]]; then
# Classic ESP32 (T-Beam, PICO-D4): simple boot for both roles. The
# companion's BLE controller leaves no DRAM for MCUboot and the
# repeater is CLI-only (WiFi OTA overflows DRAM by ~10 KB), so
# zephyr.bin is the complete bootable image at the 0x1000 ROM
# bootloader offset; also wrapped as a full-flash merged image.
python -m esptool --chip "$chip" merge-bin \
--output "$out-merged.bin" \
--flash-mode dio --flash-freq 40m --flash-size 4MB \
0x1000 build/zephyr/zephyr.bin
cp build/zephyr/zephyr.bin "$out.bin"
else
# S3/C-series: sysbuild + MCUboot. Only the merged image (MCUboot @
# 0x0 + signed app @ 0x10000) is bootable on a bare chip. The signed
# app alone is the app-only update payload (configurator
# "flash-update" at 0x10000, WiFi-OTA upload) and is NOT bootable
# standalone -- never publish it as a plain .bin (bricked boards when
# flashed like classic-ESP32's image, GH #42).
local flash_size
flash_size=$(python3 zephcore/scripts/dts_flash_size.py \
"${ZEPHYR_DTS:-build/zephcore/zephyr/zephyr.dts}")
python -m esptool --chip "$chip" merge-bin \
--output "$out-merged.bin" \
--flash-mode dio --flash-freq 40m --flash-size "$flash_size" \
0x00000 build/mcuboot/zephyr/zephyr.bin \
0x10000 build/zephcore/zephyr/zephyr.signed.bin
cp build/zephcore/zephyr/zephyr.signed.bin "$out-update.bin"
fi
;;
esac
}
# Native-Linux presets (not Zephyr boards — built with -b native_sim plus an
# EXTRA_CONF_FILE). Each targets a real SBC arch, so it is cross-compiled.
Linux_boards=(
femtofox
rak6421
rak6421_pi5
)
# One manifest row: target|role|variant|confs|stem|sysbuild|host|cross_compile
build_row() {
local target=$1 role=$2 variant=$3 confs=$4 stem=$5 sysbuild=$6 host=$7 cross=$8
local tag="$role${variant:+-$variant}"
local cmd=(west build -b "$target" zephcore --pristine)
[[ -n $sysbuild ]] && cmd+=("--$sysbuild")
local extra=()
if [[ -n $host ]]; then
# Native-Linux presets are cross-compiled for their SBC's arch.
extra+=(-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile
-DNATIVE_TARGET_HOST="$host" -DCROSS_COMPILE="$cross")
fi
[[ -n $confs ]] && extra+=(-DEXTRA_CONF_FILE="$confs")
((${#extra[@]})) && cmd+=(-- "${extra[@]}")
# nRF54L15 boards. No bootloader exists for this SoC (it has no USB
# peripheral at all), so these build with --no-sysbuild and the app links at
# RRAM base 0x0 -- zephyr.hex IS the whole image. Flashing is SWD only, which
# is why they publish a .hex and no .uf2/.zip, and why they are download-only
# in the Mesh America catalog.
nRF54L_boards=(
me25ls02/nrf54l15/cpuapp
xiao_nrf54l15/nrf54l15/cpuapp
seeed_lr2021_evk/nrf54l15/cpuapp
)
echo "Now building $target $tag"
"${cmd[@]}"
package "$GROUP" "$target" "$stem" "$tag"
}
# SWD-only ARM platforms. Neither SoC has a USB device peripheral, so there is
# no bootloader, no UF2 and no DFU path -- zephyr.hex links at the flash origin
# and IS the whole image, written with an external probe. Same story as the
# nRF54L boards above, which is why they publish a .hex and nothing else and are
# download-only in the Mesh America catalog.
MG24_boards=(
xiao_mg24
)
python3 zephcore/scripts/board_manifest.py check
mapfile -t ROWS < <(python3 zephcore/scripts/board_manifest.py matrix "$GROUP" $ROLE_FILTER)
for row in "${ROWS[@]}"; do
row=${row%$'\r'} # Python on Windows prints CRLF
IFS='|' read -r target role variant confs stem sysbuild host cross <<<"$row"
build_row "$target" "$role" "$variant" "$confs" "$stem" "$sysbuild" "$host" "$cross"
done
STM32WL_boards=(
lora_e5_mini
)
ESP32_boards=(
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_wifi_lora32_v4_r8/esp32s3/procpu
heltec_wireless_tracker/esp32s3/procpu
heltec_wireless_tracker_v2/esp32s3/procpu
meshnology_w12/esp32s3/procpu
ttgo_tbeam/esp32/procpu
)
if [[ $1 == "nrf" ]]; then
for board in "${nRF_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
# build nRF companions (production is the default — no extra conf needed)
echo "Now building $board companion"
if [[ $board == "wio_tracker_l1" ]]; then
west build -b "$board" zephcore --pristine -- -DCONFIG_ZEPHCORE_EASTER_EGG_DOOM=y
else
west build -b "$board" zephcore --pristine
fi
mv build/zephyr/zephyr.uf2 firmware/"$board"-companion-"$COMMIT_HASH".uf2
mv build/zephyr/zephyr.zip firmware/"$board"-companion-"$COMMIT_HASH".zip
# build nRF repeaters
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.uf2 firmware/"$board"-repeater-"$COMMIT_HASH".uf2
mv build/zephyr/zephyr.zip firmware/"$board"-repeater-"$COMMIT_HASH".zip
# Heltec T114 is sold both with and without the TFT module.
# Build dedicated screenless variants matching upstream's
# Heltec_t114_without_display_* PIO envs.
if [[ $board == "heltec_t114" ]]; then
echo "Now building $board companion (noscreen)"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/nrf52840/heltec_t114/no_display.conf"
mv build/zephyr/zephyr.uf2 firmware/"$board"-companion-noscreen-"$COMMIT_HASH".uf2
mv build/zephyr/zephyr.zip firmware/"$board"-companion-noscreen-"$COMMIT_HASH".zip
echo "Now building $board repeater (noscreen)"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/nrf52840/heltec_t114/no_display.conf"
mv build/zephyr/zephyr.uf2 firmware/"$board"-repeater-noscreen-"$COMMIT_HASH".uf2
mv build/zephyr/zephyr.zip firmware/"$board"-repeater-noscreen-"$COMMIT_HASH".zip
fi
done
# ZephCore's storage formatter — published as the `erase` package for the
if [[ $GROUP == "nrf" ]]; then
# ZephCore's storage formatter, published as the `erase` package for the
# Mesh America configurator (spec §4a). MeshCore's official erase targets a
# different flash layout and only partially wipes a ZephCore node, so each
# nRF52 board points `erase` at the formatter for its SoftDevice (v6/v7 have
# different partition maps; see SOFTDEVICE in gen_provider_catalog.py).
# Copied under stable, un-hashed names so the catalog's erase URLs stay
# stable. The .zip drives the configurator's automated DFU erase flow; the
# .uf2 is the manual drag-and-drop fallback. Skipped if not built yet.
# different partition maps). Copied under stable, un-hashed names so the
# catalog's erase URLs stay stable. The .zip drives the configurator's
# automated DFU erase flow; the .uf2 is the manual drag-and-drop fallback.
for sd in 6 7; do
for ext in zip uf2; do
f="formatter/SoftDevice_v${sd}_formatter.${ext}"
@@ -134,261 +130,11 @@ if [[ $1 == "nrf" ]]; then
done
done
# Device art we ship ourselves (see `own_img` in gen_provider_catalog.py).
# Published alongside the firmware so the catalog resolves it against the
# same --url-base as everything else, rather than hardcoding a host.
# Device art we ship ourselves (`own_img` in the manifests). Published
# alongside the firmware so the catalog resolves it against the same
# --url-base as everything else, rather than hardcoding a host.
if compgen -G "img/*" > /dev/null; then
cp img/* firmware/
echo "Published device art: $(ls img/ | tr '\n' ' ')"
fi
fi
if [[ $1 == "nrf54l" ]]; then
for board in "${nRF54L_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
echo "Now building $board companion"
west build -b "$board" zephcore --pristine --no-sysbuild
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH".hex
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine --no-sysbuild -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH".hex
done
fi
# Silicon Labs EFR32MG24. Needs the Silabs BLE controller blob (west blobs fetch
# hal_silabs) before the first build -- CI fetches it in the build-swd job.
# Flashed over SWD with pyocd/J-Link; the XIAO's USB-C is a debug bridge, not a
# device port, so there is no browser-flashable path.
if [[ $1 == "mg24" ]]; then
for board in "${MG24_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
echo "Now building $board companion"
west build -b "$board" zephcore --pristine
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH".hex
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH".hex
done
fi
# STM32WL. No Bluetooth and no USB device: the companion speaks MeshCore serial
# framing over USART1 (bridged to USB-C by the onboard USB-UART chip) and the
# repeater uses the same UART for its CLI. Flashed over SWD/ST-Link.
if [[ $1 == "stm32wl" ]]; then
for board in "${STM32WL_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
echo "Now building $board companion"
west build -b "$board" zephcore --pristine
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH".hex
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH".hex
done
fi
if [[ $1 == "linux" ]]; then
for board in "${Linux_boards[@]}"; do
# Pick the native_sim variant + cross toolchain for the target SBC arch.
case "$board" in
femtofox)
# Luckfox Pico Mini — ARMv7-A (32-bit) → native_sim (32-bit)
zboard="native_sim"
host="arm"
cross="/usr/bin/arm-linux-gnueabihf-"
;;
rak6421|rak6421_pi5)
# Raspberry Pi — aarch64 (64-bit) → native_sim/native/64
zboard="native_sim/native/64"
host="aarch64"
cross="/usr/bin/aarch64-linux-gnu-"
;;
*)
echo "Unknown linux board: $board"
exit 1
;;
esac
# build native-Linux companion (TCP transport — the default role)
echo "Now building $board companion (native linux)"
west build -b "$zboard" zephcore --pristine -- \
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
-DNATIVE_TARGET_HOST="$host" \
-DCROSS_COMPILE="$cross" \
-DEXTRA_CONF_FILE="boards/linux_native/$board.conf"
# native_sim emits zephcore_native_linux.exe; ship it as an extension-less
# per-board name (zephcore_linux_<board>-<role>-<hash>).
mv build/zephyr/zephcore_native_linux.exe firmware/zephcore_linux_"$board"-companion-"$COMMIT_HASH"
# build native-Linux repeater
echo "Now building $board repeater (native linux)"
west build -b "$zboard" zephcore --pristine -- \
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
-DNATIVE_TARGET_HOST="$host" \
-DCROSS_COMPILE="$cross" \
-DEXTRA_CONF_FILE="boards/linux_native/$board.conf;boards/common/repeater.conf"
mv build/zephyr/zephcore_native_linux.exe firmware/zephcore_linux_"$board"-repeater-"$COMMIT_HASH"
done
fi
if [[ $1 == "esp32" ]]; then
for board in "${ESP32_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
if [[ $board =~ (esp32[^/]*) ]]; then
chip="${BASH_REMATCH[1]}"
else
echo "Unknown chip for: $board"
exit 1;
fi
# Classic ESP32 (e.g. T-Beam, PICO-D4) uses Zephyr's simple-boot path for
# both roles: a self-contained zephyr.bin flashed at the 0x1000 ROM
# bootloader offset. The companion keeps BLE (whose controller reserves
# ~50KB DRAM, leaving no room for MCUboot) and the repeater is CLI-only
# (WiFi OTA's functional driver + 64KB heap overflow DRAM by ~10KB). The
# S3/C-series have the DRAM headroom and use sysbuild + MCUboot below.
if [[ $chip == "esp32" ]]; then
if [[ $2 == "companions" ]]; then
role="companion"
echo "Now building $board companion (simple boot)"
west build -b "$board" zephcore --pristine
elif [[ $2 == "repeaters" ]]; then
role="repeater"
echo "Now building $board repeater (simple boot)"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
else
continue
fi
# Simple-boot zephyr.bin is already the complete bootable image;
# wrap it in a full-flash merged image at the 0x1000 offset.
python -m esptool --chip "$chip" merge-bin \
--output firmware/"$board_clean_for_path"-"$role"-"$COMMIT_HASH"-merged.bin \
--flash-mode dio --flash-freq 40m --flash-size 4MB \
0x1000 build/zephyr/zephyr.bin
cp build/zephyr/zephyr.bin firmware/"$board_clean_for_path"-"$role"-"$COMMIT_HASH".bin
continue
fi
if [[ $2 == "companions" ]]; then
# build ESP32 companions (production is the default)
#
# The USB CDC-ACM companion transport (boards/common/esp32s3_usb.conf)
# is NOT selected here. zephcore/CMakeLists.txt auto-includes it for
# companion builds on every S3 board whose board.overlay declares the
# USB OTG CDC-ACM node, so a plain build below already gets it — and a
# developer building the same board by hand gets identical firmware.
# Keeping the choice in one place is the point: release artifacts
# already diverge from a plain build in layout (sysbuild/MCUboot), and
# a second divergence in which transports the app speaks would make
# user bug reports unreproducible from source.
echo "Now building $board companion"
west build -b "$board" zephcore --pristine --sysbuild
FLASH_SIZE=$(
python3 -c '
import re
import sys
path = sys.argv[1] if len(sys.argv) > 1 else "build/zephcore/zephyr/zephyr.dts"
dts = open(path, encoding="utf-8", errors="replace").read()
def parse_cell(tok: str) -> int:
t = tok.strip()
return int(t, 16) if t.lower().startswith("0x") else int(t)
m = re.search(
r"flash0:\s*flash@[^{]*\{[^}]*?reg\s*=\s*<\s*(?:0x[0-9a-fA-F]+|[0-9]+)\s+"
r"((?:0x)?[0-9a-fA-F]+)\s*>",
dts,
re.DOTALL,
)
if not m:
m = re.search(
r"compatible\s*=\s*\"soc-nv-flash\"\s*;\s*[\s\S]*?"
r"reg\s*=\s*<\s*(?:0x[0-9a-fA-F]+|[0-9]+)\s+((?:0x)?[0-9a-fA-F]+)\s*>",
dts,
)
if not m:
raise SystemExit("flash reg not found in " + path)
size = parse_cell(m.group(1))
print(str(size // 1048576) + "MB")
' "${ZEPHYR_DTS:-build/zephcore/zephyr/zephyr.dts}"
)
# MCUboot/sysbuild boards: only the merged (MCUboot + signed app)
# image is bootable on a bare/existing chip at 0x0. The signed app
# alone requires MCUboot already present and must land at 0x10000 —
# never publish it as a bootable "plain .bin" (bricks boards when
# flashed like classic-ESP32's self-contained zephyr.bin, see GH #42).
python -m esptool --chip "$chip" merge-bin \
--output firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH"-merged.bin \
--flash-mode dio --flash-freq 40m --flash-size "$FLASH_SIZE" \
0x00000 build/mcuboot/zephyr/zephyr.bin \
0x10000 build/zephcore/zephyr/zephyr.signed.bin
# Signed app image (slot0 @ 0x10000). App-only update payload: used by
# the Mesh America configurator's "flash-update" (esptool writes it at
# 0x10000 over an existing MCUboot) and as a WiFi-OTA payload. NOT
# bootable standalone — never flash at 0x0 (see GH #42).
cp build/zephcore/zephyr/zephyr.signed.bin \
firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH"-update.bin
fi
if [[ $2 == "repeaters" ]]; then
# build ESP32 repeaters
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine --sysbuild -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
FLASH_SIZE=$(
python3 -c '
import re
import sys
path = sys.argv[1] if len(sys.argv) > 1 else "build/zephcore/zephyr/zephyr.dts"
dts = open(path, encoding="utf-8", errors="replace").read()
def parse_cell(tok: str) -> int:
t = tok.strip()
return int(t, 16) if t.lower().startswith("0x") else int(t)
m = re.search(
r"flash0:\s*flash@[^{]*\{[^}]*?reg\s*=\s*<\s*(?:0x[0-9a-fA-F]+|[0-9]+)\s+"
r"((?:0x)?[0-9a-fA-F]+)\s*>",
dts,
re.DOTALL,
)
if not m:
m = re.search(
r"compatible\s*=\s*\"soc-nv-flash\"\s*;\s*[\s\S]*?"
r"reg\s*=\s*<\s*(?:0x[0-9a-fA-F]+|[0-9]+)\s+((?:0x)?[0-9a-fA-F]+)\s*>",
dts,
)
if not m:
raise SystemExit("flash reg not found in " + path)
size = parse_cell(m.group(1))
print(str(size // 1048576) + "MB")
' "${ZEPHYR_DTS:-build/zephcore/zephyr/zephyr.dts}"
)
# See companion branch above — the signed app alone isn't bootable
# standalone, so only publish the merged image for these boards.
python -m esptool --chip "$chip" merge-bin \
--output firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH"-merged.bin \
--flash-mode dio --flash-freq 40m --flash-size "$FLASH_SIZE" \
0x00000 build/mcuboot/zephyr/zephyr.bin \
0x10000 build/zephcore/zephyr/zephyr.signed.bin
# Signed app image (slot0 @ 0x10000). App-only update payload: the
# Mesh America configurator's "flash-update" (esptool writes it at
# 0x10000 over an existing MCUboot) and the WiFi-OTA payload uploaded
# to slot1 via the repeater's /update page. NOT bootable standalone —
# never flash this at 0x0 (see GH #42).
cp build/zephcore/zephyr/zephyr.signed.bin \
firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH"-update.bin
fi
done
fi
+41 -27
View File
@@ -1,33 +1,37 @@
# Supported Boards
Board strings for use with `west build -b <board> zephcore`.
Board strings for use with `west build -b <board> zephcore`. The lists are generated from the board
manifests (`zephcore/boards/**/zephcore.yml`) by `python zephcore/scripts/board_manifest.py docs`;
edit the manifests, not the lists.
## nRF52840
<!-- boards:nrf52 -->
```
rak4631
rak3401_1watt
wio_tracker_l1
wio_tracker_l1_1w
t1000_e
gat562_30s
heltec_t096
heltec_t1
heltec_t114
ikoka_nano_30dbm
lilygo_techo
lilygo_timpulse_plus
meshtracker_x1
muziworks_r1neo
promicro_lr2021 # source-only, no published firmware
promicro_sx1262
rak3401_1watt
rak4631
rak_wismesh_tag
sensecap_solar
t1000_e
thinknode_m1
thinknode_m3
thinknode_m6
rak_wismesh_tag
ikoka_nano_30dbm
sensecap_solar
wio_tracker_l1
wio_tracker_l1_1w
xiao_nrf52840
lilygo_techo
promicro_sx1262
promicro_lr2021
heltec_t114
heltec_t096
heltec_t1
gat562_30s
muziworks_r1neo
lilygo_timpulse_plus
```
<!-- /boards -->
> **RAK WisMesh Pocket** (WisBlock pocket): use `-b rak4631` — same board string and firmware as **RAK4631**.
>
@@ -45,23 +49,26 @@ lilygo_timpulse_plus
## ESP32
<!-- boards: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_wifi_lora32_v4_r8/esp32s3/procpu
heltec_wireless_tracker/esp32s3/procpu
heltec_wireless_tracker_v2/esp32s3/procpu
thinknode_m9/esp32s3/procpu
lilygo_t3s3/esp32s3/procpu
lilygo_tlora_c6/esp32c6/hpcore
meshnology_w12/esp32s3/procpu
station_g2/esp32s3/procpu
thinknode_m9/esp32s3/procpu # source-only, no published firmware
ttgo_lora32/esp32/procpu # source-only, no published firmware
ttgo_tbeam/esp32/procpu
ttgo_lora32/esp32/procpu # source-only, no published firmware
xiao_esp32c3
xiao_esp32c6/esp32c6/hpcore
xiao_esp32s3/esp32s3/procpu
```
<!-- /boards -->
> ESP32 boards require `west blobs fetch hal_espressif` before first build.
>
@@ -124,9 +131,11 @@ ttgo_lora32/esp32/procpu # source-only, no published firmware
## STM32WL
<!-- boards:stm32wl -->
```
lora_e5_mini
```
<!-- /boards -->
> **Seeed LoRa-E5 mini** (`lora_e5_mini`): STM32WLE5JC with the integrated
> SX1262-class sub-GHz radio. No BLE and no USB device — the companion
@@ -135,18 +144,23 @@ lora_e5_mini
## MG24 (Silicon Labs)
<!-- boards:mg24 -->
```
xiao_mg24
```
<!-- /boards -->
> Requires `west blobs fetch hal_silabs` and pyocd.
## nRF54L
<!-- boards:nrf54l -->
```
xiao_nrf54l15/nrf54l15/cpuapp
me25ls02/nrf54l15/cpuapp
seeed_lr2021_evk/nrf54l15/cpuapp
xiao_nrf54l15/nrf54l15/cpuapp
```
<!-- /boards -->
> Requires `--no-sysbuild` flag: `west build -b xiao_nrf54l15/nrf54l15/cpuapp zephcore --no-sysbuild`
>
+48 -156
View File
@@ -58,51 +58,6 @@ MAKERS = {
"meshnology": "Meshnology",
}
# Manufacturer per device name.
MAKER_BY_DEVICE = {
"RAK WisBlock / WisMesh (RAK 4631)": "rak",
"RAK WisMesh 1W Booster (3401 + 13302)": "rak",
"RAK WisMesh Tag": "rak",
"Seeed Studio Wio Tracker L1 Pro": "seeed",
"Seeed Studio SenseCAP T1000-E": "seeed",
"Seeed Studio SenseCAP Solar": "seeed",
"Seeed Studio SenseCAP MeshTracker X1": "seeed",
"Seeed Studio Xiao nRF52 WIO": "seeed",
"Seeed Studio Xiao C3": "seeed",
"Seeed Studio Xiao S3 WIO": "seeed",
"Seeed Studio Xiao ESP32-C6": "seeed",
"Elecrow ThinkNode M1": "elecrow",
"Elecrow ThinkNode M3": "elecrow",
"Elecrow ThinkNode M6": "elecrow",
"Elecrow ThinkNode M9": "elecrow",
"Ikoka Nano": "Ikoka",
"LilyGo T-Echo": "lilygo",
"LilyGo T-Beam (SX1262)": "lilygo",
"LilyGo T-Impulse Plus": "lilygo",
"LilyGo T-Lora C6": "lilygo",
"LilyGo T3 S3 (SX126x)": "lilygo",
"ProMicro nrf52 (faketec)": "promicro",
"Heltec T114": "heltec",
"Heltec Mesh Node T096": "heltec",
"Heltec Mesh Node T1": "heltec",
"Heltec v3": "heltec",
"Heltec v4": "heltec",
"Heltec v4.3": "heltec",
"Heltec Wireless Tracker": "heltec",
"Heltec Wireless Tracker v2": "heltec",
"GAT-IoT GAT562 30s": "gat-iot",
"UnitEng Station G2": "uniteng",
"Femtofox (Luckfox Pico Mini)": "femtofox",
"RAK6421 WisMesh (Raspberry Pi)": "rak",
"RAK6421 WisMesh (Raspberry Pi 5)": "rak",
"muzi works R1 Neo": "muziworks",
"MinewSemi ME25LS02": "minewsemi",
"Seeed Studio Xiao nRF54L15": "seeed",
"Seeed Studio Xiao MG24": "seeed",
"Seeed Studio LoRa-E5 mini": "seeed",
"Meshnology W12": "meshnology",
}
DESCRIPTION = (
"ZephCore is an independent, ground-up implementation of the MeshCore "
"protocol on the Zephyr RTOS. It is wire-compatible with stock MeshCore "
@@ -140,124 +95,69 @@ CUSTOM_ROLES = {
}
# ---------------------------------------------------------------------------
# Board -> device mapping
# Board -> device mapping, from the board manifests
#
# `stem` : the exact filename stem build.sh emits (board_clean_for_path).
# Every published board has a `catalog:` section in its manifest
# (zephcore/boards/<platform>/<board>/zephcore.yml, Linux presets
# zephcore/boards/linux_native/<preset>.zephcore.yml; schema in
# zephcore/scripts/board_manifest.py). Each entry below carries:
# `stem` : the exact filename stem build.sh emits (target with / -> -).
# `kind` : nrf | esp32 | nrf54l | mg24 | stm32wl | linux -- drives device
# type, file types, extensions.
# `device` : the MeshCore-canonical device name to FOLD into, or a new tile name.
# Folding is by exact name, so a `new` tile named to MeshCore's
# convention merges by itself the day MeshCore adds that device.
# `maker` : key of MAKERS (the hardware manufacturer).
# `new` : True if this device is not in the official catalog (its own tile).
# `img` : filename in MeshCore's own image set (resolved against IMG_BASE).
# `own_img`: filename we ship from this repo's img/ dir (resolved against
# --url-base). Takes precedence over `img`.
# `variant`: optional filename infix (e.g. "noscreen") + shown as subTitle.
# `subtitle`: optional role-row subTitle to disambiguate hardware variants.
# `softdevice`: nRF52 only, 6 or 7, from the board's CONFIG_ZEPHCORE_SD_FWID.
# ---------------------------------------------------------------------------
BOARDS = [
# --- nRF52: fold into existing MeshCore devices -----------------------
dict(stem="rak4631", kind="nrf", device="RAK WisBlock / WisMesh (RAK 4631)"),
dict(stem="rak3401_1watt", kind="nrf", device="RAK WisMesh 1W Booster (3401 + 13302)"),
dict(stem="wio_tracker_l1", kind="nrf", device="Seeed Studio Wio Tracker L1 Pro"),
dict(stem="t1000_e", kind="nrf", device="Seeed Studio SenseCAP T1000-E"),
dict(stem="thinknode_m1", kind="nrf", device="Elecrow ThinkNode M1"),
dict(stem="thinknode_m3", kind="nrf", device="Elecrow ThinkNode M3"),
dict(stem="thinknode_m6", kind="nrf", device="Elecrow ThinkNode M6"),
dict(stem="rak_wismesh_tag", kind="nrf", device="RAK WisMesh Tag"),
dict(stem="ikoka_nano_30dbm", kind="nrf", device="Ikoka Nano", subtitle="30 dBm"),
dict(stem="sensecap_solar", kind="nrf", device="Seeed Studio SenseCAP Solar"),
dict(stem="xiao_nrf52840", kind="nrf", device="Seeed Studio Xiao nRF52 WIO"),
dict(stem="lilygo_techo", kind="nrf", device="LilyGo T-Echo"),
dict(stem="promicro_sx1262", kind="nrf", device="ProMicro nrf52 (faketec)"),
dict(stem="heltec_t114", kind="nrf", device="Heltec T114"),
dict(stem="heltec_t114", kind="nrf", device="Heltec T114",
variant="noscreen", subtitle="No screen"),
dict(stem="heltec_t1", kind="nrf", device="Heltec Mesh Node T1"),
dict(stem="gat562_30s", kind="nrf", device="GAT-IoT GAT562 30s"),
dict(stem="muziworks_r1neo", kind="nrf", device="muzi works R1 Neo"),
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "zephcore", "scripts"))
import board_manifest # noqa: E402
# --- nRF52: new ZephCore-only hardware (own tile) ---------------------
dict(stem="lilygo_timpulse_plus", kind="nrf", device="LilyGo T-Impulse Plus", new=True, img="lora.svg"),
dict(stem="heltec_t096", kind="nrf", device="Heltec Mesh Node T096", new=True, img="lora.svg"),
# Name is pre-matched to MeshCore's flasher convention ("Seeed Studio" +
# full product name, as with "Seeed Studio SenseCAP T1000-E"), so this
# folds into their tile automatically the moment they add one -- the merge
# is by exact name. Their flasher config has no X1 entry yet even though
# MeshCore firmware supports it, hence new=True and our own photo for now.
# WHEN IT APPEARS: drop new=True and own_img so their art and tile win.
dict(stem="meshtracker_x1", kind="nrf", device="Seeed Studio SenseCAP MeshTracker X1",
new=True, own_img="meshtracker_x1.jpg"),
KIND = {"nrf52": "nrf", "esp32": "esp32", "nrf54l": "nrf54l",
"mg24": "mg24", "stm32wl": "stm32wl", "linux": "linux"}
KIND_ORDER = ["nrf", "esp32", "nrf54l", "mg24", "stm32wl", "linux"]
SD_FWID = {0x00B6: 6, 0x0123: 7} # S140 v6.1.1 / v7.3.0
# --- ESP32 (sysbuild/MCUboot, -merged.bin): fold ---------------------
dict(stem="xiao_esp32c3", kind="esp32", device="Seeed Studio Xiao C3"),
dict(stem="xiao_esp32s3-esp32s3-procpu", kind="esp32", device="Seeed Studio Xiao S3 WIO"),
dict(stem="station_g2-esp32s3-procpu", kind="esp32", device="UnitEng Station G2"),
dict(stem="heltec_wifi_lora32_v3-esp32s3-procpu", kind="esp32", device="Heltec v3"),
dict(stem="heltec_wifi_lora32_v4-esp32s3-procpu", kind="esp32", device="Heltec v4"),
# MeshCore folds the R8 (ESP32-S3R8, 8 MB octal PSRAM) into its "Heltec v4"
# tile as "[R8] ..." role rows rather than giving it a device of its own, so
# it folds here too, told apart by the subTitle.
dict(stem="heltec_wifi_lora32_v4_r8-esp32s3-procpu", kind="esp32", device="Heltec v4",
subtitle="R8"),
dict(stem="heltec_wireless_tracker-esp32s3-procpu", kind="esp32", device="Heltec Wireless Tracker"),
dict(stem="heltec_wireless_tracker_v2-esp32s3-procpu", kind="esp32", device="Heltec Wireless Tracker v2"),
# MeshCore splits this board by radio -- "(SX126x)" and "(SX127x)" are two
# separate tiles sharing the "LilyGo T3 S3" group. We only build the SX1262
# variant, so fold into the SX126x tile; an SX127x port would be its own entry.
dict(stem="lilygo_t3s3-esp32s3-procpu", kind="esp32", device="LilyGo T3 S3 (SX126x)"),
# ThinkNode M9 pulled from the build.sh release matrix 2026-07-22 (bring-up
# still in progress — keypad/GPS/battery unverified on hardware). Restore
# this line together with the build.sh entry once it's release-ready.
# dict(stem="thinknode_m9-esp32s3-procpu", kind="esp32", device="Elecrow ThinkNode M9"),
# Classic ESP32 T-Beam: ships -merged.bin (full-flash, 0x0) too.
dict(stem="ttgo_tbeam-esp32-procpu", kind="esp32", device="LilyGo T-Beam (SX1262)"),
# --- ESP32: new ZephCore-only hardware (own tile) --------------------
dict(stem="xiao_esp32c6-esp32c6-hpcore", kind="esp32", device="Seeed Studio Xiao ESP32-C6", new=True, img="xiao_esp32c6.svg"),
dict(stem="lilygo_tlora_c6-esp32c6-hpcore", kind="esp32", device="LilyGo T-Lora C6", new=True, img="lilygo_tlora_c6.svg"),
dict(stem="heltec_wifi_lora32_v43-esp32s3-procpu", kind="esp32", device="Heltec v4.3", new=True, img="heltec_v4.svg"),
# MeshCore firmware has a meshnology_w12 variant but their flasher config has
# no W12 device and no meshnology maker, so there is nothing to fold into yet.
# The name follows their maker-plus-product convention, so it folds by itself
# if they add one. WHEN IT APPEARS: drop new=True (and own_img, if one is set).
dict(stem="meshnology_w12-esp32s3-procpu", kind="esp32", device="Meshnology W12", new=True, img="lora.svg"),
def softdevice_of(board):
text = open(os.path.join(board.board_dir, "board.conf"), encoding="utf-8").read()
m = re.search(r"^CONFIG_ZEPHCORE_SD_FWID=(0x[0-9A-Fa-f]+)", text, re.M)
if not m or int(m.group(1), 16) not in SD_FWID:
raise SystemExit(f"{board.name}: no known CONFIG_ZEPHCORE_SD_FWID in board.conf")
return SD_FWID[int(m.group(1), 16)]
# --- nRF54L15 (SWD only, download-only tile): new tile ----------------
# The nRF54L15 has no USB peripheral, so there is no bootloader and no
# WebUSB/DFU path the configurator could drive -- the Type-C port on the
# MX25LE02 carrier is a CH340x UART bridge. The tile therefore publishes
# the .hex as a plain download and the user flashes it over SWD.
dict(stem="me25ls02-nrf54l15-cpuapp", kind="nrf54l", device="MinewSemi ME25LS02",
new=True, img="lora.svg"),
# Same story, except the XIAO carries a SAMD11 CMSIS-DAP bridge, so its own
# USB cable is enough (openocd/pyocd) -- still not a browser flash flow.
dict(stem="xiao_nrf54l15-nrf54l15-cpuapp", kind="nrf54l",
device="Seeed Studio Xiao nRF54L15", new=True, img="lora.svg"),
# Same XIAO nRF54L15 host and the same CMSIS-DAP story, but a different kit:
# the Semtech LoRa Plus expansion board with a Wio-LR2021 module. Separate
# tile because the radio and the whole D0..D10 wiring differ.
dict(stem="seeed_lr2021_evk-nrf54l15-cpuapp", kind="nrf54l",
device="Semtech LR2021 LoRa Plus EVK", new=True, img="lora.svg"),
# --- SWD-only ARM boards (download-only tiles): new tiles -------------
# Same shape as the nRF54L entries: no USB device peripheral means no
# bootloader and no browser-flashable path, so the tile publishes the .hex
# and the user flashes it with an external probe. MeshCore has no MG24 or
# STM32WL support at all, so both are ZephCore-only tiles.
dict(stem="xiao_mg24", kind="mg24", device="Seeed Studio Xiao MG24",
new=True, img="lora.svg"),
# No Bluetooth on this SoC -- the companion speaks MeshCore serial framing
# over USART1, bridged to USB-C by the onboard USB-UART chip, so it is
# offered under companionUsb (a wired serial port, same as the app sees on
# any CDC-ACM board) rather than companionBle.
dict(stem="lora_e5_mini", kind="stm32wl", device="Seeed Studio LoRa-E5 mini",
new=True, img="lora.svg"),
def load_catalog_boards():
"""Catalog entries in tile order: platform group, folded before new, then name."""
out = []
for b in board_manifest.load_boards():
if b.release is None or b.catalog is None:
continue
c = b.catalog
base = dict(stem=b.stem, kind=KIND[b.platform], device=c["device"], maker=c["maker"])
for k in ("new", "img", "own_img", "subtitle"):
if c.get(k):
base[k] = c[k]
if b.platform == "nrf52":
base["softdevice"] = softdevice_of(b)
out.append(base)
for v in b.release.get("variants") or []:
e = dict(base, variant=v["id"])
e.pop("subtitle", None)
if v.get("subtitle"):
e["subtitle"] = v["subtitle"]
out.append(e)
out.sort(key=lambda e: (KIND_ORDER.index(e["kind"]), bool(e.get("new")), e["device"].lower(),
e.get("variant") or ""))
return out
# --- Native Linux (noflash, download only): new tiles ----------------
dict(stem="zephcore_linux_femtofox", kind="linux", device="Femtofox (Luckfox Pico Mini)", new=True, img="lora.svg"),
dict(stem="zephcore_linux_rak6421", kind="linux", device="RAK6421 WisMesh (Raspberry Pi)", new=True, img="rpi.svg"),
dict(stem="zephcore_linux_rak6421_pi5", kind="linux", device="RAK6421 WisMesh (Raspberry Pi 5)", new=True, img="rpi.svg"),
]
DEVICE_TYPE = {"nrf": "nrf52", "esp32": "esp32", "linux": "noflash",
"nrf54l": "noflash", "mg24": "noflash", "stm32wl": "noflash"}
@@ -267,14 +167,6 @@ DEVICE_TYPE = {"nrf": "nrf52", "esp32": "esp32", "linux": "noflash",
# own formatter tool instead. The formatter is SoftDevice-specific (partition map
# differs between SD v6 and v7), so each board maps to its SD's formatter .zip.
# Files are published to firmware-dist by build.sh (stable names, no hash).
SOFTDEVICE = {
"rak4631": 6, "rak3401_1watt": 6, "thinknode_m1": 6, "thinknode_m3": 6,
"thinknode_m6": 6, "rak_wismesh_tag": 6, "lilygo_techo": 6,
"lilygo_timpulse_plus": 6, "promicro_sx1262": 6, "heltec_t114": 6,
"heltec_t096": 6, "heltec_t1": 6, "gat562_30s": 6, "muziworks_r1neo": 6,
"wio_tracker_l1": 7, "t1000_e": 7, "ikoka_nano_30dbm": 7,
"sensecap_solar": 7, "xiao_nrf52840": 7, "meshtracker_x1": 7,
}
FORMATTER_FILE = {6: "SoftDevice_v6_formatter.zip", 7: "SoftDevice_v7_formatter.zip"}
# Companion firmware is a single image that serves both transports (and, on
@@ -370,7 +262,7 @@ def build(assets, url_base, version):
dev["firmware"].append(opt)
used_roles.add(role)
for board in BOARDS:
for board in load_catalog_boards():
name = board["device"]
subtitle = board.get("subtitle")
comp = files_for(assets, board, "companion")
@@ -381,7 +273,7 @@ def build(assets, url_base, version):
continue
if name not in devices:
maker = MAKER_BY_DEVICE[name] # KeyError if a device is unmapped
maker = board["maker"]
used_makers.add(maker)
dev = {
"maker": maker,
@@ -402,7 +294,7 @@ def build(assets, url_base, version):
# nRF52 boards get ZephCore's own SoftDevice-specific formatter as `erase`.
erase = None
if board["kind"] == "nrf":
erase = f"{base}/{FORMATTER_FILE[SOFTDEVICE[board['stem']]]}"
erase = f"{base}/{FORMATTER_FILE[board['softdevice']]}"
if comp:
for role in COMPANION_ROLES[board["kind"]]:
+3
View File
@@ -1 +1,4 @@
adafruit-nrfutil==0.5.3.post16
# Board manifests (zephcore/scripts/board_manifest.py), read by build.sh and
# gen_provider_catalog.py.
PyYAML>=6.0
+265 -473
View File
@@ -28,419 +28,244 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/zephyr_new_files.cmake)
# Custom board root: boards/<mcu>/<board>/ discovered via BOARD_ROOT
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
# ========== Auto-Pairing Helper ==========
# For each .conf file, check if a same-named .overlay exists in the same directory.
# If it does, auto-include it in EXTRA_DTC_OVERLAY_FILE. This enables variants to
# pack both Kconfig and device tree changes together (e.g., no_display.conf + no_display.overlay).
#
# Usage: zephcore_auto_pair_overlay("path/to/config.conf")
#
function(zephcore_auto_pair_overlay CONF_FILE)
if(NOT CONF_FILE)
return()
endif()
# User-supplied EXTRA_CONF_FILE entries are relative to the application
# source dir (e.g. "boards/linux_native/femtofox.conf"). CMake's EXISTS does
# not resolve relative paths against it, so resolve to absolute first — else
# the paired .overlay is silently skipped (which dropped the SX1262 node and
# broke ZEPHCORE_LORA when device wiring moved out of linux_common.overlay).
if(NOT IS_ABSOLUTE "${CONF_FILE}")
set(CONF_FILE "${CMAKE_CURRENT_SOURCE_DIR}/${CONF_FILE}")
endif()
if(NOT EXISTS "${CONF_FILE}")
return()
endif()
string(REPLACE ".conf" ".overlay" OVERLAY_FILE "${CONF_FILE}")
if(EXISTS "${OVERLAY_FILE}")
# Append once, keeping the FIRST position. The user-extras pairing loop
# below walks the whole combined EXTRA_CONF_FILE, so it re-pairs every
# conf the auto chain already paired -- board.overlay used to land in
# the list three times that way. Position is precedence in devicetree,
# so silently re-appending an overlay also moved it later and let it
# override things it was never meant to.
if(EXTRA_DTC_OVERLAY_FILE)
if(NOT "${OVERLAY_FILE}" IN_LIST EXTRA_DTC_OVERLAY_FILE)
set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${OVERLAY_FILE}" PARENT_SCOPE)
message(STATUS " [auto-pair] ${CONF_FILE} → ${OVERLAY_FILE}")
endif()
else()
set(EXTRA_DTC_OVERLAY_FILE "${OVERLAY_FILE}" PARENT_SCOPE)
message(STATUS " [auto-pair] ${CONF_FILE} → ${OVERLAY_FILE}")
endif()
# Single source of truth for the firmware version string (vMAJOR.MINOR.PATCH-zephyr).
# This exact string is what the device reports, what the BLE Device Information
# Service advertises (CONFIG_BT_DIS_FW_REV_STR, generated from it below), what the
# GitHub release is tagged/named, and what the Mesh America catalog uses as its
# version key. The release workflow reads this line directly — keep its form.
set(ZEPHCORE_FIRMWARE_VERSION "1.17.5c-zephcore")
# ========== Helpers ==========
# Append FILE to the overlay list once, keeping its FIRST position. Position is
# precedence in devicetree, so a re-append would silently move an overlay later
# and let it override things it was never meant to.
function(zephcore_add_overlay FILE)
if(EXISTS "${FILE}" AND NOT "${FILE}" IN_LIST ZEPHCORE_OVERLAYS)
set(ZEPHCORE_OVERLAYS ${ZEPHCORE_OVERLAYS} "${FILE}" PARENT_SCOPE)
endif()
endfunction()
# ========== Board Configuration Hierarchy ==========
# prj.conf → zephcore_common.conf → <platform>_common.conf → <board>/board.conf
# Append a conf to the chain as given (a missing file stays Zephyr's error to
# report), plus its same-named .overlay if one exists, so a variant can ship
# Kconfig and devicetree changes together (no_display.conf + no_display.overlay).
# The overlay lookup resolves relative paths against the app source dir: CMake's
# EXISTS does not, and a skipped pairing once dropped the SX1262 node from a
# native-Linux preset.
function(zephcore_add_conf FILE)
set(ZEPHCORE_CONFS ${ZEPHCORE_CONFS} "${FILE}" PARENT_SCOPE)
if(NOT IS_ABSOLUTE "${FILE}")
set(FILE "${CMAKE_CURRENT_SOURCE_DIR}/${FILE}")
endif()
string(REGEX REPLACE "\\.conf$" ".overlay" _ov "${FILE}")
if(EXISTS "${_ov}" AND NOT "${_ov}" IN_LIST ZEPHCORE_OVERLAYS)
set(ZEPHCORE_OVERLAYS ${ZEPHCORE_OVERLAYS} "${_ov}" PARENT_SCOPE)
message(STATUS " [auto-pair] ${FILE} → ${_ov}")
endif()
endfunction()
# Recover BOARD and EXTRA_CONF_FILE from sysbuild cache (needed before find_package)
# ========== User input ==========
# The confs/overlays the user asked for, kept apart from what we derive so that
# a reconfigure without --pristine starts from the same input instead of from
# our own previous output (which used to get the auto chain prepended again).
# A value different from the one we last wrote is new user input.
foreach(_kind CONF_FILE DTC_OVERLAY_FILE)
if(NOT DEFINED ZEPHCORE_WRITTEN_EXTRA_${_kind}
OR NOT "${EXTRA_${_kind}}" STREQUAL "${ZEPHCORE_WRITTEN_EXTRA_${_kind}}")
set(ZEPHCORE_USER_EXTRA_${_kind} "${EXTRA_${_kind}}" CACHE INTERNAL "")
endif()
endforeach()
set(ZEPHCORE_USER_CONFS "${ZEPHCORE_USER_EXTRA_CONF_FILE}")
# Under sysbuild the user's EXTRA_CONF_FILE goes to sysbuild, not the app, and
# BOARD is not set yet: recover both from the sysbuild cache.
if(NOT BOARD AND DEFINED SYSBUILD_CACHE AND EXISTS "${SYSBUILD_CACHE}")
file(STRINGS "${SYSBUILD_CACHE}" _sysbuild_strings ENCODING UTF-8)
foreach(_str ${_sysbuild_strings})
if(_str MATCHES "^BOARD:STRING=(.+)")
set(BOARD "${CMAKE_MATCH_1}")
endif()
# User-specified EXTRA_CONF_FILE (e.g., repeater.conf) is passed to sysbuild,
# not the app. Recover it so our auto-include logic can detect it.
if(_str MATCHES "^EXTRA_CONF_FILE:UNINITIALIZED=(.+)")
set(_SYSBUILD_USER_CONF "${CMAKE_MATCH_1}")
list(APPEND ZEPHCORE_USER_CONFS "${CMAKE_MATCH_1}")
endif()
endforeach()
if(BOARD)
message(STATUS "Sysbuild: BOARD=${BOARD}")
endif()
if(_SYSBUILD_USER_CONF)
# Merge user extras into EXTRA_CONF_FILE so downstream checks see them
if(EXTRA_CONF_FILE)
set(EXTRA_CONF_FILE "${EXTRA_CONF_FILE};${_SYSBUILD_USER_CONF}")
else()
set(EXTRA_CONF_FILE "${_SYSBUILD_USER_CONF}")
endif()
message(STATUS "Sysbuild: user EXTRA_CONF_FILE=${_SYSBUILD_USER_CONF}")
endif()
message(STATUS "Sysbuild: BOARD=${BOARD} user confs=${ZEPHCORE_USER_CONFS}")
endif()
# Build common config file list based on detected platform
set(ZEPHCORE_COMMON_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/zephcore_common.conf")
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/zephcore_board.cmake)
# Detect platform from board name and add platform-specific common config
# For boards with qualifiers like "wio_tracker_l1/nrf52840", check both the full BOARD
# string and the BOARD_QUALIFIERS which contains just the qualifier (e.g., "nrf52840")
if(BOARD MATCHES ".*nrf52.*" OR BOARD MATCHES "rak4631" OR BOARD MATCHES "rak_wismesh_tag" OR BOARD MATCHES "rak3401_1watt" OR BOARD MATCHES "wio_tracker" OR BOARD MATCHES "ikoka_nano" OR BOARD MATCHES "t1000_e" OR BOARD MATCHES "meshtracker_x1" OR BOARD MATCHES "thinknode_m1" OR BOARD MATCHES "thinknode_m3" OR BOARD MATCHES "thinknode_m6" OR BOARD MATCHES "promicro_lr2021" OR BOARD MATCHES "promicro_sx1262" OR BOARD MATCHES "sensecap_solar" OR BOARD MATCHES "xiao_nrf52840" OR BOARD MATCHES "lilygo_techo" OR BOARD MATCHES "lilygo_timpulse_plus" OR BOARD MATCHES "heltec_t114" OR BOARD MATCHES "heltec_t096" OR BOARD MATCHES "heltec_t1" OR BOARD MATCHES "gat562" OR BOARD MATCHES "muziworks_r1neo")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf52.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf52_common.conf")
elseif(BOARD MATCHES ".*esp32.*" OR BOARD MATCHES "station_g2" OR BOARD MATCHES "thinknode_m9" OR BOARD MATCHES "lilygo_tlora_c6" OR BOARD MATCHES "heltec_wifi_lora32_v3" OR BOARD MATCHES "heltec_wifi_lora32_v4" OR BOARD MATCHES "meshnology_w12")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*esp32.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_common.conf")
elseif(BOARD MATCHES ".*nrf54l.*" OR BOARD MATCHES "me25ls02")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf54l_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*nrf54l.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/nrf54l_common.conf")
elseif(BOARD MATCHES ".*mg24.*" OR BOARD MATCHES ".*efr32.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*mg24.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf")
elseif(BOARD MATCHES "lora_e5" OR BOARD MATCHES ".*stm32wl.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/stm32wl_common.conf")
elseif(BOARD MATCHES "native_sim" OR BOARD MATCHES "native_posix")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/linux_native/linux_common.conf")
# ========== Build class ==========
# Role and debug class decide the auto-includes below, which have to be chosen
# before Kconfig exists. They are derived from the user's conf *names* (exact
# match, not substring) or from a -DCONFIG_ZEPHCORE_ROLE_* on the command line,
# and checked against the resolved Kconfig after find_package().
set(_user_conf_names "")
foreach(_c IN LISTS ZEPHCORE_USER_CONFS)
get_filename_component(_n "${_c}" NAME_WE)
list(APPEND _user_conf_names "${_n}")
endforeach()
if("observer" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_OBSERVER)
set(ZEPHCORE_BUILD_ROLE observer)
elseif("room_server" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_ROOM_SERVER)
set(ZEPHCORE_BUILD_ROLE room_server)
elseif("repeater" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_REPEATER)
set(ZEPHCORE_BUILD_ROLE repeater)
else()
set(ZEPHCORE_PLATFORM_CONF "")
set(ZEPHCORE_BUILD_ROLE companion)
endif()
# Check for board-specific config in new folder structure
# Priority: boards/<board>/board.conf > boards/<board>_<variant>.conf (legacy)
#
# For custom boards (e.g., wio_tracker_l1/nrf52840), BOARD contains the base name
# For Zephyr boards with qualifiers (e.g., rak4631/nrf52840), BOARD contains base/qualifier
# We extract just the base board name for folder lookup
if(BOARD)
string(REPLACE "/" ";" BOARD_PARTS "${BOARD}")
list(GET BOARD_PARTS 0 BOARD_BASE)
else()
set(BOARD_BASE "")
endif()
# Search for board.conf in possible locations
set(ZEPHCORE_BOARD_CONF "")
file(GLOB_RECURSE BOARD_CONF_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.conf")
if(BOARD_CONF_CANDIDATES)
list(GET BOARD_CONF_CANDIDATES 0 ZEPHCORE_BOARD_CONF)
endif()
# Fallback: direct path without vendor subdirectory
if(NOT ZEPHCORE_BOARD_CONF AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf")
set(ZEPHCORE_BOARD_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf")
endif()
# ========== DTC Overlay Precedence ==========
# board.overlay goes in FIRST, so everything applied after it can override it.
#
# Devicetree is last-writer-wins per property, and this list is that order. The
# board overlay is the board's *baseline*; the overlays paired with the confs
# that follow (repeater/pm_esp32/esp32s3_usb/no_display/...) are deliberate
# per-role deviations from it, and the conf chain already documents that same
# precedence ("user extras last so they can override"). Until 2026-08-28 the
# overlay order was the exact opposite of the conf order, with two live
# consequences:
# - esp32s3_usb.overlay could not move the console off USB Serial/JTAG on the
# boards that pick a console in board.overlay (heltec V4/V43, wireless
# tracker v2) -- the reroute was overridden and the console pointed at a
# peripheral that no longer owned the pins.
# - pm_esp32.overlay could not hold the SX1262 NSS across light sleep on
# heltec_wireless_tracker, whose board.overlay re-declares spi2 cs-gpios
# without ESP32_GPIO_SLEEP_HOLD_EN -- i.e. the exact floating-NSS failure
# that overlay exists to prevent.
# partitions.overlay is appended after everything (see below) and stays there:
# it must also be fed verbatim to the MCUboot image.
set(ZEPHCORE_BOARD_OVERLAY "")
file(GLOB_RECURSE BOARD_OVERLAY_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/board.overlay")
if(BOARD_OVERLAY_CANDIDATES)
list(GET BOARD_OVERLAY_CANDIDATES 0 ZEPHCORE_BOARD_OVERLAY)
endif()
# Fallback: direct path without vendor subdirectory
if(NOT ZEPHCORE_BOARD_OVERLAY AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay")
set(ZEPHCORE_BOARD_OVERLAY "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay")
endif()
# Append to EXTRA_DTC_OVERLAY_FILE (preserves any user-specified extras)
if(ZEPHCORE_BOARD_OVERLAY AND EXISTS ${ZEPHCORE_BOARD_OVERLAY})
if(EXTRA_DTC_OVERLAY_FILE)
set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE)
else()
set(EXTRA_DTC_OVERLAY_FILE "${ZEPHCORE_BOARD_OVERLAY}" CACHE STRING "" FORCE)
# Diagnostic overlays count as debug builds: on ESP32 they keep the USB port for
# the console and get debug_esp32.conf's console log backend.
set(ZEPHCORE_BUILD_DEBUG FALSE)
foreach(_n debug debug_esp32 hangdebug gps_debug)
if(_n IN_LIST _user_conf_names)
set(ZEPHCORE_BUILD_DEBUG TRUE)
endif()
endif()
endforeach()
# Build the EXTRA_CONF_FILE list (append to any user-provided extras)
# Auto-pair each .conf with a same-named .overlay if it exists.
set(ZEPHCORE_CONF_FILES "")
if(EXISTS ${ZEPHCORE_COMMON_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_COMMON_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_COMMON_CONF}")
endif()
if(ZEPHCORE_PLATFORM_CONF AND EXISTS ${ZEPHCORE_PLATFORM_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_PLATFORM_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_PLATFORM_CONF}")
# ========== Config chain ==========
# Kconfig and devicetree share one precedence order, later wins:
# zephcore_common → <platform>_common → esp32c3s3_common → version → board
# → auto-included feature confs → user extras → (debug_esp32) → partitions.overlay
#
# board.overlay goes FIRST among overlays: it is the board's baseline, and the
# overlays paired with later confs (repeater/pm_esp32/esp32s3_usb/no_display/...)
# are deliberate per-role deviations from it. Until 2026-08-28 the overlay order
# was the opposite of the conf order, which broke esp32s3_usb.overlay's console
# reroute on boards that pick a console in board.overlay (heltec V4/V43, wireless
# tracker v2) and pm_esp32.overlay's SX1262 NSS sleep-hold on
# heltec_wireless_tracker.
set(ZEPHCORE_CONFS "")
set(ZEPHCORE_OVERLAYS "")
zephcore_board_file(ZEPHCORE_BOARD_OVERLAY board.overlay)
zephcore_add_overlay("${ZEPHCORE_BOARD_OVERLAY}")
set(_zc "${CMAKE_CURRENT_SOURCE_DIR}/boards/common")
set(_platform_conf_of_nrf52 ${_zc}/nrf52_common.conf)
set(_platform_conf_of_nrf54l ${_zc}/nrf54l_common.conf)
set(_platform_conf_of_esp32 ${_zc}/esp32_common.conf)
set(_platform_conf_of_mg24 ${_zc}/mg24_common.conf)
set(_platform_conf_of_stm32wl ${_zc}/stm32wl_common.conf)
set(_platform_conf_of_linux ${CMAKE_CURRENT_SOURCE_DIR}/boards/linux_native/linux_common.conf)
zephcore_add_conf(${_zc}/zephcore_common.conf)
if(ZEPHCORE_PLATFORM)
zephcore_add_conf(${_platform_conf_of_${ZEPHCORE_PLATFORM}})
endif()
# ESP32-S3 and ESP32-C3 (shared BLE controller): overrides esp32_common.conf,
# stays below board.conf. Every S3 board target carries the /esp32s3/
# qualifier; the C3 boards are single-SoC and are named without one
# (xiao_esp32c3), so match the chip in the name (see esp32c3s3_common.conf).
# stays below board.conf. Every S3 target carries the /esp32s3/ qualifier; the
# C3 boards are single-SoC and named without one (xiao_esp32c3).
if(BOARD MATCHES "/esp32s3/" OR BOARD MATCHES "esp32c3")
list(APPEND ZEPHCORE_CONF_FILES "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32c3s3_common.conf")
zephcore_add_conf(${_zc}/esp32c3s3_common.conf)
endif()
if(ZEPHCORE_BOARD_CONF AND EXISTS ${ZEPHCORE_BOARD_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_BOARD_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_BOARD_CONF}")
# The BLE DIS firmware revision is Kconfig, so it is generated from the one
# version string above rather than kept by hand in a second place.
set(_version_conf "${CMAKE_BINARY_DIR}/zephcore_version.conf")
file(WRITE "${_version_conf}" "CONFIG_BT_DIS_FW_REV_STR=\"${ZEPHCORE_FIRMWARE_VERSION}\"\n")
list(APPEND ZEPHCORE_CONFS "${_version_conf}")
zephcore_board_file(ZEPHCORE_BOARD_CONF board.conf)
if(ZEPHCORE_BOARD_CONF)
zephcore_add_conf("${ZEPHCORE_BOARD_CONF}")
endif()
# Production is the default. Debug logging is opt-in via debug.conf.
# Auto-include WiFi OTA for ESP32 repeater builds.
# Enables WiFi AP + HTTP server + MCUboot image management.
# Requires --sysbuild to build MCUboot alongside the app.
# Non-ESP32 builds and companion builds are unaffected.
#
# Excluded on the classic ESP32 (e.g. T-Beam, board qualifier ".../esp32/...").
# It builds, but only because the WiFi DT node would be left disabled; with WiFi
# actually functional, the driver buffers + 64KB OTA heap overflow the classic
# ESP32's DRAM by ~10KB. Rather than trim WiFi/heap to an unverifiable margin,
# the classic ESP32 repeater stays CLI-only on simple boot (handled in build.sh).
# The ESP32-S3/C-series have the DRAM headroom and keep WiFi OTA.
# ========== ESP32 CPU clock ==========
# Espressif parts idle in the tens of mA (WAITI gates the core clock but leaves
# PLL/peripherals/RAM powered), so CPU frequency is a first-order term in a
# battery node's draw — unlike nRF52, which already idles at microamps. Pull
# every ESP32 build down to 80 MHz, matching what Arduino MeshCore ships.
# hal_espressif derives ESP_DEFAULT_CPU_FREQ_MHZ from cpu@0 directly, so 80
# needs no module patch (our 0002 carried that until the Zephyr-main bump).
#
# Observers are the exception and keep the SoC default (240 MHz on S3/classic,
# 160 MHz on C3/C6): they run a full WiFi + TLS + MQTT stack and are mains- or
# solar-with-a-big-panel powered, so throughput matters more than current.
# Implemented as "don't apply the overlay" rather than an override, so the
# per-SoC maximum is inherited instead of hardcoded here.
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND NOT EXTRA_CONF_FILE MATCHES "observer")
set(ZEPHCORE_ESP32_CPU_OVERLAY
"${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32_cpu_80mhz.overlay")
if(EXISTS ${ZEPHCORE_ESP32_CPU_OVERLAY})
if(EXTRA_DTC_OVERLAY_FILE)
set(EXTRA_DTC_OVERLAY_FILE
"${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_ESP32_CPU_OVERLAY}" CACHE STRING "" FORCE)
else()
set(EXTRA_DTC_OVERLAY_FILE
"${ZEPHCORE_ESP32_CPU_OVERLAY}" CACHE STRING "" FORCE)
endif()
# ---- Auto-included per-role features (ESP32 only today) ----
if(ZEPHCORE_PLATFORM STREQUAL "esp32")
# CPU clock. Espressif parts idle in the tens of mA (WAITI gates the core
# clock but leaves PLL/peripherals/RAM powered), so CPU frequency is a
# first-order term in a battery node's draw — unlike nRF52, which already
# idles at microamps. Every ESP32 build runs at 80 MHz, matching Arduino
# MeshCore; hal_espressif derives ESP_DEFAULT_CPU_FREQ_MHZ from cpu@0, so no
# module patch is needed. Observers keep the SoC maximum (240 MHz S3/classic,
# 160 MHz C3/C6): full WiFi + TLS + MQTT on mains or big-panel solar, so
# throughput matters more than current. "Don't apply the overlay" rather than
# an override, so the per-SoC maximum is inherited, not hardcoded.
if(NOT ZEPHCORE_BUILD_ROLE STREQUAL "observer")
zephcore_add_overlay(${_zc}/esp32_cpu_80mhz.overlay)
message(STATUS " ESP32 CPU: 80 MHz (observers keep the SoC maximum)")
endif()
endif()
# ========== ESP32 light sleep (allowlisted repeater boards) ==========
# 80 MHz above cuts the idle draw; light sleep is what removes it. See
# boards/common/pm_esp32.conf for the mechanism and its requirements — this
# block is only the policy that decides who gets it automatically.
#
# REPEATERS ONLY, and the gate is deliberately the same one WiFi OTA uses
# below: the Espressif HCI driver takes no pm_policy locks, so a companion
# build would light-sleep mid-advertising or mid-connection. repeater.conf
# sets CONFIG_BT=n, which removes the problem instead of racing it.
#
# BOARD ALLOWLIST, not a platform check. The hard requirement is DIO1 on an
# RTC-capable pin (GPIO 0-21 on the S3) so a received packet can wake the SoC.
# Every board below wires DIO1 to GPIO14. Deliberately ABSENT and must stay
# absent unless their wiring changes:
# xiao_esp32s3 (DIO1=GPIO39), station_g2 (GPIO48), thinknode_m9 (GPIO42),
# lilygo_tlora_c6 (GPIO23, outside the C6's LP range 0-7),
# lilygo_t3s3 (GPIO33)
# -> cannot wake on a received packet; the node would go deaf while
# looking excellent on a current meter.
# ttgo_lora32 -> SX127x; the DIO1 wake flag lives in the sx126x half of
# patches/zephyr/0003, so nothing marks the pin as a wake source.
# heltec_wifi_lora32_v4_r8 -> absent for a different reason: its wiring does
# qualify (DIO1=GPIO14, NSS=GPIO8, RESET=GPIO12 on &spi2, button on
# gpio0 0), so every criterion stated here and in pm_esp32.conf is met.
# It is out because CONFIG_PM has never been validated on the board.
# Without the anchoring below it would be swept in by substring match
# rather than by decision. Add it once someone has run pm_esp32.conf's
# three checks on hardware.
#
# CONSOLE, and why it does NOT gate this list. A native USB Serial/JTAG console
# cannot survive light sleep: the peripheral drops off the bus and the HAL has
# no USJ wake source (only UART0/1/2 and LP-UART). Of the boards below, only
# Heltec V3 consoles on a physical uart0 (CP2102). V4, V4.3 and Wireless
# Tracker V2 choose usb_serial in their board.overlay, and the uart0 reroute in
# boards/common/esp32s3_usb.overlay is COMPANION-ONLY (the auto-include below
# excludes repeater/observer/room_server), so a repeater build of those boards
# keeps a USJ console.
#
# The consequence is accepted, not overlooked: on V4/V4.3/WT-V2 the USB CLI
# stops answering once ZEPHCORE_PM_BOOT_AWAKE_MS expires, and the console flush
# in helpers/pm_esp32_console.c targets UART0, which is not their console, so it
# is inert there. Remote admin over LoRa is unaffected, and connecting a
# terminal normally resets the board (the bridge drives EN from DTR) which
# re-arms the window. These boards are in the field deliberately to get light
# sleep exercised; drop them from the regex to take it away.
#
# Anchored, and every member spelled out. CMake MATCHES is a substring search,
# so the bare alternation matched any BOARD merely *containing* one of these
# names at any offset: "heltec_wifi_lora32_v4" also matches
# "heltec_wifi_lora32_v4_r8", so that board would get CONFIG_PM=y without
# anyone deciding it should. The leading "^" is what stops mid-string matches
# and is not made redundant by the tail.
#
# BOARD carries its qualifiers (heltec_wifi_lora32_v4/esp32s3/procpu), and may
# carry a revision as heltec_wifi_lora32_v3@1/esp32s3/procpu, so the tail
# accepts "@", "/" or end-of-string rather than "$" alone. No board here
# declares a revision today; without "@" one that later did would drop out of
# this list silently.
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater"
AND BOARD MATCHES "^(heltec_wifi_lora32_v3|heltec_wifi_lora32_v4|heltec_wifi_lora32_v43|heltec_wireless_tracker|heltec_wireless_tracker_v2)(@|/|$)")
set(ZEPHCORE_PM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/pm_esp32.conf")
if(EXISTS ${ZEPHCORE_PM_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_PM_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_PM_CONF}")
# Light sleep: REPEATERS on boards that declare it. 80 MHz cuts the idle draw;
# light sleep is what removes it (mechanism and requirements:
# boards/common/pm_esp32.conf). Repeaters only: the Espressif HCI driver takes
# no pm_policy locks, so a companion would light-sleep mid-advertising or
# mid-connection; repeater.conf sets CONFIG_BT=n. Which boards qualify is a
# hardware fact (DIO1 on an RTC-wake-capable GPIO, and validated on hardware),
# so the board declares it: `capabilities: light_sleep: true` in its
# zephcore.yml. pm_esp32.conf lists which boards fail the wiring test and why;
# a board that passes it but was never validated (v4_r8) stays undeclared.
if(ZEPHCORE_BUILD_ROLE STREQUAL "repeater" AND ZEPHCORE_BOARD_LIGHT_SLEEP)
zephcore_add_conf(${_zc}/pm_esp32.conf)
message(STATUS " ESP32 light sleep: auto-enabled (DIO1 wake-capable repeater board)")
endif()
endif()
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND EXTRA_CONF_FILE MATCHES "repeater"
AND NOT BOARD MATCHES "/esp32/")
set(ZEPHCORE_WIFI_OTA_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/wifi_ota.conf")
if(EXISTS ${ZEPHCORE_WIFI_OTA_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_WIFI_OTA_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_WIFI_OTA_CONF}")
# WiFi OTA for S3/C-series repeaters: WiFi AP + HTTP server + MCUboot image
# management, so these builds need --sysbuild. Not on the classic ESP32
# (".../esp32/..."): with WiFi functional, the driver buffers + 64 KB OTA heap
# overflow its DRAM by ~10 KB, so the classic repeater stays CLI-only on simple
# boot.
if(ZEPHCORE_BUILD_ROLE STREQUAL "repeater" AND NOT BOARD MATCHES "/esp32/")
zephcore_add_conf(${_zc}/wifi_ota.conf)
message(STATUS " WiFi OTA: auto-enabled (ESP32 repeater)")
endif()
endif()
# Auto-include the ESP32-S3 USB CDC-ACM companion transport.
#
# The policy lives here rather than in build.sh so that a locally built
# companion is the same firmware as the published release. The ESP32 release
# flow already diverges from a plain build in its *layout* (sysbuild/MCUboot);
# letting it diverge in which *transports* the app speaks as well would make
# user bug reports unreproducible from source.
#
# Detection is the board's own devicetree, not a name list: a board that
# includes esp32s3_usb_otg.dtsi has declared the cdc_acm_uart node, which is
# exactly the precondition the USB companion call sites are compiled behind
# (see ZEPHCORE_USB_STACK in src/main_companion.cpp). Boards without it are
# skipped automatically -- Heltec V3 (USB-C via CP2102, uses
# serial_companion.conf), Wireless Tracker V1, and every C3/C6/classic-ESP32
# part, which has no DWC2 controller at all.
#
# COMPANION ROLE ONLY. esp32s3_usb.conf sets CONFIG_ZEPHCORE_COMPANION_USB=y,
# which carries no role dependency of its own, and its paired overlay moves the
# console to uart0. In a repeater/observer/room-server build that would both
# compile the companion USB stack and strand the serial CLI on GPIO43/44.
# serial_companion.conf is excluded for a different reason: COMPANION_SERIAL
# `depends on !COMPANION_USB`, so adding this would silently disable it.
#
# DEBUG BUILDS ARE EXCLUDED. ESP32 has no RTT (debug_esp32.conf routes logs to
# the console backend) and this conf hands that USB port to the binary
# companion protocol, so an auto-enabled debug build would have nowhere to log.
# Pass both confs explicitly to debug the USB transport itself; logs then need
# a UART adapter on GPIO43/44.
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common" AND ZEPHCORE_BOARD_CONF
AND NOT EXTRA_CONF_FILE MATCHES "repeater|observer|room_server|serial_companion|esp32s3_usb|debug")
get_filename_component(ZEPHCORE_BOARD_DIR "${ZEPHCORE_BOARD_CONF}" DIRECTORY)
if(EXISTS "${ZEPHCORE_BOARD_DIR}/board.overlay")
file(READ "${ZEPHCORE_BOARD_DIR}/board.overlay" ZEPHCORE_BOARD_OVERLAY_TEXT)
if(ZEPHCORE_BOARD_OVERLAY_TEXT MATCHES "esp32s3_usb_otg\\.dtsi")
set(ZEPHCORE_USB_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/esp32s3_usb.conf")
if(EXISTS ${ZEPHCORE_USB_CONF})
list(APPEND ZEPHCORE_CONF_FILES ${ZEPHCORE_USB_CONF})
zephcore_auto_pair_overlay("${ZEPHCORE_USB_CONF}")
message(STATUS " USB CDC companion: auto-enabled (board declares USB OTG CDC-ACM)")
endif()
# USB CDC-ACM companion transport on S3 boards whose board.overlay includes
# esp32s3_usb_otg.dtsi (that include declares the cdc_acm_uart node the USB
# companion call sites are compiled behind — see ZEPHCORE_USB_STACK in
# src/main_companion.cpp). Lives here, not in build.sh, so a locally built
# companion is the same firmware as the release. Companion only:
# esp32s3_usb.conf enables the companion USB stack and its overlay moves the
# console to uart0, which would strand a server role's CLI on GPIO43/44. Not
# with serial_companion (COMPANION_SERIAL depends on !COMPANION_USB) and not in
# debug builds (ESP32 has no RTT; the port must stay the log console).
if(ZEPHCORE_BUILD_ROLE STREQUAL "companion" AND NOT ZEPHCORE_BUILD_DEBUG
AND NOT "serial_companion" IN_LIST _user_conf_names
AND NOT "esp32s3_usb" IN_LIST _user_conf_names
AND ZEPHCORE_BOARD_OVERLAY)
file(READ "${ZEPHCORE_BOARD_OVERLAY}" _board_overlay_text)
if(_board_overlay_text MATCHES "esp32s3_usb_otg\\.dtsi")
zephcore_add_conf(${_zc}/esp32s3_usb.conf)
message(STATUS " USB CDC companion: auto-enabled (board declares USB OTG CDC-ACM)")
endif()
unset(ZEPHCORE_BOARD_OVERLAY_TEXT)
endif()
endif()
# Append to EXTRA_CONF_FILE — auto-generated configs first, user extras last.
# User-specified extras (repeater.conf, debug.conf) MUST come after the auto chain
# so they can override settings (e.g. CONFIG_BT=n in repeater.conf).
if(ZEPHCORE_CONF_FILES)
if(EXTRA_CONF_FILE)
set(EXTRA_CONF_FILE "${ZEPHCORE_CONF_FILES};${EXTRA_CONF_FILE}" CACHE STRING "" FORCE)
else()
set(EXTRA_CONF_FILE "${ZEPHCORE_CONF_FILES}" CACHE STRING "" FORCE)
endif()
endif()
# ---- User extras last, so they override everything above ----
foreach(_c IN LISTS ZEPHCORE_USER_CONFS)
zephcore_add_conf("${_c}")
endforeach()
# ESP32 debug builds: debug.conf is nRF/SEGGER-RTT shaped — on ESP32 RTT yields
# no output (no J-Link; logs go out the UART console) and the deferred-log DRAM
# overflows the RAM-tight classic ESP32. Append debug_esp32.conf LAST so it wins
# over debug.conf (console backend, smaller buffers, trimmed companion arrays).
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common"
AND EXTRA_CONF_FILE MATCHES "debug" AND NOT EXTRA_CONF_FILE MATCHES "debug_esp32")
set(ZEPHCORE_DEBUG_ESP32_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/debug_esp32.conf")
if(EXISTS ${ZEPHCORE_DEBUG_ESP32_CONF})
set(EXTRA_CONF_FILE "${EXTRA_CONF_FILE};${ZEPHCORE_DEBUG_ESP32_CONF}" CACHE STRING "" FORCE)
message(STATUS " ESP32 debug: appended debug_esp32.conf (no RTT, smaller buffers)")
endif()
# overflows the classic ESP32. debug_esp32.conf goes LAST so it wins over
# debug.conf (console backend, smaller buffers, trimmed companion arrays).
if(ZEPHCORE_PLATFORM STREQUAL "esp32" AND ZEPHCORE_BUILD_DEBUG
AND NOT "debug_esp32" IN_LIST _user_conf_names)
zephcore_add_conf(${_zc}/debug_esp32.conf)
message(STATUS " ESP32 debug: appended debug_esp32.conf (no RTT, smaller buffers)")
endif()
# Auto-pair user-supplied EXTRA_CONF_FILE entries with same-named .overlay files.
# This allows repeater.conf + repeater.overlay, wifi_ota.conf + wifi_ota.overlay, etc.
if(EXTRA_CONF_FILE)
foreach(_conf IN LISTS EXTRA_CONF_FILE)
zephcore_auto_pair_overlay("${_conf}")
endforeach()
endif()
# User-supplied overlays (-DEXTRA_DTC_OVERLAY_FILE) win over everything derived
# above, like user confs do. Passed through as given — Zephyr resolves relative
# paths and reports a missing file — and moved to the end if already listed.
# (Before 2026-09-23 they went in FIRST, i.e. lowest precedence.)
foreach(_ov IN LISTS ZEPHCORE_USER_EXTRA_DTC_OVERLAY_FILE)
list(REMOVE_ITEM ZEPHCORE_OVERLAYS "${_ov}")
list(APPEND ZEPHCORE_OVERLAYS "${_ov}")
endforeach()
# ========== DTC Overlay Hierarchy ==========
# Flash partition layout lives in a separate boards/<...>/<board>/partitions.overlay
# so the SAME file can be fed to the MCUboot image too (see sysbuild/CMakeLists.txt).
# The app and MCUboot MUST share one flash map — otherwise MCUboot stages/looks for
# the OTA image at a different address than the app writes it to, and OTA silently
# reverts on reboot. Keeping it in its own file is what makes that sharing possible.
set(ZEPHCORE_BOARD_PARTITIONS "")
file(GLOB_RECURSE BOARD_PARTITIONS_CANDIDATES "${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/partitions.overlay")
if(BOARD_PARTITIONS_CANDIDATES)
list(GET BOARD_PARTITIONS_CANDIDATES 0 ZEPHCORE_BOARD_PARTITIONS)
endif()
if(NOT ZEPHCORE_BOARD_PARTITIONS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/partitions.overlay")
set(ZEPHCORE_BOARD_PARTITIONS "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/partitions.overlay")
endif()
if(ZEPHCORE_BOARD_PARTITIONS AND EXISTS ${ZEPHCORE_BOARD_PARTITIONS})
if(EXTRA_DTC_OVERLAY_FILE)
set(EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE};${ZEPHCORE_BOARD_PARTITIONS}" CACHE STRING "" FORCE)
else()
set(EXTRA_DTC_OVERLAY_FILE "${ZEPHCORE_BOARD_PARTITIONS}" CACHE STRING "" FORCE)
endif()
endif()
# Flash partitions last, even after user overlays: the SAME file is fed to the
# MCUboot image
# (sysbuild/CMakeLists.txt). App and MCUboot must share one flash map, otherwise
# MCUboot looks for the OTA image at a different address than the app writes it
# to and OTA silently reverts on reboot.
zephcore_board_file(ZEPHCORE_BOARD_PARTITIONS partitions.overlay)
zephcore_add_overlay("${ZEPHCORE_BOARD_PARTITIONS}")
set(EXTRA_CONF_FILE "${ZEPHCORE_CONFS}" CACHE STRING "" FORCE)
set(EXTRA_DTC_OVERLAY_FILE "${ZEPHCORE_OVERLAYS}" CACHE STRING "" FORCE)
set(ZEPHCORE_WRITTEN_EXTRA_CONF_FILE "${EXTRA_CONF_FILE}" CACHE INTERNAL "")
set(ZEPHCORE_WRITTEN_EXTRA_DTC_OVERLAY_FILE "${EXTRA_DTC_OVERLAY_FILE}" CACHE INTERNAL "")
# Debug: Show what configs are being used
message(STATUS "ZephCore config hierarchy:")
message(STATUS " Common: ${ZEPHCORE_COMMON_CONF}")
message(STATUS " Platform: ${ZEPHCORE_PLATFORM_CONF}")
message(STATUS " Board conf: ${ZEPHCORE_BOARD_CONF}")
message(STATUS " Board overlay: ${ZEPHCORE_BOARD_OVERLAY}")
message(STATUS " Board dir: ${ZEPHCORE_BOARD_DIR}")
message(STATUS " Platform: ${ZEPHCORE_PLATFORM} Role: ${ZEPHCORE_BUILD_ROLE} Debug: ${ZEPHCORE_BUILD_DEBUG}")
message(STATUS " EXTRA_CONF_FILE: ${EXTRA_CONF_FILE}")
message(STATUS " EXTRA_DTC_OVERLAY_FILE: ${EXTRA_DTC_OVERLAY_FILE}")
@@ -452,27 +277,39 @@ set(TF_PSA_CRYPTO_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zephcore)
# Dynamic build date - always current, even without --pristine
# The build-class guess made before Kconfig existed must match the role Kconfig
# actually resolved; otherwise the auto-included confs above were chosen for the
# wrong role (e.g. the ESP32 USB companion transport in a server build, which
# strands the serial CLI). Pass the role via boards/common/<role>.conf.
set(_kconfig_role companion)
foreach(_r repeater room_server observer)
string(TOUPPER "${_r}" _R)
if(CONFIG_ZEPHCORE_ROLE_${_R})
set(_kconfig_role ${_r})
endif()
endforeach()
if(NOT _kconfig_role STREQUAL ZEPHCORE_BUILD_ROLE)
message(FATAL_ERROR
"ZephCore: Kconfig resolved role '${_kconfig_role}' but the build was "
"classified as '${ZEPHCORE_BUILD_ROLE}' from EXTRA_CONF_FILE. Select the "
"role with boards/common/${_kconfig_role}.conf (or -DCONFIG_ZEPHCORE_ROLE_*).")
endif()
# Build date and epoch, injected into every app TU. Both are day-granular so a
# reconfigure on the same day leaves every compile command unchanged and ccache's
# direct mode keeps hitting. The epoch is mesh time sync's "provably dead clock"
# floor (a local time below it is impossible for a running build); midnight UTC
# of the build day keeps that true. string(TIMESTAMP) honors SOURCE_DATE_EPOCH
# for reproducible builds.
string(TIMESTAMP ZEPHCORE_BUILD_DATE "%Y %b %d" UTC)
add_definitions(-DFIRMWARE_BUILD_DATE="${ZEPHCORE_BUILD_DATE}")
# Build-time UNIX epoch: mesh time sync's "provably dead clock" floor (a local
# time below this is impossible for a running build). string(TIMESTAMP) honors
# SOURCE_DATE_EPOCH for reproducible builds.
string(TIMESTAMP ZEPHCORE_BUILD_EPOCH "%s" UTC)
string(TIMESTAMP _now "%s" UTC)
math(EXPR ZEPHCORE_BUILD_EPOCH "${_now} - (${_now} % 86400)")
add_definitions(-DFIRMWARE_BUILD_EPOCH=${ZEPHCORE_BUILD_EPOCH}u)
# Single source of truth for the firmware version string (vMAJOR.MINOR.PATCH-zephyr).
# Injected globally like FIRMWARE_BUILD_DATE above, so every app TU sees the same
# value; the per-app `#ifndef FIRMWARE_VERSION` fallbacks only apply to builds that
# bypass this injection. NOTE: the BLE DIS value in boards/common/zephcore_common.conf
# (CONFIG_BT_DIS_FW_REV_STR) is Kconfig, not C, so it must be bumped here AND there.
# The single source of truth for the firmware version. This exact string is what
# the device reports, what CONFIG_BT_DIS_FW_REV_STR advertises, what the GitHub
# release is tagged/named, and what the Mesh America catalog uses as its version
# key — so the configurator can match a running device against the catalog. Keep
# all four identical; the release workflow reads this value directly.
set(ZEPHCORE_FIRMWARE_VERSION "1.17.5c-zephcore")
# Version string: set once near the top of this file (the release workflow reads
# that line). The per-app `#ifndef FIRMWARE_VERSION` fallbacks only apply to
# builds that bypass this injection.
add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}")
add_subdirectory(lib/monocypher)
@@ -551,10 +388,8 @@ target_sources_ifdef(CONFIG_SENSOR app PRIVATE adapters/sensors/t1000e_analog.c)
# and the weak battery_curve_default. A board-specific battery_curve.c is compiled
# alongside it to override the weak symbol with measured cell data.
target_sources(app PRIVATE helpers/battery_curve.c)
file(GLOB_RECURSE _BOARD_BATTERY_CURVE
"${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/battery_curve.c")
zephcore_board_file(_BOARD_BATTERY_CURVE battery_curve.c)
if(_BOARD_BATTERY_CURVE)
list(GET _BOARD_BATTERY_CURVE 0 _BOARD_BATTERY_CURVE)
message(STATUS "ZephCore Battery: board-specific curve (${_BOARD_BATTERY_CURVE})")
target_sources(app PRIVATE "${_BOARD_BATTERY_CURVE}")
else()
@@ -572,10 +407,8 @@ endif()
# bus put any inserted card into SPI idle before the radio driver initialises,
# so it cannot disturb the bus. Registers itself with SYS_INIT; nothing calls
# into it, so it is simply compiled in when the board provides one.
file(GLOB_RECURSE _BOARD_SD_QUIESCE
"${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/sd_quiesce.c")
zephcore_board_file(_BOARD_SD_QUIESCE sd_quiesce.c)
if(_BOARD_SD_QUIESCE)
list(GET _BOARD_SD_QUIESCE 0 _BOARD_SD_QUIESCE)
message(STATUS "ZephCore SD: shared-bus quiesce (${_BOARD_SD_QUIESCE})")
target_sources(app PRIVATE "${_BOARD_SD_QUIESCE}")
endif()
@@ -637,7 +470,6 @@ if(CONFIG_ZEPHCORE_RADIO_LR1110)
${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio/lr11xx
)
# Driver header (lr11xx_lora.h) is in the Zephyr tree after patching
get_filename_component(ZEPHYR_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE)
target_include_directories(app PRIVATE
${ZEPHYR_DIR}/drivers/lora/lr11xx
)
@@ -649,9 +481,8 @@ elseif(CONFIG_ZEPHCORE_RADIO_LR2021)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/radio/lr20xx
)
get_filename_component(ZEPHYR_DIR_LR20 ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE)
target_include_directories(app PRIVATE
${ZEPHYR_DIR_LR20}/drivers/lora/lr20xx
${ZEPHYR_DIR}/drivers/lora/lr20xx
)
elseif(CONFIG_ZEPHCORE_RADIO_SX127X)
message(STATUS "ZephCore Radio: SX127x (Zephyr loramac-node driver)")
@@ -665,29 +496,42 @@ else()
adapters/radio/SX126xRadio.cpp
)
# Extension API header (sx126x_ext.h) is in the Zephyr driver tree
get_filename_component(ZEPHYR_DIR_SX ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr ABSOLUTE)
target_include_directories(app PRIVATE
${ZEPHYR_DIR_SX}/drivers/lora/native/sx126x
${ZEPHYR_DIR}/drivers/lora/native/sx126x
)
endif()
# ========== Role-Specific Sources ==========
if(CONFIG_ZEPHCORE_ROLE_REPEATER)
message(STATUS "ZephCore Role: REPEATER")
# Source order is link order and so affects the image; keep it stable.
if(CONFIG_ZEPHCORE_ROLE_REPEATER OR CONFIG_ZEPHCORE_ROLE_ROOM_SERVER)
if(CONFIG_ZEPHCORE_ROLE_REPEATER)
message(STATUS "ZephCore Role: REPEATER")
target_sources(app PRIVATE
src/main_repeater.cpp
app/RepeaterMesh.cpp
app/RepeaterRegionCLI.cpp
)
else()
message(STATUS "ZephCore Role: ROOM SERVER")
target_sources(app PRIVATE
src/main_room_server.cpp
app/RoomServerMesh.cpp
app/RoomServerRegionCLI.cpp
)
endif()
# Shared by both server roles: persistence under /lfs/repeater/, client ACL,
# region filtering, transport keys, and the text CLI.
target_sources(app PRIVATE
src/main_repeater.cpp
app/RepeaterMesh.cpp
app/RepeaterRegionCLI.cpp
app/RepeaterDataStore.cpp
helpers/ClientACL.cpp
helpers/RegionMap.cpp
helpers/TransportKeyStore.cpp
helpers/CommonCLI.cpp
)
# Headless repeater (e.g. native-Linux SBC): no display/buttons/buzzer, so the
# Headless build (e.g. native-Linux SBC): no display/buttons/buzzer, so the
# real ui_* implementation (helpers/ui-button or helpers/ui-joystick) isn't
# compiled. main_repeater.cpp calls ui_* unconditionally — pull in the weak
# no-op stubs to satisfy the link. Mirrors the companion TCP branch above.
# compiled, but the server mains call ui_* unconditionally — the weak no-op
# stubs satisfy the link.
if(NOT CONFIG_ZEPHCORE_UI_DESIGN_BUTTON AND NOT CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)
target_sources(app PRIVATE helpers/ui/ui_headless_stubs.c)
target_include_directories(app PRIVATE
@@ -704,54 +548,26 @@ if(CONFIG_ZEPHCORE_ROLE_REPEATER)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/usb
)
if(CONFIG_ZEPHCORE_REPEATER_UPLINK AND CONFIG_MQTT_LIB)
target_sources(app PRIVATE
app/RepeaterUplink.cpp
app/observer_creds.cpp
adapters/wifi/ZephyrWiFiStation.c
adapters/mqtt/ZephyrMQTTPublisher.c
)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/wifi
${CMAKE_CURRENT_SOURCE_DIR}/adapters/mqtt
${CMAKE_CURRENT_SOURCE_DIR}/app
)
elseif(CONFIG_ZEPHCORE_REPEATER_UPLINK)
message(WARNING "ZephCore repeater uplink requested, but MQTT is disabled in this configuration. Build will exclude uplink runtime.")
if(CONFIG_ZEPHCORE_ROLE_REPEATER)
if(CONFIG_ZEPHCORE_REPEATER_UPLINK AND CONFIG_MQTT_LIB)
target_sources(app PRIVATE
app/RepeaterUplink.cpp
app/observer_creds.cpp
adapters/wifi/ZephyrWiFiStation.c
adapters/mqtt/ZephyrMQTTPublisher.c
)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/wifi
${CMAKE_CURRENT_SOURCE_DIR}/adapters/mqtt
${CMAKE_CURRENT_SOURCE_DIR}/app
)
elseif(CONFIG_ZEPHCORE_REPEATER_UPLINK)
message(WARNING "ZephCore repeater uplink requested, but MQTT is disabled in this configuration. Build will exclude uplink runtime.")
endif()
target_compile_definitions(app PRIVATE ZEPHCORE_REPEATER=1)
else()
target_compile_definitions(app PRIVATE ZEPHCORE_ROOM_SERVER=1)
endif()
target_compile_definitions(app PRIVATE ZEPHCORE_REPEATER=1)
elseif(CONFIG_ZEPHCORE_ROLE_ROOM_SERVER)
message(STATUS "ZephCore Role: ROOM SERVER")
target_sources(app PRIVATE
src/main_room_server.cpp
app/RoomServerMesh.cpp
app/RoomServerRegionCLI.cpp
app/RepeaterDataStore.cpp
helpers/ClientACL.cpp
helpers/RegionMap.cpp
helpers/TransportKeyStore.cpp
helpers/CommonCLI.cpp
)
# Headless build (e.g. native-Linux SBC): no display/buttons/buzzer, so the
# real ui_* implementation (helpers/ui-button or helpers/ui-joystick) isn't
# compiled. main_room_server.cpp calls ui_* unconditionally — pull in the weak
# no-op stubs to satisfy the link. Mirrors the repeater/observer branches.
if(NOT CONFIG_ZEPHCORE_UI_DESIGN_BUTTON AND NOT CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)
target_sources(app PRIVATE helpers/ui/ui_headless_stubs.c)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui
)
endif()
# Shared USBD CDC ACM init + 1200-baud DFU + DTR event/callback module.
if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT AND (CONFIG_USB_CDC_ACM OR CONFIG_USBD_CDC_ACM_CLASS))
target_sources(app PRIVATE
adapters/usb/ZephyrUSBCDC.cpp
)
endif()
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/usb
)
target_compile_definitions(app PRIVATE ZEPHCORE_ROOM_SERVER=1)
elseif(CONFIG_ZEPHCORE_ROLE_OBSERVER)
message(STATUS "ZephCore Role: OBSERVER")
target_sources(app PRIVATE
@@ -928,35 +744,11 @@ if(CONFIG_ZEPHCORE_WIFI_OTA)
zephyr_linker_sources(SECTIONS ${CMAKE_CURRENT_SOURCE_DIR}/adapters/ota/sections-rom.ld)
endif()
# ========== Post-build: ESP32 flash instructions ==========
if(ZEPHCORE_PLATFORM_CONF MATCHES "esp32_common")
if(CONFIG_ZEPHCORE_WIFI_OTA)
# MCUboot build (via --sysbuild): merged image or west flash
message(STATUS "")
message(STATUS "ESP32 Flash Commands (MCUboot + WiFi OTA):")
message(STATUS " Flash all (MCUboot + app): west flash --build-dir build_dir")
message(STATUS " OTA update binary: build_dir/zephcore/zephyr/zephyr.signed.bin")
message(STATUS " Full erase first: esptool --port COMx erase_flash")
message(STATUS "")
else()
# Simple boot: zephyr.bin is a self-contained image at 0x0.
message(STATUS "")
message(STATUS "ESP32 Flash Commands:")
message(STATUS " Normal update (preserves NVS + user data):")
message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin")
message(STATUS " Full flash (after erase_flash):")
message(STATUS " esptool --port COMx --baud 921600 erase_flash")
message(STATUS " esptool --port COMx --baud 921600 write_flash 0x0 build_dir/zephyr/zephyr.bin")
message(STATUS " Or use: west flash --build-dir build_dir")
message(STATUS "")
endif()
endif()
# ========== Post-build: Generate DFU zip for OTA updates (nRF52 only) ==========
# This runs after ALL build steps (linking, objcopy) via a custom target
# Only applicable on nRF52 boards with Adafruit bootloader
find_program(ADAFRUIT_NRFUTIL adafruit-nrfutil)
if(ADAFRUIT_NRFUTIL AND ZEPHCORE_PLATFORM_CONF MATCHES "nrf52_common")
if(ADAFRUIT_NRFUTIL AND ZEPHCORE_PLATFORM STREQUAL "nrf52")
set(DFU_ZIP "${CMAKE_BINARY_DIR}/zephyr/zephyr.zip")
set(DFU_HEX "${CMAKE_BINARY_DIR}/zephyr/zephyr.hex")
add_custom_target(dfu_zip ALL
+4 -1
View File
@@ -1120,7 +1120,10 @@ config ZEPHCORE_DISPLAY_MONO_TFT
config ZEPHCORE_EASTER_EGG_DOOM
bool "Enable Doom raycaster easter egg"
default n
# Shipped on the Wio Tracker L1 companion. A Kconfig default rather than a
# build.sh flag, so a local build is the same firmware as the release; not
# in board.conf, which would also enable it in that board's repeater build.
default y if BOARD_WIO_TRACKER_L1 && ZEPHCORE_ROLE_COMPANION
depends on ZEPHCORE_UI_DISPLAY && ZEPHCORE_UI_BUTTONS
help
Hidden easter egg: Wolf3D-style raycaster on the OLED.
+1 -2
View File
@@ -1,7 +1,6 @@
# Packet Logging Only - Common to Repeater boards
# Build: west build -b <board> zephcore --pristine -- \
# -DCONFIG_ZEPHCORE_ROLE_REPEATER=y \
# -DEXTRA_CONF_FILE="boards/common/packet_logging.conf"
# -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/packet_logging.conf"
#
# Output: Clean packet logs only (RAW, RX, TX lines)
# No debug spam from zephcore_lora, zephcore_dispatcher, etc.
+3 -5
View File
@@ -166,11 +166,9 @@ CONFIG_BT_DIS_MANUF_NAME_STR="ZephCore"
CONFIG_BT_DIS_MODEL_NUMBER=y
CONFIG_BT_DIS_MODEL_NUMBER_STR="ZephCore"
CONFIG_BT_DIS_FW_REV=y
# Keep in sync with ZEPHCORE_FIRMWARE_VERSION in CMakeLists.txt (the single source
# of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't
# read a C macro, so it must be bumped here too. Phones that read DIS and phones
# that query CMD_DEVICE_QUERY should see the same version.
CONFIG_BT_DIS_FW_REV_STR="1.17.5c-zephcore"
# CONFIG_BT_DIS_FW_REV_STR is generated by CMakeLists.txt from
# ZEPHCORE_FIRMWARE_VERSION (build dir zephcore_version.conf), so phones that read
# DIS and phones that query CMD_DEVICE_QUERY always see the same version.
CONFIG_BT_DIS_SW_REV=y
CONFIG_BT_DIS_SW_REV_STR="Zephyr"
CONFIG_BT_DIS_PNP=n
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_wifi_lora32_v3/esp32s3/procpu
capabilities:
light_sleep: true
release:
roles: [companion, repeater]
catalog:
device: "Heltec v3"
maker: heltec
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_wifi_lora32_v4/esp32s3/procpu
capabilities:
light_sleep: true
release:
roles: [companion, repeater]
catalog:
device: "Heltec v4"
maker: heltec
@@ -0,0 +1,11 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_wifi_lora32_v43/esp32s3/procpu
capabilities:
light_sleep: true
release:
roles: [companion, repeater]
catalog:
device: "Heltec v4.3"
maker: heltec
new: true
img: heltec_v4.svg
@@ -0,0 +1,14 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_wifi_lora32_v4_r8/esp32s3/procpu
# No light_sleep capability on purpose: the wiring qualifies (DIO1=GPIO14,
# NSS=GPIO8, RESET=GPIO12, button on gpio0) but CONFIG_PM was never validated on
# this board. Declare it once pm_esp32.conf's three checks pass on hardware.
release:
roles: [companion, repeater]
# Catalog: MeshCore folds the R8 (ESP32-S3R8, 8 MB octal PSRAM) into its
# "Heltec v4" tile as "[R8] ..." role rows rather than a device of its own, so it
# folds here too, told apart by the subtitle.
catalog:
device: "Heltec v4"
maker: heltec
subtitle: R8
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_wireless_tracker/esp32s3/procpu
capabilities:
light_sleep: true
release:
roles: [companion, repeater]
catalog:
device: "Heltec Wireless Tracker"
maker: heltec
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_wireless_tracker_v2/esp32s3/procpu
capabilities:
light_sleep: true
release:
roles: [companion, repeater]
catalog:
device: "Heltec Wireless Tracker v2"
maker: heltec
@@ -0,0 +1,10 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: lilygo_t3s3/esp32s3/procpu
release:
roles: [companion, repeater]
# Catalog: MeshCore splits this board by radio -- "(SX126x)" and "(SX127x)" are
# two tiles sharing the "LilyGo T3 S3" group. We only build the SX1262 variant, so
# fold into the SX126x tile; an SX127x port would be its own entry.
catalog:
device: "LilyGo T3 S3 (SX126x)"
maker: lilygo
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: lilygo_tlora_c6/esp32c6/hpcore
release:
roles: [companion, repeater]
catalog:
device: "LilyGo T-Lora C6"
maker: lilygo
new: true
img: lilygo_tlora_c6.svg
@@ -0,0 +1,13 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: meshnology_w12/esp32s3/procpu
release:
roles: [companion, repeater]
# Catalog: MeshCore firmware has a meshnology_w12 variant but their flasher config
# has no W12 device and no meshnology maker, so there is nothing to fold into yet.
# The name follows their maker-plus-product convention, so it folds by itself if
# they add one. WHEN IT APPEARS: drop `new` (and `own_img`, if one is set).
catalog:
device: "Meshnology W12"
maker: meshnology
new: true
img: lora.svg
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: station_g2/esp32s3/procpu
release:
roles: [companion, repeater]
catalog:
device: "UnitEng Station G2"
maker: uniteng
@@ -0,0 +1,6 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
# Pulled from the release matrix 2026-07-22: bring-up still in progress (keypad,
# GPS, battery unverified on hardware). To publish, add:
# release: {roles: [companion, repeater]}
# catalog: {device: "Elecrow ThinkNode M9", maker: elecrow}
target: thinknode_m9/esp32s3/procpu
@@ -0,0 +1,3 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
# Not in the release matrix (bring-up / unvalidated).
target: ttgo_lora32/esp32/procpu
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: ttgo_tbeam/esp32/procpu
release:
roles: [companion, repeater]
catalog:
device: "LilyGo T-Beam (SX1262)"
maker: lilygo
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: xiao_esp32c3
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio Xiao C3"
maker: seeed
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: xiao_esp32c6/esp32c6/hpcore
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio Xiao ESP32-C6"
maker: seeed
new: true
img: xiao_esp32c6.svg
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: xiao_esp32s3/esp32s3/procpu
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio Xiao S3 WIO"
maker: seeed
+5 -2
View File
@@ -276,8 +276,11 @@ Steps:
2. Copy board.conf and board.overlay from THIS directory
3. Uncomment the sections matching your platform
4. Fill in YOUR pin numbers and partition layout
5. Add board detection to CMakeLists.txt (platform detection block, ~line 270):
Add `BOARD MATCHES "your_board"` to the correct platform line
5. Add a zephcore.yml manifest (copy one from a similar board; schema in
zephcore/scripts/board_manifest.py). No CMake edit is needed: the
boards/<platform>/ directory the board lives in selects its platform.
Leave out `release:` until the board is validated on hardware, then run
`python zephcore/scripts/board_manifest.py check`.
6. Build and iterate!
@@ -0,0 +1,12 @@
# ZephCore native-Linux preset manifest — schema in zephcore/scripts/board_manifest.py
target: native_sim
linux:
host: arm
cross_compile: /usr/bin/arm-linux-gnueabihf-
release:
roles: [companion, repeater]
catalog:
device: "Femtofox (Luckfox Pico Mini)"
maker: femtofox
new: true
img: lora.svg
@@ -0,0 +1,12 @@
# ZephCore native-Linux preset manifest — schema in zephcore/scripts/board_manifest.py
target: native_sim/native/64
linux:
host: aarch64
cross_compile: /usr/bin/aarch64-linux-gnu-
release:
roles: [companion, repeater]
catalog:
device: "RAK6421 WisMesh (Raspberry Pi)"
maker: rak
new: true
img: rpi.svg
@@ -0,0 +1,12 @@
# ZephCore native-Linux preset manifest — schema in zephcore/scripts/board_manifest.py
target: native_sim/native/64
linux:
host: aarch64
cross_compile: /usr/bin/aarch64-linux-gnu-
release:
roles: [companion, repeater]
catalog:
device: "RAK6421 WisMesh (Raspberry Pi 5)"
maker: rak
new: true
img: rpi.svg
@@ -0,0 +1,12 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: xiao_mg24
release:
roles: [companion, repeater]
# Catalog: no USB device peripheral, so no bootloader and no browser-flashable
# path; the .hex is flashed with an external probe (the XIAO's USB-C is a debug
# bridge). MeshCore has no MG24 support, hence a ZephCore-only tile.
catalog:
device: "Seeed Studio Xiao MG24"
maker: seeed
new: true
img: lora.svg
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: gat562_30s
release:
roles: [companion, repeater]
catalog:
device: "GAT-IoT GAT562 30s"
maker: gat-iot
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_t096
release:
roles: [companion, repeater]
catalog:
device: "Heltec Mesh Node T096"
maker: heltec
new: true
img: lora.svg
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_t1
release:
roles: [companion, repeater]
catalog:
device: "Heltec Mesh Node T1"
maker: heltec
@@ -0,0 +1,11 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: heltec_t114
release:
roles: [companion, repeater]
variants:
- id: noscreen
conf: no_display.conf
subtitle: "No screen"
catalog:
device: "Heltec T114"
maker: heltec
@@ -0,0 +1,8 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: ikoka_nano_30dbm
release:
roles: [companion, repeater]
catalog:
device: "Ikoka Nano"
maker: Ikoka
subtitle: "30 dBm"
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: lilygo_techo
release:
roles: [companion, repeater]
catalog:
device: "LilyGo T-Echo"
maker: lilygo
@@ -0,0 +1,9 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: lilygo_timpulse_plus
release:
roles: [companion, repeater]
catalog:
device: "LilyGo T-Impulse Plus"
maker: lilygo
new: true
img: lora.svg
@@ -0,0 +1,14 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: meshtracker_x1
release:
roles: [companion, repeater]
# Catalog: name pre-matched to MeshCore's flasher convention ("Seeed Studio" +
# full product name), so this folds into their tile automatically the moment
# they add one -- the merge is by exact name. Their flasher config has no X1 entry
# yet even though MeshCore firmware supports it, hence new + our own photo.
# WHEN IT APPEARS: drop `new` and `own_img` so their art and tile win.
catalog:
device: "Seeed Studio SenseCAP MeshTracker X1"
maker: seeed
new: true
own_img: meshtracker_x1.jpg
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: muziworks_r1neo
release:
roles: [companion, repeater]
catalog:
device: "muzi works R1 Neo"
maker: muziworks
@@ -0,0 +1,3 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
# Not in the release matrix (bring-up / unvalidated).
target: promicro_lr2021
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: promicro_sx1262
release:
roles: [companion, repeater]
catalog:
device: "ProMicro nrf52 (faketec)"
maker: promicro
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: rak3401_1watt
release:
roles: [companion, repeater]
catalog:
device: "RAK WisMesh 1W Booster (3401 + 13302)"
maker: rak
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: rak4631
release:
roles: [companion, repeater]
catalog:
device: "RAK WisBlock / WisMesh (RAK 4631)"
maker: rak
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: rak_wismesh_tag
release:
roles: [companion, repeater]
catalog:
device: "RAK WisMesh Tag"
maker: rak
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: sensecap_solar
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio SenseCAP Solar"
maker: seeed
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: t1000_e
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio SenseCAP T1000-E"
maker: seeed
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: thinknode_m1
release:
roles: [companion, repeater]
catalog:
device: "Elecrow ThinkNode M1"
maker: elecrow
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: thinknode_m3
release:
roles: [companion, repeater]
catalog:
device: "Elecrow ThinkNode M3"
maker: elecrow
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: thinknode_m6
release:
roles: [companion, repeater]
catalog:
device: "Elecrow ThinkNode M6"
maker: elecrow
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: wio_tracker_l1
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio Wio Tracker L1 Pro"
maker: seeed
@@ -0,0 +1,4 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: wio_tracker_l1_1w
release:
roles: [companion, repeater]
@@ -0,0 +1,7 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: xiao_nrf52840
release:
roles: [companion, repeater]
catalog:
device: "Seeed Studio Xiao nRF52 WIO"
maker: seeed
@@ -0,0 +1,12 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: me25ls02/nrf54l15/cpuapp
release:
roles: [companion, repeater]
# Catalog: the nRF54L15 has no USB peripheral, so there is no bootloader and no
# WebUSB/DFU path -- the Type-C port on the carrier is a CH340x UART bridge. The
# tile publishes the .hex as a plain download, flashed over SWD.
catalog:
device: "MinewSemi ME25LS02"
maker: minewsemi
new: true
img: lora.svg
@@ -0,0 +1,14 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: seeed_lr2021_evk/nrf54l15/cpuapp
release:
roles: [companion, repeater]
# Catalog: same XIAO nRF54L15 host and CMSIS-DAP story as xiao_nrf54l15, but a
# different kit (Semtech LoRa Plus expansion board + Wio-LR2021 module), so a
# separate tile: the radio and the whole D0..D10 wiring differ. Maker is seeed:
# there is no Semtech entry in MAKERS, and before the manifests this device had no
# maker at all, which made the catalog generator fail once its firmware was built.
catalog:
device: "Semtech LR2021 LoRa Plus EVK"
maker: seeed
new: true
img: lora.svg
@@ -0,0 +1,12 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: xiao_nrf54l15/nrf54l15/cpuapp
release:
roles: [companion, repeater]
# Catalog: no USB bootloader (nRF54L15); the XIAO carries a SAMD11 CMSIS-DAP
# bridge, so its own USB cable is enough (openocd/pyocd) -- still not a browser
# flash flow. Download-only tile.
catalog:
device: "Seeed Studio Xiao nRF54L15"
maker: seeed
new: true
img: lora.svg
@@ -0,0 +1,12 @@
# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py
target: lora_e5_mini
release:
roles: [companion, repeater]
# Catalog: no Bluetooth on this SoC -- the companion speaks MeshCore serial
# framing over USART1, bridged to USB-C by the onboard USB-UART chip, so it is
# offered as companionUsb rather than companionBle. SWD-flashed, download-only.
catalog:
device: "Seeed Studio LoRa-E5 mini"
maker: seeed
new: true
img: lora.svg
+91
View File
@@ -0,0 +1,91 @@
# Resolve the ZephCore board directory, the platform, and the build class.
#
# Shared by zephcore/CMakeLists.txt and zephcore/sysbuild/CMakeLists.txt so the
# app and the MCUboot image always agree on which board directory they read.
# Runs before find_package(Zephyr): nothing here may depend on Kconfig or DT.
#
# Inputs: BOARD name[@revision][/soc[/cpucluster]]
# ZEPHCORE_SOURCE_DIR the application source dir
# Outputs: ZEPHCORE_BOARD_BASE board name without revision or qualifiers
# ZEPHCORE_BOARD_DIR boards/<platform-dir>/<board>, or "" if none
# ZEPHCORE_PLATFORM nrf52 | nrf54l | esp32 | mg24 | stm32wl | linux | ""
# ZEPHCORE_BOARD_LIGHT_SLEEP TRUE if the board manifest declares it
# zephcore_board_file(<out-var> <file-name>)
# absolute path of <file-name> inside the board dir, or "".
string(REGEX REPLACE "[@/].*$" "" ZEPHCORE_BOARD_BASE "${BOARD}")
# ---- Board directory -------------------------------------------------------
# boards/<platform-dir>/<board>/. Exactly one may exist: two would make every
# per-board file (conf, overlay, partitions, battery curve) ambiguous, and the
# old "take the first glob match" behaviour picked one silently.
set(ZEPHCORE_BOARD_DIR "")
if(ZEPHCORE_BOARD_BASE)
file(GLOB _zb_candidates LIST_DIRECTORIES true
"${ZEPHCORE_SOURCE_DIR}/boards/*/${ZEPHCORE_BOARD_BASE}")
set(_zb_dirs "")
foreach(_c IN LISTS _zb_candidates)
if(IS_DIRECTORY "${_c}")
list(APPEND _zb_dirs "${_c}")
endif()
endforeach()
list(LENGTH _zb_dirs _zb_count)
if(_zb_count GREATER 1)
message(FATAL_ERROR
"ZephCore: board '${ZEPHCORE_BOARD_BASE}' has ${_zb_count} directories:\n"
" ${_zb_dirs}\nKeep exactly one under boards/<platform>/.")
elseif(_zb_count EQUAL 1)
set(ZEPHCORE_BOARD_DIR "${_zb_dirs}")
endif()
endif()
function(zephcore_board_file OUT NAME)
if(ZEPHCORE_BOARD_DIR AND EXISTS "${ZEPHCORE_BOARD_DIR}/${NAME}")
set(${OUT} "${ZEPHCORE_BOARD_DIR}/${NAME}" PARENT_SCOPE)
else()
set(${OUT} "" PARENT_SCOPE)
endif()
endfunction()
# ---- Platform --------------------------------------------------------------
# The board's directory is its platform. Boards without a ZephCore directory
# (a plain upstream board built for experiments) fall back to the SoC named in
# the board qualifier; native_sim is the Linux port, whose presets are
# boards/linux_native/<preset>.conf rather than board directories.
set(ZEPHCORE_PLATFORM "")
if(ZEPHCORE_BOARD_DIR)
get_filename_component(_zb_parent "${ZEPHCORE_BOARD_DIR}" DIRECTORY)
get_filename_component(_zb_platdir "${_zb_parent}" NAME)
if(_zb_platdir STREQUAL "nrf52840")
set(ZEPHCORE_PLATFORM nrf52)
elseif(_zb_platdir MATCHES "^(nrf54l|esp32|mg24|stm32wl)$")
set(ZEPHCORE_PLATFORM "${_zb_platdir}")
else()
message(FATAL_ERROR "ZephCore: unknown platform directory boards/${_zb_platdir}/")
endif()
elseif(BOARD MATCHES "nrf52")
set(ZEPHCORE_PLATFORM nrf52)
elseif(BOARD MATCHES "esp32")
set(ZEPHCORE_PLATFORM esp32)
elseif(BOARD MATCHES "nrf54l")
set(ZEPHCORE_PLATFORM nrf54l)
elseif(BOARD MATCHES "mg24|efr32")
set(ZEPHCORE_PLATFORM mg24)
elseif(BOARD MATCHES "stm32wl|lora_e5")
set(ZEPHCORE_PLATFORM stm32wl)
elseif(BOARD MATCHES "^native_(sim|posix)")
set(ZEPHCORE_PLATFORM linux)
endif()
# ---- Capabilities from the board manifest ----------------------------------
# zephcore.yml (schema: scripts/board_manifest.py). Read by line match because
# this runs before Zephyr has set up Python; `board_manifest.py check` enforces
# the exact two-space-indented ` <capability>: true|false` form this relies on.
set(ZEPHCORE_BOARD_LIGHT_SLEEP FALSE)
zephcore_board_file(_zb_manifest zephcore.yml)
if(_zb_manifest)
file(STRINGS "${_zb_manifest}" _zb_ls REGEX "^ light_sleep: true[ \t]*$")
if(_zb_ls)
set(ZEPHCORE_BOARD_LIGHT_SLEEP TRUE)
endif()
endif()
+27
View File
@@ -34,4 +34,31 @@ if(EXISTS ${ZEPHCORE_SOURCE_DIR}/patches/zephyr-new)
${ZEPHCORE_ZEPHYR_DIR}/${REL_PATH} COPYONLY)
message(STATUS " [zephyr-new] ${REL_PATH}")
endforeach()
# Remove copies whose source was deleted from patches/zephyr-new. The list
# of what we copied last time lives next to the patch stamps. A file is only
# removed while git reports it untracked in the Zephyr tree: once upstream
# ships a file at the same path (LR11xx is heading that way), it is theirs
# and must be left alone.
set(_zn_manifest "${ZEPHCORE_ZEPHYR_DIR}/.zephcore_new_files.list")
if(EXISTS "${_zn_manifest}")
file(STRINGS "${_zn_manifest}" _zn_previous)
foreach(REL_PATH IN LISTS _zn_previous)
if(REL_PATH IN_LIST ZEPHCORE_NEW_FILES
OR NOT EXISTS "${ZEPHCORE_ZEPHYR_DIR}/${REL_PATH}")
continue()
endif()
execute_process(
COMMAND git ls-files --error-unmatch -- "${REL_PATH}"
WORKING_DIRECTORY "${ZEPHCORE_ZEPHYR_DIR}"
RESULT_VARIABLE _zn_tracked
OUTPUT_QUIET ERROR_QUIET)
if(NOT _zn_tracked EQUAL 0)
file(REMOVE "${ZEPHCORE_ZEPHYR_DIR}/${REL_PATH}")
message(STATUS " [zephyr-new] removed stale ${REL_PATH}")
endif()
endforeach()
endif()
string(REPLACE ";" "\n" _zn_list "${ZEPHCORE_NEW_FILES}")
file(WRITE "${_zn_manifest}" "${_zn_list}\n")
endif()
+6 -11
View File
@@ -174,7 +174,12 @@ function(zephcore_apply_patches PATCH_DIR TARGET_DIR LABEL)
list(APPEND PATCH_PATHS "${_path}")
endforeach()
if(PATCH_PATHS)
message(STATUS " [${LABEL}] Resetting stale files for: ${PATCH_NAME}")
# This discards any uncommitted edits in these files of the
# target tree (e.g. an in-progress driver change not yet
# regenerated into the patch) -- say so loudly.
message(WARNING " [${LABEL}] ${PATCH_NAME} does not apply; "
"resetting these files in ${TARGET_DIR} to HEAD, discarding "
"any local edits in them: ${PATCH_PATHS}")
execute_process(
COMMAND git checkout -- ${PATCH_PATHS}
WORKING_DIRECTORY "${TARGET_DIR}"
@@ -233,13 +238,3 @@ if(EXISTS ${ZEPHCORE_SOURCE_DIR}/patches/modules/loramac-node)
"loramac-node"
)
endif()
# Apply patches to hal_espressif module (ESP32 BLE controller glue)
if(EXISTS ${ZEPHCORE_SOURCE_DIR}/patches/modules/hal-espressif)
message(STATUS "Applying ZephCore patches to hal_espressif...")
zephcore_apply_patches(
"${ZEPHCORE_SOURCE_DIR}/patches/modules/hal-espressif"
"${ZEPHCORE_MODULES_DIR}/hal/espressif"
"hal-espressif"
)
endif()
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""ZephCore board manifests: the one list of boards and what we publish for them.
Every board directory boards/<platform>/<board>/ carries a zephcore.yml; each
native-Linux preset boards/linux_native/<preset>.conf carries a sibling
<preset>.zephcore.yml. Schema (all keys optional unless marked):
target: rak4631 # REQUIRED west -b string, with qualifiers
capabilities:
light_sleep: true # DIO1 on an RTC-wake-capable GPIO AND
# validated on hardware (ESP32 repeaters)
release: # absent = not published (bring-up)
roles: [companion, repeater] # REQUIRED in release
variants: # extra published builds of this board
- id: noscreen # artifact infix
conf: no_display.conf # relative to the board dir
subtitle: No screen # catalog role-row subtitle
catalog: # absent = published but not in the catalog
device: RAK WisBlock / WisMesh (RAK 4631) # REQUIRED in catalog
maker: rak # REQUIRED in catalog, key of MAKERS
new: true # own tile instead of folding into MeshCore's
img: lora.svg | own_img: x.jpg
subtitle: 30 dBm
linux: # native-Linux presets only
host: arm | aarch64
cross_compile: /usr/bin/arm-linux-gnueabihf-
Readers:
- zephcore/CMakeLists.txt reads capabilities.light_sleep with a line match
(it runs before Zephyr has set up Python), so `check` enforces the literal
two-space-indented ` light_sleep: 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)
`board_manifest.py check` validates every manifest; CI should run it.
"""
import os
import re
import sys
import yaml
APP_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
BOARDS_DIR = os.path.join(APP_DIR, "boards")
PLATFORM_DIRS = {"nrf52840": "nrf52", "nrf54l": "nrf54l", "esp32": "esp32",
"mg24": "mg24", "stm32wl": "stm32wl"}
ROLES = ("companion", "repeater", "room_server", "observer")
KEYS = {"target", "capabilities", "release", "catalog", "linux"}
CAPS = {"light_sleep"}
CATALOG_KEYS = {"device", "maker", "new", "img", "own_img", "subtitle"}
CAP_LINE = re.compile(r"^ (\w+): (true|false)\s*$", re.M)
class Board:
def __init__(self, name, platform, manifest_path, data, board_dir=None, conf=None):
self.name = name # board dir name or linux preset name
self.platform = platform # nrf52 | nrf54l | esp32 | mg24 | stm32wl | linux
self.manifest_path = manifest_path
self.board_dir = board_dir # None for linux presets
self.conf = conf # linux preset conf, relative to APP_DIR
self.target = data.get("target")
self.capabilities = data.get("capabilities") or {}
self.release = data.get("release")
self.catalog = data.get("catalog")
self.linux = data.get("linux") or {}
@property
def chip(self):
"""ESP32 chip name from the target (esp32, esp32s3, esp32c3, esp32c6)."""
m = re.search(r"(esp32[^/]*)", self.target or "")
return m.group(1) if m else None
@property
def stem(self):
"""Artifact filename stem, exactly as build.sh has always named files."""
if self.platform == "linux":
return f"zephcore_linux_{self.name}"
return self.target.replace("/", "-")
@property
def sysbuild(self):
"""--sysbuild / --no-sysbuild / None (west default) for release builds."""
if self.platform == "esp32":
return True if self.chip != "esp32" else None # classic ESP32: simple boot
if self.platform == "nrf54l":
return False
return None
def _load(path):
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def load_boards():
boards = []
for plat_dir, platform in PLATFORM_DIRS.items():
root = os.path.join(BOARDS_DIR, plat_dir)
if not os.path.isdir(root):
continue
for name in sorted(os.listdir(root)):
bdir = os.path.join(root, name)
path = os.path.join(bdir, "zephcore.yml")
if os.path.isfile(path):
boards.append(Board(name, platform, path, _load(path), board_dir=bdir))
lroot = os.path.join(BOARDS_DIR, "linux_native")
if os.path.isdir(lroot):
for fn in sorted(os.listdir(lroot)):
if fn.endswith(".zephcore.yml"):
name = fn[: -len(".zephcore.yml")]
boards.append(Board(name, "linux", os.path.join(lroot, fn),
_load(os.path.join(lroot, fn)),
conf=f"boards/linux_native/{name}.conf"))
return boards
def check(boards, makers=None):
errors = []
for b in boards:
where = os.path.relpath(b.manifest_path, APP_DIR)
data = _load(b.manifest_path)
for k in set(data) - KEYS:
errors.append(f"{where}: unknown key '{k}'")
if not b.target:
errors.append(f"{where}: missing target")
elif b.platform != "linux" and not re.match(rf"^{re.escape(b.name)}(@|/|$)", b.target):
errors.append(f"{where}: target '{b.target}' does not name board '{b.name}'")
for k, v in b.capabilities.items():
if k not in CAPS:
errors.append(f"{where}: unknown capability '{k}'")
if not isinstance(v, bool):
errors.append(f"{where}: capability '{k}' must be true/false")
if b.capabilities:
text = open(b.manifest_path, encoding="utf-8").read()
block = re.search(r"^capabilities:\n((?: .*\n?)*)", text, re.M)
found = {m.group(1) for m in CAP_LINE.finditer(block.group(1))} if block else set()
if found != set(b.capabilities):
errors.append(f"{where}: write capabilities as two-space-indented "
f"' <name>: true|false' lines (CMake reads them by line)")
if b.release is not None:
roles = b.release.get("roles") or []
if not roles or any(r not in ROLES for r in roles):
errors.append(f"{where}: release.roles must be a non-empty subset of {ROLES}")
for v in b.release.get("variants") or []:
if not v.get("id") or not v.get("conf"):
errors.append(f"{where}: variant needs id and conf")
elif b.board_dir and not os.path.isfile(os.path.join(b.board_dir, v["conf"])):
errors.append(f"{where}: variant conf '{v['conf']}' not found")
if b.catalog is not None:
for k in set(b.catalog) - CATALOG_KEYS:
errors.append(f"{where}: unknown catalog key '{k}'")
if not b.catalog.get("device") or not b.catalog.get("maker"):
errors.append(f"{where}: catalog needs device and maker")
if makers is not None and b.catalog.get("maker") not in makers:
errors.append(f"{where}: catalog maker '{b.catalog.get('maker')}' not in MAKERS")
if b.release is None:
errors.append(f"{where}: catalog entry without a release")
if b.platform == "linux" and not {"host", "cross_compile"} <= set(b.linux):
errors.append(f"{where}: linux presets need linux.host and linux.cross_compile")
return errors
def matrix(boards, group, role_filter=None):
"""Release builds for one build.sh group: target|role|variant|confs|stem|sysbuild|host|cross."""
want = {"nrf": "nrf52"}.get(group, group)
if role_filter:
role_filter = {"companions": "companion", "repeaters": "repeater"}[role_filter]
rows = []
for b in boards:
if b.platform != want or b.release is None:
continue
sb = {True: "sysbuild", False: "no-sysbuild", None: ""}[b.sysbuild]
base_confs = [b.conf] if b.conf else []
builds = [("", base_confs)]
for v in b.release.get("variants") or []:
rel = os.path.relpath(os.path.join(b.board_dir, v["conf"]), APP_DIR).replace(os.sep, "/")
builds.append((v["id"], base_confs + [rel]))
for role in b.release["roles"]:
if role_filter and role != role_filter:
continue
for variant, confs in builds:
c = list(confs)
if role != "companion":
# Role conf before the variant conf, as build.sh always ordered it.
c = ([c[0]] if b.conf else []) + [f"boards/common/{role}.conf"] + c[1 if b.conf else 0:]
rows.append("|".join([b.target, role, variant, ";".join(c), b.stem, sb,
b.linux.get("host", ""), b.linux.get("cross_compile", "")]))
return rows
DOCS_GROUPS = [("nrf52", "nRF52840"), ("esp32", "ESP32"), ("stm32wl", "STM32WL"),
("mg24", "MG24"), ("nrf54l", "nRF54L")]
def docs(boards, path):
"""Rewrite each '<!-- boards:<platform> -->' ... '<!-- /boards -->' block."""
text = open(path, encoding="utf-8").read()
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)
lines = [b.target if b.release is not None
else f"{b.target:<{width}} # source-only, no published firmware"
for b in group]
block = "<!-- boards:%s -->\n```\n%s\n```\n<!-- /boards -->" % (platform, "\n".join(lines))
text = re.sub(r"<!-- boards:%s -->.*?<!-- /boards -->" % platform, lambda _m: block,
text, flags=re.S)
open(path, "w", encoding="utf-8", newline="\n").write(text)
def main(argv):
boards = load_boards()
cmd = argv[1] if len(argv) > 1 else "check"
if cmd == "check":
errs = check(boards)
for e in errs:
print(e, file=sys.stderr)
print(f"{len(boards)} manifests, {len(errs)} errors")
return 1 if errs else 0
if cmd == "matrix":
for row in matrix(boards, argv[2], argv[3] if len(argv) > 3 else None):
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"))
return 0
print(__doc__)
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Print the flash size of a built image's SoC flash node as '<N>MB'.
Used by build.sh to pass esptool merge-bin a --flash-size that matches the
board, read from the final devicetree rather than duplicated per board.
dts_flash_size.py [path/to/zephyr.dts] (default: build/zephcore/zephyr/zephyr.dts)
"""
import re
import sys
path = sys.argv[1] if len(sys.argv) > 1 else "build/zephcore/zephyr/zephyr.dts"
dts = open(path, encoding="utf-8", errors="replace").read()
def parse_cell(tok: str) -> int:
t = tok.strip()
return int(t, 16) if t.lower().startswith("0x") else int(t)
m = re.search(
r"flash0:\s*flash@[^{]*\{[^}]*?reg\s*=\s*<\s*(?:0x[0-9a-fA-F]+|[0-9]+)\s+"
r"((?:0x)?[0-9a-fA-F]+)\s*>",
dts,
re.DOTALL,
)
if not m:
m = re.search(
r"compatible\s*=\s*\"soc-nv-flash\"\s*;\s*[\s\S]*?"
r"reg\s*=\s*<\s*(?:0x[0-9a-fA-F]+|[0-9]+)\s+((?:0x)?[0-9a-fA-F]+)\s*>",
dts,
)
if not m:
raise SystemExit("flash reg not found in " + path)
size = parse_cell(m.group(1))
print(str(size // 1048576) + "MB")
+4 -3
View File
@@ -1,7 +1,8 @@
# ZephCore Sysbuild Configuration
# Enables MCUboot bootloader for ESP32 OTA firmware updates.
# Only used when building with --sysbuild flag (ESP32 repeater builds).
# nRF52/nRF54L/MG24 builds do NOT use --sysbuild and are unaffected.
# Enables the MCUboot bootloader whenever --sysbuild is passed. Used by the
# ESP32-S3/C-series release builds (companion and repeater: MCUboot @ 0x0, app
# @ 0x10000) and by any build with wifi_ota.conf. Classic ESP32, nRF52, nRF54L,
# MG24 and STM32WL builds do not use --sysbuild.
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_MCUBOOT_MODE_OVERWRITE_ONLY=y
+8 -20
View File
@@ -31,18 +31,12 @@ include("${APP_DIR}/cmake/zephyr_new_files.cmake")
#
# Static conf files (mcuboot_flashsize_*.conf) are used instead of generating
# a temp file to avoid Windows line-ending issues with cmake file(WRITE).
string(REPLACE "/" ";" _board_parts "${BOARD}")
list(GET _board_parts 0 _board_base)
file(GLOB_RECURSE _board_conf_candidates
"${APP_DIR}/boards/*/${_board_base}/board.conf")
if(NOT _board_conf_candidates AND
EXISTS "${APP_DIR}/boards/${_board_base}/board.conf")
set(_board_conf_candidates "${APP_DIR}/boards/${_board_base}/board.conf")
endif()
if(_board_conf_candidates)
list(GET _board_conf_candidates 0 _board_conf)
#
# The board directory comes from the same resolver the app uses, so both images
# always read the same board.
include("${APP_DIR}/cmake/zephcore_board.cmake")
zephcore_board_file(_board_conf board.conf)
if(_board_conf)
file(READ "${_board_conf}" _board_conf_content)
set(_flashsize_fragment "")
@@ -72,14 +66,8 @@ endif()
# uploads report success and then silently boot the old image on reset.
#
# Feeding the same partitions.overlay to MCUboot keeps both flash maps identical.
file(GLOB_RECURSE _board_partitions
"${APP_DIR}/boards/*/${_board_base}/partitions.overlay")
if(NOT _board_partitions AND
EXISTS "${APP_DIR}/boards/${_board_base}/partitions.overlay")
set(_board_partitions "${APP_DIR}/boards/${_board_base}/partitions.overlay")
endif()
if(_board_partitions)
list(GET _board_partitions 0 _board_partitions_file)
zephcore_board_file(_board_partitions_file partitions.overlay)
if(_board_partitions_file)
set(mcuboot_EXTRA_DTC_OVERLAY_FILE "${_board_partitions_file}"
CACHE INTERNAL "MCUboot partitions overlay: shared with the app image")
endif()