program-format: document AND-record field layout (firmware >=3.0)

Add a per-byte breakdown of the multi-record AND record format from
clsProgram.cs:326-436, with the enuCondOP and enuCondArgType companion
enum tables. Note the Arg1_Traditional special case where the Cond
u16 carries the condition instead of the structured Arg1_* fields,
and flag the open question about disk byte order for Arg1_IX /
Arg2_IX / CompConst (Read says LE, accessors say BE; needs a
non-symmetric controlled capture to disambiguate).
This commit is contained in:
Ryan Malloy 2026-05-12 03:19:56 -06:00
parent 2300be0f6c
commit 7a766d69ba

View File

@ -363,10 +363,81 @@ table:
| `WHEN` (5) | Event trigger | event-id at bytes 9-10 (no swap); cmd/par/pr2 zero | | `WHEN` (5) | Event trigger | event-id at bytes 9-10 (no swap); cmd/par/pr2 zero |
| `AT` (6) | Single-occurrence time trigger | time fields (layout TBD) | | `AT` (6) | Single-occurrence time trigger | time fields (layout TBD) |
| `EVERY` (7) | Recurring time trigger | interval (layout TBD) | | `EVERY` (7) | Recurring time trigger | interval (layout TBD) |
| `AND` (8) | One AND-IF condition | structured: `OP` / `Arg1_ArgType` / `Arg1_IX` / `Arg1_Field` / `Arg2_*` / `CompConst` (layout TBD) | | `AND` (8) | One AND-IF condition | structured `OP` / `Arg1` / `Arg2` / `CompConst` — see [AND-record layout](#and-record-layout-firmware-30) |
| `OR` (9) | Separator between AND-groups | **no payload** — only the `prog_type` byte is set | | `OR` (9) | Separator between AND-groups | **no payload** — only the `prog_type` byte is set |
| `THEN` (10) | Action | same `cmd` / `par` / `pr2` layout as compact form | | `THEN` (10) | Action | same `cmd` / `par` / `pr2` layout as compact form |
#### `AND`-record layout (firmware ≥3.0)
Per the `clsProgram.cs:326-436` accessors, an `AND` record's 14 bytes
decompose as:
| Byte(s) | Field | Type | Notes |
|--------:|---|---|---|
| 0 | `prog_type` | byte | `= 8` (AND) |
| 1 | `OP` | byte | `enuCondOP` — operator (see below) |
| 2 | `Arg1_ArgType` | byte | `enuCondArgType` — what Arg1 is (Zone, Unit, …) |
| 3-4 | `Arg1_IX` | u16 | Index/number of the Arg1 object |
| 5 | `Arg1_Field` | byte | Which sub-field of Arg1 (per-type enum) |
| 6 | `Arg2_ArgType` | byte | |
| 7-8 | `Arg2_IX` | u16 | |
| 9 | `Arg2_Field` | byte | |
| 10-11 | `CompConst` | u16 | Constant operand for comparison ops |
| 12-13 | (unused, zero) | | |
`enuCondOP` (byte 1):
| Value | Name |
|------:|---|
| 0 | `Arg1_Traditional` |
| 1 | `Arg1_EQ_Arg2` |
| 2 | `Arg1_NE_Arg2` |
| 3 | `Arg1_LT_Arg2` |
| 4 | `Arg1_GT_Arg2` |
| 5 | `Arg1_Odd` |
| 6 | `Arg1_Even` |
| 7 | `Arg1_Multiple_Arg2` |
| 8 | `Arg1_IN_Arg2` |
| 9 | `Arg1_NOT_IN_Arg2` |
`enuCondArgType` (bytes 2, 6):
| Value | Name |
|------:|---|
| 0 | `Constant` |
| 1 | `UserSetting` |
| 2 | `Zone` |
| 3 | `Unit` |
| 4 | `Thermostat` |
| 5 | `Auxillary` |
| 6 | `Area` |
| 7 | `TimeDate` |
| 8 | `Audio` |
| 9 | `AccessControl` |
| 10 | `Message` |
| 11 | `System` |
**Important special case:** when `OP == Arg1_Traditional` (= 0), the
AND record's condition is rendered from the **`Cond` u16 at bytes
1-2** using the same per-family scheme as compact-form `cond` (see
[cond/cond2 bit split](#condcond2-bit-split) above) — see
`clsText.cs:2281-2284 GetComplexConditionText`. The richer
`Arg1_*` / `Arg2_*` / `CompConst` fields above are only used when
`OP > 0`. So the same byte positions serve double duty: as `Cond`
(traditional) or as `OP` + `Arg1_ArgType` (structured).
:::caution[Open: disk byte order for `Arg1_IX` / `Arg2_IX` / `CompConst`]
The C# `clsProgram.Read` reads u16 fields **little-endian** via
`clsPcaCryptFileStream.ReadUInt16`, but the `Arg1_IX` /
`Arg2_IX` / `CompConst` accessors index `Data[]` as **big-endian**.
This implies an implicit byte swap somewhere we haven't fully traced.
Our one captured `AND IF NEVER` record (`08 00 00 00 01 00 00 00 00
00 00 00 00 00`) is byte-symmetric (all zero except byte 4 = `0x01`)
so it doesn't disambiguate. Resolving the disk byte order needs a
controlled capture of `AND IF ZONE 5 SECURE` or `AND IF UNIT 7 ON` —
the first non-symmetric example will pin it down.
:::
A 5-record block from our test fixture (one WHEN, three ANDs, one A 5-record block from our test fixture (one WHEN, three ANDs, one
THEN, all in adjacent slots): THEN, all in adjacent slots):