Files
lucky-cat/docs/ota-low-battery-guard.zh-CN.md

9.2 KiB

OTA 低电量保护实现说明

一、问题原因

原有 OTA 流程在服务端批准升级后,只检查以下条件:

  • Wi-Fi 是否已连接;
  • 当前是否存在语音交互;
  • OTA 所需 Internal RAM 和 DMA 连续内存是否充足;
  • 非强制升级是否已获得小程序确认。

流程没有读取设备的电池状态。因此,即使设备电量只剩 5%,只要服务端下发强制升级,固件仍会开始 HTTPS 下载、写入备用 OTA 分区并重启。

低电量升级存在两个主要风险:

  1. 下载和写 Flash 期间功耗增加,设备可能因电压下降而复位。
  2. 即使 ESP-IDF 使用双 OTA 分区,反复掉电和重试仍会造成升级失败、重复下载及额外耗电。

电池模块已经维护了统一状态,包括百分比、状态有效性、充电状态和最近一次有效采样时间,因此 OTA 不需要重新读取 ADC,也不应该自己计算电量。

二、解决方式

在 OTA 已经获得服务端批准、但尚未开始下载固件的位置增加电量门禁。

处理规则如下:

  • 电量小于 20%:禁止开始 OTA。
  • 电量等于或高于 20%:允许继续 OTA。
  • 电量状态无效、尚未产生有效采样或采样已经过期:禁止开始 OTA。
  • 强制升级不能绕过电量门禁。
  • 正在充电但电量低于 20%:仍然禁止升级。
  • 升级一旦开始,不因百分比在安装过程中下降而主动取消。
  • 低电量属于等待条件,不属于 OTA 错误,不增加连续失败次数。
  • 等待期间释放 OTA 占用的语音资源,60 秒后重新上报版本并获取新的下载链接。

新增 OTA 状态:

typedef enum {
    CONTROL_PLANE_OTA_STATE_DISABLED = 0,
    CONTROL_PLANE_OTA_STATE_IDLE,
    CONTROL_PLANE_OTA_STATE_WAITING_FOR_NETWORK,
    CONTROL_PLANE_OTA_STATE_WAITING_FOR_RESOURCES,
    CONTROL_PLANE_OTA_STATE_CHECKING,
    CONTROL_PLANE_OTA_STATE_WAITING_FOR_CONFIRMATION,
    CONTROL_PLANE_OTA_STATE_WAITING_FOR_POWER,
    CONTROL_PLANE_OTA_STATE_DOWNLOADING,
    CONTROL_PLANE_OTA_STATE_READY_TO_REBOOT,
    CONTROL_PLANE_OTA_STATE_ERROR,
    CONTROL_PLANE_OTA_STATE_STOPPED,
} control_plane_ota_state_t;

WAITING_FOR_POWER 用于表达“升级已获批准,但当前电池条件不允许开始”,避免将其错误归类为网络或安装失败。

三、具体代码

1. 配置项

CONFIG_TQ_OTA_MIN_BATTERY_PERCENT=20
CONFIG_TQ_OTA_POWER_RETRY_MS=60000

对应的 Kconfig 定义:

config TQ_OTA_MIN_BATTERY_PERCENT
    int "Minimum battery percentage to start OTA"
    default 20
    range 0 100
    depends on TQ_OTA_ENABLE && TQ_BATTERY_ENABLE
    help
        Defer an approved OTA update while the latest valid battery
        percentage is below this threshold. Forced updates do not
        bypass this safety gate. Once installation starts, percentage
        changes do not cancel the in-progress update.

config TQ_OTA_POWER_RETRY_MS
    int "OTA low-battery recheck interval (ms)"
    default 60000
    range 10000 3600000
    depends on TQ_OTA_ENABLE && TQ_BATTERY_ENABLE
    help
        Delay before reporting the version again after an approved OTA
        update is deferred because battery status is low, unavailable,
        or stale. A fresh report also refreshes expiring download URLs.

2. OTA 内部电量判定结果

typedef enum {
    CONTROL_PLANE_OTA_POWER_READY = 0,
    CONTROL_PLANE_OTA_POWER_LOW,
    CONTROL_PLANE_OTA_POWER_UNAVAILABLE,
} control_plane_ota_power_state_t;

三个结果分别代表:

  • READY:电量信息可信且达到升级门槛。
  • LOW:电量信息可信,但低于升级门槛。
  • UNAVAILABLE:电池状态读取失败、无效或过期。

3. 电量有效性和门槛判断

static control_plane_ota_power_state_t control_plane_ota_get_power_state(
        domain_battery_status_t *out_status,
        int64_t *out_age_ms,
        esp_err_t *out_error)
{
    if (out_status == NULL || out_age_ms == NULL || out_error == NULL) {
        return CONTROL_PLANE_OTA_POWER_UNAVAILABLE;
    }

    memset(out_status, 0, sizeof(*out_status));
    *out_age_ms = -1;
    *out_error = ESP_OK;

#if CONFIG_TQ_BATTERY_ENABLE
    esp_err_t err = domain_battery_get_status(out_status);
    if (err != ESP_OK) {
        *out_error = err;
        return CONTROL_PLANE_OTA_POWER_UNAVAILABLE;
    }

    int64_t now_ms = esp_timer_get_time() / 1000;
    if (out_status->last_valid_sample_time_ms > 0 &&
        now_ms >= out_status->last_valid_sample_time_ms) {
        *out_age_ms = now_ms - out_status->last_valid_sample_time_ms;
    }

    int64_t max_age_ms =
            (int64_t)CONFIG_TQ_BATTERY_SAMPLE_INTERVAL_SEC * 3000LL;
    if (max_age_ms < 30000LL) {
        max_age_ms = 30000LL;
    }
    if (!out_status->valid || *out_age_ms < 0 ||
        *out_age_ms > max_age_ms) {
        return CONTROL_PLANE_OTA_POWER_UNAVAILABLE;
    }
    if (out_status->percentage < CONFIG_TQ_OTA_MIN_BATTERY_PERCENT) {
        return CONTROL_PLANE_OTA_POWER_LOW;
    }
#else
    (void)out_status;
#endif

    return CONTROL_PLANE_OTA_POWER_READY;
}

采样最大有效时间取电池采样周期的 3 倍,并且最少允许 30 秒。默认每 10 秒采样一次,因此默认过期时间为 30 秒。

这里直接比较百分比,而不使用 DOMAIN_BATTERY_LEVEL_LOW。原因是现有 LOW 状态在小于等于 20% 时经过连续 3 次确认才进入,并且要恢复到 25% 才退出。直接比较可以准确实现“低于 20% 禁止,20% 允许”的产品规则。

4. OTA 下载前门禁

domain_battery_status_t battery = {0};
int64_t battery_age_ms = -1;
esp_err_t battery_err = ESP_OK;
control_plane_ota_power_state_t power_state =
        control_plane_ota_get_power_state(&battery,
                                          &battery_age_ms,
                                          &battery_err);
if (power_state != CONTROL_PLANE_OTA_POWER_READY) {
    initial_check_pending = false;
    control_plane_ota_release_voice_window(&voice_window);
    control_plane_ota_set_state(CONTROL_PLANE_OTA_STATE_WAITING_FOR_POWER,
                                ESP_OK,
                                response->http_status);

    if (power_state == CONTROL_PLANE_OTA_POWER_LOW) {
        ESP_LOGW(TAG,
                 "OTA %s deferred: battery=%u%% minimum=%u%% charging=%d",
                 response->firmware.version,
                 (unsigned)battery.percentage,
                 (unsigned)CONFIG_TQ_OTA_MIN_BATTERY_PERCENT,
                 battery.charge_state == DOMAIN_BATTERY_CHARGE_CHARGING
                         ? 1 : 0);
    } else {
        ESP_LOGW(TAG,
                 "OTA %s deferred: battery status unavailable "
                 "valid=%d age_ms=%lld err=%s",
                 response->firmware.version,
                 battery.valid ? 1 : 0,
                 (long long)battery_age_ms,
                 esp_err_to_name(battery_err));
    }

    heap_caps_free(response);
    control_plane_ota_wait_interruptible(CONFIG_TQ_OTA_POWER_RETRY_MS);
    continue;
}

这段代码位于服务端批准升级之后、固件下载之前。低电量分支不会调用安装函数,也不会显示 OTA 升级表情。

四、解决成果

实现后的 OTA 流程具备以下结果:

  1. 低于 20% 时,普通升级和强制升级都不会开始下载。
  2. 20% 边界行为明确,20% 可以升级,19% 不可以升级。
  3. 电池数据不可信时采用保守策略,不冒险升级。
  4. 低电量等待不会污染 OTA 失败计数,也不会触发错误退避。
  5. 等待时恢复语音服务,不会在 60 秒等待期间持续占用 OTA 网络窗口。
  6. 每次重试都会重新上报版本,避免继续使用已经过期的云存储签名链接。
  7. ESP-IDF 5.5.4 完整构建通过,新配置已进入生成的 sdkconfig。

预期低电量日志:

OTA 1.0.3 deferred: battery=19% minimum=20% charging=0

预期电量数据不可用日志:

OTA 1.0.3 deferred: battery status unavailable valid=0 age_ms=-1 err=ESP_OK

五、后续影响

正向影响

  • 降低低电量升级过程中掉电或复位的概率。
  • 不改变服务端接口和固件下载协议。
  • 不改变服务端所需的文件大小、SHA256 字段和 ESP-IDF 双分区校验机制。由于固件代码发生变化,新构建产物的文件大小和 SHA256 需要重新计算。
  • 不改变现有电量显示、低电提示音和充电状态逻辑。

行为变化

  • 设备低于 20% 时,即使服务端下发 force=1,也会等待电量恢复。
  • 充电状态不会绕过门槛。低于 20% 的设备需要先充到 20%。
  • 电池监控初始化失败或长期没有有效采样时,OTA 会持续处于等待电量状态,不会自动冒险升级。
  • 低电量期间每 60 秒重新进行一次版本上报,服务端会看到重复检查请求。

不处理的情况

  • OTA 开始后不会持续监控电量并主动取消。这样可以避免百分比抖动导致反复下载和额外耗电。
  • 本次修改不增加“电量不足无法升级”的专用语音或屏幕提示,只提供状态和日志。
  • 如果未来需要允许“插着稳定外部电源时低电量升级”,必须增加可靠的外部电源存在检测。当前 TP4057 状态引脚只能表示正在充电,不能保证供电线不会在升级途中被拔掉。