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

313 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云端抽签首次请求失败:会话准备问题与修复说明
## 1. 对应提交
```text
d386de2 fix(multimodal): prepare session for cloud fortune draw
```
本文说明设备启动后,必须先完成一次语音对话,SW2 抽签按钮才能成功请求云端的问题。本提交同时处理了后续发现的系统事件任务栈溢出;栈问题单独记录在《云端抽签触发系统事件任务栈溢出:问题与修复说明》中。
## 2. 问题现象
设备已经连接 Wi-Fi 和多模态 WebSocket,但尚未进行语音对话时,第一次按下 SW2 会出现:
```text
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 则可以正常发送抽签请求。
问题发生时的关键运行状态为:
```text
wifi=1 ws(conn=1 starting=0 stopping=0) task=0 listening=0
```
这表示网络和 WebSocket 已经可用,但云端多模态 task 尚未创建,服务端也没有进入 Listening 状态。
## 3. 问题原因
### 3.1 WebSocket 连接不等于多模态会话就绪
原抽签逻辑收到 SW2 短按后,直接构造并发送 `RequestToRespond`:
```c
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);
```
但发送接口要求以下条件全部满足:
```c
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`:
```text
PTT_STATE_WAIT_WIFI
-> PTT_STATE_WAIT_START_SENT
-> 发送 Start
-> 等待 task 和 dialog
-> 等待 DialogStateChanged(Listening)
-> 发送 SpeechStart
```
所以用户观察到的“必须先语音一次”,本质上不是抽签依赖语音内容,而是语音流程替抽签完成了缺失的会话准备步骤。
### 3.3 原错误日志合并了两种状态
上层将所有 `ESP_ERR_INVALID_STATE` 都记录为:
```text
multimodal session unavailable or request pending
```
本次失败实际属于 `multimodal session unavailable`,不是已有抽签请求仍处于 pending。
## 4. 解决方式
### 4.1 将抽签改为异步请求
SW2 事件不再要求 Prompt 当场发送成功,而是登记一个 pending 请求:
```c
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;
}
```
相关状态含义如下:
```c
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 的状态快照后推进:
```c
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;
}
```
处理规则为:
```text
WebSocket 未连接
-> 请求连接或重连
WebSocket 已连接,但没有 active task
-> 发送 Start
task 已存在,但尚未 Listening
-> 等待服务端事件
task 已存在且服务端处于 Listening
-> 发送“我要抽签” Prompt
```
### 4.3 缺少 task 时自动发送 Start
```c
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
```c
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:
```c
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 秒:
```c
#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、响应结束和客户端停止时,原有清理入口会同时复位新增状态:
```c
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. 修复后的流程
```text
SW2 短按
-> 登记抽签请求
-> 唤醒 multimodal watchdog
-> 检查 WebSocket
-> 没有 task 时发送 Start
-> 等待 task_id、dialog_id 和 Listening
-> 发送“我要抽签”
-> 接收模型抽签指令
-> 展示对应签型并播放 TTS
-> RespondingEnded 后清除 pending
```
预期关键日志为:
```text
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 时的交互优先级;
- 云端返回的抽签设备指令是否始终包含有效签型索引。