
为什么越来越多的人喜欢买智能恒温电水壶呢?智能电水壶有哪些优势呢?吸引大家的原因是什么呢?我觉得应该是功能好,操作简单,价格适中,如果可以和家庭设备一起联动,那样就更好了。那既然大家都如此钟爱智能恒温电水壶,又是生活中必不可少的部分,那我们赶紧研究一款便捷智能的电水壶吧!不仅可以满足自己的需要,也能满足大家的需求。
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 平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。
该内容对您有帮助吗?
是我要提建议