纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。 Co-authored-by: Cursor <cursoragent@cursor.com>
1364 lines
53 KiB
C
1364 lines
53 KiB
C
/*****************************************************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);
|
||
} |