10 KiB
云端抽签首次请求失败:会话准备问题与修复说明
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 时的交互优先级;
- 云端返回的抽签设备指令是否始终包含有效签型索引。