# OTA 周期检查与 HTTPS 下载稳定性修复说明 ## 1. 提交信息 ```text 451d989 fix(ota): stabilize periodic checks and HTTPS downloads ``` 本提交解决三个相关问题: 1. 首次 OTA 检查完成后,后续周期检查可能永远不再执行。 2. 下载腾讯云 COS 固件时,TLS 接收缓冲区可能不足,导致 HTTPS 响应头读取失败。 3. 固件版本仍为 `1.0.0`,无法作为新的 OTA 测试目标与旧固件区分。 ## 2. 问题原因 ### 2.1 后续 OTA 检查被常驻云端 task 永久阻塞 多模态语音使用的是常驻双工 task。一次对话结束后,WebSocket 和云端 task 不会立即销毁,而是保持在 `Listening` 状态等待下一轮对话。 旧的 OTA 空闲判断要求同时满足: ```c return !state.has_active_task && !state.client_speech_active && !state.remote_responding && !state.local_responding_started; ``` 其中 `has_active_task` 的含义是“云端 task 是否存在”,不是“用户是否正在对话”。 正常多轮对话空闲时的状态可能是: ```text has_active_task=1 server_listening=1 client_speech_active=0 remote_responding=0 local_responding_started=0 ``` 此时用户没有说话,设备也没有播放回答,实际上可以安全进行 OTA 检查。但因为 `has_active_task=1`,旧逻辑始终认为设备忙碌。 结果是: ```text 首次开机检查成功 -> 云端 task 建立并常驻 -> OTA 周期到期 -> 空闲判断持续失败 -> 每秒继续等待资源 -> 后续检查永远无法开始 ``` 所以即使把检查间隔改成 5 分钟,也不会在 5 分钟后看到第二次请求。这不是计时器没有到,而是计时器到期后一直卡在资源等待阶段。 ### 2.2 等待资源时缺少明确诊断信息 旧代码进入 `WAITING_FOR_RESOURCES` 后只更新内部状态,没有打印具体阻塞原因。从串口日志看起来像 OTA worker 没有继续运行,很难区分以下情况: - 正在录音; - 正在播放云端回复; - 本地回复事件尚未结束; - 仅仅存在一个空闲的常驻 task; - 获取多模态状态失败。 如果每秒都打印状态又会产生大量重复日志,因此需要“一次等待周期只打印一次”的诊断机制。 ### 2.3 OTA HTTPS 响应读取失败 固件下载地址使用腾讯云 COS 的 HTTPS 链接。日志中出现过: ```text esp-tls-mbedtls: read error :-0x7100 platform_ota: OTA response header fetch failed cp_ota: OTA install failed: ESP_ERR_INVALID_RESPONSE ``` TLS 握手和 HTTP 响应读取需要一个足够大的入站缓冲区。原配置为: ```text CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=8192 ``` 当服务端证书链、TLS 记录或响应头超过当前可接收范围时,mbedTLS 无法完整读取记录,最终表现为 HTTPS 响应无效,而不是固件 SHA-256 错误。 ### 2.4 固件版本无法区分 设备向 OTA 服务端上报 `version.txt` 中的版本。旧值是: ```text 1.0.0 ``` 如果新编译固件仍使用旧版本号,服务端无法可靠判断设备是否已经升级,也可能重复下发同一版本。 ## 3. 解决方式 ### 3.1 用“真实交互状态”判断 OTA 是否可以运行 修复后的判断不再把空闲常驻 task 当作繁忙条件: ```c static bool control_plane_ota_runtime_is_idle(void) { if (!control_plane_voice_turn_is_idle()) { return false; } control_plane_multimodal_runtime_state_t state = {0}; if (control_plane_multimodal_get_runtime_state(&state) != ESP_OK) { return true; } /* A duplex cloud task may remain in Listening between voice turns. OTA * can retire and later restore that task once all real interaction has * stopped. */ return !state.client_speech_active && !state.remote_responding && !state.local_responding_started; } ``` 这里仍然保留了四层保护: 1. `control_plane_voice_turn_is_idle()`:设备按键/本地唤醒控制的整轮语音流程必须处于空闲。 2. `client_speech_active`:设备不能正在向云端发送用户语音。 3. `remote_responding`:云端不能正在下发回答。 4. `local_responding_started`:本地播放链路不能仍处于回复阶段。 只移除了 `!state.has_active_task`。OTA 获取资源窗口后会主动停止这个空闲 task,检查完成后再恢复 WebSocket 和本地唤醒,因此不会同时使用同一套 TLS、音频和内存资源。 ### 3.2 增加一次性资源等待日志 worker 增加一个局部标志: ```c bool initial_check_pending = true; bool resource_wait_logged = false; ``` 当检查已经到期但语音仍忙时,只打印一次当前状态: ```c if (err == ESP_ERR_INVALID_STATE) { control_plane_ota_set_state( CONTROL_PLANE_OTA_STATE_WAITING_FOR_RESOURCES, ESP_OK, -1); if (!resource_wait_logged) { resource_wait_logged = true; control_plane_multimodal_runtime_state_t state = {0}; if (control_plane_multimodal_get_runtime_state(&state) == ESP_OK) { ESP_LOGI(TAG, "OTA check due; waiting for voice interaction: " "task=%d listening=%d speech=%d responding=%d " "local_responding=%d", state.has_active_task ? 1 : 0, state.server_listening ? 1 : 0, state.client_speech_active ? 1 : 0, state.remote_responding ? 1 : 0, state.local_responding_started ? 1 : 0); } else { ESP_LOGI(TAG, "OTA check due; waiting for runtime resources"); } } control_plane_ota_wait_interruptible(1000U); continue; } resource_wait_logged = false; ``` 网络断开或资源重新可用后会清除标志。下一次独立的等待周期仍会打印一次,不会丢失后续诊断信息。 ### 3.3 将 TLS 入站缓冲区提高到 16 KB 配置修改为: ```text CONFIG_MBEDTLS_DYNAMIC_BUFFER=y CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 ``` 含义如下: - 入站缓冲区从 8 KB 增加到 16 KB,用于接收更大的证书链、TLS 记录和 HTTP 响应。 - 出站缓冲区仍为 4 KB,因为设备请求体较小,不需要同步扩大。 - 动态缓冲保持启用,TLS 会话结束后相关内存可以释放,不是永久占用 16 KB。 - 入站和出站使用非对称大小,避免不必要地同时扩大两个方向。 ### 3.4 更新固件版本 ```text 1.0.0 -> 1.0.2 ``` 设备上报版本变为 `1.0.2`。后端如果要让该设备继续升级,目标版本必须按照后端版本比较规则高于当前版本,并且固件文件内部版本也应与发布信息一致。 ## 4. 解决成果 1. 周期到期后,即使云端 task 常驻在 `Listening`,OTA 仍能在真实语音空闲时执行。 2. OTA 不会在用户录音、云端回复或本地回复期间强行抢占资源。 3. 资源暂时不可用时,日志会明确显示是哪种语音状态阻塞检查,同时不会每秒刷屏。 4. COS HTTPS 下载能够接收更大的 TLS 入站记录,降低响应头读取失败和 `ESP_ERR_INVALID_RESPONSE` 的概率。 5. 新固件以 `1.0.2` 上报,服务端能够区分升级前后的设备版本。 6. 使用 ESP-IDF 5.5.4 完整编译通过。 ## 5. 后续影响 ### 5.1 正向影响 - 5 分钟调试间隔和正式 6 小时间隔都能正常触发后续检查。 - 常驻双工会话不再错误阻止 OTA。 - OTA 等待语音结束后才运行,对正常对话的影响更可控。 - HTTPS 下载兼容更大的证书链和 TLS 数据记录。 ### 5.2 资源影响 TLS 入站缓冲上限增加了 8 KB。由于启用了动态缓冲,这部分主要体现在 HTTPS 会话期间的峰值内存,而不是设备全程常驻占用。 OTA 已经通过 voice window 暂停本地唤醒和多模态网络服务,以释放 internal RAM。后续如果新增其他会同时运行的 HTTPS 服务,仍应遵守同样的资源互斥策略。 ### 5.3 行为影响 - 周期检查到期时如果用户正在对话,实际请求时间会顺延,直到语音链路空闲。 - OTA 可以停止空闲的常驻云端 task,检查结束后会重新建立 WebSocket;这是预期行为。 - 版本改为 `1.0.2` 后,服务端不应再向设备下发 `1.0.1` 或相同版本,除非后端明确支持强制降级。 ### 5.4 仍需实机验证的项目 1. 将检查间隔设为 5 分钟,确认首次检查后还能出现第二次和第三次请求。 2. 在检查到期前主动开始对话,确认先打印等待语音日志,对话结束后再执行 OTA。 3. 使用 COS 固件链接进行完整下载,确认不再出现 TLS 响应头读取失败。 4. 确认下载后的 size 和 SHA-256 校验仍然有效。 5. 确认检查结束后本地唤醒和 WebSocket 均能恢复。