feat(main): 添加启动模块自检与汇总功能
- 新增 SystemBoot 类用于跟踪各模块初始化状态,提供位图标记与报告功能 - 在 main.cpp 中重构初始化流程,按顺序初始化各模块并记录状态 - 添加 system_boot.cpp 模块文件到构建配置 - CAN反馈任务增加冷启动延迟,避免与GD32初始化窗口冲突 refactor(ota): 优化OTA协调器线程安全机制 - 修改 lock()/unlock() 方法返回take结果,确保mutex正确释放 - 修复多任务并发时mutex死锁问题,避免vTaskPriorityDisinherit断言 - 统一finalize失败后的状态复位逻辑,确保协调器回到IDLE状态 - 优化忙状态超时检测与自愈机制 perf(boot): 优化冷启动时序与资源管理 - 简化README中冷启动优化说明,聚焦核心逻辑 - 优化BOD阈值设置为LVL5,改进电源稳定性 - 增加BLE OTA工作线程优先级,减少丢包情况 - 优化堆告警节流机制,在OTA期间降低告警频率 fix(relay): 改进GD32应用存活检测逻辑 - 调整GD32 APP存活验证时序,增加会话建立验证 - 修复HELLO建立session后未验证role的问题 - 优化FINISH后等待APP启动的静默时间与重试机制 - 统一GD32探测逻辑到OTA中继任务 refactor(server): 优化HTTP服务端内存与性能 - 使用静态缓冲区替代动态分配,减少堆碎片化风险 - 优化CAN批量发送期间暂停反馈任务的机制 - 调整BLE连接参数请求逻辑,提升传输性能 - 改进错误处理流程,避免堆紧张时的额外分配 bump(version): 更新固件版本至0.3.50 - 更新版本号从0.3.49到0.3.50 - 更新编译时间戳与Git哈希值
This commit is contained in:
+6
-6
@@ -4,11 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 CAN 通信控制台</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260717w">
|
||||
<link rel="stylesheet" href="css/base.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260717x">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260717x">
|
||||
</head>
|
||||
<body class="can-page">
|
||||
<div class="container can-container">
|
||||
@@ -318,6 +318,6 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/can.js?v=20260717w"></script>
|
||||
<script type="module" src="js/can.js?v=20260717x"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+167
-1
@@ -122,6 +122,9 @@
|
||||
font-size: 20px; font-weight: 700; word-break: break-word;
|
||||
}
|
||||
.metric .v.highlight { color: var(--accent); }
|
||||
.metric .v.ok { color: var(--green); }
|
||||
.metric .v.warn { color: var(--amber); }
|
||||
.metric .v.err { color: var(--red); }
|
||||
|
||||
.status-kv-grid {
|
||||
display: grid;
|
||||
@@ -150,7 +153,134 @@
|
||||
word-break: break-word;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.status-section-title { margin-top: 14px; margin-bottom: 8px; font-size: 11px; color: var(--text-label); text-transform: uppercase; letter-spacing: .08em; }
|
||||
|
||||
/* === 健康状态 → 启动自检(点击展开,非常驻) === */
|
||||
.badge-stat-click {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: border-color .15s ease, background .15s ease;
|
||||
}
|
||||
.badge-stat-click:hover,
|
||||
.badge-stat-click:focus-visible {
|
||||
outline: none;
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--bg-elevated));
|
||||
}
|
||||
.badge-stat-click.is-open {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
|
||||
}
|
||||
.health-boot-hint {
|
||||
font-size: 9px;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.badge-stat-click.is-open .health-boot-hint {
|
||||
color: var(--accent);
|
||||
}
|
||||
.health-boot-panel {
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
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: 6px;
|
||||
}
|
||||
.health-boot-title {
|
||||
font-size: 11px;
|
||||
color: var(--text-label);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.health-boot-summary {
|
||||
font-size: 13px;
|
||||
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: 8px;
|
||||
}
|
||||
.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 CHIPS (/system/status boot) === */
|
||||
.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: 5px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-root);
|
||||
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(--green);
|
||||
border-color: color-mix(in srgb, var(--green) 35%, var(--border));
|
||||
}
|
||||
.boot-mod.ok .boot-mod-dot { background: var(--green); }
|
||||
.boot-mod.fail {
|
||||
color: var(--red);
|
||||
border-color: color-mix(in srgb, var(--red) 35%, var(--border));
|
||||
}
|
||||
.boot-mod.fail .boot-mod-dot { background: var(--red); }
|
||||
.boot-mod.fail.optional {
|
||||
color: var(--amber);
|
||||
border-color: color-mix(in srgb, var(--amber) 35%, var(--border));
|
||||
}
|
||||
.boot-mod.fail.optional .boot-mod-dot { background: var(--amber); }
|
||||
|
||||
.status-section-title {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-label);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
font-family: var(--font);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ===== 堆概况 ===== */
|
||||
.heap-group {
|
||||
@@ -179,6 +309,7 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.heap-bar-row {
|
||||
@@ -196,6 +327,7 @@
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
/* 百分比数值与文案同一行;进度条单独占下一行 */
|
||||
@@ -208,6 +340,7 @@
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.heap-bar-track {
|
||||
@@ -257,6 +390,39 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 抽屉内数值/任务名/JSON 强制等宽,避免被其它样式冲掉 */
|
||||
.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);
|
||||
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.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.status-task-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
||||
+21
-10
@@ -9,11 +9,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 OTA 中继控制台</title>
|
||||
<link rel="stylesheet" href="css/base.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260716q">
|
||||
<link rel="stylesheet" href="css/base.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/layout.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/components.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/can.css?v=20260718b">
|
||||
<link rel="stylesheet" href="css/responsive.css?v=20260718b">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -244,9 +244,11 @@
|
||||
<div class="v err" id="bConn">离线</div>
|
||||
<div class="k">连接状态</div>
|
||||
</div>
|
||||
<div class="badge-stat">
|
||||
<div class="badge-stat badge-stat-click" id="healthBadge"
|
||||
role="button" tabindex="0" title="点击查看启动自检"
|
||||
aria-expanded="false" aria-controls="healthBootPanel">
|
||||
<div class="v" id="mHealth">--</div>
|
||||
<div class="k">健康状态</div>
|
||||
<div class="k">健康状态 <span class="health-boot-hint">▾ 自检</span></div>
|
||||
</div>
|
||||
<div class="badge-stat">
|
||||
<div class="v err" id="connBadge">offline</div>
|
||||
@@ -257,6 +259,15 @@
|
||||
<div class="k">GATT 状态</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 启动自检:点健康状态展开,默认折叠 -->
|
||||
<div class="health-boot-panel collapsed" id="healthBootPanel" hidden>
|
||||
<div class="health-boot-head">
|
||||
<span class="health-boot-title">启动自检</span>
|
||||
<span class="health-boot-summary" id="mBootResult">--</span>
|
||||
</div>
|
||||
<div class="health-boot-failed">失败/跳过:<b id="mBootFailed">--</b></div>
|
||||
<div class="boot-module-list" id="bootModuleList" aria-label="启动模块列表"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -425,8 +436,8 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/app.js?v=20260716q"></script>
|
||||
<script type="module" src="js/ota/console-mode.js?v=20260716q"></script>
|
||||
<script type="module" src="js/ble_ota.js?v=20260716q"></script>
|
||||
<script type="module" src="js/app.js?v=20260718b"></script>
|
||||
<script type="module" src="js/ota/console-mode.js?v=20260718b"></script>
|
||||
<script type="module" src="js/ble_ota.js?v=20260718d"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const UUIDS = {
|
||||
version: 0x0004,
|
||||
};
|
||||
|
||||
export const UI_BUILD = '2026-07-16-ble-begin-ready';
|
||||
export const UI_BUILD = '2026-07-18-ble-fix';
|
||||
export const BLE_OTA_NOTIFY_ACK = 0xA1;
|
||||
export const BLE_OTA_NOTIFY_FINISH = 0xB1;
|
||||
export const BLE_OTA_NOTIFY_SHA256 = 0xC1;
|
||||
|
||||
@@ -199,8 +199,10 @@ export function handleNotify(event) {
|
||||
updateProgressFromAck(state.activeTotalSize, state.activePacketTotal, state.lastAckBytes);
|
||||
}
|
||||
|
||||
const ackText = `[ACK] cmd=0x${ackCmd.toString(16).padStart(2, '0').toUpperCase()} seq=${ackSeq} bytes=${state.lastAckBytes} (${formatBytes(state.lastAckBytes)}) raw=${hex}`;
|
||||
bleLog('ACK', ackText);
|
||||
/* 传输期少打日志:整窗 ACK 频繁时 hex 刷屏拖慢 UI/主线程 */
|
||||
if (!state.uploading || (ackSeq % 16) === 0 || ackSeq < 2) {
|
||||
bleLog('ACK', `cmd=0x${ackCmd.toString(16).padStart(2, '0').toUpperCase()} seq=${ackSeq} bytes=${state.lastAckBytes} (${formatBytes(state.lastAckBytes)})`);
|
||||
}
|
||||
|
||||
// ★ 放宽匹配条件:只要 cmd 匹配 + generation 匹配 + 字节数足够,即接受 ACK
|
||||
// 不再要求 ackSeq >= expectedSeq,因为 ESP32 可能用较低的 seq 发送 ACK
|
||||
|
||||
@@ -238,6 +238,7 @@ export async function uploadFirmware() {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Windows WebBT 同时只允许一个 GATT 操作,必须逐包 await,禁止 Promise.all 并发写 */
|
||||
await state.dataInChar.writeValueWithoutResponse(packet);
|
||||
}
|
||||
|
||||
|
||||
+12
-16
@@ -20,7 +20,7 @@
|
||||
import {
|
||||
state, el, getBaseUrl, getDeviceIp, setDeviceIp, parseDbc, readDbcText, buildSummary,
|
||||
rebuildMergedDbc, makeDbcSourceId
|
||||
} from './can/dbc.js?v=20260717w';
|
||||
} from './can/dbc.js?v=20260717x';
|
||||
import {
|
||||
updateDbcFileInfo,
|
||||
filterMessages,
|
||||
@@ -30,12 +30,11 @@ import {
|
||||
recomputeTransmitterFilterEmpty,
|
||||
updateNodeFilterLabel,
|
||||
updateBatchUi
|
||||
} from './can/messages.js?v=20260717w';
|
||||
} from './can/messages.js?v=20260717x';
|
||||
import {
|
||||
ensureMessageDefaults,
|
||||
renderSelectedMessage,
|
||||
updateEncodedPreview
|
||||
} from './can/signals.js?v=20260717w';
|
||||
} from './can/signals.js?v=20260717x';
|
||||
import {
|
||||
renderHistory,
|
||||
sendSelectedMessage,
|
||||
@@ -44,15 +43,15 @@ import {
|
||||
sendManualFrame,
|
||||
resetManualFields,
|
||||
loadPreviewToManual
|
||||
} from './can/send.js?v=20260717w';
|
||||
} from './can/send.js?v=20260717x';
|
||||
import {
|
||||
scheduleCanPersist,
|
||||
loadCanSession,
|
||||
bindCanPersistLifecycle
|
||||
} from './can/persist.js?v=20260717w';
|
||||
import { escapeHtml as sanitizeHtml, formatUptime } from './common/utils.js?v=20260717w';
|
||||
import { showToast } from './common/toast.js?v=20260717w';
|
||||
import { initCanMonitor } from './can_monitor.js?v=20260717w';
|
||||
} from './can/persist.js?v=20260717x';
|
||||
import { escapeHtml as sanitizeHtml, formatUptime } from './common/utils.js?v=20260717x';
|
||||
import { showToast } from './common/toast.js?v=20260717x';
|
||||
import { initCanMonitor } from './can_monitor.js?v=20260717x';
|
||||
|
||||
// ==================== 本地工具 ====================
|
||||
// 原 can.js 的 formatUptime 输出格式与 common/utils.js 略有差异(d>0 时只到 h),
|
||||
@@ -63,10 +62,10 @@ import { initCanMonitor } from './can_monitor.js?v=20260717w';
|
||||
function cacheElements() {
|
||||
[
|
||||
'toolIp', 'defaultChannel', 'manualChannel', 'manualId', 'manualDlc', 'manualData',
|
||||
'dbcDropZone', 'dbcFileInput', 'dbcFileInfo', 'dbcFileName', 'dbcMessageCount',
|
||||
'dbcDropZone', 'dbcFileInput', 'dbcFileInfo', 'dbcMessageCount',
|
||||
'dbcSignalCount', 'dbcNodeCount', 'dbcFileSize', 'dbcFileCount', 'dbcFileChips',
|
||||
'dbcSearch', 'dbcHint', 'clearDbcBtn',
|
||||
'importDbcBtn', 'replaceDbcBtn',
|
||||
'importDbcBtn',
|
||||
'messageList', 'sendSelectedBtn', 'generatedHex', 'generatedBytes', 'encodeHint',
|
||||
'dbcResp', 'dbcRespText', 'loadPreviewToRawBtn', 'manualHint', 'sendManualBtn',
|
||||
'resetManualBtn', 'manualResp', 'manualRespText', 'historyList', 'toast', 'toastIco',
|
||||
@@ -149,11 +148,10 @@ function afterDbcSourcesChanged(opts) {
|
||||
if (opts.resetSelection || !state.selectedMessageKey) {
|
||||
selectMessage(state.dbc.messages[0].key);
|
||||
} else {
|
||||
renderSelectedMessage();
|
||||
updateEncodedPreview();
|
||||
}
|
||||
} else {
|
||||
renderSelectedMessage();
|
||||
updateEncodedPreview();
|
||||
}
|
||||
if (el.dbcHint) {
|
||||
el.dbcHint.hidden = true;
|
||||
@@ -268,7 +266,6 @@ function bindEvents() {
|
||||
if (el.manualChannel) el.manualChannel.value = el.defaultChannel.value;
|
||||
if (el.canChannelPill) el.canChannelPill.textContent = 'CH' + el.defaultChannel.value;
|
||||
renderMessageList();
|
||||
renderSelectedMessage();
|
||||
updateEncodedPreview();
|
||||
scheduleCanPersist();
|
||||
});
|
||||
@@ -277,7 +274,6 @@ function bindEvents() {
|
||||
if (el.dbcFileInput) el.dbcFileInput.click();
|
||||
}
|
||||
if (el.importDbcBtn) el.importDbcBtn.addEventListener('click', openDbcPicker);
|
||||
if (el.replaceDbcBtn) el.replaceDbcBtn.addEventListener('click', openDbcPicker);
|
||||
if (el.dbcDropZone) {
|
||||
el.dbcDropZone.addEventListener('dragover', function (event) {
|
||||
event.preventDefault();
|
||||
@@ -389,7 +385,7 @@ function init() {
|
||||
selectMessage(state.dbc.messages[0].key);
|
||||
}
|
||||
} else {
|
||||
renderSelectedMessage();
|
||||
updateEncodedPreview();
|
||||
}
|
||||
updateBatchUi();
|
||||
refreshManualHint();
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
* 包含:共享 state/el、基础工具、DBC 解析、cssEscape、readDbcText。
|
||||
*
|
||||
* 本模块是 can 子系统的基础层,不依赖其他 can 子模块;state/el 在此导出,
|
||||
* 供 messages/signals/send/feedback 共享(ES Module 的 live binding 保证引用一致)。
|
||||
* 供 messages/signals/send 共享(ES Module 的 live binding 保证引用一致)。
|
||||
*/
|
||||
|
||||
// ==================== 共享状态 ====================
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* feedback.js - 1Hz 反馈轮询(预留模块)
|
||||
*
|
||||
* 重新导出 common/connection.js 的连接管理函数,供 can 入口文件按需使用。
|
||||
* 1Hz 轮询 /can/feedback 或 ID 快照功能可在本模块按需实现。
|
||||
*/
|
||||
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi, testConnection, refreshSystemStatusNow } from '../common/connection.js';
|
||||
|
||||
// 重新导出 common/connection.js 的连接管理函数,供 can 入口文件按需使用。
|
||||
export { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi, testConnection, refreshSystemStatusNow };
|
||||
@@ -5,12 +5,12 @@
|
||||
*
|
||||
* 依赖:dbc.js(state/el/padHex/normalizeWhitespace/cssEscape)、
|
||||
* common/utils.js(escapeHtml/formatBytes)、
|
||||
* signals.js(renderInlineSignalGrid/ensureMessageDefaults/renderSelectedMessage/updateEncodedPreview)。
|
||||
* signals.js(renderInlineSignalGrid/ensureMessageDefaults/updateEncodedPreview)。
|
||||
*/
|
||||
import { state, el, padHex, normalizeWhitespace, cssEscape } from './dbc.js?v=20260717v';
|
||||
import { escapeHtml as sanitizeHtml, formatBytes } from '../common/utils.js?v=20260717v';
|
||||
import { renderInlineSignalGrid, ensureMessageDefaults, renderSelectedMessage, updateEncodedPreview } from './signals.js?v=20260717v';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717v';
|
||||
import { state, el, padHex, normalizeWhitespace, cssEscape } from './dbc.js?v=20260717x';
|
||||
import { escapeHtml as sanitizeHtml, formatBytes } from '../common/utils.js?v=20260717x';
|
||||
import { renderInlineSignalGrid, ensureMessageDefaults, updateEncodedPreview } from './signals.js?v=20260717x';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717x';
|
||||
|
||||
// ==================== DBC 文件信息 ====================
|
||||
/** @description 更新 DBC 文件信息卡片(文件名/报文数/信号数/节点数/大小)。 */
|
||||
@@ -395,7 +395,6 @@ export function selectMessage(key) {
|
||||
state.selectedMessageKey = key;
|
||||
ensureMessageDefaults(msg);
|
||||
renderMessageList();
|
||||
renderSelectedMessage();
|
||||
updateEncodedPreview();
|
||||
scheduleCanPersist();
|
||||
}
|
||||
|
||||
+1
-12
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* 依赖:dbc.js(state/el/parseDbc/rebuildMergedDbc)
|
||||
*/
|
||||
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js?v=20260717v';
|
||||
import { state, el, parseDbc, rebuildMergedDbc } from './dbc.js?v=20260717x';
|
||||
|
||||
var LS_KEY = 'dcu_can_session_v1';
|
||||
var MAX_HISTORY = 200;
|
||||
@@ -135,17 +135,6 @@ export function scheduleCanPersist() {
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/** @description 清空持久化(全部清除 DBC 时)。 */
|
||||
export function clearCanSessionPersist() {
|
||||
if (saveTimer) {
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = null;
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(LS_KEY);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 从 localStorage 恢复会话到 state + 表单。
|
||||
* @returns {{ restored: boolean, dbcCount: number, historyCount: number }}
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
* 注意:formatTime 保留本地实现(HH:MM:SS,不含毫秒),与 common/utils.js
|
||||
* 的 formatTime(含毫秒)不同,以保证发送历史时间戳输出格式一致。
|
||||
*/
|
||||
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js?v=20260717w';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717w';
|
||||
import { showToast } from '../common/toast.js?v=20260717w';
|
||||
import { updateBatchUi } from './messages.js?v=20260717w';
|
||||
import { ensureMessageDefaults, getSignalValueMap, getSelectedMessage, updateEncodedPreview, encodeMessageData } from './signals.js?v=20260717w';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717w';
|
||||
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js?v=20260717x';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717x';
|
||||
import { showToast } from '../common/toast.js?v=20260717x';
|
||||
import { updateBatchUi } from './messages.js?v=20260717x';
|
||||
import { ensureMessageDefaults, getSignalValueMap, getSelectedMessage, updateEncodedPreview, encodeMessageData } from './signals.js?v=20260717x';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717x';
|
||||
|
||||
// ==================== 本地工具(保持与原 can.js 一致) ====================
|
||||
function formatTime(date) {
|
||||
@@ -109,7 +109,7 @@ async function sendCanBatchRequest(channel, frames) {
|
||||
if (!json.results || !Array.isArray(json.results)) {
|
||||
throw new Error('批量响应缺少 results');
|
||||
}
|
||||
/* 设备中途 fatal 会 skipped 剩余帧(少分配堆);前端补齐等长 results */
|
||||
/* 设备可能返回 skipped;补齐与 frames 等长的 results */
|
||||
var results = json.results.slice();
|
||||
var need = frames.length - results.length;
|
||||
if (need > 0) {
|
||||
@@ -172,12 +172,12 @@ export function renderHistory() {
|
||||
}
|
||||
|
||||
// ==================== 批量发送 ====================
|
||||
/** 单批最多帧数(与设备 /can/send_batch 上限 128 对齐;一次 HTTP 连发,避免分块成“一阵一阵”) */
|
||||
/** 单批最多帧数(与设备 /can/send_batch 上限一致) */
|
||||
var BATCH_CHUNK = 128;
|
||||
|
||||
/**
|
||||
* @description 批量发送所有勾选的报文。
|
||||
* 优先一次 HTTP /can/send_batch(设备侧帧间 5ms);失败则回退单帧(无额外间隔)。
|
||||
* 优先 POST /can/send_batch;不支持时回退逐帧 /can/send。
|
||||
* 批量过程中不重绘历史,结束后统一刷新。
|
||||
*/
|
||||
export async function sendCheckedMessages() {
|
||||
|
||||
+4
-15
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* signals.js - 信号编辑器渲染 + 信号编解码(raw ↔ physical)
|
||||
*
|
||||
* 包含:信号行渲染、信号值存取、编码写入、浮点/整数/Motorola/Intel 位操作、
|
||||
* renderSelectedMessage shim。
|
||||
* 包含:行内信号表格、信号值存取、编码写入、浮点/整数/Motorola/Intel 位操作。
|
||||
*
|
||||
* 依赖:dbc.js(state/el/padHex/bytesToHex/cssEscape/findSignal)、common/utils.js(escapeHtml)。
|
||||
*/
|
||||
import { state, el, padHex, bytesToHex, cssEscape, findSignal } from './dbc.js?v=20260717v';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717v';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717v';
|
||||
import { state, el, padHex, bytesToHex, cssEscape, findSignal } from './dbc.js?v=20260717x';
|
||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717x';
|
||||
import { scheduleCanPersist } from './persist.js?v=20260717x';
|
||||
|
||||
// ==================== 信号值存取 ====================
|
||||
/** @description 确保报文有 signalValues bag,并为每个信号填充默认值。返回 bag。 */
|
||||
@@ -81,15 +80,6 @@ export function getActiveSignals(message) {
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 旧版 shim ====================
|
||||
/**
|
||||
* @description 旧版"顶部独立信号编辑面板"的兼容 shim。
|
||||
* 该面板已移除,信号编辑现在通过报文行展开完成;
|
||||
* 仍被 selectMessage、handleDbcFile、clearDbc、handleSignalInputEvent 调用,
|
||||
* 保留空函数以避免修改所有调用方。
|
||||
*/
|
||||
export function renderSelectedMessage() {}
|
||||
|
||||
// ==================== 信号行渲染 ====================
|
||||
/** @description 在报文展开行内渲染信号表格(inline grid)。 */
|
||||
export function renderInlineSignalGrid(message, host) {
|
||||
@@ -178,7 +168,6 @@ function handleSignalInputEvent(evt) {
|
||||
setSignalValue(message, name, input.value);
|
||||
var signal = findSignal(message, name);
|
||||
if (signal && signal.multiplexRole === 'mux') {
|
||||
if (key === state.selectedMessageKey) renderSelectedMessage();
|
||||
var inlineGrid = document.querySelector('.message-row-expand[data-key="' + cssEscape(key) + '"] .signal-table-host');
|
||||
if (inlineGrid) renderInlineSignalGrid(message, inlineGrid);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* can_monitor.js - CAN 接收监控(独立页 can_monitor.html 与 can.html 右侧抽屉共用)
|
||||
*/
|
||||
import { showToast } from './common/toast.js?v=20260717v';
|
||||
import { showToast } from './common/toast.js?v=20260717x';
|
||||
|
||||
var state = { fbTimer: null, fbAbortCtrl: null };
|
||||
|
||||
|
||||
@@ -49,11 +49,105 @@ export function updateHealthUi(healthCode, freeHeap) {
|
||||
mh.textContent = health;
|
||||
mh.className = 'v ' + (healthCode === 0 ? 'ok' : healthCode === 1 ? 'warn' : 'err');
|
||||
}
|
||||
ensureHealthBootToggleBound();
|
||||
}
|
||||
/**
|
||||
* 每次 pollStatus 成功后刷新堆/栈/CPU(固定周期约 1000ms,中继期 400ms;
|
||||
* sample_ms 由设备两次采样墙钟差给出;WiFi 上传 ESP32 期间 pause 不刷)。
|
||||
*/
|
||||
|
||||
/** 健康状态 badge 点击展开/收起启动自检面板(只绑一次)。 */
|
||||
function ensureHealthBootToggleBound() {
|
||||
var badge = document.getElementById('healthBadge');
|
||||
var panel = document.getElementById('healthBootPanel');
|
||||
if (!badge || !panel || badge.dataset.bootToggleBound === '1') return;
|
||||
badge.dataset.bootToggleBound = '1';
|
||||
|
||||
function toggle() {
|
||||
var open = panel.classList.contains('collapsed') || panel.hasAttribute('hidden');
|
||||
if (open) {
|
||||
panel.hidden = false;
|
||||
panel.classList.remove('collapsed');
|
||||
badge.classList.add('is-open');
|
||||
badge.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
panel.hidden = true;
|
||||
panel.classList.add('collapsed');
|
||||
badge.classList.remove('is-open');
|
||||
badge.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
badge.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
});
|
||||
badge.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 启动自检内容(/system/status.boot);挂在健康状态展开区,非常驻。 */
|
||||
function updateBootInitUi(data) {
|
||||
var boot = data && data.boot ? data.boot : null;
|
||||
var resultEl = document.getElementById('mBootResult');
|
||||
var failedEl = document.getElementById('mBootFailed');
|
||||
var listEl = document.getElementById('bootModuleList');
|
||||
if (!boot) {
|
||||
if (resultEl) {
|
||||
resultEl.textContent = '--';
|
||||
resultEl.className = 'health-boot-summary';
|
||||
}
|
||||
if (failedEl) {
|
||||
failedEl.textContent = '--';
|
||||
failedEl.className = '';
|
||||
}
|
||||
if (listEl) listEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
var criticalOk = !!boot.critical_ok;
|
||||
var allOk = !!boot.all_ok;
|
||||
var summary = boot.summary || (allOk ? 'OK (all modules)' : (criticalOk ? 'OK (critical)' : 'FAIL'));
|
||||
var failed = (boot.failed && String(boot.failed).trim()) ? String(boot.failed) : '';
|
||||
var tone = allOk ? 'ok' : (criticalOk ? 'warn' : 'err');
|
||||
|
||||
if (resultEl) {
|
||||
resultEl.textContent = summary;
|
||||
resultEl.className = 'health-boot-summary ' + tone;
|
||||
resultEl.title = 'mask=0x' + (Number(boot.mask) || 0).toString(16);
|
||||
}
|
||||
if (failedEl) {
|
||||
failedEl.textContent = failed || (allOk ? '无' : '--');
|
||||
failedEl.className = failed ? tone : 'ok';
|
||||
}
|
||||
|
||||
if (listEl) {
|
||||
var modules = Array.isArray(boot.modules) ? boot.modules : [];
|
||||
if (!modules.length) {
|
||||
listEl.innerHTML = '';
|
||||
} else {
|
||||
listEl.innerHTML = modules.map(function (m) {
|
||||
var ok = !!m.ok;
|
||||
var critical = !!m.critical;
|
||||
var cls = 'boot-mod ' + (ok ? 'ok' : (critical ? 'fail' : 'fail optional'));
|
||||
var tip = (m.name || '') + (ok ? ' 成功' : ' 失败') + (critical ? ' · 关键' : ' · 非关键');
|
||||
return '<span class="' + cls + '" title="' + escapeHtml(tip) + '">' +
|
||||
'<span class="boot-mod-dot"></span>' +
|
||||
escapeHtml(m.name || '?') +
|
||||
'</span>';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
if (!window._bootInitLogged) {
|
||||
window._bootInitLogged = true;
|
||||
var logLine = '启动自检: ' + summary + (failed ? ' · 失败/跳过: ' + failed : '');
|
||||
try {
|
||||
addLog(allOk ? 'I' : (criticalOk ? 'W' : 'E'), 'BOOT', logLine);
|
||||
} catch (_) {}
|
||||
}
|
||||
ensureHealthBootToggleBound();
|
||||
}
|
||||
|
||||
export function updateHeapAndTaskStatusUi(data) {
|
||||
if (!data) return;
|
||||
var ota = data.ota || {};
|
||||
@@ -62,6 +156,8 @@ export function updateHeapAndTaskStatusUi(data) {
|
||||
if (el) el.textContent = value;
|
||||
};
|
||||
|
||||
updateBootInitUi(data);
|
||||
|
||||
// ---- 堆概况:已用 / 总计 + 占用条 ----
|
||||
function setFill(id, pct) {
|
||||
var el = document.getElementById(id);
|
||||
|
||||
+25
-1
@@ -208,7 +208,13 @@ export function updateSystemStatusUi(data) {
|
||||
var displayRelayState = isGd32Complete ? 5 : (isGd32Relaying && rs === 0 ? 1 : rs);
|
||||
if (window.currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); }
|
||||
|
||||
if (isGd32Error && (window.currentOtaTarget === 'gd32' || window.otaSessionTarget === 'gd32')) {
|
||||
/* 本会话正在 GD32 路径(含仅 RELAYING 而 target 被清的情况) */
|
||||
var gd32Session =
|
||||
window.otaSessionState === OtaSessionState.RELAYING ||
|
||||
window.currentOtaTarget === 'gd32' ||
|
||||
window.otaSessionTarget === 'gd32';
|
||||
|
||||
if (isGd32Error && gd32Session) {
|
||||
var failDetail = 'GD32 OTA 失败';
|
||||
if (relayTotal > 0) {
|
||||
failDetail += '(已转发 ' + relaySeq + '/' + relayTotal +
|
||||
@@ -218,6 +224,7 @@ export function updateSystemStatusUi(data) {
|
||||
}
|
||||
window.cleanupAfterOtaFailure(failDetail);
|
||||
window.pollLogs();
|
||||
return;
|
||||
} else if (isGd32Error) {
|
||||
// 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理
|
||||
updateStagesFromRelayState(255);
|
||||
@@ -233,6 +240,23 @@ export function updateSystemStatusUi(data) {
|
||||
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
|
||||
window.gd32RelaySessionLive = true;
|
||||
}
|
||||
/*
|
||||
* 设备侧 ERROR 常被 status 软清为 IDLE,前端收不到 code=8。
|
||||
* 已见真实中继、设备已回 IDLE、且未宣布完成 → 按失败收尾,释放会话。
|
||||
*/
|
||||
var deviceIdleAfterRelay =
|
||||
(sysStateCode === 0 || sysStateCode === 2) &&
|
||||
(rs === 0 || rs === 5) &&
|
||||
window.gd32RelaySessionLive &&
|
||||
!window.gd32CompleteAnnounced;
|
||||
if (deviceIdleAfterRelay) {
|
||||
window.cleanupAfterOtaFailure(
|
||||
'GD32 中继已结束但未成功(设备已回 IDLE' +
|
||||
(relayTotal > 0 ? ',已转发 ' + relaySeq + '/' + relayTotal : '') + ')'
|
||||
);
|
||||
window.pollLogs();
|
||||
return;
|
||||
}
|
||||
/* 包进度:进入 FINISHING/COMPLETE 时先补 100%,再走完成/收尾 UI */
|
||||
if (window.gd32RelaySessionLive && relayTotal > 0 &&
|
||||
(relaySeq >= relayTotal || rs >= 4 || isGd32Complete)) {
|
||||
|
||||
@@ -58,6 +58,31 @@ export async function startUpload(chip) {
|
||||
if (!file) return;
|
||||
// ★ 状态机守卫:用统一的会话活跃判断替代裸 isUploading。
|
||||
// FAILED/DONE 会话需要先复位才能发起新上传(下方 otaSessionReset)。
|
||||
// RELAYING 可能因 ERROR 被设备软清为 IDLE 而前端未收尾,先探测再决定。
|
||||
if (otaSessionActive()) {
|
||||
var busyState = window.otaSessionState;
|
||||
if (busyState === OtaSessionState.RELAYING || busyState === OtaSessionState.VERIFYING) {
|
||||
try {
|
||||
var staleProbe = await fetch(window.getBaseUrl() + '/system/status', {
|
||||
signal: AbortSignal.timeout(2000)
|
||||
});
|
||||
if (staleProbe.ok) {
|
||||
var staleSt = await staleProbe.json();
|
||||
var so = staleSt.ota || {};
|
||||
var ssc0 = Number.isFinite(so.sys_state_code) ? so.sys_state_code : -1;
|
||||
var rs0 = Number.isFinite(so.relay_state) ? so.relay_state : -1;
|
||||
/* 设备已空闲/完成/错误残留:前端会话是陈旧活跃态,强制清掉 */
|
||||
var deviceFree = (ssc0 === 0 || ssc0 === 2 || ssc0 === 4 || ssc0 === 8) &&
|
||||
(rs0 === 0 || rs0 === 5 || rs0 === 255 || rs0 < 0);
|
||||
if (deviceFree) {
|
||||
addLog('W', '上传', '检测到陈旧会话 ' + busyState +
|
||||
'(设备已 ' + (so.sys_state || ssc0) + '),自动清理后继续');
|
||||
otaSessionReset();
|
||||
}
|
||||
}
|
||||
} catch (_) { /* 探测失败则仍按占用拒绝 */ }
|
||||
}
|
||||
}
|
||||
if (otaSessionActive()) {
|
||||
var busyChip = (window.otaSessionTarget || window.activeUploadChip || window.flashTarget || chip || 'ota').toUpperCase();
|
||||
addLog('W', '上传', '已有 ' + busyChip + ' OTA 会话进行中 (' + window.otaSessionState + '),拒绝新的 ' + chip.toUpperCase() + ' 上传');
|
||||
@@ -70,11 +95,7 @@ export async function startUpload(chip) {
|
||||
window.otaSessionTarget = chip;
|
||||
|
||||
window.pausePollingForUpload();
|
||||
// ★ 必须先抢锁再 await:守卫同步设置,await 期间重复点击才能被拦下。
|
||||
// 旧实现把 isUploading 设在 await handle.getFile() 之后 → 用户在 BLE 期间
|
||||
// 狂点 WiFi 上传按钮时,多个 startUpload 都能通过守卫,最终并发 xhr.send,
|
||||
// 设备端互斥被绕过、403 路径走漏。
|
||||
// ★ 状态机:进入 UPLOADING 态(内部同步 isUploading = true)
|
||||
/* 先进入 UPLOADING(同步 isUploading),再 await 读文件,防止重复提交 */
|
||||
otaSessionTransition(OtaSessionState.UPLOADING);
|
||||
window.activeUploadChip = chip;
|
||||
/* 新上传:清掉上次完成标志,避免立刻被 status 残留判成“又完成了” */
|
||||
|
||||
@@ -159,9 +159,39 @@ export function startRelayProgressWatch() {
|
||||
if (rstate >= 1 && rstate <= 4) {
|
||||
window.gd32RelaySessionLive = true;
|
||||
}
|
||||
/* 中继失败:必须把前端会话从 RELAYING 拉到 FAILED,否则无法新开上传 */
|
||||
if (code === 8 || rstate === 255) {
|
||||
stopRelayProgressWatch();
|
||||
window.pollLogs();
|
||||
if (window.otaSessionState === OtaSessionState.RELAYING ||
|
||||
window.otaSessionState === OtaSessionState.UPLOADING) {
|
||||
var failMsg = 'GD32 中继失败(relay_state=' + rstate +
|
||||
', sys=' + (o.sys_state || code) + ')';
|
||||
if (typeof window.cleanupAfterOtaFailure === 'function') {
|
||||
window.cleanupAfterOtaFailure(failMsg);
|
||||
} else {
|
||||
otaSessionTransition(OtaSessionState.FAILED, failMsg);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* ERROR 可能在 status 路径被软清为 IDLE,前端收不到 code=8。
|
||||
* 本会话已见过真实中继,设备却已回 IDLE 且未宣布完成 → 按失败清理。
|
||||
*/
|
||||
var idleAfterRelay = (code === 0 || code === 2) &&
|
||||
(rstate === 0 || rstate === 5) &&
|
||||
window.gd32RelaySessionLive &&
|
||||
!window.gd32CompleteAnnounced &&
|
||||
window.otaSessionState === OtaSessionState.RELAYING;
|
||||
if (idleAfterRelay) {
|
||||
stopRelayProgressWatch();
|
||||
window.pollLogs();
|
||||
if (typeof window.cleanupAfterOtaFailure === 'function') {
|
||||
window.cleanupAfterOtaFailure('GD32 中继已结束但未成功(设备已回 IDLE)');
|
||||
} else {
|
||||
otaSessionTransition(OtaSessionState.FAILED, 'GD32 中继已结束但未成功');
|
||||
}
|
||||
return;
|
||||
}
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user