188 lines
6.8 KiB
Python
Executable File
188 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
ESP32 Mass Flash Script (filtered & robust)
|
|
|
|
Changes in this version:
|
|
- Filters out non-USB system serials (e.g., /dev/ttyS*)
|
|
- Only enumerates typical USB serial ports: /dev/ttyUSB* and /dev/ttyACM*
|
|
- Further filters to known USB-serial vendor IDs by default:
|
|
* FTDI 0x0403
|
|
* SiliconLabs/CP210x 0x10C4
|
|
* QinHeng/CH34x 0x1A86
|
|
* Prolific PL2303 0x067B
|
|
* Espressif native 0x303A
|
|
- Adds --ports to override selection (glob or comma-separated patterns)
|
|
- Uses detect_esp32.detect_chip_type(port) for exact chip string
|
|
- Maps to correct idf.py target before flashing
|
|
|
|
Example:
|
|
python3 flash_all.py --project /path/to/project --start-ip 192.168.1.50
|
|
python3 flash_all.py --project . --ports '/dev/ttyUSB*,/dev/ttyACM*'
|
|
"""
|
|
import argparse
|
|
import fnmatch
|
|
import glob
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List, Dict
|
|
|
|
try:
|
|
import serial.tools.list_ports as list_ports
|
|
except Exception:
|
|
print("pyserial is required: pip install pyserial")
|
|
raise
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, SCRIPT_DIR)
|
|
try:
|
|
import detect_esp32
|
|
except Exception as e:
|
|
print("Error: detect_esp32.py must be in the same directory and importable.")
|
|
print(f"Import error: {e}")
|
|
sys.exit(1)
|
|
|
|
# Known USB VIDs commonly used for ESP32 dev boards/adapters
|
|
KNOWN_VIDS = {0x0403, 0x10C4, 0x1A86, 0x067B, 0x303A}
|
|
|
|
def map_chip_to_idf_target(chip_str: str) -> str:
|
|
if not chip_str or chip_str == 'Unknown':
|
|
return 'unknown'
|
|
s = chip_str.upper()
|
|
if s.startswith('ESP32-S3'):
|
|
return 'esp32s3'
|
|
if s.startswith('ESP32-S2'):
|
|
return 'esp32s2'
|
|
if s.startswith('ESP32-C3'):
|
|
return 'esp32c3'
|
|
if s.startswith('ESP32-C6'):
|
|
return 'esp32c6'
|
|
if s.startswith('ESP32-H2'):
|
|
return 'esp32h2'
|
|
if s.startswith('ESP32'):
|
|
return 'esp32'
|
|
return 'unknown'
|
|
|
|
def run(cmd: List[str], cwd: str = None, check: bool = True) -> int:
|
|
print(">>", " ".join(cmd))
|
|
proc = subprocess.run(cmd, cwd=cwd)
|
|
if check and proc.returncode != 0:
|
|
raise RuntimeError(f"Command failed with code {proc.returncode}: {' '.join(cmd)}")
|
|
return proc.returncode
|
|
|
|
def ensure_target(project_dir: str, target: str):
|
|
if not target or target == 'unknown':
|
|
raise ValueError("Unknown IDF target; cannot set-target.")
|
|
run(['idf.py', 'set-target', target], cwd=project_dir, check=True)
|
|
|
|
def flash_device(project_dir: str, port: str, idf_target: str, baud: int = 460800) -> bool:
|
|
try:
|
|
ensure_target(project_dir, idf_target)
|
|
run(['idf.py', '-p', port, '-b', str(baud), 'flash'], cwd=project_dir, check=True)
|
|
return True
|
|
except Exception as e:
|
|
print(f" Flash failed on {port}: {e}")
|
|
return False
|
|
|
|
def match_any(path: str, patterns: List[str]) -> bool:
|
|
return any(fnmatch.fnmatch(path, pat) for pat in patterns)
|
|
|
|
def list_ports_filtered(patterns: List[str] = None) -> List[object]:
|
|
"""Return a filtered list of pyserial list_ports items."""
|
|
ports = list(list_ports.comports())
|
|
filtered = []
|
|
for p in ports:
|
|
dev = p.device
|
|
# Default pattern filter: only /dev/ttyUSB* and /dev/ttyACM*
|
|
if patterns:
|
|
if not match_any(dev, patterns):
|
|
continue
|
|
else:
|
|
if not (dev.startswith('/dev/ttyUSB') or dev.startswith('/dev/ttyACM')):
|
|
continue
|
|
# VID filter (allow if vid is known or missing (some systems omit it), but exclude obvious non-USB)
|
|
vid = getattr(p, 'vid', None)
|
|
if vid is not None and vid not in KNOWN_VIDS:
|
|
# Skip unknown vendor to reduce noise; user can override with --ports
|
|
continue
|
|
filtered.append(p)
|
|
return filtered
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Mass flash multiple ESP32 devices with proper chip detection.")
|
|
ap.add_argument('--project', required=True, help='Path to the ESP-IDF project to flash')
|
|
ap.add_argument('--ssid', help='WiFi SSID (optional)')
|
|
ap.add_argument('--password', help='WiFi password (optional)')
|
|
ap.add_argument('--start-ip', default='192.168.1.50', help='Base IP address for plan display')
|
|
ap.add_argument('--baud', type=int, default=460800, help='Flashing baud rate')
|
|
ap.add_argument('--dry-run', action='store_true', help='Plan only; do not flash')
|
|
ap.add_argument('--ports', help='Comma-separated glob(s), e.g. "/dev/ttyUSB*,/dev/ttyACM*" to override selection')
|
|
args = ap.parse_args()
|
|
|
|
project_dir = os.path.abspath(args.project)
|
|
if not os.path.isdir(project_dir):
|
|
print(f"Project directory not found: {project_dir}")
|
|
sys.exit(1)
|
|
|
|
patterns = None
|
|
if args.ports:
|
|
patterns = [pat.strip() for pat in args.ports.split(',') if pat.strip()]
|
|
|
|
devices = list_ports_filtered(patterns)
|
|
print(f"Found {len(devices)} USB serial device(s) after filtering")
|
|
if not devices:
|
|
print("No candidate USB serial ports found. Try --ports '/dev/ttyUSB*,/dev/ttyACM*' or check permissions.")
|
|
|
|
device_list: List[Dict] = []
|
|
for idx, dev in enumerate(devices, 1):
|
|
port = dev.device
|
|
print(f"Probing {port} for exact chip...")
|
|
raw_chip = detect_esp32.detect_chip_type(port)
|
|
idf_target = map_chip_to_idf_target(raw_chip)
|
|
if idf_target == 'unknown':
|
|
print(f" WARNING: Could not determine idf.py target for {port} (got '{raw_chip}')")
|
|
device_list.append({
|
|
'number': idx,
|
|
'port': port,
|
|
'raw_chip': raw_chip,
|
|
'idf_target': idf_target,
|
|
'info': dev,
|
|
})
|
|
|
|
# Plan output
|
|
base = args.start_ip.split('.')
|
|
try:
|
|
base0, base1, base2, base3 = int(base[0]), int(base[1]), int(base[2]), int(base[3])
|
|
except Exception:
|
|
base0, base1, base2, base3 = 192, 168, 1, 50
|
|
|
|
print("\nFlash plan:")
|
|
for d in device_list:
|
|
ip_last = base3 + d['number'] - 1
|
|
ip = f"{base0}.{base1}.{base2}.{ip_last}"
|
|
print(f" Device {d['number']:2d}: {d['port']} -> {d['raw_chip']} [{d['idf_target']}] -> {ip}")
|
|
|
|
if args.dry_run:
|
|
print("\nDry run: not flashing any devices.")
|
|
return
|
|
|
|
failed = []
|
|
for d in device_list:
|
|
if d['idf_target'] == 'unknown':
|
|
print(f"\n ERROR: Unknown IDF target for {d['port']} (raw chip '{d['raw_chip']}'). Skipping.")
|
|
failed.append(d['number'])
|
|
continue
|
|
print(f"\nFlashing {d['port']} as target {d['idf_target']}...")
|
|
ok = flash_device(project_dir, d['port'], d['idf_target'], baud=args.baud)
|
|
if not ok:
|
|
failed.append(d['number'])
|
|
|
|
if failed:
|
|
print(f"\nCompleted with failures on devices: {failed}")
|
|
sys.exit(2)
|
|
print("\nAll devices flashed successfully.")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|