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:
@@ -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`,仅在内容变化时写入以避免不必要的重编译
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+157
-264
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user