feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制
移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2 CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发 实现流量控制,提高OTA传输稳定性。 refactor(stack): 统一任务栈大小配置并添加CPU统计功能 - 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配 - 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样 - 更新main.cpp中的任务创建使用新的栈大小常量 - 调整health_monitor和ota_relay等任务的栈分配 perf(ota): 改进ESP32和GD32 OTA分区预擦策略 - ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞 - GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量 - 优化OTA协调器状态管理,修复FINALIZING状态残留问题 fix(log): 改进UDP日志发送的堆内存管理 - 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer - 降低UDP发送跳过的内存阈值从10KB到6KB - 降低日志排水最小内存阈值从8KB到4KB docs(readme): 更新OTA配置参数文档说明 移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为 基于CREDIT背压机制的描述。
This commit is contained in:
+32
-16
@@ -66,7 +66,8 @@
|
||||
|
||||
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
|
||||
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
|
||||
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW + 4)
|
||||
// 深度略大于窗口:seq=0 整分区预擦期间前端若仍误发,可多扛几批,减少“队列满丢包→重传风暴”。
|
||||
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW * 2 + 8)
|
||||
|
||||
struct BleOtaDataBatch {
|
||||
uint8_t *data;
|
||||
@@ -83,7 +84,7 @@ struct BleEsp32FinishCtx {
|
||||
uint16_t session_id;
|
||||
};
|
||||
|
||||
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免历史多次漏 free。
|
||||
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免漏 free。
|
||||
// 注意:若调用 restartEsp32()(内部 esp_restart 不返回),析构不会执行,
|
||||
// 但这是设备重启路径,资源由系统回收。
|
||||
struct BleFinishCtxDeleter {
|
||||
@@ -236,13 +237,10 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
// ============================================================
|
||||
// WiFi 共存策略:BLE OTA 传输期间的处理
|
||||
//
|
||||
// 历史:曾在此处把 WiFi 切到 WIFI_PS_MAX_MODEM 以把空中时间让给 BLE。
|
||||
// 但 MAX_MODEM 下 STA 监听间隔被拉到极大,HTTP server 的 TCP 握手/数据包
|
||||
// 会大量丢失或延迟到超时 → BLE OTA 期间任何 WiFi 请求(包括互斥拒绝应返回的
|
||||
// 403)都到不了设备,前端 XHR 长时间无响应,表现为"Wifi 上传既不成功也不报错"。
|
||||
//
|
||||
// 现策略:不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
|
||||
// 不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
|
||||
// 协调器照常以 403 拒绝并发 OTA。ESP32-C3 coex 默认调度已能保证 BLE OTA 可用。
|
||||
// (曾尝试切到 WIFI_PS_MAX_MODEM 让出空中时间,但 STA 监听间隔被拉到极大,
|
||||
// HTTP server 的 TCP 握手/数据包大量丢失或超时,前端 XHR 长时间无响应。)
|
||||
// 仍保留 setBleOtaActive() 暂停 RSSI 轮询(避免 get_ap_info 的射频活动干扰 BLE)。
|
||||
// ============================================================
|
||||
static bool s_wifi_ps_boosted = false;
|
||||
@@ -254,7 +252,7 @@ static void ble_ota_boost_wifi_ps()
|
||||
{
|
||||
if (s_wifi_ps_boosted) return;
|
||||
s_wifi_ps_boosted = true;
|
||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
||||
// 通过协调器通知 WiFi 层
|
||||
OtaCoordinator::instance().notifyBleCoexStart();
|
||||
DEBUG_INFO(TAG, "BLE OTA 数据传输进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
|
||||
}
|
||||
@@ -266,7 +264,7 @@ static void ble_ota_restore_wifi_ps()
|
||||
{
|
||||
if (!s_wifi_ps_boosted) return;
|
||||
s_wifi_ps_boosted = false;
|
||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
||||
// 通过协调器通知 WiFi 层
|
||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||
DEBUG_INFO(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
|
||||
}
|
||||
@@ -933,7 +931,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
ctx->session_id = session_id;
|
||||
BaseType_t ok = xTaskCreate(ble_gd32_relay_wait_task,
|
||||
"ble_gd32_wait",
|
||||
3072,
|
||||
TASK_STACK_BLE_GD32_WAIT,
|
||||
ctx,
|
||||
5,
|
||||
NULL);
|
||||
@@ -992,7 +990,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
||||
"ble_ota_finish",
|
||||
4096,
|
||||
TASK_STACK_BLE_FINISH,
|
||||
finish_ctx, // heap context; task 负责 free
|
||||
5,
|
||||
NULL);
|
||||
@@ -1015,7 +1013,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_restart_task,
|
||||
"ble_ota_restart",
|
||||
2048,
|
||||
TASK_STACK_BLE_RESTART,
|
||||
NULL,
|
||||
5,
|
||||
NULL);
|
||||
@@ -1223,17 +1221,25 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
// ACK 按累计连续进度触发,而不是按当前到达包的 seq 触发。缺口包到达后
|
||||
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
|
||||
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
|
||||
//
|
||||
// ★ seq=0 强制 ACK:begin 含整分区预擦,可能耗时数秒~数十秒。
|
||||
// 前端必须等“首包已落盘”ACK 后再发窗口内其余包,否则队列满丢包→重传极慢。
|
||||
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
|
||||
BLE_OTA_CHUNK_PAYLOAD;
|
||||
bool reachedAckBoundary = completedPackets > 0U &&
|
||||
(completedPackets % BLE_OTA_ACK_WINDOW) == 0U;
|
||||
bool firstPacketReady = (seq == 0U) && progressMade;
|
||||
if (progressMade &&
|
||||
(reachedAckBoundary ||
|
||||
transferComplete ||
|
||||
isLastPacket)) {
|
||||
isLastPacket ||
|
||||
firstPacketReady)) {
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
seq, data_len, (unsigned int)afterReceived, total_size);
|
||||
} else if (firstPacketReady) {
|
||||
DEBUG_INFO(TAG, "ESP32 begin 就绪 ACK: seq=0 received=%u total=%" PRIu32,
|
||||
(unsigned)afterReceived, total_size);
|
||||
}
|
||||
uint16_t ackSeq = s_reorder.expected > 0U
|
||||
? (uint16_t)(s_reorder.expected - 1U)
|
||||
@@ -1384,7 +1390,9 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = seq;
|
||||
const bool transfer_complete = stored_size >= meta.total_size;
|
||||
// seq=0:prepare 含整分区预擦,写完首包必须 ACK,前端才能继续发后续窗口
|
||||
const bool checkpoint =
|
||||
(seq == 0U) ||
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
|
||||
transfer_complete ||
|
||||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
@@ -1681,6 +1689,14 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
s_conn_fallback_requested = true;
|
||||
ble_ota_request_conn_params(BleOta::instance().m_connHandle, true);
|
||||
}
|
||||
/* 优先 LE 2M PHY(空中速率约翻倍);对端不支持则保持 1M,失败仅打日志 */
|
||||
{
|
||||
const uint8_t phy_mask = BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK;
|
||||
int phy_rc = ble_gap_set_prefered_le_phy(
|
||||
BleOta::instance().m_connHandle, phy_mask, phy_mask,
|
||||
BLE_GAP_LE_PHY_CODED_ANY);
|
||||
DEBUG_INFO(TAG, "请求 LE PHY 1M|2M: rc=%d", phy_rc);
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
@@ -2040,7 +2056,7 @@ esp_err_t BleOta::resumeStack()
|
||||
m_suspendedForWifiOta = false;
|
||||
|
||||
if (!m_stackRunning) {
|
||||
/* 仅当历史上曾 hard-deinit 过才需要完整 serverStart */
|
||||
/* 仅当曾 hard-deinit 过才需要完整 serverStart */
|
||||
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
esp_err_t err = serverStart();
|
||||
@@ -2125,7 +2141,7 @@ void BleOta::taskEntry(void *pvParameters)
|
||||
if (s_otaDataQueue) {
|
||||
BaseType_t wk_ok = xTaskCreate(ble_ota_data_worker_task,
|
||||
"ble_ota_worker",
|
||||
4096, // 峰值仅 ~1.9KB,4KB 留有充足余量
|
||||
TASK_STACK_BLE_WORKER, // 实测 peak~1.1KB,负载峰值曾~1.9KB
|
||||
NULL,
|
||||
10, // 中等优先级,避免被 NimBLE host task 过度抢占
|
||||
NULL);
|
||||
|
||||
+15
-181
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* http_handlers.cpp - HTTP 请求处理器实现
|
||||
* 从 wifi_ota.cpp 提取的 HTTP handler 函数
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
@@ -28,6 +27,7 @@
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "health_monitor.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "monitor/cpu_stats.h"
|
||||
#include "spi_master.h"
|
||||
#include "canfd_bridge.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
@@ -208,7 +208,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||
|
||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||
// ★ L1: WiFi OTA 开始时恢复 BLE 共存状态(通过协调器)
|
||||
// WiFi OTA 开始时恢复 BLE 共存状态(通过协调器)
|
||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||
|
||||
char peek;
|
||||
@@ -307,7 +307,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
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));
|
||||
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,12 +615,10 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
valid[i] = getLatestCanFeedback(i, &results[i]);
|
||||
}
|
||||
|
||||
/* 估算缓冲大小:每通道 ~2KB (统计+ID条目) */
|
||||
char *resp = (char *)calloc(1, 8192);
|
||||
if (!resp) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
/* ★ 修复:改用静态缓冲区,避免每次请求 malloc 8KB 加剧堆碎片化 */
|
||||
static char s_feedback_json[8192];
|
||||
char *resp = s_feedback_json;
|
||||
memset(resp, 0, sizeof(s_feedback_json));
|
||||
|
||||
char *p = resp;
|
||||
p += sprintf(p, "{\"channels\":[");
|
||||
@@ -670,7 +668,6 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, (int)(p - resp));
|
||||
free(resp);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -882,7 +879,6 @@ esp_err_t spi_send_handler(httpd_req_t *req)
|
||||
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
|
||||
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144;
|
||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
|
||||
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;
|
||||
@@ -899,167 +895,6 @@ static SemaphoreHandle_t statusJsonMutex(void)
|
||||
return s_status_json_mux;
|
||||
}
|
||||
|
||||
struct CpuTaskSample {
|
||||
UBaseType_t task_number;
|
||||
configRUN_TIME_COUNTER_TYPE runtime;
|
||||
};
|
||||
|
||||
struct CpuTaskMetric {
|
||||
const char* name;
|
||||
UBaseType_t task_number;
|
||||
uint64_t runtime_delta;
|
||||
double usage_pct;
|
||||
};
|
||||
|
||||
struct CpuUsageSnapshot {
|
||||
bool available;
|
||||
bool valid;
|
||||
uint32_t sample_ms;
|
||||
double usage_pct;
|
||||
double idle_pct;
|
||||
CpuTaskMetric tasks[CPU_TASK_SNAPSHOT_MAX];
|
||||
size_t task_count;
|
||||
};
|
||||
|
||||
static portMUX_TYPE s_cpu_stats_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static CpuTaskSample s_cpu_last_samples[CPU_TASK_SNAPSHOT_MAX];
|
||||
static size_t s_cpu_last_count = 0;
|
||||
static configRUN_TIME_COUNTER_TYPE s_cpu_last_total = 0;
|
||||
static int64_t s_cpu_last_time_us = 0;
|
||||
static bool s_cpu_has_last = false;
|
||||
|
||||
static uint64_t runtime_delta(configRUN_TIME_COUNTER_TYPE current, configRUN_TIME_COUNTER_TYPE previous)
|
||||
{
|
||||
return current >= previous ? (uint64_t)(current - previous) : 0U;
|
||||
}
|
||||
|
||||
static bool task_name_is_idle(const char* name)
|
||||
{
|
||||
return name && strncmp(name, "IDLE", 4) == 0;
|
||||
}
|
||||
|
||||
static void reset_cpu_usage_baseline_locked(const TaskStatus_t* tasks, size_t task_count,
|
||||
configRUN_TIME_COUNTER_TYPE total_runtime,
|
||||
int64_t now_us)
|
||||
{
|
||||
size_t copy_count = std::min(task_count, CPU_TASK_SNAPSHOT_MAX);
|
||||
for (size_t i = 0; i < copy_count; ++i) {
|
||||
s_cpu_last_samples[i].task_number = tasks[i].xTaskNumber;
|
||||
s_cpu_last_samples[i].runtime = tasks[i].ulRunTimeCounter;
|
||||
}
|
||||
s_cpu_last_count = copy_count;
|
||||
s_cpu_last_total = total_runtime;
|
||||
s_cpu_last_time_us = now_us;
|
||||
s_cpu_has_last = true;
|
||||
}
|
||||
|
||||
// ★ 改为输出参数模式:CpuUsageSnapshot 608B + CpuTaskSample[24] 192B = 800B
|
||||
// 之前全在栈上,system_status_handler 又拷一份 608B → 叠加 1.4KB,
|
||||
// 加上 OtaSnapshot/TaskStackMetric/wifi_ap_record_t 约 2.3KB 栈消耗。
|
||||
// HTTP 主线程上传 100% 后调用此 handler 时极易栈溢出。改堆分配。
|
||||
static void sample_cpu_usage(CpuUsageSnapshot& result)
|
||||
{
|
||||
result = {};
|
||||
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
|
||||
result.available = true;
|
||||
|
||||
UBaseType_t task_count = uxTaskGetNumberOfTasks();
|
||||
if (task_count == 0) {
|
||||
return;
|
||||
}
|
||||
task_count = std::min<UBaseType_t>(task_count + 2U, CPU_TASK_SNAPSHOT_MAX);
|
||||
|
||||
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
|
||||
if (!tasks) {
|
||||
return;
|
||||
}
|
||||
|
||||
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
|
||||
UBaseType_t captured_count = uxTaskGetSystemState(tasks, task_count, &total_runtime);
|
||||
int64_t now_us = esp_timer_get_time();
|
||||
if (captured_count == 0 || captured_count > task_count) {
|
||||
DEBUG_WARN("STATUS", "CPU stats: uxTaskGetSystemState 返回 0 (actual_tasks=%lu, buf_slots=%lu)",
|
||||
(unsigned long)uxTaskGetNumberOfTasks(), (unsigned long)task_count);
|
||||
free(tasks);
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ previous_samples[24] = 192B,改堆分配减少栈压力
|
||||
CpuTaskSample* previous_samples = (CpuTaskSample*)calloc(CPU_TASK_SNAPSHOT_MAX, sizeof(CpuTaskSample));
|
||||
if (!previous_samples) {
|
||||
free(tasks);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t previous_count = 0;
|
||||
configRUN_TIME_COUNTER_TYPE previous_total = 0;
|
||||
int64_t previous_time_us = 0;
|
||||
bool had_previous = false;
|
||||
|
||||
portENTER_CRITICAL(&s_cpu_stats_mux);
|
||||
previous_count = s_cpu_last_count;
|
||||
previous_total = s_cpu_last_total;
|
||||
previous_time_us = s_cpu_last_time_us;
|
||||
had_previous = s_cpu_has_last;
|
||||
for (size_t i = 0; i < previous_count; ++i) {
|
||||
previous_samples[i] = s_cpu_last_samples[i];
|
||||
}
|
||||
reset_cpu_usage_baseline_locked(tasks, captured_count, total_runtime, now_us);
|
||||
portEXIT_CRITICAL(&s_cpu_stats_mux);
|
||||
|
||||
bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
|
||||
uint64_t total_delta = can_compute ? runtime_delta(total_runtime, previous_total) : 0U;
|
||||
uint64_t idle_delta = 0U;
|
||||
if (can_compute && total_delta > 0U) {
|
||||
for (UBaseType_t i = 0; i < captured_count; ++i) {
|
||||
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
|
||||
bool found_previous = false;
|
||||
for (size_t j = 0; j < previous_count; ++j) {
|
||||
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
|
||||
last_runtime = previous_samples[j].runtime;
|
||||
found_previous = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found_previous) {
|
||||
continue;
|
||||
}
|
||||
uint64_t delta = runtime_delta(tasks[i].ulRunTimeCounter, last_runtime);
|
||||
double pct = (double)delta * 100.0 / (double)total_delta;
|
||||
if (task_name_is_idle(tasks[i].pcTaskName)) {
|
||||
idle_delta += delta;
|
||||
}
|
||||
if (result.task_count < CPU_TASK_SNAPSHOT_MAX) {
|
||||
CpuTaskMetric& metric = result.tasks[result.task_count++];
|
||||
metric.name = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
|
||||
metric.task_number = tasks[i].xTaskNumber;
|
||||
metric.runtime_delta = delta;
|
||||
metric.usage_pct = pct;
|
||||
}
|
||||
}
|
||||
|
||||
result.valid = true;
|
||||
result.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
|
||||
result.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
|
||||
result.usage_pct = std::max(0.0, 100.0 - result.idle_pct);
|
||||
}
|
||||
|
||||
if (!result.valid) {
|
||||
DEBUG_DBG("STATUS", "CPU stats: valid=false had_prev=%d total=%lu prev_total=%lu "
|
||||
"can_compute=%d delta=%lu captured=%lu",
|
||||
(int)had_previous,
|
||||
(unsigned long)total_runtime, (unsigned long)previous_total,
|
||||
(int)can_compute, (unsigned long)total_delta,
|
||||
(unsigned long)captured_count);
|
||||
}
|
||||
|
||||
free(previous_samples);
|
||||
free(tasks);
|
||||
#else
|
||||
result.available = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
|
||||
{
|
||||
if (handle == NULL) return 0;
|
||||
@@ -1089,6 +924,7 @@ static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...)
|
||||
return offset + (int)written;
|
||||
}
|
||||
|
||||
/** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样) */
|
||||
static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu)
|
||||
{
|
||||
offset = append_jsonf(buf, cap, offset,
|
||||
@@ -1168,7 +1004,9 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
connected = true;
|
||||
}
|
||||
|
||||
// ★ PR1: 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter
|
||||
// 先自愈孤儿忙态(如 SHA 失败后卡在 ESP32_FINALIZING),再取快照给 UI
|
||||
OtaCoordinator::instance().healOrphanStates();
|
||||
// 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter
|
||||
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
|
||||
|
||||
size_t free_heap = esp_get_free_heap_size();
|
||||
@@ -1190,20 +1028,17 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
}
|
||||
}
|
||||
|
||||
/* CPU 采样在堆上;失败则跳过(available=false),不再 500 */
|
||||
/* CPU:只读 cpu_stats 任务的 1Hz 快照,禁止在 httpd 里 uxTaskGetSystemState */
|
||||
CpuUsageSnapshot cpu_local = {};
|
||||
CpuUsageSnapshot *cpu = &cpu_local;
|
||||
CpuUsageSnapshot *cpu_heap = nullptr;
|
||||
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 1024U)) {
|
||||
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 512U)) {
|
||||
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;
|
||||
}
|
||||
CpuStats::instance().getSnapshot(*cpu);
|
||||
|
||||
char *resp = s_status_json;
|
||||
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
|
||||
@@ -1309,11 +1144,10 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /system/probe — GD32 APP 单次 SPI 探测(PR2)
|
||||
// GET /system/probe — GD32 APP 单次 SPI 探测
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 显式触发一次 GD32 APP 存活探测。
|
||||
* 从 system_status_handler 移出的副作用,改为独立端点。
|
||||
* 成功时更新 OtaRelay 缓存的 alive/version 并返回版本信息。
|
||||
*/
|
||||
esp_err_t system_probe_handler(httpd_req_t *req)
|
||||
|
||||
+15
-13
@@ -166,7 +166,7 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
|
||||
inst.setStaRetryCount(0);
|
||||
inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志
|
||||
inst.stateUnlock();
|
||||
// ★ M2: 推送 WiFi 连接状态给 HealthMonitor(解耦监控层主动查询)
|
||||
// 推送 WiFi 连接状态给 HealthMonitor
|
||||
HealthMonitor::instance().setWifiConnected(true);
|
||||
break;
|
||||
|
||||
@@ -177,7 +177,7 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
|
||||
bool exhausted = inst.getStaRetryExhausted();
|
||||
int retryCount = inst.getStaRetryCount();
|
||||
inst.stateUnlock();
|
||||
// ★ M2: 推送 WiFi 断开状态给 HealthMonitor
|
||||
// 推送 WiFi 断开状态给 HealthMonitor
|
||||
HealthMonitor::instance().setWifiConnected(false);
|
||||
|
||||
if (!exhausted) {
|
||||
@@ -401,15 +401,17 @@ esp_err_t WifiOta::httpServerStart()
|
||||
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
||||
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
||||
/* LWIP_MAX_SOCKETS=12;HTTP 内部约 3 + UDP 日志 1 → 业务 socket 不宜过大。
|
||||
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。 */
|
||||
config.max_open_sockets = 4;
|
||||
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
|
||||
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
|
||||
config.lru_purge_enable = false;
|
||||
/* 1MB sequential 擦写时,httpd 在 write 阶段可能 >60s 不调 recv;
|
||||
* 超时后 handler 内会重试,但过短会放大“卡 100%”观感。 */
|
||||
config.recv_wait_timeout = 120;
|
||||
config.send_wait_timeout = 60;
|
||||
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。
|
||||
* ★ 修复:增加到 7,防止浏览器超时 abort 后旧连接占满所有槽位。 */
|
||||
config.max_open_sockets = 7;
|
||||
/* ★ 修复:启用 LRU 淘汰,当所有 socket 被占满时主动关闭最旧空闲连接。
|
||||
* OTA 上传期间前端已暂停轮询(pausePollingForUpload),不会踢掉 upload 连接。
|
||||
* 若仍担心极端情况,可在 ota_upload_handler 入口设置该 session 的更长超时。 */
|
||||
config.lru_purge_enable = true;
|
||||
/* ★ 修复:缩短超时,加速释放废弃连接。
|
||||
* OTA 写 Flash 期间 handler 本身持有连接不会被超时踢(httpd 只在 idle 时检测)。 */
|
||||
config.recv_wait_timeout = 10;
|
||||
config.send_wait_timeout = 10;
|
||||
|
||||
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
||||
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
||||
@@ -508,7 +510,7 @@ bool WifiOta::isConnected()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)
|
||||
// IBleCoexListener 接口实现
|
||||
// ============================================================
|
||||
void WifiOta::onBleOtaStart()
|
||||
{
|
||||
@@ -603,7 +605,7 @@ bool WifiOta::earlyInit()
|
||||
// 创建后台重连任务:避免在系统事件任务中调用 esp_wifi_connect()/vTaskDelay 导致死锁
|
||||
if (WifiOta::instance().m_reconnectTask == nullptr) {
|
||||
BaseType_t ret = xTaskCreate(&WifiOta::reconnectTask, "wifi_reconnect",
|
||||
4096, NULL, 5,
|
||||
TASK_STACK_WIFI_RECONNECT, NULL, 5,
|
||||
&WifiOta::instance().m_reconnectTask);
|
||||
if (ret != pdPASS) {
|
||||
DEBUG_ERR("WIFI", "创建重连任务失败");
|
||||
|
||||
@@ -90,7 +90,7 @@ public:
|
||||
/** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */
|
||||
void setBleOtaActive(bool v) { m_bleOtaActive.store(v); }
|
||||
|
||||
// ── IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)──
|
||||
// ── IBleCoexListener 接口实现 ──
|
||||
/** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询。 */
|
||||
void onBleOtaStart() override;
|
||||
/** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */
|
||||
|
||||
Reference in New Issue
Block a user