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:
@@ -126,11 +126,11 @@ export function updateHeapAndTaskStatusUi(data) {
|
||||
setCpuFill(usagePct);
|
||||
|
||||
if (taskList) {
|
||||
var visibleTasks = tasks.slice(0, 8);
|
||||
if (!visibleTasks.length) {
|
||||
/* 按占用降序渲染全部任务;列表容器 CSS 默认露出约 3 条,其余滚动 */
|
||||
if (!tasks.length) {
|
||||
taskList.innerHTML = '<div class="status-empty">当前采样没有任务增量</div>';
|
||||
} else {
|
||||
taskList.innerHTML = visibleTasks.map(function (task) {
|
||||
taskList.innerHTML = tasks.map(function (task) {
|
||||
var pct = Number(task.usage_pct) || 0;
|
||||
var delta = Number(task.runtime_delta) || 0;
|
||||
var fillClass = pct >= 85 ? 'err' : pct >= 70 ? 'warn' : '';
|
||||
@@ -177,6 +177,16 @@ export function updateHeapAndTaskStatusUi(data) {
|
||||
if (!names.length) {
|
||||
taskList.innerHTML = '<div class="status-empty">当前状态未包含任务栈信息</div>';
|
||||
} else {
|
||||
/* 峰值占用比例降序;容器默认露出最高 3 条,其余滚动 */
|
||||
names.sort(function (a, b) {
|
||||
var ia = taskStack[a] || {};
|
||||
var ib = taskStack[b] || {};
|
||||
var sa = Number(ia.stack_size_bytes) || 0;
|
||||
var sb = Number(ib.stack_size_bytes) || 0;
|
||||
var pa = sa > 0 ? ((Number(ia.peak_used_bytes) || 0) * 100 / sa) : 0;
|
||||
var pb = sb > 0 ? ((Number(ib.peak_used_bytes) || 0) * 100 / sb) : 0;
|
||||
return pb - pa;
|
||||
});
|
||||
taskList.innerHTML = names.map(function (name) {
|
||||
var item = taskStack[name] || {};
|
||||
var stackSize = Number(item.stack_size_bytes) || 0;
|
||||
|
||||
+32
-12
@@ -25,7 +25,7 @@
|
||||
* window.updateSystemStatusUi / window.updateEsp32StagesFromPhase
|
||||
* window.applyRebootingRuntimeState / window.applyDisconnectedRuntimeState / window.applyUploadingRuntimeState
|
||||
* window.otaSessionActive / window.otaSessionTransition / window.otaSessionReset
|
||||
* window.updateUploadButtonLabel / window.updateSpiHint
|
||||
* window.updateUploadButtonLabel
|
||||
*/
|
||||
import { addLog } from './log.js';
|
||||
import { showToast } from './toast.js';
|
||||
@@ -140,13 +140,18 @@ export async function pollStatus() {
|
||||
window.consecutivePollFailures = 0;
|
||||
addLog('I', '系统', '设备已恢复在线,轮询恢复正常');
|
||||
showToast('设备已恢复在线', 'success');
|
||||
// 设备离线后恢复在线,清理残留的 OTA 会话状态
|
||||
if (window.otaSessionState !== window.OtaSessionState.IDLE) {
|
||||
// 仅清理终态残留。禁止对活跃会话 otaSessionReset:
|
||||
// 旧逻辑任意非 IDLE 都 reset -> 中继中一次 status 超时恢复后会把 RELAYING
|
||||
// 清成 IDLE,UI 不再跟踪;REBOOTING 被清掉后永远进不了启动完成。
|
||||
var ssRec = window.otaSessionState;
|
||||
if (ssRec === window.OtaSessionState.FAILED ||
|
||||
ssRec === window.OtaSessionState.DONE) {
|
||||
window.otaSessionReset();
|
||||
addLog('I', '系统', '已清理 OTA 会话状态');
|
||||
addLog('I', '系统', '已清理终态 OTA 会话残留');
|
||||
}
|
||||
}
|
||||
if (window.isEsp32Rebooting) {
|
||||
if (window.isEsp32Rebooting ||
|
||||
window.otaSessionState === window.OtaSessionState.REBOOTING) {
|
||||
var rbElapsed = Math.round((Date.now() - window.rebootStartTime) / 1000);
|
||||
if (!window.rebootObservedDown && rbElapsed < 10) {
|
||||
// pre-reboot 窗口:后端发响应后约 1s 才 esp_restart,期间设备仍响应轮询
|
||||
@@ -161,7 +166,12 @@ export async function pollStatus() {
|
||||
addLog('I', '系统', 'ESP32 已恢复在线');
|
||||
showToast('ESP32 已恢复在线', 'success');
|
||||
// ★ 状态机:ESP32 重启完成 → DONE
|
||||
if (window.otaSessionState === window.OtaSessionState.REBOOTING) window.otaSessionTransition(window.OtaSessionState.DONE);
|
||||
if (window.otaSessionState === window.OtaSessionState.REBOOTING) {
|
||||
window.otaSessionTransition(window.OtaSessionState.DONE);
|
||||
}
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
try { window.updateSystemStatusUi(data); } catch (e2) { console.warn('updateSystemStatusUi 异常:', e2); }
|
||||
@@ -225,11 +235,16 @@ export async function pollStatus() {
|
||||
window.rebootObservedDown = true; // 确认设备已下线(正在重启)
|
||||
var elapsed = Math.round((Date.now() - window.rebootStartTime) / 1000);
|
||||
if (elapsed > 60) {
|
||||
window.otaSessionTransition(window.OtaSessionState.FAILED, 'ESP32 重启超时');
|
||||
window.isEsp32Rebooting = false;
|
||||
window.rebootObservedDown = false;
|
||||
// ★ 统一走 cleanupAfterOtaFailure:清 xhr/定时器/按钮/阶段,避免只改 state 留下假死 UI
|
||||
addLog('E', '系统', 'ESP32 重启超时,设备未恢复');
|
||||
showToast('ESP32 重启超时,请检查设备', 'error');
|
||||
if (typeof window.cleanupAfterOtaFailure === 'function') {
|
||||
window.cleanupAfterOtaFailure('ESP32 重启超时');
|
||||
} else {
|
||||
window.otaSessionTransition(window.OtaSessionState.FAILED, 'ESP32 重启超时');
|
||||
window.isEsp32Rebooting = false;
|
||||
window.rebootObservedDown = false;
|
||||
}
|
||||
updateConnectionUi(false, '未连接');
|
||||
window.applyDisconnectedRuntimeState();
|
||||
} else {
|
||||
@@ -302,13 +317,19 @@ export async function pollLogs() {
|
||||
// esp_restart() 是硬件复位,不保证 TCP FIN/RST;靠轮询超时/网络错误判断
|
||||
// "已下线"不可靠。设备启动后会推 [I][MAIN] ESP32-C3 OTA Relay v1.0 启动,
|
||||
// 收到这行即说明设备已重启回来,直接推进到阶段 6(启动完成)。
|
||||
if (window.isEsp32Rebooting && m[2] === 'MAIN' && /OTA Relay v1\.0 启动/.test(m[3])) {
|
||||
/* 与 main 启动 banner 对齐:ESP32-C3 OTA Relay <FW_VERSION> 启动(勿写死 v1.0) */
|
||||
if (window.isEsp32Rebooting && m[2] === 'MAIN' && /OTA Relay .*启动/.test(m[3])) {
|
||||
window.isEsp32Rebooting = false;
|
||||
window.rebootObservedDown = false;
|
||||
window.updateEsp32StagesFromPhase(6); // 启动完成
|
||||
addLog('I', '系统', 'ESP32 已恢复在线(启动 banner 收到)');
|
||||
showToast('ESP32 已恢复在线', 'success');
|
||||
if (window.otaSessionState === window.OtaSessionState.REBOOTING) window.otaSessionTransition(window.OtaSessionState.DONE);
|
||||
if (window.otaSessionState === window.OtaSessionState.REBOOTING) {
|
||||
window.otaSessionTransition(window.OtaSessionState.DONE);
|
||||
}
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 设备已报「全部数据包已转发」时,立刻补 UI 100% 进度。
|
||||
@@ -331,7 +352,6 @@ export async function pollLogs() {
|
||||
setTimeout(function () {
|
||||
window.updateUploadButtonLabel();
|
||||
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
|
||||
window.updateSpiHint();
|
||||
// IP 输入变化时清零失败计数并立即刷新状态
|
||||
var ipInput = document.getElementById('espIp');
|
||||
if (ipInput) {
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
*/
|
||||
// ==================== 工具函数 ====================
|
||||
/** @description 格式化字节大小为人类可读字符串(B/KB/MB)。 */
|
||||
/** @description 字节格式化为 B / KB(不升到 MB,避免 1024KB 显示成 1.00MB)。 */
|
||||
export function formatBytes(bytes) {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '--';
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
var kb = bytes / 1024;
|
||||
if (Math.abs(kb - Math.round(kb)) < 1e-9) return Math.round(kb) + ' KB';
|
||||
return kb.toFixed(1) + ' KB';
|
||||
}
|
||||
/** @description 格式化 Date 对象为 HH:MM:SS.mmm 时间字符串。 */
|
||||
export function formatTime(date) {
|
||||
|
||||
Reference in New Issue
Block a user