- 帧间添加5ms延时,暂停CAN反馈避免与批量SPI竞争导致偶发崩溃 - 单次批量容量从48帧提升至128帧,最大body大小从12KB增至48KB - 单帧超时从400ms降至80ms,失败后不再拖慢整体速度 - 暂停0xC1反馈任务降低worker竞态和WDT风险 - 中途致命错误后跳过后续帧发送,避免每帧等待超时 - 添加详细的JSON错误响应格式 - 修复内存分配失败时的资源清理问题 refactor(wifi): 优化冷启动流程避免电源竞争 - 电源轨稳定等待策略调整:POWERON/BROWNOUT复位时等待200ms - 仅BROWNOUT复位后清除PHY校准数据,避免每次上电全量校准 - 电源稳定等待期间等待400ms后再进行NVS/WiFi操作 - WiFi启动前延时从500ms调整为300ms - BOD阈值适当放宽避免上电瞬间复位 perf(monitor): 调整堆内存监控策略 - 内存紧张时增大健康监控间隔:低于8KB时从3s增至10s - 避免日志输出消耗更多堆内存造成雪崩效应 docs(readme): 更新冷启动优化说明 - 详细描述电源轨稳定等待机制 - 说明仅BROWNOUT后才强制PHY重校准的策略 - 解释避免与GD32抢电流的设计考虑 style(html): 更新前端资源版本号 - 更新CSS和JavaScript文件的版本参数 - 修复页面加载时的资源缓存问题 fix(ble): 修复阶段显示状态问题 - 终态阶段全部显示为完成状态(绿色) - 优化状态栏样式更新逻辑 feat(ota): 优化SPI通信超时处理 - 将总超时拆分为发送和轮询两个阶段 - 禁止轮询单独占用500ms避免短超时批量卡顿 - 优化极短超时情况下的时间分配策略 refactor(can): 减少批量操作时的日志输出 - SPI失败和GD32拒绝时按类型聚合日志 - 每32次相同错误才输出一次完整日志 - 避免大量错误时UDP/格式化耗尽堆内存
412 lines
16 KiB
JavaScript
412 lines
16 KiB
JavaScript
/**
|
||
* 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?v=20260717v';
|
||
import { escapeHtml as sanitizeHtml } from '../common/utils.js?v=20260717v';
|
||
import { scheduleCanPersist } from './persist.js?v=20260717v';
|
||
|
||
// ==================== 信号值存取 ====================
|
||
/** @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 = '<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 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 = '<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(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-msg="' + sanitizeHtml(message.key) + '" data-signal="' + sanitizeHtml(signal.name) +
|
||
'" value="' + sanitizeHtml(bag[signal.name]) + '">';
|
||
}
|
||
return '' +
|
||
'<div class="signal-row">' +
|
||
'<span class="signal-cell signal-cell-index">' + (index + 1) + '</span>' +
|
||
'<span class="signal-cell signal-cell-name">' +
|
||
'<b>' + sanitizeHtml(signal.name) + '</b>' +
|
||
'<small>' + sanitizeHtml((signal.receivers || []).join(', ') || '-') + '</small>' +
|
||
'</span>' +
|
||
'<span class="signal-cell signal-cell-value">' + inputHtml + '</span>' +
|
||
'<span class="signal-cell signal-cell-range">' +
|
||
'<b>' + sanitizeHtml(rangeText) + '</b>' +
|
||
(enumKeys.length ? '<small>' + sanitizeHtml(enumKeys.slice(0, 3).map(function (key) { return key + ':' + signal.enumMap[key]; }).join(' / ')) + (enumKeys.length > 3 ? ' ...' : '') + '</small>' : '<small>' + sanitizeHtml(factorText) + '</small>') +
|
||
'</span>' +
|
||
'<span class="signal-cell signal-cell-meta">' +
|
||
'<b>' + sanitizeHtml(bitRange) + '</b>' +
|
||
'<small>' + sanitizeHtml(typeText) + '</small>' +
|
||
'</span>' +
|
||
'<span class="signal-cell signal-cell-comment" title="' + sanitizeHtml(signal.comment || '') + '">' + sanitizeHtml(comment) + '</span>' +
|
||
'</div>';
|
||
}
|
||
|
||
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();
|
||
scheduleCanPersist();
|
||
}
|
||
|
||
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 <b>' + message.dlc + '</b>,当前编码 <b>' + state.generatedData.length +
|
||
'</b> 字节,发送通道 <b>CH' + (el.defaultChannel ? el.defaultChannel.value : '0') + '</b>。';
|
||
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 '<div class="status-empty">无数据</div>';
|
||
var html = [];
|
||
for (var i = 0; i < bytes.length; i++) {
|
||
html.push('<span class="byte-pill"><b>B' + i + '</b><span>' + padHex(bytes[i], 2) + '</span></span>');
|
||
}
|
||
return html.join('');
|
||
}
|
||
|
||
// ==================== 信号编解码(raw ↔ physical) ====================
|
||
/** @description 将报文 + 信号值编码为 Uint8Array。 */
|
||
export function encodeMessageData(message, signalValues) {
|
||
var dlc = message && message.dlc != null ? Number(message.dlc) : 0;
|
||
if (!Number.isFinite(dlc) || dlc < 0) dlc = 0;
|
||
if (dlc > 64) dlc = 64;
|
||
/* DLC=0:无数据区(如 CANFD_BOOT),不写信号,直接空帧 */
|
||
if (dlc === 0) {
|
||
return new Uint8Array(0);
|
||
}
|
||
var bytes = new Uint8Array(dlc);
|
||
var activeSignals = getActiveSignals(message);
|
||
for (var i = 0; i < activeSignals.length; i++) {
|
||
var signal = activeSignals[i];
|
||
/* 信号完全落在 DLC 外则跳过,避免把空帧/定义异常拖成批量失败 */
|
||
if (!signalFitsInDlc(signal, dlc)) {
|
||
continue;
|
||
}
|
||
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;
|
||
}
|
||
|
||
/** @description 粗判信号位域是否至少有一部分落在 dlc 字节内(越界整段则跳过)。 */
|
||
function signalFitsInDlc(signal, dlc) {
|
||
if (!signal || dlc <= 0) return false;
|
||
var maxBit = dlc * 8 - 1;
|
||
if (signal.byteOrder === 'little') {
|
||
var end = signal.startBit + signal.length - 1;
|
||
return signal.startBit >= 0 && signal.startBit <= maxBit && end <= maxBit;
|
||
}
|
||
/* Motorola:用起始位所在字节粗判;更严的检查仍在 setBitByIndex */
|
||
var startByte = Math.floor(signal.startBit / 8);
|
||
return startByte >= 0 && startByte < dlc && signal.length > 0;
|
||
}
|
||
|
||
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 +
|
||
'(缓冲区 ' + bytes.length + ' 字节 / DLC=' + bytes.length + ')');
|
||
}
|
||
if (bitValue) bytes[byteIndex] |= (1 << bitInByte);
|
||
else bytes[byteIndex] &= ~(1 << bitInByte);
|
||
}
|