247 lines
9.2 KiB
Markdown
247 lines
9.2 KiB
Markdown
# 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 状态:
|
|
|
|
```c
|
|
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. 配置项
|
|
|
|
```text
|
|
CONFIG_TQ_OTA_MIN_BATTERY_PERCENT=20
|
|
CONFIG_TQ_OTA_POWER_RETRY_MS=60000
|
|
```
|
|
|
|
对应的 Kconfig 定义:
|
|
|
|
```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 内部电量判定结果
|
|
|
|
```c
|
|
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. 电量有效性和门槛判断
|
|
|
|
```c
|
|
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 下载前门禁
|
|
|
|
```c
|
|
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`。
|
|
|
|
预期低电量日志:
|
|
|
|
```text
|
|
OTA 1.0.3 deferred: battery=19% minimum=20% charging=0
|
|
```
|
|
|
|
预期电量数据不可用日志:
|
|
|
|
```text
|
|
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 状态引脚只能表示正在充电,不能保证供电线不会在升级途中被拔掉。
|