1
0

Compare commits

...

17 Commits

Author SHA1 Message Date
Aiden
4ebbb963bd selector lamps 2026-05-28 10:12:08 +10:00
Aiden
4364d0ed48 updates 2026-05-27 21:37:50 +10:00
Aiden
21f0e455ee webcam copy 2026-05-27 12:17:12 +10:00
Aiden
c0304c575c traces 2026-05-27 11:50:10 +10:00
Aiden
0d099235c5 knee page discovery 2026-05-26 19:08:15 +10:00
Aiden
57547fb6ed knee 2026-05-26 18:38:08 +10:00
Aiden
a187214e06 knee auto tests 2026-05-26 18:35:13 +10:00
Aiden
c007f2180c otehr lamps 2026-05-26 18:25:03 +10:00
Aiden
566b4ab108 gated run 2026-05-26 17:44:16 +10:00
Aiden
8d98beb6aa more lamps 2026-05-26 17:18:05 +10:00
Aiden
fb282fea49 black lamp refining 2026-05-26 17:09:52 +10:00
Aiden
de992c6087 Black flare lamp 2026-05-26 17:06:43 +10:00
Aiden
99946170cf more lamp work 2026-05-26 17:03:49 +10:00
Aiden
fec48518c1 isolating lamp behaviour 2026-05-26 16:59:09 +10:00
Aiden
11b6a2dc3b Shutter display 2026-05-26 16:38:32 +10:00
Aiden
3e1d30527f Intresting 2026-05-26 16:17:24 +10:00
Aiden
d9a9dade41 copy in progress 2026-05-26 16:01:20 +10:00
160 changed files with 982031 additions and 333 deletions

View File

@@ -55,14 +55,29 @@ To start the current emulator harness:
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\table-sweep-ack-000-07f.json --log captures\table-sweep-ack-000-07f.txt --result-json captures\table-sweep-ack-000-07f-result.json
.\.venv\Scripts\python.exe scripts\state_map_runner.py --preset ok --prompt-screen
.\.venv\Scripts\python.exe scripts\state_map_runner.py --analyze-log captures\ack-race-000-001.txt
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --dry-run
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --port COM5 --duration 30 --log captures\ccu-keepalive.txt
.\.venv\Scripts\python.exe h8536_emulator_state_search.py --preset connect-queue --target ok --first-hit --json-out build\connect-state-search-ok.json
.\.venv\Scripts\python.exe h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity
.\.venv\Scripts\python.exe h8536_emulator.py --max-steps 250000 --p9-fast-path --eeprom-seed blank --eeprom-save build\emulator-eeprom-boot.bin --eeprom-report build\emulator-eeprom-boot.txt --eeprom-report-json build\emulator-eeprom-boot.json
```
The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed.
Optional webcam snapshots for `scripts\serial_scenario.py` use OpenCV; install it only on the bench machine that needs panel photos:
The current PT2/protocol reconstruction is documented in [docs/pt2-protocol.md](docs/pt2-protocol.md).
```powershell
.\.venv\Scripts\python.exe -m pip install opencv-python
```
Example snapshot run:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\copy-step-006d-006c-1000ms.json --parity E --quiet-console --log captures\copy-webcam.txt --result-json captures\copy-webcam-result.json --snapshot-dir captures\copy-webcam-shots --camera-index 4 --snapshot-delays 0.5
```
Current bench calibration: webcam index `4` gives the useful panel view, and a `0.5` second post-command delay is enough for LCD changes to settle without producing too many images.
The current PT2/protocol reconstruction is documented in [docs/pt2-protocol.md](docs/pt2-protocol.md), with focused mini-notes for [COPY state](docs/pt2-copy-state-machine.md), [menu state](docs/pt2-menu-state-machine.md), the [session rhythm ROM trace](docs/pt2-session-rhythm-trace.md), [continuation commands](docs/pt2-continuation-command-trace.md), and [report aftermath handling](docs/pt2-report-aftermath-trace.md).
## Real Bench Serial Format
@@ -133,6 +148,8 @@ Minimal smoke-test shape:
- Includes a bench ACK probe that reproduces the `01 00 00...` -> `01 00 01...` visible retry burst, waits for `07 80 40 20 90 2D`, then sends a candidate command-5 ACK and reports whether the target keeps repeating.
- Includes a checksum-resynchronizing bench receiver that scans RX byte streams for valid six-byte frames, avoids common shifted-heartbeat false locks, and can fall back to the old fixed six-byte slicer with `--sync fixed`.
- Includes a JSON scenario bench runner for repeatable multi-step serial tests, including low-latency ACK-aware command-1 probes that can send the current command-5 ACK candidate immediately after the retry frame appears, with explicit max-ACK/max-target guardrails.
- The scenario runner can optionally schedule webcam snapshots on each command send, writing image paths into logs/result JSON without delaying timing-sensitive serial steps.
- Includes a modular fake-CCU runner in `ccu_emulator/` that seeds active state, listens for complete RCP report frames, and immediately sends the neutral command-5 ACK `05 00 40 00 00 1F`; optional periodic refresh frames let lamp/value streaming be tested separately from report ACKs.
- Includes a PT2 state-map-aware bench runner/analyzer for the current CONNECT gate proof: it hunts a fresh device `07...` visible-drain token candidate, sends exactly one selector-zero command-4 force, probes `E000[0]` with command 1, optionally uses command 7 to recover a hidden finalized response, and labels likely token-destroying turns.
- Includes a bounded emulator CONNECT state-search tool that patches small ROM-derived RAM/table surfaces, runs either the direct CONNECT branch or the selector-zero queue dispatch path, and classifies LCD outcomes as OK, DXC, NOT ACT, or other.
- Includes a bench-log replay harness that feeds recorded host TX frames back into the ROM emulator with bench-style UART byte timing by default and asserts parity against the real device's observed response/LCD state.
@@ -150,7 +167,7 @@ Current serial observations:
- Bench serial-format finding: real hardware talks `38400 8E1`. Earlier `8N1` captures primarily exercised SCI1 parity/error handling and retry echoes, not the normal command path. After switching bench scripts to even parity, the selector-zero CONNECT path can reach `CONNECT: OK`.
- Bench CONNECT recovery finding: `CONNECT:NOT ACT` is recoverable without a power cycle. This makes it a normal no-active-session/cleared-state display rather than a terminal latch; tests can now probe from the idle NOT ACT state directly, then separately check whether OK is held or needs periodic CCU-like refresh traffic.
- Bench CONNECT cadence finding: the `40 -> 80 -> C0` sequence stayed at `CONNECT:NOT ACT` with 10 ms, 50 ms, and 150 ms gaps, but produced `CONNECT: OK` then returned to `CONNECT:NOT ACT` with 700 ms and 1.5 s gaps. At 700 ms, single `40`/`80`/`C0` frames did not work, but all tested two-frame pairs did. Repeated `80 -> 80` at about 700 ms also worked, so the values do not need to differ. The no-power-cycle NOT ACT recovery capture produced repeated `02 00 02 00 00 5A` OK-path responses before heartbeat traffic resumed.
- Bench special-selector finding: in the CONNECT OK advance sweep, command-5 selector `0x006C` (`05 00 6C 00 00 33`) produced `CONNECT OK` then a blank LCD with the CAM POWER lamp still on, while selector `0x006D` (`05 00 6D 00 00 32`) produced `CONNECT OK` then `COPY IN PROGRESS` then `CONNECT NOT ACT`. A later fresh isolated `ack-006d` run in `captures/connect-ok-advance-special-20260526-153339.txt` reproduced the copy path after a relay power-cycle. Forced ROM decoding confirms `0x006C -> H'2FAF` and `0x006D -> H'3015`; the `0x006D` path sets display selector `F732=H'1903`, a long `F798` countdown, and the ROM contains the `COPY IN PROGRESS` LCD string. LCD dispatch now traces through `493E[0x19] -> H'930A`, with local table entry `H'9F6A` building `COPY IN PROGRESS` and entry `H'9FDA` building `COPY COMPLETED`; `0x006C` appears to be the completion/exit sibling only after `0x006D` has set the `F795.6/F795.7` copy flags. The RCP-TX7 manual identifies `COPY IN PROGRESS` as the multi-camera `COPY TO SLAVES` transfer state over the RS232C command-link system, with controls locked until `COPY COMPLETED`.
- Bench special-selector finding: in the CONNECT OK advance sweep, command-5 selector `0x006C` (`05 00 6C 00 00 33`) produced `CONNECT OK` then a blank LCD with the CAM POWER lamp still on, while selector `0x006D` (`05 00 6D 00 00 32`) produced `CONNECT OK` then `COPY IN PROGRESS` then `CONNECT NOT ACT`. A later fresh isolated `ack-006d` run in `captures/connect-ok-advance-special-20260526-153339.txt` reproduced the copy path after a relay power-cycle. Forced ROM decoding confirms `0x006C -> H'2FAF` and `0x006D -> H'3015`; the `0x006D` path sets display selector `F732=H'1903`, a long `F798` countdown, and the ROM contains the `COPY IN PROGRESS` LCD string. LCD dispatch now traces through `493E[0x19] -> H'930A`, with local table entry `H'9F6A` building `COPY IN PROGRESS` and entry `H'9FDA` building `COPY COMPLETED`; `0x006C` appears to be the completion/exit sibling only after `0x006D` has set the `F795.6/F795.7` copy flags. The RCP-side menu trace now also identifies page `0x01` table entry `H'6FF0` as `OTHERS / COPY TO SLAVES`; it is gated by `E400[0x0015]`, and its local copy-start branch also requires `F791.7` before setting `F76E.6`, `F795.7`, `F731.7`, `F798=H'C8`, `F711.7`, and `F726=H'64`. The RCP-TX7 manual identifies `COPY IN PROGRESS` as the multi-camera `COPY TO SLAVES` transfer state over the RS232C command-link system, with controls locked until `COPY COMPLETED`.
- ROM report-source finding: the active `02/01 ...` frames exposed during CONNECT OK attempts are autonomous `F870 -> BAF2 -> BA26` report-queue transmissions, not ordinary command-1 readbacks. The ROM sets `FAA2.3/FAA3.7` after sending them, so the CCU probably needs to answer in that continuation window with command `4`, `5`, or `6` to consume the report queue and keep the session alive.
- Board/P9 finding: traced MCU pin 62 `P91` reaches X24164 pin 6 `SCL`, and MCU pin 68 `P97` reaches the shared X24164 pin 5 `SDA` node. The emulator now treats the ROM's `C121/C08B/C0DB/C10C/C142` P9 routines as an X24164-style two-wire EEPROM bus, with ROM logical addresses `0x000-0x7FF` on the `H'A0/H'A1` control-byte family and `0x800-0xFFF` on `H'E0/H'E1`.
- EEPROM role finding: `loc_40BB` checks `P7DR.7` and the `F402 == H'6B6F` signature before defaulting EEPROM/shadow tables; `loc_4103` writes ROM default words through `BFE0`, `loc_41D2` reads sixteen 8-byte records into `F7B0-F82F`, and the command-4 path at `BD2B-BD5F` can persist serial table writes when `F76E.7` is set.
@@ -159,6 +176,7 @@ Current serial observations:
- Emulator board-state finding: P7 now reads external pin state for input bits, so the DIP-off default is modeled as `--p7-input 0xFF`; `--eeprom-seed factory` can pre-seed the X24164 devices and `F400-F4FF` shadow from the ROM default table for already-initialized-state experiments.
- 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.
- Emulator panel-input finding: `h8536_emulator_rx_probe.py` can now inject ROM-level panel edges using traced shadow/dirty bytes. `--panel-press cam-power` models `F105 -> F6D4.3/F6F2.4`, `--panel-press call` models `F006 -> F6DB.5/F6F3.3`, and raw specs such as `--panel F6D4.6=press` let adjacent report-capable table entries be probed while the ROM still decides whether a serial report, menu action, or no-op happens.
- 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.
@@ -249,6 +267,7 @@ For gate/queue and table reports:
python h8536_serial_gate.py --help
python h8536_rx_branch_trace.py --help
python h8536_report_source_trace.py --help
python h8536_panel_button_trace.py --help
python h8536_table_xrefs.py --help
python h8536_ccu_seed_hints.py --help
python h8536_eeprom_layout.py --help
@@ -258,6 +277,7 @@ python h8536_consistency.py --help
- `h8536_serial_gate.py`: reports the autonomous TX gate and report queue evidence.
- `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_panel_button_trace.py`: decodes the `H'2706` panel-button jump table and traces known CALL/CAM POWER reports back through the input shadow bytes.
- `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_eeprom_layout.py`: mines the X24164 EEPROM layout, ROM factory defaults, persistent record slots, and serial selector-to-EEPROM offset mapping.
@@ -289,6 +309,9 @@ python h8536_emulator_rx_divergence.py --help
- `--trace-report-gates`, `--trace-report-queue`, and `--trace-ram-lifecycle`: inspect the serial report queue, `loc_4046`/`F9C4` gate, and watched RAM byte history.
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
- `h8536_emulator_rx_probe.py --wait-heartbeats 1 --panel-press cam-power`: inject the traced CAM POWER edge into the ROM panel shadow/dirty path and listen for the resulting selector `0x0007` behavior.
- `h8536_emulator_rx_probe.py --panel call=press --panel call=release --keep-listening`: preserve ordered synthetic panel actions and test the CALL active/inactive path through the normal ROM dispatcher.
- `h8536_emulator_rx_probe.py --panel F6D4.6=press`: probe an adjacent raw matrix bit from the table at `H'2706`; raw specs may be shadow bytes like `F6D4.6` or source bytes like `F105.6`.
- `h8536_emulator_rx_probe.py --uart-timing --uart-baud 38400 "04 00 00 80 00"`: inject all six host bytes with bench-style wire spacing of about 260 us per byte, letting RXI/TXI/timers interleave; if the ROM has not cleared `RDRF` before the next byte, the SCI model raises `ORER`. The real bench link is `8E1`.
- `h8536_emulator_rx_probe.py --uart-timing --uart-format 8E1 --tx-wire-timing --wait-heartbeats 2 --post-frame-ms 700 "04 00 00 80 00 DE" "04 00 00 80 00 DE"`: replay the CONNECT refresh shape after heartbeat readiness and keep the emulator running for a bench-scale gap after each frame. The RAM trace now tags interesting accesses with the executing ROM PC, models SCI1 TDRE/TXI at 8E1 character time, and reports whether X24164 EEPROM bytes were written.
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
@@ -336,14 +359,17 @@ python h8536_emulator_rx_divergence.py --help
- `h8536/protocol_capture.py`: timestamped serial capture parser, frame recombiner, and cadence/gate-session analyzer.
- `h8536/serial_scenario.py`: JSON-driven bench scenario engine shared by real-device serial scripts.
- `h8536/state_map_runner.py`: PT2 state-map proof runner and bench-log analyzer for visible-drain token, selector-zero force, `E000[0]` readback, and command-7 recovery experiments.
- `ccu_emulator/`: modular fake-CCU package for reactive report ACKs and optional periodic state refreshes.
- `h8536/serial_gate.py`: autonomous TX gate/queue state-machine reconstruction.
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
- `h8536/panel_button_trace.py`: panel button matrix/jump-table tracer for CALL, CAM POWER, and adjacent report-capable button paths.
- `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/eeprom_layout.py`: ROM miner for X24164 EEPROM defaults, 8-byte record slots, and serial persistence mapping.
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, bench-style UART injection timing, P9/X24164 EEPROM bus model, LCD model, manual-derived FRT timer scheduling, runner, probe, CLI, and peripheral scaffolding.
- `h8536/emulator/eeprom_image.py`: logical EEPROM image dump/report helpers for emulator runs, including factory diffs and record-slot summaries.
- `h8536/emulator/panel.py`: synthetic panel input descriptors for known CALL/CAM POWER edges and raw ROM shadow/source-bit injection.
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
- `h8536/emulator/state_search.py`: bounded internal-state search for CONNECT LCD outcomes using ROM execution plus explicit RAM/table patches.
- `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path.
@@ -354,7 +380,7 @@ python h8536_emulator_rx_divergence.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_ccu_seed_hints.py`, `h8536_eeprom_layout.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
- `h8536_serial_gate.py`, `h8536_report_source_trace.py`, `h8536_panel_button_trace.py`, `h8536_table_xrefs.py`, `h8536_ccu_seed_hints.py`, `h8536_eeprom_layout.py`, `h8536_consistency.py`: sidecar analysis CLI wrappers.
- `h8536_emulator.py`, `h8536_emulator_probe.py`, `h8536_emulator_rx_probe.py`, `h8536_emulator_rx_divergence.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.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
# 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
### CAM POWER
- Emitted selector: `0x0007`
- Handler: `H'1F40`
- Edge source: `F105 -> F6D4` via `F6F2.4`
- Trigger bit: `F6D4.3`
- Table slot: `H'274C` -> `H'1F40`
- Current-level tests: F6D4.3
- State writes: E80E
### IRIS/M.BLACK LINK
- Emitted selector: `0x0013`
- Handler: `H'200E`
- Edge source: `F006 -> F6DB` via `F6F3.3`
- Trigger bit: `F6DB.7`
- Table slot: `H'27C4` -> `H'200E`
- Current-level tests: F6DB.7
- State writes: E826
### CALL
- Emitted selector: `0x0015`
- Handler: `H'20A1`
- Edge source: `F006 -> F6DB` via `F6F3.3`
- Trigger bit: `F6DB.5`
- Table slot: `H'27C0` -> `H'20A1`
- Current-level tests: F6DB.5
- State writes: E82A
## Button Matrix Entries With Serial Reports
| Source | Shadow bit | Dirty | Handler | Selector(s) | State writes |
| --- | --- | --- | --- | --- | --- |
| `F105` | `F6D4.6` | `F6F2.4` | `H'2048` | `0x006B` | `E8D6` |
| `F105` | `F6D4.3` | `F6F2.4` | `H'1F40` | `0x0007` | `E80E` |
| `F105` | `F6D4.2` | `F6F2.4` | `H'1EDE` | `0x0017`, `0x0018` | `E82E`, `E830` |
| `F105` | `F6D4.1` | `F6F2.4` | `H'1EA9` | `0x00F8` | `E9F0` |
| `F105` | `F6D4.0` | `F6F2.4` | `H'1E20` | `0x00B7`, `0x00C4`, `0x00C6`, `0x0097` | `E96E`, `E988`, `E98C`, `E92E` |
| `F106` | `F6D3.7` | `F6F2.3` | `H'1DCE` | `0x0096` | `E92C` |
| `F106` | `F6D3.6` | `F6F2.3` | `H'1D87` | `0x0097` | `E92E` |
| `F106` | `F6D3.5` | `F6F2.3` | `H'1D56` | `0x001A` | `E834` |
| `F106` | `F6D3.4` | `F6F2.3` | `H'1D25` | `0x001A` | `E834` |
| `F106` | `F6D3.3` | `F6F2.3` | `H'1CF4` | `0x001A` | `E834` |
| `F106` | `F6D3.2` | `F6F2.3` | `H'1CCE` | `0x001A` | `E834` |
| `F106` | `F6D3.1` | `F6F2.3` | `H'1CB2` | `0x001A` | `E834` |
| `F109` | `F6D0.7` | `F6F2.0` | `H'24E8` | `0x008F` | `E91E` |
| `F109` | `F6D0.6` | `F6F2.0` | `H'252E` | `0x008F` | `E91E` |
| `F109` | `F6D0.3` | `F6F2.0` | `H'24A9` | `0x0083` | `E906` |
| `F109` | `F6D0.2` | `F6F2.0` | `H'2408` | `0x0083` | `E906` |
| `F109` | `F6D0.1` | `F6F2.0` | `H'2390` | `0x0083` | `E906` |
| `F005` | `F6DC.7` | `F6F3.4` | `H'20F1` | `0x00B9` | `E972` |
| `F005` | `F6DC.5` | `F6F3.4` | `H'2204` | `0x0093` | `E926` |
| `F005` | `F6DC.4` | `F6F3.4` | `H'226D` | `0x0093` | `E926` |
| `F005` | `F6DC.3` | `F6F3.4` | `H'22A6` | `0x0093` | `E926` |
| `F005` | `F6DC.1` | `F6F3.4` | `H'22FC` | `0x0093` | `E926` |
| `F005` | `F6DC.0` | `F6F3.4` | `H'2326` | `0x0093` | `E926` |
| `F006` | `F6DB.7` | `F6F3.3` | `H'200E` | `0x0013` | `E826` |
| `F006` | `F6DB.5` | `F6F3.3` | `H'20A1` | `0x0015` | `E82A` |
| `F006` | `F6DB.3` | `F6F3.3` | `H'20BE` | `0x009A` | `E934` |
## 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.

View File

@@ -0,0 +1,200 @@
; H8/536 ROM disassembly
; input: ROM\M27C512@DIP28_1.BIN
; bytes: 65536
; vector mode: min
; analysis: linear sweep
;
; Notes from the manual:
; - H8/536 uses the H8/500 CPU instruction set.
; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.
; - The register field is H'FE80-H'FFFF; names below come from appendix B.
; - @aa:8 short absolute operands use BR as the upper address byte.
; - SCI baud inference uses section 14.2.8 BRR formulas when SMR/BRR are known.
; - LCD inference treats E-clock H'F200/H'F201 accesses as status/control and data candidates.
; Memory Map
; H'0000-H'009F exception_vectors vectors
; H'00A0-H'00FF dtc_vectors dtc_vectors
; H'0100-H'F67F program_or_external program
; H'F680-H'FE7F on_chip_ram ram
; H'FE80-H'FFFF register_field registers
; Vectors
; H'0000 reset -> vec_reset_1000 (H'1000)
; H'0004 invalid_instruction -> vec_reset_1000 (H'1000)
; H'0006 zero_divide -> vec_reset_1000 (H'1000)
; H'0008 trap_vs -> vec_reset_1000 (H'1000)
; H'0010 address_error -> vec_reset_1000 (H'1000)
; H'0012 trace -> vec_reset_1000 (H'1000)
; H'0016 nmi -> vec_nmi_4393 (H'4393)
; H'0020 trapa_0 -> vec_reset_1000 (H'1000)
; H'0022 trapa_1 -> vec_reset_1000 (H'1000)
; H'0024 trapa_2 -> vec_reset_1000 (H'1000)
; H'0026 trapa_3 -> vec_reset_1000 (H'1000)
; H'0028 trapa_4 -> vec_reset_1000 (H'1000)
; H'002A trapa_5 -> vec_reset_1000 (H'1000)
; H'002C trapa_6 -> vec_reset_1000 (H'1000)
; H'002E trapa_7 -> vec_reset_1000 (H'1000)
; H'0030 trapa_8 -> vec_reset_1000 (H'1000)
; H'0032 trapa_9 -> vec_reset_1000 (H'1000)
; H'0034 trapa_a -> vec_reset_1000 (H'1000)
; H'0036 trapa_b -> vec_reset_1000 (H'1000)
; H'0038 trapa_c -> vec_reset_1000 (H'1000)
; H'003A trapa_d -> vec_reset_1000 (H'1000)
; H'003C trapa_e -> vec_reset_1000 (H'1000)
; H'003E trapa_f -> vec_reset_1000 (H'1000)
; H'0040 irq0 -> vec_reset_1000 (H'1000)
; H'0042 interval_timer -> vec_interval_timer_BFC4 (H'BFC4)
; H'0048 irq1 -> vec_reset_1000 (H'1000)
; H'0050 irq2 -> vec_reset_1000 (H'1000)
; H'0052 irq3 -> vec_irq3_3C30 (H'3C30)
; H'0058 irq4 -> vec_irq4_3AC7 (H'3AC7)
; H'005A irq5 -> vec_reset_1000 (H'1000)
; H'0062 frt1_ocia -> vec_frt1_ocia_BEEA (H'BEEA)
; H'006A frt2_ocia -> vec_frt2_ocia_BF23 (H'BF23)
; H'0080 sci1_eri -> vec_sci1_eri_BB57 (H'BB57)
; H'0082 sci1_rxi -> vec_sci1_rxi_BB67 (H'BB67)
; H'0084 sci1_txi -> vec_sci1_txi_BA84 (H'BA84)
; H'0090 ad_adi -> vec_ad_adi_3D99 (H'3D99)
; Symbols
; mem_E026 H'E026 program_or_external memory r=4 w=0 width=word
; mem_E02A H'E02A program_or_external memory r=1 w=0 width=word
; mem_E046 H'E046 program_or_external memory r=0 w=1 width=word
; mem_E110 H'E110 program_or_external memory r=1 w=0 width=word
; mem_E134 H'E134 program_or_external memory r=1 w=0 width=word
; mem_E824 H'E824 program_or_external memory r=0 w=1 width=word
; mem_E826 H'E826 program_or_external memory r=0 w=4 width=word
; mem_E82A H'E82A program_or_external memory r=0 w=1 width=word
; mem_E8D6 H'E8D6 program_or_external memory r=0 w=1 width=word
; ram_F6D4 H'F6D4 on_chip_ram ram r=1 w=0 width=byte
; ram_F6DB H'F6DB on_chip_ram ram r=3 w=0 width=byte
; ram_F713 H'F713 on_chip_ram ram r=2 w=2 width=byte
; ram_F726 H'F726 on_chip_ram ram r=0 w=1 width=byte
; ram_F730 H'F730 on_chip_ram ram r=1 w=0 width=byte
; ram_F731 H'F731 on_chip_ram ram r=5 w=2 width=byte
; ram_F732 H'F732 on_chip_ram ram r=0 w=1 width=word
; ram_F76A H'F76A on_chip_ram ram r=0 w=1 width=word
; ram_F76E H'F76E on_chip_ram ram r=1 w=1 width=byte
; ram_F791 H'F791 on_chip_ram ram r=2 w=1 width=byte
; ram_F798 H'F798 on_chip_ram ram r=0 w=2 width=byte
; ram_FB03 H'FB03 on_chip_ram ram r=1 w=1 width=byte
; Board Profile
; Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.
; H8 pin 66 P95/TXD (TXD) -> MAX202 pin 11
; H8 pin 67 P96/RXD (RXD) -> MAX202 pin 12
; SCI2 pin routing is disabled by SYSCR2.P9SCI2E=0 in the observed setup.
; LCD/Text Scan
; search 'CONNECT': not literal, hits=0
1FC0: F7 13 C5 1D ROTR.B @(H'13C5,R7)
1FC4: E8 24 07 80 00 MOV:G.W #H'8000, @(H'24,R0)
1FC9: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
1FCB: 5B 40 12 MOV:I.W #H'4012, R3 ; dataflow R3=H'4012
1FCE: 1E 1E 83 BSR loc_3E54
1FD1: 19 RTS
1FD2: 15 F7 91 D7 BCLR.B #7, @H'F791 ; refs ram_F791 in on_chip_ram
1FD6: 15 F7 13 D5 BCLR.B #5, @H'F713 ; refs ram_F713 in on_chip_ram
1FDA: 1D E8 24 06 00 MOV:G.W #H'00, @H'E824 ; refs mem_E824 in program_or_external
1FDF: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
1FE1: 5B 40 12 MOV:I.W #H'4012, R3 ; dataflow R3=H'4012
1FE4: 1E 1E 6D BSR loc_3E54
1FE7: 19 RTS
1FE8: 1D E0 26 80 MOV:G.W @H'E026, R0 ; refs mem_E026 in program_or_external
1FEC: A8 CF BSET.W #15, R0
1FEE: 1D E8 26 90 MOV:G.W R0, @H'E826 ; refs mem_E826 in program_or_external
1FF2: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
1FF4: 5B 00 13 MOV:I.W #H'0013, R3 ; dataflow R3=H'0013
1FF7: 1E 1E 5A BSR loc_3E54
1FFA: 19 RTS
1FFB: 1D E0 26 80 MOV:G.W @H'E026, R0 ; refs mem_E026 in program_or_external
1FFF: A8 DF BCLR.W #15, R0
2001: 1D E8 26 90 MOV:G.W R0, @H'E826 ; refs mem_E826 in program_or_external
2005: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
2007: 5B 00 13 MOV:I.W #H'0013, R3 ; dataflow R3=H'0013
200A: 1E 1E 47 BSR loc_3E54
200D: 19 RTS
200E: 15 F6 DB F7 BTST.B #7, @H'F6DB ; refs ram_F6DB in on_chip_ram
2012: 27 33 BEQ loc_2047
2014: 15 F7 31 04 03 CMP:G.B #H'03, @H'F731 ; refs ram_F731 in on_chip_ram
2019: 22 2C BHI loc_2047
201B: 15 F7 91 F5 BTST.B #5, @H'F791 ; refs ram_F791 in on_chip_ram
201F: 26 14 BNE loc_2035
2021: 1D E0 26 80 MOV:G.W @H'E026, R0 ; refs mem_E026 in program_or_external
2025: A8 CE BSET.W #14, R0
2027: 1D E8 26 90 MOV:G.W R0, @H'E826 ; refs mem_E826 in program_or_external
202B: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
202D: 5B 00 13 MOV:I.W #H'0013, R3 ; dataflow R3=H'0013
2030: 1E 1E 21 BSR loc_3E54
2033: 20 12 BRA loc_2047
loc_2035:
2035: 1D E0 26 80 MOV:G.W @H'E026, R0 ; refs mem_E026 in program_or_external
2039: A8 DE BCLR.W #14, R0
203B: 1D E8 26 90 MOV:G.W R0, @H'E826 ; refs mem_E826 in program_or_external
203F: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
2041: 5B 00 13 MOV:I.W #H'0013, R3 ; dataflow R3=H'0013
2044: 1E 1E 0D BSR loc_3E54
loc_2047:
2047: 19 RTS
2048: 15 F6 D4 F6 BTST.B #6, @H'F6D4 ; refs ram_F6D4 in on_chip_ram
204C: 27 52 BEQ loc_20A0
204E: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram
2053: 22 4B BHI loc_20A0
2055: 15 F7 30 F7 BTST.B #7, @H'F730 ; refs ram_F730 in on_chip_ram
2059: 27 19 BEQ loc_2074
205B: 1D E8 D6 07 80 00 MOV:G.W #H'8000, @H'E8D6 ; refs mem_E8D6 in program_or_external
2061: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
2063: 5B 00 6B MOV:I.W #H'006B, R3 ; dataflow R3=H'006B
2066: 1E 1D EB BSR loc_3E54
2069: 15 F7 31 C7 BSET.B #7, @H'F731 ; refs ram_F731 in on_chip_ram
206D: 15 F7 98 06 C8 MOV:G.B #H'C8, @H'F798 ; refs ram_F798 in on_chip_ram
2072: 20 2C BRA loc_20A0
loc_2074:
2074: 1D F7 32 13 CLR.W @H'F732 ; refs ram_F732 in on_chip_ram
2078: 15 FB 03 D7 BCLR.B #7, @H'FB03 ; refs ram_FB03 in on_chip_ram
207C: 1D E0 46 13 CLR.W @H'E046 ; refs mem_E046 in program_or_external
2080: 1D F7 6A 13 CLR.W @H'F76A ; refs ram_F76A in on_chip_ram
2084: 1E 28 73 BSR loc_48FA
2087: 15 F7 13 C6 BSET.B #6, @H'F713 ; refs ram_F713 in on_chip_ram
208B: 15 F7 26 06 1E MOV:G.B #H'1E, @H'F726 ; refs ram_F726 in on_chip_ram
2090: 15 F7 6E C6 BSET.B #6, @H'F76E ; refs ram_F76E in on_chip_ram
2094: 15 F7 31 C7 BSET.B #7, @H'F731 ; refs ram_F731 in on_chip_ram
2098: 15 F7 98 06 C8 MOV:G.B #H'C8, @H'F798 ; refs ram_F798 in on_chip_ram
209D: 1E 36 38 BSR loc_56D8
loc_20A0:
20A0: 19 RTS
20A1: 1D E0 2A 80 MOV:G.W @H'E02A, R0 ; refs mem_E02A in program_or_external
20A5: 15 F6 DB F5 BTST.B #5, @H'F6DB ; refs ram_F6DB in on_chip_ram
20A9: 27 04 BEQ loc_20AF
20AB: A8 CF BSET.W #15, R0
20AD: 20 02 BRA loc_20B1
loc_20AF:
20AF: A8 DF BCLR.W #15, R0
loc_20B1:
20B1: 1D E8 2A 90 MOV:G.W R0, @H'E82A ; refs mem_E82A in program_or_external
20B5: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
20B7: 5B 00 15 MOV:I.W #H'0015, R3 ; dataflow R3=H'0015
20BA: 1E 1D 97 BSR loc_3E54
20BD: 19 RTS
20BE: 15 F6 DB F3 BTST.B #3, @H'F6DB ; refs ram_F6DB in on_chip_ram
20C2: 27 2C BEQ loc_20F0
20C4: 15 F7 31 04 03 CMP:G.B #H'03, @H'F731 ; refs ram_F731 in on_chip_ram
20C9: 22 25 BHI loc_20F0
20CB: 1D E1 10 16 TST.W @H'E110 ; refs mem_E110 in program_or_external
20CF: 27 05 BEQ loc_20D6
20D1: 1E 06 14 BSR loc_26E8
20D4: 20 1A BRA loc_20F0
loc_20D6:
20D6: 1D E1 34 80 MOV:G.W @H'E134, R0 ; refs mem_E134 in program_or_external
20DA: A8 FB BTST.W #11, R0
20DC: 27 04 BEQ loc_20E2
20DE: A8 DB BCLR.W #11, R0

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,135 @@
; H8/536 ROM disassembly
; input: ROM\M27C512@DIP28_1.BIN
; bytes: 65536
; vector mode: min
; analysis: linear sweep
;
; Notes from the manual:
; - H8/536 uses the H8/500 CPU instruction set.
; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.
; - The register field is H'FE80-H'FFFF; names below come from appendix B.
; - @aa:8 short absolute operands use BR as the upper address byte.
; - SCI baud inference uses section 14.2.8 BRR formulas when SMR/BRR are known.
; - LCD inference treats E-clock H'F200/H'F201 accesses as status/control and data candidates.
; Memory Map
; H'0000-H'009F exception_vectors vectors
; H'00A0-H'00FF dtc_vectors dtc_vectors
; H'0100-H'F67F program_or_external program
; H'F680-H'FE7F on_chip_ram ram
; H'FE80-H'FFFF register_field registers
; Vectors
; H'0000 reset -> vec_reset_1000 (H'1000)
; H'0004 invalid_instruction -> vec_reset_1000 (H'1000)
; H'0006 zero_divide -> vec_reset_1000 (H'1000)
; H'0008 trap_vs -> vec_reset_1000 (H'1000)
; H'0010 address_error -> vec_reset_1000 (H'1000)
; H'0012 trace -> vec_reset_1000 (H'1000)
; H'0016 nmi -> vec_nmi_4393 (H'4393)
; H'0020 trapa_0 -> vec_reset_1000 (H'1000)
; H'0022 trapa_1 -> vec_reset_1000 (H'1000)
; H'0024 trapa_2 -> vec_reset_1000 (H'1000)
; H'0026 trapa_3 -> vec_reset_1000 (H'1000)
; H'0028 trapa_4 -> vec_reset_1000 (H'1000)
; H'002A trapa_5 -> vec_reset_1000 (H'1000)
; H'002C trapa_6 -> vec_reset_1000 (H'1000)
; H'002E trapa_7 -> vec_reset_1000 (H'1000)
; H'0030 trapa_8 -> vec_reset_1000 (H'1000)
; H'0032 trapa_9 -> vec_reset_1000 (H'1000)
; H'0034 trapa_a -> vec_reset_1000 (H'1000)
; H'0036 trapa_b -> vec_reset_1000 (H'1000)
; H'0038 trapa_c -> vec_reset_1000 (H'1000)
; H'003A trapa_d -> vec_reset_1000 (H'1000)
; H'003C trapa_e -> vec_reset_1000 (H'1000)
; H'003E trapa_f -> vec_reset_1000 (H'1000)
; H'0040 irq0 -> vec_reset_1000 (H'1000)
; H'0042 interval_timer -> vec_interval_timer_BFC4 (H'BFC4)
; H'0048 irq1 -> vec_reset_1000 (H'1000)
; H'0050 irq2 -> vec_reset_1000 (H'1000)
; H'0052 irq3 -> vec_irq3_3C30 (H'3C30)
; H'0058 irq4 -> vec_irq4_3AC7 (H'3AC7)
; H'005A irq5 -> vec_reset_1000 (H'1000)
; H'0062 frt1_ocia -> vec_frt1_ocia_BEEA (H'BEEA)
; H'006A frt2_ocia -> vec_frt2_ocia_BF23 (H'BF23)
; H'0080 sci1_eri -> vec_sci1_eri_BB57 (H'BB57)
; H'0082 sci1_rxi -> vec_sci1_rxi_BB67 (H'BB67)
; H'0084 sci1_txi -> vec_sci1_txi_BA84 (H'BA84)
; H'0090 ad_adi -> vec_ad_adi_3D99 (H'3D99)
; Symbols
; mem_E02E H'E02E program_or_external memory r=1 w=0 width=word
; mem_E030 H'E030 program_or_external memory r=1 w=0 width=word
; mem_E826 H'E826 program_or_external memory r=2 w=0 width=word
; mem_E82E H'E82E program_or_external memory r=0 w=1 width=word
; ram_F711 H'F711 on_chip_ram ram r=4 w=4 width=byte
; ram_F713 H'F713 on_chip_ram ram r=2 w=2 width=byte
; ram_F716 H'F716 on_chip_ram ram r=4 w=4 width=byte
; ram_F791 H'F791 on_chip_ram ram r=4 w=4 width=byte
; Board Profile
; Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.
; H8 pin 66 P95/TXD (TXD) -> MAX202 pin 11
; H8 pin 67 P96/RXD (RXD) -> MAX202 pin 12
; SCI2 pin routing is disabled by SYSCR2.P9SCI2E=0 in the observed setup.
; LCD/Text Scan
; search 'CONNECT': not literal, hits=0
2E00: 30 FE A3 BRA loc_2CA6
2E03: 30 FE A0 BRA loc_2CA6
2E06: 1D E8 26 FF BTST.W #15, @H'E826 ; refs mem_E826 in program_or_external
2E0A: 27 0A BEQ loc_2E16
2E0C: 15 F7 91 C6 BSET.B #6, @H'F791 ; refs ram_F791 in on_chip_ram
2E10: 15 F7 13 C4 BSET.B #4, @H'F713 ; refs ram_F713 in on_chip_ram
2E14: 20 08 BRA loc_2E1E
loc_2E16:
2E16: 15 F7 91 D6 BCLR.B #6, @H'F791 ; refs ram_F791 in on_chip_ram
2E1A: 15 F7 13 D4 BCLR.B #4, @H'F713 ; refs ram_F713 in on_chip_ram
loc_2E1E:
2E1E: 1D E8 26 FE BTST.W #14, @H'E826 ; refs mem_E826 in program_or_external
2E22: 27 0A BEQ loc_2E2E
2E24: 15 F7 91 C5 BSET.B #5, @H'F791 ; refs ram_F791 in on_chip_ram
2E28: 15 F7 16 C7 BSET.B #7, @H'F716 ; refs ram_F716 in on_chip_ram
2E2C: 20 08 BRA loc_2E36
loc_2E2E:
2E2E: 15 F7 91 D5 BCLR.B #5, @H'F791 ; refs ram_F791 in on_chip_ram
2E32: 15 F7 16 D7 BCLR.B #7, @H'F716 ; refs ram_F716 in on_chip_ram
loc_2E36:
2E36: 30 FE 6D BRA loc_2CA6
2E39: FC E0 00 81 MOV:G.W @(-H'2000,R4), R1
2E3D: A9 FF BTST.W #15, R1
2E3F: 26 0E BNE loc_2E4F
2E41: A9 FE BTST.W #14, R1
2E43: 26 0A BNE loc_2E4F
2E45: 15 F7 11 D0 BCLR.B #0, @H'F711 ; refs ram_F711 in on_chip_ram
2E49: 15 F7 16 D5 BCLR.B #5, @H'F716 ; refs ram_F716 in on_chip_ram
2E4D: 20 08 BRA loc_2E57
loc_2E4F:
2E4F: 15 F7 11 C0 BSET.B #0, @H'F711 ; refs ram_F711 in on_chip_ram
2E53: 15 F7 16 C5 BSET.B #5, @H'F716 ; refs ram_F716 in on_chip_ram
loc_2E57:
2E57: 30 FE 4C BRA loc_2CA6
2E5A: FC E0 00 81 MOV:G.W @(-H'2000,R4), R1
2E5E: A9 FF BTST.W #15, R1
2E60: 26 06 BNE loc_2E68
2E62: 15 F7 11 D1 BCLR.B #1, @H'F711 ; refs ram_F711 in on_chip_ram
2E66: 20 04 BRA loc_2E6C
loc_2E68:
2E68: 15 F7 11 C1 BSET.B #1, @H'F711 ; refs ram_F711 in on_chip_ram
loc_2E6C:
2E6C: 30 FE 37 BRA loc_2CA6
2E6F: 1D E0 30 81 MOV:G.W @H'E030, R1 ; refs mem_E030 in program_or_external
2E73: 1D E0 2E 71 CMP:G.W @H'E02E, R1 ; refs mem_E02E in program_or_external
2E77: 27 0C BEQ loc_2E85
2E79: 1D E8 2E 91 MOV:G.W R1, @H'E82E ; refs mem_E82E in program_or_external
2E7D: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80
2E7F: 5B 00 17 MOV:I.W #H'0017, R3 ; dataflow R3=H'0017

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
Table/Index Cross-Reference Report for build\rom_0013_handler_linear.json
=========================================================================
Static offsets are emitted only when an index register value can be derived from nearby immediate loads in the current JSON. Other indexed accesses are dynamic.
LCD correlation hints
term 'CONNECT': no LCD/text candidate hits in current decompile
term 'CONNECT: OK': no LCD/text candidate hits in current decompile
term 'CONNECT: NOT ACT': no LCD/text candidate hits in current decompile
term 'NOT ACT': no LCD/text candidate hits in current decompile
term 'COMM LINK': no LCD/text candidate hits in current decompile
term 'COMPLETED': no LCD/text candidate hits in current decompile
caveat: LCD strings can be builder/script output; absence of a literal term does not disprove runtime composition.
primary_value_table_candidate H'E000-H'E3FF (negative H'2000; direct H'F900-H'F91F)
accesses=4 reads=4 writes=0 dynamic=2
static offsets: H'002E, H'0030
functions: <no function>:4
- H'2E39 read index dynamic via R4 operand @(-H'2000,R4); <no function>; MOV:G.W @(-H'2000,R4), R1
- H'2E5A read index dynamic via R4 operand @(-H'2000,R4); <no function>; MOV:G.W @(-H'2000,R4), R1
- H'2E6F read offset H'0030 selector 0x018 -> H'E030; <no function>; MOV:G.W @H'E030, R1
- H'2E73 read offset H'002E selector 0x017 -> H'E02E; <no function>; CMP:G.W @H'E02E, R1
secondary_value_table_candidate H'E400-H'E7FF (negative H'1C00; direct H'F940-H'F95F)
accesses=0 reads=0 writes=0 dynamic=0
no references found in current JSON
current_value_table_candidate H'E800-H'EBFF (negative H'1800; direct H'F920-H'F93F)
accesses=3 reads=2 writes=1 dynamic=0
static offsets: H'0026, H'002E
functions: <no function>:3
- H'2E06 read offset H'0026 selector 0x013 -> H'E826; <no function>; BTST.W #15, @H'E826
- H'2E1E read offset H'0026 selector 0x013 -> H'E826; <no function>; BTST.W #14, @H'E826
- H'2E79 write offset H'002E selector 0x017 -> H'E82E; <no function>; MOV:G.W R1, @H'E82E
flag_table_candidate H'EC00-H'EFFF (negative H'1400; direct H'F980-H'F99F)
accesses=0 reads=0 writes=0 dynamic=0
no references found in current JSON

View File

@@ -793,6 +793,108 @@
]
},
"selector_candidates": [
{
"accesses": [
{
"access": "read",
"address_hex": "H'17D0",
"function": "loc_17C9",
"instruction": "BTST.W #12, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'1802",
"function": "loc_17FB",
"instruction": "BTST.W #12, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'183A",
"function": "loc_182D",
"instruction": "BTST.W #5, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'189E",
"function": "loc_1891",
"instruction": "BTST.W #5, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'18F4",
"function": "loc_18E7",
"instruction": "BTST.W #5, @H'E126",
"table": "primary_value_table_candidate"
}
],
"cmd1_read_frame": "01 01 13 00 00 49",
"name": "white_balance_black_flare_mode_lane",
"reasons": [
"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",
"primary_value_table_candidate read in loc_18E7: BTST.W #5, @H'E126",
"Bench-visible white-balance and black/flare lamp lane."
],
"score": 19,
"seed_frames": [
{
"cmd0_frame": "00 01 13 80 00 C8",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 01 13 40 00 08",
"value": 16384,
"value_hex": "0x4000"
},
{
"cmd0_frame": "00 01 13 20 00 68",
"value": 8192,
"value_hex": "0x2000"
},
{
"cmd0_frame": "00 01 13 10 20 78",
"value": 4128,
"value_hex": "0x1020"
},
{
"cmd0_frame": "00 01 13 40 40 48",
"value": 16448,
"value_hex": "0x4040"
},
{
"cmd0_frame": "00 01 13 80 40 88",
"value": 32832,
"value_hex": "0x8040"
},
{
"cmd0_frame": "00 01 13 00 20 68",
"value": 32,
"value_hex": "0x0020"
},
{
"cmd0_frame": "00 01 13 00 40 08",
"value": 64,
"value_hex": "0x0040"
},
{
"cmd0_frame": "00 01 13 00 00 48",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 147,
"selector_hex": "0x093",
"tables": [
"primary_value_table_candidate"
]
},
{
"accesses": [
{
@@ -859,61 +961,6 @@
"flag_table_candidate"
]
},
{
"accesses": [
{
"access": "read",
"address_hex": "H'17D0",
"function": "loc_17C9",
"instruction": "BTST.W #12, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'1802",
"function": "loc_17FB",
"instruction": "BTST.W #12, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'183A",
"function": "loc_182D",
"instruction": "BTST.W #5, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'189E",
"function": "loc_1891",
"instruction": "BTST.W #5, @H'E126",
"table": "primary_value_table_candidate"
},
{
"access": "read",
"address_hex": "H'18F4",
"function": "loc_18E7",
"instruction": "BTST.W #5, @H'E126",
"table": "primary_value_table_candidate"
}
],
"cmd1_read_frame": "01 01 13 00 00 49",
"name": "state_selector_candidate",
"reasons": [
"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",
"primary_value_table_candidate read in loc_18E7: BTST.W #5, @H'E126"
],
"score": 15,
"seed_frames": [],
"selector": 147,
"selector_hex": "0x093",
"tables": [
"primary_value_table_candidate"
]
},
{
"accesses": [
{
@@ -1044,6 +1091,75 @@
"current_value_table_candidate"
]
},
{
"accesses": [],
"cmd1_read_frame": "01 00 6B 00 00 30",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 107,
"dispatch_index_hex": "0x06B",
"entry_address_hex": "H'297C",
"selector": 107,
"selector_hex": "0x06B",
"target": 12146,
"target_hex": "H'2F72",
"target_label_or_hex": "H'2F72"
},
"name": "standard_lamp_lane",
"reasons": [
"when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
"Bench-visible STANDARD lamp lane found from ROM-derived F6D4.6 handler candidate.",
"selector dispatches to H'2F72"
],
"score": 11,
"seed_frames": [
{
"cmd0_frame": "00 00 6B 80 00 B1",
"value": 32768,
"value_hex": "0x8000"
}
],
"selector": 107,
"selector_hex": "0x06B",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 15 00 00 4E",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 21,
"dispatch_index_hex": "0x015",
"entry_address_hex": "H'28D0",
"selector": 21,
"selector_hex": "0x015",
"target": 11833,
"target_hex": "H'2E39",
"target_label_or_hex": "H'2E39"
},
"name": "call_and_red_tally_lamp_lane",
"reasons": [
"observed RCP autonomous report frame(s): 00 00 15 80 00 CF, 00 00 15 00 00 4F",
"Bench-visible CALL lamp and red tally lane; local CALL handler mirrors F6DB.5 into E800[0x0015].15.",
"selector dispatches to H'2E39"
],
"score": 9,
"seed_frames": [
{
"cmd0_frame": "00 00 15 80 00 CF",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 00 15 00 00 4F",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 21,
"selector_hex": "0x015",
"tables": []
},
{
"accesses": [
{
@@ -1124,31 +1240,6 @@
"current_value_table_candidate"
]
},
{
"accesses": [],
"cmd1_read_frame": "01 00 6B 00 00 30",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 107,
"dispatch_index_hex": "0x06B",
"entry_address_hex": "H'297C",
"selector": 107,
"selector_hex": "0x06B",
"target": 12146,
"target_hex": "H'2F72",
"target_label_or_hex": "H'2F72"
},
"name": "connection_latch_clear_candidate",
"reasons": [
"when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
"selector dispatches to H'2F72"
],
"score": 7,
"seed_frames": [],
"selector": 107,
"selector_hex": "0x06B",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 6C 00 00 37",
@@ -1199,6 +1290,202 @@
"selector_hex": "0x06D",
"tables": []
},
{
"accesses": [
{
"access": "read",
"address_hex": "H'17A7",
"function": "loc_1795",
"instruction": "BTST.W #15, @H'E220",
"table": "primary_value_table_candidate"
}
],
"cmd1_read_frame": "01 01 90 00 00 CA",
"name": "knee_auto_lamp_or_page_status_lane",
"reasons": [
"primary_value_table_candidate read in loc_1795: BTST.W #15, @H'E220",
"Bench-visible KNEE AUTO source; ROM notes indicate timed KNEE/detail page interaction."
],
"score": 7,
"seed_frames": [
{
"cmd0_frame": "00 01 90 80 00 4B",
"value": 32768,
"value_hex": "0x8000"
}
],
"selector": 272,
"selector_hex": "0x110",
"tables": [
"primary_value_table_candidate"
]
},
{
"accesses": [],
"cmd1_read_frame": "01 00 13 00 00 48",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 19,
"dispatch_index_hex": "0x013",
"entry_address_hex": "H'28CC",
"selector": 19,
"selector_hex": "0x013",
"target": 11782,
"target_hex": "H'2E06",
"target_label_or_hex": "H'2E06"
},
"name": "slave_and_iris_mblack_link_lamps",
"reasons": [
"Selector 0x0013 is a two-bit lamp/status word. ROM dispatch H'2E06 reads current table word H'E826 and fans bit 15 and bit 14 into panel latch RAM.",
"0x8000 SLAVE lamp: sets F791.6 and F713.4",
"0x4000 IRIS/M.BLACK LINK lamp: sets F791.5 and F716.7",
"selector dispatches to H'2E06"
],
"score": 6,
"seed_frames": [
{
"cmd0_frame": "00 00 13 80 00 C9",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 00 13 40 00 09",
"value": 16384,
"value_hex": "0x4000"
},
{
"cmd0_frame": "00 00 13 00 00 49",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 19,
"selector_hex": "0x013",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 17 00 00 4C",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 23,
"dispatch_index_hex": "0x017",
"entry_address_hex": "H'28D4",
"selector": 23,
"selector_hex": "0x017",
"target": 11909,
"target_hex": "H'2E85",
"target_label_or_hex": "H'2E85"
},
"name": "bars_lamp_lane",
"reasons": [
"Bench-visible BARS lamp/latch lane; low writes do not reliably clear the visible latch.",
"selector dispatches to H'2E85"
],
"score": 6,
"seed_frames": [
{
"cmd0_frame": "00 00 17 80 00 CD",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 00 17 40 00 0D",
"value": 16384,
"value_hex": "0x4000"
},
{
"cmd0_frame": "00 00 17 00 00 4D",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 23,
"selector_hex": "0x017",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 1A 00 00 41",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 26,
"dispatch_index_hex": "0x01A",
"entry_address_hex": "H'28DA",
"selector": 26,
"selector_hex": "0x01A",
"target": 11972,
"target_hex": "H'2EC4",
"target_label_or_hex": "H'2EC4"
},
"name": "monitor_selector_lamps",
"reasons": [
"Bench-visible MONITOR selector cluster found from ROM-derived button-output sweep.",
"selector dispatches to H'2EC4"
],
"score": 6,
"seed_frames": [
{
"cmd0_frame": "00 00 1A 08 08 40",
"value": 2056,
"value_hex": "0x0808"
},
{
"cmd0_frame": "00 00 1A 20 20 40",
"value": 8224,
"value_hex": "0x2020"
},
{
"cmd0_frame": "00 00 1A 40 40 40",
"value": 16448,
"value_hex": "0x4040"
},
{
"cmd0_frame": "00 00 1A 80 80 40",
"value": 32896,
"value_hex": "0x8080"
}
],
"selector": 26,
"selector_hex": "0x01A",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 24 00 00 7F",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 36,
"dispatch_index_hex": "0x024",
"entry_address_hex": "H'28EE",
"selector": 36,
"selector_hex": "0x024",
"target": 12044,
"target_hex": "H'2F0C",
"target_label_or_hex": "H'2F0C"
},
"name": "lcd_selector_button_lamp",
"reasons": [
"Bench-visible LCD selector-button lamp lane.",
"selector dispatches to H'2F0C"
],
"score": 6,
"seed_frames": [
{
"cmd0_frame": "00 00 24 80 00 FE",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 00 24 00 00 7E",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 36,
"selector_hex": "0x024",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 07 00 00 5C",
@@ -1224,31 +1511,6 @@
"selector_hex": "0x007",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 15 00 00 4E",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 21,
"dispatch_index_hex": "0x015",
"entry_address_hex": "H'28D0",
"selector": 21,
"selector_hex": "0x015",
"target": 11833,
"target_hex": "H'2E39",
"target_label_or_hex": "H'2E39"
},
"name": "call_button_report_candidate",
"reasons": [
"observed RCP autonomous report frame(s): 00 00 15 80 00 CF, 00 00 15 00 00 4F",
"selector dispatches to H'2E39"
],
"score": 5,
"seed_frames": [],
"selector": 21,
"selector_hex": "0x015",
"tables": []
},
{
"accesses": [
{
@@ -1349,6 +1611,108 @@
"selector_hex": "0x0F8",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 01 02 00 00 58",
"name": "iris_readout_lane",
"reasons": [
"Bench-visible IRIS seven-segment/display lane."
],
"score": 4,
"seed_frames": [
{
"cmd0_frame": "00 01 02 80 00 D9",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 01 02 40 00 19",
"value": 16384,
"value_hex": "0x4000"
},
{
"cmd0_frame": "00 01 02 00 00 59",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 130,
"selector_hex": "0x082",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 01 03 00 00 59",
"name": "combined_iris_shutter_master_gain_status_lane",
"reasons": [
"Bench-visible combined status/readout lane; clear behavior appears latched or copied elsewhere."
],
"score": 4,
"seed_frames": [
{
"cmd0_frame": "00 01 03 80 00 D8",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 01 03 40 00 18",
"value": 16384,
"value_hex": "0x4000"
},
{
"cmd0_frame": "00 01 03 20 00 78",
"value": 8192,
"value_hex": "0x2000"
},
{
"cmd0_frame": "00 01 03 00 04 5C",
"value": 4,
"value_hex": "0x0004"
},
{
"cmd0_frame": "00 01 03 00 00 58",
"value": 0,
"value_hex": "0x0000"
}
],
"selector": 131,
"selector_hex": "0x083",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 01 0F 00 00 55",
"name": "shutter_display_status_lane",
"reasons": [
"Bench-visible shutter/status display lane; local F6D0.6/F6D0.7 handlers also queue this selector."
],
"score": 4,
"seed_frames": [
{
"cmd0_frame": "00 01 0F 80 00 D4",
"value": 32768,
"value_hex": "0x8000"
},
{
"cmd0_frame": "00 01 0F 20 00 74",
"value": 8192,
"value_hex": "0x2000"
},
{
"cmd0_frame": "00 01 0F 08 00 5C",
"value": 2048,
"value_hex": "0x0800"
},
{
"cmd0_frame": "00 01 0F 10 00 44",
"value": 4096,
"value_hex": "0x1000"
}
],
"selector": 143,
"selector_hex": "0x08F",
"tables": []
},
{
"accesses": [
{
@@ -1441,29 +1805,6 @@
"primary_value_table_candidate"
]
},
{
"accesses": [
{
"access": "read",
"address_hex": "H'17A7",
"function": "loc_1795",
"instruction": "BTST.W #15, @H'E220",
"table": "primary_value_table_candidate"
}
],
"cmd1_read_frame": "01 01 90 00 00 CA",
"name": "state_selector_candidate",
"reasons": [
"primary_value_table_candidate read in loc_1795: BTST.W #15, @H'E220"
],
"score": 3,
"seed_frames": [],
"selector": 272,
"selector_hex": "0x110",
"tables": [
"primary_value_table_candidate"
]
},
{
"accesses": [],
"cmd1_read_frame": "01 00 12 00 00 49",
@@ -1488,30 +1829,6 @@
"selector_hex": "0x012",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 13 00 00 48",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 19,
"dispatch_index_hex": "0x013",
"entry_address_hex": "H'28CC",
"selector": 19,
"selector_hex": "0x013",
"target": 11782,
"target_hex": "H'2E06",
"target_label_or_hex": "H'2E06"
},
"name": "state_selector_candidate",
"reasons": [
"selector dispatches to H'2E06"
],
"score": 2,
"seed_frames": [],
"selector": 19,
"selector_hex": "0x013",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 16 00 00 4D",
@@ -1536,30 +1853,6 @@
"selector_hex": "0x016",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 17 00 00 4C",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 23,
"dispatch_index_hex": "0x017",
"entry_address_hex": "H'28D4",
"selector": 23,
"selector_hex": "0x017",
"target": 11909,
"target_hex": "H'2E85",
"target_label_or_hex": "H'2E85"
},
"name": "state_selector_candidate",
"reasons": [
"selector dispatches to H'2E85"
],
"score": 2,
"seed_frames": [],
"selector": 23,
"selector_hex": "0x017",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 18 00 00 43",
@@ -1584,54 +1877,6 @@
"selector_hex": "0x018",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 1A 00 00 41",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 26,
"dispatch_index_hex": "0x01A",
"entry_address_hex": "H'28DA",
"selector": 26,
"selector_hex": "0x01A",
"target": 11972,
"target_hex": "H'2EC4",
"target_label_or_hex": "H'2EC4"
},
"name": "state_selector_candidate",
"reasons": [
"selector dispatches to H'2EC4"
],
"score": 2,
"seed_frames": [],
"selector": 26,
"selector_hex": "0x01A",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 24 00 00 7F",
"dispatch_target": {
"decoded_code": false,
"dispatch_index": 36,
"dispatch_index_hex": "0x024",
"entry_address_hex": "H'28EE",
"selector": 36,
"selector_hex": "0x024",
"target": 12044,
"target_hex": "H'2F0C",
"target_label_or_hex": "H'2F0C"
},
"name": "state_selector_candidate",
"reasons": [
"selector dispatches to H'2F0C"
],
"score": 2,
"seed_frames": [],
"selector": 36,
"selector_hex": "0x024",
"tables": []
},
{
"accesses": [],
"cmd1_read_frame": "01 00 25 00 00 7E",
@@ -1898,7 +2143,7 @@
}
],
"summary": {
"candidate_count": 41,
"candidate_count": 44,
"confidence": "medium",
"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."
},

View File

@@ -10,6 +10,13 @@ Table Model:
- flag_table_candidate: H'EC00-H'EFFF; accesses=6 static selectors=0x000
Highest-Value Selector Candidates:
- 0x093 white_balance_black_flare_mode_lane: score=19 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
seed frames: 0x8000 -> 00 01 13 80 00 C8; 0x4000 -> 00 01 13 40 00 08; 0x2000 -> 00 01 13 20 00 68
readback frame: 01 01 13 00 00 49
- 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
@@ -17,12 +24,6 @@ Highest-Value Selector Candidates:
- 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
@@ -42,6 +43,18 @@ Highest-Value Selector Candidates:
- 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
- 0x06B standard_lamp_lane: score=11 tables=none
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
- Bench-visible STANDARD lamp lane found from ROM-derived F6D4.6 handler candidate.
- selector dispatches to H'2F72
seed frames: 0x8000 -> 00 00 6B 80 00 B1
readback frame: 01 00 6B 00 00 30
- 0x015 call_and_red_tally_lamp_lane: score=9 tables=none
- observed RCP autonomous report frame(s): 00 00 15 80 00 CF, 00 00 15 00 00 4F
- Bench-visible CALL lamp and red tally lane; local CALL handler mirrors F6DB.5 into E800[0x0015].15.
- selector dispatches to H'2E39
seed frames: 0x8000 -> 00 00 15 80 00 CF; 0x0000 -> 00 00 15 00 00 4F
readback frame: 01 00 15 00 00 4E
- 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
@@ -52,10 +65,6 @@ Highest-Value Selector Candidates:
- 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
@@ -64,14 +73,37 @@ Highest-Value Selector Candidates:
- continuation command 5 calls BE70 for selector 0x006D
- selector dispatches to H'3015
readback frame: 01 00 6D 00 00 36
- 0x110 knee_auto_lamp_or_page_status_lane: score=7 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_1795: BTST.W #15, @H'E220
- Bench-visible KNEE AUTO source; ROM notes indicate timed KNEE/detail page interaction.
seed frames: 0x8000 -> 00 01 90 80 00 4B
readback frame: 01 01 90 00 00 CA
- 0x013 slave_and_iris_mblack_link_lamps: score=6 tables=none
- Selector 0x0013 is a two-bit lamp/status word. ROM dispatch H'2E06 reads current table word H'E826 and fans bit 15 and bit 14 into panel latch RAM.
- 0x8000 SLAVE lamp: sets F791.6 and F713.4
- 0x4000 IRIS/M.BLACK LINK lamp: sets F791.5 and F716.7
- selector dispatches to H'2E06
seed frames: 0x8000 -> 00 00 13 80 00 C9; 0x4000 -> 00 00 13 40 00 09; 0x0000 -> 00 00 13 00 00 49
readback frame: 01 00 13 00 00 48
- 0x017 bars_lamp_lane: score=6 tables=none
- Bench-visible BARS lamp/latch lane; low writes do not reliably clear the visible latch.
- selector dispatches to H'2E85
seed frames: 0x8000 -> 00 00 17 80 00 CD; 0x4000 -> 00 00 17 40 00 0D; 0x0000 -> 00 00 17 00 00 4D
readback frame: 01 00 17 00 00 4C
- 0x01A monitor_selector_lamps: score=6 tables=none
- Bench-visible MONITOR selector cluster found from ROM-derived button-output sweep.
- selector dispatches to H'2EC4
seed frames: 0x0808 -> 00 00 1A 08 08 40; 0x2020 -> 00 00 1A 20 20 40; 0x4040 -> 00 00 1A 40 40 40
readback frame: 01 00 1A 00 00 41
- 0x024 lcd_selector_button_lamp: score=6 tables=none
- Bench-visible LCD selector-button lamp lane.
- selector dispatches to H'2F0C
seed frames: 0x8000 -> 00 00 24 80 00 FE; 0x0000 -> 00 00 24 00 00 7E
readback frame: 01 00 24 00 00 7F
- 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
@@ -91,24 +123,10 @@ Highest-Value Selector Candidates:
- 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
- 0x082 iris_readout_lane: score=4 tables=none
- Bench-visible IRIS seven-segment/display lane.
seed frames: 0x8000 -> 00 01 02 80 00 D9; 0x4000 -> 00 01 02 40 00 19; 0x0000 -> 00 01 02 00 00 59
readback frame: 01 01 02 00 00 58
Display Text Hints:
- CONNECT: 0 hit(s)

433
build/rom_f109_handlers.asm Normal file
View File

@@ -0,0 +1,433 @@
; H8/536 ROM disassembly
; input: ROM\M27C512@DIP28_1.BIN
; bytes: 65536
; vector mode: min
; analysis: recursive trace from vectors
;
; Notes from the manual:
; - H8/536 uses the H8/500 CPU instruction set.
; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.
; - The register field is H'FE80-H'FFFF; names below come from appendix B.
; - @aa:8 short absolute operands use BR as the upper address byte.
; - SCI baud inference uses section 14.2.8 BRR formulas when SMR/BRR are known.
; - LCD inference treats E-clock H'F200/H'F201 accesses as status/control and data candidates.
; - Pass --clock-hz to convert SCI BRR settings into numeric baud rates.
; - Cycle counts use Appendix A tables A-7/A-8 for on-chip access with no external wait states.
; Memory Map
; H'0000-H'009F exception_vectors vectors
; H'00A0-H'00FF dtc_vectors dtc_vectors
; H'0100-H'F67F program_or_external program
; H'F680-H'FE7F on_chip_ram ram
; H'FE80-H'FFFF register_field registers
; Vectors
; H'0000 reset -> vec_reset_1000 (H'1000)
; H'0004 invalid_instruction -> vec_reset_1000 (H'1000)
; H'0006 zero_divide -> vec_reset_1000 (H'1000)
; H'0008 trap_vs -> vec_reset_1000 (H'1000)
; H'0010 address_error -> vec_reset_1000 (H'1000)
; H'0012 trace -> vec_reset_1000 (H'1000)
; H'0016 nmi -> vec_nmi_4393 (H'4393)
; H'0020 trapa_0 -> vec_reset_1000 (H'1000)
; H'0022 trapa_1 -> vec_reset_1000 (H'1000)
; H'0024 trapa_2 -> vec_reset_1000 (H'1000)
; H'0026 trapa_3 -> vec_reset_1000 (H'1000)
; H'0028 trapa_4 -> vec_reset_1000 (H'1000)
; H'002A trapa_5 -> vec_reset_1000 (H'1000)
; H'002C trapa_6 -> vec_reset_1000 (H'1000)
; H'002E trapa_7 -> vec_reset_1000 (H'1000)
; H'0030 trapa_8 -> vec_reset_1000 (H'1000)
; H'0032 trapa_9 -> vec_reset_1000 (H'1000)
; H'0034 trapa_a -> vec_reset_1000 (H'1000)
; H'0036 trapa_b -> vec_reset_1000 (H'1000)
; H'0038 trapa_c -> vec_reset_1000 (H'1000)
; H'003A trapa_d -> vec_reset_1000 (H'1000)
; H'003C trapa_e -> vec_reset_1000 (H'1000)
; H'003E trapa_f -> vec_reset_1000 (H'1000)
; H'0040 irq0 -> vec_reset_1000 (H'1000)
; H'0042 interval_timer -> vec_interval_timer_BFC4 (H'BFC4)
; H'0048 irq1 -> vec_reset_1000 (H'1000)
; H'0050 irq2 -> vec_reset_1000 (H'1000)
; H'0052 irq3 -> vec_irq3_3C30 (H'3C30)
; H'0058 irq4 -> vec_irq4_3AC7 (H'3AC7)
; H'005A irq5 -> vec_reset_1000 (H'1000)
; H'0062 frt1_ocia -> vec_frt1_ocia_BEEA (H'BEEA)
; H'006A frt2_ocia -> vec_frt2_ocia_BF23 (H'BF23)
; H'0080 sci1_eri -> vec_sci1_eri_BB57 (H'BB57)
; H'0082 sci1_rxi -> vec_sci1_rxi_BB67 (H'BB67)
; H'0084 sci1_txi -> vec_sci1_txi_BA84 (H'BA84)
; H'0090 ad_adi -> vec_ad_adi_3D99 (H'3D99)
; Symbols
; mem_E106 H'E106 program_or_external memory r=5 w=2 width=word
; mem_E110 H'E110 program_or_external memory r=7 w=0 width=word
; mem_E11E H'E11E program_or_external memory r=4 w=0 width=word
; mem_E506 H'E506 program_or_external memory r=2 w=0 width=word
; mem_E906 H'E906 program_or_external memory r=0 w=4 width=word
; mem_E91E H'E91E program_or_external memory r=0 w=2 width=word
; mem_E922 H'E922 program_or_external memory r=0 w=1 width=word
; mem_F404 H'F404 program_or_external memory r=11 w=0 width=byte
; ram_F6D0 H'F6D0 on_chip_ram ram r=13 w=0 width=byte
; ram_F6F4 H'F6F4 on_chip_ram ram r=0 w=2 width=word
; ram_F6F6 H'F6F6 on_chip_ram ram r=0 w=4 width=byte
; ram_F730 H'F730 on_chip_ram ram r=4 w=0 width=byte
; ram_F731 H'F731 on_chip_ram ram r=7 w=0 width=byte
; ram_F791 H'F791 on_chip_ram ram r=11 w=0 width=byte
; Board Profile
; Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.
; H8 pin 66 P95/TXD (TXD) -> MAX202 pin 11
; H8 pin 67 P96/RXD (RXD) -> MAX202 pin 12
; SCI2 pin routing is disabled by SYSCR2.P9SCI2E=0 in the observed setup.
; LCD/Text Scan
; search 'CONNECT': not literal, hits=0
; LCD text candidates
; ... 1 more LCD text candidates
2390: 15 F6 D0 F1 BTST.B #1, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
2394: 37 00 70 BEQ loc_2407 ; cycles=3/7 nt/t
2397: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=6
239C: 32 00 68 BHI loc_2407 ; cycles=3/7 nt/t
239F: 1D E1 10 FF BTST.W #15, @H'E110 ; refs mem_E110 in program_or_external; cycles=6
23A3: 27 05 BEQ loc_23AA ; cycles=3/8 nt/t
23A5: 1E 03 40 BSR loc_26E8 ; cycles=14
23A8: 20 5D BRA loc_2407 ; cycles=7
loc_23AA:
23AA: 15 F6 D0 F2 BTST.B #2, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
23AE: 36 00 CF BNE loc_2480 ; cycles=3/7 nt/t
23B1: 15 F7 30 F7 BTST.B #7, @H'F730 ; refs ram_F730 in on_chip_ram; cycles=6
23B5: 27 35 BEQ loc_23EC ; cycles=3/8 nt/t
23B7: 1D E1 06 80 MOV:G.W @H'E106, R0 ; refs mem_E106 in program_or_external; cycles=6
loc_23BB:
23BB: A8 1B SHLR.W R0 ; cycles=3
loc_23BD:
23BD: A8 81 MOV:G.W R0, R1 ; cycles=3
23BF: 27 26 BEQ loc_23E7 ; cycles=3/8 nt/t
23C1: 1D E5 06 51 AND.W @H'E506, R1 ; refs mem_E506 in program_or_external; cycles=6
23C5: 0C FF C4 51 AND.W #H'FFC4, R1 ; cycles=4
23C9: 27 F0 BEQ loc_23BB ; cycles=3/8 nt/t
23CB: 1D E9 06 91 MOV:G.W R1, @H'E906 ; refs mem_E906 in program_or_external; cycles=6
23CF: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
23D1: 5B 00 83 MOV:I.W #H'0083, R3 ; dataflow R3=H'0083; cycles=3
23D4: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
23D8: 27 08 BEQ loc_23E2 ; cycles=3/7 nt/t
23DA: 15 F4 04 F5 BTST.B #5, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
23DE: 27 02 BEQ loc_23E2 ; cycles=3/7 nt/t
23E0: AB CE BSET.W #14, R3 ; cycles=3
loc_23E2:
23E2: 1E 1A 6F BSR loc_3E54 ; cycles=13
23E5: 20 20 BRA loc_2407 ; cycles=8
loc_23E7:
23E7: 58 00 04 MOV:I.W #H'0004, R0 ; dataflow R0=H'0004; cycles=3
23EA: 20 48 BRA loc_2434 ; cycles=7
loc_23EC:
23EC: 1D E1 06 D0 BCLR.W #0, @H'E106 ; refs mem_E106 in program_or_external; cycles=9
23F0: 5C 00 00 MOV:I.W #H'0000, R4 ; dataflow R4=H'0000; cycles=3
23F3: 5B 00 83 MOV:I.W #H'0083, R3 ; dataflow R3=H'0083; cycles=3
23F6: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
23FA: 27 08 BEQ loc_2404 ; cycles=3/7 nt/t
23FC: 15 F4 04 F5 BTST.B #5, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
2400: 27 02 BEQ loc_2404 ; cycles=3/7 nt/t
2402: AB CE BSET.W #14, R3 ; cycles=3
loc_2404:
2404: 1E F6 2E BSR loc_1A35 ; cycles=13
loc_2407:
2407: 19 RTS ; cycles=13
2408: 15 F6 D0 F2 BTST.B #2, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
240C: 37 00 70 BEQ loc_247F ; cycles=3/7 nt/t
240F: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=6
2414: 32 00 68 BHI loc_247F ; cycles=3/7 nt/t
2417: 1D E1 10 FF BTST.W #15, @H'E110 ; refs mem_E110 in program_or_external; cycles=6
241B: 27 05 BEQ loc_2422 ; cycles=3/8 nt/t
241D: 1E 02 C8 BSR loc_26E8 ; cycles=14
2420: 20 5D BRA loc_247F ; cycles=7
loc_2422:
2422: 15 F6 D0 F1 BTST.B #1, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
2426: 26 58 BNE loc_2480 ; cycles=3/7 nt/t
2428: 15 F7 30 F7 BTST.B #7, @H'F730 ; refs ram_F730 in on_chip_ram; cycles=7
242C: 27 36 BEQ loc_2464 ; cycles=3/7 nt/t
242E: 1D E1 06 80 MOV:G.W @H'E106, R0 ; refs mem_E106 in program_or_external; cycles=7
loc_2432:
2432: A8 1A SHLL.W R0 ; cycles=3
loc_2434:
2434: A8 81 MOV:G.W R0, R1 ; cycles=3
2436: 27 26 BEQ loc_245E ; cycles=3/7 nt/t
2438: 1D E5 06 51 AND.W @H'E506, R1 ; refs mem_E506 in program_or_external; cycles=7
243C: 0C FF C4 51 AND.W #H'FFC4, R1 ; cycles=4
2440: 27 F0 BEQ loc_2432 ; cycles=3/7 nt/t
2442: 1D E9 06 91 MOV:G.W R1, @H'E906 ; refs mem_E906 in program_or_external; cycles=7
2446: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
2448: 5B 00 83 MOV:I.W #H'0083, R3 ; dataflow R3=H'0083; cycles=3
244B: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=6
244F: 27 08 BEQ loc_2459 ; cycles=3/8 nt/t
2451: 15 F4 04 F5 BTST.B #5, @H'F404 ; refs mem_F404 in program_or_external; cycles=6
2455: 27 02 BEQ loc_2459 ; cycles=3/8 nt/t
2457: AB CE BSET.W #14, R3 ; cycles=3
loc_2459:
2459: 1E 19 F8 BSR loc_3E54 ; cycles=14
245C: 20 21 BRA loc_247F ; cycles=7
loc_245E:
245E: 58 80 00 MOV:I.W #H'8000, R0 ; dataflow R0=H'8000; cycles=3
2461: 30 FF 59 BRA loc_23BD ; cycles=8
loc_2464:
2464: 1D E1 06 D0 BCLR.W #0, @H'E106 ; refs mem_E106 in program_or_external; cycles=9
2468: 5C 00 01 MOV:I.W #H'0001, R4 ; dataflow R4=H'0001; cycles=3
246B: 5B 00 83 MOV:I.W #H'0083, R3 ; dataflow R3=H'0083; cycles=3
246E: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
2472: 27 08 BEQ loc_247C ; cycles=3/7 nt/t
2474: 15 F4 04 F5 BTST.B #5, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
2478: 27 02 BEQ loc_247C ; cycles=3/7 nt/t
247A: AB CE BSET.W #14, R3 ; cycles=3
loc_247C:
247C: 1E F5 B6 BSR loc_1A35 ; cycles=13
loc_247F:
247F: 19 RTS ; cycles=13
loc_2480:
2480: 15 F7 30 F7 BTST.B #7, @H'F730 ; refs ram_F730 in on_chip_ram; cycles=7
2484: 27 05 BEQ loc_248B ; cycles=3/7 nt/t
2486: 58 40 00 MOV:I.W #H'4000, R0 ; dataflow R0=H'4000; cycles=3
2489: 20 03 BRA loc_248E ; cycles=8
loc_248B:
248B: 58 00 20 MOV:I.W #H'0020, R0 ; dataflow R0=H'0020; cycles=3
loc_248E:
248E: 1D E9 06 90 MOV:G.W R0, @H'E906 ; refs mem_E906 in program_or_external; cycles=7
2492: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
2494: 5B 00 83 MOV:I.W #H'0083, R3 ; dataflow R3=H'0083; cycles=3
2497: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=6
249B: 27 08 BEQ loc_24A5 ; cycles=3/8 nt/t
249D: 15 F4 04 F5 BTST.B #5, @H'F404 ; refs mem_F404 in program_or_external; cycles=6
24A1: 27 02 BEQ loc_24A5 ; cycles=3/8 nt/t
24A3: AB CE BSET.W #14, R3 ; cycles=3
loc_24A5:
24A5: 1E 19 AC BSR loc_3E54 ; cycles=14
24A8: 19 RTS ; cycles=12
24A9: 15 F6 D0 F3 BTST.B #3, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=6
24AD: 27 38 BEQ loc_24E7 ; cycles=3/8 nt/t
24AF: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=6
24B4: 22 31 BHI loc_24E7 ; cycles=3/7 nt/t
24B6: 1D E1 10 FF BTST.W #15, @H'E110 ; refs mem_E110 in program_or_external; cycles=7
24BA: 27 05 BEQ loc_24C1 ; cycles=3/7 nt/t
24BC: 1E 02 29 BSR loc_26E8 ; cycles=13
24BF: 20 26 BRA loc_24E7 ; cycles=8
loc_24C1:
24C1: 15 F7 30 F7 BTST.B #7, @H'F730 ; refs ram_F730 in on_chip_ram; cycles=6
24C5: 27 20 BEQ loc_24E7 ; cycles=3/8 nt/t
24C7: 1D E1 06 80 MOV:G.W @H'E106, R0 ; refs mem_E106 in program_or_external; cycles=6
24CB: A8 E0 BNOT.W #0, R0 ; cycles=3
24CD: 1D E9 06 90 MOV:G.W R0, @H'E906 ; refs mem_E906 in program_or_external; cycles=6
24D1: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
24D3: 5B 00 83 MOV:I.W #H'0083, R3 ; dataflow R3=H'0083; cycles=3
24D6: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
24DA: 27 08 BEQ loc_24E4 ; cycles=3/7 nt/t
24DC: 15 F4 04 F5 BTST.B #5, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
24E0: 27 02 BEQ loc_24E4 ; cycles=3/7 nt/t
24E2: AB CE BSET.W #14, R3 ; cycles=3
loc_24E4:
24E4: 1E 19 6D BSR loc_3E54 ; cycles=13
loc_24E7:
24E7: 19 RTS ; cycles=13
24E8: 15 F6 D0 F7 BTST.B #7, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
24EC: 27 3F BEQ loc_252D ; cycles=3/7 nt/t
24EE: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=7
24F3: 22 38 BHI loc_252D ; cycles=3/8 nt/t
24F5: 1D E1 10 FE BTST.W #14, @H'E110 ; refs mem_E110 in program_or_external; cycles=6
24F9: 27 05 BEQ loc_2500 ; cycles=3/8 nt/t
24FB: 1E 01 EA BSR loc_26E8 ; cycles=14
24FE: 20 2D BRA loc_252D ; cycles=7
loc_2500:
2500: 15 F6 F6 13 CLR.B @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=9
2504: 1D E1 1E 80 MOV:G.W @H'E11E, R0 ; refs mem_E11E in program_or_external; cycles=7
2508: A8 FF BTST.W #15, R0 ; cycles=3
250A: 26 05 BNE loc_2511 ; cycles=3/7 nt/t
250C: 58 80 00 MOV:I.W #H'8000, R0 ; dataflow R0=H'8000; cycles=3
250F: 20 02 BRA loc_2513 ; cycles=8
loc_2511:
2511: A8 13 CLR.W R0 ; dataflow R0=H'0000; cycles=3
loc_2513:
2513: 1D E9 1E 90 MOV:G.W R0, @H'E91E ; refs mem_E91E in program_or_external; cycles=6
2517: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
2519: 5B 00 8F MOV:I.W #H'008F, R3 ; dataflow R3=H'008F; cycles=3
251C: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
2520: 27 08 BEQ loc_252A ; cycles=3/7 nt/t
2522: 15 F4 04 F4 BTST.B #4, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
2526: 27 02 BEQ loc_252A ; cycles=3/7 nt/t
2528: AB CE BSET.W #14, R3 ; cycles=3
loc_252A:
252A: 1E 19 27 BSR loc_3E54 ; cycles=13
loc_252D:
252D: 19 RTS ; cycles=13
252E: 15 F6 D0 F6 BTST.B #6, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
2532: 27 3F BEQ loc_2573 ; cycles=3/7 nt/t
2534: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=7
2539: 22 38 BHI loc_2573 ; cycles=3/8 nt/t
253B: 1D E1 10 FE BTST.W #14, @H'E110 ; refs mem_E110 in program_or_external; cycles=6
253F: 27 05 BEQ loc_2546 ; cycles=3/8 nt/t
2541: 1E 01 A4 BSR loc_26E8 ; cycles=14
2544: 20 2D BRA loc_2573 ; cycles=7
loc_2546:
2546: 15 F6 F6 13 CLR.B @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=9
254A: 1D E1 1E 80 MOV:G.W @H'E11E, R0 ; refs mem_E11E in program_or_external; cycles=7
254E: A8 FD BTST.W #13, R0 ; cycles=3
2550: 26 05 BNE loc_2557 ; cycles=3/7 nt/t
2552: 58 20 00 MOV:I.W #H'2000, R0 ; dataflow R0=H'2000; cycles=3
2555: 20 02 BRA loc_2559 ; cycles=8
loc_2557:
2557: A8 13 CLR.W R0 ; dataflow R0=H'0000; cycles=3
loc_2559:
2559: 1D E9 1E 90 MOV:G.W R0, @H'E91E ; refs mem_E91E in program_or_external; cycles=6
255D: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
255F: 5B 00 8F MOV:I.W #H'008F, R3 ; dataflow R3=H'008F; cycles=3
2562: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
2566: 27 08 BEQ loc_2570 ; cycles=3/7 nt/t
2568: 15 F4 04 F4 BTST.B #4, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
256C: 27 02 BEQ loc_2570 ; cycles=3/7 nt/t
256E: AB CE BSET.W #14, R3 ; cycles=3
loc_2570:
2570: 1E 18 E1 BSR loc_3E54 ; cycles=13
loc_2573:
2573: 19 RTS ; cycles=13
2574: 15 F6 D0 F4 BTST.B #4, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
2578: 37 01 45 BEQ loc_26C0 ; cycles=3/7 nt/t
257B: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=6
2580: 22 51 BHI loc_25D3 ; cycles=3/7 nt/t
2582: 1D E1 10 FE BTST.W #14, @H'E110 ; refs mem_E110 in program_or_external; cycles=7
2586: 27 05 BEQ loc_258D ; cycles=3/7 nt/t
2588: 1E 01 5D BSR loc_26E8 ; cycles=13
258B: 20 46 BRA loc_25D3 ; cycles=8
loc_258D:
258D: 1D E1 1E 80 MOV:G.W @H'E11E, R0 ; refs mem_E11E in program_or_external; cycles=6
2591: A8 FF BTST.W #15, R0 ; cycles=3
2593: 26 06 BNE loc_259B ; cycles=3/8 nt/t
2595: A8 FD BTST.W #13, R0 ; cycles=3
2597: 26 26 BNE loc_25BF ; cycles=3/8 nt/t
2599: 20 38 BRA loc_25D3 ; cycles=8
loc_259B:
259B: 15 F6 D0 F5 BTST.B #5, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=6
259F: 27 05 BEQ loc_25A6 ; cycles=3/8 nt/t
25A1: 1E 00 8F BSR loc_2633 ; cycles=14
25A4: 20 2D BRA loc_25D3 ; cycles=7
loc_25A6:
25A6: 5C 00 00 MOV:I.W #H'0000, R4 ; dataflow R4=H'0000; cycles=3
25A9: 5B 00 91 MOV:I.W #H'0091, R3 ; dataflow R3=H'0091; cycles=3
25AC: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
25B0: 27 08 BEQ loc_25BA ; cycles=3/7 nt/t
25B2: 15 F4 04 F4 BTST.B #4, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
25B6: 27 02 BEQ loc_25BA ; cycles=3/7 nt/t
25B8: AB CE BSET.W #14, R3 ; cycles=3
loc_25BA:
25BA: 1E F4 78 BSR loc_1A35 ; cycles=13
25BD: 20 14 BRA loc_25D3 ; cycles=8
loc_25BF:
25BF: 15 F6 D0 F5 BTST.B #5, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=6
25C3: 27 05 BEQ loc_25CA ; cycles=3/8 nt/t
25C5: 1E 00 FF BSR loc_26C7 ; cycles=14
25C8: 20 09 BRA loc_25D3 ; cycles=7
loc_25CA:
25CA: 15 F6 F6 06 C0 MOV:G.B #H'C0, @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=9
25CF: 1D F6 F4 13 CLR.W @H'F6F4 ; refs ram_F6F4 in on_chip_ram; cycles=8
loc_25D3:
25D3: 19 RTS ; cycles=13
25D4: 15 F6 D0 F5 BTST.B #5, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
25D8: 37 00 E5 BEQ loc_26C0 ; cycles=3/7 nt/t
25DB: 15 F7 31 04 02 CMP:G.B #H'02, @H'F731 ; refs ram_F731 in on_chip_ram; cycles=6
25E0: 22 50 BHI loc_2632 ; cycles=3/7 nt/t
25E2: 1D E1 10 FE BTST.W #14, @H'E110 ; refs mem_E110 in program_or_external; cycles=7
25E6: 27 05 BEQ loc_25ED ; cycles=3/7 nt/t
25E8: 1E 00 FD BSR loc_26E8 ; cycles=13
25EB: 20 45 BRA loc_2632 ; cycles=8
loc_25ED:
25ED: 1D E1 1E 80 MOV:G.W @H'E11E, R0 ; refs mem_E11E in program_or_external; cycles=6
25F1: A8 FF BTST.W #15, R0 ; cycles=3
25F3: 26 06 BNE loc_25FB ; cycles=3/8 nt/t
25F5: A8 FD BTST.W #13, R0 ; cycles=3
25F7: 26 25 BNE loc_261E ; cycles=3/8 nt/t
25F9: 20 37 BRA loc_2632 ; cycles=8
loc_25FB:
25FB: 15 F6 D0 F4 BTST.B #4, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=6
25FF: 27 04 BEQ loc_2605 ; cycles=3/8 nt/t
2601: 0E 30 BSR loc_2633 ; cycles=14
2603: 20 2D BRA loc_2632 ; cycles=8
loc_2605:
2605: 5C 00 01 MOV:I.W #H'0001, R4 ; dataflow R4=H'0001; cycles=3
2608: 5B 00 91 MOV:I.W #H'0091, R3 ; dataflow R3=H'0091; cycles=3
260B: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=6
260F: 27 08 BEQ loc_2619 ; cycles=3/8 nt/t
2611: 15 F4 04 F4 BTST.B #4, @H'F404 ; refs mem_F404 in program_or_external; cycles=6
2615: 27 02 BEQ loc_2619 ; cycles=3/8 nt/t
2617: AB CE BSET.W #14, R3 ; cycles=3
loc_2619:
2619: 1E F4 19 BSR loc_1A35 ; cycles=14
261C: 20 14 BRA loc_2632 ; cycles=7
loc_261E:
261E: 15 F6 D0 F4 BTST.B #4, @H'F6D0 ; refs ram_F6D0 in on_chip_ram; cycles=7
2622: 27 05 BEQ loc_2629 ; cycles=3/7 nt/t
2624: 1E 00 A0 BSR loc_26C7 ; cycles=13
2627: 20 09 BRA loc_2632 ; cycles=8
loc_2629:
2629: 15 F6 F6 06 80 MOV:G.B #H'80, @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=9
262E: 1D F6 F4 13 CLR.W @H'F6F4 ; refs ram_F6F4 in on_chip_ram; cycles=9
loc_2632:
2632: 19 RTS ; cycles=12
loc_2633:
2633: 1D E9 22 07 80 00 MOV:G.W #H'8000, @H'E922 ; refs mem_E922 in program_or_external; cycles=9
2639: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
263B: 5B 00 91 MOV:I.W #H'0091, R3 ; dataflow R3=H'0091; cycles=3
263E: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
2642: 27 08 BEQ loc_264C ; cycles=3/7 nt/t
2644: 15 F4 04 F4 BTST.B #4, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
2648: 27 02 BEQ loc_264C ; cycles=3/7 nt/t
264A: AB CE BSET.W #14, R3 ; cycles=3
loc_264C:
264C: 1E 18 05 BSR loc_3E54 ; cycles=13
264F: 19 RTS ; cycles=13

16353
build/rom_f109_handlers.json Normal file

File diff suppressed because it is too large Load Diff

107
build/rom_f109_tail.asm Normal file
View File

@@ -0,0 +1,107 @@
; H8/536 ROM disassembly
; input: ROM\M27C512@DIP28_1.BIN
; bytes: 65536
; vector mode: min
; analysis: recursive trace from vectors
;
; Notes from the manual:
; - H8/536 uses the H8/500 CPU instruction set.
; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.
; - The register field is H'FE80-H'FFFF; names below come from appendix B.
; - @aa:8 short absolute operands use BR as the upper address byte.
; - SCI baud inference uses section 14.2.8 BRR formulas when SMR/BRR are known.
; - LCD inference treats E-clock H'F200/H'F201 accesses as status/control and data candidates.
; - Pass --clock-hz to convert SCI BRR settings into numeric baud rates.
; - Cycle counts use Appendix A tables A-7/A-8 for on-chip access with no external wait states.
; Memory Map
; H'0000-H'009F exception_vectors vectors
; H'00A0-H'00FF dtc_vectors dtc_vectors
; H'0100-H'F67F program_or_external program
; H'F680-H'FE7F on_chip_ram ram
; H'FE80-H'FFFF register_field registers
; Vectors
; H'0000 reset -> vec_reset_1000 (H'1000)
; H'0004 invalid_instruction -> vec_reset_1000 (H'1000)
; H'0006 zero_divide -> vec_reset_1000 (H'1000)
; H'0008 trap_vs -> vec_reset_1000 (H'1000)
; H'0010 address_error -> vec_reset_1000 (H'1000)
; H'0012 trace -> vec_reset_1000 (H'1000)
; H'0016 nmi -> vec_nmi_4393 (H'4393)
; H'0020 trapa_0 -> vec_reset_1000 (H'1000)
; H'0022 trapa_1 -> vec_reset_1000 (H'1000)
; H'0024 trapa_2 -> vec_reset_1000 (H'1000)
; H'0026 trapa_3 -> vec_reset_1000 (H'1000)
; H'0028 trapa_4 -> vec_reset_1000 (H'1000)
; H'002A trapa_5 -> vec_reset_1000 (H'1000)
; H'002C trapa_6 -> vec_reset_1000 (H'1000)
; H'002E trapa_7 -> vec_reset_1000 (H'1000)
; H'0030 trapa_8 -> vec_reset_1000 (H'1000)
; H'0032 trapa_9 -> vec_reset_1000 (H'1000)
; H'0034 trapa_a -> vec_reset_1000 (H'1000)
; H'0036 trapa_b -> vec_reset_1000 (H'1000)
; H'0038 trapa_c -> vec_reset_1000 (H'1000)
; H'003A trapa_d -> vec_reset_1000 (H'1000)
; H'003C trapa_e -> vec_reset_1000 (H'1000)
; H'003E trapa_f -> vec_reset_1000 (H'1000)
; H'0040 irq0 -> vec_reset_1000 (H'1000)
; H'0042 interval_timer -> vec_interval_timer_BFC4 (H'BFC4)
; H'0048 irq1 -> vec_reset_1000 (H'1000)
; H'0050 irq2 -> vec_reset_1000 (H'1000)
; H'0052 irq3 -> vec_irq3_3C30 (H'3C30)
; H'0058 irq4 -> vec_irq4_3AC7 (H'3AC7)
; H'005A irq5 -> vec_reset_1000 (H'1000)
; H'0062 frt1_ocia -> vec_frt1_ocia_BEEA (H'BEEA)
; H'006A frt2_ocia -> vec_frt2_ocia_BF23 (H'BF23)
; H'0080 sci1_eri -> vec_sci1_eri_BB57 (H'BB57)
; H'0082 sci1_rxi -> vec_sci1_rxi_BB67 (H'BB67)
; H'0084 sci1_txi -> vec_sci1_txi_BA84 (H'BA84)
; H'0090 ad_adi -> vec_ad_adi_3D99 (H'3D99)
; Symbols
; mem_E924 H'E924 program_or_external memory r=0 w=1 width=word
; mem_F404 H'F404 program_or_external memory r=1 w=0 width=byte
; ram_F6F6 H'F6F6 on_chip_ram ram r=0 w=2 width=byte
; ram_F732 H'F732 on_chip_ram ram r=1 w=1 width=word
; ram_F734 H'F734 on_chip_ram ram r=0 w=1 width=word
; ram_F791 H'F791 on_chip_ram ram r=1 w=0 width=byte
; ram_FB02 H'FB02 on_chip_ram ram r=0 w=1 width=byte
; ram_FB03 H'FB03 on_chip_ram ram r=1 w=1 width=byte
; Board Profile
; Board trace ties the H8/536 SCI1 pins to a MAX202 RS232 transceiver.
; H8 pin 66 P95/TXD (TXD) -> MAX202 pin 11
; H8 pin 67 P96/RXD (RXD) -> MAX202 pin 12
; SCI2 pin routing is disabled by SYSCR2.P9SCI2E=0 in the observed setup.
; LCD/Text Scan
; search 'CONNECT': not literal, hits=0
; LCD text candidates
; ... 1 more LCD text candidates
26C0: 15 F6 F6 13 CLR.B @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=9
26C4: 30 FF 0C BRA loc_25D3 ; cycles=7
26C7: 15 F6 F6 13 CLR.B @H'F6F6 ; refs ram_F6F6 in on_chip_ram; cycles=8
26CB: 1D E9 24 07 FF 80 MOV:G.W #H'FF80, @H'E924 ; refs mem_E924 in program_or_external; cycles=9
26D1: 52 80 MOV:E.B #H'80, R2 ; dataflow R2=H'80; cycles=2
26D3: 5B 00 92 MOV:I.W #H'0092, R3 ; dataflow R3=H'0092; cycles=3
26D6: 15 F7 91 F7 BTST.B #7, @H'F791 ; refs ram_F791 in on_chip_ram; cycles=7
26DA: 27 08 BEQ loc_26E4 ; cycles=3/7 nt/t
26DC: 15 F4 04 F4 BTST.B #4, @H'F404 ; refs mem_F404 in program_or_external; cycles=7
26E0: 27 02 BEQ loc_26E4 ; cycles=3/7 nt/t
26E2: AB CE BSET.W #14, R3 ; cycles=3
loc_26E4:
26E4: 1E 17 6D BSR loc_3E54 ; cycles=13
26E7: 19 RTS ; cycles=13
26E8: 15 FB 03 C7 BSET.B #7, @H'FB03 ; refs ram_FB03 in on_chip_ram; cycles=9
26EC: 26 08 BNE loc_26F6 ; cycles=3/7 nt/t
26EE: 1D F7 32 81 MOV:G.W @H'F732, R1 ; refs ram_F732 in on_chip_ram; cycles=7
26F2: 1D F7 34 91 MOV:G.W R1, @H'F734 ; refs ram_F734 in on_chip_ram; cycles=7
loc_26F6:
26F6: 1D F7 32 07 1C 01 MOV:G.W #H'1C01, @H'F732 ; refs ram_F732 in on_chip_ram; cycles=11
26FC: 15 FB 02 06 14 MOV:G.B #H'14, @H'FB02 ; refs ram_FB02 in on_chip_ram; cycles=9
2701: 1E 21 F6 BSR loc_48FA ; cycles=14
2704: 19 RTS ; cycles=12

2076
build/rom_f109_tail.json Normal file

File diff suppressed because it is too large Load Diff

4263
build/rom_others_menu.asm Normal file

File diff suppressed because it is too large Load Diff

219643
build/rom_others_menu.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

220100
build/rom_others_menu_deep.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

221410
build/rom_others_menu_gate.json Normal file

File diff suppressed because it is too large Load Diff

4642
build/rom_others_page1.asm Normal file

File diff suppressed because it is too large Load Diff

239015
build/rom_others_page1.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,7 @@
typedef uint8_t u8;
typedef uint16_t u16;
#define BIT(n) (1u << (n))
extern volatile u8 MEM8[0x10000];
#define SCI1_SCR MEM8[0xFEDAu]
@@ -193,6 +194,36 @@ extern volatile u8 MEM8[0x10000];
* evidence: H'1A09, H'1A71, H'3F90, H'407F, H'BB35, H'BC79, H'BC99, H'BD1E
* - flag_table_candidate at H'EC00 (bit_flags); observed write
* evidence: H'4088, H'BC82, H'BC9D, H'BD22, H'BD39, H'BDE9
* panel selector semantics:
* - 0x0013 slave_and_iris_mblack_link_lamps: Selector 0x0013 is a two-bit lamp/status word. ROM dispatch H'2E06 reads current table word H'E826 and fans bit 15 and bit 14 into panel latch RAM.
* current word: H'E826; dispatch: H'2E06
* 0x8000 -> SLAVE lamp: sets F791.6 and F713.4; RAM F791.6, F713.4
* 0x4000 -> IRIS/M.BLACK LINK lamp: sets F791.5 and F716.7; RAM F791.5, F716.7
* observed values: 0x8000 SLAVE lamp on; 0x4000 IRIS/M.BLACK LINK lamp on; 0x0000 SLAVE and IRIS/M.BLACK LINK latch bits clear through H'2E06
* state machine: iris_mblack_link_closed_loop_state_candidate: Bench-proven closed loop: the RCP reports local IRIS/M.BLACK LINK intent, the CCU ACKs selector 0x0013, then the CCU mirrors the accepted selector state back with command 0. The mirrored state controls the next toggle direction.
* frames: active report 00 00 13 40 00 09; clear report 00 00 13 00 00 49; ACK 05 00 13 00 00 4C; mirror active 00 00 13 40 00 09; mirror clear 00 00 13 00 00 49
* local trigger candidates: provisional_iris_mblack_link_button_toggle_report F006.7 / F6DB.7: When F6DB.7 is asserted and F731 <= 3, the ROM toggles current-table bit 14 at H'E826 based on F791.5, then queues selector 0x0013 through loc_3E54.; H'1FE8/H'1FFB: Adjacent local helpers set or clear current-table bit 15 at H'E826 and queue selector 0x0013.
* evidence: bench: 00 00 13 80 00 C9 lights far-right SLAVE lamp, bench: 00 00 13 40 00 09 lights IRIS/M.BLACK LINK lamp, ROM: H'2E06-H'2E32 tests H'E826 bits 15/14 and sets/clears F791/F713/F716 latch bits
* - 0x0015 call_and_red_tally_lamp_lane: Bench-visible CALL lamp and red tally lane; local CALL handler mirrors F6DB.5 into E800[0x0015].15.
* current word: H'E82A; dispatch: handler unknown
* observed values: 0x8000 CALL lamp and red tally on; 0x0000 CALL inactive/clear report
* evidence: bench: 00 00 15 80 00 CF lights CALL and red tally, ROM: H'20A1-H'20BA reads F6DB.5, writes H'E82A, and queues selector 0x0015
* - 0x0017 bars_lamp_lane: Bench-visible BARS lamp/latch lane; low writes do not reliably clear the visible latch.
* current word: H'E82E; dispatch: handler unknown
* observed values: 0x8000 BARS lamp on; 0x4000 BARS lamp/latch on; 0x0000 BARS low write; visible latch may remain
* evidence: bench: 00 00 17 80 00 CD lights BARS, bench: 00 00 17 40 00 0D also lights the BARS latch in neighbor sweep, ROM: H'1EDE can queue selector 0x0017 from F6D4.2
* - 0x001A monitor_selector_lamps: Bench-visible MONITOR selector cluster found from ROM-derived button-output sweep.
* current word: H'E834; dispatch: handler unknown
* observed values: 0x0808 MONITOR ENC lamp; 0x2020 MONITOR B lamp; 0x4040 MONITOR G lamp; 0x8080 MONITOR R lamp
* evidence: bench: 00 00 1A 08 08 40 lights MONITOR ENC, bench: 00 00 1A 20 20 40 lights MONITOR B, bench: 00 00 1A 40 40 40 lights MONITOR G, bench: 00 00 1A 80 80 40 lights MONITOR R, ROM: H'1CB2-H'1D56 writes packed values to H'E834 and queues selector 0x001A
* - 0x0024 lcd_selector_button_lamp: Bench-visible LCD selector-button lamp lane.
* current word: H'E848; dispatch: dispatch unknown
* observed values: 0x8000 LCD selector-button lamp visible; 0x0000 lamp remained visible at 0.5 s in isolation run
* - 0x006B standard_lamp_lane: Bench-visible STANDARD lamp lane found from ROM-derived F6D4.6 handler candidate.
* current word: H'E8D6; dispatch: handler unknown
* observed values: 0x8000 STANDARD lamp on
* evidence: bench: 00 00 6B 80 00 B1 lights STANDARD, ROM: H'2048 can write H'E8D6=0x8000 and queue selector 0x006B from F6D4.6
* - ... 5 more panel selector annotations
* state variable candidates:
* - event_queue_read_cursor_candidate H'F9B4: reads 1, writes 2; bits 5
* evidence: H'BE78, H'BE95, H'BE99
@@ -357,12 +388,197 @@ void frt2_ocia_candidate_tick_isr(void)
}
static void sci1_candidate_panel_selector_annotation(u16 logical_index, u16 value)
{
/* Known bench/ROM selector labels. This helper is commentary for the decompile. */
switch (logical_index) {
case 0x0013u:
/* 0x0013 slave_and_iris_mblack_link_lamps; current word H'E826; H'2E06. */
if ((value & 0x8000u) != 0u) {
/* SLAVE lamp: sets F791.6 and F713.4. */
} else {
/* SLAVE lamp: clears F791.6 and F713.4. */
}
if ((value & 0x4000u) != 0u) {
/* IRIS/M.BLACK LINK lamp: sets F791.5 and F716.7. */
} else {
/* IRIS/M.BLACK LINK lamp: clears F791.5 and F716.7. */
}
if (value == 0x8000u) {
/* SLAVE lamp on. */
}
if (value == 0x4000u) {
/* IRIS/M.BLACK LINK lamp on. */
}
if (value == 0x0000u) {
/* SLAVE and IRIS/M.BLACK LINK latch bits clear through H'2E06. */
}
break;
case 0x0015u:
/* 0x0015 call_and_red_tally_lamp_lane; current word H'E82A; handler unknown. */
if (value == 0x8000u) {
/* CALL lamp and red tally on. */
}
if (value == 0x0000u) {
/* CALL inactive/clear report. */
}
break;
case 0x0017u:
/* 0x0017 bars_lamp_lane; current word H'E82E; handler unknown. */
if (value == 0x8000u) {
/* BARS lamp on. */
}
if (value == 0x4000u) {
/* BARS lamp/latch on. */
}
if (value == 0x0000u) {
/* BARS low write; visible latch may remain. */
}
break;
case 0x001Au:
/* 0x001A monitor_selector_lamps; current word H'E834; handler unknown. */
if (value == 0x0808u) {
/* MONITOR ENC lamp. */
}
if (value == 0x2020u) {
/* MONITOR B lamp. */
}
if (value == 0x4040u) {
/* MONITOR G lamp. */
}
if (value == 0x8080u) {
/* MONITOR R lamp. */
}
break;
case 0x0024u:
/* 0x0024 lcd_selector_button_lamp; current word H'E848; dispatch unknown. */
if (value == 0x8000u) {
/* LCD selector-button lamp visible. */
}
if (value == 0x0000u) {
/* lamp remained visible at 0.5 s in isolation run. */
}
break;
case 0x006Bu:
/* 0x006B standard_lamp_lane; current word H'E8D6; handler unknown. */
if (value == 0x8000u) {
/* STANDARD lamp on. */
}
break;
case 0x0082u:
/* 0x0082 iris_readout_lane; current word H'E904; dispatch unknown. */
if (value == 0x8000u) {
/* IRIS display OP. */
}
if (value == 0x4000u) {
/* IRIS display 1.4. */
}
if (value == 0x0000u) {
/* IRIS display blank. */
}
break;
case 0x0083u:
/* 0x0083 combined_iris_shutter_master_gain_status_lane; current word H'E906; dispatch unknown. */
if (value == 0x8000u) {
/* IRIS AUTO, SHUTTER OFF, MASTER GAIN -3. */
}
if (value == 0x4000u) {
/* IRIS AUTO, SHUTTER OFF, MASTER GAIN 0. */
}
if (value == 0x2000u) {
/* IRIS AUTO, SHUTTER OFF, MASTER GAIN 3. */
}
if (value == 0x0004u) {
/* IRIS AUTO, SHUTTER OFF, MASTER GAIN HP. */
}
if (value == 0x0000u) {
/* same visible state remained at 0.5 s. */
}
break;
case 0x008Fu:
/* 0x008F shutter_display_status_lane; current word H'E91E; dispatch unknown. */
if (value == 0x8000u) {
/* IRIS AUTO plus shutter value beginning with 1. */
}
if (value == 0x2000u) {
/* IRIS AUTO plus shutter 00.0. */
}
if (value == 0x0800u) {
/* IRIS AUTO plus shutter EVS. */
}
if (value == 0x1000u) {
/* IRIS AUTO plus shutter OFF. */
}
break;
case 0x0093u:
/* 0x0093 white_balance_black_flare_mode_lane; current word H'E926; dispatch unknown. */
if (value == 0x8000u) {
/* BLACK/FLARE MANUAL plus white-balance PRESET. */
}
if (value == 0x4000u) {
/* BLACK/FLARE MANUAL plus white-balance AUTO. */
}
if (value == 0x2000u) {
/* BLACK/FLARE MANUAL plus white-balance MANUAL. */
}
if (value == 0x1020u) {
/* BLACK/FLARE MANUAL plus white-balance MANUAL. */
}
if (value == 0x4040u) {
/* BLACK/FLARE AUTO plus white-balance AUTO. */
}
if (value == 0x8040u) {
/* BLACK/FLARE AUTO plus white-balance PRESET. */
}
if (value == 0x0020u) {
/* BLACK/FLARE MANUAL plus white-balance MANUAL. */
}
if (value == 0x0040u) {
/* BLACK/FLARE AUTO plus white-balance MANUAL. */
}
if (value == 0x0000u) {
/* BLACK/FLARE MANUAL plus white-balance MANUAL. */
}
break;
case 0x0110u:
/* 0x0110 knee_auto_lamp_or_page_status_lane; current word H'EA20; dispatch unknown. */
if (value == 0x8000u) {
/* KNEE AUTO lamp/status on. */
}
break;
default:
break;
}
}
void provisional_iris_mblack_link_button_toggle_report(void)
{
/* Provisional name for ROM H'200E: When F6DB.7 is asserted and F731 <= 3, the ROM toggles current-table bit 14 at H'E826 based on F791.5, then queues selector 0x0013 through loc_3E54. */
/* Source F006.7 / F6DB.7; gate F731 <= 3; current state F791.5. */
if ((MEM8[0xF6DBu] & BIT(7)) == 0u) {
return;
}
if (MEM8[0xF731u] > 3u) {
return;
}
if ((MEM8[0xF791u] & BIT(5)) == 0u) {
/* Requests selector 0x0013=0x4000: 00 00 13 40 00 09. */
/* CCU should ACK 05 00 13 00 00 4C, then mirror 00 00 13 40 00 09. */
} else {
/* Requests selector 0x0013=0x0000: 00 00 13 00 00 49. */
/* CCU should ACK 05 00 13 00 00 4C, then mirror 00 00 13 00 00 49. */
}
}
void sci1_process_candidate_protocol_command(void)
{
u8 command = sci1_rx_candidate_command();
u16 logical_index = sci1_rx_candidate_logical_index();
u16 value = sci1_rx_candidate_value();
sci1_candidate_panel_selector_annotation(logical_index, value);
bool session_active = MEM8[0xFAA2u] != 0u;
if (!session_active) {

View File

@@ -18,32 +18,32 @@ primary_value_table_candidate H'E000-H'E3FF (negative H'2000; direct H'F900-H'F9
accesses=31 reads=21 writes=10 dynamic=11
static offsets: H'0000, H'0004, H'0006, H'0046, H'0080, H'0102, H'0124, H'0126, H'014E, H'016E, H'0172, H'01EC, H'0220
functions: loc_BBAB:5, loc_2650:3, loc_4096:3, loc_1795:2, loc_19DB:2, loc_1A35:2, loc_48FA:2, vec_ad_adi_3D99:2, <no function>:1, loc_1705:1, loc_174D:1, loc_17C9:1
- H'170C read offset H'014E -> H'E14E; loc_1705; BTST.W #15, @H'E14E
- H'175A read offset H'016E -> H'E16E; loc_174D; BTST.W #13, @H'E16E
- H'179C read offset H'0172 -> H'E172; loc_1795; BTST.W #13, @H'E172
- H'17A7 read offset H'0220 -> H'E220; loc_1795; BTST.W #15, @H'E220
- H'17D0 read offset H'0126 -> H'E126; loc_17C9; BTST.W #12, @H'E126
- H'1802 read offset H'0126 -> H'E126; loc_17FB; BTST.W #12, @H'E126
- H'183A read offset H'0126 -> H'E126; loc_182D; BTST.W #5, @H'E126
- H'189E read offset H'0126 -> H'E126; loc_1891; BTST.W #5, @H'E126
- H'18F4 read offset H'0126 -> H'E126; loc_18E7; BTST.W #5, @H'E126
- H'170C read offset H'014E selector 0x0A7 -> H'E14E; loc_1705; BTST.W #15, @H'E14E
- H'175A read offset H'016E selector 0x0B7 -> H'E16E; loc_174D; BTST.W #13, @H'E16E
- H'179C read offset H'0172 selector 0x0B9 -> H'E172; loc_1795; BTST.W #13, @H'E172
- H'17A7 read offset H'0220 selector 0x110 -> H'E220; loc_1795; BTST.W #15, @H'E220
- H'17D0 read offset H'0126 selector 0x093 -> H'E126; loc_17C9; BTST.W #12, @H'E126
- H'1802 read offset H'0126 selector 0x093 -> H'E126; loc_17FB; BTST.W #12, @H'E126
- H'183A read offset H'0126 selector 0x093 -> H'E126; loc_182D; BTST.W #5, @H'E126
- H'189E read offset H'0126 selector 0x093 -> H'E126; loc_1891; BTST.W #5, @H'E126
- H'18F4 read offset H'0126 selector 0x093 -> H'E126; loc_18E7; BTST.W #5, @H'E126
- H'19E3 read index dynamic via R3 operand @(-H'2000,R3); loc_19DB; MOV:G.W @(-H'2000,R3), R0
- H'1A03 read index dynamic via R3 operand @(-H'2000,R3); loc_19DB; CMP:G.W @(-H'2000,R3), R1
- H'1A3D read index dynamic via R3 operand @(-H'2000,R3); loc_1A35; MOV:G.W @(-H'2000,R3), R0
- H'1A6B read index dynamic via R3 operand @(-H'2000,R3); loc_1A35; CMP:G.W @(-H'2000,R3), R0
- H'2657 read offset H'0124 -> H'E124; loc_2650; MOV:G.W @H'E124, R0
- H'266F read offset H'0004 -> H'E004; loc_2650; BTST.W #13, @H'E004
- H'268B read offset H'0124 -> H'E124; loc_2650; CMP:G.W @H'E124, R0
- H'3DDA read offset H'0102 -> H'E102; vec_ad_adi_3D99; MOV:G.W @H'E102, R0
- H'3DFA read offset H'0102 -> H'E102; vec_ad_adi_3D99; CMP:G.W @H'E102, R1
- H'2657 read offset H'0124 selector 0x092 -> H'E124; loc_2650; MOV:G.W @H'E124, R0
- H'266F read offset H'0004 selector 0x002 -> H'E004; loc_2650; BTST.W #13, @H'E004
- H'268B read offset H'0124 selector 0x092 -> H'E124; loc_2650; CMP:G.W @H'E124, R0
- H'3DDA read offset H'0102 selector 0x081 -> H'E102; vec_ad_adi_3D99; MOV:G.W @H'E102, R0
- H'3DFA read offset H'0102 selector 0x081 -> H'E102; vec_ad_adi_3D99; CMP:G.W @H'E102, R1
- H'3F8C write index dynamic via R0 operand @(-H'2000,R0); <no function>; CLR.W @(-H'2000,R0)
- H'402C write offset H'0046 -> H'E046; loc_400C; CLR.W @H'E046
- H'402C write offset H'0046 selector 0x023 -> H'E046; loc_400C; CLR.W @H'E046
- H'4077 write index dynamic via R0 operand @(-H'2000,R0); loc_4075; CLR.W @(-H'2000,R0)
- H'4096 write offset H'0000 -> H'E000; loc_4096; MOV:G.W #H'0080, @H'E000
- H'409C write offset H'0006 -> H'E006; loc_4096; MOV:G.W #H'8000, @H'E006
- H'40A2 write offset H'0080 -> H'E080; loc_4096; MOV:G.W #H'FFFF, @H'E080
- H'490F read offset H'01EC -> H'E1EC; loc_48FA; BTST.W #13, @H'E1EC
- H'4915 read offset H'01EC -> H'E1EC; loc_48FA; MOV:G.W @H'E1EC, R0
- H'4096 write offset H'0000 selector 0x000 -> H'E000; loc_4096; MOV:G.W #H'0080, @H'E000
- H'409C write offset H'0006 selector 0x003 -> H'E006; loc_4096; MOV:G.W #H'8000, @H'E006
- H'40A2 write offset H'0080 selector 0x040 -> H'E080; loc_4096; MOV:G.W #H'FFFF, @H'E080
- H'490F read offset H'01EC selector 0x0F6 -> H'E1EC; loc_48FA; BTST.W #13, @H'E1EC
- H'4915 read offset H'01EC selector 0x0F6 -> H'E1EC; loc_48FA; MOV:G.W @H'E1EC, R0
- H'BC75 write index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W R0, @(-H'2000,R4)
- H'BC95 write index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W R0, @(-H'2000,R4)
- H'BCEC read index dynamic via R4 operand @(-H'2000,R4); loc_BBAB; MOV:G.W @(-H'2000,R4), R0
@@ -66,16 +66,16 @@ current_value_table_candidate H'E800-H'EBFF (negative H'1800; direct H'F920-H'F9
accesses=14 reads=1 writes=13 dynamic=8
static offsets: H'0000, H'0006, H'0080, H'0102, H'0124, H'01EC
functions: loc_4096:3, loc_BBAB:3, <no function>:1, loc_15E0:1, loc_19DB:1, loc_1A35:1, loc_2650:1, loc_4075:1, loc_48FA:1, loc_BAF2:1
- H'15ED write offset H'0102 -> H'E902; loc_15E0; MOV:G.W R1, @H'E902
- H'15ED write offset H'0102 selector 0x081 -> H'E902; loc_15E0; MOV:G.W R1, @H'E902
- H'1A09 write index dynamic via R3 operand @(-H'1800,R3); loc_19DB; MOV:G.W R1, @(-H'1800,R3)
- H'1A71 write index dynamic via R3 operand @(-H'1800,R3); loc_1A35; MOV:G.W R0, @(-H'1800,R3)
- H'2691 write offset H'0124 -> H'E924; loc_2650; MOV:G.W R0, @H'E924
- H'2691 write offset H'0124 selector 0x092 -> H'E924; loc_2650; MOV:G.W R0, @H'E924
- H'3F90 write index dynamic via R0 operand @(-H'1800,R0); <no function>; CLR.W @(-H'1800,R0)
- H'407F write index dynamic via R0 operand @(-H'1800,R0); loc_4075; CLR.W @(-H'1800,R0)
- H'40A8 write offset H'0000 -> H'E800; loc_4096; MOV:G.W #H'0080, @H'E800
- H'40AE write offset H'0006 -> H'E806; loc_4096; MOV:G.W #H'8000, @H'E806
- H'40B4 write offset H'0080 -> H'E880; loc_4096; MOV:G.W #H'FFFF, @H'E880
- H'491D write offset H'01EC -> H'E9EC; loc_48FA; MOV:G.W R0, @H'E9EC
- H'40A8 write offset H'0000 selector 0x000 -> H'E800; loc_4096; MOV:G.W #H'0080, @H'E800
- H'40AE write offset H'0006 selector 0x003 -> H'E806; loc_4096; MOV:G.W #H'8000, @H'E806
- H'40B4 write offset H'0080 selector 0x040 -> H'E880; loc_4096; MOV:G.W #H'FFFF, @H'E880
- H'491D write offset H'01EC selector 0x0F6 -> H'E9EC; loc_48FA; MOV:G.W R0, @H'E9EC
- H'BB35 read index dynamic via R0 operand @(-H'1800,R0); loc_BAF2; MOV:G.W @(-H'1800,R0), R4
- H'BC79 write index dynamic via R4 operand @(-H'1800,R4); loc_BBAB; MOV:G.W R0, @(-H'1800,R4)
- H'BC99 write index dynamic via R4 operand @(-H'1800,R4); loc_BBAB; MOV:G.W R0, @(-H'1800,R4)
@@ -85,7 +85,7 @@ flag_table_candidate H'EC00-H'EFFF (negative H'1400; direct H'F980-H'F99F)
accesses=6 reads=0 writes=6 dynamic=5
static offsets: H'0200
functions: loc_BBAB:5, loc_4075:1
- H'4088 write offset H'0200 -> H'EE00; loc_4075; CLR.W @(-H'1400,R0)
- H'4088 write offset H'0200 selector 0x000 -> H'EE00; loc_4075; CLR.W @(-H'1400,R0)
- H'BC82 write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)
- H'BC9D write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)
- H'BD22 write index dynamic via R5 operand @(-H'1400,R5); loc_BBAB; BSET.B #7, @(-H'1400,R5)

86
ccu_emulator/README.md Normal file
View File

@@ -0,0 +1,86 @@
# PT2 Fake CCU
This folder contains a small modular fake-CCU runner for the Sony RCP PT2-style serial link.
The first goal is simple:
1. Open the RCP serial link at `38400 8E1`.
2. Optionally wait for heartbeat.
3. Seed active selector-zero state.
4. Listen for complete RCP report frames.
5. Immediately answer report-looking frames with the neutral command-5 ACK:
```text
05 00 40 00 00 1F
```
ROM and emulator evidence says this consumes the outstanding report cursor without triggering COPY or lamp/display side effects.
## Run
Dry-run configuration:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --dry-run
```
Run against the bench RCP on COM5:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --port COM5 --duration 30 --log captures\ccu-keepalive.txt
```
Power-cycle first through the COM6 relay:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --port COM5 --power-cycle --relay-port COM6 --duration 30 --log captures\ccu-keepalive-powercycle.txt
```
Try the older three-frame CONNECT cadence seed instead of the command-0 active seed:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --seed connect-sequence --seed-gap 0.700 --duration 30
```
Optionally add periodic state refresh traffic:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --refresh-active --refresh-interval 0.600 --duration 30
```
Enable the bench-proven IRIS/M.BLACK LINK closed-loop module:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --iris-mblack-link --duration 60 --log captures\ccu-iris-mblack-link.txt
```
When the RCP reports selector `0x0013` as `0x4000` or `0x0000`, this module sends:
```text
05 00 13 00 00 4C ; ACK selector 0x0013
00 00 13 40 00 09 ; mirror active, or 00 00 13 00 00 49 for clear
```
That matches the bench-proven IRIS/M.BLACK LINK state machine: the RCP reports local intent, the CCU acknowledges the selector, then the CCU mirrors the accepted state back so the next physical press toggles the other way.
## Layout
- `frames.py`: checksums, built-in frames, and simple host-frame builders.
- `iris_mblack_link.py`: selector `0x0013` ACK-and-mirror state-machine module.
- `modules.py`: small protocol-module interface for feature-specific CCU behavior.
- `policy.py`: decides whether an RCP frame should be ACKed.
- `refresh.py`: optional periodic state-refresh scheduling.
- `serial_link.py`: serial read/write plus checksum-resync frame detection.
- `controller.py`: event loop and stats.
- `cli.py`: command-line entry point.
## Why Periodic Lamp/Value Writes Help
Repeated button/lamp/status writes probably do play into the normal CCU flow. The ROM reloads the broad `F9C5` session watchdog on every complete six-byte RX frame, and command-0/command-4/command-6 table writes refresh the selector tables that drive lamps, readouts, and menus.
So a real CCU likely does both:
- reactively ACK RCP reports so the report queue advances, and
- stream or refresh panel state so the visible UI remains current.
The first version keeps those concerns separate: reactive ACKs are always available, while periodic refresh frames are opt-in with `--refresh-frame` or `--refresh-active`.

30
ccu_emulator/__init__.py Normal file
View File

@@ -0,0 +1,30 @@
"""Small fake-CCU helpers for the Sony RCP PT2-style serial link."""
from .controller import CcuEmulator, CcuStats
from .frames import (
ACTIVE_SEED_COMMAND0,
CONNECT_CADENCE_SEQUENCE,
HEARTBEAT_FRAME,
NEUTRAL_ACK_FRAME,
format_frame,
frame_checksum,
frame_checksum_ok,
parse_frame,
)
from .iris_mblack_link import IrisMblackLinkModule
from .policy import AckPolicy
__all__ = [
"ACTIVE_SEED_COMMAND0",
"CONNECT_CADENCE_SEQUENCE",
"CcuEmulator",
"CcuStats",
"HEARTBEAT_FRAME",
"IrisMblackLinkModule",
"NEUTRAL_ACK_FRAME",
"AckPolicy",
"format_frame",
"frame_checksum",
"frame_checksum_ok",
"parse_frame",
]

5
ccu_emulator/__main__.py Normal file
View File

@@ -0,0 +1,5 @@
from .cli import main
if __name__ == "__main__":
raise SystemExit(main())

208
ccu_emulator/cli.py Normal file
View File

@@ -0,0 +1,208 @@
from __future__ import annotations
import argparse
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import TextIO
from h8536.bench_connect_lcd import (
BenchLogger,
_import_serial,
_read_relay_lines,
_relay_command,
_relay_settle,
add_serial_format_args,
open_device_serial,
serial_format_label,
)
from .controller import CcuConfig, CcuEmulator
from .frames import ACTIVE_SEED_COMMAND0, CONNECT_CADENCE_SEQUENCE, NEUTRAL_ACK_FRAME, format_frame, parse_frame
from .iris_mblack_link import IrisMblackLinkModule
from .modules import CcuModule
from .policy import AckPolicy
from .refresh import PeriodicRefresh
from .serial_link import SerialLink
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Run a small fake CCU for the Sony RCP PT2-style serial link.")
parser.add_argument("--port", default="COM5", help="RS232 serial port connected to the RCP")
parser.add_argument("--baud", type=int, default=38400, help="RCP serial baud rate")
add_serial_format_args(parser)
parser.add_argument("--duration", type=float, default=30.0, help="seconds to run the CCU loop")
parser.add_argument("--sync", choices=("checksum", "fixed"), default="checksum", help="RX frame sync strategy")
parser.add_argument("--log", type=Path, help="capture log path")
parser.add_argument(
"--seed",
choices=("command0", "connect-sequence", "none"),
default="command0",
help="built-in wake-up seed before reactive ACK loop",
)
parser.add_argument("--seed-frame", action="append", type=parse_frame, help="custom seed frame; repeatable")
parser.add_argument("--seed-gap", type=float, default=0.050, help="seconds to listen after each seed frame")
parser.add_argument("--ready-heartbeats", type=int, default=1, help="heartbeats to observe before seeding")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="seconds to wait for ready heartbeat")
parser.add_argument("--ack-frame", type=parse_frame, default=NEUTRAL_ACK_FRAME, help="ACK frame to send after RCP reports")
parser.add_argument("--ack-delay", type=float, default=0.0, help="seconds to wait after detecting an RCP frame before ACK")
parser.add_argument("--no-ack-heartbeats", action="store_true", help="do not ACK heartbeat frames")
parser.add_argument("--no-ack-reports", action="store_true", help="do not ACK report-looking frames")
parser.add_argument(
"--no-ack-unlabeled",
action="store_true",
help="do not ACK checksum-valid unlabeled frames outside known report command bytes",
)
parser.add_argument("--refresh-frame", action="append", type=parse_frame, help="optional periodic refresh frame")
parser.add_argument(
"--refresh-active",
action="store_true",
help="periodically refresh selector zero with command0 0x8080",
)
parser.add_argument("--refresh-interval", type=float, default=0.0, help="seconds between optional refresh frames")
parser.add_argument("--loop-poll", type=float, default=0.001, help="sleep between service loop iterations")
parser.add_argument(
"--iris-mblack-link",
action="store_true",
help="enable the selector 0x0013 IRIS/M.BLACK LINK ACK-and-mirror module",
)
parser.add_argument(
"--iris-mblack-link-mirror-delay",
type=float,
default=0.050,
help="seconds between the selector 0x0013 ACK and command-0 mirror",
)
parser.add_argument("--power-cycle", action="store_true", help="power-cycle DUT through relay before starting")
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port")
parser.add_argument("--relay-baud", type=int, default=115200, help="Pico relay serial baud rate")
parser.add_argument("--power-off-command", default="off", help="relay command used to remove DUT power")
parser.add_argument("--power-on-command", default="on", help="relay command used to apply DUT power")
parser.add_argument("--off-seconds", type=float, default=1.5, help="seconds to hold DUT powered off")
parser.add_argument("--relay-settle", type=float, default=2.0, help="seconds to wait after opening relay port")
parser.add_argument("--dry-run", action="store_true", help="print configuration without opening serial ports")
return parser
def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
args = build_arg_parser().parse_args(argv)
seed_frames = _seed_frames(args)
refresh_frames = _refresh_frames(args)
modules = _modules(args)
log_path = args.log or _default_log_path()
if args.dry_run:
_print_dry_run(args, seed_frames, refresh_frames, modules, log_path, stdout)
return 0
serial = _import_serial()
logger = BenchLogger(log_path, stdout=stdout)
relay = None
try:
logger.emit("PT2 fake CCU")
logger.emit(f"device={args.port} {args.baud} {serial_format_label(args)} sync={args.sync}")
logger.emit(f"log={log_path}")
logger.emit(f"ack_frame={format_frame(args.ack_frame)}")
logger.emit("seed_frames=" + (" | ".join(format_frame(frame) for frame in seed_frames) or "none"))
if refresh_frames:
logger.emit(
f"refresh_interval={args.refresh_interval:.3f}s frames="
+ " | ".join(format_frame(frame) for frame in refresh_frames)
)
if modules:
logger.emit("modules=" + " | ".join(module.name for module in modules))
with open_device_serial(serial, args) as device:
if args.power_cycle:
relay = serial.Serial(args.relay_port, args.relay_baud, timeout=0.25)
_relay_settle(relay, args.relay_settle, logger)
_relay_command(relay, args.power_off_command, logger)
time.sleep(max(0.0, args.off_seconds))
device.reset_input_buffer()
_relay_command(relay, args.power_on_command, logger)
else:
device.reset_input_buffer()
link = SerialLink(device, logger, sync_mode=args.sync)
config = CcuConfig(
seed_frames=tuple(seed_frames),
seed_gap=args.seed_gap,
ack_delay=args.ack_delay,
ready_heartbeats=args.ready_heartbeats,
ready_timeout=args.ready_timeout,
loop_poll=args.loop_poll,
)
policy = AckPolicy(
ack_frame=args.ack_frame,
ack_reports=not args.no_ack_reports,
ack_heartbeats=not args.no_ack_heartbeats,
ack_unlabeled_checksum_frames=not args.no_ack_unlabeled,
)
refresh = PeriodicRefresh(frames=refresh_frames, interval=args.refresh_interval)
CcuEmulator(link, logger, config=config, ack_policy=policy, refresh=refresh, modules=modules).run(
args.duration
)
return 0
finally:
if relay is not None:
relay.close()
logger.close()
def _seed_frames(args: argparse.Namespace) -> list[bytes]:
if args.seed_frame:
return list(args.seed_frame)
if args.seed == "none":
return []
if args.seed == "connect-sequence":
return list(CONNECT_CADENCE_SEQUENCE)
return [ACTIVE_SEED_COMMAND0]
def _refresh_frames(args: argparse.Namespace) -> list[bytes]:
frames = list(args.refresh_frame or [])
if args.refresh_active:
frames.append(ACTIVE_SEED_COMMAND0)
return frames
def _modules(args: argparse.Namespace) -> tuple[CcuModule, ...]:
modules: list[CcuModule] = []
if args.iris_mblack_link:
modules.append(IrisMblackLinkModule(mirror_delay=max(0.0, args.iris_mblack_link_mirror_delay)))
return tuple(modules)
def _print_dry_run(
args: argparse.Namespace,
seed_frames: list[bytes],
refresh_frames: list[bytes],
modules: tuple[CcuModule, ...],
log_path: Path,
stdout: TextIO,
) -> None:
print(f"device={args.port} {args.baud} {serial_format_label(args)} sync={args.sync}", file=stdout)
print(f"duration={args.duration:.3f}s log={log_path}", file=stdout)
print(f"power_cycle={int(args.power_cycle)} relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"ack_frame={format_frame(args.ack_frame)}", file=stdout)
print("seed_frames=" + (" | ".join(format_frame(frame) for frame in seed_frames) or "none"), file=stdout)
print(
f"refresh_interval={args.refresh_interval:.3f}s frames="
+ (" | ".join(format_frame(frame) for frame in refresh_frames) or "none"),
file=stdout,
)
print("modules=" + (" | ".join(module.name for module in modules) or "none"), file=stdout)
def _default_log_path() -> Path:
return Path("captures") / f"ccu-emulator-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
if __name__ == "__main__":
raise SystemExit(main())

179
ccu_emulator/controller.py Normal file
View File

@@ -0,0 +1,179 @@
from __future__ import annotations
import time
from dataclasses import dataclass
from h8536.bench_connect_lcd import BenchLogger, format_frame
from .frames import ACTIVE_SEED_COMMAND0
from .modules import CcuModule, ModuleDecision
from .policy import AckPolicy
from .refresh import PeriodicRefresh
from .serial_link import RxFrame, SerialLink
@dataclass
class CcuStats:
rx_frames: int = 0
tx_frames: int = 0
ack_frames: int = 0
module_frames: int = 0
seed_frames: int = 0
refresh_frames: int = 0
started_at: float = 0.0
ended_at: float = 0.0
@property
def elapsed(self) -> float:
end = self.ended_at or time.monotonic()
return max(0.0, end - self.started_at)
@dataclass(frozen=True)
class CcuConfig:
seed_frames: tuple[bytes, ...] = (ACTIVE_SEED_COMMAND0,)
seed_gap: float = 0.050
ack_delay: float = 0.0
ready_heartbeats: int = 1
ready_timeout: float = 10.0
loop_poll: float = 0.001
class CcuEmulator:
"""Event-driven fake CCU for the PT2-style RCP serial protocol."""
def __init__(
self,
link: SerialLink,
logger: BenchLogger,
*,
config: CcuConfig | None = None,
ack_policy: AckPolicy | None = None,
refresh: PeriodicRefresh | None = None,
modules: tuple[CcuModule, ...] = (),
) -> None:
self.link = link
self.logger = logger
self.config = config or CcuConfig()
self.ack_policy = ack_policy or AckPolicy()
self.refresh = refresh or PeriodicRefresh()
self.modules = modules
self.stats = CcuStats()
def run(self, duration: float) -> CcuStats:
self.stats = CcuStats(started_at=time.monotonic())
self.logger.event(
"CCU_START "
f"duration={duration:.3f}s seed_frames={len(self.config.seed_frames)} "
f"ack={format_frame(self.ack_policy.ack_frame)} modules={len(self.modules)}"
)
self._wait_ready()
self._send_seed_frames()
self.refresh.start()
deadline = time.monotonic() + max(0.0, duration)
try:
while time.monotonic() < deadline:
self._service_rx()
self._service_refresh()
time.sleep(max(0.0, self.config.loop_poll))
except KeyboardInterrupt:
self.logger.event("CCU_STOP keyboard_interrupt")
finally:
self.stats.ended_at = time.monotonic()
self._emit_summary()
return self.stats
def _wait_ready(self) -> None:
if self.config.ready_heartbeats <= 0:
self.logger.event("READY skipped")
return
self.logger.event(
f"READY_WAIT heartbeats={self.config.ready_heartbeats} timeout={self.config.ready_timeout:.3f}s"
)
start_count = self.link.detector.labels["heartbeat"]
deadline = time.monotonic() + max(0.0, self.config.ready_timeout)
while time.monotonic() < deadline:
for frame in self.link.read_available():
self._record_rx(frame)
if self.link.detector.labels["heartbeat"] - start_count >= self.config.ready_heartbeats:
self.logger.event(f"READY heartbeat_count={self.link.detector.labels['heartbeat']}")
return
time.sleep(max(0.0, self.config.loop_poll))
self.logger.event(f"READY_TIMEOUT heartbeat_count={self.link.detector.labels['heartbeat']}")
def _send_seed_frames(self) -> None:
for index, frame in enumerate(self.config.seed_frames, start=1):
self.link.send(frame, f"seed[{index}]")
self.stats.seed_frames += 1
self.stats.tx_frames += 1
self._listen_for(self.config.seed_gap)
def _listen_for(self, seconds: float) -> None:
deadline = time.monotonic() + max(0.0, seconds)
while time.monotonic() < deadline:
self._service_rx()
self._service_refresh()
time.sleep(max(0.0, self.config.loop_poll))
def _service_rx(self) -> None:
for item in self.link.read_available():
self._record_rx(item)
suppress_default_ack = self._service_modules(item)
if suppress_default_ack:
continue
decision = self.ack_policy.decide(item.frame, item.label)
if not decision.should_ack:
self.logger.event(f"ACK_SKIP reason={decision.reason} frame={format_frame(item.frame)}")
continue
if self.config.ack_delay > 0:
time.sleep(self.config.ack_delay)
self.link.send(decision.frame, f"ack reason={decision.reason}")
self.stats.ack_frames += 1
self.stats.tx_frames += 1
def _service_modules(self, item: RxFrame) -> bool:
suppress_default_ack = False
for module in self.modules:
decision = module.on_rx(item.frame, item.label)
if decision is None:
continue
if decision.reason:
self.logger.event(
f"MODULE {module.name} reason={decision.reason} frame={format_frame(item.frame)}"
)
self._send_module_decision(decision)
suppress_default_ack = suppress_default_ack or decision.suppress_default_ack
return suppress_default_ack
def _send_module_decision(self, decision: ModuleDecision) -> None:
for tx in decision.tx:
if tx.delay > 0:
time.sleep(tx.delay)
self.link.send(tx.frame, tx.label)
self.stats.module_frames += 1
self.stats.tx_frames += 1
def _service_refresh(self) -> None:
for frame in self.refresh.due_frames():
self.link.send(frame, "refresh")
self.stats.refresh_frames += 1
self.stats.tx_frames += 1
def _record_rx(self, item: RxFrame) -> None:
self.stats.rx_frames += 1
def _emit_summary(self) -> None:
self.logger.emit()
self.logger.emit("CCU Summary")
self.logger.emit(f"elapsed={self.stats.elapsed:.3f}s")
self.logger.emit(f"rx_frames={self.stats.rx_frames}")
self.logger.emit(f"tx_frames={self.stats.tx_frames}")
self.logger.emit(f"ack_frames={self.stats.ack_frames}")
self.logger.emit(f"module_frames={self.stats.module_frames}")
self.logger.emit(f"seed_frames={self.stats.seed_frames}")
self.logger.emit(f"refresh_frames={self.stats.refresh_frames}")
self.logger.emit(f"resync_events={self.link.detector.resync_events}")
self.logger.emit(f"dropped_bytes={self.link.detector.dropped_bytes}")
for label, count in sorted(self.link.detector.labels.items()):
self.logger.emit(f"{label}={count}")

59
ccu_emulator/frames.py Normal file
View File

@@ -0,0 +1,59 @@
from __future__ import annotations
from h8536.bench_connect_lcd import format_frame, frame_checksum, frame_checksum_ok, parse_frame
FRAME_LENGTH = 6
HEARTBEAT_FRAME = bytes.fromhex("0000000080DA")
# Command 0, selector 0, value 0x8080. This seeds E000/E800 selector zero.
ACTIVE_SEED_COMMAND0 = bytes.fromhex("00000080805A")
# The older bench cadence sequence. It is still useful as an optional wake-up
# strategy because the real panel proved timing-sensitive around these frames.
CONNECT_CADENCE_SEQUENCE = (
bytes.fromhex("04000040001E"),
bytes.fromhex("0400008000DE"),
bytes.fromhex("040000C0009E"),
)
# Command 5, selector 0x0040, value ignored. ROM trace shows this is the safest
# neutral report-consume candidate.
NEUTRAL_ACK_FRAME = bytes.fromhex("05004000001F")
def build_frame(command: int, selector: int, value: int) -> bytes:
"""Build a six-byte host frame for simple page-0/page-1 selectors.
This helper covers the selector encodings we currently use in fake-CCU
probes. Keep more exotic mapping in one place when we learn it.
"""
if not 0 <= command <= 0xFF:
raise ValueError("command byte out of range")
if not 0 <= selector <= 0x01FF:
raise ValueError("selector out of supported range 0x000-0x1FF")
if not 0 <= value <= 0xFFFF:
raise ValueError("value out of range")
if selector <= 0x007F:
byte1 = 0x00
byte2 = selector
elif selector <= 0x017F:
byte1 = 0x01
byte2 = selector - 0x0080
else:
byte1 = 0x02
byte2 = selector - 0x0180
frame = bytes(
[
command & 0xFF,
byte1 & 0xFF,
byte2 & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF,
]
)
return frame + bytes([frame_checksum(frame)])

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
from dataclasses import dataclass
from .frames import build_frame, frame_checksum_ok
from .modules import ModuleDecision, ModuleTx
SELECTOR_IRIS_MBLACK_LINK = 0x0013
IRIS_MBLACK_LINK_CLEAR = 0x0000
IRIS_MBLACK_LINK_ACTIVE = 0x4000
def selector_from_frame(frame: bytes) -> int | None:
if len(frame) != 6:
return None
if frame[1] == 0x00:
return frame[2]
if frame[1] == 0x01:
return 0x0080 + frame[2]
if frame[1] == 0x02:
return 0x0180 + frame[2]
return None
def value_from_frame(frame: bytes) -> int | None:
if len(frame) != 6:
return None
return ((frame[3] << 8) | frame[4]) & 0xFFFF
@dataclass
class IrisMblackLinkModule:
"""Closed-loop CCU side for the IRIS/M.BLACK LINK button/report path."""
mirror_delay: float = 0.050
report_commands: frozenset[int] = frozenset({0x00, 0x01, 0x02})
handled_values: frozenset[int] = frozenset({IRIS_MBLACK_LINK_CLEAR, IRIS_MBLACK_LINK_ACTIVE})
name: str = "iris_mblack_link"
def on_rx(self, frame: bytes, label: str = "") -> ModuleDecision | None:
if not frame_checksum_ok(frame) or frame[0] not in self.report_commands:
return None
selector = selector_from_frame(frame)
if selector != SELECTOR_IRIS_MBLACK_LINK:
return None
value = value_from_frame(frame)
if value not in self.handled_values:
return None
state = "active" if value == IRIS_MBLACK_LINK_ACTIVE else "clear"
ack = build_frame(0x05, SELECTOR_IRIS_MBLACK_LINK, 0x0000)
mirror = build_frame(0x00, SELECTOR_IRIS_MBLACK_LINK, value)
return ModuleDecision(
tx=(
ModuleTx(ack, f"{self.name} ack selector=0x0013 value=0x{value:04X}"),
ModuleTx(
mirror,
f"{self.name} mirror {state} selector=0x0013 value=0x{value:04X}",
delay=self.mirror_delay,
),
),
suppress_default_ack=True,
reason=f"{self.name}_{state}_report",
)

25
ccu_emulator/modules.py Normal file
View File

@@ -0,0 +1,25 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class ModuleTx:
frame: bytes
label: str
delay: float = 0.0
@dataclass(frozen=True)
class ModuleDecision:
tx: tuple[ModuleTx, ...] = ()
suppress_default_ack: bool = False
reason: str = ""
class CcuModule(Protocol):
name: str
def on_rx(self, frame: bytes, label: str = "") -> ModuleDecision | None:
"""Inspect an RCP frame and optionally provide CCU response frames."""

44
ccu_emulator/policy.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from dataclasses import dataclass
from .frames import HEARTBEAT_FRAME, NEUTRAL_ACK_FRAME, frame_checksum_ok
REPORT_COMMAND_BYTES = frozenset({0x00, 0x01, 0x02})
@dataclass(frozen=True)
class AckDecision:
should_ack: bool
frame: bytes = NEUTRAL_ACK_FRAME
reason: str = ""
@dataclass(frozen=True)
class AckPolicy:
"""Decides whether an RCP-origin frame should get a continuation ACK."""
ack_frame: bytes = NEUTRAL_ACK_FRAME
ack_reports: bool = True
ack_heartbeats: bool = True
ack_unlabeled_checksum_frames: bool = True
def decide(self, frame: bytes, label: str = "") -> AckDecision:
if not frame_checksum_ok(frame):
return AckDecision(False, self.ack_frame, "checksum_bad")
if frame == HEARTBEAT_FRAME:
if self.ack_heartbeats:
return AckDecision(True, self.ack_frame, "heartbeat_report")
return AckDecision(False, self.ack_frame, "heartbeat_ignored")
if frame[0] in REPORT_COMMAND_BYTES:
if self.ack_reports:
return AckDecision(True, self.ack_frame, f"report_cmd_{frame[0]:02X}")
return AckDecision(False, self.ack_frame, "reports_disabled")
if label == "checksum_ok_unlabeled" and self.ack_unlabeled_checksum_frames:
return AckDecision(True, self.ack_frame, "unlabeled_checksum_ok")
return AckDecision(False, self.ack_frame, f"non_report_cmd_{frame[0]:02X}")

41
ccu_emulator/refresh.py Normal file
View File

@@ -0,0 +1,41 @@
from __future__ import annotations
import time
from dataclasses import dataclass, field
@dataclass
class PeriodicRefresh:
"""Small scheduler for optional CCU state-refresh frames."""
frames: list[bytes] = field(default_factory=list)
interval: float = 0.0
_next_due: float | None = None
_index: int = 0
@property
def enabled(self) -> bool:
return bool(self.frames) and self.interval > 0
def start(self, now: float | None = None) -> None:
if not self.enabled:
self._next_due = None
return
self._next_due = (time.monotonic() if now is None else now) + self.interval
def due_frames(self, now: float | None = None) -> list[bytes]:
if not self.enabled:
return []
current = time.monotonic() if now is None else now
if self._next_due is None:
self._next_due = current + self.interval
return []
if current < self._next_due:
return []
frame = self.frames[self._index % len(self.frames)]
self._index += 1
while self._next_due <= current:
self._next_due += self.interval
return [frame]

View File

@@ -0,0 +1,67 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterable
from h8536.bench_connect_lcd import BenchLogger, FrameDetector, format_frame, label_frame
@dataclass(frozen=True)
class RxFrame:
frame: bytes
label: str
class SerialLink:
"""Thin serial-port wrapper with checksum-resync frame detection."""
def __init__(
self,
device: Any,
logger: BenchLogger,
*,
sync_mode: str = "checksum",
) -> None:
self.device = device
self.logger = logger
self.detector = FrameDetector(sync_mode=sync_mode)
def reset_input(self) -> None:
self.device.reset_input_buffer()
self.detector = FrameDetector(sync_mode=self.detector.sync_mode)
def read_available(self) -> list[RxFrame]:
waiting = getattr(self.device, "in_waiting", 0)
data = self.device.read(waiting or 1)
if not data:
return []
dropped_before = self.detector.dropped_bytes
self.logger.chunk("RX", data)
frames = [RxFrame(frame, label) for frame, label in self.detector.feed(data)]
for item in frames:
self.logger.event(f"DETECT {item.label} {format_frame(item.frame)}")
dropped_now = self.detector.dropped_bytes - dropped_before
if dropped_now:
self.logger.event(
f"RESYNC dropped_bytes={dropped_now} total_dropped={self.detector.dropped_bytes} "
f"buffered={len(self.detector.buffer)}"
)
return frames
def send(self, frame: bytes, label: str) -> None:
self.device.write(frame)
self.device.flush()
self.logger.chunk("TX", frame)
self.logger.event(f"SENT {label} {format_frame(frame)}")
def labels(self) -> dict[str, int]:
return dict(self.detector.labels)
def label_for_frame(frame: bytes) -> str:
return label_frame(frame)
def format_frames(frames: Iterable[bytes]) -> str:
return " | ".join(format_frame(frame) for frame in frames)

View File

@@ -0,0 +1,101 @@
# PT2 Button Report Bench Plan
Date: 2026-05-26
## Question
After the SHUTTER ON/OFF test exposed queued frames:
```text
02 01 0F 80 00 D6
01 01 0F 80 00 D5
```
the next bench question is whether other buttons share the same common queue-service gate, or whether each button/selector needs its own feature gate.
## Current ROM Model
The known button ROM trace says many physical buttons share this front-door path:
```text
panel byte snapshot -> shadow byte -> dirty bit -> loc_1C0E jump table -> handler -> loc_3E54 report builder
```
CALL and CAM POWER use this path, and SHUTTER ON/OFF now appears to use it too once the CCU side services the queued report stream.
The handlers diverge after the front door. Some directly build reports. Others depend on session/menu/feature state, especially secondary-table `E400` bits.
## Test Method
Run a no-button baseline first:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\button-report-common-gate-baseline.json --parity E --quiet-console --log captures\button-common-baseline.txt --result-json captures\button-common-baseline-result.json
```
Then run one physical button candidate through the same common gate:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\button-report-common-gate-press.json --parity E --quiet-console --log captures\button-common-SHORTNAME.txt --result-json captures\button-common-SHORTNAME-result.json
```
Compare the result JSONs:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario_compare.py captures\button-common-baseline-result.json captures\button-common-SHORTNAME-result.json --show-labels
```
If the candidate run has extra ACK-target frames, decode their selector/value. That suggests the button shares the common queue-service gate.
## Broad-Gate Fallback
If common-gate produces no extra selector for a button that should plausibly report, try the broader secondary-gate run:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\button-report-broad-gates-press.json --parity E --quiet-console --log captures\button-broad-SHORTNAME.txt --result-json captures\button-broad-SHORTNAME-result.json
```
Compare it against the same baseline:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario_compare.py captures\button-common-baseline-result.json captures\button-broad-SHORTNAME-result.json --show-labels
```
If a button only emits in the broad-gate run, it likely needs a selector-specific feature/report gate rather than only the common queue-service gate.
## Practical Candidate Order
Start with known or visually obvious controls:
- SHUTTER ON/OFF: positive control, should reproduce selector `0x008F` value `0x8000`.
- CAM POWER: known direct selector `0x0007`, useful sanity check.
- CALL: known direct selector `0x0015`, but it can emit outside active state so treat it as a control.
- KNEE AUTO, DETAIL, BARS, WHITE BALANCE, BLACK/FLARE, and OTHER: useful unknowns because prior display/lamp tests suggest nearby ROM handlers and selector state.
Run one physical button per capture. The queue emits background reports, so button identity is much easier to recover by comparing against the no-button baseline than by reading one log in isolation.
## First Common-Gate Results
The first common-gate bench set compared each one-button run against `button-common-baseline`.
Positive results:
| Button | Extra queued report(s) | Current meaning |
| --- | --- | --- |
| BARS | `01/02 00 17 80 00`, `01/02 00 18 80 00` | BARS shares the common queue-service gate. This matches the ROM handler that emits selectors `0x0017` and `0x0018`. |
| IRIS AUTO | `01/02 01 1A 08 00` | IRIS AUTO shares the common queue-service gate. This matches selector `0x009A` in the ROM button trace. |
No extra ACK-target frames were found in these common-gate runs:
- STANDARD
- MASTER
- SLAVE
- KNEE AUTO
- BLACK/FLARE FLARE
- OTHERS
Interpretation: at least some local controls use the same queue-service gate as SHUTTER ON/OFF, but not every physical button emits through this simple state. The silent candidates may need a different secondary/menu/session gate, may be display-only in this mode, or may not have been captured as a single edge event.
OTHERS is now treated separately in `pt2-menu-state-machine.md` because the ROM evidence points to a local menu/display key rather than a simple report button.
Note: the result JSONs preserved the per-button summaries, but the test log path was reused as `button-common-SHORTNAME.txt` for the candidate runs. For future runs, replace `SHORTNAME` in both `--log` and `--result-json` so the full timeline is preserved per button.

View File

@@ -0,0 +1,209 @@
# PT2 Continuation Command Trace
This note enumerates the command paths that are active after the ROM sees `FAA2 != 0`.
## Dispatcher Shape
RX command dispatch starts after six-byte frame validation at `H'BBD6-H'BC67`.
Important gates:
- `F9C3 == 6`.
- SCI/RX physical error latch `FAA4.7 == 0`.
- Checksum matches `0x5A ^ F860 ^ F861 ^ F862 ^ F863 ^ F864 == F865`.
- Command number is `F860 & 0x07`.
- `F861.7` rejects normal command handling.
- `FAA2 == 0` enters the initial dispatcher.
- `FAA2 != 0` enters the continuation dispatcher.
Continuation split:
| Incoming command bits | Path | Meaning |
| --- | --- | --- |
| `command & 0x04 != 0` | `H'BC3A-H'BC56` | explicit continuation commands `4/5/6/7` |
| `command & 0x04 == 0`, `FAA2.3 == 0` | `H'BC5C -> H'BE6F` | ignored/returns |
| `command & 0x04 == 0`, `FAA2.3 == 1` | `H'BC5C-H'BC67 -> H'BC15` | clears queued-report continuation bit and re-enters initial command handling |
That last row is easy to miss: command `0/1/2/3` can consume the report-continuation latch when `FAA2.3` was set by an autonomous report, then command `0/1/2` can run as an initial command. Command `3` still has no recognized initial handler and falls to the clear/return path.
Selector bytes are packed from `F861:F862`, mapped by `loc_622B`, then doubled into `R4 = selector * 2` before the command handlers use the selector as a word-table offset.
## Explicit Continuation Commands
| Command | Handler | Table writes | Queue/report behavior | Cleanup |
| --- | --- | --- | --- | --- |
| `4` | `H'BD0E` | writes `E000`; selector zero also writes `E800`; sets `EC00.7` | calls `BE70` to append selector to `F970`; if `FAA2.3` set, advances `F9B5` | clears `FAA3`, clears `FAA2` |
| `5` | `H'BD80` | none | special selectors may append to `F970` or clear latches; if `FAA2.3` set, advances `F9B5` | clears `FAA3`, clears `FAA2` |
| `6` | `H'BDDB` | writes `E400`; sets `EC00.6` | does not append to `F970`; if `FAA2.3` set, advances `F9B5` | clears `FAA3`, clears `FAA2` |
| `7` | `H'BE05` | none | copies previous finalized TX frame from `F858-F85C` to `F850-F854` and retransmits | does not clear `FAA2/FAA3` in this handler |
## `BE70`: Selector Processing Queue Append
`BE70` is the shared helper used by command `0`, command `4`, and selected command-`5` cases.
Path `H'BE70-H'BE9D`:
- Scans the `F970` word ring from consumer cursor `F9B9` to producer cursor `F9B4`.
- If the selector is already present, returns without appending a duplicate.
- If absent, writes the selector to `F970 + 2*F9B4`.
- Increments `F9B4` and clears bit 5, making the queue a 32-entry ring.
Practical meaning:
- `F970` means "process this selector internally".
- It is separate from the serial-visible report queue at `F870`.
- Re-sending the same selector may refresh table values without creating another queued selector entry if the old entry has not been consumed yet.
## Command 4: Continuation Primary Write
Selector zero branch:
- `H'BD12-H'BD18`: builds value from `F863` and forces low byte `0x80`.
- `H'BD1A`: writes `E000[0]`.
- `H'BD1E`: writes `E800[0]`.
- `H'BD22`: sets `EC00[0].7`.
- `H'BD26`: calls `BE70`, appending selector zero to `F970`.
Nonzero selector branch:
- `H'BD2B-H'BD35`: builds value from `F863/F864` and writes `E000[selector]`.
- `H'BD39`: sets `EC00[selector].7`.
- `H'BD3D-H'BD45`: if the selector has a mapped shadow slot, writes the value into the `F400` shadow area.
- `H'BD49-H'BD5F`: if `F76E.7` is set, uses the mapped shadow offset plus the `F76E` page nibble and calls `BFE0`, the EEPROM/persistent-write path.
- `H'BD64`: calls `BE70`, appending the selector to `F970`.
Persistent-write detail:
- If the selector-to-shadow map entry is zero, the mirror/persist path is skipped.
- `BFE0` seeds retry timer `F840=0x0A`, writes via the P9/X24164 helper path, reads back for verification, and sets `F841.7` on timeout/failure.
Report consumption:
- `H'BD67-H'BD71`: if `FAA2.3` was set by a queued report, increments `F9B5` and clears `F9B5.7`.
- `H'BD75-H'BD79`: clears `FAA3` and `FAA2`.
Practical meaning:
- Command `4` is a continuation write/update, not a generic always-live command.
- Command `4` selector zero can refresh both the primary/current table and queue the CONNECT selector.
- Nonzero command `4` does not directly refresh `E800`; autonomous report values still come from the older/current `E800` value unless another path updates it.
## Command 5: ACK-Like / Special Selector Command
Command `5` reaches `H'BD80` only from the continuation dispatcher:
- `FAA2 != 0`.
- `command & 0x04 != 0`.
- `F861.7 == 0`.
- `F860 & 0x07 == 5`.
Special enqueue selectors:
| Selector | Branch | Later selector dispatch | Current meaning |
| --- | --- | --- | --- |
| `0x006C` | `H'BD80 -> H'BDBF` | `H'2FAF` | COPY completion/exit sibling when copy flags are live |
| `0x006D` | `H'BD85 -> H'BDBF` | `H'3015` | COPY in-progress/start refresh |
| `0x006E` | `H'BD8A/H'BD8F -> H'BDBF` | `H'2CA6` | special-accepted but selector dispatch is currently no-op |
The duplicate `0x006E` compare at `H'BD8A` and `H'BD8F` appears redundant in the current decode.
Selector-table math for the special group:
- Table base is `H'28A6`.
- `0x006C` indexes `H'297E`, whose pointer is `H'2FAF`.
- `0x006D` indexes `H'2980`, whose pointer is `H'3015`.
- `0x006E` indexes `H'2982`, whose pointer is `H'2CA6`.
- `H'2CA6` clears `F769.7` and returns.
Latch-clear selectors:
| Selector | Gate | Effect |
| --- | --- | --- |
| `0x006B` | only if `F731.7` set | clears `F731.7` and `F790.7` |
| `0x0096` | only if `F731.7` set | clears `F731.7` and `F790.7` |
| `0x0097` | only if `F731.7` set | clears `F731.7` and `F790.7` |
| `0x00C6` | only if `F731.7` set | clears `F731.7` and `F790.7` |
| `0x00F8` | only if `F731.7` set | clears `F731.7` and `F790.7` |
Report consumption and cleanup:
- `H'BDC2-H'BDCC`: if `FAA2.3` was set by a queued report, increments `F9B5` and clears `F9B5.7`.
- `H'BDD0-H'BDD4`: clears `FAA3` and `FAA2`.
Practical meaning:
- Command `5` is the closest thing to an ACK, but only inside the continuation side.
- Most command-5 selectors do not have selector-specific meaning; they just consume/clear the continuation state if one is live.
- `0x006C/0x006D/0x006E` are command-5 special cases because they enter the `F970` selector-processing queue.
- `0x006E` is accepted as special by the command handler, but its current selector-table target is `2CA6`, the no-op return path.
## Command 6: Continuation Secondary Write
Path `H'BDDB-H'BE03`:
- `H'BDDB-H'BDE5`: builds value from `F863/F864` and writes `E400[selector]`.
- `H'BDE9`: sets `EC00[selector].6`.
- `H'BDED-H'BDF7`: if `FAA2.3` was set by a queued report, increments `F9B5` and clears `F9B5.7`.
- `H'BDFB-H'BDFF`: clears `FAA3` and `FAA2`.
Practical meaning:
- Command `6` is the secondary feature/status table write used by menu visibility and gate code.
- It does not append the selector to `F970`, so it changes later decisions but does not itself force immediate selector dispatch.
## Command 7: Retransmit
Path `H'BE05-H'BE25`:
- Copies previous finalized TX bytes `F858-F85C` into staging bytes `F850-F854`.
- Loads `F9C0=0x1F`.
- Calls `BA26` to finalize/send again.
- `BA26` then waits for `F9C0 == 0`, reloads `F9C0=0x64`, reloads `F9C4=0x07`, copies staging bytes back to `F858-F85C`, recomputes the checksum into `F85D`, and starts SCI1 TX.
Practical meaning:
- Command `7` repeats the previous finalized TX frame.
- It is valid from both initial and continuation dispatch.
- It does not acknowledge/consume `F870` report queue entries by itself and does not clear `FAA2/FAA3` in this handler.
- The copied old checksum byte is not trusted; `BA26` recomputes the checksum.
## Distinct `07` Error Echo Path
The ROM also emits `07...` frames from an error/retry path that is separate from explicit command `7`.
Triggers:
- SCI/RX physical error latched in `FAA4.7`.
- Checksum mismatch at `H'BBF0`.
Path `H'BE29-H'BE6A`:
- Clears `FAA4.7`.
- Requires `FAA5.7`; if the session gate is absent, no echo is sent.
- Increments retry counter `FAA6`.
- For the first two retry attempts, stages:
```text
byte0 = 0x07
byte1 = RX[1]
byte2 = RX[2]
byte3 = RX[3]
byte4 = RX[4]
```
- Calls `BA26` to send the error echo.
- After too many retries, loads `F9C0=0x1F` and clears `FAA3/FAA2` instead of sending another echo.
Practical meaning:
- A `07 ...` frame is not automatically a normal command-7 retransmit.
- Old captures made with the wrong serial parity can easily be retry/error echoes.
- Valid frames clear `FAA6` at `H'BBF3`.
## Bench Implications
- To update active selector state during a report window, use command `4` or `6` while `FAA2 != 0`.
- To consume a queued report without changing tables, command `5` is the cleanest continuation ACK shape.
- To trigger COPY side effects, command `5` with `0x006D` starts/refreshes `COPY IN PROGRESS`, and command `5` with `0x006C` completes/exits only while the copy flags are live.
- Do not treat command `7` as an ACK. It is a repeat probe.
- A low command `0/1/2` sent while `FAA2.3` is live may clear the report continuation and then run as an initial command, which can make timing-sensitive bench traces look like an ACK plus a new command in one frame.

View File

@@ -0,0 +1,154 @@
# PT2 Copy State Machine
This is a focused reference for the COPY behavior seen on the RCP LCD and traced in the ROM.
## Known Entry Points
### Serial Start / Progress
Frame:
```text
05 00 6D 00 00 32
00 00 6D 00 00 37 ; family-00 set/queue form to test
```
ROM path:
- Command 5 accepts selector `0x006D` at `BD80-BDBF`.
- The selector is queued through `BE70`.
- Selector `0x006D` dispatches to `H'3015`.
Observed effects from forced decode:
- Sets `F731.7`.
- Sets `F795.6/F795.7`.
- Loads `F798=H'C8`.
- Sets display selector `F732=H'1903`.
- Sets `FB02=H'64`.
- Calls the display/report bridge at `48FA`.
- Sets `F76E.6`.
LCD dispatch:
- `F732` high byte `0x19` selects `493E[0x19] -> H'930A`.
- The local table at `H'931C` maps substate `0x03` to `H'9F6A`.
- `H'9F6A` builds `COPY` / `IN PROGRESS`.
### Serial Complete / Exit
Frame:
```text
05 00 6C 00 00 33
00 00 6C 00 00 36 ; family-00 set/queue form observed during active report drain
```
ROM path:
- Command 5 accepts selector `0x006C` at `BD80-BDBF`.
- The selector is queued through `BE70`.
- Selector `0x006C` dispatches to `H'2FAF`.
Observed effects from forced decode:
- Manipulates `F76E`, `F795`, `F797`, and `F799`.
- Can set display selector `F732=H'1904`.
- Sets `FB02=H'14`.
- Calls the display/report bridge at `48FA`.
LCD dispatch:
- `F732` high byte `0x19` selects `493E[0x19] -> H'930A`.
- The local table at `H'931C` maps substate `0x04` to `H'9FDA`.
- `H'9FDA` builds `COPY` / `COMPLETED`.
### RCP-Side Menu Start
ROM path:
- OTHERS menu page: `493E[0x01] -> H'631C`.
- Local page table: `H'632E`.
- `COPY TO SLAVES` entry handler: `H'6FF0`.
Required gates:
- The entry descriptor before `H'6FF0` requires `E400[0x0015] != 0`.
- The local COPY action branch requires `F770.2` and `F791.7`.
Local branch effects:
- Sets `F76E.6`.
- Sets `F795.7`.
- Sets `F731.7`.
- Loads `F798=H'C8`.
- Sets `F711.7`.
- Loads `F726=H'64`.
- Calls `loc_5500`.
- Displays `COPY TO SLAVES`.
If `F770.2` is set while `F791.7` is clear, the ROM diverts through `H'704C` to a `SET RCP` / `MASTER` display path instead of starting copy.
## Working State Model
| State | Likely indicators | Entry | Exit |
| --- | --- | --- | --- |
| Idle / no copy | `F731.7` clear | Boot, timeout, or completion cleanup | `0x006D` or local COPY branch |
| Copy in progress | `F731.7`, `F795.6/F795.7`, `F798` live, `F732=H'1903` | `05 00 6D 00 00 32` or local COPY branch | `0x006C` in the live window, or timer expiry |
| Copy completed | `F732=H'1904` | `05 00 6C 00 00 33` while copy flags are live | Display/session timeout or next state update |
| Timeout / not active | `F731.7` cleared by timer path | `F797` or `F798` reaches zero | Normal CONNECT recovery traffic |
## Bench Results
Observed on the real panel:
```text
006C alone -> CONNECT OK -> blank
006D alone -> CONNECT OK -> COPY IN PROGRESS -> CONNECT NOT ACT
006D -> 006C after 250 ms -> COPY IN PROGRESS -> COPY COMPLETED
006D -> 006C after 1.0-1.5 s -> COPY IN PROGRESS -> COPY COMPLETED
006D -> 006C after 2.0-2.5 s -> COPY IN PROGRESS -> CONNECT NOT ACT
006D repeated, then 006C -> COPY IN PROGRESS held longer -> COPY COMPLETED
006D repeated without 006C -> COPY IN PROGRESS -> CONNECT NOT ACT
```
Family-00 comparison run on 2026-05-27:
```text
00 00 6D 00 00 37 -> 00 00 6C 00 00 36
-> immediate 04 00 6D / 04 00 6C table readbacks
-> no command-5-like 01 00 02 / 02 00 04 response rhythm
05 00 6D 00 00 32 -> 05 00 6C 00 00 33
-> 01 00 02 responses after 006D
-> 02 00 04 responses after 006C
```
Webcam-confirmed LCD results from the same 2026-05-27 run:
```text
00 00 6D 00 00 37 -> COPY IN PROGRESS
00 00 6C 00 00 36 -> COPY COMPLETED
05 00 6D 00 00 32 -> COPY IN PROGRESS
05 00 6C 00 00 33 -> COPY COMPLETED
```
The 250 ms and 1000 ms gap variants both produced the same LCD sequence. This
means the family-00 selector/write form can reach the COPY LCD side effects, even
though its serial response rhythm is the table-readback rhythm rather than the
command-5 `01 00 02` / `02 00 04` rhythm.
Current interpretation:
- `0x006D` is a copy-start/progress-window refresh selector.
- `0x006C` is a completion/exit selector that only behaves cleanly while the copy window is live.
- The observed `00 00 6C 00 00 36` frame can display `COPY COMPLETED` when it is sent inside a live copy window, but should not be read as a stateless "show completed" command.
- The copy window is transient and timer-controlled.
- The panel does not treat `0x006C` as a stateless "show completed" command.
## Open Questions
- What sets `F791.7` during normal CCU/RCP operation?
- What exact official PT2 name belongs to selectors `0x006C`, `0x006D`, and `0x006E`?
- Whether `0x006E` is a copy cancel/error sibling or an unrelated special selector.
- Whether the CCU sends repeated progress refreshes during a real COPY TO SLAVES operation.

View File

@@ -0,0 +1,78 @@
# PT2 IRIS/M.BLACK LINK ROM Trace
This note records the ROM evidence for the bench-visible
`IRIS/M.BLACK LINK` lamp.
## Confirmed Host Trigger
The isolated bench trigger is:
```text
00 00 13 40 00 09 ; command 0, selector 0x0013, value 0x4000
```
Command 0 mirrors nonzero selector writes into both `E000[selector]` and
`E800[selector]`, then queues the selector for internal processing. For this
frame that means:
```text
E000[0x0013] = 0x4000
E800[0x0013] = 0x4000
```
The current-table word is `E800 + 2 * 0x0013 = H'E826`.
## Selector Handler
The selector dispatch table maps selector `0x0013` to `H'2E06`.
Focused linear decode:
```text
H'2E06 BTST.W #15, @H'E826
set/clear F791.6 and F713.4
H'2E1E BTST.W #14, @H'E826
set/clear F791.5 and F716.7
```
Bench labels:
| Selector value | ROM latch bits | Visible result |
| --- | --- | --- |
| `0x8000` | `F791.6`, `F713.4` | far-right `SLAVE` lamp |
| `0x4000` | `F791.5`, `F716.7` | `IRIS/M.BLACK LINK` lamp |
| `0x0000` | clears both bit groups through `H'2E06` | both latch groups clear |
## Other Trigger Path
There is also a local panel-input path:
```text
F006.7 / F6DB.7 -> H'200E -> H'E826 bit14 -> loc_3E54 queues selector 0x0013
```
At `H'200E`, the ROM checks `F6DB.7` and requires `F731 <= 3`. It then uses
`F791.5` as a current-state toggle:
- if `F791.5` is clear, it sets `H'E826.14`,
- if `F791.5` is set, it clears `H'E826.14`,
- in both cases it calls `loc_3E54` with selector `0x0013`.
This is not a separate lamp driver. It feeds the same `E800[0x0013].14`
state consumed by the selector handler.
## Practical Meaning
The strongest current model is:
- CCU/host can drive the lamp directly by sending selector `0x0013` value
`0x4000` through command 0.
- The panel can also toggle/report the same state through a local input lane,
but only when the relevant session/page gate is open.
- No other decoded selector currently appears to directly set the same
`F791.5` plus `F716.7` latch pair.
Generated semantics now label selector `0x0013`, `E800[0x0013]`, and the
`IRIS/M.BLACK LINK` / `SLAVE` bit meanings so pseudo-code output does not leave
this as a generic table write.

View File

@@ -0,0 +1,193 @@
# PT2 IRIS/M.BLACK LINK State Machine
Date: 2026-05-27
This note records the bench-proven closed loop for the `IRIS/M.BLACK LINK`
button/lamp path.
## Short Answer
There is no current evidence that the CCU first sends a separate
"this function exists" capability command for `IRIS/M.BLACK LINK`.
The stronger model is:
1. The CCU/RCP session must be awake/active.
2. The CCU must service the RCP report queue.
3. The CCU is the authoritative owner of selector state.
4. When the RCP reports a local button intent, the CCU ACKs the report and
mirrors the resulting selector value back to the RCP.
For this control, selector `0x0013` bit `0x4000` is the
`IRIS/M.BLACK LINK` state.
## Proven Frames
Selector value frames:
```text
00 00 13 40 00 09 ; command 0, selector 0x0013, value 0x4000, active
00 00 13 00 00 49 ; command 0, selector 0x0013, value 0x0000, clear
```
Report ACK:
```text
05 00 13 00 00 4C ; command 5 ACK/continuation for selector 0x0013
```
Readback request:
```text
01 00 13 00 00 48 ; command 1 read selector 0x0013
```
Observed readback shapes:
```text
04 00 13 40 00 0D ; command-0 write response, selector 0x0013 active
04 00 13 00 00 4D ; command-0 write response, selector 0x0013 clear
04 13 00 40 00 0D ; command-1 readback response, selector 0x0013 active
04 13 00 00 00 4D ; command-1 readback response, selector 0x0013 clear
```
## Successful Closed Loop
Capture:
```text
captures/iris-mblack-link-mirror-state-machine.txt
captures/iris-mblack-link-mirror-state-machine-result.json
```
Scenario:
```text
scenarios/iris-mblack-link-mirror-state-machine.json
```
The visible panel behavior was:
```text
press 1: lamp on
press 2: lamp off
press 3: lamp on
```
The serial behavior matched that cycle.
### Baseline Clear
```text
TX 00 00 13 00 00 49
RX 04 00 13 00 00 4D
```
### Press 1: Active
```text
RX 00 00 13 40 00 09
TX 05 00 13 00 00 4C ; ACK report
TX 00 00 13 40 00 09 ; mirror active back
RX 04 00 13 40 00 0D
TX 01 00 13 00 00 48 ; readback
RX 04 13 00 40 00 0D
```
### Press 2: Clear
```text
RX 00 00 13 00 00 49
TX 05 00 13 00 00 4C ; ACK report
TX 00 00 13 00 00 49 ; mirror clear back
RX 04 00 13 00 00 4D
TX 01 00 13 00 00 48 ; readback
RX 04 13 00 00 00 4D
```
### Press 3: Active Again
```text
RX 00 00 13 40 00 09
TX 05 00 13 00 00 4C
TX 00 00 13 40 00 09
RX 04 00 13 40 00 0D
TX 01 00 13 00 00 48
RX 04 13 00 40 00 0D
```
## Interpretation
The RCP does not appear to treat the local button press as final local state.
It reports local intent to the CCU.
The CCU then:
1. ACKs the selector report with command `5`.
2. Applies the chosen state back to the RCP with command `0`.
The RCP uses the mirrored/current selector state to decide the next toggle
direction. This explains the earlier failed/incomplete behavior:
- Without mirroring `0x0013=0x4000` back, repeated presses could keep reporting
active because the RCP still believed the selector was clear.
- Once the CCU mirrored active back, the next press reported clear.
- Once the CCU mirrored clear back, the next press reported active again.
## Wakeup Versus Capability
The successful test also had active session traffic:
```text
00 00 00 80 80 5A ; active selector-zero keepalive/report
05 00 00 00 00 5F ; ACK for selector zero
```
That traffic looks like general session/connected behavior, not a
per-function enable for `IRIS/M.BLACK LINK`.
So the current working model is:
- `CONNECT: OK` / active rhythm opens the report path and keeps the panel from
falling back to `CONNECT: NOT ACT`.
- Selector `0x0013` state tells the RCP the current value of this specific
control/lamp.
- There may still be feature-specific gates for other controls, but this test
did not require a distinct `IRIS/M.BLACK LINK exists` command.
## ROM Correlation
The ROM trace in `docs/pt2-iris-mblack-link-rom-trace.md` matches the bench
behavior:
```text
F006.7 / F6DB.7 -> H'200E -> H'E826 bit14 -> loc_3E54 queues selector 0x0013
```
At `H'200E`, the ROM reads `F791.5` as the current `IRIS/M.BLACK LINK` state:
- if `F791.5` is clear, the local press queues/set reports `0x0013=0x4000`;
- if `F791.5` is set, the local press queues/clear reports `0x0013=0x0000`.
The selector handler for `0x0013` updates `F791.5` from `E800[0x0013].14`.
That is why mirroring command-0 selector state back to the RCP completes the
toggle loop.
## Implications
For a CCU emulator:
- Maintain an authoritative selector table.
- Treat RCP button reports as requested state changes, not just notifications.
- ACK each report with command `5`.
- Write the accepted selector value back with command `0`.
- Keep the selector-zero/session rhythm alive separately.
For other latched buttons:
- The same pattern is likely: RCP emits a selector report, then expects the CCU
to mirror the accepted state back.
- A button that only reports "active" repeatedly may not be broken; it may be
waiting for the CCU to update the selector state that controls its next
toggle direction.

175
docs/pt2-knee-rom-trace.md Normal file
View File

@@ -0,0 +1,175 @@
# PT2 KNEE ROM Trace
Date: 2026-05-26
## Short Version
KNEE is not a simple held selector bit. The ROM has a dedicated local panel-input handler at `loc_1795`, reached when the panel input word at `F104` changes. That handler uses CCU-side selector state as gates, then either shows a short timed KNEE page or reports a KNEE value change on selector `0x00BC`.
This explains the bench pattern where KNEE AUTO can light or flash and then clear even while serial traffic remains healthy: one ROM path is a timed display/page override, not a maintained lamp value.
## Input Path
The local panel path is:
```text
F104 changes -> F692 updated -> F6F0.1 set -> main dispatcher calls loc_1795
```
The important code anchors are:
```text
3B33 MOV:G.W @H'F104, R0
3B37 CMP:G.W @H'F692, R0
3B3D BSET.B #1, @H'F6F0
3B41 MOV:G.W R0, @H'F692
1630 BCLR.B #1, @H'F6F0
1636 JSR @loc_1795
```
So serial writes can prepare the state table, but the main KNEE control branch appears to require the panel-side input lane to change.
## Handler Decision Table
`loc_1795` reads two words from the primary selector table:
| ROM read | Selector meaning |
| --- | --- |
| `@H'E172` | `E000[0x00B9]` |
| `@H'E220` | `E000[0x0110]` |
The branch is:
```text
1795 if F731 > 2: skip
179C if E000[0x00B9].13 == 0: loc_2127()
17A7 else if E000[0x0110].15 == 1: loc_2127()
17B2 else:
delta = F692 - F6B2
report/update selector 0x00BC via loc_19A2
17C0 F6B2 = F692
```
Practical interpretation:
| Condition | ROM effect | Current meaning |
| --- | --- | --- |
| `0x00B9.13 = 0` | timed KNEE page via `loc_2127` | KNEE value reporting not enabled |
| `0x00B9.13 = 1`, `0x0110.15 = 1` | timed KNEE page via `loc_2127` | override/inhibit mode |
| `0x00B9.13 = 1`, `0x0110.15 = 0` | value delta reported as selector `0x00BC` | likely live KNEE control/report lane |
This downgrades the earlier bench label that treated `0x00B9.15` as the main KNEE gate. The ROM gate for the panel-input handler is `0x00B9.13`.
## Timed KNEE Page
`loc_2127` is the timed page path:
```text
2127 set FB03.7
212D save old F732 into F734, if this is a fresh override
2135 F732 = 0x1C03
213B FB02 = 0x14
2140 call loc_48FA
```
The timer path later decrements `FB02`; when it expires, `loc_48EF` restores `F732` from `F734` and redraws. That is the likely cause of "lights, then falls away" observations.
The LCD dispatcher confirms `F732=0x1C03` is a KNEE page:
```text
493E[0x1C] -> 0x92CC
0x92DE[3] -> 0x95CE
0x960B prints "KNEE"
```
On that KNEE page, the second line is selected like this:
```text
if E000[0x0110].15: "DL"
else if E000[0x00B9].15: "PRESET"
else: "AUTO"
```
So `0x00B9.15` still matters, but it appears to choose the KNEE page label (`PRESET` versus `AUTO`) rather than enabling the report path. `0x0110.15` selects the `DL` label and also forces the timed page path from `loc_1795`.
## Bench Implications
Useful frames to test this model:
| Purpose | Frame |
| --- | --- |
| clear `0x00B9` | `00 01 39 00 00 62` |
| set `0x00B9.13` gate | `00 01 39 20 00 42` |
| set `0x00B9.15` label bit | `00 01 39 80 00 E2` |
| set `0x00B9.15` and `.13` | `00 01 39 A0 00 C2` |
| clear `0x0110` | `00 01 90 00 00 CB` |
| set `0x0110.15` override | `00 01 90 80 00 4B` |
| read `0x00BC` | `01 01 3C 00 00 66` |
The most direct live-control test is:
1. Reach `CONNECT: OK`.
2. Send `00 01 39 20 00 42` to set `0x00B9.13`.
3. Send `00 01 90 00 00 CB` to clear `0x0110.15`.
4. Move or exercise the physical KNEE-related control, if available.
5. Watch for TX/report traffic around selector `0x00BC`.
If no physical KNEE input is moved, the ROM may not enter `loc_1795`, because the trigger is the `F104` panel input change path rather than the serial write itself.
## Bench Observation: DTL / KNEE
The first `knee-rom-gate-and-value-probe` bench run produced a new LCD state with `DTL` on the left and `KNEE` on the right. That is an important confirmation: page `0x1C` contains both a KNEE entry and a DETAIL entry.
Relevant ROM text and page entries:
```text
0x92DE[3] -> 0x95CE KNEE page entry
0x92DE[7] -> 0x97C8 DETAIL page entry
0x960B prints "KNEE"
0x9805 prints "DETAIL"
```
The bench display probably shows the page-0x1C menu neighborhood, with DETAIL abbreviated as `DTL` and KNEE as the selected/right-side item. The log does not encode LCD pixels, but the serial timing around the observation is consistent with the KNEE probe:
```text
00 01 39 20 00 42 ; set 0x00B9.13 gate
00 01 39 A0 00 C2 ; set 0x00B9.15 + 0x00B9.13
00 01 90 80 00 4B ; set 0x0110.15 timed KNEE/DL override
```
Next isolation target: determine whether `DTL/KNEE` appears from `0x00B9.13`, `0x00B9.A0`, `0x0110.15`, or only the combined sequence.
## Panel Correlation
The physical panel has both DETAIL and KNEE buttons near the LCD. That matches the ROM page-`0x1C` neighborhood and makes the observed `DTL/KNEE` state likely to be a local menu/button context rather than a generic lamp status.
Practical implication: once the CCU-side selector gates are prepared, pressing the physical DETAIL/KNEE buttons may be the missing `F104` panel-input transition that calls `loc_1795`. If so, the useful evidence should be:
- visible LCD movement between DETAIL and KNEE entries,
- possible KNEE timed page redraws,
- TX/report traffic for selector `0x00BC` when KNEE input movement is accepted,
- or other page-`0x1C` selector reports from neighboring DETAIL/KNEE controls.
## Follow-Up: Lamp Without LCD Movement
The follow-up run after discovering the DETAIL/KNEE buttons did not reproduce an LCD page change. The observed result was:
- only the later KNEE cases/windows illuminated the KNEE AUTO lamp,
- none of those cases changed the LCD.
Current interpretation:
- `0x0110.15` is still the strongest KNEE AUTO lamp/status source.
- Serial table writes can light KNEE AUTO without necessarily entering the LCD DETAIL/KNEE page.
- The LCD path probably needs an additional local-display/menu condition, not just the CCU-side selector bits.
- The physical DETAIL/KNEE buttons may be scanned by the local panel path, but in this test they did not create a visible LCD transition or new serial report evidence.
This separates two related but distinct paths:
```text
KNEE AUTO lamp/status: mostly selector/table driven, strongest source 0x0110.15
DETAIL/KNEE LCD page: local page/menu context, likely page 0x1C plus panel/menu state
```
Next ROM target: trace the DETAIL and KNEE button scan bits through the page-`0x1C` menu dispatcher, not just the `loc_1795` KNEE value handler.

View File

@@ -0,0 +1,157 @@
# PT2 Known Button ROM Trace
This trace starts from known serial-visible button reports and walks backward into the panel input scanner.
Known reports:
- `00 00 07 80 00 DD` = selector `0x0007`, value `0x8000`, observed CAM POWER report.
- `00 00 15 80 00 CF` = selector `0x0015`, value `0x8000`, observed CALL active report.
- `00 00 15 00 00 4F` = selector `0x0015`, value `0x0000`, observed CALL inactive report.
## Shared Button Path
CALL and CAM POWER both use the same general panel edge machinery:
```text
external panel byte snapshot
-> shadow byte F6D*
-> dirty byte F6F2/F6F3
-> main scanner loc_15E0
-> bit dispatcher loc_1C0E
-> table H'2706
-> per-button handler
-> loc_3E54 serial report queue
```
That means the lamps and buttons are not just conceptually related. The ROM reads panel byte shadows and drives panel output masks through the same external panel-chip area, then uses per-button handlers to decide whether a press should become a serial report, a local menu action, or nothing.
## CAM POWER
Trace:
```text
F105 external/panel byte changes
-> F6D4 shadow changes
-> F6F2.4 dirty
-> loc_1BA0
-> loc_1C0E table slot H'274C
-> handler H'1F40
-> writes E80E
-> queues selector 0x0007 through loc_3E54
```
Important handler behavior:
```text
1F40: tests F6D4.3
1F4C: tests E000[0x0007] / E00E bit 15
1F52: writes E800[0x0007] / E80E = 0x8000
1F61: can write E80E = 0x0000 when already set and page gate allows
1F68: R3 = 0x0007
1F6B: BSR loc_3E54
```
So CAM POWER is a real local button path, not a host-only selector. The handler checks the current button level and local/session gates before emitting.
## CALL
Trace:
```text
F006 external/panel byte changes
-> F6DB shadow changes
-> F6F3.3 dirty
-> loc_1BF8
-> loc_1C0E table slot H'27C0
-> handler H'20A1
-> writes E82A
-> queues selector 0x0015 through loc_3E54
```
Important handler behavior:
```text
20A1: reads E000[0x0015] / E02A
20A5: tests F6DB.5
20AB: if pressed, sets bit 15
20AF: if released, clears bit 15
20B1: writes E800[0x0015] / E82A
20B7: R3 = 0x0015
20BA: BSR loc_3E54
```
This cleanly matches the bench readings: CALL has both active and inactive reports because the handler explicitly sets or clears bit 15 from the current button level.
## Divergence
Other buttons do follow the same input edge path, but many diverge after `loc_1C0E`:
- Some handlers are `H'1C25`, an immediate `RTS`, so that matrix position is ignored.
- Some handlers only change local menu/page state such as `F731`, `F732`, `FB03`, or `F798`.
- Some handlers queue reports, but only if session/menu gates like `F731`, `F730`, `F791`, or selector bits in `E000/E400` allow it.
- Several handlers queue different selectors such as `0x0083`, `0x008F`, `0x0093`, `0x009A`, `0x00B9`, `0x00F8`, etc.
The generated report `build/panel_button_trace.md` lists the current table-derived map.
## Bench Correlation: Common Queue Gate
The first common-gate button-report bench set used the queued-report ACK stream that exposed SHUTTER ON/OFF.
Confirmed positives:
| Physical button | Report selectors/value | ROM match |
| --- | --- | --- |
| BARS | `0x0017 = 0x8000`, `0x0018 = 0x8000` | `F105/F6D4.2 -> H'1EDE -> 0x0017/0x0018` |
| IRIS AUTO | `0x009A = 0x0800` | `F006/F6DB.3 -> H'20BE -> 0x009A` |
Common-gate runs for STANDARD, MASTER, SLAVE, KNEE AUTO, and BLACK/FLARE FLARE did not differ from the no-button baseline. That is evidence against one universal "all buttons emit now" gate. Some controls likely need additional menu/session/secondary-table state, or are not represented as simple edge reports in this mode.
## F109 Shutter/Menu Cluster
A focused trace of the `F109 -> F6D0` input byte found local-key handlers that are close to the OTHERS/shutter/menu machinery:
| Source bit | Handler | Behavior |
| --- | --- | --- |
| `F6D0.1` | `H'2390` | Queues selector `0x0083`, unless `E000[0x0088].15` diverts to timed page `F732=0x1C01` |
| `F6D0.2` | `H'2408` | Queues selector `0x0083`, unless `E000[0x0088].15` diverts to timed page `F732=0x1C01` |
| `F6D0.3` | `H'24A9` | Queues selector `0x0083`, unless `E000[0x0088].15` diverts to timed page `F732=0x1C01` |
| `F6D0.7` | `H'24E8` | Queues selector `0x008F` with local mask `0x8000` or `0x0000`; `E000[0x0088].14` diverts to timed page `F732=0x1C01` |
| `F6D0.6` | `H'252E` | Queues selector `0x008F` with local mask `0x2000` or `0x0000`; `E000[0x0088].14` diverts to timed page `F732=0x1C01` |
| `F6D0.4/F6D0.5` | `H'2574/H'25D4` | Uses selectors `0x0091/0x0092` and modal byte `F6F6`; `E000[0x0088].14` can divert to timed page `F732=0x1C01` |
This cluster explains why some adjacent controls may look like menu/display keys rather than ordinary reports. A CCU-side bit in selector `0x0088` can retarget them into a timed local page overlay, currently matching the DETAIL/KNEE neighborhood rather than the OTHERS page.
## OTHERS Difference
OTHERS page `0x01` is not driven by one of the simple confirmed report handlers above. The page-1 handlers consume low action bits in `F770`:
- `H'6EE4` consumes `F770.0/F770.1` for OTHERS/SHUTTER local actions.
- `H'6FF0` consumes `F770.2` for COPY TO SLAVES.
- No decoded direct writer for these low bits has been found yet.
That is the current best explanation for the bench result where the OTHERS physical press matched the no-button baseline: the shared scanner may see the key, but the useful OTHERS action is gated behind a separate local page/action latch.
## Emulator Probe
The emulator can inject these as ROM-level panel edges:
```powershell
.\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --wait-heartbeats 1 --panel-press cam-power
.\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --panel call=press --panel call=release --keep-listening
.\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --panel F6D4.6=press
```
This is a post-scan injection model. It sets the external source byte, the `F6D*` shadow byte, the matching `F6E*` previous-shadow byte to the opposite bit level, and the `F6F2/F6F3` dirty bit. The ROM then runs its normal `loc_15E0 -> loc_1C0E -> H'2706` dispatch path, so gates and no-op handlers still behave like firmware rather than like forced serial output.
## Tooling
Regenerate the map with:
```powershell
.\.venv\Scripts\python.exe h8536_panel_button_trace.py
```
Outputs:
- `build/panel_button_trace.md`
- `build/panel_button_trace.json`

View File

@@ -0,0 +1,241 @@
# 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` | CAM POWER lamp blink confirmed by isolated run |
| `0x0015 = 0x8000/0x0000` | CALL lamp blink confirmed; red tally also blinked in the same phase |
| `0x0012`, `0x0013`, `0x0016`, `0x0017`, `0x0018`, `0x001A` | ordered candidates for SLAVE, green tally, BARS, MASTER, and neighboring lamp states |
Follow-up `lamp-isolate-cam-call` result:
- First phase blinked CAM POWER.
- Second phase blinked CALL and red tally.
Follow-up `lamp-isolate-known-neighbors` result:
- Visible order was SLAVE, then green tally, then BARS.
- The pattern repeated, and at one point SLAVE and BARS were visible together.
- Treat the ordered mapping as likely but not final until a fresh-boot single-selector run separates latch/persistence effects from the selector under test.
Follow-up `lamp-isolate-neighbor-single-boot` result:
| Fresh-boot candidate | Visible result |
| --- | --- |
| `0x0012 = 0x8000/0x0000` | no visible change reported |
| `0x0013 = 0x8000/0x0000` | SLAVE lamp |
| `0x0016 = 0x8000/0x0000` | camera tally green |
| `0x0017 = 0x8000/0x0000` | BARS lamp |
| `0x0018 = 0x8000/0x0000` | no visible result reported yet |
| `0x001A = 0x8000/0x0000` | no visible result reported yet |
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.
Follow-up `panel-atlas-standard-master-*` webcam runs:
- `0x0012`, `0x0013`, and `0x0014` high-nibble/selected-bit sweeps did not
produce a clean STANDARD or MASTER lamp trigger. The `0x0013=0x8000` SLAVE
positive control still worked.
- `0x0010`, `0x0011`, `0x0015`, `0x0016`, `0x0017`, `0x0018`, `0x0019`, and
`0x001A` high-nibble sweeps did not produce a clean STANDARD or MASTER lamp
trigger.
- `0x0008` through `0x000F` high-nibble sweeps did not produce a clean
STANDARD or MASTER lamp trigger.
- `0x0017=0x4000` lit the same far-right bottom BARS lamp/latch as the known
`0x0017=0x8000` family. Later `0x0018` rows in that run were latch-contaminated
and need fresh-boot isolation before assigning a separate meaning.
Current implication: STANDARD and MASTER are probably not simple direct
command-0 high-nibble writes in the `0x0008`-`0x001A` pocket, despite the older
broad run that made MASTER flash. Treat that older observation as a real
bench-visible event but not yet an isolated selector mapping.
Later refinement: the ROM-derived fresh-boot output sweep found a clean
STANDARD trigger at `0x006B = 0x8000` (`00 00 6B 80 00 B1`). It also reclassed
the formerly vague `0x001A` pocket as the MONITOR selector cluster:
| Selector/value | Visible effect |
| --- | --- |
| `0x001A = 0x0808` | MONITOR ENC |
| `0x001A = 0x2020` | MONITOR B |
| `0x001A = 0x4040` | MONITOR G |
| `0x001A = 0x8080` | MONITOR R |
| `0x006B = 0x8000` | STANDARD |
### `panel-atlas-big-visual-sweep-0001-017f-highbits`
The broad webcam sweep and fresh-boot isolation pass produced these confirmed
or near-confirmed panel-output mappings:
| Selector/value | Current meaning |
| --- | --- |
| `0x0013 = 0x4000` | `IRIS/M.BLACK LINK` lamp |
| `0x0024 = 0x8000` | LCD selector-button lamp |
| `0x0024 = 0x0000` | LCD selector-button lamp remained visible at 0.5 s; not a simple clear in this timing window |
| `0x0082 = 0x8000` | IRIS readout `OP` |
| `0x0082 = 0x4000` | IRIS readout `1.4` |
| `0x0082 = 0x0000` | IRIS readout blank |
| `0x0083 = 0x8000` | MASTER GAIN `-3`, SHUTTER `OFF`, and IRIS AUTO |
| `0x0083 = 0x0000` | same visible state remained at 0.5 s; likely latched/copied elsewhere |
| `0x0093 = 0x8000` | BLACK/FLARE MANUAL plus white-balance PRESET |
| `0x0093 = 0x4000` | BLACK/FLARE MANUAL plus white-balance AUTO |
| `0x0093 = 0x2000` | BLACK/FLARE MANUAL plus white-balance MANUAL |
| `0x0093 = 0x0000` | BLACK/FLARE MANUAL plus white-balance MANUAL |
Run health was good: no resync events, no dropped bytes, and command-4
readbacks appeared for the listed writes.
ROM trace now confirms the `0x0013` bit split:
- `E800[0x0013]` is `H'E826`.
- Selector `0x0013` dispatches to `H'2E06`.
- `H'E826.15` sets/clears `F791.6` and `F713.4`, matching SLAVE.
- `H'E826.14` sets/clears `F791.5` and `F716.7`, matching
`IRIS/M.BLACK LINK`.
- Local handler `H'200E` can also toggle `H'E826.14` from panel input
`F006.7/F6DB.7` when its session gate allows it.
Interpretation:
- `0x0082` is the cleanest direct readout lane found so far: set values update
the IRIS display and `0x0000` blanks it.
- `0x0083` appears to be a combined status/display lane rather than only the
MASTER GAIN display. Its `0x8000` state also brings up SHUTTER `OFF` and IRIS
AUTO, and the clear write did not visually clear it at 0.5 s.
- `0x0093` selects white-balance mode. In this run, `0x2000` and `0x0000` both
presented white-balance MANUAL, so bit 13 may be redundant in this context or
may need another gate to show a distinct state.
### `lamp-broad-status-selector-sweep`
Visible result:
- KNEE AUTO lamp flashed a few times.
- No other new visible result was reported.
- Follow-up isolation saw KNEE AUTO toward the end of the run, then blinking.
Candidate selectors in that run:
`0x0003`, `0x0040`, `0x0081`, `0x0092`, `0x00A7`, `0x00B7`, `0x00B9`, `0x0110`
Interpretation:
- KNEE AUTO is likely in this broader status cluster.
- Because the visible change happened toward the end, strongest next candidates are `0x00A7`, `0x00B7`, `0x00B9`, and `0x0110`, with `0x0092` kept as a guard candidate.
- Exact selector/value still needs isolation because the broad sweep changed several selectors in sequence.
Follow-up `lamp-isolate-knee-tail-single-boot` result:
| Fresh-boot candidate | Visible result |
| --- | --- |
| `0x0092` | iris AUTO/OFF behavior |
| `0x00A7` | no visible result reported |
| `0x00B7` | no visible result reported |
| `0x00B9` | KNEE AUTO |
| `0x0110` | KNEE AUTO |
Interpretation:
- `0x00B9` and `0x0110` are real KNEE-related selectors, but a ROM trace now shows they are not a simple OR-held lamp pair.
- `loc_1795` is the local KNEE/panel-input handler. It is reached from `F104 -> F692 -> F6F0.1`, then reads `E000[0x00B9]` and `E000[0x0110]`.
- The ROM gate for the live KNEE value/report branch is `0x00B9.13`, not `0x00B9.15`.
- `0x0110.15` forces a timed KNEE page/display override. That fits the observed "lights, then clears" behavior.
- `0x00B9.15` still matters on the timed KNEE LCD page: it selects the `PRESET` label when `0x0110.15` is clear. With both clear, the same page labels KNEE as `AUTO`.
- When `0x00B9.13` is set and `0x0110.15` is clear, the ROM computes `F692 - F6B2` and reports/updates selector `0x00BC`. That is now the strongest candidate for the KNEE control value lane.
Follow-up `lamp-knee-or-precedence` result:
- Case 1 (`0x00B9.15` set, then `0x0110.15` set, then `0x00B9` cleared) kept KNEE AUTO on until near the end, when `0x0110` was cleared.
- Case 2 (`0x0110.15` set, then `0x00B9.15` set, then `0x0110` cleared) turned KNEE AUTO off well before the end, even though `0x00B9` had been set.
- This argues against a simple OR model. Current best interpretation: `0x0110.15` is the stronger live display/control source for KNEE AUTO; `0x00B9.15` is related, but may be a transient, secondary status source, or only meaningful with another gate active.
Follow-up `lamp-knee-sustain-compare` result:
- Repeated `0x00B9.15` refresh never lit KNEE AUTO.
- Repeated `0x0110.15` refresh lit KNEE AUTO, but it turned off again around the middle of the repeated-refresh window.
- This makes `0x00B9.15` look like a context-sensitive or stale interpretation rather than a maintained lamp source.
- `0x0110.15` remains the best KNEE AUTO source, but it is not sufficient by itself to hold the lamp. It likely needs a surrounding CCU status refresh, or another selector periodically clears/rebuilds the visible lamp bank.
Follow-up `lamp-knee-context-hold` result:
- Pairing `0x00B9.15` with the known `0x0093=0x9020` active refresh did not light KNEE AUTO.
- Pairing `0x0110.15` with `0x0093=0x9020` did light KNEE AUTO, but it still turned off around the middle of the run.
- The serial log stayed clean: repeated table readbacks and `02 00 02 00 00 5A` active responses continued, with no resync or NOT ACT-style serial collapse.
- Current best hypothesis: `0x0110.15` behaves more like an edge/pulse or consumed display request than a pure level-held lamp bit. Repeating the same high value may not retrigger it after the display task has consumed the state.
ROM follow-up:
- Detailed notes: `docs/pt2-knee-rom-trace.md`.
- The timed path sets `F732=0x1C03`, `FB02=0x14`, calls `loc_48FA`, and later restores the previous page through `loc_48EF`.
- `F732=0x1C03` dispatches to a KNEE LCD page. Its second line is `DL` when `0x0110.15` is set, `PRESET` when `0x0110.15` is clear and `0x00B9.15` is set, otherwise `AUTO`.
- `knee-rom-gate-and-value-probe` produced a new bench LCD state with `DTL` on the left and `KNEE` on the right. This matches the ROM page-0x1C neighborhood: KNEE entry at `0x95CE`, DETAIL entry at `0x97C8`.
- The panel physically has DETAIL and KNEE buttons near the LCD, so this is likely a local menu/button context. Pressing those buttons during the prepared gate window may supply the `F104` transition that the ROM needs before it calls `loc_1795`.
- Follow-up isolation lit KNEE AUTO in the later KNEE cases/windows but did not change the LCD. Treat the KNEE AUTO lamp and the DETAIL/KNEE LCD page as related but separate paths: `0x0110.15` remains the strongest lamp/status source, while LCD movement likely needs an additional local menu/display gate.
- Next bench retest should include `0x00B9.13` (`00 01 39 20 00 42`) and the `0x00BC` report/read lane, not only the older `0x00B9.15` / `0x0110.15` pair.
## 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
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-isolate-neighbor-single-boot.json --parity E --log captures\lamp-isolate-neighbor-single-boot.txt --result-json captures\lamp-isolate-neighbor-single-boot-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-isolate-knee-tail-single-boot.json --parity E --log captures\lamp-isolate-knee-tail-single-boot.txt --result-json captures\lamp-isolate-knee-tail-single-boot-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-isolate-knee-bit-scan.json --parity E --log captures\lamp-isolate-knee-bit-scan.txt --result-json captures\lamp-isolate-knee-bit-scan-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-knee-or-precedence.json --parity E --log captures\lamp-knee-or-precedence.txt --result-json captures\lamp-knee-or-precedence-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-knee-sustain-compare.json --parity E --log captures\lamp-knee-sustain-compare.txt --result-json captures\lamp-knee-sustain-compare-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-knee-context-hold.json --parity E --log captures\lamp-knee-context-hold.txt --result-json captures\lamp-knee-context-hold-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\lamp-knee-edge-refresh.json --parity E --log captures\lamp-knee-edge-refresh.txt --result-json captures\lamp-knee-edge-refresh-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\knee-rom-gate-and-value-probe.json --parity E --log captures\knee-rom-gate-and-value-probe.txt --result-json captures\knee-rom-gate-and-value-probe-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\knee-rom-dtl-knee-isolation.json --parity E --log captures\knee-rom-dtl-knee-isolation.txt --result-json captures\knee-rom-dtl-knee-isolation-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\knee-detail-physical-button-watch.json --parity E --log captures\knee-detail-physical-button-watch.txt --result-json captures\knee-detail-physical-button-watch-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.
- For the single-boot follow-ups, each candidate gets a fresh power cycle. That is deliberate: it tests whether a lamp is truly driven by that selector rather than retained from a previous write.

View File

@@ -0,0 +1,276 @@
# PT2 Menu State Machine
This is a focused reference for the ROM menu/display selection machinery that drives LCD pages and panel soft-key activity.
## Page Dispatch
Primary display selector:
- `F732` holds the current display/page selector.
- The high byte of `F732` selects a page wrapper through `493E`.
- The low byte is used as a page-local substate/selection, commonly through `F733`.
Important dispatcher:
- `48FA` bridges table/report state into LCD page dispatch.
- `493E[page]` points at a wrapper.
- Wrappers pass a local table pointer to `5FD2`.
For COPY status:
- `F732=H'1903` selects page `0x19`, substate `0x03`, `COPY IN PROGRESS`.
- `F732=H'1904` selects page `0x19`, substate `0x04`, `COPY COMPLETED`.
For OTHERS:
- `493E[0x01] -> H'631C`.
- Local table `H'632E` includes the OTHERS pages.
## Local Entry Selection
`loc_5FD2` is the local menu chooser.
Important RAM:
| RAM | Role |
| --- | --- |
| `F72C` | visible/selectable entry bitmask |
| `F72E` | count of visible/selectable entries |
| `F72F` | cached page high byte |
| `F733` | selected local entry index |
| `FB03.7` | no-entry/error/display suppression flag |
| `FB02` | display/session timer or message timer |
Observed algorithm shape:
1. If the page changed, clear `F72C/F72E` and cache the new page in `F72F`.
2. Walk the page-local handler table from the last entry down.
3. For each handler, read descriptor words immediately before the handler.
4. Compare the entry state descriptor against `F731`.
5. If the entry has required selector descriptors, test `E400 + selector*2`.
6. If requirements pass, set the entry bit in `F72C` and increment `F72E`.
7. Clamp or reset `F733` so it points at a visible entry.
8. If no entries are visible, set `FB03.7`, set `FB02=H'14`, clear `F72F`, and return `R4=H'FFFE`.
Practical meaning:
- A page can exist in ROM but be invisible/inactive until the CCU seeds the right `E400` feature/status selector.
- Command 6 writes the `E400-E7FF` secondary table, but only on the continuation side of the protocol.
## OTHERS / COPY TO SLAVES Gates
OTHERS page map:
| Local entry | Handler | Visible text | Required secondary selector |
| --- | --- | --- | --- |
| 1 | `H'6FF0` | `OTHERS` / `COPY TO SLAVES` | `E400[0x0015] != 0` |
| 2 | `H'70F6` | `OTHERS` / `CAM ID SET` | `E400[0x0043] != 0` |
| 3 | `H'7188` | `OTHERS` / `CAM ID IND` | `E400[0x0037] != 0` |
| 4 | `H'7258` | `OTHERS` / `CAM BARS` | `E400[0x0038] != 0` |
| 5-6 | `H'7328/H'73D8` | marker/percentage pages | `E400[0x0027] != 0` |
COPY TO SLAVES local action gate:
- Handler `H'6FF0` watches `F770.2`.
- If `F770.2` is clear, it only displays the OTHERS/COPY page.
- If `F770.2` is set and `F791.7` is set, it enters the local copy-start branch.
- If `F770.2` is set and `F791.7` is clear, it diverts to `SET RCP` / `MASTER`.
Root OTHERS soft-key bits:
- Root handler `H'6EE4` tests `E000[0x008F]` at `H'E11E`.
- Bit 11 sets `F711.6`.
- Bit 12 sets `F711.4`.
These bits are not only OTHERS soft-key enables. Bench tests show `E000[0x008F]` directly changes the shutter seven-segment display and iris AUTO lamp, so treat it as a packed camera/display status selector that also feeds the OTHERS root handler.
## Button / Lamp Masks
Important RAM:
| RAM | Role |
| --- | --- |
| `F711-F718` | panel output masks used by external panel chips |
| `F711.4-F711.7` | soft-key/menu-related bits seen around OTHERS/COPY |
| `F726` | countdown that temporarily preserves some soft-key bits |
| `F770` | local panel action/change code |
Relevant ROM behavior:
- Init clears `F711-F717` and sets `F718=H'FF`.
- `5A7A` clears `F711.4-F711.7` if `F726 == 0`.
- The FRT timer path decrements `F726`; when it expires, it clears `F713.6` and `F711.4-F711.7`.
- The OTHERS/COPY branch sets `F711.7` and `F726=H'64` to keep the local key/display state alive briefly.
## OTHERS Action Latch Trace
The missing OTHERS mechanism appears to be `F770`, not another simple command-0 selector.
Root OTHERS/SHUTTER handler `H'6EE4`:
- Reads `F770`, clears it, and only keeps low bits `0x01/0x02`.
- If the first action path is present and `E400[0x008F].11` is enabled, writes `E800[0x008F]=0x0800` and queues selector `0x008F`.
- If the second action path is present and `E400[0x008F].12` is enabled, writes `E800[0x008F]=0x1000` and queues selector `0x008F`.
COPY TO SLAVES handler `H'6FF0`:
- Reads `F770`, clears it, and only keeps bit `0x04`.
- If `F770.2` is clear, it only displays the OTHERS/COPY page.
- If `F770.2` is set and `F791.7` is set, it enters the local copy-start branch.
- If `F770.2` is set and `F791.7` is clear, it diverts to `SET RCP` / `MASTER` and stages restore selector `F734=0x0101`.
Static direct writers found so far only write high bits into `F770`:
| Writer family | Value | Meaning candidate |
| --- | --- | --- |
| `H'4394` | `0x80` | value/dial page redraw or change latch |
| `H'4457` | `0x40` | value/dial page redraw or change latch |
| `H'451A` | `0x20` | value/dial page redraw or change latch |
No decoded direct instruction has been found that writes `F770=0x01`, `0x02`, or `0x04`.
Updated interpretation:
- `E000[0x008F]` and `E400[0x0015]` can make OTHERS-adjacent UI state visible, but they do not synthesize the local OTHERS action.
- The physical OTHERS/COPY/menu action likely arrives through a separate local key latch or indirect panel-chip path that produces the low `F770` bits only when the page machinery is in the right context.
- Pressing OTHERS under the generic queued-report gate can therefore legitimately produce no new serial report.
## Adjacent F109 Menu/Shutter Key Trace
A focused trace of the `F109 -> F6D0` local-key handlers found a neighboring menu/shutter cluster. These are useful context because they share the same local-page style, but they do not directly prove the OTHERS low-bit latch.
| Source bit | Handler | Selector/page effect |
| --- | --- | --- |
| `F6D0.7` | `H'24E8` | Writes `E800[0x008F]=0x8000` or `0x0000`; if `E000[0x0088].14` is set, diverts to timed page `F732=0x1C01` |
| `F6D0.6` | `H'252E` | Writes `E800[0x008F]=0x2000` or `0x0000`; if `E000[0x0088].14` is set, diverts to timed page `F732=0x1C01` |
| `F6D0.4/F6D0.5` | `H'2574/H'25D4` | Uses selectors `0x0091/0x0092` and modal byte `F6F6`; if `E000[0x0088].14` is set, diverts to timed page `F732=0x1C01` |
| `F6D0.1/F6D0.2/F6D0.3` | `H'2390/H'2408/H'24A9` | Uses selector `0x0083`; if `E000[0x0088].15` is set, diverts to timed page `F732=0x1C01` |
`F732=0x1C01` is in the DETAIL/KNEE neighborhood seen in earlier traces, not the OTHERS page `0x01`. This suggests selector `0x0088` can retarget adjacent local keys into a timed menu overlay, while OTHERS itself still depends on the page-1 wrapper and the `F770` low-bit action latch.
## Bench Implications
To make the local COPY path available from the panel, the fake CCU probably needs to:
1. Recover to a live `CONNECT: OK` style session.
2. Seed root OTHERS soft-key bits:
```text
00 01 0F 18 00 4C ; E000[0x008F] bits 11+12
```
3. During a live continuation/report window, seed the COPY page visibility bit:
```text
06 00 15 00 01 48 ; E400[0x0015] nonzero
```
4. Still satisfy the `F791.7` local copy-start gate.
The current hardest unknown is step 4: the ROM uses `F791.7` in several places, but the source that sets it has not yet been identified.
## Bench Observation: OTHERS Gate Probe
Run:
```text
00 00 00 80 00 DA ; recover/seed CONNECT OK
00 01 0F 18 00 4C ; E000[0x008F] = 0x1800
06 00 15 00 01 48 ; E400[0x0015] = 0x0001, sent in active window
```
Observed on the real panel without touching the controls:
- LCD stayed at `CONNECT: OK`.
- SHUTTER seven-segment display changed to something like `EUS`; manuals make this likely `EVS` rendered on a seven-segment display.
- Iris AUTO lamp illuminated.
- OTHERS menu did not appear by itself.
Interpretation:
- The sequence reached real UI state, not only serial parser state.
- `E000[0x008F]=0x1800` is now a candidate shutter/mode-status value as well as an OTHERS soft-key source. Treat the earlier "soft-key bits" interpretation as incomplete.
- `E400[0x0015]=0x0001` may be the OTHERS/COPY visibility bit, but it may also affect an iris/auto feature path. Isolate before assigning a final meaning.
Recommended isolation probes:
```text
00 01 0F 08 00 5C ; E000[0x008F] bit 11 only
00 01 0F 10 00 44 ; E000[0x008F] bit 12 only
00 01 0F 18 00 4C ; E000[0x008F] bits 11+12
06 00 15 00 00 49 ; E400[0x0015] clear/zero
06 00 15 00 01 48 ; E400[0x0015] low nonzero
06 00 15 80 00 C9 ; E400[0x0015] high nonzero
```
Scenario files:
- `scenarios/others-isolate-008f-bit11.json`
- `scenarios/others-isolate-008f-bit12.json`
- `scenarios/others-isolate-008f-bits11-12.json`
- `scenarios/others-isolate-e400-0015-low.json`
- `scenarios/others-isolate-e400-0015-high.json`
- `scenarios/others-isolate-008f-then-e400-clear.json`
Isolation results:
| Scenario | Visible panel result | Serial result |
| --- | --- | --- |
| `others-isolate-008f-bit11` | Iris AUTO lamp on, SHUTTER seven-segment shows observed `EUS`, likely manual `EVS` | `04 01 0F 08 00 58`, then repeated `02 00 02 00 00 5A` |
| `others-isolate-008f-bit12` | Iris AUTO lamp on, SHUTTER seven-segment shows literal letters `OFF` | `04 01 0F 10 00 40`, then repeated `02 00 02 00 00 5A` |
| `others-isolate-008f-bits11-12` | Iris AUTO lamp on, SHUTTER seven-segment shows observed `EUS`, likely manual `EVS` | `04 01 0F 18 00 48`, then repeated `02 00 02 00 00 5A` |
| `others-isolate-e400-0015-low` | LCD stays `CONNECT: OK` only | repeated `01 00 02 00 00 59` after command 6 |
| `others-isolate-e400-0015-high` | LCD stays `CONNECT: OK` only | repeated `01 00 02 00 00 59` after command 6 |
| `others-isolate-008f-then-e400-clear` | Iris AUTO lamp on, SHUTTER seven-segment shows observed `EUS`, likely manual `EVS` | repeated `01 00 02 00 00 59` after command 6 |
Updated interpretation:
- `E000[0x008F]` directly affects visible shutter/iris UI state.
- Bit 11 selects the observed `EUS` shutter display, probably manual `EVS`; bit 12 selects literal shutter-display text `OFF`; when both are present, the likely `EVS` display appears to win.
- The iris AUTO lamp turns on for either bit 11 or bit 12, so it may be tied to the same status selector or to the resulting display mode.
- `E400[0x0015]` does not visibly change the panel by itself, even though command 6 does alter the report stream from `02 00 02 00 00 5A` to `01 00 02 00 00 59`.
- Keep `E400[0x0015]` as a probable OTHERS/COPY visibility/report-gate candidate, but do not assign the shutter/iris effect to it.
Manual correlation:
- The RCP-TX7 operating instructions list `OTHERS (1/6: SHUTTER)` with an `EVS` button, and say to use the shutter block `C.SCAN` or `SHUTTER ON/OFF` buttons when not using EVS.
- The same RCP-TX7 manual lists `EVS/ECS` under OTHERS for DXC-D30/D30P normal settings.
- A later CCU/RCP manual states that when EVS is on, `EVS` is displayed; when the shutter switch is off, `OFF` is displayed.
- Therefore `E000[0x008F].11` is best labeled `shutter_evs_display_or_mode`, and `E000[0x008F].12` is best labeled `shutter_off_display_or_mode` until ROM traces split display-only status from actual camera setting.
## Bench Observation: OTHERS Common-Gate Button Press
After the queued-report button tests found BARS and IRIS AUTO reports, the physical OTHERS button was tested with the same common queue-service gate.
Result:
- `button-common-OTHERS-result.json` matched `button-common-baseline-result.json`.
- No extra ACK-target frames appeared.
- The queued selector stream still included baseline selector `0x0015 = 0x0000`, but OTHERS did not turn that into a visible/new report.
Interpretation:
- OTHERS does not behave like BARS or IRIS AUTO under the simple common queue-service gate.
- This supports the ROM model that OTHERS is a local menu/display key whose useful effect depends on additional page/menu state, not just the generic report queue being serviced.
- The next fair comparison needs an OTHERS-specific no-button baseline because the OTHERS seeds themselves alter display/report state.
New focused scenarios:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\others-menu-gated-baseline.json --parity E --quiet-console --log captures\others-menu-gated-baseline.txt --result-json captures\others-menu-gated-baseline-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\others-menu-gated-press.json --parity E --quiet-console --log captures\others-menu-gated-press.txt --result-json captures\others-menu-gated-press-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario_compare.py captures\others-menu-gated-baseline-result.json captures\others-menu-gated-press-result.json --show-labels
```
Result of the first OTHERS-gated comparison:
- The current baseline rerun overwrote the accidental first baseline capture; `serial_scenario.py` writes logs/results fresh when explicit paths are reused.
- `others-menu-gated-baseline-result.json` and `others-menu-gated-press-result.json` were identical at the ACK-target level.
- Both had `rx_frames=153`, `ack_sent=64`, and no label-count increase in the press run.
- The seeded state was visible in the queued stream as `0x008F = 0x1800`, but pressing OTHERS still did not create an extra serial report.
Updated interpretation:
- The simple common queue gate is not enough for OTHERS.
- The current OTHERS-adjacent seeds are also not enough for OTHERS to emit a report.
- OTHERS is increasingly likely to be a local menu/page navigation key whose effect is visible only if the ROM has selected the right page/context (`F732/F733/F72C/F731/F770`), rather than a host-visible selector-report button like BARS or IRIS AUTO.

338
docs/pt2-panel-atlas.md Normal file
View File

@@ -0,0 +1,338 @@
# PT2 Panel Atlas
This note tracks bench tests that intentionally drive visible panel outputs from
PT2/command-0 table writes.
## Scenario Files
Current compact atlas scenarios:
```text
scenarios/panel-atlas-operator-lamps-v1.json
scenarios/panel-atlas-readout-status-v1.json
scenarios/panel-atlas-right-stack-isolation-v1.json
scenarios/panel-atlas-right-stack-fresh-latch-v1.json
scenarios/panel-atlas-standard-master-bit-sweep-v1.json
scenarios/panel-atlas-standard-master-neighbor-sweep-v2.json
scenarios/panel-atlas-standard-master-lower-neighbor-sweep-v3.json
```
Both are designed for webcam capture with the calibrated bench settings:
```text
--parity E --camera-index 4 --snapshot-delays 0.5
```
## Run: 2026-05-27
Logs:
```text
captures/panel-atlas-operator-lamps-v1-webcam.txt
captures/panel-atlas-readout-status-v1-webcam.txt
captures/panel-atlas-right-stack-isolation-v1-webcam.txt
captures/panel-atlas-right-stack-fresh-latch-v1-webcam.txt
captures/panel-atlas-standard-master-bit-sweep-v1-webcam.txt
captures/panel-atlas-standard-master-neighbor-sweep-v2-webcam.txt
captures/panel-atlas-standard-master-lower-neighbor-sweep-v3-webcam.txt
```
Snapshots:
```text
captures/panel-atlas-operator-lamps-v1-webcam-shots/
captures/panel-atlas-readout-status-v1-webcam-shots/
captures/panel-atlas-right-stack-isolation-v1-webcam-shots/
captures/panel-atlas-right-stack-fresh-latch-v1-webcam-shots/
captures/panel-atlas-standard-master-bit-sweep-v1-webcam-shots/
captures/panel-atlas-standard-master-neighbor-sweep-v2-webcam-shots/
captures/panel-atlas-standard-master-lower-neighbor-sweep-v3-webcam-shots/
```
Serial health:
| Run | RX frames | TX frames | Resync | Dropped bytes |
| --- | ---: | ---: | ---: | ---: |
| operator lamps | 82 | 19 | 0 | 0 |
| readout/status | 76 | 16 | 0 | 0 |
| right-stack isolation | 115 | 26 | 0 | 0 |
| right-stack fresh latch | 20 | 12 | 0 | 0 |
| standard/master bit sweep v1 | 144 | 34 | 0 | 0 |
| standard/master neighbor sweep v2 | 188 | 47 | 0 | 0 |
| standard/master lower-neighbor sweep v3 | 188 | 47 | 0 | 0 |
The compact and isolation runs stayed in the expected table-readback plus
`02 00 02 00 00 5A` CONNECT-OK response rhythm.
The fresh-latch run ended with one trailing unframed byte after the final send.
There were no resync events or dropped bytes; this is consistent with the run
ending while the next response frame was just beginning.
## Confirmed Visible Effects
## Far-Right Stack Reference
The physical far-right stack, top to bottom, is:
```text
TALLY light, with camera number
STANDARD
MASTER
SLAVE
CAM POWER
BARS
```
Use these names instead of generic "right-side status" labels when reviewing
webcam crops.
Readout/status run:
| Frame | Selector/value | Visible effect |
| --- | --- | --- |
| `00 01 0F 08 00 5C` | `E000[0x008F]=0x0800` | SHUTTER display shows `EUS`/likely `EVS`; iris AUTO lamp is lit |
| `00 01 0F 10 00 44` | `E000[0x008F]=0x1000` | SHUTTER display shows `OFF`; iris AUTO lamp remains lit |
| `00 01 13 80 00 C8` | `E000[0x0093]=0x8000` | white-balance / black-flare lamp cluster changes, consistent with prior preset/manual observation |
| `00 01 13 90 20 F8` | `E000[0x0093]=0x9020` | black-flare manual-context candidate remains visible |
| `00 01 13 90 FF 27` | `E000[0x0093]=0x90FF` | black-flare auto-context candidate remains visible |
| `00 01 90 80 00 4B` | `E000[0x0110]=0x8000` | KNEE AUTO lamp lights |
The `0x00B9` gate writes in this compact run did not light KNEE AUTO by
themselves at the 0.5 s snapshot point. That matches the ROM model where
`0x00B9.13` is more of a report-path gate, while `0x0110.15` is the stronger
visible KNEE AUTO source.
Operator-lamp run:
| Frame | Selector/value | Visible effect |
| --- | --- | --- |
| `00 00 13 80 00 C9` | `E000[0x0013]=0x8000` | far-right SLAVE lamp lights, based on stack order and crop position |
| `00 00 17 80 00 CD` | `E000[0x0017]=0x8000` | far-right bottom white BARS lamp lights |
| `00 00 1A 80 00 C0` | `E000[0x001A]=0x8000` | lower right white lamp appeared lit in the compact run, later refined as likely `0x0017` carryover |
Right-stack isolation/fresh-latch refinement:
| Frame | Selector/value | Visible effect |
| --- | --- | --- |
| `00 00 15 80 00 CF` | `E000[0x0015]=0x8000` | CALL lamp lights |
| `00 00 15 00 00 4F` | `E000[0x0015]=0x0000` | CALL lamp clears |
| `00 00 13 80 00 C9` | `E000[0x0013]=0x8000` | far-right SLAVE lamp lights |
| `00 00 13 00 00 49` | `E000[0x0013]=0x0000` | SLAVE lamp clears |
| `00 00 17 80 00 CD` | `E000[0x0017]=0x8000` | far-right bottom white BARS lamp lights |
| `00 00 17 00 00 4D` | `E000[0x0017]=0x0000` | lamp remains lit; low write does not clear it |
| `00 00 1A 80 00 C0` | `E000[0x001A]=0x8000` | no independent BARS-lamp effect from fresh boot |
| `00 00 07 80 00 DD` / `00 00 07 00 00 5D` | `E000[0x0007]` high/low | no clear visible delta from CONNECT-OK baseline |
So `0x0017` is currently the strongest BARS lamp-on/latch selector.
`0x001A` should not be labeled as the same lamp source from the prior compact
run; that was likely carryover after `0x0017` had latched the white lamp on.
`0x0007` is still protocol-relevant because the real panel emits the matching
CAM POWER event frame, but the host-write visible lamp mapping is not separated
from the CONNECT-OK baseline yet.
ROM trace refinement: selector `0x0013` dispatches to `H'2E06`, reads current
table word `E800[0x0013]` at `H'E826`, and maps bit 15 to SLAVE while bit 14
maps to `IRIS/M.BLACK LINK`. See `docs/pt2-iris-mblack-link-rom-trace.md`.
STANDARD/MASTER hunt:
| Run | Tested selector/value pocket | Result |
| --- | --- | --- |
| `panel-atlas-standard-master-bit-sweep-v1` | `0x0012`, `0x0013`, `0x0014` with high-bit/high-nibble candidates | no clean STANDARD or MASTER lamp trigger; positive SLAVE control worked |
| `panel-atlas-standard-master-neighbor-sweep-v2` | `0x0010`, `0x0011`, `0x0015`, `0x0016`, `0x0017`, `0x0018`, `0x0019`, `0x001A` high-nibble candidates | no clean STANDARD or MASTER trigger |
| `panel-atlas-standard-master-lower-neighbor-sweep-v3` | `0x0008` through `0x000F` high-nibble candidates | no clean STANDARD or MASTER trigger |
The `v2` sweep did show the far-right bottom BARS lamp/latch from the
`0x0017` family when using non-`0x8000` high-nibble values. The clearest
transition is `00 00 17 40 00 0D` (`E000[0x0017]=0x4000`) lighting the same
bottom white BARS lamp that `0x0017=0x8000` can light. The lamp persisted after
`0x0017=0x0000`, matching the existing BARS latch behavior. Later `0x0018`
rows are contaminated by that latch and need a fresh-boot isolation run before
labeling `0x0018` as a separate BARS source.
## Next Atlas Step
The next useful run is still a clear/ack/state-transition probe for the
`0x0017` BARS latch, now including `0x0017=0x4000`:
- test likely sibling selectors around `0x0016`, `0x0018`, `0x0019`, and `0x001A`
from a fresh boot after `0x0017` has latched on,
- try command-7 repeat/ack and selector-zero refreshes to see whether the lamp
clears through a state-machine transition rather than a simple low write,
- test `0x0007` with an alternate baseline, because CONNECT OK already lights
CAM POWER and masks any host-write lamp delta.
- do not re-run `0x0008` through `0x0014` high-nibble values for
STANDARD/MASTER unless the ROM trace points back there; the webcam sweeps did
not show those lamps in that pocket.
## ROM-Derived Button Output Sweep
To skip the physical RCP button-press side and directly test likely "on" states
from ROM handlers, use:
```powershell
.\.venv\Scripts\python.exe scripts\build_rom_button_output_sweep.py
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\panel-atlas-rom-button-output-candidates-v1.json --parity E --quiet-console --log captures\panel-atlas-rom-button-output-candidates-v1-webcam.txt --result-json captures\panel-atlas-rom-button-output-candidates-v1-webcam-result.json --snapshot-dir captures\panel-atlas-rom-button-output-candidates-v1-webcam-shots --camera-index 4 --snapshot-delays 0.5
```
This scenario power-cycles before every candidate, seeds `CONNECT: OK`, sends
one command-0 selector/value from the ROM-derived candidate list, and captures a
single webcam image 0.5 s later. It is intentionally slower than a continuous
sweep but should avoid most latch/carryover ambiguity.
Run result notes from `captures/panel-atlas-rom-button-output-candidates-v1-webcam-shots/`:
| Case | Frame | Selector/value | Visible result |
| --- | --- | --- | --- |
| 001 | `00 00 13 40 00 09` | `0x0013 = 0x4000` | IRIS/M.BLACK LINK |
| 002 | `00 00 13 80 00 C9` | `0x0013 = 0x8000` | SLAVE |
| 003 | `00 00 15 80 00 CF` | `0x0015 = 0x8000` | CALL and red tally |
| 004 | `00 00 17 80 00 CD` | `0x0017 = 0x8000` | BARS |
| 005 | `00 01 90 80 00 4B` | `0x0110 = 0x8000` | KNEE AUTO |
| 006 | `00 00 1A 08 08 40` | `0x001A = 0x0808` | MONITOR ENC |
| 007 | `00 00 1A 20 20 40` | `0x001A = 0x2020` | MONITOR B |
| 008 | `00 00 1A 40 40 40` | `0x001A = 0x4040` | MONITOR G |
| 009 | `00 00 1A 80 80 40` | `0x001A = 0x8080` | MONITOR R |
| 010 | `00 00 6B 80 00 B1` | `0x006B = 0x8000` | STANDARD |
| 011 | `00 01 03 00 04 5C` | `0x0083 = 0x0004` | IRIS AUTO, SHUTTER OFF, MASTER GAIN HP |
| 012 | `00 01 03 40 00 18` | `0x0083 = 0x4000` | IRIS AUTO, SHUTTER OFF, MASTER GAIN 0 |
| 013 | `00 01 03 20 00 78` | `0x0083 = 0x2000` | IRIS AUTO, SHUTTER OFF, MASTER GAIN 3 |
| 014 | `00 01 0F 80 00 D4` | `0x008F = 0x8000` | IRIS AUTO, SHUTTER begins with `1...` |
| 015 | `00 01 0F 20 00 74` | `0x008F = 0x2000` | IRIS AUTO, SHUTTER `00.0` |
| 016 | `00 01 0F 08 00 5C` | `0x008F = 0x0800` | IRIS AUTO, SHUTTER EVS |
| 017 | `00 01 0F 10 00 44` | `0x008F = 0x1000` | IRIS AUTO, SHUTTER OFF |
| 018 | `00 01 13 10 20 78` | `0x0093 = 0x1020` | BLACK/FLARE MANUAL, white balance MANUAL |
| 019 | `00 01 13 40 40 48` | `0x0093 = 0x4040` | BLACK/FLARE AUTO, white balance AUTO |
| 020 | `00 01 13 80 40 88` | `0x0093 = 0x8040` | BLACK/FLARE AUTO, white balance PRESET |
| 021 | `00 01 13 00 20 68` | `0x0093 = 0x0020` | BLACK/FLARE MANUAL, white balance MANUAL |
| 022 | `00 01 13 00 40 08` | `0x0093 = 0x0040` | BLACK/FLARE AUTO, white balance MANUAL |
| 023 | `00 01 1A 08 00 49` | `0x009A = 0x0800` | no panel change observed |
| 024 | `00 01 37 20 00 4C` | `0x00B7 = 0x2000` | no panel change observed |
The run directory contains 28 candidate photos. The user-supplied ordered notes
covered the first 24, so cases 025-028 still need visual review before assigning
meanings:
| Case | Frame | Selector/value | Candidate |
| --- | --- | --- | --- |
| 025 | `00 01 39 40 00 22` | `0x00B9 = 0x4000` | F6DC.7 handler value candidate |
| 026 | `00 01 44 80 00 9F` | `0x00C4 = 0x8000` | F6D4.0 bundle selector candidate |
| 027 | `00 01 46 80 00 9D` | `0x00C6 = 0x8000` | F6D4.0 bundle selector candidate |
| 028 | `00 01 78 80 00 A3` | `0x00F8 = 0x8000` | F6D4.1 handler candidate |
Key refinements from this run:
- `0x001A` is now best labeled as the MONITOR selector cluster: `ENC`, `B`, `G`,
and `R` appeared cleanly from the four packed values.
- `0x006B = 0x8000` is the first clean STANDARD lamp trigger.
- `0x0083` is a MASTER GAIN/status display word, with values observed for `HP`,
`0`, and `3`, while also lighting IRIS AUTO and showing SHUTTER OFF.
- `0x008F` carries local shutter display/value states beyond the earlier
EVS/OFF bits.
- `0x0093` now has stronger white-balance plus BLACK/FLARE field mapping:
high/mid bit combinations select WB AUTO/PRESET/MANUAL and BLACK/FLARE
AUTO/MANUAL together.
## Broad Visual Sweep Workflow
For exploratory lamp/readout mining, use the generated big sweep rather than
hand-writing thousands of frames:
```powershell
.\.venv\Scripts\python.exe scripts\build_panel_visual_sweep.py scenarios\panel-atlas-big-visual-sweep-0001-017f-highbits.json --start 0x0001 --end 0x017F --values 0x8000,0x4000,0x2000,0x1000,0x0800 --power-cycle-every 32 --ok-every 8 --listen 0.65 --clear-listen 0.15 --ok-listen 0.30
```
Run the generated scenario with webcam snapshots:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\panel-atlas-big-visual-sweep-0001-017f-highbits.json --parity E --quiet-console --log captures\panel-atlas-big-visual-sweep-0001-017f-highbits-webcam.txt --result-json captures\panel-atlas-big-visual-sweep-0001-017f-highbits-webcam-result.json --snapshot-dir captures\panel-atlas-big-visual-sweep-0001-017f-highbits-webcam-shots --camera-index 4 --snapshot-delays 0.5
```
The generated high-bit sweep covers selectors `0x0001` through `0x017F` with
five candidate values per selector, for 1,915 candidate snapshots. It power
cycles every 32 selectors to reduce latch contamination. Selector zero is
omitted because it controls the CONNECT OK baseline.
After the run, create labeled review sheets:
```powershell
.\.venv\Scripts\python.exe scripts\make_panel_sweep_contact_sheets.py captures\panel-atlas-big-visual-sweep-0001-017f-highbits-webcam-shots --output-dir captures\panel-atlas-big-visual-sweep-0001-017f-highbits-sheets --only-candidates --crop panel --cols 4 --rows 5 --thumb-width 360
```
If a sheet shows a visible change, the image label has the exact trigger form:
`candidate_XXXX_YYYY` means command-0 wrote `E000[0xXXXX]=0xYYYY`. Use that
selector/value in a smaller fresh-boot isolation scenario.
## Broad Sweep Findings
Run:
```text
captures/panel-atlas-big-visual-sweep-0001-017f-highbits-webcam.txt
captures/panel-atlas-big-visual-sweep-0001-017f-highbits-webcam-result.json
captures/panel-atlas-big-visual-sweep-0001-017f-highbits-webcam-shots/
```
Serial health:
| RX frames | TX frames | Resync | Dropped bytes | Snapshots |
| ---: | ---: | ---: | ---: | ---: |
| 706 | 2372 | 0 | 0 | 1916 |
User-reviewed new visible hits from the broad sweep:
| Candidate label | Frame | Selector/value | Reported visible effect |
| --- | --- | --- | --- |
| `candidate_0013_4000` | `00 00 13 40 00 09` | `E000[0x0013]=0x4000` | `IRIS/M.BLACK LINK` area/lamp |
| `candidate_0024_8000` | `00 00 24 80 00 FE` | `E000[0x0024]=0x8000` | LCD selector button/lamp |
| `candidate_0082_8000` | `00 01 02 80 00 D9` | `E000[0x0082]=0x8000` | IRIS readout shows `OP` |
| `candidate_0082_4000` | `00 01 02 40 00 19` | `E000[0x0082]=0x4000` | IRIS readout shows `1.4` |
| `candidate_0083_8000` | `00 01 03 80 00 D8` | `E000[0x0083]=0x8000` | MASTER GAIN readout shows `-3` |
| `candidate_0093_8000` | `00 01 13 80 00 C8` | `E000[0x0093]=0x8000` | white-balance PRESET lamp |
| `candidate_0093_4000` | `00 01 13 40 00 08` | `E000[0x0093]=0x4000` | white-balance AUTO lamp |
| `candidate_0093_2000` | `00 01 13 20 00 68` | `E000[0x0093]=0x2000` | white-balance MANUAL lamp |
The serial log shows immediate command-4 table readback frames for these writes,
so the RCP accepted the selector updates.
Fresh-boot isolation scenario:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\panel-atlas-big-hits-isolation-v1.json --parity E --quiet-console --log captures\panel-atlas-big-hits-isolation-v1-webcam.txt --result-json captures\panel-atlas-big-hits-isolation-v1-webcam-result.json --snapshot-dir captures\panel-atlas-big-hits-isolation-v1-webcam-shots --camera-index 4 --snapshot-delays 0.5
```
Create review sheets for that isolation run:
```powershell
.\.venv\Scripts\python.exe scripts\make_panel_sweep_contact_sheets.py captures\panel-atlas-big-hits-isolation-v1-webcam-shots --output-dir captures\panel-atlas-big-hits-isolation-v1-sheets --crop panel --cols 3 --rows 4 --thumb-width 420
```
Fresh-boot isolation results:
| Frame | Selector/value | Confirmed visible effect |
| --- | --- | --- |
| `00 00 13 40 00 09` | `E000[0x0013]=0x4000` | `IRIS/M.BLACK LINK` lamp |
| `00 00 24 80 00 FE` | `E000[0x0024]=0x8000` | LCD selector-button lamp |
| `00 00 24 00 00 7E` | `E000[0x0024]=0x0000` | same LCD selector-button lamp remained visible at 0.5 s |
| `00 01 02 80 00 D9` | `E000[0x0082]=0x8000` | IRIS readout `OP` |
| `00 01 02 40 00 19` | `E000[0x0082]=0x4000` | IRIS readout `1.4` |
| `00 01 02 00 00 59` | `E000[0x0082]=0x0000` | IRIS readout blank |
| `00 01 03 80 00 D8` | `E000[0x0083]=0x8000` | IRIS AUTO lamp, SHUTTER `OFF`, MASTER GAIN `-3` |
| `00 01 03 00 00 58` | `E000[0x0083]=0x0000` | same IRIS AUTO / SHUTTER `OFF` / MASTER GAIN `-3` state remained visible at 0.5 s |
| `00 01 13 80 00 C8` | `E000[0x0093]=0x8000` | BLACK/FLARE MANUAL plus white-balance PRESET |
| `00 01 13 40 00 08` | `E000[0x0093]=0x4000` | BLACK/FLARE MANUAL plus white-balance AUTO |
| `00 01 13 20 00 68` | `E000[0x0093]=0x2000` | BLACK/FLARE MANUAL plus white-balance MANUAL |
| `00 01 13 00 00 48` | `E000[0x0093]=0x0000` | BLACK/FLARE MANUAL plus white-balance MANUAL |
Interpretation:
- `0x0082` is a direct IRIS display/status selector. Clearing it blanks the IRIS
readout, so this one behaves like a simple display source.
- `0x0083=0x8000` drives a combined state: MASTER GAIN `-3`, SHUTTER `OFF`, and
IRIS AUTO. Clearing the selector did not visibly clear that state in the
isolation run, so it may be latched or copied into another display bank.
- `0x0093` is now a confirmed white-balance mode selector with
`0x8000=PRESET`, `0x4000=AUTO`, and `0x0000/0x2000=MANUAL` under this test
context. BLACK/FLARE MANUAL was also present for all tested `0x0093` states.
- `0x0024=0x8000` lights an LCD selector-button lamp, but `0x0024=0x0000` did
not clear it in this timing window.

View File

@@ -4,6 +4,16 @@ This document is the current working model for the serial protocol spoken by the
A later RCP manual mentions a "PT2 compatibility mode" for controlling the same CCU family this panel was made for. We are using "PT2" here as a practical label for this six-byte SCI1 protocol model. It is not yet a claim that every field name matches Sony's official PT2 terminology.
Focused companion notes:
- [PT2 Copy State Machine](pt2-copy-state-machine.md)
- [PT2 Menu State Machine](pt2-menu-state-machine.md)
- [PT2 Session Rhythm ROM Trace](pt2-session-rhythm-trace.md)
- [PT2 Continuation Command Trace](pt2-continuation-command-trace.md)
- [PT2 Report Aftermath ROM Trace](pt2-report-aftermath-trace.md)
- [PT2 Shutter Display Trace](pt2-shutter-display-trace.md)
- [PT2 Button Report Bench Plan](pt2-button-report-bench-plan.md)
## Current High-Confidence Facts
- The real bench link is `38400 8E1`, not `38400 8N1`.
@@ -428,6 +438,9 @@ ROM report-source update:
- The active `02/01 ...` frames seen during CONNECT OK attempts are best modeled as `F870 -> BAF2 -> BA26` report-queue transmissions.
- `BAF2` dequeues a report word, encodes the first three TX bytes, reads the payload from `E800 + 2*selector`, and `BA26` appends the `0x5A` XOR checksum.
- After sending a queued report, the ROM sets `FAA2.3` and `FAA3.7`; command `4`, `5`, or `6` can then consume/advance the report only while that continuation latch is live.
- The emitted report does not appear to encode a required ACK selector. The ROM consume test is `FAA2.3`, so a generic command-5 continuation ACK such as `05 00 40 00 00 1F` is the cleanest report-cursor consume candidate.
- Low commands `0/1/2` during the report wait can clear the continuation latch and re-enter initial dispatch, but they do not advance `F9B5`. Command `7` retransmits and also does not advance `F9B5`.
- Emulator testing now supports the reactive ACK model: waiting for a finished RCP report frame, then sending `05 00 40 00 00 1F`, repeatedly advanced `F9B5`, cleared `FAA2/FAA3`, and kept the emulated LCD at `CONNECT: OK`.
- This makes a reactive fake-CCU test more valuable than another blind fixed-delay matrix: recover to OK, wait for the first active report, then send one candidate continuation/ACK frame.
## Candidate CCU Seed Values
@@ -562,10 +575,29 @@ The `0x006D` copy path is now confirmed outside the earlier all-suite ordering c
- Selector `0x006D` dispatches to `H'3015`. Forced decoding shows it sets `F731.7`, loads `F798=H'C8`, sets `F795.6/F795.7`, sets display selector `F732=H'1903`, sets `FB02=H'64`, calls `48FA`, then sets `F76E.6`.
- The LCD dispatch for these states is now traced: `loc_48FA` reads the high byte at `F732`, so `F732=H'1903/H'1904` selects display page `0x19`, not direct page `0x03/0x04`. `493E[0x19] -> H'930A`; that page's local table at `H'931C` includes `H'9F6A` for `COPY` / `IN PROGRESS` and `H'9FDA` for `COPY` / `COMPLETED`. The low byte at `F733` is the substate selector: `0x03` is in-progress and `0x04` is completed.
- This makes the likely copy handshake: `0x006D` starts copy and sets the `F795.6/F795.7` in-progress flags; `0x006C` is the completion/exit sibling only when those flags are live. Sending `0x006C` alone can therefore blank or clear state instead of displaying `COPY COMPLETED`.
- Bench step-through confirmed the sequence model: `006C` alone produced `CONNECT OK -> blank`; `006D` alone produced `CONNECT OK -> COPY IN PROGRESS`; `006D` followed by `006C` after 250 ms produced a brief `COPY IN PROGRESS` then `COPY COMPLETED`; the same after 1.0 s and 1.5 s produced a longer `COPY IN PROGRESS` then `COPY COMPLETED`; after 2.0 s or 2.5 s it fell to `CONNECT:NOT ACT` instead of completing. Repeating `006D` before `006C` also completed successfully in the 2x and 3x repeat tests. A longer `006D` hold test kept `COPY IN PROGRESS` active for several seconds and then completed when `006C` arrived, while the same hold without `006C` timed out from `COPY IN PROGRESS` to `CONNECT:NOT ACT`. This points to `006D` as an in-progress/progress-window refresh selector and `006C` as the explicit completion/exit selector, not a stateless command pair.
- The FRT1 timer path decrements `F797` and `F798`; when either reaches zero, it clears `F731.7`. This matches the observed transient display modes falling back to `CONNECT:NOT ACT`.
- The string `COPY IN PROGRESS` is present in the ROM LCD resources, so the `006D` result is not a generic serial artifact.
- Manual interpretation: the RCP-TX7 operating manual describes `COPY IN PROGRESS` as the LCD state shown during the multi-camera `COPY TO SLAVES` data-transfer operation over the RS232C command-link system. During that state, all linked RCP units display the message and their buttons/knobs are locked until `COPY COMPLETED`. Therefore selector `0x006D` is best treated as entering a command-link copy/data-transfer state, not as a normal CCU connection ACK.
RCP-side OTHERS/COPY menu trace:
- The OTHERS menu is page `0x01`: `493E[0x01] -> H'631C`, local table `H'632E`.
- Local table entry 1 points to `H'6FF0`, the page that renders `OTHERS` / `COPY TO SLAVES`.
- The entry descriptor immediately before `H'6FF0` requires selector `0x0015` in the secondary table: `E400[0x0015] != 0`. Because command 6 writes `E400`, this is probably a CCU-provided feature/visibility bit, and command 6 must be sent in a live continuation/report window to have an effect.
- The root OTHERS/SHUTTER page `H'6EE4` consumes `F770.0/F770.1`; the COPY page `H'6FF0` consumes `F770.2`. Both handlers read `F770`, clear it, and then act on those low bits.
- The only decoded direct writers to `F770` found so far write high bits `0x80`, `0x40`, and `0x20` from value/dial redraw paths. No direct decoded write to `F770=0x01/0x02/0x04` has been found yet, so the physical OTHERS/COPY action probably arrives through an indirect panel-key latch or page-local mechanism.
- When the page sees `F770.2` set, it only follows the local copy-start branch if `F791.7` is already set. That branch sets `F76E.6`, `F795.7`, `F731.7`, `F798=H'C8`, `F711.7`, `F726=H'64`, calls `loc_5500`, then displays `COPY TO SLAVES`. This is the RCP-side equivalent of the serial `0x006D` copy-start effect.
- If `F770.2` is set while `F791.7` is clear, the ROM diverts through `H'704C` to a `SET RCP` / `MASTER` display path instead of starting copy. That makes `F791.7` a second, likely master/link/session gate for the physical COPY operation.
- The OTHERS root handler at `H'6EE4` also tests primary selector `E000[0x008F]` (`H'E11E`) bits 11 and 12, and uses them to set `F711.6` and `F711.4`. Bench isolation shows this selector is broader than an OTHERS gate: bit 11 makes the SHUTTER seven-segment display show observed `EUS`, probably the manual's `EVS` display, and bit 12 makes it show the literal letters `OFF`; either bit also illuminates the iris AUTO lamp.
- Candidate gate probes, not final protocol truth:
```text
00 01 0F 18 00 4C ; command 0, set E000[0x008F] bits 11+12
06 00 15 00 01 48 ; command 6, set E400[0x0015] nonzero; requires live continuation
06 00 15 80 00 C9 ; command 6, alternate nonzero visibility value; requires live continuation
```
Read table state:
```powershell
@@ -588,6 +620,106 @@ This fits the real panel behavior:
- Correct fake-CCU traffic can wake it to `CONNECT: OK`.
- Without richer CCU state, readouts illuminate but show placeholders like `----`.
## Active-State Local-Control Watch
Bench evidence now suggests `CONNECT: OK` is maintained by an ongoing CCU refresh stream, not by one magic wake frame. This creates a useful local-control test: keep the RCP in the active state and press/turn physical controls while logging device TX.
Two JSON scenarios are set up for that:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\active-control-report-watch-quiet.json --parity E --log captures\active-control-report-watch-quiet.txt --result-json captures\active-control-report-watch-quiet-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\active-control-report-watch-broad.json --parity E --log captures\active-control-report-watch-broad.txt --result-json captures\active-control-report-watch-broad-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\active-control-report-watch-gated.json --parity E --log captures\active-control-report-watch-gated.txt --result-json captures\active-control-report-watch-gated-result.json
```
The quiet scenario sends the selector-zero OK seed, then refreshes `E000[0x0093]=0x9020` every 0.60 s. This is the lowest-noise active hold that has already kept `CONNECT: OK` alive.
The broad scenario streams `E000[0x008F]=0x1800` and `E000[0x0093]=0xFFFF` every cycle. This is noisier, but may open more local-control/status gates.
The gated scenario first seeds candidate secondary-table feature bits with command 6:
```text
06 00 15 80 00 C9 ; E400[0x0015] OTHERS/COPY visibility candidate
06 01 0F 18 00 4A ; E400[0x008F] shutter/OTHERS bits 11+12
06 01 13 FF FF 4E ; E400[0x0093] broad local-report/status gate candidate
```
Then it streams `E000[0x008F]=0x1800` and `E000[0x0093]=0x90FF`. This is the better next watch if quiet/broad E000-only refreshes do not produce local-control TX.
## Bench Webcam Capture
For tests where LCD timing/state matters, `serial_scenario.py` can take webcam
snapshots tied to exact TX commands. Current bench calibration:
```powershell
--camera-index 4 --snapshot-delays 0.5
```
Camera index `4` is the working panel-facing webcam on the current PC. A single
`0.5` second post-TX delay captures readable LCD changes without creating the
large image sets produced by `0,0.25,1.0`.
Example:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\copy-step-006d-006c-1000ms.json --parity E --quiet-console --log captures\copy-webcam.txt --result-json captures\copy-webcam-result.json --snapshot-dir captures\copy-webcam-shots --camera-index 4 --snapshot-delays 0.5
```
After a run, summarize unexpected device frames with:
```powershell
.\.venv\Scripts\python.exe scripts\serial_scenario_unexpected.py captures\active-control-report-watch-quiet.txt --show-all
.\.venv\Scripts\python.exe scripts\serial_scenario_unexpected.py captures\active-control-report-watch-broad.txt --show-all
.\.venv\Scripts\python.exe scripts\serial_scenario_unexpected.py captures\active-control-report-watch-gated.txt --show-all
```
Expected refresh responses are ignored by default: heartbeat, table readbacks, and `02 00 02 00 00 5A`. Any remaining checksum-valid frame is a candidate local-control report. This is where physical button/dial reports should appear if the RCP only talks while the CCU keeps the selector tables active.
The unexpected-frame summarizer also labels known autonomous button frames so they are not mistaken for newly unlocked controls:
```text
00 00 15 80 00 CF ; known CALL active report
00 00 15 00 00 4F ; known CALL inactive report
00 00 07 80 00 DD ; known CAM POWER report
```
First quiet/broad watch captures on 2026-05-26 showed no unexpected checksum-valid frames in the observed window:
```text
active-control-report-watch-quiet: 114 detected frames, all expected refresh/heartbeat/readback
active-control-report-watch-broad: 133 detected frames, all expected refresh/heartbeat/readback
```
Those runs stopped mid-scenario, so this is not a full negative proof. The practical interpretation is narrower: E000 active-state refresh alone did not make the pressed controls emit visible TX in the watched window. The gated command-6 version is the next ROM-supported test.
The first gated watch capture produced a new periodic active-state response:
```text
02 00 04 00 00 5C ; gated 0x0004 transition candidate, seen twice
01 00 04 00 00 5F ; gated 0x0004 active response candidate, seen 328 times
```
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` blinked CAM POWER in isolation.
- `0x0015 = 0x8000/0x0000` blinked CALL and red tally in isolation.
- Fresh-boot isolation maps `0x0013` to SLAVE, `0x0016` to green tally, and `0x0017` to BARS for the `0x8000/0x0000` value pair.
- Fresh-boot isolation maps `0x0092` to iris AUTO/OFF behavior, and maps both `0x00B9` and `0x0110` to KNEE-related behavior.
- A ROM trace now explains why the KNEE tests looked non-level-held. `loc_1795` is reached from the panel input lane (`F104 -> F692 -> F6F0.1`) and reads `E000[0x00B9]` plus `E000[0x0110]`.
- The live KNEE value/report gate is `0x00B9.13`, not the earlier bench-only `0x00B9.15` guess.
- `0x0110.15` forces a timed KNEE page/display override (`F732=0x1C03`, `FB02=0x14`), which matches the observed "lights, then clears" behavior.
- On that KNEE LCD page, `0x0110.15` selects `DL`, `0x00B9.15` selects `PRESET`, and both clear selects `AUTO`.
- When `0x00B9.13` is set and `0x0110.15` is clear, the ROM reports/updates selector `0x00BC` from the `F692 - F6B2` panel-input delta. See `docs/pt2-knee-rom-trace.md`.
- The first KNEE ROM probe produced a new bench LCD state with `DTL` on the left and `KNEE` on the right, matching the ROM page-0x1C DETAIL/KNEE neighborhood.
- A follow-up isolation lit the KNEE AUTO lamp in later KNEE windows but did not change the LCD. Current model: KNEE AUTO lamp/status is mostly selector driven, while the DETAIL/KNEE LCD page needs an additional local menu/display condition.
## What Is Still Unknown
- The official PT2 names for commands and selectors.
@@ -612,7 +744,7 @@ This fits the real panel behavior:
5. Dump selector table state before and after CONNECT OK.
6. Seed selectors `0x003`, `0x040`, and `0x0F6` after selector-zero OK and watch lamps/readouts.
7. Mine selector dispatch handlers for known UI text terms: `IRIS`, `GAIN`, `SHUTTER`, `BARS`, `BLACK`, `CALL`, `AUTO`, `DIAG`.
8. Build a fake-CCU streamer that repeatedly writes a small selector set and logs which RCP reports appear.
8. Run the active-control watch scenarios and map any unexpected frames back to physical controls.
## Source Files And Reports

View File

@@ -0,0 +1,215 @@
# PT2 Report Aftermath ROM Trace
This note tracks what the ROM does after the RCP emits an autonomous serial report from the `F870` report queue.
## Core Model
The RCP report path is:
```text
3E54 / 4046 enqueue selector into F870
3FD3 checks send gates
BAF2 dequeues/stages a report from F870 and E800
BA26 sends the six-byte SCI1 frame
BB46-BB51 arms the continuation/resend aftermath
```
The important consequence is that `BAF2` does not advance the report consumer cursor. The emitted report remains outstanding until the host sends a valid continuation command.
## Report Queue
| RAM | Role |
| --- | --- |
| `F870` | autonomous serial-report word ring |
| `F9B0` | producer cursor |
| `F9B5` | consumer/outstanding cursor |
Producer paths:
- `H'3E54`: when `R2.7` is set, deduplicates and appends `R3` to `F870`, then advances `F9B0`.
- `H'4046-H'4070`: when heartbeat/report cadence expires and the report queue is empty, appends selector `0x0000` to `F870`.
Consumer path:
- `H'BAF2` compares `F9B5` against `F9B0`.
- If equal, no report is pending.
- If different, it reads selector `F870[ F9B5 ]`, reads the report value from `E800[selector]`, stages the outgoing frame, and calls `BA26`.
- `BAF2` leaves `F9B5` unchanged.
Only continuation command `4`, `5`, or `6` advances `F9B5`, and only when `FAA2.3` is still set.
## Report Send Gates
`H'3FD3` allows `BAF2` only when:
- `FAA2 == 0`.
- `F9C0 == 0`.
- If `FAA5.7` is set, `F9C3 == 0`, meaning no partial RX frame is currently being assembled.
This means host RX traffic can block report sending while a session is active.
## Aftermath Window
After `BA26` sends the report frame, `H'BB46-H'BB51` arms the report aftermath:
| Address | Write | Meaning |
| --- | --- | --- |
| `H'BB00` | set `FAA2.3` | queued report needs continuation |
| `H'BB46` | `F9C6 = 0x01F4` | resend spacing countdown |
| `H'BB4C` | `F9C8 = 0x14` | resend/retry budget |
| `H'BB51` | `FAA3 = 0x80` | live resend/window marker |
While this is live, valid RX frames enter the continuation dispatcher because `FAA2 != 0`.
## Clean Continuation Consume
Commands `4`, `5`, and `6` all share the same report-consume pattern:
```text
if FAA2.3 set:
F9B5 += 1
clear F9B5.7
clear FAA3
clear FAA2
```
Handler addresses:
| Command | Handler | Consume block |
| --- | --- | --- |
| `4` | `H'BD0E` | `H'BD67-H'BD79` |
| `5` | `H'BD80` | `H'BDC2-H'BDD4` |
| `6` | `H'BDDB` | `H'BDED-H'BDFF` |
The ROM does not appear to require the continuation ACK selector to match the emitted report selector. The consume test is `FAA2.3`, not selector equality.
## Non-Clean Paths
Low commands `0/1/2/3` while `FAA2.3` is live:
- `H'BC5C` clears `FAA2.3`.
- If the bit was set, `H'BC63` clears `FAA3`.
- `H'BC67` re-enters initial dispatch.
- This does not advance `F9B5`.
Command `7`:
- Retransmits the previous finalized TX frame through `H'BE05-H'BE25`.
- Does not clear `FAA2/FAA3`.
- Does not advance `F9B5`.
TX/RX overlap interlock:
- At `H'BA84-H'BAA7`, TXI checks `FAA2.3 && FAA5.7 && F9C3 != 0`.
- If true, it clears `FAA2.3`, clears `FAA3`, disables TXI, and loads `F9C0=0x1F`.
- This collapses the report window without consuming `F9B5`.
- Bench implication: starting a host frame before the RCP has finished TX can destroy the very continuation window being targeted.
## Resend And Expiry
`H'BE9E-H'BEE8` manages the unconsumed report aftermath:
- Masks `FAA3` with `FAA5.7`.
- If the session gate is gone, `FAA3` becomes zero and `FAA2` is cleared.
- If `FAA3.7` is live and `F9C6 != 0`, it waits.
- If `F9C6 == 0` and `F9C8 != 0`, it decrements `F9C8`, reloads `F9C6=0x01F4`, clears `F9C3`, and calls `BA26` to resend the same finalized frame.
- If `F9C8 == 0`, it clears `F9C5`, forcing the broader session timeout path.
So an unconsumed report can be resent repeatedly, then eventually force the session watchdog to expire.
## Session Timeout
`F9C5` is the broad active-session watchdog:
- A complete six-byte RX frame reloads `F9C5=0x14` at `H'BB9E`.
- FRT2 decrements `F9C5` at `H'BF31-H'BF37`.
- `H'3FEF` observes expiry, clears `F9B5/F9B0`, clears `FAA5.7`, and if the gate was previously live calls `H'400C`.
- `H'400C -> H'4075 -> H'4217` clears broad session/display state and redraws `CONNECT:NOT ACT`.
Related timers:
| RAM | Role |
| --- | --- |
| `F9C0` | post-TX/report-send gate; decremented by FRT1 |
| `F9C1` | RX inter-byte timer; partial-frame timeout |
| `F9C4` | heartbeat/report enqueue cadence |
| `F9C5` | active serial-session watchdog |
| `F9C6/F9C8` | unconsumed report resend spacing/budget |
## Fake-CCU Implications
Best pure report ACK:
```text
05 00 40 00 00 1F
```
Rationale:
- Command `5` consumes the report cursor when `FAA2.3` is live.
- Selector `0x0040` is not one of the known command-5 special side-effect selectors.
- Value bytes are ignored by the generic command-5 consume path.
Same-selector command-5 ACKs are plausible for human readability, but not ROM-required:
```text
05 00 00 00 00 5F ; ACK selector 0x0000
05 00 02 00 00 5D ; ACK selector 0x0002
05 00 07 00 00 58 ; ACK selector 0x0007
05 00 15 00 00 4A ; ACK selector 0x0015
05 01 0F 00 00 51 ; ACK selector 0x008F
```
Use command `4` only when the host intentionally wants to refresh primary/current state while consuming:
```text
04 00 00 80 00 DE ; selector-zero active refresh
```
Use command `6` only when the host intentionally wants to refresh secondary feature gates while consuming:
```text
06 00 15 00 01 48 ; E400[0x0015] = 1
06 01 0F 18 00 4A ; E400[0x008F] = 0x1800
```
Avoid command-5 special selectors as generic ACKs unless the side effect is desired:
```text
05 00 6C 00 00 33 ; COPY completion/exit sibling
05 00 6D 00 00 32 ; COPY IN PROGRESS/start
05 00 6E 00 00 31 ; special accepted, later handler is cleanup/no-op-like
```
## Proposed Normal Loop
A plausible fake-CCU report loop is:
1. Keep the serial format at `38400 8E1`.
2. Seed active state, especially selector zero.
3. Listen for a complete RCP TX frame.
4. Wait until the RCP frame is fully transmitted.
5. Quickly send a command-5 generic ACK before `F9C6/F9C8` resend handling starts.
6. Continue sending enough valid six-byte frames to reload `F9C5` before session expiry.
7. Add command-4/command-6 state refreshes only when we intentionally want to update lamps, displays, or menu gates.
This is the current best ROM-supported path for maintaining `CONNECT: OK` without accidentally consuming the report incorrectly or triggering COPY/menu side effects.
## Emulator Check
Reactive emulator testing supports this model:
- Seeded active state with command `0`, selector zero value `0x8080`.
- Waited for a report window where `FAA2.3` and `FAA3.7` were set and SCI1 TXI was idle.
- Sent `05 00 40 00 00 1F`.
- The ACK path reached the command-5 consume block and advanced `F9B5`, then cleared `FAA3` and `FAA2`.
- Repeating that reactive loop 12 times kept the emulated LCD at `CONNECT: OK`.
Observed clean consume shape after each ACK:
```text
before: FAA2=08 FAA3=80 F9B5=N
after: FAA2=00 FAA3=00 F9B5=N+1
```
A fixed-delay or extra-guard strategy was less reliable. In one run, waiting an additional 1 ms after TX idle before sending the ACK only consumed two reports before falling back to `CONNECT:NOT ACT`. That result should not be overfit as an exact real-device limit, but it reinforces the ROM timing lesson: ACK as a reaction to a finished frame, not as a loose periodic delay.

View File

@@ -0,0 +1,186 @@
# PT2 Session Rhythm ROM Trace
This note tracks ROM evidence for the CCU/RCP "session rhythm": which received commands update selector state, which internal queues process those selectors, and which timers expire visible states back toward `CONNECT: NOT ACT`.
## RX Command Front Door
The serial command dispatcher reads `F860 & 0x07` at `H'BC08-H'BC0C`.
Dispatch is split by `FAA2`:
| Command | Dispatcher state | Handler | Main effect |
| --- | --- | --- | --- |
| `0` | initial/idle, `FAA2 == 0` | `H'BC69` | writes primary `E000` and current/report `E800`, sets `EC00` dirty bit 7, appends selector to `F970`, replies with command-4 style echo |
| `1` | initial/idle, `FAA2 == 0`, `F861.7 == 0` | `H'BCD7` | reads primary `E000` and replies |
| `2` | initial/idle, `FAA2 == 0` | `H'BD04` | clears `FAA2.7`, likely abort/clear |
| `4` | continuation, `FAA2 != 0` | `H'BD0E` | writes primary `E000`, sets `EC00` dirty bit 7, appends selector to `F970`; selector zero also writes `E800`; no immediate reply |
| `5` | continuation, `FAA2 != 0` | `H'BD80` | ACK/selector side-effect path; special selectors can append to `F970` or clear latches |
| `6` | continuation, `FAA2 != 0` | `H'BDDB` | writes secondary `E400`, sets `EC00` dirty bit 6; no immediate reply |
| `7` | both states | `H'BE05` | retransmits previous frame or emits retry/error echo |
Practical meaning:
- The normal active link is stateful. Commands `4/5/6` only do their intended work in the continuation side where `FAA2 != 0`.
- Command `0` is both a value update and a continuation opener because it sets `FAA2.7`, writes tables, appends the selector to `F970`, and emits a reply.
- Command `4` is not identical to command `0`: for nonzero selectors it updates `E000` but does not directly update `E800` in this handler. That matters because autonomous reports read values from `E800`.
- Command `6` does not directly display anything. It changes the secondary feature/visibility table consumed later by local page code such as `5FD2`.
## Serial Session Timeout
The broad CCU-traffic watchdog is `F9C5`.
RXI frame capture path:
- `H'BB90-H'BB96` stores received bytes into `F868-F86D` and increments `F9C3`.
- When `F9C3 == 6`, `H'BB9E` loads `F9C5=0x14`.
- Main-loop handler `H'BBAB` only validates/dispatches a frame once `F9C3 == 6`.
FRT2 OCIA timer path:
- `H'BF31-H'BF37` decrements `F9C5` when nonzero.
- `H'3FEF` observes `F9C5`; once it is zero, the main loop can clear `F9B5/F9B0`, clear `FAA5.7`, then call `H'400C`.
- `H'400C` clears the broad session/display state and calls `H'4217`, which redraws `CONNECT:NOT ACT`.
Practical meaning:
- Any complete six-byte RX frame refreshes the short serial-session watchdog, independent of whether it ultimately produces the desired command-side effect.
- This is probably the main `CONNECT: OK -> CONNECT:NOT ACT` timer when CCU-like traffic stops.
- Separate display overlays still use `FB02`, so a page can expire even while serial traffic is otherwise alive.
## Selector Queue
`H'BE70` appends selectors to the processing queue at `F970`. `H'3E54` can also append to this queue when its mode byte has `R2.6` set.
Important queue RAM:
| RAM | Role |
| --- | --- |
| `F970` | selector-processing ring |
| `F9B4` | queue write cursor |
| `F9B9` | queue read cursor |
Queue processing happens at `H'2806` during the main loop:
1. If `F9B9 == F9B4`, no selector is pending.
2. Otherwise it reads the next selector from `F970`, increments `F9B9`, and masks the selector to `0x01FF`.
3. If the selector matches any active display slot (`F736/F738/F73A/F73C/F73E/F740/F742/F754`), it calls `H'48FA` before the selector-specific dispatch.
4. It then jumps through the selector handler table at `H'28A6`.
This explains why the same incoming selector can either look inert or redraw the LCD: the selector must match the current active display/page slots before `48FA` is called from the queue bridge.
## Autonomous Report Queue
The report queue is separate from the selector-processing queue.
| RAM | Role |
| --- | --- |
| `F870` | autonomous outbound report queue |
| `F9B0` | report producer cursor |
| `F9B5` | report consumer cursor |
Important report path:
- `H'3E54` appends report selectors to `F870` when its mode byte has `R2.7` set.
- `H'3FD3` sends reports only when `FAA2 == 0`, `F9C0 == 0`, and if `FAA5.7` is set then `F9C3 == 0`.
- `H'BAF2` drains `F870`, reads the report value from `E800 + 2*selector`, stages a six-byte TX frame, and sends it through `H'BA26`.
- `H'BB00` sets `FAA2.3` when a queued report is sent.
- After a report send, the ROM creates a continuation window with `F9C6=0x01F4`, `F9C8=0x14`, and `FAA3=0x80`.
- Continuation commands `4`, `5`, or `6` advance `F9B5` when `FAA2.3` was set, then clear `FAA3/FAA2`.
Practical meaning:
- A fake CCU probably needs to consume autonomous reports, not only stream status words.
- If a report is sent and the CCU does not answer during the continuation window, the RCP can repeat, retry, or let the broader session gates decay.
- Because report values come from `E800`, command `0` and local RCP report handlers are stronger report-value refreshes than nonzero command `4` writes.
## TX / Heartbeat Timing Gates
`H'BA26` is the common TX-finalize helper.
Observed reloads:
- `BA26` sets `F9C0=0x64` and `F9C4=0x07` after send finalization.
- TX completion later sets `F9C0=0x09`, or `F9C0=0xF0` when `F795.6` is set.
- The FRT2 path decrements `F9C4`; `H'4046` can enqueue heartbeat/report selector `0x0000` when the queue is empty and the gate is open.
Practical meaning:
- The roughly 700 ms heartbeat cadence is the `F9C4=0x07` post-send countdown with the current FRT2 tick model.
- COPY state can slow or alter TX pacing through `F795.6 -> F9C0=0xF0`.
## Display Restore Timer
`FB03.7` marks a temporary/timed display override. `FB02` is its countdown.
Writers that start timed display overrides:
| Address | State written | Visible candidate |
| --- | --- | --- |
| `H'1726` | `F732=0x1C07`, `FB02=0x14`, `FB03.7=1` | DETAIL/KNEE-style timed page |
| `H'176E` | `F732=0x1C06`, `FB02=0x14`, `FB03.7=1` | DETAIL/KNEE-style timed page |
| `H'2135` | `F732=0x1C03`, `FB02=0x14`, `FB03.7=1` | KNEE page seen in bench probes |
| `H'26F6` | `F732=0x1C01`, `FB02=0x14`, `FB03.7=1` | adjacent local menu overlay |
| `H'3004` | `F732=0x1904`, `FB02=0x14`, `FB03.7=1` | `COPY COMPLETED` |
| `H'3038` | `F732=0x1903`, `FB02=0x64`, `FB03.7=1` | `COPY IN PROGRESS` |
| `H'7092` | `FB03.7=1`, `FB02=0x14` | `SET RCP` / `MASTER` fallback from local COPY gate |
FRT2 OCIA timer path `H'BF50-H'BF6B`:
- If `FB03.7` is clear, nothing happens.
- If `FB02 != 0`, the timer decrements `FB02`.
- If `FB02 == 0`, it clears `FB03.7` and calls `H'48EF`.
- `H'48EF` restores `F732` from `F734`, then calls `H'48FA`.
Practical meaning:
- Many visible pages are deliberate timed overlays, not durable states.
- A scenario can appear to "drop to NOT ACT" simply because the overlay expires and restores the saved page.
## COPY / Activity Latch Timers
COPY-related selectors are handled through command 5 and the `F970` selector queue:
| Selector | Handler | Timer/latch effect |
| --- | --- | --- |
| `0x006C` | `H'2FAF` | completion/exit sibling; can clear `F731.7`, load `F797=0x1E` or `0x14`, and display `COPY COMPLETED` |
| `0x006D` | `H'3015` | start/progress; sets `F731.7`, `F795.6`, `F795.7`, `F798=0xC8`, displays `COPY IN PROGRESS` |
| `0x006E` | via command-5 special path | appended to `F970`; exact selector handler still needs labeling |
FRT2 OCIA timeout path:
- `H'BFA3-H'BFAF`: if `F797` is nonzero, decrement it; when it reaches zero, clear `F731.7`.
- `H'BFB3-H'BFBF`: if `F798` is nonzero, decrement it; when it reaches zero, clear `F731.7`.
Practical meaning:
- `F731.7` is an activity/copy/session latch with explicit timeout clear paths.
- `0x006D` refreshes the long progress window (`F798=0xC8` and `FB02=0x64`).
- `0x006C` only makes sense as a completion/exit frame when the copy/progress flags are live.
## Reset / NOT ACT Baseline
`H'400C` clears broad session/display state:
- clears `F732`, `FB03`, `F791`, `F795`, `F76E`
- calls `H'4217`
`H'4217`:
- clears `F798`
- sets `F731.7`
- clears output masks and writes the LCD text `CONNECT:NOT ACT`
This looks like the firmware's explicit inactive-session baseline.
## Current Rhythm Hypothesis
The session rhythm is probably not one magic keepalive frame. The ROM suggests a loop with three layers:
1. **Any complete six-byte RX frame** refreshes the broad `F9C5` session watchdog.
2. **Command-0 primary selector updates** keep both `E000` and `E800` current, append active selectors to `F970`, and reply.
3. **Command-4 continuation primary updates** keep `E000` current and append selectors to `F970`, but only selector zero directly refreshes `E800`.
4. **Autonomous reports from `F870`** must be consumed by continuation commands so the report cursor advances.
5. **Command-6 secondary selector updates** advertise which features/menu entries are valid through `E400`.
6. **Command-5 ACK/special selectors** advance report windows and drive side-effect selectors such as `0x006C/0x006D`.
The panel stays active when enough selector updates hit the right active slots and refresh the timed/latch counters before FRT2 expires them.

View File

@@ -0,0 +1,214 @@
# PT2 Shutter Display Trace
This note collects the current ROM evidence for the shutter seven-segment display and related selector traffic.
## Confirmed Bench Mapping
The real panel responds to primary selector `0x008F`:
| Command 0 write | Meaning candidate | Observed panel result |
| --- | --- | --- |
| `00 01 0F 08 00 5C` | `E000[0x008F].11` | SHUTTER shows observed `EUS`, likely manual `EVS`; iris AUTO lamp on |
| `00 01 0F 10 00 44` | `E000[0x008F].12` | SHUTTER shows literal `OFF`; iris AUTO lamp on |
| `00 01 0F 18 00 4C` | bits 11 and 12 | likely `EVS` wins over `OFF`; iris AUTO lamp on |
| `00 01 0F 00 00 54` | live default/clear | in the short clear-control run, SHUTTER swapped to `OFF`, then back to `EVS` when bit 11 was restored |
Manual correlation:
- The RCP-TX7 manual has `OTHERS (1/6: SHUTTER)` with an `EVS` button.
- The RCP-TX7 menu table lists `EVS/ECS` under OTHERS for DXC-D30/D30P.
- A later CCU/RCP manual says the shutter display shows `EVS` when EVS is on and `OFF` when the shutter switch is off.
So `0x008F.11` is currently best labeled `shutter_evs_display_or_mode`. `0x008F.12` and `0x008F=0` both have `OFF` display evidence, so treat `OFF` as the live default/fallback shutter display unless a later bit-isolation run splits forced OFF from default OFF.
## Selector 0x008F ROM Path
Primary table address:
```text
selector 0x008F -> E000 + 2 * 0x008F = H'E11E
```
The OTHERS/SHUTTER menu handler is `H'6EE4`.
Important instructions:
```text
6F6E: BTST.W #11, @H'E11E
6F74: BSET.B #6, R0
6F76: BTST.W #12, @H'E11E
6F7C: BSET.B #4, R0
6F7E: MOV:G.B R0, @H'F711
```
This is the direct consumer seen so far. The current decompile does not show another static read of `H'E11E` outside this handler.
`H'6EE4` also programs local descriptor RAM before this:
```text
6EFD: F73E = H'088F
6F09: F74E = H'0800
6F0F: F742 = H'088F
6F1B: F752 = H'1000
```
Those descriptors match selector `0x008F` plus the two observed masks `0x0800` and `0x1000`, so the generic panel-output updater also appears to know about the same selector.
## Local Panel Trigger Path
`H'6EE4` can also generate a selector `0x008F` report/update from local OTHERS/SHUTTER controls:
```text
6F26: read F770 local action byte
6F2E: keep low two action bits
6F37: if action bit path and E400[0x008F].11 is enabled, set E800[0x008F] = 0x0800
6F45: if alternate action path and E400[0x008F].12 is enabled, set E800[0x008F] = 0x1000
6F53: R3 = 0x008F
6F64: call 3E54 report/target-frame builder
```
Practical meaning:
- The CCU can force the display state by writing `E000[0x008F]`.
- The RCP can also try to report local EVS/OFF changes through selector `0x008F`, but only when the secondary-table feature bits `E400[0x008F].11/.12` allow it.
Additional local-key trace:
- `F109 -> F6D0.7` handler `H'24E8` writes `E800[0x008F]=0x8000` or `0x0000`, then queues selector `0x008F`.
- `F109 -> F6D0.6` handler `H'252E` writes `E800[0x008F]=0x2000` or `0x0000`, then queues selector `0x008F`.
- Both handlers first check the current session/page gate (`F731 <= 2`) and both can divert to timed page `F732=0x1C01` when `E000[0x0088].14` is set.
This splits selector `0x008F` into at least two roles:
- CCU-visible display/status bits `0x0800` and `0x1000`, bench-mapped to `EVS` and `OFF`.
- Local key/report bits `0x8000` and `0x2000`, emitted by the physical panel path when gates allow.
So the display bits that light `EVS/OFF` are not necessarily the same bits the local SHUTTER/OTHERS-adjacent buttons emit.
## Adjacent Shutter Value Family
The periodic/control-change scanner at `H'15E0` processes changed input words collected by IRQ3 from the external panel chips. Around the shutter path it calls `H'19A2`, which compares a local delta against the current primary table value and, if changed, writes the `E800` current table and calls `H'3E54`.
Candidate selectors:
| Handler | Source RAM | Gate | Selector sent | Meaning candidate |
| --- | --- | --- | --- | --- |
| `H'17C9` | `F6AE/F6CE` | `E000[0x0093].12` | `0x00A3` | clear-scan/shutter value lane |
| `H'17FB` | `F6AC/F6CC` | `E000[0x0093].12` | `0x00A4` | clear-scan/shutter value lane |
| `H'182D` | `F6AA/F6CA` | `E000[0x0093].5`, `F717.2` clear | `0x00A5` | shutter value lane |
| `H'182D` | `F6AA/F6CA` | `F717.2` set | `0x00D8` | alternate/DXC-637 shutter lane |
| `H'1891` | `F6A8/F6C8` | `E000[0x0093].5`, `F717.2` clear | `0x0080` | shutter value lane |
| `H'1891` | `F6A8/F6C8` | `F717.2` set | `0x00D9` | alternate/DXC-637 shutter lane |
| `H'18E7` | `F6A6/F6C6` | `E000[0x0093].5`, `F717.2` clear | `0x00A6` | shutter value lane |
| `H'18E7` | `F6A6/F6C6` | `F717.2` set | `0x00DA` | alternate/DXC-637 shutter lane |
| `H'194A` | `F6A4/F6C4` | `F731 <= 3` | `0x0080` | shared value lane |
| `H'1979` | `F6A2/F6C2` plus `F68C` scale | `F731 <= 3` | `0x0081` | scaled analog value lane |
This cluster is a strong candidate for the numeric shutter speed / clear-scan frequency side of the display. It is not yet bench-confirmed.
ROM refinement:
- `F6AE/F6AC` are sampled from the external panel bus in the IRQ3 `A8` branch (`H'3CCB`), from `F00C/F00A`.
- `F6AA/F6A8/F6A6/F6A4/F6A2` are sampled from the IRQ3 `A9` branch (`H'3C49`), from `F00C/F00A/F008/F006/F004`.
- Those IRQ3 branches set dirty bits in `F6F1`, then the `H'15E0` scanner fans changes into the selector reports above.
- Several lanes can OR bit 14 into the report selector when `F791` and `F404` feature bits are live. So emitted report traffic may carry a tagged variant of the base selector even though the table readback probes below use the base selector.
- This makes the adjacent selector family more likely to be live local-panel state than a simple CCU-write display latch.
## Candidate Probe Frames
Readback:
```text
01 01 0F 00 00 55 ; read 0x008F
01 01 13 00 00 49 ; read 0x0093 gate/status
01 01 23 00 00 79 ; read 0x00A3
01 01 24 00 00 7E ; read 0x00A4
01 01 25 00 00 7F ; read 0x00A5
01 01 58 00 00 02 ; read 0x00D8
01 01 00 00 00 5A ; read 0x0080
01 01 59 00 00 03 ; read 0x00D9
01 01 26 00 00 7C ; read 0x00A6
01 01 5A 00 00 00 ; read 0x00DA
01 01 01 00 00 5B ; read 0x0081
```
Potential display probes after `CONNECT: OK`:
```text
00 01 0F 00 00 54 ; clear selector 0x008F
00 01 13 80 00 C8 ; set 0x0093 high bit candidate
00 01 13 FF FF 48 ; enable all 0x0093 gates candidate
```
Use the value-lane selectors cautiously. They may represent shutter/clear-scan numeric values, but they are also part of the local control-report path, so bench probing should change one selector at a time and record LCD, shutter display, iris AUTO, and emitted report frames.
Important methodology caveat:
- `CONNECT:NOT ACT` globally clears volatile panel presentation. A blank LCD/segment result after a long wait is not evidence that a tested selector cleared the panel.
- For selector-display tests, record the visible state inside the short post-write window, before the session can fall back to `NOT ACT`.
- Use the timeout-control scenario below to measure the current bench's natural EVS-to-NOT-ACT cleanup time. Treat results after that point as timeout-contaminated.
- Re-seed `CONNECT: OK` immediately before gate writes when a prior readback sweep may have consumed enough time for the visible state to expire.
## Adjacent Selector Bench Scenarios
These JSON scenarios are set up for the current bench runner and default to `38400 8E1`:
```text
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-adjacent-readback.json --parity E --log captures\shutter-adjacent-readback.txt --result-json captures\shutter-adjacent-readback-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-008f-evs-timeout-control.json --parity E --log captures\shutter-008f-evs-timeout-control.txt --result-json captures\shutter-008f-evs-timeout-control-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-008f-evs-clear-control.json --parity E --log captures\shutter-008f-evs-clear-control.txt --result-json captures\shutter-008f-evs-clear-control-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-gate-8000.json --parity E --log captures\shutter-0093-gate-8000.txt --result-json captures\shutter-0093-gate-8000-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-gate-ffff.json --parity E --log captures\shutter-0093-gate-ffff.txt --result-json captures\shutter-0093-gate-ffff-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-bit-isolation.json --parity E --log captures\shutter-0093-bit-isolation.txt --result-json captures\shutter-0093-bit-isolation-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-auto-candidates.json --parity E --log captures\shutter-0093-blackflare-auto-candidates.txt --result-json captures\shutter-0093-blackflare-auto-candidates-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-slow-marked.json --parity E --log captures\shutter-0093-blackflare-slow-marked.txt --result-json captures\shutter-0093-blackflare-slow-marked-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-field-groups.json --parity E --log captures\shutter-0093-blackflare-field-groups.txt --result-json captures\shutter-0093-blackflare-field-groups-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-field-confirm.json --parity E --log captures\shutter-0093-blackflare-field-confirm.txt --result-json captures\shutter-0093-blackflare-field-confirm-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-field-stream-confirm.json --parity E --log captures\shutter-0093-blackflare-field-stream-confirm.txt --result-json captures\shutter-0093-blackflare-field-stream-confirm-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-confirm-f020.json --parity E --log captures\shutter-0093-blackflare-confirm-f020.txt --result-json captures\shutter-0093-blackflare-confirm-f020-result.json
-Manual-auto-manual
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-confirm-ff20.json --parity E --log captures\shutter-0093-blackflare-confirm-ff20.txt --result-json captures\shutter-0093-blackflare-confirm-ff20-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-confirm-9f20.json --parity E --log captures\shutter-0093-blackflare-confirm-9f20.txt --result-json captures\shutter-0093-blackflare-confirm-9f20-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-confirm-90ff.json --parity E --log captures\shutter-0093-blackflare-confirm-90ff.txt --result-json captures\shutter-0093-blackflare-confirm-90ff-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-confirm-9fff.json --parity E --log captures\shutter-0093-blackflare-confirm-9fff.txt --result-json captures\shutter-0093-blackflare-confirm-9fff-result.json
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\shutter-0093-blackflare-lowbyte-groups.json --parity E --log captures\shutter-0093-blackflare-lowbyte-groups.txt --result-json captures\shutter-0093-blackflare-lowbyte-groups-result.json
```
Run order:
1. `shutter-adjacent-readback.json`: read-only baseline for the adjacent shutter cluster after the selector-zero `CONNECT: OK` seed.
2. `shutter-008f-evs-timeout-control.json`: measures how long EVS survives before `CONNECT:NOT ACT` clears it with no explicit clear command.
3. `shutter-008f-evs-clear-control.json`: checks whether the EVS/OFF display selector can be cleared live inside the safe pre-timeout window.
4. `shutter-0093-gate-8000.json`: tests the ROM-observed `0x0093` high-bit gate without enabling every unknown bit.
5. `shutter-0093-gate-ffff.json`: broader gate test if the targeted high-bit run is uneventful.
6. `shutter-0093-bit-isolation.json`: isolates the `0x0093` bits after bench evidence showed this selector also controls white-balance and black/flare lamps.
7. `shutter-0093-blackflare-auto-candidates.json`: uses the known `0x9020` black/flare-manual context and adds each remaining bit to hunt for the `0xFFFF` black/flare-AUTO effect.
8. `shutter-0093-blackflare-slow-marked.json`: repeats the hunt without clear/OK resets between candidates, so white-balance should flicker less and black/flare AUTO transitions can be matched to candidate holds.
9. `shutter-0093-blackflare-field-groups.json`: tests multi-bit field groups after the slow-marked run suggested black/flare AUTO is not one single extra bit over `0x9020`.
10. `shutter-0093-blackflare-field-confirm.json`: shorter, longer-hold confirmation of the broad field groups after the first field-groups run produced three MANUAL->AUTO->MANUAL swaps.
11. `shutter-0093-blackflare-field-stream-confirm.json`: repeats each candidate every 0.60 s after the longer-hold confirmation fell into `CONNECT:NOT ACT`, testing the CCU-refresh model directly.
12. `shutter-0093-blackflare-confirm-*.json`: one-candidate streamed confirmations. Each run includes a known `0xFFFF` AUTO positive control, then brackets one candidate with `0x9020` manual context. After the positive-control section, the expected candidate pattern is MANUAL -> AUTO -> MANUAL -> AUTO -> MANUAL only if that candidate drives black/flare AUTO.
13. `shutter-0093-blackflare-lowbyte-groups.json`: keeps the high byte fixed at `0x90` and varies only the low byte, after `0x90FF` and `0x9FFF` both toggled black/flare AUTO.
## Bench Observations 2026-05-26
Observed visible effects from the first adjacent-selector run set:
| Scenario | Serial evidence | Observed panel result | Current interpretation |
| --- | --- | --- | --- |
| `shutter-adjacent-readback` | all read selectors returned value `0x0000` | no specific visible note | baseline cluster was clear |
| `shutter-0093-gate-8000` | `E000[0x0093]` read back as `0x8000` | white-balance PRESET lamp on, black/flare MANUAL lamp on | `0x0093.15` is a live panel-status/lamp bit, not just a shutter gate |
| `shutter-0093-gate-ffff` | `E000[0x0093]` read back as `0xFFFF` | white-balance PRESET lamp on, black/flare AUTO lamp on | another `0x0093` bit overrides/selects black/flare AUTO when all bits are set |
| `shutter-0093-bit-isolation` | stepped through `0x1000`, `0x0020`, `0x1020`, `0x8000`, `0x8020`, `0x9000`, `0x9020` | white-balance lamps swapped between MANUAL and PRESET; black/flare stayed MANUAL | bit 15 remains the prime white-balance PRESET candidate; black/flare AUTO is likely outside bits 15/12/5 |
| `shutter-0093-blackflare-auto-candidates` | stepped through `0x9020`, `0xFFFF`, then `0x9020` plus each remaining bit | black/flare swapped AUTO/MANUAL more slowly than white-balance MANUAL/PRESET | the clear/OK reset before each candidate likely caused extra white-balance flicker; use the slow-marked follow-up to map black/flare transitions to exact bits |
| `shutter-0093-blackflare-slow-marked` | held `0x9020`, `0xFFFF`, then alternated `0x9020` with each candidate | white-balance stayed stable, black/flare swapped twice with large pauses; panel stayed `CONNECT: OK` throughout | likely only the manual-reference to `0xFFFF` and `0xFFFF` back to `0x9020` changed black/flare, so AUTO is probably a multi-bit field rather than one added bit |
| `shutter-0093-blackflare-field-groups` | held broad grouped values such as `0xF020`, `0xFF20`, `0x9F20`, `0x90FF`, `0x9FFF` | black/flare made three MANUAL->AUTO->MANUAL swaps | broad field combinations can produce black/flare AUTO; use the shorter confirmation run to identify which grouped windows caused the swaps |
| `shutter-0093-blackflare-field-confirm` | held `0xFFFF` for 2.8 s, then candidate groups | one MANUAL->AUTO transition at the start, then `CONNECT:NOT ACT`; PRESET white-balance lamp briefly flashed a few times while inactive | a silent 2.8 s hold is too long; traffic must be refreshed, and some lamp latch/update paths can still flash during inactive display cleanup |
| `shutter-0093-blackflare-field-stream-confirm` | streamed every candidate at 0.60 s and saw 148 `02 00 02 00 00 5A` OK-path responses with no resync errors | manual/auto/manual groups were visible, then `CONNECT:NOT ACT` after the stream ended | the stream itself kept OK alive; the final inactive state is likely just the refresh stopping. Candidate isolation needs streamed, one-candidate tests |
| `shutter-0093-blackflare-confirm-f020`, `ff20`, `9f20` | each completed cleanly with 137 OK-path responses and no resync errors | MANUAL->AUTO->MANUAL only | only the built-in `0xFFFF` positive control produced AUTO; these candidates did not drive black/flare AUTO by themselves |
| `shutter-0093-blackflare-confirm-90ff`, `9fff` | each completed cleanly with 137 OK-path responses and no resync errors | black/flare went back and forth a couple of times | `0x90FF` is sufficient to drive the AUTO effect, so the black/flare field is likely in the low byte of `0x0093`; `0x9FFF` is a superset rather than a separate high-byte requirement |
| `shutter-008f-evs-clear-control` | `0x008F` writes acknowledged as `0x0800`, `0x0000`, `0x0800` | SHUTTER swapped between EVS and OFF while iris AUTO lamp stayed on | `0x008F` is a packed shutter/display status word; clearing it does not simply mean "blank" |
| `shutter-008f-evs-timeout-control` | only `0x008F=0x0800` was sent after OK seed | EVS/iris AUTO, then `CONNECT:NOT ACT` | later blanking is timeout cleanup, not selector-clear evidence |
This changes the local naming: the "adjacent shutter" group should be treated as a broader camera-status and panel-output word cluster. `0x008F` currently covers shutter EVS/OFF plus iris AUTO side effects; `0x0093` covers at least white-balance manual/preset and black/flare manual/auto side effects, and still gates some ROM-observed shutter/clear-scan report lanes. Current black/flare evidence points to `0x0093` high byte `0x90` as enough context, with the low byte selecting the black/flare MANUAL/AUTO state.

View File

@@ -141,6 +141,9 @@ def format_frame(data: bytes) -> str:
def label_frame(frame: bytes) -> str:
labels = {
bytes.fromhex("0000000080DA"): "heartbeat",
bytes.fromhex("00000080805A"): "active_selector0_keepalive_report",
bytes.fromhex("00006C000036"): "copy_completion_exit_selector_006c_candidate",
bytes.fromhex("00006D000037"): "copy_in_progress_selector_006d_candidate",
bytes.fromhex("02000200005A"): "connect_ok_path_response_candidate",
bytes.fromhex("010002000059"): "connect_c0_path_response_candidate",
bytes.fromhex("07804040A07D"): "visible_40A0_family_40",
@@ -148,6 +151,38 @@ def label_frame(frame: bytes) -> str:
bytes.fromhex("0780C040A0FD"): "visible_40A0_family_C0",
bytes.fromhex("07804020902D"): "visible_retry_0040_2090_candidate",
bytes.fromhex("0780C060205D"): "visible_C0_6020_family_candidate",
bytes.fromhex("0000158000CF"): "known_call_button_active_report",
bytes.fromhex("00001500004F"): "known_call_button_inactive_report",
bytes.fromhex("0000078000DD"): "known_cam_power_button_report",
bytes.fromhex("000013000049"): "known_iris_mblack_link_clear_report_candidate",
bytes.fromhex("000013400009"): "known_iris_mblack_link_active_report_candidate",
bytes.fromhex("0000138000C9"): "known_selector_0013_bit15_report_candidate",
bytes.fromhex("000013C00089"): "known_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
bytes.fromhex("00010F8000D4"): "known_shutter_onoff_bit7_report_candidate",
bytes.fromhex("00010F200074"): "known_shutter_onoff_bit6_report_candidate",
bytes.fromhex("00010F000054"): "known_shutter_onoff_clear_report_candidate",
bytes.fromhex("010013000048"): "queued_iris_mblack_link_clear_report_candidate",
bytes.fromhex("02001300004B"): "queued_iris_mblack_link_clear_report_candidate",
bytes.fromhex("010013400008"): "queued_iris_mblack_link_active_report_candidate",
bytes.fromhex("02001340000B"): "queued_iris_mblack_link_active_report_candidate",
bytes.fromhex("0100138000C8"): "queued_selector_0013_bit15_report_candidate",
bytes.fromhex("0200138000CB"): "queued_selector_0013_bit15_report_candidate",
bytes.fromhex("010013C00088"): "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
bytes.fromhex("020013C0008B"): "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
bytes.fromhex("01010F8000D5"): "queued_shutter_onoff_bit7_report_candidate",
bytes.fromhex("02010F8000D6"): "queued_shutter_onoff_bit7_report_candidate",
bytes.fromhex("01010F200075"): "queued_shutter_onoff_bit6_report_candidate",
bytes.fromhex("02010F200076"): "queued_shutter_onoff_bit6_report_candidate",
bytes.fromhex("01010F000055"): "queued_shutter_onoff_clear_report_candidate",
bytes.fromhex("02010F000056"): "queued_shutter_onoff_clear_report_candidate",
bytes.fromhex("0100178000CC"): "queued_bars_button_selector_0017_active_candidate",
bytes.fromhex("0200178000CF"): "queued_bars_button_selector_0017_active_candidate",
bytes.fromhex("0100188000C3"): "queued_bars_button_selector_0018_active_candidate",
bytes.fromhex("0200188000C0"): "queued_bars_button_selector_0018_active_candidate",
bytes.fromhex("01011A080048"): "queued_iris_auto_button_selector_009a_active_candidate",
bytes.fromhex("02011A08004B"): "queued_iris_auto_button_selector_009a_active_candidate",
bytes.fromhex("01000400005F"): "gated_active_0004_response_candidate",
bytes.fromhex("02000400005C"): "gated_active_0004_transition_candidate",
}
label = labels.get(frame, "")
if label:

168
h8536/camera_snapshots.py Normal file
View File

@@ -0,0 +1,168 @@
from __future__ import annotations
import re
import heapq
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9_.-]+")
class CameraSnapshots:
"""Small optional OpenCV webcam wrapper for bench-test snapshots."""
def __init__(
self,
*,
camera_index: int,
output_dir: Path,
warmup_seconds: float = 0.5,
width: int | None = None,
height: int | None = None,
) -> None:
self.camera_index = camera_index
self.output_dir = output_dir
self.warmup_seconds = max(0.0, warmup_seconds)
self.width = width
self.height = height
self.cv2: Any | None = None
self.device: Any | None = None
self._condition = threading.Condition()
self._tasks: list[tuple[float, int, dict[str, Any]]] = []
self._records: list[dict[str, Any]] = []
self._thread: threading.Thread | None = None
self._seq = 0
self._closing = False
def open(self) -> None:
try:
import cv2
except ImportError as exc: # pragma: no cover - depends on local bench environment.
raise SystemExit(
"OpenCV is required for webcam snapshots. Install it with: "
".\\.venv\\Scripts\\python.exe -m pip install opencv-python"
) from exc
self.output_dir.mkdir(parents=True, exist_ok=True)
self.cv2 = cv2
self.device = cv2.VideoCapture(self.camera_index)
if not self.device.isOpened():
raise SystemExit(f"could not open webcam index {self.camera_index}")
if self.width:
self.device.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
if self.height:
self.device.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
self._warm_up()
self._thread = threading.Thread(target=self._run, name="camera-snapshots", daemon=True)
self._thread.start()
def close(self) -> None:
with self._condition:
self._closing = True
self._condition.notify_all()
if self._thread is not None:
self._thread.join()
self._thread = None
if self.device is not None:
self.device.release()
self.device = None
def schedule(
self,
*,
label: str,
frame_text: str,
phase: str,
delay_seconds: float = 0.0,
step_index: int | None = None,
) -> dict[str, Any]:
if self.cv2 is None or self.device is None:
raise RuntimeError("camera snapshotter is not open")
due_monotonic = time.monotonic() + max(0.0, delay_seconds)
due_timestamp = datetime.now() + timedelta(seconds=max(0.0, delay_seconds))
timestamp = due_timestamp.strftime("%Y%m%d-%H%M%S-%f")[:-3]
step_part = f"step{step_index:03d}_" if step_index is not None else ""
frame_part = frame_text.replace(" ", "")
filename = _safe_name(f"{timestamp}_{step_part}{phase}_{label}_{frame_part}.jpg")
path = self.output_dir / filename
record: dict[str, Any] = {
"path": str(path),
"scheduled_timestamp": timestamp,
"camera_index": self.camera_index,
"step_index": step_index,
"phase": phase,
"delay_seconds": delay_seconds,
"label": label,
"frame": frame_text,
"status": "scheduled",
}
with self._condition:
self._seq += 1
heapq.heappush(self._tasks, (due_monotonic, self._seq, record))
self._records.append(record)
self._condition.notify_all()
return record
def records(self) -> list[dict[str, Any]]:
with self._condition:
return [dict(record) for record in self._records]
def _run(self) -> None:
while True:
with self._condition:
while not self._tasks and not self._closing:
self._condition.wait()
if not self._tasks and self._closing:
return
due_monotonic, _seq, record = self._tasks[0]
wait_seconds = due_monotonic - time.monotonic()
if wait_seconds > 0:
self._condition.wait(wait_seconds)
continue
heapq.heappop(self._tasks)
self._write_snapshot(record)
def _write_snapshot(self, record: dict[str, Any]) -> None:
if self.cv2 is None or self.device is None:
record["status"] = "error"
record["error"] = "camera closed before capture"
return
image = None
ok = False
for _attempt in range(3):
ok, image = self.device.read()
if ok:
break
time.sleep(0.020)
if not ok or image is None:
record["status"] = "error"
record["error"] = f"webcam index {self.camera_index} did not return an image"
return
path = Path(str(record["path"]))
if not self.cv2.imwrite(str(path), image):
record["status"] = "error"
record["error"] = f"failed to write webcam snapshot {path}"
return
record["status"] = "written"
record["captured_timestamp"] = datetime.now().strftime("%Y%m%d-%H%M%S-%f")[:-3]
def _warm_up(self) -> None:
if self.device is None:
return
deadline = time.monotonic() + self.warmup_seconds
while time.monotonic() < deadline:
self.device.read()
time.sleep(0.020)
self.device.read()
def _safe_name(text: str) -> str:
cleaned = _SAFE_NAME_RE.sub("_", text.strip()).strip("._")
return cleaned or "snapshot.jpg"

View File

@@ -9,6 +9,7 @@ from typing import Any
from .formatting import h16
from .lcd_text import analyze_lcd_text
from .panel_selectors import panel_selector_semantics_payload
from .rom import Rom
from .serial_semantics import OBSERVED_TX_REPORT_OVERLAY
from .table_xrefs import analyze_table_xrefs
@@ -125,6 +126,7 @@ def analyze_ccu_seed_hints(payload: Mapping[str, Any], *, rom_path: Path | None
selector_hints = _selector_hints_from_tables(table_analysis)
_merge_special_selectors(selector_hints)
_merge_observed_reports(selector_hints)
_merge_panel_selector_semantics(selector_hints)
dispatch = _dispatch_table_summary(payload, rom_path)
for entry in dispatch.get("interesting_entries", []):
@@ -362,6 +364,30 @@ def _merge_observed_reports(hints: dict[int, JsonObject]) -> None:
hint["reasons"].append(f"observed RCP autonomous report frame(s): {frames}")
def _merge_panel_selector_semantics(hints: dict[int, JsonObject]) -> None:
for item in panel_selector_semantics_payload():
selector = int(item["selector"])
hint = hints.setdefault(selector, _new_selector_hint(selector))
hint["score"] += 4
hint["name"] = str(item.get("name") or hint["name"])
summary = str(item.get("summary") or "").strip()
if summary:
hint["reasons"].append(summary)
for effect in item.get("effects", []):
if not isinstance(effect, Mapping):
continue
name = effect.get("name") or "panel effect"
mask = effect.get("mask_hex") or "mask?"
when_set = effect.get("when_set") or "set"
hint["reasons"].append(f"{mask} {name}: {when_set}")
for meaning in item.get("value_meanings", []):
if not isinstance(meaning, Mapping):
continue
value = meaning.get("value")
if isinstance(value, int):
_add_seed_value(hint, value)
def _seed_plan(hints: Mapping[int, JsonObject]) -> JsonObject:
planned = [
(0x000, 0x8080, "selector zero active/connect candidate from emulator state search"),
@@ -595,3 +621,7 @@ __all__ = [
"selector_bytes",
"write_ccu_seed_hints",
]
if __name__ == "__main__":
raise SystemExit(main())

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

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

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

307
h8536/panel_button_trace.py Normal file
View File

@@ -0,0 +1,307 @@
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",
0x0013: "IRIS/M.BLACK LINK",
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())

349
h8536/panel_selectors.py Normal file
View File

@@ -0,0 +1,349 @@
from __future__ import annotations
from copy import deepcopy
from typing import Any
JsonObject = dict[str, Any]
CURRENT_TABLE_BASE = 0xE800
PRIMARY_TABLE_BASE = 0xE000
SECONDARY_TABLE_BASE = 0xE400
PANEL_SELECTOR_SEMANTICS: tuple[JsonObject, ...] = (
{
"selector": 0x0013,
"selector_hex": "0x0013",
"name": "slave_and_iris_mblack_link_lamps",
"summary": (
"Selector 0x0013 is a two-bit lamp/status word. ROM dispatch H'2E06 "
"reads current table word H'E826 and fans bit 15 and bit 14 into panel latch RAM."
),
"state_machine": {
"name_candidate": "iris_mblack_link_closed_loop_state_candidate",
"summary": (
"Bench-proven closed loop: the RCP reports local IRIS/M.BLACK LINK intent, "
"the CCU ACKs selector 0x0013, then the CCU mirrors the accepted selector "
"state back with command 0. The mirrored state controls the next toggle direction."
),
"active_report_frame": "00 00 13 40 00 09",
"clear_report_frame": "00 00 13 00 00 49",
"ack_frame": "05 00 13 00 00 4C",
"active_mirror_frame": "00 00 13 40 00 09",
"clear_mirror_frame": "00 00 13 00 00 49",
"confidence": "bench-high",
},
"primary_word_address": PRIMARY_TABLE_BASE + 0x0013 * 2,
"primary_word_address_hex": "H'E026",
"current_word_address": CURRENT_TABLE_BASE + 0x0013 * 2,
"current_word_address_hex": "H'E826",
"dispatch_handler": "H'2E06",
"confidence": "high",
"evidence": [
"bench: 00 00 13 80 00 C9 lights far-right SLAVE lamp",
"bench: 00 00 13 40 00 09 lights IRIS/M.BLACK LINK lamp",
"ROM: H'2E06-H'2E32 tests H'E826 bits 15/14 and sets/clears F791/F713/F716 latch bits",
],
"effects": [
{
"bit": 15,
"mask": 0x8000,
"mask_hex": "0x8000",
"name": "SLAVE lamp",
"when_set": "sets F791.6 and F713.4",
"when_clear": "clears F791.6 and F713.4",
"ram_bits": ["F791.6", "F713.4"],
"handler_range": "H'2E06-H'2E1A",
"confidence": "high",
},
{
"bit": 14,
"mask": 0x4000,
"mask_hex": "0x4000",
"name": "IRIS/M.BLACK LINK lamp",
"when_set": "sets F791.5 and F716.7",
"when_clear": "clears F791.5 and F716.7",
"ram_bits": ["F791.5", "F716.7"],
"handler_range": "H'2E1E-H'2E32",
"confidence": "high",
},
],
"local_triggers": [
{
"kind": "panel_input_toggle",
"source": "F006.7 / F6DB.7",
"handler": "H'200E",
"name_candidate": "provisional_iris_mblack_link_button_toggle_report",
"summary": (
"When F6DB.7 is asserted and F731 <= 3, the ROM toggles current-table "
"bit 14 at H'E826 based on F791.5, then queues selector 0x0013 through loc_3E54."
),
"gate": "F731 <= 3",
"current_state_bit": "F791.5",
"active_value": 0x4000,
"active_value_hex": "0x4000",
"clear_value": 0x0000,
"clear_value_hex": "0x0000",
"writes": ["H'E826 bit14"],
"queue_call": "loc_3E54",
"confidence": "medium-high",
},
{
"kind": "local_helper_set_clear",
"source": "H'1FE8/H'1FFB",
"handler": "H'1FE8-H'200D",
"summary": "Adjacent local helpers set or clear current-table bit 15 at H'E826 and queue selector 0x0013.",
"writes": ["H'E826 bit15"],
"queue_call": "loc_3E54",
"confidence": "medium",
},
],
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "SLAVE lamp on", "confidence": "high"},
{"value": 0x4000, "value_hex": "0x4000", "meaning": "IRIS/M.BLACK LINK lamp on", "confidence": "high"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "SLAVE and IRIS/M.BLACK LINK latch bits clear through H'2E06", "confidence": "high"},
],
},
{
"selector": 0x0015,
"selector_hex": "0x0015",
"name": "call_and_red_tally_lamp_lane",
"summary": "Bench-visible CALL lamp and red tally lane; local CALL handler mirrors F6DB.5 into E800[0x0015].15.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0015 * 2,
"primary_word_address_hex": "H'E02A",
"current_word_address": CURRENT_TABLE_BASE + 0x0015 * 2,
"current_word_address_hex": "H'E82A",
"dispatch_handler": "handler unknown",
"confidence": "bench-high",
"evidence": [
"bench: 00 00 15 80 00 CF lights CALL and red tally",
"ROM: H'20A1-H'20BA reads F6DB.5, writes H'E82A, and queues selector 0x0015",
],
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "CALL lamp and red tally on", "confidence": "bench-high"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "CALL inactive/clear report", "confidence": "bench-high"},
],
},
{
"selector": 0x0017,
"selector_hex": "0x0017",
"name": "bars_lamp_lane",
"summary": "Bench-visible BARS lamp/latch lane; low writes do not reliably clear the visible latch.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0017 * 2,
"primary_word_address_hex": "H'E02E",
"current_word_address": CURRENT_TABLE_BASE + 0x0017 * 2,
"current_word_address_hex": "H'E82E",
"dispatch_handler": "handler unknown",
"confidence": "bench-high",
"evidence": [
"bench: 00 00 17 80 00 CD lights BARS",
"bench: 00 00 17 40 00 0D also lights the BARS latch in neighbor sweep",
"ROM: H'1EDE can queue selector 0x0017 from F6D4.2",
],
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "BARS lamp on", "confidence": "bench-high"},
{"value": 0x4000, "value_hex": "0x4000", "meaning": "BARS lamp/latch on", "confidence": "bench-medium-high"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "BARS low write; visible latch may remain", "confidence": "bench-medium"},
],
},
{
"selector": 0x001A,
"selector_hex": "0x001A",
"name": "monitor_selector_lamps",
"summary": "Bench-visible MONITOR selector cluster found from ROM-derived button-output sweep.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x001A * 2,
"primary_word_address_hex": "H'E034",
"current_word_address": CURRENT_TABLE_BASE + 0x001A * 2,
"current_word_address_hex": "H'E834",
"dispatch_handler": "handler unknown",
"confidence": "bench-high",
"evidence": [
"bench: 00 00 1A 08 08 40 lights MONITOR ENC",
"bench: 00 00 1A 20 20 40 lights MONITOR B",
"bench: 00 00 1A 40 40 40 lights MONITOR G",
"bench: 00 00 1A 80 80 40 lights MONITOR R",
"ROM: H'1CB2-H'1D56 writes packed values to H'E834 and queues selector 0x001A",
],
"value_meanings": [
{"value": 0x0808, "value_hex": "0x0808", "meaning": "MONITOR ENC lamp", "confidence": "bench-high"},
{"value": 0x2020, "value_hex": "0x2020", "meaning": "MONITOR B lamp", "confidence": "bench-high"},
{"value": 0x4040, "value_hex": "0x4040", "meaning": "MONITOR G lamp", "confidence": "bench-high"},
{"value": 0x8080, "value_hex": "0x8080", "meaning": "MONITOR R lamp", "confidence": "bench-high"},
],
},
{
"selector": 0x0024,
"selector_hex": "0x0024",
"name": "lcd_selector_button_lamp",
"summary": "Bench-visible LCD selector-button lamp lane.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0024 * 2,
"primary_word_address_hex": "H'E048",
"current_word_address": CURRENT_TABLE_BASE + 0x0024 * 2,
"current_word_address_hex": "H'E848",
"confidence": "bench-medium",
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "LCD selector-button lamp visible", "confidence": "bench-medium"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "lamp remained visible at 0.5 s in isolation run", "confidence": "bench-low"},
],
},
{
"selector": 0x006B,
"selector_hex": "0x006B",
"name": "standard_lamp_lane",
"summary": "Bench-visible STANDARD lamp lane found from ROM-derived F6D4.6 handler candidate.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x006B * 2,
"primary_word_address_hex": "H'E0D6",
"current_word_address": CURRENT_TABLE_BASE + 0x006B * 2,
"current_word_address_hex": "H'E8D6",
"dispatch_handler": "handler unknown",
"confidence": "bench-high",
"evidence": [
"bench: 00 00 6B 80 00 B1 lights STANDARD",
"ROM: H'2048 can write H'E8D6=0x8000 and queue selector 0x006B from F6D4.6",
],
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "STANDARD lamp on", "confidence": "bench-high"},
],
},
{
"selector": 0x0082,
"selector_hex": "0x0082",
"name": "iris_readout_lane",
"summary": "Bench-visible IRIS seven-segment/display lane.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0082 * 2,
"primary_word_address_hex": "H'E104",
"current_word_address": CURRENT_TABLE_BASE + 0x0082 * 2,
"current_word_address_hex": "H'E904",
"confidence": "bench-high",
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "IRIS display OP", "confidence": "bench-high"},
{"value": 0x4000, "value_hex": "0x4000", "meaning": "IRIS display 1.4", "confidence": "bench-high"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "IRIS display blank", "confidence": "bench-high"},
],
},
{
"selector": 0x0083,
"selector_hex": "0x0083",
"name": "combined_iris_shutter_master_gain_status_lane",
"summary": "Bench-visible combined status/readout lane; clear behavior appears latched or copied elsewhere.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0083 * 2,
"primary_word_address_hex": "H'E106",
"current_word_address": CURRENT_TABLE_BASE + 0x0083 * 2,
"current_word_address_hex": "H'E906",
"confidence": "bench-medium-high",
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "IRIS AUTO, SHUTTER OFF, MASTER GAIN -3", "confidence": "bench-medium-high"},
{"value": 0x4000, "value_hex": "0x4000", "meaning": "IRIS AUTO, SHUTTER OFF, MASTER GAIN 0", "confidence": "bench-high"},
{"value": 0x2000, "value_hex": "0x2000", "meaning": "IRIS AUTO, SHUTTER OFF, MASTER GAIN 3", "confidence": "bench-high"},
{"value": 0x0004, "value_hex": "0x0004", "meaning": "IRIS AUTO, SHUTTER OFF, MASTER GAIN HP", "confidence": "bench-high"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "same visible state remained at 0.5 s", "confidence": "bench-low"},
],
},
{
"selector": 0x008F,
"selector_hex": "0x008F",
"name": "shutter_display_status_lane",
"summary": "Bench-visible shutter/status display lane; local F6D0.6/F6D0.7 handlers also queue this selector.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x008F * 2,
"primary_word_address_hex": "H'E11E",
"current_word_address": CURRENT_TABLE_BASE + 0x008F * 2,
"current_word_address_hex": "H'E91E",
"confidence": "bench-high",
"evidence": [
"bench: 00 01 0F 80 00 D4 shows IRIS AUTO and shutter value beginning with 1",
"bench: 00 01 0F 20 00 74 shows IRIS AUTO and shutter 00.0",
"bench: 00 01 0F 08 00 5C shows IRIS AUTO and shutter EVS",
"bench: 00 01 0F 10 00 44 shows IRIS AUTO and shutter OFF",
"ROM: H'24E8/H'252E write H'E91E and queue selector 0x008F from F6D0.7/F6D0.6",
],
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "IRIS AUTO plus shutter value beginning with 1", "confidence": "bench-medium-high"},
{"value": 0x2000, "value_hex": "0x2000", "meaning": "IRIS AUTO plus shutter 00.0", "confidence": "bench-high"},
{"value": 0x0800, "value_hex": "0x0800", "meaning": "IRIS AUTO plus shutter EVS", "confidence": "bench-high"},
{"value": 0x1000, "value_hex": "0x1000", "meaning": "IRIS AUTO plus shutter OFF", "confidence": "bench-high"},
],
},
{
"selector": 0x0093,
"selector_hex": "0x0093",
"name": "white_balance_black_flare_mode_lane",
"summary": "Bench-visible white-balance and black/flare lamp lane.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0093 * 2,
"primary_word_address_hex": "H'E126",
"current_word_address": CURRENT_TABLE_BASE + 0x0093 * 2,
"current_word_address_hex": "H'E926",
"confidence": "bench-high",
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "BLACK/FLARE MANUAL plus white-balance PRESET", "confidence": "bench-high"},
{"value": 0x4000, "value_hex": "0x4000", "meaning": "BLACK/FLARE MANUAL plus white-balance AUTO", "confidence": "bench-high"},
{"value": 0x2000, "value_hex": "0x2000", "meaning": "BLACK/FLARE MANUAL plus white-balance MANUAL", "confidence": "bench-high"},
{"value": 0x1020, "value_hex": "0x1020", "meaning": "BLACK/FLARE MANUAL plus white-balance MANUAL", "confidence": "bench-high"},
{"value": 0x4040, "value_hex": "0x4040", "meaning": "BLACK/FLARE AUTO plus white-balance AUTO", "confidence": "bench-high"},
{"value": 0x8040, "value_hex": "0x8040", "meaning": "BLACK/FLARE AUTO plus white-balance PRESET", "confidence": "bench-high"},
{"value": 0x0020, "value_hex": "0x0020", "meaning": "BLACK/FLARE MANUAL plus white-balance MANUAL", "confidence": "bench-high"},
{"value": 0x0040, "value_hex": "0x0040", "meaning": "BLACK/FLARE AUTO plus white-balance MANUAL", "confidence": "bench-high"},
{"value": 0x0000, "value_hex": "0x0000", "meaning": "BLACK/FLARE MANUAL plus white-balance MANUAL", "confidence": "bench-high"},
],
},
{
"selector": 0x0110,
"selector_hex": "0x0110",
"name": "knee_auto_lamp_or_page_status_lane",
"summary": "Bench-visible KNEE AUTO source; ROM notes indicate timed KNEE/detail page interaction.",
"primary_word_address": PRIMARY_TABLE_BASE + 0x0110 * 2,
"primary_word_address_hex": "H'E220",
"current_word_address": CURRENT_TABLE_BASE + 0x0110 * 2,
"current_word_address_hex": "H'EA20",
"confidence": "bench-high",
"evidence": [
"bench: 00 01 90 80 00 4B lights KNEE AUTO",
"ROM: KNEE notes identify E000[0x0110].15 as a strong KNEE AUTO/timed display source",
],
"value_meanings": [
{"value": 0x8000, "value_hex": "0x8000", "meaning": "KNEE AUTO lamp/status on", "confidence": "bench-high"},
],
},
)
def panel_selector_semantics_payload() -> list[JsonObject]:
return deepcopy(list(PANEL_SELECTOR_SEMANTICS))
def known_panel_selector(selector: int) -> JsonObject | None:
normalized = selector & 0x01FF
for item in PANEL_SELECTOR_SEMANTICS:
if int(item["selector"]) == normalized:
return deepcopy(item)
return None
def selector_word_address(table_base: int, selector: int) -> int:
return (table_base + ((selector & 0x01FF) * 2)) & 0xFFFF
def describe_selector_value(selector: int, value: int) -> list[str]:
item = known_panel_selector(selector)
if item is None:
return []
normalized_value = value & 0xFFFF
lines: list[str] = []
for meaning in item.get("value_meanings", []):
if not isinstance(meaning, dict) or meaning.get("value") != normalized_value:
continue
lines.append(str(meaning.get("meaning") or "known panel selector value"))
for effect in item.get("effects", []):
if not isinstance(effect, dict) or not isinstance(effect.get("mask"), int):
continue
mask = int(effect["mask"]) & 0xFFFF
state = "set" if normalized_value & mask else "clear"
action = effect.get("when_set") if state == "set" else effect.get("when_clear")
name = str(effect.get("name") or f"bit {effect.get('bit', '?')}")
if action:
lines.append(f"{name}: bit {effect.get('bit', '?')} {state}; {action}")
else:
lines.append(f"{name}: bit {effect.get('bit', '?')} {state}")
return lines

View File

@@ -278,6 +278,7 @@ def _declarations(tx_candidate: JsonObject | None, rx_candidate: JsonObject | No
"typedef uint8_t u8;",
"typedef uint16_t u16;",
"",
"#define BIT(n) (1u << (n))",
"extern volatile u8 MEM8[0x10000];",
"",
f"#define {channel}_SCR MEM8[{_c_hex(scr)}]",
@@ -424,6 +425,7 @@ def _semantics_lines(
lines.extend(_command_effect_comment_lines(protocol.get("command_effects"), opts, prefix=" * "))
lines.extend(_response_schema_comment_lines(_schema_list(protocol), opts, prefix=" * "))
lines.extend(_table_map_comment_lines(_table_map_list(protocol), opts, prefix=" * "))
lines.extend(_panel_selector_comment_lines(protocol.get("panel_selector_semantics"), opts, prefix=" * "))
lines.extend(_state_variable_comment_lines(protocol.get("state_variable_candidates"), opts, prefix=" * "))
lines.extend(_retry_error_comment_lines(protocol.get("retry_error_model"), opts, prefix=" * "))
lines.extend(_gate_queue_comment_lines(protocol.get("gate_queue_model"), opts, prefix=" * "))
@@ -466,6 +468,8 @@ def _semantics_lines(
)
lines.extend(_gate_queue_predicate_function_lines(protocol.get("gate_queue_model")))
lines.extend(_timer_architecture_function_lines(protocol))
lines.extend(_panel_selector_function_lines(protocol.get("panel_selector_semantics")))
lines.extend(_panel_selector_provisional_function_lines(protocol.get("panel_selector_semantics")))
lines.extend(
[
"void sci1_process_candidate_protocol_command(void)",
@@ -474,6 +478,8 @@ def _semantics_lines(
" u16 logical_index = sci1_rx_candidate_logical_index();",
" u16 value = sci1_rx_candidate_value();",
"",
" sci1_candidate_panel_selector_annotation(logical_index, value);",
"",
],
)
lines.extend(_command_dispatch_switch_lines(commands, opts))
@@ -644,6 +650,70 @@ def _table_map_comment_lines(
return lines
def _panel_selector_comment_lines(
value: object,
opts: SerialPseudocodeOptions,
*,
prefix: str,
) -> list[str]:
selectors = _object_list(value)
if not selectors:
return []
lines = [f"{prefix}panel selector semantics:"]
for selector in selectors[:6]:
selector_hex = selector.get("selector_hex") or _selector_hex(selector.get("selector"))
name = selector.get("name") or "panel_selector"
current = selector.get("current_word_address_hex") or "current table"
dispatch = selector.get("dispatch_handler") or "dispatch unknown"
summary = _comment_text(str(selector.get("summary") or "bench/ROM selector annotation"))
lines.append(f"{prefix}- {selector_hex} {name}: {summary}")
lines.append(f"{prefix} current word: {current}; dispatch: {dispatch}")
for effect in _object_list(selector.get("effects"))[:4]:
mask = effect.get("mask_hex") or _selector_hex(effect.get("mask"))
effect_name = effect.get("name") or "effect"
when_set = _comment_text(str(effect.get("when_set") or "set"))
bits = ", ".join(str(item) for item in effect.get("ram_bits", []))
suffix = f"; RAM {bits}" if bits else ""
lines.append(f"{prefix} {mask} -> {effect_name}: {when_set}{suffix}")
meanings = []
for meaning in _object_list(selector.get("value_meanings"))[:4]:
value_hex = meaning.get("value_hex") or _selector_hex(meaning.get("value"))
label = _comment_text(str(meaning.get("meaning") or "known panel value"))
meanings.append(f"{value_hex} {label}")
if meanings:
lines.append(f"{prefix} observed values: {'; '.join(meanings)}")
state_machine = selector.get("state_machine")
if isinstance(state_machine, dict):
name_candidate = state_machine.get("name_candidate") or "selector_state_machine_candidate"
summary = _comment_text(str(state_machine.get("summary") or "bench-proven selector state-machine candidate"))
lines.append(f"{prefix} state machine: {name_candidate}: {summary}")
active = state_machine.get("active_report_frame")
clear = state_machine.get("clear_report_frame")
ack = state_machine.get("ack_frame")
mirror_active = state_machine.get("active_mirror_frame")
mirror_clear = state_machine.get("clear_mirror_frame")
if active or clear or ack:
lines.append(
f"{prefix} frames: active report {active or '?'}; clear report {clear or '?'}; "
f"ACK {ack or '?'}; mirror active {mirror_active or '?'}; mirror clear {mirror_clear or '?'}"
)
triggers = []
for trigger in _object_list(selector.get("local_triggers"))[:3]:
source = trigger.get("source") or trigger.get("handler") or "local path"
summary = _comment_text(str(trigger.get("summary") or "local trigger candidate"))
name_candidate = trigger.get("name_candidate")
prefix_text = f"{name_candidate} " if name_candidate else ""
triggers.append(f"{prefix_text}{source}: {summary}")
if triggers:
lines.append(f"{prefix} local trigger candidates: {'; '.join(triggers)}")
evidence = ", ".join(str(item) for item in selector.get("evidence", []) if item)
if opts.include_evidence and evidence:
lines.append(f"{prefix} evidence: {_comment_text(evidence)}")
if len(selectors) > 6:
lines.append(f"{prefix}- ... {len(selectors) - 6} more panel selector annotations")
return lines
def _state_variable_comment_lines(
value: object,
opts: SerialPseudocodeOptions,
@@ -955,6 +1025,121 @@ def _timer_architecture_function_lines(protocol: JsonObject) -> list[str]:
)
def _panel_selector_function_lines(value: object) -> list[str]:
selectors = _object_list(value)
if not selectors:
return [
"static void sci1_candidate_panel_selector_annotation(u16 logical_index, u16 value)",
"{",
" (void)logical_index;",
" (void)value;",
"}",
"",
]
lines = [
"static void sci1_candidate_panel_selector_annotation(u16 logical_index, u16 value)",
"{",
" /* Known bench/ROM selector labels. This helper is commentary for the decompile. */",
" switch (logical_index) {",
]
for selector in selectors:
selector_value = selector.get("selector")
if not isinstance(selector_value, int):
continue
selector_hex = selector.get("selector_hex") or f"0x{selector_value:04X}"
name = _comment_text(str(selector.get("name") or "panel selector"))
current = selector.get("current_word_address_hex") or "current table"
dispatch = selector.get("dispatch_handler") or "dispatch unknown"
lines.append(f" case 0x{selector_value & 0x01FF:04X}u:")
lines.append(f" /* {selector_hex} {name}; current word {current}; {dispatch}. */")
for effect in _object_list(selector.get("effects")):
mask = effect.get("mask")
if not isinstance(mask, int):
continue
effect_name = _comment_text(str(effect.get("name") or "panel effect"))
when_set = _comment_text(str(effect.get("when_set") or "set"))
when_clear = _comment_text(str(effect.get("when_clear") or "clear"))
lines.append(f" if ((value & 0x{mask & 0xFFFF:04X}u) != 0u) {{")
lines.append(f" /* {effect_name}: {when_set}. */")
lines.append(" } else {")
lines.append(f" /* {effect_name}: {when_clear}. */")
lines.append(" }")
for meaning in _object_list(selector.get("value_meanings")):
known_value = meaning.get("value")
if not isinstance(known_value, int):
continue
label = _comment_text(str(meaning.get("meaning") or "known panel value"))
lines.append(f" if (value == 0x{known_value & 0xFFFF:04X}u) {{")
lines.append(f" /* {label}. */")
lines.append(" }")
lines.append(" break;")
lines.extend(
[
" default:",
" break;",
" }",
"}",
"",
],
)
return lines
def _panel_selector_provisional_function_lines(value: object) -> list[str]:
selectors = _object_list(value)
lines: list[str] = []
for selector in selectors:
selector_value = selector.get("selector")
if not isinstance(selector_value, int):
continue
state_machine = selector.get("state_machine")
if not isinstance(state_machine, dict):
continue
for trigger in _object_list(selector.get("local_triggers")):
name = str(trigger.get("name_candidate") or "").strip()
if not name:
continue
handler = _comment_text(str(trigger.get("handler") or "handler unknown"))
source = _comment_text(str(trigger.get("source") or "source unknown"))
gate = _comment_text(str(trigger.get("gate") or "gate unknown"))
current_bit = _comment_text(str(trigger.get("current_state_bit") or "current state bit unknown"))
summary = _comment_text(str(trigger.get("summary") or "local trigger candidate"))
active_value = _int_from_object(trigger.get("active_value"), 0x4000)
clear_value = _int_from_object(trigger.get("clear_value"), 0x0000)
active_report = _comment_text(str(state_machine.get("active_report_frame") or "active report unknown"))
clear_report = _comment_text(str(state_machine.get("clear_report_frame") or "clear report unknown"))
ack_frame = _comment_text(str(state_machine.get("ack_frame") or "ACK unknown"))
active_mirror = _comment_text(str(state_machine.get("active_mirror_frame") or "active mirror unknown"))
clear_mirror = _comment_text(str(state_machine.get("clear_mirror_frame") or "clear mirror unknown"))
safe_name = _safe_identifier(name)
lines.extend(
[
f"void {safe_name}(void)",
"{",
f" /* Provisional name for ROM {handler}: {summary} */",
f" /* Source {source}; gate {gate}; current state {current_bit}. */",
" if ((MEM8[0xF6DBu] & BIT(7)) == 0u) {",
" return;",
" }",
" if (MEM8[0xF731u] > 3u) {",
" return;",
" }",
"",
" if ((MEM8[0xF791u] & BIT(5)) == 0u) {",
f" /* Requests selector 0x{selector_value & 0x01FF:04X}=0x{active_value & 0xFFFF:04X}: {active_report}. */",
f" /* CCU should ACK {ack_frame}, then mirror {active_mirror}. */",
" } else {",
f" /* Requests selector 0x{selector_value & 0x01FF:04X}=0x{clear_value & 0xFFFF:04X}: {clear_report}. */",
f" /* CCU should ACK {ack_frame}, then mirror {clear_mirror}. */",
" }",
"}",
"",
],
)
return lines
def _timer_tick_function_lines(function_name: str, counters: list[JsonObject], summary: str) -> list[str]:
lines = [
f"void {function_name}(void)",
@@ -1130,6 +1315,16 @@ def _command_hex(value: object) -> str:
return "?"
def _selector_hex(value: object) -> str:
if isinstance(value, int):
return f"0x{value & 0xFFFF:04X}"
return "?"
def _int_from_object(value: object, default: int) -> int:
return value if isinstance(value, int) else default
def _tx_functions(candidate: JsonObject, opts: SerialPseudocodeOptions) -> list[str]:
length = _int_field(candidate, "frame_length", 6)
seed = _int_field(candidate, "checksum_seed", 0x5A)
@@ -1386,3 +1581,7 @@ def _safe_identifier(value: str) -> str:
def _comment_text(text: str) -> str:
return text.replace("*/", "* /").replace("\r", " ").replace("\n", " ")
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -7,7 +7,7 @@ import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, TextIO
from typing import Any, Callable, TextIO
from .bench_connect_lcd import (
BenchLogger,
@@ -21,15 +21,18 @@ from .bench_connect_lcd import (
_send_frame,
_wait_for_ready,
format_frame,
frame_checksum,
frame_checksum_ok,
parse_frame,
serial_format_label,
)
from .camera_snapshots import CameraSnapshots
from .serial_table_dump import build_read_frame, decode_table_read_response
DEFAULT_ACK_TARGET = bytes.fromhex("07804020902D")
DEFAULT_ACK_FRAME = bytes.fromhex("05004000001F")
HEARTBEAT_FRAME = bytes.fromhex("0000000080DA")
@dataclass
@@ -42,6 +45,9 @@ class ScenarioContext:
table_rows: list[dict[str, Any]] = field(default_factory=list)
target_counts: dict[str, int] = field(default_factory=dict)
tx_records: list[dict[str, Any]] = field(default_factory=list)
snapshot_records: list[dict[str, Any]] = field(default_factory=list)
snapshotter: CameraSnapshots | None = None
current_step_index: int | None = None
ack_sent: int = 0
abort_requested: bool = False
@@ -52,6 +58,37 @@ def default_log_path(scenario: dict[str, Any]) -> Path:
return Path("captures") / f"{safe_name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
def _snapshot_output_dir(args: argparse.Namespace, log_path: Path) -> Path | None:
if args.snapshot_dir is not None:
return args.snapshot_dir
if args.camera_index is not None:
return log_path.parent / f"{log_path.stem}-snapshots"
return None
def _snapshot_camera_index(args: argparse.Namespace) -> int:
return 0 if args.camera_index is None else args.camera_index
def _snapshot_delays(args: argparse.Namespace) -> list[float]:
text = str(args.snapshot_delays or "").strip()
if not text:
return []
delays: list[float] = []
for part in text.split(","):
item = part.strip()
if not item:
continue
try:
delay = float(item)
except ValueError as exc:
raise SystemExit(f"invalid snapshot delay {item!r}") from exc
if delay < 0:
raise SystemExit("snapshot delays must be zero or positive")
delays.append(delay)
return delays
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run JSON-described serial bench scenarios against the real RCP."
@@ -69,6 +106,39 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--sync", choices=("checksum", "fixed"), default="checksum", help="RX frame sync strategy")
parser.add_argument("--log", type=Path, help="capture log path")
parser.add_argument("--result-json", type=Path, help="write machine-readable scenario summary")
parser.add_argument(
"--quiet-console",
action="store_true",
help="keep full logs in --log but suppress RX/DETECT chatter on the console",
)
parser.add_argument(
"--camera-index",
type=int,
help="enable webcam snapshots with this OpenCV camera index; use 0 for the default camera",
)
parser.add_argument(
"--snapshot-dir",
type=Path,
help="directory for webcam snapshots; enables camera index 0 if --camera-index is omitted",
)
parser.add_argument(
"--snapshot-delays",
default="0",
help="comma-separated seconds after each send to capture, e.g. 0,0.25,1.0",
)
parser.add_argument(
"--snapshot-before-send",
action="store_true",
help="also capture immediately before each send",
)
parser.add_argument(
"--snapshot-acks",
action="store_true",
help="capture snapshots for generated ACK frames as well as scenario send steps",
)
parser.add_argument("--snapshot-warmup", type=float, default=0.5, help="seconds to warm up the webcam")
parser.add_argument("--snapshot-width", type=int, help="requested webcam capture width")
parser.add_argument("--snapshot-height", type=int, help="requested webcam capture height")
parser.add_argument("--dry-run", action="store_true", help="print the plan without opening serial ports")
return parser
@@ -83,8 +153,11 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
return 0
serial = _import_serial()
logger = BenchLogger(log_path, stdout=stdout)
console = _FilteredStdout(stdout, _quiet_console_line) if args.quiet_console else stdout
logger = BenchLogger(log_path, stdout=console)
detector = FrameDetector(sync_mode=args.sync)
snapshotter: CameraSnapshots | None = None
ctx: ScenarioContext | None = None
try:
logger.emit("Serial bench scenario")
logger.emit(f"name={scenario.get('name', args.scenario.stem)}")
@@ -93,24 +166,54 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
f"relay={args.relay_port} {args.relay_baud} sync={args.sync}"
)
logger.emit(f"log={log_path}")
snapshot_dir = _snapshot_output_dir(args, log_path)
if snapshot_dir is not None:
snapshotter = CameraSnapshots(
camera_index=_snapshot_camera_index(args),
output_dir=snapshot_dir,
warmup_seconds=args.snapshot_warmup,
width=args.snapshot_width,
height=args.snapshot_height,
)
snapshotter.open()
logger.emit(
f"snapshots={snapshot_dir} camera_index={_snapshot_camera_index(args)} "
f"delays={','.join(f'{delay:g}' for delay in _snapshot_delays(args))}"
)
with open_device_serial(serial, args) as device:
ctx = ScenarioContext(args=args, logger=logger, detector=detector, device=device)
ctx = ScenarioContext(
args=args,
logger=logger,
detector=detector,
device=device,
snapshotter=snapshotter,
)
try:
for index, step in enumerate(_scenario_steps(scenario), start=1):
if ctx.abort_requested:
logger.event("SCENARIO_ABORT requested by prior step")
break
action, spec = _normalize_step(step)
ctx.current_step_index = index
logger.event(f"STEP {index} {action}")
_run_step(ctx, action, spec)
finally:
ctx.current_step_index = None
if ctx.relay is not None:
ctx.relay.close()
if snapshotter is not None and ctx is not None:
snapshotter.close()
ctx.snapshot_records = snapshotter.records()
snapshotter = None
if ctx is None:
raise RuntimeError("scenario did not create a context")
_emit_summary(ctx, logger)
if args.result_json:
_write_result_json(args.result_json, scenario, log_path, ctx)
return 0
finally:
if snapshotter is not None:
snapshotter.close()
logger.close()
@@ -165,14 +268,23 @@ def _run_step(ctx: ScenarioContext, action: str, spec: dict[str, Any]) -> None:
raise SystemExit(2)
elif action in {"drain", "listen", "wait"}:
_listen(ctx, float(spec.get("seconds", spec.get("value", 0.0))))
elif action == "listen_ack":
_step_listen_ack(ctx, spec)
elif action == "listen_ack_until_quiet":
_step_listen_ack_until_quiet(ctx, spec)
elif action == "send":
frame = _parse_required_frame(spec.get("frame"))
label = str(spec.get("label", "send"))
_send_and_record(ctx, frame, label)
capture = bool(spec.get("snapshot", True))
_send_and_record(ctx, frame, label, capture=capture)
if float(spec.get("listen", 0.0)) > 0:
_listen(ctx, float(spec.get("listen", 0.0)))
elif action == "wait_for":
_step_wait_for(ctx, spec)
elif action == "prompt":
_step_prompt(ctx, spec)
elif action == "note":
_step_note(ctx, spec)
elif action == "table_sweep":
_step_table_sweep(ctx, spec)
elif action == "repeat":
@@ -214,6 +326,24 @@ def _step_wait_for(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
raise SystemExit(3)
def _step_prompt(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
message = str(spec.get("message", spec.get("value", "Press Enter to continue.")))
ctx.logger.event(f"PROMPT {message}")
input(message + " ")
def _step_note(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
message = str(spec.get("message", spec.get("value", "")))
if bool(spec.get("banner", False)):
ctx.logger.emit("")
ctx.logger.emit("NOTE " + "=" * 68)
ctx.logger.event(f"NOTE {message}")
ctx.logger.emit("NOTE " + "=" * 68)
ctx.logger.emit("")
else:
ctx.logger.event(f"NOTE {message}")
def _step_repeat(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
count = max(0, int(spec.get("count", 1)))
steps = spec.get("steps", [])
@@ -230,9 +360,13 @@ def _step_table_sweep(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
selectors = _selector_list(spec)
gap = float(spec.get("gap", 0.080))
ack = _ack_config(spec.get("ack_on", {}))
ack_note = (
"ack=disabled"
if not ack["enabled"]
else f"ack_targets={len(ack['targets'])} ack_frame={format_frame(ack['frame'])}"
)
ctx.logger.event(
f"TABLE_SWEEP selectors={len(selectors)} gap={gap:.3f}s "
f"ack_targets={len(ack['targets'])} ack_frame={format_frame(ack['frame'])}"
f"TABLE_SWEEP selectors={len(selectors)} gap={gap:.3f}s {ack_note}"
)
for selector in selectors:
if ctx.abort_requested:
@@ -240,13 +374,80 @@ def _step_table_sweep(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
break
frame = build_read_frame(selector)
ctx.logger.event(f"READ selector=0x{selector:03X} frame={format_frame(frame)}")
_send_and_record(ctx, frame, f"read_0x{selector:03X}")
_send_and_record(ctx, frame, f"read_0x{selector:03X}", capture=bool(spec.get("snapshot", False)))
_listen_with_ack(ctx, gap, selector, ack)
def _step_listen_ack(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
seconds = float(spec.get("seconds", spec.get("value", 1.0)))
ack = _ack_config_from_step(spec)
ack_text = (
f"ack_frame={format_frame(ack['frame'])}"
if ack["ack_mode"] == "fixed"
else f"ack_mode={ack['ack_mode']}"
)
ctx.logger.event(
f"LISTEN_ACK seconds={seconds:.3f} target_mode={ack['target_mode']} targets={len(ack['targets'])} "
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
)
_listen_with_ack(ctx, seconds, None, ack)
def _step_listen_ack_until_quiet(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
seconds = float(spec.get("seconds", spec.get("value", 10.0)))
quiet_seconds = float(spec.get("quiet_seconds", spec.get("quiet", 0.750)))
ack = _ack_config_from_step(spec)
ack_text = (
f"ack_frame={format_frame(ack['frame'])}"
if ack["ack_mode"] == "fixed"
else f"ack_mode={ack['ack_mode']}"
)
ctx.logger.event(
f"LISTEN_ACK_UNTIL_QUIET seconds={seconds:.3f} quiet={quiet_seconds:.3f} "
f"target_mode={ack['target_mode']} targets={len(ack['targets'])} "
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
)
_listen_with_ack(ctx, seconds, None, ack, quiet_seconds=quiet_seconds)
def _ack_config_from_step(spec: dict[str, Any]) -> dict[str, Any]:
return _ack_config(
{
"enabled": spec.get("enabled", True),
"frames": spec.get("frames", spec.get("frame")),
"ack_frame": spec.get("ack_frame"),
"ack_guard": spec.get("ack_guard", 0.020),
"poll_interval": spec.get("poll_interval", 0.005),
"post_ack_read": spec.get("post_ack_read", 0.250),
"once_per_selector": spec.get("once_per_frame", False),
"max_acks": spec.get("max_acks"),
"max_target_hits": spec.get("max_target_hits"),
"abort_on_limit": spec.get("abort_on_limit", False),
"ack_mode": spec.get("ack_mode", spec.get("mode", "fixed")),
"target_mode": spec.get("target_mode", spec.get("match", "explicit")),
"limit_scope": spec.get("limit_scope", spec.get("scope", "local")),
"respond_on": spec.get("respond_on", spec.get("send_on", [])),
}
)
def _ack_config(raw: Any) -> dict[str, Any]:
spec = raw if isinstance(raw, dict) else {}
targets = _parse_frame_list(spec.get("frames", spec.get("frame", DEFAULT_ACK_TARGET)))
enabled = bool(spec.get("enabled", True))
ack_mode = str(spec.get("ack_mode", spec.get("mode", "fixed"))).strip().lower().replace("-", "_")
if ack_mode not in {"fixed", "cmd5_selector"}:
raise SystemExit(f"unknown ack_mode {ack_mode!r}")
target_mode = str(spec.get("target_mode", spec.get("match", "explicit"))).strip().lower().replace("-", "_")
if target_mode == "queued_report":
target_mode = "queued_reports"
if target_mode not in {"explicit", "queued_reports"}:
raise SystemExit(f"unknown target_mode {target_mode!r}")
limit_scope = str(spec.get("limit_scope", spec.get("scope", "global"))).strip().lower().replace("-", "_")
if limit_scope not in {"global", "local"}:
raise SystemExit(f"unknown limit_scope {limit_scope!r}")
targets = set() if target_mode == "queued_reports" else _parse_frame_list(spec.get("frames", spec.get("frame", DEFAULT_ACK_TARGET)))
if not enabled:
targets = set()
return {
"targets": set(targets),
"frame": _parse_optional_frame(spec.get("ack_frame"), DEFAULT_ACK_FRAME),
@@ -254,37 +455,103 @@ def _ack_config(raw: Any) -> dict[str, Any]:
"poll_interval": float(spec.get("poll_interval", 0.005)),
"post_read": float(spec.get("post_ack_read", 0.250)),
"once_per_selector": bool(spec.get("once_per_selector", True)),
"enabled": bool(spec.get("enabled", True)),
"enabled": enabled,
"max_acks": _optional_int(spec.get("max_acks")),
"max_target_hits": _optional_int(spec.get("max_target_hits")),
"abort_on_limit": bool(spec.get("abort_on_limit", True)),
"ack_mode": ack_mode,
"target_mode": target_mode,
"limit_scope": limit_scope,
"respond_on": _response_rules(spec.get("respond_on", [])),
}
def _ack_frame_for_target(target: bytes, ack: dict[str, Any]) -> bytes:
if ack["ack_mode"] == "fixed":
return ack["frame"]
if len(target) != 6:
raise SystemExit(f"cannot build selector ACK for malformed target {target!r}")
body = bytes([0x05, target[1] & 0x7F, target[2], 0x00, 0x00])
return body + bytes([frame_checksum(body)])
def _ack_matches(frame: bytes, ack: dict[str, Any]) -> bool:
if ack["target_mode"] == "explicit":
return frame in ack["targets"]
if frame == HEARTBEAT_FRAME or not frame_checksum_ok(frame):
return False
return frame[0] in {0x00, 0x01, 0x02} and not (frame[1] & 0x80)
def _response_rules(raw: Any) -> list[dict[str, Any]]:
values = raw if isinstance(raw, list) else ([raw] if raw else [])
rules: list[dict[str, Any]] = []
for index, value in enumerate(values):
if not isinstance(value, dict):
raise SystemExit("respond_on entries must be objects")
targets = _parse_frame_list(value.get("frames", value.get("frame")))
response_frame = _parse_required_frame(value.get("send", value.get("response")))
label = str(value.get("label", f"respond_on_{index + 1}"))
rules.append(
{
"targets": targets,
"frame": response_frame,
"label": label,
"delay": float(value.get("delay", 0.0)),
"listen": float(value.get("listen", 0.0)),
"once": bool(value.get("once", True)),
}
)
return rules
def _listen_with_ack(
ctx: ScenarioContext,
seconds: float,
selector: int,
selector: int | None,
ack: dict[str, Any],
*,
quiet_seconds: float | None = None,
) -> list[bytes]:
deadline = time.monotonic() + max(0.0, seconds)
observed: list[bytes] = []
pending: list[bytes] = []
pending_index = 0
last_activity = time.monotonic()
acked_targets: set[bytes] = set()
fired_responses: set[int] = set()
ack_start = ctx.ack_sent
target_start = sum(ctx.target_counts.values())
def enqueue(frames: list[bytes]) -> None:
nonlocal last_activity
if not frames:
return
observed.extend(frames)
pending.extend(frames)
last_activity = time.monotonic()
while time.monotonic() < deadline:
frames = _read_available(ctx, selector=selector)
observed.extend(frames)
if not frames:
enqueue(frames)
if not frames and pending_index >= len(pending):
if quiet_seconds is not None and time.monotonic() - last_activity >= quiet_seconds:
ctx.logger.event(f"LISTEN_ACK_QUIET quiet={quiet_seconds:.3f}s")
break
sleep_for = min(max(0.001, ack["poll_interval"]), max(0.0, deadline - time.monotonic()))
if sleep_for > 0:
time.sleep(sleep_for)
continue
if not ack["enabled"]:
pending_index = len(pending)
continue
for frame in frames:
if frame not in ack["targets"]:
while pending_index < len(pending):
frame = pending[pending_index]
pending_index += 1
if not _ack_matches(frame, ack):
continue
_count_target(ctx, frame)
if _ack_limit_reached(ctx, ack):
if _ack_limit_reached(ctx, ack, ack_start=ack_start, target_start=target_start):
ctx.logger.event("ACK_LIMIT reached before ACK send")
if ack["abort_on_limit"]:
ctx.abort_requested = True
@@ -294,15 +561,30 @@ def _listen_with_ack(
continue
acked_targets.add(frame)
if ack["guard"] > 0:
observed.extend(_listen(ctx, ack["guard"], selector=selector))
_send_and_record(ctx, ack["frame"], "ack")
enqueue(_listen(ctx, ack["guard"], selector=selector))
_send_and_record(ctx, _ack_frame_for_target(frame, ack), "ack", capture=ctx.args.snapshot_acks)
ctx.ack_sent += 1
if _ack_limit_reached(ctx, ack):
if _ack_limit_reached(ctx, ack, ack_start=ack_start, target_start=target_start):
ctx.logger.event("ACK_LIMIT reached after ACK send")
if ack["abort_on_limit"]:
ctx.abort_requested = True
if ack["post_read"] > 0:
observed.extend(_listen(ctx, ack["post_read"], selector=selector))
enqueue(_listen(ctx, ack["post_read"], selector=selector))
for rule_index, rule in enumerate(ack["respond_on"]):
if frame not in rule["targets"]:
continue
if rule["once"] and rule_index in fired_responses:
continue
fired_responses.add(rule_index)
if rule["delay"] > 0:
time.sleep(rule["delay"])
ctx.logger.event(
f"RESPOND_ON target={format_frame(frame)} "
f"send={format_frame(rule['frame'])} label={rule['label']}"
)
_send_and_record(ctx, rule["frame"], rule["label"], capture=ctx.args.snapshot_acks)
if rule["listen"] > 0:
enqueue(_listen(ctx, rule["listen"], selector=selector))
if ctx.abort_requested:
return observed
return observed
@@ -368,8 +650,12 @@ def _record_table_rows(ctx: ScenarioContext, frames: list[bytes], selector: int
ctx.logger.event(f"TABLE selector=0x{selector:03X} echo={echo:02X} value={value:04X}")
def _send_and_record(ctx: ScenarioContext, frame: bytes, label: str) -> None:
def _send_and_record(ctx: ScenarioContext, frame: bytes, label: str, *, capture: bool = True) -> None:
if capture:
_capture_snapshots(ctx, frame, label, phase_prefix="pre", delays=[])
_send_frame(ctx.device, frame, ctx.logger, label)
if capture:
_capture_snapshots(ctx, frame, label, phase_prefix="tx", delays=_snapshot_delays(ctx.args))
ctx.tx_records.append(
{
"label": label,
@@ -379,6 +665,45 @@ def _send_and_record(ctx: ScenarioContext, frame: bytes, label: str) -> None:
)
def _capture_snapshots(
ctx: ScenarioContext,
frame: bytes,
label: str,
*,
phase_prefix: str,
delays: list[float],
) -> None:
if ctx.snapshotter is None:
return
frame_text = format_frame(frame)
if phase_prefix == "pre":
if not ctx.args.snapshot_before_send:
return
phases = [("pre", 0.0)]
else:
phases = [(_delay_phase(delay), delay) for delay in delays]
for phase, delay in phases:
try:
record = ctx.snapshotter.schedule(
label=label,
frame_text=frame_text,
phase=phase,
delay_seconds=delay,
step_index=ctx.current_step_index,
)
except RuntimeError as exc:
ctx.logger.event(f"SNAPSHOT_ERROR {label} {phase} {exc}")
continue
ctx.snapshot_records.append(record)
ctx.logger.event(f"SNAPSHOT_SCHEDULE {phase} {label} {record['path']}")
def _delay_phase(delay: float) -> str:
millis = int(round(max(0.0, delay) * 1000.0))
return f"tx+{millis:04d}ms"
def _count_target(ctx: ScenarioContext, frame: bytes) -> None:
text = format_frame(frame)
ctx.target_counts[text] = ctx.target_counts.get(text, 0) + 1
@@ -433,12 +758,24 @@ def _optional_int(raw: Any) -> int | None:
return _int_value(raw)
def _ack_limit_reached(ctx: ScenarioContext, ack: dict[str, Any]) -> bool:
def _ack_limit_reached(
ctx: ScenarioContext,
ack: dict[str, Any],
*,
ack_start: int = 0,
target_start: int = 0,
) -> bool:
if ack.get("limit_scope") == "local":
ack_count = ctx.ack_sent - ack_start
target_count = sum(ctx.target_counts.values()) - target_start
else:
ack_count = ctx.ack_sent
target_count = sum(ctx.target_counts.values())
max_acks = ack.get("max_acks")
if max_acks is not None and ctx.ack_sent >= max_acks:
if max_acks is not None and ack_count >= max_acks:
return True
max_target_hits = ack.get("max_target_hits")
if max_target_hits is not None and sum(ctx.target_counts.values()) >= max_target_hits:
if max_target_hits is not None and target_count >= max_target_hits:
return True
return False
@@ -449,26 +786,149 @@ def _print_dry_run(args: argparse.Namespace, scenario: dict[str, Any], log_path:
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"sync={args.sync}", file=stdout)
print(f"log={log_path}", file=stdout)
snapshot_dir = _snapshot_output_dir(args, log_path)
if snapshot_dir is not None:
print(
f"snapshots={snapshot_dir} camera_index={_snapshot_camera_index(args)} "
f"before_send={int(args.snapshot_before_send)} "
f"delays={','.join(f'{delay:g}' for delay in _snapshot_delays(args))}",
file=stdout,
)
for index, step in enumerate(_scenario_steps(scenario), start=1):
action, spec = _normalize_step(step)
print(f"step[{index}]={action}", file=stdout)
_print_step_dry_run(action, spec, stdout)
class _FilteredStdout:
def __init__(self, stdout: TextIO, predicate: Callable[[str], bool]) -> None:
self.stdout = stdout
self.predicate = predicate
self.buffer = ""
def write(self, text: str) -> int:
self.buffer += text
while "\n" in self.buffer:
line, self.buffer = self.buffer.split("\n", 1)
if self.predicate(line):
self.stdout.write(line + "\n")
self.stdout.flush()
return len(text)
def flush(self) -> None:
if self.buffer:
if self.predicate(self.buffer):
self.stdout.write(self.buffer)
self.buffer = ""
self.stdout.flush()
def _quiet_console_line(line: str) -> bool:
if not line.strip():
return True
keep_fragments = (
"Serial bench scenario",
"name=",
"device=",
"log=",
"STEP ",
"PROMPT ",
"NOTE ",
"SNAPSHOT_SCHEDULE ",
"SNAPSHOT_ERROR ",
"RESPOND_ON ",
"Summary",
"rx_frames=",
"resync_events=",
"tx_frames=",
"abort_requested=",
"known_shutter",
"queued_shutter",
"iris_mblack",
"selector_0013",
)
return any(fragment in line for fragment in keep_fragments)
def _print_step_dry_run(action: str, spec: dict[str, Any], stdout: TextIO, *, indent: str = " ") -> None:
if action == "send":
frame = _parse_required_frame(spec.get("frame"))
print(f" frame={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}", file=stdout)
print(f"{indent}frame={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}", file=stdout)
if float(spec.get("listen", 0.0)) > 0:
print(f"{indent}listen={float(spec.get('listen', 0.0)):.3f}s", file=stdout)
elif action in {"drain", "listen", "wait"}:
print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 0.0))):.3f}", file=stdout)
elif action in {"listen_ack", "listen_ack_until_quiet"}:
ack = _ack_config_from_step(spec)
print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 1.0))):.3f}", file=stdout)
if action == "listen_ack_until_quiet":
print(
f"{indent}quiet={float(spec.get('quiet_seconds', spec.get('quiet', 0.750))):.3f}s",
file=stdout,
)
if not ack["enabled"]:
print(f"{indent}ack=disabled", file=stdout)
else:
print(f"{indent}target_mode={ack['target_mode']}", file=stdout)
for target in sorted(ack["targets"]):
print(f"{indent}ack_target={format_frame(target)}", file=stdout)
print(f"{indent}ack_mode={ack['ack_mode']}", file=stdout)
if ack["ack_mode"] == "fixed":
print(f"{indent}ack_frame={format_frame(ack['frame'])}", file=stdout)
print(
f"{indent}limit_scope={ack['limit_scope']} max_acks={ack['max_acks']} "
f"max_target_hits={ack['max_target_hits']}",
file=stdout,
)
for rule in ack["respond_on"]:
print(
f"{indent}respond_on={len(rule['targets'])} "
f"send={format_frame(rule['frame'])} label={rule['label']} "
f"once={int(rule['once'])}",
file=stdout,
)
elif action in {"prompt", "note"}:
message = str(spec.get("message", spec.get("value", "Press Enter to continue.")))
print(f"{indent}message={message}", file=stdout)
elif action == "wait_ready":
print(
f"{indent}heartbeats={int(spec.get('heartbeats', 2))} "
f"timeout={float(spec.get('timeout', 10.0)):.3f}s "
f"require={int(bool(spec.get('require', False)))}",
file=stdout,
)
elif action == "table_sweep":
selectors = _selector_list(spec)
ack = _ack_config(spec.get("ack_on", {}))
if selectors:
first = selectors[0]
last = selectors[-1]
print(f" selectors={len(selectors)} first=0x{first:03X} last=0x{last:03X}", file=stdout)
print(f"{indent}selectors={len(selectors)} first=0x{first:03X} last=0x{last:03X}", file=stdout)
else:
print(f"{indent}selectors=0", file=stdout)
print(f"{indent}gap={float(spec.get('gap', 0.080)):.3f}", file=stdout)
if not ack["enabled"]:
print(f"{indent}ack=disabled", file=stdout)
else:
print(" selectors=0", file=stdout)
print(f" gap={float(spec.get('gap', 0.080)):.3f}", file=stdout)
for target in sorted(ack["targets"]):
print(f" ack_target={format_frame(target)}", file=stdout)
print(f" ack_frame={format_frame(ack['frame'])}", file=stdout)
print(f" max_acks={ack['max_acks']} max_target_hits={ack['max_target_hits']}", file=stdout)
print(f"{indent}ack_target={format_frame(target)}", file=stdout)
print(f"{indent}ack_frame={format_frame(ack['frame'])}", file=stdout)
print(
f"{indent}limit_scope={ack['limit_scope']} max_acks={ack['max_acks']} "
f"max_target_hits={ack['max_target_hits']}",
file=stdout,
)
elif action == "repeat":
count = max(0, int(spec.get("count", 1)))
steps = spec.get("steps", [])
step_count = len(steps) if isinstance(steps, list) else 0
print(f"{indent}count={count} steps={step_count}", file=stdout)
if not isinstance(steps, list):
return
for child_index, child in enumerate(steps, start=1):
child_action, child_spec = _normalize_step(child)
print(f"{indent}child[{child_index}]={child_action}", file=stdout)
_print_step_dry_run(child_action, child_spec, stdout, indent=indent + " ")
def _emit_summary(ctx: ScenarioContext, logger: BenchLogger) -> None:
@@ -477,6 +937,7 @@ def _emit_summary(ctx: ScenarioContext, logger: BenchLogger) -> None:
logger.emit(f"rx_frames={len(ctx.detector.frames)} trailing_unframed_bytes={len(ctx.detector.buffer)}")
logger.emit(f"resync_events={ctx.detector.resync_events} dropped_bytes={ctx.detector.dropped_bytes}")
logger.emit(f"tx_frames={len(ctx.tx_records)} ack_sent={ctx.ack_sent} table_response_rows={len(ctx.table_rows)}")
logger.emit(f"snapshots={len(ctx.snapshot_records)}")
logger.emit(f"abort_requested={int(ctx.abort_requested)}")
for target, count in sorted(ctx.target_counts.items()):
logger.emit(f"ack_target {target}={count}")
@@ -500,6 +961,7 @@ def _write_result_json(path: Path, scenario: dict[str, Any], log_path: Path, ctx
"labels": dict(ctx.detector.labels),
"tx_frames": ctx.tx_records,
"ack_sent": ctx.ack_sent,
"snapshots": ctx.snapshot_records,
"abort_requested": ctx.abort_requested,
"ack_targets": ctx.target_counts,
"table_rows": ctx.table_rows,

View File

@@ -0,0 +1,121 @@
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Any, TextIO
from .bench_connect_lcd import format_frame, label_frame, parse_frame
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Compare two serial_scenario result JSON files and highlight extra button/report traffic."
)
parser.add_argument("baseline", type=Path, help="baseline result JSON, usually a no-button run")
parser.add_argument("candidate", type=Path, help="candidate result JSON, usually a one-button run")
parser.add_argument("--show-labels", action="store_true", help="also show label count deltas")
return parser
def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
args = build_arg_parser().parse_args(argv)
baseline = _load_result(args.baseline)
candidate = _load_result(args.candidate)
print(format_comparison(baseline, candidate, show_labels=args.show_labels), file=stdout)
return 0
def _load_result(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
result = json.load(handle)
if not isinstance(result, dict):
raise SystemExit(f"{path} is not a result JSON object")
result["_path"] = str(path)
return result
def format_comparison(
baseline: dict[str, Any],
candidate: dict[str, Any],
*,
show_labels: bool = False,
) -> str:
base_targets = Counter(_string_int_mapping(baseline.get("ack_targets", {})))
candidate_targets = Counter(_string_int_mapping(candidate.get("ack_targets", {})))
target_delta = candidate_targets - base_targets
lines = [
"Serial scenario comparison",
f"baseline={baseline.get('_path', '')}",
f"candidate={candidate.get('_path', '')}",
f"baseline_log={baseline.get('log', '')}",
f"candidate_log={candidate.get('log', '')}",
f"baseline_rx_frames={baseline.get('rx_frames', 0)} candidate_rx_frames={candidate.get('rx_frames', 0)}",
f"baseline_ack_sent={baseline.get('ack_sent', 0)} candidate_ack_sent={candidate.get('ack_sent', 0)}",
]
if target_delta:
lines.append("extra ACK-target frames in candidate:")
for frame_text, count in sorted(target_delta.items(), key=_frame_sort_key):
lines.append(f" +{count:3d} {frame_text} {_describe_frame(frame_text)}")
else:
lines.append("no extra ACK-target frames found in candidate")
missing = base_targets - candidate_targets
if missing:
lines.append("baseline ACK-target frames missing/decreased in candidate:")
for frame_text, count in sorted(missing.items(), key=_frame_sort_key):
lines.append(f" -{count:3d} {frame_text} {_describe_frame(frame_text)}")
if show_labels:
base_labels = Counter(_string_int_mapping(baseline.get("labels", {})))
candidate_labels = Counter(_string_int_mapping(candidate.get("labels", {})))
label_delta = candidate_labels - base_labels
lines.append("label count increases:")
if label_delta:
for label, count in sorted(label_delta.items()):
lines.append(f" +{count:3d} {label}")
else:
lines.append(" none")
return "\n".join(lines)
def _string_int_mapping(raw: Any) -> dict[str, int]:
if not isinstance(raw, dict):
return {}
output: dict[str, int] = {}
for key, value in raw.items():
try:
output[str(key)] = int(value)
except (TypeError, ValueError):
continue
return output
def _describe_frame(frame_text: str) -> str:
frame = parse_frame(frame_text)
selector = ((frame[1] & 0x7F) << 7) | frame[2]
value = (frame[3] << 8) | frame[4]
label = label_frame(frame) or "checksum_ok_unlabeled"
return f"cmd=0x{frame[0]:02X} selector=0x{selector:04X} value=0x{value:04X} label={label}"
def _frame_sort_key(item: tuple[str, int]) -> tuple[int, int, int, str]:
frame_text, _count = item
try:
frame = parse_frame(frame_text)
except argparse.ArgumentTypeError:
return (0xFFFF, 0xFFFF, 0xFFFF, frame_text)
selector = ((frame[1] & 0x7F) << 7) | frame[2]
value = (frame[3] << 8) | frame[4]
return (selector, frame[0], value, frame_text)
__all__ = [
"build_arg_parser",
"format_comparison",
"main",
]

View File

@@ -0,0 +1,158 @@
from __future__ import annotations
import argparse
import re
import sys
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, TextIO
DETECT_RE = re.compile(
r"^(?P<time>\d\d:\d\d:\d\d\.\d{3})\s+DETECT\s+"
r"(?:(?P<label>\S+)\s+)?(?P<frame>(?:[0-9A-Fa-f]{2}\s*){6})\s*$"
)
DEFAULT_IGNORED_LABELS = {
"heartbeat",
"connect_ok_path_response_candidate",
"connect_c0_path_response_candidate",
"table_readback_candidate",
"active_selector0_keepalive_report",
"gated_active_0004_response_candidate",
"gated_active_0004_transition_candidate",
}
KNOWN_FRAME_LABELS = {
"00 00 00 80 80 5A": "active_selector0_keepalive_report",
"00 00 6C 00 00 36": "copy_completion_exit_selector_006c_candidate",
"00 00 6D 00 00 37": "copy_in_progress_selector_006d_candidate",
"00 00 15 80 00 CF": "known_call_button_active_report",
"00 00 15 00 00 4F": "known_call_button_inactive_report",
"00 00 07 80 00 DD": "known_cam_power_button_report",
"00 00 13 00 00 49": "known_iris_mblack_link_clear_report_candidate",
"00 00 13 40 00 09": "known_iris_mblack_link_active_report_candidate",
"00 00 13 80 00 C9": "known_selector_0013_bit15_report_candidate",
"00 00 13 C0 00 89": "known_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
"01 00 13 00 00 48": "queued_iris_mblack_link_clear_report_candidate",
"02 00 13 00 00 4B": "queued_iris_mblack_link_clear_report_candidate",
"01 00 13 40 00 08": "queued_iris_mblack_link_active_report_candidate",
"02 00 13 40 00 0B": "queued_iris_mblack_link_active_report_candidate",
"01 00 13 80 00 C8": "queued_selector_0013_bit15_report_candidate",
"02 00 13 80 00 CB": "queued_selector_0013_bit15_report_candidate",
"01 00 13 C0 00 88": "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
"02 00 13 C0 00 8B": "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
"01 00 17 80 00 CC": "queued_bars_button_selector_0017_active_candidate",
"02 00 17 80 00 CF": "queued_bars_button_selector_0017_active_candidate",
"01 00 18 80 00 C3": "queued_bars_button_selector_0018_active_candidate",
"02 00 18 80 00 C0": "queued_bars_button_selector_0018_active_candidate",
"01 01 1A 08 00 48": "queued_iris_auto_button_selector_009a_active_candidate",
"02 01 1A 08 00 4B": "queued_iris_auto_button_selector_009a_active_candidate",
"01 00 04 00 00 5F": "gated_active_0004_response_candidate",
"02 00 04 00 00 5C": "gated_active_0004_transition_candidate",
}
@dataclass(frozen=True)
class DetectedFrame:
timestamp: str
label: str
frame: str
line_number: int
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Show unexpected DETECT frames from a serial_scenario capture log."
)
parser.add_argument("log", type=Path, help="serial_scenario capture log")
parser.add_argument(
"--include-refresh",
action="store_true",
help="include heartbeat, table-readback, and CONNECT OK refresh frames",
)
parser.add_argument(
"--show-all",
action="store_true",
help="print every matching DETECT frame after the summary",
)
return parser
def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
args = build_arg_parser().parse_args(argv)
text = args.log.read_text(encoding="utf-8")
frames = parse_detected_frames(text.splitlines())
interesting = frames if args.include_refresh else filter_expected_refresh(frames)
print(format_report(frames, interesting, include_refresh=args.include_refresh, show_all=args.show_all), file=stdout)
return 0
def parse_detected_frames(lines: Iterable[str]) -> list[DetectedFrame]:
frames: list[DetectedFrame] = []
for line_number, line in enumerate(lines, start=1):
match = DETECT_RE.match(line.strip())
if not match:
continue
frame = " ".join(match.group("frame").upper().split())
label = KNOWN_FRAME_LABELS.get(frame, match.group("label") or "checksum_ok_unlabeled")
frames.append(
DetectedFrame(
timestamp=match.group("time"),
label=label,
frame=frame,
line_number=line_number,
)
)
return frames
def filter_expected_refresh(frames: Iterable[DetectedFrame]) -> list[DetectedFrame]:
return [frame for frame in frames if frame.label not in DEFAULT_IGNORED_LABELS]
def format_report(
frames: list[DetectedFrame],
interesting: list[DetectedFrame],
*,
include_refresh: bool,
show_all: bool,
) -> str:
lines = [
"Serial scenario unexpected-frame summary",
f"detected_frames={len(frames)} mode={'all' if include_refresh else 'unexpected-only'}",
]
label_counts = Counter(frame.label for frame in frames)
if label_counts:
lines.append("labels:")
for label, count in sorted(label_counts.items()):
lines.append(f" {label}: {count}")
ignored = len(frames) - len(interesting)
if not include_refresh:
lines.append(f"ignored_expected_refresh={ignored}")
lines.append(f"interesting_frames={len(interesting)}")
frame_counts = Counter((frame.frame, frame.label) for frame in interesting)
if frame_counts:
lines.append("interesting frame counts:")
for (frame_text, label), count in sorted(frame_counts.items(), key=lambda item: (-item[1], item[0][0])):
first = next(frame for frame in interesting if frame.frame == frame_text and frame.label == label)
lines.append(f" {count:4d}x {frame_text} label={label} first={first.timestamp} line={first.line_number}")
else:
lines.append("no unexpected checksum-valid frames found")
if show_all and interesting:
lines.append("timeline:")
for frame in interesting:
lines.append(f" {frame.timestamp} line={frame.line_number} {frame.frame} label={frame.label}")
return "\n".join(lines)
__all__ = [
"DetectedFrame",
"filter_expected_refresh",
"format_report",
"main",
"parse_detected_frames",
]

View File

@@ -4,6 +4,8 @@ import re
from collections.abc import Iterable, Mapping
from typing import Any
from .panel_selectors import panel_selector_semantics_payload
JsonObject = dict[str, Any]
@@ -140,6 +142,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"tx_report_model": None,
"periodic_resend_model": None,
"timer_interrupt_model": None,
"panel_selector_semantics": [],
"confidence": "low",
"confidence_score": 0.0,
"caveat": "No protocol semantics are emitted without both RX and TX serial reconstruction candidates.",
@@ -212,6 +215,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"tx_report_model": tx_report_model,
"periodic_resend_model": periodic_resend_model,
"timer_interrupt_model": timer_interrupt_model,
"panel_selector_semantics": panel_selector_semantics_payload(),
"evidence": evidence,
}
return {
@@ -233,6 +237,7 @@ def analyze_serial_semantics(payload: Mapping[str, Any]) -> JsonObject:
"tx_report_model": protocol["tx_report_model"],
"periodic_resend_model": protocol["periodic_resend_model"],
"timer_interrupt_model": protocol["timer_interrupt_model"],
"panel_selector_semantics": protocol["panel_selector_semantics"],
"confidence": protocol["confidence"],
"confidence_score": protocol["confidence_score"],
"caveat": protocol["caveat"],

View File

@@ -287,6 +287,7 @@ def _logical_operand_accesses(
logical_address: int | None = None
if isinstance(offset, int):
logical_address = (int(table["logical_base_address"]) + offset) & 0xFFFF
selector = _selector_for_table_offset(table, offset)
access = _base_access(ins, functions, semantic_accesses)
access.update(
{
@@ -306,6 +307,9 @@ def _logical_operand_accesses(
if logical_address is not None:
access["logical_address"] = logical_address
access["logical_address_hex"] = h16(logical_address)
if selector is not None:
access["selector"] = selector
access["selector_hex"] = f"0x{selector:03X}"
accesses.append(access)
return accesses
@@ -342,6 +346,7 @@ def _direct_logical_address_access(
) -> JsonObject:
base = int(table["logical_base_address"])
offset = address - base
selector = _selector_for_table_offset(table, offset)
access = _base_access(ins, functions, semantic_accesses)
access.update(
{
@@ -359,6 +364,9 @@ def _direct_logical_address_access(
"access": _access_direction(ins, address) or "read_write_candidate",
}
)
if selector is not None:
access["selector"] = selector
access["selector_hex"] = f"0x{selector:03X}"
return access
@@ -373,6 +381,7 @@ def _direct_candidate_address_access(
offset = address - base
access = _base_access(ins, functions, semantic_accesses)
logical_offset = DIRECT_TABLE_TO_LOGICAL_OFFSET.get(base)
selector = _selector_for_table_offset(table, offset)
access.update(
{
"table": table["name"],
@@ -392,6 +401,9 @@ def _direct_candidate_address_access(
if logical_offset is not None:
access["semantic_negative_offset"] = logical_offset
access["semantic_negative_offset_hex"] = h16(logical_offset)
if selector is not None:
access["selector"] = selector
access["selector_hex"] = f"0x{selector:03X}"
return access
@@ -564,6 +576,8 @@ def _format_access_line(access: Mapping[str, Any]) -> str:
index_text = f"index dynamic via {access.get('index_register')} operand {operand}"
else:
index_text = f"offset {h16(int(index or 0))}"
if access.get("selector_hex"):
index_text += f" selector {access['selector_hex']}"
if access.get("logical_address_hex"):
index_text += f" -> {access['logical_address_hex']}"
elif access.get("direct_address_hex"):
@@ -599,6 +613,19 @@ def _summarize_functions(accesses: Iterable[Mapping[str, Any]]) -> list[JsonObje
return sorted(summaries.values(), key=lambda item: (-int(item["access_count"]), str(item["label"])))
def _selector_for_table_offset(table: Mapping[str, Any], offset: int | str) -> int | None:
if not isinstance(offset, int):
return None
element = str(table.get("element_candidate") or "")
if element == "word_value":
if offset % 2:
return None
return (offset // 2) & 0x01FF
if element == "bit_flags":
return offset & 0x01FF
return None
def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
call_graph = payload.get("call_graph")
if not isinstance(call_graph, Mapping):

View File

@@ -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())

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python3
"""Compatibility wrapper for the H8/536 panel button trace CLI."""
from h8536.panel_button_trace import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,52 @@
{
"name": "active-control-report-watch-broad",
"notes": [
"Hold CONNECT OK while streaming broader camera/status words that may enable more local controls.",
"Press or turn one physical control at a time while this runs. Note the rough time and control name.",
"This is noisier than the quiet watch: each cycle writes 0x008F and 0x0093, so use the unexpected-frame summarizer after the run."
],
"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_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.25
},
{
"action": "repeat",
"count": 80,
"steps": [
{
"action": "send",
"label": "broad_refresh_008f_1800",
"frame": "00 01 0F 18 00 4C",
"listen": 0.30
},
{
"action": "send",
"label": "broad_refresh_0093_ffff",
"frame": "00 01 13 FF FF 48",
"listen": 0.30
}
]
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,70 @@
{
"name": "active-control-report-watch-gated",
"notes": [
"Hold CONNECT OK and seed candidate secondary-table E400 gates before watching for local-control TX reports.",
"This follows the ROM clue that some local report paths require E400 feature bits, not only E000 display/status words.",
"Press or turn one physical control at a time while this runs. Note the rough time and control name."
],
"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_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.25
},
{
"action": "send",
"label": "secondary_gate_0015_other_copy_candidate",
"frame": "06 00 15 80 00 C9",
"listen": 0.30
},
{
"action": "send",
"label": "secondary_gate_008f_shutter_bits_11_12",
"frame": "06 01 0F 18 00 4A",
"listen": 0.30
},
{
"action": "send",
"label": "secondary_gate_0093_all_bits",
"frame": "06 01 13 FF FF 4E",
"listen": 0.30
},
{
"action": "repeat",
"count": 80,
"steps": [
{
"action": "send",
"label": "gated_refresh_008f_1800",
"frame": "00 01 0F 18 00 4C",
"listen": 0.30
},
{
"action": "send",
"label": "gated_refresh_0093_90ff",
"frame": "00 01 13 90 FF 27",
"listen": 0.30
}
]
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,46 @@
{
"name": "active-control-report-watch-quiet",
"notes": [
"Hold the panel in CONNECT OK with the least noisy proven refresh stream, then watch for local-control TX reports.",
"Press or turn one physical control at a time while this runs. Note the rough time and control name.",
"Expected refresh traffic is table readback for 0x0093 plus 02 00 02 00 00 5A OK-path responses; anything else is interesting."
],
"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_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.25
},
{
"action": "repeat",
"count": 100,
"steps": [
{
"action": "send",
"label": "quiet_active_refresh_0093_9020",
"frame": "00 01 13 90 20 F8",
"listen": 0.60
}
]
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,208 @@
{
"name": "button-report-broad-gates-press",
"notes": [
"One-button candidate test with broad secondary E400 gates enabled for ROM-observed report selectors.",
"Run this only after the common-gate press test fails to produce a clear extra selector.",
"If a button only emits here, it probably needs a selector-specific feature/report gate rather than only the common queue-service gate."
],
"steps": [
{
"action": "prompt",
"message": "Broad-gate run: pick one physical button. When the NOTE banner appears, press it once. Press Enter to start."
},
{
"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_connect_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "send",
"label": "secondary_gate_0007",
"frame": "06 00 07 FF FF 5B",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0013",
"frame": "06 00 13 FF FF 4F",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0015",
"frame": "06 00 15 FF FF 49",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0017",
"frame": "06 00 17 FF FF 4B",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0018",
"frame": "06 00 18 FF FF 44",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_001a",
"frame": "06 00 1A FF FF 46",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_006b",
"frame": "06 01 6B FF FF 36",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0083",
"frame": "06 01 03 FF FF 5E",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_008f",
"frame": "06 01 0F FF FF 52",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0093",
"frame": "06 01 13 FF FF 4E",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0096",
"frame": "06 01 16 FF FF 4B",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_0097",
"frame": "06 01 17 FF FF 4A",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_009a",
"frame": "06 01 1A FF FF 47",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_00b7",
"frame": "06 01 37 FF FF 6A",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_00b9",
"frame": "06 01 39 FF FF 64",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_00c4",
"frame": "06 02 44 FF FF 1A",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_00c6",
"frame": "06 02 46 FF FF 18",
"listen": 0.04
},
{
"action": "send",
"label": "secondary_gate_00f8",
"frame": "06 02 78 FF FF 26",
"listen": 0.04
},
{
"action": "send",
"label": "cmd5_latch_clear_0096",
"frame": "05 01 16 00 00 48",
"listen": 0.05
},
{
"action": "listen_ack",
"seconds": 3.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 48,
"abort_on_limit": false
},
{
"action": "send",
"label": "recover_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.45
},
{
"action": "listen_ack",
"seconds": 1.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 32,
"abort_on_limit": false
},
{
"action": "note",
"message": "PRESS THE ONE TEST BUTTON NOW; broad secondary gates are enabled.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 16.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 96,
"abort_on_limit": false
},
{
"action": "send",
"label": "repeat_last_for_hidden_report_check",
"frame": "07 00 00 00 00 5D",
"listen": 0.80
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,100 @@
{
"name": "button-report-common-gate-baseline",
"notes": [
"No-button baseline for queued local-control report tests.",
"Do not press or turn controls during the NOTE window. Compare this result JSON against one-button candidate runs.",
"This uses the same common queue-service gate that exposed SHUTTER ON/OFF as selector 0x010F."
],
"steps": [
{
"action": "prompt",
"message": "Baseline run: do not touch the panel during the NOTE window. Press Enter to start."
},
{
"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_connect_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "cmd5_latch_clear_0096",
"frame": "05 01 16 00 00 48",
"listen": 0.05
},
{
"action": "listen_ack",
"seconds": 3.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 48,
"abort_on_limit": false
},
{
"action": "send",
"label": "recover_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.45
},
{
"action": "listen_ack",
"seconds": 1.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 32,
"abort_on_limit": false
},
{
"action": "note",
"message": "NO BUTTONS NOW; baseline queued-report ACK window is running.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 16.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 96,
"abort_on_limit": false
},
{
"action": "send",
"label": "repeat_last_for_hidden_report_check",
"frame": "07 00 00 00 00 5D",
"listen": 0.80
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,100 @@
{
"name": "button-report-common-gate-press",
"notes": [
"One-button candidate test for queued local-control reports.",
"Run the common-gate baseline first, then run this once per physical button candidate and compare result JSON files.",
"If a button produces an extra selector here, it probably shares the same common queue-service gate as SHUTTER ON/OFF."
],
"steps": [
{
"action": "prompt",
"message": "Pick one physical button for this run. When the NOTE banner appears, press it once. Press Enter to start."
},
{
"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_connect_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "cmd5_latch_clear_0096",
"frame": "05 01 16 00 00 48",
"listen": 0.05
},
{
"action": "listen_ack",
"seconds": 3.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 48,
"abort_on_limit": false
},
{
"action": "send",
"label": "recover_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.45
},
{
"action": "listen_ack",
"seconds": 1.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 32,
"abort_on_limit": false
},
{
"action": "note",
"message": "PRESS THE ONE TEST BUTTON NOW; live queued-report ACK window is running.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 16.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 96,
"abort_on_limit": false
},
{
"action": "send",
"label": "repeat_last_for_hidden_report_check",
"frame": "07 00 00 00 00 5D",
"listen": 0.80
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,47 @@
{
"name": "copy-family00-006d-006c-1000ms",
"notes": [
"Recover to CONNECT OK, send family-00 selector 0x006D, then family-00 selector 0x006C 1 second later.",
"Gives the LCD time to visibly enter COPY IN PROGRESS if family-00 0x006D drives the same queue path as command 5."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "send",
"label": "family00_copy_start_006d",
"frame": "00 00 6D 00 00 37",
"listen": 1.0
},
{
"action": "send",
"label": "family00_copy_complete_candidate_006c",
"frame": "00 00 6C 00 00 36",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,47 @@
{
"name": "copy-family00-006d-006c-250ms",
"notes": [
"Recover to CONNECT OK, send family-00 selector 0x006D, then family-00 selector 0x006C 250 ms later.",
"Compares family-00 set/queue behavior against the known command-5 COPY start/complete pair."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "send",
"label": "family00_copy_start_006d",
"frame": "00 00 6D 00 00 37",
"listen": 0.25
},
{
"action": "send",
"label": "family00_copy_complete_candidate_006c",
"frame": "00 00 6C 00 00 36",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,53 @@
{
"name": "copy-hold-006d-5x-006c",
"notes": [
"Recover to CONNECT OK, send 006D every 1 second for five starts/progress ticks, then send 006C.",
"Tests whether repeated 006D can hold the copy-in-progress state for a longer transfer before completion."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "repeat",
"count": 5,
"steps": [
{
"action": "send",
"label": "copy_progress_006d",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
}
]
},
{
"action": "send",
"label": "copy_complete_candidate_006c",
"frame": "05 00 6C 00 00 33",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,51 @@
{
"name": "copy-hold-006d-5x-no-complete",
"notes": [
"Recover to CONNECT OK, send 006D every 1 second five times, then do not send 006C.",
"Tests whether the timeout/fallback is measured from the last 006D progress tick."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "repeat",
"count": 5,
"steps": [
{
"action": "send",
"label": "copy_progress_006d",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
}
]
},
{
"action": "listen",
"seconds": 10.0
}
]
}

View File

@@ -0,0 +1,53 @@
{
"name": "copy-repeat-006d-2x-006c",
"notes": [
"Recover to CONNECT OK, send 006D twice one second apart, then send 006C.",
"Tests whether repeating the in-progress selector extends/resets the completion window or interferes with completion."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d_1",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d_2",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
},
{
"action": "send",
"label": "copy_complete_candidate_006c",
"frame": "05 00 6C 00 00 33",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,59 @@
{
"name": "copy-repeat-006d-3x-006c",
"notes": [
"Recover to CONNECT OK, send 006D three times one second apart, then send 006C.",
"Tests whether repeated in-progress selectors can keep the copy state alive longer than the single-start window."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d_1",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d_2",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d_3",
"frame": "05 00 6D 00 00 32",
"listen": 1.0
},
{
"action": "send",
"label": "copy_complete_candidate_006c",
"frame": "05 00 6C 00 00 33",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,47 @@
{
"name": "copy-step-006d-006c-1500ms",
"notes": [
"Recover to CONNECT OK, send 006D, then send 006C 1.5 seconds later.",
"Narrows the working copy-complete window between the known 1.0s success and 2.5s failure."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d",
"frame": "05 00 6D 00 00 32",
"listen": 1.5
},
{
"action": "send",
"label": "copy_complete_candidate_006c",
"frame": "05 00 6C 00 00 33",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,47 @@
{
"name": "copy-step-006d-006c-2000ms",
"notes": [
"Recover to CONNECT OK, send 006D, then send 006C 2.0 seconds later.",
"Narrows the copy-complete acceptance window before the known 2.5s failure."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"timeout": 10.0,
"heartbeats": 2,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "ok_baseline_1",
"frame": "04 00 00 80 00 DE",
"listen": 0.7
},
{
"action": "send",
"label": "ok_baseline_2",
"frame": "04 00 00 80 00 DE",
"listen": 1.0
},
{
"action": "send",
"label": "copy_start_006d",
"frame": "05 00 6D 00 00 32",
"listen": 2.0
},
{
"action": "send",
"label": "copy_complete_candidate_006c",
"frame": "05 00 6C 00 00 33",
"listen": 10.0
}
]
}

View File

@@ -0,0 +1,227 @@
{
"name": "iris-mblack-link-mirror-state-machine",
"notes": [
"IRIS/M.BLACK LINK closed-loop state-machine probe.",
"The RCP reports local button intent as selector 0x0013. This scenario ACKs the report and mirrors the reported selector value back as a command-0 CCU table update.",
"Expected cycle if the CCU owns the latched state: press 1 emits 00 00 13 40 00 09, mirror active, press 2 emits 00 00 13 00 00 49, mirror clear, press 3 emits active again.",
"If every press still emits active, the local handler is not seeing the mirrored E800/F791 state or the physical input path is level-style rather than toggle-style."
],
"steps": [
{
"action": "prompt",
"message": "Prepare to test the closed-loop IRIS/M.BLACK LINK state machine. Press Enter to power-cycle and start."
},
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.20
},
{
"action": "send",
"label": "selector_zero_connect_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.20
},
{
"action": "send",
"label": "cmd5_latch_clear_0096",
"frame": "05 01 16 00 00 48",
"listen": 0.02
},
{
"action": "listen_ack_until_quiet",
"seconds": 28.00,
"quiet_seconds": 0.90,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 180,
"abort_on_limit": false
},
{
"action": "send",
"label": "force_selector_0013_clear_baseline",
"frame": "00 00 13 00 00 49",
"listen": 0.40
},
{
"action": "send",
"label": "recover_connect_ok_seed_before_press_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.20
},
{
"action": "note",
"message": "PRESS IRIS/M.BLACK LINK ONCE NOW. The script will mirror selector 0x0013 back to the RCP when it sees the report.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 9.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 80,
"abort_on_limit": false,
"respond_on": [
{
"frames": [
"00 00 13 40 00 09",
"01 00 13 40 00 08",
"02 00 13 40 00 0B"
],
"send": "00 00 13 40 00 09",
"label": "mirror_selector_0013_active_from_button",
"delay": 0.050,
"listen": 0.20,
"once": true
},
{
"frames": [
"00 00 13 00 00 49",
"01 00 13 00 00 48",
"02 00 13 00 00 4B"
],
"send": "00 00 13 00 00 49",
"label": "mirror_selector_0013_clear_from_button",
"delay": 0.050,
"listen": 0.20,
"once": true
}
]
},
{
"action": "send",
"label": "readback_selector_0013_after_press_1",
"frame": "01 00 13 00 00 48",
"listen": 0.60
},
{
"action": "note",
"message": "PRESS IRIS/M.BLACK LINK A SECOND TIME NOW. If the mirror completed the latch, this should report clear.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 9.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 80,
"abort_on_limit": false,
"respond_on": [
{
"frames": [
"00 00 13 40 00 09",
"01 00 13 40 00 08",
"02 00 13 40 00 0B"
],
"send": "00 00 13 40 00 09",
"label": "mirror_selector_0013_active_from_button",
"delay": 0.050,
"listen": 0.20,
"once": true
},
{
"frames": [
"00 00 13 00 00 49",
"01 00 13 00 00 48",
"02 00 13 00 00 4B"
],
"send": "00 00 13 00 00 49",
"label": "mirror_selector_0013_clear_from_button",
"delay": 0.050,
"listen": 0.20,
"once": true
}
]
},
{
"action": "send",
"label": "readback_selector_0013_after_press_2",
"frame": "01 00 13 00 00 48",
"listen": 0.60
},
{
"action": "note",
"message": "PRESS IRIS/M.BLACK LINK A THIRD TIME NOW to check that active returns after a mirrored clear.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 9.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 80,
"abort_on_limit": false,
"respond_on": [
{
"frames": [
"00 00 13 40 00 09",
"01 00 13 40 00 08",
"02 00 13 40 00 0B"
],
"send": "00 00 13 40 00 09",
"label": "mirror_selector_0013_active_from_button",
"delay": 0.050,
"listen": 0.20,
"once": true
},
{
"frames": [
"00 00 13 00 00 49",
"01 00 13 00 00 48",
"02 00 13 00 00 4B"
],
"send": "00 00 13 00 00 49",
"label": "mirror_selector_0013_clear_from_button",
"delay": 0.050,
"listen": 0.20,
"once": true
}
]
},
{
"action": "send",
"label": "readback_selector_0013_after_press_3",
"frame": "01 00 13 00 00 48",
"listen": 0.60
},
{
"action": "send",
"label": "repeat_last_for_hidden_report_check",
"frame": "07 00 00 00 00 5D",
"listen": 0.60
},
{
"action": "listen",
"seconds": 1.00
}
]
}

View File

@@ -0,0 +1,114 @@
{
"name": "iris-mblack-link-report-after-quiet-press",
"notes": [
"Second-pass IRIS/M.BLACK LINK button report test.",
"This drains the startup/current-state report queue until it goes quiet before asking for a physical press, so a fresh selector-0013 report is easier to separate from boot backlog.",
"Expected fresh report shapes are 00/01/02 00 13 40 00 for active, or 00/01/02 00 13 00 00 for clear. Page-1 selector 01 13 is not this button; it is selector 0x0093."
],
"steps": [
{
"action": "prompt",
"message": "Prepare to test IRIS/M.BLACK LINK after the report queue drains. Press Enter to power-cycle and start."
},
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.20
},
{
"action": "send",
"label": "selector_zero_connect_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.20
},
{
"action": "send",
"label": "cmd5_latch_clear_0096",
"frame": "05 01 16 00 00 48",
"listen": 0.02
},
{
"action": "listen_ack_until_quiet",
"seconds": 28.00,
"quiet_seconds": 0.90,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 180,
"abort_on_limit": false
},
{
"action": "send",
"label": "recover_connect_ok_seed_before_press",
"frame": "00 00 00 80 00 DA",
"listen": 0.20
},
{
"action": "note",
"message": "QUEUE IS QUIET. PRESS IRIS/M.BLACK LINK ONCE NOW; fresh-report ACK window is running.",
"banner": true
},
{
"action": "listen_ack_until_quiet",
"seconds": 14.00,
"quiet_seconds": 1.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 64,
"abort_on_limit": false
},
{
"action": "send",
"label": "recover_connect_ok_seed_before_second_press",
"frame": "00 00 00 80 00 DA",
"listen": 0.20
},
{
"action": "note",
"message": "PRESS IRIS/M.BLACK LINK A SECOND TIME NOW to test the opposite toggle state.",
"banner": true
},
{
"action": "listen_ack_until_quiet",
"seconds": 14.00,
"quiet_seconds": 1.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.005,
"post_ack_read": 0.035,
"poll_interval": 0.003,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 64,
"abort_on_limit": false
},
{
"action": "send",
"label": "repeat_last_for_hidden_report_check",
"frame": "07 00 00 00 00 5D",
"listen": 0.60
},
{
"action": "listen",
"seconds": 1.00
}
]
}

View File

@@ -0,0 +1,125 @@
{
"name": "iris-mblack-link-report-press",
"notes": [
"Focused queued-report test for the IRIS/M.BLACK LINK physical button.",
"The ROM path at H'200E toggles E800[0x0013].14 when F006.7/F6DB.7 is active and F731 <= 3, then calls loc_3E54 with R2=0x80 R3=0x0013.",
"This scenario services the local report queue by ACKing checksum-valid 00/01/02 report frames with command 5 for the observed selector.",
"Expected selector-0013 report shapes include 00 00 13 40 00 09 for active, 00 00 13 00 00 49 for clear, and bit15 variants 00 00 13 80 00 C9 / 00 00 13 C0 00 89. Queued 01/02 variants are also valid evidence."
],
"steps": [
{
"action": "prompt",
"message": "Prepare to test the IRIS/M.BLACK LINK button. Press Enter to power-cycle and start the live ACK windows."
},
{
"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_connect_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "cmd5_latch_clear_0096",
"frame": "05 01 16 00 00 48",
"listen": 0.05
},
{
"action": "listen_ack",
"seconds": 3.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 48,
"abort_on_limit": false
},
{
"action": "send",
"label": "recover_connect_ok_seed",
"frame": "00 00 00 80 00 DA",
"listen": 0.45
},
{
"action": "listen_ack",
"seconds": 1.20,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 32,
"abort_on_limit": false
},
{
"action": "note",
"message": "PRESS IRIS/M.BLACK LINK ONCE NOW; live queued-report ACK window is running.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 10.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 96,
"abort_on_limit": false
},
{
"action": "send",
"label": "refresh_connect_ok_seed_before_second_press",
"frame": "00 00 00 80 00 DA",
"listen": 0.35
},
{
"action": "note",
"message": "PRESS IRIS/M.BLACK LINK A SECOND TIME NOW to test the toggle-clear report.",
"banner": true
},
{
"action": "listen_ack",
"seconds": 10.00,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"ack_guard": 0.010,
"post_ack_read": 0.070,
"poll_interval": 0.004,
"once_per_frame": false,
"limit_scope": "local",
"max_acks": 96,
"abort_on_limit": false
},
{
"action": "send",
"label": "repeat_last_for_hidden_report_check",
"frame": "07 00 00 00 00 5D",
"listen": 0.80
},
{
"action": "listen",
"seconds": 1.50
}
]
}

View File

@@ -0,0 +1,100 @@
{
"name": "knee-detail-physical-button-watch",
"notes": [
"Create clean watch windows for the physical DETAIL and KNEE buttons near the LCD.",
"The ROM trace suggests these buttons may be the missing F104 panel-input transition into loc_1795.",
"Watch the console labels and LCD. During the labeled windows, press DETAIL, KNEE, then DETAIL/KNEE alternately."
],
"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": "ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "prepare_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 0.30
},
{
"action": "send",
"label": "prepare_00b9_bit13_gate",
"frame": "00 01 39 20 00 42",
"listen": 0.80
},
{
"action": "listen",
"seconds": 5.0
},
{
"action": "send",
"label": "read_00bc_after_detail_press_window",
"frame": "01 01 3C 00 00 66",
"listen": 0.80
},
{
"action": "send",
"label": "refresh_00b9_bit13_gate_for_knee_press",
"frame": "00 01 39 20 00 42",
"listen": 0.50
},
{
"action": "listen",
"seconds": 5.0
},
{
"action": "send",
"label": "read_00bc_after_knee_press_window",
"frame": "01 01 3C 00 00 66",
"listen": 0.80
},
{
"action": "send",
"label": "prepare_00b9_a0_for_page_label_watch",
"frame": "00 01 39 A0 00 C2",
"listen": 0.80
},
{
"action": "listen",
"seconds": 6.0
},
{
"action": "send",
"label": "prepare_0110_dl_override_then_press_buttons",
"frame": "00 01 90 80 00 4B",
"listen": 0.80
},
{
"action": "listen",
"seconds": 6.0
},
{
"action": "send",
"label": "final_read_00bc",
"frame": "01 01 3C 00 00 66",
"listen": 0.80
}
]
}

View File

@@ -0,0 +1,206 @@
{
"name": "knee-rom-dtl-knee-isolation",
"notes": [
"Isolate which ROM-derived KNEE selector sequence creates the bench DTL/KNEE LCD state.",
"Record the exact case label visible in the console when DTL/KNEE appears or disappears.",
"Cases separate 0x00B9.13 gate, 0x00B9.15 label bit, and 0x0110.15 timed override."
],
"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": "case_a_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_a_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_a_clear_00b9",
"frame": "00 01 39 00 00 62",
"listen": 0.30
},
{
"action": "send",
"label": "case_a_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 0.30
},
{
"action": "send",
"label": "case_a_set_00b9_bit13_only_watch_dtl_knee",
"frame": "00 01 39 20 00 42",
"listen": 3.00
},
{
"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": "case_b_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_b_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_b_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 0.30
},
{
"action": "send",
"label": "case_b_set_00b9_bits15_and_13_watch_dtl_knee",
"frame": "00 01 39 A0 00 C2",
"listen": 3.00
},
{
"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": "case_c_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_c_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_c_clear_00b9",
"frame": "00 01 39 00 00 62",
"listen": 0.30
},
{
"action": "send",
"label": "case_c_set_0110_bit15_only_watch_dtl_knee",
"frame": "00 01 90 80 00 4B",
"listen": 3.00
},
{
"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": "case_d_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_d_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_d_set_00b9_bit13_then_0110_override_part1",
"frame": "00 01 39 20 00 42",
"listen": 0.80
},
{
"action": "send",
"label": "case_d_set_0110_bit15_after_00b9_bit13_watch_dtl_knee",
"frame": "00 01 90 80 00 4B",
"listen": 3.00
},
{
"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": "case_e_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_e_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_e_set_00b9_a0_then_0110_override_part1",
"frame": "00 01 39 A0 00 C2",
"listen": 0.80
},
{
"action": "send",
"label": "case_e_set_0110_bit15_after_00b9_a0_watch_dtl_knee",
"frame": "00 01 90 80 00 4B",
"listen": 3.00
}
]
}

View File

@@ -0,0 +1,132 @@
{
"name": "knee-rom-gate-and-value-probe",
"notes": [
"Probe the ROM-derived KNEE model from docs/pt2-knee-rom-trace.md.",
"The key correction is that 0x00B9.13 gates the live KNEE value/report branch, while 0x0110.15 forces the timed KNEE page.",
"Serial writes may only prepare the state. If the panel has a KNEE-related control, move it during the long gate windows and watch for selector 0x00BC reports or visible KNEE changes."
],
"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": "ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "clear_00b9",
"frame": "00 01 39 00 00 62",
"listen": 0.30
},
{
"action": "send",
"label": "clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 0.80
},
{
"action": "send",
"label": "read_00bc_baseline",
"frame": "01 01 3C 00 00 66",
"listen": 0.60
},
{
"action": "send",
"label": "set_00b9_bit13_gate_move_knee_control_if_possible",
"frame": "00 01 39 20 00 42",
"listen": 4.00
},
{
"action": "send",
"label": "read_00bc_after_gate_window",
"frame": "01 01 3C 00 00 66",
"listen": 0.80
},
{
"action": "send",
"label": "set_00b9_bits15_and_13_preset_label_plus_gate",
"frame": "00 01 39 A0 00 C2",
"listen": 1.50
},
{
"action": "send",
"label": "set_0110_bit15_dl_timed_page_override",
"frame": "00 01 90 80 00 4B",
"listen": 2.50
},
{
"action": "send",
"label": "clear_0110_return_to_value_path",
"frame": "00 01 90 00 00 CB",
"listen": 1.00
},
{
"action": "send",
"label": "read_00bc_after_override_clear",
"frame": "01 01 3C 00 00 66",
"listen": 0.80
},
{
"action": "repeat",
"count": 2,
"steps": [
{
"action": "send",
"label": "00bc_direct_low_probe",
"frame": "00 01 3C 00 00 67",
"listen": 0.35
},
{
"action": "send",
"label": "00bc_direct_mid_probe",
"frame": "00 01 3C 40 00 27",
"listen": 0.35
},
{
"action": "send",
"label": "00bc_direct_high_probe",
"frame": "00 01 3C 80 00 E7",
"listen": 0.35
},
{
"action": "send",
"label": "read_00bc_direct_probe",
"frame": "01 01 3C 00 00 66",
"listen": 0.50
}
]
},
{
"action": "send",
"label": "final_clear_00b9",
"frame": "00 01 39 00 00 62",
"listen": 0.50
},
{
"action": "send",
"label": "final_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 1.50
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -0,0 +1,206 @@
{
"name": "lamp-isolate-knee-bit-scan",
"notes": [
"Bit-scan the two fresh-boot KNEE AUTO candidates, 0x00B9 and 0x0110.",
"Each selector gets its own fresh CONNECT OK seed, then high-byte bits are tested one at a time with clears between writes.",
"Record whether KNEE AUTO, KNEE manual, or any neighboring paint/detail lamp changes during each label."
],
"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_for_00b9",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_00b9",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_00b9_bit15_watch",
"frame": "00 01 39 80 00 E2",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_00b9_clear_after_bit15",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_00b9_bit14_watch",
"frame": "00 01 39 40 00 22",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_00b9_clear_after_bit14",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_00b9_bit13_watch",
"frame": "00 01 39 20 00 42",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_00b9_clear_after_bit13",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_00b9_bit12_watch",
"frame": "00 01 39 10 00 72",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_00b9_clear_after_bit12",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_00b9_bit11_watch",
"frame": "00 01 39 08 00 6A",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_00b9_clear_after_bit11",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_00b9_lowff_watch",
"frame": "00 01 39 00 FF 9D",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_00b9_clear_after_lowff",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"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_for_0110",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0110",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0110_bit15_watch",
"frame": "00 01 90 80 00 4B",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_0110_clear_after_bit15",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_0110_bit14_watch",
"frame": "00 01 90 40 00 8B",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_0110_clear_after_bit14",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_0110_bit13_watch",
"frame": "00 01 90 20 00 EB",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_0110_clear_after_bit13",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_0110_bit12_watch",
"frame": "00 01 90 10 00 DB",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_0110_clear_after_bit12",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_0110_bit11_watch",
"frame": "00 01 90 08 00 C3",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_0110_clear_after_bit11",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
},
{
"action": "send",
"label": "candidate_0110_lowff_watch",
"frame": "00 01 90 00 FF 34",
"listen": 1.30
},
{
"action": "send",
"label": "candidate_0110_clear_after_lowff",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
}
]
}

View File

@@ -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
}
]
}

View File

@@ -0,0 +1,260 @@
{
"name": "lamp-isolate-knee-tail-single-boot",
"notes": [
"Fresh-boot isolation for the late broad-status candidates after KNEE AUTO appeared toward the end of the previous run.",
"Selector 0x0092 is included as a guard before the stronger tail candidates 0x00A7, 0x00B7, 0x00B9, and 0x0110.",
"Each candidate tests high, clear, all bits, clear."
],
"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_for_0092",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0092",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0092_high_watch",
"frame": "00 01 12 80 00 C9",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_0092_clear_after_high",
"frame": "00 01 12 00 00 49",
"listen": 0.90
},
{
"action": "send",
"label": "candidate_0092_all_bits_watch",
"frame": "00 01 12 FF FF 49",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_0092_clear_after_all",
"frame": "00 01 12 00 00 49",
"listen": 0.90
},
{
"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_for_00a7",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_00a7",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_00a7_high_watch",
"frame": "00 01 27 80 00 FC",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_00a7_clear_after_high",
"frame": "00 01 27 00 00 7C",
"listen": 0.90
},
{
"action": "send",
"label": "candidate_00a7_all_bits_watch",
"frame": "00 01 27 FF FF 7C",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_00a7_clear_after_all",
"frame": "00 01 27 00 00 7C",
"listen": 0.90
},
{
"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_for_00b7",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_00b7",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_00b7_high_watch",
"frame": "00 01 37 80 00 EC",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_00b7_clear_after_high",
"frame": "00 01 37 00 00 6C",
"listen": 0.90
},
{
"action": "send",
"label": "candidate_00b7_all_bits_watch",
"frame": "00 01 37 FF FF 6C",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_00b7_clear_after_all",
"frame": "00 01 37 00 00 6C",
"listen": 0.90
},
{
"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_for_00b9",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_00b9",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_00b9_high_watch",
"frame": "00 01 39 80 00 E2",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_00b9_clear_after_high",
"frame": "00 01 39 00 00 62",
"listen": 0.90
},
{
"action": "send",
"label": "candidate_00b9_all_bits_watch",
"frame": "00 01 39 FF FF 62",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_00b9_clear_after_all",
"frame": "00 01 39 00 00 62",
"listen": 0.90
},
{
"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_for_0110",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0110",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0110_high_watch",
"frame": "00 01 90 80 00 4B",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_0110_clear_after_high",
"frame": "00 01 90 00 00 CB",
"listen": 0.90
},
{
"action": "send",
"label": "candidate_0110_all_bits_watch",
"frame": "00 01 90 FF FF CB",
"listen": 1.80
},
{
"action": "send",
"label": "candidate_0110_clear_after_all",
"frame": "00 01 90 00 00 CB",
"listen": 0.90
}
]
}

View File

@@ -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
}
]
}

View File

@@ -0,0 +1,238 @@
{
"name": "lamp-isolate-neighbor-single-boot",
"notes": [
"Fresh-boot isolation for the selector cluster that produced SLAVE, green tally, BARS, MASTER, and related lamps.",
"Each candidate starts from a fresh power cycle and CONNECT OK seed so latched lamps from the previous selector cannot contaminate the result.",
"Record the visible lamp during the high hold and whether the low write clears it."
],
"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_for_0012",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0012",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0012_high_watch",
"frame": "00 00 12 80 00 C8",
"listen": 2.00
},
{
"action": "send",
"label": "candidate_0012_low_clear_watch",
"frame": "00 00 12 00 00 48",
"listen": 1.20
},
{
"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_for_0013",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0013",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0013_high_watch",
"frame": "00 00 13 80 00 C9",
"listen": 2.00
},
{
"action": "send",
"label": "candidate_0013_low_clear_watch",
"frame": "00 00 13 00 00 49",
"listen": 1.20
},
{
"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_for_0016",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0016",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0016_high_watch",
"frame": "00 00 16 80 00 CC",
"listen": 2.00
},
{
"action": "send",
"label": "candidate_0016_low_clear_watch",
"frame": "00 00 16 00 00 4C",
"listen": 1.20
},
{
"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_for_0017",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0017",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0017_high_watch",
"frame": "00 00 17 80 00 CD",
"listen": 2.00
},
{
"action": "send",
"label": "candidate_0017_low_clear_watch",
"frame": "00 00 17 00 00 4D",
"listen": 1.20
},
{
"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_for_0018",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_0018",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_0018_high_watch",
"frame": "00 00 18 80 00 C2",
"listen": 2.00
},
{
"action": "send",
"label": "candidate_0018_low_clear_watch",
"frame": "00 00 18 00 00 42",
"listen": 1.20
},
{
"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_for_001a",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2_for_001a",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "candidate_001a_high_watch",
"frame": "00 00 1A 80 00 C0",
"listen": 2.00
},
{
"action": "send",
"label": "candidate_001a_low_clear_watch",
"frame": "00 00 1A 00 00 40",
"listen": 1.20
}
]
}

View File

@@ -0,0 +1,110 @@
{
"name": "lamp-knee-context-hold",
"notes": [
"Test whether the known quiet active refresh context lets KNEE AUTO stay on.",
"The prior sustain test showed repeated 0x00B9.15 never lit KNEE AUTO, while repeated 0x0110.15 lit it then cleared around mid-window.",
"This pairs each KNEE candidate with the proven 0x0093=0x9020 active refresh stream."
],
"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": "case_00b9_context_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_00b9_context_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 10,
"steps": [
{
"action": "send",
"label": "context_0093_9020_refresh_for_00b9",
"frame": "00 01 13 90 20 F8",
"listen": 0.25
},
{
"action": "send",
"label": "context_00b9_bit15_knee_test",
"frame": "00 01 39 80 00 E2",
"listen": 0.35
}
]
},
{
"action": "send",
"label": "clear_00b9_with_context_watch",
"frame": "00 01 39 00 00 62",
"listen": 1.00
},
{
"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": "case_0110_context_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_0110_context_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 10,
"steps": [
{
"action": "send",
"label": "context_0093_9020_refresh_for_0110",
"frame": "00 01 13 90 20 F8",
"listen": 0.25
},
{
"action": "send",
"label": "context_0110_bit15_knee_test",
"frame": "00 01 90 80 00 4B",
"listen": 0.35
}
]
},
{
"action": "send",
"label": "clear_0110_with_context_watch",
"frame": "00 01 90 00 00 CB",
"listen": 1.00
}
]
}

View File

@@ -0,0 +1,116 @@
{
"name": "lamp-knee-edge-refresh",
"notes": [
"Test whether KNEE AUTO needs a low-to-high edge on 0x0110.15 rather than repeated high writes.",
"The context-hold test kept active serial responses alive, but KNEE AUTO still turned off mid-run.",
"This alternates clear/set for 0x0110.15, first by itself and then with 0x0093=0x9020 context refresh."
],
"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": "edge_only_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "edge_only_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 10,
"steps": [
{
"action": "send",
"label": "edge_only_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 0.18
},
{
"action": "send",
"label": "edge_only_set_0110_bit15_watch",
"frame": "00 01 90 80 00 4B",
"listen": 0.42
}
]
},
{
"action": "send",
"label": "edge_only_final_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 1.00
},
{
"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": "edge_context_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "edge_context_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 10,
"steps": [
{
"action": "send",
"label": "edge_context_0093_9020_refresh",
"frame": "00 01 13 90 20 F8",
"listen": 0.20
},
{
"action": "send",
"label": "edge_context_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 0.16
},
{
"action": "send",
"label": "edge_context_set_0110_bit15_watch",
"frame": "00 01 90 80 00 4B",
"listen": 0.34
}
]
},
{
"action": "send",
"label": "edge_context_final_clear_0110",
"frame": "00 01 90 00 00 CB",
"listen": 1.00
}
]
}

View File

@@ -0,0 +1,110 @@
{
"name": "lamp-knee-or-precedence",
"notes": [
"Test whether KNEE AUTO is an OR of 0x00B9.15 and 0x0110.15, or whether latest-write/hidden precedence matters.",
"Case 1 sets 0x00B9.15, then 0x0110.15, then clears 0x00B9 while 0x0110 remains set.",
"Case 2 sets 0x0110.15, then 0x00B9.15, then clears 0x0110 while 0x00B9 remains set."
],
"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": "case1_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case1_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case1_set_00b9_bit15_watch_knee_auto",
"frame": "00 01 39 80 00 E2",
"listen": 1.50
},
{
"action": "send",
"label": "case1_set_0110_bit15_while_00b9_still_set",
"frame": "00 01 90 80 00 4B",
"listen": 1.50
},
{
"action": "send",
"label": "case1_clear_00b9_watch_if_knee_stays_on",
"frame": "00 01 39 00 00 62",
"listen": 1.50
},
{
"action": "send",
"label": "case1_clear_0110_watch_knee_off",
"frame": "00 01 90 00 00 CB",
"listen": 1.20
},
{
"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": "case2_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case2_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case2_set_0110_bit15_watch_knee_auto",
"frame": "00 01 90 80 00 4B",
"listen": 1.50
},
{
"action": "send",
"label": "case2_set_00b9_bit15_while_0110_still_set",
"frame": "00 01 39 80 00 E2",
"listen": 1.50
},
{
"action": "send",
"label": "case2_clear_0110_watch_if_knee_stays_on",
"frame": "00 01 90 00 00 CB",
"listen": 1.50
},
{
"action": "send",
"label": "case2_clear_00b9_watch_knee_off",
"frame": "00 01 39 00 00 62",
"listen": 1.20
}
]
}

View File

@@ -0,0 +1,98 @@
{
"name": "lamp-knee-sustain-compare",
"notes": [
"Compare how long KNEE AUTO stays lit from repeated 0x00B9.15 versus repeated 0x0110.15 refreshes.",
"This follows the precedence result where clearing 0x0110 turned KNEE AUTO off even after 0x00B9 had been set.",
"Record whether each repeated-refresh phase holds KNEE AUTO continuously or only flashes it."
],
"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": "case_00b9_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_00b9_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 8,
"steps": [
{
"action": "send",
"label": "refresh_00b9_bit15_hold_test",
"frame": "00 01 39 80 00 E2",
"listen": 0.55
}
]
},
{
"action": "send",
"label": "clear_00b9_watch_knee_off",
"frame": "00 01 39 00 00 62",
"listen": 1.50
},
{
"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": "case_0110_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_0110_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 8,
"steps": [
{
"action": "send",
"label": "refresh_0110_bit15_hold_test",
"frame": "00 01 90 80 00 4B",
"listen": 0.55
}
]
},
{
"action": "send",
"label": "clear_0110_watch_knee_off",
"frame": "00 01 90 00 00 CB",
"listen": 1.50
}
]
}

View File

@@ -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
}
]
}

Some files were not shown because too many files have changed in this diff Show More