refactor(main): 移除ota_mutex.h全局变量并重构ESP32 OTA互斥锁管理
- 将全局变量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类中管理
This commit is contained in:
@@ -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 # 高精度定时器
|
||||
)
|
||||
+2
-5
@@ -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 模式
|
||||
|
||||
+5
-8
@@ -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();
|
||||
|
||||
@@ -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 <cstdint>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 更新
|
||||
// ============================================================
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
+54
-20
@@ -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 锁(无活跃会话)");
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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<OtaCoordinator*>(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<OtaCoordinator*>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// ============================================================
|
||||
|
||||
+18
-2
@@ -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;
|
||||
|
||||
+34
-20
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
#include "freertos/task.h"
|
||||
#include <atomic>
|
||||
|
||||
#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; }
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* canfd_bridge.cpp - CANFD 3通道通信桥接实现(M4: 从 SpiMaster 提取)
|
||||
*/
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
// ============================================================
|
||||
// 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
|
||||
@@ -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 解耦)
|
||||
|
||||
// ============================================================
|
||||
// 无锁内部版本实现
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+143
-56
@@ -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('触发异常');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user