feat(main): 添加启动模块自检与汇总功能

- 新增 SystemBoot 类用于跟踪各模块初始化状态,提供位图标记与报告功能
- 在 main.cpp 中重构初始化流程,按顺序初始化各模块并记录状态
- 添加 system_boot.cpp 模块文件到构建配置
- CAN反馈任务增加冷启动延迟,避免与GD32初始化窗口冲突

refactor(ota): 优化OTA协调器线程安全机制

- 修改 lock()/unlock() 方法返回take结果,确保mutex正确释放
- 修复多任务并发时mutex死锁问题,避免vTaskPriorityDisinherit断言
- 统一finalize失败后的状态复位逻辑,确保协调器回到IDLE状态
- 优化忙状态超时检测与自愈机制

perf(boot): 优化冷启动时序与资源管理

- 简化README中冷启动优化说明,聚焦核心逻辑
- 优化BOD阈值设置为LVL5,改进电源稳定性
- 增加BLE OTA工作线程优先级,减少丢包情况
- 优化堆告警节流机制,在OTA期间降低告警频率

fix(relay): 改进GD32应用存活检测逻辑

- 调整GD32 APP存活验证时序,增加会话建立验证
- 修复HELLO建立session后未验证role的问题
- 优化FINISH后等待APP启动的静默时间与重试机制
- 统一GD32探测逻辑到OTA中继任务

refactor(server): 优化HTTP服务端内存与性能

- 使用静态缓冲区替代动态分配,减少堆碎片化风险
- 优化CAN批量发送期间暂停反馈任务的机制
- 调整BLE连接参数请求逻辑,提升传输性能
- 改进错误处理流程,避免堆紧张时的额外分配

bump(version): 更新固件版本至0.3.50

- 更新版本号从0.3.49到0.3.50
- 更新编译时间戳与Git哈希值
This commit is contained in:
小龙虾
2026-07-17 02:02:36 +08:00
parent 6364eacfb1
commit 1b10f89d8c
39 changed files with 851 additions and 331 deletions
+20 -16
View File
@@ -238,13 +238,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
}
// ============================================================
// WiFi 共存策略:BLE OTA 传输期间的处理
//
// 不切换 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)。
// BLE OTA 期间 WiFi 共存:保持 WIFI_PS_NONE;暂停 RSSI 轮询
// ============================================================
static bool s_wifi_ps_boosted = false;
@@ -365,24 +359,28 @@ static int ble_ota_request_conn_params(uint16_t conn_handle, bool windows_compat
{
struct ble_gap_upd_params params = {};
if (windows_compatible) {
/* 15–20 ms:多数 Windows 主机可接受 */
params.itvl_min = 12;
params.itvl_max = 16;
params.min_ce_len = 0;
params.max_ce_len = 0;
} else {
/* 7.5–15 ms:勿锁死 7.5,否则 Windows 常拒然后掉到更慢默认值 */
params.itvl_min = 6;
params.itvl_max = 6;
params.itvl_max = 12;
params.min_ce_len = 12;
params.max_ce_len = 12;
params.max_ce_len = 24;
}
params.latency = 0;
params.supervision_timeout = 200;
int rc = ble_gap_update_params(conn_handle, &params);
DEBUG_INFO(TAG, "请求 BLE 连接参数(%s): interval=%.1f..%.1fms rc=%d",
DEBUG_INFO(TAG, "请求 BLE 连接参数(%s): interval=%.1f..%.1fms ce=%u..%u rc=%d",
windows_compatible ? "Windows兼容" : "高速",
params.itvl_min * 1.25f,
params.itvl_max * 1.25f,
(unsigned)params.min_ce_len,
(unsigned)params.max_ce_len,
rc);
return rc;
}
@@ -646,6 +644,11 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
esp32_reorder_alloc();
// ★ BLE OTA 开始:降低 WiFi 射频占用
ble_ota_boost_wifi_ps();
/* 刷写前再要一次高速连接参数(连接时若未谈妥,此处补刀) */
if (BleOta::instance().m_bleConnected &&
BleOta::instance().m_connHandle != 0xFFFFU) {
(void)ble_ota_request_conn_params(BleOta::instance().m_connHandle, false);
}
return ESP_OK;
}
@@ -1317,6 +1320,10 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
s_ble_gd32_meta.total_size = total_size;
s_ble_gd32_meta_active = true;
ble_ota_boost_wifi_ps();
if (BleOta::instance().m_bleConnected &&
BleOta::instance().m_connHandle != 0xFFFFU) {
(void)ble_ota_request_conn_params(BleOta::instance().m_connHandle, false);
}
}
}
@@ -2017,11 +2024,8 @@ esp_err_t BleOta::serverStart()
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 不受影响。
* ESP32-C3 WiFi/BLE 共用射频:此处仅软挂起(停广播、断连、拒收新数据),
* 不调用 nimble_port_stop / deinit / controller_disable。
*/
if (m_suspendedForWifiOta) {
DEBUG_DBG(TAG, "suspendStack: 已挂起,跳过");
@@ -2146,7 +2150,7 @@ void BleOta::taskEntry(void *pvParameters)
"ble_ota_worker",
TASK_STACK_BLE_WORKER, // 实测 peak~1.1KB,负载峰值曾~1.9KB
NULL,
10, // 中等优先级,避免被 NimBLE host task 过度抢占
12, // 略提高:尽快排空队列,减少 withoutResponse 连发丢包
NULL);
if (wk_ok != pdPASS) {
DEBUG_ERR(TAG, "OTA 数据工作线程创建失败,BLE OTA 数据传输不可用");
+16 -27
View File
@@ -34,6 +34,7 @@
#include "monitor/can_feedback_task.h"
#include "monitor/task_stack_check.h"
#include "ble_ota.h"
#include "system_boot.h"
static size_t parseHexToBytes(const char *hex, uint8_t *out, size_t max_out)
{
@@ -641,7 +642,7 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
valid[i] = getLatestCanFeedback(i, &results[i]);
}
/* ★ 修复:改用静态缓冲区,避免每次请求 malloc 8KB 加剧堆碎片化 */
/* 静态响应缓冲,减少频繁大块堆分配 */
static char s_feedback_json[8192];
char *resp = s_feedback_json;
memset(resp, 0, sizeof(s_feedback_json));
@@ -837,17 +838,15 @@ esp_err_t can_send_handler(httpd_req_t *req)
* @brief POST /can/send_batch — 同通道连续发送多帧,单次 HTTP 完成。
*
* JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] }
* 返回: { "status":"ok", "results":[ { "status":"ok"|"error", ... }, ... ] }
* 帧间 5ms;暂停 CAN 反馈,避免与批量 SPI 竞态导致偶发崩溃。
* 返回: { "status":"ok", "results":[ ... ], 可选 skipped/abort_* }
* 帧间间隔 kInterFrameDelayMs;批量期间暂停 CAN 反馈任务。
*/
esp_err_t can_send_batch_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
/* 单次尽量装下整批,避免前端分块导致“一阵一阵” */
constexpr int kMaxBody = 49152;
constexpr int kMaxFrames = 128;
/* 成功路径通常 <20ms;失败路径不再拖到数百 ms */
constexpr uint32_t kPerFrameTimeoutMs = 80U;
constexpr uint32_t kInterFrameDelayMs = 5U;
@@ -906,7 +905,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON_AddStringToObject(out_root, "status", "ok");
cJSON_AddItemToObject(out_root, "results", results);
/* 暂停 0xC1 反馈抢 SPI,降低 worker 竞态 / 长时间占线导致的 WDT 风险 */
/* 批量发送期间暂停 CAN 反馈,独占 SPI 业务路径 */
pauseCanFeedbackTask(true);
auto fill_err = [](cJSON *item, int code, uint8_t gd32_err) {
@@ -918,11 +917,6 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON_AddNumberToObject(item, "gd32_err", gd32_err);
};
/* 中途致命错误后不再逐帧 SPI 重试(否则每帧再等一整段超时) */
bool abort_rest = false;
int abort_code = -2;
uint8_t abort_gd32 = 0U;
for (int i = 0; i < frame_count; i++) {
(void)esp_task_wdt_reset();
@@ -932,11 +926,6 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
}
cJSON_AddItemToArray(results, item);
if (abort_rest) {
fill_err(item, abort_code, abort_gd32);
continue; /* 无 SPI、无帧间延时 */
}
cJSON *fr = cJSON_GetArrayItem(frames, i);
if (!cJSON_IsObject(fr)) {
fill_err(item, -1, 0);
@@ -987,25 +976,22 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON_AddStringToObject(item, "recv", resp_hex);
} else {
fill_err(item, resp_len, gd32_err);
/* SPI 失败 / BUS_OFF / TEC 过高:后面不再逐帧 SPI,也不再为每帧分配 cJSON */
/* SPI 失败 / BUS_OFF / TEC 过高:中止剩余帧,由 skipped 字段告知前端 */
const bool fatal_spi = (resp_len == -2);
const bool fatal_bus = (resp_len == -3) &&
(gd32_err == 0xE2U || gd32_err == 0xE3U);
if (fatal_spi || fatal_bus) {
abort_rest = true;
abort_code = resp_len;
abort_gd32 = gd32_err;
const int skipped = frame_count - i - 1;
if (skipped > 0) {
cJSON_AddNumberToObject(out_root, "skipped", skipped);
cJSON_AddNumberToObject(out_root, "abort_code", abort_code);
cJSON_AddNumberToObject(out_root, "abort_gd32_err", abort_gd32);
cJSON_AddNumberToObject(out_root, "abort_code", resp_len);
cJSON_AddNumberToObject(out_root, "abort_gd32_err", gd32_err);
}
break; /* 不再为剩余帧 CreateObject,避免堆被打穿 */
break;
}
}
if (i + 1 < frame_count && !abort_rest) {
if (i + 1 < frame_count) {
vTaskDelay(pdMS_TO_TICKS(kInterFrameDelayMs));
}
}
@@ -1018,9 +1004,8 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON_Delete(out_root);
out_root = nullptr;
if (!printed) {
/* 堆已紧时勿再走大段错误路径分配;给前端最小 JSON */
static const char kOom[] =
"{\"status\":\"error\",\"code\":\"json_print\",\"message\":\"JSON输出失败(堆不足)\"}";
"{\"status\":\"error\",\"code\":\"json_print\",\"message\":\"JSON输出失败\"}";
httpd_resp_set_status(req, "500 Internal Server Error");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, kOom, HTTPD_RESP_USE_STRLEN);
@@ -1104,7 +1089,8 @@ 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;
/* 含 boot 模块列表与 CPU 任务;OTA 后 largest free 常偏紧,仍用静态缓冲避免 calloc */
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 7168;
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;
@@ -1344,6 +1330,9 @@ esp_err_t system_status_handler(httpd_req_t *req)
(unsigned long)ls.stats.max_resp_latency_us);
}
/* 启动自检位图(与串口 BOOT report 同源) */
len = SystemBoot::appendStatusJson(resp, SYSTEM_STATUS_JSON_CAP, len);
// 前导逗号与前一块分隔;之前因 append_cpu_status_json 尾随逗号叠加
// 形成 "],," 导致前端 JSON.parse 报 "Expected double-quoted property name"
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, ",\"task_stack\": {");
+9 -20
View File
@@ -313,12 +313,9 @@ esp_err_t WifiOta::initSta()
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
// ★ 冷启动时在 wifi_start 前增加延迟
// 长时间断电后首次上电,WiFi PHY 校准需要额外时间
// esp_wifi_start() 内部会触发 PHY 初始化和校准
// 如果校准未完成就启动射频,可能导致连接失败
/* 冷启动时在 wifi_start 前短延时 */
if (WifiOta::instance().m_coldBoot) {
DEBUG_INFO("WIFI", "冷启动: wifi_start 前再等 300ms...");
DEBUG_INFO("WIFI", "冷启动: wifi_start 前等待 300ms");
vTaskDelay(pdMS_TO_TICKS(300));
}
@@ -400,16 +397,11 @@ esp_err_t WifiOta::httpServerStart()
config.server_port = HTTP_SERVER_PORT;
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)。
* ★ 修复:增加到 7,防止浏览器超时 abort 后旧连接占满所有槽位。 */
/* LWIP_MAX_SOCKETS=12;HTTP 内部约占 3 + UDP 日志 1;业务连接上限 7 */
config.max_open_sockets = 7;
/* ★ 修复:启用 LRU 淘汰,当所有 socket 被占满时主动关闭最旧空闲连接。
* OTA 上传期间前端已暂停轮询(pausePollingForUpload),不会踢掉 upload 连接。
* 若仍担心极端情况,可在 ota_upload_handler 入口设置该 session 的更长超时。 */
/* 满连接时淘汰最久未用的空闲 socket */
config.lru_purge_enable = true;
/* ★ 修复:缩短超时,加速释放废弃连接。
* OTA 写 Flash 期间 handler 本身持有连接不会被超时踢(httpd 只在 idle 时检测)。 */
/* idle 收发超时(秒);进行中的 handler 连接不受此 idle 检测踢掉 */
config.recv_wait_timeout = 10;
config.send_wait_timeout = 10;
@@ -551,10 +543,9 @@ bool WifiOta::earlyInit()
"OTHER",
WifiOta::instance().m_coldBoot ? "是" : "否");
/* 与 GD32/CAN 收发器同板上电时,电源上升沿慢 + 负载大。
* 若立刻清 PHY/开 WiFi 全量校准,易触发 brownout,表现为「上电起不来,拔插才好」。 */
/* 冷启动:电源与外设上电完成后再初始化 NVS/WiFi */
if (WifiOta::instance().m_coldBoot) {
DEBUG_INFO("WIFI", "冷启动: 电源轨稳定等待 400ms(再做 NVS/WiFi)...");
DEBUG_INFO("WIFI", "冷启动: 等待 400ms 后初始化 NVS/WiFi");
vTaskDelay(pdMS_TO_TICKS(400));
}
@@ -576,9 +567,7 @@ bool WifiOta::earlyInit()
DEBUG_INFO("WIFI", "NVS 初始化成功");
}
// ★ 冷启动:只清 WiFi 连接缓存;勿在每次 POWERON 都清 PHY。
// 全量 PHY 校准电流大,与 GD32 同时上电易 brownout → 必须再插拔才能起来。
// 仅在欠压复位后清 PHY,避免校准脏数据死循环。
/* 冷启动清理 WiFi 连接缓存;欠压复位后再清 PHY 强制重校准 */
if (WifiOta::instance().m_coldBoot) {
err = clearNvsNamespace("nvs.net80211");
if (err == ESP_OK) {
@@ -589,7 +578,7 @@ bool WifiOta::earlyInit()
if (reset_reason == ESP_RST_BROWNOUT) {
err = clearNvsNamespace("phy");
if (err == ESP_OK) {
DEBUG_WARN("WIFI", "BROWNOUT 后: 已清除 PHY 校准,将重新校准");
DEBUG_WARN("WIFI", "欠压复位后: 已清除 PHY 校准数据");
}
}
}