重构说明/hly_ai.c/单事件循环 · actor

把纠缠的多线程状态机重构为单事件循环

原实现用 _ai_mutex 既当状态锁、又包住会阻塞的 player 调用,并在事件回调链里直接调阻塞 stop/play,导致冻结与自死锁。重构把所有输入收口为「投递命令 → 单 worker 串行执行」。本说明记录前后结构、命令映射、修掉的 bug 与待核对项。

文件
crash/hly_ai_refactored.c
对外 API
保持不变
线程模型
5 线程 → 1 worker
3 把 → 2 把(窄锁)
修复 bug
6
01

核心转变

重构前 · 病灶结构
  • 持锁调阻塞:_ai_mutex 被握着调 player_stop,而 mic(每 50ms)、VAD、KWS 全要抢这把锁 → player 一卡全冻结
  • 回调内重入:事件回调跑在 player 线程上,却直接调阻塞 stop → 自死锁 / 锁反转
  • 状态打散:5 个线程 + 3 锁 + 2 队列 + 1 信号量;set_stat post 不查返回值 → 静默丢状态
重构后 · 单事件循环
  • 锁外阻塞:阻塞 player 调用只发生在 worker,且不在 player 线程 → 不连累热路径
  • 回调只投递:所有回调 post(cmd) 即返回 → player 线程永不自重入
  • 状态收口:唯一 worker 的一个 switch 独占状态机,无需锁;队列满显式告警
KWS 唤醒
hly_ai_worker · 唯一线程
  • 读 / 改 _ai_stat(独占,无锁)
  • player_stop / play_data(阻塞安全,有 svc/tkl 超时兜底)
  • publish(AI_STAT_EVENT) → UI
VAD 变化
AI 事件
shutup / 定时器
start / stop
↑ 只 post 一条 cmd 到 _ai_cmd_queue · 立即返回 · 不加锁 · 不阻塞
例外:两条不进队列的路径 mic 数据(每 ~50ms)只做无锁读判断后把音频喂给 agent,绝不进队列;idle 定时器续期(TTS keep-alive)非阻塞、不碰 player,直接调用 _hly_ai_re_idel_start,避免每个 TTS 数据块都灌队列。
02

结构对照

原来重构后
_ai_mutex 包住阻塞 player 调用删除;状态仅 worker 写,热路径无锁读
_hly_ai_sem + sem_task(shutup)AI_CMD_SHUTUP
_hly_ai_stat_queue + stat_task合并进 worker 的 _hly_ai_apply_stat()
_hly_ai_debug_task + debug_step_*删除(抓 hang 的脚手架,已完成使命)
回调直接调 player_stop回调只 _hly_ai_post(cmd),stop/play 在 worker
5 线程 + 3 锁 + 2 队列 + 1 信号量1 命令队列 + 1 worker + 2 窄锁
(_timer_mutex / _alert_mutex)
03

事件 → 命令映射

每个 wukong 事件 / 输入映射成一条语义命令,决策逻辑(进 LISTEN / IDLE / THINK 等)全部放在 worker 里,投递方不做判断。

来源 / 事件命令worker 行为
KWS 唤醒AI_CMD_WAKEUP(IDLE 时)停全部播放 + chat break + 进 LISTEN + 唤醒提示音
shutup / idle 超时AI_CMD_SHUTUP停 FG + chat break + 进 IDLE + 退出提示音
chat_overAI_CMD_CHAT_OVER清 wake_up_flag + 退出提示音
VAD START / STOPAI_CMD_VAD_START/STOP门控后开/停上行;STOP 且非 SPEAK → THINK
ASR 空 / 错AI_CMD_ASR_FAILTHINK → LISTEN
TTS_PREAI_CMD_TTS_PRE→ SPEAK
PLAY_ENDAI_CMD_PLAY_ENDwake_up_flag ? LISTEN : IDLE
EXITAI_CMD_EXIT→ IDLE
PLAY_CTL_PAUSE/PLAY/REPLAYAI_CMD_PLAY_*停 BG / 恢复 / 重播
TTS_START/DATA/STOP…(不进队列)直接续 idle 定时器(非阻塞 keep-alive)
mic 数据(~50ms)(不进队列)无锁读门控后直接 tuya_ai_audio_input
worker 主体(节选)
STATIC VOID _hly_ai_worker_cb(PVOID_T args) {
    hly_ai_msg_t m;
    while (1) {
        if (tal_queue_fetch(_ai_cmd_queue, &m, SEM_WAIT_FOREVER) != OPRT_OK) continue;
        switch (m.cmd) {
        case AI_CMD_WAKEUP:
            if (!ai_start || _ai_stat != AI_STAT_IDEL) break;
            wukong_audio_player_stop(AI_PLAYER_ALL);   // 只卡 worker 自己
            wukong_audio_input_reset();
            tuya_ai_agent_event(AI_EVENT_CHAT_BREAK, 0);
            wake_up_flag = TRUE; paly_alart = TRUE;
            _hly_ai_apply_stat(AI_STAT_LISTEN);        // 独占改状态, 无锁
            wukong_audio_player_alert(AI_TOY_ALERT_TYPE_WAKEUP, FALSE);
            break;
        case AI_CMD_PLAY_END:
            if (paly_alart) paly_alart = FALSE;
            _hly_ai_apply_stat(wake_up_flag ? AI_STAT_LISTEN : AI_STAT_IDEL);
            break;
        ...
        }
    }
}
04

顺手修掉的 6 个 bug

  1. _hly_ai_posttal_queue_post 返回值并告警 —— 不再像原 _hly_ai_set_stat 静默丢状态。
  2. hly_ai_start / stop / chat_over 补上 return OPRT_OK —— 不再返回栈垃圾。
  3. _hly_ai_event 改为纯 void,不再 return 值。
  4. sprintf(&str, ...)snprintf(path, sizeof, ...),类型与越界都修正。
  5. 删除 mic 路径每帧的 TAL_PR_NOTICE 日志刷屏。
  6. KWS 回调取消每帧加锁,门控移入 worker。
05

待你核对项

因无法在此编译、且未持有全部头文件,以下与工程头文件相关处请确认:

  • hly_fs_load_psram(path, &data, &len) 第一参类型(原误传 &str,已改传字符串首址)。
  • snprintf 在该 libc 可用;否则回退 sprintf((char*)str, ...)
  • THREAD_CFG_T 字段、OPRT_COM_ERROR/OPRT_INVALID_PARMwukong_audio_player_set_resume/set_replaytuya_ai_input_start/stop 名称一致。
  • KWS 的 (INT_T)data 在 T5(32 位)安全,保留原写法以免引入头文件假设。
  • 验证场景:唤醒 → 打断(shutup)→ 隔夜待机再唤醒 全程有声、无卡顿、无 hang。
配套前提 本重构消除的是「业务层 / 播放器层」的重入与持锁阻塞;「底层 tkl_ao_put_frame 无限重试」必须同时保留前一轮的超时修复,二者配套才能根除「隔夜唤醒无声」。