Files

380 lines
16 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
mkdir -p firmware
COMMIT_HASH=$(git rev-parse --short HEAD)
nRF_boards=(
rak4631
rak3401_1watt
wio_tracker_l1
t1000_e
meshtracker_x1
thinknode_m1
thinknode_m3
thinknode_m6
rak_wismesh_tag
ikoka_nano_30dbm
sensecap_solar
xiao_nrf52840
lilygo_techo
lilygo_timpulse_plus
promicro_sx1262
heltec_t114
heltec_t096
gat562_30s
muziworks_r1neo
)
# 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
)
# 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
)
# 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
)
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_wireless_tracker/esp32s3/procpu
heltec_wireless_tracker_v2/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
# 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.
for sd in 6 7; do
for ext in zip uf2; do
f="formatter/SoftDevice_v${sd}_formatter.${ext}"
if [[ -f "$f" ]]; then
cp "$f" firmware/
echo "Published formatter: $f"
else
echo "NOTE: $f not present — erase package for SoftDevice v${sd} will 404"
fi
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.
if compgen -G "img/*" > /dev/null; then
cp img/* firmware/
echo "Published device art: $(ls img/ | tr '\n' ' ')"
fi
fi
if [[ $1 == "nrf54l" ]]; then
for board in "${nRF54L_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
echo "Now building $board companion"
west build -b "$board" zephcore --pristine --no-sysbuild
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH".hex
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine --no-sysbuild -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH".hex
done
fi
# Silicon Labs EFR32MG24. Needs the Silabs BLE controller blob (west blobs fetch
# hal_silabs) before the first build -- CI fetches it in the build-swd job.
# Flashed over SWD with pyocd/J-Link; the XIAO's USB-C is a debug bridge, not a
# device port, so there is no browser-flashable path.
if [[ $1 == "mg24" ]]; then
for board in "${MG24_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
echo "Now building $board companion"
west build -b "$board" zephcore --pristine
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH".hex
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH".hex
done
fi
# STM32WL. No Bluetooth and no USB device: the companion speaks MeshCore serial
# framing over USART1 (bridged to USB-C by the onboard USB-UART chip) and the
# repeater uses the same UART for its CLI. Flashed over SWD/ST-Link.
if [[ $1 == "stm32wl" ]]; then
for board in "${STM32WL_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
echo "Now building $board companion"
west build -b "$board" zephcore --pristine
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-companion-"$COMMIT_HASH".hex
echo "Now building $board repeater"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
mv build/zephyr/zephyr.hex firmware/"$board_clean_for_path"-repeater-"$COMMIT_HASH".hex
done
fi
if [[ $1 == "linux" ]]; then
for board in "${Linux_boards[@]}"; do
# Pick the native_sim variant + cross toolchain for the target SBC arch.
case "$board" in
femtofox)
# Luckfox Pico Mini — ARMv7-A (32-bit) → native_sim (32-bit)
zboard="native_sim"
host="arm"
cross="/usr/bin/arm-linux-gnueabihf-"
;;
rak6421|rak6421_pi5)
# Raspberry Pi — aarch64 (64-bit) → native_sim/native/64
zboard="native_sim/native/64"
host="aarch64"
cross="/usr/bin/aarch64-linux-gnu-"
;;
*)
echo "Unknown linux board: $board"
exit 1
;;
esac
# build native-Linux companion (TCP transport — the default role)
echo "Now building $board companion (native linux)"
west build -b "$zboard" zephcore --pristine -- \
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
-DNATIVE_TARGET_HOST="$host" \
-DCROSS_COMPILE="$cross" \
-DEXTRA_CONF_FILE="boards/linux_native/$board.conf"
# native_sim emits zephcore_native_linux.exe; ship it as an extension-less
# per-board name (zephcore_linux_<board>-<role>-<hash>).
mv build/zephyr/zephcore_native_linux.exe firmware/zephcore_linux_"$board"-companion-"$COMMIT_HASH"
# build native-Linux repeater
echo "Now building $board repeater (native linux)"
west build -b "$zboard" zephcore --pristine -- \
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
-DNATIVE_TARGET_HOST="$host" \
-DCROSS_COMPILE="$cross" \
-DEXTRA_CONF_FILE="boards/linux_native/$board.conf;boards/common/repeater.conf"
mv build/zephyr/zephcore_native_linux.exe firmware/zephcore_linux_"$board"-repeater-"$COMMIT_HASH"
done
fi
if [[ $1 == "esp32" ]]; then
for board in "${ESP32_boards[@]}"; do
board_clean_for_path=$(echo "$board" | sed -e 's/\//-/g')
if [[ $board =~ (esp32[^/]*) ]]; then
chip="${BASH_REMATCH[1]}"
else
echo "Unknown chip for: $board"
exit 1;
fi
# Classic ESP32 (e.g. T-Beam, PICO-D4) uses Zephyr's simple-boot path for
# both roles: a self-contained zephyr.bin flashed at the 0x1000 ROM
# bootloader offset. The companion keeps BLE (whose controller reserves
# ~50KB DRAM, leaving no room for MCUboot) and the repeater is CLI-only
# (WiFi OTA's functional driver + 64KB heap overflow DRAM by ~10KB). The
# S3/C-series have the DRAM headroom and use sysbuild + MCUboot below.
if [[ $chip == "esp32" ]]; then
if [[ $2 == "companions" ]]; then
role="companion"
echo "Now building $board companion (simple boot)"
west build -b "$board" zephcore --pristine
elif [[ $2 == "repeaters" ]]; then
role="repeater"
echo "Now building $board repeater (simple boot)"
west build -b "$board" zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
else
continue
fi
# Simple-boot zephyr.bin is already the complete bootable image;
# wrap it in a full-flash merged image at the 0x1000 offset.
python -m esptool --chip "$chip" merge-bin \
--output firmware/"$board_clean_for_path"-"$role"-"$COMMIT_HASH"-merged.bin \
--flash-mode dio --flash-freq 40m --flash-size 4MB \
0x1000 build/zephyr/zephyr.bin
cp build/zephyr/zephyr.bin firmware/"$board_clean_for_path"-"$role"-"$COMMIT_HASH".bin
continue
fi
if [[ $2 == "companions" ]]; then
# build ESP32 companions (production is the default)
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