跳到主要内容

固件 OTA 升级

本指南说明如何使用 agentic-kit 的 iot_ota API 实现设备固件 OTA(Over-The-Air)升级。

SDK 只提供云端协议原语——版本上报、升级查询、状态回报;固件的下载与烧写由应用负责(例如 ESP-IDF 的 esp_ota_* 或厂商自有的 bootloader API)。完整示例见 examples/esp-idf/ota-demo

工作原理

设备启动 ──> iot_client_init (自动上报当前版本)

v
iot_ota_check_upgrade() ──> 云端返回升级信息 (URL / 版本 / 大小 / 哈希)

有升级?
/ \
否 是
│ │
保持运行 iot_ota_report_status(UPGRADING)

v
下载固件 (info.url) + 烧写 flash ← 应用实现

┌─────┴─────┐
成功 失败
│ │
report_status(FINI) report_status(EXEC)
│ │
重启生效 重试 / 放弃

三个 API

API云端接口用途
iot_ota_report_versiontuya.device.versions.update (v4.1)上报当前固件版本(iot_client_init 会用 iot_client_config_t.sw_ver 自动调用,NULL 时用 SDK 默认 IOT_SDK_SW_VER
iot_ota_check_upgradetuya.device.upgrade.get (v4.4)查询是否有待升级固件,返回 URL / 版本 / 大小 / 哈希(云端与已上报的版本比较,不再传版本号)
iot_ota_report_statustuya.device.upgrade.status.update (v4.1)回报升级生命周期状态

iot_ota_check_upgrade 返回的升级信息

typedef struct {
bool has_upgrade; // 云端是否有升级
char *version; // 目标版本号
char *url; // 固件下载 URL(优先 cdnUrl,回退 httpsUrl)
long file_size; // 固件大小(字节)
int channel; // 固件通道(0 = 主 MCU)
char *md5; // MD5 校验(可能为 NULL)
char *hmac; // HMAC 校验(可能为 NULL)
} iot_ota_upgrade_info_t;

字段为堆分配,用完必须调 iot_ota_upgrade_info_free() 释放。

升级状态枚举

typedef enum {
OTA_STATUS_IDLE = 0, // 空闲
OTA_STATUS_UPGRADING = 1, // 升级中(下载/烧写前)
OTA_STATUS_UPGRAD_FINI = 2, // 升级成功(通常重启后回报)
OTA_STATUS_UPGRD_EXEC = 3, // 升级失败
OTA_STATUS_UPGRD_ABORT = 4, // 升级中止
} iot_ota_status_t;

完整示例(ESP-IDF)

以下步骤摘自 examples/esp-idf/ota-demo/main/main.c,使用 esp_http_client 下载、esp_ota_* 烧写。

1. 分区表

OTA 需要两个 app 分区(ota_0 / ota_1)和一个 otadata 分区。demo 使用的 partitions.csv(16MB flash,每个 app 分区 4MB,可容纳约 4MB 的固件):

# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
otadata, data, ota, 0x10000, 0x2000,
ota_0, app, ota_0, 0x20000, 4M,
ota_1, app, ota_1, , 4M,

2. sdkconfig 关键项

# 给 TLS + HTTP + esp_ota 留够栈
CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
# 16MB flash(容纳双 4MB OTA 分区)
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
# 自定义分区表
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# 启用公共 CA 证书包(cdnUrl 下载需要)
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y

3. 初始化与升级查询

#include "iot_client.h"
#include "iot_ota.h"
#include "esp_app_desc.h"

const esp_app_desc_t *desc = esp_app_get_description();

iot_client_config_t iot_cfg = {
.devid = DEFAULT_DEVID,
.secret_key = DEFAULT_SECRET_KEY,
.local_key = DEFAULT_LOCAL_KEY,
.region = DEFAULT_REGION,
.env = DEFAULT_ENV,
/* mqtt_auto_connect = false: 只用 ATOP HTTP,不连 MQTT */
.mqtt_auto_connect = false,
/* 应用固件版本:init 时自动上报,供云端 OTA 比较(NULL 用 SDK 默认) */
.sw_ver = desc->version,
};
iot_client_t *iot = iot_client_init(&iot_cfg);

/* 查询升级(云端与 init 时上报的 sw_ver 比较,无需再传版本号) */
iot_ota_upgrade_info_t info = {0};
int rc = iot_ota_check_upgrade(iot, 0, &info);
if (rc == OPRT_OK && info.has_upgrade) {
ESP_LOGI(TAG, "upgrade -> %s url=%s size=%ld",
info.version, info.url, info.file_size);
}

4. 上报状态、下载、烧写

/* 下载前上报"升级中" */
iot_ota_report_status(iot, 0, OTA_STATUS_UPGRADING);

/* 用 esp_http_client 下载 info.url,逐块 esp_ota_write */
esp_err_t err = download_and_flash(info.url);
iot_ota_upgrade_info_free(iot, &info);

if (err != ESP_OK) {
iot_ota_report_status(iot, 0, OTA_STATUS_UPGRD_EXEC);
return;
}

/* 成功后上报"完成",然后重启 */
iot_ota_report_status(iot, 0, OTA_STATUS_UPGRAD_FINI);
esp_restart();

download_and_flash 的核心流程(完整代码见 demo):

static esp_err_t download_and_flash(const char *url)
{
const esp_partition_t *part = esp_ota_get_next_update_partition(NULL);

esp_http_client_config_t http_cfg = {
.url = url,
.timeout_ms = 30000,
.buffer_size = 4096,
.crt_bundle_attach = esp_crt_bundle_attach, /* 公共 CA 包 */
};
/* ... open / fetch headers / 检查 200 ... */

esp_ota_handle_t handle;
esp_ota_begin(part, OTA_WITH_SEQUENTIAL_WRITES, &handle);

while ((n = esp_http_client_read(client, buf, sizeof(buf))) > 0) {
esp_ota_write(handle, buf, n); /* 逐块写入 */
}

esp_ota_end(handle);
esp_ota_set_boot_partition(part); /* 切换启动分区 */
return ESP_OK;
}

5. 首次启动验证(防回滚)

重启后,新的固件应当把自己标记为有效,否则 ESP-IDF 会在若干次重启后回滚到旧分区:

static void mark_current_valid(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t state;
if (esp_ota_get_state_partition(running, &state) == ESP_OK
&& state == ESP_OTA_IMG_PENDING_VERIFY) {
esp_ota_mark_app_valid_cancel_rollback();
}
}

app_main 开头调用一次即可。

构建与烧写

cd examples/esp-idf/ota-demo
idf set-target esp32s3
idf build
idf flash monitor

首次烧写会写到 ota_0;后续 OTA 写入 ota_1 并切换启动。

注意事项

  • SDK 不下载/不烧写——iot_ota 只负责云端协议;下载校验、分区管理、防回滚全部由应用实现。
  • 栈要足够大——TLS 握手 + HTTP 缓冲 + esp_ota_write 需要较大栈空间(demo 用 16KB)。
  • 回报时机——UPGRADING 在下载前、FINI 在重启前、EXEC 在失败时;漏报会导致云端升级面板状态不准。
  • MD5/HMAC 可选校验——info.md5 / info.hmac 可能为 NULL;若存在,建议在 esp_ota_end 后做一次校验再切分区。