SPI Link v2 Self-Test / selftest (push) Has been cancelled
添加SPI Link v2的PC端自测功能,包含交叉校验基准测试,覆盖HELLO/OTA/CAN/PING等 多种协议场景。同时重构了health monitor中的多字段读取逻辑,消除了volatile变量 的撕裂读问题,并增强了BLE OTA的数据传输可靠性。 refactor(health_monitor): 修复heap hook多字段撕裂读问题 采用portMUX_CRITICAL临界区实现多字段一致快照读取,消除任务上下文与heap hook 之间的数据竞争。修改了HookSnap结构体设计,通过一次性拷贝volatile变量避免 跨字段组合时的数据不一致。JSON输出中明确标注estimated值的置信度。 fix(ble_ota): 实现DATA_IN丢包负反馈机制 当malloc失败或队列满时发送缺口ACK通知前端按completed_bytes续传,避免 静默丢包导致的8秒超时重传风暴。增加节流机制防止负ACK在重传叠加时打爆 NimBLE mbuf池,提升低内存环境下的OTA传输稳定性。 refactor(spi_master): 从堆分配改为固定Job池管理 将SPI worker的动态堆分配Job改为固定大小的静态池管理,避免频繁的 calloc/free操作影响实时性。新增acquireJobSlot/releaseJobSlot机制 管理池槽生命周期,提升SPI通信的可靠性和内存使用效率。
343 lines
13 KiB
CSS
343 lines
13 KiB
CSS
/* ============================================================
|
||
layout.css — 命令栏头部 / 主布局 / 卡片 / 表单 / 按钮 / 侧滑抽屉
|
||
============================================================ */
|
||
|
||
/* ================= HEADER — 命令栏 ================= */
|
||
.header-bar {
|
||
position: relative;
|
||
display: flex; align-items: center; gap: 16px;
|
||
padding: 16px 22px; margin-bottom: 26px;
|
||
background: 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;
|
||
overflow: hidden;
|
||
}
|
||
/* 顶部三色霓虹描边: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;
|
||
}
|
||
/* 右上角极淡的芯片电路装饰 */
|
||
.header-bar::after {
|
||
content: "";
|
||
position: absolute; right: -40px; top: -40px;
|
||
width: 220px; height: 220px; pointer-events: none;
|
||
background:
|
||
radial-gradient(circle at 30% 30%, rgba(47,216,193,.10), transparent 55%);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.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--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; }
|