fix: flash.py binary detection, boot_app0 discovery, add --erase flag

- Fix merged binary detection: check partition table magic (0xAA50) at
  offset 0x8000 instead of bootloader magic (0xE9) at offset 0 — both
  merged and app-only binaries start with 0xE9, causing app-only
  binaries to be flashed at wrong address
- Fix boot_app0.bin discovery: handle versioned PlatformIO package
  directories (e.g. framework-arduinoespressif32@3.20009.0)
- Add --erase flag: full flash erase before writing (recommended for
  recovery from corrupted flash)
This commit is contained in:
James L
2026-02-22 21:47:00 -05:00
parent 760e92f186
commit adc330dc15

View File

@@ -53,9 +53,39 @@ BUILD_DIR = ".pio/build/heltec_V4_boundary"
BOOTLOADER_BIN = os.path.join(BUILD_DIR, "bootloader.bin") BOOTLOADER_BIN = os.path.join(BUILD_DIR, "bootloader.bin")
PARTITIONS_BIN = os.path.join(BUILD_DIR, "partitions.bin") PARTITIONS_BIN = os.path.join(BUILD_DIR, "partitions.bin")
FIRMWARE_BIN = os.path.join(BUILD_DIR, "rnode_firmware_heltec32v4_boundary.bin") FIRMWARE_BIN = os.path.join(BUILD_DIR, "rnode_firmware_heltec32v4_boundary.bin")
BOOT_APP0_BIN = os.path.expanduser(
"~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin" # ESP32 partition table magic bytes (first two bytes of a partition table entry)
) PARTITION_TABLE_MAGIC = b'\xaa\x50'
def find_boot_app0():
"""Find boot_app0.bin from PlatformIO framework packages.
Handles versioned package directories (e.g. framework-arduinoespressif32@3.20009.0).
"""
pio_dir = os.path.expanduser("~/.platformio/packages")
# Try exact name first
exact = os.path.join(pio_dir, "framework-arduinoespressif32",
"tools", "partitions", "boot_app0.bin")
if os.path.isfile(exact):
return exact
# Try versioned directories
if os.path.isdir(pio_dir):
for name in sorted(os.listdir(pio_dir), reverse=True):
if name.startswith("framework-arduinoespressif32"):
candidate = os.path.join(pio_dir, name, "tools", "partitions", "boot_app0.bin")
if os.path.isfile(candidate):
return candidate
# Bundled fallback
bundled = os.path.join(os.path.dirname(__file__), "Release", "boot_app0.bin")
if os.path.isfile(bundled):
return bundled
return None
BOOT_APP0_BIN = find_boot_app0()
# ── Helpers ──────────────────────────────────────────────────────────────────── # ── Helpers ────────────────────────────────────────────────────────────────────
@@ -212,13 +242,8 @@ def merge_firmware(output_path, esptool_cmd):
# boot_app0 can come from PlatformIO or be bundled # boot_app0 can come from PlatformIO or be bundled
boot_app0 = BOOT_APP0_BIN boot_app0 = BOOT_APP0_BIN
if not os.path.isfile(boot_app0): if not boot_app0 or not os.path.isfile(boot_app0):
# Check if bundled in Release/ print("Error: boot_app0.bin not found.")
alt = os.path.join(os.path.dirname(__file__), "Release", "boot_app0.bin")
if os.path.isfile(alt):
boot_app0 = alt
else:
print(f"Error: boot_app0.bin not found at {BOOT_APP0_BIN}")
print(" Run 'pio run -e heltec_V4_boundary' first, or install PlatformIO.") print(" Run 'pio run -e heltec_V4_boundary' first, or install PlatformIO.")
return False return False
required["boot_app0"] = boot_app0 required["boot_app0"] = boot_app0
@@ -265,27 +290,29 @@ def flash_firmware(firmware_path, port, esptool_cmd, baud=BAUD_RATE):
print(f" Chip: {CHIP} Baud: {baud} Flash: {FLASH_SIZE}\n") print(f" Chip: {CHIP} Baud: {baud} Flash: {FLASH_SIZE}\n")
# Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000) # Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000)
# The app-only .bin for this project is ~800KB. The merged binary adds #
# bootloader+partitions+boot_app0 padding (~64KB) so it's slightly larger # Both merged and app-only binaries start with 0xE9 (ESP32 image magic), so
# but still well under 2MB. A true app-only binary won't have the bootloader # that byte alone cannot distinguish them. Instead, check for the partition
# signature at offset 0. Check for the ESP32 bootloader magic byte (0xE9). # table magic (0xAA 0x50) at offset 0x8000 — only merged binaries contain
# the partition table embedded at that offset.
size = os.path.getsize(firmware_path) size = os.path.getsize(firmware_path)
is_merged = False is_merged = False
try: try:
with open(firmware_path, "rb") as f: with open(firmware_path, "rb") as f:
magic = f.read(1) if size > 0x8002: # Must be large enough to contain partition table area
if magic == b'\xe9': f.seek(0x8000)
# Has bootloader magic — this is a merged binary starting at 0x0 pt_magic = f.read(2)
if pt_magic == PARTITION_TABLE_MAGIC:
is_merged = True is_merged = True
except Exception: except Exception:
pass pass
if is_merged: if is_merged:
flash_addr = f"0x{BOOTLOADER_ADDR:x}" flash_addr = f"0x{BOOTLOADER_ADDR:x}"
print(f" Detected: merged binary (bootloader magic 0xE9) → flash at {flash_addr}") print(f" Detected: merged binary (partition table at 0x8000) -> flash at {flash_addr}")
else: else:
flash_addr = f"0x{APP_ADDR:x}" flash_addr = f"0x{APP_ADDR:x}"
print(f" Detected: app-only binary flash at {flash_addr}") print(f" Detected: app-only binary -> flash at {flash_addr}")
cmd = esptool_cmd + [ cmd = esptool_cmd + [
"--chip", CHIP, "--chip", CHIP,
@@ -319,6 +346,7 @@ Examples:
python flash.py --file firmware.bin # Flash a specific file python flash.py --file firmware.bin # Flash a specific file
python flash.py --merge-only # Build merged binary from PlatformIO output python flash.py --merge-only # Build merged binary from PlatformIO output
python flash.py --port /dev/ttyACM0 # Specify serial port python flash.py --port /dev/ttyACM0 # Specify serial port
python flash.py --erase --download # Erase flash, then download and flash
""", """,
) )
parser.add_argument("--file", "-f", help="Path to firmware binary to flash") parser.add_argument("--file", "-f", help="Path to firmware binary to flash")
@@ -330,6 +358,8 @@ Examples:
help="Merge PlatformIO build output into single binary, don't flash") help="Merge PlatformIO build output into single binary, don't flash")
parser.add_argument("--no-merge", action="store_true", parser.add_argument("--no-merge", action="store_true",
help="Skip merge step, use existing merged binary or --file") help="Skip merge step, use existing merged binary or --file")
parser.add_argument("--erase", action="store_true",
help="Erase entire flash before writing (recommended for recovery)")
args = parser.parse_args() args = parser.parse_args()
baud = args.baud baud = args.baud
@@ -415,6 +445,21 @@ Examples:
print("Aborted.") print("Aborted.")
sys.exit(0) sys.exit(0)
if args.erase:
print(f"Erasing flash on {port}...")
erase_cmd = esptool_cmd + [
"--chip", CHIP,
"--port", port,
"--baud", baud,
"erase_flash",
]
result = subprocess.run(erase_cmd)
if result.returncode != 0:
print("\nErase FAILED.")
sys.exit(1)
print("Flash erased. Waiting for device to re-enumerate...")
time.sleep(3)
if flash_firmware(firmware_path, port, esptool_cmd, baud): if flash_firmware(firmware_path, port, esptool_cmd, baud):
print() print()
print("╔══════════════════════════════════════════╗") print("╔══════════════════════════════════════════╗")