ESP32掌控终端项目(详细+长篇+源码)

ESP32掌控终端项目(详细+长篇+源码)

项目涉及技术栈:

LVGL,MQTT,HTTP,FreeRTOS,摄像头,蓝牙,SD卡读取,ESP-ADF音频框架,网路获取天气,网路获取实时时间

lvgl基础函数可看我另一篇随笔

LVGL 8.3.0开发实战:高频函数速查与移植避坑指南 - 沁拒离 - 博客园

逐步实现整体框架

1、显示文本标签

c 复制代码
    // 创建页面容器
    page1 = lv_obj_create(lv_scr_act());  
    lv_obj_add_style(page1, &style, 0);	
    lv_obj_add_flag(page1, LV_OBJ_FLAG_HIDDEN); // 初始隐藏   
	
  // 创建一个标签对象,并将其添加到页面1中
    lv_obj_t *title = lv_label_create(page1);
    // 设置标签的文本为"应用"
    lv_label_set_text(title, "应用");
    // 为标签对象添加样式
    lv_obj_add_style(title, &style, LV_PART_MAIN);
    // 将标签对象对齐到页面的顶部中间位置,并设置偏移量为0和10
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);

简单图解:

流程:

先创建页面容器(page1),在页面容器(page1)中再创建标题容器(title),文本在标题容器中显示

2、显示中文字符

一、生成中文字体文件

  1. 使用在线转换工具

    LVGL 官方提供了 在线字体转换工具,支持将 TTF/WOFF 字体文件转换为 LVGL 可用的 .c 格式字体文件125。

    • 参数设置
      • 字体大小:建议 16px 及以上,避免显示模糊68。
      • BPP(抗锯齿):推荐选择 4 位,提升显示效果46。
      • 字符范围 :选择所需汉字范围(如 0x4E00-0x9FFF 表示常用汉字),或手动输入特定字符810。
      • 压缩选项 :勾选 Compressed 可减小字体体积36。

    二、配置项目与代码

  2. 添加字体文件到工程

    • 将生成的 .c 文件(如 font_alipuhui20.c)放入 工程

    在CMakeLists.txt中添加进编译

lv_conf.h 中声明字体并启用多字体字符支持:

复制代码
LV_FONT_FMT_TXT_LARGE

选项的中文解释翻译:

启用字体字号

设置字体样式

1、全局样式设置(推荐 ):

c 复制代码
 // 创建页面样式
    static lv_style_t style;    
    lv_style_init(&style);

    lv_style_set_text_font(&style, &font_alipuhui20);  // 设置全局字体样式

2、单句样式设置:

c 复制代码
    // 创建扫描状态标签
    lv_obj_t *page1_1 = lv_label_create(page1);
    lv_label_set_text(page1_1, "WLAN 扫描了...");
    lv_obj_set_style_text_color(page1_1, lv_color_hex(0x000000), 0); // 设置文本颜色为黑色
    lv_obj_set_style_text_font(page1_1, &font_alipuhui20, 0);

3、插入图片

图片转化网址

图像转换器 --- LVGL --- Image Converter --- LVGL

将生成图片.c源文件添加进工程

c 复制代码
    /**************** Logo显示逻辑 ****************/
    // 创建独立Logo容器(直接放在根屏幕)
    logo_container = lv_obj_create(lv_scr_act());
    lv_obj_remove_style_all(logo_container); // 移除logo_container的所有样式
    lv_obj_set_size(logo_container, LV_PCT(100), LV_PCT(100)); // 全屏
    lv_obj_set_layout(logo_container, LV_LAYOUT_FLEX);    // 设置logo_container的布局为flex布局
    // 设置logo_container的flex对齐方式为居中对齐
    lv_obj_set_flex_align(logo_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

    // 创建Logo图片
    LV_IMG_DECLARE(Pic2);//图片源文件(.c)
    lv_obj_t *logo_img = lv_img_create(logo_container);// 创建lvgl图片对象,在容器内
    lv_img_set_src(logo_img, &Pic2);//设置图片对象图像源

4、开机旋转logo制作

c 复制代码
void lv_gui_animation_start() // 显示开机界面
{
 /****************开机logo ****************/
    // 创建全屏临时容器(确保覆盖整个屏幕)
    lv_obj_t *logo_pag_pre = lv_obj_create(lv_scr_act());
    lv_obj_remove_style_all(logo_pag_pre);       // 清除默认样式
    lv_obj_set_size(logo_pag_pre, LV_PCT(100), LV_PCT(100)); // 全屏尺寸
    lv_obj_set_layout(logo_pag_pre, LV_LAYOUT_FLEX); // 启用弹性布局
    lv_obj_set_flex_align(logo_pag_pre, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); // 内容居中
    // // 声明并创建Logo图片
    LV_IMG_DECLARE(Pic2);
    lv_obj_t *logo_img = lv_img_create(logo_pag_pre);
    lv_img_set_src(logo_img, &Pic2);
    // 精确居中控制(双重保障)
    lv_obj_center(logo_img);                    // 弹性布局居中
    lv_obj_align(logo_img, LV_ALIGN_CENTER, 0, 0); // 绝对坐标居中
    // lv_img_set_pivot(logo_img, 60, 60); // 设置图片旋转中心   
    lv_img_set_pivot(logo_img, 100, 100); // 设置图片旋转中心   
    /****************开机logo动画****************/
    // // // 设置旋转动画
    lv_anim_t a; // 创建动画变量
    lv_anim_init(&a); // 初始化动画变量
    lv_anim_set_var(&a, logo_img); // 动画变量赋值为logo图片
    lv_anim_set_exec_cb(&a, set_angle); // 创建设置角度的回调函数
    lv_anim_set_values(&a, 0, 3600); // 设置动画旋转角度的开始值和结尾值
    lv_anim_set_time(&a, 200); // 设置转一圈的周期是200毫秒
    lv_anim_set_repeat_count(&a, 5); // 设置旋转5次
    lv_anim_start(&a); // 动画开始
    // 延迟2秒后删除logo_pag_pre对象
    lv_obj_del_delayed(logo_pag_pre, 2000);
    // 给logo_pag_pre对象添加删除事件回调函数
    lv_obj_add_event_cb(logo_pag_pre, logo_del_cb, LV_EVENT_DELETE, NULL);
///////////////////////////////
}

流程:创建全屏动画容器页面(logo_pag_pre),在lvgl中声明并创建Logo图片,为图片添加动画,动画结束删除容器页面

效果

5、制作应用

5.1、制作应用图标

例如原神"应用"的制作

首页应用图标 点击进入应用效果

简要说明:"应用"为按键+图片实现。在按键上覆盖图片(应用图标),按键按下触发对应按键事件(进入应用)

c 复制代码
	/****************应用4,原神 ****************/
    // 创建第4个应用的按键
    lv_obj_t *icon4 = lv_btn_create(page1);//创建按钮
    lv_obj_add_style(icon4, &btn_style, 0);
    lv_obj_set_style_bg_color(icon4, lv_color_hex(0xd8b010), 0);
    lv_obj_set_pos(icon4, 15, 147);
    lv_obj_add_event_cb(icon4, yuanshen_event_handler, LV_EVENT_CLICKED, NULL);//按键回调(应用触发)函数为yuanshen_event_handler(),需用户编写实际功能

    // 为按钮添加图片(使其看起来像应用)
    lv_obj_t * img4 = lv_img_create(icon4);
    // 声明一个图像
    LV_IMG_DECLARE(yuanshen);
    // 设置图像对象显示的图像
    lv_img_set_src(img4, &yuanshen);
    lv_obj_align(img4, LV_ALIGN_CENTER, 0, 0);

其中图标的通用样式(btn_style):

c 复制代码
   	// 设置应用图标通用样式
    static lv_style_t btn_style;
    // 初始化按钮样式
    lv_style_init(&btn_style);
    // 设置按钮圆角半径为16
    lv_style_set_radius(&btn_style, 16);  
    // 设置按钮背景透明度为100%
    lv_style_set_bg_opa( &btn_style, LV_OPA_COVER );
    // 设置按钮文字颜色为白色
    lv_style_set_text_color(&btn_style, lv_color_hex(0xffffff)); 
    // 设置按钮边框宽度为0
    lv_style_set_border_width(&btn_style, 0);
    // 设置按钮内边距为5
    lv_style_set_pad_all(&btn_style, 5);
    // 设置按钮宽度为80
    lv_style_set_width(&btn_style, 80);  
    // 设置按钮高度为80
    lv_style_set_height(&btn_style, 80); 

5.2、应用触发回调编写

以应用4,原神为例,设计应用点击进入应用:原神加载界面(用图片实现)

首先按键回调(应用触发)函数为yuanshen_event_handler()

c 复制代码
/*********************** 应用4原神回调 ****************************/
void yuanshen_event_handler(lv_event_t * e)
{
    // 创建一个界面
    static lv_style_t style;
    lv_style_init(&style);
    lv_style_set_radius(&style, 10);  
    lv_style_set_bg_opa( &style, LV_OPA_COVER );
    lv_style_set_bg_color(&style, lv_color_hex(0xffffff));
    lv_style_set_border_width(&style, 0);
    lv_style_set_pad_all(&style, 0);
    lv_style_set_width(&style, 320);  
    lv_style_set_height(&style, 240); 

    // 创建一个图标对象,并将其添加到当前活动屏幕上
    app_page = lv_obj_create(lv_scr_act());
    // 将样式添加到图标对象上
    lv_obj_add_style(app_page, &style, 0);
    
    // // 声明并创建图片
    LV_IMG_DECLARE(yuanshenqidong);
    // lv_obj_t *logo_img = lv_img_create(logo_pag_pre);
    lv_obj_t *logo_img = lv_img_create(app_page);   
    lv_img_set_src(logo_img, &yuanshenqidong);
    // 精确居中控制(双重保障)
    lv_obj_center(logo_img);                    // 弹性布局居中
    lv_obj_align(logo_img, LV_ALIGN_CENTER, 0, 0); // 绝对坐标居中
    //退出应用部分暂略,后续添加(5.3添加应用退出按钮)
}

进入应用效果:

5.3添加应用退出按钮

c 复制代码
    // 创建标题背景
    // 创建一个对象,用于显示标题
    lv_obj_t *att_title = lv_obj_create(app_page);
    // 设置对象的大小
    lv_obj_set_size(att_title, 40, 40);
    // 设置对象间隙
    lv_obj_set_style_pad_all(att_title, 0, 0);  // 设置间隙
    // 对齐对象
    lv_obj_align(att_title, LV_ALIGN_TOP_LEFT, 0, 0);
    // 设置对象背景颜色
    lv_obj_set_style_bg_color(att_title, lv_color_hex(0xffffff), 0);

   // 创建后退按钮
    btn_bk_back = lv_btn_create(att_title);
    lv_obj_align(btn_bk_back, LV_ALIGN_LEFT_MID, 0, 0);
    lv_obj_set_size(btn_bk_back, 60, 30);
    lv_obj_set_style_border_width(btn_bk_back, 0, 0); // 设置边框宽度
    lv_obj_set_style_pad_all(btn_bk_back, 0, 0);  // 设置间隙
    lv_obj_set_style_bg_opa(btn_bk_back, LV_OPA_TRANSP, LV_PART_MAIN); // 背景透明
    lv_obj_set_style_shadow_opa(btn_bk_back, LV_OPA_TRANSP, LV_PART_MAIN); // 阴影透明
    lv_obj_add_event_cb(btn_bk_back, btn_home_back_cb, LV_EVENT_CLICKED, NULL); // 添加按键处理函数
    // 创建后退按钮符号
    lv_obj_t *label_back = lv_label_create(btn_bk_back);  // 创建一个标签对象,并将其与btn_bk_back关联
    lv_label_set_text(label_back, LV_SYMBOL_LEFT);  // 按键上显示左箭头符号
    lv_obj_set_style_text_font(label_back, &lv_font_montserrat_20, 0); // 设置标签的字体为lv_font_montserrat_20
    lv_obj_set_style_text_color(label_back, lv_color_hex(0xA9A9A9), 0);  // 设置标签的字体颜色为深灰
    lv_obj_align(label_back, LV_ALIGN_CENTER, -10, 0); // 将标签对象居中对齐,并设置x轴偏移量为-10

5.4退出按钮回调函数

c 复制代码
// 应用返回主界面按钮事件处理函数
void btn_home_back_cb(lv_event_t * e)
{
	//按应用区别添加相应操作(释放内存,删除应用运行中的任务等......)
    lv_obj_del(app_page); // 删除画布
    icon_flag = 0;//应用标识,0为主页面
}

由于应用回调函数是在容器(画布)app_page中创建,删除容器,则会显示在其底层的容器(page1主界面),以此实现退出应用,按应用的情况在退出回调中应该添加相应操作如(释放内存,删除应用运行中的任务等......)。

6、wifi连接

c 复制代码
#include "esp_wifi.h"   
#include "protocol_examples_common.h"
// 添加网络接口相关头文件
#include "esp_netif.h"
// 确保事件循环头文件存在
#include "esp_event.h"
// 包含示例连接功能的头文件
#include "nvs_flash.h"
#include <esp_system.h>

//// 连接WiFi(menuconfig中配置)
void wifi_connect(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());       // 初始化非易失存储
    ESP_ERROR_CHECK(esp_netif_init());       // 初始化网络堆栈
    ESP_ERROR_CHECK(esp_event_loop_create_default()); // 创建事件循环
    // 检查example_connect函数的返回值,确保连接成功
    ESP_ERROR_CHECK(example_connect());      // 连接WiFi
}

wifi连接使用库函数快速实现。

在menuconfig中配置wifi账号与密码(在文章后面menuconfig部分有详细记录)

7、MQTT应用

该应用控制MQTT设备云设备,

需要添加protocol_examples_common组件库

protocol_examples_common介绍:

"protocol_examples_common.h"//用于存放与协议(Protocol)相关的通用示例代码或共享组件,为开发者提供可复用的模块或模板,帮助快速理解和使用项目中涉及的通信协议(如 HTTP、TCP、MQTT 等)。

工程位置 添加内容

先看现象:

点击亮灯图标 点击灭灯图标
掌机终端打印: 掌机终端打印:

这里使用掌机终端推送MQTT主题,另一块esp32核心板订阅相同主题,并解析数据,做出对应指令操作(亮/灭灯)

详细完整代码在文章结尾,这里不做详细的讲解,仅介绍代码关键的部分。

推送主题

c 复制代码
//推送主题
void put_led_on() {
    // if (mqtt_connected) {
        const char *status = "led_on";//推送主题
        esp_mqtt_client_publish(mqtt_client, SUB_TOPIC, status, strlen(status), 0, 0);

        // esp_mqtt_client_publish(mqtt_client, "ledstate", status, strlen(status), 0, 0);
        ESP_LOGI(TAG3, "led_on");
    // }
}

绑定绑定推送主题事件。由应用MQTT内的点击事件触发。

订阅主题

c 复制代码
//----------------MQTT订阅-----------------------//
esp_mqtt_client_subscribe(mqtt_client, SUB_TOPIC, 0);//订阅主题ledctrl

处理MQTT接收事件(部分)

c 复制代码
 static void mqtt_event_handler(void *handler_args, esp_event_base_t base, 
                              int32_t event_id, void *event_data) {
    esp_mqtt_event_handle_t event = event_data;
    switch (event->event_id) {
case MQTT_EVENT_DATA: {//<MQTT接收数据事件>
            // 处理接收数据
            //----------------判断接收主题-----------------------------------------------------------------//
            char topic[event->topic_len + 1];
            memcpy(topic, event->topic, event->topic_len);
            topic[event->topic_len] = '\0';
            //----------------判断接收数据-----------------------------------------------------------------//
            char data[event->data_len + 1];
            memcpy(data, event->data, event->data_len);
            data[event->data_len] = '\0';
            //----------------打印接收主题+数据-----------------------------------------------------------------//
            ESP_LOGI(TAG3, "Received: Topic=%s, Data=%s", topic, data);
            
            lv_label_set_text_fmt(label_topic, "Topic: %s", topic);
            lv_label_set_text_fmt(label_data, "Data: %s", data);

            if (strcmp(topic, SUB_TOPIC) == 0) {//判断接收主题(ledctrl)数据------>控制led灯->推送led状态信息
                if (strcmp(data, "on") == 0) {
                    gpio_set_level(OUTPUT_PIN, 0);
                    //推送led状态信息到主题ledstate
                    esp_mqtt_client_publish(mqtt_client, PUB_TOPIC3, "led设备已开启", 0, 0, 0);//同时推送状态到主题ledstate
                } else if (strcmp(data, "off") == 0) {
                    gpio_set_level(OUTPUT_PIN, 1);
                    esp_mqtt_client_publish(mqtt_client, PUB_TOPIC3, "led设备已关闭", 0, 0, 0);
                }

            }
            break;
}

接收到订阅的主题数据,可以判断内容后执行相应的用户操作(例如:点灯,调节屏幕亮度,启动/关闭外设......)

12、HTTP综合应用

12.1、HTTP获取实时时间

使用淘宝的时间戳获取API

c 复制代码
#define WEB_SERVER "acs.m.taobao.com"
// 定义WEB服务器端口
#define WEB_PORT "80"
// 定义WEB服务器路径
#define WEB_PATH "/gw/mtop.common.getTimestamp/"

通过更改ESP32官方http get exampl ,实现获取该网址时间戳,获取成功后通过CJson解析出时间戳

编写时间戳转化函数,转化成时间

注意获取到的时间戳是单位:毫秒

时区是UT8?需要转化为东八市区(北京时间)

将获取的时间转化为本地时间保存(好处是不用再次使用网络获取,仅需从本地获取就可得到准确的时间,节省算力和网路带宽)

我将其显示到主页面顶部实时刷新

c 复制代码
void show_time_task(void *pvParameter)
{
        // 设置时区为中国标准时间(东八区)
    setenv("TZ", "CST-8", 1);
    tzset();
    // 已经从网络或HTTP中获取了时间戳(单位:毫秒)
    time_t base_time = g_timestamp_ms / 1000;  // 修正:毫秒转秒
    int64_t base_us = esp_timer_get_time(); // 获取时的微秒数(系统运行时间)

    while (1) {
        int64_t now_us = esp_timer_get_time();
        time_t now = base_time + (now_us - base_us) / 1000000;  // 当前时间戳(秒)
        
        struct tm timeinfo;
        // 将now时间转换为本地时间,并存储在timeinfo中
        localtime_r(&now, &timeinfo);

        // char time_save_buf[64];
        strftime(time_save_buf, sizeof(time_save_buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
        printf("现在时间:%s\n", time_save_buf);

        vTaskDelay(1000 / portTICK_PERIOD_MS); // 每秒打印一次
    }
}

LVGL的刷新显示

需注意,想要实时显示时间,需要刷新显示时间的容器

这里使用使用LVGL定时器回调函数进行刷新显示(1s)

创建定时器:

c 复制代码
    //使用LVGL(Light and Versatile Graphics Library)创建一个定时器
    lv_timer_create(update_time_cb, 1000, NULL);  // 每 1000ms 更新一次

定时器回调

c 复制代码
//更新日期显示(1s回调)
void update_time_cb(lv_timer_t *timer)
{
    if (time_label) {//time_label为显示时间的容器
        lv_label_set_text(time_label, time_save_buf);//将时间重新写入容器
    }
}

12.2、HTTP获取天气信息

与上文获取时间流程基本一致,源码过长不做展示

效果:

流程:

通过http发送获取本地位置信息(城市),再将城市信息添加进请求URL中向心知天气API请求,获取到天气信息通过CJson解析出城市和气温保存到字符数组,将字符数组显示到界面上

idf_component.yml配置

yml 复制代码
## IDF Component Manager Manifest File
dependencies:
  espressif/esp32-camera: "^2.0.10" # 摄像头驱动
  lvgl/lvgl: "~8.3.0"
  espressif/esp_lvgl_port: "~1.4.0" # LVGL接口
  espressif/esp_lcd_touch_ft5x06: "~1.0.6" # 触摸屏驱动

  protocol_examples_common: # MQTT
    path: ${IDF_PATH}/examples/common_components/protocol_examples_common

更改完idf_component.yml配置需要重新选择芯片,才能下载组件,选完芯片需要重新配置menuconfig

menuconfig配置

配置Flash大小

Flash size

配置使用分区表

Partition Table

分区表

partitions.csv

txt 复制代码
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  24k
phy_init, data, phy,     0xf000,  4k
factory,  app,  factory, ,        8M
storage,  data, spiffs,  ,        3M

wifi配置

配置账号密码

lvgl配置

选择14,24号字体

Font:

选择大字体

若是遇到色彩失真:

设置LVGL色彩:

Component config

LVGL configuration

Color settings

结束语(源码):

博主目前在广州,深圳找实习。

如有大佬(HR)能推荐实习,恳请私信我(主页侧边栏有wx二维码),帮帮小弟,万分感谢

由于添加的组件和图片过多,上传不了github(限制25MB),就不上传了,我放网盘

代码运行需先配置wifi联网,配置MQTT连接参数,心知天气秘钥

否则得先注释网络部分
wifi扫描连接(博主代码填入配置连接自动),音乐播放器,蓝牙HID还未移植,可自行移植,或移植立创开发板实战派s3掌机例程相关部分

链接:https://caiyun.139.com/w/i/2nFZ6kCfsGdhz

提取码:gu6c

复制内容打开移动云盘PC客户端,操作更方便哦
如果文章对你有所帮助,可以帮我点一下左下角推荐该文,万分感谢

博主目前在广州,深圳找实习。

如有大佬(HR,BOSS)能推荐实习,恳请私信小弟给个机会,帮帮小弟,万分感谢