Skip to content

App 点灯

概述

在上一章中,已经成功连接到 Tuya 平台,并且在 App 中添加了一个设备。但是那支持最简单的配网实现,没有错误、配网状态打印、启动配网等功能。
因此,上述功能会在本章当中统一添加,并介绍如何通过 “智能生活App” 实现控制 T2-U 开发板上的 LED 灯。

注意

本章内容较多,建议先完成配网功能的完善,再进行后续操作。

完善配网功能

状态处理

TuyaOS 的 Wi-Fi 单品免不了几个状态:

状态列表

  • 联网成功:联网成功之后,需要发起MQTT 连接到服务器
  • 网络断开: 当网络断开时,需要关闭MQTT 连接
  • MQTT 连接状态: 当MQTT 连接状态发生改变时,需要打印当前状态

这三个状态需要开发者自行订阅,并在回调函数中处理。可参考以下示例:

注意:在开始订阅事件时,先引用 tuya_svc_netmgr.hmqc_app.h 头文件,不同的事件可以使用一个回调函数进行处理。

C
ty_subscribe_event(EVENT_LINK_UP, "quickstart", __soc_dev_net_status_cb, SUBSCRIBE_TYPE_NORMAL);// 联网成功
ty_subscribe_event(EVENT_LINK_DOWN, "quickstart", __soc_dev_net_status_cb, SUBSCRIBE_TYPE_NORMAL);// 网络断开
ty_subscribe_event(EVENT_MQTT_CONNECTED, "quickstart", __soc_dev_net_status_cb, SUBSCRIBE_TYPE_NORMAL);// MQTT 连接状态
C
STATIC OPERATE_RET __soc_dev_net_status_cb(VOID *data)
{
    STATIC BOOL_T s_syn_all_status = FALSE;

    TAL_PR_DEBUG("network status changed!");
    if (tuya_svc_netmgr_linkage_is_up(LINKAGE_TYPE_DEFAULT))
    {
        TAL_PR_DEBUG("linkage status changed, current status is up");
        if (get_mqc_conn_stat())
        {
            TAL_PR_DEBUG("mqtt is connected!");

            if (FALSE == s_syn_all_status)
            {
                //upload_device_all_status(); // 没有定义该函数,先保留,后续再添加
                s_syn_all_status = TRUE;
            }
            // UserTODO
        }
    }
    else
    {
        TAL_PR_DEBUG("linkage status changed, current status is down");

        // UserTODO
    }

    return OPRT_OK;
}

日志打印

上一章当中,我们在对设备进行 soc 初始化时,只是创建了 TY_IOT_CBS_S iot_cbs 回调结构体,但是并没有定义它的回调函数。
因此,在这一章当中,我们会完善配网功能,添加日志打印功能。

TY_IOT_CBS_S 结构体定义如下:
C
typedef struct {
    /** status update */
    GW_STATUS_CHANGED_CB gw_status_cb; // 设备激活的状态发生改变
    /** gateway upgrade */
    GW_UG_INFORM_CB gw_ug_cb; // 设备上线回调函数
    /** gateway reset */
    GW_RESET_IFM_CB gw_reset_cb; // 设备重置回调函数
    /** structured DP info */
    DEV_OBJ_DP_CMD_CB dev_obj_dp_cb; //  obj 类型 DP 指令下发
    /** raw DP info */
    DEV_RAW_DP_CMD_CB dev_raw_dp_cb; // 有 raw 类型 DP 指令下发
    /** DP query */
    DEV_DP_QUERY_CB dev_dp_query_cb; // 需要查询指定 DP 当前的状态
    /** sub-device upgrade */
    DEV_UG_INFORM_CB dev_ug_cb; // 所属的子设备开始进入升级流程,并告知开发者拉取升级数据时所需的 URL 等必要信息。
    /** sub-device reset */
    DEV_RESET_IFM_CB dev_reset_cb; // 设备所属的子设备被重置了(单品类设备无需关心)
    /** active short url */
    ACTIVE_SHORTURL_CB active_shorturl; // 支持扫码激活
    /** gateway upgrade pre-condition */
    GW_UG_INFORM_CB pre_gw_ug_cb; // 网关升级前置条件回调函数
    /** sub-device upgrade pre-condition */
    DEV_UG_INFORM_CB pre_dev_ug_cb; // 通知开发者,设备所属的子设备有升级请求,开发者通过返回值告诉开发框架当前是否允许升级。
} TY_IOT_CBS_S;

在上一章当中,即使不定义回调函数,程序也可以正常运行。因此,我们只需要定义几个必要的回调函数即可。它们分别是:

回调函数列表

  • gw_status_cb:设备激活的状态发生改变时,打印当前状态;
  • gw_ug_cb:设备上线回调函数,打印上线成功日志;
  • gw_reset_cb:设备重置回调函数,打印重置成功日志;
  • dev_obj_dp_cb:obj 类型 DP 指令下发回调函数,打印指令内容;
  • dev_raw_dp_cb:raw 类型 DP 指令下发回调函数,打印指令内容;
  • dev_dp_query_cb:需要查询指定 DP 当前的状态回调函数,打印查询结果。

代码如下:

C
TY_IOT_CBS_S iot_cbs = {
  .gw_status_cb = __soc_dev_status_changed_cb,
  .gw_ug_cb = __soc_dev_rev_upgrade_info_cb,
  .gw_reset_cb = __soc_dev_reset_inform_cb,
  .dev_obj_dp_cb = __soc_dev_obj_dp_cmd_cb,
  .dev_raw_dp_cb = __soc_dev_raw_dp_cmd_cb,
  .dev_dp_query_cb = __soc_dev_dp_query_cb,
};
C
STATIC VOID_T __soc_dev_status_changed_cb(IN CONST GW_STATUS_E status)
{
    TAL_PR_DEBUG("SOC TUYA-Cloud Status:%d", status);
    return;
}
C
STATIC OPERATE_RET __soc_dev_rev_upgrade_info_cb(IN CONST FW_UG_S *fw)
{
    TAL_PR_DEBUG("SOC Rev Upgrade Info");
    TAL_PR_DEBUG("fw->tp:%d", fw->tp);
    TAL_PR_DEBUG("fw->fw_url:%s", fw->fw_url);
    TAL_PR_DEBUG("fw->fw_hmac:%s", fw->fw_hmac);
    TAL_PR_DEBUG("fw->sw_ver:%s", fw->sw_ver);
    TAL_PR_DEBUG("fw->file_size:%u", fw->file_size);

    return OPRT_OK;
}
C
STATIC VOID_T __soc_dev_reset_inform_cb(GW_RESET_TYPE_E type)
{
    TAL_PR_DEBUG("reset type %d", type);

    return;
}
C
STATIC VOID_T __soc_dev_obj_dp_cmd_cb(IN CONST TY_RECV_OBJ_DP_S *dp)
{

    TAL_PR_DEBUG("SOC Rev DP Obj Cmd t1:%d t2:%d CNT:%u", dp->cmd_tp, dp->dtt_tp, dp->dps_cnt);
    return;
}
C
STATIC VOID_T __soc_dev_raw_dp_cmd_cb(IN CONST TY_RECV_RAW_DP_S *dp)
{
    TAL_PR_DEBUG("SOC Rev DP Raw Cmd t1:%d t2:%d dpid:%d len:%u", dp->cmd_tp, dp->dtt_tp, dp->dpid, dp->len);

    return;
}
C
STATIC VOID_T __soc_dev_dp_query_cb(IN CONST TY_DP_QUERY_S *dp_qry)
{
    UINT32_T index = 0;

    TAL_PR_DEBUG("SOC Rev DP Query Cmd");
    if (dp_qry->cid != NULL)
    {
        TAL_PR_ERR("soc not have cid.%s", dp_qry->cid);
    }

    if (dp_qry->cnt == 0)
    {
        TAL_PR_DEBUG("soc rev all dp query");
    }
    else
    {
        TAL_PR_DEBUG("soc rev dp query cnt:%d", dp_qry->cnt);
        for (index = 0; index < dp_qry->cnt; index++)
        {
            TAL_PR_DEBUG("rev dp query:%d", dp_qry->dpid[index]);
            // UserTODO
        }
    }

    return;
}

烧录验证

修改完代码后,需要重新烧录程序到开发板上。打开串口监视,成功日志如下:

重置配网

当前代码,设备如果已经配置了 Wi-Fi ,会自动把 Wi-Fi 连接信息保存起来,等待下次上电时自动连接。如果没有重置配网操作的话,设备会保持之前的 Wi-Fi 连接,无法进入配网模式,这时候就需要通过重置配网操作来重新配置 Wi-Fi 连接。
通对于有按键的设备,有以下操作进入配网模式:

配网操作

  • 长按配网: 长按设备上的配网按键,直到指示灯闪烁,说明设备进入配网模式。
  • 短按数次配网: 短按设备上的配网按键,达到配网次数之后,进入配网模式。
  • 复位次数配网: 通过复位的次数重置配网,从而进入配网模式。

没有按键的设备

如果设备没有按键,通常是重启次数配网,即让设备上电/断电几次,直到指示灯闪烁,说明设备进入配网模式。

T2-U 开发板上,正好有一个按键连接到了 P7 引脚,我们可以通过长按该按键来进入配网模式。

icon
第一步

新建 user_key.cuser_key.h 文件用来开发 P7 按键的驱动代码和逻辑。

无效图片
icon
第二步

user_key.c 文件中实现按键的初始化和扫描任务。查看代码

icon
第三步

user_key.h 文件中定义按键的相关常量和函数接口。查看代码

icon
第四步

tuya_device.cuaer_main 调用 user_key_init(); 即可。

无效图片

示例代码

查看代码
C
/**
 * @file user_key.c
 * @author Seahi-Mo (seahi-mo@foxmail.com)
 * @brief 按键处理模块,实现设备配网和重置功能
 * @version 0.1
 * @date 2026-02-21
 *
 * @copyright SeaHi (c) 2026
 *
 */
#include "tkl_gpio.h"
#include "tal_log.h"
#include "tal_system.h"
#include "tal_thread.h"
#ifdef ENABLE_WIFI_SERVICE
#include "tuya_iot_wifi_api.h"
#else
#include "tuya_iot_base_api.h"
#endif
#include "user_key.h"
/**
 * @brief 按键引脚定义
 */
STATIC TUYA_GPIO_NUM_E user_key_pin_id = USER_KEY_PIN_ID;
/**
 * @brief 按键监控任务函数
 * @param args 任务参数(未使用)
 * @return
 * @note 该函数在独立线程中运行,持续监控按键状态
 *       实现了长按按键检测,长按触发设备重置和配网
 */
STATIC VOID_T app_key_task(VOID_T *args)
{
	OPERATE_RET op_ret = OPRT_OK;						 // 操作返回值
	TUYA_GPIO_LEVEL_E read_level = TUYA_GPIO_LEVEL_HIGH; // 读取的按键电平
	UINT32_T time_start = 0, timer_end = 0;				 // 按键按下开始和结束时间
	// 无限循环监控按键状态
	for (;;)
	{
		// 读取按键当前电平
		tkl_gpio_read(user_key_pin_id, &read_level);
		// 检测到按键按下(低电平)
		if (TUYA_GPIO_LEVEL_LOW == read_level)
		{
			// 3ms延时去抖
			tal_system_sleep(3);
			// 再次读取电平确认
			tkl_gpio_read(user_key_pin_id, &read_level);
			// 如果电平不是低电平,说明是抖动,跳过本次检测
			if (TUYA_GPIO_LEVEL_LOW != read_level)
			{
				continue; // jitter
			}
			// 记录按键按下的开始时间
			time_start = tal_system_get_millisecond();
			// 持续检测按键状态,直到按键释放
			while (TUYA_GPIO_LEVEL_LOW == read_level)
			{
				// 30ms延时,避免频繁读取
				tal_system_sleep(30);
				// 读取当前按键电平
				tkl_gpio_read(user_key_pin_id, &read_level);
				// 记录当前时间
				timer_end = tal_system_get_millisecond();
				// 检查是否达到长按时间阈值
				if (timer_end - time_start >= LONE_PRESS_TIME)
				{
					// 输出长按日志
					TAL_PR_DEBUG("long press, remove device");
					/* 长按状态达成,重置设备,触发配网 */
					// 调用涂鸦IoT接口,重置设备并进入配网模式
					op_ret = tuya_iot_wf_gw_unactive();

					// 检查操作是否成功
					if (op_ret != OPRT_OK)
					{
						// 输出错误日志
						TAL_PR_ERR("long press tuya_iot_wf_gw_unactive error, %d", op_ret);
					}
					// 退出长按检测循环
					break;
				}
			}
		}
		// 100ms延时,降低CPU占用
		tal_system_sleep(100);
	}
	return;
}
/**
 * @brief 按键初始化函数
 * @return VOID_T 无
 * @note 初始化按键GPIO引脚并创建按键监控线程
 */
VOID_T app_key_init(VOID_T)
{
	OPERATE_RET rt = OPRT_OK;
	// 初始化按键引脚
	TUYA_GPIO_BASE_CFG_T key_cfg = {
		.mode = TUYA_GPIO_PULLUP,	  // 上拉模式
		.direct = TUYA_GPIO_INPUT,	  // 输入方向
		.level = TUYA_GPIO_LEVEL_HIGH // 初始高电平
	};
	// 初始化GPIO并记录错误日志
	TUYA_CALL_ERR_LOG(tkl_gpio_init(user_key_pin_id, &key_cfg));
	/* 创建并启动按键监控线程 */
	THREAD_HANDLE key_task_handle; // 线程句柄
	THREAD_CFG_T thread_cfg = {
		.thrdname = "user_key_task", // 线程名称
		.priority = THREAD_PRIO_6,	 // 线程优先级
		.stackDepth = 4096			 // 线程栈大小
	};
	// 创建并启动线程,记录错误日志
	TUYA_CALL_ERR_LOG(tal_thread_create_and_start(&key_task_handle, NULL, NULL, app_key_task, NULL, &thread_cfg));
	return;
}
C
/**
 * @file user_key.h
 * @author Seahi-Mo (seahi-mo@foxmail.com)
 * @brief
 * @version 0.1
 * @date 2026-02-21
 *
 * @copyright SeaHi (c) 2026
 *
 */
#ifndef __USER_KEY_H__
#define __USER_KEY_H__

#define LONE_PRESS_TIME 3000 // long press time, uint: ms
#define USER_KEY_PIN_ID 7  // 按键GPIO引脚号,根据实际硬件连接修改

VOID_T app_key_init(VOID_T);
#endif

LED 网络指示

重置配网完成之后,还有一个问题:T2-U 开发板不看日志的话,无法知道当前是否处于配网模式或已经成功连接,这时候就需要通过 LED 网络指示来判断。
但是又因为我们需要用 App 控制这一盏 LED 灯,因此需要对 LED 灯的状态进行分类处理,可以分为以下三种状态:

LED 状态划分

  • 未连接/配网失败:LED 灯快闪(闪烁间隔 100ms)
  • 配网模式:LED 灯慢闪(闪烁间隔 500ms)
  • 已连接:LED 灯常亮
icon
第一步

新建 net_led.cnet_led.h 文件用来开发 LED 网络指示的驱动代码和逻辑。

无效图片
icon
第二步

net_led.c 文件中实现 LED 网络指示的驱动代码,包括初始化、快闪、慢闪和常亮等功能。查看代码

icon
第三步

net_led.h 文件中声明 LED 网络指示的驱动函数接口,包括初始化,控制亮灭等函数。

icon
第四步

tuya_device.c 文件中调用初始化函数;

示例代码

查看代码
C
/**
 * @file net_led.c
 * @author Seahi-Mo (seahi-mo@foxmail.com)
 * @brief
 * @version 0.1
 * @date 2026-02-21
 *
 * @copyright SeaHi (c) 2026
 *
 */
#include "tal_log.h"
#include "tkl_gpio.h"
#include "tal_thread.h"
#include "net_led.h"
#include "dp_process.h"
#include "tuya_svc_netmgr.h"
#include <gw_intf.h>

STATIC TUYA_GPIO_NUM_E sg_led_pin = NET_LED_GPIO_PORT;
STATIC UINT8_T cur_led_status = NET_LED_OFF;

THREAD_HANDLE blink_task_handle = NULL;

CHAR_T user_wifi_status = 0; // wifi状态

STATIC VOID_T status_display_task(VOID_T *args)
{
	for (;;)
	{
		switch (user_wifi_status)
		{
		case 0: // not connected
			set_led_status(NET_LED_ON);
			tal_system_sleep(50);
			set_led_status(NET_LED_OFF);
			tal_system_sleep(50);
			break;
		case 1: // connecting
			set_led_status(NET_LED_ON);
			tal_system_sleep(500);
			set_led_status(NET_LED_OFF);
			tal_system_sleep(500);
			break;
		case 2: // activated
			set_led_status(NET_LED_OFF);
			tal_system_sleep(1000);
			break;
		default:
			set_led_status(NET_LED_OFF);
			break;
		}
	}
	return;
}
VOID_T net_led_init(void)
{
	OPERATE_RET rt = OPRT_OK;

	TUYA_GPIO_BASE_CFG_T led_cfg = {
		.mode = TUYA_GPIO_PUSH_PULL,
		.direct = TUYA_GPIO_OUTPUT,
		.level = TUYA_GPIO_LEVEL_HIGH};
	TUYA_CALL_ERR_LOG(tkl_gpio_init(sg_led_pin, &led_cfg));
	set_led_status(NET_LED_OFF);

	THREAD_CFG_T blink_thread_cfg = {
		.thrdname = "led_task",
		.priority = THREAD_PRIO_6,
		.stackDepth = 4096};
	TUYA_CALL_ERR_LOG(tal_thread_create_and_start(&blink_task_handle, NULL, NULL, status_display_task, NULL, &blink_thread_cfg));
	return;
}

VOID_T set_led_status(BOOL_T led_status)
{
	if (NET_LED_ON == led_status)
	{
		tkl_gpio_write(sg_led_pin, TUYA_GPIO_LEVEL_HIGH);
	}
	else
	{
		tkl_gpio_write(sg_led_pin, TUYA_GPIO_LEVEL_LOW);
	}

	cur_led_status = led_status;
	return;
}

BOOL_T get_led_status(void)
{
	return cur_led_status;
}
C
/**
 * @file net_led.h
 * @author Seahi-Mo (seahi-mo@foxmail.com)
 * @brief
 * @version 0.1
 * @date 2026-02-21
 *
 * @copyright SeaHi (c) 2026
 *
 */

#ifndef __NET_LED_H__
#define __NET_LED_H__

#define NET_LED_GPIO_PORT 17
#define NET_LED_ON 0
#define NET_LED_OFF 1

extern BOOL_T user_wifi_status; // 0:未连接 1:正在配网或正在连接,2:已连接
VOID_T net_led_init(void);
VOID_T set_led_status(BOOL_T led_status);
BOOL_T get_led_status(void);
#endif

DP 数据处理

什么是DP数据?

DP(Data Point)是涂鸦对产品功能定义的数据模型,用于描述产品的功能。 它具备以下几种类型:

  • 开关型(也称布尔型)(bool):用于控制产品的开关状态,例如打开/关闭。
  • 数值型(value):用于表示产品的数值状态,例如温度、湿度、电压等。
  • 字符串型(string):用于表示产品的文本状态,例如产品名称、版本号等。
  • 枚举型(enum):用于表示产品的离散状态,例如方向、颜色等。
  • 故障型(bitmap):用于表示产品的故障状态,例如是否有错误、是否有警告等。

DP 数据会通过一段 json 字符串进行传输,例如:

json
{"data":{"devId":"6cec2bf344b92d174bpch6","dps":{"20":true}},"protocol":5,"t":1773242156}

一般来说,DP 数据的处理可以分为三种动作:

  • 上报:产品将自己的状态数据(如开关状态、数值状态等)上传到云平台。
  • 校验:云平台会对接收到的指令进行校验,确保指令的合法性和有效性。
  • 下发:产品根据接收到的指令进行状态更新或其他操作。

这三种动作都需要我们自行实现,不过涂鸦的例程当中提供了关于 DP 数据处理的代码,我们可以参考这些代码甚至直接使用。

复制代码

icon
第一步

在 “tuyaos_demo_quickstart” demo 中复制“dp_process.c” 和 “dp_process.h” 文件到我们的项目中。

无效图片
icon
第二步

修改 “dp_process.c” 中的 app_led.hnet_led.h

无效图片
icon
第三步

编译测试,确保没有错误。

实现控制 LED

有了 DP 数据处理的相关代码之后,我们就可以通过调用 dp_process 函数来处理 DP 数据。并控制 LED 的亮灭,步骤如下:

icon
第一步

修改闪灯逻辑,避免 LED 的控制效果与配网状态显示冲突。

无效图片
icon
第二步

dp_obj_process 函数中修改 LED 状态的设置逻辑。
在 “dp_process.c” 定位到 94 行.如右图中修改。

无效图片
icon
第三步

在 “tuya_device.c” 中引用 “dp_process.h” 头文件。

icon
第四步

在 “tuya_device.c” 的 __soc_dev_obj_dp_cmd_cb 函数中调用 dp_obj_process 函数。
如右图所示

无效图片
icon
第五步

编译并烧录程序到开发板。

icon
第六步

测试 LED 控制功能。
打开 App,连接到开发板的 Wi-Fi 网络。
在 App 中找到我们的设备,点击开关按钮,观察 LED 的亮灭。