# \[nibeheatpump\] Critical logic bug in NibeGW Arduino firmware causing Alarm 251

**URL:** <https://community.openhab.org/t/nibeheatpump-critical-logic-bug-in-nibegw-arduino-firmware-causing-alarm-251/170732>\
**Category:** Bindings\
**Tags:** nibe-heatump\
**Created:** [October 20, 2026, 5:39am UTC](https://community.openhab.org/t/nibeheatpump-critical-logic-bug-in-nibegw-arduino-firmware-causing-alarm-251/170732 "2026-10-20T05:39:03Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![AlfredSimple](https://community.openhab.org/letter_avatar_proxy/v4/letter/a/b9e5f3/32.png) [@AlfredSimple](https://community.openhab.org/u/AlfredSimple)\
**Post date:** [October 20, 2026, 5:39am UTC](https://community.openhab.org/t/nibeheatpump-critical-logic-bug-in-nibegw-arduino-firmware-causing-alarm-251/170732/1 "2026-10-20T05:39:03Z")

</div>

Hi everyone,

I found two bugs—well, actually, Claude found them in the NibeGW binding. I hope there are experts here who can look into these bugs; I’m a mechanical engineer, not a software engineer.

**Bug 1:**  
There is a critical boolean logic bug in the NibeGW Arduino firmware source code included in the repository contrib folder. This bug causes frequent, random “Alarm 251 (COM error ACC Modbus 40)” on Nibe SMO 40 / F-series controllers (and identical Alpha Innotec units like the NP-CS40).

**Component:** `org.openhab.binding.nibeheatpump` / Arduino NibeGW firmware  
**File:** `contrib/NibeGW/Arduino/NibeGW/NibeGw.cpp`  
**Function:** `checkNibeMessage()`  
**Affected versions:** All current versions including v5.10

**Expected Behavior**

A frame with a mismatched checksum should only be accepted if **both** `checksum == 0x5C` AND `msg_checksum == 0xC5` are true at the same time.

**Current Behavior**

The checksum escape handling in `checkNibeMessage()` uses incorrect boolean logic. The Nibe RS-485 protocol substitutes a computed checksum of `0x5C` (the frame start byte) with `0xC5`. This special case is currently handled with OR-like inequality logic instead of AND logic.

As a result, the gateway **silently accepts frames with an incorrect checksum** whenever either condition is accidentally true independently. This leads to corrupted data being forwarded to the heat pump. Because invalid frames are not NAK’d correctly, it triggers **Alarm 251** on the heat pump controller (about 2 to 3 times a day in my environment).

**Affected Code**

cpp

```auto
// NibeGw.cpp, function checkNibeMessage() — CURRENT (buggy):
if (checksum != msg_checksum)
{
    // if checksum is 0x5C (start character),
    // heat pump seems to send 0xC5 checksum
    if (checksum != 0x5C && msg_checksum != 0xC5)
        return -2;
}

```

The current condition accepts a frame (does NOT return -2) when:

- `checksum == 0x5C` → regardless of what `msg_checksum` is, OR
- `msg_checksum == 0xC5` → regardless of what `checksum` is

Both conditions must be true **simultaneously** for the escape logic to be valid.

**Proposed Solution**

cpp

```auto
// FIXED CODE:
if (checksum != msg_checksum)
{
    if (!(checksum == 0x5C && msg_checksum == 0xC5))
        return -2;
}

```

**Truth Table**

| **checksum** | **msg\_checksum** | **Current behaviour** | **Correct behaviour** |
| --- | --- | --- | --- |
| **`0x5C`** | `0xC5` | Accepted | Accepted |
| **`0x5C`** | `0xAB` | **Wrongly Accepted** | Rejected |
| **`0xAB`** | `0xC5` | **Wrongly Accepted** | Rejected |
| **`0xAB`** | `0xCD` | Rejected | Rejected |

**My Environment**

- **Hardware:** Arduino Mega 2560 with WaveShare TTL to RS485 module
- **Heat Pump:** Alpha Innotec NP-CS40 controller / NP-AW 20-08 (identical to Nibe SMO40 / F2120-8)

**Bug 2: State Machine Deadlock due to Missing Inter-Frame Timeout**

In addition to the checksum bug, I found a second severe issue in the Arduino firmware that causes the gateway to lock up permanently until a manual reset, also triggering Alarm 251.

**Component:** `org.openhab.binding.nibeheatpump` / Arduino NibeGW firmware  
**File:** `contrib/NibeGW/Arduino/NibeGW/NibeGw.cpp`  
**Function:** `loop()`, state `STATE_WAIT_DATA`  
**Affected versions:** All current versions including v5.10

**Expected Behavior**

If a partial frame is received (e.g., due to RS-485 line noise, electrical interference, or a brief physical disconnection), the state machine should automatically reset to `STATE_WAIT_START` after a safe timeout period so that normal communication can resume.

**Current Behavior**

The RS-485 receive state machine has no timeout mechanism while in `STATE_WAIT_DATA`. If a frame is interrupted and incomplete, the state machine stalls indefinitely waiting for bytes that will never arrive.

While stalled in this state, the gateway stops responding to the heat pump’s token requests. After approximately 15 seconds without a valid ACK, the Nibe SMO 40 / F-series controller raises **Alarm 251 (COM error ACC Modbus 40)**, locking up the heating system until it is manually power-cycled.

**Root Cause**

cpp

```auto
// NibeGw.cpp — CURRENT (no timeout):
case STATE_WAIT_DATA:
    if (RS485->available() > 0)
    {
        byte b = RS485->read();
        // ... processes byte, but if no more bytes arrive:
        // state stays STATE_WAIT_DATA forever
    }
    break;

```

If `RS485->available()` drops to 0 after an incomplete frame, the state machine does nothing on subsequent loops and stays stuck in `STATE_WAIT_DATA` forever.

**Proposed Solution**

Adding a `millis()`-based timeout resolves this. At 9600 baud, a complete frame (max ~80 bytes) takes less than 85 ms. A 100 ms timeout is perfectly safe—long enough for legitimate intra-frame gaps, but short enough to recover thousands of milliseconds before Alarm 251 would trigger.

**NibeGw.h** — Add constant and private member:

cpp

```auto
#define FRAME_TIMEOUT_MS 100 // safe inter-frame timeout at 9600 baud

// In private section of class NibeGw:
unsigned long lastByteTimestamp;

```

**NibeGw.cpp** — Initialize in constructor:

cpp

```auto
lastByteTimestamp = 0;

```

**NibeGw.cpp** — `STATE_WAIT_START`, start timeout on frame detection:

cpp

```auto
if (b == 0x5C) {
    buffer = b;
    index = 1;
    state = STATE_WAIT_DATA;
    lastByteTimestamp = millis(); // start timeout

```

**NibeGw.cpp** — `STATE_WAIT_DATA`, add check at the top of the case:

cpp

```auto
case STATE_WAIT_DATA:
    if (millis() - lastByteTimestamp > FRAME_TIMEOUT_MS)
    {
        state = STATE_WAIT_START; // recover from incomplete frame
        break;
    }
    if (RS485->available() > 0)
    {
        byte b = RS485->read();
        lastByteTimestamp = millis(); // refresh timeout on each byte
        // ... rest of the logic remains unchanged

```

I wanted to open a GitHub issue, but my GitHub account is currently caught in a shadowban/404 loop by their automated spam filters. I hope a maintainer can pick this up here and apply the fix to the repository.

I would also like to take this opportunity to thank all the developers and maintainers of this binding and the NibeGW firmware.

Thank you so much for your hard work, dedication, and for maintaining this project for the community! It is an amazing piece of software that makes integrating our heat pumps so much easier. I really appreciate your time and effort in looking into these fixes to make it even more stable for everyone.

Keep up the great work! Thank you!
