diff --git a/core/archipelago/src/mesh/flash.rs b/core/archipelago/src/mesh/flash.rs index fc2f2931..607f9f7a 100644 --- a/core/archipelago/src/mesh/flash.rs +++ b/core/archipelago/src/mesh/flash.rs @@ -686,17 +686,13 @@ const ESPTOOL_CHIP: &str = "esp32s3"; /// first hiccup, retry once at a conservative baud before giving up. const ESPTOOL_FALLBACK_BAUD: &str = "115200"; -/// A standalone `erase_flash` (full-chip erase via a single ROM opcode) does -/// NOT work with --no-stub — confirmed live 2026-07-23 against a real -/// Heltec V4: "A fatal error occurred: ESP32-S3 ROM does not support -/// function erase_flash." The ESP32-S3 ROM bootloader only implements -/// per-sector erase (used internally as write_flash goes), not a bulk -/// full-chip erase opcode — that's a stub-firmware-only feature, and -/// Debian's esptool package has no stub (see esptool_global_args' doc -/// comment). `write_flash --erase-all` gets the same "erase everything -/// first" outcome our "always erase before write" default requires, but via -/// the per-sector mechanism the ROM bootloader actually supports, as one -/// esptool invocation instead of two. +/// `write_flash --erase-all` erases the whole chip before writing, in one +/// esptool invocation. This needs the esp32s3 stub flasher loaded (see +/// esptool_global_args' doc comment) — without it, --erase-all hits the +/// exact same ROM limitation a standalone `erase_flash` does ("ESP32-S3 ROM +/// does not support function erase_flash", confirmed live 2026-07-23), since +/// esptool's --erase-all is implemented as the same full-chip-erase command, +/// not a per-sector loop. async fn esptool_erase_and_write(path: &str, image: &Path, job: &Arc) -> Result<()> { job.set_stage(FlashStage::Writing).await; let image_str = image.to_string_lossy().to_string(); @@ -711,28 +707,33 @@ async fn esptool_erase_and_write(path: &str, image: &Path, job: &Arc) Ok(()) } -/// esptool's global flags (--chip/--port/--baud/--no-stub) MUST precede the -/// subcommand token (erase_flash/write_flash/...) — confirmed live -/// 2026-07-23: appending `--baud 115200` after the subcommand on the retry -/// path produced "esptool: error: unrecognized arguments: --baud 115200" -/// every time, so the fallback-baud retry never actually got a chance to -/// run. Building global args separately from subcommand args keeps this -/// correct by construction instead of relying on call-site ordering. +/// esptool's global flags (--chip/--port/--baud) MUST precede the subcommand +/// token (erase_flash/write_flash/...) — confirmed live 2026-07-23: +/// appending `--baud 115200` after the subcommand on the retry path +/// produced "esptool: error: unrecognized arguments: --baud 115200" every +/// time, so the fallback-baud retry never actually got a chance to run. +/// Building global args separately from subcommand args keeps this correct +/// by construction instead of relying on call-site ordering. /// -/// --no-stub is not optional here: Debian's `esptool` package -/// (4.7.0+dfsg-0.1) ships without the precompiled "stub flasher" blobs -/// (stripped for DFSG compliance), so the default stub-loader flash mode -/// fails immediately with `FileNotFoundError: ... stub_flasher_32s3.json` -/// — also confirmed live. --no-stub talks directly to the ROM bootloader -/// instead, which doesn't need those files (slower, but actually works on -/// this packaging). +/// Normal stub-loader mode (no --no-stub) needs the esp32s3 stub flasher +/// blob at /usr/lib/python3/dist-packages/esptool/targets/stub_flasher/ +/// stub_flasher_32s3.json — Debian's `esptool` package (4.7.0+dfsg-0.1) +/// ships without it (stripped for DFSG compliance: the prebuilt blob has no +/// buildable-from-source path Debian could verify), so scripts/self-update.sh +/// fetches the exact same file from the matching upstream esptool release +/// tag and installs it alongside the apt package (see the esptool install +/// step there). --no-stub (talk directly to the ROM bootloader, skip the +/// stub) was tried first and works for connecting, but the ROM bootloader +/// doesn't implement a full-chip-erase opcode at all — only the stub does — +/// so --no-stub broke our "always erase before write" default outright +/// rather than just being slower. Restoring the real stub file is the +/// correct fix, not routing around its absence. fn esptool_global_args<'a>(path: &'a str, baud: Option<&'a str>) -> Vec<&'a str> { let mut args = vec!["--chip", ESPTOOL_CHIP, "--port", path]; if let Some(b) = baud { args.push("--baud"); args.push(b); } - args.push("--no-stub"); args } diff --git a/scripts/self-update.sh b/scripts/self-update.sh index 3bf13471..99ea24f6 100755 --- a/scripts/self-update.sh +++ b/scripts/self-update.sh @@ -102,6 +102,34 @@ if ! command -v esptool >/dev/null 2>&1; then fi fi +# Debian's esptool package (4.7.0+dfsg-0.1) ships without the precompiled +# esp32s3 "stub flasher" blob (stripped for DFSG compliance — no +# buildable-from-source path Debian could verify). Without it, esptool's +# normal stub-loader mode fails outright (FileNotFoundError), and the ROM +# bootloader fallback (--no-stub) doesn't implement a full-chip erase at +# all — confirmed live 2026-07-23 flashing a real Heltec V4, both ways. +# Fetching the exact same file from the matching upstream esptool release +# tag restores full (and correct) flashing behavior — it's the same +# open-source codebase, just the one blob Debian's packaging couldn't +# include. +if command -v esptool >/dev/null 2>&1; then + STUB_DIR="/usr/lib/python3/dist-packages/esptool/targets/stub_flasher" + STUB_FILE="$STUB_DIR/stub_flasher_32s3.json" + if [ ! -f "$STUB_FILE" ]; then + log "Fetching esptool's esp32s3 stub flasher (missing from the Debian package)..." + ESPTOOL_VERSION=$(esptool version 2>/dev/null | tail -1 | tr -d ' \t') + if [ -n "$ESPTOOL_VERSION" ] && sudo curl -fsSL -o "$STUB_FILE" \ + "https://raw.githubusercontent.com/espressif/esptool/v${ESPTOOL_VERSION}/esptool/targets/stub_flasher/stub_flasher_32s3.json" \ + 2>>"$LOG_FILE"; then + sudo chmod 644 "$STUB_FILE" + ok "esp32s3 stub flasher installed" + else + sudo rm -f "$STUB_FILE" 2>/dev/null + warn "Unable to fetch esp32s3 stub flasher; LoRa firmware flashing will be unavailable" + fi + fi +fi + # Build-time prerequisites for reticulum-daemon/build.sh's PyInstaller step # below (discovered the hard way: ensurepip needs python3-venv, and # PyInstaller itself needs objdump + libpython3.13.so at build time — none