feat(ota): 统一BLE/WiFi OTA协调器接口并优化并发控制

- 添加 beginEsp32OtaWithRetry 统一重试逻辑,防止锁残留导致的并发问题
- 实现 finalizeForReboot 和 restartEsp32 统一封装收尾重启流程
- 修复BLE/WiFi双通道并发OTA的安全隐患,增强系统状态检查

refactor(ota): 优化SHA计算内存管理

- 使用静态缓冲区替代动态分配,减少内存碎片
- 重构OTA协调器状态机,增强错误处理机制

perf(monitor): 增强堆内存监控功能

- 添加最大空闲块大小监控,提供更详细的内存状态信息
- 更新健康监测算法,提高内存异常检测准确性
This commit is contained in:
小龙虾
2026-06-19 03:02:33 +08:00
parent 41fee936df
commit 7feef70750
13 changed files with 559 additions and 233 deletions
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 26
#define FW_VERSION_STRING "0.3.26"
#define FW_VERSION_PATCH 27
#define FW_VERSION_STRING "0.3.27"
// 编译时间
#define BUILD_TIME "2026-06-18 01:32:44"
#define BUILD_TIME "2026-06-19 02:00:09"
// Git 信息
#define GIT_HASH "2586582"
#define GIT_HASH "41fee93"
+4 -1
View File
@@ -16,6 +16,7 @@
#include "esp_err.h"
#include "driver/temperature_sensor.h"
#include "esp_heap_caps.h"
#include "health_monitor.h"
#include "debug_log.h"
#include "server/wifi_ota.h"
@@ -197,7 +198,9 @@ void HealthMonitor::taskEntry(void *pvParameters)
while (1) {
uint32_t free_heap = esp_get_free_heap_size();
if (free_heap < HEAP_WARNING_MIN) {
DEBUG_WARN("MON", "堆内存不足: %lu < %lu", free_heap, HEAP_WARNING_MIN);
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
DEBUG_WARN("MON", "堆内存不足: free=%lu min=%lu largest_blk=%lu",
free_heap, esp_get_minimum_free_heap_size(), largest_block);
}
health_state_t cur = self.getState();
if (cur >= SYS_CRITICAL) {
+4 -10
View File
@@ -171,30 +171,24 @@ esp_err_t Esp32OtaCore::computeShaFromPartition()
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
uint8_t *read_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!read_buf) {
DEBUG_ERR(TAG, "SHA 计算: malloc 失败");
mbedtls_sha256_free(&ctx);
return ESP_ERR_NO_MEM;
}
// ★ static 缓冲区:OTA 操作由 OtaCoordinator 互斥串行化,无并发风险
static uint8_t s_sha_read_buf[OTA_BLOCK_SIZE];
size_t offset = 0;
while (offset < m_received) {
size_t to_read = m_received - offset;
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
esp_err_t rd_err = esp_partition_read(m_partition, offset, read_buf, to_read);
esp_err_t rd_err = esp_partition_read(m_partition, offset, s_sha_read_buf, to_read);
if (rd_err != ESP_OK) {
DEBUG_ERR(TAG, "SHA 计算: 分区读失败 @offset %u: %s",
(unsigned)offset, esp_err_to_name(rd_err));
free(read_buf);
mbedtls_sha256_free(&ctx);
return rd_err;
}
mbedtls_sha256_update(&ctx, read_buf, to_read);
mbedtls_sha256_update(&ctx, s_sha_read_buf, to_read);
offset += to_read;
}
free(read_buf);
mbedtls_sha256_finish(&ctx, m_sha256);
mbedtls_sha256_free(&ctx);
m_shaComputed = true;
+71 -4
View File
@@ -8,6 +8,9 @@
#include "esp32_ota_core.h"
#include "relay/ota_relay.h"
#include "monitor/debug_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define TAG "OTA_COORD"
@@ -283,11 +286,18 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
// ★ 卡死自愈检查
if (h) watchdogCheckLocked();
// 系统忙检查
// 系统忙检查:GD32 传输/中继进行中,或另一通道的 ESP32 OTA 进行中,均拒绝。
// ★ 必须显式拒绝 ESP32_RECEIVING/FINALIZING/REBOOTING:
// 若放行,本调用会落到 Esp32OtaCore::begin 去抢全局互斥失败而返回 INVALID_STATE,
// 随后 beginEsp32OtaWithRetry 的 forceCleanup 会把另一通道的活跃会话
// (esp_ota_abort + 释放互斥)误中止 → 两通道并发写同一 OTA 分区。
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING)) {
DEBUG_WARN(TAG, "GD32 正在 %s,拒绝 ESP32 OTA",
m_sysState == OTA_SYS_GD32_STORING ? "存储" : "中继");
m_sysState == OTA_SYS_GD32_RELAYING ||
m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
(int)m_sysState, getSystemStateString());
unlock(h);
return ESP_ERR_INVALID_STATE;
}
@@ -309,6 +319,29 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
return err;
}
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
{
esp_err_t err = beginEsp32Ota(total_size);
if (err == ESP_ERR_INVALID_STATE) {
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
// 这种情况只可能是"锁残留"——前一次 OTA 异常退出没释放全局互斥,
// 而协调器状态已回 IDLE。
// 若系统正处于任何活跃 OTA(ESP32/GD32),绝不强制清理——那会
// 把另一通道正在进行的 esp_ota_handle 中止、释放互斥,导致
// 两通道并发写同一 OTA 分区(之前 BLE 与 WiFi 可同时 OTA 的根因)。
if (!isIdle()) {
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
(int)getSystemState());
return err;
}
// 协调器空闲但 Esp32OtaCore.begin 仍被拒 → 锁残留,强制清理后重试
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝但系统空闲,判定为锁残留,强制清理状态机后重试");
Esp32OtaCore::instance().forceCleanup();
err = beginEsp32Ota(total_size);
}
return err;
}
void OtaCoordinator::notifyEsp32ReceiveComplete()
{
bool h = lock();
@@ -339,6 +372,40 @@ void OtaCoordinator::notifyEsp32Rebooting()
unlock(h);
}
// ============================================================
// ESP32 OTA 收尾+重启序列(BLE/WiFi 共用封装)
// 关键:finalize() 内部已 end+set_boot+释放锁,reboot=false 返回后
// m_active 仍为 true、m_ended 为 true,调用方可在重启前自由查询状态。
// ============================================================
Esp32FinalizeResult OtaCoordinator::finalizeForReboot(const char *frontend_sha_hex)
{
Esp32FinalizeResult r = {};
notifyEsp32ReceiveComplete();
// reboot=false:让 core.finalize 真正返回,由调用方上报 device_sha 后再调 restartEsp32
r.err = Esp32OtaCore::instance().finalize(frontend_sha_hex,
/*reboot=*/false,
r.device_sha_hex);
if (r.err == ESP_OK) {
notifyEsp32FinalizeComplete();
}
return r;
}
void OtaCoordinator::restartEsp32()
{
notifyEsp32Rebooting();
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
// 不会返回
}
int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)
{
if (err == ESP_OK) return 0; // 成功
if (err == ESP_ERR_INVALID_VERSION) return 1; // SHA 不匹配
return 2; // 其他失败
}
// ============================================================
// 中止
// ============================================================
+42
View File
@@ -35,6 +35,15 @@ typedef enum {
OTA_SYS_ERROR // 错误状态
} ota_system_state_t;
/**
* @brief ESP32 OTA 收尾结果(由 finalizeForReboot 返回)。
* err 为 ESP_OK 时 device_sha_hex 有效;SHA 不匹配时也填充 device_sha_hex 供前端核对。
*/
typedef struct {
esp_err_t err; ///< ESP_OK / ESP_ERR_INVALID_VERSION(SHA不匹配) / 其他
char device_sha_hex[65]; ///< 设备端 SHA hex(成功或 SHA 不匹配时填充)
} Esp32FinalizeResult;
/**
* @brief 全局 OTA 协调器(单例)
*
@@ -101,6 +110,14 @@ public:
*/
esp_err_t beginEsp32Ota(uint32_t total_size);
/**
* @brief 发起 ESP32 OTA,若被 ESP_ERR_INVALID_STATE 拒绝则 forceCleanup 后重试一次。
* 封装 BLE/WiFi 两端完全相同的重试模式,调用方只需调一次。
* @param total_size 固件总字节数。
* @return ESP_OK 成功;其他 = 协调器拒绝或 Esp32OtaCore::begin 失败。
*/
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size);
/**
* @brief ESP32 OTA 固件接收完毕,准备收尾。
* 更新系统状态为 OTA_SYS_ESP32_FINALIZING。
@@ -117,6 +134,23 @@ public:
*/
void notifyEsp32Rebooting();
/**
* @brief ESP32 OTA 收尾(统一封装 BLE/WiFi 两端重复的收尾序列)。
* 步骤:notifyEsp32ReceiveComplete → Esp32OtaCore::finalize(reboot=false)
* → 成功则 notifyEsp32FinalizeComplete。
* 不重启;调用方负责把 device_sha_hex 上报前端后再调 restartEsp32()。
* @param frontend_sha_hex 前端 64 字符 hex SHA-256,nullptr 表示跳过比对。
* @return Esp32FinalizeResult,err 为 ESP_OK/ESP_ERR_INVALID_VERSION/其他。
*/
Esp32FinalizeResult finalizeForReboot(const char *frontend_sha_hex);
/**
* @brief 重启 ESP32(统一封装 BLE/WiFi 两端重复的重启序列)。
* 步骤:notifyEsp32Rebooting → vTaskDelay(500ms) → esp_restart()。
* 应在 finalizeForReboot 成功并完成前端上报后调用。不会返回。
*/
void restartEsp32();
// ── 中止 ──
/**
* @brief 中止当前 OTA 操作(GD32 或 ESP32)。
@@ -167,6 +201,14 @@ public:
*/
void reset();
// ── ESP32 OTA finalize 错误分类(BLE/WiFi 共用)──
/**
* @brief 把 finalize 返回的 esp_err_t 映射为前端可读的事件码。
* 统一 BLE/WiFi 两端语义,便于日志/状态一致。
* @return 0=成功;1=SHA 不匹配;2=其他失败。
*/
static int esp32FinalizeErrCategory(esp_err_t err);
private:
OtaCoordinator() = default;
~OtaCoordinator() = default;
+29 -2
View File
@@ -835,6 +835,28 @@ esp_err_t OtaRelay::metaVerifyChecksum()
return ESP_OK;
}
esp_err_t OtaRelay::verifyFrontendSha(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32])
{
if (!frontend_sha32) return ESP_ERR_INVALID_ARG;
// 1) 计算设备端 SHA 并写入 meta.checksum(metaFinalizeChecksum 内部已校验固件完整性)
esp_err_t err = metaFinalizeChecksum();
if (err != ESP_OK) return err;
// 2) 读回 meta 拿到 checksum,回传给调用方
ota_meta_t meta;
err = metaLoad(&meta);
if (err != ESP_OK) return err;
if (out_device_sha) memcpy(out_device_sha, meta.checksum, 32);
// 3) 比对前端 SHA 与设备端 SHA
if (memcmp(meta.checksum, frontend_sha32, 32) != 0) {
return ESP_ERR_INVALID_CRC;
}
return ESP_OK;
}
esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len)
{
if (!instance().m_gd32Partition) {
@@ -1167,8 +1189,13 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
result = OTA_RESULT_ERR_TIMEOUT;
break;
if (++retry_count >= 10U) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_TIMEOUT;
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
continue; // 重发本包
}
uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? 0xF0F0F0F0U : (seq + 1U);
+10
View File
@@ -180,6 +180,16 @@ public:
* @brief 校验固件完整性(SHA-256).
*/
static esp_err_t metaVerifyChecksum();
/**
* @brief 校验前端提供的 SHA-256 是否与设备端计算的一致。
* BLE/WiFi 传输层共用:内部完成 metaFinalizeChecksum → metaLoad → memcmp。
* 无论是否匹配,都会通过 out_device_sha 回传设备端 SHA(便于前端核对)。
* @param frontend_sha32 前端 32 字节原始 SHA(非 hex)。
* @param out_device_sha 可为 nullptr;非空时输出设备端 32 字节 SHA。
* @return ESP_OK 匹配;ESP_ERR_INVALID_CRC 不匹配;其他 = 元数据/计算失败。
*/
static esp_err_t verifyFrontendSha(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32]);
// ── 公开状态成员 ──
static relay_state_t relayState;
+61 -81
View File
@@ -127,15 +127,13 @@ static void ble_esp32_restart_task(void *pvParameters)
(void)pvParameters;
DEBUG_INFO(TAG, "[RESTART] 推送 FINISH_REBOOT_NOW 后 500ms 重启");
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
OtaCoordinator::instance().restartEsp32(); // 内部 notify + delay + esp_restart,不返回
}
/**
* @brief 异步完成 ESP32 OTA 收尾并触发重启。
* @param pvParameters BleEsp32FinishCtx(堆分配,任务负责释放)。
*/
* @param pvParameters BleEsp32FinishCtx(堆分配,任务负责释放)。
*/
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
{
BleEsp32FinishCtx *ctx = (BleEsp32FinishCtx *)pvParameters;
@@ -166,26 +164,17 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节", (unsigned)core.getReceived());
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
// 关键:reboot=false,让 finalize 真正返回、拿到 device_sha_hex,
// 再依次推送 SHA 通知 + FINISH_DONE + FINISH_REBOOT_NOW,
// 最后延迟重启。若 reboot=true,core 内部直接 esp_restart(),
// 下面所有 notifyResponse / sendFinishEvent 都不会执行,
// 前端也就拿不到设备端 SHA 与"收尾完成/即将重启"事件。
char device_sha_hex[65] = {0};
esp_err_t err = core.finalize(frontend_sha_hex,
/*reboot=*/false,
device_sha_hex);
// ★ 收尾序列(notify + finalize)由协调器统一封装,reboot=false 让 device_sha 可被读取
Esp32FinalizeResult fr = OtaCoordinator::instance().finalizeForReboot(frontend_sha_hex);
if (err == ESP_OK) {
OtaCoordinator::instance().notifyEsp32FinalizeComplete();
if (fr.err == ESP_OK) {
// 1) 把设备端 SHA 通过 SHA256 通知推给前端
if (device_sha_hex[0] != '\0') {
if (fr.device_sha_hex[0] != '\0') {
uint8_t device_sha[32] = {0};
for (int i = 0; i < 32; ++i) {
unsigned int byte = 0;
sscanf(&device_sha_hex[i * 2], "%02x", &byte);
sscanf(&fr.device_sha_hex[i * 2], "%02x", &byte);
device_sha[i] = (uint8_t)byte;
}
sendSha256Notify(device_sha, session_id);
@@ -193,24 +182,22 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
// 2) 等待 SHA 通知从 NimBLE host 队列送出
vTaskDelay(pdMS_TO_TICKS(150));
// 3) 推送"OTA 收尾完成"与"即将重启"事件
// 3) 推送"OTA 收尾完成"事件
sendFinishEvent(BLE_OTA_FINISH_DONE, packBleFinishValue(session_id, 0));
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, packBleFinishValue(session_id, 0));
// 4) 通知全部发出后延迟 500ms 重启
DEBUG_INFO(TAG, "[FINISH] 通知已发送,500ms 后重启...");
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
// 不会返回
} else if (err == ESP_ERR_INVALID_VERSION) {
// 4) 通知全部发出后由协调器统一重启(notify + delay + esp_restart)
DEBUG_INFO(TAG, "[FINISH] 通知已发送,重启...");
if (ctx) free(ctx);
OtaCoordinator::instance().restartEsp32(); // 不返回
} else if (fr.err == ESP_ERR_INVALID_VERSION) {
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, packBleFinishValue(session_id, 0));
BleOta::instance().m_esp32RebootPending = false;
// ★ finalize 内部已 abort 并释放 mutex,这里释放 reorder buffer
esp32_reorder_free();
} else {
DEBUG_ERR(TAG, "[FINISH] OTA 收尾失败: %s", esp_err_to_name(err));
DEBUG_ERR(TAG, "[FINISH] OTA 收尾失败: %s", esp_err_to_name(fr.err));
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
packBleFinishValue(session_id, 1));
BleOta::instance().m_esp32RebootPending = false;
@@ -223,41 +210,39 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
}
// ============================================================
// WiFi 共存优化:BLE OTA 传输期间降低 WiFi 射频占用
// WiFi 共存策略:BLE OTA 传输期间的处理
//
// 历史:曾在此处把 WiFi 切到 WIFI_PS_MAX_MODEM 以把空中时间让给 BLE。
// 但 MAX_MODEM 下 STA 监听间隔被拉到极大,HTTP server 的 TCP 握手/数据包
// 会大量丢失或延迟到超时 → BLE OTA 期间任何 WiFi 请求(包括互斥拒绝应返回的
// 403)都到不了设备,前端 XHR 长时间无响应,表现为"Wifi 上传既不成功也不报错"。
//
// 现策略:不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
// 协调器照常以 403 拒绝并发 OTA。ESP32-C3 coex 默认调度已能保证 BLE OTA 可用。
// 仍保留 setBleOtaActive() 暂停 RSSI 轮询(避免 get_ap_info 的射频活动干扰 BLE)。
// ============================================================
static bool s_wifi_ps_boosted = false;
/**
* @brief BLE OTA 开始时,将 WiFi 切换到最大省电模式,减少射频抢占。
* WiFi STA 仍保持连接,但信标监听间隔大幅拉长,BLE 获得更多空中时间。
* @brief BLE OTA 开始时的 WiFi 共存处理(暂停 RSSI 轮询,不切省电模式)。
*/
static void ble_ota_boost_wifi_ps()
{
if (s_wifi_ps_boosted) return;
esp_err_t err = esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
if (err == ESP_OK) {
s_wifi_ps_boosted = true;
WifiOta::instance().setBleOtaActive(true); // ★ 暂停 WiFi RSSI 轮询
DEBUG_INFO(TAG, "WiFi 已切换到 MAX_MODEM 省电模式(BLE OTA 传输优化)");
} else {
DEBUG_WARN(TAG, "WiFi 省电模式切换失败: %s", esp_err_to_name(err));
}
s_wifi_ps_boosted = true;
WifiOta::instance().setBleOtaActive(true); // ★ 暂停 WiFi RSSI 轮询
DEBUG_INFO(TAG, "BLE OTA 进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
}
/**
* @brief BLE OTA 结束/中止时,恢复 WiFi 省电策略为 WIFI_PS_NONE。
* @brief BLE OTA 结束/中止时恢复 WiFi RSSI 轮询。
*/
static void ble_ota_restore_wifi_ps()
{
if (!s_wifi_ps_boosted) return;
esp_err_t err = esp_wifi_set_ps(WIFI_PS_NONE);
if (err == ESP_OK) {
s_wifi_ps_boosted = false;
WifiOta::instance().setBleOtaActive(false); // ★ 恢复 WiFi RSSI 轮询
DEBUG_INFO(TAG, "WiFi 已恢复 WIFI_PS_NONE 模式");
} else {
DEBUG_WARN(TAG, "WiFi 省电模式恢复失败: %s", esp_err_to_name(err));
}
s_wifi_ps_boosted = false;
WifiOta::instance().setBleOtaActive(false); // ★ 恢复 WiFi RSSI 轮询
DEBUG_INFO(TAG, "BLE OTA 结束:恢复 WiFi RSSI 轮询");
}
// ============================================================
@@ -558,14 +543,8 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
*/
static esp_err_t ble_esp32_ota_begin(size_t total_size)
{
// ★ 通过全局协调器发起 ESP32 OTA
esp_err_t err = OtaCoordinator::instance().beginEsp32Ota(total_size);
if (err == ESP_ERR_INVALID_STATE) {
// 协调器拒绝 → 尝试强制清理后重试
DEBUG_WARN(TAG, "ESP32 OTA 被协调器拒绝,强制清理状态机后重试");
Esp32OtaCore::instance().forceCleanup();
err = OtaCoordinator::instance().beginEsp32Ota(total_size);
}
// ★ 通过协调器发起(内部已含 forceCleanup 重试)
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
if (err != ESP_OK) {
return err;
}
@@ -765,32 +744,24 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
char device_sha_hex[65] = {0};
shaBytesToHexUpper(frontend_sha, frontend_sha_hex);
err = OtaRelay::metaFinalizeChecksum();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 缓存 SHA-256 计算失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, 3));
return 0;
}
err = OtaRelay::metaLoad(&meta);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 SHA-256 元数据读取失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, 3));
return 0;
}
shaBytesToHexUpper(meta.checksum, device_sha_hex);
sendSha256Notify(meta.checksum, session_id);
// ★ 统一走共享 helper:计算设备端 SHA → 比对前端 SHA
uint8_t device_sha[32] = {0};
err = OtaRelay::verifyFrontendSha(frontend_sha, device_sha);
shaBytesToHexUpper(device_sha, device_sha_hex);
sendSha256Notify(device_sha, session_id);
DEBUG_INFO(TAG, "GD32 前端 SHA-256: %s, session=%u", frontend_sha_hex, session_id);
DEBUG_INFO(TAG, "GD32 缓存 SHA-256: %s", device_sha_hex);
if (memcmp(meta.checksum, frontend_sha, 32) != 0) {
if (err == ESP_ERR_INVALID_CRC) {
DEBUG_ERR(TAG, "GD32 缓存 SHA-256 与前端文件不匹配,拒绝触发 SPI 中继");
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
packBleFinishValue(session_id, 0));
return 0;
} else if (err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 SHA-256 计算/读取失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, 3));
return 0;
}
DEBUG_INFO(TAG, "GD32 固件就绪且 SHA-256 匹配 (%" PRIu32 " / %" PRIu32 " 字节),通过协调器触发中继",
@@ -1032,13 +1003,22 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return;
}
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
// ★ 幂等保护:若已有活跃 ESP32 OTA 会话,说明这是客户端因 ACK 超时
// 重传的 seq=0(同一会话续传)。若再次走 begin,会被协调器以
// "OTA 已 ESP32_RECEIVING" 拒绝 → 误报"被其他通道占用"并中止整条
// OTA 流程。此处跳过 begin 复用现有会话,后续 reorder buffer 会
// 按 seq 去重(seq=0 < expected 时作为重复包丢弃)。
if (Esp32OtaCore::instance().isActive()) {
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin");
} else {
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
}
return;
}
return;
}
}
+96 -67
View File
@@ -48,6 +48,9 @@ WifiOta& WifiOta::instance()
static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U;
// ★ OTA 接收共享缓冲区:由 OtaCoordinator 互斥串行化,无并发风险,避免频繁 malloc/free 导致堆碎片化
static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];
/**
* @brief 按指定对齐值向上取整。
* @param value 待对齐的值。
@@ -509,47 +512,39 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
return ESP_OK;
}
// ── 完整模式:finalize 不重启,先把含设备端 SHA 的结果返回给前端 ──
// 关键:reboot=false,让 core.finalize 真正返回,才能拿到 device_sha_hex
// 并把响应发回前端;待响应发完后再延迟重启。
// 若 reboot=true,core 内部直接 esp_restart(),HTTP 响应永远发不出去,
// 前端会得到 "Failed to fetch" 而错过核对结果。
// ── 完整模式:收尾+重启序列由协调器统一封装 ──
// 关键:reboot=false 行为由 finalizeForReboot 内部封装,
// 响应发回前端后再由 restartEsp32() 统一通知+delay+esp_restart。
DEBUG_INFO("OTA", "ESP32 OTA finalize, %u bytes...", (unsigned)core.getReceived());
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
esp_err_t err = core.finalize(has_frontend_sha ? frontend_sha_hex : nullptr,
/*reboot=*/false,
device_sha_hex);
Esp32FinalizeResult fr = OtaCoordinator::instance().finalizeForReboot(
has_frontend_sha ? frontend_sha_hex : nullptr);
if (err == ESP_OK) {
OtaCoordinator::instance().notifyEsp32FinalizeComplete();
if (fr.err == ESP_OK) {
// 1) 把包含设备端 SHA 的响应发回前端
char resp[320] = {0};
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\", "
"\"sha256\": \"%s\"}",
device_sha_hex);
fr.device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
// 2) 响应发完后延迟 500ms 重启,给 TCP 缓冲区时间 flush 到客户端
DEBUG_INFO("OTA", "响应已发送,500ms 后重启...");
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
// 不会返回
// 2) 响应发完后由协调器统一重启(notify + delay + esp_restart)
DEBUG_INFO("OTA", "响应已发送,重启...");
OtaCoordinator::instance().restartEsp32(); // 不返回
return ESP_OK;
}
// 失败:core 已内部清理(abort + mutex release)
if (err == ESP_ERR_INVALID_VERSION) {
if (fr.err == ESP_ERR_INVALID_VERSION) {
char err_resp[256] = {0};
snprintf(err_resp, sizeof(err_resp),
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", \"frontend\": \"%s\", \"device\": \"%s\"}",
frontend_sha_hex, device_sha_hex);
frontend_sha_hex, fr.device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, err_resp, strlen(err_resp));
} else {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(fr.err));
}
return ESP_FAIL;
}
@@ -564,15 +559,13 @@ static esp_err_t restart_handler(httpd_req_t *req)
(void)req;
http_set_cors_headers(req);
DEBUG_INFO("OTA", "收到重启请求,500ms 后重启...");
DEBUG_INFO("OTA", "收到重启请求,重启...");
const char resp[] = "{\"status\": \"ok\", \"message\": \"rebooting\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
OtaCoordinator::instance().restartEsp32(); // 内部 notify + delay + esp_restart,不返回
return ESP_OK;
}
@@ -613,7 +606,8 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
static esp_err_t ota_upload_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
// ★ 入口追踪:用于确认 HTTP 请求是否真的到达设备(排查"前端点了但设备没反应")
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d", req->content_len);
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
// ★ WiFi OTA 入口:强制恢复 WIFI_PS_NONE,防止 BLE OTA 残留省电模式导致断连
@@ -671,14 +665,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_FAIL;
}
// ★ 通过全局协调器发起 ESP32 OTA
esp_err_t err = OtaCoordinator::instance().beginEsp32Ota(total_size);
if (err == ESP_ERR_INVALID_STATE) {
// 协调器拒绝(系统忙或通道被占用)→ 尝试强制清理后重试
DEBUG_WARN("WIFI", "ESP32 OTA 被协调器拒绝,强制清理状态机后重试");
Esp32OtaCore::instance().forceCleanup();
err = OtaCoordinator::instance().beginEsp32Ota(total_size);
}
// ★ 通过协调器发起(内部已含 forceCleanup 重试)
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
@@ -690,13 +678,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
Esp32OtaCore &core = Esp32OtaCore::instance();
// 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
uint8_t *fw_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!fw_buf) {
core.abort();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
return ESP_FAIL;
}
// ★ 使用文件级 static 缓冲区,避免堆碎片化(HTTP server 栈仅 8KB)
uint8_t *fw_buf = s_ota_recv_buf;
size_t total_firmware_written = 0;
size_t total_received = json_header_len; // 已消费的字节数
@@ -709,7 +692,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
if (ret <= 0) {
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret);
free(fw_buf);
core.abort();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
return ESP_FAIL;
@@ -718,7 +700,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
err = core.write(fw_buf, (size_t)ret);
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "OTA write failed: %s", esp_err_to_name(err));
free(fw_buf);
core.abort();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
return ESP_FAIL;
@@ -737,7 +718,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
}
}
free(fw_buf);
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)core.getReceived(), json_header_len);
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
httpd_resp_set_type(req, "application/json");
@@ -805,11 +785,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
}
OtaRelay::metaClear();
uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!buf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
return ESP_FAIL;
}
uint8_t *buf = s_ota_recv_buf;
int received = 0;
@@ -837,7 +813,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
first_chunk_len, esp_err_to_name(err));
free(buf);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
return ESP_FAIL;
}
@@ -857,7 +832,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
if (ret <= 0) {
DEBUG_ERR("WIFI", "GD32 upload interrupted: received=%d expected=%d",
received, req->content_len);
free(buf);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
return ESP_FAIL;
}
@@ -866,7 +840,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s",
(unsigned)gd32_offset, ret, esp_err_to_name(err));
free(buf);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
return ESP_FAIL;
}
@@ -875,8 +848,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
gd32_offset += (uint32_t)ret;
}
free(buf);
// update meta with written size
OtaRelay::metaUpdate(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
@@ -891,18 +862,17 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
WifiOta::instance().stateLock();
WifiOta::instance().setGd32StorageDone(true);
WifiOta::instance().stateUnlock();
DEBUG_INFO("WIFI", "GD32 firmware fully stored, computing SHA-256...");
// 计算并存储 SHA-256 校验和
DEBUG_INFO("WIFI", "GD32 firmware fully stored, pre-computing SHA-256...");
// 预计算 SHA-256(写入 NVS),由前端在 /ota/relay 时再校验通过后触发中继
esp_err_t sha_err = OtaRelay::metaFinalizeChecksum();
if (sha_err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
}
DEBUG_INFO("WIFI", "auto-triggering relay");
// ★ 通过协调器触发中继
OtaCoordinator::instance().triggerGd32Relay();
// ★ 不再自动 triggerGd32Relay:安全闸门已统一在 /ota/relay 处理(带 SHA 校验)
}
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\", "
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
@@ -910,8 +880,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
/**
* @brief HTTP POST /ota/relay 处理器
* @param req
* @return
* 必传 body: {"sha256": "<64-char hex>"}。
* 校验通过后触发 GD32 中继;响应回传设备端 SHA(对齐 BLE 的 sendSha256Notify)。
*/
static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
{
@@ -919,6 +889,7 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
// 1) 固件就绪检查
ota_meta_t meta;
memset(&meta, 0, sizeof(meta));
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) {
@@ -926,14 +897,72 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
return ESP_FAIL;
}
// ★ 通过全局协调器触发 GD32 中继
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
if (trigger_err != ESP_OK) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay trigger failed - system busy or firmware not ready");
// 2) 解析前端 SHA-256(必须 64 字符 hex)
char frontend_sha_hex[65] = {0};
if (req->content_len > 0 && req->content_len < 512) {
char buf[512] = {0};
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret > 0) {
buf[ret] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root) {
cJSON *sha_item = cJSON_GetObjectItem(root, "sha256");
if (sha_item && cJSON_IsString(sha_item) &&
strlen(cJSON_GetStringValue(sha_item)) == 64) {
strncpy(frontend_sha_hex, cJSON_GetStringValue(sha_item), 64);
frontend_sha_hex[64] = '\0';
}
cJSON_Delete(root);
}
}
}
if (frontend_sha_hex[0] == '\0') {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"missing sha256 in body (frontend SHA-256 required)");
return ESP_FAIL;
}
const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
// 3) hex(64) → 32 字节
uint8_t frontend_sha[32] = {0};
for (int i = 0; i < 32; ++i) {
char hex[3] = { frontend_sha_hex[i*2], frontend_sha_hex[i*2+1], 0 };
frontend_sha[i] = (uint8_t)strtoul(hex, nullptr, 16);
}
// 4) 共享 helper:计算设备端 SHA → 比对 → 回传 device_sha(无论是否匹配都回传)
uint8_t device_sha[32] = {0};
char device_sha_hex[65] = {0};
esp_err_t verify_err = OtaRelay::verifyFrontendSha(frontend_sha, device_sha);
for (int i = 0; i < 32; ++i) {
snprintf(&device_sha_hex[i*2], 3, "%02X", device_sha[i]);
}
if (verify_err == ESP_ERR_INVALID_CRC) {
char err_resp[256];
snprintf(err_resp, sizeof(err_resp),
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", "
"\"device\": \"%s\"}", device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, err_resp, strlen(err_resp));
return ESP_FAIL;
} else if (verify_err != ESP_OK) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "SHA compute failed");
return ESP_FAIL;
}
// 5) 校验通过 → 触发中继
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
if (trigger_err != ESP_OK) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"relay trigger failed - system busy or firmware not ready");
return ESP_FAIL;
}
// 6) 成功响应回传设备端 SHA(对齐 BLE 的 sendSha256Notify)
char resp[256];
snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"message\": \"OTA relay triggered\", "
"\"sha256\": \"%s\"}", device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));