More ccu based mining
This commit is contained in:
@@ -33,6 +33,7 @@ To run the newer sidecar protocol and gate/queue analysis tools:
|
||||
.\.venv\Scripts\python.exe h8536_rx_branch_trace.py build\rom_decompiled.json --out build\rom_rx_branch_trace.txt
|
||||
.\.venv\Scripts\python.exe h8536_report_source_trace.py build\rom_decompiled.json --out build\rom_report_sources.txt
|
||||
.\.venv\Scripts\python.exe h8536_table_xrefs.py --out build\rom_table_xrefs.txt
|
||||
.\.venv\Scripts\python.exe h8536_ccu_seed_hints.py build\rom_decompiled.json --out build\rom_ccu_seed_hints.txt
|
||||
.\.venv\Scripts\python.exe h8536_consistency.py build\rom_decompiled.json --out build\rom_consistency.txt
|
||||
.\.venv\Scripts\python.exe h8536_protocol_capture.py ROM\rcp-txd-idle-only.txt
|
||||
```
|
||||
@@ -81,6 +82,7 @@ The real-device bench helper uses `pyserial`; install repo dependencies with `.\
|
||||
- Emits a focused SCI1 RX branch trace covering RXI/ERI byte capture, six-byte checksum validation, selector decode, the `FAA2 == 0` initial dispatcher, the `FAA2 != 0` continuation dispatcher, command `0x00/0x01/0x02/0x04/0x05/0x06/0x07` handlers, table surfaces, retry/error echoes, the separate `BE70/F970` selector-processing and `3E54/F870` serial-report queues, the TXI/RXI continuation-collapse interlock, RX-to-TX feedback loops, and session-timeout side effects.
|
||||
- Traces direct callers to `loc_3E54` to identify report queue sources and conservatively flags whether observed report indexes such as `0x0007` are ROM-proven constants or runtime/capture observations.
|
||||
- Generates table/index cross-reference reports for candidate value/current/secondary/flag tables and LCD text correlations.
|
||||
- Mines ROM-backed CCU seed hints from table xrefs, selector dispatch, LCD text terms, and observed report overlays, then proposes syntactically valid command-0 seed frames and command-1 readback frames for high-value selectors.
|
||||
- Adds a Sony RCP-TX7 board profile that ties H8/536 pin 66 `P95/TXD` and pin 67 `P96/RXD` to the MAX202 RS232 transceiver.
|
||||
- Flags/manual-annotates TEMP-register access ordering for FRT and A/D 16-bit peripheral registers.
|
||||
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
|
||||
@@ -121,6 +123,7 @@ Current serial observations:
|
||||
- RX probe finding: the `--preset connect-lcd` sequence is sensitive to injection timing and modeled external state. With timed UART injection, the emulator can still reach `CONNECT: OK`/`02 00 02 00 00 5A`, while the real bench remains at `CONNECT NOT ACT`; this points to missing session/P9/external-panel context rather than a simple checksum or UART-spacing issue.
|
||||
- Emulator state-search finding: the minimum ROM-visible OK display condition is now reproducible without serial. Direct entry at `loc_2CB9` with `E000[0]=0x8080` and unsuppressed `F730=0` reaches `CONNECT: OK`; the queued selector-zero path also reaches OK when `F970[0]=0`, `F9B9=0`, `F9B4=1`, `E000[0]=0x8080`, and `F730=0`. This makes the bench problem sharper: prove whether serial can retain `E000[0]=0x8080` and enqueue selector zero without the reset/clobber path clearing it first.
|
||||
- Bench follow-up: replaying the emulator CONNECT sequence on the real device did not switch the LCD to OK. The real device answered the `04 00 00 80 00 DE` step with `07 80 C0 60 20 5D` in the captured run and remained at `CONNECT NOT ACT`, so the next mismatch to chase is the missing visible `07 80 C0 60 20 5D` response/session context rather than the LCD OK branch.
|
||||
- CCU seed-hint finding: `build\rom_ccu_seed_hints.txt` currently ranks selector `0x000`, `0x0F6`, `0x003`, and `0x040` as the highest-value fake-CCU stream candidates. The generated seed frames are `00 00 00 80 80 5A`, `00 01 76 20 00 0D`, `00 00 03 80 00 D9`, and `00 00 40 FF FF 1A`, with command-1 readbacks listed beside them.
|
||||
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
|
||||
|
||||
The generated listing is written to:
|
||||
@@ -143,6 +146,7 @@ build/rom_serial_pseudocode.c
|
||||
build/rom_serial_gate.txt
|
||||
build/rom_report_sources.txt
|
||||
build/rom_table_xrefs.txt
|
||||
build/rom_ccu_seed_hints.txt
|
||||
build/rom_consistency.txt
|
||||
build/callgraph.dot
|
||||
```
|
||||
@@ -207,6 +211,7 @@ python h8536_serial_gate.py --help
|
||||
python h8536_rx_branch_trace.py --help
|
||||
python h8536_report_source_trace.py --help
|
||||
python h8536_table_xrefs.py --help
|
||||
python h8536_ccu_seed_hints.py --help
|
||||
python h8536_consistency.py --help
|
||||
```
|
||||
|
||||
@@ -214,6 +219,7 @@ python h8536_consistency.py --help
|
||||
- `h8536_rx_branch_trace.py`: reports the SCI1 RX branch tree. Current finding: command `0x04/0x05/0x06` are continuation-path commands behind `FAA2 != 0`, so a standalone command-4 force from idle should not reach `BD0E`.
|
||||
- `h8536_report_source_trace.py`: traces direct `loc_3E54` report enqueue sources. Current finding: no direct static `R3 = 0x0007` enqueue in the JSON, so CAM power `0x0007` remains runtime/capture-observed unless a later indirect/table path proves it.
|
||||
- `h8536_table_xrefs.py`: emits candidate table/index xrefs and LCD text correlation hints.
|
||||
- `h8536_ccu_seed_hints.py`: mines table, dispatch, LCD, and observed-report hints for the CCU-side state stream the RCP may expect before active displays/reports.
|
||||
- `h8536_consistency.py`: flags JSON-to-pseudocode semantic hazards such as byte immediates written to word destinations.
|
||||
|
||||
For the emulator harness:
|
||||
@@ -282,6 +288,7 @@ python h8536_emulator_rx_probe.py --help
|
||||
- `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction.
|
||||
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
|
||||
- `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation.
|
||||
- `h8536/ccu_seed_hints.py`: ROM miner for likely fake-CCU state seed selectors and candidate command/readback frames.
|
||||
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
|
||||
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, 38400 8N1 UART injection timing, P9/X24164 EEPROM bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding.
|
||||
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
|
||||
@@ -294,7 +301,7 @@ python h8536_emulator_rx_probe.py --help
|
||||
- `h8536_pseudocode.py`: pseudocode CLI wrapper.
|
||||
- `h8536_serial_pseudocode.py`: focused serial pseudocode CLI wrapper.
|
||||
- `h8536_protocol_trace.py`, `h8536_protocol_capture.py`: protocol analysis CLI wrappers.
|
||||
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
|
||||
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
|
||||
- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_bench_replay.py`: emulator CLI wrappers.
|
||||
- `h8536_emulator_state_search.py`: emulator CONNECT state-search CLI wrapper.
|
||||
- `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence.
|
||||
|
||||
1981
build/rom_ccu_seed_hints.json
Normal file
1981
build/rom_ccu_seed_hints.json
Normal file
File diff suppressed because it is too large
Load Diff
168
build/rom_ccu_seed_hints.txt
Normal file
168
build/rom_ccu_seed_hints.txt
Normal file
@@ -0,0 +1,168 @@
|
||||
H8/536 CCU Seed Hint Report
|
||||
|
||||
Summary: The RCP likely waits for the CCU to seed mirrored state tables, then uses those selector values to update LCD text, panel lamps, and report state changes.
|
||||
Confidence: medium
|
||||
|
||||
Table Model:
|
||||
- primary_value_table_candidate: H'E000-H'E3FF; accesses=31 static selectors=0x000, 0x002, 0x003, 0x023, 0x040, 0x081, 0x092, 0x093, 0x0A7, 0x0B7, 0x0B9, 0x0F6
|
||||
- secondary_value_table_candidate: H'E400-H'E7FF; accesses=8 static selectors=none
|
||||
- current_value_table_candidate: H'E800-H'EBFF; accesses=14 static selectors=0x000, 0x003, 0x040, 0x081, 0x092, 0x0F6
|
||||
- flag_table_candidate: H'EC00-H'EFFF; accesses=6 static selectors=0x000
|
||||
|
||||
Highest-Value Selector Candidates:
|
||||
- 0x000 heartbeat_or_idle_report_candidate: score=18 tables=primary_value_table_candidate, current_value_table_candidate, flag_table_candidate
|
||||
- primary_value_table_candidate write in loc_4096: MOV:G.W #H'0080, @H'E000
|
||||
- current_value_table_candidate write in loc_4096: MOV:G.W #H'0080, @H'E800
|
||||
- flag_table_candidate write in loc_4075: CLR.W @(-H'1400,R0)
|
||||
- idle report selector and CONNECT OK emulator condition both center on selector zero
|
||||
seed frames: 0x0080 -> 00 00 00 00 80 DA; 0x8080 -> 00 00 00 80 80 5A
|
||||
readback frame: 01 00 00 00 00 5B
|
||||
- 0x093 state_selector_candidate: score=15 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_17C9: BTST.W #12, @H'E126
|
||||
- primary_value_table_candidate read in loc_17FB: BTST.W #12, @H'E126
|
||||
- primary_value_table_candidate read in loc_182D: BTST.W #5, @H'E126
|
||||
- primary_value_table_candidate read in loc_1891: BTST.W #5, @H'E126
|
||||
readback frame: 01 01 13 00 00 49
|
||||
- 0x0F6 active_status_bridge_candidate: score=14 tables=primary_value_table_candidate, current_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_48FA: BTST.W #13, @H'E1EC
|
||||
- primary_value_table_candidate read in loc_48FA: MOV:G.W @H'E1EC, R0
|
||||
- current_value_table_candidate write in loc_48FA: MOV:G.W R0, @H'E9EC
|
||||
- loc_48FA tests E1EC bit13 and can enqueue report selector 0x00F6
|
||||
seed frames: 0x2000 -> 00 01 76 20 00 0D
|
||||
readback frame: 01 01 76 00 00 2C
|
||||
- 0x003 default_enabled_bit_candidate: score=11 tables=primary_value_table_candidate, current_value_table_candidate
|
||||
- primary_value_table_candidate write in loc_4096: MOV:G.W #H'8000, @H'E006
|
||||
- current_value_table_candidate write in loc_4096: MOV:G.W #H'8000, @H'E806
|
||||
- ROM default table writes E000/E800 selector 0x003 to 0x8000
|
||||
seed frames: 0x8000 -> 00 00 03 80 00 D9
|
||||
readback frame: 01 00 03 00 00 58
|
||||
- 0x040 default_all_ones_or_status_block_candidate: score=11 tables=primary_value_table_candidate, current_value_table_candidate
|
||||
- primary_value_table_candidate write in loc_4096: MOV:G.W #H'FFFF, @H'E080
|
||||
- current_value_table_candidate write in loc_4096: MOV:G.W #H'FFFF, @H'E880
|
||||
- ROM default table writes E000/E800 selector 0x040 to 0xFFFF and bench tests repeatedly touched the 0x40 family
|
||||
seed frames: 0xFFFF -> 00 00 40 FF FF 1A; 0x4030 -> 00 00 40 40 30 6A
|
||||
readback frame: 01 00 40 00 00 1B
|
||||
- 0x081 state_selector_candidate: score=9 tables=primary_value_table_candidate, current_value_table_candidate
|
||||
- primary_value_table_candidate read in vec_ad_adi_3D99: MOV:G.W @H'E102, R0
|
||||
- primary_value_table_candidate read in vec_ad_adi_3D99: CMP:G.W @H'E102, R1
|
||||
- current_value_table_candidate write in loc_15E0: MOV:G.W R1, @H'E902
|
||||
readback frame: 01 01 01 00 00 5B
|
||||
- 0x092 state_selector_candidate: score=9 tables=primary_value_table_candidate, current_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_2650: MOV:G.W @H'E124, R0
|
||||
- primary_value_table_candidate read in loc_2650: CMP:G.W @H'E124, R0
|
||||
- current_value_table_candidate write in loc_2650: MOV:G.W R0, @H'E924
|
||||
readback frame: 01 01 12 00 00 48
|
||||
- 0x06B connection_latch_clear_candidate: score=7 tables=none
|
||||
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
|
||||
- selector dispatches to H'2F72
|
||||
readback frame: 01 00 6B 00 00 30
|
||||
- 0x06C command5_be70_candidate: score=7 tables=none
|
||||
- continuation command 5 calls BE70 for selector 0x006C
|
||||
- selector dispatches to H'2FAF
|
||||
readback frame: 01 00 6C 00 00 37
|
||||
- 0x06D command5_be70_candidate: score=7 tables=none
|
||||
- continuation command 5 calls BE70 for selector 0x006D
|
||||
- selector dispatches to H'3015
|
||||
readback frame: 01 00 6D 00 00 36
|
||||
- 0x007 camera_power_report_candidate: score=5 tables=none
|
||||
- observed RCP autonomous report frame(s): 00 00 07 80 00 DD
|
||||
- selector dispatches to H'2DC3
|
||||
readback frame: 01 00 07 00 00 5C
|
||||
- 0x015 call_button_report_candidate: score=5 tables=none
|
||||
- observed RCP autonomous report frame(s): 00 00 15 80 00 CF, 00 00 15 00 00 4F
|
||||
- selector dispatches to H'2E39
|
||||
readback frame: 01 00 15 00 00 4E
|
||||
- 0x023 state_selector_candidate: score=5 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate write in loc_400C: CLR.W @H'E046
|
||||
- selector dispatches to H'2EE6
|
||||
readback frame: 01 00 23 00 00 78
|
||||
- 0x06E command5_be70_candidate: score=5 tables=none
|
||||
- continuation command 5 calls BE70 for selector 0x006E
|
||||
readback frame: 01 00 6E 00 00 35
|
||||
- 0x096 connection_latch_clear_candidate: score=5 tables=none
|
||||
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
|
||||
readback frame: 01 01 16 00 00 4C
|
||||
- 0x097 connection_latch_clear_candidate: score=5 tables=none
|
||||
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
|
||||
readback frame: 01 01 17 00 00 4D
|
||||
- 0x0C6 connection_latch_clear_candidate: score=5 tables=none
|
||||
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
|
||||
readback frame: 01 01 46 00 00 1C
|
||||
- 0x0F8 connection_latch_clear_candidate: score=5 tables=none
|
||||
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
|
||||
readback frame: 01 01 78 00 00 22
|
||||
- 0x002 state_selector_candidate: score=3 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_2650: BTST.W #13, @H'E004
|
||||
readback frame: 01 00 02 00 00 59
|
||||
- 0x0A7 state_selector_candidate: score=3 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_1705: BTST.W #15, @H'E14E
|
||||
readback frame: 01 01 27 00 00 7D
|
||||
- 0x0B7 state_selector_candidate: score=3 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_174D: BTST.W #13, @H'E16E
|
||||
readback frame: 01 01 37 00 00 6D
|
||||
- 0x0B9 state_selector_candidate: score=3 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_1795: BTST.W #13, @H'E172
|
||||
readback frame: 01 01 39 00 00 63
|
||||
- 0x110 state_selector_candidate: score=3 tables=primary_value_table_candidate
|
||||
- primary_value_table_candidate read in loc_1795: BTST.W #15, @H'E220
|
||||
readback frame: 01 01 90 00 00 CA
|
||||
- 0x012 state_selector_candidate: score=2 tables=none
|
||||
- selector dispatches to H'2E03
|
||||
readback frame: 01 00 12 00 00 49
|
||||
|
||||
Display Text Hints:
|
||||
- CONNECT: 0 hit(s)
|
||||
- COMM LINK: 4 hit(s) - H'77F4 'literal COMM LINK', H'78F4 'literal COMM LINK', H'77F4 'COMM LINK ITEM-1Xw'
|
||||
- COMPLETED: 2 hit(s) - H'A027 'literal COMPLETED', H'A025 'COMPLETED'
|
||||
- CAM: 6 hit(s) - H'7149 'literal CAM', H'71FC 'literal CAM', H'72C7 'literal CAM'
|
||||
- BARS: 12 hit(s) - H'72D1 'literal BARS', H'757D 'literal BARS', H'9C61 'literal BARS'
|
||||
- BLACK: 22 hit(s) - H'65CC 'literal BLACK', H'6647 'literal BLACK', H'6709 'literal BLACK'
|
||||
- IRIS: 6 hit(s) - H'6461 'literal IRIS', H'6A92 'literal IRIS', H'A5CA 'literal IRIS'
|
||||
- GAIN: 10 hit(s) - H'6825 'literal GAIN', H'7813 'literal GAIN', H'98A1 'literal GAIN'
|
||||
- SHUTTER: 4 hit(s) - H'6FB2 'literal SHUTTER', H'781A 'literal SHUTTER', H'6FAE 'SHUTTER Xo'
|
||||
- CALL: 8 hit(s) - H'B53E 'literal CALL', H'B563 'literal CALL', H'B62F 'literal CALL'
|
||||
- POWER: 0 hit(s)
|
||||
- AUTO: 34 hit(s) - H'693E 'literal AUTO', H'6A52 'literal AUTO', H'6B40 'literal AUTO'
|
||||
- DIAG: 6 hit(s) - H'6BF5 'literal DIAG', H'6C19 'literal DIAG', H'6E46 'literal DIAG'
|
||||
- DXC: 0 hit(s)
|
||||
|
||||
Selector Dispatch Hints:
|
||||
- table H'28A6: 25 non-default/interesting entries
|
||||
- selector 0x000 -> H'2CB9 (dispatch index 0x000)
|
||||
- selector 0x007 -> H'2DC3 (dispatch index 0x007)
|
||||
- selector 0x012 -> H'2E03 (dispatch index 0x012)
|
||||
- selector 0x013 -> H'2E06 (dispatch index 0x013)
|
||||
- selector 0x015 -> H'2E39 (dispatch index 0x015)
|
||||
- selector 0x016 -> H'2E5A (dispatch index 0x016)
|
||||
- selector 0x017 -> H'2E85 (dispatch index 0x017)
|
||||
- selector 0x018 -> H'2E6F (dispatch index 0x018)
|
||||
- selector 0x01A -> H'2EC4 (dispatch index 0x01A)
|
||||
- selector 0x023 -> H'2EE6 (dispatch index 0x023)
|
||||
- selector 0x024 -> H'2F0C (dispatch index 0x024)
|
||||
- selector 0x025 -> H'2F1C (dispatch index 0x025)
|
||||
- selector 0x043 -> H'2F4A (dispatch index 0x043)
|
||||
- selector 0x04A -> H'2F5C (dispatch index 0x04A)
|
||||
- selector 0x04E -> H'2F5C (dispatch index 0x04E)
|
||||
- selector 0x052 -> H'2F5C (dispatch index 0x052)
|
||||
|
||||
Candidate Fake-CCU Seed Plan:
|
||||
- cmd0 seed selector 0x000 = 0x8080: 00 00 00 80 80 5A
|
||||
selector zero active/connect candidate from emulator state search
|
||||
- cmd0 seed selector 0x003 = 0x8000: 00 00 03 80 00 D9
|
||||
ROM default state also sets selector 0x003 high bit
|
||||
- cmd0 seed selector 0x040 = 0xFFFF: 00 00 40 FF FF 1A
|
||||
ROM default all-ones/status candidate touched by bench 0x40 family
|
||||
- cmd0 seed selector 0x0F6 = 0x2000: 00 01 76 20 00 0D
|
||||
sets E1EC bit13 candidate used by loc_48FA report bridge
|
||||
|
||||
Bench Implications:
|
||||
- Do not wait for non-heartbeat reports as the only activation source; the CCU may be expected to push initial table state first.
|
||||
- Use command 0 writes for initial seeding, then command 1 readbacks for verification. Treat command 4/5/6 as continuation-only until a live report proves otherwise.
|
||||
- Selector zero remains the highest-value activation candidate because the emulator reaches CONNECT OK when E000[0]=0x8080 and the selector-zero processing queue runs.
|
||||
- E1EC/selector 0x00F6 is a strong follow-up candidate because loc_48FA tests bit13 there and can enqueue report 0x00F6.
|
||||
- LCD text terms such as CAM/BARS/BLACK/COMM LINK appear in ROM records, but they are not direct serial payload strings; they point to selector-driven display builders.
|
||||
|
||||
Caveats:
|
||||
- Selector names are candidates, not confirmed protocol labels.
|
||||
- Static table xrefs prove that firmware reads/writes a selector; they do not prove the external CCU must seed it on boot.
|
||||
- Generated frames are syntactically valid six-byte host frames; bench safety still depends on timing and current RCP state.
|
||||
597
h8536/ccu_seed_hints.py
Normal file
597
h8536/ccu_seed_hints.py
Normal file
@@ -0,0 +1,597 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .formatting import h16
|
||||
from .lcd_text import analyze_lcd_text
|
||||
from .rom import Rom
|
||||
from .serial_semantics import OBSERVED_TX_REPORT_OVERLAY
|
||||
from .table_xrefs import analyze_table_xrefs
|
||||
|
||||
|
||||
JsonObject = dict[str, Any]
|
||||
|
||||
DEFAULT_INPUT = Path("build/rom_decompiled.json")
|
||||
DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN")
|
||||
CHECKSUM_SEED = 0x5A
|
||||
DISPLAY_TERMS = (
|
||||
"CONNECT",
|
||||
"COMM LINK",
|
||||
"COMPLETED",
|
||||
"CAM",
|
||||
"BARS",
|
||||
"BLACK",
|
||||
"IRIS",
|
||||
"GAIN",
|
||||
"SHUTTER",
|
||||
"CALL",
|
||||
"POWER",
|
||||
"AUTO",
|
||||
"DIAG",
|
||||
"DXC",
|
||||
)
|
||||
|
||||
SPECIAL_SELECTORS: tuple[JsonObject, ...] = (
|
||||
{
|
||||
"selector": 0x000,
|
||||
"name": "connection_or_heartbeat_root_candidate",
|
||||
"reason": "idle report selector and CONNECT OK emulator condition both center on selector zero",
|
||||
"seed_values": [0x0080, 0x8080],
|
||||
},
|
||||
{
|
||||
"selector": 0x003,
|
||||
"name": "default_enabled_bit_candidate",
|
||||
"reason": "ROM default table writes E000/E800 selector 0x003 to 0x8000",
|
||||
"seed_values": [0x8000],
|
||||
},
|
||||
{
|
||||
"selector": 0x040,
|
||||
"name": "default_all_ones_or_status_block_candidate",
|
||||
"reason": "ROM default table writes E000/E800 selector 0x040 to 0xFFFF and bench tests repeatedly touched the 0x40 family",
|
||||
"seed_values": [0xFFFF, 0x4030],
|
||||
},
|
||||
{
|
||||
"selector": 0x0F6,
|
||||
"name": "active_status_bridge_candidate",
|
||||
"reason": "loc_48FA tests E1EC bit13 and can enqueue report selector 0x00F6",
|
||||
"seed_values": [0x2000],
|
||||
},
|
||||
{
|
||||
"selector": 0x006C,
|
||||
"name": "command5_be70_candidate",
|
||||
"reason": "continuation command 5 calls BE70 for selector 0x006C",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x006D,
|
||||
"name": "command5_be70_candidate",
|
||||
"reason": "continuation command 5 calls BE70 for selector 0x006D",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x006E,
|
||||
"name": "command5_be70_candidate",
|
||||
"reason": "continuation command 5 calls BE70 for selector 0x006E",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x006B,
|
||||
"name": "connection_latch_clear_candidate",
|
||||
"reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x0096,
|
||||
"name": "connection_latch_clear_candidate",
|
||||
"reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x0097,
|
||||
"name": "connection_latch_clear_candidate",
|
||||
"reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x00C6,
|
||||
"name": "connection_latch_clear_candidate",
|
||||
"reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
|
||||
"seed_values": [],
|
||||
},
|
||||
{
|
||||
"selector": 0x00F8,
|
||||
"name": "connection_latch_clear_candidate",
|
||||
"reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
|
||||
"seed_values": [],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def load_seed_hints_input(path: Path) -> JsonObject:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
if not isinstance(payload, dict) or "instructions" not in payload:
|
||||
raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
|
||||
return payload
|
||||
|
||||
|
||||
def analyze_ccu_seed_hints(payload: Mapping[str, Any], *, rom_path: Path | None = DEFAULT_ROM) -> JsonObject:
|
||||
table_analysis = analyze_table_xrefs(payload)
|
||||
selector_hints = _selector_hints_from_tables(table_analysis)
|
||||
_merge_special_selectors(selector_hints)
|
||||
_merge_observed_reports(selector_hints)
|
||||
|
||||
dispatch = _dispatch_table_summary(payload, rom_path)
|
||||
for entry in dispatch.get("interesting_entries", []):
|
||||
selector = int(entry["selector"])
|
||||
hint = selector_hints.setdefault(selector, _new_selector_hint(selector))
|
||||
hint["score"] += 2
|
||||
hint["reasons"].append(f"selector dispatches to {entry['target_label_or_hex']}")
|
||||
hint["dispatch_target"] = entry
|
||||
|
||||
display = _display_hint_summary(payload, rom_path)
|
||||
seed_plan = _seed_plan(selector_hints)
|
||||
|
||||
candidates = sorted(
|
||||
selector_hints.values(),
|
||||
key=lambda item: (-int(item["score"]), int(item["selector"])),
|
||||
)
|
||||
return {
|
||||
"kind": "ccu_seed_hints",
|
||||
"summary": {
|
||||
"candidate_count": len(candidates),
|
||||
"core_model": (
|
||||
"The RCP likely waits for the CCU to seed mirrored state tables, then uses those "
|
||||
"selector values to update LCD text, panel lamps, and report state changes."
|
||||
),
|
||||
"confidence": "medium",
|
||||
},
|
||||
"table_model": _table_model_summary(table_analysis),
|
||||
"selector_candidates": candidates[:80],
|
||||
"display_text_hints": display,
|
||||
"dispatch_table": dispatch,
|
||||
"seed_plan": seed_plan,
|
||||
"bench_implications": [
|
||||
"Do not wait for non-heartbeat reports as the only activation source; the CCU may be expected to push initial table state first.",
|
||||
"Use command 0 writes for initial seeding, then command 1 readbacks for verification. Treat command 4/5/6 as continuation-only until a live report proves otherwise.",
|
||||
"Selector zero remains the highest-value activation candidate because the emulator reaches CONNECT OK when E000[0]=0x8080 and the selector-zero processing queue runs.",
|
||||
"E1EC/selector 0x00F6 is a strong follow-up candidate because loc_48FA tests bit13 there and can enqueue report 0x00F6.",
|
||||
"LCD text terms such as CAM/BARS/BLACK/COMM LINK appear in ROM records, but they are not direct serial payload strings; they point to selector-driven display builders.",
|
||||
],
|
||||
"caveats": [
|
||||
"Selector names are candidates, not confirmed protocol labels.",
|
||||
"Static table xrefs prove that firmware reads/writes a selector; they do not prove the external CCU must seed it on boot.",
|
||||
"Generated frames are syntactically valid six-byte host frames; bench safety still depends on timing and current RCP state.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def format_text_report(analysis: Mapping[str, Any]) -> str:
|
||||
summary = analysis["summary"]
|
||||
lines = [
|
||||
"H8/536 CCU Seed Hint Report",
|
||||
"",
|
||||
f"Summary: {summary['core_model']}",
|
||||
f"Confidence: {summary['confidence']}",
|
||||
"",
|
||||
"Table Model:",
|
||||
]
|
||||
for table in analysis.get("table_model", []):
|
||||
lines.append(
|
||||
f"- {table['name']}: {table['logical_range_hex']}; accesses={table['access_count']} "
|
||||
f"static selectors={', '.join(table.get('static_selectors_hex', [])[:12]) or 'none'}"
|
||||
)
|
||||
|
||||
lines.extend(["", "Highest-Value Selector Candidates:"])
|
||||
for hint in analysis.get("selector_candidates", [])[:24]:
|
||||
lines.append(
|
||||
f"- {hint['selector_hex']} {hint['name']}: score={hint['score']} "
|
||||
f"tables={', '.join(hint.get('tables', [])) or 'none'}"
|
||||
)
|
||||
for reason in hint.get("reasons", [])[:4]:
|
||||
lines.append(f" - {reason}")
|
||||
frames = hint.get("seed_frames", [])
|
||||
if frames:
|
||||
frame_text = "; ".join(f"{frame['value_hex']} -> {frame['cmd0_frame']}" for frame in frames[:3])
|
||||
lines.append(f" seed frames: {frame_text}")
|
||||
read_frame = hint.get("cmd1_read_frame")
|
||||
if read_frame:
|
||||
lines.append(f" readback frame: {read_frame}")
|
||||
|
||||
display = analysis.get("display_text_hints", {})
|
||||
lines.extend(["", "Display Text Hints:"])
|
||||
for hit in display.get("term_hits", [])[:16]:
|
||||
samples = ", ".join(
|
||||
f"{sample['address_hex']} {sample['text']!r}"
|
||||
for sample in hit.get("samples", [])[:3]
|
||||
)
|
||||
lines.append(f"- {hit['term']}: {hit['hit_count']} hit(s){f' - {samples}' if samples else ''}")
|
||||
|
||||
dispatch = analysis.get("dispatch_table", {})
|
||||
lines.extend(["", "Selector Dispatch Hints:"])
|
||||
lines.append(
|
||||
f"- table {dispatch.get('table_base_hex', 'unknown')}: "
|
||||
f"{dispatch.get('interesting_count', 0)} non-default/interesting entries"
|
||||
)
|
||||
for entry in dispatch.get("interesting_entries", [])[:16]:
|
||||
lines.append(
|
||||
f" - selector {entry['selector_hex']} -> {entry['target_label_or_hex']} "
|
||||
f"(dispatch index {entry['dispatch_index_hex']})"
|
||||
)
|
||||
|
||||
seed_plan = analysis.get("seed_plan", {})
|
||||
lines.extend(["", "Candidate Fake-CCU Seed Plan:"])
|
||||
for step in seed_plan.get("steps", []):
|
||||
lines.append(f"- {step['name']}: {step['frame']}")
|
||||
lines.append(f" {step['why']}")
|
||||
|
||||
lines.extend(["", "Bench Implications:"])
|
||||
for item in analysis.get("bench_implications", []):
|
||||
lines.append(f"- {item}")
|
||||
|
||||
lines.extend(["", "Caveats:"])
|
||||
for item in analysis.get("caveats", []):
|
||||
lines.append(f"- {item}")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def write_ccu_seed_hints(
|
||||
input_path: Path,
|
||||
output_path: Path,
|
||||
*,
|
||||
rom_path: Path | None = DEFAULT_ROM,
|
||||
as_json: bool = False,
|
||||
) -> JsonObject:
|
||||
analysis = analyze_ccu_seed_hints(load_seed_hints_input(input_path), rom_path=rom_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if as_json:
|
||||
output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
else:
|
||||
output_path.write_text(format_text_report(analysis), encoding="utf-8")
|
||||
return analysis
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None, stdout: Any | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Mine ROM hints for CCU-to-RCP state seeding candidates.")
|
||||
parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
|
||||
parser.add_argument("--rom", type=Path, default=DEFAULT_ROM, help="ROM binary used for LCD text and dispatch-table mining")
|
||||
parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text")
|
||||
parser.add_argument("--out", type=Path, default=None, help="write report to this path")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
stream = stdout
|
||||
if stream is None:
|
||||
import sys
|
||||
|
||||
stream = sys.stdout
|
||||
|
||||
rom_path = args.rom if args.rom and args.rom.exists() else None
|
||||
analysis = analyze_ccu_seed_hints(load_seed_hints_input(args.input), rom_path=rom_path)
|
||||
rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n" if args.json else format_text_report(analysis)
|
||||
if args.out:
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(rendered, encoding="utf-8")
|
||||
print(f"wrote {args.out}", file=stream)
|
||||
else:
|
||||
print(rendered, end="", file=stream)
|
||||
return 0
|
||||
|
||||
|
||||
def encode_host_frame(command: int, selector: int, value: int = 0) -> list[int]:
|
||||
byte1, byte2 = selector_bytes(selector)
|
||||
frame = [command & 0x07, byte1, byte2, (value >> 8) & 0xFF, value & 0xFF]
|
||||
frame.append(checksum(frame))
|
||||
return frame
|
||||
|
||||
|
||||
def frame_hex(frame: list[int]) -> str:
|
||||
return " ".join(f"{byte & 0xFF:02X}" for byte in frame)
|
||||
|
||||
|
||||
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 checksum(frame_without_checksum: list[int]) -> int:
|
||||
value = CHECKSUM_SEED
|
||||
for byte in frame_without_checksum[:5]:
|
||||
value ^= byte & 0xFF
|
||||
return value & 0xFF
|
||||
|
||||
|
||||
def _selector_hints_from_tables(table_analysis: Mapping[str, Any]) -> dict[int, JsonObject]:
|
||||
hints: dict[int, JsonObject] = {}
|
||||
for table in table_analysis.get("tables", []):
|
||||
if not isinstance(table, Mapping):
|
||||
continue
|
||||
table_name = str(table.get("name", "unknown_table"))
|
||||
element = str(table.get("element_candidate", ""))
|
||||
for access in table.get("accesses", []):
|
||||
if not isinstance(access, Mapping) or not isinstance(access.get("offset"), int):
|
||||
continue
|
||||
selector = _selector_from_offset(int(access["offset"]), element)
|
||||
if selector is None:
|
||||
continue
|
||||
hint = hints.setdefault(selector, _new_selector_hint(selector))
|
||||
hint["score"] += _access_score(access, table_name)
|
||||
if table_name not in hint["tables"]:
|
||||
hint["tables"].append(table_name)
|
||||
hint["accesses"].append(
|
||||
{
|
||||
"address_hex": access.get("instruction_address_hex"),
|
||||
"function": access.get("function_label"),
|
||||
"table": table_name,
|
||||
"access": access.get("access"),
|
||||
"instruction": access.get("instruction"),
|
||||
}
|
||||
)
|
||||
reason = _access_reason(access, table_name)
|
||||
if reason not in hint["reasons"]:
|
||||
hint["reasons"].append(reason)
|
||||
return hints
|
||||
|
||||
|
||||
def _merge_special_selectors(hints: dict[int, JsonObject]) -> None:
|
||||
for item in SPECIAL_SELECTORS:
|
||||
selector = int(item["selector"])
|
||||
hint = hints.setdefault(selector, _new_selector_hint(selector))
|
||||
hint["score"] += 5
|
||||
hint["name"] = str(item["name"])
|
||||
hint["reasons"].append(str(item["reason"]))
|
||||
for value in item.get("seed_values", []):
|
||||
_add_seed_value(hint, int(value))
|
||||
|
||||
|
||||
def _merge_observed_reports(hints: dict[int, JsonObject]) -> None:
|
||||
for report in OBSERVED_TX_REPORT_OVERLAY:
|
||||
selector = int(report["logical_index"])
|
||||
hint = hints.setdefault(selector, _new_selector_hint(selector))
|
||||
hint["score"] += 3
|
||||
hint["name"] = str(report["name_candidate"])
|
||||
frames = ", ".join(str(frame) for frame in report.get("observed_frames_hex", []))
|
||||
hint["reasons"].append(f"observed RCP autonomous report frame(s): {frames}")
|
||||
|
||||
|
||||
def _seed_plan(hints: Mapping[int, JsonObject]) -> JsonObject:
|
||||
planned = [
|
||||
(0x000, 0x8080, "selector zero active/connect candidate from emulator state search"),
|
||||
(0x003, 0x8000, "ROM default state also sets selector 0x003 high bit"),
|
||||
(0x040, 0xFFFF, "ROM default all-ones/status candidate touched by bench 0x40 family"),
|
||||
(0x0F6, 0x2000, "sets E1EC bit13 candidate used by loc_48FA report bridge"),
|
||||
]
|
||||
steps: list[JsonObject] = []
|
||||
for selector, value, why in planned:
|
||||
frame = frame_hex(encode_host_frame(0x00, selector, value))
|
||||
readback = frame_hex(encode_host_frame(0x01, selector, 0))
|
||||
hint = hints.get(selector)
|
||||
if hint is not None:
|
||||
_add_seed_value(hint, value)
|
||||
steps.append(
|
||||
{
|
||||
"selector": selector,
|
||||
"selector_hex": f"0x{selector:03X}",
|
||||
"value": value,
|
||||
"value_hex": f"0x{value:04X}",
|
||||
"name": f"cmd0 seed selector 0x{selector:03X} = 0x{value:04X}",
|
||||
"frame": frame,
|
||||
"readback_frame": readback,
|
||||
"why": why,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"model": "candidate initial CCU state push using command 0 writes, verified with command 1 reads",
|
||||
"steps": steps,
|
||||
}
|
||||
|
||||
|
||||
def _table_model_summary(table_analysis: Mapping[str, Any]) -> list[JsonObject]:
|
||||
rows: list[JsonObject] = []
|
||||
for table in table_analysis.get("tables", []):
|
||||
if not isinstance(table, Mapping):
|
||||
continue
|
||||
element = str(table.get("element_candidate", ""))
|
||||
selectors = []
|
||||
for offset in table.get("static_offsets", []):
|
||||
if isinstance(offset, int):
|
||||
selector = _selector_from_offset(offset, element)
|
||||
if selector is not None:
|
||||
selectors.append(selector)
|
||||
rows.append(
|
||||
{
|
||||
"name": table.get("name"),
|
||||
"logical_range_hex": f"{table.get('logical_base_address_hex')}-{table.get('logical_range_end_hex')}",
|
||||
"access_count": table.get("access_count", 0),
|
||||
"static_selectors": sorted(set(selectors)),
|
||||
"static_selectors_hex": [f"0x{selector:03X}" for selector in sorted(set(selectors))],
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def _display_hint_summary(payload: Mapping[str, Any], rom_path: Path | None) -> JsonObject:
|
||||
del payload
|
||||
if rom_path is None or not rom_path.exists():
|
||||
return {"term_hits": [], "note": "ROM binary was not available for LCD text mining."}
|
||||
rom = Rom(rom_path.read_bytes())
|
||||
text = analyze_lcd_text(rom, None, search_terms=DISPLAY_TERMS, max_candidates=360)
|
||||
term_hits = []
|
||||
for search in text.get("searches", []):
|
||||
if not isinstance(search, Mapping):
|
||||
continue
|
||||
samples = []
|
||||
for address in search.get("literal_hits", [])[:4]:
|
||||
if isinstance(address, int):
|
||||
samples.append({"address_hex": h16(address), "text": f"literal {search.get('term')}"})
|
||||
for hit in search.get("candidate_hits", [])[:6]:
|
||||
if not isinstance(hit, Mapping):
|
||||
continue
|
||||
samples.append(
|
||||
{
|
||||
"address_hex": h16(int(hit.get("address", 0))),
|
||||
"text": str(hit.get("trimmed") or hit.get("text") or ""),
|
||||
}
|
||||
)
|
||||
hit_count = len(search.get("literal_hits", []) or []) + len(search.get("candidate_hits", []) or [])
|
||||
term_hits.append({"term": search.get("term"), "hit_count": hit_count, "samples": samples[:8]})
|
||||
return {
|
||||
"term_hits": term_hits,
|
||||
"regions": text.get("regions", [])[:8],
|
||||
"note": "Text hits are ROM display resources, not literal serial payloads.",
|
||||
}
|
||||
|
||||
|
||||
def _dispatch_table_summary(payload: Mapping[str, Any], rom_path: Path | None) -> JsonObject:
|
||||
table_base = 0x28A6
|
||||
entries = _indirect_entries(payload, table_base)
|
||||
if not entries and rom_path is not None and rom_path.exists():
|
||||
entries = _raw_dispatch_entries(rom_path, table_base, 128)
|
||||
if not entries:
|
||||
return {"table_base": table_base, "table_base_hex": h16(table_base), "interesting_count": 0, "interesting_entries": []}
|
||||
|
||||
target_counts = Counter(int(entry["target"]) for entry in entries if isinstance(entry.get("target"), int))
|
||||
default_target, _ = target_counts.most_common(1)[0]
|
||||
interesting: list[JsonObject] = []
|
||||
for entry in entries:
|
||||
index = int(entry["index"])
|
||||
selector = _selector_from_dispatch_index(index)
|
||||
if selector is None:
|
||||
continue
|
||||
target = int(entry["target"])
|
||||
label = entry.get("target_label") or h16(target)
|
||||
decoded = bool(entry.get("decoded_code", True))
|
||||
if target == default_target and decoded:
|
||||
continue
|
||||
interesting.append(
|
||||
{
|
||||
"selector": selector,
|
||||
"selector_hex": f"0x{selector:03X}",
|
||||
"dispatch_index": index,
|
||||
"dispatch_index_hex": f"0x{index:03X}",
|
||||
"entry_address_hex": h16(int(entry.get("entry_address", table_base + index * 2))),
|
||||
"target": target,
|
||||
"target_hex": h16(target),
|
||||
"target_label_or_hex": str(label),
|
||||
"decoded_code": decoded,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"table_base": table_base,
|
||||
"table_base_hex": h16(table_base),
|
||||
"entry_count": len(entries),
|
||||
"default_target_hex": h16(default_target),
|
||||
"interesting_count": len(interesting),
|
||||
"interesting_entries": interesting[:80],
|
||||
}
|
||||
|
||||
|
||||
def _indirect_entries(payload: Mapping[str, Any], table_base: int) -> list[JsonObject]:
|
||||
indirect = payload.get("indirect_flow")
|
||||
if not isinstance(indirect, Mapping):
|
||||
return []
|
||||
for site in indirect.get("sites", []):
|
||||
if not isinstance(site, Mapping):
|
||||
continue
|
||||
table = site.get("table")
|
||||
if isinstance(table, Mapping) and int(table.get("base", -1)) == table_base:
|
||||
entries = table.get("entries", [])
|
||||
if isinstance(entries, list):
|
||||
return [dict(entry) for entry in entries if isinstance(entry, Mapping)]
|
||||
return []
|
||||
|
||||
|
||||
def _raw_dispatch_entries(rom_path: Path, table_base: int, count: int) -> list[JsonObject]:
|
||||
rom = Rom(rom_path.read_bytes())
|
||||
entries = []
|
||||
for index in range(count):
|
||||
address = table_base + index * 2
|
||||
if not rom.contains(address, 2):
|
||||
break
|
||||
target = rom.u16(address)
|
||||
entries.append({"index": index, "entry_address": address, "target": target, "target_label": None, "decoded_code": True})
|
||||
return entries
|
||||
|
||||
|
||||
def _selector_from_dispatch_index(index: int) -> int | None:
|
||||
if 0 <= index <= 0x007F:
|
||||
return index
|
||||
if 0x0100 <= index <= 0x01FF:
|
||||
return index - 0x0080
|
||||
if 0x0200 <= index <= 0x027F:
|
||||
return index - 0x0080
|
||||
return None
|
||||
|
||||
|
||||
def _selector_from_offset(offset: int, element: str) -> int | None:
|
||||
if element == "word_value":
|
||||
if offset % 2:
|
||||
return None
|
||||
return (offset // 2) & 0x01FF
|
||||
return offset & 0x01FF
|
||||
|
||||
|
||||
def _new_selector_hint(selector: int) -> JsonObject:
|
||||
return {
|
||||
"selector": selector,
|
||||
"selector_hex": f"0x{selector:03X}",
|
||||
"name": "state_selector_candidate",
|
||||
"score": 0,
|
||||
"tables": [],
|
||||
"reasons": [],
|
||||
"accesses": [],
|
||||
"seed_frames": [],
|
||||
"cmd1_read_frame": frame_hex(encode_host_frame(0x01, selector, 0)),
|
||||
}
|
||||
|
||||
|
||||
def _add_seed_value(hint: JsonObject, value: int) -> None:
|
||||
frames = hint.setdefault("seed_frames", [])
|
||||
value_hex = f"0x{value & 0xFFFF:04X}"
|
||||
if any(frame.get("value_hex") == value_hex for frame in frames):
|
||||
return
|
||||
frames.append(
|
||||
{
|
||||
"value": value & 0xFFFF,
|
||||
"value_hex": value_hex,
|
||||
"cmd0_frame": frame_hex(encode_host_frame(0x00, int(hint["selector"]), value)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _access_score(access: Mapping[str, Any], table_name: str) -> int:
|
||||
score = 1
|
||||
if access.get("access") == "read":
|
||||
score += 1
|
||||
if access.get("access") == "write":
|
||||
score += 1
|
||||
if "primary" in table_name or "current" in table_name:
|
||||
score += 1
|
||||
return score
|
||||
|
||||
|
||||
def _access_reason(access: Mapping[str, Any], table_name: str) -> str:
|
||||
function = access.get("function_label") or "<no function>"
|
||||
instruction = access.get("instruction") or ""
|
||||
return f"{table_name} {access.get('access')} in {function}: {instruction}"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"analyze_ccu_seed_hints",
|
||||
"checksum",
|
||||
"encode_host_frame",
|
||||
"format_text_report",
|
||||
"frame_hex",
|
||||
"load_seed_hints_input",
|
||||
"main",
|
||||
"selector_bytes",
|
||||
"write_ccu_seed_hints",
|
||||
]
|
||||
8
h8536_ccu_seed_hints.py
Normal file
8
h8536_ccu_seed_hints.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compatibility wrapper for the H8/536 CCU seed-hint miner."""
|
||||
|
||||
from h8536.ccu_seed_hints import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
118
tests/test_ccu_seed_hints.py
Normal file
118
tests/test_ccu_seed_hints.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from h8536.ccu_seed_hints import (
|
||||
analyze_ccu_seed_hints,
|
||||
checksum,
|
||||
encode_host_frame,
|
||||
frame_hex,
|
||||
main,
|
||||
selector_bytes,
|
||||
write_ccu_seed_hints,
|
||||
)
|
||||
|
||||
|
||||
def reference(address: int) -> dict:
|
||||
return {"address": address}
|
||||
|
||||
|
||||
def instruction(address: int, mnemonic: str, operands: str = "", refs: list[int] | None = None) -> dict:
|
||||
return {
|
||||
"address": address,
|
||||
"mnemonic": mnemonic,
|
||||
"operands": operands,
|
||||
"text": f"{mnemonic} {operands}".strip(),
|
||||
"references": [reference(item) for item in (refs or [])],
|
||||
"targets": [],
|
||||
}
|
||||
|
||||
|
||||
def payload() -> dict:
|
||||
return {
|
||||
"instructions": [
|
||||
instruction(0xC000, "MOV:G.W", "#H'0006, R3"),
|
||||
instruction(0xC004, "MOV:G.W", "@(-H'2000,R3), R0"),
|
||||
instruction(0xC008, "MOV:G.W", "R1, @H'E1EC", [0xE1EC]),
|
||||
],
|
||||
"call_graph": {
|
||||
"nodes": [
|
||||
{"start": 0xC000, "end": 0xC0FF, "label": "loc_C000"},
|
||||
],
|
||||
},
|
||||
"indirect_flow": {
|
||||
"sites": [
|
||||
{
|
||||
"table": {
|
||||
"base": 0x28A6,
|
||||
"entries": [
|
||||
{"index": 0, "entry_address": 0x28A6, "target": 0x2CB9, "target_label": "loc_2CB9"},
|
||||
{"index": 1, "entry_address": 0x28A8, "target": 0x1234, "target_label": "loc_1234"},
|
||||
{"index": 2, "entry_address": 0x28AA, "target": 0x1234, "target_label": "loc_1234"},
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class CcuSeedHintsTest(unittest.TestCase):
|
||||
def test_selector_encoding_matches_loc_622b_ranges(self):
|
||||
self.assertEqual(selector_bytes(0x000), (0x00, 0x00))
|
||||
self.assertEqual(selector_bytes(0x07F), (0x00, 0x7F))
|
||||
self.assertEqual(selector_bytes(0x080), (0x01, 0x00))
|
||||
self.assertEqual(selector_bytes(0x17F), (0x01, 0xFF))
|
||||
self.assertEqual(selector_bytes(0x180), (0x02, 0x00))
|
||||
self.assertEqual(selector_bytes(0x1FF), (0x02, 0x7F))
|
||||
|
||||
def test_frame_encoding_uses_xor_seed(self):
|
||||
frame = encode_host_frame(0x00, 0x000, 0x8080)
|
||||
|
||||
self.assertEqual(frame, [0x00, 0x00, 0x00, 0x80, 0x80, 0x5A])
|
||||
self.assertEqual(checksum(frame[:5]), frame[5])
|
||||
self.assertEqual(frame_hex(frame), "00 00 00 80 80 5A")
|
||||
|
||||
def test_analysis_emits_seed_plan_and_selector_reasons(self):
|
||||
analysis = analyze_ccu_seed_hints(payload(), rom_path=None)
|
||||
by_selector = {
|
||||
int(item["selector"]): item
|
||||
for item in analysis["selector_candidates"]
|
||||
}
|
||||
|
||||
self.assertEqual(analysis["kind"], "ccu_seed_hints")
|
||||
self.assertIn(0x000, by_selector)
|
||||
self.assertIn(0x0F6, by_selector)
|
||||
self.assertIn("00 00 00 80 80 5A", [step["frame"] for step in analysis["seed_plan"]["steps"]])
|
||||
self.assertIn("01 01 76 00 00 2C", [step["readback_frame"] for step in analysis["seed_plan"]["steps"]])
|
||||
|
||||
def test_write_json_output(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
input_path = Path(tmp) / "rom.json"
|
||||
output_path = Path(tmp) / "hints.json"
|
||||
input_path.write_text(json.dumps(payload()), encoding="utf-8")
|
||||
|
||||
write_ccu_seed_hints(input_path, output_path, rom_path=None, as_json=True)
|
||||
|
||||
written = json.loads(output_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(written["kind"], "ccu_seed_hints")
|
||||
self.assertGreaterEqual(written["summary"]["candidate_count"], 1)
|
||||
|
||||
def test_cli_writes_text_report(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
input_path = Path(tmp) / "rom.json"
|
||||
output_path = Path(tmp) / "hints.txt"
|
||||
input_path.write_text(json.dumps(payload()), encoding="utf-8")
|
||||
|
||||
stdout = io.StringIO()
|
||||
rc = main([str(input_path), "--rom", str(Path(tmp) / "missing.bin"), "--out", str(output_path)], stdout=stdout)
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("wrote", stdout.getvalue())
|
||||
self.assertIn("Candidate Fake-CCU Seed Plan", output_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user