通用 Wi-Fi SDK Demo

更新时间:2024-06-25 10:03:13下载pdf

在涂鸦开发者平台下载的模组 SDK 开发资料包内,有相关的应用 Demo 可以用于参考开发应用代码,此文档以智能单插 Demo 为例,介绍如何通过涂鸦平台,使用模组 SDK 开发方式,开发一款智能单插产品。

准备工作

  • 使用通用 Wi-Fi SDK Demo 前,您至少已经在 涂鸦开发者平台 上创建了一款采用 Wi-Fi 通讯方式的智能产品,例如一款电工插座产品。详细步骤请参考 产品智能化

  • 在产品开发的 硬件开发 阶段,选择 涂鸦模组 SDK 开发 方式,并根据产品需求选择合适的模组,下载开发使用的 SDK 和 功能调试文件。

    通用 Wi-Fi SDK Demo

Demo 目录结构

平台下载到的 SDK 开发资料包解压后,可以在 apps 文件夹下找到 demo 例程,这里以插座 Demo 为例演示,目录结构如下。

通用 Wi-Fi SDK Demo

Demo 中包含的文件及相关说明如下所示:

文件名 说明
tuya_device.c 包含设备相关初始化,功能代码,产测等
hw_table.c 硬件相关的初始化及控制操作等
tuya_device.h 头文件,其中包含关键固件 key 参数
hw_table.h 头文件,硬件控制接口等

Demo 代码说明

本章节介绍 Demo 代码是如何实现插座的功能,以及怎么参考 Demo 进行应用代码的开发。

通信逻辑

设备与涂鸦云端交互的逻辑主要概括如下:

  1. 涂鸦设备与云端使用 Json 数据格式进行交互,数据的标识和定义是在平台第一步选择功能点处配置的,demo 代码在 tuya_device.h 文件中通过 typedef 定义 DP 信息

    通用 Wi-Fi SDK Demo

  2. 在涂鸦开发者平台创建产品会产生 PID 信息,PID 是设备的唯一标识,产品的配置都是和PID绑定的,比如App UI面板、云端配置、语音功能等,因此需要将 PID 信息写入到代码中,demo 代码在 tuya_device.h 中通过宏定义写入 PID 信息

    通用 Wi-Fi SDK Demo

  3. 设备要连接到涂鸦云端,需要进行授权,授权码的获取和授权的流程请参考 通用 Wi-Fi SDK 烧录、授权和产测

    说明:现阶段获取授权码需要通过工单,提供代码内的PID信息获取。

Demo 功能

Demo 实现的功能概括如下:

功能名称 实现方式
本地控制 检测按键IO口状态,控制继电器IO口输出信号对继电器进行控制,如果设备联网,将状态上报到云端
进入配网状态 检测按键IO口状态,如果检测到长按按键超过一定时间后(5S),调用重置函数,使设备进入到配网状态
状态指示 判断设备当前的状态,并通过LED灯处于亮、灭、快闪、慢闪对不同状态进行指示
App配网 此部分代码涂鸦SDK已经封装完成,设备进入到配网状态后,使用App选择对应的配网方式进行配网即可,无需编写代码
App控制 解析App下发数据对硬件进行控制,并将状态上报到云端
App移除 调用移除接口,控制设备进入到配网状态
语音控制 此部分代码与App对设备进行控制原理相同
定时开关 启动软件定时器,间隔固定时间,更新倒计时,判断时间结束时,控制设备响应相应的动作
断电记忆 控制设备到一个状态时,启用一个定时器,设置时间,时间到后,将设备当前的状态写到Flash内,当重新上电时,去Flash中读取设备状态参数,对设备进行控制,保证设备处于断电前的状态
OTA升级 接收云端推送升级固件,并更新运行
产测 扫描产测SSID并判断信号强度是否满足要求,进行对应的指示

功能实现

在了解 Demo 代码是如何实现设备管控功能前,您需要了解 SDK 的初始化流程:

通用 Wi-Fi SDK Demo

整个流程中,应用相关功能的实现,您需要重点关注 4 个函数:

user_main() // 入口函数,通过调用下边三个函数完成 SDK 初始化 pre_device_init() // 查看 SDK 版本,设置打印日志输出 app_init() // 设置 Wi-Fi 模组工作模式,注册产测回调函数 device_init() // 注册功能回调函数,完成硬件相关的初始化
  • 查看 SDK 版本

    //打印SDK相关信息、固件名称和版本、设置日志打印等级 VOID pre_device_init(VOID) { OPERATE_RET op_ret = OPRT_OK; PR_DEBUG("%s",tuya_iot_get_sdk_info()); PR_DEBUG("%s:%s",APP_BIN_NAME,DEV_SW_VERSION); PR_NOTICE("firmware compiled at %s %s", __DATE__, __TIME__); SetLogManageAttr(TY_LOG_LEVEL_INFO); }
  • 设置Wi-Fi模组工作的模式

    //设置Wi-Fi模组工作的模式 VOID app_init(VOID) { app_cfg_set(GWCM_LOW_POWER,prod_test); }
  • 完成硬件相关的初始化,注册功能回调函数

    //完成硬件相关的初始化,注册功能回调函数 OPERATE_RET device_init(VOID) { OPERATE_RET op_ret = OPRT_OK; //功能代码相关的回调函数     TY_IOT_CBS_S wf_cbs = {         status_changed_cb,\         gw_ug_inform_cb,\         gw_reset_cb,\         dev_obj_dp_cb,\         dev_raw_dp_cb,\         dev_dp_query_cb,\         NULL, }; //IoT框架的初始化     op_ret = tuya_iot_wf_soc_dev_init_param(func_select.gwcm_mode_user, WF_START_SMART_FIRST, &wf_cbs, FIRMWAIRE_KEY, PRODUCT_KEY, DEV_SW_VERSION);     if(OPRT_OK != op_ret) {         PR_ERR("tuya_iot_wf_soc_dev_init err:%d",op_ret);         return op_ret; } //硬件相关的初始化     op_ret = init_hw(&g_hw_table);     if(OPRT_OK != op_ret) {         PR_ERR("init_hw err:%d",op_ret);         return op_ret;     }     //查询Wi-Fi模组的状态回调函数     op_ret = tuya_iot_reg_get_wf_nw_stat_cb(__get_wf_status);     if(OPRT_OK != op_ret) {     PR_ERR("tuya_iot_reg_get_wf_nw_stat_cb err:%d",op_ret);         return op_ret;         }     //状态保存的回调函数,倒计时的回调函数,Wi-Fi指示状态的回调函数     op_ret = differ_init();     if(OPRT_OK != op_ret) {         PR_ERR("differ_init err:%d",op_ret);         return op_ret; } //内存状态查询回调函数,用于判断程序内存是否正常     op_ret = sys_add_timer(memory_left_timer_cb, NULL, &func_select.memory_left_timer);     if(OPRT_OK != op_ret) {         return op_ret;     }else {         sys_start_timer(func_select.memory_left_timer,2*1000,TIMER_CYCLE);     }     INT_T size = tuya_hal_system_getheapsize();     PR_DEBUG("device_init ok  free_mem_size:%d",size);     return OPRT_OK; } //倒计时、断电记忆等功能的实现 STATIC OPERATE_RET differ_init(VOID) { OPERATE_RET op_ret = OPRT_OK; if(func_select.is_save_stat) { PR_DEBUG("is_save_stat = %d",func_select.is_save_stat); read_saved_stat(); op_ret = sys_add_timer(save_stat_timer_cb, NULL, &func_select.save_stat_timer); if(OPRT_OK != op_ret) { return op_ret; } } if(func_select.is_count_down) { op_ret = sys_add_timer(cd_timer_cb, NULL, &func_select.cd_timer); if(OPRT_OK != op_ret) { return op_ret; }else { PR_NOTICE("cd_timer ID:%d",func_select.cd_timer); sys_start_timer(func_select.cd_timer, 1000, TIMER_CYCLE); } } op_ret = sys_add_timer(wf_timer_cb,NULL, &func_select.wf_timer); if(OPRT_OK != op_ret) { PR_ERR("sys_add_timer failed! op_ret:%d", op_ret); return op_ret; } return OPRT_OK; }

后续章节是对功能相关的代码介绍,部分函数对应多个功能实现,所以在对应功能内展示相关部分代码,其他部分代码使用 ***** 替代。

本地控制

//在硬件初始化时调用处理函数 HW_TABLE g_hw_table =  {     .wifi_stat_led.io_cfg = {.type = IO_DRIVE_LEVEL_LOW, .pin = TY_GPIOA_0},     .rst_button = {TY_GPIOA_5,TRUE,LP_ONCE_TRIG,KEY_RST_TIME,50,key_process},     .channels = channels }; VOID key_process(IN TY_GPIO_PORT_E port, IN PUSH_KEY_TYPE_E type, IN INT_T cnt) { ******************* else if(NORMAL_KEY == type) { if(g_hw_table.rst_button.port== port) { BOOL_T is_every_active = hw_get_all_channel_stat(); hw_trig_all_channel(is_every_active); if(func_select.is_save_stat) { sys_start_timer(func_select.save_stat_timer, 5000, TIMER_ONCE); } op_ret = upload_all_dp_stat(); if(OPRT_OK != op_ret) { PR_ERR("upload_channel_stat op_ret:%d",op_ret); return op_ret; } } } }
// 继电器控制引脚配置 STATIC CTRL_CHANNEL channels[] =  {     {         .relay  = {.type = IO_DRIVE_LEVEL_HIGH, .pin = TY_GPIOA_14},         .button = {TY_GPIOA_5,TRUE,LP_INVALID,0,50,NULL},         .led.io_cfg = {.type = IO_DRIVE_LEVEL_NOT_EXIST },         .dpid   = DP_SWITCH,         .cd_dpid = DP_COUNT_DOWN,         .prtest_swti1_count = 0     } }
//指示灯控制引脚配置 STATIC OPERATE_RET led_pin_reg(INOUT OUT_PUT_S *output) {     OPERATE_RET op_ret;     if(NULL == output) {         PR_ERR("NULL pointer");         return OPRT_INVALID_PARM;     }     if(!IS_IO_TYPE_ALL_PERIPH(output->io_cfg.type)) {         PR_ERR("IO type not define");         return OPRT_INVALID_PARM;     }          if(output->io_cfg.type != IO_DRIVE_LEVEL_NOT_EXIST) {        op_ret = tuya_create_led_handle(output->io_cfg.pin,\                                       IO_ACTIVE_TYPE(output->io_cfg.type),\                                       &(output->io_handle));        if(OPRT_OK != op_ret) {             PR_ERR("op_ret =%d",op_ret);             return op_ret;        }     }     return OPRT_OK; }

进入配网状态

涂鸦模组 SDK 支持实现 Wi-Fi 快连配网和热点配网两种配网方式,对应涂鸦智能 App 中的 EZ模式AP模式

此 Demo 通过控制按键长按控制设备进入到不同的配网状态。g_hw_table 中注册按键处理函数 key_process,判断按键长按,调用 tuya_iot_wf_gw_unactive 控制设备进入配网状态。配网状态的切换形式是循环机制:

  • 设备当前处于非配网状态时,调用 tuya_iot_wf_gw_unactive 会进入到 Wi-Fi 快连配网状态。
  • 如果设备在 Wi-Fi 快连配网状态,调用 tuya_iot_wf_gw_unactive 会进入到热点配网状态。
HW_TABLE g_hw_table =  {     .wifi_stat_led.io_cfg = {.type = IO_DRIVE_LEVEL_LOW, .pin = TY_GPIOA_0},     .rst_button = {TY_GPIOA_5,TRUE,LP_ONCE_TRIG,KEY_RST_TIME,50,key_process},     .channels = channels }; VOID key_process(IN TY_GPIO_PORT_E port, IN PUSH_KEY_TYPE_E type, IN INT_T cnt) { ******************* if(LONG_KEY == type) { if(g_hw_table.rst_button.port== port) { PR_NOTICE("LONG PRESS KEY!"); op_ret = tuya_iot_wf_gw_unactive(); if(OPRT_OK != op_ret) { PR_ERR("tuya_iot_wf_gw_unactive op_ret:%d",op_ret); return; } } } ******************* }

查询网络状态

当网络状态变化时,device_init 函数中的 tuya_iot_reg_get_wf_nw_stat_cb 回调函数调用 __get_wf_status,通过hw_set_wifi_led_stat 对查询设备的当前状态。

OPERATE_RET device_init(VOID) { ***************************** op_ret = tuya_iot_reg_get_wf_nw_stat_cb(__get_wf_status); if(OPRT_OK != op_ret) { PR_ERR("tuya_iot_reg_get_wf_nw_stat_cb err:%d",op_ret); return op_ret; } ***************************** } STATIC VOID __get_wf_status(IN CONST GW_WIFI_NW_STAT_E stat) { OPERATE_RET op_ret = OPRT_OK; func_select.sys_wifi_stat = stat; hw_set_wifi_led_stat(stat); STATIC BOOL_T proc_flag = FALSE; if((STAT_STA_CONN <= stat)&&(proc_flag == FALSE)) { op_ret = upload_all_dp_stat(); if(OPRT_OK != op_ret) { PR_ERR("upload_all_dp_stat op_ret:%d",op_ret); return; } proc_flag = TRUE; } return; }

App 配网

根据设备当前的状态指示,App 选择对应的配网方式进行配网,此部分代码 SDK 内已实现,无需您再开发。

注意:配网成功后,设备会调用 Start_boot_up 重启一次,将所有的 DP 当前的状态上报到云端,用于 App 控制面板显示。

// 设备连接到云端时,会调用 Start_boot_up 重启一次将设备当前所有 DP 的状态进行上报,用于 App 面板显示 VOID Start_boot_up(VOID) { OPERATE_RET op_ret = OPRT_OK; op_ret = upload_all_dp_stat(); if(OPRT_OK != op_ret) { PR_ERR("upload_channel_stat op_ret:%d",op_ret); return; } } OPERATE_RET upload_all_dp_stat(VOID) { OPERATE_RET op_ret = OPRT_OK; INT_T count_sec = 0; INT_T ch_idx = 0, dp_idx = 0; INT_T dp_cnt = 0; if(func_select.is_count_down) { dp_cnt = g_hw_table.channel_num*2; }else { dp_cnt = g_hw_table.channel_num; } TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(dp_cnt*SIZEOF(TY_OBJ_DP_S)); if(NULL == dp_arr) { PR_ERR("malloc failed"); return OPRT_MALLOC_FAILED; } memset(dp_arr, 0, dp_cnt*SIZEOF(TY_OBJ_DP_S)); for(ch_idx = 0,dp_idx = 0; (ch_idx < g_hw_table.channel_num)&&(dp_idx < dp_cnt); ch_idx++,dp_idx++) { dp_arr[dp_idx].dpid = get_channel_dpid(ch_idx); dp_arr[dp_idx].type = PROP_BOOL; dp_arr[dp_idx].time_stamp = 0; dp_arr[dp_idx].value.dp_bool = get_channel_stat(ch_idx); if(func_select.is_count_down) { dp_idx++; dp_arr[dp_idx].dpid = get_channel_cddpid(ch_idx); dp_arr[dp_idx].type = PROP_VALUE; dp_arr[dp_idx].time_stamp = 0; if(g_hw_table.channels[ch_idx].cd_sec >= 0) { dp_arr[dp_idx].value.dp_value = g_hw_table.channels[ch_idx].cd_sec;//get_cd_remain_sec(ch_idx); }else { dp_arr[dp_idx].value.dp_value = 0; } } } op_ret = dev_report_dp_json_async(get_gw_cntl()->gw_if.id,dp_arr,dp_cnt); Free(dp_arr); dp_arr = NULL; if(OPRT_OK != op_ret) { PR_ERR("dev_report_dp_json_async op_ret:%d",op_ret); return op_ret; } return OPRT_OK; }

App 控制

device_init 函数中,注册了 DP 数据的处理回调函数 dev_obj_dp_cb。当 App 接收到数据时,dev_obj_dp_cb 调用 deal_dps_proc 对接收到的 DP 数据处理,并执行相关硬件,然后调用 dev_report_dp_json_async 将状态返回给 App。

OPERATE_RET device_init(VOID) { *************************** TY_IOT_CBS_S wf_cbs = { status_changed_cb,\ gw_ug_inform_cb,\ gw_reset_cb,\ dev_obj_dp_cb,\ dev_raw_dp_cb,\ dev_dp_query_cb,\ NULL, }; **************************** } VOID dev_obj_dp_cb(IN CONST TY_RECV_OBJ_DP_S *dp) { PR_DEBUG("dp->cid:%s dp->dps_cnt:%d",dp->cid,dp->dps_cnt); deal_dps_proc(dp); } STATIC VOID deal_dps_proc(IN CONST TY_RECV_OBJ_DP_S *dp) { OPERATE_RET op_ret = OPRT_OK; INT_T i = 0,ch_index; for(i = 0;i < dp->dps_cnt;i++) { PR_DEBUG("dpid:%d type:%d time_stamp:%d",dp->dps[i].dpid,dp->dps[i].type,dp->dps[i].time_stamp); switch(dp->dps[i].dpid) { case DP_SWITCH: { ch_index = hw_set_channel_by_dpid(dp->dps[i].dpid, dp->dps[i].value.dp_bool); if( ch_index >= 0) { if((func_select.is_count_down)&&(g_hw_table.channels[ch_index].cd_sec >= 0)) { g_hw_table.channels[ch_index].cd_sec = -1; } op_ret = upload_channel_stat(ch_index); if(OPRT_OK != op_ret) { PR_ERR("upload_channel_stat op_ret:%d",op_ret); break; } } if(func_select.is_save_stat) { sys_start_timer(func_select.save_stat_timer, 5000, TIMER_ONCE); } } break; case DP_COUNT_DOWN: { ch_index = hw_find_channel_by_cd_dpid(dp->dps[i].dpid); if(ch_index >= 0) { if(dp->dps[i].value.dp_value == 0) { g_hw_table.channels[ch_index].cd_sec = -1; }else { g_hw_table.channels[ch_index].cd_sec = dp->dps[i].value.dp_value; } op_ret = upload_channel_stat(ch_index); if(OPRT_OK != op_ret) { PR_ERR("upload_channel_stat op_ret:%d",op_ret); break; } } } break; default: break; } } } OPERATE_RET upload_channel_stat(IN UINT_T ch_idx) { OPERATE_RET op_ret = OPRT_OK; INT_T count_sec = 0; INT_T dp_idx = 0; INT_T dp_cnt = 0; dp_cnt = (func_select.is_count_down)?2:1; TY_OBJ_DP_S *dp_arr = (TY_OBJ_DP_S *)Malloc(dp_cnt*SIZEOF(TY_OBJ_DP_S)); if(NULL == dp_arr) { PR_ERR("malloc failed"); return OPRT_MALLOC_FAILED; } memset(dp_arr, 0, dp_cnt*SIZEOF(TY_OBJ_DP_S)); dp_arr[dp_idx].dpid = get_channel_dpid(ch_idx); dp_arr[dp_idx].type = PROP_BOOL; dp_arr[dp_idx].time_stamp = 0; dp_arr[dp_idx].value.dp_bool = get_channel_stat(ch_idx); dp_idx ++; if(func_select.is_count_down) { dp_arr[dp_idx].dpid = get_channel_cddpid(ch_idx); dp_arr[dp_idx].type = PROP_VALUE; dp_arr[dp_idx].time_stamp = 0; if(g_hw_table.channels[ch_idx].cd_sec >= 0) { dp_arr[dp_idx].value.dp_value = g_hw_table.channels[ch_idx].cd_sec;//get_cd_remain_sec(ch_idx); }else { dp_arr[dp_idx].value.dp_value = 0; } dp_idx ++; } if(dp_idx != dp_cnt) { PR_ERR("dp_idx:%d,dp_cnt:%d",dp_idx,dp_cnt); Free(dp_arr); dp_arr = NULL; return OPRT_COM_ERROR; } op_ret = dev_report_dp_json_async(get_gw_cntl()->gw_if.id,dp_arr,dp_cnt); Free(dp_arr); dp_arr = NULL; if(OPRT_OK != op_ret) { PR_ERR("dev_report_dp_json_async op_ret:%d",op_ret); return op_ret; } return OPRT_OK; } //App 查询设备状态 VOID dev_dp_query_cb(IN CONST TY_DP_QUERY_S *dp_qry) { PR_DEBUG("Recv DP Query Cmd"); OPERATE_RET op_ret = OPRT_OK; op_ret = upload_all_dp_stat(); if(OPRT_OK != op_ret) { PR_ERR("upload_all_dp_stat op_ret:%d",op_ret); return; } }

App 移除

在 App 移除设备时,设备接收到移除命令后,调用 gw_reset_cb 重置设备并进入配网状态。

VOID gw_reset_cb(IN CONST GW_RESET_TYPE_E type) { OPERATE_RET op_ret = OPRT_OK; if(GW_REMOTE_RESET_FACTORY != type) { return; } if(func_select.is_save_stat) { // 恢复出厂设置的上电能力默认关闭 op_ret = wd_common_fuzzy_delete(STORE_OFF_MOD); if(OPRT_OK != op_ret) { PR_ERR("clear power_stat op_ret:%d",op_ret); } } return; }

语音控制

语音控制与 App 控制实现原理一样,详细请参考 App 配网App 控制App 移除

注意:您需要在平台增值服务开通语音功能后,才能使用音响对设备进行控制,具体参考 第三方音响接入服务

倒计时开关

func_select 中使能倒计时功能,在 differ_init 中判断倒计时功能为真,启动软件定时器注册 cd_timer_cb 回调函数,每隔 30 秒更新一次倒计时,倒计时结束后执行相应动作。

STATIC OPERATE_RET differ_init(VOID) { OPERATE_RET op_ret = OPRT_OK; ******************************* if(func_select.is_count_down) { op_ret = sys_add_timer(cd_timer_cb, NULL, &func_select.cd_timer); if(OPRT_OK != op_ret) { return op_ret; }else { PR_NOTICE("cd_timer ID:%d",func_select.cd_timer); sys_start_timer(func_select.cd_timer, 1000, TIMER_CYCLE); } } op_ret = sys_add_timer(wf_timer_cb,NULL, &func_select.wf_timer); if(OPRT_OK != op_ret) { PR_ERR("sys_add_timer failed! op_ret:%d", op_ret); return op_ret; } return OPRT_OK; } FUNC_SELECT func_select = { .is_save_stat = FALSE, .is_count_down = TRUE, .gwcm_mode_user = GWCM_SPCL_MODE, .cd_upload_period = 30, .prod_flag = FALSE }; STATIC VOID cd_timer_cb(IN UINT_T timerID, IN PVOID pTimerArg) { UCHAR_T i = 0; OPERATE_RET op_ret = OPRT_OK; for(i = 0; i<g_hw_table.channel_num; i++) { if(g_hw_table.channels[i].cd_sec < 0) { continue; }else { --g_hw_table.channels[i].cd_sec; if(g_hw_table.channels[i].cd_sec <= 0) { g_hw_table.channels[i].cd_sec = -1; hw_trig_channel(i); op_ret = upload_channel_stat(i); if(OPRT_OK != op_ret) { PR_ERR("upload_channel_stat op_ret:%d",op_ret); return ; } }else { if(g_hw_table.channels[i].cd_sec % 30 == 0) { op_ret = upload_channel_stat(i); if(OPRT_OK != op_ret) { PR_ERR("upload_channel_stat op_ret:%d",op_ret); return; } } } } } }

断电记忆

func_select 中使能断电记忆功能,在 differ_init 中判断断电记忆功能为真,启动软件定时器注册 save_stat_timer_cb 回调函数,每隔 5 秒调用 wd_common_write 将当前状态写入到驱动(Flash)中。

当再次上电时,调用 read_saved_stat 从 Flash 中读取当前的状态,控制进入到此状态,实现断电记忆,并发送给 App 显示。

#define STORE_OFF_MOD "save_off_stat" #define STORE_CHANGE "init_stat_save" FUNC_SELECT func_select = { .is_save_stat = FALSE, .is_count_down = TRUE, .gwcm_mode_user = GWCM_SPCL_MODE, .cd_upload_period = 30, .prod_flag = FALSE }; STATIC VOID save_stat_timer_cb(IN UINT_T timerID,IN PVOID pTimerArg) { OPERATE_RET op_ret = OPRT_OK; INT_T i; PR_DEBUG("save stat"); IN CONST BYTE_T buff[48] = "{\"power\":["; for(i=0; i<g_hw_table.channel_num; ++i) { if(g_hw_table.channels[i].stat) { strcat(buff, "true"); } else if(!g_hw_table.channels[i].stat) { strcat(buff, "false"); } if(i < g_hw_table.channel_num -1) { strcat(buff, ","); }else { strcat(buff, "]}"); } } PR_DEBUG("%s", buff); op_ret = wd_common_write(STORE_OFF_MOD,buff,strlen(buff)); if(OPRT_OK != op_ret) { PR_DEBUG("kvs_write err:%02x",op_ret); } } VOID read_saved_stat(VOID) { PR_DEBUG("_______________SAVE________________"); OPERATE_RET op_ret = OPRT_OK; cJSON *root = NULL, *js_power = NULL, *js_ch_stat = NULL; UINT_T buff_len = 0; UCHAR_T *buff = NULL; INT_T i; op_ret = wd_common_read(STORE_OFF_MOD,&buff,&buff_len); if(OPRT_OK != op_ret) { PR_DEBUG("msf_get_single err:%02x",op_ret); return; } PR_DEBUG("read stat: %s", buff); root = cJSON_Parse(buff); Free(buff); if(NULL == root) { PR_ERR("cjson parse err"); return; } js_power = cJSON_GetObjectItem(root, "power"); if(NULL == js_power) { PR_ERR("cjson get power error"); goto JSON_PARSE_ERR; } UCHAR_T count = cJSON_GetArraySize(js_power); if(count != g_hw_table.channel_num) { for(i = 0;i< g_hw_table.channel_num; ++i) { hw_set_channel(i, FALSE); } return; } for(i=0; i< g_hw_table.channel_num; ++i) { js_ch_stat = cJSON_GetArrayItem(js_power, i); if(js_ch_stat == NULL) { PR_ERR("cjson %d ch stat not found", i); goto JSON_PARSE_ERR; }else { if(js_ch_stat->type == cJSON_True) { hw_set_channel(i, TRUE); }else { hw_set_channel(i, FALSE); } } } JSON_PARSE_ERR: cJSON_Delete(root); root = NULL; }

OTA 升级

在涂鸦开发者平台配置升级,设备收到升级推送后调用 gw_ug_inform_cb 回调函数进行固件更新并运行

VOID gw_ug_inform_cb(IN CONST FW_UG_S *fw) { PR_DEBUG("Rev GW Upgrade Info"); PR_DEBUG("fw->fw_url:%s", fw->fw_url); //PR_DEBUG("fw->fw_md5:%s", fw->fw_md5); PR_DEBUG("fw->sw_ver:%s", fw->sw_ver); PR_DEBUG("fw->file_size:%d", fw->file_size); tuya_iot_upgrade_gw(fw, get_file_data_cb, upgrade_notify_cb, NULL); } OPERATE_RET get_file_data_cb(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 pri_data) { PR_DEBUG("Rev File Data"); PR_DEBUG("Total_len:%d ", total_len); PR_DEBUG("Offset:%d Len:%d", offset, len); return OPRT_OK; } VOID upgrade_notify_cb(IN CONST FW_UG_S *fw, IN CONST INT_T download_result, IN PVOID pri_data) { PR_DEBUG("download Finish"); PR_DEBUG("download_result:%d", download_result); }

产测

app_init 中通过 app_cfg_set 注册产测函数,扫描在 tuya_main.c 中定义的 TEST_SSID,判断信号强度。

  • 如果信号强度大于 -60dB,认为设备信号正常,启动软件定时器,调用 protest_switch_timer_cb 回调函数,进行功能测试。

  • 如果信号强度小于 -60dB,认为设备信号异常,做出相应指示,引导用户进行问题排查。

    #define TEST_SSID "tuya_mdev_test1" VOID app_init(VOID) { app_cfg_set(GWCM_LOW_POWER,prod_test); } STATIC VOID prod_test(IN BOOL_T flag, IN CHAR_T rssi) { OPERATE_RET op_ret = OPRT_OK; PR_DEBUG("dev_test_start_cb"); PR_DEBUG("rssi:%d", rssi); if((rssi<-60) ||(flag == FALSE)) { PR_ERR("rssi:%d,flag:%d",rssi,flag); return; } op_ret = sys_add_timer(protest_switch_timer_cb,NULL,&g_hw_table.switch_timer); if(OPRT_OK != op_ret) { PR_ERR("sys_add_timer switch_timer err"); return ; } op_ret = prod_test_init_hw(&g_hw_table); if(OPRT_OK != op_ret) { PR_ERR("prod_test_init_hw err"); return ; } func_select.prod_flag = TRUE; } STATIC VOID protest_switch_timer_cb(IN UINT timerID, IN PVOID pTimerArg) { STATIC UCHAR_T act_cnt = 0; STATIC UCHAR_T target_ch = 0; PR_DEBUG("all relay timer callback"); if( target_ch < get_all_ch_num()) { hw_trig_channel(target_ch); act_cnt++; } if(target_ch >= get_all_ch_num()) { act_cnt = 0; target_ch = 0; }else if(act_cnt < 6) { sys_start_timer(g_hw_table.switch_timer, RELAY_ACT_INTER, TIMER_ONCE); }else{ act_cnt = 0; target_ch++; sys_start_timer(g_hw_table.switch_timer, RELAY_ACT_INTER, TIMER_ONCE); } }

在开发过程中,您应该加入设备状态的监控函数和打印信息,以便及时发现异常,解决相关问题。Demo 代码中通过启动软件定时器,调用 memory_left_timer_cb 回调函数,监控内存信息,确认设备运行正常。

OPERATE_RET device_init(VOID) { ************************************** op_ret = sys_add_timer(memory_left_timer_cb, NULL, &func_select.memory_left_timer); if(OPRT_OK != op_ret) { return op_ret; }else { sys_start_timer(func_select.memory_left_timer,2*1000,TIMER_CYCLE); } INT_T size = tuya_hal_system_getheapsize(); PR_DEBUG("device_init ok free_mem_size:%d",size); return OPRT_OK; }

固件编译

SDK 内提供编译脚本,您可以通过如下命令调用脚本可实现代码编译。

  • 编译指令:进入到编译脚本所在目录,执行 ./build_app.sh apps/<项目名称> <项目名称> <版本号>

  • 命令示例:

    ./build_app.sh apps/one_plug_demo one_plug_demo 1.0.0

编译成功后会在 apps/项目名称/output/版本号/xxx.bin 目录下生成烧录用的二进制(bin)文件,如下图片所示:

通用 Wi-Fi SDK Demo

编译出的 bin 文件,名称中含 QIO 和 DOUT 尾缀的是生产固件,需要根据自己用的 Flash 的工作模式进行选择,生产时使用,名称中含 _(1)_(2) 的是用户区固件,调试阶段使用,名称中含 _ug 的是升级固件,OTA时使用。

文件名 说明
one_plug_demo_(1)_1.0.0.bin 用户区固件,用于芯片烧录工具使用
one_plug_demo_(2)_1.0.0.bin 用户区固件,用于芯片烧录工具使用
one_plug_demo_DOUT_1.0.0.bin 生产固件,用于模组Flash工作方式为DOUT的模组
one_plug_demo_QIO_1.0.0.bin 生产固件,用于模组Flash工作方式为QIO的模组
one_plug_demo_ug_1.0.0.bin 升级固件,用于上传涂鸦开发者平台用户区固件和 OTA 固件

烧录授权

将固件上传至涂鸦开发者平台,申请激活码,通过涂鸦 云模组烧录授权平台 写入授权信息,详情参考 通用 Wi-Fi 烧录、授权和产测

日志等级

在调试过程中可以通过添加打印信息,监控设备的运行状态,调试代码。

涂鸦通用 SDK 的日志等级有以下几种分类:

  • Notice
  • Warning
  • Err
  • Debug

您可以通过设置日志的等级来决定哪些日志信息输出,例如设置日志输出等级为TY_LOG_LEVEL_INFO,则ERRWARNNOTICEINFO相关信息会显示,DEBUGTRACE则不会显示。

/* tuya sdk definition of log level */ typedef INT_T LOG_LEVEL; #define TY_LOG_LEVEL_ERR       0  // 错误信息,程序正常运行不应发生的信息  #define TY_LOG_LEVEL_WARN      1  // 警告信息 #define TY_LOG_LEVEL_NOTICE    2  // 需要注意的信息 #define TY_LOG_LEVEL_INFO      3  // 通知信息 #define TY_LOG_LEVEL_DEBUG     4  // 程序运行调试信息,RELEASE版本中删除 #define TY_LOG_LEVEL_TRACE     5  // 程序运行路径信息,RELEASE版本中删除