SPI Link v2 Self-Test / selftest (push) Has been cancelled
添加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通信的可靠性和内存使用效率。
7.7 KiB
7.7 KiB
目标
把 BLE OTA DATA_IN 路径的 OOM/队列满从“静默丢包”变为“可恢复协议反馈”,让前端在一个窗口内收到缺口反馈并按 completed_bytes 续传退避,避免整窗口 8s 超时重发风暴。仅做方案 A(负 ACK 反馈),不上固定缓冲池;节流复用现有窗口节流思路。
现状核对(已核实)
- 平台 ESP32-C3,无 PSRAM,堆紧张;稳态堆水位需实测后才能评估是否上池,故本计划不涉及池。
ble_ota_data.cpp:597malloc(recv_len)失败仅DEBUG_ERR,回调仍返回 0(ATT 成功)。ble_ota_data.cpp:602xQueueSend失败同样静默: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解析逻辑一致,可抽一个小 helperparse_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,不暴露到头文件。
- (a) 回调里解析前 7 字节头取 cmd(与 worker 中
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 不写,无需锁。
验收
- 构造低堆注入:临时在 malloc 前插
if (s_inject_fail) { heap_buf=nullptr; }开关(测试桩,提交前移除),或用heap_caps_set_minimum_size压低堆水位。 - 触发 OTA 上传,观察:
- 日志出现
DATA_IN 堆内存不足/DATA_IN满+sendNegativeAckIfDue发送记录。 - 客户端在窗口内(≤200ms~8s)收到缺口 ACK,
lastAckBytes推进,从resumeSeq续传,不出现整窗口 8s 超时。 s_ota_drop_*计数器增长,负 ACK 后传输恢复,无永久卡死。
- 日志出现
- 正常路径回归:不注入失败时 ACK 行为、传输耗时、收尾 SHA256 不变。
- 池耗尽有计数:
s_ota_drop_queue_full/s_ota_drop_malloc_fail在 health monitor/日志可查。
实施顺序
- 加丢包计数 +
ble_ota_session_reset清零。 - 加负 ACK 节流状态 +
sendNegativeAckIfDue(ESP32 路径)。 - 抽
parse_cmd_from_batchhelper。 - 改造
ble_ota_data_in_access两条丢包路径接入负 ACK。 - 低堆注入测试验收,移除注入桩。
- (可选)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。