Files
2026-07-17 15:29:53 -04:00

232 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
wipe_device.py — Wipe ATOMS3 MacroPad NVS and LittleFS partitions.
Usage:
python wipe_device.py # auto-detect COM port
python wipe_device.py COM3 # use specified COM port
The script will:
1. Find the connected MacroPad automatically
2. Send {"cmd":"bootloader"} to trigger ROM download mode
3. Wait for the device to re-enumerate as USB-JTAG
4. Erase NVS (0x009000, 20 KB) — clears settings
5. Erase LittleFS (0x670000, 1.5 MB) — clears all macros
6. Reset the device back into normal firmware
"""
import glob
import json
import os
import subprocess
import sys
import time
import serial
import serial.tools.list_ports
# Force UTF-8 output on Windows terminals
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ESPRESSIF_VID = 0x303A
DEVICE_ID = "ATOMS3-MACROPAD"
BAUD_RATE = 115200
# Partition layout (matches default_8MB.csv flashed with firmware)
NVS_OFFSET = 0x009000
NVS_SIZE = 0x005000 # 20 KB
LITTLEFS_OFFSET = 0x670000
LITTLEFS_SIZE = 0x180000 # 1.5 MB
def find_esptool() -> str:
"""Locate esptool.exe inside the Arduino15 package cache."""
pattern = os.path.join(
os.environ.get("LOCALAPPDATA", ""),
"Arduino15", "packages", "*", "tools", "esptool_py", "*", "esptool.exe",
)
matches = sorted(glob.glob(pattern), reverse=True) # newest version first
if matches:
return matches[0]
raise FileNotFoundError(
"esptool.exe not found in Arduino15 package cache.\n"
"Make sure the M5Stack board package is installed via arduino-cli."
)
def list_espressif_ports() -> set[str]:
"""Return the set of COM port names currently belonging to Espressif devices."""
return {
p.device
for p in serial.tools.list_ports.comports()
if p.vid == ESPRESSIF_VID
}
def find_macropad_port() -> str | None:
"""Scan Espressif COM ports and return the one that responds as the MacroPad."""
for p in serial.tools.list_ports.comports():
if p.vid != ESPRESSIF_VID:
continue
try:
ser = serial.Serial()
ser.port = p.device
ser.baudrate = BAUD_RATE
ser.timeout = 2
ser.dtr = False
ser.rts = False
ser.open()
time.sleep(0.1)
ser.dtr = True # signal host-connected to TinyUSB CDC
time.sleep(0.3)
ser.reset_input_buffer()
ser.write(b'{"cmd":"ping"}\n')
line = ser.readline().decode("utf-8", errors="ignore").strip()
ser.close()
if line:
data = json.loads(line)
if data.get("id") == DEVICE_ID:
return p.device
except (serial.SerialException, json.JSONDecodeError, OSError):
pass
return None
def send_bootloader_command(port: str) -> None:
"""Open the port and fire the bootloader command. Ignores read errors on disconnect."""
ser = serial.Serial()
ser.port = port
ser.baudrate = BAUD_RATE
ser.timeout = 2
ser.dtr = False
ser.rts = False
ser.open()
time.sleep(0.1)
ser.dtr = True
time.sleep(0.3)
ser.reset_input_buffer()
ser.write(b'{"cmd":"bootloader"}\n')
ser.flush()
try:
# Read {"rsp":"ok"} — may throw if device resets mid-read
ser.read(64)
except (serial.SerialException, OSError):
pass
finally:
try:
ser.close()
except Exception:
pass
def wait_for_port_change(old_ports: set[str], timeout: float = 10.0) -> str:
"""
Block until a new Espressif COM port appears that wasn't in old_ports.
Returns the new port name, or raises TimeoutError.
"""
deadline = time.time() + timeout
while time.time() < deadline:
current = list_espressif_ports()
new = current - old_ports
if new:
return new.pop()
time.sleep(0.25)
raise TimeoutError(
"Timed out waiting for device to re-enumerate in download mode.\n"
"Check USB connection and try again."
)
def erase_region(esptool: str, port: str, offset: int, size: int, keep_stub: bool) -> None:
"""Run esptool to erase one flash region.
keep_stub=True leaves the stub running so the next call can reconnect
without a full reset cycle.
"""
after = "no-reset" if keep_stub else "watchdog-reset"
cmd = [
esptool,
"--chip", "esp32s3",
"--port", port,
"--baud", "460800",
"--before", "no-reset",
"--after", after,
"erase-region",
hex(offset),
hex(size),
]
result = subprocess.run(cmd, capture_output=True, text=True)
output = result.stdout + result.stderr
if result.returncode != 0:
print(output)
raise RuntimeError(f"esptool failed (exit {result.returncode})")
def main() -> None:
print("=" * 40)
print(" ATOMS3 MacroPad -- Device Wiper")
print("=" * 40 + "\n")
print("[ 1/5 ] Locating esptool...", end=" ", flush=True)
esptool = find_esptool()
print(f"OK\n {esptool}")
forced_port = sys.argv[1].upper() if len(sys.argv) > 1 else None
if forced_port:
print(f"[ 2/5 ] Using specified port {forced_port}...", end=" ", flush=True)
port = forced_port
print("OK")
else:
print("[ 2/5 ] Searching for MacroPad...", end=" ", flush=True)
port = find_macropad_port()
if not port:
print("NOT FOUND")
print("\nNo MacroPad detected. Make sure the device is connected and firmware is running.")
sys.exit(1)
print(f"found on {port}")
print("[ 3/5 ] Entering download mode...", end=" ", flush=True)
ports_before = list_espressif_ports()
send_bootloader_command(port)
# Wait for old port to vanish, then for the JTAG port to appear
time.sleep(0.5)
try:
jtag_port = wait_for_port_change(ports_before - {port}, timeout=10.0)
except TimeoutError as e:
print("TIMEOUT")
print(f"\n{e}")
sys.exit(1)
print(f"device on {jtag_port}")
# Let the ROM fully settle
time.sleep(0.5)
print(f"[ 4/5 ] Erasing NVS "
f"(0x{NVS_OFFSET:06X} 0x{NVS_OFFSET + NVS_SIZE - 1:06X}, "
f"{NVS_SIZE // 1024} KB)...", end=" ", flush=True)
try:
erase_region(esptool, jtag_port, NVS_OFFSET, NVS_SIZE, keep_stub=True)
except RuntimeError as e:
print("FAILED")
print(f"\n{e}")
sys.exit(1)
print("OK")
# Final erase resets the device
print(f"[ 5/5 ] Erasing LittleFS "
f"(0x{LITTLEFS_OFFSET:06X} 0x{LITTLEFS_OFFSET + LITTLEFS_SIZE - 1:06X}, "
f"{LITTLEFS_SIZE // 1024} KB)...", end=" ", flush=True)
try:
erase_region(esptool, jtag_port, LITTLEFS_OFFSET, LITTLEFS_SIZE, keep_stub=False)
except RuntimeError as e:
print("FAILED")
print(f"\n{e}")
sys.exit(1)
print("OK")
print("\nDone. NVS and LittleFS are blank -- device is in a fresh state.\n")
if __name__ == "__main__":
main()