feat(spi_link): 添加SPI Link v2 PC自测框架并优化协议稳定性
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通信的可靠性和内存使用效率。
This commit is contained in:
小龙虾
2026-07-21 01:16:43 +08:00
parent 00b716c8db
commit 770ae14156
29 changed files with 1712 additions and 2428 deletions
+256 -155
View File
@@ -1,241 +1,342 @@
/* === layout.css — 头部/主布局/卡片/表单/按钮/连接状态 === */
/* === HEADER — compact status bar === */
/* ============================================================
layout.css — 命令栏头部 / 主布局 / 卡片 / 表单 / 按钮 / 侧滑抽屉
============================================================ */
/* ================= HEADER — 命令栏 ================= */
.header-bar {
display: flex; align-items: center; gap: 12px;
padding: 14px 20px; margin-bottom: 24px;
background: var(--bg-panel);
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: 10px;
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(--accent);
box-shadow: 0 0 8px var(--accent-dim);
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: 15px; font-weight: 700; }
.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: 6px;
padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 500;
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); }
.pill.offline .indicator { background: var(--red); }
.pill.processing .indicator { background: var(--amber); animation: blink 1s infinite; }
.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); }
@keyframes blink { 50% { opacity: .35; } }
.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; } }
/* === MAIN LAYOUT === */
.pill-val {
font-weight: 700; color: var(--text);
font-family: var(--font-mono);
}
/* ================= 主布局 ================= */
.main-layout {
display: grid;
grid-template-columns: 380px 1fr;
grid-template-columns: 400px 1fr;
gap: var(--gap);
align-items: start;
}
/* === 右侧滑出抽屉:堆与任务栈 ===
* 总宽 = peek 把手 + 面板;默认 translateX(面板宽) 只露把手,hover 整板探出 */
.sys-drawer {
--sys-drawer-panel-w: min(380px, calc(100vw - 48px));
--sys-drawer-peek: 36px;
position: fixed;
top: 72px;
right: 0;
z-index: 80;
width: calc(var(--sys-drawer-panel-w) + var(--sys-drawer-peek));
height: calc(100vh - 88px);
max-height: calc(100vh - 88px);
display: flex;
flex-direction: row;
align-items: stretch;
transform: translateX(var(--sys-drawer-panel-w));
transition: transform .28s cubic-bezier(.22, 1, .36, 1);
filter: drop-shadow(-6px 0 18px rgba(0, 0, 0, .35));
}
.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: 10px;
padding: 14px 4px;
background: linear-gradient(180deg, #1a2a3a 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-size: 12px;
font-weight: 600;
letter-spacing: .12em;
color: var(--text-dim);
text-transform: none;
}
.sys-drawer:hover .sys-drawer-rail-text,
.sys-drawer:focus-within .sys-drawer-rail-text {
color: var(--accent);
}
.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;
}
/* === CARDS === */
/* ================= 卡片 ================= */
.card {
background: var(--bg-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: 8px;
display: flex; align-items: center; gap: 10px;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--border-subtle);
font-family: var(--font-display);
font-size: 13px; font-weight: 600;
color: var(--text-dim);
text-transform: uppercase; letter-spacing: .06em;
color: var(--text);
letter-spacing: .05em;
cursor: pointer;
user-select: none;
}
.card-head .ico { font-size: 15px; }
.card-head-btn {
margin-left: auto;
padding: 4px 10px;
position: relative;
z-index: 2;
/* 图标芯片:圆角方块底色取自卡片子系统色 */
.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-body { padding: 16px 18px; }
.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 { cursor: pointer; }
.card-head .collapse-arrow {
margin-left: auto; font-size: 10px; opacity: .45;
margin-left: auto; font-size: 10px; color: var(--text-label);
transition: transform .2s;
}
.card.collapsed .collapse-arrow { transform: rotate(-90deg); }
/* === FORM ELEMENTS === */
/* ================= 表单 ================= */
.form-row { margin-bottom: 14px; }
.form-row:last-child { margin-bottom: 0; }
.form-label {
display: block; font-size: 11px; color: var(--text-label);
text-transform: uppercase; letter-spacing: .08em; margin-bottom: 6px;
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 12px; font-size: 13px; font-family: inherit;
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 .15s;
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(--border-focus);
box-shadow: 0 0 0 3px var(--accent-dim);
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; }
/* === BUTTONS === */
/* ================= 按钮 ================= */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
padding: 9px 16px; border: none; border-radius: var(--radius-sm);
font-size: 13px; font-weight: 600; cursor: pointer;
transition: opacity .15s, transform .15s;
font-family: inherit; white-space: nowrap;
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:disabled { opacity: .4; cursor: not-allowed; transform: none; }
.btn-primary { background: var(--accent); color: #07121c; }
.btn-primary:hover:not(:disabled) { opacity: .88; }
.btn-success { background: var(--green); color: #051a0c; }
.btn-danger { background: var(--red); color: #fff; }
.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: transparent; color: var(--text-dim);
border: 1px solid var(--border);
background: rgba(255,255,255,.02); color: var(--text-dim);
border: 1px solid var(--border-strong);
}
.btn-outline:hover:not(:disabled) { background: var(--bg-elevated); color: var(--text); }
.btn-ghost {
background: transparent; color: var(--text-dim);
.btn-outline:hover:not(:disabled) {
background: var(--bg-hover); color: var(--text);
border-color: #3d5a84;
}
.btn-ghost:hover:not(:disabled) { background: var(--bg-elevated); color: var(--text); }
.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; }
/* === CONNECTION STATUS ROW === */
/* ---- 多色子系统页签(关键)----
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); }
.conn-indicator.err .light { background: var(--red); }
.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 12px;
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.6;
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);
}
.helper-note.ok { border-color: rgba(34,197,94,.25); color: #b8f5cb; }
.helper-note.warn { border-color: rgba(245,158,11,.25); color: #fbd38d; }
.helper-note.err { border-color: rgba(239,68,68,.25); color: #fecaca; }
.helper-note code { color: #d7fbff; font-family: var(--font-mono); }
.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; }