diff --git a/src/content/docs/reference/program-format.mdx b/src/content/docs/reference/program-format.mdx index d73ab52..10fc978 100644 --- a/src/content/docs/reference/program-format.mdx +++ b/src/content/docs/reference/program-format.mdx @@ -469,16 +469,18 @@ Pure discriminator — carries no payload. Marks the boundary between alternativ Same `cmd` / `par` / `pr2` layout as compact-form records (`cmd` at byte 5, `par` at byte 6, `pr2` at bytes 7-8 LE). -### Structured-OP AND records (future RE) +### Structured-OP AND records (`OP > 0`) -For the **traditional** AND case (`OP == 0`, the common case), the -AND record's byte layout is the single-AND-IF table in the [per-record -byte layouts](#per-record-byte-layouts-firmware-30-multi-record-form) -section above: byte 1 = family + operand bits, bytes 3-4 BE = instance. +The AND record has **two modes** distinguished by the `OP` byte: -For **structured-OP** cases (e.g. `AND IF TEMPERATURE > 70`), the C# -encoder at `frmAutomationEditBlock.cs:988-1000` populates additional -fields per the `clsProgram.cs:326-436` accessor view: +- **Traditional (`OP == 0`):** the simple-AND-IF case covered in the + table above. Byte 2 holds a `ProgramCond` family code (ZONE=4, + CTRL=8, …) and bytes 3-4 hold `instance << 8`. +- **Structured (`OP > 0`):** comparison-operator conditions like + `DATE IS EQUAL TO 12/31`, `TEMPERATURE > 70`, `ZONE IS BYPASSED`. + +Per the `clsProgram.cs:326-436` accessors, a structured AND record's +14 bytes decompose as: | Byte(s) (in-memory `Data[]`) | Field | Type | | --- | --- | --- | @@ -491,20 +493,20 @@ fields per the `clsProgram.cs:326-436` accessor view: | 9 | `Arg2_Field` | byte | | 10-11 | `CompConst` | u16 | -`enuCondOP` (byte 1 when `OP > 0`): +`enuCondOP` (byte 1): -| Value | Name | -| ---: | --- | -| 0 | `Arg1_Traditional` (= the common case, byte 1 = family + operand, see table above) | -| 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` | +| Value | Name | UI label | +| ---: | --- | --- | +| 0 | `Arg1_Traditional` | (simple condition, see table above) | +| 1 | `Arg1_EQ_Arg2` | IS EQUAL TO | +| 2 | `Arg1_NE_Arg2` | IS NOT EQUAL TO | +| 3 | `Arg1_LT_Arg2` | IS LESS THAN | +| 4 | `Arg1_GT_Arg2` | IS GREATER THAN | +| 5 | `Arg1_Odd` | IS ODD | +| 6 | `Arg1_Even` | IS EVEN | +| 7 | `Arg1_Multiple_Arg2` | IS A MULTIPLE OF | +| 8 | `Arg1_IN_Arg2` | IS IN | +| 9 | `Arg1_NOT_IN_Arg2` | IS NOT IN | `enuCondArgType` (bytes 2, 6): @@ -523,12 +525,69 @@ fields per the `clsProgram.cs:326-436` accessor view: | 10 | `Message` | | 11 | `System` | -The disk-byte-order question for the structured-OP case (whether -bytes 1-2 map to `OP` + `Arg1_ArgType` after the LE Read-time swap, -or whether the encoder uses a different path) is unresolved — needs a -controlled `AND IF TEMP > 70` capture to disambiguate. The `omni_pca` -decoder exposes `CondOP` and `CondArgType` enums for callers who want -to inspect raw bytes once captures are available. +#### Worked example: `AND IF DATE IS EQUAL TO 12/31` + +Authored as a 5-line block, the AND record landed at: + +``` +08 07 01 00 00 01 00 1f 0c 00 00 00 00 00 +``` + +Tracing the byte flow through Read (LE u16) → setter (swap to BE in `Data[]`) → accessor (BE read): + +| Byte position | Disk | Read u16 LE | After setter (Data[]) | Accessor reads | +| --- | --- | --- | --- | --- | +| 1-2 | `07 01` | Cond=`0x0107` | `Data[1]=01, Data[2]=07` | OP=`1` (`Arg1_EQ_Arg2`), Arg1_ArgType=`7` (`TimeDate`) | +| 3-4 | `00 00` | Cond2=`0x0000` | `Data[3]=00, Data[4]=00` | Arg1_IX=`0` (CURRENT_DATE) | +| 5 | `01` | — | `Data[5]=01` | Arg1_Field=`1` | +| 6 | `00` | — | `Data[6]=00` | Arg2_ArgType=`0` (`Constant`) | +| 7-8 | `1f 0c` | Pr2=`0x0c1f` | `Data[7]=0c, Data[8]=1f` | Arg2_IX=`0x0c1f` = (month=12, day=31) | +| 9 | `00` | — | `Data[9]=00` | Arg2_Field=`0` | +| 10-11 | `00 00` | — | `Data[10]=00, Data[11]=00` | CompConst=`0` | + +#### How Traditional and Structured share the same byte slots + +The C# `clsConditionLine.Cond` property at `clsConditionLine.cs:17-33` +is the bridge between the two modes: + +```csharp +public ushort Cond { + get { + if (OP == enuCondOP.Arg1_Traditional) + return ((Arg1.ArgType << 8) | (Arg1.IX >> 8)); + return 0; + } + set { + OP = enuCondOP.Arg1_Traditional; + Arg1.ArgType = (enuCondArgType)(value >> 8); + Arg1.IX = (ushort)((value & 0xFF) << 8); + } +} +``` + +When you set `Cond = 0x0405` for "Zone 5 Secure": +- `Arg1.ArgType` receives `0x04` (the family code value, stored as a + raw byte — semantically a `ProgramCond.ZONE` even though the field + type is `enuCondArgType`) +- `Arg1.IX` receives `(0x05 << 8) = 0x0500` (the instance number + shifted up by 8 bits) + +These get serialised through the same `clsProgram.Cond` / +`Cond2` setters as compact-form records, ending up on disk as +`04 00 00 05` at bytes 1-4. The same disk slots therefore double +as: + +- `OP` (byte 2 LE → `Data[1]`) and `Arg1_ArgType` (byte 1 LE → + `Data[2]`) for structured records, OR +- `family << 8 | instance >> 8` packed into the `Cond` u16 for + Traditional records (using the same byte storage but interpreted + via the `clsConditionLine.Cond` synthesis) + +The `omni_pca.Program` class exposes `and_op`, `and_arg1_argtype`, +`and_arg1_ix`, `and_arg2_ix` etc. as direct accessors for the +structured case, plus `and_family` / `and_instance` as +Traditional-friendly aliases (with `and_instance` doing the right +shift for the Traditional case automatically). ### Other open items @@ -581,11 +640,23 @@ if p.is_multi_record(): elif p.prog_type == ProgramType.EVERY: interval = p.every_interval # e.g. 5 for "5 SECONDS" - # AND (8) — one AND-IF condition + # AND (8) — one AND-IF condition (either Traditional or Structured) elif p.prog_type == ProgramType.AND: - family = p.and_family # high byte: 0x04=ZONE, 0x08=CTRL, 0x0A=CTRL+ON, ... - operand = (family & 0x02) >> 1 # ON / NOT_READY / ENABLED - instance = p.and_instance # zone#, unit#, MiscConditional value + if p.and_op == 0: + # Traditional: family code at byte 1, instance at byte 4 + family = p.and_family # 0x04=ZONE, 0x08=CTRL, 0x0A=CTRL+ON, ... + operand = (family & 0x02) >> 1 # ON / NOT_READY / ENABLED + instance = p.and_instance # zone#, unit#, MiscConditional value + else: + # Structured: e.g. "AND IF DATE IS EQUAL TO 12/31" + op_ = p.and_op # enuCondOP (1=EQ, 2=NE, 3=LT, 4=GT, ...) + arg1_type = p.and_arg1_argtype # enuCondArgType (TimeDate=7, ...) + arg1_ix = p.and_arg1_ix # object index or sub-field code + arg1_fld = p.and_arg1_field + arg2_type = p.and_arg2_argtype # often Constant (=0) + arg2_ix = p.and_arg2_ix # constant value or 2nd object + arg2_fld = p.and_arg2_field + compconst = p.and_compconst # OR (9) — pure separator, no fields elif p.prog_type == ProgramType.OR: