- 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.
5.5 KiB
Node-RED: NVL payload layout
CODESYS sends and receives binary UDP packets (struct layout). Use this layout in Node-RED to parse NVL_Out (PLC → Node-RED) and to build NVL_In (Node-RED → PLC).
Important: Packing and alignment depend on the CODESYS runtime. Typical: BOOL = 1 byte, INT = 2 bytes (little-endian on Intel/ARM). If your platform uses different alignment, adjust offsets. You can log the first few packets and compare with the PLC online values to confirm.
1. Type sizes (typical)
| Type | Size (bytes) |
|---|---|
| BOOL | 1 |
| INT | 2 |
| DINT | 4 |
2. NVL_Out (PLC → Node-RED) – receive and parse
PLC sends one block: all rooms’ struct_room_outs followed by struct_boiler_status.
struct_room_outs (per room)
6 BOOLs in order: l_1, l_2, l_3, l_4, l_5, l_6 (light state; with Option C this is both output and status).
- Size per room: 6 bytes.
- Rooms in order: l_masterBedroom, l_masterBathroom, l_bedroom_1, l_bedroom_2, l_bathroom, l_guestWc, l_kitchen, l_pantry, l_livingRoom, l_dinningRoom, l_entrance, l_hallway, l_outVeranda, l_outFront, l_outBack, l_outSide.
- Total for lights: 16 × 6 = 96 bytes.
struct_boiler_status (after all rooms)
| Offset (from start of boiler_status) | Type | Field |
|---|---|---|
| 0 | BOOL | state |
| 1 | BOOL | relay_output |
| 2 | INT | on_time_minutes |
| 4 | INT | remaining_minutes |
| 6 | BOOL | emergency_active |
| 7 | BOOL | time_limit_reached |
| 8 | BOOL | error_state |
| 9 | BOOL | (padding possible) |
| 10 | INT | error_code |
Size: 12 bytes (or 16 if aligned). So total NVL_Out ≈ 96 + 12 = 108 bytes (or 112 if boiler is aligned).
Node-RED: parse NVL_Out (example)
// msg.payload = Buffer (UDP payload from PLC)
const buf = msg.payload;
const roomSize = 6;
const roomNames = ['l_masterBedroom','l_masterBathroom','l_bedroom_1','l_bedroom_2','l_bathroom','l_guestWc','l_kitchen','l_pantry','l_livingRoom','l_dinningRoom','l_entrance','l_hallway','l_outVeranda','l_outFront','l_outBack','l_outSide'];
const out = { rooms: {}, boiler_status: {} };
for (let r = 0; r < 16; r++) {
const base = r * roomSize;
out.rooms[roomNames[r]] = {
l_1: !!buf[base+0], l_2: !!buf[base+1], l_3: !!buf[base+2],
l_4: !!buf[base+3], l_5: !!buf[base+4], l_6: !!buf[base+5]
};
}
const bo = 96; // offset of boiler_status
out.boiler_status = {
state: !!buf[bo+0],
relay_output: !!buf[bo+1],
on_time_minutes: buf.readInt16LE(bo+2),
remaining_minutes: buf.readInt16LE(bo+4),
emergency_active: !!buf[bo+6],
time_limit_reached: !!buf[bo+7],
error_state: !!buf[bo+8],
error_code: buf.readInt16LE(bo+10)
};
msg.payload = out;
return msg;
3. NVL_In (Node-RED → PLC) – build and send
PLC expects one block: all rooms’ struct_room_cmds followed by struct_boiler_cmd.
struct_room_cmds (per room)
20 BOOLs in order:
ha_l1_on, ha_l1_off, ha_l2_on, ha_l2_off, ha_l3_on, ha_l3_off, ha_l4_on, ha_l4_off, ha_l5_on, ha_l5_off, ha_l6_on, ha_l6_off, zigbee_sw1..6, ha_all_on, ha_all_off.
- Size per room: 20 bytes.
- Rooms in order: masterBedroom, masterBathroom, bedroom_1, bedroom_2, bathroom, guestWc, kitchen, pantry, livingRoom, dinningRoom, entrance, hallway, outVeranda, outFront, outBack, outSide.
- Total for rooms: 16 × 20 = 320 bytes.
struct_boiler_cmd
| Offset | Type | Field |
|---|---|---|
| 0 | BOOL | ha_on |
| 1 | BOOL | ha_off |
| 2 | BOOL | schedule_on |
| 3 | BOOL | schedule_off |
| 4 | BOOL | emergency_stop |
| 5 | (padding) | - |
| 6 | INT | max_on_time_minutes |
Size: 8 bytes. So total NVL_In = 320 + 8 = 328 bytes.
Node-RED: build NVL_In (example)
// Build binary payload for PLC from commands
const roomSize = 20;
const roomNames = ['masterBedroom','masterBathroom','bedroom_1','bedroom_2','bathroom','guestWc','kitchen','pantry','livingRoom','dinningRoom','entrance','hallway','outVeranda','outFront','outBack','outSide'];
const buf = Buffer.alloc(328);
const rooms = msg.payload.rooms || {};
const boiler = msg.payload.boiler || {};
for (let r = 0; r < 16; r++) {
const c = rooms[roomNames[r]] || {};
const base = r * roomSize;
buf[base+0] = c.ha_l1_on ? 1 : 0; buf[base+1] = c.ha_l1_off ? 1 : 0;
buf[base+2] = c.ha_l2_on ? 1 : 0; buf[base+3] = c.ha_l2_off ? 1 : 0;
buf[base+4] = c.ha_l3_on ? 1 : 0; buf[base+5] = c.ha_l3_off ? 1 : 0;
buf[base+6] = c.ha_l4_on ? 1 : 0; buf[base+7] = c.ha_l4_off ? 1 : 0;
buf[base+8] = c.ha_l5_on ? 1 : 0; buf[base+9] = c.ha_l5_off ? 1 : 0;
buf[base+10]= c.ha_l6_on ? 1 : 0; buf[base+11]= c.ha_l6_off ? 1 : 0;
for (let i = 0; i < 6; i++) buf[base+12+i] = c['zigbee_sw'+(i+1)] ? 1 : 0;
buf[base+18] = c.ha_all_on ? 1 : 0;
buf[base+19] = c.ha_all_off ? 1 : 0;
}
const bo = 320;
buf[bo+0] = boiler.ha_on ? 1 : 0;
buf[bo+1] = boiler.ha_off ? 1 : 0;
buf[bo+2] = boiler.schedule_on ? 1 : 0;
buf[bo+3] = boiler.schedule_off ? 1 : 0;
buf[bo+4] = boiler.emergency_stop ? 1 : 0;
buf.writeInt16LE(boiler.max_on_time_minutes ?? 480, bo+6);
msg.payload = buf;
return msg;
4. Ports and flow
- NVL_Out (PLC → Node-RED): Node-RED UDP In listens on e.g. 5555 (same as “destination port” in CODESYS NVL Sender).
- NVL_In (Node-RED → PLC): Node-RED UDP Out sends to PLC_IP:5556 (5556 = listen port of CODESYS NVL Receiver).
Send NVL_In when commands change (or at a fixed interval, e.g. 100 ms) so the PLC always has the latest commands.