responses

This commit is contained in:
Aiden
2026-05-13 12:30:37 +10:00
parent 455886691a
commit f99a60710e
28 changed files with 2709 additions and 1 deletions

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Cautious RCP-TX7 host-frame command sweep.
This sends checksum-valid 6-byte frames of the observed shape:
prefix1 prefix2 command state value checksum
The checksum is the current working hypothesis:
checksum = 0x5A xor prefix1 xor prefix2 xor command xor state xor value
The script logs TX frames plus any RX frames. It highlights RX frames that differ
from the known heartbeat so a run can be scanned quickly.
"""
from __future__ import annotations
import argparse
import datetime as dt
import sys
import time
try:
import serial
except ImportError:
print(
"Missing dependency: pyserial\n"
"Install it with: python -m pip install pyserial",
file=sys.stderr,
)
raise SystemExit(2)
HEARTBEAT = bytes.fromhex("00 00 00 00 80 DA")
def parse_int(text: str) -> int:
value = int(text, 0)
if not 0 <= value <= 0xFF:
raise argparse.ArgumentTypeError(f"must be a byte: {text}")
return value
def build_frame(prefix1: int, prefix2: int, command: int, state: int, value: int) -> bytes:
body = bytes([prefix1, prefix2, command, state, value])
checksum = 0x5A
for byte in body:
checksum ^= byte
return body + bytes([checksum])
def hex_preview(data: bytes) -> str:
return " ".join(f"{byte:02X}" for byte in data)
def make_logger(path: str | None):
log_file = open(path, "a", encoding="utf-8") if path else None
def emit(line: str) -> None:
print(line)
if log_file:
log_file.write(line + "\n")
log_file.flush()
return emit, log_file
def drain_rx(ser: serial.Serial, emit, until: float, frame_size: int) -> int:
buffer = bytearray()
interesting = 0
while time.monotonic() < until:
data = ser.read(64)
if not data:
continue
buffer.extend(data)
while len(buffer) >= frame_size:
frame = bytes(buffer[:frame_size])
del buffer[:frame_size]
stamp = dt.datetime.now().strftime("%H:%M:%S.%f")[:-3]
marker = "" if frame == HEARTBEAT else " NON_HEARTBEAT"
emit(f"{stamp} RX frame {frame_size:03d} {hex_preview(frame)}{marker}")
if frame != HEARTBEAT:
interesting += 1
return interesting
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Sweep checksum-valid command bytes and log non-heartbeat RX."
)
parser.add_argument("--port", required=True, help="serial port, for example COM5")
parser.add_argument("--baud", type=int, default=38400)
parser.add_argument("--start", type=parse_int, default=0x00)
parser.add_argument("--end", type=parse_int, default=0x20)
parser.add_argument("--prefix1", type=parse_int, default=0x00)
parser.add_argument("--prefix2", type=parse_int, default=0x00)
parser.add_argument("--state", type=parse_int, default=0x00)
parser.add_argument("--value", type=parse_int, default=0x80)
parser.add_argument(
"--settle",
type=float,
default=1.5,
help="seconds to listen before the sweep starts",
)
parser.add_argument(
"--after-each",
type=float,
default=0.8,
help="seconds to listen after each transmitted frame",
)
parser.add_argument(
"--after",
type=float,
default=3.0,
help="seconds to listen after the whole sweep",
)
parser.add_argument("--frame-size", type=int, default=6)
parser.add_argument("--log", help="append capture/transmit log to this file")
parser.add_argument(
"--stop-on-non-heartbeat",
action="store_true",
help="stop the sweep if the RCP sends any non-heartbeat frame",
)
parser.add_argument(
"--prompt-screen",
action="store_true",
help="after each command, prompt for the observed RCP screen state",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="print frames without opening the serial port",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.end < args.start:
raise SystemExit("--end must be >= --start")
frames = [
(command, build_frame(args.prefix1, args.prefix2, command, args.state, args.value))
for command in range(args.start, args.end + 1)
]
if args.dry_run:
for command, frame in frames:
print(f"cmd 0x{command:02X}: {hex_preview(frame)}")
return 0
emit, log_file = make_logger(args.log)
try:
with serial.Serial(
port=args.port,
baudrate=args.baud,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=0.05,
write_timeout=1.0,
rtscts=False,
dsrdtr=False,
xonxoff=False,
) as ser:
ser.reset_input_buffer()
emit(
f"Sweeping commands 0x{args.start:02X}-0x{args.end:02X} "
f"on {ser.port} at {ser.baudrate} 8N1"
)
drain_rx(ser, emit, time.monotonic() + args.settle, args.frame_size)
for command, frame in frames:
stamp = dt.datetime.now().strftime("%H:%M:%S.%f")[:-3]
emit(f"{stamp} TX cmd 0x{command:02X} frame {len(frame):03d} {hex_preview(frame)}")
ser.write(frame)
ser.flush()
interesting = drain_rx(
ser,
emit,
time.monotonic() + args.after_each,
args.frame_size,
)
if interesting and args.stop_on_non_heartbeat:
emit(f"Stopping after cmd 0x{command:02X}: non-heartbeat RX observed")
break
if args.prompt_screen:
screen = input(
f"Screen after cmd 0x{command:02X} "
"(blank = no change, q = stop): "
).strip()
if screen:
stamp = dt.datetime.now().strftime("%H:%M:%S.%f")[:-3]
emit(f"{stamp} SCREEN cmd 0x{command:02X} {screen}")
if screen.lower() in {"q", "quit", "stop"}:
break
drain_rx(ser, emit, time.monotonic() + args.after, args.frame_size)
except KeyboardInterrupt:
print("Stopped.")
return 0
except serial.SerialException as exc:
print(f"Serial error: {exc}", file=sys.stderr)
return 1
finally:
if log_file:
log_file.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())