代码改动: - AudioLoop 加 vTaskDelay(1),让出 Core 1 idle task 防 WiFi/RTC 饥饿 - BackgroundTask 优先级 2 → 5,提升 Opus 解码实时性 - LVGL 刷新 5ms → 16ms (60Hz),CPU 占用降 60% - GIF 定时器 20ms → 33ms (3 处),PSRAM 流量减半 - AI 字幕推送 100ms 节流,避免 LVGL 锁争抢 - EnterIdleHibernate 清空 audio_decode_queue_,防 standby_sound 残留误触发首帧 - PowerSaveTimer OnEnterSleepMode 加 device_state 守卫,拦截 dialog/connecting 期间关功放(修复欢迎语期间被静音 bug) - 取消开机 ADC 阻塞采样,开机播报响应从 6 秒缩到 < 3 秒 新增规划: - Phase 7 占位文档:电量保护 + PowerSaveTimer 重构 + 唤醒杂音根治 + RTC 抖动缓解 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
228 lines
6.5 KiB
C
228 lines
6.5 KiB
C
/**
|
||
* 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 <stdio.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
|
||
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;
|
||
}
|