feat(ota): 实现 GD32 Flash 存储抽象层和 OTA 协调器接口解耦
- 新增 storage/gd32_flash_store.cpp 组件,提供 GD32 Flash 存储抽象层 - 修改 ota_coordinator.cpp/h 使其继承 IRelayListener 接口,实现与 OtaRelay 的解耦设计 - 将 GD32 相关存储操作统一通过协调器访问,解决 H3 绕过协调器的问题 - 添加完整的 Doxygen 注释到所有公共接口和关键函数 - 更新 CMakeLists.txt 构建配置,添加 storage 组件依赖 - 重构元数据加载/清除操作使用 Gd32FlashStore 实例而非 OtaRelay 直接访问 - 添加中继事件回调接口实现,使 OtaRelay 可通知协调器状态变化
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# ESP32-C3 OTA 中继固件 — 架构分析报告
|
||||
|
||||
## 总体评价
|
||||
|
||||
项目核心设计思路**方向正确**:单例模式、协调器串行化、事件驱动状态机、ESP32 OTA 核心复用 — 这些都是优秀的嵌入式架构选择。但在职责分配和模块耦合方面存在显著问题,主要集中在 **OtaRelay 职责过载** 和 **传输层绕过协调器** 两个核心缺陷上。
|
||||
|
||||
---
|
||||
|
||||
## 高危问题 (4 个)
|
||||
|
||||
### H1. `OtaRelay` 职责严重超载(9 项职责)
|
||||
- **文件**: [ota_relay.h](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.h) — 375 行,30+ public 方法
|
||||
- **问题**: 同时承担:①事件驱动状态机 ②Flash 分区读写 ③NVS 元数据持久化 ④SHA-256 校验 ⑤AES-128-ECB 加密 ⑥CRC32 计算 ⑦协议帧解析 ⑧挑战认证 ⑨GD32 Bootloader 协商
|
||||
- **影响**: 任何存储/加密层的修改都可能波及状态机逻辑,测试和维护成本高
|
||||
|
||||
### H2. `wifi_ota.cpp` 是 God Class(1827 行,8+ 职责)
|
||||
- **文件**: [wifi_ota.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp)
|
||||
- **问题**: 同时管理 WiFi STA/AP、HTTP 服务器全部路由、GD32 固件接收+Flash 写入、状态查询 API、RSSI 采集、BLE 共存、日志 HTTP 接口
|
||||
- **影响**: 修改任何单一功能都需要理解整个文件,回归风险极高
|
||||
|
||||
### H3. 传输层绕过协调器直接访问 OtaRelay
|
||||
- **证据**:
|
||||
- `wifi_ota.cpp` 直接调用 `OtaRelay::flashWriteChunk()`、`OtaRelay::metaSave()`、`OtaRelay::metaUpdate()`
|
||||
- `ble_ota.cpp` 直接访问 `OtaRelay::resultQueue`、`OtaRelay::metaLoad()`、`OtaRelay::verifyFrontendSha()`
|
||||
- **矛盾**: [ota_coordinator.h](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.h) L52 注释明确要求"传输层不再直接调用 OtaRelay / Esp32OtaCore 的底层接口",但实际被违反
|
||||
- **影响**: 协调器的"唯一入口"设计意图落空,状态一致性无法保证
|
||||
|
||||
### H4. OtaCoordinator ↔ OtaRelay 双向紧耦合
|
||||
- **证据**: 协调器直接操作 `OtaRelay::dispatchQueue`、`getState()`、`metaLoad()`、`metaClear()`([ota_coordinator.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp) 多处);OtaRelay 回调协调器 `notifyRelayStarted/Complete()`
|
||||
- **影响**: 任一端修改都可能波及另一端,违反依赖倒置原则
|
||||
|
||||
---
|
||||
|
||||
## 中危问题 (5 个)
|
||||
|
||||
### M1. 全局互斥机制双重
|
||||
- `ota_mutex.h` 全局锁 + `OtaCoordinator` 状态锁两套并行,`Esp32OtaCore::begin()` 被两层锁保护
|
||||
- 增加死锁面和调试难度
|
||||
|
||||
### M2. HealthMonitor 向上依赖 WifiOta
|
||||
- [health_monitor.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp) `#include "server/wifi_ota.h"` — 底层监控模块依赖上层服务器模块
|
||||
- 违反层次化架构原则
|
||||
|
||||
### M3. OtaRelay 大量公开静态成员
|
||||
- `relayState`、`dispatchQueue`、`resultQueue`、`gd32AppAlive` 等 7 个 public static 成员实质是全局变量
|
||||
- 虽有 mutex 保护但破坏封装性
|
||||
|
||||
### M4. SpiMaster 职责偏多(836 行)
|
||||
- [spi_master.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp) 同时承担 SPI 总线驱动、OTA 帧协议、CANFD 通信、APP Ping 协议解析
|
||||
- CANFD 与 OTA 帧协议是不同抽象层
|
||||
|
||||
### M5. 构建系统无模块隔离
|
||||
- [main/CMakeLists.txt](file:///g:/Robot_v2/DCU/ESP32_COMM/main/CMakeLists.txt) 所有 10 个 .cpp 文件注册在单一 `idf_component_register` 中
|
||||
- 任何模块可 `#include` 任何其他模块头文件,无编译期依赖约束
|
||||
|
||||
---
|
||||
|
||||
## 低危问题 (3 个)
|
||||
|
||||
### L1. BleOta → WifiOta 横向耦合
|
||||
- BLE 直接调用 `WifiOta::instance().setBleOtaActive()` 管理射频共存,应由协调器管理
|
||||
|
||||
### L2. WiFi 密码硬编码
|
||||
- [app_config.h](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h) 明文写入源码,应使用 NVS 或 Kconfig
|
||||
|
||||
### L3. `const_cast` 绕过 const
|
||||
- [ota_coordinator.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp) `getSystemState() const` 中 `const_cast` 获取锁,`m_mutex` 应声明为 `mutable`
|
||||
|
||||
---
|
||||
|
||||
## 并发安全问题
|
||||
|
||||
| 问题 | 位置 | 风险 |
|
||||
|---|---|---|
|
||||
| `processEvent()` 读 `relayState` 未持锁 | ota_relay.cpp | 状态撕裂读 |
|
||||
| BLE `m_bleConnected`/`m_connHandle` 无保护 | ble_ota.cpp/h | 并发读写 |
|
||||
| `DebugLog::log()` 自动初始化非线程安全 | debug_log.cpp | 竞态初始化 |
|
||||
| `DebugLog::log()` 1.1KB 栈帧 | debug_log.cpp | 4096 栈任务中栈溢出高风险 |
|
||||
|
||||
---
|
||||
|
||||
## 架构亮点(值得保留)
|
||||
|
||||
- **协调器模式**:串行化 GD32/ESP32 操作的思路正确
|
||||
- **事件驱动状态机**:OtaRelay 的状态表设计清晰,7 状态 + 事件驱动模式优于线性流程
|
||||
- **看门狗自愈**:协调器 90s watchdog 自动脱离卡死状态
|
||||
- **RAII 守卫**:StateLockGuard 等模式保证锁释放
|
||||
- **ESP32 OTA 核心复用**:BLE/WiFi 共用 Esp32OtaCore,正确隔离传输层差异
|
||||
|
||||
---
|
||||
|
||||
## 改进路线图(按优先级)
|
||||
|
||||
### Task 1: 提取 GD32 Flash 存储抽象层 [解决 H1 + H3]
|
||||
- 创建 `storage/gd32_flash_store.h/.cpp`
|
||||
- 封装 `flashWriteChunk()`、`flashReadChunk()`、`metaLoad/Save/Update/Clear()`、`computeFlashSha256()`
|
||||
- 从 OtaRelay 中剥离存储职责
|
||||
|
||||
### Task 2: 协调器补齐 GD32 存储 API [解决 H3]
|
||||
- 在 OtaCoordinator 中新增 `writeGd32Chunk()`、`verifyGd32Firmware()` 等接口
|
||||
- 传输层(WiFi/BLE)只通过协调器操作,消除绕过
|
||||
|
||||
### Task 3: 拆分 wifi_ota.cpp [解决 H2]
|
||||
- HTTP 路由处理提取为 `server/http_handlers.cpp`
|
||||
- GD32 固件接收逻辑移入协调器或独立 receiver
|
||||
|
||||
### Task 4: 统一互斥机制 [解决 M1]
|
||||
- 移除 `ota_mutex.h` 全局锁,全部由 OtaCoordinator 管理
|
||||
|
||||
### Task 5: 解耦 HealthMonitor [解决 M2]
|
||||
- 提供 `setWifiConnected(bool)` 推送接口,替代主动查询 WifiOta
|
||||
|
||||
### Task 6: 修复并发安全 [解决并发问题]
|
||||
- `processEvent()` 读 `relayState` 加锁
|
||||
- BLE 状态变量加 mutex 或 atomic 保护
|
||||
- `m_mutex` 改为 `mutable`
|
||||
|
||||
---
|
||||
|
||||
## 被否决的方案
|
||||
|
||||
| 方案 | 否决原因 |
|
||||
|---|---|
|
||||
| 将 CMakeLists 拆分为多个 ESP-IDF 组件 | 嵌入式资源受限,多组件增加构建复杂度和 Flash 占用,收益有限 |
|
||||
| 拆分 SpiMaster 为多个类 | SPI 驱动+帧协议+CANFD 在嵌入式场景下紧耦合合理,强行拆分会增加调用开销 |
|
||||
| 引入接口/抽象基类 | C++ 虚函数在 ESP32-C3 上增加 Flash 和 RAM 开销,当前规模下不值得 |
|
||||
+4
-2
@@ -1,4 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
# ESP32-C3 OTA 中继项目 - 根构建文件
|
||||
# 要求 ESP-IDF v6.0+,使用 idf.py build 编译
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(ESP32_COMM)
|
||||
project(ESP32_COMM) # 项目名:ESP32 通信模块
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# ESP32-C3FH4 OTA Relay - Partition Table (4MB Flash)
|
||||
# Name, Type, SubType, Offset, Size, Label
|
||||
# NVS: 系统配置 + OTA 元数据 (24KB)
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
# OTA Data: AB 分区启动标志 (8KB)
|
||||
otadata, data, ota, 0xF000, 0x2000,
|
||||
# GD32 OTA: GD32 固件缓存区 (252KB),使用 spiffs 子类型以便 esp_partition API 直接读写
|
||||
gd32_ota, data, spiffs, 0x11000, 0x3F000,
|
||||
# APP0: ESP32 固件主分区 (1.25MB)
|
||||
app0, app, ota_0, 0x50000, 0x140000,
|
||||
# APP1: ESP32 固件备份分区 (1.25MB),支持 AB 分区回滚
|
||||
app1, app, ota_1, 0x190000, 0x140000,
|
||||
# SPIFFS: 用户数据文件系统 (1.06MB)
|
||||
spiffs, data, spiffs, 0x2D0000, 0x110000,
|
||||
|
+32
-28
@@ -1,41 +1,45 @@
|
||||
idf_component_register(
|
||||
# ESP32-C3 OTA 中继项目 - 主组件构建配置
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"core/main.cpp"
|
||||
"ota/esp32_ota_core.cpp"
|
||||
"ota/ota_coordinator.cpp"
|
||||
"relay/ota_relay.cpp"
|
||||
"transport/spi_master.cpp"
|
||||
"transport/protocol.cpp"
|
||||
"server/wifi_ota.cpp"
|
||||
"server/ble_ota.cpp"
|
||||
"monitor/health_monitor.cpp"
|
||||
"monitor/debug_log.cpp"
|
||||
"core/main.cpp" # 程序入口 (app_main)
|
||||
"ota/esp32_ota_core.cpp" # ESP32 本机 OTA 核心(分区写入/SHA验证)
|
||||
"ota/ota_coordinator.cpp" # OTA 多通道协调器(状态机/互斥)
|
||||
"relay/ota_relay.cpp" # GD32 SPI OTA 中继状态机
|
||||
"storage/gd32_flash_store.cpp" # GD32 Flash 存储抽象层(H1 提取)
|
||||
"transport/spi_master.cpp" # SPI 总线驱动 + CANFD 接口
|
||||
"transport/protocol.cpp" # 帧协议 + AES 加密 + CRC
|
||||
"server/wifi_ota.cpp" # WiFi HTTP OTA 服务器
|
||||
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
|
||||
"server/ble_ota.cpp" # BLE GATT OTA 服务器
|
||||
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存)
|
||||
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"core"
|
||||
"ota"
|
||||
"relay"
|
||||
"storage" # GD32 Flash 存储抽象层头文件
|
||||
"transport"
|
||||
"server"
|
||||
"monitor"
|
||||
"include"
|
||||
"include" # 公共头文件(app_config.h, version.h, ota_mutex.h)
|
||||
REQUIRES
|
||||
app_update
|
||||
cjson
|
||||
esp_driver_spi
|
||||
esp_driver_tsens
|
||||
esp_driver_uart
|
||||
esp_wifi
|
||||
esp_netif
|
||||
esp_event
|
||||
esp_partition
|
||||
esp_timer
|
||||
esp_http_server
|
||||
esp_blockdev
|
||||
nvs_flash
|
||||
freertos
|
||||
mbedtls
|
||||
bt
|
||||
app_update # ESP32 OTA API
|
||||
cjson # JSON 解析/生成
|
||||
esp_driver_spi # SPI 主设备驱动
|
||||
esp_driver_tsens # 温度传感器驱动
|
||||
esp_driver_uart # UART 驱动(调试日志)
|
||||
esp_wifi # WiFi STA/AP 模式
|
||||
esp_netif # 网络接口层
|
||||
esp_event # 事件循环
|
||||
esp_partition # Flash 分区操作
|
||||
esp_timer # 高精度定时器
|
||||
esp_http_server # HTTP OTA 服务器
|
||||
esp_blockdev # Flash 块设备
|
||||
nvs_flash # NVS 存储(OTA 元数据)
|
||||
freertos # RTOS 内核
|
||||
mbedtls # SHA-256 / AES 加密
|
||||
bt # NimBLE BLE 协议栈
|
||||
)
|
||||
|
||||
# 每次构建时自动刷新 version.h(BUILD_TIME / GIT_HASH)
|
||||
|
||||
+41
-41
@@ -13,9 +13,9 @@
|
||||
// ============================================================
|
||||
// 固件版本(由 version.h 自动生成,此处提供 C++ constexpr 访问)
|
||||
// ============================================================
|
||||
inline constexpr const char *FW_VERSION = FW_VERSION_STRING;
|
||||
inline constexpr const char *FW_BUILD_TIME = BUILD_TIME;
|
||||
inline constexpr const char *FW_GIT_HASH = GIT_HASH;
|
||||
inline constexpr const char *FW_VERSION = FW_VERSION_STRING; ///< 固件版本字符串(自动生成)
|
||||
inline constexpr const char *FW_BUILD_TIME = BUILD_TIME; ///< 编译时间字符串(自动生成)
|
||||
inline constexpr const char *FW_GIT_HASH = GIT_HASH; ///< Git 提交哈希(自动生成)
|
||||
|
||||
// ============================================================
|
||||
// WiFi 工作模式选择
|
||||
@@ -27,9 +27,9 @@ inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
|
||||
// ============================================================
|
||||
// WiFi STA 模式配置(连接路由器)
|
||||
// ============================================================
|
||||
inline constexpr const char* WIFI_STA_SSID = "004舰桥";
|
||||
inline constexpr const char* WIFI_STA_PASSWORD = "q13362875622";
|
||||
inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000;
|
||||
inline constexpr const char* WIFI_STA_SSID = "004舰桥"; ///< STA 模式目标 WiFi 名称
|
||||
inline constexpr const char* WIFI_STA_PASSWORD = "q13362875622"; ///< STA 模式 WiFi 密码
|
||||
inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000; ///< STA 连接超时(毫秒)
|
||||
inline constexpr uint8_t WIFI_STA_MAX_RETRY = 5; // 最大重连次数,超过后切换 AP 模式
|
||||
|
||||
// STA 静态 IP
|
||||
@@ -44,16 +44,16 @@ inline constexpr const char* WIFI_STA_STATIC_DNS = "192.168.31.1";
|
||||
// ============================================================
|
||||
// WiFi AP 模式配置(设备当热点)
|
||||
// ============================================================
|
||||
inline constexpr const char* WIFI_AP_SSID = "ESP32_OTA_RELAY";
|
||||
inline constexpr const char* WIFI_AP_PASSWORD = "12345678";
|
||||
inline constexpr uint8_t WIFI_AP_CHANNEL = 6;
|
||||
inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4;
|
||||
inline constexpr const char* WIFI_AP_SSID = "ESP32_OTA_RELAY"; ///< AP 模式热点名称
|
||||
inline constexpr const char* WIFI_AP_PASSWORD = "12345678"; ///< AP 模式热点密码
|
||||
inline constexpr uint8_t WIFI_AP_CHANNEL = 6; ///< AP 模式工作信道
|
||||
inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4; ///< AP 模式最大连接数
|
||||
|
||||
// ============================================================
|
||||
// HTTP 服务器配置
|
||||
// ============================================================
|
||||
inline constexpr uint16_t HTTP_SERVER_PORT = 80;
|
||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
|
||||
inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服务器监听端口
|
||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< HTTP 服务器任务栈大小(字节)
|
||||
|
||||
// ============================================================
|
||||
// OTA 配置
|
||||
@@ -68,40 +68,40 @@ inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送
|
||||
// ============================================================
|
||||
// 健康监控阈值
|
||||
// ============================================================
|
||||
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
|
||||
inline constexpr int32_t WIFI_RSSI_WARNING = -75;
|
||||
inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88;
|
||||
inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000;
|
||||
inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32;
|
||||
inline constexpr int32_t TEMP_WARNING = 80;
|
||||
inline constexpr int32_t TEMP_CRITICAL = 100;
|
||||
inline constexpr size_t HEAP_WARNING_MIN = 10240;
|
||||
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85; ///< WiFi 信号严重警告阈值(dBm)
|
||||
inline constexpr int32_t WIFI_RSSI_WARNING = -75; ///< WiFi 信号一般警告阈值(dBm)
|
||||
inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; ///< OTA 传输期间 WiFi 信号临界阈值
|
||||
inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; ///< RSSI 采样最大有效期(毫秒)
|
||||
inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; ///< 每 N 包执行一次健康检查
|
||||
inline constexpr int32_t TEMP_WARNING = 80; ///< 芯片温度警告阈值(℃)
|
||||
inline constexpr int32_t TEMP_CRITICAL = 100; ///< 芯片温度严重警告阈值(℃)
|
||||
inline constexpr size_t HEAP_WARNING_MIN = 10240; ///< 堆内存警告阈值(字节)
|
||||
|
||||
// ============================================================
|
||||
// 调试配置(UDP 日志)
|
||||
// ============================================================
|
||||
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1;
|
||||
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3";
|
||||
inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
|
||||
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; ///< UDP 调试日志开关(1=启用)
|
||||
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; ///< UDP 日志目标 IP
|
||||
inline constexpr uint16_t DEBUG_LOG_PORT = 6666; ///< UDP 日志目标端口
|
||||
|
||||
// ============================================================
|
||||
// BLE OTA 配置
|
||||
// ============================================================
|
||||
inline constexpr const char* BLE_DEVICE_NAME = "ESP32-OTA-RELAY";
|
||||
inline constexpr uint16_t BLE_MTU_SIZE = 512;
|
||||
inline constexpr uint16_t BLE_OTA_SERVICE_UUID = 0xFFFF;
|
||||
inline constexpr uint16_t BLE_OTA_CTRL_UUID = 0x0001;
|
||||
inline constexpr uint16_t BLE_OTA_DATA_IN_UUID = 0x0002;
|
||||
inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
|
||||
inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7;
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8;
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9; // SHA-256哈希验证命令(仅从分区读回算 SHA-256,前端比对后再决定是否触发)
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_RESTART_ESP32 = 0xFA; // 重启 ESP32 命令(配套前端 expectedReboot 自动重连)
|
||||
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00;
|
||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01;
|
||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480;
|
||||
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11;
|
||||
inline constexpr const char* BLE_DEVICE_NAME = "ESP32-OTA-RELAY"; ///< BLE 广播设备名
|
||||
inline constexpr uint16_t BLE_MTU_SIZE = 512; ///< BLE MTU 协商大小
|
||||
inline constexpr uint16_t BLE_OTA_SERVICE_UUID = 0xFFFF; ///< OTA GATT 服务 UUID
|
||||
inline constexpr uint16_t BLE_OTA_CTRL_UUID = 0x0001; ///< OTA 控制特征 UUID(写入命令)
|
||||
inline constexpr uint16_t BLE_OTA_DATA_IN_UUID = 0x0002; ///< OTA 数据输入特征 UUID(写固件包)
|
||||
inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003; ///< OTA 数据输出特征 UUID(通知/ACK)
|
||||
inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004; ///< OTA 版本信息特征 UUID(只读)
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7; ///< BLE 控制命令:触发 GD32 中继 OTA
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8; ///< BLE 控制命令:触发 ESP32 本机 OTA
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9; ///< BLE 控制命令:SHA-256 哈希验证
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_RESTART_ESP32 = 0xFA; ///< BLE 控制命令:重启 ESP32
|
||||
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; ///< BLE 数据通道:GD32 固件包标识
|
||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; ///< BLE 数据通道:ESP32 固件包标识
|
||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; ///< BLE 单包数据载荷大小(字节)
|
||||
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; ///< BLE 包头+序号+校验开销(字节)
|
||||
// Reorder buffer 窗口大小(吸收 BLE 窗口内乱序到达的包,须 >= BLE_OTA_ACK_WINDOW)
|
||||
inline constexpr size_t BLE_OTA_REORDER_WINDOW = 32;
|
||||
|
||||
@@ -118,9 +118,9 @@ inline constexpr uint8_t MASTER_KEY[] = {
|
||||
inline constexpr size_t SCRAMBLE_CONST_LEN = 16;
|
||||
|
||||
// GD32_CHIP_UID 仅保留为默认占位,运行时以握手包解出的芯片地址 UID 为准。
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_LSB = 0x1FFFF7E8U;
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MID = 0x1FFFF7ECU;
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MSB = 0x1FFFF7F0U;
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_LSB = 0x1FFFF7E8U; ///< GD32 设备序列号低字 Flash 地址
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MID = 0x1FFFF7ECU; ///< GD32 设备序列号中字 Flash 地址
|
||||
inline constexpr uint32_t DEVICE_SERIAL_ADDR_MSB = 0x1FFFF7F0U; ///< GD32 设备序列号高字 Flash 地址
|
||||
|
||||
inline constexpr uint8_t GD32_CHIP_UID[] = {
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
#include "freertos/semphr.h"
|
||||
#include <cstdint>
|
||||
|
||||
extern SemaphoreHandle_t g_esp32OtaMutex;
|
||||
extern volatile bool g_esp32OtaOccupied;
|
||||
extern SemaphoreHandle_t g_esp32OtaMutex; ///< ESP32 OTA 全局互斥锁句柄(main.cpp 中创建)
|
||||
extern volatile bool g_esp32OtaOccupied; ///< ESP32 OTA 分区占用标志(true=正在OTA)
|
||||
|
||||
// 获取全局 OTA 占用标志(带超时),成功返回 true
|
||||
// 内部: Take Mutex → 检查标志 → 若空闲则置 true → Give Mutex
|
||||
/** @brief 获取全局 OTA 占用标志(带超时),成功返回 true。 */
|
||||
/**
|
||||
* @brief 获取全局 OTA 占用标志(带超时)。
|
||||
* @param timeout_ms 等待互斥锁的超时时间(毫秒)。
|
||||
* @return true 成功获取并置占用标志,false 已占用或超时。
|
||||
*/
|
||||
static inline bool esp32_ota_mutex_acquire(uint32_t timeout_ms) {
|
||||
if (!g_esp32OtaMutex) return false;
|
||||
if (xSemaphoreTake(g_esp32OtaMutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) return false;
|
||||
@@ -27,9 +29,10 @@ static inline bool esp32_ota_mutex_acquire(uint32_t timeout_ms) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 释放全局 OTA 占用标志
|
||||
// 内部: Take Mutex → 清除标志 → Give Mutex
|
||||
/** @brief 释放全局 OTA 占用标志。 */
|
||||
/**
|
||||
* @brief 释放全局 OTA 占用标志。
|
||||
* @note 内部: Take Mutex → 清除标志 → Give Mutex。
|
||||
*/
|
||||
static inline void esp32_ota_mutex_release(void) {
|
||||
if (!g_esp32OtaMutex) return;
|
||||
xSemaphoreTake(g_esp32OtaMutex, portMAX_DELAY);
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 29
|
||||
#define FW_VERSION_STRING "0.3.29"
|
||||
#define FW_VERSION_PATCH 30
|
||||
#define FW_VERSION_STRING "0.3.30"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-06-19 23:45:14"
|
||||
#define BUILD_TIME "2026-06-21 23:33:01"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "59c729d"
|
||||
#define GIT_HASH "3fff959"
|
||||
|
||||
@@ -108,6 +108,11 @@ bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将环形缓冲区所有日志导出为 JSON 数组字符串。
|
||||
* @param out_len 输出 JSON 字符串长度。
|
||||
* @return 动态分配的 JSON 字符串,调用者须 free()。
|
||||
*/
|
||||
char* DebugLog::ringDrainJson(int *out_len)
|
||||
{
|
||||
if (!out_len) return NULL;
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
// 调试日志配置(DEBUG_LOG_IP/PORT/ENABLED 已在 app_config.h 中定义)
|
||||
|
||||
// UART0 调试配置
|
||||
#define UART_DEBUG_TX_GPIO 21
|
||||
#define UART_DEBUG_RX_GPIO 20
|
||||
#define UART_DEBUG_BAUD_RATE 115200
|
||||
#define UART_DEBUG_TX_GPIO 21 ///< UART0 TX GPIO 引脚号
|
||||
#define UART_DEBUG_RX_GPIO 20 ///< UART0 RX GPIO 引脚号
|
||||
#define UART_DEBUG_BAUD_RATE 115200 ///< UART0 波特率
|
||||
|
||||
// 日志级别
|
||||
typedef enum {
|
||||
@@ -74,6 +74,12 @@ public:
|
||||
|
||||
/** @brief 弹出一条最旧日志并从环形缓冲区移除。 */
|
||||
bool ringPopOldest(char *out, size_t out_size, int *out_len);
|
||||
|
||||
/**
|
||||
* @brief 将环形缓冲区所有日志导出为 JSON 数组字符串。
|
||||
* @param out_len 输出 JSON 字符串长度。
|
||||
* @return 动态分配的 JSON 字符串,调用者须 free()。
|
||||
*/
|
||||
char* ringDrainJson(int *out_len);
|
||||
|
||||
/** @brief 当前日志级别(运行时可改)。 */
|
||||
@@ -103,9 +109,9 @@ private:
|
||||
};
|
||||
|
||||
// 便捷宏 — 直接调用单例方法
|
||||
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__) ///< 错误级别日志宏
|
||||
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__) ///< 警告级别日志宏
|
||||
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__) ///< 信息级别日志宏
|
||||
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__) ///< 调试级别日志宏
|
||||
|
||||
#endif // DEBUG_LOG_H
|
||||
|
||||
@@ -20,16 +20,17 @@ typedef enum {
|
||||
SYS_SAFE_SHUTDOWN = 3 // 安全关机
|
||||
} health_state_t;
|
||||
|
||||
/** @brief 健康监控模块内部状态码,用于函数返回值。 */
|
||||
typedef enum {
|
||||
HEALTH_MON_OK = 0,
|
||||
HEALTH_MON_WARNING = 1,
|
||||
HEALTH_MON_CRITICAL = 2,
|
||||
HEALTH_MON_SHUTDOWN = 3
|
||||
HEALTH_MON_OK = 0, ///< 健康检查通过
|
||||
HEALTH_MON_WARNING = 1, ///< 健康检查警告(可继续)
|
||||
HEALTH_MON_CRITICAL = 2, ///< 健康检查危险(建议中止)
|
||||
HEALTH_MON_SHUTDOWN = 3 ///< 需要安全关机
|
||||
} health_status_t;
|
||||
|
||||
// WiFi RSSI 阈值(来自 app_config.h)
|
||||
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
|
||||
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
|
||||
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING ///< WiFi 信号正常阈值(别名)
|
||||
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL ///< WiFi 信号弱阈值(别名)
|
||||
|
||||
/**
|
||||
* @brief 系统健康监控类(单例)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "ota_coordinator.h"
|
||||
#include "esp32_ota_core.h"
|
||||
#include "relay/ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -102,10 +103,13 @@ void OtaCoordinator::init()
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0;
|
||||
|
||||
// ★ H4 解耦:注册协调器为 OtaRelay 的中继事件监听器
|
||||
OtaRelay::setRelayListener(this);
|
||||
|
||||
// ★ 重启恢复:如果 NVS 里有完整固件元数据,开机时立即进入 STORED 状态,
|
||||
// 否则用户重启后看到的状态是 IDLE 但 NVS 里其实有未中继的固件,前后不一致。
|
||||
ota_meta_t boot_meta;
|
||||
if (OtaRelay::metaLoad(&boot_meta) == ESP_OK &&
|
||||
if (Gd32FlashStore::instance().metaLoad(&boot_meta) == ESP_OK &&
|
||||
boot_meta.total_size > 0 &&
|
||||
boot_meta.stored_size >= boot_meta.total_size) {
|
||||
m_sysState = OTA_SYS_GD32_STORED;
|
||||
@@ -140,7 +144,7 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size)
|
||||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),先清理再开始新存储
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) {
|
||||
DEBUG_INFO(TAG, "清理旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||||
OtaRelay::metaClear();
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
}
|
||||
|
||||
if (h) {
|
||||
@@ -167,6 +171,50 @@ void OtaCoordinator::completeGd32Storage()
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::saveGd32Meta(const ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaSave(meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::loadGd32Meta(ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaLoad(meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::clearGd32Meta()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaClear();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32])
|
||||
{
|
||||
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::finalizeGd32Checksum()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaFinalizeChecksum();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::eraseGd32Partition(uint32_t size)
|
||||
{
|
||||
return Gd32FlashStore::instance().erasePartition(size);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GD32 中继阶段
|
||||
// ============================================================
|
||||
@@ -200,7 +248,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
|
||||
|
||||
// 验证固件是否确实已存储(通过 NVS 元数据)
|
||||
ota_meta_t meta;
|
||||
if (h && (OtaRelay::metaLoad(&meta) != ESP_OK ||
|
||||
if (h && (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK ||
|
||||
meta.total_size == 0 ||
|
||||
meta.stored_size < meta.total_size)) {
|
||||
DEBUG_WARN(TAG, "GD32 固件未完整存储,拒绝触发中继");
|
||||
@@ -247,6 +295,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:GD32 中继已开始,更新系统状态为 GD32_RELAYING。 */
|
||||
void OtaCoordinator::notifyRelayStarted()
|
||||
{
|
||||
bool h = lock();
|
||||
@@ -258,6 +307,7 @@ void OtaCoordinator::notifyRelayStarted()
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:GD32 中继已完成,根据结果转为 COMPLETE 或 ERROR。 */
|
||||
void OtaCoordinator::notifyRelayComplete(bool success)
|
||||
{
|
||||
bool h = lock();
|
||||
@@ -276,6 +326,21 @@ void OtaCoordinator::notifyRelayComplete(bool success)
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)
|
||||
// ============================================================
|
||||
/** @brief IRelayListener 回调:中继已开始,委托给 notifyRelayStarted()。 */
|
||||
void OtaCoordinator::onRelayStarted()
|
||||
{
|
||||
notifyRelayStarted();
|
||||
}
|
||||
|
||||
/** @brief IRelayListener 回调:中继已完成,委托给 notifyRelayComplete()。 */
|
||||
void OtaCoordinator::onRelayComplete(bool success)
|
||||
{
|
||||
notifyRelayComplete(success);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ESP32 OTA 阶段
|
||||
// ============================================================
|
||||
@@ -342,6 +407,7 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:ESP32 固件接收完毕,状态转为 FINALIZING。 */
|
||||
void OtaCoordinator::notifyEsp32ReceiveComplete()
|
||||
{
|
||||
bool h = lock();
|
||||
@@ -352,6 +418,7 @@ void OtaCoordinator::notifyEsp32ReceiveComplete()
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:ESP32 OTA 收尾完成(SHA验证+标记启动),状态转为 REBOOTING。 */
|
||||
void OtaCoordinator::notifyEsp32FinalizeComplete()
|
||||
{
|
||||
bool h = lock();
|
||||
@@ -362,6 +429,7 @@ void OtaCoordinator::notifyEsp32FinalizeComplete()
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
/** @brief 通知协调器:ESP32 即将重启。 */
|
||||
void OtaCoordinator::notifyEsp32Rebooting()
|
||||
{
|
||||
bool h = lock();
|
||||
@@ -399,6 +467,11 @@ void OtaCoordinator::restartEsp32()
|
||||
// 不会返回
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将 ESP32 OTA finalize 错误码分类。
|
||||
* @param err finalize 返回的 esp_err_t。
|
||||
* @return 0=成功,1=SHA不匹配,2=其他失败。
|
||||
*/
|
||||
int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)
|
||||
{
|
||||
if (err == ESP_OK) return 0; // 成功
|
||||
@@ -416,7 +489,7 @@ void OtaCoordinator::abortCurrent()
|
||||
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState);
|
||||
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
OtaRelay::metaClear();
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
OtaRelay::instance().reset();
|
||||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
@@ -445,6 +518,7 @@ ota_system_state_t OtaCoordinator::getSystemState() const
|
||||
return s;
|
||||
}
|
||||
|
||||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||||
bool OtaCoordinator::isIdle() const
|
||||
{
|
||||
return (getSystemState() == OTA_SYS_IDLE ||
|
||||
@@ -452,6 +526,7 @@ bool OtaCoordinator::isIdle() const
|
||||
getSystemState() == OTA_SYS_GD32_COMPLETE);
|
||||
}
|
||||
|
||||
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
|
||||
bool OtaCoordinator::isGd32Busy() const
|
||||
{
|
||||
ota_system_state_t s = getSystemState();
|
||||
@@ -459,6 +534,7 @@ bool OtaCoordinator::isGd32Busy() const
|
||||
s == OTA_SYS_GD32_RELAYING);
|
||||
}
|
||||
|
||||
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
|
||||
bool OtaCoordinator::isEsp32Busy() const
|
||||
{
|
||||
ota_system_state_t s = getSystemState();
|
||||
@@ -467,6 +543,7 @@ bool OtaCoordinator::isEsp32Busy() const
|
||||
s == OTA_SYS_ESP32_REBOOTING);
|
||||
}
|
||||
|
||||
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。 */
|
||||
const char* OtaCoordinator::getSystemStateString() const
|
||||
{
|
||||
switch (getSystemState()) {
|
||||
@@ -483,11 +560,13 @@ const char* OtaCoordinator::getSystemStateString() const
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
|
||||
bool OtaCoordinator::getGd32AppAlive() const
|
||||
{
|
||||
return OtaRelay::getGd32AppAlive();
|
||||
}
|
||||
|
||||
/** @brief 获取 GD32 固件是否已存储完成(线程安全)。 */
|
||||
bool OtaCoordinator::isGd32StorageDone() const
|
||||
{
|
||||
OtaCoordinator* self = const_cast<OtaCoordinator*>(this);
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_err.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "relay/ota_relay.h" // IRelayListener 接口定义(H4 解耦)
|
||||
|
||||
// ============================================================
|
||||
// 系统级 OTA 状态枚举
|
||||
@@ -51,10 +53,11 @@ typedef struct {
|
||||
* 传输层(WiFi/BLE)通过本协调器发起所有 OTA 操作,
|
||||
* 不再直接调用 OtaRelay / Esp32OtaCore 的底层接口。
|
||||
*/
|
||||
class OtaCoordinator {
|
||||
class OtaCoordinator : public IRelayListener {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取 OtaCoordinator 单例实例。
|
||||
* @return OtaCoordinator 单例引用。
|
||||
*/
|
||||
static OtaCoordinator& instance();
|
||||
|
||||
@@ -80,6 +83,67 @@ public:
|
||||
*/
|
||||
void completeGd32Storage();
|
||||
|
||||
// ── GD32 存储操作(传输层通过协调器操作 Flash,解决 H3 绕过问题)──
|
||||
/**
|
||||
* @brief 向 GD32 Flash 分区写入固件数据块。
|
||||
* 传输层(WiFi/BLE)通过此接口写入,不再直接调用 Gd32FlashStore。
|
||||
* @param offset 写入偏移。
|
||||
* @param data 数据缓冲区。
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 保存 GD32 OTA 元数据。
|
||||
* @param meta 元数据结构体。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t saveGd32Meta(const ota_meta_t *meta);
|
||||
|
||||
/**
|
||||
* @brief 加载 GD32 OTA 元数据。
|
||||
* @param meta 输出元数据结构体。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t loadGd32Meta(ota_meta_t *meta);
|
||||
|
||||
/**
|
||||
* @brief 更新 GD32 OTA 元数据的进度。
|
||||
* @param stored_size 已存储大小。
|
||||
* @param last_seq 最后包序号。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq);
|
||||
|
||||
/**
|
||||
* @brief 清除 GD32 OTA 元数据。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t clearGd32Meta();
|
||||
|
||||
/**
|
||||
* @brief 校验前端 SHA-256 与设备端固件是否一致。
|
||||
* @param frontend_sha32 前端 32 字节 SHA。
|
||||
* @param out_device_sha 输出设备端 32 字节 SHA(可为 nullptr)。
|
||||
* @return ESP_OK 匹配,ESP_ERR_INVALID_CRC 不匹配。
|
||||
*/
|
||||
esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32]);
|
||||
|
||||
/**
|
||||
* @brief 计算 GD32 固件 SHA-256 并写入元数据。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t finalizeGd32Checksum();
|
||||
|
||||
/**
|
||||
* @brief 擦除 GD32 OTA 分区。
|
||||
* @param size 擦除大小。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t eraseGd32Partition(uint32_t size);
|
||||
|
||||
// ── GD32 中继阶段 ──
|
||||
/**
|
||||
* @brief 触发 GD32 中继转发。
|
||||
@@ -161,37 +225,44 @@ public:
|
||||
// ── 状态查询 ──
|
||||
/**
|
||||
* @brief 获取当前系统级 OTA 状态。
|
||||
* @return 当前 ota_system_state_t 状态值。
|
||||
*/
|
||||
ota_system_state_t getSystemState() const;
|
||||
|
||||
/**
|
||||
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
|
||||
* @return true 系统空闲(IDLE/STORED/COMPLETE)。
|
||||
*/
|
||||
bool isIdle() const;
|
||||
|
||||
/**
|
||||
* @brief 检查是否有 GD32 相关操作正在进行。
|
||||
* @return true 表示 STORING 或 RELAYING。
|
||||
*/
|
||||
bool isGd32Busy() const;
|
||||
|
||||
/**
|
||||
* @brief 检查是否有 ESP32 OTA 正在进行。
|
||||
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
|
||||
*/
|
||||
bool isEsp32Busy() const;
|
||||
|
||||
/**
|
||||
* @brief 获取系统状态的可读字符串描述。
|
||||
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
|
||||
*/
|
||||
const char* getSystemStateString() const;
|
||||
|
||||
// ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)──
|
||||
/**
|
||||
* @brief 获取 GD32 APP 存活状态。
|
||||
* @return true 表示 GD32 APP 存活。
|
||||
*/
|
||||
bool getGd32AppAlive() const;
|
||||
|
||||
/**
|
||||
* @brief GD32 固件存储完成标志(供状态查询)。
|
||||
* @return true 表示固件已完整存储。
|
||||
*/
|
||||
bool isGd32StorageDone() const;
|
||||
|
||||
@@ -205,10 +276,25 @@ public:
|
||||
/**
|
||||
* @brief 把 finalize 返回的 esp_err_t 映射为前端可读的事件码。
|
||||
* 统一 BLE/WiFi 两端语义,便于日志/状态一致。
|
||||
* @param err finalize 返回的 esp_err_t。
|
||||
* @return 0=成功;1=SHA 不匹配;2=其他失败。
|
||||
*/
|
||||
static int esp32FinalizeErrCategory(esp_err_t err);
|
||||
|
||||
// ── IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)──
|
||||
/**
|
||||
* @brief IRelayListener 回调:中继已开始。
|
||||
* 由 OtaRelay task 在 forwardFirmware() 前通过 s_relayListener 调用。
|
||||
*/
|
||||
void onRelayStarted() override;
|
||||
|
||||
/**
|
||||
* @brief IRelayListener 回调:中继已完成。
|
||||
* 由 OtaRelay task 在 forwardFirmware() 返回后通过 s_relayListener 调用。
|
||||
* @param success true 成功,false 失败。
|
||||
*/
|
||||
void onRelayComplete(bool success) override;
|
||||
|
||||
private:
|
||||
OtaCoordinator() = default;
|
||||
~OtaCoordinator() = default;
|
||||
|
||||
+121
-309
@@ -16,19 +16,16 @@
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_partition.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#include "aes/esp_aes.h"
|
||||
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
|
||||
#include "mbedtls/private/sha256.h"
|
||||
|
||||
#include "ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "spi_master.h"
|
||||
#include "protocol.h"
|
||||
#include "health_monitor.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "ota_coordinator.h" // ★ 通知 OtaCoordinator 中继开始/完成
|
||||
// ★ H4 解耦:OtaRelay 不再直接 #include "ota_coordinator.h",
|
||||
// 通过 IRelayListener 接口回调通知协调器,依赖方向变为单向。
|
||||
|
||||
// ============================================================
|
||||
// 静态成员定义
|
||||
@@ -42,6 +39,18 @@ QueueHandle_t OtaRelay::dispatchQueue = NULL;
|
||||
QueueHandle_t OtaRelay::resultQueue = NULL;
|
||||
SemaphoreHandle_t OtaRelay::s_stateMutex = NULL;
|
||||
TaskHandle_t OtaRelay::taskHandle = NULL;
|
||||
IRelayListener *OtaRelay::s_relayListener = nullptr;
|
||||
|
||||
// ── 中继事件监听器(解决 H4 双向耦合)──
|
||||
void OtaRelay::setRelayListener(IRelayListener *listener)
|
||||
{
|
||||
s_relayListener = listener;
|
||||
}
|
||||
|
||||
IRelayListener* OtaRelay::getRelayListener()
|
||||
{
|
||||
return s_relayListener;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 状态表:将每个 relay_state_t 映射到对应的 handler
|
||||
@@ -273,7 +282,7 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
|
||||
// 加载元数据获取固件大小
|
||||
ota_meta_t meta;
|
||||
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG: 元数据无效");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
@@ -409,7 +418,7 @@ ota_result_t OtaRelay::handleError(relay_event_t event, const uint8_t *payload,
|
||||
ota_result_t OtaRelay::forwardFirmware()
|
||||
{
|
||||
ota_meta_t meta;
|
||||
if (OtaRelay::metaLoad(&meta) != ESP_OK) {
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK) {
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
@@ -417,7 +426,7 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
|
||||
// 固件完整性校验
|
||||
{
|
||||
esp_err_t verify_err = metaVerifyChecksum();
|
||||
esp_err_t verify_err = Gd32FlashStore::instance().metaVerifyChecksum();
|
||||
if (verify_err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "固件完整性校验失败,中止转发");
|
||||
reset();
|
||||
@@ -492,6 +501,13 @@ void OtaRelay::reset()
|
||||
// ============================================================
|
||||
// 内部工具函数(不变)
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 检查缓冲区中所有字节是否为指定值。
|
||||
* @param buf 数据缓冲区。
|
||||
* @param len 缓冲区长度。
|
||||
* @param value 目标字节值。
|
||||
* @return true 所有字节均为指定值,false 否则。
|
||||
*/
|
||||
static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value)
|
||||
{
|
||||
if (!buf || len == 0) return false;
|
||||
@@ -501,6 +517,13 @@ 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;
|
||||
@@ -511,6 +534,13 @@ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in,
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 记录 GD32 SPI 响应的十六进制负载(调试用)。
|
||||
* @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};
|
||||
@@ -522,6 +552,17 @@ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_
|
||||
n > 0 ? hex : "<empty>", len > n ? "..." : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 循环轮询 SPI 直到响应不再是 WAIT 类型,超过最大轮询次数后放弃。
|
||||
* @param stage 当前阶段名称(日志用)。
|
||||
* @param resp_type 输入/输出响应类型(初始为 SPI_OTA_RSP_WAIT)。
|
||||
* @param resp_payload 输出响应负载缓冲区。
|
||||
* @param resp_max 响应负载缓冲区最大长度。
|
||||
* @param resp_len 输出实际响应长度。
|
||||
* @param timeout_ms 单次 SPI 轮询超时(毫秒)。
|
||||
* @param max_wait_rounds 最大 WAIT 轮询次数。
|
||||
* @return ESP_OK 成功,ESP_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
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)
|
||||
@@ -539,6 +580,13 @@ esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type,
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查响应负载是否为带指定序号的结构化 ACK(0xBA 0xC0 + seq32)。
|
||||
* @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;
|
||||
@@ -548,6 +596,14 @@ bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32
|
||||
return seq == expected_seq;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析结构化错误响应(0xBA 0xC0 + seq32 + err_code)。
|
||||
* @param payload 响应负载数据。
|
||||
* @param len 负载长度。
|
||||
* @param seq_out 输出序号(可为 nullptr)。
|
||||
* @param err_out 输出错误码(可为 nullptr)。
|
||||
* @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;
|
||||
@@ -558,18 +614,23 @@ bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief 检查响应是否为握手回显 ACK(0x00 0xAC)。 */
|
||||
bool OtaRelay::isHandshakeEchoAck(const uint8_t *payload, size_t len)
|
||||
{ return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU; }
|
||||
|
||||
/** @brief 检查响应是否为挑战认证 ACK(36字节挑战响应)。 */
|
||||
bool OtaRelay::isChallengePayloadAck(const uint8_t *payload, size_t len)
|
||||
{ return payload && len == GD32_CHALLENGE_RESPONSE_LEN; }
|
||||
|
||||
/** @brief 检查响应是否为确认明文 ACK(0xBA 0xC0)。 */
|
||||
bool OtaRelay::isConfirmPlainAck(const uint8_t *payload, size_t len)
|
||||
{ return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U; }
|
||||
|
||||
/** @brief 检查响应是否为固件大小 seq=0 的结构化 ACK。 */
|
||||
bool OtaRelay::isSizeSeq0Ack(const uint8_t *payload, size_t len)
|
||||
{ return isStructuredAckWithSeq(payload, len, 0U); }
|
||||
|
||||
/** @brief 检查响应是否为结束指令 ACK(0x0A 0xAC)。 */
|
||||
bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len)
|
||||
{
|
||||
if (!payload) return false;
|
||||
@@ -577,6 +638,19 @@ bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 轮询 SPI 直到匹配指定的 ACK 形状,处理 WAIT 和过期 ACK。
|
||||
* @param stage 当前阶段名称。
|
||||
* @param resp_type 输入/输出响应类型。
|
||||
* @param resp_payload 输入/输出响应负载。
|
||||
* @param resp_max 响应负载缓冲区最大长度。
|
||||
* @param resp_len 输入/输出实际响应长度。
|
||||
* @param poll_timeout_ms 单次 SPI 轮询超时。
|
||||
* @param wait_rounds 最大 WAIT 轮询次数。
|
||||
* @param maxExtraPolls 最大额外轮询次数。
|
||||
* @param matcher ACK 形状匹配函数。
|
||||
* @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,
|
||||
@@ -609,6 +683,14 @@ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage,
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析挑战响应负载,验证 nonce、派生会话密钥和 UID。
|
||||
* @param resp_payload GD32 响应负载(36字节)。
|
||||
* @param resp_len 响应负载长度。
|
||||
* @param out_k_session 输出 16 字节会话密钥。
|
||||
* @param out_uid 输出 12 字节 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)
|
||||
{
|
||||
@@ -641,6 +723,13 @@ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief AES-128-ECB 加密 48 字节(分 3 个 16 字节块)。
|
||||
* @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)
|
||||
@@ -648,6 +737,12 @@ bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, ui
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 使用 CRC32(IEEE 多项式 0xEDB88320)对 key+data48 计算 4 字节校验值。
|
||||
* @param key 16 字节密钥(混入 CRC)。
|
||||
* @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;
|
||||
@@ -658,301 +753,10 @@ 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);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Flash / NVS 操作(不变)
|
||||
// ============================================================
|
||||
esp_err_t OtaRelay::metaLoad(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint32_t val = 0;
|
||||
if (nvs_get_u32(handle, "total_size", &val) == ESP_OK) meta->total_size = val;
|
||||
if (nvs_get_u32(handle, "stored_size", &val) == ESP_OK) meta->stored_size = val;
|
||||
if (nvs_get_u32(handle, "last_seq", &val) == ESP_OK) meta->last_seq = val;
|
||||
|
||||
size_t sha_len = sizeof(meta->checksum);
|
||||
if (nvs_get_blob(handle, "checksum", meta->checksum, &sha_len) == ESP_OK && sha_len == sizeof(meta->checksum)) {
|
||||
} else {
|
||||
memset(meta->checksum, 0, sizeof(meta->checksum));
|
||||
}
|
||||
|
||||
nvs_close(handle);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::metaSave(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "无法打开 NVS 写入: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
nvs_set_u32(handle, "total_size", meta->total_size);
|
||||
nvs_set_u32(handle, "stored_size", meta->stored_size);
|
||||
nvs_set_u32(handle, "last_seq", meta->last_seq);
|
||||
nvs_set_blob(handle, "checksum", meta->checksum, sizeof(meta->checksum));
|
||||
err = nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "NVS commit 失败: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::metaUpdate(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = OtaRelay::metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
|
||||
return OtaRelay::metaSave(&meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::metaClear()
|
||||
{
|
||||
DEBUG_INFO("OTA", "清除 OTA 元数据");
|
||||
ota_meta_t meta = {};
|
||||
return OtaRelay::metaSave(&meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::computeFlashSha256(uint8_t out_sha256[32])
|
||||
{
|
||||
if (!out_sha256) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR("OTA", "computeSha256: 元数据无效");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
if (mbedtls_sha256_starts(&ctx, 0) != 0) {
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
static constexpr size_t kChunkSize = 1024;
|
||||
uint8_t *buf = (uint8_t *)malloc(kChunkSize);
|
||||
if (!buf) {
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
uint32_t remaining = meta.total_size;
|
||||
uint32_t offset = 0;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = (remaining > kChunkSize) ? kChunkSize : remaining;
|
||||
err = flashReadChunk(offset, buf, to_read);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "computeSha256: Flash 读取失败 offset=%" PRIu32, offset);
|
||||
free(buf);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return err;
|
||||
}
|
||||
if (mbedtls_sha256_update(&ctx, buf, to_read) != 0) {
|
||||
free(buf);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
offset += to_read;
|
||||
remaining -= to_read;
|
||||
}
|
||||
|
||||
free(buf);
|
||||
int ret = mbedtls_sha256_finish(&ctx, out_sha256);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return (ret == 0) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::metaFinalizeChecksum()
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
if (meta.total_size == 0 || meta.stored_size < meta.total_size) {
|
||||
DEBUG_ERR("OTA", "metaFinalize: 固件不完整 stored=%" PRIu32 " total=%" PRIu32,
|
||||
meta.stored_size, meta.total_size);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
err = computeFlashSha256(meta.checksum);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "metaFinalize: SHA-256 计算失败");
|
||||
return err;
|
||||
}
|
||||
|
||||
char hex[65] = {0};
|
||||
for (int i = 0; i < 32; ++i)
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", meta.checksum[i]);
|
||||
DEBUG_INFO("OTA", "固件 SHA-256: %s", hex);
|
||||
|
||||
return metaSave(&meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::metaVerifyChecksum()
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
if (bytesAllValue(meta.checksum, sizeof(meta.checksum), 0x00)) {
|
||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,跳过校验(旧数据兼容)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t computed[32] = {0};
|
||||
err = computeFlashSha256(computed);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "metaVerify: SHA-256 计算失败");
|
||||
return err;
|
||||
}
|
||||
|
||||
if (memcmp(computed, meta.checksum, 32) != 0) {
|
||||
char hex_stored[65] = {0}, hex_computed[65] = {0};
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&hex_stored[i * 2], sizeof(hex_stored) - i * 2, "%02X", meta.checksum[i]);
|
||||
snprintf(&hex_computed[i * 2], sizeof(hex_computed) - i * 2, "%02X", computed[i]);
|
||||
}
|
||||
DEBUG_ERR("OTA", "固件完整性校验失败!");
|
||||
DEBUG_ERR("OTA", " 存储 SHA-256: %s", hex_stored);
|
||||
DEBUG_ERR("OTA", " 计算 SHA-256: %s", hex_computed);
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "固件完整性校验通过 (SHA-256 匹配)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::verifyFrontendSha(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32])
|
||||
{
|
||||
if (!frontend_sha32) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
// 1) 计算设备端 SHA 并写入 meta.checksum(metaFinalizeChecksum 内部已校验固件完整性)
|
||||
esp_err_t err = metaFinalizeChecksum();
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// 2) 读回 meta 拿到 checksum,回传给调用方
|
||||
ota_meta_t meta;
|
||||
err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
if (out_device_sha) memcpy(out_device_sha, meta.checksum, 32);
|
||||
|
||||
// 3) 比对前端 SHA 与设备端 SHA
|
||||
if (memcmp(meta.checksum, frontend_sha32, 32) != 0) {
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!instance().m_gd32Partition) {
|
||||
instance().m_gd32Partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
|
||||
if (!instance().m_gd32Partition) {
|
||||
DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
const esp_partition_t *part = instance().m_gd32Partition;
|
||||
|
||||
// ★ 计算 head/tail padding,使写入地址和长度都对齐到 4 字节
|
||||
// WiFi HTTP chunk 边界不一定 4 对齐,flash 写入必须对齐。
|
||||
size_t head_pad = offset % 4;
|
||||
uint32_t aligned_offset = offset - head_pad;
|
||||
size_t total = head_pad + len;
|
||||
size_t aligned_len = (total + 3) & ~(size_t)3;
|
||||
|
||||
if (aligned_offset + aligned_len > part->size) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk越界: offset=%" PRIu32 " len=%zu aligned_off=%" PRIu32 " aligned_len=%zu part=%" PRIu32,
|
||||
offset, len, aligned_offset, aligned_len, part->size);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
// 快速路径:offset 和 len 都对齐
|
||||
if (head_pad == 0 && len % 4 == 0) {
|
||||
return esp_partition_write(part, offset, data, len);
|
||||
}
|
||||
|
||||
if (head_pad > 0 || len % 4 != 0) {
|
||||
DEBUG_WARN("OTA", "flashWriteChunk 非对齐: offset=%" PRIu32 " len=%zu head=%zu aligned_len=%zu",
|
||||
offset, len, head_pad, aligned_len);
|
||||
}
|
||||
|
||||
uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len);
|
||||
if (!aligned_buf) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk malloc失败: %zu字节", aligned_len);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// 读回 head padding(offset 之前的字节,属于上一次写入的尾部)
|
||||
if (head_pad > 0) {
|
||||
esp_err_t err = esp_partition_read(part, aligned_offset, aligned_buf, head_pad);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk读回head失败: offset=%" PRIu32 " head=%zu err=%s",
|
||||
aligned_offset, head_pad, esp_err_to_name(err));
|
||||
free(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
// 拷贝实际数据
|
||||
memcpy(aligned_buf + head_pad, data, len);
|
||||
|
||||
// 读回 tail padding(数据末尾到 4 字节边界的字节)
|
||||
size_t tail_pad = aligned_len - total;
|
||||
if (tail_pad > 0) {
|
||||
esp_err_t err = esp_partition_read(part, aligned_offset + total, aligned_buf + total, tail_pad);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk读回tail失败: offset=%" PRIu32 " tail=%zu err=%s",
|
||||
aligned_offset + total, tail_pad, esp_err_to_name(err));
|
||||
free(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err = esp_partition_write(part, aligned_offset, aligned_buf, aligned_len);
|
||||
free(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t OtaRelay::flashReadChunk(uint32_t offset, uint8_t *data, size_t len)
|
||||
{
|
||||
if (!instance().m_gd32Partition) {
|
||||
instance().m_gd32Partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
|
||||
if (!instance().m_gd32Partition) {
|
||||
DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
return esp_partition_read(instance().m_gd32Partition, offset, data, len);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 协议子步骤实现
|
||||
// ============================================================
|
||||
/** @brief 协议子步骤:请求 GD32 APP 进入 Bootloader OTA 模式。 */
|
||||
esp_err_t OtaRelay::requestGd32AppEnterBootloader()
|
||||
{
|
||||
DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口");
|
||||
@@ -966,6 +770,7 @@ esp_err_t OtaRelay::requestGd32AppEnterBootloader()
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送握手请求(0x00 0xAC)并等待 GD32 回显 ACK。 */
|
||||
ota_result_t OtaRelay::doHandshakeReq()
|
||||
{
|
||||
DEBUG_INFO("OTA", "--- 握手请求 ---");
|
||||
@@ -1016,6 +821,7 @@ ota_result_t OtaRelay::doHandshakeReq()
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:执行挑战认证(发送 nonce、验证响应、派生会话密钥、发送确认)。 */
|
||||
ota_result_t OtaRelay::doChallengeAndAuth()
|
||||
{
|
||||
uint8_t resp_type = 0;
|
||||
@@ -1086,6 +892,7 @@ ota_result_t OtaRelay::doChallengeAndAuth()
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送固件总大小给 GD32 并等待 seq=0 的确认。 */
|
||||
ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
||||
{
|
||||
uint8_t resp_type = 0;
|
||||
@@ -1116,12 +923,17 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 协议子步骤:逐包转发固件数据给 GD32。
|
||||
* 流程:从 Flash 读取 → AES 加密 → SPI 发送 → 等待 ACK → 失败则重发。
|
||||
* 每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包执行一次健康检查。
|
||||
*/
|
||||
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
{
|
||||
DEBUG_INFO("OTA", "开始转发 %" PRIu32 " 包", m_transferTotalPackets);
|
||||
|
||||
ota_meta_t meta;
|
||||
if (OtaRelay::metaLoad(&meta) != ESP_OK) {
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "元数据加载失败");
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
@@ -1154,7 +966,7 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
|
||||
}
|
||||
|
||||
esp_err_t err = OtaRelay::flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
@@ -1297,6 +1109,7 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA 指令并验证 GD32 APP 存活。 */
|
||||
ota_result_t OtaRelay::doFinishAndVerify()
|
||||
{
|
||||
DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP");
|
||||
@@ -1365,7 +1178,7 @@ ota_result_t OtaRelay::doFinishAndVerify()
|
||||
|
||||
DEBUG_INFO("OTA", "=== OTA 完成,等待 GD32 APP 启动 ===");
|
||||
|
||||
OtaRelay::metaClear();
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
|
||||
// ★ APP 存活验证
|
||||
gd32AppAlive = false;
|
||||
@@ -1425,8 +1238,8 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||
// ★ 通知协调器中继已开始(确保协调器状态与实际中继一致)
|
||||
OtaCoordinator::instance().notifyRelayStarted();
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
|
||||
if (s_relayListener) s_relayListener->onRelayStarted();
|
||||
ota_result_t result = instance().forwardFirmware();
|
||||
|
||||
uint32_t result_msg = (uint32_t)result;
|
||||
@@ -1441,8 +1254,7 @@ 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);
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
|
||||
if (s_relayListener) s_relayListener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
}
|
||||
}
|
||||
|
||||
+77
-76
@@ -22,17 +22,9 @@
|
||||
#include "freertos/task.h"
|
||||
#include "transport/protocol.h"
|
||||
#include "transport/spi_master.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "esp_partition.h"
|
||||
|
||||
#define OTA_META_NVS_NAMESPACE "ota_meta"
|
||||
|
||||
typedef struct {
|
||||
uint32_t total_size;
|
||||
uint32_t stored_size;
|
||||
uint32_t last_seq;
|
||||
uint8_t checksum[32];
|
||||
} ota_meta_t;
|
||||
|
||||
// ============================================================
|
||||
// 阶段状态枚举(精简为 7 个状态,原 17 个"微状态"合并为阶段)
|
||||
// ============================================================
|
||||
@@ -58,26 +50,49 @@ typedef enum {
|
||||
RELAY_EVT_ABORT // 外部中止
|
||||
} relay_event_t;
|
||||
|
||||
/** @brief OTA 中继操作结果码枚举。 */
|
||||
typedef enum {
|
||||
OTA_RESULT_OK = 0,
|
||||
OTA_RESULT_ERR_TIMEOUT = -1,
|
||||
OTA_RESULT_ERR_CRC = -2,
|
||||
OTA_RESULT_ERR_AUTH = -3,
|
||||
OTA_RESULT_ERR_STORAGE = -4,
|
||||
OTA_RESULT_ERR_GD32 = -5,
|
||||
OTA_RESULT_ERR_HEALTH = -6,
|
||||
OTA_RESULT_ERR_USER_ABORT = -7
|
||||
OTA_RESULT_OK = 0, ///< 操作成功
|
||||
OTA_RESULT_ERR_TIMEOUT = -1, ///< 等待 GD32 响应超时
|
||||
OTA_RESULT_ERR_CRC = -2, ///< 固件 CRC/SHA-256 校验失败
|
||||
OTA_RESULT_ERR_AUTH = -3, ///< 挑战认证失败
|
||||
OTA_RESULT_ERR_STORAGE = -4, ///< Flash 存储错误
|
||||
OTA_RESULT_ERR_GD32 = -5, ///< GD32 返回错误
|
||||
OTA_RESULT_ERR_HEALTH = -6, ///< 健康检查未通过
|
||||
OTA_RESULT_ERR_USER_ABORT = -7 ///< 用户主动中止
|
||||
} ota_result_t;
|
||||
|
||||
// ============================================================
|
||||
// IRelayListener - 中继事件监听接口(解决 H4 双向耦合)
|
||||
// ============================================================
|
||||
// OtaCoordinator 实现此接口并注册到 OtaRelay,
|
||||
// OtaRelay 通过接口回调通知协调器,不再直接 #include ota_coordinator.h。
|
||||
// 依赖方向变为单向:OtaCoordinator → OtaRelay(通过接口)。
|
||||
/**
|
||||
* @brief 中继事件监听器接口。
|
||||
* OtaCoordinator 实现此接口,OtaRelay 通过回调通知状态变更,
|
||||
* 消除 OtaRelay → OtaCoordinator 的直接依赖(解决 H4 双向耦合)。
|
||||
*/
|
||||
class IRelayListener {
|
||||
public:
|
||||
virtual ~IRelayListener() = default;
|
||||
/** @brief 中继已开始(forwardFirmware 被调用)。 */
|
||||
virtual void onRelayStarted() = 0;
|
||||
/** @brief 中继已完成。 @param success true=成功,false=失败。 */
|
||||
virtual void onRelayComplete(bool success) = 0;
|
||||
};
|
||||
|
||||
class OtaRelay {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取OtaRelay单例引用.
|
||||
* @return OtaRelay 单例引用。
|
||||
*/
|
||||
static OtaRelay& instance();
|
||||
|
||||
/**
|
||||
* @brief OTA中继FreeRTOS任务入口.
|
||||
* @param pvParameters FreeRTOS 任务参数(未使用)。
|
||||
*/
|
||||
static void taskEntry(void *pvParameters);
|
||||
|
||||
@@ -100,46 +115,57 @@ public:
|
||||
// ── 状态查询(线程安全)──
|
||||
/**
|
||||
* @brief 获取当前中继状态.
|
||||
* @return 当前 relay_state_t 状态值。
|
||||
*/
|
||||
static relay_state_t getState();
|
||||
/**
|
||||
* @brief 获取中继当前序号.
|
||||
* @return 当前中继序号。
|
||||
*/
|
||||
static uint32_t getRelaySeq();
|
||||
/**
|
||||
* @brief 获取中继总包数.
|
||||
* @return 本次中继的总包数。
|
||||
*/
|
||||
static uint32_t getRelayTotalPackets();
|
||||
/**
|
||||
* @brief 获取GD32 APP存活标志位.
|
||||
* @return true 表示 GD32 APP 存活。
|
||||
*/
|
||||
static bool getGd32AppAlive();
|
||||
/**
|
||||
* @brief 获取GD32固件版本.
|
||||
* @param out 输出版本信息结构体指针。
|
||||
*/
|
||||
static void getGd32Version(gd32_version_t *out);
|
||||
/**
|
||||
* @brief 设置中继状态.
|
||||
* @param s 目标状态。
|
||||
*/
|
||||
static void setState(relay_state_t s);
|
||||
/**
|
||||
* @brief 设置中继当前序号.
|
||||
* @param seq 序号值。
|
||||
*/
|
||||
static void setRelaySeq(uint32_t seq);
|
||||
/**
|
||||
* @brief 设置中继总包数.
|
||||
* @param total 总包数。
|
||||
*/
|
||||
static void setRelayTotalPackets(uint32_t total);
|
||||
/**
|
||||
* @brief 设置GD32 APP存活标志位.
|
||||
* @param alive true 表示存活。
|
||||
*/
|
||||
static void setGd32AppAlive(bool alive);
|
||||
/**
|
||||
* @brief 设置GD32固件版本.
|
||||
* @param ver 版本信息结构体引用。
|
||||
*/
|
||||
static void setGd32Version(const gd32_version_t &ver);
|
||||
/**
|
||||
* @brief 获取 OTA 中继任务句柄。
|
||||
* @return FreeRTOS 任务句柄。
|
||||
*/
|
||||
static TaskHandle_t getTaskHandle();
|
||||
|
||||
@@ -148,69 +174,37 @@ public:
|
||||
*/
|
||||
void reset();
|
||||
|
||||
// ── Flash 操作 ──
|
||||
// ── 中继事件监听器(解决 H4 双向耦合)──
|
||||
/**
|
||||
* @brief Flash写入接口(支持非4字节对齐写入).
|
||||
* @brief 注册中继事件监听器(由 OtaCoordinator 在 init 时调用)。
|
||||
* @param listener 监听器指针,传 nullptr 取消注册。
|
||||
*/
|
||||
static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
static void setRelayListener(IRelayListener *listener);
|
||||
/**
|
||||
* @brief 从OTA缓存分区读取数据块.
|
||||
* @brief 获取当前注册的监听器。
|
||||
* @return 监听器指针,未注册时为 nullptr。
|
||||
*/
|
||||
static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
|
||||
/**
|
||||
* @brief 从Flash加载元数据.
|
||||
*/
|
||||
static esp_err_t metaLoad(ota_meta_t *meta);
|
||||
/**
|
||||
* @brief 保存元数据到Flash.
|
||||
*/
|
||||
static esp_err_t metaSave(const ota_meta_t *meta);
|
||||
/**
|
||||
* @brief 传输过程中更新元数据.
|
||||
*/
|
||||
static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
|
||||
/**
|
||||
* @brief OTA完成或中止时清除元数据.
|
||||
*/
|
||||
static esp_err_t metaClear();
|
||||
/**
|
||||
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
|
||||
*/
|
||||
static esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
|
||||
/**
|
||||
* @brief 固件接收完毕后计算SHA-256并写入元数据.
|
||||
*/
|
||||
static esp_err_t metaFinalizeChecksum();
|
||||
/**
|
||||
* @brief 校验固件完整性(SHA-256).
|
||||
*/
|
||||
static esp_err_t metaVerifyChecksum();
|
||||
/**
|
||||
* @brief 校验前端提供的 SHA-256 是否与设备端计算的一致。
|
||||
* BLE/WiFi 传输层共用:内部完成 metaFinalizeChecksum → metaLoad → memcmp。
|
||||
* 无论是否匹配,都会通过 out_device_sha 回传设备端 SHA(便于前端核对)。
|
||||
* @param frontend_sha32 前端 32 字节原始 SHA(非 hex)。
|
||||
* @param out_device_sha 可为 nullptr;非空时输出设备端 32 字节 SHA。
|
||||
* @return ESP_OK 匹配;ESP_ERR_INVALID_CRC 不匹配;其他 = 元数据/计算失败。
|
||||
*/
|
||||
static esp_err_t verifyFrontendSha(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32]);
|
||||
static IRelayListener* getRelayListener();
|
||||
|
||||
// ── Flash 操作已迁移至 Gd32FlashStore(storage/gd32_flash_store.h)──
|
||||
|
||||
// ── 公开状态成员 ──
|
||||
static relay_state_t relayState;
|
||||
static uint32_t relaySeq;
|
||||
static uint32_t relayTotalPackets;
|
||||
static volatile bool gd32AppAlive;
|
||||
static gd32_version_t gd32Version;
|
||||
static QueueHandle_t dispatchQueue;
|
||||
static QueueHandle_t resultQueue;
|
||||
static TaskHandle_t taskHandle;
|
||||
static relay_state_t relayState; ///< 当前状态机状态
|
||||
static uint32_t relaySeq; ///< 当前中继会话序号
|
||||
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
||||
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
||||
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
||||
static QueueHandle_t dispatchQueue; ///< 事件分发队列
|
||||
static QueueHandle_t resultQueue; ///< 结果回传队列
|
||||
static TaskHandle_t taskHandle; ///< OTA 中继任务句柄
|
||||
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
|
||||
|
||||
// ── 线程安全锁 ──
|
||||
static SemaphoreHandle_t s_stateMutex;
|
||||
static void initMutex();
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50);
|
||||
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁
|
||||
static void initMutex(); ///< 初始化状态互斥锁(app_main 中调用)
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
||||
|
||||
/** @brief RAII 状态锁守护类,自动获取/释放 s_stateMutex。 */
|
||||
class StateLockGuard {
|
||||
public:
|
||||
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
|
||||
@@ -227,11 +221,10 @@ public:
|
||||
};
|
||||
|
||||
// ── 实例状态 ──
|
||||
const esp_partition_t *m_gd32Partition = NULL;
|
||||
session_ctx_t m_sessionCtx;
|
||||
uint8_t m_nonceServer[16] = {0};
|
||||
uint8_t m_kSession[16] = {0};
|
||||
uint8_t m_gd32UidRuntime[12] = {0};
|
||||
session_ctx_t m_sessionCtx; ///< AES 会话上下文
|
||||
uint8_t m_nonceServer[16] = {0}; ///< 服务端随机数
|
||||
uint8_t m_kSession[16] = {0}; ///< 会话密钥(挑战认证后生成)
|
||||
uint8_t m_gd32UidRuntime[12] = {0}; ///< GD32 芯片 UID(运行时握手获取)
|
||||
|
||||
static constexpr size_t GD32_UID_LEN = 12;
|
||||
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
||||
@@ -271,26 +264,34 @@ private:
|
||||
// ── 协议子步骤(由状态 handler 内部调用)──
|
||||
/**
|
||||
* @brief 请求GD32 APP进入Bootloader.
|
||||
* @return ESP_OK 成功,SPI 通信失败返回错误码。
|
||||
*/
|
||||
esp_err_t requestGd32AppEnterBootloader();
|
||||
/**
|
||||
* @brief 执行握手请求(发送 HANDSHAKE_REQ).
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
ota_result_t doHandshakeReq();
|
||||
/**
|
||||
* @brief 执行挑战认证.
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_AUTH 认证失败。
|
||||
*/
|
||||
ota_result_t doChallengeAndAuth();
|
||||
/**
|
||||
* @brief 发送固件总大小.
|
||||
* @param size 固件总大小(字节)。
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_TIMEOUT 超时。
|
||||
*/
|
||||
ota_result_t doSendFirmwareSize(uint32_t size);
|
||||
/**
|
||||
* @brief 转发下一批数据包(由 TRANSFERRING 状态循环调用).
|
||||
* @param deadline_tick 全局超时时刻(0=不限时)。
|
||||
* @return OTA_RESULT_OK 成功,负值表示错误。
|
||||
*/
|
||||
ota_result_t doRelayPackets(TickType_t deadline_tick);
|
||||
/**
|
||||
* @brief 发送结束指令并验证 APP 存活.
|
||||
* @return OTA_RESULT_OK 成功,负值表示错误。
|
||||
*/
|
||||
ota_result_t doFinishAndVerify();
|
||||
|
||||
|
||||
+13
-31
@@ -44,6 +44,7 @@
|
||||
#include "esp32_ota_core.h"
|
||||
#include "ota_coordinator.h"
|
||||
#include "ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "health_monitor.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
@@ -633,11 +634,13 @@ static void sendFinishEvent(uint8_t stage, uint32_t value)
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 将 session_id 和 code 打包为 32 位 BLE FINISH 事件值。 */
|
||||
static uint32_t packBleFinishValue(uint16_t session_id, uint16_t code)
|
||||
{
|
||||
return ((uint32_t)code << 16) | (uint32_t)session_id;
|
||||
}
|
||||
|
||||
/** @brief 将 32 字节 SHA-256 转为大写十六进制字符串(65字节含'\0')。 */
|
||||
static void shaBytesToHexUpper(const uint8_t sha[32], char out_hex[65])
|
||||
{
|
||||
if (!sha || !out_hex) return;
|
||||
@@ -647,6 +650,7 @@ static void shaBytesToHexUpper(const uint8_t sha[32], char out_hex[65])
|
||||
out_hex[64] = '\0';
|
||||
}
|
||||
|
||||
/** @brief 通过 BLE Notify 推送设备端 SHA-256 给前端(用于比对验证)。 */
|
||||
static void sendSha256Notify(const uint8_t sha[32], uint16_t session_id)
|
||||
{
|
||||
if (!sha) return;
|
||||
@@ -659,6 +663,7 @@ static void sendSha256Notify(const uint8_t sha[32], uint16_t session_id)
|
||||
BleOta::instance().notifyResponse(sha_pkt, sizeof(sha_pkt));
|
||||
}
|
||||
|
||||
/** @brief BLE 触发 GD32 中继后等待结果的 FreeRTOS 任务。 */
|
||||
static void ble_gd32_relay_wait_task(void *pvParameters)
|
||||
{
|
||||
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)pvParameters;
|
||||
@@ -747,7 +752,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
// 检查固件是否存储完毕 → 通过协调器触发 GD32 OTA 中继
|
||||
ota_meta_t meta = {};
|
||||
esp_err_t err = OtaRelay::metaLoad(&meta);
|
||||
esp_err_t err = OtaCoordinator::instance().loadGd32Meta(&meta);
|
||||
if (err == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
@@ -758,7 +763,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
// ★ 统一走共享 helper:计算设备端 SHA → 比对前端 SHA
|
||||
uint8_t device_sha[32] = {0};
|
||||
err = OtaRelay::verifyFrontendSha(frontend_sha, device_sha);
|
||||
err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha);
|
||||
shaBytesToHexUpper(device_sha, device_sha_hex);
|
||||
sendSha256Notify(device_sha, session_id);
|
||||
DEBUG_INFO(TAG, "GD32 前端 SHA-256: %s, session=%u", frontend_sha_hex, session_id);
|
||||
@@ -1111,7 +1116,7 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
meta.stored_size = 0;
|
||||
meta.last_seq = 0;
|
||||
|
||||
esp_err_t err = OtaRelay::metaSave(&meta);
|
||||
esp_err_t err = OtaCoordinator::instance().saveGd32Meta(&meta);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "元数据保存失败");
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
@@ -1124,7 +1129,7 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
|
||||
return;
|
||||
}
|
||||
@@ -1151,7 +1156,7 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, &buf[data_offset], data_len);
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(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));
|
||||
@@ -1159,7 +1164,7 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
}
|
||||
|
||||
uint32_t stored_size = flash_offset + (uint32_t)data_len;
|
||||
err = OtaRelay::metaUpdate(stored_size, seq);
|
||||
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
|
||||
}
|
||||
@@ -1174,14 +1179,14 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
seq, flash_offset, stored_size);
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (OtaRelay::metaLoad(&meta) == ESP_OK &&
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
ble_ota_restore_wifi_ps();
|
||||
esp_err_t sha_err = OtaRelay::metaFinalizeChecksum();
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
@@ -1388,7 +1393,6 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
|
||||
/** @brief BLE GATT 客户端连接状态。 */
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
}
|
||||
return 0;
|
||||
@@ -1554,7 +1558,6 @@ static void ble_ota_on_sync(void)
|
||||
static void ble_ota_on_reset(int reason)
|
||||
{
|
||||
DEBUG_WARN(TAG, "NimBLE 协议栈重置: reason=%d", reason);
|
||||
/** @brief NimBLE 协议栈同步完成标志。 */
|
||||
BleOta::instance().m_bleReady = false;
|
||||
}
|
||||
|
||||
@@ -1678,30 +1681,9 @@ esp_err_t BleOta::notifyResponse(const uint8_t *data, size_t len)
|
||||
*/
|
||||
bool BleOta::isConnected()
|
||||
{
|
||||
/** @brief BLE GATT 客户端连接状态。 */
|
||||
return BleOta::instance().m_bleConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通知中继模块开始向 GD32 转发固件。
|
||||
*/
|
||||
void BleOta::signalRelay()
|
||||
{
|
||||
if (OtaRelay::dispatchQueue == NULL) {
|
||||
DEBUG_ERR(TAG, "OTA 调度队列未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t msg = 1;
|
||||
BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &msg, pdMS_TO_TICKS(200));
|
||||
if (ret != pdTRUE) {
|
||||
DEBUG_ERR(TAG, "OTA 调度信号发送失败: 队列满");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 调度信号已发送 → ota_relay_task");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BLE OTA FreeRTOS 任务入口
|
||||
//
|
||||
|
||||
+8
-13
@@ -43,11 +43,6 @@ public:
|
||||
* @return 已连接返回 true,未连接返回 false。
|
||||
*/
|
||||
bool isConnected();
|
||||
/**
|
||||
* @brief 触发中继模块向 GD32 转发固件。
|
||||
*/
|
||||
void signalRelay();
|
||||
|
||||
/** @brief 获取 BLE OTA 守护任务句柄。 */
|
||||
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
|
||||
|
||||
@@ -55,16 +50,16 @@ public:
|
||||
TaskHandle_t getWorkerTaskHandle() const { return m_workerTaskHandle; }
|
||||
|
||||
// 以下状态为 public,供 C 回调函数访问
|
||||
bool m_bleReady = false;
|
||||
bool m_bleConnected = false;
|
||||
uint16_t m_connHandle = 0xFFFF;
|
||||
bool m_bleReady = false; ///< BLE GATT 服务注册完成标志
|
||||
bool m_bleConnected = false; ///< BLE 客户端连接状态
|
||||
uint16_t m_connHandle = 0xFFFF; ///< BLE 连接句柄(0xFFFF=未连接)
|
||||
|
||||
// BLE 传输相关(OTA 写入状态已移至 Esp32OtaCore)
|
||||
bool m_esp32RebootPending = false; // 收尾任务去重标志
|
||||
uint8_t m_esp32LastCmd = 0; // 最后一次数据包命令(用于补发 ACK)
|
||||
uint16_t m_esp32LastSeq = 0xFFFFU; // 最后一次数据包序号(用于补发 ACK)
|
||||
TaskHandle_t m_taskHandle = nullptr;
|
||||
TaskHandle_t m_workerTaskHandle = nullptr;
|
||||
bool m_esp32RebootPending = false; ///< 收尾任务去重标志
|
||||
uint8_t m_esp32LastCmd = 0; ///< 最后一次数据包命令(用于补发 ACK)
|
||||
uint16_t m_esp32LastSeq = 0xFFFFU; ///< 最后一次数据包序号(用于补发 ACK)
|
||||
TaskHandle_t m_taskHandle = nullptr; ///< BLE OTA 守护任务句柄
|
||||
TaskHandle_t m_workerTaskHandle = nullptr; ///< BLE OTA 数据工作线程句柄
|
||||
};
|
||||
|
||||
#endif // BLE_OTA_H
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
/*
|
||||
* http_handlers.cpp - HTTP 请求处理器实现
|
||||
* 从 wifi_ota.cpp 提取的 HTTP handler 函数
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_netif.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include "app_config.h"
|
||||
#include "http_handlers.h"
|
||||
#include "wifi_ota.h"
|
||||
#include "esp32_ota_core.h"
|
||||
#include "ota_coordinator.h"
|
||||
#include "ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "health_monitor.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "spi_master.h"
|
||||
#include "ble_ota.h"
|
||||
|
||||
static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U;
|
||||
|
||||
// OTA 接收共享缓冲区
|
||||
static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];
|
||||
|
||||
static uint32_t align_up_u32(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
return ((value + alignment - 1U) / alignment) * alignment;
|
||||
}
|
||||
|
||||
void http_set_cors_headers(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
|
||||
}
|
||||
|
||||
esp_err_t http_options_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
httpd_resp_set_status(req, "204 No Content");
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t esp32_ota_update_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
if (!core.isActive()) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char frontend_sha_hex[65] = {0};
|
||||
bool has_frontend_sha = false;
|
||||
bool verify_only = false;
|
||||
if (req->content_len > 0 && req->content_len < 256) {
|
||||
char buf[256] = {0};
|
||||
int ret = httpd_req_recv(req, buf, req->content_len);
|
||||
if (ret > 0) {
|
||||
buf[ret] = '\0';
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (root) {
|
||||
cJSON *sha_item = cJSON_GetObjectItem(root, "sha256");
|
||||
if (sha_item && cJSON_IsString(sha_item)) {
|
||||
const char *sha_str = cJSON_GetStringValue(sha_item);
|
||||
if (sha_str && strlen(sha_str) == 64) {
|
||||
strncpy(frontend_sha_hex, sha_str, 64);
|
||||
frontend_sha_hex[64] = '\0';
|
||||
has_frontend_sha = true;
|
||||
DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex);
|
||||
}
|
||||
}
|
||||
cJSON *verify_item = cJSON_GetObjectItem(root, "verify_only");
|
||||
if (verify_item && cJSON_IsBool(verify_item)) {
|
||||
verify_only = cJSON_IsTrue(verify_item);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char device_sha_hex[65] = {0};
|
||||
|
||||
if (verify_only) {
|
||||
DEBUG_INFO("OTA", "ESP32 OTA verify-only mode, %u bytes", (unsigned)core.getReceived());
|
||||
esp_err_t sha_err = core.computeDeviceSha(device_sha_hex);
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "verify-only: SHA 计算失败: %s", esp_err_to_name(sha_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(sha_err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
char ok_resp[256] = {0};
|
||||
snprintf(ok_resp, sizeof(ok_resp),
|
||||
"{\"status\": \"ok\", \"message\": \"SHA-256 computed\", \"sha256\": \"%s\"}",
|
||||
device_sha_hex);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, ok_resp, strlen(ok_resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "ESP32 OTA finalize, %u bytes...", (unsigned)core.getReceived());
|
||||
Esp32FinalizeResult fr = OtaCoordinator::instance().finalizeForReboot(
|
||||
has_frontend_sha ? frontend_sha_hex : nullptr);
|
||||
|
||||
if (fr.err == ESP_OK) {
|
||||
char resp[320] = {0};
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\", "
|
||||
"\"sha256\": \"%s\"}",
|
||||
fr.device_sha_hex);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
DEBUG_INFO("OTA", "响应已发送,重启...");
|
||||
OtaCoordinator::instance().restartEsp32();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (fr.err == ESP_ERR_INVALID_VERSION) {
|
||||
char err_resp[256] = {0};
|
||||
snprintf(err_resp, sizeof(err_resp),
|
||||
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", \"frontend\": \"%s\", \"device\": \"%s\"}",
|
||||
frontend_sha_hex, fr.device_sha_hex);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, err_resp, strlen(err_resp));
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(fr.err));
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t restart_handler(httpd_req_t *req)
|
||||
{
|
||||
(void)req;
|
||||
http_set_cors_headers(req);
|
||||
|
||||
DEBUG_INFO("OTA", "收到重启请求,重启...");
|
||||
|
||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"rebooting\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
OtaCoordinator::instance().restartEsp32();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
||||
{
|
||||
int total = 0;
|
||||
int depth = 1;
|
||||
while (total < buf_size - 1) {
|
||||
int ret = httpd_req_recv(req, buf + total, 1);
|
||||
if (ret <= 0) return total;
|
||||
total += ret;
|
||||
buf[total] = '\0';
|
||||
if (buf[total - 1] == '{') depth++;
|
||||
else if (buf[total - 1] == '}') {
|
||||
depth--;
|
||||
if (depth == 0) return total;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d", req->content_len);
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||
|
||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||
WifiOta::instance().setBleOtaActive(false);
|
||||
|
||||
char peek;
|
||||
int peek_ret = httpd_req_recv(req, &peek, 1);
|
||||
if (peek_ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
bool is_esp32_ota = false;
|
||||
uint32_t total_size = 0;
|
||||
int json_header_len = 0;
|
||||
uint8_t pre_read_buf[256] = {0};
|
||||
int pre_read_len = 1;
|
||||
pre_read_buf[0] = (uint8_t)peek;
|
||||
|
||||
if (peek == '{') {
|
||||
char json_buf[256];
|
||||
json_buf[0] = peek;
|
||||
json_header_len = recv_json_header(req, json_buf + 1, sizeof(json_buf) - 1);
|
||||
if (json_header_len > 0) {
|
||||
json_header_len++;
|
||||
json_buf[json_header_len] = '\0';
|
||||
pre_read_len = json_header_len;
|
||||
if (pre_read_len > (int)sizeof(pre_read_buf)) pre_read_len = (int)sizeof(pre_read_buf);
|
||||
memcpy(pre_read_buf, json_buf, pre_read_len);
|
||||
|
||||
cJSON *root = cJSON_Parse(json_buf);
|
||||
if (root) {
|
||||
cJSON *type_item = cJSON_GetObjectItem(root, "type");
|
||||
if (type_item && cJSON_IsString(type_item) &&
|
||||
strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
|
||||
is_esp32_ota = true;
|
||||
DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
|
||||
}
|
||||
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
|
||||
if (size_item) {
|
||||
total_size = (uint32_t)cJSON_GetNumberValue(size_item);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_esp32_ota) {
|
||||
if (total_size == 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
uint8_t *fw_buf = s_ota_recv_buf;
|
||||
|
||||
size_t total_firmware_written = 0;
|
||||
size_t total_received = json_header_len;
|
||||
int last_percent = 0;
|
||||
|
||||
while (total_received < req->content_len) {
|
||||
size_t to_read = req->content_len - total_received;
|
||||
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
||||
|
||||
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret);
|
||||
core.abort();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
err = core.write(fw_buf, (size_t)ret);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "OTA write failed: %s", esp_err_to_name(err));
|
||||
core.abort();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
total_firmware_written += ret;
|
||||
total_received += ret;
|
||||
|
||||
if (total_size > 0) {
|
||||
int percent = (int)((total_firmware_written * 100) / total_size);
|
||||
if (percent / 10 > last_percent / 10) {
|
||||
last_percent = percent;
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%d%%)",
|
||||
(unsigned int)total_firmware_written, total_size, percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)core.getReceived(), json_header_len);
|
||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const uint32_t gd32_fw_size = (uint32_t)req->content_len;
|
||||
uint32_t gd32_valid_size;
|
||||
if (json_header_len > 0 && (uint32_t)json_header_len < gd32_fw_size) {
|
||||
gd32_valid_size = (total_size > 0 && total_size < gd32_fw_size) ? total_size : (gd32_fw_size - (uint32_t)json_header_len);
|
||||
} else {
|
||||
gd32_valid_size = gd32_fw_size;
|
||||
}
|
||||
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_valid_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||
if (gd32_valid_size == 0U || gd32_valid_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u rounded48=%u max=%u",
|
||||
(unsigned)gd32_valid_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
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().stateUnlock();
|
||||
|
||||
const uint32_t gd32_erase_size = align_up_u32(gd32_valid_size, GD32_OTA_FLASH_SECTOR_SIZE);
|
||||
DEBUG_INFO("WIFI", "擦除 gd32_ota 分区: %u bytes (fw=%u)",
|
||||
(unsigned)gd32_erase_size, (unsigned)gd32_valid_size);
|
||||
esp_err_t erase_err = OtaCoordinator::instance().eraseGd32Partition(gd32_erase_size);
|
||||
if (erase_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
||||
char err_msg[96];
|
||||
snprintf(err_msg, sizeof(err_msg), "erase gd32_ota failed: %s", esp_err_to_name(erase_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
OtaCoordinator::instance().clearGd32Meta();
|
||||
|
||||
uint8_t *buf = s_ota_recv_buf;
|
||||
|
||||
int received = 0;
|
||||
|
||||
uint32_t expected_fw_size_raw = gd32_valid_size;
|
||||
ota_meta_t meta_raw = {
|
||||
.total_size = expected_fw_size_raw,
|
||||
.stored_size = 0,
|
||||
.last_seq = 0,
|
||||
.checksum = {0}
|
||||
};
|
||||
OtaCoordinator::instance().saveGd32Meta(&meta_raw);
|
||||
uint32_t gd32_offset = 0;
|
||||
|
||||
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
||||
|
||||
if (json_header_len == 0 && pre_read_len > 0) {
|
||||
int first_chunk_len = pre_read_len;
|
||||
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
||||
if (first_chunk_len > 0) {
|
||||
gd32_offset = 0;
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(gd32_offset, pre_read_buf, (size_t)first_chunk_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
|
||||
first_chunk_len, esp_err_to_name(err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received = first_chunk_len;
|
||||
gd32_offset += (uint32_t)first_chunk_len;
|
||||
OtaCoordinator::instance().updateGd32Meta(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
|
||||
}
|
||||
} else {
|
||||
received = pre_read_len;
|
||||
}
|
||||
|
||||
while (received < req->content_len) {
|
||||
size_t to_read = req->content_len - received;
|
||||
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
||||
|
||||
int ret = httpd_req_recv(req, (char *)buf, to_read);
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "GD32 upload interrupted: received=%d expected=%d",
|
||||
received, req->content_len);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(gd32_offset, buf, (size_t)ret);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s",
|
||||
(unsigned)gd32_offset, ret, esp_err_to_name(err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
received += ret;
|
||||
gd32_offset += (uint32_t)ret;
|
||||
}
|
||||
|
||||
OtaCoordinator::instance().updateGd32Meta(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
|
||||
|
||||
DEBUG_INFO("WIFI", "GD32 upload done: %u / %u bytes", (unsigned)gd32_offset, expected_fw_size_raw);
|
||||
|
||||
ota_meta_t final_meta;
|
||||
OtaCoordinator::instance().loadGd32Meta(&final_meta);
|
||||
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(true);
|
||||
WifiOta::instance().stateUnlock();
|
||||
DEBUG_INFO("WIFI", "GD32 firmware fully stored, pre-computing SHA-256...");
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
|
||||
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\", "
|
||||
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
|
||||
DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
|
||||
|
||||
ota_meta_t meta;
|
||||
memset(&meta, 0, sizeof(meta));
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "firmware not fully stored");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char frontend_sha_hex[65] = {0};
|
||||
if (req->content_len > 0 && req->content_len < 512) {
|
||||
char buf[512] = {0};
|
||||
int ret = httpd_req_recv(req, buf, req->content_len);
|
||||
if (ret > 0) {
|
||||
buf[ret] = '\0';
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (root) {
|
||||
cJSON *sha_item = cJSON_GetObjectItem(root, "sha256");
|
||||
if (sha_item && cJSON_IsString(sha_item) &&
|
||||
strlen(cJSON_GetStringValue(sha_item)) == 64) {
|
||||
strncpy(frontend_sha_hex, cJSON_GetStringValue(sha_item), 64);
|
||||
frontend_sha_hex[64] = '\0';
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (frontend_sha_hex[0] == '\0') {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
||||
"missing sha256 in body (frontend SHA-256 required)");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint8_t frontend_sha[32] = {0};
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
char hex[3] = { frontend_sha_hex[i*2], frontend_sha_hex[i*2+1], 0 };
|
||||
frontend_sha[i] = (uint8_t)strtoul(hex, nullptr, 16);
|
||||
}
|
||||
|
||||
uint8_t device_sha[32] = {0};
|
||||
char device_sha_hex[65] = {0};
|
||||
esp_err_t verify_err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha);
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&device_sha_hex[i*2], 3, "%02X", device_sha[i]);
|
||||
}
|
||||
|
||||
if (verify_err == ESP_ERR_INVALID_CRC) {
|
||||
char err_resp[256];
|
||||
snprintf(err_resp, sizeof(err_resp),
|
||||
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", "
|
||||
"\"device\": \"%s\"}", device_sha_hex);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, err_resp, strlen(err_resp));
|
||||
return ESP_FAIL;
|
||||
} else if (verify_err != ESP_OK) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "SHA compute failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
char resp[256];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"ok\", \"message\": \"OTA relay triggered\", "
|
||||
"\"sha256\": \"%s\"}", device_sha_hex);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t logs_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
int json_len = 0;
|
||||
char *json = DebugLog::instance().ringDrainJson(&json_len);
|
||||
|
||||
if (json && json_len > 0) {
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, json, json_len);
|
||||
free(json);
|
||||
} else {
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, "[]", 2);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t can_send_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
char buf[256] = {0};
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
|
||||
cJSON *id_item = cJSON_GetObjectItem(root, "id");
|
||||
cJSON *data_item = cJSON_GetObjectItem(root, "data");
|
||||
cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
|
||||
|
||||
if (!ch_item || !id_item) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
|
||||
uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
|
||||
uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
|
||||
|
||||
uint8_t tx_data[CAN_MAX_DLC] = {0};
|
||||
uint8_t tx_dlc = 0;
|
||||
|
||||
if (data_item && cJSON_IsString(data_item)) {
|
||||
const char *hex_str = cJSON_GetStringValue(data_item);
|
||||
size_t hex_len = strlen(hex_str);
|
||||
for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
|
||||
char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
|
||||
tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
|
||||
tx_dlc++;
|
||||
}
|
||||
}
|
||||
|
||||
if (tx_dlc == 0 && dlc > 0) {
|
||||
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
|
||||
uint8_t resp_data[CAN_MAX_DLC] = {0};
|
||||
int resp_len = SpiMaster::instance().canSend(ch, ext_id, tx_data, tx_dlc,
|
||||
resp_data, sizeof(resp_data), 1000);
|
||||
|
||||
char resp[512];
|
||||
if (resp_len >= 0) {
|
||||
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
|
||||
for (int i = 0; i < resp_len; i++) {
|
||||
sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
|
||||
}
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
|
||||
ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"error\", \"code\": %d}", resp_len);
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t spi_send_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
char buf[512] = {0};
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *type_item = cJSON_GetObjectItem(root, "type");
|
||||
cJSON *data_item = cJSON_GetObjectItem(root, "data");
|
||||
cJSON *timeout_item = cJSON_GetObjectItem(root, "timeout_ms");
|
||||
|
||||
if (!type_item || !cJSON_IsString(type_item)) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 type 字段");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
const char *type_hex = cJSON_GetStringValue(type_item);
|
||||
uint8_t frame_type = (uint8_t)strtol(type_hex, NULL, 16);
|
||||
|
||||
uint32_t timeout_ms = timeout_item ? (uint32_t)cJSON_GetNumberValue(timeout_item) : 1000;
|
||||
|
||||
uint8_t payload_data[SPI_OTA_FRAME_MAX] = {0};
|
||||
size_t payload_len = 0;
|
||||
if (data_item && cJSON_IsString(data_item)) {
|
||||
const char *hex_str = cJSON_GetStringValue(data_item);
|
||||
size_t hex_len = strlen(hex_str);
|
||||
for (size_t i = 0; i < hex_len / 2 && i < SPI_OTA_FRAME_MAX; i++) {
|
||||
char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
|
||||
payload_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
|
||||
payload_len++;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("SPI", "WEB SPI: type=0x%02X, payload=%zu 字节",
|
||||
frame_type, payload_len);
|
||||
cJSON_Delete(root);
|
||||
|
||||
char resp[1024];
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0};
|
||||
size_t resp_len = 0;
|
||||
esp_err_t spi_err = SpiMaster::instance().otaTransfer(frame_type, payload_data, payload_len,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
timeout_ms);
|
||||
|
||||
if (spi_err == ESP_OK) {
|
||||
char resp_hex[SPI_OTA_FRAME_MAX * 2 + 1] = {0};
|
||||
for (size_t i = 0; i < resp_len; i++) {
|
||||
sprintf(&resp_hex[i * 2], "%02X", resp_payload[i]);
|
||||
}
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"ok\", \"type\": \"%02X\", \"payload_len\": %zu, \"resp_type\": \"%02X\", \"resp_len\": %zu, \"recv\": \"%s\"}",
|
||||
frame_type, payload_len, resp_type, resp_len, resp_hex);
|
||||
DEBUG_INFO("SPI", "WEB SPI 响应: type=0x%02X len=%zu payload=%s",
|
||||
resp_type, resp_len, resp_hex);
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"error\", \"code\": %d, \"message\": \"%s\"}",
|
||||
spi_err, esp_err_to_name(spi_err));
|
||||
DEBUG_ERR("SPI", "WEB SPI 失败: %s", esp_err_to_name(spi_err));
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||
|
||||
struct TaskStackMetric {
|
||||
const char* name;
|
||||
TaskHandle_t handle;
|
||||
size_t stack_size_bytes;
|
||||
};
|
||||
|
||||
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
|
||||
{
|
||||
if (handle == NULL) return 0;
|
||||
UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(handle);
|
||||
return (size_t)highWaterMark;
|
||||
}
|
||||
|
||||
static size_t get_task_stack_peak_used_bytes(TaskHandle_t handle, size_t stack_size_bytes)
|
||||
{
|
||||
size_t high_watermark = get_task_stack_high_watermark_bytes(handle);
|
||||
return (stack_size_bytes >= high_watermark) ? (stack_size_bytes - high_watermark) : 0;
|
||||
}
|
||||
|
||||
static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...)
|
||||
{
|
||||
if (!buf || cap == 0 || offset < 0 || (size_t)offset >= cap) return offset;
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, args);
|
||||
va_end(args);
|
||||
if (n < 0) return offset;
|
||||
size_t written = (size_t)n;
|
||||
if (offset + (int)written >= (int)cap) {
|
||||
buf[cap - 1] = '\0';
|
||||
return (int)(cap - 1);
|
||||
}
|
||||
return offset + (int)written;
|
||||
}
|
||||
|
||||
esp_err_t system_status_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
char ip_str[16] = "0.0.0.0";
|
||||
int8_t rssi = -127;
|
||||
bool connected = false;
|
||||
|
||||
if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().getStaConnected()) {
|
||||
esp_netif_ip_info_t ip_info;
|
||||
esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
|
||||
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
|
||||
}
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
|
||||
rssi = ap_info.rssi;
|
||||
}
|
||||
connected = true;
|
||||
}
|
||||
gd32_version_t pingVer = {};
|
||||
OtaRelay::getGd32Version(&pingVer);
|
||||
|
||||
ota_system_state_t curSysState = OtaCoordinator::instance().getSystemState();
|
||||
bool ota_busy = (curSysState == OTA_SYS_GD32_STORING ||
|
||||
curSysState == OTA_SYS_GD32_RELAYING ||
|
||||
curSysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
curSysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
curSysState == OTA_SYS_ESP32_REBOOTING);
|
||||
if (!ota_busy &&
|
||||
pingVer.app_major == 0 &&
|
||||
pingVer.boot_major == 0 &&
|
||||
OtaRelay::getState() == RELAY_IDLE) {
|
||||
static uint32_t last_ping_ms = 0;
|
||||
static uint8_t ping_attempts = 0;
|
||||
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
|
||||
if (ping_attempts < 5 && now_ms - last_ping_ms > 5000) {
|
||||
last_ping_ms = now_ms;
|
||||
++ping_attempts;
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ver = {};
|
||||
if (SpiMaster::instance().appPing(500, &uptime, &ver) == ESP_OK) {
|
||||
OtaRelay::setGd32AppAlive(true);
|
||||
OtaRelay::setGd32Version(ver);
|
||||
ping_attempts = 0;
|
||||
} else {
|
||||
OtaRelay::setGd32AppAlive(false);
|
||||
if (ping_attempts >= 5) {
|
||||
DEBUG_WARN("STATUS", "GD32 版本号读取失败,已达最大重试次数 (5),停止尝试");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ota_meta_t meta;
|
||||
memset(&meta, 0, sizeof(meta));
|
||||
OtaCoordinator::instance().loadGd32Meta(&meta);
|
||||
|
||||
gd32_version_t snapVer2 = {};
|
||||
OtaRelay::getGd32Version(&snapVer2);
|
||||
ota_system_state_t sysState = OtaCoordinator::instance().getSystemState();
|
||||
size_t free_heap = esp_get_free_heap_size();
|
||||
size_t min_free_heap = esp_get_minimum_free_heap_size();
|
||||
size_t largest_free_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||
size_t internal_free_heap = heap_caps_get_free_size(INTERNAL_HEAP_CAPS);
|
||||
size_t internal_min_free_heap = heap_caps_get_minimum_free_size(INTERNAL_HEAP_CAPS);
|
||||
size_t internal_largest_free_block = heap_caps_get_largest_free_block(INTERNAL_HEAP_CAPS);
|
||||
size_t internal_heap_total = heap_caps_get_total_size(INTERNAL_HEAP_CAPS);
|
||||
size_t default_heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
|
||||
TaskStackMetric task_metrics[] = {
|
||||
{ "http_server", xTaskGetCurrentTaskHandle(), HTTP_SERVER_STACK_SIZE },
|
||||
{ "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 },
|
||||
{ "ota_relay", OtaRelay::getTaskHandle(), 6144 },
|
||||
{ "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 },
|
||||
{ "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 },
|
||||
{ "ble_ota", BleOta::instance().getTaskHandle(), 4096 },
|
||||
{ "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 }
|
||||
};
|
||||
|
||||
char resp[4096];
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, "
|
||||
"\"free_heap\": %lu, \"min_free_heap\": %lu, \"largest_free_block\": %lu, "
|
||||
"\"internal_free_heap\": %lu, \"internal_min_free_heap\": %lu, \"internal_largest_free_block\": %lu, "
|
||||
"\"internal_heap_total\": %lu, \"default_heap_total\": %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\"}, "
|
||||
"\"task_stack\": {",
|
||||
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
|
||||
(unsigned long)free_heap,
|
||||
(unsigned long)min_free_heap,
|
||||
(unsigned long)largest_free_block,
|
||||
(unsigned long)internal_free_heap,
|
||||
(unsigned long)internal_min_free_heap,
|
||||
(unsigned long)internal_largest_free_block,
|
||||
(unsigned long)internal_heap_total,
|
||||
(unsigned long)default_heap_total,
|
||||
(double)HealthMonitor::instance().getLastTemperature(),
|
||||
(unsigned long)(esp_timer_get_time() / 1000000),
|
||||
WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_AP ? "AP" : "STA",
|
||||
ip_str,
|
||||
rssi,
|
||||
connected ? "true" : "false",
|
||||
OtaCoordinator::instance().getSystemStateString(),
|
||||
(int)sysState,
|
||||
(unsigned long)meta.total_size,
|
||||
(unsigned long)meta.stored_size,
|
||||
OtaCoordinator::instance().isGd32StorageDone() ? "true" : "false",
|
||||
OtaRelay::getState(),
|
||||
(unsigned long)OtaRelay::getRelaySeq(),
|
||||
(unsigned long)OtaRelay::getRelayTotalPackets(),
|
||||
OtaRelay::getGd32AppAlive() ? "true" : "false",
|
||||
snapVer2.app_major, snapVer2.app_minor,
|
||||
snapVer2.app_patch, snapVer2.app_build,
|
||||
snapVer2.boot_major, snapVer2.boot_minor,
|
||||
snapVer2.boot_patch, snapVer2.boot_build);
|
||||
|
||||
for (size_t i = 0; i < sizeof(task_metrics) / sizeof(task_metrics[0]); ++i) {
|
||||
len = append_jsonf(resp, sizeof(resp), len,
|
||||
"%s\"%s\":{\"stack_size_bytes\":%lu,\"high_watermark_bytes\":%lu,\"peak_used_bytes\":%lu}",
|
||||
i == 0 ? "" : ",",
|
||||
task_metrics[i].name,
|
||||
(unsigned long)task_metrics[i].stack_size_bytes,
|
||||
(unsigned long)get_task_stack_high_watermark_bytes(task_metrics[i].handle),
|
||||
(unsigned long)get_task_stack_peak_used_bytes(task_metrics[i].handle, task_metrics[i].stack_size_bytes));
|
||||
}
|
||||
len = append_jsonf(resp, sizeof(resp), len, "}}");
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef HTTP_HANDLERS_H
|
||||
#define HTTP_HANDLERS_H
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
// HTTP CORS and options
|
||||
void http_set_cors_headers(httpd_req_t *req);
|
||||
esp_err_t http_options_handler(httpd_req_t *req);
|
||||
|
||||
// OTA handlers
|
||||
esp_err_t esp32_ota_update_handler(httpd_req_t *req);
|
||||
esp_err_t ota_upload_handler(httpd_req_t *req);
|
||||
esp_err_t ota_relay_trigger_handler(httpd_req_t *req);
|
||||
esp_err_t restart_handler(httpd_req_t *req);
|
||||
|
||||
// Utility handlers
|
||||
esp_err_t logs_handler(httpd_req_t *req);
|
||||
esp_err_t can_send_handler(httpd_req_t *req);
|
||||
esp_err_t spi_send_handler(httpd_req_t *req);
|
||||
esp_err_t system_status_handler(httpd_req_t *req);
|
||||
|
||||
#endif // HTTP_HANDLERS_H
|
||||
+4
-979
File diff suppressed because it is too large
Load Diff
+9
-20
@@ -14,10 +14,10 @@
|
||||
#include "freertos/task.h"
|
||||
#include <atomic>
|
||||
|
||||
// SHA-256 验证所需类型前向声明
|
||||
/** @brief WiFi 工作模式枚举,区分 AP(设备热点)和 STA(连接路由器)模式。 */
|
||||
typedef enum {
|
||||
OTA_WIFI_MODE_AP,
|
||||
OTA_WIFI_MODE_STA
|
||||
OTA_WIFI_MODE_AP, ///< AP 模式:设备作为热点供局域网设备连接
|
||||
OTA_WIFI_MODE_STA ///< STA 模式:设备连接上级路由器
|
||||
} ota_wifi_mode_t;
|
||||
|
||||
class WifiOta {
|
||||
@@ -38,11 +38,6 @@ public:
|
||||
* @param pvParameters FreeRTOS 任务参数。
|
||||
*/
|
||||
static void taskEntry(void *pvParameters);
|
||||
/**
|
||||
* @brief 通知 OTA 中继开始向 GD32 转发固件。
|
||||
*/
|
||||
void signalRelay();
|
||||
|
||||
/**
|
||||
* @brief 获取当前 WiFi 工作模式。
|
||||
* @return 当前 WiFi 模式(AP 或 STA)。
|
||||
@@ -76,11 +71,6 @@ public:
|
||||
bool getGd32StorageDone() const { return m_gd32StorageDone.load(); }
|
||||
/** @brief 设置 GD32 固件存储完成标志。 */
|
||||
void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); }
|
||||
/** @brief 获取中继触发标志。 */
|
||||
bool getRelayTriggered() const { return m_relayTriggered.load(); }
|
||||
/** @brief 设置中继触发标志。 */
|
||||
void setRelayTriggered(bool v) { m_relayTriggered.store(v); }
|
||||
|
||||
// ── 状态互斥锁(保护复合读-改-写操作)──
|
||||
/** @brief 获取状态互斥锁句柄。 */
|
||||
SemaphoreHandle_t getStateMutex() const { return m_stateMutex; }
|
||||
@@ -90,13 +80,13 @@ public:
|
||||
void stateUnlock() { if (m_stateMutex) xSemaphoreGive(m_stateMutex); }
|
||||
|
||||
// ── 初始化/运行时状态(仅 OTA 任务访问,无需额外锁)──
|
||||
ota_wifi_mode_t m_wifiMode = OTA_WIFI_MODE_AP;
|
||||
bool m_coldBoot = false;
|
||||
SemaphoreHandle_t m_staConnectedSem = NULL;
|
||||
SemaphoreHandle_t m_staGotIpSem = NULL;
|
||||
TaskHandle_t m_reconnectTask = nullptr;
|
||||
ota_wifi_mode_t m_wifiMode = OTA_WIFI_MODE_AP; ///< 当前 WiFi 工作模式
|
||||
bool m_coldBoot = false; ///< 是否为冷启动(首次上电)
|
||||
SemaphoreHandle_t m_staConnectedSem = NULL; ///< STA 连接成功信号量
|
||||
SemaphoreHandle_t m_staGotIpSem = NULL; ///< STA 获取 IP 信号量
|
||||
TaskHandle_t m_reconnectTask = nullptr; ///< STA 后台重连任务句柄
|
||||
|
||||
httpd_handle_t m_httpServer = NULL;
|
||||
httpd_handle_t m_httpServer = NULL; ///< HTTP OTA 服务器句柄
|
||||
|
||||
// ★ BLE OTA 传输期间暂停 WiFi 射频活动(减少共存干扰)
|
||||
std::atomic<bool> m_bleOtaActive{false};
|
||||
@@ -112,7 +102,6 @@ private:
|
||||
std::atomic<int> m_staRetryCount{0};
|
||||
std::atomic<bool> m_staRetryExhausted{false};
|
||||
std::atomic<bool> m_gd32StorageDone{false};
|
||||
std::atomic<bool> m_relayTriggered{false};
|
||||
SemaphoreHandle_t m_stateMutex = NULL;
|
||||
TaskHandle_t m_taskHandle = nullptr;
|
||||
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* gd32_flash_store.cpp - GD32 OTA 固件 Flash 存储抽象层实现
|
||||
*
|
||||
* 从 OtaRelay 剥离的 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_partition.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
|
||||
#include "mbedtls/private/sha256.h"
|
||||
|
||||
#include "gd32_flash_store.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
// ============================================================
|
||||
// 单例
|
||||
// ============================================================
|
||||
Gd32FlashStore& Gd32FlashStore::instance()
|
||||
{
|
||||
static Gd32FlashStore inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分区懒初始化
|
||||
// ============================================================
|
||||
const esp_partition_t* Gd32FlashStore::getPartition()
|
||||
{
|
||||
if (!m_partition) {
|
||||
m_partition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
|
||||
if (!m_partition) {
|
||||
DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
|
||||
}
|
||||
}
|
||||
return m_partition;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Flash 分区读写
|
||||
// ============================================================
|
||||
|
||||
/** @brief 向 gd32_ota 分区写入数据块,自动处理非 4 字节对齐的读写。 */
|
||||
esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
const esp_partition_t *part = getPartition();
|
||||
if (!part) return ESP_ERR_NOT_FOUND;
|
||||
|
||||
// 计算 head/tail padding,使写入地址和长度都对齐到 4 字节
|
||||
size_t head_pad = offset % 4;
|
||||
uint32_t aligned_offset = offset - head_pad;
|
||||
size_t total = head_pad + len;
|
||||
size_t aligned_len = (total + 3) & ~(size_t)3;
|
||||
|
||||
if (aligned_offset + aligned_len > part->size) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk越界: offset=%" PRIu32 " len=%zu aligned_off=%" PRIu32 " aligned_len=%zu part=%" PRIu32,
|
||||
offset, len, aligned_offset, aligned_len, part->size);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
// 快速路径:offset 和 len 都对齐
|
||||
if (head_pad == 0 && len % 4 == 0) {
|
||||
return esp_partition_write(part, offset, data, len);
|
||||
}
|
||||
|
||||
if (head_pad > 0 || len % 4 != 0) {
|
||||
DEBUG_WARN("OTA", "flashWriteChunk 非对齐: offset=%" PRIu32 " len=%zu head=%zu aligned_len=%zu",
|
||||
offset, len, head_pad, aligned_len);
|
||||
}
|
||||
|
||||
uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len);
|
||||
if (!aligned_buf) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk malloc失败: %zu字节", aligned_len);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
// 读回 head padding
|
||||
if (head_pad > 0) {
|
||||
esp_err_t err = esp_partition_read(part, aligned_offset, aligned_buf, head_pad);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk读回head失败: offset=%" PRIu32 " head=%zu err=%s",
|
||||
aligned_offset, head_pad, esp_err_to_name(err));
|
||||
free(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
// 拷贝实际数据
|
||||
memcpy(aligned_buf + head_pad, data, len);
|
||||
|
||||
// 读回 tail padding
|
||||
size_t tail_pad = aligned_len - total;
|
||||
if (tail_pad > 0) {
|
||||
esp_err_t err = esp_partition_read(part, aligned_offset + total, aligned_buf + total, tail_pad);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk读回tail失败: offset=%" PRIu32 " tail=%zu err=%s",
|
||||
aligned_offset + total, tail_pad, esp_err_to_name(err));
|
||||
free(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err = esp_partition_write(part, aligned_offset, aligned_buf, aligned_len);
|
||||
free(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 从 gd32_ota 分区读取数据块。 */
|
||||
esp_err_t Gd32FlashStore::flashReadChunk(uint32_t offset, uint8_t *data, size_t len)
|
||||
{
|
||||
const esp_partition_t *part = getPartition();
|
||||
if (!part) return ESP_ERR_NOT_FOUND;
|
||||
return esp_partition_read(part, offset, data, len);
|
||||
}
|
||||
|
||||
/** @brief 擦除 gd32_ota 分区指定范围。 */
|
||||
esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
{
|
||||
const esp_partition_t *part = getPartition();
|
||||
if (!part) return ESP_ERR_NOT_FOUND;
|
||||
if (size > part->size) {
|
||||
DEBUG_ERR("OTA", "erasePartition: size=%" PRIu32 " > partition=%" PRIu32, size, part->size);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return esp_partition_erase_range(part, 0, size);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NVS 元数据持久化
|
||||
// ============================================================
|
||||
|
||||
/** @brief 从 NVS 加载 OTA 元数据,命名空间不存在时返回全零结构。 */
|
||||
esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint32_t val = 0;
|
||||
if (nvs_get_u32(handle, "total_size", &val) == ESP_OK) meta->total_size = val;
|
||||
if (nvs_get_u32(handle, "stored_size", &val) == ESP_OK) meta->stored_size = val;
|
||||
if (nvs_get_u32(handle, "last_seq", &val) == ESP_OK) meta->last_seq = val;
|
||||
|
||||
size_t sha_len = sizeof(meta->checksum);
|
||||
if (nvs_get_blob(handle, "checksum", meta->checksum, &sha_len) == ESP_OK && sha_len == sizeof(meta->checksum)) {
|
||||
} else {
|
||||
memset(meta->checksum, 0, sizeof(meta->checksum));
|
||||
}
|
||||
|
||||
nvs_close(handle);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 将 OTA 元数据写入 NVS。 */
|
||||
esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "无法打开 NVS 写入: %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
nvs_set_u32(handle, "total_size", meta->total_size);
|
||||
nvs_set_u32(handle, "stored_size", meta->stored_size);
|
||||
nvs_set_u32(handle, "last_seq", meta->last_seq);
|
||||
nvs_set_blob(handle, "checksum", meta->checksum, sizeof(meta->checksum));
|
||||
err = nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "NVS commit 失败: %s", esp_err_to_name(err));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 传输过程中更新元数据的 stored_size 和 last_seq 字段。 */
|
||||
esp_err_t Gd32FlashStore::metaUpdate(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
|
||||
return metaSave(&meta);
|
||||
}
|
||||
|
||||
/** @brief OTA 完成或中止时清除元数据(全零写入)。 */
|
||||
esp_err_t Gd32FlashStore::metaClear()
|
||||
{
|
||||
DEBUG_INFO("OTA", "清除 OTA 元数据");
|
||||
ota_meta_t meta = {};
|
||||
return metaSave(&meta);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SHA-256 校验
|
||||
// ============================================================
|
||||
|
||||
/** @brief 从 gd32_ota 分区读取固件并计算 SHA-256,输出 32 字节哈希。 */
|
||||
esp_err_t Gd32FlashStore::computeFlashSha256(uint8_t out_sha256[32])
|
||||
{
|
||||
if (!out_sha256) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR("OTA", "computeSha256: 元数据无效");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
if (mbedtls_sha256_starts(&ctx, 0) != 0) {
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
static constexpr size_t kChunkSize = 1024;
|
||||
uint8_t *buf = (uint8_t *)malloc(kChunkSize);
|
||||
if (!buf) {
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
uint32_t remaining = meta.total_size;
|
||||
uint32_t offset = 0;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = (remaining > kChunkSize) ? kChunkSize : remaining;
|
||||
err = flashReadChunk(offset, buf, to_read);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "computeSha256: Flash 读取失败 offset=%" PRIu32, offset);
|
||||
free(buf);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return err;
|
||||
}
|
||||
if (mbedtls_sha256_update(&ctx, buf, to_read) != 0) {
|
||||
free(buf);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
offset += to_read;
|
||||
remaining -= to_read;
|
||||
}
|
||||
|
||||
free(buf);
|
||||
int ret = mbedtls_sha256_finish(&ctx, out_sha256);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
return (ret == 0) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
/** @brief 固件接收完毕后计算 SHA-256 并写入 meta.checksum。 */
|
||||
esp_err_t Gd32FlashStore::metaFinalizeChecksum()
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
if (meta.total_size == 0 || meta.stored_size < meta.total_size) {
|
||||
DEBUG_ERR("OTA", "metaFinalize: 固件不完整 stored=%" PRIu32 " total=%" PRIu32,
|
||||
meta.stored_size, meta.total_size);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
err = computeFlashSha256(meta.checksum);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "metaFinalize: SHA-256 计算失败");
|
||||
return err;
|
||||
}
|
||||
|
||||
char hex[65] = {0};
|
||||
for (int i = 0; i < 32; ++i)
|
||||
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", meta.checksum[i]);
|
||||
DEBUG_INFO("OTA", "固件 SHA-256: %s", hex);
|
||||
|
||||
return metaSave(&meta);
|
||||
}
|
||||
|
||||
/** @brief 校验固件完整性:重新计算 Flash SHA-256 并与 meta.checksum 比对。 */
|
||||
esp_err_t Gd32FlashStore::metaVerifyChecksum()
|
||||
{
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// 检查 checksum 是否全零(未计算过)
|
||||
bool all_zero = true;
|
||||
for (size_t i = 0; i < sizeof(meta.checksum); ++i) {
|
||||
if (meta.checksum[i] != 0x00) { all_zero = false; break; }
|
||||
}
|
||||
if (all_zero) {
|
||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,跳过校验(旧数据兼容)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t computed[32] = {0};
|
||||
err = computeFlashSha256(computed);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "metaVerify: SHA-256 计算失败");
|
||||
return err;
|
||||
}
|
||||
|
||||
if (memcmp(computed, meta.checksum, 32) != 0) {
|
||||
char hex_stored[65] = {0}, hex_computed[65] = {0};
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&hex_stored[i * 2], sizeof(hex_stored) - i * 2, "%02X", meta.checksum[i]);
|
||||
snprintf(&hex_computed[i * 2], sizeof(hex_computed) - i * 2, "%02X", computed[i]);
|
||||
}
|
||||
DEBUG_ERR("OTA", "固件完整性校验失败!");
|
||||
DEBUG_ERR("OTA", " 存储 SHA-256: %s", hex_stored);
|
||||
DEBUG_ERR("OTA", " 计算 SHA-256: %s", hex_computed);
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "固件完整性校验通过 (SHA-256 匹配)");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t Gd32FlashStore::verifyFrontendSha(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32])
|
||||
{
|
||||
if (!frontend_sha32) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
// 1) 计算设备端 SHA 并写入 meta.checksum
|
||||
esp_err_t err = metaFinalizeChecksum();
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// 2) 读回 meta 拿到 checksum
|
||||
ota_meta_t meta;
|
||||
err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
if (out_device_sha) memcpy(out_device_sha, meta.checksum, 32);
|
||||
|
||||
// 3) 比对前端 SHA 与设备端 SHA
|
||||
if (memcmp(meta.checksum, frontend_sha32, 32) != 0) {
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* gd32_flash_store.h - GD32 OTA 固件 Flash 存储抽象层
|
||||
*
|
||||
* 从 OtaRelay 中剥离的 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验职责。
|
||||
* 传输层(WiFi/BLE)和 OtaRelay 均通过本类操作 GD32 固件存储,
|
||||
* 不再由 OtaRelay 承担存储职责(解决 H1 职责超载)。
|
||||
*
|
||||
* 线程模型:
|
||||
* - 所有方法均为 static,由调用方(协调器/OtaRelay/传输层)保证串行化
|
||||
* - 分区指针懒初始化,首次使用时查找并缓存
|
||||
*/
|
||||
#ifndef GD32_FLASH_STORE_H
|
||||
#define GD32_FLASH_STORE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_partition.h"
|
||||
|
||||
#define OTA_META_NVS_NAMESPACE "ota_meta" ///< OTA 元数据 NVS 命名空间
|
||||
|
||||
/** @brief OTA 传输元数据结构,持久化到 NVS 用于断点续传和完整性校验。 */
|
||||
typedef struct {
|
||||
uint32_t total_size; ///< 固件总大小(字节)
|
||||
uint32_t stored_size; ///< 已存储到 Flash 的大小(字节)
|
||||
uint32_t last_seq; ///< 最后一次成功写入的包序号
|
||||
uint8_t checksum[32]; ///< 固件 SHA-256 校验值
|
||||
} ota_meta_t;
|
||||
|
||||
/**
|
||||
* @brief GD32 OTA 固件 Flash 存储抽象层(纯静态工具类)。
|
||||
*
|
||||
* 封装 gd32_ota 分区的读写、NVS 元数据持久化、SHA-256 校验。
|
||||
* 传输层通过 OtaCoordinator 或直接调用本类完成存储操作,
|
||||
* OtaRelay 仅负责 SPI 中继状态机,不再承担存储职责。
|
||||
*/
|
||||
class Gd32FlashStore {
|
||||
public:
|
||||
/**
|
||||
* @brief 获取单例实例。
|
||||
* @return Gd32FlashStore 单例引用。
|
||||
*/
|
||||
static Gd32FlashStore& instance();
|
||||
|
||||
// ── Flash 分区读写 ──
|
||||
|
||||
/**
|
||||
* @brief 向 gd32_ota 分区写入数据块(支持非 4 字节对齐写入)。
|
||||
* @param offset 分区内写入偏移(字节)。
|
||||
* @param data 待写入数据缓冲区。
|
||||
* @param len 数据长度(字节)。
|
||||
* @return ESP_OK 成功,其他错误码表示失败。
|
||||
*/
|
||||
esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 从 gd32_ota 分区读取数据块。
|
||||
* @param offset 分区内读取偏移(字节)。
|
||||
* @param data 输出数据缓冲区。
|
||||
* @param len 读取长度(字节)。
|
||||
* @return ESP_OK 成功,其他错误码表示失败。
|
||||
*/
|
||||
esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 擦除 gd32_ota 分区指定范围。
|
||||
* @param size 擦除大小(字节),从偏移 0 开始。
|
||||
* @return ESP_OK 成功,其他错误码表示失败。
|
||||
*/
|
||||
esp_err_t erasePartition(uint32_t size);
|
||||
|
||||
// ── NVS 元数据持久化 ──
|
||||
|
||||
/**
|
||||
* @brief 从 NVS 加载 OTA 元数据(命名空间不存在时返回全零结构)。
|
||||
* @param meta 输出元数据结构体指针。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t metaLoad(ota_meta_t *meta);
|
||||
|
||||
/**
|
||||
* @brief 将 OTA 元数据写入 NVS。
|
||||
* @param meta 待保存的元数据结构体指针。
|
||||
* @return ESP_OK 成功,NVS 写入失败时返回错误码。
|
||||
*/
|
||||
esp_err_t metaSave(const ota_meta_t *meta);
|
||||
|
||||
/**
|
||||
* @brief 传输过程中更新元数据的 stored_size 和 last_seq。
|
||||
* @param stored_size 已存储大小(字节)。
|
||||
* @param last_seq 最后成功写入的包序号。
|
||||
* @return ESP_OK 成功,其他错误码表示失败。
|
||||
*/
|
||||
esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
|
||||
|
||||
/**
|
||||
* @brief OTA 完成或中止时清除元数据(全零写入)。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t metaClear();
|
||||
|
||||
// ── SHA-256 校验 ──
|
||||
|
||||
/**
|
||||
* @brief 计算 gd32_ota 分区中已存储固件的 SHA-256。
|
||||
* @param out_sha256 输出 32 字节 SHA-256 哈希。
|
||||
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 元数据无效。
|
||||
*/
|
||||
esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
|
||||
|
||||
/**
|
||||
* @brief 固件接收完毕后计算 SHA-256 并写入元数据。
|
||||
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 固件不完整。
|
||||
*/
|
||||
esp_err_t metaFinalizeChecksum();
|
||||
|
||||
/**
|
||||
* @brief 校验固件完整性(重新计算 SHA-256 并与 meta.checksum 比对)。
|
||||
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败。
|
||||
*/
|
||||
esp_err_t metaVerifyChecksum();
|
||||
|
||||
/**
|
||||
* @brief 校验前端提供的 SHA-256 是否与设备端计算的一致。
|
||||
* @param frontend_sha32 前端 32 字节原始 SHA(非 hex)。
|
||||
* @param out_device_sha 可为 nullptr;非空时输出设备端 32 字节 SHA。
|
||||
* @return ESP_OK 匹配;ESP_ERR_INVALID_CRC 不匹配;其他 = 元数据/计算失败。
|
||||
*/
|
||||
esp_err_t verifyFrontendSha(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32]);
|
||||
|
||||
/**
|
||||
* @brief 获取分区指针(懒初始化)。
|
||||
* @return 分区指针,找不到时返回 nullptr。
|
||||
*/
|
||||
const esp_partition_t* getPartition();
|
||||
|
||||
private:
|
||||
Gd32FlashStore() = default;
|
||||
~Gd32FlashStore() = default;
|
||||
Gd32FlashStore(const Gd32FlashStore&) = delete;
|
||||
Gd32FlashStore& operator=(const Gd32FlashStore&) = delete;
|
||||
|
||||
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
||||
};
|
||||
|
||||
#endif // GD32_FLASH_STORE_H
|
||||
@@ -55,6 +55,10 @@ void Protocol::init()
|
||||
|
||||
/**
|
||||
* @brief 根据时间戳、地址和随机种子生成 16 字节 nonce。
|
||||
* @param timestamp 当前时间戳(tick)。
|
||||
* @param mem_addr 内存地址(增加随机性)。
|
||||
* @param rand_seed 随机种子。
|
||||
* @param nonce_out 输出 16 字节 nonce 缓冲区。
|
||||
*/
|
||||
void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out)
|
||||
@@ -84,7 +88,10 @@ void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 会话密钥初始化
|
||||
* @brief 会话密钥初始化。
|
||||
* @param sess 会话上下文指针。
|
||||
* @param k_session 16 字节会话密钥。
|
||||
* @return 0 成功,-1 参数无效。
|
||||
*/
|
||||
int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
|
||||
{
|
||||
@@ -100,6 +107,7 @@ int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥和状态。
|
||||
* @param sess 会话上下文指针。
|
||||
*/
|
||||
void Protocol::sessionKeyClear(session_ctx_t *sess)
|
||||
{
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
#define FRAME_OVERHEAD (6) // SOF + CMD + LEN(2) + CRC16(2)
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16;
|
||||
inline constexpr size_t CHIP_UID_LEN = 12;
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16; ///< AES-128 主密钥长度(字节)
|
||||
inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(字节)
|
||||
|
||||
// AES 会话上下文
|
||||
typedef struct {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
#define SPI_MASTER_CS_GPIO (4) // CS 片选引脚
|
||||
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
|
||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||
#define SPI_QUEUE_SIZE (1)
|
||||
#define SPI_OTA_FRAME_MAX (96)
|
||||
#define SPI_QUEUE_SIZE (1) ///< SPI 事务队列深度
|
||||
#define SPI_OTA_FRAME_MAX (96) ///< SPI OTA 单帧最大字节数
|
||||
|
||||
// GD32 SPI OTA 帧类型
|
||||
#define SPI_OTA_SOF (0xA5)
|
||||
|
||||
+10
-2
@@ -1,6 +1,14 @@
|
||||
import urllib.request, json, time, sys
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ota_upload_test.py - ESP32 OTA 上传集成测试脚本
|
||||
|
||||
ESP_IP = "192.168.31.127"
|
||||
用法: python ota_upload_test.py
|
||||
前提: 已编译 build/ESP32_COMM.bin,ESP32 已连接网络。
|
||||
步骤: 上传固件 → 触发更新 → 等待重启并验证版本。
|
||||
"""
|
||||
import urllib.request, json, time, sys
|
||||
|
||||
ESP_IP = "192.168.31.127" # 目标 ESP32 IP 地址(根据实际网络修改)
|
||||
|
||||
# Step 1: Upload ESP32 firmware
|
||||
bin_path = r"G:\Robot_v2\DCU\ESP32_COMM\build\ESP32_COMM.bin"
|
||||
|
||||
+12
-3
@@ -1,6 +1,15 @@
|
||||
import serial, sys, time
|
||||
ser = serial.Serial('COM15', 115200, timeout=1)
|
||||
outfile = open('G:\\Robot_v2\\DCU\\ESP32_COMM\\serial_log.txt', 'w', encoding='utf-8')
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
serial_monitor.py - ESP32 串口日志采集工具
|
||||
|
||||
用法: python serial_monitor.py
|
||||
依赖: pip install pyserial
|
||||
功能: 监听 COM15 串口 (115200 波特率),将输出记录到 serial_log.txt,
|
||||
最多运行 5 分钟。
|
||||
"""
|
||||
import serial, sys, time
|
||||
ser = serial.Serial('COM15', 115200, timeout=1) # COM 口和波特率根据实际修改
|
||||
outfile = open('G:\\Robot_v2\\DCU\\ESP32_COMM\\serial_log.txt', 'w', encoding='utf-8') # 输出文件路径
|
||||
start = time.time()
|
||||
while time.time() - start < 300: # 最多 5 分钟
|
||||
line = ser.readline()
|
||||
|
||||
+33
-1
@@ -1,4 +1,16 @@
|
||||
// ==================== 全局状态 ====================
|
||||
/**
|
||||
* app.js - ESP32 OTA 中继控制台主脚本(WiFi OTA 通道)
|
||||
*
|
||||
* 职责:
|
||||
* - 固件文件选择 / HEX→BIN 转换 / SHA-256 计算
|
||||
* - WiFi OTA 上传流程(ESP32 本机 + GD32 中继)
|
||||
* - 系统状态轮询(/status + /logs)
|
||||
* - CAN/SPI 调试工具
|
||||
* - UI 状态管理(进度条、阶段流程、Toast、日志)
|
||||
*
|
||||
* 依赖:index.html 中定义的 DOM 元素、style.css 样式
|
||||
*/
|
||||
// ==================== 全局状态 ====================
|
||||
let esp32File = null;
|
||||
let gd32File = null;
|
||||
let esp32FileHandle = null; // FileSystemFileHandle for hot reload
|
||||
@@ -104,6 +116,7 @@ function showVerifyActions(type) {
|
||||
void type;
|
||||
}
|
||||
|
||||
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
|
||||
function setActivePhase(phase) {
|
||||
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
|
||||
activePhase = phase;
|
||||
@@ -118,6 +131,7 @@ function setActivePhase(phase) {
|
||||
var fill = document.getElementById('progressFill');
|
||||
if (fill) fill.classList.remove('done');
|
||||
}
|
||||
/** @description 标记指定阶段为完成,更新进度条颜色。 */
|
||||
function markPhaseDone(phase) {
|
||||
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
|
||||
var stepMap = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota' };
|
||||
@@ -129,6 +143,7 @@ function markPhaseDone(phase) {
|
||||
if (fill) fill.classList.add('done');
|
||||
}
|
||||
}
|
||||
/** @description 更新进度条百分比和文本显示。 */
|
||||
function setProgress(pct, text) {
|
||||
var fillEl = document.getElementById('progressFill');
|
||||
var txtEl = document.getElementById('progressTxt');
|
||||
@@ -137,6 +152,7 @@ function setProgress(pct, text) {
|
||||
if (txtEl) txtEl.textContent = text;
|
||||
if (pctEl) pctEl.textContent = pct + '%';
|
||||
}
|
||||
/** @description 重置进度条和阶段状态到初始值。 */
|
||||
function resetProgress() {
|
||||
setProgress(0, '等待操作...');
|
||||
activePhase = null;
|
||||
@@ -150,11 +166,13 @@ function resetProgress() {
|
||||
if (fill) fill.classList.remove('done');
|
||||
}
|
||||
// ==================== 工具函数 ====================
|
||||
/** @description 格式化字节大小为人类可读字符串(B/KB/MB)。 */
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
}
|
||||
/** @description 格式化 Date 对象为 HH:MM:SS.mmm 时间字符串。 */
|
||||
function formatTime(date) {
|
||||
var h = String(date.getHours()).padStart(2, '0');
|
||||
var m = String(date.getMinutes()).padStart(2, '0');
|
||||
@@ -162,6 +180,7 @@ function formatTime(date) {
|
||||
var ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||
return h + ':' + m + ':' + s + '.' + ms;
|
||||
}
|
||||
/** @description 格式化秒数为人类可读运行时间(d/h/m/s)。 */
|
||||
function formatUptime(sec) {
|
||||
if (!Number.isFinite(sec) || sec < 0) return '--';
|
||||
const d = Math.floor(sec / 86400);
|
||||
@@ -173,6 +192,7 @@ function formatUptime(sec) {
|
||||
if (m > 0) return m + 'm ' + s + 's';
|
||||
return s + 's';
|
||||
}
|
||||
/** @description 转义 HTML 特殊字符,防止 XSS 注入。 */
|
||||
function escapeHtml(text) {
|
||||
return String(text == null ? '' : text)
|
||||
.replace(/&/g, '&')
|
||||
@@ -190,6 +210,7 @@ function formatHttpErrorDetail(text) {
|
||||
if (detail.length > 180) detail = detail.slice(0, 177) + '...';
|
||||
return detail;
|
||||
}
|
||||
/** @description 显示 Toast 提示消息,支持 info/success/warning/error 类型。 */
|
||||
function showToast(message, type) {
|
||||
type = type || 'success';
|
||||
const toast = document.getElementById('toast');
|
||||
@@ -210,6 +231,7 @@ function getBaseUrl() {
|
||||
function makeLogHash(level, tag, message) {
|
||||
return level + '|' + tag + '|' + message;
|
||||
}
|
||||
/** @description 添加一条日志条目到环形缓冲区和日志面板。 */
|
||||
function addLog(level, tag, message) {
|
||||
var scroll = document.getElementById('logScroll');
|
||||
var time = formatTime(new Date());
|
||||
@@ -666,6 +688,7 @@ function updateSystemStatusUi(data) {
|
||||
setOtaProgressBadge(storageProgress);
|
||||
}
|
||||
}
|
||||
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
||||
async function testConnection() {
|
||||
// 如果在降速模式,先恢复正常轮询
|
||||
if (pollSlowMode) {
|
||||
@@ -1016,6 +1039,7 @@ async function convertHexFileToBin(file) {
|
||||
return { file: binFile, converted: true, baseAddr: result.baseAddr, endAddr: result.endAddr };
|
||||
}
|
||||
// ==================== 文件处理 ====================
|
||||
/** @description 处理用户选择的固件文件(HEX→BIN 转换、SHA-256 计算、文件校验)。 */
|
||||
function processFile(file, chip) {
|
||||
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
||||
var isHexByName = file.name.toLowerCase().endsWith('.hex');
|
||||
@@ -1083,6 +1107,7 @@ function processFile(file, chip) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @description 清除已选固件文件并重置对应 UI 状态。 */
|
||||
function clearFile(chip) {
|
||||
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
||||
if (chip === 'esp32') { esp32File = null; esp32FileHandle = null; window._esp32FileSha256 = null; }
|
||||
@@ -1099,6 +1124,7 @@ function clearFile(chip) {
|
||||
// 但前端只用一个 `isUploading` bool 锁同一通道。这里在每次 startUpload
|
||||
// 入口主动查询 /system/status,若发现另一通道已经进入 OTA 活跃态
|
||||
// (ESP32 三个状态或 GD32 存储/中继),直接拒绝并提示用户。
|
||||
/** @description 启动 ESP32 本机 OTA 上传流程(互斥获取→分块上传→SHA验证→重启)。 */
|
||||
async function startUpload(chip) {
|
||||
var file = chip === 'esp32' ? esp32File : gd32File;
|
||||
if (!file || isUploading) return;
|
||||
@@ -1317,6 +1343,7 @@ async function startUpload(chip) {
|
||||
}
|
||||
// ==================== ESP32 OTA 完成升级 ====================
|
||||
// 一次调用完成:校验 SHA-256 → 设置启动分区 → 自动重启(无需确认按钮)
|
||||
/** @description 验证 ESP32 固件 SHA-256 是否与设备端计算值一致。 */
|
||||
async function verifyEsp32Firmware() {
|
||||
updateEsp32StagesFromPhase(3); // 校验中
|
||||
var sha256 = window._esp32FileSha256;
|
||||
@@ -1463,6 +1490,7 @@ async function restartEsp32() {
|
||||
}
|
||||
}
|
||||
// ==================== GD32 OTA 触发(带 SHA-256 校验,对齐 BLE 0xF7)====================
|
||||
/** @description 触发 GD32 中继 OTA 流程(通过 HTTP 接口)。 */
|
||||
async function triggerGd32Relay() {
|
||||
var sha256 = window._gd32FileSha256;
|
||||
if (!sha256) {
|
||||
@@ -1507,6 +1535,7 @@ async function triggerGd32Relay() {
|
||||
}
|
||||
|
||||
// ==================== 手动触发 GD32 OTA (调试/旧页面兜底) ====================
|
||||
/** @description 触发 GD32 OTA 中继(封装函数,调用实际 API)。 */
|
||||
async function triggerRelay() {
|
||||
// 转发到统一入口:自动使用最近一次上传的 GD32 文件 SHA
|
||||
await triggerGd32Relay();
|
||||
@@ -1558,6 +1587,7 @@ async function diagnoseNetwork() {
|
||||
diagResult = result.join(';');
|
||||
return diagResult;
|
||||
}
|
||||
/** @description 启动定时轮询(状态 + 日志)。 */
|
||||
function startPolling() {
|
||||
consecutivePollFailures = 0;
|
||||
pollSlowMode = false;
|
||||
@@ -1606,6 +1636,7 @@ function resumePollingAfterUpload() {
|
||||
pollStatus();
|
||||
pollLogs();
|
||||
}
|
||||
/** @description 轮询 ESP32 系统状态(GET /status)并更新 UI。 */
|
||||
async function pollStatus() {
|
||||
if (pollingPausedForUpload) return;
|
||||
try {
|
||||
@@ -1684,6 +1715,7 @@ document.addEventListener('visibilitychange', function () {
|
||||
});
|
||||
// ==================== ESP32 实时日志拉取 (增量) ====================
|
||||
var lastLogTimestamp = 0;
|
||||
/** @description 轮询 ESP32 日志(GET /logs)并追加到日志面板。 */
|
||||
async function pollLogs() {
|
||||
if (pollingPausedForUpload) return;
|
||||
try {
|
||||
|
||||
@@ -419,6 +419,7 @@
|
||||
bleLog('CFG', `目标切换为 ${cfg.label}: packetCmd=0x${cfg.packetCmd.toString(16).padStart(2, '0').toUpperCase()}, payload=${cfg.recommendedPayload}`);
|
||||
}
|
||||
|
||||
/** @description 连接 BLE 设备(扫描→连接→发现服务→使能通知)。 */
|
||||
async function connectBle() {
|
||||
if (!navigator.bluetooth) {
|
||||
bleLog('BLE', '当前浏览器不支持 Web Bluetooth', 'E');
|
||||
@@ -494,6 +495,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 断开 BLE 连接并清理资源。 */
|
||||
async function disconnectBle() {
|
||||
// 用户主动断开:取消任何进行中的重连 / expectedReboot
|
||||
state.expectedReboot = false;
|
||||
@@ -534,6 +536,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 处理 BLE Notify 事件(解析 ACK/ERROR/PROGRESS 等响应类型)。 */
|
||||
function handleNotify(event) {
|
||||
const view = event.target.value;
|
||||
const bytes = new Uint8Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
||||
@@ -1100,6 +1103,7 @@
|
||||
bleLog('FILE', '文件已清除');
|
||||
}
|
||||
|
||||
/** @description 构建 BLE OTA 数据包(seq + totalSize + payload + cmd + CRC)。 */
|
||||
function buildPacket(seq, totalSize, payload, cmd) {
|
||||
const packet = new Uint8Array(7 + payload.length);
|
||||
packet[0] = cmd;
|
||||
@@ -1118,6 +1122,7 @@
|
||||
}
|
||||
|
||||
|
||||
/** @description 分块上传固件到 ESP32(通过 BLE GATT 写入)。 */
|
||||
async function uploadFirmware() {
|
||||
// 每次刷写前尝试重新从本地读取文件
|
||||
if (state.selectedFileHandle) {
|
||||
@@ -1454,6 +1459,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 自动刷写流程:上传 ESP32 固件 → SHA验证 → 触发 GD32 中继。 */
|
||||
async function autoFlash() {
|
||||
// 每次刷写前尝试重新从本地读取文件(支持热重载:用户重新编译后无需重新选择)
|
||||
if (state.selectedFileHandle) {
|
||||
@@ -1507,6 +1513,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 中止当前 BLE OTA 任务并清理状态。 */
|
||||
function abortTask() {
|
||||
state.abortRequested = true;
|
||||
bleLog('OTA', '已请求中止,等待当前分包返回');
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.html - ESP32 OTA 中继控制台主页面
|
||||
支持 WiFi OTA(ESP32 本机 + GD32 中继)和 BLE OTA 双通道固件升级。
|
||||
配套脚本:app.js (WiFi OTA), ble_ota.js (BLE OTA), style.css (UI 样式)
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
Reference in New Issue
Block a user