feat(ota):实现ESP32和GD32的OTA上传流程

- 添加了upload-esp32.js用于处理ESP32的OTA上传,包括文件上传、SHA-256验证和设备重启。
- 引入了upload-gd32.js,用于触发GD32 OTA中继并跟踪中继进度。
- 针对OTA流程,增强了会话管理和错误处理能力。
- 通过在上传过程中显示吐司通知和进度更新,改进了用户反馈。
This commit is contained in:
小龙虾
2026-07-15 12:12:19 +08:00
parent 5a89cec34b
commit a47a027178
48 changed files with 9201 additions and 8212 deletions
+256
View File
@@ -0,0 +1,256 @@
/**
* dbc.js - DBC 解析器 + 共享状态/元素缓存/基础工具
*
* 从 tools/can.js 拆分。对应原 can.js 约 4~21 行(state/el)、78~104 行(基础工具)、
* 178~335 行(DBC 解析)、830~833 行(cssEscape)、1335~1352 行(readDbcText)。
*
* 本模块是 can 子系统的基础层,不依赖其他 can 子模块;state/el 在此导出,
* 供 messages/signals/send/feedback 共享(ES Module 的 live binding 保证引用一致)。
*/
// ==================== 共享状态 ====================
export var state = {
dbcFile: null,
dbcText: '',
dbc: null,
filteredMessages: [],
selectedMessageKey: '',
signalValues: {},
checkedKeys: {},
expandedKeys: {},
transmitterFilter: Object.create(null),
transmitterFilterEmpty: true,
generatedData: new Uint8Array(0),
history: [],
latestStatus: null,
batchInFlight: false
};
// ==================== 共享元素缓存 ====================
// 由入口文件(js/can.js)的 cacheElements() 填充,各子模块通过 import 引用同一对象。
export var el = {};
// ==================== 基础工具 ====================
/** @description 拼接设备基础 URL(http://<ip>),ip 取自 el.toolIp,默认 192.168.31.127。 */
export function getBaseUrl() {
var ip = (el.toolIp && el.toolIp.value ? el.toolIp.value.trim() : '') || '192.168.31.127';
return 'http://' + ip;
}
/** @description 解析手动输入的 CAN ID(支持 0x/hex/十进制)。 */
export function parseNumericId(input) {
var raw = String(input == null ? '' : input).trim();
if (!raw) return NaN;
if (/^0x/i.test(raw)) return parseInt(raw, 16);
if (/^[0-9]+$/.test(raw)) return parseInt(raw, 10);
if (/^[0-9a-fA-F]+h$/i.test(raw)) return parseInt(raw.slice(0, -1), 16);
if (/^[0-9a-fA-F]+$/.test(raw)) return parseInt(raw, 16);
return NaN;
}
/** @description 将数值格式化为指定宽度的十六进制大写字符串。 */
export function padHex(value, width) {
return value.toString(16).toUpperCase().padStart(width, '0');
}
/** @description 将字节数组转为连续的十六进制字符串。 */
export function bytesToHex(bytes) {
if (!bytes || !bytes.length) return '';
var parts = [];
for (var i = 0; i < bytes.length; i++) {
parts.push(padHex(bytes[i], 2));
}
return parts.join('');
}
/** @description 转义 CSS 选择器中的特殊字符。 */
export 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; });
}
/** @description 去除回车并 trim,用于 DBC 搜索等输入归一化。 */
export function normalizeWhitespace(text) {
return String(text || '').replace(/\r/g, '').trim();
}
// ==================== DBC 文件读取 ====================
/** @description 读取 DBC 文件文本,优先 UTF-8,失败回退 GB18030,自动去 BOM。 */
export 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));
}
}
}
// ==================== DBC 解析 ====================
/** @description 解析 DBC 文本,返回 { messages, nodeNames, messageMap }。 */
export function parseDbc(text) {
var lines = String(text || '').replace(/\r/g, '').split('\n');
var messages = [];
var messageMap = {};
var comments = { messages: {}, signals: {} };
var valueTables = {};
var sigDefaults = {};
var nodeNames = [];
var current = null;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var nodeMatch = line.match(/^BU_\s*:\s*(.*)$/);
if (nodeMatch) {
nodeNames = nodeMatch[1].trim() ? nodeMatch[1].trim().split(/\s+/) : [];
continue;
}
var messageMatch = line.match(/^BO_\s+(\d+)\s+([A-Za-z0-9_]+)\s*:\s*(\d+)\s+(.+)$/);
if (messageMatch) {
current = {
idRaw: parseInt(messageMatch[1], 10),
name: messageMatch[2],
dlc: parseInt(messageMatch[3], 10),
transmitter: messageMatch[4].trim(),
signals: [],
comment: ''
};
current.id = current.idRaw & 0x1FFFFFFF;
current.isExtended = current.idRaw > 0x7FF;
current.key = String(current.idRaw) + ':' + current.name;
messages.push(current);
messageMap[current.idRaw] = current;
continue;
}
var signalMatch = line.match(/^SG_\s+([A-Za-z0-9_]+)(?:\s+((?:M|m-?\d+)))?\s*:\s*(\d+)\|(\d+)@([01])([+-])\s+\(([^\),]+),([^\)]+)\)\s+\[([^\|]*)\|([^\]]*)\]\s+"([^"]*)"\s+(.+)$/);
if (signalMatch && current) {
var muxInfo = parseMultiplexToken(signalMatch[2]);
current.signals.push({
name: signalMatch[1],
startBit: parseInt(signalMatch[3], 10),
length: parseInt(signalMatch[4], 10),
byteOrder: signalMatch[5] === '1' ? 'little' : 'big',
isSigned: signalMatch[6] === '-',
factor: parseFloat(signalMatch[7]),
offset: parseFloat(signalMatch[8]),
min: signalMatch[9].trim() === '' ? null : parseFloat(signalMatch[9]),
max: signalMatch[10].trim() === '' ? null : parseFloat(signalMatch[10]),
unit: signalMatch[11],
receivers: signalMatch[12].split(/\s*,\s*/),
comment: '',
valueType: 'int',
enumMap: {},
defaultValue: null,
multiplexRole: muxInfo.role,
multiplexValue: muxInfo.value
});
continue;
}
var messageCommentMatch = line.match(/^CM_\s+BO_\s+(\d+)\s+"([\s\S]*)";$/);
if (messageCommentMatch) {
comments.messages[messageCommentMatch[1]] = messageCommentMatch[2];
continue;
}
var signalCommentMatch = line.match(/^CM_\s+SG_\s+(\d+)\s+([A-Za-z0-9_]+)\s+"([\s\S]*)";$/);
if (signalCommentMatch) {
comments.signals[signalCommentMatch[1] + ':' + signalCommentMatch[2]] = signalCommentMatch[3];
continue;
}
var valueMatch = line.match(/^VAL_\s+(\d+)\s+([A-Za-z0-9_]+)\s+(.+);$/);
if (valueMatch) {
var table = {};
var pairRe = /(-?\d+)\s+"([^"]*)"/g;
var m;
while ((m = pairRe.exec(valueMatch[3])) !== null) {
table[m[1]] = m[2];
}
valueTables[valueMatch[1] + ':' + valueMatch[2]] = table;
continue;
}
var valTypeMatch = line.match(/^SIG_VALTYPE_\s+(\d+)\s+([A-Za-z0-9_]+)\s*:\s*(\d+)\s*;$/);
if (valTypeMatch) {
var msg = messageMap[valTypeMatch[1]];
if (!msg) continue;
var signal = findSignal(msg, valTypeMatch[2]);
if (signal) {
signal.valueType = valTypeMatch[3] === '1' ? 'float' : valTypeMatch[3] === '2' ? 'double' : 'int';
}
continue;
}
/* BA_ "GenSigDefaultValue" SG_ <msgId> <signalName> <value>;
* 注意该值是物理值(已应用 factor/offset),与 GenSigStartValue(raw)不同 */
var sigDefaultMatch = line.match(/^BA_\s+"GenSigDefaultValue"\s+SG_\s+(\d+)\s+([A-Za-z0-9_]+)\s+(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*;$/);
if (sigDefaultMatch) {
sigDefaults[sigDefaultMatch[1] + ':' + sigDefaultMatch[2]] = sigDefaultMatch[3];
continue;
}
}
messages.forEach(function (msg) {
msg.comment = comments.messages[String(msg.idRaw)] || '';
msg.signals.forEach(function (signal) {
signal.comment = comments.signals[String(msg.idRaw) + ':' + signal.name] || '';
signal.enumMap = valueTables[String(msg.idRaw) + ':' + signal.name] || {};
var dv = sigDefaults[String(msg.idRaw) + ':' + signal.name];
signal.defaultValue = (dv != null && Number.isFinite(Number(dv))) ? dv : null;
});
});
return {
messages: messages,
nodeNames: nodeNames,
messageMap: messageMap
};
}
/** @description 在报文的 signals 数组中按名称查找信号。 */
export function findSignal(message, signalName) {
for (var i = 0; i < message.signals.length; i++) {
if (message.signals[i].name === signalName) return message.signals[i];
}
return null;
}
/** @description 解析 SG_ 行的 multiplex token(M / mN / 空)。 */
export function parseMultiplexToken(token) {
if (!token) return { role: null, value: null };
if (token === 'M') return { role: 'mux', value: null };
var match = token.match(/^m(-?\d+)$/);
if (match) {
return { role: 'value', value: parseInt(match[1], 10) };
}
return { role: null, value: null };
}
/** @description 更新顶部 DBC / 报文数量摘要 pill。 */
export function buildSummary() {
if (!state.dbc) {
if (el.dbcSummaryPill) el.dbcSummaryPill.textContent = '未导入 DBC';
if (el.messageSummaryPill) el.messageSummaryPill.textContent = '0 报文';
return;
}
if (el.dbcSummaryPill) {
el.dbcSummaryPill.textContent = (state.dbc.nodeNames.length || 0) + ' 节点';
}
if (el.messageSummaryPill) {
el.messageSummaryPill.textContent = state.dbc.messages.length + ' 报文';
}
}