# MTC5 Controller — AI Programming Knowledge Base **Audience:** an AI agent that helps installers program/commission an **MTC5** controller through the **MTC5Coder** Windows app and a cloud server. **Purpose of this file:** give the agent everything it needs to (1) judge **honestly** whether an installer's request is achievable on this hardware, (2) if achievable, produce a **program sketch** that MTC5Coder will compile into an uploadable `.app` file, and (3) if not achievable, explain **why** in terms of the concrete limits below. > **Output file.** The sketch content is **JSON** (schema: `mtc5_program.schema.json`), > but it MUST be saved with the **`.pgmai`** file extension (e.g. `my_install.pgmai`) — > that is what MTC5Coder's **File ▸ Load AI Program (.pgmai)…** opens. Do not use a > `.json` extension for the deliverable; the bytes inside are still plain JSON. > **Source of truth.** Every statement here was read out of the firmware > (`MTC5Coder/Firmware/*.h`, `MTC5.c`) and the app (`Form1.cs`, `Params.cs`, > `MemMap.cs`). Where firmware and app are known to differ or where a behaviour is > a known defect, it is called out in **§12 Firmware notes**. Do **not** promise > anything that is listed there as broken/unimplemented. > **Status of the toolchain.** A **sketch** of the JSON→`.app` reader exists at > `JsonProgramReader.cs` (`Form1.LoadProgramJson`) — not yet wired to a menu or > compile-tested; it reuses the app's existing encoders. This document *defines the > contract* it follows. The `.app` writer that already exists (`Form1.SaveApp`) is the > byte-format reference (§10). --- ## 1. Hardware at a glance One PIC18F26K22 controller ("MTC5 / Solar Sentinel"), 0.5 °C-resolution firmware build (`DEG_STEP05`), WiFi-connected to a cloud server through an ESP8266 bridge. | Resource | Count | Notes | |---|---|---| | Relay outputs | **4** | RA, RB, RC, RD (a.k.a. Relay1..4). **RA = SPDT, voltage-free; RB = SPST, voltage-free; RC = SPST, voltage-free; RD = SPST, switches the mains supply** (RD is NOT voltage-free — it is internally connected to mains L). | | Analog outputs | **2** | Terminal labels **AN0 / AN1** (= AnOut1 / AnOut2 in software). Each is **jumper-configurable to 0–10 V or PWM (10 V)**. Firmware drives them identically as a 5 kHz, 0..200-tick value (0.5 % steps); the jumper only selects the electrical output form. | | Temperature sensor inputs | **4** | T1, T2, T3, T4 (channels A,B,C,D). | | Digital inputs | **2** | DI1, DI2 (threshold-read, ON when input high). | | Real-time clock | 1 | DS1302, used by the 2 schedules and auto season logic. | | MEM (virtual) relays | **4** | MA..MD (MEMRelay1..4) — internal latches/intermediates. | | MEM alarm flag | 1 | MEM_ALARM — auto-set when any used sensor faults. | Sensor types (one global setting, applies to all 4 channels): **NTC10K**, **NTC20K**, **PT1000**, **KTY81 (1k)**. *(PT100 is selectable in older docs but NOT implemented in this build — it silently falls back to NTC10K; see §12.)* Temperature range after linearisation: clamped to **−30 … +205 °C** internally; practical configurable setpoint range is **0 … 90 °C** for "Good/Min" targets and **0 … 100 °C** for "Max/overheat", **0 … 110 °C** for the absolute MaxGood sanity limit (see §8 for exact clamps). Resolution **0.5 °C**. --- ## 2. The core idea — two programs, swapped by season The controller stores **two complete configurations**: * **HEAT** set (a.k.a. Winter) — used when `modes` bit7 = 0 * **COOL** set (a.k.a. Summer) — used when `modes` bit7 = 1 The active set is selected live by the **season bit** (`modes` bit7), which can be changed from the keypad, remotely, or automatically by the **Auto Heat/Cool** rule (§7.6). On every season change the firmware reloads that set's parameters and operand program from its own EEPROM bank. **A `.app` file always carries both sets.** If an installation only uses one season, the unused set should still be valid — the sketch may set `"coolSameAsHeat": true` to have MTC5Coder duplicate the HEAT set into COOL. --- ## 3. The relay logic engine (the heart of "programming") Each **physical relay** is driven by a **sum-of-products** boolean equation: ``` Relay = Term0 OR Term1 OR Term2 OR Term3 (up to 4 OR-terms) Termk = Op0 AND Op1 AND Op2 AND Op3 AND Op4 (exactly 5 AND-operands) ``` * A term is TRUE only when **all 5 of its operands evaluate TRUE** (`check5col_result` requires the sum of the 5 results to equal 5). * The relay is ON if **any** of its 4 terms is TRUE. * Unused operand slots in an active term must be **TRUE** (constant) so they don't block the AND. Unused terms must contain a **FALSE** so they never fire. *(The JSON compiler does this padding automatically — see §9.)* **MEM relays** (MA..MD) use the **same engine** but with **2 OR-terms × 5 operands** each. They are not physical outputs — they are internal boolean results you compute the same way you compute a relay. **A MEM relay's output can be used in two independent ways (and both at once):** 1. **As a condition inside any cell** — put `MEMRelayN is ON/OFF` (§4, item 4) in any cell of **any** relay or MEM-relay term. This is how you (a) factor a condition once and reference it from several relays, (b) build a **latch/flag**, or (c) get **more than the 5 AND-conditions per term / 4 terms per relay** by computing part of the logic in a MEM relay and AND-ing its state into a physical relay's term. 2. **Merged into its like-named physical relay** (MA→RA, MB→RB, MC→RC, MD→RD) — when merge is enabled for that relay (§7.7), the physical relay turns ON if **its own equation is true OR the merged MEM relay is ON**. This adds an extra OR *beyond* the 4 terms, and only couples a MEM relay to the relay of the same letter. Use #1 to feed a MEM result into *arbitrary* logic; use #2 as a quick "also turn this relay on when MA/MB/MC/MD is set." MEM relays are also the drivers for Auto Heat/Cool (§7.6). **MEM_ALARM** is special: firmware sets it automatically whenever any *used* sensor is faulty (open/short). You can read it as a condition (§4 item 5) but not drive it. ### 3.1 Implicit "system ON" gate If a term contains **no** explicit mode operand (`RUN ALWAYS`, `RUN DURING OFF`, `MODES==…`), it is automatically gated by the global ON bit: the term can only fire when the controller is ON (`modes` bit0 = 1). To make a term run while the controller is OFF, include a `RUN DURING OFF` or `RUN ALWAYS` operand. --- ## 4. Operands — the vocabulary **A "cell" = one operand = one condition.** In the MTC5Coder grid each editable box is a cell. A physical relay is a 4×5 grid: **4 rows = OR-terms, 5 columns = the AND-cells** of that term (a term fires only when all 5 cells are TRUE; see §3). A MEM relay is 2×5. Every cell holds exactly **one** condition from the complete set below, and any cell may be **negated** (`"negate": true`). ### 4.0 Complete set of conditions a cell can hold Pick exactly one form per cell: 1. **Sensor comparison** — `Tn >/< reference` (n = 1..4). The reference is one of the 21 values in the §4.1 table. Hysteresis is automatic. 2. **Digital input** — `DI1`/`DI2` is `ON`/`OFF`. 3. **Physical-relay state** — `Relay1..Relay4` (= RA..RD) is `ON`/`OFF`. 4. **MEM-relay state** — `MEMRelay1..MEMRelay4` (= MA..MD) is `ON`/`OFF`. 5. **Alarm** — `MEM_ALARM` is `ON`/`OFF` (device-set on sensor fault). 6. **Schedule zone** — `Zone1`/`Zone2` is `ACTIVE`/`INACTIVE`. 7. **Timer condition** — `Timer1..Timer4` with one of 6 tests: `running`, `endpoint`, `atOrAboveThreshold`, `belowThreshold`, `crossedUp`, `crossedDown` (§6.2). 8. **MATH vs constant** — `MATHn >/< MathkConst` (n,k = 1..3 → 9 comparators, §5.1). 9. **Mode / run gate** — `MODES==ON`, `MODES==HEAT`, `MODES==COOL`, `RUN DURING OFF`, `RUN ALWAYS` (§3.1). 10. **Constant** — `TRUE` / `FALSE` (used for padding/disabling; §3). That is the *entire* vocabulary — there is nothing else a cell can test. The detail of each form follows. (The internal `(Tx,Sign,Ty)` word encoding and the per-operand sign convention are handled by MTC5Coder; the exact recipe is in Appendix A.) ### 4.1 Comparison operand: `sensor >/< reference` Compares one **sensor** against one **reference value**, with built-in hysteresis. * **sensor (left side):** `T1`, `T2`, `T3`, `T4`. * **operator:** `>` or `<`. * **reference (right side), choose one:** | Reference | Meaning | |---|---| | `T1+dT1` … `T4+dT4` | sensor value **plus its offset dTn plus its hysteresis dTnHIST**. Use this to build differentials (e.g. *T1 > T2+dT2*). | | `T1.Good`,`T1.Min`,`T1.Max` … `T4.*` | the 12 temperature setpoints. | | `T.Deice1`, `T.Deice2` | the 2 de-ice setpoints. | | `MATH1`, `MATH2`, `MATH3` | a computed MATH value (§5). | The numeric **range of every reference/setpoint** (and all other parameters) is in §8. **Hysteresis:** when the relay using this operand was already ON, the reference is nudged by ±0.5 °C (`CHISTVALUE`) in the closing direction so the relay doesn't chatter. For the `Tn+dTn` references the per-channel `dTn.HIST` (0..10 °C) is the hysteresis band. > A sensor that is faulted (open/short) forces its comparison to FALSE **and raises the > alarm**. Design fail-safe accordingly. ### 4.2 Digital-input operand * `DI1`, `DI2`, each tested for state `ON` or `OFF`. ### 4.3 Boolean-signal operands (no comparison) Tested for an ON/active state (or its negation): | Signal | Meaning | |---|---| | `Relay1`..`Relay4` | live state of physical relay A..D (complete state, incl. min-on-time). | | `MEMRelay1`..`MEMRelay4` | live state of MEM relay MA..MD. | | `MEM_ALARM` | any used sensor is faulty. | | `Zone1` | schedule-1 time window active. | | `Zone2` | schedule-2 window active (the "night" window; also forced off during vacation/skip-days). | | `Timer1`..`Timer4` tests | see §6.2 (running / value≥threshold / value/< Math1Const` … | the 9 MATH-vs-constant comparators (§5.1). | | `RUN ALWAYS` | always true; also lifts the system-OFF gate (§3.1). | | `RUN DURING OFF` | true while controller is OFF; lifts the OFF gate. | | `MODES==ON` | controller ON (modes bit0). | | `MODES==COOL` | season = COOL (modes bit7 set). | | `MODES==HEAT` | season = HEAT (modes bit7 clear). | | `TRUE` / `FALSE` | constants (for padding / disabling terms). | Any operand may be **negated** (`"negate": true`). --- ## 5. MATH engine (3 cells) Three independent cells, each computed every 0.1 s: ``` MATHn = Left (+ or −) Right (signed 16-bit; wraps; NO multiply/divide) ``` * **Left / Right operands:** `T1`..`T4` (sensors), any single parameter by name (setpoints, dT*, dT*HIST, MATH values, gains…), or the composites `T1+dT1`..`T4+dT4`. * **Operation:** `+` or `−` only. **There is no ×, ÷, min, max, abs, etc.** * **Destination:** by default the cell's own value `MATH1/2/3_VALUE`, OR it can be routed to overwrite one of: `T1.Min`, `T2.Min`, `T3.Min`, `T4.Min`, `T1.Max`, `Timer3.Thd`, `Timer4.Thd`. (This is how you make a **floating setpoint** — e.g. `T2.Min = T1 − 5` recomputed continuously.) A MATH value can then be used as a comparison reference (§4.1) or compared to a constant (§5.1). ### 5.1 MATH-vs-constant comparators (relay operands) `MATHn >/< MathkConst` for n,k ∈ {1,2,3} → 9 boolean operands. Three tunable constants `Math1Const`, `Math2Const`, `Math3Const` (raw device units). > **Units in MATH:** MATH works in **raw device units**. Temperature raw = °C × 2 > (0.5 °C resolution). So "subtract 5 °C" is a constant of **10** in raw. Keep MATH > operands homogeneous (don't mix a temperature with a second count). `MathkConst`, > timer values and AnOut refs are all raw. --- ## 6. Timer engine (4 unified timers) Four identical counters. Each is fully described by: direction, end behaviour, time base, a **source** signal, a **scenario**, plus its `Max` and `Threshold` counts. ### 6.1 Timer configuration | Field | Options | |---|---| | **direction** | `up` (count 0→Max) or `down` (count Max→0). | | **atEnd** | `stop` (fire a one-tick end pulse, halt) or `reload` (snap back to start, fire pulse, repeat → periodic generator). | | **base** | `0.1s`, `1s`, `10s`, `1min` (the count unit). | | **max** | endpoint count, 0..65535 (duration = max × base). | | **threshold** | comparison point, 0..65535 (used by the `≥threshold` operands). | | **source** | what the timer listens to: `none`, `Relay1`..`Relay4`, `MEMRelay1`..`MEMRelay4`, `DI1`, `DI2`, `Timer1`..`Timer4` (the other timers' end pulses — for chaining). | | **scenario** | how the source drives the count — 15 options below. | **Scenarios** (`source` interacts with the counter): | Name | Behaviour | |---|---| | `alwaysRun` | source ignored; ticks every cycle. | | `runWhileHigh_holdLow` | tick while source ON; hold when OFF (run-hour accumulator). | | `runWhileHigh_resetLow` | tick while ON; snap to start when OFF. | | `runWhileLow_holdHigh` | tick while source OFF; hold when ON. | | `runWhileLow_resetHigh` | tick while OFF; snap to start when ON. | | `riseStart_thenNonstop` | rising edge arms+resets, then runs nonstop (one-shot delay). | | `riseReset_nonstop` | every rising edge resets; runs nonstop (watchdog). | | `fallStart_thenNonstop` | as above on falling edge. | | `fallReset_nonstop` | as above on falling edge. | | `riseHold_runFromInit` | rising edge halts; value held. | | `riseReset_nonstop2` | (duplicate of riseReset_nonstop). | | `runHigh_fallReset` | run while ON; falling edge resets. | | `runHigh_riseReset` | run while ON; rising edge re-arms. | | `riseLoadThd_start` | rising edge sets value=threshold then runs (skip-to-threshold). | | `fallLoadThd_start` | falling edge sets value=threshold then runs. | ### 6.2 Timer operands (use a timer's state in relay/MEM logic) Per timer: `running` (value>0), `endpoint` (one-tick pulse at end), `atOrAboveThreshold` (value ≥ threshold), `belowThreshold` (value < threshold), `crossedUp` (one-tick pulse crossing threshold upward), `crossedDown` (one-tick pulse downward). **Common patterns:** delay-on (`runWhileHigh_holdLow` + `atOrAboveThreshold`), delay-off (down + `riseStart`), pulse generator (up + `reload` + `endpoint`), duty-cycle/slow-PWM (up + `reload` with threshold = on-time → use `belowThreshold`/`atOrAboveThreshold`). --- ## 7. Other subsystems ### 7.1 Analog outputs (2) Terminals **AN0 / AN1** (= AnOut1 / AnOut2), each **jumper-selectable 0–10 V or PWM (10 V)** — the firmware/config is identical either way; the jumper only sets the electrical form, so note the installer's choice in the I/O map (§7.8) but it needs no parameter. Each channel: `data = Tx − Ty` (signed) from two numeric operands, fed through a regulator into a 0..100 % PWM. | Field | Meaning | |---|---| | **mode** | `linear` (map data[refLo..refHi] → PWM[min..max]), `P`, `PI` (incremental/velocity regulators), `hold` (freeze). | | **input.tx / input.ty** | numeric operands: `OFF`(0), `FULL`(full-scale const), `T1`..`T4`, any parameter by name, or composites `T1+dT1`..`T4+dT4`. Reverse action = swap tx/ty or swap min/max. | | **pwmMinPct / pwmMaxPct** | output clamp, 0..100 %. | | **refLo / refHi** | the data span that maps to min..max (raw units; temp raw = °C×2). | | **slewMaxPerSec** | 0..63 PWM ticks/s; 0 = unlimited. Also sets P/PI reaction speed. | | **deadband** | 0..255 raw error; \|error\| ≤ deadband holds PWM (P/PI only). | | **relayOverride** | optional: when a chosen relay (RA..RD/MA..MD) is in a chosen state, force PWM to min/max/0 or disabled. | > P/PI gains are derived **internally** from `slewMaxPerSec` and the ref span — the > installer tunes "response speed", not raw Kp/Ki. ### 7.2 Schedules (2) Time-of-day windows from the RTC. Express as minutes-of-day (0..1439). * `schedule1` → operand **Zone1** (active inside [on,off), supports wrap past midnight). * `schedule2` → operand **Zone2** (the firmware's "night" window). A vacation/skip-days mechanism can suppress Zone1/Zone2 (date-based; advanced, rarely set via sketch). ### 7.3 Per-channel minimum ON-time Anti-chatter hold of ~**1 second** per relay. **Fixed in this build, NOT configurable** through the `.app` (no parameter maps to it). Do not promise tunable min-on/off times. ### 7.4 Sensor calibration `Calibr_A..D` exist but are **forced to 100 (no-op) every scan** in this build — **per-sensor calibration is effectively disabled**. Do not promise sensor trimming. ### 7.5 Absolute "MaxGood" sensor sanity limits `MaxGoodT1..4` (0..110 °C) cap each `Tn.Good`: the firmware clamps `Tn.Good` to at most `MaxGoodTn` on every scan. The installer uses `MaxGoodTn` as a **hard ceiling** so the end user cannot raise the target above a safe value from the mobile app (§7.9). See §12 for a clamp defect on T4. ### 7.6 Auto Heat/Cool (season switch rule) One rule that sets the season bit automatically. Options (`device.autoHeatCool`): `manual`, `forceHeat`, `forceCool`, `MA_on→cool`, `MA_on→heat`, `MB_on→cool`, `MB_on→heat`, `MC_on→cool`, `MC_on→heat`, `MD_on→cool`, `MD_on→heat`, `DI1_on→heat/off→cool`, `DI1_on→cool/off→heat`, `DI2_on→heat/off→cool`, `DI2_on→cool/off→heat`. ### 7.7 Relay merge & changeover sequencer * **merge:** OR a MEM relay into its **like-named** physical relay (MA→RA, MB→RB, MC→RC, MD→RD) — enabled per relay. When on, the physical relay = (its own 4-term equation) **OR** (the MEM relay state), i.e. an extra OR term sourced from the same-letter MEM relay. This is the "merge" use in §3; it does NOT let you merge MA into RB — for that, reference `MEMRelay1 is ON` as a condition in RB's cell instead. * **changeover:** protect a compressor while a 4-way valve flips. Exactly **one LEAD** relay (the valve) and one or more **PROTECTED** relays (compressors held OFF during the transition). Two rest times: `offRestSec` (protected-off before the lead moves) and `settleRestSec` (after the lead moves, before protected restart), each 0..250 s. ### 7.8 I/O mapping (physical port → plant role) A textual map from each physical terminal to what it is wired to in the plant. It is **app-side metadata only** (the device stores no text), saved inside the `.app` file as a `# MAP key=value` comment block and editable in MTC5Coder via **File ▸ I/O Mapping…**. It is the **same for both seasons** (physical wiring doesn't change with HEAT/COOL). The 12 ports (JSON keys): `sensor1..sensor4` (TS1..TS4), `button1`,`button2` (DI1,DI2), `relayA..relayD` (RA..RD), `anOut1`,`anOut2` (AN0,AN1). **The agent must propose this mapping** from the installer's description — especially **which real sensor is wired to TS1/TS2/TS3/TS4**, since every temperature comparison in the logic refers to `T1..T4` by port. Decide the sensor→port assignment first, then write the relay equations consistently with it. Put roles for relays (pump/valve/fan/heater/…), analog outputs, and any buttons in the map too. Use `"(unused)"` for ports not wired. Example: `sensor1="Collector temperature"`, `relayA="Solar pump"`, `anOut1="Variable-speed pump (0-10V)"`. ### 7.9 End-user adjustment via the mobile app After commissioning, the end user monitors the plant and adjusts behaviour through a **mobile app** (separate from MTC5Coder, which is the installer's tool). The end user can change parameters live, but the **intended/safe knobs are the target setpoints `T1.Good`..`T4.Good`** — each is **pre-limited internally to `MaxGoodTn`** (§7.5), so the installer bounds how far the user can push the target while still leaving day-to-day comfort tuning in the user's hands. **Design guidance for the agent:** express the user-facing comfort target as `Tn.Good` and set `MaxGoodTn` to that channel's safe ceiling. Keep protective limits (`Tn.Max`, overheat thresholds, timers, and the operand logic itself) out of the user's normal reach so end-user edits cannot defeat safety. Note in the design which `Tn.Good` the user is expected to adjust. --- ## 8. Parameters — names, units, ranges (agent-settable) Express the *simple* parameters below in **engineering units**; MTC5Coder applies the wire scaling. Temperatures are 0.5 °C resolution. Values are **per set** (HEAT/COOL); keep device-global ones identical across sets. | Name (use in JSON `params`) | Unit | Range (this build) | Meaning | |---|---|---|---| | `dT1`..`dT4` | °C | −20 … +20 | per-channel offset added to `Tn+dTn` references. | | `dT1.HIST`..`dT4.HIST` | °C | 0 … 10 | hysteresis band for `Tn+dTn`. | | `T1.Good`..`T4.Good` | °C | 0 … 90 (≤ MaxGoodTn) | target/"satisfied" setpoint. | | `T1.Min`..`T4.Min` | °C | 0 … 90 | minimum-enable setpoint. | | `T1.Max`..`T4.Max` | °C | 0 … 100 | overheat/limit setpoint. | | `T.Deice1`,`T.Deice2` | °C | −20 … +20 | de-ice thresholds. | | `MaxGoodT1`..`MaxGoodT4` | °C | 0 … 110 | absolute sanity cap on each Good (see §12 for T4). | | `Math1Const`..`Math3Const` | raw | 0 … 65535 | MATH comparator constants (temp raw = °C×2). | | `Schedule1_ON/OFF`, `Schedule2_ON/OFF` | min-of-day | 0 … 1439 | schedule windows. | | `TimeoutLead`, `TimeoutFollow` | s | 0 … 250 | changeover rest times. | | `SensorType` | enum | NTC10K / NTC20K / PT1000 / KTY81 | global sensor type. | **Set via structured blocks, not `params`** (MTC5Coder packs these into the raw words). Every field and its range: | Block | Field | Type / Range | Notes | |---|---|---|---| | `timers[]` (×4) | `id` | 1..4 | Timer1..4 | | | `direction` | `up` \| `down` | up = 0→max, down = max→0 | | | `atEnd` | `stop` \| `reload` | | | | `base` | `0.1s` \| `1s` \| `10s` \| `1min` | count unit | | | `max` | 0 … 65535 | endpoint count (duration = max × base) | | | `threshold` | 0 … 65535 | compare point for the timer conditions | | | `source` | `none`,`Relay1..4`,`MEMRelay1..4`,`DI1`,`DI2`,`Timer1..4` | drives the count | | | `scenario` | one of the 15 names in §6.1 | how source acts on the counter | | `math[]` (×3) | `cell` | 1..3 | | | | `left`,`right` | operand name | `T1..T4`, a parameter name, `MATH1..3`, or `T1+dT1..T4+dT4` | | | `op` | `+` \| `-` | no ×/÷ | | | `dest` | `MATH1/2/3_VALUE`,`T1.Min`,`T2.Min`,`T3.Min`,`T4.Min`,`T1.Max`,`Timer3.Thd`,`Timer4.Thd` | default = own MATH value | | `anOut[]` (×2) | `id` | 1..2 | AN0 / AN1 | | | `mode` | `linear` \| `P` \| `PI` \| `hold` | | | | `input.tx`,`input.ty` | operand name | `OFF`,`FULL`,`T1..T4`, a parameter name, or `T1+dT1..T4+dT4`; data = tx − ty | | | `pwmMinPct`,`pwmMaxPct` | 0 … 100 % | output clamp | | | `refLo`,`refHi` | −32768 … 32767 (raw; temp raw = °C×2) | data span mapped to min..max | | | `slewMaxPerSec` | 0 … 63 | ticks/s; 0 = unlimited | | | `deadband` | 0 … 255 (raw) | P/PI only | | | `relayOverride.relay` | `Relay1..4` \| `MEMRelay1..4` | optional | | | `relayOverride.whenState` | `ON` \| `OFF` | | | | `relayOverride.action` | `disabled` \| `forceMin` \| `forceMax` \| `killPWM` | | | `schedules` | `schedule1/2.onMinute`,`offMinute` | 0 … 1439 (min-of-day) | Zone1 / Zone2 | | `relayMerge` | `A`,`B`,`C`,`D` | bool | merge MEM into same-letter relay (§7.7) | | `changeover` | `lead` | `A`\|`B`\|`C`\|`D`\|null | exactly one | | | `protected` | subset of `A..D` | held OFF during transition | | | `offRestSec`,`settleRestSec` | 0 … 250 s | = TimeoutLead / TimeoutFollow | | `device` | `sensorType` | NTC10K/NTC20K/PT1000/KTY81 | global | | | `powerOn` | bool | modes bit0 | | | `autoHeatCool` | one of the 16 names in §7.6 | season-switch rule | | `relay/MEM terms` | operands | see §4 | → ProgramRe / ProgramMem words | Values outside a range are silently clamped by the firmware (see §11); stay within range. **Read-only / device-computed (never settable):** `MATH1/2/3_VALUE`, live `Timer1..4`, `PRODUCT_Q1234`, `PRODUCT_MQ1234`, `MEM_RELAYS`, `Temperatura_A..D`, `Reading_DInputs`, `CUrrent_time`, `WiFiOK`, `AnOut1/2` live, `PowerONTimer`. --- ## 9. The program sketch (what the agent emits) JSON content, saved as a **`.pgmai`** file (see "Output file" above). Formal schema: **`mtc5_program.schema.json`**. Example: **`example_program.pgmai`**. Shape: ```jsonc { "schema": "mtc5_program/v1", "meta": { "title": "...", "author": "...", "notes": "..." }, "feasibility": { // ALWAYS fill this in, even when feasible "verdict": "feasible | partial | infeasible", "reasons": [ "short honest bullet referencing a concrete limit" ], "assumptions": [ "what you assumed about wiring/sensors" ] }, "device": { "sensorType": "NTC10K", "powerOn": true, // modes bit0 "autoHeatCool": "manual" // §7.6 }, "ioMapping": { // §7.8 — physical port → plant role (AI proposes this) "sensor1": "Collector temperature", // which real sensor is on TS1..TS4 — decide FIRST "sensor2": "Tank bottom", "sensor3": "(unused)", "sensor4": "(unused)", "button1": "(unused)", "button2": "(unused)", "relayA": "Solar pump", "relayB": "(unused)", "relayC": "(unused)", "relayD": "(unused)", "anOut1": "(unused)", "anOut2": "(unused)" }, "coolSameAsHeat": false, // true → duplicate heat into cool "sets": { "heat": { /* SetConfig */ }, "cool": { /* SetConfig */ } } } ``` ### 9.1 `SetConfig` ```jsonc { "params": { "T1.Good": 50.0, "dT1": 6.0, "dT1.HIST": 2.0, "T1.Min": 10.0, "Schedule1_ON": 360, "Schedule1_OFF": 1320 }, "relays": { "A": { "terms": [ [ , , ... up to 5 ], ... up to 4 terms ] }, "B": { "terms": [ ... ] }, "C": { ... }, "D": { ... } }, "memRelays": { // up to 2 terms × 5 operands each "MA": { "terms": [ ... ] }, "MB": {...}, "MC": {...}, "MD": {...} }, "math": [ { "cell": 1, "left": "T1", "op": "-", "right": "T2", "dest": "MATH1_VALUE" } ], "mathConstants": { "Math1Const": 10 }, "timers": [ { "id": 1, "direction": "up", "atEnd": "stop", "base": "1s", "max": 300, "threshold": 300, "source": "Relay1", "scenario": "runWhileHigh_holdLow" } ], "anOut": [ { "id": 1, "mode": "PI", "input": { "tx": "T1", "ty": "T1.Good" }, "pwmMinPct": 0, "pwmMaxPct": 100, "refLo": -10, "refHi": 10, "slewMaxPerSec": 10, "deadband": 1, "relayOverride": { "relay": "Relay2", "whenState": "ON", "action": "killPWM" } } ], "schedules": { "schedule1": { "onMinute": 360, "offMinute": 1320 }, "schedule2": { "onMinute": 1380, "offMinute": 360 } }, "relayMerge": { "A": false, "B": false, "C": false, "D": false }, "changeover": { "lead": null, "protected": [], "offRestSec": 30, "settleRestSec": 30 } } ``` All blocks are optional; omit what the install doesn't use (compiler fills defaults: relays/MEM with all-FALSE terms = never on, timers/anOut inert, etc.). ### 9.2 Operand forms (used inside `terms[]`) ```jsonc { "compare": { "left": "T1", "op": ">", "right": "T2+dT2" } } // §4.1 { "input": "DI1", "is": "ON" } // §4.2 { "relay": "Relay1", "is": "ON" } // §4.3 (Relay/MEMRelay) { "alarm": "ON" } // MEM_ALARM is ON { "zone": "Zone1", "is": "ACTIVE" } { "timer": "Timer1", "test": "atOrAboveThreshold" } // §6.2 { "mathCompare": { "math": "MATH1", "op": ">", "const": "Math1Const" } } { "mode": "ON" } // ON | OFF_RUN | COOL | HEAT | ALWAYS { "const": true } // or false ``` Optional on any operand: `"negate": true`, `"note": "human reason"`. A `term` lists 1..5 operands; the compiler pads to 5 with `TRUE`. A relay/MEM with fewer than its max terms (4 / 2) has the rest filled with a `FALSE` term. --- ## 10. `.app` file format (reference for the MTC5Coder writer) Plain text. `#` lines are comments; blank lines ignored; values `0xHHHH` separated by `,` and/or newlines. **422 words total, in this order:** 1. Operands — **HEAT** set: 80 RELAY words then 40 MEM words (120). 2. Operands — **COOL** set: 80 + 40 (120). 3. Parameters — **HEAT** set: 91 words. 4. Parameters — **COOL** set: 91 words. RELAY block layout = 4 relays × (4 terms × 5 operands) = 80 words, relay A,B,C,D in order, each as term0[op0..4], term1[…], term2, term3. MEM block = 4 mem relays × (2 terms × 5) = 40. Operand word: `Ty[6:0] | Tx[10:9..] (3 bits at <<9) | NOT (1<<13)`. Parameter words are raw 16-bit wire values (apply each parameter's scaling per §8 / `Params.cs`). Parameter routing to device nodes is internal to MTC5Coder (`MemMap.cs`) and not the agent's concern. --- ## 11. How to judge feasibility (hard limits — quote these when saying "no") * **4 relays, 2 analog outputs, 4 temp sensors, 2 digital inputs.** Nothing more. * **≤ 4 OR-terms per relay; ≤ 5 AND-operands per term.** Need more ANDs? Push part of the logic into a MEM relay and AND its state in (MEM relays add 4×(2×5) more slots). * **≤ 2 OR-terms per MEM relay.** * **A comparison is always one sensor vs one reference.** No "sensor vs sensor" except via the `Tn+dTn` references or a MATH cell. No three-way compares in one operand. * **MATH = add/subtract only**, 3 cells, signed 16-bit wrap. **No ×, ÷, min/max/abs.** * **Floating setpoints** only into: T1..4.Min, T1.Max, Timer3/4.Thd (via MATH dest). * **4 timers**, one source + one scenario each; chain via end-pulse sources. * **Min on/off time, sensor calibration: not configurable** (§7.3, §7.4). * **PT100 not supported** (§1, §12). * **Regulation (P/PI) is available only on the 2 analog outputs**, never on relays (relays are pure boolean + min-on-time + optional changeover). * **One Auto-Heat/Cool rule, one changeover group (single LEAD).** * Temperature setpoints clamp to the ranges in §8; values outside are silently clamped. If a request exceeds these, set `feasibility.verdict` to `partial`/`infeasible` and say which limit, in plain language, with the closest achievable alternative. --- ## 12. Firmware notes (known defects / build-specific truths — do not over-promise) These are properties of the **current** firmware that affect what is truthful to claim. *(The major app↔firmware operand mismatches found during this review were already fixed; the items below are either build facts or open defects flagged to the owner.)* 1. **`MaxGoodT4` clamp defect** (`check_param_05deg.h`): values > 220 raw (110 °C) collapse to 20 raw (10 °C) instead of 220 (a copy-paste bug; the other 3 channels clamp correctly to 110 °C). Keep `MaxGoodT4` ≤ 110 °C and be aware setting it high can crater the T4 Good ceiling. **Open — flagged to owner.** 2. **PT100 not implemented** — selecting it falls back to NTC10K. Offer NTC10K/NTC20K/ PT1000/KTY81 only. 3. **Per-sensor calibration disabled** — `Calibr_A..D` are forced to 100 each scan. 4. **Minimum on-time fixed ≈ 1 s** per relay; not exposed as a parameter. 5. **`eval_logic_operand` / `Serve_timerX` appear to be legacy** (the live timer driver `tick_timer` reads sources via `eval_timer_source`). Left in place per owner; do not rely on them. 6. **Defaults (`SetupPar`) are partly stale** vs the current parameter map, but this is moot for the agent: a `.app` upload writes **all** parameters for both sets, so the sketch fully determines device state regardless of power-on defaults. --- ## Appendix A — operand → `(Tx, Sign, Ty)` recipe (for the MTC5Coder JSON reader) Word = `BuildWord(TxVal, SignVal, TyId)` = `Ty&0x7F | (Tx&7)<<9 | (Sign&1)<<13`. `TyId` values come from `Form1.TyItems`; `TxVal` from `TxItems` (T1=0,T2=1,T3=2,T4=3,DI1=5,DI2=6); `Sign` 0=`>`,1=`<`. | JSON operand | TyId | TxVal | Sign | Notes | |---|---|---|---|---| | `compare {left:Tn, op, right}` | TyId of `right` (2..22) | Tn (0..3) | `>`→0, `<`→1 | hysteresis automatic in firmware. | | `input DIk is ON/OFF` | any 2..22* | DI1=5/DI2=6 | ON→1, OFF→0 | *firmware reads Tx first for DI; app stores a benign Ty. | | `relay RelayN is ON/OFF` | 23..26 (RA..RD) | — | **ON→`<`(1), OFF→`>`(0)** | relay/MEM use the Flip convention. | | `relay MEMRelayN is ON/OFF` | 27..30 | — | ON→`<`, OFF→`>` | | | `alarm ON/OFF` | 31 | — | ON→`<`, OFF→`>` | | | `zone ZoneN is ACTIVE` | 32 (Zone1) / 33 (Zone2) | — | ACTIVE→`<`, else `>` | | | `timer Timer.. test ..` | 34..37, 64..87 | — | direct: assert→`>`(0), negate→`<`(1) | see §6.2 ↔ Operand_Server IDs. | | `mathCompare {math,op,const}` | 38..46 | — | `>`→0, `<`→1 | MATHn vs MathkConst, pure compare. | | `mode ON` | 49 | — | `>`→assert | | | `mode COOL` | 50 | — | `>`→assert | (matches firmware: 50=COOL.) | | `mode HEAT` | 51 | — | `>`→assert | (51=HEAT.) | | `mode OFF_RUN` (RUN DURING OFF) | 48 | — | — | returns explicit-run override. | | `mode ALWAYS` (RUN ALWAYS) | 47 | — | — | returns explicit-run override. | | `const true/false` | 1 / 0 | — | — | TRUE=1, FALSE=0. | > The Flip vs direct sign convention is the subtle part — implement it once here and the > agent never has to think about bit 13.