Files
ESP32_COMM/.zcode/plans/plan-sess_d0b3f2a2-ff3f-4a86-9a4d-b7534d876929.md
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

7.7 KiB
Raw Blame History

目标

把 BLE OTA DATA_IN 路径的 OOM/队列满从“静默丢包”变为“可恢复协议反馈”,让前端在一个窗口内收到缺口反馈并按 completed_bytes 续传退避,避免整窗口 8s 超时重发风暴。仅做方案 A(负 ACK 反馈),不上固定缓冲池;节流复用现有窗口节流思路。

现状核对(已核实)

  • 平台 ESP32-C3,无 PSRAM,堆紧张;稳态堆水位需实测后才能评估是否上池,故本计划不涉及池。
  • ble_ota_data.cpp:597 malloc(recv_len) 失败仅 DEBUG_ERR,回调仍返回 0(ATT 成功)。
  • ble_ota_data.cpp:602 xQueueSend 失败同样静默:free(heap_buf) + DEBUG_WARN("...丢...等重传"),无协议反馈。两条路径都需修,不只 malloc 路径。
  • 客户端窗口内 seq≥1 用 writeValueWithoutResponse(即发即忘),收不到 ATT Error Response;只有 seq=0 用 writeValueWithResponse。因此“返回 ATT 错误码”对窗口内丢包无效,必须靠 DATA_OUT 通知通道发窗口 ACK。原报告“返回 ATT 错误”建议在窗口路径不成立,本计划以通知 ACK 为主。
  • 客户端 transfer.js 已支持按 completed_bytes 续传(resumeSeq = ceil(lastAckBytes / payloadSize),最多 3 次重试,窗口 64 包),所以设备端多发一个缺口 ACK 不会破坏客户端语义,只会让续传更早开始。
  • 现有 sendWindowAck(cmd, seq, completed_bytes) 在 ble_ota_data.cpp:440 已封装好,seq 取 s_reorder.expected - 1(最近连续落盘序号),负 ACK 直接复用即可,无需新协议字段、无需改客户端。
  • 现有节流是“进度触发 + 每 BLE_OTA_ACK_WINDOW(64) 包边界”(见 process_esp32_packet:691-712),不是时间间隔。丢包路径发生在写入前、无进度,无法复用边界判定,需为负 ACK 引入轻量节流(避免重传叠加时 ACK 通知打爆 NimBLE mbuf 池)。

改动点(集中在 main/server/ble_ota_data.cpp)

1. 新增丢包计数(可观测性,满足“池耗尽/丢包有计数”验收)

  • 在文件静态区新增:
    • static uint32_t s_ota_drop_malloc_fail = 0; // malloc 失败累计
    • static uint32_t s_ota_drop_queue_full = 0; // 队列满累计
    • static uint32_t s_neg_ack_sent = 0; // 负 ACK 发送累计
  • 在 ble_ota_session_reset()(已有复位点,约 line 414)中清零,与 reorder 状态同生命周期。
  • 通过现有 health monitor 或 DEBUG_DBG 暴露(按现有日志风格,不新增上报通道)。

2. 新增负 ACK 节流状态

  • 复用“每 64 包/最小间隔”思路,但丢包路径无 completedPackets,故用包计数 + 最小时间间隔双约束:
    • static uint32_t s_neg_ack_count = 0; // 自上次发负 ACK 以来累计丢包数
    • static int64_t s_last_neg_ack_us = 0; // 上次负 ACK 时刻(esp_timer_get_time())
  • 节流规则(在丢包路径触发):
    • 累计丢包 ≥ BLE_OTA_ACK_WINDOW(64) 或 距上次 ≥ 200ms 时,发一次负 ACK 并复位计数/时间。
    • 否则只计数、不发,避免重传风暴期间 ACK 通知打爆 mbuf 池。
  • 常量定义在文件内 static constexpr(与 BLE_OTA_BUF_SIZE 同位置,约 line 580),不污染 app_config.h。
  • 时间用 esp_timer_get_time()(已是项目内惯用 API,见 reorder/health 用法)。

3. 新增负 ACK 发送辅助函数

  • 在 sendWindowAck(line 460 后)下方新增:
    // 丢包路径专用:发缺口 ACK 让前端按 completed_bytes 续传退避,而非等 8s 超时。
    // 节流:每 BLE_OTA_ACK_WINDOW 个丢包或 200ms 最多一次,防止 ACK 通知打爆 mbuf 池。
    static void sendNegativeAckIfDue(uint8_t cmd);
    
  • 实现:
    • 读取 s_reorder.expected:>0 时 ackSeq = expected - 1,completed = Esp32OtaCore::getReceived();expected==0(首包都没落盘)时 ackSeq=0、completed=0(等价于让前端从头续发,客户端已支持此语义)。
    • 节流判定通过后调用 sendWindowAck(cmd, ackSeq, completed) 并 s_neg_ack_sent++。
    • GD32 路径同样适用(sendWindowAck 已按 cmd 区分),但 GD32 的 expected 状态在 s_ble_gd32_meta,需在函数内按 cmd 分支取 expected/received;为简单起见,先实现 ESP32 路径,GD32 路径仅计数不发 ACK(GD32 是中继转发,缺口由 GD32 侧自行反馈),并在注释中标注。

4. 改造 ble_ota_data_in_access 丢包路径(line 596-605)

  • malloc 失败分支:s_ota_drop_malloc_fail++ → sendNegativeAckIfDue(cmd)(cmd 从 batch 头解析,见下)→ 保留 DEBUG_ERR。
  • 队列满分支:s_ota_drop_queue_full++ → sendNegativeAckIfDue(cmd) → 保留 DEBUG_WARN。
  • cmd 解析问题:当前 line 566-596 的回调只把 mbuf 拷到 buf,未解析 cmd。负 ACK 需要 cmd 字节。两种做法:
    • (a) 回调里解析前 7 字节头取 cmd(与 worker 中 ble_ota_process_data_batch 解析逻辑一致,可抽一个小 helper parse_cmd_from_batch(buf, recv_len))。
    • (b) 不解析,统一用 BLE_OTA_CMD_ESP32_DATA(0x01)(C3 自身 OTA 是主路径,GD32 经 BLE 中继较少)。
    • 选 (a):抽 helper,正确性优先,避免误报 cmd 让前端续传走错分支。helper 放在 ble_ota_data.cpp 内 static,不暴露到头文件。

5. 不改动的部分

  • 回调返回值仍为 0(窗口内 write-without-response 返回错误对端也收不到,且会污染 ATT 层;负 ACK 走通知通道才是正反馈)。
  • 不引入固定缓冲池(C3 无 PSRAM,池规模需实测,本计划范围内不做)。
  • 不改客户端 tools/js/ble/*(现有续传语义已兼容)。
  • 不改协议字段/UUID/通知格式。

风险与权衡

  • 负 ACK 抖动:极端低堆时丢包密集,节流会让部分丢包延迟反馈,客户端可能仍等部分超时。权衡已用 200ms 兜底,可接受;若实测仍风暴,再调常量。
  • 首包未落盘时负 ACK:expected==0 时发 ackSeq=0, completed=0,等价“一切重来”。客户端 seq=0 用 write-with-response,本就等首包 ACK;此时负 ACK 不会误导。
  • GD32 路径:本计划 GD32 仅计数不发负 ACK,避免改动 GD32 中继反馈语义;如需对称可后续扩展。
  • 节流状态并发:s_neg_ack_* 仅在 NimBLE host task 回调中读写(单线程),worker task 不写,无需锁。

验收

  1. 构造低堆注入:临时在 malloc 前插 if (s_inject_fail) { heap_buf=nullptr; } 开关(测试桩,提交前移除),或用 heap_caps_set_minimum_size 压低堆水位。
  2. 触发 OTA 上传,观察:
    • 日志出现 DATA_IN 堆内存不足 / DATA_IN满 + sendNegativeAckIfDue 发送记录。
    • 客户端在窗口内(≤200ms~8s)收到缺口 ACK,lastAckBytes 推进,从 resumeSeq 续传,不出现整窗口 8s 超时。
    • s_ota_drop_* 计数器增长,负 ACK 后传输恢复,无永久卡死。
  3. 正常路径回归:不注入失败时 ACK 行为、传输耗时、收尾 SHA256 不变。
  4. 池耗尽有计数:s_ota_drop_queue_full / s_ota_drop_malloc_fail 在 health monitor/日志可查。

实施顺序

  1. 加丢包计数 + ble_ota_session_reset 清零。
  2. 加负 ACK 节流状态 + sendNegativeAckIfDue(ESP32 路径)。
  3. 抽 parse_cmd_from_batch helper。
  4. 改造 ble_ota_data_in_access 两条丢包路径接入负 ACK。
  5. 低堆注入测试验收,移除注入桩。
  6. (可选)GD32 路径对称负 ACK,视后续需要。

涉及文件

  • main/server/ble_ota_data.cpp(主要改动:line 414 reset、line 460 后新增函数、line 596-605 丢包路径、line 580 附近常量)
  • 不改:ble_ota_priv.h、ble_ota.cpp、app_config.h、客户端 JS。