Refine PLC algorithm documentation for lighting control and EtherCAT integration
- Updated the output structure for `struct_room_outs` to focus solely on light states, removing unnecessary status feedback fields. - Simplified the control logic for lights 1 to 6, aligning with the new design approach for Option C. - Added detailed instructions for declaring `EtherCAT_RelayFeedback` and `EtherCAT_Outputs` in the Global Variable List, enhancing clarity for integration with the EL2809 module. This update improves the documentation's accuracy and usability for developers working on the home automation system.
This commit is contained in:
154
codesys/src/NVL/nodered-payload.md
Normal file
154
codesys/src/NVL/nodered-payload.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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)
|
||||
|
||||
```javascript
|
||||
// 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)
|
||||
|
||||
```javascript
|
||||
// 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.
|
||||
Reference in New Issue
Block a user