diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cf0bc4..2238557 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/build.sh b/build.sh index 85e5bf4..28d6668 100644 --- a/build.sh +++ b/build.sh @@ -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///zephcore.yml and +# zephcore/boards/linux_native/.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 | build.sh esp32 }" +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_--). - 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 diff --git a/docs/supported_boards.md b/docs/supported_boards.md index 674a8cd..a2a4b72 100644 --- a/docs/supported_boards.md +++ b/docs/supported_boards.md @@ -1,33 +1,37 @@ # Supported Boards -Board strings for use with `west build -b zephcore`. +Board strings for use with `west build -b 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 + ``` -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 ``` + > **RAK WisMesh Pocket** (WisBlock pocket): use `-b rak4631` — same board string and firmware as **RAK4631**. > @@ -45,23 +49,26 @@ lilygo_timpulse_plus ## 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 ``` + > 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 + ``` lora_e5_mini ``` + > **Seeed LoRa-E5 mini** (`lora_e5_mini`): STM32WLE5JC with the integrated > SX1262-class sub-GHz radio. No BLE and no USB device — the companion @@ -135,18 +144,23 @@ lora_e5_mini ## MG24 (Silicon Labs) + ``` xiao_mg24 ``` + > Requires `west blobs fetch hal_silabs` and pyocd. ## nRF54L + ``` -xiao_nrf54l15/nrf54l15/cpuapp me25ls02/nrf54l15/cpuapp +seeed_lr2021_evk/nrf54l15/cpuapp +xiao_nrf54l15/nrf54l15/cpuapp ``` + > Requires `--no-sysbuild` flag: `west build -b xiao_nrf54l15/nrf54l15/cpuapp zephcore --no-sysbuild` > diff --git a/gen_provider_catalog.py b/gen_provider_catalog.py index d49fd3a..9682ac0 100644 --- a/gen_provider_catalog.py +++ b/gen_provider_catalog.py @@ -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///zephcore.yml, Linux presets +# zephcore/boards/linux_native/.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"]]: diff --git a/requirements.txt b/requirements.txt index af8cfcf..704ebba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 01edf31..b585eb5 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -28,419 +28,244 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/zephyr_new_files.cmake) # Custom board root: boards/// 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 → _common.conf → /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.conf > boards/_.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 → _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/<...>//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/.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 diff --git a/zephcore/Kconfig b/zephcore/Kconfig index 5428eba..c56ba17 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -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. diff --git a/zephcore/boards/common/packet_logging.conf b/zephcore/boards/common/packet_logging.conf index 439489b..3f9be60 100644 --- a/zephcore/boards/common/packet_logging.conf +++ b/zephcore/boards/common/packet_logging.conf @@ -1,7 +1,6 @@ # Packet Logging Only - Common to Repeater boards # Build: west build -b 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. diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index ada7c24..5370f51 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -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 diff --git a/zephcore/boards/esp32/heltec_wifi_lora32_v3/zephcore.yml b/zephcore/boards/esp32/heltec_wifi_lora32_v3/zephcore.yml new file mode 100644 index 0000000..2ece04c --- /dev/null +++ b/zephcore/boards/esp32/heltec_wifi_lora32_v3/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/heltec_wifi_lora32_v4/zephcore.yml b/zephcore/boards/esp32/heltec_wifi_lora32_v4/zephcore.yml new file mode 100644 index 0000000..ae2c438 --- /dev/null +++ b/zephcore/boards/esp32/heltec_wifi_lora32_v4/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/heltec_wifi_lora32_v43/zephcore.yml b/zephcore/boards/esp32/heltec_wifi_lora32_v43/zephcore.yml new file mode 100644 index 0000000..8ba02e1 --- /dev/null +++ b/zephcore/boards/esp32/heltec_wifi_lora32_v43/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/heltec_wifi_lora32_v4_r8/zephcore.yml b/zephcore/boards/esp32/heltec_wifi_lora32_v4_r8/zephcore.yml new file mode 100644 index 0000000..1cd878d --- /dev/null +++ b/zephcore/boards/esp32/heltec_wifi_lora32_v4_r8/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/heltec_wireless_tracker/zephcore.yml b/zephcore/boards/esp32/heltec_wireless_tracker/zephcore.yml new file mode 100644 index 0000000..3b94055 --- /dev/null +++ b/zephcore/boards/esp32/heltec_wireless_tracker/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/heltec_wireless_tracker_v2/zephcore.yml b/zephcore/boards/esp32/heltec_wireless_tracker_v2/zephcore.yml new file mode 100644 index 0000000..ffe28fb --- /dev/null +++ b/zephcore/boards/esp32/heltec_wireless_tracker_v2/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/lilygo_t3s3/zephcore.yml b/zephcore/boards/esp32/lilygo_t3s3/zephcore.yml new file mode 100644 index 0000000..d427b14 --- /dev/null +++ b/zephcore/boards/esp32/lilygo_t3s3/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/lilygo_tlora_c6/zephcore.yml b/zephcore/boards/esp32/lilygo_tlora_c6/zephcore.yml new file mode 100644 index 0000000..4cb9545 --- /dev/null +++ b/zephcore/boards/esp32/lilygo_tlora_c6/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/meshnology_w12/zephcore.yml b/zephcore/boards/esp32/meshnology_w12/zephcore.yml new file mode 100644 index 0000000..8fbd72a --- /dev/null +++ b/zephcore/boards/esp32/meshnology_w12/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/station_g2/zephcore.yml b/zephcore/boards/esp32/station_g2/zephcore.yml new file mode 100644 index 0000000..744fd22 --- /dev/null +++ b/zephcore/boards/esp32/station_g2/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/thinknode_m9/zephcore.yml b/zephcore/boards/esp32/thinknode_m9/zephcore.yml new file mode 100644 index 0000000..2b08ebc --- /dev/null +++ b/zephcore/boards/esp32/thinknode_m9/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/ttgo_lora32/zephcore.yml b/zephcore/boards/esp32/ttgo_lora32/zephcore.yml new file mode 100644 index 0000000..75e7531 --- /dev/null +++ b/zephcore/boards/esp32/ttgo_lora32/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/ttgo_tbeam/zephcore.yml b/zephcore/boards/esp32/ttgo_tbeam/zephcore.yml new file mode 100644 index 0000000..b13395d --- /dev/null +++ b/zephcore/boards/esp32/ttgo_tbeam/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/xiao_esp32c3/zephcore.yml b/zephcore/boards/esp32/xiao_esp32c3/zephcore.yml new file mode 100644 index 0000000..8fc1f6b --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32c3/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/xiao_esp32c6/zephcore.yml b/zephcore/boards/esp32/xiao_esp32c6/zephcore.yml new file mode 100644 index 0000000..f87715a --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32c6/zephcore.yml @@ -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 diff --git a/zephcore/boards/esp32/xiao_esp32s3/zephcore.yml b/zephcore/boards/esp32/xiao_esp32s3/zephcore.yml new file mode 100644 index 0000000..32f29e6 --- /dev/null +++ b/zephcore/boards/esp32/xiao_esp32s3/zephcore.yml @@ -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 diff --git a/zephcore/boards/example_board/README.md b/zephcore/boards/example_board/README.md index 5103f7c..727b9b4 100644 --- a/zephcore/boards/example_board/README.md +++ b/zephcore/boards/example_board/README.md @@ -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// 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! diff --git a/zephcore/boards/linux_native/femtofox.zephcore.yml b/zephcore/boards/linux_native/femtofox.zephcore.yml new file mode 100644 index 0000000..9dabe2d --- /dev/null +++ b/zephcore/boards/linux_native/femtofox.zephcore.yml @@ -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 diff --git a/zephcore/boards/linux_native/rak6421.zephcore.yml b/zephcore/boards/linux_native/rak6421.zephcore.yml new file mode 100644 index 0000000..666295f --- /dev/null +++ b/zephcore/boards/linux_native/rak6421.zephcore.yml @@ -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 diff --git a/zephcore/boards/linux_native/rak6421_pi5.zephcore.yml b/zephcore/boards/linux_native/rak6421_pi5.zephcore.yml new file mode 100644 index 0000000..4161be7 --- /dev/null +++ b/zephcore/boards/linux_native/rak6421_pi5.zephcore.yml @@ -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 diff --git a/zephcore/boards/mg24/xiao_mg24/zephcore.yml b/zephcore/boards/mg24/xiao_mg24/zephcore.yml new file mode 100644 index 0000000..94618f7 --- /dev/null +++ b/zephcore/boards/mg24/xiao_mg24/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/gat562_30s/zephcore.yml b/zephcore/boards/nrf52840/gat562_30s/zephcore.yml new file mode 100644 index 0000000..ed49713 --- /dev/null +++ b/zephcore/boards/nrf52840/gat562_30s/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/heltec_t096/zephcore.yml b/zephcore/boards/nrf52840/heltec_t096/zephcore.yml new file mode 100644 index 0000000..ff61718 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t096/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/heltec_t1/zephcore.yml b/zephcore/boards/nrf52840/heltec_t1/zephcore.yml new file mode 100644 index 0000000..f68dcaf --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t1/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/heltec_t114/zephcore.yml b/zephcore/boards/nrf52840/heltec_t114/zephcore.yml new file mode 100644 index 0000000..6868001 --- /dev/null +++ b/zephcore/boards/nrf52840/heltec_t114/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/zephcore.yml b/zephcore/boards/nrf52840/ikoka_nano_30dbm/zephcore.yml new file mode 100644 index 0000000..6260bbb --- /dev/null +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/zephcore.yml @@ -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" diff --git a/zephcore/boards/nrf52840/lilygo_techo/zephcore.yml b/zephcore/boards/nrf52840/lilygo_techo/zephcore.yml new file mode 100644 index 0000000..b581802 --- /dev/null +++ b/zephcore/boards/nrf52840/lilygo_techo/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/lilygo_timpulse_plus/zephcore.yml b/zephcore/boards/nrf52840/lilygo_timpulse_plus/zephcore.yml new file mode 100644 index 0000000..081dfb9 --- /dev/null +++ b/zephcore/boards/nrf52840/lilygo_timpulse_plus/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/meshtracker_x1/zephcore.yml b/zephcore/boards/nrf52840/meshtracker_x1/zephcore.yml new file mode 100644 index 0000000..d94c074 --- /dev/null +++ b/zephcore/boards/nrf52840/meshtracker_x1/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/muziworks_r1neo/zephcore.yml b/zephcore/boards/nrf52840/muziworks_r1neo/zephcore.yml new file mode 100644 index 0000000..5dd8036 --- /dev/null +++ b/zephcore/boards/nrf52840/muziworks_r1neo/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/promicro_lr2021/zephcore.yml b/zephcore/boards/nrf52840/promicro_lr2021/zephcore.yml new file mode 100644 index 0000000..ad6c63e --- /dev/null +++ b/zephcore/boards/nrf52840/promicro_lr2021/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/promicro_sx1262/zephcore.yml b/zephcore/boards/nrf52840/promicro_sx1262/zephcore.yml new file mode 100644 index 0000000..0c9832d --- /dev/null +++ b/zephcore/boards/nrf52840/promicro_sx1262/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/rak3401_1watt/zephcore.yml b/zephcore/boards/nrf52840/rak3401_1watt/zephcore.yml new file mode 100644 index 0000000..6d25795 --- /dev/null +++ b/zephcore/boards/nrf52840/rak3401_1watt/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/rak4631/zephcore.yml b/zephcore/boards/nrf52840/rak4631/zephcore.yml new file mode 100644 index 0000000..aac8d47 --- /dev/null +++ b/zephcore/boards/nrf52840/rak4631/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/zephcore.yml b/zephcore/boards/nrf52840/rak_wismesh_tag/zephcore.yml new file mode 100644 index 0000000..0d169c5 --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/sensecap_solar/zephcore.yml b/zephcore/boards/nrf52840/sensecap_solar/zephcore.yml new file mode 100644 index 0000000..2cdfb7c --- /dev/null +++ b/zephcore/boards/nrf52840/sensecap_solar/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/t1000_e/zephcore.yml b/zephcore/boards/nrf52840/t1000_e/zephcore.yml new file mode 100644 index 0000000..1334c2e --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/thinknode_m1/zephcore.yml b/zephcore/boards/nrf52840/thinknode_m1/zephcore.yml new file mode 100644 index 0000000..5aafc2b --- /dev/null +++ b/zephcore/boards/nrf52840/thinknode_m1/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/thinknode_m3/zephcore.yml b/zephcore/boards/nrf52840/thinknode_m3/zephcore.yml new file mode 100644 index 0000000..d78ef51 --- /dev/null +++ b/zephcore/boards/nrf52840/thinknode_m3/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/thinknode_m6/zephcore.yml b/zephcore/boards/nrf52840/thinknode_m6/zephcore.yml new file mode 100644 index 0000000..fee76e1 --- /dev/null +++ b/zephcore/boards/nrf52840/thinknode_m6/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/zephcore.yml b/zephcore/boards/nrf52840/wio_tracker_l1/zephcore.yml new file mode 100644 index 0000000..8985165 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf52840/wio_tracker_l1_1w/zephcore.yml b/zephcore/boards/nrf52840/wio_tracker_l1_1w/zephcore.yml new file mode 100644 index 0000000..9b231f4 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1_1w/zephcore.yml @@ -0,0 +1,4 @@ +# ZephCore board manifest — schema in zephcore/scripts/board_manifest.py +target: wio_tracker_l1_1w +release: + roles: [companion, repeater] diff --git a/zephcore/boards/nrf52840/xiao_nrf52840/zephcore.yml b/zephcore/boards/nrf52840/xiao_nrf52840/zephcore.yml new file mode 100644 index 0000000..5ce4c66 --- /dev/null +++ b/zephcore/boards/nrf52840/xiao_nrf52840/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf54l/me25ls02/zephcore.yml b/zephcore/boards/nrf54l/me25ls02/zephcore.yml new file mode 100644 index 0000000..7725e12 --- /dev/null +++ b/zephcore/boards/nrf54l/me25ls02/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf54l/seeed_lr2021_evk/zephcore.yml b/zephcore/boards/nrf54l/seeed_lr2021_evk/zephcore.yml new file mode 100644 index 0000000..4d29cee --- /dev/null +++ b/zephcore/boards/nrf54l/seeed_lr2021_evk/zephcore.yml @@ -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 diff --git a/zephcore/boards/nrf54l/xiao_nrf54l15/zephcore.yml b/zephcore/boards/nrf54l/xiao_nrf54l15/zephcore.yml new file mode 100644 index 0000000..cb61a03 --- /dev/null +++ b/zephcore/boards/nrf54l/xiao_nrf54l15/zephcore.yml @@ -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 diff --git a/zephcore/boards/stm32wl/lora_e5_mini/zephcore.yml b/zephcore/boards/stm32wl/lora_e5_mini/zephcore.yml new file mode 100644 index 0000000..0aec454 --- /dev/null +++ b/zephcore/boards/stm32wl/lora_e5_mini/zephcore.yml @@ -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 diff --git a/zephcore/cmake/zephcore_board.cmake b/zephcore/cmake/zephcore_board.cmake new file mode 100644 index 0000000..4a1b6e2 --- /dev/null +++ b/zephcore/cmake/zephcore_board.cmake @@ -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//, 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( ) +# absolute path of inside the board dir, or "". + +string(REGEX REPLACE "[@/].*$" "" ZEPHCORE_BOARD_BASE "${BOARD}") + +# ---- Board directory ------------------------------------------------------- +# boards///. 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//.") + 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/.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 ` : 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() diff --git a/zephcore/cmake/zephyr_new_files.cmake b/zephcore/cmake/zephyr_new_files.cmake index f749877..b233b8b 100644 --- a/zephcore/cmake/zephyr_new_files.cmake +++ b/zephcore/cmake/zephyr_new_files.cmake @@ -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() diff --git a/zephcore/cmake/zephyr_patches.cmake b/zephcore/cmake/zephyr_patches.cmake index 4f48f77..9ed3cca 100644 --- a/zephcore/cmake/zephyr_patches.cmake +++ b/zephcore/cmake/zephyr_patches.cmake @@ -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() diff --git a/zephcore/scripts/board_manifest.py b/zephcore/scripts/board_manifest.py new file mode 100644 index 0000000..39c4b56 --- /dev/null +++ b/zephcore/scripts/board_manifest.py @@ -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/// carries a zephcore.yml; each +native-Linux preset boards/linux_native/.conf carries a sibling +.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 [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"' : 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 '' ... '' 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 = "\n```\n%s\n```\n" % (platform, "\n".join(lines)) + text = re.sub(r".*?" % 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)) diff --git a/zephcore/scripts/dts_flash_size.py b/zephcore/scripts/dts_flash_size.py new file mode 100644 index 0000000..03dbfc9 --- /dev/null +++ b/zephcore/scripts/dts_flash_size.py @@ -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 '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") diff --git a/zephcore/sysbuild.conf b/zephcore/sysbuild.conf index d521c5e..eaa8466 100644 --- a/zephcore/sysbuild.conf +++ b/zephcore/sysbuild.conf @@ -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 diff --git a/zephcore/sysbuild/CMakeLists.txt b/zephcore/sysbuild/CMakeLists.txt index b4c5988..17cd1e3 100644 --- a/zephcore/sysbuild/CMakeLists.txt +++ b/zephcore/sysbuild/CMakeLists.txt @@ -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()