Initial public release
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user