/* OTA 的 ESP 胶水(C2)。**只进 IDF 目标**;三条规矩(明文 HTTP + sha256、不签名、 120 s 自证健康)与「重启 = 五路全断一次」在 ota.h。 ---- 谁在哪个任务里跑 ------------------------------------------------------ | 事 | 任务 | |---|---| | `ota` 命令判定、把请求交出去 | 执行器任务(命令处理器 → `ota_request()` → `start()`,只入队) | | HTTP 下载、算 sha256、写 flash、切启动分区 | **本文件自己的 `t_ota` 任务**,优先级与 `t_link` 同(最低一档) | | 健康判据、到点重启 | `t_link`(`ota_esp_poll()`) | | 兜底回滚 | esp_timer(app_main 卡死、`t_link` 永远起不来时也能回滚) | 下载要几秒到几十秒,放进执行器任务就是让按键序列等 TCP;放进 `t_link` 就是让遥测停摆。 ---- 写 flash 对巡检的影响 ---------------------------------------------- 擦一个 4 KB 扇区要几十 ms,期间 cache 关掉、两个核上跑 flash 代码的任务都停。 `OTA_WITH_SEQUENTIAL_WRITES` 让擦除跟着写走(一次一个扇区),而不是在 esp_ota_begin 里 一口气擦 1 MB(那是秒级的停顿);每写一块再让出一个 tick。代价是 t_safety 偶尔晚一两拍, 上板看 status 的 `safety.skipped` 与巡检日志里的「最迟」。 ---- 重启前等一下 -------------------------------------------------------- 回应要先发出去(命令处理器调 reboot 回调时回应还没写),事件要先落盘(t_log 攒批最多 1 s)。 所以重启一律是「排到 `REBOOT_DELAY_MS` 之后」,由 `t_link` 执行。 */ #include #include "esp_app_desc.h" #include "esp_http_client.h" #include "esp_log.h" #include "esp_ota_ops.h" #include "esp_system.h" #include "esp_timer.h" #include "evlog.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" #include "hal_time.h" #include "mbedtls/sha256.h" #include "netcfg.h" #include "ota.h" #include "ota_esp.h" #include "tasks.h" static const char *TAG = "ota"; #define REBOOT_DELAY_MS (EVLOG_MAX_AGE_MS + 1000u) /* 回应发出去 + 事件落盘 */ #define HTTP_TIMEOUT_MS 15000 #define CHUNK 4096 #define STATUS_EVERY (64u * 1024u) /* 进度多久更新一次 status(每块都更新也行,只是没必要) */ #define BACKSTOP_EXTRA_MS 30000u /* 兜底定时器比判据晚这么久:正常路径先走 */ static QueueHandle_t s_q; static volatile bool s_boot_done; static volatile uint32_t s_reboot_at; /* 0 = 没排;否则 = 上电毫秒 */ static volatile bool s_rollback; /* 排的是回滚(标记无效再重启),不是普通重启 */ static bool s_pending; /* 只在 t_link 里读写(初始化之后) */ static esp_timer_handle_t s_backstop; static void status_edit(void (*fn)(ota_status_t *, uint32_t), uint32_t v) { ota_status_t st; ota_status_get(&st); fn(&st, v); ota_status_set(&st); } static void set_got(ota_status_t *st, uint32_t v) { st->got = v; } static void schedule_reboot(bool rollback) { s_rollback = rollback; uint32_t at = hal_time_now_ms() + REBOOT_DELAY_MS; s_reboot_at = (at == 0) ? 1 : at; } /* ota.h 的 reboot 回调:已就绪的同一镜像再来一条带 reboot 的命令。执行器任务里调。 */ static void cb_reboot(void *ctx) { (void)ctx; evlog_put(0, LOG_EV_OTA, OTA_EV_REBOOT, 0, 0); schedule_reboot(false); } /* ota.h 的 start 回调:只入队。执行器任务里调。 */ static hal_err_t cb_start(void *ctx, const ota_req_t *req) { (void)ctx; return (s_q != NULL && xQueueSend(s_q, req, 0) == pdTRUE) ? HAL_OK : HAL_ERR_STATE; } /* 下载 + 校验 + 写 + 切分区。返回 OTA_E_NONE = 已就绪。 */ static ota_err_t run(const ota_req_t *req) { static uint8_t buf[CHUNK]; /* 静态:只有本任务用 */ const esp_partition_t *running = esp_ota_get_running_partition(); const esp_partition_t *part = esp_ota_get_next_update_partition(NULL); if (part == NULL || part == running) { return OTA_E_NOSLOT; } if (req->size > part->size) { return OTA_E_SIZE; } /* 上一次 READY 的是另一个镜像:先把启动分区指回自己,下到一半断电也不会去启动半个镜像。 */ const esp_partition_t *boot = esp_ota_get_boot_partition(); if (boot != NULL && boot != running) { (void)esp_ota_set_boot_partition(running); } const esp_http_client_config_t hc = { .url = req->url, .timeout_ms = HTTP_TIMEOUT_MS, .keep_alive_enable = true, }; esp_http_client_handle_t cli = esp_http_client_init(&hc); if (cli == NULL) { return OTA_E_HTTP; } ota_err_t err = OTA_E_NONE; esp_ota_handle_t h = 0; bool begun = false; mbedtls_sha256_context sha; mbedtls_sha256_init(&sha); if (esp_http_client_open(cli, 0) != ESP_OK) { err = OTA_E_HTTP; goto out; } const int64_t len = esp_http_client_fetch_headers(cli); const int code = esp_http_client_get_status_code(cli); if (code != 200) { ESP_LOGE(TAG, "HTTP %d", code); err = OTA_E_HTTP; goto out; } if (len > 0 && len != (int64_t)req->size) { /* 0 / -1 = 没给 Content-Length(chunked),按字节数核 */ ESP_LOGE(TAG, "Content-Length %lld ≠ size %lu", (long long)len, (unsigned long)req->size); err = OTA_E_SIZE; goto out; } esp_err_t e = esp_ota_begin(part, OTA_WITH_SEQUENTIAL_WRITES, &h); if (e != ESP_OK) { ESP_LOGE(TAG, "esp_ota_begin: %s", esp_err_to_name(e)); /* 待验证的固件去覆盖备用槽,IDF 自己也会拒(ota.c 的 PENDING 已经先挡了一道) */ err = (e == ESP_ERR_OTA_ROLLBACK_INVALID_STATE) ? OTA_E_NOSLOT : OTA_E_FLASH; goto out; } begun = true; mbedtls_sha256_starts(&sha, 0); uint32_t got = 0; uint32_t shown = 0; while (got < req->size) { const uint32_t want = (req->size - got < CHUNK) ? (req->size - got) : CHUNK; const int n = esp_http_client_read(cli, (char *)buf, (int)want); if (n < 0) { err = OTA_E_HTTP; goto out; } if (n == 0) { break; /* 连接收了:字节数不够由下面判 */ } mbedtls_sha256_update(&sha, buf, (size_t)n); e = esp_ota_write(h, buf, (size_t)n); if (e != ESP_OK) { ESP_LOGE(TAG, "esp_ota_write: %s", esp_err_to_name(e)); err = OTA_E_FLASH; goto out; } got += (uint32_t)n; if (got - shown >= STATUS_EVERY) { shown = got; status_edit(set_got, got); } vTaskDelay(1); /* 把 cache 关掉的空档摊开,巡检不至于连着晚好几拍 */ } status_edit(set_got, got); if (got != req->size) { ESP_LOGE(TAG, "只收到 %lu / %lu 字节", (unsigned long)got, (unsigned long)req->size); err = OTA_E_SIZE; goto out; } uint8_t digest[32]; mbedtls_sha256_finish(&sha, digest); if (memcmp(digest, req->sha256, sizeof(digest)) != 0) { char hex[65]; ota_sha256_hex(digest, hex); ESP_LOGE(TAG, "sha256 对不上:算出来 %s", hex); err = OTA_E_HASH; goto out; } begun = false; /* esp_ota_end 无论成败都释放句柄 */ e = esp_ota_end(h); if (e != ESP_OK) { ESP_LOGE(TAG, "esp_ota_end: %s", esp_err_to_name(e)); err = (e == ESP_ERR_OTA_VALIDATE_FAILED) ? OTA_E_IMAGE : OTA_E_FLASH; goto out; } /* 哈希对得上只说明「是服务器想发的那个文件」;再核一眼是不是本工程的固件 —— 服务器那头传错一个 .bin(别的项目的),ESP 镜像校验照样过。 */ esp_app_desc_t d; if (esp_ota_get_partition_description(part, &d) != ESP_OK || strncmp(d.project_name, esp_app_get_description()->project_name, sizeof(d.project_name)) != 0) { ESP_LOGE(TAG, "不是本工程的固件(project_name「%.32s」)", d.project_name); err = OTA_E_IMAGE; goto out; } e = esp_ota_set_boot_partition(part); if (e != ESP_OK) { ESP_LOGE(TAG, "esp_ota_set_boot_partition: %s", esp_err_to_name(e)); err = OTA_E_BOOT; goto out; } ESP_LOGI(TAG, "新固件 %.32s 已就绪(%s @0x%lx),%s", d.version, part->label, (unsigned long)part->address, req->reboot ? "马上重启" : "下次复位生效"); out: if (begun) { esp_ota_abort(h); } mbedtls_sha256_free(&sha); esp_http_client_close(cli); esp_http_client_cleanup(cli); return err; } static void task_ota(void *arg) { (void)arg; ota_req_t req; for (;;) { if (xQueueReceive(s_q, &req, portMAX_DELAY) != pdTRUE) { continue; } ESP_LOGI(TAG, "开始下载 %s(%lu 字节)", req.url, (unsigned long)req.size); evlog_put(0, LOG_EV_OTA, OTA_EV_START, 0, (int32_t)req.size); const ota_err_t err = run(&req); ota_status_t st; ota_status_get(&st); st.err = err; if (err == OTA_E_NONE) { st.phase = OTA_READY; ota_status_set(&st); evlog_put(0, LOG_EV_OTA, OTA_EV_READY, 0, (int32_t)req.size); if (req.reboot) { evlog_put(0, LOG_EV_OTA, OTA_EV_REBOOT, 0, (int32_t)req.size); schedule_reboot(false); } } else { st.phase = OTA_IDLE; ota_status_set(&st); evlog_put(0, LOG_EV_OTA, OTA_EV_FAIL, 0, (int32_t)err); ESP_LOGE(TAG, "OTA 失败:%s", ota_err_str(err)); } } } /* 兜底:判据的正常路径在 t_link 里;app_main 卡死 / t_link 起不来时它来回滚。 esp_timer 任务里调,事件来不及落盘 —— 这条路本来就只在出大事时走。 */ static void backstop_cb(void *arg) { (void)arg; esp_ota_img_states_t s; if (esp_ota_get_state_partition(esp_ota_get_running_partition(), &s) == ESP_OK && s == ESP_OTA_IMG_PENDING_VERIFY) { ESP_LOGE(TAG, "兜底定时器:新固件 %u s 内没自证健康,回滚", (unsigned)((OTA_HEALTH_MS + BACKSTOP_EXTRA_MS) / 1000u)); esp_ota_mark_app_invalid_rollback_and_reboot(); } } hal_err_t ota_esp_init(void) { ota_init(NULL, NULL, NULL); ota_status_t st; ota_status_get(&st); const esp_partition_t *running = esp_ota_get_running_partition(); esp_ota_img_states_t state; if (running != NULL && esp_ota_get_state_partition(running, &state) == ESP_OK) { s_pending = (state == ESP_OTA_IMG_PENDING_VERIFY); } st.pending = s_pending; const esp_partition_t *bad = esp_ota_get_last_invalid_partition(); esp_app_desc_t d; if (bad != NULL && esp_ota_get_partition_description(bad, &d) == ESP_OK) { snprintf(st.bad, sizeof(st.bad), "%.*s", (int)sizeof(st.bad) - 1, d.version); ESP_LOGW(TAG, "%s 里的固件 %s 被判过无效(回滚过)", bad->label, st.bad); } ota_status_set(&st); const esp_partition_t *next = esp_ota_get_next_update_partition(NULL); ESP_LOGI(TAG, "运行在 %s @0x%lx%s;备用槽 %s", running ? running->label : "?", running ? (unsigned long)running->address : 0ul, s_pending ? "(⚠️ 待验证:" "120 s 内要连上 broker,否则回滚)" : "", next ? next->label : "没有(分区表不是双槽)"); if (next != NULL && next->size != OTA_SLOT_BYTES) { ESP_LOGE(TAG, "备用槽 %lu 字节 ≠ OTA_SLOT_BYTES %lu:ota.h 与 partitions.csv 没对上", (unsigned long)next->size, (unsigned long)OTA_SLOT_BYTES); } if (s_pending) { const esp_timer_create_args_t ta = { .callback = backstop_cb, .name = "ota_backstop" }; if (esp_timer_create(&ta, &s_backstop) == ESP_OK) { esp_timer_start_once(s_backstop, (uint64_t)(OTA_HEALTH_MS + BACKSTOP_EXTRA_MS) * 1000u); } } if (next == NULL) { return HAL_ERR_STATE; /* 命令回 state:ota_init 没登记回调 */ } s_q = xQueueCreate(1, sizeof(ota_req_t)); if (s_q == NULL || xTaskCreate(task_ota, "t_ota", 6144, NULL, task_desc(TASK_LINK)->prio, NULL) != pdPASS) { return HAL_ERR_STATE; } ota_init(cb_start, cb_reboot, NULL); ota_status_set(&st); /* ota_init 清了状态,把 pending / bad 放回去 */ return HAL_OK; } void ota_esp_boot_done(void) { s_boot_done = true; } void ota_esp_poll(void) { const uint32_t now = hal_time_now_ms(); if (s_pending) { netcfg_t cfg; net_status_t ns; netcfg_get(&cfg); net_status_get(&ns); const ota_health_in_t in = { .pending = true, .now_ms = now, .boot_done = s_boot_done, .wifi_set = cfg.ssid[0] != '\0', .wifi_up = ns.wifi == NET_UP, .mqtt_set = cfg.uri[0] != '\0', .mqtt_up = ns.mqtt == NET_UP, }; const ota_health_t h = ota_health(&in); if (h == OTA_H_VALID) { const esp_err_t e = esp_ota_mark_app_valid_cancel_rollback(); s_pending = false; if (s_backstop != NULL) { esp_timer_stop(s_backstop); } ota_status_t st; ota_status_get(&st); st.pending = false; ota_status_set(&st); evlog_put(0, LOG_EV_OTA, OTA_EV_VALID, 0, (int32_t)e); ESP_LOGI(TAG, "新固件 %s 自证健康(%lu ms),取消回滚:%s", esp_app_get_description()->version, (unsigned long)now, esp_err_to_name(e)); } else if (h == OTA_H_ROLLBACK && s_reboot_at == 0) { s_pending = false; /* 不再判;下面到点回滚 */ evlog_put(0, LOG_EV_OTA, OTA_EV_ROLLBACK, 0, 0); ESP_LOGE(TAG, "新固件 %u s 内没自证健康(WiFi %s / MQTT %s),回滚", (unsigned)(OTA_HEALTH_MS / 1000u), net_state_str(ns.wifi), net_state_str(ns.mqtt)); schedule_reboot(true); } } if (s_reboot_at != 0 && (int32_t)(now - s_reboot_at) >= 0) { if (s_rollback) { esp_ota_mark_app_invalid_rollback_and_reboot(); /* 成功就不返回 */ ESP_LOGE(TAG, "回滚失败(没有可回的旧版?),就地重启"); } ESP_LOGW(TAG, "为换固件重启:五路全断一次,起来后按意图恢复"); esp_restart(); } }