feat(can): 添加CAN总线错误计数器和总线状态监控
添加了硬件发送错误计数器(tec)、接收错误计数器(rec)和总线状态位图(bus_state)字段, 用于更全面的CAN总线状态监控。同时新增CAN_RX_MODE_BRIDGE_0_TO_12模式,支持CAN0到 CAN1和CAN2的桥接功能。 BREAKING CHANGE: CAN反馈协议格式扩展,增加了3个字节的状态信息。
This commit is contained in:
+464
-95
@@ -8,9 +8,14 @@
|
||||
filteredMessages: [],
|
||||
selectedMessageKey: '',
|
||||
signalValues: {},
|
||||
checkedKeys: {},
|
||||
expandedKeys: {},
|
||||
transmitterFilter: Object.create(null),
|
||||
transmitterFilterEmpty: true,
|
||||
generatedData: new Uint8Array(0),
|
||||
history: [],
|
||||
latestStatus: null
|
||||
latestStatus: null,
|
||||
batchInFlight: false
|
||||
};
|
||||
|
||||
var el = {};
|
||||
@@ -21,13 +26,14 @@
|
||||
'testConnBtn', 'connIndicator', 'connIndicatorTxt', 'connectionNote',
|
||||
'dbcDropZone', 'dbcFileInput', 'dbcFileInfo', 'dbcFileName', 'dbcMessageCount',
|
||||
'dbcSignalCount', 'dbcNodeCount', 'dbcFileSize', 'dbcSearch', 'dbcHint', 'clearDbcBtn',
|
||||
'messageList', 'emptyMessageState', 'messageWorkspace', 'selectedMessageTitle',
|
||||
'selectedMessageSubtitle', 'selectedMessageMeta', 'selectedMessageComment',
|
||||
'sendSelectedBtn', 'signalGrid', 'generatedHex', 'generatedBytes', 'encodeHint',
|
||||
'messageList', 'sendSelectedBtn', 'generatedHex', 'generatedBytes', 'encodeHint',
|
||||
'dbcResp', 'dbcRespText', 'loadPreviewToRawBtn', 'manualHint', 'sendManualBtn',
|
||||
'resetManualBtn', 'manualResp', 'manualRespText', 'historyList', 'toast', 'toastIco',
|
||||
'toastMsg', 'canConnPill', 'canConnTxt', 'canVersionPill', 'canChannelPill',
|
||||
'dbcSummaryPill', 'messageSummaryPill'
|
||||
'dbcSummaryPill', 'messageSummaryPill',
|
||||
'selectedCountPill', 'selectAllBox', 'expandAllBtn', 'collapseAllBtn', 'sendCheckedBtn',
|
||||
'nodeFilterWrap', 'nodeFilterBtn', 'nodeFilterLabel', 'nodeFilterPanel', 'nodeFilterSearch',
|
||||
'nodeFilterAllBtn', 'nodeFilterNoneBtn', 'nodeFilterList'
|
||||
].forEach(function (id) {
|
||||
el[id] = document.getElementById(id);
|
||||
});
|
||||
@@ -338,6 +344,12 @@
|
||||
function filterMessages() {
|
||||
var keyword = normalizeWhitespace(el.dbcSearch ? el.dbcSearch.value : '').toLowerCase();
|
||||
var messages = state.dbc ? state.dbc.messages.slice() : [];
|
||||
if (!state.transmitterFilterEmpty) {
|
||||
messages = messages.filter(function (msg) {
|
||||
var tx = msg.transmitter || 'Vector__XXX';
|
||||
return !!state.transmitterFilter[tx];
|
||||
});
|
||||
}
|
||||
if (keyword) {
|
||||
messages = messages.filter(function (msg) {
|
||||
var idHex = '0x' + padHex(msg.id, msg.isExtended ? 8 : 3);
|
||||
@@ -353,39 +365,167 @@
|
||||
}
|
||||
state.filteredMessages = messages;
|
||||
renderMessageList();
|
||||
updateNodeFilterLabel();
|
||||
}
|
||||
|
||||
function collectTransmitters() {
|
||||
var set = Object.create(null);
|
||||
var list = [];
|
||||
if (!state.dbc) return list;
|
||||
state.dbc.messages.forEach(function (msg) {
|
||||
var tx = msg.transmitter || 'Vector__XXX';
|
||||
if (!set[tx]) { set[tx] = 1; list.push(tx); }
|
||||
});
|
||||
list.sort(function (a, b) { return a.localeCompare(b); });
|
||||
return list;
|
||||
}
|
||||
|
||||
function renderNodeFilterList() {
|
||||
if (!el.nodeFilterList) return;
|
||||
var txs = collectTransmitters();
|
||||
var kw = (el.nodeFilterSearch && el.nodeFilterSearch.value || '').trim().toLowerCase();
|
||||
if (kw) txs = txs.filter(function (n) { return n.toLowerCase().indexOf(kw) >= 0; });
|
||||
if (!txs.length) {
|
||||
el.nodeFilterList.innerHTML = '<div class="status-empty">无匹配节点</div>';
|
||||
return;
|
||||
}
|
||||
var counts = Object.create(null);
|
||||
if (state.dbc) {
|
||||
state.dbc.messages.forEach(function (msg) {
|
||||
var tx = msg.transmitter || 'Vector__XXX';
|
||||
counts[tx] = (counts[tx] || 0) + 1;
|
||||
});
|
||||
}
|
||||
el.nodeFilterList.innerHTML = txs.map(function (name) {
|
||||
var checked = !!state.transmitterFilter[name] ? ' checked' : '';
|
||||
return '<label class="can-node-filter-item">' +
|
||||
'<input type="checkbox" data-node="' + sanitizeHtml(name) + '"' + checked + '>' +
|
||||
'<span class="can-node-filter-name">' + sanitizeHtml(name) + '</span>' +
|
||||
'<span class="can-node-filter-count">' + (counts[name] || 0) + '</span>' +
|
||||
'</label>';
|
||||
}).join('');
|
||||
var inputs = el.nodeFilterList.querySelectorAll('input[type="checkbox"]');
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
inputs[i].addEventListener('change', function () {
|
||||
var name = this.getAttribute('data-node');
|
||||
if (this.checked) state.transmitterFilter[name] = true;
|
||||
else delete state.transmitterFilter[name];
|
||||
recomputeTransmitterFilterEmpty();
|
||||
filterMessages();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function recomputeTransmitterFilterEmpty() {
|
||||
var any = false;
|
||||
for (var k in state.transmitterFilter) {
|
||||
if (state.transmitterFilter[k]) { any = true; break; }
|
||||
}
|
||||
state.transmitterFilterEmpty = !any;
|
||||
}
|
||||
|
||||
function updateNodeFilterLabel() {
|
||||
if (!el.nodeFilterLabel) return;
|
||||
if (state.transmitterFilterEmpty) {
|
||||
el.nodeFilterLabel.textContent = '发送节点:全部';
|
||||
if (el.nodeFilterBtn) el.nodeFilterBtn.classList.remove('active');
|
||||
} else {
|
||||
var picked = Object.keys(state.transmitterFilter).filter(function (k) { return state.transmitterFilter[k]; });
|
||||
var preview = picked.length === 1 ? picked[0] : ('已选 ' + picked.length + ' 个');
|
||||
el.nodeFilterLabel.textContent = '发送节点:' + preview;
|
||||
if (el.nodeFilterBtn) el.nodeFilterBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function setNodeFilterPanelOpen(open) {
|
||||
if (!el.nodeFilterPanel || !el.nodeFilterBtn) return;
|
||||
if (open) {
|
||||
renderNodeFilterList();
|
||||
el.nodeFilterPanel.hidden = false;
|
||||
el.nodeFilterBtn.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
el.nodeFilterPanel.hidden = true;
|
||||
el.nodeFilterBtn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function bindNodeFilterEvents() {
|
||||
if (!el.nodeFilterBtn) return;
|
||||
el.nodeFilterBtn.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
setNodeFilterPanelOpen(el.nodeFilterPanel.hidden);
|
||||
});
|
||||
document.addEventListener('click', function (event) {
|
||||
if (!el.nodeFilterWrap || el.nodeFilterPanel.hidden) return;
|
||||
if (!el.nodeFilterWrap.contains(event.target)) setNodeFilterPanelOpen(false);
|
||||
});
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && el.nodeFilterPanel && !el.nodeFilterPanel.hidden) {
|
||||
setNodeFilterPanelOpen(false);
|
||||
}
|
||||
});
|
||||
if (el.nodeFilterSearch) {
|
||||
el.nodeFilterSearch.addEventListener('input', renderNodeFilterList);
|
||||
}
|
||||
if (el.nodeFilterAllBtn) el.nodeFilterAllBtn.addEventListener('click', function () {
|
||||
collectTransmitters().forEach(function (n) { state.transmitterFilter[n] = true; });
|
||||
recomputeTransmitterFilterEmpty();
|
||||
renderNodeFilterList();
|
||||
filterMessages();
|
||||
});
|
||||
if (el.nodeFilterNoneBtn) el.nodeFilterNoneBtn.addEventListener('click', function () {
|
||||
state.transmitterFilter = Object.create(null);
|
||||
state.transmitterFilterEmpty = true;
|
||||
renderNodeFilterList();
|
||||
filterMessages();
|
||||
});
|
||||
}
|
||||
|
||||
function renderMessageList() {
|
||||
if (!el.messageList) return;
|
||||
if (!state.dbc) {
|
||||
el.messageList.innerHTML = '<div class="status-empty">等待导入 DBC...</div>';
|
||||
updateBatchUi();
|
||||
return;
|
||||
}
|
||||
if (!state.filteredMessages.length) {
|
||||
el.messageList.innerHTML = '<div class="status-empty">没有匹配的报文。</div>';
|
||||
updateBatchUi();
|
||||
return;
|
||||
}
|
||||
var channelTxt = el.defaultChannel ? el.defaultChannel.value : '0';
|
||||
var rows = state.filteredMessages.map(function (msg, index) {
|
||||
var active = msg.key === state.selectedMessageKey ? ' active' : '';
|
||||
var checked = state.checkedKeys[msg.key] ? ' checked' : '';
|
||||
var expanded = !!state.expandedKeys[msg.key];
|
||||
var caret = expanded ? '\u25BE' : '\u25B8';
|
||||
var idHex = '0x' + padHex(msg.id, msg.isExtended ? 8 : 3);
|
||||
var comment = msg.comment || '';
|
||||
if (comment.length > 36) comment = comment.slice(0, 36) + '...';
|
||||
return '' +
|
||||
'<button class="message-row' + active + '" data-key="' + sanitizeHtml(msg.key) + '">' +
|
||||
var rowHtml = '' +
|
||||
'<div class="message-row' + active + '" data-key="' + sanitizeHtml(msg.key) + '" role="button" tabindex="0">' +
|
||||
'<span class="message-col message-col-check"><input type="checkbox" class="message-check" data-key="' + sanitizeHtml(msg.key) + '"' + checked + '></span>' +
|
||||
'<span class="message-col message-col-toggle"><button type="button" class="message-toggle' + (expanded ? ' expanded' : '') + '" data-key="' + sanitizeHtml(msg.key) + '" aria-label="展开/收起信号">' + caret + '</button></span>' +
|
||||
'<span class="message-col message-col-index">' + (index + 1) + '</span>' +
|
||||
'<span class="message-col message-col-name">' + sanitizeHtml(msg.name) + '</span>' +
|
||||
'<span class="message-col message-col-name" title="' + sanitizeHtml(msg.name) + '">' + sanitizeHtml(msg.name) + '</span>' +
|
||||
'<span class="message-col message-col-id">' + sanitizeHtml(idHex) + '</span>' +
|
||||
'<span class="message-col message-col-channel">CH' + sanitizeHtml(el.defaultChannel ? el.defaultChannel.value : '0') + '</span>' +
|
||||
'<span class="message-col message-col-channel">CH' + sanitizeHtml(channelTxt) + '</span>' +
|
||||
'<span class="message-col message-col-type">' + sanitizeHtml(msg.isExtended ? '扩展' : '标准') + '</span>' +
|
||||
'<span class="message-col message-col-dlc">' + sanitizeHtml(msg.dlc) + '</span>' +
|
||||
'<span class="message-col message-col-signal">' + sanitizeHtml(msg.signals.length) + '</span>' +
|
||||
'<span class="message-col message-col-node">' + sanitizeHtml(msg.transmitter || 'Vector__XXX') + '</span>' +
|
||||
'<span class="message-col message-col-node" title="' + sanitizeHtml(msg.transmitter || 'Vector__XXX') + '">' + sanitizeHtml(msg.transmitter || 'Vector__XXX') + '</span>' +
|
||||
'<span class="message-col message-col-comment" title="' + sanitizeHtml(msg.comment || '') + '">' + sanitizeHtml(comment || '-') + '</span>' +
|
||||
'</button>';
|
||||
'</div>';
|
||||
var expandHtml = '<div class="message-row-expand" data-key="' + sanitizeHtml(msg.key) + '"' + (expanded ? '' : ' hidden') + '>' +
|
||||
'<div class="signal-table-host"></div>' +
|
||||
'</div>';
|
||||
return rowHtml + expandHtml;
|
||||
}).join('');
|
||||
el.messageList.innerHTML =
|
||||
'<div class="message-table">' +
|
||||
'<div class="message-table-head">' +
|
||||
'<span class="message-col message-col-check"></span>' +
|
||||
'<span class="message-col message-col-toggle"></span>' +
|
||||
'<span class="message-col message-col-index">#</span>' +
|
||||
'<span class="message-col message-col-name">报文名称</span>' +
|
||||
'<span class="message-col message-col-id">标识符</span>' +
|
||||
@@ -398,14 +538,123 @@
|
||||
'</div>' +
|
||||
'<div class="message-table-body">' + rows + '</div>' +
|
||||
'</div>';
|
||||
var buttons = el.messageList.querySelectorAll('.message-row');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
buttons[i].addEventListener('click', function () {
|
||||
selectMessage(this.getAttribute('data-key'));
|
||||
|
||||
var rowEls = el.messageList.querySelectorAll('.message-row');
|
||||
for (var i = 0; i < rowEls.length; i++) {
|
||||
rowEls[i].addEventListener('click', handleMessageRowClick);
|
||||
rowEls[i].addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
selectMessage(this.getAttribute('data-key'));
|
||||
}
|
||||
});
|
||||
}
|
||||
var checks = el.messageList.querySelectorAll('.message-check');
|
||||
for (var j = 0; j < checks.length; j++) {
|
||||
checks[j].addEventListener('click', function (event) { event.stopPropagation(); });
|
||||
checks[j].addEventListener('change', function () {
|
||||
var k = this.getAttribute('data-key');
|
||||
if (this.checked) state.checkedKeys[k] = true;
|
||||
else delete state.checkedKeys[k];
|
||||
updateBatchUi();
|
||||
});
|
||||
}
|
||||
var toggles = el.messageList.querySelectorAll('.message-toggle');
|
||||
for (var t = 0; t < toggles.length; t++) {
|
||||
toggles[t].addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
toggleMessageExpand(this.getAttribute('data-key'));
|
||||
});
|
||||
}
|
||||
|
||||
state.filteredMessages.forEach(function (msg) {
|
||||
if (state.expandedKeys[msg.key]) {
|
||||
var host = el.messageList.querySelector('.message-row-expand[data-key="' + cssEscape(msg.key) + '"] .signal-table-host');
|
||||
if (host) renderInlineSignalGrid(msg, host);
|
||||
}
|
||||
});
|
||||
updateBatchUi();
|
||||
}
|
||||
|
||||
function handleMessageRowClick(event) {
|
||||
if (event.target.closest('.message-check') || event.target.closest('.message-toggle')) return;
|
||||
var key = this.getAttribute('data-key');
|
||||
selectMessage(key);
|
||||
}
|
||||
|
||||
function toggleMessageExpand(key) {
|
||||
if (!state.dbc) return;
|
||||
var msg = null;
|
||||
for (var i = 0; i < state.dbc.messages.length; i++) {
|
||||
if (state.dbc.messages[i].key === key) { msg = state.dbc.messages[i]; break; }
|
||||
}
|
||||
if (!msg) return;
|
||||
var willExpand = !state.expandedKeys[key];
|
||||
if (willExpand) {
|
||||
state.expandedKeys[key] = true;
|
||||
ensureMessageDefaults(msg);
|
||||
} else {
|
||||
delete state.expandedKeys[key];
|
||||
}
|
||||
var panel = el.messageList.querySelector('.message-row-expand[data-key="' + cssEscape(key) + '"]');
|
||||
var toggle = el.messageList.querySelector('.message-toggle[data-key="' + cssEscape(key) + '"]');
|
||||
if (panel) {
|
||||
if (willExpand) {
|
||||
panel.hidden = false;
|
||||
var host = panel.querySelector('.signal-table-host');
|
||||
if (host) renderInlineSignalGrid(msg, host);
|
||||
} else {
|
||||
panel.hidden = true;
|
||||
}
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.classList.toggle('expanded', willExpand);
|
||||
toggle.textContent = willExpand ? '\u25BE' : '\u25B8';
|
||||
}
|
||||
}
|
||||
|
||||
function renderInlineSignalGrid(message, host) {
|
||||
if (!host) return;
|
||||
ensureMessageDefaults(message);
|
||||
var activeSignals = getActiveSignals(message);
|
||||
if (!activeSignals.length) {
|
||||
host.innerHTML = '<div class="status-empty">当前报文没有信号定义。</div>';
|
||||
return;
|
||||
}
|
||||
var rows = activeSignals.map(function (signal, index) {
|
||||
return renderSignalRow(message, signal, index);
|
||||
}).join('');
|
||||
host.innerHTML =
|
||||
'<div class="signal-table signal-table-inline">' +
|
||||
'<div class="signal-table-head">' +
|
||||
'<span class="signal-cell signal-cell-index">#</span>' +
|
||||
'<span class="signal-cell signal-cell-name">信号名称</span>' +
|
||||
'<span class="signal-cell signal-cell-value">输入值</span>' +
|
||||
'<span class="signal-cell signal-cell-range">范围 / 枚举</span>' +
|
||||
'<span class="signal-cell signal-cell-meta">位定义</span>' +
|
||||
'<span class="signal-cell signal-cell-comment">注释</span>' +
|
||||
'</div>' +
|
||||
'<div class="signal-table-body">' + rows + '</div>' +
|
||||
'</div>';
|
||||
bindSignalInputs(host, message);
|
||||
}
|
||||
|
||||
function updateBatchUi() {
|
||||
var keys = Object.keys(state.checkedKeys);
|
||||
var count = keys.length;
|
||||
if (el.selectedCountPill) el.selectedCountPill.textContent = '已勾选 ' + count;
|
||||
if (el.sendCheckedBtn) {
|
||||
el.sendCheckedBtn.disabled = count === 0 || state.batchInFlight;
|
||||
el.sendCheckedBtn.textContent = state.batchInFlight ? '发送中...' : ('批量发送勾选 (' + count + ')');
|
||||
}
|
||||
if (el.selectAllBox) {
|
||||
var visible = state.filteredMessages || [];
|
||||
var allChecked = visible.length > 0 && visible.every(function (m) { return !!state.checkedKeys[m.key]; });
|
||||
var someChecked = visible.some(function (m) { return !!state.checkedKeys[m.key]; });
|
||||
el.selectAllBox.checked = allChecked;
|
||||
el.selectAllBox.indeterminate = !allChecked && someChecked;
|
||||
}
|
||||
}
|
||||
function selectMessage(key) {
|
||||
if (!state.dbc) return;
|
||||
var msg = null;
|
||||
@@ -417,15 +666,43 @@
|
||||
}
|
||||
if (!msg) return;
|
||||
state.selectedMessageKey = key;
|
||||
state.signalValues = {};
|
||||
msg.signals.forEach(function (signal) {
|
||||
state.signalValues[signal.name] = getDefaultSignalValue(signal);
|
||||
});
|
||||
ensureMessageDefaults(msg);
|
||||
renderMessageList();
|
||||
renderSelectedMessage();
|
||||
updateEncodedPreview();
|
||||
}
|
||||
|
||||
function ensureMessageDefaults(message) {
|
||||
if (!message) return null;
|
||||
var bag = state.signalValues[message.key];
|
||||
if (!bag) {
|
||||
bag = {};
|
||||
state.signalValues[message.key] = bag;
|
||||
}
|
||||
for (var i = 0; i < message.signals.length; i++) {
|
||||
var sig = message.signals[i];
|
||||
if (bag[sig.name] == null || bag[sig.name] === '') {
|
||||
bag[sig.name] = getDefaultSignalValue(sig);
|
||||
}
|
||||
}
|
||||
return bag;
|
||||
}
|
||||
|
||||
function getSignalValueMap(message) {
|
||||
if (!message) return {};
|
||||
return state.signalValues[message.key] || ensureMessageDefaults(message);
|
||||
}
|
||||
|
||||
function setSignalValue(message, signalName, value) {
|
||||
if (!message) return;
|
||||
var bag = state.signalValues[message.key];
|
||||
if (!bag) {
|
||||
bag = {};
|
||||
state.signalValues[message.key] = bag;
|
||||
}
|
||||
bag[signalName] = value;
|
||||
}
|
||||
|
||||
function getDefaultSignalValue(signal) {
|
||||
var enumKeys = Object.keys(signal.enumMap || {});
|
||||
if (enumKeys.length) return enumKeys[0];
|
||||
@@ -454,7 +731,8 @@
|
||||
if (!message) return [];
|
||||
var muxSignal = getMultiplexerSignal(message);
|
||||
if (!muxSignal) return message.signals.slice();
|
||||
var currentValue = Number(state.signalValues[muxSignal.name]);
|
||||
var bag = getSignalValueMap(message);
|
||||
var currentValue = Number(bag[muxSignal.name]);
|
||||
return message.signals.filter(function (signal) {
|
||||
if (!signal.multiplexRole || signal.multiplexRole === 'mux') return true;
|
||||
return signal.multiplexRole === 'value' && Number.isFinite(currentValue) && signal.multiplexValue === currentValue;
|
||||
@@ -462,62 +740,20 @@
|
||||
}
|
||||
|
||||
function renderSelectedMessage() {
|
||||
var msg = getSelectedMessage();
|
||||
var hasMessage = !!msg;
|
||||
if (el.emptyMessageState) el.emptyMessageState.style.display = hasMessage ? 'none' : '';
|
||||
if (el.messageWorkspace) el.messageWorkspace.style.display = hasMessage ? '' : 'none';
|
||||
if (!msg) return;
|
||||
|
||||
var idHex = '0x' + padHex(msg.id, msg.isExtended ? 8 : 3);
|
||||
el.selectedMessageTitle.textContent = msg.name;
|
||||
el.selectedMessageSubtitle.textContent = idHex + ' · ' + (msg.isExtended ? '扩展帧' : '标准帧');
|
||||
el.selectedMessageComment.textContent = msg.comment || '暂无报文注释。';
|
||||
var activeSignals = getActiveSignals(msg);
|
||||
var muxSignal = getMultiplexerSignal(msg);
|
||||
el.selectedMessageMeta.innerHTML = '' +
|
||||
metaCard('发送节点', msg.transmitter || 'Vector__XXX') +
|
||||
metaCard('DLC', String(msg.dlc)) +
|
||||
metaCard('信号数', activeSignals.length + ' / ' + msg.signals.length) +
|
||||
metaCard('通道', 'CH' + (el.defaultChannel ? el.defaultChannel.value : '0'));
|
||||
if (muxSignal && el.selectedMessageComment) {
|
||||
var muxValue = state.signalValues[muxSignal.name];
|
||||
el.selectedMessageComment.textContent =
|
||||
(msg.comment ? msg.comment + ' · ' : '') +
|
||||
'Mux ' + muxSignal.name + '=' + muxValue + ',当前展示并编码匹配该值的信号。';
|
||||
}
|
||||
renderSignalGrid(msg);
|
||||
// 顶部独立的“信号编辑”面板已移除,所有信号编辑现在通过报文行展开完成。
|
||||
// 这里保留函数以兼容旧调用方(selectMessage 等),不再触碰已删除的 DOM。
|
||||
}
|
||||
|
||||
function metaCard(label, value) {
|
||||
return '<div class="can-meta-card"><span>' + sanitizeHtml(label) + '</span><b>' + sanitizeHtml(value) + '</b></div>';
|
||||
}
|
||||
|
||||
function renderSignalGrid(message) {
|
||||
if (!el.signalGrid) return;
|
||||
var activeSignals = getActiveSignals(message);
|
||||
if (!activeSignals.length) {
|
||||
el.signalGrid.innerHTML = '<div class="status-empty">当前报文没有信号定义。</div>';
|
||||
return;
|
||||
}
|
||||
var rows = activeSignals.map(function (signal, index) {
|
||||
return renderSignalRow(signal, index);
|
||||
}).join('');
|
||||
el.signalGrid.innerHTML =
|
||||
'<div class="signal-table">' +
|
||||
'<div class="signal-table-head">' +
|
||||
'<span class="signal-cell signal-cell-index">#</span>' +
|
||||
'<span class="signal-cell signal-cell-name">信号名称</span>' +
|
||||
'<span class="signal-cell signal-cell-value">输入值</span>' +
|
||||
'<span class="signal-cell signal-cell-range">范围 / 枚举</span>' +
|
||||
'<span class="signal-cell signal-cell-meta">位定义</span>' +
|
||||
'<span class="signal-cell signal-cell-comment">注释</span>' +
|
||||
'</div>' +
|
||||
'<div class="signal-table-body">' + rows + '</div>' +
|
||||
'</div>';
|
||||
bindSignalInputs(message);
|
||||
function renderSignalGrid(/* message */) {
|
||||
// 旧的独立信号编辑表已移除,信号编辑全部走报文行内展开 renderInlineSignalGrid。
|
||||
}
|
||||
|
||||
function renderSignalRow(signal, index) {
|
||||
function renderSignalRow(message, signal, index) {
|
||||
var bag = getSignalValueMap(message);
|
||||
var enumKeys = Object.keys(signal.enumMap || {});
|
||||
var bitRange = signal.startBit + '|' + signal.length + ' @' + (signal.byteOrder === 'little' ? 'Intel' : 'Motorola');
|
||||
var rangeText = '';
|
||||
@@ -532,16 +768,16 @@
|
||||
if (comment.length > 48) comment = comment.slice(0, 48) + '...';
|
||||
var inputHtml = '';
|
||||
if (enumKeys.length) {
|
||||
inputHtml = '<select class="input signal-input signal-input-compact" data-signal="' + sanitizeHtml(signal.name) + '">' +
|
||||
inputHtml = '<select class="input signal-input signal-input-compact" data-msg="' + sanitizeHtml(message.key) + '" data-signal="' + sanitizeHtml(signal.name) + '">' +
|
||||
enumKeys.map(function (key) {
|
||||
var label = signal.enumMap[key];
|
||||
var selected = String(state.signalValues[signal.name]) === String(key) ? ' selected' : '';
|
||||
var selected = String(bag[signal.name]) === String(key) ? ' selected' : '';
|
||||
return '<option value="' + sanitizeHtml(key) + '"' + selected + '>' +
|
||||
sanitizeHtml(key + ' - ' + label) + '</option>';
|
||||
}).join('') + '</select>';
|
||||
} else {
|
||||
inputHtml = '<input class="input signal-input signal-input-compact" type="number" step="any" data-signal="' + sanitizeHtml(signal.name) +
|
||||
'" value="' + sanitizeHtml(state.signalValues[signal.name]) + '">';
|
||||
inputHtml = '<input class="input signal-input signal-input-compact" type="number" step="any" data-msg="' + sanitizeHtml(message.key) + '" data-signal="' + sanitizeHtml(signal.name) +
|
||||
'" value="' + sanitizeHtml(bag[signal.name]) + '">';
|
||||
}
|
||||
return '' +
|
||||
'<div class="signal-row">' +
|
||||
@@ -563,29 +799,41 @@
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function bindSignalInputs(message) {
|
||||
var inputs = el.signalGrid.querySelectorAll('.signal-input');
|
||||
function handleSignalInputEvent(evt) {
|
||||
var input = evt.currentTarget;
|
||||
var key = input.getAttribute('data-msg');
|
||||
var name = input.getAttribute('data-signal');
|
||||
if (!key || !name || !state.dbc) return;
|
||||
var message = null;
|
||||
for (var i = 0; i < state.dbc.messages.length; i++) {
|
||||
if (state.dbc.messages[i].key === key) { message = state.dbc.messages[i]; break; }
|
||||
}
|
||||
if (!message) return;
|
||||
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);
|
||||
}
|
||||
if (key === state.selectedMessageKey) updateEncodedPreview();
|
||||
}
|
||||
|
||||
function cssEscape(value) {
|
||||
if (window.CSS && CSS.escape) return CSS.escape(value);
|
||||
return String(value).replace(/[^a-zA-Z0-9_-]/g, function (ch) { return '\\\\' + ch; });
|
||||
}
|
||||
|
||||
function bindSignalInputs(container, message) {
|
||||
if (!container) return;
|
||||
var inputs = container.querySelectorAll('.signal-input');
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
inputs[i].addEventListener('input', function () {
|
||||
var name = this.getAttribute('data-signal');
|
||||
state.signalValues[name] = this.value;
|
||||
var signal = findSignal(message, name);
|
||||
if (signal && signal.multiplexRole === 'mux') {
|
||||
renderSelectedMessage();
|
||||
}
|
||||
updateEncodedPreview();
|
||||
});
|
||||
inputs[i].addEventListener('change', function () {
|
||||
var name = this.getAttribute('data-signal');
|
||||
state.signalValues[name] = this.value;
|
||||
var signal = findSignal(message, name);
|
||||
if (signal && signal.multiplexRole === 'mux') {
|
||||
renderSelectedMessage();
|
||||
}
|
||||
updateEncodedPreview();
|
||||
});
|
||||
inputs[i].addEventListener('input', handleSignalInputEvent);
|
||||
inputs[i].addEventListener('change', handleSignalInputEvent);
|
||||
}
|
||||
if (message && message.signals.length && message.key === state.selectedMessageKey) {
|
||||
updateEncodedPreview();
|
||||
}
|
||||
if (message && message.signals.length) updateEncodedPreview();
|
||||
}
|
||||
|
||||
function updateEncodedPreview() {
|
||||
@@ -597,7 +845,7 @@
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.generatedData = encodeMessageData(message, state.signalValues);
|
||||
state.generatedData = encodeMessageData(message, getSignalValueMap(message));
|
||||
var hex = bytesToHex(state.generatedData);
|
||||
el.generatedHex.textContent = hex || '--';
|
||||
el.generatedBytes.innerHTML = renderByteStrip(state.generatedData);
|
||||
@@ -840,6 +1088,79 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function sendCheckedMessages() {
|
||||
if (!state.dbc) { showToast('请先导入 DBC', 'warning'); return; }
|
||||
var keys = Object.keys(state.checkedKeys);
|
||||
if (!keys.length) { showToast('请先勾选要发送的报文', 'warning'); return; }
|
||||
if (state.batchInFlight) return;
|
||||
state.batchInFlight = true;
|
||||
updateBatchUi();
|
||||
var channel = parseInt(el.defaultChannel.value, 10) || 0;
|
||||
var ok = 0, fail = 0;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
var msg = null;
|
||||
for (var m = 0; m < state.dbc.messages.length; m++) {
|
||||
if (state.dbc.messages[m].key === key) { msg = state.dbc.messages[m]; break; }
|
||||
}
|
||||
if (!msg) continue;
|
||||
ensureMessageDefaults(msg);
|
||||
var idHex = '0x' + padHex(msg.id, msg.isExtended ? 8 : 3);
|
||||
var dataBytes;
|
||||
try {
|
||||
dataBytes = encodeMessageData(msg, getSignalValueMap(msg));
|
||||
} catch (err) {
|
||||
fail++;
|
||||
addHistoryEntry({
|
||||
time: formatTime(new Date()),
|
||||
mode: 'BATCH',
|
||||
ok: false,
|
||||
channel: channel,
|
||||
idHex: idHex,
|
||||
dlc: msg.dlc,
|
||||
dataHex: '',
|
||||
resultText: '编码失败: ' + err.message
|
||||
});
|
||||
continue;
|
||||
}
|
||||
var dataHex = bytesToHex(dataBytes);
|
||||
try {
|
||||
var result = await sendCanRequest({
|
||||
ch: channel,
|
||||
id: msg.id,
|
||||
dlc: msg.dlc,
|
||||
data: dataHex
|
||||
});
|
||||
if (result.status === 'ok') ok++; else fail++;
|
||||
addHistoryEntry({
|
||||
time: formatTime(new Date()),
|
||||
mode: 'BATCH',
|
||||
ok: result.status === 'ok',
|
||||
channel: channel,
|
||||
idHex: idHex,
|
||||
dlc: msg.dlc,
|
||||
dataHex: dataHex,
|
||||
resultText: msg.name + ' · ' + buildSendResponseText(result)
|
||||
});
|
||||
} catch (err) {
|
||||
fail++;
|
||||
addHistoryEntry({
|
||||
time: formatTime(new Date()),
|
||||
mode: 'BATCH',
|
||||
ok: false,
|
||||
channel: channel,
|
||||
idHex: idHex,
|
||||
dlc: msg.dlc,
|
||||
dataHex: dataHex,
|
||||
resultText: msg.name + ' · ' + err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
state.batchInFlight = false;
|
||||
updateBatchUi();
|
||||
var tone = fail === 0 ? 'success' : (ok === 0 ? 'error' : 'warning');
|
||||
showToast('批量发送完成:成功 ' + ok + ',失败 ' + fail, tone);
|
||||
}
|
||||
async function sendSelectedMessage() {
|
||||
var msg = getSelectedMessage();
|
||||
if (!msg) {
|
||||
@@ -1001,15 +1322,38 @@
|
||||
showToast('已载入到原始发送区', 'success');
|
||||
}
|
||||
|
||||
async function readDbcText(file) {
|
||||
var buf = await file.arrayBuffer();
|
||||
var bytes = new Uint8Array(buf);
|
||||
var stripBom = function (s) {
|
||||
return (s && s.charCodeAt(0) === 0xFEFF) ? s.slice(1) : s;
|
||||
};
|
||||
try {
|
||||
var strictUtf8 = new TextDecoder('utf-8', { fatal: true });
|
||||
return stripBom(strictUtf8.decode(bytes));
|
||||
} catch (e) {
|
||||
try {
|
||||
var gb = new TextDecoder('gb18030');
|
||||
return stripBom(gb.decode(bytes));
|
||||
} catch (e2) {
|
||||
return stripBom(new TextDecoder('utf-8').decode(bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDbcFile(file) {
|
||||
if (!file) return;
|
||||
var text = await file.text();
|
||||
var text = await readDbcText(file);
|
||||
var dbc = parseDbc(text);
|
||||
state.dbcFile = file;
|
||||
state.dbcText = text;
|
||||
state.dbc = dbc;
|
||||
state.selectedMessageKey = '';
|
||||
state.signalValues = {};
|
||||
state.checkedKeys = {};
|
||||
state.expandedKeys = {};
|
||||
state.transmitterFilter = Object.create(null);
|
||||
state.transmitterFilterEmpty = true;
|
||||
updateDbcFileInfo(file, dbc);
|
||||
buildSummary();
|
||||
filterMessages();
|
||||
@@ -1031,6 +1375,10 @@
|
||||
state.filteredMessages = [];
|
||||
state.selectedMessageKey = '';
|
||||
state.signalValues = {};
|
||||
state.checkedKeys = {};
|
||||
state.expandedKeys = {};
|
||||
state.transmitterFilter = Object.create(null);
|
||||
state.transmitterFilterEmpty = true;
|
||||
state.generatedData = new Uint8Array(0);
|
||||
if (el.dbcFileInput) el.dbcFileInput.value = '';
|
||||
if (el.dbcSearch) el.dbcSearch.value = '';
|
||||
@@ -1083,6 +1431,27 @@
|
||||
el.manualDlc.addEventListener('input', refreshManualHint);
|
||||
el.manualId.addEventListener('input', refreshManualHint);
|
||||
el.toolIp.addEventListener('change', testConnection);
|
||||
if (el.sendCheckedBtn) el.sendCheckedBtn.addEventListener('click', sendCheckedMessages);
|
||||
if (el.selectAllBox) el.selectAllBox.addEventListener('change', function () {
|
||||
var visible = state.filteredMessages || [];
|
||||
if (this.checked) {
|
||||
visible.forEach(function (m) { state.checkedKeys[m.key] = true; });
|
||||
} else {
|
||||
visible.forEach(function (m) { delete state.checkedKeys[m.key]; });
|
||||
}
|
||||
renderMessageList();
|
||||
});
|
||||
if (el.expandAllBtn) el.expandAllBtn.addEventListener('click', function () {
|
||||
var visible = state.filteredMessages || [];
|
||||
visible.forEach(function (m) { state.expandedKeys[m.key] = true; ensureMessageDefaults(m); });
|
||||
renderMessageList();
|
||||
});
|
||||
if (el.collapseAllBtn) el.collapseAllBtn.addEventListener('click', function () {
|
||||
var visible = state.filteredMessages || [];
|
||||
visible.forEach(function (m) { delete state.expandedKeys[m.key]; });
|
||||
renderMessageList();
|
||||
});
|
||||
bindNodeFilterEvents();
|
||||
}
|
||||
|
||||
function handleDbcError(err) {
|
||||
|
||||
Reference in New Issue
Block a user