diff --git a/main/include/version.h b/main/include/version.h index d48ae68..cc79a6b 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -8,7 +8,7 @@ #define FW_VERSION_STRING "0.3.12" // 编译时间 -#define BUILD_TIME "2026-06-17 02:12:38" +#define BUILD_TIME "2026-06-17 03:09:19" // Git 信息 -#define GIT_HASH "5b2c5f8" +#define GIT_HASH "98b5cc1" diff --git a/main/monitor/debug_log.cpp b/main/monitor/debug_log.cpp index e7f226f..2ceb003 100644 --- a/main/monitor/debug_log.cpp +++ b/main/monitor/debug_log.cpp @@ -196,7 +196,7 @@ esp_err_t DebugLog::uartInit() } m_uartInitialized = true; - ESP_LOGI(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)", + ESP_LOGD(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)", UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_DEBUG_BAUD_RATE); return ESP_OK; diff --git a/main/ota/ota_coordinator.cpp b/main/ota/ota_coordinator.cpp index bfaf215..866804f 100644 --- a/main/ota/ota_coordinator.cpp +++ b/main/ota/ota_coordinator.cpp @@ -23,28 +23,26 @@ OtaCoordinator& OtaCoordinator::instance() // ============================================================ // 线程安全辅助 // ============================================================ -void OtaCoordinator::lock() +bool OtaCoordinator::lock() { - // ★ 修复:FreeRTOS mutex (xSemaphoreCreateMutex) 只能由 take 的 task give。 - // 超时后必须在 unlock() 中跳过 give,避免 xTaskPriorityDisinherit assert。 - // 此处假设 OtaCoordinator 内部不嵌套 lock(全局已确认:无嵌套调用)。 - m_locked = false; - if (m_mutex) { - m_locked = (xSemaphoreTake(m_mutex, kLockTimeout) == pdTRUE); - if (!m_locked) { - DEBUG_WARN(TAG, "lock() 超时 (%u ms),本次状态变更被跳过", - (unsigned)pdTICKS_TO_MS(kLockTimeout)); - } + // ★ 修复: 返回 take 结果,由调用方传回 unlock()。 + // 旧实现用成员 bool m_locked 记录 take 结果,多任务并发 lock() 时 + // 超时方会把 m_locked 置 false,覆盖持锁方的 true,导致 unlock() 跳过 give, + // mutex 永久死锁 → 后续 take 超时触发 vTaskPriorityDisinherit assert → PANIC。 + if (!m_mutex) return false; + bool ok = (xSemaphoreTake(m_mutex, kLockTimeout) == pdTRUE); + if (!ok) { + DEBUG_WARN(TAG, "lock() 超时 (%u ms),本次状态变更被跳过", + (unsigned)pdTICKS_TO_MS(kLockTimeout)); } + return ok; } -void OtaCoordinator::unlock() +void OtaCoordinator::unlock(bool handle) { - // ★ 修复:仅在 lock() 成功 take 时才 give。 - // 错误的 task 给同一 mutex give 会触发 xTaskPriorityDisinherit assert → PANIC。 - if (m_locked && m_mutex) { + // ★ 修复: 仅在对应的 lock() 成功 take 时才 give。 + if (handle && m_mutex) { xSemaphoreGive(m_mutex); - m_locked = false; } } @@ -119,51 +117,51 @@ void OtaCoordinator::init() // ============================================================ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size) { - lock(); + bool h = lock(); // ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程 - watchdogCheckLocked(); + if (h) watchdogCheckLocked(); // 系统忙检查:仅拒绝真正"在传输"的活跃状态 // STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖 - // ★ 修复:之前把所有非 IDLE 都当忙,导致"Flash 缓存里有固件但没在传输"时无法上传新固件 - if (m_sysState == OTA_SYS_ESP32_RECEIVING || + if (h && (m_sysState == OTA_SYS_ESP32_RECEIVING || m_sysState == OTA_SYS_ESP32_FINALIZING || m_sysState == OTA_SYS_ESP32_REBOOTING || m_sysState == OTA_SYS_GD32_STORING || - m_sysState == OTA_SYS_GD32_RELAYING) { + m_sysState == OTA_SYS_GD32_RELAYING)) { DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } // 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),先清理再开始新存储 - if (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE) { + if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) { DEBUG_INFO(TAG, "清理旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size); OtaRelay::metaClear(); } - m_sysState = OTA_SYS_GD32_STORING; - m_gd32StorageDone = false; - m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻 - - DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING", total_size); - unlock(); - return ESP_OK; + if (h) { + m_sysState = OTA_SYS_GD32_STORING; + m_gd32StorageDone = false; + m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻 + DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING", total_size); + } + unlock(h); + return h ? ESP_OK : ESP_ERR_INVALID_STATE; } void OtaCoordinator::completeGd32Storage() { - lock(); - if (m_sysState == OTA_SYS_GD32_STORING) { + bool h = lock(); + if (h && m_sysState == OTA_SYS_GD32_STORING) { m_sysState = OTA_SYS_GD32_STORED; m_gd32StorageDone = true; m_busySinceTick = 0; // ★ STORED 是非忙状态 DEBUG_INFO(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED"); - } else { + } else if (h) { DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState); } - unlock(); + unlock(h); } // ============================================================ @@ -171,59 +169,60 @@ void OtaCoordinator::completeGd32Storage() // ============================================================ esp_err_t OtaCoordinator::triggerGd32Relay() { - lock(); + bool h = lock(); // ★ 卡死自愈检查 - watchdogCheckLocked(); + if (h) watchdogCheckLocked(); // 校验:必须处于"已存储"或"已中继完成"状态 - // ★ 修复:COMPLETE(旧固件已成功推给 GD32,NVS 元数据已被 doFinishAndVerify 清掉, - // OtaRelay 状态为 DONE)也能触发——但需要先回滚到 STORED 以保证 m_busySinceTick 一致。 - // 通常用户应先上传新固件再触发中继,COMPLETE 时元数据已被清,metaLoad 会返回 stored=0 - // 并在 188 行被拒绝,自然不会走到这里。 - if (m_sysState != OTA_SYS_GD32_STORED && m_sysState != OTA_SYS_IDLE) { + if (h && m_sysState != OTA_SYS_GD32_STORED && m_sysState != OTA_SYS_IDLE) { if (m_sysState == OTA_SYS_GD32_STORING) { DEBUG_WARN(TAG, "GD32 正在存储中,拒绝触发中继"); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } if (m_sysState == OTA_SYS_GD32_RELAYING) { DEBUG_WARN(TAG, "GD32 中继已在进行中"); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } if (m_sysState == OTA_SYS_ESP32_RECEIVING || m_sysState == OTA_SYS_ESP32_FINALIZING || m_sysState == OTA_SYS_ESP32_REBOOTING) { DEBUG_WARN(TAG, "ESP32 OTA 进行中,拒绝中继触发"); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } } // 验证固件是否确实已存储(通过 NVS 元数据) ota_meta_t meta; - if (OtaRelay::metaLoad(&meta) != ESP_OK || + if (h && (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0 || - meta.stored_size < meta.total_size) { + meta.stored_size < meta.total_size)) { DEBUG_WARN(TAG, "GD32 固件未完整存储,拒绝触发中继"); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } // 检查 OtaRelay 是否空闲 - if (OtaRelay::getState() != RELAY_IDLE && + if (h && OtaRelay::getState() != RELAY_IDLE && OtaRelay::getState() != RELAY_DONE && OtaRelay::getState() != RELAY_ERROR) { DEBUG_WARN(TAG, "OtaRelay 正忙 (state=%d),拒绝触发", (int)OtaRelay::getState()); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } // 向 OtaRelay task 发送启动消息 - if (!OtaRelay::dispatchQueue) { + if (h && !OtaRelay::dispatchQueue) { DEBUG_ERR(TAG, "OtaRelay dispatchQueue 未就绪"); - unlock(); + unlock(h); + return ESP_ERR_INVALID_STATE; + } + + if (!h) { + unlock(h); return ESP_ERR_INVALID_STATE; } @@ -237,39 +236,41 @@ esp_err_t OtaCoordinator::triggerGd32Relay() DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败"); m_sysState = OTA_SYS_GD32_STORED; m_busySinceTick = 0; // 回退到 STORED(非忙) - unlock(); + unlock(h); return ESP_FAIL; } - unlock(); + unlock(h); return ESP_OK; } void OtaCoordinator::notifyRelayStarted() { - lock(); - if (m_sysState != OTA_SYS_GD32_RELAYING) { + bool h = lock(); + if (h && m_sysState != OTA_SYS_GD32_RELAYING) { m_sysState = OTA_SYS_GD32_RELAYING; m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻 DEBUG_INFO(TAG, "中继已开始, 系统状态 → GD32_RELAYING"); } - unlock(); + unlock(h); } void OtaCoordinator::notifyRelayComplete(bool success) { - lock(); - if (success) { - m_sysState = OTA_SYS_GD32_COMPLETE; - m_gd32StorageDone = false; - m_busySinceTick = 0; // ★ COMPLETE 非忙 - DEBUG_INFO(TAG, "GD32 中继完成, 系统状态 → GD32_COMPLETE"); - } else { - m_sysState = OTA_SYS_ERROR; - m_busySinceTick = 0; // ★ ERROR 非忙(可被后续请求恢复) - DEBUG_ERR(TAG, "GD32 中继失败, 系统状态 → ERROR"); + bool h = lock(); + if (h) { + if (success) { + m_sysState = OTA_SYS_GD32_COMPLETE; + m_gd32StorageDone = false; + m_busySinceTick = 0; // ★ COMPLETE 非忙 + DEBUG_INFO(TAG, "GD32 中继完成, 系统状态 → GD32_COMPLETE"); + } else { + m_sysState = OTA_SYS_ERROR; + m_busySinceTick = 0; // ★ ERROR 非忙(可被后续请求恢复) + DEBUG_ERR(TAG, "GD32 中继失败, 系统状态 → ERROR"); + } } - unlock(); + unlock(h); } // ============================================================ @@ -277,60 +278,65 @@ void OtaCoordinator::notifyRelayComplete(bool success) // ============================================================ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size) { - lock(); + bool h = lock(); // ★ 卡死自愈检查 - watchdogCheckLocked(); + if (h) watchdogCheckLocked(); // 系统忙检查 - // ★ 修复:STORING/RELAYING 都拒绝(防止中继中改 gd32_ota 分区被 ESP32 OTA 误触发)。 - // STORED/COMPLETE 允许——它们是非活跃状态,ESP32 OTA 与 gd32_ota 分区无冲突。 - if (m_sysState == OTA_SYS_GD32_STORING || - m_sysState == OTA_SYS_GD32_RELAYING) { + if (h && (m_sysState == OTA_SYS_GD32_STORING || + m_sysState == OTA_SYS_GD32_RELAYING)) { DEBUG_WARN(TAG, "GD32 正在 %s,拒绝 ESP32 OTA", m_sysState == OTA_SYS_GD32_STORING ? "存储" : "中继"); - unlock(); + unlock(h); return ESP_ERR_INVALID_STATE; } - // 委托 Esp32OtaCore 处理分区/互斥逻辑 - esp_err_t err = Esp32OtaCore::instance().begin(total_size); - if (err == ESP_OK) { - m_sysState = OTA_SYS_ESP32_RECEIVING; - m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻 - DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING", total_size); - } else { - DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err)); + esp_err_t err = ESP_ERR_INVALID_STATE; + if (h) { + // 委托 Esp32OtaCore 处理分区/互斥逻辑 + err = Esp32OtaCore::instance().begin(total_size); + if (err == ESP_OK) { + m_sysState = OTA_SYS_ESP32_RECEIVING; + m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻 + DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING", total_size); + } else { + DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err)); + } } - unlock(); + unlock(h); return err; } void OtaCoordinator::notifyEsp32ReceiveComplete() { - lock(); - if (m_sysState == OTA_SYS_ESP32_RECEIVING) { + bool h = lock(); + if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) { m_sysState = OTA_SYS_ESP32_FINALIZING; DEBUG_INFO(TAG, "ESP32 固件接收完毕, 系统状态 → ESP32_FINALIZING"); } - unlock(); + unlock(h); } void OtaCoordinator::notifyEsp32FinalizeComplete() { - lock(); - m_sysState = OTA_SYS_ESP32_REBOOTING; - DEBUG_INFO(TAG, "ESP32 OTA 收尾完成, 系统状态 → ESP32_REBOOTING"); - unlock(); + bool h = lock(); + if (h) { + m_sysState = OTA_SYS_ESP32_REBOOTING; + DEBUG_INFO(TAG, "ESP32 OTA 收尾完成, 系统状态 → ESP32_REBOOTING"); + } + unlock(h); } void OtaCoordinator::notifyEsp32Rebooting() { - lock(); - m_sysState = OTA_SYS_ESP32_REBOOTING; - DEBUG_INFO(TAG, "ESP32 即将重启, 系统状态 → ESP32_REBOOTING"); - unlock(); + bool h = lock(); + if (h) { + m_sysState = OTA_SYS_ESP32_REBOOTING; + DEBUG_INFO(TAG, "ESP32 即将重启, 系统状态 → ESP32_REBOOTING"); + } + unlock(h); } // ============================================================ @@ -338,23 +344,25 @@ void OtaCoordinator::notifyEsp32Rebooting() // ============================================================ void OtaCoordinator::abortCurrent() { - lock(); - DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState); + bool h = lock(); + if (h) { + DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState); - if (m_sysState == OTA_SYS_GD32_STORING) { - OtaRelay::metaClear(); - } else if (m_sysState == OTA_SYS_GD32_RELAYING) { - OtaRelay::instance().reset(); - } else if (m_sysState == OTA_SYS_ESP32_RECEIVING || - m_sysState == OTA_SYS_ESP32_FINALIZING) { - Esp32OtaCore::instance().abort(); + if (m_sysState == OTA_SYS_GD32_STORING) { + OtaRelay::metaClear(); + } else if (m_sysState == OTA_SYS_GD32_RELAYING) { + OtaRelay::instance().reset(); + } else if (m_sysState == OTA_SYS_ESP32_RECEIVING || + m_sysState == OTA_SYS_ESP32_FINALIZING) { + Esp32OtaCore::instance().abort(); + } + + m_sysState = OTA_SYS_IDLE; + m_gd32StorageDone = false; + m_busySinceTick = 0; // ★ 离开忙状态 + DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE"); } - - m_sysState = OTA_SYS_IDLE; - m_gd32StorageDone = false; - m_busySinceTick = 0; // ★ 离开忙状态 - DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE"); - unlock(); + unlock(h); } // ============================================================ @@ -364,9 +372,9 @@ ota_system_state_t OtaCoordinator::getSystemState() const { // 对于纯读取,短暂加锁保证一致性 OtaCoordinator* self = const_cast(this); - self->lock(); + bool h = self->lock(); ota_system_state_t s = m_sysState; - self->unlock(); + self->unlock(h); return s; } @@ -416,9 +424,9 @@ bool OtaCoordinator::getGd32AppAlive() const bool OtaCoordinator::isGd32StorageDone() const { OtaCoordinator* self = const_cast(this); - self->lock(); + bool h = self->lock(); bool done = m_gd32StorageDone; - self->unlock(); + self->unlock(h); return done; } @@ -427,10 +435,12 @@ bool OtaCoordinator::isGd32StorageDone() const // ============================================================ void OtaCoordinator::reset() { - lock(); - m_sysState = OTA_SYS_IDLE; - m_gd32StorageDone = false; - m_busySinceTick = 0; // ★ 复位 - DEBUG_INFO(TAG, "协调器已完全复位"); - unlock(); + bool h = lock(); + if (h) { + m_sysState = OTA_SYS_IDLE; + m_gd32StorageDone = false; + m_busySinceTick = 0; // ★ 复位 + DEBUG_INFO(TAG, "协调器已完全复位"); + } + unlock(h); } diff --git a/main/ota/ota_coordinator.h b/main/ota/ota_coordinator.h index c6b7d1d..a37f074 100644 --- a/main/ota/ota_coordinator.h +++ b/main/ota/ota_coordinator.h @@ -174,8 +174,12 @@ private: OtaCoordinator& operator=(const OtaCoordinator&) = delete; // 线程安全辅助 - void lock(); - void unlock(); + // ★ lock() 返回是否成功 take;unlock(handle) 据此决定是否 give。 + // 旧实现用共享 bool m_locked 记录 take 结果,多任务并发 lock() 时 + // 超时方会把 m_locked 置 false,覆盖持锁方的 true,导致 unlock() 跳过 give, + // mutex 永久死锁 → 后续 take 超时触发 vTaskPriorityDisinherit assert → PANIC。 + bool lock(); + void unlock(bool handle); // ★ 卡死自愈检查:若忙状态停留超过 watchdog 超时,强制转为 ERROR。 // 用于在中继 task 因任何原因(崩溃/SPI 死锁/看门狗复位残留)未能回调 @@ -194,7 +198,6 @@ private: // 互斥锁 SemaphoreHandle_t m_mutex = nullptr; - bool m_locked = false; // ★ 记录最近一次 lock() 是否成功 take;unlock() 据此跳过错误 give static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100); // ★ 忙状态 watchdog 超时。GD32 中继理论最大耗时 ≈ 60s(全局超时) + 15s(APP存活轮询) + 1s(bootloader) ≈ 76s, diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index ad64c4f..cb92e98 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -420,8 +420,6 @@ ota_result_t OtaRelay::forwardFirmware() } // 全局超时计算 - // ★ 收敛到 1 min:30.8 KB / 30 KB·s ≈ 1 s 数据 + 握手/认证/等待 ≈ 2~3 s, - // 1 min 已是数十倍安全余量。旧 10 min 会在中继异常时把协调器锁住极久。 constexpr uint32_t kMinGlobalTimeoutMs = 20U * 1000U; // 20 s 最小超时 constexpr uint32_t kMaxGlobalTimeoutMs = 60U * 1000U; // 60 s 硬上限 uint32_t dynamic_ms = kMinGlobalTimeoutMs + (meta.total_size / 1024U) * 50U; // 50 ms / KB @@ -444,10 +442,6 @@ ota_result_t OtaRelay::forwardFirmware() if (res != OTA_RESULT_OK) goto cleanup; // 步骤2: SIZE_NEG(内部完成大小协商 → TRANSFERRING) - // ★ 此处检查在 SIZE_NEG handler 执行之前。从日志看握手刚完成(~0.6 s)就触发超限, - // 真实根因:handleSizeNeg 是同步阻塞的——它本身就会跑完整个 doSendFirmwareSize, - // 真正的"步骤2 超限"应在 handler 之后检查。修复:把"前已超限"检查全部删除, - // 改为在 processEvent 之后用 deadline 校验返回值。handler 内部已经有自己的超时。 res = processEvent(RELAY_EVT_START, nullptr, 0); if (res != OTA_RESULT_OK) goto cleanup; diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index e55e0b7..a4be5b1 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -7,6 +7,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" +#include "esp_log.h" #include "esp_wifi.h" #include "esp_netif.h" #include "esp_http_server.h" @@ -1164,21 +1165,37 @@ static esp_err_t system_status_handler(httpd_req_t *req) gd32_version_t pingVer = {}; OtaRelay::getGd32Version(&pingVer); - /* 若尚无 GD32 版本信息且 OTA 中继空闲,尝试一次 SPI PING 获取 */ - if (pingVer.app_major == 0 && + /* 若尚无 GD32 版本信息且 OTA 中继空闲,尝试 SPI PING 获取(最多 5 次) */ + // ★ 修复:OTA 活跃期间(存储/中继/ESP32 OTA)跳过 appPing(), + // 避免 HTTP 任务长期持有 SPI mutex 与 relay 任务争用, + // 导致优先级继承异常 → vTaskPriorityDisinheritAfterTimeout assert。 + ota_system_state_t curSysState = OtaCoordinator::instance().getSystemState(); + bool ota_busy = (curSysState == OTA_SYS_GD32_STORING || + curSysState == OTA_SYS_GD32_RELAYING || + curSysState == OTA_SYS_ESP32_RECEIVING || + curSysState == OTA_SYS_ESP32_FINALIZING || + curSysState == OTA_SYS_ESP32_REBOOTING); + if (!ota_busy && + pingVer.app_major == 0 && pingVer.boot_major == 0 && OtaRelay::getState() == RELAY_IDLE) { static uint32_t last_ping_ms = 0; + static uint8_t ping_attempts = 0; uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000); - if (now_ms - last_ping_ms > 5000) { /* 5 秒节流 */ + if (ping_attempts < 5 && now_ms - last_ping_ms > 5000) { /* 5 秒节流 */ last_ping_ms = now_ms; + ++ping_attempts; uint32_t uptime = 0; gd32_version_t ver = {}; if (SpiMaster::instance().appPing(500, &uptime, &ver) == ESP_OK) { OtaRelay::setGd32AppAlive(true); OtaRelay::setGd32Version(ver); + ping_attempts = 0; /* 成功后重置,允许后续更新 */ } else { OtaRelay::setGd32AppAlive(false); + if (ping_attempts >= 5) { + DEBUG_WARN("STATUS", "GD32 版本号读取失败,已达最大重试次数 (5),停止尝试"); + } } } } @@ -1502,7 +1519,7 @@ bool WifiOta::earlyInit() nvs_erase_all(nvs_handle); nvs_commit(nvs_handle); nvs_close(nvs_handle); - DEBUG_INFO("WIFI", "冷启动: 已清除 WiFi NVS 缓存 (nvs.net80211)"); + DEBUG_DBG("WIFI", "冷启动: 已清除 WiFi NVS 缓存 (nvs.net80211)"); } else { DEBUG_INFO("WIFI", "冷启动: 无 WiFi NVS 缓存分区,跳过清除"); } @@ -1512,7 +1529,7 @@ bool WifiOta::earlyInit() nvs_erase_all(nvs_handle); nvs_commit(nvs_handle); nvs_close(nvs_handle); - DEBUG_INFO("WIFI", "冷启动: 已清除 PHY 校准缓存,将重新校准"); + DEBUG_DBG("WIFI", "冷启动: 已清除 PHY 校准缓存,将重新校准"); } } @@ -1522,7 +1539,7 @@ bool WifiOta::earlyInit() if (mtx) { // 通过临时获取再赋值(私有成员在同类内可访问) WifiOta::instance().m_stateMutex = mtx; - DEBUG_INFO("WIFI", "状态互斥锁已创建"); + DEBUG_DBG("WIFI", "状态互斥锁已创建"); } else { DEBUG_ERR("WIFI", "创建状态互斥锁失败"); } @@ -1542,14 +1559,14 @@ bool WifiOta::earlyInit() if (ret != pdPASS) { DEBUG_ERR("WIFI", "创建重连任务失败"); } else { - DEBUG_INFO("WIFI", "WiFi 重连任务已创建"); + DEBUG_DBG("WIFI", "WiFi 重连任务已创建"); } } // 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前) ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &WifiOta::wifiEventHandler, NULL)); - DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册"); + DEBUG_DBG("WIFI", "WiFi/IP 事件回调已注册"); esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); if (sta_netif == NULL) { @@ -1557,12 +1574,18 @@ bool WifiOta::earlyInit() if (sta_netif == NULL) { DEBUG_ERR("WIFI", "创建 STA netif 失败"); } else { - DEBUG_INFO("WIFI", "STA netif 已创建"); + DEBUG_DBG("WIFI", "STA netif 已创建"); } } else { DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建"); } + // ★ 压制 WiFi 框架内部 INFO 日志(pp/net80211/wifi/wifi_init 等 tag 大量启动打印) + esp_log_level_set("wifi", ESP_LOG_WARN); + esp_log_level_set("wifi_init", ESP_LOG_WARN); + esp_log_level_set("pp", ESP_LOG_WARN); + esp_log_level_set("net80211", ESP_LOG_WARN); + wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg)); DEBUG_INFO("WIFI", "WiFi driver 初始化成功"); diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index f3f0613..f6cca3b 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -285,13 +285,18 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, */ esp_err_t SpiMaster::init() { - LockGuard lock(m_mutex); - // 创建 SPI 总线互斥锁(必须在 SPI 设备初始化之前) - m_mutex = xSemaphoreCreateMutex(); - if (m_mutex == NULL) { - DEBUG_ERR("SPI", "SPI 互斥锁创建失败"); - return ESP_ERR_NO_MEM; + // ★ 修复:先创建 mutex,再使用 LockGuard。 + // 旧代码在 LockGuard 构造时 m_mutex 仍为 NULL, + // 虽然 LockGuard 会跳过 NULL take/give,但在极端情况下 + // 其他任务可能在 mutex 创建后立即尝试 take,产生竞态。 + if (!m_mutex) { + m_mutex = xSemaphoreCreateMutex(); + if (m_mutex == NULL) { + DEBUG_ERR("SPI", "SPI 互斥锁创建失败"); + return ESP_ERR_NO_MEM; + } } + LockGuard lock(m_mutex); DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ==="); @@ -560,7 +565,6 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver { LockGuard lock(m_mutex); constexpr uint8_t kAppPingType = 0xA1U; - constexpr size_t kAppPingPollBytes = 32U; uint8_t tx_frame[8] = {0}; size_t tx_len = 0U; @@ -574,7 +578,14 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver while (xTaskGetTickCount() <= deadline) { ++attempt; - esp_err_t err = transferClocked(tx_frame, tx_len, NULL, 0U); + /* 发送 PING 帧,填充至 16 字节: + * GD32 SPI 从机可能在 CS 断言时丢失首字节(EXTI/SPI 中断竞态), + * 填充确保即使首字节丢失,后续仍有完整 0xA5 帧头可供扫描定位。 */ + uint8_t tx_padded[16]; + memset(tx_padded, 0xFF, sizeof(tx_padded)); + memcpy(tx_padded, tx_frame, tx_len); /* [0..4]=PING帧, [5..15]=0xFF */ + + esp_err_t err = transferClocked(tx_padded, sizeof(tx_padded), NULL, 0U); if (err != ESP_OK) { DEBUG_DBG("SPI", "APP PING 请求发送失败[%lu]: %s", (unsigned long)attempt, esp_err_to_name(err)); @@ -583,10 +594,10 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver } /* 给 GD32 充足时间处理 CS 释放中断并准备响应帧 */ - vTaskDelay(pdMS_TO_TICKS(3)); + vTaskDelay(pdMS_TO_TICKS(10)); - uint8_t rx_raw[kAppPingPollBytes]; - uint8_t dummy[kAppPingPollBytes]; + uint8_t rx_raw[48]; /* 增大缓冲区,吸收 SPI 字节偏移 */ + uint8_t dummy[48]; memset(dummy, 0xFF, sizeof(dummy)); memset(rx_raw, 0xFF, sizeof(rx_raw)); diff --git a/tools/app.js b/tools/app.js index b2a7629..478527a 100644 --- a/tools/app.js +++ b/tools/app.js @@ -507,8 +507,8 @@ function updateSystemStatusUi(data) { document.getElementById('pillRelayTxt').textContent = relayText; var pr = document.getElementById('pillRelay'); if (rs === 255) pr.className = 'pill offline'; - else if (rs > 0 && rs < 17) pr.className = 'pill processing'; - else if (rs === 17 || rs === 18) pr.className = 'pill online'; + else if (rs > 0 && rs < 5) pr.className = 'pill processing'; + else if (rs === 5) pr.className = 'pill online'; else pr.className = 'pill idle'; // OTA cache progress (GD32 upload to ESP32 flash) var total = ota.total_size || 0; @@ -524,44 +524,39 @@ function updateSystemStatusUi(data) { var relayTotal = ota.relay_total_packets || 0; var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0; var gd32AppAlive = ota.gd32_app_alive; - if (rs > 0 && rs <= 8) { - // Authentication or size negotiation phase + // ★ 修复:后端实际状态机枚举 (main/relay/ota_relay.h): + // 0=RELAY_IDLE, 1=RELAY_HANDSHAKING, 2=RELAY_SIZE_NEG, + // 3=RELAY_TRANSFERRING, 4=RELAY_FINISHING, 5=RELAY_DONE, 0xFF=RELAY_ERROR + // 旧版 1-18 范围判断已与新状态机不匹配,进度条永远停在认证中阶段。 + if (rs === 1) { + // HANDSHAKING: 握手 + 挑战认证 setActivePhase('gd32Relay'); - if (rs <= 6) { - setProgress(0, '认证中: ' + (relayStateDetailMap[rs] || '')); - } else { - setProgress(0, '协商固件大小...'); - } - } else if (rs >= 9 && rs <= 13) { - // Active data forwarding + setProgress(0, '握手与认证中...'); + } else if (rs === 2) { + // SIZE_NEG: 固件大小协商 + setActivePhase('gd32Relay'); + setProgress(0, '协商固件大小...'); + } else if (rs === 3) { + // TRANSFERRING: 数据包传输 setActivePhase('gd32Relay'); setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包'); - } else if (rs >= 14 && rs <= 15) { - // Transfer complete - setActivePhase('gd32Relay'); - setProgress(100, '数据转发完成 (' + relayTotal + ' 包)'); - } else if (rs === 16) { - // OTA end sent - setActivePhase('gd32Relay'); - setProgress(100, 'OTA 刷写中...'); - } else if (rs === 17) { - // OTA complete - setActivePhase('gd32Relay'); - setProgress(100, 'OTA 完成,等待 APP 启动...'); - } else if (rs === 18) { - // APP verify phase + } else if (rs === 4) { + // FINISHING: 结束OTA + APP存活验证 setActivePhase('gd32Relay'); if (gd32AppAlive) { markPhaseDone('gd32Relay'); setProgress(100, 'GD32 刷写完成'); - // Mark status bar as success var sb = document.getElementById('stageStatusBar'); var st = document.getElementById('stageStatusText'); if (sb) sb.className = 'stage-status-bar ok'; if (st) st.textContent = 'GD32 刷写完成'; } else { - setProgress(100, '转发完成,验证 APP 存活...'); + setProgress(100, '收尾中,等待 APP 启动...'); } + } else if (rs === 5) { + // DONE: 中继完成(metaClear 已清空,relaySeq/relayTotal 会被 reset 为 0) + setActivePhase('gd32Relay'); + setProgress(100, 'OTA 完成'); } else if (total > 0 && stored < total && !isUploading) { // GD32 upload to cache in progress (handled by upload progress if uploading) setActivePhase('gd32Upload');