feat(http): 添加CPU使用率监控功能
添加了详细的CPU使用率统计功能,包括: - 实现FreeRTOS运行时统计采样机制 - 添加CPU任务占用率计算和快照功能 - 在/system/status接口中增加CPU状态JSON数据 - 支持实时监控各任务的运行时间和占用百分比 更新了前端界面以显示CPU使用情况: - 添加CPU使用率进度条和百分比显示 - 显示空闲时间、采样窗口和任务数量 - 展示前8个最耗时任务的详细统计信息 同时优化了CAN FD桥接实现,重构为SPI协议帧传输方式, 并改进了前端CAN页面布局,将CAN表单迁移到独立页面。
This commit is contained in:
+107
-14
@@ -360,8 +360,12 @@ function setHelper(id, text, type) {
|
||||
el.innerHTML = text;
|
||||
}
|
||||
function updateCanHint() {
|
||||
var dlc = parseInt(document.getElementById('canDlc').value) || 0;
|
||||
var dataHex = document.getElementById('canData').value.trim().replace(/\s/g, '');
|
||||
var dlcEl = document.getElementById('canDlc');
|
||||
var dataEl = document.getElementById('canData');
|
||||
var hintEl = document.getElementById('canHint');
|
||||
if (!dlcEl || !dataEl || !hintEl) return;
|
||||
var dlc = parseInt(dlcEl.value) || 0;
|
||||
var dataHex = dataEl.value.trim().replace(/\s/g, '');
|
||||
if (!dataHex) {
|
||||
setHelper('canHint', '输入十六进制数据后会自动检查字节数是否与 DLC 一致。', '');
|
||||
return;
|
||||
@@ -490,6 +494,78 @@ function updateHeapAndTaskStatusUi(data) {
|
||||
setFill(fillId, pct);
|
||||
setText(pctId, pct.toFixed(1) + '%');
|
||||
}
|
||||
function setCpuFill(pct) {
|
||||
var el = document.getElementById('cpuUsageFill');
|
||||
if (!el) return;
|
||||
var safePct = Math.max(0, Math.min(100, Number(pct) || 0));
|
||||
el.style.width = safePct + '%';
|
||||
el.className = 'heap-bar-fill cpu-fill' + (safePct >= 85 ? ' danger' : safePct >= 70 ? ' warn' : '');
|
||||
}
|
||||
function updateCpuUsageUi(cpu) {
|
||||
cpu = cpu || {};
|
||||
var taskList = document.getElementById('cpuTaskList');
|
||||
if (!cpu.available) {
|
||||
setText('cpuUsageText', '未启用 FreeRTOS 统计');
|
||||
setText('cpuUsagePct', '--');
|
||||
setText('cpuIdlePct', '--');
|
||||
setText('cpuSampleMs', '--');
|
||||
setText('cpuTaskCount', '--');
|
||||
setCpuFill(0);
|
||||
if (taskList) taskList.innerHTML = '<div class="status-empty">固件未启用 CPU runtime stats</div>';
|
||||
return;
|
||||
}
|
||||
if (!cpu.valid) {
|
||||
setText('cpuUsageText', '等待下一次采样...');
|
||||
setText('cpuUsagePct', '--');
|
||||
setText('cpuIdlePct', '--');
|
||||
setText('cpuSampleMs', '--');
|
||||
setText('cpuTaskCount', '--');
|
||||
setCpuFill(0);
|
||||
if (taskList) taskList.innerHTML = '<div class="status-empty">首次刷新只建立基线,下一次刷新后显示 CPU 占用</div>';
|
||||
return;
|
||||
}
|
||||
var usagePct = Number(cpu.usage_pct) || 0;
|
||||
var idlePct = Number(cpu.idle_pct) || 0;
|
||||
var sampleMs = Number(cpu.sample_ms) || 0;
|
||||
var tasks = Array.isArray(cpu.tasks) ? cpu.tasks.slice() : [];
|
||||
tasks.sort(function (a, b) {
|
||||
return (Number(b.usage_pct) || 0) - (Number(a.usage_pct) || 0);
|
||||
});
|
||||
|
||||
setText('cpuUsageText', 'CPU 已用 ' + formatPercent(usagePct));
|
||||
setText('cpuUsagePct', formatPercent(usagePct));
|
||||
setText('cpuIdlePct', formatPercent(idlePct));
|
||||
setText('cpuSampleMs', sampleMs > 0 ? sampleMs + ' ms' : '--');
|
||||
setText('cpuTaskCount', String(tasks.length));
|
||||
setCpuFill(usagePct);
|
||||
|
||||
if (taskList) {
|
||||
var visibleTasks = tasks.slice(0, 8);
|
||||
if (!visibleTasks.length) {
|
||||
taskList.innerHTML = '<div class="status-empty">当前采样没有任务增量</div>';
|
||||
} else {
|
||||
taskList.innerHTML = visibleTasks.map(function (task) {
|
||||
var pct = Number(task.usage_pct) || 0;
|
||||
var delta = Number(task.runtime_delta) || 0;
|
||||
var fillClass = pct >= 85 ? 'err' : pct >= 70 ? 'warn' : '';
|
||||
return '' +
|
||||
'<div class="status-task cpu-task">' +
|
||||
'<div class="status-task-head">' +
|
||||
'<div class="status-task-name">' + escapeHtml(task.name || 'task') + '</div>' +
|
||||
'<div class="status-task-ratio">' + escapeHtml(formatPercent(pct)) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="status-task-bar"><div class="status-task-bar-fill ' + fillClass + '" style="width:' + Math.max(0, Math.min(100, pct)) + '%"></div></div>' +
|
||||
'<div class="status-task-meta">' +
|
||||
'<div><span>任务号</span><b>' + escapeHtml(task.task_number) + '</b></div>' +
|
||||
'<div><span>增量</span><b>' + escapeHtml(delta) + '</b></div>' +
|
||||
'<div><span>占比</span><b>' + escapeHtml(formatPercent(pct)) + '</b></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
updateCpuUsageUi(data.cpu);
|
||||
setText('mInternalHeap', data.internal_free_heap !== undefined ? formatBytes(data.internal_free_heap) : '--');
|
||||
setText('mInternalMinHeap', data.internal_min_free_heap !== undefined ? formatBytes(data.internal_min_free_heap) : '--');
|
||||
setText('mInternalLargestBlock', data.internal_largest_free_block !== undefined ? formatBytes(data.internal_largest_free_block) : '--');
|
||||
@@ -819,10 +895,18 @@ async function refreshSystemStatusNow(event) {
|
||||
}
|
||||
// ==================== CAN 帧发送 ====================
|
||||
async function sendCanFrame() {
|
||||
var ch = parseInt(document.getElementById('canChannel').value);
|
||||
var idStr = document.getElementById('canId').value.trim();
|
||||
var dlc = parseInt(document.getElementById('canDlc').value) || 8;
|
||||
var dataStr = document.getElementById('canData').value.trim();
|
||||
var chEl = document.getElementById('canChannel');
|
||||
var idEl = document.getElementById('canId');
|
||||
var dlcEl = document.getElementById('canDlc');
|
||||
var dataEl = document.getElementById('canData');
|
||||
if (!chEl || !idEl || !dlcEl || !dataEl) {
|
||||
showToast('请在独立 CAN 页面中发送帧', 'warning');
|
||||
return;
|
||||
}
|
||||
var ch = parseInt(chEl.value);
|
||||
var idStr = idEl.value.trim();
|
||||
var dlc = parseInt(dlcEl.value) || 8;
|
||||
var dataStr = dataEl.value.trim();
|
||||
var extId = 0;
|
||||
if (idStr.toLowerCase().indexOf('0x') === 0) extId = parseInt(idStr, 16);
|
||||
else { extId = parseInt(idStr, 10); if (isNaN(extId)) extId = parseInt(idStr, 16); }
|
||||
@@ -854,10 +938,15 @@ async function sendCanFrame() {
|
||||
}
|
||||
}
|
||||
function clearCanFields() {
|
||||
document.getElementById('canId').value = '0x18DA00F1';
|
||||
document.getElementById('canDlc').value = '8';
|
||||
document.getElementById('canData').value = '0102030405060708';
|
||||
document.getElementById('canResp').style.display = 'none';
|
||||
var idEl = document.getElementById('canId');
|
||||
var dlcEl = document.getElementById('canDlc');
|
||||
var dataEl = document.getElementById('canData');
|
||||
var respEl = document.getElementById('canResp');
|
||||
if (!idEl || !dlcEl || !dataEl) return;
|
||||
idEl.value = '0x18DA00F1';
|
||||
dlcEl.value = '8';
|
||||
dataEl.value = '0102030405060708';
|
||||
if (respEl) respEl.style.display = 'none';
|
||||
updateCanHint();
|
||||
}
|
||||
// ==================== SPI 发送 ====================
|
||||
@@ -1001,10 +1090,14 @@ function setupDropZone(zoneId, chip) {
|
||||
}
|
||||
setupDropZone('uploadZoneEsp32', 'esp32');
|
||||
setupDropZone('uploadZoneGd32', 'gd32');
|
||||
document.getElementById('canData').addEventListener('input', updateCanHint);
|
||||
document.getElementById('canDlc').addEventListener('input', updateCanHint);
|
||||
document.getElementById('spiType').addEventListener('input', updateSpiHint);
|
||||
document.getElementById('spiData').addEventListener('input', updateSpiHint);
|
||||
var canDataEl = document.getElementById('canData');
|
||||
var canDlcEl = document.getElementById('canDlc');
|
||||
if (canDataEl) canDataEl.addEventListener('input', updateCanHint);
|
||||
if (canDlcEl) canDlcEl.addEventListener('input', updateCanHint);
|
||||
var spiTypeEl = document.getElementById('spiType');
|
||||
var spiDataEl = document.getElementById('spiData');
|
||||
if (spiTypeEl) spiTypeEl.addEventListener('input', updateSpiHint);
|
||||
if (spiDataEl) spiDataEl.addEventListener('input', updateSpiHint);
|
||||
// ==================== Intel HEX → BIN 转换 ====================
|
||||
/**
|
||||
* 将 Intel HEX 字符串解析为原始 BIN 字节数组。
|
||||
|
||||
+11
-13
@@ -93,8 +93,6 @@
|
||||
abortBtn: $('abortBtn'),
|
||||
uploadZone: $('uploadZone'),
|
||||
firmwareFile: $('firmwareFile'),
|
||||
payloadSize: $('payloadSize'),
|
||||
ctrlHex: $('ctrlHex'),
|
||||
connStatus: $('connStatus'), // 共享连接指示器
|
||||
connStatusTxt: $('connStatusTxt'), // 共享连接指示器文本
|
||||
connState: $('connState'),
|
||||
@@ -120,6 +118,11 @@
|
||||
return TARGETS[window.flashTarget] || TARGETS.esp32;
|
||||
}
|
||||
|
||||
function currentPayloadSize() {
|
||||
const cfg = currentTarget();
|
||||
return Math.max(32, Math.min(498, Number(cfg.recommendedPayload) || 480));
|
||||
}
|
||||
|
||||
function flowTarget() {
|
||||
return TARGETS[state.uploadTargetKey] || currentTarget();
|
||||
}
|
||||
@@ -324,7 +327,7 @@
|
||||
if (!Number.isFinite(totalSize) || totalSize <= 0) return;
|
||||
const safeBytes = Math.max(0, Math.min(totalSize, ackBytes >>> 0));
|
||||
const percent = (safeBytes / totalSize) * 100;
|
||||
const payloadSize = Math.max(1, Number(els.payloadSize.value) || 480);
|
||||
const payloadSize = Math.max(1, currentPayloadSize());
|
||||
const ackedPackets = Math.min(packetTotal, Math.ceil(safeBytes / payloadSize));
|
||||
const cfg = currentTarget();
|
||||
setProgress(percent, `[${cfg.label}] 已确认 ${ackedPackets}/${packetTotal} 包`);
|
||||
@@ -363,8 +366,6 @@
|
||||
els.connectBtn.disabled = false;
|
||||
}
|
||||
els.disconnectBtn.disabled = !connected;
|
||||
els.payloadSize.disabled = state.uploading;
|
||||
els.ctrlHex.disabled = state.uploading;
|
||||
els.autoFlashBtn.disabled = !idle;
|
||||
els.abortBtn.disabled = !state.uploading;
|
||||
|
||||
@@ -373,7 +374,7 @@
|
||||
} else if (!state.notificationsEnabled) {
|
||||
setReadyHint('设备已连接,正在自动准备刷写通道。若长时间无响应,请断开后重连。');
|
||||
} else if (!state.selectedFile) {
|
||||
setReadyHint('设备已连接。下一步请选择固件文件,并确认目标芯片与 Payload。');
|
||||
setReadyHint('设备已连接。下一步请选择目标芯片与固件文件。');
|
||||
} else {
|
||||
setReadyHint('设备与文件都已就绪,可以直接开始刷写或执行一键刷写。');
|
||||
}
|
||||
@@ -406,8 +407,6 @@
|
||||
function refreshTargetUi() {
|
||||
const cfg = currentTarget();
|
||||
updateBleStageLabels(cfg);
|
||||
els.ctrlHex.value = cfg.ctrlHex;
|
||||
els.payloadSize.value = String(cfg.recommendedPayload);
|
||||
if (state.selectedFile) updateFileInfo(state.selectedFile);
|
||||
setConnectionState(!!state.server);
|
||||
// 同步目标胶囊标签
|
||||
@@ -552,7 +551,7 @@
|
||||
// 不再用 ackMinSeenSeq 作下界 —— 因为本 run 第一个窗口的 seq 本身就是 0..31,
|
||||
// 下界过严会把正常 ACK 也误判为越界(导致 ackBytes 永远 0)。
|
||||
if (state.uploading && state.activeTotalSize > 0) {
|
||||
const payloadSize = Math.max(1, Number(els.payloadSize.value) || 480);
|
||||
const payloadSize = Math.max(1, currentPayloadSize());
|
||||
const maxExpectedSeq = Math.ceil(state.activeTotalSize / payloadSize) - 1;
|
||||
if (ackSeq > maxExpectedSeq + BLE_OTA_ACK_WINDOW) {
|
||||
bleLog('ACK', `忽略越界 ACK: seq=${ackSeq} (run 范围 0..${maxExpectedSeq})`, 'W');
|
||||
@@ -954,7 +953,7 @@
|
||||
els.fileName.textContent = file ? file.name : '-';
|
||||
els.fileType.textContent = file ? (file.name.toLowerCase().includes('.hex') ? 'Intel HEX' : 'Binary') : '-';
|
||||
els.fileSize.textContent = file ? formatBytes(file.size) : '0 B';
|
||||
const payloadSize = Number(els.payloadSize.value) || 480;
|
||||
const payloadSize = currentPayloadSize();
|
||||
const packets = file ? Math.ceil(file.size / payloadSize) : 0;
|
||||
els.packetCount.textContent = String(packets);
|
||||
els.sentCount.textContent = '0';
|
||||
@@ -1141,7 +1140,7 @@
|
||||
|
||||
const cfg = currentTarget();
|
||||
state.uploadTargetKey = window.flashTarget || 'esp32';
|
||||
const payloadSize = Math.max(32, Math.min(498, Number(els.payloadSize.value) || 480));
|
||||
const payloadSize = Math.max(32, Math.min(498, currentPayloadSize()));
|
||||
const file = state.selectedFile;
|
||||
|
||||
state.abortRequested = false;
|
||||
@@ -1401,7 +1400,7 @@
|
||||
setBleStageText('发送 0xF7 + SHA-256,等待设备端缓存校验...');
|
||||
showBleVerifyInfo(frontSha, '等待设备端比对...', { text: '已发送文件 SHA-256,等待 GD32 缓存校验' });
|
||||
await sleep(30);
|
||||
const cmdHex = els.ctrlHex.value || cfg.ctrlHex;
|
||||
const cmdHex = cfg.ctrlHex;
|
||||
const cmdBase = hexToBytes(cmdHex);
|
||||
const cmd = new Uint8Array(cmdBase.length + 32 + 2);
|
||||
cmd.set(cmdBase, 0);
|
||||
@@ -1554,7 +1553,6 @@
|
||||
els.firmwareFile.addEventListener('change', syncSelectedFileFromInput);
|
||||
els.firmwareFile.addEventListener('input', syncSelectedFileFromInput);
|
||||
window.addEventListener('focus', () => setTimeout(syncSelectedFileFromInput, 0));
|
||||
els.payloadSize.addEventListener('change', () => updateFileInfo(state.selectedFile));
|
||||
}
|
||||
|
||||
// 暴露到 window,方便外部模式切换/集成时调用(默认 API:init / disconnect)
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 通信控制台</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260624b">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
|
||||
<header class="header-bar can-header">
|
||||
<div class="header-brand">
|
||||
<span class="dot-mark"></span>
|
||||
<strong>ESP32 CAN 通信控制台</strong>
|
||||
</div>
|
||||
<div class="header-status">
|
||||
<span class="pill offline" id="canConnPill"><span class="indicator"></span><span id="canConnTxt">未连接</span></span>
|
||||
<span class="pill" id="canVersionPill">v--</span>
|
||||
<span class="pill" id="canChannelPill">CH0</span>
|
||||
<span class="pill" id="dbcSummaryPill">未导入 DBC</span>
|
||||
<span class="pill" id="messageSummaryPill">0 报文</span>
|
||||
</div>
|
||||
<div class="header-sub can-header-actions">
|
||||
<a class="pill can-link-pill" href="index.html">返回 OTA 控制台</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="card can-toolbar-card">
|
||||
<div class="card-head"><span class="ico">🧰</span> 工作台设置</div>
|
||||
<div class="card-body">
|
||||
<div class="can-toolbar-grid">
|
||||
<div class="can-toolbar-block">
|
||||
<label class="form-label" for="toolIp">ESP32 IP</label>
|
||||
<div class="input-group">
|
||||
<input class="input" type="text" id="toolIp" value="192.168.31.127" placeholder="输入 IP 地址">
|
||||
<button class="btn btn-outline" id="testConnBtn" type="button">测试</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="can-toolbar-block can-toolbar-channel">
|
||||
<label class="form-label" for="defaultChannel">默认通道</label>
|
||||
<select class="input" id="defaultChannel">
|
||||
<option value="0">CH0</option>
|
||||
<option value="1">CH1</option>
|
||||
<option value="2">CH2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="can-toolbar-block can-toolbar-upload">
|
||||
<label class="form-label">DBC 文件</label>
|
||||
<div class="upload-zone upload-zone-sm dbc-dropzone" id="dbcDropZone">
|
||||
<div class="upload-ico">📥</div>
|
||||
<h4>导入 DBC</h4>
|
||||
<p>点击或拖拽 .dbc 文件</p>
|
||||
</div>
|
||||
<input type="file" id="dbcFileInput" accept=".dbc,text/plain" style="display:none;">
|
||||
</div>
|
||||
|
||||
<div class="can-toolbar-block can-toolbar-search">
|
||||
<label class="form-label" for="dbcSearch">报文搜索</label>
|
||||
<input class="input" type="text" id="dbcSearch" placeholder="按报文名、ID、节点或信号名过滤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-info can-file-info-inline" id="dbcFileInfo">
|
||||
<div class="file-info-row">
|
||||
<span class="can-file-name">📄 <span id="dbcFileName">-</span></span>
|
||||
<button class="btn btn-ghost btn-sm" id="clearDbcBtn" type="button">清除</button>
|
||||
</div>
|
||||
<div class="file-info-grid">
|
||||
<div class="file-meta-item"><span>报文</span><b id="dbcMessageCount">0</b></div>
|
||||
<div class="file-meta-item"><span>信号</span><b id="dbcSignalCount">0</b></div>
|
||||
<div class="file-meta-item"><span>节点</span><b id="dbcNodeCount">0</b></div>
|
||||
<div class="file-meta-item"><span>大小</span><b id="dbcFileSize">0 B</b></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="can-toolbar-foot">
|
||||
<div class="conn-indicator err" id="connIndicator">
|
||||
<span class="light"></span>
|
||||
<span id="connIndicatorTxt">等待连接测试</span>
|
||||
</div>
|
||||
<div class="helper-note" id="connectionNote">
|
||||
当前页面会直接调用 <code>/system/status</code> 和 <code>/can/send</code>。
|
||||
</div>
|
||||
<div class="helper-note" id="dbcHint">
|
||||
当前解析器会自动识别常见 DBC 的 <code>BO_</code>、<code>SG_</code>、<code>VAL_</code>、
|
||||
<code>CM_</code> 和 <code>SIG_VALTYPE_</code>。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card can-table-card">
|
||||
<div class="card-head"><span class="ico">📚</span> 报文列表</div>
|
||||
<div class="card-body can-table-card-body">
|
||||
<div class="message-list" id="messageList">
|
||||
<div class="status-empty">等待导入 DBC...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="can-editor-layout">
|
||||
<section class="card can-signal-card">
|
||||
<div class="card-head"><span class="ico">🎚️</span> 信号编辑</div>
|
||||
<div class="card-body">
|
||||
<div class="can-empty-state" id="emptyMessageState">
|
||||
<div class="can-empty-icon">📡</div>
|
||||
<h3>先导入 DBC,再选择一条 CAN 报文</h3>
|
||||
<p>上方按表格选报文,下方按信号名直接编辑,右侧查看编码结果并发送。</p>
|
||||
</div>
|
||||
|
||||
<div id="messageWorkspace" style="display:none;">
|
||||
<div class="can-selection-bar">
|
||||
<div class="can-selection-copy">
|
||||
<div class="can-title" id="selectedMessageTitle">-</div>
|
||||
<div class="can-subtitle" id="selectedMessageSubtitle">-</div>
|
||||
</div>
|
||||
<div class="can-selection-actions">
|
||||
<button class="btn btn-outline" id="loadPreviewToRawBtn" type="button">载入原始帧</button>
|
||||
<button class="btn btn-primary" id="sendSelectedBtn" type="button">发送当前报文</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="can-meta-grid" id="selectedMessageMeta"></div>
|
||||
<div class="helper-note can-comment-note" id="selectedMessageComment">暂无报文注释。</div>
|
||||
|
||||
<div class="signal-grid signal-table-host" id="signalGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="can-sidepanel">
|
||||
<section class="card">
|
||||
<div class="card-head"><span class="ico">🛰️</span> 帧预览</div>
|
||||
<div class="card-body">
|
||||
<div class="can-frame-panel can-frame-panel-compact">
|
||||
<div class="can-panel-kicker">编码结果</div>
|
||||
<div class="can-frame-hex" id="generatedHex">--</div>
|
||||
<div class="can-byte-strip" id="generatedBytes"></div>
|
||||
<div class="helper-note" id="encodeHint">选择报文后,信号值会实时编码为 CAN 数据。</div>
|
||||
<div class="can-response" id="dbcResp" style="display:none;">
|
||||
<div class="can-response-label">设备响应</div>
|
||||
<div id="dbcRespText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head"><span class="ico">✍️</span> 原始帧发送</div>
|
||||
<div class="card-body">
|
||||
<div class="row-2 can-raw-grid">
|
||||
<div class="form-row" style="margin:0;">
|
||||
<label class="form-label" for="manualChannel">通道</label>
|
||||
<select class="input" id="manualChannel">
|
||||
<option value="0">CH0</option>
|
||||
<option value="1">CH1</option>
|
||||
<option value="2">CH2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" style="margin:0;">
|
||||
<label class="form-label" for="manualId">CAN ID</label>
|
||||
<input class="input" type="text" id="manualId" value="0x18DA00F1" placeholder="支持 0x 或十进制">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:10px;">
|
||||
<label class="form-label" for="manualDlc">DLC</label>
|
||||
<input class="input" type="number" id="manualDlc" value="8" min="0" max="64">
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:10px;">
|
||||
<label class="form-label" for="manualData">HEX 数据</label>
|
||||
<input class="input" type="text" id="manualData" value="0102030405060708" placeholder="01020304...">
|
||||
</div>
|
||||
<div class="helper-note" id="manualHint">
|
||||
原始帧模式适合绕过 DBC 直接发 HEX,或发送当前预览帧后二次微调。
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" id="sendManualBtn" type="button">发送原始帧</button>
|
||||
<button class="btn btn-outline" id="resetManualBtn" type="button">重置</button>
|
||||
</div>
|
||||
<div class="can-response" id="manualResp" style="display:none;">
|
||||
<div class="can-response-label">设备响应</div>
|
||||
<div id="manualRespText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card-head"><span class="ico">🕘</span> 最近发送</div>
|
||||
<div class="card-body">
|
||||
<div class="history-list history-list-compact" id="historyList">
|
||||
<div class="status-empty">尚未发送任何 CAN 帧。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast">
|
||||
<span class="toast-ico" id="toastIco">✓</span>
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script src="can.js?v=20260624b"></script>
|
||||
</body>
|
||||
</html>
|
||||
+1111
File diff suppressed because it is too large
Load Diff
+46
-38
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
index.html - ESP32 OTA 中继控制台主页面
|
||||
支持 WiFi OTA(ESP32 本机 + GD32 中继)和 BLE OTA 双通道固件升级。
|
||||
配套脚本:app.js (WiFi OTA), ble_ota.js (BLE OTA), style.css (UI 样式)
|
||||
@@ -61,18 +61,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bleModeCards" style="display:none;">
|
||||
<div class="row-2" id="bleParamRow" style="margin-top:0;">
|
||||
<div class="form-row" style="margin:0;">
|
||||
<label class="form-label" for="payloadSize">每包 Payload</label>
|
||||
<input id="payloadSize" class="input" type="number" min="32" max="498" value="480">
|
||||
</div>
|
||||
<div class="form-row" style="margin:0;">
|
||||
<label class="form-label" for="ctrlHex">触发命令(HEX)</label>
|
||||
<input id="ctrlHex" class="input" type="text" value="F7" maxlength="8">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conn-indicator err" id="connStatus" style="margin-top:12px;">
|
||||
<span class="light"></span>
|
||||
<span id="connStatusTxt">等待连接</span>
|
||||
@@ -157,7 +145,7 @@
|
||||
<b id="fileSha256" style="font-size:10px;word-break:break-all;font-family:var(--font-mono);">-</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note">建议使用 Chrome / Edge。若首包失败,可将 Payload 从 480 降为 240 再试。</div>
|
||||
<div class="footer-note">建议使用 Chrome / Edge。每包 Payload 与触发命令由系统按目标自动设置。</div>
|
||||
</div>
|
||||
<div style="height:12px"></div>
|
||||
<div id="wifiEsp32ActionPane">
|
||||
@@ -302,7 +290,25 @@
|
||||
<button class="btn btn-ghost btn-sm card-head-btn" onclick="refreshSystemStatusNow(event)">刷新</button>
|
||||
<span class="collapse-arrow">▼</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body">
|
||||
<div class="status-section-title">CPU 占用</div>
|
||||
<div class="heap-group cpu-group">
|
||||
<div class="heap-group-title">ESP32 Runtime <span style="font-weight:400;color:var(--text-dim);font-size:10px;">(FreeRTOS)</span></div>
|
||||
<div class="heap-bar-row">
|
||||
<span class="heap-bar-text" id="cpuUsageText">等待采样...</span>
|
||||
<span class="heap-bar-track"><span class="heap-bar-fill cpu-fill" id="cpuUsageFill"></span></span>
|
||||
<span class="heap-bar-pct" id="cpuUsagePct">--</span>
|
||||
</div>
|
||||
<div class="heap-detail-row">
|
||||
<span>空闲: <b id="cpuIdlePct">--</b></span>
|
||||
<span>窗口: <b id="cpuSampleMs">--</b></span>
|
||||
<span>任务数: <b id="cpuTaskCount">--</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-task-list cpu-task-list" id="cpuTaskList">
|
||||
<div class="status-empty">等待下一次状态刷新...</div>
|
||||
</div>
|
||||
|
||||
<div class="status-section-title">堆概况</div>
|
||||
<div class="heap-group">
|
||||
<div class="heap-group-title">内部 DRAM <span style="font-weight:400;color:var(--text-dim);font-size:10px;">(数据堆)</span></div>
|
||||
@@ -327,35 +333,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CAN panel -->
|
||||
<!-- CAN workspace entry -->
|
||||
<div class="card collapsed">
|
||||
<div class="card-head" onclick="toggleCollapse(this)">
|
||||
<span class="ico">📨</span> CAN 帧发送
|
||||
<span class="ico">📨</span> CAN 通信页
|
||||
<span class="collapse-arrow">▼</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="can-grid">
|
||||
<div class="form-row" style="margin:0"><label class="form-label">通道</label>
|
||||
<select class="input" id="canChannel"><option value="0">CH0</option><option value="1">CH1</option><option value="2">CH2</option></select>
|
||||
<div class="can-launch-card">
|
||||
<div class="can-launch-copy">
|
||||
<div class="can-launch-top">
|
||||
<div>
|
||||
<div class="can-launch-kicker">CAN Workspace</div>
|
||||
<h3>独立 CAN 通信工作台</h3>
|
||||
</div>
|
||||
<span class="can-launch-badge">DBC / HEX</span>
|
||||
</div>
|
||||
<p class="can-launch-desc">导入 DBC 后自动读取报文和信号,按物理值实时组帧,并复用现有 <code>/can/send</code> 接口直接发送。</p>
|
||||
<div class="can-launch-points">
|
||||
<span class="can-launch-point">自动解析 DBC</span>
|
||||
<span class="can-launch-point">信号实时组帧</span>
|
||||
<span class="can-launch-point">保留原始 HEX 发送</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin:0"><label class="form-label">扩展帧 ID</label>
|
||||
<input class="input" type="text" id="canId" value="0x18DA00F1">
|
||||
<div class="can-launch-actions">
|
||||
<a class="btn btn-primary can-launch-primary" href="can.html">进入 CAN 页面</a>
|
||||
<button class="btn btn-outline can-launch-secondary" type="button" onclick="window.open('can.html','_blank','noopener')">新窗口打开</button>
|
||||
</div>
|
||||
<div class="form-row" style="margin:0"><label class="form-label">DLC</label>
|
||||
<input class="input" type="number" id="canDlc" value="8" min="1" max="64">
|
||||
<div class="can-launch-foot">
|
||||
<span class="can-launch-foot-label">说明</span>
|
||||
<span>手工 CAN 表单已迁到独立页面,避免和 OTA 控制流混在一起;SPI 调试仍保留在当前页。</span>
|
||||
</div>
|
||||
<div class="form-row" style="margin:0"><label class="form-label">数据 (HEX)</label>
|
||||
<input class="input" type="text" id="canData" value="0102030405060708" placeholder="01020304...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="helper-note" id="canHint">输入十六进制数据后会自动检查字节数是否与 DLC 一致。</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" onclick="sendCanFrame()">发送</button>
|
||||
<button class="btn btn-outline" onclick="clearCanFields()">清空</button>
|
||||
</div>
|
||||
<div id="canResp" style="display:none;margin-top:12px;padding:12px;background:var(--bg-elevated);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:12px;">
|
||||
<div style="color:var(--text-label);margin-bottom:4px;">响应</div>
|
||||
<div id="canRespText" style="color:var(--accent);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -418,7 +426,7 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=20260619e"></script>
|
||||
<script src="app.js?v=20260623a"></script>
|
||||
<script>
|
||||
window.connectionMode = window.connectionMode || 'wifi';
|
||||
window.flashTarget = window.flashTarget || 'esp32';
|
||||
@@ -670,6 +678,6 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
applySharedModeUi();
|
||||
})();
|
||||
</script>
|
||||
<script src="ble_ota.js?v=20260617b"></script>
|
||||
<script src="ble_ota.js?v=20260625a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+626
@@ -370,6 +370,16 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
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,0.025);
|
||||
}
|
||||
.cpu-fill {
|
||||
background: linear-gradient(90deg, #38bdf8, #22c55e);
|
||||
}
|
||||
|
||||
.heap-group-title {
|
||||
font-size: 11px;
|
||||
@@ -769,10 +779,595 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
.can-grid {
|
||||
display: grid; grid-template-columns: 70px 1fr 60px 1fr; gap: 10px;
|
||||
}
|
||||
.can-launch-card {
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(34,211,238,.18), 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(34,211,238,.14);
|
||||
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(34,211,238,.9), rgba(34,211,238,.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: #88ecff;
|
||||
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(34,211,238,.08);
|
||||
border: 1px solid rgba(34,211,238,.18);
|
||||
color: #a9f4ff;
|
||||
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(--accent);
|
||||
box-shadow: 0 0 10px rgba(34,211,238,.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 {
|
||||
box-shadow: 0 10px 24px rgba(34,211,238,.18);
|
||||
}
|
||||
.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(34,211,238,.18);
|
||||
}
|
||||
.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-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;
|
||||
}
|
||||
.spi-grid {
|
||||
display: grid; grid-template-columns: 80px 1fr 80px 80px; gap: 10px;
|
||||
}
|
||||
|
||||
/* === CAN WORKSPACE PAGE === */
|
||||
.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: 1700px;
|
||||
}
|
||||
.can-header {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
z-index: 5;
|
||||
backdrop-filter: blur(14px);
|
||||
background: rgba(15,22,31,.86);
|
||||
}
|
||||
.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(260px, 1.1fr) 180px minmax(220px, .9fr) minmax(260px, 1fr);
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.can-toolbar-block {
|
||||
min-width: 0;
|
||||
}
|
||||
.can-toolbar-upload .upload-zone {
|
||||
height: 100%;
|
||||
min-height: 88px;
|
||||
padding: 18px 14px;
|
||||
}
|
||||
.can-file-info-inline {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.can-file-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
.can-toolbar-foot {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
align-items: start;
|
||||
}
|
||||
.can-table-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
.can-editor-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.can-sidepanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.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: auto;
|
||||
}
|
||||
.message-table {
|
||||
min-width: 1200px;
|
||||
}
|
||||
.message-table-head,
|
||||
.message-row {
|
||||
display: grid;
|
||||
grid-template-columns: 52px minmax(180px, 1.6fr) 150px 88px 88px 76px 76px 150px minmax(220px, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
.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: 11px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
border-right: 1px solid rgba(255,255,255,.03);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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,
|
||||
.history-item-top {
|
||||
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: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.byte-pill {
|
||||
min-width: 62px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(34,211,238,.08);
|
||||
border: 1px solid rgba(34,211,238,.16);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.byte-pill b {
|
||||
display: block;
|
||||
color: var(--text-label);
|
||||
font-size: 10px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.signal-table-host {
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.signal-table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.signal-table-head,
|
||||
.signal-row {
|
||||
display: grid;
|
||||
grid-template-columns: 52px minmax(220px, 1.2fr) minmax(220px, 1fr) minmax(220px, 1fr) minmax(190px, .9fr) minmax(220px, 1fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
.signal-table-head {
|
||||
background: #182330;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.signal-table-body {
|
||||
max-height: 520px;
|
||||
overflow: 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: 10px;
|
||||
max-height: 620px;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.history-list-compact {
|
||||
max-height: 360px;
|
||||
}
|
||||
.history-item {
|
||||
padding: 12px 13px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.history-item-time {
|
||||
color: var(--text-label);
|
||||
font-size: 11px;
|
||||
}
|
||||
.history-item-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .1em;
|
||||
}
|
||||
.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: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
.history-item-hex {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #7ef3ff;
|
||||
word-break: break-all;
|
||||
}
|
||||
.history-item-foot {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* === TOAST === */
|
||||
.toast {
|
||||
position: fixed; top: 45%; left: 50%; transform: translateX(-50%) translateY(30px);
|
||||
@@ -868,6 +1463,13 @@ code { color: #d3f9ff; font-family: var(--font-mono); }
|
||||
@media (max-width: 1100px) {
|
||||
.main-layout { grid-template-columns: 1fr; }
|
||||
.badge-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.can-toolbar-grid,
|
||||
.can-toolbar-foot,
|
||||
.can-editor-layout { grid-template-columns: 1fr; }
|
||||
.message-list {
|
||||
max-height: 420px;
|
||||
}
|
||||
.can-meta-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.container { padding: 16px; }
|
||||
@@ -876,5 +1478,29 @@ code { color: #d3f9ff; font-family: var(--font-mono); }
|
||||
.status-kv-grid, .status-task-meta { grid-template-columns: 1fr; }
|
||||
.can-grid { grid-template-columns: 1fr 1fr; }
|
||||
.btn-row { flex-direction: column; }
|
||||
.can-meta-grid { grid-template-columns: 1fr; }
|
||||
.can-launch-top,
|
||||
.can-launch-actions,
|
||||
.can-launch-foot {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.can-selection-bar,
|
||||
.can-selection-actions,
|
||||
.history-item-top {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.can-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
.can-frame-hex {
|
||||
font-size: 15px;
|
||||
}
|
||||
.message-table,
|
||||
.signal-table {
|
||||
min-width: 980px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user