#!/usr/bin/env python3 """Flash this firmware onto another ESP32-P4 board over USB, then give it its own identity. Steps: pick the port, check the chip (ESP32-P4, revision the image supports, flash size), optionally erase, write bootloader + partition table + app, wait for the board's IP in its boot log, then set hostname / stream name / multicast address over the web API so it doesn't clash with other boards on the network. Needs esptool (in the ESP-IDF Python env, or `pip install esptool`) and a build (`idf.py build`). Every question can also be answered with an option, e.g.: tools/flash_board.py --port /dev/ttyACM0 --erase --hostname p4-aes67-2 --name "P4 AES67 2" \\ --mcast 239.69.1.11 Boards already running this firmware and on the network update over the web (OTA) instead. """ import argparse import json import os import re import struct import subprocess import sys import time import urllib.request REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MIN_FLASH_MB = 32 # the bootloader header says 32 MB (sdkconfig); smaller chips aren't supported def die(msg): print(f"\nERROR: {msg}") sys.exit(1) def ask(prompt, default=None): s = input(f"{prompt}{f' [{default}]' if default is not None else ''}: ").strip() return s or (default if default is not None else "") def yes(prompt, default): s = input(f"{prompt} [{'Y/n' if default else 'y/N'}]: ").strip().lower() return default if not s else s.startswith("y") def esptool(port, *args, capture=True): cmd = [sys.executable, "-m", "esptool", "--chip", "esp32p4", "--port", port, *args] if not capture: if subprocess.call(cmd) != 0: die("esptool failed (see above)") return "" r = subprocess.run(cmd, capture_output=True, text=True) if r.returncode != 0: die(f"esptool {' '.join(args)} failed:\n{r.stdout}{r.stderr}") return r.stdout def pick_port(given): if given: return given try: from serial.tools import list_ports except ImportError: die("pyserial missing: run inside the ESP-IDF environment or `pip install esptool`") ports = [p for p in list_ports.comports() if "ACM" in p.device or "USB" in p.device] if not ports: die("no USB serial port found: connect the board's USB-C port (not the PoE/Ethernet side)") for i, p in enumerate(ports, 1): print(f" {i}) {p.device} {p.description}") if len(ports) == 1: return ports[0].device if yes(f"Use {ports[0].device}?", True) else die("no port chosen") n = ask("Port number", "1") return ports[int(n) - 1].device def load_images(build): """(offset, path) pairs and flash options from build/flash_args.""" fa = os.path.join(build, "flash_args") if not os.path.exists(fa): die(f"{fa} not found: build first (idf.py build)") lines = open(fa).read().split("\n") opts = lines[0].split() images = [] for line in lines[1:]: if line.strip(): off, path = line.split() images.append((off, os.path.join(build, path))) return opts, images def image_rev_range(app): """Chip revision range from the app image header (esp_image_header_t), as (min, max) * 100.""" h = open(app, "rb").read(24) if h[0] != 0xE9: die(f"{app} is not an ESP app image") min_full, max_full = struct.unpack_from("