diff --git a/README.md b/README.md index 7f30faa..4db1c67 100644 --- a/README.md +++ b/README.md @@ -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 发现 diff --git a/main/core/main.cpp b/main/core/main.cpp index f534d43..6b72a49 100644 --- a/main/core/main.cpp +++ b/main/core/main.cpp @@ -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()); diff --git a/main/include/version.h b/main/include/version.h index f782dfe..67b491d 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -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" diff --git a/main/monitor/health_monitor.cpp b/main/monitor/health_monitor.cpp index 64d4e7b..9e7d6d2 100644 --- a/main/monitor/health_monitor.cpp +++ b/main/monitor/health_monitor.cpp @@ -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 && diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index 313b88f..e8a4d0f 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -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(malloc(static_cast(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(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(cJSON_GetNumberValue(id_item)); uint8_t dlc = dlc_item ? static_cast(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(parseHexToBytes(cJSON_GetStringValue(data_item), - tx_data, CAN_MAX_DLC)); + const char *hex = cJSON_GetStringValue(data_item); + if (hex) { + tx_dlc = static_cast(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(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(resp_len) < sizeof(resp_data)) + ? static_cast(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(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; } diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index b462b2b..65cc9ed 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -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 校准,将重新校准"); + } } } diff --git a/main/transport/canfd_bridge.cpp b/main/transport/canfd_bridge.cpp index 78c73e1..1e92236 100644 --- a/main/transport/canfd_bridge.cpp +++ b/main/transport/canfd_bridge.cpp @@ -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(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; } diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 32c4f0b..799db14 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -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); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index b581ece..1a63beb 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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 diff --git a/tools/can.html b/tools/can.html index 80551e6..90e054a 100644 --- a/tools/can.html +++ b/tools/can.html @@ -4,11 +4,11 @@ ESP32 CAN 通信控制台 - - - - - + + + + +
@@ -318,6 +318,6 @@ 操作成功
- + diff --git a/tools/index.html b/tools/index.html index 042e96b..0c4c716 100644 --- a/tools/index.html +++ b/tools/index.html @@ -9,11 +9,11 @@ ESP32 OTA 中继控制台 - - - - - + + + + +
@@ -425,8 +425,8 @@ 操作成功
- - - + + + diff --git a/tools/js/ble/stages.js b/tools/js/ble/stages.js index 1ede4c8..dbbe8cf 100644 --- a/tools/js/ble/stages.js +++ b/tools/js/ble/stages.js @@ -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 : ''); } } diff --git a/tools/js/can.js b/tools/js/can.js index 6a62f8d..1e50645 100644 --- a/tools/js/can.js +++ b/tools/js/can.js @@ -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), diff --git a/tools/js/can/messages.js b/tools/js/can/messages.js index 7e5df9d..5413264 100644 --- a/tools/js/can/messages.js +++ b/tools/js/can/messages.js @@ -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 文件信息卡片(文件名/报文数/信号数/节点数/大小)。 */ diff --git a/tools/js/can/persist.js b/tools/js/can/persist.js index c6f2283..22d9a69 100644 --- a/tools/js/can/persist.js +++ b/tools/js/can/persist.js @@ -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; diff --git a/tools/js/can/send.js b/tools/js/can/send.js index 26d5290..770afbc 100644 --- a/tools/js/can/send.js +++ b/tools/js/can/send.js @@ -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() { diff --git a/tools/js/can/signals.js b/tools/js/can/signals.js index b46f1ea..111897e 100644 --- a/tools/js/can/signals.js +++ b/tools/js/can/signals.js @@ -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。 */ diff --git a/tools/js/can_monitor.js b/tools/js/can_monitor.js index e5b8446..cb8cf98 100644 --- a/tools/js/can_monitor.js +++ b/tools/js/can_monitor.js @@ -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 }; diff --git a/tools/js/ota/progress.js b/tools/js/ota/progress.js index 7b6b518..dcd1889 100644 --- a/tools/js/ota/progress.js +++ b/tools/js/ota/progress.js @@ -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) {