feat(ota): 添加GD32 OTA转发进度追踪和重发机制

- 升级固件版本从0.2.10到0.2.11
- 新增全局变量g_relay_seq和g_relay_total_packets用于跟踪转发进度
- 实现OTA转发过程中的包重发逻辑,解决GD32 Flash缓冲区满时的问题
- 添加重试计数器,超过3次重发限制时标记为GD32错误
- 在system_status_handler中暴露转发序列号和总包数到API接口
- 优化前端UI显示,添加分阶段进度指示器(EPS32 OTA、GD32上传、GD32转发)
- 实现详细的转发进度百分比计算和状态反馈
- 移除未使用的AES解密函数以减少代码体积
- 修复注释中的标点符号错误(->替换为→)
- 更新CSS样式以支持新的进度显示需求
This commit is contained in:
小龙虾
2026-05-31 01:45:53 +08:00
parent 6f4be9189c
commit 56f26fa0d1
7 changed files with 196 additions and 60 deletions
+79 -9
View File
@@ -13,6 +13,53 @@ let logSocket = null;
let latestSystemStatus = null;
let isEsp32Rebooting = false;
let rebootStartTime = 0;
// ==================== 刷写进度 ====================
var activePhase = null; // 'esp32Ota' | 'gd32Upload' | 'gd32Relay' | null
function setActivePhase(phase) {
activePhase = phase;
var stepMap = { esp32Ota: 'ppEsp32', gd32Upload: 'ppGd32Up', gd32Relay: 'ppGd32Relay' };
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');
}
function markPhaseDone(phase) {
var stepMap = { esp32Ota: 'ppEsp32', gd32Upload: 'ppGd32Up', gd32Relay: 'ppGd32Relay' };
var el = document.getElementById(stepMap[phase]);
if (el) { el.classList.remove('active'); el.classList.add('done'); }
// All done? turn progress bar green
if (phase === 'gd32Relay') {
var fill = document.getElementById('progressFill');
if (fill) fill.classList.add('done');
}
}
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 + '%';
}
function resetProgress() {
setProgress(0, '等待操作...');
activePhase = null;
['ppEsp32', 'ppGd32Up', 'ppGd32Relay'].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');
}
// ==================== 工具函数 ====================
function formatBytes(bytes) {
@@ -183,6 +230,7 @@ function applyDisconnectedRuntimeState() {
document.getElementById('mRSSI').textContent = '--';
document.getElementById('bOTAProgress').textContent = '--';
document.getElementById('bOTAProgress').className = 'v err';
resetProgress();
resetStageFlow();
}
@@ -362,13 +410,32 @@ function updateSystemStatusUi(data) {
else if (rs === 17) pr.className = 'pill online';
else pr.className = 'pill idle';
// OTA progress
// OTA cache progress (GD32 upload to ESP32 flash)
var total = ota.total_size || 0;
var stored = ota.stored_size || 0;
var progress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
var storageProgress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
var bp = document.getElementById('bOTAProgress');
bp.textContent = progress + '%';
bp.className = 'v ' + (progress === 100 ? 'ok' : progress > 0 ? 'warn' : '');
bp.textContent = storageProgress + '%';
bp.className = 'v ' + (storageProgress === 100 ? 'ok' : storageProgress > 0 ? 'warn' : '');
// GD32 relay forwarding progress
var relaySeq = ota.relay_seq || 0;
var relayTotal = ota.relay_total_packets || 0;
var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0;
if (rs > 0 && rs < 17) {
// Relay is active — show relay progress
setActivePhase('gd32Relay');
setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包');
} else if (total > 0 && stored < total && !isUploading) {
// GD32 upload to cache in progress (handled by upload progress if uploading)
setActivePhase('gd32Upload');
setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
} else if (rs === 17) {
// Relay done
markPhaseDone('gd32Relay');
setProgress(100, 'GD32 刷写完成');
}
// stage flow from real state
updateStagesFromRelayState(rs);
@@ -814,7 +881,7 @@ async function startUpload(chip) {
var origText = uploadBtn.textContent;
uploadBtn.disabled = true;
uploadBtn.textContent = '上传中...';
document.getElementById('progressWrap').classList.add('show');
setActivePhase(chip === 'esp32' ? 'esp32Ota' : 'gd32Upload');
addLog('I', '上传', '开始上传: ' + file.name + ' \u2192 ' + chip.toUpperCase());
showToast('开始上传固件到 ' + chip.toUpperCase(), 'success');
@@ -841,10 +908,7 @@ async function startUpload(chip) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
uploadProgress = pct;
document.getElementById('progressFill').style.width = pct + '%';
document.getElementById('progressTxt').textContent =
'已上传 ' + formatBytes(e.loaded) + ' / ' + formatBytes(e.total);
document.getElementById('progressPct').textContent = pct + '%';
setProgress(pct, '已上传 ' + formatBytes(e.loaded) + ' / ' + formatBytes(e.total));
document.getElementById('bOTAProgress').textContent = pct + '%';
}
});
@@ -857,7 +921,9 @@ async function startUpload(chip) {
if (xhr.status === 200) {
addLog('I', '上传', '固件上传完成!');
showToast('固件上传成功', 'success');
setProgress(100, '上传完成');
if (chip === 'esp32') {
markPhaseDone('esp32Ota');
var countdown = 3;
addLog('I', 'OTA', countdown + ' 秒后自动调用 POST /ota/update 完成升级并重启...');
var timer = setInterval(function () {
@@ -869,6 +935,10 @@ async function startUpload(chip) {
triggerEsp32Update();
}
}, 1000);
} else {
markPhaseDone('gd32Upload');
setActivePhase('gd32Relay');
setProgress(0, '等待触发转发...');
}
} else {
var detail = formatHttpErrorDetail(xhr.responseText);
+19 -10
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 OTA 中继控制台</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css?v=20260530b">
</head>
<body>
<div class="container">
@@ -104,21 +104,30 @@
</div>
</div>
<!-- 4. Common -->
<!-- 4. Flash Progress -->
<div class="card">
<div class="card-head"><span class="ico">⚡</span> 通用操作</div>
<div class="card-head"><span class="ico">⚡</span> 刷写进度</div>
<div class="card-body">
<div class="progress-wrap" id="progressWrap">
<!-- Phase step indicators -->
<div class="progress-phase-row">
<div class="pp-step active" id="ppEsp32"><span class="pp-ico">📦</span><span class="pp-txt">ESP32 OTA</span></div>
<div class="pp-step" id="ppGd32Up"><span class="pp-ico">📤</span><span class="pp-txt">GD32 上传</span></div>
<div class="pp-step" id="ppGd32Relay"><span class="pp-ico">🔀</span><span class="pp-txt">GD32 转发</span></div>
</div>
<!-- Unified progress bar -->
<div class="progress-wrap show" id="progressWrap">
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-text">
<span id="progressTxt">准备上传...</span>
<span id="progressTxt">等待操作...</span>
<span id="progressPct">0%</span>
</div>
</div>
<button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button>
<a class="btn btn-outline btn-block" href="ble_ota.html?v=20260509-0046" style="margin-top:10px;text-decoration:none;">
切换到 BLE 蓝牙刷写
</a>
<div style="margin-top:12px;">
<button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button>
<a class="btn btn-outline btn-block" href="ble_ota.html?v=20260509-0046" style="margin-top:10px;text-decoration:none;">
切换到 BLE 蓝牙刷写
</a>
</div>
</div>
</div>
@@ -258,6 +267,6 @@
<span id="toastMsg">操作成功</span>
</div>
<script src="app.js?v=20260529"></script>
<script src="app.js?v=20260530f"></script>
</body>
</html>
+56 -8
View File
@@ -1,4 +1,4 @@
/* ===== ESP32 OTA 控制台 — 工业调试台风格 ===== */
/* ===== ESP32 OTA 控制台 — 工业调试台风格 ===== */
:root {
--bg-root: #090e15;
@@ -229,25 +229,72 @@ select option { background: var(--bg-panel); color: var(--text); }
.file-meta-item b { font-weight: 600; }
/* === PROGRESS === */
.progress-wrap { margin-top: 14px; display: none; }
.progress-phase-row {
display: flex; align-items: center; gap: 0; margin-bottom: 14px;
flex-wrap: nowrap;
background: var(--bg-input);
border-radius: 20px;
padding: 4px;
border: 1px solid var(--border);
}
.pp-step {
flex: 1;
display: flex; align-items: center; justify-content: center; gap: 5px;
padding: 7px 10px; border-radius: 16px;
font-size: 12px; font-weight: 600; color: var(--text-dim);
background: transparent;
transition: all .25s ease; white-space: nowrap;
cursor: default;
}
.pp-ico { font-size: 13px; opacity: .45; transition: all .25s ease; }
.pp-txt { transition: color .25s ease; }
.pp-sep { display: none; }
/* Active step — highlighted capsule */
.pp-step.active {
color: #07121c;
background: var(--accent);
box-shadow: 0 0 10px var(--accent-dim);
}
.pp-step.active .pp-ico { opacity: 1; }
.pp-step.active .pp-txt { color: #07121c; }
/* Done step — green outlined capsule */
.pp-step.done {
color: var(--green);
background: rgba(34,197,94,.12);
}
.pp-step.done .pp-ico { opacity: 1; }
.pp-step.done .pp-txt::after {
content: ' ✓'; font-size: 10px;
}
.progress-wrap { margin-top: 0; display: none; }
.progress-wrap.show { display: block; }
.progress-track {
width: 100%; height: 8px; border-radius: 20px;
width: 100%; height: 10px; border-radius: 20px;
background: var(--bg-input); overflow: hidden;
box-shadow: inset 0 1px 2px rgba(0,0,0,.3);
}
.progress-fill {
height: 100%; border-radius: 20px; background: var(--accent);
width: 0%; transition: width .25s ease;
height: 100%; border-radius: 20px;
background: linear-gradient(90deg, #22d3ee, #06b6d4);
width: 0%; transition: width .3s ease;
position: relative;
box-shadow: 0 0 8px rgba(34,211,238,.3);
}
.progress-fill::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.25) 50%, transparent 100%);
animation: sweep 1.8s linear infinite;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.3) 50%, transparent 100%);
animation: sweep 1.6s linear infinite;
}
.progress-fill.done {
background: linear-gradient(90deg, #22c55e, #16a34a);
box-shadow: 0 0 8px rgba(34,197,94,.3);
}
@keyframes sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
.progress-text {
display: flex; justify-content: space-between; margin-top: 6px;
display: flex; justify-content: space-between; margin-top: 7px;
font-size: 12px; color: var(--text-dim);
}
@@ -413,3 +460,4 @@ select option { background: var(--bg-panel); color: var(--text); }
.can-grid { grid-template-columns: 1fr 1fr; }
.btn-row { flex-direction: column; }
}