Files
kkelomatic_home/codesys/src/NVL/nodered-payload.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

5.5 KiB
Raw Blame History

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.