固件 C2:OTA 双槽 + 失败回滚(已上板实测正常 / 哈希错 / 回滚三条路径)
- 分区重排(16 MB,nvs 0x9000 不动):ota_0 / ota_1 各 3 MB、otadata 0x610000、evlog 挪到 0x612000;
flash 大小改正为 16 MB;开 CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE
- ota.{h,c}(宿主机可测):URL / sha256 校验、判定(下载中 busy、待验证拒、同镜像不重下)、健康判据
- ota_esp.{h,c}:t_ota 任务经 HTTP 下载、边下边算 sha256、核 project_name 后切启动分区;
新固件 120 s 内连不上 broker 就回滚(t_link 判 + esp_timer 兜底);重启前等回应发出、事件落盘
- 新命令 ota = 13(两条链路都收,reboot 默认 false:复位 = 五台手机断电一次);status 加 ota 一块;
新事件 LOG_EV_OTA = 18;PROJECT_VER 取 PHONERAIL_FW_VERSION
- test_ota.c 13 例,全工程 442 例全绿;版本 0.1.0-c2
- 服务器:nginx 7765 只放行 /fw/<16 hex>.bin(不过门户,路由器转发已加);tools/ota_push.py 推送;
rack protocol.py 加事件 18 / 命令 13(已部署)
- 板 B:推送到自证健康 19 s;错哈希不切分区;删掉 broker 账号后 120 s 回滚,bad 报出被回滚的版本
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+189
@@ -0,0 +1,189 @@
|
||||
/* OTA 的平台无关部分(C2)。说明在 ota.h。 */
|
||||
#include "ota.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "hal_lock.h"
|
||||
|
||||
static ota_start_fn s_start;
|
||||
static ota_reboot_fn s_reboot;
|
||||
static void *s_ctx;
|
||||
static ota_status_t s_st;
|
||||
|
||||
void ota_init(ota_start_fn start, ota_reboot_fn reboot, void *ctx)
|
||||
{
|
||||
hal_crit_enter();
|
||||
memset(&s_st, 0, sizeof(s_st));
|
||||
s_start = start;
|
||||
s_reboot = reboot;
|
||||
s_ctx = ctx;
|
||||
hal_crit_exit();
|
||||
}
|
||||
|
||||
bool ota_url_ok(const char *url)
|
||||
{
|
||||
static const char k_scheme[] = "http://";
|
||||
if (url == NULL || strncmp(url, k_scheme, sizeof(k_scheme) - 1) != 0) {
|
||||
return false;
|
||||
}
|
||||
const size_t n = strlen(url);
|
||||
if (n <= sizeof(k_scheme) - 1 || n > OTA_URL_MAX) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const unsigned char ch = (unsigned char)url[i];
|
||||
if (ch <= 0x20 || ch >= 0x7F || ch == '"' || ch == '\\') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int hexval(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') {
|
||||
return c - '0';
|
||||
}
|
||||
if (c >= 'a' && c <= 'f') {
|
||||
return c - 'a' + 10;
|
||||
}
|
||||
return -1; /* 大写也算错:同一个哈希只许一种写法(ota.h) */
|
||||
}
|
||||
|
||||
bool ota_parse_sha256(const char *hex, uint8_t out[32])
|
||||
{
|
||||
if (hex == NULL || strlen(hex) != 64) {
|
||||
return false;
|
||||
}
|
||||
uint8_t tmp[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
const int hi = hexval(hex[2 * i]);
|
||||
const int lo = hexval(hex[2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) {
|
||||
return false;
|
||||
}
|
||||
tmp[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
memcpy(out, tmp, sizeof(tmp)); /* 半截错的不留在 out 里 */
|
||||
return true;
|
||||
}
|
||||
|
||||
void ota_sha256_hex(const uint8_t sha[32], char out[65])
|
||||
{
|
||||
static const char k[] = "0123456789abcdef";
|
||||
for (int i = 0; i < 32; i++) {
|
||||
out[2 * i] = k[sha[i] >> 4];
|
||||
out[2 * i + 1] = k[sha[i] & 0x0F];
|
||||
}
|
||||
out[64] = '\0';
|
||||
}
|
||||
|
||||
ota_action_t ota_plan(const ota_status_t *st, const ota_req_t *req)
|
||||
{
|
||||
if (st->phase == OTA_DL) {
|
||||
return OTA_ACT_BUSY;
|
||||
}
|
||||
if (st->phase == OTA_READY && memcmp(st->sha256, req->sha256, sizeof(st->sha256)) == 0) {
|
||||
return req->reboot ? OTA_ACT_REBOOT : OTA_ACT_NOTHING;
|
||||
}
|
||||
/* READY 了另一个镜像:重新下载会覆盖同一个备用槽,胶水里会先把启动分区切回当前的
|
||||
(esp_ota_begin 本身就会让那个槽失效)—— 这里照常开工。 */
|
||||
if (st->pending) {
|
||||
return OTA_ACT_PENDING;
|
||||
}
|
||||
return OTA_ACT_START;
|
||||
}
|
||||
|
||||
int ota_request(const ota_req_t *req)
|
||||
{
|
||||
ota_status_t st;
|
||||
ota_status_get(&st);
|
||||
const ota_action_t a = ota_plan(&st, req);
|
||||
switch (a) {
|
||||
case OTA_ACT_START:
|
||||
if (s_start == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* 先把状态置成 DL 再交任务:命令处理器下一条 status 立刻看得到「在下载」,
|
||||
也挡住了紧跟着的第二条 ota(它会拿到 BUSY)。 */
|
||||
st.phase = OTA_DL;
|
||||
st.err = OTA_E_NONE;
|
||||
st.got = 0;
|
||||
st.size = req->size;
|
||||
memcpy(st.sha256, req->sha256, sizeof(st.sha256));
|
||||
ota_status_set(&st);
|
||||
if (s_start(s_ctx, req) != HAL_OK) {
|
||||
st.phase = OTA_IDLE;
|
||||
ota_status_set(&st);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
case OTA_ACT_REBOOT:
|
||||
if (s_reboot == NULL) {
|
||||
return -1;
|
||||
}
|
||||
s_reboot(s_ctx);
|
||||
return 0;
|
||||
case OTA_ACT_NOTHING:
|
||||
return 0;
|
||||
default:
|
||||
return (int)a;
|
||||
}
|
||||
}
|
||||
|
||||
void ota_status_set(const ota_status_t *st)
|
||||
{
|
||||
hal_crit_enter();
|
||||
s_st = *st;
|
||||
hal_crit_exit();
|
||||
}
|
||||
|
||||
void ota_status_get(ota_status_t *st)
|
||||
{
|
||||
hal_crit_enter();
|
||||
*st = s_st;
|
||||
hal_crit_exit();
|
||||
}
|
||||
|
||||
const char *ota_phase_str(ota_phase_t p)
|
||||
{
|
||||
switch (p) {
|
||||
case OTA_IDLE:
|
||||
return "idle";
|
||||
case OTA_DL:
|
||||
return "dl";
|
||||
case OTA_READY:
|
||||
return "ready";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char *ota_err_str(ota_err_t e)
|
||||
{
|
||||
static const char *const k[OTA_E_COUNT] = {
|
||||
[OTA_E_NONE] = "", [OTA_E_HTTP] = "http", [OTA_E_SIZE] = "size",
|
||||
[OTA_E_HASH] = "hash", [OTA_E_FLASH] = "flash", [OTA_E_IMAGE] = "image",
|
||||
[OTA_E_BOOT] = "boot", [OTA_E_NOSLOT] = "noslot",
|
||||
};
|
||||
return ((unsigned)e < OTA_E_COUNT) ? k[e] : "?";
|
||||
}
|
||||
|
||||
ota_health_t ota_health(const ota_health_in_t *in)
|
||||
{
|
||||
if (!in->pending) {
|
||||
return OTA_H_NONE;
|
||||
}
|
||||
bool net_ok;
|
||||
if (in->mqtt_set) {
|
||||
net_ok = in->mqtt_up;
|
||||
} else if (in->wifi_set) {
|
||||
net_ok = in->wifi_up;
|
||||
} else {
|
||||
net_ok = true;
|
||||
}
|
||||
if (in->boot_done && net_ok) {
|
||||
return OTA_H_VALID;
|
||||
}
|
||||
return (in->now_ms >= OTA_HEALTH_MS) ? OTA_H_ROLLBACK : OTA_H_WAIT;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户