1
0

testing around OK state

This commit is contained in:
Aiden
2026-05-26 14:01:10 +10:00
parent 4e0ef92e25
commit 74a2e2fd2c
5 changed files with 1049 additions and 0 deletions

View File

@@ -48,6 +48,7 @@ To start the current emulator harness:
.\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --preset connect-lcd .\.venv\Scripts\python.exe h8536_emulator_rx_probe.py --preset connect-lcd
.\.venv\Scripts\python.exe h8536_emulator_rx_divergence.py --default-frames --uart-timing --wait-heartbeats 2 --summary-only .\.venv\Scripts\python.exe h8536_emulator_rx_divergence.py --default-frames --uart-timing --wait-heartbeats 2 --summary-only
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen .\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite minimal --prompt-observation --result-json captures\connect-ok-minimal-result.json
.\.venv\Scripts\python.exe scripts\serial_ack_probe.py --ack-frame "05 00 40 00 00 1F" .\.venv\Scripts\python.exe scripts\serial_ack_probe.py --ack-frame "05 00 40 00 00 1F"
.\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json .\.venv\Scripts\python.exe scripts\serial_scenario.py scenarios\ack-race-000-001.json --log captures\ack-race-000-001.txt --result-json captures\ack-race-000-001-result.json
.\.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\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
@@ -60,6 +61,8 @@ To start the current emulator harness:
The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed. The real-device bench helper uses `pyserial`; install repo dependencies with `.\.venv\Scripts\python.exe -m pip install -r requirements.txt` if needed.
The current PT2/protocol reconstruction is documented in [docs/pt2-protocol.md](docs/pt2-protocol.md).
## Real Bench Serial Format ## Real Bench Serial Format
The real RCP serial link is `38400 8E1`, not `38400 8N1`. This is backed by the ROM SCI1 init: The real RCP serial link is `38400 8E1`, not `38400 8N1`. This is backed by the ROM SCI1 init:
@@ -124,6 +127,7 @@ Minimal smoke-test shape:
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM. - Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, can optionally schedule bench-style UART byte arrivals at real spacing, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects. - Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, can optionally schedule bench-style UART byte arrivals at real spacing, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects.
- Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs. - Includes a bench helper for replaying the emulator-derived CONNECT LCD frame sequence against the real device through COM5, with optional COM6 relay power cycling and timestamped capture logs.
- Includes a CONNECT: OK bench matrix runner that power-cycles between cases and tests the known sequence, single frames, primer pairs, order permutations, inter-frame gaps, repeats, and hold time to separate magic-frame, primer, cadence, and latch behavior.
- 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 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 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. - 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.
@@ -142,6 +146,8 @@ Current serial observations:
- Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`. - Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`.
- Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue. - Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue.
- 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 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.
- 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`. - 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. - 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.
- EEPROM layout finding: `build\rom_eeprom_layout.txt` currently identifies the ROM factory table at `H'C964-H'CA63`, the F400 shadow defaults, page 0 offset `0x000-0x007` as the signature/options header (`00 00 6B 6F FE 00 00 00`), pages 1-F offset `0x00-0x07` as blank-by-default record slots, and 89 selector mappings from the `H'C564` table into F400/EEPROM offsets. `F404` defaults to `H'FE00` and is tested as option/feature bits, while `F76E` combines persistence enable, dispatch suppression, and low-nibble EEPROM page selection. - EEPROM layout finding: `build\rom_eeprom_layout.txt` currently identifies the ROM factory table at `H'C964-H'CA63`, the F400 shadow defaults, page 0 offset `0x000-0x007` as the signature/options header (`00 00 6B 6F FE 00 00 00`), pages 1-F offset `0x00-0x07` as blank-by-default record slots, and 89 selector mappings from the `H'C564` table into F400/EEPROM offsets. `F404` defaults to `H'FE00` and is tested as option/feature bits, while `F76E` combines persistence enable, dispatch suppression, and low-nibble EEPROM page selection.
@@ -291,6 +297,10 @@ python h8536_emulator_rx_divergence.py --help
- `h8536_emulator_state_search.py --preset connect-queue --target ok --first-hit --json-out build\connect-state-search-ok.json`: run the bounded emulator state search for the minimum selector-zero queue condition that reaches `CONNECT: OK`. The default matrix varies `E000[0]` and `F730`, seeds `F970[0]=0`, starts at `loc_2806`, and executes real ROM code into the LCD handler. - `h8536_emulator_state_search.py --preset connect-queue --target ok --first-hit --json-out build\connect-state-search-ok.json`: run the bounded emulator state search for the minimum selector-zero queue condition that reaches `CONNECT: OK`. The default matrix varies `E000[0]` and `F730`, seeds `F970[0]=0`, starts at `loc_2806`, and executes real ROM code into the LCD handler.
- `h8536_emulator_state_search.py --preset custom --pc 0x2CB9 --word E000=0x8080 --byte F730=0 --target ok`: directly test the CONNECT handler branch with explicit internal state patches. - `h8536_emulator_state_search.py --preset custom --pc 0x2CB9 --word E000=0x8080 --byte F730=0 --target ok`: directly test the CONNECT handler branch with explicit internal state patches.
- `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text. - `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --prompt-screen`: power-cycle the bench device, wait for heartbeat readiness, send `04 00 00 40 00 1E`, `04 00 00 80 00 DE`, `04 00 00 C0 00 9E`, log RX/TX, and prompt for observed LCD text.
- `scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --no-power-cycle --prompt-before-send --prompt-screen --post-sequence-read 10 --log captures\connect-notact-to-ok.txt`: prove the recoverable path by waiting for `CONNECT:NOT ACT`, then sending the CONNECT sequence without cycling power.
- `scripts\connect_ok_matrix.py --suite minimal --prompt-observation --result-json captures\connect-ok-minimal-result.json`: run the first reproducibility pass for the 8E1 CONNECT: OK discovery. It power-cycles between cases and tests the known sequence, each single frame, and the likely primer pairs.
- `scripts\connect_ok_matrix.py --suite gap --prompt-observation --result-json captures\connect-ok-gap-result.json`: rerun the known `40 -> 80 -> C0` order with varied inter-frame gaps to test whether cadence matters.
- `scripts\connect_ok_matrix.py --suite hold --prompt-observation --result-json captures\connect-ok-hold-result.json`: rerun the known order with longer post-send observation windows to test whether CONNECT: OK is latched or needs continued traffic.
- `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode. - `h8536_emulator_bench_replay.py captures\bench-connect-lcd-sequence-20260525-214411.txt --assert-bench-parity`: replay a real bench log into the emulator using timed UART RX by default and intentionally fail while any response/LCD state still diverges from the bench-observed `CONNECT NOT ACT` plus `07 80 C0 60 20 5D` path. Pass `--polite-rx` for the old wait-until-consumed injection mode.
- Current status: boots from `H'1000`, initializes SCI1, models the traced X24164 EEPROM bus on P9, captures P9 byte candidates, can optionally fast-path known P9 EEPROM routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`. - Current status: boots from `H'1000`, initializes SCI1, models the traced X24164 EEPROM bus on P9, captures P9 byte candidates, can optionally fast-path known P9 EEPROM routines, schedules FRT1/FRT2 OCIA from timer registers and `--clock-hz`, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`.
@@ -343,6 +353,7 @@ python h8536_emulator_rx_divergence.py --help
- `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.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. - `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. - `scripts/bench_connect_lcd_sequence.py`: real-device COM5/COM6 bench runner for the CONNECT LCD sequence.
- `scripts/connect_ok_matrix.py`: real-device COM5/COM6 CONNECT: OK reproducibility matrix runner for single-frame, pair, order, gap, repeat, and hold tests.
- `scripts/serial_table_dump.py`: read-only COM5/COM6 command-1 table sweep for inferring live EEPROM-backed parameter state. Bench scripts default to `38400 8E1`. - `scripts/serial_table_dump.py`: read-only COM5/COM6 command-1 table sweep for inferring live EEPROM-backed parameter state. Bench scripts default to `38400 8E1`.
- `scripts/serial_scenario.py`: JSON-driven COM5/COM6 bench scenario runner for chained probes, waits, read sweeps, and ACK-on-target experiments. - `scripts/serial_scenario.py`: JSON-driven COM5/COM6 bench scenario runner for chained probes, waits, read sweeps, and ACK-on-target experiments.
- `scripts/state_map_runner.py`: COM5/COM6 PT2 state-map proof runner and offline bench-log analyzer. - `scripts/state_map_runner.py`: COM5/COM6 PT2 state-map proof runner and offline bench-log analyzer.

592
docs/pt2-protocol.md Normal file
View File

@@ -0,0 +1,592 @@
# PT2 Protocol Working Notes
This document is the current working model for the serial protocol spoken by the Sony RCP-TX7 panel ROM.
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.
## Current High-Confidence Facts
- The real bench link is `38400 8E1`, not `38400 8N1`.
- The ROM uses H8/536 SCI1 through the MAX202 RS232 transceiver.
- Frames are six bytes long.
- The checksum is `0x5A XOR byte0 XOR byte1 XOR byte2 XOR byte3 XOR byte4`.
- The ROM validates the checksum before normal command dispatch.
- The first byte is decoded as `command = byte0 & 0x07`.
- Bytes 1 and 2 encode a logical selector.
- Bytes 3 and 4 are a 16-bit value for write-style commands.
- The protocol is stateful. Some commands only work while a continuation/session latch is live.
- `CONNECT:NOT ACT` is recoverable without a power cycle.
- `CONNECT: OK` can be reached on real hardware after using the correct `8E1` serial format.
## Hardware Path
SCI1 is the external serial path:
- H8/536 pin 66, `P95/TXD`, goes to MAX202 pin 11.
- MAX202 pin 12 goes to H8/536 pin 67, `P96/RXD`.
- The ROM initializes SCI1 as async 8-bit, even parity, 1 stop.
ROM evidence:
- `build/rom_decompiled.asm:437`: `SCI1_SMR = H'24`.
- `build/rom_decompiled.asm:438`: `SCI1_SCR = H'3C`.
- `build/rom_decompiled.asm:439`: `SCI1_BRR = H'07`.
Bench implication:
- Use `38400 8E1` for all real-device captures and probes.
- Old `38400 8N1` captures mostly exercised parity/error handling and retry echoes. Do not assign normal protocol meaning to old `8N1` `07...` frames until they are reproduced under `8E1`.
## Frame Format
Working host/RCP frame layout:
```text
byte0 command byte; ROM uses byte0 & 0x07
byte1 selector page/high bits; byte1.7 is rejected by normal handlers
byte2 selector low byte
byte3 value high byte
byte4 value low byte
byte5 checksum = 0x5A XOR byte0..byte4
```
Examples:
```text
00 00 00 80 80 5A ; command 0, selector 0x000, value 0x8080
01 00 00 00 00 5B ; command 1, read selector 0x000
04 00 00 80 00 DE ; command 4 shape, selector 0x000, value high 0x80
07 00 00 00 00 5D ; command 7, repeat previous finalized TX frame
```
## Selector Decode
The ROM builds a raw selector from bytes 1 and 2, then maps it through `loc_622B`.
Known decode:
| byte1 page | byte2 range | selector |
| --- | --- | --- |
| page 0, or pages 4-7 | `00-7F` | `0x000 + byte2` |
| page 1 | `00-FF` | `0x080 + byte2` |
| page 2 | `00-7F` | `0x180 + byte2` |
| page 3 | any | `0x1FF` fallback |
| invalid range | any | `0x1FF` fallback |
Important caveats:
- `byte1.7` is rejected before normal command handling.
- Frames like `01 80 40 ...` may look like a selector encoding, but the normal command path rejects `byte1.7`.
- Pages 4-7 appear to alias the page-0 path when the low byte is in range.
## Checksum
Checksum formula:
```python
checksum = 0x5A
for b in frame[:5]:
checksum ^= b
```
The ROM path:
- RXI captures bytes into `F868-F86D`.
- Main-loop processing copies them to `F860-F865`.
- Physical error latch `FAA4.7` is checked before checksum dispatch.
- Checksum mismatch enters the retry/error path.
- Valid checksum clears retry counter `FAA6`, decodes selector, and dispatches on `byte0 & 0x07`.
Key ROM areas:
- RXI/ERI capture: `BB57`, `BB67`.
- Validation and checksum: `BBAB-BBF0`.
- Selector decode call: `BC01 -> 622B`.
- Command dispatch: `BC08-BC67`.
## Command Model
The biggest protocol lesson is that the command set has two modes:
- Initial dispatcher: active while `FAA2 == 0`.
- Continuation dispatcher: active while `FAA2 != 0`.
That means command numbers are not globally meaningful. Command `4`, `5`, and `6` are continuation-path commands, not normal idle commands.
| Command | Path | Current meaning | Response |
| --- | --- | --- | --- |
| `0x00` | initial | Set primary/current value, queue selector processing | Immediate `0x04` echo-style response |
| `0x01` | initial | Read primary value table | Immediate `0x04` readback response |
| `0x02` | initial | Quiet clear/no-op style command | No immediate response seen in ROM |
| `0x04` | continuation | Set/update value without immediate response | Usually no immediate response |
| `0x05` | continuation | ACK/session-clear/pending handling | Usually no immediate response |
| `0x06` | continuation | Set secondary value table | Usually no immediate response |
| `0x07` | both | Retransmit previous finalized TX frame | Repeats last TX frame |
## Command Details
### Command 0: Initial Set Value
Path: `BC69`.
Conditions:
- Valid checksum.
- `FAA2 == 0`.
- `byte1.7 == 0`.
Effects:
- Writes value into `E000 + 2*selector`.
- Writes value into `E800 + 2*selector`.
- Sets dirty flag bit 7 in `EC00 + selector`.
- Calls `BE70`, which appends the selector into the `F970` selector-processing queue.
- Sends an immediate `0x04` response through `BA26`.
Selector-zero special case:
- For selector `0x000`, the low byte is forced to `0x80`.
- This makes selector-zero writes look like `xx80`, not arbitrary `xxLL`.
Important candidate:
```text
00 00 00 80 80 5A ; selector 0 = 0x8080, strongest CONNECT OK seed
```
### Command 1: Read Value
Path: `BCD7`.
Effects:
- Reads `E000 + 2*selector`.
- Stages a `0x04` response.
- Clears `FAA2.7`.
Useful readback examples:
```text
01 00 00 00 00 5B ; read selector 0x000
01 00 40 00 00 1B ; read selector 0x040
01 01 76 00 00 2C ; read selector 0x0F6
```
Bench implication:
- Command 1 verifies table state.
- It is not an ACK and does not enter continuation handling.
- Command 7 after command 1 can repeat the last finalized readback.
### Command 2: Initial Clear/No-Op Candidate
Path: `BD04`.
Effects:
- Clears `FAA2.7`.
- Returns without staging an obvious response.
Meaning is still unclear. Treat it as a quiet/session-control candidate, not as a data write.
### Command 4: Continuation Set Value
Path: `BD0E`.
Conditions:
- Valid checksum.
- `FAA2 != 0`.
- Command bit 2 set.
- `byte1.7 == 0`.
Effects:
- Writes value into `E000 + 2*selector`.
- Selector zero also updates `E800`.
- Nonzero selectors set dirty flag bit 7 in `EC00 + selector`.
- Can mirror/persist mapped nonzero selectors through the `F400`/EEPROM path when `F76E.7` allows it.
- If `FAA2.3` was set by a queued report, command 4 can advance `F9B5` to consume that report.
- Clears `FAA3` and `FAA2` before exit.
Known CONNECT test frames:
```text
04 00 00 40 00 1E
04 00 00 80 00 DE
04 00 00 C0 00 9E
```
ROM caveat:
- A standalone command 4 from a truly idle `FAA2 == 0` state should not reach `BD0E`.
- Bench evidence now proves the panel can still recover from `CONNECT:NOT ACT` without power cycling, so visible `NOT ACT` is not equivalent to "all serial continuation state is impossible".
### Command 5: Continuation ACK/Clear Candidate
Path: `BD80`.
Conditions:
- Continuation path only.
Effects:
- Usually no immediate response.
- Selectors `0x006C`, `0x006D`, and `0x006E` call `BE70`.
- If `F731.7` is set, selectors `0x006B`, `0x0096`, `0x0097`, `0x00C6`, and `0x00F8` clear `F731.7/F790.7`.
- If `FAA2.3` was set by a queued report, command 5 can advance `F9B5`.
- Clears `FAA3` and `FAA2` before exit.
Bench implication:
- Command 5 is not a generic always-live ACK.
- It only has ACK-like meaning when the continuation latch is live.
### Command 6: Continuation Secondary Set
Path: `BDDB`.
Effects:
- Writes value into `E400 + 2*selector`.
- Sets dirty flag bit 6 in `EC00 + selector`.
- Can advance queued-report state when `FAA2.3` is live.
- Clears `FAA3` and `FAA2`.
### Command 7: Repeat Previous TX
Path: `BE05`.
Effects:
- Copies the previous finalized TX frame back into staging.
- Sends it again through `BA26`.
- Works from initial and continuation paths.
Bench implication:
- Command 7 is useful as a "what did you last finalize?" probe.
- It does not prove a hidden continuation token by itself.
## RCP Transmit Frames
The TX side uses the same six-byte checksum model.
TX staging:
- `F850-F854`: staging bytes.
- `F858-F85C`: finalized bytes.
- `F85D`: computed checksum.
- `BA26`: finalizes and starts SCI1 TX.
- TXI sends bytes 1-5 after the first TDR write.
Known RCP-origin frames:
| Frame | Confidence | Current meaning |
| --- | --- | --- |
| `00 00 00 00 80 DA` | high | idle heartbeat / selector-zero report |
| `00 00 07 80 00 DD` | medium-high | observed CAM POWER button/report candidate |
| `00 00 15 80 00 CF` | medium-high | observed CALL on/report candidate |
| `00 00 15 00 00 4F` | medium-high | observed CALL off/report candidate |
| `02 00 02 00 00 5A` | medium | emulator CONNECT OK path response candidate |
Heartbeat:
- Idle frame: `00 00 00 00 80 DA`.
- Observed cadence: about 700 ms.
- ROM path: `loc_4067` enqueues selector 0, `loc_BAF2/BB08` dequeues it, `BB1C/BB20/BB2B` stages TX bytes, `BA26` emits the frame.
- FRT2 timing model: `TCR=H'02`, `OCRA=H'7A12`, modeled as a 100 ms tick at 10 MHz; `F9C4=0x07` gives about 700 ms post-send heartbeat delay.
## Retry/Error 07 Frames
`07...` frames are easy to misread.
The ROM can generate a `0x07` retry/error echo when:
- A physical RX error occurs, or
- A checksum mismatch occurs, and
- `FAA5.7` is set, and
- Retry count `FAA6` is below two.
Path:
- `BE29` retry gate.
- `BE4D` stages `F850=0x07`.
- `F851-F854` copy host `RX[1:4]`.
- `BA26` sends it.
Bench implication:
- A visible `07...` frame is not automatically a normal status report or ACK.
- Old `8N1` captures produced many misleading `07...` frames because parity errors exercised this path.
## Table Model
The ROM behaves like a selector-indexed state machine. The CCU likely seeds values, and the RCP updates LCD/lamp/control behavior from those values.
| Table | Range | Role |
| --- | --- | --- |
| Primary value table | `E000-E3FF` | Command 0/4 writes, command 1 reads |
| Secondary value table | `E400-E7FF` | Command 6 writes |
| Current/report table | `E800-EBFF` | Used when RCP builds outbound report frames |
| Dirty/flag table | `EC00-EFFF` | Per-selector flags, bit7 for primary writes, bit6 for secondary writes |
| EEPROM/shadow | `F400-F4FF` | Optional mapped persistence/config surface |
Important details:
- Selector zero is special in command 0 and command 4.
- Command 0 writes both `E000` and `E800`.
- Command 4 writes `E800` only for selector zero in the current ROM evidence.
- `BAF2` reads `E800 + 2*selector` when building autonomous RCP reports.
- `BE70/F970` is a selector-processing queue.
- `3E54/F870` is a separate serial-visible report queue.
Do not mix up:
- `F970`: "process this selector internally".
- `F870`: "send this selector/report over serial".
## State And Queues
Important RAM/state bytes:
| Address | Working name | Meaning |
| --- | --- | --- |
| `FAA2.7` | RX command in progress | Set on initial parse, cleared on exits |
| `FAA2.3` | queued report continuation needed | Set after autonomous report send |
| `FAA3.7` | pending resend mask | Set after queued report send |
| `FAA4.7` | RX physical error latch | Set by SCI1 ERI |
| `FAA5.7` | RX session gate | Set while `F9C5` is alive after complete RX |
| `FAA6` | retry counter | Limits retry/error echoes |
| `F9C1` | inter-byte timeout | Reloaded on RXI |
| `F9C3` | RX byte count | Counts up to six |
| `F9C4` | heartbeat/report cadence gate | Controls idle heartbeat enqueue |
| `F9C5` | RX/session timeout | Loaded with `0x14` after full RX frame |
| `F9B0/F9B5` | serial report queue cursors | Drive `F870`/`BAF2` |
| `F9B4/F9B9` | selector-processing queue cursors | Drive `F970`/`2806` |
Session expiry:
- A complete six-byte RX frame loads `F9C5=0x14`.
- FRT2 decrements `F9C5`.
- When `F9C5` reaches zero, `loc_3FEF` can clear queues/session state.
- If `FAA5.7` was set, expiry calls `loc_400C`.
- `loc_400C` clears connection/session RAM and refreshes inactive display state.
This explains why random traffic tends to settle back to `CONNECT:NOT ACT`.
## CONNECT State
`CONNECT` strings are built through the LCD driver, not received as literal serial text.
Known LCD path:
```text
FAF0-FAFF line buffer -> 3ECC -> 3F28 -> 3F40 -> F200/F201 LCD ports
```
ROM/emulator findings:
- Boot/no-active-session display can show `CONNECT:NOT ACT`.
- Direct emulator entry at `loc_2CB9` with `E000[0]=0x8080` and `F730=0` reaches `CONNECT: OK`.
- Queued selector-zero path reaches OK when:
- `F970[0]=0`
- `F9B9=0`
- `F9B4=1`
- `E000[0]=0x8080`
- `F730=0`
- Selector zero dispatches through the `28A6` jump table into the CONNECT handler window.
Bench findings:
- Correct `8E1` serial format made the CONNECT path work on real hardware.
- Real hardware can recover from `CONNECT:NOT ACT` to `CONNECT: OK` without a power cycle.
- Successful active-looking state included:
- `CONNECT: OK`
- CAM POWER lamp illuminated
- numeric readouts illuminated as `----`
- Matrix tests show that cadence matters:
- `40 -> 80 -> C0` with 10 ms, 50 ms, or 150 ms inter-frame gaps stayed at `CONNECT:NOT ACT` after a fresh power-cycle test.
- The same order with 700 ms and 1.5 s inter-frame gaps produced `CONNECT: OK` before falling back to `CONNECT:NOT ACT`.
- At 700 ms gaps, no single frame worked by itself.
- At 700 ms gaps, every tested two-frame pair worked: `40 -> 80`, `80 -> C0`, and `40 -> C0`.
- A repeated identical pair also worked: `80 -> 80` at about 700 ms produced eight `02 00 02 00 00 5A` OK-path responses, then heartbeat traffic resumed.
- The no-power-cycle recovery test from an already visible `CONNECT:NOT ACT` state produced repeated `02 00 02 00 00 5A` OK-path responses, then returned to heartbeat traffic.
Current interpretation:
- `CONNECT:NOT ACT` is a normal no-active-session/cleared-state display, not a terminal latch.
- `CONNECT: OK` is table/state driven, probably selector-zero active/connect state.
- `0x8080` at selector zero is the strongest known active/connect value.
- The panel likely expects the CCU to keep seeding or refreshing state after entering OK.
- The working fake-CCU sequence is probably not "three frames as fast as possible"; it appears to need CCU-like cadence or a live session window, roughly on the heartbeat/report timescale.
- A single selector-zero continuation-shaped frame is insufficient in the current tests; two selector-zero writes at the working cadence are enough. They do not need to carry different values, because `80 -> 80` also worked.
## Candidate CCU Seed Values
These are syntactically valid host frames produced from ROM table mining. Use them as candidate fake-CCU state seeds, not as final protocol truth.
| Selector | Candidate value | Frame | Why it matters |
| --- | --- | --- | --- |
| `0x000` | `0x8080` | `00 00 00 80 80 5A` | strongest CONNECT OK seed |
| `0x003` | `0x8000` | `00 00 03 80 00 D9` | ROM default enabled bit candidate |
| `0x040` | `0xFFFF` | `00 00 40 FF FF 1A` | ROM default all-ones/status block candidate |
| `0x040` | `0x4030` | `00 00 40 40 30 6A` | bench-touched 0x40 family value |
| `0x0F6` | `0x2000` | `00 01 76 20 00 0D` | `loc_48FA` tests `E1EC.13` and can enqueue report `0x00F6` |
Readbacks:
```text
01 00 00 00 00 5B ; selector 0x000
01 00 03 00 00 58 ; selector 0x003
01 00 40 00 00 1B ; selector 0x040
01 01 76 00 00 2C ; selector 0x0F6
```
## Observed Button/Panel Reports
Before the protocol format was corrected, the RCP appeared to emit only a few report families by itself:
```text
00 00 00 00 80 DA ; heartbeat
00 00 07 80 00 DD ; CAM POWER candidate
00 00 15 80 00 CF ; CALL on candidate
00 00 15 00 00 4F ; CALL off candidate
```
Current interpretation:
- The RCP can report some panel events.
- Many other controls probably need CCU-provided state before they become reportable or meaningful.
- The CCU likely streams display/lamp/readout state to the RCP, while the RCP reports operator changes back.
## EEPROM And Board Config
The P9 bus is not the external PT2 serial link. It is a bit-banged EEPROM/config path:
- H8 pin 62, `P91`, reaches X24164 pin 6 `SCL`.
- H8 pin 68, `P97`, reaches shared X24164 pin 5 `SDA`.
ROM findings:
- `loc_40BB` checks `P7DR.7` and `F402 == H'6B6F` before deciding whether to default EEPROM/shadow tables.
- `loc_4103` writes ROM default words through `BFE0`.
- `loc_41D2` reads sixteen 8-byte records into `F7B0-F82F`.
- Command 4 can persist mapped serial table writes when `F76E.7` is set.
Current interpretation:
- EEPROM stores panel/config/default state.
- It can affect startup and option behavior.
- After the `8E1` discovery, EEPROM is less likely to be the fundamental reason CONNECT failed, but it can still influence which selectors/features are active.
## Known Useful Bench Commands
Minimal CONNECT sequence runner:
```powershell
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --parity E --prompt-screen
```
Recover from `CONNECT:NOT ACT` without power cycling:
```powershell
.\.venv\Scripts\python.exe scripts\bench_connect_lcd_sequence.py --port COM5 --relay-port COM6 --no-power-cycle --parity E --prompt-before-send --prompt-screen --post-sequence-read 10 --log captures\connect-notact-to-ok.txt
```
Run the reproducibility/minimization matrix:
```powershell
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite minimal --parity E --prompt-observation --result-json captures\connect-ok-minimal-result.json
```
Test timing/cadence:
```powershell
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite gap --parity E --prompt-observation --result-json captures\connect-ok-gap-result.json
```
Test whether OK is held:
```powershell
.\.venv\Scripts\python.exe scripts\connect_ok_matrix.py --suite hold --parity E --prompt-observation --result-json captures\connect-ok-hold-result.json
```
Current matrix result summary:
```text
minimal suite at 150 ms gaps: all cases stayed CONNECT NOT ACT
gap suite at 10/50/150 ms: stayed CONNECT NOT ACT
gap suite at 700 ms and 1.5 s: CONNECT OK, then CONNECT NOT ACT
minimal suite at 700 ms gaps: singles stayed CONNECT NOT ACT; all pairs reached CONNECT OK then CONNECT NOT ACT
repeated 80 -> 80 at about 700 ms: CONNECT OK responses, then CONNECT NOT ACT
hold suite at 150 ms gaps: stayed CONNECT NOT ACT
no-power-cycle NOT ACT recovery: CONNECT OK responses observed, then heartbeat resumes
```
Read table state:
```powershell
.\.venv\Scripts\python.exe scripts\serial_table_dump.py --port COM5 --relay-port COM6 --start 0x000 --count 0x200 --parity E --log captures\table-read-8e1.txt
```
## Current Best Model Of Normal CCU/RCP Communication
The protocol looks like a shared selector table with stateful reporting:
1. CCU sends initial state seeds into selector tables, especially selector zero and status/display selectors.
2. RCP updates LCD, lamps, and numeric readouts from selector dispatch handlers.
3. RCP emits heartbeat/report frames from `E800` via the `F870 -> BAF2` report queue.
4. Host/CCU uses continuation commands to consume/ACK/update live reports while `FAA2/FAA3` gates are active.
5. If the CCU stops talking or session state expires, RCP clears volatile session state and returns to `CONNECT:NOT ACT`.
This fits the real panel behavior:
- Idle panel emits heartbeat.
- Correct fake-CCU traffic can wake it to `CONNECT: OK`.
- Without richer CCU state, readouts illuminate but show placeholders like `----`.
## What Is Still Unknown
- The official PT2 names for commands and selectors.
- Which selectors drive every lamp and numeric display.
- Whether the CCU sends a periodic refresh stream after CONNECT OK.
- Exact hold time before OK falls back to NOT ACT, if no refresh traffic follows.
- Whether command 4 CONNECT success depends on an existing continuation latch, timing, or a side effect created by earlier frames.
- How EEPROM option bits change selector behavior.
- Whether all old visible `07...` families can be reproduced under `8E1`.
## Next Best Refinements
1. Finish the CONNECT matrix runs:
- rerun `hold` with 700 ms gaps to measure how long OK remains without refresh traffic.
2. Test whether periodic `80` refreshes hold CONNECT OK, and find the longest safe refresh interval.
3. Dump selector table state before and after CONNECT OK.
4. Seed selectors `0x003`, `0x040`, and `0x0F6` after selector-zero OK and watch lamps/readouts.
5. Mine selector dispatch handlers for known UI text terms: `IRIS`, `GAIN`, `SHUTTER`, `BARS`, `BLACK`, `CALL`, `AUTO`, `DIAG`.
6. Build a fake-CCU streamer that repeatedly writes a small selector set and logs which RCP reports appear.
## Source Files And Reports
Generated evidence:
- `build/rom_decompiled.asm`
- `build/rom_rx_branch_trace.txt`
- `build/rom_ccu_seed_hints.txt`
- `build/rom_eeprom_layout.txt`
- `build/rom_table_xrefs.txt`
- `build/connect-state-search-ok.json`
Useful tools:
- `h8536_protocol_trace.py`
- `h8536_protocol_capture.py`
- `h8536_rx_branch_trace.py`
- `h8536_ccu_seed_hints.py`
- `h8536_emulator_rx_probe.py`
- `h8536_emulator_state_search.py`
- `scripts/bench_connect_lcd_sequence.py`
- `scripts/connect_ok_matrix.py`
- `scripts/serial_table_dump.py`
- `scripts/serial_scenario.py`

387
h8536/connect_ok_matrix.py Normal file
View File

@@ -0,0 +1,387 @@
from __future__ import annotations
import argparse
import json
import sys
import time
from dataclasses import dataclass
from datetime import datetime
from itertools import permutations
from pathlib import Path
from typing import Any, TextIO
from .bench_connect_lcd import (
BenchLogger,
COMMAND7_REPEAT_FRAME,
FrameDetector,
add_serial_format_args,
_import_serial,
open_device_serial,
_read_for,
_relay_command,
_relay_settle,
_send_frame,
_wait_for_ready,
format_frame,
frame_checksum_ok,
serial_format_label,
)
FRAME_40_DXC = bytes.fromhex("04000040001E")
FRAME_80_OK = bytes.fromhex("0400008000DE")
FRAME_C0_PRIORITY = bytes.fromhex("040000C0009E")
NAMED_FRAMES = {
"40": FRAME_40_DXC,
"80": FRAME_80_OK,
"c0": FRAME_C0_PRIORITY,
}
@dataclass(frozen=True)
class MatrixCase:
name: str
frames: tuple[bytes, ...]
gap: float = 0.150
repeat: int = 1
post_read: float = 3.0
note: str = ""
def default_log_path(suite: str) -> Path:
safe_suite = "".join(char if char.isalnum() or char in "-_" else "-" for char in suite)
return Path("captures") / f"connect-ok-matrix-{safe_suite}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run a reproducibility matrix for the CONNECT: OK bench behavior."
)
parser.add_argument("--suite", choices=("baseline", "minimal", "single", "pair", "order", "gap", "repeat", "hold", "all"), default="minimal")
parser.add_argument("--case", action="append", help="run only matching case names; repeatable")
parser.add_argument("--limit", type=int, help="run only the first N selected cases")
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("--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("--no-power-cycle", action="store_true", help="do not power-cycle between cases")
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 power off between cases")
parser.add_argument("--relay-settle", type=float, default=2.0, help="seconds to wait after opening the relay port")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="seconds to wait for heartbeat after power-on")
parser.add_argument("--ready-heartbeats", type=int, default=2, help="heartbeat frames to observe before sending")
parser.add_argument("--require-ready", action="store_true", help="abort a case if readiness heartbeats are not observed")
parser.add_argument("--pre-case-drain", type=float, default=0.250, help="seconds to drain/log RX before sending each case")
parser.add_argument("--post-case-read", type=float, default=3.0, help="default seconds to listen after each case")
parser.add_argument("--default-gap", type=float, default=0.150, help="default seconds to listen between frames")
parser.add_argument("--gaps", default="0.010,0.050,0.150,0.700,1.500", help="comma-separated gaps for --suite gap")
parser.add_argument("--repeat-counts", default="1,2,4", help="comma-separated repeat counts for --suite repeat")
parser.add_argument("--hold-seconds", default="3,8,20", help="comma-separated post-read times for --suite hold")
parser.add_argument("--command7-after", action="store_true", help="send command-7 previous-frame probe after each case")
parser.add_argument("--sync", choices=("checksum", "fixed"), default="checksum", help="RX frame sync strategy")
parser.add_argument("--prompt-observation", action="store_true", help="prompt for observed LCD/lamp state after each case")
parser.add_argument("--pause-between-cases", action="store_true", help="wait for Enter before starting the next case")
parser.add_argument("--log", type=Path, help="capture log path")
parser.add_argument("--result-json", type=Path, help="write machine-readable case summary")
parser.add_argument("--dry-run", action="store_true", help="print selected cases 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)
cases = select_cases(args)
log_path = args.log or default_log_path(args.suite)
if args.dry_run:
_print_dry_run(args, cases, log_path, stdout)
return 0
serial = _import_serial()
logger = BenchLogger(log_path, stdout=stdout)
results: list[dict[str, Any]] = []
try:
logger.emit("CONNECT: OK bench matrix")
logger.emit(
f"suite={args.suite} cases={len(cases)} device={args.port} {args.baud} {serial_format_label(args)} "
f"relay={args.relay_port} {args.relay_baud} sync={args.sync}"
)
logger.emit(f"log={log_path}")
with open_device_serial(serial, args) as device:
relay = None
try:
if not args.no_power_cycle:
relay = serial.Serial(args.relay_port, args.relay_baud, timeout=0.25)
_relay_settle(relay, args.relay_settle, logger)
for index, case in enumerate(cases, start=1):
if args.pause_between_cases and index > 1:
input(f"Press Enter to start case {index}/{len(cases)}: {case.name}")
result = _run_case(args, device, relay, logger, case, index, len(cases))
results.append(result)
if args.require_ready and not result["ready"]:
logger.event("ABORT readiness was required")
break
finally:
if relay is not None:
relay.close()
_emit_matrix_summary(logger, results)
if args.result_json:
_write_result_json(args.result_json, log_path, args, results)
return 0
finally:
logger.close()
def select_cases(args: argparse.Namespace) -> list[MatrixCase]:
cases = build_cases(
args.suite,
default_gap=args.default_gap,
default_post_read=args.post_case_read,
gaps=_parse_float_csv(args.gaps),
repeat_counts=_parse_int_csv(args.repeat_counts),
hold_seconds=_parse_float_csv(args.hold_seconds),
)
if args.case:
filters = [item.lower() for item in args.case]
cases = [case for case in cases if any(fragment in case.name.lower() for fragment in filters)]
if args.limit is not None:
cases = cases[: max(0, args.limit)]
if not cases:
raise SystemExit("no matrix cases selected")
return cases
def build_cases(
suite: str,
*,
default_gap: float = 0.150,
default_post_read: float = 3.0,
gaps: list[float] | None = None,
repeat_counts: list[int] | None = None,
hold_seconds: list[float] | None = None,
) -> list[MatrixCase]:
gaps = gaps or [0.010, 0.050, 0.150, 0.700, 1.500]
repeat_counts = repeat_counts or [1, 2, 4]
hold_seconds = hold_seconds or [3.0, 8.0, 20.0]
def case(name: str, keys: tuple[str, ...], *, gap: float = default_gap, repeat: int = 1, post_read: float = default_post_read, note: str = "") -> MatrixCase:
return MatrixCase(name=name, frames=tuple(NAMED_FRAMES[key] for key in keys), gap=gap, repeat=repeat, post_read=post_read, note=note)
baseline = [
case("baseline-40-80-c0", ("40", "80", "c0"), note="known emulator-derived order"),
]
single = [
case("single-40", ("40",), note="tests whether the DXC/low path alone wakes the panel"),
case("single-80", ("80",), note="tests whether the OK/high path alone wakes the panel"),
case("single-c0", ("c0",), note="tests whether the priority-combined path alone wakes the panel"),
]
pair = [
case("pair-40-80", ("40", "80"), note="tests primer-then-OK without the C0 branch"),
case("pair-80-c0", ("80", "c0"), note="tests OK followed by priority branch"),
case("pair-40-c0", ("40", "c0"), note="tests DXC/low followed by priority branch"),
case("pair-80-40", ("80", "40"), note="tests reverse OK/DXC ordering"),
case("pair-c0-80", ("c0", "80"), note="tests C0 as primer for OK"),
case("pair-c0-40", ("c0", "40"), note="tests C0 as primer for DXC"),
]
order = [
case("order-" + "-".join(keys), keys, note="three-frame order/permutation test")
for keys in permutations(("40", "80", "c0"), 3)
]
gap_cases = [
case(f"gap-{_gap_name(gap)}-40-80-c0", ("40", "80", "c0"), gap=gap, note="same order with varied inter-frame delay")
for gap in gaps
]
repeat_cases = [
case(f"repeat-{count}x-40-80-c0", ("40", "80", "c0"), repeat=count, note="tests whether repeated cadence is required")
for count in repeat_counts
]
hold_cases = [
case(f"hold-{_gap_name(seconds)}s-40-80-c0", ("40", "80", "c0"), post_read=seconds, note="tests whether CONNECT OK persists without continued traffic")
for seconds in hold_seconds
]
suites = {
"baseline": baseline,
"minimal": baseline + [single[1], single[0], single[2]] + pair[:3],
"single": single,
"pair": pair,
"order": order,
"gap": gap_cases,
"repeat": repeat_cases,
"hold": hold_cases,
"all": baseline + single + pair + order + gap_cases + repeat_cases + hold_cases,
}
return _dedupe_cases(suites[suite])
def _run_case(
args: argparse.Namespace,
device: Any,
relay: Any | None,
logger: BenchLogger,
case: MatrixCase,
index: int,
total: int,
) -> dict[str, Any]:
detector = FrameDetector(sync_mode=args.sync)
logger.emit()
logger.emit(f"CASE {index}/{total} {case.name}")
logger.emit(f"note={case.note or '(none)'}")
logger.emit(f"gap={case.gap:.3f}s repeat={case.repeat} post_read={case.post_read:.3f}s")
for frame_index, frame in enumerate(case.frames, start=1):
logger.emit(f"case_frame[{frame_index}]={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}")
if args.no_power_cycle:
device.reset_input_buffer()
logger.event("POWER_CYCLE skipped")
else:
if relay is None:
raise SystemExit("relay was not opened")
_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)
ready = _wait_for_ready(device, detector, logger, args.ready_timeout, args.ready_heartbeats)
if args.require_ready and not ready:
observation = _prompt_observation(args, logger, case)
return _case_result(case, detector, ready=ready, observation=observation)
if args.pre_case_drain > 0:
logger.event(f"DRAIN before case {args.pre_case_drain:.3f}s")
_read_for(device, detector, logger, args.pre_case_drain)
for repeat_index in range(max(1, case.repeat)):
if case.repeat > 1:
logger.event(f"BEGIN case_repeat {repeat_index + 1}/{case.repeat}")
for frame_index, frame in enumerate(case.frames, start=1):
_send_frame(device, frame, logger, f"{case.name}.r{repeat_index + 1}.f{frame_index}")
_read_for(device, detector, logger, case.gap)
if args.command7_after:
_send_frame(device, COMMAND7_REPEAT_FRAME, logger, f"{case.name}.command7_after")
_read_for(device, detector, logger, case.gap)
if case.post_read > 0:
logger.event(f"POST_READ {case.post_read:.3f}s")
_read_for(device, detector, logger, case.post_read)
observation = _prompt_observation(args, logger, case)
result = _case_result(case, detector, ready=ready, observation=observation)
logger.event(
f"CASE_RESULT {case.name} ready={int(ready)} rx_frames={result['rx_frames']} "
f"labels={json.dumps(result['labels'], sort_keys=True)}"
)
return result
def _prompt_observation(args: argparse.Namespace, logger: BenchLogger, case: MatrixCase) -> str:
if not args.prompt_observation:
return ""
prompt = f"{case.name}: LCD/lamps/readouts observation, or Enter to skip: "
observation = input(prompt).strip()
logger.event(f"OBSERVATION {case.name}: {observation or '(no note)'}")
return observation
def _case_result(case: MatrixCase, detector: FrameDetector, *, ready: bool, observation: str) -> dict[str, Any]:
return {
"name": case.name,
"note": case.note,
"frames": [format_frame(frame) for frame in case.frames],
"gap": case.gap,
"repeat": case.repeat,
"post_read": case.post_read,
"ready": ready,
"rx_frames": len(detector.frames),
"labels": dict(detector.labels),
"resync_events": detector.resync_events,
"dropped_bytes": detector.dropped_bytes,
"trailing_unframed_bytes": len(detector.buffer),
"observation": observation,
}
def _emit_matrix_summary(logger: BenchLogger, results: list[dict[str, Any]]) -> None:
logger.emit()
logger.emit("Matrix Summary")
logger.emit(f"cases={len(results)}")
for result in results:
labels = ", ".join(f"{key}={value}" for key, value in sorted(result["labels"].items())) or "no_rx_frames"
note = result["observation"] or "(no observation)"
logger.emit(
f"{result['name']}: ready={int(result['ready'])} rx_frames={result['rx_frames']} "
f"{labels} observation={note}"
)
def _write_result_json(path: Path, log_path: Path, args: argparse.Namespace, results: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"suite": args.suite,
"log": str(log_path),
"serial_format": f"{args.baud} {serial_format_label(args)}",
"cases": results,
}
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def _print_dry_run(args: argparse.Namespace, cases: list[MatrixCase], log_path: Path, stdout: TextIO) -> None:
print(f"suite={args.suite}", file=stdout)
print(f"cases={len(cases)}", file=stdout)
print(f"device={args.port} {args.baud} {serial_format_label(args)}", file=stdout)
print(f"relay={args.relay_port} {args.relay_baud}", file=stdout)
print(f"power_cycle_between_cases={int(not args.no_power_cycle)}", file=stdout)
print(f"log={log_path}", file=stdout)
for index, case in enumerate(cases, start=1):
print(
f"case[{index}]={case.name} gap={case.gap:.3f}s repeat={case.repeat} "
f"post_read={case.post_read:.3f}s",
file=stdout,
)
for frame in case.frames:
print(f" frame={format_frame(frame)} checksum_ok={int(frame_checksum_ok(frame))}", file=stdout)
if case.note:
print(f" note={case.note}", file=stdout)
if args.command7_after:
print(f"command7_after={format_frame(COMMAND7_REPEAT_FRAME)}", file=stdout)
def _dedupe_cases(cases: list[MatrixCase]) -> list[MatrixCase]:
seen: set[str] = set()
deduped: list[MatrixCase] = []
for case in cases:
if case.name in seen:
continue
seen.add(case.name)
deduped.append(case)
return deduped
def _parse_float_csv(text: str) -> list[float]:
return [float(part.strip()) for part in text.split(",") if part.strip()]
def _parse_int_csv(text: str) -> list[int]:
return [int(part.strip(), 0) for part in text.split(",") if part.strip()]
def _gap_name(value: float) -> str:
if value >= 1:
return f"{value:.1f}".rstrip("0").rstrip(".").replace(".", "p")
milliseconds = int(round(value * 1000))
return f"{milliseconds}ms"
__all__ = [
"FRAME_40_DXC",
"FRAME_80_OK",
"FRAME_C0_PRIORITY",
"MatrixCase",
"build_arg_parser",
"build_cases",
"main",
"select_cases",
]

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
"""Bench runner for CONNECT: OK reproducibility matrix tests."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from h8536.connect_ok_matrix import main
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,45 @@
import io
import unittest
from h8536.connect_ok_matrix import build_cases, main
class ConnectOkMatrixTest(unittest.TestCase):
def test_minimal_suite_starts_with_known_sequence_and_single_ok(self):
cases = build_cases("minimal")
self.assertEqual(cases[0].name, "baseline-40-80-c0")
self.assertEqual(cases[1].name, "single-80")
self.assertIn("pair-40-80", [case.name for case in cases])
def test_gap_suite_uses_requested_inter_frame_gaps(self):
cases = build_cases("gap", gaps=[0.01, 0.7])
self.assertEqual([case.name for case in cases], ["gap-10ms-40-80-c0", "gap-700ms-40-80-c0"])
self.assertEqual([case.gap for case in cases], [0.01, 0.7])
def test_dry_run_defaults_to_even_parity_and_lists_frames(self):
stdout = io.StringIO()
exit_code = main(["--dry-run", "--suite", "minimal", "--limit", "2"], stdout=stdout)
self.assertEqual(exit_code, 0)
output = stdout.getvalue()
self.assertIn("device=COM5 38400 8E1", output)
self.assertIn("case[1]=baseline-40-80-c0", output)
self.assertIn("case[2]=single-80", output)
self.assertIn("frame=04 00 00 80 00 DE checksum_ok=1", output)
def test_case_filter_selects_named_subset(self):
stdout = io.StringIO()
exit_code = main(["--dry-run", "--suite", "all", "--case", "pair-c0-80"], stdout=stdout)
self.assertEqual(exit_code, 0)
output = stdout.getvalue()
self.assertIn("cases=1", output)
self.assertIn("case[1]=pair-c0-80", output)
if __name__ == "__main__":
unittest.main()