Initial import: keyrelay, a WiFi-controlled USB HID keyboard
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
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
#!/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:
|
||||
termios.tcsetattr(fd, termios.TCSAFLUSH, saved)
|
||||
os.close(fd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user