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