Initial commit: CODESYS tree, integration docs, Node-RED scripts.

PLC application sources (NVL, rooms, boiler), HA/Node-RED integration guide, and NVL patch utilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 21:21:56 +03:00
commit d98c44bfa3
27 changed files with 3844 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,22 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,13 @@
(* === DECLARATION === *)
TYPE struct_lights:
STRUCT
l_1 : BOOL;
l_2 : BOOL;
l_3 : BOOL;
l_4 : BOOL;
l_5 : BOOL;
l_6 : BOOL;
END_STRUCT
END_TYPE
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,32 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,21 @@
(* === DECLARATION === *)
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
END_STRUCT
END_TYPE
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,15 @@
(* === DECLARATION === *)
TYPE struct_switches :
STRUCT
all_off:BOOL;
all_on:BOOL;
sw_1:BOOL;
sw_2:BOOL;
sw_3:BOOL;
sw_4:BOOL;
sw_5:BOOL;
sw_6:BOOL;
END_STRUCT
END_TYPE
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,23 @@
(* === DECLARATION === *)
TYPE struct_zones :
STRUCT
masterBedroom: struct_room_outs;
masterBathroom: struct_room_outs;
bedroom_1: struct_room_outs;
bedroom_2: struct_room_outs;
bathroom: struct_room_outs;
guest_wc: struct_room_outs;
kitchen: struct_room_outs;
pantry: struct_room_outs;
livingRoom: struct_room_outs;
diningRoom: struct_room_outs;
entrance: struct_room_outs;
hallway: struct_room_outs;
veranda: struct_room_outs;
front: struct_room_outs;
back: struct_room_outs;
side: struct_room_outs;
END_STRUCT
END_TYPE
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,8 @@
(* === DECLARATION === *)
{attribute 'qualified_only'}
VAR_GLOBAL
// ---------- Option C: relay feedback (copy of output) ----------
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,26 @@
(* === DECLARATION === *)
//This gobal variable list is received via the network.
//Protocol: UDP
//Layout matches Node-RED nvl-send (no legacy struct_switches removed on PLC)
{attribute 'qualified_only'}
VAR_GLOBAL
cmd_livingRoom : struct_room_cmds;
cmd_dinningRoom : struct_room_cmds;
cmd_kitchenArea : struct_room_cmds;
cmd_hallway : struct_room_cmds;
cmd_office : struct_room_cmds;
cmd_bedroom : struct_room_cmds;
cmd_masterBedroom : struct_room_cmds;
cmd_masterBath : struct_room_cmds;
cmd_shower : struct_room_cmds;
cmd_guestWc : struct_room_cmds;
cmd_outFront : struct_room_cmds;
cmd_coveredVeranda : struct_room_cmds;
cmd_outYard : struct_room_cmds;
cmd_outBack : struct_room_cmds;
boiler_cmd : struct_boiler_cmd;
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,23 @@
(* === DECLARATION === *)
{attribute 'qualified_only'}
VAR_GLOBAL
light_livingRoom : struct_room_outs;
light_dinningRoom : struct_room_outs;
light_kitchenArea : struct_room_outs;
light_hallway : struct_room_outs;
light_office : struct_room_outs;
light_bedroom : struct_room_outs;
light_masterBedroom : struct_room_outs;
light_masterBath : struct_room_outs;
light_shower : struct_room_outs;
light_guestWc : struct_room_outs;
light_outFront : struct_room_outs;
light_coveredVeranda : struct_room_outs;
light_outYard : struct_room_outs;
light_outBack : struct_room_outs;
boiler_status : struct_boiler_status;
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,151 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)
// =====================================================
// 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;

View File

@@ -0,0 +1,57 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)
// =====================================================
// 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;

View File

@@ -0,0 +1,58 @@
(* === DECLARATION === *)
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
(* === IMPLEMENTATION === *)
// =====================================================
// 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;

View File

@@ -0,0 +1,20 @@
(* === DECLARATION === *)
FUNCTION_BLOCK fb_switch
VAR_INPUT
switches: struct_switches;
END_VAR
VAR_OUTPUT
lights: struct_lights;
END_VAR
VAR
all_off: fb_toogleButton;
all_on: fb_toogleButton;
sw_1: fb_toogleButton;
sw_2: fb_toogleButton;
sw_3: fb_toogleButton;
sw_4: fb_toogleButton;
sw_5: fb_toogleButton;
sw_6: fb_toogleButton;
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,25 @@
(* === DECLARATION === *)
FUNCTION_BLOCK fb_toogleButton
VAR_INPUT
Input :BOOL;
END_VAR
VAR_OUTPUT
Output :BOOL;
END_VAR
VAR
Switch: BOOL;
Light: BOOL;
Flag1: BOOL;
Flag2: BOOL;
RT: R_TRIG;
Pulse: TP;
CTU_0: CTU;
CTD_0: CTD;
CTU_1: CTU;
CTU_2: CTU;
TOF_0: TOF;
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,33 @@
(* === DECLARATION === *)
(* Water heater fb_boiler; coil mapped to NVL_Sender.boiler_status.relay_output (%QX5.4). *)
PROGRAM Boiler_PRG
VAR
heater : fb_boiler;
max_on_minutes : INT;
max_on_time : TIME;
END_VAR
(* === IMPLEMENTATION === *)
max_on_minutes := NVL_Receiver.boiler_cmd.max_on_time_minutes;
IF max_on_minutes <= 0 THEN
max_on_minutes := 480;
END_IF;
max_on_time := T#1M * max_on_minutes;
heater(
ha_on := NVL_Receiver.boiler_cmd.ha_on,
ha_off := NVL_Receiver.boiler_cmd.ha_off,
schedule_on := NVL_Receiver.boiler_cmd.schedule_on,
schedule_off := NVL_Receiver.boiler_cmd.schedule_off,
emergency_stop := NVL_Receiver.boiler_cmd.emergency_stop,
max_on_time := max_on_time
);
NVL_Sender.boiler_status.state := heater.state;
NVL_Sender.boiler_status.relay_output := heater.relay_control;
NVL_Sender.boiler_status.on_time_minutes := DINT_TO_INT(heater.on_time_seconds / 60);
NVL_Sender.boiler_status.remaining_minutes := DINT_TO_INT(heater.remaining_seconds / 60);
NVL_Sender.boiler_status.emergency_active := heater.emergency_active;
NVL_Sender.boiler_status.time_limit_reached := heater.time_limit_reached;
NVL_Sender.boiler_status.error_state := heater.error_state;
NVL_Sender.boiler_status.error_code := heater.error_code;

View File

@@ -0,0 +1,22 @@
(* === DECLARATION === *)
PROGRAM Lights
VAR
masterBedroom : fb_switch;
masterBathroom: fb_switch;
bedroom_1: fb_switch;
bedroom_2: fb_switch;
bathroom: fb_switch;
guest_wc: fb_switch;
kitchen: fb_switch;
pantry: fb_switch;
livingRoom: fb_switch;
diningRoom: fb_switch;
veranda: fb_switch;
entrance: fb_switch;
front: fb_switch;
back: fb_switch;
side: fb_switch;
hallway: fb_switch;
END_VAR
(* === IMPLEMENTATION === *)

View File

@@ -0,0 +1,116 @@
(* === DECLARATION === *)
(* Room lighting (redesign). Shower channels l_1..l_5 only l_6 is water heater (Boiler_PRG). *)
PROGRAM Lights_new
VAR
Living_Room: FB_ROOM;
Dininng_Room: fb_room;
Kitchen_Area: fb_room;
Hallway: fb_room;
Office: fb_room;
Bedroom: fb_room;
Master_Bedroom: fb_room;
Master_bath: fb_room;
Shower: fb_room;
Guest_wc: fb_room;
Outside_front: fb_room;
Covered_Veranda: fb_room;
Yard: fb_room;
Outside_back: fb_room;
END_VAR
(* === IMPLEMENTATION === *)
Living_Room(
switches := NVL_Receiver.cmd_livingRoom,
relay_status := NVL_Sender.light_livingRoom
);
NVL_Sender.light_livingRoom := Living_Room.lights;
Dininng_Room(
switches := NVL_Receiver.cmd_dinningRoom,
relay_status := NVL_Sender.light_dinningRoom
);
NVL_Sender.light_dinningRoom := Dininng_Room.lights;
Kitchen_Area(
switches := NVL_Receiver.cmd_kitchenArea,
relay_status := NVL_Sender.light_kitchenArea
);
NVL_Sender.light_kitchenArea := Kitchen_Area.lights;
Hallway(
switches := NVL_Receiver.cmd_hallway,
relay_status := NVL_Sender.light_hallway
);
NVL_Sender.light_hallway := Hallway.lights;
Office(
switches := NVL_Receiver.cmd_office,
relay_status := NVL_Sender.light_office
);
NVL_Sender.light_office := Office.lights;
Bedroom(
switches := NVL_Receiver.cmd_bedroom,
relay_status := NVL_Sender.light_bedroom
);
NVL_Sender.light_bedroom := Bedroom.lights;
Master_Bedroom(
switches := NVL_Receiver.cmd_masterBedroom,
relay_status := NVL_Sender.light_masterBedroom
);
NVL_Sender.light_masterBedroom := Master_Bedroom.lights;
Master_bath(
switches := NVL_Receiver.cmd_masterBath,
relay_status := NVL_Sender.light_masterBath
);
NVL_Sender.light_masterBath := Master_bath.lights;
Shower(
switches := NVL_Receiver.cmd_shower,
relay_status := NVL_Sender.light_shower
);
// l_6 not used for shower water heater uses boiler_status.relay_output (%QX5.4)
NVL_Sender.light_shower.l_6 := FALSE;
NVL_Sender.light_shower.l_6_status := FALSE;
NVL_Sender.light_shower.l_1 := Shower.lights.l_1;
NVL_Sender.light_shower.l_2 := Shower.lights.l_2;
NVL_Sender.light_shower.l_3 := Shower.lights.l_3;
NVL_Sender.light_shower.l_4 := Shower.lights.l_4;
NVL_Sender.light_shower.l_5 := Shower.lights.l_5;
NVL_Sender.light_shower.l_1_status := Shower.lights.l_1;
NVL_Sender.light_shower.l_2_status := Shower.lights.l_2;
NVL_Sender.light_shower.l_3_status := Shower.lights.l_3;
NVL_Sender.light_shower.l_4_status := Shower.lights.l_4;
NVL_Sender.light_shower.l_5_status := Shower.lights.l_5;
Guest_wc(
switches := NVL_Receiver.cmd_guestWc,
relay_status := NVL_Sender.light_guestWc
);
NVL_Sender.light_guestWc := Guest_wc.lights;
Outside_front(
switches := NVL_Receiver.cmd_outFront,
relay_status := NVL_Sender.light_outFront
);
NVL_Sender.light_outFront := Outside_front.lights;
Covered_Veranda(
switches := NVL_Receiver.cmd_coveredVeranda,
relay_status := NVL_Sender.light_coveredVeranda
);
NVL_Sender.light_coveredVeranda := Covered_Veranda.lights;
Yard(
switches := NVL_Receiver.cmd_outYard,
relay_status := NVL_Sender.light_outYard
);
NVL_Sender.light_outYard := Yard.lights;
Outside_back(
switches := NVL_Receiver.cmd_outBack,
relay_status := NVL_Sender.light_outBack
);
NVL_Sender.light_outBack := Outside_back.lights;

View File

@@ -0,0 +1,10 @@
(* === DECLARATION === *)
PROGRAM PLC_PRG
VAR
masterBedroom: fb_switch;
END_VAR
(* === IMPLEMENTATION === *)
// Room lights first; Boiler_PRG last (dedicated boiler_status.relay_output, not shower l_6).
Lights_new();
Boiler_PRG();

118
codesys-tree/README.md Normal file
View File

@@ -0,0 +1,118 @@
# Local CODESYS Tree
Local mirror of the CODESYS project staged at:
`C:\codesys-projects\Codesys_Home_Automation\Home_Automation.project`
This tree was generated from the `read_project_inventory` MCP call so you can edit
POUs, GVLs, DUTs and Programs directly in Cursor on macOS, then push the changes
back to the Windows VM with the `sync_project` MCP tool.
## Layout
Mirrors the CODESYS POU pane:
```
Device/
Plc Logic/
Application/
PLC_PRG.st
0.Structures/ - DUTs (struct_*)
1.Variables/ - GVLs (NVL_Sender, NVL_Receiver, GVL)
2.Functions/ - (empty)
3.Function Blocks/
4.Programs/
```
Hardware / non-textual nodes (`Device`, `EtherCAT_Master`, `EK1100`, `K1..K4`,
`GPIOs_A_B`, `Onewire`, `Camera device`, `SPI`, `I?C`, `Task Configuration`,
`Library Manager`, `Project Settings`, `__VisualizationStyle`) are **not** mirrored
as files — they live only in the .project XML and are listed in
`_manifest.json` under `non_textual_tree` for reference.
## File format (.st files)
Each file contains a single CODESYS object. Two sections are delimited by sentinel
banner comments so the splitter is unambiguous:
```
(* === DECLARATION === *)
<everything that goes in the declaration editor (VAR blocks, header, attribs)>
(* === IMPLEMENTATION === *)
<the ST body — empty for DUTs / GVLs / declaration-only programs>
```
If a section is empty, leave the line after its banner blank — do not delete the
banner. The sync helper expects both banners to be present.
## Manifest
`_manifest.json` maps every `.st` file back to its CODESYS `object_path`
(e.g. `Device / Plc Logic / Application / 3.Function Blocks / fb_light`) and
records whether the object originally had a declaration and/or implementation.
This is what the sync step uses to build the `textual_updates` payload.
## I/O snapshot
`_io.json` is the raw output of `read_device_io` (all devices + channels +
variable mappings + IEC addresses).
`_io.md` is the human-readable summary: per-device tables, current
mappings against `NVL_Sender.*`, plus a "findings worth flagging" section
that highlights duplicates, unmapped channels, and gaps between the FB logic
and the physical wiring.
Refresh both with `read_device_io` then regenerate. The IO snapshot is
**read-only here** — to push mapping changes back, use the `write_io_mapping`
tool (or the `io_mappings` list inside `sync_project`).
## Workflow
### 1. Edit locally
Open any `.st` file in Cursor and edit either the declaration or the implementation
section. Don't move the banner lines.
### 2. Dry-run sync (preview)
Call the MCP tool `sync_project` with `dry_run: true` and a `textual_updates`
list built from the files you changed. Each entry looks like:
```json
{
"object_path": "Device / Plc Logic / Application / 3.Function Blocks / fb_light",
"declaration": "FUNCTION_BLOCK fb_light\nVAR_INPUT\n ...\nEND_VAR",
"implementation": "// ST body here"
}
```
### 3. Apply
Re-run `sync_project` with `dry_run: false`. The agent opens the project once,
applies all updates in a single open/save cycle, and reports back per-object
status.
### 4. Build
After a successful sync, run `build_project` to verify the project still
compiles on the VM.
## Re-syncing from CODESYS → local
If the project changes on the Windows side, re-run `read_project_inventory` and
regenerate the affected `.st` files plus `_manifest.json`. Treat the VM project
file as the source of truth for the **structure** (folders, object kinds, IO
mappings); treat the local tree as the source of truth only for content you've
edited here.
## Not included
These items live in the project but are not mirrored as files:
- `Library Manager` (library refs — managed in CODESYS UI)
- `Task Configuration` / `MainTask` / `EtherCAT_Task` (task settings)
- Device / fieldbus nodes (`Device`, `EtherCAT_Master`, `EK1100`, `K1..K4`,
`GPIOs_A_B`, `Onewire`, `Camera device`, `SPI`, `I?C`) — manage via
`manage_device`, `read_device_io`, `write_io_mapping`.
- `Project Settings`, `SoftMotion General Axis Pool`, `__VisualizationStyle`.

2204
codesys-tree/_io.json Normal file

File diff suppressed because it is too large Load Diff

164
codesys-tree/_io.md Normal file
View File

@@ -0,0 +1,164 @@
# Device I/O Inventory
Snapshot of all hardware I/O channels and their CODESYS variable mappings.
Source: `read_device_io` against `C:\codesys-projects\Codesys_Home_Automation\Home_Automation.project`
(channels_only=true). Raw data: `_io.json`.
## Device tree (hardware)
| Device | Path | In | Out | Mapped out | Addr range |
|--------|------|---:|---:|---:|------------|
| GPIOs_A_B | `Device/GPIOs_A_B` | 1 (DWORD) | 1 (DWORD) | 0 | `%ID1` / `%QD2` |
| K1 | `Device/EtherCAT_Master/EK1100/K1` | 16 BOOL | 0 | — | `%IX0.0..%IX1.7` |
| K2 | `Device/EtherCAT_Master/EK1100/K2` | 0 | 16 BOOL | 14 / 16 | `%QX0.0..%QX1.7` |
| K3 | `Device/EtherCAT_Master/EK1100/K3` | 0 | 16 BOOL | 15 / 16 | `%QX2.0..%QX3.7` |
| K4 | `Device/EtherCAT_Master/EK1100/K4` | 0 | 16 BOOL | 12 / 16 | `%QX4.0..%QX5.7` |
`Onewire`, `Camera device`, `SPI`, `I?C`, `EtherCAT_Master` and `EK1100`
return no host parameters / channels of their own (they're parents/couplers).
**Totals:** 16 digital inputs (none mapped), 48 digital outputs (41 mapped, 7 free).
## Findings worth flagging
### Duplicate output mapping (likely a bug)
`Application.NVL_Sender.light_livingRoom.l_1` is written by **two** physical output channels:
| Where | IEC addr |
|-------|----------|
| K2 ch 10 | `%QX1.1` |
| K4 ch 16 | `%QX5.7` |
Both physical relays will switch together whenever the variable is written.
One of these should be remapped — usually `K4 %QX5.7` would be the one to free up
(based on K4's role as the spillover terminal).
### Unmapped output channels
| Device | IEC addr | Channel |
|--------|----------|---------|
| GPIOs_A_B | `%QD2` | (DWORD) |
| K2 | `%QX0.5` | Ch 6 |
| K2 | `%QX0.7` | Ch 8 |
| K3 | `%QX2.3` | Ch 4 |
| K4 | `%QX5.2` | Ch 11 |
| K4 | `%QX5.3` | Ch 12 |
| K4 | `%QX5.5` | Ch 14 |
| K4 | `%QX5.6` | Ch 15 |
### No inputs mapped
K1 (16-channel digital input terminal, `%IX0.0..%IX1.7`) is wired up at hardware
level but has zero variable mappings. If you have physical switches on K1, the
PLC currently can't see them.
### Variable target
All mapped outputs write into `Application.NVL_Sender.*` — i.e. the global
variable list that is *broadcast over UDP* to the receiver. This is the "Option C"
relay-feedback pattern referenced in the `GVL` comment: the program writes the
desired relay state into the NVL_Sender variable, the EtherCAT terminal switches
the actual relay, and the value read back is exactly the value just written.
No outputs are mapped against the application's room/light function block outputs
(`fb_room.lights.*` / `fb_light.light_output`). The new `fb_room` / `fb_light`
logic is therefore not yet driving the physical relays — that wiring would need
to be done in `PLC_PRG` (or `Lights_new`) by assigning each FB output into the
corresponding `NVL_Sender.light_*.l_N` slot before the IO write phase.
## Per-channel detail
### K1 (`Device/EtherCAT_Master/EK1100/K1`)
| Ch | IEC addr | Dir | Variable |
|---:|----------|-----|----------|
| 1 | `%IX0.0` | INP | _(unmapped)_ |
| 2 | `%IX0.1` | INP | _(unmapped)_ |
| 3 | `%IX0.2` | INP | _(unmapped)_ |
| 4 | `%IX0.3` | INP | _(unmapped)_ |
| 5 | `%IX0.4` | INP | _(unmapped)_ |
| 6 | `%IX0.5` | INP | _(unmapped)_ |
| 7 | `%IX0.6` | INP | _(unmapped)_ |
| 8 | `%IX0.7` | INP | _(unmapped)_ |
| 9 | `%IX1.0` | INP | _(unmapped)_ |
| 10 | `%IX1.1` | INP | _(unmapped)_ |
| 11 | `%IX1.2` | INP | _(unmapped)_ |
| 12 | `%IX1.3` | INP | _(unmapped)_ |
| 13 | `%IX1.4` | INP | _(unmapped)_ |
| 14 | `%IX1.5` | INP | _(unmapped)_ |
| 15 | `%IX1.6` | INP | _(unmapped)_ |
| 16 | `%IX1.7` | INP | _(unmapped)_ |
### K2 (`Device/EtherCAT_Master/EK1100/K2`)
| Ch | IEC addr | Dir | Variable |
|---:|----------|-----|----------|
| 1 | `%QX0.0` | OUT | `Application.NVL_Sender.light_dinningRoom.l_1` |
| 2 | `%QX0.1` | OUT | `Application.NVL_Sender.l_entrance.l_1` |
| 3 | `%QX0.2` | OUT | `Application.NVL_Sender.light_dinningRoom.l_2` |
| 4 | `%QX0.3` | OUT | `Application.NVL_Sender.light_dinningRoom.l_3` |
| 5 | `%QX0.4` | OUT | `Application.NVL_Sender.light_guestWc.l_1` |
| 6 | `%QX0.5` | OUT | _(unmapped)_ |
| 7 | `%QX0.6` | OUT | `Application.NVL_Sender.light_dinningRoom.l_4` |
| 8 | `%QX0.7` | OUT | _(unmapped)_ |
| 9 | `%QX1.0` | OUT | `Application.NVL_Sender.l_entrance.l_2` |
| 10 | `%QX1.1` | OUT | `Application.NVL_Sender.light_livingRoom.l_1` ⚠ duplicated on K4 ch 16 |
| 11 | `%QX1.2` | OUT | `Application.NVL_Sender.light_livingRoom.l_2` |
| 12 | `%QX1.3` | OUT | `Application.NVL_Sender.light_livingRoom.l_3` |
| 13 | `%QX1.4` | OUT | `Application.NVL_Sender.light_livingRoom.l_4` |
| 14 | `%QX1.5` | OUT | `Application.NVL_Sender.light_kitchenArea.l_1` |
| 15 | `%QX1.6` | OUT | `Application.NVL_Sender.light_kitchenArea.l_2` |
| 16 | `%QX1.7` | OUT | `Application.NVL_Sender.light_kitchenArea.l_3` |
### K3 (`Device/EtherCAT_Master/EK1100/K3`)
| Ch | IEC addr | Dir | Variable |
|---:|----------|-----|----------|
| 1 | `%QX2.0` | OUT | `Application.NVL_Sender.light_kitchenArea.l_4` |
| 2 | `%QX2.1` | OUT | `Application.NVL_Sender.light_hallway.l_1` |
| 3 | `%QX2.2` | OUT | `Application.NVL_Sender.light_office.l_1` |
| 4 | `%QX2.3` | OUT | _(unmapped)_ |
| 5 | `%QX2.4` | OUT | `Application.NVL_Sender.light_shower.l_1` |
| 6 | `%QX2.5` | OUT | `Application.NVL_Sender.light_shower.l_2` |
| 7 | `%QX2.6` | OUT | `Application.NVL_Sender.light_hallway.l_2` |
| 8 | `%QX2.7` | OUT | `Application.NVL_Sender.light_bedroom.l_1` |
| 9 | `%QX3.0` | OUT | `Application.NVL_Sender.light_bedroom.l_2` |
| 10 | `%QX3.1` | OUT | `Application.NVL_Sender.light_masterBedroom.l_1` |
| 11 | `%QX3.2` | OUT | `Application.NVL_Sender.light_masterBedroom.l_2` |
| 12 | `%QX3.3` | OUT | `Application.NVL_Sender.light_masterBedroom.l_3` |
| 13 | `%QX3.4` | OUT | `Application.NVL_Sender.light_masterBedroom.l_4` |
| 14 | `%QX3.5` | OUT | `Application.NVL_Sender.light_masterBath.l_1` |
| 15 | `%QX3.6` | OUT | `Application.NVL_Sender.light_masterBath.l_2` |
| 16 | `%QX3.7` | OUT | `Application.NVL_Sender.light_masterBath.l_3` |
### K4 (`Device/EtherCAT_Master/EK1100/K4`)
| Ch | IEC addr | Dir | Variable |
|---:|----------|-----|----------|
| 1 | `%QX4.0` | OUT | `Application.NVL_Sender.light_livingRoom.l_5` |
| 2 | `%QX4.1` | OUT | `Application.NVL_Sender.light_outBack.l_1` |
| 3 | `%QX4.2` | OUT | `Application.NVL_Sender.light_outBack.l_2` |
| 4 | `%QX4.3` | OUT | `Application.NVL_Sender.light_outFront.l_2` |
| 5 | `%QX4.4` | OUT | `Application.NVL_Sender.light_coveredVeranda.l_2` |
| 6 | `%QX4.5` | OUT | `Application.NVL_Sender.light_coveredVeranda.l_3` |
| 7 | `%QX4.6` | OUT | `Application.NVL_Sender.light_coveredVeranda.l_4` |
| 8 | `%QX4.7` | OUT | `Application.NVL_Sender.l_outSide.l_1` |
| 9 | `%QX5.0` | OUT | `Application.NVL_Sender.l_outFront.l_1` |
| 10 | `%QX5.1` | OUT | `Application.NVL_Sender.l_outVeranda.l_4` |
| 11 | `%QX5.2` | OUT | _(unmapped)_ |
| 12 | `%QX5.3` | OUT | _(unmapped)_ |
| 13 | `%QX5.4` | OUT | `Application.NVL_Sender.boiler_status.relay_output` |
| 14 | `%QX5.5` | OUT | _(unmapped)_ |
| 15 | `%QX5.6` | OUT | _(unmapped)_ |
| 16 | `%QX5.7` | OUT | `Application.NVL_Sender.light_livingRoom.l_1` ⚠ duplicates K2 ch 10 |
### GPIOs_A_B (`Device/GPIOs_A_B`)
| Ch | IEC addr | Dir | Variable |
|---:|----------|-----|----------|
| GPIOs in | `%ID1` | INP (DWORD) | _(unmapped)_ |
| GPIOs out | `%QD2` | OUT (DWORD) | _(unmapped)_ |
The Raspberry Pi onboard GPIO bank exists in the project but is not wired to any
variables.

185
codesys-tree/_manifest.json Normal file
View File

@@ -0,0 +1,185 @@
{
"project_path": "C:\\codesys-projects\\Codesys_Home_Automation\\Home_Automation.project",
"staged_project_path": "C:\\codesys-projects\\Codesys_Home_Automation\\Home_Automation_Boiler_Phase1\\Home_Automation.project",
"staged_project_folder": "Home_Automation_Boiler_Phase1",
"active_application": "Application",
"generated_at": "2026-05-24T20:24:00+03:00",
"inventory_refreshed_at": "2026-05-24T20:24:00+03:00",
"io_snapshot": {
"file": "_io.json",
"summary": "_io.md",
"captured_at": "2026-05-24T20:24:00+03:00",
"device_count": 9,
"channels_only": true
},
"convention": {
"declaration_marker": "(* === DECLARATION === *)",
"implementation_marker": "(* === IMPLEMENTATION === *)",
"notes": "Each .st file contains the declaration section followed by the implementation section, separated by the markers above. Empty section after a marker means CODESYS reported no content for that section."
},
"objects": [
{
"file": "Device/Plc Logic/Application/PLC_PRG.st",
"object_path": "Device / Plc Logic / Application / PLC_PRG",
"kind": "program",
"has_declaration": true,
"has_implementation": true
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_switches.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_switches",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_lights.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_lights",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_room_outs.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_room_outs",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_room_cmds.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_room_cmds",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_boiler_cmd.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_boiler_cmd",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_boiler_status.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_boiler_status",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/0.Structures/struct_zones.st",
"object_path": "Device / Plc Logic / Application / 0.Structures / struct_zones",
"kind": "dut",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/1.Variables/NVL_Sender.st",
"object_path": "Device / Plc Logic / Application / 1.Variables / NVL_Sender",
"kind": "gvl",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/1.Variables/NVL_Receiver.st",
"object_path": "Device / Plc Logic / Application / 1.Variables / NVL_Receiver",
"kind": "gvl",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/1.Variables/GVL.st",
"object_path": "Device / Plc Logic / Application / 1.Variables / GVL",
"kind": "gvl",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/3.Function Blocks/fb_toogleButton.st",
"object_path": "Device / Plc Logic / Application / 3.Function Blocks / fb_toogleButton",
"kind": "function_block",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/3.Function Blocks/fb_switch.st",
"object_path": "Device / Plc Logic / Application / 3.Function Blocks / fb_switch",
"kind": "function_block",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/3.Function Blocks/fb_light.st",
"object_path": "Device / Plc Logic / Application / 3.Function Blocks / fb_light",
"kind": "function_block",
"has_declaration": true,
"has_implementation": true
},
{
"file": "Device/Plc Logic/Application/3.Function Blocks/fb_room.st",
"object_path": "Device / Plc Logic / Application / 3.Function Blocks / fb_room",
"kind": "function_block",
"has_declaration": true,
"has_implementation": true
},
{
"file": "Device/Plc Logic/Application/3.Function Blocks/fb_boiler.st",
"object_path": "Device / Plc Logic / Application / 3.Function Blocks / fb_boiler",
"kind": "function_block",
"has_declaration": true,
"has_implementation": true
},
{
"file": "Device/Plc Logic/Application/4.Programs/Lights.st",
"object_path": "Device / Plc Logic / Application / 4.Programs / Lights",
"kind": "program",
"has_declaration": true,
"has_implementation": false
},
{
"file": "Device/Plc Logic/Application/4.Programs/Lights_new.st",
"object_path": "Device / Plc Logic / Application / 4.Programs / Lights_new",
"kind": "program",
"has_declaration": true,
"has_implementation": true
},
{
"file": "Device/Plc Logic/Application/4.Programs/Boiler_PRG.st",
"object_path": "Device / Plc Logic / Application / 4.Programs / Boiler_PRG",
"kind": "program",
"has_declaration": true,
"has_implementation": true
}
],
"non_textual_tree": [
{"depth": 0, "kind": "node", "name": "Project Settings"},
{"depth": 0, "kind": "device", "name": "Device"},
{"depth": 1, "kind": "node", "name": "Plc Logic"},
{"depth": 2, "kind": "application", "name": "Application"},
{"depth": 3, "kind": "node", "name": "Library Manager"},
{"depth": 3, "kind": "node", "name": "Task Configuration"},
{"depth": 4, "kind": "node", "name": "MainTask"},
{"depth": 5, "kind": "node", "name": "PLC_PRG"},
{"depth": 4, "kind": "node", "name": "EtherCAT_Task"},
{"depth": 3, "kind": "folder", "name": "3.Function Blocks"},
{"depth": 3, "kind": "folder", "name": "1.Variables"},
{"depth": 3, "kind": "folder", "name": "2.Functions"},
{"depth": 3, "kind": "folder", "name": "4.Programs"},
{"depth": 3, "kind": "folder", "name": "0.Structures"},
{"depth": 1, "kind": "node", "name": "SoftMotion General Axis Pool"},
{"depth": 1, "kind": "device", "name": "GPIOs_A_B"},
{"depth": 1, "kind": "node", "name": "Onewire"},
{"depth": 1, "kind": "node", "name": "Camera device"},
{"depth": 2, "kind": "device", "name": "<Empty>"},
{"depth": 1, "kind": "node", "name": "SPI"},
{"depth": 1, "kind": "node", "name": "I?C"},
{"depth": 1, "kind": "device", "name": "EtherCAT_Master"},
{"depth": 2, "kind": "device", "name": "EK1100"},
{"depth": 3, "kind": "device", "name": "K1"},
{"depth": 3, "kind": "device", "name": "K2"},
{"depth": 3, "kind": "device", "name": "K3"},
{"depth": 3, "kind": "device", "name": "K4"},
{"depth": 0, "kind": "node", "name": "__VisualizationStyle"}
]
}