docs(README): 合并WiFi OTA和BLE OTA

- 调整ESP32_COMM/finalize函数执行顺序:先进行SHA-256校验比对,
  再调用esp_ota_end和设置启动分区
- 更新BLE传输收尾描述,明确设备端读回OTA分区进行SHA比对的流程,
  以及FINISH事件的上报机制
This commit is contained in:
小龙虾
2026-06-16 23:17:06 +08:00
parent 5060db0a8e
commit 5b2c5f8ff9
25 changed files with 1725 additions and 3542 deletions
+407 -32
View File
@@ -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));
}
}
}
+17
View File
@@ -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();
+5
View File
@@ -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};