/** * signals.js - 信号编辑器渲染 + 信号编解码(raw ↔ physical) * * 包含:信号行渲染、信号值存取、编码写入、浮点/整数/Motorola/Intel 位操作、 * renderSelectedMessage shim。 * * 依赖:dbc.js(state/el/padHex/bytesToHex/cssEscape/findSignal)、common/utils.js(escapeHtml)。 */ import { state, el, padHex, bytesToHex, cssEscape, findSignal } from './dbc.js'; import { escapeHtml as sanitizeHtml } from '../common/utils.js'; // ==================== 信号值存取 ==================== /** @description 确保报文有 signalValues bag,并为每个信号填充默认值。返回 bag。 */ export 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; } export 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) { /* 优先使用 DBC 中 BA_ "GenSigDefaultValue" 声明的物理值,否则回退为 0 */ if (signal && signal.defaultValue != null && Number.isFinite(Number(signal.defaultValue))) { return String(signal.defaultValue); } return '0'; } /** @description 获取当前选中的报文对象(无选中返回 null)。 */ export function getSelectedMessage() { if (!state.dbc || !state.selectedMessageKey) return null; for (var i = 0; i < state.dbc.messages.length; i++) { if (state.dbc.messages[i].key === state.selectedMessageKey) return state.dbc.messages[i]; } return null; } function getMultiplexerSignal(message) { if (!message) return null; for (var i = 0; i < message.signals.length; i++) { if (message.signals[i].multiplexRole === 'mux') return message.signals[i]; } return null; } /** @description 根据当前 multiplexer 值返回激活信号列表。 */ export function getActiveSignals(message) { if (!message) return []; var muxSignal = getMultiplexerSignal(message); if (!muxSignal) return message.signals.slice(); 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; }); } // ==================== 旧版 shim ==================== /** * @description 旧版"顶部独立信号编辑面板"的兼容 shim。 * 该面板已移除,信号编辑现在通过报文行展开完成; * 仍被 selectMessage、handleDbcFile、clearDbc、handleSignalInputEvent 调用, * 保留空函数以避免修改所有调用方。 */ export function renderSelectedMessage() {} // ==================== 信号行渲染 ==================== /** @description 在报文展开行内渲染信号表格(inline grid)。 */ export function renderInlineSignalGrid(message, host) { if (!host) return; ensureMessageDefaults(message); var activeSignals = getActiveSignals(message); if (!activeSignals.length) { host.innerHTML = '
当前报文没有信号定义。
'; return; } var rows = activeSignals.map(function (signal, index) { return renderSignalRow(message, signal, index); }).join(''); host.innerHTML = '
' + '
' + '#' + '信号名称' + '输入值' + '范围 / 枚举' + '位定义' + '注释' + '
' + '
' + rows + '
' + '
'; bindSignalInputs(host, message); } 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 = ''; if (Number.isFinite(signal.min) || Number.isFinite(signal.max)) { rangeText = (Number.isFinite(signal.min) ? signal.min : '-') + ' ~ ' + (Number.isFinite(signal.max) ? signal.max : '-'); } else { rangeText = '未声明'; } var typeText = (signal.valueType || 'int').toUpperCase() + ' / ' + (signal.isSigned ? 'signed' : 'unsigned'); var factorText = 'factor ' + signal.factor + ' / offset ' + signal.offset + (signal.unit ? (' / ' + signal.unit) : ''); var comment = signal.comment || '-'; if (comment.length > 48) comment = comment.slice(0, 48) + '...'; var inputHtml = ''; if (enumKeys.length) { inputHtml = ''; } else { inputHtml = ''; } return '' + '
' + '' + (index + 1) + '' + '' + '' + sanitizeHtml(signal.name) + '' + '' + sanitizeHtml((signal.receivers || []).join(', ') || '-') + '' + '' + '' + inputHtml + '' + '' + '' + sanitizeHtml(rangeText) + '' + (enumKeys.length ? '' + sanitizeHtml(enumKeys.slice(0, 3).map(function (key) { return key + ':' + signal.enumMap[key]; }).join(' / ')) + (enumKeys.length > 3 ? ' ...' : '') + '' : '' + sanitizeHtml(factorText) + '') + '' + '' + '' + sanitizeHtml(bitRange) + '' + '' + sanitizeHtml(typeText) + '' + '' + '' + sanitizeHtml(comment) + '' + '
'; } 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 bindSignalInputs(container, message) { if (!container) return; var inputs = container.querySelectorAll('.signal-input'); for (var i = 0; i < inputs.length; i++) { inputs[i].addEventListener('input', handleSignalInputEvent); inputs[i].addEventListener('change', handleSignalInputEvent); } if (message && message.signals.length && message.key === state.selectedMessageKey) { updateEncodedPreview(); } } // ==================== 编码预览 ==================== /** @description 根据当前选中报文 + 信号值更新编码预览(hex / byte strip / hint)。 */ export function updateEncodedPreview() { var message = getSelectedMessage(); if (!message) { state.generatedData = new Uint8Array(0); if (el.generatedHex) el.generatedHex.textContent = '--'; if (el.generatedBytes) el.generatedBytes.innerHTML = ''; return; } try { state.generatedData = encodeMessageData(message, getSignalValueMap(message)); var hex = bytesToHex(state.generatedData); el.generatedHex.textContent = hex || '--'; el.generatedBytes.innerHTML = renderByteStrip(state.generatedData); var hint = 'DLC ' + message.dlc + ',当前编码 ' + state.generatedData.length + ' 字节,发送通道 CH' + (el.defaultChannel ? el.defaultChannel.value : '0') + '。'; if (el.encodeHint) { el.encodeHint.className = 'helper-note ok'; el.encodeHint.innerHTML = hint; } } catch (err) { state.generatedData = new Uint8Array(0); el.generatedHex.textContent = '--'; el.generatedBytes.innerHTML = ''; if (el.encodeHint) { el.encodeHint.className = 'helper-note err'; el.encodeHint.innerHTML = '编码失败:' + sanitizeHtml(err.message); } } } function renderByteStrip(bytes) { if (!bytes || !bytes.length) return '
无数据
'; var html = []; for (var i = 0; i < bytes.length; i++) { html.push('B' + i + '' + padHex(bytes[i], 2) + ''); } return html.join(''); } // ==================== 信号编解码(raw ↔ physical) ==================== /** @description 将报文 + 信号值编码为 Uint8Array。 */ export function encodeMessageData(message, signalValues) { var bytes = new Uint8Array(message.dlc); var activeSignals = getActiveSignals(message); for (var i = 0; i < activeSignals.length; i++) { var signal = activeSignals[i]; var parsed = parseSignalInput(signal, signalValues[signal.name]); if (signal.valueType === 'float' || signal.valueType === 'double') { writeFloatSignal(bytes, signal, parsed.numeric); } else { writeIntegerSignal(bytes, signal, parsed); } } return bytes; } function parseSignalInput(signal, inputValue) { var value = inputValue; if (value === '' || value == null) value = getDefaultSignalValue(signal); var num = Number(value); if (!Number.isFinite(num)) throw new Error(signal.name + ' 不是合法数字'); if (Number.isFinite(signal.min) && num < signal.min) { throw new Error(signal.name + ' 低于最小值 ' + signal.min); } if (Number.isFinite(signal.max) && num > signal.max) { throw new Error(signal.name + ' 高于最大值 ' + signal.max); } return { numeric: num, text: String(value).trim() }; } function isIntegerLike(value) { return /^-?\d+$/.test(String(value == null ? '' : value).trim()); } function physicalToRawBigInt(signal, parsedValue) { if ((signal.factor === 1 || signal.factor === 1.0) && (signal.offset === 0 || signal.offset === 0.0) && isIntegerLike(parsedValue.text)) { return BigInt(parsedValue.text); } var rawNumber = signal.factor === 0 ? Math.round(signal.offset ? (parsedValue.numeric - signal.offset) : parsedValue.numeric) : Math.round((parsedValue.numeric - signal.offset) / signal.factor); if (!Number.isFinite(rawNumber)) { throw new Error(signal.name + ' 原始值计算失败'); } return BigInt(rawNumber); } function bigPow2(bits) { return 1n << BigInt(bits); } function maxUnsignedValue(bits) { return bigPow2(bits) - 1n; } function minSignedValue(bits) { return -(1n << BigInt(bits - 1)); } function maxSignedValue(bits) { return (1n << BigInt(bits - 1)) - 1n; } function writeIntegerSignal(bytes, signal, parsedValue) { var raw = physicalToRawBigInt(signal, parsedValue); if (signal.length <= 0 || signal.length > 64) { throw new Error(signal.name + ' 位宽超出当前页面支持范围'); } if (signal.isSigned) { var min = minSignedValue(signal.length); var max = maxSignedValue(signal.length); if (raw < min || raw > max) { throw new Error(signal.name + ' 原始值超出带符号范围'); } } else { var maxUnsigned = maxUnsignedValue(signal.length); if (raw < 0 || raw > maxUnsigned) { throw new Error(signal.name + ' 原始值超出无符号范围'); } } var unsignedRaw = raw < 0 ? bigPow2(signal.length) + raw : raw; if (signal.byteOrder === 'little') { for (var i = 0; i < signal.length; i++) { var bitVal = Number((unsignedRaw >> BigInt(i)) & 1n); var bitIndex = signal.startBit + i; setBitByIndex(bytes, bitIndex, bitVal); } return; } var positions = buildMotorolaBitPositions(signal.startBit, signal.length); for (var j = 0; j < positions.length; j++) { var shift = signal.length - 1 - j; var motorolaBit = Number((unsignedRaw >> BigInt(shift)) & 1n); setBitByIndex(bytes, positions[j], motorolaBit); } } function writeFloatSignal(bytes, signal, physicalValue) { if (signal.length !== 32 && signal.length !== 64) { throw new Error(signal.name + ' 浮点信号长度需为 32 或 64 位'); } if (signal.byteOrder !== 'little') { throw new Error(signal.name + ' 浮点信号当前仅支持 Intel 小端'); } if (signal.startBit % 8 !== 0) { throw new Error(signal.name + ' 浮点信号起始位必须按字节对齐'); } var buffer = new ArrayBuffer(signal.length / 8); var view = new DataView(buffer); if (signal.length === 32) view.setFloat32(0, physicalValue, true); else view.setFloat64(0, physicalValue, true); var rawBytes = new Uint8Array(buffer); for (var i = 0; i < rawBytes.length; i++) { bytes[(signal.startBit / 8) + i] = rawBytes[i]; } } function buildMotorolaBitPositions(startBit, length) { var positions = []; var bit = startBit; for (var i = 0; i < length; i++) { positions.push(bit); if (bit % 8 === 0) bit += 15; else bit -= 1; } return positions; } function setBitByIndex(bytes, bitIndex, bitValue) { var byteIndex = Math.floor(bitIndex / 8); var bitInByte = bitIndex % 8; if (byteIndex < 0 || byteIndex >= bytes.length) { throw new Error('信号写入越界,bit=' + bitIndex); } if (bitValue) bytes[byteIndex] |= (1 << bitInByte); else bytes[byteIndex] &= ~(1 << bitInByte); }