Implement the combined XNCP unicast API - #732
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for the combined XNCP unicast send API (COMBINED_SEND) so the stack can apply extended timeout and (manual) source routing and send a unicast in a single custom EZSP frame, reducing round trips (notably beneficial for networked coordinators).
Changes:
- Add new XNCP command/feature definitions for
SEND_UNICAST_{REQ,RSP}andCOMBINED_SEND. - Route
ControllerApplication.send_packet()down the combined-send path when supported and payload size permits, with fallbacks for oversized payloads and native source-route cases. - Add/extend tests to validate request shaping, combined-send selection, and fallback behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_ezsp.py | Adds unit coverage for xncp_send_unicast() request encoding (flags + optional fields). |
| tests/test_application.py | Adds integration-style tests ensuring send_packet() uses combined send when available and falls back when it can’t. |
| bellows/zigbee/application.py | Introduces combined-send selection logic and a payload-size threshold constant. |
| bellows/ezsp/xncp.py | Defines the new XNCP command IDs, feature bit, request/response structs, and flags. |
| bellows/ezsp/init.py | Implements xncp_send_unicast() on the EZSP layer to send the new combined command. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b1c8266 to
0227c12
Compare
|
The newly-added APS encryption option is encoded within the APS frame itself so it doesn't need to be included in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #732 +/- ##
=======================================
Coverage 99.54% 99.55%
=======================================
Files 64 64
Lines 4205 4263 +58
=======================================
+ Hits 4186 4244 +58
Misses 19 19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Reviewed together with the firmware side (NabuCasa/silabs-firmware-builder#209): the wire format agrees byte-for-byte between the two (APS frame layout, conditional ieee+Bool and LV source-route fields keyed off the same flag bits, u32 status + u8 sequence reply), and the feature bit / command id match. The firmware's apply_extended_timeout also faithfully mirrors the host-side set_extended_timeout logic, including the random address-table-entry replacement, so behavior is preserved when the round trips move into the NCP. Fallback gating looks right and is well tested: native (non-manual) source routes, oversized payloads, and firmwares without COMBINED_SEND all take the existing multi-command path, and the ACK ⇒ no-extended-timeout rule carries over. 437 tests pass locally.
Edit (2026-07-28): the size-guard comment referenced below turned out to be partially incorrect (wrong transport mechanism, overstated impact) — the verified analysis is in the follow-up comment: #732 (comment)
Original closing line (superseded)
One substantive comment on the size guard below.
|
Follow-up from a second, independent review pass (a duplicate
|
|
Follow-up on the (now-resolved) TL;DR: the 128-byte cap cannot break any packet that is actually transmittable over the air, so my original comment overstated the impact. The binding limit is also not the ASH/EZSP transport (which carries 220-byte frames in current stacks, contrary to the stale UG100/UG101 figures I cited) — it is a hard 119-byte cap on the Transport limits — what my original comment got wrongThe "ASH data field 3–128 bytes" (UG101) and "customFrame payload max 119 bytes" (UG100) figures describe the legacy frame format and are stale as transport limits: in current stacks the ASH data field is bounded by the EZSP frame size, not by 128.
So the worst-case combined-send frame (128 B of data plus every optional field ≈ 184 B on the wire) is comfortably transportable: there is no ASH-level rejection, and The real limit: a hard 119-byte customFrame payload cap in the closed-source XNCP plugin (disassembly)The cap measured on hardware is enforced in bl fetchInt8u ; read customFrame payload length
cmp r0, #119 ; 0x77
bhi.n <function epilogue> ; > 119: return immediately
...
bl sl_zigbee_af_xncp_incoming_custom_frame_cb ; only reached when length <= 119
bl appendInt32u ; status + reply appended only on this pathSemantics: an over-119 payload is dropped before the XNCP dispatcher runs — the extension callback is never invoked and no status/reply bytes are appended to the response. That matches the observed "parsing is halted early", rules out any partial parse or truncated send on the firmware side, and corresponds to the otherwise-unreferenced Byte budget: what can and cannot hit the capThe serialized
The unfragmented over-the-air APS ceiling is ~82 B, and it shrinks by ~2 + 2·R once a source-route subframe is in the NWK header — always below the corresponding row above. So no packet that could actually be transmitted can exceed the 119-byte cap, and the 128 guard cannot break real traffic. The residue: payloads of 93–128 B — producible by callers but unsendable regardless (the stack rejects them) — pass Suggestion (robustness nit, not a blocker): size the check against what is actually sent — build the request first and require Process note: these reviews now have local access to the full Simplicity SDK — including the NCP sources and the prebuilt stack libraries used for the disassembly above — for source-level comparison. Previously only silabs-firmware-builder reviews had that, which is why the first version of this comment leaned on stale public documentation. |
Feature detect
XNCP_FEATURE_COMBINED_SENDto reduce round trips when sending unicast commands. This is especially useful for networked coordinators.Firmware PR: NabuCasa/silabs-firmware-builder#209