## 核心变更 ### 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>
1043 lines
34 KiB
C
1043 lines
34 KiB
C
#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;
|
||
}
|
||
}
|
||
|
||
// 快速渲染帧到 canvas(palette 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;
|
||
}
|
||
}
|
||
}
|