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