From 98b5cc1ba1f0186c3a3eede0d2bc836db80191f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Wed, 17 Jun 2026 02:18:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(ota):=20=E5=BC=95=E5=85=A5OTA=E5=8D=8F?= =?UTF-8?q?=E8=B0=83=E5=99=A8=E5=92=8C=E4=BA=8B=E4=BB=B6=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加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流程状态机图表和说明 - 补充新的控制命令和配置参数说明 --- README.md | 138 +++-- main/CMakeLists.txt | 1 + main/core/main.cpp | 5 + main/include/version.h | 4 +- main/ota/esp32_ota_core.cpp | 13 +- main/ota/ota_coordinator.cpp | 436 ++++++++++++++++ main/ota/ota_coordinator.h | 205 ++++++++ main/relay/ota_relay.cpp | 954 +++++++++++++++++------------------ main/relay/ota_relay.h | 421 ++++++---------- main/server/ble_ota.cpp | 172 +++++-- main/server/wifi_ota.cpp | 105 ++-- 11 files changed, 1535 insertions(+), 919 deletions(-) create mode 100644 main/ota/ota_coordinator.cpp create mode 100644 main/ota/ota_coordinator.h diff --git a/README.md b/README.md index c32733b..605f134 100644 --- a/README.md +++ b/README.md @@ -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 收发器) ``` -- **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`,仅在内容变化时写入以避免不必要的重编译 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 410b96a..54c3dc9 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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" diff --git a/main/core/main.cpp b/main/core/main.cpp index 75cce9a..f3149c9 100644 --- a/main/core/main.cpp +++ b/main/core/main.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); diff --git a/main/include/version.h b/main/include/version.h index 8195509..d48ae68 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -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" diff --git a/main/ota/esp32_ota_core.cpp b/main/ota/esp32_ota_core.cpp index 02d6ff7..202e8a4 100644 --- a/main/ota/esp32_ota_core.cpp +++ b/main/ota/esp32_ota_core.cpp @@ -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; diff --git a/main/ota/ota_coordinator.cpp b/main/ota/ota_coordinator.cpp new file mode 100644 index 0000000..bfaf215 --- /dev/null +++ b/main/ota/ota_coordinator.cpp @@ -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(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(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(); +} diff --git a/main/ota/ota_coordinator.h b/main/ota/ota_coordinator.h new file mode 100644 index 0000000..c6b7d1d --- /dev/null +++ b/main/ota/ota_coordinator.h @@ -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 +#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 diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 02dda3c..ad64c4f 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -1,8 +1,13 @@ -/* - * ota_relay.cpp - OTA 中继核心状态机实现 - * T2.3, T2.4, T3.1 - T3.7 +/* + * ota_relay.cpp - OTA 中继事件驱动状态机实现 * - * 所有功能封装为 OtaRelay 类方法。 + * 将原 17 状态"声明式赋值"重构为 7 状态事件驱动架构: + * - 每个协议阶段封装为一个状态 handler + * - processEvent() 根据当前状态分发事件 + * - 状态转换显式通过 setState() 完成 + * - forwardFirmware() 保留为便捷入口,内部驱动状态机循环 + * + * 协议子步骤与加密/Flash 工具方法保持不变。 */ #include #include @@ -23,6 +28,7 @@ #include "protocol.h" #include "health_monitor.h" #include "monitor/debug_log.h" +#include "ota_coordinator.h" // ★ 通知 OtaCoordinator 中继开始/完成 // ============================================================ // 静态成员定义 @@ -36,18 +42,28 @@ QueueHandle_t OtaRelay::dispatchQueue = NULL; QueueHandle_t OtaRelay::resultQueue = NULL; SemaphoreHandle_t OtaRelay::s_stateMutex = NULL; -/** - * @brief 获取OtaRelay单例引用. - * @return OtaRelay单例引用. - */ +// ============================================================ +// 状态表:将每个 relay_state_t 映射到对应的 handler +// ============================================================ +const OtaRelay::RelayStateDef OtaRelay::s_stateTable[] = { + { "IDLE", &OtaRelay::handleIdle }, + { "HANDSHAKING", &OtaRelay::handleHandshaking }, + { "SIZE_NEG", &OtaRelay::handleSizeNeg }, + { "TRANSFERRING", &OtaRelay::handleTransferring }, + { "FINISHING", &OtaRelay::handleFinishing }, + { "DONE", &OtaRelay::handleDone }, + // RELAY_ERROR = 0xFF, 索引特殊处理 +}; + +// ============================================================ +// 单例 & 线程安全 +// ============================================================ OtaRelay& OtaRelay::instance() { static OtaRelay inst; return inst; } -/** - * @brief 初始化状态互斥锁. - */ + void OtaRelay::initMutex() { if (!s_stateMutex) { @@ -55,50 +71,30 @@ void OtaRelay::initMutex() } } -/** - * @brief 获取当前中继状态. - * @return 当前中继状态. - */ relay_state_t OtaRelay::getState() { StateLockGuard lock; return relayState; } -/** - * @brief 获取中继当前序号. - * @return 当前序号值. - */ uint32_t OtaRelay::getRelaySeq() { StateLockGuard lock; return relaySeq; } -/** - * @brief 获取中继总包数. - * @return 总包数. - */ uint32_t OtaRelay::getRelayTotalPackets() { StateLockGuard lock; return relayTotalPackets; } -/** - * @brief 获取GD32 APP存活标志位. - * @return true 存活,false 未存活. - */ bool OtaRelay::getGd32AppAlive() { StateLockGuard lock; return gd32AppAlive; } -/** - * @brief 获取GD32固件版本. - * @param out 输出参数,版本结构体指针. - */ void OtaRelay::getGd32Version(gd32_version_t *out) { if (!out) return; @@ -106,69 +102,396 @@ void OtaRelay::getGd32Version(gd32_version_t *out) *out = gd32Version; } -/** - * @brief 设置中继状态. - * @param s 目标状态值. - */ void OtaRelay::setState(relay_state_t s) { StateLockGuard lock; relayState = s; + DEBUG_DBG("OTA", "状态转换: → %s", + (s < RELAY_ERROR) ? s_stateTable[s].name : "ERROR"); } -/** - * @brief 设置中继当前序号. - * @param seq 序号值. - */ void OtaRelay::setRelaySeq(uint32_t seq) { StateLockGuard lock; relaySeq = seq; } -/** - * @brief 设置中继总包数. - * @param total 总包数. - */ void OtaRelay::setRelayTotalPackets(uint32_t total) { StateLockGuard lock; relayTotalPackets = total; } -/** - * @brief 设置GD32 APP存活标志位. - * @param alive 存活状态. - */ void OtaRelay::setGd32AppAlive(bool alive) { StateLockGuard lock; gd32AppAlive = alive; } -/** - * @brief 设置GD32固件版本. - * @param ver 版本结构体引用. - */ void OtaRelay::setGd32Version(const gd32_version_t &ver) { StateLockGuard lock; gd32Version = ver; } +// ============================================================ +// 事件分发核心:processEvent() +// ============================================================ +ota_result_t OtaRelay::processEvent(relay_event_t event, const uint8_t *payload, size_t len) +{ + relay_state_t cur = relayState; + // ERROR 状态:仅接受 reset(通过 handleError 的 EVT_START → IDLE) + if (cur == RELAY_ERROR) { + return handleError(event, payload, len); + } + + // DONE 状态:仅接受 EVT_START(重新开始) + if (cur == RELAY_DONE) { + return handleDone(event, payload, len); + } + + // 查找状态表 + if (cur > RELAY_DONE) { + DEBUG_ERR("OTA", "processEvent: 未知状态 %d", (int)cur); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_GD32; + } + + StateHandler handler = s_stateTable[cur].handler; + if (!handler) { + DEBUG_ERR("OTA", "processEvent: 状态 %s 无 handler", s_stateTable[cur].name); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_GD32; + } + + DEBUG_DBG("OTA", "processEvent: state=%s event=%d", + s_stateTable[cur].name, (int)event); + + ota_result_t result = (this->*handler)(event, payload, len); + return result; +} // ============================================================ -// 内部工具函数 +// IDLE 状态 handler // ============================================================ +ota_result_t OtaRelay::handleIdle(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; -/** - * @brief 检查缓冲区是否全部为指定值. - * @param buf 缓冲区指针. - * @param len 缓冲区长度. - * @param value 待检查的值. - * @return true 全部为指定值,false 不是. - */ + if (event == RELAY_EVT_START) { + DEBUG_INFO("OTA", "=== 中继状态机启动: IDLE → HANDSHAKING ==="); + reset(); + setState(RELAY_HANDSHAKING); + return OTA_RESULT_OK; + } + + DEBUG_WARN("OTA", "IDLE 忽略事件: %d", (int)event); + return OTA_RESULT_OK; +} + +// ============================================================ +// HANDSHAKING 状态 handler(阶段1: 握手 + 挑战认证) +// ============================================================ +ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; + + if (event == RELAY_EVT_ABORT) { + DEBUG_ERR("OTA", "HANDSHAKING 收到 ABORT"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_USER_ABORT; + } + + if (event == RELAY_EVT_TIMEOUT) { + DEBUG_ERR("OTA", "HANDSHAKING 超时"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_TIMEOUT; + } + + if (event != RELAY_EVT_START && event != RELAY_EVT_RESP_OK) { + DEBUG_WARN("OTA", "HANDSHAKING 忽略事件: %d", (int)event); + return OTA_RESULT_OK; + } + + // 进入 HANDSHAKING 阶段:执行完整的握手 + 认证流程 + DEBUG_INFO("OTA", "=== 阶段1: 握手与认证开始 ==="); + + // 步骤1: 请求 GD32 进入 Bootloader + esp_err_t err = requestGd32AppEnterBootloader(); + if (err != ESP_OK) { + DEBUG_WARN("OTA", "GD32 进入 Bootloader 请求未确认,继续尝试握手"); + } + + // 步骤2: 发送握手请求 + ota_result_t res = doHandshakeReq(); + if (res != OTA_RESULT_OK) { + setState(RELAY_ERROR); + return res; + } + + // 步骤3: 挑战认证 + res = doChallengeAndAuth(); + if (res != OTA_RESULT_OK) { + setState(RELAY_ERROR); + return res; + } + + // 认证完成 → 进入大小协商 + setState(RELAY_SIZE_NEG); + DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立, → SIZE_NEG ==="); + return OTA_RESULT_OK; +} + +// ============================================================ +// SIZE_NEG 状态 handler(阶段2: 固件大小协商) +// ============================================================ +ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; + + if (event == RELAY_EVT_ABORT) { + DEBUG_ERR("OTA", "SIZE_NEG 收到 ABORT"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_USER_ABORT; + } + + if (event == RELAY_EVT_TIMEOUT) { + DEBUG_ERR("OTA", "SIZE_NEG 超时"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_TIMEOUT; + } + + // 加载元数据获取固件大小 + ota_meta_t meta; + if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) { + DEBUG_ERR("OTA", "SIZE_NEG: 元数据无效"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_STORAGE; + } + + DEBUG_INFO("OTA", "=== 阶段2: 固件大小协商 (%" PRIu32 " 字节) ===", meta.total_size); + + ota_result_t res = doSendFirmwareSize(meta.total_size); + if (res != OTA_RESULT_OK) { + setState(RELAY_ERROR); + return res; + } + + // 初始化传输进度 + m_transferTotalPackets = (meta.total_size + GD32_OTA_PACKET_SIZE - 1U) / GD32_OTA_PACKET_SIZE; + m_transferCurrentSeq = 0; + m_transferFlashOffset = 0; + m_transferRetryCount = 0; + relayTotalPackets = m_transferTotalPackets; + relaySeq = 0; + + setState(RELAY_TRANSFERRING); + DEBUG_INFO("OTA", "=== 阶段2 完成: 总包数=%" PRIu32 ", → TRANSFERRING ===", m_transferTotalPackets); + return OTA_RESULT_OK; +} + +// ============================================================ +// TRANSFERRING 状态 handler(阶段3: 数据包传输) +// ============================================================ +ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; + + if (event == RELAY_EVT_ABORT) { + DEBUG_ERR("OTA", "TRANSFERRING 收到 ABORT"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_USER_ABORT; + } + + if (event == RELAY_EVT_HEALTH_CRIT) { + DEBUG_ERR("OTA", "TRANSFERRING 健康检查危险,中止"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_HEALTH; + } + + DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets); + + ota_result_t res = doRelayPackets(m_transferDeadline); + if (res != OTA_RESULT_OK) { + setState(RELAY_ERROR); + return res; + } + + setState(RELAY_FINISHING); + DEBUG_INFO("OTA", "=== 阶段3 完成: → FINISHING ==="); + return OTA_RESULT_OK; +} + +// ============================================================ +// FINISHING 状态 handler(阶段4-5: 结束 + APP 验证) +// ============================================================ +ota_result_t OtaRelay::handleFinishing(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; + + if (event == RELAY_EVT_ABORT) { + DEBUG_ERR("OTA", "FINISHING 收到 ABORT"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_USER_ABORT; + } + + if (event == RELAY_EVT_TIMEOUT) { + DEBUG_ERR("OTA", "FINISHING 超时"); + setState(RELAY_ERROR); + return OTA_RESULT_ERR_TIMEOUT; + } + + DEBUG_INFO("OTA", "=== 阶段4-5: 结束OTA + APP验证 ==="); + + ota_result_t res = doFinishAndVerify(); + if (res != OTA_RESULT_OK) { + setState(RELAY_ERROR); + return res; + } + + setState(RELAY_DONE); + DEBUG_INFO("OTA", "=== OTA 全流程完成: → DONE ==="); + return OTA_RESULT_OK; +} + +// ============================================================ +// DONE 状态 handler +// ============================================================ +ota_result_t OtaRelay::handleDone(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; + + if (event == RELAY_EVT_START) { + // 允许从 DONE 重新开始 + DEBUG_INFO("OTA", "DONE → 重新开始"); + setState(RELAY_IDLE); + return OTA_RESULT_OK; + } + + DEBUG_DBG("OTA", "DONE 忽略事件: %d", (int)event); + return OTA_RESULT_OK; +} + +// ============================================================ +// ERROR 状态 handler +// ============================================================ +ota_result_t OtaRelay::handleError(relay_event_t event, const uint8_t *payload, size_t len) +{ + (void)payload; + (void)len; + + if (event == RELAY_EVT_START || event == RELAY_EVT_ABORT) { + DEBUG_INFO("OTA", "ERROR → 复位到 IDLE"); + reset(); + return OTA_RESULT_OK; + } + + DEBUG_DBG("OTA", "ERROR 忽略事件: %d", (int)event); + return OTA_RESULT_ERR_GD32; +} + +// ============================================================ +// forwardFirmware() - 便捷入口,驱动状态机循环 +// ============================================================ +ota_result_t OtaRelay::forwardFirmware() +{ + ota_meta_t meta; + if (OtaRelay::metaLoad(&meta) != ESP_OK) { + return OTA_RESULT_ERR_STORAGE; + } + + DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size); + + // 固件完整性校验 + { + esp_err_t verify_err = metaVerifyChecksum(); + if (verify_err != ESP_OK) { + DEBUG_ERR("OTA", "固件完整性校验失败,中止转发"); + reset(); + return OTA_RESULT_ERR_CRC; + } + } + + // 全局超时计算 + // ★ 收敛到 1 min:30.8 KB / 30 KB·s ≈ 1 s 数据 + 握手/认证/等待 ≈ 2~3 s, + // 1 min 已是数十倍安全余量。旧 10 min 会在中继异常时把协调器锁住极久。 + constexpr uint32_t kMinGlobalTimeoutMs = 20U * 1000U; // 20 s 最小超时 + constexpr uint32_t kMaxGlobalTimeoutMs = 60U * 1000U; // 60 s 硬上限 + uint32_t dynamic_ms = kMinGlobalTimeoutMs + (meta.total_size / 1024U) * 50U; // 50 ms / KB + uint32_t global_timeout_ms = (dynamic_ms > kMaxGlobalTimeoutMs) ? kMaxGlobalTimeoutMs : dynamic_ms; + TickType_t start_tick = xTaskGetTickCount(); + m_transferDeadline = start_tick + pdMS_TO_TICKS(global_timeout_ms); + + DEBUG_INFO("OTA", "全局超时: %" PRIu32 " ms, start_tick=%" PRIu32 ", deadline tick=%" PRIu32, + global_timeout_ms, (uint32_t)start_tick, (uint32_t)m_transferDeadline); + + // ── 驱动状态机 ── + // 步骤0: IDLE → HANDSHAKING(handleIdle 内部 reset() + setState(HANDSHAKING)) + ota_result_t res = processEvent(RELAY_EVT_START, nullptr, 0); + if (res != OTA_RESULT_OK) goto cleanup; + + // 步骤1: HANDSHAKING(内部完成握手+认证 → SIZE_NEG) + // ★ 注意:processEvent(START) 在 HANDSHAKING handler 内同步执行 doHandshakeReq + doChallengeAndAuth + // 期间 SPI 阻塞轮询 8~30 秒。超时检查必须放在同步 handler 执行之后。 + res = processEvent(RELAY_EVT_START, nullptr, 0); + if (res != OTA_RESULT_OK) goto cleanup; + + // 步骤2: SIZE_NEG(内部完成大小协商 → TRANSFERRING) + // ★ 此处检查在 SIZE_NEG handler 执行之前。从日志看握手刚完成(~0.6 s)就触发超限, + // 真实根因:handleSizeNeg 是同步阻塞的——它本身就会跑完整个 doSendFirmwareSize, + // 真正的"步骤2 超限"应在 handler 之后检查。修复:把"前已超限"检查全部删除, + // 改为在 processEvent 之后用 deadline 校验返回值。handler 内部已经有自己的超时。 + res = processEvent(RELAY_EVT_START, nullptr, 0); + if (res != OTA_RESULT_OK) goto cleanup; + + // 步骤3: TRANSFERRING(内部完成所有数据包 → FINISHING) + res = processEvent(RELAY_EVT_START, nullptr, 0); + if (res != OTA_RESULT_OK) goto cleanup; + + // 步骤4: FINISHING(结束OTA + APP验证 → DONE) + res = processEvent(RELAY_EVT_START, nullptr, 0); + if (res != OTA_RESULT_OK) goto cleanup; + +cleanup: + if (res != OTA_RESULT_OK) { + DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)relayState); + reset(); + } + return res; +} + +// ============================================================ +// 状态机复位 +// ============================================================ +void OtaRelay::reset() +{ + StateLockGuard lock; + relayState = RELAY_IDLE; + relaySeq = 0; + relayTotalPackets = 0; + m_transferTotalPackets = 0; + m_transferCurrentSeq = 0; + m_transferFlashOffset = 0; + m_transferRetryCount = 0; + m_transferDeadline = 0; + Protocol::sessionKeyClear(&m_sessionCtx); + memset(&m_sessionCtx, 0, sizeof(session_ctx_t)); + memset(m_kSession, 0, sizeof(m_kSession)); + memset(m_nonceServer, 0, sizeof(m_nonceServer)); + memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime)); + DEBUG_INFO("OTA", "状态机已复位 → IDLE"); +} + +// ============================================================ +// 内部工具函数(不变) +// ============================================================ static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value) { if (!buf || len == 0) return false; @@ -178,13 +501,6 @@ static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value) return true; } -/** - * @brief 使用AES-128-ECB加密单个16字节块. - * @param key 16字节密钥. - * @param in 16字节输入数据. - * @param out 16字节输出数据. - * @return 0 成功,-1 失败. - */ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out) { esp_aes_context ctx; @@ -195,13 +511,6 @@ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, return ret; } -/** - * @brief 记录响应载荷日志. - * @param stage 阶段名称字符串. - * @param resp_type 响应类型. - * @param payload 载荷指针. - * @param len 载荷长度. - */ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len) { char hex[193] = {0}; @@ -213,17 +522,6 @@ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_ n > 0 ? hex : "", len > n ? "..." : ""); } -/** - * @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 OtaRelay::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) @@ -232,7 +530,7 @@ esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type, uint32_t round = 0; while (*resp_type == SPI_OTA_RSP_WAIT && round < max_wait_rounds) { ++round; - DEBUG_WARN("OTA", "%s WAIT, poll #%u...", stage, round); + DEBUG_WARN("OTA", "%s WAIT, poll #%" PRIu32 "...", stage, round); esp_err_t err = SpiMaster::instance().otaPollResponse(resp_type, resp_payload, resp_max, resp_len, 100); if (err == ESP_ERR_TIMEOUT) { *resp_type = SPI_OTA_RSP_WAIT; continue; } if (err != ESP_OK) return err; @@ -241,13 +539,6 @@ esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type, return ESP_OK; } -/** - * @brief 判断是否为带序号的结构化ACK. - * @param payload 载荷指针. - * @param len 载荷长度. - * @param expected_seq 期望的序号. - * @return true 匹配成功,false 不匹配. - */ bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq) { if (!payload || len < 6) return false; @@ -257,14 +548,6 @@ bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32 return seq == expected_seq; } -/** - * @brief 解析结构化错误帧. - * @param payload 载荷指针. - * @param len 载荷长度. - * @param seq_out 输出错误序号. - * @param err_out 输出错误码. - * @return true 解析成功,false 解析失败. - */ bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out) { if (!payload || len < 7) return false; @@ -275,69 +558,25 @@ bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t return true; } -/** - * @brief 判断是否为握手回显ACK. - * @param payload 载荷指针. - * @param len 载荷长度. - * @return true 是握手回显ACK,false 不是. - */ bool OtaRelay::isHandshakeEchoAck(const uint8_t *payload, size_t len) { return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU; } -/** - * @brief 判断是否为挑战应答ACK. - * @param payload 载荷指针. - * @param len 载荷长度. - * @return true 是挑战应答ACK,false 不是. - */ bool OtaRelay::isChallengePayloadAck(const uint8_t *payload, size_t len) { return payload && len == GD32_CHALLENGE_RESPONSE_LEN; } -/** - * @brief 判断是否为普通确认ACK. - * @param payload 载荷指针. - * @param len 载荷长度. - * @return true 是确认ACK,false 不是. - */ bool OtaRelay::isConfirmPlainAck(const uint8_t *payload, size_t len) { return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U; } -/** - * @brief 判断是否为大小序号0的ACK. - * @param payload 载荷指针. - * @param len 载荷长度. - * @return true 是大小序号0ACK,false 不是. - */ bool OtaRelay::isSizeSeq0Ack(const uint8_t *payload, size_t len) { return isStructuredAckWithSeq(payload, len, 0U); } -/** - * @brief 判断是否为普通的结束ACK. - * @param payload 载荷指针. - * @param len 载荷长度. - * @return true 是结束ACK,false 不是. - */ bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len) { if (!payload) return false; if (len == 2U && payload[0] == 0x0AU && payload[1] == 0xACU) return true; - /* structured ACK 0xBAC0 no longer accepted as FINISH success */ return false; } -/** - * @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 成功,其他失败. - */ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage, uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, size_t *resp_len, @@ -370,14 +609,6 @@ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage, return ESP_OK; } -/** - * @brief 解析并验证挑战应答载荷. - * @param resp_payload 响应载荷指针. - * @param resp_len 响应长度. - * @param out_k_session 输出会话密钥. - * @param out_uid 输出GD32 UID. - * @return true 验证通过,false 验证失败. - */ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len, uint8_t *out_k_session, uint8_t *out_uid) { @@ -410,13 +641,6 @@ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_ return true; } -/** - * @brief 使用AES-128-ECB加密48字节数据. - * @param key 16字节密钥. - * @param in 48字节输入数据. - * @param out 48字节输出数据. - * @return true 加密成功,false 加密失败. - */ bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out) { for (int i = 0; i < 3; ++i) @@ -424,12 +648,6 @@ bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, ui return true; } -/** - * @brief 使用密钥计算48字节数据的CRC32. - * @param key 16字节密钥. - * @param data48 48字节输入数据. - * @param output 输出4字节CRC32结果. - */ void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output) { uint32_t crc = 0xFFFFFFFFU; @@ -440,11 +658,9 @@ void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, output[2] = (uint8_t)((crc >> 16) & 0xFFU); output[3] = (uint8_t)((crc >> 24) & 0xFFU); } -/** - * @brief 从 Flash 加载元数据 - * @param meta 输出参数,加载后的元数据结构体。 - * @return ESP_OK 成功,其他失败。 - */ +// ============================================================ +// Flash / NVS 操作(不变) +// ============================================================ esp_err_t OtaRelay::metaLoad(ota_meta_t *meta) { if (!meta) return ESP_ERR_INVALID_ARG; @@ -453,7 +669,6 @@ esp_err_t OtaRelay::metaLoad(ota_meta_t *meta) nvs_handle_t handle; esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READONLY, &handle); if (err != ESP_OK) { - // NVS 中没有元数据是正常状态(首次使用) return ESP_OK; } @@ -464,7 +679,6 @@ esp_err_t OtaRelay::metaLoad(ota_meta_t *meta) size_t sha_len = sizeof(meta->checksum); if (nvs_get_blob(handle, "checksum", meta->checksum, &sha_len) == ESP_OK && sha_len == sizeof(meta->checksum)) { - // checksum 已从 NVS 恢复 } else { memset(meta->checksum, 0, sizeof(meta->checksum)); } @@ -473,11 +687,6 @@ esp_err_t OtaRelay::metaLoad(ota_meta_t *meta) return ESP_OK; } -/** - * @brief 保存元数据到 Flash - * @param meta 待保存的元数据结构体。 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::metaSave(const ota_meta_t *meta) { if (!meta) return ESP_ERR_INVALID_ARG; @@ -502,12 +711,6 @@ esp_err_t OtaRelay::metaSave(const ota_meta_t *meta) return err; } -/** - * @brief 传输过程中更新元数据 - * @param stored_size 已存储的固件大小(字节)。 - * @param last_seq 最后成功的分块序号。 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::metaUpdate(uint32_t stored_size, uint32_t last_seq) { ota_meta_t meta; @@ -520,10 +723,6 @@ esp_err_t OtaRelay::metaUpdate(uint32_t stored_size, uint32_t last_seq) return OtaRelay::metaSave(&meta); } -/** - * @brief OTA 完成后或中止时清除元数据 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::metaClear() { DEBUG_INFO("OTA", "清除 OTA 元数据"); @@ -531,11 +730,6 @@ esp_err_t OtaRelay::metaClear() return OtaRelay::metaSave(&meta); } -/** - * @brief 计算 gd32_ota 分区中已存储固件的 SHA-256 - * @param out_sha256 输出 32 字节 SHA-256 摘要。 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::computeFlashSha256(uint8_t out_sha256[32]) { if (!out_sha256) return ESP_ERR_INVALID_ARG; @@ -587,10 +781,6 @@ esp_err_t OtaRelay::computeFlashSha256(uint8_t out_sha256[32]) return (ret == 0) ? ESP_OK : ESP_FAIL; } -/** - * @brief 固件接收完毕后,计算 SHA-256 并写入元数据 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::metaFinalizeChecksum() { ota_meta_t meta; @@ -617,17 +807,12 @@ esp_err_t OtaRelay::metaFinalizeChecksum() return metaSave(&meta); } -/** - * @brief 中继转发前校验固件完整性 - * @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败。 - */ esp_err_t OtaRelay::metaVerifyChecksum() { ota_meta_t meta; esp_err_t err = metaLoad(&meta); if (err != ESP_OK) return err; - // 全零 checksum 表示从未计算过(兼容旧固件) if (bytesAllValue(meta.checksum, sizeof(meta.checksum), 0x00)) { DEBUG_WARN("OTA", "metaVerify: checksum 未计算,跳过校验(旧数据兼容)"); return ESP_OK; @@ -656,13 +841,6 @@ esp_err_t OtaRelay::metaVerifyChecksum() return ESP_OK; } -/** - * @brief Flash 写入接口 - * @param offset 分区内写入偏移。 - * @param data 待写入的数据指针。 - * @param len 数据长度(字节)。 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len) { if (!instance().m_gd32Partition) { @@ -674,7 +852,6 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t } } - // --- offset 必须4字节对齐 --- if (offset % 4 != 0) { DEBUG_ERR("OTA", "flashWriteChunk offset非4对齐: offset=%" PRIu32, offset); return ESP_ERR_INVALID_ARG; @@ -682,7 +859,6 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t const esp_partition_t *part = instance().m_gd32Partition; - // --- len已对齐:直接写入 --- if (len % 4 == 0) { if (offset + len > part->size) { DEBUG_ERR("OTA", "flashWriteChunk越界: offset=%" PRIu32 " len=%zu part_size=%" PRIu32, @@ -692,7 +868,6 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t return esp_partition_write(part, offset, data, len); } - // --- len未对齐:read-modify-write --- size_t aligned_len = (len + 3) & ~(size_t)3; if (offset + aligned_len > part->size) { @@ -704,7 +879,6 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t DEBUG_WARN("OTA", "flashWriteChunk len非4对齐: len=%zu aligned_len=%zu offset=%" PRIu32, len, aligned_len, offset); - // 读回尾部padding字节(offset+len处,最多3字节) size_t pad_len = aligned_len - len; uint8_t pad[4] = {0}; esp_err_t err = esp_partition_read(part, offset + len, pad, pad_len); @@ -714,7 +888,6 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t return err; } - // 拼接:data + pad -> aligned_buf uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len); if (!aligned_buf) { DEBUG_ERR("OTA", "flashWriteChunk malloc失败: %zu字节", aligned_len); @@ -728,13 +901,6 @@ esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t return err; } -/** - * @brief 从 OTA 缓存分区读取指定偏移和长度的数据块。 - * @param offset 分区内读取偏移。 - * @param data 读取缓冲区指针。 - * @param len 读取长度(字节)。 - * @return ESP_OK 成功,其他失败。 - */ esp_err_t OtaRelay::flashReadChunk(uint32_t offset, uint8_t *data, size_t len) { if (!instance().m_gd32Partition) { @@ -749,19 +915,14 @@ esp_err_t OtaRelay::flashReadChunk(uint32_t offset, uint8_t *data, size_t len) return esp_partition_read(instance().m_gd32Partition, offset, data, len); } -/** - * @brief 阶段1 - 发起与 GD32 的握手 - * @return 操作结果码。 - */ +// ============================================================ +// 协议子步骤实现 +// ============================================================ esp_err_t OtaRelay::requestGd32AppEnterBootloader() { DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口"); esp_err_t err = SpiMaster::instance().appRequestBootloader(); if (err == ESP_OK) { - /* 收到确认:可能来自 APP(即将跳转 BOOT)或 BOOT 本身(已在 BOOT)。 - * 如果来自 APP,GD32 需要复位并重新初始化 SPI 外设,需等 ~1-2s; - * 如果已在 BOOT,SPI 已经就绪,200ms 足够。 - * 等待 1000ms 给 GD32 足够时间完成系统复位和 Bootloader 初始化。 */ DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪"); vTaskDelay(pdMS_TO_TICKS(1000)); return ESP_OK; @@ -770,15 +931,9 @@ esp_err_t OtaRelay::requestGd32AppEnterBootloader() return err; } - -/** - * @brief 阶段1 - 发送握手请求并等待确认. - * @return 操作结果码. - */ -ota_result_t OtaRelay::sendHandshakeReq() +ota_result_t OtaRelay::doHandshakeReq() { - DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ==="); - DEBUG_INFO("OTA", "发送 HANDSHAKE_REQ (0x00 0xAC)"); + DEBUG_INFO("OTA", "--- 握手请求 ---"); uint8_t payload[] = {0x00, 0xAC}; uint8_t last_resp_type = 0U; @@ -789,10 +944,6 @@ ota_result_t OtaRelay::sendHandshakeReq() constexpr uint32_t kHandshakePollTimeoutMs = 200U; constexpr uint32_t kHandshakeRetryGapMs = 100U; - /** @brief 当前 OTA 中继状态机状态。 */ - relayState = RELAY_HANDSHAKE_SENT; - DEBUG_INFO("OTA", "状态: RELAY_HANDSHAKE_SENT"); - for (uint32_t attempt = 0U; attempt < kHandshakeAttempts; ++attempt) { uint8_t resp_type = 0U; uint8_t resp_payload[64] = {0}; @@ -815,31 +966,22 @@ ota_result_t OtaRelay::sendHandshakeReq() last_resp_len = resp_len; if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK && isHandshakeEchoAck(resp_payload, resp_len)) { - DEBUG_INFO("OTA", "握手请求已确认 (attempt=%lu)", (unsigned long)(attempt + 1U)); + DEBUG_INFO("OTA", "握手请求已确认 (attempt=%" PRIu32 ")", attempt + 1U); return OTA_RESULT_OK; } } - DEBUG_WARN("OTA", "HANDSHAKE_REQ 未就绪,重试 %lu/%lu: err=%d resp_type=0x%02X resp_len=%u", - (unsigned long)(attempt + 1U), - (unsigned long)kHandshakeAttempts, - last_err, - last_resp_type, - (unsigned)last_resp_len); + DEBUG_WARN("OTA", "HANDSHAKE_REQ 未就绪,重试 %" PRIu32 "/%" PRIu32 ": err=%d resp_type=0x%02X resp_len=%u", + attempt + 1U, kHandshakeAttempts, last_err, last_resp_type, (unsigned)last_resp_len); vTaskDelay(pdMS_TO_TICKS(kHandshakeRetryGapMs)); } DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d, resp_type=0x%02X, resp_len=%zu", last_err, last_resp_type, last_resp_len); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } -/** - * @brief 发送随机数并与 GD32 相互认证 - * @return 操作结果码。 - */ -ota_result_t OtaRelay::sendNonceAndAuthenticate() +ota_result_t OtaRelay::doChallengeAndAuth() { uint8_t resp_type = 0; uint8_t resp_payload[64] = {0}; @@ -852,9 +994,7 @@ ota_result_t OtaRelay::sendNonceAndAuthenticate() Protocol::instance().scrambleGenerate(timestamp, mem_addr, rand_seed, m_nonceServer); DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---"); - DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08X", timestamp); - relayState = RELAY_CHALLENGE_SENT; - DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_SENT"); + DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08" PRIX32, timestamp); esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, m_nonceServer, 16, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000); @@ -864,17 +1004,13 @@ ota_result_t OtaRelay::sendNonceAndAuthenticate() 1000, 12, 8, OtaRelay::isChallengePayloadAck); if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isChallengePayloadAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } - relayState = RELAY_CHALLENGE_RESPOND; DEBUG_INFO("OTA", "已收到 GD32 握手数据包"); - DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_RESPOND"); if (!parseAndVerifyChallengePayload(resp_payload, resp_len, m_kSession, m_gd32UidRuntime)) { DEBUG_ERR("OTA", "CHALLENGE 解包/校验失败"); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_AUTH; } @@ -885,13 +1021,10 @@ ota_result_t OtaRelay::sendNonceAndAuthenticate() uint8_t encrypted_confirm[16] = {0}; if (aes128EcbEncryptBlockLocal(m_kSession, plaintext, encrypted_confirm) != 0) { DEBUG_ERR("OTA", "加密确认块失败"); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_AUTH; } DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY/CONFIRM ---"); - relayState = RELAY_SESSION_KEY_SENT; - DEBUG_INFO("OTA", "状态: RELAY_SESSION_KEY_SENT"); resp_len = 0; err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16, @@ -902,11 +1035,9 @@ ota_result_t OtaRelay::sendNonceAndAuthenticate() 1000, 8, 8, OtaRelay::isConfirmPlainAck); if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isConfirmPlainAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "SESSION_KEY/CONFIRM 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } - /* 诊断: 打印 confirm 响应完整内容,确认是 2B 还是意外读到了 36B challenge 响应 */ { char hex[73] = {0}; size_t n = (resp_len > 36U) ? 36U : resp_len; @@ -916,27 +1047,17 @@ ota_result_t OtaRelay::sendNonceAndAuthenticate() DEBUG_INFO("OTA", "CONFIRM 验证通过, resp_len=%zu, payload=%s", resp_len, hex); } - relayState = RELAY_AUTH_COMPLETE; - DEBUG_INFO("OTA", "状态: RELAY_AUTH_COMPLETE"); - DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ==="); - Protocol::sessionKeyInit(&m_sessionCtx, m_kSession); return OTA_RESULT_OK; } -/** - * @brief 向 GD32 发送固件总大小 - * @param size 固件总字节数。 - * @return 操作结果码。 - */ -ota_result_t OtaRelay::sendFirmwareSize(uint32_t size) +ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size) { uint8_t resp_type = 0; uint8_t resp_payload[32] = {0}; size_t resp_len = 0; - DEBUG_INFO("OTA", "=== 阶段2: 发送固件大小 ==="); - DEBUG_INFO("OTA", "固件大小: %u 字节", size); + DEBUG_INFO("OTA", "固件大小: %" PRIu32 " 字节", size); uint8_t size_payload[4]; size_payload[0] = (uint8_t)(size & 0xFF); @@ -944,9 +1065,6 @@ ota_result_t OtaRelay::sendFirmwareSize(uint32_t size) size_payload[2] = (uint8_t)((size >> 16) & 0xFF); size_payload[3] = (uint8_t)((size >> 24) & 0xFF); - relayState = RELAY_SIZE_SENT; - DEBUG_INFO("OTA", "状态: RELAY_SIZE_SENT"); - esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); logRespPayload("SEND_SIZE", resp_type, resp_payload, resp_len); @@ -956,24 +1074,16 @@ ota_result_t OtaRelay::sendFirmwareSize(uint32_t size) 2000, 300, 24, OtaRelay::isSizeSeq0Ack); if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || !isSizeSeq0Ack(resp_payload, resp_len)) { DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } - DEBUG_INFO("OTA", "大小已发送: %u 字节,等待 GD32 确认...", size); - relayState = RELAY_SIZE_ACK; - DEBUG_INFO("OTA", "状态: RELAY_SIZE_ACK"); - + DEBUG_INFO("OTA", "大小已发送: %" PRIu32 " 字节,等待 GD32 确认...", size); return OTA_RESULT_OK; } -/** - * @brief 从 Flash 向 GD32 转发存储的固件(48B/包) - * @return 操作结果码。 - */ -ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick) +ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick) { - DEBUG_INFO("OTA", "=== 阶段3: 从 Flash 转发固件 ==="); + DEBUG_INFO("OTA", "开始转发 %" PRIu32 " 包", m_transferTotalPackets); ota_meta_t meta; if (OtaRelay::metaLoad(&meta) != ESP_OK) { @@ -981,23 +1091,15 @@ ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick) return OTA_RESULT_ERR_STORAGE; } - uint32_t total_packets = (meta.total_size + GD32_OTA_PACKET_SIZE - 1U) / GD32_OTA_PACKET_SIZE; - uint32_t flash_offset = 0; uint8_t tx_buf[64] = {0}; uint8_t resp_type = 0; uint8_t resp_payload[32] = {0}; size_t resp_len = 0; ota_result_t result = OTA_RESULT_OK; - DEBUG_INFO("OTA", "总包数: %u, 每包 %u 字节", total_packets, (unsigned)GD32_OTA_PACKET_SIZE); - DEBUG_INFO("OTA", "状态: RELAY_RELAY_ACTIVE"); - relayState = RELAY_RELAY_ACTIVE; - relayTotalPackets = total_packets; - relaySeq = 0; - - for (uint32_t seq = 0; seq < total_packets; seq++) { + for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) { uint32_t retry_count = 0; - if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == total_packets - 1) { + if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == m_transferTotalPackets - 1) { health_status_t health = HealthMonitor::instance().checkBeforeOtaRelay(); if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) { DEBUG_ERR("OTA", "健康检查危险,中止 OTA"); @@ -1005,50 +1107,49 @@ ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick) } } - /* 全局超时检查:避免单包 WAIT 循环累计导致整体挂起 */ if (deadline_tick != 0 && xTaskGetTickCount() >= deadline_tick) { - DEBUG_ERR("OTA", "全局超时: 包 %u/%u 已超限,中止转发", seq, total_packets); + DEBUG_ERR("OTA", "全局超时: 包 %" PRIu32 "/%" PRIu32 " 已超限,中止转发", seq, m_transferTotalPackets); return OTA_RESULT_ERR_USER_ABORT; } uint8_t plain48[GD32_OTA_PACKET_SIZE]; memset(plain48, 0xFF, sizeof(plain48)); size_t chunk_size = GD32_OTA_PACKET_SIZE; - if (seq == total_packets - 1) { + if (seq == m_transferTotalPackets - 1) { chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE); } - esp_err_t err = OtaRelay::flashReadChunk(flash_offset, plain48, chunk_size); + esp_err_t err = OtaRelay::flashReadChunk(m_transferFlashOffset, plain48, chunk_size); if (err != ESP_OK) { - DEBUG_ERR("OTA", "Flash 读取失败, offset=%u, err=%d", flash_offset, err); + DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err); return OTA_RESULT_ERR_STORAGE; } - tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00; /* session_num 预留 */ + tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00; tx_buf[4] = (uint8_t)(seq & 0xFFU); tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU); tx_buf[6] = 0x00; tx_buf[7] = 0x00; if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) { - DEBUG_ERR("OTA", "包 %u 加密失败", seq); + DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq); return OTA_RESULT_ERR_AUTH; } - uint32_t if_end = (seq == total_packets - 1) ? 0xFFFFFFFFU : 0x00000000U; + uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U; tx_buf[56] = (uint8_t)(if_end & 0xFFU); tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU); tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU); tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU); calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]); - DEBUG_DBG("OTA", "包 %u: chunk=%u if_end=0x%08X crc32=%02X%02X%02X%02X", - seq, (unsigned)chunk_size, (unsigned)if_end, + DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X", + seq, (unsigned)chunk_size, if_end, tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]); - relayState = RELAY_PACKET_SENT; + m_transferCurrentSeq = seq; - resend_packet: + resend_packet: while (1) { resp_len = 0; err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf), @@ -1058,51 +1159,39 @@ ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick) err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10); } if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) { - DEBUG_WARN("OTA", "数据包 %u 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len); + DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len); result = OTA_RESULT_ERR_TIMEOUT; break; } - uint32_t expected_ack = (seq == total_packets - 1) ? 0xF0F0F0F0U : (seq + 1U); + uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? 0xF0F0F0F0U : (seq + 1U); - /* 跳过上一阶段残留的、seq 不匹配的结构化 ACK。 - * - * 典型场景:SIZE 阶段的 ACK(seq=0) 仍挂在 DMA 上, - * 包 0 发出后第一次 poll 读到 seq=0 而非期望的 seq=1。 - * 继续轮询直到拿到正确的 ACK 或超时。 */ if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) { uint32_t err_seq = 0; uint8_t err_code = 0; if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) { if (err_seq == seq) { - DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code); - relayState = RELAY_PACKET_ACK_RETRY; + DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code); if (++retry_count >= 3U) { - DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); + DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq); result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32; break; } - continue; /* 重发本包 */ + continue; } } - /* 非期望 ACK,也非错误帧 -> 可能是残留 ACK,循环 poll 直到拿到正确的。 - * 超时/非ACK 退出时需要重发当前包(GD32 Flash 缓冲满时会发 WAIT 后排空, - * ESP32 必须重发才能让 GD32 重新处理)。 */ bool got_expected = false; for (uint32_t retry = 0; retry < 10; ++retry) { - DEBUG_DBG("OTA", "包 %u 收到非目标ACK(seq不匹配),继续轮询(%u/5)...", seq, retry + 1); + DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1); err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000); if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) { err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5); } if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) { - /* poll 超时或收到非 ACK(WAIT/ERROR)-> GD32 Flash 缓冲可能已满, - * 正在排空。重发当前包让 GD32 重新入队处理。 */ - DEBUG_WARN("OTA", "包 %u poll 超时/非ACK,重发 (retry=%u/3)", seq, retry_count + 1); - relayState = RELAY_PACKET_ACK_RETRY; + DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1); if (++retry_count >= 3U) { - DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); + DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq); result = OTA_RESULT_ERR_GD32; break; } @@ -1113,43 +1202,35 @@ ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick) break; } if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { - break; /* 下面统一处理 */ + break; } } if (got_expected) { - /* 匹配成功 */ - flash_offset += (uint32_t)chunk_size; - relayState = RELAY_PACKET_ACK_OK; + m_transferFlashOffset += (uint32_t)chunk_size; break; } - /* 最终检查是否为错误帧 */ if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { - DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code); - relayState = RELAY_PACKET_ACK_RETRY; - if (++retry_count >=10U) { - DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); + DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code); + if (++retry_count >= 10U) { + DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq); result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32; break; } - continue; /* 重发本包 */ + continue; } - /* 轮询耗尽仍未拿到期望 ACK -> 重发当前包 */ - DEBUG_WARN("OTA", "包 %u 轮询耗尽未匹配,重发 (retry=%u/3)", seq, retry_count + 1); - relayState = RELAY_PACKET_ACK_RETRY; + DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1); if (++retry_count >= 10U) { - DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); + DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq); result = OTA_RESULT_ERR_GD32; break; } goto resend_packet; } - /* 匹配成功 */ - flash_offset += (uint32_t)chunk_size; - relayState = RELAY_PACKET_ACK_OK; + m_transferFlashOffset += (uint32_t)chunk_size; break; } @@ -1158,23 +1239,16 @@ ota_result_t OtaRelay::relayPacketsFromFlash(TickType_t deadline_tick) } relaySeq = seq + 1; - if (seq % (total_packets / 10 + 1) == 0) { - uint32_t progress = (relaySeq * 100U) / total_packets; - DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)", relaySeq, total_packets, progress); + if (seq % (m_transferTotalPackets / 10 + 1) == 0) { + uint32_t progress = (relaySeq * 100U) / m_transferTotalPackets; + DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", relaySeq, m_transferTotalPackets, progress); } } - relayState = RELAY_RECEIVE_DONE; - DEBUG_DBG("OTA", "状态: RELAY_RECEIVE_DONE"); return result; } -/** - * @brief 结束 OTA - 发送 0x0A AC 并清理 - * @return 操作结果码。 - */ -ota_result_t OtaRelay::finishOta() +ota_result_t OtaRelay::doFinishAndVerify() { - DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ==="); DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP"); uint8_t finish_payload[2] = {0x0A, 0xAC}; @@ -1182,19 +1256,16 @@ ota_result_t OtaRelay::finishOta() uint8_t resp_payload[32] = {0}; size_t resp_len = 0; - /* FINISH_OTA 重试机制:GD32 Flash 缓冲未排空时会回 WAIT, - * 需要重试直到收到正确的 ACK(0x0A 0xAC) 或超时。 */ for (uint32_t retry = 0; retry < 10; ++retry) { resp_type = 0; resp_len = 0; memset(resp_payload, 0, sizeof(resp_payload)); - relayState = RELAY_FINISH_SENT; esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len); if (err != ESP_OK) { - DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%u/10", err, retry + 1); + DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1); vTaskDelay(pdMS_TO_TICKS(100)); continue; } @@ -1203,67 +1274,50 @@ ota_result_t OtaRelay::finishOta() sizeof(resp_payload), &resp_len, 1000, 10, 8, OtaRelay::isFinishPlainAck); if (err != ESP_OK) { - DEBUG_WARN("OTA", "FINISH_OTA poll 失败, err=%d, retry=%u/10", err, retry + 1); + DEBUG_WARN("OTA", "FINISH_OTA poll 失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1); vTaskDelay(pdMS_TO_TICKS(100)); continue; } if (resp_type == SPI_OTA_RSP_ACK && isFinishPlainAck(resp_payload, resp_len)) { - /* 成功收到 FINISH_OTA ACK */ break; } - /* 检查是否为 WAIT 帧(GD32 Flash 缓冲未排空) */ if (resp_type == SPI_OTA_RSP_WAIT) { - DEBUG_INFO("OTA", "FINISH_OTA 收到 WAIT, GD32 正在排空 Flash 缓冲, retry=%u/10", retry + 1); + DEBUG_INFO("OTA", "FINISH_OTA 收到 WAIT, GD32 正在排空 Flash 缓冲, retry=%" PRIu32 "/10", retry + 1); vTaskDelay(pdMS_TO_TICKS(200)); continue; } - /* 检查是否为错误帧 */ uint32_t err_seq = 0; uint8_t err_code = 0; if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) { - DEBUG_ERR("OTA", "FINISH_OTA 收到错误帧 err=0x%02X, retry=%u/10", err_code, retry + 1); + DEBUG_ERR("OTA", "FINISH_OTA 收到错误帧 err=0x%02X, retry=%" PRIu32 "/10", err_code, retry + 1); vTaskDelay(pdMS_TO_TICKS(100)); continue; } - DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%u/10", + DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%" PRIu32 "/10", resp_type, resp_len, retry + 1); vTaskDelay(pdMS_TO_TICKS(100)); } if (resp_type != SPI_OTA_RSP_ACK || !isFinishPlainAck(resp_payload, resp_len)) { DEBUG_ERR("OTA", "FINISH_OTA 失败, err=0, resp_type=0x%02X, resp_len=%zu", resp_type, resp_len); - relayState = RELAY_ERROR; return OTA_RESULT_ERR_GD32; } - /* isFinishPlainAck already verified: only 0x0A 0xAC (len==2) passes. - * Structured error frames (0xBA 0xC0 ...) are rejected above. */ - - relayState = RELAY_DONE; Protocol::sessionKeyClear(&m_sessionCtx); memset(&m_sessionCtx, 0, sizeof(session_ctx_t)); memset(m_kSession, 0, sizeof(m_kSession)); memset(m_nonceServer, 0, sizeof(m_nonceServer)); memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime)); - DEBUG_INFO("OTA", "状态: RELAY_DONE"); DEBUG_INFO("OTA", "=== OTA 完成,等待 GD32 APP 启动 ==="); OtaRelay::metaClear(); - /* ============================================================ - * 阶段5: APP 存活验证 - * - * GD32 BOOT 处理完 FINISH_OTA 后会跳转到 APP。 - * APP 启动需要初始化外设和 SPI,大约需要 1-2 秒。 - * 我们以 500ms 间隔反复 ping APP,最多等 15 秒。 - * 收到 APP_ALIVE 回复才认为 OTA 真正成功。 - * ============================================================ */ - relayState = RELAY_APP_VERIFY; + // ★ APP 存活验证 gd32AppAlive = false; for (uint32_t attempt = 0; attempt < 30; ++attempt) { @@ -1274,14 +1328,14 @@ ota_result_t OtaRelay::finishOta() if (ping_err == ESP_OK) { gd32AppAlive = true; gd32Version = ping_ver; - DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d", - (unsigned long)uptime, + DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d", + uptime, ping_ver.app_major, ping_ver.app_minor, ping_ver.app_patch, ping_ver.app_build, ping_ver.boot_major, ping_ver.boot_minor, ping_ver.boot_patch, ping_ver.boot_build); DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ==="); break; } - DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%u/30)", attempt + 1); + DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1); } if (!gd32AppAlive) { @@ -1293,120 +1347,17 @@ ota_result_t OtaRelay::finishOta() return OTA_RESULT_OK; } -/** - * @brief 从 Flash 向 GD32 转发固件 - * @return 操作结果码。 - */ -ota_result_t OtaRelay::forwardFirmware() -{ - ota_meta_t meta; - if (OtaRelay::metaLoad(&meta) != ESP_OK) { - return OTA_RESULT_ERR_STORAGE; - } - - DEBUG_INFO("OTA", "开始固件转发: %u 字节", - meta.total_size); - - // 固件完整性校验 - { - esp_err_t verify_err = metaVerifyChecksum(); - if (verify_err != ESP_OK) { - DEBUG_ERR("OTA", "固件完整性校验失败,中止转发"); - return OTA_RESULT_ERR_CRC; - } - } - - /* 全局超时:动态计算,min = 5min, max = 10min, 按 total_size/1024*2ms 递增 */ - constexpr uint32_t kMinGlobalTimeoutMs = 5U * 60U * 1000U; - constexpr uint32_t kMaxGlobalTimeoutMs = 10U * 60U * 1000U; - uint32_t dynamic_ms = kMinGlobalTimeoutMs + (meta.total_size / 1024U) * 2U; - uint32_t global_timeout_ms = (dynamic_ms > kMaxGlobalTimeoutMs) ? kMaxGlobalTimeoutMs : dynamic_ms; - TickType_t start_tick = xTaskGetTickCount(); - TickType_t deadline_tick = start_tick + pdMS_TO_TICKS(global_timeout_ms); - - DEBUG_INFO("OTA", "全局超时: %u ms (%u 秒), deadline tick=%u", - global_timeout_ms, global_timeout_ms / 1000U, (unsigned)deadline_tick); - - requestGd32AppEnterBootloader(); - - /* 在每个阶段前检查全局超时,超时则 reset() 并返回 OTA_RESULT_ERR_USER_ABORT */ - if (xTaskGetTickCount() >= deadline_tick) { - DEBUG_ERR("OTA", "全局超时: 在 requestBootloader 阶段已超限 (%u ms),中止 OTA", global_timeout_ms); - reset(); - return OTA_RESULT_ERR_USER_ABORT; - } - - ota_result_t res = sendHandshakeReq(); - if (res != OTA_RESULT_OK) { reset(); return res; } - - if (xTaskGetTickCount() >= deadline_tick) { - DEBUG_ERR("OTA", "全局超时: 在 sendHandshakeReq 阶段已超限 (%u ms),中止 OTA", global_timeout_ms); - reset(); - return OTA_RESULT_ERR_USER_ABORT; - } - - res = sendNonceAndAuthenticate(); - if (res != OTA_RESULT_OK) { reset(); return res; } - - if (xTaskGetTickCount() >= deadline_tick) { - DEBUG_ERR("OTA", "全局超时: 在 sendNonceAndAuthenticate 阶段已超限 (%u ms),中止 OTA", global_timeout_ms); - reset(); - return OTA_RESULT_ERR_USER_ABORT; - } - - res = sendFirmwareSize(meta.total_size); - if (res != OTA_RESULT_OK) { reset(); return res; } - - if (xTaskGetTickCount() >= deadline_tick) { - DEBUG_ERR("OTA", "全局超时: 在 sendFirmwareSize 阶段已超限 (%u ms),中止 OTA", global_timeout_ms); - reset(); - return OTA_RESULT_ERR_USER_ABORT; - } - - res = relayPacketsFromFlash(deadline_tick); - if (res != OTA_RESULT_OK) { reset(); return res; } - - if (xTaskGetTickCount() >= deadline_tick) { - DEBUG_ERR("OTA", "全局超时: 在 relayPacketsFromFlash 阶段已超限 (%u ms),中止 OTA", global_timeout_ms); - reset(); - return OTA_RESULT_ERR_USER_ABORT; - } - - res = finishOta(); - - return res; -} - -/** - * @brief 重置状态机 - */ -void OtaRelay::reset() -{ - StateLockGuard lock; - /** @brief 当前 OTA 中继状态机状态。 */ - relayState = RELAY_IDLE; - relaySeq = 0; - relayTotalPackets = 0; - Protocol::sessionKeyClear(&m_sessionCtx); - memset(&m_sessionCtx, 0, sizeof(session_ctx_t)); - memset(m_kSession, 0, sizeof(m_kSession)); - memset(m_nonceServer, 0, sizeof(m_nonceServer)); - memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime)); -} - -/** - * @brief OTA 中继 FreeRTOS 任务 - * @param pvParameters FreeRTOS 任务参数,未使用。 - */ +// ============================================================ +// FreeRTOS 任务入口 +// ============================================================ void OtaRelay::taskEntry(void *pvParameters) { + (void)pvParameters; DEBUG_INFO("OTA", "OTA 中继任务启动"); initMutex(); - /** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */ dispatchQueue = xQueueCreate(4, sizeof(uint32_t)); - /** @brief OTA 结果回传队列,将执行结果返回给调用方。 */ resultQueue = xQueueCreate(4, sizeof(uint32_t)); if (!dispatchQueue || !resultQueue) { @@ -1422,7 +1373,9 @@ void OtaRelay::taskEntry(void *pvParameters) continue; } - DEBUG_INFO("OTA", "收到 OTA 调度信号: %lu,开始转发...", (unsigned long)msg); + DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg); + // ★ 通知协调器中继已开始(确保协调器状态与实际中继一致) + OtaCoordinator::instance().notifyRelayStarted(); ota_result_t result = instance().forwardFirmware(); uint32_t result_msg = (uint32_t)result; @@ -1430,7 +1383,6 @@ void OtaRelay::taskEntry(void *pvParameters) if (result == OTA_RESULT_OK) { DEBUG_INFO("OTA", "OTA 转发成功完成"); - /* 保持 RELAY_DONE 状态 3 秒,让网页轮询有机会看到完成状态 */ vTaskDelay(pdMS_TO_TICKS(3000)); } else if (result == OTA_RESULT_ERR_USER_ABORT) { DEBUG_ERR("OTA", "OTA 全局超时中止 (OTA_RESULT_ERR_USER_ABORT=%d)", result); @@ -1438,6 +1390,8 @@ void OtaRelay::taskEntry(void *pvParameters) DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result); } instance().reset(); + // ★ 通知协调器中继结束(成功→GD32_COMPLETE,失败→ERROR→IDLE) + // 避免 m_sysState 卡在 OTA_SYS_GD32_RELAYING,导致后续 beginGd32Storage() 全部被拒 + OtaCoordinator::instance().notifyRelayComplete(result == OTA_RESULT_OK); } } - diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index 45e719f..16e6c34 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -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 \ No newline at end of file +#endif // OTA_RELAY_H diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index aa93ae8..e3e2b0e 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -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 不可用", diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index a4006da..e55e0b7 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -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); } }