Files
2026-06-12 17:03:10 +08:00

358 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OTA Manager 回调文档
> 版本: 1.0
> 目标芯片: ESP32-S3
> 框架: ESP-IDF 5.1.x
> 模块: `ota_manager.h` / `ota_manager.c`
---
## 1. 架构概览
```
┌──────────────────────────────────────────────────────────┐
│ 传输层(USB / 串口 / 蓝牙 / SD / HTTP
│ ↓ │
│ ota_begin(size) → ota_write(chunk)×N → ota_end() │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ OTA Manager(本模块) │ │
│ │ · 状态机管理(IDLE → PROGRESS → VERIFY → DONE)│ │
│ │ · esp_ota_ops 封装(分区选择 / 擦除 / 写入) │ │
│ │ · 回调触发(进度 / 状态 / 完成 / 错误) │ │
│ │ · LED 反馈 │ │
│ └──────────────┬──────────────────────────────────┘ │
│ │ 回调(可注册 4 种) │
│ ┌───────────┼───────────┬──────────────┐ │
│ ▼ ▼ ▼ ▼ │
│ progress state complete error │
│ (进度条) (状态灯) (重启逻辑) (告警/日志) │
└──────────────────────────────────────────────────────────┘
```
**设计原则**OTA Manager 只负责底层固件写入与分区切换,**不关心**固件数据从哪里来。传输层代码只需顺序调用三个 API,并通过回调感知进度。
---
## 2. 状态机
```
ota_begin()
IDLE ──────────────────────────→ IN_PROGRESS
↑ │
│ │ ota_write()
│ ▼ (循环)
│ IN_PROGRESS
│ │
│ ┌─────────────┼──────────────┐
│ │ ota_end() │ (写入失败) │
│ ▼ ▼ │
│ VERIFYING ERROR │
│ │ │ │
│ ┌───────────┤ │ ota_abort() │
│ │ 成功 │ 失败 ▼ │
│ ▼ ▼ IDLE │
│ COMPLETE ERROR │
│ │ │
│ │ esp_restart() │
│ ▼ │
│ (重启) │
└────────────────────────────────────────────────────┘
```
**状态枚举** (`ota_state_t`)
| 状态 | 含义 | 允许的操作 |
|------|------|-----------|
| `OTA_STATE_IDLE` | 空闲,就绪 | `ota_begin()` |
| `OTA_STATE_IN_PROGRESS` | 正在接收固件 | `ota_write()`, `ota_end()`, `ota_abort()` |
| `OTA_STATE_VERIFYING` | 正在校验固件 | 无(内部过程) |
| `OTA_STATE_COMPLETE` | 更新成功 | 重启设备 |
| `OTA_STATE_ERROR` | 发生错误 | `ota_abort()` |
---
## 3. 回调详解
### 3.1 进度回调 — `ota_progress_cb_t`
```c
typedef void (*ota_progress_cb_t)(int percent, size_t bytes_written, size_t total_size);
```
**触发时机**:每次 `ota_write()` 成功返回后。
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `percent` | `int` | 完成百分比 (0100)。若 `ota_begin()` 传入 `total_size=0`,则始终为 0 |
| `bytes_written` | `size_t` | 累计已写入字节数 |
| `total_size` | `size_t` | 声明的固件总字节数(即 `ota_begin` 的参数,0=未知) |
**用途**:驱动进度条 UI、串口日志输出。
**示例**
```c
static void on_progress(int percent, size_t written, size_t total)
{
printf("\rOTA: %d%% (%zu/%zu bytes)", percent, written, total);
// 或通过 LVGL 更新进度条:
// lv_bar_set_value(bar, percent, LV_ANIM_OFF);
}
```
---
### 3.2 状态变化回调 — `ota_state_cb_t`
```c
typedef void (*ota_state_cb_t)(ota_state_t old_state, ota_state_t new_state);
```
**触发时机**:状态机每次发生转换时。
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `old_state` | `ota_state_t` | 变化前的状态 |
| `new_state` | `ota_state_t` | 变化后的状态 |
**典型用法**
| 场景 | 检查条件 | 操作 |
|------|---------|------|
| 更新成功 | `new_state == OTA_STATE_COMPLETE` | 自动重启 `esp_restart()` |
| 更新失败 | `new_state == OTA_STATE_ERROR` | 显示错误提示、记录日志 |
| 开始更新 | `new_state == OTA_STATE_IN_PROGRESS` | 禁用用户交互、显示进度 UI |
| 恢复空闲 | `new_state == OTA_STATE_IDLE` | 恢复用户交互 |
**示例**
```c
static void on_state_change(ota_state_t old, ota_state_t new_state)
{
ESP_LOGI("app", "OTA state: %d → %d", old, new_state);
if (new_state == OTA_STATE_COMPLETE) {
ESP_LOGI("app", "Update OK, restarting in 3s...");
vTaskDelay(pdMS_TO_TICKS(3000));
esp_restart();
} else if (new_state == OTA_STATE_ERROR) {
ESP_LOGE("app", "OTA failed, back to idle");
}
}
```
---
### 3.3 完成回调 — `ota_complete_cb_t`
```c
typedef void (*ota_complete_cb_t)(bool success, ota_error_t error_code);
```
**触发时机**
- `ota_end()` 处理完毕后
- `ota_abort()` 处理完毕后
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `success` | `bool` | `true` = 更新成功,`false` = 失败或被中止 |
| `error_code` | `ota_error_t` | 失败时的错误码(`success=true` 时始终为 `OTA_ERR_NONE` |
**与状态变化回调的区别**
- `ota_state_cb_t` 关注「状态发生了什么变化」
- `ota_complete_cb_t` 关注「这次 OTA 最终成功了还是失败了」+ 失败原因
两者可同时使用,但建议至少注册一个来处理重启逻辑。
**示例**
```c
static void on_complete(bool success, ota_error_t err)
{
if (success) {
ESP_LOGI("app", "OTA complete, rebooting...");
esp_restart();
} else {
ESP_LOGW("app", "OTA failed, err=%d", err);
// 返回主界面
}
}
```
---
### 3.4 错误回调 — `ota_error_cb_t`
```c
typedef void (*ota_error_cb_t)(ota_error_t error_code, const char *message);
```
**触发时机**:每当发生可恢复或不致命的错误时(写入失败、校验失败等)。
**参数**
| 参数 | 类型 | 说明 |
|------|------|------|
| `error_code` | `ota_error_t` | 错误码枚举值 |
| `message` | `const char *` | 人类可读的错误描述(静态字符串,无需释放) |
**错误码一览**
| 错误码 | 值 | 含义 |
|-------|---|------|
| `OTA_ERR_NONE` | 0 | 无错误 |
| `OTA_ERR_NOT_INITIALIZED` | 1 | 未调用 `ota_begin()` |
| `OTA_ERR_ALREADY_IN_PROGRESS` | 2 | 已有进行中的 OTA |
| `OTA_ERR_BEGIN_FAILED` | 3 | `ota_begin()` 失败(分区擦除失败等) |
| `OTA_ERR_WRITE_FAILED` | 4 | 写入 flash 失败 |
| `OTA_ERR_END_FAILED` | 5 | `ota_end()` 失败 |
| `OTA_ERR_ABORT_FAILED` | 6 | `ota_abort()` 失败 |
| `OTA_ERR_SIZE_MISMATCH` | 7 | 实写大小与声明不符 |
| `OTA_ERR_NO_OTA_PARTITION` | 8 | 分区表无可用 OTA 分区 |
| `OTA_ERR_STATE_ERROR` | 9 | 当前状态不允许该操作 |
| `OTA_ERR_VERIFY_FAILED` | 10 | 固件校验失败 |
---
## 4. 完整注册示例
```c
#include "ota_manager.h"
#include "esp_system.h"
/* ---- 回调实现 ---- */
static void on_progress(int pct, size_t written, size_t total)
{
printf("\rOTA %d%% (%zu/%zu)", pct, written, total);
}
static void on_state_change(ota_state_t old, ota_state_t new_state)
{
if (new_state == OTA_STATE_COMPLETE) {
printf("\nUpdate OK. Rebooting...\n");
esp_restart();
}
}
static void on_complete(bool ok, ota_error_t err)
{
if (!ok) printf("\nOTA failed: %d\n", err);
}
static void on_error(ota_error_t code, const char *msg)
{
ESP_LOGE("app", "OTA error %d: %s", code, msg);
}
/* ---- 注册(在 app_main 初始化阶段调用一次) ---- */
void ota_callbacks_init(void)
{
ota_set_progress_callback(on_progress);
ota_set_state_callback(on_state_change);
ota_set_complete_callback(on_complete);
ota_set_error_callback(on_error);
}
```
---
## 5. 与 LVGL UI 集成要点
### 5.1 FreeRTOS 线程安全
LVGL 对象操作**必须在 UI Task 中进行**。OTA 的调用方通常运行在另一个 Task 中,回调在调用线程中触发。因此:
```c
// ❌ 错误:在进度回调中直接操作 LVGL
static void on_progress(int pct, ...) {
lv_bar_set_value(ui_bar, pct, LV_ANIM_OFF); // 非 UI 线程!
}
// ✅ 正确:通过全局变量 + LVGL Timer 轮询
static volatile int g_ota_pct = 0;
static void on_progress(int pct, ...) {
g_ota_pct = pct;
}
// 在 LVGL Timer10Hz)中:
static void lv_timer_update_ota(lv_timer_t *timer) {
lv_bar_set_value(ui_bar, g_ota_pct, LV_ANIM_ON);
}
```
### 5.2 建议的 UI 状态映射
| OTA 状态 | UI 行为 |
|---------|--------|
| IDLE | 显示「固件升级」入口 |
| IN_PROGRESS | 显示进度条 + 「正在更新…」 |
| VERIFYING | 进度条 100%,文字改为「正在校验…」 |
| COMPLETE | 「更新成功,3 秒后重启」+ 倒计时 |
| ERROR | 红色警告 + 错误描述 + «重试»按钮 |
---
## 6. LED 反馈模式
OTA Manager 内置了 LED 反馈(通过 `hw_led_set()`),无需额外配置。
| 阶段 | LED 行为 | 含义 |
|------|---------|------|
| `begin` 调用后 | 常亮 | OTA 进行中 |
| 每次 `write` | 短暂熄灭→亮起 | 数据写入闪烁(约 2ms) |
| `end` 成功 | 常亮 | 更新完成,等待重启 |
| `end` 失败 / `abort` | 熄灭 | 回到空闲 |
---
## 7. 错误处理建议
```
ota_begin() 返回非 ESP_OK
├─ OTA_ERR_ALREADY_IN_PROGRESS → 先调 ota_abort(),再重试 begin
├─ OTA_ERR_NO_OTA_PARTITION → 检查分区表是否正确烧录
└─ OTA_ERR_BEGIN_FAILED → 检查 flash 是否正常
ota_write() 返回非 ESP_OK
└─ 调 ota_abort(),提示用户重试
ota_end() 返回非 ESP_OK
└─ 调 ota_abort(),固件未切换,设备仍运行旧版本
```
---
## 8. 平台兼容性
| 特性 | 要求 |
|------|------|
| ESP-IDF 版本 | ≥ 5.0(本模块使用 v5.1.x API |
| 分区表 | 必须含 `factory` + 至少一个 `ota_0` 类型分区 |
| Flash 大小 | ≥ 2MB(推荐 8MB,双 OTA 槽位) |
| `hw_led_set()` | 需由项目提供(当前在 `hw_init.h` 中定义) |
---
## 9. API 快速参考
| 函数 | 返回值 | 副作用 |
|------|-------|--------|
| `ota_begin(size)` | `esp_err_t` | 擦除 OTA 分区,LED 亮 |
| `ota_write(data, len)` | `esp_err_t` | 写入 flash,触发进度回调,LED 闪烁 |
| `ota_end()` | `esp_err_t` | 校验固件,切换启动分区,触发完成回调 |
| `ota_abort()` | `esp_err_t` | 放弃更新,LED 灭,回到 IDLE |
| `ota_get_state()` | `ota_state_t` | 无 |
| `ota_get_bytes_written()` | `size_t` | 无 |
| `ota_get_firmware_size()` | `size_t` | 无 |
| `ota_get_running_partition()` | `const esp_partition_t *` | 无 |
| `ota_get_update_partition()` | `const esp_partition_t *` | 无 |
| `ota_set_progress_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_set_complete_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_set_state_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_set_error_callback(cb)` | `void` | 覆盖之前的回调 |
| `ota_clear_callbacks()` | `void` | 注销所有回调 |