智能烧水壶 (Bluetooth版)03——离线控制功能实现篇

Posted 三明治开发社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了智能烧水壶 (Bluetooth版)03——离线控制功能实现篇相关的知识,希望对你有一定的参考价值。

前言

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

完整Demo可在 tuya-iotos-embeded-demo-ble-smart-kettle 中获取。

功能设定

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

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

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

离线控制功能实现

(1)指示灯驱动控制

指示灯作为烧水壶状态指示和配网指示,驱动形式为电平驱动,基本设置如下:

指示灯管脚HL显示模式
红色 P_LED_REDP24/GPIO_PB5固定(常亮/常灭)
橙色 P_LED_ORANGEP26/GPIO_PB4固定(常亮/常灭)
绿色 P_GREENP6/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;
    }
}

(2)继电器驱动控制

继电器的通断用于控制加热电路,驱动形式为电平驱动,基本设置如下:

继电器管脚HL
继电器 P_RELAYP14/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;
    }
}

(3)蜂鸣器驱动控制

蜂鸣器作为按键提示音和故障报警提示音,驱动形式为方波信号驱动,基本设置如下:

蜂鸣器管脚模式方波信号(PWM)
蜂鸣器 P_BUZZERP17/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;
    }
}

(4)按键检测与处理

为了后期的程序扩展,采用注册回调函数的方式实现按键检测与处理。

代码实现:

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();	/* 判断与处理 */
}

(5)温度采集与处理

根据硬件方案中关于NTC温度传感器的介绍可知,通过ADC模块采集到的端口电压值即可换算出当前温度值。由于本案例使用的芯片平台不支持浮点数运算,这里我们采用查表法来获取当前温度。首先我们编写一个脚本,将NTC的R-T表中的电阻值读出并转换为电压值,然后按照顺序放入数组中存储,这样我们就可以通过查询数组将电压值转换为温度值。脚本可在tuya-iotos-embeded-demo-ble-temperature-alarm中获取。

a. 温度采集

NTC端口设置如下:

温度传感器管脚
温度传感器 P_NTCADC/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;
}

(6)应用层功能实现

在以上相关驱动代码实现后,下面进行智能烧水壶相关功能的实现。

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_BOILP7/GPIO_PC3轻触打开/关闭煮沸功能
保温键 P_KEY_KEEPP8/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();
}

以上就是蓝牙版智能烧水壶离线控制功能实现操作方法,如果大家有疑问或者有更好的方案欢迎留言讨论~

以上是关于智能烧水壶 (Bluetooth版)03——离线控制功能实现篇的主要内容,如果未能解决你的问题,请参考以下文章

智能烧水壶 (Bluetooth 版)02——系统创建篇

智能烧水壶 (Bluetooth版)04——云端控制篇

智能烧水壶(WIFI版)01——硬件设计篇

涂鸦智能烧水壶软件实现之云端控制(完结)

涂鸦智能烧水壶软件实现之温度采集和过温报警功能

涂鸦智能烧水壶软件实现之水温调节和灯光提示