fix(mesh): standalone erase_flash unsupported in --no-stub ROM mode

Real esptool output on a live Heltec V4 exposed the next layer of the
--no-stub fix: "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 streams data), not a bulk
full-chip-erase opcode — that's a stub-firmware-only feature, and Debian's
esptool package ships without the stub (hence --no-stub in the first
place).

write_flash's own --erase-all flag gets the same "erase everything before
writing" outcome our "always erase before write" default requires, but via
the per-sector mechanism the ROM bootloader actually supports — one esptool
invocation instead of a separate erase_flash + write_flash pair.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssmithx 2026-07-23 12:28:51 +00:00
parent 76d14c3bf9
commit 0ca8f25b1b

View File

@ -686,17 +686,27 @@ 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
/// 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.
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::Erasing).await;
esptool_with_retry(path, &["erase_flash"], job)
.await
.context("esptool erase_flash failed")?;
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();
esptool_with_retry(path, &["write_flash", "0x0", &image_str], job) esptool_with_retry(
.await path,
.context("esptool write_flash failed")?; &["write_flash", "--erase-all", "0x0", &image_str],
job,
)
.await
.context("esptool write_flash failed")?;
Ok(()) Ok(())
} }