/** * bg_gif_demo.c — 背景图 + 透明 GIF 叠加(方案 C 实现) */ #include "bg_gif_demo.h" #include "fatfs.h" #include "esp_log.h" #include "esp_heap_caps.h" #include "esp_lvgl_port.h" #include "esp_spiffs.h" #include "lvgl.h" #include "jpeg_decoder.h" #include #include #include static const char *TAG = "BG_GIF"; // 运行时状态 static lv_obj_t *g_bg_img = NULL; static lv_obj_t *g_gif_obj = NULL; static uint8_t *g_bg_data = NULL; // 背景图解码后的 RGB565 buffer (PSRAM) static uint8_t *g_gif_data = NULL; // GIF 文件二进制 (PSRAM) static lv_img_dsc_t g_bg_dsc; static lv_img_dsc_t g_gif_dsc; static bool g_running = false; // 确保 SPIFFS 已挂载 static esp_err_t ensure_spiffs_mounted(void) { size_t total = 0, used = 0; if (esp_spiffs_info("storage", &total, &used) == ESP_OK) { return ESP_OK; } ESP_LOGI(TAG, "SPIFFS 未挂载,自动挂载..."); esp_vfs_spiffs_conf_t conf = { .base_path = "/spiflash", .partition_label = "storage", .max_files = 5, .format_if_mount_failed = false, }; return esp_vfs_spiffs_register(&conf); } // 加载 GIF 文件到 PSRAM static esp_err_t load_gif_to_psram(const char *path) { FILE *f = fopen(path, "rb"); if (!f) { ESP_LOGE(TAG, "GIF 打开失败: %s", path); return ESP_FAIL; } fseek(f, 0, SEEK_END); size_t sz = ftell(f); fseek(f, 0, SEEK_SET); g_gif_data = heap_caps_malloc(sz, MALLOC_CAP_SPIRAM); if (!g_gif_data) { ESP_LOGE(TAG, "GIF PSRAM 分配失败: %d bytes", (int)sz); fclose(f); return ESP_ERR_NO_MEM; } if (fread(g_gif_data, 1, sz, f) != sz) { heap_caps_free(g_gif_data); g_gif_data = NULL; fclose(f); return ESP_FAIL; } fclose(f); // 构造 GIF 的 lv_img_dsc_t(CF_RAW,由 LVGL gifdec 解码) memset(&g_gif_dsc, 0, sizeof(g_gif_dsc)); g_gif_dsc.header.cf = LV_IMG_CF_RAW; g_gif_dsc.data_size = sz; g_gif_dsc.data = g_gif_data; ESP_LOGI(TAG, "GIF 已加载到 PSRAM: %s (%.1f KB)", path, sz / 1024.0); return ESP_OK; } // 加载并解码背景 JPG 到 RGB565 buffer static esp_err_t load_bg_jpg(const char *path) { esp_jpeg_image_output_t outdata; esp_err_t ret = DecodeImg((char *)path, &g_bg_data, &outdata); if (ret != ESP_OK || !g_bg_data) { ESP_LOGE(TAG, "背景图解码失败: %s, err=%s", path, esp_err_to_name(ret)); return ESP_FAIL; } // 构造 lv_img_dsc_t(TRUE_COLOR,已解码 RGB565) memset(&g_bg_dsc, 0, sizeof(g_bg_dsc)); g_bg_dsc.header.cf = LV_IMG_CF_TRUE_COLOR; g_bg_dsc.header.w = outdata.width; g_bg_dsc.header.h = outdata.height; g_bg_dsc.data_size = outdata.width * outdata.height * 2; g_bg_dsc.data = g_bg_data; ESP_LOGI(TAG, "背景图已解码: %dx%d (%.1f KB RGB565)", outdata.width, outdata.height, g_bg_dsc.data_size / 1024.0); return ESP_OK; } esp_err_t bg_gif_demo_start(const char *bg_jpg_path, const char *gif_path) { if (g_running) { ESP_LOGW(TAG, "已在运行,先 stop"); return ESP_ERR_INVALID_STATE; } if (ensure_spiffs_mounted() != ESP_OK) { ESP_LOGE(TAG, "SPIFFS 挂载失败"); return ESP_FAIL; } // 1. 加载背景图(一次性,常驻) if (load_bg_jpg(bg_jpg_path) != ESP_OK) { return ESP_FAIL; } // 2. 加载 GIF if (load_gif_to_psram(gif_path) != ESP_OK) { free(g_bg_data); g_bg_data = NULL; return ESP_FAIL; } // 3. 在 LVGL 任务中创建图层 if (!lvgl_port_lock(200)) { ESP_LOGE(TAG, "lvgl_port_lock 失败"); return ESP_FAIL; } // 底层:背景图(lv_img) g_bg_img = lv_img_create(lv_scr_act()); lv_img_set_src(g_bg_img, &g_bg_dsc); lv_obj_align(g_bg_img, LV_ALIGN_CENTER, 0, 0); // 上层:透明 GIF(lv_gif,自动叠加在 bg_img 之上) g_gif_obj = lv_gif_create(lv_scr_act()); lv_gif_set_src(g_gif_obj, &g_gif_dsc); // 240×360 GIF 在 360×360 屏幕中居中显示 lv_obj_align(g_gif_obj, LV_ALIGN_CENTER, 0, 0); lvgl_port_unlock(); g_running = true; ESP_LOGI(TAG, "✓ 背景 + GIF 叠加显示启动"); return ESP_OK; } void bg_gif_demo_stop(void) { if (!g_running) return; g_running = false; if (lvgl_port_lock(200)) { if (g_gif_obj) { lv_obj_del(g_gif_obj); g_gif_obj = NULL; } if (g_bg_img) { lv_obj_del(g_bg_img); g_bg_img = NULL; } lvgl_port_unlock(); } if (g_bg_data) { free(g_bg_data); g_bg_data = NULL; } if (g_gif_data) { heap_caps_free(g_gif_data); g_gif_data = NULL; } ESP_LOGI(TAG, "已停止并释放资源"); } bool bg_gif_demo_is_running(void) { return g_running; } esp_err_t bg_gif_demo_switch_gif(const char *new_gif_path) { if (!g_running) { ESP_LOGW(TAG, "bg_gif_demo 未启动,无法切换"); return ESP_ERR_INVALID_STATE; } if (!new_gif_path) { return ESP_FAIL; } // 去重:同路径重复调用无副作用 static char last_gif_path[64] = {0}; if (strcmp(new_gif_path, last_gif_path) == 0) { return ESP_OK; } if (!lvgl_port_lock(200)) { ESP_LOGE(TAG, "switch_gif: lvgl_port_lock 失败"); return ESP_FAIL; } // 1. 先释放旧 GIF PSRAM(确保切换期间峰值只占用一份) if (g_gif_data) { heap_caps_free(g_gif_data); g_gif_data = NULL; } // 2. 加载新 GIF(复用 load_gif_to_psram 逻辑) if (load_gif_to_psram(new_gif_path) != ESP_OK) { lvgl_port_unlock(); ESP_LOGE(TAG, "switch_gif: 加载失败 %s", new_gif_path); return ESP_FAIL; } // 3. 更新 LVGL(lv_gif_set_src 内部会重建解码器 + 重启定时器) lv_gif_set_src(g_gif_obj, &g_gif_dsc); // 4. set_src 内部会重建 10ms 定时器,重设为 20ms 降低 CPU 占用 // (CLAUDE.md "lv_gif_set_src 会重建定时器" 经验) lv_gif_t *gifobj = (lv_gif_t *)g_gif_obj; if (gifobj->timer) { lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量 } lvgl_port_unlock(); strncpy(last_gif_path, new_gif_path, sizeof(last_gif_path) - 1); last_gif_path[sizeof(last_gif_path) - 1] = '\0'; ESP_LOGI(TAG, "✓ 切换 GIF: %s", new_gif_path); return ESP_OK; }