feat(ble): 完善BLE OTA架构并优化性能

- 实现BLE OTA数据路径分离:添加ble_ota_data.cpp处理重排序/工作线程/DATA_IN,
  和ble_ota_host.cpp处理GATT/GAP/广播/主机功能
- 将滑动ACK窗口从32增大到64,实测Windows WebBT环境下固件传输速度提升约10%
  (从40KB/s提升到44KB/s),内存消耗约30KB重排序缓冲区
- 优化调试日志级别,将OTA相关信息从INFO调整为DBG以减少日志冗余

refactor(can): 改进CAN反馈监听机制

- 将CAN反馈任务从1Hz主动轮询模式改为被动事件监听模式,
  使用SpiMaster::takeEvent()替代主动发送0xC1轮询请求
- 更新相关头文件注释说明架构变更,将"周期性轮询"改为"被动接收推送"
- 修改调试日志级别,将暂停/恢复状态日志从INFO调整为DBG
This commit is contained in:
小龙虾
2026-07-18 01:16:06 +08:00
parent 1b10f89d8c
commit b807645a20
47 changed files with 4361 additions and 2890 deletions
+64 -34
View File
@@ -12,6 +12,17 @@
*/
import { addLog } from '../common/log.js';
// ==================== 常量 ====================
// ESP32 OTA 阶段 DOM 节点 id(es1..es6 + esd1..esd6)
var ESP32_STAGE_IDS = ['es1', 'es2', 'es3', 'es4', 'es5', 'es6'];
var ESP32_DETAIL_IDS = ['esd1', 'esd2', 'esd3', 'esd4', 'esd5', 'esd6'];
// 顶部步骤 pill → phase 名映射
var STEP_MAP = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota' };
// 进度条 DOM 元素懒缓存(首次访问后复用,DOM 未就绪时回退到 getElementById)
var progressFillEl = null;
var progressTxtEl = null;
var progressPctEl = null;
// ==================== 折叠面板 ====================
export function toggleCollapse(headEl) {
headEl.closest('.card').classList.toggle('collapsed');
@@ -29,25 +40,37 @@ export function switchStageFlow(target) {
if (target === 'esp32') {
if (esp32El) esp32El.style.display = '';
if (gd32El) gd32El.style.display = 'none';
} else {
} else if (target === 'gd32') {
if (esp32El) esp32El.style.display = 'none';
if (gd32El) gd32El.style.display = '';
} else {
// null / 'ble' / 未知:隐藏两套面板(reset 语义,避免残留显示)
if (esp32El) esp32El.style.display = 'none';
if (gd32El) gd32El.style.display = 'none';
}
// 重置 ESP32 阶段节点
['es1','es2','es3','es4','es5','es6'].forEach(function(id) {
resetEsp32StageNodes();
resetGd32StageNodes();
}
/** @description 把 ESP32 阶段节点(es1..es6 / esd1..esd6)复位到初始态。 */
function resetEsp32StageNodes() {
ESP32_STAGE_IDS.forEach(function (id) {
var el = document.getElementById(id);
if (el) el.className = 'stage-node';
});
['esd1','esd2','esd3','esd4','esd5','esd6'].forEach(function(id) {
ESP32_DETAIL_IDS.forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
// 重置 GD32 阶段节点
['s1','s2','s3','s4','s5','s6'].forEach(function(id) {
}
/** @description 把 GD32 阶段节点(s1..s6 / sd1..sd6)复位到初始态。 */
function resetGd32StageNodes() {
['s1', 's2', 's3', 's4', 's5', 's6'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.className = 'stage-node';
});
['sd1','sd2','sd3','sd4','sd5','sd6'].forEach(function(id) {
['sd1', 'sd2', 'sd3', 'sd4', 'sd5', 'sd6'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
@@ -59,10 +82,8 @@ export const esp32PhaseLabels = ['', '上传中', '写入中', '校验中', '应
export function updateEsp32StagesFromPhase(phase) {
window.esp32OtaPhase = phase;
var ids = ['es1', 'es2', 'es3', 'es4', 'es5', 'es6'];
var detailIds = ['esd1', 'esd2', 'esd3', 'esd4', 'esd5', 'esd6'];
var all = ids.map(function(id) { return document.getElementById(id); });
var details = detailIds.map(function(id) { return document.getElementById(id); });
var all = ESP32_STAGE_IDS.map(function (id) { return document.getElementById(id); });
var details = ESP32_DETAIL_IDS.map(function (id) { return document.getElementById(id); });
all.forEach(function (s) { if (s) s.className = 'stage-node'; });
details.forEach(function (d) { if (d) d.textContent = ''; });
var statusBar = document.getElementById('stageStatusBar');
@@ -78,7 +99,7 @@ export function updateEsp32StagesFromPhase(phase) {
if (!all[i]) continue;
if (i < phase - 1) {
all[i].className = 'stage-node done';
} else if (i === phase - 1 && phase >= 1) {
} else if (i === phase - 1) {
all[i].className = (phase >= 6) ? 'stage-node done' : 'stage-node live';
if (details[i]) details[i].textContent = esp32PhaseLabels[phase] || '';
}
@@ -95,9 +116,11 @@ export function updateEsp32StagesFromPhase(phase) {
}
export function updateEsp32StagesError() {
var ids = ['es1', 'es2', 'es3', 'es4', 'es5', 'es6'];
var all = ids.map(function(id) { return document.getElementById(id); });
var all = ESP32_STAGE_IDS.map(function (id) { return document.getElementById(id); });
var details = ESP32_DETAIL_IDS.map(function (id) { return document.getElementById(id); });
all.forEach(function (s) { if (s) s.className = 'stage-node'; });
// ★ 失败态:阶段下方文案也要清空,避免残留"上传中/校验中"与红色样式并存
details.forEach(function (d) { if (d) d.textContent = ''; });
var statusBar = document.getElementById('stageStatusBar');
var statusText = document.getElementById('stageStatusText');
if (statusBar) statusBar.className = 'stage-status-bar err';
@@ -117,7 +140,14 @@ export function updateEsp32StagesError() {
export function cleanupAfterOtaFailure(reason) {
// 幂等:已在 FAILED 且无活跃 XHR 时只刷新文案,避免 abort 事件二次递归清状态
var alreadyFailed = window.otaSessionState === window.OtaSessionState.FAILED;
var chip = window.activeUploadChip || window.otaSessionTarget || window.currentOtaTarget || window.flashTarget || 'esp32';
/* chip 判定优先级(权威来源顺序):
* activeUploadChip — XHR 上传开始时由 startUpload 写入,WiFi 路径权威
* otaSessionTarget — OTA 会话状态机目标(仅 WiFi 走 OtaSessionState,BLE 不走)
* currentOtaTarget — switchStageFlow 设置的当前面板目标
* flashTarget — 用户选择的目标(BLE 路径未设上述两者时兜底)
* 'esp32' — 最终默认值 */
var chip = window.activeUploadChip || window.otaSessionTarget ||
window.currentOtaTarget || window.flashTarget || 'esp32';
// 0. 状态机:转入 FAILED(先于标志重置)
window.otaSessionTransition(window.OtaSessionState.FAILED, reason);
@@ -142,16 +172,12 @@ export function cleanupAfterOtaFailure(reason) {
setActivePhase(null);
window.currentOtaTarget = null;
// 4. 恢复上传按钮(两枚都解禁,避免 chip 判错时另一侧仍灰)
// 4. 恢复上传按钮(两枚都解禁并恢复原文案,避免 chip 判错或一侧残留"上传中")
['uploadEsp32Btn', 'uploadGd32Btn'].forEach(function (btnId) {
var btn = document.getElementById(btnId);
if (!btn) return;
btn.disabled = false;
if (window.uploadBtnOrigText &&
((chip === 'gd32' && btnId === 'uploadGd32Btn') ||
(chip !== 'gd32' && btnId === 'uploadEsp32Btn'))) {
btn.textContent = window.uploadBtnOrigText;
}
if (window.uploadBtnOrigText) btn.textContent = window.uploadBtnOrigText;
});
window.activeUploadChip = null;
window.otaSessionTarget = null;
@@ -175,9 +201,8 @@ export function cleanupAfterOtaFailure(reason) {
export function setActivePhase(phase) {
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
window.activePhase = phase;
var stepMap = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota' };
Object.keys(stepMap).forEach(function(p) {
var el = document.getElementById(stepMap[p]);
Object.keys(STEP_MAP).forEach(function (p) {
var el = document.getElementById(STEP_MAP[p]);
if (!el) return;
el.classList.remove('active', 'done');
if (p === phase) el.classList.add('active');
@@ -190,8 +215,7 @@ export function setActivePhase(phase) {
/** @description 标记指定阶段为完成,更新进度条颜色。 */
export function markPhaseDone(phase) {
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
var stepMap = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota' };
var el = document.getElementById(stepMap[phase]);
var el = document.getElementById(STEP_MAP[phase]);
if (el) { el.classList.remove('active'); el.classList.add('done'); }
// All done? turn progress bar green
if (phase === 'gd32Ota' || phase === 'esp32Ota') {
@@ -200,14 +224,20 @@ export function markPhaseDone(phase) {
}
}
/** @description 更新进度条百分比和文本显示。 */
/** @description 更新进度条百分比和文本显示。对 pct 做边界校验,过滤 NaN/undefined。 */
export function setProgress(pct, text) {
var fillEl = document.getElementById('progressFill');
var txtEl = document.getElementById('progressTxt');
var pctEl = document.getElementById('progressPct');
if (fillEl) fillEl.style.width = pct + '%';
if (txtEl) txtEl.textContent = text;
if (pctEl) pctEl.textContent = pct + '%';
// 数值校验:BLE ACK 高频调用路径偶尔传入 undefined/NaN,需兜底避免出现 "undefined%"
var n = Number(pct);
if (!Number.isFinite(n)) n = 0;
if (n < 0) n = 0;
else if (n > 100) n = 100;
// 懒缓存 DOM 元素引用;DOM 未就绪时回退到 getElementById(兼容早期调用)
if (!progressFillEl || !progressFillEl.isConnected) progressFillEl = document.getElementById('progressFill');
if (!progressTxtEl || !progressTxtEl.isConnected) progressTxtEl = document.getElementById('progressTxt');
if (!progressPctEl || !progressPctEl.isConnected) progressPctEl = document.getElementById('progressPct');
if (progressFillEl) progressFillEl.style.width = n + '%';
if (progressTxtEl) progressTxtEl.textContent = text;
if (progressPctEl) progressPctEl.textContent = n + '%';
}
/** @description 重置进度条和阶段状态到初始值。 */
@@ -216,7 +246,7 @@ export function resetProgress() {
window.activePhase = null;
window.esp32OtaPhase = 0;
switchStageFlow(null);
['ppEsp32', 'ppGd32Ota'].forEach(function(id) {
['ppEsp32', 'ppGd32Ota'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.classList.remove('active', 'done');
});