Zigbee Sub-Device Connectivity

Last Updated on : 2023-09-06 10:19:58

This topic describes how to enable Zigbee features and connect to third-party Zigbee devices on top of Zigbee functionality.

This topic only describes software implementation. Connecting your hardware to Tuya’s Zigbee module is the prerequisite. Before you code, check for hardware connection by reading the module’s version number.

Background

Zigbee sub-device connectivity aims to lower the barrier to developing a Zigbee gateway. Tuya’s hardware and software-integrated solution provides Zigbee module and software implementation, allowing you to build a Zigbee gateway with no code and connect to Zigbee sub-devices in the Tuya ecosystem.

TuyaOS Gateway Development Framework provides Zigbee interfaces for connection to third-party Zigbee devices.

Scenarios

Zigbee sub-devices in the Tuya ecosystem come across various categories, including electrical products, lights, home appliances, and sensors. However, developing a Tuya-enabled smart gateway can be complex and costly. Given this, Tuya’s generic gateway solution is the optimal choice.

If the Tuya development framework has not imported the chip platform you use, for example, your proprietary chip, but you want to enable your hardware to connect to Zigbee sub-devices in the Tuya ecosystem. The solution described in this topic should be your choice. Connect your hardware to Tuya’s Zigbee module through the serial port and enable Zigbee features in the software. Then, your product becomes Zigbee-capable.

On top of the basic Zigbee functionality, you can extend your product to connect third-party Zigbee sub-devices to the Tuya IoT Development Platform by using the Zigbee interfaces provided by TuyaOS Gateway Development Framework. This can help to differentiate your product in the market.

Zigbee features

This section describes how to enable Zigbee features with TuyaOS Gateway Development Framework and implement Zigbee gateway capabilities with Tuya’s Zigbee module.

Two interfaces are used to enable Zigbee features.

  • Zigbee initialization
  • Zigbee startup

These two interfaces have the same parameters in JSON, but differ in configurations.

Configuration description

Fields in the JSON data:

Field Description
storage_path The path to persistent storage where Zigbee network information and sub-device information resides. This field is deprecated. Set the storage path using tuya_load_config/tuya_set_config.
cache_path The path to temporary storage where OTA firmware updates reside. This field is deprecated. Set the storage path using tuya_load_config/tuya_set_config.
dev_name Specifies the serial port number.
cts Specifies whether to enable hardware flow control.
  • 1: Enable
  • 0: Disable
Whether to enable hardware flow control depends on the Zigbee module you use.
thread_mode Specifies the mode to run the Zigbee host.
  • 1: Thread (Recommended)
  • 0: Process

Example

// ...
#include "tuya_zigbee_api.h"

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

    // Initialize Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Zigbee extension

This section describes how to enable your gateway to connect to third-party Zigbee sub-devices, which can make your gateway more friendly for compatibility with Zigbee devices.

Before you get started, it can be helpful to get acquainted with how to implement a Zigbee sub-device connectivity with an SDK. For more information, see Sub-Device Connectivity.

Register allowlist

TuyaOS Gateway Development Framework allows for connection to third-party Zigbee sub-devices by means of allowlist registration.

Register the Manufacturer Name and Model Identifier of the third-party Zigbee device to the SDK. The Basic Cluster present in a Zigbee sub-device stores these two attributes.

After a Zigbee sub-device joins the network, the SDK matches this device’s Manufacturer Name and Model Identifier against the allowlist. If a match is found, the SDK will record the MAC address of this device and notify the application of data reporting from this device through a callback. Otherwise, the SDK handles this device internally.

Implementation:

  • Define the TY_Z3_DEV_S struct array variable. Add the Manufacturer Name and Model Identifier of the third-party Zigbee devices to the array.
  • Define the TY_Z3_DEVLIST_S struct variable. Assign the TY_Z3_DEV_S struct array variable and its length to this variable.
  • Define the TY_Z3_DEV_CBS_S struct variable to register the Zigbee sub-device management callbacks.
  • Call tuya_zigbee_custom_dev_mgr_init to register the allowlist and the Zigbee sub-device management callback.

Example:

#include "tuya_zigbee_api.h"

// Define an allowlist
STATIC TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TZ3000" },
    // ...
};

STATIC TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

STATIC VOID __my_z3_dev_join(TY_Z3_DESC_S *dev) {}
STATIC VOID __my_z3_dev_leave(CONST CHAR_T *dev_id) {}

int main(int argc, char **argv)
{
    OPERATE_RET op_ret = OPRT_OK;
    // Zigbee sub-device management callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .leave       = __my_z3_dev_leave,
    };

    // Register allowlist and device callback
    op_ret = tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_custom_dev_mgr_init err: %d", op_ret);
        return op_ret;
    }

    return 0;
}

Join a network

After a permitted device joins the network, the SDK calls the join callback to notify the application for processing. In the callback, you should parse the sub-device information and call the device binding interface to connect the sub-device to the Tuya IoT Development Platform. Implementation:

  • Register the TY_Z3_DEV_CBS_S join callback for network joining. In this callback, parse the sub-device information and call tuya_iot_gw_bind_dev to connect the sub-device to the Tuya IoT Development Platform.
  • Register the TY_Z3_DEV_CBS_S report callback for reporting data. In this callback, convert Zigbee Cluster Library (ZCL) data to Tuya-defined data point (DP) and call the DP reporting interface to upload the DP data to the Tuya IoT Development Platform.
  • Register the TY_GW_SUBDEV_MGR_CBS_S dev_bind callback for binding notification. In this callback, send the ZCL data to the sub-device to query its current status.
  • The SDK permits the sub-device to join the network. The sub-device enters pairing mode with the reset button.

The process of sub-device joining the network:

SDKDeviceApplicationCloudPermit JoinDevice CommissioningTY_Z3_DEV_CBS_S.jointuya_iot_gw_bind_devDevice Bind RequestDevice Bind ResponseTY_GW_SUBDEV_MGR_CBS_S.dev_bindRead Attributestuya_zigbee_send_dataZCL PacketZCL PacketTY_Z3_DEV_CBS_S.reportZCL to DPdev_report_dp_json_asyncDP ReportSDKDeviceApplicationCloud

Example:

// ...
#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

#define PROFILE_ID_HA                           0x0104

#define ZCL_BASIC_CLUSTER_ID                    0x0000
#define ZCL_ON_OFF_CLUSTER_ID                   0x0006

#define ZCL_CMD_TYPE_GLOBAL                     0x00
#define ZCL_CMD_TYPE_PRIVATE                    0x01

#define ZCL_READ_ATTRIBUTES_COMMAND_ID          0x00
#define ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID 0x01
#define ZCL_REPORT_ATTRIBUTES_COMMAND_ID        0x0A
#define ZCL_READ_ATTRIBUTER_RESPONSE_HEADER     3 /* Attribute ID: 2 Bytes, Status: 1 Byte */
#define ZCL_REPORT_ATTRIBUTES_HEADER            2 /* Attribute ID: 2 Bytes */

TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TS0002" },
};

TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs    = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

/**
 * @brief Data reporting notification
 * @note You need to convert ZCL data into DP data, and then report the DP data to the cloud. Meanwhile, refresh the sub-device heartbeat to maintain its online status.
 */
STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    TY_OBJ_DP_S dp_data = {0};

	// Check if the device is bound
    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

	// Refresh device heartbeat
    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    // For simplicity, this demo uses a 1-gang smart switch to show how data is processed. In an actual project, you need to implement the mapping between ZCL data and DP data.
    if ((frame->profile_id != PROFILE_ID_HA) ||
        (frame->cluster_id != ZCL_ON_OFF_CLUSTER_ID)) {
        return;
    }

    if (frame->cmd_id == ZCL_REPORT_ATTRIBUTES_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_REPORT_ATTRIBUTES_HEADER+1];
    } else if (frame->cmd_id == ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_READ_ATTRIBUTER_RESPONSE_HEADER+1];
    } else {
        return;
    }
    dp_data.dpid = frame->src_endpoint;
    dp_data.type = PROP_BOOL;

	// Report DP
    op_ret = dev_report_dp_json_async(frame->id, &dp_data, 1);
    if (op_ret != OPRT_OK) {
        PR_ERR("dev_report_dp_json_async err: %d", op_ret);
        return;
    }
}

/**
 * @brief Joining network notification
 * @note According to sub-device information, map the sub-device identify to its PID.
 */
STATIC VOID __my_z3_dev_join(TY_Z3_DESC_S *dev)
{
    OPERATE_RET op_ret = OPRT_OK;

	// For simplicity, the PID and the firmware version number are hard coded. In an actual project, they should match the real sub-device.
    op_ret = tuya_iot_gw_bind_dev(GP_DEV_ATH_1, 0, dev->id, "ckj1pnvy", "1.0.0");
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_iot_gw_bind_dev err: %d", op_ret);
        return;
    }
}

/**
 * @brief Binding result notification
 * @note You need to send the attribute reading command to the sub-device to instruct it to report the current attribute value.
 */
STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};
    USHORT_T atrr_buf[5] = { 0x0000, 0x4001, 0x4002, 0x8001, 0x5000 };

	// Binding failed. Remove the sub-device.
    if (result != OPRT_OK) {
        PR_ERR("dev_id: %s bind err", dev_id);
        tuya_zigbee_del_dev(dev_id);
        return;
    }

	// Set heartbeat
    op_ret = tuya_iot_set_dev_hb_cfg(dev_id, 120, 3, FALSE);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
    }

	// For simplicity, this demo uses a 1-gang smart switch to show command sending. The logic is hard coded. In an actual project, you need to process it as required.
    strncpy(frame.id, dev_id, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
    frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    frame.src_endpoint = 0x01;
    frame.dst_endpoint = 0xff;
    frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;
    frame.msg_length = SIZEOF(atrr_buf);
    frame.message = (UCHAR_T *)atrr_buf;

    op_ret = tuya_zigbee_send_data(&frame);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        return;
    }
}

STATIC OPERATE_RET user_zigbee_custom_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;

    // Zigbee callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .report      = __my_z3_dev_report,
    };

    // Sub-device management callback
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
        .dev_bind_ifm  = __dev_bind_cb,
    };

	// Register device management
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// Register allowlist
    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

	// Initialize third-party sub-device service
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

	// Initialize Zigbee service
	TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
	TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

	// ...

	return 0;
}

Control and report

  • Control (send command to sub-device): To execute a command from the mobile app or automation, the SDK calls the callback for sending DP commands to notify the application for processing. In the callback, convert the DP data to the ZCL data and send the converted data to the sub-device.

  • Report (report data to cloud): After the sub-device executes a command from a mobile app or physical operation, it sends the current status to the SDK. The SDK calls the data reporting callback to notify the application for processing. In the callback, convert the ZCL data to the DP data and upload the converted data to the cloud.

Data reporting is used as a response to command sending. Therefore, after the SDK calls the callback for sending DP commands, the Zigbee sub-device proactively reports the current status of the corresponding attribute after command execution. For third-party Zigbee sub-devices without the proactive data reporting logic, the SDK should query the corresponding attribute.

Methods to implement device control and data reporting:

  • Register the TY_GW_SUBDEV_MGR_CBS_S dp_cmd_obj callback for sending DP data. In this callback, convert the DP data to the ZCL data and call tuya_zigbee_send_data to send the converted data to the sub-device.
  • Register the TY_Z3_DEV_CBS_S report callback for reporting data. In this callback, convert the ZCL data to the DP data and call the DP reporting interface to upload the DP data to the Tuya IoT Development Platform.

The process of controlling a sub-device:

App/CloudSDKApplicationDeviceDP CommandTY_GW_SUBDEV_MGR_CBS_S.dp_cmd_objDP to ZCLtuya_zigbee_send_dataZCL PacketZCL PacketTY_Z3_DEV_CBS_S.reportZCL to DPdev_report_dp_json_asyncDP ReportApp/CloudSDKApplicationDevice

Example:

#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

#define PROFILE_ID_HA                           0x0104

#define ZCL_BASIC_CLUSTER_ID                    0x0000
#define ZCL_ON_OFF_CLUSTER_ID                   0x0006

#define ZCL_CMD_TYPE_GLOBAL                     0x00
#define ZCL_CMD_TYPE_PRIVATE                    0x01

#define ZCL_READ_ATTRIBUTES_COMMAND_ID          0x00
#define ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID 0x01
#define ZCL_REPORT_ATTRIBUTES_COMMAND_ID        0x0A
#define ZCL_READ_ATTRIBUTER_RESPONSE_HEADER     3 /* Attribute ID: 2 Bytes, Status: 1 Byte */
#define ZCL_REPORT_ATTRIBUTES_HEADER            2 /* Attribute ID: 2 Bytes */

TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TS0002" },
};

TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs    = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

/**
 * @brief DP command sending callback
 * @note You need to convert DP data into ZCL data, and then send it to the sub-device.
 */
STATIC VOID __dev_cmd_obj_cb(CONST TY_RECV_OBJ_DP_S *cmd)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    PR_DEBUG("cmd_tp: %d, dtt_tp: %d, dps_cnt: %u", cmd->cmd_tp, cmd->dtt_tp, cmd->dps_cnt);

	// Check if the device is bound
    dev_if = tuya_iot_get_dev_if(cmd->cid);
    if (dev_if == NULL) {
        PR_WARN("%s is not bind", cmd->cid);
        return;
    }

	// For simplicity, this demo uses a 1-gang smart switch to show how data is processed. In an actual project, you need to implement the mapping between ZCL data and DP data.
    for (i = 0; i < cmd->dps_cnt; i++) {
        BYTE_T dpid = cmd->dps[i].dpid;
        TY_Z3_APS_FRAME_S frame = {0};
        switch (dpid)
        {
        case 1 ... 6:
            if (cmd->dps[i].type != PROP_BOOL) {
                PR_WARN("dp type: %d invalid", cmd->dps[i].type);
                break;
            }
            strncpy(frame.id, cmd->cid, SIZEOF(frame.id));
            frame.profile_id = PROFILE_ID_HA;
            frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
            frame.cmd_type = ZCL_CMD_TYPE_PRIVATE;
            frame.frame_type = 0;
            frame.src_endpoint = 0x01;
            frame.dst_endpoint = dpid;
            frame.cmd_id = cmd->dps[i].value.dp_bool;
            frame.msg_length = 0;
            op_ret = tuya_zigbee_send_data(&frame);
            if (op_ret != OPRT_OK) {
                PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
            }
            break;

        default:
            PR_WARN("dpid: %d is not supported", cmd->dps[i].dpid);
            break;
        }
    }
}

/**
 * @brief Data reporting notification
 * @note You need to convert ZCL data into DP data, and then report the DP data to the cloud. Meanwhile, refresh the sub-device heartbeat to maintain its online status.
 */
STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    TY_OBJ_DP_S dp_data = {0};

	// Check if the device is bound
    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

	// Refresh device heartbeat
    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    // For simplicity, this demo uses a 1-gang smart switch to show how data is processed. In an actual project, you need to implement the mapping between ZCL data and DP data.
    if ((frame->profile_id != PROFILE_ID_HA) ||
        (frame->cluster_id != ZCL_ON_OFF_CLUSTER_ID)) {
        return;
    }

    if (frame->cmd_id == ZCL_REPORT_ATTRIBUTES_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_REPORT_ATTRIBUTES_HEADER+1];
    } else if (frame->cmd_id == ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_READ_ATTRIBUTER_RESPONSE_HEADER+1];
    } else {
        return;
    }
    dp_data.dpid = frame->src_endpoint;
    dp_data.type = PROP_BOOL;

	// Report DP
    op_ret = dev_report_dp_json_async(frame->id, &dp_data, 1);
    if (op_ret != OPRT_OK) {
        PR_ERR("dev_report_dp_json_async err: %d", op_ret);
        return;
    }
}

STATIC OPERATE_RET user_zigbee_custom_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;

    // Zigbee callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .report      = __my_z3_dev_report,
    };

    // Sub-device management callback
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
        .dp_cmd_obj  = __dev_cmd_obj_cb,
    };

	// Register device management
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// Register allowlist
    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

	// Initialize third-party sub-device service
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // Initialize Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Manage heartbeat

The heartbeat mechanism monitors the connection between the gateway and sub-devices. There are two methods to achieve the heartbeat check.

  • The active method. The sub-device proactively reports data to the gateway regularly.
  • The passive method. The SDK regularly requests the status of the sub-device such as the version number.

For the active method, configure the third-party sub-device to regularly report its version number. The application refreshes the sub-device heartbeat in the data reporting callback.

For the passive method, the SDK takes care of the heartbeat check with the following methods.

  • Register the TY_Z3_DEV_CBS_S notify callback for initialization notification and the TY_GW_SUBDEV_MGR_CBS_S dev_bind callback for binding notification. In the callback, call tuya_iot_set_dev_hb_cfg to configure the heartbeat.
  • Register the TY_GW_SUBDEV_MGR_CBS_S dev_hb callback for heartbeat checks. In this callback, read the firmware version of the sub-device.
  • Register the TY_Z3_DEV_CBS_S report callback for reporting data. In this callback, call tuya_iot_fresh_dev_hb to refresh the sub-device heartbeat.

The process of heartbeat management:

ApplicationSDKDevicetuya_iot_set_dev_hb_cfgDevice Online TimeoutTY_GW_SUBDEV_MGR_CBS_S.dev_hbRead Versiontuya_zigbee_send_dataZCL PacketZCL PacketTY_Z3_DEV_CBS_S.reporttuya_iot_fresh_dev_hbApplicationSDKDevice

Example:

#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

#define PROFILE_ID_HA                           0x0104

#define ZCL_BASIC_CLUSTER_ID                    0x0000
#define ZCL_ON_OFF_CLUSTER_ID                   0x0006

#define ZCL_CMD_TYPE_GLOBAL                     0x00
#define ZCL_CMD_TYPE_PRIVATE                    0x01

#define ZCL_READ_ATTRIBUTES_COMMAND_ID          0x00
#define ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID 0x01
#define ZCL_REPORT_ATTRIBUTES_COMMAND_ID        0x0A
#define ZCL_READ_ATTRIBUTER_RESPONSE_HEADER     3 /* Attribute ID: 2 Bytes, Status: 1 Byte */
#define ZCL_REPORT_ATTRIBUTES_HEADER            2 /* Attribute ID: 2 Bytes */

TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TS0002" },
};

TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs    = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

/**
 * @brief Binding result notification
 */
STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    OPERATE_RET op_ret = OPRT_OK;

	// For simplicity, irrelevant logic is omitted.
	// Set heartbeat
    op_ret = tuya_iot_set_dev_hb_cfg(dev_id, 120, 3, FALSE);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
    }

    // ...
}

/**
 * @brief Heartbeat check notification
 */
STATIC VOID __dev_hb_cb(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};
    USHORT_T atrr_buf[1] = { 0x0001 };

    strncpy(frame.id, dev_id, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.cluster_id = ZCL_BASIC_CLUSTER_ID;
    frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    frame.src_endpoint = 0x01;
    frame.dst_endpoint = 0xFF;
    frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;

    frame.msg_length = SIZEOF(atrr_buf);
    frame.message = (UCHAR_T *)atrr_buf;

    op_ret = tuya_zigbee_send_data(&frame);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        return;
    }
}

/**
 * @brief Initialization notification callback
 * @note Traverse all sub-devices. If the device type is the one you registered, configure the device heartbeat.
 */
STATIC VOID __my_z3_dev_notify(VOID)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    VOID *iter = NULL;

    dev_if = tuya_iot_dev_traversal(&iter);
    while (dev_if) {
        if (dev_if->tp == GP_DEV_ATH_1) {
            op_ret = tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
            if (op_ret != OPRT_OK) {
                PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
            }
        }
        dev_if = tuya_iot_dev_traversal(&iter);
    }
}

/**
 * @brief Data reporting notification
 */
STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
	OPERATE_RET op_ret = OPRT_OK;

	// For simplicity, irrelevant logic is omitted.
	// Refresh device heartbeat
    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    // ...
}

STATIC OPERATE_RET user_zigbee_custom_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;

    // Zigbee callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

    // Sub-device management callback
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
    	.dev_bind = __dev_bind_cb,
        .dev_hb   = __dev_hb_cb,
    };

	// Register device management
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// Register allowlist
    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

	// Initialize third-party sub-device service
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // Initialize Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Remove and reset

Remove a sub-device from the Zigbee network, which can be triggered in two ways.

  • Mobile app: Register the TY_GW_SUBDEV_MGR_CBS_S dev_del callback for removing a sub-device and the dev_reset callback for removing a sub-device and clearing data. In the callback, call tuya_zigbee_del_dev to remove the sub-device from the Zigbee network.
  • Manual operation on the physical device: Register the TY_Z3_DEV_CBS_S leave callback for network leaving. In this callback, call tuya_iot_gw_unbind_dev to remove the sub-device from the Tuya IoT Development Platform.

Example:

// ...
#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

/**
 * @brief Device removing callback
 * @note In the callback, implement removing the sub-device from the cloud.
 */
STATIC VOID __my_z3_dev_leave(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_iot_gw_unbind_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_iot_gw_unbind_dev err: %d", op_ret);
        return;
    }
}

/**
 * @brief Device removing notification callback
 * @note In the callback, implement removing the sub-device from the Zigbee network.
 */
STATIC VOID __dev_del_cb(CONST CHAR_T *dev_id, CONST GW_DELDEV_TYPE type)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_del_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_del_dev err: %d", op_ret);
        return;
    }
}

/**
 * @brief Notification callback for removing device and clearing data
 * @note In the callback, implement removing the sub-device from the Zigbee network.
 */
STATIC VOID __dev_reset_cb(CONST CHAR_T *dev_id, DEV_RESET_TYPE_E type)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_del_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_del_dev err: %d", op_ret);
        return;
    }
}

STATIC OPERATE_RET user_zigbee_custom_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;

    // Zigbee callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .leave      = __my_z3_dev_report,
    };

    // Sub-device management callback
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
    	.dev_del   = __dev_del_cb,
        .dev_reset = __dev_reset_cb,
    };

	// Register device management
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// Register allowlist
    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

	// Initialize third-party sub-device service
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // Initialize Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Update device

The Zigbee OTA Cluster is used to update the Zigbee sub-device. The SDK provides the firmware update interface and the update process, allowing you to easily implement the OTA update feature.

To deploy an OTA update, you need to upload the firmware update to the Tuya IoT Development Platform and verify the OTA update. For more information, see Sub-Device OTA Update.

Implementation:

  • Register the TY_GW_SUBDEV_MGR_CBS_S dev_upgrade callback for update notification. In this callback, call tuya_zigbee_upgrade_dev to download and install the update.
  • Register the TY_Z3_DEV_CBS_S upgrade_end callback for result notification In this callback, report the firmware version of the sub-device. The mobile app determines the update result based on the received version number.

Example:

// ...
#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

/**
 * @brief The update notification callback
 */
STATIC VOID __dev_upgrade_cb(CONST CHAR_T *dev_id, CONST FW_UG_S *fw)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_upgrade_dev(dev_id, fw);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_upgrade_dev err: %d", op_ret);
        return;
    }

    return;
}

/**
 * @brief The update result notification callback
 * @note You need to determine the update result. If the update fails, report a failure. For a successful update, report the new firmware version number.
 */
STATIC VOID __my_z3_dev_upgrade_end(CONST CHAR_T *dev_id, INT_T rc, UCHAR_T version)
{
    CHAR_T sw_ver[SW_VER_LEN+1] = {0};

    if (rc != OPRT_OK) {
        tuya_iot_dev_upgd_result_report(dev_id, GP_DEV_ATH_1, 4);
        return;
    }

	// The following rules for firmware versioning are defined by Tuya. You can handle them based on your needs.
    CHAR_T iver1 = (CHAR_T)(version & 0x0F);
    CHAR_T iver2 = (CHAR_T)((version >> 4) & 0x03);
    CHAR_T iver3 = (CHAR_T)((version >> 6) & 0x03);
    snprintf(sw_ver, SIZEOF(sw_ver), "%d.%d.%d", iver3, iver2, iver1);

    tuya_iot_gw_subdevice_update(dev_id, sw_ver);

    return;
}

STATIC OPERATE_RET user_zigbee_custom_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;

    // Zigbee callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .upgrade_end = __my_z3_dev_upgrade_end,
    };

    // Sub-device management callback
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
    	.dev_upgrade = __dev_upgrade_cb,
    };

	// Register device management
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// Register allowlist
    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

	// Initialize third-party sub-device service
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // Initialize Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Initial status

After startup, the application reads the attributes of the sub-device and syncs it to the cloud.

Implementation:

  • Register the TY_Z3_DEV_CBS_S notify callback for initialization callback. In this callback, traverse all the sub-devices and send the attribute reading command to them.
  • Register the TY_Z3_DEV_CBS_S report callback for reporting data. In this callback, convert the ZCL data to the DP data and call the DP reporting interface to upload the DP data to the Tuya IoT Development Platform.

Example:

#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

#define PROFILE_ID_HA                           0x0104

#define ZCL_BASIC_CLUSTER_ID                    0x0000
#define ZCL_ON_OFF_CLUSTER_ID                   0x0006

#define ZCL_CMD_TYPE_GLOBAL                     0x00
#define ZCL_CMD_TYPE_PRIVATE                    0x01

#define ZCL_READ_ATTRIBUTES_COMMAND_ID          0x00
#define ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID 0x01
#define ZCL_REPORT_ATTRIBUTES_COMMAND_ID        0x0A
#define ZCL_READ_ATTRIBUTER_RESPONSE_HEADER     3 /* Attribute ID: 2 Bytes, Status: 1 Byte */
#define ZCL_REPORT_ATTRIBUTES_HEADER            2 /* Attribute ID: 2 Bytes */

TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TS0002" },
};

TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs    = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

/**
 * @brief Initialization notification callback
 * @note Traverse all sub-devices. If the device type is the one you registered, send the query command to the sub-device.
 */
STATIC VOID __my_z3_dev_notify(VOID)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    VOID *iter = NULL;

    dev_if = tuya_iot_dev_traversal(&iter);
    while (dev_if) {
        TY_Z3_APS_FRAME_S frame = {0};
    	USHORT_T atrr_buf[5] = { 0x0000, 0x4001, 0x4002, 0x8001, 0x5000 };

        if (dev_if->tp == GP_DEV_ATH_1) {
            op_ret = tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
            if (op_ret != OPRT_OK) {
                PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
            }
        }

		// For simplicity, this demo uses a 1-gang smart switch to show command sending. The logic is hard coded. In an actual project, you need to process it as required.
    	strncpy(frame.id, dev_if->id, SIZEOF(frame.id));
    	frame.profile_id = PROFILE_ID_HA;
    	frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
    	frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    	frame.src_endpoint = 0x01;
    	frame.dst_endpoint = 0xff;
    	frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;
    	frame.msg_length = SIZEOF(atrr_buf);
    	frame.message = (UCHAR_T *)atrr_buf;

    	op_ret = tuya_zigbee_send_data(&frame);
    	if (op_ret != OPRT_OK) {
        	PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        	return;
        }

        dev_if = tuya_iot_dev_traversal(&iter);
    }
}

/**
 * @brief Data reporting notification
 * @note You need to convert ZCL data into DP data, and then report the DP data to the cloud. Meanwhile, refresh the sub-device heartbeat to maintain its online status.
 */
STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    TY_OBJ_DP_S dp_data = {0};

	// Check if the device is bound
    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

	// Refresh device heartbeat
    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    // For simplicity, this demo uses a 1-gang smart switch to show how data is processed. In an actual project, you need to implement the mapping between ZCL data and DP data.
    if ((frame->profile_id != PROFILE_ID_HA) ||
        (frame->cluster_id != ZCL_ON_OFF_CLUSTER_ID)) {
        return;
    }

    if (frame->cmd_id == ZCL_REPORT_ATTRIBUTES_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_REPORT_ATTRIBUTES_HEADER+1];
    } else if (frame->cmd_id == ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_READ_ATTRIBUTER_RESPONSE_HEADER+1];
    } else {
        return;
    }
    dp_data.dpid = frame->src_endpoint;
    dp_data.type = PROP_BOOL;

	// Report DP
    op_ret = dev_report_dp_json_async(frame->id, &dp_data, 1);
    if (op_ret != OPRT_OK) {
        PR_ERR("dev_report_dp_json_async err: %d", op_ret);
        return;
    }
}

STATIC OPERATE_RET user_zigbee_custom_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;

    // Zigbee callback
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

	// Register allowlist
    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
	ty_cJSON *app_cfg = ty_cJSON_CreateObject();
    if (app_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(app_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(app_cfg, "cache_path", "/tmp/");

    // Set storage path
    TUYA_CALL_ERR_RETURN(tuya_set_config(app_cfg));

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }

    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

	// Initialize third-party sub-device service
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // Initialize Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// Start Zigbee service
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Appendix

Validator

/**
 * Usage guide:
 *   1. Copy the content and save it to the zigbee_app.c file.
 *   2. In the macro definition TEST_UART_NAME, change the serial device to the one connected to the Zigbee module.
 *   3. Compile with the cross-compilation toolchain.
 *     $ gcc zigbee_app.c -o zigbee_app -lpthread
 *   4. Copy zigbee_app to the target board and run it. If the module version is read, it means the communication works fine. Otherwise, check the serial port.
 *     Example of successful output:
 *     Send data[5:5]: 1a c0 38 bc 7e
 *     Recv data[128:7]: 1a c1 02 0b 0a 52 7e
 *     Send data[4:4]: 80 70 78 7e
 *     Ncp reset success.
 *     Send data[8:8]: 00 42 21 a8 53 dd 4f 7e
 *     Recv data[128:11]: 01 42 a1 a8 53 28 15 d7 c1 bf 7e
 *     Send data[4:4]: 81 60 59 7e
 *     Ncp set version success.
 *     Send data[9:9]: 11 43 21 57 54 39 51 2b 7e
 *     Recv data[128:14]: 12 43 a1 57 54 39 15 b3 59 9c 5a 7a 1c 7e
 *     Send data[4:4]: 82 50 3a 7e
 *     COO manuId:0001, version:1.0.8.
 */
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

#include <termios.h>
#include <sys/ttydefaults.h>

#define TEST_UART_NAME      "/dev/ttyS1"
#define TEST_UART_SPEED     115200
#define TEST_UART_STIO_BIT  1
#define TEST_UART_RTSCTS    TRUE
#define TEST_UART_FLOWCTR   TRUE

#define UART_DEBUG_PRINTF   1

typedef unsigned int bool;
#ifndef FALSE
#define FALSE 0
#endif

#ifndef TRUE
#define TRUE 1
#endif

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

static const struct { int bps; speed_t posixBaud; } baudTable[] =
{ { 600, B600    },
  { 1200, B1200   },
  { 2400, B2400   },
  { 4800, B4800   },
  { 9600, B9600   },
  { 19200, B19200  },
  { 38400, B38400  },
  { 57600, B57600  },
  { 115200, B115200 },
  { 230400, B230400 } };

#define BAUD_TABLE_SIZE (sizeof(baudTable) / sizeof(baudTable[0]) )

// Define CRTSCTS for both ingoing and outgoing hardware flow control
// Try to resolve the numerous aliases for the bit flags
#if defined(CCTS_OFLOW) && defined(CRTS_IFLOW) && !defined(__NetBSD__)
  #undef CRTSCTS
  #define CRTSCTS (CCTS_OFLOW | CRTS_IFLOW)
#endif
#if defined(CTSFLOW) && defined(RTSFLOW)
  #undef CRTSCTS
  #define CRTSCTS (CTSFLOW | RTSFLOW)
#endif
#ifdef CNEW_RTSCTS
  #undef CRSTCTS
  #define CRTSCTS CNEW_RTSCTS
#endif
#ifndef CRTSCTS
  #define CRTSCTS 0
#endif

#define ASCII_XON   0x11
#define ASCII_XOFF  0x13

// Define the termios bit fields modified by ezspSerialInit
// (CREAD is omitted as it is often not supported by modern hardware)
#define CFLAG_MASK (CLOCAL | CSIZE | PARENB | HUPCL | CRTSCTS)
#define IFLAG_MASK (IXON | IXOFF | IXANY | BRKINT | INLCR | IGNCR | ICRNL \
                    | INPCK | ISTRIP | IMAXBEL)
#define LFLAG_MASK (ICANON | ECHO | IEXTEN | ISIG)
#define OFLAG_MASK (OPOST)

static int  g_uart_fd    = -1;
static char g_ezspSeq    = 0;
static char g_ashFramNum = 0;
static char g_ashAckNum  = 0;
static char gcur_ezspSeq = 0;

static unsigned short halCommonCrc16(unsigned char newByte, unsigned short prevResult);

int ezspSetupSerialPort(unsigned char *uart_name, unsigned int bps, unsigned char stopBits, bool rtsCts, bool flowControl)
{
    int i = 0;
    struct termios tios, checkTios;
    speed_t baud;

    // Setting this up front prevents us from closing an unset serial port FD
    // when breaking on error.
    g_uart_fd = -1;

    while (TRUE) { // break out of while on any error
        for (i = 0; i < BAUD_TABLE_SIZE; i++) {
            if (baudTable[i].bps == bps) {
                break;
            }
        }

        if (i < BAUD_TABLE_SIZE) {
            baud = baudTable[i].posixBaud;
        } else {
            printf("Invalid baud rate %d bps\r\n", bps);
            break;
        }

        if ((stopBits != 1) && (stopBits != 2)) {
             printf("Invalid number of stop bits:%d.\r\n", stopBits);
            break;
        }

        g_uart_fd = open(uart_name, O_RDWR | O_NOCTTY | O_NONBLOCK);

        if (g_uart_fd == -1) {
             printf("Serial port open failed:%s\r\n", strerror(errno));
            break;
        }

        tcflush(g_uart_fd, TCIOFLUSH); // flush all input and output data

        //#define ANDROID_PLATEM
    #if 0//#ifdef ANDROID_PLATEM

        fcntl(*serialPortFdReturn, F_SETFL, 04000);
    #else
        fcntl(g_uart_fd, F_SETFL, FNDELAY);
    #endif

        tcgetattr(g_uart_fd, &tios); // get current serial port options

        cfsetispeed(&tios, baud);
        cfsetospeed(&tios, baud);

        tios.c_cflag |= CLOCAL; // ignore modem inputs
        tios.c_cflag |= CREAD; // receive enable (a legacy flag)
        tios.c_cflag &= ~CSIZE; // 8 data bits
        tios.c_cflag |= CS8;
        tios.c_cflag &= ~PARENB; // no parity
        if (stopBits == 1) {
            tios.c_cflag &= ~CSTOPB;
        } else {
            tios.c_cflag |= CSTOPB;
        }
        if (flowControl && rtsCts) {
            tios.c_cflag |= CRTSCTS;
        } else {
            tios.c_cflag &= ~CRTSCTS;
        }

        tios.c_iflag &= ~(BRKINT | INLCR | IGNCR | ICRNL | INPCK | ISTRIP | IMAXBEL | IXON | IXOFF | IXANY);

        if (flowControl && !rtsCts) {
            tios.c_iflag |= (IXON | IXOFF); // SW flow control
        } else {
            tios.c_iflag &= ~(IXON | IXOFF);
        }

        tios.c_lflag &= ~(ICANON | ECHO | IEXTEN | ISIG); // raw input

        tios.c_oflag &= ~OPOST; // raw output

        memset(tios.c_cc, _POSIX_VDISABLE, NCCS); // disable all control chars

    #if 0//#ifdef ANDROID_PLATEM
        tios.c_cc[VSTART] = ('q'&037);           // define XON and XOFF
        tios.c_cc[VSTOP] = ('s'&037);
    #else
        tios.c_cc[VSTART] = CSTART;           // define XON and XOFF
        tios.c_cc[VSTOP] = CSTOP;
    #endif

        tios.c_cc[VMIN] = 1;
        tios.c_cc[VTIME] = 0;

        memset(checkTios.c_cc, _POSIX_VDISABLE, NCCS);
        tcsetattr(g_uart_fd, TCSAFLUSH, &tios);  // set EZSP serial port options
        tcgetattr(g_uart_fd, &checkTios); // and read back the result

        // Verify that the fields written have the right values
        i = (tios.c_cflag ^ checkTios.c_cflag) & CFLAG_MASK;
        if (i) {
            // Try again since macOS mojave seems to not have hardware flow control enabled
            tios.c_cflag &= ~CRTSCTS;
            tcsetattr(g_uart_fd, TCSAFLUSH, &tios);  // set EZSP serial port options
            tcgetattr(g_uart_fd, &checkTios);      // and read back the result
            i = (tios.c_cflag ^ checkTios.c_cflag) & CFLAG_MASK;
            if (i) {
                 printf("Termios cflag(s) in error: 0x%04X\r\n", i);
                break;
            }
        }

        i = (tios.c_iflag ^ checkTios.c_iflag) & IFLAG_MASK;
        if (i) {
             printf("Termios c_iflag(s) in error: 0x%04X\r\n", i);
            break;
        }
        i = (tios.c_lflag ^ checkTios.c_lflag) & LFLAG_MASK;
        if (i) {
             printf("Termios c_lflag(s) in error: 0x%04X\r\n", i);
            break;
        }
        i = (tios.c_oflag ^ checkTios.c_oflag) & OFLAG_MASK;
        if (i) {
             printf("Termios c_oflag(s) in error: 0x%04X\r\n", i);
            break;
        }

        for (i = 0; i < NCCS; i++) {
            if (tios.c_cc[i] != checkTios.c_cc[i]) {
                break;
            }
        }
        if (i != NCCS) {
             printf("Termios c_cc(s) in error: 0x%04X\r\n", i);
            break;
        }

        if (  (cfgetispeed(&checkTios) != baud)|| (cfgetospeed(&checkTios) != baud)) {
             printf("Could not set baud rate to %d bps\r\n", bps);
            break;
        }

        return 0;
    }

    if (g_uart_fd != -1) {
        close(g_uart_fd);
        g_uart_fd = -1;
    }

    return -1;
}

static void tuya_uart_send(unsigned char *buf, unsigned int len)
{
    int count = 0;
    int i = 0;
    count = write(g_uart_fd, buf, len);
#ifdef UART_DEBUG_PRINTF
    printf( "Send data[%d:%d]: ", len, count);
    for(i = 0; i < len; i++) {
        printf("%02x ", buf[i]&0xFF);
    }
    printf("\n");
#endif
}

static int tuya_uart_recv(unsigned char *buf, unsigned int len)
{
    int read_len = 0;
    int i = 0;
    read_len = read(g_uart_fd, buf, len);
#ifdef UART_DEBUG_PRINTF
    if(read_len > 0) {
        printf("Recv data[%d:%d]: ", len, read_len);
        for(i = 0; i < read_len; i++){
            printf("%02x ", buf[i]&0xFF);
        }
        printf("\n");
    }
#endif
    return read_len;
}

static void ashCrc(unsigned char *buf, int len, unsigned short *crcData)
{
    unsigned short crcDataTmp = 0xFFFF;
    int i = 0;
    for(i=0; i<len; i++) {
        crcDataTmp = halCommonCrc16(buf[i], crcDataTmp);
    }
    *crcData  = crcDataTmp;
}

// Define constants for the LFSR in ashRandomizeBuffer()
#define LFSR_POLY   0xB8 // polynomial
#define LFSR_SEED   0x42 // initial value (seed)

static unsigned char ashRandomizeArray(unsigned char seed, unsigned char *buf, unsigned char len)
{
  if (seed == 0) {
    seed = LFSR_SEED;
  }
  while (len--) {
    *buf++ ^= seed;
    seed = (seed & 1) ? ((seed >> 1) ^ LFSR_POLY) : (seed >> 1);
  }
  return seed;
}

static unsigned short halCommonCrc16(unsigned char newByte, unsigned short prevResult)
{
  prevResult = ((unsigned short) (prevResult >> 8)) | ((unsigned short) (prevResult << 8));
  prevResult ^= newByte;
  prevResult ^= (prevResult & 0xff) >> 4;
  prevResult ^= (unsigned short) (((unsigned short) (prevResult << 8)) << 4);

  prevResult ^= ((unsigned char) (((unsigned char) (prevResult & 0xff)) << 5))
                | ((unsigned short) ((unsigned short) ((unsigned char) (((unsigned char) (prevResult & 0xff)) >> 3)) << 8));

  return prevResult;
}

static void paraRandom(unsigned char *inoutStr, int inLen)
{
    ashRandomizeArray(0, inoutStr, inLen);
}

static void get_ncp_info_cmd(unsigned char *buf, unsigned int *len)
{
    unsigned char cmdBuf[9] = {0x00, 0x00, 0x21, 0x57, 0x54, 0x39, 0x00, 0x00, 0x7E};
    unsigned short crcData = 0;
    unsigned char ezspSeqTmp = 0;

    cmdBuf[0] =  ((g_ashFramNum & 0x07) << 4) | (g_ashFramNum & 0x07);
    ezspSeqTmp = g_ezspSeq;
    paraRandom(&ezspSeqTmp, 1);
    cmdBuf[1] = ezspSeqTmp;

    ashCrc(cmdBuf, 6, &crcData);
    cmdBuf[6] = ((crcData >> 8)& 0xff);
    cmdBuf[7] = (crcData & 0xff);

    memcpy(buf, cmdBuf, 9);
    *len = 9;

    gcur_ezspSeq = g_ezspSeq;
    g_ashFramNum++;
    g_ezspSeq++;
}

static void get_ncp_reset_cmd(unsigned char *buf, unsigned int *len)
{
    unsigned char cmdBuf[9] = {0x1A, 0xC0, 0x00, 0x00, 0x7E};
    unsigned short crcData = 0;

    ashCrc(&cmdBuf[1], 1, &crcData);
    cmdBuf[2] = ((crcData >> 8)& 0xff);
    cmdBuf[3] = (crcData & 0xff);

    memcpy(buf, cmdBuf, 5);
    *len = 5;
}

static void get_ncp_setver_cmd(unsigned char *buf, unsigned int *len)
{
    unsigned char cmdBuf[9] = {0x00, 0x00, 0x21, 0xa8, 0x53, 0x00, 0x00, 0x7E};
    unsigned short crcData = 0;
    unsigned char ezspSeqTmp = 0;

    cmdBuf[0] =  ((g_ashFramNum & 0x07) << 4) | (g_ashFramNum & 0x07);
    ezspSeqTmp = g_ezspSeq;
    paraRandom(&ezspSeqTmp, 1);
    cmdBuf[1] = ezspSeqTmp;

    ashCrc(cmdBuf, 5, &crcData);
    cmdBuf[5] = ((crcData >> 8)& 0xff);
    cmdBuf[6] = (crcData & 0xff);

    memcpy(buf, cmdBuf, 8);
    *len = 8;

    gcur_ezspSeq = g_ezspSeq;
    g_ashFramNum++;
    g_ezspSeq++;
}

static void get_ack_cmd(unsigned char *buf, unsigned int *len)
{
    unsigned char cmdBuf[4] = {0x00, 0x00, 0x00, 0x7E};
    unsigned short crcData = 0;
    cmdBuf[0] = ((0x1<<7)| (g_ashAckNum & 0x07));

    ashCrc(cmdBuf, 1, &crcData);
    cmdBuf[1] = ((crcData >> 8)& 0xff);
    cmdBuf[2] = (crcData & 0xff);

    memcpy(buf, cmdBuf, 4);
    *len = 4;

    g_ashAckNum++;
}

static void tuya_send_ack(void)
{
    unsigned char send_buf[128]={0};
    unsigned int send_len = 0;

    get_ack_cmd(send_buf,&send_len);
    tuya_uart_send(send_buf,send_len);
}

static void paraNcpData(unsigned char *inAsh, int inAshLen, unsigned char *outEzsp, int *outEzspLen)
{
    *outEzspLen = inAshLen-1-2-1; // 1byte ASH header + 2byte CRC + 1byte tail
    memcpy(outEzsp, inAsh+1, *outEzspLen);
    paraRandom(outEzsp, *outEzspLen);
}

static void para_ncp_info(unsigned short  *manuId, unsigned short  *verNUm,unsigned char *inAsh, int inAshLen)
{
    unsigned char outEzsp[32] = {0};
    int outEzspLen = 0;
    paraNcpData(inAsh, inAshLen, outEzsp, &outEzspLen);
    *manuId = ((outEzsp[7]&0xFF)<<8) | (outEzsp[6] & 0xFF);
    *verNUm = ((outEzsp[9]&0xFF)<<8) | (outEzsp[8] & 0xFF);
}

void uart_recv_hand(bool is_skip, char *data_buf,  int *data_len)
{
    unsigned char recv_cmd[128]={0};

    unsigned char total_recv_cmd[128]={0};
    int read_result = 0;
    int total_len = 0;
    int i = 0;
    int data_index = 0;
    sleep(1);

    do {
        read_result = tuya_uart_recv(recv_cmd, 128);
        if(read_result > 0){
            memcpy(total_recv_cmd+total_len, recv_cmd, read_result);
            total_len += read_result;
            memset(recv_cmd, 0, 128);
        }
    } while(read_result > 0);

    if (FALSE == is_skip && total_len > 0) {

        if (total_len <= 4) // is ack
            return;

        for (i = 0; i < total_len; i++) {
            if (0x7E == total_recv_cmd[i]) {
                break;
            }
        }
        if (i == 3) { // skip ack
            data_index += 4;
        }
        memcpy(data_buf, total_recv_cmd+data_index, total_len-data_index);
        *data_len = total_len-data_index;
    }
    if (total_len > 4) {
        tuya_send_ack();
    }
}

static int ncp_reset(void)
{
    unsigned char send_cmd[20]={0};
    unsigned char recv_cmd[128]={0};
    unsigned int send_len = 0;
    unsigned int recv_len = 0;
    int send_num = 0;
    int recv_num = 0;
    do {
        get_ncp_reset_cmd(send_cmd,&send_len);
        tuya_uart_send(send_cmd, send_len);
        recv_num = 0;
        do {
            usleep(10*1000);
            uart_recv_hand(FALSE, recv_cmd, &recv_len);
            recv_num++;
        } while ((recv_len <= 0) && (recv_num <= 200));
        send_num++;
    } while ((recv_len <= 0) && (send_num <= 3));

    if(recv_len > 0)
        return 0;
    else
        return -1;
}

static int ncp_ver_set(void)
{
    unsigned char send_cmd[20] = {0};
    unsigned char recv_cmd[128] = {0};
    unsigned int send_len = 0;
    unsigned int recv_len = 0;
    int send_num = 0;
    int recv_num = 0;

    do {
        get_ncp_setver_cmd(send_cmd, &send_len);
        tuya_uart_send(send_cmd, send_len);
        recv_num = 0;
        do {
            usleep(10*1000);
            uart_recv_hand(FALSE, recv_cmd, &recv_len);
            recv_num++;
        } while((recv_len <= 0) && (recv_num <= 200));
        send_num++;
    } while ((recv_len <= 0) && (send_num <= 3));

    if (recv_len > 0)
        return 0;
    else
        return -1;
}

static int check_zigbee_is_ok(void)
{
    unsigned char send_cmd[20]={0};
    unsigned char recv_cmd[128]={0};
    unsigned int send_len = 0;
    unsigned int recv_len = 0;
    int read_result = 0;
    unsigned short  manuId = 0;
    unsigned short  verNUm = 0;
    unsigned char ver_str[11]={0};
    int ret = 0;

    ret = ncp_reset();
    if (ret < 0) {
        printf("Ncp reset error.\n");
        return -1;
    }else{
        printf("Ncp reset success.\n");
    }

    ret = ncp_ver_set();
    if (ret < 0) {
        printf("Ncp set version error.\n");
        return -1;
    } else {
        printf("Ncp set version success.\n");
    }

    get_ncp_info_cmd(send_cmd,&send_len);
    tuya_uart_send(send_cmd, send_len);
    uart_recv_hand(FALSE, recv_cmd, &recv_len);

    if (recv_len > 0) {
        para_ncp_info(&manuId, &verNUm, recv_cmd, recv_len);
        sprintf(ver_str, "%d.%d.%d", ((verNUm>>12) & 0xF),((verNUm>>8) & 0xF),(verNUm&0xFF));
        printf("COO manuId:%04x, version:%s.\n", manuId, ver_str);
    } else {
        printf("COO error.\n");
    }

    return 0;
}

int main(char argc, char *argv[])
{
    int ret = 0;

    // open UART
    ret = ezspSetupSerialPort(TEST_UART_NAME, TEST_UART_SPEED, TEST_UART_STIO_BIT, TEST_UART_RTSCTS, TEST_UART_FLOWCTR);
    if (0 == ret) {
        check_zigbee_is_ok();
    } else {
         printf("ezspSetupSerialPort err: %d\n", ret);
    }

    return ret;
}