1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden
4ebbb963bd selector lamps 2026-05-28 10:12:08 +10:00
Aiden
4364d0ed48 updates 2026-05-27 21:37:50 +10:00
54 changed files with 30831 additions and 326 deletions

View File

@@ -1820,7 +1820,7 @@
] ]
}, },
"is_noop": false, "is_noop": false,
"known_report": "0x0013", "known_report": "IRIS/M.BLACK LINK",
"shadow": "F6DB", "shadow": "F6DB",
"source": "F006", "source": "F006",
"table_address": 10180, "table_address": 10180,
@@ -2881,6 +2881,38 @@
"table_address_hex": "H'274C", "table_address_hex": "H'274C",
"table_offset": 70 "table_offset": 70
}, },
{
"bank": "IRQ3 A8 panel byte path",
"bit": 7,
"dirty": "F6F3.3",
"dispatcher": "1BF8",
"handler": 8206,
"handler_hex": "H'200E",
"handler_summary": {
"instruction_count_scanned": 20,
"level_tests": [
"F6DB.7"
],
"report_selectors": [
19
],
"report_selectors_hex": [
"0x0013"
],
"state_writes": [
"E826"
]
},
"is_noop": false,
"known_report": "IRIS/M.BLACK LINK",
"name": "IRIS/M.BLACK LINK",
"selector": 19,
"shadow": "F6DB",
"source": "F006",
"table_address": 10180,
"table_address_hex": "H'27C4",
"table_offset": 190
},
{ {
"bank": "IRQ3 A8 panel byte path", "bank": "IRQ3 A8 panel byte path",
"bit": 5, "bit": 5,

View File

@@ -15,6 +15,16 @@ The key table is the indirect handler table at `H'2706`, used by `loc_1C0E` afte
- Current-level tests: F6D4.3 - Current-level tests: F6D4.3
- State writes: E80E - State writes: E80E
### IRIS/M.BLACK LINK
- Emitted selector: `0x0013`
- Handler: `H'200E`
- Edge source: `F006 -> F6DB` via `F6F3.3`
- Trigger bit: `F6DB.7`
- Table slot: `H'27C4` -> `H'200E`
- Current-level tests: F6DB.7
- State writes: E826
### CALL ### CALL
- Emitted selector: `0x0015` - Emitted selector: `0x0015`

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -10,6 +10,13 @@ Table Model:
- flag_table_candidate: H'EC00-H'EFFF; accesses=6 static selectors=0x000 - flag_table_candidate: H'EC00-H'EFFF; accesses=6 static selectors=0x000
Highest-Value Selector Candidates: Highest-Value Selector Candidates:
- 0x093 white_balance_black_flare_mode_lane: score=19 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_17C9: BTST.W #12, @H'E126
- primary_value_table_candidate read in loc_17FB: BTST.W #12, @H'E126
- primary_value_table_candidate read in loc_182D: BTST.W #5, @H'E126
- primary_value_table_candidate read in loc_1891: BTST.W #5, @H'E126
seed frames: 0x8000 -> 00 01 13 80 00 C8; 0x4000 -> 00 01 13 40 00 08; 0x2000 -> 00 01 13 20 00 68
readback frame: 01 01 13 00 00 49
- 0x000 heartbeat_or_idle_report_candidate: score=18 tables=primary_value_table_candidate, current_value_table_candidate, flag_table_candidate - 0x000 heartbeat_or_idle_report_candidate: score=18 tables=primary_value_table_candidate, current_value_table_candidate, flag_table_candidate
- primary_value_table_candidate write in loc_4096: MOV:G.W #H'0080, @H'E000 - primary_value_table_candidate write in loc_4096: MOV:G.W #H'0080, @H'E000
- current_value_table_candidate write in loc_4096: MOV:G.W #H'0080, @H'E800 - current_value_table_candidate write in loc_4096: MOV:G.W #H'0080, @H'E800
@@ -17,12 +24,6 @@ Highest-Value Selector Candidates:
- idle report selector and CONNECT OK emulator condition both center on selector zero - idle report selector and CONNECT OK emulator condition both center on selector zero
seed frames: 0x0080 -> 00 00 00 00 80 DA; 0x8080 -> 00 00 00 80 80 5A seed frames: 0x0080 -> 00 00 00 00 80 DA; 0x8080 -> 00 00 00 80 80 5A
readback frame: 01 00 00 00 00 5B readback frame: 01 00 00 00 00 5B
- 0x093 state_selector_candidate: score=15 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_17C9: BTST.W #12, @H'E126
- primary_value_table_candidate read in loc_17FB: BTST.W #12, @H'E126
- primary_value_table_candidate read in loc_182D: BTST.W #5, @H'E126
- primary_value_table_candidate read in loc_1891: BTST.W #5, @H'E126
readback frame: 01 01 13 00 00 49
- 0x0F6 active_status_bridge_candidate: score=14 tables=primary_value_table_candidate, current_value_table_candidate - 0x0F6 active_status_bridge_candidate: score=14 tables=primary_value_table_candidate, current_value_table_candidate
- primary_value_table_candidate read in loc_48FA: BTST.W #13, @H'E1EC - primary_value_table_candidate read in loc_48FA: BTST.W #13, @H'E1EC
- primary_value_table_candidate read in loc_48FA: MOV:G.W @H'E1EC, R0 - primary_value_table_candidate read in loc_48FA: MOV:G.W @H'E1EC, R0
@@ -42,6 +43,18 @@ Highest-Value Selector Candidates:
- ROM default table writes E000/E800 selector 0x040 to 0xFFFF and bench tests repeatedly touched the 0x40 family - ROM default table writes E000/E800 selector 0x040 to 0xFFFF and bench tests repeatedly touched the 0x40 family
seed frames: 0xFFFF -> 00 00 40 FF FF 1A; 0x4030 -> 00 00 40 40 30 6A seed frames: 0xFFFF -> 00 00 40 FF FF 1A; 0x4030 -> 00 00 40 40 30 6A
readback frame: 01 00 40 00 00 1B readback frame: 01 00 40 00 00 1B
- 0x06B standard_lamp_lane: score=11 tables=none
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
- Bench-visible STANDARD lamp lane found from ROM-derived F6D4.6 handler candidate.
- selector dispatches to H'2F72
seed frames: 0x8000 -> 00 00 6B 80 00 B1
readback frame: 01 00 6B 00 00 30
- 0x015 call_and_red_tally_lamp_lane: score=9 tables=none
- observed RCP autonomous report frame(s): 00 00 15 80 00 CF, 00 00 15 00 00 4F
- Bench-visible CALL lamp and red tally lane; local CALL handler mirrors F6DB.5 into E800[0x0015].15.
- selector dispatches to H'2E39
seed frames: 0x8000 -> 00 00 15 80 00 CF; 0x0000 -> 00 00 15 00 00 4F
readback frame: 01 00 15 00 00 4E
- 0x081 state_selector_candidate: score=9 tables=primary_value_table_candidate, current_value_table_candidate - 0x081 state_selector_candidate: score=9 tables=primary_value_table_candidate, current_value_table_candidate
- primary_value_table_candidate read in vec_ad_adi_3D99: MOV:G.W @H'E102, R0 - primary_value_table_candidate read in vec_ad_adi_3D99: MOV:G.W @H'E102, R0
- primary_value_table_candidate read in vec_ad_adi_3D99: CMP:G.W @H'E102, R1 - primary_value_table_candidate read in vec_ad_adi_3D99: CMP:G.W @H'E102, R1
@@ -52,10 +65,6 @@ Highest-Value Selector Candidates:
- primary_value_table_candidate read in loc_2650: CMP:G.W @H'E124, R0 - primary_value_table_candidate read in loc_2650: CMP:G.W @H'E124, R0
- current_value_table_candidate write in loc_2650: MOV:G.W R0, @H'E924 - current_value_table_candidate write in loc_2650: MOV:G.W R0, @H'E924
readback frame: 01 01 12 00 00 48 readback frame: 01 01 12 00 00 48
- 0x06B connection_latch_clear_candidate: score=7 tables=none
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7
- selector dispatches to H'2F72
readback frame: 01 00 6B 00 00 30
- 0x06C command5_be70_candidate: score=7 tables=none - 0x06C command5_be70_candidate: score=7 tables=none
- continuation command 5 calls BE70 for selector 0x006C - continuation command 5 calls BE70 for selector 0x006C
- selector dispatches to H'2FAF - selector dispatches to H'2FAF
@@ -64,14 +73,37 @@ Highest-Value Selector Candidates:
- continuation command 5 calls BE70 for selector 0x006D - continuation command 5 calls BE70 for selector 0x006D
- selector dispatches to H'3015 - selector dispatches to H'3015
readback frame: 01 00 6D 00 00 36 readback frame: 01 00 6D 00 00 36
- 0x110 knee_auto_lamp_or_page_status_lane: score=7 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_1795: BTST.W #15, @H'E220
- Bench-visible KNEE AUTO source; ROM notes indicate timed KNEE/detail page interaction.
seed frames: 0x8000 -> 00 01 90 80 00 4B
readback frame: 01 01 90 00 00 CA
- 0x013 slave_and_iris_mblack_link_lamps: score=6 tables=none
- Selector 0x0013 is a two-bit lamp/status word. ROM dispatch H'2E06 reads current table word H'E826 and fans bit 15 and bit 14 into panel latch RAM.
- 0x8000 SLAVE lamp: sets F791.6 and F713.4
- 0x4000 IRIS/M.BLACK LINK lamp: sets F791.5 and F716.7
- selector dispatches to H'2E06
seed frames: 0x8000 -> 00 00 13 80 00 C9; 0x4000 -> 00 00 13 40 00 09; 0x0000 -> 00 00 13 00 00 49
readback frame: 01 00 13 00 00 48
- 0x017 bars_lamp_lane: score=6 tables=none
- Bench-visible BARS lamp/latch lane; low writes do not reliably clear the visible latch.
- selector dispatches to H'2E85
seed frames: 0x8000 -> 00 00 17 80 00 CD; 0x4000 -> 00 00 17 40 00 0D; 0x0000 -> 00 00 17 00 00 4D
readback frame: 01 00 17 00 00 4C
- 0x01A monitor_selector_lamps: score=6 tables=none
- Bench-visible MONITOR selector cluster found from ROM-derived button-output sweep.
- selector dispatches to H'2EC4
seed frames: 0x0808 -> 00 00 1A 08 08 40; 0x2020 -> 00 00 1A 20 20 40; 0x4040 -> 00 00 1A 40 40 40
readback frame: 01 00 1A 00 00 41
- 0x024 lcd_selector_button_lamp: score=6 tables=none
- Bench-visible LCD selector-button lamp lane.
- selector dispatches to H'2F0C
seed frames: 0x8000 -> 00 00 24 80 00 FE; 0x0000 -> 00 00 24 00 00 7E
readback frame: 01 00 24 00 00 7F
- 0x007 camera_power_report_candidate: score=5 tables=none - 0x007 camera_power_report_candidate: score=5 tables=none
- observed RCP autonomous report frame(s): 00 00 07 80 00 DD - observed RCP autonomous report frame(s): 00 00 07 80 00 DD
- selector dispatches to H'2DC3 - selector dispatches to H'2DC3
readback frame: 01 00 07 00 00 5C readback frame: 01 00 07 00 00 5C
- 0x015 call_button_report_candidate: score=5 tables=none
- observed RCP autonomous report frame(s): 00 00 15 80 00 CF, 00 00 15 00 00 4F
- selector dispatches to H'2E39
readback frame: 01 00 15 00 00 4E
- 0x023 state_selector_candidate: score=5 tables=primary_value_table_candidate - 0x023 state_selector_candidate: score=5 tables=primary_value_table_candidate
- primary_value_table_candidate write in loc_400C: CLR.W @H'E046 - primary_value_table_candidate write in loc_400C: CLR.W @H'E046
- selector dispatches to H'2EE6 - selector dispatches to H'2EE6
@@ -91,24 +123,10 @@ Highest-Value Selector Candidates:
- 0x0F8 connection_latch_clear_candidate: score=5 tables=none - 0x0F8 connection_latch_clear_candidate: score=5 tables=none
- when F731.7 is set, command 5 on this selector clears F731.7/F790.7 - when F731.7 is set, command 5 on this selector clears F731.7/F790.7
readback frame: 01 01 78 00 00 22 readback frame: 01 01 78 00 00 22
- 0x002 state_selector_candidate: score=3 tables=primary_value_table_candidate - 0x082 iris_readout_lane: score=4 tables=none
- primary_value_table_candidate read in loc_2650: BTST.W #13, @H'E004 - Bench-visible IRIS seven-segment/display lane.
readback frame: 01 00 02 00 00 59 seed frames: 0x8000 -> 00 01 02 80 00 D9; 0x4000 -> 00 01 02 40 00 19; 0x0000 -> 00 01 02 00 00 59
- 0x0A7 state_selector_candidate: score=3 tables=primary_value_table_candidate readback frame: 01 01 02 00 00 58
- primary_value_table_candidate read in loc_1705: BTST.W #15, @H'E14E
readback frame: 01 01 27 00 00 7D
- 0x0B7 state_selector_candidate: score=3 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_174D: BTST.W #13, @H'E16E
readback frame: 01 01 37 00 00 6D
- 0x0B9 state_selector_candidate: score=3 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_1795: BTST.W #13, @H'E172
readback frame: 01 01 39 00 00 63
- 0x110 state_selector_candidate: score=3 tables=primary_value_table_candidate
- primary_value_table_candidate read in loc_1795: BTST.W #15, @H'E220
readback frame: 01 01 90 00 00 CA
- 0x012 state_selector_candidate: score=2 tables=none
- selector dispatches to H'2E03
readback frame: 01 00 12 00 00 49
Display Text Hints: Display Text Hints:
- CONNECT: 0 hit(s) - CONNECT: 0 hit(s)

View File

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

View File

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

View File

@@ -48,9 +48,26 @@ Optionally add periodic state refresh traffic:
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --refresh-active --refresh-interval 0.600 --duration 30 .\.venv\Scripts\python.exe scripts\ccu_emulator.py --refresh-active --refresh-interval 0.600 --duration 30
``` ```
Enable the bench-proven IRIS/M.BLACK LINK closed-loop module:
```powershell
.\.venv\Scripts\python.exe scripts\ccu_emulator.py --iris-mblack-link --duration 60 --log captures\ccu-iris-mblack-link.txt
```
When the RCP reports selector `0x0013` as `0x4000` or `0x0000`, this module sends:
```text
05 00 13 00 00 4C ; ACK selector 0x0013
00 00 13 40 00 09 ; mirror active, or 00 00 13 00 00 49 for clear
```
That matches the bench-proven IRIS/M.BLACK LINK state machine: the RCP reports local intent, the CCU acknowledges the selector, then the CCU mirrors the accepted state back so the next physical press toggles the other way.
## Layout ## Layout
- `frames.py`: checksums, built-in frames, and simple host-frame builders. - `frames.py`: checksums, built-in frames, and simple host-frame builders.
- `iris_mblack_link.py`: selector `0x0013` ACK-and-mirror state-machine module.
- `modules.py`: small protocol-module interface for feature-specific CCU behavior.
- `policy.py`: decides whether an RCP frame should be ACKed. - `policy.py`: decides whether an RCP frame should be ACKed.
- `refresh.py`: optional periodic state-refresh scheduling. - `refresh.py`: optional periodic state-refresh scheduling.
- `serial_link.py`: serial read/write plus checksum-resync frame detection. - `serial_link.py`: serial read/write plus checksum-resync frame detection.

View File

@@ -11,6 +11,7 @@ from .frames import (
frame_checksum_ok, frame_checksum_ok,
parse_frame, parse_frame,
) )
from .iris_mblack_link import IrisMblackLinkModule
from .policy import AckPolicy from .policy import AckPolicy
__all__ = [ __all__ = [
@@ -19,6 +20,7 @@ __all__ = [
"CcuEmulator", "CcuEmulator",
"CcuStats", "CcuStats",
"HEARTBEAT_FRAME", "HEARTBEAT_FRAME",
"IrisMblackLinkModule",
"NEUTRAL_ACK_FRAME", "NEUTRAL_ACK_FRAME",
"AckPolicy", "AckPolicy",
"format_frame", "format_frame",

View File

@@ -20,6 +20,8 @@ from h8536.bench_connect_lcd import (
from .controller import CcuConfig, CcuEmulator from .controller import CcuConfig, CcuEmulator
from .frames import ACTIVE_SEED_COMMAND0, CONNECT_CADENCE_SEQUENCE, NEUTRAL_ACK_FRAME, format_frame, parse_frame from .frames import ACTIVE_SEED_COMMAND0, CONNECT_CADENCE_SEQUENCE, NEUTRAL_ACK_FRAME, format_frame, parse_frame
from .iris_mblack_link import IrisMblackLinkModule
from .modules import CcuModule
from .policy import AckPolicy from .policy import AckPolicy
from .refresh import PeriodicRefresh from .refresh import PeriodicRefresh
from .serial_link import SerialLink from .serial_link import SerialLink
@@ -64,6 +66,18 @@ def build_arg_parser() -> argparse.ArgumentParser:
parser.add_argument("--refresh-interval", type=float, default=0.0, help="seconds between optional refresh frames") parser.add_argument("--refresh-interval", type=float, default=0.0, help="seconds between optional refresh frames")
parser.add_argument("--loop-poll", type=float, default=0.001, help="sleep between service loop iterations") parser.add_argument("--loop-poll", type=float, default=0.001, help="sleep between service loop iterations")
parser.add_argument(
"--iris-mblack-link",
action="store_true",
help="enable the selector 0x0013 IRIS/M.BLACK LINK ACK-and-mirror module",
)
parser.add_argument(
"--iris-mblack-link-mirror-delay",
type=float,
default=0.050,
help="seconds between the selector 0x0013 ACK and command-0 mirror",
)
parser.add_argument("--power-cycle", action="store_true", help="power-cycle DUT through relay before starting") parser.add_argument("--power-cycle", action="store_true", help="power-cycle DUT through relay before starting")
parser.add_argument("--relay-port", default="COM6", help="Pico relay serial port") parser.add_argument("--relay-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("--relay-baud", type=int, default=115200, help="Pico relay serial baud rate")
@@ -80,10 +94,11 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
args = build_arg_parser().parse_args(argv) args = build_arg_parser().parse_args(argv)
seed_frames = _seed_frames(args) seed_frames = _seed_frames(args)
refresh_frames = _refresh_frames(args) refresh_frames = _refresh_frames(args)
modules = _modules(args)
log_path = args.log or _default_log_path() log_path = args.log or _default_log_path()
if args.dry_run: if args.dry_run:
_print_dry_run(args, seed_frames, refresh_frames, log_path, stdout) _print_dry_run(args, seed_frames, refresh_frames, modules, log_path, stdout)
return 0 return 0
serial = _import_serial() serial = _import_serial()
@@ -100,6 +115,8 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
f"refresh_interval={args.refresh_interval:.3f}s frames=" f"refresh_interval={args.refresh_interval:.3f}s frames="
+ " | ".join(format_frame(frame) for frame in refresh_frames) + " | ".join(format_frame(frame) for frame in refresh_frames)
) )
if modules:
logger.emit("modules=" + " | ".join(module.name for module in modules))
with open_device_serial(serial, args) as device: with open_device_serial(serial, args) as device:
if args.power_cycle: if args.power_cycle:
@@ -128,7 +145,9 @@ def main(argv: list[str] | None = None, *, stdout: TextIO = sys.stdout) -> int:
ack_unlabeled_checksum_frames=not args.no_ack_unlabeled, ack_unlabeled_checksum_frames=not args.no_ack_unlabeled,
) )
refresh = PeriodicRefresh(frames=refresh_frames, interval=args.refresh_interval) refresh = PeriodicRefresh(frames=refresh_frames, interval=args.refresh_interval)
CcuEmulator(link, logger, config=config, ack_policy=policy, refresh=refresh).run(args.duration) CcuEmulator(link, logger, config=config, ack_policy=policy, refresh=refresh, modules=modules).run(
args.duration
)
return 0 return 0
finally: finally:
if relay is not None: if relay is not None:
@@ -153,10 +172,18 @@ def _refresh_frames(args: argparse.Namespace) -> list[bytes]:
return frames return frames
def _modules(args: argparse.Namespace) -> tuple[CcuModule, ...]:
modules: list[CcuModule] = []
if args.iris_mblack_link:
modules.append(IrisMblackLinkModule(mirror_delay=max(0.0, args.iris_mblack_link_mirror_delay)))
return tuple(modules)
def _print_dry_run( def _print_dry_run(
args: argparse.Namespace, args: argparse.Namespace,
seed_frames: list[bytes], seed_frames: list[bytes],
refresh_frames: list[bytes], refresh_frames: list[bytes],
modules: tuple[CcuModule, ...],
log_path: Path, log_path: Path,
stdout: TextIO, stdout: TextIO,
) -> None: ) -> None:
@@ -170,6 +197,7 @@ def _print_dry_run(
+ (" | ".join(format_frame(frame) for frame in refresh_frames) or "none"), + (" | ".join(format_frame(frame) for frame in refresh_frames) or "none"),
file=stdout, file=stdout,
) )
print("modules=" + (" | ".join(module.name for module in modules) or "none"), file=stdout)
def _default_log_path() -> Path: def _default_log_path() -> Path:

View File

@@ -6,6 +6,7 @@ from dataclasses import dataclass
from h8536.bench_connect_lcd import BenchLogger, format_frame from h8536.bench_connect_lcd import BenchLogger, format_frame
from .frames import ACTIVE_SEED_COMMAND0 from .frames import ACTIVE_SEED_COMMAND0
from .modules import CcuModule, ModuleDecision
from .policy import AckPolicy from .policy import AckPolicy
from .refresh import PeriodicRefresh from .refresh import PeriodicRefresh
from .serial_link import RxFrame, SerialLink from .serial_link import RxFrame, SerialLink
@@ -16,6 +17,7 @@ class CcuStats:
rx_frames: int = 0 rx_frames: int = 0
tx_frames: int = 0 tx_frames: int = 0
ack_frames: int = 0 ack_frames: int = 0
module_frames: int = 0
seed_frames: int = 0 seed_frames: int = 0
refresh_frames: int = 0 refresh_frames: int = 0
started_at: float = 0.0 started_at: float = 0.0
@@ -48,12 +50,14 @@ class CcuEmulator:
config: CcuConfig | None = None, config: CcuConfig | None = None,
ack_policy: AckPolicy | None = None, ack_policy: AckPolicy | None = None,
refresh: PeriodicRefresh | None = None, refresh: PeriodicRefresh | None = None,
modules: tuple[CcuModule, ...] = (),
) -> None: ) -> None:
self.link = link self.link = link
self.logger = logger self.logger = logger
self.config = config or CcuConfig() self.config = config or CcuConfig()
self.ack_policy = ack_policy or AckPolicy() self.ack_policy = ack_policy or AckPolicy()
self.refresh = refresh or PeriodicRefresh() self.refresh = refresh or PeriodicRefresh()
self.modules = modules
self.stats = CcuStats() self.stats = CcuStats()
def run(self, duration: float) -> CcuStats: def run(self, duration: float) -> CcuStats:
@@ -61,7 +65,7 @@ class CcuEmulator:
self.logger.event( self.logger.event(
"CCU_START " "CCU_START "
f"duration={duration:.3f}s seed_frames={len(self.config.seed_frames)} " f"duration={duration:.3f}s seed_frames={len(self.config.seed_frames)} "
f"ack={format_frame(self.ack_policy.ack_frame)}" f"ack={format_frame(self.ack_policy.ack_frame)} modules={len(self.modules)}"
) )
self._wait_ready() self._wait_ready()
self._send_seed_frames() self._send_seed_frames()
@@ -115,6 +119,9 @@ class CcuEmulator:
def _service_rx(self) -> None: def _service_rx(self) -> None:
for item in self.link.read_available(): for item in self.link.read_available():
self._record_rx(item) self._record_rx(item)
suppress_default_ack = self._service_modules(item)
if suppress_default_ack:
continue
decision = self.ack_policy.decide(item.frame, item.label) decision = self.ack_policy.decide(item.frame, item.label)
if not decision.should_ack: if not decision.should_ack:
self.logger.event(f"ACK_SKIP reason={decision.reason} frame={format_frame(item.frame)}") self.logger.event(f"ACK_SKIP reason={decision.reason} frame={format_frame(item.frame)}")
@@ -125,6 +132,28 @@ class CcuEmulator:
self.stats.ack_frames += 1 self.stats.ack_frames += 1
self.stats.tx_frames += 1 self.stats.tx_frames += 1
def _service_modules(self, item: RxFrame) -> bool:
suppress_default_ack = False
for module in self.modules:
decision = module.on_rx(item.frame, item.label)
if decision is None:
continue
if decision.reason:
self.logger.event(
f"MODULE {module.name} reason={decision.reason} frame={format_frame(item.frame)}"
)
self._send_module_decision(decision)
suppress_default_ack = suppress_default_ack or decision.suppress_default_ack
return suppress_default_ack
def _send_module_decision(self, decision: ModuleDecision) -> None:
for tx in decision.tx:
if tx.delay > 0:
time.sleep(tx.delay)
self.link.send(tx.frame, tx.label)
self.stats.module_frames += 1
self.stats.tx_frames += 1
def _service_refresh(self) -> None: def _service_refresh(self) -> None:
for frame in self.refresh.due_frames(): for frame in self.refresh.due_frames():
self.link.send(frame, "refresh") self.link.send(frame, "refresh")
@@ -141,6 +170,7 @@ class CcuEmulator:
self.logger.emit(f"rx_frames={self.stats.rx_frames}") self.logger.emit(f"rx_frames={self.stats.rx_frames}")
self.logger.emit(f"tx_frames={self.stats.tx_frames}") self.logger.emit(f"tx_frames={self.stats.tx_frames}")
self.logger.emit(f"ack_frames={self.stats.ack_frames}") self.logger.emit(f"ack_frames={self.stats.ack_frames}")
self.logger.emit(f"module_frames={self.stats.module_frames}")
self.logger.emit(f"seed_frames={self.stats.seed_frames}") self.logger.emit(f"seed_frames={self.stats.seed_frames}")
self.logger.emit(f"refresh_frames={self.stats.refresh_frames}") self.logger.emit(f"refresh_frames={self.stats.refresh_frames}")
self.logger.emit(f"resync_events={self.link.detector.resync_events}") self.logger.emit(f"resync_events={self.link.detector.resync_events}")

View File

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

25
ccu_emulator/modules.py Normal file
View File

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

View File

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

View File

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

View File

@@ -71,6 +71,81 @@ Follow-up `lamp-isolate-neighbor-single-boot` result:
This confirms that the host/CCU can directly drive panel lamps through selector-table writes. It also validates using the ROM dispatch-neighbor list around `0x0007` and `0x0015` as a high-value lamp map. This confirms that the host/CCU can directly drive panel lamps through selector-table writes. It also validates using the ROM dispatch-neighbor list around `0x0007` and `0x0015` as a high-value lamp map.
Follow-up `panel-atlas-standard-master-*` webcam runs:
- `0x0012`, `0x0013`, and `0x0014` high-nibble/selected-bit sweeps did not
produce a clean STANDARD or MASTER lamp trigger. The `0x0013=0x8000` SLAVE
positive control still worked.
- `0x0010`, `0x0011`, `0x0015`, `0x0016`, `0x0017`, `0x0018`, `0x0019`, and
`0x001A` high-nibble sweeps did not produce a clean STANDARD or MASTER lamp
trigger.
- `0x0008` through `0x000F` high-nibble sweeps did not produce a clean
STANDARD or MASTER lamp trigger.
- `0x0017=0x4000` lit the same far-right bottom BARS lamp/latch as the known
`0x0017=0x8000` family. Later `0x0018` rows in that run were latch-contaminated
and need fresh-boot isolation before assigning a separate meaning.
Current implication: STANDARD and MASTER are probably not simple direct
command-0 high-nibble writes in the `0x0008`-`0x001A` pocket, despite the older
broad run that made MASTER flash. Treat that older observation as a real
bench-visible event but not yet an isolated selector mapping.
Later refinement: the ROM-derived fresh-boot output sweep found a clean
STANDARD trigger at `0x006B = 0x8000` (`00 00 6B 80 00 B1`). It also reclassed
the formerly vague `0x001A` pocket as the MONITOR selector cluster:
| Selector/value | Visible effect |
| --- | --- |
| `0x001A = 0x0808` | MONITOR ENC |
| `0x001A = 0x2020` | MONITOR B |
| `0x001A = 0x4040` | MONITOR G |
| `0x001A = 0x8080` | MONITOR R |
| `0x006B = 0x8000` | STANDARD |
### `panel-atlas-big-visual-sweep-0001-017f-highbits`
The broad webcam sweep and fresh-boot isolation pass produced these confirmed
or near-confirmed panel-output mappings:
| Selector/value | Current meaning |
| --- | --- |
| `0x0013 = 0x4000` | `IRIS/M.BLACK LINK` lamp |
| `0x0024 = 0x8000` | LCD selector-button lamp |
| `0x0024 = 0x0000` | LCD selector-button lamp remained visible at 0.5 s; not a simple clear in this timing window |
| `0x0082 = 0x8000` | IRIS readout `OP` |
| `0x0082 = 0x4000` | IRIS readout `1.4` |
| `0x0082 = 0x0000` | IRIS readout blank |
| `0x0083 = 0x8000` | MASTER GAIN `-3`, SHUTTER `OFF`, and IRIS AUTO |
| `0x0083 = 0x0000` | same visible state remained at 0.5 s; likely latched/copied elsewhere |
| `0x0093 = 0x8000` | BLACK/FLARE MANUAL plus white-balance PRESET |
| `0x0093 = 0x4000` | BLACK/FLARE MANUAL plus white-balance AUTO |
| `0x0093 = 0x2000` | BLACK/FLARE MANUAL plus white-balance MANUAL |
| `0x0093 = 0x0000` | BLACK/FLARE MANUAL plus white-balance MANUAL |
Run health was good: no resync events, no dropped bytes, and command-4
readbacks appeared for the listed writes.
ROM trace now confirms the `0x0013` bit split:
- `E800[0x0013]` is `H'E826`.
- Selector `0x0013` dispatches to `H'2E06`.
- `H'E826.15` sets/clears `F791.6` and `F713.4`, matching SLAVE.
- `H'E826.14` sets/clears `F791.5` and `F716.7`, matching
`IRIS/M.BLACK LINK`.
- Local handler `H'200E` can also toggle `H'E826.14` from panel input
`F006.7/F6DB.7` when its session gate allows it.
Interpretation:
- `0x0082` is the cleanest direct readout lane found so far: set values update
the IRIS display and `0x0000` blanks it.
- `0x0083` appears to be a combined status/display lane rather than only the
MASTER GAIN display. Its `0x8000` state also brings up SHUTTER `OFF` and IRIS
AUTO, and the clear write did not visually clear it at 0.5 s.
- `0x0093` selects white-balance mode. In this run, `0x2000` and `0x0000` both
presented white-balance MANUAL, so bit 13 may be redundant in this context or
may need another gate to show a distinct state.
### `lamp-broad-status-selector-sweep` ### `lamp-broad-status-selector-sweep`
Visible result: Visible result:

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

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

View File

@@ -154,9 +154,21 @@ def label_frame(frame: bytes) -> str:
bytes.fromhex("0000158000CF"): "known_call_button_active_report", bytes.fromhex("0000158000CF"): "known_call_button_active_report",
bytes.fromhex("00001500004F"): "known_call_button_inactive_report", bytes.fromhex("00001500004F"): "known_call_button_inactive_report",
bytes.fromhex("0000078000DD"): "known_cam_power_button_report", bytes.fromhex("0000078000DD"): "known_cam_power_button_report",
bytes.fromhex("000013000049"): "known_iris_mblack_link_clear_report_candidate",
bytes.fromhex("000013400009"): "known_iris_mblack_link_active_report_candidate",
bytes.fromhex("0000138000C9"): "known_selector_0013_bit15_report_candidate",
bytes.fromhex("000013C00089"): "known_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
bytes.fromhex("00010F8000D4"): "known_shutter_onoff_bit7_report_candidate", bytes.fromhex("00010F8000D4"): "known_shutter_onoff_bit7_report_candidate",
bytes.fromhex("00010F200074"): "known_shutter_onoff_bit6_report_candidate", bytes.fromhex("00010F200074"): "known_shutter_onoff_bit6_report_candidate",
bytes.fromhex("00010F000054"): "known_shutter_onoff_clear_report_candidate", bytes.fromhex("00010F000054"): "known_shutter_onoff_clear_report_candidate",
bytes.fromhex("010013000048"): "queued_iris_mblack_link_clear_report_candidate",
bytes.fromhex("02001300004B"): "queued_iris_mblack_link_clear_report_candidate",
bytes.fromhex("010013400008"): "queued_iris_mblack_link_active_report_candidate",
bytes.fromhex("02001340000B"): "queued_iris_mblack_link_active_report_candidate",
bytes.fromhex("0100138000C8"): "queued_selector_0013_bit15_report_candidate",
bytes.fromhex("0200138000CB"): "queued_selector_0013_bit15_report_candidate",
bytes.fromhex("010013C00088"): "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
bytes.fromhex("020013C0008B"): "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
bytes.fromhex("01010F8000D5"): "queued_shutter_onoff_bit7_report_candidate", bytes.fromhex("01010F8000D5"): "queued_shutter_onoff_bit7_report_candidate",
bytes.fromhex("02010F8000D6"): "queued_shutter_onoff_bit7_report_candidate", bytes.fromhex("02010F8000D6"): "queued_shutter_onoff_bit7_report_candidate",
bytes.fromhex("01010F200075"): "queued_shutter_onoff_bit6_report_candidate", bytes.fromhex("01010F200075"): "queued_shutter_onoff_bit6_report_candidate",

View File

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

View File

@@ -105,6 +105,7 @@ INPUT_BYTES = [
KNOWN_REPORTS = { KNOWN_REPORTS = {
0x0007: "CAM POWER", 0x0007: "CAM POWER",
0x0013: "IRIS/M.BLACK LINK",
0x0015: "CALL", 0x0015: "CALL",
} }

349
h8536/panel_selectors.py Normal file
View File

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

View File

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

View File

@@ -270,6 +270,8 @@ def _run_step(ctx: ScenarioContext, action: str, spec: dict[str, Any]) -> None:
_listen(ctx, float(spec.get("seconds", spec.get("value", 0.0)))) _listen(ctx, float(spec.get("seconds", spec.get("value", 0.0))))
elif action == "listen_ack": elif action == "listen_ack":
_step_listen_ack(ctx, spec) _step_listen_ack(ctx, spec)
elif action == "listen_ack_until_quiet":
_step_listen_ack_until_quiet(ctx, spec)
elif action == "send": elif action == "send":
frame = _parse_required_frame(spec.get("frame")) frame = _parse_required_frame(spec.get("frame"))
label = str(spec.get("label", "send")) label = str(spec.get("label", "send"))
@@ -378,7 +380,38 @@ def _step_table_sweep(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
def _step_listen_ack(ctx: ScenarioContext, spec: dict[str, Any]) -> None: def _step_listen_ack(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
seconds = float(spec.get("seconds", spec.get("value", 1.0))) seconds = float(spec.get("seconds", spec.get("value", 1.0)))
ack = _ack_config( ack = _ack_config_from_step(spec)
ack_text = (
f"ack_frame={format_frame(ack['frame'])}"
if ack["ack_mode"] == "fixed"
else f"ack_mode={ack['ack_mode']}"
)
ctx.logger.event(
f"LISTEN_ACK seconds={seconds:.3f} target_mode={ack['target_mode']} targets={len(ack['targets'])} "
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
)
_listen_with_ack(ctx, seconds, None, ack)
def _step_listen_ack_until_quiet(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
seconds = float(spec.get("seconds", spec.get("value", 10.0)))
quiet_seconds = float(spec.get("quiet_seconds", spec.get("quiet", 0.750)))
ack = _ack_config_from_step(spec)
ack_text = (
f"ack_frame={format_frame(ack['frame'])}"
if ack["ack_mode"] == "fixed"
else f"ack_mode={ack['ack_mode']}"
)
ctx.logger.event(
f"LISTEN_ACK_UNTIL_QUIET seconds={seconds:.3f} quiet={quiet_seconds:.3f} "
f"target_mode={ack['target_mode']} targets={len(ack['targets'])} "
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
)
_listen_with_ack(ctx, seconds, None, ack, quiet_seconds=quiet_seconds)
def _ack_config_from_step(spec: dict[str, Any]) -> dict[str, Any]:
return _ack_config(
{ {
"enabled": spec.get("enabled", True), "enabled": spec.get("enabled", True),
"frames": spec.get("frames", spec.get("frame")), "frames": spec.get("frames", spec.get("frame")),
@@ -393,18 +426,9 @@ def _step_listen_ack(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
"ack_mode": spec.get("ack_mode", spec.get("mode", "fixed")), "ack_mode": spec.get("ack_mode", spec.get("mode", "fixed")),
"target_mode": spec.get("target_mode", spec.get("match", "explicit")), "target_mode": spec.get("target_mode", spec.get("match", "explicit")),
"limit_scope": spec.get("limit_scope", spec.get("scope", "local")), "limit_scope": spec.get("limit_scope", spec.get("scope", "local")),
"respond_on": spec.get("respond_on", spec.get("send_on", [])),
} }
) )
ack_text = (
f"ack_frame={format_frame(ack['frame'])}"
if ack["ack_mode"] == "fixed"
else f"ack_mode={ack['ack_mode']}"
)
ctx.logger.event(
f"LISTEN_ACK seconds={seconds:.3f} target_mode={ack['target_mode']} targets={len(ack['targets'])} "
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
)
_listen_with_ack(ctx, seconds, None, ack)
def _ack_config(raw: Any) -> dict[str, Any]: def _ack_config(raw: Any) -> dict[str, Any]:
@@ -438,6 +462,7 @@ def _ack_config(raw: Any) -> dict[str, Any]:
"ack_mode": ack_mode, "ack_mode": ack_mode,
"target_mode": target_mode, "target_mode": target_mode,
"limit_scope": limit_scope, "limit_scope": limit_scope,
"respond_on": _response_rules(spec.get("respond_on", [])),
} }
@@ -458,28 +483,71 @@ def _ack_matches(frame: bytes, ack: dict[str, Any]) -> bool:
return frame[0] in {0x00, 0x01, 0x02} and not (frame[1] & 0x80) return frame[0] in {0x00, 0x01, 0x02} and not (frame[1] & 0x80)
def _response_rules(raw: Any) -> list[dict[str, Any]]:
values = raw if isinstance(raw, list) else ([raw] if raw else [])
rules: list[dict[str, Any]] = []
for index, value in enumerate(values):
if not isinstance(value, dict):
raise SystemExit("respond_on entries must be objects")
targets = _parse_frame_list(value.get("frames", value.get("frame")))
response_frame = _parse_required_frame(value.get("send", value.get("response")))
label = str(value.get("label", f"respond_on_{index + 1}"))
rules.append(
{
"targets": targets,
"frame": response_frame,
"label": label,
"delay": float(value.get("delay", 0.0)),
"listen": float(value.get("listen", 0.0)),
"once": bool(value.get("once", True)),
}
)
return rules
def _listen_with_ack( def _listen_with_ack(
ctx: ScenarioContext, ctx: ScenarioContext,
seconds: float, seconds: float,
selector: int, selector: int | None,
ack: dict[str, Any], ack: dict[str, Any],
*,
quiet_seconds: float | None = None,
) -> list[bytes]: ) -> list[bytes]:
deadline = time.monotonic() + max(0.0, seconds) deadline = time.monotonic() + max(0.0, seconds)
observed: list[bytes] = [] observed: list[bytes] = []
pending: list[bytes] = []
pending_index = 0
last_activity = time.monotonic()
acked_targets: set[bytes] = set() acked_targets: set[bytes] = set()
fired_responses: set[int] = set()
ack_start = ctx.ack_sent ack_start = ctx.ack_sent
target_start = sum(ctx.target_counts.values()) target_start = sum(ctx.target_counts.values())
def enqueue(frames: list[bytes]) -> None:
nonlocal last_activity
if not frames:
return
observed.extend(frames)
pending.extend(frames)
last_activity = time.monotonic()
while time.monotonic() < deadline: while time.monotonic() < deadline:
frames = _read_available(ctx, selector=selector) frames = _read_available(ctx, selector=selector)
observed.extend(frames) enqueue(frames)
if not frames: if not frames and pending_index >= len(pending):
if quiet_seconds is not None and time.monotonic() - last_activity >= quiet_seconds:
ctx.logger.event(f"LISTEN_ACK_QUIET quiet={quiet_seconds:.3f}s")
break
sleep_for = min(max(0.001, ack["poll_interval"]), max(0.0, deadline - time.monotonic())) sleep_for = min(max(0.001, ack["poll_interval"]), max(0.0, deadline - time.monotonic()))
if sleep_for > 0: if sleep_for > 0:
time.sleep(sleep_for) time.sleep(sleep_for)
continue continue
if not ack["enabled"]: if not ack["enabled"]:
pending_index = len(pending)
continue continue
for frame in frames: while pending_index < len(pending):
frame = pending[pending_index]
pending_index += 1
if not _ack_matches(frame, ack): if not _ack_matches(frame, ack):
continue continue
_count_target(ctx, frame) _count_target(ctx, frame)
@@ -493,7 +561,7 @@ def _listen_with_ack(
continue continue
acked_targets.add(frame) acked_targets.add(frame)
if ack["guard"] > 0: if ack["guard"] > 0:
observed.extend(_listen(ctx, ack["guard"], selector=selector)) enqueue(_listen(ctx, ack["guard"], selector=selector))
_send_and_record(ctx, _ack_frame_for_target(frame, ack), "ack", capture=ctx.args.snapshot_acks) _send_and_record(ctx, _ack_frame_for_target(frame, ack), "ack", capture=ctx.args.snapshot_acks)
ctx.ack_sent += 1 ctx.ack_sent += 1
if _ack_limit_reached(ctx, ack, ack_start=ack_start, target_start=target_start): if _ack_limit_reached(ctx, ack, ack_start=ack_start, target_start=target_start):
@@ -501,7 +569,22 @@ def _listen_with_ack(
if ack["abort_on_limit"]: if ack["abort_on_limit"]:
ctx.abort_requested = True ctx.abort_requested = True
if ack["post_read"] > 0: if ack["post_read"] > 0:
observed.extend(_listen(ctx, ack["post_read"], selector=selector)) enqueue(_listen(ctx, ack["post_read"], selector=selector))
for rule_index, rule in enumerate(ack["respond_on"]):
if frame not in rule["targets"]:
continue
if rule["once"] and rule_index in fired_responses:
continue
fired_responses.add(rule_index)
if rule["delay"] > 0:
time.sleep(rule["delay"])
ctx.logger.event(
f"RESPOND_ON target={format_frame(frame)} "
f"send={format_frame(rule['frame'])} label={rule['label']}"
)
_send_and_record(ctx, rule["frame"], rule["label"], capture=ctx.args.snapshot_acks)
if rule["listen"] > 0:
enqueue(_listen(ctx, rule["listen"], selector=selector))
if ctx.abort_requested: if ctx.abort_requested:
return observed return observed
return observed return observed
@@ -753,6 +836,7 @@ def _quiet_console_line(line: str) -> bool:
"NOTE ", "NOTE ",
"SNAPSHOT_SCHEDULE ", "SNAPSHOT_SCHEDULE ",
"SNAPSHOT_ERROR ", "SNAPSHOT_ERROR ",
"RESPOND_ON ",
"Summary", "Summary",
"rx_frames=", "rx_frames=",
"resync_events=", "resync_events=",
@@ -760,6 +844,8 @@ def _quiet_console_line(line: str) -> bool:
"abort_requested=", "abort_requested=",
"known_shutter", "known_shutter",
"queued_shutter", "queued_shutter",
"iris_mblack",
"selector_0013",
) )
return any(fragment in line for fragment in keep_fragments) return any(fragment in line for fragment in keep_fragments)
@@ -772,25 +858,14 @@ def _print_step_dry_run(action: str, spec: dict[str, Any], stdout: TextIO, *, in
print(f"{indent}listen={float(spec.get('listen', 0.0)):.3f}s", file=stdout) print(f"{indent}listen={float(spec.get('listen', 0.0)):.3f}s", file=stdout)
elif action in {"drain", "listen", "wait"}: elif action in {"drain", "listen", "wait"}:
print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 0.0))):.3f}", file=stdout) print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 0.0))):.3f}", file=stdout)
elif action == "listen_ack": elif action in {"listen_ack", "listen_ack_until_quiet"}:
ack = _ack_config( ack = _ack_config_from_step(spec)
{
"enabled": spec.get("enabled", True),
"frames": spec.get("frames", spec.get("frame")),
"ack_frame": spec.get("ack_frame"),
"ack_guard": spec.get("ack_guard", 0.020),
"poll_interval": spec.get("poll_interval", 0.005),
"post_ack_read": spec.get("post_ack_read", 0.250),
"once_per_selector": spec.get("once_per_frame", False),
"max_acks": spec.get("max_acks"),
"max_target_hits": spec.get("max_target_hits"),
"abort_on_limit": spec.get("abort_on_limit", False),
"ack_mode": spec.get("ack_mode", spec.get("mode", "fixed")),
"target_mode": spec.get("target_mode", spec.get("match", "explicit")),
"limit_scope": spec.get("limit_scope", spec.get("scope", "local")),
}
)
print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 1.0))):.3f}", file=stdout) print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 1.0))):.3f}", file=stdout)
if action == "listen_ack_until_quiet":
print(
f"{indent}quiet={float(spec.get('quiet_seconds', spec.get('quiet', 0.750))):.3f}s",
file=stdout,
)
if not ack["enabled"]: if not ack["enabled"]:
print(f"{indent}ack=disabled", file=stdout) print(f"{indent}ack=disabled", file=stdout)
else: else:
@@ -805,6 +880,13 @@ def _print_step_dry_run(action: str, spec: dict[str, Any], stdout: TextIO, *, in
f"max_target_hits={ack['max_target_hits']}", f"max_target_hits={ack['max_target_hits']}",
file=stdout, file=stdout,
) )
for rule in ack["respond_on"]:
print(
f"{indent}respond_on={len(rule['targets'])} "
f"send={format_frame(rule['frame'])} label={rule['label']} "
f"once={int(rule['once'])}",
file=stdout,
)
elif action in {"prompt", "note"}: elif action in {"prompt", "note"}:
message = str(spec.get("message", spec.get("value", "Press Enter to continue."))) message = str(spec.get("message", spec.get("value", "Press Enter to continue.")))
print(f"{indent}message={message}", file=stdout) print(f"{indent}message={message}", file=stdout)

View File

@@ -31,6 +31,18 @@ KNOWN_FRAME_LABELS = {
"00 00 15 80 00 CF": "known_call_button_active_report", "00 00 15 80 00 CF": "known_call_button_active_report",
"00 00 15 00 00 4F": "known_call_button_inactive_report", "00 00 15 00 00 4F": "known_call_button_inactive_report",
"00 00 07 80 00 DD": "known_cam_power_button_report", "00 00 07 80 00 DD": "known_cam_power_button_report",
"00 00 13 00 00 49": "known_iris_mblack_link_clear_report_candidate",
"00 00 13 40 00 09": "known_iris_mblack_link_active_report_candidate",
"00 00 13 80 00 C9": "known_selector_0013_bit15_report_candidate",
"00 00 13 C0 00 89": "known_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
"01 00 13 00 00 48": "queued_iris_mblack_link_clear_report_candidate",
"02 00 13 00 00 4B": "queued_iris_mblack_link_clear_report_candidate",
"01 00 13 40 00 08": "queued_iris_mblack_link_active_report_candidate",
"02 00 13 40 00 0B": "queued_iris_mblack_link_active_report_candidate",
"01 00 13 80 00 C8": "queued_selector_0013_bit15_report_candidate",
"02 00 13 80 00 CB": "queued_selector_0013_bit15_report_candidate",
"01 00 13 C0 00 88": "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
"02 00 13 C0 00 8B": "queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
"01 00 17 80 00 CC": "queued_bars_button_selector_0017_active_candidate", "01 00 17 80 00 CC": "queued_bars_button_selector_0017_active_candidate",
"02 00 17 80 00 CF": "queued_bars_button_selector_0017_active_candidate", "02 00 17 80 00 CF": "queued_bars_button_selector_0017_active_candidate",
"01 00 18 80 00 C3": "queued_bars_button_selector_0018_active_candidate", "01 00 18 80 00 C3": "queued_bars_button_selector_0018_active_candidate",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
{
"name": "panel-atlas-big-hits-isolation-v1",
"notes": [
"Fresh-boot isolation pass for user-reviewed hits from panel-atlas-big-visual-sweep-0001-017f-highbits.",
"Each case power-cycles, seeds CONNECT OK, captures a baseline image, sends the candidate, then captures a clear/low write.",
"Expected observations are from the first broad image review and must be confirmed here."
],
"steps": [
{"action": "note", "message": "Case 1: 0x0013=0x4000, reported Iris/MBlack Link", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case001_baseline_before_0013_4000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case001_candidate_0013_4000_iris_mblack_link", "frame": "00 00 13 40 00 09", "listen": 1.0},
{"action": "send", "label": "case001_clear_0013", "frame": "00 00 13 00 00 49", "listen": 0.7},
{"action": "note", "message": "Case 2: 0x0024=0x8000, reported LCD selector button", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case002_baseline_before_0024_8000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case002_candidate_0024_8000_lcd_selector_button", "frame": "00 00 24 80 00 FE", "listen": 1.0},
{"action": "send", "label": "case002_clear_0024", "frame": "00 00 24 00 00 7E", "listen": 0.7},
{"action": "note", "message": "Case 3: 0x0082=0x8000, reported IRIS display OP", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case003_baseline_before_0082_8000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case003_candidate_0082_8000_iris_op", "frame": "00 01 02 80 00 D9", "listen": 1.0},
{"action": "send", "label": "case003_clear_0082", "frame": "00 01 02 00 00 59", "listen": 0.7},
{"action": "note", "message": "Case 4: 0x0082=0x4000, reported IRIS display 1.4", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case004_baseline_before_0082_4000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case004_candidate_0082_4000_iris_1_4", "frame": "00 01 02 40 00 19", "listen": 1.0},
{"action": "send", "label": "case004_clear_0082", "frame": "00 01 02 00 00 59", "listen": 0.7},
{"action": "note", "message": "Case 5: 0x0083=0x8000, reported MASTER GAIN display -3", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case005_baseline_before_0083_8000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case005_candidate_0083_8000_master_gain_minus_3", "frame": "00 01 03 80 00 D8", "listen": 1.0},
{"action": "send", "label": "case005_clear_0083", "frame": "00 01 03 00 00 58", "listen": 0.7},
{"action": "note", "message": "Case 6: 0x0093=0x8000, reported white balance PRESET", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case006_baseline_before_0093_8000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case006_candidate_0093_8000_white_balance_preset", "frame": "00 01 13 80 00 C8", "listen": 1.0},
{"action": "send", "label": "case006_clear_0093", "frame": "00 01 13 00 00 48", "listen": 0.7},
{"action": "note", "message": "Case 7: 0x0093=0x4000, reported white balance AUTO", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case007_baseline_before_0093_4000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case007_candidate_0093_4000_white_balance_auto", "frame": "00 01 13 40 00 08", "listen": 1.0},
{"action": "send", "label": "case007_clear_0093", "frame": "00 01 13 00 00 48", "listen": 0.7},
{"action": "note", "message": "Case 8: 0x0093=0x2000, reported white balance MANUAL", "banner": true},
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.5},
{"action": "send", "label": "case008_baseline_before_0093_2000", "frame": "00 00 00 80 00 DA", "listen": 0.7},
{"action": "send", "label": "case008_candidate_0093_2000_white_balance_manual", "frame": "00 01 13 20 00 68", "listen": 1.0},
{"action": "send", "label": "case008_clear_0093", "frame": "00 01 13 00 00 48", "listen": 0.7},
{"action": "listen", "seconds": 1.0}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
{
"name": "panel-atlas-operator-lamps-v1",
"notes": [
"Compact visible-output atlas for operator lamps and tally-adjacent selectors.",
"Use webcam snapshots to tie each selector/value write to the panel state.",
"This intentionally stays on command-0 primary-table writes and avoids COPY/menu latch selectors."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "selector_zero_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "cam_power_candidate_0007_high",
"frame": "00 00 07 80 00 DD",
"listen": 0.90
},
{
"action": "send",
"label": "cam_power_candidate_0007_low",
"frame": "00 00 07 00 00 5D",
"listen": 0.50
},
{
"action": "send",
"label": "call_candidate_0015_high",
"frame": "00 00 15 80 00 CF",
"listen": 0.90
},
{
"action": "send",
"label": "call_candidate_0015_low",
"frame": "00 00 15 00 00 4F",
"listen": 0.50
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_neighbors",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "send",
"label": "neighbor_0012_high",
"frame": "00 00 12 80 00 C8",
"listen": 0.90
},
{
"action": "send",
"label": "neighbor_0012_low",
"frame": "00 00 12 00 00 48",
"listen": 0.45
},
{
"action": "send",
"label": "neighbor_0013_high",
"frame": "00 00 13 80 00 C9",
"listen": 0.90
},
{
"action": "send",
"label": "neighbor_0013_low",
"frame": "00 00 13 00 00 49",
"listen": 0.45
},
{
"action": "send",
"label": "neighbor_0016_high",
"frame": "00 00 16 80 00 CC",
"listen": 0.90
},
{
"action": "send",
"label": "neighbor_0016_low",
"frame": "00 00 16 00 00 4C",
"listen": 0.45
},
{
"action": "send",
"label": "neighbor_0017_high",
"frame": "00 00 17 80 00 CD",
"listen": 0.90
},
{
"action": "send",
"label": "neighbor_0017_low",
"frame": "00 00 17 00 00 4D",
"listen": 0.45
},
{
"action": "send",
"label": "neighbor_0018_high",
"frame": "00 00 18 80 00 C2",
"listen": 0.90
},
{
"action": "send",
"label": "neighbor_0018_low",
"frame": "00 00 18 00 00 42",
"listen": 0.45
},
{
"action": "send",
"label": "neighbor_001a_high",
"frame": "00 00 1A 80 00 C0",
"listen": 0.90
},
{
"action": "send",
"label": "neighbor_001a_low",
"frame": "00 00 1A 00 00 40",
"listen": 0.45
},
{
"action": "listen",
"seconds": 0.80
}
]
}

View File

@@ -0,0 +1,124 @@
{
"name": "panel-atlas-readout-status-v1",
"notes": [
"Compact visible-output atlas for readouts and status clusters.",
"Targets known shutter display, white-balance/black-flare, and KNEE candidate selectors.",
"Use webcam snapshots to tie each selector/value write to visible LCD, lamp, and seven-segment states."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "selector_zero_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "shutter_008f_evs_bit11",
"frame": "00 01 0F 08 00 5C",
"listen": 0.90
},
{
"action": "send",
"label": "shutter_008f_off_bit12",
"frame": "00 01 0F 10 00 44",
"listen": 0.90
},
{
"action": "send",
"label": "shutter_008f_clear",
"frame": "00 01 0F 00 00 54",
"listen": 0.50
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_0093",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "send",
"label": "status_0093_bit15_wb_preset_black_manual",
"frame": "00 01 13 80 00 C8",
"listen": 0.90
},
{
"action": "send",
"label": "status_0093_9020_black_manual_context",
"frame": "00 01 13 90 20 F8",
"listen": 0.90
},
{
"action": "send",
"label": "status_0093_90ff_black_auto_candidate",
"frame": "00 01 13 90 FF 27",
"listen": 0.90
},
{
"action": "send",
"label": "status_0093_clear",
"frame": "00 01 13 00 00 48",
"listen": 0.50
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_knee",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "send",
"label": "knee_00b9_bit13_report_gate",
"frame": "00 01 39 20 00 42",
"listen": 0.90
},
{
"action": "send",
"label": "knee_00b9_bits15_13_label_gate",
"frame": "00 01 39 A0 00 C2",
"listen": 0.90
},
{
"action": "send",
"label": "knee_0110_bit15_auto_or_dl_candidate",
"frame": "00 01 90 80 00 4B",
"listen": 1.10
},
{
"action": "send",
"label": "knee_0110_clear",
"frame": "00 01 90 00 00 CB",
"listen": 0.70
},
{
"action": "send",
"label": "knee_00b9_clear",
"frame": "00 01 39 00 00 62",
"listen": 0.70
},
{
"action": "listen",
"seconds": 0.80
}
]
}

View File

@@ -0,0 +1,124 @@
{
"name": "panel-atlas-right-stack-fresh-latch-v1",
"notes": [
"Fresh-boot isolation for right-side status selectors that looked latched in the compact run.",
"Each selector gets its own power cycle before high/low testing so 0x0017 cannot contaminate 0x001A.",
"Use webcam snapshots to compare CONNECT OK baseline, high write, and low write."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "case_0017_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_0017_ok_seed_2_baseline",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_0017_high_bars_family",
"frame": "00 00 17 80 00 CD",
"listen": 0.90
},
{
"action": "send",
"label": "case_0017_low_clear_attempt",
"frame": "00 00 17 00 00 4D",
"listen": 1.00
},
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "case_001a_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_001a_ok_seed_2_baseline",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_001a_high_bars_family",
"frame": "00 00 1A 80 00 C0",
"listen": 0.90
},
{
"action": "send",
"label": "case_001a_low_clear_attempt",
"frame": "00 00 1A 00 00 40",
"listen": 1.00
},
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "case_0007_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_0007_ok_seed_2_baseline",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "case_0007_high_cam_power",
"frame": "00 00 07 80 00 DD",
"listen": 0.90
},
{
"action": "send",
"label": "case_0007_low_clear_attempt",
"frame": "00 00 07 00 00 5D",
"listen": 1.00
}
]
}

View File

@@ -0,0 +1,154 @@
{
"name": "panel-atlas-right-stack-isolation-v1",
"notes": [
"Tight right-side lamp isolation for CAM POWER/CALL/BARS/status candidates.",
"Each candidate is held high then low twice so webcam frames can be compared against the same baseline.",
"Watch the right-side CAM POWER/BARS/status stack and the CALL/CAM POWER lamps; CONNECT OK traffic is kept alive throughout."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.25
},
{
"action": "send",
"label": "selector_zero_ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "send",
"label": "selector_zero_ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.60
},
{
"action": "repeat",
"count": 2,
"steps": [
{
"action": "send",
"label": "candidate_0007_high_cam_power",
"frame": "00 00 07 80 00 DD",
"listen": 0.85
},
{
"action": "send",
"label": "candidate_0007_low_cam_power",
"frame": "00 00 07 00 00 5D",
"listen": 0.65
}
]
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_0015",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "repeat",
"count": 2,
"steps": [
{
"action": "send",
"label": "candidate_0015_high_call",
"frame": "00 00 15 80 00 CF",
"listen": 0.85
},
{
"action": "send",
"label": "candidate_0015_low_call",
"frame": "00 00 15 00 00 4F",
"listen": 0.65
}
]
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_0013",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "repeat",
"count": 2,
"steps": [
{
"action": "send",
"label": "candidate_0013_high_orange_status",
"frame": "00 00 13 80 00 C9",
"listen": 0.85
},
{
"action": "send",
"label": "candidate_0013_low_orange_status",
"frame": "00 00 13 00 00 49",
"listen": 0.65
}
]
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_0017",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "repeat",
"count": 2,
"steps": [
{
"action": "send",
"label": "candidate_0017_high_bars_family",
"frame": "00 00 17 80 00 CD",
"listen": 0.85
},
{
"action": "send",
"label": "candidate_0017_low_bars_family",
"frame": "00 00 17 00 00 4D",
"listen": 0.65
}
]
},
{
"action": "send",
"label": "selector_zero_ok_refresh_before_001a",
"frame": "00 00 00 80 00 DA",
"listen": 0.40
},
{
"action": "repeat",
"count": 2,
"steps": [
{
"action": "send",
"label": "candidate_001a_high_bars_family",
"frame": "00 00 1A 80 00 C0",
"listen": 0.85
},
{
"action": "send",
"label": "candidate_001a_low_bars_family",
"frame": "00 00 1A 00 00 40",
"listen": 0.65
}
]
},
{
"action": "listen",
"seconds": 0.80
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
{
"name": "panel-atlas-standard-master-bit-sweep-v1",
"notes": [
"Targeted bench sweep for the far-right STANDARD and MASTER lamps.",
"Uses CONNECT OK seed frames, a known SLAVE positive control, then bit sweeps around adjacent selector words.",
"Watch far-right stack top to bottom: tally, STANDARD, MASTER, SLAVE, CAM POWER, BARS."
],
"steps": [
{
"action": "power_cycle",
"off_seconds": 1.5
},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": 10.0,
"require": true
},
{
"action": "drain",
"seconds": 0.8
},
{
"action": "send",
"label": "ok_seed_1",
"frame": "00 00 00 80 00 DA",
"listen": 0.6
},
{
"action": "send",
"label": "ok_seed_2",
"frame": "00 00 00 80 00 DA",
"listen": 0.6
},
{
"action": "send",
"label": "positive_control_0013_8000_slave_on",
"frame": "00 00 13 80 00 C9",
"listen": 0.8
},
{
"action": "send",
"label": "clear_0013_after_slave",
"frame": "00 00 13 00 00 49",
"listen": 0.5
},
{
"action": "repeat",
"count": 1,
"steps": [
{"action": "send", "label": "candidate_0013_4000", "frame": "00 00 13 40 00 09", "listen": 0.75},
{"action": "send", "label": "clear_0013_after_4000", "frame": "00 00 13 00 00 49", "listen": 0.45},
{"action": "send", "label": "candidate_0013_2000", "frame": "00 00 13 20 00 69", "listen": 0.75},
{"action": "send", "label": "clear_0013_after_2000", "frame": "00 00 13 00 00 49", "listen": 0.45},
{"action": "send", "label": "candidate_0013_1000", "frame": "00 00 13 10 00 59", "listen": 0.75},
{"action": "send", "label": "clear_0013_after_1000", "frame": "00 00 13 00 00 49", "listen": 0.45},
{"action": "send", "label": "candidate_0013_0800", "frame": "00 00 13 08 00 41", "listen": 0.75},
{"action": "send", "label": "clear_0013_after_0800", "frame": "00 00 13 00 00 49", "listen": 0.45}
]
},
{
"action": "send",
"label": "ok_refresh_before_0012",
"frame": "00 00 00 80 00 DA",
"listen": 0.6
},
{
"action": "repeat",
"count": 1,
"steps": [
{"action": "send", "label": "candidate_0012_4000", "frame": "00 00 12 40 00 08", "listen": 0.75},
{"action": "send", "label": "clear_0012_after_4000", "frame": "00 00 12 00 00 48", "listen": 0.45},
{"action": "send", "label": "candidate_0012_2000", "frame": "00 00 12 20 00 68", "listen": 0.75},
{"action": "send", "label": "clear_0012_after_2000", "frame": "00 00 12 00 00 48", "listen": 0.45},
{"action": "send", "label": "candidate_0012_1000", "frame": "00 00 12 10 00 58", "listen": 0.75},
{"action": "send", "label": "clear_0012_after_1000", "frame": "00 00 12 00 00 48", "listen": 0.45},
{"action": "send", "label": "candidate_0012_0800", "frame": "00 00 12 08 00 40", "listen": 0.75},
{"action": "send", "label": "clear_0012_after_0800", "frame": "00 00 12 00 00 48", "listen": 0.45},
{"action": "send", "label": "candidate_0012_8000", "frame": "00 00 12 80 00 C8", "listen": 0.75},
{"action": "send", "label": "clear_0012_after_8000", "frame": "00 00 12 00 00 48", "listen": 0.45}
]
},
{
"action": "send",
"label": "ok_refresh_before_0014",
"frame": "00 00 00 80 00 DA",
"listen": 0.6
},
{
"action": "repeat",
"count": 1,
"steps": [
{"action": "send", "label": "candidate_0014_4000", "frame": "00 00 14 40 00 0E", "listen": 0.75},
{"action": "send", "label": "clear_0014_after_4000", "frame": "00 00 14 00 00 4E", "listen": 0.45},
{"action": "send", "label": "candidate_0014_2000", "frame": "00 00 14 20 00 6E", "listen": 0.75},
{"action": "send", "label": "clear_0014_after_2000", "frame": "00 00 14 00 00 4E", "listen": 0.45},
{"action": "send", "label": "candidate_0014_1000", "frame": "00 00 14 10 00 5E", "listen": 0.75},
{"action": "send", "label": "clear_0014_after_1000", "frame": "00 00 14 00 00 4E", "listen": 0.45},
{"action": "send", "label": "candidate_0014_0800", "frame": "00 00 14 08 00 46", "listen": 0.75},
{"action": "send", "label": "clear_0014_after_0800", "frame": "00 00 14 00 00 4E", "listen": 0.45},
{"action": "send", "label": "candidate_0014_8000", "frame": "00 00 14 80 00 CE", "listen": 0.75},
{"action": "send", "label": "clear_0014_after_8000", "frame": "00 00 14 00 00 4E", "listen": 0.45}
]
},
{
"action": "listen",
"seconds": 1.0
}
]
}

View File

@@ -0,0 +1,70 @@
{
"name": "panel-atlas-standard-master-lower-neighbor-sweep-v3",
"notes": [
"Third STANDARD/MASTER hunt over lower neighbor selectors 0x0008-0x000F.",
"This tests whether the far-right STANDARD/MASTER lamps live closer to the CAM POWER/CALL selector pocket than the SLAVE/BARS pocket.",
"Watch far-right stack top to bottom: tally, STANDARD, MASTER, SLAVE, CAM POWER, BARS."
],
"steps": [
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.8},
{"action": "send", "label": "ok_seed_1", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "ok_seed_2", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "positive_control_0013_8000_slave_on", "frame": "00 00 13 80 00 C9", "listen": 0.8},
{"action": "send", "label": "clear_0013_after_slave", "frame": "00 00 13 00 00 49", "listen": 0.6},
{"action": "send", "label": "candidate_0008_8000", "frame": "00 00 08 80 00 D2", "listen": 0.6},
{"action": "send", "label": "candidate_0008_4000", "frame": "00 00 08 40 00 12", "listen": 0.6},
{"action": "send", "label": "candidate_0008_2000", "frame": "00 00 08 20 00 72", "listen": 0.6},
{"action": "send", "label": "candidate_0008_1000", "frame": "00 00 08 10 00 42", "listen": 0.6},
{"action": "send", "label": "clear_0008", "frame": "00 00 08 00 00 52", "listen": 0.45},
{"action": "send", "label": "candidate_0009_8000", "frame": "00 00 09 80 00 D3", "listen": 0.6},
{"action": "send", "label": "candidate_0009_4000", "frame": "00 00 09 40 00 13", "listen": 0.6},
{"action": "send", "label": "candidate_0009_2000", "frame": "00 00 09 20 00 73", "listen": 0.6},
{"action": "send", "label": "candidate_0009_1000", "frame": "00 00 09 10 00 43", "listen": 0.6},
{"action": "send", "label": "clear_0009", "frame": "00 00 09 00 00 53", "listen": 0.45},
{"action": "send", "label": "ok_refresh_before_000A", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "candidate_000A_8000", "frame": "00 00 0A 80 00 D0", "listen": 0.6},
{"action": "send", "label": "candidate_000A_4000", "frame": "00 00 0A 40 00 10", "listen": 0.6},
{"action": "send", "label": "candidate_000A_2000", "frame": "00 00 0A 20 00 70", "listen": 0.6},
{"action": "send", "label": "candidate_000A_1000", "frame": "00 00 0A 10 00 40", "listen": 0.6},
{"action": "send", "label": "clear_000A", "frame": "00 00 0A 00 00 50", "listen": 0.45},
{"action": "send", "label": "candidate_000B_8000", "frame": "00 00 0B 80 00 D1", "listen": 0.6},
{"action": "send", "label": "candidate_000B_4000", "frame": "00 00 0B 40 00 11", "listen": 0.6},
{"action": "send", "label": "candidate_000B_2000", "frame": "00 00 0B 20 00 71", "listen": 0.6},
{"action": "send", "label": "candidate_000B_1000", "frame": "00 00 0B 10 00 41", "listen": 0.6},
{"action": "send", "label": "clear_000B", "frame": "00 00 0B 00 00 51", "listen": 0.45},
{"action": "send", "label": "ok_refresh_before_000C", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "candidate_000C_8000", "frame": "00 00 0C 80 00 D6", "listen": 0.6},
{"action": "send", "label": "candidate_000C_4000", "frame": "00 00 0C 40 00 16", "listen": 0.6},
{"action": "send", "label": "candidate_000C_2000", "frame": "00 00 0C 20 00 76", "listen": 0.6},
{"action": "send", "label": "candidate_000C_1000", "frame": "00 00 0C 10 00 46", "listen": 0.6},
{"action": "send", "label": "clear_000C", "frame": "00 00 0C 00 00 56", "listen": 0.45},
{"action": "send", "label": "candidate_000D_8000", "frame": "00 00 0D 80 00 D7", "listen": 0.6},
{"action": "send", "label": "candidate_000D_4000", "frame": "00 00 0D 40 00 17", "listen": 0.6},
{"action": "send", "label": "candidate_000D_2000", "frame": "00 00 0D 20 00 77", "listen": 0.6},
{"action": "send", "label": "candidate_000D_1000", "frame": "00 00 0D 10 00 47", "listen": 0.6},
{"action": "send", "label": "clear_000D", "frame": "00 00 0D 00 00 57", "listen": 0.45},
{"action": "send", "label": "ok_refresh_before_000E", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "candidate_000E_8000", "frame": "00 00 0E 80 00 D4", "listen": 0.6},
{"action": "send", "label": "candidate_000E_4000", "frame": "00 00 0E 40 00 14", "listen": 0.6},
{"action": "send", "label": "candidate_000E_2000", "frame": "00 00 0E 20 00 74", "listen": 0.6},
{"action": "send", "label": "candidate_000E_1000", "frame": "00 00 0E 10 00 44", "listen": 0.6},
{"action": "send", "label": "clear_000E", "frame": "00 00 0E 00 00 54", "listen": 0.45},
{"action": "send", "label": "candidate_000F_8000", "frame": "00 00 0F 80 00 D5", "listen": 0.6},
{"action": "send", "label": "candidate_000F_4000", "frame": "00 00 0F 40 00 15", "listen": 0.6},
{"action": "send", "label": "candidate_000F_2000", "frame": "00 00 0F 20 00 75", "listen": 0.6},
{"action": "send", "label": "candidate_000F_1000", "frame": "00 00 0F 10 00 45", "listen": 0.6},
{"action": "send", "label": "clear_000F", "frame": "00 00 0F 00 00 55", "listen": 0.45},
{"action": "listen", "seconds": 1.0}
]
}

View File

@@ -0,0 +1,70 @@
{
"name": "panel-atlas-standard-master-neighbor-sweep-v2",
"notes": [
"Second targeted STANDARD/MASTER hunt after 0x0012/0x0013/0x0014 high-bit pass did not show them.",
"Sweeps neighboring selector words with high-nibble values; these are the values most similar to confirmed lamp controls.",
"Watch far-right stack top to bottom: tally, STANDARD, MASTER, SLAVE, CAM POWER, BARS."
],
"steps": [
{"action": "power_cycle", "off_seconds": 1.5},
{"action": "wait_ready", "heartbeats": 2, "timeout": 10.0, "require": true},
{"action": "drain", "seconds": 0.8},
{"action": "send", "label": "ok_seed_1", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "ok_seed_2", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "positive_control_0013_8000_slave_on", "frame": "00 00 13 80 00 C9", "listen": 0.8},
{"action": "send", "label": "clear_0013_after_slave", "frame": "00 00 13 00 00 49", "listen": 0.6},
{"action": "send", "label": "candidate_0010_8000", "frame": "00 00 10 80 00 CA", "listen": 0.6},
{"action": "send", "label": "candidate_0010_4000", "frame": "00 00 10 40 00 0A", "listen": 0.6},
{"action": "send", "label": "candidate_0010_2000", "frame": "00 00 10 20 00 6A", "listen": 0.6},
{"action": "send", "label": "candidate_0010_1000", "frame": "00 00 10 10 00 5A", "listen": 0.6},
{"action": "send", "label": "clear_0010", "frame": "00 00 10 00 00 4A", "listen": 0.45},
{"action": "send", "label": "candidate_0011_8000", "frame": "00 00 11 80 00 CB", "listen": 0.6},
{"action": "send", "label": "candidate_0011_4000", "frame": "00 00 11 40 00 0B", "listen": 0.6},
{"action": "send", "label": "candidate_0011_2000", "frame": "00 00 11 20 00 6B", "listen": 0.6},
{"action": "send", "label": "candidate_0011_1000", "frame": "00 00 11 10 00 5B", "listen": 0.6},
{"action": "send", "label": "clear_0011", "frame": "00 00 11 00 00 4B", "listen": 0.45},
{"action": "send", "label": "ok_refresh_before_0015", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "candidate_0015_4000", "frame": "00 00 15 40 00 0F", "listen": 0.6},
{"action": "send", "label": "candidate_0015_2000", "frame": "00 00 15 20 00 6F", "listen": 0.6},
{"action": "send", "label": "candidate_0015_1000", "frame": "00 00 15 10 00 5F", "listen": 0.6},
{"action": "send", "label": "candidate_0015_0800", "frame": "00 00 15 08 00 47", "listen": 0.6},
{"action": "send", "label": "clear_0015", "frame": "00 00 15 00 00 4F", "listen": 0.45},
{"action": "send", "label": "candidate_0016_8000", "frame": "00 00 16 80 00 CC", "listen": 0.6},
{"action": "send", "label": "candidate_0016_4000", "frame": "00 00 16 40 00 0C", "listen": 0.6},
{"action": "send", "label": "candidate_0016_2000", "frame": "00 00 16 20 00 6C", "listen": 0.6},
{"action": "send", "label": "candidate_0016_1000", "frame": "00 00 16 10 00 5C", "listen": 0.6},
{"action": "send", "label": "clear_0016", "frame": "00 00 16 00 00 4C", "listen": 0.45},
{"action": "send", "label": "ok_refresh_before_0017", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "candidate_0017_4000", "frame": "00 00 17 40 00 0D", "listen": 0.6},
{"action": "send", "label": "candidate_0017_2000", "frame": "00 00 17 20 00 6D", "listen": 0.6},
{"action": "send", "label": "candidate_0017_1000", "frame": "00 00 17 10 00 5D", "listen": 0.6},
{"action": "send", "label": "candidate_0017_0800", "frame": "00 00 17 08 00 45", "listen": 0.6},
{"action": "send", "label": "clear_0017", "frame": "00 00 17 00 00 4D", "listen": 0.45},
{"action": "send", "label": "candidate_0018_8000", "frame": "00 00 18 80 00 C2", "listen": 0.6},
{"action": "send", "label": "candidate_0018_4000", "frame": "00 00 18 40 00 02", "listen": 0.6},
{"action": "send", "label": "candidate_0018_2000", "frame": "00 00 18 20 00 62", "listen": 0.6},
{"action": "send", "label": "candidate_0018_1000", "frame": "00 00 18 10 00 52", "listen": 0.6},
{"action": "send", "label": "clear_0018", "frame": "00 00 18 00 00 42", "listen": 0.45},
{"action": "send", "label": "ok_refresh_before_0019", "frame": "00 00 00 80 00 DA", "listen": 0.6},
{"action": "send", "label": "candidate_0019_8000", "frame": "00 00 19 80 00 C3", "listen": 0.6},
{"action": "send", "label": "candidate_0019_4000", "frame": "00 00 19 40 00 03", "listen": 0.6},
{"action": "send", "label": "candidate_0019_2000", "frame": "00 00 19 20 00 63", "listen": 0.6},
{"action": "send", "label": "candidate_0019_1000", "frame": "00 00 19 10 00 53", "listen": 0.6},
{"action": "send", "label": "clear_0019", "frame": "00 00 19 00 00 43", "listen": 0.45},
{"action": "send", "label": "candidate_001A_8000", "frame": "00 00 1A 80 00 C0", "listen": 0.6},
{"action": "send", "label": "candidate_001A_4000", "frame": "00 00 1A 40 00 00", "listen": 0.6},
{"action": "send", "label": "candidate_001A_2000", "frame": "00 00 1A 20 00 60", "listen": 0.6},
{"action": "send", "label": "candidate_001A_1000", "frame": "00 00 1A 10 00 50", "listen": 0.6},
{"action": "send", "label": "clear_001A", "frame": "00 00 1A 00 00 40", "listen": 0.45},
{"action": "listen", "seconds": 1.0}
]
}

View File

@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Build a JSON serial_scenario for broad visible panel sweeps."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Iterable
CHECKSUM_SEED = 0x5A
CONNECT_OK_FRAME = "00 00 00 80 00 DA"
SLAVE_POSITIVE_CONTROL = "00 00 13 80 00 C9"
SLAVE_CLEAR = "00 00 13 00 00 49"
def main() -> int:
args = build_arg_parser().parse_args()
values = parse_int_list(args.values)
skips = set(parse_int_list(args.skip)) if args.skip else set()
selectors = [
selector
for selector in range(parse_int(args.start), parse_int(args.end) + 1)
if selector not in skips and (args.include_selector_zero or selector != 0)
]
if not selectors:
raise SystemExit("selector range is empty after skips")
scenario = build_scenario(args, selectors, values)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8")
candidate_images = len(selectors) * len(values)
windows = window_count(len(selectors), args.power_cycle_every)
print(f"wrote {args.output}")
print(f"selectors={len(selectors)} values={len(values)} candidate_snapshots={candidate_images}")
print(f"power_cycle_windows={windows} estimated_candidate_time={candidate_images * args.listen / 60:.1f}min")
return 0
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Generate a serial_scenario JSON file for a broad panel-output webcam sweep."
)
parser.add_argument("output", type=Path, help="scenario JSON path to write")
parser.add_argument("--start", default="0x0001", help="first logical selector, inclusive")
parser.add_argument("--end", default="0x017F", help="last logical selector, inclusive")
parser.add_argument(
"--values",
default="0x8000,0x4000,0x2000,0x1000,0x0800",
help="comma-separated 16-bit values to try for each selector",
)
parser.add_argument(
"--skip",
default="",
help="comma-separated logical selectors to skip, e.g. 0x006C,0x006D",
)
parser.add_argument(
"--include-selector-zero",
action="store_true",
help="include selector 0 in the sweep; omitted by default because it controls CONNECT OK state",
)
parser.add_argument("--listen", type=float, default=0.65, help="seconds to listen after each candidate send")
parser.add_argument("--clear-listen", type=float, default=0.15, help="seconds after selector clear writes")
parser.add_argument("--ok-listen", type=float, default=0.30, help="seconds after CONNECT OK refresh writes")
parser.add_argument(
"--ok-every",
type=int,
default=8,
help="refresh CONNECT OK every N selectors; use 0 to disable periodic refresh",
)
parser.add_argument(
"--power-cycle-every",
type=int,
default=32,
help="power-cycle every N selectors to limit latch contamination; use 0 for one long session",
)
parser.add_argument("--off-seconds", type=float, default=1.5, help="relay power-off time per window")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="wait_ready timeout")
parser.add_argument("--drain", type=float, default=0.5, help="seconds to drain after ready")
parser.add_argument("--final-listen", type=float, default=1.0, help="seconds to listen at the end")
return parser
def build_scenario(args: argparse.Namespace, selectors: list[int], values: list[int]) -> dict[str, object]:
start = selectors[0]
end = selectors[-1]
value_text = ",".join(f"0x{value:04X}" for value in values)
scenario: dict[str, object] = {
"name": f"panel-atlas-big-visual-sweep-{start:03X}-{end:03X}",
"notes": [
"Broad visible-output sweep generated by scripts/build_panel_visual_sweep.py.",
"Candidate selector/value sends have webcam snapshots enabled; CONNECT OK refreshes and clears do not.",
"Use image filenames candidate_XXXX_YYYY to refine any visible trigger into a smaller follow-up scenario.",
f"Values: {value_text}",
],
"steps": [],
}
steps: list[dict[str, object]] = scenario["steps"] # type: ignore[assignment]
for window_index, window in enumerate(selector_windows(selectors, args.power_cycle_every), start=1):
add_session_prelude(args, steps, window_index)
if window_index == 1:
steps.append(send_step("positive_control_0013_8000_slave_on", SLAVE_POSITIVE_CONTROL, args.listen))
steps.append(send_step("clear_0013_after_slave", SLAVE_CLEAR, args.clear_listen, snapshot=False))
for selector_index, selector in enumerate(window, start=1):
if args.ok_every > 0 and (selector_index - 1) % args.ok_every == 0:
steps.append(
send_step(
f"ok_refresh_before_{selector:04X}",
CONNECT_OK_FRAME,
args.ok_listen,
snapshot=False,
)
)
for value in values:
steps.append(
send_step(
f"candidate_{selector:04X}_{value:04X}",
frame_hex(0x00, selector, value),
args.listen,
)
)
steps.append(
send_step(
f"clear_{selector:04X}",
frame_hex(0x00, selector, 0),
args.clear_listen,
snapshot=False,
)
)
steps.append({"action": "listen", "seconds": args.final_listen})
return scenario
def add_session_prelude(args: argparse.Namespace, steps: list[dict[str, object]], window_index: int) -> None:
steps.extend(
[
{"action": "power_cycle", "off_seconds": args.off_seconds},
{
"action": "wait_ready",
"heartbeats": 2,
"timeout": args.ready_timeout,
"require": True,
},
{"action": "drain", "seconds": args.drain},
send_step(f"window_{window_index:03d}_ok_seed_1", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
send_step(f"window_{window_index:03d}_ok_seed_2", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
]
)
def send_step(label: str, frame: str, listen: float, *, snapshot: bool = True) -> dict[str, object]:
step: dict[str, object] = {
"action": "send",
"label": label,
"frame": frame,
"listen": listen,
}
if not snapshot:
step["snapshot"] = False
return step
def selector_windows(selectors: list[int], size: int) -> Iterable[list[int]]:
if size <= 0:
yield selectors
return
for index in range(0, len(selectors), size):
yield selectors[index : index + size]
def window_count(selector_count: int, size: int) -> int:
if size <= 0:
return 1
return (selector_count + size - 1) // size
def frame_hex(command: int, selector: int, value: int) -> str:
selector_hi, selector_lo = selector_bytes(selector)
data = bytes(
[
command & 0xFF,
selector_hi,
selector_lo,
(value >> 8) & 0xFF,
value & 0xFF,
]
)
return " ".join(f"{byte:02X}" for byte in data + bytes([frame_checksum(data)]))
def selector_bytes(selector: int) -> tuple[int, int]:
selector &= 0x01FF
if selector <= 0x007F:
return 0x00, selector
if selector <= 0x017F:
return 0x01, selector - 0x0080
return 0x02, selector - 0x0180
def frame_checksum(data: bytes) -> int:
checksum = CHECKSUM_SEED
for value in data[:5]:
checksum ^= value
return checksum & 0xFF
def parse_int_list(text: str) -> list[int]:
values = []
for part in text.split(","):
item = part.strip()
if item:
values.append(parse_int(item))
return values
def parse_int(text: str) -> int:
return int(str(text).strip(), 0)
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Build a fresh-boot webcam sweep for ROM-derived panel-output candidates."""
from __future__ import annotations
import argparse
import json
from dataclasses import dataclass
from pathlib import Path
CHECKSUM_SEED = 0x5A
CONNECT_OK_FRAME = "00 00 00 80 00 DA"
@dataclass(frozen=True)
class Candidate:
label: str
selector: int
value: int
note: str
CANDIDATES: tuple[Candidate, ...] = (
Candidate("positive_0013_4000_iris_mblack_link", 0x0013, 0x4000, "known IRIS/M.BLACK LINK lamp positive control"),
Candidate("positive_0013_8000_slave", 0x0013, 0x8000, "known SLAVE lamp positive control"),
Candidate("positive_0015_8000_call", 0x0015, 0x8000, "known CALL lamp positive control"),
Candidate("positive_0017_8000_bars", 0x0017, 0x8000, "known BARS lamp positive control"),
Candidate("positive_0110_8000_knee_auto", 0x0110, 0x8000, "known KNEE AUTO positive control"),
Candidate("rom_001a_0808_multi_button_default", 0x001A, 0x0808, "F6D3 group default/fallback value"),
Candidate("rom_001a_2020_f6d3_bit3_family", 0x001A, 0x2020, "F6D3.3-style packed state candidate"),
Candidate("rom_001a_4040_f6d3_bit4_family", 0x001A, 0x4040, "F6D3.4-style packed state candidate"),
Candidate("rom_001a_8080_f6d3_bit5_family", 0x001A, 0x8080, "F6D3.5-style packed state candidate"),
Candidate("rom_006b_8000_f6d4_bit6_candidate", 0x006B, 0x8000, "F6D4.6 handler report value"),
Candidate("rom_0083_0004_f6d0_step_candidate", 0x0083, 0x0004, "F6D0.1 lower-step value candidate"),
Candidate("rom_0083_4000_high_tag_candidate", 0x0083, 0x4000, "0x0083 high-bit/tag candidate"),
Candidate("rom_0083_2000_high_tag_candidate", 0x0083, 0x2000, "0x0083 high-bit/tag candidate"),
Candidate("rom_008f_8000_f6d0_bit7_local", 0x008F, 0x8000, "F6D0.7 local SHUTTER/OTHERS report bit"),
Candidate("rom_008f_2000_f6d0_bit6_local", 0x008F, 0x2000, "F6D0.6 local SHUTTER/OTHERS report bit"),
Candidate("known_008f_0800_evs_display", 0x008F, 0x0800, "known EVS/shutter display positive control"),
Candidate("known_008f_1000_off_display", 0x008F, 0x1000, "known OFF/shutter display positive control"),
Candidate("rom_0093_1020_f6dc_bit5_context", 0x0093, 0x1020, "F6DC.5 handler context candidate"),
Candidate("rom_0093_4040_f6dc_bit4_context", 0x0093, 0x4040, "F6DC.4 handler context candidate"),
Candidate("rom_0093_8040_f6dc_bit3_context", 0x0093, 0x8040, "F6DC.3 handler context candidate"),
Candidate("rom_0093_0020_f6dc_bit1_context", 0x0093, 0x0020, "F6DC.1 handler low-field candidate"),
Candidate("rom_0093_0040_f6dc_bit0_context", 0x0093, 0x0040, "F6DC.0 handler low-field candidate"),
Candidate("rom_009a_0800_iris_auto_candidate", 0x009A, 0x0800, "F6DB.3 IRIS AUTO report candidate"),
Candidate("rom_00b7_2000_f6d4_bit0_bundle", 0x00B7, 0x2000, "F6D4.0 bundle selector candidate"),
Candidate("rom_00b9_4000_f6dc_bit7_candidate", 0x00B9, 0x4000, "F6DC.7 handler value candidate"),
Candidate("rom_00c4_8000_f6d4_bit0_bundle", 0x00C4, 0x8000, "F6D4.0 bundle selector candidate"),
Candidate("rom_00c6_8000_f6d4_bit0_bundle", 0x00C6, 0x8000, "F6D4.0 bundle selector candidate"),
Candidate("rom_00f8_8000_f6d4_bit1_candidate", 0x00F8, 0x8000, "F6D4.1 handler candidate"),
)
def main() -> int:
args = build_arg_parser().parse_args()
scenario = build_scenario(args)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8")
print(f"wrote {args.output}")
print(f"candidates={len(CANDIDATES)} snapshots={len(CANDIDATES)}")
print(f"estimated_hold_time={len(CANDIDATES) * args.listen / 60:.1f}min plus power-cycle/ready time")
return 0
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"output",
nargs="?",
type=Path,
default=Path("scenarios/panel-atlas-rom-button-output-candidates-v1.json"),
)
parser.add_argument("--listen", type=float, default=0.75, help="seconds to listen after each candidate send")
parser.add_argument("--ok-listen", type=float, default=0.25, help="seconds to listen after CONNECT OK seeds")
parser.add_argument("--drain", type=float, default=0.25, help="seconds to drain after ready")
parser.add_argument("--off-seconds", type=float, default=1.5, help="relay power-off time before each candidate")
parser.add_argument("--ready-heartbeats", type=int, default=2, help="heartbeats before each candidate")
parser.add_argument("--ready-timeout", type=float, default=10.0, help="ready wait timeout")
return parser
def build_scenario(args: argparse.Namespace) -> dict[str, object]:
steps: list[dict[str, object]] = []
for index, candidate in enumerate(CANDIDATES, start=1):
steps.extend(candidate_steps(args, index, candidate))
steps.append({"action": "listen", "seconds": 0.8})
return {
"name": "panel-atlas-rom-button-output-candidates-v1",
"notes": [
"Fresh-boot webcam sweep for ROM-derived button/report output candidates.",
"This deliberately skips physical RCP button presses: each candidate sends command 0 directly.",
"Each candidate gets its own power-cycle/CONNECT-OK baseline to reduce latch contamination.",
"Candidate snapshots only are enabled; setup, CONNECT OK seeds, and clears should not produce webcam images.",
"Run with --camera-index 4 --snapshot-delays 0.5 on the current bench.",
],
"steps": steps,
}
def candidate_steps(args: argparse.Namespace, index: int, candidate: Candidate) -> list[dict[str, object]]:
label_base = f"case{index:03d}_{candidate.label}"
return [
{"action": "power_cycle", "off_seconds": args.off_seconds},
{
"action": "wait_ready",
"heartbeats": args.ready_heartbeats,
"timeout": args.ready_timeout,
"require": True,
},
{"action": "drain", "seconds": args.drain},
send_step(f"{label_base}_ok_seed_1", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
send_step(f"{label_base}_ok_seed_2", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
{
"action": "note",
"message": (
f"{label_base}: selector 0x{candidate.selector:04X}=0x{candidate.value:04X}; "
f"{candidate.note}"
),
},
send_step(label_base, frame_hex(0x00, candidate.selector, candidate.value), args.listen, snapshot=True),
send_step(
f"{label_base}_clear",
frame_hex(0x00, candidate.selector, 0x0000),
0.12,
snapshot=False,
),
]
def send_step(label: str, frame: str, listen: float, *, snapshot: bool) -> dict[str, object]:
step: dict[str, object] = {
"action": "send",
"label": label,
"frame": frame,
"listen": listen,
}
if not snapshot:
step["snapshot"] = False
return step
def frame_hex(command: int, selector: int, value: int) -> str:
selector_hi, selector_lo = selector_bytes(selector)
data = bytes([command & 0xFF, selector_hi, selector_lo, (value >> 8) & 0xFF, value & 0xFF])
return " ".join(f"{byte:02X}" for byte in data + bytes([frame_checksum(data)]))
def selector_bytes(selector: int) -> tuple[int, int]:
selector &= 0x01FF
if selector <= 0x007F:
return 0x00, selector
if selector <= 0x017F:
return 0x01, selector - 0x0080
return 0x02, selector - 0x0180
def frame_checksum(data: bytes) -> int:
value = CHECKSUM_SEED
for byte in data[:5]:
value ^= byte & 0xFF
return value & 0xFF
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Build labeled contact sheets from serial_scenario webcam snapshots."""
from __future__ import annotations
import argparse
import math
from pathlib import Path
import cv2
import numpy as np
CROP_PRESETS = {
"full": (0.00, 0.00, 1.00, 1.00),
"panel": (0.03, 0.10, 0.98, 0.94),
"right-stack": (0.70, 0.23, 0.92, 0.86),
"lcd": (0.00, 0.22, 0.38, 0.78),
}
def main() -> int:
args = build_arg_parser().parse_args()
images = sorted(args.snapshot_dir.glob(args.glob))
if args.only_candidates:
images = [path for path in images if "candidate_" in path.name]
if not images:
raise SystemExit(f"no images found in {args.snapshot_dir}")
output_dir = args.output_dir or args.snapshot_dir.with_name(args.snapshot_dir.name + "-sheets")
output_dir.mkdir(parents=True, exist_ok=True)
per_sheet = args.cols * args.rows
total_sheets = math.ceil(len(images) / per_sheet)
written: list[Path] = []
for sheet_index in range(total_sheets):
group = images[sheet_index * per_sheet : (sheet_index + 1) * per_sheet]
sheet = build_sheet(group, args)
out = output_dir / f"sheet-{sheet_index + 1:03d}.jpg"
cv2.imwrite(str(out), sheet)
written.append(out)
print(f"images={len(images)} sheets={len(written)} output_dir={output_dir}")
if written:
print(f"first={written[0]}")
print(f"last={written[-1]}")
return 0
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Create labeled contact sheets from webcam snapshots.")
parser.add_argument("snapshot_dir", type=Path, help="directory containing serial_scenario snapshots")
parser.add_argument("--output-dir", type=Path, help="directory for generated contact sheets")
parser.add_argument("--glob", default="*.jpg", help="image glob within snapshot_dir")
parser.add_argument("--cols", type=int, default=4, help="thumbnail columns per sheet")
parser.add_argument("--rows", type=int, default=5, help="thumbnail rows per sheet")
parser.add_argument("--thumb-width", type=int, default=360, help="thumbnail image width")
parser.add_argument("--label-height", type=int, default=48, help="label area height per thumbnail")
parser.add_argument(
"--crop",
choices=sorted(CROP_PRESETS),
default="panel",
help="crop preset before thumbnailing",
)
parser.add_argument(
"--only-candidates",
action="store_true",
help="include only filenames containing candidate_",
)
return parser
def build_sheet(paths: list[Path], args: argparse.Namespace) -> np.ndarray:
cells = [build_cell(path, args) for path in paths]
blank = np.full_like(cells[0], 245)
while len(cells) < args.cols * args.rows:
cells.append(blank.copy())
rows = []
for row_index in range(args.rows):
row_cells = cells[row_index * args.cols : (row_index + 1) * args.cols]
rows.append(np.hstack(row_cells))
return np.vstack(rows)
def build_cell(path: Path, args: argparse.Namespace) -> np.ndarray:
image = cv2.imread(str(path))
if image is None:
image = np.full((240, 320, 3), 220, dtype=np.uint8)
image = crop_image(image, CROP_PRESETS[args.crop])
thumb_height = max(1, int(image.shape[0] * args.thumb_width / max(1, image.shape[1])))
thumb = cv2.resize(image, (args.thumb_width, thumb_height), interpolation=cv2.INTER_AREA)
label = snapshot_label(path)
cell = np.full((args.label_height + thumb_height, args.thumb_width, 3), 245, dtype=np.uint8)
cell[args.label_height : args.label_height + thumb_height, 0 : args.thumb_width] = thumb
draw_label(cell, label, args.label_height)
return cell
def crop_image(image: np.ndarray, crop: tuple[float, float, float, float]) -> np.ndarray:
h, w = image.shape[:2]
x1, y1, x2, y2 = crop
left = min(w - 1, max(0, int(w * x1)))
top = min(h - 1, max(0, int(h * y1)))
right = min(w, max(left + 1, int(w * x2)))
bottom = min(h, max(top + 1, int(h * y2)))
return image[top:bottom, left:right]
def draw_label(cell: np.ndarray, text: str, label_height: int) -> None:
font = cv2.FONT_HERSHEY_SIMPLEX
scale = 0.48
thickness = 1
max_chars = 45
line1 = text[:max_chars]
line2 = text[max_chars : max_chars * 2]
cv2.putText(cell, line1, (6, 18), font, scale, (0, 0, 0), thickness, cv2.LINE_AA)
if line2:
cv2.putText(cell, line2, (6, min(label_height - 8, 38)), font, scale, (0, 0, 0), thickness, cv2.LINE_AA)
def snapshot_label(path: Path) -> str:
name = path.name
if "_tx_0500ms_" in name:
label = name.split("_tx_0500ms_", 1)[1]
elif "_tx_" in name:
label = name.split("_tx_", 1)[1]
else:
label = name
return label.rsplit("_", 1)[0]
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -86,6 +86,14 @@ class BenchConnectLcdTest(unittest.TestCase):
def test_label_frame_marks_copy_in_progress_selector_006d_candidate(self): def test_label_frame_marks_copy_in_progress_selector_006d_candidate(self):
self.assertEqual(label_frame(bytes.fromhex("00006D000037")), "copy_in_progress_selector_006d_candidate") self.assertEqual(label_frame(bytes.fromhex("00006D000037")), "copy_in_progress_selector_006d_candidate")
def test_label_frame_marks_iris_mblack_link_reports(self):
self.assertEqual(label_frame(bytes.fromhex("000013400009")), "known_iris_mblack_link_active_report_candidate")
self.assertEqual(label_frame(bytes.fromhex("02001300004B")), "queued_iris_mblack_link_clear_report_candidate")
self.assertEqual(
label_frame(bytes.fromhex("010013C00088")),
"queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -1,8 +1,12 @@
import unittest import unittest
from collections import Counter
from ccu_emulator.controller import CcuConfig, CcuEmulator
from ccu_emulator.frames import ACTIVE_SEED_COMMAND0, NEUTRAL_ACK_FRAME, build_frame, frame_checksum_ok from ccu_emulator.frames import ACTIVE_SEED_COMMAND0, NEUTRAL_ACK_FRAME, build_frame, frame_checksum_ok
from ccu_emulator.iris_mblack_link import IrisMblackLinkModule
from ccu_emulator.policy import AckPolicy from ccu_emulator.policy import AckPolicy
from ccu_emulator.refresh import PeriodicRefresh from ccu_emulator.refresh import PeriodicRefresh
from ccu_emulator.serial_link import RxFrame
class CcuEmulatorFrameTests(unittest.TestCase): class CcuEmulatorFrameTests(unittest.TestCase):
@@ -29,5 +33,95 @@ class PeriodicRefreshTests(unittest.TestCase):
self.assertEqual(refresh.due_frames(now=10.6), []) self.assertEqual(refresh.due_frames(now=10.6), [])
class IrisMblackLinkModuleTests(unittest.TestCase):
def test_active_report_gets_selector_ack_and_mirror(self):
module = IrisMblackLinkModule(mirror_delay=0.012)
decision = module.on_rx(bytes.fromhex("000013400009"), "known_iris_mblack_link_active_report_candidate")
self.assertIsNotNone(decision)
assert decision is not None
self.assertTrue(decision.suppress_default_ack)
self.assertEqual(
[tx.frame for tx in decision.tx],
[bytes.fromhex("05001300004C"), bytes.fromhex("000013400009")],
)
self.assertEqual(decision.tx[1].delay, 0.012)
self.assertIn("active", decision.reason)
def test_clear_report_gets_selector_ack_and_mirror(self):
module = IrisMblackLinkModule()
decision = module.on_rx(bytes.fromhex("02001300004B"), "queued_iris_mblack_link_clear_report_candidate")
self.assertIsNotNone(decision)
assert decision is not None
self.assertEqual(
[tx.frame for tx in decision.tx],
[bytes.fromhex("05001300004C"), bytes.fromhex("000013000049")],
)
self.assertIn("clear", decision.reason)
def test_non_0013_report_is_ignored(self):
module = IrisMblackLinkModule()
self.assertIsNone(module.on_rx(bytes.fromhex("0000158000CF"), "known_call_button_active_report"))
class CcuEmulatorModuleIntegrationTests(unittest.TestCase):
def test_module_response_suppresses_generic_neutral_ack(self):
link = FakeLink(RxFrame(bytes.fromhex("000013400009"), "known_iris_mblack_link_active_report_candidate"))
logger = FakeLogger()
emulator = CcuEmulator(
link,
logger,
config=CcuConfig(seed_frames=(), ready_heartbeats=0),
modules=(IrisMblackLinkModule(mirror_delay=0.0),),
)
emulator._service_rx()
self.assertEqual(
[frame for frame, _label in link.sent],
[bytes.fromhex("05001300004C"), bytes.fromhex("000013400009")],
)
self.assertEqual(emulator.stats.ack_frames, 0)
self.assertEqual(emulator.stats.module_frames, 2)
self.assertEqual(emulator.stats.tx_frames, 2)
class FakeDetector:
def __init__(self) -> None:
self.labels = Counter()
self.resync_events = 0
self.dropped_bytes = 0
class FakeLink:
def __init__(self, *items: RxFrame) -> None:
self.items = list(items)
self.sent: list[tuple[bytes, str]] = []
self.detector = FakeDetector()
def read_available(self) -> list[RxFrame]:
if not self.items:
return []
return [self.items.pop(0)]
def send(self, frame: bytes, label: str) -> None:
self.sent.append((frame, label))
class FakeLogger:
def __init__(self) -> None:
self.lines: list[str] = []
def event(self, text: str) -> None:
self.lines.append(text)
def emit(self, line: str = "") -> None:
self.lines.append(line)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -0,0 +1,69 @@
import unittest
from h8536.panel_selectors import (
CURRENT_TABLE_BASE,
describe_selector_value,
known_panel_selector,
panel_selector_semantics_payload,
selector_word_address,
)
class PanelSelectorSemanticsTest(unittest.TestCase):
def test_selector_0013_maps_to_current_table_word_and_lamp_bits(self):
item = known_panel_selector(0x0013)
self.assertIsNotNone(item)
assert item is not None
self.assertEqual(item["current_word_address_hex"], "H'E826")
self.assertEqual(selector_word_address(CURRENT_TABLE_BASE, 0x0013), 0xE826)
text = " ".join(describe_selector_value(0x0013, 0x4000))
self.assertIn("IRIS/M.BLACK LINK", text)
self.assertIn("F791.5", text)
self.assertIn("F716.7", text)
def test_selector_payload_includes_local_panel_toggle_source(self):
by_selector = {
int(item["selector"]): item
for item in panel_selector_semantics_payload()
}
selector_0013 = by_selector[0x0013]
trigger_text = " ".join(
str(trigger.get("summary", ""))
for trigger in selector_0013.get("local_triggers", [])
)
self.assertIn("F6DB.7", trigger_text)
self.assertIn("H'E826", trigger_text)
trigger_names = " ".join(
str(trigger.get("name_candidate", ""))
for trigger in selector_0013.get("local_triggers", [])
)
self.assertIn("provisional_iris_mblack_link_button_toggle_report", trigger_names)
def test_selector_payload_includes_closed_loop_state_machine(self):
item = known_panel_selector(0x0013)
self.assertIsNotNone(item)
assert item is not None
state_machine = item["state_machine"]
self.assertEqual(state_machine["name_candidate"], "iris_mblack_link_closed_loop_state_candidate")
self.assertEqual(state_machine["ack_frame"], "05 00 13 00 00 4C")
self.assertEqual(state_machine["active_mirror_frame"], "00 00 13 40 00 09")
self.assertEqual(state_machine["clear_mirror_frame"], "00 00 13 00 00 49")
def test_rom_button_output_sweep_meanings_are_available(self):
monitor_text = " ".join(describe_selector_value(0x001A, 0x4040))
standard_text = " ".join(describe_selector_value(0x006B, 0x8000))
shutter_text = " ".join(describe_selector_value(0x008F, 0x2000))
white_balance_text = " ".join(describe_selector_value(0x0093, 0x8040))
self.assertIn("MONITOR G", monitor_text)
self.assertIn("STANDARD", standard_text)
self.assertIn("shutter 00.0", shutter_text)
self.assertIn("white-balance PRESET", white_balance_text)
if __name__ == "__main__":
unittest.main()

View File

@@ -494,6 +494,77 @@ class SerialPseudocodeTest(unittest.TestCase):
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text) self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text) self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
def test_panel_selector_semantics_are_emitted_as_comments_and_helper(self):
analysis = {
"protocol_semantics": [
{
"confidence": "medium",
"confidence_score": 0.7,
"commands": [],
"panel_selector_semantics": [
{
"selector": 0x0013,
"selector_hex": "0x0013",
"name": "slave_and_iris_mblack_link_lamps",
"summary": "Selector 0x0013 reads H'E826 and controls two lamp bits.",
"current_word_address_hex": "H'E826",
"dispatch_handler": "H'2E06",
"state_machine": {
"name_candidate": "iris_mblack_link_closed_loop_state_candidate",
"summary": "RCP reports local intent, CCU ACKs selector 0x0013, then mirrors accepted state back.",
"active_report_frame": "00 00 13 40 00 09",
"clear_report_frame": "00 00 13 00 00 49",
"ack_frame": "05 00 13 00 00 4C",
"active_mirror_frame": "00 00 13 40 00 09",
"clear_mirror_frame": "00 00 13 00 00 49",
},
"effects": [
{
"bit": 14,
"mask": 0x4000,
"mask_hex": "0x4000",
"name": "IRIS/M.BLACK LINK lamp",
"when_set": "sets F791.5 and F716.7",
"when_clear": "clears F791.5 and F716.7",
"ram_bits": ["F791.5", "F716.7"],
},
],
"local_triggers": [
{
"source": "F006.7 / F6DB.7",
"handler": "H'200E",
"name_candidate": "provisional_iris_mblack_link_button_toggle_report",
"summary": "H'200E toggles H'E826 bit14 and queues selector 0x0013.",
"gate": "F731 <= 3",
"current_state_bit": "F791.5",
"active_value": 0x4000,
"clear_value": 0x0000,
},
],
},
],
}
]
}
with patch("h8536.serial_pseudocode.analyze_serial_semantics", return_value=analysis):
text = generate_serial_pseudocode(candidate_payload())
self.assertIn("panel selector semantics:", text)
self.assertIn("0x0013 slave_and_iris_mblack_link_lamps", text)
self.assertIn("0x4000 -> IRIS/M.BLACK LINK lamp", text)
self.assertIn("F791.5", text)
self.assertIn("F716.7", text)
self.assertIn("F006.7 / F6DB.7", text)
self.assertIn("iris_mblack_link_closed_loop_state_candidate", text)
self.assertIn("ACK 05 00 13 00 00 4C", text)
self.assertIn("static void sci1_candidate_panel_selector_annotation", text)
self.assertIn("case 0x0013u:", text)
self.assertIn("void provisional_iris_mblack_link_button_toggle_report(void)", text)
self.assertIn("Source F006.7 / F6DB.7; gate F731 <= 3; current state F791.5.", text)
self.assertIn("Requests selector 0x0013=0x4000", text)
self.assertIn("CCU should ACK 05 00 13 00 00 4C, then mirror 00 00 13 40 00 09.", text)
def test_timer_source_models_emit_separate_tick_isrs(self): def test_timer_source_models_emit_separate_tick_isrs(self):
analysis = { analysis = {
"protocol_semantics": [ "protocol_semantics": [

View File

@@ -84,6 +84,67 @@ class SerialScenarioTest(unittest.TestCase):
self.assertIn("before_send=1", output) self.assertIn("before_send=1", output)
self.assertIn("delays=0,0.25,1", output) self.assertIn("delays=0,0.25,1", output)
def test_dry_run_summarizes_listen_ack_until_quiet(self):
scenario = {
"name": "unit-quiet-ack",
"steps": [
{
"action": "listen_ack_until_quiet",
"seconds": 12.0,
"quiet_seconds": 0.9,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"max_acks": 16,
}
],
}
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "scenario.json"
path.write_text(json.dumps(scenario), encoding="utf-8")
stdout = io.StringIO()
exit_code = main([str(path), "--dry-run"], stdout=stdout)
output = stdout.getvalue()
self.assertEqual(exit_code, 0)
self.assertIn("step[1]=listen_ack_until_quiet", output)
self.assertIn("seconds=12.000", output)
self.assertIn("quiet=0.900s", output)
self.assertIn("target_mode=queued_reports", output)
self.assertIn("ack_mode=cmd5_selector", output)
def test_dry_run_summarizes_respond_on_rules(self):
scenario = {
"name": "unit-respond-on",
"steps": [
{
"action": "listen_ack",
"seconds": 2.0,
"target_mode": "queued_reports",
"ack_mode": "cmd5_selector",
"respond_on": [
{
"frames": ["00 00 13 40 00 09"],
"send": "00 00 13 40 00 09",
"label": "mirror_selector_0013_active_from_button",
}
],
}
],
}
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "scenario.json"
path.write_text(json.dumps(scenario), encoding="utf-8")
stdout = io.StringIO()
exit_code = main([str(path), "--dry-run"], stdout=stdout)
output = stdout.getvalue()
self.assertEqual(exit_code, 0)
self.assertIn("respond_on=1", output)
self.assertIn("send=00 00 13 40 00 09", output)
self.assertIn("label=mirror_selector_0013_active_from_button", output)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@@ -0,0 +1,20 @@
import unittest
from h8536.serial_scenario_unexpected import parse_detected_frames
class SerialScenarioUnexpectedTest(unittest.TestCase):
def test_parse_detected_frames_relabels_iris_mblack_link_report(self):
frames = parse_detected_frames(
[
"12:00:00.000 DETECT checksum_ok_unlabeled 00 00 13 40 00 09",
"12:00:00.100 DETECT checksum_ok_unlabeled 02 00 13 00 00 4B",
]
)
self.assertEqual(frames[0].label, "known_iris_mblack_link_active_report_candidate")
self.assertEqual(frames[1].label, "queued_iris_mblack_link_clear_report_candidate")
if __name__ == "__main__":
unittest.main()

View File

@@ -405,6 +405,19 @@ class SerialSemanticsTest(unittest.TestCase):
self.assertIn("faa2 == 0", gate_text) self.assertIn("faa2 == 0", gate_text)
self.assertIn("not rom constants", gate_text) self.assertIn("not rom constants", gate_text)
def test_panel_selector_semantics_include_iris_mblack_link_trace(self):
semantics = only_semantics(self, planned_semantics_payload())
panel = semantics["panel_selector_semantics"]
panel_text = semantic_text(panel)
self.assertIn("0x0013", panel_text)
self.assertIn("iris/m.black link", panel_text)
self.assertIn("h'e826", panel_text)
self.assertIn("f791.5", panel_text)
self.assertIn("f716.7", panel_text)
self.assertIn("f6db.7", panel_text)
def test_actual_dispatch_split_marks_initial_and_continuation_commands(self): def test_actual_dispatch_split_marks_initial_and_continuation_commands(self):
semantics = only_semantics( semantics = only_semantics(
self, self,

View File

@@ -130,9 +130,9 @@ class TableXrefsTest(unittest.TestCase):
self.assertIn("Table/Index Cross-Reference Report for sample.json", text) self.assertIn("Table/Index Cross-Reference Report for sample.json", text)
self.assertIn("primary_value_table_candidate H'E000", text) self.assertIn("primary_value_table_candidate H'E000", text)
self.assertIn("offset H'0006 -> H'E006", text) self.assertIn("offset H'0006 selector 0x003 -> H'E006", text)
self.assertIn("offset H'0124 -> H'E124", text) self.assertIn("offset H'0124 selector 0x092 -> H'E124", text)
self.assertIn("offset H'0002 at H'F922", text) self.assertIn("offset H'0002 selector 0x001 at H'F922", text)
self.assertIn("index dynamic via R4", text) self.assertIn("index dynamic via R4", text)
self.assertIn("term 'CONNECT': no LCD/text candidate hits", text) self.assertIn("term 'CONNECT': no LCD/text candidate hits", text)
self.assertIn("term 'COMM LINK': 1 candidate", text) self.assertIn("term 'COMM LINK': 1 candidate", text)