Files
ESP32_COMM/main/server/ble_ota_data.cpp
T
小龙虾 770ae14156
SPI Link v2 Self-Test / selftest (push) Has been cancelled
feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
添加SPI Link v2的PC端自测功能,包含交叉校验基准测试,覆盖HELLO/OTA/CAN/PING等
多种协议场景。同时重构了health monitor中的多字段读取逻辑,消除了volatile变量
的撕裂读问题,并增强了BLE OTA的数据传输可靠性。

refactor(health_monitor): 修复heap hook多字段撕裂读问题

采用portMUX_CRITICAL临界区实现多字段一致快照读取,消除任务上下文与heap hook
之间的数据竞争。修改了HookSnap结构体设计,通过一次性拷贝volatile变量避免
跨字段组合时的数据不一致。JSON输出中明确标注estimated值的置信度。

fix(ble_ota): 实现DATA_IN丢包负反馈机制

当malloc失败或队列满时发送缺口ACK通知前端按completed_bytes续传,避免
静默丢包导致的8秒超时重传风暴。增加节流机制防止负ACK在重传叠加时打爆
NimBLE mbuf池,提升低内存环境下的OTA传输稳定性。

refactor(spi_master): 从堆分配改为固定Job池管理

将SPI worker的动态堆分配Job改为固定大小的静态池管理,避免频繁的
calloc/free操作影响实时性。新增acquireJobSlot/releaseJobSlot机制
管理池槽生命周期,提升SPI通信的可靠性和内存使用效率。
2026-07-21 01:16:43 +08:00

1129 lines
48 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* ble_ota_data.cpp - BLE OTA 数据路径:reorder、写 Flash、worker、DATA_IN
*/
#include "ble_ota_priv.h"
// ============================================================
// 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)
uint32_t cachedCount = 0; ///< 当前已缓存的乱序包数
};
Esp32OtaReorderBuf s_reorder;
uint32_t s_fallback_ooo_count = 0; // 回退模式下自上次 ACK 以来的乱序包计数
uint32_t s_gd32_ooo_count = 0; // GD32 路径自上次 ACK 以来的乱序/重复包计数
ota_meta_t s_ble_gd32_meta = {};
bool s_ble_gd32_meta_active = false;
bool s_conn_fallback_requested = false;
// ============================================================
// DATA_IN 丢包负反馈:把 malloc 失败 / 队列满从"静默丢包"变成
// "主动发缺口 ACK 让前端按 completed_bytes 续传退避"。
//
// 现状:writeValueWithoutResponse 窗口内丢包时,NimBLE 不会向对端
// 回 ATT Error Response(即发即忘),返回非零 ATT 错误对窗口内
// 包无效,只能走 DATA_OUT 通知通道发窗口 ACK。
// 静默丢包会让前端等满 8s 窗口超时后整窗口重发 → 重传风暴叠加
// 低堆时可能二次失败。这里在丢包路径补发一次缺口 ACK(seq 取
// s_reorder.expected-1,completed 取实际落盘字节),让前端立刻
// 从 lastAckBytes 续传,客户端已有续传语义无需改动。
//
// 节流:丢包路径无"进度/窗口边界"可复用,用"丢包计数 ≥ ACK_WINDOW
// 或距上次 ≥ NEG_ACK_MIN_INTERVAL_US"双约束,防止重传叠加时
// ACK 通知打爆 NimBLE mbuf 池(与现有正常路径节流同动机)。
// 状态仅在 NimBLE host task 回调中读写(单线程),无需锁。
// ============================================================
static uint32_t s_ota_drop_malloc_fail = 0; // DATA_IN malloc 失败累计
static uint32_t s_ota_drop_queue_full = 0; // DATA_IN 队列满累计
static uint32_t s_neg_ack_sent = 0; // 负 ACK 发送累计
static uint32_t s_neg_ack_drop_count = 0; // 自上次发负 ACK 以来累计丢包数
static int64_t s_last_neg_ack_us = 0; // 上次负 ACK 时刻(esp_timer_get_time)
// 节流常量:与正常 ACK 路径同窗口量级(BLE_OTA_ACK_WINDOW=64),
// 最小间隔 200ms 兜底,避免极端低堆时负 ACK 抖动。
static constexpr uint32_t NEG_ACK_DROP_WINDOW = 64; // 丢包数阈值
static constexpr int64_t NEG_ACK_MIN_INTERVAL_US = 200 * 1000; // 200ms
// 前向声明(alloc/free 互相引用)
void esp32_reorder_free();
uint16_t ble_ota_log_conn_params(uint16_t conn_handle, const char *phase)
{
struct ble_gap_conn_desc desc = {};
int rc = ble_gap_conn_find(conn_handle, &desc);
if (rc != 0) {
DEBUG_WARN(TAG, "%sBLE param读失败 rc=%d", phase, rc);
return 0;
}
DEBUG_DBG(TAG, "%sitv=%.2f lat=%u",
phase,
desc.conn_itvl * 1.25f,
(unsigned)desc.conn_latency);
return desc.conn_itvl;
}
int ble_ota_request_conn_params(uint16_t conn_handle, bool windows_compatible)
{
struct ble_gap_upd_params params = {};
if (windows_compatible) {
/* 回退:10–12.5ms(itvl 8..10)。比原 15–20ms 更激进,让 Windows 回退也不至于掉到 20ms。
* 10ms 是多数 Windows 主机可接受的较激进下限,仍留 3 档让对端有选择。 */
params.itvl_min = 8;
params.itvl_max = 10;
params.min_ce_len = 0;
params.max_ce_len = 0;
} else {
/* 高速:7.5–10ms(itvl 6..8)。比原 7.5–15ms 激进收窄,让对端尽量落在 7.5–10ms
* 而不是被对端选到 15ms。仍保留 3 档(6/7/8)让对端有选择,避免锁死 7.5 被拒
* 然后掉到更慢默认值。ce_len 6..12 保证事件时间不被 OS 砍得更短。 */
params.itvl_min = 6;
params.itvl_max = 8;
params.min_ce_len = 6;
params.max_ce_len = 12;
}
params.latency = 0;
params.supervision_timeout = 200;
int rc = ble_gap_update_params(conn_handle, &params);
DEBUG_DBG(TAG, "BLEp(%s) %.1f-%.1f rc=%d",
windows_compatible ? "Win" : "Hi",
params.itvl_min * 1.25f,
params.itvl_max * 1.25f,
rc);
return rc;
}
/**
* @brief 分配 reorder buffer(slots 指针/长度数组)。
* 在 ble_esp32_ota_begin() 中调用。分配失败则置 fallback=true,
* 后续自动回退到丢弃策略。
*/
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.cachedCount = 0;
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分配失败,回退丢弃 free=%u",
(unsigned)esp_get_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_DBG(TAG, "Reorder ok win=%u cap=%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 重新分配。
*/
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_reorder.cachedCount = 0;
s_fallback_ooo_count = 0;
}
/**
* @brief 取当前 reorder buffer 已缓存的乱序包数(每个 slot 持有 malloc 的 payload)。
* 供堆低水位快照采样,定位"OTA 期低谷是不是 BLE reorder 引起"。
* 非 OTA 期 active=false,返回 0。
*/
uint32_t ble_ota_get_reorder_cached_count()
{
return s_reorder.active ? s_reorder.cachedCount : 0;
}
/**
* @brief flush expected 对应的连续缓存包到 flash,并释放对应 slot。
* 由 esp32_write_ordered 在每次 expected 推进后调用。
* @return flush 的包数(不含当前已写入的包)。
*/
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 (s_reorder.cachedCount > 0) {
s_reorder.cachedCount--;
}
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, "乱序 s=%u e=%u ACK=%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乱序 s=%u e=%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_INFO(TAG, "Reorder补齐 seq=%u flush=%u exp→%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占 s=%u i=%u 跳过",
seq, (unsigned)idx);
return ESP_OK;
}
// data 可能来自静态 buf(ble_ota_data_in_access 的合并缓冲区),
// 后续包处理会覆盖它,必须拷贝一份
uint8_t *copy = (uint8_t *)malloc(data_len);
if (!copy) {
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
DEBUG_ERR(TAG, "Reorder OOM s=%u e=%u n=%u ACK+abort",
seq, (unsigned)s_reorder.expected, (unsigned)curReceived);
sendWindowAck(cmd,
s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
curReceived);
return ESP_ERR_NO_MEM;
}
memcpy(copy, data, data_len);
s_reorder.slots[idx] = copy;
s_reorder.lens[idx] = data_len;
s_reorder.cachedCount++;
if (s_reorder.cachedCount == 1U) {
DEBUG_INFO(TAG, "Reorder缺口 exp=%u cache seq=%u",
(unsigned)s_reorder.expected, seq);
}
DEBUG_DBG(TAG, "Reorder cache s=%u i=%u exp=%u",
seq, (unsigned)idx, (unsigned)s_reorder.expected);
return ESP_OK;
}
// seq 超出窗口范围(防御性,理论不会发生)
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
DEBUG_WARN(TAG, "超窗 s=%u e=%u w=%u ACK=%u",
seq, (unsigned)s_reorder.expected,
(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 调用。
// ─────────────────────────────────────────────────────────────
/**
* @brief 初始化 ESP32 本机 OTA 写入会话。
* 委托 Esp32OtaCore::begin,并复位 BLE 传输相关状态。
* @param total_size 固件总大小(字节)。
* @return ESP_OK 成功,其他为失败错误码。
*/
esp_err_t ble_esp32_ota_begin(size_t total_size)
{
// ★ 通过协调器发起(内部已含 forceCleanup 重试)
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_BLE);
if (err != ESP_OK) {
return err;
}
// 复位 BLE 传输相关状态
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();
/* 刷写前再要一次高速连接参数(连接时若未谈妥,此处补刀) */
if (BleOta::instance().m_bleConnected &&
BleOta::instance().m_connHandle != 0xFFFFU) {
(void)ble_ota_request_conn_params(BleOta::instance().m_connHandle, false);
}
return ESP_OK;
}
/**
* @brief BLE OTA 会话状态统一复位(共用清理内核)。
*
* 集中处理三个清理动作:
* 1. abortCurrent():通知协调器按当前状态分发清理
* (GD32_STORING→metaClear, ESP32_RECEIVING→core.abort, IDLE→空操作),
* 确保下次 begin() 不会被残留状态假拒绝。
* 2. core.abort() 兜底:若 Esp32OtaCore 仍 isActive(异常路径),单独再 abort。
* 3. BLE OTA 会话状态字段复位:m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending
* + esp32_reorder_free() 释放 reorder buffer + s_gd32_ooo_count 归零。
*
* 不做:OTA 数据队列清理(调用方按需做)、WiFi PS 恢复(调用方按需做)。
* 这两个动作在不同调用路径下时序/前置条件不同,交给调用方更安全。
*
* 调用方:
* - ble_esp32_ota_abort(): ESP32 OTA 异常/中止路径
* - 0xFB handler: 用户主动中止
* - BLE_GAP_EVENT_DISCONNECT handler: BLE 断开(含 GD32 存储断开)
*/
void ble_ota_session_reset()
{
// ★ 无条件通知协调器释放通道占用(m_sysState → IDLE)。
// abortCurrent() 对 IDLE 状态是空操作,对其他状态均复位到 IDLE,
// 确保后续 begin() 不会被残留的 ESP32_RECEIVING / ERROR 等状态假拒绝。
OtaCoordinator::instance().abortCurrent();
// 若 ESP32 OTA core 仍活跃(兜底),单独再 abort 一次
if (Esp32OtaCore::instance().isActive()) {
Esp32OtaCore::instance().abort();
}
// 复位 BLE OTA 会话状态,避免下次会话读到旧值
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
BleOta::instance().m_esp32LastCmd = 0;
BleOta::instance().m_esp32RebootPending = false;
// 释放 ESP32 reorder buffer(无 buffer 时为空操作)
esp32_reorder_free();
// 复位 GD32 路径乱序包计数器(节流 ACK 计数),保证下次 GD32 上传从干净状态开始
s_gd32_ooo_count = 0;
s_ble_gd32_meta = {};
s_ble_gd32_meta_active = false;
// 复位 DATA_IN 丢包负反馈计数/节流状态,下次会话从干净状态开始
s_ota_drop_malloc_fail = 0;
s_ota_drop_queue_full = 0;
s_neg_ack_sent = 0;
s_neg_ack_drop_count = 0;
s_last_neg_ack_us = 0;
}
/**
* @brief ESP32 OTA 异常/中止时调用:会话复位 + WiFi PS 恢复。
*
* 关键:必须先通知协调器释放 m_sysState(避免下次 begin 被 ESP32_RECEIVING
* 状态假拒绝),再调 core.abort() 释放 mutex。两者重复 abort 幂等安全。
*/
void ble_esp32_ota_abort()
{
ble_ota_session_reset();
// ★ BLE OTA 中止:恢复 WiFi 射频
ble_ota_restore_wifi_ps();
}
/**
* @brief 发送窗口 ACK 通知,回报已处理的分块序号与累计字节数。
* @param cmd 对应的数据命令字节。
* @param seq 当前确认的分块序号。
* @param completed_bytes 已完成写入的累计字节数。
*/
void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
{
uint8_t ack[8] = {0};
ack[0] = BLE_OTA_NOTIFY_ACK;
ack[1] = cmd;
ack[2] = (uint8_t)(seq & 0xFFU);
ack[3] = (uint8_t)((seq >> 8) & 0xFFU);
ack[4] = (uint8_t)(completed_bytes & 0xFFU);
ack[5] = (uint8_t)((completed_bytes >> 8) & 0xFFU);
ack[6] = (uint8_t)((completed_bytes >> 16) & 0xFFU);
ack[7] = (uint8_t)((completed_bytes >> 24) & 0xFFU);
esp_err_t err = BleOta::instance().notifyResponse(ack, sizeof(ack));
if (err != ESP_OK) {
DEBUG_WARN(TAG, "ACK失败 cmd=0x%02X seq=%u %s",
cmd, seq, esp_err_to_name(err));
} else {
DEBUG_DBG(TAG, "已发送 ACK: cmd=0x%02X seq=%u bytes=%lu",
cmd, seq, (unsigned long)completed_bytes);
}
}
/**
* @brief 从 DATA_IN batch 头部解析 cmd 字节。
*
* DATA_IN 回调只把 mbuf 拷到 buf,未解析协议头;负 ACK 需要 cmd 字节
* 区分 ESP32/GD32 续传分支,避免误报让前端走错路径。与 worker 中
* ble_ota_process_data_batch 的包头解析保持一致(buf[0] = cmd)。
*
* @param buf 数据缓冲区。
* @param recv_len 数据长度。
* @return cmd 字节;非法长度返回 0(GD32 命令),调用方据此不发误导性 ACK。
*/
static uint8_t parse_cmd_from_batch(const uint8_t *buf, size_t recv_len)
{
// 与 ble_ota_process_data_batch 一致:至少 7 字节头才算合法包
if (recv_len < 7 || buf == nullptr) {
return 0;
}
return buf[0];
}
/**
* @brief 丢包路径负 ACK:发缺口 ACK 让前端按 completed_bytes 续传退避。
*
* 触发于 DATA_IN 回调中 malloc 失败 / 队列满。窗口内包用
* writeValueWithoutResponse(即发即忘),收不到 ATT Error Response,
* 只能走 DATA_OUT 通知通道。负 ACK 复用 sendWindowAck,seq 取
* s_reorder.expected-1(最近连续落盘序号),completed 取实际落盘字节,
* 前端 resumeSeq = ceil(bytes/payloadSize) 续传,无需改客户端。
*
* 节流:丢包数 ≥ NEG_ACK_DROP_WINDOW 或距上次 ≥ NEG_ACK_MIN_INTERVAL_US
* 才发一次,防止重传风暴期间 ACK 通知打爆 NimBLE mbuf 池。
* 状态仅在 NimBLE host task 回调中读写,无需锁。
*
* 仅 ESP32 路径发负 ACK(s_reorder 状态在此文件内可见);GD32 中继转发
* 的缺口反馈由 GD32 侧自行处理,本函数对 GD32 cmd 仅累计不发送。
*/
static void sendNegativeAckIfDue(uint8_t cmd)
{
// 累计丢包(无论是否发送,计数都增长,供可观测性)
s_neg_ack_drop_count += 1;
// 节流判定:丢包数达阈值 或 距上次发送超过最小间隔
const int64_t now_us = esp_timer_get_time();
bool by_count = (s_neg_ack_drop_count >= NEG_ACK_DROP_WINDOW);
bool by_time = ((now_us - s_last_neg_ack_us) >= NEG_ACK_MIN_INTERVAL_US);
// 首次(s_last_neg_ack_us==0)也允许立即发,避免低堆首包丢就憋住
bool first_time = (s_last_neg_ack_us == 0);
if (!(by_count || by_time || first_time)) {
return;
}
// 仅 ESP32 路径发负 ACK;GD32 中继缺口由 GD32 侧反馈,这里只计不发
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
// expected>0:已落盘过包,ackSeq=expected-1,completed=实际落盘字节
// expected==0:首包都没落盘,发 ackSeq=0/completed=0,等价"从头续发"
// (客户端 seq=0 用 write-with-response,本就等首包 ACK,不会误导)
uint16_t ackSeq = (s_reorder.expected > 0U)
? (uint16_t)(s_reorder.expected - 1U)
: 0U;
uint32_t completed = (s_reorder.expected > 0U)
? (uint32_t)Esp32OtaCore::instance().getReceived()
: 0U;
sendWindowAck(cmd, ackSeq, completed);
s_neg_ack_sent += 1;
DEBUG_WARN(TAG, "DATA_IN 负ACK cmd=0x%02X seq=%u bytes=%lu (drop:malloc=%u qfull=%u)",
cmd, ackSeq, (unsigned long)completed,
(unsigned)s_ota_drop_malloc_fail, (unsigned)s_ota_drop_queue_full);
}
// 复位节流状态
s_neg_ack_drop_count = 0;
s_last_neg_ack_us = now_us;
}
/**
* @brief 发送收尾阶段事件通知(开始/完成/失败/重启等)。
* @param stage 阶段标识码。
* @param value 阶段附带值(错误码或耗时等)。
*/
void sendFinishEvent(uint8_t stage, uint32_t value)
{
uint8_t pkt[6] = {0};
pkt[0] = BLE_OTA_NOTIFY_FINISH;
pkt[1] = stage;
pkt[2] = (uint8_t)(value & 0xFFU);
pkt[3] = (uint8_t)((value >> 8) & 0xFFU);
pkt[4] = (uint8_t)((value >> 16) & 0xFFU);
pkt[5] = (uint8_t)((value >> 24) & 0xFFU);
esp_err_t err = BleOta::instance().notifyResponse(pkt, sizeof(pkt));
if (err != ESP_OK) {
DEBUG_WARN(TAG, "发送 FINISH 事件失败: stage=0x%02X err=%s",
stage, esp_err_to_name(err));
}
}
/** @brief 将 session_id 和 code 打包为 32 位 BLE FINISH 事件值。 */
uint32_t packBleFinishValue(uint16_t session_id, uint16_t code)
{
return ((uint32_t)code << 16) | (uint32_t)session_id;
}
/** @brief 将 32 字节 SHA-256 转为大写十六进制字符串(65字节含'\0')。 */
void shaBytesToHexUpper(const uint8_t sha[32], char out_hex[65])
{
if (!sha || !out_hex) return;
for (int i = 0; i < 32; ++i) {
snprintf(&out_hex[i * 2], 3, "%02X", sha[i]);
}
out_hex[64] = '\0';
}
/** @brief 通过 BLE Notify 推送设备端 SHA-256 给前端(用于比对验证)。 */
void sendSha256Notify(const uint8_t sha[32], uint16_t session_id)
{
if (!sha) return;
uint8_t sha_pkt[35] = {0};
sha_pkt[0] = BLE_OTA_NOTIFY_SHA256;
memcpy(&sha_pkt[1], sha, 32);
sha_pkt[33] = (uint8_t)(session_id & 0xFFU);
sha_pkt[34] = (uint8_t)((session_id >> 8) & 0xFFU);
BleOta::instance().notifyResponse(sha_pkt, sizeof(sha_pkt));
}
/** @brief BLE 触发 GD32 中继后等待结果的 FreeRTOS 任务。 */
void ble_gd32_relay_wait_task(void *pvParameters)
{
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)pvParameters;
uint16_t session_id = ctx ? ctx->session_id : 0;
if (ctx) free(ctx);
uint32_t result_msg = (uint32_t)OTA_RESULT_ERR_TIMEOUT;
bool got_result = false;
QueueHandle_t rq = OtaRelay::getResultQueue();
if (rq) {
got_result = (xQueueReceive(rq, &result_msg, pdMS_TO_TICKS(120000)) == pdTRUE);
}
if (!got_result) {
DEBUG_ERR(TAG, "GD32 BLE OTA 等待中继结果超时");
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, (uint16_t)(-OTA_RESULT_ERR_TIMEOUT)));
vTaskDelete(NULL);
return;
}
ota_result_t result = (ota_result_t)((int32_t)result_msg);
if (result == OTA_RESULT_OK) {
DEBUG_INFO(TAG, "GD32中继完成 stage=0x11 sess=%u", session_id);
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_DONE,
packBleFinishValue(session_id, 0));
} else {
uint16_t code = (uint16_t)(result < 0 ? -result : result);
DEBUG_ERR(TAG, "GD32 BLE OTA 中继失败: result=%d", (int)result);
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
packBleFinishValue(session_id, code));
}
vTaskDelete(NULL);
}
// ============================================================
// GATT 访问回调
// ============================================================
/*
* CTRL (0x0001) - Write
* 写入 0xF7 触发 OTA 中继
*/
/**
* @brief GATT CTRL 特征访问回调,处理控制命令写入。
* @param conn_handle BLE 连接句柄。
* @param attr_handle GATT 属性句柄。
* @param ctxt GATT 访问上下文。
* @param arg 回调用户参数。
* @return NimBLE ATT 错误码,0 表示成功。
*/
int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
(void)conn_handle;
(void)attr_handle;
(void)arg;
if (BleOta::instance().isSuspendedForWifiOta()) {
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
}
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
}
// 使用较大静态缓冲区:writeValueWithoutResponse 快速发送时,
// NimBLE 可能将多个写请求合并到同一个 mbuf 链中
// ★ static: NimBLE host task 栈只有 4096 字节,不能用栈分配
// ★ 4096 字节可容纳 8 个合并包(8×491=3928),避免截断丢包
static constexpr size_t BLE_OTA_BUF_SIZE = 4096;
static uint8_t buf[BLE_OTA_BUF_SIZE];
int recv_len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
if (recv_len < 8) {
DEBUG_WARN(TAG, "DATA_IN 数据太短: %d 字节 (最少 8: 7头+1数据)", recv_len);
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
// 将数据拷贝到堆上并送入工作队列,避免在 NimBLE host task 中执行
// 耗时的 flash 操作(esp_ota_begin / esp_ota_write)导致 BLE 连接断开。
if (s_otaDataQueue) {
// 解析 cmd 字节供负 ACK 区分 ESP32/GD32 续传分支(与 worker 解析一致)
uint8_t cmd = parse_cmd_from_batch(buf, (size_t)recv_len);
uint8_t *heap_buf = (uint8_t *)malloc((size_t)recv_len);
if (heap_buf) {
memcpy(heap_buf, buf, (size_t)recv_len);
BleOtaDataBatch item = { heap_buf, (size_t)recv_len };
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
// 队列满:发缺口 ACK 让前端按 completed_bytes 续传退避,
// 而非等满 8s 窗口超时后整窗口重发 → 重传风暴叠加低堆二次失败
s_ota_drop_queue_full += 1;
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
free(heap_buf);
sendNegativeAckIfDue(cmd);
}
} else {
// malloc 失败:同上,发缺口 ACK 形成可恢复协议反馈,避免静默丢包
s_ota_drop_malloc_fail += 1;
DEBUG_ERR(TAG, "DATA_IN 堆内存不足 (%d 字节),丢弃", recv_len);
sendNegativeAckIfDue(cmd);
}
}
return 0;
}
/**
* @brief 处理单个 ESP32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
*/
static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
const uint8_t *buf, size_t data_offset, size_t data_len,
uint32_t expected_payload)
{
if (seq == 0) {
if (total_size == 0) {
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return true;
}
// 活跃 BLE 会话中的 seq=0 一律按重传处理。窗口 0 的 ACK 丢失时,
// 前端会从累计字节 0 续发;此时重置 OTA 会丢掉已缓存的后续包。
// 新上传由 0xFB/断连先清理旧会话,不能用 seq=0 猜测会话边界。
if (Esp32OtaCore::instance().isActive()) {
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
if (curCh != OTA_CHANNEL_BLE) {
DEBUG_WARN(TAG, "ESP32被ch=%d占用,BLE拒抢",
(int)curCh);
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return true;
}
DEBUG_INFO(TAG, "ESP32已激活 seq=0重传,保留reorder");
} 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 true;
}
}
}
// ★ 会话有效性检查:如果 begin() 失败(如互斥拒绝),后续包也应丢弃
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
return false; // 外层推进 offset,继续下一包
}
// ★ 防重复写入保护:如果已接收字节 >= 声明大小,跳过(重试引起的重复包)
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);
return false; // 外层推进 offset,继续下一包
}
// ★ 使用带序号顺序保护的写入:拒绝乱序包并立即 ACK 通知发送端
uint32_t beforeReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
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));
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
return true;
}
// ★ 仅在实际写入新字节时才触发 ACK:重复包/乱序缓存包不写入 flash,
// 若在窗口边界触发 ACK 会携带陈旧字节数,被发送端误判为丢包。
uint32_t afterReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
bool progressMade = (afterReceived > beforeReceived);
// 完成判定:以实际按序写入字节数为准。
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
bool transferComplete = (total_size > 0 && afterReceived >= total_size);
// ACK 按累计连续进度触发,而不是按当前到达包的 seq 触发。缺口包到达后
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
//
// ★ seq=0 强制 ACK:begin 后首包需尽快 ACK,前端才能继续窗口。
// 前端必须等“首包已落盘”ACK 后再发窗口内其余包,否则队列满丢包→重传极慢。
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
BLE_OTA_CHUNK_PAYLOAD;
bool reachedAckBoundary = completedPackets > 0U &&
(completedPackets % BLE_OTA_ACK_WINDOW) == 0U;
bool firstPacketReady = (seq == 0U) && progressMade;
if (progressMade &&
(reachedAckBoundary ||
transferComplete ||
isLastPacket ||
firstPacketReady)) {
if (isLastPacket || transferComplete) {
DEBUG_DBG(TAG, "ESP32尾包 s=%u n=%zu rx=%u tot=%" PRIu32,
seq, data_len, (unsigned int)afterReceived, total_size);
} else if (firstPacketReady) {
DEBUG_DBG(TAG, "ESP32 begin 就绪 ACK: seq=0 received=%u total=%" PRIu32,
(unsigned)afterReceived, total_size);
}
uint16_t ackSeq = s_reorder.expected > 0U
? (uint16_t)(s_reorder.expected - 1U)
: seq;
sendWindowAck(cmd, ackSeq, afterReceived);
}
if (transferComplete) {
DEBUG_INFO(TAG, "ESP32收完 %u/%" PRIu32 "B chunks=%u",
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq;
ble_ota_restore_wifi_ps();
}
return false;
}
/**
* @brief 处理单个 GD32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
*/
/** GD32 路径硬失败:与 WiFi abortCurrentIfEpoch 对齐,复位协调器 + 本通道 RAM meta。 */
void ble_gd32_fail_abort(const char *why)
{
DEBUG_ERR(TAG, "GD32 OTA 中止: %s", why ? why : "?");
OtaCoordinator::instance().abortCurrent();
s_ble_gd32_meta = {};
s_ble_gd32_meta_active = false;
s_gd32_ooo_count = 0;
ble_ota_restore_wifi_ps();
}
static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
const uint8_t *buf, size_t data_offset, size_t data_len,
uint32_t expected_payload)
{
if (seq == 0 && total_size > 0) {
// 与 ESP32 BLE 路径一致:活跃会话中的 seq=0 视为首包重传,禁止再次 prepare
// (否则会 erase 分区、清 meta,毁掉已写入进度)。
if (s_ble_gd32_meta_active && s_ble_gd32_meta.total_size == total_size) {
if (s_ble_gd32_meta.stored_size > 0) {
DEBUG_INFO(TAG, "GD32已激活 seq=0重传 skip stored=%" PRIu32,
s_ble_gd32_meta.stored_size);
sendWindowAck(cmd, 0, s_ble_gd32_meta.stored_size);
return false;
}
DEBUG_INFO(TAG, "GD32未落盘 seq=0重传首块(不prepare)");
// fall through:stored_size==0 时重写 offset 0
} else {
// 与 WiFi 共用 prepareGd32Storage:validate + begin + erase + clearMeta + saveMeta
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(
total_size, OTA_CHANNEL_BLE);
if (prep_err == ESP_ERR_INVALID_SIZE) {
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " max=%zu",
total_size, OTA_MAX_FIRMWARE_SIZE);
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
return true;
} else if (prep_err == ESP_ERR_INVALID_STATE) {
DEBUG_ERR(TAG, "GD32 OTA 协调器拒绝存储(系统忙)");
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return true;
} else if (prep_err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return true;
}
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
total_size, data_len);
s_ble_gd32_meta = {};
s_ble_gd32_meta.total_size = total_size;
s_ble_gd32_meta_active = true;
ble_ota_boost_wifi_ps();
if (BleOta::instance().m_bleConnected &&
BleOta::instance().m_connHandle != 0xFFFFU) {
(void)ble_ota_request_conn_params(BleOta::instance().m_connHandle, false);
}
}
}
if (!s_ble_gd32_meta_active || s_ble_gd32_meta.total_size == 0) {
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
return true;
}
ota_meta_t &meta = s_ble_gd32_meta;
// ── 顺序检查:GD32 Flash 必须按序写入,无 reorder buffer ──
// 改进:乱序/重复包不再中止整个 batch,而是丢弃当前包 + 节流 ACK 报告缺口,
// 让前端尽快知道设备端实际进度(last_seq)并重传缺口。
// 策略与 ESP32 fallback 路径一致(参考 esp32_write_ordered 行 459-477)。
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
// 重复包:seq <= last_seq(已写入过)
// ★ 重复包静默跳过,不发送 ACK:发送端已持有上一窗口的有效 ACK 进度,
// 重复包的 ACK 会携带未增长的字节数,被发送端误判为当前窗口丢包。
if (seq <= meta.last_seq) {
DEBUG_DBG(TAG, "GD32 重复包: seq=%u <= last_seq=%" PRIu32 ",跳过",
seq, meta.last_seq);
return false; // 外层推进 offset,继续下一包
}
// 乱序包:seq > last_seq + 1(缺口)
// 节流策略:每 BLE_OTA_ACK_WINDOW 个乱序包才发一次 ACK,或在窗口边界处发 ACK。
// 避免大量乱序包涌入时触发 ACK 风暴,耗尽 NimBLE mbuf 池导致 ESP_ERR_NO_MEM。
s_gd32_ooo_count++;
bool shouldAck =
(s_gd32_ooo_count >= BLE_OTA_ACK_WINDOW) ||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
if (shouldAck) {
DEBUG_WARN(TAG, "GD32乱序 s=%u last=%" PRIu32 " ACK=%" PRIu32 " ooo=%u",
seq, meta.last_seq, meta.stored_size, (unsigned)s_gd32_ooo_count);
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
s_gd32_ooo_count = 0;
} else {
DEBUG_DBG(TAG, "GD32乱序 s=%u last=%" PRIu32 " 抑ACK ooo=%u",
seq, meta.last_seq, (unsigned)s_gd32_ooo_count);
}
return false; // 外层推进 offset,继续下一包
}
// 顺序包到达,复位乱序计数器
if (s_gd32_ooo_count != 0) {
s_gd32_ooo_count = 0;
}
// --- 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);
return false; // 外层推进 offset,继续下一包
}
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);
ble_gd32_fail_abort("write OOB");
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, 0);
return true;
}
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(flash_offset, &buf[data_offset], data_len);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
seq, flash_offset, esp_err_to_name(err));
ble_gd32_fail_abort(esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)err);
return true;
}
uint32_t stored_size = flash_offset + (uint32_t)data_len;
meta.stored_size = stored_size;
meta.last_seq = seq;
const bool transfer_complete = stored_size >= meta.total_size;
// seq=0:prepare 后写完首包必须 ACK,前端才能继续发后续窗口
const bool checkpoint =
(seq == 0U) ||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
transfer_complete ||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD);
// NVS commit 是毫秒级操作,不能每 480 字节执行一次。仅在 ACK 窗口边界
// 和尾包持久化;正常完成时尾包一定落盘,后续 SHA/中继读取到的语义不变。
if (checkpoint) {
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 元数据 checkpoint 失败 (seq=%u): %s",
seq, esp_err_to_name(err));
ble_gd32_fail_abort(esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)err);
return true;
}
sendWindowAck(cmd, seq, stored_size);
}
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
seq, flash_offset, stored_size);
// --- 检查 GD32 固件是否接收完毕 ---
if (transfer_complete) {
DEBUG_INFO(TAG, "GD32收完 %" PRIu32 "/%" PRIu32 "B chunks=%u",
meta.stored_size, meta.total_size, seq + 1);
s_ble_gd32_meta_active = false;
ble_ota_restore_wifi_ps();
// 与 WiFi 共用 finishGd32StorageAfterReceive:complete + SHA
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 SHA失败:%s", esp_err_to_name(sha_err));
}
}
return false;
}
/**
* @brief 处理一批 OTA 数据包(在工作线程上下文中调用)。
* 包含 ESP32 和 GD32 的数据接收逻辑。
* @param buf 数据缓冲区(包含一个或多个合并的包)。
* @param recv_len 数据长度。
*/
void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
{
// 循环处理 mbuf 中的所有完整数据包
size_t offset = 0;
while (offset + 7 <= recv_len) {
// 解析包头
uint8_t cmd = buf[offset + 0];
uint16_t seq = (uint16_t)(buf[offset + 1] | (buf[offset + 2] << 8));
uint32_t total_size = (uint32_t)(buf[offset + 3] | (buf[offset + 4] << 8) |
(buf[offset + 5] << 16) | (buf[offset + 6] << 24));
size_t data_offset = offset + 7;
// 根据包序号和固件总大小计算期望 payload,而非依赖 recv_len
// 这样即使 NimBLE mbuf 合并导致缓冲区截断,也不会误判包长度
uint32_t expected_payload = (total_size > 0)
? ((uint32_t)BLE_OTA_CHUNK_PAYLOAD < (total_size - (uint32_t)seq * (uint32_t)BLE_OTA_CHUNK_PAYLOAD)
? (uint32_t)BLE_OTA_CHUNK_PAYLOAD
: (total_size - (uint32_t)seq * (uint32_t)BLE_OTA_CHUNK_PAYLOAD))
: (uint32_t)BLE_OTA_CHUNK_PAYLOAD;
size_t data_len = (size_t)expected_payload;
size_t remaining_after_header = recv_len - data_offset;
// 如果缓冲区中实际可用数据不足,截断 data_len
if (remaining_after_header < expected_payload) {
if (remaining_after_header == 0) {
// 没有更多数据,退出循环
break;
}
// 缓冲区截断,使用实际可用数据
data_len = remaining_after_header;
}
// 当前包总长度: header(7) + payload(data_len)
size_t packet_len = 7 + data_len;
DEBUG_DBG(TAG, "DATA_IN c=0x%02X s=%u tot=%" PRIu32 " n=%zu",
cmd, seq, total_size, data_len);
// 分发到对应通道的包处理器
bool abort_batch = false;
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
abort_batch = process_esp32_packet(cmd, seq, total_size,
buf, data_offset, data_len, expected_payload);
} else if (cmd == BLE_OTA_CMD_GD32_DATA) {
abort_batch = process_gd32_packet(cmd, seq, total_size,
buf, data_offset, data_len, expected_payload);
} else {
DEBUG_WARN(TAG, "DATA_IN 未知 cmd: 0x%02X offset=%zu", cmd, offset);
}
if (abort_batch) {
return;
}
// 推进到下一个包
offset += packet_len;
// 检查剩余数据是否足够组成一个完整的包头
if (offset + 7 > recv_len) break;
}
}
/**
* @brief OTA 数据工作线程:从队列接收数据并调用处理函数。
* 独立于 NimBLE host task,避免 flash 操作阻塞 BLE 协议栈。
*/
void ble_ota_data_worker_task(void *pvParameters)
{
(void)pvParameters;
BleOta::instance().m_workerTaskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO(TAG, "OTA 数据工作线程已启动");
while (1) {
BleOtaDataBatch item;
if (xQueueReceive(s_otaDataQueue, &item, portMAX_DELAY) == pdTRUE) {
if (!item.data || item.len == 0) {
free(item.data);
continue;
}
// 若 BLE 已断开或 WiFi OTA 挂起,丢弃队列中的残留数据
if (!BleOta::instance().isConnected() ||
BleOta::instance().isSuspendedForWifiOta()) {
DEBUG_DBG(TAG, "BLE不可用 丢%zuB队列", item.len);
free(item.data);
continue;
}
ble_ota_process_data_batch(item.data, item.len);
free(item.data);
}
}
}
/*
* VERSION (0x0004) - Read
* 返回固件版本字符串
*/
/**
* @brief GATT VERSION 特征访问回调,返回固件版本字符串。
* @param conn_handle BLE 连接句柄。
* @param attr_handle GATT 属性句柄。
* @param ctxt GATT 访问上下文。
* @param arg 回调用户参数。
* @return NimBLE ATT 错误码,0 表示成功。
*/