移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2 CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发 实现流量控制,提高OTA传输稳定性。 refactor(stack): 统一任务栈大小配置并添加CPU统计功能 - 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配 - 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样 - 更新main.cpp中的任务创建使用新的栈大小常量 - 调整health_monitor和ota_relay等任务的栈分配 perf(ota): 改进ESP32和GD32 OTA分区预擦策略 - ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞 - GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量 - 优化OTA协调器状态管理,修复FINALIZING状态残留问题 fix(log): 改进UDP日志发送的堆内存管理 - 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer - 降低UDP发送跳过的内存阈值从10KB到6KB - 降低日志排水最小内存阈值从8KB到4KB docs(readme): 更新OTA配置参数文档说明 移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为 基于CREDIT背压机制的描述。
385 lines
15 KiB
JavaScript
385 lines
15 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';
|
||
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 = '<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();
|
||
}
|
||
|
||
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 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);
|
||
}
|