feat(can): 优化批量发送性能并修复竞态问题

- 帧间添加5ms延时,暂停CAN反馈避免与批量SPI竞争导致偶发崩溃
- 单次批量容量从48帧提升至128帧,最大body大小从12KB增至48KB
- 单帧超时从400ms降至80ms,失败后不再拖慢整体速度
- 暂停0xC1反馈任务降低worker竞态和WDT风险
- 中途致命错误后跳过后续帧发送,避免每帧等待超时
- 添加详细的JSON错误响应格式
- 修复内存分配失败时的资源清理问题

refactor(wifi): 优化冷启动流程避免电源竞争

- 电源轨稳定等待策略调整:POWERON/BROWNOUT复位时等待200ms
- 仅BROWNOUT复位后清除PHY校准数据,避免每次上电全量校准
- 电源稳定等待期间等待400ms后再进行NVS/WiFi操作
- WiFi启动前延时从500ms调整为300ms
- BOD阈值适当放宽避免上电瞬间复位

perf(monitor): 调整堆内存监控策略

- 内存紧张时增大健康监控间隔:低于8KB时从3s增至10s
- 避免日志输出消耗更多堆内存造成雪崩效应

docs(readme): 更新冷启动优化说明

- 详细描述电源轨稳定等待机制
- 说明仅BROWNOUT后才强制PHY重校准的策略
- 解释避免与GD32抢电流的设计考虑

style(html): 更新前端资源版本号

- 更新CSS和JavaScript文件的版本参数
- 修复页面加载时的资源缓存问题

fix(ble): 修复阶段显示状态问题

- 终态阶段全部显示为完成状态(绿色)
- 优化状态栏样式更新逻辑

feat(ota): 优化SPI通信超时处理

- 将总超时拆分为发送和轮询两个阶段
- 禁止轮询单独占用500ms避免短超时批量卡顿
- 优化极短超时情况下的时间分配策略

refactor(can): 减少批量操作时的日志输出

- SPI失败和GD32拒绝时按类型聚合日志
- 每32次相同错误才输出一次完整日志
- 避免大量错误时UDP/格式化耗尽堆内存
This commit is contained in:
小龙虾
2026-07-17 00:24:09 +08:00
parent 5db3f66b8e
commit 6364eacfb1
19 changed files with 256 additions and 116 deletions
+103 -36
View File
@@ -16,6 +16,7 @@
#include "esp_ota_ops.h"
#include "esp_task_wdt.h"
#include "esp_heap_caps.h"
#include "esp_rom_sys.h"
#include "cJSON.h"
#include "app_config.h"
@@ -837,26 +838,28 @@ esp_err_t can_send_handler(httpd_req_t *req)
*
* JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] }
* 返回: { "status":"ok", "results":[ { "status":"ok"|"error", ... }, ... ] }
* 帧间无人为延时;单帧 SPI 超时 400ms(批量优先吞吐)。
* 帧间 5ms;暂停 CAN 反馈,避免与批量 SPI 竞态导致偶发崩溃。
*/
esp_err_t can_send_batch_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
constexpr int kMaxBody = 12288;
constexpr int kMaxFrames = 48;
constexpr uint32_t kPerFrameTimeoutMs = 400U;
/* 单次尽量装下整批,避免前端分块导致“一阵一阵” */
constexpr int kMaxBody = 49152;
constexpr int kMaxFrames = 128;
/* 成功路径通常 <20ms;失败路径不再拖到数百 ms */
constexpr uint32_t kPerFrameTimeoutMs = 80U;
constexpr uint32_t kInterFrameDelayMs = 5U;
int content_len = req->content_len;
if (content_len <= 0 || content_len > kMaxBody) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "body 长度无效或过大");
return ESP_FAIL;
return http_send_json_error(req, "400 Bad Request", "bad_body",
"body 长度无效或过大");
}
char *buf = static_cast<char *>(malloc(static_cast<size_t>(content_len) + 1U));
if (!buf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存不足");
return ESP_FAIL;
return http_send_json_error(req, "500 Internal Server Error", "oom", "内存不足");
}
int received = 0;
@@ -864,8 +867,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
int ret = httpd_req_recv(req, buf + received, content_len - received);
if (ret <= 0) {
free(buf);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
return ESP_FAIL;
return http_send_json_error(req, "400 Bad Request", "recv_fail", "接收数据失败");
}
received += ret;
}
@@ -873,41 +875,71 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON *root = cJSON_Parse(buf);
free(buf);
buf = nullptr;
if (!root) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
return ESP_FAIL;
return http_send_json_error(req, "400 Bad Request", "json_parse", "JSON 解析失败");
}
cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
cJSON *frames = cJSON_GetObjectItem(root, "frames");
if (!ch_item || !cJSON_IsArray(frames)) {
cJSON_Delete(root);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "需要 ch 与 frames 数组");
return ESP_FAIL;
return http_send_json_error(req, "400 Bad Request", "bad_args", "需要 ch 与 frames 数组");
}
can_channel_t ch = static_cast<can_channel_t>(cJSON_GetNumberValue(ch_item));
const int frame_count = cJSON_GetArraySize(frames);
if (frame_count <= 0 || frame_count > kMaxFrames) {
cJSON_Delete(root);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "frames 数量无效(1~48)");
return ESP_FAIL;
return http_send_json_error(req, "400 Bad Request", "bad_frames",
"frames 数量无效(1~128)");
}
cJSON *out_root = cJSON_CreateObject();
cJSON *results = cJSON_CreateArray();
if (out_root == nullptr || results == nullptr) {
cJSON_Delete(out_root);
cJSON_Delete(results);
cJSON_Delete(root);
return http_send_json_error(req, "500 Internal Server Error", "oom", "JSON 分配失败");
}
cJSON_AddStringToObject(out_root, "status", "ok");
cJSON_AddItemToObject(out_root, "results", results);
/* 暂停 0xC1 反馈抢 SPI,降低 worker 竞态 / 长时间占线导致的 WDT 风险 */
pauseCanFeedbackTask(true);
auto fill_err = [](cJSON *item, int code, uint8_t gd32_err) {
if (!item) {
return;
}
cJSON_AddStringToObject(item, "status", "error");
cJSON_AddNumberToObject(item, "code", code);
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++) {
cJSON *fr = cJSON_GetArrayItem(frames, i);
(void)esp_task_wdt_reset();
cJSON *item = cJSON_CreateObject();
if (item == nullptr) {
break;
}
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)) {
cJSON_AddStringToObject(item, "status", "error");
cJSON_AddNumberToObject(item, "code", -1);
cJSON_AddNumberToObject(item, "gd32_err", 0);
fill_err(item, -1, 0);
continue;
}
@@ -915,53 +947,88 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
cJSON *data_item = cJSON_GetObjectItem(fr, "data");
cJSON *dlc_item = cJSON_GetObjectItem(fr, "dlc");
if (!id_item) {
cJSON_AddStringToObject(item, "status", "error");
cJSON_AddNumberToObject(item, "code", -1);
cJSON_AddNumberToObject(item, "gd32_err", 0);
fill_err(item, -1, 0);
continue;
}
uint32_t ext_id = static_cast<uint32_t>(cJSON_GetNumberValue(id_item));
uint8_t dlc = dlc_item ? static_cast<uint8_t>(cJSON_GetNumberValue(dlc_item)) : 0U;
uint8_t tx_data[CAN_MAX_DLC] = {0};
if (dlc > CAN_MAX_DLC) {
dlc = CAN_MAX_DLC;
}
uint8_t tx_data[CAN_MAX_DLC];
memset(tx_data, 0, sizeof(tx_data));
uint8_t tx_dlc = 0U;
if (data_item && cJSON_IsString(data_item)) {
tx_dlc = static_cast<uint8_t>(parseHexToBytes(cJSON_GetStringValue(data_item),
tx_data, CAN_MAX_DLC));
const char *hex = cJSON_GetStringValue(data_item);
if (hex) {
tx_dlc = static_cast<uint8_t>(parseHexToBytes(hex, tx_data, CAN_MAX_DLC));
}
}
if (tx_dlc == 0U && dlc > 0U) {
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
tx_dlc = dlc;
}
uint8_t resp_data[CAN_MAX_DLC] = {0};
uint8_t resp_data[CAN_MAX_DLC];
memset(resp_data, 0, sizeof(resp_data));
uint8_t gd32_err = 0U;
int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc,
resp_data, sizeof(resp_data), kPerFrameTimeoutMs, &gd32_err);
if (resp_len >= 0) {
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
bytesToHexUpper(resp_data, static_cast<size_t>(resp_len), resp_hex);
char resp_hex[CAN_MAX_DLC * 2 + 1];
memset(resp_hex, 0, sizeof(resp_hex));
const size_t copy_n = (static_cast<size_t>(resp_len) < sizeof(resp_data))
? static_cast<size_t>(resp_len) : sizeof(resp_data);
bytesToHexUpper(resp_data, copy_n, resp_hex);
cJSON_AddStringToObject(item, "status", "ok");
cJSON_AddNumberToObject(item, "sent", tx_dlc);
cJSON_AddNumberToObject(item, "recv_len", resp_len);
cJSON_AddNumberToObject(item, "recv_len", static_cast<double>(copy_n));
cJSON_AddStringToObject(item, "recv", resp_hex);
} else {
cJSON_AddStringToObject(item, "status", "error");
cJSON_AddNumberToObject(item, "code", resp_len);
cJSON_AddNumberToObject(item, "gd32_err", gd32_err);
fill_err(item, resp_len, gd32_err);
/* SPI 失败 / BUS_OFF / TEC 过高:后面不再逐帧 SPI,也不再为每帧分配 cJSON */
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);
}
break; /* 不再为剩余帧 CreateObject,避免堆被打穿 */
}
}
if (i + 1 < frame_count && !abort_rest) {
vTaskDelay(pdMS_TO_TICKS(kInterFrameDelayMs));
}
}
pauseCanFeedbackTask(false);
cJSON_Delete(root);
root = nullptr;
char *printed = cJSON_PrintUnformatted(out_root);
cJSON_Delete(out_root);
out_root = nullptr;
if (!printed) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON 输出失败");
/* 堆已紧时勿再走大段错误路径分配;给前端最小 JSON */
static const char kOom[] =
"{\"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);
return ESP_FAIL;
}
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, printed, strlen(printed));
httpd_resp_send(req, printed, HTTPD_RESP_USE_STRLEN);
free(printed);
return ESP_OK;
}