diff --git a/codesys/src/DUT/struct_boiler_cmd.typ b/codesys/src/DUT/struct_boiler_cmd.typ new file mode 100644 index 0000000..c86a8dd --- /dev/null +++ b/codesys/src/DUT/struct_boiler_cmd.typ @@ -0,0 +1,14 @@ +(* + DUT: struct_boiler_cmd + Boiler commands from Node-RED/Home Assistant. +*) +TYPE struct_boiler_cmd : +STRUCT + ha_on: BOOL; + ha_off: BOOL; + schedule_on: BOOL; + schedule_off: BOOL; + emergency_stop: BOOL; + max_on_time_minutes: INT; +END_STRUCT +END_TYPE diff --git a/codesys/src/DUT/struct_boiler_status.typ b/codesys/src/DUT/struct_boiler_status.typ new file mode 100644 index 0000000..b52e682 --- /dev/null +++ b/codesys/src/DUT/struct_boiler_status.typ @@ -0,0 +1,16 @@ +(* + DUT: struct_boiler_status + Boiler status to Node-RED/Home Assistant. +*) +TYPE struct_boiler_status : +STRUCT + state: BOOL; + relay_output: BOOL; + on_time_minutes: INT; + remaining_minutes: INT; + emergency_active: BOOL; + time_limit_reached: BOOL; + error_state: BOOL; + error_code: INT; +END_STRUCT +END_TYPE diff --git a/codesys/src/DUT/struct_room_cmds.typ b/codesys/src/DUT/struct_room_cmds.typ new file mode 100644 index 0000000..7f110b7 --- /dev/null +++ b/codesys/src/DUT/struct_room_cmds.typ @@ -0,0 +1,32 @@ +(* + DUT: struct_room_cmds + Room lighting commands from Node-RED/Home Assistant (flat for JSON). + Used as input to fb_room. +*) +TYPE struct_room_cmds : +STRUCT + // Home Assistant Commands (set/reset) + ha_l1_on: BOOL; + ha_l1_off: BOOL; + ha_l2_on: BOOL; + ha_l2_off: BOOL; + ha_l3_on: BOOL; + ha_l3_off: BOOL; + ha_l4_on: BOOL; + ha_l4_off: BOOL; + ha_l5_on: BOOL; + ha_l5_off: BOOL; + ha_l6_on: BOOL; + ha_l6_off: BOOL; + // Zigbee Switch Inputs (edge detection for toggle) + zigbee_sw1: BOOL; + zigbee_sw2: BOOL; + zigbee_sw3: BOOL; + zigbee_sw4: BOOL; + zigbee_sw5: BOOL; + zigbee_sw6: BOOL; + // Global Commands + ha_all_on: BOOL; + ha_all_off: BOOL; +END_STRUCT +END_TYPE diff --git a/codesys/src/DUT/struct_room_outs.typ b/codesys/src/DUT/struct_room_outs.typ new file mode 100644 index 0000000..0b953da --- /dev/null +++ b/codesys/src/DUT/struct_room_outs.typ @@ -0,0 +1,15 @@ +(* + DUT: struct_room_outs + Room light outputs (and status to Node-RED). With Option C, one set is enough: + we send this to the relay and to Node-RED as "light status". +*) +TYPE struct_room_outs : +STRUCT + l_1: BOOL; + l_2: BOOL; + l_3: BOOL; + l_4: BOOL; + l_5: BOOL; + l_6: BOOL; +END_STRUCT +END_TYPE diff --git a/codesys/src/GVL/GVL_IO.gvl b/codesys/src/GVL/GVL_IO.gvl new file mode 100644 index 0000000..b7c845e --- /dev/null +++ b/codesys/src/GVL/GVL_IO.gvl @@ -0,0 +1,27 @@ +(* + GVL: GVL_IO + Option C relay feedback only. EL2809 DO: map channels directly in the device tree + to NVL_Out (e.g. NVL_Out.l_masterBedroom.l_1..l_6, ...) and boiler relay. + Initialize EtherCAT_RelayFeedback to zero at startup (or use default init). +*) +VAR_GLOBAL + // ---------- Option C: relay feedback (copy of output for next cycle) ---------- + EtherCAT_RelayFeedback: STRUCT + masterBedroom: struct_room_outs; + masterBathroom: struct_room_outs; + bedroom_1: struct_room_outs; + bedroom_2: struct_room_outs; + bathroom: struct_room_outs; + guest_wc: struct_room_outs; + kitchen: struct_room_outs; + pantry: struct_room_outs; + livingRoom: struct_room_outs; + diningRoom: struct_room_outs; + entrance: struct_room_outs; + hallway: struct_room_outs; + veranda: struct_room_outs; + front: struct_room_outs; + back: struct_room_outs; + side: struct_room_outs; + END_STRUCT; +END_VAR diff --git a/codesys/src/GVL/GVL_NVL_placeholders.gvl b/codesys/src/GVL/GVL_NVL_placeholders.gvl new file mode 100644 index 0000000..5e39045 --- /dev/null +++ b/codesys/src/GVL/GVL_NVL_placeholders.gvl @@ -0,0 +1,48 @@ +(* + GVL: NVL stubs (or replace by CODESYS Network Variable lists NVL_In / NVL_Out). + In the real project, bind these to your UDP/network variable sender/receiver. + DI_Emergency_Stop: link to physical input or leave FALSE. +*) +VAR_GLOBAL + NVL_In: STRUCT + masterBedroom: struct_room_cmds; + masterBathroom: struct_room_cmds; + bedroom_1: struct_room_cmds; + bedroom_2: struct_room_cmds; + bathroom: struct_room_cmds; + guestWc: struct_room_cmds; + kitchen: struct_room_cmds; + pantry: struct_room_cmds; + livingRoom: struct_room_cmds; + dinningRoom: struct_room_cmds; + entrance: struct_room_cmds; + hallway: struct_room_cmds; + outVeranda: struct_room_cmds; + outFront: struct_room_cmds; + outBack: struct_room_cmds; + outSide: struct_room_cmds; + boiler: struct_boiler_cmd; + END_STRUCT; + + NVL_Out: STRUCT + l_masterBedroom: struct_room_outs; + l_masterBathroom: struct_room_outs; + l_bedroom_1: struct_room_outs; + l_bedroom_2: struct_room_outs; + l_bathroom: struct_room_outs; + l_guestWc: struct_room_outs; + l_kitchen: struct_room_outs; + l_pantry: struct_room_outs; + l_livingRoom: struct_room_outs; + l_dinningRoom: struct_room_outs; + l_entrance: struct_room_outs; + l_hallway: struct_room_outs; + l_outVeranda: struct_room_outs; + l_outFront: struct_room_outs; + l_outBack: struct_room_outs; + l_outSide: struct_room_outs; + boiler_status: struct_boiler_status; + END_STRUCT; + + DI_Emergency_Stop: BOOL := FALSE; +END_VAR diff --git a/codesys/src/NVL/README.md b/codesys/src/NVL/README.md new file mode 100644 index 0000000..822ca6d --- /dev/null +++ b/codesys/src/NVL/README.md @@ -0,0 +1,82 @@ +# Network Variables (NVL) for Node-RED + +This folder describes the **network variable** setup used for CODESYS ↔ Node-RED communication. The PLC sends light/boiler **status** to Node-RED and receives **commands** from Node-RED over UDP. + +## Overview + +| Direction | CODESYS name | Node-RED role | Content | +|-----------|--------------|---------------|---------| +| **PLC → Node-RED** | **NVL_Out** (Sender) | Receive UDP | Light states + boiler status | +| **Node-RED → PLC** | **NVL_In** (Receiver) | Send UDP | Light commands + boiler commands | + +- **Protocol**: UDP +- **Typical interval**: 50 ms cyclic (configurable in CODESYS) +- **Payload**: Binary (struct layout) or see [Payload layout for Node-RED](nodered-payload.md) for parsing. + +--- + +## 1. NVL_Out (PLC → Node-RED) + +**Purpose:** PLC sends current light outputs and boiler status so Node-RED can forward them to Home Assistant / MQTT. + +**Variables sent (in order):** + +| Variable | Type | Description | +|----------|------|-------------| +| `l_masterBedroom` | struct_room_outs | 12 BOOLs (l_1..l_6, l_1_status..l_6_status) | +| `l_masterBathroom` | struct_room_outs | same | +| `l_bedroom_1` .. `l_bedroom_2` | struct_room_outs | same | +| `l_bathroom`, `l_guestWc`, `l_kitchen`, `l_pantry` | struct_room_outs | same | +| `l_livingRoom`, `l_dinningRoom`, `l_entrance`, `l_hallway` | struct_room_outs | same | +| `l_outVeranda`, `l_outFront`, `l_outBack`, `l_outSide` | struct_room_outs | same | +| `boiler_status` | struct_boiler_status | state, relay_output, on_time_minutes, remaining_minutes, emergency_active, time_limit_reached, error_state, error_code | + +**In CODESYS:** Create an **NVL Sender** (Network Variable List), bind it to the **NVL_Out** structure (the same struct as in `GVL_NVL_placeholders.gvl` → `NVL_Out`). Set protocol to UDP, destination IP/port to your Node-RED host, task and interval (e.g. EtherCAT_Task, 50 ms). + +--- + +## 2. NVL_In (Node-RED → PLC) + +**Purpose:** Node-RED sends commands (HA ON/OFF, Zigbee toggle edges, boiler on/off, etc.) so the PLC can drive lights and the boiler. + +**Variables received (in order):** + +| Variable | Type | Description | +|----------|------|-------------| +| `masterBedroom` .. `side` (15 rooms) | struct_room_cmds | ha_l1_on, ha_l1_off, ... ha_l6_on, ha_l6_off, zigbee_sw1..6, ha_all_on, ha_all_off | +| `boiler` | struct_boiler_cmd | ha_on, ha_off, schedule_on, schedule_off, emergency_stop, max_on_time_minutes | + +**In CODESYS:** Create an **NVL Receiver**, bind it to the **NVL_In** structure. Set protocol to UDP, listen port, task and interval. The PLC will overwrite `NVL_In` with received data each cycle. + +**In Node-RED:** Send one UDP packet per update (or cyclic) with the same binary layout as `NVL_In` so CODESYS can unpack it into the receiver struct. + +--- + +## 3. CODESYS configuration steps + +1. **Add NVL Sender** + - Device tree → Add Object → Network Variable List (Sender). + - Set protocol to **UDP**, destination IP = Node-RED host, port = e.g. **5555** (receive port on Node-RED). + - Add variables: add the **entire NVL_Out** struct (or add each member; see CODESYS help). + - Set task (e.g. EtherCAT_Task) and interval (e.g. T#50ms). + +2. **Add NVL Receiver** + - Add Object → Network Variable List (Receiver). + - Set protocol to **UDP**, listen port = e.g. **5556**. + - Add variables: add the **entire NVL_In** struct. + - Set task and interval. + +3. **Bind to GVL** + - Either the NVL Sender/Receiver **are** the GVL variables (you map the NVL to the symbols `NVL_Out` and `NVL_In`), or you copy NVL → GVL in the program. Typically you bind the NVL list to a GVL so that `NVL_Out` and `NVL_In` in your code are the same memory the NVL driver sends/receives. + +4. **Ports and IP** + - Fill in your PLC IP and Node-RED IP. Node-RED **receives** on the port you set as “destination port” in the NVL Sender. Node-RED **sends** to the PLC IP and the port you set as “listen port” in the NVL Receiver. + +--- + +## 4. Node-RED usage + +- **Receive (PLC → Node-RED):** Use a **UDP in** node listening on the port configured in the CODESYS NVL Sender (e.g. 5555). The payload is binary; see [nodered-payload.md](nodered-payload.md) for struct layout and how to parse it to JSON for MQTT/HA. +- **Send (Node-RED → PLC):** Use a **UDP out** node targeting the PLC IP and the NVL Receiver port (e.g. 5556). Build the payload buffer from your command (e.g. from MQTT/HA) using the same layout as [nodered-payload.md](nodered-payload.md). + +See **nodered-payload.md** in this folder for byte layout and example parsing/building. diff --git a/codesys/src/NVL/nodered-payload.md b/codesys/src/NVL/nodered-payload.md new file mode 100644 index 0000000..a2cea44 --- /dev/null +++ b/codesys/src/NVL/nodered-payload.md @@ -0,0 +1,154 @@ +# Node-RED: NVL payload layout + +CODESYS sends and receives **binary** UDP packets (struct layout). Use this layout in Node-RED to parse **NVL_Out** (PLC → Node-RED) and to build **NVL_In** (Node-RED → PLC). + +**Important:** Packing and alignment depend on the CODESYS runtime. Typical: BOOL = 1 byte, INT = 2 bytes (little-endian on Intel/ARM). If your platform uses different alignment, adjust offsets. You can log the first few packets and compare with the PLC online values to confirm. + +--- + +## 1. Type sizes (typical) + +| Type | Size (bytes) | +|------|--------------| +| BOOL | 1 | +| INT | 2 | +| DINT | 4 | + +--- + +## 2. NVL_Out (PLC → Node-RED) – receive and parse + +PLC sends one block: all rooms’ `struct_room_outs` followed by `struct_boiler_status`. + +### struct_room_outs (per room) + +6 BOOLs in order: `l_1, l_2, l_3, l_4, l_5, l_6` (light state; with Option C this is both output and status). + +- **Size per room:** 6 bytes. +- **Rooms in order:** l_masterBedroom, l_masterBathroom, l_bedroom_1, l_bedroom_2, l_bathroom, l_guestWc, l_kitchen, l_pantry, l_livingRoom, l_dinningRoom, l_entrance, l_hallway, l_outVeranda, l_outFront, l_outBack, l_outSide. +- **Total for lights:** 16 × 6 = **96 bytes**. + +### struct_boiler_status (after all rooms) + +| Offset (from start of boiler_status) | Type | Field | +|-------------------------------------|------|--------| +| 0 | BOOL | state | +| 1 | BOOL | relay_output | +| 2 | INT | on_time_minutes | +| 4 | INT | remaining_minutes | +| 6 | BOOL | emergency_active | +| 7 | BOOL | time_limit_reached | +| 8 | BOOL | error_state | +| 9 | BOOL | (padding possible) | +| 10 | INT | error_code | + +**Size:** 12 bytes (or 16 if aligned). So **total NVL_Out** ≈ 96 + 12 = **108 bytes** (or 112 if boiler is aligned). + +### Node-RED: parse NVL_Out (example) + +```javascript +// msg.payload = Buffer (UDP payload from PLC) +const buf = msg.payload; +const roomSize = 6; +const roomNames = ['l_masterBedroom','l_masterBathroom','l_bedroom_1','l_bedroom_2','l_bathroom','l_guestWc','l_kitchen','l_pantry','l_livingRoom','l_dinningRoom','l_entrance','l_hallway','l_outVeranda','l_outFront','l_outBack','l_outSide']; + +const out = { rooms: {}, boiler_status: {} }; + +for (let r = 0; r < 16; r++) { + const base = r * roomSize; + out.rooms[roomNames[r]] = { + l_1: !!buf[base+0], l_2: !!buf[base+1], l_3: !!buf[base+2], + l_4: !!buf[base+3], l_5: !!buf[base+4], l_6: !!buf[base+5] + }; +} + +const bo = 96; // offset of boiler_status +out.boiler_status = { + state: !!buf[bo+0], + relay_output: !!buf[bo+1], + on_time_minutes: buf.readInt16LE(bo+2), + remaining_minutes: buf.readInt16LE(bo+4), + emergency_active: !!buf[bo+6], + time_limit_reached: !!buf[bo+7], + error_state: !!buf[bo+8], + error_code: buf.readInt16LE(bo+10) +}; + +msg.payload = out; +return msg; +``` + +--- + +## 3. NVL_In (Node-RED → PLC) – build and send + +PLC expects one block: all rooms’ `struct_room_cmds` followed by `struct_boiler_cmd`. + +### struct_room_cmds (per room) + +20 BOOLs in order: +ha_l1_on, ha_l1_off, ha_l2_on, ha_l2_off, ha_l3_on, ha_l3_off, ha_l4_on, ha_l4_off, ha_l5_on, ha_l5_off, ha_l6_on, ha_l6_off, zigbee_sw1..6, ha_all_on, ha_all_off. + +- **Size per room:** 20 bytes. +- **Rooms in order:** masterBedroom, masterBathroom, bedroom_1, bedroom_2, bathroom, guestWc, kitchen, pantry, livingRoom, dinningRoom, entrance, hallway, outVeranda, outFront, outBack, outSide. +- **Total for rooms:** 16 × 20 = **320 bytes**. + +### struct_boiler_cmd + +| Offset | Type | Field | +|--------|------|--------| +| 0 | BOOL | ha_on | +| 1 | BOOL | ha_off | +| 2 | BOOL | schedule_on | +| 3 | BOOL | schedule_off | +| 4 | BOOL | emergency_stop | +| 5 | (padding) | - | +| 6 | INT | max_on_time_minutes | + +**Size:** 8 bytes. So **total NVL_In** = 320 + 8 = **328 bytes**. + +### Node-RED: build NVL_In (example) + +```javascript +// Build binary payload for PLC from commands +const roomSize = 20; +const roomNames = ['masterBedroom','masterBathroom','bedroom_1','bedroom_2','bathroom','guestWc','kitchen','pantry','livingRoom','dinningRoom','entrance','hallway','outVeranda','outFront','outBack','outSide']; + +const buf = Buffer.alloc(328); +const rooms = msg.payload.rooms || {}; +const boiler = msg.payload.boiler || {}; + +for (let r = 0; r < 16; r++) { + const c = rooms[roomNames[r]] || {}; + const base = r * roomSize; + buf[base+0] = c.ha_l1_on ? 1 : 0; buf[base+1] = c.ha_l1_off ? 1 : 0; + buf[base+2] = c.ha_l2_on ? 1 : 0; buf[base+3] = c.ha_l2_off ? 1 : 0; + buf[base+4] = c.ha_l3_on ? 1 : 0; buf[base+5] = c.ha_l3_off ? 1 : 0; + buf[base+6] = c.ha_l4_on ? 1 : 0; buf[base+7] = c.ha_l4_off ? 1 : 0; + buf[base+8] = c.ha_l5_on ? 1 : 0; buf[base+9] = c.ha_l5_off ? 1 : 0; + buf[base+10]= c.ha_l6_on ? 1 : 0; buf[base+11]= c.ha_l6_off ? 1 : 0; + for (let i = 0; i < 6; i++) buf[base+12+i] = c['zigbee_sw'+(i+1)] ? 1 : 0; + buf[base+18] = c.ha_all_on ? 1 : 0; + buf[base+19] = c.ha_all_off ? 1 : 0; +} + +const bo = 320; +buf[bo+0] = boiler.ha_on ? 1 : 0; +buf[bo+1] = boiler.ha_off ? 1 : 0; +buf[bo+2] = boiler.schedule_on ? 1 : 0; +buf[bo+3] = boiler.schedule_off ? 1 : 0; +buf[bo+4] = boiler.emergency_stop ? 1 : 0; +buf.writeInt16LE(boiler.max_on_time_minutes ?? 480, bo+6); + +msg.payload = buf; +return msg; +``` + +--- + +## 4. Ports and flow + +- **NVL_Out (PLC → Node-RED):** Node-RED **UDP In** listens on e.g. **5555** (same as “destination port” in CODESYS NVL Sender). +- **NVL_In (Node-RED → PLC):** Node-RED **UDP Out** sends to **PLC_IP:5556** (5556 = listen port of CODESYS NVL Receiver). + +Send **NVL_In** when commands change (or at a fixed interval, e.g. 100 ms) so the PLC always has the latest commands. diff --git a/codesys/src/POUs/PLC_App.st b/codesys/src/POUs/PLC_App.st new file mode 100644 index 0000000..3d86ba4 --- /dev/null +++ b/codesys/src/POUs/PLC_App.st @@ -0,0 +1,65 @@ +(* + POU: PLC_App + Main program (Option C: copy of output as relay feedback). + Requires: NVL_In, NVL_Out, GVL_IO (EtherCAT_RelayFeedback). + Map EL2809 DO channels in the device tree directly to NVL_Out (e.g. Ch0..5 = l_masterBedroom.l_1..l_6) + and Ch15 to boiler relay (e.g. NVL_Out.boiler_status.relay_output or a GVL BOOL). + Optional: DI_Emergency_Stop (physical E-Stop input). +*) +PROGRAM PLC_App +VAR + masterBedroom: fb_room; + masterBathroom: fb_room; + bedroom_1: fb_room; + bedroom_2: fb_room; + bathroom: fb_room; + guest_wc: fb_room; + kitchen: fb_room; + pantry: fb_room; + livingRoom: fb_room; + diningRoom: fb_room; + entrance: fb_room; + hallway: fb_room; + veranda: fb_room; + front: fb_room; + back: fb_room; + side: fb_room; + boiler: fb_boiler; +END_VAR + +// ========== SECTION 1: LIGHTING (Option C: relay feedback = copy of output) ========== + +// Master Bedroom +masterBedroom(switches := NVL_In.masterBedroom, relay_status := EtherCAT_RelayFeedback.masterBedroom); +EtherCAT_RelayFeedback.masterBedroom := masterBedroom.lights; +NVL_Out.l_masterBedroom := masterBedroom.lights; + +// Master Bathroom +masterBathroom(switches := NVL_In.masterBathroom, relay_status := EtherCAT_RelayFeedback.masterBathroom); +EtherCAT_RelayFeedback.masterBathroom := masterBathroom.lights; +NVL_Out.l_masterBathroom := masterBathroom.lights; + +// Bedroom 1..2, bathroom, guest_wc, kitchen, pantry, livingRoom, diningRoom, +// entrance, hallway, veranda, front, back, side: same pattern (fb_room, RelayFeedback, NVL_Out). +bedroom_1(switches := NVL_In.bedroom_1, relay_status := EtherCAT_RelayFeedback.bedroom_1); +EtherCAT_RelayFeedback.bedroom_1 := bedroom_1.lights; +NVL_Out.l_bedroom_1 := bedroom_1.lights; + +// ========== SECTION 2: BOILER ========== + +boiler( + ha_on := NVL_In.boiler.ha_on, + ha_off := NVL_In.boiler.ha_off, + schedule_on := NVL_In.boiler.schedule_on, + schedule_off := NVL_In.boiler.schedule_off, + emergency_stop := NVL_In.boiler.emergency_stop OR DI_Emergency_Stop, + max_on_time := T#8H +); +NVL_Out.boiler_status.relay_output := boiler.relay_control; +NVL_Out.boiler_status.state := boiler.state; +NVL_Out.boiler_status.on_time_minutes := DINT_TO_INT(boiler.on_time_seconds / 60); +NVL_Out.boiler_status.remaining_minutes := DINT_TO_INT(boiler.remaining_seconds / 60); +NVL_Out.boiler_status.emergency_active := boiler.emergency_active; +NVL_Out.boiler_status.time_limit_reached := boiler.time_limit_reached; +NVL_Out.boiler_status.error_state := boiler.error_state; +NVL_Out.boiler_status.error_code := boiler.error_code; diff --git a/codesys/src/POUs/fb_boiler.st b/codesys/src/POUs/fb_boiler.st new file mode 100644 index 0000000..a7a4aeb --- /dev/null +++ b/codesys/src/POUs/fb_boiler.st @@ -0,0 +1,89 @@ +(* + POU: fb_boiler + Simple ON/OFF boiler: time limit + emergency stop. Priority: Emergency > Time limit > OFF > ON. +*) +FUNCTION_BLOCK fb_boiler +VAR_INPUT + ha_on: BOOL; + ha_off: BOOL; + schedule_on: BOOL; + schedule_off: BOOL; + emergency_stop: BOOL; + max_on_time: TIME := T#8H; +END_VAR +VAR_OUTPUT + relay_control: BOOL; + state: BOOL; + on_time_seconds: DINT; + remaining_seconds: DINT; + emergency_active: BOOL; + time_limit_reached: BOOL; + error_state: BOOL; + error_code: INT; +END_VAR +VAR + internal_state: BOOL := FALSE; + r_trig_ha_on: R_TRIG; + r_trig_ha_off: R_TRIG; + r_trig_schedule_on: R_TRIG; + r_trig_schedule_off: R_TRIG; + r_trig_emergency: R_TRIG; + f_trig_emergency: F_TRIG; + on_timer: TON; + max_on_seconds: DINT; +END_VAR + +max_on_seconds := TIME_TO_DINT(max_on_time) / 1000; + +r_trig_ha_on(CLK := ha_on); +r_trig_ha_off(CLK := ha_off); +r_trig_schedule_on(CLK := schedule_on); +r_trig_schedule_off(CLK := schedule_off); +r_trig_emergency(CLK := emergency_stop); +f_trig_emergency(CLK := emergency_stop); + +IF emergency_stop THEN + internal_state := FALSE; + emergency_active := TRUE; + error_state := TRUE; + error_code := 1; +ELSIF on_timer.Q THEN + internal_state := FALSE; + time_limit_reached := TRUE; + error_state := TRUE; + error_code := 2; +ELSE + IF f_trig_emergency.Q THEN + emergency_active := FALSE; + error_state := FALSE; + error_code := 0; + END_IF; + IF NOT internal_state THEN + time_limit_reached := FALSE; + IF error_code = 2 THEN + error_state := FALSE; + error_code := 0; + END_IF; + END_IF; + IF r_trig_ha_off.Q OR r_trig_schedule_off.Q THEN + internal_state := FALSE; + ELSIF (r_trig_ha_on.Q OR r_trig_schedule_on.Q) AND NOT time_limit_reached THEN + internal_state := TRUE; + END_IF; +END_IF; + +on_timer(IN := internal_state AND NOT emergency_active, PT := max_on_time); + +IF internal_state THEN + on_time_seconds := TIME_TO_DINT(on_timer.ET) / 1000; + remaining_seconds := max_on_seconds - on_time_seconds; + IF remaining_seconds < 0 THEN + remaining_seconds := 0; + END_IF; +ELSE + on_time_seconds := 0; + remaining_seconds := max_on_seconds; +END_IF; + +relay_control := internal_state AND NOT emergency_active; +state := internal_state; diff --git a/codesys/src/POUs/fb_light.st b/codesys/src/POUs/fb_light.st new file mode 100644 index 0000000..de0f753 --- /dev/null +++ b/codesys/src/POUs/fb_light.st @@ -0,0 +1,37 @@ +(* + POU: fb_light + Single light control: HA ON/OFF + Zigbee toggle. Priority: HA OFF > HA ON > Zigbee. +*) +FUNCTION_BLOCK fb_light +VAR_INPUT + ha_on: BOOL; + ha_off: BOOL; + zigbee_sw: BOOL; + relay_status: BOOL; +END_VAR +VAR_OUTPUT + light_output: BOOL; + light_status: BOOL; +END_VAR +VAR + r_trig_ha_on: R_TRIG; + r_trig_ha_off: R_TRIG; + r_trig_zigbee: R_TRIG; + light_state: BOOL := FALSE; +END_VAR + +// Implementation +r_trig_ha_on(CLK := ha_on); +r_trig_ha_off(CLK := ha_off); +r_trig_zigbee(CLK := zigbee_sw); + +IF r_trig_ha_off.Q THEN + light_state := FALSE; +ELSIF r_trig_ha_on.Q THEN + light_state := TRUE; +ELSIF r_trig_zigbee.Q THEN + light_state := NOT light_state; +END_IF; + +light_output := light_state; +light_status := relay_status; diff --git a/codesys/src/POUs/fb_room.st b/codesys/src/POUs/fb_room.st new file mode 100644 index 0000000..0b0ee2f --- /dev/null +++ b/codesys/src/POUs/fb_room.st @@ -0,0 +1,56 @@ +(* + POU: fb_room + Room-level lighting: 6× fb_light; global ha_all_on / ha_all_off overwrite outputs. +*) +FUNCTION_BLOCK fb_room +VAR_INPUT + switches: struct_room_cmds; + relay_status: struct_room_outs; +END_VAR +VAR_OUTPUT + lights: struct_room_outs; +END_VAR +VAR + l1: fb_light; + l2: fb_light; + l3: fb_light; + l4: fb_light; + l5: fb_light; + l6: fb_light; +END_VAR + +// Light 1..6 (only l_X output; relay_status = previous cycle output for Option C) +l1(ha_on := switches.ha_l1_on, ha_off := switches.ha_l1_off, zigbee_sw := switches.zigbee_sw1, relay_status := relay_status.l_1); +lights.l_1 := l1.light_output; + +l2(ha_on := switches.ha_l2_on, ha_off := switches.ha_l2_off, zigbee_sw := switches.zigbee_sw2, relay_status := relay_status.l_2); +lights.l_2 := l2.light_output; + +l3(ha_on := switches.ha_l3_on, ha_off := switches.ha_l3_off, zigbee_sw := switches.zigbee_sw3, relay_status := relay_status.l_3); +lights.l_3 := l3.light_output; + +l4(ha_on := switches.ha_l4_on, ha_off := switches.ha_l4_off, zigbee_sw := switches.zigbee_sw4, relay_status := relay_status.l_4); +lights.l_4 := l4.light_output; + +l5(ha_on := switches.ha_l5_on, ha_off := switches.ha_l5_off, zigbee_sw := switches.zigbee_sw5, relay_status := relay_status.l_5); +lights.l_5 := l5.light_output; + +l6(ha_on := switches.ha_l6_on, ha_off := switches.ha_l6_off, zigbee_sw := switches.zigbee_sw6, relay_status := relay_status.l_6); +lights.l_6 := l6.light_output; + +// Global commands +IF switches.ha_all_off THEN + lights.l_1 := FALSE; + lights.l_2 := FALSE; + lights.l_3 := FALSE; + lights.l_4 := FALSE; + lights.l_5 := FALSE; + lights.l_6 := FALSE; +ELSIF switches.ha_all_on THEN + lights.l_1 := TRUE; + lights.l_2 := TRUE; + lights.l_3 := TRUE; + lights.l_4 := TRUE; + lights.l_5 := TRUE; + lights.l_6 := TRUE; +END_IF; diff --git a/codesys/src/README.md b/codesys/src/README.md new file mode 100644 index 0000000..b0bdf82 --- /dev/null +++ b/codesys/src/README.md @@ -0,0 +1,64 @@ +# CODESYS Source Tree (Redesign / PLC_App) + +Structured folder and source files for the home automation redesign: lighting (fb_light, fb_room) and water boiler (fb_boiler), with Option C relay feedback. + +## Folder Structure + +``` +codesys/src/ +├── DUT/ # Data Unit Types +│ ├── struct_room_cmds.typ +│ ├── struct_room_outs.typ +│ ├── struct_boiler_cmd.typ +│ └── struct_boiler_status.typ +├── GVL/ # Global Variable Lists +│ ├── GVL_IO.gvl # EtherCAT_RelayFeedback, EtherCAT_Outputs (EL2809) +│ └── GVL_NVL_placeholders.gvl # NVL_In, NVL_Out, DI_Emergency_Stop (for Node-RED) +├── NVL/ # Network Variables (Node-RED) +│ ├── README.md # NVL_Out / NVL_In config, CODESYS + Node-RED setup +│ └── nodered-payload.md # Binary payload layout + parse/build examples +├── POUs/ # Program Organization Units +│ ├── fb_light.st # Single light FB +│ ├── fb_room.st # Room (6 lights) FB +│ ├── fb_boiler.st # Boiler FB +│ └── PLC_App.st # Main program +└── README.md +``` + +## Contents + +| Item | Description | +|------|-------------| +| **DUT** | `struct_room_cmds`, `struct_room_outs`, `struct_boiler_cmd`, `struct_boiler_status` | +| **GVL_IO** | `EtherCAT_RelayFeedback` (Option C), `EtherCAT_Outputs` (Ch0..Ch15 for EL2809) | +| **GVL_NVL_placeholders** | `NVL_In`, `NVL_Out` (struct stubs), `DI_Emergency_Stop`; replace with real NVL config if needed | +| **fb_light** | One light: HA ON/OFF + Zigbee toggle, relay_status in → light_output/light_status out | +| **fb_room** | Six fb_light instances + ha_all_on / ha_all_off overwrite | +| **fb_boiler** | ON/OFF with max-on-time and emergency stop | +| **PLC_App** | Calls all rooms and boiler; copies room outputs to EtherCAT_RelayFeedback and EtherCAT_Outputs | + +## How to Use in CODESYS + +1. **Create or open a CODESYS project** (e.g. Control for Raspberry Pi). +2. **Add DUTs**: Create new DUTs under the device or application and paste the content of each `.typ` file (or use **Add Object → DUT** and paste). +3. **Add GVLs**: Create a GVL (e.g. `GVL_IO`), paste `GVL_IO.gvl`. Create/configure NVL_In and NVL_Out for your UDP/network variables and ensure their structure matches the design (struct_room_cmds per room, struct_boiler_cmd; struct_room_outs per room, struct_boiler_status). +4. **Add POUs**: Create POUs (Function Block / Program), set language to Structured Text, and paste the corresponding `.st` file. Order: add `fb_light`, then `fb_room`, then `fb_boiler`, then `PLC_App`. +5. **Task**: Call `PLC_App` from your main task (e.g. EtherCAT_Task or MainTask). +6. **I/O**: Link `EtherCAT_Outputs.Ch0`..`Ch15` to the EL2809 output process image (or build one WORD from Ch0..Ch15 and link that to the EL2809). + +## Network variables (Node-RED) + +- **NVL_Out** (PLC → Node-RED): light states + boiler status, sent via UDP. +- **NVL_In** (Node-RED → PLC): light commands + boiler commands, received via UDP. + +See **NVL/README.md** for CODESYS NVL Sender/Receiver setup and **NVL/nodered-payload.md** for binary payload layout and Node-RED parse/build examples. + +## Dependencies + +- **Standard library** (R_TRIG, F_TRIG, TON, TIME_TO_DINT, DINT_TO_INT, etc.). +- **NVL_In / NVL_Out**: Use **GVL_NVL_placeholders.gvl** or bind your NVL Sender/Receiver to the same struct layout (see NVL/README.md). +- **DI_Emergency_Stop**: Optional BOOL; set to FALSE or link to a physical input. + +## Design Reference + +See **docs/codesys/plc-algorithm-design.md** for full algorithm description, I/O mapping, and Option C relay feedback. diff --git a/docs/codesys/plc-algorithm-design.md b/docs/codesys/plc-algorithm-design.md index 6f51689..5f3316c 100644 --- a/docs/codesys/plc-algorithm-design.md +++ b/docs/codesys/plc-algorithm-design.md @@ -139,25 +139,19 @@ END_STRUCT END_TYPE ``` -#### Output: struct_room_outs (flat, control + status feedback) +#### Output: struct_room_outs (flat – light state only) + +With Option C (no relay read-back), one set of values is enough: we send `l_1..l_6` to the relay and to Node-RED as "light status". ```iec TYPE struct_room_outs : STRUCT - l_1: BOOL; // Light 1 control output - l_2: BOOL; // Light 2 control output - l_3: BOOL; // Light 3 control output - l_4: BOOL; // Light 4 control output - l_5: BOOL; // Light 5 control output - l_6: BOOL; // Light 6 control output - - // Status feedback (read from actual relay outputs) - l_1_status: BOOL; // Actual relay 1 state - l_2_status: BOOL; // Actual relay 2 state - l_3_status: BOOL; // Actual relay 3 state - l_4_status: BOOL; // Actual relay 4 state - l_5_status: BOOL; // Actual relay 5 state - l_6_status: BOOL; // Actual relay 6 state + l_1: BOOL; // Light 1 + l_2: BOOL; // Light 2 + l_3: BOOL; // Light 3 + l_4: BOOL; // Light 4 + l_5: BOOL; // Light 5 + l_6: BOOL; // Light 6 END_STRUCT END_TYPE ``` @@ -258,65 +252,24 @@ END_VAR // fb_room - Implementation (per redesign) // ===================================================== -// Light 1 Control -l1( - ha_on := switches.ha_l1_on, - ha_off := switches.ha_l1_off, - zigbee_sw := switches.zigbee_sw1, - relay_status := relay_status.l_1_status -); +// Light 1..6 (relay_status = previous cycle output for Option C) +l1(ha_on := switches.ha_l1_on, ha_off := switches.ha_l1_off, zigbee_sw := switches.zigbee_sw1, relay_status := relay_status.l_1); lights.l_1 := l1.light_output; -lights.l_1_status := l1.light_status; -// Light 2 Control -l2( - ha_on := switches.ha_l2_on, - ha_off := switches.ha_l2_off, - zigbee_sw := switches.zigbee_sw2, - relay_status := relay_status.l_2_status -); +l2(ha_on := switches.ha_l2_on, ha_off := switches.ha_l2_off, zigbee_sw := switches.zigbee_sw2, relay_status := relay_status.l_2); lights.l_2 := l2.light_output; -lights.l_2_status := l2.light_status; -// Light 3 Control -l3( - ha_on := switches.ha_l3_on, - ha_off := switches.ha_l3_off, - zigbee_sw := switches.zigbee_sw3, - relay_status := relay_status.l_3_status -); +l3(ha_on := switches.ha_l3_on, ha_off := switches.ha_l3_off, zigbee_sw := switches.zigbee_sw3, relay_status := relay_status.l_3); lights.l_3 := l3.light_output; -lights.l_3_status := l3.light_status; -// Light 4 Control -l4( - ha_on := switches.ha_l4_on, - ha_off := switches.ha_l4_off, - zigbee_sw := switches.zigbee_sw4, - relay_status := relay_status.l_4_status -); +l4(ha_on := switches.ha_l4_on, ha_off := switches.ha_l4_off, zigbee_sw := switches.zigbee_sw4, relay_status := relay_status.l_4); lights.l_4 := l4.light_output; -lights.l_4_status := l4.light_status; -// Light 5 Control -l5( - ha_on := switches.ha_l5_on, - ha_off := switches.ha_l5_off, - zigbee_sw := switches.zigbee_sw5, - relay_status := relay_status.l_5_status -); +l5(ha_on := switches.ha_l5_on, ha_off := switches.ha_l5_off, zigbee_sw := switches.zigbee_sw5, relay_status := relay_status.l_5); lights.l_5 := l5.light_output; -lights.l_5_status := l5.light_status; -// Light 6 Control -l6( - ha_on := switches.ha_l6_on, - ha_off := switches.ha_l6_off, - zigbee_sw := switches.zigbee_sw6, - relay_status := relay_status.l_6_status -); +l6(ha_on := switches.ha_l6_on, ha_off := switches.ha_l6_off, zigbee_sw := switches.zigbee_sw6, relay_status := relay_status.l_6); lights.l_6 := l6.light_output; -lights.l_6_status := l6.light_status; // Global Commands (overwrite outputs per redesign) IF switches.ha_all_off THEN @@ -594,6 +547,149 @@ state := internal_state; ## Part 3: Main Program Integration +### 3.0 Where to declare EtherCAT_RelayFeedback (and optional EtherCAT_Outputs) + +You only need **EtherCAT_RelayFeedback** in a GVL for Option C (previous-cycle output). For the EL2809 DO channels you can **map directly in the device tree** to the variables you already write: e.g. link Ch0..Ch5 to `NVL_Out.l_masterBedroom.l_1` … `l_6`, Ch6..Ch11 to `NVL_Out.l_masterBathroom.l_1` … `l_6`, and Ch15 to `NVL_Out.boiler_status.relay_output` (or a GVL BOOL). Then no **EtherCAT_Outputs** GVL or assignments in `PLC_App` are needed. + +1. In CODESYS, add or open a **Global Variable List** (e.g. `GVL_IO`). +2. Declare **EtherCAT_RelayFeedback** (one `struct_room_outs` per room) as below. Optionally add **EtherCAT_Outputs** (or a WORD) only if you prefer not to map the EL2809 directly to NVL_Out. + +**Example GVL (e.g. `GVL_IO`):** + +```iec +VAR_GLOBAL + // ---------- Option C: relay feedback (copy of output) ---------- + EtherCAT_RelayFeedback: STRUCT + masterBedroom: struct_room_outs; + masterBathroom: struct_room_outs; + bedroom_1: struct_room_outs; + bedroom_2: struct_room_outs; + bathroom: struct_room_outs; + guest_wc: struct_room_outs; + kitchen: struct_room_outs; + pantry: struct_room_outs; + livingRoom: struct_room_outs; + diningRoom: struct_room_outs; + entrance: struct_room_outs; + hallway: struct_room_outs; + veranda: struct_room_outs; + front: struct_room_outs; + back: struct_room_outs; + side: struct_room_outs; + END_STRUCT; + + // ---------- Outputs to EL2809 (15 lights + 1 boiler) ---------- + EtherCAT_Outputs: STRUCT + masterBedroom_l1: BOOL; + masterBedroom_l2: BOOL; + masterBedroom_l3: BOOL; + masterBedroom_l4: BOOL; + masterBedroom_l5: BOOL; + masterBedroom_l6: BOOL; + masterBathroom_l1: BOOL; + masterBathroom_l2: BOOL; + masterBathroom_l3: BOOL; + masterBathroom_l4: BOOL; + masterBathroom_l5: BOOL; + masterBathroom_l6: BOOL; + bedroom_1_l1: BOOL; + bedroom_1_l2: BOOL; + bedroom_1_l3: BOOL; + bedroom_1_l4: BOOL; + bedroom_1_l5: BOOL; + bedroom_1_l6: BOOL; + bedroom_2_l1: BOOL; + bedroom_2_l2: BOOL; + bedroom_2_l3: BOOL; + bedroom_2_l4: BOOL; + bedroom_2_l5: BOOL; + bedroom_2_l6: BOOL; + bathroom_l1: BOOL; + bathroom_l2: BOOL; + bathroom_l3: BOOL; + bathroom_l4: BOOL; + bathroom_l5: BOOL; + bathroom_l6: BOOL; + guest_wc_l1: BOOL; + guest_wc_l2: BOOL; + guest_wc_l3: BOOL; + guest_wc_l4: BOOL; + guest_wc_l5: BOOL; + guest_wc_l6: BOOL; + kitchen_l1: BOOL; + kitchen_l2: BOOL; + kitchen_l3: BOOL; + kitchen_l4: BOOL; + kitchen_l5: BOOL; + kitchen_l6: BOOL; + pantry_l1: BOOL; + pantry_l2: BOOL; + pantry_l3: BOOL; + pantry_l4: BOOL; + pantry_l5: BOOL; + pantry_l6: BOOL; + livingRoom_l1: BOOL; + livingRoom_l2: BOOL; + livingRoom_l3: BOOL; + livingRoom_l4: BOOL; + livingRoom_l5: BOOL; + livingRoom_l6: BOOL; + diningRoom_l1: BOOL; + diningRoom_l2: BOOL; + diningRoom_l3: BOOL; + diningRoom_l4: BOOL; + diningRoom_l5: BOOL; + diningRoom_l6: BOOL; + entrance_l1: BOOL; + entrance_l2: BOOL; + entrance_l3: BOOL; + entrance_l4: BOOL; + entrance_l5: BOOL; + entrance_l6: BOOL; + hallway_l1: BOOL; + hallway_l2: BOOL; + hallway_l3: BOOL; + hallway_l4: BOOL; + hallway_l5: BOOL; + hallway_l6: BOOL; + veranda_l1: BOOL; + veranda_l2: BOOL; + veranda_l3: BOOL; + veranda_l4: BOOL; + veranda_l5: BOOL; + veranda_l6: BOOL; + front_l1: BOOL; + front_l2: BOOL; + front_l3: BOOL; + front_l4: BOOL; + front_l5: BOOL; + front_l6: BOOL; + back_l1: BOOL; + back_l2: BOOL; + back_l3: BOOL; + back_l4: BOOL; + back_l5: BOOL; + back_l6: BOOL; + side_l1: BOOL; + side_l2: BOOL; + side_l3: BOOL; + side_l4: BOOL; + side_l5: BOOL; + side_l6: BOOL; + boiler_relay: BOOL; + END_STRUCT; + + // ---------- Optional: EL2809 output word (link this to the device in I/O mapping) ---------- + // EL2809_Output: WORD; // Then in PLC_App assign bits from EtherCAT_Outputs to EL2809_Output +END_VAR +``` + +3. **Initialization:** In the GVL properties, set **"Variable Initialization"** so that `EtherCAT_RelayFeedback` is initialized (e.g. CODESYS initializes STRUCT to zero by default). Or in the first scan of `PLC_App` you can clear it once. + +4. **Linking to the EL2809:** In the **Device Tree**, open the EtherCAT master → EL2809 → **I/O Mapping** (or **Process Image**). Link each EL2809 **Output** channel directly to the variable you write (e.g. Ch0 → `NVL_Out.l_masterBedroom.l_1`, Ch1 → `l_2`, … Ch15 → boiler relay). Then `PLC_App` only assigns `NVL_Out.l_* := .lights` and boiler status; no separate output struct needed. Alternatively, link the 16 bits to a **WORD** or 16 BOOLs that you assign from room lights and boiler in `PLC_App` (e.g. an optional `EtherCAT_Outputs` GVL). + +So: **EtherCAT_RelayFeedback** is in a **GVL** for Option C. The EL2809 output is linked either **directly to NVL_Out** (and boiler) or to an optional output GVL that you fill in `PLC_App`. + ### 3.1 PLC_App structure ```iec @@ -633,7 +729,7 @@ VAR END_VAR ``` -**Option C (relay feedback):** In a GVL, define `EtherCAT_RelayFeedback` (one `struct_room_outs` per room) and `EtherCAT_Outputs` (outputs to EL2809). Initialize `EtherCAT_RelayFeedback` to zero so the first cycle has defined behavior. Each cycle, after each `fb_room` call, copy `EtherCAT_RelayFeedback. := .lights` (see Section 3.2). +**Option C (relay feedback):** In a GVL, define `EtherCAT_RelayFeedback` (one `struct_room_outs` per room). Initialize it to zero so the first cycle has defined behavior. Each cycle, after each `fb_room` call, copy `EtherCAT_RelayFeedback. := .lights`. Map the EL2809 DO channels in the device tree directly to the variables you write (e.g. `NVL_Out.l_*` and boiler relay); a separate `EtherCAT_Outputs` GVL is not required (see Section 3.2). ### 3.2 Main Program Logic @@ -724,7 +820,7 @@ The EL2809 has a **16-bit process image** (one word) for the output commands. In **Chosen for this project: Option C** — relay “feedback” is a **copy of the output** we write to the EL2809. No hardware read-back; HA and the PLC stay in sync with the commanded state. Relay/wiring faults are not detected. -Relay feedback is the value passed into `fb_room` as `relay_status` and used for `l_X_status` in the status sent to Node-RED/HA. With Option C it is the same as the control output (from the previous cycle), so the UI reflects what we commanded. +Relay feedback is the value passed into `fb_room` as `relay_status` (same struct as output: `l_1..l_6`) and sent to Node-RED/HA as light status. With Option C it is the same as the control output (from the previous cycle), so the UI reflects what we commanded. **Your hardware**: **EL2809** (16-channel DO). The EL2809 drives the outputs from the process image; whether it exposes a **read-back** (TxPDO / “Input” or “Status”) depends on the EtherCAT PDO configuration. In CODESYS, after scanning the EL2809, check the device’s process image for an **input** or **status** word (data from device → PLC). **EL2809 has no read-back (confirmed):** process image = 16 output bits only, no input. Use **Option C** (copy of output) or **Option B** (auxiliary contacts). Which models *do* have input? See table below. @@ -813,13 +909,13 @@ Assume the EL2809’s read-back is available as a **WORD** in a GVL named `GVL_I ```iec // Map EL2809 read-back (WORD) to relay feedback. Adjust bit order and room/channel // mapping to match your wiring. Example: Ch0..Ch5 = masterBedroom l_1..l_6. -EtherCAT_RelayFeedback.masterBedroom.l_1_status := GVL_IO.EL2809_Input.%X0; // Ch0 -EtherCAT_RelayFeedback.masterBedroom.l_2_status := GVL_IO.EL2809_Input.%X1; -EtherCAT_RelayFeedback.masterBedroom.l_3_status := GVL_IO.EL2809_Input.%X2; -EtherCAT_RelayFeedback.masterBedroom.l_4_status := GVL_IO.EL2809_Input.%X3; -EtherCAT_RelayFeedback.masterBedroom.l_5_status := GVL_IO.EL2809_Input.%X4; -EtherCAT_RelayFeedback.masterBedroom.l_6_status := GVL_IO.EL2809_Input.%X5; -EtherCAT_RelayFeedback.masterBathroom.l_1_status := GVL_IO.EL2809_Input.%X6; +EtherCAT_RelayFeedback.masterBedroom.l_1 := GVL_IO.EL2809_Input.%X0; // Ch0 +EtherCAT_RelayFeedback.masterBedroom.l_2 := GVL_IO.EL2809_Input.%X1; +EtherCAT_RelayFeedback.masterBedroom.l_3 := GVL_IO.EL2809_Input.%X2; +EtherCAT_RelayFeedback.masterBedroom.l_4 := GVL_IO.EL2809_Input.%X3; +EtherCAT_RelayFeedback.masterBedroom.l_5 := GVL_IO.EL2809_Input.%X4; +EtherCAT_RelayFeedback.masterBedroom.l_6 := GVL_IO.EL2809_Input.%X5; +EtherCAT_RelayFeedback.masterBathroom.l_1 := GVL_IO.EL2809_Input.%X6; // ... continue for all 15 lights (channels 0..14). Channel 15 = boiler relay; // if you want boiler relay read-back, assign to your boiler status struct instead. ``` @@ -827,8 +923,8 @@ EtherCAT_RelayFeedback.masterBathroom.l_1_status := GVL_IO.EL2809_Input.%X6; **If you have 16 BOOLs (e.g. `GVL_IO.EL2809_Ch0` … `GVL_IO.EL2809_Ch15`):** ```iec -EtherCAT_RelayFeedback.masterBedroom.l_1_status := GVL_IO.EL2809_Ch0; -EtherCAT_RelayFeedback.masterBedroom.l_2_status := GVL_IO.EL2809_Ch1; +EtherCAT_RelayFeedback.masterBedroom.l_1 := GVL_IO.EL2809_Ch0; +EtherCAT_RelayFeedback.masterBedroom.l_2 := GVL_IO.EL2809_Ch1; // ... same mapping as above, using EL2809_Ch2 .. EL2809_Ch15 ``` @@ -862,7 +958,7 @@ Each `fb_room` is then called with `relay_status := EtherCAT_RelayFeedback..l_X_status` (and boiler if applicable). | +| 4 | In `PLC_App` (before calling `fb_room`), assign each Input bit to the corresponding `EtherCAT_RelayFeedback..l_X` (and boiler if applicable). | | 5 | Pass `EtherCAT_RelayFeedback.` as `relay_status` into each `fb_room`. | So: **relay feedback = value read from the EL2809’s Input process image**, when the device provides it. @@ -875,7 +971,7 @@ The relay has (or you add) an **auxiliary (feedback) contact** that closes when - **Wiring**: Relay coil ← DO channel; relay auxiliary contact → DI channel. - **In the program**: The DI channel *is* the relay feedback. - e.g. `EtherCAT_RelayFeedback.masterBedroom.l_1_status := GVL.EtherCAT_Master.DI_Module.Ch5;` + e.g. `EtherCAT_RelayFeedback.masterBedroom.l_1 := GVL.EtherCAT_Master.DI_Module.Ch5;` (where Ch5 is the DI connected to relay 1’s auxiliary contact.) So: **relay feedback = state of the DI that is wired to the relay’s auxiliary contact.** @@ -886,10 +982,8 @@ So: **relay feedback = state of the DI that is wired to the relay’s auxiliary If the EL2809 does **not** expose an input/read-back in the process image and you do **not** use auxiliary contacts (Option B), use the **output command as the “feedback”**: -- **In the program**: Build `EtherCAT_RelayFeedback` from the **same variables** you write to the EL2809 outputs. For example, after you assign `EtherCAT_Outputs.masterBedroom_l1 := masterBedroom.lights.l_1;`, also set - `EtherCAT_RelayFeedback.masterBedroom.l_1_status := masterBedroom.lights.l_1;` - (or copy from a single source of truth for each channel). -- Then `l_X_status` in `struct_room_outs` reflects “what we commanded,” not “what the relay actually did.” HA and the PLC stay in sync, but **relay or wiring faults are not detected**. +- **In the program**: After each `fb_room` call, set `EtherCAT_RelayFeedback. := .lights`. So `l_1..l_6` are both the relay output and the "status" passed back next cycle and sent to Node-RED. +- Then the single set `l_1..l_6` reflects “what we commanded,” not “what the relay actually did.” HA and the PLC stay in sync, but **relay or wiring faults are not detected**. **Implementation:** Section 3.2 (PLC_App): after each `fb_room` call, set `EtherCAT_RelayFeedback. := .lights`; initialize `EtherCAT_RelayFeedback` to zero at startup.