feat(protocol): 更新SPI Link协议至v2版本并优化CAN通信

- 将SPI OTA帧格式升级为SPI Link v2,采用新的帧格式:
  | SOF0 0xA5 | SOF1 0x5A | VER 0x02 | FLAGS | SESSION 4B LE |
  TX_SEQ | ACK_SEQ | CREDIT | TYPE | LEN 2B LE | RSV 2B | PAYLOAD |
  CRC32 4B LE |
- 新增V2_OVERHEAD=20字节开销,APP最大payload为236字节,BOOT为108字节
- 修改帧类型定义:ENTER_BOOT从0xB5合并至0xB0,新增CAN业务类型0xC0~C3
- 更新CAN协议文档,支持SPI Link v2-only模式,调整最大业务payload计算
- 扩展0xC1帧功能,支持双向通信和1Hz主动推送ID快照
- 添加0xC3完整ID快照帧类型,包含seq/rx_count等完整字段
- 更新协议文档中的错误码定义和反馈payload格式

refactor(main): 重构主程序入口并添加CAN反馈任务

- 在main.cpp中添加CAN反馈任务,更新启动流程注释
- 创建createCriticalTask辅助函数,统一关键任务创建逻辑
- 调整SPI初始化顺序,确保CAN反馈任务依赖正确
- 优化错误处理机制,关键任务失败时立即重启
- 将GD32版本探测移至ota_relay任务处理
- 更新版本号至0.3.48,调整编译时间和Git哈希

config: 更新SPI和BLE配置参数

- 添加SPI主机相关配置参数,包括CS GPIO、READY GPIO等引脚定义
- 配置SPI缓冲区大小:APP为256字节,BOOT为128字节
- 更新BLE滑动窗口参数,ACK窗口设为32,数据队列深度为72
- 调整SPI工作队列大小至5120字节,增加任务堆栈监控说明
This commit is contained in:
小龙虾
2026-07-16 23:00:15 +08:00
parent 51c89b3065
commit 5db3f66b8e
57 changed files with 4728 additions and 2098 deletions
+91 -7
View File
@@ -9,8 +9,9 @@
// ==================== 共享状态 ====================
export var state = {
dbcFile: null,
dbcText: '',
/** @type {{ id: string, name: string, size: number, dbc: object }[]} */
dbcSources: [],
/** 合并视图 { messages, nodeNames, messageMap };由 rebuildMergedDbc() 维护 */
dbc: null,
filteredMessages: [],
selectedMessageKey: '',
@@ -30,10 +31,32 @@ export var state = {
export var el = {};
// ==================== 基础工具 ====================
/** @description 拼接设备基础 URL(http://<ip>),ip 取自 el.toolIp,默认 192.168.31.127。 */
var LS_ESP_IP = 'dcu_esp_ip';
var DEFAULT_ESP_IP = '192.168.31.127';
/** @description 设备 IP:优先 localStorage(与 OTA 页共用),否则默认。 */
export function getDeviceIp() {
try {
var saved = localStorage.getItem(LS_ESP_IP);
if (saved && saved.trim()) return saved.trim();
} catch (_) {}
if (el.toolIp && el.toolIp.value && el.toolIp.value.trim()) {
return el.toolIp.value.trim();
}
return DEFAULT_ESP_IP;
}
/** @description 记住设备 IP(status 回报或其它页写入)。 */
export function setDeviceIp(ip) {
if (!ip || !String(ip).trim()) return;
var v = String(ip).trim();
try { localStorage.setItem(LS_ESP_IP, v); } catch (_) {}
if (el.toolIp) el.toolIp.value = v;
}
/** @description 拼接设备基础 URL(http://<ip>)。 */
export function getBaseUrl() {
var ip = (el.toolIp && el.toolIp.value ? el.toolIp.value.trim() : '') || '192.168.31.127';
return 'http://' + ip;
return 'http://' + getDeviceIp();
}
/** @description 解析手动输入的 CAN ID(支持 0x/hex/十进制)。 */
@@ -239,15 +262,76 @@ export function parseMultiplexToken(token) {
return { role: null, value: null };
}
/**
* 将多个 DBC 源合并为统一 messages 列表。
* 每条报文 key = sourceId:idRaw:name,避免不同文件同 ID/同名冲突。
*/
export function rebuildMergedDbc() {
if (!state.dbcSources.length) {
state.dbc = null;
return null;
}
var messages = [];
var nodeSet = Object.create(null);
var nodeNames = [];
var messageMap = {};
for (var s = 0; s < state.dbcSources.length; s++) {
var src = state.dbcSources[s];
var dbc = src.dbc;
if (!dbc || !dbc.messages) continue;
for (var n = 0; n < (dbc.nodeNames || []).length; n++) {
var nn = dbc.nodeNames[n];
if (nn && !nodeSet[nn]) {
nodeSet[nn] = 1;
nodeNames.push(nn);
}
}
for (var i = 0; i < dbc.messages.length; i++) {
var msg = dbc.messages[i];
var merged = {
idRaw: msg.idRaw,
name: msg.name,
dlc: msg.dlc,
transmitter: msg.transmitter,
signals: msg.signals,
comment: msg.comment,
id: msg.id,
isExtended: msg.isExtended,
sourceId: src.id,
sourceName: src.name,
key: src.id + ':' + msg.idRaw + ':' + msg.name
};
messages.push(merged);
messageMap[merged.key] = merged;
}
}
state.dbc = { messages: messages, nodeNames: nodeNames, messageMap: messageMap };
return state.dbc;
}
/** @description 生成唯一 source id(同名文件可多次添加时追加 #2) */
export function makeDbcSourceId(fileName) {
var base = String(fileName || 'dbc').replace(/[^\w.\-]+/g, '_');
var id = base;
var n = 2;
while (state.dbcSources.some(function (s) { return s.id === id; })) {
id = base + '#' + n;
n++;
}
return id;
}
/** @description 更新顶部 DBC / 报文数量摘要 pill。 */
export function buildSummary() {
if (!state.dbc) {
if (!state.dbc || !state.dbcSources.length) {
if (el.dbcSummaryPill) el.dbcSummaryPill.textContent = '未导入 DBC';
if (el.messageSummaryPill) el.messageSummaryPill.textContent = '0 报文';
return;
}
var nFiles = state.dbcSources.length;
if (el.dbcSummaryPill) {
el.dbcSummaryPill.textContent = (state.dbc.nodeNames.length || 0) + ' 节点';
el.dbcSummaryPill.textContent =
nFiles + ' 文件 · ' + (state.dbc.nodeNames.length || 0) + ' 节点';
}
if (el.messageSummaryPill) {
el.messageSummaryPill.textContent = state.dbc.messages.length + ' 报文';