diff --git a/.gitignore b/.gitignore index 915f44e..4cae925 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,11 @@ rtt_*.log *.a *.zip +# ...except the prebuilt formatter DFU packages. These are shipped artifacts, not +# build output: build.sh publishes them as the Mesh America configurator's `erase` +# packages (see formatter/README.md, PROVIDER_CATALOG.md), so they must be tracked. +!formatter/*.zip + # OS junk .DS_Store Thumbs.db diff --git a/PROVIDER_CATALOG.md b/PROVIDER_CATALOG.md index 56a935c..e436f92 100644 --- a/PROVIDER_CATALOG.md +++ b/PROVIDER_CATALOG.md @@ -65,19 +65,21 @@ Notes on the mapping: boards without dedicated art fall back to a neutral LoRa icon. Swap in ZephCore-branded art by pointing `img` at your own absolute HTTPS URLs. -> [!WARNING] -> **The configurator's "Erase Flash" is not ZephCore-aware — do not use it on a ZephCore node.** -> `erase` is a MeshCore device-level field, not a provider field, so a folded nRF52 device -> inherits the *base* device's formatter (e.g. Wio Tracker L1 → `WioTrackerL1_QSPIFlash_Format`). -> That formatter targets MeshCore/Ripple's flash layout, which does not match ZephCore's -> LittleFS — so it performs a partial, inconsistent wipe (observed: nukes `channels`, leaves -> identity/prefs/contacts) and can leave the filesystem half-corrupted. New tiles inherit no -> `erase` at all, so Erase Flash is a no-op there. -> -> To factory-reset a ZephCore node, use ZephCore's own mechanism, which formats **all** of -> `/lfs` + `/ext` (and the NVS bond partition) and reboots: the `erase` command over the USB -> serial CLI, the companion app's factory reset, or simply reflash (ZephCore auto-formats on -> first boot when it detects an incompatible/blank FS). +- **Erase / "wipe settings" (nRF52) uses ZephCore's own formatter.** MeshCore's official erase + targets a different flash layout and only *partially* wipes a ZephCore node (observed on a Wio + Tracker L1: nuked `channels`, left identity/prefs/contacts, LittleFS half-corrupted). Since spec + §4a we override it: each nRF52 firmware option carries an `erase` pointing at ZephCore's + formatter. The formatter is **SoftDevice-specific** (v6 and v7 have different partition maps), so + `SOFTDEVICE` in the generator maps every nRF52 board to `SoftDevice_v6_formatter.zip` or + `SoftDevice_v7_formatter.zip`; `build.sh` publishes both from `formatter/` under stable, un-hashed + names so the erase URLs never change. The `.zip` drives the automated DFU erase flow; the `.uf2` + is published too as the manual drag-and-drop fallback. + - `erase` goes on the **firmware option**, never the device object — a device-level `erase` is an + official-only field that providers get silently ignored + warned for (§4a). + - ESP32 has no `erase` (the field is nRF52-only); `flash-wipe` already reinstalls cleanly. + - Users can still factory-reset on-device without the flasher: the `erase` command over the USB + serial CLI, the app's factory reset, or a reflash (ZephCore auto-formats on first boot when it + detects an incompatible/blank FS). All format `/lfs` + `/ext` + the NVS bond partition. ## Local test diff --git a/build.sh b/build.sh index a7006b7..9c888ab 100644 --- a/build.sh +++ b/build.sh @@ -82,6 +82,26 @@ if [[ $1 == "nrf" ]]; then 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 fi if [[ $1 == "linux" ]]; then diff --git a/formatter/README.md b/formatter/README.md index 0edbd0b..36c6297 100644 --- a/formatter/README.md +++ b/formatter/README.md @@ -1,7 +1,50 @@ -nRF formatter tools - -- QSPI is formatted for all supported boards -- Watch out for softdevice version! Flashing the wrong version can corrupt the node and you'll need a full bootloader reflash with adafruit-nrfutil! - - You can check what softdevice version you use if you open INFO_UF2.TXT on the storage drive when in DFU mode. Bootloader should say "sxxx 6.x.x" for v6 and "sxxx 7.x.x" for v7 -- Formatter output logs from the process over serial -- After format, it puts back the device to Mass Storage DFU mode +nRF formatter tools + +- QSPI is formatted for all supported boards +- Watch out for softdevice version! Flashing the wrong version can corrupt the node and you'll need a full bootloader reflash with adafruit-nrfutil! + - You can check what softdevice version you use if you open INFO_UF2.TXT on the storage drive when in DFU mode. Bootloader should say "sxxx 6.x.x" for v6 and "sxxx 7.x.x" for v7 + - That warning applies to the **`.uf2`** files, which have no version guard. The **`.zip`** packages carry an `--sd-req` guard (v6 = `0x00B6`, v7 = `0x0123`), so the bootloader *rejects* a mismatched package instead of corrupting the node. +- Formatter output logs from the process over serial +- After format, it puts back the device to Mass Storage DFU mode + +## Files + +| SoftDevice | Boards | +|------------|--------| +| **v6** | RAK4631, RAK3401 1W, ThinkNode M1/M3/M6, RAK WisMesh Tag, LilyGo T-Echo, LilyGo T-Impulse Plus, ProMicro SX1262, Heltec T114, Heltec T096, GAT562 30s | +| **v7** | Wio Tracker L1, T1000-E, Ikoka Nano 30dBm, SenseCAP Solar, XIAO nRF52840 | + +- **`.uf2`** — manual drag-and-drop onto the UF2 mass-storage drive. +- **`.zip`** — Adafruit DFU package. Used by the Mesh America configurator as ZephCore's + `erase` package (its automated erase flow), and flashable by hand with + `adafruit-nrfutil dfu serial -pkg -p COMx -b 115200 --singlebank --touch 1200`. + See `PROVIDER_CATALOG.md` for how the catalog wires these up per board. + +`build.sh nrf` copies all four files into `firmware/` under these exact names so the +catalog's `erase` URLs stay stable across releases — don't rename them. + +## Rebuilding + +Two universal builds, one per SoftDevice. Source is `zephcore/tools/formatter`. The +partition map comes from the build board's overlay, and `qspi_probe.c` probes QSPI +bare-metal, so one image covers every board on that SoftDevice. + +```bash +# SoftDevice v6 (universal target: rak4631) +west build -b rak4631 zephcore/tools/formatter --pristine -d build_fmt6 +cp build_fmt6/zephyr/zephyr.uf2 formatter/SoftDevice_v6_formatter.uf2 +cp build_fmt6/zephyr/zephyr.zip formatter/SoftDevice_v6_formatter.zip + +# SoftDevice v7 (universal target: t1000_e) +west build -b t1000_e zephcore/tools/formatter --pristine -d build_fmt7 +cp build_fmt7/zephyr/zephyr.uf2 formatter/SoftDevice_v7_formatter.uf2 +cp build_fmt7/zephyr/zephyr.zip formatter/SoftDevice_v7_formatter.zip +``` + +The `.zip` needs `adafruit-nrfutil` on PATH (`pip install adafruit-nrfutil`); the build +prints `Formatter DFU zip: ENABLED (sd-req=...)` when wired up, and silently skips the +zip otherwise. `--sd-req` is read automatically from the build board's `board.conf` +(`CONFIG_ZEPHCORE_SD_FWID`), so it always matches the SoftDevice being targeted. + +To support a new board, add its QSPI pin mapping to `known_boards[]` in +`zephcore/tools/formatter/src/qspi_probe.c` — no DTS or Kconfig changes needed. diff --git a/formatter/SoftDevice_v6_formatter.uf2 b/formatter/SoftDevice_v6_formatter.uf2 index d28d086..010c40c 100644 Binary files a/formatter/SoftDevice_v6_formatter.uf2 and b/formatter/SoftDevice_v6_formatter.uf2 differ diff --git a/formatter/SoftDevice_v6_formatter.zip b/formatter/SoftDevice_v6_formatter.zip new file mode 100644 index 0000000..1215bbb Binary files /dev/null and b/formatter/SoftDevice_v6_formatter.zip differ diff --git a/formatter/SoftDevice_v7_formatter.uf2 b/formatter/SoftDevice_v7_formatter.uf2 index b7872d3..3f0b4d8 100644 Binary files a/formatter/SoftDevice_v7_formatter.uf2 and b/formatter/SoftDevice_v7_formatter.uf2 differ diff --git a/formatter/SoftDevice_v7_formatter.zip b/formatter/SoftDevice_v7_formatter.zip new file mode 100644 index 0000000..52d3f98 Binary files /dev/null and b/formatter/SoftDevice_v7_formatter.zip differ diff --git a/gen_provider_catalog.py b/gen_provider_catalog.py index b4de921..8d8e140 100644 --- a/gen_provider_catalog.py +++ b/gen_provider_catalog.py @@ -175,6 +175,21 @@ BOARDS = [ DEVICE_TYPE = {"nrf": "nrf52", "esp32": "esp32", "linux": "noflash"} +# nRF52 erase package (spec §4a). ZephCore's LittleFS layout differs from MeshCore's, +# so the official erase would wipe the wrong region — we point `erase` at ZephCore's +# 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, "gat562_30s": 6, + "wio_tracker_l1": 7, "t1000_e": 7, "ikoka_nano_30dbm": 7, + "sensecap_solar": 7, "xiao_nrf52840": 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 # Linux, TCP). Expose it under each applicable role, all pointing at one file. COMPANION_ROLES = { @@ -241,10 +256,12 @@ def build(assets, url_base, version): for (t, name, title) in triples ] - def add_option(dev, role, subtitle, version, triples): + def add_option(dev, role, subtitle, version, triples, erase=None): opt = {"role": role, "version": {version: {"notes": "", "files": spec_files(triples)}}} if subtitle: opt["subTitle"] = subtitle + if erase: + opt["erase"] = erase # spec §4a: on the firmware option, not the device dev["firmware"].append(opt) used_roles.add(role) @@ -275,11 +292,16 @@ def build(assets, url_base, version): stats["new" if board.get("new") else "fold"] += 1 dev = devices[name] + # 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']]]}" + if comp: for role in COMPANION_ROLES[board["kind"]]: - add_option(dev, role, subtitle, version, comp) + add_option(dev, role, subtitle, version, comp, erase) if repe: - add_option(dev, "repeater", subtitle, version, repe) + add_option(dev, "repeater", subtitle, version, repe, erase) catalog = { "description": DESCRIPTION, diff --git a/zephcore/tools/formatter/CMakeLists.txt b/zephcore/tools/formatter/CMakeLists.txt index 842ba9a..7a7658d 100644 --- a/zephcore/tools/formatter/CMakeLists.txt +++ b/zephcore/tools/formatter/CMakeLists.txt @@ -54,7 +54,43 @@ endif() message(STATUS "Formatter board overlay: ${FORMATTER_BOARD_OVERLAY}") message(STATUS "Formatter board conf: ${FMT_BOARD_CONF}") +# Read the SoftDevice firmware ID from the MAIN board.conf for this board. +# The Adafruit bootloader rejects a DFU package whose --sd-req doesn't match the +# SoftDevice it's running, which is exactly the guard we want: the SDv6 formatter +# must never apply to an SDv7 node (wrong partition map = corrupted node). +# SoftDevice v6 -> 0x00B6 SoftDevice v7 -> 0x0123 +file(GLOB_RECURSE MAIN_BOARD_CONF_CANDIDATES + "${CMAKE_CURRENT_SOURCE_DIR}/../../boards/*/${BOARD_BASE}/board.conf") +if(MAIN_BOARD_CONF_CANDIDATES) + list(GET MAIN_BOARD_CONF_CANDIDATES 0 MAIN_BOARD_CONF) + file(STRINGS "${MAIN_BOARD_CONF}" _sd_fwid_line REGEX "^CONFIG_ZEPHCORE_SD_FWID=") + if(_sd_fwid_line) + string(REGEX REPLACE "^CONFIG_ZEPHCORE_SD_FWID=" "" FORMATTER_SD_REQ "${_sd_fwid_line}") + endif() +endif() + find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(zephcore_formatter) target_sources(app PRIVATE src/main.c src/qspi_probe.c) + +# Generate an Adafruit DFU package (.zip) next to the .uf2. The .zip is what the +# Mesh America configurator uses for its automated `erase` flow (spec §4a); the +# .uf2 remains the manual drag-and-drop fallback. See PROVIDER_CATALOG.md. +find_program(ADAFRUIT_NRFUTIL adafruit-nrfutil) +if(ADAFRUIT_NRFUTIL AND FORMATTER_SD_REQ) + add_custom_target(dfu_zip ALL + COMMAND ${ADAFRUIT_NRFUTIL} dfu genpkg + --dev-type 0x0052 + --sd-req ${FORMATTER_SD_REQ} + --application ${CMAKE_BINARY_DIR}/zephyr/zephyr.hex + ${CMAKE_BINARY_DIR}/zephyr/zephyr.zip + DEPENDS ${CMAKE_BINARY_DIR}/zephyr/zephyr.hex + COMMENT "Generating formatter DFU package: zephyr.zip (sd-req=${FORMATTER_SD_REQ})" + VERBATIM + ) + add_dependencies(dfu_zip zephyr_final) + message(STATUS "Formatter DFU zip: ENABLED (sd-req=${FORMATTER_SD_REQ})") +else() + message(STATUS "Formatter DFU zip: DISABLED (adafruit-nrfutil or SD_FWID missing)") +endif()