testing around OK state
This commit is contained in:
11
README.md
11
README.md
@@ -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
592
docs/pt2-protocol.md
Normal 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
387
h8536/connect_ok_matrix.py
Normal 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",
|
||||||
|
]
|
||||||
14
scripts/connect_ok_matrix.py
Normal file
14
scripts/connect_ok_matrix.py
Normal 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())
|
||||||
45
tests/test_connect_ok_matrix.py
Normal file
45
tests/test_connect_ok_matrix.py
Normal 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()
|
||||||
Reference in New Issue
Block a user