Skip to content

Fix spurious "Unknown status" warning from XNCP replies on Simplicity SDK firmware - #744

Open
zigpy-review-bot wants to merge 1 commit into
devfrom
zigpy-bot/fix-xncp-status-decoding
Open

Fix spurious "Unknown status" warning from XNCP replies on Simplicity SDK firmware#744
zigpy-review-bot wants to merge 1 commit into
devfrom
zigpy-bot/fix-xncp-status-decoding

Conversation

@zigpy-review-bot

@zigpy-review-bot zigpy-review-bot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

The problem

On firmware built against Simplicity SDK, every startup and every periodic network backup logs a WARNING that reads like a real failure but is a normal, expected condition:

DEBUG [bellows.ezsp] Sending XNCP frame: XncpCommand(command_id=<XncpCommandId.GET_MFG_TOKEN_OVERRIDE_REQ: 2>, status=<EmberStatus.SUCCESS: 0>, payload=GetMfgTokenOverrideReq(token=<EzspMfgTokenId.MFG_CUSTOM_EUI_64: 12>))
DEBUG [bellows.ezsp] Received XNCP frame: XncpCommand(command_id=<XncpCommandId.GET_MFG_TOKEN_OVERRIDE_RSP: 32770>, status=<EmberStatus.undefined_0x2d: 45>, payload=GetMfgTokenOverrideRsp(value=b''))
WARNING [bellows.types.named] Unknown status <EmberStatus.undefined_0x2d: 45>, converting to generic <sl_Status.FAIL: 1>

Seen on a Home Assistant Connect ZBT-2 running 9.1.1.0 build 0 (20260805160939) — a builder artifact from Simplicity SDK 2026.6.1, not a released image.

Worth being explicit that no shipped firmware hits this yet: silabs-firmware-builder's latest release (v2026.02.23) predates its Simplicity SDK migration, so released images still take the Gecko SDK path and decode fine. This is a fix ahead of the planned Simplicity SDK beta rather than a response to breakage in the field.

Root cause

An XNCP frame is a uint16 command ID, then one status byte, then the payload. The firmware types that byte as the SDK's native status type, so its meaning depends on which SDK the image was built against (xncp_types.h):

build status type OK bad argument not found
Gecko SDK 4.x EmberStatus 0x00 0x02 0x03
Simplicity SDK sl_status_t, truncated to one byte 0x00 0x21 0x2D

The split is on #ifdef STACK_TYPES_HEADER, so it tracks the SDK family rather than any particular release. bellows decodes that byte as an EmberStatus unconditionally and then runs it through sl_Status.from_ember_status(). On Simplicity SDK firmware this goes wrong two ways:

  • 0x2D (SL_STATUS_NOT_FOUND) is not a valid EmberStatus, so it becomes undefined_0x2d and trips the "Unknown status" warning.
  • 0x21 (SL_STATUS_INVALID_PARAMETER) is worse, because it is a valid EmberStatus — it silently mis-decodes as the unrelated EmberStatus.SERIAL_INVALID_PORT, with no warning at all.

The path that hits this on every startup is load_network_info()can_burn_userdata_custom_eui64()get_mfg_token(MFG_CUSTOM_EUI_64). The firmware only overrides MFG_STRING and MFG_BOARD_NAME, so NOT_FOUND for any other token is the correct, expected reply, and get_mfg_token() already handles it by falling back to the raw token value. Only the log line is wrong.

The fix

Give the XNCP status byte its own XncpStatus enum instead of borrowing EmberStatus, and compare against it directly rather than routing it through sl_Status.from_ember_status() — that helper translates EZSP command statuses, and its warning is a genuinely useful signal that this was drowning out.

The values the two encodings actually use don't collide, so both fit in one enum. But bellows only ever needs OK vs. not-OK here, and 0x00 is success under either interpretation, so nothing depends on that staying true if firmware adds status values later.

No behaviour change beyond the log line

I checked this exhaustively rather than by inspection. For all 256 possible status byte values, the old and new success tests agree on whether to raise InvalidCommandError:

for b in range(256):
    old = sl_Status.from_ember_status(EmberStatus(b)) != sl_Status.OK
    new = XncpStatus(b) != XncpStatus.OK
    assert old == new          # holds for every byte

That follows from the only SL_STATUS_MAP entries producing sl_Status.OK being EmberStatus.SUCCESS and EzspStatus.SUCCESS, both 0x00. Request frames also serialize to the same bytes as before, since XncpStatus.OK and EmberStatus.SUCCESS are both 0x00 in one byte.

Why not just extend SL_STATUS_MAP

Adding 0x2D there would silence the warning, but unlike #742/#743 — where NETWORK_BUSY really is an EmberStatus that was mapped to the wrong sl_Status — this byte isn't an EmberStatus value at all, so there's nothing correct to map it to. It would also leave the 0x21SERIAL_INVALID_PORT mis-decode in place.

Tests

Added a regression test covering both encodings of "this token has no override", asserting that the call succeeds and logs no warning. It fails on dev for the 0x2D case and passes for both with this change. Full suite: 443 passed.

@zigpy-review-bot

zigpy-review-bot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

One adjacent thing I noticed while tracing this. It's a firmware-side observation, not something to fix here, but flagging it in case it's useful upstream.

The XNCP status slot is a single byte (uint8_t rsp_status in xncp_core.c, written to replyPayload[2]), but on Simplicity SDK builds xncp_status_t is typedef'd to sl_status_t. So the firmware implicitly truncates a 32-bit status to 8 bits on every reply.

That's harmless today, because only three values are ever returned and all of them survive truncation (SL_STATUS_OK 0x0000, SL_STATUS_INVALID_PARAMETER 0x0021, SL_STATUS_NOT_FOUND 0x002D). It stops being harmless as soon as a handler returns anything >= 0x100, and the SL_STATUS_ZIGBEE_* range at 0x0Cxx collides directly with bytes that are already meaningful:

sl_status_t truncated collides with
SL_STATUS_ZIGBEE_DELIVERY_FAILED 0x0C020x02 EMBER_BAD_ARGUMENT (the byte Gecko SDK builds use)
SL_STATUS_ZIGBEE_MAX_MESSAGE_LIMIT_REACHED 0x0C030x03 EMBER_NOT_FOUND
SL_STATUS_ZIGBEE_APS_MESSAGE_CANCELED 0x0C210x21 SL_STATUS_INVALID_PARAMETER

The bounded part: no error value in sl_Status truncates to 0x00, so a truncated failure can never be read as success. It degrades to a different error, not to no error.

Nothing to do in bellows either way — it has to tolerate already-shipped firmware, and the XncpStatus in this PR treats every non-zero byte as an opaque failure, so a truncated value still fails cleanly.

Separately, a documentation nit in the same header, since it caused a real mistake: the #ifdef STACK_TYPES_HEADER branch is labelled // Simplicity SDK (2025.x), but the commit that introduced it (df44c39, "Simplicity SDK 2026.6.0") pinned simplicity_sdk:2026.6.0, and every manifest still pins 2026.6.x today. The branch isn't version-scoped either — it keys off STACK_TYPES_HEADER, so it covers Simplicity SDK generally. I took that label at face value and had to correct "2025.x" out of this PR's description and commit message after the fact, so it seems worth fixing before it misleads someone else.

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.55%. Comparing base (818f191) to head (f774ddf).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #744   +/-   ##
=======================================
  Coverage   99.55%   99.55%           
=======================================
  Files          64       64           
  Lines        4263     4269    +6     
=======================================
+ Hits         4244     4250    +6     
  Misses         19       19           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… SDK firmware

The XNCP frame's one status byte is typed by the firmware as the SDK's
native status type, so it is an `EmberStatus` on Gecko SDK 4.x builds but
the low octet of an `sl_status_t` on Simplicity SDK builds.

bellows decoded it as an `EmberStatus` unconditionally, so a normal "this
token has no override" reply (0x2D, `SL_STATUS_NOT_FOUND`) became
`undefined_0x2d` and tripped the "Unknown status" warning on every startup
and every periodic backup. 0x21 (`SL_STATUS_INVALID_PARAMETER`) was worse:
being a valid `EmberStatus`, it silently mis-decoded as the unrelated
`SERIAL_INVALID_PORT`.

Give the byte its own `XncpStatus` enum and compare against it directly
instead of routing it through `sl_Status.from_ember_status()`, which
translates EZSP command statuses. For all 256 status byte values the old
and new success tests agree on whether to raise `InvalidCommandError`, so
only the log line changes.
@zigpy-review-bot
zigpy-review-bot force-pushed the zigpy-bot/fix-xncp-status-decoding branch from 9d0cba4 to f774ddf Compare August 18, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant