Zigbee 子设备接入

更新时间:2023-09-06 10:19:58下载pdf

本文档分为两部分,第一部分是 Zigbee 业务,介绍如何开启 Zigbee 功能。第二部分是 Zigbee 扩展,在开启 Zigbee 功能的基础上,如何接入三方生态 Zigbee 子设备。

本文主要介绍软件实现。硬件连接涂鸦 Zigbee 模组是前提条件,建议编码之前先检查模组的连通性,可以使用下文 验证程序 读取模组版本号来验证。

背景信息

Zigbee 子设备接入旨在降低 Zigbee 网关的开发门槛。涂鸦提供软硬一体化方案,硬件上使用涂鸦 Zigbee 模组,软件上开启 Zigbee 功能,帮助您零代码实现 Zigbee 网关开发,能够接入涂鸦生态的 Zigbee 子设备。

TuyaOS 网关开发框架还提供了 Zigbee 接口,支持接入三方生态 Zigbee 子设备。

应用场景

涂鸦 Zigbee 子设备品类众多,覆盖了电工、照明、大小家电、传感等类目,开发一款智能网关能够接入涂鸦生态 Zigbee 子设备门槛极高,开发成本太高,最佳方案是选择涂鸦公版网关。

但如果您选择的芯片平台是涂鸦未导入的,自主设计硬件,并且需要支持接入涂鸦生态 Zigbee 子设备,则本方案是最优的选择。硬件上通过 MCU 串口连接涂鸦 Zigbee 模组,软件上开启 Zigbee 功能,该产品就具备了 Zigbee 网关能力。

此外,如果您还需要把三方生态 Zigbee 子设备接入到 涂鸦 IoT 开发平台,TuyaOS 网关开发框架提供了 Zigbee 接口,支持接入三方生态 Zigbee 子设备,增强产品的差异性。

Zigbee 业务

本小节介绍使用 TuyaOS 网关开发框架如何开启 Zigbee 功能,通过搭配涂鸦 Zigbee 模组,实现 Zigbee 网关能力。

开启 Zigbee 功能主要涉及两个接口:

  • Zigbee 初始化
  • Zigbee 启动

以上接口的参数都是相同的 JSON 数据,区别在于配置信息不同。

配置说明

JSON 数据字段说明如下:

字段 描述
storage_path 持久化存储路径,用于存储 Zigbee 网络信息、子设备信息等数据。该字段已废弃,存储路径为通用配置,通过 tuya_load_config/tuya_set_config 接口设置。
cache_path 临时存储路径,用于存储 OTA 固件。该字段已废弃,存储路径为通用配置,通过 tuya_load_config/tuya_set_config 接口设置。
dev_name 指定串口设备号。
cts 是否开启硬件流控:
  • 1:打开
  • 0:关闭
根据您使用的 Zigbee 模组要求来设置是否开启硬件流控。
thread_mode 以线程还是进程方式运行 Zigbee HOST:
  • 1:线程(推荐使用)
  • 0:进程

使用示例

// ...
#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/");

    // 设置存储路径
    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);

    // 初始化 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

Zigbee 扩展

本小节将会介绍如何接入三方 Zigbee 子设备,开发出既支持涂鸦生态 Zigbee 子设备,又支持三方 Zigbee 子设备的网关产品,增强产品差异性。

把三方 Zigbee 子设备接入到涂鸦 IoT 开发平台,除了需要具备 Zigbee 基础知识,还要了解 SDK 接入子设备的开发流程,请参考 子设备接入

注册白名单

TuyaOS 网关开发框架是通过注册白名单的机制来支持三方生态 Zigbee 子设备接入的。

您需要把待接入的三方 Zigbee 子设备 厂商号ManufacturerName Attribute)和 型号列表ModelIdentifier Attribute)注册到 SDK,厂商号和型号分别是子设备入网时 Basic Cluster 中包含的。

子设备入网后,SDK 根据子设备的厂商号和型号从白名单列表中查询,如果匹配则记录设备的 MAC 地址,该设备上报的数据通过回调通知应用处理。如果入网的设备没在白名单中,则 SDK 内部处理。

实现方法:

  • 定义 TY_Z3_DEV_S 结构体数组变量,把待接入的三方子设备厂商号和型号添加到数组。
  • 定义 TY_Z3_DEVLIST_S 结构体变量,把 TY_Z3_DEV_S 结构体数组变量以及长度赋值到该变量。
  • 定义 TY_Z3_DEV_CBS_S 结构体变量,注册 Zigbee 子设备管理回调。
  • 调用 tuya_zigbee_custom_dev_mgr_init 接口注册白名单列表和 Zigbee 子设备管理回调。

使用示例:

#include "tuya_zigbee_api.h"

// 定义白名单列表
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 子设备管理回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .leave       = __my_z3_dev_leave,
    };

    // 注册白名单和设备回调
    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;
}

设备入网

在白名单中的设备入网后,SDK 调用入网回调通知应用处理,您需要在回调中解析子设备信息,然后调用绑定接口把子设备接入涂鸦 IoT 开发平台。实现方法如下:

  • 注册 TY_Z3_DEV_CBS_Sjoin 入网回调,在回调中通过解析子设备入网携带的信息,调用 tuya_iot_gw_bind_dev 接口把子设备接入到涂鸦 IoT 开发平台。
  • 注册 TY_Z3_DEV_CBS_Sreport 数据上报回调,在回调中把 ZCL(Zigbee Cluster Library) 数据转换成涂鸦 DP(Data Point)数据,调用 DP 上报接口把数据同步到涂鸦 IoT 开发平台。
  • 注册 TY_GW_SUBDEV_MGR_CBS_Sdev_bind 绑定通知回调,在回调中下发 ZCL 数据给子设备,查询子设备当前状态。
  • SDK 允许子设备入网,子设备通过复位按键进入配网状态。

设备入网的流程:

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

使用示例:

// ...
#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 数据上报通知
 * @note 您需要把 ZCL 数据转换成 DP 数据,然后把 DP 数据上报到云端。同时,还要刷新子设备心跳,以维持子设备在线状态。
 */
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};

	// 检查设备是否绑定
    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;
    }

	// 刷新设备心跳
    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);
    }

    // 这里为了方便演示,本示例以一路开关为例,仅对数据做简单处理,实际开发中需要完善 ZCL 和 DP 的映射处理
    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;

	// 上报 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 入网通知
 * @note 您需要根据子设备信息映射到对应的 PID,然后绑定子设备。
 */
STATIC VOID __my_z3_dev_join(TY_Z3_DESC_S *dev)
{
    OPERATE_RET op_ret = OPRT_OK;

	// 这里为了方便演示,本示例产品 ID 和设备固件版本都写死,实际开发中您需要根据子设备信息动态调整。
    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 绑定结果通知
 * @note 您需要下发读取属性命令给子设备,让子设备上报当前的属性值。
 */
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 };

	// 绑定失败,移除子设备
    if (result != OPRT_OK) {
        PR_ERR("dev_id: %s bind err", dev_id);
        tuya_zigbee_del_dev(dev_id);
        return;
    }

	// 配置设备心跳
    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);
    }

	// 这里为了方便演示,本示例以一路开关为例,写死了下发的数据,实际开发中需要根据实际情况处理
    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 回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .report      = __my_z3_dev_report,
    };

    // 子设备管理回调
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
        .dev_bind_ifm  = __dev_bind_cb,
    };

	// 注册设备管理
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// 注册白名单
    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/");

    // 设置存储路径
    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);

	// 初始化三方生态子设备业务
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

	// 初始化 Zigbee 业务
	TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
	TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

	// ...

	return 0;
}

控制和上报

  • 控制(指令下发过程):无论是从 App 控制还是执行联动控制,SDK 都会调用 DP 指令下发回调通知应用处理。您需要在 DP 回调中把 DP 数据转换成 Zigbee ZCL 数据,然后下发给子设备。

  • 上报(数据上报过程):无论是通过控制指令还是物理方式控制 Zigbee 子设备,Zigbee 子设备会把当前新状态上报到 SDK。SDK 调用数据上报回调通知应用处理,您需要在数据上报回调中把 Zigbee ZCL 数据转换成 DP 数据,然后同步到云端。

指令下发是以数据上报作为应答标志。因此,进入 DP 指令下发回调后要有对应 DP 上报,通常由 Zigbee 子设备主动上报,如果三方 Zigbee 子设备没有主动上报逻辑,则需要在下发控制指令后,还需要查询对应的属性值。

实现控制和上报的方法如下:

  • 注册 TY_GW_SUBDEV_MGR_CBS_Sdp_cmd_obj DP 指令下发回调,在回调中把 DP 数据转换成 Zigbee ZCL 数据,调用 tuya_zigbee_send_data 接口下发给子设备。
  • 注册 TY_Z3_DEV_CBS_Sreport 数据上报回调,在回调中把 ZCL 数据转换成 DP 数据,调用 DP 上报接口把数据同步到涂鸦 IoT 开发平台。

设备控制的流程:

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

使用示例:

#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 指令下发回调
 * @note 您需要把 DP 数据转换成 ZCL 数据,然后下发给子设备。
 */
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);

	// 检查设备是否绑定
    dev_if = tuya_iot_get_dev_if(cmd->cid);
    if (dev_if == NULL) {
        PR_WARN("%s is not bind", cmd->cid);
        return;
    }

	// 为了方便演示,本示例以一路开关为例,仅对数据做简单处理,实际开发中需要完善 ZCL 和 DP 的映射处理
    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 数据上报通知
 * @note 您需要把 ZCL 数据转换成 DP 数据,然后把 DP 数据上报到云端。同时,还要刷新子设备心跳,以维持子设备在线状态。
 */
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};

	// 检查设备是否绑定
    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;
    }

	// 刷新设备心跳
    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);
    }

    // 为了方便演示,本示例以一路开关为例,对数据只做简单处理,实际开发中需要完善 ZCL 和 DP 的映射处理
    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;

	// 上报 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 回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .report      = __my_z3_dev_report,
    };

    // 子设备管理回调
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
        .dp_cmd_obj  = __dev_cmd_obj_cb,
    };

	// 注册设备管理
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// 注册白名单
    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/");

    // 设置存储路径
    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);

	// 初始化三方生态子设备业务
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // 初始化 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

心跳管理

SDK 子设备的在离线状态是通过心跳机制来实现的,通常有两种实现方式:

  • 主动上报,子设备以一定的时间间隔定期上报数据。
  • 被动查询,SDK 以一定的时间间隔定期查询子设备状态,如读取版本号。

主动上报比较简单,三方 Zigbee 子设备配置以一定周期上报版本即可,应用在数据上报回调中刷新子设备心跳。

而被动查询是由 SDK 实现的心跳管理,实现方法如下:

  • 注册 TY_Z3_DEV_CBS_Snotify 初始化通知回调,以及 TY_GW_SUBDEV_MGR_CBS_Sdev_bind 绑定通知回调,在回调中调用 tuya_iot_set_dev_hb_cfg 接口配置设备心跳。
  • 注册 TY_GW_SUBDEV_MGR_CBS_Sdev_hb 心跳查询回调,在回调中读取设备固件版本。
  • 注册 TY_Z3_DEV_CBS_Sreport 数据上报回调,在回调中调用 tuya_iot_fresh_dev_hb 接口刷新设备心跳。

心跳管理的流程:

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

使用示例:

#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 绑定结果通知
 */
STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    OPERATE_RET op_ret = OPRT_OK;

	// 这里为了方便演示,省略了其他逻辑
	// 配置设备心跳
    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 心跳查询通知
 */
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 初始化通知回调
 * @note 您需要遍历所有子设备,如果设备类型是您注册的类型,则配置设备心跳。
 */
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 数据上报通知
 */
STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
	OPERATE_RET op_ret = OPRT_OK;

	// 这里为了方便演示,省略了其他处理
	// 刷新设备心跳
    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 回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

    // 子设备管理回调
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
    	.dev_bind = __dev_bind_cb,
        .dev_hb   = __dev_hb_cb,
    };

	// 注册设备管理
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// 注册白名单
    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/");

    // 设置存储路径
    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);

	// 初始化三方生态子设备业务
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // 初始化 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

移除重置

移除重置是指把子设备从 Zigbee 网络中移除,根据触发源不同,可以分为:

  • App 重置:注册 TY_GW_SUBDEV_MGR_CBS_Sdev_del 移除回调和 dev_reset 移除并清除数据回调,在回调中调用 tuya_zigbee_del_dev 接口把子设备从 Zigbee 网络中剔除。
  • 本地重置的实现方法:注册 TY_Z3_DEV_CBS_Sleave 移除回调,在回调中调用 tuya_iot_gw_unbind_dev 接口把子设备从涂鸦 IoT 开发平台上移除。

使用示例:

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

/**
 * @brief 移除回调
 * @note 您需要在回调中实现把子设备从云端移除。
 */
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 移除通知回调
 * @note 您需要在回调中实现把子设备从 Zigbee 网络中剔除。
 */
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 移除并清空数据通知回调
 * @note 您需要在回调中实现把子设备从 Zigbee 网络中剔除。
 */
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 回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .leave      = __my_z3_dev_report,
    };

    // 子设备管理回调
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
    	.dev_del   = __dev_del_cb,
        .dev_reset = __dev_reset_cb,
    };

	// 注册设备管理
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// 注册白名单
    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/");

    // 设置存储路径
    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);

	// 初始化三方生态子设备业务
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // 初始化 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

设备升级

Zigbee 子设备升级使用标准 OTA Cluster,SDK 提供了升级接口,封装升级过程,降低应用的开发门槛。

设备固件上传到涂鸦 IoT 开发平台以及 OTA 验证,请参考 子设备 OTA 升级

OTA 升级的实现方法如下:

  • 注册 TY_GW_SUBDEV_MGR_CBS_Sdev_upgrade 升级通知回调,在回调中调用 tuya_zigbee_upgrade_dev 接口进行固件下载和升级。
  • 注册 TY_Z3_DEV_CBS_Supgrade_end 升级结果通知回调,在回调中上报子设备的固件版本,App 上提示升级成功是以上报的版本号为依据。

使用示例:

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

/**
 * @brief 升级通知回调
 */
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 升级结果通知回调
 * @note 您需要判断升级结果,如果失败则上报升级失败,如果成功则上报新的固件版本号。
 */
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;
    }

	// 注意,以下固件版本号规则是涂鸦定义的规则,您需要根据实际情况来处理。
    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 回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .upgrade_end = __my_z3_dev_upgrade_end,
    };

    // 子设备管理回调
    TY_GW_SUBDEV_MGR_CBS_S dev_mgr_cbs = {
    	.dev_upgrade = __dev_upgrade_cb,
    };

	// 注册设备管理
	TUYA_CALL_ERR_RETURN(tuya_subdev_user_sigle_type_reg(&dev_mgr_cbs, GP_DEV_ATH_1));

	// 注册白名单
    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/");

    // 设置存储路径
    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);

	// 初始化三方生态子设备业务
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // 初始化 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

初始状态

应用启动后,通常需要查询子设备当前的属性值,然后上报到云端,以实现状态同步。

初始状态同步的实现方法如下:

  • 注册 TY_Z3_DEV_CBS_S notify 初始化通知回调,在回调中遍历所有子设备,分别下发读取属性指令给每个子设备。
  • 注册 TY_Z3_DEV_CBS_Sreport 数据上报回调,在回调中把 ZCL 数据转换成 DP 数据,调用 DP 上报接口把数据同步到涂鸦 IoT 开发平台。

使用示例:

#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 初始化通知回调
 * @note 您需要遍历所有子设备,如果设备类型是您注册的类型,则下发查询指令。
 */
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);
            }
        }

		// 这里为了方便演示,本示例以一路开关为例,写死了下发的数据,实际开发中需要根据实际情况处理
    	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 数据上报通知
 * @note 您需要把 ZCL 数据转换成 DP 数据,然后把 DP 数据上报到云端。同时,还要刷新子设备心跳,以维持子设备在线状态。
 */
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};

	// 检查设备是否绑定
    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;
    }

	// 刷新设备心跳
    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);
    }

    // 为了方便演示,本示例以一路开关为例,对数据只做简单处理,实际开发中需要完善 ZCL 和 DP 的映射处理
    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;

	// 上报 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 回调
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

	// 注册白名单
    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/");

    // 设置存储路径
    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);

	// 初始化三方生态子设备业务
	TUYA_CALL_ERR_RETURN(user_zigbee_custom_init());

    // 初始化 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));

	// 启动 Zigbee 业务
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));

    // ...

    return 0;
}

附录

验证程序

/**
 * 使用指南:
 *   1. 拷贝内容保存到 zigbee_app.c 文件中
 *   2. 修改宏定义 TEST_UART_NAME,改成连接 Zigbee 模组对应的串口设备
 *   3. 使用交叉编译工具链编译
 *     $ gcc zigbee_app.c -o zigbee_app -lpthread
 *   4. 把 zigbee_app 拷贝到目标板子上运行,能读取到模组版本则表示通讯正常,否则需要检查串口
 *     成功输入打印示例:
 *     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;
}