fix(ota): 修复OtaCoordinator互斥锁死锁和SPI appPing首字节丢失

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