使用不同通讯协议模组开发智能烧水壶

更新时间Invalid date

概况

为什么越来越多的人喜欢买智能恒温电水壶呢?智能电水壶有哪些优势呢?吸引大家的原因是什么呢?我觉得应该是功能好,操作简单,价格适中,如果可以和家庭设备一起联动,那样就更好了。那既然大家都如此钟爱智能恒温电水壶,又是生活中必不可少的部分,那我们赶紧研究一款便捷智能的电水壶吧!不仅可以满足自己的需要,也能满足大家的需求。

功能设计

  • App 远程控制,触摸按键控制。

  • 恒温控制,保温模式可选,预约定时控制,煮水模式可选。

  • 干烧断电,故障告警。

硬件框架

物料清单

硬件 (7)软件 (7)
  • Wi-Fi&BLE 模组

    数量:1

    适用于 Wi-Fi&BLE 模组方案查看详情

  • BLE 模组

    数量:1

    适用于 BLE 模组方案查看详情

  • NTC 温度传感器

    数量:1

    日本芝浦/华工高理温度传感器,感温精确达到±1%

  • STRIX 温控器

    数量:1

  • 蜂鸣器

    数量:1

    用于煮沸时产生蜂鸣

  • 触摸按键

    数量:1

  • 按键检测芯片

    数量:1

    选用 TS02N 作为按键检测芯片

步骤

  • 第 1 步:硬件方案设计

    本文采纳了两种不同通信协议的模组进行开发,其主控板分别使用了涂鸦智能的低功耗嵌入式 Wi-Fi+BLE 双协议云模组BLE 云模组 进行开发。两种模组的硬件设计方案一致,软件设计方案有所区别(将在下文进行介绍)。

    电源设计

    原理图

    主控设计

    原理图

    PCB

    功能模块

    电源设计电路

    电源系统带有各种保护,包括过温保护(OTP)、VCC 欠压锁定保护(UVLO)、过载保护(OLP)、短路保护(SCP)和开环保护。该电路是 220V 转 5V 降压电路,输入级由保险电阻 F2、防雷压敏电阻 RV1、整流桥堆 D3、EMI 滤波电容 C5 和 C6 以及滤波电感 L1 组成。保险电阻 F2 为阻燃可熔的绕线电阻,它同时具备多个功能:

    • 将桥堆D3的浪涌电流限制在安全的范围;
    • 差模噪声的衰减;
    • 在其它任何元件出现短路故障时,充当输入保险丝的功能(元件故障时必须安全开路,不应产生任何冒烟、冒火及过热发光现象)。压敏电阻RT1用于防雷保护,提高系统可靠性。功率处理级由宽电压高效率电源芯片MP174A 、续流二极管 D5、输出电感 L2 及输出电容 C4 构成。

    电路特点

    无噪音,发热低。

    NTC 温度传感器

    ​精准控温使用的是日本芝浦/华工高理温度传感器,感温精确达到±1%(B=3950, R=10K)。

    温度检测电路图如下:

    • 设备采用了100K负温度系数的温度探头,温度为100 度时系数为 6.6k 左右,而 0 度时在 300k 左右,常温下 100K 左右。
    • R5 和 R7 在电源板上,其余部分在主控板上。温度采集使用的是 NTC 热敏电阻,热敏电阻在不同温度下有不同的阻值,根据此特性,模组通过 ADC 口采集此时的电压,从而换算出此时的温度值。
    • R8 为 20k 的定值电阻,R7 为热敏电阻(常温下阻值为 100K),ADC 为电压采样点。当采集电压后,根据欧姆定律即可算出热敏电阻的阻值。

    STRIX 温控器

    温控器是一个电水壶的心脏。在鸣笛式电水壶设计原理基础上,增设了限温自动控制器(双金属 恒温控制器 或者磁控恒温自动开关)。当水沸腾时,产生的水蒸气使感温元件的 双金属片 变形,驱动微型开关切断电源,极大地提高了电热水壶的安全性能。一些高品质的电热水壶中的限温控制器采用一种类似 记忆合金 的新型热敏材料(自动恒温控制开关),当壶内水面低于电热管(电热管温度超过100°C)时,自动恒温控制开关便会自动切断电源,保护电热水壶不被烧毁。

    下图为英国 STRIX 温控器正反面照片:

    蜂鸣器

    本方案使用了 无源蜂鸣器,它由外部驱动,也称他激式蜂鸣器。
    其发声原理是:方波信号输入后,由谐振装置转换为声音信号输出。

    无源蜂鸣器控制电路如下所示:

    其中,

    • 电阻 R3 在主控板上,其余部分在电源板上。R3 的作用主要是限流,同时也起到了一定的抗干扰能力。
    • 无源蜂鸣器外部是不带振荡源的,如果让蜂鸣器正常工作,必须给频率500Hz~4.5KHz 之间的脉冲信号来驱动它。蜂鸣器电流较大,因而需要用三极管来驱动,并且为此加了一个 510 欧的电阻(R3)作为限流电阻。
    • D4是续流二极管,产生续流作用,其作用为防止突然断电时产生的高压反向电动势击穿其他元件以及使用寿命缩短。

    预留的管脚

    预留该管脚,用作后续功能扩展。

    触摸按键

    ​本方案设计中在触摸焊盘上加了导电泡棉,增强导电性。

    导电泡棉

    触摸按键

    按键检测芯片

    选用 TS02N 作为按键检测芯片。TS02N 是双通道电容式传感器,具有自动灵敏度校准功能,其电源电压范围为2.5 ~ 5.5V。通过并联输出端口(OUT1 和 OUT2),芯片由低电平触发,可以检测触摸感知的结果。由于有了 SYNC 功能,两个 TS02N 可以同时在一个应用程序上工作。

    管脚分配

    管脚 作用
    P7 煮沸键
    P8 保温键

    TS02N 使用说明

    触摸焊盘 P1 时,P8 输出低电平;同理,触摸焊盘 P2 时,P7 输出低电平。

    触摸检测电路

    电气原理图

    主控板上设有主控芯片,无线信号接收及发生装置与主控芯片集成为一体或相互独立。

    主控板上设有煮沸控制开关和保温控制开关。

    电源板上设有继电器,继电器包括继电器线圈和继电器开关,继电器线圈通过电源板与主控板电性连接,继电器开关与加热电路电性连接、并控制加热电路的通断。

    整机实物图

  • 第 2 步:创建产品

    1. 登录 涂鸦 IoT 平台

    2. 选择 创建产品

    3. 在页面左下角,选择 找不到品类?

    4. 自定义创建 区域内,填写参数后单击 创建产品

      • 产品名称、产品描述、产品型号:用户自定义填写。
      • 通讯协议:本方案包含两种模组的嵌入式开发,因此此处可以选择 Wi-Fi-蓝牙蓝牙

    5. 功能定义 页签中,添加自定义功能。

      本方案中需要添加以下功功能点:煮沸、温度设置、当前温度、保温温度、用水类型、故障警告。

    6. 设备面板 页签中,按照界面提示选择 App 面板。调试阶段推荐选择开发调试面板便于测试。

    7. 硬件开发 页签中,选择 涂鸦标准模组SDK开发

    8. 选择一款模组后,选择右下角 免费领取10个激活码 获取对应的 UUID、authkey 以及 MAC地址,以便填入后续的SDK中。

  • 第 3 步:搭建开发环境

    Wi-Fi&BLE 方案环境搭建

    Wi-Fi&BLE 模组方案基于 BK7231N 平台进行 SoC 开发。开发所用的涂鸦通用 SDK 编译需要 Linux 环境,因此需要先安装 Linux 开发环境,然后从涂鸦仓库拉取包含 SDK 环境的 Demo 例程。

    1. 下载 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
      
    2. 在现有的 Demo 基础上搭建系统框架。

      当前 tuya_demo_template 的文件组成如下:

      ├── src	
      |    └── tuya_device.c            //应用层入口文件
      |
      ├── include				//头文件目录
      |    └──  tuya_device.h
      |
      └── output              //编译产物
      
    3. tuya_demo_template 文件夹更名为 bk7231n_smart_kettle_demo

    4. 并更改 tuya_device.h 文件中的代码,填入产品创建步骤中获取的 PID,该步骤主要作用是配网后同步手机端的 App 界面。

    编译与烧录

    1. 在 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 文件为我们要烧录到板子中的应用固件。

    2. 下载、安装并启动 BK7231T 芯片烧录工具,烧录对应的 .bin 文件。

      注意:烧录时需按下复位键,单击 烧录 后再松开复位键,此时可以看见烧录进度条在移动,最终烧录完成,具体步骤如下图:

      更多烧录授权信息请参考:Wi-Fi + BLE 系列模组烧录授权

    BLE 方案环境搭建

    BLE 模组方案使用涂鸦 BLE SDK 和 Telink 芯片平台 TLSR825x 进行开发,以下为搭建开发环境的步骤。

    获取 BLE SDK

    下载 TLSR825x 对应的BLE SDK Demo:tuya_ble_sdk_Demo_Project_tlsr8253.git

    我们将在\ble_sdk_multimode\tuya_ble_app中进行智能烧水壶应用代码的编写。

    下载并安装 IDE

    下载 Telink 官方 IDE 并安装:Eclipse (IDE for TLSR8 Chips)注意!必须安装在C盘

    修改代码并编译

    将代码导入 Eclipse,可以直接在 Eclipse 进行代码修改,也可以先使用自己熟悉的代码编辑器。

    1. 修改 PID。

      tuya_ble_app_demo.h 中填入创建产品过程中获取的智能烧水壶的 PID。

      #define APP_PRODUCT_ID      "ihqaiy9c"
      
    2. 修改 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 */
       ```
      
    3. 修改以下代码使授权码生效。

      tuya_ble_app_demo.c 中找到 tuya_ble_app_init()函数,将device_param.device_id_len = 0;device_param.device_id_len = 16;(可以参考此行代码的注释)。

    4. 修改日志口。

      由于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
      
    5. 编译代码。

      使用Eclipse对代码进行编译,输出文件目录为ble_sdk_multimode\8258_module\8258_module.bin;编译前需修改工程配置中的头文件包含路径,根据SDK中的文件夹名称进行相应修改,修改方法参考下图:

    下载并安装烧录工具

    下载 Telink 官方烧录工具并安装:Burning and Debugging Tools for all Series

    下载并调试程序

    1. 按下图链接电路板与 Telink 烧录器。

    2. 打开 BDT,按下图步骤进行芯片选择、打开文件后,下载并复位。

      注意

      • 如果需要烧录的文件路径不变,重新编译后不需要重复”打开文件“这一步。
      • 如果下载时提示“Swire err”可以点击“SWS”进行刷新。

      程序载入后,使用串口调试工具进行日志查看,将波特率设置为 230400,并按照之前修改的日志口进行连接。

      复位后将看到以下日志输出,包括我们之前修改的 PID、uuid 、Auth key、Mac 地址,以及版本号等其他信息:

    3. 程序启动后,可以使用涂鸦智能 App 搜索到我们的设备并进行绑定,可以看到设备名称显示为我们创建的产品名称”智能烧水壶“。

      至此,智能烧水壶 Demo 开发前的准备工作就完成了。

  • 第 4 步:软件方案设计

    常见的烧水壶一般有煮沸、保温功能,本案例在此基础上,使用涂鸦的模组和 SDK 实现了对烧水壶的远程控制,包括保温温度设置、水质模式选择、预约烧水等功能,另外还增加了干烧报警等功能,使烧水壶更加智能化。

    本案例中的智能烧水壶设备上提供煮沸、保温2个按键,以及3个指示灯和1个蜂鸣器用于状态提示,可以本地控制也可以远程控制,具体功能设定如下:

    功能 说明
    煮沸 轻触煮沸键,蜂鸣器“滴”一声提醒,切换煮沸功能开/关;
    煮沸功能打开时,红灯亮,加热打开直至达到煮沸温度;
    煮沸功能关闭或煮沸完成时,红灯灭,加热关闭。
    保温 轻触保温键,蜂鸣器“滴”一声提醒,切换保温功能开/关;
    保温功能打开且未达到保温温度时,橙灯亮,绿灯灭,加热根据当前温度打开或关闭;
    保温功能关闭或达到保温温度时,橙灯灭,绿灯亮,加热关闭;
    用水类型为自来水时,先加热至煮沸再保温至保温温度;
    用水类型为纯净水时,直接加热至保温温度;
    保温温度默认为 55 ℃,可通过 App 设置,设置范围在45~90℃;
    用水类型默认为自来水,可通过 App 设置。
    干烧报警 检测到干烧后自动关闭加热,硬件断电,蜂鸣器长鸣报警。
    配网 上电配网,3分钟后未配网成功,仅可本地控制;
    长按保温键 5 秒进入配网状态,3 分钟后未配网成功,仅可本地控制;
    等待配网时,绿灯快闪。
    远程控制 可通过 App 操作的项目有:打开/关闭煮沸功能和保温功能、设置保温温度和用水类型、查看当前温度和故障状态、预约烧水时间。

    下面我们将介绍实现上述功能的具体方案和过程。

    Wi-Fi&BLE 方案软件设计

    完整示例代码可在 Wi-Fi&BLE 模组方案代码 中获取。

    温度显示和过温报警

    温度采集原理

    温度采集原理图如下:

    温度采集方案是使用热敏电阻,热敏电阻在不同温度下有不同的阻值,根据此特性,通过电路设计和软件程序配合采集到热敏电阻的阻值,从而计算出当前的温度值。

    采样电路图如下:R8 为 20k 的定值电阻,R7 为热敏电阻(常温下阻值为 100k)。ADC 是电压采样点,采集电压后,根据欧姆定律即可算出热敏电阻的阻值。

    1. 得到热敏电阻阻值 Rt,根据 B3950 的热敏曲线即可算出当前温度值。

      图中参数含义如下所示:

      参数 说明
      R0 25°C 下的电阻阻值。本文中选用的热敏电阻在 25°C 时阻值为 10k
      R 当前温度下电阻的阻值
      T0 开尔文温度 (273.15+25)
      T 开尔文温度(273.15+当前摄氏度温度)
      B 热敏特性常数
      exp e^n(e的n次方)
    2. 通过采样电路采样热敏电阻两端的电压,从而计算出 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);
    
        }
    
    }
    

    在产品创建和五大功能模式都实现的基础上接着实现语音播报、加热、档位切换的功能。

    离线控制

    功能简述

    智能烧水壶设备上提供煮沸、保温两个按键。

    • 离线状态下可以设定煮沸和取消煮沸。
    • 可以设定保温,离线状态下保温温度设定为 55 摄氏度。
    功能 说明
    煮沸
    • 触摸按键 1,触发方式:轻触。按下按键默认将水煮沸。
    • 按下按键开启煮沸,再按关闭煮沸。
    • App 控制:煮沸。
    保温键
    • 触摸按键 2,触发方式:轻触。
    • 轻触进入默认的自来水模式下的55度保温模式,按下按键开启煮沸,再按关闭煮沸。
    • 长按5s进入配网模式
    • 不对按键进行操作,且 App 上也无操作,则默认凉白开模式(即不保温)。

    按键采集原理

    按键采集芯片选用TS02N,触摸按键采集原理图如下:

    管脚分配:

    管脚 作用
    P7 煮沸键
    P8 保温键

    TS02N 使用说明:

    • 输入P1\P2:低电平触发

    • P1触发后OUT1输出低电平

    • P2触发后OUT2输出低电平

    离线控制代码

    为了后期的程序扩展,采用回调函数注册的方式实现按键控制。

    • 按键按下时,按键指示灯亮,按键松开,按键指示灯灭;按一次按键蜂鸣器响一下
    • 可以使用注册函数直接注册所用IO口,以及注册按键按下触发的回调函数
    • 可以设置长按时间,配置按键长按触发的回调函数

    按键驱动代码如下:

    /*使用时只需要通过 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 数据控制水壶的煮沸功能,开启和关闭触发后要实现的具体功能如下:

    开启:

    • 水壶进入煮沸模式
    • 因煮沸和保温互斥,所以关闭保温
    • 更新煮沸和保温的 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);
    
    }
    

    此时煮沸控制的执行函数已实现。

    实现设定保温温度的函数

    设定保温温度的函数是通过云端设定进行触发,触发后要实现的具体功能如下:

    • 根据云端发送的设置值设定保温温度值
    • 更新 dp_keep_warm_set 的值为所设的温度值,并上报到云端

    具体代码实现,需要在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);
        }
    }
    

    此时保温温度设定函数已实现。

    实现保温控制的执行函数

    云端可以控制界面上的保温按钮决定是否开启保温,触发后要实现的具体功能如下:

    开启:

    • 判断当前设定的水质模式,若设定为自来水模式,烧水壶切换到保温 1 模式(先煮沸再保温)
    • 判断当前设定的水质模式,若设定为纯净水模式,烧水壶切换到保温 2 模式(直接保温到设定温度)
    • 关闭煮沸 DP,上报煮沸 DP 和保温 DP 数据到云端

    关闭:
    切换烧水壶的工作状态到自然模式(无操作)

    具体代码实现,需要在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);
    }
    

    此时保温设置的执行函数已实现。

    实现水质选择设定函数

    云端可以控制水质选择,触发后要实现的具体功能如下:

    • 根据传下的设定值设置水质模式:自来水/纯净水
    • 若此时水壶处于自来水保温状态,且此时云端控制水质模式切换为纯净水,水壶状态切换到纯净水保温模式
    • 若此时水壶处于纯净水保温状态,且此时云端控制水质模式切换为自来水,水壶状态切换到自来水保温模式
    • 更新水质模式的dp数据并上报

    具体代码实现,需要在 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 点数据下发,从而触发烧水壶执行煮沸功能。

    云定时煮沸开启:

    • 触发后发送煮沸dp_boil为开启状态
    • 执行后将定时开启的状态清零

    云端控制代码实现

    上面已经实现了相应的执行函数,下面我们将实现 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 方案软件设计

    完整示例代码可在 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 平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。