Sending an unsolicited multistate input report to the ZC

I have an "Aqara Wireless Mini Switch" ( https://www.amazon.com/gp/product/B07D19YXND ) which identifies as a Xiaomi WXKG11LM.  This button is designed to send semi-nonstandard commands to a supported ZC.  It advertises a Multistate Input cluster, and when you press the button, it sends an unsolicited ZB_ZCL_CMD_REPORT_ATTRIB to the ZC.  Compatible ZCs know that this means the user pressed the button, and they can be programmed to respond in different ways.

I would like to make my nRF52840 emulate the behavior of the Xiaomi switch.  I have a working prototype, however the code is pretty hacky.  I'd like to know if there is a better way of doing this.  I'm worried that my implementation depends on too many internal ZBOSS details and will break easily if ZBOSS modifies their headers in the future.

The first hack is because I could not find any code in ZBOSS that implements the Multistate Input cluster:

/* Multistate input endpoint */

#define ZB_ZCL_CLUSTER_ID_MULTI_INPUT_SERVER_ROLE_INIT    multistate_input_init_server
#define ZB_ZCL_CLUSTER_ID_MULTI_INPUT_CLIENT_ROLE_INIT    multistate_input_init_client

zb_ret_t check_value_multistate_server(zb_uint16_t attr_id, zb_uint8_t endpoint, zb_uint8_t *value)
{
    LOG_ERR("%s(0x%08x): not implemented", __func__, attr_id);
    return RET_OK;
}

static zb_bool_t process_multistate_srv(zb_uint8_t param)
{
    return ZB_FALSE;
}

void multistate_input_init_server()
{
    zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
        ZB_ZCL_CLUSTER_SERVER_ROLE,
        check_value_multistate_server,
        (zb_zcl_cluster_write_attr_hook_t)NULL,
        process_multistate_srv);
}

void multistate_input_init_client()
{
    k_oops();
}

struct multistate_attr_values {
    zb_uint16_t number_of_states;
    zb_bool_t out_of_service;
    zb_uint16_t present_value;
    zb_uint8_t status_flags;
};

static struct multistate_attr_values multistate_attr_values = {
    .number_of_states = 16,
};

// ZBOSS supports the binary input cluster but not multistate input
// Reuse the binary input defs when they're helpful. Define the rest of
// the attributes here:
#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_OUT_OF_SERVICE_ID \
    ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_OUT_OF_SERVICE_ID
#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_STATUS_FLAG_ID \
    ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_STATUS_FLAG_ID

#define MULTISTATE_INPUT_NUMBER_OF_STATES_ID        0x004a

#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_NUMBER_OF_STATES_ID(data_ptr) \
{                                                                   \
  MULTISTATE_INPUT_NUMBER_OF_STATES_ID, \
  ZB_ZCL_ATTR_TYPE_U16, \
  ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL | ZB_ZCL_ATTR_ACCESS_REPORTING, \
  (void*) data_ptr \
}

#define MULTISTATE_INPUT_PRESENT_VALUE_ID        0x0055

#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_PRESENT_VALUE_ID(data_ptr) \
{                                                                   \
  MULTISTATE_INPUT_PRESENT_VALUE_ID, \
  ZB_ZCL_ATTR_TYPE_U16, \
  ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL | ZB_ZCL_ATTR_ACCESS_REPORTING, \
  (void*) data_ptr \
}

ZB_ZCL_START_DECLARE_ATTRIB_LIST(multistate_attr_list)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_NUMBER_OF_STATES_ID, &multistate_attr_values.number_of_states)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_OUT_OF_SERVICE_ID, &multistate_attr_values.out_of_service)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_PRESENT_VALUE_ID, &multistate_attr_values.present_value)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_STATUS_FLAG_ID, &multistate_attr_values.status_flags)
ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST;

zb_zcl_cluster_desc_t multistate_input_cluster[] =
{
    ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
        ZB_ZCL_ARRAY_SIZE(multistate_attr_list, zb_zcl_attr_t),    /* attr_count */
        multistate_attr_list,                    /* attr_desc_list */
        ZB_ZCL_CLUSTER_SERVER_ROLE,                /* cluster_role_mask */
        ZB_ZCL_MANUF_CODE_INVALID                /* manuf_code */
    ),
};

ZB_DECLARE_SIMPLE_DESC(2, 0);
ZB_AF_SIMPLE_DESC_TYPE(2, 0) simple_desc_multistate_ep =
{
    MULTI_INPUT_ENDPOINT,    /* endpoint */
    ZB_AF_HA_PROFILE_ID,    /* app_profile_id */
    ZB_HA_TEST_DEVICE_ID,    /* app_device_id */
    0,            /* app_device_version */
    0,            /* reserved */
    1,            /* app_input_cluster_count */
    0,            /* app_output_cluster_count */
    {            /* app_cluster_list[] */
        ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
    }
};

ZB_AF_DECLARE_ENDPOINT_DESC(multistate_ep,
    MULTI_INPUT_ENDPOINT,    /* ep_id */
    ZB_AF_HA_PROFILE_ID,    /* profile_id */
    0,            /* unused */
    NULL,            /* unused */
    1,            /* cluster_number */
    multistate_input_cluster, /* cluster_list */
    (zb_af_simple_desc_1_1_t*)&simple_desc_multistate_ep, /* simple_desc */
    0,            /* rep_count */
    NULL,            /* rep_ctx */
    0,            /* lev_ctrl_count */
    NULL);            /* lev_ctrl_ctx */

The second hack is because I could not figure out how to send an unsolicited ZB_ZCL_CMD_REPORT_ATTRIB packet without "decomposing" the ZBOSS macros and manually stitching the payload together, byte by byte:

static void send_multistate_report(zb_bufid_t bufid, zb_uint16_t idx_and_flags)
{
    // Send a single multistate report to the ZC on initial button press
    // This can be used to trigger events that can't be done with a
    // standard binding. Intended to mimic the behavior of Xiaomi WXKG11LM
    if ((idx_and_flags & FLAG_MASK) != 0) {
        zb_buf_free(bufid);
        return;
    }

    zb_uint16_t short_addr = 0;

    // adapted from ZB_ZCL_CMD_ON_OFF_OFF_WITH_EFFECT_ID
    zb_uint8_t *ptr = ZB_ZCL_START_PACKET_REQ(bufid)

    // expanded from ZB_ZCL_CONSTRUCT_SPECIFIC_COMMAND_REQ_FRAME_CONTROL
    // but with different flags set
    ZB_ZCL_CONSTRUCT_FRAME_CONTROL(
        ZB_ZCL_FRAME_TYPE_COMMON,
        ZB_ZCL_NOT_MANUFACTURER_SPECIFIC,
        ZB_ZCL_FRAME_DIRECTION_TO_CLI,
        true),                    /* dis_default_resp */
        0,                    /* no manuf_code */

    ZB_ZCL_CONSTRUCT_COMMAND_HEADER_REQ(ptr, ZB_ZCL_GET_SEQ_NUM(),
        ZB_ZCL_CMD_REPORT_ATTRIB);
    ZB_ZCL_PACKET_PUT_DATA16_VAL(ptr, MULTISTATE_INPUT_PRESENT_VALUE_ID);
    ZB_ZCL_PACKET_PUT_DATA8(ptr, ZB_ZCL_ATTR_TYPE_U16);
    ZB_ZCL_PACKET_PUT_DATA16_VAL(ptr, idx_and_flags);
    ZB_ZCL_FINISH_PACKET(bufid, ptr)
    ZB_ZCL_SEND_COMMAND_SHORT(bufid,
        short_addr,                /* addr */
        ZB_APS_ADDR_MODE_16_ENDP_PRESENT,    /* dst_addr_mode */
        1,                    /* dst_ep */
        MULTI_INPUT_ENDPOINT,            /* ep */
        ZB_AF_HA_PROFILE_ID,            /* prof_id */
        ZB_ZCL_CLUSTER_ID_MULTI_INPUT,        /* cluster */
        NULL                    /* cb */);
}
  • Hi, 

    From our expert:

    Since the Multistate Input cluster is not in ZBOSS, you will have to implement it yourself, as you have already started with. It's a good start, but I would recommend that you try to look at the implementation of other clusters and try to keep it as close to that as possible, as well as not reusing definitions from other clusters, just in case. We don't have a guide for implementing custom clusters in NCS, but there are some resources in nRF5 SDK that you can use. One is the short guide Declaring custom cluster. The other thing is the Pressure Measurement cluster. Part of cluster implementations (the c-files) are internal parts of ZBOSS, so not available to the user. However, in nRF5 SDK the implementation of the Pressure Measurement cluster is available for this exact purpose. Here are the files ( and ), so you don't have to download the entire SDK for those two files. I don't think the differences between cluster implementations in NCS and nRF5 SDK should be any different, since they both use the same Zigbee stack, but you should check to be sure.

    As for your second point, if you want to receive attribute reports, then you should configure reporting, and not do it the way you are doing now.

    Regards,
    Amanda

  • The Xiaomi switch, whose behavior I am trying to emulate, does not require configuring attribute reports.  That is why I am sending them "unsolicited."

    Is there a cleaner way for me to construct + send these packets?

    Would you accept a pull request to sdk-nrf that implements the Multistate Input cluster, so I don't have to maintain it out of tree going forward?

  • Hi, 

    It still seems like the Xiaomi switch is configuring attribute reporting locally on the device, which it can do without necessarily having to send a configure reporting command to the receiver, as that command is only to inform the receiver of how it should expect reports from the sender. Sending unsolicited attribute reports without configuring reporting on the device goes against the specification:

    The developer suggested making the device send the configure reporting command to itself as if it were both sender and receiver, in order to configure this. So there is something you can do and configure it such that you disable periodic reporting and instead only report on value change if the case is that the attribute that is reported changes when you press the button.

    Can you provide a sniffer trace of the behavior with the Xiaomi button? What commands do you send and what do you actually want your device to do?

    -Amanda

  • The developer suggested making the device send the configure reporting command to itself as if it were both sender and receiver, in order to configure this.

    What is the API that I should call?  Could you please post an example?

    I tried this, to no avail:

     zb_zcl_start_attr_reporting(
       MULTI_INPUT_ENDPOINT,
       ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
       ZB_ZCL_CLUSTER_CLIENT_ROLE,
       MULTISTATE_INPUT_PRESENT_VALUE_ID);

    No matter what arguments I pass in, it returns RET_NOT_FOUND.  The binary traces generated by zb_zcl_find_reporting_info() suggest that it is not finding any rep_info struct to try to match against my arguments, because the tracepoint at line 827 (rep_info_count) isn't appearing in the log:

    2022-06-08 21:53:42,230 ts=003f m=0x0100 lev=1 zb_zcl_start_attr_reporting:1483 data=[02,00,00,00,12,00,00,00,55,00,00,00]
    2022-06-08 21:53:42,230 ts=003f m=0x0100 lev=1 zb_zcl_find_reporting_info:820 data=[02,00,00,00,12,00,00,00,55,00,00,00]
    2022-06-08 21:53:42,230 ts=003f m=0x0100 lev=2 zb_zcl_find_reporting_info:863 data=[00,00,00,00]
    2022-06-08 21:53:42,233 ts=003f m=0x0100 lev=1 zb_zcl_start_attr_reporting:1494 data=[e4,ff,ff,ff]

    I am looking at zboss/production/src/zcl/zcl_reporting.c from nRF Connect v1.9.1.

Related