diff --git a/src/content/docs/reference/program-format.mdx b/src/content/docs/reference/program-format.mdx index 3ebe7a5..8de9b91 100644 --- a/src/content/docs/reference/program-format.mdx +++ b/src/content/docs/reference/program-format.mdx @@ -1,6 +1,6 @@ --- title: Program record format -description: Wire layout of the Omni Pro II's 14-byte program record — the panel-side automation engine's storage unit. Includes the Mon/Day file-format swap, the Remark variant, and a worked example. +description: Wire layout of the Omni Pro II's 14-byte program record — the panel-side automation engine's storage unit. Covers the compact single-record form, the firmware-≥3.0 multi-record form, the EVENT Mon/Day file swap, the Remark variant, and a worked example. --- A "program" in Omni Pro II vocabulary is one line of the panel's built-in @@ -24,16 +24,27 @@ would use 12 bytes with one cond — we don't yet support those.) | Offset | Field | Type | Notes | |-------:|---|---|---| | 0 | `prog_type` | byte | `ProgramType` enum | -| 1-2 | `cond` | BE u16 | Primary condition (opaque this pass) | -| 3-4 | `cond2` | BE u16 | Secondary condition (opaque this pass) | +| 1-2 | `cond` | **LE u16** | First inline AND-IF condition (zero = no condition) | +| 3-4 | `cond2` | **LE u16** | Second inline AND-IF condition (zero = no condition) | | 5 | `cmd` | byte | [`Command`](/reference/library-api/) enum | | 6 | `par` | byte | Command parameter (level, mode, …) | -| 7-8 | `pr2` | BE u16 | Secondary parameter (usually object number) | +| 7-8 | `pr2` | **LE u16** | Secondary parameter (usually object number) | | 9-10 | `month`, `day` | bytes | **Swapped to `day, month` on disk for `EVENT` type** — see below | | 11 | `days` | byte | `Days` bitmask (Mon=`0x02` … Sun=`0x80`) | | 12 | `hour` | byte | 0-23 for absolute time; offset for sunrise/sunset-relative | | 13 | `minute` | byte | 0-59 | +:::caution[Byte order] +The three 16-bit fields (`cond`, `cond2`, `pr2`) are **little-endian**: +byte N is the low byte and byte N+1 is the high byte. The 32-bit +`remark_id` in the REMARK variant is the one big-endian field +(see below). Earlier revisions of this page documented all of them +as big-endian — that was wrong. The mistake was caught by authoring +known programs in PC Access and byte-diffing the resulting `.pca`; +see the [clausal-RE findings notes](https://github.com/warehack-ing/omni-pca/blob/main/pca-re/clausal-re/FINDINGS.md) +for the empirical work. +::: + ### `Remark` variant When `prog_type == REMARK` (4) the bytes at offsets 1-4 hold a single @@ -79,21 +90,26 @@ for p in iter_defined(acct.programs): ### `ProgramType` (byte 0) -| Value | Name | Meaning | -|------:|---|---| -| 0 | `FREE` | Unused slot (all 14 bytes zero) | -| 1 | `TIMED` | Fires at a specific time of day on selected weekdays | -| 2 | `EVENT` | Fires when a panel event occurs (zone open, X-10, …) | -| 3 | `YEARLY` | Fires on a specific calendar date each year | -| 4 | `REMARK` | Stores a RemarkID + free-text association | -| 5 | `WHEN` | Connector — string multiple records into one line *(RE-pending)* | -| 6 | `AT` | Connector *(RE-pending)* | -| 7 | `EVERY` | Connector *(RE-pending)* | -| 8 | `AND` | Connector *(RE-pending)* | -| 9 | `OR` | Connector *(RE-pending)* | -| 10 | `THEN` | Connector *(RE-pending)* | +The 11 values split into two **encoding families**. Which family a +block uses depends on the panel firmware version — see [Compact vs +multi-record form](#compact-vs-multi-record-form) below. -Source: `enuProgramType.cs`. +| Value | Name | Family | Meaning | +|------:|---|---|---| +| 0 | `FREE` | compact | Unused slot (all 14 bytes zero) | +| 1 | `TIMED` | compact | Time-of-day trigger with inline action | +| 2 | `EVENT` | compact | Panel event (zone open, X-10, …) with inline action | +| 3 | `YEARLY` | compact | Yearly calendar trigger with inline action | +| 4 | `REMARK` | compact | RemarkID + free-text association (see [Remark variant](#remark-variant)) | +| 5 | `WHEN` | multi-record | Event-trigger record *(firmware ≥3.0.0)* | +| 6 | `AT` | multi-record | Time-trigger record *(firmware ≥3.0.0)* | +| 7 | `EVERY` | multi-record | Recurring-trigger record *(firmware ≥3.0.0)* | +| 8 | `AND` | multi-record | AND-condition record *(firmware ≥3.0.0)* | +| 9 | `OR` | multi-record | OR-alternative separator *(firmware ≥3.0.0)* | +| 10 | `THEN` | multi-record | Action record *(firmware ≥3.0.0)* | + +Source: `enuProgramType.cs`; the firmware gate is +`Features.Add(MultiLinePrograms, 196608u)` at `clsCapOMNI_PRO_II.cs:290`. ### `ConditionFamily` (high bits of `cond` / `cond2`) @@ -135,22 +151,26 @@ Take this slot from our live fixture (slot 22): Decoded as a file record: -| Offset | Bytes | Field | Value | +| Offset | Bytes | Field | Value (LE for u16) | |-------:|:--|:--|:--| | 0 | `01` | `prog_type` | `TIMED` (1) | -| 1-2 | `8d 09` | `cond` | `0x8d09` | -| 3-4 | `9b 09` | `cond2` | `0x9b09` | +| 1-2 | `8d 09` | `cond` | `0x098d` → CTRL family, Unit 397, OFF | +| 3-4 | `9b 09` | `cond2` | `0x099b` → CTRL family, Unit 411, OFF | | 5 | `44` | `cmd` | `0x44` (raw — opcode is one of the many `enuUnitCommand` values) | | 6 | `03` | `par` | 3 | -| 7-8 | `01 00` | `pr2` | 256 | +| 7-8 | `01 00` | `pr2` | `0x0001` = 1 (object #1) | | 9 | `08` | `month` (TIMED → no swap) | 8 | | 10 | `0c` | `day` | 12 | | 11 | `3e` | `days` | `Mon\|Tue\|Wed\|Thu\|Fri` (weekdays) | | 12 | `07` | `hour` | 07 | | 13 | `0f` | `minute` | 15 | -So: "TIMED program firing at 07:15 on weekdays, doing command 0x44 with -par=3 and pr2=256, gated by the condition pair 0x8d09 / 0x9b09." +So: "TIMED program firing at 07:15 on weekdays, doing command `0x44` +with `par=3` against object #1, gated by `Unit 397 OFF AND IF Unit 411 OFF`." + +Note that the **bytes** at offsets 7-8 are `01 00` but the **value** is 1 +(not 256): the low byte comes first. Same for `cond` (`8d 09` → +`0x098d`, not `0x8d09`) and `cond2`. ## `cond`/`cond2` bit split @@ -311,22 +331,88 @@ Program.from_file_record(body) # for .pca table slots with matching `encode_wire_bytes()` / `encode_file_record()` round-trip methods. -## What we don't yet know +## Compact vs multi-record form -This page covers the byte-level mechanics. The semantic decoding of -`cond` / `cond2` — what zone number, security mode, or time clock a -specific 16-bit value refers to — is a separate reverse-engineering -pass. So is the multi-record clausal encoding hinted at by the -`WHEN/AT/EVERY/AND/OR/THEN` connector values in `ProgramType`. Concretely: +The same user-visible "program block" (1 WHEN + 0-N AND-IFs + 1 THEN) +has two on-disk encodings. PC Access picks **compact** whenever the +block can fit, and **multi-record** otherwise. -- **Multi-record clausal encoding.** No program in our live fixture uses - the `WHEN / AT / EVERY / AND / OR / THEN` ProgType values — so we - can't yet say whether they reference adjacent slots, use extra bytes - within a single slot, or live in some separate clause table. +### Compact form (always available) + +The block fits in **one** 14-byte record. `prog_type` is `TIMED`, +`EVENT`, or `YEARLY` depending on the trigger kind: + +- The action goes inline in `cmd` / `par` / `pr2`. +- Up to **two** AND-IF conditions go inline in `cond` and `cond2`. +- For `EVENT`, the event identifier replaces the calendar + month/day at bytes 9-10 (see [the EVENT `Evt` u16 section](#also-for-event-bytes-910-are-an-event-identifier-not-a-date)). + +This is how 100% of records in any panel running firmware <3.0 are +encoded. It is also how PC Access encodes any block that fits the +constraints, even on firmware ≥3.0 — see the simplification rules +in `frmAutomationEditBlock.cs:589 SimplifyLines`. + +### Multi-record form (firmware ≥3.0.0 only) + +When the block cannot fit in compact form, PC Access emits one +14-byte record per "line", consuming sequential slots in the program +table: + +| ProgType | Role | Notable fields | +|---|---|---| +| `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) | +| `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 | + +A 5-record block from our test fixture (one WHEN, three ANDs, one +THEN, all in adjacent slots): + +``` +slot N+0: 05 00 00 00 00 00 00 00 00 04 01 00 00 00 WHEN +slot N+1: 08 00 00 00 01 00 00 00 00 00 00 00 00 00 AND IF NEVER +slot N+2: 08 00 00 00 01 00 00 00 00 00 00 00 00 00 AND IF NEVER +slot N+3: 08 00 00 00 01 00 00 00 00 00 00 00 00 00 AND IF NEVER +slot N+4: 0a 00 00 00 00 01 00 01 00 00 00 00 00 00 THEN UNIT 1 ON +``` + +The firmware gate is `Features.Add(MultiLinePrograms, 196608u)` in +`clsCapOMNI_PRO_II.cs:290` (the 24-bit value packs as +`major*65536 + minor*256 + revision` → 3.0.0). + +When MultiLinePrograms is OFF (firmware <3.0): + +- PC Access's `Or` toolbar button and "Add Comment Block" menu item + are disabled. +- PC Access refuses to save any block with 3+ AND-IFs, an OR clause, + or a comment. +- The panel's program table can only contain ProgType values 0-4. + +The user-facing panel from this repository runs firmware 2.16A, so +all programs in its `.pca` are compact-form by necessity. Our test +captures of the multi-record form were produced by using PC Access's +**Account → Version Override → "Controller Firmware >= 3.0"**, which +spoofs the capability for editing only; the resulting `.pca` won't +load on a real 2.16A panel. + +## Still on the RE backlog + +This page covers the byte-level mechanics for the **compact form** +fully and the **multi-record form** at the record-discriminator +level. Concrete follow-up work: + +- **AND-record's structured-condition fields** (`OP` / `Arg1_*` / + `Arg2_*` / `CompConst` per `frmAutomationEditBlock.cs:988-1000`). + We have one observation (`AND IF NEVER` → byte 4 = `0x01`); a + controlled-input sweep is needed to map the rest. +- **AT** and **EVERY** trigger records — no captured examples. +- **Comment blocks** — multi-record chained `REMARK` records. - **`DoubleProgramConditional` capability flag.** We hardcode 14-byte - records for the Omni Pro II. Other models may use the 12-byte form. - Locating where the panel advertises the flag (and which non-OPII - models clear it) is its own follow-up. + records for the Omni Pro II. Other models clear the flag and use + 12-byte records with only `cond` (no `cond2`). Adding support is + its own follow-up. ## Python usage