复现与日志基线
+冷启动抓 5 分钟串口日志,同时记录路由器关联和 DHCP 日志,确认失败发生在关联前、关联后取 IP、还是运行中断线。
+ +diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..906f61f --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev-server", + "runtimeExecutable": "python", + "runtimeArgs": ["-m", "http.server", "8080"], + "port": 8080, + "autoPort": true + }, + { + "name": "tools-server", + "runtimeExecutable": "python", + "runtimeArgs": ["-m", "http.server", "8080", "--directory", "tools"], + "port": 8080, + "autoPort": true + } + ] +} diff --git a/README.md b/README.md index 2e5d4d0..c32733b 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ ESP32_COMM/ |------|------|------| | 1 | `begin(total_size)` | 选择非当前 OTA 分区,获取全局互斥,`esp_ota_begin` | | 2 | `write(data, len)` | 循环调用 `esp_ota_write`(WiFi 通过 HTTP 流,BLE 通过 GATT 块) | -| 3 | `finalize(sha_hex, reboot)` | `esp_ota_end` → 从分区读回计算 SHA-256 → 比对前端 SHA → `esp_ota_set_boot_partition` → 可选重启 | +| 3 | `finalize(sha_hex, reboot)` | 从分区读回计算 SHA-256 → 比对前端 SHA → `esp_ota_end` → `esp_ota_set_boot_partition` → 可选重启 | | - | `abort()` | 中止并清理(释放 mutex + 复位状态) | **SHA-256 校验**: 固件写入完成后从 OTA 分区读回计算,不做增量 SHA。前端与设备端比对一致才 set_boot,否则返回 `ESP_ERR_INVALID_VERSION`。 @@ -272,7 +272,7 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器) - **ACK 窗口**: 每 32 包等待一次 DATA_OUT ACK(带累计字节数验证) - **窗口重传**: ACK 超时或字节数不足时重发整个窗口(最多 3 次) -- **ESP32 收尾**: BLE 传输完成后由前端发 `0xF8 + 32B 文件 SHA-256`,设备端创建异步任务执行 finalize(含 SHA 比对),结果通过 FINISH 事件通知前端 +- **ESP32 收尾**: BLE 传输完成后由前端发 `0xF8 + 32B 文件 SHA-256`,设备端读回 OTA 分区做 SHA 比对,通过后完成 `esp_ota_end`、设置启动分区并重启;FINISH 仅上报完成/重启/错误 - **GD32 触发**: 传输完成后自动发 `0xF7` 触发 SPI 中继 - **合并包**: NimBLE 可能将多个 writeWithoutResponse 合并到同一 mbuf,接收端支持一次解析多个数据包 diff --git a/docs/CODE_REVIEW_REPORT_2026-06-13.html b/docs/CODE_REVIEW_REPORT_2026-06-13.html deleted file mode 100644 index f55735d..0000000 --- a/docs/CODE_REVIEW_REPORT_2026-06-13.html +++ /dev/null @@ -1,970 +0,0 @@ - - -
- - -SpiMaster 是单例但没有任何互斥锁。多个 FreeRTOS 任务直接调用 SPI 方法:ota_relay 任务 (优先级 MAX-2)、wifi_ota 任务 (MAX-3)、health_monitor 任务 (MAX-4)。协议操作是多步非原子的(如 otaTransfer = 发送帧 + 轮询响应,CS在两次传输之间释放),高优先级任务可在协议序列中间抢占。
-SemaphoreHandle_t m_mutex,所有公共方法入口加锁。对于多步协议操作(otaTransfer等),使用统一的锁粒度保护整个请求-响应序列。
- MASTER_KEY 使用 NIST AES 测试向量,SCRAMBLE_CONST 使用简单递增序列,GD32_CHIP_UID 使用 0x00-0x0B。注释标注"需填入实际密钥"但从未替换。
-两个调用路径产生非对齐值:(1) WiFi OTA: gd32_offset 从 httpd_req_recv 返回值累加,最后一个chunk是固件余数;(2) BLE OTA: data_len = recv_len - 11(7字节头+4字节CRC),不保证4字节对齐。esp_partition_write 要求 offset 和 size 均4字节对齐。
-BleOta 和 WifiOta 各自维护独立的 m_esp32OtaActive、m_esp32OtaHandle、m_esp32UpdatePartition,但底层 esp_ota_begin/write/end 操作的是同一个OTA分区。两个通道同时触发时:第二个 esp_ota_begin 会失败或覆盖句柄;esp_ota_write 可能交错写入同一分区;导致固件损坏+变砖。
-XOR校验无法检测:偶数个相同位置的位翻转、字节交换、全零段丢失。固件数据的静默损坏几乎不可检测。
-同一值 0xA5 表示两种完全不同的语义。GD32在OTA窗口回复ACK时,ESP32可能将ACK字节误解析为OTA帧起始,导致协议状态机混乱。
-waitUntilNotWait() 每次调用有 max_wait_rounds 上限,但整个 forwardFirmware() 流程没有全局超时。GD32 持续返回 WAIT 时,单个 SEND_SIZE阶段可挂起 ~78秒,完整流程可挂起数分钟。OTA_RESULT_ERR_USER_ABORT 已定义但从未使用。
-任何网页都可以向 ESP32 发起 OTA 请求。结合占位密钥问题,攻击链完整:恶意网页 → 上传伪造固件 → 通过占位密钥认证 → 烧录恶意GD32固件。
-| # | 严重度 | 文件 | 问题 | 影响 |
|---|---|---|---|---|
| 9 | HIGH | wifi_ota.cpp:121 | WiFi事件回调中调用 esp_wifi_connect() 重连 | 在系统事件任务中调用可能死锁 |
| 10 | HIGH | health_monitor.cpp | healthState 静态变量无锁跨任务读写 | 竞态导致健康判断错误 |
| 11 | HIGH | ota_relay.h:80-84 | 5个静态变量(relayState等)无锁跨任务访问 | 协议状态不一致 |
| 12 | HIGH | wifi_ota.h:33-47 | WifiOta 所有成员 public 且无锁 | HTTP回调与WiFi事件回调竞争 |
| 13 | HIGH | ble_ota.h | BleOta 成员无锁,BLE GATT回调与主任务竞争 | BLE OTA 数据损坏 |
| 14 | HIGH | debug_log.cpp:296 | log() 栈分配 msg[512] + log_buf[600] = 1112字节 | 低栈任务中栈溢出 |
| 15 | HIGH | wifi_ota.cpp:1008 | spi_send_handler 栈分配 1728+ 字节 | HTTP server栈仅8KB,接近溢出 |
| 16 | HIGH | ota_relay.cpp:456 | sendNonceAndAuthenticate() nonce 仅用 esp_random() 一次 | 认证会话密钥可预测 |
| 17 | HIGH | ota_relay.h:25 | ota_meta_t.checksum[32] 从未填充/验证 | 固件完整性无保障 |
| 18 | HIGH | wifi_ota.cpp:429-444 | ESP32 OTA 完成→重启,无回滚验证 | 新固件若无法启动将变砖 |
| 19 | HIGH | spi_master.cpp:258 | syncTransfer/recv 忽略 timeout_ms 参数 | SPI事务可能无限等待 |
| 20 | HIGH | wifi_ota.cpp:599-629 | esp_ota_write 失败后 break,未调用 esp_ota_abort | OTA分区处于不一致状态 |
| 21 | HIGH | ble_ota.cpp:439-444 | BLE OTA 帧预留4字节CRC但减去计算不匹配 | 最后几个字节被误截断 |
| 22 | HIGH | spi_master.h | SPI仅4线,无硬件流控/就绪信号 | 高速传输时GD32来不及处理 |
项目中有 5个静态全局变量 (relayState, relaySeq, relayTotalPackets, gd32AppAlive, gd32Version) 和 大量public成员 (WifiOta/BleOta所有状态),跨越3+个FreeRTOS任务和多个ISR/callback上下文访问,没有任何一个被mutex/atomic/critical section保护。这是系统性问题,不是个别bug。
-SpiMaster 单例是唯一的SPI通信通道,但没有任何仲裁机制。WiFi/BLE/健康监控/OTA中继四个功能模块各自直接调用,协议级操作(请求-响应对)不是原子的。需要从架构层面引入SPI总线仲裁器。
-认证握手存在但密钥硬编码→形同虚设;固件校验字段存在但未填充→形同虚设;CORS开放→任何人可触发OTA。安全功能"看起来有"但实际上不提供保护,这种"安全假象"比没有安全功能更危险,因为它给了虚假的信心。
-| 优先级 | 修复项 | 工作量 | 紧急度 | 进度 |
|---|---|---|---|---|
| P0 | -添加 SPI 总线互斥锁 | -小 | -极高 — 生产环境必崩 | -- |
| P0 | -替换硬编码密钥为安全存储 | -中 | -极高 — 安全漏洞 | -- |
| P0 | -Flash 写入对齐检查 | -小 | -极高 — 数据损坏 | -- |
| P1 | -添加全局 OTA 超时+中止机制 | -中 | -高 — 设备挂死 | -- |
| P1 | -ESP32 OTA 添加回滚验证 | -中 | -高 — 变砖风险 | -- |
| P1 | -替换 XOR 为 CRC-8/16 校验 | -小 | -高 — 固件损坏 | -- |
| P1 | -消除 0xA5 值碰撞 | -极小 | -高 — 协议混乱 | -- |
| P2 | -添加 OTA 单源互斥 (BLE/WiFi) | -中 | -中高 | -- |
| P2 | -为所有跨任务状态添加锁/atomic | -大 | -中高 | -- |
| P2 | -修复 WiFi 事件回调中的重连调用 | -小 | -中 | -- |
| P3 | -修复大栈分配问题 | -小 | -中 | -- |
| P3 | -限制 CORS 来源 | -小 | -中 | -- |
| P3 | -实现 ota_meta_t 校验填充与验证 | -中 | -中 | -- |
| 原始严重度 | 问题描述 | 驳回原因 |
|---|---|---|
| CRITICAL | -SPI DMA缓冲区栈分配导致数据损坏/硬Fault | -ESP-IDF 6.0 的 spi_master 驱动内置 setup_dma_priv_buffer() 函数自动检测并对齐DMA缓冲区,栈分配仅导致性能损失,不引发数据损坏 |
-
| HIGH | -TOCTOU: relayState 检查后 reset() 无锁 | -虽然模式存在,但当前代码流程中检查和重置在同一线程同步执行,实际竞态窗口极小,风险被高估 | -
+ 面向长时间断电后无法连接 STA 的问题,按“复现证据 → 最小诊断 → P0 修复 → 运行期恢复 → 验证矩阵”的顺序推进,避免一次性大改掩盖真实根因。 +
+ +先处理能直接解释“长时间断电后首连失败”和“失败后不恢复”的路径。
+| 优先级 | +根因 | +位置 | +影响 | +
|---|---|---|---|
| P0 | +冷启动擦 PHY 校准 + 仅 500ms 延迟 | +wifi_ota.cpp L1524-L1531, L340-L342 |
+ 全量 PHY 校准可能需要更长时间,反复擦除会让首连更不稳定。 | +
| P0 | +重试耗尽标志运行中不恢复 | +wifi_ota.cpp L196-L208 |
+ m_staRetryExhausted=true 后可能长期阻止后续重连。 |
+
| P1 | +listen_interval=3 偏短 |
+ wifi_ota.cpp L324 |
+ 长时间空闲或 AP 策略严格时,STA 更容易被驱逐。 | +
| P1 | +无 IP_EVENT_STA_LOST_IP 处理 |
+ wifi_ota.cpp L228-L242 |
+ 设备可能已经无 IP,但内部仍认为已连接。 | +
| P1 | +主循环无 WiFi 断线看门狗 | +wifi_ota.cpp L1679-L1707 |
+ 运行期断线后缺少强制恢复策略。 | +
| P2 | +断线原因码未解码 | +wifi_ota.cpp L201 |
+ 日志只有数字,难以区分认证、AP 不存在、握手超时、AP 驱逐。 | +
| P2 | +SpiMaster mutex 空指针风险 | +spi_master.cpp L288-L290 |
+ 先 lock 空 mutex 再创建 mutex,属于明确初始化顺序问题。 | +
| P2 | +NVS 初始化错误处理不完整 | +wifi_ota.cpp L1492-L1508 |
+ 只处理两个错误码,其他 NVS 异常缺少统一恢复路径。 | +
本阶段只补充诊断能力和采集日志,不改变核心连接策略。
+复位原因: POWER_ON / BROWNOUT / SOFTWARE / 其他冷启动 是否成立,是否擦除 WiFi NVS 或 PHYSTA 已启动、STA 已关联到 AP、STA 获取 IPSTA 断开 的 reason code 与可读原因STA 重试已耗尽 和是否切换 AP从可观测性开始,再进入 P0/P1 修复,最后做覆盖测试。
+冷启动抓 5 分钟串口日志,同时记录路由器关联和 DHCP 日志,确认失败发生在关联前、关联后取 IP、还是运行中断线。
+ +解码断线 reason,补充 retry、exhausted、heap、uptime 和 WiFi API 返回值日志。
+ +冷启动 PHY 延迟提升到 1500ms;移除无条件 PHY 擦除;重试耗尽增加 30s 冷却恢复机制。
+ +处理 LOST_IP,增大 listen_interval,配置 inactive time,并加入断线 60s 后的 WiFi 看门狗恢复。
+ +ESP-IDF v6.0 全量构建,执行冷启动、路由器晚启动、DHCP 变化、路由器重启、弱信号与 BLE OTA 后恢复测试。
+ +勾选状态保存在当前浏览器 localStorage,便于多次打开继续跟踪。
+每个场景至少保留串口日志;涉及路由器的场景同步保留 AP/DHCP 日志。
+| 场景 | +操作 | +通过标准 | +
|---|---|---|
| 长时间断电后冷启动 | +断电足够久后上电,抓 5 分钟日志,重复 5 次。 | +STA 能连接并获取 IP,不依赖手动重启。 | +
| 路由器晚启动 60-120s | +先启动 ESP32,延迟启动路由器或 WiFi 服务。 | +首次失败后仍能自动恢复 STA。 | +
| DHCP 服务慢或租约变化 | +缩短 DHCP 租约或重启 DHCP 服务。 | +LOST_IP 后标记断线并重新获取 IP。 | +
| 路由器重启 | +ESP32 在线时重启路由器,重复 3 次。 | +断线后自动恢复,HTTP 服务重新可访问。 | +
| WiFi 密码错误 | +临时配置错误密码或错误 SSID。 | +reason 可读,不高频无限重试。 | +
| 弱信号环境 | +拉远距离或降低 AP 发射功率。 | +RSSI、断线原因、恢复动作均可观察。 | +
| BLE OTA 后 WiFi | +执行 BLE OTA 流程后访问 WiFi HTTP API。 | +WiFi 省电模式恢复为 WIFI_PS_NONE。 |
+
| SPI 初始化 | +启动后执行 SPI/CAN 相关 API 或中继流程。 | +无 mutex NULL 风险,无初始化异常。 | +
修复目标是提高恢复能力,同时避免掩盖配置错误或产生高频重试。
+PHY 延迟仅冷启动生效,热启动不受影响。验证时记录冷启动总耗时。
+用 30s 冷却期和看门狗分级恢复控制频率,并保留错误 reason。
+stop/start 仅在断线 60s+ 且多次强制重连失败后执行,此时 HTTP 已不可用。
+设备为有线供电,listen_interval=10 与 inactive time 的稳定性收益优先。
根据本机 ESP-IDF 环境调整串口号和 Python/IDF 命令入口。
+# 冷启动串口日志采集 +python serial_monitor.py + +# 预期输出文件 +G:\Robot_v2\DCU\ESP32_COMM\serial_log.txt+
# ESP-IDF v6.0 构建 +idf.py build + +# 可选:清理后构建 +idf.py fullclean +idf.py build+
以下备选方案暂不采用,除非后续日志推翻当前判断。
+支持 .bin 和 .hex 格式
-