fix(mesh): esptool --no-stub + fix retry's broken --baud arg ordering

The improved error logging from the last fix immediately paid off — real
esptool output on the next attempt showed two distinct bugs:

1. 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: FileNotFoundError: ... No such
   file or directory: '.../stub_flasher_32s3.json'. Fixed with --no-stub,
   which talks directly to the ROM bootloader instead — the standard
   workaround for this exact Debian packaging gap.

2. The fallback-baud retry appended `--baud 115200` AFTER the subcommand
   token (erase_flash/write_flash), but esptool requires global flags
   before the subcommand — every retry failed with "esptool: error:
   unrecognized arguments: --baud 115200", so the retry logic added
   earlier never actually got a chance to run. esptool_with_retry now
   builds global args (--chip/--port/--baud/--no-stub) and subcommand args
   separately and always concatenates them in the right order, instead of
   relying on call-site ordering of a single flat arg list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssmithx 2026-07-23 03:21:39 +00:00
parent ff532465cf
commit a1cb83dfb2

View File

@ -657,36 +657,48 @@ const ESPTOOL_FALLBACK_BAUD: &str = "115200";
async fn esptool_erase_and_write(path: &str, image: &Path, job: &Arc<FlashJob>) -> 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<FlashJob>) -> 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<FlashJob>) -> 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<FlashJob>) -> 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:#})"))