#!/usr/bin/env python3 """console.py -- talk to the board's serial console (stdlib only, no pyserial). The console lives on /dev/ttyACM0: it is the CDC interface of the same composite USB device as the HID keyboard, so it is present on whatever machine the board is plugged into. It is where the station network and the control passphrase are set. python3 tools/console.py # interactive, Ctrl-] to quit python3 tools/console.py /dev/ttyACM0 status # one command, print the reply python3 tools/console.py wifi "My SSID" "hunter2" python3 tools/console.py token "a-long-passphrase" python3 tools/console.py help The baud rate is nominal: a USB CDC link carries no signalling, so 115200 is a formality. Do not use this to type keystrokes -- there is no such command, by design. """ from __future__ import annotations import os import select import sys import termios import tty DEFAULT_PORT = "/dev/ttyACM0" QUIT = "\x1d" # Ctrl-] def open_raw(port: str) -> tuple[int, list]: fd = os.open(port, os.O_RDWR | os.O_NOCTTY) saved = termios.tcgetattr(fd) tty.setraw(fd) return fd, saved def drain(fd: int, seconds: float) -> str: out = bytearray() deadline_remaining = seconds while deadline_remaining > 0: ready, _, _ = select.select([fd], [], [], deadline_remaining) if not ready: break try: chunk = os.read(fd, 4096) except OSError: break if not chunk: break out += chunk deadline_remaining = 1.0 # keep reading while it keeps talking return out.decode(errors="replace") def main() -> int: args = sys.argv[1:] port = DEFAULT_PORT if args and args[0].startswith("/dev/"): port = args[0] args = args[1:] if not os.path.exists(port): print(f"{port} does not exist -- is the board plugged in here?", file=sys.stderr) return 1 fd, saved = open_raw(port) try: if args: os.write(fd, (" ".join(args) + "\n").encode()) sys.stdout.write(drain(fd, 2.0)) sys.stdout.flush() return 0 print(f"{port} -- interactive console, Ctrl-] to quit, `help` for commands") stdin_fd = sys.stdin.fileno() while True: ready, _, _ = select.select([stdin_fd, fd], [], [], 0.2) if stdin_fd in ready: char = os.read(stdin_fd, 1) if not char or char == QUIT.encode(): break os.write(fd, char) if fd in ready: sys.stdout.write(drain(fd, 0.05)) sys.stdout.flush() return 0 finally: # `reboot` makes the device disappear underneath us: the CDC node is gone the # instant the board resets, so restoring the port settings can legitimately fail. # The command already reached the board -- do not turn a successful `reboot` into # a Python traceback and a non-zero exit. try: termios.tcsetattr(fd, termios.TCSAFLUSH, saved) except (OSError, termios.error): # termios.error is NOT an OSError here pass try: os.close(fd) except OSError: pass if __name__ == "__main__": sys.exit(main())