Rdzleo 14776acb0a feat: 完成 AI/吧唧双模式完全隔离重构 + 触摸坐标日志 + SPIFFS 预烧录
## 核心变更

### 1. 双模式完全隔离 (Phase 2+4)
- 拆分 InitializeButtons() 为 InitializeBadgeModeButtons() + InitializeAiModeButtons()
- 构造函数按 device_mode 分支:吧唧模式不创建 PowerSaveTimer/BackgroundTask
- 吧唧模式不注册音量/故事按键回调,避免调用 GetAudioCodec() 崩溃
- GPIO0 由 iot_button 统一处理,dzbj_button 仅注册 KEY2(GPIO4)
- SetDeviceState() 中 background_task_ 空指针保护

### 2. 吧唧模式 BOOT 按键崩溃修复
- 新增 dzbj_boot_click_handler()(C 函数,避免 lvgl.h 与 display.h 冲突)
- 移植 dzbj 的唤醒屏幕/退出手电筒/返回Home 完整逻辑

### 3. esp_timer 阻塞 LVGL 渲染修复
- iot_button 回调在 esp_timer 任务中执行,vTaskDelay 会阻塞 lv_tick_inc
- 改为 xTaskCreate 派发到独立 FreeRTOS 任务,避免冻结 LVGL 渲染

### 4. 触摸坐标日志 + SPIFFS 预烧录
- esp_lvgl_port_touch.c 添加触摸坐标打印
- CMakeLists.txt 添加 spiffs_create_partition_image 自动打包 spiffs_image/

### 5. dzbj 模块文件新增
- device_mode: NVS 设备模式管理 (AI=0/吧唧=1)
- dzbj_button: GPIO4 KEY2 中断 + BOOT 点击处理
- dzbj_ble: BLE GATT 图传服务 (0x0B00)
- dzbj_battery: ADC 电池电压监测
- sleep_mgr: 10s 超时熄屏低功耗管理
- pages: 图片浏览/GIF播放/PWM亮度
- fatfs: SPIFFS 文件管理

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:23:04 +08:00

1043 lines
34 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "lvgl.h"
#if LV_USE_GIF
#include "extra/libs/gif/lv_gif.h"
#include "extra/libs/gif/gifdec.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#endif
#include "esp_heap_caps.h"
#include "fatfs.h"
#include "pages_pwm.h"
#include "esp_lvgl_port.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "jpeg_decoder.h"
#include "screens/ui_ScreenImg.h"
#include <inttypes.h>
// 前向声明界面切换函数
extern void _ui_screen_change(lv_obj_t **target, lv_scr_load_anim_t fademode, int spd, int delay, void (*target_init)(void));
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>
#include <strings.h>
#include <stdbool.h>
#include <unistd.h>
char img_path[40];
char *img_filename;
lv_obj_t *app_img;
lv_obj_t *act_mainscreen;
uint8_t *app_img_data = 0;
esp_jpeg_image_output_t outdata;
lv_img_dsc_t image;
#define MAX_IMAGE_FILES 10
#define MAX_FILENAME_LEN 32
static char spiffs_image_files[MAX_IMAGE_FILES][MAX_FILENAME_LEN];
static int spiffs_image_count = 0;
static int current_image_index = 0;
static bool image_list_initialized = false;
#if LV_USE_GIF
// === 自定义 GIF 播放器(替代 lv_gif性能优化 ===
// 优化: 1.Palette LUT查表 2.TRUE_COLOR无Alpha 3.后台线程解码流水线
static gd_GIF *gif_decoder = NULL; // gifdec 解码句柄
static lv_obj_t *gif_img_obj = NULL; // 普通 lv_img 控件(替代 lv_gif
static uint16_t *gif_rgb565_buf[2] = {NULL, NULL}; // 双缓冲 RGB565 帧 (PSRAM)
static lv_img_dsc_t gif_frame_dsc; // LVGL 图片描述符TRUE_COLOR无Alpha
static uint16_t gif_palette_lut[256]; // 调色板 RGB565 查找表
static volatile uint8_t gif_front_idx = 0; // 当前显示的缓冲区索引
static lv_timer_t *gif_play_timer = NULL; // LVGL 播放定时器
static TaskHandle_t gif_decode_task_handle = NULL; // 后台解码任务句柄
static volatile bool gif_playing = false; // 播放状态标志
static volatile bool gif_new_frame_ready = false; // 新帧就绪标志
static uint32_t gif_last_frame_ms = 0; // 上一帧显示时间戳
static uint8_t *gif_psram_buf = NULL; // GIF 文件数据PSRAM
static bool current_is_gif = false; // 当前是否为 GIF 模式
// 前向声明
static bool is_gif_file(const char *filename);
static void gif_player_start(void);
static void gif_player_stop(void);
#endif // LV_USE_GIF
// 从NVS中读取图片路径
esp_err_t nvs_read_img(void) {
nvs_handle_t nvs_handle; // NVS 句柄
esp_err_t err; // NVS 错误码
err = nvs_open("config", NVS_READONLY, &nvs_handle);// 打开 NVS 句柄
if (err != ESP_OK) return err; // 如果打开失败,返回错误码
size_t imgname_len;
err = nvs_get_str(nvs_handle, "img_filename", NULL, &imgname_len);// 获取图片路径长度
if (err == ESP_OK) {
img_filename = malloc(imgname_len);// 分配内存
err = nvs_get_str(nvs_handle, "img_filename", img_filename, &imgname_len);// 获取图片路径
if (err != ESP_OK) {
nvs_close(nvs_handle);// 关闭 NVS 句柄
return err; // 如果获取失败,返回错误码
}
ESP_LOGI("NVS", "img_filename: %s", img_filename);// 打印图片路径
}
nvs_close(nvs_handle);// 关闭 NVS 句柄
return err;
}
// 测试改变NVS中的图片路径
esp_err_t nvs_change_img(char *imgname) {
nvs_handle_t nvs_handle;// NVS 句柄
esp_err_t err;
err = nvs_open("config", NVS_READWRITE, &nvs_handle);// 打开 NVS 句柄
if (err != ESP_OK) goto close_handle;
err = nvs_set_str(nvs_handle, "img_filename", imgname);// 设置图片路径
if (err != ESP_OK) goto close_handle;
err = nvs_commit(nvs_handle);// 提交更改
if (err != ESP_OK) goto close_handle;// 如果提交失败,关闭句柄并返回错误码
close_handle:
nvs_close(nvs_handle); // 关闭 NVS 句柄
return err;
}
// 仅更新现有图片,显示其他图片
// img_name: 图片文件名为NULL时从NVS读取
void app_img_change(const char *img_name){
// 释放之前的图片数据
if(app_img_data){
free(app_img_data);
app_img_data = NULL;
ESP_LOGI("IMG", "释放之前显示的图片数据缓存");
}
const char *current_img_name = img_name;
// 如果没有指定图片名从NVS读取
if(!current_img_name) {
esp_err_t ret_nvs = nvs_read_img();// 从NVS中读取图片路径
if(ret_nvs != ESP_OK){
ESP_LOGE("NVS","图片路径获取失败2");
return;
}
current_img_name = img_filename;
}
// 构建图片路径
snprintf(img_path, sizeof(img_path), "/spiflash/%s", current_img_name);// 格式化图片路径
ESP_LOGI("IMG", "准备显示图片: %s, 路径: %s", current_img_name, img_path);
// 检查文件是否存在
struct stat file_stat;
if(stat(img_path, &file_stat) != 0) {
ESP_LOGE("IMG", "文件不存在: %s", img_path);
return;
}
ESP_LOGI("IMG", "文件大小: %ld 字节", file_stat.st_size);
// 解码图片
esp_err_t ret = DecodeImg(img_path,&app_img_data,&outdata);// 解码图片
if(ret == ESP_OK){
ESP_LOGI("IMG", "图片解码成功,数据地址: %p, 宽度: %d, 高度: %d",
app_img_data, outdata.width, outdata.height);
// 检查解码后的数据
if(app_img_data == NULL) {
ESP_LOGE("IMG", "解码数据为空");
return;
}
// 配置图片数据
image.header.cf = LV_IMG_CF_TRUE_COLOR;
image.header.always_zero = 0;
image.header.reserved = 0;
image.header.w = outdata.width;
image.header.h = outdata.height;
image.data_size = outdata.output_len;
image.data = app_img_data;
// 获取屏幕对象
act_mainscreen = lv_scr_act();
if(act_mainscreen == NULL) {
ESP_LOGE("IMG", "获取屏幕对象失败");
return;
}
// 如果图片对象不存在,创建它
if(app_img == NULL) {
app_img = lv_img_create(act_mainscreen);
if(app_img == NULL) {
ESP_LOGE("IMG", "创建图片对象失败");
return;
}
lv_obj_center(app_img);
ESP_LOGI("IMG", "创建图片对象成功");
}
// 更新图片显示
lvgl_port_lock(0);// 锁定LVGL端口
lv_img_set_src(app_img, &image);// 设置图片源
lv_scr_load(act_mainscreen);// 加载主屏幕
lvgl_port_unlock();// 解锁LVGL端口
ESP_LOGI("IMG", "图片显示成功: %s", current_img_name);
} else {
ESP_LOGE("IMG", "图片解码失败,错误码: %d", ret);
}
}
// 完整的图片显示初始化
void app_img_display(){
ESP_LOGI("IMG", "开始显示图片");
esp_err_t ret_nvs = nvs_read_img();// 从NVS中读取图片路径
if(ret_nvs != ESP_OK){
ESP_LOGE("NVS","图片路径获取失败1");
return;
}
ESP_LOGI("IMG", "图片路径: %s", img_filename);
snprintf(img_path, sizeof(img_path), "/spiflash/%s",img_filename);// 格式化图片路径
ESP_LOGI("IMG", "完整路径: %s", img_path);
// 检查文件是否存在
struct stat file_stat;
if(stat(img_path, &file_stat) != 0){
ESP_LOGE("IMG", "文件不存在");
return;
}
ESP_LOGI("IMG", "文件大小: %ld 字节", file_stat.st_size);
esp_err_t ret = DecodeImg(img_path,&app_img_data,&outdata);// 解码图片
if(ret == ESP_OK){
ESP_LOGI("IMG", "图片解码成功,数据地址: %p", app_img_data);
// 检查解码后的数据
if(app_img_data == NULL){
ESP_LOGE("IMG", "解码数据为空");
return;
}
image.header.cf = LV_IMG_CF_TRUE_COLOR;
image.header.always_zero = 0;
image.header.reserved = 0;
image.header.w = outdata.width;
image.header.h = outdata.height;
image.data_size = outdata.output_len;
image.data = app_img_data;
ESP_LOGI("IMG", "LV_IMG_CF_RGB565 值: %d", LV_IMG_CF_RGB565);
ESP_LOGI("IMG", "设置图片数据: 宽度=%lu, 高度=%lu, 数据大小=%lu", (unsigned long)image.header.w, (unsigned long)image.header.h, (unsigned long)image.data_size);
act_mainscreen = lv_scr_act();// 获取当前主屏幕对象
if(act_mainscreen == NULL){
ESP_LOGE("IMG", "获取屏幕对象失败");
return;
}
ESP_LOGI("IMG", "获取屏幕对象成功");
app_img = lv_img_create(act_mainscreen);// 创建图片对象
if(app_img == NULL){
ESP_LOGE("IMG", "创建图片对象失败");
return;
}
ESP_LOGI("IMG", "创建图片对象成功");
lvgl_port_lock(0);// 锁定LVGL端口
ESP_LOGI("IMG", "设置图片源前");
lv_img_set_src(app_img, &image);// 设置图片源
ESP_LOGI("IMG", "设置图片源后");
lv_obj_center(app_img);// 居中显示图片
ESP_LOGI("IMG", "居中显示图片后");
lv_scr_load(act_mainscreen);// 加载主屏幕
ESP_LOGI("IMG", "加载主屏幕后");
lvgl_port_unlock();// 解锁LVGL端口
vTaskDelay(50);// 延时50ms
pwm_init();// 初始化PWM
ESP_LOGI("IMG", "图片显示完成");
} else {
ESP_LOGE("IMG", "图片解码失败,错误码: %d", ret);
}
}
// 新的显示测试屏幕函数
void app_test_display(){
lvgl_port_lock(0);// 锁定LVGL端口
// 获取或创建屏幕对象
lv_obj_t *screen = lv_scr_act();
if(screen == NULL) {
screen = lv_obj_create(NULL);// 创建屏幕对象
if(screen == NULL) {
ESP_LOGE("TEST", "Failed to create screen object");// 创建屏幕对象失败
lvgl_port_unlock();// 解锁LVGL端口
return;
}
}
// 清空屏幕
lv_obj_clean(screen);
// 创建标签
lv_obj_t *label = lv_label_create(screen);
if(label) {
lv_label_set_text(label, "Test Screen\nLCD is working!");// 设置标签文本
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
// 创建基本样式
static lv_style_t style;// 基本样式
lv_style_init(&style);// 初始化基本样式
lv_style_set_text_font(&style, &lv_font_montserrat_14);// 设置字体
lv_style_set_text_color(&style, lv_color_hex(0xf9076a));// 设置文本颜色
lv_style_set_bg_color(&style, lv_color_hex(0x000000));// 设置背景颜色
lv_obj_add_style(label, &style, 0);// 添加样式到标签
}
lv_scr_load(screen);// 加载屏幕
lvgl_port_unlock();// 解锁LVGL端口
}
// 图片循环显示任务 - 显示spiffs中的所有图片
void img_loop_task(void *pvParameters) {
// 存储SPIFFS中的图片文件名
char *image_files[10]; // 最多支持10张图片
int file_count = 0;
int current_index = 0;
static bool backlight_initialized = false;
// 初始化背光(只执行一次)
if(!backlight_initialized) {
pwm_init();
backlight_initialized = true;// 初始化背光
ESP_LOGI("IMG_LOOP", "背光初始化完成");
}
while(1) {
// 重新扫描SPIFFS中的图片文件
ESP_LOGI("IMG_LOOP", "开始扫描SPIFFS中的图片文件");
// 打开SPIFFS目录
DIR *dir = opendir("/spiflash");
if(!dir) {
ESP_LOGE("IMG_LOOP", "无法打开SPIFFS目录");
vTaskDelay(pdMS_TO_TICKS(3000));
continue;
}
// 重置文件计数
file_count = 0;
// 遍历目录
struct dirent *entry;
while((entry = readdir(dir)) != NULL && file_count < 10) {
// 检查是否是图片文件(.jpg, .jpeg, .png等
const char *name = entry->d_name;
int len = strlen(name);
if(len > 4) {
const char *ext = name + len - 4;
if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 ||
strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0) {
// 存储图片文件名
image_files[file_count] = strdup(name);
ESP_LOGI("IMG_LOOP", "发现图片文件: %s", name);
file_count++;
}
}
}
closedir(dir);
// 检查是否找到图片
if(file_count == 0) {
ESP_LOGE("IMG_LOOP", "未找到图片文件");
vTaskDelay(pdMS_TO_TICKS(3000));
continue;
}
ESP_LOGI("IMG_LOOP", "共发现 %d 张图片,开始循环显示", file_count);
// 循环显示所有图片
for(current_index = 0; current_index < file_count; current_index++) {
const char *current_image = image_files[current_index];
ESP_LOGI("IMG_LOOP", "显示图片 %d/%d: %s", current_index + 1, file_count, current_image);
// 使用修改后的app_img_change函数显示图片
app_img_change(current_image);
// 等待3秒
vTaskDelay(pdMS_TO_TICKS(3000));
}
// 释放文件名内存
for(int i = 0; i < file_count; i++) {
free(image_files[i]);
}
// 再次扫描前短暂延时
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 图片切换任务 - 显示指定的两张图片
void img_switch_task(void *pvParameters) {
char *image_files[] = {"default.jpg", "02.jpg"};
int file_count = 2;
int current_index = 0;
while(1) {
// 使用修改后的app_img_change函数显示图片
const char *current_image = image_files[current_index];
ESP_LOGI("IMG_SWITCH", "切换到图片: %s", current_image);
app_img_change(current_image);
// 切换到下一张图片
current_index = (current_index + 1) % file_count;
// 等待2秒
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 初始化SPIFFS图片列表
void init_spiffs_image_list(void) {
if(image_list_initialized) {
ESP_LOGI("IMG_LIST", "图片列表已初始化,跳过");
return;
}
ESP_LOGI("IMG_LIST", "开始扫描SPIFFS中的图片文件");
// 打开SPIFFS目录
DIR *dir = opendir("/spiflash");
if(!dir) {
ESP_LOGE("IMG_LIST", "无法打开SPIFFS目录");
return;
}
// 重置文件计数
spiffs_image_count = 0;
// 遍历目录
struct dirent *entry;
while((entry = readdir(dir)) != NULL && spiffs_image_count < MAX_IMAGE_FILES) {
// 检查是否是图片文件(.jpg, .jpeg, .png等
const char *name = entry->d_name;
int len = strlen(name);
if(len > 4 && len < MAX_FILENAME_LEN) {
const char *ext = name + len - 4;
if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 ||
strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0
#if LV_USE_GIF
|| strcasecmp(ext, ".gif") == 0
#endif
) {
// 存储图片文件名到静态缓冲区
strncpy(spiffs_image_files[spiffs_image_count], name, MAX_FILENAME_LEN - 1);
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
ESP_LOGI("IMG_LIST", "发现图片文件: %s", name);
spiffs_image_count++;
}
}
}
closedir(dir);
// 检查是否找到图片
if(spiffs_image_count == 0) {
ESP_LOGE("IMG_LIST", "未找到图片文件");
return;
}
image_list_initialized = true;
ESP_LOGI("IMG_LIST", "图片列表初始化完成,共发现 %d 张图片", spiffs_image_count);
// 查找default.jpg并设置为当前索引
for(int i = 0; i < spiffs_image_count; i++) {
if(strcmp(spiffs_image_files[i], "default.jpg") == 0) {
current_image_index = i;
ESP_LOGI("IMG_LIST", "设置默认图片索引: %d", current_image_index);
break;
}
}
}
// 获取下一张图片
const char* get_next_image(void) {
if(!image_list_initialized || spiffs_image_count == 0) {
ESP_LOGE("IMG_LIST", "图片列表未初始化或为空");
return NULL;
}
current_image_index = (current_image_index + 1) % spiffs_image_count;
ESP_LOGI("IMG_LIST", "切换到下一张图片,索引: %d/%d", current_image_index + 1, spiffs_image_count);
return spiffs_image_files[current_image_index];
}
// 获取上一张图片
const char* get_prev_image(void) {
if(!image_list_initialized || spiffs_image_count == 0) {
ESP_LOGE("IMG_LIST", "图片列表未初始化或为空");
return NULL;
}
current_image_index = (current_image_index - 1 + spiffs_image_count) % spiffs_image_count;
ESP_LOGI("IMG_LIST", "切换到上一张图片,索引: %d/%d", current_image_index + 1, spiffs_image_count);
return spiffs_image_files[current_image_index];
}
// 重置图片列表
void free_spiffs_image_list(void) {
if(!image_list_initialized) {
return;
}
spiffs_image_count = 0;
current_image_index = 0;
image_list_initialized = false;
ESP_LOGI("IMG_LIST", "图片列表已重置");
}
// 根据文件名设置当前图片索引
bool set_image_index_by_name(const char *name) {
if(!image_list_initialized || spiffs_image_count == 0 || !name) {
return false;
}
for(int i = 0; i < spiffs_image_count; i++) {
if(strcmp(spiffs_image_files[i], name) == 0) {
current_image_index = i;
ESP_LOGI("IMG_LIST", "设置图片索引为 %d: %s", i, name);
return true;
}
}
ESP_LOGW("IMG_LIST", "未找到图片: %s", name);
return false;
}
// BLE接收图片后导航到ScreenImg显示
void ble_image_navigate(const char *filename) {
// 将新文件直接追加到列表(避免重扫 SPIFFS 目录,节省 ~200ms
if (!image_list_initialized) {
init_spiffs_image_list();
}
// 检查文件是否已在列表中(避免重复)
bool found = false;
for (int i = 0; i < spiffs_image_count; i++) {
if (strcmp(spiffs_image_files[i], filename) == 0) {
current_image_index = i;
found = true;
break;
}
}
if (!found && spiffs_image_count < MAX_IMAGE_FILES) {
strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1);
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
current_image_index = spiffs_image_count;
spiffs_image_count++;
}
// 检查是否已在ScreenImg界面
lvgl_port_lock(0);
bool already_on_screen = (lv_scr_act() == ui_ScreenImg);
if (!already_on_screen) {
// 不在ScreenImg导航过去SCREEN_LOADED事件会触发update_ui_ImgBle
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
}
lvgl_port_unlock();
// 已在ScreenImg时_ui_screen_change不会触发SCREEN_LOADED需手动更新图片
if (already_on_screen) {
update_ui_ImgBle(filename);
}
ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename);
}
// BLE接收图片后导航显示携带预加载数据跳过 SPIFFS 重读)
void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size) {
// 将新文件追加到列表
if (!image_list_initialized) {
init_spiffs_image_list();
}
bool found = false;
for (int i = 0; i < spiffs_image_count; i++) {
if (strcmp(spiffs_image_files[i], filename) == 0) {
current_image_index = i;
found = true;
break;
}
}
if (!found && spiffs_image_count < MAX_IMAGE_FILES) {
strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1);
spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0';
current_image_index = spiffs_image_count;
spiffs_image_count++;
}
#if LV_USE_GIF
// 如果有预加载数据且是 GIF直接用内存数据显示跳过 SPIFFS 重读)
if (data && data_size > 0 && is_gif_file(filename)) {
// 停止旧 GIF 播放器
gif_player_stop();
if (gif_psram_buf) {
free(gif_psram_buf);
}
// BLE 数据直接作为 GIF 源(所有权转移)
gif_psram_buf = data;
// 打开 gifdec 解码器(从 PSRAM 内存源)
gif_decoder = gd_open_gif_data(gif_psram_buf);
if (!gif_decoder) {
ESP_LOGE("GIF", "gifdec 打开失败");
free(gif_psram_buf);
gif_psram_buf = NULL;
return;
}
// 确保在 ScreenImg 界面
lvgl_port_lock(0);
bool already_on_screen = (lv_scr_act() == ui_ScreenImg);
if (!already_on_screen) {
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
}
lvgl_port_unlock();
// 启动自定义 GIF 播放器
gif_player_start();
ESP_LOGI("IMG_LIST", "BLE GIF直通显示(优化): %s", filename);
return;
}
#endif // LV_USE_GIF
// 非 GIF 或无预加载数据,释放 BLE 数据,走常规 SPIFFS 路径
if (data) {
free(data);
}
lvgl_port_lock(0);
bool already_on_screen = (lv_scr_act() == ui_ScreenImg);
if (!already_on_screen) {
_ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init);
}
lvgl_port_unlock();
if (already_on_screen) {
update_ui_ImgBle(filename);
}
ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename);
}
// 获取当前图片文件名
const char* get_current_image(void) {
if(!image_list_initialized || spiffs_image_count == 0) {
ESP_LOGE("IMG_LIST", "图片列表未初始化或为空");
return NULL;
}
return spiffs_image_files[current_image_index];
}
// 删除当前图片并从列表中移除
bool delete_current_image(void) {
if(!image_list_initialized || spiffs_image_count == 0) {
ESP_LOGE("IMG_DEL", "图片列表未初始化或为空");
return false;
}
const char *current_img = spiffs_image_files[current_image_index];
// 构建完整路径
char full_path[64];
snprintf(full_path, sizeof(full_path), "/spiflash/%s", current_img);
ESP_LOGI("IMG_DEL", "准备删除图片: %s", full_path);
// 从SPIFFS文件系统中删除文件
if(unlink(full_path) != 0) {
ESP_LOGE("IMG_DEL", "删除文件失败: %s", full_path);
return false;
}
ESP_LOGI("IMG_DEL", "文件删除成功: %s", current_img);
// 从列表中移除该图片
for(int i = current_image_index; i < spiffs_image_count - 1; i++) {
strncpy(spiffs_image_files[i], spiffs_image_files[i + 1], MAX_FILENAME_LEN);
}
// 更新计数
spiffs_image_count--;
// 如果列表为空,重置状态
if(spiffs_image_count == 0) {
ESP_LOGI("IMG_DEL", "所有图片已删除");
image_list_initialized = false;
current_image_index = 0;
return true;
}
// 调整当前索引(如果删除的是最后一张,回到第一张)
if(current_image_index >= spiffs_image_count) {
current_image_index = 0;
}
ESP_LOGI("IMG_DEL", "图片列表已更新,剩余 %d 张图片,当前索引: %d",
spiffs_image_count, current_image_index);
return true;
}
#if LV_USE_GIF
// 判断文件是否为 GIF 格式
static bool is_gif_file(const char *filename) {
int len = strlen(filename);
if (len < 4) return false;
return strcasecmp(filename + len - 4, ".gif") == 0;
}
// === GIF 播放器内部函数 ===
// 构建调色板 RGB565 查找表(每次帧解码前调用,处理局部调色板)
static void gif_build_palette_lut(gd_Palette *palette) {
for (int i = 0; i < palette->size; i++) {
uint8_t r = palette->colors[i * 3 + 0];
uint8_t g = palette->colors[i * 3 + 1];
uint8_t b = palette->colors[i * 3 + 2];
lv_color_t c = lv_color_make(r, g, b); // 自动处理 LV_COLOR_16_SWAP
gif_palette_lut[i] = c.full;
}
}
// 快速渲染帧到 canvaspalette LUT 替代逐像素 lv_color_make
static void gif_render_frame_fast(gd_GIF *gif) {
int i = gif->fy * gif->width + gif->fx;
for (int j = 0; j < gif->fh; j++) {
for (int k = 0; k < gif->fw; k++) {
uint8_t index = gif->frame[(gif->fy + j) * gif->width + gif->fx + k];
if (!gif->gce.transparency || index != gif->gce.tindex) {
uint16_t c = gif_palette_lut[index];
gif->canvas[(i + k) * 3 + 0] = c & 0xff;
gif->canvas[(i + k) * 3 + 1] = (c >> 8) & 0xff;
gif->canvas[(i + k) * 3 + 2] = 0xff;
}
}
i += gif->width;
}
}
// canvas (RGB565+Alpha 3字节/像素) -> RGB565 (2字节/像素) 快速拷贝
static void gif_canvas_to_rgb565(gd_GIF *gif, uint16_t *out) {
uint8_t *canvas = gif->canvas;
int total = gif->width * gif->height;
for (int i = 0; i < total; i++) {
out[i] = (uint16_t)canvas[i * 3] | ((uint16_t)canvas[i * 3 + 1] << 8);
}
}
// 后台解码任务FreeRTOS与 LVGL 显示并行)
static void gif_decode_task(void *pvParameters) {
while (1) {
// 等待解码请求
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (!gif_playing || !gif_decoder) break;
// LZW 解码(含 dispose 处理)
int ret = gd_get_frame(gif_decoder);
if (ret == 0) {
// GIF 循环播放
gd_rewind(gif_decoder);
ret = gd_get_frame(gif_decoder);
}
if (ret < 0 || !gif_playing) break;
// 更新调色板 LUT可能有局部调色板变化
gif_build_palette_lut(gif_decoder->palette);
// 快速渲染到 canvas + 拷贝到后台 RGB565 缓冲
gif_render_frame_fast(gif_decoder);
uint8_t back = 1 - gif_front_idx;
gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[back]);
// 标记新帧就绪
gif_new_frame_ready = true;
}
gif_decode_task_handle = NULL;
vTaskDelete(NULL);
}
// LVGL 定时器回调(检查新帧并切换显示)
static void gif_play_timer_cb(lv_timer_t *t) {
if (!gif_playing || !gif_decoder || !gif_img_obj) return;
// 检查 GIF 帧延时
uint32_t delay_ms = gif_decoder->gce.delay * 10;
if (delay_ms < 20) delay_ms = 20;
uint32_t elapsed = lv_tick_elaps(gif_last_frame_ms);
if (elapsed < delay_ms) return;
if (!gif_new_frame_ready) return;
gif_last_frame_ms = lv_tick_get();
gif_new_frame_ready = false;
// 切换前后缓冲
uint8_t back = 1 - gif_front_idx;
gif_front_idx = back;
// 更新 LVGL 图片源TRUE_COLOR无 Alpha 混合)
gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[gif_front_idx];
lv_img_cache_invalidate_src(&gif_frame_dsc);
lv_obj_invalidate(gif_img_obj);
// 通知后台线程解码下一帧
if (gif_decode_task_handle) {
xTaskNotifyGive(gif_decode_task_handle);
}
}
// 启动自定义 GIF 播放器
static void gif_player_start(void) {
if (!gif_decoder || !gif_psram_buf) return;
uint16_t w = gif_decoder->width;
uint16_t h = gif_decoder->height;
size_t buf_size = w * h * sizeof(uint16_t);
// 分配双缓冲 RGB565 帧 (PSRAM)
for (int i = 0; i < 2; i++) {
if (!gif_rgb565_buf[i]) {
gif_rgb565_buf[i] = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
if (!gif_rgb565_buf[i]) {
ESP_LOGE("GIF", "RGB565 缓冲分配失败: %d", i);
return;
}
}
memset(gif_rgb565_buf[i], 0, buf_size);
}
// 初始化调色板 LUT
gif_build_palette_lut(gif_decoder->palette);
// 同步解码第一帧(确保立即显示)
int ret = gd_get_frame(gif_decoder);
if (ret <= 0) {
ESP_LOGE("GIF", "首帧解码失败");
return;
}
gif_render_frame_fast(gif_decoder);
gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[0]);
gif_front_idx = 0;
gif_new_frame_ready = false;
gif_last_frame_ms = lv_tick_get();
// 配置 LVGL 图片描述符TRUE_COLOR无 Alpha
gif_frame_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
gif_frame_dsc.header.always_zero = 0;
gif_frame_dsc.header.reserved = 0;
gif_frame_dsc.header.w = w;
gif_frame_dsc.header.h = h;
gif_frame_dsc.data_size = buf_size;
gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[0];
// 创建 lv_img 控件 + LVGL 播放定时器
lvgl_port_lock(0);
lv_obj_add_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN);
gif_img_obj = lv_img_create(ui_ScreenImg);
lv_obj_set_size(gif_img_obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_align(gif_img_obj, LV_ALIGN_CENTER);
lv_obj_clear_flag(gif_img_obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_move_background(gif_img_obj);
lv_img_set_src(gif_img_obj, &gif_frame_dsc);
gif_play_timer = lv_timer_create(gif_play_timer_cb, 10, NULL);
lvgl_port_unlock();
// 启动后台解码任务
gif_playing = true;
current_is_gif = true;
xTaskCreatePinnedToCore(gif_decode_task, "gif_dec", 4096, NULL, 5, &gif_decode_task_handle, 1);
// 触发后台解码第2帧
xTaskNotifyGive(gif_decode_task_handle);
ESP_LOGI("GIF", "播放器启动: %dx%d, 双缓冲 %dKB×2", w, h, buf_size / 1024);
}
// 停止自定义 GIF 播放器
static void gif_player_stop(void) {
// 停止后台解码任务
gif_playing = false;
if (gif_decode_task_handle) {
xTaskNotifyGive(gif_decode_task_handle);
// 等待任务退出(最多 500ms
for (int i = 0; i < 50 && gif_decode_task_handle; i++) {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 删除 LVGL 定时器和控件
lvgl_port_lock(0);
if (gif_play_timer) {
lv_timer_del(gif_play_timer);
gif_play_timer = NULL;
}
if (gif_img_obj) {
lv_obj_del(gif_img_obj);
gif_img_obj = NULL;
}
lvgl_port_unlock();
// 关闭 gifdec 解码器
if (gif_decoder) {
gd_close_gif(gif_decoder);
gif_decoder = NULL;
}
// 释放 RGB565 双缓冲
for (int i = 0; i < 2; i++) {
if (gif_rgb565_buf[i]) {
free(gif_rgb565_buf[i]);
gif_rgb565_buf[i] = NULL;
}
}
current_is_gif = false;
gif_new_frame_ready = false;
}
// 清理 GIF 资源(公开接口,供界面切换时调用)
void pages_cleanup_gif(void) {
gif_player_stop();
// 释放 GIF 源文件缓冲区
if (gif_psram_buf) {
free(gif_psram_buf);
gif_psram_buf = NULL;
}
}
#endif // LV_USE_GIF
// 更新ui_ImgBle控件的图片支持 JPEG
void update_ui_ImgBle(const char *img_name) {
if(!img_name) {
ESP_LOGE("IMG_UI", "图片名为空");
return;
}
if(!ui_ImgBle) {
ESP_LOGE("IMG_UI", "ui_ImgBle控件不存在");
return;
}
static uint8_t *ui_img_data = NULL;
static lv_img_dsc_t ui_image;
// 构建图片路径
snprintf(img_path, sizeof(img_path), "/spiflash/%s", img_name);
ESP_LOGI("IMG_UI", "准备显示图片: %s, 路径: %s", img_name, img_path);
// 检查文件是否存在
struct stat file_stat;
if(stat(img_path, &file_stat) != 0) {
ESP_LOGE("IMG_UI", "文件不存在: %s", img_path);
return;
}
ESP_LOGI("IMG_UI", "文件大小: %ld 字节", file_stat.st_size);
#if LV_USE_GIF
if (is_gif_file(img_name)) {
// === GIF 显示路径自定义播放器Palette LUT + 无Alpha + 后台解码) ===
// 释放之前的 JPEG 数据
if(ui_img_data) {
free(ui_img_data);
ui_img_data = NULL;
}
// 停止旧 GIF 播放器 + 释放旧 GIF 数据
gif_player_stop();
if (gif_psram_buf) {
free(gif_psram_buf);
gif_psram_buf = NULL;
}
// 将 GIF 文件整体读入 PSRAM
FILE *gif_file = fopen(img_path, "rb");
if (!gif_file) {
ESP_LOGE("IMG_UI", "GIF文件打开失败: %s", img_path);
return;
}
gif_psram_buf = heap_caps_malloc(file_stat.st_size, MALLOC_CAP_SPIRAM);
if (!gif_psram_buf) {
ESP_LOGE("IMG_UI", "PSRAM分配失败: %ld 字节", file_stat.st_size);
fclose(gif_file);
return;
}
fread(gif_psram_buf, 1, file_stat.st_size, gif_file);
fclose(gif_file);
// 打开 gifdec 解码器(从 PSRAM 内存源)
gif_decoder = gd_open_gif_data(gif_psram_buf);
if (!gif_decoder) {
ESP_LOGE("IMG_UI", "gifdec 打开失败: %s", img_name);
free(gif_psram_buf);
gif_psram_buf = NULL;
return;
}
// 启动自定义 GIF 播放器Palette LUT + 双缓冲流水线)
gif_player_start();
ESP_LOGI("IMG_UI", "GIF显示启动(优化): %s", img_name);
} else
#endif // LV_USE_GIF
{
// === JPEG 显示路径 ===
#if LV_USE_GIF
// 停止 GIF 播放器并释放资源
gif_player_stop();
if (gif_psram_buf) {
free(gif_psram_buf);
gif_psram_buf = NULL;
}
// 恢复 JPEG 控件显示
lvgl_port_lock(0);
lv_obj_clear_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN);
lvgl_port_unlock();
#endif
// 释放之前的图片数据
if(ui_img_data) {
free(ui_img_data);
ui_img_data = NULL;
ESP_LOGI("IMG_UI", "释放之前的图片数据");
}
// 解码图片
esp_jpeg_image_output_t ui_outdata;
esp_err_t ret = DecodeImg(img_path, &ui_img_data, &ui_outdata);
if(ret == ESP_OK) {
ESP_LOGI("IMG_UI", "图片解码成功,宽度: %d, 高度: %d", ui_outdata.width, ui_outdata.height);
if(ui_img_data == NULL) {
ESP_LOGE("IMG_UI", "解码数据为空");
return;
}
// 配置图片数据
ui_image.header.cf = LV_IMG_CF_TRUE_COLOR;
ui_image.header.always_zero = 0;
ui_image.header.reserved = 0;
ui_image.header.w = ui_outdata.width;
ui_image.header.h = ui_outdata.height;
ui_image.data_size = ui_outdata.output_len;
ui_image.data = ui_img_data;
lvgl_port_lock(0);
lv_img_set_src(ui_ImgBle, &ui_image);
lvgl_port_unlock();
ESP_LOGI("IMG_UI", "JPEG图片更新成功: %s", img_name);
} else {
ESP_LOGE("IMG_UI", "图片解码失败,错误码: %d", ret);
ui_img_data = NULL;
}
}
}