Files
P4-USB-transfer/main/LVGL/lvgl_lottie.c
T
zhengxinandCursor 851e347cf5 初始提交卡包 USB 工程
纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:54:50 +08:00

1364 lines
53 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 files **************************************************************************/
#include "lvgl_lottie.h"
#include "lvgl_init.h"
#include "ui.h"
#include "remapping.h"
#include "EMOJI_STORAGE/emoji_storage.h"
#include "MOTOR/motor_control.h"
#include "lvgl_src.h"
#include "esp_log.h"
#include "esp_system.h"
#include <sys/stat.h>
// 【解决方案】外部声明正念活跃标志
extern volatile bool g_mindfulness_active;
/******************************************************* variable declaration ************************************************************************/
static const char *TAG_LVGL = "lvgl";
/* Lottie 资源按 60fps 帧号组织,播放目标统一降到 30fps。 */
#define LOTTIE_SOURCE_FPS 60
#define LOTTIE_TARGET_FPS 30
/* music1 播放期间预解析 music2,用于减少 intro->loop 切换黑屏 */
lv_obj_t *g_music_preload_lottie = NULL;
lv_obj_t *lottie = NULL; //lottie动画对象的指针变量
lv_obj_t *current_lottie = NULL; //当前lottie动画对象的指针变量
bool g_music_preload_ready = false;
char g_music_preload_path[128] = {0};
uint8_t *g_music_preload_fb = NULL;
// Lottie动画相关变量
lv_obj_t *ui_Screen_Lottie = NULL; // 专门用于播放动画的独立屏幕
static uint8_t s_music_listen_passes_completed = 0; /* 本周期内已播完的 listen 段次数(0/1/2) */
/* 通话动画总帧数(接听首段与循环段用) */
static int32_t s_calling_total_frames = 0;
/* 通话表情状态:接听后直接 20-80% 无限循环;挂断由 handle_normal_sticker 直接回默认 */
static bool s_calling_mode_active = false;
/* 通话循环中被「播放音乐」打断:暂停音乐后回到通话 20%-80% 循环(与 resume_music_loop_after_oneshot 对称) */
static bool s_resume_call_after_music_stop = false;
/* 通话中插播其它表情时收到挂断:等当前插播播完再回默认 */
static bool s_hangup_deferred_after_intersperse = false;
volatile char g_target_path[32] = {0}; // 表情调用地址
volatile bool g_ble_play_sticker_active = false;
volatile bool g_ble_play_sticker_pending_valid = false;
/* 播放音乐:intro 一次 → music_listen(40%-75%)持续循环;退出时若在播 intro 则只等播完 */
static bool music_playing_intro = false; /* 正在播 music_emoji1(一次) */
static bool music_playing_loop = false; /* 正在播 music_listen 段(两遍中的一遍或两遍) */
static bool music_exit_pending = false; /* 已收到退出指令,intro 播完后不切 listen */
bool resume_music_loop_after_oneshot = false; /* 持续播放音乐时收到其他指令,播完该指令一次后切回音乐段 */
bool resume_calling_loop_after_oneshot = false; /* 通话循环被插播:播完插播后恢复 calling 20%-80% 段 */
bool resume_ai_respond_loop_after_oneshot = false; /* AI 回复循环被插播:播完插播后恢复 conversation 循环 */
static char music_listen_path_buf[128] = {0}; /* 供 callback 切到 music_listen 用 */
static char music_cycle_intro_path_buf[128] = {0}; /* 大循环内每次回到 intro 用的路径 */
volatile bool g_is_ktv_playing = false; /* KTV 持续循环播放态 */
static char ktv_loop_path_buf[128] = {0}; /* KTV 循环恢复路径 */
static bool s_ai_respond_loop_active = false; /* 正在 conversation 循环 */
static bool s_ai_respond_loop_armed = false; /* 0x0020/21/2E/2F hearing 播完后启动 conversation 循环 */
static char s_ai_respond_loop_path_buf[128] = {0}; /* AI 回复循环路径缓存 */
static char s_ai_respond_loop_paths[2][128] = {{0}}; /* 固定双路径循环:AI_respond / conversation2 */
static uint8_t s_ai_respond_loop_index = 0; /* 当前循环到的路径下标 */
/* 音乐循环被 KTV 抢占时挂起,0x0025 结束后恢复(0x001A/0x001B 在 KTV 期间可取消恢复) */
static bool s_music_suspend_for_ktv_valid = false;
static char s_suspend_intro[128] = {0};
static char s_suspend_listen[128] = {0};
static bool s_suspend_was_intro = false;
static bool s_suspend_was_loop = false;
/* 正念 breath 两遍播放可观测计数(用于 start_cb 打点)。 */
static uint8_t s_mindfulness_pass_started = 0;
/* AI 回复固定双路径:AI_respond + /extflash/0/conversation2.json */
static bool prepare_fixed_ai_respond_loop_paths(void)
{
const char *path_ai = lottie_files[AI_respond_emojo];
const char *path_conversation2 = "/extflash/0/conversation2.json";
struct stat st;
s_ai_respond_loop_paths[0][0] = '\0';
s_ai_respond_loop_paths[1][0] = '\0';
if (path_ai == NULL || path_ai[0] == '\0' || stat(path_ai, &st) != 0)
{
return false;
}
if (stat(path_conversation2, &st) != 0)
{
return false;
}
strncpy(s_ai_respond_loop_paths[0], path_ai, sizeof(s_ai_respond_loop_paths[0]) - 1);
s_ai_respond_loop_paths[0][sizeof(s_ai_respond_loop_paths[0]) - 1] = '\0';
strncpy(s_ai_respond_loop_paths[1], path_conversation2, sizeof(s_ai_respond_loop_paths[1]) - 1);
s_ai_respond_loop_paths[1][sizeof(s_ai_respond_loop_paths[1]) - 1] = '\0';
s_ai_respond_loop_index = 0;
return true;
}
/** 主驾/副驾/左后/右后唤醒:hearing 0~80% 后进入与 0x0020 相同的 conversation 双路径循环,0x0045 停表 */
static bool action_triggers_ai_respond_hearing_sequence(uint16_t action_id)
{
return action_id == emoji_event_awake_diver
|| action_id == emoji_event_awake_passenger
|| action_id == emoji_event_left_rear_wake
|| action_id == emoji_event_right_rear_wake;
}
extern char pending_sentinel_switch_path[128] ; /* NOTIFY_SENTINEL_DO_SWITCH 时使用 */
extern char g_current_lottie_path[128];
extern const char *default_emoji_path;
extern char g_play_lottie_path[128];
extern uint16_t current_emoji_action_id;
/******************************************************* functions definition ************************************************************************/
/**
* 与 LVGL 对象相关的清理;调用方必须已持有 lvgl_api_lock,且 LVGL 尚未 lv_deinit。
* try_preload_music_listen_lottie 在持锁状态下调用。
*/
static void clear_music_preload_unlocked(void)
{
if (g_music_preload_lottie != NULL && g_music_preload_lottie != current_lottie && lv_obj_is_valid(g_music_preload_lottie))
{
lv_obj_del(g_music_preload_lottie);
g_music_preload_lottie = NULL;
}
if (g_music_preload_lottie == current_lottie)
{
/* 预加载对象已成为当前播放对象,buffer 仍在使用中,不能释放 */
g_music_preload_ready = false;
g_music_preload_path[0] = '\0';
return;
}
if (g_music_preload_fb)
{
heap_caps_free(g_music_preload_fb);
g_music_preload_fb = NULL;
}
g_music_preload_lottie = NULL;
g_music_preload_path[0] = '\0';
g_music_preload_ready = false;
}
void clear_music_preload(void)
{
/* lv_deinit 之后 ui_Screen_Lottie 已置 NULL:不得再 lv_obj_del,只释放预加载 raw buffer */
if (ui_Screen_Lottie == NULL)
{
if (g_music_preload_fb)
{
heap_caps_free(g_music_preload_fb);
g_music_preload_fb = NULL;
}
g_music_preload_lottie = NULL;
g_music_preload_path[0] = '\0';
g_music_preload_ready = false;
return;
}
/* 从 LVGL 任务上下文(手势等)调用时已在 example_lvgl_port_task 内持锁,禁止再次 acquire */
if (lvgl_port_task_is_current())
{
clear_music_preload_unlocked();
}
else
{
_lock_acquire(&lvgl_api_lock);
clear_music_preload_unlocked();
_lock_release(&lvgl_api_lock);
}
}
void try_preload_music_listen_lottie(const char *path)
{
if (path == NULL || path[0] == '\0' || ui_Screen_Lottie == NULL)
return;
/* 已经就绪且路径一致,复用 */
if (g_music_preload_ready && (strncmp(g_music_preload_path, path, sizeof(g_music_preload_path)) == 0))
return;
/* 清理上一次预加载缓存 */
if (g_music_preload_lottie != NULL || g_music_preload_fb != NULL)
clear_music_preload_unlocked();
g_music_preload_fb = heap_caps_malloc(LOTTIE_BUF_SIZE, MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA);
if (g_music_preload_fb == NULL)
{
ESP_LOGW(TAG_LVGL, "music preload: alloc fb failed");
return;
}
/* 须在持有 lvgl_api_lock 的上下文中调用(如 change_task 内),避免与 LVGL 线程竞态 */
lv_obj_t *obj = lv_lottie_create(ui_Screen_Lottie);
if (obj == NULL)
{
clear_music_preload_unlocked();
ESP_LOGW(TAG_LVGL, "music preload: create lottie failed");
return;
}
lv_lottie_set_buffer(obj, LOTTIE_ANIM_WIDTH, LOTTIE_ANIM_HEIGHT, g_music_preload_fb);
lv_obj_align(obj, LV_ALIGN_CENTER, 4, 0);
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
if (strncmp(path, "/extflash/", 10) == 0)
extflash_lock();
lv_lottie_set_src_file(obj, path);
if (strncmp(path, "/extflash/", 10) == 0)
extflash_unlock();
lv_anim_t *anim = lv_lottie_get_anim(obj);
if (anim == NULL)
{
ESP_LOGW(TAG_LVGL, "music preload: no anim after set_src, path=%s", path);
lv_obj_del(obj);
heap_caps_free(g_music_preload_fb);
g_music_preload_fb = NULL;
return;
}
anim->duration = anim->duration * 2;
anim->repeat_cnt = LV_ANIM_REPEAT_INFINITE;
anim->completed_cb = NULL;
g_music_preload_lottie = obj;
strncpy(g_music_preload_path, path, sizeof(g_music_preload_path) - 1);
g_music_preload_path[sizeof(g_music_preload_path) - 1] = '\0';
g_music_preload_ready = true;
ESP_LOGI(TAG_LVGL, "music preload ready: %s", g_music_preload_path);
}
/* 设置当前 lottie 按时间区间循环播放 (start_s ~ end_s 秒) */
void set_current_lottie_range_time(float start_s, float end_s)
{
if (current_lottie == NULL)
return;
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim == NULL)
return;
int32_t total = anim->end_value;
int32_t start_frame = (int32_t)(start_s * (float)LOTTIE_SOURCE_FPS);
int32_t end_frame = (int32_t)(end_s * (float)LOTTIE_SOURCE_FPS);
if (start_frame < 0)
start_frame = 0;
if (end_frame > total)
end_frame = total;
if (start_frame >= end_frame)
return;
anim->start_value = start_frame;
anim->end_value = end_frame;
anim->act_time = 0;
anim->duration = (end_frame - start_frame) * 1000 / LOTTIE_TARGET_FPS;
anim->repeat_cnt = LV_ANIM_REPEAT_INFINITE;
anim->completed_cb = NULL;
ESP_LOGI(TAG_LVGL, "lottie range time: %.1fs~%.1fs (frame %ld~%ld)", (double)start_s, (double)end_s, (long)start_frame, (long)end_frame);
}
/* 设置当前 lottie 按进度比例循环播放 (start_ratio ~ end_ratio, 0~1) */
void set_current_lottie_range_ratio(float start_ratio, float end_ratio)
{
if (current_lottie == NULL)
return;
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim == NULL)
return;
int32_t total = anim->end_value;
int32_t start_frame = (int32_t)(start_ratio * (float)total);
int32_t end_frame = (int32_t)(end_ratio * (float)total);
if (start_frame < 0)
start_frame = 0;
if (end_frame > total)
end_frame = total;
if (start_frame >= end_frame)
return;
anim->start_value = start_frame;
anim->end_value = end_frame;
anim->act_time = 0;
anim->duration = (end_frame - start_frame) * 1000 / LOTTIE_TARGET_FPS;
anim->repeat_cnt = LV_ANIM_REPEAT_INFINITE;
anim->completed_cb = NULL;
ESP_LOGI(TAG_LVGL, "lottie range ratio: %.0f%%~%.0f%% (frame %ld~%ld)", (double)(start_ratio * 100.0f), (double)(end_ratio * 100.0f), (long)start_frame, (long)end_frame);
}
/* 设置当前 lottie 按进度比例播放一次 (start_ratio ~ end_ratio),播完后走 completed_cb(如回默认) */
static void set_current_lottie_range_ratio_once(float start_ratio, float end_ratio)
{
if (current_lottie == NULL)
return;
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim == NULL)
return;
int32_t total = anim->end_value;
int32_t start_frame = (int32_t)(start_ratio * (float)total);
int32_t end_frame = (int32_t)(end_ratio * (float)total);
if (start_frame < 0)
start_frame = 0;
if (end_frame > total)
end_frame = total;
if (start_frame >= end_frame)
return;
anim->start_value = start_frame;
anim->end_value = end_frame;
anim->act_time = 0;
anim->duration = (end_frame - start_frame) * 1000 / LOTTIE_TARGET_FPS;
anim->repeat_cnt = 0; /* 播放一次 */
anim->completed_cb = lottie_anim_completed_cb;
ESP_LOGI(TAG_LVGL, "lottie range ratio once: %.0f%%~%.0f%% (frame %ld~%ld)", (double)(start_ratio * 100.0f), (double)(end_ratio * 100.0f), (long)start_frame, (long)end_frame);
}
/* 按进度比例播放一次,使用自定义 completed_cb(哨兵等) */
static void set_current_lottie_range_ratio_once_with_cb(float start_ratio, float end_ratio, lv_anim_completed_cb_t cb)
{
if (current_lottie == NULL || cb == NULL)
return;
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim == NULL)
return;
int32_t total = anim->end_value;
int32_t start_frame = (int32_t)(start_ratio * (float)total);
int32_t end_frame = (int32_t)(end_ratio * (float)total);
if (start_frame < 0)
start_frame = 0;
if (end_frame > total)
end_frame = total;
if (start_frame >= end_frame)
return;
anim->start_value = start_frame;
anim->end_value = end_frame;
anim->act_time = 0;
anim->duration = (end_frame - start_frame) * 1000 / LOTTIE_TARGET_FPS;
anim->repeat_cnt = 0;
anim->completed_cb = cb;
ESP_LOGI(TAG_LVGL, "lottie range ratio once(cb): %.0f%%~%.0f%%", (double)(start_ratio * 100.0f), (double)(end_ratio * 100.0f));
}
/* 按进度比例播放 N+1 次(repeat_cnt=N),播完后走 completed_cb */
static void set_current_lottie_range_ratio_repeat_with_cb(float start_ratio, float end_ratio, uint16_t repeat_cnt, lv_anim_completed_cb_t cb)
{
if (current_lottie == NULL || cb == NULL)
return;
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim == NULL)
return;
int32_t total = anim->end_value;
int32_t start_frame = (int32_t)(start_ratio * (float)total);
int32_t end_frame = (int32_t)(end_ratio * (float)total);
if (start_frame < 0)
start_frame = 0;
if (end_frame > total)
end_frame = total;
if (start_frame >= end_frame)
return;
anim->start_value = start_frame;
anim->end_value = end_frame;
anim->act_time = 0;
anim->duration = (end_frame - start_frame) * 1000 / LOTTIE_TARGET_FPS;
anim->repeat_cnt = repeat_cnt;
anim->completed_cb = cb;
ESP_LOGI(TAG_LVGL, "lottie range ratio repeat(cb): %.0f%%~%.0f%% x%d",
(double)(start_ratio * 100.0f), (double)(end_ratio * 100.0f), (int)(repeat_cnt + 1));
}
/* music_listen 段播放结束回调(当前实现为持续循环,理论上不会进入) */
static void music_listen_pass_completed_cb(lv_anim_t *anim);
static void mindfulness_two_pass_start_cb(lv_anim_t *anim);
static void ai_respond_loop_pass_completed_cb(lv_anim_t *anim);
static void ai_respond_set_infinite_loop_on_current(void)
{
/* AI 循环段按路径设置区间:
* - conversation2: 0%-100%
* - 其他(conversation/AI_respond): 20%-80%
* 均为单次播放,播完回调切到下一段。 */
if (strstr((const char *)s_ai_respond_loop_path_buf, "conversation2.json") != NULL)
{
set_current_lottie_range_ratio_repeat_with_cb(0.0f, 0.9f, 0, ai_respond_loop_pass_completed_cb);
}
else
{
set_current_lottie_range_ratio_repeat_with_cb(0.1f, 0.9f, 0, ai_respond_loop_pass_completed_cb);
}
}
static void ai_respond_loop_pass_completed_cb(lv_anim_t *anim)
{
(void)anim;
if (!s_ai_respond_loop_active)
{
return;
}
if (s_ai_respond_loop_paths[0][0] == '\0' || s_ai_respond_loop_paths[1][0] == '\0')
{
return;
}
s_ai_respond_loop_index = (uint8_t)((s_ai_respond_loop_index + 1) % 2U);
if (change_task_handle)
{
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_RESUME_AI_RESPOND_LOOP, eSetValueWithOverwrite);
}
}
/* 进入 music_listen 持续循环:40%-75% 无限循环 */
static void music_begin_listen_loop_phase(void)
{
s_music_listen_passes_completed = 0;
set_current_lottie_range_ratio(0.4f, 0.75f);
}
/* 在当前 calling 动画上配置 20%-80% 无限循环(调用方须已持 lvgl_api_lock) */
static void calling_apply_twenty_eighty_loop_unlocked(void)
{
if (current_lottie == NULL)
return;
lv_anim_t *a = lv_lottie_get_anim(current_lottie);
if (a == NULL)
return;
int32_t total = (s_calling_total_frames > 1) ? s_calling_total_frames : a->end_value;
int32_t start_frame = (int32_t)(0.2f * (float)total);
int32_t end_frame = (int32_t)(0.8f * (float)total);
if (start_frame < 0) start_frame = 0;
if (end_frame > total) end_frame = total;
if (start_frame >= end_frame)
return;
a->start_value = start_frame;
a->end_value = end_frame;
a->act_time = 0;
a->duration = (end_frame - start_frame) * 1000 / LOTTIE_TARGET_FPS;
if (a->duration < 1) a->duration = 1;
a->repeat_cnt = LV_ANIM_REPEAT_INFINITE;
a->completed_cb = NULL;
ESP_LOGI(TAG_LVGL, "通话: 进入 20-80%% 循环 (frame %ld~%ld)", (long)start_frame, (long)end_frame);
}
/* 哨兵模式:当前播放的为 guard 或 protect(0=无) */
static uint16_t sentinel_current = 0;
/* 哨兵 80-100% 播完后要切换到的下一个表情事件 */
static uint16_t sentinel_pending_next = 0;
/* 哨兵 0-20% 播完:进入 20-80% 循环 */
static void sentinel_intro_completed_cb(lv_anim_t *anim)
{
(void)anim;
set_current_lottie_range_ratio(0.2f, 0.8f);
ESP_LOGI(TAG_LVGL, "哨兵: intro 0-20%% 完成,进入 20-80%% 循环");
}
/* 哨兵 80-100% 播完:通知 change_task 加载下一表情并播 0-20%(回调在 LVGL 持锁中,不能直接 switch) */
static void sentinel_outro_completed_cb(lv_anim_t *anim)
{
(void)anim;
if (sentinel_pending_next == 0)
return;
char *path = remapping(sentinel_pending_next);
if (path && path[0] != '\0' && change_task_handle)
{
strncpy(pending_sentinel_switch_path, path, sizeof(pending_sentinel_switch_path) - 1);
pending_sentinel_switch_path[sizeof(pending_sentinel_switch_path) - 1] = '\0';
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_SENTINEL_DO_SWITCH, eSetValueWithOverwrite);
sentinel_current = sentinel_pending_next;
ESP_LOGI(TAG_LVGL, "哨兵: outro 80-100%% 完成,通知切换并播下一表情 0-20%%");
}
sentinel_pending_next = 0;
}
/* music_emoji 播完:若未收到退出则通知 change_task 切到 music_listen 持续循环;否则回默认(回调在 LVGL 持锁中,不能直接 switch) */
static void music_intro_completed_cb(lv_anim_t *anim)
{
(void)anim;
if (!music_playing_intro)
{
ESP_LOGW(TAG_LVGL, "音乐: intro_completed_cb 触发但 music_playing_intro 已为 false,忽略(可能是旧动画残留回调)");
return;
}
music_playing_intro = false;
if (music_exit_pending)
{
music_exit_pending = false;
back2default = true;
ESP_LOGI(TAG_LVGL, "音乐: intro 播完且已退出,回默认");
return;
}
if (music_listen_path_buf[0] != '\0' && change_task_handle)
{
ESP_LOGI(TAG_LVGL, "[TS] music_intro before NOTIFY_MUSIC_INTRO_DONE t=%lums", (unsigned long)esp_log_timestamp());
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_MUSIC_INTRO_DONE, eSetValueWithOverwrite);
ESP_LOGI(TAG_LVGL, "[TS] music_intro after NOTIFY_MUSIC_INTRO_DONE t=%lums", (unsigned long)esp_log_timestamp());
ESP_LOGI(TAG_LVGL, "音乐: intro 播完,通知进入 music_listen 持续循环(40%%-75%%)");
}
}
/* music_listen 段播放结束回调(当前实现为持续循环,正常不会进入) */
static void music_listen_pass_completed_cb(lv_anim_t *anim)
{
(void)anim;
if (music_exit_pending)
{
music_playing_loop = false;
back2default = true;
ESP_LOGI(TAG_LVGL, "音乐: listen 段播完且已请求退出,回默认");
return;
}
}
/* 正念 breath:每一遍开始时打点,便于现场确认第二遍是否真实启动。 */
static void mindfulness_two_pass_start_cb(lv_anim_t *anim)
{
if (anim == NULL)
{
return;
}
s_mindfulness_pass_started++;
ESP_LOGI(TAG_LVGL,
"mindfulness: breath 第 %d 遍开始 (act_time=%ld start=%ld end=%ld curr=%ld)",
(int)s_mindfulness_pass_started,
(long)anim->act_time,
(long)anim->start_value,
(long)anim->end_value,
(long)anim->current_value);
}
// 在 lvgl_init.txt 的 change_task 函数外部或内部开头定义
static int prev_page_idx = 0; // 用于记录跳槽前的页面索引
/**
* 立即执行「回默认表情」清理:重置所有音乐/KTV/哨兵状态并切换到默认动画。
* 提取为独立函数,使停音乐指令可同步完成清理,避免 back2default 跨轮次与后续指令竞争。
* 调用时必须**未**持有 lvgl_api_lock(内部自行 acquire/release)。
*/
static void execute_back2default(int *last_active_page_ptr, const char *reason)
{
ESP_LOGI("BLE_STICKER_GUARD",
"CLEAR g_ble_play_sticker_active=false (reason=%s, was=%d)",
reason ? reason : "back2default", (int)g_ble_play_sticker_active);
back2default = false;
g_ble_play_sticker_active = false;
g_ble_play_sticker_pending_valid = false;
g_ble_play_sticker_pending_action_id = 0;
clear_music_preload();
sentinel_current = 0;
sentinel_pending_next = 0;
music_playing_intro = false;
music_playing_loop = false;
music_exit_pending = false;
resume_music_loop_after_oneshot = false;
resume_calling_loop_after_oneshot = false;
resume_ai_respond_loop_after_oneshot = false;
music_cycle_intro_path_buf[0] = '\0';
s_music_listen_passes_completed = 0;
s_calling_total_frames = 0;
g_is_ktv_playing = false;
ktv_loop_path_buf[0] = '\0';
s_music_suspend_for_ktv_valid = false;
s_suspend_intro[0] = '\0';
s_suspend_listen[0] = '\0';
s_suspend_was_intro = false;
s_suspend_was_loop = false;
s_mindfulness_pass_started = 0;
s_resume_call_after_music_stop = false;
s_hangup_deferred_after_intersperse = false;
s_ai_respond_loop_active = false;
s_ai_respond_loop_armed = false;
s_ai_respond_loop_path_buf[0] = '\0';
s_ai_respond_loop_paths[0][0] = '\0';
s_ai_respond_loop_paths[1][0] = '\0';
s_ai_respond_loop_index = 0;
s_mindfulness_pass_started = 0;
// 【解决方案】清除正念活跃标志,恢复手势切换
// 当表情播放完成回到默认表情时,确保手势功能恢复
g_mindfulness_active = false;
ESP_LOGI("mindfulness", "back to default, gesture enabled");
char cur_pack = emoji_storage_get_using_pack();
if (cur_pack == 'A' || cur_pack == 'B' || cur_pack == 'C' || cur_pack == 'D')
{
char pack_default[48];
snprintf(pack_default, sizeof(pack_default), "/extflash/%c/default.json", cur_pack);
struct stat st;
if (stat(pack_default, &st) == 0)
{
switch_lottie_animation_cross_path(pack_default);
}
else
{
switch_lottie_animation_cross_path(default_emoji_path);
}
}
else
{
switch_lottie_animation(default_emojo);
}
_lock_acquire(&lvgl_api_lock);
last_lottie_id = 0;
if (last_active_page_ptr && *last_active_page_ptr != 0)
{
ESP_LOGI(TAG_LVGL, "动画播放完毕,跳回页面: %d", *last_active_page_ptr);
ui_change_page(*last_active_page_ptr, LV_SCR_LOAD_ANIM_FADE_ON);
*last_active_page_ptr = 0;
}
else
{
ESP_LOGI(TAG_LVGL, "动画播放完毕,保持在待机页");
}
_lock_release(&lvgl_api_lock);
}
//清空音乐 KTV BLE队列 等所有播放状态
void reset_all_playback_state(void)
{
/* 一次播放生命周期结束后允许同 action_id 再次触发 */
current_emoji_action_id = 0;
g_ble_play_sticker_active = false;
g_ble_play_sticker_pending_valid = false;
g_ble_play_sticker_pending_action_id = 0;
g_ble_play_sticker_pending_direction = 0;
music_playing_intro = false;
music_playing_loop = false;
music_exit_pending = false;
resume_music_loop_after_oneshot = false;
resume_calling_loop_after_oneshot = false;
s_music_listen_passes_completed = 0;
music_cycle_intro_path_buf[0] = '\0';
g_is_ktv_playing = false;
ktv_loop_path_buf[0] = '\0';
sentinel_current = 0;
sentinel_pending_next = 0;
s_calling_mode_active = false;
s_resume_call_after_music_stop = false;
s_hangup_deferred_after_intersperse = false;
s_ai_respond_loop_active = false;
s_ai_respond_loop_armed = false;
s_ai_respond_loop_path_buf[0] = '\0';
s_ai_respond_loop_paths[0][0] = '\0';
s_ai_respond_loop_paths[1][0] = '\0';
s_ai_respond_loop_index = 0;
clear_music_preload();
// 【解决方案】清除正念活跃标志,恢复手势切换
g_mindfulness_active = false;
ESP_LOGI("mindfulness", "reset playback state, gesture enabled");
}
/** 切回默认表情(无锁版本) */
void switch_to_default_emoji(void)
{
char cur_pack = emoji_storage_get_using_pack();
if (cur_pack == 'A' || cur_pack == 'B' || cur_pack == 'C' || cur_pack == 'D')
{
char pack_default[48];
snprintf(pack_default, sizeof(pack_default), "/extflash/%c/default.json", cur_pack);
struct stat st;
if (stat(pack_default, &st) == 0)
switch_lottie_animation_cross_path(pack_default);
else
switch_lottie_animation_cross_path(default_emoji_path);
}
else
{
switch_lottie_animation(default_emojo);
}
last_lottie_id = 0;
}
/** 确保在 Lottie 页面可见(从其他UI页面返回)- 需持锁 */
static void ensure_lottie_page_visible(int *last_active_page_ptr)
{
if (current_page_idx != 0)
{
*last_active_page_ptr = current_page_idx;
ESP_LOGI(TAG_LVGL, "play_sticker: 当前在页%d,切回 Lottie 页", current_page_idx);
current_page_idx = 0;
is_ui_mode = false;
}
/* 即使 current_page_idx 已是 0,也可能未加载 Lottie 屏(状态不同步)或动画曾被暂停,
* 否则 Lottie 不跑帧、completed_cb 永不触发,BLE pending 队列会永远卡住。 */
if (ui_Screen_Lottie != NULL && lv_obj_is_valid(ui_Screen_Lottie) &&
lv_screen_active() != ui_Screen_Lottie)
{
lv_screen_load(ui_Screen_Lottie);
}
if (current_lottie != NULL && lv_obj_is_valid(current_lottie))
{
lv_obj_remove_flag(current_lottie, LV_OBJ_FLAG_HIDDEN);
lv_anim_t *a = lv_lottie_get_anim(current_lottie);
if (a) lv_anim_resume(a);
}
}
/* 在 change_task 中重新加载 calling 并钳到 20%~80% 循环;会销毁旧 lottie 以绑定新的 lv_anim(避免 intro 结束后 anim 已 lv_free 悬空) */
static void calling_reload_and_enter_loop(int *last_active_page_ptr, const char *done_log)
{
(void)last_active_page_ptr;
char *path_result = remapping(emoji_event_answer_call);
static char pack_path_buf_call[80];
const char *path_to_play = path_result;
if (path_result && path_result[0] != '\0')
{
char cur_pack = emoji_storage_get_using_pack();
if (cur_pack == 'A' || cur_pack == 'B' || cur_pack == 'C' || cur_pack == 'D')
{
const char *basename = strrchr(path_result, '/');
basename = basename ? (basename + 1) : path_result;
snprintf(pack_path_buf_call, sizeof(pack_path_buf_call), "/extflash/%c/%s", cur_pack, basename);
struct stat st;
if (stat(pack_path_buf_call, &st) == 0)
path_to_play = pack_path_buf_call;
}
}
if (!path_to_play || !path_to_play[0])
{
ESP_LOGW(TAG_LVGL, "%s: 无 calling 路径,跳过", done_log ? done_log : "calling");
return;
}
s_calling_mode_active = true;
g_ble_play_sticker_active = true;
switch_lottie_animation_cross_path(path_to_play);
_lock_acquire(&lvgl_api_lock);
if (current_lottie != NULL)
{
lv_anim_t *a = lv_lottie_get_anim(current_lottie);
if (a)
s_calling_total_frames = a->end_value;
}
calling_apply_twenty_eighty_loop_unlocked();
_lock_release(&lvgl_api_lock);
current_emoji_action_id = emoji_event_answer_call;
ESP_LOGI(TAG_LVGL, "%s", done_log ? done_log : "calling: loop ok");
}
bool lvgl_lottie_completed_try_deferred_hangup(void)
{
if (!s_hangup_deferred_after_intersperse)
{
return false;
}
s_hangup_deferred_after_intersperse = false;
resume_calling_loop_after_oneshot = false;
if (change_task_handle)
{
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_DEFERRED_HANGUP, eSetValueWithOverwrite);
}
ESP_LOGI(TAG_LVGL, "通话: 插播播完,通知执行挂起挂断→回默认");
return true;
}
bool lvgl_lottie_completed_try_resume_ai_respond_loop(void)
{
if (!resume_ai_respond_loop_after_oneshot)
{
return false;
}
resume_ai_respond_loop_after_oneshot = false;
if (change_task_handle)
{
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_RESUME_AI_RESPOND_LOOP, eSetValueWithOverwrite);
}
ESP_LOGI(TAG_LVGL, "AI回复插播: 播完,通知恢复 conversation 循环");
return true;
}
bool lvgl_lottie_completed_try_start_ai_respond_loop(void)
{
if (!s_ai_respond_loop_armed)
{
return false;
}
s_ai_respond_loop_armed = false;
if (change_task_handle)
{
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_START_AI_RESPOND_LOOP, eSetValueWithOverwrite);
}
ESP_LOGI(TAG_LVGL, "AI回复: hearing 播完,通知切 conversation 循环");
return true;
}
//处理停止/暂停音乐动作
/** 处理内部命令层 (NOTIFY_* 系列) - 返回 true 表示已处理 */
bool handle_internal_commands(uint32_t bits, int *last_active_page_ptr)
{
switch (bits)
{
case NOTIFY_PLAY_BY_PATH:
if (g_play_lottie_path[0] != '\0')
{
ESP_LOGI("recv_change_task", "play by path: %s", g_play_lottie_path);
switch_lottie_animation_cross_path(g_play_lottie_path);
}
return true;
case NOTIFY_EXIT_ALL_MODES:
ESP_LOGI("BLE_STICKER_GUARD", "EXIT_ALL_MODES: 清理所有状态回默认");
reset_all_playback_state();
back2default = false;
switch_to_default_emoji();
_lock_acquire(&lvgl_api_lock);
last_lottie_id = 0;
if (*last_active_page_ptr != 0)
{
ui_change_page(*last_active_page_ptr, LV_SCR_LOAD_ANIM_FADE_ON);
*last_active_page_ptr = 0;
}
_lock_release(&lvgl_api_lock);
return true;
case NOTIFY_RESUME_MUSIC_LOOP:
if (g_is_ktv_playing && ktv_loop_path_buf[0] != '\0')
{
switch_lottie_animation_cross_path(ktv_loop_path_buf);
set_current_lottie_range_ratio(0.25f, 0.75f);
music_playing_loop = true;
ESP_LOGI(TAG_LVGL, "音乐插播: 已恢复 KTV 循环");
}
else if (music_listen_path_buf[0] != '\0')
{
switch_lottie_animation_cross_path(music_listen_path_buf);
music_begin_listen_loop_phase();
music_playing_loop = true;
ESP_LOGI(TAG_LVGL, "音乐: 插播播完,已恢复 music_listen 持续循环");
}
return true;
case NOTIFY_RESUME_CALLING_LOOP:
calling_reload_and_enter_loop(last_active_page_ptr, "通话: 插播播完,已恢复 20-80% 循环");
return true;
case NOTIFY_START_AI_RESPOND_LOOP:
if (s_ai_respond_loop_paths[s_ai_respond_loop_index][0] != '\0')
{
strncpy(s_ai_respond_loop_path_buf,
s_ai_respond_loop_paths[s_ai_respond_loop_index],
sizeof(s_ai_respond_loop_path_buf) - 1);
s_ai_respond_loop_path_buf[sizeof(s_ai_respond_loop_path_buf) - 1] = '\0';
switch_lottie_animation_cross_path(s_ai_respond_loop_path_buf);
ai_respond_set_infinite_loop_on_current();
s_ai_respond_loop_active = true;
/* 当前画面已是 conversation 段,与上一段插播的 action_id 脱钩,否则去抖会丢弃同 id 的再次插播 */
current_emoji_action_id = 0;
ESP_LOGI(TAG_LVGL, "AI回复: hearing 播完,开始双路径20-80%%循环(%s)", s_ai_respond_loop_path_buf);
}
return true;
case NOTIFY_RESUME_AI_RESPOND_LOOP:
if (s_ai_respond_loop_paths[s_ai_respond_loop_index][0] != '\0')
{
strncpy(s_ai_respond_loop_path_buf,
s_ai_respond_loop_paths[s_ai_respond_loop_index],
sizeof(s_ai_respond_loop_path_buf) - 1);
s_ai_respond_loop_path_buf[sizeof(s_ai_respond_loop_path_buf) - 1] = '\0';
switch_lottie_animation_cross_path(s_ai_respond_loop_path_buf);
ai_respond_set_infinite_loop_on_current();
s_ai_respond_loop_active = true;
current_emoji_action_id = 0;
ESP_LOGI(TAG_LVGL, "AI回复: 切换到下一段20-80%%循环(%s)", s_ai_respond_loop_path_buf);
}
return true;
case NOTIFY_DEFERRED_HANGUP:
reset_all_playback_state();
back2default = false;
execute_back2default(last_active_page_ptr, "hangup_deferred_after_intersperse");
ESP_LOGI(TAG_LVGL, "通话: 挂起挂断已执行,回默认");
return true;
case NOTIFY_MUSIC_INTRO_DONE:
ESP_LOGI("recv_change_task", "[TS] change_task handle NOTIFY_MUSIC_INTRO_DONE t=%lums", (unsigned long)esp_log_timestamp());
if (music_listen_path_buf[0] != '\0')
{
switch_lottie_animation_cross_path(music_listen_path_buf);
music_begin_listen_loop_phase();
music_playing_loop = true;
ESP_LOGI(TAG_LVGL, "音乐: intro 播完,开始 music_listen 持续循环");
}
return true;
case NOTIFY_MUSIC_REPLAY_INTRO:
/* 改为 music_listen 持续循环后,该通知不再使用 */
return true;
case NOTIFY_SENTINEL_DO_SWITCH:
if (pending_sentinel_switch_path[0] != '\0')
{
switch_lottie_animation_cross_path(pending_sentinel_switch_path);
set_current_lottie_range_ratio_once_with_cb(0.0f, 0.2f, sentinel_intro_completed_cb);
pending_sentinel_switch_path[0] = '\0';
ESP_LOGI(TAG_LVGL, "哨兵: 已切换并播下一表情");
}
return true;
case NOTIFY_ACTION_QUEUE_READY:
/* 队列就绪通知,已从队列取出 action_id,无需额外处理 */
return true;
default:
return false;
}
}
/** 处理停止音乐指令 - 返回 true 表示已处理 */
bool handle_stop_music(uint16_t action_id, int *last_active_page_ptr)
{
if (action_id != emoji_event_stop_music && action_id != emoji_event_pause_music)
return false;
if (g_is_ktv_playing)
{
is_playing_music = false;
s_music_suspend_for_ktv_valid = false;
s_suspend_intro[0] = '\0';
s_suspend_listen[0] = '\0';
ESP_LOGI(TAG_LVGL, "音乐: KTV 播放中收到停止,取消恢复");
return true;
}
is_playing_music = false;
if (music_playing_intro)
{
/* 通话中被音乐打断:intro 阶段收到 0x0003/0x0004 也立即回到通话循环 */
if (s_resume_call_after_music_stop)
{
s_resume_call_after_music_stop = false;
music_playing_intro = false;
music_exit_pending = false;
reset_all_playback_state();
calling_reload_and_enter_loop(last_active_page_ptr, "通话: intro 阶段暂停音乐后恢复通话循环");
return true;
}
music_exit_pending = true;
ESP_LOGI(TAG_LVGL, "音乐: 停止指令,等 intro 播完");
/* 勿执行函数末尾 reset_all,否则会清掉 music_exit_pending,intro 结束回调无法按预期退出 */
return true;
}
else if (music_playing_loop)
{
music_playing_loop = false;
if (s_resume_call_after_music_stop)
{
s_resume_call_after_music_stop = false;
reset_all_playback_state();
calling_reload_and_enter_loop(last_active_page_ptr, "通话: 暂停音乐后恢复通话循环");
return true;
}
reset_all_playback_state();
back2default = false;
switch_to_default_emoji();
_lock_acquire(&lvgl_api_lock);
last_lottie_id = 0;
if (*last_active_page_ptr != 0)
{
ui_change_page(*last_active_page_ptr, LV_SCR_LOAD_ANIM_FADE_ON);
*last_active_page_ptr = 0;
}
_lock_release(&lvgl_api_lock);
ESP_LOGI(TAG_LVGL, "音乐: 停止循环,回默认");
}
else
{
g_ble_play_sticker_active = false;
ESP_LOGI(TAG_LVGL, "音乐: 当前未播放,忽略停止");
}
reset_all_playback_state();
return true;
}
/** 处理音乐播放:0x0002 intro 一次 -> music_listen;0x0005 直接进入 listen(与 intro 播完后同态) */
bool handle_play_music(uint16_t action_id, const char *path_to_play, int *last_active_page_ptr)
{
/* 车机仅发「播放音乐中」:与 0x0002 播完 intro 后的 music_listen 段一致,BLE 可直通插播,0x0003/0x0004 可停 */
if (action_id == emoji_event_continue_play_music)
{
if (path_to_play == NULL || path_to_play[0] == '\0')
return false;
strncpy(music_listen_path_buf, path_to_play, sizeof(music_listen_path_buf) - 1);
music_listen_path_buf[sizeof(music_listen_path_buf) - 1] = '\0';
music_cycle_intro_path_buf[0] = '\0';
music_playing_intro = false;
music_exit_pending = false;
s_music_listen_passes_completed = 0;
if (g_is_ktv_playing)
{
s_music_suspend_for_ktv_valid = true;
s_suspend_intro[0] = '\0';
strncpy(s_suspend_listen, music_listen_path_buf, sizeof(s_suspend_listen) - 1);
s_suspend_listen[sizeof(s_suspend_listen) - 1] = '\0';
s_suspend_was_intro = false;
is_playing_music = true;
ESP_LOGI(TAG_LVGL, "KTV前台:0x0005 music_listen 已挂起");
return true;
}
if (music_listen_path_buf[0] != '\0')
{
_lock_acquire(&lvgl_api_lock);
try_preload_music_listen_lottie(music_listen_path_buf);
_lock_release(&lvgl_api_lock);
}
_lock_acquire(&lvgl_api_lock);
ensure_lottie_page_visible(last_active_page_ptr);
_lock_release(&lvgl_api_lock);
if (s_calling_mode_active)
{
s_resume_call_after_music_stop = true;
ESP_LOGI(TAG_LVGL, "通话: 被音乐打断,暂停音乐后将恢复通话循环");
}
s_calling_mode_active = false;
switch_lottie_animation_cross_path(path_to_play);
music_begin_listen_loop_phase();
music_playing_loop = true;
g_is_ktv_playing = false;
ktv_loop_path_buf[0] = '\0';
s_music_suspend_for_ktv_valid = false;
ESP_LOGI(TAG_LVGL, "音乐: 0x0005 直接进入 music_listen 40%%-75%% 持续循环");
return true;
}
if (action_id != emoji_event_play_music)
return false;
// 准备 listen 路径
char *listen_result = remapping(emoji_event_continue_play_music);
if (listen_result && listen_result[0] != '\0')
{
char cur = emoji_storage_get_using_pack();
if (cur == 'A' || cur == 'B' || cur == 'C' || cur == 'D')
{
const char *base = strrchr(listen_result, '/');
base = base ? (base + 1) : listen_result;
snprintf(music_listen_path_buf, sizeof(music_listen_path_buf), "/extflash/%c/%s", cur, base);
}
else
{
strncpy(music_listen_path_buf, listen_result, sizeof(music_listen_path_buf) - 1);
}
}
else music_listen_path_buf[0] = '\0';
strncpy(music_cycle_intro_path_buf, path_to_play, sizeof(music_cycle_intro_path_buf) - 1);
s_music_listen_passes_completed = 0;
// KTV 前台时挂起音乐
if (g_is_ktv_playing)
{
s_music_suspend_for_ktv_valid = true;
strncpy(s_suspend_intro, music_cycle_intro_path_buf, sizeof(s_suspend_intro) - 1);
strncpy(s_suspend_listen, music_listen_path_buf, sizeof(s_suspend_listen) - 1);
s_suspend_was_intro = true;
is_playing_music = true;
ESP_LOGI(TAG_LVGL, "KTV前台:音乐开始已挂起");
return true;
}
/* intro 播放期间预解析 music_listen,切 listen 时可 promote 预加载对象,避免二次读盘/解析卡顿 */
if (music_listen_path_buf[0] != '\0') {
_lock_acquire(&lvgl_api_lock);
try_preload_music_listen_lottie(music_listen_path_buf);
_lock_release(&lvgl_api_lock);
}
// 正常播放音乐 intro
_lock_acquire(&lvgl_api_lock);
ensure_lottie_page_visible(last_active_page_ptr);
_lock_release(&lvgl_api_lock);
if (s_calling_mode_active)
{
s_resume_call_after_music_stop = true;
ESP_LOGI(TAG_LVGL, "通话: 被音乐打断,暂停音乐后将恢复通话循环");
}
s_calling_mode_active = false;
switch_lottie_animation_cross_path(path_to_play);
set_current_lottie_range_ratio_once_with_cb(0.0f, 0.7f, music_intro_completed_cb);
music_playing_intro = true;
g_is_ktv_playing = false;
ktv_loop_path_buf[0] = '\0';
s_music_suspend_for_ktv_valid = false;
ESP_LOGI(TAG_LVGL, "音乐: 播 intro 一次 -> 进入 music_listen 持续循环");
return true;
}
bool lvgl_lottie_calling_active_for_ble(void)
{
return s_calling_mode_active;
}
bool lvgl_lottie_ai_respond_loop_active_for_ble(void)
{
return s_ai_respond_loop_active;
}
bool lvgl_lottie_should_skip_play_sticker_motor(uint16_t action_id, uint8_t direction)
{
/* 0x0045 在循环内:不在这里发 direction,回默认流程里统一回中 */
if (action_id == emoji_event_AI_respond && s_ai_respond_loop_active)
{
return true;
}
/* direction==0:resume 置位晚于 BLE,首条插播需 s_ai_respond_loop_active;避免 0x00→回中 */
if (direction == 0u && (resume_ai_respond_loop_after_oneshot || s_ai_respond_loop_active))
{
return true;
}
return false;
}
bool lvgl_lottie_mindfulness_block_ble_for_ble(void)
{
/* 正念两遍 breath 播放期间:屏蔽 BLE 指令(不直通、不排队)。 */
return (current_emoji_action_id == emoji_event_mindfulness) && g_ble_play_sticker_active;
}
/** 处理哨兵模式 (0x0032/0x0033) */
bool handle_sentinel_mode(uint16_t action_id, const char *path_to_play, int *last_active_page_ptr)
{
if (action_id != emoji_event_sentinel_guard && action_id != emoji_event_sentinel_protect)
return false;
_lock_acquire(&lvgl_api_lock);
ensure_lottie_page_visible(last_active_page_ptr);
_lock_release(&lvgl_api_lock);
switch_lottie_animation_cross_path(path_to_play);
set_current_lottie_range_ratio(0.2f, 0.8f);
sentinel_current = action_id;
sentinel_pending_next = 0;
ESP_LOGI(TAG_LVGL, "哨兵: 20%%-80%% 循环");
return true;
}
/** 处理普通表情 (非特殊模式) */
void handle_normal_sticker(uint16_t action_id, const char *path_to_play, int *last_active_page_ptr)
{
g_ble_play_sticker_active = true;
/* 首次播放表情时清除开机标记,避免 completed_cb 误判执行亮屏逻辑 */
if (first_display) {
first_display = false;
}
bool was_music_active = music_playing_intro || music_playing_loop || g_is_ktv_playing;
bool was_ai_respond_active = s_ai_respond_loop_active;
/* 0x0045 在 AI 循环态下是“停止命令”,优先于插播恢复标记。 */
if (action_id == emoji_event_AI_respond && was_ai_respond_active)
{
reset_all_playback_state();
(void)motor_post_cmd(0x0Au);
execute_back2default(last_active_page_ptr, "ai_respond_stop");
ESP_LOGI(TAG_LVGL, "AI回复: 收到 0x0045,停止 conversation 循环并回默认");
return;
}
if (was_music_active)
{
resume_music_loop_after_oneshot = true;
ESP_LOGI(TAG_LVGL, "音乐插播: 标记播完后恢复");
}
if (was_ai_respond_active)
{
resume_ai_respond_loop_after_oneshot = true;
ESP_LOGI(TAG_LVGL, "AI回复插播: 标记播完后恢复 conversation 循环");
}
music_playing_intro = false;
music_playing_loop = false;
g_is_ktv_playing = false;
ktv_loop_path_buf[0] = '\0';
sentinel_current = 0;
s_ai_respond_loop_active = false;
if (!action_triggers_ai_respond_hearing_sequence(action_id))
{
s_ai_respond_loop_armed = false;
}
_lock_acquire(&lvgl_api_lock);
ensure_lottie_page_visible(last_active_page_ptr);
_lock_release(&lvgl_api_lock);
/* 挂断:直接回默认。原「当前帧续播到 100%」依赖 completed_cb,在 20%-80% 无限循环段
* 下 total/帧区间易算错,或 total<=1 时静默 return 未挂回调,会导致永远不回默认。 */
if (action_id == emoji_event_hangup_call)
{
const bool resume_music_after_hangup =
resume_music_loop_after_oneshot && music_listen_path_buf[0] != '\0';
const bool resume_ai_after_hangup =
resume_ai_respond_loop_after_oneshot && s_ai_respond_loop_path_buf[0] != '\0';
if (resume_music_after_hangup)
{
s_calling_mode_active = false;
resume_calling_loop_after_oneshot = false;
resume_music_loop_after_oneshot = false;
reset_all_playback_state();
switch_lottie_animation_cross_path(music_listen_path_buf);
_lock_acquire(&lvgl_api_lock);
music_begin_listen_loop_phase();
_lock_release(&lvgl_api_lock);
music_playing_loop = true;
is_playing_music = true;
ESP_LOGI(TAG_LVGL, "通话: 挂断后恢复音乐播放循环");
ESP_LOGI("recv_change_task", "播放表情: 0x%04X -> music_listen", action_id);
return;
}
if (resume_ai_after_hangup)
{
s_calling_mode_active = false;
resume_calling_loop_after_oneshot = false;
resume_ai_respond_loop_after_oneshot = false;
if (change_task_handle)
{
xTaskNotify(change_task_handle, (uint32_t)NOTIFY_RESUME_AI_RESPOND_LOOP, eSetValueWithOverwrite);
}
ESP_LOGI(TAG_LVGL, "通话: 挂断后恢复 AI conversation 循环");
ESP_LOGI("recv_change_task", "播放表情: 0x%04X -> ai_conversation_loop", action_id);
return;
}
/* 通话中插播了其它表情:等插播播完再挂断回默认(不立刻切画面) */
if (resume_calling_loop_after_oneshot && !s_calling_mode_active)
{
s_hangup_deferred_after_intersperse = true;
ESP_LOGI(TAG_LVGL, "通话: 挂断挂起,等待插播表情播完");
return;
}
s_calling_mode_active = false;
resume_calling_loop_after_oneshot = false;
reset_all_playback_state();
execute_back2default(last_active_page_ptr, "hangup_call");
ESP_LOGI(TAG_LVGL, "通话: 挂断,立即回默认表情");
ESP_LOGI("recv_change_task", "播放表情: 0x%04X -> default", action_id);
return;
}
{
const bool was_in_calling_loop = s_calling_mode_active;
if (was_in_calling_loop && action_id != emoji_event_answer_call)
{
resume_calling_loop_after_oneshot = true;
ESP_LOGI(TAG_LVGL, "通话插播: 标记播完后恢复通话循环段");
}
}
/* 仅接听(0x000B)为通话循环态;其它表情会打断通话,需清标志以免 BLE 仍按「持续态」直通逻辑混乱 */
s_calling_mode_active = (action_id == emoji_event_answer_call);
switch_lottie_animation_cross_path(path_to_play);
_lock_acquire(&lvgl_api_lock);
/* 切换后确保新 Lottie 未处于暂停(从其它 UI 返回时 ui_nav 可能 pause 过旧实例) */
if (current_lottie != NULL && lv_obj_is_valid(current_lottie))
{
lv_obj_remove_flag(current_lottie, LV_OBJ_FLAG_HIDDEN);
lv_anim_t *new_anim = lv_lottie_get_anim(current_lottie);
if (new_anim) lv_anim_resume(new_anim);
}
last_lottie_id = 1; // 非0,completed_cb触发back2default
if (action_id == emoji_event_answer_call)
{
if (current_lottie != NULL)
{
lv_anim_t *a = lv_lottie_get_anim(current_lottie);
if (a) s_calling_total_frames = a->end_value; /* 记录通话动画总帧数(100%) */
}
calling_apply_twenty_eighty_loop_unlocked();
last_lottie_id = 0; /* 无限循环段,与 switch 中 calling 分支一致,避免 completed_cb 误清 BLE 态 */
ESP_LOGI(TAG_LVGL, "通话: 直接 20-80%% 无限循环");
}
else if (action_triggers_ai_respond_hearing_sequence(action_id))
{
/* 0x0020/21/2E/2F:hearing 只播前 80%,播完后由 completed_cb 拉起 AI 循环。 */
set_current_lottie_range_ratio_once(0.0f, 0.8f);
ESP_LOGI(TAG_LVGL, "AI回复前置: hearing(0x%04X) 播放区间 0-80%%", action_id);
}
else if (action_id == emoji_event_mindfulness)
{
/* 正念 breath 强制两遍串联播放。
* 直接用 LVGL repeat 机制做 2 次播放,避免在 completed_cb 内重配动画导致第二遍未实际启动。 */
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim)
{
s_mindfulness_pass_started = 0;
/* LVGL 这里的 repeat_cnt 表示“总播放次数”,不是“额外重复次数”。
* 设为 2 才是完整播放两遍。 */
anim->repeat_cnt = 2; /* 总共播放 2 遍 */
anim->start_cb = mindfulness_two_pass_start_cb;
anim->completed_cb = lottie_anim_completed_cb;
ESP_LOGI(TAG_LVGL, "mindfulness: breath 启用强制两遍播放(repeat_cnt=2)");
}
}
if (resume_music_loop_after_oneshot && current_lottie != NULL &&
action_id != emoji_event_answer_call)
{
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim)
{
anim->repeat_cnt = 0;
anim->completed_cb = lottie_anim_completed_cb;
}
}
if (resume_calling_loop_after_oneshot && current_lottie != NULL &&
action_id != emoji_event_answer_call)
{
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim)
{
anim->repeat_cnt = 0;
anim->completed_cb = lottie_anim_completed_cb;
}
}
if (resume_ai_respond_loop_after_oneshot && current_lottie != NULL &&
action_id != emoji_event_answer_call)
{
lv_anim_t *anim = lv_lottie_get_anim(current_lottie);
if (anim)
{
anim->repeat_cnt = 0;
anim->completed_cb = lottie_anim_completed_cb;
}
}
if (action_triggers_ai_respond_hearing_sequence(action_id))
{
if (prepare_fixed_ai_respond_loop_paths())
{
s_ai_respond_loop_armed = true;
strncpy(s_ai_respond_loop_path_buf, s_ai_respond_loop_paths[0], sizeof(s_ai_respond_loop_path_buf) - 1);
s_ai_respond_loop_path_buf[sizeof(s_ai_respond_loop_path_buf) - 1] = '\0';
ESP_LOGI(TAG_LVGL, "AI回复: 0x%04X 已触发,hearing 播完后按固定双路径20-80%%循环(%s <-> %s)",
action_id, s_ai_respond_loop_paths[0], s_ai_respond_loop_paths[1]);
}
else
{
s_ai_respond_loop_armed = false;
s_ai_respond_loop_path_buf[0] = '\0';
s_ai_respond_loop_paths[0][0] = '\0';
s_ai_respond_loop_paths[1][0] = '\0';
ESP_LOGW(TAG_LVGL, "AI回复: 固定双路径文件缺失,无法启动循环");
}
}
_lock_release(&lvgl_api_lock);
ESP_LOGI("recv_change_task", "播放表情: 0x%04X, path=%s", action_id, path_to_play);
}