Browser or terminal controller drives an ESP32-S3 that acts as a Logitech G815 into a target machine running no software. Disarmed by default; arming requires the passphrase, the board button, or the serial console, and every fail-safe path cancels in-flight work. - src/: firmware with pure, host-testable modules (keystate, proto, text, ws_codec, sha1), the WebSocket control server, and the served page - tests/: host suites for the pure modules, relayctl parity, and page JS - tools/: relayctl terminal controller, serial console, bootloader helper, and an evdev target watcher - platformio.ini: bridge (USB HID) and console (no-HID) environments - README.md: single source of truth for behaviour, protocol and safety LLM-Generated: true
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
#!/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())
|