feat: 智能家居控制系统 v1.0 初始版本

- 环境监测:温湿度/光照/空气质量传感器采集
- 智能控制:时间段/降温/通风三种自动模式
- 闹钟系统:3个闹钟+温和唤醒功能
- 远程控制:MQTT双向通信
- 本地显示:LVGL图形界面
- 双MCU架构,FreeRTOS 10任务并行
- 完整的1250行README文档
This commit is contained in:
Wang Beihong
2026-02-07 23:04:28 +08:00
commit a0febb1e5b
54 changed files with 7918 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
idf_component_register(SRCS "ui_display.c" "lvgl_st7735s_use.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_lcd esp_lvgl_port
)

View File

@@ -0,0 +1,33 @@
/* LCD size */
#define EXAMPLE_LCD_H_RES (160)
#define EXAMPLE_LCD_V_RES (80)
/* LCD SPI总线配置 */
#define EXAMPLE_LCD_SPI_NUM (SPI2_HOST) // 使用SPI2主机接口进行通信
/* LCD显示参数配置 */
#define EXAMPLE_LCD_PIXEL_CLK_HZ (40 * 1000 * 1000) // 像素时钟频率设置为40MHz控制数据传输速度
/* LCD命令和参数配置 */
#define EXAMPLE_LCD_CMD_BITS (8) // 命令位数为8位用于发送LCD控制命令
#define EXAMPLE_LCD_PARAM_BITS (8) // 参数位数为8位用于发送命令参数
/* LCD颜色和缓冲区配置 */
#define EXAMPLE_LCD_BITS_PER_PIXEL (16) // 每个像素使用16位颜色(RGB565格式)
#define EXAMPLE_LCD_DRAW_BUFF_DOUBLE (1) // 启用双缓冲模式,提高显示流畅度
#define EXAMPLE_LCD_DRAW_BUFF_HEIGHT (50) // 绘图缓冲区高度为50行影响刷新性能
/* LCD背光配置 */
#define EXAMPLE_LCD_BL_ON_LEVEL (1) // 背光开启电平为高电平(1)
/* LCD pins */
#define EXAMPLE_LCD_GPIO_SCLK (GPIO_NUM_2)
#define EXAMPLE_LCD_GPIO_MOSI (GPIO_NUM_3)
#define EXAMPLE_LCD_GPIO_RST (GPIO_NUM_9)
#define EXAMPLE_LCD_GPIO_DC (GPIO_NUM_8)
#define EXAMPLE_LCD_GPIO_CS (GPIO_NUM_7)
#define EXAMPLE_LCD_GPIO_BL (GPIO_NUM_6)
void start_lvgl_demo(void);

View File

@@ -0,0 +1,38 @@
#ifndef UI_DISPLAY_H
#define UI_DISPLAY_H
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief 初始化UI界面
*
* 该函数负责创建LVGL的用户界面元素用于显示传感器数据
*/
void ui_display_init(void);
/**
* @brief 更新传感器数据显示
*
* 该函数用于更新LVGL界面上的传感器数据
*
* @param temperature 温度值(°C)-1.0表示无效
* @param humidity 湿度值(%)-1.0表示无效
* @param lux 光照强度(lx)-1.0表示无效
* @param ppm 空气中有害气体浓度(ppm)
* @param quality_level 空气质量等级描述
*/
void ui_update_sensor_data(float temperature, float humidity, float lux, float ppm, const char* quality_level);
/* Time page APIs */
void ui_show_time_page(void);
void ui_show_sensor_page(void);
void ui_time_update(void);
void ui_toggle_page(void);
#ifdef __cplusplus
}
#endif
#endif /* UI_DISPLAY_H */

View File

@@ -0,0 +1,236 @@
#include <stdio.h>
#include "lvgl_st7735s_use.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lvgl_port.h" // 包含LVGL端口头文件用于LVGL与ESP硬件的接口
#include "ui_display.h" // 添加新UI界面的头文件
static const char *TAG = "lvgl_st7735s_use"; // 用于日志输出的标签,便于调试时识别日志来源
/* LCD IO和面板句柄 */
// lcd_io: LCD面板IO句柄用于与LCD进行通信
// lcd_panel: LCD面板句柄用于控制LCD的各种操作
static esp_lcd_panel_io_handle_t lcd_io = NULL;
static esp_lcd_panel_handle_t lcd_panel = NULL;
/* LVGL显示和触摸 */
// lvgl_disp: LVGL显示设备句柄用于LVGL库与显示设备的交互
static lv_display_t *lvgl_disp = NULL;
/**
* @brief 初始化LCD硬件和SPI接口
*
* 该函数负责初始化LCD所需的GPIO、SPI总线并配置LCD面板
* 包括背光控制、SPI总线配置、面板IO配置和面板驱动安装
*
* @return esp_err_t 初始化结果ESP_OK表示成功
*/
static esp_err_t app_lcd_init(void)
{
esp_err_t ret = ESP_OK;
/* LCD背光配置 */
// 配置背光GPIO为输出模式用于控制LCD的背光开关
gpio_config_t bk_gpio_config = {
.mode = GPIO_MODE_OUTPUT, // 设置GPIO为输出模式
.pin_bit_mask = 1ULL << EXAMPLE_LCD_GPIO_BL // 设置背光GPIO引脚
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config)); // 应用GPIO配置并检查错误
/* LCD初始化 */
ESP_LOGD(TAG, "初始化SPI总线"); // 输出调试日志
// 配置SPI总线参数包括时钟、数据线和最大传输大小
const spi_bus_config_t buscfg = {
.sclk_io_num = EXAMPLE_LCD_GPIO_SCLK, // SPI时钟引脚
.mosi_io_num = EXAMPLE_LCD_GPIO_MOSI, // SPI主输出从输入引脚
.miso_io_num = GPIO_NUM_NC, // 未使用MISO引脚
.quadwp_io_num = GPIO_NUM_NC, // 未使用WP引脚
.quadhd_io_num = GPIO_NUM_NC, // 未使用HD引脚
.max_transfer_sz = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_DRAW_BUFF_HEIGHT * sizeof(uint16_t), // 最大传输大小
};
// 初始化SPI总线使用DMA自动分配通道
ESP_RETURN_ON_ERROR(spi_bus_initialize(EXAMPLE_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI初始化失败");
ESP_LOGD(TAG, "安装面板IO");
// 配置LCD面板IO的SPI参数
const esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = EXAMPLE_LCD_GPIO_DC, // 数据/命令选择引脚
.cs_gpio_num = EXAMPLE_LCD_GPIO_CS, // 片选引脚
.pclk_hz = EXAMPLE_LCD_PIXEL_CLK_HZ, // 像素时钟频率
.lcd_cmd_bits = EXAMPLE_LCD_CMD_BITS, // 命令位数
.lcd_param_bits = EXAMPLE_LCD_PARAM_BITS, // 参数位数
.spi_mode = 3, // SPI模式
.trans_queue_depth = 10, // 传输队列深度
};
// 创建LCD面板IO用于SPI通信
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)EXAMPLE_LCD_SPI_NUM, &io_config, &lcd_io), err, TAG, "创建面板IO失败");
ESP_LOGD(TAG, "安装LCD驱动");
// 配置LCD面板设备参数
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = EXAMPLE_LCD_GPIO_RST, // 复位引脚
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0)
.rgb_endian = LCD_RGB_ENDIAN_RGB, // RGB字节序旧版本
#else
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, // RGB元素顺序新版本
#endif
.bits_per_pixel = EXAMPLE_LCD_BITS_PER_PIXEL, // 每像素位数
};
// 创建ST7789 LCD面板驱动
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(lcd_io, &panel_config, &lcd_panel), err, TAG, "创建面板失败");
// 复位LCD面板
esp_lcd_panel_reset(lcd_panel);
// 初始化LCD面板
esp_lcd_panel_init(lcd_panel);
// 设置显示窗口,确保使用正确的分辨率(偏移)
esp_lcd_panel_set_gap(lcd_panel, 1, 26);
// 反转颜色
esp_lcd_panel_invert_color(lcd_panel, true);
// 打开LCD显示
esp_lcd_panel_disp_on_off(lcd_panel, true);
/* 打开LCD背光 */
ESP_ERROR_CHECK(gpio_set_level(EXAMPLE_LCD_GPIO_BL, EXAMPLE_LCD_BL_ON_LEVEL));
return ret;
// 错误处理标签,用于清理资源
err:
if (lcd_panel) {
esp_lcd_panel_del(lcd_panel); // 删除面板
}
if (lcd_io) {
esp_lcd_panel_io_del(lcd_io); // 删除面板IO
}
spi_bus_free(EXAMPLE_LCD_SPI_NUM); // 释放SPI总线资源
return ret;
}
/**
* @brief 初始化LVGL图形库
*
* 该函数负责初始化LVGL库并配置显示设备
* 包括LVGL任务配置、显示缓冲区配置和旋转设置
*
* @return esp_err_t 初始化结果ESP_OK表示成功
*/
static esp_err_t app_lvgl_init(void)
{
/* 初始化LVGL */
// 配置LVGL任务参数
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 4, /* LVGL任务优先级数值越高优先级越高 */
.task_stack = 4096, /* LVGL任务堆栈大小单位为字节 */
.task_affinity = -1, /* LVGL任务绑定核心-1表示不绑定特定核心 */
.task_max_sleep_ms = 500, /* LVGL任务最大睡眠时间单位为毫秒 */
.timer_period_ms = 5 /* LVGL定时器周期单位为毫秒用于处理动画和输入 */
};
// 初始化LVGL端口
ESP_RETURN_ON_ERROR(lvgl_port_init(&lvgl_cfg), TAG, "LVGL端口初始化失败");
/* 添加LCD屏幕 */
ESP_LOGD(TAG, "添加LCD屏幕");
// 配置LVGL显示设备参数
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = lcd_io, // LCD面板IO句柄
.panel_handle = lcd_panel, // LCD面板句柄
.buffer_size = EXAMPLE_LCD_H_RES * EXAMPLE_LCD_DRAW_BUFF_HEIGHT, // 缓冲区大小
.double_buffer = EXAMPLE_LCD_DRAW_BUFF_DOUBLE, // 是否使用双缓冲
.hres = EXAMPLE_LCD_H_RES, // 水平分辨率
.vres = EXAMPLE_LCD_V_RES, // 垂直分辨率
.monochrome = false, // 是否为单色显示
#if LVGL_VERSION_MAJOR >= 9
.color_format = LV_COLOR_FORMAT_RGB565, // 颜色格式LVGL v9及以上
#endif
.rotation = { // 旋转设置
.swap_xy = 1, // 交换X和Y轴以实现横向显示
.mirror_x = 1, // 是否水平镜像
.mirror_y = 0, // 是否垂直镜像
},
.flags = { // 标志位
.buff_dma = true, // 是否使用DMA缓冲区
#if LVGL_VERSION_MAJOR >= 9
.swap_bytes = false, // 是否交换字节序LVGL v9及以上
#endif
}
};
// 添加LVGL显示设备
lvgl_disp = lvgl_port_add_disp(&disp_cfg);
return ESP_OK;
}
/**
* @brief 创建并显示LVGL主界面
*
* 该函数负责创建LVGL的用户界面元素包括图像、标签和按钮
* 并设置它们的位置和属性
*/
static void app_main_display(void)
{
// 获取当前活动屏幕对象
lv_obj_t *scr = lv_scr_act();
/* 任务锁定 */
// 锁定LVGL任务防止在创建UI对象时被中断
lvgl_port_lock(0);
/* 设置屏幕背景为黑色 */
lv_obj_set_style_bg_color(scr, lv_color_white(), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
/* 创建标签 */
// 创建标签对象
lv_obj_t *label = lv_label_create(scr);
// 设置标签文本为"ESP32C3-LVGL"
lv_label_set_text(label, "ESP32C3-LVGL1");
// 设置标签文本颜色为白色
lv_obj_set_style_text_color(label, lv_color_black(), 0);
// 设置标签文本字体大小为更小的字体
lv_obj_set_style_text_font(label, &lv_font_unscii_8, 0);
// 设置标签位置在屏幕中心
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
/* 任务解锁 */
// 解锁LVGL任务
lvgl_port_unlock();
}
/**
* @brief 启动LVGL演示程序
*
* 该函数是程序的入口点负责初始化LCD硬件、LVGL库并显示主界面
*/
void start_lvgl_demo(void)
{
/* LCD硬件初始化 */
// 初始化LCD硬件和SPI接口
ESP_ERROR_CHECK(app_lcd_init());
/* LVGL初始化 */
// 初始化LVGL图形库
ESP_ERROR_CHECK(app_lvgl_init());
/* 显示LVGL对象 */
// 创建并显示LVGL主界面
// app_main_display();
/* 显示LVGL对象 - 使用新的UI界面初始化函数 */
ui_display_init();
}

View File

@@ -0,0 +1,258 @@
#include "ui_display.h"
#include "esp_log.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
#include <time.h>
static const char *TAG = "ui_display";
// 全局变量用于存储传感器数据标签
static lv_obj_t *temp_label = NULL;
static lv_obj_t *humid_label = NULL;
static lv_obj_t *lux_label = NULL;
static lv_obj_t *air_quality_label = NULL; // 新增空气质量标签
/* Time page objects */
static lv_obj_t *time_container = NULL;
static lv_obj_t *date_label = NULL;
static lv_obj_t *time_label = NULL;
static bool time_page_visible = false;
/**
* @brief 初始化UI界面
*
* 该函数负责创建LVGL的用户界面元素用于显示传感器数据
* 优化布局以适应非触摸屏设备,所有内容在一个屏幕内显示
*/
void ui_display_init(void)
{
// 获取当前活动屏幕对象
lv_obj_t *scr = lv_scr_act();
/* 任务锁定 */
lvgl_port_lock(0);
/* 设置屏幕背景为白色 */
lv_obj_set_style_bg_color(scr, lv_color_white(), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
/* 创建标题标签 */
lv_obj_t *title_label = lv_label_create(scr);
lv_label_set_text(title_label, "IoT Home Monitor");
lv_obj_set_style_text_color(title_label, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_set_style_text_font(title_label, &lv_font_unscii_8, 0);
lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 2); // 调整标题位置
/* 创建温度标签 */
temp_label = lv_label_create(scr);
lv_label_set_text(temp_label, "Temp: --.- C");
lv_obj_set_style_text_color(temp_label, lv_color_black(), 0);
lv_obj_set_style_text_font(temp_label, &lv_font_unscii_8, 0);
lv_obj_align(temp_label, LV_ALIGN_TOP_LEFT, 3, 20); // 调整位置
/* 创建湿度标签 */
humid_label = lv_label_create(scr);
lv_label_set_text(humid_label, "Humidity: --.- %");
lv_obj_set_style_text_color(humid_label, lv_color_black(), 0);
lv_obj_set_style_text_font(humid_label, &lv_font_unscii_8, 0);
lv_obj_align(humid_label, LV_ALIGN_TOP_LEFT, 3, 35); // 调整位置
/* 创建光照标签 */
lux_label = lv_label_create(scr);
lv_label_set_text(lux_label, "Light: --.- lux");
lv_obj_set_style_text_color(lux_label, lv_color_black(), 0);
lv_obj_set_style_text_font(lux_label, &lv_font_unscii_8, 0);
lv_obj_align(lux_label, LV_ALIGN_TOP_LEFT, 3, 50); // 调整位置
/* 创建空气质量标签 */
air_quality_label = lv_label_create(scr);
lv_label_set_text(air_quality_label, "IAQ : --.- Index");
lv_obj_set_style_text_color(air_quality_label, lv_color_black(), 0);
lv_obj_set_style_text_font(air_quality_label, &lv_font_unscii_8, 0);
lv_obj_align(air_quality_label, LV_ALIGN_TOP_LEFT, 3, 65); // 调整位置
/* 任务解锁 */
lvgl_port_unlock();
// 创建时间页面(初始隐藏)
lvgl_port_lock(0);
time_container = lv_obj_create(lv_scr_act());
lv_obj_set_size(time_container, lv_pct(100), lv_pct(100));
lv_obj_set_style_bg_color(time_container, lv_color_white(), 0);
lv_obj_set_style_bg_opa(time_container, LV_OPA_COVER, 0);
date_label = lv_label_create(time_container);
lv_label_set_text(date_label, "---- ---- -- ---");
lv_obj_set_style_text_color(date_label, lv_color_black(), 0);
lv_obj_set_style_text_font(date_label, &lv_font_unscii_8, 0);
lv_obj_align(date_label, LV_ALIGN_TOP_MID, 0, 6);
time_label = lv_label_create(time_container);
lv_label_set_text(time_label, "--:--:--");
lv_obj_set_style_text_color(time_label, lv_color_black(), 0);
lv_obj_set_style_text_font(time_label, &lv_font_unscii_16, 0);
lv_obj_align(time_label, LV_ALIGN_CENTER, 0, 12);
// 默认显示时间页面,隐藏传感器页面
lv_obj_clear_flag(time_container, LV_OBJ_FLAG_HIDDEN);
if (temp_label) lv_obj_add_flag(temp_label, LV_OBJ_FLAG_HIDDEN);
if (humid_label) lv_obj_add_flag(humid_label, LV_OBJ_FLAG_HIDDEN);
if (lux_label) lv_obj_add_flag(lux_label, LV_OBJ_FLAG_HIDDEN);
if (air_quality_label) lv_obj_add_flag(air_quality_label, LV_OBJ_FLAG_HIDDEN);
time_page_visible = true;
lvgl_port_unlock();
}
/**
* @brief 更新传感器数据显示
*
* 该函数用于更新LVGL界面上的传感器数据
*
* @param temperature 温度值(°C)-1.0表示无效
* @param humidity 湿度值(%)-1.0表示无效
* @param lux 光照强度(lx)-1.0表示无效
* @param ppm 空气中有害气体浓度(ppm)
* @param quality_level 空气质量等级描述
*/
void ui_update_sensor_data(float temperature, float humidity, float lux, float ppm, const char* quality_level)
{
if (temp_label != NULL && humid_label != NULL && lux_label != NULL && air_quality_label != NULL)
{
/* 任务锁定 */
lvgl_port_lock(0);
// 更新温度标签 - 缩短文本以节省空间
if (temperature >= -0.5) // -1.0表示无效
{
char temp_str[32];
snprintf(temp_str, sizeof(temp_str), "Temp: %.2f C", temperature);
lv_label_set_text(temp_label, temp_str);
}
else
{
lv_label_set_text(temp_label, "Temp: Invalid");
}
// 更新湿度标签 - 缩短文本以节省空间
if (humidity >= -0.5) // -1.0表示无效
{
char humid_str[32];
snprintf(humid_str, sizeof(humid_str), "Humidity: %.2f %%", humidity);
lv_label_set_text(humid_label, humid_str);
}
else
{
lv_label_set_text(humid_label, "Humidity: Invalid");
}
// 更新光照标签 - 缩短文本以节省空间
if (lux >= -0.5) // -1.0表示无效
{
char lux_str[32];
snprintf(lux_str, sizeof(lux_str), "Light: %.2f lx", lux);
lv_label_set_text(lux_label, lux_str);
}
else
{
lv_label_set_text(lux_label, "Light: Invalid");
}
// 更新空气质量标签 - 缩短文本以节省空间
if (ppm >= 0) // 空气质量值有效
{
char ppm_str[32];
snprintf(ppm_str, sizeof(ppm_str), "IAQ : %.2f Index", ppm);
// 根据空气质量等级更改颜色
lv_color_t color = lv_color_black(); // 默认黑色
if (ppm <= 20.0f) {
color = lv_color_make(0, 128, 0); // 绿色 - 空气质量优秀
} else if (ppm <= 100.0f) {
color = lv_color_make(0, 0, 0); // 黑色 - 空气质量良好
} else if (ppm <= 300.0f) {
color = lv_color_make(255, 165, 0); // 橙色 - 轻度污染
} else {
color = lv_color_make(255, 0, 0); // 红色 - 中重度污染
}
lv_label_set_text(air_quality_label, ppm_str);
lv_obj_set_style_text_color(air_quality_label, color, 0);
}
else
{
lv_label_set_text(air_quality_label, "IAQ : Invalid");
lv_obj_set_style_text_color(air_quality_label, lv_color_black(), 0);
}
/* 任务解锁 */
lvgl_port_unlock();
}
}
/* Show time page */
void ui_show_time_page(void)
{
lvgl_port_lock(0);
if (time_container)
{
lv_obj_clear_flag(time_container, LV_OBJ_FLAG_HIDDEN);
}
if (temp_label) lv_obj_add_flag(temp_label, LV_OBJ_FLAG_HIDDEN);
if (humid_label) lv_obj_add_flag(humid_label, LV_OBJ_FLAG_HIDDEN);
if (lux_label) lv_obj_add_flag(lux_label, LV_OBJ_FLAG_HIDDEN);
if (air_quality_label) lv_obj_add_flag(air_quality_label, LV_OBJ_FLAG_HIDDEN);
time_page_visible = true;
lvgl_port_unlock();
}
/* Show sensor page */
void ui_show_sensor_page(void)
{
lvgl_port_lock(0);
if (time_container)
{
lv_obj_add_flag(time_container, LV_OBJ_FLAG_HIDDEN);
}
if (temp_label) lv_obj_clear_flag(temp_label, LV_OBJ_FLAG_HIDDEN);
if (humid_label) lv_obj_clear_flag(humid_label, LV_OBJ_FLAG_HIDDEN);
if (lux_label) lv_obj_clear_flag(lux_label, LV_OBJ_FLAG_HIDDEN);
if (air_quality_label) lv_obj_clear_flag(air_quality_label, LV_OBJ_FLAG_HIDDEN);
time_page_visible = false;
lvgl_port_unlock();
}
/* Toggle between pages */
void ui_toggle_page(void)
{
if (time_page_visible)
ui_show_sensor_page();
else
ui_show_time_page();
}
/* Update time label (call periodically) */
void ui_time_update(void)
{
if (!time_page_visible || time_label == NULL)
return;
time_t now = time(NULL);
struct tm tm_now;
localtime_r(&now, &tm_now);
char date_buf[64];
char time_buf[32];
char weekday_buf[32];
// 年月日和星期合并为一行
strftime(date_buf, sizeof(date_buf), "%Y-%m-%d", &tm_now);
const char *weekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
snprintf(date_buf + strlen(date_buf), sizeof(date_buf) - strlen(date_buf), " %s", weekdays[tm_now.tm_wday]);
// 时分秒
strftime(time_buf, sizeof(time_buf), "%H:%M:%S", &tm_now);
lvgl_port_lock(0);
lv_label_set_text(date_label, date_buf);
lv_label_set_text(time_label, time_buf);
lvgl_port_unlock();
}