- 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.
1121 lines
46 KiB
Markdown
1121 lines
46 KiB
Markdown
# PLC Algorithm Design - Lighting and Water Boiler Control
|
||
|
||
## Overview
|
||
|
||
This document defines the PLC algorithms for the home automation system:
|
||
1. **Lighting Control** - Improved logic with command-based control for Home Assistant and toggle-based for Zigbee switches
|
||
2. **Water Boiler Control** - Simple ON/OFF with safety features (time limit, emergency stop)
|
||
|
||
> **Naming convention**: New types, FBs, program, and NVLs use **different names** (no suffix) so the
|
||
> redesign can run alongside the existing project. Existing names (`struct_switches`, `fb_switch`, `PLC_PRG`, etc.) are left unchanged.
|
||
|
||
### Name Mapping (existing → new, no suffix)
|
||
|
||
| Existing name (do not touch) | New name | Kind |
|
||
|------------------------------|----------|------|
|
||
| `struct_switches` | `struct_room_cmds` | DUT (type) |
|
||
| `struct_lights` | `struct_room_outs` | DUT (type) |
|
||
| `fb_toogleButton` | `fb_light` | Function Block |
|
||
| `fb_switch` | `fb_room` | Function Block |
|
||
| — (new) | `fb_boiler` | Function Block |
|
||
| — (new) | `struct_boiler_cmd` | DUT (type) |
|
||
| — (new) | `struct_boiler_status` | DUT (type) |
|
||
| `PLC_PRG` | `PLC_App` | Program |
|
||
| `Lights` | (merged into `PLC_App`) | Program |
|
||
| `NVL_Sender` | `NVL_Out` | NVL |
|
||
| `NVL_Receiver` | `NVL_In` | NVL |
|
||
|
||
## System Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ CODESYS PLC (Raspberry Pi) │
|
||
├─────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
|
||
│ │ NVL_In │ │ MainTask │ │ NVL_Out │ │
|
||
│ │ (from Node-RED)│ │ (4ms cycle) │ │ (to Node-RED) │ │
|
||
│ └────────┬────────┘ └────────┬────────┘ └──────────┬──────────┘ │
|
||
│ │ │ │ │
|
||
│ ▼ ▼ ▲ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ PLC_App │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │
|
||
│ │ │ Lights │ │ Boiler │ │ Safety_Monitor │ │ │
|
||
│ │ │ (section) │ │ (section) │ │ (future) │ │ │
|
||
│ │ └──────┬──────┘ └──────┬──────┘ └─────────────────────────┘ │ │
|
||
│ └─────────┼────────────────┼──────────────────────────────────────┘ │
|
||
│ │ │ │
|
||
│ ▼ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ EtherCAT I/O │ │
|
||
│ │ ┌──────────────┐ ┌──────────────────────────────┐ │ │
|
||
│ │ │ EL1809 │ │ EL2809 │ │ │
|
||
│ │ │ 16x DI 24V │ │ 16x DO 24V (Relays) │ │ │
|
||
│ │ │ (Switches) │ │ Lights + Boiler │ │ │
|
||
│ │ └──────────────┘ └──────────────────────────────┘ │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────────┐ ┌─────────────────┐
|
||
│ Physical │ │ Physical │
|
||
│ Switches │ │ Relays │
|
||
└─────────────────┘ └─────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Light organization: do you need 6 per room?
|
||
|
||
### Short answers
|
||
|
||
- **Do you need exactly 6 predefined?** No. It’s a convenient maximum. Unused slots are fine (no relay wired). You can use a smaller max (e.g. 4) or a larger one (e.g. 8) if you prefer.
|
||
- **Is room-based organization a good idea?** Yes. It matches how people think and how voice/HA work (“turn off bedroom”, “kitchen lights”). Keep organizing by room.
|
||
- **Best approach:** Room-based with a **fixed max lights per room** (e.g. 6 or 8). Use only the slots you need; leave the rest logically “empty.” Optionally, use **named slots** per room (e.g. `main`, `bedside_left`) instead of `l_1`..`l_6` so the PLC matches your naming and you don’t “waste” slots mentally.
|
||
|
||
### Options (trade-offs)
|
||
|
||
| Approach | Description | Pros | Cons |
|
||
|---------|-------------|------|------|
|
||
| **A. Fixed N per room (current)** | Every room has e.g. 6 slots (`l_1`..`l_6`). Use only what you need. | Simple, same code for every room, easy Node-RED mapping. | Empty slots still in struct/UI; adding a 7th light in one room needs a new room or structure change. |
|
||
| **B. Named slots per room** | Each room has semantic slots: `main`, `bedside_left`, `under_cabinet`, etc. (from `light-naming-configuration.md`). Not all rooms have all slots. | Clear meaning, matches your naming; “unused” is obvious. | More types or optional slots in CODESYS; Node-RED must know which rooms have which slots. |
|
||
| **C. Flat list of lights** | One list of e.g. 64 lights; each has a relay index. Room exists only in HA/Node-RED. | No per-room limit; add lights by config. | “Turn off all kitchen” is done in HA/Node-RED (send OFF to each kitchen light). PLC doesn’t do room-level all_off. |
|
||
|
||
### Recommendation
|
||
|
||
- **Stay room-based** for control and UX.
|
||
- **Keep a max per room (e.g. 6 or 8)** and accept that some slots are unused. No need to “predefine” exactly 6; think of it as “up to 6 (or 8) circuits per room.”
|
||
- If you want the PLC to reflect real names, consider **B (named slots)** later: e.g. `struct_room_lights` with `main`, `bedside_left`, `bedside_right`, `closet`, … and only wire the ones that exist in that room. That’s a refinement of the same idea, not a different architecture.
|
||
|
||
So: you don’t *need* 6 predefined; you need a *maximum* per room. Organizing by room is a good idea; the rest is how many slots per room and whether they’re named or generic.
|
||
|
||
---
|
||
|
||
## Part 1: Lighting Control Algorithm
|
||
|
||
**Alignment**: This section follows the structures and logic from **`docs/redesign/fb_switch-redesign-recommendation.md`**: flat `struct_room_cmds` / `struct_room_outs`, `fb_light` with HA ON/OFF + Zigbee toggle only, and `fb_room` applying global commands by overwriting outputs.
|
||
|
||
### 1.1 Design Goals
|
||
|
||
- **Command-based control** for Home Assistant (explicit ON/OFF)
|
||
- **Toggle-based control** for Zigbee switches (edge detection)
|
||
- **State synchronization** between PLC and Home Assistant
|
||
- **Relay status feedback** for actual state monitoring
|
||
|
||
### 1.2 Data Structures (per Redesign Recommendation)
|
||
|
||
#### Input: struct_room_cmds (flat, for Node-RED/JSON compatibility)
|
||
|
||
```iec
|
||
TYPE struct_room_cmds :
|
||
STRUCT
|
||
// Home Assistant Commands (set/reset)
|
||
ha_l1_on: BOOL; // HA command: Light 1 ON
|
||
ha_l1_off: BOOL; // HA command: Light 1 OFF
|
||
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 switch 1 (edge detected)
|
||
zigbee_sw2: BOOL;
|
||
zigbee_sw3: BOOL;
|
||
zigbee_sw4: BOOL;
|
||
zigbee_sw5: BOOL;
|
||
zigbee_sw6: BOOL;
|
||
|
||
// Global Commands
|
||
ha_all_on: BOOL; // HA: All lights ON
|
||
ha_all_off: BOOL; // HA: All lights OFF
|
||
END_STRUCT
|
||
END_TYPE
|
||
```
|
||
|
||
#### 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
|
||
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
|
||
```
|
||
|
||
### 1.3 Function Block: fb_light (per Redesign)
|
||
|
||
Individual light control: HA ON/OFF + Zigbee toggle only. No all_on/all_off inside this FB.
|
||
|
||
```iec
|
||
FUNCTION_BLOCK fb_light
|
||
VAR_INPUT
|
||
// Home Assistant Commands
|
||
ha_on: BOOL; // HA command: Turn ON
|
||
ha_off: BOOL; // HA command: Turn OFF
|
||
|
||
// Zigbee Switch Input
|
||
zigbee_sw: BOOL; // Zigbee switch (edge detected)
|
||
|
||
// Status Feedback
|
||
relay_status: BOOL; // Actual relay state (read from EtherCAT)
|
||
END_VAR
|
||
VAR_OUTPUT
|
||
light_output: BOOL; // Control output to relay
|
||
light_status: BOOL; // Status to send back (actual relay state)
|
||
END_VAR
|
||
VAR
|
||
// Edge triggers
|
||
r_trig_ha_on: R_TRIG;
|
||
r_trig_ha_off: R_TRIG;
|
||
r_trig_zigbee: R_TRIG;
|
||
|
||
// Internal state
|
||
light_state: BOOL := FALSE;
|
||
END_VAR
|
||
```
|
||
|
||
#### Algorithm Logic (Priority: HA OFF > HA ON > Zigbee toggle)
|
||
|
||
```iec
|
||
// =====================================================
|
||
// fb_light - Implementation (per redesign)
|
||
// =====================================================
|
||
// Priority (highest to lowest):
|
||
// 1. HA OFF command - Always turns light OFF
|
||
// 2. HA ON command - Always turns light ON
|
||
// 3. Zigbee toggle - Toggles current state
|
||
// 4. Maintain current state
|
||
// =====================================================
|
||
|
||
// Edge detection for commands
|
||
r_trig_ha_on(CLK := ha_on);
|
||
r_trig_ha_off(CLK := ha_off);
|
||
r_trig_zigbee(CLK := zigbee_sw);
|
||
|
||
// State machine
|
||
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;
|
||
|
||
// Output assignment
|
||
light_output := light_state;
|
||
|
||
// Status feedback (read from actual relay)
|
||
light_status := relay_status;
|
||
```
|
||
|
||
### 1.4 Function Block: fb_room (per Redesign)
|
||
|
||
Room-level block: 6× fb_light; global commands applied by overwriting outputs.
|
||
|
||
```iec
|
||
FUNCTION_BLOCK fb_room
|
||
VAR_INPUT
|
||
switches: struct_room_cmds;
|
||
relay_status: struct_room_outs; // Read from EtherCAT outputs (actual relay states)
|
||
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
|
||
```
|
||
|
||
#### Algorithm Logic
|
||
|
||
```iec
|
||
// =====================================================
|
||
// fb_room - Implementation (per redesign)
|
||
// =====================================================
|
||
|
||
// 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;
|
||
|
||
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 (overwrite outputs per redesign)
|
||
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;
|
||
```
|
||
|
||
---
|
||
|
||
## Part 2: Water Boiler Control Algorithm
|
||
|
||
### 2.1 Design Goals
|
||
|
||
Based on your requirements:
|
||
- **Simple ON/OFF control** via relay output only
|
||
- **Maximum heating time limit** (safety feature)
|
||
- **Emergency stop button** support
|
||
- **State monitoring and feedback**
|
||
|
||
### 2.2 Data Structures
|
||
|
||
#### Boiler Commands (from Node-RED/Home Assistant)
|
||
|
||
```iec
|
||
TYPE struct_boiler_cmd :
|
||
STRUCT
|
||
// Control Commands
|
||
ha_on: BOOL; // Home Assistant: Turn ON
|
||
ha_off: BOOL; // Home Assistant: Turn OFF
|
||
schedule_on: BOOL; // Scheduled ON (from automation)
|
||
schedule_off: BOOL; // Scheduled OFF (from automation)
|
||
|
||
// Safety Input
|
||
emergency_stop: BOOL; // Emergency stop (physical button or HA)
|
||
|
||
// Configuration (can be set via Node-RED)
|
||
max_on_time_minutes: INT; // Maximum ON time in minutes (default: 480 = 8 hours)
|
||
END_STRUCT
|
||
END_TYPE
|
||
```
|
||
|
||
#### Boiler Status (to Node-RED/Home Assistant)
|
||
|
||
```iec
|
||
TYPE struct_boiler_status :
|
||
STRUCT
|
||
// State
|
||
state: BOOL; // Current boiler state (ON/OFF)
|
||
relay_output: BOOL; // Actual relay output state
|
||
|
||
// Runtime Info
|
||
on_time_minutes: INT; // Current ON time in minutes
|
||
remaining_minutes: INT; // Remaining time before auto-shutoff
|
||
|
||
// Safety Status
|
||
emergency_active: BOOL; // Emergency stop is active
|
||
time_limit_reached: BOOL; // Max time limit was reached
|
||
|
||
// Error Handling
|
||
error_state: BOOL; // Error detected
|
||
error_code: INT; // Error code (0=none, 1=emergency, 2=time limit)
|
||
END_STRUCT
|
||
END_TYPE
|
||
```
|
||
|
||
### 2.3 Function Block: fb_boiler
|
||
|
||
Simple ON/OFF boiler control with time limit and emergency stop.
|
||
|
||
```iec
|
||
FUNCTION_BLOCK fb_boiler
|
||
VAR_INPUT
|
||
// Control Commands
|
||
ha_on: BOOL; // Home Assistant ON command
|
||
ha_off: BOOL; // Home Assistant OFF command
|
||
schedule_on: BOOL; // Scheduled ON
|
||
schedule_off: BOOL; // Scheduled OFF
|
||
|
||
// Safety
|
||
emergency_stop: BOOL; // Emergency stop input
|
||
|
||
// Configuration
|
||
max_on_time: TIME := T#8H; // Maximum ON time (default 8 hours)
|
||
END_VAR
|
||
VAR_OUTPUT
|
||
// Outputs
|
||
relay_control: BOOL; // Control output to relay
|
||
|
||
// Status
|
||
state: BOOL; // Current state
|
||
on_time_seconds: DINT; // Current ON time in seconds
|
||
remaining_seconds: DINT; // Remaining time in seconds
|
||
|
||
// Safety Status
|
||
emergency_active: BOOL; // Emergency stop active
|
||
time_limit_reached: BOOL; // Time limit was reached
|
||
error_state: BOOL; // Error flag
|
||
error_code: INT; // Error code
|
||
END_VAR
|
||
VAR
|
||
// Internal State
|
||
internal_state: BOOL := FALSE;
|
||
last_state: BOOL := FALSE;
|
||
|
||
// Edge Detection
|
||
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;
|
||
|
||
// Timers
|
||
on_timer: TON; // Counts ON time
|
||
|
||
// Time tracking
|
||
max_on_seconds: DINT;
|
||
END_VAR
|
||
```
|
||
|
||
### 2.4 Algorithm Logic
|
||
|
||
```iec
|
||
// =====================================================
|
||
// fb_boiler - Implementation
|
||
// =====================================================
|
||
// Priority (highest to lowest):
|
||
// 1. Emergency Stop - Immediate shutdown
|
||
// 2. Time Limit Exceeded - Auto shutdown
|
||
// 3. OFF Commands - Turn off
|
||
// 4. ON Commands - Turn on (if safe)
|
||
// =====================================================
|
||
|
||
// Calculate max time in seconds
|
||
max_on_seconds := TIME_TO_DINT(max_on_time) / 1000;
|
||
|
||
// Edge detection for commands
|
||
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);
|
||
|
||
// =====================================================
|
||
// SAFETY CHECKS (highest priority)
|
||
// =====================================================
|
||
|
||
// Priority 1: Emergency Stop
|
||
IF emergency_stop THEN
|
||
internal_state := FALSE;
|
||
emergency_active := TRUE;
|
||
error_state := TRUE;
|
||
error_code := 1; // Emergency stop active
|
||
|
||
// Priority 2: Time Limit Check
|
||
ELSIF on_timer.Q THEN
|
||
internal_state := FALSE;
|
||
time_limit_reached := TRUE;
|
||
error_state := TRUE;
|
||
error_code := 2; // Time limit exceeded
|
||
|
||
// =====================================================
|
||
// CONTROL LOGIC (only when safe)
|
||
// =====================================================
|
||
ELSE
|
||
// Clear safety flags when emergency released
|
||
IF f_trig_emergency.Q THEN
|
||
emergency_active := FALSE;
|
||
error_state := FALSE;
|
||
error_code := 0;
|
||
END_IF;
|
||
|
||
// Clear time limit flag when boiler turned off
|
||
IF NOT internal_state THEN
|
||
time_limit_reached := FALSE;
|
||
IF error_code = 2 THEN
|
||
error_state := FALSE;
|
||
error_code := 0;
|
||
END_IF;
|
||
END_IF;
|
||
|
||
// Priority 3: OFF Commands (HA OFF or Schedule OFF)
|
||
IF r_trig_ha_off.Q OR r_trig_schedule_off.Q THEN
|
||
internal_state := FALSE;
|
||
|
||
// Priority 4: ON Commands (HA ON or Schedule ON)
|
||
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;
|
||
|
||
// =====================================================
|
||
// TIMER MANAGEMENT
|
||
// =====================================================
|
||
|
||
// ON timer - counts total ON time
|
||
on_timer(
|
||
IN := internal_state AND NOT emergency_active,
|
||
PT := max_on_time
|
||
);
|
||
|
||
// Calculate current ON time in seconds
|
||
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;
|
||
|
||
// =====================================================
|
||
// OUTPUT ASSIGNMENT
|
||
// =====================================================
|
||
|
||
// Relay control - only ON when state is ON and no emergency
|
||
relay_control := internal_state AND NOT emergency_active;
|
||
|
||
// State output
|
||
state := internal_state;
|
||
```
|
||
|
||
### 2.5 State Diagram
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ EMERGENCY STOP │
|
||
│ (highest priority) │
|
||
│ Sets: emergency_active = TRUE │
|
||
│ error_code = 1 │
|
||
│ relay_control = FALSE │
|
||
└─────────────────────────────────────────┘
|
||
│
|
||
┌──────────────────┴──────────────────┐
|
||
▼ │
|
||
┌───────────┐ │
|
||
│ │ │
|
||
START ───►│ OFF │◄──────────────────────────────┤
|
||
│ │ │
|
||
└─────┬─────┘ │
|
||
│ │
|
||
│ ON Command │
|
||
│ (ha_on OR schedule_on) │
|
||
│ AND NOT emergency_stop │
|
||
│ AND NOT time_limit_reached │
|
||
▼ │
|
||
┌───────────┐ │
|
||
│ │ Time Limit │
|
||
│ ON │─────Exceeded──────────────────┘
|
||
│ │ (on_timer.Q)
|
||
└─────┬─────┘
|
||
│
|
||
│ OFF Command
|
||
│ (ha_off OR schedule_off)
|
||
│
|
||
└──────────────────────────────────────►OFF
|
||
```
|
||
|
||
---
|
||
|
||
## 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
|
||
PROGRAM PLC_App
|
||
VAR
|
||
// =====================================================
|
||
// LIGHTING CONTROL
|
||
// =====================================================
|
||
// Room instances (fb_room)
|
||
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;
|
||
|
||
// =====================================================
|
||
// WATER BOILER CONTROL
|
||
// =====================================================
|
||
boiler: fb_boiler;
|
||
|
||
// =====================================================
|
||
// GLOBAL COMMANDS
|
||
// =====================================================
|
||
global_all_lights_off: BOOL;
|
||
global_all_lights_on: BOOL;
|
||
END_VAR
|
||
```
|
||
|
||
**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
|
||
|
||
```iec
|
||
// =====================================================
|
||
// PLC_App - Main Program Implementation (Option C: copy of output as feedback)
|
||
// =====================================================
|
||
// EtherCAT_RelayFeedback is filled from the OUTPUT of each room (no hardware read-back).
|
||
// Initialize EtherCAT_RelayFeedback to zero at startup so first cycle has defined behavior.
|
||
|
||
// =====================================================
|
||
// SECTION 1: LIGHTING CONTROL
|
||
// =====================================================
|
||
|
||
// Master Bedroom
|
||
masterBedroom(
|
||
switches := NVL_In.masterBedroom,
|
||
relay_status := EtherCAT_RelayFeedback.masterBedroom // Option C: previous cycle output
|
||
);
|
||
EtherCAT_RelayFeedback.masterBedroom := masterBedroom.lights; // Copy output for next cycle
|
||
NVL_Out.l_masterBedroom := masterBedroom.lights;
|
||
EtherCAT_Outputs.masterBedroom_l1 := masterBedroom.lights.l_1;
|
||
EtherCAT_Outputs.masterBedroom_l2 := masterBedroom.lights.l_2;
|
||
EtherCAT_Outputs.masterBedroom_l3 := masterBedroom.lights.l_3;
|
||
EtherCAT_Outputs.masterBedroom_l4 := masterBedroom.lights.l_4;
|
||
EtherCAT_Outputs.masterBedroom_l5 := masterBedroom.lights.l_5;
|
||
EtherCAT_Outputs.masterBedroom_l6 := masterBedroom.lights.l_6;
|
||
|
||
// Repeat same pattern for all other rooms:
|
||
// masterBathroom( switches := NVL_In.masterBathroom, relay_status := EtherCAT_RelayFeedback.masterBathroom );
|
||
// EtherCAT_RelayFeedback.masterBathroom := masterBathroom.lights;
|
||
// NVL_Out.l_masterBathroom := masterBathroom.lights;
|
||
// EtherCAT_Outputs.masterBathroom_l1 := masterBathroom.lights.l_1; ... l_2..l_6
|
||
// ... bedroom_1, bedroom_2, bathroom, guest_wc, kitchen, pantry, livingRoom, diningRoom,
|
||
// entrance, hallway, veranda, front, back, side (each: call fb_room, copy to EtherCAT_RelayFeedback, NVL_Out, EtherCAT_Outputs)
|
||
|
||
// =====================================================
|
||
// SECTION 2: WATER BOILER CONTROL
|
||
// =====================================================
|
||
|
||
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, // Physical button OR remote
|
||
max_on_time := T#8H
|
||
);
|
||
|
||
// Map outputs
|
||
EtherCAT_Outputs.boiler_relay := boiler.relay_control;
|
||
|
||
// Status to Node-RED
|
||
NVL_Out.boiler_status.state := boiler.state;
|
||
NVL_Out.boiler_status.relay_output := boiler.relay_control;
|
||
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;
|
||
```
|
||
|
||
---
|
||
|
||
## Part 4: I/O Mapping
|
||
|
||
### 4.1 EtherCAT Digital Outputs: EL2809
|
||
|
||
**Module**: Beckhoff **EL2809** – 16-channel digital output, 24 V DC, 0.5 A per channel (overload/short-circuit protected).
|
||
|
||
| Channel | Address (typical) | Function | Description |
|
||
|---------|--------------------|----------|-------------|
|
||
| 1–15 | 16#1A00 … 16#1A0E | Lighting | Room light relays |
|
||
| 16 | 16#1A0F | Boiler | Water boiler relay |
|
||
|
||
The EL2809 has a **16-bit process image** (one word) for the output commands. In CODESYS, the EtherCAT device will expose an output variable (e.g. `EL2809_Outputs` or similar) – map your lighting and boiler control bits to the corresponding channels.
|
||
|
||
### 4.2 EtherCAT Digital Inputs (if using physical emergency stop)
|
||
|
||
| Address | Function | Description |
|
||
|---------|----------|-------------|
|
||
| 16#1900 - 16#190F | Switches | Room physical switches (existing) |
|
||
| TBD | Emergency | Boiler emergency stop button (optional) |
|
||
|
||
### 4.3 How relay feedback is obtained (with EL2809)
|
||
|
||
**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` (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.
|
||
|
||
| Model | Channels | Process image | Notes |
|
||
|-------|----------|---------------|-------|
|
||
| **EL2032** | 2 DO | 2 outputs + 2 diagnostic inputs | 24 V DC, 2 A. |
|
||
| **EL2034** | 4 DO | 4 outputs + 4 diagnostic inputs | 24 V DC, 2 A; short-circuit, line-break. |
|
||
| **EL2042** | 8 DO | 8 outputs + diagnostic inputs | 24 V DC, 0.5 A; confirm in datasheet. |
|
||
| EL2002, EL2004, EL2008 | 2/4/8 DO | Output only | No input. |
|
||
| EL2084, EL2088 | 4/8 DO | Output only | No input. |
|
||
| **EL2809** | 16 DO | **Output only** | No input (your module). |
|
||
|
||
On EL2032/EL2034 the input bits are **diagnostic** (e.g. short-circuit, line-break), not necessarily "output on" state. For true output-state read-back without wiring use Option B (auxiliary contacts) or Option C (copy of output).
|
||
|
||
There are **three practical ways** to get relay feedback:
|
||
|
||
---
|
||
|
||
#### Option A: Output read-back (only for DO modules that have Input/Status)
|
||
|
||
If the EL2809’s EtherCAT configuration in CODESYS exposes an **input** (TxPDO) or **status** word from the device, that is the read-back of the output state. Use the steps and code below. **Note:** The EL2809 datasheet specifies only “16 output bits” in the process image; not all DO modules provide a separate input/read-back. In CODESYS, verify whether an **Input** (or **Status**) process image exists for the EL2809; if it does not, use Option C instead.
|
||
|
||
---
|
||
|
||
**1. EtherCAT process image (reminder)**
|
||
|
||
- **Output process image**: PLC → device. You write here to drive the EL2809 channels (your light/boiler commands).
|
||
- **Input process image**: Device → PLC. If the EL2809 supports read-back, it will send data here (e.g. actual output state or status). This is the TxPDO from the slave.
|
||
|
||
Option A uses this **input** data as relay feedback.
|
||
|
||
---
|
||
|
||
**2. Check whether the EL2809 has read-back in CODESYS**
|
||
|
||
1. Open your CODESYS project and the **Device Tree** (often under **Application** or **EtherCAT Master**).
|
||
2. Expand the EtherCAT master and locate the **EL2809** device.
|
||
3. Open the EL2809 and look at its **I/O Mapping** or **Process Image** (name depends on CODESYS version).
|
||
4. You should see at least:
|
||
- **Output**: one 16-bit word (or 16 BOOLs) – this is what you write to drive the relays.
|
||
5. Check if there is also:
|
||
- **Input**: one 16-bit word (or 16 BOOLs) – data *from* the EL2809.
|
||
If **Input** (or **Status** / **Actual value**) is present, the module supports read-back; use Option A. If only **Output** exists, use Option C.
|
||
|
||
---
|
||
|
||
**3. Where the variable appears**
|
||
|
||
After scanning the bus (or adding the device), CODESYS usually creates symbols for the process image, for example:
|
||
|
||
- Under a **GVL** (Global Variable List) linked to the EtherCAT device, or
|
||
- Under **I/O Mapping** as a linked variable (e.g. `EtherCAT_Master.EL2809.Input` or `EL2809_Input`).
|
||
|
||
The exact path depends on your CODESYS version and how the EtherCAT device was added. Typical patterns:
|
||
|
||
- `GVL.EtherCAT.EL2809_Input` (WORD or 16 BOOLs)
|
||
- `EtherCAT_Master.EL2809.Inputs` or `.Input`
|
||
- Or 16 separate bits: `EL2809_Input_0` … `EL2809_Input_15`
|
||
|
||
If no Input symbol exists, create a **linked variable**: add a new variable in a GVL, set its type to WORD (or ARRAY[0..15] OF BOOL), and link it to the EL2809’s **Input** process image byte/word in the I/O mapping dialog.
|
||
|
||
---
|
||
|
||
**4. Channel-to-light mapping**
|
||
|
||
You must map each EL2809 channel to your feedback structure. Example mapping (adjust to your wiring):
|
||
|
||
| EL2809 channel | Use | Feedback target |
|
||
|----------------|-----|------------------|
|
||
| 0 (bit 0) | Light | e.g. masterBedroom l_1 |
|
||
| 1 | Light | masterBedroom l_2 |
|
||
| … | … | … |
|
||
| 14 | Light | last room / l_6 |
|
||
| 15 | Boiler | boiler relay |
|
||
|
||
Define your own table: which physical channel drives which room’s which light (and which channel is the boiler). Then map the **Input** bits to `EtherCAT_RelayFeedback` accordingly.
|
||
|
||
---
|
||
|
||
**5. Example code: fill `EtherCAT_RelayFeedback` from EL2809 Input**
|
||
|
||
Assume the EL2809’s read-back is available as a **WORD** in a GVL named `GVL_IO`, with the symbol `EL2809_Input` (or as 16 BOOLs `EL2809_Input_0` … `EL2809_Input_15`). Adjust names to match your project.
|
||
|
||
**If you have a WORD (e.g. `GVL_IO.EL2809_Input`):**
|
||
|
||
```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 := 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.
|
||
```
|
||
|
||
**If you have 16 BOOLs (e.g. `GVL_IO.EL2809_Ch0` … `GVL_IO.EL2809_Ch15`):**
|
||
|
||
```iec
|
||
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
|
||
```
|
||
|
||
**Where to call this:** Execute this mapping in the **same task** that runs `PLC_App`, and **before** you call `fb_room` instances (so `EtherCAT_RelayFeedback` is up to date when passed as `relay_status`). Typically this is at the top of `PLC_App` or in a dedicated “I/O update” section.
|
||
|
||
---
|
||
|
||
**6. Ensure `EtherCAT_RelayFeedback` is defined**
|
||
|
||
You need a global (or program-level) variable that holds the feedback for all rooms and the boiler, e.g.:
|
||
|
||
```iec
|
||
VAR_GLOBAL
|
||
EtherCAT_RelayFeedback: STRUCT
|
||
masterBedroom: struct_room_outs;
|
||
masterBathroom: struct_room_outs;
|
||
// ... all rooms ...
|
||
// If boiler feedback is separate: boiler_relay_status: BOOL;
|
||
END_STRUCT;
|
||
END_VAR
|
||
```
|
||
|
||
Each `fb_room` is then called with `relay_status := EtherCAT_RelayFeedback.<room>`. The boiler’s `relay_output` in `struct_boiler_status` can be set from the same EL2809 input bit (channel 15) if you use it for status.
|
||
|
||
---
|
||
|
||
**7. Summary Option A**
|
||
|
||
| Step | Action |
|
||
|------|--------|
|
||
| 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` (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.
|
||
|
||
---
|
||
|
||
#### Option B: Auxiliary contacts on the relays (hardware feedback)
|
||
|
||
The relay has (or you add) an **auxiliary (feedback) contact** that closes when the relay is energized. That contact is wired to a **digital input** on the EtherCAT bus (e.g. spare channels on the EL1809 or an extra DI module).
|
||
|
||
- **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 := 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.**
|
||
|
||
---
|
||
|
||
#### Option C: No hardware feedback – use command as “feedback” (EL2809 without read-back)
|
||
|
||
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**: 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.
|
||
|
||
---
|
||
|
||
#### Summary (with EL2809)
|
||
|
||
| Option | What you need | True feedback? | This project |
|
||
|--------|----------------|----------------|--------------|
|
||
| **A** | DO input/read-back in process image | Yes | Not used (EL2809 has no input). |
|
||
| **B** | Relay auxiliary contacts + DI | Yes | Not used. |
|
||
| **C** | Copy of output variables | No | **Used.** See Section 3.2. |
|
||
|
||
**Option C (chosen):** `fb_room` gets `relay_status` from `EtherCAT_RelayFeedback`. Each cycle, after calling `fb_room`, we set `EtherCAT_RelayFeedback.<room> := <room>.lights`, so the next cycle uses this as relay feedback. Initialize `EtherCAT_RelayFeedback` to zero at startup.
|
||
|
||
---
|
||
|
||
## Part 5: Network Variables
|
||
|
||
### 5.1 NVL_Out (PLC → Node-RED)
|
||
|
||
```iec
|
||
VAR_GLOBAL
|
||
// Lighting status (struct_room_outs per room)
|
||
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
|
||
boiler_status: struct_boiler_status;
|
||
END_VAR
|
||
```
|
||
|
||
### 5.2 NVL_In (Node-RED → PLC)
|
||
|
||
```iec
|
||
VAR_GLOBAL
|
||
// Lighting commands (struct_room_cmds per room)
|
||
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 commands
|
||
boiler: struct_boiler_cmd;
|
||
END_VAR
|
||
```
|
||
|
||
---
|
||
|
||
## Part 6: Error Codes Reference
|
||
|
||
### Boiler Error Codes
|
||
|
||
| Code | Name | Description | Action |
|
||
|------|------|-------------|--------|
|
||
| 0 | No Error | Normal operation | None |
|
||
| 1 | Emergency Stop | Emergency stop activated | Release emergency stop, then send ON command |
|
||
| 2 | Time Limit | Maximum ON time exceeded | Send OFF command, then ON again to reset |
|
||
|
||
---
|
||
|
||
## Part 7: Testing Checklist
|
||
|
||
### Lighting Tests
|
||
|
||
- [ ] HA ON command turns light ON
|
||
- [ ] HA OFF command turns light OFF
|
||
- [ ] Zigbee toggle switches light state
|
||
- [ ] All-off command turns all lights OFF
|
||
- [ ] All-on command turns all lights ON
|
||
- [ ] Relay status feedback is accurate
|
||
- [ ] Multiple commands in sequence work correctly
|
||
|
||
### Boiler Tests
|
||
|
||
- [ ] HA ON command starts boiler
|
||
- [ ] HA OFF command stops boiler
|
||
- [ ] Schedule ON command works
|
||
- [ ] Schedule OFF command works
|
||
- [ ] Emergency stop immediately stops boiler
|
||
- [ ] Emergency stop prevents ON commands
|
||
- [ ] Releasing emergency stop allows normal operation
|
||
- [ ] Time limit auto-shutoff works (test with short time)
|
||
- [ ] ON time counter is accurate
|
||
- [ ] Remaining time is accurate
|
||
- [ ] Error codes are set correctly
|
||
- [ ] Status feedback to HA is correct
|
||
|
||
---
|
||
|
||
## Implementation Notes
|
||
|
||
1. **Redesign alignment**: Lighting control (Part 1) follows **`docs/redesign/fb_switch-redesign-recommendation.md`**: flat `struct_room_cmds` / `struct_room_outs`, `fb_light` (HA ON/OFF + Zigbee toggle), `fb_room` with global commands applied by overwriting outputs. Node-RED should send `ha_l1_on`/`ha_l1_off` and `zigbee_sw1` (edge) per the redesign.
|
||
|
||
2. **Backward Compatibility**: The new lighting structure replaces the old toggle-only logic; Node-RED and HA need to be updated to the new command format.
|
||
|
||
2. **Emergency Stop**: Can be either a physical button (EtherCAT input) or a virtual command from Home Assistant/Node-RED.
|
||
|
||
3. **Time Limit Configuration**: Default is 8 hours but can be changed via `max_on_time` parameter or Node-RED configuration.
|
||
|
||
4. **Task Configuration**: Run on EtherCAT_Task (4ms cycle) for responsive control.
|
||
|
||
---
|
||
|
||
**Document Version**: 1.1
|
||
**Created**: 2026-02-07
|
||
**Updated**: Aligned Part 1 (Lighting) with `docs/redesign/fb_switch-redesign-recommendation.md`.
|
||
**Status**: Design Complete - Ready for Implementation
|