feat(ota): 添加WiFi OTA期间BLE堆管理机制

- WiFi OTA时自动暂停BLE以腾出堆空间给HTTP收流
- OTA完成后自动恢复BLE栈功能
- 避免WiFi OTA与BLE共存时的堆竞争问题

fix(debug_log): 优化堆紧张时日志处理策略

- 当堆空间小于8KB时,/logs接口仍返回最小JSON避免沉默失败
- 增加堆门控机制,OTA/SPI关键日志优先保留
- UDP发送在堆紧张时跳过,避免与HTTP回包争夺LWIP堆
- 调整日志缓冲区大小,减少栈压力

chore(version): 更新固件版本至0.3.43

- 版本号从0.3.42提升到0.3.43
- 更新编译时间和Git哈希信息

refactor(can_feedback): 优化CAN反馈任务配置

- 将反馈payload最大长度改为SPI_OTA_FRAME_MAX常量
- 使用takeEvent替代takeRingRx方法
This commit is contained in:
小龙虾
2026-07-14 12:10:13 +08:00
parent 76782349a9
commit adb3386da4
18 changed files with 1739 additions and 756 deletions
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 42
#define FW_VERSION_STRING "0.3.42"
#define FW_VERSION_PATCH 43
#define FW_VERSION_STRING "0.3.43"
// 编译时间
#define BUILD_TIME "2026-07-13 03:05:29"
#define BUILD_TIME "2026-07-14 12:05:28"
// Git 信息
#define GIT_HASH "0b36349"
#define GIT_HASH "7678234"
+2 -2
View File
@@ -28,7 +28,7 @@ namespace {
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
constexpr size_t kRingPayloadMax = 128U; ///< 反馈 payload 最大长度
constexpr size_t kRingPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致,避免截断
constexpr const char *kTag = "CAN_FB";
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
@@ -72,7 +72,7 @@ static void taskLoop(void *)
}
/* 取出 checkGd32Request 存入的 type/payload */
if (!SpiMaster::instance().takeRingRx(&ring_type, ring_payload,
if (!SpiMaster::instance().takeEvent(&ring_type, ring_payload,
sizeof(ring_payload), &ring_len)) {
continue;
}
+57 -10
View File
@@ -24,6 +24,8 @@
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"
#include "esp_heap_caps.h"
#include "driver/uart.h"
#include "debug_log.h"
@@ -117,13 +119,37 @@ char* DebugLog::ringDrainJson(int *out_len)
{
if (!out_len) return NULL;
*out_len = 0;
/* 堆紧时不 malloc:返回堆上最小 "[]",调用方仍 free。 */
const uint32_t free_now = esp_get_free_heap_size();
if (free_now < kDrainMinFree) {
char *empty = (char *)malloc(3);
if (!empty) return NULL;
empty[0] = '[';
empty[1] = ']';
empty[2] = '\0';
*out_len = 2;
return empty;
}
int count = ringCount();
// ★ 限制单次导出条数,避免 count=64 时 malloc(32KB) 在堆紧张时失败
// 前端每秒轮询,16 条/次足够保持实时性;超出的旧条目下一轮再拉
static constexpr int kMaxDrain = 16;
/* OTA 沉默失败排查需要一次多拉;上限 24 条 */
static constexpr int kMaxDrain = 24;
if (count > kMaxDrain) count = kMaxDrain;
int capacity = count * (LOG_RING_SIZE * 2) + 16;
if (capacity < 16) capacity = 16;
const size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
if (largest < (size_t)capacity) {
char *empty = (char *)malloc(3);
if (!empty) return NULL;
empty[0] = '[';
empty[1] = ']';
empty[2] = '\0';
*out_len = 2;
return empty;
}
char *buf = (char*)malloc(capacity);
if (!buf) return NULL;
@@ -333,14 +359,16 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
{
esp_err_t udp_ret = ESP_FAIL;
esp_err_t uart_ret = ESP_FAIL;
const uint32_t free_now = esp_get_free_heap_size();
// UART 通道(UART 不可用时跳过 uart_write_bytes 调用)
if (m_uartAvailable) {
uart_ret = uartSend(data, len);
}
// UDP 通道(WiFi 未连接时 sendto 失败,静默跳过)
if (m_initialized) {
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
* WiFi 未连接时 sendto 失败也静默跳过。 */
if (m_initialized && free_now >= kSkipUdpFree) {
if (m_udpSock < 0) {
ensureUdpSocket();
}
@@ -355,12 +383,16 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
}
}
// 同时写入环形缓冲区(供 HTTP /logs 拉取)
// 同时写入环形缓冲区(供 HTTP /logs 拉取)——静态,不占堆
ringPush((const char *)data, (int)len);
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
return ESP_OK;
}
/* 仅 ring 成功也算可用(OTA 堆紧时常见) */
if (m_logRingMutex) {
return ESP_OK;
}
return ESP_FAIL;
}
@@ -381,7 +413,22 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
return;
}
char prefix[32];
/* 堆门控:OTA/SPI 的 INFO 必须进 ring,否则 WebUI 只能看到“沉默失败”。 */
const uint32_t free_now = esp_get_free_heap_size();
const bool keep_ota_spi = (tag != nullptr &&
(strcmp(tag, "OTA") == 0 || strcmp(tag, "SPI") == 0));
if (!keep_ota_spi) {
if (free_now < kDropWarnFree && level > DEBUG_LOG_ERR) {
return; // 无关模块:仅 ERR
}
if (free_now < kDropInfoFree && level > DEBUG_LOG_WARN) {
return; // 无关模块:丢 INFO/DBG
}
} else if (free_now < kDropWarnFree && level >= DEBUG_LOG_DBG) {
return; // OTA/SPI:极紧时仍丢 DBG,INFO/WARN/ERR 保留
}
char prefix[24];
const char *level_str;
switch (level) {
case DEBUG_LOG_ERR: level_str = "E"; break;
@@ -392,14 +439,14 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
}
snprintf(prefix, sizeof(prefix), "[%s][%s]", level_str, tag);
// 控制单条日志栈缓冲大小,避免调用任务(如 can_fb)因日志格式化爆栈。
/* 栈缓冲与 LOG_RING_SIZE 对齐,减轻 httpd 等任务栈压。 */
va_list args;
va_start(args, fmt);
char msg[256];
char msg[160];
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
char log_buf[320];
char log_buf[192];
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
// ★ snprintf 截断时返回"本应写入的长度"(可能 > sizeof(log_buf)),
+12 -1
View File
@@ -99,14 +99,25 @@ private:
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
bool m_uartAvailable = false;
/* 环形日志:OTA 中继期需保留关键 INFO,64×160≈10KB BSS。 */
static constexpr int LOG_RING_SLOTS = 64;
static constexpr int LOG_RING_SIZE = 256;
static constexpr int LOG_RING_SIZE = 160;
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE];
int m_logRingLens[LOG_RING_SLOTS];
int m_logRingHead = 0;
int m_logRingCount = 0;
SemaphoreHandle_t m_logRingMutex = NULL;
/* 堆门控(防 OTA 时 UDP/格式化抢 LWIP 堆):
* free < kDropWarnFree → 非 OTA/SPI 只保留 ERR
* free < kDropInfoFree → 非 OTA/SPI 丢 INFO/DBG(OTA/SPI 的 INFO 仍进 ring)
* free < kSkipUdpFree → 不 sendto(仍 UART+ring)
* free < kDrainMinFree → /logs 尽量仍返回 ring(降低门槛,避免“沉默失败”) */
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
static constexpr uint32_t kSkipUdpFree = 10U * 1024U;
static constexpr uint32_t kDrainMinFree = 8U * 1024U;
esp_err_t ensureUdpSocket();
void ringPush(const char *data, int len);
};
+10 -3
View File
@@ -22,6 +22,7 @@
#include "debug_log.h"
#include "task_stack_check.h"
#include "stack_overflow_hook.h"
#include "ota/ota_coordinator.h"
// ============================================================
// 栈水位主动巡检
@@ -234,13 +235,19 @@ void HealthMonitor::taskEntry(void *pvParameters)
uint32_t free_heap = esp_get_free_heap_size();
if (free_heap < HEAP_WARNING_MIN) {
/* OTA 期间堆会短暂变紧,限流避免串口/UDP 二次吃堆、刷屏 */
/* WiFi OTA 收流时 TCP 窗口 + httpd 会把 free 压到 5~10KB,属预期,勿当故障刷屏。 */
static TickType_t s_last_heap_warn = 0;
TickType_t now = xTaskGetTickCount();
if ((now - s_last_heap_warn) >= pdMS_TO_TICKS(2000)) {
const bool ota_busy = !OtaCoordinator::instance().isIdle();
const uint32_t interval_ms = ota_busy ? 15000U : 3000U;
/* OTA 中只在 free 极低(<4KB)时告警;空闲时维持 10KB 阈值 */
const uint32_t warn_floor = ota_busy ? 4096U : (uint32_t)HEAP_WARNING_MIN;
if (free_heap < warn_floor &&
(now - s_last_heap_warn) >= pdMS_TO_TICKS(interval_ms)) {
s_last_heap_warn = now;
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
DEBUG_WARN("MON", "堆内存不足: free=%lu min=%lu largest_blk=%lu",
DEBUG_WARN("MON", "堆内存偏紧%s: free=%lu min=%lu largest_blk=%lu",
ota_busy ? "(OTA中)" : "",
(unsigned long)free_heap,
(unsigned long)esp_get_minimum_free_heap_size(),
(unsigned long)largest_block);
+7 -4
View File
@@ -213,14 +213,17 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
return ESP_ERR_INVALID_STATE;
}
/* sequential 模式写时可能触发扇区擦除,喂狗防 WDT */
/* sequential 模式写时可能触发扇区擦除(禁 cache),期间 WiFi 易饿死。
* 写后 yield 一次即可;写前再 yield 会无谓拖慢吞吐(httpd 侧另有堆感知 pacing)。 */
(void)esp_task_wdt_reset();
esp_err_t err = esp_ota_write(m_handle, data, len);
taskYIELD();
if (err == ESP_OK) {
m_received += len;
} else {
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u offset=%u)",
esp_err_to_name(err), (unsigned)len, (unsigned)m_received);
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u offset=%u free=%lu)",
esp_err_to_name(err), (unsigned)len, (unsigned)m_received,
(unsigned long)esp_get_free_heap_size());
}
return err;
}
@@ -433,7 +436,7 @@ bool Esp32OtaCore::forceCleanup()
if (!s_mutex) return false;
// 在互斥保护下检查 s_occupied
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false;
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(500)) != pdTRUE) return false;
bool was_occupied = s_occupied;
if (was_occupied) {
+45 -4
View File
@@ -10,6 +10,7 @@
#include "relay/ota_relay.h"
#include "storage/gd32_flash_store.h"
#include "transport/spi_master.h"
#include "server/ble_ota.h"
#include "monitor/debug_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
@@ -18,6 +19,23 @@
#define TAG "OTA_COORD"
/** WiFi OTA 时挂起 BLE;BLE 通道自身 OTA 不挂。调用勿持 m_mutex。 */
static void wifiOtaSuspendBleIfNeeded(ota_channel_t channel)
{
if (channel != OTA_CHANNEL_WIFI) {
return;
}
(void)BleOta::instance().suspendStack();
}
/** 回到可空闲态时恢复 BLE。调用勿持 m_mutex。 */
static void wifiOtaResumeBleIfNeeded(void)
{
if (BleOta::instance().isSuspendedForWifiOta()) {
(void)BleOta::instance().resumeStack();
}
}
// ============================================================
// 单例
// ============================================================
@@ -190,10 +208,16 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
return err;
}
/* WiFi GD32 上传前先挂 BLE,腾堆给 HTTP 收流 */
wifiOtaSuspendBleIfNeeded(channel);
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
err = beginGd32Storage(total_size, channel);
if (err != ESP_OK) {
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
if (channel == OTA_CHANNEL_WIFI) {
wifiOtaResumeBleIfNeeded();
}
return err;
}
@@ -223,15 +247,20 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
void OtaCoordinator::completeGd32Storage()
{
bool h = lock();
bool resume_ble = false;
if (h && m_sysState == OTA_SYS_GD32_STORING) {
m_sysState = OTA_SYS_GD32_STORED;
m_gd32StorageDone = true;
m_busySinceTick = 0; // ★ STORED 是非忙状态
resume_ble = (m_otaChannel == OTA_CHANNEL_WIFI);
DEBUG_INFO(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
} else if (h) {
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
}
unlock(h);
if (resume_ble) {
wifiOtaResumeBleIfNeeded();
}
}
esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
@@ -315,7 +344,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
uint32_t cmd = 1;
BaseType_t ret = xQueueSend(dq, &cmd, pdMS_TO_TICKS(200));
BaseType_t ret = xQueueSend(dq, &cmd, pdMS_TO_TICKS(100));
if (ret != pdTRUE) {
DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败");
m_sysState = OTA_SYS_GD32_STORED;
@@ -360,6 +389,8 @@ void OtaCoordinator::notifyRelayComplete(bool success)
}
}
unlock(h);
/* 中继结束:若 BLE 仍因 WiFi OTA 挂起则恢复 */
wifiOtaResumeBleIfNeeded();
}
// ============================================================
@@ -451,8 +482,6 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
m_otaChannel = channel; // ★ 记录发起通道
m_sysState = OTA_SYS_ESP32_RECEIVING;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING (epoch=%u, channel=%d)",
total_size, m_otaEpoch, (int)channel);
pauseCanFeedbackTask(true);
} else {
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
@@ -465,6 +494,9 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel)
{
/* WiFi 路径:在 begin 前挂 BLE,把 BT 堆腾给 OTA 收流(BLE 通道 OTA 不挂)。 */
wifiOtaSuspendBleIfNeeded(channel);
esp_err_t err = beginEsp32Ota(total_size, channel);
if (err == ESP_ERR_INVALID_STATE) {
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
@@ -478,6 +510,9 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
if (!isIdleState(cur) && cur != OTA_SYS_ERROR) {
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
(int)cur);
if (channel == OTA_CHANNEL_WIFI) {
wifiOtaResumeBleIfNeeded();
}
return err;
}
@@ -489,6 +524,9 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
Esp32OtaCore::instance().forceCleanup();
err = beginEsp32Ota(total_size, channel);
}
if (err != ESP_OK && channel == OTA_CHANNEL_WIFI) {
wifiOtaResumeBleIfNeeded();
}
return err;
}
@@ -547,7 +585,7 @@ Esp32FinalizeResult OtaCoordinator::finalizeForReboot(const char *frontend_sha_h
void OtaCoordinator::restartEsp32()
{
notifyEsp32Rebooting();
vTaskDelay(pdMS_TO_TICKS(500));
vTaskDelay(pdMS_TO_TICKS(100));
esp_restart();
// 不会返回
}
@@ -573,6 +611,7 @@ void OtaCoordinator::doAbortLocked()
m_gd32StorageDone = false;
m_busySinceTick = 0;
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
/* 释放协调器锁后由调用方 resume BLE;doAbortLocked 可能持锁,标记后外层 resume */
}
void OtaCoordinator::abortCurrent()
@@ -582,6 +621,7 @@ void OtaCoordinator::abortCurrent()
doAbortLocked();
}
unlock(h);
wifiOtaResumeBleIfNeeded();
}
uint32_t OtaCoordinator::getOtaEpoch() const
@@ -607,6 +647,7 @@ bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
doAbortLocked();
unlock(h);
wifiOtaResumeBleIfNeeded();
return true;
}
+145 -168
View File
@@ -50,6 +50,8 @@ static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
static constexpr uint32_t kLastPacketAckSeq = 0xF0F0F0F0U; // 末包 ACK 序号约定
// 10 秒无成功帧停刷:跨包级别的背压/卡死检测,比单包 15s 超时更早触发。
static constexpr uint32_t kStallTimeoutMs = 8000U;
// ============================================================
// 静态成员定义
@@ -349,6 +351,17 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa
DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets);
/* 转发预算:自本阶段起算。正常 ~15ms/包,预算 40ms/包 + 20s 底(上限 120s)。 */
constexpr uint32_t kXferMinMs = 20U * 1000U;
constexpr uint32_t kXferMaxMs = 120U * 1000U;
constexpr uint32_t kXferPerPktMs = 40U;
uint32_t xfer_ms = kXferMinMs + m_transferTotalPackets * kXferPerPktMs;
if (xfer_ms > kXferMaxMs) {
xfer_ms = kXferMaxMs;
}
m_transferDeadline = xTaskGetTickCount() + pdMS_TO_TICKS(xfer_ms);
DEBUG_INFO("OTA", "转发超时预算: %" PRIu32 " ms (%" PRIu32 " 包)", xfer_ms, m_transferTotalPackets);
ota_result_t res = doRelayPackets(m_transferDeadline);
if (res != OTA_RESULT_OK) {
setState(RELAY_ERROR);
@@ -437,6 +450,9 @@ ota_result_t OtaRelay::forwardFirmware()
DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size);
/* 中继全程禁止 worker 把 OTA ACK/WAIT 当 EVENT 偷读 */
SpiMaster::instance().setSuppressRingService(true);
// 固件完整性校验
{
esp_err_t verify_err = Gd32FlashStore::instance().metaVerifyChecksum();
@@ -447,16 +463,10 @@ ota_result_t OtaRelay::forwardFirmware()
}
}
// 全局超时计算
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
uint32_t global_timeout_ms = (dynamic_ms > kMaxGlobalTimeoutMs) ? kMaxGlobalTimeoutMs : dynamic_ms;
TickType_t start_tick = xTaskGetTickCount();
m_transferDeadline = start_tick + pdMS_TO_TICKS(global_timeout_ms);
DEBUG_INFO("OTA", "全局超时: %" PRIu32 " ms, start_tick=%" PRIu32 ", deadline tick=%" PRIu32,
global_timeout_ms, (uint32_t)start_tick, (uint32_t)m_transferDeadline);
/* 全局 deadline 在进入 TRANSFERRING 时才启动(见 handleTransferring)。
* 握手 + Boot 擦 Flash(SIZE 阶段长时间 WAIT)不得占用转发预算,
* 否则 1117 包会在擦除后仅剩十余秒被“全局超时”误杀。 */
m_transferDeadline = 0;
// ── 驱动状态机 ──
// 5 步顺序推进:IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
@@ -475,6 +485,9 @@ ota_result_t OtaRelay::forwardFirmware()
if (res != OTA_RESULT_OK) {
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
reset();
} else {
/* 成功路径:解除按铃抑制(reset 也会做,成功时状态机可能停在 DONE) */
SpiMaster::instance().setSuppressRingService(false);
}
return res;
}
@@ -498,6 +511,7 @@ void OtaRelay::reset()
memset(m_kSession, 0, sizeof(m_kSession));
memset(m_nonceServer, 0, sizeof(m_nonceServer));
memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime));
SpiMaster::instance().setSuppressRingService(false);
DEBUG_INFO("OTA", "状态机已复位 → IDLE");
}
@@ -977,6 +991,7 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
return OTA_RESULT_ERR_TIMEOUT;
}
SpiMaster::instance().confirmTxSeqAccepted();
DEBUG_INFO("OTA", "大小协商完成: %" PRIu32 " 字节,GD32 已就绪收包", size);
return OTA_RESULT_OK;
}
@@ -1034,162 +1049,111 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
uint8_t resp_type = 0;
uint8_t resp_payload[32] = {0};
size_t resp_len = 0;
uint32_t retry_count = 0;
ota_result_t result = OTA_RESULT_OK;
bool got_expected_credit_ok = false;
const uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? kLastPacketAckSeq : (seq + 1U);
/*
* 协议要点:
* - GD32 ISR 收 DATA 后立刻回 WAIT,主循环 AES/Flash 后再 ACK。
* - 一旦见过 WAIT,说明包已进从机;再发 DATA = DUP-SEQ,只会反复 WAIT。
* - 因此:见 WAIT 后只 poll;仅「完全无响应」才允许重发 DATA。
*/
constexpr uint32_t kMaxDataSends = 3U;
constexpr uint32_t kAckWaitMs = 6000U; /* 单次发送后等 ACK 上限(覆盖 GD32 增量 drain) */
constexpr uint32_t kPollSliceMs = 80U;
// ★ 单包耗时统计,用于定位 OTA 速度瓶颈。
TickType_t pkt_start = xTaskGetTickCount();
/* 单包硬超时:避免重试环 + WAIT/CREDIT 轮询在异常时“静默卡死”十几分钟。 */
const TickType_t pkt_deadline = pkt_start + pdMS_TO_TICKS(15000);
TickType_t last_stall_log = pkt_start;
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
resend_packet:
while (1) {
if (xTaskGetTickCount() >= pkt_deadline) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 单包超时(15s), retries=%" PRIu32 " credit=%u",
seq, retry_count, (unsigned)SpiMaster::instance().remoteCredit());
result = OTA_RESULT_ERR_TIMEOUT;
break;
}
if ((xTaskGetTickCount() - last_stall_log) >= pdMS_TO_TICKS(2000)) {
DEBUG_INFO("OTA", "包 %" PRIu32 " 仍在等待 ACK (retry=%" PRIu32 " credit=%u)",
seq, retry_count, (unsigned)SpiMaster::instance().remoteCredit());
last_stall_log = xTaskGetTickCount();
}
bool saw_wait = false;
for (uint32_t send_i = 0; send_i < kMaxDataSends; ++send_i) {
/* 已见 WAIT:禁止再发 DATA,只延长 poll */
if (saw_wait && send_i > 0U) {
DEBUG_INFO("OTA", "包 %" PRIu32 " 已见 WAIT,继续 poll 不重发", seq);
} else {
resp_len = 0;
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, 64,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
resp_type = 0;
esp_err_t err = SpiMaster::instance().otaTransfer(
SPI_OTA_CMD_DATA, tx_buf, 64,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200);
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
if (err == ESP_OK) {
/* WAIT 常见于 GD32 主循环 AES/写 Flash:多轮 poll,勿过早放弃。 */
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 200, 40);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
saw_wait = true;
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
if (++retry_count >= kMaxResendOnNack) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_TIMEOUT;
break;
}
vTaskDelay(pdMS_TO_TICKS(2));
continue; // 重发本包
}
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
uint32_t err_seq = 0;
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
if (err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
if (++retry_count >= kMaxResendOnStructError) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break;
}
continue;
}
}
/* CREDIT 感知:GD32 写 Flash / DATA 队列满时 CREDIT=0,耐心 POLL。 */
if (SpiMaster::instance().remoteCredit() == 0U) {
DEBUG_INFO("OTA", "包 %" PRIu32 " GD32 CREDIT=0,等待恢复...", seq);
for (uint32_t cw = 0; cw < 100U; ++cw) {
if (xTaskGetTickCount() >= pkt_deadline) {
break;
}
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload,
sizeof(resp_payload), &resp_len, 100);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK &&
isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
got_expected_credit_ok = true;
break;
SpiMaster::instance().confirmTxSeqAccepted();
m_transferFlashOffset += (uint32_t)chunk_size;
if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) {
DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32,
seq + 1U, m_transferTotalPackets);
}
return false;
}
}
const TickType_t poll_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(kAckWaitMs);
TickType_t last_hb = xTaskGetTickCount();
while (xTaskGetTickCount() < poll_deadline) {
if ((xTaskGetTickCount() - last_hb) >= pdMS_TO_TICKS(1000)) {
DEBUG_INFO("OTA", "包 %" PRIu32 " 等 ACK… send=%" PRIu32 " type=0x%02X wait=%d credit=%u",
seq, send_i, resp_type, (int)saw_wait,
(unsigned)SpiMaster::instance().remoteCredit());
last_hb = xTaskGetTickCount();
}
esp_err_t err = SpiMaster::instance().otaPollResponse(
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, kPollSliceMs);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
saw_wait = true;
continue;
}
if (err == ESP_OK && SpiMaster::instance().remoteCredit() > 0U) {
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK) {
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
SpiMaster::instance().confirmTxSeqAccepted();
m_transferFlashOffset += (uint32_t)chunk_size;
if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) {
DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32,
seq + 1U, m_transferTotalPackets);
}
return false;
}
uint32_t err_seq = 0;
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) &&
err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 结构化错误 err=0x%02X", seq, err_code);
if (err_code == 0x04U) {
*out_result = OTA_RESULT_ERR_CRC;
return true;
}
saw_wait = false; /* 允许带错误码的重发 */
break;
}
/* 陈旧 ACK,继续 poll */
}
}
/* 超时:仅当从未见 WAIT 时才重发 DATA;见过 WAIT 则再 poll 一轮 */
if (saw_wait) {
if (send_i + 1U >= kMaxDataSends) {
break;
}
/* 把下一轮当作「只 poll」:send_i 增加但 saw_wait 使跳过 transfer */
continue;
}
if (send_i + 1U < kMaxDataSends) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 无 WAIT/ACK,重发 DATA (%" PRIu32 "/%" PRIu32 ")",
seq, send_i + 1U, kMaxDataSends);
vTaskDelay(pdMS_TO_TICKS(2));
}
}
bool got_expected = false;
if (got_expected_credit_ok) {
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
for (uint32_t retry = 0; retry < 10; ++retry) {
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
if (++retry_count >= kMaxResendOnStructError) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32;
break;
}
// ★ resend 前延迟
vTaskDelay(pdMS_TO_TICKS(1));
got_expected_credit_ok = false;
goto resend_packet;
}
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
got_expected = true;
break;
}
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
break;
}
}
if (got_expected) {
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
if (++retry_count >= kMaxResendOnNack) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break;
}
continue;
}
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
if (++retry_count >= kMaxResendOnNack) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32;
break;
}
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
vTaskDelay(pdMS_TO_TICKS(1));
goto resend_packet;
}
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
TickType_t pkt_elapsed = xTaskGetTickCount() - pkt_start;
DEBUG_DBG("OTA", "包 %" PRIu32 " 完成: elapsed=%" PRIu32 " ms, retries=%" PRIu32, seq,
(uint32_t)(pkt_elapsed * portTICK_PERIOD_MS), retry_count);
if (result != OTA_RESULT_OK) {
*out_result = result;
return true; // 中止外层循环
}
return false; // 继续下一包
DEBUG_ERR("OTA", "包 %" PRIu32 " 失败 (elapsed=%" PRIu32 " ms credit=%u type=0x%02X saw_wait=%d)",
seq,
(uint32_t)((xTaskGetTickCount() - pkt_start) * portTICK_PERIOD_MS),
(unsigned)SpiMaster::instance().remoteCredit(),
resp_type, (int)saw_wait);
*out_result = OTA_RESULT_ERR_TIMEOUT;
return true;
}
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
@@ -1202,6 +1166,7 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
uint8_t tx_buf[64] = {0};
ota_result_t result = OTA_RESULT_OK;
TickType_t last_success_tick = xTaskGetTickCount();
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
@@ -1219,35 +1184,31 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
return OTA_RESULT_ERR_USER_ABORT;
}
// 准备单包:读 Flash + 加密 + 组装
// 停刷检测:自上一包成功 ACK 起超过 kStallTimeoutMs 则中止
if ((xTaskGetTickCount() - last_success_tick) >= pdMS_TO_TICKS(kStallTimeoutMs)) {
DEBUG_ERR("OTA", "10 秒无成功帧, 中止转发: seq=%" PRIu32 "/%" PRIu32, seq, m_transferTotalPackets);
return OTA_RESULT_ERR_TIMEOUT;
}
size_t chunk_size = 0;
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
if (result != OTA_RESULT_OK) {
return result;
}
/* 先写入「正在发第 N 包」,前端 /system/status 能看到卡在哪 */
setRelaySeq(seq);
// 发送单包并等待 ACK(内含重试循环)
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
DEBUG_ERR("OTA", "中止于包 %" PRIu32 "/%" PRIu32 " result=%d",
seq, m_transferTotalPackets, (int)result);
return result; // 中止
}
setRelaySeq(seq + 1);
// 进度只写 relay_seq 供 /system/status;不打文字日志(前端进度条负责展示)。
// ★ 包间延迟:给 GD32 写 Flash + 让低优先级 httpd 能应答 /system/status。
// ota_relay 优先级高于 wifi_ota:若不主动让出,前端只能看到 SIZE→完成,中间全跳过。
if (seq < m_transferTotalPackets - 1) {
uint32_t gap_ms = CONFIG_DCU_OTA_PACKET_INTERVAL_MS;
if (gap_ms < 1U) {
gap_ms = 1U;
}
/* 每 16 包额外让出 15ms,保证 status/logs 轮询有机会跑 */
if (((seq + 1U) % 16U) == 0U) {
gap_ms = (gap_ms > 15U) ? gap_ms : 15U;
}
vTaskDelay(pdMS_TO_TICKS(gap_ms));
}
last_success_tick = xTaskGetTickCount();
}
DEBUG_INFO("OTA", "全部数据包已转发: %" PRIu32 " 包", m_transferTotalPackets);
return result;
}
@@ -1301,7 +1262,7 @@ ota_result_t OtaRelay::doSendFinishOta()
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len);
if (err != ESP_OK) {
DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
@@ -1310,7 +1271,7 @@ ota_result_t OtaRelay::doSendFinishOta()
1000, 10, 8, OtaRelay::isFinishPlainAck);
if (err != ESP_OK) {
DEBUG_WARN("OTA", "FINISH_OTA poll 失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
@@ -1320,7 +1281,7 @@ ota_result_t OtaRelay::doSendFinishOta()
if (resp_type == SPI_OTA_RSP_WAIT) {
DEBUG_INFO("OTA", "FINISH_OTA 收到 WAIT, GD32 正在排空 Flash 缓冲, retry=%" PRIu32 "/10", retry + 1);
vTaskDelay(pdMS_TO_TICKS(200));
vTaskDelay(pdMS_TO_TICKS(20));
continue;
}
@@ -1328,13 +1289,13 @@ ota_result_t OtaRelay::doSendFinishOta()
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
DEBUG_ERR("OTA", "FINISH_OTA 收到错误帧 err=0x%02X, retry=%" PRIu32 "/10", err_code, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%" PRIu32 "/10",
resp_type, resp_len, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
vTaskDelay(pdMS_TO_TICKS(10));
}
if (resp_type != SPI_OTA_RSP_ACK || !isFinishPlainAck(resp_payload, resp_len)) {
@@ -1351,8 +1312,24 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
// ★ APP 存活验证
setGd32AppAlive(false);
// GD32 刚跳转到新 APP,SPI v2 session_id 已重置为 0。
// 必须先 hello() 建立新 session,否则 appPing 的帧被 otaFindFrame
// 以 SESSION mismatch 拒绝(旧 session_id vs 新 APP session_id=0)。
bool session_established = false;
for (uint32_t attempt = 0; attempt < 10; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(500));
if (!session_established) {
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
if (hr == ESP_OK) {
session_established = true;
} else {
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/10)", attempt + 1);
continue;
}
}
if (probeAndCacheAppAlive(timeout_ms)) {
break;
}
+128 -11
View File
@@ -31,6 +31,7 @@
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "nimble/ble.h" /* BLE_ERR_REM_USER_CONN_TERM */
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "host/ble_att.h"
@@ -829,6 +830,10 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
(void)attr_handle;
(void)arg;
if (BleOta::instance().isSuspendedForWifiOta()) {
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
}
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
}
@@ -1093,6 +1098,10 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
(void)attr_handle;
(void)arg;
if (BleOta::instance().isSuspendedForWifiOta()) {
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
}
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
}
@@ -1490,9 +1499,10 @@ static void ble_ota_data_worker_task(void *pvParameters)
continue;
}
// 若 BLE 已断开,丢弃队列中的残留数据
if (!BleOta::instance().isConnected()) {
DEBUG_DBG(TAG, "BLE 已断开,丢弃 %zu 字节队列数据", item.len);
// 若 BLE 已断开或 WiFi OTA 挂起,丢弃队列中的残留数据
if (!BleOta::instance().isConnected() ||
BleOta::instance().isSuspendedForWifiOta()) {
DEBUG_DBG(TAG, "BLE 不可用/挂起,丢弃 %zu 字节队列数据", item.len);
free(item.data);
continue;
}
@@ -1640,6 +1650,15 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
if (BleOta::instance().isSuspendedForWifiOta()) {
/* WiFi OTA 中拒绝 BLE 连接,立刻踢掉 */
DEBUG_WARN(TAG, "WiFi OTA 中,拒绝 BLE 连接 handle=%d",
event->connect.conn_handle);
(void)ble_gap_terminate(event->connect.conn_handle,
BLE_ERR_REM_USER_CONN_TERM);
BleOta::instance().m_bleConnected = false;
return 0;
}
// 连接成功
BleOta::instance().m_bleConnected = true;
BleOta::instance().m_connHandle = event->connect.conn_handle;
@@ -1673,6 +1692,13 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
BleOta::instance().m_bleConnected = false;
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
s_conn_fallback_requested = false;
/* WiFi OTA 软挂起期间:主动踢连触发的 disconnect 不得 abort WiFi OTA,也不得再开广播 */
if (BleOta::instance().isSuspendedForWifiOta()) {
DEBUG_INFO(TAG, "WiFi OTA 挂起中:忽略 BLE 断开对 OTA 状态的影响");
return 0;
}
// ★ BLE 断开时恢复 WiFi 射频
ble_ota_restore_wifi_ps();
@@ -1688,16 +1714,24 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
OtaCoordinator::stateToString(cur));
} else {
if (cur == OTA_SYS_GD32_STORING) {
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 GD32 固件存储");
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
cur == OTA_SYS_ESP32_FINALIZING) {
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
cur == OTA_SYS_ESP32_FINALIZING ||
cur == OTA_SYS_GD32_STORING) {
/* 仅当当前 OTA 通道是 BLE 时才因断连中止;WiFi 通道的 RECEIVING 不能被误杀 */
ota_channel_t ch = OTA_CHANNEL_WIFI;
{
/* get channel via snapshot if available */
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
ch = snap.channel;
}
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 +
// 会话状态复位(m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/
// reorder/s_gd32_ooo_count)
if (ch == OTA_CHANNEL_BLE) {
DEBUG_WARN(TAG, "BLE 断开,中止 BLE 通道 OTA (state=%s)",
OtaCoordinator::stateToString(cur));
ble_ota_session_reset();
} else {
DEBUG_INFO(TAG, "BLE 断开但当前为 WiFi OTA,不中止会话");
}
} else {
ble_ota_session_reset();
}
}
@@ -1706,6 +1740,9 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
if (BleOta::instance().isSuspendedForWifiOta()) {
return 0; /* WiFi OTA 中不要自动再开广播 */
}
DEBUG_INFO(TAG, "广播周期结束,重新开始");
ble_ota_start_advertising();
return 0;
@@ -1750,6 +1787,11 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
*/
static void ble_ota_start_advertising(void) // kept as free function for BLE callback compatibility
{
if (BleOta::instance().isSuspendedForWifiOta()) {
DEBUG_DBG(TAG, "WiFi OTA 挂起中,跳过广播");
return;
}
int rc;
// 自动推断可用地址类型(优先 Public,不可用则生成 Random)
@@ -1886,6 +1928,11 @@ esp_err_t BleOta::serverStart()
{
int rc;
if (m_stackRunning) {
DEBUG_INFO(TAG, "serverStart: 栈已在运行,跳过");
return ESP_OK;
}
DEBUG_INFO(TAG, "=== 初始化 BLE OTA GATT 服务器 ===");
DEBUG_INFO(TAG, " Device: %s", BLE_DEVICE_NAME);
DEBUG_INFO(TAG, " MTU: %" PRIu16, BLE_MTU_SIZE);
@@ -1935,10 +1982,80 @@ esp_err_t BleOta::serverStart()
nimble_port_freertos_init(ble_ota_nimble_host_task);
// s_data_out_val_handle 由 on_sync 回调填充,此处不复制
BleOta::instance().m_stackRunning = true;
BleOta::instance().m_suspendedForWifiOta = false;
DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成");
return ESP_OK;
}
esp_err_t BleOta::suspendStack()
{
/*
* ★ 软挂起 only(ESP32-C3 WiFi/BLE 共用射频)。
* 禁止在此调用 nimble_port_stop / deinit / controller_disable:
* - 会从 httpd 任务阻塞甚至打挂 WiFi,表现为「一点击上传就失联」;
* - C3 上 deinit BT 控制器与 WiFi 共存状态机冲突。
* 软挂起:停广播、断连、拒收新数据,NimBLE host 仍跑、WiFi 不受影响。
*/
if (m_suspendedForWifiOta) {
DEBUG_DBG(TAG, "suspendStack: 已挂起,跳过");
return ESP_OK;
}
if (m_stackRunning) {
(void)ble_gap_adv_stop();
if (m_bleConnected && m_connHandle != 0xFFFFU) {
(void)ble_gap_terminate(m_connHandle, BLE_ERR_REM_USER_CONN_TERM);
/* 短等即可,勿长时间阻塞 httpd */
vTaskDelay(pdMS_TO_TICKS(30));
}
}
m_bleConnected = false;
m_connHandle = 0xFFFFU;
/* 保持 m_bleReady / m_stackRunning:栈仍在,仅业务拒绝连接与写 */
m_suspendedForWifiOta = true;
if (s_otaDataQueue) {
BleOtaDataBatch drop;
while (xQueueReceive(s_otaDataQueue, &drop, 0) == pdTRUE) {
free(drop.data);
}
}
return ESP_OK;
}
esp_err_t BleOta::resumeStack()
{
if (!m_suspendedForWifiOta) {
return ESP_OK;
}
m_suspendedForWifiOta = false;
if (!m_stackRunning) {
/* 仅当历史上曾 hard-deinit 过才需要完整 serverStart */
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
(unsigned long)esp_get_free_heap_size());
esp_err_t err = serverStart();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "resumeStack/serverStart 失败: %s", esp_err_to_name(err));
m_suspendedForWifiOta = true;
return err;
}
return ESP_OK;
}
/* 软挂起恢复:重新开广播即可 */
DEBUG_INFO(TAG, "WiFi OTA 结束:恢复 BLE 广播 free=%lu",
(unsigned long)esp_get_free_heap_size());
m_bleReady = true;
ble_ota_start_advertising();
return ESP_OK;
}
/**
* @brief 通过 BLE Notify 将 GD32 响应推送给上位机。
* @param data 待推送的数据指针。
+19
View File
@@ -31,6 +31,23 @@ public:
* @return ESP_OK 成功,其他失败。
*/
esp_err_t serverStart();
/**
* @brief WiFi OTA 期间软挂起 BLE(停广播、断连、拒收写;不 deinit 控制器)。
* ESP32-C3 上 hard deinit 会打挂 WiFi 共存,导致上传立刻失联。
* @return ESP_OK 已挂起或本就未运行。
*/
esp_err_t suspendStack();
/**
* @brief WiFi OTA 结束后恢复 BLE 广播(栈仍在则只重新 adv)。
* @return ESP_OK 成功或无需恢复。
*/
esp_err_t resumeStack();
/** @brief 当前是否因 WiFi OTA 挂起 BLE。 */
bool isSuspendedForWifiOta() const { return m_suspendedForWifiOta; }
/**
* @brief 通过 BLE Notify 推送响应数据。
* @param data 待推送的数据指针。
@@ -60,6 +77,8 @@ public:
uint16_t m_esp32LastSeq = 0xFFFFU; ///< 最后一次数据包序号(用于补发 ACK)
TaskHandle_t m_taskHandle = nullptr; ///< BLE OTA 守护任务句柄
TaskHandle_t m_workerTaskHandle = nullptr; ///< BLE OTA 数据工作线程句柄
bool m_stackRunning = false; ///< NimBLE 已 init 且 host 在跑
bool m_suspendedForWifiOta = false; ///< 因 WiFi OTA 主动挂起
};
#endif // BLE_OTA_H
+148 -126
View File
@@ -8,6 +8,7 @@
#include <algorithm>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_http_server.h"
#include "esp_wifi.h"
#include "esp_netif.h"
@@ -121,7 +122,7 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req)
httpd_resp_send(req, resp, strlen(resp));
DEBUG_INFO("OTA", "响应已发送,等待 TCP 刷新...");
vTaskDelay(pdMS_TO_TICKS(500));
vTaskDelay(pdMS_TO_TICKS(100));
OtaCoordinator::instance().restartEsp32();
return ESP_OK;
}
@@ -172,21 +173,16 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
return total;
}
/* OTA 上传静态收块:避免 OTA 中途再 malloc(2KB) 与 TCP 窗口抢堆。 */
static uint8_t s_ota_upload_recv[OTA_BLOCK_SIZE];
static uint8_t s_ota_json_scratch[256];
static uint8_t s_ota_preread[256];
esp_err_t ota_upload_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
// per-request 接收块:2KB,避免大块 malloc 失败 / 碎片
uint8_t *recv_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!recv_buf) {
DEBUG_ERR("WIFI", "/ota/upload malloc(%u) 失败 free=%lu largest=%lu",
(unsigned)OTA_BLOCK_SIZE,
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
return ESP_FAIL;
}
uint8_t *recv_buf = s_ota_upload_recv;
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d free=%lu largest=%lu",
req->content_len,
(unsigned long)esp_get_free_heap_size(),
@@ -201,35 +197,21 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
int peek_ret = httpd_req_recv(req, &peek, 1);
if (peek_ret <= 0) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
free(recv_buf);
return ESP_FAIL;
}
bool is_esp32_ota = false;
uint32_t total_size = 0;
int json_header_len = 0;
// ★ 改为堆分配:pre_read_buf[256]+json_buf[256] 同时在栈上占 512B,
// 上传 100% 后 GD32 路径调用 computeFlashSha256(栈上 mbedtls_sha256_context
// ~108B)+ 日志格式化 608B,叠加逼近 8192 栈极限 → 栈溢出卡死。
// 移到堆上释放栈压力。json_buf 在解析完成后立即释放。
uint8_t *pre_read_buf = (uint8_t *)calloc(256, 1);
if (!pre_read_buf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
/* 静态 scratch:OTA 期间少两次 256B heap 分配 */
uint8_t *pre_read_buf = s_ota_preread;
memset(pre_read_buf, 0, 256);
int pre_read_len = 1;
pre_read_buf[0] = (uint8_t)peek;
if (peek == '{') {
char *json_buf = (char *)calloc(256, 1);
if (!json_buf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
char *json_buf = (char *)s_ota_json_scratch;
memset(json_buf, 0, 256);
json_buf[0] = peek;
json_header_len = recv_json_header(req, json_buf + 1, 255);
if (json_header_len > 0) {
@@ -245,7 +227,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
if (type_item && cJSON_IsString(type_item) &&
strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
is_esp32_ota = true;
DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
}
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
if (size_item) {
@@ -254,17 +235,17 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
cJSON_Delete(root);
}
}
free(json_buf); // JSON 解析完成,立即释放
}
if (is_esp32_ota) {
if (total_size == 0) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
/* ESP32 路径不再需要 pre_read(JSON 已消费,固件从流继续读) */
pre_read_len = 0;
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) {
@@ -272,8 +253,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
} else {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
}
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
@@ -286,76 +265,90 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
size_t total_firmware_written = 0;
size_t total_received = json_header_len;
size_t last_log_recv = 0;
DEBUG_INFO("WIFI", "ESP32 OTA 开始收流: content_len=%d json_hdr=%d",
req->content_len, json_header_len);
DEBUG_INFO("WIFI", "ESP32 OTA 开始收流: content_len=%d json_hdr=%d fw_expect=%d",
req->content_len, json_header_len,
(req->content_len > json_header_len)
? (req->content_len - json_header_len) : 0);
while (total_received < req->content_len) {
size_t to_read = req->content_len - total_received;
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
/* 浏览器 upload 进度到 100% 只表示 TCP 发完;设备仍在 recv+Flash。
* Flash 扇区擦除期间 cache 暂停,必须让 WiFi/LWIP 有机会跑,否则 TCP 卡死。
* C3 上 WiFi+BLE 双栈堆极紧:free 过低时 pbuf 申请失败 → 半路卡住/超时。
* 因此 pacing 按 free heap 自适应,而不是固定每 N 块 delay。 */
const int64_t t_recv0 = esp_timer_get_time();
uint32_t write_chunks = 0U;
while (total_received < (size_t)req->content_len) {
size_t to_read = (size_t)req->content_len - total_received;
if (to_read > OTA_BLOCK_SIZE) {
to_read = OTA_BLOCK_SIZE;
}
/* 堆过紧时先让 LWIP 回收 TCP 窗口,再 recv,避免 pbuf 分配失败 */
{
const uint32_t free_pre = esp_get_free_heap_size();
if (free_pre < 8192U) {
(void)esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 2U));
}
}
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
if (ret <= 0) {
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d",
ret, (unsigned)total_received, req->content_len);
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d free=%lu",
ret, (unsigned)total_received, req->content_len,
(unsigned long)esp_get_free_heap_size());
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
err = core.write(fw_buf, (size_t)ret);
if (err != ESP_OK) {
DEBUG_ERR("WIFI", "OTA write failed: %s @%u",
esp_err_to_name(err), (unsigned)total_firmware_written);
DEBUG_ERR("WIFI", "OTA write failed: %s @%u free=%lu",
esp_err_to_name(err), (unsigned)total_firmware_written,
(unsigned long)esp_get_free_heap_size());
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
total_firmware_written += (size_t)ret;
total_received += (size_t)ret;
++write_chunks;
/* 长写 Flash:喂狗 + 进度日志,避免“浏览器 100% 而设备仍在写”时假死无日志 */
(void)esp_task_wdt_reset();
if (total_received - last_log_recv >= (128U * 1024U) ||
total_received >= (size_t)req->content_len) {
last_log_recv = total_received;
DEBUG_INFO("WIFI", "ESP32 OTA 收流进度: %u / %d (written=%u)",
(unsigned)total_received, req->content_len,
(unsigned)total_firmware_written);
}
}
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)",
(unsigned int)core.getReceived(), json_header_len);
DEBUG_INFO("WIFI", "堆(写完): free=%lu largest=%lu",
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
/* ★ 先释放上传缓冲再回包:堆碎时 largest~1KB,先 free 4KB/2KB 再 send,
* 避免 LWIP/httpd 发响应时 malloc pbuf 失败导致客户端卡在 100%。 */
free(recv_buf);
recv_buf = nullptr;
free(pre_read_buf);
pre_read_buf = nullptr;
/* 回包前不打 INFO:堆紧时 DEBUG_INFO→UDP 会与 httpd_resp_send 抢 TX pbuf。
* 先让 LWIP 回收,成功后再记一条。 */
vTaskDelay(pdMS_TO_TICKS(10));
(void)esp_task_wdt_reset();
taskYIELD();
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
const char resp[] =
"{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", "
"\"target\": \"esp32\"}";
httpd_resp_set_type(req, "application/json");
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
httpd_resp_set_hdr(req, "Connection", "close");
esp_err_t send_err = httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
if (send_err != ESP_OK) {
DEBUG_ERR("WIFI", "ESP32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
esp_err_to_name(send_err),
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
/* 再试一次极简回包 */
send_err = httpd_resp_send(req, "{\"status\":\"ok\",\"target\":\"esp32\"}",
HTTPD_RESP_USE_STRLEN);
if (send_err != ESP_OK) {
DEBUG_ERR("WIFI", "ESP32 OTA 简短响应仍失败: %s", esp_err_to_name(send_err));
}
} else {
DEBUG_INFO("WIFI", "ESP32 OTA 上传响应已发送 (free=%lu)",
DEBUG_INFO("WIFI",
"ESP32 OTA 存储+响应完成: %u B elapsed=%lld ms free=%lu",
(unsigned int)core.getReceived(),
(long long)((esp_timer_get_time() - t_recv0) / 1000),
(unsigned long)esp_get_free_heap_size());
}
return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL;
@@ -379,22 +372,16 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u max=%u",
(unsigned)gd32_valid_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
} else if (prep_err == ESP_ERR_INVALID_STATE) {
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
} else if (prep_err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
char err_msg[96];
snprintf(err_msg, sizeof(err_msg), "prepare gd32_ota failed: %s", esp_err_to_name(prep_err));
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
@@ -411,7 +398,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
size_t last_log_recv = (size_t)received;
if (json_header_len == 0 && pre_read_len > 0) {
int first_chunk_len = pre_read_len;
@@ -425,8 +411,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
// ★ 首块写入失败:用 abortCurrentIfEpoch 避免误中止新会话
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
received = first_chunk_len;
@@ -448,8 +432,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
// ★ 客户端断开(刷新网页/关浏览器/网络掉):用 abortCurrentIfEpoch 避免误中止新会话
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
@@ -460,8 +442,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
// ★ 写入失败:同上,用 abortCurrentIfEpoch
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
free(recv_buf);
free(pre_read_buf);
return ESP_FAIL;
}
@@ -469,46 +449,28 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
gd32_offset += (uint32_t)ret;
(void)esp_task_wdt_reset();
if ((size_t)received - last_log_recv >= (64U * 1024U) ||
(size_t)received >= (size_t)req->content_len) {
last_log_recv = (size_t)received;
DEBUG_INFO("WIFI", "GD32 OTA 收流进度: %d / %d (written=%u)",
received, req->content_len, (unsigned)gd32_offset);
}
}
OtaCoordinator::instance().updateGd32Meta(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
DEBUG_INFO("WIFI", "GD32 upload done: %u / %u bytes", (unsigned)gd32_offset, expected_fw_size_raw);
ota_meta_t final_meta;
OtaCoordinator::instance().loadGd32Meta(&final_meta);
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
/* 先释放上传缓冲再算 SHA,避免与 2KB recv 并存 */
free(recv_buf);
recv_buf = nullptr;
free(pre_read_buf);
pre_read_buf = nullptr;
/* 先让出再算 SHA;回包前不打 INFO,避免与 httpd_resp_send 抢堆 */
vTaskDelay(pdMS_TO_TICKS(10));
(void)esp_task_wdt_reset();
DEBUG_INFO("WIFI", "GD32 firmware fully stored, pre-computing SHA-256 (free=%lu)...",
(unsigned long)esp_get_free_heap_size());
// 与 BLE 共用 finishGd32StorageAfterReceive:complete + SHA
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
if (sha_err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
}
} else {
free(recv_buf);
recv_buf = nullptr;
free(pre_read_buf);
pre_read_buf = nullptr;
}
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\", "
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Connection", "close");
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
if (send_err != ESP_OK) {
DEBUG_ERR("WIFI", "GD32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
@@ -516,7 +478,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
(unsigned long)esp_get_free_heap_size(),
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
} else {
DEBUG_INFO("WIFI", "GD32 OTA 上传响应已发送 (free=%lu)",
DEBUG_INFO("WIFI", "GD32 OTA 存储+响应完成: %u / %u free=%lu",
(unsigned)gd32_offset, expected_fw_size_raw,
(unsigned long)esp_get_free_heap_size());
}
return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL;
@@ -886,7 +849,7 @@ esp_err_t spi_send_handler(httpd_req_t *req)
uint8_t resp_type = 0;
uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0};
size_t resp_len = 0;
esp_err_t spi_err = SpiMaster::instance().otaTransfer(frame_type, payload_data, payload_len,
esp_err_t spi_err = SpiMaster::instance().transact(frame_type, payload_data, payload_len,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
timeout_ms);
@@ -913,8 +876,24 @@ esp_err_t spi_send_handler(httpd_req_t *req)
}
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 8192;
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 24;
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144;
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 16;
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
static SemaphoreHandle_t s_status_json_mux = nullptr;
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
static SemaphoreHandle_t statusJsonMutex(void)
{
if (s_status_json_mux == nullptr) {
portENTER_CRITICAL(&s_status_json_init_mux);
if (s_status_json_mux == nullptr) {
s_status_json_mux = xSemaphoreCreateMutex();
}
portEXIT_CRITICAL(&s_status_json_init_mux);
}
return s_status_json_mux;
}
struct CpuTaskSample {
UBaseType_t task_number;
@@ -1124,6 +1103,39 @@ esp_err_t system_status_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
/* 堆极紧时仍返回精简 JSON,禁止 500(前端轮询会刷红)。 */
const size_t largest_now = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
if (largest_now < 512U) {
char tiny[192];
int n = snprintf(tiny, sizeof(tiny),
"{\"version\":\"%s\",\"free_heap\":%lu,\"largest_free_block\":%lu,"
"\"wifi\":{\"connected\":%s},\"ota\":{\"sys_state\":\"degraded\"},"
"\"cpu\":{\"available\":false,\"valid\":false}}",
FW_VERSION,
(unsigned long)esp_get_free_heap_size(),
(unsigned long)largest_now,
(WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA &&
WifiOta::instance().getStaConnected()) ? "true" : "false");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, tiny, n > 0 ? n : 0);
return ESP_OK;
}
SemaphoreHandle_t mux = statusJsonMutex();
if (mux) {
if (xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
/* 并发 status 冲突:仍回简包,避免 200 */
char busy[160];
snprintf(busy, sizeof(busy),
"{\"version\":\"%s\",\"ota\":{\"sys_state\":\"busy\"},"
"\"cpu\":{\"available\":false,\"valid\":false}}",
FW_VERSION);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, busy, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
}
char ip_str[16] = "0.0.0.0";
int8_t rssi = -127;
bool connected = false;
@@ -1162,19 +1174,24 @@ esp_err_t system_status_handler(httpd_req_t *req)
task_metrics[i].handle = xTaskGetCurrentTaskHandle();
}
}
CpuUsageSnapshot* cpu = (CpuUsageSnapshot*)calloc(1, sizeof(CpuUsageSnapshot));
if (!cpu) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
return ESP_FAIL;
}
sample_cpu_usage(*cpu);
char* resp = (char*)calloc(1, SYSTEM_STATUS_JSON_CAP);
if (!resp) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
free(cpu);
return ESP_FAIL;
/* CPU 采样在堆上;失败则跳过(available=false),不再 500 */
CpuUsageSnapshot cpu_local = {};
CpuUsageSnapshot *cpu = &cpu_local;
CpuUsageSnapshot *cpu_heap = nullptr;
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 1024U)) {
cpu_heap = (CpuUsageSnapshot *)calloc(1, sizeof(CpuUsageSnapshot));
if (cpu_heap) {
cpu = cpu_heap;
sample_cpu_usage(*cpu);
}
} else {
cpu_local.available = false;
cpu_local.valid = false;
}
char *resp = s_status_json;
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
int len = snprintf(resp, SYSTEM_STATUS_JSON_CAP,
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, "
@@ -1219,7 +1236,8 @@ esp_err_t system_status_handler(httpd_req_t *req)
len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, *cpu);
/* SPI Link v2 统计(SPEC §12.1) */
/* SPI Link v2 统计(SPEC §12.1)— linkStats 用 5ms 有界超时取 SPI 锁,
* OTA 中继期间拿不到锁时返回零值统计,绝不阻塞 /system/status 响应。 */
{
SpiMaster::SpiLinkStats ls = SpiMaster::instance().linkStats();
const char *role_str = "unknown";
@@ -1263,9 +1281,13 @@ esp_err_t system_status_handler(httpd_req_t *req)
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, "}}");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
free(resp);
free(cpu);
httpd_resp_send(req, resp, len > 0 ? len : 0);
if (cpu_heap) {
free(cpu_heap);
}
if (mux) {
xSemaphoreGive(mux);
}
return ESP_OK;
}
+4 -2
View File
@@ -424,8 +424,10 @@ esp_err_t WifiOta::httpServerStart()
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
config.lru_purge_enable = false;
config.recv_wait_timeout = 60; // 1MB OTA 写 Flash 可能超过 30s 空闲读
config.send_wait_timeout = 30;
/* 1MB sequential 擦写时,httpd 在 write 阶段可能 >60s 不调 recv;
* 超时后 handler 内会重试,但过短会放大“卡 100%”观感。 */
config.recv_wait_timeout = 120;
config.send_wait_timeout = 60;
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
+2 -2
View File
@@ -32,7 +32,7 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
size_t resp_len = 0U;
/* 两步法:otaTransfer 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
* CAN 帧非幂等,命令只发一次,不重发。 */
esp_err_t ret = SpiMaster::instance().otaSendAndPoll(
esp_err_t ret = SpiMaster::instance().transact(
kSpiCanFrameType,
tx_buf, tx_len,
&resp_type, recv_buf, recv_max, &resp_len, timeout_ms);
@@ -175,7 +175,7 @@ int canFdSetRxMode(can_rx_mode_t mode,
/* 两步法破解延迟一拍:发命令一次 + poll 真实响应。
* 0xC2 幂等,但两步法避免无效重试。 */
esp_err_t ret = SpiMaster::instance().otaSendAndPoll(
esp_err_t ret = SpiMaster::instance().transact(
kSpiCanSetModeType,
&req, 1U,
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
File diff suppressed because it is too large Load Diff
+115 -4
View File
@@ -19,6 +19,8 @@
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "spi_link_v2.h"
// SPI 配置常量
@@ -110,6 +112,33 @@ public:
/** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
uint8_t remoteCredit() const { return m_remoteCredit; }
/**
* @brief OTA 中继期间禁止 worker 按铃读。
*
* PB6 在 OTA 下也会因 WAIT/ACK 拉低。若 worker 在 job 间隙把 RESPONSE
* 当 EVENT 偷读,业务 poll 只能空等 → 约 10s 后 DATA 重发 → GD32 DUP-SEQ,
* 表现为刷写断断续续。中继开始设 true,结束/复位设 false。
*/
void setSuppressRingService(bool suppress);
/**
* @brief OTA 业务层确认当前请求已成功(结构化 ACK 校验通过)。
* 在 SPI 头 ack_seq 偶发失配但仍接受业务 ACK 时补齐 m_txSeq,避免后续包卡死。
*/
void confirmTxSeqAccepted();
/**
* @brief L4 统一请求入口:经 SPI worker 串行执行(send+poll)。
*/
esp_err_t transact(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
/**
* @brief L4 取出 GD32 EVENT 帧(worker 已读入事件队列)。
*/
bool takeEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len);
// ============================================================
// SPI Link v2 链路层(阶段1:帧格式 + 状态成员就位)
// ============================================================
@@ -138,6 +167,9 @@ public:
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
SpiLinkStats linkStats();
/** @brief L4 链路状态(linkStats 别名)。 */
SpiLinkStats linkStatus() { return linkStats(); }
/** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。
* 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。
* 成功后 m_v2Active=true,序号归零。
@@ -251,14 +283,71 @@ private:
};
SpiLinkState m_linkState;
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,SPI 任务消费。 */
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,仅 SPI worker 消费。 */
volatile bool m_gd32RequestPending = false;
/** SPI 任务句柄(用于 ISR 唤醒)。 */
/** OTA 中继时为 true:worker 不 serviceRing,避免偷读 OTA ACK/WAIT。 */
volatile bool m_suppressRingService = false;
/** SPI worker 任务句柄(ISR 唯一通知目标)。 */
TaskHandle_t m_spiTaskHandle = NULL;
/** GD32 按铃请求帧缓冲区。 */
TaskHandle_t m_workerTask = NULL;
QueueHandle_t m_jobQueue = NULL;
/** 事件计数信号量(worker push / takeEvent pop)。 */
SemaphoreHandle_t m_eventSem = NULL;
/** GD32 按铃请求帧缓冲区(兼容 takeRingRx 单槽)。 */
uint8_t m_ringRxBuf[SPI_OTA_FRAME_MAX] = {};
size_t m_ringRxLen = 0;
uint8_t m_ringRxType = 0; ///< GD32 按铃请求帧 TYPE(checkGd32Request 填充)
uint8_t m_ringRxType = 0;
// ── L4 多槽事件队列(worker 写入,业务 takeEvent 读出)──
static constexpr uint8_t kEventSlots = 4U;
struct EventSlot {
uint8_t type;
uint16_t len;
uint8_t data[SPI_OTA_FRAME_MAX];
};
EventSlot m_eventQ[kEventSlots] = {};
uint8_t m_eventHead = 0;
uint8_t m_eventTail = 0;
uint8_t m_eventCount = 0;
// ── L4 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
enum class JobKind : uint8_t {
Transact = 0,
Poll,
Hello,
EnterBoot,
Ping,
WaitBoot,
ServiceRing,
};
struct Job {
JobKind kind = JobKind::Transact;
uint32_t timeout_ms = 0;
uint8_t type = 0;
uint8_t expected_type = 0xFFU;
uint8_t role_hint = 0;
size_t payload_len = 0;
uint8_t payload[SPI_OTA_FRAME_MAX] = {};
uint8_t *out_type = nullptr;
uint8_t *out_payload = nullptr;
size_t out_max = 0;
size_t *out_len = nullptr;
uint32_t *out_uptime = nullptr;
gd32_version_t *out_version = nullptr;
esp_err_t result = ESP_FAIL;
SemaphoreHandle_t done = nullptr;
};
static constexpr UBaseType_t kJobQueueLen = 6;
bool onWorker() const;
esp_err_t submitJob(Job &job);
void executeJob(Job &job);
static void workerEntry(void *arg);
void workerLoop();
esp_err_t serviceRingOnce();
bool pushEvent(uint8_t type, const uint8_t *payload, size_t len);
bool popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t *len);
// ── 帧内同步状态机 ──
volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口)
@@ -272,6 +361,8 @@ private:
volatile uint8_t m_expectedRxSeq = 0; ///< 期望收到的对端序号
volatile uint8_t m_gd32Role = 0; ///< GD32 角色(0=unknown, 1=app, 2=boot)
volatile bool m_v2Active = false; ///< v2 session 是否激活
/** 最近一次 otaFindFrame 解析到的 RESPONSE 是否与 m_lastSentSeq 归属匹配 */
volatile bool m_lastRxAckMatched = false;
SpiLinkStats m_stats = {}; ///< 链路统计
// RAII 锁守卫
@@ -321,6 +412,26 @@ private:
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms = 0U);
/**
* @brief 底层传输,支持 SPI 事务 flags(如 SPI_TRANS_CS_KEEP_ACTIVE)。
* 多段同 CS 变长读时:先 spi_device_acquire_bus,段间 KEEP_ACTIVE,末段清 flag 后 release_bus。
*/
esp_err_t transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms, uint32_t trans_flags);
/**
* @brief L3 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
* 调用者须已持锁;仅在 DATA_READY 已低(有数据)时调用,禁止盲读。
* @param out_frame 完整帧输出(含头+payload+CRC)
* @param out_max 输出缓冲容量
* @param out_len 实际帧长
* @param timeout_ms 单段 SPI 超时
*/
esp_err_t readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
uint32_t timeout_ms);
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
/**
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
+18 -5
View File
@@ -14,8 +14,19 @@ CONFIG_DCU_OTA_PACKET_INTERVAL_MS=3
# 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE)
# 提到 12 留足缓冲,配合 can_feedback_task 退避从根本上避免 socket 饥荒
CONFIG_LWIP_MAX_SOCKETS=12
CONFIG_LWIP_TCP_WND_DEFAULT=11480
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=11520
# C3 堆紧:WiFi+BLE 双栈时 TCP 窗口过大 → OTA 收流 free 压到 ~5KB → 卡半/堆爆。
# 6144=6KB≈4*MSS:优先压峰值堆;OTA 瓶颈多在 Flash 擦写,吞吐损失通常可接受。
# 注意:仅改 defaults 不会覆盖已有 sdkconfig,构建前须同步写入 sdkconfig。
CONFIG_LWIP_TCP_WND_DEFAULT=6144
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=6144
# 乱序队列 pbuf:4→2,少占堆;HTTP 顺序上传基本不需要大 OOSEQ
CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=2
# WiFi 动态缓冲(C3 + NimBLE 常驻):32 过大,OTA 峰值与 TCP 窗口叠加易堆碎。
# 16/16/6 对单连接 OTA 足够,省出 ~10–20KB 峰值给 Flash 写路径。
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=6
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=16
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=16
# FreeRTOS runtime stats -- /system/status CPU usage
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
@@ -39,10 +50,12 @@ CONFIG_BT_NIMBLE_HR_SERVICE=n
CONFIG_BT_NIMBLE_BAS_SERVICE=n
CONFIG_BT_NIMBLE_DIS_SERVICE=n
# ── NimBLE: OTA 窗口突发接收容量(恢复 7feef70 对应的 IDF 6.0 默认值)──
# ── NimBLE: 缓冲略收(WiFi OTA 优先;BLE OTA 单连接仍够用)──
# 原 24/24 为 BLE 突发优化,但与 WiFi OTA 抢堆导致“卡半 + free~5KB”。
# 12/16:BLE 吞吐略降,WiFi 峰值堆更稳。
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=24
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=16
# Event buffers: 30→16 (省 ~1 KB)
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=16
# Prepared write entries: 64→16 (省 ~1 KB)
+194 -66
View File
@@ -129,11 +129,103 @@ function otaSessionTransition(newState, reason) {
else if (newState !== OtaSessionState.VERIFYING) isEsp32Rebooting = false;
}
/** @description 将会话从 FAILED/DONE 复位到 IDLE(用户发起新上传或设备空闲时调用)。 */
function otaSessionReset() {
/**
* @description 清掉本会话残留 UI/标志(失败、超时、中止、新会话开始前都要调)。
* 不改变 otaSessionState(由调用方负责 FAILED/IDLE 转换)。
* @param {object} [opts]
* @param {boolean} [opts.keepErrorStages] true=保留错误阶段样式;false=节点复位到待机
* @param {string} [opts.statusText] 状态栏文案;null 表示「待机」
* @param {boolean} [opts.errorStyle] 状态栏是否红色
*/
function clearOtaSessionResiduals(opts) {
opts = opts || {};
stopRelayProgressWatch();
stopUploadWaitHeartbeat();
gd32CompleteAnnounced = false;
relayWatchLastSeq = -1;
gd32RelaySessionLive = false;
lastSysStateCode = -1;
lastRelayState = 0;
isEsp32Rebooting = false;
rebootObservedDown = false;
rebootStartTime = 0;
isUploading = false;
/* 先摘掉引用再 abort,避免 abort 事件回调再次进入 cleanup 递归 */
var xh = activeXhr;
activeXhr = null;
if (xh) {
try { xh.abort(); } catch (_) {}
}
// 校验面板
var verifyInfo = document.getElementById('stageVerifyInfo');
if (verifyInfo) verifyInfo.style.display = 'none';
var sviFront = document.getElementById('sviFrontSha');
var sviDevice = document.getElementById('sviDeviceSha');
var sviResult = document.getElementById('sviResult');
if (sviFront) sviFront.textContent = '-';
if (sviDevice) sviDevice.textContent = '-';
if (sviResult) {
sviResult.textContent = '-';
sviResult.className = 'svi-val';
}
// 阶段节点
if (!opts.keepErrorStages) {
['es1','es2','es3','es4','es5','es6','s1','s2','s3','s4','s5','s6'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.className = 'stage-node';
});
['esd1','esd2','esd3','esd4','esd5','esd6','sd1','sd2','sd3','sd4','sd5','sd6'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
esp32OtaPhase = 0;
}
// 顶部步骤 pill
['ppEsp32', 'ppGd32Ota'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.classList.remove('active', 'done');
});
var fill = document.getElementById('progressFill');
if (fill) fill.classList.remove('done');
activePhase = null;
// 状态栏
var sb = document.getElementById('stageStatusBar');
var st = document.getElementById('stageStatusText');
if (sb) {
sb.className = opts.errorStyle ? 'stage-status-bar err' : 'stage-status-bar';
}
if (st) {
st.textContent = (opts.statusText !== undefined && opts.statusText !== null)
? opts.statusText
: '待机';
}
// 设备侧缓存展示:失败后不要继续显示“已满缓存像成功”
var mOtaCache = document.getElementById('mOtaCache');
if (mOtaCache && opts.clearCacheDisplay) {
mOtaCache.textContent = '0 / 216 KB';
mOtaCache.className = 'v';
}
var sOtaStored = document.getElementById('sOtaStored');
if (sOtaStored && opts.clearCacheDisplay) {
sOtaStored.textContent = '--';
}
}
/** @description 将会话从 FAILED/DONE 复位到 IDLE(用户发起新上传或设备空闲时调用)。 */
function otaSessionReset() {
clearOtaSessionResiduals({
keepErrorStages: false,
statusText: '待机',
errorStyle: false,
clearCacheDisplay: false
});
if (otaSessionState !== OtaSessionState.IDLE) {
otaSessionState = OtaSessionState.IDLE;
otaSessionTarget = null;
@@ -141,6 +233,7 @@ function otaSessionReset() {
currentOtaTarget = null;
isUploading = false;
}
activeUploadChip = null;
}
// ==================== 折叠面板 ====================
@@ -237,63 +330,48 @@ function updateEsp32StagesError() {
* @param {string} reason 失败原因描述
*/
function cleanupAfterOtaFailure(reason) {
stopRelayProgressWatch();
// 0. 状态机:转入 FAILED(先于标志重置,确保 otaSessionActive() 即刻返回 false)
var chip = activeUploadChip || otaSessionTarget || currentOtaTarget || window.flashTarget || 'esp32';
// 0. 状态机:转入 FAILED(先于标志重置)
otaSessionTransition(OtaSessionState.FAILED, reason);
// 1. 重置上传锁
isUploading = false;
stopUploadWaitHeartbeat();
// 1. 清掉本会话全部残留标志 / 定时器 / 校验面板 / 旧 progress 语义
clearOtaSessionResiduals({
keepErrorStages: true,
statusText: reason || '失败',
errorStyle: true,
clearCacheDisplay: true
});
// 2. 中止正在进行的 XHR(如有)
if (activeXhr) {
try { activeXhr.abort(); } catch (_) {}
activeXhr = null;
}
// 3. 标记阶段指示器为错误态
if (currentOtaTarget === 'esp32') {
// 2. 阶段指示器标错
if (chip === 'esp32') {
updateEsp32StagesError();
} else if (currentOtaTarget === 'gd32') {
} else {
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. 进度条显示失败信息
// 3. 进度条失败文案(clear 已把 fill 的 done 去掉)
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 || otaSessionTarget || window.flashTarget || 'esp32';
// 4. 恢复上传按钮
var btnId = (chip === 'gd32') ? 'uploadGd32Btn' : 'uploadEsp32Btn';
var btn = document.getElementById(btnId);
if (btn && uploadBtnOrigText) {
if (btn) {
btn.disabled = false;
btn.textContent = uploadBtnOrigText;
if (uploadBtnOrigText) btn.textContent = uploadBtnOrigText;
}
activeUploadChip = null;
otaSessionTarget = null;
uploadBtnOrigText = '';
updateUploadButtonLabel();
// 10. 恢复轮询
// 5. 恢复轮询(之后 status 不得覆盖 FAILED UI,直至用户新上传或设备 idle 复位)
resumePollingAfterUpload();
// 11. 日志
addLog('E', 'OTA', '刷写流程已中止: ' + (reason || '未知原因'));
addLog('E', 'OTA', '刷写流程已中止,已清理残留状态: ' + (reason || '未知原因'));
}
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
@@ -928,7 +1006,15 @@ function updateSystemStatusUi(data) {
if (currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); }
if (isGd32Error && (currentOtaTarget === 'gd32' || otaSessionTarget === 'gd32')) {
cleanupAfterOtaFailure('GD32 OTA 失败');
var failDetail = 'GD32 OTA 失败';
if (relayTotal > 0) {
failDetail += '(已转发 ' + relaySeq + '/' + relayTotal +
',relay_state=' + rs + ',sys=' + (sysState || sysStateCode) + ')';
} else {
failDetail += '(relay_state=' + rs + ',sys=' + (sysState || sysStateCode) + ')';
}
cleanupAfterOtaFailure(failDetail);
pollLogs();
} else if (isGd32Error) {
// 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理
updateStagesFromRelayState(255);
@@ -936,27 +1022,26 @@ function updateSystemStatusUi(data) {
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' || sysStateCode === 4)) ||
/* 包已全部转发且 APP 已起来:即使日志停在 SEND_SIZE 也算完成 */
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal &&
(isGd32Relaying || otaSessionState === OtaSessionState.RELAYING || rs >= 3))) {
// 已完成:只进一次 UI/toast,后续轮询直接忽略
if (!gd32CompleteAnnounced && otaSessionState !== OtaSessionState.DONE) {
} else if (otaSessionState === OtaSessionState.UPLOADING) {
/* 上传进行中:不要用旧缓存/旧完成态覆盖进度条 */
} else if (otaSessionState === OtaSessionState.RELAYING) {
/* 必须先看到本会话真实中继(HANDSHAKE~FINISHING),才允许 DONE;
* 禁止一进 RELAYING 就吃掉上一次的 rs=5 / app_alive。 */
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
gd32RelaySessionLive = true;
}
if (gd32RelaySessionLive &&
(isGd32Complete || rs === 5 ||
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal && rs >= 3))) {
if (!gd32CompleteAnnounced) {
markGd32OtaUiComplete('GD32 刷写完成');
}
} else if (isGd32Storing || (total > 0 && stored < total && !isUploading)) {
setActivePhase('gd32Ota');
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
if (!isUploading) {
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total) + '');
}
} else if (rs === 1) {
// HANDSHAKING: 握手 + 挑战认证
// HANDSHAKING
setActivePhase('gd32Ota');
setProgress(50, '握手与认证中...');
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0)) {
// TRANSFERRING:以 relay_seq 为准(优先于 rs===2,避免 total 预写后一直显示 0/N)
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0 && rs !== 5)) {
// TRANSFERRING
setActivePhase('gd32Ota');
var relayOverall = 60 + Math.round(relayProgress * 0.30);
var relayLabel = relayTotal > 0
@@ -964,24 +1049,28 @@ function updateSystemStatusUi(data) {
: '数据包传输中...';
setProgress(relayOverall, relayLabel);
} else if (rs === 2 || (isGd32Relaying && relaySeq === 0)) {
// SIZE_NEG:只说协商中,不写「0/N」(total 会提前写入 status,易被误认为卡在 0 包)
// SIZE_NEG
setActivePhase('gd32Ota');
setProgress(55, relayTotal > 0
? ('大小协商中(共 ' + relayTotal + ' 包)')
: '固件大小协商中');
} else if (rs === 4) {
// FINISHING: 结束OTA + APP存活验证
setActivePhase('gd32Ota');
if (gd32AppAlive) {
if (!gd32CompleteAnnounced && otaSessionState !== OtaSessionState.DONE) {
markGd32OtaUiComplete('GD32 刷写完成');
if (!gd32CompleteAnnounced) {
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
}
} else if (!gd32CompleteAnnounced) {
setProgress(95, '收尾中,等待 APP 启动...');
}
} else if (total > 0 && stored >= total && !isGd32Relaying && otaSessionState !== OtaSessionState.RELAYING) {
} else if (!gd32RelaySessionLive) {
setActivePhase('gd32Ota');
setProgress(45, '上传完成,等待自动启动 GD32 OTA...');
setProgress(48, '已触发中继,等待设备进入握手…');
}
} else if (isGd32Storing || (total > 0 && stored < total && !isUploading &&
otaSessionState !== OtaSessionState.DONE &&
otaSessionState !== OtaSessionState.IDLE)) {
setActivePhase('gd32Ota');
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
if (!isUploading) {
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total) + '');
}
}
}
/** @description 测试与 ESP32 的连接(GET /status)。 */
@@ -1405,6 +1494,13 @@ async function startUpload(chip) {
// ★ 状态机:进入 UPLOADING 态(内部同步 isUploading = true)
otaSessionTransition(OtaSessionState.UPLOADING);
activeUploadChip = chip;
/* 新上传:清掉上次完成标志,避免立刻被 status 残留判成“又完成了” */
gd32CompleteAnnounced = false;
relayWatchLastSeq = -1;
gd32RelaySessionLive = false;
if (chip === 'esp32') {
updateEsp32StagesFromPhase(1);
}
// 跨通道互斥前置检查(含 BLE 取消后的瞬态重试)
var _probeReady = false;
for (var _pi = 0; _pi < 4; _pi++) {
@@ -1596,6 +1692,21 @@ async function startUpload(chip) {
uploadBtn.textContent = uploadBtnOrigText;
updateUploadButtonLabel();
if (xhr.status === 200) {
/* 校验 body:避免空响应/网关误 200 被当成上传成功 */
var bodyTxt = (xhr.responseText || '').trim();
var bodyOk = false;
try {
var bj = bodyTxt ? JSON.parse(bodyTxt) : null;
bodyOk = !!(bj && (bj.status === 'ok' || bj.target === chip));
} catch (_pe) {
bodyOk = bodyTxt.indexOf('"status"') >= 0 && bodyTxt.indexOf('ok') >= 0;
}
if (!bodyOk) {
addLog('E', '上传', 'HTTP 200 但响应异常: ' + (bodyTxt.slice(0, 120) || '(空)'));
showToast('上传响应异常,请重试', 'error');
cleanupAfterOtaFailure('响应异常');
return;
}
// 成功路径只打一条「上传完成」日志,避免 [上传]+[OTA] 语义重复
if (chip === 'esp32') {
addLog('I', 'OTA', '上传完成,开始 SHA-256 校验…');
@@ -1757,12 +1868,19 @@ var relayWatchLastUiLogAt = 0;
var relayWatchInFlight = false;
var gd32CompleteAnnounced = false;
var relayWatchLastSeq = -1;
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
var gd32RelaySessionLive = false;
function markGd32OtaUiComplete(msg) {
// 已公告过:禁止再改 className / setProgress / toast,否则每 400ms 重触发动效会「疯狂跳 ✓」
if (gd32CompleteAnnounced) {
return;
}
/* 仅本会话 RELAYING/FAILED 可宣布完成;IDLE/UPLOADING 时 status 里的旧 relay 残留一律忽略 */
if (otaSessionState !== OtaSessionState.RELAYING &&
otaSessionState !== OtaSessionState.FAILED) {
return;
}
gd32CompleteAnnounced = true;
stopRelayProgressWatch();
@@ -1824,7 +1942,12 @@ function startRelayProgressWatch() {
var alive = !!o.gd32_app_alive;
var rseq = Number(o.relay_seq) || 0;
var rtot = Number(o.relay_total_packets) || 0;
if (code === 4 || rstate === 5 || (alive && rtot > 0 && rseq >= rtot && rstate >= 3)) {
if (rstate >= 1 && rstate <= 4) {
gd32RelaySessionLive = true;
}
if (gd32RelaySessionLive &&
(code === 4 || rstate === 5 ||
(alive && rtot > 0 && rseq >= rtot && rstate >= 3))) {
markGd32OtaUiComplete('GD32 刷写完成');
pollLogs();
return;
@@ -2060,6 +2183,11 @@ async function pollStatus() {
consecutivePollFailures = 0;
addLog('I', '系统', '设备已恢复在线,轮询恢复正常');
showToast('设备已恢复在线', 'success');
// 设备离线后恢复在线,清理残留的 OTA 会话状态
if (otaSessionState !== OtaSessionState.IDLE) {
otaSessionReset();
addLog('I', '系统', '已清理 OTA 会话状态');
}
}
if (isEsp32Rebooting) {
var rbElapsed = Math.round((Date.now() - rebootStartTime) / 1000);
+1 -1
View File
@@ -427,7 +427,7 @@
<span id="toastMsg">操作成功</span>
</div>
<script src="app.js?v=20260713i"></script>
<script src="app.js?v=20260713m"></script>
<script>
window.connectionMode = window.connectionMode || 'wifi';
window.flashTarget = window.flashTarget || 'esp32';