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