docs(readme): 更新文档说明
- 添加项目使用指南 - 完善API接口说明 - 修正错误的配置示例
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user