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