diff --git a/docs/pt2-lamp-selector-map.md b/docs/pt2-lamp-selector-map.md new file mode 100644 index 0000000..ef3ca3f --- /dev/null +++ b/docs/pt2-lamp-selector-map.md @@ -0,0 +1,82 @@ +# PT2 Lamp And Panel Output Selector Map + +This note tracks bench-visible lamp/readout effects from CCU-to-RCP selector writes. + +## Current Model + +The panel lamps and seven-segment displays are driven by selector-table state, not by one monolithic "connected" flag. Command 0 writes into the primary/current tables, and several selectors immediately affect visible panel outputs while `CONNECT: OK` is alive. + +Known active-state foundation: + +- `E000[0x0000] = 0x8080` wakes/holds `CONNECT: OK`. +- `E000[0x008F]` drives shutter `EVS`/`OFF` style display state and iris AUTO side effects. +- `E000[0x0093]` drives at least white-balance and black/flare lamp state. + +## Bench Observations 2026-05-26 + +### `lamp-0093-lowbyte-sweep` + +Result: no new visible behavior beyond the already-known `0x0093` family. + +Interpretation: + +- The earlier `0x0093` mapping still stands. +- Individual low-byte probes inside a streamed `0x90xx` context did not reveal a new lamp in this run. +- Keep `0x9020` as a useful manual/baseline context and `0x90FF` / `0xFFFF` as black/flare AUTO positive controls. + +### `lamp-known-button-selector-probe` + +Visible result: + +- CAM button/lamp flashed on/off. +- CALL lamp flashed on/off. +- BARS and MASTER lamps flashed on/off. +- Camera tally changed red, then later green. +- Each visible output illuminated by itself, not as a broad all-lamps blast. + +Serial result: + +- The run stayed in normal `CONNECT: OK` response cadence. +- Each command-0 write produced an immediate `04 ...` readback-style frame and repeated `02 00 02 00 00 5A` active responses. + +Candidate mapping: + +| Selector/value pair | Current meaning | +| --- | --- | +| `0x0007 = 0x8000/0x0000` | strongest CAM POWER lamp candidate | +| `0x0015 = 0x8000/0x0000` | strongest CALL lamp candidate | +| `0x0012`, `0x0013`, `0x0016`, `0x0017`, `0x0018`, `0x001A` | BARS, MASTER, tally red/green candidates; exact assignment still needs isolation | + +This confirms that the host/CCU can directly drive panel lamps through selector-table writes. It also validates using the ROM dispatch-neighbor list around `0x0007` and `0x0015` as a high-value lamp map. + +### `lamp-broad-status-selector-sweep` + +Visible result: + +- KNEE AUTO lamp flashed a few times. +- No other new visible result was reported. + +Candidate selectors in that run: + +`0x0003`, `0x0040`, `0x0081`, `0x0092`, `0x00A7`, `0x00B7`, `0x00B9`, `0x0110` + +Interpretation: + +- KNEE AUTO is likely in this broader status cluster. +- Exact selector/value still needs isolation because the broad sweep changed several selectors in sequence. + +## Follow-Up Isolation Scenarios + +Run these with the console visible and record the exact label shown when each lamp changes: + +```powershell +.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-isolate-cam-call.json --parity E --log captures\lamp-isolate-cam-call.txt --result-json captures\lamp-isolate-cam-call-result.json +.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-isolate-known-neighbors.json --parity E --log captures\lamp-isolate-known-neighbors.txt --result-json captures\lamp-isolate-known-neighbors-result.json +.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-isolate-knee-status-selectors.json --parity E --log captures\lamp-isolate-knee-status-selectors.txt --result-json captures\lamp-isolate-knee-status-selectors-result.json +``` + +Method notes: + +- Record visible changes immediately during each labeled hold. Later `CONNECT: NOT ACT` cleanup is not selector evidence. +- If a selector causes a latch or unexpected mode, stop and keep the log instead of continuing the whole sweep. +- Prefer exact notes like `selector_0018_high -> tally red`, because the logs already preserve send timestamps and readback frames. diff --git a/docs/pt2-protocol.md b/docs/pt2-protocol.md index c7ebee7..c9d35c8 100644 --- a/docs/pt2-protocol.md +++ b/docs/pt2-protocol.md @@ -673,6 +673,18 @@ The first gated watch capture produced a new periodic active-state response: After treating those as expected gated refresh traffic, the capture had zero novel frames. A direct search found no known CALL/CAM POWER frames in that log. So the gated setup changed the session/status response shape, but it did not yet prove that physical controls beyond the already-known autonomous buttons are reporting. +## Lamp Selector Mapping + +Bench lamp sweeps now prove that several panel outputs are directly driven by command-0 selector writes while `CONNECT: OK` is alive. The current detailed map lives in `docs/pt2-lamp-selector-map.md`. + +Newest confirmed behavior: + +- `lamp-known-button-selector-probe` made CAM, CALL, BARS, MASTER, and camera tally outputs flash individually. +- `0x0007 = 0x8000/0x0000` is the strongest CAM POWER lamp candidate. +- `0x0015 = 0x8000/0x0000` is the strongest CALL lamp candidate. +- Neighbor selectors `0x0012`, `0x0013`, `0x0016`, `0x0017`, `0x0018`, and `0x001A` likely contain BARS/MASTER/tally red/green assignments, pending isolation. +- A broader status sweep made KNEE AUTO flash; candidate selectors are `0x0003`, `0x0040`, `0x0081`, `0x0092`, `0x00A7`, `0x00B7`, `0x00B9`, and `0x0110`. + ## What Is Still Unknown - The official PT2 names for commands and selectors. diff --git a/h8536/emulator/report_queue_probe.py b/h8536/emulator/report_queue_probe.py new file mode 100644 index 0000000..7569026 --- /dev/null +++ b/h8536/emulator/report_queue_probe.py @@ -0,0 +1,336 @@ +from __future__ import annotations + +import argparse +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from ..formatting import h16, parse_int +from .cli import load_rom +from .eeprom_image import write_eeprom_snapshot +from .runner import H8536Emulator +from .rx_probe import RunContext, _run_until, _rx_ready, format_frame +from .uart import UartTiming + + +CHECKSUM_SEED = 0x5A +REPORT_QUEUE_START = 0xF870 +REPORT_QUEUE_HEAD = 0xF9B0 +REPORT_QUEUE_TAIL = 0xF9B5 +TX_STAGING_START = 0xF850 +TX_FRAME_START = 0xF858 +TX_FRAME_LENGTH = 6 +CURRENT_TABLE_START = 0xE800 + + +@dataclass(frozen=True) +class ReportQueueProbeResult: + rom_path: Path + report_word: int + payload_selector: int + payload: int + expected_frame: bytes + boot_summary: str + steps: int + stopped_reason: str + tx_frames: tuple[bytes, ...] + staging_bytes: bytes + finalized_bytes: bytes + state: dict[str, int] + eeprom_writes: tuple[str, ...] + + @property + def emitted_expected_frame(self) -> bool: + return self.expected_frame in self.tx_frames + + def as_dict(self) -> dict[str, Any]: + return { + "kind": "h8536_report_queue_probe", + "rom_path": str(self.rom_path), + "report_word": f"0x{self.report_word:04X}", + "payload_selector": f"0x{self.payload_selector:04X}", + "payload": f"0x{self.payload:04X}", + "expected_frame": format_frame(self.expected_frame), + "emitted_expected_frame": self.emitted_expected_frame, + "boot_summary": self.boot_summary, + "steps": self.steps, + "stopped_reason": self.stopped_reason, + "tx_frames": [format_frame(frame) for frame in self.tx_frames], + "staging_bytes_f850_f855": format_frame(self.staging_bytes), + "finalized_bytes_f858_f85d": format_frame(self.finalized_bytes), + "state": {name: _format_state_value(name, value) for name, value in self.state.items()}, + "eeprom_writes": list(self.eeprom_writes), + } + + def lines(self) -> list[str]: + lines = [ + f"rom={self.rom_path}", + self.boot_summary, + f"report_word={h16(self.report_word)} payload_selector={h16(self.payload_selector)} payload={h16(self.payload)}", + f"expected_frame={format_frame(self.expected_frame)}", + f"emitted_expected_frame={int(self.emitted_expected_frame)}", + f"stopped={self.stopped_reason} steps={self.steps}", + "tx_frames=" + (" | ".join(format_frame(frame) for frame in self.tx_frames) or "none"), + f"staging_F850_F855={format_frame(self.staging_bytes)}", + f"finalized_F858_F85D={format_frame(self.finalized_bytes)}", + "state:", + ] + for name, value in self.state.items(): + width = 4 if name in {"queue_word", "current_table_word"} else 2 + lines.append(f" {name}=0x{value:0{width}X}") + if self.eeprom_writes: + lines.append("eeprom_writes:") + lines.extend(f" {line}" for line in self.eeprom_writes) + else: + lines.append("eeprom_writes=none") + return lines + + +def build_expected_report_frame(report_word: int, payload: int) -> bytes: + first, second, third = encode_report_header(report_word) + frame = bytes( + [ + first, + second, + third, + (payload >> 8) & 0xFF, + payload & 0xFF, + ] + ) + return frame + bytes([frame_checksum(frame)]) + + +def encode_report_header(report_word: int) -> tuple[int, int, int]: + raw = report_word & 0xFFFF + encoded_selector = _loc_6206_selector_encode(raw) + + r1 = _swap_bytes(raw) + r1 = (r1 & 0xFF00) | ((r1 & 0xFF) >> 1) + r2 = r1 & 0xFF + + first = r1 & 0x07 + third = encoded_selector & 0xFF + swapped_encoded = _swap_bytes(encoded_selector) + second = (swapped_encoded & 0xFF) | (r2 & 0x78) + return first & 0xFF, second & 0xFF, third & 0xFF + + +def report_payload_selector(report_word: int) -> int: + return report_word & 0x01FF + + +def frame_checksum(frame_without_checksum: bytes) -> int: + checksum = CHECKSUM_SEED + for value in frame_without_checksum[: TX_FRAME_LENGTH - 1]: + checksum ^= value + return checksum & 0xFF + + +def run_report_queue_probe( + *, + report_word: int = 0x0204, + payload: int = 0x0000, + queue_index: int = 0, + rom_path: Path | None = None, + boot_steps: int = 250_000, + max_steps: int = 200_000, + eeprom_seed: str = "blank", + eeprom_image: bytes | None = None, + tx_wire_timing: bool = False, + uart_baud: int = 38_400, + uart_format: str = "8E1", + p9_fast_path: bool = True, + p9_fast_input: int = 0xFF, + p7_input: int = 0xFF, +) -> tuple[H8536Emulator, ReportQueueProbeResult]: + rom_bytes, discovered_rom_path = load_rom(rom_path) + emulator = H8536Emulator( + rom_bytes, + p9_fast_path_enabled=p9_fast_path, + p9_fast_default_input_byte=p9_fast_input, + p7_input=p7_input, + eeprom_seed=eeprom_seed, + sci1_tx_timing=UartTiming.from_format(uart_format, baud=uart_baud) if tx_wire_timing else None, + ) + if eeprom_image is not None: + emulator.memory.load_eeprom_image(eeprom_image) + + boot_context = RunContext() + boot_used, boot_reason = _run_until(emulator, boot_steps, _rx_ready, boot_context) + boot_summary = ( + f"boot={boot_reason} steps={boot_used} pc={h16(emulator.cpu.pc)} " + f"rx_serviceable={int(_rx_ready(emulator))} lcd_display={emulator.memory.lcd.display_text(lines=4)!r}" + ) + + payload_selector = report_payload_selector(report_word) + expected_frame = build_expected_report_frame(report_word, payload) + _seed_report_queue(emulator, report_word=report_word, payload=payload, queue_index=queue_index) + emulator.memory.clear_eeprom_write_log() + + start_frame_count = len(emulator.sci1.tx_frames) + + def emitted_expected(inner: H8536Emulator) -> bool: + return expected_frame in inner.sci1.tx_frames[start_frame_count:] + + run_context = RunContext() + steps, reason = _run_until(emulator, max_steps, emitted_expected, run_context) + stopped_reason = "expected_frame" if reason == "predicate" else reason + + state = _state_snapshot(emulator, queue_index=queue_index, payload_selector=payload_selector) + staging = bytes(emulator.memory.read8(address) for address in range(TX_STAGING_START, TX_STAGING_START + TX_FRAME_LENGTH)) + finalized = bytes(emulator.memory.read8(address) for address in range(TX_FRAME_START, TX_FRAME_START + TX_FRAME_LENGTH)) + result = ReportQueueProbeResult( + rom_path=discovered_rom_path, + report_word=report_word & 0xFFFF, + payload_selector=payload_selector, + payload=payload & 0xFFFF, + expected_frame=expected_frame, + boot_summary=boot_summary, + steps=steps, + stopped_reason=stopped_reason, + tx_frames=tuple(emulator.sci1.tx_frames[start_frame_count:]), + staging_bytes=staging, + finalized_bytes=finalized, + state=state, + eeprom_writes=tuple(emulator.memory.p9_bus.x24164_bus.write_log_lines(limit=80)), + ) + return emulator, result + + +def build_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Seed the ROM report queue and let the H8/536 ROM build the matching SCI1 TX frame." + ) + parser.add_argument("--rom", type=Path, help="ROM image path; defaults to ROM/M27C512@DIP28_1.BIN") + parser.add_argument("--report-word", type=parse_int, default=0x0204, help="word to place in F870 report queue") + parser.add_argument("--payload", type=parse_int, default=0x0000, help="word to place in E800[current selector]") + parser.add_argument("--queue-index", type=parse_int, default=0, help="F870 queue slot to seed") + parser.add_argument("--boot-steps", type=int, default=250_000, help="maximum boot steps before queue seeding") + parser.add_argument("--max-steps", type=int, default=200_000, help="maximum steps after queue seeding") + parser.add_argument("--tx-wire-timing", action="store_true", help="model UART TX character time between TXI bytes") + parser.add_argument("--uart-baud", type=parse_int, default=38_400, help="baud rate used with --tx-wire-timing") + parser.add_argument("--uart-format", default="8E1", help="UART format used with --tx-wire-timing") + parser.add_argument("--no-p9-fast-path", action="store_true", help="disable shortcut handling for known P9 routines") + parser.add_argument("--p9-fast-input", type=parse_int, default=0xFF, help="default byte returned by P9 fast-path reads") + parser.add_argument("--p7-input", type=parse_int, default=0xFF, help="external P7 pin state; DIP-off default is 0xFF") + parser.add_argument("--eeprom-seed", choices=("blank", "factory"), default="blank", help="initial X24164/shadow state") + parser.add_argument("--eeprom-load", type=Path, help="load a 0x1000-byte logical EEPROM image before booting") + parser.add_argument("--eeprom-save", type=Path, help="save the final EEPROM image") + parser.add_argument("--eeprom-report", type=Path, help="write a readable EEPROM snapshot") + parser.add_argument("--eeprom-report-json", type=Path, help="write a structured EEPROM snapshot") + parser.add_argument("--eeprom-report-include-image", action="store_true", help="include full EEPROM image in JSON") + parser.add_argument("--json", action="store_true", help="print JSON instead of text") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_arg_parser().parse_args(argv) + emulator, result = run_report_queue_probe( + report_word=args.report_word, + payload=args.payload, + queue_index=args.queue_index, + rom_path=args.rom, + boot_steps=args.boot_steps, + max_steps=args.max_steps, + eeprom_seed=args.eeprom_seed, + eeprom_image=args.eeprom_load.read_bytes() if args.eeprom_load else None, + tx_wire_timing=args.tx_wire_timing, + uart_baud=args.uart_baud, + uart_format=args.uart_format, + p9_fast_path=not args.no_p9_fast_path, + p9_fast_input=args.p9_fast_input, + p7_input=args.p7_input, + ) + + if args.json: + print(json.dumps(result.as_dict(), indent=2, sort_keys=True)) + else: + for line in result.lines(): + print(line) + + if args.eeprom_save: + args.eeprom_save.parent.mkdir(parents=True, exist_ok=True) + args.eeprom_save.write_bytes(emulator.memory.dump_eeprom_image()) + print(f"eeprom_saved={args.eeprom_save}") + if args.eeprom_report: + write_eeprom_snapshot(emulator.memory, args.eeprom_report, rom_bytes=emulator.memory.rom.data) + print(f"eeprom_report={args.eeprom_report}") + if args.eeprom_report_json: + write_eeprom_snapshot( + emulator.memory, + args.eeprom_report_json, + rom_bytes=emulator.memory.rom.data, + as_json=True, + include_image_hex=args.eeprom_report_include_image, + ) + print(f"eeprom_report_json={args.eeprom_report_json}") + return 0 + + +def _seed_report_queue( + emulator: H8536Emulator, + *, + report_word: int, + payload: int, + queue_index: int, +) -> None: + queue_index &= 0x7F + queue_address = REPORT_QUEUE_START + (queue_index * 2) + payload_address = CURRENT_TABLE_START + (report_payload_selector(report_word) * 2) + memory = emulator.memory + memory.write16(queue_address, report_word) + memory.write16(payload_address, payload) + memory.write8(REPORT_QUEUE_TAIL, queue_index) + memory.write8(REPORT_QUEUE_HEAD, (queue_index + 1) & 0x7F) + memory.write8(0xF9C0, 0x00) + memory.write8(0xF9C3, 0x00) + memory.write8(0xFAA2, 0x00) + memory.write8(0xFAA3, 0x00) + + +def _state_snapshot(emulator: H8536Emulator, *, queue_index: int, payload_selector: int) -> dict[str, int]: + memory = emulator.memory + queue_address = REPORT_QUEUE_START + ((queue_index & 0x7F) * 2) + payload_address = CURRENT_TABLE_START + ((payload_selector & 0x01FF) * 2) + return { + "queue_head_F9B0": memory.read8(REPORT_QUEUE_HEAD), + "queue_tail_F9B5": memory.read8(REPORT_QUEUE_TAIL), + "queue_word": memory.read16(queue_address), + "current_table_word": memory.read16(payload_address), + "tx_gate_F9C0": memory.read8(0xF9C0), + "rx_index_F9C3": memory.read8(0xF9C3), + "heartbeat_gate_F9C4": memory.read8(0xF9C4), + "session_flags_FAA2": memory.read8(0xFAA2), + "pending_mask_FAA3": memory.read8(0xFAA3), + } + + +def _loc_6206_selector_encode(value: int) -> int: + value &= 0x01FF + if value <= 0x007F: + return value + if value <= 0x017F: + return ((value - 0x0080) + 0x0100) & 0xFFFF + return ((value - 0x0180) + 0x0200) & 0xFFFF + + +def _swap_bytes(value: int) -> int: + value &= 0xFFFF + return ((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF) + + +def _format_state_value(name: str, value: int) -> str: + width = 4 if name in {"queue_word", "current_table_word"} else 2 + return f"0x{value:0{width}X}" + + +__all__ = [ + "ReportQueueProbeResult", + "build_expected_report_frame", + "encode_report_header", + "frame_checksum", + "main", + "report_payload_selector", + "run_report_queue_probe", +] diff --git a/h8536_emulator_report_queue_probe.py b/h8536_emulator_report_queue_probe.py new file mode 100644 index 0000000..108ecd7 --- /dev/null +++ b/h8536_emulator_report_queue_probe.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""Compatibility wrapper for the H8/536 report-queue emulator probe.""" + +from h8536.emulator.report_queue_probe import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scenarios/lamp-0093-lowbyte-sweep.json b/scenarios/lamp-0093-lowbyte-sweep.json new file mode 100644 index 0000000..1fd2a2f --- /dev/null +++ b/scenarios/lamp-0093-lowbyte-sweep.json @@ -0,0 +1,160 @@ +{ + "name": "lamp-0093-lowbyte-sweep", + "notes": [ + "Hold CONNECT OK and vary only the low byte of E000[0x0093] while keeping high byte 0x90.", + "Record white-balance AUTO/PRESET/MANUAL, black/flare AUTO/MANUAL/FLARE, iris AUTO, shutter display, and LCD.", + "Known references: 0x9020 has behaved like a black/flare manual context; 0x90FF has produced black/flare auto toggles." + ], + "steps": [ + { + "action": "power_cycle", + "off_seconds": 1.5 + }, + { + "action": "wait_ready", + "heartbeats": 2, + "timeout": 10.0, + "require": true + }, + { + "action": "drain", + "seconds": 0.25 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_1", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_2", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "baseline_0093_9020", + "frame": "00 01 13 90 20 F8", + "listen": 0.60 + }, + { + "action": "repeat", + "count": 2, + "steps": [ + { + "action": "send", + "label": "candidate_0093_9000", + "frame": "00 01 13 90 00 D8", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9000", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9001", + "frame": "00 01 13 90 01 D9", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9001", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9002", + "frame": "00 01 13 90 02 DA", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9002", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9004", + "frame": "00 01 13 90 04 DC", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9004", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9008", + "frame": "00 01 13 90 08 D0", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9008", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9010", + "frame": "00 01 13 90 10 C8", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9010", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9040", + "frame": "00 01 13 90 40 98", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9040", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "candidate_0093_9080", + "frame": "00 01 13 90 80 58", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_9080", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + }, + { + "action": "send", + "label": "positive_0093_90ff", + "frame": "00 01 13 90 FF 27", + "listen": 0.55 + }, + { + "action": "send", + "label": "baseline_0093_9020_after_90ff", + "frame": "00 01 13 90 20 F8", + "listen": 0.55 + } + ] + }, + { + "action": "listen", + "seconds": 0.80 + } + ] +} diff --git a/scenarios/lamp-broad-status-selector-sweep.json b/scenarios/lamp-broad-status-selector-sweep.json new file mode 100644 index 0000000..15cadfe --- /dev/null +++ b/scenarios/lamp-broad-status-selector-sweep.json @@ -0,0 +1,184 @@ +{ + "name": "lamp-broad-status-selector-sweep", + "notes": [ + "Cautious broader primary-table sweep for ROM-mined status selectors that may drive lamps/readouts.", + "Each selector is tested with 0x8000 then 0xFFFF, followed by 0x0000 clear. Record visible lamp/readout changes inside each short window.", + "If the panel enters CONNECT NOT ACT, stop the run and note the selector label immediately before the transition." + ], + "steps": [ + { + "action": "power_cycle", + "off_seconds": 1.5 + }, + { + "action": "wait_ready", + "heartbeats": 2, + "timeout": 10.0, + "require": true + }, + { + "action": "drain", + "seconds": 0.25 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_1", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_2", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_0003_default_high", + "frame": "00 00 03 80 00 D9", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0003_all_bits", + "frame": "00 00 03 FF FF 59", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0003_clear", + "frame": "00 00 03 00 00 59", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_0040_high", + "frame": "00 00 40 80 00 9A", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0040_all_bits", + "frame": "00 00 40 FF FF 1A", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0040_clear", + "frame": "00 00 40 00 00 1A", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_0081_high", + "frame": "00 01 01 80 00 DA", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0081_all_bits", + "frame": "00 01 01 FF FF 5A", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0081_clear", + "frame": "00 01 01 00 00 5A", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_0092_high", + "frame": "00 01 12 80 00 C9", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0092_all_bits", + "frame": "00 01 12 FF FF 49", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0092_clear", + "frame": "00 01 12 00 00 49", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_00a7_high", + "frame": "00 01 27 80 00 FC", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_00a7_all_bits", + "frame": "00 01 27 FF FF 7C", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_00a7_clear", + "frame": "00 01 27 00 00 7C", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_00b7_high", + "frame": "00 01 37 80 00 EC", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_00b7_all_bits", + "frame": "00 01 37 FF FF 6C", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_00b7_clear", + "frame": "00 01 37 00 00 6C", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_00b9_high", + "frame": "00 01 39 80 00 E2", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_00b9_all_bits", + "frame": "00 01 39 FF FF 62", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_00b9_clear", + "frame": "00 01 39 00 00 62", + "listen": 0.40 + }, + { + "action": "send", + "label": "selector_0110_high", + "frame": "00 01 90 80 00 4B", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0110_all_bits", + "frame": "00 01 90 FF FF CB", + "listen": 0.80 + }, + { + "action": "send", + "label": "selector_0110_clear", + "frame": "00 01 90 00 00 CB", + "listen": 0.40 + }, + { + "action": "listen", + "seconds": 0.80 + } + ] +} diff --git a/scenarios/lamp-isolate-cam-call.json b/scenarios/lamp-isolate-cam-call.json new file mode 100644 index 0000000..f1b425f --- /dev/null +++ b/scenarios/lamp-isolate-cam-call.json @@ -0,0 +1,82 @@ +{ + "name": "lamp-isolate-cam-call", + "notes": [ + "Isolate the two strongest known button/lamp selectors from the previous probe.", + "Record whether 0x0007 high/low maps to CAM POWER and whether 0x0015 high/low maps to CALL.", + "Each state is held long enough to see a visible lamp transition while staying below the usual inactive timeout." + ], + "steps": [ + { + "action": "power_cycle", + "off_seconds": 1.5 + }, + { + "action": "wait_ready", + "heartbeats": 2, + "timeout": 10.0, + "require": true + }, + { + "action": "drain", + "seconds": 0.25 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_1", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_2", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "repeat", + "count": 3, + "steps": [ + { + "action": "send", + "label": "cam_candidate_0007_high", + "frame": "00 00 07 80 00 DD", + "listen": 1.15 + }, + { + "action": "send", + "label": "cam_candidate_0007_low", + "frame": "00 00 07 00 00 5D", + "listen": 0.85 + } + ] + }, + { + "action": "send", + "label": "selector_zero_ok_refresh_before_call", + "frame": "00 00 00 80 00 DA", + "listen": 0.50 + }, + { + "action": "repeat", + "count": 3, + "steps": [ + { + "action": "send", + "label": "call_candidate_0015_high", + "frame": "00 00 15 80 00 CF", + "listen": 1.15 + }, + { + "action": "send", + "label": "call_candidate_0015_low", + "frame": "00 00 15 00 00 4F", + "listen": 0.85 + } + ] + }, + { + "action": "listen", + "seconds": 0.80 + } + ] +} diff --git a/scenarios/lamp-isolate-knee-status-selectors.json b/scenarios/lamp-isolate-knee-status-selectors.json new file mode 100644 index 0000000..48b7418 --- /dev/null +++ b/scenarios/lamp-isolate-knee-status-selectors.json @@ -0,0 +1,232 @@ +{ + "name": "lamp-isolate-knee-status-selectors", + "notes": [ + "Isolate broad status selectors after the prior sweep made the KNEE AUTO lamp flash.", + "Each candidate is tested high, clear, all-bits, clear. Record the exact console label when KNEE AUTO changes.", + "Stop early if the panel latches or falls out of CONNECT OK." + ], + "steps": [ + { + "action": "power_cycle", + "off_seconds": 1.5 + }, + { + "action": "wait_ready", + "heartbeats": 2, + "timeout": 10.0, + "require": true + }, + { + "action": "drain", + "seconds": 0.25 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_1", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_2", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_0003_high", + "frame": "00 00 03 80 00 D9", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0003_clear_after_high", + "frame": "00 00 03 00 00 59", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0003_all_bits", + "frame": "00 00 03 FF FF 59", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0003_clear_after_all", + "frame": "00 00 03 00 00 59", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0040_high", + "frame": "00 00 40 80 00 9A", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0040_clear_after_high", + "frame": "00 00 40 00 00 1A", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0040_all_bits", + "frame": "00 00 40 FF FF 1A", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0040_clear_after_all", + "frame": "00 00 40 00 00 1A", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0081_high", + "frame": "00 01 01 80 00 DA", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0081_clear_after_high", + "frame": "00 01 01 00 00 5A", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0081_all_bits", + "frame": "00 01 01 FF FF 5A", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0081_clear_after_all", + "frame": "00 01 01 00 00 5A", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0092_high", + "frame": "00 01 12 80 00 C9", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0092_clear_after_high", + "frame": "00 01 12 00 00 49", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0092_all_bits", + "frame": "00 01 12 FF FF 49", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0092_clear_after_all", + "frame": "00 01 12 00 00 49", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_00a7_high", + "frame": "00 01 27 80 00 FC", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_00a7_clear_after_high", + "frame": "00 01 27 00 00 7C", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_00a7_all_bits", + "frame": "00 01 27 FF FF 7C", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_00a7_clear_after_all", + "frame": "00 01 27 00 00 7C", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_00b7_high", + "frame": "00 01 37 80 00 EC", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_00b7_clear_after_high", + "frame": "00 01 37 00 00 6C", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_00b7_all_bits", + "frame": "00 01 37 FF FF 6C", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_00b7_clear_after_all", + "frame": "00 01 37 00 00 6C", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_00b9_high", + "frame": "00 01 39 80 00 E2", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_00b9_clear_after_high", + "frame": "00 01 39 00 00 62", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_00b9_all_bits", + "frame": "00 01 39 FF FF 62", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_00b9_clear_after_all", + "frame": "00 01 39 00 00 62", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0110_high", + "frame": "00 01 90 80 00 4B", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0110_clear_after_high", + "frame": "00 01 90 00 00 CB", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0110_all_bits", + "frame": "00 01 90 FF FF CB", + "listen": 1.20 + }, + { + "action": "send", + "label": "selector_0110_clear_after_all", + "frame": "00 01 90 00 00 CB", + "listen": 0.70 + }, + { + "action": "listen", + "seconds": 0.80 + } + ] +} diff --git a/scenarios/lamp-isolate-known-neighbors.json b/scenarios/lamp-isolate-known-neighbors.json new file mode 100644 index 0000000..2e29469 --- /dev/null +++ b/scenarios/lamp-isolate-known-neighbors.json @@ -0,0 +1,118 @@ +{ + "name": "lamp-isolate-known-neighbors", + "notes": [ + "Isolate ROM dispatch neighbors that caused BARS, MASTER, and camera tally changes in the prior run.", + "Watch the console label and record exact mappings such as selector_0018_high -> tally red.", + "Each selector is tested high then low twice." + ], + "steps": [ + { + "action": "power_cycle", + "off_seconds": 1.5 + }, + { + "action": "wait_ready", + "heartbeats": 2, + "timeout": 10.0, + "require": true + }, + { + "action": "drain", + "seconds": 0.25 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_1", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_2", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "repeat", + "count": 2, + "steps": [ + { + "action": "send", + "label": "selector_0012_high", + "frame": "00 00 12 80 00 C8", + "listen": 1.10 + }, + { + "action": "send", + "label": "selector_0012_low", + "frame": "00 00 12 00 00 48", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0013_high", + "frame": "00 00 13 80 00 C9", + "listen": 1.10 + }, + { + "action": "send", + "label": "selector_0013_low", + "frame": "00 00 13 00 00 49", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0016_high", + "frame": "00 00 16 80 00 CC", + "listen": 1.10 + }, + { + "action": "send", + "label": "selector_0016_low", + "frame": "00 00 16 00 00 4C", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0017_high", + "frame": "00 00 17 80 00 CD", + "listen": 1.10 + }, + { + "action": "send", + "label": "selector_0017_low", + "frame": "00 00 17 00 00 4D", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_0018_high", + "frame": "00 00 18 80 00 C2", + "listen": 1.10 + }, + { + "action": "send", + "label": "selector_0018_low", + "frame": "00 00 18 00 00 42", + "listen": 0.70 + }, + { + "action": "send", + "label": "selector_001a_high", + "frame": "00 00 1A 80 00 C0", + "listen": 1.10 + }, + { + "action": "send", + "label": "selector_001a_low", + "frame": "00 00 1A 00 00 40", + "listen": 0.70 + } + ] + }, + { + "action": "listen", + "seconds": 0.80 + } + ] +} diff --git a/scenarios/lamp-known-button-selector-probe.json b/scenarios/lamp-known-button-selector-probe.json new file mode 100644 index 0000000..f31ae34 --- /dev/null +++ b/scenarios/lamp-known-button-selector-probe.json @@ -0,0 +1,136 @@ +{ + "name": "lamp-known-button-selector-probe", + "notes": [ + "Probe host writes to selector families already tied to autonomous CAM POWER and CALL reports, plus nearby ROM dispatch entries.", + "Record CAM POWER, CALL, BARS, AUTO buttons, and any lamp/display movement immediately during each hold.", + "This script avoids command 5 copy/latch selectors; it only writes command-0 primary table values." + ], + "steps": [ + { + "action": "power_cycle", + "off_seconds": 1.5 + }, + { + "action": "wait_ready", + "heartbeats": 2, + "timeout": 10.0, + "require": true + }, + { + "action": "drain", + "seconds": 0.25 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_1", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "selector_zero_ok_seed_2", + "frame": "00 00 00 80 00 DA", + "listen": 0.60 + }, + { + "action": "send", + "label": "cam_power_candidate_0007_high", + "frame": "00 00 07 80 00 DD", + "listen": 1.00 + }, + { + "action": "send", + "label": "cam_power_candidate_0007_low", + "frame": "00 00 07 00 00 5D", + "listen": 1.00 + }, + { + "action": "send", + "label": "call_candidate_0015_high", + "frame": "00 00 15 80 00 CF", + "listen": 1.00 + }, + { + "action": "send", + "label": "call_candidate_0015_low", + "frame": "00 00 15 00 00 4F", + "listen": 1.00 + }, + { + "action": "send", + "label": "neighbor_0012_high", + "frame": "00 00 12 80 00 C8", + "listen": 0.90 + }, + { + "action": "send", + "label": "neighbor_0012_low", + "frame": "00 00 12 00 00 48", + "listen": 0.50 + }, + { + "action": "send", + "label": "neighbor_0013_high", + "frame": "00 00 13 80 00 C9", + "listen": 0.90 + }, + { + "action": "send", + "label": "neighbor_0013_low", + "frame": "00 00 13 00 00 49", + "listen": 0.50 + }, + { + "action": "send", + "label": "neighbor_0016_high", + "frame": "00 00 16 80 00 CC", + "listen": 0.90 + }, + { + "action": "send", + "label": "neighbor_0016_low", + "frame": "00 00 16 00 00 4C", + "listen": 0.50 + }, + { + "action": "send", + "label": "neighbor_0017_high", + "frame": "00 00 17 80 00 CD", + "listen": 0.90 + }, + { + "action": "send", + "label": "neighbor_0017_low", + "frame": "00 00 17 00 00 4D", + "listen": 0.50 + }, + { + "action": "send", + "label": "neighbor_0018_high", + "frame": "00 00 18 80 00 C2", + "listen": 0.90 + }, + { + "action": "send", + "label": "neighbor_0018_low", + "frame": "00 00 18 00 00 42", + "listen": 0.50 + }, + { + "action": "send", + "label": "neighbor_001a_high", + "frame": "00 00 1A 80 00 C0", + "listen": 0.90 + }, + { + "action": "send", + "label": "neighbor_001a_low", + "frame": "00 00 1A 00 00 40", + "listen": 0.50 + }, + { + "action": "listen", + "seconds": 0.80 + } + ] +} diff --git a/tests/test_emulator_report_queue_probe.py b/tests/test_emulator_report_queue_probe.py new file mode 100644 index 0000000..c5d1c98 --- /dev/null +++ b/tests/test_emulator_report_queue_probe.py @@ -0,0 +1,26 @@ +import unittest + +from h8536.emulator.report_queue_probe import ( + build_expected_report_frame, + encode_report_header, + report_payload_selector, +) + + +class EmulatorReportQueueProbeTest(unittest.TestCase): + def test_report_word_0204_builds_observed_gated_active_frame(self): + self.assertEqual(encode_report_header(0x0204), (0x01, 0x00, 0x04)) + self.assertEqual(report_payload_selector(0x0204), 0x0004) + self.assertEqual(build_expected_report_frame(0x0204, 0x0000), bytes.fromhex("01 00 04 00 00 5F")) + + def test_report_word_0404_builds_observed_transition_frame(self): + self.assertEqual(encode_report_header(0x0404), (0x02, 0x00, 0x04)) + self.assertEqual(report_payload_selector(0x0404), 0x0004) + self.assertEqual(build_expected_report_frame(0x0404, 0x0000), bytes.fromhex("02 00 04 00 00 5C")) + + def test_payload_bytes_feed_frame_value_and_checksum(self): + self.assertEqual(build_expected_report_frame(0x0204, 0x1234), bytes.fromhex("01 00 04 12 34 79")) + + +if __name__ == "__main__": + unittest.main()