diff --git a/core/archipelago/src/mesh/flash.rs b/core/archipelago/src/mesh/flash.rs index 229dcf93..33d1eaa5 100644 --- a/core/archipelago/src/mesh/flash.rs +++ b/core/archipelago/src/mesh/flash.rs @@ -657,36 +657,48 @@ const ESPTOOL_FALLBACK_BAUD: &str = "115200"; async fn esptool_erase_and_write(path: &str, image: &Path, job: &Arc) -> Result<()> { job.set_stage(FlashStage::Erasing).await; - esptool_with_retry( - &["--chip", ESPTOOL_CHIP, "--port", path, "erase_flash"], - job, - ) - .await - .context("esptool erase_flash failed")?; + esptool_with_retry(path, &["erase_flash"], job) + .await + .context("esptool erase_flash failed")?; job.set_stage(FlashStage::Writing).await; let image_str = image.to_string_lossy().to_string(); - esptool_with_retry( - &[ - "--chip", - ESPTOOL_CHIP, - "--port", - path, - "write_flash", - "0x0", - &image_str, - ], - job, - ) - .await - .context("esptool write_flash failed")?; + esptool_with_retry(path, &["write_flash", "0x0", &image_str], job) + .await + .context("esptool write_flash failed")?; Ok(()) } -async fn esptool_with_retry(args: &[&str], job: &Arc) -> Result<()> { +/// 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. +/// +/// --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). +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 +} + +async fn esptool_with_retry(path: &str, subcommand: &[&str], job: &Arc) -> Result<()> { let mut cmd = Command::new("esptool"); - cmd.args(args); + cmd.args(esptool_global_args(path, None)); + cmd.args(subcommand); match run_streamed(cmd, None, job).await { Ok(()) => Ok(()), Err(first_err) => { @@ -695,8 +707,8 @@ async fn esptool_with_retry(args: &[&str], job: &Arc) -> Result<()> { )) .await; let mut retry = Command::new("esptool"); - retry.args(args); - retry.args(["--baud", ESPTOOL_FALLBACK_BAUD]); + retry.args(esptool_global_args(path, Some(ESPTOOL_FALLBACK_BAUD))); + retry.args(subcommand); run_streamed(retry, None, job) .await .context(format!("retry also failed (first attempt: {first_err:#})"))