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:
@@ -291,7 +291,7 @@ ESP32 通过 SPI 转发 CAN 帧到 GD32(GD32 端驱动外部 CAN 收发器)
|
||||
| IP 分配 | DHCP(可切换静态 IP 192.168.31.32)|
|
||||
| 国家码 | CN(允许信道 1-13)|
|
||||
|
||||
**冷启动优化**: 复位原因为 POWERON/BROWNOUT 时,清除 WiFi NVS 缓存 + PHY 校准数据,增加 500ms 启动延迟,防止长时间断电后校准残留导致连接失败。
|
||||
**冷启动优化**: 复位原因为 POWERON/BROWNOUT 时:电源轨稳定等待 + 仅清 WiFi 连接缓存;**仅 BROWNOUT 后**才清 PHY 强制重校准(避免每次上电全量 RF 校准与 GD32 抢电流导致起不来、需再插拔)。`wifi_start` 前另有短延迟。BOD 阈值略放宽(LVL5)。
|
||||
|
||||
### mDNS 发现
|
||||
|
||||
|
||||
@@ -49,6 +49,14 @@ extern "C" void app_main(void)
|
||||
esp_chip_info_t chip_info;
|
||||
esp_chip_info(&chip_info);
|
||||
|
||||
/* 上电/欠压复位:先等电源与 GD32 侧负载稳定,再进 WiFi/SPI 重活 */
|
||||
{
|
||||
esp_reset_reason_t rr = esp_reset_reason();
|
||||
if (rr == ESP_RST_POWERON || rr == ESP_RST_BROWNOUT) {
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
}
|
||||
|
||||
Esp32OtaCore::initMutex();
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 48
|
||||
#define FW_VERSION_STRING "0.3.48"
|
||||
#define FW_VERSION_PATCH 49
|
||||
#define FW_VERSION_STRING "0.3.49"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-07-16 22:59:52"
|
||||
#define BUILD_TIME "2026-07-17 00:10:21"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "51c89b3"
|
||||
#define GIT_HASH "5db3f66"
|
||||
|
||||
@@ -224,7 +224,9 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
static TickType_t s_last_heap_warn = 0;
|
||||
TickType_t now = xTaskGetTickCount();
|
||||
const bool ota_busy = !OtaCoordinator::instance().isIdle();
|
||||
const uint32_t interval_ms = ota_busy ? 15000U : 3000U;
|
||||
/* 堆越紧越要少打日志(日志本身还要占堆/UDP) */
|
||||
const uint32_t interval_ms = ota_busy ? 15000U
|
||||
: (free_heap < 8192U ? 10000U : 3000U);
|
||||
/* OTA 中只在 free 极低(<4KB)时告警;空闲时维持 10KB 阈值 */
|
||||
const uint32_t warn_floor = ota_busy ? 4096U : (uint32_t)HEAP_WARNING_MIN;
|
||||
if (free_heap < warn_floor &&
|
||||
|
||||
+103
-36
@@ -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;
|
||||
}
|
||||
|
||||
+20
-12
@@ -318,8 +318,8 @@ esp_err_t WifiOta::initSta()
|
||||
// esp_wifi_start() 内部会触发 PHY 初始化和校准
|
||||
// 如果校准未完成就启动射频,可能导致连接失败
|
||||
if (WifiOta::instance().m_coldBoot) {
|
||||
DEBUG_INFO("WIFI", "冷启动: 等待 PHY 就绪...");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
DEBUG_INFO("WIFI", "冷启动: wifi_start 前再等 300ms...");
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
@@ -533,12 +533,12 @@ void WifiOta::onBleOtaEnd()
|
||||
*/
|
||||
bool WifiOta::earlyInit()
|
||||
{
|
||||
DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
|
||||
DEBUG_INFO("WIFI", "=== WiFi 早期初始化 ===");
|
||||
|
||||
// ★ 冷启动检测:判断是否为上电复位
|
||||
// ★ 冷启动检测:判断是否为上电/欠压复位
|
||||
esp_reset_reason_t reset_reason = esp_reset_reason();
|
||||
WifiOta::instance().m_coldBoot = (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_BROWNOUT);
|
||||
|
||||
|
||||
DEBUG_INFO("WIFI", "复位原因: %d (%s), 冷启动: %s",
|
||||
reset_reason,
|
||||
(reset_reason == ESP_RST_POWERON) ? "POWER_ON" :
|
||||
@@ -551,6 +551,13 @@ bool WifiOta::earlyInit()
|
||||
"OTHER",
|
||||
WifiOta::instance().m_coldBoot ? "是" : "否");
|
||||
|
||||
/* 与 GD32/CAN 收发器同板上电时,电源上升沿慢 + 负载大。
|
||||
* 若立刻清 PHY/开 WiFi 全量校准,易触发 brownout,表现为「上电起不来,拔插才好」。 */
|
||||
if (WifiOta::instance().m_coldBoot) {
|
||||
DEBUG_INFO("WIFI", "冷启动: 电源轨稳定等待 400ms(再做 NVS/WiFi)...");
|
||||
vTaskDelay(pdMS_TO_TICKS(400));
|
||||
}
|
||||
|
||||
esp_err_t err = nvs_flash_init();
|
||||
bool nvs_erased = false;
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
@@ -569,20 +576,21 @@ bool WifiOta::earlyInit()
|
||||
DEBUG_INFO("WIFI", "NVS 初始化成功");
|
||||
}
|
||||
|
||||
// ★ 冷启动时强制清除 WiFi NVS 缓存,避免残留脏数据
|
||||
// 长时间断电后 NVS 中的 WiFi 配置缓存可能不一致
|
||||
// ★ 冷启动:只清 WiFi 连接缓存;勿在每次 POWERON 都清 PHY。
|
||||
// 全量 PHY 校准电流大,与 GD32 同时上电易 brownout → 必须再插拔才能起来。
|
||||
// 仅在欠压复位后清 PHY,避免校准脏数据死循环。
|
||||
if (WifiOta::instance().m_coldBoot) {
|
||||
// 清除 WiFi 驱动内部 NVS 缓存键
|
||||
err = clearNvsNamespace("nvs.net80211");
|
||||
if (err == ESP_OK) {
|
||||
DEBUG_DBG("WIFI", "冷启动: 已清除 WiFi NVS 缓存 (nvs.net80211)");
|
||||
} else {
|
||||
DEBUG_INFO("WIFI", "冷启动: 无 WiFi NVS 缓存分区,跳过清除");
|
||||
}
|
||||
// 同样清除 PHY 校准数据分区,强制下次全量校准
|
||||
err = clearNvsNamespace("phy");
|
||||
if (err == ESP_OK) {
|
||||
DEBUG_DBG("WIFI", "冷启动: 已清除 PHY 校准缓存,将重新校准");
|
||||
if (reset_reason == ESP_RST_BROWNOUT) {
|
||||
err = clearNvsNamespace("phy");
|
||||
if (err == ESP_OK) {
|
||||
DEBUG_WARN("WIFI", "BROWNOUT 后: 已清除 PHY 校准,将重新校准");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,17 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
&resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s",
|
||||
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret));
|
||||
/* 批量失败时勿每帧打日志:UDP/格式化会把堆打穿 */
|
||||
static uint32_t s_spi_fail_n = 0U;
|
||||
static int s_last_spi_err = 0;
|
||||
const int err_i = static_cast<int>(ret);
|
||||
if (s_spi_fail_n == 0U || err_i != s_last_spi_err || (s_spi_fail_n % 32U) == 0U) {
|
||||
DEBUG_WARN("CAN", "CAN_SEND SPI失败: ch=%u id=0x%08lX err=%s (x%lu)",
|
||||
(unsigned)channel, (unsigned long)ext_id, esp_err_to_name(ret),
|
||||
(unsigned long)(s_spi_fail_n + 1U));
|
||||
}
|
||||
s_last_spi_err = err_i;
|
||||
++s_spi_fail_n;
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = 0U;
|
||||
}
|
||||
@@ -87,8 +96,17 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
if (gd32_err_out) {
|
||||
*gd32_err_out = gd32_err;
|
||||
}
|
||||
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X",
|
||||
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err);
|
||||
{
|
||||
static uint32_t s_rej_n = 0U;
|
||||
static uint8_t s_last_rej = 0xFFU;
|
||||
if (s_rej_n == 0U || gd32_err != s_last_rej || (s_rej_n % 32U) == 0U) {
|
||||
DEBUG_WARN("CAN", "CAN_SEND GD32拒绝: ch=%u id=0x%08lX resp=0x%02X err=0x%02X (x%lu)",
|
||||
(unsigned)channel, (unsigned long)ext_id, resp_type, gd32_err,
|
||||
(unsigned long)(s_rej_n + 1U));
|
||||
}
|
||||
s_last_rej = gd32_err;
|
||||
++s_rej_n;
|
||||
}
|
||||
return -3;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h" // esp_random() for HELLO session_id
|
||||
#include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM
|
||||
|
||||
#include "spi_master.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include "app_config.h"
|
||||
@@ -1229,7 +1228,28 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
* ESP_OK - 读到过期帧(正常,忽略)
|
||||
* ESP_ERR_INVALID_RESPONSE - 读到 0xFF(正常,延迟一拍)
|
||||
* 其他 - SPI 真实错误 */
|
||||
const uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
|
||||
/* 将总超时切成 send + poll,禁止 poll 单独再给 500ms(短超时批量时会「失败卡很久」) */
|
||||
uint32_t send_timeout = (timeout_ms > 500U) ? 500U : timeout_ms;
|
||||
if (send_timeout == 0U) {
|
||||
send_timeout = 1U;
|
||||
}
|
||||
/* 至少给 poll 留一点时间;总超时很短时 send/poll 各半 */
|
||||
uint32_t poll_timeout;
|
||||
if (timeout_ms > send_timeout) {
|
||||
poll_timeout = timeout_ms - send_timeout;
|
||||
} else if (timeout_ms >= 40U) {
|
||||
send_timeout = timeout_ms / 2U;
|
||||
if (send_timeout == 0U) {
|
||||
send_timeout = 1U;
|
||||
}
|
||||
poll_timeout = timeout_ms - send_timeout;
|
||||
} else {
|
||||
poll_timeout = timeout_ms; /* 极短超时:发完立刻 poll,总预算仍受 deadline 约束 */
|
||||
}
|
||||
if (poll_timeout == 0U) {
|
||||
poll_timeout = 1U;
|
||||
}
|
||||
|
||||
esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len,
|
||||
nullptr, nullptr, 0U, nullptr, send_timeout,
|
||||
/*expect_stale=*/true);
|
||||
@@ -1237,15 +1257,10 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
return send_ret;
|
||||
}
|
||||
|
||||
/* 等 GD32 完成 onCsRelease;poll 内仅在 DATA_READY 低时变长读(不盲 CS)。 */
|
||||
{
|
||||
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
|
||||
vTaskDelay(gap);
|
||||
}
|
||||
/* 等 GD32 完成 onCsRelease 再 poll;5ms 兼顾「能发出」与批量节奏 */
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
|
||||
/* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */
|
||||
const uint32_t poll_timeout = (timeout_ms > send_timeout)
|
||||
? (timeout_ms - send_timeout) : 500U;
|
||||
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout,
|
||||
/*expected_type=*/0xFFU);
|
||||
}
|
||||
|
||||
@@ -83,3 +83,7 @@ CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MIN_CONN_INTERVAL=12
|
||||
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MAX_CONN_INTERVAL=16
|
||||
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SLAVE_LATENCY=0
|
||||
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SUPERVISION_TMO=200
|
||||
|
||||
# 与 GD32 同板上电:略放宽欠压阈值,减少上电瞬间复位(仍保留 BOD)
|
||||
CONFIG_ESP_BROWNOUT_DET=y
|
||||
CONFIG_ESP_BROWNOUT_DET_LVL_SEL_5=y
|
||||
|
||||
+6
-6
@@ -4,11 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 通信控制台</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260717t">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260717t">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260717t">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260717t">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260717t">
|
||||
<link rel="stylesheet" href="css/base.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260717w">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
@@ -318,6 +318,6 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/can.js?v=20260717t"></script>
|
||||
<script type="module" src="js/can.js?v=20260717w"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+8
-8
@@ -9,11 +9,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 OTA 中继控制台</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260716o">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260716o">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260716o">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260716o">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260716o">
|
||||
<link rel="stylesheet" href="css/base.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260716q">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -425,8 +425,8 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/app.js?v=20260716p"></script>
|
||||
<script type="module" src="js/ota/console-mode.js?v=20260716p"></script>
|
||||
<script type="module" src="js/ble_ota.js?v=20260716p"></script>
|
||||
<script type="module" src="js/app.js?v=20260716q"></script>
|
||||
<script type="module" src="js/ota/console-mode.js?v=20260716q"></script>
|
||||
<script type="module" src="js/ble_ota.js?v=20260716q"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -26,11 +26,13 @@ export function resetBleStages() {
|
||||
|
||||
export function setBleStage(stageNum, detail, barState) {
|
||||
// stageNum: 1-6, detail: sub-step text
|
||||
// 终态 stageNum===6:全部 done(绿);中间步当前为 live(蓝)
|
||||
var finished = stageNum >= 6;
|
||||
for (var i = 0; i < bleStageIds.length; i++) {
|
||||
var el = document.getElementById(bleStageIds[i]);
|
||||
if (!el) continue;
|
||||
if (i + 1 < stageNum) el.className = 'stage-node done';
|
||||
else if (i + 1 === stageNum) el.className = 'stage-node live';
|
||||
else if (i + 1 === stageNum) el.className = finished ? 'stage-node done' : 'stage-node live';
|
||||
else el.className = 'stage-node';
|
||||
}
|
||||
// Set detail text
|
||||
@@ -41,7 +43,8 @@ export function setBleStage(stageNum, detail, barState) {
|
||||
// Update status bar
|
||||
var bar = document.getElementById('stageStatusBar');
|
||||
if (bar) {
|
||||
bar.className = 'stage-status-bar' + (barState ? ' ' + barState : (stageNum > 0 ? ' active' : ''));
|
||||
var cls = barState || (finished ? 'ok' : (stageNum > 0 ? 'active' : ''));
|
||||
bar.className = 'stage-status-bar' + (cls ? ' ' + cls : '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -20,7 +20,7 @@
|
||||
import {
|
||||
state, el, getBaseUrl, getDeviceIp, setDeviceIp, parseDbc, readDbcText, buildSummary,
|
||||
rebuildMergedDbc, makeDbcSourceId
|
||||
} from './can/dbc.js?v=20260717t';
|
||||
} from './can/dbc.js?v=20260717w';
|
||||
import {
|
||||
updateDbcFileInfo,
|
||||
filterMessages,
|
||||
@@ -30,12 +30,12 @@ import {
|
||||
recomputeTransmitterFilterEmpty,
|
||||
updateNodeFilterLabel,
|
||||
updateBatchUi
|
||||
} from './can/messages.js?v=20260717t';
|
||||
} from './can/messages.js?v=20260717w';
|
||||
import {
|
||||
ensureMessageDefaults,
|
||||
renderSelectedMessage,
|
||||
updateEncodedPreview
|
||||
} from './can/signals.js?v=20260717t';
|
||||
} from './can/signals.js?v=20260717w';
|
||||
import {
|
||||
renderHistory,
|
||||
sendSelectedMessage,
|
||||
@@ -44,15 +44,15 @@ import {
|
||||
sendManualFrame,
|
||||
resetManualFields,
|
||||
loadPreviewToManual
|
||||
} from './can/send.js?v=20260717t';
|
||||
} from './can/send.js?v=20260717w';
|
||||
import {
|
||||
scheduleCanPersist,
|
||||
loadCanSession,
|
||||
bindCanPersistLifecycle
|
||||
} from './can/persist.js?v=20260717t';
|
||||
import { escapeHtml as sanitizeHtml, formatUptime } from './common/utils.js?v=20260717t';
|
||||
import { showToast } from './common/toast.js?v=20260717t';
|
||||
import { initCanMonitor } from './can_monitor.js?v=20260717t';
|
||||
} from './can/persist.js?v=20260717w';
|
||||
import { escapeHtml as sanitizeHtml, formatUptime } from './common/utils.js?v=20260717w';
|
||||
import { showToast } from './common/toast.js?v=20260717w';
|
||||
import { initCanMonitor } from './can_monitor.js?v=20260717w';
|
||||
|
||||
// ==================== 本地工具 ====================
|
||||
// 原 can.js 的 formatUptime 输出格式与 common/utils.js 略有差异(d>0 时只到 h),
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
* common/utils.js(escapeHtml/formatBytes)、
|
||||
* signals.js(renderInlineSignalGrid/ensureMessageDefaults/renderSelectedMessage/updateEncodedPreview)。
|
||||
*/
|
||||
import { state, el, padHex, normalizeWhitespace, cssEscape } from './dbc.js?v=20260717t';
|
||||
import { escapeHtml as sanitizeHtml, formatBytes } from '../common/utils.js?v=20260717t';
|
||||
import { renderInlineSignalGrid, ensureMessageDefaults, renderSelectedMessage, updateEncodedPreview } from './signals.js?v=20260717t';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717t';
|
||||
import { state, el, padHex, normalizeWhitespace, cssEscape } from './dbc.js?v=20260717v';
|
||||
import { escapeHtml as sanitizeHtml, formatBytes } from '../common/utils.js?v=20260717v';
|
||||
import { renderInlineSignalGrid, ensureMessageDefaults, renderSelectedMessage, updateEncodedPreview } from './signals.js?v=20260717v';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717v';
|
||||
|
||||
// ==================== DBC 文件信息 ====================
|
||||
/** @description 更新 DBC 文件信息卡片(文件名/报文数/信号数/节点数/大小)。 */
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* 依赖:dbc.js(state/el/parseDbc/rebuildMergedDbc)
|
||||
*/
|
||||
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js?v=20260717t';
|
||||
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js?v=20260717v';
|
||||
|
||||
var LS_KEY = 'dcu_can_session_v1';
|
||||
var MAX_HISTORY = 200;
|
||||
|
||||
+21
-10
@@ -12,12 +12,12 @@
|
||||
* 注意:formatTime 保留本地实现(HH:MM:SS,不含毫秒),与 common/utils.js
|
||||
* 的 formatTime(含毫秒)不同,以保证发送历史时间戳输出格式一致。
|
||||
*/
|
||||
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js?v=20260717t';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717t';
|
||||
import { showToast } from '../common/toast.js?v=20260717t';
|
||||
import { updateBatchUi } from './messages.js?v=20260717t';
|
||||
import { ensureMessageDefaults, getSignalValueMap, getSelectedMessage, updateEncodedPreview, encodeMessageData } from './signals.js?v=20260717t';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717t';
|
||||
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js?v=20260717w';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717w';
|
||||
import { showToast } from '../common/toast.js?v=20260717w';
|
||||
import { updateBatchUi } from './messages.js?v=20260717w';
|
||||
import { ensureMessageDefaults, getSignalValueMap, getSelectedMessage, updateEncodedPreview, encodeMessageData } from './signals.js?v=20260717w';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717w';
|
||||
|
||||
// ==================== 本地工具(保持与原 can.js 一致) ====================
|
||||
function formatTime(date) {
|
||||
@@ -109,7 +109,18 @@ async function sendCanBatchRequest(channel, frames) {
|
||||
if (!json.results || !Array.isArray(json.results)) {
|
||||
throw new Error('批量响应缺少 results');
|
||||
}
|
||||
return json.results;
|
||||
/* 设备中途 fatal 会 skipped 剩余帧(少分配堆);前端补齐等长 results */
|
||||
var results = json.results.slice();
|
||||
var need = frames.length - results.length;
|
||||
if (need > 0) {
|
||||
var pad = {
|
||||
status: 'error',
|
||||
code: (json.abort_code != null) ? json.abort_code : -2,
|
||||
gd32_err: (json.abort_gd32_err != null) ? json.abort_gd32_err : 0
|
||||
};
|
||||
for (var p = 0; p < need; p++) results.push(pad);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ==================== 响应框 ====================
|
||||
@@ -161,12 +172,12 @@ export function renderHistory() {
|
||||
}
|
||||
|
||||
// ==================== 批量发送 ====================
|
||||
/** 单批最多帧数(与设备 /can/send_batch 上限一致) */
|
||||
var BATCH_CHUNK = 48;
|
||||
/** 单批最多帧数(与设备 /can/send_batch 上限 128 对齐;一次 HTTP 连发,避免分块成“一阵一阵”) */
|
||||
var BATCH_CHUNK = 128;
|
||||
|
||||
/**
|
||||
* @description 批量发送所有勾选的报文。
|
||||
* 优先一次 HTTP /can/send_batch 在设备侧紧接连发;失败则回退单帧紧接。
|
||||
* 优先一次 HTTP /can/send_batch(设备侧帧间 5ms);失败则回退单帧(无额外间隔)。
|
||||
* 批量过程中不重绘历史,结束后统一刷新。
|
||||
*/
|
||||
export async function sendCheckedMessages() {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
*
|
||||
* 依赖:dbc.js(state/el/padHex/bytesToHex/cssEscape/findSignal)、common/utils.js(escapeHtml)。
|
||||
*/
|
||||
import { state, el, padHex, bytesToHex, cssEscape, findSignal } from './dbc.js?v=20260717t';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717t';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717t';
|
||||
import { state, el, padHex, bytesToHex, cssEscape, findSignal } from './dbc.js?v=20260717v';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717v';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717v';
|
||||
|
||||
// ==================== 信号值存取 ====================
|
||||
/** @description 确保报文有 signalValues bag,并为每个信号填充默认值。返回 bag。 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* can_monitor.js - CAN 接收监控(独立页 can_monitor.html 与 can.html 右侧抽屉共用)
|
||||
*/
|
||||
import { showToast } from './common/toast.js?v=20260717t';
|
||||
import { showToast } from './common/toast.js?v=20260717v';
|
||||
|
||||
var state = { fbTimer: null, fbAbortCtrl: null };
|
||||
|
||||
|
||||
@@ -72,12 +72,16 @@ export function updateEsp32StagesFromPhase(phase) {
|
||||
if (statusText) statusText.textContent = '待机';
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < phase - 1 && i < 6; i++) {
|
||||
if (all[i]) all[i].className = 'stage-node done';
|
||||
}
|
||||
if (phase >= 1 && phase <= 6) {
|
||||
if (all[phase - 1]) all[phase - 1].className = 'stage-node live';
|
||||
if (details[phase - 1]) details[phase - 1].textContent = esp32PhaseLabels[phase] || '';
|
||||
/* phase=N:前 N-1 步 done;第 N 步进行中 live。
|
||||
* phase=6(启动完成/已完成)是终态:全部 done(绿),最后一步不要 live(蓝)。 */
|
||||
for (var i = 0; i < 6; i++) {
|
||||
if (!all[i]) continue;
|
||||
if (i < phase - 1) {
|
||||
all[i].className = 'stage-node done';
|
||||
} else if (i === phase - 1 && phase >= 1) {
|
||||
all[i].className = (phase >= 6) ? 'stage-node done' : 'stage-node live';
|
||||
if (details[i]) details[i].textContent = esp32PhaseLabels[phase] || '';
|
||||
}
|
||||
}
|
||||
if (statusBar && statusText) {
|
||||
if (phase >= 6) {
|
||||
|
||||
Reference in New Issue
Block a user