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

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

// 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

// 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

// 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

#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

lastByteTimestamp = 0;

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

cpp

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

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!