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 */);
}
Parents
  • 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 (zb_zcl_pres_measurement.h and

    /**
     * Copyright (c) 2018 - 2022, Nordic Semiconductor ASA
     *
     * All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without modification,
     * are permitted provided that the following conditions are met:
     *
     * 1. Redistributions of source code must retain the above copyright notice, this
     *    list of conditions and the following disclaimer.
     *
     * 2. Redistributions in binary form, except as embedded into a Nordic
     *    Semiconductor ASA integrated circuit in a product or a software update for
     *    such product, must reproduce the above copyright notice, this list of
     *    conditions and the following disclaimer in the documentation and/or other
     *    materials provided with the distribution.
     *
     * 3. Neither the name of Nordic Semiconductor ASA nor the names of its
     *    contributors may be used to endorse or promote products derived from this
     *    software without specific prior written permission.
     *
     * 4. This software, with or without modification, must only be used with a
     *    Nordic Semiconductor ASA integrated circuit.
     *
     * 5. Any software provided in binary form under this license must not be reverse
     *    engineered, decompiled, modified and/or disassembled.
     *
     * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
     * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
     * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
     * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE
     * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
     * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
     * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     *
     */
    #include "zboss_api.h"
    #include "zb_zcl_pres_measurement.h"
    #include "app_error.h"
    #include "nrf_log.h"
    
    /**@brief Function which pre-validates the value of attributes before they are written.
     * 
     * @param [in] attr_id  Attribute ID
     * @param [in] endpoint Endpoint
     * @param [in] p_value  Pointer to the value of the attribute which is to be validated
     * 
     * @return ZB_TRUE if the value is valid, ZB_FALSE otherwise.
     */
    static zb_ret_t check_value_pres_measurement(zb_uint16_t attr_id, zb_uint8_t endpoint, zb_uint8_t * p_value)
    {
        zb_ret_t ret = ZB_FALSE;
        zb_int16_t val = ZB_ZCL_ATTR_GETS16(p_value);
    
        NRF_LOG_DEBUG("Pre-validating value %hi of Pressure attribute %d", val, attr_id);
    
        switch(attr_id)
        {
            case ZB_ZCL_ATTR_PRES_MEASUREMENT_VALUE_ID:
                /* Check if the value is unknown. */
                if (val != ZB_ZCL_ATTR_PRES_MEASUREMENT_VALUE_UNKNOWN)
                {      
                    /* Check if the value is higher than the minimal allowed. */
                    zb_zcl_attr_t * p_attr_min_desc = zb_zcl_get_attr_desc_a(endpoint,
                                                                             ZB_ZCL_CLUSTER_ID_PRES_MEASUREMENT,
                                                                             ZB_ZCL_CLUSTER_SERVER_ROLE,
                                                                             ZB_ZCL_ATTR_PRES_MEASUREMENT_MIN_VALUE_ID);
                    ASSERT(p_attr_min_desc);
    
                    zb_int16_t minimal_value = ZB_ZCL_GET_ATTRIBUTE_VAL_S16(p_attr_min_desc);
    
                    if ((minimal_value != ZB_ZCL_ATTR_PRES_MEASUREMENT_MIN_VALUE_INVALID) && 
                        (minimal_value > val))
                    {
                        break;
                    }
    
                    /* Check if the value is lower than the maximal allowed. */
                    zb_zcl_attr_t * p_attr_max_desc = zb_zcl_get_attr_desc_a(endpoint,
                                                                             ZB_ZCL_CLUSTER_ID_PRES_MEASUREMENT,
                                                                             ZB_ZCL_CLUSTER_SERVER_ROLE,
                                                                             ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_ID);
                    ASSERT(p_attr_max_desc);
    
                    zb_int16_t maximal_value = ZB_ZCL_GET_ATTRIBUTE_VAL_S16(p_attr_max_desc);
    
                    if ((maximal_value != ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_INVALID) &&
                        (val > maximal_value))
                    {
                        break;
                    }
                }
    
                ret = ZB_TRUE;
                break;
    
            case ZB_ZCL_ATTR_PRES_MEASUREMENT_MIN_VALUE_ID:
                /* Check the invalid value */
                if (val != ZB_ZCL_ATTR_PRES_MEASUREMENT_MIN_VALUE_INVALID)
                {
                    /* Check the value is in bounds */
                    if ((val < ZB_ZCL_ATTR_PRES_MEASUREMENT_MIN_VALUE_MIN_VALUE) ||
                        (val > ZB_ZCL_ATTR_PRES_MEASUREMENT_MIN_VALUE_MAX_VALUE))
                    {
                        break;
                    }
                }
    
                ret = ZB_TRUE;
                break;
    
            case ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_ID:
                /* Check the invalid value */
                if (val != ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_INVALID)
                {
                    /* Check the value is in bounds */
                    if ((val < ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_MIN_VALUE) ||
    #if ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_MAX_VALUE != 0x7FFF       
                        /* Avoid compiler warning about always false condition */
                        (val > ZB_ZCL_ATTR_PRES_MEASUREMENT_MAX_VALUE_MAX_VALUE))
    #else
                        (0))
    #endif
                    {
                        break;
                    }
                }
    
                ret = ZB_TRUE;
                break;
    
            default:
               break;
        }
    
        return ret;
    }
    
    /**@brief Hook which is being called whenever a new value of attribute is being written.
     * 
     * @param [in] endpoint Endpoint
     * @param [in] attr_id Attribute ID
     * @param [in] new_value Pointer to the new value of the attribute
     */
    static void zb_zcl_pres_measurement_write_attr_hook(zb_uint8_t endpoint, zb_uint16_t attr_id, zb_uint8_t * new_value)
    {
        UNUSED_PARAMETER(new_value);
    
        NRF_LOG_DEBUG("Writing attribute %d of Pressure Measurement Cluster on endpoint %d", attr_id, endpoint);
    
        if (attr_id == ZB_ZCL_ATTR_PRES_MEASUREMENT_VALUE_ID)
        {
    	      /* Implement your own write attributes hook if needed. */
        }
    }
    
    /**@brief Function which initialises the server side of Pressure Measurement Cluster. */
    void zb_zcl_pres_measurement_init_server()
    {
        zb_ret_t ret = zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_PRES_MEASUREMENT,
                                                   ZB_ZCL_CLUSTER_SERVER_ROLE,
                                                   check_value_pres_measurement,
                                                   zb_zcl_pres_measurement_write_attr_hook,
                                                   (zb_zcl_cluster_handler_t)NULL);
        ASSERT(ret == RET_OK);
    }
    
    /**@brief Function which initialises the client side of Pressure Measurement Cluster. */
    void zb_zcl_pres_measurement_init_client()
    {
        zb_ret_t ret = zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_PRES_MEASUREMENT,
                                                   ZB_ZCL_CLUSTER_CLIENT_ROLE,
                                                   check_value_pres_measurement,
                                                   zb_zcl_pres_measurement_write_attr_hook,
                                                   (zb_zcl_cluster_handler_t)NULL);
        ASSERT(ret == RET_OK);
    }
    
    ), 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

Reply
  • 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

Children
  • 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.

  • Hi, 

    Sorry for the delay. From the team:

    The customer's cluster implementation was refactored and fixed (as the attachment multistate_cluster.c ) due to the issues mentioned below.

    The refactored cluster implementation was not tested but should be free from compilation errors.

    1. Keep the code organized and use macros related to the specific cluster so there will be no unpleasant surprises later.
    2. Check carefully attribute properties, and count how many attributes for every cluster are reportable.
    Multistate Input cluster has TWO reportable attributes, not ZERO.
    Putting incorrect information will result in broken reporting mechanism for those attributes.
    Reporting context of the correct length (number of reportable attributes) must be created for the reporting mechanism to work correctly.
    3. Count clusters that are implemented, taking into account their roles (client or server).
    4. Multistate Input (Basic) cluster expects no cluster-specific commands - No zb_zcl_cluster_handler_t functions are required and a pointer to those can be set to NULL.
    The client-side of the cluster has no attributes - the zb_zcl_cluster_check_value_t function can be set to NULL in such a case.
    5. Using macros for declaring attributes lists, cluster lists, and device definitions results in a cleaner code and simpler implementation.
    6. Some clusters are mandatory and shall not be skipped. If you are implementing a multi-endpoint device, some of those clusters are not necessary at every endpoint as a single instance per device may be sufficient.
    7. As usual, write a function that initializes all implemented attributes to the desired values.

    I will start my vacation tomorrow. If you need further help, please create a new support case. Sorry for the inconvenience. 

    Regards,
    Amanda

Related