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:
@@ -4,11 +4,11 @@
|
|||||||
// 固件版本
|
// 固件版本
|
||||||
#define FW_VERSION_MAJOR 0
|
#define FW_VERSION_MAJOR 0
|
||||||
#define FW_VERSION_MINOR 3
|
#define FW_VERSION_MINOR 3
|
||||||
#define FW_VERSION_PATCH 42
|
#define FW_VERSION_PATCH 43
|
||||||
#define FW_VERSION_STRING "0.3.42"
|
#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 信息
|
// Git 信息
|
||||||
#define GIT_HASH "0b36349"
|
#define GIT_HASH "7678234"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace {
|
|||||||
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
|
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
|
||||||
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
|
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
|
||||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
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";
|
constexpr const char *kTag = "CAN_FB";
|
||||||
|
|
||||||
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
|
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
|
||||||
@@ -72,8 +72,8 @@ static void taskLoop(void *)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 取出 checkGd32Request 存入的 type/payload */
|
/* 取出 checkGd32Request 存入的 type/payload */
|
||||||
if (!SpiMaster::instance().takeRingRx(&ring_type, ring_payload,
|
if (!SpiMaster::instance().takeEvent(&ring_type, ring_payload,
|
||||||
sizeof(ring_payload), &ring_len)) {
|
sizeof(ring_payload), &ring_len)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
-10
@@ -24,6 +24,8 @@
|
|||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
#include "driver/uart.h"
|
#include "driver/uart.h"
|
||||||
|
|
||||||
#include "debug_log.h"
|
#include "debug_log.h"
|
||||||
@@ -117,13 +119,37 @@ char* DebugLog::ringDrainJson(int *out_len)
|
|||||||
{
|
{
|
||||||
if (!out_len) return NULL;
|
if (!out_len) return NULL;
|
||||||
*out_len = 0;
|
*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();
|
int count = ringCount();
|
||||||
// ★ 限制单次导出条数,避免 count=64 时 malloc(32KB) 在堆紧张时失败
|
/* OTA 沉默失败排查需要一次多拉;上限 24 条 */
|
||||||
// 前端每秒轮询,16 条/次足够保持实时性;超出的旧条目下一轮再拉
|
static constexpr int kMaxDrain = 24;
|
||||||
static constexpr int kMaxDrain = 16;
|
|
||||||
if (count > kMaxDrain) count = kMaxDrain;
|
if (count > kMaxDrain) count = kMaxDrain;
|
||||||
int capacity = count * (LOG_RING_SIZE * 2) + 16;
|
int capacity = count * (LOG_RING_SIZE * 2) + 16;
|
||||||
if (capacity < 16) capacity = 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);
|
char *buf = (char*)malloc(capacity);
|
||||||
if (!buf) return NULL;
|
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 udp_ret = ESP_FAIL;
|
||||||
esp_err_t uart_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 调用)
|
// UART 通道(UART 不可用时跳过 uart_write_bytes 调用)
|
||||||
if (m_uartAvailable) {
|
if (m_uartAvailable) {
|
||||||
uart_ret = uartSend(data, len);
|
uart_ret = uartSend(data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UDP 通道(WiFi 未连接时 sendto 失败,静默跳过)
|
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
|
||||||
if (m_initialized) {
|
* WiFi 未连接时 sendto 失败也静默跳过。 */
|
||||||
|
if (m_initialized && free_now >= kSkipUdpFree) {
|
||||||
if (m_udpSock < 0) {
|
if (m_udpSock < 0) {
|
||||||
ensureUdpSocket();
|
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);
|
ringPush((const char *)data, (int)len);
|
||||||
|
|
||||||
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
|
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
/* 仅 ring 成功也算可用(OTA 堆紧时常见) */
|
||||||
|
if (m_logRingMutex) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +413,22 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
|
|||||||
return;
|
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;
|
const char *level_str;
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case DEBUG_LOG_ERR: level_str = "E"; break;
|
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);
|
snprintf(prefix, sizeof(prefix), "[%s][%s]", level_str, tag);
|
||||||
|
|
||||||
// 控制单条日志栈缓冲大小,避免调用任务(如 can_fb)因日志格式化爆栈。
|
/* 栈缓冲与 LOG_RING_SIZE 对齐,减轻 httpd 等任务栈压。 */
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
char msg[256];
|
char msg[160];
|
||||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||||
va_end(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);
|
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
|
||||||
|
|
||||||
// ★ snprintf 截断时返回"本应写入的长度"(可能 > sizeof(log_buf)),
|
// ★ snprintf 截断时返回"本应写入的长度"(可能 > sizeof(log_buf)),
|
||||||
|
|||||||
@@ -99,14 +99,25 @@ private:
|
|||||||
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
|
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
|
||||||
bool m_uartAvailable = false;
|
bool m_uartAvailable = false;
|
||||||
|
|
||||||
|
/* 环形日志:OTA 中继期需保留关键 INFO,64×160≈10KB BSS。 */
|
||||||
static constexpr int LOG_RING_SLOTS = 64;
|
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];
|
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE];
|
||||||
int m_logRingLens[LOG_RING_SLOTS];
|
int m_logRingLens[LOG_RING_SLOTS];
|
||||||
int m_logRingHead = 0;
|
int m_logRingHead = 0;
|
||||||
int m_logRingCount = 0;
|
int m_logRingCount = 0;
|
||||||
SemaphoreHandle_t m_logRingMutex = NULL;
|
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();
|
esp_err_t ensureUdpSocket();
|
||||||
void ringPush(const char *data, int len);
|
void ringPush(const char *data, int len);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "debug_log.h"
|
#include "debug_log.h"
|
||||||
#include "task_stack_check.h"
|
#include "task_stack_check.h"
|
||||||
#include "stack_overflow_hook.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();
|
uint32_t free_heap = esp_get_free_heap_size();
|
||||||
if (free_heap < HEAP_WARNING_MIN) {
|
if (free_heap < HEAP_WARNING_MIN) {
|
||||||
/* OTA 期间堆会短暂变紧,限流避免串口/UDP 二次吃堆、刷屏 */
|
/* WiFi OTA 收流时 TCP 窗口 + httpd 会把 free 压到 5~10KB,属预期,勿当故障刷屏。 */
|
||||||
static TickType_t s_last_heap_warn = 0;
|
static TickType_t s_last_heap_warn = 0;
|
||||||
TickType_t now = xTaskGetTickCount();
|
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;
|
s_last_heap_warn = now;
|
||||||
uint32_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
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)free_heap,
|
||||||
(unsigned long)esp_get_minimum_free_heap_size(),
|
(unsigned long)esp_get_minimum_free_heap_size(),
|
||||||
(unsigned long)largest_block);
|
(unsigned long)largest_block);
|
||||||
|
|||||||
@@ -213,14 +213,17 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
|
|||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sequential 模式写时可能触发扇区擦除,喂狗防 WDT */
|
/* sequential 模式写时可能触发扇区擦除(禁 cache),期间 WiFi 易饿死。
|
||||||
|
* 写后 yield 一次即可;写前再 yield 会无谓拖慢吞吐(httpd 侧另有堆感知 pacing)。 */
|
||||||
(void)esp_task_wdt_reset();
|
(void)esp_task_wdt_reset();
|
||||||
esp_err_t err = esp_ota_write(m_handle, data, len);
|
esp_err_t err = esp_ota_write(m_handle, data, len);
|
||||||
|
taskYIELD();
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
m_received += len;
|
m_received += len;
|
||||||
} else {
|
} else {
|
||||||
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u offset=%u)",
|
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u offset=%u free=%lu)",
|
||||||
esp_err_to_name(err), (unsigned)len, (unsigned)m_received);
|
esp_err_to_name(err), (unsigned)len, (unsigned)m_received,
|
||||||
|
(unsigned long)esp_get_free_heap_size());
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -433,7 +436,7 @@ bool Esp32OtaCore::forceCleanup()
|
|||||||
if (!s_mutex) return false;
|
if (!s_mutex) return false;
|
||||||
|
|
||||||
// 在互斥保护下检查 s_occupied
|
// 在互斥保护下检查 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;
|
bool was_occupied = s_occupied;
|
||||||
if (was_occupied) {
|
if (was_occupied) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "relay/ota_relay.h"
|
#include "relay/ota_relay.h"
|
||||||
#include "storage/gd32_flash_store.h"
|
#include "storage/gd32_flash_store.h"
|
||||||
#include "transport/spi_master.h"
|
#include "transport/spi_master.h"
|
||||||
|
#include "server/ble_ota.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
@@ -18,6 +19,23 @@
|
|||||||
|
|
||||||
#define TAG "OTA_COORD"
|
#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;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* WiFi GD32 上传前先挂 BLE,腾堆给 HTTP 收流 */
|
||||||
|
wifiOtaSuspendBleIfNeeded(channel);
|
||||||
|
|
||||||
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
|
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
|
||||||
err = beginGd32Storage(total_size, channel);
|
err = beginGd32Storage(total_size, channel);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
|
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
|
||||||
|
if (channel == OTA_CHANNEL_WIFI) {
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,15 +247,20 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
|
|||||||
void OtaCoordinator::completeGd32Storage()
|
void OtaCoordinator::completeGd32Storage()
|
||||||
{
|
{
|
||||||
bool h = lock();
|
bool h = lock();
|
||||||
|
bool resume_ble = false;
|
||||||
if (h && 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 是非忙状态
|
||||||
|
resume_ble = (m_otaChannel == OTA_CHANNEL_WIFI);
|
||||||
DEBUG_INFO(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
|
DEBUG_INFO(TAG, "GD32 固件存储完成, 系统状态 → GD32_STORED");
|
||||||
} else if (h) {
|
} else if (h) {
|
||||||
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
|
DEBUG_WARN(TAG, "completeGd32Storage 在非预期状态: %d", (int)m_sysState);
|
||||||
}
|
}
|
||||||
unlock(h);
|
unlock(h);
|
||||||
|
if (resume_ble) {
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
|
esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
|
||||||
@@ -315,7 +344,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
|
|||||||
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||||||
|
|
||||||
uint32_t cmd = 1;
|
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) {
|
if (ret != pdTRUE) {
|
||||||
DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败");
|
DEBUG_ERR(TAG, "向 OtaRelay 发送启动消息失败");
|
||||||
m_sysState = OTA_SYS_GD32_STORED;
|
m_sysState = OTA_SYS_GD32_STORED;
|
||||||
@@ -360,6 +389,8 @@ void OtaCoordinator::notifyRelayComplete(bool success)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
unlock(h);
|
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_otaChannel = channel; // ★ 记录发起通道
|
||||||
m_sysState = OTA_SYS_ESP32_RECEIVING;
|
m_sysState = OTA_SYS_ESP32_RECEIVING;
|
||||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
|
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
|
||||||
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING (epoch=%u, channel=%d)",
|
|
||||||
total_size, m_otaEpoch, (int)channel);
|
|
||||||
pauseCanFeedbackTask(true);
|
pauseCanFeedbackTask(true);
|
||||||
} else {
|
} else {
|
||||||
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
|
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)
|
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);
|
esp_err_t err = beginEsp32Ota(total_size, channel);
|
||||||
if (err == ESP_ERR_INVALID_STATE) {
|
if (err == ESP_ERR_INVALID_STATE) {
|
||||||
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
|
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
|
||||||
@@ -478,6 +510,9 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
|
|||||||
if (!isIdleState(cur) && cur != OTA_SYS_ERROR) {
|
if (!isIdleState(cur) && cur != OTA_SYS_ERROR) {
|
||||||
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
|
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
|
||||||
(int)cur);
|
(int)cur);
|
||||||
|
if (channel == OTA_CHANNEL_WIFI) {
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,6 +524,9 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channe
|
|||||||
Esp32OtaCore::instance().forceCleanup();
|
Esp32OtaCore::instance().forceCleanup();
|
||||||
err = beginEsp32Ota(total_size, channel);
|
err = beginEsp32Ota(total_size, channel);
|
||||||
}
|
}
|
||||||
|
if (err != ESP_OK && channel == OTA_CHANNEL_WIFI) {
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,7 +585,7 @@ Esp32FinalizeResult OtaCoordinator::finalizeForReboot(const char *frontend_sha_h
|
|||||||
void OtaCoordinator::restartEsp32()
|
void OtaCoordinator::restartEsp32()
|
||||||
{
|
{
|
||||||
notifyEsp32Rebooting();
|
notifyEsp32Rebooting();
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
esp_restart();
|
esp_restart();
|
||||||
// 不会返回
|
// 不会返回
|
||||||
}
|
}
|
||||||
@@ -573,6 +611,7 @@ void OtaCoordinator::doAbortLocked()
|
|||||||
m_gd32StorageDone = false;
|
m_gd32StorageDone = false;
|
||||||
m_busySinceTick = 0;
|
m_busySinceTick = 0;
|
||||||
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
||||||
|
/* 释放协调器锁后由调用方 resume BLE;doAbortLocked 可能持锁,标记后外层 resume */
|
||||||
}
|
}
|
||||||
|
|
||||||
void OtaCoordinator::abortCurrent()
|
void OtaCoordinator::abortCurrent()
|
||||||
@@ -582,6 +621,7 @@ void OtaCoordinator::abortCurrent()
|
|||||||
doAbortLocked();
|
doAbortLocked();
|
||||||
}
|
}
|
||||||
unlock(h);
|
unlock(h);
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t OtaCoordinator::getOtaEpoch() const
|
uint32_t OtaCoordinator::getOtaEpoch() const
|
||||||
@@ -607,6 +647,7 @@ bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
|
|||||||
doAbortLocked();
|
doAbortLocked();
|
||||||
|
|
||||||
unlock(h);
|
unlock(h);
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+147
-170
@@ -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 kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
|
||||||
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
||||||
static constexpr uint32_t kLastPacketAckSeq = 0xF0F0F0F0U; // 末包 ACK 序号约定
|
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);
|
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);
|
ota_result_t res = doRelayPackets(m_transferDeadline);
|
||||||
if (res != OTA_RESULT_OK) {
|
if (res != OTA_RESULT_OK) {
|
||||||
setState(RELAY_ERROR);
|
setState(RELAY_ERROR);
|
||||||
@@ -437,6 +450,9 @@ ota_result_t OtaRelay::forwardFirmware()
|
|||||||
|
|
||||||
DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size);
|
DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size);
|
||||||
|
|
||||||
|
/* 中继全程禁止 worker 把 OTA ACK/WAIT 当 EVENT 偷读 */
|
||||||
|
SpiMaster::instance().setSuppressRingService(true);
|
||||||
|
|
||||||
// 固件完整性校验
|
// 固件完整性校验
|
||||||
{
|
{
|
||||||
esp_err_t verify_err = Gd32FlashStore::instance().metaVerifyChecksum();
|
esp_err_t verify_err = Gd32FlashStore::instance().metaVerifyChecksum();
|
||||||
@@ -447,16 +463,10 @@ ota_result_t OtaRelay::forwardFirmware()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局超时计算
|
/* 全局 deadline 在进入 TRANSFERRING 时才启动(见 handleTransferring)。
|
||||||
constexpr uint32_t kMinGlobalTimeoutMs = 20U * 1000U; // 20 s 最小超时
|
* 握手 + Boot 擦 Flash(SIZE 阶段长时间 WAIT)不得占用转发预算,
|
||||||
constexpr uint32_t kMaxGlobalTimeoutMs = 60U * 1000U; // 60 s 硬上限
|
* 否则 1117 包会在擦除后仅剩十余秒被“全局超时”误杀。 */
|
||||||
uint32_t dynamic_ms = kMinGlobalTimeoutMs + (meta.total_size / 1024U) * 50U; // 50 ms / KB
|
m_transferDeadline = 0;
|
||||||
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);
|
|
||||||
|
|
||||||
// ── 驱动状态机 ──
|
// ── 驱动状态机 ──
|
||||||
// 5 步顺序推进:IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
|
// 5 步顺序推进:IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
|
||||||
@@ -475,6 +485,9 @@ ota_result_t OtaRelay::forwardFirmware()
|
|||||||
if (res != OTA_RESULT_OK) {
|
if (res != OTA_RESULT_OK) {
|
||||||
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
|
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
|
||||||
reset();
|
reset();
|
||||||
|
} else {
|
||||||
|
/* 成功路径:解除按铃抑制(reset 也会做,成功时状态机可能停在 DONE) */
|
||||||
|
SpiMaster::instance().setSuppressRingService(false);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -498,6 +511,7 @@ void OtaRelay::reset()
|
|||||||
memset(m_kSession, 0, sizeof(m_kSession));
|
memset(m_kSession, 0, sizeof(m_kSession));
|
||||||
memset(m_nonceServer, 0, sizeof(m_nonceServer));
|
memset(m_nonceServer, 0, sizeof(m_nonceServer));
|
||||||
memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime));
|
memset(m_gd32UidRuntime, 0, sizeof(m_gd32UidRuntime));
|
||||||
|
SpiMaster::instance().setSuppressRingService(false);
|
||||||
DEBUG_INFO("OTA", "状态机已复位 → IDLE");
|
DEBUG_INFO("OTA", "状态机已复位 → IDLE");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,6 +991,7 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
|||||||
return OTA_RESULT_ERR_TIMEOUT;
|
return OTA_RESULT_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpiMaster::instance().confirmTxSeqAccepted();
|
||||||
DEBUG_INFO("OTA", "大小协商完成: %" PRIu32 " 字节,GD32 已就绪收包", size);
|
DEBUG_INFO("OTA", "大小协商完成: %" PRIu32 " 字节,GD32 已就绪收包", size);
|
||||||
return OTA_RESULT_OK;
|
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_type = 0;
|
||||||
uint8_t resp_payload[32] = {0};
|
uint8_t resp_payload[32] = {0};
|
||||||
size_t resp_len = 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);
|
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();
|
TickType_t pkt_start = xTaskGetTickCount();
|
||||||
/* 单包硬超时:避免重试环 + WAIT/CREDIT 轮询在异常时“静默卡死”十几分钟。 */
|
bool saw_wait = false;
|
||||||
const TickType_t pkt_deadline = pkt_start + pdMS_TO_TICKS(15000);
|
|
||||||
TickType_t last_stall_log = pkt_start;
|
|
||||||
|
|
||||||
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
|
for (uint32_t send_i = 0; send_i < kMaxDataSends; ++send_i) {
|
||||||
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
|
/* 已见 WAIT:禁止再发 DATA,只延长 poll */
|
||||||
resend_packet:
|
if (saw_wait && send_i > 0U) {
|
||||||
while (1) {
|
DEBUG_INFO("OTA", "包 %" PRIu32 " 已见 WAIT,继续 poll 不重发", seq);
|
||||||
if (xTaskGetTickCount() >= pkt_deadline) {
|
} else {
|
||||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 单包超时(15s), retries=%" PRIu32 " credit=%u",
|
resp_len = 0;
|
||||||
seq, retry_count, (unsigned)SpiMaster::instance().remoteCredit());
|
resp_type = 0;
|
||||||
result = OTA_RESULT_ERR_TIMEOUT;
|
esp_err_t err = SpiMaster::instance().otaTransfer(
|
||||||
break;
|
SPI_OTA_CMD_DATA, tx_buf, 64,
|
||||||
}
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200);
|
||||||
if ((xTaskGetTickCount() - last_stall_log) >= pdMS_TO_TICKS(2000)) {
|
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||||
DEBUG_INFO("OTA", "包 %" PRIu32 " 仍在等待 ACK (retry=%" PRIu32 " credit=%u)",
|
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||||
seq, retry_count, (unsigned)SpiMaster::instance().remoteCredit());
|
saw_wait = true;
|
||||||
last_stall_log = xTaskGetTickCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
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_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));
|
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK &&
|
||||||
continue; // 重发本包
|
isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||||
}
|
SpiMaster::instance().confirmTxSeqAccepted();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (err == ESP_OK && SpiMaster::instance().remoteCredit() > 0U) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool got_expected = false;
|
|
||||||
if (got_expected_credit_ok) {
|
|
||||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||||
break;
|
if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) {
|
||||||
|
DEBUG_INFO("OTA", "转发进度 %" PRIu32 "/%" PRIu32,
|
||||||
|
seq + 1U, m_transferTotalPackets);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
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);
|
const TickType_t poll_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(kAckWaitMs);
|
||||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
TickType_t last_hb = xTaskGetTickCount();
|
||||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
|
while (xTaskGetTickCount() < poll_deadline) {
|
||||||
}
|
if ((xTaskGetTickCount() - last_hb) >= pdMS_TO_TICKS(1000)) {
|
||||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
DEBUG_INFO("OTA", "包 %" PRIu32 " 等 ACK… send=%" PRIu32 " type=0x%02X wait=%d credit=%u",
|
||||||
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
|
seq, send_i, resp_type, (int)saw_wait,
|
||||||
if (++retry_count >= kMaxResendOnStructError) {
|
(unsigned)SpiMaster::instance().remoteCredit());
|
||||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
last_hb = xTaskGetTickCount();
|
||||||
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) {
|
esp_err_t err = SpiMaster::instance().otaPollResponse(
|
||||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, kPollSliceMs);
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
saw_wait = true;
|
||||||
if (++retry_count >= kMaxResendOnNack) {
|
|
||||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
|
||||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK) {
|
||||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
|
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||||
if (++retry_count >= kMaxResendOnNack) {
|
SpiMaster::instance().confirmTxSeqAccepted();
|
||||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||||
result = OTA_RESULT_ERR_GD32;
|
if (seq < 5U || (seq % 10U) == 0U || seq + 1U == m_transferTotalPackets) {
|
||||||
break;
|
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 */
|
||||||
}
|
}
|
||||||
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1));
|
|
||||||
goto resend_packet;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
/* 超时:仅当从未见 WAIT 时才重发 DATA;见过 WAIT 则再 poll 一轮 */
|
||||||
break;
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TickType_t pkt_elapsed = xTaskGetTickCount() - pkt_start;
|
DEBUG_ERR("OTA", "包 %" PRIu32 " 失败 (elapsed=%" PRIu32 " ms credit=%u type=0x%02X saw_wait=%d)",
|
||||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 完成: elapsed=%" PRIu32 " ms, retries=%" PRIu32, seq,
|
seq,
|
||||||
(uint32_t)(pkt_elapsed * portTICK_PERIOD_MS), retry_count);
|
(uint32_t)((xTaskGetTickCount() - pkt_start) * portTICK_PERIOD_MS),
|
||||||
|
(unsigned)SpiMaster::instance().remoteCredit(),
|
||||||
if (result != OTA_RESULT_OK) {
|
resp_type, (int)saw_wait);
|
||||||
*out_result = result;
|
*out_result = OTA_RESULT_ERR_TIMEOUT;
|
||||||
return true; // 中止外层循环
|
return true;
|
||||||
}
|
|
||||||
return false; // 继续下一包
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
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};
|
uint8_t tx_buf[64] = {0};
|
||||||
ota_result_t result = OTA_RESULT_OK;
|
ota_result_t result = OTA_RESULT_OK;
|
||||||
|
TickType_t last_success_tick = xTaskGetTickCount();
|
||||||
|
|
||||||
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
|
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
|
||||||
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
|
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
|
||||||
@@ -1219,35 +1184,31 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
|||||||
return OTA_RESULT_ERR_USER_ABORT;
|
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;
|
size_t chunk_size = 0;
|
||||||
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
|
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
|
||||||
if (result != OTA_RESULT_OK) {
|
if (result != OTA_RESULT_OK) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 先写入「正在发第 N 包」,前端 /system/status 能看到卡在哪 */
|
||||||
|
setRelaySeq(seq);
|
||||||
|
|
||||||
// 发送单包并等待 ACK(内含重试循环)
|
// 发送单包并等待 ACK(内含重试循环)
|
||||||
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
|
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
|
||||||
|
DEBUG_ERR("OTA", "中止于包 %" PRIu32 "/%" PRIu32 " result=%d",
|
||||||
|
seq, m_transferTotalPackets, (int)result);
|
||||||
return result; // 中止
|
return result; // 中止
|
||||||
}
|
}
|
||||||
|
|
||||||
setRelaySeq(seq + 1);
|
setRelaySeq(seq + 1);
|
||||||
// 进度只写 relay_seq 供 /system/status;不打文字日志(前端进度条负责展示)。
|
last_success_tick = xTaskGetTickCount();
|
||||||
|
|
||||||
// ★ 包间延迟:给 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
DEBUG_INFO("OTA", "全部数据包已转发: %" PRIu32 " 包", m_transferTotalPackets);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1301,7 +1262,7 @@ ota_result_t OtaRelay::doSendFinishOta()
|
|||||||
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len);
|
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1);
|
DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1);
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1310,7 +1271,7 @@ ota_result_t OtaRelay::doSendFinishOta()
|
|||||||
1000, 10, 8, OtaRelay::isFinishPlainAck);
|
1000, 10, 8, OtaRelay::isFinishPlainAck);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_WARN("OTA", "FINISH_OTA poll 失败, err=%d, retry=%" PRIu32 "/10", err, retry + 1);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,7 +1281,7 @@ ota_result_t OtaRelay::doSendFinishOta()
|
|||||||
|
|
||||||
if (resp_type == SPI_OTA_RSP_WAIT) {
|
if (resp_type == SPI_OTA_RSP_WAIT) {
|
||||||
DEBUG_INFO("OTA", "FINISH_OTA 收到 WAIT, GD32 正在排空 Flash 缓冲, retry=%" PRIu32 "/10", retry + 1);
|
DEBUG_INFO("OTA", "FINISH_OTA 收到 WAIT, GD32 正在排空 Flash 缓冲, retry=%" PRIu32 "/10", retry + 1);
|
||||||
vTaskDelay(pdMS_TO_TICKS(200));
|
vTaskDelay(pdMS_TO_TICKS(20));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,13 +1289,13 @@ ota_result_t OtaRelay::doSendFinishOta()
|
|||||||
uint8_t err_code = 0;
|
uint8_t err_code = 0;
|
||||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
|
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);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%" PRIu32 "/10",
|
DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%" PRIu32 "/10",
|
||||||
resp_type, resp_len, retry + 1);
|
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)) {
|
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 存活验证
|
// ★ APP 存活验证
|
||||||
setGd32AppAlive(false);
|
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) {
|
for (uint32_t attempt = 0; attempt < 10; ++attempt) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
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)) {
|
if (probeAndCacheAppAlive(timeout_ms)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+129
-12
@@ -31,6 +31,7 @@
|
|||||||
#include "nimble/nimble_port_freertos.h"
|
#include "nimble/nimble_port_freertos.h"
|
||||||
#include "host/ble_hs.h"
|
#include "host/ble_hs.h"
|
||||||
#include "host/ble_gap.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/gap/ble_svc_gap.h"
|
||||||
#include "services/gatt/ble_svc_gatt.h"
|
#include "services/gatt/ble_svc_gatt.h"
|
||||||
#include "host/ble_att.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)attr_handle;
|
||||||
(void)arg;
|
(void)arg;
|
||||||
|
|
||||||
|
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||||
|
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||||
|
}
|
||||||
|
|
||||||
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
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)attr_handle;
|
||||||
(void)arg;
|
(void)arg;
|
||||||
|
|
||||||
|
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||||
|
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
||||||
|
}
|
||||||
|
|
||||||
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||||
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
@@ -1490,9 +1499,10 @@ static void ble_ota_data_worker_task(void *pvParameters)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 若 BLE 已断开,丢弃队列中的残留数据
|
// 若 BLE 已断开或 WiFi OTA 挂起,丢弃队列中的残留数据
|
||||||
if (!BleOta::instance().isConnected()) {
|
if (!BleOta::instance().isConnected() ||
|
||||||
DEBUG_DBG(TAG, "BLE 已断开,丢弃 %zu 字节队列数据", item.len);
|
BleOta::instance().isSuspendedForWifiOta()) {
|
||||||
|
DEBUG_DBG(TAG, "BLE 不可用/挂起,丢弃 %zu 字节队列数据", item.len);
|
||||||
free(item.data);
|
free(item.data);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1640,6 +1650,15 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
|||||||
|
|
||||||
case BLE_GAP_EVENT_CONNECT:
|
case BLE_GAP_EVENT_CONNECT:
|
||||||
if (event->connect.status == 0) {
|
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_bleConnected = true;
|
||||||
BleOta::instance().m_connHandle = event->connect.conn_handle;
|
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_bleConnected = false;
|
||||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||||
s_conn_fallback_requested = false;
|
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 断开时恢复 WiFi 射频
|
||||||
ble_ota_restore_wifi_ps();
|
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) {
|
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
|
||||||
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
|
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
|
||||||
OtaCoordinator::stateToString(cur));
|
OtaCoordinator::stateToString(cur));
|
||||||
} else {
|
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
|
||||||
if (cur == OTA_SYS_GD32_STORING) {
|
cur == OTA_SYS_ESP32_FINALIZING ||
|
||||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 GD32 固件存储");
|
cur == OTA_SYS_GD32_STORING) {
|
||||||
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
|
/* 仅当当前 OTA 通道是 BLE 时才因断连中止;WiFi 通道的 RECEIVING 不能被误杀 */
|
||||||
cur == OTA_SYS_ESP32_FINALIZING) {
|
ota_channel_t ch = OTA_CHANNEL_WIFI;
|
||||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
|
{
|
||||||
|
/* get channel via snapshot if available */
|
||||||
|
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
|
||||||
|
ch = snap.channel;
|
||||||
}
|
}
|
||||||
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 +
|
if (ch == OTA_CHANNEL_BLE) {
|
||||||
// 会话状态复位(m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/
|
DEBUG_WARN(TAG, "BLE 断开,中止 BLE 通道 OTA (state=%s)",
|
||||||
// reorder/s_gd32_ooo_count)
|
OtaCoordinator::stateToString(cur));
|
||||||
|
ble_ota_session_reset();
|
||||||
|
} else {
|
||||||
|
DEBUG_INFO(TAG, "BLE 断开但当前为 WiFi OTA,不中止会话");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ble_ota_session_reset();
|
ble_ota_session_reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1706,6 +1740,9 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||||
|
if (BleOta::instance().isSuspendedForWifiOta()) {
|
||||||
|
return 0; /* WiFi OTA 中不要自动再开广播 */
|
||||||
|
}
|
||||||
DEBUG_INFO(TAG, "广播周期结束,重新开始");
|
DEBUG_INFO(TAG, "广播周期结束,重新开始");
|
||||||
ble_ota_start_advertising();
|
ble_ota_start_advertising();
|
||||||
return 0;
|
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
|
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;
|
int rc;
|
||||||
|
|
||||||
// 自动推断可用地址类型(优先 Public,不可用则生成 Random)
|
// 自动推断可用地址类型(优先 Public,不可用则生成 Random)
|
||||||
@@ -1886,6 +1928,11 @@ esp_err_t BleOta::serverStart()
|
|||||||
{
|
{
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
|
if (m_stackRunning) {
|
||||||
|
DEBUG_INFO(TAG, "serverStart: 栈已在运行,跳过");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_INFO(TAG, "=== 初始化 BLE OTA GATT 服务器 ===");
|
DEBUG_INFO(TAG, "=== 初始化 BLE OTA GATT 服务器 ===");
|
||||||
DEBUG_INFO(TAG, " Device: %s", BLE_DEVICE_NAME);
|
DEBUG_INFO(TAG, " Device: %s", BLE_DEVICE_NAME);
|
||||||
DEBUG_INFO(TAG, " MTU: %" PRIu16, BLE_MTU_SIZE);
|
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);
|
nimble_port_freertos_init(ble_ota_nimble_host_task);
|
||||||
|
|
||||||
// s_data_out_val_handle 由 on_sync 回调填充,此处不复制
|
// s_data_out_val_handle 由 on_sync 回调填充,此处不复制
|
||||||
|
BleOta::instance().m_stackRunning = true;
|
||||||
|
BleOta::instance().m_suspendedForWifiOta = false;
|
||||||
DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成");
|
DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成");
|
||||||
return ESP_OK;
|
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 响应推送给上位机。
|
* @brief 通过 BLE Notify 将 GD32 响应推送给上位机。
|
||||||
* @param data 待推送的数据指针。
|
* @param data 待推送的数据指针。
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ public:
|
|||||||
* @return ESP_OK 成功,其他失败。
|
* @return ESP_OK 成功,其他失败。
|
||||||
*/
|
*/
|
||||||
esp_err_t serverStart();
|
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 推送响应数据。
|
* @brief 通过 BLE Notify 推送响应数据。
|
||||||
* @param data 待推送的数据指针。
|
* @param data 待推送的数据指针。
|
||||||
@@ -60,6 +77,8 @@ public:
|
|||||||
uint16_t m_esp32LastSeq = 0xFFFFU; ///< 最后一次数据包序号(用于补发 ACK)
|
uint16_t m_esp32LastSeq = 0xFFFFU; ///< 最后一次数据包序号(用于补发 ACK)
|
||||||
TaskHandle_t m_taskHandle = nullptr; ///< BLE OTA 守护任务句柄
|
TaskHandle_t m_taskHandle = nullptr; ///< BLE OTA 守护任务句柄
|
||||||
TaskHandle_t m_workerTaskHandle = 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
|
#endif // BLE_OTA_H
|
||||||
|
|||||||
+148
-126
@@ -8,6 +8,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
#include "esp_wifi.h"
|
#include "esp_wifi.h"
|
||||||
#include "esp_netif.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));
|
httpd_resp_send(req, resp, strlen(resp));
|
||||||
|
|
||||||
DEBUG_INFO("OTA", "响应已发送,等待 TCP 刷新...");
|
DEBUG_INFO("OTA", "响应已发送,等待 TCP 刷新...");
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
OtaCoordinator::instance().restartEsp32();
|
OtaCoordinator::instance().restartEsp32();
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -172,21 +173,16 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
|||||||
return total;
|
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)
|
esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||||
{
|
{
|
||||||
http_set_cors_headers(req);
|
http_set_cors_headers(req);
|
||||||
|
|
||||||
// per-request 接收块:2KB,避免大块 malloc 失败 / 碎片
|
uint8_t *recv_buf = s_ota_upload_recv;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d free=%lu largest=%lu",
|
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d free=%lu largest=%lu",
|
||||||
req->content_len,
|
req->content_len,
|
||||||
(unsigned long)esp_get_free_heap_size(),
|
(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);
|
int peek_ret = httpd_req_recv(req, &peek, 1);
|
||||||
if (peek_ret <= 0) {
|
if (peek_ret <= 0) {
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
||||||
free(recv_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_esp32_ota = false;
|
bool is_esp32_ota = false;
|
||||||
uint32_t total_size = 0;
|
uint32_t total_size = 0;
|
||||||
int json_header_len = 0;
|
int json_header_len = 0;
|
||||||
// ★ 改为堆分配:pre_read_buf[256]+json_buf[256] 同时在栈上占 512B,
|
/* 静态 scratch:OTA 期间少两次 256B heap 分配 */
|
||||||
// 上传 100% 后 GD32 路径调用 computeFlashSha256(栈上 mbedtls_sha256_context
|
uint8_t *pre_read_buf = s_ota_preread;
|
||||||
// ~108B)+ 日志格式化 608B,叠加逼近 8192 栈极限 → 栈溢出卡死。
|
memset(pre_read_buf, 0, 256);
|
||||||
// 移到堆上释放栈压力。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;
|
|
||||||
}
|
|
||||||
int pre_read_len = 1;
|
int pre_read_len = 1;
|
||||||
pre_read_buf[0] = (uint8_t)peek;
|
pre_read_buf[0] = (uint8_t)peek;
|
||||||
|
|
||||||
if (peek == '{') {
|
if (peek == '{') {
|
||||||
char *json_buf = (char *)calloc(256, 1);
|
char *json_buf = (char *)s_ota_json_scratch;
|
||||||
if (!json_buf) {
|
memset(json_buf, 0, 256);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
|
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
json_buf[0] = peek;
|
json_buf[0] = peek;
|
||||||
json_header_len = recv_json_header(req, json_buf + 1, 255);
|
json_header_len = recv_json_header(req, json_buf + 1, 255);
|
||||||
if (json_header_len > 0) {
|
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) &&
|
if (type_item && cJSON_IsString(type_item) &&
|
||||||
strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
|
strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
|
||||||
is_esp32_ota = true;
|
is_esp32_ota = true;
|
||||||
DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
|
|
||||||
}
|
}
|
||||||
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
|
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
|
||||||
if (size_item) {
|
if (size_item) {
|
||||||
@@ -254,17 +235,17 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
free(json_buf); // JSON 解析完成,立即释放
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_esp32_ota) {
|
if (is_esp32_ota) {
|
||||||
if (total_size == 0) {
|
if (total_size == 0) {
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ESP32 路径不再需要 pre_read(JSON 已消费,固件从流继续读) */
|
||||||
|
pre_read_len = 0;
|
||||||
|
|
||||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
|
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
if (err == ESP_ERR_INVALID_STATE) {
|
if (err == ESP_ERR_INVALID_STATE) {
|
||||||
@@ -272,8 +253,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
} else {
|
} else {
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||||
}
|
}
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
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_firmware_written = 0;
|
||||||
size_t total_received = json_header_len;
|
size_t total_received = json_header_len;
|
||||||
size_t last_log_recv = 0;
|
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "ESP32 OTA 开始收流: content_len=%d json_hdr=%d",
|
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)
|
||||||
|
? (req->content_len - json_header_len) : 0);
|
||||||
|
|
||||||
while (total_received < req->content_len) {
|
/* 浏览器 upload 进度到 100% 只表示 TCP 发完;设备仍在 recv+Flash。
|
||||||
size_t to_read = req->content_len - total_received;
|
* Flash 扇区擦除期间 cache 暂停,必须让 WiFi/LWIP 有机会跑,否则 TCP 卡死。
|
||||||
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
|
* 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);
|
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||||
if (ret <= 0) {
|
if (ret <= 0) {
|
||||||
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d",
|
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d free=%lu",
|
||||||
ret, (unsigned)total_received, req->content_len);
|
ret, (unsigned)total_received, req->content_len,
|
||||||
|
(unsigned long)esp_get_free_heap_size());
|
||||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = core.write(fw_buf, (size_t)ret);
|
err = core.write(fw_buf, (size_t)ret);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "OTA write failed: %s @%u",
|
DEBUG_ERR("WIFI", "OTA write failed: %s @%u free=%lu",
|
||||||
esp_err_to_name(err), (unsigned)total_firmware_written);
|
esp_err_to_name(err), (unsigned)total_firmware_written,
|
||||||
|
(unsigned long)esp_get_free_heap_size());
|
||||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
total_firmware_written += (size_t)ret;
|
total_firmware_written += (size_t)ret;
|
||||||
total_received += (size_t)ret;
|
total_received += (size_t)ret;
|
||||||
|
++write_chunks;
|
||||||
|
|
||||||
/* 长写 Flash:喂狗 + 进度日志,避免“浏览器 100% 而设备仍在写”时假死无日志 */
|
|
||||||
(void)esp_task_wdt_reset();
|
(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 字节)",
|
/* 回包前不打 INFO:堆紧时 DEBUG_INFO→UDP 会与 httpd_resp_send 抢 TX pbuf。
|
||||||
(unsigned int)core.getReceived(), json_header_len);
|
* 先让 LWIP 回收,成功后再记一条。 */
|
||||||
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;
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
(void)esp_task_wdt_reset();
|
(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");
|
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) {
|
if (send_err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "ESP32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
DEBUG_ERR("WIFI", "ESP32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
||||||
esp_err_to_name(send_err),
|
esp_err_to_name(send_err),
|
||||||
(unsigned long)esp_get_free_heap_size(),
|
(unsigned long)esp_get_free_heap_size(),
|
||||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
(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 {
|
} 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());
|
(unsigned long)esp_get_free_heap_size());
|
||||||
}
|
}
|
||||||
return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL;
|
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",
|
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u max=%u",
|
||||||
(unsigned)gd32_valid_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
(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");
|
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;
|
return ESP_FAIL;
|
||||||
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||||
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
} else if (prep_err != ESP_OK) {
|
} else if (prep_err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
|
DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
|
||||||
char err_msg[96];
|
char err_msg[96];
|
||||||
snprintf(err_msg, sizeof(err_msg), "prepare gd32_ota failed: %s", esp_err_to_name(prep_err));
|
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);
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
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);
|
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) {
|
if (json_header_len == 0 && pre_read_len > 0) {
|
||||||
int first_chunk_len = pre_read_len;
|
int first_chunk_len = pre_read_len;
|
||||||
@@ -425,8 +411,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
// ★ 首块写入失败:用 abortCurrentIfEpoch 避免误中止新会话
|
// ★ 首块写入失败:用 abortCurrentIfEpoch 避免误中止新会话
|
||||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
received = first_chunk_len;
|
received = first_chunk_len;
|
||||||
@@ -448,8 +432,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
// ★ 客户端断开(刷新网页/关浏览器/网络掉):用 abortCurrentIfEpoch 避免误中止新会话
|
// ★ 客户端断开(刷新网页/关浏览器/网络掉):用 abortCurrentIfEpoch 避免误中止新会话
|
||||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,8 +442,6 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
// ★ 写入失败:同上,用 abortCurrentIfEpoch
|
// ★ 写入失败:同上,用 abortCurrentIfEpoch
|
||||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||||
free(recv_buf);
|
|
||||||
free(pre_read_buf);
|
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,46 +449,28 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
gd32_offset += (uint32_t)ret;
|
gd32_offset += (uint32_t)ret;
|
||||||
|
|
||||||
(void)esp_task_wdt_reset();
|
(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));
|
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;
|
ota_meta_t final_meta;
|
||||||
OtaCoordinator::instance().loadGd32Meta(&final_meta);
|
OtaCoordinator::instance().loadGd32Meta(&final_meta);
|
||||||
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
|
if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) {
|
||||||
/* 先释放上传缓冲再算 SHA,避免与 2KB recv 并存 */
|
/* 先让出再算 SHA;回包前不打 INFO,避免与 httpd_resp_send 抢堆 */
|
||||||
free(recv_buf);
|
|
||||||
recv_buf = nullptr;
|
|
||||||
free(pre_read_buf);
|
|
||||||
pre_read_buf = nullptr;
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
(void)esp_task_wdt_reset();
|
(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
|
// 与 BLE 共用 finishGd32StorageAfterReceive:complete + SHA
|
||||||
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
|
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
|
||||||
if (sha_err != ESP_OK) {
|
if (sha_err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
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\", "
|
const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\", "
|
||||||
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
|
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
|
||||||
httpd_resp_set_type(req, "application/json");
|
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));
|
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
|
||||||
if (send_err != ESP_OK) {
|
if (send_err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "GD32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
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)esp_get_free_heap_size(),
|
||||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||||
} else {
|
} 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());
|
(unsigned long)esp_get_free_heap_size());
|
||||||
}
|
}
|
||||||
return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL;
|
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_type = 0;
|
||||||
uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0};
|
uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0};
|
||||||
size_t resp_len = 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,
|
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||||
timeout_ms);
|
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 uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||||
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 8192;
|
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
|
||||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 24;
|
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 {
|
struct CpuTaskSample {
|
||||||
UBaseType_t task_number;
|
UBaseType_t task_number;
|
||||||
@@ -1124,6 +1103,39 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
{
|
{
|
||||||
http_set_cors_headers(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";
|
char ip_str[16] = "0.0.0.0";
|
||||||
int8_t rssi = -127;
|
int8_t rssi = -127;
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
@@ -1162,20 +1174,25 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
task_metrics[i].handle = xTaskGetCurrentTaskHandle();
|
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);
|
/* CPU 采样在堆上;失败则跳过(available=false),不再 500 */
|
||||||
if (!resp) {
|
CpuUsageSnapshot cpu_local = {};
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
|
CpuUsageSnapshot *cpu = &cpu_local;
|
||||||
free(cpu);
|
CpuUsageSnapshot *cpu_heap = nullptr;
|
||||||
return ESP_FAIL;
|
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,
|
int len = snprintf(resp, SYSTEM_STATUS_JSON_CAP,
|
||||||
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, "
|
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, "
|
||||||
"\"free_heap\": %lu, \"min_free_heap\": %lu, \"largest_free_block\": %lu, "
|
"\"free_heap\": %lu, \"min_free_heap\": %lu, \"largest_free_block\": %lu, "
|
||||||
@@ -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);
|
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();
|
SpiMaster::SpiLinkStats ls = SpiMaster::instance().linkStats();
|
||||||
const char *role_str = "unknown";
|
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, "}}");
|
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, "}}");
|
||||||
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
httpd_resp_send(req, resp, len);
|
httpd_resp_send(req, resp, len > 0 ? len : 0);
|
||||||
free(resp);
|
if (cpu_heap) {
|
||||||
free(cpu);
|
free(cpu_heap);
|
||||||
|
}
|
||||||
|
if (mux) {
|
||||||
|
xSemaphoreGive(mux);
|
||||||
|
}
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -424,8 +424,10 @@ esp_err_t WifiOta::httpServerStart()
|
|||||||
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
|
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
|
||||||
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
|
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
|
||||||
config.lru_purge_enable = false;
|
config.lru_purge_enable = false;
|
||||||
config.recv_wait_timeout = 60; // 1MB OTA 写 Flash 可能超过 30s 空闲读
|
/* 1MB sequential 擦写时,httpd 在 write 阶段可能 >60s 不调 recv;
|
||||||
config.send_wait_timeout = 30;
|
* 超时后 handler 内会重试,但过短会放大“卡 100%”观感。 */
|
||||||
|
config.recv_wait_timeout = 120;
|
||||||
|
config.send_wait_timeout = 60;
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
||||||
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
|||||||
size_t resp_len = 0U;
|
size_t resp_len = 0U;
|
||||||
/* 两步法:otaTransfer 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
|
/* 两步法:otaTransfer 发命令(丢弃延迟一拍的过期响应)+ otaPollResponse 读真实响应。
|
||||||
* CAN 帧非幂等,命令只发一次,不重发。 */
|
* CAN 帧非幂等,命令只发一次,不重发。 */
|
||||||
esp_err_t ret = SpiMaster::instance().otaSendAndPoll(
|
esp_err_t ret = SpiMaster::instance().transact(
|
||||||
kSpiCanFrameType,
|
kSpiCanFrameType,
|
||||||
tx_buf, tx_len,
|
tx_buf, tx_len,
|
||||||
&resp_type, recv_buf, recv_max, &resp_len, timeout_ms);
|
&resp_type, recv_buf, recv_max, &resp_len, timeout_ms);
|
||||||
@@ -175,7 +175,7 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
|||||||
|
|
||||||
/* 两步法破解延迟一拍:发命令一次 + poll 真实响应。
|
/* 两步法破解延迟一拍:发命令一次 + poll 真实响应。
|
||||||
* 0xC2 幂等,但两步法避免无效重试。 */
|
* 0xC2 幂等,但两步法避免无效重试。 */
|
||||||
esp_err_t ret = SpiMaster::instance().otaSendAndPoll(
|
esp_err_t ret = SpiMaster::instance().transact(
|
||||||
kSpiCanSetModeType,
|
kSpiCanSetModeType,
|
||||||
&req, 1U,
|
&req, 1U,
|
||||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
||||||
|
|||||||
+810
-325
File diff suppressed because it is too large
Load Diff
+115
-4
@@ -19,6 +19,8 @@
|
|||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
#include "spi_link_v2.h"
|
#include "spi_link_v2.h"
|
||||||
|
|
||||||
// SPI 配置常量
|
// SPI 配置常量
|
||||||
@@ -110,6 +112,33 @@ public:
|
|||||||
/** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
|
/** @brief 读取对端最新上报的 CREDIT(0 表示对端忙,应 POLL 等待而非重发)。 */
|
||||||
uint8_t remoteCredit() const { return m_remoteCredit; }
|
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:帧格式 + 状态成员就位)
|
// SPI Link v2 链路层(阶段1:帧格式 + 状态成员就位)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -138,6 +167,9 @@ public:
|
|||||||
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
|
/** @brief 获取链路统计快照(线程安全,持锁读取)。 */
|
||||||
SpiLinkStats linkStats();
|
SpiLinkStats linkStats();
|
||||||
|
|
||||||
|
/** @brief L4 链路状态(linkStats 别名)。 */
|
||||||
|
SpiLinkStats linkStatus() { return linkStats(); }
|
||||||
|
|
||||||
/** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。
|
/** @brief 发起 HELLO 握手建立 v2 session(SPEC §5.1)。
|
||||||
* 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。
|
* 生成随机非零 SESSION_ID,发送 LINK_HELLO,等待 HELLO_ACK。
|
||||||
* 成功后 m_v2Active=true,序号归零。
|
* 成功后 m_v2Active=true,序号归零。
|
||||||
@@ -251,14 +283,71 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
SpiLinkState m_linkState;
|
SpiLinkState m_linkState;
|
||||||
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,SPI 任务消费。 */
|
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,仅 SPI worker 消费。 */
|
||||||
volatile bool m_gd32RequestPending = false;
|
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;
|
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] = {};
|
uint8_t m_ringRxBuf[SPI_OTA_FRAME_MAX] = {};
|
||||||
size_t m_ringRxLen = 0;
|
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; ///< 本端剩余可接收帧数(信用窗口)
|
volatile uint8_t m_localCredit = 8; ///< 本端剩余可接收帧数(信用窗口)
|
||||||
@@ -272,6 +361,8 @@ private:
|
|||||||
volatile uint8_t m_expectedRxSeq = 0; ///< 期望收到的对端序号
|
volatile uint8_t m_expectedRxSeq = 0; ///< 期望收到的对端序号
|
||||||
volatile uint8_t m_gd32Role = 0; ///< GD32 角色(0=unknown, 1=app, 2=boot)
|
volatile uint8_t m_gd32Role = 0; ///< GD32 角色(0=unknown, 1=app, 2=boot)
|
||||||
volatile bool m_v2Active = false; ///< v2 session 是否激活
|
volatile bool m_v2Active = false; ///< v2 session 是否激活
|
||||||
|
/** 最近一次 otaFindFrame 解析到的 RESPONSE 是否与 m_lastSentSeq 归属匹配 */
|
||||||
|
volatile bool m_lastRxAckMatched = false;
|
||||||
SpiLinkStats m_stats = {}; ///< 链路统计
|
SpiLinkStats m_stats = {}; ///< 链路统计
|
||||||
|
|
||||||
// RAII 锁守卫
|
// RAII 锁守卫
|
||||||
@@ -321,6 +412,26 @@ private:
|
|||||||
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
||||||
uint8_t *rx_buf, size_t rx_len,
|
uint8_t *rx_buf, size_t rx_len,
|
||||||
uint32_t timeout_ms = 0U);
|
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 响应轮询(调用者须已持有锁)。
|
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
|
||||||
|
|||||||
+18
-5
@@ -14,8 +14,19 @@ CONFIG_DCU_OTA_PACKET_INTERVAL_MS=3
|
|||||||
# 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE)
|
# 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE)
|
||||||
# 提到 12 留足缓冲,配合 can_feedback_task 退避从根本上避免 socket 饥荒
|
# 提到 12 留足缓冲,配合 can_feedback_task 退避从根本上避免 socket 饥荒
|
||||||
CONFIG_LWIP_MAX_SOCKETS=12
|
CONFIG_LWIP_MAX_SOCKETS=12
|
||||||
CONFIG_LWIP_TCP_WND_DEFAULT=11480
|
# C3 堆紧:WiFi+BLE 双栈时 TCP 窗口过大 → OTA 收流 free 压到 ~5KB → 卡半/堆爆。
|
||||||
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=11520
|
# 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
|
# FreeRTOS runtime stats -- /system/status CPU usage
|
||||||
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
|
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_BAS_SERVICE=n
|
||||||
CONFIG_BT_NIMBLE_DIS_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_1_BLOCK_COUNT=12
|
||||||
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=24
|
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=12
|
||||||
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
|
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=16
|
||||||
# Event buffers: 30→16 (省 ~1 KB)
|
# Event buffers: 30→16 (省 ~1 KB)
|
||||||
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=16
|
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=16
|
||||||
# Prepared write entries: 64→16 (省 ~1 KB)
|
# Prepared write entries: 64→16 (省 ~1 KB)
|
||||||
|
|||||||
+208
-80
@@ -129,11 +129,103 @@ function otaSessionTransition(newState, reason) {
|
|||||||
else if (newState !== OtaSessionState.VERIFYING) isEsp32Rebooting = false;
|
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();
|
stopRelayProgressWatch();
|
||||||
|
stopUploadWaitHeartbeat();
|
||||||
|
|
||||||
gd32CompleteAnnounced = false;
|
gd32CompleteAnnounced = false;
|
||||||
relayWatchLastSeq = -1;
|
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) {
|
if (otaSessionState !== OtaSessionState.IDLE) {
|
||||||
otaSessionState = OtaSessionState.IDLE;
|
otaSessionState = OtaSessionState.IDLE;
|
||||||
otaSessionTarget = null;
|
otaSessionTarget = null;
|
||||||
@@ -141,6 +233,7 @@ function otaSessionReset() {
|
|||||||
currentOtaTarget = null;
|
currentOtaTarget = null;
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
}
|
}
|
||||||
|
activeUploadChip = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 折叠面板 ====================
|
// ==================== 折叠面板 ====================
|
||||||
@@ -237,63 +330,48 @@ function updateEsp32StagesError() {
|
|||||||
* @param {string} reason 失败原因描述
|
* @param {string} reason 失败原因描述
|
||||||
*/
|
*/
|
||||||
function cleanupAfterOtaFailure(reason) {
|
function cleanupAfterOtaFailure(reason) {
|
||||||
stopRelayProgressWatch();
|
var chip = activeUploadChip || otaSessionTarget || currentOtaTarget || window.flashTarget || 'esp32';
|
||||||
// 0. 状态机:转入 FAILED(先于标志重置,确保 otaSessionActive() 即刻返回 false)
|
|
||||||
|
// 0. 状态机:转入 FAILED(先于标志重置)
|
||||||
otaSessionTransition(OtaSessionState.FAILED, reason);
|
otaSessionTransition(OtaSessionState.FAILED, reason);
|
||||||
|
|
||||||
// 1. 重置上传锁
|
// 1. 清掉本会话全部残留标志 / 定时器 / 校验面板 / 旧 progress 语义
|
||||||
isUploading = false;
|
clearOtaSessionResiduals({
|
||||||
stopUploadWaitHeartbeat();
|
keepErrorStages: true,
|
||||||
|
statusText: reason || '失败',
|
||||||
|
errorStyle: true,
|
||||||
|
clearCacheDisplay: true
|
||||||
|
});
|
||||||
|
|
||||||
// 2. 中止正在进行的 XHR(如有)
|
// 2. 阶段指示器标错
|
||||||
if (activeXhr) {
|
if (chip === 'esp32') {
|
||||||
try { activeXhr.abort(); } catch (_) {}
|
|
||||||
activeXhr = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 标记阶段指示器为错误态
|
|
||||||
if (currentOtaTarget === 'esp32') {
|
|
||||||
updateEsp32StagesError();
|
updateEsp32StagesError();
|
||||||
} else if (currentOtaTarget === 'gd32') {
|
} else {
|
||||||
updateStagesFromRelayState(255);
|
updateStagesFromRelayState(255);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 状态栏标红
|
// 3. 进度条失败文案(clear 已把 fill 的 done 去掉)
|
||||||
var sb = document.getElementById('stageStatusBar');
|
|
||||||
var st = document.getElementById('stageStatusText');
|
|
||||||
if (sb) sb.className = 'stage-status-bar err';
|
|
||||||
if (st) st.textContent = reason || '失败';
|
|
||||||
|
|
||||||
// 5. 进度条显示失败信息
|
|
||||||
setProgress(0, reason || '刷写失败');
|
setProgress(0, reason || '刷写失败');
|
||||||
|
|
||||||
// 6. 重置活跃阶段
|
|
||||||
setActivePhase(null);
|
setActivePhase(null);
|
||||||
|
|
||||||
// 7. 清除 OTA 目标(下次操作时重新设置)
|
|
||||||
currentOtaTarget = null;
|
currentOtaTarget = null;
|
||||||
|
|
||||||
// 8. 隐藏校验面板
|
// 4. 恢复上传按钮
|
||||||
var verifyInfo = document.getElementById('stageVerifyInfo');
|
|
||||||
if (verifyInfo) verifyInfo.style.display = 'none';
|
|
||||||
|
|
||||||
// 9. 恢复上传按钮(基于实际上传的芯片,而非当前 UI 选中的标签)
|
|
||||||
var chip = activeUploadChip || otaSessionTarget || window.flashTarget || 'esp32';
|
|
||||||
var btnId = (chip === 'gd32') ? 'uploadGd32Btn' : 'uploadEsp32Btn';
|
var btnId = (chip === 'gd32') ? 'uploadGd32Btn' : 'uploadEsp32Btn';
|
||||||
var btn = document.getElementById(btnId);
|
var btn = document.getElementById(btnId);
|
||||||
if (btn && uploadBtnOrigText) {
|
if (btn) {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = uploadBtnOrigText;
|
if (uploadBtnOrigText) btn.textContent = uploadBtnOrigText;
|
||||||
}
|
}
|
||||||
activeUploadChip = null;
|
activeUploadChip = null;
|
||||||
|
otaSessionTarget = null;
|
||||||
|
uploadBtnOrigText = '';
|
||||||
|
|
||||||
updateUploadButtonLabel();
|
updateUploadButtonLabel();
|
||||||
|
|
||||||
// 10. 恢复轮询
|
// 5. 恢复轮询(之后 status 不得覆盖 FAILED UI,直至用户新上传或设备 idle 复位)
|
||||||
resumePollingAfterUpload();
|
resumePollingAfterUpload();
|
||||||
|
|
||||||
// 11. 日志
|
addLog('E', 'OTA', '刷写流程已中止,已清理残留状态: ' + (reason || '未知原因'));
|
||||||
addLog('E', 'OTA', '刷写流程已中止: ' + (reason || '未知原因'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
|
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
|
||||||
@@ -928,7 +1006,15 @@ function updateSystemStatusUi(data) {
|
|||||||
if (currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); }
|
if (currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); }
|
||||||
|
|
||||||
if (isGd32Error && (currentOtaTarget === 'gd32' || otaSessionTarget === 'gd32')) {
|
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) {
|
} else if (isGd32Error) {
|
||||||
// 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理
|
// 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理
|
||||||
updateStagesFromRelayState(255);
|
updateStagesFromRelayState(255);
|
||||||
@@ -936,52 +1022,55 @@ function updateSystemStatusUi(data) {
|
|||||||
var stErr = document.getElementById('stageStatusText');
|
var stErr = document.getElementById('stageStatusText');
|
||||||
if (sbErr) sbErr.className = 'stage-status-bar err';
|
if (sbErr) sbErr.className = 'stage-status-bar err';
|
||||||
if (stErr) stErr.textContent = 'GD32 错误(待机)';
|
if (stErr) stErr.textContent = 'GD32 错误(待机)';
|
||||||
} else if (isGd32Complete || rs === 5 ||
|
} else if (otaSessionState === OtaSessionState.UPLOADING) {
|
||||||
(gd32AppAlive && (sysState === 'GD32_COMPLETE' || sysStateCode === 4)) ||
|
/* 上传进行中:不要用旧缓存/旧完成态覆盖进度条 */
|
||||||
/* 包已全部转发且 APP 已起来:即使日志停在 SEND_SIZE 也算完成 */
|
} else if (otaSessionState === OtaSessionState.RELAYING) {
|
||||||
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal &&
|
/* 必须先看到本会话真实中继(HANDSHAKE~FINISHING),才允许 DONE;
|
||||||
(isGd32Relaying || otaSessionState === OtaSessionState.RELAYING || rs >= 3))) {
|
* 禁止一进 RELAYING 就吃掉上一次的 rs=5 / app_alive。 */
|
||||||
// 已完成:只进一次 UI/toast,后续轮询直接忽略
|
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
|
||||||
if (!gd32CompleteAnnounced && otaSessionState !== OtaSessionState.DONE) {
|
gd32RelaySessionLive = true;
|
||||||
markGd32OtaUiComplete('GD32 刷写完成');
|
|
||||||
}
|
}
|
||||||
} else if (isGd32Storing || (total > 0 && stored < total && !isUploading)) {
|
if (gd32RelaySessionLive &&
|
||||||
|
(isGd32Complete || rs === 5 ||
|
||||||
|
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal && rs >= 3))) {
|
||||||
|
if (!gd32CompleteAnnounced) {
|
||||||
|
markGd32OtaUiComplete('GD32 刷写完成');
|
||||||
|
}
|
||||||
|
} else if (rs === 1) {
|
||||||
|
// HANDSHAKING
|
||||||
|
setActivePhase('gd32Ota');
|
||||||
|
setProgress(50, '握手与认证中...');
|
||||||
|
} 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
|
||||||
|
? ('已转发 ' + relaySeq + ' / ' + relayTotal + ' 包 (' + relayProgress + '%)')
|
||||||
|
: '数据包传输中...';
|
||||||
|
setProgress(relayOverall, relayLabel);
|
||||||
|
} else if (rs === 2 || (isGd32Relaying && relaySeq === 0)) {
|
||||||
|
// SIZE_NEG
|
||||||
|
setActivePhase('gd32Ota');
|
||||||
|
setProgress(55, relayTotal > 0
|
||||||
|
? ('大小协商中(共 ' + relayTotal + ' 包)')
|
||||||
|
: '固件大小协商中');
|
||||||
|
} else if (rs === 4) {
|
||||||
|
setActivePhase('gd32Ota');
|
||||||
|
if (!gd32CompleteAnnounced) {
|
||||||
|
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
||||||
|
}
|
||||||
|
} else if (!gd32RelaySessionLive) {
|
||||||
|
setActivePhase('gd32Ota');
|
||||||
|
setProgress(48, '已触发中继,等待设备进入握手…');
|
||||||
|
}
|
||||||
|
} else if (isGd32Storing || (total > 0 && stored < total && !isUploading &&
|
||||||
|
otaSessionState !== OtaSessionState.DONE &&
|
||||||
|
otaSessionState !== OtaSessionState.IDLE)) {
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
|
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
|
||||||
if (!isUploading) {
|
if (!isUploading) {
|
||||||
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total) + '');
|
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total) + '');
|
||||||
}
|
}
|
||||||
} else if (rs === 1) {
|
|
||||||
// HANDSHAKING: 握手 + 挑战认证
|
|
||||||
setActivePhase('gd32Ota');
|
|
||||||
setProgress(50, '握手与认证中...');
|
|
||||||
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0)) {
|
|
||||||
// TRANSFERRING:以 relay_seq 为准(优先于 rs===2,避免 total 预写后一直显示 0/N)
|
|
||||||
setActivePhase('gd32Ota');
|
|
||||||
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
|
||||||
var relayLabel = relayTotal > 0
|
|
||||||
? ('已转发 ' + relaySeq + ' / ' + relayTotal + ' 包 (' + relayProgress + '%)')
|
|
||||||
: '数据包传输中...';
|
|
||||||
setProgress(relayOverall, relayLabel);
|
|
||||||
} else if (rs === 2 || (isGd32Relaying && relaySeq === 0)) {
|
|
||||||
// SIZE_NEG:只说协商中,不写「0/N」(total 会提前写入 status,易被误认为卡在 0 包)
|
|
||||||
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 刷写完成');
|
|
||||||
}
|
|
||||||
} else if (!gd32CompleteAnnounced) {
|
|
||||||
setProgress(95, '收尾中,等待 APP 启动...');
|
|
||||||
}
|
|
||||||
} else if (total > 0 && stored >= total && !isGd32Relaying && otaSessionState !== OtaSessionState.RELAYING) {
|
|
||||||
setActivePhase('gd32Ota');
|
|
||||||
setProgress(45, '上传完成,等待自动启动 GD32 OTA...');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
||||||
@@ -1405,6 +1494,13 @@ async function startUpload(chip) {
|
|||||||
// ★ 状态机:进入 UPLOADING 态(内部同步 isUploading = true)
|
// ★ 状态机:进入 UPLOADING 态(内部同步 isUploading = true)
|
||||||
otaSessionTransition(OtaSessionState.UPLOADING);
|
otaSessionTransition(OtaSessionState.UPLOADING);
|
||||||
activeUploadChip = chip;
|
activeUploadChip = chip;
|
||||||
|
/* 新上传:清掉上次完成标志,避免立刻被 status 残留判成“又完成了” */
|
||||||
|
gd32CompleteAnnounced = false;
|
||||||
|
relayWatchLastSeq = -1;
|
||||||
|
gd32RelaySessionLive = false;
|
||||||
|
if (chip === 'esp32') {
|
||||||
|
updateEsp32StagesFromPhase(1);
|
||||||
|
}
|
||||||
// 跨通道互斥前置检查(含 BLE 取消后的瞬态重试)
|
// 跨通道互斥前置检查(含 BLE 取消后的瞬态重试)
|
||||||
var _probeReady = false;
|
var _probeReady = false;
|
||||||
for (var _pi = 0; _pi < 4; _pi++) {
|
for (var _pi = 0; _pi < 4; _pi++) {
|
||||||
@@ -1596,6 +1692,21 @@ async function startUpload(chip) {
|
|||||||
uploadBtn.textContent = uploadBtnOrigText;
|
uploadBtn.textContent = uploadBtnOrigText;
|
||||||
updateUploadButtonLabel();
|
updateUploadButtonLabel();
|
||||||
if (xhr.status === 200) {
|
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] 语义重复
|
// 成功路径只打一条「上传完成」日志,避免 [上传]+[OTA] 语义重复
|
||||||
if (chip === 'esp32') {
|
if (chip === 'esp32') {
|
||||||
addLog('I', 'OTA', '上传完成,开始 SHA-256 校验…');
|
addLog('I', 'OTA', '上传完成,开始 SHA-256 校验…');
|
||||||
@@ -1757,12 +1868,19 @@ var relayWatchLastUiLogAt = 0;
|
|||||||
var relayWatchInFlight = false;
|
var relayWatchInFlight = false;
|
||||||
var gd32CompleteAnnounced = false;
|
var gd32CompleteAnnounced = false;
|
||||||
var relayWatchLastSeq = -1;
|
var relayWatchLastSeq = -1;
|
||||||
|
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
|
||||||
|
var gd32RelaySessionLive = false;
|
||||||
|
|
||||||
function markGd32OtaUiComplete(msg) {
|
function markGd32OtaUiComplete(msg) {
|
||||||
// 已公告过:禁止再改 className / setProgress / toast,否则每 400ms 重触发动效会「疯狂跳 ✓」
|
// 已公告过:禁止再改 className / setProgress / toast,否则每 400ms 重触发动效会「疯狂跳 ✓」
|
||||||
if (gd32CompleteAnnounced) {
|
if (gd32CompleteAnnounced) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* 仅本会话 RELAYING/FAILED 可宣布完成;IDLE/UPLOADING 时 status 里的旧 relay 残留一律忽略 */
|
||||||
|
if (otaSessionState !== OtaSessionState.RELAYING &&
|
||||||
|
otaSessionState !== OtaSessionState.FAILED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
gd32CompleteAnnounced = true;
|
gd32CompleteAnnounced = true;
|
||||||
stopRelayProgressWatch();
|
stopRelayProgressWatch();
|
||||||
|
|
||||||
@@ -1824,7 +1942,12 @@ function startRelayProgressWatch() {
|
|||||||
var alive = !!o.gd32_app_alive;
|
var alive = !!o.gd32_app_alive;
|
||||||
var rseq = Number(o.relay_seq) || 0;
|
var rseq = Number(o.relay_seq) || 0;
|
||||||
var rtot = Number(o.relay_total_packets) || 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 刷写完成');
|
markGd32OtaUiComplete('GD32 刷写完成');
|
||||||
pollLogs();
|
pollLogs();
|
||||||
return;
|
return;
|
||||||
@@ -2060,6 +2183,11 @@ async function pollStatus() {
|
|||||||
consecutivePollFailures = 0;
|
consecutivePollFailures = 0;
|
||||||
addLog('I', '系统', '设备已恢复在线,轮询恢复正常');
|
addLog('I', '系统', '设备已恢复在线,轮询恢复正常');
|
||||||
showToast('设备已恢复在线', 'success');
|
showToast('设备已恢复在线', 'success');
|
||||||
|
// 设备离线后恢复在线,清理残留的 OTA 会话状态
|
||||||
|
if (otaSessionState !== OtaSessionState.IDLE) {
|
||||||
|
otaSessionReset();
|
||||||
|
addLog('I', '系统', '已清理 OTA 会话状态');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (isEsp32Rebooting) {
|
if (isEsp32Rebooting) {
|
||||||
var rbElapsed = Math.round((Date.now() - rebootStartTime) / 1000);
|
var rbElapsed = Math.round((Date.now() - rebootStartTime) / 1000);
|
||||||
|
|||||||
+1
-1
@@ -427,7 +427,7 @@
|
|||||||
<span id="toastMsg">操作成功</span>
|
<span id="toastMsg">操作成功</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js?v=20260713i"></script>
|
<script src="app.js?v=20260713m"></script>
|
||||||
<script>
|
<script>
|
||||||
window.connectionMode = window.connectionMode || 'wifi';
|
window.connectionMode = window.connectionMode || 'wifi';
|
||||||
window.flashTarget = window.flashTarget || 'esp32';
|
window.flashTarget = window.flashTarget || 'esp32';
|
||||||
|
|||||||
Reference in New Issue
Block a user