From 7a766d69bac81e1acb020cf82bc994b68eeb4e6c Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 12 May 2026 03:19:56 -0600 Subject: [PATCH] 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). --- src/content/docs/reference/program-format.mdx | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/src/content/docs/reference/program-format.mdx b/src/content/docs/reference/program-format.mdx index 8de9b91..ba1d432 100644 --- a/src/content/docs/reference/program-format.mdx +++ b/src/content/docs/reference/program-format.mdx @@ -363,10 +363,81 @@ table: | `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) | | `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 | | `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 THEN, all in adjacent slots):