feat(ota): 引入OTA协调器和事件驱动状态机重构

- 添加OtaCoordinator单例模式,提供9状态系统级OTA协调功能
- 重构OtaRelay为事件驱动状态机,从4阶段扩展为6阶段状态机
- 实现系统级状态管理:OTA_SYS_IDLE/GD32_STORING/GD32_RELAYING等9个状态
- 传输层通过OtaCoordinator统一入口管理ESP32/GD32 OTA操作序列化

fix(protocol): 优化协议层安全配置和参数管理

- Scramble常量改为运行时随机生成,提升安全性
- GD32芯片UID从握手包动态获取,移除编译期常量
- 添加运行时SCRAMBLE_CONST参数说明

refactor(esp32_ota): 优化ESP32 OTA分区擦除策略

- BLE OTA采用实际固件大小而非分区全尺寸进行擦除
- 避免全分区擦除导致的Task Watchdog超时问题
- 擦除大小对齐到64KB块边界确保尾部数据干净

feat(transport): 增强SPI传输层功能和可靠性

- 添加Flash元数据记录(total_size/stored_size/last_seq/checksum)
- 支持中继恢复判断和完整性校验
- 添加ACK错误码支持(0x04=CRC错误)

feat(ble): 实现BLE传输层乱序重排和预检功能

- 添加32槽reorder buffer处理窗口内乱序包
- 新增0xF9命令支持仅SHA校验不入库的预检模式
- 完善BLE OTA收尾流程和状态跟踪

docs(readme): 更新架构文档和API说明

- 详细描述OtaCoordinator系统级状态流转
- 更新OTA流程状态机图表和说明
- 补充新的控制命令和配置参数说明
This commit is contained in:
小龙虾
2026-06-17 02:18:42 +08:00
parent 5b2c5f8ff9
commit 98b5cc1ba1
11 changed files with 1535 additions and 919 deletions
+90 -48
View File
@@ -1,6 +1,6 @@
# ESP32-C3 OTA Relay
**固件版本: v0.3.12** | 构建时间: 2026-06-14 | 基于 ESP-IDF v6.0
**固件版本: v0.3.12** | 构建时间: 2026-06-17 | 基于 ESP-IDF v6.0
## 项目概述
@@ -43,12 +43,13 @@ ESP32-C3FH4 OTA 中继固件,作为 **GD32E508VET6** 的 OTA 升级桥梁。
| **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_relay** | `relay/ota_relay.{h,cpp}` | GD32 OTA 中继核心状态机(四阶段 15 状态) |
| **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 会话密钥管理) |
| **protocol** | `transport/protocol.{h,cpp}` | 协议层(nonce 搅伴生成、AES 会话密钥管理、运行时 SCRAMBLE_CONST) |
## 目录结构
@@ -61,14 +62,16 @@ ESP32_COMM/
├── cmake/
│ └── gen_version_script.cmake # 构建时自动生成 version.h
├── main/
│ ├── CMakeLists.txt # 注册 11 个源文件 + 依赖
│ ├── CMakeLists.txt # 注册 12 个源文件 + 依赖
│ ├── include/
│ │ ├── app_config.h # 统一配置 (constexpr: WiFi/BLE/OTA/阈值/密钥)
│ │ ├── version.h # 自动生成版本信息 (v0.3.12)
│ │ └── 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 核心
│ ├── 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)
@@ -107,7 +110,7 @@ ESP32_COMM/
| `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 | 等待调度信号 → 四阶段状态机 → SPI 转发 |
| `ota_relay` | `OtaRelay::taskEntry` | configMAX_PRIORITIES - 2 | 12288 | 等待 dispatchQueue 调度信号 → 事件驱动状态机(6 阶段)→ SPI 转发 → resultQueue 回传结果 |
额外后台任务:
@@ -119,7 +122,8 @@ ESP32_COMM/
### 跨任务通信
- **OTA 调度**: WiFi/BLE 任务通过 `OtaRelay::dispatchQueue` (FreeRTOS 队列, 深度 4) 发送调度信号给 `ota_relay` 任务;结果通过 `resultQueue` 回传
- **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 连接状态和重连计数器
@@ -128,29 +132,54 @@ ESP32_COMM/
## OTA 核心流程
### 一、GD32 OTA 中继(四阶段 + APP 验证)
### 一、GD32 OTA 中继(事件驱动状态机,6 阶段 + ERROR)
上位机上传 GD32 固件到 ESP32 本地 `gd32_ota` 分区 → 收到触发命令后启动状态机:
上位机上传 GD32 固件到 ESP32 本地 `gd32_ota` 分区(通过 **OtaCoordinator** 协调上传状态)→ 收到触发命令后由 **OtaRelay** 事件驱动状态机执行转发:
```
阶段1 ─ 安全握手 (5 状态)
RELAY_HANDSHAKE_SENT → RELAY_CHALLENGE_SENT → RELAY_CHALLENGE_RESPOND
→ RELAY_SESSION_KEY_SENT → RELAY_AUTH_COMPLETE
状态机总览:
阶段2 ─ 固件大小协商 (2 状态)
RELAY_SIZE_SENT → RELAY_SIZE_ACK
阶段3 ─ 数据包转发 (4 状态)
RELAY_RELAY_ACTIVE → RELAY_PACKET_SENT → RELAY_PACKET_ACK_OK
[重试] RELAY_PACKET_ACK_RETRY
阶段4 ─ 完成与跳转 (3 状态)
RELAY_RECEIVE_DONE → RELAY_FINISH_SENT → RELAY_DONE
阶段5 ─ APP 存活验证
RELAY_APP_VERIFY (PING GD32 APP, 最多 30 次 × 500ms)
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 超时)
@@ -159,17 +188,21 @@ ESP32_COMM/
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 健康检查(温度/堆)
- 支持重试(单包最多 10 次),结构化 ACK 含 seq 校验,残留 ACK 自动过滤
- 全局超时: 动态计算 5~10 分钟
- 支持结构化 ACK 含 seq 校验 + 错误码(`0x04`=CRC 错误),残留 ACK 自动过滤
- 单包最多 10 次重试,全局超时动态计算 5~10 分钟
- **Flash 元数据**: 通过 NVS 记录固件 `total_size`/`stored_size`/`last_seq`/`checksum`,用于完整性校验和中继恢复判断
### 二、ESP32 本机 OTA(WiFi + BLE 共用)
核心逻辑封装在 `Esp32OtaCore` 单例中,WiFi 和 BLE 通道共用:
核心逻辑封装在 `Esp32OtaCore` 单例中,WiFi 和 BLE 通道共用。
传输层通过 **OtaCoordinator** 统一入口(`beginEsp32Ota()`)发起操作,由协调器管理系统状态并委托 `Esp32OtaCore` 执行底层写入:
| 步骤 | 方法 | 说明 |
|------|------|------|
@@ -178,7 +211,7 @@ ESP32_COMM/
| 3 | `finalize(sha_hex, reboot)` | 从分区读回计算 SHA-256 → 比对前端 SHA → `esp_ota_end` → `esp_ota_set_boot_partition` → 可选重启 |
| - | `abort()` | 中止并清理(释放 mutex + 复位状态) |
**SHA-256 校验**: 固件写入完成后从 OTA 分区读回计算,不做增量 SHA。前端与设备端比对一致才 set_boot,否则返回 `ESP_ERR_INVALID_VERSION`。
**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` 后,新固件崩溃自动回退。
@@ -198,6 +231,7 @@ ESP32_COMM/
| 0x02 | OTA_CMD_DATA | OTA 数据帧 |
| 0x03 | OTA_CMD_CANCEL | 取消 OTA |
| 0xB0 | APP_ENTER_BOOT_CMD | 通知 GD32 APP 进入 Bootloader |
| 0xB5 | APP_ENTER_BOOT_ACK_OK | GD32 APP 确认进入 Bootloader |
| 0x80 | RSP_ACK | 响应:数据就绪 |
| 0x81 | RSP_WAIT | 响应:GD32 忙,请等待 |
| 0x82 | RSP_ERROR | 响应:错误 |
@@ -263,7 +297,7 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器)
| UUID | 特征 | 属性 | 用途 |
|------|------|------|------|
| 0xFFFF | Service | - | OTA GATT 服务 |
| 0x0001 | CTRL | Write | 控制命令:`0xF7`=触发GD32中继, `0xF8+32B SHA`=ESP32 OTA, `0xFA`=重启ESP32 |
| 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 | 固件版本字符串 |
@@ -272,7 +306,8 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器)
- **ACK 窗口**: 每 32 包等待一次 DATA_OUT ACK(带累计字节数验证)
- **窗口重传**: ACK 超时或字节数不足时重发整个窗口(最多 3 次)
- **ESP32 收尾**: BLE 传输完成后由前端发 `0xF8 + 32B 文件 SHA-256`,设备端读回 OTA 分区做 SHA 比对,通过后完成 `esp_ota_end`、设置启动分区并重启;FINISH 仅上报完成/重启/错误
- **乱序重排**: 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,接收端支持一次解析多个数据包
@@ -283,7 +318,7 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器)
| 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 版本等 |
| GET | `/system/status` | 系统状态 JSON | - | 含版本/WiFi/OTA/温度/堆/GD32 版本及**OtaCoordinator 系统级状态**等 |
| GET | `/logs` | 拉取调试日志 | - | JSON 数组字符串 |
| POST | `/can/send` | 发送 CAN 帧 | `{"ch":0,"id":123,"dlc":8,"data":"0102..."}` | `{"status":"ok","recv":"..."}` |
| POST | `/spi/send` | SPI 直接发送 | `{"type":"01","data":"00AC","timeout_ms":1000}` | `{"status":"ok","resp_type":".."}` |
@@ -291,7 +326,7 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器)
### POST /ota/upload 协议
通过 JSON 头中的 `type` 字段自动区分目标:
通过 JSON 头中的 `type` 字段自动区分目标,传输层通过 **OtaCoordinator** 统一管理系统状态:
```json
// ESP32 OTA
@@ -303,8 +338,8 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器)
<binary firmware data...>
```
- **ESP32 模式**: 直接写入 OTA 分区,进度上报,完成后调用 `/ota/update` 收尾
- **GD32 模式**: 写入 `gd32_ota` 分区 → 擦除 → 计算 SHA-256 → 自动触发 SPI 中继
- **ESP32 模式**: `OtaCoordinator::beginEsp32Ota()` → `Esp32OtaCore::write()` 流式写入 OTA 分区 → 前端调 `/ota/update` 收尾
- **GD32 模式**: `OtaCoordinator::beginGd32Storage()` → 写入 `gd32_ota` 分区 → `OtaCoordinator::completeGd32Storage()` → 计算 SHA-256 → 自动触发 SPI 中继
### curl 示例
@@ -332,10 +367,10 @@ curl http://192.168.31.1/logs
- ESP32 固件上传与 OTA 升级(含 SHA-256 前端校验)
- GD32 固件上传(支持 .bin/.hex)与 SPI 中继触发
- 实时 OTA 流程阶段显示(6 阶段进度追踪)
- 实时 OTA 流程阶段显示(6 阶段进度追踪,含 **OtaCoordinator 系统级状态**)
- CAN 帧手动发送面板
- SPI 直接发送面板
- 系统状态监控(温度、RSSI、空闲堆、最小堆、GD32 APP/BOOT 版本、OTA 缓存)
- 系统状态监控(温度、RSSI、空闲堆、最小堆、GD32 APP/BOOT 版本、OTA 缓存、**协调器系统状态**)
- 调试日志面板(HTTP `/logs` 轮询,按级别过滤)
- 一键重启 ESP32
@@ -392,30 +427,37 @@ UDP socket 延迟创建(首次 `send()` 时才分配),WiFi 未连接时 `s
| 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` | -75 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_RESEND_RETRIES` | 10 | 单包最大重发次数 |
| OTA | `OTA_PACKET_HEALTH_CHECK_INTERVAL` | 32 | 每 N 包健康检查 |
| OTA | `OTA_PACKET_DELAY_MS` | 5 | 每包发送后延时(ms) |
| 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 | 温度危险阈值 |
| 阈值 | `WIFI_RSSI_CRITICAL` | -85 dBm | RSSI 危险阈值 |
| 阈值 | `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:110 | AES-128 主密钥 | ⚠️ 占位值,生产环境需更换 |
| `SCRAMBLE_CONST[16]` | app_config.h:115 | Nonce 搅伴常量 | ⚠️ 占位值,生产环境需更换 |
| `GD32_CHIP_UID[12]` | app_config.h:125 | 运行时从握手包获取 | 仅保留默认占位 |
| `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 均已实现 |
| SHA-256 | 最终校验 | 从分区读回计算,前端比对 | WiFi/BLE/GD32 均已实现 |
## 构建与部署
@@ -515,15 +557,15 @@ tools\serve.bat
GD32 固件通过 ESP32 中转烧录,不需要串口连接 GD32:
**步骤 1** — 在 ESP32 WebUI 的 **"GD32 SPI 中继"** 卡片中上传 GD32 固件(.bin 或 .hex)
**步骤 1** — 在 ESP32 WebUI 的 **"GD32 SPI 中继"** 卡片中上传 GD32 固件(.bin 或 .hex),上传过程由 **OtaCoordinator** 管理系统状态(`GD32_STORING → GD32_STORED`)
**步骤 2** — 上传完成后,系统自动计算 SHA-256 并触发 SPI 中继转发
**步骤 2** — 上传完成后系统自动计算 SHA-256 并校验完整性,通过后由 **OtaRelay 事件驱动状态机** 执行转发
**步骤 3** — ESP32 通过四阶段安全握手协议将固件逐包转发给 GD32 Bootloader
**步骤 3** — ESP32 通过六阶段安全握手协议(HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE)将固件逐包转发给 GD32 Bootloader
**步骤 4** — 转发完成后 GD32 APP 启动,ESP32 轮询 PING 确认 APP 存活(最多 15 秒)
> 也可手动点击 **"触发 GD32 OTA 中继"** 按钮重新触发转发。
> 也可手动点击 **"触发 GD32 OTA 中继"** 按钮重新触发转发(通过 `OtaCoordinator::triggerGd32Relay()` 投递)。
### 四、Python 辅助工具
@@ -554,12 +596,12 @@ CONFIG_LWIP_TCP_SND_BUF_DEFAULT=11520
启用 `CONFIG_APP_ROLLBACK_ENABLE` 后:
1. 新固件首次启动必须在主循环前调用 `esp_ota_mark_app_valid_cancel_rollback()`
2. 若新固件在标记前崩溃/重启,bootloader 自动回退到上一个有效分区
3. `main.cpp:140` 在任务创建完成后执行此标记
3. `main.cpp:145` 在任务创建完成后执行此标记
## 固件版本管理
位于 `cmake/gen_version_script.cmake`,在每次 `idf.py build` 时自动执行:
- **版本号**: `0.3.12`(手动修改 CMake 文件中的 MAJOR/MINOR/PATCH)
- **构建时间**: 自动写入 `%Y-%m-%d %H:%M:%S`
- **Git Hash**: 自动从 `git rev-parse --short HEAD` 获取
- **Git Hash**: 自动从 `git rev-parse --short HEAD` 获取 (当前 `5b2c5f8`)
- 输出到 `main/include/version.h`,仅在内容变化时写入以避免不必要的重编译
+1
View File
@@ -2,6 +2,7 @@
SRCS
"core/main.cpp"
"ota/esp32_ota_core.cpp"
"ota/ota_coordinator.cpp"
"relay/ota_relay.cpp"
"transport/spi_master.cpp"
"transport/protocol.cpp"
+5
View File
@@ -15,6 +15,7 @@
#include "monitor/health_monitor.h"
#include "relay/ota_relay.h"
#include "ota/ota_coordinator.h"
#include "transport/spi_master.h"
#include "server/wifi_ota.h"
#include "server/ble_ota.h"
@@ -79,6 +80,10 @@ extern "C" void app_main(void)
Protocol::instance().init();
DEBUG_INFO("MAIN", "协议层初始化完成");
// ★ 全局 OTA 协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
OtaCoordinator::instance().init();
DEBUG_INFO("MAIN", "OTA 全局协调器已初始化");
esp_err_t ret = SpiMaster::instance().init();
if (ret != ESP_OK) {
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret);
+2 -2
View File
@@ -8,7 +8,7 @@
#define FW_VERSION_STRING "0.3.12"
// 编译时间
#define BUILD_TIME "2026-06-16 23:01:18"
#define BUILD_TIME "2026-06-17 02:12:38"
// Git 信息
#define GIT_HASH "2875ef2"
#define GIT_HASH "5b2c5f8"
+9 -4
View File
@@ -106,10 +106,15 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
return ESP_FAIL;
}
// ★ 必须传分区大小,而非固件实际大小
// esp_ota_begin 用 size 参数决定擦除多少 flash 扇区
// 传固件大小只擦部分扇区,未擦除区域可能残留脏数据,导致 esp_ota_end 校验失败
esp_err_t err = esp_ota_begin(m_partition, m_partition->size, &m_handle);
// BLE OTA 修复:传固件实际大小而非分区全尺寸,避免全分区擦除导致
// 长时间阻塞触发 Task Watchdog(ESP32-C3 单核 5s 超时)。
// esp_ota_begin 按 image_size 擦除;esp_ota_end 仅校验已写数据,
// 未擦除残留扇区不影响校验。size 对齐到 64KB 块边界确保尾部干净。
size_t erase_size = (total_size + 0xFFFF) & ~0xFFFFULL;
if (erase_size > m_partition->size) {
erase_size = m_partition->size;
}
esp_err_t err = esp_ota_begin(m_partition, erase_size, &m_handle);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
m_partition = nullptr;
+436
View File
@@ -0,0 +1,436 @@
/*
* ota_coordinator.cpp - 全局 OTA 协调器实现
*
* OtaCoordinator 是系统级 OTA 状态的唯一权威来源。
* 传输层(WiFi/BLE)通过本协调器发起所有 OTA 操作。
*/
#include "ota_coordinator.h"
#include "esp32_ota_core.h"
#include "relay/ota_relay.h"
#include "monitor/debug_log.h"
#define TAG "OTA_COORD"
// ============================================================
// 单例
// ============================================================
OtaCoordinator& OtaCoordinator::instance()
{
static OtaCoordinator inst;
return inst;
}
// ============================================================
// 线程安全辅助
// ============================================================
void OtaCoordinator::lock()
{
// ★ 修复:FreeRTOS mutex (xSemaphoreCreateMutex) 只能由 take 的 task give。
// 超时后必须在 unlock() 中跳过 give,避免 xTaskPriorityDisinherit assert。
// 此处假设 OtaCoordinator 内部不嵌套 lock(全局已确认:无嵌套调用)。
m_locked = false;
if (m_mutex) {
m_locked = (xSemaphoreTake(m_mutex, kLockTimeout) == pdTRUE);
if (!m_locked) {
DEBUG_WARN(TAG, "lock() 超时 (%u ms),本次状态变更被跳过",
(unsigned)pdTICKS_TO_MS(kLockTimeout));
}
}
}
void OtaCoordinator::unlock()
{
// ★ 修复:仅在 lock() 成功 take 时才 give。
// 错误的 task 给同一 mutex give 会触发 xTaskPriorityDisinherit assert → PANIC。
if (m_locked && m_mutex) {
xSemaphoreGive(m_mutex);
m_locked = false;
}
}
// ============================================================
// 卡死自愈检查(必须在持锁状态下调用)
// ============================================================
void OtaCoordinator::watchdogCheckLocked()
{
// 仅对"忙"状态(可能卡死的)做检查
switch (m_sysState) {
case OTA_SYS_GD32_STORING:
case OTA_SYS_GD32_RELAYING:
case OTA_SYS_ESP32_RECEIVING:
case OTA_SYS_ESP32_FINALIZING:
case OTA_SYS_ESP32_REBOOTING:
break;
default:
return; // IDLE/STORED/COMPLETE/ERROR 无需检查
}
if (m_busySinceTick == 0) {
// 未记录起始时刻(理论上不应发生),保守不触发
return;
}
TickType_t elapsed = xTaskGetTickCount() - m_busySinceTick;
if (elapsed < kBusyWatchdogTicks) {
return; // 未超时
}
// ★ 超时自愈:强制转 ERROR。
// ERROR 不在任何 beginXxx() 的拒绝列表中,下次请求会自动从 ERROR 恢复。
// 清理由各子系统自行负责(abortCurrent 中会按 ERROR 不做实际清理,仅复位状态)。
DEBUG_ERR(TAG, "忙状态 %d 卡死超过 %" PRIu32 " ms,强制自愈 → ERROR",
(int)m_sysState, (uint32_t)pdTICKS_TO_MS(kBusyWatchdogTicks));
m_sysState = OTA_SYS_ERROR;
m_busySinceTick = 0;
}
// ============================================================
// 初始化
// ============================================================
void OtaCoordinator::init()
{
if (!m_mutex) {
m_mutex = xSemaphoreCreateMutex();
if (m_mutex) {
DEBUG_INFO(TAG, "OTA 全局协调器已初始化");
} else {
DEBUG_ERR(TAG, "协调器 mutex 创建失败");
}
}
m_sysState = OTA_SYS_IDLE;
m_gd32StorageDone = false;
m_busySinceTick = 0;
// ★ 重启恢复:如果 NVS 里有完整固件元数据,开机时立即进入 STORED 状态,
// 否则用户重启后看到的状态是 IDLE 但 NVS 里其实有未中继的固件,前后不一致。
ota_meta_t boot_meta;
if (OtaRelay::metaLoad(&boot_meta) == ESP_OK &&
boot_meta.total_size > 0 &&
boot_meta.stored_size >= boot_meta.total_size) {
m_sysState = OTA_SYS_GD32_STORED;
m_gd32StorageDone = true;
DEBUG_INFO(TAG, "启动恢复: NVS 检测到完整 GD32 固件 (%" PRIu32 " 字节), → GD32_STORED",
boot_meta.total_size);
}
}
// ============================================================
// GD32 存储阶段
// ============================================================
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size)
{
lock();
// ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程
watchdogCheckLocked();
// 系统忙检查:仅拒绝真正"在传输"的活跃状态
// STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖
// ★ 修复:之前把所有非 IDLE 都当忙,导致"Flash 缓存里有固件但没在传输"时无法上传新固件
if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
m_sysState == OTA_SYS_ESP32_REBOOTING ||
m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING) {
DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState);
unlock();
return ESP_ERR_INVALID_STATE;
}
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),先清理再开始新存储
if (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE) {
DEBUG_INFO(TAG, "清理旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
OtaRelay::metaClear();
}
m_sysState = OTA_SYS_GD32_STORING;
m_gd32StorageDone = false;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING", total_size);
unlock();
return ESP_OK;
}
void OtaCoordinator::completeGd32Storage()
{
lock();
if (m_sysState == OTA_SYS_GD32_STORING) {
m_sysState = OTA_SYS_GD32_STORED;
m_gd32StorageDone = true;
m_busySinceTick = 0; // ★ STORED 是非忙状态
DEBUG_INFO(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
} else {
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
}
unlock();
}
// ============================================================
// GD32 中继阶段
// ============================================================
esp_err_t OtaCoordinator::triggerGd32Relay()
{
lock();
// ★ 卡死自愈检查
watchdogCheckLocked();
// 校验:必须处于"已存储"或"已中继完成"状态
// ★ 修复:COMPLETE(旧固件已成功推给 GD32,NVS 元数据已被 doFinishAndVerify 清掉,
// OtaRelay 状态为 DONE)也能触发——但需要先回滚到 STORED 以保证 m_busySinceTick 一致。
// 通常用户应先上传新固件再触发中继,COMPLETE 时元数据已被清,metaLoad 会返回 stored=0
// 并在 188 行被拒绝,自然不会走到这里。
if (m_sysState != OTA_SYS_GD32_STORED && m_sysState != OTA_SYS_IDLE) {
if (m_sysState == OTA_SYS_GD32_STORING) {
DEBUG_WARN(TAG, "GD32 正在存储中,拒绝触发中继");
unlock();
return ESP_ERR_INVALID_STATE;
}
if (m_sysState == OTA_SYS_GD32_RELAYING) {
DEBUG_WARN(TAG, "GD32 中继已在进行中");
unlock();
return ESP_ERR_INVALID_STATE;
}
if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
m_sysState == OTA_SYS_ESP32_REBOOTING) {
DEBUG_WARN(TAG, "ESP32 OTA 进行中,拒绝中继触发");
unlock();
return ESP_ERR_INVALID_STATE;
}
}
// 验证固件是否确实已存储(通过 NVS 元数据)
ota_meta_t meta;
if (OtaRelay::metaLoad(&meta) != ESP_OK ||
meta.total_size == 0 ||
meta.stored_size < meta.total_size) {
DEBUG_WARN(TAG, "GD32 固件未完整存储,拒绝触发中继");
unlock();
return ESP_ERR_INVALID_STATE;
}
// 检查 OtaRelay 是否空闲
if (OtaRelay::getState() != RELAY_IDLE &&
OtaRelay::getState() != RELAY_DONE &&
OtaRelay::getState() != RELAY_ERROR) {
DEBUG_WARN(TAG, "OtaRelay 正忙 (state=%d),拒绝触发", (int)OtaRelay::getState());
unlock();
return ESP_ERR_INVALID_STATE;
}
// 向 OtaRelay task 发送启动消息
if (!OtaRelay::dispatchQueue) {
DEBUG_ERR(TAG, "OtaRelay dispatchQueue 未就绪");
unlock();
return ESP_ERR_INVALID_STATE;
}
m_sysState = OTA_SYS_GD32_RELAYING;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
uint32_t cmd = 1;
BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &cmd, pdMS_TO_TICKS(200));
if (ret != pdTRUE) {
DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败");
m_sysState = OTA_SYS_GD32_STORED;
m_busySinceTick = 0; // 回退到 STORED(非忙)
unlock();
return ESP_FAIL;
}
unlock();
return ESP_OK;
}
void OtaCoordinator::notifyRelayStarted()
{
lock();
if (m_sysState != OTA_SYS_GD32_RELAYING) {
m_sysState = OTA_SYS_GD32_RELAYING;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
DEBUG_INFO(TAG, "中继已开始, 系统状态 → GD32_RELAYING");
}
unlock();
}
void OtaCoordinator::notifyRelayComplete(bool success)
{
lock();
if (success) {
m_sysState = OTA_SYS_GD32_COMPLETE;
m_gd32StorageDone = false;
m_busySinceTick = 0; // ★ COMPLETE 非忙
DEBUG_INFO(TAG, "GD32 中继完成, 系统状态 → GD32_COMPLETE");
} else {
m_sysState = OTA_SYS_ERROR;
m_busySinceTick = 0; // ★ ERROR 非忙(可被后续请求恢复)
DEBUG_ERR(TAG, "GD32 中继失败, 系统状态 → ERROR");
}
unlock();
}
// ============================================================
// ESP32 OTA 阶段
// ============================================================
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
{
lock();
// ★ 卡死自愈检查
watchdogCheckLocked();
// 系统忙检查
// ★ 修复:STORING/RELAYING 都拒绝(防止中继中改 gd32_ota 分区被 ESP32 OTA 误触发)。
// STORED/COMPLETE 允许——它们是非活跃状态,ESP32 OTA 与 gd32_ota 分区无冲突。
if (m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING) {
DEBUG_WARN(TAG, "GD32 正在 %s,拒绝 ESP32 OTA",
m_sysState == OTA_SYS_GD32_STORING ? "存储" : "中继");
unlock();
return ESP_ERR_INVALID_STATE;
}
// 委托 Esp32OtaCore 处理分区/互斥逻辑
esp_err_t err = Esp32OtaCore::instance().begin(total_size);
if (err == ESP_OK) {
m_sysState = OTA_SYS_ESP32_RECEIVING;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING", total_size);
} else {
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
}
unlock();
return err;
}
void OtaCoordinator::notifyEsp32ReceiveComplete()
{
lock();
if (m_sysState == OTA_SYS_ESP32_RECEIVING) {
m_sysState = OTA_SYS_ESP32_FINALIZING;
DEBUG_INFO(TAG, "ESP32 固件接收完毕, 系统状态 → ESP32_FINALIZING");
}
unlock();
}
void OtaCoordinator::notifyEsp32FinalizeComplete()
{
lock();
m_sysState = OTA_SYS_ESP32_REBOOTING;
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成, 系统状态 → ESP32_REBOOTING");
unlock();
}
void OtaCoordinator::notifyEsp32Rebooting()
{
lock();
m_sysState = OTA_SYS_ESP32_REBOOTING;
DEBUG_INFO(TAG, "ESP32 即将重启, 系统状态 → ESP32_REBOOTING");
unlock();
}
// ============================================================
// 中止
// ============================================================
void OtaCoordinator::abortCurrent()
{
lock();
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState);
if (m_sysState == OTA_SYS_GD32_STORING) {
OtaRelay::metaClear();
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
OtaRelay::instance().reset();
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
Esp32OtaCore::instance().abort();
}
m_sysState = OTA_SYS_IDLE;
m_gd32StorageDone = false;
m_busySinceTick = 0; // ★ 离开忙状态
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
unlock();
}
// ============================================================
// 状态查询
// ============================================================
ota_system_state_t OtaCoordinator::getSystemState() const
{
// 对于纯读取,短暂加锁保证一致性
OtaCoordinator* self = const_cast<OtaCoordinator*>(this);
self->lock();
ota_system_state_t s = m_sysState;
self->unlock();
return s;
}
bool OtaCoordinator::isIdle() const
{
return (getSystemState() == OTA_SYS_IDLE ||
getSystemState() == OTA_SYS_GD32_STORED ||
getSystemState() == OTA_SYS_GD32_COMPLETE);
}
bool OtaCoordinator::isGd32Busy() const
{
ota_system_state_t s = getSystemState();
return (s == OTA_SYS_GD32_STORING ||
s == OTA_SYS_GD32_RELAYING);
}
bool OtaCoordinator::isEsp32Busy() const
{
ota_system_state_t s = getSystemState();
return (s == OTA_SYS_ESP32_RECEIVING ||
s == OTA_SYS_ESP32_FINALIZING ||
s == OTA_SYS_ESP32_REBOOTING);
}
const char* OtaCoordinator::getSystemStateString() const
{
switch (getSystemState()) {
case OTA_SYS_IDLE: return "IDLE";
case OTA_SYS_GD32_STORING: return "GD32_STORING";
case OTA_SYS_GD32_STORED: return "GD32_STORED";
case OTA_SYS_GD32_RELAYING: return "GD32_RELAYING";
case OTA_SYS_GD32_COMPLETE: return "GD32_COMPLETE";
case OTA_SYS_ESP32_RECEIVING: return "ESP32_RECEIVING";
case OTA_SYS_ESP32_FINALIZING: return "ESP32_FINALIZING";
case OTA_SYS_ESP32_REBOOTING: return "ESP32_REBOOTING";
case OTA_SYS_ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
bool OtaCoordinator::getGd32AppAlive() const
{
return OtaRelay::getGd32AppAlive();
}
bool OtaCoordinator::isGd32StorageDone() const
{
OtaCoordinator* self = const_cast<OtaCoordinator*>(this);
self->lock();
bool done = m_gd32StorageDone;
self->unlock();
return done;
}
// ============================================================
// 完全复位
// ============================================================
void OtaCoordinator::reset()
{
lock();
m_sysState = OTA_SYS_IDLE;
m_gd32StorageDone = false;
m_busySinceTick = 0; // ★ 复位
DEBUG_INFO(TAG, "协调器已完全复位");
unlock();
}
+205
View File
@@ -0,0 +1,205 @@
/*
* ota_coordinator.h - 全局 OTA 协调器
*
* OtaCoordinator 是系统级 OTA 状态的唯一权威来源,负责:
* 1. 串行化 GD32 中继与 ESP32 OTA 操作(同一时刻只能进行一种)
* 2. 为传输层(WiFi/BLE)提供统一的 OTA 操作入口
* 3. 通过 FreeRTOS 队列向 OtaRelay task 投递中继命令
* 4. 向上层(web UI / status API)暴露系统级状态
*
* 线程模型:
* - 状态读写由内部 mutex 保护
* - begin/complete 接口可在任意任务中调用
* - triggerGd32Relay() 向 OtaRelay task 的 dispatchQueue 发送消息
*/
#ifndef OTA_COORDINATOR_H
#define OTA_COORDINATOR_H
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_err.h"
// ============================================================
// 系统级 OTA 状态枚举
// ============================================================
typedef enum {
OTA_SYS_IDLE = 0,
OTA_SYS_GD32_STORING, // 正在从传输层接收 GD32 固件
OTA_SYS_GD32_STORED, // GD32 固件已完整存储,等待中继触发
OTA_SYS_GD32_RELAYING, // 正在通过 SPI 向 GD32 中继固件
OTA_SYS_GD32_COMPLETE, // GD32 OTA 完成(APP 已存活确认)
OTA_SYS_ESP32_RECEIVING, // 正在接收 ESP32 自身固件
OTA_SYS_ESP32_FINALIZING, // 正在收尾 ESP32 OTA(SHA校验 + end + set_boot)
OTA_SYS_ESP32_REBOOTING, // 即将重启 ESP32
OTA_SYS_ERROR // 错误状态
} ota_system_state_t;
/**
* @brief 全局 OTA 协调器(单例)
*
* 提供系统级 OTA 状态管理与操作串行化。
* 传输层(WiFi/BLE)通过本协调器发起所有 OTA 操作,
* 不再直接调用 OtaRelay / Esp32OtaCore 的底层接口。
*/
class OtaCoordinator {
public:
/**
* @brief 获取 OtaCoordinator 单例实例。
*/
static OtaCoordinator& instance();
// ── 初始化 ──
/**
* @brief 初始化协调器(创建 mutex 和队列)。
* 在 OtaRelay task 创建前调用。
*/
void init();
// ── GD32 存储阶段(传输层 → Flash)──
/**
* @brief 传输层开始接收 GD32 固件前调用。
* 若系统正忙(ESP32 OTA 进行中或 GD32 中继中),返回失败。
* @param total_size 固件总字节数。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙。
*/
esp_err_t beginGd32Storage(uint32_t total_size);
/**
* @brief GD32 固件接收完毕时调用。
* 更新系统状态为 OTA_SYS_GD32_STORED。
*/
void completeGd32Storage();
// ── GD32 中继阶段 ──
/**
* @brief 触发 GD32 中继转发。
* 向 OtaRelay task 的 dispatchQueue 发送启动消息。
* @return ESP_OK 消息已投递,ESP_ERR_INVALID_STATE 不允许触发。
*/
esp_err_t triggerGd32Relay();
/**
* @brief OtaRelay task 回调:中继已开始。
* 由 OtaRelay 在开始 forwardFirmware() 时调用。
*/
void notifyRelayStarted();
/**
* @brief OtaRelay task 回调:中继已完成。
* 由 OtaRelay 在 forwardFirmware() 返回后调用。
* @param success true 成功,false 失败。
*/
void notifyRelayComplete(bool success);
// ── ESP32 OTA 阶段 ──
/**
* @brief ESP32 OTA 开始接收固件。
* 内部委托 Esp32OtaCore::begin()。
* @param total_size 固件总字节数。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙或通道被占用。
*/
esp_err_t beginEsp32Ota(uint32_t total_size);
/**
* @brief ESP32 OTA 固件接收完毕,准备收尾。
* 更新系统状态为 OTA_SYS_ESP32_FINALIZING。
*/
void notifyEsp32ReceiveComplete();
/**
* @brief ESP32 OTA 收尾完成(finalize 成功)。
*/
void notifyEsp32FinalizeComplete();
/**
* @brief ESP32 即将重启。
*/
void notifyEsp32Rebooting();
// ── 中止 ──
/**
* @brief 中止当前 OTA 操作(GD32 或 ESP32)。
* 内部调用对应子系统的 abort/reset。
*/
void abortCurrent();
// ── 状态查询 ──
/**
* @brief 获取当前系统级 OTA 状态。
*/
ota_system_state_t getSystemState() const;
/**
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
*/
bool isIdle() const;
/**
* @brief 检查是否有 GD32 相关操作正在进行。
*/
bool isGd32Busy() const;
/**
* @brief 检查是否有 ESP32 OTA 正在进行。
*/
bool isEsp32Busy() const;
/**
* @brief 获取系统状态的可读字符串描述。
*/
const char* getSystemStateString() const;
// ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)──
/**
* @brief 获取 GD32 APP 存活状态。
*/
bool getGd32AppAlive() const;
/**
* @brief GD32 固件存储完成标志(供状态查询)。
*/
bool isGd32StorageDone() const;
// ── 完全复位 ──
/**
* @brief 完全复位协调器和所有子系统状态。
*/
void reset();
private:
OtaCoordinator() = default;
~OtaCoordinator() = default;
OtaCoordinator(const OtaCoordinator&) = delete;
OtaCoordinator& operator=(const OtaCoordinator&) = delete;
// 线程安全辅助
void lock();
void unlock();
// ★ 卡死自愈检查:若忙状态停留超过 watchdog 超时,强制转为 ERROR。
// 用于在中继 task 因任何原因(崩溃/SPI 死锁/看门狗复位残留)未能回调
// notifyRelayComplete/notifyEsp32*Complete 时,让协调器自动脱离卡死状态。
// 必须在已持锁状态下调用。
void watchdogCheckLocked();
// 系统状态
ota_system_state_t m_sysState = OTA_SYS_IDLE;
// 进入当前忙状态的时刻(tick)。IDLE/STORED/COMPLETE/ERROR 时为 0。
TickType_t m_busySinceTick = 0;
// GD32 存储跟踪
bool m_gd32StorageDone = false;
// 互斥锁
SemaphoreHandle_t m_mutex = nullptr;
bool m_locked = false; // ★ 记录最近一次 lock() 是否成功 take;unlock() 据此跳过错误 give
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100);
// ★ 忙状态 watchdog 超时。GD32 中继理论最大耗时 ≈ 60s(全局超时) + 15s(APP存活轮询) + 1s(bootloader) ≈ 76s,
// 取 90 s,正常流程远不会触及,仅在 task 卡死时触发自愈。
static constexpr TickType_t kBusyWatchdogTicks = pdMS_TO_TICKS(90U * 1000U);
};
#endif // OTA_COORDINATOR_H
+454 -500
View File
File diff suppressed because it is too large Load Diff
+157 -264
View File
@@ -1,8 +1,16 @@
/*
* ota_relay.h - OTA 中继核心状态机
* T2.3, T3.1 - T3.7
* ota_relay.h - OTA 中继事件驱动状态机
*
* OtaRelay 类封装 OTA 中继状态机、Flash 存储和元数据管理。
* OtaRelay 将 GD32 SPI OTA 协议封装为事件驱动的状态机:
* 1. 每个协议阶段对应一个状态(共 6 个阶段状态 + ERROR)
* 2. 状态通过 processEvent() 驱动转换,而非顺序赋值
* 3. 外部(OtaCoordinator / transport layer)通过 dispatchQueue 触发中继
* 4. forwardFirmware() 保留为便捷入口,内部驱动状态机循环
*
* 状态转换:
* IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
* 任意状态 ──(error)──→ ERROR
* ERROR/DONE ──(reset)──→ IDLE
*/
#ifndef OTA_RELAY_H
#define OTA_RELAY_H
@@ -24,26 +32,31 @@ typedef struct {
uint8_t checksum[32];
} ota_meta_t;
// ============================================================
// 阶段状态枚举(精简为 7 个状态,原 17 个"微状态"合并为阶段)
// ============================================================
typedef enum {
RELAY_IDLE = 0,
RELAY_HANDSHAKE_SENT,
RELAY_CHALLENGE_SENT,
RELAY_CHALLENGE_RESPOND,
RELAY_SESSION_KEY_SENT,
RELAY_AUTH_COMPLETE,
RELAY_SIZE_SENT,
RELAY_SIZE_ACK,
RELAY_RELAY_ACTIVE,
RELAY_PACKET_SENT,
RELAY_PACKET_ACK_OK,
RELAY_PACKET_ACK_RETRY,
RELAY_RECEIVE_DONE,
RELAY_FINISH_SENT,
RELAY_DONE,
RELAY_APP_VERIFY,
RELAY_ERROR = 0xFF
RELAY_IDLE = 0, // 空闲
RELAY_HANDSHAKING, // 阶段1: 握手 + 挑战认证
RELAY_SIZE_NEG, // 阶段2: 固件大小协商
RELAY_TRANSFERRING, // 阶段3: 数据包传输
RELAY_FINISHING, // 阶段4-5: 结束OTA + APP存活验证
RELAY_DONE, // 完成
RELAY_ERROR = 0xFF // 错误
} relay_state_t;
// ============================================================
// 事件类型(驱动状态转换)
// ============================================================
typedef enum {
RELAY_EVT_START = 0, // 启动中继流程
RELAY_EVT_RESP_OK, // 收到 GD32 有效响应
RELAY_EVT_RESP_ERROR, // 收到 GD32 错误响应
RELAY_EVT_TIMEOUT, // 超时
RELAY_EVT_HEALTH_CRIT, // 健康检查危险
RELAY_EVT_ABORT // 外部中止
} relay_event_t;
typedef enum {
OTA_RESULT_OK = 0,
OTA_RESULT_ERR_TIMEOUT = -1,
@@ -58,141 +71,117 @@ typedef enum {
class OtaRelay {
public:
/**
* @brief 获取OtaRelay单例引用.
* @return OtaRelay单例引用.
* @brief 获取OtaRelay单例引用.
*/
static OtaRelay& instance();
/**
* @brief OTA中继FreeRTOS任务入口.
* @param pvParameters FreeRTOS任务参数.
* @brief OTA中继FreeRTOS任务入口.
*/
static void taskEntry(void *pvParameters);
/**
* @brief 从Flash向GD32转发固件(主流程).
* @brief 从Flash向GD32转发固件(便捷入口,内部驱动状态机循环).
* @return 操作结果码.
*/
ota_result_t forwardFirmware();
/* ---- 线程安全的状态访问接口 ----
* OTA 任务写、HTTP 任务读,通过 mutex 保证一致性。
* 状态机内部(OTA 任务)可直接写 static 成员,
* 跨任务读/写必须走这些 API。
*/
// ── 事件驱动接口 ──
/**
* @brief 获取当前中继状态.
* @return 当前中继状态.
* @brief 向状态机投递事件,驱动状态转换.
* @param event 事件类型.
* @param payload 关联数据(响应载荷等),可为nullptr.
* @param len 数据长度.
* @return 当前操作结果码(OTA_RESULT_OK 表示事件已处理且可继续).
*/
ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len);
// ── 状态查询(线程安全)──
/**
* @brief 获取当前中继状态.
*/
static relay_state_t getState();
/**
* @brief 获取中继当前序号.
* @return 当前序号值.
* @brief 获取中继当前序号.
*/
static uint32_t getRelaySeq();
/**
* @brief 获取中继总包数.
* @return 总包数.
* @brief 获取中继总包数.
*/
static uint32_t getRelayTotalPackets();
/**
* @brief 获取GD32 APP存活标志位.
* @return true 存活,false 未存活.
* @brief 获取GD32 APP存活标志位.
*/
static bool getGd32AppAlive();
/**
* @brief 获取GD32固件版本.
* @param out 输出参数,版本结构体指针.
* @brief 获取GD32固件版本.
*/
static void getGd32Version(gd32_version_t *out);
/**
* @brief 设置中继状态.
* @param s 目标状态值.
* @brief 设置中继状态.
*/
static void setState(relay_state_t s);
/**
* @brief 设置中继当前序号.
* @param seq 序号值.
* @brief 设置中继当前序号.
*/
static void setRelaySeq(uint32_t seq);
/**
* @brief 设置中继总包数.
* @param total 总包数.
* @brief 设置中继总包数.
*/
static void setRelayTotalPackets(uint32_t total);
/**
* @brief 设置GD32 APP存活标志位.
* @param alive 存活状态.
* @brief 设置GD32 APP存活标志位.
*/
static void setGd32AppAlive(bool alive);
/**
* @brief 获取GD32固件版本.
* @param ver 待设置的版本结构体引用.
* @brief 设置GD32固件版本.
*/
static void setGd32Version(const gd32_version_t &ver);
/**
* @brief 重置状态机.
* @brief 重置状态机到 IDLE.
*/
void reset();
// ── Flash 操作 ──
/**
* @brief Flash写入接口(支持非4字节对齐写入).
* @param offset 分区内写入偏移.
* @param data 待写入的数据指针.
* @param len 数据长度(字节).
* @return ESP_OK 成功,其他失败.
* @brief Flash写入接口(支持非4字节对齐写入).
*/
static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
/**
* @brief 从OTA缓存分区读取数据块.
* @param offset 分区内读取偏移.
* @param data 读取缓冲区指针.
* @param len 读取长度(字节).
* @return ESP_OK 成功,其他失败.
* @brief 从OTA缓存分区读取数据块.
*/
static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
/**
* @brief 从Flash加载元数据.
* @param meta 输出参数,加载后的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
* @brief 从Flash加载元数据.
*/
static esp_err_t metaLoad(ota_meta_t *meta);
/**
* @brief 保存元数据到Flash.
* @param meta 待保存的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
* @brief 保存元数据到Flash.
*/
static esp_err_t metaSave(const ota_meta_t *meta);
/**
* @brief 传输过程中更新元数据.
* @param stored_size 已存储的固件大小(字节).
* @param last_seq 最后成功的分块序号.
* @return ESP_OK 成功,其他失败.
* @brief 传输过程中更新元数据.
*/
static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
/**
* @brief OTA完成或中止时清除元数据.
* @return ESP_OK 成功,其他失败.
* @brief OTA完成或中止时清除元数据.
*/
static esp_err_t metaClear();
/**
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
* @param out_sha256 输出32字节SHA-256摘要.
* @return ESP_OK 成功,其他失败.
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
*/
static esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
/**
* @brief 固件接收完毕后计算SHA-256并写入元数据.
* @return ESP_OK 成功,其他失败.
* @brief 固件接收完毕后计算SHA-256并写入元数据.
*/
static esp_err_t metaFinalizeChecksum();
/**
* @brief 校验固件完整性(SHA-256).
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败.
* @brief 校验固件完整性(SHA-256).
*/
static esp_err_t metaVerifyChecksum();
// 公开状态(状态机内部直接访问;跨任务请用 getter/setter)
// ── 公开状态成员 ──
static relay_state_t relayState;
static uint32_t relaySeq;
static uint32_t relayTotalPackets;
@@ -201,33 +190,19 @@ public:
static QueueHandle_t dispatchQueue;
static QueueHandle_t resultQueue;
// 线程安全锁
// ── 线程安全锁 ──
static SemaphoreHandle_t s_stateMutex;
/**
* @brief 初始化状态互斥锁.
*/
static void initMutex();
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50);
class StateLockGuard {
public:
/**
* @brief 构造时获取互斥锁.
* @param timeout 等待超时(时钟节拍),默认50ms.
*/
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
m_locked = (s_stateMutex && xSemaphoreTake(s_stateMutex, timeout) == pdTRUE);
}
/**
* @brief 析构时释放互斥锁.
*/
~StateLockGuard() {
if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); }
}
/**
* @brief 检查是否成功获取锁.
* @return true 已锁定,false 未锁定.
*/
bool isLocked() const { return m_locked; }
StateLockGuard(const StateLockGuard&) = delete;
StateLockGuard& operator=(const StateLockGuard&) = delete;
@@ -235,7 +210,7 @@ public:
bool m_locked = false;
};
// 实例状态(public,供状态机方法访问)
// ── 实例状态 ──
const esp_partition_t *m_gd32Partition = NULL;
session_ctx_t m_sessionCtx;
uint8_t m_nonceServer[16] = {0};
@@ -245,177 +220,95 @@ public:
static constexpr size_t GD32_UID_LEN = 12;
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
/**
* @brief 请求GD32 APP进入Bootloader.
* @return 操作结果码.
*/
esp_err_t requestGd32AppEnterBootloader();
/**
* @brief 发送握手请求.
* @return 操作结果码.
*/
ota_result_t sendHandshakeReq();
/**
* @brief 发送随机数并与GD32相互认证.
* @return 操作结果码.
*/
ota_result_t sendNonceAndAuthenticate();
/**
* @brief 向GD32发送固件总大小.
* @param size 固件总字节数.
* @return 操作结果码.
*/
ota_result_t sendFirmwareSize(uint32_t size);
/**
* @brief 从Flash向GD32转发固件包.
* @param deadline_tick 截止时间戳(可选),0表示无超时.
* @return 操作结果码.
*/
ota_result_t relayPacketsFromFlash(TickType_t deadline_tick = 0);
/**
* @brief 完成OTA并发送结束指令.
* @return 操作结果码.
*/
ota_result_t finishOta();
/**
* @brief 使用AES-128-ECB加密单个16字节块.
* @param key 16字节密钥.
* @param in 16字节输入数据.
* @param out 16字节输出数据.
* @return 0 成功,-1 失败.
*/
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
/**
* @brief 记录响应载荷日志.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型.
* @param payload 载荷指针.
* @param len 载荷长度.
*/
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
/**
* @brief 循环等待直到响应不再是WAIT状态.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param timeout_ms 单次轮询超时毫秒.
* @param max_wait_rounds 最大WAIT轮次.
* @return ESP_OK 成功,其他失败.
*/
esp_err_t waitUntilNotWait(const char *stage, uint8_t *resp_type,
uint8_t *resp_payload, size_t resp_max,
size_t *resp_len, uint32_t timeout_ms,
uint32_t max_wait_rounds);
/**
* @brief 判断是否为带序号的结构化ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param expected_seq 期望的序号.
* @return true 匹配成功,false 不匹配.
*/
static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq);
/**
* @brief 解析结构化错误帧.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param seq_out 输出错误序号.
* @param err_out 输出错误码.
* @return true 解析成功,false 解析失败.
*/
static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out);
/**
* @brief 判断是否为握手回显ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是握手回显ACK,false 不是.
*/
static bool isHandshakeEchoAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为挑战应答ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是挑战应答ACK,false 不是.
*/
static bool isChallengePayloadAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为普通确认ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是确认ACK,false 不是.
*/
static bool isConfirmPlainAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为大小序号0的ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是大小序号0ACK,false 不是.
*/
static bool isSizeSeq0Ack(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为普通的结束ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是结束ACK,false 不是.
*/
static bool isFinishPlainAck(const uint8_t *payload, size_t len);
/**
* @brief 轮询直到收到预期的ACK形状.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 响应缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param poll_timeout_ms 轮询超时毫秒.
* @param wait_rounds WAIT状态最大轮询次数.
* @param maxExtraPolls 额外轮询次数.
* @param matcher 匹配回调函数.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t pollUntilExpectedAckShape(const char *stage,
uint8_t *resp_type,
uint8_t *resp_payload,
size_t resp_max,
size_t *resp_len,
uint32_t poll_timeout_ms,
uint32_t wait_rounds,
uint32_t maxExtraPolls,
bool (*matcher)(const uint8_t *, size_t));
/**
* @brief 解析并验证挑战应答载荷.
* @param resp_payload 响应载荷指针.
* @param resp_len 响应长度.
* @param out_k_session 输出会话密钥.
* @param out_uid 输出GD32 UID.
* @return true 验证通过,false 验证失败.
*/
static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
uint8_t *out_k_session, uint8_t *out_uid);
/**
* @brief 使用AES-128-ECB加密48字节数据.
* @param key 16字节密钥.
* @param in 48字节输入数据.
* @param out 48字节输出数据.
* @return true 加密成功,false 加密失败.
*/
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
/**
* @brief 使用密钥计算48字节数据的CRC32.
* @param key 16字节密钥.
* @param data48 48字节输入数据.
* @param output 输出4字节CRC32结果.
*/
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
private:
OtaRelay() = default;
~OtaRelay() = default;
OtaRelay(const OtaRelay&) = delete;
OtaRelay& operator=(const OtaRelay&) = delete;
// ── 状态机内部类型 ──
/**
* @brief 状态处理函数类型:接收事件 + 载荷,返回结果码.
* 返回 OTA_RESULT_OK 表示事件已成功处理且状态机可继续;
* 返回负值表示错误,状态机转入 RELAY_ERROR.
*/
typedef ota_result_t (OtaRelay::*StateHandler)(relay_event_t event,
const uint8_t *payload, size_t len);
struct RelayStateDef {
const char *name; // 状态名称(日志用)
StateHandler handler; // 事件处理器
};
// ── 状态表(const,编译期确定)──
static const RelayStateDef s_stateTable[];
// ── 各状态的事件处理器 ──
ota_result_t handleIdle(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleHandshaking(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleSizeNeg(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleTransferring(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleFinishing(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleDone(relay_event_t event, const uint8_t *payload, size_t len);
ota_result_t handleError(relay_event_t event, const uint8_t *payload, size_t len);
// ── 协议子步骤(由状态 handler 内部调用)──
/**
* @brief 请求GD32 APP进入Bootloader.
*/
esp_err_t requestGd32AppEnterBootloader();
/**
* @brief 执行握手请求(发送 HANDSHAKE_REQ).
*/
ota_result_t doHandshakeReq();
/**
* @brief 执行挑战认证.
*/
ota_result_t doChallengeAndAuth();
/**
* @brief 发送固件总大小.
*/
ota_result_t doSendFirmwareSize(uint32_t size);
/**
* @brief 转发下一批数据包(由 TRANSFERRING 状态循环调用).
*/
ota_result_t doRelayPackets(TickType_t deadline_tick);
/**
* @brief 发送结束指令并验证 APP 存活.
*/
ota_result_t doFinishAndVerify();
// ── 传输进度跟踪(状态机内部)──
uint32_t m_transferTotalPackets = 0;
uint32_t m_transferCurrentSeq = 0;
uint32_t m_transferFlashOffset = 0;
uint32_t m_transferRetryCount = 0;
TickType_t m_transferDeadline = 0;
// ── 加密/工具方法(不变)──
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
esp_err_t waitUntilNotWait(const char *stage, uint8_t *resp_type,
uint8_t *resp_payload, size_t resp_max,
size_t *resp_len, uint32_t timeout_ms,
uint32_t max_wait_rounds);
static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq);
static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out);
static bool isHandshakeEchoAck(const uint8_t *payload, size_t len);
static bool isChallengePayloadAck(const uint8_t *payload, size_t len);
static bool isConfirmPlainAck(const uint8_t *payload, size_t len);
static bool isSizeSeq0Ack(const uint8_t *payload, size_t len);
static bool isFinishPlainAck(const uint8_t *payload, size_t len);
static esp_err_t pollUntilExpectedAckShape(const char *stage,
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len,
uint32_t poll_timeout_ms, uint32_t wait_rounds,
uint32_t maxExtraPolls,
bool (*matcher)(const uint8_t *, size_t));
static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
uint8_t *out_k_session, uint8_t *out_uid);
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
};
#endif // OTA_RELAY_H
#endif // OTA_RELAY_H
+131 -41
View File
@@ -42,6 +42,7 @@
#include "ble_ota.h"
#include "wifi_ota.h"
#include "esp32_ota_core.h"
#include "ota_coordinator.h"
#include "ota_relay.h"
#include "health_monitor.h"
#include "monitor/debug_log.h"
@@ -59,6 +60,16 @@
#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小
#define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85 // ESP32 OTA SHA-256 不匹配
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
#define BLE_OTA_DATA_QUEUE_LEN 16
struct BleOtaDataBatch {
uint8_t *data;
size_t len;
};
static QueueHandle_t s_otaDataQueue = nullptr;
// ============================================================
// 前向声明
// ============================================================
@@ -66,6 +77,7 @@ static void ble_ota_start_advertising(void);
static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
static void sendFinishEvent(uint8_t stage, uint32_t value);
static void esp32_reorder_free();
static void ble_ota_data_worker_task(void *pvParameters);
// ============================================================
/**
@@ -100,8 +112,8 @@ static void ble_esp32_restart_task(void *pvParameters)
DEBUG_INFO(TAG, "[RESTART] 推送 FINISH_REBOOT_NOW 后 500ms 重启");
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
// 不会返回
}
/**
@@ -135,6 +147,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节", (unsigned)core.getReceived());
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
// 关键:reboot=false,让 finalize 真正返回、拿到 device_sha_hex,
// 再依次推送 SHA 通知 + FINISH_DONE + FINISH_REBOOT_NOW,
@@ -147,6 +160,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
device_sha_hex);
if (err == ESP_OK) {
OtaCoordinator::instance().notifyEsp32FinalizeComplete();
// 1) 把设备端 SHA 通过 SHA256 通知推给前端
if (device_sha_hex[0] != '\0') {
uint8_t sha_pkt[33] = {0};
@@ -168,6 +182,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
// 4) 通知全部发出后延迟 500ms 重启
DEBUG_INFO(TAG, "[FINISH] 通知已发送,500ms 后重启...");
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
// 不会返回
} else if (err == ESP_ERR_INVALID_VERSION) {
@@ -524,12 +539,13 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
*/
static esp_err_t ble_esp32_ota_begin(size_t total_size)
{
esp_err_t err = Esp32OtaCore::instance().begin(total_size);
// ★ 通过全局协调器发起 ESP32 OTA
esp_err_t err = OtaCoordinator::instance().beginEsp32Ota(total_size);
if (err == ESP_ERR_INVALID_STATE) {
// ★ OTA 锁被占用,走完整的状态机退出 + 清理锁,然后重试一次
DEBUG_WARN(TAG, "ESP32 OTA 锁被占用,强制清理状态机后重试");
// 协调器拒绝 → 尝试强制清理后重试
DEBUG_WARN(TAG, "ESP32 OTA 被协调器拒绝,强制清理状态机后重试");
Esp32OtaCore::instance().forceCleanup();
err = Esp32OtaCore::instance().begin(total_size);
err = OtaCoordinator::instance().beginEsp32Ota(total_size);
}
if (err != ESP_OK) {
return err;
@@ -645,15 +661,15 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X len=%d", buf[0], len);
if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) {
// 检查固件是否存储完毕 → 触发 GD32 OTA 中继
// 检查固件是否存储完毕 → 通过协调器触发 GD32 OTA 中继
ota_meta_t meta;
esp_err_t err = OtaRelay::metaLoad(&meta);
if (err == ESP_OK &&
meta.total_size > 0 &&
meta.stored_size >= meta.total_size) {
DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继",
DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),通过协调器触发中继",
meta.stored_size, meta.total_size);
BleOta::instance().signalRelay();
OtaCoordinator::instance().triggerGd32Relay();
} else {
DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")",
meta.stored_size, meta.total_size);
@@ -770,9 +786,36 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
// 将数据拷贝到堆上并送入工作队列,避免在 NimBLE host task 中执行
// 耗时的 flash 操作(esp_ota_begin / esp_ota_write)导致 BLE 连接断开。
if (s_otaDataQueue) {
uint8_t *heap_buf = (uint8_t *)malloc((size_t)recv_len);
if (heap_buf) {
memcpy(heap_buf, buf, (size_t)recv_len);
BleOtaDataBatch item = { heap_buf, (size_t)recv_len };
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
DEBUG_WARN(TAG, "DATA_IN 工作队列满,丢弃 %d 字节(等待客户端重传)", recv_len);
free(heap_buf);
}
} else {
DEBUG_ERR(TAG, "DATA_IN 堆内存不足 (%d 字节),丢弃", recv_len);
}
}
return 0;
}
/**
* @brief 处理一批 OTA 数据包(在工作线程上下文中调用)。
* 包含 ESP32 和 GD32 的数据接收逻辑。
* @param buf 数据缓冲区(包含一个或多个合并的包)。
* @param recv_len 数据长度。
*/
static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
{
// 循环处理 mbuf 中的所有完整数据包
size_t offset = 0;
while (offset + 7 <= (size_t)recv_len) {
while (offset + 7 <= recv_len) {
// 解析包头
uint8_t cmd = buf[offset + 0];
uint16_t seq = (uint16_t)(buf[offset + 1] | (buf[offset + 2] << 8));
@@ -790,7 +833,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
: (uint32_t)BLE_OTA_CHUNK_PAYLOAD;
size_t data_len = (size_t)expected_payload;
size_t remaining_after_header = (size_t)recv_len - data_offset;
size_t remaining_after_header = recv_len - data_offset;
// 如果缓冲区中实际可用数据不足,截断 data_len
if (remaining_after_header < expected_payload) {
@@ -813,16 +856,15 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
if (seq == 0) {
if (total_size == 0) {
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return BLE_ATT_ERR_UNLIKELY;
return;
}
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
return BLE_ATT_ERR_OTA_BUSY;
}
return BLE_ATT_ERR_UNLIKELY;
return;
}
}
@@ -830,7 +872,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
offset += packet_len;
if (offset + 7 > (size_t)recv_len) break;
if (offset + 7 > recv_len) break;
continue;
}
@@ -841,7 +883,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
(unsigned)curReceived, total_size, seq);
sendWindowAck(cmd, seq, curReceived);
offset += packet_len;
if (offset + 7 > (size_t)recv_len) break;
if (offset + 7 > recv_len) break;
continue;
}
@@ -851,24 +893,18 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
seq, esp_err_to_name(err));
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
return BLE_ATT_ERR_UNLIKELY;
return;
}
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
seq, (unsigned int)Esp32OtaCore::instance().getReceived());
// 完成判定:以实际按序写入字节数为准。
// reorder buffer 模式下,尾包可能是乱序到达被缓存的包,
// 此刻 getReceived() 尚未达到 total_size,不能据此判定完成。
// isLastPacket 仅记录"这是最后一包的形态",不单独作为完成条件。
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
bool transferComplete = (total_size > 0 &&
Esp32OtaCore::instance().getReceived() >= total_size);
// ACK 触发条件:
// - 窗口末尾包 (seq+1 是窗口整数倍)
// - 数据全部按序写完(含 reorder flush 后达到 total_size)
// - 尾包形态(兜底:即使 getReceived 不足也回一个 ACK,让客户端续传)
// ACK 触发条件
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
transferComplete ||
isLastPacket) {
@@ -879,17 +915,13 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
}
// ★ 仅当数据真正全部按序写完时才标记传输完成。
// 尾包乱序缓存但缺口未填满时,不算完成(等待客户端续传缺口包)。
if (transferComplete) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq;
// ★ ESP32 OTA 传输完成:恢复 WiFi 射频
ble_ota_restore_wifi_ps();
// 注:与 WiFi 流程对齐,传输完成后由前端发送 0xF8 + SHA-256 触发 finalize,
// 设备端不再自动创建收尾任务。
}
}
// --- GD32 OTA:首个分块时创建存储元数据 ---
@@ -900,7 +932,12 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " rounded48=%" PRIu32 " max=%zu",
total_size, rounded_size, OTA_MAX_FIRMWARE_SIZE);
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
return BLE_ATT_ERR_UNLIKELY;
return;
}
if (OtaCoordinator::instance().beginGd32Storage(total_size) != ESP_OK) {
DEBUG_ERR(TAG, "GD32 OTA 协调器拒绝存储(系统忙)");
return;
}
ota_meta_t meta = {};
@@ -911,49 +948,48 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
esp_err_t err = OtaRelay::metaSave(&meta);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "元数据保存失败");
return BLE_ATT_ERR_UNLIKELY;
OtaCoordinator::instance().abortCurrent();
return;
}
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
meta.total_size, data_len);
// ★ GD32 BLE OTA 开始:降低 WiFi 射频占用
ble_ota_boost_wifi_ps();
}
ota_meta_t meta = {};
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
return BLE_ATT_ERR_UNLIKELY;
return;
}
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
DEBUG_ERR(TAG, "GD32 分包序号不连续: seq=%u last_seq=%" PRIu32,
seq, meta.last_seq);
return BLE_ATT_ERR_UNLIKELY;
return;
}
// --- GD32 写入 Flash ---
uint32_t flash_offset = meta.stored_size;
// ★ 防重复写入保护:如果已存储字节 >= 声明大小,跳过重试引起的重复包
if (flash_offset >= meta.total_size) {
DEBUG_WARN(TAG, "GD32 OTA 已收满 (%" PRIu32 " >= %" PRIu32 "),跳过重复 seq=%u",
flash_offset, meta.total_size, seq);
sendWindowAck(cmd, seq, flash_offset);
offset += packet_len;
if (offset + 7 > (size_t)recv_len) break;
if (offset + 7 > recv_len) break;
continue;
}
if (flash_offset + (uint32_t)data_len > meta.total_size) {
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
flash_offset, data_len, meta.total_size);
return BLE_ATT_ERR_UNLIKELY;
return;
}
esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, &buf[data_offset], data_len);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
seq, flash_offset, esp_err_to_name(err));
return BLE_ATT_ERR_UNLIKELY;
return;
}
uint32_t stored_size = flash_offset + (uint32_t)data_len;
@@ -977,9 +1013,8 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
meta.stored_size >= meta.total_size) {
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
meta.stored_size, meta.total_size, seq + 1);
// ★ GD32 BLE OTA 传输完成:恢复 WiFi 射频
OtaCoordinator::instance().completeGd32Storage();
ble_ota_restore_wifi_ps();
// 计算并存储 SHA-256 校验和
esp_err_t sha_err = OtaRelay::metaFinalizeChecksum();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
@@ -993,10 +1028,38 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
offset += packet_len;
// 检查剩余数据是否足够组成一个完整的包头
if (offset + 7 > (size_t)recv_len) break;
if (offset + 7 > recv_len) break;
}
}
return 0;
/**
* @brief OTA 数据工作线程:从队列接收数据并调用处理函数。
* 独立于 NimBLE host task,避免 flash 操作阻塞 BLE 协议栈。
*/
static void ble_ota_data_worker_task(void *pvParameters)
{
(void)pvParameters;
DEBUG_INFO(TAG, "OTA 数据工作线程已启动");
while (1) {
BleOtaDataBatch item;
if (xQueueReceive(s_otaDataQueue, &item, portMAX_DELAY) == pdTRUE) {
if (!item.data || item.len == 0) {
free(item.data);
continue;
}
// 若 BLE 已断开,丢弃队列中的残留数据
if (!BleOta::instance().isConnected()) {
DEBUG_DBG(TAG, "BLE 已断开,丢弃 %zu 字节队列数据", item.len);
free(item.data);
continue;
}
ble_ota_process_data_batch(item.data, item.len);
free(item.data);
}
}
}
/*
@@ -1166,6 +1229,15 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
case BLE_GAP_EVENT_DISCONNECT:
DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d",
event->disconnect.reason);
// ★ 断开时清空数据队列,丢弃属于旧会话的残留包
if (s_otaDataQueue) {
BleOtaDataBatch stale;
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
free(stale.data);
}
}
BleOta::instance().m_bleConnected = false;
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
// ★ BLE 断开时恢复 WiFi 射频,并中止未完成的 ESP32 OTA
@@ -1479,6 +1551,24 @@ void BleOta::taskEntry(void *pvParameters)
(void)pvParameters;
DEBUG_INFO(TAG, "=== BLE OTA 任务启动 ===");
// 创建 OTA 数据队列和工作线程(在 NimBLE 初始化之前完成)
s_otaDataQueue = xQueueCreate(BLE_OTA_DATA_QUEUE_LEN, sizeof(BleOtaDataBatch));
if (s_otaDataQueue) {
BaseType_t wk_ok = xTaskCreate(ble_ota_data_worker_task,
"ble_ota_worker",
8192, // 6KB→8KB,flash 操作需要足够栈空间
NULL,
10, // 中等优先级,避免被 NimBLE host task 过度抢占
NULL);
if (wk_ok != pdPASS) {
DEBUG_ERR(TAG, "OTA 数据工作线程创建失败,BLE OTA 数据传输不可用");
vQueueDelete(s_otaDataQueue);
s_otaDataQueue = nullptr;
}
} else {
DEBUG_ERR(TAG, "OTA 数据队列创建失败,BLE OTA 数据传输不可用");
}
esp_err_t err = BleOta::instance().serverStart();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用",
+45 -60
View File
@@ -25,6 +25,7 @@
#include "ota_mutex.h"
#include "wifi_ota.h"
#include "esp32_ota_core.h"
#include "ota_coordinator.h"
#include "ota_relay.h"
#include "health_monitor.h"
#include "monitor/debug_log.h"
@@ -513,11 +514,13 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
// 若 reboot=true,core 内部直接 esp_restart(),HTTP 响应永远发不出去,
// 前端会得到 "Failed to fetch" 而错过核对结果。
DEBUG_INFO("OTA", "ESP32 OTA finalize, %u bytes...", (unsigned)core.getReceived());
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
esp_err_t err = core.finalize(has_frontend_sha ? frontend_sha_hex : nullptr,
/*reboot=*/false,
device_sha_hex);
if (err == ESP_OK) {
OtaCoordinator::instance().notifyEsp32FinalizeComplete();
// 1) 把包含设备端 SHA 的响应发回前端
char resp[320] = {0};
snprintf(resp, sizeof(resp),
@@ -530,6 +533,7 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
// 2) 响应发完后延迟 500ms 重启,给 TCP 缓冲区时间 flush 到客户端
DEBUG_INFO("OTA", "响应已发送,500ms 后重启...");
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
// 不会返回
return ESP_OK;
@@ -566,6 +570,7 @@ static esp_err_t restart_handler(httpd_req_t *req)
httpd_resp_send(req, resp, strlen(resp));
vTaskDelay(pdMS_TO_TICKS(500));
OtaCoordinator::instance().notifyEsp32Rebooting();
esp_restart();
return ESP_OK;
}
@@ -665,13 +670,13 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_FAIL;
}
Esp32OtaCore &core = Esp32OtaCore::instance();
esp_err_t err = core.begin(total_size);
// ★ 通过全局协调器发起 ESP32 OTA
esp_err_t err = OtaCoordinator::instance().beginEsp32Ota(total_size);
if (err == ESP_ERR_INVALID_STATE) {
// ★ OTA 锁被占用,走完整的状态机退出 + 清理锁,然后重试一次
DEBUG_WARN("WIFI", "ESP32 OTA 锁被占用,强制清理状态机后重试");
core.forceCleanup();
err = core.begin(total_size);
// 协调器拒绝(系统忙或通道被占用)→ 尝试强制清理后重试
DEBUG_WARN("WIFI", "ESP32 OTA 被协调器拒绝,强制清理状态机后重试");
Esp32OtaCore::instance().forceCleanup();
err = OtaCoordinator::instance().beginEsp32Ota(total_size);
}
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
@@ -682,6 +687,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_FAIL;
}
Esp32OtaCore &core = Esp32OtaCore::instance();
// 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
uint8_t *fw_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!fw_buf) {
@@ -757,6 +764,14 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
OtaRelay::instance().reset();
}
// ★ 通过全局协调器注册 GD32 存储开始
if (OtaCoordinator::instance().beginGd32Storage(gd32_valid_size) != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
return ESP_FAIL;
}
WifiOta::instance().stateLock();
WifiOta::instance().setGd32StorageDone(false);
WifiOta::instance().setRelayTriggered(false);
@@ -870,6 +885,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
ota_meta_t final_meta;
OtaRelay::metaLoad(&final_meta);
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
// ★ 通知协调器 GD32 存储完成
OtaCoordinator::instance().completeGd32Storage();
WifiOta::instance().stateLock();
WifiOta::instance().setGd32StorageDone(true);
WifiOta::instance().stateUnlock();
@@ -880,11 +897,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
}
DEBUG_INFO("WIFI", "auto-triggering relay");
// 自动触发中继
if (OtaRelay::dispatchQueue) {
uint32_t cmd = 1; // 1 = start relay
xQueueSend(OtaRelay::dispatchQueue, &cmd, 0);
}
// ★ 通过协调器触发中继
OtaCoordinator::instance().triggerGd32Relay();
}
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
@@ -911,27 +925,10 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
return ESP_FAIL;
}
relay_state_t curState = OtaRelay::getState();
WifiOta::instance().stateLock();
if (WifiOta::instance().getRelayTriggered() && curState != RELAY_IDLE &&
curState != RELAY_DONE && curState != RELAY_APP_VERIFY && curState != RELAY_ERROR) {
WifiOta::instance().stateUnlock();
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered");
return ESP_FAIL;
}
/* 上一轮中继已结束/失败/空闲,重置标志允许重新触发 */
WifiOta::instance().setRelayTriggered(false);
WifiOta::instance().setGd32StorageDone(false);
WifiOta::instance().stateUnlock();
if (!OtaRelay::dispatchQueue) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay queue not ready");
return ESP_FAIL;
}
WifiOta::instance().signalRelay();
if (!WifiOta::instance().getRelayTriggered()) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "relay trigger failed");
// ★ 通过全局协调器触发 GD32 中继
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
if (trigger_err != ESP_OK) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay trigger failed - system busy or firmware not ready");
return ESP_FAIL;
}
@@ -1192,9 +1189,10 @@ static esp_err_t system_status_handler(httpd_req_t *req)
gd32_version_t snapVer2 = {};
OtaRelay::getGd32Version(&snapVer2);
char resp[1024];
ota_system_state_t sysState = OtaCoordinator::instance().getSystemState();
char resp[1280];
int len = snprintf(resp, sizeof(resp),
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, \"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}}",
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, \"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}}",
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
(unsigned long)esp_get_free_heap_size(),
(unsigned long)esp_get_minimum_free_heap_size(),
@@ -1204,9 +1202,11 @@ static esp_err_t system_status_handler(httpd_req_t *req)
ip_str,
rssi,
connected ? "true" : "false",
OtaCoordinator::instance().getSystemStateString(),
(int)sysState,
(unsigned long)meta.total_size,
(unsigned long)meta.stored_size,
WifiOta::instance().getGd32StorageDone() ? "true" : "false",
OtaCoordinator::instance().isGd32StorageDone() ? "true" : "false",
OtaRelay::getState(),
(unsigned long)OtaRelay::getRelaySeq(),
(unsigned long)OtaRelay::getRelayTotalPackets(),
@@ -1394,35 +1394,20 @@ void WifiOta::signalRelay()
ota_meta_t meta;
memset(&meta, 0, sizeof(meta));
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) {
DEBUG_WARN("WIFI", "GD32 固件未完整存储,拒绝触发转发: stored=%lu total=%lu",
(unsigned long)meta.stored_size, (unsigned long)meta.total_size);
DEBUG_WARN("WIFI", "GD32 固件未完整存储,拒绝触发转发: stored=%" PRIu32 " total=%" PRIu32,
meta.stored_size, meta.total_size);
return;
}
relay_state_t st = OtaRelay::getState();
WifiOta::instance().stateLock();
if (WifiOta::instance().getRelayTriggered() && st != RELAY_IDLE &&
st != RELAY_DONE && st != RELAY_APP_VERIFY && st != RELAY_ERROR) {
WifiOta::instance().stateUnlock();
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", st);
return;
}
/* 上一轮中继已结束/失败/空闲,重置标志 */
WifiOta::instance().setRelayTriggered(false);
WifiOta::instance().setGd32StorageDone(false);
WifiOta::instance().setRelayTriggered(true);
WifiOta::instance().stateUnlock();
if (OtaRelay::dispatchQueue) {
uint32_t msg = 1;
BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &msg, pdMS_TO_TICKS(100));
if (ret == pdTRUE) {
DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
} else {
DEBUG_ERR("WIFI", "OTA 调度信号发送失败");
WifiOta::instance().setRelayTriggered(false);
}
// ★ 通过全局协调器触发中继
esp_err_t err = OtaCoordinator::instance().triggerGd32Relay();
if (err == ESP_OK) {
WifiOta::instance().stateLock();
WifiOta::instance().setRelayTriggered(true);
WifiOta::instance().stateUnlock();
DEBUG_INFO("WIFI", "OTA 中继已通过协调器触发");
} else {
DEBUG_ERR("WIFI", "OTA 中继队列尚未就绪");
DEBUG_ERR("WIFI", "OTA 中继触发失败: %s", esp_err_to_name(err));
WifiOta::instance().setRelayTriggered(false);
}
}