Thingy:53 Zigbee End Device — Configure Reporting failures + malformed Report Attributes flood

On a custom Zigbee End Device application for the Thingy:53 (NCS v2.9.3), ZCL Configure Reporting commands sent by Zigbee2MQTT are rejected with ZCL status FAILURE for almost every cluster/attribute except Temperature Measurement and two genOnOff instances, even though the attribute descriptors, endpoint declarations and reporting-context sizing all look correct against the ZBOSS headers. Separately, the device spontaneously and continuously emits malformed Report Attributes frames for msRelativeHumidity (~8 msg/s, confirmed to survive a full device power-cycle), independent of whatever reporting configuration is active. We've ruled out everything visible from prj.conf, the ZBOSS module's own Kconfig, and our application source — the remaining behaviour appears to be inside the precompiled ZBOSS reporting engine.

Environment

Board

Thingy:53 (nRF5340)

SDK

nRF Connect SDK v2.9.3, ZBOSS production libraries

Zephyr

v3.7.99-ncs2-3

Role

Zigbee End Device

Coordinator / gateway

Zigbee2MQTT (external converter), zigbee-herdsman

App

Custom multi-endpoint sensor app — 7 endpoints

Endpoint / cluster map:

EP

Clusters

Reportable attributes

10 (env)

Basic, Identify, msTemperatureMeasurement, msPressureMeasurement, msRelativeHumidity, msIlluminanceMeasurement, genPowerCfg

temp, pressure, humidity, illuminance, battery voltage/percentage

11 (imu)

custom 0xFC01, genBinaryInput

6 custom float attrs, motion (boolean)

12 (mag)

custom 0xFC02

4 custom float attrs

13 (air)

custom 0xFC03

3 custom float attrs

30 (light)

genOnOff, genLevelCtrl, custom 0xFC04

onOff (boolean), currentLevel (uint8)

31 (buzzer)

genOnOff

onOff (boolean)

32 (cfg)

custom 0xFC05

(write-only config, not reported)

Each custom endpoint is built with a small macro (ZB_DECLARE_CUSTOM_EP) wrapping ZB_AF_DECLARE_ENDPOINT_DESC() + ZBOSS_DEVICE_DECLARE_REPORTING_CTX(ctx, 8) — 8 reporting slots per endpoint, well above the 1–6 reportable attributes any single endpoint actually has.

Symptom 1 — Configure Reporting rejected with ZCL status FAILURE for most clusters

After joining, our Z2M external converter's configure() runs ~17 bind + Configure Reporting + Read commands across the 7 endpoints. In a log capture triggered by an explicit bridge/request/device/configure MQTT request (bypassing Z2M's own reconfigure-gating, so we know for certain this actually executed on the device), the device responded:

msTemperatureMeasurement.configReport([{attribute:'measuredValue', minimumReportInterval:10, maximumReportInterval:300, reportableChange:10}])  -> SUCCESS
msRelativeHumidity.configReport([{attribute:'measuredValue', minimumReportInterval:0xFFFF, maximumReportInterval:0xFFFF, reportableChange:0}])   -> FAILURE
msPressureMeasurement.configReport([{attribute:'measuredValue', minimumReportInterval:10, maximumReportInterval:300, reportableChange:1}])        -> FAILURE
msIlluminanceMeasurement.configReport([{attribute:'measuredValue', minimumReportInterval:10, maximumReportInterval:300, reportableChange:1}])     -> FAILURE
genPowerCfg.configReport([{attribute:'batteryPercentageRemaining', minimumReportInterval:60, maximumReportInterval:3600, reportableChange:1}])   -> FAILURE
64513 (custom IMU cluster).configReport([...6 float attrs...])                                                                                    -> FAILURE
64514 (custom MAG cluster).configReport([...4 float attrs...])                                                                                    -> FAILURE
64515 (custom AIR cluster).configReport([...3 float attrs...])                                                                                    -> FAILURE
genBinaryInput.configReport([{attribute:'presentValue', minimumReportInterval:0, maximumReportInterval:300, reportableChange:1}])                -> FAILURE
genLevelCtrl.configReport([{attribute:'currentLevel', minimumReportInterval:0, maximumReportInterval:300, reportableChange:1}])                  -> FAILURE
genOnOff.configReport([{attribute:'onOff', minimumReportInterval:0, maximumReportInterval:300, reportableChange:0}])  (light, EP30)              -> SUCCESS
genOnOff.configReport([{attribute:'onOff', minimumReportInterval:0, maximumReportInterval:300, reportableChange:0}])  (buzzer, EP31)             -> SUCCESS

Initially this looked like "only the first Configure Reporting call per endpoint in a burst succeeds, every later one fails" (temperature was 1st on EP10, both onOff calls were the only/1st call on their endpoints). We tested this directly:

  • Added a 300 ms delay after every individual bind/configureReporting/read step (not just between endpoint blocks) — made zero difference, identical failures, even though the whole sequence now took ~10x longer. This rules out a command-queue/timing race.
  • Reordered so msPressureMeasurement is sent FIRST on EP10 (before temperature) and genLevelCtrl FIRST on EP30 (before onOff) — in that run, pressure succeeded in that position, but genLevelCtrl still failed even as the first command sent to its endpoint, and critically, even the calls that stopped showing FAILURE still never produced live attribute reports in Home Assistant/Z2M afterwards. So "no FAILURE status" does not reliably mean the device's reporting engine is actually scheduling/sending reports for that attribute.

Symptom 2 — Malformed, continuous Report Attributes flood for msRelativeHumidity

Independent of the above, the device continuously emits a Report Attributes frame for msRelativeHumidity's measuredValueroughly every 120 ms (~8.35 msg/s), confirmed via zigbee-herdsman's Failed to parse frame errors and Zigbee2MQTT's own Health page message-rate counter (which matched almost exactly). This is not throttled by minimumReportInterval/maximumReportInterval in any configuration we've sent.

Decoded byte-for-byte from the raw payload ("data":[12,0,0,<seq>,10], <seq> incrementing as expected):

Byte

Value

Meaning

0

0x0C

ZCL frame control: global command, direction server→client, Manufacturer-Specific bit incorrectly SET

1–2

0x00 0x00

Consumed as a (bogus, always-zero) manufacturer code, because of the MS bit above

3

<seq>

Transaction sequence number — correct, increments normally

4

0x0A

Command ID = Report Attributes — correct

—

(none)

Buffer ends here — attribute ID / type / value payload is completely missing

zigbee-herdsman correctly rejects this as unparseable (RangeError [ERR_OUT_OF_RANGE] ... offset is out of range). This is not a herdsman/Z2M bug — the frame genuinely is malformed/truncated as transmitted by the device.

Tested and ruled out:

  • Simply no longer requesting this attribute's reporting — flood continues unchanged (an already-active device-side report isn't cancelled by not re-asking for it).
  • Explicitly sending the ZCL spec's "deactivate reporting" sentinel (minimumReportInterval = maximumReportInterval = 0xFFFF) — confirmed delivered to the device (via an explicit MQTT-triggered Reconfigure), initially rejected with FAILURE; on a later attempt it was accepted (SUCCESS), but the flood continued completely unaffectedeven after the deactivation command was accepted.
  • Full device power-cycle (not a reflash — just power off/on) — flood resumed at an unchanged rate (~7.8 msg/s measured over a 5-minute post-boot window), ruling out any theory involving corrupted/stale RAM state accumulated across repeated test rounds. This reproduces identically from a cold boot.

What we've already ruled out

  • Attribute access flags. Compared the ZBOSS header macros for Temperature/Pressure/Humidity/Illuminance's MeasuredValue descriptors byte-for-byte — all four are declared identically (ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, non-manufacturer-specific). Not a permissions issue.
  • Per-endpoint reporting slot table size. ZBOSS_DEVICE_DECLARE_REPORTING_CTX(ctx, 8) — 8 slots per endpoint, no endpoint has more than 6 reportable attributes. zb_zcl_reporting.h and zboss_api_af.h confirm rep_info_count/reporting_info are genuinely per-endpoint fields ("moved here from device descriptor"), correctly wired via ZB_AF_DECLARE_ENDPOINT_DESC().
  • Device-context structure. In this ZBOSS version, zb_af_device_ctx_t (zboss_api_zcl.h) only has ep_count/ep_desc_list — no device-wide reporting fields exist that could be mis-set.
  • Command ordering / timing / device radio queue. A 300 ms delay after every individual step made no difference (see Symptom 1); reordering which cluster is sent first on an endpoint also didn't produce a consistent "first always succeeds" pattern.
  • prj.conf / prj_release.conf. No Zigbee/ZCL/reporting-related Kconfig tunable of any kind — only library/driver enables (CONFIG_ZIGBEE=y, CONFIG_ZIGBEE_ROLE_END_DEVICE=y, CONFIG_ZIGBEE_USE_SOFTWARE_AES=y, CONFIG_ZIGBEE_CHANNEL_SELECTION_MODE_MULTI=y, sensor/ADC/PWM/log/USB settings).
  • ZBOSS module's own Kconfig (nrfxlib/zboss/Kconfig, read in full) — only offers library-variant selection (production / development / ZIGBEE_ENABLE_TRACES debug libraries) and Green Power feature set. Nothing reporting-related.
  • Our own sensor-push application code. zb_device_update_env() (the function that calls zb_zcl_set_attr_val() for temp/pressure/humidity/illuminance every sampling cycle) treats all four attributes with the byte-for-byte identical call pattern. The periodic sampling loop (ZB_SCHEDULE_APP_ALARM-based self-reschedule) pushes fresh values for every sensor unconditionally, every cycle, with no attribute special-cased or skipped.

Minimal repro

The endpoint/cluster declaration pattern (same for every endpoint, only cluster IDs differ):

#define RO_REPORTABLE (ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING)

ZB_ZCL_DECLARE_PRESSURE_MEASUREMENT_ATTRIB_LIST(pressure_attr_list, &env_ctx.pres_measure_value,
                                                 &env_ctx.pres_min_measure_value,
                                                 &env_ctx.pres_max_measure_value,
                                                 &env_ctx.pres_tolerance);

static zb_zcl_cluster_desc_t env_cluster_list[] = {
  /* ... Basic, Identify, Temp ... */
  ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT,
                       ZB_ZCL_ARRAY_SIZE(pressure_attr_list, zb_zcl_attr_t),
                       (pressure_attr_list), ZB_ZCL_CLUSTER_SERVER_ROLE,
                       ZB_ZCL_MANUF_CODE_INVALID),
  /* ... Humidity, Illuminance, PowerConfig ... */
};

#define ZB_DECLARE_CUSTOM_EP(ep_var, ep_id, n_in, cluster_list_arr, ...)                 \
  ZB_AF_SIMPLE_DESC_TYPE(n_in, 0) simple_desc_##ep_var = { ep_id, ZB_AF_HA_PROFILE_ID,    \
    ZB_HA_CUSTOM_ATTR_DEVICE_ID, 0, 0, n_in, 0, { __VA_ARGS__ } };                        \
  ZBOSS_DEVICE_DECLARE_REPORTING_CTX(reporting_info_##ep_var, 8);                         \
  ZB_AF_DECLARE_ENDPOINT_DESC(ep_var, ep_id, ZB_AF_HA_PROFILE_ID, 0, NULL,                \
    ZB_ZCL_ARRAY_SIZE(cluster_list_arr, zb_zcl_cluster_desc_t), cluster_list_arr,         \
    (zb_af_simple_desc_1_1_t *)&simple_desc_##ep_var, 8, reporting_info_##ep_var, 0, NULL)

ZB_DECLARE_CUSTOM_EP(env_ep, EP_ENV, 7, env_cluster_list,
                      ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_CLUSTER_ID_IDENTIFY,
                      ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT, ZB_ZCL_CLUSTER_ID_PRESSURE_MEASUREMENT,
                      ZB_ZCL_CLUSTER_ID_REL_HUMIDITY_MEASUREMENT,
                      ZB_ZCL_CLUSTER_ID_ILLUMINANCE_MEASUREMENT, ZB_ZCL_CLUSTER_ID_POWER_CONFIG);

Steps:

  1. Flash the app, pair with Zigbee2MQTT.
  2. From the coordinator, bind + send Configure Reporting for msTemperatureMeasurement.measuredValue — succeeds, and periodic reports arrive correctly.
  3. Bind + send Configure Reporting for msPressureMeasurement.measuredValue with the same shape of request — device responds with ZCL status FAILURE (or, in some runs, SUCCESS but with no periodic reports ever actually sent).
  4. Observe msRelativeHumidity spontaneously emitting malformed Report Attributes frames (data: [0x0C, 0x00, 0x00, <seq>, 0x0A]) at ~8/s, with no Configure Reporting ever having been accepted for it, and unaffected by sending the 0xFFFF/0xFFFF deactivation command or power-cycling the device.

Questions

  1. Is there a known limitation in ZBOSS v2.9.3's production libraries on how many distinct clusters/attributes a single endpoint (or the whole device) can have actively reporting at once, beyond the application-supplied ZBOSS_DEVICE_DECLARE_REPORTING_CTX() slot count — something not exposed via Kconfig or the public headers?
  2. Is Configure Reporting known to behave differently for genOnOff/msTemperatureMeasurement versus other standard clusters (e.g. msPressureMeasurement, genPowerCfg, genLevelCtrl, genBinaryInput) on an End Device role specifically?
  3. Could a stale/corrupted reporting-table entry on the device explain the msRelativeHumidity frame corruption (Manufacturer-Specific bit set when it shouldn't be) even across a full power-cycle — or does that point at something else entirely (e.g. the attribute's type/size metadata)?
  4. Is CONFIG_ZIGBEE_ENABLE_TRACES the right path to get actionable binary traces for this, and if so, what's the expected workflow for submitting them against a support ticket?
  5. Are there any known issues in this ZBOSS version specific to ZB_ZCL_CLUSTER_SERVER_ROLE_INIT = NULL for custom manufacturer-range clusters (0xFC01–0xFC05) interacting with the reporting engine, versus a built-in cluster with a real init function?

Happy to provide the full src/ tree, .mjs converter, and raw Z2M debug logs on request.

Related