From ff590d89962bb517948396bcce47263ca6d8f27b Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Tue, 20 Oct 2026 21:13:50 +0800 Subject: [PATCH] =?UTF-8?q?docs(review):=20=E4=B8=93=E5=AE=B6=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=202026-10-19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/project_introduction.html | 7 + .../ESP32_COMM_专家评审_2026-10-19.html | 160 +++ docs/review/README.md | 11 + .../CODE_REVIEW_REPORT_2026-06-13.html | 970 ++++++++++++++++++ .../archive/COMMUNICATION_AUDIT_2026-05-05.md | 74 ++ .../esp32_architecture_review_2026-06-27.md | 301 ++++++ 6 files changed, 1523 insertions(+) create mode 100644 docs/review/ESP32_COMM_专家评审_2026-10-19.html create mode 100644 docs/review/README.md create mode 100644 docs/review/archive/CODE_REVIEW_REPORT_2026-06-13.html create mode 100644 docs/review/archive/COMMUNICATION_AUDIT_2026-05-05.md create mode 100644 docs/review/archive/esp32_architecture_review_2026-06-27.md diff --git a/docs/project_introduction.html b/docs/project_introduction.html index 07c3e60..00571cd 100644 --- a/docs/project_introduction.html +++ b/docs/project_introduction.html @@ -1545,6 +1545,13 @@ python "F:\.espressif\v6.0\esp-idf\tools\idf.py" build +
2026-10-19 复核更新(专家评审,基线 af1f682 / v0.5.2):上方 2026-08-16 四项结论原文保留,复核结果如下—— +
① 零认证:仍存在,且发现 /spi/send 可原样透传任意 SPI 帧类型、timeout_ms 无上限,升级为致命(评审 F-01 / S-12)。 +
② 凭据硬编码:仍存在,且 STA 口令为真实口令、已进入 git 历史,需轮换(S-08)。 +
③ 无固件签名:仍存在;另发现 ESP32 回滚保护取消过早、回滚开关所在 config/sdkconfig.defaults 未被构建引用(S-01 / S-02)。 +
④ AES-ECB:仍存在。 +
“缓解现状”中“nonce 每会话随机绑定防重放”不成立:nonce 仅 4 字节 tick 变化且明文写入日志(S-06)。新增:BLE GATT 无配对/加密(S-09)。 +
完整问题清单、既有评审跟踪与修复路线图见 docs/review/ESP32_COMM_专家评审_2026-10-19.html;历史评审原文已恢复至 docs/review/archive/。

20. 版本演进摘要(0.3.x → 0.5.2)

diff --git a/docs/review/ESP32_COMM_专家评审_2026-10-19.html b/docs/review/ESP32_COMM_专家评审_2026-10-19.html new file mode 100644 index 0000000..0eab1f4 --- /dev/null +++ b/docs/review/ESP32_COMM_专家评审_2026-10-19.html @@ -0,0 +1,160 @@ + +ESP32_COMM 专家评审 2026-10-19
+

ESP32_COMM 专家评审报告

+

评审日期 2026-10-19 · 代码基线 master af1f682(固件 v0.5.2,ESP-IDF v6.0,ESP32-C3FH4)

+

评审方式:多专家视角并行通读 + 主审逐条复核 · 本报告只新增 docs/,未改动任何源码

+

综合评分 5.1 / 10

+ +
+
1致命
+
12严重
+
17一般
+
7建议
+
23既有条目已修复
+
12既有条目仍存在
+
+ + + +

1. 项目概述与架构

+

ESP32_COMM 是机器人 DCU 板上 ESP32-C3 的通信与 OTA 中枢固件:上位机经 WiFi HTTP / BLE GATT 把固件交给 ESP32;ESP32 自身走 app0/app1 AB 分区升级,GD32E508 固件经 SPI Link v2 六阶段握手 + AES-128 逐包加密中继,电机节点固件再经 GD32 CAN 二次中继;同时提供三路 CANFD 控制面与遥测回收。

+
[PC / 手机网页]──WiFi HTTP(22 路由) / BLE GATT(0xFFFF)──▶[ESP32-C3FH4]──SPI Link v2 13.33MHz 5线──▶[GD32E508]──3×CANFD──▶[电机节点]
+                                                         │ gd32_ota 252KB 缓存            │ Bootloader / APP
+                                                         │ app0/app1 AB 自升级            │ 电机 OTA CAN 中继
+
+ + + + +
层目录 / 模块职责规模(行)
接入层server/ wifi_ota · http_handlers · ble_ota(_data/_host)STA/AP、HTTP 路由、NimBLE GATT、窗口 ACK / reorder≈ 6.6k(http_handlers 2832)
协调层ota/ OtaCoordinator · Esp32OtaCore9 态单例、唯一权威入口、150 s 忙态看门狗、epoch≈ 2.0k
执行层relay/ OtaRelay · storage/ Gd32FlashStore · monitor/ MotorOtaRelayGD32 中继状态机、gd32_ota 分区与 NVS meta、电机 OTA 喂数≈ 3.5k
传输层transport/ SpiMaster · protocol · canfd_bridge(+ 仓外 spi_link_v2)Link v2 主站(会话/序号/credit/CRC32/重试)、AES/nonce、CAN 0xC0~0xC8≈ 4.3k
横切monitor/ health · debug_log · can_feedback · cpu_stats · task_stack 等;core/健康门控、三路日志、遥测轮询、启动自检位图≈ 3.6k
+

任务模型:ota_relay(23) · wifi_ota/ble_ota(22) · health_mon(21) · ble_ota_worker(12) · spi_worker(8,唯一持 SPI) · ota_feed(7) · httpd(5) · can_fb(4) · cpu_stats/uart_rx(1)。

+ +

2. 评审范围与方法

+
+ +

3. 问题总表(37 条)

+
编号严重度类别问题主要位置
F-01致命安全HTTP 全部端点零认证 + CORS * + /spi/send 原始透传main/server/http_handlers.cpp:77-83, 1685-1756
S-01严重构建干净克隆构建出的是另一份固件(关键配置文件未生效且自相矛盾)CMakeLists.txt
S-02严重OTA回滚保护取消过早:mark_app_valid 早于自检判定main/core/main.cpp:131-147
S-03严重并发看门狗/中止路径与中继任务并发:清会话密钥但中继任务不停main/ota/ota_coordinator.cpp:625-635
S-04严重OTA忙态看门狗 150 s 短于 GD32 中继合法最坏耗时main/include/app_config.h:126
S-05严重健壮性WiFi 运行期断线重试 5 次后永不重连,也不回退 APmain/server/wifi_ota.cpp:94-100, 180-192, 751-777
S-06严重安全GD32 握手 nonce 可预测且时间戳明文入日志main/relay/ota_relay.cpp:1027-1035
S-07严重健壮性GET /can/mode 查询失败时会把 CAN 接收模式写回 ALL_PASSmain/server/http_handlers.cpp:1020-1030
S-08严重安全凭据与主密钥明文入仓,AP 弱口令main/include/app_config.h:31-32, 39-40, 161-164
S-09严重安全BLE GATT 无配对/加密,范围内任意设备可 OTA 或重启main/server/ble_ota_host.cpp:366-376
S-10严重实时性RSSI 更新覆盖过温 SAFE_SHUTDOWN 健康状态main/monitor/health_monitor.cpp:578-600
S-11严重实时性栈溢出钩子中调用可阻塞的 uart_write_bytesmain/monitor/task_stack.cpp:27-40
S-12严重安全/spi/send 的 timeout_ms 无上限,可长期占死 SPI workermain/server/http_handlers.cpp:1715
G-01一般OTA完成回调不校验状态与 epoch,可能覆盖新会话main/ota/ota_coordinator.cpp:372-392, 568-576
G-02一般并发协调器锁超时 100 ms,lock 失败直接跳过状态切换main/include/app_config.h:124
G-03一般OTA确认 APP 存活前即清除 meta;失败路径连发 3 次 CANCELmain/relay/ota_relay.cpp:1554, 1573-1579, 1637-1643
G-04一般OTA校验缺口:先置 STORED 后算 SHA;ESP32 finalize 可不带 SHA、不比对实收字节main/storage/gd32_flash_store.cpp(finishGd32StorageAfterReceive 275-279 附近)
G-05一般内存OTA 对齐缓冲正常完成后不释放(常驻约 4 KB)main/ota/ota_coordinator.cpp:203, 625-635
G-06一般OTA电机 OTA 与 GD32 OTA 共用分区和 meta,互斥不完整main/server/http_handlers.cpp:2622-2642, 2697-2700, 2748-2776
G-07一般内存JSON 截断输出非法 JSON;motor_state 栈占用约 2.5 KBmain/server/http_handlers.cpp:2383, 2493
G-08一般健壮性输入校验不足与未定义行为main/server/http_handlers.cpp:323, 1056-1061, 1256, 1340-1341, 1521
G-09一般并发suspendStack 跨任务写非原子 BLE 连接状态main/server/ble_ota.cpp:276-279
G-10一般内存SPI 事务超时返回后 DMA 可能仍在使用栈上缓冲main/transport/spi_master.cpp:1001-1019
G-11一般健壮性tick 比较非回绕安全main/transport/spi_master.cpp:883-889, 1443-1445, 1560-1572, 2058-2061, 2156-2163, 2544-2556
G-12一般协议SPI 主站边界缺陷(resp_len 越读、out_init_status 未回填、HELLO 失败后会话不一致)main/transport/spi_master.cpp:175, 622-627, 1692-1697, 2005-2042
G-13一般构建CI 必然失败且不构建固件.github/workflows/
G-14一般可维护性任务数 > 20 时 CPU 统计冻结并每秒刷 WARNmain/monitor/cpu_stats.cpp:106-117
G-15一般安全UDP 日志不可关闭、目标硬编码,fd 关闭存在竞态main/monitor/debug_log.cpp:689-711
G-16一般可维护性日志环满时选择性覆盖导致 /logs 时间序错乱main/monitor/debug_log.cpp:341-352
G-17一般OTAOTA_MAX_FIRMWARE_SIZE 注释数值错误main/include/app_config.h:77
A-01建议构建仓库卫生:gitignore 行内注释失效、被忽略目录仍被跟踪、本机路径入库.gitignore:25,31,89-91
A-02建议文档版本信息与文档漂移main/include/version.h:18-21
A-03建议构建提交信息与内容不符,历史评审被静默删除git 历史 af1f682
A-04建议实时性CS 前 esp_rom_delay_us 忙等最长 1 msmain/transport/spi_master.cpp:946-952
A-05建议实时性间接优先级倒置main/core/main.cpp
A-06建议可维护性死代码与量程不符main/monitor/health_monitor.cpp:389-396, 365
A-07建议可维护性日志格式串与参数不匹配、注释过期main/transport/spi_master.cpp:924-925
+ +

4. 问题详情

致命(1)

致命 F-01 · HTTP 全部端点零认证 + CORS * + /spi/send 原始透传 [安全]

+
位置
main/server/http_handlers.cpp:77-83, 1685-1756
+
现象
22 条路由无任何鉴权;所有响应带 Access-Control-Allow-Origin: *;/spi/send 的 frame_type 直接取自请求(strtol),无白名单,经 SpiMaster::transact 原样下发 GD32,绕过 OtaCoordinator 互斥。
风险
同网段任意主机(或用户浏览的任意网页借 CORS 跨站)可上传/切换固件、重启、发 CAN 帧、向 GD32 注入任意 SPI 命令(含进 Boot、擦写类命令),机器人电机总线可被远程操控。
修改建议
最小改造:① 实施已设计的 Bearer token(NVS 存放,首次配网生成);② CORS 改为白名单来源或去掉;③ /spi/send 加 frame_type 白名单(仅只读诊断类),并在 OTA 忙时拒绝;④ 默认编译期关闭 /spi/send 与 /spi/speedtest。

严重(12)

严重 S-01 · 干净克隆构建出的是另一份固件(关键配置文件未生效且自相矛盾) [构建]

+
位置
CMakeLists.txt;sdkconfig.defaults;config/sdkconfig.defaults:7-13,23,34,89-90;.gitignore:33
+
现象
自定义分区表、4MB Flash、FREERTOS_HZ=1000、APP_ROLLBACK 只写在 config/sdkconfig.defaults,根 CMakeLists 未设置 SDKCONFIG_DEFAULTS,IDF 只读根目录 sdkconfig.defaults;sdkconfig 被 gitignore 未入库。config/ 版本还写着 FLASHMODE_QIO 与 BT_BLUEDROID_ENABLED,与根配置(DIO、NimBLE)冲突。
风险
新环境/CI/同事机器构建得到:单 factory 分区(无 gd32_ota、无 app0/app1,ESP32 自升级不可用)、HZ=100(pdMS_TO_TICKS(1/3/5)=0,SPI 等待退化为零超时/忙让)、无回滚。若有人改为引用 config/ 版本,则 QIO 导致 OTA 读回 SHA 必错。
修改建议
把分区表/Flash 大小/HZ/回滚并入根 sdkconfig.defaults,删除或标注废弃 config/sdkconfig.defaults;代码中加 static_assert(configTICK_RATE_HZ == 1000);CI 增加一次 idf.py build 并校验分区表。

严重 S-02 · 回滚保护取消过早:mark_app_valid 早于自检判定 [OTA]

+
位置
main/core/main.cpp:131-147
+
现象
esp_ota_mark_app_valid_cancel_rollback() 在 SystemBoot::report() 之前无条件调用;随后若 critical_ok 为 false 才 3 秒后 esp_restart()。
风险
新固件关键初始化失败(SPI/WiFi/HTTP 起不来)时已被标记有效,重启后仍运行坏固件,回滚机制形同虚设;在仅能网页升级的现场即“软变砖”。
修改建议
先 SystemBoot::report(),仅 critical_ok 才 mark valid;更稳妥是在 WiFi/HTTP 可达且 SPI L0 通过后(或前端确认后)再 mark,失败时 esp_ota_mark_app_invalid_rollback_and_reboot()。

严重 S-03 · 看门狗/中止路径与中继任务并发:清会话密钥但中继任务不停 [并发]

+
位置
main/ota/ota_coordinator.cpp:625-635;main/relay/ota_relay.cpp:539-554
+
现象
doAbortLocked 在 httpd 任务内、持协调器锁调用 OtaRelay::abortRemoteAndReset(true),其 reset() 清零 m_kSession / m_nonceServer、解除 ring 抑制;而 ota_relay 任务中的 forwardFirmware 没有任何取消标志,仍在用这些数据收发 SPI。OTA_RESULT_ERR_USER_ABORT 从未被触发。
风险
中继后续包以全零密钥加密发往 GD32、两个任务同时发 CANCEL/数据,GD32 半会话残留;与 S-04 叠加时在慢速但合法的中继中可复现。
修改建议
引入 std::atomic<bool> 取消标志 + 事件组:abort 只置标志并等待中继任务自行收尾(带超时),reset() 只能由中继任务自身调用;协调器不在持锁状态下做 1.5 s 级 SPI 操作。

严重 S-04 · 忙态看门狗 150 s 短于 GD32 中继合法最坏耗时 [OTA]

+
位置
main/include/app_config.h:126;main/relay/ota_relay.cpp:361-368
+
现象
OTA_COORD_BUSY_WATCHDOG_MS=150000;传输阶段自身预算上限 120 s,且只从 TRANSFERRING 开始计;进 Boot(2×12 s+HELLO)、握手 20×1.6 s、SIZE_NEG 最多 120×500 ms、FINISH 10 次+APP 存活轮询都在预算之外。
风险
接近满尺寸固件或总线繁忙时,本可成功的中继会被看门狗中止(并触发 S-03 的并发问题),表现为偶发“OTA 卡在 9x% 后失败”。
修改建议
看门狗改为按阶段续期(中继任务每阶段/每 N 包喂狗),或将阈值设为各阶段上限之和 + 裕量;统一由中继任务上报进度时间戳。

严重 S-05 · WiFi 运行期断线重试 5 次后永不重连,也不回退 AP [健壮性]

+
位置
main/server/wifi_ota.cpp:94-100, 180-192, 751-777
+
现象
重连任务达到 WIFI_STA_MAX_RETRY 后 setStaRetryExhausted(true);该标志只在 STA_START/CONNECTED/初始化时复位(156/168/291 行),断线事件在 exhausted 时不再唤醒重连;AP 回退只在开机流程中执行。
风险
路由器重启或短暂断电超过几次重试窗口后,设备永久失联,只能现场断电;HTTP/OTA/CAN 控制全部不可用。
修改建议
耗尽后改为长周期退避(如 30 s~5 min)持续重连,或运行期回退 AP+STA 并行;同时把耗尽状态纳入 /system/status 与健康监控。

严重 S-06 · GD32 握手 nonce 可预测且时间戳明文入日志 [安全]

+
位置
main/relay/ota_relay.cpp:1027-1035;main/transport/protocol.cpp:90-131
+
现象
nonce_server = scrambleGenerate(xTaskGetTickCount(), &m_nonceServer, 0x12345678);种子为常量、地址固定,按字节映射使同一次开机内 nonce 第 4~15 字节恒定,仅 4 字节 tick 变化;tick 以 DEBUG_INFO 明文打印(并经 UDP 广播到局域网)。
风险
会话密钥 K_session = AES(MASTER_KEY, nonce) 的新鲜性只剩 32 bit 且可从日志获知,配合硬编码 MASTER_KEY(S-08)可离线复现会话密钥、重放或伪造 OTA 数据。
修改建议
16 字节全部取自 esp_fill_random()(确保 RF 已开或 bootloader_random_enable);删除 timestamp 日志;scrambleGenerate 仅保留为兼容路径。

严重 S-07 · GET /can/mode 查询失败时会把 CAN 接收模式写回 ALL_PASS [健壮性]

+
位置
main/server/http_handlers.cpp:1020-1030
+
现象
0xC1 查询失败的回退路径调用 canFdSetRxMode(CAN_RX_MODE_ALL_PASS, &actual, 2000) 作为“查询”,这是一次写操作。
风险
前端轮询 GET 一次失败即静默把用户设定的 FILTER / CAN0→1/2 BRIDGE 模式改回全收,电机总线桥接中断,且无日志可见。GET 有副作用也违背 HTTP 语义。
修改建议
增加只读查询命令(或复用 0xC1 重试),失败时返回 {ok:false} 与缓存值,绝不写入。

严重 S-08 · 凭据与主密钥明文入仓,AP 弱口令 [安全]

+
位置
main/include/app_config.h:31-32, 39-40, 161-164;main/server/wifi_ota.cpp:264
+
现象
STA SSID/密码(真实口令)、AP 密码 12345678(WPA/WPA2 混合)、MASTER_KEY = 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10(示例向量)均为源码常量,且已进入 git 历史。
风险
仓库任何读者可接入现场 WiFi 与设备 AP;MASTER_KEY 公开即 GD32 挑战-应答与逐包加密全部失效,可向 GD32 注入任意固件。
修改建议
迁移到 NVS(出厂配置写入)或 Kconfig + 不入库的 sdkconfig;轮换已泄露的 WiFi 口令;每设备派生密钥(MASTER_KEY ⊕ UID KDF);CI 加 gitleaks 扫描。

严重 S-09 · BLE GATT 无配对/加密,范围内任意设备可 OTA 或重启 [安全]

+
位置
main/server/ble_ota_host.cpp:366-376
+
现象
CTRL/DATA_IN 特征仅 BLE_GATT_CHR_F_WRITE / WRITE_NO_RSP,未要求 WRITE_ENC/AUTHEN,未配置 SM。
风险
10~30 m 内任意手机可发 0xFA 重启、0xFB 中止、上传固件触发 GD32 中继。
修改建议
启用 LE Secure Connections(Passkey 或 OOB),关键特征加 BLE_GATT_CHR_F_WRITE_ENC | WRITE_AUTHEN;或至少在 CTRL 上加应用层 token。

严重 S-10 · RSSI 更新覆盖过温 SAFE_SHUTDOWN 健康状态 [实时性]

+
位置
main/monitor/health_monitor.cpp:578-600;main/server/wifi_ota.cpp:853
+
现象
温度与 RSSI 共用 m_healthState;updateFromWifiRssi 只要 next != cur 就 setState(SYS_NORMAL/WARNING/CRITICAL),会把温度路径设置的 SYS_SAFE_SHUTDOWN 改写掉。
风险
过温门控被下一次 RSSI 采样清除,高温下仍允许启动 OTA(Flash 擦写 + 射频满负荷),存在硬件损伤与写坏风险。
修改建议
拆分温度/RSSI 子状态,整体状态取两者最严重值;SAFE_SHUTDOWN 只能由温度路径解除。

严重 S-11 · 栈溢出钩子中调用可阻塞的 uart_write_bytes [实时性]

+
位置
main/monitor/task_stack.cpp:27-40
+
现象
vApplicationStackOverflowHook 运行在 vTaskSwitchContext(调度器临界区)内,却调用带互斥锁、可能阻塞的 uart_write_bytes。
风险
溢出发生时可能在钩子内死锁/二次异常,丢失现场信息,甚至无法走到 esp_system_abort。
修改建议
改用 esp_rom_printf / ets_printf(ROM 直写),或只置标志后立即 abort,由 panic handler 输出任务名。

严重 S-12 · /spi/send 的 timeout_ms 无上限,可长期占死 SPI worker [安全]

+
位置
main/server/http_handlers.cpp:1715;main/transport/spi_master.cpp:646
+
现象
timeout_ms 由 double 直接强转 uint32;submitJob 内再 ×3+10 s,大数值会回绕或得到极长等待。
风险
一次请求即可让 httpd(单线程)与唯一的 spi_worker 长时间阻塞,CAN 反馈、心跳、OTA 全部停摆。
修改建议
限制 timeout_ms ∈ [10, 5000],超范围 400;submitJob 内做饱和运算。

一般(17)

一般 G-01 · 完成回调不校验状态与 epoch,可能覆盖新会话 [OTA]

+
位置
main/ota/ota_coordinator.cpp:372-392, 568-576
+
现象
notifyRelayComplete / notifyEsp32FinalizeComplete 直接设置 COMPLETE/ERROR,不检查当前状态和 epoch。
风险
看门狗中止后迟到的完成回调会把新开始的 GD32_STORING 会话改成 COMPLETE/ERROR。
修改建议
回调携带启动时的 epoch,不一致即丢弃并记日志。

一般 G-02 · 协调器锁超时 100 ms,lock 失败直接跳过状态切换 [并发]

+
位置
main/include/app_config.h:124;main/ota/ota_coordinator.cpp
+
现象
持锁期间存在远端 CANCEL(≤1.5 s)、NVS 擦写、esp_ota_begin 等长操作;其他调用方 100 ms 拿不到锁即放弃,关键通知(中继完成、恢复 CAN 反馈)被丢。
风险
状态卡住直至 150 s 看门狗中止,前端表现为“完成后长时间不恢复”。
修改建议
长操作移出临界区;关键通知改为投递队列由协调器任务串行处理。

一般 G-03 · 确认 APP 存活前即清除 meta;失败路径连发 3 次 CANCEL [OTA]

+
位置
main/relay/ota_relay.cpp:1554, 1573-1579, 1637-1643
+
现象
doFinishAndVerify 在 doWaitForAppAlive 成功前清除 gd32 meta;失败时 3 处各发一次 CANCEL(每次最长 1.5 s)。
风险
GD32 新 APP 起不来时无法用已缓存固件重试中继,必须重新上传;失败收尾最长多耗 4.5 s。
修改建议
APP 存活确认后再清 meta;CANCEL 收敛到单一出口。

一般 G-04 · 校验缺口:先置 STORED 后算 SHA;ESP32 finalize 可不带 SHA、不比对实收字节 [OTA]

+
位置
main/storage/gd32_flash_store.cpp(finishGd32StorageAfterReceive 275-279 附近);main/ota/esp32_ota_core.cpp finalize
+
现象
SHA 计算失败时状态仍为 STORED、checksum 全零(中继时会再算一次兜底);/ota/update 请求体缺少 sha256 或 ≥256B 时以 nullptr 跳过比对,且不检查 m_received == 声明大小。
风险
截断上传可能被当作完整固件写入启动分区(依赖 esp_ota_end 的镜像校验兜底)。
修改建议
SHA 成功后再置 STORED;finalize 强制要求 SHA 并校验字节数;请求体超长时排空并 400。

一般 G-05 · OTA 对齐缓冲正常完成后不释放(常驻约 4 KB) [内存]

+
位置
main/ota/ota_coordinator.cpp:203, 625-635;main/storage/gd32_flash_store.cpp:162-179
+
现象
acquireAlignBufForOta() 在 beginGd32Storage 分配;releaseAlignBufForOta() 只在中止路径调用。
风险
成功完成一次 GD32 上传后约 4 KB 内部 DRAM 常驻,与 0.5.0 “上传缓冲 RAII 省 4 KB” 的目标相抵。
修改建议
在 completeGd32Storage / 中继完成路径同样释放,或用 RAII 绑定会话生命周期。

一般 G-06 · 电机 OTA 与 GD32 OTA 共用分区和 meta,互斥不完整 [OTA]

+
位置
main/server/http_handlers.cpp:2622-2642, 2697-2700, 2748-2776
+
现象
/motor/ota 只检查“当前空闲”,不占用协调器;/motor/ota/relay 不区分 meta 来源也不检查协调器状态;注释“relay 复核 SHA”与“relay 不做 SHA 校验”自相矛盾。
风险
与 BLE 的 GD32 存储并发写同一分区;可能把 GD32 镜像中继给电机节点。
修改建议
电机 OTA 纳入 OtaCoordinator 状态机(新增 MOTOR_STORING/RELAYING),meta 增加镜像类型字段并在中继前校验。

一般 G-07 · JSON 截断输出非法 JSON;motor_state 栈占用约 2.5 KB [内存]

+
位置
main/server/http_handlers.cpp:2383, 2493;main/server/http_util.h
+
现象
http_json_append 截断时钳制在 cap-1,调用方 len>0 判断失效;/motor/state JSON 每条约 290 B,循环仅留 128 B 余量,超过约 10 个电机必然截断;栈上 motor_state_table_t 约 2.5 KB(httpd 栈 7 KB)。
风险
前端解析失败;httpd 栈余量被压缩。
修改建议
改 chunked 逐条输出;大结构改静态或堆分配。

一般 G-08 · 输入校验不足与未定义行为 [健壮性]

+
位置
main/server/http_handlers.cpp:323, 1056-1061, 1256, 1340-1341, 1521
+
现象
double→uint32/uint8 强转无范围检查;readInt 有符号溢出;http_hex_to_bytes 不校验非 hex 字符;POST /can/mode 非法 mode 静默当 ALL_PASS;send_batch 最多 128 帧×80 ms 阻塞 httpd 约 10 s。
风险
异常输入导致错误下发或 httpd 长时间无响应。
修改建议
统一参数解析 helper(范围、类型、hex 校验),非法即 400;批量发送移到后台任务。

一般 G-09 · suspendStack 跨任务写非原子 BLE 连接状态 [并发]

+
位置
main/server/ble_ota.cpp:276-279
+
现象
在 httpd 任务里直接写 m_bleConnected / m_connHandle,与 NimBLE host 任务竞争。
风险
挂起/恢复期间状态错乱,可能在挂起后被断连事件重新开启广播(待实机确认)。
修改建议
通过 ble_npl 事件投递到 host 任务内修改,或改 atomic。

一般 G-10 · SPI 事务超时返回后 DMA 可能仍在使用栈上缓冲 [内存]

+
位置
main/transport/spi_master.cpp:1001-1019
+
现象
spi_device_get_trans_result 重试 1 s 仍超时即返回,栈上的 transaction 与 staging 缓冲可能仍被 DMA 访问。
风险
极端情况下栈数据被改写(use-after-return),表现为随机崩溃。
修改建议
超时后复位 SPI 总线/设备再返回,或改为静态/成员缓冲。

一般 G-11 · tick 比较非回绕安全 [健壮性]

+
位置
main/transport/spi_master.cpp:883-889, 1443-1445, 1560-1572, 2058-2061, 2156-2163, 2544-2556
+
现象
多处 now >= deadline / tick <= deadline 直接比较。
风险
运行约 49.7 天(1 kHz)回绕时,个别等待可能变成极长阻塞。
修改建议
统一改用 xTaskCheckForTimeOut 或 (int32_t)(now - deadline) >= 0。

一般 G-12 · SPI 主站边界缺陷(resp_len 越读、out_init_status 未回填、HELLO 失败后会话不一致) [协议]

+
位置
main/transport/spi_master.cpp:175, 622-627, 1692-1697, 2005-2042
+
现象
copyFoundFrame 写入未截断长度,0x82 统计按其读取 resp_payload[0..5];submitJob 不回填 out_init_status(当前调用方传 nullptr,潜在);ENTER_BOOT 内 HELLO 重同步失败后 m_v2Active=false 且持有 GD32 未知的新 session。
风险
小缓冲调用方越界读;跨任务查询拿不到结果;失败后一段时间内会话校验失效。
修改建议
长度统一截断到 resp_max;补齐输出字段回填;HELLO 失败恢复旧 session 或强制下一帧 sess0。

一般 G-13 · CI 必然失败且不构建固件 [构建]

+
位置
.github/workflows/spi-link-selftest.yml
+
现象
.github/workflows/spi-link-selftest.yml 检查 ../spi_link 不存在即 exit 1,而没有任何步骤提供该目录;workflow 也不执行 idf.py build。另外 Gitea 上是否启用 Actions 未知。
风险
门禁形同虚设,S-01 一类构建问题无法被发现。
修改建议
把 spi_link 独立成仓并以 submodule 引入(同时解决 components/transport 的 ../../../spi_link 仓外依赖),CI 增加 espressif/idf:v6.0 容器构建。

一般 G-14 · 任务数 > 20 时 CPU 统计冻结并每秒刷 WARN [可维护性]

+
位置
main/monitor/cpu_stats.cpp:106-117
+
现象
uxTaskGetSystemState 在数组不足时返回 0 而非截断,快照冻结;常驻约 19 个任务,加按需任务即超限。
风险
/system/status 的 CPU 数据失真,日志被刷屏。
修改建议
数组按 uxTaskGetNumberOfTasks()+裕量动态分配,WARN 节流。

一般 G-15 · UDP 日志不可关闭、目标硬编码,fd 关闭存在竞态 [安全]

+
位置
main/monitor/debug_log.cpp:689-711;main/include/app_config.h:96
+
现象
有 IP 即向 192.168.31.3:6666 发送全部日志(含 S-06 的 nonce 时间戳);一个任务读取 socket fd 后另一任务可能将其关闭,fd 被 httpd 复用时日志会写进 HTTP 连接。
风险
信息泄露;极端情况下污染 HTTP 响应流。
修改建议
Kconfig 开关默认关闭;fd 访问加锁或改单写者任务。

一般 G-16 · 日志环满时选择性覆盖导致 /logs 时间序错乱 [可维护性]

+
位置
main/monitor/debug_log.cpp:341-352
+
现象
满环时跳过 ERR/WARN、覆盖 INFO/DBG 槽位。
风险
排查事故时日志顺序失真。
修改建议
分级双环,或保留序号字段由前端排序。

一般 G-17 · OTA_MAX_FIRMWARE_SIZE 注释数值错误 [OTA]

+
位置
main/include/app_config.h:77
+
现象
216*1024 = 221184 = 0x36000,与 GD32 APP_MAX_SIZE 实际对齐;注释却写 “0x35F90”(=221072)。0627 P0-3 对 0x36000 的换算(221296)同样有误。
风险
维护者据注释误判上限,引发两侧不一致。
修改建议
改为 0x36000 字面量 + static_assert,注释删除错误换算。

建议(7)

建议 A-01 · 仓库卫生:gitignore 行内注释失效、被忽略目录仍被跟踪、本机路径入库 [构建]

+
位置
.gitignore:25,31,89-91;.trae/;.zcode/;.vscode/settings.json
+
现象
行内 # 注释使 *.exe、flasher_args.json、*.workspace、*.suo 规则失效;.trae/(5)、.zcode/(11) 过程文件已在 gitignore 中却仍被跟踪;.vscode 含本机 Windows 路径与 COM15(且写成 Robot_v2\DFU)。
风险
噪声与泄露本机信息。
修改建议
注释另起一行;git rm --cached 过程目录;.vscode 改模板。

建议 A-02 · 版本信息与文档漂移 [文档]

+
位置
main/include/version.h:18-21;README.md
+
现象
GIT_HASH="770ae14" 而 HEAD 为 af1f682(落后 12 个提交);FW_VERSION_STRING 后挂超长变更注释;README/白皮书顶部写“git 770ae14”;README 任务表名称与代码不符(ble_data_worker→ble_ota_worker、motor_feed→ota_feed),缺 httpd/ota_cancel 等任务;引用的 .cache/ 前端快照不在仓库。
风险
现场版本追溯困难。
修改建议
构建时用 git describe 生成 GIT_HASH;变更记录移到 CHANGELOG.md。

建议 A-03 · 提交信息与内容不符,历史评审被静默删除 [构建]

+
位置
git 历史 af1f682
+
现象
af1f682 标为 “docs(readme): 更新文档说明”,实际删除 docs/esp32_architecture_review_2026-06-27.md 等 4 份文档;更早 5b2c5f8 删除了 2026-06-13 评审与 2026-05-05 通信梳理。
风险
评审结论无从追溯,问题被重复发现。
修改建议
评审记录统一归档到 docs/review/(本次已恢复存档),删除须独立提交并说明。

建议 A-04 · CS 前 esp_rom_delay_us 忙等最长 1 ms [实时性]

+
位置
main/transport/spi_master.cpp:946-952
+
现象
单核芯片上 prio 8 的 spi_worker 每帧前忙等,期间低优先级任务饥饿。
风险
CPU 利用率与 BLE/WiFi 吞吐受影响。
修改建议
改为基于 esp_timer 的剩余时间判断,< 100 µs 才忙等。

建议 A-05 · 间接优先级倒置 [实时性]

+
位置
main/core/main.cpp;README 任务表
+
现象
ota_relay(23)、health_mon(21) 通过 spi_worker(8) 完成 SPI 事务,等待期间被 9~20 优先级任务(ble_ota_worker 12、lwIP 18 等)抢占。
风险
高优先级任务的时延不可预期。
修改建议
Job 提交时临时提升 worker 优先级(优先级继承)或让 worker 位于调用方之上。

建议 A-06 · 死代码与量程不符 [可维护性]

+
位置
main/monitor/health_monitor.cpp:389-396, 365;main/monitor/task_stack.cpp:30-39
+
现象
g_stackOverflowDetected 置位后钩子立即 abort,health_mon 永远看不到;温度传感器量程配置为 20~80 °C,而 TEMP_CRITICAL=100 °C 超出量程。
风险
告警逻辑失效。
修改建议
删除死分支或改为 RTC_NOINIT 变量跨重启上报;量程改 -10~110。

建议 A-07 · 日志格式串与参数不匹配、注释过期 [可维护性]

+
位置
main/transport/spi_master.cpp:924-925;main/server/wifi_ota.cpp:403
+
现象
格式串 3 个说明符却传 5 个参数;路由注册注释写 35 次,实际 22 路由+21 OPTIONS=43 次。
风险
日志误导。
修改建议
开启 -Wformat 并修正注释。
+ +

5. 既有评审结论跟踪

+

来源:0816=project_introduction.html §19 安全审计(2026-08-16);0613=CODE_REVIEW_REPORT_2026-06-13.html(v0.3.12);0627=esp32_architecture_review_2026-06-27.md;0505=COMMUNICATION_AUDIT_2026-05-05.md。

+

统计:已修复 23 · 部分修复 5 · 仍存在/结论不成立 12 · 未逐条核对 2。总体看,历史上的并发、函数拆分、资源清理类问题大多已整改;安全类结论(认证、凭据、签名、ECB、nonce)一条都没有关闭。

+
来源编号原结论当前状态当前代码证据
0816①HTTP 22 端点零认证仍存在http_handlers.cpp:77-83 CORS *,无鉴权;见 F-01
0816②WiFi 凭据与 MASTER_KEY 明文入仓仍存在app_config.h:31-40,161-164;见 S-08
0816③无固件签名仍存在仅 SHA-256 完整性;未启用 Secure Boot / 签名校验
0816④AES-ECB 确定性模式仍存在ota_relay.cpp 609-620, 836-855 esp_aes_crypt_ecb
0816缓解nonce 每会话随机绑定防重放结论不成立nonce 仅 tick 4 字节变化且明文入日志;见 S-06
0613#1SPI 总线无并发保护已修复spi_master.cpp:588-821 单 worker + Job 池 + m_mutex
0613#2硬编码占位密钥仍存在同 0816 ②
0613#3Flash 写入无对齐检查已修复gd32_flash_store.cpp:199-276 flashWriteChunk 头尾 pad read-modify-write
0613#4BLE/WiFi 双通道 ESP32 OTA 冲突已修复Esp32OtaCore s_occupied + OtaCoordinator 单例互斥
0613#5SPI 帧 XOR 伪校验已修复(迁移)otaCrc8 已移除,Link v2 用 CRC32-MPEG2(实现在仓外 spi_link,未能直接核对)
0613#60xA5 常量碰撞已修复spi_master.h:33 SPI_APP_ENTER_BOOT_ACK_OK=0xB5
0613#7OTA 会话无全局超时部分修复传输阶段 120 s 预算 + 150 s 看门狗,但看门狗短于最坏耗时且无法停止中继任务;见 S-03/S-04
0613#8CORS 允许所有来源仍存在http_handlers.cpp:79
0613#11OtaRelay 静态状态无锁部分修复成员已 private + StateLockGuard,reset() 仍可被外部任务调用;见 S-03
0613#16nonce 仅用 esp_random 一次/可预测仍存在(形式变化)现为 tick+固定地址+常量种子;见 S-06
0613#17ota_meta checksum 从未填充已修复metaFinalizeChecksum + metaVerifyChecksum(gd32_flash_store.cpp:555-620)
0613#18ESP32 OTA 无回滚验证部分修复代码调用 mark_app_valid,但调用过早且回滚开关所在配置未生效;见 S-01/S-02
0613#19SPI 忽略 timeout已修复spi_master.cpp:981-1019 有界等待(残留 G-10)
0613#20esp_ota_write 失败未 abort已修复esp32_ota_core finalizeCleanupAndReturn/beginCleanupAndReturn
0613#22SPI 无就绪信号已修复DATA_READY GPIO10 ISR + credit 流控
0627P0-1metaVerifyChecksum 全零跳过校验已修复gd32_flash_store.cpp:563-569 返回 ESP_ERR_INVALID_STATE
0627P0-2RSSI CRITICAL→NORMAL 无法降级已修复(引入新问题)health_monitor.cpp:596-598 可直接恢复 NORMAL,但会覆盖过温状态;见 S-10
0627P0-3OTA_MAX_FIRMWARE_SIZE 注释不一致仍存在(原结论换算亦有误)app_config.h:77;216 KiB 恰为 0x36000,注释 0x35F90 错误;见 G-17
0627P0-4凭证硬编码入仓库仍存在同 0816 ②
0627P0-5BleEsp32FinishCtx 三处 free 易漏已修复ble_ota_host.cpp:183-193 由任务负责 free,失败路径统一释放
0627P0-7doRelayPackets 180 行 + goto已修复拆出 sendRelayPacketWithRetry,阈值命名常量
0627P0-8ble_ota_process_data_batch 245 行已修复已拆为 process_esp32_packet / process_gd32_packet(ble_ota_data.cpp:878, 1001, 1167)
0627P1-AOtaRelay 状态成员封装破坏部分修复已 private;锁内外混合访问仍在
0627P1-9/P1-10getSnapshot 双重加锁 / 多次取状态已修复stateToString + isIdleState,快照持锁一次取值
0627P1-11/P1-12esp32_ota_core 清理序列重复已修复finalizeCleanupAndReturn / beginCleanupAndReturn
0627P1-13/P1-14doFinishAndVerify 双职责 / APP 探测重复已修复doSendFinishOta + doWaitForAppAlive;probeAndCacheAppAlive
0627P1-17metaUpdate 读改写非原子已修复gd32_flash_store.cpp:432-448 全程持 m_nvsMutex
0627P1-18metaLoad nvs_open 失败当 OK已修复gd32_flash_store.cpp:345-355 区分 NOT_FOUND
0627P1-22s_ota_recv_buf 全局共享已修复0.5.0 改为 OTA 会话内 RAII malloc
0627P1-23一次性任务创建失败上报不一致已修复ble_ota_host.cpp:130-135, 189-194, 212-215 均 sendFinishEvent
0627P1-24reorder slot malloc 失败静默丢弃已修复ble_ota_data.cpp:466-474 发窗口 ACK 并返回 ESP_ERR_NO_MEM
0627P1-25protocol.h 死代码未逐条核对—
0627P2-88notifyRelayStarted/Complete 纯转发仍存在ota_coordinator.cpp
0627P2-89两个通知函数几乎相同部分修复已共用 helper
0627P2-920xF0F0F0F0 魔法数字已修复kLastPacketAckSeq
0627其余 P2样板抽取/配置化/风格类约 50 项未逐条核对低风险风格项,本次未逐条复核,建议下轮评审处理
0505UDPUDP 日志可选、建议可关闭仍存在无开关,默认开启并硬编码目标;见 G-15
+ +

6. 优先级修复路线图

+ + + + +
阶段目标条目预估
P0 · 本周堵住远程控制面,恢复可复现构建F-01(token + CORS 白名单 + /spi/send 白名单/默认关闭)、S-12、S-01(合并 sdkconfig.defaults + HZ static_assert)、S-08(轮换 WiFi 口令,凭据迁 NVS)2~3 人日
P1 · 2 周内消除变砖与失联路径S-02(mark_app_valid 后移)、S-03 + S-04(中继取消标志与分阶段看门狗)、S-05(断线长周期重连)、S-07、S-10、S-11、G-01、G-024~6 人日,需 GD32 全链路 OTA 回归
P2 · 1 个月加固密码学与无线面S-06(esp_fill_random nonce)、S-09(BLE LESC + 加密特征)、0816 ③④(签名 / AES-CTR 或 GCM,需三工程同步)、G-06、G-155~8 人日(含 GD32 侧)
P3 · 持续工程卫生G-13(spi_link submodule + 固件构建 CI)、G-03~G-05、G-07~G-12、G-14、G-16、G-17、A-01~A-07随迭代
+ +

7. 总结评分

+ +
维度评分理由
固件架构与分层7 / 10接入/协调/执行/传输分层清晰,协调器单例 + 单 SPI worker 设计合理
SPI 协议与传输6 / 10会话/序号/credit/CRC32 完备,诊断充分;边界与回绕细节待补
OTA 可靠性5 / 10互斥与 epoch 进步明显;中止协作、看门狗预算、回滚时机存在变砖级缺口
FreeRTOS 与实时性6 / 10任务优先级经事故调优;钩子与健康状态逻辑有真实缺陷
网络与无线安全2 / 10零认证 + CORS * + 原始 SPI 透传 + 硬编码密钥 + BLE 无加密
代码质量与可维护性6 / 10历史评审的大函数/重复样板多数已整改;超长文件(http_handlers 2.8k 行)仍需拆分
构建与仓库卫生3 / 10干净克隆不可复现、CI 必败、仓外依赖、过程文件入库
文档6 / 10白皮书详尽,但版本/任务表漂移,历史评审被静默删除
综合5.1 / 10链路与 OTA 工程扎实、诊断能力强,历史评审的结构性问题大多已整改;但“可信局域网”前提下的零认证控制面、不可复现的构建配置、回滚时机与中止协作缺陷,使其距离量产/现场部署仍有明显距离。
+
结论:建议在下一个版本(0.5.3)前完成 P0 全部条目,并把本报告纳入 docs/project_introduction.html §19 的跟踪;后续评审请统一归档到 docs/review/,不要随文档整理删除。
+

本报告由 Hark 专家评审生成 · 2026-10-19 · 基线 af1f682。行号以该提交为准,后续提交可能漂移。

+ \ No newline at end of file diff --git a/docs/review/README.md b/docs/review/README.md new file mode 100644 index 0000000..8bbd066 --- /dev/null +++ b/docs/review/README.md @@ -0,0 +1,11 @@ +# docs/review — 评审记录归档 + +| 文件 | 日期 | 说明 | +|---|---|---| +| [ESP32_COMM_专家评审_2026-10-19.html](ESP32_COMM_专家评审_2026-10-19.html) | 2026-10-19 | 全量专家评审(基线 af1f682 / v0.5.2),含既有评审逐条跟踪 | +| [archive/esp32_architecture_review_2026-06-27.md](archive/esp32_architecture_review_2026-06-27.md) | 2026-06-27 | 架构扫描(原 docs/,af1f682 删除,原样恢复) | +| [archive/CODE_REVIEW_REPORT_2026-06-13.html](archive/CODE_REVIEW_REPORT_2026-06-13.html) | 2026-06-13 | 高风险代码评审 v0.3.12(原 docs/,5b2c5f8 删除,原样恢复) | +| [archive/COMMUNICATION_AUDIT_2026-05-05.md](archive/COMMUNICATION_AUDIT_2026-05-05.md) | 2026-05-05 | 通信梳理(原 docs/,5b2c5f8 删除,原样恢复) | +| ../project_introduction.html §19 | 2026-08-16 / 2026-10-19 | 安全审计原文 + 2026-10-19 复核更新 | + +约定:评审记录只增不删;新评审引用并跟踪旧结论。 diff --git a/docs/review/archive/CODE_REVIEW_REPORT_2026-06-13.html b/docs/review/archive/CODE_REVIEW_REPORT_2026-06-13.html new file mode 100644 index 0000000..f55735d --- /dev/null +++ b/docs/review/archive/CODE_REVIEW_REPORT_2026-06-13.html @@ -0,0 +1,970 @@ + + + + + +ESP32_COMM 高风险代码评审报告 + + + + +
+ + +
+

ESP32_COMM 高风险代码评审报告

+
多维度并行审查 + 对抗性验证 · 4维度 × 33项确认发现
+
+ 📋 项目: ESP32_COMM (ESP32-C3 OTA Relay) + 📅 日期: 2026-06-13 + 🔌 固件版本: v0.3.12 + 👥 审查Agent: 4 Review + 36 Verify + 1 Synthesize +
+
+ + +
+
+
33
+
确认发现
+
+
+
8
+
CRITICAL
+
+
+
18
+
HIGH
+
+
+
4
+
MEDIUM
+
+
+
33/36
+
验证通过率
+
+
+
2
+
已驳回(误报)
+
+
+ + +
+
📊 各维度发现分布
+
+
+
8
+
+
+
+
+
+
内存安全
+
+
+
10
+
+
+
+
+
+
并发/RTOS
+
+
+
9
+
+
+
+
+
+
OTA/固件
+
+
+
7
+
+
+
+
+
+
SPI/协议
+
+
+
+ + +
+
🚨 TOP 3 最危险 Bug
+ + +
+
+
+
Finding #1 — CRITICAL
+
SPI 总线无并发保护 — 多任务竞争导致协议损坏
+
+
+ CRITICAL + 并发竞态 +
+
+
+
+ 文件: + spi_master.h:73-142, ota_relay.cpp, wifi_ota.cpp + 维度: + 并发/RTOS + 验证: + ✔ HIGH置信度 — 源码确认 +
+

SpiMaster 是单例但没有任何互斥锁。多个 FreeRTOS 任务直接调用 SPI 方法:ota_relay 任务 (优先级 MAX-2)、wifi_ota 任务 (MAX-3)、health_monitor 任务 (MAX-4)。协议操作是多步非原子的(如 otaTransfer = 发送帧 + 轮询响应,CS在两次传输之间释放),高优先级任务可在协议序列中间抢占。

+
// spi_master.h — 无任何mutex成员 +class SpiMaster { +private: + spi_device_handle_t m_spiDevice = NULL; // ← 唯一成员,无锁 +};
+
+
⚠ 影响
+ 帧交错(A任务的请求配对B任务的响应)、GD32状态机混乱、固件传输数据损坏。生产环境高频CAN通信+OTA并发时必现。 +
+
+
🔧 修复建议
+ 在 SpiMaster 中添加 SemaphoreHandle_t m_mutex,所有公共方法入口加锁。对于多步协议操作(otaTransfer等),使用统一的锁粒度保护整个请求-响应序列。 +
+
+
+ + +
+
+
+
Finding #2 — CRITICAL
+
硬编码占位密钥 — 认证体系形同虚设
+
+
+ CRITICAL + 安全/OTA +
+
+
+
+ 文件: + app_config.h:107-115 + 维度: + OTA/固件完整性 + 验证: + ✔ HIGH置信度 — 源码确认 +
+

MASTER_KEY 使用 NIST AES 测试向量,SCRAMBLE_CONST 使用简单递增序列,GD32_CHIP_UID 使用 0x00-0x0B。注释标注"需填入实际密钥"但从未替换。

+
// ⚠️ 请填入实际密钥 ← 注释说"需填入"但从未填入 +inline constexpr uint8_t MASTER_KEY[] = { + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, // ← NIST AES测试向量! + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 +}; +inline constexpr uint8_t SCRAMBLE_CONST[] = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // ← 简单递增序列 + 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 +}; +inline constexpr uint8_t GD32_CHIP_UID[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, // ← 0x00-0x0B + 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B +};
+
+
⚠ 影响
+ 攻击者可轻易伪造认证握手,向 GD32 注入恶意固件。GD32_CHIP_UID占位值意味着所有设备共享相同"身份"(如果运行时未正确覆盖)。 +
+
+
🔧 修复建议
+ 从安全存储(NVS/efuse)读取密钥;移除所有硬编码占位值;添加编译期断言确保非测试密钥;MASTER_KEY和SCRAMBLE_CONST应在首次设备配准时由安全随机数生成。 +
+
+
+ + +
+
+
+
Finding #3 — CRITICAL
+
Flash 写入无对齐检查 — 可能导致固件损坏
+
+
+ CRITICAL + 内存安全/OTA +
+
+
+
+ 文件: + ota_relay.cpp:308-320, wifi_ota.cpp:711/740, ble_ota.cpp:575 + 维度: + 内存安全 + OTA完整性 + 验证: + ✔ HIGH置信度 — 源码确认 +
+
esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len) +{ + // ... 无对齐检查 ... + return esp_partition_write(instance().m_gd32Partition, offset, data, len); // ← offset/len可能非4字节对齐 +}
+

两个调用路径产生非对齐值:(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字节对齐。

+
+
⚠ 影响
+ 非对齐写入导致 ESP_ERR_INVALID_ARG 或触发 read-modify-write 异常,GD32固件最后一个chunk损坏或丢失。 +
+
+
🔧 修复建议
+ flashWriteChunk 内部做4字节对齐对齐(不足部分 read-modify-write),或在上层确保只传对齐参数。 +
+
+
+
+ + +
+
🔴 其他 CRITICAL 级发现
+ +
+
+
+
Finding #4 — CRITICAL
+
BLE/WiFi 双通道 OTA 同时触发 — ESP32 OTA状态冲突
+
+
CRITICAL
+
+
+
+ 文件:wifi_ota.h:42-46, ble_ota.h + 验证:✔ HIGH置信度 +
+

BleOta 和 WifiOta 各自维护独立的 m_esp32OtaActive、m_esp32OtaHandle、m_esp32UpdatePartition,但底层 esp_ota_begin/write/end 操作的是同一个OTA分区。两个通道同时触发时:第二个 esp_ota_begin 会失败或覆盖句柄;esp_ota_write 可能交错写入同一分区;导致固件损坏+变砖。

+
🔧 修复建议
添加全局 OTA 互斥标志,同一时刻只允许一个OTA源(BLE或WiFi)。
+
+
+ +
+
+
+
Finding #5 — CRITICAL
+
SPI 协议帧仅用 XOR 伪校验 — 数据损坏不可检测
+
+
CRITICAL
+
+
+
+ 文件:spi_master.cpp:134-141 + 验证:✔ HIGH置信度 +
+
uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len) { + uint8_t crc = 0; + for (size_t i = 0; i < len; ++i) + crc ^= data[i]; // ← 简单XOR,非CRC8 + return crc; +}
+

XOR校验无法检测:偶数个相同位置的位翻转、字节交换、全零段丢失。固件数据的静默损坏几乎不可检测。

+
🔧 修复建议
替换为 CRC-8/MAXIM 或 CRC-16/CCITT,与GD32团队协商。
+
+
+ +
+
+
+
Finding #6 — CRITICAL
+
SPI_OTA_SOF 与 SPI_APP_ENTER_BOOT_ACK_OK 值碰撞 (0xA5)
+
+
CRITICAL
+
+
+
+ 文件:spi_master.h:21,26 + 验证:✔ HIGH置信度 +
+
#define SPI_OTA_SOF (0xA5) // OTA帧起始标记 +#define SPI_APP_ENTER_BOOT_ACK_OK (0xA5) // APP确认进入Bootloader
+

同一值 0xA5 表示两种完全不同的语义。GD32在OTA窗口回复ACK时,ESP32可能将ACK字节误解析为OTA帧起始,导致协议状态机混乱。

+
🔧 修复建议
修改其中一个常量值,消除歧义。例如将 SPI_APP_ENTER_BOOT_ACK_OK 改为 0xB5。
+
+
+ +
+
+
+
Finding #7 — CRITICAL
+
OTA 会话无全局超时 — 可无限挂起
+
+
CRITICAL
+
+
+
+ 文件:ota_relay.cpp:76-91,562; ota_relay.h:59 + 验证:✔ HIGH置信度 +
+

waitUntilNotWait() 每次调用有 max_wait_rounds 上限,但整个 forwardFirmware() 流程没有全局超时。GD32 持续返回 WAIT 时,单个 SEND_SIZE阶段可挂起 ~78秒,完整流程可挂起数分钟。OTA_RESULT_ERR_USER_ABORT 已定义但从未使用。

+
🔧 修复建议
在 forwardFirmware() 入口记录开始时间,每个阶段检查全局超时(如5分钟),超时则中止并清理。
+
+
+ +
+
+
+
Finding #8 — CRITICAL
+
HTTP CORS 允许所有来源 — 任意网页可触发 OTA
+
+
CRITICAL
+
+
+
+ 文件:wifi_ota.cpp:58 + 验证:✔ HIGH置信度 +
+
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+

任何网页都可以向 ESP32 发起 OTA 请求。结合占位密钥问题,攻击链完整:恶意网页 → 上传伪造固件 → 通过占位密钥认证 → 烧录恶意GD32固件。

+
🔧 修复建议
限制 CORS 为本地网络来源,或移除 CORS头并使用同源策略。
+
+
+
+ + +
+
🟠 HIGH 级关键发现
+
+ 共 18 项 HIGH 级发现,点击展开详情 + +
+ + + + + + + + + + + + + + + + + + + + +
#严重度文件问题影响
9HIGHwifi_ota.cpp:121WiFi事件回调中调用 esp_wifi_connect() 重连在系统事件任务中调用可能死锁
10HIGHhealth_monitor.cpphealthState 静态变量无锁跨任务读写竞态导致健康判断错误
11HIGHota_relay.h:80-845个静态变量(relayState等)无锁跨任务访问协议状态不一致
12HIGHwifi_ota.h:33-47WifiOta 所有成员 public 且无锁HTTP回调与WiFi事件回调竞争
13HIGHble_ota.hBleOta 成员无锁,BLE GATT回调与主任务竞争BLE OTA 数据损坏
14HIGHdebug_log.cpp:296log() 栈分配 msg[512] + log_buf[600] = 1112字节低栈任务中栈溢出
15HIGHwifi_ota.cpp:1008spi_send_handler 栈分配 1728+ 字节HTTP server栈仅8KB,接近溢出
16HIGHota_relay.cpp:456sendNonceAndAuthenticate() nonce 仅用 esp_random() 一次认证会话密钥可预测
17HIGHota_relay.h:25ota_meta_t.checksum[32] 从未填充/验证固件完整性无保障
18HIGHwifi_ota.cpp:429-444ESP32 OTA 完成→重启,无回滚验证新固件若无法启动将变砖
19HIGHspi_master.cpp:258syncTransfer/recv 忽略 timeout_ms 参数SPI事务可能无限等待
20HIGHwifi_ota.cpp:599-629esp_ota_write 失败后 break,未调用 esp_ota_abortOTA分区处于不一致状态
21HIGHble_ota.cpp:439-444BLE OTA 帧预留4字节CRC但减去计算不匹配最后几个字节被误截断
22HIGHspi_master.hSPI仅4线,无硬件流控/就绪信号高速传输时GD32来不及处理
+
+ + +
+
🏗 架构级隐患
+ +
+

① 全局状态泛滥,无封装无同步

+

项目中有 5个静态全局变量 (relayState, relaySeq, relayTotalPackets, gd32AppAlive, gd32Version) 和 大量public成员 (WifiOta/BleOta所有状态),跨越3+个FreeRTOS任务和多个ISR/callback上下文访问,没有任何一个被mutex/atomic/critical section保护。这是系统性问题,不是个别bug。

+
+ +
+

② 单通道SPI + 多任务无仲裁

+

SpiMaster 单例是唯一的SPI通信通道,但没有任何仲裁机制。WiFi/BLE/健康监控/OTA中继四个功能模块各自直接调用,协议级操作(请求-响应对)不是原子的。需要从架构层面引入SPI总线仲裁器。

+
+ +
+

③ 安全架构不完整

+

认证握手存在但密钥硬编码→形同虚设;固件校验字段存在但未填充→形同虚设;CORS开放→任何人可触发OTA。安全功能"看起来有"但实际上不提供保护,这种"安全假象"比没有安全功能更危险,因为它给了虚假的信心。

+
+
+ + +
+
🛠 优先修复顺序
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
优先级修复项工作量紧急度进度
P0添加 SPI 总线互斥锁小极高 — 生产环境必崩
P0替换硬编码密钥为安全存储中极高 — 安全漏洞
P0Flash 写入对齐检查小极高 — 数据损坏
P1添加全局 OTA 超时+中止机制中高 — 设备挂死
P1ESP32 OTA 添加回滚验证中高 — 变砖风险
P1替换 XOR 为 CRC-8/16 校验小高 — 固件损坏
P1消除 0xA5 值碰撞极小高 — 协议混乱
P2添加 OTA 单源互斥 (BLE/WiFi)中中高
P2为所有跨任务状态添加锁/atomic大中高
P2修复 WiFi 事件回调中的重连调用小中
P3修复大栈分配问题小中
P3限制 CORS 来源小中
P3实现 ota_meta_t 校验填充与验证中中
+
+ + +
+
✅ 已驳回发现(误报)
+ + + + + + + + + + + + + + + + +
原始严重度问题描述驳回原因
CRITICALSPI DMA缓冲区栈分配导致数据损坏/硬FaultESP-IDF 6.0 的 spi_master 驱动内置 setup_dma_priv_buffer() 函数自动检测并对齐DMA缓冲区,栈分配仅导致性能损失,不引发数据损坏
HIGHTOCTOU: relayState 检查后 reset() 无锁虽然模式存在,但当前代码流程中检查和重置在同一线程同步执行,实际竞态窗口极小,风险被高估
+
+ + +
+

ESP32_COMM 高风险代码评审报告 · 由 4维并行审查 + 36项对抗性验证 生成 · 2026-06-13

+

审查维度: 内存安全 · 并发/RTOS · OTA/固件完整性 · SPI通信/协议

+
+ +
+ + + + + diff --git a/docs/review/archive/COMMUNICATION_AUDIT_2026-05-05.md b/docs/review/archive/COMMUNICATION_AUDIT_2026-05-05.md new file mode 100644 index 0000000..dcca2df --- /dev/null +++ b/docs/review/archive/COMMUNICATION_AUDIT_2026-05-05.md @@ -0,0 +1,74 @@ +# ESP32_COMM 通信梳理(2026-05-05) + +## 结论 + +当前“ESP32 ↔ 上位机(tools WebUI)”的主通信方式是 **HTTP**,不是 UDP。 + +### WebUI 实际在用的接口 + +- `GET /system/status`:每 1 秒轮询一次,用于在线状态、WiFi、heap、温度、OTA 状态展示 +- `GET /logs`:每 1 秒轮询一次,用于拉取并清空日志环形缓冲区 +- `POST /can/send`:发送 CAN 请求到 GD32 +- `POST /ota/upload` / `POST /ota/relay` / `POST /ota/update`:OTA 相关 + +## 日志链路 + +所有 `DEBUG_*` 日志目前会并行写入三处: + +1. **UART0** + - 本地串口调试 +2. **UDP 单播** → `DEBUG_LOG_IP:DEBUG_LOG_PORT` + - 可供独立 UDP 调试接收端使用 +3. **内部环形缓冲区** + - 被 `GET /logs` 读取 + - 这是当前 WebUI 日志面板真正使用的通道 + +## 为什么 `{"t":"hb"}` 是多余的 + +之前在 `wifi_ota_task()` 里,每 3 秒会发送一次: + +```json +{"t":"hb"} +``` + +它是通过 `debug_log_send()` 发出的,所以会同时进入: +- UART +- UDP +- `/logs` 对应的环形缓冲区 + +这会导致 WebUI 把它显示成一条普通日志: + +- level: `I` +- tag: `ESP32` +- message: `{"t":"hb"}` + +但 WebUI 本来已经每秒轮询 `/system/status`,完全可以判断设备在线,不需要再靠这种“伪日志心跳”。 + +## 已处理 + +已删除 `wifi_ota_task()` 中每 3 秒发送一次的 `{"t":"hb"}`。 + +## 现在是否还有重复/不必要通信 + +### 对 WebUI 来说 + +有两条并行链路,但用途不同,不算重复: + +- `/system/status`:结构化状态 +- `/logs`:文本日志 + +这两条都保留是合理的。 + +### 对底层日志输出来说 + +日志同时发 UART + UDP + ring buffer,属于“一份日志三路镜像”。 + +这对调试友好,但从“最简设计”角度看,**UDP 在当前 WebUI 场景里不是必需的**。 + +也就是说: + +- **必须保留:** ring buffer + `/logs` +- **建议保留:** UART0 +- **可选保留:** UDP 6666 + +如果后续你想继续精简,可以做“方案2”:把 UDP 也做成可关闭,甚至默认关闭。 diff --git a/docs/review/archive/esp32_architecture_review_2026-06-27.md b/docs/review/archive/esp32_architecture_review_2026-06-27.md new file mode 100644 index 0000000..00bb6d4 --- /dev/null +++ b/docs/review/archive/esp32_architecture_review_2026-06-27.md @@ -0,0 +1,301 @@ +# ESP32 软件架构扫描报告(2026-06-27) + +## 背景 +上一轮评审(2026-06-25,见 `docs/esp32_architecture_review_2026-06-25.md`)已完成 8 项优化:根目录清理、OtaCoordinator 状态查询单次取值、9 个 GD32 存储 wrapper inline 化、HTTP URI 路由表化、OtaRelay commonReject helper、SpiMaster *Unlocked 折叠、HealthMonitor RAII LockGuard、编译验证。 + +本轮扫描基于上述优化完成后的代码,新发现约 74 项可优化点,分三类: +- **新发现**(约 65 项):上轮完全未涉及 +- **遗留**(3 项):上轮评审已提到但当时标"不在本次范围" +- **残留**(3 项):上轮已做但未完全覆盖 + +按风险/收益分 P0 / P1 / P2 三档。 + +--- + +## P0 优先(功能正确性 / 安全性 bug) + +### P0-1 metaVerifyChecksum 全零跳过校验是隐性后门 +- **文件**:[main/storage/gd32_flash_store.cpp:299-306](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L299-L306) +- **问题**:checksum 全零时直接返回 `ESP_OK`,注释"旧数据兼容"。meta 被意外清零时校验会"通过",掩盖固件损坏 +- **建议**:返回 `ESP_ERR_INVALID_STATE`,由调用方决定是否容忍 + +### P0-2 RSSI 状态机 CRITICAL→NORMAL 无法降级 +- **文件**:[main/monitor/health_monitor.cpp:333-357](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L333-L357) +- **问题**:"恢复到 NORMAL"只在 `cur == SYS_WARNING` 时触发,CRITICAL 状态下 RSSI 恢复正常不会降级 +- **建议**:明确状态转换表,补 CRITICAL → NORMAL 直接降级路径 + +### P0-3 OTA_MAX_FIRMWARE_SIZE 注释与 0x36000 不一致 +- **文件**:[main/include/app_config.h:62](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L62) +- **问题**:`216*1024 = 221184`,注释"对齐 GD32 APP_MAX_SIZE (0x36000)",但 `0x36000 = 221296`,相差 112 字节,并非对齐 +- **建议**:确认正确值,用十六进制表达 `0x36000` 或修正注释 + +### P0-4 凭证硬编码入仓库 +- **文件**:[main/include/app_config.h:31,48,112-115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L31) +- **问题**:WiFi 密码、AP 密码、MASTER_KEY 明文入仓库,README 也标注"⚠️ 占位值,生产环境需更换" +- **建议**:迁移到 NVS/Kconfig + env 文件,CI 加凭证扫描规则 + +### P0-5 BleEsp32FinishCtx 三处 free 易漏 +- **文件**:[main/server/ble_ota.cpp:854,158,190,210](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L854) +- **问题**:ctx 在 3 个不同分支释放,`if (ctx) free(ctx)` 防御式写法暗示历史 bug;第 190 行后 `restartEsp32()` 不返回,理论上 free 不执行 +- **建议**:RAII 封装 `BleFinishCtxDeleter`,任务入口 `BleFinishCtxDeleter guard{ctx};`,所有 return 路径自动释放 +- **备注**:上轮评审 G 项已提到("不是精简,是健壮性,留后续"),本轮方案改为 RAII deleter(上轮原方案是改静态单例) + +### P0-6 GD32 固件大小校验逻辑双通道重复 +- **文件**:[main/server/ble_ota.cpp:1115-1121](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1115-L1121) vs [main/server/http_handlers.cpp:302-308](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L302-L308) +- **问题**:两处完全相同的校验:`uint32_t rounded = ((total + PKT - 1) / PKT) * PKT; if (total == 0 || total > MAX || rounded > MAX) {...}`,但 BLE 用 `BLE_OTA_FINISH_ERR_GD32_SIZE` 上报,HTTP 用 `HTTPD_500` 返回,错误码语义不对齐 +- **建议**:抽取 `OtaCoordinator::validateGd32Size(total_size) -> esp_err_t`,两路调用方各自翻译为通道特定错误回报 + +### P0-7 doRelayPackets 180 行 + goto + 重试阈值混乱 +- **文件**:[main/relay/ota_relay.cpp:936-1115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L936-L1115) +- **问题**:约 180 行,`for` → 内嵌 `while(1)` → 内嵌 `for` 重试 → 2 处 `goto resend_packet`(行 1057、1091);重试计数器阈值不一致:行 1015 `>=10U`、行 1032 `>=3U`、行 1050 `>=3U`、行 1075 `>=10U`、行 1084 `>=10U`,同一 `retry_count` 既限 3 又限 10 +- **建议**:拆分为 `preparePacket(seq)` + `sendPacketWithRetry(seq, tx_buf)` + 外层 `doRelayPackets`;重试阈值提取常量 `kMaxResendOnNack`/`kMaxResendOnStructError`/`kMaxResendOnPollExhaust` + +### P0-8 ble_ota_process_data_batch 245 行 +- **文件**:[main/server/ble_ota.cpp:975-1219](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L975-L1219) +- **问题**:单函数处理 ESP32 与 GD32 两条独立数据流,ESP32 分支内嵌 6 个子逻辑,GD32 分支内嵌 9 个子逻辑,循环内多处 `offset += packet_len; if (offset + 7 > recv_len) break;` 重复推进 +- **建议**:拆分为 `process_esp32_packet(...)` 与 `process_gd32_packet(...)`,主循环只负责解包 + 分发 + +--- + +## P1 优先(数据竞争 / 资源泄漏 / 双通道一致性) + +### P1-A OtaRelay 状态成员封装破坏(一组原子改动) + +**头文件**:[main/relay/ota_relay.h:228-237](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.h#L228-L237) + +**问题**:把 `relayState/relaySeq/relayTotalPackets/gd32AppAlive/gd32Version/taskHandle/s_relayListener/s_stateMutex` 全部声明为 `public static`,但 cpp 内部多处直接访问绕过带锁的 setter/getter,与外部带锁读取形成数据竞争: + +| 位置 | 直接访问 | +|---|---| +| [ota_relay.cpp:331-332](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L331-L332) | `handleSizeNeg` 直接写 `relayTotalPackets/relaySeq` 无锁 | +| [ota_relay.cpp:1102,1104-1105](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1102-L1105) | `doRelayPackets` 直接写读 `relaySeq` 无锁 | +| [ota_relay.cpp:1189,1197-1198,1209](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1189-L1209) | `doFinishAndVerify` 直接写读 `gd32AppAlive/gd32Version` 无锁 | +| [ota_relay.cpp:470,478](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L470-L478) | `forwardFirmware` 直接读 `relayState` 无锁 | +| [ota_relay.cpp:1272,1288](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1272) | `taskEntry` 直接访问 `s_relayListener` 绕过 getter | + +**建议**:所有静态状态成员改 `private`,统一通过 setter/getter;relay task 内部若已持锁,在 `StateLockGuard` 块内访问。`s_stateMutex` 改 private,`StateLockGuard` 作为友元或保留 public 嵌套类。 + +### P1-9 getSnapshot 双重加锁且可能不一致 +- **文件**:[main/ota/ota_coordinator.cpp:571-591](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L571-L591) +- **问题**:行 576-581 持锁读取 `m_sysState` 填入 `snap.sys_state` 后释放;行 582 调 `getSystemStateString()` 又内部调 `getSystemState()` 二次加锁。两次取值之间状态可能变化,导致 `snap.sys_state`(第一次取的值)与 `snap.sys_state_str`(第二次取的值)描述不同时刻状态,前端展示自相矛盾 +- **建议**:让 `getSystemStateString` 接受 `ota_system_state_t` 参数(重载 `static const char* stateToString(ota_system_state_t)`),`getSnapshot` 持锁时一次性取值并就地转字符串 +- **备注**:上轮 A.2 只改了 `isIdle/isGd32Busy/isEsp32Busy`,`getSnapshot` 未覆盖 + +### P1-10 getSystemStateString 等仍独立加锁 +- **文件**:[main/ota/ota_coordinator.cpp:509-549](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L509-L549) +- **问题**:`isIdle`、`isGd32Busy`、`isEsp32Busy`、`getSystemStateString` 各自调 `getSystemState()`(加锁取值再解锁)。`if (!isIdle() && !isGd32Busy())` 这类连续调用会加锁 2~3 次 +- **建议**:提供"取一次状态后多次判断"的接口,或让调用方先 `getSystemState()` 取一次再传给各判断函数 +- **备注**:上轮 A.2 残留 + +### P1-11 esp32_ota_core finalize 3 处清理序列重复 +- **文件**:[main/ota/esp32_ota_core.cpp:281-288,312-318,332-339](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L281-L339) +- **问题**:3 处几乎相同:`esp_ota_abort(m_handle); m_ended = false; releaseMutex(); resetState(); return xxx;`(SHA 计算失败 / SHA 不匹配 / esp_ota_end 失败) +- **建议**:提取私有方法 `finalizeCleanupAndReturn(esp_err_t code)`,或用 `goto cleanup` + +### P1-12 esp32_ota_core begin 3 处清理序列重复 +- **文件**:[main/ota/esp32_ota_core.cpp:126-130,135-141,151-158](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L126-L158) +- **问题**:3 处清理序列:分区找不到 / 固件太大 / `esp_ota_begin` 失败,模式高度相似,差异仅在是否清零 `m_handle` 和返回码 +- **建议**:提取 `beginCleanupAndReturn(esp_err_t code)`,或统一出错点设置返回码后 `goto begin_fail` + +### P1-13 doFinishAndVerify 100 行双职责 +- **文件**:[main/relay/ota_relay.cpp:1118-1216](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1118-L1216) +- **问题**:单函数承担两个协议阶段:(1) 发送 FINISH_OTA 并等 ACK(1120-1176);(2) APP 存活轮询验证(1184-1213)。函数名含 "Verify",但两个阶段职责不同、超时参数不同、重试策略不同,行 1191 `for (attempt < 30)` 与 FINISH_OTA 的 `for (retry < 10)` 嵌套语义混淆 +- **建议**:拆分为 `doSendFinishOta()` + `doWaitForAppAlive()`,`doFinishAndVerify` 改为顺序调用两者并做 session 清理 + +### P1-14 APP 存活探测模式三处重复 +- **文件**:[main/relay/ota_relay.cpp:1191-1207,1240-1261](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1191-L1207) + [main/ota/ota_coordinator.cpp:596-615](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L596-L615) +- **问题**:同一"调 `SpiMaster::appPing` + 写 `gd32AppAlive`/`gd32Version` + 日志版本号"模式在 3 处重复:(1) `doFinishAndVerify` 轮询(30 次重试);(2) `taskEntry` 启动探测(1 次);(3) `OtaCoordinator::probeGd32AppOnce`(1 次)。日志格式、版本号字段打印、成功/失败处理高度相似但不完全相同 +- **建议**:提取 `static void OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t* out_uptime)`,三处调用方分别指定重试次数和超时 + +### P1-15 setWifiConnected 未加锁 +- **文件**:[main/monitor/health_monitor.cpp:320-323,248](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L320-L323) +- **问题**:直接写 `volatile bool m_wifiConnected`,未走 `m_mutex`。虽然 `volatile` 保证可见性,但与 `m_healthState` 等其他状态的访问模式不一致。`checkBeforeOta` 行 248 读 `m_wifiConnected` 也未加锁,存在读写竞态 +- **建议**:统一通过 LockGuard 保护,或显式说明为何此字段例外(如使用 `std::atomic`) +- **备注**:上轮 F 给 HealthMonitor 加了 LockGuard,但 `m_wifiConnected` 漏了 + +### P1-16 温度 esp_timer_handle_t 是局部变量,资源无法回收 +- **文件**:[main/monitor/health_monitor.cpp:182-194](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L182-L194) +- **问题**:`esp_timer_handle_t temp_timer` 是局部变量未存为成员。任务若被删除,定时器持续运行,回调仍会访问 `instance()` 的成员 +- **建议**:将 `temp_timer` 存为成员,提供 `deinit()` 在任务删除时 `esp_timer_stop` + `esp_timer_delete` + +### P1-17 metaUpdate 读-改-写非原子 +- **文件**:[main/storage/gd32_flash_store.cpp:188-198](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L188-L198) +- **问题**:`metaLoad` → 修改 → `metaSave` 中间无锁,若两个任务同时调用 `metaUpdate`,会丢失更新 +- **建议**:在 Gd32FlashStore 内增加 NVS 互斥锁,或将"由调用方串行化"约束以运行时断言强制 + +### P1-18 metaLoad NVS open 失败一律当 OK +- **文件**:[main/storage/gd32_flash_store.cpp:142-145](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L142-L145) +- **问题**:`nvs_open` 失败时直接 `return ESP_OK;`(注释说"命名空间不存在时返回全零结构"),但 NVS open 失败原因可能不止"不存在"(如 NVS 损坏),一律当 OK 会掩盖真实错误 +- **建议**:区分 `ESP_ERR_NVS_NOT_FOUND`(OK + 全零)与其他错误(返回 err) + +### P1-19 DebugLog init 不检查 uartInit 返回值 +- **文件**:[main/monitor/debug_log.cpp:252-272](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L252-L272) +- **问题**:`uartInit();`(行 258)返回值被丢弃,UART 初始化失败仍设 `m_initialized = true` 并打印"调试日志已初始化" +- **建议**:检查返回值,失败时降级(仅 ring buffer)并在日志中标注 UART 不可用 + +### P1-20 ringDrainJson 容量估算与边界检查 +- **文件**:[main/monitor/debug_log.cpp:116-151](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L116-L151) +- **问题**:`capacity = count * (LOG_RING_SIZE * 2) + 16`,count=64、LOG_RING_SIZE=256 → 最大 32784 字节,单次 malloc 在 ESP32-C3(~160KB heap)上风险较大;`for` 循环 `pos < capacity - 4` 的边界检查在 `first` 分支后逗号写入(行 133)未做边界检查,可能溢出 +- **建议**:分块 realloc,或对单条日志超长时截断并标记 + +### P1-21 关键任务创建失败不停止初始化 +- **文件**:[main/core/main.cpp:69-73,101-105,117-121,133-137](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L69-L137) +- **问题**:任务创建失败仅 `DEBUG_ERR`,不停止后续初始化,可能导致部分功能缺失但系统"看起来正常启动" +- **建议**:关键任务(health_mon、ota_relay)失败时 `ESP_ERROR_CHECK(ESP_FAIL)` 或进入 safe state + +### P1-22 s_ota_recv_buf 全局静态被双 handler 共享 +- **文件**:[main/server/http_handlers.cpp:36,249,337](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L36) +- **问题**:`static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];` 在 ESP32 与 GD32 两条路径间共享。httpd 默认单线程不会并发,但若未来 `max_open_sockets > 1` 启用并发请求,两个 `ota_upload_handler` 同时跑会互相覆盖 buffer +- **建议**:改为 handler 内栈分配(若 `OTA_BLOCK_SIZE < 4KB` 可行),或改为 per-request 分配 + +### P1-23 一次性任务创建失败上报不一致 +- **文件**:[main/server/ble_ota.cpp:816-821,875-878,896-899](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L816-L899) +- **问题**: + - ble_gd32_relay_wait_task 创建失败:`free(ctx); sendFinishEvent(ERR_GD32_RELAY, ...);` — 完整上报 + - ble_esp32_finish_and_reboot_task 创建失败:`free(finish_ctx); m_esp32RebootPending = false;` — **未上报 FINISH 错误事件**,前端会一直等 + - ble_esp32_restart_task 创建失败:`m_esp32RebootPending = false;` — **未上报任何错误** +- **建议**:统一"任务创建失败 → 释放 ctx → 复位 pending 标志 → 上报 FINISH 错误事件"三步 + +### P1-24 reorder slot malloc 失败静默丢弃 +- **文件**:[main/server/ble_ota.cpp:513-518](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L513-L518) +- **问题**:malloc 失败时返回 `ESP_OK` 让上层认为已处理,但实际数据丢失。若客户端窗口已用完(无重传预算),固件写入会永久卡在缺口。注释说"靠客户端重传"但客户端无法感知设备端 malloc 失败 +- **建议**:malloc 失败时至少发一次 `sendWindowAck` 报告当前 `expected`,让客户端立即重传缺口;或返回错误触发 abort + +### P1-25 protocol.h 死代码 +- **文件**:[main/transport/protocol.h:16-18,25-29](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/protocol.h#L16-L29) + [main/transport/protocol.cpp:96-118](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/protocol.cpp#L96-L118) +- **问题**: + 1. `protocol.h` 仍定义 `FRAME_SOF 0xEB`、`FRAME_MAX_PAYLOAD 64`、`FRAME_OVERHEAD 6`,但 `protocol.cpp` 注释(第 8-10 行)明确说"帧构建、AES-CBC 加解密、CRC16 等中间件代码已移除",这三个常量无任何引用 + 2. `session_ctx_t.iv[16]` 在 `sessionKeyInit` 中被 `memset(iv, 0, 16)` 后再无任何写入;`session_ctx_t.used` 初始化为 0 后再无任何代码置 1。整个 `session_ctx_t` 实际只用到 `key[16]` +- **建议**:删除 `FRAME_SOF`/`FRAME_MAX_PAYLOAD`/`FRAME_OVERHEAD`;精简 `session_ctx_t` 为 `{ uint8_t key[16]; }` 或整体移除 + +### P1-26 GD32 OTA 写入序列两路径重复 +- **文件**:[main/server/ble_ota.cpp:1113-1208](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1113-L1208)(BLE)vs [main/server/http_handlers.cpp:314-413](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L314-L413)(HTTP) +- **问题**:两路径都执行相同序列:`beginGd32Storage` → `saveGd32Meta` → 循环 `writeGd32Chunk` + `updateGd32Meta` → `completeGd32Storage` + `finalizeGd32Checksum`。但 HTTP 路径多了 `eraseGd32Partition` 和 `clearGd32Meta`(行 327、335),BLE 路径没有。这暗示 BLE 路径可能依赖前次 HTTP 上传后的已擦除状态,或在 `beginGd32Storage` 内隐式擦除——这种隐式假设是 bug 温床 +- **建议**:抽取 `OtaCoordinator::storeGd32FirmwareFromStream(...)` 接收"读块回调",BLE 与 HTTP 各自实现回调,统一擦除/写入/meta/完成序列 + +--- + +## P2 优先(样板抽取 / 配置化 / 风格统一) + +### P2-A 跨模块样板抽取(高 ROI) + +| # | 主题 | 涉及位置 | +|---|---|---| +| P2-27 | Hex 编解码工具统一 | [http_handlers.cpp:460-463,558-562,629-633](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L460-L633)、[ble_ota.cpp:174-178,642-649,861-863](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L174-L863) 共 6+ 处。**关键 bug**:SHA-256 在 BLE 路径大小写混用(`shaBytesToHexUpper` 大写 vs 行 861-863 `%02x` 小写),HTTP 路径用大写 `%02X`,前端比对 SHA 时大小写敏感会出错 | +| P2-28 | SHA-256 → hex 循环 | [esp32_ota_core.cpp:230-234,254-257,290-293,323-325](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L230-L325) 4 处 + [gd32_flash_store.cpp:283-286,316-320](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L283-L320) 2 处。提取 `static void sha256ToHex(const uint8_t in[32], char out[65])` | +| P2-29 | HTTP JSON body 接收+解析 | [http_handlers.cpp:71-95,436-452,522-535,595-608](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L71-L608) 4 处。缓冲区上限不一(256/512)。提取 `http_recv_json(httpd_req_t*, char*, size_t, cJSON**)` | +| P2-30 | HTTP 错误响应样板 | [http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp) 17 处 `httpd_resp_send_err + return ESP_FAIL`。提取 `http_send_json_error/http_send_json_ok` | +| P2-31 | 忙状态判断 if 链 | [ota_coordinator.cpp:136-140](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L136-L140)(beginGd32Storage) vs [ota_coordinator.cpp:346-350](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L346-L350)(beginEsp32Ota)。提取 `bool isAnyChannelBusy() const` | +| P2-32 | 三个校验函数重复 metaLoad 样板 | [gd32_flash_store.cpp:265-275,292-298,331-351](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L265-L351) | + +### P2-B 函数过长 / 高圈复杂度 + +| # | 函数 | 行数 | 文件 | +|---|---|---|---| +| P2-33 | ble_ota_process_data_batch | 245 行 | [ble_ota.cpp:975-1219](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L975-L1219)(见 P0-8) | +| P2-34 | ota_upload_handler | 240 行 | [http_handlers.cpp:180-420](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L180-L420)。ESP32/GD32 双路径混杂,共享 `pre_read_buf`/`pre_read_len`。拆为 `handle_esp32_upload`/`handle_gd32_upload` | +| P2-35 | ble_ota_ctrl_access | 190 行 | [ble_ota.cpp:717-907](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L717-L907)。3 命令分支长度极不均(0xF7 90 行 / 0xF8 54 行 / 0xFA 19 行),`session_id` 提取重复 | +| P2-36 | doRelayPackets | 180 行 + goto | [ota_relay.cpp:936-1115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L936-L1115)(见 P0-7) | +| P2-37 | doFinishAndVerify | 100 行双职责 | [ota_relay.cpp:1118-1216](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1118-L1216)(见 P1-13) | +| P2-38 | app_main | 124 行 | [main.cpp:30-154](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L30-L154)。拆为 `initEarlyServices` + `createOtaTasks` + `markAppValid` | + +### P2-C 配置化(magic number → app_config.h) + +| # | 主题 | 位置 | +|---|---|---| +| P2-39 | 任务栈/优先级散落硬编码 | [main.cpp:63,65,95,97,111,113,127,129](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L63)、[ble_ota.cpp:812,871,892,1723](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L812)、[wifi_ota.cpp:623](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L623)、[http_handlers.cpp:905-913](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L905-L913)(task_metrics 必须手工同步,已出现一处不对齐风险) | +| P2-40 | HTTP server config | [wifi_ota.cpp:419-424](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L419-L424)(`max_uri_handlers=32` 实际只用 18) | +| P2-41 | BLE 连接参数 | [ble_ota.cpp:1395-1401](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1395-L1401)(itvl/latency/supervision) | +| P2-42 | AP 静态 IP | [wifi_ota.cpp:234-236,240,258-259](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L234-L259) | +| P2-43 | BLE_OTA_ACK_WINDOW 位置 | [ble_ota.cpp:58](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L58) 用 `#define` 应移入 app_config.h 作 `inline constexpr` | +| P2-44 | debug_log magic number | [debug_log.cpp:199,317,352,365,369](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L199)(1024/512/600/32) | +| P2-45 | 健康巡检周期 | [health_monitor.cpp:190,208](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L190)(1000000/5000) | +| P2-46 | 温度采样间隔 | [health_monitor.cpp:190](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L190) | + +### P2-D 死代码 / 未使用配置 + +| # | 项 | 位置 | +|---|---|---| +| P2-47 | `DEBUG_LOG_ENABLED` 全代码库零引用 | [app_config.h:83](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L83) | +| P2-48 | `GD32_CHIP_UID[]` 占位 | [app_config.h:120-128](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L120-L128) | +| P2-49 | `BLE_OTA_PACKET_OVERHEAD` 零引用 | [app_config.h:104](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L104) | +| P2-50 | `WIFI_RSSI_WEAK` 零引用 | [health_monitor.h:33](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L33) | +| P2-51 | `DEBUG_LOG_NONE/ALL` 零引用 | [debug_log.h:35,40](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L35) | +| P2-52 | `WIFI_STA_USE_STATIC_IP` 临时调试开关 | [app_config.h:36](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L36) | +| P2-53 | `ota_snapshot.h` 仅聚合无价值 | [ota_snapshot.h](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_snapshot.h) 全文 | + +### P2-E 其他低风险风格类 + +| # | 项 | 位置 | +|---|---|---| +| P2-54 | SpiMaster 公共接口混合多个抽象层 | [spi_master.h:60-100](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.h#L60-L100)(上轮 D 残留) | +| P2-55 | DebugLog 未用 RAII(与 HealthMonitor 风格不一) | [debug_log.cpp:53-60,78-108,162-174](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L53-L174) | +| P2-56 | DebugLog logLevel 暴露在 public | [debug_log.h:86](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L86) | +| P2-57 | DebugLog 三路 sink 抽象为列表 | [debug_log.cpp:300-333](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L300-L333)(上轮 E 项遗留) | +| P2-58 | DebugLog send() 返回值无人检查,可改 void | [debug_log.cpp:329-332](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L329-L332) | +| P2-59 | ensureUdpSocket 失败后每条日志都重试 | [debug_log.cpp:278-292](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L278-L292) | +| P2-60 | DebugLog log() 三次缓冲区拼接 | [debug_log.cpp:342-373](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L342-L373) | +| P2-61 | DebugLog 头注释与实现不一致(UDP 无开关) | [debug_log.h:6-7](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L6-L7) vs [debug_log.cpp:311-324](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L311-L324) | +| P2-62 | DebugLog 重复 include | [debug_log.h:23-24](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L23-L24) vs [debug_log.cpp:20,23](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L20) | +| P2-63 | health_monitor WIFI_RSSI_NORMAL 别名位置错误 | [health_monitor.h:32-33](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L32-L33) | +| P2-64 | health_state_t 与 health_status_t 两套枚举语义重叠 | [health_monitor.h:16-29](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L16-L29) | +| P2-65 | health_monitor 阈值检查样板可抽 applyThresholdAlarm | [health_monitor.cpp:129-156,333-357](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L129-L357) | +| P2-66 | health_monitor checkBeforeOta 与 checkBeforeOtaRelay 分级样板重复 | [health_monitor.cpp:232-311](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L232-L311) | +| P2-67 | health_monitor taskEntry 职责混杂 | [health_monitor.cpp:165-210](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L165-L210) | +| P2-68 | health_monitor logEvent 与相邻 DEBUG 重复日志 | [health_monitor.cpp:220-223](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L220-L223) | +| P2-69 | health_monitor tempMonitorCallback 注释硬编码温度值 | [health_monitor.cpp:293](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L293) | +| P2-70 | gd32_flash_store NVS 字段名硬编码 | [gd32_flash_store.cpp:136-185](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L136-L185) | +| P2-71 | gd32_flash_store tag "OTA" 与模块名不一致 | [gd32_flash_store.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp) 全文 | +| P2-72 | gd32_flash_store SHA 长度 32 裸数字 | [gd32_flash_store.h:26](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.h#L26) 等 | +| P2-73 | gd32_flash_store flashWriteChunk 错误清理重复 | [gd32_flash_store.cpp:74-108](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L74-L108) | +| P2-74 | gd32_flash_store computeFlashSha256 错误清理重复 | [gd32_flash_store.cpp:224-262](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L224-L262) | +| P2-75 | gd32_flash_store 全零 checksum 检查可简化为 memcmp | [gd32_flash_store.cpp:299-302](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L299-L302) | +| P2-76 | gd32_flash_store flashWriteChunk 非对齐警告条件恒真 | [gd32_flash_store.cpp:65-72](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L65-L72) | +| P2-77 | gd32_flash_store m_partition 懒初始化无线程安全 | [gd32_flash_store.cpp:30-40](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L30-L40) | +| P2-78 | main.cpp 4 处 xTaskCreatePinnedToCore 样板 | [main.cpp:60-137](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L60-L137) | +| P2-79 | main.cpp 启动横幅分隔线硬编码 4 次 | [main.cpp:50,56,149,153](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L50-L153) | +| P2-80 | main.cpp xRet 变量重复使用 | [main.cpp:59,69,92,101,108,117,124,133](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L59-L133) | +| P2-81 | http_handlers.cpp 单文件 TAG 混用 OTA/WIFI/SPI | [http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp) 全文 | +| P2-82 | canFdSend 返回 -1 无法区分失败模式 | [canfd_bridge.cpp:59-68](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/canfd_bridge.cpp#L59-L68) | +| P2-83 | otaCrc8 实为 XOR 校验和,命名误导 | [spi_master.cpp:180-187](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L180-L187) | +| P2-84 | otaParseFrame/otaFindFrame 验证逻辑重复 | [spi_master.cpp:225-279](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L225-L279) | +| P2-85 | CAN 帧 vs SPI OTA 帧两层 framing 未抽象 | [canfd_bridge.cpp:12-51](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/canfd_bridge.cpp#L12-L51) + [spi_master.cpp:198-279](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L198-L279) | +| P2-86 | BLE OTA WiFi 共存标志位冗余 | [ble_ota.cpp:226-250](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L226-L250) | +| P2-87 | http_handlers task_metrics 硬编码任务栈大小 | [http_handlers.cpp:905-913](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L905-L913) | +| P2-88 | notifyRelayStarted/Complete 纯转发中间层 | [ota_coordinator.cpp:264-307](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L264-L307) | +| P2-89 | notifyEsp32FinalizeComplete 与 notifyEsp32Rebooting 几乎相同 | [ota_coordinator.cpp:409-428](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L409-L428) | +| P2-90 | doFinishAndVerify 成功路径与 reset() session 清理重复 | [ota_relay.cpp:1178-1182 vs 487-504](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1178-L1182) | +| P2-91 | metaLoad 单次中继被调 3 次 | [ota_coordinator.cpp:213-221](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L213-L221) + [ota_relay.cpp:311-316,438-440](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L311-L440) | +| P2-92 | forwardFirmware 中 expected_ack=0xF0F0F0F0 魔法数字 | [ota_relay.cpp:1024](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1024) | +| P2-93 | doRelayPackets:918 WARN 级别输出纯配置信息(调试残留) | [ota_relay.cpp:918](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L918) | +| P2-94 | app_config 配置项类型不一致(uint8_t 应为 bool) | [app_config.h:25,36,83](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L25) | + +--- + +## 不建议改的地方(保留上轮结论) + +- OTA 事件状态机的 5 步驱动顺序、健康检查时机、`OtaCoordinator` 的 watchdog 自愈、`forceCleanup` 仅在 `isIdle()` 启动。 +- `Esp32OtaCore::s_mutex + s_occupied` 双层 mutex 语义("持锁查占用" + "持续占用期")。 +- 单例模式已统一(`static XXX inst; return inst;` + `= delete` 拷贝构造/赋值)。 +- 无 TODO/FIXME/XXX/HACK 标记,无注释掉的死代码。 + +--- + +## 预估收益 + +- 修复 4 项功能性 bug(校验后门 / RSSI 状态机降级 / 固件大小注释 / 凭证安全)。 +- 消除 6 处数据竞争(OtaRelay 状态访问 + setWifiConnected)。 +- 6 个超长函数拆分(245/240/190/180/124/100 行),可维护性显著提升。 +- `main/` 行数预计再减约 400~500 行(去重 + 死代码)。 +- 双通道(BLE/HTTP)一致性通过 `validateGd32Size` / `storeGd32FirmwareFromStream` 抽取彻底对齐。 +- protocol.h / app_config.h 死代码清理约减 50+ 行。 + +--- + +## 推荐落地顺序 + +1. **第 1 批(P0)**:P0-1 ~ P0-8。先修功能正确性 / 安全性 bug。P0-7/P0-8 函数拆分风险高,需配合 GD32 全链路 OTA 回归。 +2. **第 2 批(P1.A)**:OtaRelay 状态成员封装一组(P1-A + P1-15),消除数据竞争,原子改动。 +3. **第 3 批(P1.B)** +4. **第 4 批(P2.A/B)**:样板抽取(P2-27 ~ P2-32)+ 函数拆分(P2-33 ~ P2-38)。 +5. **第 5 批(P2.C/D/E)**:配置化(P2-39 ~ P2-46)+ 死代码删除(P2-47 ~ P2-53)+ 风格统一(P2-54 ~ P2-94),可随业务变更顺手做。