#!/usr/bin/env python3 """to_bootloader.py -- put the AtomS3 Lite into ROM USB download mode, stdlib only. The firmware owns the USB pads (USB-OTG / TinyUSB), so esptool cannot pulse the board into its bootloader by itself: with an app running, `pio run -t upload` sits at "Connecting...". The Arduino CDC implementation watches for a line-coding change to 1200 baud and responds by calling usb_persist_restart(RESTART_BOOTLOADER), which hands the pads back to the built-in USB-Serial/JTAG and forces a download-mode boot. Setting a tty's baud rate is exactly the SET_LINE_CODING control transfer that triggers it, so termios is enough -- no pyserial, no GPIO0 (which this board does not break out). python3 tools/to_bootloader.py [port] # default /dev/ttyACM0 pio run -e bridge -t upload # then flash With no host cooperation at all: hold the top button while the board powers up and keep holding for 3 s (the LED goes solid red). The firmware does the same thing. """ from __future__ import annotations import glob import os import sys import termios import time VENDOR_ESPRESSIF = "303a" ROM_MARKER = "JTAG" # the ROM enumerates as "USB JTAG/serial debug unit" def read_sysfs(path: str) -> str: try: with open(path) as handle: return handle.read().strip() except OSError: return "" def download_mode_port(timeout: float) -> str | None: """Wait for the ROM's USB-Serial/JTAG port to show up.""" deadline = time.time() + timeout while time.time() < deadline: for device in glob.glob("/sys/bus/usb/devices/*"): if read_sysfs(f"{device}/idVendor") != VENDOR_ESPRESSIF: continue if ROM_MARKER not in read_sysfs(f"{device}/product"): continue for tty in glob.glob(f"{device}/*/tty/ttyACM*"): return "/dev/" + tty.rsplit("/", 1)[-1] time.sleep(0.2) return None def touch(port: str) -> None: """Ask the running firmware for a download-mode reboot by setting 1200 baud.""" fd = os.open(port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) try: # tcgetattr -> [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] (indices 4 # and 5 are the speeds); setting them is what makes the kernel issue the CDC # SET_LINE_CODING the firmware watches for at 1200 baud. attrs = termios.tcgetattr(fd) attrs[4] = termios.B1200 attrs[5] = termios.B1200 termios.tcsetattr(fd, termios.TCSANOW, attrs) time.sleep(0.3) finally: # The chip resets underneath us; EPROTO/ENODEV here means it worked. try: os.close(fd) except OSError: pass def main() -> int: port = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyACM0" if not os.path.exists(port): print(f"{port} does not exist -- is the board plugged in?", file=sys.stderr) return 1 print(f"touching {port} at 1200 baud ...") try: touch(port) except OSError as exc: print(f"port dropped out ({exc.strerror}) -- that is the reboot") found = download_mode_port(timeout=10.0) if found: print(f"ROM download mode reached on {found}") print("now flash with: pio run -e bridge -t upload") return 0 print("no ROM download port appeared.", file=sys.stderr) print("fallback: hold the top button while the board powers up and keep holding " "for 3 s (LED solid red), then flash.", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())