#!/usr/bin/env python3 """Build a fresh-boot webcam sweep for ROM-derived panel-output candidates.""" from __future__ import annotations import argparse import json from dataclasses import dataclass from pathlib import Path CHECKSUM_SEED = 0x5A CONNECT_OK_FRAME = "00 00 00 80 00 DA" @dataclass(frozen=True) class Candidate: label: str selector: int value: int note: str CANDIDATES: tuple[Candidate, ...] = ( Candidate("positive_0013_4000_iris_mblack_link", 0x0013, 0x4000, "known IRIS/M.BLACK LINK lamp positive control"), Candidate("positive_0013_8000_slave", 0x0013, 0x8000, "known SLAVE lamp positive control"), Candidate("positive_0015_8000_call", 0x0015, 0x8000, "known CALL lamp positive control"), Candidate("positive_0017_8000_bars", 0x0017, 0x8000, "known BARS lamp positive control"), Candidate("positive_0110_8000_knee_auto", 0x0110, 0x8000, "known KNEE AUTO positive control"), Candidate("rom_001a_0808_multi_button_default", 0x001A, 0x0808, "F6D3 group default/fallback value"), Candidate("rom_001a_2020_f6d3_bit3_family", 0x001A, 0x2020, "F6D3.3-style packed state candidate"), Candidate("rom_001a_4040_f6d3_bit4_family", 0x001A, 0x4040, "F6D3.4-style packed state candidate"), Candidate("rom_001a_8080_f6d3_bit5_family", 0x001A, 0x8080, "F6D3.5-style packed state candidate"), Candidate("rom_006b_8000_f6d4_bit6_candidate", 0x006B, 0x8000, "F6D4.6 handler report value"), Candidate("rom_0083_0004_f6d0_step_candidate", 0x0083, 0x0004, "F6D0.1 lower-step value candidate"), Candidate("rom_0083_4000_high_tag_candidate", 0x0083, 0x4000, "0x0083 high-bit/tag candidate"), Candidate("rom_0083_2000_high_tag_candidate", 0x0083, 0x2000, "0x0083 high-bit/tag candidate"), Candidate("rom_008f_8000_f6d0_bit7_local", 0x008F, 0x8000, "F6D0.7 local SHUTTER/OTHERS report bit"), Candidate("rom_008f_2000_f6d0_bit6_local", 0x008F, 0x2000, "F6D0.6 local SHUTTER/OTHERS report bit"), Candidate("known_008f_0800_evs_display", 0x008F, 0x0800, "known EVS/shutter display positive control"), Candidate("known_008f_1000_off_display", 0x008F, 0x1000, "known OFF/shutter display positive control"), Candidate("rom_0093_1020_f6dc_bit5_context", 0x0093, 0x1020, "F6DC.5 handler context candidate"), Candidate("rom_0093_4040_f6dc_bit4_context", 0x0093, 0x4040, "F6DC.4 handler context candidate"), Candidate("rom_0093_8040_f6dc_bit3_context", 0x0093, 0x8040, "F6DC.3 handler context candidate"), Candidate("rom_0093_0020_f6dc_bit1_context", 0x0093, 0x0020, "F6DC.1 handler low-field candidate"), Candidate("rom_0093_0040_f6dc_bit0_context", 0x0093, 0x0040, "F6DC.0 handler low-field candidate"), Candidate("rom_009a_0800_iris_auto_candidate", 0x009A, 0x0800, "F6DB.3 IRIS AUTO report candidate"), Candidate("rom_00b7_2000_f6d4_bit0_bundle", 0x00B7, 0x2000, "F6D4.0 bundle selector candidate"), Candidate("rom_00b9_4000_f6dc_bit7_candidate", 0x00B9, 0x4000, "F6DC.7 handler value candidate"), Candidate("rom_00c4_8000_f6d4_bit0_bundle", 0x00C4, 0x8000, "F6D4.0 bundle selector candidate"), Candidate("rom_00c6_8000_f6d4_bit0_bundle", 0x00C6, 0x8000, "F6D4.0 bundle selector candidate"), Candidate("rom_00f8_8000_f6d4_bit1_candidate", 0x00F8, 0x8000, "F6D4.1 handler candidate"), ) def main() -> int: args = build_arg_parser().parse_args() scenario = build_scenario(args) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8") print(f"wrote {args.output}") print(f"candidates={len(CANDIDATES)} snapshots={len(CANDIDATES)}") print(f"estimated_hold_time={len(CANDIDATES) * args.listen / 60:.1f}min plus power-cycle/ready time") return 0 def build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "output", nargs="?", type=Path, default=Path("scenarios/panel-atlas-rom-button-output-candidates-v1.json"), ) parser.add_argument("--listen", type=float, default=0.75, help="seconds to listen after each candidate send") parser.add_argument("--ok-listen", type=float, default=0.25, help="seconds to listen after CONNECT OK seeds") parser.add_argument("--drain", type=float, default=0.25, help="seconds to drain after ready") parser.add_argument("--off-seconds", type=float, default=1.5, help="relay power-off time before each candidate") parser.add_argument("--ready-heartbeats", type=int, default=2, help="heartbeats before each candidate") parser.add_argument("--ready-timeout", type=float, default=10.0, help="ready wait timeout") return parser def build_scenario(args: argparse.Namespace) -> dict[str, object]: steps: list[dict[str, object]] = [] for index, candidate in enumerate(CANDIDATES, start=1): steps.extend(candidate_steps(args, index, candidate)) steps.append({"action": "listen", "seconds": 0.8}) return { "name": "panel-atlas-rom-button-output-candidates-v1", "notes": [ "Fresh-boot webcam sweep for ROM-derived button/report output candidates.", "This deliberately skips physical RCP button presses: each candidate sends command 0 directly.", "Each candidate gets its own power-cycle/CONNECT-OK baseline to reduce latch contamination.", "Candidate snapshots only are enabled; setup, CONNECT OK seeds, and clears should not produce webcam images.", "Run with --camera-index 4 --snapshot-delays 0.5 on the current bench.", ], "steps": steps, } def candidate_steps(args: argparse.Namespace, index: int, candidate: Candidate) -> list[dict[str, object]]: label_base = f"case{index:03d}_{candidate.label}" return [ {"action": "power_cycle", "off_seconds": args.off_seconds}, { "action": "wait_ready", "heartbeats": args.ready_heartbeats, "timeout": args.ready_timeout, "require": True, }, {"action": "drain", "seconds": args.drain}, send_step(f"{label_base}_ok_seed_1", CONNECT_OK_FRAME, args.ok_listen, snapshot=False), send_step(f"{label_base}_ok_seed_2", CONNECT_OK_FRAME, args.ok_listen, snapshot=False), { "action": "note", "message": ( f"{label_base}: selector 0x{candidate.selector:04X}=0x{candidate.value:04X}; " f"{candidate.note}" ), }, send_step(label_base, frame_hex(0x00, candidate.selector, candidate.value), args.listen, snapshot=True), send_step( f"{label_base}_clear", frame_hex(0x00, candidate.selector, 0x0000), 0.12, snapshot=False, ), ] def send_step(label: str, frame: str, listen: float, *, snapshot: bool) -> dict[str, object]: step: dict[str, object] = { "action": "send", "label": label, "frame": frame, "listen": listen, } if not snapshot: step["snapshot"] = False return step def frame_hex(command: int, selector: int, value: int) -> str: selector_hi, selector_lo = selector_bytes(selector) data = bytes([command & 0xFF, selector_hi, selector_lo, (value >> 8) & 0xFF, value & 0xFF]) return " ".join(f"{byte:02X}" for byte in data + bytes([frame_checksum(data)])) def selector_bytes(selector: int) -> tuple[int, int]: selector &= 0x01FF if selector <= 0x007F: return 0x00, selector if selector <= 0x017F: return 0x01, selector - 0x0080 return 0x02, selector - 0x0180 def frame_checksum(data: bytes) -> int: value = CHECKSUM_SEED for byte in data[:5]: value ^= byte & 0xFF return value & 0xFF if __name__ == "__main__": raise SystemExit(main())