子设备接入指南

更新时间:2022-11-24 09:20:18下载pdf

子设备接入是网关最重要的能力之一,本文档对子设备接入的关键流程进行详细说明,通过本文档开发者将会了解基于综合 SDK 如何接入子设备。

前提条件

本文档的前置条件是在涂鸦 IoT 平台上创建一个子设备产品,详细请参见 创建子设备

创建子设备

创建子设备产品跟创建网关产品是大同小异的,本小节只介绍关键的步骤,将会演示在涂鸦 IoT 平台上创建一个三路开关产品。

  1. 登录 涂鸦 IoT 开发平台

  2. 标准类目 中选择 电工 > 开关

    子设备接入指南

  3. 智能化方式选择 产品开发,产品方案选择 自定义方案

    子设备接入指南

  4. 填写产品名称和产品型号,通讯协议选择 Zigbee,这里的通讯协议只作为产品的描述,不影响实际使用,所以无论实际是接入什么类型协议子设备,默认选择 Zigbee 即可。

    子设备接入指南

  5. 本文以创建三路开关为例,所以添加了三个标准功能点,分别代表三路控制。

    子设备接入指南

  6. 单击 设备面板 选项卡,在 设备面板 页面根据个人偏好,选择合适的 App 面板。

    子设备接入指南

至此,三路开关产品已经创建完成了。

开发向导

网关是涂鸦云与子设备通讯的桥梁,网关与涂鸦云间的通信逻辑由 SDK 实现,网关与子设备的通信逻辑需要开发者实现,开发者开发的代码称为 应用

SDK 和应用的交互机制

  • SDK -> 应用采用回调方式
  • 应用 -> SDK 采用函数接口方式

通常,网关主控使用串口方式与无线模组通讯,无线模组使用特定的无线协议(如 Zigbee、Bluetooth 等)跟子设备进行无线通讯。

子设备接入指南

设备入网

设备入网是子设备通过网关绑定到涂鸦云的过程,也称设备激活。

从 C 端用户的角度来看,标准的操作流程:

  1. 打开涂鸦智能 App,选择网关进入网关面板主页,单击 添加子设备,让网关处于允许添加子设备状态;

  2. 长按子设备复位按键,让子设备进入配网状态,进入配网状态的子设备定时广播;

  3. 子设备加入到网关所在的网络后,网关把子设备绑定到涂鸦云,App 上会显示已入绑定的子设备列表,再单击 完成 结束入网。

设备入网的交互流程示意图:

子设备接入指南

根据以上示意图,SDK 与应用涉及三部分交互:

  1. SDK 通知应用允许或禁止子设备入网。
  2. 应用调用 SDK 的设备绑定接口,把子设备绑定到涂鸦云。
  3. SDK 通知应用子设备的绑定结果。

代码的实现步骤:

  1. 调用 tuya_iot_reg_gw_mgr_cb 接口注册设备管理回调,并实现对应的回调接口。

  2. 实现网关与子设备的通信逻辑开发。

  3. 子设备入网后,调用 tuya_iot_gw_bind_dev 接口把子设备绑定到涂鸦云。

示例代码:

STATIC BOOL_T __dev_add_cb(CONST GW_PERMIT_DEV_TP_T tp, CONST BOOL_T permit, CONST UINT_T timeout)
{
    /** TODO
     * permit == TRUE,允许子设备入网,入网超时时间为 timeout,超时禁止子设备入网
     * permit == FALSE, 禁止子设备入网
     */
     
     /**
      * 以下代码为演示子设备接入,应用收到 SDK 的允许子设备入网通知时:
      *   a) 直接使用固定的信息绑定子设备。
      *   b) 绑定之前,检查该子设备是否已绑定,避免重复绑定。
      */
      DEV_DESC_IF_S *dev_if = NULL;
      CHAR_T *dev_id = "abcdefabcdef";
      CHAR_T *pid    = "d1xabwgg"; // 涂鸦 IoT 平台上创建的三路开关
      if (permit) {
        dev_if = tuya_iot_get_dev_if(dev_id);
        if (dev_if && dev_if->bind) {
            return TRUE;
        }
        tuya_iot_gw_bind_dev(DEV_ATTACH_MOD_1, 0, dev_id, pid, "1.0.0");
      }
      
      return TRUE;
}

STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    /**
     * result != OPRT_OK,绑定失败,移除子设备
     * result == OPRT_OK
     *  a) 使用 tuya_iot_set_dev_hb_cfg 接口配置子设备心跳超时
     *  b) 读取子设备当前状态并上报
     */
     
     /**
      * 以下代码为演示子设备接入,绑定成功时:
      *   a) 配置子设备心跳超时时间为3分钟,查询次数为3次。
      *   b) 上报子设备状态。
      */
      if (result == OPRT_OK) {
          tuya_iot_set_dev_hb_cfg(dev_id, 120, 3, FALSE);

          TY_OBJ_DP_S *dps = (TY_OBJ_DP_S *)Malloc(3 * SIZEOF(TY_OBJ_DP_S));
          if (dps == NULL) {
              return;
          }
          
          // 三路开关全开
          for (int i = 0; i < 3; i++) {
              dps[i].dpid = (i + 1);
            dps[i].type = PROP_BOOL;
            dps[i].value.dp_bool = TRUE;
          }     
          dev_report_dp_json_async(dev_id, dps, 3);
      }
}

/**
 * 定义回调处理,入网过程涉及 dev_add 和 dev_bind_ifm 回调
 */
TY_IOT_DEV_CBS_S dev_mgr_cbs = {
    .dev_add       = __dev_add_cb,
    .dev_bind_ifm  = __dev_bind_cb,
};

/**
 * 参数1:协议类型,tp 合理范围 [DEV_ATTACH_MOD_1, DEV_ATTACH_MOD_10]
 *               tp < DEV_ATTACH_MOD_1 预留给 SDK
 * 参数2:设备管理回调结构体
 */
tuya_iot_reg_gw_mgr_cb(DEV_ATTACH_MOD_1, &dev_mgr_cbs);

设备控制

涂鸦对设备功能进行了抽象,用功能点来表示。功能点支持数值型、布尔型、枚举型、字符串型、故障型以及 RAW 透传数据,类似 C 语言的数据类型,对嵌入式开发非常友好。

指令下发时,开发者需要把功能点转成特定协议指令下发给子设备,子设备执行完控制指令,要把新状态同步到涂鸦云,涂鸦云以上报信息作为控制的确认响应。

数据上报时,开发者需要把特定协议指令转成功能点上报到涂鸦云。

设备控制的交互流程示意图:

子设备接入指南

根据以上示意图,SDK 与应用涉及两部分交互:

  1. SDK 通知应用处理功能点指令。
  2. 应用调用 SDK 的功能点上报接口,把子设备的功能点上报到涂鸦云。

代码的实现步骤:

  1. 调用 tuya_iot_reg_dp_cb 接口注册功能点管理回调,并实现对应的回调接口。

  2. 实现网关与子设备的通信逻辑开发。

  3. 收到子设备的上报数据,把数据转成功能点,调用 dev_report_dp_json_asyncdev_report_dp_raw_sync 接口把子设备的功能点数据上报到涂鸦云。

示例代码:

STATIC VOID __dev_cmd_obj_cb(CONST TY_RECV_OBJ_DP_S *cmd)
{
     /**
      * 回调中实现把 OBJ 类型功能点转成特定协议数据下发给子设备
      *   a) 读取子设备的 uddd 信息,uddd 是绑定接口的参数,由开发者自定义,用来描述子设备,以方便映射处理
      *   b) 根据 uddd 把功能点映射到子设备特定协议数据
      */
     
     /**
      * 以下代码为演示子设备控制处理,收到控制指令时:
      *   a) 解析收到的功能点数据并打印。
      *   b) 直接把收到的数据上报给涂鸦云。
      */  
     for (INT_T i = 0; i < cmd->dps_cnt; i++) {
         PR_DEBUG("dpid: %d", cmd->dps[i].dpid);
             switch (cmd->dps[i].type) {
             case PROP_BOOL:
                 PR_DEBUG("dp_bool value: %d", cmd->dps[i].value.dp_bool);
                 break;
             case PROP_VALUE:
                 PR_DEBUG("dp_value value: %d", cmd->dps[i].value.dp_value);
                 break;
             case PROP_ENUM:
                 PR_DEBUG("dp_enum value: %d", cmd->dps[i].value.dp_enum);
                 break;
             case PROP_STR:
                 PR_DEBUG("dp_str value: %s", cmd->dps[i].value.dp_str);
                 break;
             }
     }
     
     dev_report_dp_json_async(cmd->cid, cmd->dps, cmd->dps_cnt);
}

STATIC VOID __dev_cmd_raw_cb(CONST TY_RECV_RAW_DP_S *dp)
{
    /**
     * 回调中实现把 RAW 类型功能点转成特定协议数据下发给子设备
     */
}

TY_IOT_DP_CBS_S dev_dp_cbs = {
    .obj   = __dev_cmd_obj_cb,
    .raw   = __dev_cmd_raw_cb,
};

/**
 * 参数1:设备类型,可以指定功能点回调是仅子设备、仅网关、还者子设备+网关
 * 参数2:协议类型,tp 合理范围 [DEV_ATTACH_MOD_1, DEV_ATTACH_MOD_10]
 *               tp < DEV_ATTACH_MOD_1 预留给 SDK
 * 参数3:功能点管理回调
 */
tuya_iot_reg_dp_cb(DP_DEV, DEV_ATTACH_MOD_1, &dev_dp_cbs);

心跳保活

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

  1. 应用定期 ping 子设备(可以通过定期读取版本号等方式实现 ping)。
  2. 子设备定时上报数据(上报版本号等)。

SDK 实现了心跳管理,采用的是方式1,当心跳超时时,通知应用查询子设备是否在线,应用收到子设备上报的任何信息,都调用 tuya_iot_fresh_dev_hb 接口刷新心跳。

注册心跳超时通知回调:

STATIC VOID __dev_hb_cb(CONST CHAR_T *dev_id)
{
    /**
     * 回调中查询子设备是否在线,如读取版本号。
     * 当收到子设备上报任何消息,都调用 tuya_iot_fresh_dev_hb 接口刷新心跳。
     */

     /**
      * 以下代码为演示子设备心跳,收到心跳超时时:
      *   a) 直接调用心跳刷新,让其一直保持在线。
      */ 
     tuya_iot_fresh_dev_hb(dev_id);
}

TY_IOT_DEV_CBS_S dev_mgr_cbs = {
    .dev_hb        = __dev_hb_cb,
    .dev_add       = __dev_add_cb,
    .dev_bind_ifm  = __dev_bind_cb,
};

tuya_iot_reg_gw_mgr_cb(DEV_ATTACH_MOD_1, &dev_mgr_cbs);

SDK 默认的心跳超时是 10 秒,如需要调整心跳超时时间,需要在程序启动的时候遍历所有子设备,然后配置心跳超时时间。

DEV_DESC_IF_S *dev_if = NULL;
VOID *iter = NULL;

dev_if = tuya_iot_dev_traversal(&iter);
while (dev_if) {
    if (dev_if->tp == DEV_ATTACH_MOD_1) {
        // 读取子设备状态,同步到涂鸦云
        // 调用 tuya_iot_set_dev_hb_cfg 设置心跳超时
        
        /**
         * 以下代码为演示子设备心跳配置,程序启动时把已入网的子设备心跳超时都配置成 3 分钟。
         */ 
          tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
    }
}

重置移除

重置和移除的逻辑比较简单,从 App 上 移除移除并清除数据 时,SDK 触发回调通知应用处理。

注册移除和重置回调:

STATIC VOID __dev_del_cb(CONST CHAR_T *dev_id, CONST GW_DELDEV_TYPE type)
{
    /**
     * 回调中实现移除子设备逻辑。
     *   type == GWDEV_DELTYPE_MQTT:MQTT 实时移除。
     *   type == GWDEV_DELTYPE_SYNC:设备列表同步移除。
     */
}

STATIC VOID __dev_reset_cb(CONST CHAR_T *dev_id, DEV_RESET_TYPE_E type)
{
    /**
     * 回调中实现重置子设备逻辑。
     *   type == DEV_REMOTE_RESET_FACTORY:App 移除
     *   type == DEV_RESET_DATA_FACTORY:激活时需要清除数据
     */
}

TY_IOT_DEV_CBS_S dev_mgr_cbs = {
    .dev_del       = __dev_del_cb,
    .dev_reset     = __dev_reset_cb,
    .dev_hb        = __dev_hb_cb,
    .dev_add       = __dev_add_cb,
    .dev_bind_ifm  = __dev_bind_cb,
};

tuya_iot_reg_gw_mgr_cb(DEV_ATTACH_MOD_1, &dev_mgr_cbs);

设备 OTA

OTA 是嵌入式设备非常重要的功能,一般嵌入式产品都要求支持 OTA。

本小节将会介绍基于 SDK 开发子设备 OTA 的关键流程,通过本小节开发者将会了解如何实现子设备 OTA。

创建固件KEY

涂鸦 IoT 开发平台从固件 KEY 的维度来管理固件,一个固件 KEY 对应 N 个版本固件,所以在上传固件之前,我们需要先创建固件 KEY。

创建固件 KEY 的步骤如下:

  1. 我们在 创建子设备 章节创建了一个三路开关产品,在涂鸦 IoT 平台产品管理页面选择该产品,单击 继续开发

    子设备接入指南

  2. 进入产品开发页面,单击 硬件开发 选项卡,在 硬件开发 页面选择 新增自定义固件

    子设备接入指南

  3. 在新增固件页面填写信息,完成后单击 生成固件KEY,完成固件 KEY 创建。

    关键字段说明:

    固件类型:协议类型 < 10 是 SDK 预留,开发者要使用 >= 10,所以此处选择 扩展固件

    升级通道号:与绑定子设备接口的协议类型入参一致。

    固件升级超时时间:在超时时间内未上报新的版本号,App 提示升级超时。

    子设备接入指南

新建固件版本

上一小节已成功创建固件 KEY,本小节介绍如何上传固件。

  1. 单击 新增固件版本

    子设备接入指南

  2. 填写 固件版本,并上传 生产固件升级固件,单击 保存并上架

    说明:此处仅为了演示,上传的固件内容为 “Hello World” 字符串。

    子设备接入指南
  3. 上架确认,单击 确认上架

    子设备接入指南

验证

上一小节已新建一个固件版本,并将固件上传到涂鸦 IoT 平台,本小节介绍如何添加白名单验证。

  1. 展开 更多,选择 OTA升级

    子设备接入指南

  2. 选择我们新创建的固件 KEY,并单击 新建固件升级

    子设备接入指南
  3. 选择刚才新建的固件版本作为 固件版本 ,选择 App 提醒升级 作为 升级方式,并输入版本描述后,单击 确定

    子设备接入指南
  4. 单击 验证,需要输入设备虚拟 ID 添加到白名单,打开 App 进入子设备的面板主页,会提示有新版可升级。

    说明:设备虚拟 ID 可以在 App 上查看。进入子设备的面板后,轻按右上角的 “…” 打开高级页面,再轻按 设备信息 就能看到。

    子设备接入指南

代码实现

子设备 OTA 的交互流程示意图:

子设备接入指南

SDK 与应用涉及以下几部分交互:

  1. SDK 通知应用有新固件。
  2. 应用调用 SDK 的固件下载接口。
  3. SDK 分块下载固件文件,把下载的数据给应用处理。
  4. 固件下载完成,SDK 通知应用下载结果,应用根据下载结果做决策。
  5. 若下载成功,应用把固件发送给子设备,并调用 SDK 的更新进度条接口。
  6. 若下载失败,应用调用 SDK 的升级状态上报接口,上报升级失败。

代码实现示例:

STATIC CHAR_T upg_dev[DEV_ID_LEN] = {0};

STATIC OPERATE_RET __dev_ota_data(IN CONST FW_UG_S *fw, 
                                  IN CONST UINT_T total_len, 
                                  IN CONST UINT_T offset,
                                  IN CONST BYTE_T *data, 
                                  IN CONST UINT_T len, 
                                  OUT UINT_T *remain_len, 
                                  IN PVOID_T pri_data)
{
    /**
     * 固件数据接收回调接口,在该回调接口中实现把固件数据写入到文件。
     */

    /**
     * 以下代码为演示子设备固件下载:
     *   a) 直接把数据打印,没有做保存处理。
     *   b) 根据下载进度上报进度条。
     */ 
    for (INT_T i = 0; i < len; i++) {
        PR_DEBUG_RAW("%02x ", data[i]);
    }
    PR_DEBUG_RAW("\n");
    
    UINT_T percent = 0;
    percent = ((offset * 100) / (total_len+1));
    if (percent >= 99) {
        percent = 98;
    }
    tuya_iot_dev_upgd_progress_rept(percent, upg_dev, fw->tp);

    return OPRT_OK;
}

STATIC VOID __dev_ota_notify(IN CONST FW_UG_S *fw, 
                             IN CONST INT_T download_result, 
                             IN PVOID_T pri_data)
{
    /**
     * download_result == OPRT_OK,固件下载成功,通知子设备升级,并把固件发送给子设备。
     * download_result != OPRT_OK,固件下载失败,上报升级状态。
     */
     
    /**
     * 以下代码为演示子设备升级通知处理:
     *   下载成功:上报新版本。
     *   下载失败:上报升级状态。
     */
    if (download_result == OPRT_OK) {
        tuya_iot_gw_subdevice_update(upg_dev, fw->sw_ver);
    } else {
        tuya_iot_dev_upgd_result_report(upg_dev, fw->tp, 4);
    }
}

STATIC VOID __dev_upgrade_cb(CONST CHAR_T *dev_id, CONST FW_UG_S *fw)
{
    /**
     * 在回调接口中调用固件下载接口,开发者可以根据实际情况添加处理逻辑
     */
    strncpy(upg_dev, dev_id, SIZEOF(upg_dev));
    tuya_iot_upgrade_dev(dev_id, fw, __dev_ota_data, __dev_ota_notify, NULL);
}

TY_IOT_DEV_CBS_S dev_mgr_cbs = {
    .dev_upgrade   = __dev_upgrade_cb,
    .dev_del       = __dev_del_cb,
    .dev_reset     = __dev_reset_cb,
    .dev_hb        = __dev_hb_cb,
    .dev_add       = __dev_add_cb,
    .dev_bind_ifm  = __dev_bind_cb,
};

tuya_iot_reg_gw_mgr_cb(DEV_ATTACH_MOD_1, &dev_mgr_cbs);

附录

完整代码

#include <unistd.h>

#include "uni_log.h"
#include "mem_pool.h"
#include "base_os_adapter.h"
#include "tuya_iot_base_api.h"
#include "tuya_iot_com_api.h"

#include "tuya_iot_sdk_api.h"
#include "tuya_iot_sdk_defs.h"

#include "tuya_gw_subdev_api.h"

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
#include "tuya_os_adapt_bt.h"
#endif

#define PID       "fljmamufiym5fktz"                  // 替换成自己的产品 ID
#define UUID      "tuya461dbc63aeeb991f"              // 替换成自己的 UUID
#define AUTHKEY   "c8X4PR4wx1gMFaQlaZu5dfgVvVRwB8Ug"  // 替换成自己的 AUTHKEY

STATIC CHAR_T upg_dev[DEV_ID_LEN] = {0};

STATIC VOID __gw_reset_cb(GW_RESET_TYPE_E type)
{
    PR_DEBUG("gw reset callback, type: %d", type);

    if (GW_RESET_DATA_FACTORY != type) {
        exit(0);
    }

    return;
}

STATIC VOID __gw_upgrade_cb(CONST FW_UG_S *fw)
{
    PR_DEBUG("gw upgrade callback");

    if (fw == NULL) {
        PR_ERR("invalid param");
        return;
    }

    PR_DEBUG("        tp: %d", fw->tp);
    PR_DEBUG("    fw_url: %s", fw->fw_url);
    PR_DEBUG("    sw_ver: %s", fw->sw_ver);
    PR_DEBUG("   fw_hmac: %s", fw->fw_hmac);
    PR_DEBUG(" file_size: %u", fw->file_size);

    return;
}

STATIC VOID __gw_active_stat_cb(GW_STATUS_E status)
{
    PR_DEBUG("gw active stat callback, status: %d", status);

    return;
}

STATIC VOID __gw_reboot_cb(VOID)
{
    PR_DEBUG("gw reboot callback");

    exit(0);

    return;
}

STATIC VOID __nw_stat_cb(IN CONST SDK_NW_STAT_T stat)
{
    PR_DEBUG("network stat: %d", stat);

    return;
}

STATIC VOID __wired_stat_cb(IN CONST SDK_WIRED_NW_STAT_T stat)
{
    PR_DEBUG("wired stat: %d", stat);

    return;
}

STATIC BOOL_T __dev_add_cb(CONST GW_PERMIT_DEV_TP_T tp, CONST BOOL_T permit, CONST UINT_T timeout)
{
    /** TODO
     * permit == TRUE,允许子设备入网,入网超时时间为 timeout,超时禁止子设备入网
     * permit == FALSE, 禁止子设备入网
     */
     
     /**
      * 以下代码为演示子设备接入,应用收到 SDK 的允许子设备入网通知时:
      *   a) 直接使用固定的信息绑定子设备。
      *   b) 绑定之前,检查该子设备是否已绑定,避免重复绑定。
      */
      DEV_DESC_IF_S *dev_if = NULL;
      CHAR_T *dev_id = "abcdefabcdef";
      CHAR_T *pid    = "d1xabwgg"; // 涂鸦 IoT 平台上创建的三路开关
      if (permit) {
        dev_if = tuya_iot_get_dev_if(dev_id);
        if (dev_if && dev_if->bind) {
            return TRUE;
        }
        tuya_iot_gw_bind_dev(DEV_ATTACH_MOD_1, 0, dev_id, pid, "1.0.0");
      }
      
      return TRUE;
}

STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    /**
     * result != OPRT_OK,绑定失败,移除子设备
     * result == OPRT_OK
     *  a) 使用 tuya_iot_set_dev_hb_cfg 接口配置子设备心跳超时
     *  b) 读取子设备当前状态并上报
     */
     
     /**
      * 以下代码为演示子设备接入,绑定成功时:
      *   a) 配置子设备心跳超时时间为3分钟,查询次数为3次。
      *   b) 上报子设备状态。
      */
      if (result == OPRT_OK) {
          tuya_iot_set_dev_hb_cfg(dev_id, 120, 3, FALSE);

          TY_OBJ_DP_S *dps = (TY_OBJ_DP_S *)Malloc(3 * SIZEOF(TY_OBJ_DP_S));
          if (dps == NULL) {
              return;
          }
          
          // 三路开关全开
          for (int i = 0; i < 3; i++) {
              dps[i].dpid = (i + 1);
            dps[i].type = PROP_BOOL;
            dps[i].value.dp_bool = TRUE;
          }     
          dev_report_dp_json_async(dev_id, dps, 3);
      }
}

STATIC VOID __dev_hb_cb(CONST CHAR_T *dev_id)
{
    /**
     * 回调中查询子设备是否在线,如读取版本号。
     * 当收到子设备上报任何消息,都调用 tuya_iot_fresh_dev_hb 接口刷新心跳。
     */

     /**
      * 以下代码为演示子设备心跳,收到心跳超时时:
      *   a) 直接调用心跳刷新,让其一直保持在线。
      */ 
     tuya_iot_fresh_dev_hb(dev_id);
}

STATIC VOID __dev_del_cb(CONST CHAR_T *dev_id, CONST GW_DELDEV_TYPE type)
{
    /**
     * 回调中实现移除子设备逻辑。
     *   type == GWDEV_DELTYPE_MQTT:MQTT 实时移除。
     *   type == GWDEV_DELTYPE_SYNC:设备列表同步移除。
     */
}

STATIC VOID __dev_reset_cb(CONST CHAR_T *dev_id, DEV_RESET_TYPE_E type)
{
    /**
     * 回调中实现重置子设备逻辑。
     *   type == DEV_REMOTE_RESET_FACTORY:App 移除
     *   type == DEV_RESET_DATA_FACTORY:激活时需要清除数据
     */
}

STATIC OPERATE_RET __dev_ota_data(IN CONST FW_UG_S *fw, 
                                  IN CONST UINT_T total_len, 
                                  IN CONST UINT_T offset,
                                  IN CONST BYTE_T *data, 
                                  IN CONST UINT_T len, 
                                  OUT UINT_T *remain_len, 
                                  IN PVOID_T pri_data)
{
    /**
     * 固件数据接收回调接口,在该回调接口中实现把固件数据写入到文件。
     */

    /**
     * 以下代码为演示子设备固件下载:
     *   a) 直接把数据打印,没有做保存处理。
     *   b) 根据下载进度上报进度条。
     */ 
    for (INT_T i = 0; i < len; i++) {
        PR_DEBUG_RAW("%02x ", data[i]);
    }
    PR_DEBUG_RAW("\n");
    
    UINT_T percent = 0;
    percent = ((offset * 100) / (total_len+1));
    if (percent >= 99) {
        percent = 98;
    }
    tuya_iot_dev_upgd_progress_rept(percent, upg_dev, fw->tp);

    return OPRT_OK;
}

STATIC VOID __dev_ota_notify(IN CONST FW_UG_S *fw, 
                             IN CONST INT_T download_result, 
                             IN PVOID_T pri_data)
{
    /**
     * download_result == OPRT_OK,固件下载成功,通知子设备升级,并把固件发送给子设备。
     * download_result != OPRT_OK,固件下载失败,上报升级状态。
     */
     
    /**
     * 以下代码为演示子设备升级通知处理:
     *   下载成功:上报新版本。
     *   下载失败:上报升级状态。
     */
    if (download_result == OPRT_OK) {
        tuya_iot_gw_subdevice_update(upg_dev, fw->sw_ver);
    } else {
        tuya_iot_dev_upgd_result_report(upg_dev, fw->tp, 4);
    }
}

STATIC VOID __dev_upgrade_cb(CONST CHAR_T *dev_id, CONST FW_UG_S *fw)
{
    /**
     * 在回调接口中调用固件下载接口,开发者可以根据实际情况添加处理逻辑
     */
    strncpy(upg_dev, dev_id, SIZEOF(upg_dev));
    tuya_iot_upgrade_dev(dev_id, fw, __dev_ota_data, __dev_ota_notify, NULL);
}

STATIC VOID __dev_cmd_obj_cb(CONST TY_RECV_OBJ_DP_S *cmd)
{
     /**
      * 回调中实现把 OBJ 类型功能点转成特定协议数据下发给子设备
      *   a) 读取子设备的 uddd 信息,uddd 是绑定接口的参数,由开发者自定义,用来描述子设备,以方便映射处理
      *   b) 根据 uddd 把功能点映射到子设备特定协议数据
      */
     
     /**
      * 以下代码为演示子设备控制处理,收到控制指令时:
      *   a) 解析收到的功能点数据并打印。
      *   b) 直接把收到的数据上报给涂鸦云。
      */  
     for (INT_T i = 0; i < cmd->dps_cnt; i++) {
         PR_DEBUG("dpid: %d", cmd->dps[i].dpid);
             switch (cmd->dps[i].type) {
             case PROP_BOOL:
                 PR_DEBUG("dp_bool value: %d", cmd->dps[i].value.dp_bool);
                 break;
             case PROP_VALUE:
                 PR_DEBUG("dp_value value: %d", cmd->dps[i].value.dp_value);
                 break;
             case PROP_ENUM:
                 PR_DEBUG("dp_enum value: %d", cmd->dps[i].value.dp_enum);
                 break;
             case PROP_STR:
                 PR_DEBUG("dp_str value: %s", cmd->dps[i].value.dp_str);
                 break;
             }
     }
     
     dev_report_dp_json_async(cmd->cid, cmd->dps, cmd->dps_cnt);
}

STATIC VOID __dev_cmd_raw_cb(CONST TY_RECV_RAW_DP_S *dp)
{
    /**
     * 回调中实现把 RAW 类型功能点转成特定协议数据下发给子设备
     */
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    VOID *iter = NULL;

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
        .raw   = __dev_cmd_raw_cb,
    };

    /**
     * 定义回调处理,入网过程涉及 dev_add 和 dev_bind_ifm 回调
     */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_upgrade   = __dev_upgrade_cb,
        .dev_del       = __dev_del_cb,
        .dev_reset     = __dev_reset_cb,
        .dev_hb        = __dev_hb_cb,
        .dev_add       = __dev_add_cb,
        .dev_bind_ifm  = __dev_bind_cb,
    };

    /**
     * 参数1:协议类型,tp 合理范围 [DEV_ATTACH_MOD_1, DEV_ATTACH_MOD_10]
     *               tp < DEV_ATTACH_MOD_1 预留给 SDK
     * 参数2:设备管理回调结构体
     */
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(DEV_ATTACH_MOD_1, &dev_mgr_cbs));

    /**
     * 参数1:设备类型,可以指定功能点回调是仅子设备、仅网关、还者子设备+网关
     * 参数2:协议类型,tp 合理范围 [DEV_ATTACH_MOD_1, DEV_ATTACH_MOD_10]
     *               tp < DEV_ATTACH_MOD_1 预留给 SDK
     * 参数3:功能点管理回调
     */
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, DEV_ATTACH_MOD_1, &dev_dp_cbs));

    dev_if = tuya_iot_dev_traversal(&iter);
    while (dev_if) {
        if (dev_if->tp == DEV_ATTACH_MOD_1) {
            // 读取子设备状态,同步到涂鸦云
            // 调用 tuya_iot_set_dev_hb_cfg 设置心跳超时

            /**
             * 以下代码为演示子设备心跳配置,程序启动时把已入网的子设备心跳超时都配置成 3 分钟。
             */ 
              tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
        }
    }
}

int main(int argc, char **argv)
{
    OPERATE_RET rt = OPRT_OK;
    GW_PROD_INFO_S prod_info = {0};

    /* gw base callback */
    TY_GW_INFRA_CBS_S gw_cbs = {
        .gw_reset_cb       = __gw_reset_cb,
        .gw_upgrade_cb     = __gw_upgrade_cb,
        .gw_active_stat_cb = __gw_active_stat_cb,
        .gw_reboot_cb      = __gw_reboot_cb,
    };

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
    tuya_os_adapt_reg_bt_intf();
#endif

    /* initiate os-layer service*/
    tuya_os_intf_init();

    /* initiate iot-layer service */
    TUYA_CALL_ERR_RETURN(tuya_iot_init("./"));

    /* set the logging level to debug */
    SET_PR_DEBUG_LEVEL(TY_LOG_LEVEL_DEBUG);

    PR_DEBUG("SDK INFO: %s", tuya_iot_get_sdk_info());

    /* set uuid and authkey */
    prod_info.uuid     = UUID;
    prod_info.auth_key = AUTHKEY;
    TUYA_CALL_ERR_RETURN(tuya_iot_set_gw_prod_info(&prod_info));

    /* pre-initiate sdk service */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_pre_init(TRUE));  

    /* initiate application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_user_svc_init(&gw_cbs));
    TUYA_CALL_ERR_RETURN(__user_init());

    /* initiate sdk service */
#if defined(GW_SUPPORT_WIRED_WIFI) && (GW_SUPPORT_WIRED_WIFI==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wired_wf_sdk_init(IOT_GW_NET_WIRED_WIFI, GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#elif defined(WIFI_GW) && (WIFI_GW==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wf_sdk_init(GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#else
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_init(PID, USER_SW_VER, NULL, 0));
#endif

    /* register net stat notification callback */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_reg_netstat_cb(__nw_stat_cb, __wired_stat_cb, NULL));

    /* start application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_user_svc_start(NULL));

    while (1) {
        sleep(10);
    }

    return 0;
}