Files
ESP32_COMM/tools/js/ota/progress.js
T
小龙虾 a47a027178 feat(ota):实现ESP32和GD32的OTA上传流程
- 添加了upload-esp32.js用于处理ESP32的OTA上传,包括文件上传、SHA-256验证和设备重启。
- 引入了upload-gd32.js,用于触发GD32 OTA中继并跟踪中继进度。
- 针对OTA流程,增强了会话管理和错误处理能力。
- 通过在上传过程中显示吐司通知和进度更新,改进了用户反馈。
2026-07-15 12:12:19 +08:00

210 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* progress.js - 折叠面板 + 刷写进度 + 阶段节点 UI
*
* 从 app.js 的「折叠面板」「刷写进度」段提取。
* 操作 DOM: #esp32Stages / #gd32Stages / #stageStatusBar / #progressFill / #progressTxt / #progressPct
*
* 共享状态(通过 window.* 引用):
* window.activePhase / window.currentOtaTarget / window.esp32OtaPhase
*
* 未拆分的函数(通过 window.* 引用):
* window.updateStagesFromRelayState / window.cleanupAfterOtaFailure
* window.resumePollingAfterUpload / window.addLog
*/
import { addLog } from '../common/log.js';
// ==================== 折叠面板 ====================
export function toggleCollapse(headEl) {
headEl.closest('.card').classList.toggle('collapsed');
}
// ==================== 刷写进度 ====================
window.activePhase = null; // 'esp32Ota' | 'gd32Ota' | null
window.currentOtaTarget = null; // 'esp32' | 'gd32' | null — 当前 OTA 目标,决定显示哪套阶段
// 切换 OTA 阶段面板显示(原始版本,会被 console-mode.js 覆盖以支持 BLE)
export function switchStageFlow(target) {
window.currentOtaTarget = target;
var esp32El = document.getElementById('esp32Stages');
var gd32El = document.getElementById('gd32Stages');
if (target === 'esp32') {
if (esp32El) esp32El.style.display = '';
if (gd32El) gd32El.style.display = 'none';
} else {
if (esp32El) esp32El.style.display = 'none';
if (gd32El) gd32El.style.display = '';
}
// 重置 ESP32 阶段节点
['es1','es2','es3','es4','es5','es6'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.className = 'stage-node';
});
['esd1','esd2','esd3','esd4','esd5','esd6'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
// 重置 GD32 阶段节点
['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) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
}
// ESP32 OTA 阶段状态: 0=待机, 1=上传中, 2=OTA写入, 3=SHA-256校验, 4=应用固件, 5=重启中, 6=启动完成
window.esp32OtaPhase = 0;
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); });
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 (phase <= 0) {
if (statusBar) statusBar.className = 'stage-status-bar';
if (statusText) statusText.textContent = '待机';
return;
}
for (var i = 0; i < phase - 1 && i < 6; i++) {
if (all[i]) all[i].className = 'stage-node done';
}
if (phase >= 1 && phase <= 6) {
if (all[phase - 1]) all[phase - 1].className = 'stage-node live';
if (details[phase - 1]) details[phase - 1].textContent = esp32PhaseLabels[phase] || '';
}
if (statusBar && statusText) {
if (phase >= 6) {
statusBar.className = 'stage-status-bar ok';
statusText.textContent = 'ESP32 刷写完成';
} else {
statusBar.className = 'stage-status-bar active';
statusText.textContent = (esp32PhaseLabels[phase] || '进行中') + ' (' + phase + '/6)';
}
}
}
export function updateEsp32StagesError() {
var ids = ['es1', 'es2', 'es3', 'es4', 'es5', 'es6'];
var all = ids.map(function(id) { return document.getElementById(id); });
all.forEach(function (s) { if (s) s.className = 'stage-node'; });
var statusBar = document.getElementById('stageStatusBar');
var statusText = document.getElementById('stageStatusText');
if (statusBar) statusBar.className = 'stage-status-bar err';
if (statusText) statusText.textContent = '校验/应用失败';
}
/**
* @description OTA 失败后的统一清理函数。
* 所有上传失败、校验失败、中继触发失败等异常路径都应调用此函数,
* 确保 UI 状态完整重置,不会残留"上传中"或中间态。
*
* ★ 状态机改造:转入 FAILED 态,置位 otaFailReason。
* 之后轮询(pollStatus/updateSystemStatusUi)不得覆盖此失败态,
* 直到用户发起新上传(otaSessionReset)或检测到设备已回 IDLE。
* @param {string} reason 失败原因描述
*/
export function cleanupAfterOtaFailure(reason) {
var chip = window.activeUploadChip || window.otaSessionTarget || window.currentOtaTarget || window.flashTarget || 'esp32';
// 0. 状态机:转入 FAILED(先于标志重置)
window.otaSessionTransition(window.OtaSessionState.FAILED, reason);
// 1. 清掉本会话全部残留标志 / 定时器 / 校验面板 / 旧 progress 语义
window.clearOtaSessionResiduals({
keepErrorStages: true,
statusText: reason || '失败',
errorStyle: true,
clearCacheDisplay: true
});
// 2. 阶段指示器标错
if (chip === 'esp32') {
updateEsp32StagesError();
} else {
window.updateStagesFromRelayState(255);
}
// 3. 进度条失败文案(clear 已把 fill 的 done 去掉)
setProgress(0, reason || '刷写失败');
setActivePhase(null);
window.currentOtaTarget = null;
// 4. 恢复上传按钮
var btnId = (chip === 'gd32') ? 'uploadGd32Btn' : 'uploadEsp32Btn';
var btn = document.getElementById(btnId);
if (btn) {
btn.disabled = false;
if (window.uploadBtnOrigText) btn.textContent = window.uploadBtnOrigText;
}
window.activeUploadChip = null;
window.otaSessionTarget = null;
window.uploadBtnOrigText = '';
window.updateUploadButtonLabel();
// 5. 恢复轮询(之后 status 不得覆盖 FAILED UI,直至用户新上传或设备 idle 复位)
window.resumePollingAfterUpload();
addLog('E', 'OTA', '刷写流程已中止,已清理残留状态: ' + (reason || '未知原因'));
}
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
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]);
if (!el) return;
el.classList.remove('active', 'done');
if (p === phase) el.classList.add('active');
});
// Reset progress fill color
var fill = document.getElementById('progressFill');
if (fill) fill.classList.remove('done');
}
/** @description 标记指定阶段为完成,更新进度条颜色。 */
export function markPhaseDone(phase) {
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
var stepMap = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota' };
var el = document.getElementById(stepMap[phase]);
if (el) { el.classList.remove('active'); el.classList.add('done'); }
// All done? turn progress bar green
if (phase === 'gd32Ota' || phase === 'esp32Ota') {
var fill = document.getElementById('progressFill');
if (fill) fill.classList.add('done');
}
}
/** @description 更新进度条百分比和文本显示。 */
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 + '%';
}
/** @description 重置进度条和阶段状态到初始值。 */
export function resetProgress() {
setProgress(0, '等待操作...');
window.activePhase = null;
window.esp32OtaPhase = 0;
switchStageFlow(null);
['ppEsp32', 'ppGd32Ota'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.classList.remove('active', 'done');
});
var fill = document.getElementById('progressFill');
if (fill) fill.classList.remove('done');
}