Files
SC-F001/bringup/flash.py
2026-04-22 18:31:31 -05:00

109 lines
3.5 KiB
Python

"""Wrap esptool to flash the SC-F001 firmware from a build directory.
Reads `build/flasher_args.json` (produced by `idf.py build`) to get the
authoritative list of offsets + binaries, then invokes esptool once with all
of them — no hardcoded offsets here.
Requires esptool reachable either as a Python module (`pip install esptool`)
or as `esptool.py` on PATH (e.g., from an ESP-IDF activation).
"""
from __future__ import annotations
import importlib.util
import json
import shutil
import subprocess
import sys
from pathlib import Path
class FlashError(RuntimeError):
pass
def _default_build_dir() -> Path:
# bringup dir sits at SC-F001/bringup; build sits at SC-F001/build
return Path(__file__).resolve().parent.parent / "build"
def _resolve_esptool_invocation() -> list[str]:
"""Return the command prefix to run esptool, preferring the installed
module in the current interpreter, falling back to esptool.py on PATH.
Raises FlashError with an actionable message if neither is available.
"""
if importlib.util.find_spec("esptool") is not None:
return [sys.executable, "-m", "esptool"]
fallback = shutil.which("esptool.py") or shutil.which("esptool")
if fallback:
return [fallback]
raise FlashError(
"esptool is not installed in this Python and not on PATH.\n"
" Install with: "
f"{sys.executable} -m pip install -r "
f"{Path(__file__).parent / 'requirements.txt'}\n"
" Or activate an ESP-IDF shell that provides esptool.py."
)
def _load_manifest(build_dir: Path) -> dict:
manifest = build_dir / "flasher_args.json"
if not manifest.exists():
raise FlashError(
f"Build manifest not found: {manifest}\n"
f"Run `idf.py build` from SC-F001/ first, or pass --build-dir."
)
return json.loads(manifest.read_text())
def _resolve_flash_args(build_dir: Path, manifest: dict) -> list[str]:
"""Expand manifest into a (offset, abs-path) list suitable for esptool."""
args: list[str] = []
# flasher_args.json's flash_files is {offset_hex: relpath}.
for offset_hex, rel in manifest["flash_files"].items():
p = (build_dir / rel).resolve()
if not p.exists():
raise FlashError(f"Missing firmware binary: {p}")
args.append(offset_hex)
args.append(str(p))
return args
def flash(
port: str,
build_dir: Path | None = None,
baud: int = 460800,
erase_all: bool = False,
log: callable = print,
) -> None:
build_dir = (build_dir or _default_build_dir()).resolve()
manifest = _load_manifest(build_dir)
chip = manifest.get("extra_esptool_args", {}).get("chip", "esp32")
before = manifest.get("extra_esptool_args", {}).get("before", "default_reset")
after = manifest.get("extra_esptool_args", {}).get("after", "hard_reset")
esptool_cmd = _resolve_esptool_invocation()
base_cmd = esptool_cmd + [
"--chip", chip,
"--port", port,
"--baud", str(baud),
"--before", before,
"--after", after,
]
if erase_all:
log(f" erase_flash @ {port}")
subprocess.check_call(base_cmd + ["erase_flash"])
write_args = manifest.get("write_flash_args", [])
cmd = base_cmd + ["write_flash"] + write_args + _resolve_flash_args(build_dir, manifest)
log(f" flashing from {build_dir}")
log(f" files: {list(manifest['flash_files'].items())}")
try:
subprocess.check_call(cmd)
except subprocess.CalledProcessError as e:
raise FlashError(f"esptool failed (exit {e.returncode})") from e