From 7a41eac52621813fbd6e0904efb53523bce11237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Tue, 23 Jun 2026 00:05:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ota):=20=E7=A7=BB=E9=99=A4ESP32=20OTA?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E4=B8=AD=E7=9A=84=E9=AB=98=E5=8D=B1=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E9=97=AE=E9=A2=98=E5=B9=B6=E4=BC=98=E5=8C=96=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除OtaRelay职责过载问题,将状态查询统一收口到OtaCoordinator::getSnapshot() - 新增OtaSnapshot结构体,一次性获取完整的OTA系统状态快照,避免分散读取 - 将GD32 APP探测逻辑从system_status_handler中分离,提供独立的/system/probe端点 - 修复并发安全隐患,确保processEvent()读取relayState时持有锁保护 - 优化SPI通信相关代码结构,提升系统稳定性 BREAKING CHANGE: 移除了原有的分散状态查询接口,统一使用快照方式获取状态 --- .qoder/specs/ESP32_OTA_架构分析_task-09f.md | 126 -------------------- main/include/version.h | 8 +- main/ota/ota_coordinator.cpp | 51 ++++++++ main/ota/ota_coordinator.h | 36 +++++- main/ota/ota_snapshot.h | 16 +++ main/relay/ota_relay.cpp | 37 ++++++ main/relay/ota_relay.h | 22 ++++ main/server/http_handlers.cpp | 99 ++++++++------- main/server/http_handlers.h | 1 + main/server/wifi_ota.cpp | 27 ++++- tools/index.html | 2 +- 11 files changed, 236 insertions(+), 189 deletions(-) delete mode 100644 .qoder/specs/ESP32_OTA_架构分析_task-09f.md create mode 100644 main/ota/ota_snapshot.h diff --git a/.qoder/specs/ESP32_OTA_架构分析_task-09f.md b/.qoder/specs/ESP32_OTA_架构分析_task-09f.md deleted file mode 100644 index 2aa3c75..0000000 --- a/.qoder/specs/ESP32_OTA_架构分析_task-09f.md +++ /dev/null @@ -1,126 +0,0 @@ -# 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 开销,当前规模下不值得 | diff --git a/main/include/version.h b/main/include/version.h index e8fa3bb..f513cc6 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -4,11 +4,11 @@ // 固件版本 #define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 3 -#define FW_VERSION_PATCH 31 -#define FW_VERSION_STRING "0.3.31" +#define FW_VERSION_PATCH 32 +#define FW_VERSION_STRING "0.3.32" // 编译时间 -#define BUILD_TIME "2026-06-22 00:43:09" +#define BUILD_TIME "2026-06-23 00:04:33" // Git 信息 -#define GIT_HASH "00ce8db" +#define GIT_HASH "1491e9a" diff --git a/main/ota/ota_coordinator.cpp b/main/ota/ota_coordinator.cpp index dcfbe07..3e2f671 100644 --- a/main/ota/ota_coordinator.cpp +++ b/main/ota/ota_coordinator.cpp @@ -8,10 +8,12 @@ #include "esp32_ota_core.h" #include "relay/ota_relay.h" #include "storage/gd32_flash_store.h" +#include "transport/spi_master.h" #include "monitor/debug_log.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include #define TAG "OTA_COORD" @@ -599,6 +601,55 @@ bool OtaCoordinator::isGd32StorageDone() const return done; } +// ============================================================ +// OTA 状态快照(PR1: 收口状态读取) +// ============================================================ +OtaSnapshot OtaCoordinator::getSnapshot() const +{ + OtaSnapshot snap{}; + + // ── coordinator 自身状态(一次 mutex)── + bool h = lock(); + if (h) { + snap.sys_state = m_sysState; + snap.storage_done = m_gd32StorageDone; + unlock(h); + } + snap.sys_state_str = getSystemStateString(); + + // ── GD32 NVS 元数据 ── + Gd32FlashStore::instance().metaLoad(&snap.meta); + + // ── relay 进度(一次 relay mutex)── + snap.relay = OtaRelay::getSnapshot(); + + return snap; +} + +// ============================================================ +// GD32 APP 探测(PR2: 从 /system/status 移出) +// ============================================================ +esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime) +{ + uint32_t uptime = 0; + gd32_version_t ver = {}; + + esp_err_t err = SpiMaster::instance().appPing(500, &uptime, &ver); + if (err == ESP_OK) { + OtaRelay::setGd32AppAlive(true); + OtaRelay::setGd32Version(ver); + if (out_version) *out_version = ver; + if (out_uptime) *out_uptime = uptime; + DEBUG_INFO(TAG, "GD32 APP 探测成功: alive, uptime=%" PRIu32 "s", uptime); + } else { + OtaRelay::setGd32AppAlive(false); + if (out_version) *out_version = ver; + if (out_uptime) *out_uptime = 0; + DEBUG_WARN(TAG, "GD32 APP 探测失败: %s", esp_err_to_name(err)); + } + return err; +} + // ============================================================ // 完全复位 // ============================================================ diff --git a/main/ota/ota_coordinator.h b/main/ota/ota_coordinator.h index 0468577..3312a2e 100644 --- a/main/ota/ota_coordinator.h +++ b/main/ota/ota_coordinator.h @@ -20,7 +20,7 @@ #include "freertos/semphr.h" #include "esp_err.h" #include "storage/gd32_flash_store.h" -#include "relay/ota_relay.h" // IRelayListener 接口定义(H4 解耦) +#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot(H4 解耦) // ============================================================ // 系统级 OTA 状态枚举 @@ -37,6 +37,22 @@ typedef enum { OTA_SYS_ERROR // 错误状态 } ota_system_state_t; +// ============================================================ +// OTA 系统全量只读快照(PR1: 收口状态读取) +// ============================================================ +/** + * @brief OTA 系统全量只读快照。 + * 由 OtaCoordinator::getSnapshot() 一次性填充, + * 供 /system/status 等只读接口消费,避免散读各模块状态。 + */ +struct OtaSnapshot { + ota_system_state_t sys_state = OTA_SYS_IDLE; ///< 系统级 OTA 状态 + const char *sys_state_str = "IDLE"; ///< 状态字符串 + bool storage_done = false; ///< GD32 固件存储完成标志 + ota_meta_t meta = {}; ///< GD32 NVS 元数据 + RelaySnapshot relay; ///< 中继进度快照 +}; + // ============================================================ // IBleCoexListener - BLE 射频共存通知接口(解决 L1 横向耦合) // ============================================================ @@ -242,6 +258,24 @@ public: void abortCurrent(); // ── 状态查询 ── + /** + * @brief 一次性读取系统 OTA 全量状态快照(PR1: 收口状态读取)。 + * coordinator mutex + relay mutex 各取一次, + * 供 /system/status 等只读接口消费。 + * @return 包含 sys_state、meta、relay 进度的完整快照。 + */ + OtaSnapshot getSnapshot() const; + + /** + * @brief 单次 SPI 探测 GD32 APP 是否存活(PR2: 从 status 移出)。 + * 成功时更新 OtaRelay 缓存的 alive/version, + * 失败时清除 alive 标志。 + * @param out_version 输出版本信息,可为 nullptr。 + * @param out_uptime 输出 GD32 运行时间(秒),可为 nullptr。 + * @return ESP_OK 存活,其他失败。 + */ + esp_err_t probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime); + /** * @brief 获取当前系统级 OTA 状态。 * @return 当前 ota_system_state_t 状态值。 diff --git a/main/ota/ota_snapshot.h b/main/ota/ota_snapshot.h new file mode 100644 index 0000000..e326175 --- /dev/null +++ b/main/ota/ota_snapshot.h @@ -0,0 +1,16 @@ +/* + * ota_snapshot.h - OTA 状态只读快照(便捷聚合头文件) + * + * 本文件本身不包含结构体定义,仅聚合两个快照来源: + * - RelaySnapshot → 定义在 relay/ota_relay.h(一次锁保护读取 relay 进度) + * - OtaSnapshot → 定义在 ota/ota_coordinator.h(coordinator + meta + relay 快照) + * + * HTTP handler 等消费方只需 #include "ota/ota_snapshot.h" 即可使用两种快照。 + */ +#ifndef OTA_SNAPSHOT_H +#define OTA_SNAPSHOT_H + +#include "relay/ota_relay.h" // RelaySnapshot +#include "ota/ota_coordinator.h" // OtaSnapshot + +#endif // OTA_SNAPSHOT_H diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 5d6dff8..2443ef2 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -123,6 +123,18 @@ void OtaRelay::getGd32Version(gd32_version_t *out) *out = gd32Version; } +RelaySnapshot OtaRelay::getSnapshot() +{ + StateLockGuard lock; + RelaySnapshot s; + s.state = relayState; + s.seq = relaySeq; + s.total_packets = relayTotalPackets; + s.gd32_app_alive = gd32AppAlive; + s.version = gd32Version; + return s; +} + void OtaRelay::setState(relay_state_t s) { StateLockGuard lock; @@ -1241,6 +1253,31 @@ void OtaRelay::taskEntry(void *pvParameters) return; } + // ★ 启动时初始 GD32 探测:读取 GD32 APP 版本号与存活状态 + // GD32 APP 启动需要一定时间,给其 3 秒准备窗口后发起探测 + { + DEBUG_INFO("OTA", "启动初始 GD32 探测..."); + vTaskDelay(pdMS_TO_TICKS(3000)); // 等待 GD32 APP 就绪 + uint32_t uptime = 0; + gd32_version_t ping_ver = {}; + esp_err_t ping_err = SpiMaster::instance().appPing(2000, &uptime, &ping_ver); + if (ping_err == ESP_OK) { + StateLockGuard lock; + gd32AppAlive = true; + gd32Version = ping_ver; + DEBUG_INFO("OTA", "启动 GD32 探测成功: uptime=%" PRIu32 "s, " + "APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d", + uptime, + ping_ver.app_major, ping_ver.app_minor, + ping_ver.app_patch, ping_ver.app_build, + ping_ver.boot_major, ping_ver.boot_minor, + ping_ver.boot_patch, ping_ver.boot_build); + } else { + DEBUG_WARN("OTA", "启动 GD32 探测失败: %s (GD32 可能未就绪)", + esp_err_to_name(ping_err)); + } + } + while (1) { uint32_t msg = 0; BaseType_t ret = xQueueReceive(dispatchQueue, &msg, portMAX_DELAY); diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index 8e17f8e..40410de 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -82,6 +82,22 @@ public: virtual void onRelayComplete(bool success) = 0; }; +// ============================================================ +// 中继进度快照(一次锁保护读取,消除逐 getter 不一致) +// ============================================================ +/** + * @brief GD32 中继进度只读快照。 + * 由 OtaRelay::getSnapshot() 在单次 StateLockGuard 下填充, + * 供 OtaCoordinator::getSnapshot() 组装完整系统快照。 + */ +struct RelaySnapshot { + relay_state_t state = RELAY_IDLE; ///< 当前中继状态机状态 + uint32_t seq = 0; ///< 当前中继包序号 + uint32_t total_packets = 0; ///< 本次中继总包数 + bool gd32_app_alive = false; ///< GD32 APP 存活标志 + gd32_version_t version = {}; ///< GD32 固件版本 +}; + class OtaRelay { public: /** @@ -138,6 +154,12 @@ public: * @param out 输出版本信息结构体指针。 */ static void getGd32Version(gd32_version_t *out); + /** + * @brief 一次性读取所有中继状态字段(单次锁保护)。 + * 消除逐 getter 调用之间 relay task 更新状态导致的不一致。 + * @return 中继进度快照。 + */ + static RelaySnapshot getSnapshot(); /** * @brief 设置中继状态. * @param s 目标状态。 diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index 22082b8..3031c92 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -4,6 +4,7 @@ */ #include #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_http_server.h" @@ -20,6 +21,7 @@ #include "esp32_ota_core.h" #include "ota_coordinator.h" #include "ota_relay.h" +#include "ota_snapshot.h" #include "storage/gd32_flash_store.h" #include "health_monitor.h" #include "monitor/debug_log.h" @@ -721,47 +723,10 @@ esp_err_t system_status_handler(httpd_req_t *req) } 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),停止尝试"); - } - } - } - } + // ★ PR1: 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter + OtaSnapshot snap = OtaCoordinator::instance().getSnapshot(); - 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); @@ -807,19 +772,19 @@ esp_err_t system_status_handler(httpd_req_t *req) ip_str, rssi, connected ? "true" : "false", - OtaCoordinator::instance().getSystemStateString(), - (int)sysState, - (unsigned long)meta.total_size, - (unsigned long)meta.stored_size, - 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); + snap.sys_state_str, + (int)snap.sys_state, + (unsigned long)snap.meta.total_size, + (unsigned long)snap.meta.stored_size, + snap.storage_done ? "true" : "false", + snap.relay.state, + (unsigned long)snap.relay.seq, + (unsigned long)snap.relay.total_packets, + snap.relay.gd32_app_alive ? "true" : "false", + snap.relay.version.app_major, snap.relay.version.app_minor, + snap.relay.version.app_patch, snap.relay.version.app_build, + snap.relay.version.boot_major, snap.relay.version.boot_minor, + snap.relay.version.boot_patch, snap.relay.version.boot_build); for (size_t i = 0; i < sizeof(task_metrics) / sizeof(task_metrics[0]); ++i) { len = append_jsonf(resp, sizeof(resp), len, @@ -836,3 +801,33 @@ esp_err_t system_status_handler(httpd_req_t *req) httpd_resp_send(req, resp, len); return ESP_OK; } + +// ============================================================ +// GET /system/probe — GD32 APP 单次 SPI 探测(PR2) +// ============================================================ +/** + * @brief 显式触发一次 GD32 APP 存活探测。 + * 从 system_status_handler 移出的副作用,改为独立端点。 + * 成功时更新 OtaRelay 缓存的 alive/version 并返回版本信息。 + */ +esp_err_t system_probe_handler(httpd_req_t *req) +{ + http_set_cors_headers(req); + + gd32_version_t ver = {}; + uint32_t uptime = 0; + esp_err_t err = OtaCoordinator::instance().probeGd32AppOnce(&ver, &uptime); + + char resp[256]; + int len = snprintf(resp, sizeof(resp), + "{\"alive\": %s, \"uptime_sec\": %" PRIu32 ", " + "\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}", + (err == ESP_OK) ? "true" : "false", + uptime, + ver.app_major, ver.app_minor, ver.app_patch, ver.app_build, + ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, len); + return ESP_OK; +} diff --git a/main/server/http_handlers.h b/main/server/http_handlers.h index 856dacf..6271d25 100644 --- a/main/server/http_handlers.h +++ b/main/server/http_handlers.h @@ -18,5 +18,6 @@ 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); +esp_err_t system_probe_handler(httpd_req_t *req); #endif // HTTP_HANDLERS_H diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 938a47f..90538b8 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -517,20 +517,37 @@ esp_err_t WifiOta::httpServerStart() }; httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_options_uri); - httpd_uri_t system_status_uri = { + // 系统状态查询端点 + httpd_uri_t status_uri = { .uri = "/system/status", .method = HTTP_GET, .handler = system_status_handler, .user_ctx = NULL }; - httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_status_uri); - httpd_uri_t system_status_options_uri = { + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_uri); + httpd_uri_t status_options_uri = { .uri = "/system/status", .method = HTTP_OPTIONS, .handler = http_options_handler, .user_ctx = NULL }; - httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_status_options_uri); + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_options_uri); + + // PR2: GD32 APP 探测端点 + httpd_uri_t system_probe_uri = { + .uri = "/system/probe", + .method = HTTP_GET, + .handler = system_probe_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_probe_uri); + httpd_uri_t system_probe_options_uri = { + .uri = "/system/probe", + .method = HTTP_OPTIONS, + .handler = http_options_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_probe_options_uri); // CAN 帧转发 httpd_uri_t can_send_uri = { @@ -564,7 +581,7 @@ esp_err_t WifiOta::httpServerStart() }; httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri); - DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /can/send, /spi/send"); + DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /spi/send"); return ESP_OK; } diff --git a/tools/index.html b/tools/index.html index 2fb1fa6..f9ea2f6 100644 --- a/tools/index.html +++ b/tools/index.html @@ -281,7 +281,7 @@ -
+