1
0
This commit is contained in:
Aiden
2026-05-27 11:50:10 +10:00
parent 0d099235c5
commit c0304c575c
55 changed files with 26035 additions and 16 deletions

View File

@@ -57,6 +57,7 @@ from .cpu import CPUState
from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent
from .memory import MemoryAccess, MemoryMap, describe_regions
from .panel import PanelAction, PanelInjection, PanelInput, parse_panel_action, resolve_panel_input
from .peripherals import LCD, P9TraceEvent, X24164Bus, X24164Device, X24164TraceEvent, factory_default_words_from_rom
from .runner import H8536Emulator, RunReport
from .sci import SCI1, SciTxEvent
@@ -97,6 +98,9 @@ __all__ = [
"P9FastPath",
"P9FastPathConfig",
"P9FastPathEvent",
"PanelAction",
"PanelInjection",
"PanelInput",
"P9TraceEvent",
"RAMCR",
"REGISTER_FIELD_END",
@@ -137,4 +141,6 @@ __all__ = [
"factory_default_words_from_rom",
"load_rom",
"main",
"parse_panel_action",
"resolve_panel_input",
]

203
h8536/emulator/panel.py Normal file
View File

@@ -0,0 +1,203 @@
from __future__ import annotations
from dataclasses import dataclass
from ..formatting import h16
@dataclass(frozen=True)
class PanelInput:
name: str
source: int
shadow: int
previous: int
bit: int
dirty: int
dirty_bit: int
selector: int | None = None
note: str = ""
@property
def spec(self) -> str:
return f"{h16(self.shadow)}.{self.bit}"
@property
def dirty_spec(self) -> str:
return f"{h16(self.dirty)}.{self.dirty_bit}"
@dataclass(frozen=True)
class PanelAction:
panel_input: PanelInput
pressed: bool
raw: str = ""
@property
def label(self) -> str:
state = "press" if self.pressed else "release"
return f"{self.panel_input.name}:{state}"
@dataclass(frozen=True)
class PanelInjection:
action: PanelAction
source_before: int
source_after: int
shadow_before: int
shadow_after: int
previous_before: int
previous_after: int
dirty_before: int
dirty_after: int
def summary(self) -> str:
panel_input = self.action.panel_input
selector = "" if panel_input.selector is None else f" selector=0x{panel_input.selector:04X}"
return (
f"{self.action.label} source={h16(panel_input.source)} "
f"shadow={panel_input.spec} previous={h16(panel_input.previous)} "
f"dirty={panel_input.dirty_spec}{selector}"
)
PANEL_LANES: tuple[tuple[int, int, int, int, int], ...] = (
(0xF102, 0xF6D7, 0xF6E7, 0xF6F2, 7),
(0xF103, 0xF6D6, 0xF6E6, 0xF6F2, 6),
(0xF104, 0xF6D5, 0xF6E5, 0xF6F2, 5),
(0xF105, 0xF6D4, 0xF6E4, 0xF6F2, 4),
(0xF106, 0xF6D3, 0xF6E3, 0xF6F2, 3),
(0xF107, 0xF6D2, 0xF6E2, 0xF6F2, 2),
(0xF108, 0xF6D1, 0xF6E1, 0xF6F2, 1),
(0xF109, 0xF6D0, 0xF6E0, 0xF6F2, 0),
(0xF005, 0xF6DC, 0xF6EC, 0xF6F3, 4),
(0xF006, 0xF6DB, 0xF6EB, 0xF6F3, 3),
)
KNOWN_PANEL_INPUTS: dict[str, PanelInput] = {
"cam-power": PanelInput(
name="cam-power",
source=0xF105,
shadow=0xF6D4,
previous=0xF6E4,
bit=3,
dirty=0xF6F2,
dirty_bit=4,
selector=0x0007,
note="CAM POWER button; queues selector 0x0007 when gates allow",
),
"call": PanelInput(
name="call",
source=0xF006,
shadow=0xF6DB,
previous=0xF6EB,
bit=5,
dirty=0xF6F3,
dirty_bit=3,
selector=0x0015,
note="CALL button; queues selector 0x0015 active/inactive reports",
),
}
PANEL_ALIASES: dict[str, str] = {
"cam": "cam-power",
"camera-power": "cam-power",
"camera_power": "cam-power",
"cam_power": "cam-power",
"campower": "cam-power",
"power": "cam-power",
}
def resolve_panel_input(text: str) -> PanelInput:
token = _normalize_token(text)
token = PANEL_ALIASES.get(token, token)
if token in KNOWN_PANEL_INPUTS:
return KNOWN_PANEL_INPUTS[token]
if "." not in token:
raise ValueError(f"unknown panel input {text!r}; use cam-power, call, or an address bit like F6D4.3")
address_text, bit_text = token.split(".", 1)
address = _parse_address(address_text)
try:
bit = int(bit_text, 0)
except ValueError as exc:
raise ValueError(f"invalid panel bit in {text!r}") from exc
if not 0 <= bit <= 7:
raise ValueError(f"panel bit out of range in {text!r}")
for source, shadow, previous, dirty, dirty_bit in PANEL_LANES:
if address in {source, shadow}:
return PanelInput(
name=f"{h16(shadow)}.{bit}",
source=source,
shadow=shadow,
previous=previous,
bit=bit,
dirty=dirty,
dirty_bit=dirty_bit,
note="raw panel matrix input inferred from ROM shadow/dirty lane",
)
raise ValueError(f"{h16(address)} is not a known A8 panel byte shadow/source")
def parse_panel_action(text: str, *, default_pressed: bool = True) -> PanelAction:
raw = text.strip()
spec = raw
pressed = default_pressed
for separator in ("=", ":"):
if separator not in raw:
continue
left, right = raw.rsplit(separator, 1)
state = _parse_state(right)
if state is None:
continue
spec = left
pressed = state
break
return PanelAction(resolve_panel_input(spec), pressed=pressed, raw=raw)
def _parse_state(text: str) -> bool | None:
token = _normalize_token(text)
if token in {"press", "pressed", "on", "down", "1", "true", "active"}:
return True
if token in {"release", "released", "off", "up", "0", "false", "inactive"}:
return False
return None
def _normalize_token(text: str) -> str:
return text.strip().lower().replace("_", "-")
def _parse_address(text: str) -> int:
token = text.strip().upper()
if token.startswith("H'"):
token = token[2:]
elif token.startswith("$"):
token = token[1:]
elif token.startswith("0X"):
token = token[2:]
try:
value = int(token, 16)
except ValueError as exc:
raise ValueError(f"invalid panel address {text!r}") from exc
if not 0 <= value <= 0xFFFF:
raise ValueError(f"panel address out of range {text!r}")
return value
def set_bit(value: int, bit: int, enabled: bool) -> int:
mask = 1 << bit
return (value | mask) & 0xFF if enabled else (value & ~mask) & 0xFF
__all__ = [
"KNOWN_PANEL_INPUTS",
"PANEL_ALIASES",
"PANEL_LANES",
"PanelAction",
"PanelInjection",
"PanelInput",
"parse_panel_action",
"resolve_panel_input",
"set_bit",
]

View File

@@ -39,6 +39,7 @@ from .cpu import CPUState, mask, s8, s16, sign_bit
from .errors import EmulatorError, UnsupportedInstruction
from .fast_paths import P9FastPath, P9FastPathConfig
from .memory import MemoryMap
from .panel import PanelAction, PanelInjection, PanelInput, resolve_panel_input, set_bit
from .sci import SCI1
from .timers import FrtOciaScheduler, FrtRegisters
from .uart import UartTiming
@@ -134,6 +135,38 @@ class H8536Emulator:
def inject_sci1_rx_byte(self, value: int) -> None:
self.memory.inject_sci1_rx_byte(value)
def inject_panel_input(self, panel_input: PanelInput | str, *, pressed: bool = True) -> PanelInjection:
resolved = resolve_panel_input(panel_input) if isinstance(panel_input, str) else panel_input
action = PanelAction(resolved, pressed=pressed)
shadow_before = self.memory.read8(resolved.shadow)
source_before = self.memory.external.get(resolved.source, shadow_before)
previous_before = self.memory.read8(resolved.previous)
dirty_before = self.memory.read8(resolved.dirty)
source_after = set_bit(source_before, resolved.bit, pressed)
shadow_after = set_bit(shadow_before, resolved.bit, pressed)
previous_after = set_bit(previous_before, resolved.bit, not pressed)
dirty_after = dirty_before | (1 << resolved.dirty_bit)
self.memory.write8(resolved.source, source_after)
self.memory.write8(resolved.shadow, shadow_after)
# The ROM's loc_1Bxx dispatchers act on shadow XOR previous-shadow.
# Force the previous sample opposite at this bit so the main loop sees one clean edge.
self.memory.write8(resolved.previous, previous_after)
self.memory.write8(resolved.dirty, dirty_after)
return PanelInjection(
action=action,
source_before=source_before,
source_after=source_after,
shadow_before=shadow_before,
shadow_after=shadow_after,
previous_before=previous_before,
previous_after=previous_after,
dirty_before=dirty_before,
dirty_after=dirty_after,
)
def step(self) -> str:
pc = self.cpu.pc
cycles_before = self.cpu.cycles

View File

@@ -18,6 +18,7 @@ from .constants import (
from .eeprom_image import write_eeprom_snapshot
from .errors import UnsupportedInstruction
from .memory import MemoryAccess
from .panel import PanelAction, parse_panel_action, resolve_panel_input
from .runner import H8536Emulator
from .uart import UartTiming
@@ -33,6 +34,13 @@ CONNECT_LCD_FRAMES = (
)
WATCH_PCS = {
0x15E0: "main_panel_scanner",
0x1BA0: "panel_f6d4_edge_dispatch",
0x1BF8: "panel_f6db_edge_dispatch",
0x1C0E: "panel_bit_dispatch",
0x1F40: "cam_power_handler",
0x20A1: "call_handler",
0x3E54: "report_queue_enqueue",
0xBB57: "sci1_eri_entry",
0xBB67: "sci1_rxi_entry",
0xBBD6: "rx_checksum_seed",
@@ -56,6 +64,10 @@ WATCH_RANGES = (
)
ACCESS_RANGES = (
(0xF000, 0xF10F, "panel_external_bytes"),
(0xF6D0, 0xF6DF, "panel_shadow_bytes"),
(0xF6E0, 0xF6EF, "panel_previous_shadow_bytes"),
(0xF6F0, 0xF6F3, "panel_dirty_bytes"),
(0xF850, 0xF85D, "tx_staging_or_frame"),
(0xF860, 0xF86D, "rx_validation_or_capture"),
(0xF870, 0xF96F, "report_queue"),
@@ -64,8 +76,12 @@ ACCESS_RANGES = (
(0xFAA2, 0xFAA6, "serial_latches"),
(0xFAF0, 0xFAFF, "lcd_line_buffer"),
(0xE000, 0xE001, "primary_table_index_0000"),
(0xE00E, 0xE00F, "primary_cam_power_index_0007"),
(0xE02A, 0xE02B, "primary_call_index_0015"),
(0xE400, 0xE401, "secondary_table_index_0000"),
(0xE800, 0xE801, "current_table_index_0000"),
(0xE80E, 0xE80F, "current_cam_power_index_0007"),
(0xE82A, 0xE82B, "current_call_index_0015"),
(0xEC00, 0xEC01, "flag_table_index_0000"),
(0xE000, 0xE3FF, "primary_table_E000"),
(0xE400, 0xE7FF, "secondary_table_E400"),
@@ -77,6 +93,12 @@ ACCESS_RANGES = (
)
STATE_BYTES = {
0xF6D4: "panel_shadow_F6D4_cam_lane",
0xF6DB: "panel_shadow_F6DB_call_lane",
0xF6E4: "panel_previous_F6E4_cam_lane",
0xF6EB: "panel_previous_F6EB_call_lane",
0xF6F2: "panel_dirty_F6F2",
0xF6F3: "panel_dirty_F6F3",
0xF9B0: "queue_head",
0xF9B5: "queue_tail",
0xF9C0: "tx_gate",
@@ -104,12 +126,14 @@ STATE_WORDS = {
0xE000: "E000_index_0000_primary",
0xE004: "E000_index_0002_primary",
0xE008: "E000_index_0004_primary",
0xE00E: "E000_index_0007_cam_power_primary",
0xE024: "E000_index_0012_primary",
0xE026: "E000_index_0013_primary",
0xE02A: "E000_index_0015_primary",
0xE104: "E000_index_0082_primary",
0xE400: "E400_index_0000_secondary",
0xE800: "E800_index_0000_current",
0xE80E: "E800_index_0007_cam_power_current",
0xE804: "E800_index_0002_current",
0xE808: "E800_index_0004_current",
0xE824: "E800_index_0012_current",
@@ -189,6 +213,50 @@ class FrameResult:
return lines
@dataclass(frozen=True)
class PanelActionResult:
action: PanelAction
injection_summary: str
steps: int
stopped_reason: str
new_tx_bytes: bytes
new_tx_frames: list[bytes]
state_before: dict[str, int | str]
state_after: dict[str, int | str]
accesses: list[MemoryAccess]
context: RunContext
def lines(self, index: int) -> list[str]:
lines = [
f"panel_action[{index}]={self.action.label} input={self.action.panel_input.spec}",
f" injection={self.injection_summary}",
f" stopped={self.stopped_reason} steps={self.steps}",
f" new_tx_bytes={format_frame(self.new_tx_bytes) if self.new_tx_bytes else 'none'}",
]
if self.new_tx_frames:
lines.append(" new_tx_frames=" + " | ".join(format_frame(frame) for frame in self.new_tx_frames))
else:
lines.append(" new_tx_frames=none")
lcd_display = self.state_after.get("lcd_display_ascii")
if isinstance(lcd_display, str):
lines.append(f" lcd_display={lcd_display!r}")
state_changes = _state_change_lines(self.state_before, self.state_after)
if state_changes:
lines.append(" state_changes:")
lines.extend(f" {line}" for line in state_changes)
pc_lines = _pc_hit_lines(self.context)
if pc_lines:
lines.append(" pc_hits:")
lines.extend(f" {line}" for line in pc_lines)
access_lines = _access_lines(self.accesses)
if access_lines:
lines.append(" interesting_accesses:")
lines.extend(f" {line}" for line in access_lines)
if self.context.unsupported:
lines.append(f" unsupported={self.context.unsupported}")
return lines
def parse_frame(text: str) -> bytes:
normalized = text.strip().replace(",", " ").replace(":", " ").replace("-", " ").replace("_", " ")
parts = normalized.split()
@@ -209,6 +277,27 @@ def parse_frame(text: str) -> bytes:
return bytes(values)
def parse_panel_action_arg(text: str) -> PanelAction:
try:
return parse_panel_action(text)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def parse_panel_press_arg(text: str) -> PanelAction:
try:
return PanelAction(resolve_panel_input(text), pressed=True, raw=text)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def parse_panel_release_arg(text: str) -> PanelAction:
try:
return PanelAction(resolve_panel_input(text), pressed=False, raw=text)
except ValueError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
def frame_checksum(data: bytes) -> int:
checksum = CHECKSUM_SEED
for value in data[: FRAME_LENGTH - 1]:
@@ -318,6 +407,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--per-byte-steps", type=int, default=5_000, help="polite mode byte-consume limit, or UART mode step limit between byte arrivals")
parser.add_argument("--post-frame-steps", type=int, default=80_000, help="maximum steps after a full injected frame")
parser.add_argument("--post-frame-ms", type=int, help="run this many emulated milliseconds after each injected frame")
parser.add_argument("--panel", action="append", type=parse_panel_action_arg, default=[], help="synthetic panel action, e.g. cam-power, call=release, or F6D4.3=press; preserves order across repeated --panel uses")
parser.add_argument("--panel-press", action="append", type=parse_panel_press_arg, default=[], help="synthetic panel press alias/spec, e.g. cam-power, call, or F6D4.3")
parser.add_argument("--panel-release", action="append", type=parse_panel_release_arg, default=[], help="synthetic panel release alias/spec, e.g. call or F6DB.5")
parser.add_argument("--post-panel-steps", type=int, default=120_000, help="maximum steps after each synthetic panel action")
parser.add_argument("--post-panel-ms", type=int, help="run this many emulated milliseconds after each synthetic panel action")
parser.add_argument("--wait-heartbeats", type=int, default=0, help="wait for this many heartbeat frames before injecting the first host frame")
parser.add_argument("--wait-heartbeat-steps", type=int, default=1_500_000, help="maximum steps while waiting for pre-injection heartbeat frames")
parser.add_argument("--uart-timing", action="store_true", help="inject frame bytes at real UART inter-byte timing instead of waiting for RDRF consumption")
@@ -347,8 +441,9 @@ def main(argv: list[str] | None = None) -> int:
frames = list(args.frames)
if args.preset == "connect-lcd":
frames.extend(CONNECT_LCD_FRAMES)
if not frames:
raise SystemExit("pass at least one frame or use --preset connect-lcd")
panel_actions = [*args.panel, *args.panel_press, *args.panel_release]
if not frames and not panel_actions:
raise SystemExit("pass at least one frame, use --preset connect-lcd, or add --panel/--panel-press")
rom_path, emulator, boot_summary, results = run_rx_probe(
frames,
@@ -384,6 +479,19 @@ def main(argv: list[str] | None = None) -> int:
for index, result in enumerate(results):
for line in result.lines(index):
print(line)
panel_results = [
_run_panel_action(
emulator,
action,
post_panel_steps=args.post_panel_steps,
post_panel_ms=args.post_panel_ms,
stop_after_tx_frame=not args.keep_listening,
)
for action in panel_actions
]
for index, result in enumerate(panel_results):
for line in result.lines(index):
print(line)
print("total_tx_frames=" + " | ".join(format_frame(frame) for frame in emulator.sci1.tx_frames))
eeprom_writes = emulator.memory.p9_bus.x24164_bus.write_log_lines(limit=80)
if eeprom_writes:
@@ -484,6 +592,49 @@ def _run_frame(
)
def _run_panel_action(
emulator: H8536Emulator,
action: PanelAction,
*,
post_panel_steps: int,
post_panel_ms: int | None,
stop_after_tx_frame: bool,
) -> PanelActionResult:
state_before = _state_snapshot(emulator)
log_start = len(emulator.memory.access_log)
tx_byte_start = len(emulator.sci1.tx_bytes)
tx_frame_start = len(emulator.sci1.tx_frames)
injection = emulator.inject_panel_input(action.panel_input, pressed=action.pressed)
context = RunContext()
def post_predicate(inner: H8536Emulator) -> bool:
if not stop_after_tx_frame:
return False
return any(frame != HEARTBEAT_FRAME for frame in inner.sci1.tx_frames[tx_frame_start:])
if post_panel_ms is not None:
steps, reason = _run_cycles_for_ms(emulator, post_panel_ms, context)
stopped_reason = reason
else:
steps, reason = _run_until(emulator, post_panel_steps, post_predicate, context)
stopped_reason = "non_heartbeat_tx_frame" if reason == "predicate" and stop_after_tx_frame else reason
log_end = len(emulator.memory.access_log)
state_after = _state_snapshot(emulator)
return PanelActionResult(
action=action,
injection_summary=injection.summary(),
steps=steps,
stopped_reason=stopped_reason,
new_tx_bytes=bytes(emulator.sci1.tx_bytes[tx_byte_start:]),
new_tx_frames=list(emulator.sci1.tx_frames[tx_frame_start:]),
state_before=state_before,
state_after=state_after,
accesses=emulator.memory.access_log[log_start:log_end],
context=context,
)
def _inject_frame_uart_timed(
emulator: H8536Emulator,
frame: bytes,
@@ -672,11 +823,15 @@ def _parse_byte(text: str) -> int:
__all__ = [
"CONNECT_LCD_FRAMES",
"PanelActionResult",
"format_frame",
"frame_checksum",
"frame_checksum_ok",
"main",
"parse_frame",
"parse_panel_action_arg",
"parse_panel_press_arg",
"parse_panel_release_arg",
"run_rx_probe",
"UartTiming",
]