Files
lucky-cat/docs/knowledge vault/bugfix-cloud-chouqian-session-readiness.zh-CN.md
T

10 KiB
Raw Blame History

云端抽签首次请求失败:会话准备问题与修复说明

1. 对应提交

d386de2 fix(multimodal): prepare session for cloud fortune draw

本文说明设备启动后,必须先完成一次语音对话,SW2 抽签按钮才能成功请求云端的问题。本提交同时处理了后续发现的系统事件任务栈溢出;栈问题单独记录在《云端抽签触发系统事件任务栈溢出:问题与修复说明》中。

2. 问题现象

设备已经连接 Wi-Fi 和多模态 WebSocket,但尚未进行语音对话时,第一次按下 SW2 会出现:

cp_system_event: key event key=SW2 type=SHORT_PRESS ...
control_plane_mm_rt: Cloud chouqian request completed: request send failed
cp_system_event: Cloud chouqian request skipped: multimodal session unavailable or request pending

完成一次 PTT 语音对话后,再按 SW2 则可以正常发送抽签请求。

问题发生时的关键运行状态为:

wifi=1 ws(conn=1 starting=0 stopping=0) task=0 listening=0

这表示网络和 WebSocket 已经可用,但云端多模态 task 尚未创建,服务端也没有进入 Listening 状态。

3. 问题原因

3.1 WebSocket 连接不等于多模态会话就绪

原抽签逻辑收到 SW2 短按后,直接构造并发送 RequestToRespond:

const control_plane_multimodal_respond_request_t request = {
    .type = CONTROL_PLANE_MULTIMODAL_RESPOND_TYPE_PROMPT,
    .text = "我要抽签",
    .parameters_json = NULL,
};

esp_err_t err = control_plane_multimodal_send_request_to_respond(&request);

但发送接口要求以下条件全部满足:

if (task_id[0] == '\0' || dialog_id[0] == '\0' || !server_listening) {
    return ESP_ERR_INVALID_STATE;
}

设备刚启动时只有 WebSocket transport 已连接,task_id、dialog_id 和 server_listening 尚未准备好,因此请求返回 ESP_ERR_INVALID_STATE。

3.2 首次语音流程隐式完成了会话初始化

PTT 状态机在发现没有 active task 时会先发送 Start:

PTT_STATE_WAIT_WIFI
    -> PTT_STATE_WAIT_START_SENT
    -> 发送 Start
    -> 等待 task 和 dialog
    -> 等待 DialogStateChanged(Listening)
    -> 发送 SpeechStart

所以用户观察到的“必须先语音一次”,本质上不是抽签依赖语音内容,而是语音流程替抽签完成了缺失的会话准备步骤。

3.3 原错误日志合并了两种状态

上层将所有 ESP_ERR_INVALID_STATE 都记录为:

multimodal session unavailable or request pending

本次失败实际属于 multimodal session unavailable,不是已有抽签请求仍处于 pending。

4. 解决方式

4.1 将抽签改为异步请求

SW2 事件不再要求 Prompt 当场发送成功,而是登记一个 pending 请求:

esp_err_t control_plane_request_cloud_chouqian(void)
{
    uint32_t requested_ms = control_plane_now_ms();

    portENTER_CRITICAL(&s_multimodal_runtime_lock);
    if (s_cloud_chouqian_pending) {
        portEXIT_CRITICAL(&s_multimodal_runtime_lock);
        return ESP_ERR_INVALID_STATE;
    }
    s_cloud_chouqian_pending = true;
    s_cloud_chouqian_action_in_progress = false;
    s_cloud_chouqian_prompt_sent = false;
    s_cloud_chouqian_requested_ms = requested_ms;
    s_model_command_dispatched = false;
    portEXIT_CRITICAL(&s_multimodal_runtime_lock);

    ESP_LOGI(TAG, "Cloud chouqian request queued");
    control_plane_wake_multimodal_watchdog();
    return ESP_OK;
}

相关状态含义如下:

static bool s_cloud_chouqian_pending;
static bool s_cloud_chouqian_action_in_progress;
static bool s_cloud_chouqian_prompt_sent;
static uint32_t s_cloud_chouqian_requested_ms;
  • pending:本轮按钮抽签尚未结束;
  • action_in_progress:正在执行 Start 或 Prompt 发送,防止并发重复操作;
  • prompt_sent:Prompt 已发送,后续只等待云端结果和响应结束;
  • requested_ms:请求登记时间,用于限制会话准备时长。

4.2 根据会话真实状态推进请求

抽签请求由 multimodal watchdog 查询 session service 的状态快照后推进:

control_plane_multimodal_runtime_state_t state = {0};
esp_err_t state_err = control_plane_multimodal_get_runtime_state(&state);
if (state_err != ESP_OK) {
    return;
}

if (!state.connected) {
    (void)control_plane_schedule_websocket_wait_task();
    return;
}

bool should_send_start = !state.has_active_task;
bool should_send_prompt = state.has_active_task && state.server_listening;
if (!should_send_start && !should_send_prompt) {
    return;
}

处理规则为:

WebSocket 未连接
    -> 请求连接或重连

WebSocket 已连接,但没有 active task
    -> 发送 Start

task 已存在,但尚未 Listening
    -> 等待服务端事件

task 已存在且服务端处于 Listening
    -> 发送“我要抽签” Prompt

4.3 缺少 task 时自动发送 Start

if (should_send_start) {
    esp_err_t start_err = control_plane_multimodal_send_start();

    portENTER_CRITICAL(&s_multimodal_runtime_lock);
    s_cloud_chouqian_action_in_progress = false;
    portEXIT_CRITICAL(&s_multimodal_runtime_lock);

    if (start_err == ESP_OK) {
        ESP_LOGI(TAG, "Cloud chouqian waiting for multimodal session");
    } else if (start_err != ESP_ERR_INVALID_STATE) {
        ESP_LOGW(TAG,
                 "Cloud chouqian session start failed: %s",
                 esp_err_to_name(start_err));
        control_plane_clear_cloud_chouqian_pending("session start failed");
    }
    return;
}

Start 只负责创建云端 task。抽签不会发送 SpeechStart,不会启动麦克风,也不会上传音频。

4.4 Listening 后发送抽签 Prompt

const control_plane_multimodal_respond_request_t request = {
    .type = CONTROL_PLANE_MULTIMODAL_RESPOND_TYPE_PROMPT,
    .text = "我要抽签",
    .parameters_json = NULL,
};
esp_err_t request_err =
        control_plane_multimodal_send_request_to_respond(&request);

if (request_err != ESP_OK) {
    control_plane_clear_cloud_chouqian_pending("request send failed");
    return;
}

ESP_LOGI(TAG, "Cloud chouqian prompt sent");

prompt_sent 在发送前置位,防止 watchdog 周期检查和会话事件同时触发重复发送。

4.5 由会话事件继续推进

服务端确认 task 或进入 Listening 后,仅唤醒 watchdog:

case CONTROL_PLANE_MULTIMODAL_EVENT_TASK_STARTED:
    control_plane_wake_multimodal_watchdog();
    break;

case CONTROL_PLANE_MULTIMODAL_EVENT_SERVER_LISTENING:
    control_plane_wake_multimodal_watchdog();
    break;

dialog_id 在 session service 解析消息时先保存,再发出 Listening 事件,因此 watchdog 被唤醒后能够满足 Prompt 的发送条件。

4.6 增加准备超时和统一清理

会话准备最长允许 10 秒:

#define CONTROL_PLANE_CLOUD_CHOUQIAN_PREPARE_TIMEOUT_MS 10000U

if ((now_ms - requested_ms) >=
        CONTROL_PLANE_CLOUD_CHOUQIAN_PREPARE_TIMEOUT_MS) {
    control_plane_clear_cloud_chouqian_pending("session prepare timeout");
    return;
}

断线、transport error、task failed、output error、响应结束和客户端停止时,原有清理入口会同时复位新增状态:

s_cloud_chouqian_pending = false;
s_cloud_chouqian_action_in_progress = false;
s_cloud_chouqian_prompt_sent = false;
s_cloud_chouqian_requested_ms = 0U;

5. 修复后的流程

SW2 短按
    -> 登记抽签请求
    -> 唤醒 multimodal watchdog
    -> 检查 WebSocket
    -> 没有 task 时发送 Start
    -> 等待 task_id、dialog_id 和 Listening
    -> 发送“我要抽签”
    -> 接收模型抽签指令
    -> 展示对应签型并播放 TTS
    -> RespondingEnded 后清除 pending

预期关键日志为:

control_plane_mm_rt: Cloud chouqian request queued
cp_mm_session: Start sent (...)
control_plane_mm_rt: Cloud chouqian waiting for multimodal session
cp_mm_session: Cloud dialog entered Listening
control_plane_mm_rt: Cloud chouqian prompt sent
control_plane_mm_rt: Cloud chouqian result received: <签型>
control_plane_mm_rt: Cloud chouqian request completed: response ended

如果会话原本已经处于 Listening,则会跳过 Start 和等待过程,直接发送 Prompt。

6. 解决成果

  • 首次启动后不需要先进行语音对话即可发起云端抽签;
  • 抽签复用了多模态会话准备能力,但没有复用录音和音频上行;
  • 请求准备、Prompt 发送和响应等待具备明确状态;
  • watchdog 和事件并发触发时不会重复发送 Prompt;
  • 会话无法在 10 秒内准备完成时可以自动退出 pending;
  • transport 和 task 异常仍会通过统一路径释放抽签状态;
  • ESP-IDF v5.5.4 完整构建通过;
  • 生成的应用固件大小为 0x20f450 字节,最小应用分区剩余约 41%。

7. 后续影响与边界

7.1 对语音流程的影响

抽签创建的 task 与语音流程使用同一 session service。抽签结束后,如果 task 仍有效,后续 PTT 可以直接复用该 task,不需要重复发送 Start。抽签本身不会启动 ASR 或麦克风采集。

7.2 对并发按键的影响

一轮抽签从排队到响应结束期间,新的 SW2 抽签请求仍返回 ESP_ERR_INVALID_STATE,用于避免同一 task 内并发多轮抽签响应。

7.3 对断线恢复的影响

请求登记时如果 WebSocket 尚未连接,会触发已有的连接调度。若网络和鉴权无法在 10 秒内完成,请求会以 session prepare timeout 结束,用户需要网络恢复后重新按键。

7.4 对云端资源的影响

以前只有首次语音会创建云端 task;修复后首次抽签也可以创建 task。因此只使用抽签功能的设备会更早创建云端会话,但不会额外建立第二条 WebSocket 或第二个并行 task。

7.5 仍需关注的验证项

  • 冷启动后第一次按 SW2 的真实设备端到端结果;
  • 弱网环境下 10 秒准备超时是否符合产品体验;
  • 抽签准备期间同时触发 PTT 时的交互优先级;
  • 云端返回的抽签设备指令是否始终包含有效签型索引。