Files
kkelomatic_home/docs/codesys/plc-algorithm-design.md
nearxos 6ef31cd12a 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.
2026-02-08 00:47:57 +02:00

46 KiB
Raw Blame History

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. Its 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 dont “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 doesnt 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. Thats a refinement of the same idea, not a different architecture.

So: you dont 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 theyre 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)

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".

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.

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)

// =====================================================
// 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.

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

// =====================================================
// 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)

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)

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.

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

// =====================================================
// 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_1l_6, Ch6..Ch11 to NVL_Out.l_masterBathroom.l_1l_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):

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
  1. 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.

  2. 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

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

// =====================================================
// 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
115 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 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.

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 EL2809s 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_0EL2809_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 EL2809s 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 rooms 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 EL2809s read-back is available as a WORD in a GVL named GVL_IO, with the symbol EL2809_Input (or as 16 BOOLs EL2809_Input_0EL2809_Input_15). Adjust names to match your project.

If you have a WORD (e.g. GVL_IO.EL2809_Input):

// 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_Ch0GVL_IO.EL2809_Ch15):

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.:

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 boilers 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 EL2809s 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 1s auxiliary contact.)

So: relay feedback = state of the DI that is wired to the relays 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)

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)

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.

  3. Emergency Stop: Can be either a physical button (EtherCAT input) or a virtual command from Home Assistant/Node-RED.

  4. Time Limit Configuration: Default is 8 hours but can be changed via max_on_time parameter or Node-RED configuration.

  5. 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