/*****************************************************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 // 【解决方案】外部声明正念活跃标志 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); }