Files
ESP32_COMM/tools/js/can/signals.js
T
小龙虾 a47a027178 feat(ota):实现ESP32和GD32的OTA上传流程
- 添加了upload-esp32.js用于处理ESP32的OTA上传,包括文件上传、SHA-256验证和设备重启。
- 引入了upload-gd32.js,用于触发GD32 OTA中继并跟踪中继进度。
- 针对OTA流程,增强了会话管理和错误处理能力。
- 通过在上传过程中显示吐司通知和进度更新,改进了用户反馈。
2026-07-15 12:12:19 +08:00

385 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* signals.js - 信号编辑器渲染 + 信号编解码(raw ↔ physical)
*
* 从 tools/can.js 拆分。对应原 can.js 约 629~1028 行(信号行渲染、信号值存取、
* 编码写入、浮点/整数/Motorola/Intel 位操作)及 761 行的 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);
}