# LVGL_UI **Repository Path**: NetADs/lvgl_ui ## Basic Information - **Project Name**: LVGL_UI - **Description**: 保存LVGL的UI界面。 创建在ESP32-S3-N16R8开发板上,显示在2寸320x240SPI接口,ST7789V驱动的TFT_LCD屏。嵌入代码用ESP-IDF架构,用LVGL9.5.0组件开发UI界面。UI架构使用当前流行的简洁清晰易于维护的架构。 创建一个主屏幕,以时钟显示为主“00:00:00”大字体显示。日期黄历,当天天气及三天预报交替显示为辅。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-27 - **Last Updated**: 2026-07-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LVGL UI Component Library 可移植的 LVGL UI 组件库,为 ESP-IDF 项目提供通用 UI 屏幕集合。与 display 组件配合,从硬件驱动到 UI 渲染全链路封装。 ## 功能特性 - 标准 ESP-IDF 组件结构 - 统一初始化/反初始化 (`lvgl_ui_init()` / `lvgl_ui_deinit()`) - 通过抽象驱动接口挂载任意显示驱动 (`lvgl_ui_display_attach(&drv)`) - 不直接依赖特定显示硬件,适配 ST7789 / ILI9341 / 模拟器等任意驱动 - 页面管理引擎 (`lvgl_ui_page_switch()`) - 内部线程安全:所有公开 API 自动加锁 (`lvgl_ui_lock` / `lvgl_ui_unlock`) - 支持 320x240 横屏显示 - 模块化设计,每个屏幕独立封装 - 通过 Kconfig 可独立控制每个屏幕的编译开关及屏幕尺寸 - 经典图标版:32 号自定义天气图标字体(基于和风天气图标集),当天天气与三天预报全面使用图标 - LVGL v9.5.0 API 实现(依赖已锁定 `lvgl/lvgl: 9.5.0`) ## 架构:显示层与驱动层解耦 ``` +-------------------+ +-------------------+ | lvgl_ui | | 应用层 | | (显示层/UI引擎) | | (display 等驱动) | +-------------------+ +-------------------+ | - LVGL 初始化 | --注入--> | display_* 函数 | | - 页面管理 | lvgl_ui_ | 注册到 | | - 线程安全 | display_ | lvgl_ui_display_ | | - flush 回调 | driver_t | driver_t | +-------------------+ +-------------------+ | | v v lvgl_ui_display_attach(&drv) display_draw_area() 回调读取分辨率/发送像素 发送像素到硬件 SPI drv.get_width() / get_height() drv.draw_area() ``` - **驱动层** (display): 管理物理硬件,始终使用物理分辨率 (240x320)。通过填充 `lvgl_ui_display_driver_t` 结构体将函数指针注入 lvgl_ui - **显示层** (lvgl_ui): 管理 LVGL 逻辑渲染空间 (320x240 横屏),通过回调接口驱动任意显示硬件,不直接依赖特定驱动 ## 目录结构 ``` lvgl_ui/ ├── CMakeLists.txt ├── Kconfig ├── idf_component.yml ├── README.md ├── LICENSE ├── include/ │ ├── lvgl_ui.h │ ├── colors.h │ ├── ui_config.h │ ├── weather_types.h │ ├── lvgl_chinese_font.h │ └── screens/ │ ├── ui_welcome.h │ ├── ui_weather_clock.h │ ├── ui_weather_clock_modern.h │ └── ui_weather_clock_v40.h ├── src/ │ ├── lvgl_ui.c │ ├── fonts/ │ │ ├── lv_font_chinese_16.c │ │ ├── lv_font_chinese_20.c │ │ ├── lv_font_chinese_24.c │ │ ├── lv_font_chinese_40.c │ │ ├── lv_font_weather_24.c │ │ ├── lv_font_weather_32.c │ │ └── lv_font_weather_40.c │ └── screens/ │ ├── ui_welcome.c │ ├── ui_weather_clock.c │ ├── ui_weather_clock_modern.c │ └── ui_weather_clock_v40.c └── examples/ ├── welcome_demo/ ├── weather_clock_demo/ └── weather_clock_v40_demo/ ``` ## 包含的屏幕 | 屏幕名称 | 头文件 | Kconfig 开关 | 描述 | |---------|--------|-------------|------| | Welcome | `lvgl_ui_welcome_init()` | `LVGL_UI_INCLUDE_WELCOME` | 欢迎页面,居中显示标题文字、进度条和状态信息 | | Weather Clock (Classic) | `lvgl_ui_weather_clock_init()` | `LVGL_UI_INCLUDE_WEATHER_CLOCK` | 实时时钟经典样式,左右双栏布局 | | Weather Clock (Modern) | `lvgl_ui_weather_clock_modern_init()` | `LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN` | 实时时钟现代样式,卡片式自动轮播面板,玻璃拟态风格 | | Weather Clock (Classic Icon) | `lvgl_ui_weather_clock_v40_init()` | `LVGL_UI_INCLUDE_WEATHER_CLOCK_V40` | 经典双栏布局 + 和风天气图标字体:当天天气(FONT_WEATHER_32) + 三天预报文字在前图标在后(FONT_WEATHER_24) | ## 安装方法 ### 方式一:复制到 components 目录 ```bash cp -r lvgl_ui /path/to/your/project/components/ cp -r display /path/to/your/project/components/ ``` ### 方式二:使用 ESP-IDF 组件管理器(推荐) 在项目的 `idf_component.yml` 中添加: ```yaml dependencies: display: git: https://gitee.com/NetADs/display.git lvgl_ui: git: https://gitee.com/NetADs/lvgl_ui.git ``` ## 快速开始 ```c #include "display.h" #include "lvgl_ui.h" void app_main(void) { // 1. 驱动层:初始化硬件显示(物理分辨率 240x320) display_config_t cfg = { .width = 240, .height = 320, }; ESP_ERROR_CHECK(display_init(&cfg)); // 2. 驱动层:旋转到横屏 (仅操作 MADCTL 寄存器,不修改逻辑尺寸) display_set_rotation(DISPLAY_ROTATION_90); // 3. 显示层:初始化 LVGL 引擎(tick + handler 任务) lvgl_ui_init(); // 4. 显示层:构建驱动接口并挂载到 LVGL // 注意:draw_area 契约为 bool(true=成功);display_draw_area 返回 esp_err_t(ESP_OK==0), // 必须做 ==ESP_OK 转换,否则成功时会被当成失败触发双重 flush_ready。 #include "esp_err.h" static bool draw_area_wrapper(int x, int y, int w, int h, const void *data) { return display_draw_area(x, y, w, h, data) == ESP_OK; } lvgl_ui_display_driver_t drv = { .is_ready = display_is_ready, .get_io = display_get_io, .get_width = display_get_width, // 旋转后返回有效分辨率(如 320) .get_height = display_get_height, // (如 240) .draw_area = draw_area_wrapper, }; lvgl_ui_display_attach(&drv); // 5. 创建并切换页面 lv_obj_t* welcome = lvgl_ui_welcome_init(NULL); lvgl_ui_page_switch(welcome); lvgl_ui_welcome_set_text(welcome, "Hello!"); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } } ``` ## 使用示例 ### 欢迎页面 + 进度条 ```c lv_obj_t* welcome = lvgl_ui_welcome_init(NULL); lvgl_ui_page_switch(welcome); lvgl_ui_welcome_set_status_text(welcome, "正在连接 WiFi..."); lvgl_ui_welcome_set_progress(welcome, 10); vTaskDelay(pdMS_TO_TICKS(2000)); lvgl_ui_welcome_set_status_text(welcome, "WiFi 连接成功"); lvgl_ui_welcome_set_progress(welcome, 25); ``` ### 天气时钟(完整示例) ```c lv_obj_t* clock = lvgl_ui_weather_clock_init(NULL); lvgl_ui_page_switch(clock); /* 更新时间(含农历和四柱) */ clock_data_t time_data = { .hour = 14, .minute = 30, .second = 0, .day = 6, .month = 7, .year = 2026, .is_24h_format = true, .show_seconds = true, .day_name = "星期一", .lunar_date = "甲子鼠年正月初一", .four_pillars = "甲子年甲子月甲子日甲子时", }; lvgl_ui_weather_clock_update_time(clock, &time_data); /* 更新天气数据 */ weather_data_t weather = { .current = WEATHER_SUNNY, .temp_current = 28, .temp_high = 32, .temp_low = 22, .humidity = 60, .wind_direction = "东北", .wind_scale = "3-4", .location = "天津", .forecast = { { .type = WEATHER_SUNNY, .temp_high = 32, .temp_low = 22, .day_name = "今天" }, { .type = WEATHER_CLOUDY, .temp_high = 30, .temp_low = 21, .day_name = "明天" }, { .type = WEATHER_RAINY, .temp_high = 28, .temp_low = 19, .day_name = "后天" }, }, }; lvgl_ui_weather_clock_update_weather(clock, &weather); lvgl_ui_weather_clock_set_24h_format(clock, false); ``` ### 经典图标版天气时钟(独立使用示例) 仅启用 `CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK_V40=y`,不依赖经典版或现代版。右面板温度使用 32 号天气图标字体。 ```c #include "lvgl_ui.h" void app_main(void) { /* 驱动层: display_init + display_set_rotation + lvgl_ui_display_attach ... */ lv_obj_t* clock = lvgl_ui_weather_clock_v40_init(NULL); lvgl_ui_page_switch(clock); clock_data_t time_data = { .hour = 14, .minute = 30, .second = 0, .day = 6, .month = 7, .year = 2026, .is_24h_format = true, .show_seconds = true, .day_name = "星期一", .lunar_date = "甲子鼠年正月初一", .four_pillars = "甲子年甲子月甲子日甲子时", }; lvgl_ui_weather_clock_v40_update_time(clock, &time_data); weather_data_t weather = { .current = WEATHER_SUNNY, .temp_current = 28, .temp_high = 32, .temp_low = 22, .humidity = 60, .wind_direction = "东北", .wind_scale = "3-4", .location = "天津", .forecast = { { .type = WEATHER_SUNNY, .temp_high = 32, .temp_low = 22, .day_name = "明天" }, { .type = WEATHER_CLOUDY, .temp_high = 30, .temp_low = 21, .day_name = "后天" }, { .type = WEATHER_RAINY, .temp_high = 28, .temp_low = 19, .day_name = "周四" }, }, }; lvgl_ui_weather_clock_v40_update_weather(clock, &weather); } ``` ### 页面切换 ```c // 切换到新页面(自动删除旧页面:pre_free → lv_obj_del → free) lv_obj_t* clock = lvgl_ui_weather_clock_init(NULL); lvgl_ui_page_switch(clock); // 手动删除 lvgl_ui_welcome_delete(welcome); ``` ## API 参考 ### 线程安全 | 函数 | 描述 | |------|------| | `lvgl_ui_lock()` | 获取 LVGL 互斥锁(内部使用) | | `lvgl_ui_unlock()` | 释放 LVGL 互斥锁(内部使用) | 所有公开 API 内部自动加锁/解锁。外部任务调用 API 无需手动处理锁。内部回调(如 lv_timer、flush callback)运行在 handler 任务中,已持有锁。 > ⚠️ **线程安全约束(重要)** > > 1. **依赖递归锁**:`lvgl_ui_lock()` 底层调用 `lv_lock()`,**要求 LVGL 的 `lv_lock()` 必须实现为递归互斥锁**(FreeRTOS 默认使用 `xSemaphoreCreateRecursiveMutex`,安全)。若外部项目替换了 LVGL OSAL 层或使用 `LV_OS_NONE`,可能导致死锁。 > > 2. **禁止已在锁中再调 API**:外部代码不应在已持有 `lv_lock()` 或 `lvgl_ui_lock()` 的情况下调用 lvgl_ui 公开 API(导致重入锁)。所有 lvgl_ui API 内部已自动加锁,外部无需预先取锁。 > > 3. **事件回调 / timer 回调中调 API**:LVGL timer 和事件回调运行在 handler 任务上下文中,此时 handler 任务已持有 `lv_lock()`。在这些回调中调用 lvgl_ui API 会触发 `lv_lock()` 重入,**必须确保 `lv_lock()` 为递归锁**。 > > 4. **`lvgl_ui_deinit()` 中 `lv_obj_del` 触发事件回调**:`cleanup_active_page()` 内部调用 `lv_obj_del()` 删除页面对象树,可能触发外部注册的 LVGL 事件回调。若回调中调用了 lvgl_ui API,同一任务将重入 `lv_lock()`。同样依赖递归锁安全。 ### 核心 API | 函数 | 描述 | |------|------| | `lvgl_ui_init()` | 初始化 LVGL、创建 tick/handler 任务 | | `lvgl_ui_deinit()` | 清理所有资源 | | `lvgl_ui_display_attach(driver)` | 挂载显示驱动接口,创建 LVGL 显示设备 | | `lvgl_ui_page_switch(new_page)` | 切换活动页面 | **状态码:** | 枚举值 | 说明 | |--------|------| | `LVGL_UI_OK` | 操作成功 | | `LVGL_UI_ERR_TICK_TASK_FAILED` | tick 任务创建失败 | | `LVGL_UI_ERR_HANDLER_TASK_FAILED` | handler 任务创建失败 | | `LVGL_UI_ERR_DISPLAY_FAILED` | 显示驱动初始化失败 | | `LVGL_UI_ERR_INVALID_PARAM` | 参数无效 | **可配置宏(在包含头文件前定义):** | 宏 | 默认值 | 说明 | |----|--------|------| | `LVGL_UI_TICK_TASK_STACK` | 4096 | tick 任务栈大小 | | `LVGL_UI_HANDLER_TASK_STACK` | 8192 | handler 任务栈大小 | | `LVGL_UI_TICK_TASK_PRIO` | 5 | tick 任务优先级 | | `LVGL_UI_HANDLER_TASK_PRIO` | 5 | handler 任务优先级 | | `LVGL_UI_DISPLAY_BUF_LINES` | 40 | 每缓冲区像素行数 | ### 欢迎页面 API | 函数 | 描述 | |------|------| | `lvgl_ui_welcome_init(parent)` | 创建欢迎页面 | | `lvgl_ui_welcome_set_text(obj, text)` | 更新标题文字 | | `lvgl_ui_welcome_set_progress(obj, pct)` | 设置进度条 (0-100) | | `lvgl_ui_welcome_set_status_text(obj, text)` | 设置状态文字 | | `lvgl_ui_welcome_delete(obj)` | 删除欢迎页面 | ### 天气时钟 API (经典版/经典图标版共通模式,仅函数名不同) | 函数 | 描述 | |------|------| | `lvgl_ui_weather_clock_init(parent)` | 创建经典版天气时钟 | | `lvgl_ui_weather_clock_v40_init(parent)` | 创建经典图标版天气时钟 | | `lvgl_ui_weather_clock_update_time(obj, data)` | 更新时间数据(含农历、四柱) | | `lvgl_ui_weather_clock_v40_update_time(obj, data)` | 同上(V40 版) | | `lvgl_ui_weather_clock_update_weather(obj, data)` | 更新天气数据 | | `lvgl_ui_weather_clock_v40_update_weather(obj, data)` | 同上(V40 版) | | `lvgl_ui_weather_clock_set_24h_format(obj, enable)` | 24/12 小时制 | | `lvgl_ui_weather_clock_v40_set_24h_format(obj, enable)` | 同上(V40 版) | | `lvgl_ui_weather_clock_show_seconds(obj, enable)` | 显示/隐藏秒数 | | `lvgl_ui_weather_clock_v40_show_seconds(obj, enable)` | 同上(V40 版) | | `lvgl_ui_weather_clock_delete(obj)` | 删除经典版天气时钟 | | `lvgl_ui_weather_clock_v40_delete(obj)` | 删除经典图标版天气时钟 | `clock_data_t` 新增字段: | 字段 | 类型 | 说明 | |------|------|------| | `lunar_date[32]` | char[] | 农历日期,如 `"甲子鼠年正月初一"` | | `four_pillars[48]` | char[] | 四柱,如 `"甲子年甲子月甲子日甲子时"` | > 四柱字符串若为 12 个汉字(36 字节),内部自动折行显示为 `"甲子年 甲子月\n甲子日 甲子时"` 两行。 ## 字体管理契约 本组件中每种字体均配有独立的 Kconfig 编译开关,遵循 LVGL 官方字体管理风格。字体文件与屏幕页面解耦,屏幕通过 `select` 声明依赖。 ``` menu "LVGL UI Component" ├── [屏幕开关] │ ├── LVGL_UI_INCLUDE_WELCOME (default y) │ ├── LVGL_UI_INCLUDE_WEATHER_CLOCK (default y) │ ├── LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN (default y) │ └── LVGL_UI_INCLUDE_WEATHER_CLOCK_V40 (default y) │ ├── select LVGL_UI_WEATHER_FONT_24 │ ├── select LVGL_UI_WEATHER_FONT_32 │ └── select LVGL_UI_WEATHER_FONT_40 ├── [天气图标字体] │ ├── LVGL_UI_WEATHER_FONT_24 (default n) │ ├── LVGL_UI_WEATHER_FONT_32 (default n) │ └── LVGL_UI_WEATHER_FONT_40 (default n) └── [中文字体] ├── LVGL_UI_CHINESE_FONT_16 (default y) ├── LVGL_UI_CHINESE_FONT_20 (default y) ├── LVGL_UI_CHINESE_FONT_24 (default y) └── LVGL_UI_CHINESE_FONT_40 (default y) ``` **契约要点**: 1. **页面互不影响**:每个屏幕使用专属 API(`lvgl_ui_weather_clock_*` 与 `lvgl_ui_weather_clock_v40_*` 函数族各自独立),修改一个页面的字体不影响其他页面。 2. **字体独立编译**:7 个字体文件均有独立的 `CONFIG_LVGL_UI_*` 开关,可精确控制闪存占用。 3. **依赖声明**:屏幕通过 Kconfig `select` 声明所需字体,无需手动勾选。自定义屏幕也可直接启用任何字体选项。 4. **中文字体默认开启**:4 个中文字体 `default y`,适用于所有页面公共文字显示。如不使用中文可单独关闭。 5. **天气字体按需拉取**:3 个天气字体 `default n`,仅当启用 `WEATHER_CLOCK_V40` 或手动开启时才编译,避免浪费闪存。 > 新增或修改字体时,必须同步更新 Kconfig、CMakeLists.txt 的条件编译块、`lvgl_chinese_font.h` 的 `extern` 声明和 `#define` 宏。 ## 配置选项 本组件所有 Kconfig 选项默认值均为 `n`,应用方按需显式开启。通过 `idf.py menuconfig` 或 `sdkconfig.defaults` 配置。 ### 预设配置方案 根目录提供多套预设配置文件,复制为 `sdkconfig.defaults` 即可使用。 **单页面方案**: | 预设文件 | 包含页面 | |---------|---------| | `sdkconfig.defaults.welcome` | Welcome | | `sdkconfig.defaults.weather_classic` | Classic | | `sdkconfig.defaults.weather_modern` | Modern | | `sdkconfig.defaults.weather_v40` | V40(天气字体由 select 自动拉取) | **Welcome 组合方案**: | 预设文件 | 包含页面 | |---------|---------| | `sdkconfig.defaults.welcome_classic` | Welcome + Classic | | `sdkconfig.defaults.welcome_modern` | Welcome + Modern | | `sdkconfig.defaults.welcome_v40` | Welcome + V40 | **全量方案**: | 预设文件 | 包含页面 | |---------|---------| | `sdkconfig.defaults.all` | Welcome + Classic + Modern + V40 | 使用示例: ```bash # 欢迎页面 + 经典图标版天气时钟 cp sdkconfig.defaults.welcome_v40 sdkconfig.defaults idf.py build ``` **完整选项列表**: | 配置项 | 默认值 | 说明 | |--------|--------|------| | `LVGL_UI_SCREEN_WIDTH` | 320 | 逻辑屏幕宽度 | | `LVGL_UI_SCREEN_HEIGHT` | 240 | 逻辑屏幕高度 | | `LVGL_UI_INCLUDE_WELCOME` | y | 是否编译欢迎页面 | | `LVGL_UI_INCLUDE_WEATHER_CLOCK` | y | 是否编译经典天气时钟 | | `LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN` | y | 是否编译现代天气时钟 | | `LVGL_UI_INCLUDE_WEATHER_CLOCK_V40` | y | 是否编译经典图标版天气时钟 | | `LVGL_UI_CHINESE_FONT_16` | y | 是否包含 16px 中文字体 | | `LVGL_UI_CHINESE_FONT_20` | y | 是否包含 20px 中文字体 | | `LVGL_UI_CHINESE_FONT_24` | y | 是否包含 24px 中文字体 | | `LVGL_UI_CHINESE_FONT_40` | y | 是否包含 40px 中文字体(抗锯齿,~450KB flash) | | `LVGL_UI_WEATHER_FONT_24` | n | 是否包含 24px 天气图标字体(~5KB flash) | | `LVGL_UI_WEATHER_FONT_32` | n | 是否包含 32px 天气图标字体(~26KB flash) | | `LVGL_UI_WEATHER_FONT_40` | n | 是否包含 40px 天气图标字体(~42KB flash) | > V40 经典图标版启用后通过 `select` 自动拉取 `WEATHER_FONT_{24,32,40}` 三个天气字体。 > 关闭 `WEATHER_CLOCK_V40` 不会自动关闭天气字体(需手动设 n),方便在自定义屏幕中单独使用天气图标字体。 > > 各页面字体依赖关系: > > | 页面 | 左面板字体 | 右面板温度/图标字体 | 预报图标 | > |------|----------|-------------------|---------| > | Classic | FONT_CN_40/20/16 | FONT_CN_24 | 中文文字 | > | Modern | FONT_CN_40/20/16 | FONT_CN_24 | 中文文字 | > | V40 | FONT_CN_40/20/16 | FONT_WEATHER_32 | FONT_WEATHER_24 | 所有配置项均支持通过 `sdkconfig.defaults` 覆写默认值,例如只启用经典图标版天气时钟: ```ini CONFIG_LVGL_UI_INCLUDE_WELCOME=y CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK=n CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK_MODERN=n CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK_V40=y ``` ## 横屏模式说明 驱动层(display 组件)以**物理分辨率 (240x320)** 初始化,`display_set_rotation(DISPLAY_ROTATION_90)` 会同时: 1. 操作 ST7789 硬件 MADCTL 寄存器完成坐标映射(swap_xy + mirror); 2. **自动交换有效分辨率**,使 `display_get_width()/get_height()` 返回旋转后的逻辑尺寸(横屏 320x240)。 因此显示层(lvgl_ui)在 320x240 逻辑空间渲染,并**直接取 `display_get_width()/get_height()`** 即可得到正确尺寸,无需在应用层维护本地横屏尺寸。LVGL flush 回调把逻辑坐标直接传给 `display_draw_area()`,硬件侧 swap_xy 已开启,自动完成物理地址映射。 > ⚠️ **调用顺序铁律**:`display_set_rotation(90)` 必须在 `lvgl_ui_display_attach()` **之前**调用,否则 LVGL 仍按 240x320 建显示设备,横屏右侧刷新区会被 `validate_area()` 拒绝而卡死。 > > ⚠️ **色序**:ST7789V 2.0" 屏多为 **BGR** 走线。若整屏红蓝互换,在 `sdkconfig.defaults` 设 `CONFIG_DISPLAY_RGB_ELEMENT_ORDER_BGR=y`(或 `display_config_t.rgb_order = DISPLAY_RGB_ORDER_BGR`);若改完更糟则改回 RGB。 ## 示例项目 ### welcome_demo ```bash cd examples/welcome_demo idf.py set-target esp32s3 idf.py build flash monitor ``` ### weather_clock_demo ```bash cd examples/weather_clock_demo idf.py set-target esp32s3 idf.py build flash monitor ``` ### weather_clock_v40_demo 经典图标版独立示例,使用 32 号天气图标字体显示右面板温度。关闭经典版和现代版,仅启用 `CONFIG_LVGL_UI_INCLUDE_WEATHER_CLOCK_V40`。 ```bash cd examples/weather_clock_v40_demo idf.py set-target esp32s3 idf.py build flash monitor ``` ## 添加新屏幕 ### 1. 创建头文件 `include/screens/ui_yourscreen.h`: ```c #pragma once #include "lvgl.h" lv_obj_t* lvgl_ui_yourscreen_init(lv_obj_t* parent); void lvgl_ui_yourscreen_delete(lv_obj_t* screen); ``` ### 2. 创建实现文件 `src/screens/ui_yourscreen.c`: ```c #include "screens/ui_yourscreen.h" #include "lvgl_ui.h" typedef struct { lvgl_ui_page_pre_free_fn pre_free; lv_obj_t* container; } ui_yourscreen_t; static void your_pre_free(void* user_data) { } lv_obj_t* lvgl_ui_yourscreen_init(lv_obj_t* parent) { lvgl_ui_lock(); if (parent == NULL) parent = lv_scr_act(); ui_yourscreen_t* page = calloc(1, sizeof(ui_yourscreen_t)); if (page == NULL) { lvgl_ui_unlock(); return NULL; } page->pre_free = your_pre_free; page->container = lv_obj_create(parent); lv_obj_set_size(page->container, 320, 240); lv_obj_set_user_data(page->container, page); lvgl_ui_unlock(); return page->container; } void lvgl_ui_yourscreen_delete(lv_obj_t* obj) { if (obj == NULL) return; lvgl_ui_lock(); ui_yourscreen_t* page = lv_obj_get_user_data(obj); if (page) { if (page->pre_free) page->pre_free(page); lv_obj_del(obj); free(page); } else { lv_obj_del(obj); } lvgl_ui_unlock(); } ``` ### 3. 在 `Kconfig` 中添加配置开关,在 `CMakeLists.txt` 中添加条件编译,在 `lvgl_ui.h` 中添加条件 `#include` ## 依赖项 - ESP-IDF v5.5+ - LVGL v9.5.0(依赖锁定,详见 `idf_component.yml`) - [display](https://gitee.com/NetADs/display) - ST7789 显示驱动组件 ## 兼容性 ESP32 / ESP32-S2 / ESP32-S3 / ESP32-C3 ## 许可证 MIT License ## 更新日志 ### v1.4.0 - 精简接口:移除 `get_panel` 回调字段,驱动接口仅保留 5 个必要字段 - 移除未使用的异步信号量机制,简化 init/deinit 流程 - 移除 `LVGL_UI_ERR_SEMAPHORE_FAILED` 状态码 - 移除天气时钟未实现的桩函数(`update_weather`、`set_theme`、`set_brightness`) - 移除 `weather_to_string()`、`weather_to_icon()` 未使用内联函数 - 移除 `theme_variant_t`、`brightness_mode_t` 未使用枚举类型 - 修正 `lvgl_ui_deinit()` 资源释放顺序:先显存后任务后 lv_deinit - 统一屏幕尺寸引用,`ui_welcome.c` 改为使用 `ui_config.h` - 移除 `ui_weather_clock_bak.c` 未编译备份文件 ### v1.3.0 - 显示驱动解耦:`lvgl_ui_display_attach()` 改为接受 `lvgl_ui_display_driver_t` 回调结构体 - 移除对 display 组件的编译依赖,CMakeLists.txt 不再 `REQUIRES display` - flush 回调改为异步 DMA 模式,DMA 完成后才通知 LVGL - handler 任务循环间隔从 10ms 降至 5ms - 新增 `lvgl_ui_display_driver_t` 显示驱动抽象接口定义 ### v1.2.0 - 集成 display 组件:`lvgl_ui_display_attach()` 无参数,内部通过 `display_get_panel()` 等读取硬件信息 - flush 回调改用 `display_draw_area()` 发送像素,不再直接操作 `esp_lcd_panel_handle_t` - 线程安全:所有公开 API 内部封装 `lvgl_ui_lock()` / `lvgl_ui_unlock()` - 新增异步信号量机制 - 新增错误码 `LVGL_UI_ERR_SEMAPHORE_FAILED` - 屏幕文件(ui_welcome / ui_weather_clock)所有公开函数加锁保护 ### v1.1.0 - 新增统一初始化/反初始化接口 - 新增页面管理引擎 - 新增页面预释放回调约定 ### v1.0.0 - 初始版本