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:
2026-02-08 00:47:57 +02:00
parent 63b343f139
commit 6ef31cd12a
14 changed files with 873 additions and 80 deletions

View File

@@ -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 devices 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 EL2809s 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 EL2809s 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 1s auxiliary contact.)
So: **relay feedback = state of the DI that is wired to the relays auxiliary contact.**
@@ -886,10 +982,8 @@ So: **relay feedback = state of the DI that is wired to the relays 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.