feat(ota):实现ESP32和GD32的OTA上传流程
- 添加了upload-esp32.js用于处理ESP32的OTA上传,包括文件上传、SHA-256验证和设备重启。 - 引入了upload-gd32.js,用于触发GD32 OTA中继并跟踪中继进度。 - 针对OTA流程,增强了会话管理和错误处理能力。 - 通过在上传过程中显示吐司通知和进度更新,改进了用户反馈。
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
Reference in New Issue
Block a user