docs(readme): 更新文档说明
- 添加项目使用指南 - 完善API接口说明 - 修正错误的配置示例
This commit is contained in:
@@ -1,168 +0,0 @@
|
||||
# CAN 通信协议文档
|
||||
|
||||
> 更新日期:2026-10-10
|
||||
> 涉及固件:GD32 DCU_APP + ESP32 ESP32_COMM
|
||||
> SPI:**Link v2 only** · 时钟 4 MHz
|
||||
> APP 最大业务 payload:`kTxBufSize(256) - V2_OVERHEAD(20) = 236`
|
||||
> BOOT 最大业务 payload:`kTxBufSize(128) - V2_OVERHEAD(20) = 108`
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
ESP32 通过 SPI Link v2 与 GD32 通信,实现三路 CAN/CANFD 的收发控制。CAN 相关 SPI 命令占用 `0xC0` ~ `0xC3`、`0xC7`、`0xC8`。
|
||||
|
||||
| 帧类型 | 名称 | 方向 | 用途 |
|
||||
|--------|------|------|------|
|
||||
| `0xC0` | kTypeCanSend | ESP32 → GD32 | 发送一帧 CAN 数据 |
|
||||
| `0xC1` | kTypeCanFeedback | 双向 | 通道统计 + ID 快照页;GD32 亦可 1Hz 主动推送 |
|
||||
| `0xC2` | kTypeCanSetMode | ESP32 → GD32 | 切换三路 CAN 接收过滤模式 |
|
||||
| `0xC3` | kTypeIdSnapshot | ESP32 → GD32 | 完整 ID 条目(含 seq/rx_count) |
|
||||
| `0xC7` | kTypeCanBurstSend | ESP32 → GD32 | 跨通道突发发送(一次事务背靠背入多邮箱) |
|
||||
| `0xC8` | kTypeCanSetSilent | ESP32 → GD32 | 切换指定通道收发器 Silent(只听不发) |
|
||||
|
||||
---
|
||||
|
||||
## 2. SPI 协议帧格式(v2)
|
||||
|
||||
所有 SPI 帧遵循 SPI Link v2(`spi_link/spi_link_v2.h`):
|
||||
|
||||
```
|
||||
[SOF0 A5][SOF1 5A][VER 02][FLAGS][SESSION 4LE][TX_SEQ][ACK_SEQ][CREDIT][TYPE][LEN 2LE][RSV 2][PAYLOAD][CRC32 4LE]
|
||||
```
|
||||
|
||||
- 开销 `V2_OVERHEAD = 20`
|
||||
- CRC32 MPEG-2(`0x04C11DB7`,无反射)
|
||||
- **无** v1 单字节 SOF + CRC8 路径
|
||||
|
||||
应答 TYPE 常用:`0x80` ACK · `0x81` WAIT · `0x82` ERROR
|
||||
|
||||
---
|
||||
|
||||
## 3. 0xC0 — CAN 发送
|
||||
|
||||
### 请求 payload
|
||||
|
||||
```
|
||||
[0] channel 0/1/2
|
||||
[1..4] ext_id 扩展帧 ID(Big-Endian,29 位)
|
||||
[5..N] data CAN 数据负载(0~64 字节;可无,对应 DLC=0)
|
||||
```
|
||||
|
||||
最短 payload 为 **5** 字节(仅 ch+id,DLC=0,例如 `CANFD_BOOT`)。
|
||||
|
||||
### 应答 payload
|
||||
|
||||
```
|
||||
[0] 0x00(ACK)或错误码 E0~E6
|
||||
```
|
||||
|
||||
| 码 | 含义 |
|
||||
|----|------|
|
||||
| E0 | payload 过短(< 5 字节) |
|
||||
| E1 | 通道非法 |
|
||||
| E2 | BUS_OFF |
|
||||
| E3 | TEC 过高(>96,对端 ACK 失败率高) |
|
||||
| E4 | 邮箱满 |
|
||||
| E5 | 参数无效 |
|
||||
| E6 | 通道处于收发器 Silent(只听不发),入口即拒,不计错误计数 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 0xC1 — 通道统计 + ID 快照(核心)
|
||||
|
||||
GD32 可 1Hz 主动 `requestEsp32Read(0xC1)` 推送;ESP32 亦支持被动查询。
|
||||
打包上限:`kFeedbackPayloadLen = kTxBufSize - V2_OVERHEAD = 236`。
|
||||
|
||||
### 请求 payload(被动)
|
||||
|
||||
```
|
||||
[0] channel 0/1/2
|
||||
[1] page 0 = 条目 0~7,1 = 条目 8~15
|
||||
```
|
||||
|
||||
### 应答 / 推送 payload(变长)
|
||||
|
||||
固定头 **28** 字节 + 条目区:
|
||||
|
||||
```
|
||||
[0] channel
|
||||
[1] page
|
||||
[2] total_count
|
||||
[3] entry_count
|
||||
[4] mode 0=全收 1=过滤 2=桥接
|
||||
[5..8] snapshot_seq LE
|
||||
[9..12] rx_count LE
|
||||
[13..16] tx_count LE
|
||||
[17..20] rx_drop_count LE
|
||||
[21..24] error_count LE
|
||||
[25] tec
|
||||
[26] rec
|
||||
[27] bus_state bit0=init bit1=控制器睡眠 bit2=EP bit3=bus_off bit4=EW bit5=只听不发(Silent)
|
||||
[28..] 条目
|
||||
```
|
||||
|
||||
### 单条目(8 + dlc)
|
||||
|
||||
```
|
||||
[+0..+3] id LE
|
||||
[+4] dlc
|
||||
[+5] flags bit0=FD bit1=BRS bit2=ESI
|
||||
[+6..+7] reserved
|
||||
[+8..] data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 0xC2 — 模式切换
|
||||
|
||||
```
|
||||
req[0] mode 0=全收 1=过滤 2=CAN0→1/2 桥接
|
||||
rsp[0] actual 生效 mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5.1 0xC8 — 收发器 Silent(只听不发)
|
||||
|
||||
GD32 把该通道 TCAN1057AV 的 **S 脚**(CAN0=PA8 / CAN1=PB14 / CAN2=PB9)置高:
|
||||
驱动器关断,**接收机照常工作、RXD 仍镜像总线**(手册 Table 8-4),所以静默期间接收与
|
||||
0xC1 上报完全不受影响。S 脚内部上拉,MCU 复位/浮空即为 Silent。
|
||||
|
||||
```
|
||||
req[0] channel 0/1/2
|
||||
req[1] silent 1=进 Silent(只听不发) 0=回 Normal
|
||||
rsp[0] channel 回显
|
||||
rsp[1] actual 实际生效(1=Silent;与 GD32 发送门控同一个值)
|
||||
rsp[2] ch_en 该通道是否使能
|
||||
```
|
||||
|
||||
- ERROR:`E0`=payload 过短 · `E1`=通道非法/未使能 · `E2`=S 脚回读与意图不符(内部标志已回滚,不会留下"报了静默其实没静默"的状态)
|
||||
- 静默生效期间,该通道所有发送入口(0xC0 / 0xC7 / 桥接 / 电机 relay)回 **E6**,且不推进任何错误计数
|
||||
- 进 Silent 前 GD32 先 abort 本通道 PENDING 邮箱(驱动器已关,留着只会白涨 TEC)
|
||||
- 退 Silent 时 GD32 忙等收发器模式建立时间 `tMODE = 20 µs`(手册 6.9)再放开发送门
|
||||
- **不持久化**:上电 init 一律回 Normal
|
||||
- 状态读取走 0xC1 的 `bus_state` bit5,ESP32 侧不另开轮询端点
|
||||
- ESP32 HTTP:`POST /can/silent` body `{"ch":0,"on":1}`;OTA / send_batch 独占 SPI 期间返回 `{"ok":false,"err":"paused"}`
|
||||
|
||||
---
|
||||
|
||||
## 6. 0xC3 — ID 快照(完整字段)
|
||||
|
||||
固定头 4B + 条目 `(16 + dlc)`(含 id/seq/rx_count/dlc/flags/rsv/data)。
|
||||
上限同 APP:`256 - V2_OVERHEAD = 236`。
|
||||
|
||||
---
|
||||
|
||||
## 7. 实现索引
|
||||
|
||||
| 端 | 文件 |
|
||||
|----|------|
|
||||
| GD32 0xC1/C2/C8 | `DCU_APP/.../app_spi_cmd_can_feedback.cpp`(0xC8 复用同一 handler) |
|
||||
| GD32 S 脚/发送门控 | `DCU_APP/.../device/canfd.cpp` → `setTxSilent()` / `isTxSilent()` |
|
||||
| GD32 0xC3 | `app_spi_cmd_id_snapshot.cpp` |
|
||||
| GD32 0xC0 | `app_spi_cmd_can_bridge.cpp` |
|
||||
| ESP32 解析/桥 | `ESP32_COMM/main/transport/canfd_bridge.*` |
|
||||
| ESP32 读预算 | `spi_master.cpp` → `spiRespReadBudget()` |
|
||||
| 帧格式 | `spi_link/spi_link_v2.h` |
|
||||
| 图示文档 | `SPEC/communication_ids.html` · `SPEC/spi_flow_diagram.html` |
|
||||
@@ -1,301 +0,0 @@
|
||||
# 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<bool>`)
|
||||
- **备注**:上轮 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),可随业务变更顺手做。
|
||||
@@ -1,702 +0,0 @@
|
||||
# ESP32-C3FH4 OTA 中继架构设计
|
||||
|
||||
> **⚠️ v1 已废弃**(2026-07-14 标注)
|
||||
>
|
||||
> 本文档描述的是 v1 OTA 中继设计(旧帧格式 0xEB/6B/CRC16、旧引脚 GPIO6-9、旧命令 0xF0-0xF7),
|
||||
> 与当前代码实现不符。当前 SPI 通信协议以 **`SPEC/spi_link_v2.md`** 为单一权威来源
|
||||
> (SPI Link v2:16B 头帧、SESSION/SEQ/CREDIT、5-wire SPI、HELLO 握手)。
|
||||
>
|
||||
> 本文档保留作历史参考,不再维护。所有新代码与文档改动请参照 SPEC。
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
```
|
||||
[手机APP/PC] --WiFi--> [ESP32-C3FH4] --SPI--> [GD32E508VET6]
|
||||
|
|
||||
+--BLE---> [手机APP/PC]
|
||||
```
|
||||
|
||||
- **ESP32 角色**:WiFi/BLE 接收端 + SPI 透传(主机)
|
||||
- **GD32 角色**:OTA 执行端(从机),已有完整 OTA 流程
|
||||
- **ESP32 不负责 OTA 逻辑**,只做数据转发
|
||||
|
||||
---
|
||||
|
||||
## 2. 硬件连接
|
||||
|
||||
### SPI 接口
|
||||
|
||||
| ESP32 (Master) | GD32 (Slave) | 说明 |
|
||||
|---|---|---|
|
||||
| GPIO6 (SPIIO0) | PA4 (NSS) | 片选 |
|
||||
| GPIO7 (SPIQ) | PA5 (SCK) | 时钟 |
|
||||
| GPIO8 (SPIWP) | PA6 (MOSI) | 主发从收 |
|
||||
| GPIO9 (SPIHD) | PA7 (MISO) | 主收从发 |
|
||||
|
||||
> ⚠️ GD32E508 使用标准 SPI(PA4-NSS, PA5-SCK, PA6-MOSI, PA7-MISO)
|
||||
> GD32 已内置 BOOT+APP 模式,ESP32 通过 SPI 命令直接控制进入 Bootloader,无需额外 BOOT/RST 控制引脚。
|
||||
|
||||
---
|
||||
|
||||
## 3. 通信协议设计
|
||||
|
||||
ESP32 与 GD32 之间的 SPI 协议完整映射 GD32 原有 CANFD OTA 流程,分为四个阶段。
|
||||
|
||||
### 3.0 帧格式总览
|
||||
|
||||
所有 SPI 帧格式统一为:
|
||||
|
||||
```
|
||||
| SOF (1B) | CMD (1B) | LEN (2B, LE) | PAYLOAD (LEN bytes) | CRC16 (2B, LE) |
|
||||
```
|
||||
|
||||
- **SOF**: 0xEB 固定帧头
|
||||
- **CMD**: 命令码(见下表)
|
||||
- **LEN**: Payload 长度(不含帧头和 CRC)
|
||||
- **CRC16**: Modbus CRC16,低字节在前
|
||||
|
||||
> 注意:CANFD 侧(原协议)的分包为 48 bytes/packet;SPI 中继时,ESP32 接收 APP/PC 的分包可能更大,内部透传时不做分包合并,只做协议映射。
|
||||
|
||||
---
|
||||
|
||||
### 3.1 第一阶段:安全握手与认证(步骤 1-6)
|
||||
|
||||
目的是建立加密安全通道,防止非法刷写。
|
||||
|
||||
#### Step 1 & 2 — 握手发起与响应
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `0x00 AC` | 发起 OTA 请求 |
|
||||
| GD32 → ESP32 | `0x00 AC` | 响应信号,表示准备好进入 OTA 模式 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF0 (HANDSHAKE_REQ), Payload=[] → ESP32 发送 0x00 AC
|
||||
CMD=0xF0 (HANDSHAKE_REQ), Payload=[] ← GD32 回复 0x00 AC
|
||||
```
|
||||
|
||||
#### Step 3 — 主机挑战(发送随机数)
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `nonce_server` (16 bytes) | 16字节随机数,用于生成会话密钥 |
|
||||
|
||||
**搅伴算法**(时间 + 内存地址 + rand → 3轮单字节 → 2轮4字节)由 ESP32 实现,生成方法需与 GD32 预置的主密钥匹配。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF1 (CHALLENGE), Payload=[16 bytes nonce_server]
|
||||
```
|
||||
|
||||
#### Step 4 — 从机响应与身份验证
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | 握手数据包 | 使用主密钥 AES 加密随机数 + 会话密钥加密自身信息 |
|
||||
|
||||
GD32 获取芯片唯一码,使用主密钥通过 **AES 算法**加密随机数,并使用会话密钥加密自身信息,打包发送。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF1 (CHALLENGE), Payload=[握手数据包] ← GD32 响应
|
||||
```
|
||||
|
||||
Payload 结构(AES 加密后的数据):
|
||||
|
||||
```
|
||||
| AES_encrypt(nonce_server, master_key) (16B) | AES_encrypt(device_info, K_session) (16B) |
|
||||
```
|
||||
|
||||
#### Step 5 — 主机验证与会话密钥分发
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | 变换后的随机数 | 使用会话密钥加密后发送 |
|
||||
|
||||
ESP32 验证随机数,解密获取会话密钥 (K_session),对随机数进行变换后用会话密钥加密发送。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF2 (SESSION_KEY), Payload=[AES_encrypt(transformed_nonce, K_session)]
|
||||
```
|
||||
|
||||
#### Step 6 — 认证完成
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` | 安全通道建立完成 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF2 (SESSION_KEY), Payload=[] ← GD32 回复 0xBA C0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 第二阶段:传输准备(步骤 7-8)
|
||||
|
||||
#### Step 7 — 发送大小
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `Data Size` (4 bytes, LE) | APP 总数据大小 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF3 (SEND_SIZE), Payload=[size(4B, LE)]
|
||||
```
|
||||
|
||||
#### Step 8 — 确认大小
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` + 包序号 | 确认收到大小信息 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF3 (SEND_SIZE), Payload=[seq(2B)] ← GD32 回复 0xBA C0 + seq
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 第三阶段:数据分包传输循环(步骤 9-12)
|
||||
|
||||
刷写核心部分,数据加密分包发送。
|
||||
|
||||
#### Step 9 — 发送数据包
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | OTA 程序数据包 | 经会话密钥加密,含 CRC 校验码 |
|
||||
|
||||
分包大小约 **48 bytes/packet**(与原 CANFD 协议一致)。
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF4 (DATA_PACKET), Payload=[encrypted_data(48B) + CRC]
|
||||
```
|
||||
|
||||
#### Step 10 — 从机校验与确认
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` + 包序号 | 校验通过,请求下一包 |
|
||||
| GD32 → ESP32 | `0xBA C0` + 错误指示 | 校验失败,要求重发当前包(限次重试) |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF4 (DATA_PACKET), Payload=[seq(2B)] ← 正常 ACK
|
||||
CMD=0xF4 (DATA_PACKET), Payload=[seq(2B) + 0xFF] ← 错误重发请求
|
||||
```
|
||||
|
||||
#### Step 11 — 继续发送
|
||||
|
||||
主机发送下一包加密数据。
|
||||
|
||||
循环机制:`总循环次数 = 1 + 总APP大小 // 48 Byte`
|
||||
|
||||
#### Step 12 — 最后一包
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | 最后一包数据 | IFEND(结束标志位)置位 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF5 (DATA_END), Payload=[encrypted_data(N) + CRC + IFEND flag]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 第四阶段:结束与跳转(步骤 13-17)
|
||||
|
||||
#### Step 13 — 传输完成确认
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0xBA C0` + `FF FF FF FF` | 所有数据包传输完成并校验通过 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF6 (TRANSFER_DONE), Payload=[0xFF, 0xFF, 0xFF, 0xFF] ← GD32 完成确认
|
||||
```
|
||||
|
||||
#### Step 14 — 刷写结束指令
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| ESP32 → GD32 | `0x0A AC` | 刷写结束信号 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF7 (FINISH_OTA), Payload=[] → ESP32 发送 0x0A AC
|
||||
```
|
||||
|
||||
#### Step 15 — 内部跳转
|
||||
|
||||
GD32 内部执行动作:尝试跳转 APP。
|
||||
|
||||
> ESP32 无需等待此步骤完成,由 GD32 自行处理。
|
||||
|
||||
#### Step 16 — 最终确认
|
||||
|
||||
| 方向 | Signal | 说明 |
|
||||
|---|---|---|
|
||||
| GD32 → ESP32 | `0x0A AC` | 确认 APP 运行有效,回复刷写结束信号 |
|
||||
|
||||
**对应 SPI 帧:**
|
||||
|
||||
```
|
||||
CMD=0xF7 (FINISH_OTA), Payload=[] ← GD32 回复 0x0A AC
|
||||
```
|
||||
|
||||
#### Step 17 — 清理
|
||||
|
||||
流程结束,GD32 清理会话密钥、RAM 缓存等。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 命令码汇总
|
||||
|
||||
| CMD | 名称 | 方向 | Payload | 对应 OTA 阶段 |
|
||||
|---|---|---|---|---|
|
||||
| 0xF0 | `HANDSHAKE_REQ` | Bidirectional | 见 Step 1-2 | 阶段一 |
|
||||
| 0xF1 | `CHALLENGE` | Bidirectional | nonce(16B) / 握手数据包 | 阶段一 |
|
||||
| 0xF2 | `SESSION_KEY` | Bidirectional | 变换后随机数 / 0xBA C0 | 阶段一 |
|
||||
| 0xF3 | `SEND_SIZE` | ESP32 → GD32 | size(4B, LE) | 阶段二 |
|
||||
| 0xF4 | `DATA_PACKET` | ESP32 → GD32 | seq(2B) + data(48B) + CRC | 阶段三 |
|
||||
| 0xF5 | `DATA_END` | ESP32 → GD32 | seq(2B) + data(N) + CRC + IFEND | 阶段三 |
|
||||
| 0xF6 | `TRANSFER_DONE` | GD32 → ESP32 | 0xFF 0xFF 0xFF 0xFF | 阶段四 |
|
||||
| 0xF7 | `FINISH_OTA` | Bidirectional | 0x0A AC | 阶段四 |
|
||||
| 0x80 | `ACK` | GD32 → ESP32 | cmd(1B) + status(1B) | 通用 |
|
||||
| 0x81 | `ERROR` | GD32 → ESP32 | cmd(1B) + err_code(1B) | 通用 |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 时序图
|
||||
|
||||
```
|
||||
ESP32 GD32
|
||||
| |
|
||||
|---- HANDSHAKE_REQ ---->| 0x00 AC (Step 1)
|
||||
|<---- HANDSHAKE_REQ ---| 0x00 AC (Step 2)
|
||||
| |
|
||||
|---- CHALLENGE ------->| nonce_server 16B (Step 3)
|
||||
|<---- CHALLENGE ------| 握手数据包 (Step 4)
|
||||
| |
|
||||
|---- SESSION_KEY ----->| 变换后随机数 (Step 5)
|
||||
|<---- SESSION_KEY -----| 0xBA C0 (Step 6) ← 安全通道建立
|
||||
| |
|
||||
|---- SEND_SIZE -------->| Data Size (Step 7)
|
||||
|<---- SEND_SIZE -------| 0xBA C0 + seq (Step 8)
|
||||
| |
|
||||
|==== DATA LOOP ========>| (Step 9-11, 每包 48B + CRC)
|
||||
|<==== DATA LOOP =======| 0xBA C0 + seq (正常) / 错误重发
|
||||
| |
|
||||
|---- DATA_END --------->| 最后一包 + IFEND (Step 12)
|
||||
| |
|
||||
|<---- TRANSFER_DONE ---| 0xBA C0 + FF FF FF FF (Step 13)
|
||||
| |
|
||||
|---- FINISH_OTA ------->| 0x0A AC (Step 14)
|
||||
|<---- FINISH_OTA ------| 0x0A AC (Step 16) ← GD32 跳转 APP
|
||||
| |
|
||||
* 清理会话密钥 & 缓存 * (Step 17)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 ACK 状态码
|
||||
|
||||
| status | 含义 |
|
||||
|---|---|
|
||||
| 0x00 | OK |
|
||||
| 0x01 | 命令不支持 |
|
||||
| 0x02 | 认证失败 |
|
||||
| 0x03 | 大小超出范围 |
|
||||
| 0x04 | CRC 校验失败 |
|
||||
| 0x05 | 重发次数超限 |
|
||||
| 0x06 | 会话密钥无效 |
|
||||
| 0x07 | Flash 写入失败 |
|
||||
|
||||
---
|
||||
|
||||
### 3.8 重发机制
|
||||
|
||||
- GD32 检测到 CRC 错误时,回复当前包序号 + 错误指示
|
||||
- ESP32 重新发送当前包
|
||||
- 最大重发次数由 GD32 固件定义(建议 3 次)
|
||||
- 超限后 ESP32 终止传输并上报 APP
|
||||
|
||||
---
|
||||
|
||||
## 4. ESP32 软件架构
|
||||
|
||||
### 4.1 目录结构
|
||||
|
||||
```
|
||||
esp32_ota_relay/
|
||||
├── main/
|
||||
│ ├── main.c
|
||||
│ ├── app_main.c
|
||||
│ ├── ota_relay.c # OTA 中继核心逻辑(含四阶段状态机)
|
||||
│ ├── spi_master.c # SPI 驱动
|
||||
│ ├── wifi_ota.c # WiFi OTA 接收
|
||||
│ ├── ble_ota.c # BLE OTA 接收
|
||||
│ └── protocol.c # 帧封装/解析/CRC/AES
|
||||
└── CMakeLists.txt
|
||||
```
|
||||
|
||||
### 4.2 模块职责
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ ESP32 OTA Relay │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ WiFi OTA │ │ BLE OTA │ │
|
||||
│ │ Server │ │ GATT │ │
|
||||
│ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ OTA Relay │ ← 四阶段状态机 │
|
||||
│ │ Dispatcher │ 透传加密数据 │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ Protocol │ ← 帧封装/解析/CRC/AES │
|
||||
│ │ Layer │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │SPI Master │ │
|
||||
│ └─────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 OTA Relay Core 状态机
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
// 阶段一:安全握手
|
||||
RELAY_HANDSHAKE_IDLE, // 等待 HANDSHAKE_REQ
|
||||
RELAY_HANDSHAKE_WAIT, // 等待 GD32 响应 0x00 AC
|
||||
RELAY_CHALLENGE_SENT, // 已发送挑战随机数
|
||||
RELAY_CHALLENGE_RESPOND, // 等待 GD32 握手数据包
|
||||
RELAY_SESSION_KEY_SENT, // 已发送会话密钥分发
|
||||
RELAY_AUTH_COMPLETE, // 认证完成,安全通道建立
|
||||
|
||||
// 阶段二:传输准备
|
||||
RELAY_SIZE_SENT, // 已发送 Data Size
|
||||
RELAY_SIZE_ACK, // 收到大小确认
|
||||
|
||||
// 阶段三:数据传输
|
||||
RELAY_RECEIVING, // 接收固件数据
|
||||
RELAY_PACKET_SENT, // 等待 GD32 ACK
|
||||
RELAY_RECEIVE_DONE, // 传输完成(收到 DATA_END ACK)
|
||||
|
||||
// 阶段四:结束跳转
|
||||
RELAY_TRANSFER_DONE, // 收到传输完成确认
|
||||
RELAY_FINISH_SENT, // 已发送刷写结束信号
|
||||
RELAY_DONE, // 全部完成
|
||||
|
||||
RELAY_ERROR
|
||||
} relay_state_t;
|
||||
```
|
||||
|
||||
### 4.4 安全相关说明
|
||||
|
||||
- **AES 加密**:由 ESP32 实现(需预置主密钥),GD32 解密验证
|
||||
- **会话密钥 K_session**:由 Step 4/5 双方协商生成
|
||||
- **搅伴算法**:ESP32 生成 nonce_server 时使用(时间 + 内存地址 + rand → 3轮单字节 → 2轮4字节)
|
||||
- **芯片唯一码**:GD32 提供,ESP32 用于密钥派生
|
||||
|
||||
> ⚠️ 主密钥和 AES 算法细节需由 GD32 固件侧提供,本设计仅做协议映射。
|
||||
|
||||
### 4.6 数据缓冲策略
|
||||
|
||||
**方案 A:存储后转发(先存 ESP32 Flash,再发 SPI)**
|
||||
|
||||
ESP32 将 APP 发送的固件完整存入 Flash,传输完成后再通过 SPI 转发给 GD32。
|
||||
|
||||
#### Flash 分区设计
|
||||
|
||||
假设固件最大 256KB,预留足够空间:
|
||||
|
||||
```yaml
|
||||
# Partitions
|
||||
# Name, Type, SubType, Offset, Size
|
||||
otadata, data, ota, 0x9000, 0x2000 # OTA 数据区标记
|
||||
gd32_ota, data, spiffs, 0xB000, 0x40000 # GD32 固件存储区 (256KB)
|
||||
app0, app, ota_0, 0x4B000, 0x330000 # ESP32 自身 APP
|
||||
app1, app, ota_1, 0x3DC000, 0x330000
|
||||
```
|
||||
|
||||
> `gd32_ota` 分区使用 spiffs 或自定义 data 分区,存放原始固件 binary。
|
||||
|
||||
#### 接收与存储流程
|
||||
|
||||
```
|
||||
APP ESP32 GD32
|
||||
| | |
|
||||
|--- DATA_PACKET (4KB) --->| |
|
||||
| |--- write to Flash ------>| (存 Flash,不发 SPI)
|
||||
|<--- ACK (stored) --------| |
|
||||
| | |
|
||||
|--- DATA_PACKET (4KB) --->| |
|
||||
| |--- write to Flash ------>| (存 Flash,不发 SPI)
|
||||
|<--- ACK (stored) --------| |
|
||||
| | |
|
||||
| ... 循环存完所有包 ... |
|
||||
| | |
|
||||
|--- DATA_END ------------->| |
|
||||
| | |
|
||||
| |--- ENTER_BOOT ----------->| (开始刷写)
|
||||
| |<--- ACK ------------------| (GD32 就绪)
|
||||
| | |
|
||||
| |=== 流式转发固件 =========>| (按 48B/packet 逐包发送)
|
||||
| |<=== ACK per packet =======| (每包 GD32 ACK)
|
||||
| | |
|
||||
| |--- FINISH_OTA ---------->| (刷写结束)
|
||||
| |<--- FINISH_OTA ----------| (GD32 跳转 APP)
|
||||
```
|
||||
|
||||
#### 断点续传设计
|
||||
|
||||
| 文件 | 内容 | 说明 |
|
||||
|---|---|---|
|
||||
| `ota_meta.json` | `{total_size, stored_size, chunk_count, last_seq}` | 记录传输进度 |
|
||||
|
||||
- APP 重连后先读 `ota_meta.json`,获取 `stored_size`
|
||||
- 从 `last_seq + 1` 继续发送剩余包
|
||||
- ESP32 追加写入 Flash(不需要从头擦写)
|
||||
|
||||
#### 存储完成判断
|
||||
|
||||
- APP 发送 `DATA_END` 后,ESP32 更新 `ota_meta.json` 的 `stored_size = total_size`
|
||||
- ESP32 检测到 `stored_size == total_size` 后,自动开始向 GD32 转发
|
||||
|
||||
#### 内存 Buffer
|
||||
|
||||
存储同时使用 2 × 4096 bytes 乒乓 buffer 接收 APP 数据,写入 Flash 后切换 buffer 继续接收。
|
||||
|
||||
---
|
||||
|
||||
## 5. WiFi OTA 实现
|
||||
|
||||
### 5.1 工作模式
|
||||
|
||||
- **AP 模式**:ESP32 作为热点,手机直连
|
||||
- **STA 模式**:ESP32 连接路由器,APP 通过 IP 访问
|
||||
|
||||
### 5.2 HTTP OTA 服务器
|
||||
|
||||
```
|
||||
HTTP POST /ota/upload
|
||||
Content-Type: application/octet-stream
|
||||
Body: <raw firmware binary>
|
||||
|
||||
Response:
|
||||
{"status": "ok", "progress": 100}
|
||||
```
|
||||
|
||||
### 5.3 APP → ESP32 控制协议
|
||||
|
||||
APP 与 ESP32 之间使用简化帧格式:
|
||||
|
||||
```
|
||||
| CMD(1B) | LEN(2B) | PAYLOAD(LEN) | CRC16(2B) |
|
||||
```
|
||||
|
||||
命令码直接对应上述 SPI 命令码(0xF0-0xF7),但数据字段为明文(加密由 ESP32 处理后再转发给 GD32)。
|
||||
|
||||
### 5.4 固件分包(APP → ESP32)
|
||||
|
||||
```
|
||||
| CMD(1B) | SEQ(2B) | SIZE(4B) | DATA(SIZE) | CRC32(4B) |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. BLE OTA 实现
|
||||
|
||||
### 6.1 GATT Profile 设计
|
||||
|
||||
| Handle | UUID | 属性 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 0x0001 | - | Primary Service | OTA Service |
|
||||
| 0x0010 | `xxxx-xxxx-xxxx-0001` | Control Point | 写命令/通知(控制帧通道) |
|
||||
| 0x0011 | `xxxx-xxxx-xxxx-0002` | Data In | 写无回应(大规模数据通道) |
|
||||
| 0x0012 | `xxxx-xxxx-xxxx-0003` | Data Out | 通知 GD32 响应 |
|
||||
| 0x0013 | `xxxx-xxxx-xxxx-0004` | Version | 只读,ESP32 版本号 |
|
||||
|
||||
### 6.2 BLE 传输流程
|
||||
|
||||
1. APP 写 `Control Point` (0xF0) → ESP32,发起握手
|
||||
2. ESP32 通过 SPI 转发给 GD32
|
||||
3. GD32 响应通过 `Data Out` 通知 APP
|
||||
4. APP 写 `Data In` 流式发送固件分包(48B/packet)
|
||||
5. 每包完成后,ESP32 回复 ACK 到 `Data Out`
|
||||
6. APP 写 `Control Point` (0xF7) → FINISH
|
||||
7. GD32 响应后,APP 可断开连接
|
||||
|
||||
### 6.3 MTU 设置
|
||||
|
||||
- 建议 MTU ≥ 512 bytes
|
||||
- BLE 每次 `Data In` 最大传输 512 bytes(减去 ATT 头)
|
||||
- 大文件分包多次写入
|
||||
|
||||
---
|
||||
|
||||
## 7. SPI Master 配置
|
||||
|
||||
```c
|
||||
// ESP32 SPI2 配置 (VSPI)
|
||||
spi_device_handle_t spi;
|
||||
|
||||
// 建议时钟: 40MHz
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.mosi_io_num = 8,
|
||||
.miso_io_num = 7,
|
||||
.sclk_io_num = 6,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
};
|
||||
|
||||
spi_device_interface_config_t dev_cfg = {
|
||||
.clock_speed_hz = 40 * 1000 * 1000, // 40MHz
|
||||
.mode = 0, // CPOL=0, CPHA=0
|
||||
.spics_io_num = 4, // GPIO4 as CS
|
||||
.queue_size = 1,
|
||||
.command_bits = 0,
|
||||
.address_bits = 0,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误处理
|
||||
|
||||
| 场景 | 处理 |
|
||||
|---|---|
|
||||
| GD32 无响应 ACK | 超时 3s → 重发同一包,最多 3 次 → 报错 |
|
||||
| CRC 校验失败 | GD32 要求重发 → ESP32 重发当前包 |
|
||||
| 重发次数超限 | ESP32 终止传输,通知 APP |
|
||||
| WiFi 断开 | 暂停传输,等待重连 |
|
||||
| BLE 断开 | 通知 APP 重新连接 |
|
||||
| 认证失败 | 终止流程,通知 APP |
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 9. ESP32 自检与保护机制
|
||||
|
||||
ESP32-C3FH4 利用芯片内置传感器实现运行安全监控,不外挂额外传感器。
|
||||
|
||||
### 9.1 检测项
|
||||
|
||||
| 检测项 | 来源 | 阈值范围 | 处理动作 |
|
||||
|---|---|---|---|
|
||||
| 芯片温度 | 内部温度传感器 | 正常: < 80°C / 警告: 80-100°C / 停机: > 100°C | 警告时降频,停机时中止 OTA |
|
||||
| Flash 空间 | 系统 API | 可用 > 16KB(存储分区元数据) | 空间不足时拒绝 OTA 请求 |
|
||||
| WiFi 信号强度 | WiFi RSSI | 正常: > -70 dBm / 弱: -70 ~ -85 dBm / 断开: < -85 dBm | 弱时降低 OTA 速度,断开时暂停 |
|
||||
| BLE 连接状态 | BLE stack | 连接中 / 断开 | 断开时通知 APP 重连 |
|
||||
|
||||
### 9.2 检测频率
|
||||
|
||||
| 检测项 | 频率 | 方式 |
|
||||
|---|---|---|
|
||||
| 芯片温度 | 每 1 秒 | FreeRTOS Timer |
|
||||
| Flash 空间 | OTA 开始前检查 | 一次性 |
|
||||
| WiFi RSSI | 每 10 秒 | 轮询 |
|
||||
| BLE 连接 | 事件驱动 | BLE GAP 回调 |
|
||||
|
||||
### 9.3 本地判断逻辑
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
SYS_NORMAL, // 全部正常
|
||||
SYS_WARNING, // 任一检测项触发警告阈值
|
||||
SYS_CRITICAL, // 任一检测项触发停机阈值
|
||||
SYS_SAFE_SHUTDOWN // 强制停机
|
||||
} sys_health_state_t;
|
||||
|
||||
// 状态机优先级:SAFE_SHUTDOWN > CRITICAL > WARNING > NORMAL
|
||||
// 高优先级状态覆盖低优先级状态
|
||||
```
|
||||
|
||||
### 9.4 异常处理联动
|
||||
|
||||
| ESP32 健康状态 | 对 OTA 的影响 |
|
||||
|---|---|
|
||||
| `SYS_NORMAL` | 正常进行 |
|
||||
| `SYS_WARNING` | 降低 SPI 速率,继续 OTA,记录警告日志 |
|
||||
| `SYS_CRITICAL` | 暂停 OTA 传输,通知 APP 当前状态,等待恢复 |
|
||||
| `SYS_SAFE_SHUTDOWN` | 中止 OTA,发送 ABORT 到 GD32,保护性复位 |
|
||||
|
||||
### 9.5 日志记录
|
||||
|
||||
异常事件写入 Flash 日志分区(`system_log`),包含:
|
||||
|
||||
```
|
||||
[TIMESTAMP] TEMP_WARNING: 85°C
|
||||
[TIMESTAMP] OTA_PAUSED: temp_critical
|
||||
[TIMESTAMP] OTA_RESUMED
|
||||
```
|
||||
|
||||
日志可由 APP 通过 WiFi/BLE 查询。
|
||||
|
||||
### 9.6 模块架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ System Monitor │
|
||||
│ (FreeRTOS Task, Priority High) │
|
||||
├──────────────────────────────────┤
|
||||
│ temp_monitor (1s timer) │
|
||||
│ wifi_monitor (10s timer) │
|
||||
│ ble_monitor (event-driven) │
|
||||
├──────────────────────────────────┤
|
||||
│ health_state_t state │
|
||||
│ → shared with OTA task via mutex│
|
||||
└──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────┐
|
||||
│ OTA Relay Task │
|
||||
│ - checks health_state before │
|
||||
│ each GD32 DATA_PACKET send │
|
||||
│ - pauses/aborts on WARNING/ │
|
||||
│ CRITICAL/SAFE_SHUTDOWN │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 10. ESP32 固件分区(参考)
|
||||
|
||||
```yaml
|
||||
# Partitions
|
||||
# Name, Type, SubType, Offset, Size
|
||||
otadata, data, ota, 0x9000, 0x2000 # OTA 数据区标记
|
||||
gd32_ota, data, spiffs, 0xB000, 0x40000 # GD32 固件存储区 (256KB)
|
||||
app0, app, ota_0, 0x4B000, 0x330000 # ESP32 自身 APP
|
||||
app1, app, ota_1, 0x3DC000, 0x330000
|
||||
spiffs, data, spiffs, 0x670000, 0x190000 # 文件系统(日志等)
|
||||
```
|
||||
|
||||
> **注意:** `gd32_ota` 分区大小根据实际 GD32 固件最大体积调整,建议预留 256KB 以上。
|
||||
@@ -1,96 +0,0 @@
|
||||
# 前端架构约定与回归清单(tools/ Web 界面)
|
||||
|
||||
> 2026-07 网页框架化重构产物。本文档固化 tools/ 目录的模块化约定,
|
||||
> 所有新增页面/模块必须遵循;同时提供改动后的回归验证清单。
|
||||
|
||||
## 1. 架构总览
|
||||
|
||||
- **托管方式**:Web 资源不嵌入固件,由 `tools/serve.bat`(`python -m http.server 8080`)本地托管;
|
||||
浏览器跨源调用 ESP32 REST API(路由表见 `main/server/wifi_ota.cpp` kHttpRoutes)。
|
||||
改动 web 文件无需重编译固件。
|
||||
- **零构建**:原生 ES Modules,无打包器/无 CDN 依赖;页面必须经 http 访问(ES module 有 file:// CORS 限制)。
|
||||
|
||||
### 页面薄入口范式
|
||||
|
||||
每个页面 = HTML(纯结构,无内联 script/style/事件)+ 单个 `type="module"` 入口:
|
||||
|
||||
| 页面 | 入口 | 说明 |
|
||||
|---|---|---|
|
||||
| index.html | js/app.js + js/ota/console-mode.js + js/ble_ota.js | OTA 控制台(WiFi/BLE 双通道,三入口历史原因保留) |
|
||||
| motor.html | js/motor/main.js | 电机控制台(独立 seed 主题,见 §4) |
|
||||
| can.html | js/can.js | CAN 工具 |
|
||||
| can_monitor.html | js/can_monitor.js | CAN 接收监控 |
|
||||
|
||||
### JS 分层
|
||||
|
||||
```
|
||||
js/
|
||||
├── common/ 跨页面公共层:base-url(IP存取) / toast / log / polling / connection / utils
|
||||
├── ota/ OTA 状态机与上传流程(高风险区,见 §5)
|
||||
├── ble/ BLE OTA 传输时序(高风险区,见 §5)
|
||||
├── can/ CAN 子系统(dbc 为基础层,导出共享 state/el)
|
||||
├── motor/ 电机控制台
|
||||
└── *.js 页面入口(薄装配层)
|
||||
```
|
||||
|
||||
## 2. 缓存戳约定(强制)
|
||||
|
||||
- **JS 间 import 一律不带 `?v=`**——同一模块带不同 query 会被浏览器实例化两次,
|
||||
导致模块级单例(state/toast 去抖等)分裂。
|
||||
- 版本戳只出现在 HTML 的 `<script>/<link>` 标签上;改动任何 JS/CSS 后,
|
||||
**必须更新对应页面入口标签的 `?v=`**(格式 `?v=YYYYMMDD<字母>`),否则浏览器可能混载新旧模块。
|
||||
|
||||
## 3. 公共层 API 契约
|
||||
|
||||
- `common/base-url.js`:设备 IP 唯一存取点(localStorage `dcu_esp_ip`,OTA/CAN/电机页共用)。
|
||||
页面特有回退(CAN 页 `el.toolIp`、OTA 页 `#espIp` 输入框)由各自入口在此基础上包装,
|
||||
不得绕过它直接读写 localStorage。
|
||||
- `common/toast.js`:单例 toast(`#toast/#toastIco/#toastMsg` + components.css 样式),
|
||||
内置 5s 去重。适用于 index/can/can_monitor;motor 页因独立主题使用自有堆叠式 toast(§4)。
|
||||
- `common/polling.js`:递归 setTimeout 轮询 + in-flight 守卫 + Page Visibility 补偿。
|
||||
依赖 app.js 先挂载 window.OtaSessionState 等(见文件头注释的 window.* 清单),加载顺序不可变。
|
||||
- index.html 按钮事件:统一 `data-action`/`data-arg` 属性 + app.js 末尾的事件委托,
|
||||
**禁止新增内联 onclick**;window.* 函数挂载保留(polling/ota 模块间仍经 window 总线)。
|
||||
|
||||
## 4. motor 页特殊性
|
||||
|
||||
- 独立视觉主题(seed tokens,`css/motor.css`),**不引入** base.css/components.css
|
||||
(两者与 motor.css 存在 `.btn/.toast/.toolbar` 等同名类,混用会破坏视觉)。
|
||||
- 自有堆叠式 toast(`#toastContainer`,多条并存),与 common/toast.js 的单例 DOM 契约不同,
|
||||
不做统一。
|
||||
- 三条轮询链路(/motor/state、/can/feedback、/system/status)必须互不阻塞
|
||||
(服务端约束,见 wifi_ota.cpp 注释)。
|
||||
|
||||
## 5. 红线(不可动区域)
|
||||
|
||||
- `js/ble/*`:Web Bluetooth 包发送时序、重试退避、FINISH 中止——大量真机实测调优。
|
||||
- `js/ota/{session,stages,progress,upload-esp32,upload-gd32}.js`:OTA 状态机与
|
||||
window.* 状态总线(activePhase/otaSessionState/currentOtaTarget 等)。
|
||||
- index.html 视觉:shimmer / logoPulse 动画(css/responsive.css,仅 transform/opacity)不得削弱。
|
||||
- 性能规范:高频更新走增量渲染 + write-before-compare + rAF 批量;CSS 动画仅 transform/opacity。
|
||||
|
||||
## 6. 回归清单
|
||||
|
||||
验证方式:`tools/serve.bat` 启动 → 浏览器打开 `http://localhost:8080/<页面>` → 真机 IP 连接。
|
||||
|
||||
### index.html(OTA 控制台)
|
||||
- [ ] WiFi/BLE 模式切换、GD32/ESP32 目标切换(顶部按钮)
|
||||
- [ ] 固件选择:点击/拖拽上传区、清除按钮、SHA-256 显示、FSAPI 热重载
|
||||
- [ ] ESP32 上传 → 阶段流程条驱动 → 重启 → 版本核对
|
||||
- [ ] GD32 OTA relay → 进度/阶段 → 完成 toast
|
||||
- [ ] 日志面板:过滤、自动滚动、清空;JSON 状态面板;任务排序
|
||||
- [ ] 断线/重连:连接胶囊状态、"测试"按钮、重启 ESP32 按钮
|
||||
- [ ] 视觉:shimmer/logoPulse 动画正常,无掉帧
|
||||
- [ ] BLE 模式:连接、一键刷写、中止、清除文件、拖拽选文件
|
||||
|
||||
### motor.html(电机控制台)
|
||||
- [ ] 三条轮询链路存活(节点列表 1Hz 刷新、状态栏 RX/TX、健康检查)
|
||||
- [ ] 控制指令发送、周期重发、模式切换、PID 配置
|
||||
- [ ] 版本查询、NodeID 设置、电机 OTA relay
|
||||
- [ ] 输入持久化(刷新后回填)、波形显示、console 无报错
|
||||
- [ ] `window.phaseDebug` 调试句柄可用
|
||||
|
||||
### can.html / can_monitor.html
|
||||
- [ ] DBC 导入/解析/合并、报文列表/信号编辑、发送(单发/批量/手动)
|
||||
- [ ] 发送历史、会话持久化、接收监控(独立页 + 抽屉)
|
||||
- [ ] toast 仅弹一次(无双实例重复弹)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user