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", ]