From 1491e9a3de7cec97d6bf8c475691a3e3e5878dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Mon, 22 Jun 2026 01:53:16 +0800 Subject: [PATCH] =?UTF-8?q?refactor(main):=20=E7=A7=BB=E9=99=A4ota=5Fmutex?= =?UTF-8?q?.h=E5=85=A8=E5=B1=80=E5=8F=98=E9=87=8F=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84ESP32=20OTA=E4=BA=92=E6=96=A5=E9=94=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将全局变量g_esp32OtaMutex和g_esp32OtaOccupied内聚到Esp32OtaCore类中 - 通过Esp32OtaCore::initMutex()方法初始化互斥锁替代原main.cpp中的全局初始化 - 重构HealthMonitor中WiFi连接状态管理,推送连接状态到本地成员而非直接查询 - 优化OtaCoordinator中const方法的线程安全实现,移除不必要的const_cast转换 - 封装OtaRelay的队列访问器,提供getDispatchQueue()和getResultQueue()方法 - 解决BLE与WiFi之间的横向耦合问题,通过OtaCoordinator进行射频共存管理 - 移除transport/spi_master.cpp中的CANFD接口实现到独立的canfd_bridge模块 - 优化BLE OTA中ESP32会话的幂等性和新会话处理逻辑 BREAKING CHANGE: 移除了ota_mutex.h全局头文件,相关功能迁移至Esp32OtaCore类中管理 --- components/transport/CMakeLists.txt | 23 ++++ main/CMakeLists.txt | 7 +- main/core/main.cpp | 13 +- main/include/ota_mutex.h | 41 ------ main/include/version.h | 8 +- main/monitor/health_monitor.cpp | 15 ++- main/monitor/health_monitor.h | 9 ++ main/ota/esp32_ota_core.cpp | 74 ++++++++--- main/ota/esp32_ota_core.h | 15 +++ main/ota/ota_coordinator.cpp | 43 ++++-- main/ota/ota_coordinator.h | 47 ++++++- main/relay/ota_relay.cpp | 11 ++ main/relay/ota_relay.h | 20 ++- main/server/ble_ota.cpp | 54 +++++--- main/server/http_handlers.cpp | 6 +- main/server/wifi_ota.cpp | 18 ++- main/server/wifi_ota.h | 10 +- main/transport/canfd_bridge.cpp | 69 ++++++++++ main/transport/canfd_bridge.h | 64 +++++++++ main/transport/spi_master.cpp | 80 +---------- main/transport/spi_master.h | 39 +----- tools/app.js | 199 ++++++++++++++++++++-------- tools/ble_ota.js | 23 ++++ 23 files changed, 598 insertions(+), 290 deletions(-) create mode 100644 components/transport/CMakeLists.txt delete mode 100644 main/include/ota_mutex.h create mode 100644 main/transport/canfd_bridge.cpp create mode 100644 main/transport/canfd_bridge.h diff --git a/components/transport/CMakeLists.txt b/components/transport/CMakeLists.txt new file mode 100644 index 0000000..1a99cb9 --- /dev/null +++ b/components/transport/CMakeLists.txt @@ -0,0 +1,23 @@ +# transport 组件 - SPI 总线驱动、CANFD 桥接、协议层 +# 提供底层硬件通信能力,被 main 组件依赖。 +# +# PRIV_INCLUDE_DIRS 仅对本组件编译可见, +# 不会将 main/ 的子目录路径泄漏给依赖本组件的其他组件, +# 从而在编译层面强制模块边界。 + +idf_component_register( + SRCS + "../../main/transport/spi_master.cpp" + "../../main/transport/canfd_bridge.cpp" + "../../main/transport/protocol.cpp" + INCLUDE_DIRS + "../../main/transport" # spi_master.h, canfd_bridge.h, protocol.h + PRIV_INCLUDE_DIRS + "../../main" # monitor/debug_log.h(仅本组件编译时使用) + "../../main/include" # app_config.h + REQUIRES + esp_driver_spi # SPI 主设备驱动 + freertos # FreeRTOS 内核 + mbedtls # AES 加密 + esp_timer # 高精度定时器 +) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 32f0444..0470a6b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -6,8 +6,6 @@ idf_component_register( "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 服务器 @@ -19,14 +17,13 @@ idf_component_register( "ota" "relay" "storage" # GD32 Flash 存储抽象层头文件 - "transport" "server" "monitor" - "include" # 公共头文件(app_config.h, version.h, ota_mutex.h) + "include" # 公共头文件(app_config.h, version.h) REQUIRES + transport # M5: 传输层组件(SPI/CANFD/协议) app_update # ESP32 OTA API cjson # JSON 解析/生成 - esp_driver_spi # SPI 主设备驱动 esp_driver_tsens # 温度传感器驱动 esp_driver_uart # UART 驱动(调试日志) esp_wifi # WiFi STA/AP 模式 diff --git a/main/core/main.cpp b/main/core/main.cpp index 4ca6e43..58ddbfd 100644 --- a/main/core/main.cpp +++ b/main/core/main.cpp @@ -22,23 +22,18 @@ #include "transport/protocol.h" #include "monitor/debug_log.h" #include "app_config.h" -#include "ota_mutex.h" +#include "ota/esp32_ota_core.h" /** * @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。 */ -// 全局 ESP32 OTA 互斥变量(声明在 ota_mutex.h) -SemaphoreHandle_t g_esp32OtaMutex = NULL; -volatile bool g_esp32OtaOccupied = false; - extern "C" void app_main(void) { esp_chip_info_t chip_info; esp_chip_info(&chip_info); - // 创建 ESP32 OTA 全局互斥 Mutex - g_esp32OtaMutex = xSemaphoreCreateMutex(); - g_esp32OtaOccupied = false; + // ★ M1: ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore(替代旧的全局变量) + Esp32OtaCore::initMutex(); // 初始化网络接口 + 事件循环(必须最早) ESP_ERROR_CHECK(esp_netif_init()); @@ -82,6 +77,8 @@ extern "C" void app_main(void) // ★ 全局 OTA 协调器初始化(在 OTA task 创建前,确保 mutex 就绪) OtaCoordinator::instance().init(); + // ★ L1: 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener) + OtaCoordinator::instance().setBleCoexListener(&WifiOta::instance()); DEBUG_INFO("MAIN", "OTA 全局协调器已初始化"); esp_err_t ret = SpiMaster::instance().init(); diff --git a/main/include/ota_mutex.h b/main/include/ota_mutex.h deleted file mode 100644 index 5e26a2b..0000000 --- a/main/include/ota_mutex.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * ota_mutex.h - ESP32 OTA 全局互斥(BLE / WiFi 通道共享) - * - * 同一时刻只允许一个 OTA 源(BLE 或 WiFi)操作 ESP32 OTA 分区。 - * 变量定义 + Mutex 创建在 main.cpp app_main()。 - */ -#pragma once -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" -#include - -extern SemaphoreHandle_t g_esp32OtaMutex; ///< ESP32 OTA 全局互斥锁句柄(main.cpp 中创建) -extern volatile bool g_esp32OtaOccupied; ///< ESP32 OTA 分区占用标志(true=正在OTA) - -/** - * @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; - if (g_esp32OtaOccupied) { - xSemaphoreGive(g_esp32OtaMutex); - return false; - } - g_esp32OtaOccupied = true; - xSemaphoreGive(g_esp32OtaMutex); - return true; -} - -/** - * @brief 释放全局 OTA 占用标志。 - * @note 内部: Take Mutex → 清除标志 → Give Mutex。 - */ -static inline void esp32_ota_mutex_release(void) { - if (!g_esp32OtaMutex) return; - xSemaphoreTake(g_esp32OtaMutex, portMAX_DELAY); - g_esp32OtaOccupied = false; - xSemaphoreGive(g_esp32OtaMutex); -} diff --git a/main/include/version.h b/main/include/version.h index d39e515..e8fa3bb 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 30 -#define FW_VERSION_STRING "0.3.30" +#define FW_VERSION_PATCH 31 +#define FW_VERSION_STRING "0.3.31" // 编译时间 -#define BUILD_TIME "2026-06-21 23:33:01" +#define BUILD_TIME "2026-06-22 00:43:09" // Git 信息 -#define GIT_HASH "3fff959" +#define GIT_HASH "00ce8db" diff --git a/main/monitor/health_monitor.cpp b/main/monitor/health_monitor.cpp index f93b57b..380b8ed 100644 --- a/main/monitor/health_monitor.cpp +++ b/main/monitor/health_monitor.cpp @@ -18,7 +18,6 @@ #include "health_monitor.h" #include "debug_log.h" -#include "server/wifi_ota.h" // ============================================================ // 单例 @@ -247,7 +246,7 @@ health_status_t HealthMonitor::checkBeforeOta() DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意"); } - if (!WifiOta::isConnected()) { + if (!m_wifiConnected) { DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接"); return HEALTH_MON_CRITICAL; } @@ -312,6 +311,18 @@ health_status_t HealthMonitor::checkBeforeOtaRelay() return status; } +// ============================================================ +// WiFi 连接状态推送(由 WiFi 事件回调调用) +// ============================================================ +/** + * @brief 由 WiFi 事件回调推送连接状态。 + * @param connected true=已连接,false=断开。 + */ +void HealthMonitor::setWifiConnected(bool connected) +{ + m_wifiConnected = connected; +} + // ============================================================ // WiFi RSSI 更新 // ============================================================ diff --git a/main/monitor/health_monitor.h b/main/monitor/health_monitor.h index 7c66eee..3b0c170 100644 --- a/main/monitor/health_monitor.h +++ b/main/monitor/health_monitor.h @@ -75,6 +75,14 @@ public: /** @brief 获取健康监控任务句柄。 */ TaskHandle_t getTaskHandle() const { return m_taskHandle; } + /** + * @brief 由 WiFi 事件回调推送连接状态(解耦 HealthMonitor → WifiOta 依赖)。 + * WiFi 连接/断开时由 wifi_ota 事件处理器主动调用, + * HealthMonitor 不再主动查询 WifiOta::isConnected()。 + * @param connected true=已连接,false=断开。 + */ + void setWifiConnected(bool connected); + private: /** @brief 加锁读取健康状态。 */ health_state_t getState(); @@ -94,6 +102,7 @@ private: int8_t m_lastWifiRssi = -127; int64_t m_lastWifiRssiMs = 0; TaskHandle_t m_taskHandle = NULL; + volatile bool m_wifiConnected = false; ///< WiFi 连接状态(由 WiFi 事件推送) static void tempMonitorCallback(void *arg); }; diff --git a/main/ota/esp32_ota_core.cpp b/main/ota/esp32_ota_core.cpp index 604345f..41bbc65 100644 --- a/main/ota/esp32_ota_core.cpp +++ b/main/ota/esp32_ota_core.cpp @@ -17,12 +17,46 @@ #include "mbedtls/private/sha256.h" #include "esp32_ota_core.h" -#include "ota_mutex.h" #include "app_config.h" #include "monitor/debug_log.h" #define TAG "OTA_CORE" +// ============================================================ +// 分区互斥锁(M1: 内聚自 ota_mutex.h 全局变量) +// ============================================================ +SemaphoreHandle_t Esp32OtaCore::s_mutex = NULL; +volatile bool Esp32OtaCore::s_occupied = false; + +void Esp32OtaCore::initMutex() +{ + if (!s_mutex) { + s_mutex = xSemaphoreCreateMutex(); + s_occupied = false; + } +} + +bool Esp32OtaCore::acquireMutex(uint32_t timeout_ms) +{ + if (!s_mutex) return false; + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) return false; + if (s_occupied) { + xSemaphoreGive(s_mutex); + return false; + } + s_occupied = true; + xSemaphoreGive(s_mutex); + return true; +} + +void Esp32OtaCore::releaseMutex() +{ + if (!s_mutex) return; + xSemaphoreTake(s_mutex, portMAX_DELAY); + s_occupied = false; + xSemaphoreGive(s_mutex); +} + /** * @brief 获取 Esp32OtaCore 单例实例。 * @return Esp32OtaCore 引用。 @@ -56,7 +90,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) { // ★ 全局互斥:必须在清理旧会话之前检查, // 否则 WiFi 的 begin() 会先释放 BLE 持有的锁,再自己获取,导致互斥失效 - if (!esp32_ota_mutex_acquire(1000)) { + if (!acquireMutex(1000)) { DEBUG_ERR(TAG, "ESP32 OTA 被其他通道占用,拒绝 begin"); return ESP_ERR_INVALID_STATE; } @@ -67,10 +101,10 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) if (!m_ended) { esp_ota_abort(m_handle); } - esp32_ota_mutex_release(); + releaseMutex(); resetState(); // 重新获取全局互斥(刚刚释放了) - if (!esp32_ota_mutex_acquire(1000)) { + if (!acquireMutex(1000)) { DEBUG_ERR(TAG, "ESP32 OTA 重新获取互斥失败"); return ESP_ERR_INVALID_STATE; } @@ -91,7 +125,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) if (m_partition == NULL) { DEBUG_ERR(TAG, "找不到可用的 OTA 分区"); - esp32_ota_mutex_release(); + releaseMutex(); return ESP_FAIL; } @@ -102,7 +136,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) DEBUG_ERR(TAG, "固件太大: %u > 分区大小: %u", (unsigned)total_size, (unsigned)m_partition->size); m_partition = nullptr; - esp32_ota_mutex_release(); + releaseMutex(); return ESP_FAIL; } @@ -119,7 +153,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err)); m_partition = nullptr; m_handle = 0; - esp32_ota_mutex_release(); + releaseMutex(); return err; } @@ -248,7 +282,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err)); esp_ota_abort(m_handle); m_ended = false; - esp32_ota_mutex_release(); + releaseMutex(); resetState(); return sha_err; } @@ -279,7 +313,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, frontend_sha_hex, device_hex); esp_ota_abort(m_handle); m_ended = false; - esp32_ota_mutex_release(); + releaseMutex(); resetState(); return ESP_ERR_INVALID_VERSION; } @@ -300,7 +334,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, // end 失败时仍可 abort(未真正 end) esp_ota_abort(m_handle); m_ended = false; - esp32_ota_mutex_release(); + releaseMutex(); resetState(); return err; } @@ -310,12 +344,12 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, err = esp_ota_set_boot_partition(m_partition); if (err != ESP_OK) { DEBUG_ERR(TAG, "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err)); - esp32_ota_mutex_release(); + releaseMutex(); resetState(); return err; } - esp32_ota_mutex_release(); + releaseMutex(); DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,共 %u 字节", (unsigned)m_received); // 保持 m_active 直到重启或下次 begin;resetState 由 begin/abort 处理 @@ -341,7 +375,7 @@ void Esp32OtaCore::abort() if (!m_ended) { esp_ota_abort(m_handle); } - esp32_ota_mutex_release(); + releaseMutex(); resetState(); DEBUG_INFO(TAG, "ESP32 OTA 已中止并清理"); } @@ -351,7 +385,7 @@ void Esp32OtaCore::abort() * * 用于新通道(WiFi/BLE)发现锁被残留占用时,主动走完整的状态机退出流程: * 1. 若会话活跃(m_active)→ 走 abort() 完整清理(esp_ota_abort + 释放锁 + 复位状态) - * 2. 若会话不活跃但锁被占(g_esp32OtaOccupied)→ 仅释放残留锁 + * 2. 若会话不活跃但锁被占(s_occupied)→ 仅释放残留锁 * 3. 否则无需操作 * * @return true 执行了清理(调用方可重试 begin),false 无需清理。 @@ -366,16 +400,16 @@ bool Esp32OtaCore::forceCleanup() } // 会话不活跃,但锁可能被残留占用 - if (!g_esp32OtaMutex) return false; + if (!s_mutex) return false; - // 在互斥保护下检查 g_esp32OtaOccupied - if (xSemaphoreTake(g_esp32OtaMutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false; + // 在互斥保护下检查 s_occupied + if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false; - bool was_occupied = g_esp32OtaOccupied; + bool was_occupied = s_occupied; if (was_occupied) { - g_esp32OtaOccupied = false; + s_occupied = false; } - xSemaphoreGive(g_esp32OtaMutex); + xSemaphoreGive(s_mutex); if (was_occupied) { DEBUG_WARN(TAG, "强制清理: 释放残留 OTA 锁(无活跃会话)"); diff --git a/main/ota/esp32_ota_core.h b/main/ota/esp32_ota_core.h index 705c453..2846097 100644 --- a/main/ota/esp32_ota_core.h +++ b/main/ota/esp32_ota_core.h @@ -19,6 +19,8 @@ #include "esp_err.h" #include "esp_ota_ops.h" #include "esp_partition.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" class Esp32OtaCore { public: @@ -99,9 +101,22 @@ public: bool isActive() const { return m_active; } /** @brief 获取已接收的字节数。 */ size_t getReceived() const { return m_received; } + + // ── 分区互斥锁管理(M1: 从 ota_mutex.h 全局变量内聚到此类)── + /** + * @brief 初始化 ESP32 OTA 分区互斥锁(在 app_main 中调用)。 + * 替代旧的全局 g_esp32OtaMutex / g_esp32OtaOccupied。 + */ + static void initMutex(); private: Esp32OtaCore() = default; + // ── 分区互斥锁(M1: 内聚自 ota_mutex.h)── + static SemaphoreHandle_t s_mutex; ///< ESP32 OTA 分区互斥锁 + static volatile bool s_occupied; ///< 分区占用标志 + static bool acquireMutex(uint32_t timeout_ms); + static void releaseMutex(); + // 从 OTA 分区读回计算 SHA-256(WiFi 风格),结果写入 m_sha256,置 m_shaComputed=true /** * @brief 从 OTA 分区读回计算 SHA-256。 diff --git a/main/ota/ota_coordinator.cpp b/main/ota/ota_coordinator.cpp index c70a9b8..dcfbe07 100644 --- a/main/ota/ota_coordinator.cpp +++ b/main/ota/ota_coordinator.cpp @@ -27,7 +27,7 @@ OtaCoordinator& OtaCoordinator::instance() // ============================================================ // 线程安全辅助 // ============================================================ -bool OtaCoordinator::lock() +bool OtaCoordinator::lock() const { // ★ 修复: 返回 take 结果,由调用方传回 unlock()。 // 旧实现用成员 bool m_locked 记录 take 结果,多任务并发 lock() 时 @@ -42,7 +42,7 @@ bool OtaCoordinator::lock() return ok; } -void OtaCoordinator::unlock(bool handle) +void OtaCoordinator::unlock(bool handle) const { // ★ 修复: 仅在对应的 lock() 成功 take 时才 give。 if (handle && m_mutex) { @@ -266,7 +266,8 @@ esp_err_t OtaCoordinator::triggerGd32Relay() } // 向 OtaRelay task 发送启动消息 - if (h && !OtaRelay::dispatchQueue) { + QueueHandle_t dq = OtaRelay::getDispatchQueue(); + if (h && !dq) { DEBUG_ERR(TAG, "OtaRelay dispatchQueue 未就绪"); unlock(h); return ESP_ERR_INVALID_STATE; @@ -282,7 +283,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay() DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING"); uint32_t cmd = 1; - BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &cmd, pdMS_TO_TICKS(200)); + BaseType_t ret = xQueueSend(dq, &cmd, pdMS_TO_TICKS(200)); if (ret != pdTRUE) { DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败"); m_sysState = OTA_SYS_GD32_STORED; @@ -341,6 +342,28 @@ void OtaCoordinator::onRelayComplete(bool success) notifyRelayComplete(success); } +// ============================================================ +// BLE 射频共存管理(L1: 消除 ble_ota → wifi_ota 直接依赖) +// ============================================================ +void OtaCoordinator::setBleCoexListener(IBleCoexListener *listener) +{ + m_bleCoexListener = listener; +} + +void OtaCoordinator::notifyBleCoexStart() +{ + if (m_bleCoexListener) { + m_bleCoexListener->onBleOtaStart(); + } +} + +void OtaCoordinator::notifyBleCoexEnd() +{ + if (m_bleCoexListener) { + m_bleCoexListener->onBleOtaEnd(); + } +} + // ============================================================ // ESP32 OTA 阶段 // ============================================================ @@ -511,10 +534,10 @@ void OtaCoordinator::abortCurrent() ota_system_state_t OtaCoordinator::getSystemState() const { // 对于纯读取,短暂加锁保证一致性 - OtaCoordinator* self = const_cast(this); - bool h = self->lock(); + // ★ M1/L3: m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast + bool h = lock(); ota_system_state_t s = m_sysState; - self->unlock(h); + unlock(h); return s; } @@ -569,10 +592,10 @@ bool OtaCoordinator::getGd32AppAlive() const /** @brief 获取 GD32 固件是否已存储完成(线程安全)。 */ bool OtaCoordinator::isGd32StorageDone() const { - OtaCoordinator* self = const_cast(this); - bool h = self->lock(); + // ★ M1/L3: m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast + bool h = lock(); bool done = m_gd32StorageDone; - self->unlock(h); + unlock(h); return done; } diff --git a/main/ota/ota_coordinator.h b/main/ota/ota_coordinator.h index d4b8f0a..0468577 100644 --- a/main/ota/ota_coordinator.h +++ b/main/ota/ota_coordinator.h @@ -37,6 +37,25 @@ typedef enum { OTA_SYS_ERROR // 错误状态 } ota_system_state_t; +// ============================================================ +// IBleCoexListener - BLE 射频共存通知接口(解决 L1 横向耦合) +// ============================================================ +// WifiOta 实现此接口并注册到 OtaCoordinator, +// BLE OTA 开始/结束时通过协调器通知 WiFi 层, +// 消除 ble_ota → wifi_ota 的直接依赖。 +/** + * @brief BLE 射频共存监听器接口。 + * BLE OTA 开始/结束时,协调器通过此接口通知 WiFi 层暂停/恢复射频活动。 + */ +class IBleCoexListener { +public: + virtual ~IBleCoexListener() = default; + /** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询等射频活动。 */ + virtual void onBleOtaStart() = 0; + /** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */ + virtual void onBleOtaEnd() = 0; +}; + /** * @brief ESP32 OTA 收尾结果(由 finalizeForReboot 返回)。 * err 为 ESP_OK 时 device_sha_hex 有效;SHA 不匹配时也填充 device_sha_hex 供前端核对。 @@ -295,6 +314,23 @@ public: */ void onRelayComplete(bool success) override; + // ── BLE 射频共存管理(L1: 替代 ble_ota → wifi_ota 直接调用)── + /** + * @brief 注册 BLE 共存监听器(由 main 在初始化时调用)。 + * @param listener WifiOta 实现的监听器指针,nullptr 取消注册。 + */ + void setBleCoexListener(IBleCoexListener *listener); + + /** + * @brief BLE OTA 开始时调用,通知 WiFi 层暂停 RSSI 轮询。 + */ + void notifyBleCoexStart(); + + /** + * @brief BLE OTA 结束时调用,通知 WiFi 层恢复射频活动。 + */ + void notifyBleCoexEnd(); + private: OtaCoordinator() = default; ~OtaCoordinator() = default; @@ -306,8 +342,8 @@ private: // 旧实现用共享 bool m_locked 记录 take 结果,多任务并发 lock() 时 // 超时方会把 m_locked 置 false,覆盖持锁方的 true,导致 unlock() 跳过 give, // mutex 永久死锁 → 后续 take 超时触发 vTaskPriorityDisinherit assert → PANIC。 - bool lock(); - void unlock(bool handle); + bool lock() const; + void unlock(bool handle) const; // ★ 卡死自愈检查:若忙状态停留超过 watchdog 超时,强制转为 ERROR。 // 用于在中继 task 因任何原因(崩溃/SPI 死锁/看门狗复位残留)未能回调 @@ -324,8 +360,11 @@ private: // GD32 存储跟踪 bool m_gd32StorageDone = false; - // 互斥锁 - SemaphoreHandle_t m_mutex = nullptr; + // 互斥锁(M1/L3: mutable 允许在 const 方法中加锁,消除 const_cast) + mutable SemaphoreHandle_t m_mutex = nullptr; + + // BLE 共存监听器(L1: 由 WifiOta 实现并注册) + IBleCoexListener *m_bleCoexListener = nullptr; static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100); // ★ 忙状态 watchdog 超时。GD32 中继理论最大耗时 ≈ 60s(全局超时) + 15s(APP存活轮询) + 1s(bootloader) ≈ 76s, diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index c33669a..5d6dff8 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -52,6 +52,17 @@ IRelayListener* OtaRelay::getRelayListener() return s_relayListener; } +// ── 队列访问器(M3: 封装 public static 成员)── +QueueHandle_t OtaRelay::getDispatchQueue() +{ + return dispatchQueue; +} + +QueueHandle_t OtaRelay::getResultQueue() +{ + return resultQueue; +} + // ============================================================ // 状态表:将每个 relay_state_t 映射到对应的 handler // ============================================================ diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index 74f1702..8e17f8e 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -186,6 +186,20 @@ public: */ static IRelayListener* getRelayListener(); + // ── 队列访问器(解决 M3: 封装 public static 成员)── + /** + * @brief 获取事件分发队列句柄。 + * 外部模块(OtaCoordinator / BLE)通过 getter 访问,不再直接引用静态成员。 + * @return dispatchQueue 句柄,未初始化时为 NULL。 + */ + static QueueHandle_t getDispatchQueue(); + + /** + * @brief 获取结果回传队列句柄。 + * @return resultQueue 句柄,未初始化时为 NULL。 + */ + static QueueHandle_t getResultQueue(); + // ── Flash 操作已迁移至 Gd32FlashStore(storage/gd32_flash_store.h)── // ── 公开状态成员 ── @@ -194,8 +208,6 @@ public: 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) @@ -230,6 +242,10 @@ public: static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36; private: + // ── 队列(M3: 由 getter 访问,外部不再直接引用)── + static QueueHandle_t dispatchQueue; ///< 事件分发队列 + static QueueHandle_t resultQueue; ///< 结果回传队列 + OtaRelay() = default; ~OtaRelay() = default; OtaRelay(const OtaRelay&) = delete; diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index 4537ce4..47f8e76 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -38,9 +38,7 @@ // SHA-256 计算已抽离到 Esp32OtaCore,本文件不再直接引用 mbedtls #include "app_config.h" -#include "ota_mutex.h" #include "ble_ota.h" -#include "wifi_ota.h" #include "esp32_ota_core.h" #include "ota_coordinator.h" #include "ota_relay.h" @@ -234,7 +232,8 @@ static void ble_ota_boost_wifi_ps() { if (s_wifi_ps_boosted) return; s_wifi_ps_boosted = true; - WifiOta::instance().setBleOtaActive(true); // ★ 暂停 WiFi RSSI 轮询 + // ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta) + OtaCoordinator::instance().notifyBleCoexStart(); DEBUG_INFO(TAG, "BLE OTA 进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)"); } @@ -245,7 +244,8 @@ static void ble_ota_restore_wifi_ps() { if (!s_wifi_ps_boosted) return; s_wifi_ps_boosted = false; - WifiOta::instance().setBleOtaActive(false); // ★ 恢复 WiFi RSSI 轮询 + // ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta) + OtaCoordinator::instance().notifyBleCoexEnd(); DEBUG_INFO(TAG, "BLE OTA 结束:恢复 WiFi RSSI 轮询"); } @@ -571,12 +571,10 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size) */ static void ble_esp32_ota_abort() { - // ★ 通知协调器释放 ESP32 通道占用(m_sysState → IDLE), - // 否则后续 begin() 会因协调器仍记为 ESP32_RECEIVING 而假拒绝。 - ota_system_state_t cur = OtaCoordinator::instance().getSystemState(); - if (cur == OTA_SYS_ESP32_RECEIVING || cur == OTA_SYS_ESP32_FINALIZING) { - OtaCoordinator::instance().abortCurrent(); - } + // ★ 无条件通知协调器释放通道占用(m_sysState → IDLE)。 + // abortCurrent() 对 IDLE 状态是空操作,对其他状态均复位到 IDLE, + // 确保后续 begin() 不会被残留的 ESP32_RECEIVING / ERROR 等状态假拒绝。 + OtaCoordinator::instance().abortCurrent(); Esp32OtaCore::instance().abort(); BleOta::instance().m_esp32RebootPending = false; // ★ 释放 reorder buffer @@ -672,8 +670,9 @@ static void ble_gd32_relay_wait_task(void *pvParameters) uint32_t result_msg = (uint32_t)OTA_RESULT_ERR_TIMEOUT; bool got_result = false; - if (OtaRelay::resultQueue) { - got_result = (xQueueReceive(OtaRelay::resultQueue, &result_msg, pdMS_TO_TICKS(120000)) == pdTRUE); + QueueHandle_t rq = OtaRelay::getResultQueue(); + if (rq) { + got_result = (xQueueReceive(rq, &result_msg, pdMS_TO_TICKS(120000)) == pdTRUE); } if (!got_result) { @@ -783,9 +782,9 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, DEBUG_INFO(TAG, "GD32 固件就绪且 SHA-256 匹配 (%" PRIu32 " / %" PRIu32 " 字节),通过协调器触发中继", meta.stored_size, meta.total_size); - if (OtaRelay::resultQueue) { + if (OtaRelay::getResultQueue()) { uint32_t stale = 0; - while (xQueueReceive(OtaRelay::resultQueue, &stale, 0) == pdTRUE) { + while (xQueueReceive(OtaRelay::getResultQueue(), &stale, 0) == pdTRUE) { DEBUG_WARN(TAG, "清理旧 GD32 中继结果: %" PRIu32, stale); } } @@ -1020,13 +1019,28 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len) DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size); return; } - // ★ 幂等保护:若已有活跃 ESP32 OTA 会话,说明这是客户端因 ACK 超时 - // 重传的 seq=0(同一会话续传)。若再次走 begin,会被协调器以 - // "OTA 已 ESP32_RECEIVING" 拒绝 → 误报"被其他通道占用"并中止整条 - // OTA 流程。此处跳过 begin 复用现有会话,后续 reorder buffer 会 - // 按 seq 去重(seq=0 < expected 时作为重复包丢弃)。 + // ★ 会话判定:若已有活跃 ESP32 OTA 会话,根据已接收字节数区分两种情况: + // ① m_received == 0:seq=0 尚未写入 flash,说明是客户端 ACK 超时后 + // 重传的 seq=0(同一会话续传),跳过 begin。 + // ② m_received > 0:seq=0 已写入且有后续数据,说明这是新的上传会话 + // (前端 abort 后 BLE 断开未完成就发起了新传输),需 abort 旧会话 + // 再 begin,否则 reorder buffer 以旧 expected 去重导致全部丢弃。 if (Esp32OtaCore::instance().isActive()) { - DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin"); + if (Esp32OtaCore::instance().getReceived() > 0) { + DEBUG_WARN(TAG, "ESP32 OTA 新会话检测: received=%u > 0, " + "中止旧会话并重新开始", + (unsigned)Esp32OtaCore::instance().getReceived()); + ble_esp32_ota_abort(); + esp_err_t err = ble_esp32_ota_begin(total_size); + if (err != ESP_OK) { + if (err == ESP_ERR_INVALID_STATE) { + sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0); + } + return; + } + } else { + DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin"); + } } else { // 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成 esp_err_t err = ble_esp32_ota_begin(total_size); diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index 9827428..22082b8 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -24,6 +24,7 @@ #include "health_monitor.h" #include "monitor/debug_log.h" #include "spi_master.h" +#include "canfd_bridge.h" #include "ble_ota.h" static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U; @@ -180,7 +181,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req) DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len); esp_wifi_set_ps(WIFI_PS_NONE); - WifiOta::instance().setBleOtaActive(false); + // ★ L1: WiFi OTA 开始时恢复 BLE 共存状态(通过协调器) + OtaCoordinator::instance().notifyBleCoexEnd(); char peek; int peek_ret = httpd_req_recv(req, &peek, 1); @@ -563,7 +565,7 @@ esp_err_t can_send_handler(httpd_req_t *req) cJSON_Delete(root); uint8_t resp_data[CAN_MAX_DLC] = {0}; - int resp_len = SpiMaster::instance().canSend(ch, ext_id, tx_data, tx_dlc, + int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc, resp_data, sizeof(resp_data), 1000); char resp[512]; diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index df99e45..938a47f 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -22,7 +22,6 @@ #include "esp_mac.h" #include "app_config.h" -#include "ota_mutex.h" #include "wifi_ota.h" #include "esp32_ota_core.h" #include "ota_coordinator.h" @@ -149,6 +148,8 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base, inst.setStaRetryCount(0); inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志 inst.stateUnlock(); + // ★ M2: 推送 WiFi 连接状态给 HealthMonitor(解耦监控层主动查询) + HealthMonitor::instance().setWifiConnected(true); if (inst.m_staConnectedSem) { xSemaphoreGive(inst.m_staConnectedSem); } @@ -161,6 +162,8 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base, bool exhausted = inst.getStaRetryExhausted(); int retryCount = inst.getStaRetryCount(); inst.stateUnlock(); + // ★ M2: 推送 WiFi 断开状态给 HealthMonitor + HealthMonitor::instance().setWifiConnected(false); if (!exhausted) { DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", @@ -602,6 +605,19 @@ bool WifiOta::isConnected() return WifiOta::instance().getStaConnected(); } +// ============================================================ +// IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知) +// ============================================================ +void WifiOta::onBleOtaStart() +{ + setBleOtaActive(true); +} + +void WifiOta::onBleOtaEnd() +{ + setBleOtaActive(false); +} + // ---------------------------------------- // ★ WiFi 早期初始化(在 app_main 调度器启动前调用) // 包括:NVS → netif → event handler → WiFi driver → 配置 → connect diff --git a/main/server/wifi_ota.h b/main/server/wifi_ota.h index 921d36b..865e8b4 100644 --- a/main/server/wifi_ota.h +++ b/main/server/wifi_ota.h @@ -14,13 +14,15 @@ #include "freertos/task.h" #include +#include "ota/ota_coordinator.h" // IBleCoexListener 接口定义 + /** @brief WiFi 工作模式枚举,区分 AP(设备热点)和 STA(连接路由器)模式。 */ typedef enum { OTA_WIFI_MODE_AP, ///< AP 模式:设备作为热点供局域网设备连接 OTA_WIFI_MODE_STA ///< STA 模式:设备连接上级路由器 } ota_wifi_mode_t; -class WifiOta { +class WifiOta : public IBleCoexListener { public: /** * @brief 获取 WifiOta 单例实例。 @@ -93,6 +95,12 @@ public: /** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */ void setBleOtaActive(bool v) { m_bleOtaActive.store(v); } + // ── IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)── + /** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询。 */ + void onBleOtaStart() override; + /** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */ + void onBleOtaEnd() override; + /** @brief 获取 WiFi OTA 任务句柄。 */ TaskHandle_t getTaskHandle() const { return m_taskHandle; } diff --git a/main/transport/canfd_bridge.cpp b/main/transport/canfd_bridge.cpp new file mode 100644 index 0000000..ff0e1e0 --- /dev/null +++ b/main/transport/canfd_bridge.cpp @@ -0,0 +1,69 @@ +/* + * canfd_bridge.cpp - CANFD 3通道通信桥接实现(M4: 从 SpiMaster 提取) + */ +#include +#include "canfd_bridge.h" +#include "spi_master.h" + +/** + * @brief 发送一帧 CANFD 数据,可选等待并读取响应。 + * 通过 SpiMaster 的公共 syncTransfer() 完成 SPI 传输。 + * 注意: syncTransfer 内部会加 SPI 互斥锁,FreeRTOS mutex 为递归锁故安全。 + */ +int canFdSend(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc, + uint8_t *recv_buf, uint8_t recv_max, + uint32_t timeout_ms) +{ + if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { + return -1; + } + + uint8_t tx_buf[72]; + size_t tx_len = 0; + + tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF); + tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF); + memcpy(&tx_buf[tx_len], data, dlc); + tx_len += dlc; + + esp_err_t ret = SpiMaster::instance().syncTransfer( + 0, tx_buf, tx_len, recv_buf, recv_max, timeout_ms); + if (ret != ESP_OK) { + return -1; + } + + return (int)recv_max; +} + +/** + * @brief 发送一帧无需响应的 CANFD 数据。 + */ +int canFdSendNoReply(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc) +{ + if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { + return -1; + } + + uint8_t tx_buf[72]; + size_t tx_len = 0; + + tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF); + tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF); + tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF); + memcpy(&tx_buf[tx_len], data, dlc); + tx_len += dlc; + + esp_err_t ret = SpiMaster::instance().send(0, tx_buf, tx_len); + if (ret != ESP_OK) { + return -1; + } + + return 0; +} diff --git a/main/transport/canfd_bridge.h b/main/transport/canfd_bridge.h new file mode 100644 index 0000000..441ee72 --- /dev/null +++ b/main/transport/canfd_bridge.h @@ -0,0 +1,64 @@ +/* + * canfd_bridge.h - CANFD 3通道通信桥接(M4: 从 SpiMaster 提取) + * + * 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责解耦。 + * 通过 SpiMaster 的公共 SPI 接口完成底层传输。 + */ +#ifndef CANFD_BRIDGE_H +#define CANFD_BRIDGE_H + +#include + +// ============================================================ +// CANFD 帧格式(3通道) +// [通道编号: 1B] [扩展帧ID: 4B BE] [数据: 1~64B] +// ============================================================ +#define CAN_EXTENDED_ID_BITS (29) ///< CAN 扩展帧 ID 位数 +#define CAN_MAX_DLC (64) ///< 最大数据长度 +#define CAN_CHANNEL_MASK (0x03) ///< 通道号掩码 (0~2) + +// 通道定义 +typedef enum { + CAN_CHANNEL_0 = 0, + CAN_CHANNEL_1 = 1, + CAN_CHANNEL_2 = 2, + CAN_CHANNEL_MAX = 3, +} can_channel_t; + +// CANFD 帧结构 +typedef struct { + can_channel_t channel; ///< CAN 通道 (0~2) + uint32_t ext_id; ///< 扩展帧 ID (29bit, Big-Endian 存储) + uint8_t data[CAN_MAX_DLC]; ///< CAN 数据负载 (最大 64 字节) + uint8_t dlc; ///< 数据长度 (1~64) +} can_frame_t; + +/** + * @brief 发送一帧 CANFD 数据,可选等待并读取响应。 + * 内部通过 SpiMaster 的公共 SPI 接口完成传输。 + * @param channel CAN 通道编号(0~2)。 + * @param ext_id 扩展帧 ID(29 位)。 + * @param data CAN 数据负载。 + * @param dlc 数据长度。 + * @param recv_buf 接收数据缓冲区(可为 nullptr)。 + * @param recv_max 接收缓冲区最大长度。 + * @param timeout_ms 超时时间(毫秒)。 + * @return 成功时返回接收数据长度,失败返回 -1。 + */ +int canFdSend(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc, + uint8_t *recv_buf, uint8_t recv_max, + uint32_t timeout_ms); + +/** + * @brief 发送一帧无需响应的 CANFD 数据。 + * @param channel CAN 通道编号(0~2)。 + * @param ext_id 扩展帧 ID(29 位)。 + * @param data CAN 数据负载。 + * @param dlc 数据长度。 + * @return 成功返回 0,失败返回 -1。 + */ +int canFdSendNoReply(can_channel_t channel, uint32_t ext_id, + const uint8_t *data, uint8_t dlc); + +#endif // CANFD_BRIDGE_H diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index f6cca3b..0ab6454 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -14,10 +14,12 @@ #include "driver/spi_master.h" #include "soc/spi_struct.h" #include "esp_err.h" +#include "esp_log.h" #include "spi_master.h" #include "monitor/debug_log.h" + static constexpr size_t kSpiTransferMaxBytes = 256U; // ============================================================ @@ -724,83 +726,7 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload, return ESP_ERR_TIMEOUT; } -// ============================================================ -// CANFD 接口 -// ============================================================ -/** - * @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。 - * @param channel CAN 通道编号(0~2)。 - * @param ext_id 扩展帧 ID(29 位)。 - * @param data CAN 数据负载。 - * @param dlc 数据长度。 - * @param recv_buf 接收数据缓冲区。 - * @param recv_max 接收缓冲区最大长度。 - * @param timeout_ms 超时时间(毫秒)。 - * @return 成功时返回接收数据长度,失败返回 -1。 - */ -int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, - uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms) -{ - LockGuard lock(m_mutex); - if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { - return -1; - } - - uint8_t tx_buf[72]; - size_t tx_len = 0; - - tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK); - tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF); - tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF); - tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF); - tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF); - memcpy(&tx_buf[tx_len], data, dlc); - tx_len += dlc; - - esp_err_t ret = syncTransferUnlocked(0, tx_buf, tx_len, recv_buf, recv_max, timeout_ms); - if (ret != ESP_OK) { - return -1; - } - - return (int)recv_max; -} - -/** - * @brief 发送一帧无需响应的 CANFD 数据。 - * @param channel CAN 通道编号(0~2)。 - * @param ext_id 扩展帧 ID(29 位)。 - * @param data CAN 数据负载。 - * @param dlc 数据长度。 - * @return 成功返回 0,失败返回 -1。 - */ -int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc) -{ - LockGuard lock(m_mutex); - if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) { - return -1; - } - - uint8_t tx_buf[72]; - size_t tx_len = 0; - - tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK); - tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF); - tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF); - tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF); - tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF); - memcpy(&tx_buf[tx_len], data, dlc); - tx_len += dlc; - - esp_err_t ret = sendUnlocked(0, tx_buf, tx_len); - if (ret != ESP_OK) { - return -1; - } - - return 0; -} +// CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦) // ============================================================ // 无锁内部版本实现 diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index 990c829..e850f06 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -30,30 +30,7 @@ #define SPI_OTA_RSP_WAIT (0x81) #define SPI_OTA_RSP_ERROR (0x82) -// ============================================================ -// CANFD 帧格式(3通道) -// [通道编号: 1B] [扩展帧ID: 4B BE] [数据: 1~64B] -// 长度自动识别,无需发送 DLC -// ============================================================ -#define CAN_EXTENDED_ID_BITS (29) // CAN 扩展帧 ID 位数 -#define CAN_MAX_DLC (64) // 最大数据长度 -#define CAN_CHANNEL_MASK (0x03) // 通道号掩码 (0~2) - -// 通道定义 -typedef enum { - CAN_CHANNEL_0 = 0, - CAN_CHANNEL_1 = 1, - CAN_CHANNEL_2 = 2, - CAN_CHANNEL_MAX = 3, -} can_channel_t; - -// CANFD 帧结构 -typedef struct { - can_channel_t channel; // CAN 通道 (0~2) - uint32_t ext_id; // 扩展帧 ID (29bit, Big-Endian 存储) - uint8_t data[CAN_MAX_DLC]; // CAN 数据负载 (最大 64 字节) - uint8_t dlc; // 数据长度 (1~64) -} can_frame_t; +// CANFD 类型已迁移至 transport/canfd_bridge.h(M4 解耦) // GD32 版本信息(从 APP PING 响应解析) typedef struct { @@ -122,19 +99,7 @@ public: size_t payload_len, uint8_t *recv_buf, size_t recv_len, int max_retries); - // ============================================================ - // CANFD 接口 - // ============================================================ - - /** @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。 */ - int canSend(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, - uint8_t *recv_buf, uint8_t recv_max, - uint32_t timeout_ms); - - /** @brief 发送一帧无需响应的 CANFD 数据。 */ - int canSendNoReply(can_channel_t channel, uint32_t ext_id, - const uint8_t *data, uint8_t dlc); + // ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)── /** @brief 获取 SPI 设备句柄(供外部状态查询)。 */ spi_device_handle_t deviceHandle() const { return m_spiDevice; } diff --git a/tools/app.js b/tools/app.js index 6a83abd..527cc41 100644 --- a/tools/app.js +++ b/tools/app.js @@ -27,6 +27,9 @@ let latestSystemStatus = null; let isEsp32Rebooting = false; let rebootStartTime = 0; let pollingPausedForUpload = false; +let activeXhr = null; // 当前上传 XHR,用于 abort +let uploadBtnOrigText = ''; // 上传按钮原始文字 +let activeUploadChip = null; // 当前上传目标芯片 ('esp32' | 'gd32' | null) // ==================== 折叠面板 ==================== function toggleCollapse(headEl) { @@ -111,6 +114,67 @@ function updateEsp32StagesError() { if (statusText) statusText.textContent = '校验/应用失败'; } +/** + * @description OTA 失败后的统一清理函数。 + * 所有上传失败、校验失败、中继触发失败等异常路径都应调用此函数, + * 确保 UI 状态完整重置,不会残留"上传中"或中间态。 + * @param {string} reason 失败原因描述 + */ +function cleanupAfterOtaFailure(reason) { + // 1. 重置上传锁 + isUploading = false; + + // 2. 中止正在进行的 XHR(如有) + if (activeXhr) { + try { activeXhr.abort(); } catch (_) {} + activeXhr = null; + } + + // 3. 标记阶段指示器为错误态 + if (currentOtaTarget === 'esp32') { + updateEsp32StagesError(); + } else if (currentOtaTarget === 'gd32') { + updateStagesFromRelayState(255); + } + + // 4. 状态栏标红 + var sb = document.getElementById('stageStatusBar'); + var st = document.getElementById('stageStatusText'); + if (sb) sb.className = 'stage-status-bar err'; + if (st) st.textContent = reason || '失败'; + + // 5. 进度条显示失败信息 + setProgress(0, reason || '刷写失败'); + + // 6. 重置活跃阶段 + setActivePhase(null); + + // 7. 清除 OTA 目标(下次操作时重新设置) + currentOtaTarget = null; + + // 8. 隐藏校验面板 + var verifyInfo = document.getElementById('stageVerifyInfo'); + if (verifyInfo) verifyInfo.style.display = 'none'; + + // 9. 恢复上传按钮(基于实际上传的芯片,而非当前 UI 选中的标签) + var chip = activeUploadChip || window.flashTarget || 'esp32'; + var btnId = (chip === 'gd32') ? 'uploadGd32Btn' : 'uploadEsp32Btn'; + var btn = document.getElementById(btnId); + if (btn && uploadBtnOrigText) { + btn.disabled = false; + btn.textContent = uploadBtnOrigText; + } + activeUploadChip = null; + + updateUploadButtonLabel(); + + // 10. 恢复轮询 + resumePollingAfterUpload(); + + // 11. 日志 + addLog('E', 'OTA', '刷写流程已中止: ' + (reason || '未知原因')); +} + function showVerifyActions(type) { // 已移除确认/重试/取消按钮:SHA-256 校验通过后自动重启,校验失败仅记录日志 void type; @@ -631,10 +695,15 @@ function updateSystemStatusUi(data) { var displayRelayState = isGd32Complete ? 5 : (isGd32Relaying && rs === 0 ? 1 : rs); if (currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); } - if (isGd32Error) { - setActivePhase('gd32Ota'); - setOtaProgressBadge(0); - setProgress(0, 'GD32 OTA 失败'); + if (isGd32Error && currentOtaTarget === 'gd32') { + cleanupAfterOtaFailure('GD32 OTA 失败'); + } else if (isGd32Error) { + // 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理 + updateStagesFromRelayState(255); + var sbErr = document.getElementById('stageStatusBar'); + var stErr = document.getElementById('stageStatusText'); + if (sbErr) sbErr.className = 'stage-status-bar err'; + if (stErr) stErr.textContent = 'GD32 错误(待机)'; } else if (isGd32Complete || rs === 5 || (gd32AppAlive && sysState === 'GD32_COMPLETE')) { markPhaseDone('gd32Ota'); setOtaProgressBadge(100); @@ -1134,39 +1203,46 @@ async function startUpload(chip) { // 狂点 WiFi 上传按钮时,多个 startUpload 都能通过守卫,最终并发 xhr.send, // 设备端互斥被绕过、403 路径走漏。 isUploading = true; - // 跨通道互斥前置检查 - try { - var probe = await fetch(getBaseUrl() + '/system/status', { - signal: AbortSignal.timeout(2000) - }); - if (probe.ok) { - var st = await probe.json(); - var ota = st.ota || {}; - var ssc = Number.isFinite(ota.sys_state_code) ? ota.sys_state_code : -1; - // ESP32 三种活跃态(5=RECEIVING, 6=FINALIZING, 7=REBOOTING) - var esp32Busy = (ssc === 5 || ssc === 6 || ssc === 7); - // GD32 存储/中继(1=STORING, 3=RELAYING)→ 阻止新上传,避免 NVS 与 Flash 互踩 - var gd32Busy = (ssc === 1 || ssc === 3); - // 同通道(chip=esp32)已在进行中:本通道会重入;不做阻拦 - // 另一通道活跃:拒绝 - if (chip === 'esp32' && (esp32Busy || gd32Busy)) { - addLog('E', '上传', '设备已有 OTA 进行中 (sys_state=' + (ota.sys_state || ssc) + '),拒绝本次 ESP32 上传'); - showToast('设备正在 OTA 中,请等待结束后再试', 'error'); - isUploading = false; - resumePollingAfterUpload(); - return; + activeUploadChip = chip; + // 跨通道互斥前置检查(含 BLE 取消后的瞬态重试) + var _probeReady = false; + for (var _pi = 0; _pi < 4; _pi++) { + if (_pi > 0) { + var _pd = [1500, 3000, 3000][_pi - 1]; + addLog('I', '上传', '等待设备清理 OTA 状态 (' + _pd + 'ms)...'); + await new Promise(function(r) { setTimeout(r, _pd); }); + } + try { + var probe = await fetch(getBaseUrl() + '/system/status', { + signal: AbortSignal.timeout(2000) + }); + if (probe.ok) { + var st = await probe.json(); + var ota = st.ota || {}; + var ssc = Number.isFinite(ota.sys_state_code) ? ota.sys_state_code : -1; + // ★ RECEIVING (5) 可能是刚取消的 BLE OTA 残留态(设备端 GAP disconnect + // 处理需 3-4 秒),等它清理完;FINALIZING(6)/REBOOTING(7) 是真实忙,直接拒绝。 + if (ssc === 5 && _pi < 3) continue; + // ESP32 活跃态(5=RECEIVING, 6=FINALIZING, 7=REBOOTING) + var esp32Busy = (ssc === 5 || ssc === 6 || ssc === 7); + // GD32 存储/中继(1=STORING, 3=RELAYING)→ 阻止新上传,避免 NVS 与 Flash 互踩 + var gd32Busy = (ssc === 1 || ssc === 3); + if ((esp32Busy || gd32Busy)) { + addLog('E', '上传', '设备已有 OTA 进行中 (sys_state=' + (ota.sys_state || ssc) + '),拒绝本次 ' + chip.toUpperCase() + ' 上传'); + showToast('设备正在 OTA 中,请等待结束后再试', 'error'); + isUploading = false; + resumePollingAfterUpload(); + return; + } } - if (chip === 'gd32' && (esp32Busy || gd32Busy)) { - addLog('E', '上传', '设备已有 OTA 进行中 (sys_state=' + (ota.sys_state || ssc) + '),拒绝本次 GD32 上传'); - showToast('设备正在 OTA 中,请等待结束后再试', 'error'); - isUploading = false; - resumePollingAfterUpload(); - return; - } + _probeReady = true; + break; + } catch (e) { + // 探测失败:可能是设备瞬时忙或网络问题。保守继续走,由后端 403 兜底。 + addLog('W', '上传', '跨通道互斥状态探测失败: ' + e.message + '(继续尝试,由设备端兜底)'); + _probeReady = true; + break; } - } catch (e) { - // 探测失败:可能是设备瞬时忙或网络问题。保守继续走,由后端 403 兜底。 - addLog('W', '上传', '跨通道互斥状态探测失败: ' + e.message + '(继续尝试,由设备端兜底)'); } // 每次上传前尝试重新从本地读取文件 var handle = chip === 'esp32' ? esp32FileHandle : gd32FileHandle; @@ -1206,7 +1282,7 @@ async function startUpload(chip) { } } var uploadBtn = document.getElementById(chip === 'esp32' ? 'uploadEsp32Btn' : 'uploadGd32Btn'); - var origText = uploadBtn.textContent; + uploadBtnOrigText = uploadBtn.textContent; uploadBtn.disabled = true; uploadBtn.textContent = '上传中...'; switchStageFlow(chip); @@ -1275,7 +1351,8 @@ async function startUpload(chip) { combined.set(new Uint8Array(payload), headerBytes.length); body = combined.buffer; } - var xhr = new XMLHttpRequest(); + activeXhr = new XMLHttpRequest(); + var xhr = activeXhr; xhr.upload.addEventListener('progress', function (e) { if (e.lengthComputable) { var pct = Math.round((e.loaded / e.total) * 100); @@ -1290,9 +1367,10 @@ async function startUpload(chip) { }); xhr.addEventListener('load', function () { isUploading = false; + activeXhr = null; resumePollingAfterUpload(); uploadBtn.disabled = false; - uploadBtn.textContent = origText; + uploadBtn.textContent = uploadBtnOrigText; updateUploadButtonLabel(); if (xhr.status === 200) { addLog('I', '上传', '固件上传完成!'); @@ -1312,33 +1390,33 @@ async function startUpload(chip) { } else if (xhr.status === 403) { addLog('E', '上传', 'ESP32 OTA 被其他通道占用(BLE/WiFi 互斥),请稍后重试'); showToast('ESP32 OTA 被其他通道占用,请稍后重试', 'error'); - setProgress(0, 'OTA 被其他通道占用'); + cleanupAfterOtaFailure('通道被占用'); } else { var detail = formatHttpErrorDetail(xhr.responseText); var message = '上传失败: HTTP ' + xhr.status + (detail ? ' - ' + detail : ''); addLog('E', '上传', message); showToast(detail ? ('上传失败: ' + detail) : '上传失败', 'error'); + cleanupAfterOtaFailure('HTTP ' + xhr.status); } }); xhr.addEventListener('error', function () { - isUploading = false; - resumePollingAfterUpload(); - uploadBtn.disabled = false; - uploadBtn.textContent = origText; - updateUploadButtonLabel(); - addLog('E', '上传', '上传过程发生错误'); - showToast('上传出错', 'error'); + if (activeXhr) { + cleanupAfterOtaFailure('网络错误'); + showToast('上传过程发生网络错误', 'error'); + } + }); + xhr.addEventListener('abort', function () { + if (activeXhr) { + cleanupAfterOtaFailure('已中止'); + showToast('上传已中止', 'error'); + } }); xhr.open('POST', url); xhr.send(body); } catch (e) { addLog('E', '上传', '上传失败: ' + e.message); - showToast('上传失败', 'error'); - isUploading = false; - resumePollingAfterUpload(); - uploadBtn.disabled = false; - uploadBtn.textContent = origText; - updateUploadButtonLabel(); + showToast('上传失败: ' + e.message, 'error'); + cleanupAfterOtaFailure('上传异常'); } } // ==================== ESP32 OTA 完成升级 ==================== @@ -1401,9 +1479,9 @@ async function verifyEsp32Firmware() { addLog('E', 'OTA', '校验失败: ' + detail); showToast('校验失败: ' + detail, 'error'); var resultEl = document.getElementById('sviResult'); - if (resultEl) { resultEl.textContent = '? ' + detail; resultEl.className = 'svi-val err'; } - updateEsp32StagesError(); + if (resultEl) { resultEl.textContent = '✗ ' + detail; resultEl.className = 'svi-val err'; } showVerifyActions('retry'); + cleanupAfterOtaFailure('校验失败'); } } catch (e) { addLog('W', 'OTA', '应用请求出现异常(设备可能已重启): ' + e.message); @@ -1430,10 +1508,15 @@ async function retryEsp32Upload() { } async function cancelEsp32Ota() { - addLog('I', 'OTA', '已取消 OTA 刷写'); + addLog('I', 'OTA', '用户取消 OTA 刷写'); showToast('已取消 OTA', 'error'); showVerifyActions('none'); - resetProgress(); + var xhr = activeXhr; + activeXhr = null; + if (xhr) { + try { xhr.abort(); } catch (_) {} + } + cleanupAfterOtaFailure('用户取消'); } async function applyEsp32Firmware() { @@ -1496,6 +1579,7 @@ async function triggerGd32Relay() { if (!sha256) { addLog('E', 'OTA', '缺少 GD32 文件 SHA-256,请先上传文件'); showToast('SHA-256 缺失', 'error'); + cleanupAfterOtaFailure('SHA-256 缺失'); return; } addLog('I', 'OTA', 'GD32 SHA-256: ' + sha256 + ' → 触发中继...'); @@ -1522,15 +1606,18 @@ async function triggerGd32Relay() { if (err.device) { addLog('E', 'OTA', 'GD32 SHA-256 不匹配: 设备=' + err.device + ' 前端=' + sha256); showToast('SHA-256 不匹配', 'error'); + cleanupAfterOtaFailure('SHA-256 不匹配'); return; } } catch (_) {} addLog('E', 'OTA', 'GD32 触发失败: HTTP ' + resp.status + ' ' + text); showToast('GD32 触发失败', 'error'); + cleanupAfterOtaFailure('触发失败 HTTP ' + resp.status); } } catch (e) { addLog('E', 'OTA', 'GD32 触发失败: ' + e.message); showToast('GD32 触发失败', 'error'); + cleanupAfterOtaFailure('触发异常'); } } diff --git a/tools/ble_ota.js b/tools/ble_ota.js index 7a67fbe..1f82d4a 100644 --- a/tools/ble_ota.js +++ b/tools/ble_ota.js @@ -1444,6 +1444,23 @@ bleLog('OTA', `写入失败: ${err.message}`, 'E'); setProgress(0, `写入失败: ${err.message}`); setBleStageError(err.message); + // ★ BLE OTA 失败/中止后强制断开 BLE 连接,并等待实际断连完成: + // 触发 ESP32 后端的 GAP disconnect 处理器 → ble_esp32_ota_abort(), + // 清理 OTA 会话(Esp32OtaCore::abort + reorder buffer 释放 + 协调器状态复位)。 + // 等待 gattserverdisconnected 事件确保断连真正生效,避免用户快速重试时 + // 旧会话尚未清理导致 seq=0 被误判为重传。 + if (state.server && state.device && state.device.gatt && state.device.gatt.connected) { + bleLog('OTA', '刷写失败,断开 BLE 以清理 ESP32 OTA 会话...', 'W'); + try { + await new Promise((resolve) => { + let done = false; + const finish = () => { if (!done) { done = true; resolve(); } }; + state.device.addEventListener('gattserverdisconnected', finish, { once: true }); + setTimeout(finish, 1000); // 1 秒兜底超时 + state.device.gatt.disconnect(); + }); + } catch (_) {} + } return false; } finally { if (state.ackWaiter) { @@ -1495,6 +1512,12 @@ const ok = await uploadFirmware(); if (!ok || state.abortRequested) { bleLog('AUTO', '一键刷写中止', 'W'); + // ★ 确保 BLE 已断开(触发 ESP32 OTA 会话清理) + // uploadFirmware catch 块已尝试断开,这里做安全网兜底 + if (state.device && state.device.gatt && state.device.gatt.connected) { + bleLog('AUTO', '断开 BLE 以清理 ESP32 OTA 会话...', 'W'); + try { state.device.gatt.disconnect(); } catch (_) {} + } return; } if (cfg.label === 'ESP32') {