The board now enumerates as a boot keyboard plus a relative mouse on the same HID interface. The pointer is captured, never grabbed: Pointer Lock in the page, so Esc (the browser's own, never forwarded) always frees it, and every frame restates the whole button byte so a frame lost in the WiFi cannot leave a button held. Losing the lock, the tab or the link runs the same release funnel as a stuck key, and boot protocol or `mouse off` refuses mouse frames with E11 rather than sending bytes a boot interface would misparse as keys. src/mouse.h owns the report, src/output.h the queues and the funnel; the page, relayctl.py and watch_target.py carry the button bits and the evdev decoding. The verifier's own tables are pinned now — REL_WHEEL is 8, not 2, and the first version guessed from the report's field order, so a working wheel looked dead while the summary said no scroll happened. LLM-Generated: true
102 lines
3.3 KiB
Python
102 lines
3.3 KiB
Python
#!/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())
|