docs(readme): 更新文档说明

- 添加项目使用指南
- 完善API接口说明
- 修正错误的配置示例
This commit is contained in:
小龙虾
2026-10-13 00:47:15 +08:00
parent 1b50ce1657
commit af1f68222e
7 changed files with 1674 additions and 1865 deletions
+93 -597
View File
@@ -1,630 +1,126 @@
# ESP32-C3 OTA Relay
# ESP32_COMM · DCU 通信与 OTA 中枢固件
**固件版本: v0.3.12** | 构建时间: 2026-06-17 | 基于 ESP-IDF v6.0
> **文档同步版本:v0.5.2(git 770ae14)· 2026-10-11**
> 本 README 是 [docs/project_introduction.html](docs/project_introduction.html)(完整白皮书,含 21 幅 SVG 原理图)的**浓缩版**,两者须同步更新(见文末「文档同步说明」)。
## 项目概述
ESP32-C3FH4 OTA 中继固件,作为 **GD32E508VET6** 的 OTA 升级桥梁。通过 **WiFi (HTTP)** 或 **BLE (GATT)** 从上位机(手机/PC)接收固件,存储在本地 SPI Flash 的 `gd32_ota` 分区,再通过 **SPI 总线** 经四阶段安全握手协议转发给 GD32。
ESP32-C3FH4 固件,作为 GD32E508 DCU 板的通信与 OTA 中枢:
```
[手机/PC 上位机] --WiFi/BLE--> [ESP32-C3FH4] --SPI--> [GD32E508VET6]
|
+-- gd32_ota 分区 (252KB)
+-- 健康自检 (温度 + WiFi RSSI)
+-- 本机 OTA (ESP32 自升级)
[上位机 PC/手机网页] --WiFi HTTP / BLE GATT--> [ESP32-C3FH4] --SPI Link v2 (13.33MHz, 5线)--> [GD32E508]
| |
gd32_ota 252KB 固件缓存 3×CANFD 收发器(电机总线)
app0/app1 AB 自升级 电机 OTA CAN 二次中继
```
## 硬件平台
- **OTA 中继**:GD32 固件(六阶段安全握手 + AES-128 逐包加密转发)、ESP32 自身 AB 分区升级、电机节点经 GD32 CAN 二次烧写
- **SPI Link v2 主站**:会话 / 序号 / 信用流控 / CRC32 / HELLO 重同步 / 透明重试
- **CAN 控制面**:单帧、批量、跨通道同步突发(0xC7)、只听模式(0xC8)、遥测回收(0xC4/C5/C6)
| 项目 | 规格 |
## 硬件与分区
| 项 | 值 |
|------|------|
| **芯片** | ESP32-C3FH4 (RISC-V, 单核, 160MHz) |
| **Flash** | 4MB |
| **SPI 引脚** | MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO4, DATA_READY=GPIO10(GD32 PB6 开漏输出,ESP32 仅输入) |
| **SPI 拓扑** | 5-wire SPI(4 线 SPI + 1 DATA_READY 通知线),SPI Link v2 协议(详见 `SPEC/spi_link_v2.md`) |
| **SPI 速率** | 4MHz, Mode 0 |
| **UART 调试** | TX=GPIO21, RX=GPIO20, 115200 baud |
| **编译工具链** | ESP-IDF v6.0, riscv32-esp-elf |
| 芯片 | ESP32-C3FH4(RISC-V 单核 160MHz)· 4MB Flash(DIO) |
| SPI | MOSI=5, MISO=7, SCLK=6, CS=4, DATA_READY=10;13.33MHz Mode 0(`CONFIG_DCU_SPI_CLOCK_HZ`) |
| 调试串口 | UART0 TX=21/RX=20,**3 Mbps**(115200 为 sdkconfig 控制台项);UDP 日志 192.168.31.3:6666 |
| 欠压 | LVL7(≈2.8V,防长断电半复位) |
### 分区表 (`config/partitions.csv`)
| 分区 | 类型 | 偏移 | 大小 | 用途 |
|------|------|------|------|------|
| nvs | data/nvs | 0x9000 | 24KB | NVS 存储 |
| otadata | data/ota | 0xF000 | 8KB | OTA 启动信息 |
| gd32_ota | data/spiffs | 0x11000 | 252KB | GD32 固件缓存 |
| app0 | app/ota_0 | 0x50000 | 1.25MB | ESP32 OTA 槽位 0 |
| app1 | app/ota_1 | 0x190000 | 1.25MB | ESP32 OTA 槽位 1 |
| spiffs | data/spiffs | 0x2D0000 | 1.06MB | 文件系统 |
## 功能模块
| 模块 | 文件 | 描述 |
|------|------|------|
| **core** | `core/main.cpp` | 程序入口,初始化各模块并创建 4 个 FreeRTOS 任务 |
| **health_monitor** | `monitor/health_monitor.{h,cpp}` | 温度 + WiFi RSSI 监控,OTA 前健康检查 |
| **debug_log** | `monitor/debug_log.{h,cpp}` | 三路日志输出:UART0 + UDP + 环形缓冲区 |
| **ota_coordinator** | `ota/ota_coordinator.{h,cpp}` | **全局 OTA 协调器**(单例):串行化 GD32/ESP32 OTA 操作,统一传输层入口,9 状态系统级状态机 |
| **ota_relay** | `relay/ota_relay.{h,cpp}` | GD32 OTA 中继**事件驱动状态机**(6 阶段 + ERROR):通过 `dispatchQueue` 接收调度信号,`processEvent()` 驱动状态转换 |
| **esp32_ota_core** | `ota/esp32_ota_core.{h,cpp}` | ESP32 本机 OTA 核心(begin/write/finalize/abort,WiFi/BLE 共用) |
| **wifi_ota** | `server/wifi_ota.{h,cpp}` | WiFi AP/STA + HTTP OTA 服务器 + CAN 帧转发 |
| **ble_ota** | `server/ble_ota.{h,cpp}` | BLE GATT 服务器(NimBLE 协议栈) |
| **spi_master** | `transport/spi_master.{h,cpp}` | SPI 主站驱动 + CANFD 3 通道 + OTA 帧协议 |
| **protocol** | `transport/protocol.{h,cpp}` | 协议层(nonce 搅伴生成、AES 会话密钥管理、运行时 SCRAMBLE_CONST) |
## 目录结构
```
ESP32_COMM/
├── CMakeLists.txt # 顶层 CMake 项目定义
├── sdkconfig.defaults # SDK 配置默认值 (WiFi RX/TX BA=6)
├── config/
│ └── partitions.csv # 4MB Flash 分区表
├── main/
│ ├── CMakeLists.txt # 注册 12 个源文件 + 依赖
│ ├── include/
│ │ ├── app_config.h # 统一配置 (constexpr: WiFi/BLE/OTA/阈值/密钥)
│ │ ├── version.h # 手动维护的版本信息 (v0.3.56)
│ │ └── ota_mutex.h # BLE/WiFi OTA 全局互斥
│ ├── core/main.cpp # app_main 入口,4 个 FreeRTOS 任务
│ ├── relay/ota_relay.{h,cpp} # GD32 OTA 中继事件驱动状态机
│ ├── ota/
│ │ ├── esp32_ota_core.{h,cpp} # ESP32 本机 OTA 核心
│ │ └── ota_coordinator.{h,cpp} # 全局 OTA 协调器
│ ├── transport/
│ │ ├── spi_master.{h,cpp} # SPI 主站 + CANFD 3 通道
│ │ └── protocol.{h,cpp} # 协议层 (CRC16 / AES / nonce)
│ ├── server/
│ │ ├── wifi_ota.{h,cpp} # WiFi OTA HTTP 服务器
│ │ └── ble_ota.{h,cpp} # BLE GATT OTA 服务器
│ └── monitor/
│ ├── health_monitor.{h,cpp} # 温度 + RSSI 健康监控
│ └── debug_log.{h,cpp} # UART0 + UDP + 环形缓冲区日志
├── components/
│ └── cjson/ # cJSON 组件
├── managed_components/
│ └── espressif__mdns/ # mDNS 组件
├── tools/
│ ├── index.html # WiFi OTA WebUI 控制台
│ ├── ble_ota.html # BLE OTA WebUI 控制台 (Web Bluetooth)
│ ├── app.js # WebUI JavaScript
│ ├── style.css # WebUI 样式
│ ├── serve.bat # Python HTTP 服务器启动脚本 (端口 8080)
│ ├── serial_monitor.py # 串口日志采集工具 (COM15)
│ └── ota_upload_test.py # OTA 上传测试脚本
├── docs/
│ ├── esp32_ota_relay_design.md # 架构设计文档
│ ├── DEVELOPMENT_GUIDE.md # 开发者指南 (T0-T8)
│ └── COMMUNICATION_AUDIT_2026-05-05.md # 通信审计
├── .vscode/ # VS Code 调试/编译配置 (COM15, clangd)
└── .devcontainer/ # Docker 开发容器 (ESP-IDF 6.0)
```
## FreeRTOS 任务
4 个主任务全部固定运行在 ESP32-C3 单核(核 0),通过 FreeRTOS 队列通信:
| 任务名 | 函数 | 优先级 | 栈大小 | 职责 |
|--------|------|--------|--------|------|
| `health_mon` | `HealthMonitor::taskEntry` | configMAX_PRIORITIES - 4 | 4096 | 温度监控 (1s 定时器) + 堆巡检 (5s) + 健康状态管理 |
| `wifi_ota` | `WifiOta::taskEntry` | configMAX_PRIORITIES - 3 | 8192 | WiFi (AP/STA) + HTTP 服务器 (端口 80) + 状态轮询 |
| `ble_ota` | `BleOta::taskEntry` | configMAX_PRIORITIES - 3 | 8192 | BLE GATT 服务器初始化 + NimBLE Host 任务 + 心跳保活 |
| `ota_relay` | `OtaRelay::taskEntry` | configMAX_PRIORITIES - 2 | 12288 | 等待 dispatchQueue 调度信号 → 事件驱动状态机(6 阶段)→ SPI 转发 → resultQueue 回传结果 |
额外后台任务:
| 任务名 | 优先级 | 栈大小 | 职责 |
|--------|--------|--------|------|
| `wifi_reconnect` | 5 | 4096 | STA 断线重连(退避算法,独立任务避免死锁)|
| `ble_ota_finish` | 5 | 4096 | BLE ESP32 OTA 异步收尾(SHA 校验 + end + reboot)|
| `nimble_host` | NimBLE 内部 | NimBLE 内部 | BLE 协议栈事件循环 |
### 跨任务通信
- **OTA 协调**: **OtaCoordinator** 管理系统级 OTA 状态(9 状态),提供 `beginGd32Storage/completeGd32Storage/triggerGd32Relay/beginEsp32Ota` 等统一入口,内部 mutex 保护状态读写
- **OTA 调度**: WiFi/BLE 任务通过 `OtaRelay::dispatchQueue` (FreeRTOS 队列, 深度 4) 发送调度信号给 `ota_relay` 任务;结果通过 `resultQueue` 回传;OtaCoordinator 也向此队列投递中继触发命令
- **OTA 互斥**: `g_esp32OtaMutex` (全局 Mutex) + `g_esp32OtaOccupied` 标志,防止 BLE 和 WiFi 同时操作 ESP32 OTA 分区
- **中继状态**: `OtaRelay::s_stateMutex` 保护 relayState/relaySeq/relayTotalPackets 等跨任务状态(`StateLockGuard` RAII 封装)
- **WiFi 状态**: `WifiOta::m_stateMutex` + `std::atomic` 保护 WiFi 连接状态和重连计数器
- **健康状态**: `HealthMonitor::m_mutex` 保护 healthState/temperature/RSSI
- **UDP 日志**: `DebugLog::m_logRingMutex` 保护 HTTP `/logs` 环形缓冲区
## OTA 核心流程
### 一、GD32 OTA 中继(事件驱动状态机,6 阶段 + ERROR)
上位机上传 GD32 固件到 ESP32 本地 `gd32_ota` 分区(通过 **OtaCoordinator** 协调上传状态)→ 收到触发命令后由 **OtaRelay** 事件驱动状态机执行转发:
```
状态机总览:
IDLE ──(EVT_START)──→ HANDSHAKING ──→ SIZE_NEG ──→ TRANSFERRING
↑ │ │ │
│ (error) (error) (error)
│ ↓ ↓ ↓
└──── DONE ←── FINISHING ←────────────────────────── ERROR
│ ↑ │
└──── (EVT_START) ───┘ (EVT_START)───┘
```
| 阶段 | 状态 | 输入事件 | 输出/转换 |
|------|------|----------|-----------|
| **① 安全握手** | `HANDSHAKING` | EVT_START / EVT_RESP_OK | `doHandshakeReq()` → `doChallengeAndAuth()` → 建立 `K_session` AES 加密通道 |
| **② 大小协商** | `SIZE_NEG` | EVT_START | `doSendFirmwareSize()` → 加载 NVS 元数据中的 `total_size` |
| **③ 数据包转发** | `TRANSFERRING` | EVT_START | `doRelayPackets()` → 逐包 AES 加密转发 + 结构化 ACK 校验 + 健康检查 |
| **④-⑤ 完成与验证** | `FINISHING` | EVT_START | `doFinishAndVerify()` → 发送 FINISH → PING 轮询 APP 存活(最多 30 次 × 500ms) |
| **完成** | `DONE` | EVT_START → 重入 IDLE | 状态机复位 |
| **错误** | `ERROR` | 任意错误超时/健康危险 | `reset()` → IDLE |
#### 协调器系统级状态(OtaCoordinator)
```
状态流转:
IDLE ──→ GD32_STORING ──→ GD32_STORED ──→ GD32_RELAYING ──→ GD32_COMPLETE
│ │
├──→ ESP32_RECEIVING ──→ ESP32_FINALIZING ──→ ESP32_REBOOTING
└──→ ERROR (任意阶段)
```
| 系统状态 | 含义 |
|----------|------|
| `OTA_SYS_IDLE` | 空闲,可接受新请求 |
| `OTA_SYS_GD32_STORING` | 传输层正在存储 GD32 固件到 Flash |
| `OTA_SYS_GD32_STORED` | GD32 固件已完整存储,等待触发中继 |
| `OTA_SYS_GD32_RELAYING` | SPI 中继转发进行中 |
| `OTA_SYS_GD32_COMPLETE` | GD32 OTA 完成 |
| `OTA_SYS_ESP32_RECEIVING` | 接收 ESP32 自身固件 |
| `OTA_SYS_ESP32_FINALIZING` | SHA 校验 + end + set_boot |
| `OTA_SYS_ESP32_REBOOTING` | 即将重启 |
| `OTA_SYS_ERROR` | 错误状态 |
#### 安全握手协议
1. **握手请求**: ESP32 发送 `0x00 0xAC`(最多重试 10 次,200ms 超时)
2. **Challenge**: GD32 回复 36B 数据包(12B UID + 8B nonce 片段 + 16B 加密会话密钥)
3. **验证**: ESP32 验证 nonce 匹配,用 MasterKey 派生 `K_session = AES(MasterKey, nonce_server)` 并比对
4. **确认**: ESP32 发送 `nonce_server ^ 0x55` 经 `K_session` AES-ECB 加密
5. **完成**: GD32 回复 `0xBA 0xC0`,建立 AES 加密通道
> **GD32 UID **: 运行时从握手 Challenge 包解包获得,不再使用编译期常量 `GD32_CHIP_UID`。
#### 数据包转发
- 每包 48 字节固件载荷,使用 `K_session` AES-ECB 加密
- 包结构: `session(4B) + seq(4B) + encrypted48(48B) + if_end(4B) + crc32(4B)` = 64B
- 每 32 包执行一次 OTA 健康检查(温度/堆)
- 支持结构化 ACK 含 seq 校验 + 错误码(`0x04`=CRC 错误),残留 ACK 自动过滤
- 单包最多 10 次重试,全局超时动态计算 5~10 分钟
- **Flash 元数据**: 通过 NVS 记录固件 `total_size`/`stored_size`/`last_seq`/`checksum`,用于完整性校验和中继恢复判断
### 二、ESP32 本机 OTA(WiFi + BLE 共用)
核心逻辑封装在 `Esp32OtaCore` 单例中,WiFi 和 BLE 通道共用。
传输层通过 **OtaCoordinator** 统一入口(`beginEsp32Ota()`)发起操作,由协调器管理系统状态并委托 `Esp32OtaCore` 执行底层写入:
| 步骤 | 方法 | 说明 |
|------|------|------|
| 1 | `begin(total_size)` | 选择非当前 OTA 分区,获取全局互斥,`esp_ota_begin` |
| 2 | `write(data, len)` | 循环调用 `esp_ota_write`(WiFi 通过 HTTP 流,BLE 通过 GATT 块) |
| 3 | `finalize(sha_hex, reboot)` | 从分区读回计算 SHA-256 → 比对前端 SHA → `esp_ota_end` → `esp_ota_set_boot_partition` → 可选重启 |
| - | `abort()` | 中止并清理(释放 mutex + 复位状态) |
**SHA-256 校验**: 固件写入完成后从 OTA 分区读回计算,不做增量 SHA。前端与设备端 SHA 比对一致才执行 finalize(`Esp32OtaCore::finalize()` → `esp_ota_end` → `esp_ota_set_boot_partition`),否则返回 `ESP_ERR_INVALID_VERSION`。过程中系统状态由 `OtaCoordinator` 跟踪(`ESP32_RECEIVING → ESP32_FINALIZING → ESP32_REBOOTING`)。
**回滚保护**: `esp_ota_mark_app_valid_cancel_rollback()` 在启动时标记当前固件有效。启用 `CONFIG_APP_ROLLBACK_ENABLE` 后,新固件崩溃自动回退。
## 通信协议
### SPI Link v2 帧格式(底层传输,唯一现行)
真相源:`spi_link/spi_link_v2.h` · ESP32 `SpiMaster::otaBuildFrame` / `otaFindFrame`:
```
| SOF0 0xA5 | SOF1 0x5A | VER 0x02 | FLAGS | SESSION 4B LE | TX_SEQ | ACK_SEQ | CREDIT | TYPE | LEN 2B LE | RSV 2B | PAYLOAD | CRC32 4B LE |
```
- 开销 `V2_OVERHEAD=20`(Header 16 + CRC32 4,MPEG-2 无反射)
- APP 缓冲 256 → 最大 payload **236**;BOOT 缓冲 128 → 最大 payload **108**
- ESP32 读预算:`spiRespReadBudget()` 按 HELLO role → APP=256 / BOOT=128
- **无 v1 CRC8 路径**
| TYPE | 名称 | 用途 |
|------|------|------|
| 0x00 | LINK_HELLO | 建链 / HELLO_ACK |
| 0x01 | OTA_CMD_REQ | OTA 请求帧 |
| 0x02 | OTA_CMD_DATA | OTA 数据帧 |
| 0x03 | OTA_CMD_CANCEL | 取消 OTA |
| 0xB0 | ENTER_BOOT | 通知 GD32 APP 进入 Bootloader(应答 TYPE 亦 0xB0,payload 0xB5) |
| 0x80 | RSP_ACK | 响应:成功 |
| 0x81 | RSP_WAIT | 响应:GD32 忙,请等待 |
| 0x82 | RSP_ERROR | 响应:错误 |
| 0xC0~C3 | CAN 业务 | 发送 / 反馈 / 模式 / ID 快照 |
#### SPI Link v2 PC 自测(兼容性回归门禁)
`spi_link_v2.cpp` 内置 `SPI_LINK_V2_SELFTEST` PC 端自测,以 `golden_vectors.h`
中冻结的帧字节为交叉校验基准,覆盖 HELLO / OTA / CAN / PING / ENTER_BOOT /
响应类型、最大 payload、CRC 翻转、错误版本、前导垃圾字节偏移解析等场景。
向量字节由 `v2_build_frame` 现算后冻结,与协议实现互为交叉校验——任何一方
漂移都会被对方测试捕获。
独立 host CMake target(与 ESP-IDF 构建隔离,不影响固件构建):
```bash
cmake -S spi_link/test -B build/host_test
cmake --build build/host_test
ctest --test-dir build/host_test --output-on-failure # 期望 Passed: 10 Failed: 0
```
CI:`.github/workflows/spi-link-selftest.yml` 在触及 SPI Link 相关文件时自动
运行自测。`spi_link/` 是三工程共享的兄弟协议目录(与 `ESP32_COMM` 同级,本仓
通过 `../../../spi_link/` 相对路径引用),CI 检出需确保该兄弟目录就位。
### CANFD 3 通道 SPI 协议
ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器):
```
| 通道编号(1B) | 扩展帧ID(4B BE) | 数据(1~64B) |
```
| 通道 | 用途 |
|------|------|
| CH0 | 主通信通道 |
| CH1 | 备用通道 |
| CH2 | 备用通道 |
### GD32 命令 ID 定义
| 域 | 命令 | ID |
|------|------|------|
| 系统 | PING / RESET / GET_VERSION / GET_STATUS / SET_STATUS | 0x0001~0x0005 |
| Flash | ERASE / WRITE / READ / VERIFY | 0x1001~0x1004 |
| OTA | START / DATA / END / ABORT | 0x2001~0x2004 |
| 传感器 | READ / CONFIG | 0x3001~0x3002 |
| Bootloader | ENTER / JUMP_APP | 0xF001~0xF002 |
## WiFi 工作模式
支持 AP/STA 双模式,启动时优先 STA,超时自动回退 AP。
### AP 模式(设备当热点)
| 参数 | 值 |
|------|------|
| SSID | `ESP32_OTA_RELAY` |
| 密码 | `12345678` |
| IP | 192.168.31.1 |
| 信道 | 6 |
| 最大客户端 | 4 |
### STA 模式(连接路由器)
| 参数 | 值 |
|------|------|
| SSID | `004舰桥` |
| 连接超时 | 30 秒 |
| 最大重试 | 5 次(超限后自动切换 AP 模式) |
| IP 分配 | DHCP(可切换静态 IP 192.168.31.32)|
| 国家码 | CN(允许信道 1-13)|
**冷启动**: POWERON/BROWNOUT 时做短延时后初始化;清 WiFi 连接缓存;欠压复位后清 PHY 重校准;BOD 档位 LVL5。
### mDNS 发现
设备在局域网内通过 `esp32-ota-relay.local` 可发现(需 `managed_components/espressif__mdns` 组件)。
## BLE GATT 服务
基于 **NimBLE** 协议栈,设备名 `ESP32-OTA-RELAY`,ATT MTU 512 字节,支持 GD32 和 ESP32 两种 OTA 目标。
| UUID | 特征 | 属性 | 用途 |
| 分区 | 偏移 | 大小 | 用途 |
|------|------|------|------|
| 0xFFFF | Service | - | OTA GATT 服务 |
| 0x0001 | CTRL | Write | 控制命令:`0xF7`=触发GD32中继, `0xF8+32B SHA`=ESP32 OTA, `0xF9+32B SHA`=仅SHA校验(不入库/不重启), `0xFA`=重启ESP32 |
| 0x0002 | DATA_IN | Write | 固件数据块:`cmd(1)+seq(2)+total_size(4)+payload` |
| 0x0003 | DATA_OUT | Notify | 推送:ACK(窗口确认, 0xA1+8B) / FINISH(阶段事件, 0xB1+6B) / SHA256(0xC1+33B) |
| 0x0004 | VERSION | Read | 固件版本字符串 |
| nvs | 0x9000 | 24KB | 配置 + OTA 元数据(`ota_meta`:total/stored/last_seq/checksum) |
| otadata | 0xF000 | 8KB | AB 启动标志 |
| gd32_ota | 0x11000 | 252KB | GD32/电机固件缓存(互斥共用) |
| app0 / app1 | 0x50000 / 0x190000 | 各 1.25MB | ESP32 AB 分区(回滚保护) |
| spiffs | 0x2D0000 | 1.06MB | 用户数据 |
### BLE 数据传输
## 架构与任务
- **ACK 窗口**: 每 32 包等待一次 DATA_OUT ACK(带累计字节数验证)
- **窗口重传**: ACK 超时或字节数不足时重发整个窗口(最多 3 次)
- **乱序重排**: Reorder buffer(32 槽)吸收 BLE 窗口内乱序到达的包,按 seq 排序后顺序写入 `Esp32OtaCore`
- **ESP32 收尾**: BLE 传输完成后由前端发 `0xF8 + 32B 文件 SHA-256`,设备端读回 OTA 分区做 SHA 比对,通过后完成 `esp_ota_end`、设置启动分区并重启;`0xF9` 命令仅做 SHA 比对不入库(预检),FINISH 仅上报完成/重启/错误
- **GD32 触发**: 传输完成后自动发 `0xF7` 触发 SPI 中继
- **合并包**: NimBLE 可能将多个 writeWithoutResponse 合并到同一 mbuf,接收端支持一次解析多个数据包
## HTTP API (WiFi WebUI)
| 方法 | 端点 | 功能 | 请求体 | 响应 |
|------|------|------|--------|------|
| POST | `/ota/upload` | 上传固件 | JSON 头 + binary 数据 | `{"status":"ok", "target":"esp32"}` |
| POST | `/ota/update` | 完成 ESP32 OTA | `{"sha256":"...","verify_only":true/false}` | `{"status":"ok","sha256":"..."}` |
| POST | `/ota/relay` | 触发 GD32 中继 | - | `{"status":"ok"}` |
| GET | `/system/status` | 系统状态 JSON | - | 含版本/WiFi/OTA/温度/堆/GD32 版本及**OtaCoordinator 系统级状态**等 |
| GET | `/logs` | 拉取调试日志 | - | JSON 数组字符串 |
| POST | `/can/send_batch` | CAN 发送(1~N 帧,单帧即 length=1) | `{"ch":0,"frames":[{"id":123,"dlc":8,"data":"0102..."}]}` | `{"status":"ok","results":[...]}` |
| POST | `/spi/send` | SPI 直接发送 | `{"type":"01","data":"00AC","timeout_ms":1000}` | `{"status":"ok","resp_type":".."}` |
| POST | `/restart` | 重启 ESP32 | - | 500ms 后 `esp_restart()` |
### POST /ota/upload 协议
通过 JSON 头中的 `type` 字段自动区分目标,传输层通过 **OtaCoordinator** 统一管理系统状态:
```json
// ESP32 OTA
{"type": "esp32", "total_size": 524288}
<binary firmware data...>
// GD32 OTA
{"type": "gd32", "total_size": 131072}
<binary firmware data...>
```
接入层 server/: wifi_ota(STA/AP+路由表) · http_handlers(22 路由) · ble_ota*(GATT/窗口ACK/reorder/池)
协调层 ota/: OtaCoordinator(9 态单例, 唯一权威入口, 忙态看门狗 150s, epoch 保护) · Esp32OtaCore
执行层 relay/ storage/: OtaRelay(7 态事件状态机) · Gd32FlashStore(擦除水位/NVS meta/SHA/包身份校验)
monitor/: MotorOtaRelay(0xD0/D1/D2 喂数)
底层 transport/: SpiMaster(Link v2+worker 串行) · protocol(AES/nonce) · canfd_bridge(0xC0~C8)
横切 monitor/: health_monitor · debug_log(3 路) · can_feedback_task · cpu_stats · task_stack 等
```
- **ESP32 模式**: `OtaCoordinator::beginEsp32Ota()` → `Esp32OtaCore::write()` 流式写入 OTA 分区 → 前端调 `/ota/update` 收尾
- **GD32 模式**: `OtaCoordinator::beginGd32Storage()` → 写入 `gd32_ota` 分区 → `OtaCoordinator::completeGd32Storage()` → 计算 SHA-256 → 自动触发 SPI 中继
| 任务 | 优先级 | 栈 | 职责 |
|------|-------|------|------|
| ota_relay | 23 | 2560 | GD32 转发状态机 |
| wifi_ota / ble_ota | 22 | 3072/2560 | WiFi/HTTP 与 NimBLE |
| health_mon | 21 | 3072 | 温度 1s + 5s 巡检 |
| ble_data_worker | 12 | 2560 | BLE 数据排空 |
| spi_worker | 8 | 5120 | **唯一持 SPI**,串行 Job 池(7) |
| motor_feed | 7 | 4096 | 电机 OTA 0xD1 喂数 |
| wifi_reconnect / can_fb | 5/4 | 2560/4096 | 重连退避 / 0xC5(250ms)·0xC4(1s) 周期查询 |
| cpu_stats / uart_rx | 1 | 3072 | 1Hz 统计 / 串口行→/logs |
### curl 示例
## 核心机制速记(细节见白皮书对应章节)
```bash
# 查询系统状态
curl http://192.168.31.1/system/status | jq
- **SPI Link v2**(§5):帧 = 16B 头(SOF A5 5A/VER 02/FLAGS/SESSION 4LE/TX_SEQ/ACK_SEQ/CREDIT/TYPE/LEN)+ 载荷 ≤236B + CRC32-MPEG2。延迟一拍全双工:transact 两步法(发送丢弃陈旧 RX → poll 按 DATA_READY 边沿读真实响应)。credit 8/8 背压,credit=0 死锁有 PB6 强制放行 + `resetCredit()` 双破除。透明重试 `kTxRetryMax=3`(同 TX_SEQ,GD32 DUP-SEQ 去重兜底)。CS 事务间隔 ≥1ms + 快速类型(0xA1/0xA3/0xC0~C5/C7/D1/F7)禁盲读防撕裂;3 连败熔断 2s;session=0 自动 HELLO 重同步(5s 冷却)。
- **安全握手**(§6):GD32 OTA 数据面——0x00 0xAC → 36B challenge(UID 12B + nonce 回显 8B + AES(MASTER_KEY,K_session) 16B)→ `K_session = AES-128-ECB(MASTER_KEY, nonce_server)` → AES(K_session, nonce^0x55) → 0xBA 0xC0。nonce 由 scrambleGenerate(tick/栈址/0x12345678 ⊗ 每开机 RNG 扰码常数)生成。
- **GD32 OTA 中继**(§7):RELAY_IDLE→HANDSHAKING(20×1.6s)→SIZE_NEG(poll 500ms×120,GD32 擦 Flash)→TRANSFERRING(64B 包=4B session+2B seq+48B AES+4B if_end+4B CRC32(key 混入);预算 10s+25ms/包 ≤120s;每 32 包健康检查;单包 5 次 DATA×50ms,末包 ACK=0xF0F0F0F0)→FINISHING(0x0A 0xAC×10 重试 + PING 实证 APP 存活)。失败必发 SPI CANCEL 清 GD32 半会话。**包级防呆**:Cortex-M 向量表判 kApp 才放行(BOOT 包禁入 APP 区)。
- **ESP32 本机 OTA**(§7.4):`OTA_WITH_SEQUENTIAL_WRITES` 边写边擦;finalize 分区读回算 SHA-256 与前端比对,不符回 `ESP_ERR_INVALID_VERSION`+设备 SHA;`mark_app_valid_cancel_rollback()` 回滚保护;期间 `setSpiPaused(true)`。
- **BLE OTA**(§8):GATT 0xFFFF/0x0001~0x0004,MTU 512,载荷 480B;**窗口 ACK 64**(累计字节校验)+ reorder 64 槽 + 负 ACK 续传(64 包/200ms 节流);DATA_IN 池 8×512B(曾 32KB 致收尾 OOM);CTRL 0xF7 触发 GD32 中继 / 0xF8 ESP32 收尾 / 0xFA 重启 / 0xFB 中止(0xF9 预留未实现);BLE 与 WiFi OTA 软挂起共存(不 deinit 协议栈)。
- **CANFD 桥接**(§10):0xC0 单帧(最短 5B)/0xC1 统计+ID 快照分页(28B 头+条目 8+dlc,bus_state bit0~5,bit5=Silent)/0xC2 收模式(全收/过滤/CAN0→1/2 桥接)/0xC7 跨通道突发(ISR 内装载全部邮箱后回**一个** ACK,组内偏差 <1ms;单次 ≤16 帧,HTTP ≤24 帧,id=(node<<16)|suffix)/0xC8 收发器 Silent(TCAN1057AV S 脚 PA8/PB14/PB9,接收照常,不持久化,发送回 E6)。错误码 E0~E7:E2/E3(BUS_OFF/TEC>96) fatal 中止批量,E4 邮箱满,E5 电机 OTA 残留(自动 CANCEL→resetCredit→重试 START 自愈),E6 Silent,E7 被电机 OTA 独占。
- **电机 OTA**(§11):/motor/ota 上传(整区强制擦除)→ 0xD0 START(fire-and-forget)→ feedTask 0xD1 {绝对 offset+≤232B}(重传幂等;120s 整体超时;8 败/150 WAIT → QUERY)→ 0xD2 进度。
- **监控**(§12):健康 4 级(80°C/100°C;RSSI -80/-85/-88;堆 10KB)门控 OTA;日志三路(UART 3M 非阻塞 / UDP / 环 64→16 槽 →/logs)+ UART0 RX 泵;遥测 0xC5 电机状态 27 槽×14 页、0xC4 ADC(VCC=vin×11)、0xC6 电机版本、0xA3 初始化状态 13 模块故障码;CPU/任务栈/堆低水位快照/启动自检 16 位位图全进 /system/status。
# 发送 CAN 帧(单帧也走 send_batch)
curl -X POST http://192.168.31.1/can/send_batch \
-H "Content-Type: application/json" \
-d '{"ch":0,"frames":[{"id":0x18DA00F1,"dlc":8,"data":"0102030405060708"}]}'
## HTTP API(22 条路由 / 21 路径,无认证,CORS 全开)
# SPI 直接发送
curl -X POST http://192.168.31.1/spi/send \
-H "Content-Type: application/json" \
-d '{"type":"01","data":"00AC","timeout_ms":1000}'
| 方法 | 路径 | 功能 |
|------|------|------|
| POST | /ota/upload | 固件上传(JSON 头 `{"type":"esp32"\|"gd32","total_size":N}` + 二进制流式) |
| POST | /ota/update | ESP32 OTA 收尾 `{sha256, verify_only}`(SHA 比对→set_boot→重启) |
| POST | /ota/relay | 触发 GD32 中继(前端 SHA + kApp 身份校验) |
| POST | /restart | 重启 |
| GET | /logs | 日志环 chunked 流式 |
| GET | /system/status | 全量状态(?lite=1 / ?layout=1;含 spi_link 统计、启动自检、GD32 版本/ADC) |
| GET | /system/probe · /system/heap_low_log | GD32 存活探测 / 堆低水位快照 |
| POST | /can/send_batch · /can/send_burst | 批量发送(≤128 帧)/ 跨通道突发(≤24 帧) |
| GET·POST | /can/feedback · /can/mode · /can/silent | 快照查询 / 收模式 / Silent 只听 |
| POST | /spi/send | 原始 SPI 帧透传 |
| GET | /motor/state(?bin=1) · /motor/version | 电机状态二进制协议 / 版本 |
| POST·GET | /motor/ota · /motor/ota/relay · /motor/ota/status · /motor/ota/cancel | 电机 OTA 全流程 |
| GET | /spi/speedtest | SPI 性能测试(临时) |
# 获取日志
curl http://192.168.31.1/logs
```
WiFi:STA 优先(SSID「004舰桥」,FAST_SCAN,CN 1-13),冷启动退避 2s×retry≤3 / 否则 1s,5 次耗尽或 30s 超时降级 AP(`ESP32_OTA_RELAY`/`12345678`,192.168.31.1,信道 6,≤4 台)。**无 mDNS**,访问靠固定 IP。
## WebUI 工具
## BLE GATT 速查
### WiFi OTA 控制台 (`tools/index.html`)
| UUID | 特征 | 说明 |
|------|------|------|
| 0xFFFF | Service | 设备名 ESP32-OTA-RELAY |
| 0x0001 | CTRL (Write) | 0xF7/0xF8/0xFA/0xFB(0xF9 预留) |
| 0x0002 | DATA_IN (WriteNoRsp) | cmd 1B(0x00 GD32/0x01 ESP32)+seq u16+total u32+payload ≤480B |
| 0x0003 | DATA_OUT (Notify) | 0xA1 ACK 8B / 0xB1 FINISH 6B / 0xC1 SHA 35B |
| 0x0004 | VERSION (Read) | 版本+构建时间 |
- ESP32 固件上传与 OTA 升级(含 SHA-256 前端校验)
- GD32 固件上传(支持 .bin/.hex)与 SPI 中继触发
- 实时 OTA 流程阶段显示(6 阶段进度追踪,含 **OtaCoordinator 系统级状态**)
- CAN 帧手动发送面板
- SPI 直接发送面板
- 系统状态监控(温度、RSSI、空闲堆、最小堆、GD32 APP/BOOT 版本、OTA 缓存、**协调器系统状态**)
- 调试日志面板(HTTP `/logs` 轮询,按级别过滤)
- 一键重启 ESP32
### BLE OTA 控制台 (`tools/ble_ota.html`)
- **Web Bluetooth API** 连接(需 Chrome/Edge/Android Chromium)
- GD32 与 ESP32 双目标切换
- 一键刷写(自动重连)
- 窗口 ACK 重传机制(最多 3 次重试)
- SHA-256 前端+设备端比对校验(校验面板显示)
- FINISH 阶段事件通知
- 设备重启 + 自动重连(退避 1.6x 指数,最多 12 次)
## 健康监控
### 健康状态等级
| 等级 | 名称 | 触发条件 | OTA 影响 |
|------|------|----------|----------|
| 0 | SYS_NORMAL | 正常 | 允许 |
| 1 | SYS_WARNING | 温度 ≥ 80°C 或 RSSI < -80 dBm | 允许(带警告)|
| 2 | SYS_CRITICAL | RSSI < -85 dBm | ESP32 OTA 禁止,SPI 中继允许 |
| 3 | SYS_SAFE_SHUTDOWN | 温度 ≥ 100°C | 所有 OTA 禁止 |
### 监控指标
- **温度**: 1 秒定时器读取内部温度传感器(20°C~80°C 量程)
- **WiFi RSSI**: WiFi 任务每 10 秒检查一次
- **空闲堆**: 每 5 秒巡检,低于 10KB 触发警告
### OTA 前健康检查
- `checkBeforeOta()`: 完整检查(温度 + RSSI + WiFi 连接状态)
- `checkBeforeOtaRelay()`: 仅检查温度(中继阶段不依赖 WiFi)
## 诊断日志
三条并行输出路径:
1. **UART0**: GPIO21(TX), GPIO20(RX), 115200 baud — 本地串口调试
2. **UDP 单播**: `192.168.31.3:6666` — 独立调试接收端
3. **环形缓冲区**: 64 槽 × 256 字节,互斥保护 — 供 HTTP `GET /logs` 轮询拉取
日志级别: `E`(错误) / `W`(警告) / `I`(信息) / `D`(调试),运行时可调。
UDP socket 延迟创建(首次 `send()` 时才分配),WiFi 未连接时 `sendto()` 静默跳过。
**当前 WebUI 日志面板通过 HTTP `/logs` 轮询获取日志,不直接监听 UDP 6666。**
## 配置参数 (`main/include/app_config.h`)
| 分类 | 参数 | 默认值 | 说明 |
|------|------|--------|------|
| WiFi | `CONFIG_WIFI_MODE` | 1 (STA) | 0=AP, 1=STA |
| WiFi | `WIFI_STA_SSID` | "004舰桥" | 路由器名称 |
| WiFi | `WIFI_AP_SSID` | "ESP32_OTA_RELAY" | 热点名称 |
| WiFi | `WIFI_RSSI_WARNING` | -80 dBm | RSSI 警告阈值 |
| WiFi | `WIFI_RSSI_CRITICAL` | -85 dBm | RSSI 危险阈值 |
| OTA | `OTA_BLOCK_SIZE` | 8192 | 读写块大小 |
| OTA | `OTA_MAX_FIRMWARE_SIZE` | 216KB | GD32 固件上限 |
| OTA | `GD32_OTA_PACKET_SIZE` | 48 | 每包载荷字节 |
| OTA | `OTA_PACKET_HEALTH_CHECK_INTERVAL` | 32 | 每 N 包健康检查 |
| OTA | 包间节奏 | CREDIT 背压 | `SpiMaster`:`remoteCredit==0` 时 wait/拒发,无固定包间 delay |
| OTA | `OTA_WIFI_RSSI_CRITICAL` | -88 dBm | OTA 期间 RSSI 危险阈值 |
| OTA | `OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS` | 15000 | OTA 前 RSSI 采样最大时效 |
| 阈值 | `TEMP_WARNING` | 80°C | 温度警告阈值 |
| 阈值 | `TEMP_CRITICAL` | 100°C | 温度危险阈值 |
| 阈值 | `HEAP_WARNING_MIN` | 10240 B | 堆最小值警告 |
| 调试 | `DEBUG_LOG_IP` | "192.168.31.3" | UDP 日志目标 IP |
| 调试 | `DEBUG_LOG_PORT` | 6666 | UDP 日志端口 |
| BLE | `BLE_DEVICE_NAME` | "ESP32-OTA-RELAY" | BLE 广播名称 |
| BLE | `BLE_MTU_SIZE` | 512 | ATT MTU |
| BLE | `BLE_OTA_ACK_WINDOW` | 32 | ACK 窗口大小 |
| BLE | `BLE_OTA_CHUNK_PAYLOAD` | 480 | BLE 单包载荷 |
| BLE | `BLE_OTA_REORDER_WINDOW` | 32 | 乱序重排缓冲区大小 |
| BLE | `BLE_OTA_CTRL_VERIFY_SHA256` | 0xF9 | SHA-256 预检命令字 |
## 安全配置
| 参数 | 位置 | 说明 | 状态 |
|------|------|------|------|
| `MASTER_KEY[16]` | app_config.h | AES-128 主密钥 | ⚠️ 占位值,生产环境需更换 |
| `SCRAMBLE_CONST[16]` | Protocol 类运行时生成 | Nonce 搅伴常量 | 每次启动由硬件 RNG 随机生成 |
| `GD32_CHIP_UID[12]` | 运行时从握手 Challenge 包解包 | GD32 芯片 UID | 不再使用编译期常量 |
| `K_session` | 运行时派生 | `AES(MasterKey, nonce_server)` | 每次 OTA 重新生成 |
| SHA-256 | 最终校验 | 从分区读回计算,前端比对 | WiFi/BLE/GD32 均已实现 |
## 构建与部署
### 一、本地编译(Windows PowerShell + ESP-IDF v6.0)
首次编译前需设置工具链环境变量(PowerShell):
## 构建与烧录
```powershell
# ── 设置 ESP-IDF v6.0 工具链环境 ──
$env:IDF_VERSION = "6.0"
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\tools\python\v6.0\venv"
$env:IDF_TOOLS_PATH = "C:\Espressif\tools"
$env:IDF_PATH = "F:\.espressif\v6.0\esp-idf"
$env:ESP_ROM_ELF_DIR = "C:\Espressif\tools\esp-rom-elfs\20241011"
# ── 添加工具链到 PATH ──
$env:PATH = "C:\Espressif\tools\ccache\4.12.1\ccache-4.12.1-windows-x86_64;" +
"C:\Espressif\tools\cmake\4.0.3\bin;" +
"C:\Espressif\tools\esp-clang\esp-20.1.1_20250829\esp-clang\bin;" +
"C:\Espressif\tools\idf-exe\1.0.3\;" +
"C:\Espressif\tools\ninja\1.12.1\;" +
"C:\Espressif\tools\riscv32-esp-elf\esp-15.2.0_20251204\riscv32-esp-elf\bin;" +
"C:\Espressif\tools\riscv32-esp-elf\esp-15.2.0_20251204\riscv32-esp-elf\riscv32-esp-elf\bin;" +
"C:\Espressif\tools\python\v6.0\venv\Scripts;" + $env:PATH
```
编译命令:
```powershell
# ── 进入项目目录 ──
# ESP-IDF v6.0 环境(IDF_TOOLS_PATH/IDF_PATH/PATH,或用 .devcontainer)
Set-Location "G:\Robot_v2\DCU\ESP32_COMM"
# ── 全量编译(自动生成 version.h,确认零 error/warning)──
& "C:\Espressif\tools\python\v6.0\venv\Scripts\python.exe" `
"F:\.espressif\v6.0\esp-idf\tools\idf.py" build
# ── 目标文件 ──
# build/ESP32_COMM.bin ← OTA 烧录用
# build/bootloader/bootloader.bin
# build/partition_table/partition-table.bin
python "F:\.espressif\v6.0\esp-idf\tools\idf.py" build # 产物 build/ESP32_COMM.bin
& "F:\.espressif\v6.0\esp-idf\tools\idf.py" -p COM15 flash monitor # 串口烧录(恢复手段)
```
### 二、烧录方式
- **发版**:手动改 `main/include/version.h`(版本号+变更摘要注释+GIT_HASH);构建时间自动生成
- **网页升级**:控制台上传 `build/ESP32_COMM.bin` → SHA 比对 → 重启(前端按 banner「OTA Relay … 启动」+ uptime<120s 判定恢复)
- **GD32**:上传 .bin/.hex → /ota/relay → 六阶段转发 → PING 确认;**电机**:/motor/ota → /motor/ota/relay
#### 方式 A:串口烧录(首次 / 恢复)
## 网页控制台
连接 ESP32-C3 到串口(默认 COM15),直接在 PowerShell 中烧录:
`tools/` 已移出仓库(快照 `.cache/ws_era_frontend_backup/`),`tools/serve.bat`(http.server:8080)托管,跨源调 REST API,改网页无需重编固件。页面:**index.html** OTA 控制台(双通道、阶段条、启动自检面板、堆/任务抽屉、日志);**motor.html** 电机控制台(三轮询链路互不阻塞、波形);**can.html / can_monitor.html**(DBC、报文编辑、收发监控)。
```powershell
# 全量烧录(固件 + bootloader + 分区表)
& "F:\.espressif\v6.0\esp-idf\tools\idf.py" -p COM15 flash
## 安全注意与已知限制
# 仅烧录应用程序(分区不变时快速更新)
& "F:\.espressif\v6.0\esp-idf\tools\idf.py" -p COM15 app-flash
- ⚠️ **HTTP 无认证**(Bearer token 方案已设计未实施);WiFi 凭据与 MASTER_KEY 占位值明文入仓(2026-08 审计项,待迁 NVS/Kconfig)
- ⚠️ AES-ECB 确定性模式;无固件签名(仅 SHA-256 完整性 + GD32 侧挑战-应答)
- Flash 必须锁 DIO(QIO 会致 OTA 读回 SHA 必错);SPI ≥16MHz 全双工不可行
- tools/ 不在仓库;`spi_link/` 为三工程共享兄弟目录(`../../../spi_link/`),协议改动需三侧同步
# 烧录 + 监控
& "F:\.espressif\v6.0\esp-idf\tools\idf.py" -p COM15 flash monitor
```
## 文档同步说明(HTML ↔ README)
#### 方式 B:WiFi OTA 烧录(ESP32 自升级,无需串口线)
前提:ESP32 已通过 WiFi 联网(或 AP 模式),上位机与 ESP32 在同一网络。
**步骤 1 — 启动 WebUI 服务**:
```powershell
# 在项目目录下运行
tools\serve.bat
# 浏览器打开 http://localhost:8080/tools/index.html
```
**步骤 2 — 连接设备**:
- 在页面顶部输入 ESP32 IP 地址(AP 模式为 `192.168.31.1`,STA 模式查路由器分配 IP)
- 点击 **"测试"** 确认连通,状态指示应变为绿色
**步骤 3 — 上传固件**:
- 在 **"ESP32 自身升级"** 卡片中,拖拽或点击选择 `build/ESP32_COMM.bin`
- 页面自动读取文件大小并计算 SHA-256
- 点击 **"上传并升级 ESP32"**
- WebUI 自动执行:上传 → OTA 写入 → SHA-256 校验 → 设置启动分区 → 重启
- 重启后页面连接断开,ESP32 以新固件启动(约 10~15 秒)
> ⚠️ 如果上传后 ESP32 无法正常启动(反复重启),说明固件有严重 bug,此时需用 **串口烧录** 恢复。
#### 方式 C:BLE OTA 烧录(无需 WiFi)
前提:上位机(手机/PC)支持 Web Bluetooth(Chrome / Edge / Android Chromium)。
- 浏览器打开 `tools/ble_ota.html`
- 点击 **"连接设备"**,在设备选择器中选择 `ESP32-OTA-RELAY`
- 选择固件文件,点击 **"一键刷写"**
- 通过 GATT Notify 实时查看进度和 SHA-256 校验结果
### 三、GD32 SPI OTA 中继烧录
GD32 固件通过 ESP32 中转烧录,不需要串口连接 GD32:
**步骤 1** — 在 ESP32 WebUI 的 **"GD32 SPI 中继"** 卡片中上传 GD32 固件(.bin 或 .hex),上传过程由 **OtaCoordinator** 管理系统状态(`GD32_STORING → GD32_STORED`)
**步骤 2** — 上传完成后系统自动计算 SHA-256 并校验完整性,通过后由 **OtaRelay 事件驱动状态机** 执行转发
**步骤 3** — ESP32 通过六阶段安全握手协议(HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE)将固件逐包转发给 GD32 Bootloader
**步骤 4** — 转发完成后 GD32 APP 启动,ESP32 轮询 PING 确认 APP 存活(最多 15 秒)
> 也可手动点击 **"触发 GD32 OTA 中继"** 按钮重新触发转发(通过 `OtaCoordinator::triggerGd32Relay()` 投递)。
### 四、Python 辅助工具
```powershell
# 串口日志采集(COM15, 5 分钟)
python serial_monitor.py
# OTA 上传测试(HTTP)
python ota_upload_test.py <esp32_ip> <firmware.bin>
```
### 五、开发容器
`.devcontainer/` 提供 Docker 容器,含 ESP-IDF v6.0 工具链和 riscv32-esp-elf 编译器,支持 VS Code Remote-Containers 开箱即用。
## SDK 配置 (`sdkconfig.defaults`)
```ini
CONFIG_ESP_WIFI_RX_BA_WIN=6
CONFIG_ESP_WIFI_TX_BA_WIN=6
CONFIG_LWIP_MAX_SOCKETS=10
CONFIG_LWIP_TCP_WND_DEFAULT=11480
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=11520
```
## OTA 回滚保护
启用 `CONFIG_APP_ROLLBACK_ENABLE` 后:
1. 新固件首次启动必须在主循环前调用 `esp_ota_mark_app_valid_cancel_rollback()`
2. 若新固件在标记前崩溃/重启,bootloader 自动回退到上一个有效分区
3. `main.cpp:145` 在任务创建完成后执行此标记
## 固件版本管理
版本信息位于 `main/include/version.h`,**手动维护**,不再由构建脚本自动生成:
- **版本号**: `0.3.56`(发版时手动递增 `FW_VERSION_MAJOR/MINOR/PATCH` 及 `FW_VERSION_STRING`)
- **构建时间**: `BUILD_TIME`,每次发版编译时手动填写
- **Git Hash**: `GIT_HASH`,手动填写对应提交的 `git rev-parse --short HEAD` 结果
- [docs/project_introduction.html](docs/project_introduction.html) = 完整白皮书(算法字节级细节、状态机、SVG 图解、事故沉淀);本 README = 浓缩速查版,**内容同源**
- **同步规则**:凡影响功能清单 / 协议与常量数值 / 状态机 / 端点表 / 实现状态的代码变更,须**同时更新两份文档**;变更摘要写 `main/include/version.h` 注释;细节以 HTML 为准;两文件顶部「文档同步版本」随变更递增(当前 v0.5.2 / 2026-10-11)
-168
View File
@@ -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 过短(&lt; 5 字节) |
| E1 | 通道非法 |
| E2 | BUS_OFF |
| E3 | TEC 过高(&gt;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),可随业务变更顺手做。
-702
View File
@@ -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 以上。
-96
View File
@@ -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
+1 -1
View File
@@ -43,7 +43,7 @@ typedef struct {
* @param recv_buf 接收数据缓冲区(可为 nullptr)。
* @param recv_max 接收缓冲区最大长度。
* @param timeout_ms 超时时间(毫秒)。
* @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5/0xE6=通道静默拒发);
* @param gd32_err_out 可选输出,GD32 返回的细分错误码(0xE0/0xE1/0xE2/0xE3/0xE4/0xE5/0xE6=通道静默拒发/0xE7=该通道被电机 OTA 独占);
* 仅在返回 -3 时有效,其它情况清零。可为 nullptr。
* @return 成功时返回接收数据长度,失败返回 -1/-2/-3。
*/