为什么越来越多的人喜欢买智能恒温电水壶呢?智能电水壶有哪些优势呢?吸引大家的原因是什么呢?我觉得应该是功能好,操作简单,价格适中,如果可以和家庭设备一起联动,那样就更好了。那既然大家都如此钟爱智能恒温电水壶,又是生活中必不可少的部分,那我们赶紧研究一款便捷智能的电水壶吧!不仅可以满足自己的需要,也能满足大家的需求。
App 远程控制,触摸按键控制。
恒温控制,保温模式可选,预约定时控制,煮水模式可选。
干烧断电,故障告警。
本文采纳了两种不同通信协议的模组进行开发,其主控板分别使用了涂鸦智能的低功耗嵌入式 Wi-Fi+BLE 双协议云模组 和 BLE 云模组 进行开发。两种模组的硬件设计方案一致,软件设计方案有所区别(将在下文进行介绍)。
电源系统带有各种保护,包括过温保护(OTP)、VCC 欠压锁定保护(UVLO)、过载保护(OLP)、短路保护(SCP)和开环保护。该电路是 220V 转 5V 降压电路,输入级由保险电阻 F2、防雷压敏电阻 RV1、整流桥堆 D3、EMI 滤波电容 C5 和 C6 以及滤波电感 L1 组成。保险电阻 F2 为阻燃可熔的绕线电阻,它同时具备多个功能:
电路特点
无噪音,发热低。
精准控温使用的是日本芝浦/华工高理温度传感器,感温精确达到±1%(B=3950, R=10K)。
温度检测电路图如下:
温控器是一个电水壶的心脏。在鸣笛式电水壶设计原理基础上,增设了限温自动控制器(双金属 恒温控制器 或者磁控恒温自动开关)。当水沸腾时,产生的水蒸气使感温元件的 双金属片 变形,驱动微型开关切断电源,极大地提高了电热水壶的安全性能。一些高品质的电热水壶中的限温控制器采用一种类似 记忆合金 的新型热敏材料(自动恒温控制开关),当壶内水面低于电热管(电热管温度超过100°C)时,自动恒温控制开关便会自动切断电源,保护电热水壶不被烧毁。
下图为英国 STRIX 温控器正反面照片:
本方案使用了 无源蜂鸣器,它由外部驱动,也称他激式蜂鸣器。
其发声原理是:方波信号输入后,由谐振装置转换为声音信号输出。
无源蜂鸣器控制电路如下所示:
其中,
预留该管脚,用作后续功能扩展。
本方案设计中在触摸焊盘上加了导电泡棉,增强导电性。
导电泡棉
触摸按键
按键检测芯片
选用 TS02N 作为按键检测芯片。TS02N 是双通道电容式传感器,具有自动灵敏度校准功能,其电源电压范围为2.5 ~ 5.5V。通过并联输出端口(OUT1 和 OUT2),芯片由低电平触发,可以检测触摸感知的结果。由于有了 SYNC 功能,两个 TS02N 可以同时在一个应用程序上工作。
管脚分配
管脚 | 作用 |
---|---|
P7 | 煮沸键 |
P8 | 保温键 |
TS02N 使用说明
触摸焊盘 P1 时,P8 输出低电平;同理,触摸焊盘 P2 时,P7 输出低电平。
触摸检测电路
主控板上设有主控芯片,无线信号接收及发生装置与主控芯片集成为一体或相互独立。
主控板上设有煮沸控制开关和保温控制开关。
电源板上设有继电器,继电器包括继电器线圈和继电器开关,继电器线圈通过电源板与主控板电性连接,继电器开关与加热电路电性连接、并控制加热电路的通断。
登录 涂鸦 IoT 平台。
选择 创建产品。
在页面左下角,选择 找不到品类? 。
在 自定义创建 区域内,填写参数后单击 创建产品。
在 功能定义 页签中,添加自定义功能。
本方案中需要添加以下功功能点:煮沸、温度设置、当前温度、保温温度、用水类型、故障警告。
在 设备面板 页签中,按照界面提示选择 App 面板。调试阶段推荐选择开发调试面板便于测试。
在 硬件开发 页签中,选择 涂鸦标准模组SDK开发 。
选择一款模组后,选择右下角 免费领取10个激活码 获取对应的 UUID、authkey 以及 MAC地址,以便填入后续的SDK中。
Wi-Fi&BLE 模组方案基于 BK7231N 平台进行 SoC 开发。开发所用的涂鸦通用 SDK 编译需要 Linux 环境,因此需要先安装 Linux 开发环境,然后从涂鸦仓库拉取包含 SDK 环境的 Demo 例程。
下载 Tuya IoTOS Embeded WiFi & BLE sdk。
在自己创建的目录中将 Demo 克隆下来。Demo 中附带有 SDK 环境,同时其中的 apps
目录中也有几个应用案例。我们使用 apps/tuya_demo_template
这个 Demo 为开发模板,在此基础上增减代码,实现一个嵌入式系统框架。
$ cd "your directory"
$ git clone https://github.com/tuya/tuya-iotos-embeded-sdk-wifi-ble-bk7231n
在现有的 Demo 基础上搭建系统框架。
当前 tuya_demo_template
的文件组成如下:
├── src
| └── tuya_device.c //应用层入口文件
|
├── include //头文件目录
| └── tuya_device.h
|
└── output //编译产物
将 tuya_demo_template
文件夹更名为 bk7231n_smart_kettle_demo
。
并更改 tuya_device.h
文件中的代码,填入产品创建步骤中获取的 PID,该步骤主要作用是配网后同步手机端的 App 界面。
在 Linux 终端输入命令,使用 SDK 目录下的编译脚本 build_app.sh
对程序进行编译。
sh build_app.sh apps/bk7231n_smart_kettle_demo bk7231n_smart_kettle_demo 1.0.0
编译后系统会在 apps/bk7231n_smart_kettle_demo/output
目录下生成 .bin 文件,其中带 UA 后缀的 .bin 文件为我们要烧录到板子中的应用固件。
下载、安装并启动 BK7231T 芯片烧录工具,烧录对应的 .bin 文件。
注意:烧录时需按下复位键,单击 烧录 后再松开复位键,此时可以看见烧录进度条在移动,最终烧录完成,具体步骤如下图:
更多烧录授权信息请参考:Wi-Fi + BLE 系列模组烧录授权
BLE 模组方案使用涂鸦 BLE SDK 和 Telink 芯片平台 TLSR825x 进行开发,以下为搭建开发环境的步骤。
下载 TLSR825x 对应的BLE SDK Demo:tuya_ble_sdk_Demo_Project_tlsr8253.git
我们将在\ble_sdk_multimode\tuya_ble_app
中进行智能烧水壶应用代码的编写。
下载 Telink 官方 IDE 并安装:Eclipse (IDE for TLSR8 Chips)( 注意!必须安装在C盘 )
将代码导入 Eclipse,可以直接在 Eclipse 进行代码修改,也可以先使用自己熟悉的代码编辑器。
修改 PID。
在 tuya_ble_app_demo.h
中填入创建产品过程中获取的智能烧水壶的 PID。
#define APP_PRODUCT_ID "ihqaiy9c"
修改 uuid 、Auth key 和 Mac 地址。
在 tuya_ble_app_demo.c
填入申请的授权码(uuid 、auth key、mac地址,选择10组中任意1组填入):
```
static const char auth_key_test[] = "CuvQxKaA5ccA2QBLY2M2sgZr69kUrmLi";
static const char device_id_test[] = "tuya4fea76a9b3f0";
static const uint8_t mac_test[6] = {0xCA, 0x4F, 0x62, 0x4D, 0x23, 0xDC}; /* 实际 Mac地址 -- DC:23:4D:62:4F:CA */
```
修改以下代码使授权码生效。
在 tuya_ble_app_demo.c
中找到 tuya_ble_app_init()
函数,将device_param.device_id_len = 0;
为device_param.device_id_len = 16;
(可以参考此行代码的注释)。
修改日志口。
由于I/O资源有限,需要在 vendor\8258_module\app_config.h
中将日志口修改为 GPIO_PD7
:
#define UART_PRINT_DEBUG_ENABLE 1
#define PRINT_BAUD_RATE 230400 /* 波特率 */
#define DEBUG_INFO_TX_PIN GPIO_PD7 /* 日志口,下面四行宏名同步修改 */
#define PD7_FUNC AS_GPIO
#define PD7_INPUT_ENABLE 0
#define PD7_OUTPUT_ENABLE 1
#define PD7_DATA_OUT 1
编译代码。
使用Eclipse对代码进行编译,输出文件目录为ble_sdk_multimode\8258_module\8258_module.bin
;编译前需修改工程配置中的头文件包含路径,根据SDK中的文件夹名称进行相应修改,修改方法参考下图:
下载 Telink 官方烧录工具并安装:Burning and Debugging Tools for all Series。
按下图链接电路板与 Telink 烧录器。
打开 BDT,按下图步骤进行芯片选择、打开文件后,下载并复位。
注意:
- 如果需要烧录的文件路径不变,重新编译后不需要重复”打开文件“这一步。
- 如果下载时提示“Swire err”可以点击“SWS”进行刷新。
程序载入后,使用串口调试工具进行日志查看,将波特率设置为 230400,并按照之前修改的日志口进行连接。
复位后将看到以下日志输出,包括我们之前修改的 PID、uuid 、Auth key、Mac 地址,以及版本号等其他信息:
程序启动后,可以使用涂鸦智能 App 搜索到我们的设备并进行绑定,可以看到设备名称显示为我们创建的产品名称”智能烧水壶“。
至此,智能烧水壶 Demo 开发前的准备工作就完成了。
常见的烧水壶一般有煮沸、保温功能,本案例在此基础上,使用涂鸦的模组和 SDK 实现了对烧水壶的远程控制,包括保温温度设置、水质模式选择、预约烧水等功能,另外还增加了干烧报警等功能,使烧水壶更加智能化。
本案例中的智能烧水壶设备上提供煮沸、保温2个按键,以及3个指示灯和1个蜂鸣器用于状态提示,可以本地控制也可以远程控制,具体功能设定如下:
功能 | 说明 |
---|---|
煮沸 | 轻触煮沸键,蜂鸣器“滴”一声提醒,切换煮沸功能开/关; 煮沸功能打开时,红灯亮,加热打开直至达到煮沸温度; 煮沸功能关闭或煮沸完成时,红灯灭,加热关闭。 |
保温 | 轻触保温键,蜂鸣器“滴”一声提醒,切换保温功能开/关; 保温功能打开且未达到保温温度时,橙灯亮,绿灯灭,加热根据当前温度打开或关闭; 保温功能关闭或达到保温温度时,橙灯灭,绿灯亮,加热关闭; 用水类型为自来水时,先加热至煮沸再保温至保温温度; 用水类型为纯净水时,直接加热至保温温度; 保温温度默认为 55 ℃,可通过 App 设置,设置范围在45~90℃; 用水类型默认为自来水,可通过 App 设置。 |
干烧报警 | 检测到干烧后自动关闭加热,硬件断电,蜂鸣器长鸣报警。 |
配网 | 上电配网,3分钟后未配网成功,仅可本地控制; 长按保温键 5 秒进入配网状态,3 分钟后未配网成功,仅可本地控制; 等待配网时,绿灯快闪。 |
远程控制 | 可通过 App 操作的项目有:打开/关闭煮沸功能和保温功能、设置保温温度和用水类型、查看当前温度和故障状态、预约烧水时间。 |
下面我们将介绍实现上述功能的具体方案和过程。
完整示例代码可在 Wi-Fi&BLE 模组方案代码 中获取。
温度采集原理
温度采集原理图如下:
温度采集方案是使用热敏电阻,热敏电阻在不同温度下有不同的阻值,根据此特性,通过电路设计和软件程序配合采集到热敏电阻的阻值,从而计算出当前的温度值。
采样电路图如下:R8 为 20k 的定值电阻,R7 为热敏电阻(常温下阻值为 100k)。ADC 是电压采样点,采集电压后,根据欧姆定律即可算出热敏电阻的阻值。
得到热敏电阻阻值 Rt,根据 B3950 的热敏曲线即可算出当前温度值。
图中参数含义如下所示:
参数 | 说明 |
---|---|
R0 | 25°C 下的电阻阻值。本文中选用的热敏电阻在 25°C 时阻值为 10k |
R | 当前温度下电阻的阻值 |
T0 | 开尔文温度 (273.15+25) |
T | 开尔文温度(273.15+当前摄氏度温度) |
B | 热敏特性常数 |
exp | e^n(e的n次方) |
通过采样电路采样热敏电阻两端的电压,从而计算出 R 的阻值,再根据 R 的阻值计算当前的温度。
R=R0 expB (1/T-1/T0)
||
\/
T=1/(ln(R/R0)/B+1/T0)近似为T=1/(log(R/R0)/B+1/T0)
得到的T为开尔文温度,换算成摄氏度即 temp = T - 273.15
温度采集代码
初始化温度采集所用的 ADC 设备,再采集温度。
//初始化adc设备
void b3950_init(void)
{
/*create adc device,get handle*/
temper_adc = (tuya_adc_t *)tuya_driver_find(TUYA_DRV_ADC, TUYA_ADC2);
/*adc_dev cfg*/
TUYA_ADC_CFG(temper_adc, TUYA_ADC2, 0);
/*adc_dev init*/
tuya_adc_init(temper_adc);
}
//获取温度值
int cur_temper_get()
{
int Rt = 0;
float Rp = 100000;
float T2 = 273.15 + 25;
float Bx = 3950;
float Ka = 273.15;
int temp = 0;
/*Collect AD data and store it in adc_buffer*/
tuya_adc_convert(temper_adc, &adc_buf, 1);
/*req_val(0-4096) - V(0-2.4)*/
volt = (float)adc_buf *2.4/ 4096;
//volt = adc_buf;
Rt = (3.3 - volt)*20000/volt;
PR_DEBUG("Rt:%d", Rt);
temp = (int)(1/(1/T2+log(Rt/Rp)/Bx)-Ka+0.5);
PR_DEBUG("volt:%f", volt);
return temp;
}
温度显示代码
温度显示流程为每2秒采集一次温度,并将温度值上报到云端。
//线程初始化函数,创建定时器线程每2秒采集一次温度,创建主应用线程,创建互斥锁
void thread_init(void)
{
int rt = OPRT_OK;
// create mutex
if(NULL == mutex) {
rt = tuya_hal_mutex_create_init(&mutex);
if(OPRT_OK != rt) {
PR_ERR("tuya_hal_mutex_create_init err:%d",rt);
return rt;
}
}
//A timer with a period of 5 second is used to capture the temperature
rt = sys_add_timer(get_temper_timer_cb, NULL, &get_temper_timer);
if(rt != OPRT_OK) {
PR_ERR("add timer error!: %d", rt);
return;
}else {
rt = sys_start_timer(get_temper_timer,2000,TIMER_CYCLE);
if(rt != OPRT_OK) {
PR_ERR("start timer error!: %d", rt);
return;
}
}
rt = tuya_hal_thread_create(NULL, "app_kettle_thread", 512*4, TRD_PRIO_4, app_kettle_thread, NULL);
if (rt != OPRT_OK) {
PR_ERR("Create update_dp_thread error!: %d", rt);
return;
}
}
//软件定时器回调函数,定时采集温度并上报
void get_temper_timer_cb(void)
{
static int last_temper = 0;
last_temper = cur_temper_get();
if(last_temper > 150) {
last_temper = 150;
}
else if(last_temper < 0) {
last_temper = 0;
}
set_current_temperature(last_temper);
tuya_hal_mutex_lock(mutex);
temper_s.value = last_temper;
tuya_hal_mutex_unlock(mutex);
PR_DEBUG("last_temper : %d",last_temper);
report_one_dp_status(DP_TEMPER);//上报温度信息到app
}
过温报警代码
利用蜂鸣器报警,当测得温度高于 105 摄氏度时说明水壶已经没水了,此时驱动蜂鸣器响动并上报干烧状态到云端。
//定时器初始化及开启定时器函数,用于产生驱动蜂鸣器的脉冲
void timer_init(void)
{
timer = (tuya_timer_t *)tuya_driver_find(TUYA_DRV_TIMER, TUYA_TIMER1);
//The timer uses cycle mode
TUYA_TIMER_CFG(timer, TUYA_TIMER_MODE_PERIOD, tuya_timer0_cb, NULL);
tuya_timer_init(timer);
//Start the timer, 100us is a counting cycle
tuya_timer_start(timer, 100);
buzzer_set(1);
}
void tuya_timer0_cb(void *arg)
{
static uint32_t s_tick;
static int level = 1;
level = ~level;
buzzer_set(level);//设置蜂鸣器电路驱动IO的电平
if (s_tick++ >= 700) {
s_tick = 0;
tuya_timer_stop(timer);
buzzer_set(0);
}
}
//智能烧水壶主应用线程,当前只实现过温报警功能,后面会填充水温调节等功能
void app_kettle_thread(void)
{
//Over temperature protection Limiting_Temperature :105
if(get_water_temperature() > Limiting_Temperature) {
timer_init(); //buzzer on
set_dp_fault_status(TRUE);//更新故障状态 正常->干烧
report_one_dp_status(DP_FAULT);//app上显示故障信息
relay_set(OFF);
}else {
if(get_dp_fault_status() == TRUE) {
set_dp_fault_status(FALSE);//更新故障状态 干烧->正常
report_one_dp_status(DP_FAULT);//app上显示故障信息
}
}
tuya_hal_system_sleep(1000);
}
}
在产品创建和五大功能模式都实现的基础上接着实现语音播报、加热、档位切换的功能。
功能简述
智能烧水壶设备上提供煮沸、保温两个按键。
功能 | 说明 |
---|---|
煮沸 |
|
保温键 |
|
按键采集原理
按键采集芯片选用TS02N,触摸按键采集原理图如下:
管脚分配:
管脚 | 作用 |
---|---|
P7 | 煮沸键 |
P8 | 保温键 |
TS02N 使用说明:
输入P1\P2:低电平触发
P1触发后OUT1输出低电平
P2触发后OUT2输出低电平
离线控制代码
为了后期的程序扩展,采用回调函数注册的方式实现按键控制。
按键驱动代码如下:
/*使用时只需要通过 ts02n_key_init(TS02N_KEY_DEF_S* user_key_def) 函数进行注册,注册后会自动开启一个定时扫描任务去采集按键信息,按键触发后也会自动调用注册的回调函数*/
typedef VOID(* KEY_CALLBACK)();
typedef struct { // user define
unsigned int key_pin1; //ts02n CH1 PIN
unsigned int key_pin2; //ts02n CH2 PIN
KEY_CALLBACK key1_cb; //normal press key1 callback function
KEY_CALLBACK key2_cb; //normal press key2 callback function
KEY_CALLBACK key1_long_press_cb; //normal press key1 callback function
KEY_CALLBACK key2_long_press_cb; //normal press key2 callback function
KEY_CALLBACK key1_Low_level_cb; //Key1 low level process callback function
KEY_CALLBACK key2_Low_level_cb; //Key2 low level process callback function
unsigned int long_time_set; //long press key time set
unsigned int scan_time; //Scan cycle set unit: ms
}TS02N_KEY_DEF_S;
//按键初始化函数
int ts02n_key_init(TS02N_KEY_DEF_S* user_key_def)
{
int op_ret = 0;
key_mag = (TS02N_KEY_MANAGE_S *)Malloc(SIZEOF(TS02N_KEY_MANAGE_S));
memset(key_mag, 0, SIZEOF(TS02N_KEY_MANAGE_S));
//No callback function is registered key init err
if((user_key_def->key1_cb == NULL) && (user_key_def->key2_cb == NULL) && \
(user_key_def->key1_long_press_cb == NULL) && (user_key_def->key2_long_press_cb == NULL)){
return KEY_INIT_ERR;
}
//key pin init
tuya_pin_init(user_key_def->key_pin1, TUYA_PIN_MODE_IN_PU);
tuya_pin_init(user_key_def->key_pin2, TUYA_PIN_MODE_IN_PU);
//get user define
key_mag->ts02n_def_s = user_key_def;
//creat key scan handle timer
op_ret = sys_add_timer(key_timer_cb,NULL,&key_timer);
if(op_ret != KEY_INIT_OK) {
return KEY_INIT_ERR;
PR_ERR("add timer err");
}
op_ret = sys_start_timer(key_timer,key_mag->ts02n_def_s->scan_time,TIMER_CYCLE);
if(op_ret != KEY_INIT_OK) {
return KEY_INIT_ERR;
PR_ERR("start timer err");
}
}
//按键定时扫描以及处理函数,根据长按、短按等模式触发不同函数,触发的函数需要在使用前注册
static void key_timer_cb()
{
int ret = -1;
//get key press status
ret = key_scan();
switch (ret) {
case KEY1_NORMAL_PRESS: {
key_mag->ts02n_def_s->key1_cb();
}
break;
case KEY1_LONG_PRESS: {
key_mag->ts02n_def_s->key1_long_press_cb();
}
break;
case KEY2_NORMAL_PRESS: {
key_mag->ts02n_def_s->key2_cb();
}
break;
case KEY2_LONG_PRESS: {
key_mag->ts02n_def_s->key2_long_press_cb();
}
break;
default:
break;
}
}
//按键扫描函数,在key_timer_cb中调用
static int key_scan()
{
if((tuya_pin_read(key_mag->ts02n_def_s->key_pin1) == 0) && (tuya_pin_read(key_mag->ts02n_def_s->key_pin2) == 0 )) {
return NO_KEY_PRESS;
}
if(tuya_pin_read(key_mag->ts02n_def_s->key_pin1) == 0 ) {
key_mag->ts02n_def_s->key1_Low_level_cb();
key_mag->key_status.status = KEY1_DOWN;
key_mag->key_status.down_time += key_mag->ts02n_def_s->scan_time;
if(key_mag->key_status.down_time >= key_mag->ts02n_def_s->long_time_set) {
key_mag->key_status.status = KEY1_FINISH;
}
}else {
if(key_mag->key_status.status == KEY1_DOWN) {
key_mag->key_status.status = KEY1_FINISH;
}
}
if(tuya_pin_read(key_mag->ts02n_def_s->key_pin2) == 0 ) {
key_mag->ts02n_def_s->key2_Low_level_cb();
key_mag->key_status.status = KEY2_DOWN;
key_mag->key_status.down_time += key_mag->ts02n_def_s->scan_time;
if(key_mag->key_status.down_time >= key_mag->ts02n_def_s->long_time_set) {
key_mag->key_status.status = KEY2_FINISH;
}
}else {
if(key_mag->key_status.status == KEY2_DOWN) {
key_mag->key_status.status = KEY2_FINISH;
}
}
switch (key_mag->key_status.status) {
case (KEY1_FINISH): {
if(key_mag->key_status.down_time >= key_mag->ts02n_def_s->long_time_set) {
PR_DEBUG("key_pin1 long press");
key_mag->key_status.down_time = 0;
key_mag->key_status.status = KEY1_UP;
return KEY1_LONG_PRESS;
}else {
key_mag->key_status.status = KEY1_UP;
key_mag->key_status.down_time = 0;
PR_DEBUG("key_pin1 press");
return KEY1_NORMAL_PRESS;
}
}
break;
case (KEY2_FINISH): {
if(key_mag->key_status.down_time >= key_mag->ts02n_def_s->long_time_set) {
PR_DEBUG("key_pin2 long press");
key_mag->key_status.down_time = 0;
key_mag->key_status.status = KEY2_UP;
return KEY2_LONG_PRESS;
}else {
key_mag->key_status.status = KEY2_UP;
key_mag->key_status.down_time = 0;
PR_DEBUG("key_pin2 press");
return KEY2_NORMAL_PRESS;
}
}
break;
default:
return NO_KEY_PRESS;
break;
}
}
按键控制代码:
//进行按键相关的配置
TS02N_KEY_DEF_S kettle_key_def_s = {
.key_pin1 = TY_GPIOA_8,
.key_pin2 = TY_GPIOA_7,
.key1_cb = key1_cb_fun,
.key2_cb = key2_cb_fun,
.key1_long_press_cb = NULL,
.key2_long_press_cb = key2_long_press_cb_fun,
.key1_Low_level_cb = key1_Low_level_cb_fun,
.key2_Low_level_cb = key2_Low_level_cb_fun,
.long_time_set = 5000,
.scan_time = 100,
};
//初始化函数
VOID_T kettle_init()
{
b3950_init();
thread_init();
kettle_gpio_init();
ts02n_key_init(&kettle_key_def_s);//注册按键并初始化
}
//煮沸按键:按下一次,水壶模式变为煮沸(boil),再按一下取消煮沸设定,水壶模式变为(nature)
void key1_cb_fun()
{
//Button press prompt
led1_set(OFF);
buzzer_flag = 1;
//Button press prompt
if(get_kettle_work_status() == boil) { //if current status is boil, close boiling
set_kettle_work_status(nature);
set_dp_boil_value(FALSE);
report_one_dp_status(DP_BOIL);
}else {
set_kettle_work_status(boil);
set_dp_boil_value(TRUE);
report_one_dp_status(DP_BOIL);
}
}
//保温按键:按下一次,水壶模式变为保温模式(keep_warm_mode1/keep_warm_mode2),再按一下取消保温设定,水壶模式变为(nature)
void key2_cb_fun()
{
//Button press prompt
led2_set(OFF);
buzzer_flag = 1;
//Button press prompt
if(get_kettle_work_status() == keep_warm_mode1) {//if current status is keep_warm_mode1,turn off insulation function
set_kettle_work_status(nature);
set_dp_keep_warm_switch(0);
report_one_dp_status(DP_KEEP_WARM);
}else {
set_kettle_work_status(keep_warm_mode1);
//tap water warm mode :1.boil -> 2.keep warm
set_kettle_keep_warm_temper(Default_Warm_Temperature);
set_dp_keep_warm_switch(1);
report_one_dp_status(DP_KEEP_WARM);
report_one_dp_status(DP_TEMP_SET);
}
}
//保温键长按5s触发,进行配网模式
void key2_long_press_cb_fun()
{
led2_set(OFF);
tuya_iot_wf_gw_unactive();
buzzer_flag = 1;
}
//按键按下时触发
void key1_Low_level_cb_fun()
{
PR_DEBUG("key1_Low_level_cb");
led1_set(ON);
if(buzzer_flag) {
timer_init();//蜂鸣器响一下
buzzer_flag = 0;
}
}
//按键按下时触发
void key2_Low_level_cb_fun()
{
led2_set(ON);
PR_DEBUG("key2_Low_level_cb");
if(buzzer_flag) {
timer_init();//蜂鸣器响一下
buzzer_flag = 0;
}
}
模式控制代码
主线程代码。按键或云端触发会控制水壶处于不同的模式,此时主线程根据不同模式进行对应的处理。
//煮沸模式,到达沸点附近温度,开启定时任务,再加热5秒然后关闭加热档和相关状态灯
STATIC VOID boil_time_task(VOID)
{
/*Reaches near the boiling point and reheat for 5 seconds, then turn off the device*/
set_kettle_work_status(nature); //Switch to cool mode
relay_set(OFF);
led1_set(OFF);
led2_set(OFF);
state_led_set(OFF);
set_dp_boil_value(0);
report_one_dp_status(DP_BOIL);
PR_DEBUG("boil->nature");
}
//主应用线程代码,获取当前的水壶模式,然后进行相应的处理。
void app_kettle_thread(void)
{
static int temp_work_status = 0;
while(1)
{
temp_work_status = get_kettle_work_status();
//According to the different Settings of the corresponding processing
switch (temp_work_status) {
case nature: {
led1_set(OFF);//上电后处于自然模式,指示灯全灭
led2_set(OFF);
state_led_set(OFF);
relay_set(OFF);
}
break;
case boil: {
if(get_water_temperature() >= Boil_Temperature) {
int op_ret = -1;
//当水温到达沸点附近,启动软件定时器,再加热5秒钟,然后关闭加热档
op_ret = sys_add_timer(boil_time_task,NULL,&boil_timer);
if(op_ret != 0) {
PR_ERR("add timer err");
return;
}
//如果定时器启动失败,立即关闭加热档,防止一直煮沸
op_ret = sys_start_timer(boil_timer,5000,TIMER_ONCE);
if(op_ret != 0) {
PR_ERR("start timer err");
set_kettle_work_status(nature); //Switch to cool mode
relay_set(OFF);
led1_set(OFF);
led2_set(OFF);
state_led_set(OFF);
set_dp_boil_value(0);
report_one_dp_status(DP_BOIL);
PR_DEBUG("boil->nature");
return;
}
}else {
relay_set(ON);
led1_set(ON);//开启煮沸指示灯
led2_set(OFF);
state_led_set(OFF);
}
}
break;
//tap water keep warm
case keep_warm_mode1: {
state_led_set(OFF);
led1_set(ON);
if(get_water_temperature() >= Boil_Temperature) {
set_kettle_work_status(keep_warm_mode2);
led1_set(OFF);
led2_set(ON);//开启保温模式指示灯,此时未达到保温温度橙灯亮
state_led_set(OFF);
relay_set(OFF);
PR_DEBUG("keep_warm_mode1->keep_warm_mode2");
}else {
relay_set(ON);
led1_set(OFF);
led2_set(ON);//开启保温模式指示灯,此时未达到保温温度橙灯亮
state_led_set(OFF);
PR_DEBUG("keep_warm_mode1 relay_set on");
}
}
break;
//clear water keep warm
case keep_warm_mode2: {
//cur_temp > set temper close the heating
if(get_water_temperature() > get_keep_wram_temperature() - 3) {
relay_set(OFF);
led1_set(OFF);
led2_set(ON);//开启保温模式指示灯,此时未达到保温温度橙灯亮
state_led_set(OFF);
PR_DEBUG("keep_warm_mode2 relay_set 0");
//cur_temp < set temper open the heating
}else if(get_water_temperature() < (get_keep_wram_temperature() - 5)) {
relay_set(ON);
led1_set(OFF);
led2_set(ON);//开启保温模式指示灯,此时未达到保温温度橙灯亮
state_led_set(OFF);
PR_DEBUG("keep_warm_mode2 relay_set 1");
}else {
// cur_temp == set temper
//It indicates that the temperature has reached the insulation value
led1_set(OFF);
led2_set(OFF);
state_led_set(ON);//开启保温模式指示灯,此时已达到保温温度绿灯亮
relay_set(OFF);
}
}
break;
default:
break;
}
//干烧报警
//Over temperature protection, Limiting_Temperature:105 lack water warning
if(get_water_temperature() > Limiting_Temperature) {
timer_init(); //buzzer on , Voice call the police
set_dp_fault_status(TRUE); //app display warning
report_one_dp_status(DP_FAULT);
relay_set(OFF); //turn off relay
}else {
if(get_dp_fault_status() == TRUE) {
set_dp_fault_status(FALSE);
report_one_dp_status(DP_FAULT);
}
}
tuya_hal_system_sleep(1000);
}
}
本方案实现的智能烧水壶可以远程控制保温、加热、定时煮沸等功能,具体的云端控制功能点如下:
功能名称 | 控制选项 |
---|---|
煮沸 | 开启/关闭 |
设置保温温度 | 45~90摄氏度 |
保温 | 开启/关闭 |
水质选择 | 自来水/纯净水 |
温度显示 | 显示当前室温 |
当前温度显示 | 时间自行设定 |
定时煮沸 | 时间自行设定 |
干烧状态显示 | 正常/缺水 |
实现云端控制首先要实现dp数据触发的执行函数,下面我们将实现这些执行函数
实现煮沸控制的执行函数
此执行函数需要根据下发的 DP 数据控制水壶的煮沸功能,开启和关闭触发后要实现的具体功能如下:
开启:
关闭
具体代码实现,需要在 kettle_app.c 中增加执行函数
static void dp_boil_handle(IN BOOL_T bONOFF)
{
//开启煮沸
if(bONOFF == TRUE) {
set_kettle_work_status(boil);//切换水壶的工作状态到煮沸模式
set_dp_boil_value(TRUE);//设置dp_boil控制点的值为1,即开启
set_dp_keep_warm_switch(FALSE);//设置dp_keep_warm控制点的值为0,即关闭
PR_DEBUG("dp_boil on");
}else {//关闭煮沸
set_kettle_work_status(nature);//切换水壶的工作状态到自然模式
set_dp_boil_value(FALSE);//设置dp_boil控制点的值为0,即关闭
PR_DEBUG("dp_boil off");
}
report_one_dp_status(DP_BOIL);//上报dp_boil数据
report_one_dp_status(DP_KEEP_WARM);//上报dp_keep_warm数据
}
//设定烧水壶的工作状态
void set_kettle_work_status(int status)
{
tuya_hal_mutex_lock(mutex);
kettle_work_information.status = status;
tuya_hal_mutex_unlock(mutex);
}
//设置dp_boil的数据值
void set_dp_boil_value(bool value)
{
tuya_hal_mutex_lock(mutex);
boil_s.power = value;
tuya_hal_mutex_unlock(mutex);
}
//设置dp_keep_warm的数据值
void set_dp_keep_warm_switch(bool value)
{
tuya_hal_mutex_lock(mutex);
keep_warm_s.power = value;
tuya_hal_mutex_unlock(mutex);
}
此时煮沸控制的执行函数已实现。
实现设定保温温度的函数
设定保温温度的函数是通过云端设定进行触发,触发后要实现的具体功能如下:
具体代码实现,需要在kettle_app.c中增加执行函数
static void dp_keep_warm_set_handle(IN int value)
{
set_kettle_keep_warm_temper(value);//设置保温温度,注意设定保温温度不会直接触发保温
report_one_dp_status(DP_TEMP_SET);//上报dp_keep_warm_set这个dp点的设定值到云端
}
//保温温度设置范围为45~90摄氏度
void set_kettle_keep_warm_temper(int value)
{
if(44 < value < 91) {
tuya_hal_mutex_lock(mutex);
temp_set_s.value = value;
kettle_work_information.warm_temperature = value;
tuya_hal_mutex_unlock(mutex);
PR_DEBUG("set keep warm temper:%d",value);
}
}
此时保温温度设定函数已实现。
实现保温控制的执行函数
云端可以控制界面上的保温按钮决定是否开启保温,触发后要实现的具体功能如下:
开启:
关闭:
切换烧水壶的工作状态到自然模式(无操作)
具体代码实现,需要在kettle_app.c中增加执行函数
static void dp_keep_warm_handle(IN BOOL_T bONOFF)
{
//保温开启,水质模式为自来水模式,烧水壶进入保温1模式(先煮沸再保温)
if(bONOFF == TRUE && (get_water_type() == tap_water)) {
set_kettle_work_status(keep_warm_mode1);
set_dp_keep_warm_switch(TRUE);
set_dp_boil_value(FALSE);
PR_DEBUG("keep_warm_mode1");
//保温开启,水质模式为纯净水模式,烧水壶进入保温2模式(直接保温)
}else if(bONOFF == TRUE && (get_water_type() == clear_water)){
set_kettle_work_status(keep_warm_mode2);
set_dp_keep_warm_switch(TRUE);
set_dp_boil_value(FALSE);
PR_DEBUG("keep_warm_mode2");
}else {
//关闭保温
set_kettle_work_status(nature);
set_dp_keep_warm_switch(FALSE);
PR_DEBUG("close keep warm");
}
//上报dp数据
report_one_dp_status(DP_KEEP_WARM);
report_one_dp_status(DP_BOIL);
}
此时保温设置的执行函数已实现。
实现水质选择设定函数
云端可以控制水质选择,触发后要实现的具体功能如下:
具体代码实现,需要在 kettle_app.c 中增加执行函数。
void dp_water_type_handle(int value)
{
if(value == tap_water || value == clear_water) {
//设置水质模式
tuya_hal_mutex_lock(mutex);
kettle_work_information.water_mode = value;
water_type_s.value = value;
tuya_hal_mutex_unlock(mutex);
//此时水壶处于自来水保温状态,且此时云端控制水质模式切换为纯净水,水壶状态切换到纯净水保温模式
if(get_kettle_work_status() == keep_warm_mode1 && get_water_type() == clear_water) {
set_kettle_work_status(keep_warm_mode2);
//此时水壶处于纯净水保温状态,且此时云端控制水质模式切换为自来水,水壶状态切换到自来水保温模式
}else if(get_kettle_work_status() == keep_warm_mode2 && get_water_type() == tap_water) {
set_kettle_work_status(keep_warm_mode1);
}
PR_DEBUG("water choose :%d",value);
report_one_dp_status(DP_WATER_TYPE);
}
}
//获取当前水壶的工作状态
int get_kettle_work_status()
{
return kettle_work_information.status;
}
//获取当前的水质模式
int get_water_type()
{
return kettle_work_information.water_mode;
}
此时水质选择设定函数已实现。
云端定时煮沸功能
云端定时煮沸云端设定定时任务,到达定时时间后云端会下发控制命令触发 dp_boil 这个 DP 点数据下发,从而触发烧水壶执行煮沸功能。
云定时煮沸开启:
云端控制代码实现
上面已经实现了相应的执行函数,下面我们将实现 App 下发 DP 数据控制设备的功能。
云端控制代码实现:
//云端下发命令后触发此回调函数,从而触发deal_dp_proc函数,实现远程控制烧水壶工作
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);
UCHAR_T i = 0;
for(i = 0;i < dp->dps_cnt;i++) {
deal_dp_proc(&(dp->dps[i]));
//dev_report_dp_json_async(get_gw_cntl()->gw_if.id, dp->dps, dp->dps_cnt);
}
}
//app下发命令后将会触发回调函数从而执行该函数,根据下发的DP_ID以及控制值执行相关函数
VOID_T deal_dp_proc(IN CONST TY_OBJ_DP_S *root)
{
UCHAR_T dpid;
dpid = root->dpid;
switch (dpid){
//煮沸控制
case DP_BOIL: {
dp_boil_handle(root->value.dp_bool);
}
break;
//保温温度设置
case DP_TEMP_SET: {
dp_keep_warm_set_handle(root->value.dp_value);
}
break;
//保温设置
case DP_KEEP_WARM: {
dp_keep_warm_handle(root->value.dp_bool);
}
break;
//水质模式设置
case DP_WATER_TYPE: {
dp_water_type_handle(root->value.dp_enum);
}
break;
default:
break;
}
}
完整示例代码可在 BLE 模组方案代码 中获取。
指示灯驱动控制
指示灯作为烧水壶状态指示和配网指示,驱动形式为电平驱动,基本设置如下:
指示灯 | 管脚 | H | L | 显示模式 |
---|---|---|---|---|
红色 P_LED_RED | P24/GPIO_PB5 | 灭 | 亮 | 固定(常亮/常灭) |
橙色 P_LED_ORANGE | P26/GPIO_PB4 | 灭 | 亮 | 固定(常亮/常灭) |
绿色 P_GREEN | P6/GPIO_PD2 | 灭 | 亮 | 固定(常亮/常灭) / 闪烁(0.2s亮0.2s灭) |
代码实现:
#define P_LED_RED GPIO_PB5 /* P24 */
#define P_LED_ORANGE GPIO_PB4 /* P26 */
#define P_LED_GREEN GPIO_PD2 /* P6 */
#define LED_TWINKLE_TIME 200 /* 0.2s */
LED_MODE_E g_led_green_mode = LED_MODE_FIX;
LED_MODE_E g_led_green_status = OFF;
/* LED端口初始化 */
void led_init(void)
{
gpio_set_func(P_LED_RED, AS_GPIO);
gpio_set_output_en(P_LED_RED, 1);
gpio_write(P_LED_RED, 1);
gpio_set_func(P_LED_ORANGE, AS_GPIO);
gpio_set_output_en(P_LED_ORANGE, 1);
gpio_write(P_LED_ORANGE, 1);
gpio_set_output_en(P_LED_GREEN, 1);
gpio_set_func(P_LED_GREEN, AS_GPIO);
gpio_write(P_LED_GREEN, 1);
}
/* 红色LED亮灭控制 */
void set_led_red(bool b_on_off)
{
static bool s_last_status = 0;
if (s_last_status != b_on_off) {
if (b_on_off == ON) {
gpio_write(P_LED_RED, 0);
} else {
gpio_write(P_LED_RED, 1);
}
s_last_status = b_on_off;
}
}
/* 橙色LED亮灭控制 */
void set_led_orange(bool b_on_off)
{
static bool s_last_status = 0;
if (s_last_status != b_on_off) {
if (b_on_off == ON) {
gpio_write(P_LED_ORANGE, 0);
} else {
gpio_write(P_LED_ORANGE, 1);
}
s_last_status = b_on_off;
}
}
/* 绿色LED亮灭控制 */
void set_led_green(bool b_on_off)
{
static bool s_last_status = 0;
if (s_last_status != b_on_off) {
if (b_on_off == ON) {
gpio_write(P_LED_GREEN, 0);
} else {
gpio_write(P_LED_GREEN, 1);
}
s_last_status = b_on_off;
}
}
/* 绿色LED模式设置 */
void set_led_green_mode(LED_MODE_E mode)
{
g_led_green_mode = mode;
}
/* 绿色LED状态设置 */
void set_led_green_status(uint8_t status)
{
g_led_green_status = status;
}
/* 绿色LED状态更新 */
void update_led_green_status(void)
{
static bool s_status = 0;
static uint32_t s_twinkle_tm = 0;
switch (g_led_green_mode) {
case LED_MODE_FIX: /* 固定模式:根据状态设置控制LED亮灭 */
set_led_green(g_led_green_status);
s_twinkle_tm = 0;
break;
case LED_MODE_TWINKLE: /* 闪烁模式:0.2s翻转一次亮灭状态 */
if (!clock_time_exceed(s_twinkle_tm, LED_TWINKLE_TIME*1000)) {
break;
}
s_twinkle_tm = clock_time();
s_status = !s_status;
set_led_green(s_status);
break;
default:
break;
}
}
继电器驱动控制
继电器的通断用于控制加热电路,驱动形式为电平驱动,基本设置如下:
继电器 | 管脚 | H | L |
---|---|---|---|
继电器 P_RELAY | P14/GPIO_PD3 | 打开加热 | 关闭加热 |
代码实现:
#define P_RELAY GPIO_PD3 /* P14 */
/* 继电器端口初始化 */
void relay_init(void)
{
gpio_set_func(P_RELAY, AS_GPIO);
gpio_set_output_en(P_RELAY, 1);
gpio_write(P_RELAY, 0);
}
/* 继电器通断控制 */
void set_relay(bool b_on_off)
{
static bool s_last_status = 0;
if (s_last_status != b_on_off) {
if (b_on_off == ON) {
gpio_write(P_RELAY, 1);
} else {
gpio_write(P_RELAY, 0);
}
s_last_status = b_on_off;
}
}
蜂鸣器驱动控制
蜂鸣器作为按键提示音和故障报警提示音,驱动形式为方波信号驱动,基本设置如下:
蜂鸣器 | 管脚 | 模式 | 方波信号(PWM) |
---|---|---|---|
蜂鸣器 P_BUZZER | P17/GPIO_PD4 | 停止 / 滴一声(70ms) / 故障(长鸣) | 通道:PWM2 周期:500us (2KHz) 占空比:50% |
代码实现:
#define P_BUZZER GPIO_PD4 /* P17 */
#define PWM_ID_BUZZER PWM2_ID /* PWM2 */
#define BUZZER_ONCE_TIME 70 /* 70ms */
typedef BYTE_T BUZZER_MODE_E;
#define BUZZER_MODE_STOP 0x00 /* 停止 */
#define BUZZER_MODE_ONCE 0x01 /* 滴一声 */
#define BUZZER_MODE_FAULT 0x02 /* 故障 */
static uint8_t sg_buzzer_timer = OFF;
static uint32_t sg_buzzer_tm = 0;
/* 蜂鸣器端口及PWM配置初始化 */
void buzzer_pwm_init(void)
{
pwm_set_clk(CLOCK_SYS_CLOCK_HZ, CLOCK_SYS_CLOCK_HZ);
gpio_set_func(P_BUZZER, AS_PWM2_N);
pwm_set_mode(PWM_ID_BUZZER, PWM_NORMAL_MODE);
pwm_set_cycle_and_duty(PWM_ID_BUZZER, (uint16_t)(500 * CLOCK_SYS_CLOCK_1US), (uint16_t)(250 * CLOCK_SYS_CLOCK_1US)); /* 500us 2KHz 50% */
gpio_write(P_BUZZER, 0);
}
/* 蜂鸣器开关控制 */
static void set_buzzer(bool b_on_off)
{
static bool s_last_status = 0;
if (s_last_status != b_on_off) {
if (b_on_off == ON) {
pwm_start(PWM_ID_BUZZER);
} else {
pwm_stop(PWM_ID_BUZZER);
gpio_write(P_BUZZER, 0);
}
s_last_status = b_on_off;
}
}
/* 蜂鸣器模式控制 */
void set_buzzer_mode(BUZZER_MODE_E mode)
{
switch (mode) {
case BUZZER_MODE_STOP:
set_buzzer(OFF);
break;
case BUZZER_MODE_ONCE:
set_buzzer(ON);
sg_buzzer_timer = ON;
sg_buzzer_tm = clock_time();
break;
case BUZZER_MODE_FAULT:
set_buzzer(ON);
break;
default:
break;
}
}
/* 蜂鸣器状态更新 */
void update_buzzer_status(void)
{
if (sg_buzzer_timer == ON) {
if (!clock_time_exceed(sg_buzzer_tm, BUZZER_ONCE_TIME*1000)) {
return;
}
set_buzzer(OFF);
sg_buzzer_timer = OFF;
}
}
按键检测与处理
为了后期的程序扩展,采用注册回调函数的方式实现按键检测与处理
代码实现:
a. 按键注册信息及初始化
用户可注册内容设定如下:
typedef void(* KEY_CALLBACK)();
typedef struct {
uint16_t key1_pin; /* 按键1所用I/O口 */
uint16_t key2_pin; /* 按键2所用I/O口 */
KEY_CALLBACK key1_short_press_cb; /* 按键1短按触发的回调函数 */
KEY_CALLBACK key2_short_press_cb; /* 按键2短按触发的回调函数 */
KEY_CALLBACK key1_long_press_cb; /* 按键1长按触发的回调函数 */
KEY_CALLBACK key2_long_press_cb; /* 按键2长按触发的回调函数 */
uint32_t key1_long_press_time; /* 按键1长按时间设置(ms) */
uint32_t key2_long_press_time; /* 按键2长按时间设置(ms) */
uint32_t scan_time; /* 按键1扫描间隔设置(ms) */
} TS02N_KEY_DEF_T;
接下来定义按键检测所需的状态变量,和用户注册内容共同管理,然后编写按键初始化函数:
/* 按键状态 */
typedef struct {
uint8_t cur_code;
uint8_t prv_code;
uint32_t cur_time;
uint32_t prv_time;
} TS02N_KEY_STATUS_T;
/* 按键管理 */
typedef struct {
TS02N_KEY_DEF_T* ts02n_key_def_s;
TS02N_KEY_STATUS_T ts02n_key_status_s;
} TS02N_KEY_MANAGE_T;
static TS02N_KEY_MANAGE_T *sg_key_mag = NULL;
/* 按键初始化 */
uint8_t ts02n_key_init(TS02N_KEY_DEF_T* key_def)
{
/* 按键信息初始化 */
sg_key_mag = (TS02N_KEY_MANAGE_T *)tuya_ble_malloc(sizeof(TS02N_KEY_MANAGE_T));
memset(sg_key_mag, 0, sizeof(TS02N_KEY_MANAGE_T));
sg_key_mag->ts02n_key_def_s = key_def;
/* 回调函数检查 */
if ((key_def->key1_short_press_cb == NULL) &&
(key_def->key2_short_press_cb == NULL) &&
(key_def->key1_long_press_cb == NULL) &&
(key_def->key2_long_press_cb == NULL)) {
tuya_ble_free((uint8_t *)sg_key_mag);
return KEY_INIT_ERR;
}
/* 端口初始化 */
gpio_set_func(key_def->key1_pin, AS_GPIO);
gpio_set_input_en(key_def->key1_pin, 1);
gpio_setup_up_down_resistor(key_def->key1_pin, PM_PIN_PULLUP_10K);
gpio_set_func(key_def->key2_pin, AS_GPIO);
gpio_set_input_en(key_def->key2_pin, 1);
gpio_setup_up_down_resistor(key_def->key2_pin, PM_PIN_PULLUP_10K);
return KEY_INIT_OK;
}
b. 按键扫描及判断处理
首先分别定义按键1和按键2的键值以及短按确认时间:
#define KEY1_CODE 0x01
#define KEY2_CODE 0x02
#define KEY_PRESS_SHORT_TIME 50
然后编写定时循环的按键状态扫描函数和按键处理函数:
/* 获取当前键值 */
static uint8_t get_key_code(void)
{
uint8_t key_code = 0;
/* 按键1 */
if (gpio_read(sg_key_mag->ts02n_key_def_s->key1_pin) == 0) {
key_code |= KEY1_CODE;
} else {
key_code &= ~KEY1_CODE;
}
/* 按键2 */
if (gpio_read(sg_key_mag->ts02n_key_def_s->key2_pin) == 0) {
key_code |= KEY2_CODE;
} else {
key_code &= ~KEY2_CODE;
}
return key_code;
}
/* 扫描按键状态 */
static void update_key_status(uint32_t time_inc)
{
uint8_t key_code;
key_code = get_key_code();
sg_key_mag->ts02n_key_status_s.prv_time = sg_key_mag->ts02n_key_status_s.cur_time;
sg_key_mag->ts02n_key_status_s.cur_time += time_inc;
if (key_code != sg_key_mag->ts02n_key_status_s.cur_code) {
sg_key_mag->ts02n_key_status_s.prv_code = sg_key_mag->ts02n_key_status_s.cur_code;
sg_key_mag->ts02n_key_status_s.cur_code = key_code;
sg_key_mag->ts02n_key_status_s.prv_time = sg_key_mag->ts02n_key_status_s.cur_time;
sg_key_mag->ts02n_key_status_s.cur_time = 0;
} else {
sg_key_mag->ts02n_key_status_s.prv_code = sg_key_mag->ts02n_key_status_s.cur_code;
}
}
/* 判断[key_code]按键按压时间是否超过[press_time] */
static uint8_t is_key_press_over_time(uint8_t key_code, uint32_t press_time)
{
if (sg_key_mag->ts02n_key_status_s.cur_code == key_code) {
if ((sg_key_mag->ts02n_key_status_s.cur_time >= press_time) &&
(sg_key_mag->ts02n_key_status_s.prv_time < press_time)) {
return 1;
}
}
return 0;
}
/* 判断[key_code]按键释放时按压时间是否少于[press_time] */
static uint8_t is_key_release_to_release_less_time(uint8_t key_code, uint32_t press_time)
{
if ((sg_key_mag->ts02n_key_status_s.prv_code == key_code) &&
(sg_key_mag->ts02n_key_status_s.cur_code != key_code)) {
if ((sg_key_mag->ts02n_key_status_s.prv_time >= KEY_PRESS_SHORT_TIME) &&
(sg_key_mag->ts02n_key_status_s.prv_time < press_time)) {
return 1;
}
}
return 0;
}
/* 检测与处理按键事件 */
static void detect_and_handle_key_event(void)
{
/* 按键1处理 */
if (sg_key_mag->ts02n_key_def_s->key1_long_press_cb != NULL) {
/* 长按 */
if (is_key_press_over_time(KEY1_CODE, sg_key_mag->ts02n_key_def_s->key1_long_press_time)) {
sg_key_mag->ts02n_key_def_s->key1_long_press_cb();
TUYA_APP_LOG_DEBUG("key1 is long pressed");
}
/* 短按 */
if (sg_key_mag->ts02n_key_def_s->key1_short_press_cb != NULL) {
if (is_key_release_to_release_less_time(KEY1_CODE, sg_key_mag->ts02n_key_def_s->key1_long_press_time)) {
sg_key_mag->ts02n_key_def_s->key1_short_press_cb();
TUYA_APP_LOG_DEBUG("key1 is pressed");
}
}
} else {
/* 短按 */
if (sg_key_mag->ts02n_key_def_s->key1_short_press_cb != NULL) {
if (is_key_press_over_time(KEY1_CODE, KEY_PRESS_SHORT_TIME)) {
sg_key_mag->ts02n_key_def_s->key1_short_press_cb();
TUYA_APP_LOG_DEBUG("key1 is pressed");
}
}
}
/* 按键2处理 */
if (sg_key_mag->ts02n_key_def_s->key2_long_press_cb != NULL) {
/* 长按 */
if (is_key_press_over_time(KEY2_CODE, sg_key_mag->ts02n_key_def_s->key2_long_press_time)) {
sg_key_mag->ts02n_key_def_s->key2_long_press_cb();
TUYA_APP_LOG_DEBUG("key2 is long pressed");
}
/* 短按 */
if (sg_key_mag->ts02n_key_def_s->key2_short_press_cb != NULL) {
if (is_key_release_to_release_less_time(KEY2_CODE, sg_key_mag->ts02n_key_def_s->key2_long_press_time)) {
sg_key_mag->ts02n_key_def_s->key2_short_press_cb();
TUYA_APP_LOG_DEBUG("key2 is pressed");
}
}
} else {
/* 短按 */
if (sg_key_mag->ts02n_key_def_s->key2_short_press_cb != NULL) {
if (is_key_press_over_time(KEY2_CODE, KEY_PRESS_SHORT_TIME)) {
sg_key_mag->ts02n_key_def_s->key2_short_press_cb();
TUYA_APP_LOG_DEBUG("key2 is pressed");
}
}
}
}
/* 按键定时循环函数 */
void ts02n_key_loop(void)
{
static uint32_t s_key_scan_tm = 0;
/* 定时时间判断 */
if (!clock_time_exceed(s_key_scan_tm, (sg_key_mag->ts02n_key_def_s->scan_time)*1000)) {
return;
}
s_key_scan_tm = clock_time(); /* 记录当前时间 */
update_key_status(sg_key_mag->ts02n_key_def_s->scan_time); /* 扫描按键状态 */
detect_and_handle_key_event(); /* 判断与处理 */
}
温度采集与处理
根据硬件方案中关于NTC温度传感器的介绍可知,通过ADC模块采集到的端口电压值即可换算出当前温度值。由于本案例使用的芯片平台不支持浮点数运算,这里我们采用查表法来获取当前温度。首先我们编写一个脚本,将NTC的R-T表中的电阻值读出并转换为电压值,然后按照顺序放入数组中存储,这样我们就可以通过查询数组将电压值转换为温度值。脚本可在tuya-iotos-embeded-demo-ble-temperature-alarm中获取。
a. 温度采集
NTC端口设置如下:
温度传感器 | 管脚 |
---|---|
温度传感器 P_NTC | ADC/GPIO_PB6 |
代码实现:
#define P_NTC GPIO_PB6 /* ADC */
#define TEMP_ARRAY_MIN_VALUE 0 /* 数组中第一个数据对应的温度 */
#define TEMP_ARRAY_SIZE 120 /* 数据个数 */
/* NTC(B3950/100K) 温度-电压对应表 */
const uint16_t vol_data_of_temp[TEMP_ARRAY_SIZE] = {
190, 199, 209, 219, 229, 240, 251, 263, 275, 288, 301, 314, 328, 342, 357, 372, 388, 404, 420, 437, /* 0 ~ 19 */
455, 473, 491, 510, 530, 549, 570, 591, 612, 634, 656, 679, 702, 725, 749, 774, 799, 824, 849, 875, /* 20 ~ 39 */
902, 928, 955, 982, 1010, 1038, 1066, 1094, 1123, 1152, 1181, 1210, 1239, 1268, 1298, 1327, 1357, 1386, 1416, 1446, /* 40 ~ 59 */
1475, 1505, 1535, 1564, 1593, 1623, 1652, 1681, 1710, 1738, 1767, 1795, 1823, 1851, 1878, 1906, 1933, 1959, 1986, 2012, /* 60 ~ 79 */
2038, 2063, 2088, 2113, 2138, 2162, 2185, 2209, 2232, 2255, 2277, 2299, 2320, 2342, 2362, 2383, 2403, 2423, 2442, 2461, /* 80 ~ 99 */
2480, 2498, 2516, 2534, 2551, 2568, 2584, 2600, 2616, 2632, 2647, 2662, 2676, 2690, 2704, 2718, 2731, 2744, 2757, 2769 /* 100 ~ 119 */
};
/* NTC端口及ADC模块初始化 */
void ntc_adc_init(void)
{
adc_init();
adc_base_init(P_NTC);
adc_power_on_sar_adc(1);
}
/* 判断电压值是否在data[num1]和data[num2]之间 */
static uint8_t is_vol_value_between(uint16_t value, uint8_t num1, uint8_t num2)
{
if ((value >= vol_data_of_temp[num1]) && (value <= vol_data_of_temp[num2])) {
return 1;
} else {
return 0;
}
}
/* 判断电压值更接近data[num1]还是data[num2] */
static uint8_t get_closer_num(uint16_t value, uint8_t num1, uint8_t num2)
{
if ((value - vol_data_of_temp[num1]) < (vol_data_of_temp[num2] - value)) {
return num1;
} else {
return num2;
}
}
/* 转换电压值为温度值 */
static uint8_t transform_vol_to_temp(uint16_t vol_value)
{
uint8_t comp_num;
uint8_t min = 0;
uint8_t max = TEMP_ARRAY_SIZE - 1;
uint8_t temp = 0;
if (vol_value <= vol_data_of_temp[min]) {
return TEMP_ARRAY_MIN_VALUE;
}
if (vol_value >= vol_data_of_temp[max]) {
return (TEMP_ARRAY_MIN_VALUE + TEMP_ARRAY_SIZE - 1);
}
while (1) {
comp_num = (max + min) / 2;
if (vol_value == vol_data_of_temp[comp_num]) {
temp = comp_num + TEMP_ARRAY_MIN_VALUE;
break;
} else if (vol_value < vol_data_of_temp[comp_num]) {
if (is_vol_value_between(vol_value, comp_num-1, comp_num)) {
temp = get_closer_num(vol_value, comp_num-1, comp_num) + TEMP_ARRAY_MIN_VALUE;
break;
} else {
max = comp_num;
}
} else {
if (is_vol_value_between(vol_value, comp_num, comp_num+1)) {
temp = get_closer_num(vol_value, comp_num, comp_num+1) + TEMP_ARRAY_MIN_VALUE;
break;
} else {
min = comp_num;
}
}
}
return temp;
}
/* 获取当前温度 */
uint8_t get_cur_temp(void)
{
uint8_t ntc_temp;
uint16_t ntc_vol_value;
/* NTC端口及ADC模块初始化(需要在每次读取前初始化) */
ntc_adc_init();
/* 读取A/D转换结果(这里得到的直接是单位为mV的电压值) */
ntc_vol_value = (uint16_t)adc_sample_and_get_result();
TUYA_APP_LOG_DEBUG("voltage: %d", ntc_vol_value);
/* 将电压值转换为温度值 */
ntc_temp = transform_vol_to_temp(ntc_vol_value);
TUYA_APP_LOG_DEBUG("temperature: %d", ntc_temp);
return ntc_temp;
}
应用层功能实现
在以上相关驱动代码实现后,下面进行智能烧水壶相关功能的实现。
a. 初始化函数与主循环函数的编写
/* 初始化函数 【在tuya_ble_app_demo.c的tuya_ble_app_init()函数中调用】 */
void tuya_app_kettle_init(void)
{
memset(&g_kettle, 0, sizeof(g_kettle)); /* 变量初始化 */
memset(&g_kettle_flag, 0, sizeof(g_kettle_flag)); /* 标志初始化 */
set_keep_warm_temp(TEMP_KEEP_WARM_DEFAULT); /* 设置默认保温温度 */
led_init(); /* 指示灯端口初始化 */
relay_init(); /* 继电器端口初始化 */
buzzer_pwm_init(); /* 蜂鸣器端口及PWM模块初始化 */
ntc_adc_init(); /* 温度传感器端口及ADC模块初始化 */
ts02n_key_init(&user_ts02n_key_def_s); /* 按键注册并初始化 */
ble_connect_status_init(); /* 蓝牙连接状态初始化(在下一节介绍) */
}
/* 主循环函数 【在tuya_ble_app_demo.c的app_exe()函数中调用】 */
void tuya_app_kettle_loop(void)
{
update_ble_status(); /* 蓝牙连接状态更新(在下一节介绍) */
update_cur_temp(); /* 温度更新处理 */
ts02n_key_loop(); /* 按键循环处理 */
update_kettle_mode(); /* 模式更新处理 */
update_led_green_status(); /* 指示灯定时处理 */
update_buzzer_status(); /* 蜂鸣器定时处理 */
}
b. 温度更新与故障检测
每2秒更新一次温度值,如果发生温度变化,则将数据更新上报至云端,并进行一次故障检测;如果故障状态变化,更新故障信息至云端:
#define TEMP_UPPER_LIMIT 105 /* 报警温度阈值 */
#define TIME_GET_TEMP 2000 /* 温度更新间隔:2s */
typedef BYTE_T FAULT_E; /* 故障 */
#define FAULT_NORMAL 0x00 /* 正常 */
#define FAULT_LACK_WATER 0x01 /* 缺水 */
/* 更新故障信息 */
static void update_fault(FAULT_E fault)
{
g_kettle.fault = fault;
report_one_dp_data(DP_ID_FAULT, g_kettle.fault);
TUYA_APP_LOG_DEBUG("fault: %d", g_kettle.fault);
}
/* 烧水壶停止工作 */
static void stop_kettle(void)
{
set_boil_turn(OFF);
set_keep_warm_turn(OFF);
set_led_red(OFF);
set_led_orange(OFF);
set_led_green_status(OFF);
set_relay(OFF);
}
/* 故障检测处理 */
static void detect_and_handle_fault_event(void)
{
if (g_kettle.fault == FAULT_NORMAL) {
if (g_kettle.temp_cur >= TEMP_UPPER_LIMIT) { /* 超过温度上限时 */
update_fault(FAULT_LACK_WATER); /* 更新故障状态为缺水干烧 */
set_buzzer_mode(BUZZER_MODE_FAULT); /* 蜂鸣器长鸣 */
stop_kettle(); /* 停止工作 */
}
} else {
if (g_kettle.temp_cur < TEMP_UPPER_LIMIT) { /* 温度恢复后 */
update_fault(FAULT_NORMAL); /* 更新故障状态为无故障 */
set_buzzer_mode(BUZZER_MODE_STOP); /* 蜂鸣器停止 */
set_work_mode(MODE_NATURE); /* 切换至自然模式 */
}
}
}
/* 更新当前温度 */
static void update_cur_temp(void)
{
uint8_t temp;
static uint32_t s_get_temp_tm = 0;
/* 2秒定时 */
if (!clock_time_exceed(s_get_temp_tm, TIME_GET_TEMP*1000)) {
return;
}
s_get_temp_tm = clock_time();
/* 获取当前温度 */
temp = get_cur_temp();
if (g_kettle.temp_cur != temp) { /* 温度变化? */
g_kettle.temp_cur = temp; /* 更新当前温度 */
report_one_dp_data(DP_ID_TEMP_CUR, g_kettle.temp_cur);
detect_and_handle_fault_event(); /* 故障检测处理 */
}
}
c. 按键注册与事件响应处理
按键管脚及相关操作的主要响应内容设置如下:
按键 | 管脚 | 操作 | 响应 |
---|---|---|---|
煮沸键 P_KEY_BOIL | P7/GPIO_PC3 | 轻触 | 打开/关闭煮沸功能 |
保温键 P_KEY_KEEP | P8/GPIO_PC2 | 轻触 长按5秒 |
打开/关闭保温功能 进入配网状态 |
按键信息注册如下:
#define P_KEY_BOIL GPIO_PC3
#define P_KEY_KEEP GPIO_PC2
/* 用户按键信息注册 */
TS02N_KEY_DEF_T user_ts02n_key_def_s = {
.key1_pin = P_KEY_BOIL, /* P7 */
.key2_pin = P_KEY_KEEP, /* P8 */
.key1_short_press_cb = key_boil_short_press_cb_fun, /* 煮沸键轻触处理 */
.key2_short_press_cb = key_keep_short_press_cb_fun, /* 保温键轻触处理 */
.key1_long_press_cb = NULL, /* 无煮沸键长按功能 */
.key2_long_press_cb = key_keep_long_press_cb_fun, /* 保温键长按5秒处理 */
.key1_long_press_time = 0, /* 无煮沸键长按功能 */
.key2_long_press_time = 5000, /* 5s */
.scan_time = 10, /* 10ms */
};
/* 切换煮沸开/关状态 */
static void switch_boil_turn(void)
{
if (g_kettle.boil_turn == ON) {
set_boil_turn(OFF); /* 关闭煮沸功能(具体执行内容在[云端控制(3)]中介绍) */
} else {
set_boil_turn(ON); /* 打开煮沸功能 */
}
}
/* 切换保温开/关状态 */
static void switch_keep_warm_turn(void)
{
if (g_kettle.keep_warm_turn == ON) {
set_keep_warm_turn(OFF);/* 关闭保温功能(具体执行内容在[云端控制(3)]中介绍) */
} else {
set_keep_warm_turn(ON); /* 打开保温功能 */
}
}
/* 煮沸键轻触处理 */
void key_boil_short_press_cb_fun(void)
{
if (g_kettle.fault != FAULT_NORMAL) { /* 故障发生时按键无效 */
return;
}
switch_boil_turn(); /* 切换煮沸开/关状态 */
set_buzzer_mode(BUZZER_MODE_ONCE); /* 设置蜂鸣器模式为“滴一声” */
}
/* 保温键轻触处理 */
void key_keep_short_press_cb_fun(void)
{
if (g_kettle.fault != FAULT_NORMAL) { /* 故障发生时按键无效 */
return;
}
switch_keep_warm_turn(); /* 切换保温开/关状态 */
set_buzzer_mode(BUZZER_MODE_ONCE); /* 设置蜂鸣器模式为“滴一声” */
}
/* 保温键长按5秒处理 */
void key_keep_long_press_cb_fun(void)
{
if (F_BLE_BONDING == SET) { /* 已被用户绑定时无效 */
return;
}
try_to_connect_ble(); /* 尝试配网(具体执行内容在[云端控制(1)]中介绍) */
set_buzzer_mode(BUZZER_MODE_ONCE); /* 设置蜂鸣器模式为“滴一声” */
}
d. 模式更新与处理
根据功能设定,我们将烧水壶的工作模式拆分为以下4个模式:
模式 | 条件 | 状态 |
---|---|---|
自然模式 | 煮沸、保温功能均关闭 | 指示灯、加热均关闭 |
煮沸模式 | 煮沸功能打开 | 红灯亮、橙灯灭、绿灯灭、加热打开 |
保温模式1 | 煮沸功能关闭,保温功能打开,自来水模式 | 红灯灭、橙灯亮、绿灯灭、加热打开 |
保温模式2 | 煮沸功能关闭,保温功能打开,纯净水模式 | 红灯灭、橙灯/绿灯/加热根据当前温度设定状态 |
代码实现:
/* 工作模式 */
typedef BYTE_T MODE_E;
#define MODE_NATURE 0x00
#define MODE_BOIL 0x01
#define MODE_KEEP_WARM1 0x02
#define MODE_KEEP_WARM2 0x03
/* 用水类型 */
typedef BYTE_T WATER_TYPE_E;
#define WATER_TYPE_TAP 0x00
#define WATER_TYPE_PURE 0x01
/* 温度相关 */
#define TEMP_BOILED 97 /* 煮沸温度 */
#define TEMP_KEEP_WARM_DEFAULT 55 /* 默认保温温度 */
/* 自然模式 */
static void kettle_mode_nature(void)
{
set_led_red(OFF);
set_led_orange(OFF);
set_led_green_status(OFF);
set_relay(OFF);
}
/* 煮沸模式 */
static void kettle_mode_boil(void)
{
if (g_kettle.temp_cur >= TEMP_BOILED) { /* 达到煮沸温度时 */
set_water_type(WATER_TYPE_PURE); /* 更新用水类型为纯净水 */
set_boil_turn(OFF); /* 关闭煮沸功能 */
set_relay(OFF); /* 关闭加热 */
} else {
set_relay(ON); /* 打开加热 */
}
}
/* 保温模式1 */
static void kettle_mode_keep_warm1(void)
{
if (g_kettle.temp_cur >= TEMP_BOILED) { /* 达到煮沸温度时 */
set_water_type(WATER_TYPE_PURE); /* 更新用水类型为纯净水 */
set_relay(OFF); /* 关闭加热 */
} else {
set_relay(ON); /* 打开加热 */
}
}
/* 保温模式2 */
static void kettle_mode_keep_warm2(void)
{
/* 未达到保温温度时,橙灯亮,绿灯灭,温度高时加热关闭,温度低时加热打开 */
if (g_kettle.temp_cur > g_kettle.temp_set) {
set_led_orange(ON);
set_led_green_status(OFF);
set_relay(OFF);
} else if (g_kettle.temp_cur < (g_kettle.temp_set - 3)) {
set_led_orange(ON);
set_led_green_status(OFF);
set_relay(ON);
/* 达到保温温度时,橙灯灭,绿灯亮,加热关闭 */
} else {
set_led_orange(OFF);
set_led_green_status(ON);
set_relay(OFF);
}
}
/* 运行各模式 */
static void run_kettle(void)
{
if (g_kettle.fault != FAULT_NORMAL) {
return;
}
switch (g_kettle.mode) {
case MODE_NATURE:
kettle_mode_nature();
break;
case MODE_BOIL:
kettle_mode_boil();
break;
case MODE_KEEP_WARM1:
kettle_mode_keep_warm1();
break;
case MODE_KEEP_WARM2:
kettle_mode_keep_warm2();
break;
default:
break;
}
}
/* 模式更新 */
static void update_kettle_mode(void)
{
if (g_kettle.boil_turn == ON) { /* 煮沸功能打开时 */
set_work_mode(MODE_BOIL); /* 进入煮沸模式 */
} else if (g_kettle.keep_warm_turn == ON){ /* 煮沸功能关闭、保温功能打开时 */
if (g_kettle.water_type == WATER_TYPE_TAP) {/* 使用自来水时 */
set_work_mode(MODE_KEEP_WARM1); /* 进入保温模式1(先煮沸再保温) */
} else { /* 使用纯净水时 */
set_work_mode(MODE_KEEP_WARM2); /* 进入保温模式2(直接保温) */
}
} else { /* 煮沸、保温功能都关闭时 */
set_work_mode(MODE_NATURE); /* 进入自然模式 */
}
run_kettle();
}
配网状态检测
要实现和云端进行数据交互,首先要使设备配网。根据功能简述中介绍可知,我们需要先进行以下配网检测处理:
设备状态 | 执行动作 | 绿灯提示 |
---|---|---|
上电时,检测到已被用户绑定 | 不等待配网 | 不闪烁 |
上电时,检测到未被用户绑定 | 开始等待配网 | 开始闪烁 |
长按保温键5秒时,检测到未被用户绑定 | 开始等待配网 | 开始闪烁 |
开始等待配网后3分钟内,检测到已被用户连接 | 停止等待配网 | 停止闪烁 |
开始等待配网3分钟后,检测到仍未被用户绑定 | 停止等待配网,停止蓝牙广播 | 停止闪烁 |
下面我们介绍配网状态检测与处理的实现过程。
a. 在上电初始化时,先对蓝牙连接状态进行判断和标记:
/* 蓝牙连接状态初始化 */
static void ble_connect_status_init(void)
{
tuya_ble_connect_status_t ble_conn_sta;
/* 使用TUYA BLE SDK提供的API获取当前蓝牙连接状态 */
ble_conn_sta = tuya_ble_connect_status_get();
TUYA_APP_LOG_DEBUG("ble connect status: %d", ble_conn_sta);
/* 判断与标记 */
if ((ble_conn_sta == BONDING_UNCONN) ||
(ble_conn_sta == BONDING_CONN) ||
(ble_conn_sta == BONDING_UNAUTH_CONN)) {
F_BLE_BONDING = SET; /* 标记为已绑定 */
F_WAIT_BLE_CONN = CLR; /* 标记为无需等待 */
set_led_green_mode(LED_MODE_FIX); /* 绿灯不闪烁 */
} else {
F_BLE_BONDING = CLR; /* 标记为未绑定 */
F_WAIT_BLE_CONN = SET; /* 标记为开始等待 */
set_led_green_mode(LED_MODE_TWINKLE); /* 绿灯开始闪烁 */
}
}
b. 在主循环中执行等待蓝牙连接与3分钟计时时间达到后关闭蓝牙广播的过程:
#define TIME_ALLOW_CONNECT (3*60*1000) /* 3min */
static uint32_t sg_ble_tm = 0;
FLAG_BIT g_kettle_flag;
#define F_BLE_BONDING g_kettle_flag.bit0
#define F_WAIT_BLE_CONN g_kettle_flag.bit1
/* 等待蓝牙连接 */
static void wait_ble_connect(void)
{
/* 3分钟定时 */
if (!clock_time_exceed(sg_ble_tm, TIME_ALLOW_CONNECT*1000)) {
return;
}
F_WAIT_BLE_CONN = CLR; /* 关闭等待连接标志 */
set_led_green_mode(LED_MODE_FIX); /* 绿灯停止闪烁 */
bls_ll_setAdvEnable(0); /* 关闭蓝牙广播 */
}
/* 更新蓝牙状态 */
static void update_ble_status(void)
{
if (F_BLE_BONDING == CLR) { /* 未绑定? */
if (F_WAIT_BLE_CONN == SET) { /* 等待连接标志打开? */
wait_ble_connect(); /* 等待蓝牙连接 */
}
}
}
c. 在蓝牙连接状态发生改变时做如下处理:
/* 蓝牙连接状态改变时的处理函数 */
void tuya_app_kettle_ble_connect_status_change_handler(tuya_ble_connect_status_t status)
{
if (status == BONDING_CONN) { /* 蓝牙已连接? */
report_all_dp_data(); /* 上报所有DP数据,保证APP显示与设备状态一致 */
if (F_WAIT_BLE_CONN == SET) { /* 等待连接标志打开? */
F_BLE_BONDING = SET; /* 标记为已绑定 */
F_WAIT_BLE_CONN = CLR; /* 停止配网等待 */
set_led_green_mode(LED_MODE_FIX); /* 停止闪烁 */
}
}
if (status == UNBONDING_UNCONN) { /* 设备被解绑? */
F_BLE_BONDING = CLR; /* 标记为未绑定 */
bls_ll_setAdvEnable(0); /* 停止蓝牙广播 */
}
}
/* 处理BLE SDK消息的callback函数 [tuya_ble_app_demo.c] */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_CONNECTE_STATUS: /* 在状态改变事件发生时调用 */
tuya_app_kettle_ble_connect_status_change_handler(event->connect_status);
TUYA_APP_LOG_INFO("received tuya ble conncet status update event, current connect status = %d", event->connect_status);
break;
...
}
d. 保温键长按5秒时,重新尝试等待配网的执行函数如下:
/* 尝试配网 */
static void try_to_connect_ble(void)
{
F_WAIT_BLE_CONN = SET; /* 打开等待连接标志 */
set_led_green_mode(LED_MODE_TWINKLE); /* 绿灯闪烁 */
bls_ll_setAdvEnable(1); /* 打开蓝牙广播 */
sg_ble_tm = clock_time(); /* 记录当前时间 */
}
本地数据上报
在设备配网后,就可以使用APP来控制设备和查看设备上报的数据,下面是数据上报的实现过程。
/* DP ID */
#define DP_ID_BOIL 101
#define DP_ID_KEEP_WARM 102
#define DP_ID_TEMP_CUR 103
#define DP_ID_TEMP_SET 104
#define DP_ID_WATER_TYPE 105
#define DP_ID_FAULT 106
/* DP TYPE */
#define DP_TYPE_BOIL DT_BOOL
#define DP_TYPE_KEEP_WARM DT_BOOL
#define DP_TYPE_TEMP_CUR DT_VALUE
#define DP_TYPE_TEMP_SET DT_VALUE
#define DP_TYPE_WATER_TYPE DT_ENUM
#define DP_TYPE_FAULT DT_ENUM
/* 用于数据上报 */
typedef struct {
uint8_t id; /* DP点ID */
dp_type type; /* DP点数据类型 */
uint8_t len; /* DP点数据长度 */
uint8_t value; /* DP点数据 */
} DP_DATA_T;
/* 用于存储烧水壶模式和DP点参数 */
typedef struct {
MODE_E mode; /* 模式 */
uint8_t boil_turn; /* 煮沸开/关 */
uint8_t temp_cur; /* 当前温度 */
uint8_t temp_set; /* 保温温度 */
uint8_t keep_warm_turn; /* 保温开/关 */
WATER_TYPE_E water_type;/* 用水类型 */
FAULT_E fault; /* 故障 */
} KETTLE_T;
KETTLE_T g_kettle;
/* 获取DP点类型 */
static uint8_t get_dp_type(uint8_t dp_id)
{
dp_type type = 0;
switch (dp_id) {
case DP_ID_BOIL:
type = DP_TYPE_BOIL;
break;
case DP_ID_TEMP_CUR:
type = DP_TYPE_TEMP_CUR;
break;
case DP_ID_TEMP_SET:
type = DP_TYPE_TEMP_SET;
break;
case DP_ID_KEEP_WARM:
type = DP_TYPE_KEEP_WARM;
break;
case DP_ID_WATER_TYPE:
type = DP_TYPE_WATER_TYPE;
break;
case DP_ID_FAULT:
type = DP_TYPE_FAULT;
break;
default:
break;
}
return type;
}
/* 上报一个DP点 */
static void report_one_dp_data(uint8_t dp_id, uint8_t dp_value)
{
DP_DATA_T dp_data_s;
dp_data_s.id = dp_id;
dp_data_s.type = get_dp_type(dp_id);
dp_data_s.len = 0x01;
dp_data_s.value = dp_value;
/* 使用TUYA BLE SDK提供的API上报DP点数据 */
tuya_ble_dp_data_report((uint8_t *)&dp_data_s, sizeof(DP_DATA_T));
}
/* 上报所有DP点 */
static void report_all_dp_data(void)
{
report_one_dp_data(DP_ID_BOIL, g_kettle.boil_turn);
report_one_dp_data(DP_ID_KEEP_WARM, g_kettle.keep_warm_turn);
report_one_dp_data(DP_ID_TEMP_CUR, g_kettle.temp_cur);
report_one_dp_data(DP_ID_TEMP_SET, g_kettle.temp_set);
report_one_dp_data(DP_ID_WATER_TYPE, g_kettle.water_type);
report_one_dp_data(DP_ID_FAULT, g_kettle.fault);
}
接收数据处理
在 App 上改变设备状态时,会从云端下发控制数据,设备在接收到数据后进行如下处理,即可实现云端任务。
/* 设置煮沸功能开/关 */
static void set_boil_turn(uint8_t on_off)
{
if (g_kettle.fault != FAULT_NORMAL) { /* 故障发生时不执行 */
return;
}
g_kettle.boil_turn = on_off; /* 煮沸功能打开/关闭 */
report_one_dp_data(DP_ID_BOIL, g_kettle.boil_turn);
TUYA_APP_LOG_DEBUG("boil turn: %d", g_kettle.boil_turn);
set_led_red(on_off); /* 红色LED点亮/关闭 */
}
/* 设置保温功能开/关 */
static void set_keep_warm_turn(uint8_t on_off)
{
if (g_kettle.fault != FAULT_NORMAL) { /* 故障发生时不执行 */
return;
}
g_kettle.keep_warm_turn = on_off; /* 保温功能打开/关闭 */
report_one_dp_data(DP_ID_KEEP_WARM, g_kettle.keep_warm_turn);
TUYA_APP_LOG_DEBUG("keep warm turn: %d", g_kettle.keep_warm_turn);
set_led_orange(on_off); /* 橙色LED点亮/关闭 */
}
/* 设置保温温度 */
static void set_keep_warm_temp(uint8_t temp)
{
if (g_kettle.temp_set != temp) {
if ((temp >= 45) && (temp <= 90)) {
g_kettle.temp_set = temp; /* 在保温温度设定范围内时才更新参数值 */
}
}
}
/* 设置用水类型 */
static void set_water_type(WATER_TYPE_E type)
{
g_kettle.water_type = type;
}
/* DP点数据接收处理 */
void tuya_app_kettle_dp_data_handler(uint8_t *dp_data)
{
switch (dp_data[0]) {
case DP_ID_BOIL:
set_boil_turn(dp_data[3]);
*(dp_data + 3) = g_kettle.boil_turn;
break;
case DP_ID_KEEP_WARM:
set_keep_warm_turn(dp_data[3]);
*(dp_data + 3) = g_kettle.keep_warm_turn;
break;
case DP_ID_TEMP_SET:
set_keep_warm_temp(dp_data[6]);
*(dp_data + 6) = g_kettle.temp_set; /* VALUE类型下发时数据长度为4字节 */
break;
case DP_ID_WATER_TYPE:
set_water_type(dp_data[3]);
*(dp_data + 3) = g_kettle.water_type;
break;
case DP_ID_TEMP_CUR:
case DP_ID_FAULT:
default:
break;
}
}
/* 处理BLE SDK消息的callback函数 [tuya_ble_app_demo.c] */
static void tuya_cb_handler(tuya_ble_cb_evt_param_t* event)
{
...
case TUYA_BLE_CB_EVT_DP_WRITE: /* 在接收到DP数据时调用 */
dp_data_len = event->dp_write_data.data_len;
memset(dp_data_array, 0, sizeof(dp_data_array));
memcpy(dp_data_array, event->dp_write_data.p_data, dp_data_len);
tuya_app_kettle_dp_data_handler(dp_data_array);
TUYA_APP_LOG_HEXDUMP_DEBUG("received dp write data :", dp_data_array, dp_data_len);
sn = 0;
tuya_ble_dp_data_report(dp_data_array, dp_data_len);
break;
...
}
预约功能实现
预约功能可通过 云定时 功能实现,即在涂鸦IoT平台上产品开发的 功能定义 页中,打开高级云功能的 云定时 功能:
打开云定时功能后,还需在设备面板中修改云定时功能的属性,配置煮沸这个 DP 点,就可以在APP中设置定时煮沸任务,到达定时时间后云端就会下发控制命令触发 dp_boil 这个 DP 点数据下发,从而触发烧水壶执行煮沸功能。
至此智能恒温烧水壶就完成了,它可以APP远程控制,按键控制。具有水质模式切换,保温温度设定,定时烧水,故障告警等功能。在这款智能烧水壶的基础上还有很多功能可以深入开发,使体验更加人性化,智能化。同时您可以基于涂鸦 IoT 平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。
该内容对您有帮助吗?
是我要提建议