Files
lucky-cat/docs/knowledge vault/ota-periodic-check-and-https-download-fixes.zh-CN.md
T

8.8 KiB
Raw Blame History

OTA 周期检查与 HTTPS 下载稳定性修复说明

1. 提交信息

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 空闲判断要求同时满足:

return !state.has_active_task &&
       !state.client_speech_active &&
       !state.remote_responding &&
       !state.local_responding_started;

其中 has_active_task 的含义是“云端 task 是否存在”,不是“用户是否正在对话”。

正常多轮对话空闲时的状态可能是:

has_active_task=1
server_listening=1
client_speech_active=0
remote_responding=0
local_responding_started=0

此时用户没有说话,设备也没有播放回答,实际上可以安全进行 OTA 检查。但因为 has_active_task=1,旧逻辑始终认为设备忙碌。

结果是:

首次开机检查成功
    -> 云端 task 建立并常驻
    -> OTA 周期到期
    -> 空闲判断持续失败
    -> 每秒继续等待资源
    -> 后续检查永远无法开始

所以即使把检查间隔改成 5 分钟,也不会在 5 分钟后看到第二次请求。这不是计时器没有到,而是计时器到期后一直卡在资源等待阶段。

2.2 等待资源时缺少明确诊断信息

旧代码进入 WAITING_FOR_RESOURCES 后只更新内部状态,没有打印具体阻塞原因。从串口日志看起来像 OTA worker 没有继续运行,很难区分以下情况:

  • 正在录音;
  • 正在播放云端回复;
  • 本地回复事件尚未结束;
  • 仅仅存在一个空闲的常驻 task;
  • 获取多模态状态失败。

如果每秒都打印状态又会产生大量重复日志,因此需要“一次等待周期只打印一次”的诊断机制。

2.3 OTA HTTPS 响应读取失败

固件下载地址使用腾讯云 COS 的 HTTPS 链接。日志中出现过:

esp-tls-mbedtls: read error :-0x7100
platform_ota: OTA response header fetch failed
cp_ota: OTA install failed: ESP_ERR_INVALID_RESPONSE

TLS 握手和 HTTP 响应读取需要一个足够大的入站缓冲区。原配置为:

CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=8192

当服务端证书链、TLS 记录或响应头超过当前可接收范围时,mbedTLS 无法完整读取记录,最终表现为 HTTPS 响应无效,而不是固件 SHA-256 错误。

2.4 固件版本无法区分

设备向 OTA 服务端上报 version.txt 中的版本。旧值是:

1.0.0

如果新编译固件仍使用旧版本号,服务端无法可靠判断设备是否已经升级,也可能重复下发同一版本。

3. 解决方式

3.1 用“真实交互状态”判断 OTA 是否可以运行

修复后的判断不再把空闲常驻 task 当作繁忙条件:

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 增加一个局部标志:

bool initial_check_pending = true;
bool resource_wait_logged = false;

当检查已经到期但语音仍忙时,只打印一次当前状态:

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

配置修改为:

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 更新固件版本

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 均能恢复。