Refine PLC algorithm documentation for lighting control and EtherCAT integration
- Updated the output structure for `struct_room_outs` to focus solely on light states, removing unnecessary status feedback fields. - Simplified the control logic for lights 1 to 6, aligning with the new design approach for Option C. - Added detailed instructions for declaring `EtherCAT_RelayFeedback` and `EtherCAT_Outputs` in the Global Variable List, enhancing clarity for integration with the EL2809 module. This update improves the documentation's accuracy and usability for developers working on the home automation system.
This commit is contained in:
14
codesys/src/DUT/struct_boiler_cmd.typ
Normal file
14
codesys/src/DUT/struct_boiler_cmd.typ
Normal file
@@ -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
|
||||
16
codesys/src/DUT/struct_boiler_status.typ
Normal file
16
codesys/src/DUT/struct_boiler_status.typ
Normal file
@@ -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
|
||||
32
codesys/src/DUT/struct_room_cmds.typ
Normal file
32
codesys/src/DUT/struct_room_cmds.typ
Normal file
@@ -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
|
||||
15
codesys/src/DUT/struct_room_outs.typ
Normal file
15
codesys/src/DUT/struct_room_outs.typ
Normal file
@@ -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
|
||||
27
codesys/src/GVL/GVL_IO.gvl
Normal file
27
codesys/src/GVL/GVL_IO.gvl
Normal file
@@ -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
|
||||
48
codesys/src/GVL/GVL_NVL_placeholders.gvl
Normal file
48
codesys/src/GVL/GVL_NVL_placeholders.gvl
Normal file
@@ -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
|
||||
82
codesys/src/NVL/README.md
Normal file
82
codesys/src/NVL/README.md
Normal file
@@ -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.
|
||||
154
codesys/src/NVL/nodered-payload.md
Normal file
154
codesys/src/NVL/nodered-payload.md
Normal file
@@ -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.
|
||||
65
codesys/src/POUs/PLC_App.st
Normal file
65
codesys/src/POUs/PLC_App.st
Normal file
@@ -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;
|
||||
89
codesys/src/POUs/fb_boiler.st
Normal file
89
codesys/src/POUs/fb_boiler.st
Normal file
@@ -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;
|
||||
37
codesys/src/POUs/fb_light.st
Normal file
37
codesys/src/POUs/fb_light.st
Normal file
@@ -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;
|
||||
56
codesys/src/POUs/fb_room.st
Normal file
56
codesys/src/POUs/fb_room.st
Normal file
@@ -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;
|
||||
64
codesys/src/README.md
Normal file
64
codesys/src/README.md
Normal file
@@ -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.
|
||||
@@ -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_* := <room>.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.<room> := <room>.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.<room> := <room>.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.<room
|
||||
| 1 | In Device Tree → EL2809, check for **Input** (or Status) in process image. |
|
||||
| 2 | If present, note the symbol (e.g. `GVL_IO.EL2809_Input` or 16 BOOLs). |
|
||||
| 3 | Define your channel → room/light mapping table. |
|
||||
| 4 | In `PLC_App` (before calling `fb_room`), assign each Input bit to the corresponding `EtherCAT_RelayFeedback.<room>.l_X_status` (and boiler if applicable). |
|
||||
| 4 | In `PLC_App` (before calling `fb_room`), assign each Input bit to the corresponding `EtherCAT_RelayFeedback.<room>.l_X` (and boiler if applicable). |
|
||||
| 5 | Pass `EtherCAT_RelayFeedback.<room>` 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.<room> := <room>.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.<room> := <room>.lights`; initialize `EtherCAT_RelayFeedback` to zero at startup.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user