1
0
Files
h8-536-decoder/h8536/panel_button_trace.py
2026-05-27 11:50:10 +10:00

307 lines
10 KiB
Python

from __future__ import annotations
import argparse
import json
import re
from collections import defaultdict
from pathlib import Path
from typing import Any
from .decoder import H8536Decoder
from .formatting import h16
from .rom import Rom
DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN")
DEFAULT_TEXT_OUTPUT = Path("build/panel_button_trace.md")
DEFAULT_JSON_OUTPUT = Path("build/panel_button_trace.json")
BUTTON_TABLE_BASE = 0x2706
NOOP_HANDLER = 0x1C25
QUEUE_FUNCTION = 0x3E54
INPUT_BYTES = [
{
"shadow": "F6D7",
"source": "F102",
"dirty": "F6F2.7",
"dispatcher": "1B2D",
"initial_r5": 0x7E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D6",
"source": "F103",
"dirty": "F6F2.6",
"dispatcher": "1B44",
"initial_r5": 0x6E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D5",
"source": "F104",
"dirty": "F6F2.5",
"dispatcher": "1B5B",
"initial_r5": 0x5E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D4",
"source": "F105",
"dirty": "F6F2.4",
"dispatcher": "1BA0",
"initial_r5": 0x4E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D3",
"source": "F106",
"dirty": "F6F2.3",
"dispatcher": "1BB6",
"initial_r5": 0x3E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D2",
"source": "F107",
"dirty": "F6F2.2",
"dispatcher": "1BCC",
"initial_r5": 0x2E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D1",
"source": "F108",
"dirty": "F6F2.1",
"dispatcher": "1B72",
"initial_r5": 0x1E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6D0",
"source": "F109",
"dirty": "F6F2.0",
"dispatcher": "1B89",
"initial_r5": 0x0E,
"bank": "IRQ4 A8 panel byte path",
},
{
"shadow": "F6DC",
"source": "F005",
"dirty": "F6F3.4",
"dispatcher": "1BE2",
"initial_r5": 0xCE,
"bank": "IRQ3 A8 panel byte path",
},
{
"shadow": "F6DB",
"source": "F006",
"dirty": "F6F3.3",
"dispatcher": "1BF8",
"initial_r5": 0xBE,
"bank": "IRQ3 A8 panel byte path",
},
]
KNOWN_REPORTS = {
0x0007: "CAM POWER",
0x0015: "CALL",
}
def analyze_panel_buttons(rom_bytes: bytes) -> dict[str, Any]:
rom = Rom(rom_bytes)
entries = _table_entries(rom)
handler_summaries = {
target: _summarize_handler(rom, target)
for target in sorted({entry["handler"] for entry in entries if entry["handler"] != NOOP_HANDLER})
}
for entry in entries:
entry["handler_summary"] = handler_summaries.get(entry["handler"], {})
selectors = entry["handler_summary"].get("report_selectors", [])
entry["known_report"] = ", ".join(
KNOWN_REPORTS.get(int(selector), f"0x{int(selector):04X}") for selector in selectors
)
return {
"kind": "panel_button_trace",
"button_table_base": BUTTON_TABLE_BASE,
"button_table_base_hex": h16(BUTTON_TABLE_BASE),
"noop_handler": NOOP_HANDLER,
"noop_handler_hex": h16(NOOP_HANDLER),
"entries": entries,
"known_paths": _known_paths(entries),
"handler_summaries": [
{"handler": handler, "handler_hex": h16(handler), **summary}
for handler, summary in sorted(handler_summaries.items())
],
}
def format_markdown(analysis: dict[str, Any]) -> str:
lines = [
"# PT2 Known Button ROM Trace",
"",
"This report follows the panel button edge path from the serial-visible reports back into the ROM input scanner.",
"The key table is the indirect handler table at `H'2706`, used by `loc_1C0E` after byte-level panel input changes are detected.",
"",
"## Known Anchors",
"",
]
for path in analysis["known_paths"]:
lines.extend(
[
f"### {path['name']}",
"",
f"- Emitted selector: `0x{path['selector']:04X}`",
f"- Handler: `H'{path['handler']:04X}`",
f"- Edge source: `{path['source']} -> {path['shadow']}` via `{path['dirty']}`",
f"- Trigger bit: `{path['shadow']}.{path['bit']}`",
f"- Table slot: `H'{path['table_address']:04X}` -> `H'{path['handler']:04X}`",
f"- Current-level tests: {', '.join(path['handler_summary'].get('level_tests', [])) or 'none found'}",
f"- State writes: {', '.join(path['handler_summary'].get('state_writes', [])) or 'none found'}",
"",
]
)
lines.extend(
[
"## Button Matrix Entries With Serial Reports",
"",
"| Source | Shadow bit | Dirty | Handler | Selector(s) | State writes |",
"| --- | --- | --- | --- | --- | --- |",
]
)
for entry in analysis["entries"]:
selectors = entry["handler_summary"].get("report_selectors", [])
if not selectors:
continue
selector_text = ", ".join(f"`0x{int(selector):04X}`" for selector in selectors)
writes = ", ".join(f"`{write}`" for write in entry["handler_summary"].get("state_writes", [])[:4])
lines.append(
f"| `{entry['source']}` | `{entry['shadow']}.{entry['bit']}` | `{entry['dirty']}` | "
f"`H'{entry['handler']:04X}` | {selector_text} | {writes} |"
)
lines.extend(
[
"",
"## Practical Read",
"",
"- CALL and CAM POWER do share the general panel edge path with many other buttons.",
"- The shared path is: panel byte snapshot -> shadow byte -> dirty bit -> `loc_1C0E` jump table -> handler -> `loc_3E54` report.",
"- Other buttons diverge in their handlers: many require `F731/F730/F791` session/menu gates, mutate page state, or emit different selectors.",
"- Some table entries are `H'1C25`, an immediate `RTS`, so those physical matrix positions are intentionally ignored in this firmware context.",
"",
]
)
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Trace panel button matrix handlers and known CALL/CAM report sources.")
parser.add_argument("rom", nargs="?", type=Path, default=DEFAULT_ROM)
parser.add_argument("--out", type=Path, default=DEFAULT_TEXT_OUTPUT)
parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON_OUTPUT)
args = parser.parse_args(argv)
analysis = analyze_panel_buttons(args.rom.read_bytes())
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(format_markdown(analysis), encoding="utf-8")
args.json_out.parent.mkdir(parents=True, exist_ok=True)
args.json_out.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wrote {args.out}")
print(f"wrote {args.json_out}")
return 0
def _table_entries(rom: Rom) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for lane in INPUT_BYTES:
initial_r5 = int(lane["initial_r5"])
for bit in range(7, -1, -1):
table_offset = initial_r5 - (2 * (7 - bit))
table_address = BUTTON_TABLE_BASE + table_offset
handler = rom.u16(table_address)
rows.append(
{
"source": lane["source"],
"shadow": lane["shadow"],
"dirty": lane["dirty"],
"dispatcher": lane["dispatcher"],
"bank": lane["bank"],
"bit": bit,
"table_offset": table_offset,
"table_address": table_address,
"table_address_hex": h16(table_address),
"handler": handler,
"handler_hex": h16(handler),
"is_noop": handler == NOOP_HANDLER,
}
)
return rows
def _known_paths(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
paths: list[dict[str, Any]] = []
for entry in entries:
selectors = entry["handler_summary"].get("report_selectors", [])
for selector in selectors:
name = KNOWN_REPORTS.get(int(selector))
if not name:
continue
paths.append({"name": name, "selector": int(selector), **entry})
return paths
def _summarize_handler(rom: Rom, address: int) -> dict[str, Any]:
instructions = _decode_linear_handler(rom, address)
report_selectors: list[int] = []
last_r3: int | None = None
level_tests: list[str] = []
state_writes: list[str] = []
for ins in instructions:
text = ins.text
if match := re.search(r"MOV:[^#]+#H'([0-9A-F]{4}), R3", text):
last_r3 = int(match.group(1), 16)
if QUEUE_FUNCTION in ins.targets and last_r3 is not None:
if last_r3 not in report_selectors:
report_selectors.append(last_r3)
if match := re.search(r"BTST\.B #([0-7]), @H'(F6[0-9A-F]{2})", text):
test = f"{match.group(2)}.{match.group(1)}"
if test not in level_tests:
level_tests.append(test)
if match := re.search(r"@H'(E[89][0-9A-F]{2})", text):
write = match.group(1)
if _looks_like_write(text) and write not in state_writes:
state_writes.append(write)
return {
"report_selectors": report_selectors,
"report_selectors_hex": [f"0x{selector:04X}" for selector in report_selectors],
"level_tests": level_tests,
"state_writes": state_writes,
"instruction_count_scanned": len(instructions),
}
def _decode_linear_handler(rom: Rom, address: int, *, max_bytes: int = 0x90) -> list[Any]:
decoder = H8536Decoder(rom, labels={QUEUE_FUNCTION: "loc_3E54"})
instructions: list[Any] = []
pc = address
end = min(len(rom.data), address + max_bytes)
while pc < end:
ins = decoder.decode(pc)
instructions.append(ins)
pc += max(1, ins.size)
if ins.mnemonic == "RTS":
break
return instructions
def _looks_like_write(text: str) -> bool:
return text.startswith("MOV") or text.startswith("BSET") or text.startswith("BCLR") or text.startswith("CLR")
__all__ = ["analyze_panel_buttons", "format_markdown", "main"]
if __name__ == "__main__":
raise SystemExit(main())