feat(main): 添加启动模块自检与汇总功能

- 新增 SystemBoot 类用于跟踪各模块初始化状态,提供位图标记与报告功能
- 在 main.cpp 中重构初始化流程,按顺序初始化各模块并记录状态
- 添加 system_boot.cpp 模块文件到构建配置
- CAN反馈任务增加冷启动延迟,避免与GD32初始化窗口冲突

refactor(ota): 优化OTA协调器线程安全机制

- 修改 lock()/unlock() 方法返回take结果,确保mutex正确释放
- 修复多任务并发时mutex死锁问题,避免vTaskPriorityDisinherit断言
- 统一finalize失败后的状态复位逻辑,确保协调器回到IDLE状态
- 优化忙状态超时检测与自愈机制

perf(boot): 优化冷启动时序与资源管理

- 简化README中冷启动优化说明,聚焦核心逻辑
- 优化BOD阈值设置为LVL5,改进电源稳定性
- 增加BLE OTA工作线程优先级,减少丢包情况
- 优化堆告警节流机制,在OTA期间降低告警频率

fix(relay): 改进GD32应用存活检测逻辑

- 调整GD32 APP存活验证时序,增加会话建立验证
- 修复HELLO建立session后未验证role的问题
- 优化FINISH后等待APP启动的静默时间与重试机制
- 统一GD32探测逻辑到OTA中继任务

refactor(server): 优化HTTP服务端内存与性能

- 使用静态缓冲区替代动态分配,减少堆碎片化风险
- 优化CAN批量发送期间暂停反馈任务的机制
- 调整BLE连接参数请求逻辑,提升传输性能
- 改进错误处理流程,避免堆紧张时的额外分配

bump(version): 更新固件版本至0.3.50

- 更新版本号从0.3.49到0.3.50
- 更新编译时间戳与Git哈希值
This commit is contained in:
小龙虾
2026-07-17 02:02:36 +08:00
parent 6364eacfb1
commit 1b10f89d8c
39 changed files with 851 additions and 331 deletions
+25 -1
View File
@@ -208,7 +208,13 @@ export function updateSystemStatusUi(data) {
var displayRelayState = isGd32Complete ? 5 : (isGd32Relaying && rs === 0 ? 1 : rs);
if (window.currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); }
if (isGd32Error && (window.currentOtaTarget === 'gd32' || window.otaSessionTarget === 'gd32')) {
/* 本会话正在 GD32 路径(含仅 RELAYING 而 target 被清的情况) */
var gd32Session =
window.otaSessionState === OtaSessionState.RELAYING ||
window.currentOtaTarget === 'gd32' ||
window.otaSessionTarget === 'gd32';
if (isGd32Error && gd32Session) {
var failDetail = 'GD32 OTA 失败';
if (relayTotal > 0) {
failDetail += '(已转发 ' + relaySeq + '/' + relayTotal +
@@ -218,6 +224,7 @@ export function updateSystemStatusUi(data) {
}
window.cleanupAfterOtaFailure(failDetail);
window.pollLogs();
return;
} else if (isGd32Error) {
// 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理
updateStagesFromRelayState(255);
@@ -233,6 +240,23 @@ export function updateSystemStatusUi(data) {
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
window.gd32RelaySessionLive = true;
}
/*
* 设备侧 ERROR 常被 status 软清为 IDLE,前端收不到 code=8。
* 已见真实中继、设备已回 IDLE、且未宣布完成 → 按失败收尾,释放会话。
*/
var deviceIdleAfterRelay =
(sysStateCode === 0 || sysStateCode === 2) &&
(rs === 0 || rs === 5) &&
window.gd32RelaySessionLive &&
!window.gd32CompleteAnnounced;
if (deviceIdleAfterRelay) {
window.cleanupAfterOtaFailure(
'GD32 中继已结束但未成功(设备已回 IDLE' +
(relayTotal > 0 ? ',已转发 ' + relaySeq + '/' + relayTotal : '') + ')'
);
window.pollLogs();
return;
}
/* 包进度:进入 FINISHING/COMPLETE 时先补 100%,再走完成/收尾 UI */
if (window.gd32RelaySessionLive && relayTotal > 0 &&
(relaySeq >= relayTotal || rs >= 4 || isGd32Complete)) {
+26 -5
View File
@@ -58,6 +58,31 @@ export async function startUpload(chip) {
if (!file) return;
// ★ 状态机守卫:用统一的会话活跃判断替代裸 isUploading。
// FAILED/DONE 会话需要先复位才能发起新上传(下方 otaSessionReset)。
// RELAYING 可能因 ERROR 被设备软清为 IDLE 而前端未收尾,先探测再决定。
if (otaSessionActive()) {
var busyState = window.otaSessionState;
if (busyState === OtaSessionState.RELAYING || busyState === OtaSessionState.VERIFYING) {
try {
var staleProbe = await fetch(window.getBaseUrl() + '/system/status', {
signal: AbortSignal.timeout(2000)
});
if (staleProbe.ok) {
var staleSt = await staleProbe.json();
var so = staleSt.ota || {};
var ssc0 = Number.isFinite(so.sys_state_code) ? so.sys_state_code : -1;
var rs0 = Number.isFinite(so.relay_state) ? so.relay_state : -1;
/* 设备已空闲/完成/错误残留:前端会话是陈旧活跃态,强制清掉 */
var deviceFree = (ssc0 === 0 || ssc0 === 2 || ssc0 === 4 || ssc0 === 8) &&
(rs0 === 0 || rs0 === 5 || rs0 === 255 || rs0 < 0);
if (deviceFree) {
addLog('W', '上传', '检测到陈旧会话 ' + busyState +
'(设备已 ' + (so.sys_state || ssc0) + '),自动清理后继续');
otaSessionReset();
}
}
} catch (_) { /* 探测失败则仍按占用拒绝 */ }
}
}
if (otaSessionActive()) {
var busyChip = (window.otaSessionTarget || window.activeUploadChip || window.flashTarget || chip || 'ota').toUpperCase();
addLog('W', '上传', '已有 ' + busyChip + ' OTA 会话进行中 (' + window.otaSessionState + '),拒绝新的 ' + chip.toUpperCase() + ' 上传');
@@ -70,11 +95,7 @@ export async function startUpload(chip) {
window.otaSessionTarget = chip;
window.pausePollingForUpload();
// ★ 必须先抢锁再 await:守卫同步设置,await 期间重复点击才能被拦下。
// 旧实现把 isUploading 设在 await handle.getFile() 之后 → 用户在 BLE 期间
// 狂点 WiFi 上传按钮时,多个 startUpload 都能通过守卫,最终并发 xhr.send,
// 设备端互斥被绕过、403 路径走漏。
// ★ 状态机:进入 UPLOADING 态(内部同步 isUploading = true)
/* 先进入 UPLOADING(同步 isUploading),再 await 读文件,防止重复提交 */
otaSessionTransition(OtaSessionState.UPLOADING);
window.activeUploadChip = chip;
/* 新上传:清掉上次完成标志,避免立刻被 status 残留判成“又完成了” */
+30
View File
@@ -159,9 +159,39 @@ export function startRelayProgressWatch() {
if (rstate >= 1 && rstate <= 4) {
window.gd32RelaySessionLive = true;
}
/* 中继失败:必须把前端会话从 RELAYING 拉到 FAILED,否则无法新开上传 */
if (code === 8 || rstate === 255) {
stopRelayProgressWatch();
window.pollLogs();
if (window.otaSessionState === OtaSessionState.RELAYING ||
window.otaSessionState === OtaSessionState.UPLOADING) {
var failMsg = 'GD32 中继失败(relay_state=' + rstate +
', sys=' + (o.sys_state || code) + ')';
if (typeof window.cleanupAfterOtaFailure === 'function') {
window.cleanupAfterOtaFailure(failMsg);
} else {
otaSessionTransition(OtaSessionState.FAILED, failMsg);
}
}
return;
}
/*
* ERROR 可能在 status 路径被软清为 IDLE,前端收不到 code=8。
* 本会话已见过真实中继,设备却已回 IDLE 且未宣布完成 → 按失败清理。
*/
var idleAfterRelay = (code === 0 || code === 2) &&
(rstate === 0 || rstate === 5) &&
window.gd32RelaySessionLive &&
!window.gd32CompleteAnnounced &&
window.otaSessionState === OtaSessionState.RELAYING;
if (idleAfterRelay) {
stopRelayProgressWatch();
window.pollLogs();
if (typeof window.cleanupAfterOtaFailure === 'function') {
window.cleanupAfterOtaFailure('GD32 中继已结束但未成功(设备已回 IDLE)');
} else {
otaSessionTransition(OtaSessionState.FAILED, 'GD32 中继已结束但未成功');
}
return;
}
/*