From ebb3dcdf3873c2fd487c27a2e7cd6388d376ae09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Tue, 1 Sep 2026 02:49:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(debug=5Flog):=20=E6=96=B0=E5=A2=9EUART0=20?= =?UTF-8?q?RX=E6=B3=B5=E4=BB=BB=E5=8A=A1=E6=94=AF=E6=8C=81=E4=B8=B2?= =?UTF-8?q?=E5=8F=A3=E6=97=A5=E5=BF=97=E8=BD=AC=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增UART0 RX泵任务功能,将GPIO20接收到的串口数据按切行、剥离后以tag=UART推入ring buffer,通过HTTP /logs接口显示在网页日志面板。 接线方式:任意串口源TX连接至H1/H2的ESP_RX引脚,波特率3M 8N1。 feat(motor_ota): GD32 OTA增加包级防呆机制防止错误固件刷入 中继触发前校验gd32_ota镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity), 对Bootloader包或非APP包在WiFi /ota/relay与BLE 0xF7均拒绝中继, 避免GD32 APP区被错误包擦除。新增0x87完成码用于固件身份验证失败场景。 feat(motor_state): 扩展CAN状态数据结构支持CONFIG负载 扩展motor_slot_t结构体,新增config_rx_count、config_seq和config_data字段 用于存储CONFIG帧的相关信息。CAN状态数据包大小从44B扩展到76B, 页面总数从6调整为9,支持目标值回报的配置数据传输。 fix(motor_ota): 修复OTA取消流程中的信用度管理和任务清理问题 ★ 先复位credit再发送CANCEL命令,防止传输卡住时背压导致CANCEL被拒绝。 延长取消等待时间至10s覆盖最坏情况,确保feedTask正常退出而非强制删除。 取消完成后复位credit防止锁存死锁,确保后续通信正常进行。 fix(http_server): 修复HTTP请求处理中的边界检查和安全验证 拒绝chunked请求(content_len<=0)防止无限循环,添加通道参数范围验证(ch 0~2), 在OTA中继触发前验证固件身份防止错误包刷入,使用http_json_append避免缓冲区越界。 refactor(gd32_flash): 实现GD32固件身份验证算法确保刷写安全 基于Cortex-M向量表内容实现包级身份校验,通过MSP和复位向量位置 区分APP包、Bootloader包和无效固件,为OTA安全提供基础保障。 BREAKING CHANGE: 电机状态API数据格式变更,CAN通道参数验证更严格 --- .codex/config.toml | 4 - main/include/version.h | 4 +- main/monitor/debug_log.cpp | 38 + main/monitor/motor_ota_relay.cpp | 67 +- main/monitor/motor_state.cpp | 3 + main/monitor/motor_state.h | 12 +- main/ota/ota_coordinator.cpp | 10 + main/server/ble_ota_host.cpp | 19 + main/server/ble_ota_priv.h | 1 + main/server/http_handlers.cpp | 99 +- main/storage/gd32_flash_store.cpp | 53 + main/storage/gd32_flash_store.h | 26 + main/transport/spi_master.cpp | 108 +- main/transport/spi_master.h | 8 + tools/build_mem.bat | 23 - tools/can.html | 334 --- tools/can_monitor.html | 189 -- tools/continuity_test.html | 737 ------ tools/css/base.css | 135 -- tools/css/can.css | 1712 -------------- tools/css/components.css | 944 -------- tools/css/layout.css | 337 --- tools/css/motor.css | 769 ------- tools/css/responsive.css | 248 -- tools/index.html | 649 ------ tools/js/app.js | 293 --- tools/js/ble/constants.js | 59 - tools/js/ble/file.js | 225 -- tools/js/ble/gatt.js | 543 ----- tools/js/ble/session.js | 160 -- tools/js/ble/stages.js | 123 - tools/js/ble/state.js | 198 -- tools/js/ble/transfer.js | 496 ---- tools/js/ble/verify.js | 26 - tools/js/ble_ota.js | 43 - tools/js/can.js | 420 ---- tools/js/can/dbc.js | 350 --- tools/js/can/messages.js | 390 ---- tools/js/can/persist.js | 222 -- tools/js/can/send.js | 504 ---- tools/js/can/signals.js | 434 ---- tools/js/can_monitor.js | 350 --- tools/js/common/base-url.js | 34 - tools/js/common/connection.js | 1167 ---------- tools/js/common/heap-low-history.js | 196 -- tools/js/common/log.js | 113 - tools/js/common/polling.js | 418 ---- tools/js/common/toast.js | 44 - tools/js/common/utils.js | 60 - tools/js/motor/main.js | 3306 --------------------------- tools/js/motor/tick.worker.js | 33 - tools/js/ota/console-mode.js | 320 --- tools/js/ota/file-handle.js | 664 ------ tools/js/ota/progress.js | 257 --- tools/js/ota/session.js | 250 -- tools/js/ota/stages.js | 426 ---- tools/js/ota/upload-esp32.js | 614 ----- tools/js/ota/upload-gd32.js | 211 -- tools/motor.html | 372 --- tools/serve.bat | 22 - tools/spi_test.html | 175 -- 国风歌词.md | 59 + 62 files changed, 462 insertions(+), 19644 deletions(-) delete mode 100644 .codex/config.toml delete mode 100644 tools/build_mem.bat delete mode 100644 tools/can.html delete mode 100644 tools/can_monitor.html delete mode 100644 tools/continuity_test.html delete mode 100644 tools/css/base.css delete mode 100644 tools/css/can.css delete mode 100644 tools/css/components.css delete mode 100644 tools/css/layout.css delete mode 100644 tools/css/motor.css delete mode 100644 tools/css/responsive.css delete mode 100644 tools/index.html delete mode 100644 tools/js/app.js delete mode 100644 tools/js/ble/constants.js delete mode 100644 tools/js/ble/file.js delete mode 100644 tools/js/ble/gatt.js delete mode 100644 tools/js/ble/session.js delete mode 100644 tools/js/ble/stages.js delete mode 100644 tools/js/ble/state.js delete mode 100644 tools/js/ble/transfer.js delete mode 100644 tools/js/ble/verify.js delete mode 100644 tools/js/ble_ota.js delete mode 100644 tools/js/can.js delete mode 100644 tools/js/can/dbc.js delete mode 100644 tools/js/can/messages.js delete mode 100644 tools/js/can/persist.js delete mode 100644 tools/js/can/send.js delete mode 100644 tools/js/can/signals.js delete mode 100644 tools/js/can_monitor.js delete mode 100644 tools/js/common/base-url.js delete mode 100644 tools/js/common/connection.js delete mode 100644 tools/js/common/heap-low-history.js delete mode 100644 tools/js/common/log.js delete mode 100644 tools/js/common/polling.js delete mode 100644 tools/js/common/toast.js delete mode 100644 tools/js/common/utils.js delete mode 100644 tools/js/motor/main.js delete mode 100644 tools/js/motor/tick.worker.js delete mode 100644 tools/js/ota/console-mode.js delete mode 100644 tools/js/ota/file-handle.js delete mode 100644 tools/js/ota/progress.js delete mode 100644 tools/js/ota/session.js delete mode 100644 tools/js/ota/stages.js delete mode 100644 tools/js/ota/upload-esp32.js delete mode 100644 tools/js/ota/upload-gd32.js delete mode 100644 tools/motor.html delete mode 100644 tools/serve.bat delete mode 100644 tools/spi_test.html create mode 100644 国风歌词.md diff --git a/.codex/config.toml b/.codex/config.toml deleted file mode 100644 index 9ac7c6e..0000000 --- a/.codex/config.toml +++ /dev/null @@ -1,4 +0,0 @@ -sandbox_mode = "workspace-write" - -[sandbox_workspace_write] -network_access = true diff --git a/main/include/version.h b/main/include/version.h index d35d2e1..321d1c3 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -14,8 +14,8 @@ // 固件版本 #define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 3 -#define FW_VERSION_PATCH 78 -#define FW_VERSION_STRING "0.3.78" // 版本查询checkVersionWait移到tickInFlight释放后异步触发,避免/motor/version请求拖长tick导致tickStuck误报通道闪黄 +#define FW_VERSION_PATCH 82 +#define FW_VERSION_STRING "0.3.82" // DebugLog 新增 UART0 RX 泵任务(GPIO20, 3M 8N1):收到的串口行按 \n 切行、剥 \r,以 tag=UART 推入 ring → HTTP /logs 出现在网页日志面板;接线=任意串口源 TX→H1/H2 的 ESP_RX。0.3.81 GD32 OTA 包级防呆:中继触发前校验 gd32_ota 镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity),BOOT 包/非 APP 包在 WiFi /ota/relay 与 BLE 0xF7 均拒绝,GD32 APP 区不再被错误包擦除;BLE 新增 0x87 完成码 // Git 信息(手动填写) #define GIT_HASH "770ae14" \ No newline at end of file diff --git a/main/monitor/debug_log.cpp b/main/monitor/debug_log.cpp index ddba53a..0b86570 100644 --- a/main/monitor/debug_log.cpp +++ b/main/monitor/debug_log.cpp @@ -503,6 +503,35 @@ esp_err_t DebugLog::uartInit() return ESP_OK; } +// ============================================================ +// UART0 RX 泵:把 RX(GPIO20) 收到的串口行转发进 ring buffer, +// 经 HTTP /logs 出现在网页日志面板(tag=UART)。 +// 接线:H1/H2 排针上任意串口源 TX → ESP_RX(GPIO20),3M 8N1。 +// ============================================================ +static void uartRxPumpTask(void *arg) +{ + (void)arg; + uint8_t chunk[64]; + char line[128]; + int pos = 0; + + for (;;) { + const int n = uart_read_bytes(UART_NUM_0, chunk, sizeof(chunk), portMAX_DELAY); + for (int i = 0; i < n; i++) { + const char c = (char)chunk[i]; + if (c == '\n' || pos >= (int)sizeof(line) - 1) { + if (pos > 0) { + line[pos] = '\0'; + DebugLog::instance().log(DEBUG_LOG_INFO, "UART", "%s", line); + pos = 0; + } + } else if (c != '\r') { + line[pos++] = c; + } + } + } +} + esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len) { if (!m_uartInitialized || !m_uartAvailable) { @@ -558,6 +587,15 @@ esp_err_t DebugLog::init() m_debugAddr.sin_port = htons(DEBUG_LOG_PORT); m_debugAddr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP); + /* UART0 RX 泵:串口行 → ring → /logs 网页面板 */ + if (m_uartAvailable) { + if (xTaskCreate(uartRxPumpTask, "uart_rx", 3072, nullptr, 1, nullptr) == pdPASS) { + ESP_LOGI(TAG, "UART0 RX 泵已启动 (GPIO%d → /logs)", UART_DEBUG_RX_GPIO); + } else { + ESP_LOGW(TAG, "UART0 RX 泵任务创建失败(堆不足?)"); + } + } + m_initialized = true; ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT); return ESP_OK; diff --git a/main/monitor/motor_ota_relay.cpp b/main/monitor/motor_ota_relay.cpp index b71f4d9..b05b7eb 100644 --- a/main/monitor/motor_ota_relay.cpp +++ b/main/monitor/motor_ota_relay.cpp @@ -288,13 +288,16 @@ const uint32_t remaining = s_fw_size - s_fw_offset; /* feedTask 字节喂完 ≠ GD32/电机已完成。suppress 期间 0xD2 被丢; * 末包 CAN ACK / 跳转 APP 还要数百 ms。只 QUERY 一次会把前端卡在 kTransfer。 - * 退出后轮询 QUERY,直到 GD32 进 FINISHED/ERROR/IDLE,或最多 ~8s。 */ + * 退出后轮询 QUERY,直到 GD32 进 FINISHED/ERROR/IDLE,或最多 ~8s。 + * 用户取消(s_feed_running 已被 motorOtaCancel 置 false)时跳过: + * 设备端已进 IDLE、GD32 已 resetToIdle,无终态可等——立即收尾退出, + * 把 cancel task 的等待窗口压到最短(也消除强删窗口)。 */ { constexpr uint32_t kExitPollMs = 8000U; constexpr uint32_t kExitGapMs = 150U; const uint32_t exit_deadline = nowMs() + kExitPollMs; bool got_terminal = false; - while (nowMs() < exit_deadline) { + while (s_feed_running && nowMs() < exit_deadline) { /* 优先吃 ring 推送(已恢复 suppress) */ { const motor_ota_status_t st = motorOtaGetStatus(); @@ -395,6 +398,25 @@ int motorOtaStart(uint32_t fw_size, uint8_t channel) &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 300U); /* 300ms: fire-and-forget,让 worker 有足够时间完成 START。 * 原 500ms 偏保守;GD32 收到 START 立即进 kPreCancel,<50ms 响应。 */ + if (ret == ESP_OK && resp_type != kRspAck && resp_len >= 1U && + resp_payload[0] == 0xE5U) { + /* 0xE5 = GD32 端 relay 会话残留(上次 CANCEL 丢失/未达):不清会直接 + * 让本次 OTA 空转失败。补发 CANCEL(GD32 resetToIdle 归 kIdle)后 + * 重试一次 START,一次自愈即恢复,无需用户重试或刷新。 */ + DEBUG_WARN(TAG, "START 0xE5(GD32 残留会话)→ 补发 CANCEL 后重试 START"); + uint8_t ccmd[1] = {1U}; + uint8_t crt = 0; + uint8_t crp[4] = {0}; + size_t crl = 0; + SpiMaster::instance().resetCredit(); /* 解除背压,保证 CANCEL 发出 */ + (void)SpiMaster::instance().transact( + kSpiMotorOtaCmd, ccmd, 1, &crt, crp, sizeof(crp), &crl, 500U); + SpiMaster::instance().resetCredit(); /* GD32 已 resetToIdle,窗口对齐满 */ + ret = SpiMaster::instance().transact( + kSpiMotorOtaCmd, cmd, 6, + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, + 300U); + } if (ret != ESP_OK || resp_type != kRspAck) { DEBUG_WARN(TAG, "START SPI 未确认 ACK (ret=%s resp=0x%02X)——fire-and-forget 继续", esp_err_to_name(ret), resp_type); @@ -441,7 +463,7 @@ void motorOtaCancel(void) } /* 通知 GD32 取消 relay 状态机 + 等 feedTask 退出是耗时操作 - *(SPI transact 最多 1s + feedTask 退出最多 2s),不能在 httpd 线程 + *(SPI transact 最多 1s + feedTask 退出最长 ~8.2s),不能在 httpd 线程 * 同步阻塞,否则 cancel HTTP 请求本身会超时("Failed to fetch"), * 且 httpd 单线程会饿死其他端点。 * 用独立 task 异步执行,HTTP handler 立即返回。 */ @@ -453,7 +475,12 @@ void motorOtaCancel(void) } auto do_cancel = [](void *) { - /* 通知 GD32 取消 relay 状态机 */ + /* ★ 先复位 credit 再发 CANCEL:取消的典型时机恰是传输卡住(GD32 缓冲满, + * 最后一帧 RESPONSE credit=0 已锁存进 m_remoteCredit)——不先解除背压, + * CANCEL 自己会被 CREDIT 背压 refuse,GD32 端 relay 会话收不到取消而残留, + * 下次 START 被 GD32 0xE5 拒 → "取消后重新开始没法开始"。 + * GD32 收到 CANCEL 后 resetToIdle 恢复满窗口,与复位值对齐。 */ + SpiMaster::instance().resetCredit(); uint8_t cmd[1] = {1U}; /* cmd=CANCEL */ uint8_t resp_type = 0; uint8_t resp_payload[4]; @@ -462,22 +489,43 @@ void motorOtaCancel(void) kSpiMotorOtaCmd, cmd, 1, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000U); + if (ret != ESP_OK) { + /* 重试一次:再复位 credit(防首次期间又被失配帧锁 0)后重发 */ + vTaskDelay(pdMS_TO_TICKS(50)); + SpiMaster::instance().resetCredit(); + ret = SpiMaster::instance().transact( + kSpiMotorOtaCmd, cmd, 1, + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, + 1000U); + } DEBUG_WARN(TAG, "cancel: GD32 resp ret=%s type=0x%02X", esp_err_to_name(ret), resp_type); - /* 等待 feedTask 真正退出(最多 2s),避免 s_feed_running 已清但 task - * 仍卡在 SpiMaster::transact 阻塞中,导致 motorOtaIsActive() 瞬时仍 true。 */ + /* 等待 feedTask 真正退出。feedTask 最坏存活:主循环收尾 ≤0.2s + + * 退出轮询 8s ≈ 8.2s(120s 整体超时路径同样有界),等待上限取 10s + * 覆盖之——正常路径 feedTask 因 s_status 已被置 IDLE 在 ~0.3s 内退出, + * 这里只是兜底;尽量不强删(强删会泄漏 job 槽 + worker 悬垂栈写)。 */ TaskHandle_t t = s_feed_task; if (t) { - for (int i = 0; i < 200 && s_feed_task != nullptr; i++) { + for (int i = 0; i < 1000 && s_feed_task != nullptr; i++) { vTaskDelay(pdMS_TO_TICKS(10)); } if (s_feed_task != nullptr) { - DEBUG_ERR(TAG, "cancel: feedTask 未在 2s 内退出,强制删除"); + DEBUG_ERR(TAG, "cancel: feedTask 未在 10s 内退出,强制删除"); vTaskDelete(s_feed_task); s_feed_task = nullptr; + /* 强删绕过了 feedTask 收尾,suppress 必须在此恢复, + * 否则 ring service 永久关闭 → GD32 推送全部收不到。 */ + SpiMaster::instance().setSuppressRingService(false); } } s_feed_running = false; /* 双保险 */ + /* credit 复位:取消的典型时机是传输卡住(GD32 缓冲满)——最后一帧 + * RESPONSE 的 credit=0(session 合法)被锁存进 m_remoteCredit。 + * GD32 resetToIdle 后转空闲不再发帧,relay 又跑在 APP(不重启、 + * 无 session=0 帧)→ 没有任何 resync 入口能把 credit 送回来, + * 后续所有 transact 被 CREDIT 背压拒绝 → 网页所有消息收不到。 + * GD32 端 clearOtaSessionQueues 已把接收窗口恢复满,此处对齐复位。 */ + SpiMaster::instance().resetCredit(); cancel_task = nullptr; vTaskDelete(NULL); }; @@ -487,6 +535,7 @@ void motorOtaCancel(void) DEBUG_ERR(TAG, "cancel: failed to create cancel task, doing sync cancel"); cancel_task = nullptr; /* 降级:同步执行(极端 OOM 情况) */ + SpiMaster::instance().resetCredit(); /* 先解除背压,保证 CANCEL 能发出 */ uint8_t cmd[1] = {1U}; uint8_t resp_type = 0; uint8_t resp_payload[4]; @@ -503,9 +552,11 @@ void motorOtaCancel(void) if (s_feed_task != nullptr) { vTaskDelete(s_feed_task); s_feed_task = nullptr; + SpiMaster::instance().setSuppressRingService(false); } } s_feed_running = false; + SpiMaster::instance().resetCredit(); /* 同 do_cancel:防 credit=0 锁存死锁 */ } DEBUG_WARN(TAG, "cancel: dispatched feed_running=%d", (int)s_feed_running); } diff --git a/main/monitor/motor_state.cpp b/main/monitor/motor_state.cpp index a33abc1..349c6be 100644 --- a/main/monitor/motor_state.cpp +++ b/main/monitor/motor_state.cpp @@ -65,6 +65,9 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len) slot.rx_count = rdU32Le(&e[4]); slot.seq = rdU32Le(&e[8]); memcpy(slot.data, &e[12], MOTOR_STATUS_DLC); + slot.config_rx_count = rdU32Le(&e[44]); + slot.config_seq = rdU32Le(&e[48]); + memcpy(slot.config_data, &e[52], MOTOR_CONFIG_DLC); offset += MOTOR_ENTRY_SIZE; } diff --git a/main/monitor/motor_state.h b/main/monitor/motor_state.h index ffb681b..d851245 100644 --- a/main/monitor/motor_state.h +++ b/main/monitor/motor_state.h @@ -6,10 +6,12 @@ * * payload 格式(每页): * [0] page, [1] total_pages, [2] entry_count, [3] active_count - * [4..] 每个 entry 44B: + * [4..] 每个 entry 76B: * [0] node_id, [1] channel, [2] online, [3] dlc, * [4..7] rx_count (LE), [8..11] seq (LE), * [12..43] data[32] (原始 STATUS payload) + * [44..47] config_rx_count (LE), [48..51] config_seq (LE), + * [52..75] config_data[24] (原始 CONFIG payload, 目标值回报) * * 同一节点号可能同时挂在多个 CAN 通道(两台电机同号异线), * 缓存表与 GD32 端一致按 node×channel 双键存储(27 槽)。 @@ -29,9 +31,10 @@ extern "C" { #define MOTOR_CHANNELS 3 #define MOTOR_MAX_SLOTS (MOTOR_MAX_NODES * MOTOR_CHANNELS) #define MOTOR_STATUS_DLC 32 -#define MOTOR_ENTRY_SIZE 44 +#define MOTOR_CONFIG_DLC 24 +#define MOTOR_ENTRY_SIZE 76 #define MOTOR_PAGE_HEADER 4 -#define MOTOR_TOTAL_PAGES 6 +#define MOTOR_TOTAL_PAGES 9 /** @brief 单电机状态快照。 */ typedef struct { @@ -42,6 +45,9 @@ typedef struct { uint32_t rx_count; ///< 累计收到帧数 uint32_t seq; ///< GD32 侧更新序号 uint8_t data[MOTOR_STATUS_DLC]; ///< 原始 STATUS payload + uint32_t config_rx_count; ///< 累计收到 CONFIG 帧数 + uint32_t config_seq; ///< CONFIG 更新序号 + uint8_t config_data[MOTOR_CONFIG_DLC]; ///< 原始 CONFIG payload (目标值回报) } motor_slot_t; /** @brief 全局电机状态表。 */ diff --git a/main/ota/ota_coordinator.cpp b/main/ota/ota_coordinator.cpp index 0318ab5..cd2e696 100644 --- a/main/ota/ota_coordinator.cpp +++ b/main/ota/ota_coordinator.cpp @@ -310,6 +310,16 @@ esp_err_t OtaCoordinator::triggerGd32Relay() return ESP_ERR_INVALID_STATE; } + /* ★ 包级防呆:SHA 匹配只证明"传对了",不证明"传的是对的包"。 + * 把 BOOT 包中继进 APP 区会擦掉好固件,触发前必须确认镜像身份为 APP。 + * 返回 ESP_ERR_INVALID_SIZE 供调用方(WiFi/BLE)映射成具体错误提示。 */ + const Gd32ImageIdentity identity = Gd32FlashStore::instance().validateStoredImageIdentity(); + if (identity != Gd32ImageIdentity::kApp) { + DEBUG_ERR(TAG, "拒绝中继:GD32 固件身份校验失败 (identity=%d)", (int)identity); + unlock(h); + return ESP_ERR_INVALID_SIZE; + } + if (OtaRelay::getState() != RELAY_IDLE && OtaRelay::getState() != RELAY_DONE && OtaRelay::getState() != RELAY_ERROR) { diff --git a/main/server/ble_ota_host.cpp b/main/server/ble_ota_host.cpp index 58fd7c9..8ac8250 100644 --- a/main/server/ble_ota_host.cpp +++ b/main/server/ble_ota_host.cpp @@ -76,6 +76,18 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, DEBUG_INFO(TAG, "GD32就绪 SHA匹配 %" PRIu32 "/%" PRIu32 ",触发中继", meta.stored_size, meta.total_size); + + /* ★ 包级防呆:SHA 匹配只证明传输无误。BOOT 包/非 APP 包在此拒绝, + * 不进入 SPI 中继,GD32 APP 区不会被擦除。 */ + const Gd32ImageIdentity identity = Gd32FlashStore::instance().validateStoredImageIdentity(); + if (identity != Gd32ImageIdentity::kApp) { + DEBUG_ERR(TAG, "GD32 固件身份校验失败 identity=%d,拒中继", (int)identity); + sendFinishEvent(BLE_OTA_FINISH_ERR_FW_IDENTITY, + packBleFinishValue(session_id, + (identity == Gd32ImageIdentity::kBootloader) ? 1 : 2)); + return 0; + } + if (OtaRelay::getResultQueue()) { uint32_t stale = 0; while (xQueueReceive(OtaRelay::getResultQueue(), &stale, 0) == pdTRUE) { @@ -83,6 +95,13 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, } } esp_err_t trig_err = OtaCoordinator::instance().triggerGd32Relay(); + if (trig_err == ESP_ERR_INVALID_SIZE) { + /* 兜底:triggerGd32Relay 内部身份校验拒绝(与上方同判据) */ + DEBUG_ERR(TAG, "GD32 固件身份校验失败(trigger 兜底),拒中继"); + sendFinishEvent(BLE_OTA_FINISH_ERR_FW_IDENTITY, + packBleFinishValue(session_id, 2)); + return 0; + } if (trig_err != ESP_OK) { DEBUG_ERR(TAG, "GD32 中继触发失败: %s", esp_err_to_name(trig_err)); sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY, diff --git a/main/server/ble_ota_priv.h b/main/server/ble_ota_priv.h index 3fbdd29..ca9bc2a 100644 --- a/main/server/ble_ota_priv.h +++ b/main/server/ble_ota_priv.h @@ -57,6 +57,7 @@ #define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 #define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85 #define BLE_OTA_FINISH_ERR_GD32_RELAY 0x86 +#define BLE_OTA_FINISH_ERR_FW_IDENTITY 0x87 ///< 包身份不符(疑似 Bootloader/非 APP 包),拒绝中继 struct BleOtaDataBatch { uint8_t *data; diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index 03370e1..963d02a 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -230,6 +230,15 @@ esp_err_t ota_upload_handler(httpd_req_t *req) { http_set_cors_headers(req); + /* chunked 请求(无 Content-Length 头)时 IDF 置 content_len=-1: + * ESP32 分支无符号比较使收流循环失去长度上界,GD32 分支则一字节 + * 不写却返回 progress:100 假成功——必须前置拒绝 */ + if (req->content_len <= 0) { + return http_send_json_error(req, "400 Bad Request", + "bad_content_len", + "missing or invalid Content-Length"); + } + uint8_t *recv_buf = s_ota_upload_recv; DEBUG_WARN("WIFI", "POST /ota/upload clen=%d free=%lu big=%lu", req->content_len, @@ -580,7 +589,27 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req) "sha_compute_failed", "SHA compute failed"); } + /* ★ 包级防呆:SHA 匹配只证明传输无误,不证明包内容正确。 + * 上传 BOOT 包(或其他非 APP 包)时在此拒绝,GD32 不会被擦除 APP 区。 */ + const Gd32ImageIdentity identity = Gd32FlashStore::instance().validateStoredImageIdentity(); + if (identity != Gd32ImageIdentity::kApp) { + (void)BleOta::instance().resumeStack(); + const char *msg = (identity == Gd32ImageIdentity::kBootloader) + ? "检测到 Bootloader 固件(复位向量在 0x08000000 区),已拒绝刷入 APP 区,请选择 APP 固件" + : (identity == Gd32ImageIdentity::kInvalid) + ? "固件向量表非法,疑似非 GD32 APP 固件,已拒绝刷写" + : "固件存储读取失败,无法校验身份,请重新上传"; + DEBUG_ERR("OTA", "GD32 固件身份校验失败 identity=%d,拒绝中继", (int)identity); + return http_send_json_error(req, "400 Bad Request", "firmware_identity_mismatch", msg); + } + esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay(); + if (trigger_err == ESP_ERR_INVALID_SIZE) { + /* 兜底:triggerGd32Relay 内部身份校验拒绝(与上方同判据,正常不会走到) */ + (void)BleOta::instance().resumeStack(); + return http_send_json_error(req, "400 Bad Request", "firmware_identity_mismatch", + "固件身份校验失败,已拒绝刷写"); + } if (trigger_err != ESP_OK) { (void)BleOta::instance().resumeStack(); return http_send_json_error(req, "400 Bad Request", @@ -1145,6 +1174,9 @@ esp_err_t can_send_batch_handler(httpd_req_t *req) return http_send_json_error(req, "400 Bad Request", "bad_args", "ch 字段格式错误"); } long ch_val = readInt(); + if (ch_val < 0 || ch_val >= CAN_CHANNEL_MAX) { + return http_send_json_error(req, "400 Bad Request", "bad_args", "ch 非法(0~2)"); + } if (!findKey("\"frames\"")) { return http_send_json_error(req, "400 Bad Request", "bad_args", "缺少 frames 字段"); } @@ -2184,8 +2216,9 @@ esp_err_t motor_state_handler(httpd_req_t *req) * count u8 (有效 entry 数) * rsvd u16 = 0 * Entry: node u8, ch u8, online u8, dlc u8, - * rx_count u32, seq u32, data[32] - * 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*44 = 1200B。 + * rx_count u32, seq u32, data[32], + * config_rx_count u32, config_seq u32, config_data[24] + * 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*76 = 2064B。 * 无 have_data 时仍发 header(count=0),让前端区分「无数据」与「空总线」。 */ bool want_bin = http_query_bool(req, "bin"); if (want_bin) { @@ -2234,6 +2267,21 @@ esp_err_t motor_state_handler(httpd_req_t *req) buf[off++] = (uint8_t)((sq >> 24) & 0xFF); memcpy(&buf[off], s.data, MOTOR_STATUS_DLC); off += MOTOR_STATUS_DLC; + /* config_rx_count LE */ + uint32_t cr = s.config_rx_count; + buf[off++] = (uint8_t)(cr & 0xFF); + buf[off++] = (uint8_t)((cr >> 8) & 0xFF); + buf[off++] = (uint8_t)((cr >> 16) & 0xFF); + buf[off++] = (uint8_t)((cr >> 24) & 0xFF); + /* config_seq LE */ + uint32_t csq = s.config_seq; + buf[off++] = (uint8_t)(csq & 0xFF); + buf[off++] = (uint8_t)((csq >> 8) & 0xFF); + buf[off++] = (uint8_t)((csq >> 16) & 0xFF); + buf[off++] = (uint8_t)((csq >> 24) & 0xFF); + /* config_data[24] */ + memcpy(&buf[off], s.config_data, MOTOR_CONFIG_DLC); + off += MOTOR_CONFIG_DLC; count++; } } @@ -2257,7 +2305,9 @@ esp_err_t motor_state_handler(httpd_req_t *req) } char *w = s_http_json_work; - int off = snprintf(w, HTTP_JSON_WORK_CAP, + /* http_json_append 截断时钳制在 cap-1;snprintf 截断返回期望长度, + * off += snprintf 会越过 cap 导致 resp_send 越界读 */ + int off = http_json_append(w, HTTP_JSON_WORK_CAP, 0, "{\"have_data\":true,\"active_count\":%u,\"boot_active\":%s,\"age_ms\":%lu,\"motors\":[", (unsigned)tbl.active_count, tbl.boot_active ? "true" : "false", (unsigned long)age); @@ -2269,22 +2319,28 @@ esp_err_t motor_state_handler(httpd_req_t *req) /* data hex */ char hex[MOTOR_STATUS_DLC * 2 + 1]; http_bytes_to_hex_upper(s.data, MOTOR_STATUS_DLC, hex); + char chex[MOTOR_CONFIG_DLC * 2 + 1]; + http_bytes_to_hex_upper(s.config_data, MOTOR_CONFIG_DLC, chex); - off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, + off = http_json_append(w, HTTP_JSON_WORK_CAP, off, "%s{\"node\":%u,\"ch\":%u,\"online\":%s,\"dlc\":%u," - "\"rx_count\":%lu,\"seq\":%lu,\"data\":\"%s\"}", + "\"rx_count\":%lu,\"seq\":%lu,\"data\":\"%s\"," + "\"config_rx_count\":%lu,\"config_seq\":%lu,\"config_data\":\"%s\"}", first ? "" : ",", (unsigned)s.node_id, (unsigned)s.channel, s.online ? "true" : "false", (unsigned)s.dlc, (unsigned long)s.rx_count, (unsigned long)s.seq, - hex); + hex, + (unsigned long)s.config_rx_count, + (unsigned long)s.config_seq, + chex); first = false; } - if (off > 0 && (size_t)off < HTTP_JSON_WORK_CAP - 2) { - off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, "]}"); + if (off > 0) { + off = http_json_append(w, HTTP_JSON_WORK_CAP, off, "]}"); } esp_err_t ret = httpd_resp_send(req, w, off > 0 ? off : 0); @@ -2325,7 +2381,8 @@ esp_err_t motor_version_handler(httpd_req_t *req) const int64_t now_ms = esp_timer_get_time() / 1000; char *w = s_http_json_work; - int off = snprintf(w, HTTP_JSON_WORK_CAP, "{\"have_data\":true,\"nodes\":["); + int off = http_json_append(w, HTTP_JSON_WORK_CAP, 0, + "{\"have_data\":true,\"nodes\":["); bool first = true; for (uint8_t i = 0; i < MOTORVER_MAX_NODES && off > 0 && (size_t)off < HTTP_JSON_WORK_CAP - 128; ++i) { @@ -2337,7 +2394,7 @@ esp_err_t motor_version_handler(httpd_req_t *req) const long age = (s.recv_ms > 0 && now_ms >= s.recv_ms) ? static_cast(now_ms - s.recv_ms) : 0L; - off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, + off = http_json_append(w, HTTP_JSON_WORK_CAP, off, "%s{\"node\":%u,\"seq\":%lu,\"age_ms\":%ld,\"version\":\"%s\"}", first ? "" : ",", (unsigned)s.node_id, @@ -2347,8 +2404,8 @@ esp_err_t motor_version_handler(httpd_req_t *req) first = false; } - if (off > 0 && (size_t)off < HTTP_JSON_WORK_CAP - 2) { - off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, "]}"); + if (off > 0) { + off = http_json_append(w, HTTP_JSON_WORK_CAP, off, "]}"); } esp_err_t ret = httpd_resp_send(req, w, off > 0 ? off : 0); @@ -2364,7 +2421,14 @@ esp_err_t motor_ota_upload_handler(httpd_req_t *req) { http_set_cors_headers(req); - uint8_t channel = static_cast(http_query_u32(req, "ch", 0)); + const uint32_t ch_q = http_query_u32(req, "ch", 0); + if (ch_q >= CAN_CHANNEL_MAX) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"error\":\"bad channel (0~2)\"}"); + return ESP_FAIL; + } + uint8_t channel = static_cast(ch_q); const int content_len = req->content_len; if (content_len <= 0 || content_len > (int)MOTOR_OTA_MAX_FW_SIZE) { @@ -2480,7 +2544,14 @@ esp_err_t motor_ota_relay_handler(httpd_req_t *req) http_set_keepalive_hdr(req); DEBUG_INFO("MOTOR_OTA", "=== HTTP: POST /motor/ota/relay ==="); - uint8_t channel = static_cast(http_query_u32(req, "ch", 0)); + const uint32_t ch_q = http_query_u32(req, "ch", 0); + if (ch_q >= CAN_CHANNEL_MAX) { + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_sendstr(req, "{\"error\":\"bad channel (0~2)\"}"); + return ESP_FAIL; + } + uint8_t channel = static_cast(ch_q); /* 读 meta 拿 fw_size(motorOtaStart 参数的唯一来源)。 * total_size==0 = 无固件可刷(未上传过,或上次 relay 成功后 metaClear 已消费)。 diff --git a/main/storage/gd32_flash_store.cpp b/main/storage/gd32_flash_store.cpp index af9474e..be61941 100644 --- a/main/storage/gd32_flash_store.cpp +++ b/main/storage/gd32_flash_store.cpp @@ -619,3 +619,56 @@ esp_err_t Gd32FlashStore::verifyFrontendSha(const uint8_t frontend_sha32[32], } return ESP_OK; } + +// ============================================================ +// 包级身份校验(向量表内容判定) +// ============================================================ + +/* GD32E508 Flash 布局常量,与 GD32E50X_BOOT sys/config.h 对齐: + * BOOT 0x08000000 ~ 0x08011FFF(72KB) + * APP 0x08048000 ~ 0x0807DFFB(APPLATEST_START + APP_MAX_SIZE,216KB-4) + * SRAM 0x20000000 ~ 0x2001FFFF(128KB) + * APP 固件链接基址 0x08048000、BOOT 链接基址 0x08000000, + * 两者向量表第二字(Reset_Handler)必然落在各自区域内,据此区分包类型。 + * 注意:BOOT 侧 validateApplication() 使用同一判据,修改需两边同步。 */ +static constexpr uint32_t kGd32BootRegionStart = 0x08000000U; +static constexpr uint32_t kGd32BootRegionEnd = 0x08012000U; +static constexpr uint32_t kGd32AppRegionStart = 0x08048000U; +static constexpr uint32_t kGd32AppRegionEnd = 0x08048000U + 0x00035FFCU; +static constexpr uint32_t kGd32SramStart = 0x20000000U; +static constexpr uint32_t kGd32SramEnd = 0x20020000U; + +static inline uint32_t rd_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +Gd32ImageIdentity Gd32FlashStore::validateStoredImageIdentity() +{ + uint8_t vec[8] = {0}; + if (flashReadChunk(0, vec, sizeof(vec)) != ESP_OK) { + DEBUG_ERR("OTA", "身份校验:读取向量表失败"); + return Gd32ImageIdentity::kReadFailed; + } + + const uint32_t msp = rd_le32(&vec[0]); + const uint32_t reset = rd_le32(&vec[4]) & ~1U; // 清除 Thumb 位 + + const bool msp_valid = (msp >= kGd32SramStart && msp < kGd32SramEnd); + const bool reset_is_app = (reset >= kGd32AppRegionStart && reset < kGd32AppRegionEnd); + const bool reset_is_boot = (reset >= kGd32BootRegionStart && reset < kGd32BootRegionEnd); + + if (msp_valid && reset_is_app) { + return Gd32ImageIdentity::kApp; + } + + if (reset_is_boot) { + DEBUG_ERR("OTA", "身份校验:检测到 Bootloader 包 (MSP=0x%08lX RH=0x%08lX),禁止刷入 APP 区", + (unsigned long)msp, (unsigned long)reset); + return Gd32ImageIdentity::kBootloader; + } + + DEBUG_ERR("OTA", "身份校验:向量表非法 (MSP=0x%08lX RH=0x%08lX),非 GD32 APP 固件", + (unsigned long)msp, (unsigned long)reset); + return Gd32ImageIdentity::kInvalid; +} diff --git a/main/storage/gd32_flash_store.h b/main/storage/gd32_flash_store.h index f258fec..71a778c 100644 --- a/main/storage/gd32_flash_store.h +++ b/main/storage/gd32_flash_store.h @@ -28,6 +28,21 @@ typedef struct { uint8_t checksum[32]; ///< 固件 SHA-256 校验值 } ota_meta_t; +/** + * @brief gd32_ota 分区中存储镜像的身份分类(包级防呆)。 + * + * 判定依据是镜像自身内容(Cortex-M 向量表前两个字),不依赖文件名: + * SHA-256 只能证明"传对了",不能证明"传的是对的包"。 + * 把 BOOT 包(或任意非 APP 包)中继进 GD32 APP 区会擦掉好固件, + * 因此触发中继前必须确认镜像身份为 kApp。 + */ +enum class Gd32ImageIdentity : uint8_t { + kApp = 0, ///< 复位向量落在 APP 区 → 合法 GD32 APP 固件 + kBootloader, ///< 复位向量落在 BOOT 区(0x08000000~0x08011FFF)→ Bootloader 包,禁止刷入 APP + kInvalid, ///< 向量表非法 → 非 GD32 APP 固件(ESP32 镜像 / 文本 / 损坏数据) + kReadFailed, ///< 分区读取失败,无法判定 +}; + /** * @brief GD32 OTA 固件 Flash 存储抽象层(纯静态工具类)。 * @@ -139,6 +154,17 @@ public: esp_err_t verifyFrontendSha(const uint8_t frontend_sha32[32], uint8_t out_device_sha[32]); + /** + * @brief 读取 gd32_ota 分区镜像开头的 Cortex-M 向量表并判定包身份。 + * + * 检查初始 MSP 是否落在 GD32E508 SRAM、复位向量是否落在 APP 区 + * (0x08048000~0x0807DFFB,与 BOOT 侧 validateApplication() 同一判据)。 + * 复位向量落在 BOOT 区(0x08000000~0x08011FFF)则判为 Bootloader 包。 + * 供中继触发前的包级防呆使用(WiFi /ota/relay 与 BLE 0xF7 共用)。 + * @return 镜像身份分类;调用方仅应在 kApp 时允许中继。 + */ + Gd32ImageIdentity validateStoredImageIdentity(); + /** * @brief 获取分区指针(懒初始化)。 * @return 分区指针,找不到时返回 nullptr。 diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 9636b7a..70fcbdc 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -455,6 +455,7 @@ SpiMaster::Job *SpiMaster::acquireJobSlot() slot->done = done; slot->finished = false; slot->abandoned = false; + slot->executing = false; slot->in_use = true; break; } @@ -493,6 +494,7 @@ void SpiMaster::releaseJobSlot(Job *job) taskENTER_CRITICAL(&s_spiJobMux); job->finished = false; job->abandoned = false; + job->executing = false; job->in_use = false; taskEXIT_CRITICAL(&s_spiJobMux); } @@ -567,6 +569,7 @@ esp_err_t SpiMaster::submitJob(Job &job) pool_job->result = ESP_FAIL; pool_job->finished = false; pool_job->abandoned = false; + pool_job->executing = false; /* done / in_use 保持 acquire 时状态 */ const TickType_t qwait = pdMS_TO_TICKS( @@ -578,8 +581,11 @@ esp_err_t SpiMaster::submitJob(Job &job) } xTaskNotify(m_workerTask, 0x10U, eSetBits); + /* dwait 覆盖 worker 单笔执行上界:otaSendAndPoll 最多 3 次尝试、每次 ≈ + * timeout_ms,故 3×timeout+10s 保证"已开始执行的 job"必然在提交方放弃前 + * 完成(执行中 worker 在写本函数调用方的栈缓冲,栈帧必须存活到 finished)。 */ const TickType_t dwait = pdMS_TO_TICKS( - (job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 10000U); + 3U * (job.timeout_ms > 0U ? job.timeout_ms : 1000U) + 10000U); if (xSemaphoreTake(pool_job->done, dwait) == pdTRUE) { const esp_err_t result = pool_job->result; /* worker 在 give 后才写 finished;等其退出临界区后再由本侧独占 release。 */ @@ -596,10 +602,15 @@ esp_err_t SpiMaster::submitJob(Job &job) return result; } - /* 超时:标记 abandoned;若 worker 已跑完则本侧 release,否则 finishPoolJob release。 */ + /* 超时:按 worker 是否已开始执行分型(executing 由 workerLoop dequeue 时置位)。 */ taskENTER_CRITICAL(&s_spiJobMux); - pool_job->abandoned = true; + const bool executing = pool_job->executing; const bool already_done = pool_job->finished; + if (!executing && !already_done) { + /* 尚未执行:标 abandoned,worker dequeue 时跳过 executeJob—— + * 本函数即将返回,out_* 将指向已消亡栈帧,再执行即 use-after-return。 */ + pool_job->abandoned = true; + } taskEXIT_CRITICAL(&s_spiJobMux); if (already_done) { @@ -608,8 +619,38 @@ esp_err_t SpiMaster::submitJob(Job &job) releaseJobSlot(pool_job); return result; } + if (executing) { + /* 执行中:worker 此刻正写调用方栈缓冲,不可返回,须等 finished。 + * 正常时执行上界已被 dwait 覆盖,这里只兜 dequeue/dwait 边界的尾差; + * 再等一整个 dwait(执行中意味着 dequeue 早于 dwait,S+dwait 执行上界 + * ≤ 2×dwait 必然完成)。 */ + if (xSemaphoreTake(pool_job->done, dwait) == pdTRUE) { + for (;;) { + taskENTER_CRITICAL(&s_spiJobMux); + const bool fin = pool_job->finished; + taskEXIT_CRITICAL(&s_spiJobMux); + if (fin) { + break; + } + vTaskDelay(1); + } + const esp_err_t result = pool_job->result; + releaseJobSlot(pool_job); + return result; + } + /* 仅 worker 本身卡死才会走到这里(执行上界被打破):标 abandoned + * 让 worker(若仍能收尾)释放槽,本侧放弃等待。 */ + DEBUG_ERR("SPI", "worker 执行中 job 仍未完成 kind=%u(标放弃)", + (unsigned)job.kind); + taskENTER_CRITICAL(&s_spiJobMux); + if (!pool_job->finished) { + pool_job->abandoned = true; + } + taskEXIT_CRITICAL(&s_spiJobMux); + return ESP_ERR_TIMEOUT; + } - DEBUG_ERR("SPI", "worker job done timeout kind=%u(池槽交 worker 释放)", + DEBUG_ERR("SPI", "worker job done timeout kind=%u(未执行已放弃,池槽交 worker 释放)", (unsigned)job.kind); return ESP_ERR_TIMEOUT; } @@ -652,16 +693,29 @@ void SpiMaster::workerLoop() Job *job = nullptr; if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) { - /* 计时 worker 实际执行段(取帧→发→读响应→完成): - * 测速页 RTT − 本值 = 提交/调度/唤醒开销,用于区分 - * "ESP32 侧任务抢占" vs "GD32 应答慢" 的抖动归属。 */ - const int64_t w0 = esp_timer_get_time(); - m_lastProfile.t_w0 = (uint64_t)w0; - executeJob(*job); - const int64_t w1 = esp_timer_get_time(); - m_lastProfile.t_w1 = (uint64_t)w1; - const int64_t wus = w1 - w0; - m_lastWorkerExecUs = (wus > 0) ? (uint32_t)wus : 0U; + /* ★ 执行前原子检查 abandoned:提交方超时返回后 out_* 指向已消亡的 + * 栈帧,此时执行会写悬垂指针(use-after-return → 调用方栈破坏)。 + * executing 与 abandoned 判定同临界区:提交方超时路径据此区分 + * "未执行(标 abandoned 跳过)"与"执行中(必须等写完才能返回)"。 */ + taskENTER_CRITICAL(&s_spiJobMux); + const bool job_abandoned = job->abandoned; + job->executing = true; + taskEXIT_CRITICAL(&s_spiJobMux); + if (!job_abandoned) { + /* 计时 worker 实际执行段(取帧→发→读响应→完成): + * 测速页 RTT − 本值 = 提交/调度/唤醒开销,用于区分 + * "ESP32 侧任务抢占" vs "GD32 应答慢" 的抖动归属。 */ + const int64_t w0 = esp_timer_get_time(); + m_lastProfile.t_w0 = (uint64_t)w0; + executeJob(*job); + const int64_t w1 = esp_timer_get_time(); + m_lastProfile.t_w1 = (uint64_t)w1; + const int64_t wus = w1 - w0; + m_lastWorkerExecUs = (wus > 0) ? (uint32_t)wus : 0U; + } else { + DEBUG_WARN("SPI", "跳过已放弃 job kind=%u(提交方已超时返回)", + (unsigned)job->kind); + } finishPoolJob(job); continue; } @@ -985,9 +1039,12 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, SpiMaster &self = SpiMaster::instance(); - /* 更新对端 credit */ - self.m_remoteCredit = hdr.credit; - + /* ★ credit 更新必须晚于 session 校验:被拒收的帧(session 不匹配且非 0/非 EVENT) + * 其 credit 字段不可信(可能是 GD32 旧 session 残留帧的背压值)。 + * 若在拒收前更新 m_remoteCredit,一次 credit=0 的失配帧就会把 m_remoteCredit + * 锁存为 0,后续所有 transact 进入 otaTransferUnlocked 的 CREDIT 背压死循环 + * (drain 读到的帧 session 仍不匹配 → 持续 return false → m_remoteCredit 永远 0)。 + * 现象:一次通信后所有 SPI 出错,只有复位 GD32(发 session=0 触发 resync)才恢复。 */ if (self.m_v2Active) { /* SESSION_ID 校验 */ if (hdr.session_id != self.m_sessionId) { @@ -1002,10 +1059,11 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, if (!is_event && !is_sess0) { static uint32_t s_sess_mm_logs = 0U; if ((++s_sess_mm_logs % 32U) == 1U) { - DEBUG_WARN("SPI", "v2 SES mismatch got=%08lX exp=%08lX t=%02X #%lu", + DEBUG_WARN("SPI", "v2 SES mismatch got=%08lX exp=%08lX t=%02X #%lu (credit 不更新)", (unsigned long)hdr.session_id, (unsigned long)self.m_sessionId, hdr.type, (unsigned long)s_sess_mm_logs); } + /* 拒收:不更新 m_remoteCredit,避免失配帧的 credit 污染背压状态机 */ return false; } if (is_sess0 && !is_event) { @@ -1024,6 +1082,13 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, (unsigned long)hdr.session_id, (unsigned long)self.m_sessionId, hdr.type); } } + } + + /* session 校验通过(m_v2Active==false / 匹配 / sess0 放行 / EVENT 放行): + * 此帧 credit 可信,更新对端 credit。 */ + self.m_remoteCredit = hdr.credit; + + if (self.m_v2Active) { /* 响应归属:ack_seq 应等于 m_lastSentSeq。 * @@ -1754,6 +1819,9 @@ esp_err_t SpiMaster::appRequestBootloader() m_lastSentSeq = 0; m_rxAckedSeq = 0; m_expectedRxSeq = 0; + /* credit 复位:避免旧 session 锁存的 credit=0 跨重同步污染 */ + m_localCredit = 8; + m_remoteCredit = 8; uint8_t hf[V2_MAX_FRAME]; size_t hl = 0; if (v2_build_hello_frame(m_sessionId, V2_ROLE_APP, hf, &hl)) { @@ -2149,6 +2217,10 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint) m_expectedRxSeq = 0; m_gd32Role = 0; m_v2Active = false; + /* credit 与 init() 对齐:HELLO 成功时 m_remoteCredit 会被 ACK 覆盖, + * 失败时保持 8(而非残留旧值,避免上次通信锁存的 credit=0 跨 session 污染)。 */ + m_localCredit = 8; + m_remoteCredit = 8; /* 2. 构建 HELLO 帧 */ uint8_t hello_frame[V2_MAX_FRAME]; diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index 4e48a53..d17f49b 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -97,6 +97,13 @@ public: /** @brief 读取本端剩余可接收 credit(SPEC §10.1,/system/status 展示用)。 */ uint8_t localCredit() const { return m_localCredit; } + /** @brief credit 窗口复位到初始值(电机 OTA 取消等半会话终止后调用)。 + * credit 只随帧头传递:对端转空闲后不再发帧,传输期锁存的 credit=0 + * 无法自愈 → 所有 transact 被 CREDIT 背压拒绝(死锁)。 + * GD32 端 resetToIdle 已恢复接收窗口(clearOtaSessionQueues 置满), + * 本侧同步复位与之对齐;此后每帧帧头 credit 会覆盖为真实值。 */ + void resetCredit() { m_localCredit = 8; m_remoteCredit = 8; } + /** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */ TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; } @@ -378,6 +385,7 @@ private: SemaphoreHandle_t done = nullptr; ///< 池槽静态 binary,init 创建,永不 delete volatile bool finished = false; ///< worker 已 execute + give volatile bool abandoned = false; ///< 提交方已超时返回,worker 负责 release 槽 + volatile bool executing = false; ///< worker 已 dequeue(即将/正在写 out_*;超时放弃分型用) bool in_use = false; ///< 池槽占用(critical 保护) }; /** 队列深度;池 = 队列 + 1(在途执行槽),覆盖 queued + executing。 */ diff --git a/tools/build_mem.bat b/tools/build_mem.bat deleted file mode 100644 index 847f972..0000000 --- a/tools/build_mem.bat +++ /dev/null @@ -1,23 +0,0 @@ -@echo off -REM Clear MSYS/MINGW env vars to bypass IDF MSys detection when called from Git Bash -set MSYSTEM= -set MSYSTEM_CHOST= -set MSYSTEM_PREFIX= -set MINGW_CHOST= -set MINGW_PREFIX= -set MINGW_PACKAGE_PREFIX= -set CHERE_INVOKING= -set MSYS2_PATH_TYPE= - -REM Toolchain installed at C:\Espressif (see .vscode/settings.json idf.toolsPath) -set IDF_TOOLS_PATH=C:\Espressif - -call "F:\software\.espressif\v6.0\esp-idf\export.bat" -if errorlevel 1 ( - echo EXPORT_FAILED - exit /b 1 -) - -cd /d "G:\Robot_v2\DCU\ESP32_COMM" -idf.py build -exit /b %errorlevel% \ No newline at end of file diff --git a/tools/can.html b/tools/can.html deleted file mode 100644 index a9b6f0a..0000000 --- a/tools/can.html +++ /dev/null @@ -1,334 +0,0 @@ - - - - - -ESP32 CAN 通信控制台 - - - - - - - - - -
- -
-
- -
- ESP32 CAN 通信控制台 - DCU · Robot_v2 · CAN FD -
-
-
- 未连接 - IP -- - RSSI -- - 运行 -- - v-- - CH0 - 未导入 DBC - 0 报文 - 模式: -- - 轮询: -- -
- -
- - - - -
-
-
-
- 📚 - 报文列表 - - -
- - -
-
- -
- -
- -
-
- 已勾选 0 -
- - -
- - - - -
-
-
-
-
-
📥
-

导入 DBC 开始编辑报文

-

点击此处或拖拽 .dbc 文件到列表区域

-
-
- -
-
- -
-
🕘 最近发送
-
-
-
尚未发送任何 CAN 帧。
-
-
-
-
- - -
-
- - - - -
- ✓ - 操作成功 -
- - - - diff --git a/tools/can_monitor.html b/tools/can_monitor.html deleted file mode 100644 index 3333315..0000000 --- a/tools/can_monitor.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - -ESP32 CAN 接收监控 - - - - - - - - - -
- -
-
- - ESP32 CAN 接收监控 -
-
- 未连接 - 模式: -- - 轮询: 待连接 -
- -
- - -
-
🧰 连接设置
-
-
-
- -
- - -
-
-
- -
- - - -
-
-
-
-
- - 等待连接测试 -
-
- 页面每 1 秒通过 /can/feedback 拉取三路 CAN 通道快照。无 RX 的通道显示 空(无 RX)。
桥接模式:CAN0 收到的每一帧(11/29 位、FD、BRS)自动由 CAN1 与 CAN2 转发出去。 -
-
-
-
- - -
-
📊 三路 CAN 通道反馈(1Hz)
-
- -
-
- - -
-
🔬 每 ID 快照表(1Hz 刷新) - -
-
-
- - - - - - - - - - -
#CAN IDDLCFDBRSESI数据 (HEX)
等待数据...
-
-
-
- -
- -
- ✓ - 操作成功 -
- - - - \ No newline at end of file diff --git a/tools/continuity_test.html b/tools/continuity_test.html deleted file mode 100644 index 38cf657..0000000 --- a/tools/continuity_test.html +++ /dev/null @@ -1,737 +0,0 @@ - - - - - -LAN9252 通断测试 - - - - - -
- -

LAN9252 通断测试

-
通过 ESP32 /spi/send → SPI 0xA2 → GD32 驱动 EXMC 引脚,万用表测 LAN9252 端电压
- -
- ⚠️ - 不点按钮时 GD32 引脚为高阻输入态(安全)。点击模式后先预览引脚图,点"确认应用"才实际驱动引脚。测量完毕请点"停止/恢复"释放。 -
- - -
-
- 设备 IP - 读取中... - 共用主页"设备连接"配置的 IP -
-
- - -
-

LAN9252 64-QFN 引脚图

-

俯视,Pin1 右上角。点击下方模式按钮预览引脚电压,点"确认应用"发送命令。绿色=3.3V,红色=0V,橙色=控制线测试中。

-
- - - - - - - - - - - - -
-
- HIGH 3.3V - LOW 0V - 控制线测试 - 数据线空闲 - 控制线空闲 - 电源/其他 - Pin1 -
-
- - -
-

模式选择

-

点击预览引脚图,再点"确认应用"发送命令

- - -
- - - - -
- - -
- - - - - -
-
- - -
-

操作

-
- - - -
-

- 确认应用 · 发送当前选中的模式到 GD32
- 循环切换 · 每5s在 0x0000 ↔ 当前选中模式 之间切换(需先选数据线 pattern)
- 停止 / 恢复 · 恢复 EXMC 正常模式 -

-
- - -
-
- 操作日志 -
- - - - -
-
-
-
- 共 0 条 - SPI 0xA2 · UDP 6666 -
-
- -
- - - - diff --git a/tools/css/base.css b/tools/css/base.css deleted file mode 100644 index 28c07c5..0000000 --- a/tools/css/base.css +++ /dev/null @@ -1,135 +0,0 @@ -/* ============================================================ - base.css — DCU Mission Control · 设计令牌与全局基底 - 风格:深色测控台(deep blue-slate),多色子系统语义配色 - 子系统色:ESP32=青绿 · GD32=琥珀 · BLE=蓝 · CAN=绿 · 内存/调试=紫 - ============================================================ */ - -:root { - /* ---- 底色层次:深蓝灰,层层抬升,拒绝纯黑 ---- */ - --bg-root: #0a0f1a; - --bg-panel: #0e1624; - --bg-card: #131d2e; - --bg-elevated: #1a2740; - --bg-input: #0b1220; - --bg-hover: #20304c; - - /* ---- 边框 ---- */ - --border: #21304a; - --border-strong: #2e4262; - --border-subtle: #1a2740; - - /* ---- 文字 ---- */ - --text: #e9f0fa; - --text-dim: #93a8c4; - --text-label: #617a99; - - /* ---- 子系统语义色 ---- */ - --esp32: #2fd8c1; /* 青绿:ESP32 / 主操作 / WiFi OTA */ - --esp32-hi: #6ff2e0; - --esp32-dim: rgba(47,216,193,.14); - --gd32: #fcbf3f; /* 琥珀:GD32 / 中继 OTA */ - --gd32-hi: #ffd97a; - --gd32-dim: rgba(252,191,63,.14); - --ble: #5fa8ff; /* 蓝:BLE 蓝牙通道 */ - --ble-hi: #93c6ff; - --ble-dim: rgba(95,168,255,.14); - --can: #43d97b; /* 绿:CAN 总线 / 健康 */ - --can-hi: #7deda6; - --can-dim: rgba(67,217,123,.14); - --motor: #22d3ee; /* 青:电机控制台 */ - --motor-hi: #67e8f9; - --motor-dim: rgba(34,211,238,.14); - --mem: #a88bfa; /* 紫:内存 / 调试 / 日志 */ - --mem-hi: #c7b3ff; - --mem-dim: rgba(168,139,250,.14); - - /* ---- 状态语义色 ---- */ - --green: #3ddc8e; - --amber: #fcbf3f; - --red: #ff6b6b; - --red-dim: rgba(255,107,107,.14); - - /* ---- 兼容旧令牌(大量旧样式引用,勿删)---- */ - --accent: var(--esp32); - --accent-dim: var(--esp32-dim); - --border-focus: var(--esp32); - - /* ---- 圆角 ---- */ - --radius-sm: 8px; - --radius: 12px; - --radius-lg: 16px; - - /* ---- 字体 ---- - 沿用原始系统字体方案:中文优先苹方/雅黑,西文 Segoe UI; - mono 用于 SHA / HEX / 日志 / 数值(SF Mono → Fira Code → Cascadia → Consolas)。 - --font-display 保留变量名以兼容既有引用,指向同一系统栈。 */ - --font-display: "PingFang SC","Microsoft YaHei","Segoe UI",system-ui,sans-serif; - --font: "PingFang SC","Microsoft YaHei","Segoe UI",system-ui,sans-serif; - --font-mono: "SF Mono","Fira Code","Cascadia Code","Consolas",monospace; - - /* ---- 间距 / 阴影 ---- */ - --gap: 18px; - --shadow-card: 0 10px 30px rgba(3,7,16,.45), 0 1px 0 rgba(255,255,255,.03) inset; - --shadow-pop: 0 18px 48px rgba(3,7,16,.6); -} - -* { box-sizing: border-box; margin: 0; padding: 0; } - -html, body { - height: 100%; - background: var(--bg-root); - color: var(--text); - font-family: var(--font); - font-size: 14px; - line-height: 1.55; - -webkit-font-smoothing: antialiased; - overflow-x: hidden; -} - -/* ---- 环境光背景:细网格 + 双色氛围辉光 + 顶部扫光 ---- - 网格提供"工程图纸"质感;左上青绿、右下琥珀的两团极淡辉光 - 暗示 ESP32 / GD32 双核子系统,随视口固定,营造纵深。 */ -body::before { - content: ""; - position: fixed; inset: 0; z-index: 0; pointer-events: none; - background-image: - linear-gradient(rgba(148,180,226,.035) 1px, transparent 1px), - linear-gradient(90deg, rgba(148,180,226,.035) 1px, transparent 1px); - background-size: 44px 44px; - mask-image: radial-gradient(ellipse 120% 90% at 50% -8%, black 30%, transparent 78%); -} -body::after { - content: ""; - position: fixed; inset: 0; z-index: 0; pointer-events: none; - background: - radial-gradient(900px 520px at 8% -10%, rgba(47,216,193,.10), transparent 60%), - radial-gradient(820px 560px at 100% 112%, rgba(252,191,63,.07), transparent 62%), - radial-gradient(700px 480px at 88% -6%, rgba(95,168,255,.06), transparent 60%); -} - -.container { - position: relative; z-index: 1; - max-width: 1480px; margin: 0 auto; padding: 26px 32px 44px; -} - -/* ---- 全局滚动条 ---- */ -::-webkit-scrollbar { width: 9px; height: 9px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--border-strong); border-radius: 99px; - border: 2px solid var(--bg-root); -} -::-webkit-scrollbar-thumb:hover { background: #3a5378; } - -/* ---- 选区 ---- */ -::selection { background: rgba(47,216,193,.28); color: #eafffb; } - -/* ---- 通用等宽数字(让指标/百分比对齐、不跳动)---- */ -.tabular { font-variant-numeric: tabular-nums; } - -/* ---- focus 可见性(键盘可达性)---- */ -:focus-visible { - outline: 2px solid var(--esp32); - outline-offset: 2px; - border-radius: 4px; -} diff --git a/tools/css/can.css b/tools/css/can.css deleted file mode 100644 index 3484997..0000000 --- a/tools/css/can.css +++ /dev/null @@ -1,1712 +0,0 @@ -/* === can.css — CAN 面板/工作台/1Hz反馈/ID快照/报文行展开/节点过滤 等 CAN 专用样式 === */ -/* === CAN PANEL === */ -.can-grid { - display: grid; grid-template-columns: 70px 1fr 60px 1fr; gap: 10px; -} -.can-launch-card { - background: - radial-gradient(circle at top right, rgba(67,217,123,.16), transparent 28%), - linear-gradient(138deg, rgba(18,29,41,.96), rgba(20,29,40,.92) 48%, rgba(14,21,31,.98) 100%); - border: 1px solid rgba(67,217,123,.16); - border-radius: 14px; - padding: 20px; - box-shadow: - inset 0 0 0 1px rgba(255,255,255,.02), - 0 18px 40px rgba(0,0,0,.18); - position: relative; - overflow: hidden; -} -.can-launch-card::before { - content: ""; - position: absolute; - inset: 0 auto 0 0; - width: 4px; - background: linear-gradient(180deg, rgba(67,217,123,.9), rgba(67,217,123,.18)); -} -.can-launch-copy { - position: relative; - z-index: 1; -} -.can-launch-top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 10px; -} -.can-launch-copy h3 { - font-size: 22px; - line-height: 1.2; - margin: 6px 0 0; -} -.can-launch-desc { - color: var(--text-dim); - font-size: 13px; - line-height: 1.7; - max-width: 700px; -} -.can-launch-kicker { - color: #8ef0b4; - font-size: 10px; - letter-spacing: .18em; - text-transform: uppercase; - opacity: .92; -} -.can-launch-badge { - display: inline-flex; - align-items: center; - padding: 7px 12px; - border-radius: 999px; - background: rgba(67,217,123,.09); - border: 1px solid rgba(67,217,123,.22); - color: #a9f5c8; - font-size: 11px; - font-weight: 700; - letter-spacing: .12em; - text-transform: uppercase; - white-space: nowrap; -} -.can-launch-points { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 14px; -} -.can-launch-point { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 7px 11px; - border-radius: 999px; - background: rgba(255,255,255,.03); - border: 1px solid rgba(255,255,255,.06); - color: #c6d7e8; - font-size: 12px; -} -.can-launch-point::before { - content: ""; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--can); - box-shadow: 0 0 10px rgba(67,217,123,.35); -} -.can-launch-actions { - display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(180px, .85fr); - gap: 10px; - margin-top: 18px; - position: relative; - z-index: 1; -} -.can-launch-primary, -.can-launch-secondary { - min-height: 42px; -} -.can-launch-primary { - background: linear-gradient(135deg, var(--can) 0%, #22b45f 100%); - color: #04240f; - box-shadow: 0 10px 24px rgba(67,217,123,.2); -} -.can-launch-secondary { - color: #d5e4f4; - border-color: rgba(255,255,255,.08); - background: rgba(255,255,255,.015); -} -.can-launch-secondary:hover:not(:disabled) { - background: rgba(255,255,255,.05); - border-color: rgba(67,217,123,.25); -} -.can-launch-foot { - display: flex; - gap: 10px; - align-items: flex-start; - margin-top: 16px; - padding-top: 14px; - border-top: 1px solid rgba(255,255,255,.06); - color: #91a6bc; - font-size: 12px; - line-height: 1.7; - position: relative; - z-index: 1; -} -/* 电机入口:复用 can-launch 布局,换青色强调 */ -.motor-launch-card { - background: - radial-gradient(circle at top right, rgba(34,211,238,.16), transparent 28%), - linear-gradient(138deg, rgba(18,29,41,.96), rgba(20,29,40,.92) 48%, rgba(14,21,31,.98) 100%); - border-color: rgba(34,211,238,.16); -} -.motor-launch-card::before { - background: linear-gradient(180deg, rgba(34,211,238,.9), rgba(34,211,238,.18)); -} -.motor-launch-card .can-launch-kicker { color: #67e8f9; } -.motor-launch-card .can-launch-badge { - background: rgba(34,211,238,.09); - border-color: rgba(34,211,238,.22); - color: #a5f3fc; -} -.motor-launch-card .can-launch-point::before { - background: var(--motor, #22d3ee); - box-shadow: 0 0 10px rgba(34,211,238,.35); -} -.motor-launch-primary { - background: linear-gradient(135deg, var(--motor, #22d3ee) 0%, #0891b2 100%) !important; - color: #042f2e !important; - box-shadow: 0 10px 24px rgba(34,211,238,.2); -} -.motor-launch-card .can-launch-secondary:hover:not(:disabled) { - border-color: rgba(34,211,238,.25); -} - -.can-launch-foot-label { - flex: none; - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 40px; - padding: 4px 8px; - border-radius: 999px; - background: rgba(255,255,255,.04); - color: #d8e6f4; - font-size: 10px; - letter-spacing: .12em; - text-transform: uppercase; -} -/* === CAN WORKSPACE PAGE === */ -/* 右侧监控抽屉 translate 出屏时,防止 body 被撑出横向滚动条 */ -body.can-page { - overflow-x: hidden; - max-width: 100%; -} -.can-page::after { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background: - radial-gradient(circle at 10% 18%, rgba(34,211,238,.08), transparent 26%), - radial-gradient(circle at 85% 14%, rgba(245,158,11,.05), transparent 24%); -} -.can-container { - max-width: min(1700px, 100%); - width: 100%; - box-sizing: border-box; -} -.can-header { - position: sticky; - top: 16px; - z-index: 5; - backdrop-filter: blur(14px); - background: rgba(15,22,31,.86); -} -/* sticky+backdrop-filter 合成层冲突致 fadeInUp 卡首帧(translateY(10px)+opacity:0), - * 禁用入场动画。选择器需 .header-bar.can-header 以胜过 responsive.css 的 .header-bar 规则 */ -.header-bar.can-header { animation: none; } -.can-header-actions { - margin-left: auto; -} -.can-link-pill { - text-decoration: none; - color: var(--text); -} -.can-toolbar-card, -.can-table-card, -.can-editor-layout { - margin-top: 18px; -} -.can-toolbar-grid { - display: grid; - grid-template-columns: minmax(220px, 1.1fr) 140px minmax(200px, 1fr); - gap: 14px; - align-items: start; -} -.can-toolbar-grid-compact { - grid-template-columns: 140px minmax(220px, 1fr); -} -.can-toolbar-grid-dbc-only { - grid-template-columns: 1fr; - max-width: 420px; -} - -/* DBC 并入报文列表卡 */ -.can-table-card.dbc-dropzone { - position: relative; - transition: box-shadow .15s, border-color .15s; -} -.can-table-card.dbc-dropzone.dragover { - border-color: var(--accent); - box-shadow: 0 0 0 2px var(--accent-dim); -} -.can-table-card.dbc-dropzone.dragover::after { - content: '松开以导入 DBC'; - position: absolute; - inset: 0; - z-index: 5; - display: flex; - align-items: center; - justify-content: center; - background: rgba(9, 14, 21, 0.72); - color: var(--accent); - font-weight: 700; - font-size: 15px; - pointer-events: none; - border-radius: inherit; -} -/* 标题栏内:统计 + 文件 chip 同一行;chip 先铺满横轴,真不够再折行 */ -.dbc-summary-inline { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - gap: 8px 10px; - margin: 0 0 0 8px; - flex: 1 1 auto; - min-width: 0; - max-width: none; - padding: 2px 6px; - white-space: nowrap; - overflow: hidden; -} -.dbc-summary-inline .dbc-summary-meta { - flex: 0 0 auto; -} -.dbc-file-chips { - display: flex; - flex-direction: row; - flex-wrap: wrap; /* 仅在占满标题栏剩余宽度后折行 */ - align-items: center; - align-content: flex-start; - gap: 4px 6px; - flex: 1 1 auto; - min-width: 0; - max-width: none; /* 去掉 40vw/360px 上限,吃满横轴 */ - overflow: hidden; -} -.dbc-file-chip { - display: inline-flex; - align-items: center; - gap: 2px; - max-width: 140px; - padding: 1px 2px 1px 6px; - border-radius: 999px; - background: rgba(34, 211, 238, 0.1); - border: 1px solid rgba(34, 211, 238, 0.25); - font-size: 10px; - color: var(--text); - flex: 0 0 auto; -} -.dbc-file-chip-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 110px; -} -.dbc-file-chip-remove { - border: none; - background: transparent; - color: var(--text-dim); - cursor: pointer; - font-size: 14px; - line-height: 1; - padding: 0 4px; - border-radius: 50%; -} -.dbc-file-chip-remove:hover { - color: var(--red); - background: rgba(239, 68, 68, 0.12); -} -.dbc-empty-drop { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 6px; - min-height: 280px; - padding: 32px 16px; - text-align: center; - color: var(--text-dim); - cursor: pointer; - border: 1px dashed var(--border); - border-radius: var(--radius-sm); - margin: 12px; - background: rgba(255, 255, 255, 0.02); -} -.dbc-empty-drop:hover { - border-color: var(--accent); - color: var(--text); -} -.dbc-empty-drop h4 { - margin: 0; - font-size: 15px; - color: var(--text); - font-weight: 600; -} -.dbc-empty-drop p { - margin: 0; - font-size: 12px; -} -.can-table-card-body .helper-note { - margin: 0 12px 12px; -} -.can-toolbar-block { - min-width: 0; -} -.can-toolbar-upload .upload-zone { - min-height: 72px; - padding: 12px 12px; -} -.can-toolbar-upload .upload-zone h4 { font-size: 13px; margin: 4px 0 2px; } -.can-toolbar-upload .upload-zone p { font-size: 11px; margin: 0; } -/* DBC 解析摘要:强制单行 */ -.dbc-summary-strip { - display: flex; - flex-wrap: nowrap; - align-items: center; - gap: 6px 8px; - margin-top: 8px; - padding: 6px 8px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: rgba(255, 255, 255, 0.02); - font-size: 11px; - color: var(--text-dim); - min-width: 0; - white-space: nowrap; -} -.dbc-summary-strip[hidden] { display: none !important; } -.dbc-summary-name { - flex: 0 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--text); - font-weight: 600; -} -.dbc-summary-meta { - display: inline-flex; - flex-wrap: nowrap; - align-items: center; - gap: 0 8px; - flex: 0 0 auto; - white-space: nowrap; - font-variant-numeric: tabular-nums; -} -.dbc-summary-meta > span { - white-space: nowrap; - flex: 0 0 auto; -} -.dbc-summary-meta b { - color: var(--text); - font-weight: 600; - margin-right: 1px; -} -.dbc-summary-strip .btn { flex: 0 0 auto; padding: 2px 8px; white-space: nowrap; } -.can-toolbar-foot { - display: flex; - flex-wrap: wrap; - gap: 10px 16px; - margin-top: 12px; - align-items: center; -} -.can-toolbar-foot .helper-note { - flex: 1 1 160px; - margin: 0; - font-size: 11px; -} - -/* 报文列表标题栏:标题 | 摘要+chip(吃满中间) | 清除/添加 */ -.can-msg-list-head { - flex-wrap: nowrap; - row-gap: 8px; - min-width: 0; -} -.can-msg-list-head > .dbc-summary-inline { - margin-left: 8px; - flex: 1 1 auto; - min-width: 0; -} -.can-msg-head-actions { - display: inline-flex; - align-items: center; - gap: 6px; - flex: 0 0 auto; - margin-left: 8px; -} -.can-msg-list-head > .dbc-summary-inline + .can-msg-head-actions { - margin-left: 8px; -} -.can-msg-head-actions #clearDbcBtn[hidden] { - display: none !important; -} -/* 通道 / 搜索 / 勾选 / 节点 / 批量 — 同一行工具栏 */ -.can-msg-toolbar { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px 10px; - padding: 8px 14px; - border-bottom: 1px solid var(--border); - background: rgba(15, 23, 42, 0.35); - min-width: 0; - /* 不可用 overflow-x:auto:会把 overflow-y 也变成 clip,节点下拉被裁切 */ - overflow: visible; - position: relative; - z-index: 40; /* 高于 .message-table-head / 列表,避免下拉沉底 */ -} -.can-msg-channel { - display: inline-flex; - align-items: center; - gap: 6px; - flex: 0 0 auto; - font-size: 11px; - color: var(--text-dim); - text-transform: none; - letter-spacing: 0; - font-weight: 500; - cursor: pointer; - margin: 0; -} -.can-msg-channel-label { - white-space: nowrap; -} -.can-msg-channel-select { - width: 72px; - min-width: 72px; - height: 30px; - padding: 2px 6px; - font-size: 12px; -} -.can-msg-search-wrap { - flex: 1 1 140px; - min-width: 120px; - max-width: 260px; -} -.can-msg-search { - width: 100%; - padding: 5px 10px; - font-size: 12px; - height: 30px; -} -.can-msg-toolbar .can-batch-actions { - flex: 0 1 auto; - margin-left: auto; - flex-wrap: nowrap; - gap: 6px; - min-width: 0; -} -.can-msg-toolbar .pill { - white-space: nowrap; - flex-shrink: 0; -} -.can-msg-toolbar .btn-sm { - white-space: nowrap; - flex-shrink: 0; -} -.can-msg-toolbar .can-batch-all-label { - white-space: nowrap; - flex-shrink: 0; -} -.can-table-card-body { - padding: 0; -} -.can-editor-layout { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 340px); - gap: 18px; - align-items: start; - min-width: 0; - width: 100%; - max-width: 100%; -} -.can-editor-layout > .can-main-col, -.can-editor-layout > .can-sidepanel { - min-width: 0; - max-width: 100%; -} -.can-sidepanel { - display: flex; - flex-direction: column; - gap: 10px; /* 两卡间距略紧,仍保持独立卡片 */ - width: 100%; -} -.can-sidepanel > .card { - margin-top: 0; - min-width: 0; - max-width: 100%; -} -.can-preview-card .card-body, -.can-raw-card .card-body { - padding-top: 12px; - padding-bottom: 12px; -} -.dbc-dropzone { - position: relative; - overflow: hidden; -} -.dbc-dropzone::after { - content: ""; - position: absolute; - inset: -40% auto auto -10%; - width: 180px; - height: 180px; - background: radial-gradient(circle, rgba(34,211,238,.18), transparent 68%); - pointer-events: none; -} -.message-list { - max-height: 390px; - overflow-x: hidden !important; - overflow-y: auto; - overscroll-behavior-x: none; -} -.message-table { - /* 列宽随左栏宽度自适应压缩,禁止撑出横向滚动 */ - min-width: 0 !important; - width: 100%; - max-width: 100%; - table-layout: fixed; -} -.message-table-body { - min-width: 0; - width: 100%; - max-width: 100%; - overflow-x: hidden; -} -.message-table-head, -.message-row { - display: grid; - /* 报文名称勿抢宽;标识符/DLC/信号/节点给固定下限,避免被挤没 */ - grid-template-columns: - 34px /* 勾选 */ - 34px /* 展开 */ - 28px /* # */ - minmax(0, 0.55fr) /* DBC名称 */ - minmax(0, 1fr) /* 报文名称(收窄,长名省略) */ - minmax(7.2em, 0.65fr)/* 标识符 */ - 0fr /* 通道 */ - 0fr /* 类型 */ - 2.8em /* DLC */ - 2.8em /* 信号 */ - minmax(5.5em, 0.8fr) /* 发送节点 */ - minmax(0, 0.65fr); /* 注释 */ - align-items: center; - min-width: 0; - width: 100%; - max-width: 100%; - box-sizing: border-box; -} -.message-col-name { - /* 名称过长只省略,不撑开其它列 */ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.message-col-id, -.message-col-dlc, -.message-col-signal, -.message-col-node { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.message-table-head { - position: sticky; - top: 0; - z-index: 2; - background: #182330; - border-bottom: 1px solid var(--border); -} -.message-row { - width: 100%; - border: 0; - background: transparent; - color: var(--text); - text-align: left; - cursor: pointer; - border-bottom: 1px solid rgba(255,255,255,.04); - transition: background .12s ease, border-color .12s ease; -} -.message-row:hover { - background: rgba(255,255,255,.03); -} -.message-row.active { - background: linear-gradient(90deg, rgba(34,211,238,.2), rgba(34,211,238,.07)); - box-shadow: inset 3px 0 0 0 var(--accent); -} -.message-col { - padding: 8px 6px; - font-size: 12px; - line-height: 1.45; - border-right: 1px solid rgba(255,255,255,.03); - min-width: 0; - box-sizing: border-box; -} -.message-table-head .message-col { - font-size: 11px; - font-weight: 700; - letter-spacing: .08em; - color: #c8d6e7; - text-transform: uppercase; -} -.message-col:last-child { - border-right: 0; -} -.message-col-index, -.message-col-dlc, -.message-col-signal, -.message-col-channel, -.message-col-type { - text-align: center; -} -.message-col-id, -.history-item-hex, -.can-frame-hex, -.can-title, -.can-subtitle, -.byte-pill, -.history-item-main, -.history-item-foot, -.signal-table, -.signal-cell-meta, -.signal-cell-range { - font-family: var(--font-mono); -} -.message-col-id { - color: var(--accent); - font-size: 12px; -} -.message-col-dbc { - color: var(--accent); - font-size: 11px; - font-weight: 600; - opacity: .9; -} -.can-empty-state { - display: grid; - place-items: center; - min-height: 300px; - text-align: center; - border: 1px dashed rgba(34,211,238,.2); - border-radius: var(--radius); - background: linear-gradient(180deg, rgba(26,36,50,.7), rgba(20,29,40,.96)); - padding: 28px; -} -.can-empty-icon { - width: 70px; - height: 70px; - border-radius: 18px; - display: grid; - place-items: center; - margin-bottom: 14px; - background: rgba(34,211,238,.12); - font-size: 32px; - box-shadow: inset 0 0 0 1px rgba(34,211,238,.16); -} -.can-empty-state h3 { - font-size: 22px; - margin-bottom: 8px; -} -.can-empty-state p { - max-width: 560px; - color: var(--text-dim); - line-height: 1.75; -} -.can-title { - font-size: 24px; - font-weight: 700; - letter-spacing: .02em; -} -.can-subtitle { - margin-top: 6px; - color: var(--text-dim); - font-size: 13px; -} -.can-selection-bar { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; -} -.can-selection-actions { - display: flex; - gap: 10px; - flex-wrap: wrap; -} -.can-meta-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; - margin-top: 16px; -} -.can-meta-card { - background: var(--bg-elevated); - border: 1px solid var(--border); - border-radius: 10px; - padding: 12px; -} -.can-meta-card span { - display: block; - font-size: 10px; - color: var(--text-label); - letter-spacing: .08em; - text-transform: uppercase; - margin-bottom: 4px; -} -.can-meta-card b { - font-size: 14px; -} -.can-comment-note { - margin-top: 16px; -} -.can-frame-panel { - background: linear-gradient(180deg, rgba(26,36,50,.95), rgba(13,19,27,.98)); - border: 1px solid rgba(34,211,238,.12); - border-radius: 16px; - padding: 18px; - box-shadow: inset 0 0 0 1px rgba(255,255,255,.02); -} -.can-frame-panel-compact { - padding: 16px; -} -.can-panel-kicker { - color: var(--text-label); - font-size: 10px; - letter-spacing: .12em; - text-transform: uppercase; -} -.can-panel-title { - margin-top: 6px; - font-size: 16px; - font-weight: 700; -} -.can-frame-hex { - margin-top: 14px; - padding: 16px 18px; - border-radius: 12px; - background: #081019; - color: #7ef3ff; - font-size: 18px; - line-height: 1.6; - word-break: break-all; - border: 1px solid rgba(34,211,238,.16); -} -.can-byte-strip { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 8px; -} -.byte-pill { - min-width: 0; - width: 36px; - padding: 3px 2px 4px; - border-radius: 6px; - background: rgba(34,211,238,.08); - border: 1px solid rgba(34,211,238,.16); - text-align: center; - font-size: 11px; - line-height: 1.2; - font-family: var(--font-mono); - flex: 0 0 auto; -} -.byte-pill b { - display: block; - color: var(--text-label); - font-size: 9px; - font-weight: 600; - margin-bottom: 1px; - letter-spacing: 0; - font-family: var(--font); -} -.byte-pill > span { - font-variant-numeric: tabular-nums; -} -.signal-table-host { - display: block; - margin-top: 18px; - min-width: 0; - max-width: 100%; -} -.signal-table { - width: 100%; - max-width: 100%; - min-width: 0; - border: 1px solid var(--border); - border-radius: 12px; - overflow: hidden; -} -.signal-table-head, -.signal-row { - display: grid; - /* 全部可收缩,禁止固定 min 像素把容器撑宽 */ - grid-template-columns: - 2.2em - minmax(0, 1.15fr) - minmax(0, 0.9fr) - minmax(0, 0.95fr) - minmax(0, 0.85fr) - minmax(0, 0.9fr); - align-items: stretch; - min-width: 0; - width: 100%; - max-width: 100%; - box-sizing: border-box; -} -.signal-table-inline .signal-cell { - padding: 8px 8px; -} -.signal-table-inline .signal-cell b, -.signal-table-inline .signal-cell small { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; -} -.signal-table-head { - background: #182330; - border-bottom: 1px solid var(--border); -} -.signal-table-body { - max-height: 520px; - overflow-x: hidden; - overflow-y: auto; -} -.signal-row { - border-bottom: 1px solid rgba(255,255,255,.04); - background: rgba(255,255,255,.01); -} -.signal-row:nth-child(even) { - background: rgba(255,255,255,.025); -} -.signal-cell { - padding: 11px 12px; - border-right: 1px solid rgba(255,255,255,.03); - font-size: 12px; - line-height: 1.5; - min-width: 0; -} -.signal-cell:last-child { - border-right: 0; -} -.signal-table-head .signal-cell { - font-size: 11px; - font-weight: 700; - letter-spacing: .08em; - color: #c8d6e7; - text-transform: uppercase; -} -.signal-cell-index { - text-align: center; -} -.signal-cell-name b, -.signal-cell-range b, -.signal-cell-meta b { - display: block; - font-size: 12px; - color: var(--text); -} -.signal-cell-name small, -.signal-cell-range small, -.signal-cell-meta small, -.signal-cell-comment { - display: block; - margin-top: 4px; - color: var(--text-dim); - font-size: 11px; - line-height: 1.45; -} -.signal-input-compact { - min-width: 0; - padding: 8px 10px; - font-size: 12px; -} -.can-response { - margin-top: 14px; - padding: 12px 14px; - border-radius: 12px; - background: var(--bg-elevated); - border: 1px solid var(--border); - font-family: var(--font-mono); - font-size: 12px; - word-break: break-all; -} -.can-response.ok { - border-color: rgba(34,197,94,.25); - color: #c7f9d5; -} -.can-response.err { - border-color: rgba(239,68,68,.25); - color: #fecaca; -} -.can-response-label { - color: var(--text-label); - font-size: 11px; - margin-bottom: 6px; - font-family: var(--font); -} -.history-list { - display: flex; - flex-direction: column; - gap: 4px; - max-height: 620px; - overflow-x: hidden; - overflow-y: auto; - padding-right: 4px; - min-width: 0; - max-width: 100%; -} -.history-list-compact { - max-height: 420px; -} -/* 单行紧凑历史:充分利用横轴,减少行高 */ -.history-item { - padding: 5px 10px; - border-radius: 8px; - background: var(--bg-elevated); - border: 1px solid var(--border); - min-width: 0; - max-width: 100%; - box-sizing: border-box; -} -.history-item-row { - display: grid; - grid-template-columns: - auto - 56px - minmax(0, 0.7fr) - minmax(0, 0.8fr) - minmax(0, 1.6fr); - align-items: center; - gap: 8px 10px; - min-width: 0; - max-width: 100%; -} -.history-item-row.is-err { - border-color: rgba(239, 68, 68, 0.35); -} -.history-item-time { - color: var(--text-label); - font-size: 11px; - font-variant-numeric: tabular-nums; - white-space: nowrap; -} -.history-item-tag { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 2px 7px; - border-radius: 999px; - font-size: 10px; - font-weight: 700; - letter-spacing: .06em; - white-space: nowrap; - line-height: 1.3; -} -.history-item-tag.ok { - background: rgba(34,197,94,.12); - color: #b8f5cb; -} -.history-item-tag.err { - background: rgba(239,68,68,.12); - color: #fecaca; -} -.history-item-main { - margin-top: 0; - font-size: 12px; - color: var(--text); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; -} -.history-item-hex { - margin-top: 0; - font-size: 11px; - color: #7ef3ff; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; - word-break: normal; -} -.history-item-foot { - margin-top: 0; - font-size: 11px; - color: var(--text-dim); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; - text-align: right; -} -/* Click-to-Copy 样式 */ -.history-item-row .copyable { - cursor: pointer; - border-radius: 4px; - padding: 1px 4px; - margin: -1px -4px; - transition: background .15s, color .15s; -} -.history-item-row .copyable:hover { - background: rgba(255,255,255,.08); -} -.history-item-row .copyable:active { - background: rgba(255,255,255,.14); -} -.history-item-row .copyable.copied { - background: rgba(34,197,94,.18); - color: #86efac; -} - -/* === CAN 1Hz FEEDBACK === */ -.can-feedback-card { margin-top: 18px; } -.can-feedback-card .card-head { - display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; -} -.feedback-controls { display: flex; align-items: center; gap: 8px; } -.feedback-grid { - display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; -} -/* can.html 右侧监控抽屉:竖向排三路,窄宽也能读 */ -.feedback-grid-drawer { - grid-template-columns: 1fr; - gap: 12px; -} -.can-monitor-drawer { - /* 用 100% 视口减量,避免 100vw 含滚动条宽度把页面撑出横条 */ - --sys-drawer-panel-w: min(440px, calc(100% - 48px)); - max-width: 100%; -} -.can-monitor-drawer .sys-drawer-body .status-section-title:first-child { - margin-top: 0; -} -.can-monitor-drawer .conn-indicator { - display: inline-flex; - align-items: center; - padding: 2px 8px; - margin: 0; - background: transparent; - border: none; -} -.feedback-channel.ch-disabled { - opacity: 0.45; - filter: grayscale(0.35); -} -.feedback-channel.ch-disabled .feedback-ch-head { - cursor: default; -} -.feedback-channel { - background: rgba(255,255,255,.03); border: 1px solid rgba(255,255,255,.07); - border-radius: 10px; padding: 14px 16px; -} -.feedback-ch-head { - font-weight: 700; font-size: 15px; margin-bottom: 10px; - display: flex; align-items: center; gap: 8px; - cursor: pointer; - user-select: none; -} -.feedback-expand-caret { - margin-left: auto; - font-size: 11px; - color: var(--text-dim); - transition: transform .15s ease; - opacity: .7; -} -.feedback-channel.collapsed .feedback-expand-caret { - transform: rotate(-90deg); -} -.feedback-channel.collapsed .feedback-detail { - display: none; -} -.feedback-channel:not(.collapsed) .feedback-ch-head { - margin-bottom: 8px; -} -.feedback-primary .feedback-row:last-child { - border-bottom: 1px solid rgba(255,255,255,.04); -} -.feedback-channel:not(.collapsed) .feedback-primary .feedback-row:last-child { - border-bottom: 1px solid rgba(255,255,255,.04); -} -.feedback-detail { - margin-top: 2px; - padding-top: 2px; - border-top: 1px dashed rgba(255,255,255,.06); -} -.ch-dot { - width: 10px; height: 10px; border-radius: 50%; display: inline-block; -} -.ch-dot.ch0 { background: #22d3ee; } -.ch-dot.ch1 { background: #a78bfa; } -.ch-dot.ch2 { background: #f59e0b; } -.feedback-row { - display: flex; justify-content: space-between; padding: 3px 0; - font-size: 13px; border-bottom: 1px solid rgba(255,255,255,.04); -} -.feedback-row:last-child { border-bottom: none; } -.fb-label { color: var(--muted, #8892a4); } -.fb-val { font-weight: 600; font-variant-numeric: tabular-nums; cursor: pointer; border-radius: 4px; padding: 1px 4px; margin: -1px -4px; transition: background .15s; } -.fb-val:hover { background: rgba(255,255,255,.08); } -.fb-val:active { background: rgba(255,255,255,.14); } -.fb-val.copied { background: rgba(34,197,94,.18); color: #86efac; } -.fb-val.mono { font-family: monospace; } -.fb-val.ok { color: #34d399; } -.fb-val.warn { color: #f59e0b; } -.fb-val.err { color: #f87171; } -.fb-val.zero { color: #64748b; } -@media (max-width: 900px) { - .feedback-grid { grid-template-columns: 1fr; } -} - -/* === ID SNAPSHOT TABLE === */ -.idsnap-table-wrap { overflow-x: auto; } -.idsnap-table { - width: 100%; border-collapse: collapse; font-size: 13px; - font-variant-numeric: tabular-nums; -} -.idsnap-table th { - text-align: left; padding: 6px 10px; font-weight: 600; - border-bottom: 2px solid rgba(255,255,255,.12); color: var(--muted, #8892a4); - white-space: nowrap; -} -.idsnap-table td { - padding: 5px 10px; border-bottom: 1px solid rgba(255,255,255,.05); - white-space: nowrap; -} -.idsnap-table td.mono { font-family: monospace; } -.idsnap-table tr:hover td { background: rgba(255,255,255,.03); } -.idsnap-table .flag-on { color: #34d399; font-weight: 600; } -.idsnap-table .flag-off { color: #475569; } -.idsnap-data { font-family: monospace; max-width: 320px; overflow: hidden; text-overflow: ellipsis; } - -.fb-data-cell { - max-width: 200px; overflow: hidden; text-overflow: ellipsis; - font-size: 11px; word-break: break-all; line-height: 1.4; -} - -/* === CAN message row expand / batch additions === */ -.message-col-check, -.message-col-toggle { - text-align: center; - padding: 6px 2px; - /* 勿被全局单元格 overflow:hidden 裁切勾选/展开 */ - overflow: visible !important; - text-overflow: clip; -} -/* 统一:报文行 / 全选 / 节点过滤 勾选框外观 */ -.can-page input[type="checkbox"], -.message-check, -.checkbox-inline input[type="checkbox"], -.can-node-filter-item input[type="checkbox"] { - -webkit-appearance: none; - appearance: none; - width: 15px; - height: 15px; - margin: 0; - padding: 0; - flex-shrink: 0; - box-sizing: border-box; - border: 1.5px solid rgba(148, 163, 184, 0.55); - border-radius: 4px; - background: rgba(15, 23, 42, 0.9); - cursor: pointer; - vertical-align: middle; - position: relative; - display: inline-block; - transition: border-color .12s ease, background .12s ease, box-shadow .12s ease; -} -.can-page input[type="checkbox"]:hover, -.message-check:hover, -.checkbox-inline input[type="checkbox"]:hover, -.can-node-filter-item input[type="checkbox"]:hover { - border-color: rgba(34, 211, 238, 0.65); -} -.can-page input[type="checkbox"]:checked, -.message-check:checked, -.checkbox-inline input[type="checkbox"]:checked, -.can-node-filter-item input[type="checkbox"]:checked { - background: var(--accent, #22d3ee); - border-color: var(--accent, #22d3ee); - box-shadow: 0 0 0 2px rgba(34, 211, 238, 0.18); -} -.can-page input[type="checkbox"]:checked::after, -.message-check:checked::after, -.checkbox-inline input[type="checkbox"]:checked::after, -.can-node-filter-item input[type="checkbox"]:checked::after { - content: ""; - position: absolute; - left: 4px; - top: 1px; - width: 4px; - height: 8px; - border: solid #0b1220; - border-width: 0 2px 2px 0; - transform: rotate(45deg); -} -.can-page input[type="checkbox"]:focus-visible, -.message-check:focus-visible, -.checkbox-inline input[type="checkbox"]:focus-visible, -.can-node-filter-item input[type="checkbox"]:focus-visible { - outline: none; - box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.28); -} -.message-toggle { - width: 26px; - height: 26px; - min-width: 26px; - border-radius: 6px; - border: 1px solid rgba(255,255,255,.14); - background: rgba(255,255,255,.04); - color: #c8d6e7; - cursor: pointer; - font-size: 12px; - line-height: 1; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - box-sizing: border-box; - transition: background .12s ease, border-color .12s ease, color .12s ease; -} -.message-toggle:hover { - background: rgba(34,211,238,.12); - border-color: rgba(34,211,238,.4); - color: #e6fbff; -} -.message-toggle.expanded { - background: rgba(34,211,238,.18); - border-color: rgba(34,211,238,.55); - color: #e6fbff; -} - -.message-row-expand { - grid-column: 1 / -1; - background: rgba(255,255,255,.02); - border-top: 1px dashed rgba(255,255,255,.06); - border-bottom: 1px solid rgba(255,255,255,.04); - padding: 10px 12px 12px; - width: 100%; - max-width: 100%; - min-width: 0; - box-sizing: border-box; - /* 禁止展开区出现横向滑条;内容省略/折行 */ - overflow-x: hidden; -} -.message-row-expand[hidden] { - display: none; -} -.message-row-expand .signal-table-host { - margin-top: 0; - min-width: 0; - max-width: 100%; - overflow-x: hidden; -} -.message-row-expand .signal-table { - border-radius: 10px; - min-width: 0 !important; - max-width: 100%; -} -.message-row-expand .signal-table-body { - max-height: 360px; - overflow-x: hidden; - overflow-y: auto; -} -.message-row-expand .signal-cell { - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; -} -.message-row-expand .signal-input, -.message-row-expand .signal-input-compact, -.message-row-expand select.input, -.message-row-expand input.input { - width: 100%; - min-width: 0 !important; - max-width: 100%; - box-sizing: border-box; -} - -.can-batch-actions { - display: inline-flex; - align-items: center; - gap: 8px; - margin-left: auto; - flex-wrap: wrap; -} -.checkbox-inline { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-dim, #93a4bf); - cursor: pointer; - user-select: none; -} -.checkbox-inline input { - cursor: pointer; -} -.card-head-actions { - display: inline-flex; - align-items: center; - gap: 8px; -} -.btn.btn-sm { - padding: 4px 10px; - font-size: 12px; -} -.btn[disabled] { - opacity: .55; - cursor: not-allowed; -} - -/* === 2026-06-30c: 报文列表与右侧操作栏并列布局调整 === */ -/* 左列:报文列表 + 最近发送;右列:帧预览等 */ -.can-editor-layout > .can-main-col { - margin-top: 0; - display: flex; - flex-direction: column; - gap: 18px; - min-width: 0; - max-width: 100%; -} -.can-main-col > .can-table-card { - margin-top: 0; - min-width: 0; - max-width: 100%; - /* 保持 visible,避免裁切「发送节点」下拉;横向由内部 message-list 锁死 */ - overflow: visible; -} -.can-main-col > .can-history-card { - margin-top: 0; - align-self: stretch; - min-width: 0; - max-width: 100%; - overflow-x: hidden; -} -.can-table-card-body, -.can-msg-list-head, -.can-msg-toolbar { - min-width: 0; - max-width: 100%; -} -.can-frame-actions { - display: flex; - gap: 8px; - margin-top: 10px; - flex-wrap: wrap; -} -.can-frame-actions .btn { flex: 1 1 auto; min-width: 120px; } -.can-frame-panel-compact .can-frame-hex { - word-break: break-all; -} - - -/* === 2026-06-30c: 报文列表压缩列宽 + 纵向滚动美化 === */ -.message-list { - /* 提高可视高度,并改用美化的纵向滚动条 */ - max-height: calc(100vh - 300px); - min-height: 420px; - overflow-x: hidden !important; - overflow-y: auto; - overscroll-behavior-x: none; - min-width: 0; - width: 100%; - max-width: 100%; - scrollbar-width: thin; - scrollbar-color: rgba(34,211,238,.35) transparent; -} -.message-list::-webkit-scrollbar { width: 8px; } -.message-list::-webkit-scrollbar-track { - background: transparent; - margin: 4px 0; -} -.message-list::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, rgba(34,211,238,.45), rgba(34,211,238,.18)); - border-radius: 6px; - border: 2px solid transparent; - background-clip: padding-box; -} -.message-list::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, rgba(34,211,238,.65), rgba(34,211,238,.28)); - background-clip: padding-box; - border: 2px solid transparent; -} - -.msg-src { - display: inline-block; - max-width: 4.5em; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: bottom; - font-size: 10px; - font-weight: 600; - color: var(--accent); - opacity: .85; - margin-right: 2px; -} -/* 单元格统一省略号;切断 grid 项默认 min-content 最小宽 */ -.message-row .message-col, -.message-table-head .message-col { - min-width: 0 !important; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - box-sizing: border-box; -} -.message-col { - padding: 8px 6px; -} -/* 紧凑列的内边距收一点,腾出空间给名称/注释 */ -.message-col-index, -.message-col-channel, -.message-col-type, -.message-col-dlc, -.message-col-signal { - padding-left: 6px; - padding-right: 6px; -} -.message-col-check, -.message-col-toggle { - padding-left: 2px; - padding-right: 2px; - display: inline-flex; - align-items: center; - justify-content: center; -} -/* 注释列允许浅色降权显示 */ -.message-row .message-col-comment { - color: var(--muted); -} - -/* === 窄屏报文表降级(0fr 隐藏列,保持 track 数量) === */ -/* 列序:check toggle # dbc name id channel type dlc signal node comment */ -/* 隐藏列必须把 padding/border/内容贡献压到 0,否则仍会撑出横向滚动 */ -.message-col-channel, -.message-col-type { - padding: 0 !important; - border: 0 !important; - margin: 0 !important; - overflow: hidden !important; - min-width: 0 !important; - max-width: 0 !important; - font-size: 0 !important; - line-height: 0 !important; - visibility: hidden; - pointer-events: none; -} -@media (max-width: 1200px) { - .message-table-head, - .message-row { - grid-template-columns: - 34px 34px 28px - minmax(0, 0.5fr) /* dbc */ - minmax(0, 1fr) /* name */ - minmax(6.8em, 0.6fr)/* id */ - 0fr 0fr - 2.6em /* dlc */ - 0fr /* signal */ - minmax(5em, 0.7fr) /* node */ - minmax(0, 0.55fr); /* comment */ - } - .message-col-signal { - padding: 0 !important; - border: 0 !important; - overflow: hidden !important; - min-width: 0 !important; - max-width: 0 !important; - font-size: 0 !important; - visibility: hidden; - pointer-events: none; - } -} -@media (max-width: 1024px) { - .message-table-head, - .message-row { - grid-template-columns: - 34px 34px 26px - minmax(0, 0.5fr) - minmax(0, 1fr) - minmax(6.5em, 0.55fr) - 0fr 0fr - 2.6em - 0fr - 0fr /* node */ - minmax(0, 0.55fr); - } - .message-col-node { - padding: 0 !important; - border: 0 !important; - overflow: hidden !important; - min-width: 0 !important; - max-width: 0 !important; - font-size: 0 !important; - visibility: hidden; - pointer-events: none; - } -} -@media (max-width: 900px) { - .message-table-head, - .message-row { - grid-template-columns: - 34px 34px 26px - 0fr /* dbc */ - minmax(0, 1fr) - minmax(6.2em, 0.55fr) - 0fr 0fr - 2.6em - 0fr 0fr - minmax(0, 0.5fr); - } - .message-col-dbc { - padding: 0 !important; - border: 0 !important; - overflow: hidden !important; - min-width: 0 !important; - max-width: 0 !important; - font-size: 0 !important; - visibility: hidden; - pointer-events: none; - } -} - - -/* 工作台单独一行 */ -.can-top-layout { - margin-top: 18px; - display: block; -} -.can-top-layout > .can-toolbar-card { - margin-top: 0; -} -/* 历史卡:报文列表下方,单行紧凑约 8~10 条可见 */ -.can-history-card .card-body { - display: flex; - flex-direction: column; - padding-top: 10px; - padding-bottom: 10px; -} -.can-history-card .history-list { - max-height: min(42vh, 420px); - overflow-y: auto; - padding-right: 4px; - scrollbar-width: thin; - scrollbar-color: rgba(34,211,238,.35) transparent; -} -.can-history-card .history-list::-webkit-scrollbar { width: 8px; } -.can-history-card .history-list::-webkit-scrollbar-track { background: transparent; margin: 4px 0; } -.can-history-card .history-list::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, rgba(34,211,238,.45), rgba(34,211,238,.18)); - border-radius: 6px; - border: 2px solid transparent; - background-clip: padding-box; -} -.can-history-card .history-list::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, rgba(34,211,238,.65), rgba(34,211,238,.28)); - background-clip: padding-box; -} - - -/* === 2026-07-01a: 发送节点过滤下拉 === */ -.can-node-filter { - position: relative; - display: inline-flex; - z-index: 1; -} -/* 面板打开时整块提到最前 */ -.can-node-filter.is-open { - z-index: 50; -} -.can-node-filter-btn { - display: inline-flex; - align-items: center; - gap: 6px; - white-space: nowrap; -} -.can-node-filter-btn.active { - border-color: rgba(34,211,238,.65); - color: var(--accent); - background: rgba(34,211,238,.08); -} -.can-node-filter-caret { - font-size: 10px; - opacity: .75; -} -.can-node-filter-panel { - position: absolute; - top: calc(100% + 6px); - right: 0; - z-index: 60; - min-width: 260px; - max-width: 320px; - background: #182330; - border: 1px solid var(--border); - border-radius: 12px; - box-shadow: 0 18px 38px rgba(0,0,0,.45); - padding: 10px; -} -.can-node-filter-panel[hidden] { display: none; } -.can-node-filter-head { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 8px; -} -.can-node-filter-search { - width: 100%; - padding: 6px 8px; - font-size: 12px; -} -.can-node-filter-actions { - display: flex; - gap: 6px; - justify-content: flex-end; -} -.can-node-filter-list { - max-height: 260px; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 2px; - scrollbar-width: thin; - scrollbar-color: rgba(34,211,238,.35) transparent; -} -.can-node-filter-list::-webkit-scrollbar { width: 8px; } -.can-node-filter-list::-webkit-scrollbar-thumb { - background: rgba(34,211,238,.35); - border-radius: 6px; -} -.can-node-filter-item { - display: grid; - grid-template-columns: 18px 1fr auto; - align-items: center; - gap: 8px; - padding: 6px 8px; - border-radius: 8px; - cursor: pointer; - font-size: 12px; - color: var(--text); - transition: background .12s; -} -.can-node-filter-item:hover { - background: rgba(255,255,255,.04); -} -.can-node-filter-item input[type="checkbox"] { - margin: 0; -} -.can-node-filter-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-family: var(--font-mono); -} -.can-node-filter-count { - font-size: 11px; - color: var(--muted); - background: rgba(255,255,255,.05); - border-radius: 999px; - padding: 1px 8px; - min-width: 26px; - text-align: center; -} diff --git a/tools/css/components.css b/tools/css/components.css deleted file mode 100644 index c913767..0000000 --- a/tools/css/components.css +++ /dev/null @@ -1,944 +0,0 @@ -/* ============================================================ - components.css — 上传 / 文件 / 进度 / 指标 / 徽章 / 阶段流 / - 堆概况 / 任务栈 / 日志 / Toast / Tooltip 等组件 - ============================================================ */ - -/* ================= 上传区 ================= */ -.upload-zone { - border: 2px dashed var(--border-strong); border-radius: var(--radius); - padding: 30px 16px; text-align: center; cursor: pointer; - background: rgba(255,255,255,.015); - transition: border-color .22s, background .22s, transform .22s, box-shadow .22s; -} -.upload-zone:hover, .upload-zone.dragover { - border-color: var(--esp32); - background: var(--esp32-dim); - box-shadow: 0 0 0 4px rgba(47,216,193,.07); -} -.upload-zone .upload-ico { font-size: 32px; margin-bottom: 10px; } -.upload-zone h4 { - font-family: var(--font-display); - font-size: 14px; font-weight: 600; margin-bottom: 4px; letter-spacing: .02em; -} -.upload-zone p { font-size: 12px; color: var(--text-dim); } -.upload-zone-sm { padding: 18px 12px; } -.upload-zone-sm .upload-ico { font-size: 24px; margin-bottom: 6px; } -.upload-zone-sm h4 { font-size: 13px; } - -/* ================= 文件信息 ================= */ -.file-info { - display: none; margin-top: 12px; - background: var(--bg-elevated); border: 1px solid var(--border); - border-radius: var(--radius-sm); padding: 13px 15px; -} -.file-info.show { display: block; } -.file-info-row { display: flex; justify-content: space-between; align-items: center; } -.file-info-row + .file-info-row { margin-top: 10px; } -.file-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; } -.file-meta-item { - background: var(--bg-card); border: 1px solid var(--border-subtle); - padding: 8px 11px; border-radius: 7px; font-size: 12px; -} -.file-meta-item span { - color: var(--text-label); display: block; - font-family: var(--font-display); - font-size: 10px; letter-spacing: .1em; text-transform: uppercase; margin-bottom: 3px; -} -.file-meta-item b { font-weight: 600; font-family: var(--font-mono); } - -/* ================= 阶段切换胶囊(ESP32 OTA / GD32 OTA)================= */ -.progress-phase-row { - display: flex; align-items: center; gap: 4px; margin-bottom: 15px; - background: var(--bg-input); border: 1px solid var(--border); - border-radius: 999px; padding: 4px; -} -.pp-step { - flex: 1; - display: flex; align-items: center; justify-content: center; gap: 6px; - padding: 8px 10px; border-radius: 999px; - font-family: var(--font-display); - font-size: 12px; font-weight: 600; letter-spacing: .03em; - color: var(--text-dim); background: transparent; - transition: all .25s ease; white-space: nowrap; cursor: default; -} -.pp-ico { font-size: 13px; opacity: .45; transition: opacity .25s ease; } -.pp-step.active { - color: #04231e; background: linear-gradient(135deg, var(--esp32), #14b8a6); - box-shadow: 0 2px 12px rgba(47,216,193,.35); -} -.pp-step.active .pp-ico { opacity: 1; } -.pp-step.done { color: var(--can-hi); background: rgba(61,220,142,.13); } -.pp-step.done .pp-ico { opacity: 1; } -.pp-step.done .pp-txt::after { content: ' ✓'; font-size: 10px; } - -/* ================= 进度条 ================= */ -.progress-wrap { margin-top: 0; display: none; } -.progress-wrap.show { display: block; } -.progress-track { - width: 100%; height: 11px; border-radius: 999px; - background: var(--bg-input); overflow: hidden; - border: 1px solid var(--border-subtle); - box-shadow: inset 0 1px 3px rgba(0,0,0,.4); -} -.progress-fill { - height: 100%; border-radius: 999px; - background: linear-gradient(90deg, var(--esp32), #14b8a6); - width: 0%; transition: width .3s ease; - position: relative; - box-shadow: 0 0 10px rgba(47,216,193,.4); -} -.progress-fill::after { - content: ""; position: absolute; inset: 0; - background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.35) 50%, transparent 100%); - animation: sweep 1.6s linear infinite; -} -.progress-fill.done { - background: linear-gradient(90deg, var(--green), #16a374); - box-shadow: 0 0 10px rgba(61,220,142,.4); -} -@keyframes sweep { 0% { transform: translateX(-100%);} 100% { transform: translateX(100%);} } -.progress-text { - display: flex; justify-content: space-between; margin-top: 8px; - font-size: 12px; color: var(--text-dim); -} -.progress-text #progressPct { - font-family: var(--font-display); font-weight: 700; - color: var(--esp32-hi); font-variant-numeric: tabular-nums; -} - -/* ================= 按钮行 ================= */ -.btn-row { display: flex; gap: 10px; margin-top: 16px; } -.btn-row .btn { flex: 1; } - -/* ================= 运行指标 ================= */ -.metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } -.metric { - position: relative; - background: var(--bg-elevated); border: 1px solid var(--border); - border-radius: var(--radius-sm); padding: 13px 15px; - overflow: hidden; - transition: border-color .22s, transform .22s, box-shadow .22s; -} -.metric::before { - content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; - background: var(--metric-accent, var(--esp32)); - opacity: .55; -} -.metric:nth-child(1) { --metric-accent: var(--esp32); } /* ESP32 温度 */ -.metric:nth-child(2) { --metric-accent: var(--gd32); } /* GD32 温度 */ -.metric:nth-child(3) { --metric-accent: var(--ble); } /* 输入 VCC */ -.metric:nth-child(4) { --metric-accent: var(--mem); } /* VDDA */ -.metric:nth-child(5) { --metric-accent: var(--gd32); } /* GD32 APP */ -.metric:nth-child(6) { --metric-accent: var(--can); } /* GD32 BOOT */ -.metric:hover { - border-color: color-mix(in srgb, var(--metric-accent, var(--esp32)) 45%, var(--border)); - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(3,7,16,.4); -} -.metric .k { - font-family: var(--font); - font-size: 11px; font-weight: 500; color: var(--text-label); - letter-spacing: .04em; margin-bottom: 6px; -} -.metric .v { - font-family: var(--font); - font-size: 20px; font-weight: 700; line-height: 1.2; - word-break: break-word; - font-variant-numeric: tabular-nums; -} -.metric .v.highlight, .metric .v.accent { color: var(--esp32-hi); } -.metric .v.ok { color: var(--green); } -.metric .v.warn { color: var(--amber); } -.metric .v.err { color: var(--red); } - -.status-kv-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } -.status-kv { - background: var(--bg-elevated); border: 1px solid var(--border); - border-radius: var(--radius-sm); padding: 10px 13px; -} -.status-kv .k { - display: block; font-family: var(--font-display); - font-size: 10px; color: var(--text-label); - text-transform: uppercase; letter-spacing: .1em; margin-bottom: 4px; -} -.status-kv b { - display: block; color: var(--text); font-size: 13px; font-weight: 700; - word-break: break-word; font-family: var(--font-mono); -} - -/* ================= 主状态徽章 ================= */ -.badge-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } -.badge-stat { - background: var(--bg-elevated); border: 1px solid var(--border); - border-radius: var(--radius); padding: 16px 16px 13px; text-align: center; - transition: border-color .22s, transform .22s, box-shadow .22s; -} -.badge-stat:hover { - border-color: var(--border-strong); transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(3,7,16,.4); -} -.badge-stat .v { - font-family: var(--font-display); - font-size: 23px; font-weight: 700; letter-spacing: .01em; - font-variant-numeric: tabular-nums; -} -.badge-stat .v.accent { color: var(--esp32-hi); } -.badge-stat .v.ok { color: var(--green); text-shadow: 0 0 14px rgba(61,220,142,.35); } -.badge-stat .v.warn { color: var(--amber); } -.badge-stat .v.err { color: var(--red); } -.badge-stat .v.bad { color: var(--red); } -.badge-stat .k { - font-family: var(--font-display); - font-size: 10px; color: var(--text-label); - text-transform: uppercase; letter-spacing: .13em; margin-top: 4px; -} - -/* ---- 健康状态 → 启动自检 ---- */ -.badge-stat-click { cursor: pointer; user-select: none; } -.badge-stat-click:hover, .badge-stat-click:focus-visible { - outline: none; - border-color: color-mix(in srgb, var(--esp32) 45%, var(--border)); - background: color-mix(in srgb, var(--esp32) 7%, var(--bg-elevated)); -} -.badge-stat-click.is-open { - border-color: color-mix(in srgb, var(--esp32) 50%, var(--border)); -} -.health-boot-hint { - font-size: 9px; letter-spacing: 0; text-transform: none; - color: var(--text-dim); font-weight: 500; margin-left: 3px; -} -.badge-stat-click.is-open .health-boot-hint { color: var(--esp32-hi); } -.health-boot-panel { - margin-top: 12px; padding: 13px 15px; - border-radius: var(--radius-sm); - border: 1px solid var(--border); background: var(--bg-elevated); -} -.health-boot-panel.collapsed, .health-boot-panel[hidden] { display: none; } -.health-boot-head { - display: flex; align-items: baseline; justify-content: space-between; - gap: 10px; margin-bottom: 7px; -} -.health-boot-title { - font-family: var(--font-display); - font-size: 11px; color: var(--text-label); - text-transform: uppercase; letter-spacing: .1em; font-weight: 600; -} -.health-boot-summary { font-family: var(--font-display); font-size: 14px; font-weight: 700; color: var(--text); text-align: right; } -.health-boot-summary.ok { color: var(--green); } -.health-boot-summary.warn{ color: var(--amber); } -.health-boot-summary.err { color: var(--red); } -.health-boot-failed { font-size: 12px; color: var(--text-dim); margin-bottom: 9px; } -.health-boot-failed b { color: var(--text); font-weight: 600; } -.health-boot-failed b.ok { color: var(--green); } -.health-boot-failed b.warn{ color: var(--amber); } -.health-boot-failed b.err { color: var(--red); } - -/* ---- 启动模块芯片 ---- */ -.boot-module-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; min-height: 8px; } -.boot-mod { - display: inline-flex; align-items: center; gap: 6px; - padding: 4px 9px; border-radius: 999px; - border: 1px solid var(--border); background: var(--bg-root); - font-family: var(--font-mono); - font-size: 11px; font-weight: 600; color: var(--text-dim); line-height: 1.2; -} -.boot-mod .boot-mod-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim); flex-shrink: 0; } -.boot-mod.ok { color: var(--can-hi); border-color: rgba(61,220,142,.4); background: rgba(61,220,142,.08); } -.boot-mod.ok .boot-mod-dot { background: var(--green); } -.boot-mod.fail { color: #ff9d9d; border-color: rgba(255,107,107,.4); background: rgba(255,107,107,.08); } -.boot-mod.fail .boot-mod-dot { background: var(--red); } -.boot-mod.fail.optional { color: var(--gd32-hi); border-color: rgba(252,191,63,.4); background: rgba(252,191,63,.08); } -.boot-mod.fail.optional .boot-mod-dot { background: var(--amber); } - -.status-section-title { - margin-top: 15px; margin-bottom: 9px; - font-family: var(--font-display); - font-size: 11px; color: var(--text-label); - text-transform: uppercase; letter-spacing: .12em; font-weight: 600; -} - -/* ================= 堆概况 / CPU / 任务栈 ================= */ -.heap-group { - background: rgba(255,255,255,.02); - border: 1px solid var(--border); border-radius: 9px; - padding: 13px 15px; margin-bottom: 10px; -} -.heap-group:last-child { margin-bottom: 0; } -.cpu-group { margin-bottom: 8px; } -.cpu-task-list { margin-bottom: 12px; } -.cpu-task-list .status-task { background: rgba(255,255,255,.025); } -.cpu-fill { background: linear-gradient(90deg, var(--ble), var(--esp32)); } - -.heap-group-title { - font-family: var(--font-display); - font-size: 11px; font-weight: 600; color: var(--text-label); - text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px; -} -.heap-bar-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; } -.heap-bar-text { - order: 1; flex: 1 1 auto; min-width: 0; - font-family: var(--font-display); - font-size: 13px; font-weight: 600; white-space: nowrap; color: var(--text); -} -.heap-bar-pct { - order: 2; flex: 0 0 auto; - font-family: var(--font-mono); - font-size: 12px; font-weight: 600; min-width: 44px; text-align: right; - font-variant-numeric: tabular-nums; color: var(--text-dim); -} -.heap-bar-track { - order: 3; flex: 1 1 100%; width: 100%; height: 14px; - background: rgba(255,255,255,.06); border-radius: 7px; - overflow: hidden; position: relative; -} -.heap-bar-fill { - display: block; height: 100%; border-radius: 7px; - background: linear-gradient(90deg, var(--esp32), #5eead4); - transition: width .5s cubic-bezier(.4,0,.2,1); -} -.heap-bar-fill.warn { background: linear-gradient(90deg, var(--amber), #d97706); } -.heap-bar-fill.danger { background: linear-gradient(90deg, var(--red), #dc2626); } - -.heap-detail-row { - display: flex; gap: 16px; flex-wrap: wrap; - font-size: 11px; color: var(--text-dim); - margin-top: 7px; padding-top: 7px; - border-top: 1px solid rgba(255,255,255,.05); -} -.heap-detail-row span { display: inline-flex; align-items: center; gap: 4px; } -.heap-detail-row b { color: var(--text); font-weight: 500; font-family: var(--font-mono); font-size: 11px; } - -/* ---- SRAM 分段条 ---- */ -.mem-sram-track { display: flex; overflow: hidden; gap: 0; background: rgba(255,255,255,.04); } -.mem-seg { display: block; height: 100%; min-width: 0; transition: width .5s ease; } -.mem-seg-iram { background: linear-gradient(90deg, var(--mem), #7c5ce0); } -.mem-seg-data { background: linear-gradient(90deg, #fb923c, #ea580c); } -.mem-seg-bss { background: linear-gradient(90deg, #94a3b8, #64748b); } -.mem-seg-static { background: linear-gradient(90deg, #64748b, #475569); } -.mem-seg-used { background: linear-gradient(90deg, var(--esp32), #5eead4); } -.mem-seg-free { background: rgba(255,255,255,.08); } -.heap-legend { display: flex; flex-wrap: wrap; gap: 8px 14px; margin-top: 9px; font-size: 11px; color: var(--text-dim); } -.heap-legend-item { display: inline-flex; align-items: center; gap: 5px; } -.heap-legend-item b { color: var(--text); font-weight: 500; font-family: var(--font-mono); font-size: 11px; } -.mem-dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: 0 0 auto; } -.mem-dot-iram { background: var(--mem); } -.mem-dot-data { background: #f97316; } -.mem-dot-bss { background: #64748b; } -.mem-dot-static { background: #64748b; } -.mem-dot-used { background: var(--esp32); } -.mem-dot-free { background: rgba(255,255,255,.18); border: 1px solid rgba(255,255,255,.22); box-sizing: border-box; } -.mem-static-list { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: var(--text-dim); } -.mem-static-row { - display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: center; - padding: 3px 0; border-bottom: 1px solid rgba(255,255,255,.04); -} -.mem-static-row:last-child { border-bottom: 0; } -.mem-static-name { color: var(--text-label); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.mem-static-size { font-family: var(--font-mono); color: var(--text); font-variant-numeric: tabular-nums; } -.mem-static-pct { font-family: var(--font-mono); min-width: 42px; text-align: right; color: var(--text-dim); } -.heap-hint { - margin-top: 8px; padding-top: 6px; - border-top: 1px solid rgba(255,255,255,.05); - font-size: 10px; line-height: 1.5; color: var(--text-dim); opacity: .9; -} - -/* ---- 任务列表 ---- */ -.status-task-list { display: grid; gap: 8px; } -.status-task-list.list-scroll-top3 { - --status-task-row-est: 6.9rem; - max-height: calc(3 * var(--status-task-row-est) + 2 * 8px); - overflow-y: auto; overflow-x: hidden; - padding-right: 4px; scrollbar-gutter: stable; overscroll-behavior: contain; -} -.status-task-list.list-scroll-top3::-webkit-scrollbar { width: 6px; } -.status-task-list.list-scroll-top3::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 999px; } -.status-task-list.list-scroll-top3::-webkit-scrollbar-track { background: transparent; } -.status-task { - background: var(--bg-elevated); border: 1px solid var(--border); - border-radius: var(--radius-sm); padding: 11px 13px; -} -.status-task-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; } -.status-task-name { font-size: 12px; font-weight: 700; color: var(--text); font-family: var(--font-mono); } -.status-task-ratio { font-size: 11px; color: var(--text-dim); font-family: var(--font-mono); } -.status-task-bar { width: 100%; height: 8px; border-radius: 999px; background: var(--bg-input); overflow: hidden; margin-bottom: 8px; } -.status-task-bar-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--esp32), #14b8a6); } -.status-task-bar-fill.warn { background: linear-gradient(90deg, var(--amber), #d97706); } -.status-task-bar-fill.err { background: linear-gradient(90deg, var(--red), #dc2626); } -.status-task-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } -.status-task-meta span { - display: block; font-family: var(--font-display); - font-size: 10px; color: var(--text-label); - text-transform: uppercase; letter-spacing: .07em; -} -.status-task-meta b { display: block; margin-top: 2px; color: var(--text); font-size: 11px; font-family: var(--font-mono); } - -/* ---- 二级折叠 + 原始 JSON ---- */ -.status-subfold { margin-top: 15px; } -.status-subfold-head { - display: flex; align-items: center; justify-content: space-between; gap: 8px; - cursor: pointer; user-select: none; padding: 6px 0; border-radius: var(--radius-sm); -} -.status-subfold-head:hover { color: var(--text); } -.status-subfold-title { margin: 0 !important; flex: 1; } -.status-subfold-head .collapse-arrow { flex: none; font-size: 10px; color: var(--text-dim); transition: transform .15s ease; } -.status-subfold.collapsed .collapse-arrow { transform: rotate(-90deg); } -.status-subfold.collapsed .status-subfold-body { display: none; } -.status-subfold-body { margin-top: 6px; } -.status-json { - margin-top: 0; padding: 12px; - border-radius: var(--radius-sm); border: 1px solid var(--border); - background: var(--bg-input); color: var(--text-dim); - font-size: 11px; line-height: 1.65; - max-height: 280px; overflow: auto; - white-space: pre-wrap; word-break: break-word; - font-family: var(--font-mono); resize: vertical; min-height: 80px; -} -.status-empty { - padding: 13px; border: 1px dashed var(--border-strong); - border-radius: var(--radius-sm); color: var(--text-dim); font-size: 12px; -} - -/* 抽屉内强制等宽 */ -.sys-drawer-body .heap-bar-pct, -.sys-drawer-body .heap-detail-row b, -.sys-drawer-body .status-task-name, -.sys-drawer-body .status-task-ratio, -.sys-drawer-body .status-task-meta b, -.sys-drawer-body .status-json { font-family: var(--font-mono); } -.sys-drawer-body .status-section-title, -.sys-drawer-body .heap-group-title, -.sys-drawer-body .heap-bar-text { font-family: var(--font-display); color: inherit; } -.sys-drawer-body .status-section-title { color: var(--text-label); font-size: 11px; font-weight: 600; } -.sys-drawer-body .heap-bar-text { color: var(--text); font-size: 13px; font-weight: 600; } -.sys-drawer-body .status-json { color: var(--text-dim); font-size: 11px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; } - -/* ================= OTA 阶段流 ================= */ -.stage-flow { display: flex; align-items: flex-start; gap: 0; padding: 10px 0 6px; } -.stage-node { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 7px; position: relative; } -.stage-node:not(:last-child)::after { - content: ""; position: absolute; top: 19px; left: calc(50% + 20px); - width: calc(100% - 40px); height: 2px; - background: var(--border); transition: background .3s; -} -.stage-node.done:not(:last-child)::after, -.stage-node.live:not(:last-child)::after { background: var(--esp32); } -.stage-dot { - width: 38px; height: 38px; border-radius: 50%; - background: var(--bg-input); border: 2px solid var(--border-strong); - display: flex; align-items: center; justify-content: center; - font-family: var(--font-display); font-size: 16px; font-weight: 700; - color: var(--text-label); - z-index: 1; transition: all .3s; -} -.stage-node.live .stage-dot { - background: linear-gradient(135deg, var(--esp32), #14b8a6); - border-color: var(--esp32); color: #04231e; - box-shadow: 0 0 16px rgba(47,216,193,.45); -} -.stage-node.done .stage-dot { - background: linear-gradient(135deg, var(--green), #16a374); - border-color: var(--green); color: #03231a; - box-shadow: 0 0 12px rgba(61,220,142,.35); -} -.stage-node.err .stage-dot { - background: linear-gradient(135deg, var(--red), #dc4c4c); - border-color: var(--red); color: #fff; - box-shadow: 0 0 12px rgba(255,107,107,.4); -} -.stage-label { - font-family: var(--font-display); - font-size: 11px; color: var(--text-label); text-align: center; line-height: 1.3; - letter-spacing: .02em; -} -.stage-detail { - font-size: 10px; color: var(--esp32-hi); text-align: center; line-height: 1.2; - margin-top: 2px; min-height: 14px; - font-family: var(--font-mono); transition: color .3s; -} -.stage-node.live .stage-label, -.stage-node.done .stage-label, -.stage-node.err .stage-label { color: var(--text); } -.stage-node.live .stage-detail { color: var(--esp32-hi); } -.stage-node.done .stage-detail { color: var(--green); } -.stage-node.err .stage-detail { color: var(--red); } - -/* ---- 阶段状态条 ---- */ -.stage-status-bar { - display: flex; align-items: center; gap: 10px; - margin-top: 15px; padding: 11px 17px; - border-radius: 999px; - background: var(--bg-elevated); border: 1px solid var(--border); - transition: all .3s; -} -.stage-status-bar .ssb-dot { - width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; - background: var(--text-dim); animation: dotPulse 2.5s ease-in-out infinite; -} -.stage-status-bar .ssb-text { - font-family: var(--font-display); - color: var(--text-dim); font-size: 13px; font-weight: 600; letter-spacing: .02em; -} -@keyframes dotPulse { 0%,100% { opacity: .45;} 50% { opacity: .85;} } -.stage-status-bar.active { - border-color: rgba(47,216,193,.35); - background: linear-gradient(135deg, rgba(47,216,193,.09), rgba(47,216,193,.02)); -} -.stage-status-bar.active .ssb-dot { - background: var(--esp32); box-shadow: 0 0 8px var(--esp32-dim); - animation: dotPulseLive 1.2s ease-in-out infinite; -} -.stage-status-bar.active .ssb-text { color: var(--esp32-hi); } -@keyframes dotPulseLive { 0%,100% { opacity: .7; transform: scale(1);} 50% { opacity: 1; transform: scale(1.3);} } -.stage-status-bar.ok { - border-color: rgba(61,220,142,.35); - background: linear-gradient(135deg, rgba(61,220,142,.09), rgba(61,220,142,.02)); -} -.stage-status-bar.ok .ssb-dot { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.4); animation: none; } -.stage-status-bar.ok .ssb-text { color: var(--green); } -.stage-status-bar.err { - border-color: rgba(255,107,107,.35); - background: linear-gradient(135deg, rgba(255,107,107,.09), rgba(255,107,107,.02)); -} -.stage-status-bar.err .ssb-dot { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.4); animation: none; } -.stage-status-bar.err .ssb-text { color: var(--red); } - -/* ---- 校验信息面板 ---- */ -.stage-verify-info { - margin-top: 12px; padding: 13px 15px; - border-radius: var(--radius-sm); - background: var(--bg-elevated); border: 1px solid var(--border); -} -.svi-row { - display: flex; align-items: flex-start; gap: 12px; - padding: 7px 0; font-size: 12px; - border-bottom: 1px solid rgba(255,255,255,.04); -} -.svi-row:last-child { border-bottom: none; } -.svi-key { - color: var(--text-label); font-weight: 600; - font-family: var(--font-display); - text-transform: uppercase; letter-spacing: .07em; font-size: 10px; - min-width: 104px; flex-shrink: 0; padding-top: 2px; -} -.svi-val { color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; word-break: break-all; } -.svi-val.ok { color: var(--green); font-weight: 600; } -.svi-val.err { color: var(--red); font-weight: 600; } - -/* ---- 阶段结果面板 ---- */ -.stage-result { - margin-top: 12px; padding: 15px 17px; - border-radius: var(--radius-sm); - background: var(--bg-elevated); border: 1px solid var(--border); - display: flex; flex-direction: column; gap: 10px; -} -.stage-result.ok { border-color: rgba(61,220,142,.5); background: rgba(61,220,142,.08); } -.stage-result.err { border-color: rgba(255,107,107,.5); background: rgba(255,107,107,.08); } -.stage-result .sr-head { display: flex; align-items: center; gap: 10px; } -.stage-result .sr-ico { - width: 32px; height: 32px; border-radius: 50%; - display: inline-flex; align-items: center; justify-content: center; - font-size: 18px; font-weight: 700; color: #04231e; -} -.stage-result.ok .sr-ico { background: var(--green); } -.stage-result.err .sr-ico { background: var(--red); color: #fff; } -.stage-result .sr-title { font-family: var(--font-display); font-size: 16px; font-weight: 700; } -.stage-result.ok .sr-title { color: var(--green); } -.stage-result.err .sr-title { color: var(--red); } -.stage-result .sr-detail { font-size: 13px; color: var(--text); line-height: 1.55; } -.stage-result .sr-actions { display: flex; justify-content: flex-end; gap: 8px; } - -/* ================= 日志面板 ================= */ -.log-panel { display: flex; flex-direction: column; min-height: 380px; } -.log-panel .card-body { flex: 1; display: flex; flex-direction: column; padding: 0; } -.log-toolbar { - display: flex; align-items: center; justify-content: space-between; - padding: 11px 16px; border-bottom: 1px solid var(--border-subtle); - gap: 10px; flex-wrap: wrap; -} -.log-filters { display: flex; gap: 6px; } -.log-filter { - padding: 5px 12px; border-radius: 999px; - font-family: var(--font-display); - font-size: 11px; font-weight: 600; letter-spacing: .03em; - border: 1px solid var(--border); background: transparent; color: var(--text-dim); - cursor: pointer; transition: all .18s; -} -.log-filter.active { - background: linear-gradient(135deg, var(--esp32), #14b8a6); - color: #04231e; border-color: var(--esp32); - box-shadow: 0 2px 10px rgba(47,216,193,.3); -} -.log-filter:hover:not(.active) { border-color: var(--border-strong); color: var(--text); } - -.log-scroll { - flex: 1; overflow-y: auto; padding: 12px 16px; - font-family: var(--font-mono); font-size: 12px; line-height: 1.7; - max-height: 420px; background: rgba(4,8,16,.35); -} -.log-scroll::-webkit-scrollbar { width: 5px; } -.log-scroll::-webkit-scrollbar-track { background: transparent; } -.log-scroll::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } - -.log-line { - display: flex; gap: 9px; padding: 3px 0; - border-bottom: 1px solid rgba(255,255,255,.025); -} -.log-line .lt { color: var(--text-label); flex-shrink: 0; } -.log-line .lv { font-weight: 700; min-width: 20px; flex-shrink: 0; } -.log-line .lv.E { color: var(--red); } -.log-line .lv.W { color: var(--amber); } -.log-line .lv.I { color: var(--esp32); } -.log-line .lv.D { color: var(--text-label); } -.log-line .lg, .log-line .tag { color: var(--mem-hi); flex-shrink: 0; } -.log-line .lm { color: var(--text); word-break: break-all; } -.log-line .t { color: var(--text-label); flex-shrink: 0; } - -.log-foot { - display: flex; justify-content: space-between; align-items: center; - padding: 9px 16px; border-top: 1px solid var(--border-subtle); - font-size: 11px; color: var(--text-label); -} - -/* ================= Toast ================= */ -.toast { - position: fixed; top: 45%; left: 50%; - transform: translateX(-50%) translateY(30px); - z-index: 9999; - display: flex; align-items: center; gap: 11px; - padding: 14px 26px; border-radius: 12px; - font-family: var(--font-display); - font-size: 14px; font-weight: 600; letter-spacing: .02em; - color: #fff; pointer-events: none; opacity: 0; - transition: all .35s cubic-bezier(.4,0,.2,1); - box-shadow: 0 12px 40px rgba(2,5,12,.55); - border: 1px solid rgba(255,255,255,.12); -} -.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; } -.toast.ok { background: linear-gradient(135deg, #16a374, #0e8a5f); } -.toast.err { background: linear-gradient(135deg, #dc4c4c, #c03535); } -.toast.warn { background: linear-gradient(135deg, #d98a06, #b87405); } -.toast .toast-ico { font-size: 18px; } - -/* ================= 杂项 ================= */ -.row-2, .row-3, .row-4 { display: grid; gap: 10px; } -.row-2 { grid-template-columns: 1fr 1fr; } -.row-3 { grid-template-columns: repeat(3, 1fr); } -.row-4 { grid-template-columns: repeat(4, 1fr); } -.sub-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; } -.footer-note { margin-top: 12px; font-size: 12px; line-height: 1.7; color: var(--text-dim); } -code { color: var(--esp32-hi); font-family: var(--font-mono); } - -/* ================= 自定义 Tooltip(pill)================= */ -.pill[data-tooltip] { position: relative; cursor: help; } -.pill[data-tooltip]:hover::after { - content: attr(data-tooltip); - position: absolute; top: calc(100% + 10px); left: 50%; - transform: translateX(-50%); - background: var(--bg-elevated); color: var(--text); - font-size: 11px; font-weight: 400; font-family: var(--font-mono); - padding: 6px 12px; border-radius: 7px; - border: 1px solid var(--border-strong); - box-shadow: 0 6px 20px rgba(2,5,12,.5); - white-space: nowrap; z-index: 1000; pointer-events: none; - animation: tooltipFadeIn .15s ease-out; -} -.pill[data-tooltip]:hover::before { - content: ''; position: absolute; top: calc(100% + 4px); left: 50%; - transform: translateX(-50%); - border: 6px solid transparent; border-bottom-color: var(--border-strong); - z-index: 1001; pointer-events: none; - animation: tooltipFadeIn .15s ease-out; -} -@keyframes tooltipFadeIn { - from { opacity: 0; transform: translateX(-50%) translateY(3px); } - to { opacity: 1; transform: translateX(-50%) translateY(0); } -} - -/* ================= 堆低水位 Tooltip ================= */ -.heap-low-tooltip { - position: fixed; z-index: 9999; - min-width: 480px; max-width: 640px; - padding: 11px 13px 9px; - background: var(--bg-elevated); - border: 1px solid var(--border-strong); - border-radius: 9px; - box-shadow: 0 8px 28px rgba(2,5,12,.6); - font-size: 11px; color: var(--text); - pointer-events: auto; visibility: hidden; -} -.heap-low-title { font-weight: 600; margin-bottom: 6px; color: var(--text); display: flex; align-items: center; } -.heap-low-sub { font-weight: 400; color: var(--text-dim); margin-left: 6px; } -.heap-low-clear { - margin-left: auto; margin-right: 6px; height: 22px; padding: 0 9px; - border: 1px solid rgba(255,107,107,.45); border-radius: 5px; - background: rgba(255,107,107,.12); color: #ff9d9d; - font-size: 11px; line-height: 20px; cursor: pointer; - transition: background .12s, color .12s, border-color .12s; -} -.heap-low-clear:hover:not(:disabled) { background: rgba(255,107,107,.22); border-color: rgba(255,107,107,.7); color: #ffcdd2; } -.heap-low-clear:disabled { opacity: .55; cursor: wait; } -.heap-low-close { - width: 22px; height: 22px; padding: 0; border: none; border-radius: 5px; - background: transparent; color: var(--text-dim); - font-size: 18px; line-height: 1; cursor: pointer; - transition: background .12s, color .12s; -} -.heap-low-close:hover { background: rgba(255,255,255,.08); color: var(--text); } -.heap-low-table-wrap { max-height: 360px; overflow-y: auto; } -.heap-low-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 10.5px; } -.heap-low-section { margin-bottom: 6px; } -.heap-low-section + .heap-low-section { margin-top: 8px; padding-top: 6px; border-top: 1px dashed rgba(255,255,255,.15); } -.heap-low-section-title { - font-size: 11px; font-weight: 600; color: var(--text-dim); - padding: 4px 6px 2px; background: rgba(255,255,255,.03); border-radius: 3px 3px 0 0; -} -.heap-low-section-hist .heap-low-section-title { color: var(--gd32-hi); } -.heap-low-section-hist .heap-low-table { opacity: .92; } -.heap-low-table th, .heap-low-table td { - padding: 3px 6px; text-align: right; white-space: nowrap; - border-bottom: 1px solid rgba(255,255,255,.04); -} -.heap-low-table th { color: var(--text-dim); font-weight: 500; position: sticky; top: 0; background: var(--bg-elevated); text-align: right; } -.ble-low-table td:nth-child(5), .ble-low-table td:nth-child(6), -.ble-low-table td:nth-child(7), .ble-low-table td:nth-child(9) { text-align: left; } -.ble-low-table td:first-child { color: var(--text-dim); } - -/* ================= BLE 连接按钮(launch 风格) ================= - 圆形物理按键:未连接=暗淡待机;勾选=已连接,蓝光环绕 + 文字波浪动画。 - checkbox 隐藏,由 JS setConnectionState 驱动 checked,避免异步连接失败导致状态错位。 */ -.ble-launch-wrap { - display: flex; flex-direction: column; align-items: center; - gap: 12px; padding: 6px 0 2px; -} -.ble-launch-btn { - cursor: pointer; position: relative; - display: flex; flex-direction: column; align-items: center; - margin: 0; -} -.ble-launch-btn input { - opacity: 0; width: 0; height: 0; display: none; -} -/* —— 三态:idle 待机 / connecting 连接中转圈 / connected 已连接变亮 —— */ -/* 默认 text3 隐藏 */ -.ble-launch-btn .back .mid .ble-dial .center .start .text3 { - opacity: 0; -} - -/* connecting:仅光晕转圈(loading),按钮整体保持暗淡 */ -.ble-launch-btn[data-ble-state="connecting"] .back .spot-shadow, -.ble-launch-btn[data-ble-state="connecting"] .back .mid .spot-main, -.ble-launch-btn[data-ble-state="connecting"] .back .mid .ble-dial .spot-light { - opacity: 0.7; - animation: ble-spot-rot 1.4s linear infinite; -} -.ble-launch-btn[data-ble-state="connecting"] .back .mid .ble-dial .center .start .text1 { - opacity: 0; -} -.ble-launch-btn[data-ble-state="connecting"] .back .mid .ble-dial .center .start .text3 { - opacity: 1; - color: #9bb8d8; - animation: ble-text-pulse 1.2s ease-in-out infinite; -} - -/* connected:整个按钮变亮 + 静态整圈蓝光晕(不转圈)+ 文字波浪 */ -/* spot 元素默认是横向条状(为转圈动画设计),connected 时改成整圈圆环 */ -.ble-launch-btn[data-ble-state="connected"] .back .spot-shadow { - transition: opacity 1.2s ease; - opacity: 0.9; - width: 106px; height: 106px; - border-radius: 50%; - filter: blur(12px) brightness(1.2); -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .spot-main { - transition: opacity 1.2s ease; - opacity: 0.95; - width: 100px; height: 100px; - border-radius: 50%; - filter: blur(9px) brightness(1.6); -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .spot-light { - transition: opacity 1.2s ease; - opacity: 0.9; - width: 92px; height: 92px; - border-radius: 50%; - filter: blur(8px) brightness(1.2); -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .icon svg { - filter: brightness(1.15); -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .launch { - color: #e9f0fa; -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .launch > div { - animation: ble-wave-text 1.8s calc(var(--i) * -0.13s) infinite; -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .start .text1 { - transition: transform 0.5s ease, opacity 0.5s ease; - transform: translateX(10px); - opacity: 0; -} -.ble-launch-btn[data-ble-state="connected"] .back .mid .ble-dial .center .start .text2 { - opacity: 1; - animation: ble-text2 0.5s forwards; -} -.ble-launch-btn:active .back .mid .ble-dial { - transform: scale(0.94); - box-shadow: - 0 -1px 2px rgba(255,255,255,0.3), - inset 0 0 10px rgba(0,0,0,0.24), - inset 0 -13px 26px rgba(0,0,0,0.8); -} - -/* —— 外圈金属环 —— */ -.ble-launch-btn .back { - position: relative; z-index: 200; - width: 128px; height: 128px; - display: flex; justify-content: center; align-items: center; - border-radius: 50%; - background-image: linear-gradient(0, #5e5e61 20%, #212123); - box-shadow: - inset 0 1px 1px rgba(255,255,255,0.3), - inset 0 3px 5px rgba(255,255,255,0.6), - inset 0 -4px 2px rgba(255,255,255,0.05), - inset 0 -2px 0 rgba(0,0,0,0.15), - inset -2px 4px 0 rgba(0,0,0,0.3), - inset 2px 4px 0 rgba(0,0,0,0.3), - 0 -7px 16px 2px rgba(255,255,255,0.18), - 0 4px 16px 2px rgba(0,0,0,0.5), - 0 2px 5px #7c7c7c; - cursor: pointer; -} -.ble-launch-btn .back .spot-shadow { - position: absolute; - width: 96px; height: 27px; - background: rgba(95,168,255,0.9); - opacity: 0; - filter: blur(14px) brightness(1.1); -} -.ble-launch-btn .back .mid { - overflow: hidden; position: relative; - width: 106px; height: 106px; - display: flex; justify-content: center; align-items: center; - border-radius: 50%; - background: #000; - box-shadow: 0 0 4px #000, 0 1px 2px rgba(255,255,255,0.8); -} -.ble-launch-btn .back .mid::before { - content: ''; position: absolute; z-index: 500; - width: 100%; height: 100%; border-radius: 50%; - background: transparent; - box-shadow: inset 0 -12px 18px rgba(0,0,0,0.8); - pointer-events: none; -} -.ble-launch-btn .back .mid .spot-main { - position: absolute; - width: 110px; height: 20px; - background: #5fa8ff; - opacity: 0; - filter: blur(11px) brightness(1.5); -} -.ble-launch-btn .back .mid .ble-dial { - position: relative; z-index: 90; - display: flex; justify-content: center; align-items: center; - width: 100px; height: 100px; border-radius: 50%; - background: #424242; - box-shadow: - 0 -1px 2px rgba(255,255,255,0.5), - inset 0 -9px 18px rgba(0,0,0,0.8); - transition: transform 0.2s ease, box-shadow 0.2s ease; -} -.ble-launch-btn .back .mid .ble-dial .spot-light { - position: absolute; - width: 110px; height: 20px; - opacity: 0; - background: rgba(95,168,255,0.6); - filter: blur(14px) brightness(1.1); -} -.ble-launch-btn .back .mid .ble-dial .center { - position: relative; z-index: 100; - width: 86px; height: 86px; - display: flex; flex-direction: column; - align-items: center; justify-content: center; gap: 2px; - border-radius: 50%; - background: #232323; - cursor: pointer; -} -.ble-launch-btn .back .mid .ble-dial .center .icon svg { - margin-top: -3px; - width: 18px; - filter: brightness(1) grayscale(1); - transition: filter 1.5s ease; -} -.ble-launch-btn .back .mid .ble-dial .center .launch { - display: flex; margin-top: -3px; - font-size: 13px; color: #595959; - font-family: var(--font-display); - letter-spacing: 0.04em; -} -.ble-launch-btn .back .mid .ble-dial .center .start { - position: relative; - width: 100%; height: 11px; - font-size: 9px; color: #737373; - text-align: center; - font-family: var(--font-display); -} -.ble-launch-btn .back .mid .ble-dial .center .start > div { - position: absolute; left: 50%; top: 0; - transform: translateX(-50%); - white-space: nowrap; -} -.ble-launch-btn .back .mid .ble-dial .center .start .text2 { - opacity: 0; - display: inline-flex; -} -.ble-launch-btn .back .mid .ble-dial .center .start .text2 > span { - animation: ble-wave-text 1.8s calc(var(--n) * -0.13s) infinite; -} -.ble-launch-hint { - font-size: 11px; color: var(--text-dim); - letter-spacing: 0.04em; -} - -@keyframes ble-wave-text { - 0% { opacity: 0.7; } - 50% { color: #c6e0ff; opacity: 1; } - 100% { opacity: 0.7; } -} -@keyframes ble-spot-rot { - 0% { transform: rotate(0); } - 100% { transform: rotate(360deg); } -} -@keyframes ble-text2 { - 0% { transform: translateX(-80%); } - 100% { transform: translateX(-50%); } -} -@keyframes ble-text-pulse { - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } -} -.heap-low-table tbody tr:hover { background: rgba(255,255,255,.03); } -.heap-low-table td.has-suspect { color: var(--red); font-weight: 500; } -.heap-low-table td.idle-suspect { color: var(--gd32-hi); } -.heap-low-detail-meta { display: flex; gap: 18px; font-size: 10px; color: var(--text-dim); margin-bottom: 6px; } -.heap-low-detail-meta b { color: var(--text); font-family: var(--font-mono); } -.heap-low-hook-meta { - margin: 8px 0 6px; padding: 6px 9px; - background: rgba(255,107,107,.08); border-left: 2px solid var(--red); - display: flex; flex-wrap: wrap; gap: 12px; - font-size: 10px; color: var(--text-dim); -} -.heap-low-hook-title { width: 100%; color: var(--red); font-weight: 600; margin-bottom: 2px; } -.heap-low-hook-meta b { color: var(--text); font-family: var(--font-mono); } -.heap-low-caller { color: var(--red); } -.heap-low-caller-hint { color: var(--text-dim); font-size: 9px; } -.heap-low-main-row { cursor: pointer; } -.heap-low-expand-hint { font-size: 9px; color: var(--text-dim); margin-left: 4px; } -.heap-low-stack-detail > td { padding: 6px 8px 8px; background: rgba(0,0,0,.2); } -.heap-low-stack-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 10px; } -.heap-low-stack-table th, .heap-low-stack-table td { padding: 2px 6px; text-align: left; border-bottom: 1px solid rgba(255,255,255,.03); } -.heap-low-stack-table th { color: var(--text-dim); font-weight: 500; } -.heap-low-stack-table td:last-child { text-align: right; } -.heap-low-stack-table tr.crit td { color: var(--red); } -.heap-low-stack-table tr.warn td { color: var(--gd32-hi); } -.heap-low-empty { padding: 14px 6px; color: var(--text-dim); text-align: center; } -.heap-low-tip { margin-top: 6px; font-size: 10px; color: var(--text-dim); } diff --git a/tools/css/layout.css b/tools/css/layout.css deleted file mode 100644 index 5fe2922..0000000 --- a/tools/css/layout.css +++ /dev/null @@ -1,337 +0,0 @@ -/* ============================================================ - layout.css — 命令栏头部 / 主布局 / 卡片 / 表单 / 按钮 / 侧滑抽屉 - ============================================================ */ - -/* ================= HEADER — 命令栏 ================= */ -.header-bar { - position: relative; - z-index: 20; /* 高于下方卡片,保证 pill tooltip 不被遮挡 */ - display: flex; align-items: center; gap: 16px; - padding: 16px 22px; margin-bottom: 26px; - background: - radial-gradient(620px 240px at 88% -40%, rgba(47,216,193,.12), transparent 60%), - linear-gradient(160deg, var(--bg-panel) 0%, var(--bg-card) 70%); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-card); - flex-wrap: wrap; -} -/* 顶部三色霓虹描边:ESP32 青绿 → BLE 蓝 → GD32 琥珀,一条细线点出子系统谱系 */ -.header-bar::before { - content: ""; - position: absolute; top: 0; left: 0; right: 0; height: 2px; - background: linear-gradient(90deg, - var(--esp32) 0%, var(--ble) 38%, var(--gd32) 78%, transparent 100%); - opacity: .85; - border-radius: var(--radius-lg) var(--radius-lg) 0 0; -} - -.header-brand { - display: flex; align-items: center; gap: 12px; - padding-right: 20px; border-right: 1px solid var(--border); - white-space: nowrap; -} -.brand-logo { - width: 40px; height: 40px; flex-shrink: 0; - display: inline-flex; align-items: center; justify-content: center; - border-radius: 11px; - background: linear-gradient(145deg, rgba(47,216,193,.16), rgba(95,168,255,.10)); - border: 1px solid rgba(47,216,193,.3); - box-shadow: 0 0 18px rgba(47,216,193,.18), inset 0 0 12px rgba(47,216,193,.08); - color: var(--esp32); -} -.brand-logo svg { width: 24px; height: 24px; display: block; } -.brand-text { display: flex; flex-direction: column; line-height: 1.15; } -.brand-text strong { - font-family: var(--font-display); - font-size: 17px; font-weight: 700; letter-spacing: .02em; - background: linear-gradient(92deg, var(--text) 30%, var(--esp32-hi) 100%); - -webkit-background-clip: text; background-clip: text; - -webkit-text-fill-color: transparent; -} -.brand-sub { - font-family: var(--font-mono); - font-size: 10px; letter-spacing: .22em; text-transform: uppercase; - color: var(--text-label); margin-top: 2px; -} -/* 兼容旧结构:独立 dot-mark(无 brand-logo 时)仍显示为一颗呼吸 LED */ -.header-brand .dot-mark { - width: 10px; height: 10px; border-radius: 50%; - background: var(--esp32); - box-shadow: 0 0 10px var(--esp32-dim); - flex-shrink: 0; -} -.header-brand strong { font-size: 16px; font-weight: 700; } - -.header-status { - display: flex; align-items: center; gap: 8px; - flex-wrap: wrap; -} -.header-sub { - display: flex; gap: 10px; margin-left: auto; - flex-wrap: wrap; -} - -/* ---- 状态胶囊 ---- */ -.pill { - display: inline-flex; align-items: center; gap: 7px; - padding: 6px 13px; border-radius: 999px; - font-size: 12px; font-weight: 500; - font-family: var(--font-mono); - background: var(--bg-elevated); border: 1px solid var(--border); - color: var(--text-dim); - white-space: nowrap; - transition: border-color .2s, background .2s, transform .2s; -} -.pill .indicator { - width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; - background: var(--text-label); -} -.pill.online .indicator { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); } -.pill.offline .indicator { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); } -.pill.processing .indicator, -.pill.live .indicator { background: var(--amber); animation: blink 1s infinite; } -.pill.idle .indicator { background: var(--text-dim); } -.pill.online { color: var(--can-hi); border-color: rgba(61,220,142,.35); } -.pill.offline { color: #ffb3b3; border-color: rgba(255,107,107,.3); } -.pill.processing, -.pill.live { color: var(--gd32-hi); border-color: rgba(252,191,63,.35); } -@keyframes blink { 50% { opacity: .3; } } - -.pill-val { - font-weight: 700; color: var(--text); - font-family: var(--font-mono); -} - -/* ================= 主布局 ================= */ -.main-layout { - display: grid; - grid-template-columns: 400px 1fr; - gap: var(--gap); - align-items: start; -} - -/* ================= 卡片 ================= */ -.card { - position: relative; - background: linear-gradient(170deg, var(--bg-card) 0%, rgba(15,23,38,.92) 100%); - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - box-shadow: var(--shadow-card); -} -.card + .card { margin-top: var(--gap); } - -/* 子系统色带:卡片顶部 2px 渐变,由修饰类决定颜色 */ -.card::before { - content: ""; - position: absolute; top: 0; left: 0; right: 0; height: 2px; - background: linear-gradient(90deg, var(--card-accent, var(--esp32)), transparent 85%); - opacity: .7; pointer-events: none; -} -.card--esp32 { --card-accent: var(--esp32); --card-accent-dim: var(--esp32-dim); } -.card--gd32 { --card-accent: var(--gd32); --card-accent-dim: var(--gd32-dim); } -.card--ble { --card-accent: var(--ble); --card-accent-dim: var(--ble-dim); } -.card--can { --card-accent: var(--can); --card-accent-dim: var(--can-dim); } -.card--motor { --card-accent: var(--motor); --card-accent-dim: var(--motor-dim); } -.card--mem { --card-accent: var(--mem); --card-accent-dim: var(--mem-dim); } -.card--status { --card-accent: var(--green); --card-accent-dim: var(--can-dim); } - -.card-head { - display: flex; align-items: center; gap: 10px; - padding: 14px 18px; - border-bottom: 1px solid var(--border-subtle); - font-family: var(--font-display); - font-size: 13px; font-weight: 600; - color: var(--text); - letter-spacing: .05em; - cursor: pointer; - user-select: none; -} -/* 图标芯片:圆角方块底色取自卡片子系统色 */ -.card-head .ico { - width: 28px; height: 28px; flex-shrink: 0; - display: inline-flex; align-items: center; justify-content: center; - font-size: 15px; - border-radius: 8px; - background: var(--card-accent-dim, var(--esp32-dim)); - border: 1px solid color-mix(in srgb, var(--card-accent, var(--esp32)) 30%, transparent); -} -.card-head-btn { margin-left: auto; padding: 4px 10px; position: relative; z-index: 2; } -.card-body { padding: 17px 18px; } -.card.collapsed .card-body { display: none; } -.card-head .collapse-arrow { - margin-left: auto; font-size: 10px; color: var(--text-label); - transition: transform .2s; -} -.card.collapsed .collapse-arrow { transform: rotate(-90deg); } - -/* ================= 表单 ================= */ -.form-row { margin-bottom: 14px; } -.form-row:last-child { margin-bottom: 0; } -.form-label { - display: block; - font-family: var(--font-display); - font-size: 11px; color: var(--text-label); - text-transform: uppercase; letter-spacing: .12em; - margin-bottom: 7px; -} -.input, select, textarea { - width: 100%; padding: 10px 13px; - font-size: 13px; font-family: var(--font-mono); - background: var(--bg-input); color: var(--text); - border: 1px solid var(--border); border-radius: var(--radius-sm); - outline: none; - transition: border-color .18s, box-shadow .18s, background .18s; -} -.input::placeholder { color: var(--text-label); } -.input:focus, select:focus, textarea:focus { - border-color: var(--esp32); - background: color-mix(in srgb, var(--esp32) 5%, var(--bg-input)); - box-shadow: 0 0 0 3px var(--esp32-dim); -} -select { cursor: pointer; } -select option { background: var(--bg-panel); color: var(--text); } -.input-group { display: flex; gap: 8px; } -.input-group .input { flex: 1; } - -/* ================= 按钮 ================= */ -.btn { - display: inline-flex; align-items: center; justify-content: center; gap: 7px; - padding: 9px 17px; border: none; border-radius: var(--radius-sm); - font-family: var(--font-display); - font-size: 13px; font-weight: 600; letter-spacing: .03em; - cursor: pointer; white-space: nowrap; - transition: transform .16s, box-shadow .2s, background .2s, border-color .2s, color .2s, opacity .2s; -} -.btn:hover:not(:disabled) { transform: translateY(-1px); } -.btn:active:not(:disabled) { transform: translateY(0) scale(.97); } -.btn:disabled { opacity: .38; cursor: not-allowed; transform: none; } - -/* 主操作 = ESP32 青绿 */ -.btn-primary { - background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%); - color: #04231e; - box-shadow: 0 4px 16px rgba(47,216,193,.28), inset 0 1px 0 rgba(255,255,255,.25); -} -.btn-primary:hover:not(:disabled) { - box-shadow: 0 6px 22px rgba(47,216,193,.4), inset 0 1px 0 rgba(255,255,255,.3); -} -.btn-success { - background: linear-gradient(135deg, var(--green) 0%, #16a374 100%); - color: #03231a; - box-shadow: 0 4px 16px rgba(61,220,142,.25), inset 0 1px 0 rgba(255,255,255,.22); -} -.btn-danger { - background: linear-gradient(135deg, var(--red) 0%, #dc4c4c 100%); - color: #fff; - box-shadow: 0 4px 16px rgba(255,107,107,.25), inset 0 1px 0 rgba(255,255,255,.2); -} -.btn-outline { - background: rgba(255,255,255,.02); color: var(--text-dim); - border: 1px solid var(--border-strong); -} -.btn-outline:hover:not(:disabled) { - background: var(--bg-hover); color: var(--text); - border-color: #3d5a84; -} -.btn-ghost { background: transparent; color: var(--text-dim); } -.btn-ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); } -.btn-block { width: 100%; justify-content: center; } -.btn-sm { padding: 6px 12px; font-size: 12px; } - -/* ---- 多色子系统页签(关键)---- - console-mode.js 用 btn-primary / btn-outline 表达"选中/未选中", - 这里按按钮 id 把"选中态"染成各自子系统色: - WiFi=青绿 · BLE=蓝 · ESP32=青绿 · GD32=琥珀。 */ -#modeBleBtn.btn-primary { - background: linear-gradient(135deg, var(--ble) 0%, #3b82f6 100%); - color: #04142b; - box-shadow: 0 4px 16px rgba(95,168,255,.3), inset 0 1px 0 rgba(255,255,255,.25); -} -#targetGd32Tab.btn-primary { - background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%); - color: #2b1d02; - box-shadow: 0 4px 16px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3); -} -#targetEsp32Tab.btn-primary { - background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%); - color: #04231e; -} -/* GD32 OTA 大按钮 = 琥珀(区别于 ESP32 青绿) */ -#uploadGd32Btn { - background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%); - color: #2b1d02; - box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3); -} -#uploadEsp32Btn { - box-shadow: 0 6px 20px rgba(47,216,193,.32), inset 0 1px 0 rgba(255,255,255,.28); -} - -/* ================= 连接状态条 / 提示 ================= */ -.conn-indicator { - display: flex; align-items: center; gap: 10px; - padding: 12px 16px; border-radius: var(--radius-sm); - background: var(--bg-elevated); border: 1px solid var(--border); - font-size: 13px; font-weight: 500; -} -.conn-indicator .light { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } -.conn-indicator.ok .light { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); } -.conn-indicator.err .light { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); } -.conn-indicator.warn .light { background: var(--amber); } - -.helper-note { - margin-top: 10px; padding: 10px 13px; - border-radius: var(--radius-sm); - border: 1px solid var(--border); - background: var(--bg-elevated); - font-size: 12px; line-height: 1.65; color: var(--text-dim); -} -.helper-note.ok { border-color: rgba(61,220,142,.3); background: rgba(61,220,142,.07); color: #a9f0c6; } -.helper-note.warn { border-color: rgba(252,191,63,.3); background: rgba(252,191,63,.07); color: #fbe3ae; } -.helper-note.err { border-color: rgba(255,107,107,.3); background: rgba(255,107,107,.07); color: #ffc4c4; } -.helper-note code { color: var(--esp32-hi); font-family: var(--font-mono); } - -/* ================= 右侧滑出抽屉:堆与任务栈 ================= */ -.sys-drawer { - --sys-drawer-panel-w: min(390px, calc(100vw - 48px)); - --sys-drawer-peek: 38px; - position: fixed; top: 76px; right: 0; z-index: 80; - width: calc(var(--sys-drawer-panel-w) + var(--sys-drawer-peek)); - height: calc(100vh - 92px); max-height: calc(100vh - 92px); - display: flex; flex-direction: row; align-items: stretch; - transform: translateX(var(--sys-drawer-panel-w)); - transition: transform .3s cubic-bezier(.22, 1, .36, 1); - filter: drop-shadow(-8px 0 24px rgba(2,5,12,.5)); -} -.sys-drawer:hover, .sys-drawer:focus-within { transform: translateX(0); } -.sys-drawer-rail { - flex: 0 0 var(--sys-drawer-peek); width: var(--sys-drawer-peek); - display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 12px; padding: 14px 4px; - background: linear-gradient(180deg, var(--bg-elevated) 0%, var(--bg-card) 100%); - border: 1px solid var(--border); border-right: none; - border-radius: var(--radius) 0 0 var(--radius); - cursor: default; -} -.sys-drawer-rail-ico { font-size: 16px; line-height: 1; } -.sys-drawer-rail-text { - writing-mode: vertical-rl; text-orientation: mixed; - font-family: var(--font-display); - font-size: 11px; font-weight: 600; letter-spacing: .18em; - color: var(--text-dim); -} -.sys-drawer:hover .sys-drawer-rail-text, -.sys-drawer:focus-within .sys-drawer-rail-text { color: var(--mem-hi); } -.sys-drawer-panel { - flex: 1 1 var(--sys-drawer-panel-w); - width: var(--sys-drawer-panel-w); min-width: 0; height: 100%; - margin: 0 !important; - display: flex; flex-direction: column; - border-radius: 0; overflow: hidden; - background: var(--bg-card); - border: 1px solid var(--border); border-left: none; border-right: none; -} -.sys-drawer-head { cursor: default; flex-shrink: 0; } -.sys-drawer-head .card-head-btn { margin-left: auto; } -.sys-drawer-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; } diff --git a/tools/css/motor.css b/tools/css/motor.css deleted file mode 100644 index fe5fba0..0000000 --- a/tools/css/motor.css +++ /dev/null @@ -1,769 +0,0 @@ -/* motor.css - 电机控制台页面样式 - * 由 motor.html 内联 - - -

SPI 链路测速

-
临时诊断工具 [SPI-SPEEDTEST] — 探针 0xA1 APP PING,删除时直接删掉本文件
- -
-
- - - -
-
- - - -
-
- 满帧 236B = 等效电机 OTA 0xD1 数据节奏;载荷 0 = 纯握手往返。
- 判读:失败必须为 0;「最快」是链路物理极限周期;「平均-最快」差值是软件调度开销。 -
-
- -
-
等待测试…
-
- -
- - - -
时间帧数载荷失败平均最快吞吐
-
本页历史仅存内存,刷新即清空——方便改参数前后对比。
-
- - - - diff --git a/国风歌词.md b/国风歌词.md new file mode 100644 index 0000000..027cd77 --- /dev/null +++ b/国风歌词.md @@ -0,0 +1,59 @@ +# 墨染浮生 + +[Intro] +嗯... +(雨打芭蕉,檐下滴水声) +啊... + +[Verse 1] +夜半灯残宣纸白 +研一砚松烟,墨色渐开 +提笔落笔皆是梦 +点染之间,山水自归来 +二十层晕染不着急 +浓淡干湿,墨分五色意 +每一幅都像旧时故纸堆里翻出的残卷 +模糊了轮廓,却记得那年 + +[Instrumental] +(古琴泛音,远处箫声悠长) + +[Verse 2] +铺开下一尺素绢 +笔锋流转如落花时节 +一笔接一笔似檐外细雨 +宣纸上的人物便活了气息 +有些留白是刻意,有些是随意 +收好的折几叠,随它去的便由它去 +留得住的,都像深秋午后那缕闲光 +笔锋转...转...转...意到便成章 + +[Chorus] +嗯... 任笔墨流淌 +(慢慢来,不急) +啊... 到了天亮,画卷自会收 +(再添一笔,再添一笔) + +[Instrumental] +(古筝与箫对答,雨声渐浓) + +[Bridge] +窗外的雨还在芭蕉上画着圈... +案头的墨还在自行晕开... +都不急... +都不赶... + +[Chorus] +嗯... 任笔墨流淌 +(慢慢来,不急) +啊... 到了天亮,画卷自会收 +(再添一笔,再添一笔) + +[Instrumental] +(编磬轻叩,古琴余韵) + +[Outro] +嗯... +(笔锋转...) +啊... 夜安 +(墨香渐散,一盏孤灯缓缓暗下)