fix(mesh): restore esptool's esp32s3 stub instead of routing around it

--erase-all hit the exact same "ROM does not support function erase_flash"
error as a standalone erase_flash — confirmed live: esptool's --erase-all
is implemented as the same full-chip-erase command, not a per-sector loop,
so it needs the stub just as much. The real fix isn't finding another way
around the ROM bootloader's limitations — it's restoring the stub Debian's
package is missing.

Fetched the exact missing file (stub_flasher_32s3.json) from the matching
upstream esptool release tag and installed it directly on archy-x250-dev;
verified live with a read-only `flash_id` that stub mode now loads
("Uploading stub... Running stub..."). Removed --no-stub from flash.rs now
that normal stub-loader mode works correctly (faster and fully-featured vs.
the ROM-only fallback). scripts/self-update.sh now fetches and installs
this same file automatically whenever it's missing, so this isn't a
one-off manual fix tied to a single node.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssmithx 2026-07-23 13:12:15 +00:00
parent 0ca8f25b1b
commit d6019e47a5
2 changed files with 55 additions and 26 deletions

View File

@ -686,17 +686,13 @@ const ESPTOOL_CHIP: &str = "esp32s3";
/// first hiccup, retry once at a conservative baud before giving up. /// first hiccup, retry once at a conservative baud before giving up.
const ESPTOOL_FALLBACK_BAUD: &str = "115200"; const ESPTOOL_FALLBACK_BAUD: &str = "115200";
/// A standalone `erase_flash` (full-chip erase via a single ROM opcode) does /// `write_flash --erase-all` erases the whole chip before writing, in one
/// NOT work with --no-stub — confirmed live 2026-07-23 against a real /// esptool invocation. This needs the esp32s3 stub flasher loaded (see
/// Heltec V4: "A fatal error occurred: ESP32-S3 ROM does not support /// esptool_global_args' doc comment) — without it, --erase-all hits the
/// function erase_flash." The ESP32-S3 ROM bootloader only implements /// exact same ROM limitation a standalone `erase_flash` does ("ESP32-S3 ROM
/// per-sector erase (used internally as write_flash goes), not a bulk /// does not support function erase_flash", confirmed live 2026-07-23), since
/// full-chip erase opcode — that's a stub-firmware-only feature, and /// esptool's --erase-all is implemented as the same full-chip-erase command,
/// Debian's esptool package has no stub (see esptool_global_args' doc /// not a per-sector loop.
/// 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.
async fn esptool_erase_and_write(path: &str, image: &Path, job: &Arc<FlashJob>) -> Result<()> { async fn esptool_erase_and_write(path: &str, image: &Path, job: &Arc<FlashJob>) -> Result<()> {
job.set_stage(FlashStage::Writing).await; job.set_stage(FlashStage::Writing).await;
let image_str = image.to_string_lossy().to_string(); 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<FlashJob>)
Ok(()) Ok(())
} }
/// esptool's global flags (--chip/--port/--baud/--no-stub) MUST precede the /// esptool's global flags (--chip/--port/--baud) MUST precede the subcommand
/// subcommand token (erase_flash/write_flash/...) — confirmed live /// token (erase_flash/write_flash/...) — confirmed live 2026-07-23:
/// 2026-07-23: appending `--baud 115200` after the subcommand on the retry /// appending `--baud 115200` after the subcommand on the retry path
/// path produced "esptool: error: unrecognized arguments: --baud 115200" /// produced "esptool: error: unrecognized arguments: --baud 115200" every
/// every time, so the fallback-baud retry never actually got a chance to /// time, so the fallback-baud retry never actually got a chance to run.
/// run. Building global args separately from subcommand args keeps this /// Building global args separately from subcommand args keeps this correct
/// correct by construction instead of relying on call-site ordering. /// by construction instead of relying on call-site ordering.
/// ///
/// --no-stub is not optional here: Debian's `esptool` package /// Normal stub-loader mode (no --no-stub) needs the esp32s3 stub flasher
/// (4.7.0+dfsg-0.1) ships without the precompiled "stub flasher" blobs /// blob at /usr/lib/python3/dist-packages/esptool/targets/stub_flasher/
/// (stripped for DFSG compliance), so the default stub-loader flash mode /// stub_flasher_32s3.json — Debian's `esptool` package (4.7.0+dfsg-0.1)
/// fails immediately with `FileNotFoundError: ... stub_flasher_32s3.json` /// ships without it (stripped for DFSG compliance: the prebuilt blob has no
/// — also confirmed live. --no-stub talks directly to the ROM bootloader /// buildable-from-source path Debian could verify), so scripts/self-update.sh
/// instead, which doesn't need those files (slower, but actually works on /// fetches the exact same file from the matching upstream esptool release
/// this packaging). /// 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> { fn esptool_global_args<'a>(path: &'a str, baud: Option<&'a str>) -> Vec<&'a str> {
let mut args = vec!["--chip", ESPTOOL_CHIP, "--port", path]; let mut args = vec!["--chip", ESPTOOL_CHIP, "--port", path];
if let Some(b) = baud { if let Some(b) = baud {
args.push("--baud"); args.push("--baud");
args.push(b); args.push(b);
} }
args.push("--no-stub");
args args
} }

View File

@ -102,6 +102,34 @@ if ! command -v esptool >/dev/null 2>&1; then
fi fi
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 # Build-time prerequisites for reticulum-daemon/build.sh's PyInstaller step
# below (discovered the hard way: ensurepip needs python3-venv, and # below (discovered the hard way: ensurepip needs python3-venv, and
# PyInstaller itself needs objdump + libpython3.13.so at build time — none # PyInstaller itself needs objdump + libpython3.13.so at build time — none