Files
roro9stack/scripts/rdbg.py
T
twislaandClaude Opus 5.5 0fb7f4e9d5 Debug Builds: the console over Wi-Fi (Debug Console, TCP 2323)
cardputer-adv-debug (-DRORO_DEBUG, version +debug) adds a Debug Console:
after a token line, a client gets the last 6 KB of console output, live
lines (ESP-IDF logs included) and the serial commands. The socket task
only queues lines; the main loop runs them. Release builds compile none
of it. The token lives in ~/.config/roro9stack/debug-token, created by
_docker.sh and passed into the container.

All output now goes through `console`, which never waits for USB: a host
that was attached but not reading stalled the main loop up to 2 s per
line. New commands everywhere: info (slots with their versions from NVS,
since the framework stamps its own into each image), tasks, reboot,
boot other, log level, help. scripts/rdbg.py is the client; flash.sh
--debug builds it; CI builds both variants. ADR 0004.

Verified on the device: USB-flashed, then updated over Wi-Fi to a Debug
Build that confirmed on Probation; both slots hold Debug Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-04 02:44:14 +02:00

111 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""The Debug Console of a Debug Build, over Wi-Fi (TCP 2323, see ADR 0004).
Usage: scripts/rdbg.py [-H host] [-b] [command ...]
no command interactive: type commands, see every console line live; Ctrl-D or `quit` leaves
command runs it and prints what follows, until the console has been quiet for a moment
-H host the device's IP (Settings > Firmware), default $RORO_OTA_HOST
-b also print the backlog the device sends on connecting (boot messages and so on)
The token is read from ~/.config/roro9stack/debug-token (made by the first build).
"""
import os
import select
import socket
import sys
import time
PORT = 2323
TOKEN_FILE = os.path.expanduser("~/.config/roro9stack/debug-token")
def read_until(sock, marker, timeout):
"""Everything up to and including `marker`, or None on a timeout or a closed connection."""
sock.settimeout(timeout)
data = b""
while marker not in data:
try:
chunk = sock.recv(4096)
except socket.timeout:
return None
if not chunk:
return None
data += chunk
return data
def read_until_quiet(sock, quiet, out):
"""Copies everything the device sends to `out` until nothing has arrived for `quiet` seconds."""
sock.settimeout(quiet)
while True:
try:
data = sock.recv(4096)
except socket.timeout:
return True
if not data:
return False
if out:
out.write(data.decode(errors="replace"))
out.flush()
def interactive(sock):
sock.setblocking(False)
while True:
ready, _, _ = select.select([sock, sys.stdin], [], [])
if sock in ready:
data = sock.recv(4096)
if not data:
print("\n(connection closed)")
return
sys.stdout.write(data.decode(errors="replace"))
sys.stdout.flush()
if sys.stdin in ready:
line = sys.stdin.readline()
if not line:
return
sock.sendall(line.encode())
def main():
args = sys.argv[1:]
host, backlog = os.environ.get("RORO_OTA_HOST"), False
while args and args[0].startswith("-"):
flag = args.pop(0)
if flag == "-H" and args:
host = args.pop(0)
elif flag == "-b":
backlog = True
else:
sys.exit(__doc__)
if not host:
sys.exit("No device: pass -H <ip> or set RORO_OTA_HOST\n\n" + __doc__)
token = open(TOKEN_FILE).read().strip()
with socket.create_connection((host, PORT), timeout=10) as sock:
sock.sendall((token + "\n").encode())
# The device may still be finishing a previous client: wait for this connection's banner.
banner = read_until(sock, b"Backlog follows.\n", 15)
if banner is None:
sys.exit("device: no banner (wrong token, or another client is connected)")
show = sys.stdout if backlog or not args else None
if show:
show.write(banner.decode(errors="replace"))
read_until_quiet(sock, 0.5, show) # the backlog
if not args:
return interactive(sock)
command = " ".join(args)
sock.sendall((command + "\n").encode())
# The main loop echoes "> command" when it runs it; the reply follows.
echoed = read_until(sock, f"> {command}\n".encode(), 15)
if echoed is None:
sys.exit("device: the command never ran")
sys.stdout.write(echoed.decode(errors="replace").split(f"> {command}\n", 1)[1])
try:
read_until_quiet(sock, 1.5, sys.stdout)
except BrokenPipeError: # e.g. piped into head
pass
if __name__ == "__main__":
main()