docs(README): 合并WiFi OTA和BLE OTA
- 调整ESP32_COMM/finalize函数执行顺序:先进行SHA-256校验比对, 再调用esp_ota_end和设置启动分区 - 更新BLE传输收尾描述,明确设备端读回OTA分区进行SHA比对的流程, 以及FINISH事件的上报机制
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* app_config.h - 统一配置文件(C++ 版本)
|
||||
* ESP32-C3 OTA 中继项目
|
||||
*
|
||||
@@ -102,6 +102,8 @@ inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00;
|
||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01;
|
||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480;
|
||||
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11;
|
||||
// Reorder buffer 窗口大小(吸收 BLE 窗口内乱序到达的包,须 >= BLE_OTA_ACK_WINDOW)
|
||||
inline constexpr size_t BLE_OTA_REORDER_WINDOW = 32;
|
||||
|
||||
// ============================================================
|
||||
// 安全常量(需与 GD32 固件团队确认)
|
||||
@@ -112,10 +114,8 @@ inline constexpr uint8_t MASTER_KEY[] = {
|
||||
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10
|
||||
};
|
||||
|
||||
inline constexpr uint8_t SCRAMBLE_CONST[] = {
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
||||
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
|
||||
};
|
||||
// SCRAMBLE_CONST 已移至 Protocol 类,运行时由硬件 RNG 随机生成
|
||||
inline constexpr size_t SCRAMBLE_CONST_LEN = 16;
|
||||
|
||||
// GD32_CHIP_UID 仅保留为默认占位,运行时以握手包解出的芯片地址 UID 为准。
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_LSB = 0x1FFFF7E8U;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#define FW_VERSION_STRING "0.3.12"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-06-14 23:55:14"
|
||||
#define BUILD_TIME "2026-06-16 23:01:18"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "unknown"
|
||||
#define GIT_HASH "2875ef2"
|
||||
|
||||
+86
-37
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 设计依据 WiFi 原流程(wifi_ota.cpp 旧实现),修复已知 bug:
|
||||
* - 旧 wifi_ota.cpp:625 在 esp_ota_end 成功后又调 esp_ota_abort → 用 m_ended 标志避免
|
||||
* - SHA 统一从分区读回计算,与 BLE 增量计算方式统一为本风格
|
||||
* - SHA 统一从分区读回计算,不在传输过程中做增量 SHA
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
@@ -54,7 +54,14 @@ void Esp32OtaCore::resetState()
|
||||
*/
|
||||
esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
{
|
||||
// 若本核心已有未完成会话,先中止并释放全局互斥
|
||||
// ★ 全局互斥:必须在清理旧会话之前检查,
|
||||
// 否则 WiFi 的 begin() 会先释放 BLE 持有的锁,再自己获取,导致互斥失效
|
||||
if (!esp32_ota_mutex_acquire(1000)) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 被其他通道占用,拒绝 begin");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 若本核心已有未完成会话(同一通道的重试),先中止并释放全局互斥
|
||||
if (m_active) {
|
||||
DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA 会话,先中止旧会话");
|
||||
if (!m_ended) {
|
||||
@@ -62,12 +69,11 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
}
|
||||
esp32_ota_mutex_release();
|
||||
resetState();
|
||||
}
|
||||
|
||||
// ★ 全局互斥:检查 ESP32 OTA 是否已被其他通道占用
|
||||
if (!esp32_ota_mutex_acquire(1000)) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 被其他通道占用,拒绝 begin");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
// 重新获取全局互斥(刚刚释放了)
|
||||
if (!esp32_ota_mutex_acquire(1000)) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 重新获取互斥失败");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
}
|
||||
|
||||
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
|
||||
@@ -100,8 +106,10 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// 统一传 total_size 给 esp_ota_begin(WiFi 风格)
|
||||
esp_err_t err = esp_ota_begin(m_partition, total_size, &m_handle);
|
||||
// ★ 必须传分区大小,而非固件实际大小
|
||||
// esp_ota_begin 用 size 参数决定擦除多少 flash 扇区
|
||||
// 传固件大小只擦部分扇区,未擦除区域可能残留脏数据,导致 esp_ota_end 校验失败
|
||||
esp_err_t err = esp_ota_begin(m_partition, m_partition->size, &m_handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
|
||||
m_partition = nullptr;
|
||||
@@ -219,7 +227,7 @@ esp_err_t Esp32OtaCore::computeDeviceSha(char *out_device_sha_hex)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 收尾 OTA 会话:end、校验 SHA、set_boot、可选重启。
|
||||
* @brief 收尾 OTA 会话:校验 SHA、end、set_boot、可选重启。
|
||||
* @param frontend_sha_hex 前端提供的 64 字符 hex SHA-256。
|
||||
* @param reboot 成功后是否重启。
|
||||
* @param out_device_sha_hex 输出设备端 SHA hex 缓冲区。
|
||||
@@ -233,26 +241,14 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// ── esp_ota_end ──
|
||||
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_end...");
|
||||
esp_err_t err = esp_ota_end(m_handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_end 失败: %s", esp_err_to_name(err));
|
||||
// end 失败时仍可 abort(未真正 end)
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
esp32_ota_mutex_release();
|
||||
resetState();
|
||||
return err;
|
||||
}
|
||||
m_ended = true; // ★ end 成功,后续不得再 abort
|
||||
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_end 完成");
|
||||
|
||||
// ── SHA-256 校验(若前端提供)──
|
||||
// ── 先读回分区计算 SHA-256,并与前端文件 SHA 比对 ──
|
||||
if (frontend_sha_hex != nullptr && strlen(frontend_sha_hex) == 64) {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 校验 SHA-256...");
|
||||
esp_err_t sha_err = computeShaFromPartition();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
esp32_ota_mutex_release();
|
||||
resetState();
|
||||
return sha_err;
|
||||
@@ -263,11 +259,11 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
snprintf(&device_hex[i * 2], sizeof(device_hex) - i * 2, "%02x", m_sha256[i]);
|
||||
}
|
||||
|
||||
bool match = true;
|
||||
bool sha_match = true;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (tolower((unsigned char)frontend_sha_hex[i]) !=
|
||||
tolower((unsigned char)device_hex[i])) {
|
||||
match = false;
|
||||
sha_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -277,16 +273,19 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
out_device_sha_hex[64] = '\0';
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
DEBUG_ERR(TAG, "SHA-256 不匹配! frontend=%s device=%s,中止",
|
||||
if (sha_match) {
|
||||
DEBUG_INFO(TAG, "SHA-256 校验通过");
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "SHA-256 不匹配: frontend=%s device=%s",
|
||||
frontend_sha_hex, device_hex);
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
esp32_ota_mutex_release();
|
||||
resetState();
|
||||
return ESP_ERR_INVALID_VERSION; // 调用方据此返回错误给前端
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
}
|
||||
DEBUG_INFO(TAG, "SHA-256 校验通过");
|
||||
} else if (out_device_sha_hex) {
|
||||
// 未提供前端 SHA,但调用方仍想拿到设备端 SHA
|
||||
// 未提供前端 SHA,但仍计算设备端 SHA 供输出
|
||||
if (computeShaFromPartition() == ESP_OK) {
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&out_device_sha_hex[i * 2], 65 - i * 2, "%02x", m_sha256[i]);
|
||||
@@ -295,8 +294,20 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
}
|
||||
}
|
||||
|
||||
// ── esp_ota_end ──
|
||||
esp_err_t err = esp_ota_end(m_handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_end 失败: %s (0x%x)", esp_err_to_name(err), (unsigned)err);
|
||||
// end 失败时仍可 abort(未真正 end)
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
esp32_ota_mutex_release();
|
||||
resetState();
|
||||
return err;
|
||||
}
|
||||
m_ended = true; // ★ end 成功,后续不得再 abort
|
||||
|
||||
// ── 设置启动分区 ──
|
||||
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_set_boot_partition...");
|
||||
err = esp_ota_set_boot_partition(m_partition);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
|
||||
@@ -304,7 +315,6 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
resetState();
|
||||
return err;
|
||||
}
|
||||
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_set_boot_partition 完成");
|
||||
|
||||
esp32_ota_mutex_release();
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,共 %u 字节", (unsigned)m_received);
|
||||
@@ -313,7 +323,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
// (此处不 reset,以便 BLE finish task 在重启前可查询状态)
|
||||
|
||||
if (reboot) {
|
||||
DEBUG_INFO(TAG, "[FINALIZE] 500ms 后重启...");
|
||||
DEBUG_INFO(TAG, "500ms 后重启 ESP32");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
esp_restart();
|
||||
// 不会返回
|
||||
@@ -336,3 +346,42 @@ void Esp32OtaCore::abort()
|
||||
resetState();
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 已中止并清理");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 强制退出 OTA 状态机并释放互斥锁。
|
||||
*
|
||||
* 用于新通道(WiFi/BLE)发现锁被残留占用时,主动走完整的状态机退出流程:
|
||||
* 1. 若会话活跃(m_active)→ 走 abort() 完整清理(esp_ota_abort + 释放锁 + 复位状态)
|
||||
* 2. 若会话不活跃但锁被占(g_esp32OtaOccupied)→ 仅释放残留锁
|
||||
* 3. 否则无需操作
|
||||
*
|
||||
* @return true 执行了清理(调用方可重试 begin),false 无需清理。
|
||||
*/
|
||||
bool Esp32OtaCore::forceCleanup()
|
||||
{
|
||||
if (m_active) {
|
||||
// 会话仍活跃(前一个通道未正常结束),走完整 abort 流程
|
||||
DEBUG_WARN(TAG, "强制清理: 中止活跃的 OTA 会话 (received=%u)", (unsigned)m_received);
|
||||
abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 会话不活跃,但锁可能被残留占用
|
||||
if (!g_esp32OtaMutex) return false;
|
||||
|
||||
// 在互斥保护下检查 g_esp32OtaOccupied
|
||||
if (xSemaphoreTake(g_esp32OtaMutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false;
|
||||
|
||||
bool was_occupied = g_esp32OtaOccupied;
|
||||
if (was_occupied) {
|
||||
g_esp32OtaOccupied = false;
|
||||
}
|
||||
xSemaphoreGive(g_esp32OtaMutex);
|
||||
|
||||
if (was_occupied) {
|
||||
DEBUG_WARN(TAG, "强制清理: 释放残留 OTA 锁(无活跃会话)");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 统一以 WiFi 流程为基准:
|
||||
* - esp_ota_begin 第二参数传 total_size
|
||||
* - SHA-256 在 finalize 时从 OTA 分区读回计算(不在上传期间增量计算)
|
||||
* - finalize 顺序: esp_ota_end → 比对前端 SHA → esp_ota_set_boot_partition → 可选重启
|
||||
* - finalize 顺序: 读回 SHA-256 → 比对前端 SHA → esp_ota_end → esp_ota_set_boot_partition → 可选重启
|
||||
* - m_ended 标志位防止 esp_ota_end 成功后再次 esp_ota_abort 同一 handle
|
||||
*
|
||||
* WiFi 与 BLE 仅传输方式不同(HTTP / GATT),OTA 写入与收尾逻辑完全复用本核心。
|
||||
@@ -48,7 +48,7 @@ public:
|
||||
*/
|
||||
esp_err_t write(const uint8_t *data, size_t len);
|
||||
|
||||
// ── 阶段 3:收尾(end → 校验 SHA → set_boot → 可选重启)──
|
||||
// ── 阶段 3:收尾(校验 SHA → end → set_boot → 可选重启)──
|
||||
// frontend_sha_hex: 前端提供的 64 字符 hex SHA-256,nullptr 表示跳过校验直接应用
|
||||
// reboot: true 则 set_boot 成功后延时 500ms 调 esp_restart()
|
||||
// out_device_sha_hex: 输出设备端计算出的 SHA hex(缓冲区至少 65 字节),可为 nullptr
|
||||
@@ -57,7 +57,7 @@ public:
|
||||
// ESP_ERR_INVALID_VERSION SHA 不匹配(已 abort 并清理)
|
||||
// 其他 end/set_boot 失败(已清理)
|
||||
/**
|
||||
* @brief 收尾 OTA 会话:end、校验 SHA、set_boot、可选重启。
|
||||
* @brief 收尾 OTA 会话:校验 SHA、end、set_boot、可选重启。
|
||||
* @param frontend_sha_hex 前端提供的 64 字符 hex SHA-256。
|
||||
* @param reboot 成功后是否重启。
|
||||
* @param out_device_sha_hex 输出设备端 SHA hex 缓冲区。
|
||||
@@ -84,16 +84,21 @@ public:
|
||||
*/
|
||||
void abort();
|
||||
|
||||
// ── 强制退出状态机并释放互斥锁 ──
|
||||
// 用于新通道发现锁被残留占用时,主动走完整的状态机退出流程:
|
||||
// 活跃会话 → abort();仅锁残留 → 直接释放;均无 → 不操作
|
||||
// 返回 true = 执行了清理(调用方可重试 begin)
|
||||
/**
|
||||
* @brief 强制退出 OTA 状态机并释放互斥锁。
|
||||
* @return true 执行了清理,false 无需清理。
|
||||
*/
|
||||
bool forceCleanup();
|
||||
|
||||
// ── 状态查询 ──
|
||||
/** @brief 查询 OTA 是否激活。 */
|
||||
bool isActive() const { return m_active; }
|
||||
/** @brief 获取已接收的字节数。 */
|
||||
size_t getReceived() const { return m_received; }
|
||||
/** @brief 查询是否等待重启。 */
|
||||
bool isRebootPending() const { return m_rebootPending; }
|
||||
/** @brief 设置重启待处理标志。 */
|
||||
void setRebootPending(bool v) { m_rebootPending = v; }
|
||||
|
||||
private:
|
||||
Esp32OtaCore() = default;
|
||||
|
||||
@@ -117,7 +122,6 @@ private:
|
||||
uint8_t m_sha256[32] = {0};
|
||||
bool m_shaComputed = false;
|
||||
bool m_ended = false; // ★ esp_ota_end 成功后置 true,防 abort 误用
|
||||
bool m_rebootPending = false; // BLE 收尾任务去重用
|
||||
};
|
||||
|
||||
#endif // ESP32_OTA_CORE_H
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* ota_relay.cpp - OTA 中继核心状态机实现
|
||||
* T2.3, T2.4, T3.1 - T3.7
|
||||
*
|
||||
@@ -849,7 +849,7 @@ ota_result_t OtaRelay::sendNonceAndAuthenticate()
|
||||
uint32_t mem_addr = (uint32_t)(uintptr_t)m_nonceServer;
|
||||
uint32_t rand_seed = 0x12345678;
|
||||
|
||||
Protocol::scrambleGenerate(timestamp, mem_addr, rand_seed, m_nonceServer);
|
||||
Protocol::instance().scrambleGenerate(timestamp, mem_addr, rand_seed, m_nonceServer);
|
||||
|
||||
DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---");
|
||||
DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08X", timestamp);
|
||||
|
||||
+407
-32
@@ -24,6 +24,7 @@
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
// NimBLE 协议栈
|
||||
#include "nimble/nimble_port.h"
|
||||
@@ -39,6 +40,7 @@
|
||||
#include "app_config.h"
|
||||
#include "ota_mutex.h"
|
||||
#include "ble_ota.h"
|
||||
#include "wifi_ota.h"
|
||||
#include "esp32_ota_core.h"
|
||||
#include "ota_relay.h"
|
||||
#include "health_monitor.h"
|
||||
@@ -48,15 +50,9 @@
|
||||
#define BLE_OTA_NOTIFY_ACK 0xA1
|
||||
#define BLE_OTA_NOTIFY_FINISH 0xB1
|
||||
#define BLE_OTA_NOTIFY_SHA256 0xC1 // 设备端 SHA-256 上报([0xC1] + [32字节])
|
||||
#define BLE_OTA_FINISH_BEGIN_OTA_END 0x01
|
||||
#define BLE_OTA_FINISH_DONE_OTA_END 0x02
|
||||
#define BLE_OTA_FINISH_BEGIN_SET_BOOT 0x03
|
||||
#define BLE_OTA_FINISH_DONE_SET_BOOT 0x04
|
||||
#define BLE_OTA_FINISH_BEGIN_DELAY 0x05
|
||||
#define BLE_OTA_FINISH_DONE_DELAY 0x06
|
||||
#define BLE_OTA_FINISH_DONE 0x02
|
||||
#define BLE_OTA_FINISH_REBOOT_NOW 0x07
|
||||
#define BLE_OTA_FINISH_ERR_OTA_END 0x81
|
||||
#define BLE_OTA_FINISH_ERR_SET_BOOT 0x82
|
||||
#define BLE_OTA_FINISH_ERR_FINALIZE 0x81
|
||||
#define BLE_OTA_ACK_WINDOW 32
|
||||
#define BLE_ATT_ERR_OTA_BUSY 0x80 // ESP32 OTA 被其他通道占用
|
||||
#define BLE_OTA_FINISH_ERR_MUTEX 0x83 // 互斥拒绝: OTA 被其他通道占用
|
||||
@@ -69,6 +65,7 @@
|
||||
static void ble_ota_start_advertising(void);
|
||||
static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
|
||||
static void sendFinishEvent(uint8_t stage, uint32_t value);
|
||||
static void esp32_reorder_free();
|
||||
|
||||
// ============================================================
|
||||
/**
|
||||
@@ -89,8 +86,8 @@ static uint16_t s_data_out_val_handle = 0;
|
||||
/**
|
||||
* @brief 异步完成 ESP32 OTA 收尾并触发重启。
|
||||
*
|
||||
* 实际的 esp_ota_end / SHA 校验 / esp_ota_set_boot_partition / esp_restart 全部
|
||||
* 由 Esp32OtaCore::finalize 完成。本任务仅负责:补发 ACK → 调 finalize → 上报 SHA → 通知前端。
|
||||
* 实际的 SHA 校验 / esp_ota_end / esp_ota_set_boot_partition 全部由
|
||||
* Esp32OtaCore::finalize 完成。本任务仅负责:补发 ACK → 调 finalize → 上报 SHA → 通知前端。
|
||||
* @param pvParameters 任务参数,当前未使用。
|
||||
*/
|
||||
/**
|
||||
@@ -132,17 +129,15 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
|
||||
if (frontend_sha_hex) free(frontend_sha_hex);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
esp32_reorder_free();
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节", (unsigned)core.getReceived());
|
||||
|
||||
// 通知前端:开始 finalize
|
||||
sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
|
||||
|
||||
// 关键:reboot=false,让 finalize 真正返回、拿到 device_sha_hex,
|
||||
// 再依次推送 SHA 通知 + FINISH_DONE_OTA_END + FINISH_REBOOT_NOW,
|
||||
// 再依次推送 SHA 通知 + FINISH_DONE + FINISH_REBOOT_NOW,
|
||||
// 最后延迟重启。若 reboot=true,core 内部直接 esp_restart(),
|
||||
// 下面所有 notifyResponse / sendFinishEvent 都不会执行,
|
||||
// 前端也就拿不到设备端 SHA 与"收尾完成/即将重启"事件。
|
||||
@@ -167,7 +162,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
|
||||
// 3) 推送"OTA 收尾完成"与"即将重启"事件
|
||||
sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, 0);
|
||||
sendFinishEvent(BLE_OTA_FINISH_DONE, 0);
|
||||
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
|
||||
|
||||
// 4) 通知全部发出后延迟 500ms 重启
|
||||
@@ -179,16 +174,58 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, 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));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)err);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// ★ finalize 内部已 abort 并释放 mutex,这里释放 reorder buffer
|
||||
esp32_reorder_free();
|
||||
}
|
||||
|
||||
if (frontend_sha_hex) free(frontend_sha_hex);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WiFi 共存优化:BLE OTA 传输期间降低 WiFi 射频占用
|
||||
// ============================================================
|
||||
static bool s_wifi_ps_boosted = false;
|
||||
|
||||
/**
|
||||
* @brief BLE OTA 开始时,将 WiFi 切换到最大省电模式,减少射频抢占。
|
||||
* WiFi STA 仍保持连接,但信标监听间隔大幅拉长,BLE 获得更多空中时间。
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief BLE OTA 结束/中止时,恢复 WiFi 省电策略为 WIFI_PS_NONE。
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NimBLE 工具: 从 mbuf 读取原始数据
|
||||
/**
|
||||
@@ -207,6 +244,273 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
|
||||
return (int)copy_len;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ESP32 OTA 包序号顺序保护:Reorder Buffer(TCP 接收窗口模式)
|
||||
//
|
||||
// 背景:
|
||||
// esp_ota_write 是顺序追加写入,不支持 seek。
|
||||
// 客户端窗口内使用 writeValueWithoutResponse(fire-and-forget),
|
||||
// BLE 空口会导致同一窗口的包乱序到达设备端。
|
||||
//
|
||||
// 策略(窗口级 reorder buffer):
|
||||
// - seq < expected → 重复包,丢弃
|
||||
// - seq == expected → 写入 flash,expected++,
|
||||
// 并 flush 连续紧邻的缓存包
|
||||
// - seq > expected 且在窗口范围内 → 暂存到 slot,不写 flash
|
||||
// - seq 超出窗口范围 → 防御性丢弃 + ACK 报告缺口
|
||||
//
|
||||
// 与旧"丢弃策略"的区别:乱序包不再丢弃,而是暂存到 buffer,
|
||||
// 当缺口(expected)的包到达时按序 flush,从而吸收窗口内乱序,
|
||||
// 彻底消除"重传包与原发包再次乱序"导致的重传风暴。
|
||||
//
|
||||
// 内存:slots 指针/长度数组在 OTA begin 时分配(常驻 ~384B),
|
||||
// 每个 slot 的 payload 缓冲区按需 lazy malloc(最多 480B),
|
||||
// flush 后立即 free。OTA 结束/abort 时统一释放。
|
||||
// 若 begin 时分配失败 → active=false,自动回退到丢弃策略。
|
||||
//
|
||||
// ACK 协议不变:getReceived() 仅在真正按序写入 flash 时增长,
|
||||
// 客户端 resumeSeq = ceil(bytes/payloadSize) 仍正确。
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @brief ESP32 OTA reorder buffer 状态。
|
||||
* 窗口级缓存,吸收 BLE 乱序到达的包。
|
||||
*/
|
||||
struct Esp32OtaReorderBuf {
|
||||
bool active = false; ///< 是否已启用(分配成功)
|
||||
bool fallback = false; ///< 是否回退到丢弃策略(buffer 分配失败时)
|
||||
uint32_t expected = 0; ///< 下一个期望按序写入的 seq
|
||||
uint32_t windowBase = 0; ///< 当前窗口起始 seq(= 上次 flush 后的 expected)
|
||||
uint32_t windowSize = 0; ///< 窗口大小(= BLE_OTA_REORDER_WINDOW)
|
||||
uint8_t **slots = nullptr; ///< slots[i]:窗口内第 i 个包的 payload 拷贝(nullptr=未占)
|
||||
size_t *lens = nullptr; ///< lens[i]:slots[i] 的 payload 长度
|
||||
size_t payloadCap = 0; ///< 每个 slot 的容量(= BLE_OTA_CHUNK_PAYLOAD)
|
||||
};
|
||||
|
||||
static Esp32OtaReorderBuf s_reorder;
|
||||
static uint32_t s_fallback_ooo_count = 0; // 回退模式下自上次 ACK 以来的乱序包计数
|
||||
|
||||
// 前向声明(alloc/free 互相引用)
|
||||
static void esp32_reorder_free();
|
||||
|
||||
/**
|
||||
* @brief 分配 reorder buffer(slots 指针/长度数组)。
|
||||
* 在 ble_esp32_ota_begin() 中调用。分配失败则置 fallback=true,
|
||||
* 后续自动回退到丢弃策略。
|
||||
*/
|
||||
static void esp32_reorder_alloc()
|
||||
{
|
||||
// 已分配则先释放(防御性)
|
||||
if (s_reorder.slots || s_reorder.lens) {
|
||||
esp32_reorder_free();
|
||||
}
|
||||
|
||||
s_reorder.expected = 0;
|
||||
s_reorder.windowBase = 0;
|
||||
s_reorder.windowSize = (uint32_t)BLE_OTA_REORDER_WINDOW;
|
||||
s_reorder.payloadCap = BLE_OTA_CHUNK_PAYLOAD;
|
||||
s_reorder.slots = (uint8_t **)calloc(s_reorder.windowSize, sizeof(uint8_t *));
|
||||
s_reorder.lens = (size_t *)calloc(s_reorder.windowSize, sizeof(size_t));
|
||||
s_fallback_ooo_count = 0;
|
||||
|
||||
if (!s_reorder.slots || !s_reorder.lens) {
|
||||
DEBUG_WARN(TAG, "Reorder buffer 分配失败 (slots=%p lens=%p),回退到丢弃策略。"
|
||||
"heap: free=%u min=%u",
|
||||
s_reorder.slots, s_reorder.lens,
|
||||
(unsigned)esp_get_free_heap_size(),
|
||||
(unsigned)esp_get_minimum_free_heap_size());
|
||||
// 释放可能部分成功的分配
|
||||
free(s_reorder.slots); s_reorder.slots = nullptr;
|
||||
free(s_reorder.lens); s_reorder.lens = nullptr;
|
||||
s_reorder.active = false;
|
||||
s_reorder.fallback = true;
|
||||
return;
|
||||
}
|
||||
s_reorder.active = true;
|
||||
s_reorder.fallback = false;
|
||||
DEBUG_INFO(TAG, "Reorder buffer 已启用: window=%u payloadCap=%u heap=%u",
|
||||
(unsigned)s_reorder.windowSize, (unsigned)s_reorder.payloadCap,
|
||||
(unsigned)esp_get_free_heap_size());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 释放 reorder buffer 全部内存(含已占用的 slot)。
|
||||
* 同时复位 expected/windowBase 等状态,供下次 OTA begin 重新分配。
|
||||
*/
|
||||
static void esp32_reorder_free()
|
||||
{
|
||||
if (s_reorder.slots) {
|
||||
for (uint32_t i = 0; i < s_reorder.windowSize; ++i) {
|
||||
if (s_reorder.slots[i]) {
|
||||
free(s_reorder.slots[i]);
|
||||
s_reorder.slots[i] = nullptr;
|
||||
}
|
||||
}
|
||||
free(s_reorder.slots);
|
||||
s_reorder.slots = nullptr;
|
||||
}
|
||||
if (s_reorder.lens) {
|
||||
free(s_reorder.lens);
|
||||
s_reorder.lens = nullptr;
|
||||
}
|
||||
s_reorder.expected = 0;
|
||||
s_reorder.windowBase = 0;
|
||||
s_reorder.active = false;
|
||||
s_reorder.fallback = false;
|
||||
s_reorder.windowSize = 0;
|
||||
s_reorder.payloadCap = 0;
|
||||
s_fallback_ooo_count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief flush expected 对应的连续缓存包到 flash,并释放对应 slot。
|
||||
* 由 esp32_write_ordered 在每次 expected 推进后调用。
|
||||
* @return flush 的包数(不含当前已写入的包)。
|
||||
*/
|
||||
static uint32_t esp32_reorder_flush()
|
||||
{
|
||||
uint32_t flushed = 0;
|
||||
while (s_reorder.active) {
|
||||
uint32_t idx = s_reorder.expected - s_reorder.windowBase;
|
||||
// idx 应始终 < windowSize(窗口未越界)
|
||||
if (idx >= s_reorder.windowSize) break;
|
||||
if (!s_reorder.slots[idx]) break; // 缺口:后续虽有缓存但不连续
|
||||
|
||||
uint8_t *data = s_reorder.slots[idx];
|
||||
size_t len = s_reorder.lens[idx];
|
||||
esp_err_t err = Esp32OtaCore::instance().write(data, len);
|
||||
// 释放 slot
|
||||
free(s_reorder.slots[idx]);
|
||||
s_reorder.slots[idx] = nullptr;
|
||||
s_reorder.lens[idx] = 0;
|
||||
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Reorder flush 写入失败: seq=%u err=%s",
|
||||
(unsigned)s_reorder.expected, esp_err_to_name(err));
|
||||
// 写入失败不推进 expected,交由上层 abort
|
||||
return flushed;
|
||||
}
|
||||
s_reorder.expected++;
|
||||
flushed++;
|
||||
}
|
||||
return flushed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按序号顺序写入 ESP32 OTA 数据。
|
||||
*
|
||||
* 启用 reorder buffer 时:吸收窗口内乱序,缺口填满后按序 flush。
|
||||
* 回退模式(buffer 分配失败):执行旧丢弃策略(乱序包丢弃 + ACK 报告缺口)。
|
||||
*
|
||||
* @param seq 包序号。
|
||||
* @param cmd 数据命令字节(用于 ACK)。
|
||||
* @param data 负载数据指针。
|
||||
* @param data_len 负载数据长度。
|
||||
* @return ESP_OK 写入成功或被安全跳过,其他为失败。
|
||||
*/
|
||||
static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
const uint8_t *data, size_t data_len)
|
||||
{
|
||||
// ── 回退模式:旧丢弃策略 ──
|
||||
if (s_reorder.fallback) {
|
||||
// 重复包
|
||||
if (seq < s_reorder.expected) {
|
||||
DEBUG_DBG(TAG, "ESP32 重复包: seq=%u < expected=%u,跳过",
|
||||
seq, (unsigned)s_reorder.expected);
|
||||
return ESP_OK;
|
||||
}
|
||||
// 乱序包:丢弃 + 节流 ACK 报告缺口
|
||||
// ★ 节流策略:每 BLE_OTA_ACK_WINDOW 个乱序包才发一次 ACK,
|
||||
// 或在窗口边界处发 ACK。避免大量乱序包涌入时触发 ACK 风暴,
|
||||
// 耗尽 NimBLE mbuf 池导致 ESP_ERR_NO_MEM。
|
||||
if (seq > s_reorder.expected) {
|
||||
s_fallback_ooo_count++;
|
||||
bool shouldAck =
|
||||
(s_fallback_ooo_count >= BLE_OTA_ACK_WINDOW) ||
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
|
||||
if (shouldAck) {
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_WARN(TAG, "ESP32 乱序包跳过(回退): seq=%u (expected=%u),节流 ACK bytes=%u (ooo=%u)",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)curReceived,
|
||||
(unsigned)s_fallback_ooo_count);
|
||||
sendWindowAck(cmd, s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
curReceived);
|
||||
s_fallback_ooo_count = 0;
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "ESP32 乱序包跳过(回退): seq=%u (expected=%u),抑制 ACK (ooo=%u)",
|
||||
seq, (unsigned)s_reorder.expected,
|
||||
(unsigned)s_fallback_ooo_count);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
// seq == expected:正常写入
|
||||
esp_err_t err = Esp32OtaCore::instance().write(data, data_len);
|
||||
if (err != ESP_OK) return err;
|
||||
s_reorder.expected++;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ── Reorder buffer 模式 ──
|
||||
// 窗口推进检测:若 expected 已超出当前窗口基址,滑动窗口
|
||||
if (s_reorder.expected >= s_reorder.windowBase + s_reorder.windowSize) {
|
||||
s_reorder.windowBase = s_reorder.expected;
|
||||
}
|
||||
|
||||
// 重复包:seq < expected → 已写入,跳过
|
||||
if (seq < s_reorder.expected) {
|
||||
DEBUG_DBG(TAG, "ESP32 重复包: seq=%u < expected=%u,跳过",
|
||||
seq, (unsigned)s_reorder.expected);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// seq == expected:正常写入 + flush 连续缓存包
|
||||
if (seq == s_reorder.expected) {
|
||||
esp_err_t err = Esp32OtaCore::instance().write(data, data_len);
|
||||
if (err != ESP_OK) return err;
|
||||
s_reorder.expected++;
|
||||
uint32_t flushed = esp32_reorder_flush();
|
||||
if (flushed > 0) {
|
||||
DEBUG_DBG(TAG, "Reorder flush: seq=%u 后连续写入 %u 包 (expected→%u)",
|
||||
seq, (unsigned)flushed, (unsigned)s_reorder.expected);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// seq > expected:暂存到 slot(若在窗口范围内)
|
||||
uint32_t idx = seq - s_reorder.windowBase;
|
||||
if (idx < s_reorder.windowSize) {
|
||||
if (s_reorder.slots[idx]) {
|
||||
// 该 slot 已被占用(重复的乱序包),跳过
|
||||
DEBUG_DBG(TAG, "Reorder slot 已占: seq=%u idx=%u,跳过重复包",
|
||||
seq, (unsigned)idx);
|
||||
return ESP_OK;
|
||||
}
|
||||
// data 可能来自静态 buf(ble_ota_data_in_access 的合并缓冲区),
|
||||
// 后续包处理会覆盖它,必须拷贝一份
|
||||
uint8_t *copy = (uint8_t *)malloc(data_len);
|
||||
if (!copy) {
|
||||
DEBUG_WARN(TAG, "Reorder slot malloc 失败: seq=%u,丢弃该包(靠客户端重传)",
|
||||
seq);
|
||||
return ESP_OK; // 不算失败,靠客户端重传补
|
||||
}
|
||||
memcpy(copy, data, data_len);
|
||||
s_reorder.slots[idx] = copy;
|
||||
s_reorder.lens[idx] = data_len;
|
||||
DEBUG_DBG(TAG, "Reorder 缓存乱序包: seq=%u idx=%u (expected=%u)",
|
||||
seq, (unsigned)idx, (unsigned)s_reorder.expected);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// seq 超出窗口范围(防御性,理论不会发生)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_WARN(TAG, "ESP32 包超出 reorder 窗口: seq=%u (expected=%u base=%u win=%u),ACK bytes=%u",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)s_reorder.windowBase,
|
||||
(unsigned)s_reorder.windowSize, (unsigned)curReceived);
|
||||
sendWindowAck(cmd, s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
curReceived);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ESP32 OTA 薄包装:实际 begin/write/finalize 已抽离到 Esp32OtaCore
|
||||
// 此处仅保留 BLE 侧的状态复位与日志,供 data_in_access 调用。
|
||||
@@ -221,6 +525,12 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
|
||||
static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
{
|
||||
esp_err_t err = Esp32OtaCore::instance().begin(total_size);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
// ★ OTA 锁被占用,走完整的状态机退出 + 清理锁,然后重试一次
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 锁被占用,强制清理状态机后重试");
|
||||
Esp32OtaCore::instance().forceCleanup();
|
||||
err = Esp32OtaCore::instance().begin(total_size);
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
@@ -228,6 +538,10 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
BleOta::instance().m_esp32LastCmd = 0;
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
// ★ 分配 reorder buffer(失败则自动回退丢弃策略)
|
||||
esp32_reorder_alloc();
|
||||
// ★ BLE OTA 开始:降低 WiFi 射频占用
|
||||
ble_ota_boost_wifi_ps();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -238,6 +552,10 @@ static void ble_esp32_ota_abort()
|
||||
{
|
||||
Esp32OtaCore::instance().abort();
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// ★ 释放 reorder buffer
|
||||
esp32_reorder_free();
|
||||
// ★ BLE OTA 中止:恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,14 +672,14 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH。
|
||||
if (len < 33) {
|
||||
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33(前端 SHA-256 必传),拒绝触发");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_INVALID_ARG);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)ESP_ERR_INVALID_ARG);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *frontend_sha_hex = (char *)malloc(65);
|
||||
if (!frontend_sha_hex) {
|
||||
DEBUG_ERR(TAG, "CTRL 0xF8 申请内存失败");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_NO_MEM);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)ESP_ERR_NO_MEM);
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
@@ -508,7 +826,27 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err = Esp32OtaCore::instance().write(&buf[data_offset], data_len);
|
||||
// ★ 会话有效性检查:如果 begin() 失败(如互斥拒绝),后续包也应丢弃
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > (size_t)recv_len) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 防重复写入保护:如果已接收字节 >= 声明大小,跳过(重试引起的重复包)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
if (total_size > 0 && curReceived >= total_size) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 已收满 (%u >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
(unsigned)curReceived, total_size, seq);
|
||||
sendWindowAck(cmd, seq, curReceived);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > (size_t)recv_len) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 使用带序号顺序保护的写入:拒绝乱序包并立即 ACK 通知发送端
|
||||
esp_err_t err = esp32_write_ordered(seq, cmd, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
|
||||
seq, esp_err_to_name(err));
|
||||
@@ -519,26 +857,37 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
|
||||
seq, (unsigned int)Esp32OtaCore::instance().getReceived());
|
||||
|
||||
// 尾包检测: 期望 payload < 标准负载说明是真正的最后一包
|
||||
// 使用 expected_payload 而非 data_len,避免缓冲区截断导致误触发重启
|
||||
// 完成判定:以实际按序写入字节数为准。
|
||||
// reorder buffer 模式下,尾包可能是乱序到达被缓存的包,
|
||||
// 此刻 getReceived() 尚未达到 total_size,不能据此判定完成。
|
||||
// isLastPacket 仅记录"这是最后一包的形态",不单独作为完成条件。
|
||||
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
bool transferComplete = (total_size > 0 &&
|
||||
Esp32OtaCore::instance().getReceived() >= total_size);
|
||||
|
||||
// ACK 触发条件:
|
||||
// - 窗口末尾包 (seq+1 是窗口整数倍)
|
||||
// - 数据全部按序写完(含 reorder flush 后达到 total_size)
|
||||
// - 尾包形态(兜底:即使 getReceived 不足也回一个 ACK,让客户端续传)
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(total_size > 0 && Esp32OtaCore::instance().getReceived() >= total_size) ||
|
||||
transferComplete ||
|
||||
isLastPacket) {
|
||||
if (isLastPacket) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
seq, data_len, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size);
|
||||
}
|
||||
sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
|
||||
}
|
||||
|
||||
if ((total_size > 0 && Esp32OtaCore::instance().getReceived() >= total_size) || isLastPacket) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1,
|
||||
isLastPacket ? " [尾包]" : "");
|
||||
// ★ 仅当数据真正全部按序写完时才标记传输完成。
|
||||
// 尾包乱序缓存但缺口未填满时,不算完成(等待客户端续传缺口包)。
|
||||
if (transferComplete) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
BleOta::instance().m_esp32LastSeq = seq;
|
||||
// ★ ESP32 OTA 传输完成:恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
// 注:与 WiFi 流程对齐,传输完成后由前端发送 0xF8 + SHA-256 触发 finalize,
|
||||
// 设备端不再自动创建收尾任务。
|
||||
}
|
||||
@@ -567,6 +916,8 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
|
||||
meta.total_size, data_len);
|
||||
// ★ GD32 BLE OTA 开始:降低 WiFi 射频占用
|
||||
ble_ota_boost_wifi_ps();
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
@@ -583,6 +934,15 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
// --- GD32 写入 Flash ---
|
||||
uint32_t flash_offset = meta.stored_size;
|
||||
// ★ 防重复写入保护:如果已存储字节 >= 声明大小,跳过重试引起的重复包
|
||||
if (flash_offset >= meta.total_size) {
|
||||
DEBUG_WARN(TAG, "GD32 OTA 已收满 (%" PRIu32 " >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
flash_offset, meta.total_size, seq);
|
||||
sendWindowAck(cmd, seq, flash_offset);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > (size_t)recv_len) break;
|
||||
continue;
|
||||
}
|
||||
if (flash_offset + (uint32_t)data_len > meta.total_size) {
|
||||
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
|
||||
flash_offset, data_len, meta.total_size);
|
||||
@@ -617,6 +977,8 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
// ★ GD32 BLE OTA 传输完成:恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
// 计算并存储 SHA-256 校验和
|
||||
esp_err_t sha_err = OtaRelay::metaFinalizeChecksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
@@ -661,7 +1023,15 @@ static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
|
||||
const char *version = FW_VERSION;
|
||||
int rc = os_mbuf_append(ctxt->om, version, strlen(version));
|
||||
// 版本字符串: "vX.Y.Z YYYY-MM-DD HH:MM:SS"
|
||||
char ver_buf[64];
|
||||
int ver_len = snprintf(ver_buf, sizeof(ver_buf), "%s %s", FW_VERSION, FW_BUILD_TIME);
|
||||
if (ver_len < 0 || ver_len >= (int)sizeof(ver_buf)) {
|
||||
ver_len = strlen(version);
|
||||
int rc = os_mbuf_append(ctxt->om, version, ver_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
int rc = os_mbuf_append(ctxt->om, ver_buf, ver_len);
|
||||
return (rc == 0) ? 0 : BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
@@ -796,9 +1166,14 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d",
|
||||
event->disconnect.reason);
|
||||
/** @brief BLE GATT 客户端连接状态。 */
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
// ★ BLE 断开时恢复 WiFi 射频,并中止未完成的 ESP32 OTA
|
||||
ble_ota_restore_wifi_ps();
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
|
||||
ble_esp32_ota_abort();
|
||||
}
|
||||
// 恢复广播以接受新连接
|
||||
ble_ota_start_advertising();
|
||||
return 0;
|
||||
@@ -1125,4 +1500,4 @@ void BleOta::taskEntry(void *pvParameters)
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,6 +610,10 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||
|
||||
// ★ WiFi OTA 入口:强制恢复 WIFI_PS_NONE,防止 BLE OTA 残留省电模式导致断连
|
||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||
WifiOta::instance().setBleOtaActive(false);
|
||||
|
||||
// 预读 1 字节判断是 JSON 还是 binary
|
||||
char peek;
|
||||
int peek_ret = httpd_req_recv(req, &peek, 1);
|
||||
@@ -663,6 +667,12 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
esp_err_t err = core.begin(total_size);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
// ★ OTA 锁被占用,走完整的状态机退出 + 清理锁,然后重试一次
|
||||
DEBUG_WARN("WIFI", "ESP32 OTA 锁被占用,强制清理状态机后重试");
|
||||
core.forceCleanup();
|
||||
err = core.begin(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");
|
||||
@@ -1672,6 +1682,13 @@ void WifiOta::taskEntry(void *pvParameters)
|
||||
while (1) {
|
||||
tick++;
|
||||
|
||||
// ★ BLE OTA 传输期间,暂停 WiFi RSSI 轮询,避免 esp_wifi_sta_get_ap_info()
|
||||
// 触发射频活动干扰 BLE 连接事件
|
||||
if (WifiOta::instance().m_bleOtaActive.load()) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 每 10 秒检查一次 RSSI
|
||||
if (tick % 10 == 0) {
|
||||
int8_t rssi = WifiOta::getRssi();
|
||||
|
||||
@@ -97,6 +97,11 @@ public:
|
||||
|
||||
httpd_handle_t m_httpServer = NULL;
|
||||
|
||||
// ★ BLE OTA 传输期间暂停 WiFi 射频活动(减少共存干扰)
|
||||
std::atomic<bool> m_bleOtaActive{false};
|
||||
/** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */
|
||||
void setBleOtaActive(bool v) { m_bleOtaActive.store(v); }
|
||||
|
||||
private:
|
||||
// ── 跨任务共享状态(atomic + m_stateMutex 保护)──
|
||||
std::atomic<bool> m_staConnected{false};
|
||||
|
||||
@@ -40,6 +40,15 @@ void Protocol::init()
|
||||
DEBUG_INFO("PROT", "=== 初始化协议层 ===");
|
||||
DEBUG_INFO("PROT", "协议层功能: nonce 扰码 + 会话密钥管理");
|
||||
|
||||
esp_fill_random(m_scrambleConst, SCRAMBLE_CONST_LEN);
|
||||
|
||||
{
|
||||
char hex[SCRAMBLE_CONST_LEN * 2 + 1] = {0};
|
||||
for (size_t i = 0; i < SCRAMBLE_CONST_LEN; ++i)
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", m_scrambleConst[i]);
|
||||
DEBUG_INFO("PROT", "SCRAMBLE_CONST (RNG): %s", hex);
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
DEBUG_INFO("PROT", "协议层就绪");
|
||||
}
|
||||
@@ -55,11 +64,11 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
memcpy(&state[0], ×tamp, 4);
|
||||
memcpy(&state[4], &mem_addr, 4);
|
||||
memcpy(&state[8], &rand_seed, 4);
|
||||
memcpy(&state[12], SCRAMBLE_CONST, 4);
|
||||
memcpy(&state[12], m_scrambleConst, 4);
|
||||
|
||||
for (int round = 0; round < 3; round++) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
state[i] ^= SCRAMBLE_CONST[i];
|
||||
state[i] ^= m_scrambleConst[i];
|
||||
state[i] = (state[i] << 1) | (state[i] >> 7);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +76,7 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
for (int round = 0; round < 2; round++) {
|
||||
uint32_t *words = (uint32_t *)state;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
words[i] ^= SCRAMBLE_CONST[i * 4 % 16] | ((uint32_t)SCRAMBLE_CONST[i * 4 + 1] << 8);
|
||||
words[i] ^= m_scrambleConst[i * 4 % 16] | ((uint32_t)m_scrambleConst[i * 4 + 1] << 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,4 +108,4 @@ void Protocol::sessionKeyClear(session_ctx_t *sess)
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16;
|
||||
inline constexpr size_t SCRAMBLE_CONST_HERE_LEN = 16;
|
||||
inline constexpr size_t CHIP_UID_LEN = 12;
|
||||
|
||||
// AES 会话上下文
|
||||
@@ -55,7 +54,7 @@ public:
|
||||
* @param rand_seed 随机种子。
|
||||
* @param nonce_out 输出的 16 字节 nonce 缓冲区。
|
||||
*/
|
||||
static void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
/**
|
||||
* @brief 会话密钥初始化,将密钥拷贝到会话上下文。
|
||||
@@ -76,6 +75,7 @@ private:
|
||||
Protocol(const Protocol&) = delete;
|
||||
Protocol& operator=(const Protocol&) = delete;
|
||||
|
||||
uint8_t m_scrambleConst[SCRAMBLE_CONST_LEN] = {0};
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user