feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制
移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2 CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发 实现流量控制,提高OTA传输稳定性。 refactor(stack): 统一任务栈大小配置并添加CPU统计功能 - 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配 - 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样 - 更新main.cpp中的任务创建使用新的栈大小常量 - 调整health_monitor和ota_relay等任务的栈分配 perf(ota): 改进ESP32和GD32 OTA分区预擦策略 - ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞 - GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量 - 优化OTA协调器状态管理,修复FINALIZING状态残留问题 fix(log): 改进UDP日志发送的堆内存管理 - 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer - 降低UDP发送跳过的内存阈值从10KB到6KB - 降低日志排水最小内存阈值从8KB到4KB docs(readme): 更新OTA配置参数文档说明 移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为 基于CREDIT背压机制的描述。
This commit is contained in:
+80
-23
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* file-handle.js - 文件处理 + Intel HEX → BIN 转换
|
||||
*
|
||||
* 从 app.js 的「文件处理」「Intel HEX → BIN 转换」「文件处理」段提取。
|
||||
* 包含:HEX 解析、文件选择、FSAPI、SHA-256 计算、文件信息 UI、清除。
|
||||
*
|
||||
* 共享状态(通过 window.* 引用):
|
||||
* window.esp32File / window.gd32File / window.esp32FileHandle / window.gd32FileHandle
|
||||
* window._esp32FileSha256 / window._gd32FileSha256
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.updateUploadButtonLabel / window.showToast / window.addLog
|
||||
*/
|
||||
import { formatBytes } from '../common/utils.js';
|
||||
@@ -179,6 +178,73 @@ export async function convertHexFileToBin(file) {
|
||||
return { file: binFile, converted: true, baseAddr: result.baseAddr, endAddr: result.endAddr };
|
||||
}
|
||||
|
||||
// ==================== SHA-256 缓存 ====================
|
||||
// 选文件时就把 SHA-256 算好并缓存(同时记录 file meta),上传前
|
||||
// 若 handle.getFile() 拿回的 fresh file meta 未变就直接复用,
|
||||
// 避免每次点上传都重算 30–80ms 的 crypto.subtle.digest。
|
||||
// 缓存 key:size + lastModified + name 三项均一致(lastModified 在
|
||||
// 文件 rebuild 后必变,是浏览器给的最可靠 rebuild 信号)。
|
||||
/**
|
||||
* @description 计算 file 的 SHA-256 并写入缓存(sha + meta)。
|
||||
* GD32 UI 无 SHA 显示行;ESP32 同步更新 #fileSha256Esp32。
|
||||
* @param {File} file 目标文件
|
||||
* @param {'esp32'|'gd32'} chip 通道
|
||||
* @param {ArrayBuffer=} optArrayBuffer 已读的 arrayBuffer(避免重复读取)
|
||||
* @returns {Promise<string|null>} hex 大写 SHA,失败返回 null
|
||||
*/
|
||||
function _computeAndCacheFileSha(file, chip, optArrayBuffer) {
|
||||
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
||||
var shaElem = document.getElementById('fileSha256' + suffix); // GD32 可能为 null
|
||||
if (shaElem) shaElem.textContent = '计算中...';
|
||||
var bufPromise = optArrayBuffer ? Promise.resolve(optArrayBuffer) : file.arrayBuffer();
|
||||
return bufPromise.then(function(buf) {
|
||||
return crypto.subtle.digest('SHA-256', buf);
|
||||
}).then(function(hashBuffer) {
|
||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('').toUpperCase();
|
||||
if (shaElem) shaElem.textContent = hashHex;
|
||||
var meta = { size: file.size, lastModified: file.lastModified, name: file.name };
|
||||
if (chip === 'esp32') {
|
||||
window._esp32FileSha256 = hashHex;
|
||||
window._esp32FileShaMeta = meta;
|
||||
} else {
|
||||
window._gd32FileSha256 = hashHex;
|
||||
window._gd32FileShaMeta = meta;
|
||||
}
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 固件 SHA-256: ' + hashHex);
|
||||
return hashHex;
|
||||
}).catch(function(err) {
|
||||
if (shaElem) shaElem.textContent = '计算失败: ' + err.message;
|
||||
addLog('W', '文件', chip.toUpperCase() + ' SHA-256 计算失败: ' + err.message);
|
||||
if (chip === 'esp32') { window._esp32FileSha256 = null; window._esp32FileShaMeta = null; }
|
||||
else { window._gd32FileSha256 = null; window._gd32FileShaMeta = null; }
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 确保 file 的 SHA-256 已在缓存中:命中直接返回,不命中则重算并缓存。
|
||||
* @param {File} file 目标文件
|
||||
* @param {'esp32'|'gd32'} chip 通道
|
||||
* @param {ArrayBuffer=} optArrayBuffer 命中时不用,未命中时传入可避免重复读文件
|
||||
* @returns {Promise<string|null>} hex 大写 SHA
|
||||
*/
|
||||
export function ensureFileShaCached(file, chip, optArrayBuffer) {
|
||||
var cachedMeta = chip === 'esp32' ? window._esp32FileShaMeta : window._gd32FileShaMeta;
|
||||
var cachedSha = chip === 'esp32' ? window._esp32FileSha256 : window._gd32FileSha256;
|
||||
if (cachedSha && cachedMeta &&
|
||||
cachedMeta.size === file.size &&
|
||||
cachedMeta.lastModified === file.lastModified &&
|
||||
cachedMeta.name === file.name) {
|
||||
// 命中:把 UI 上的 SHA 同步刷一下(幂等),避免其它代码误清
|
||||
var shaElem = document.getElementById(chip === 'esp32' ? 'fileSha256Esp32' : 'fileSha256Gd32');
|
||||
if (shaElem) shaElem.textContent = cachedSha;
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 复用已缓存 SHA-256(文件未变)');
|
||||
return Promise.resolve(cachedSha);
|
||||
}
|
||||
return _computeAndCacheFileSha(file, chip, optArrayBuffer);
|
||||
}
|
||||
|
||||
// ==================== 文件处理 ====================
|
||||
/** @description 处理用户选择的固件文件(HEX→BIN 转换、SHA-256 计算、文件校验)。 */
|
||||
export function processFile(file, chip) {
|
||||
@@ -196,6 +262,7 @@ export function processFile(file, chip) {
|
||||
document.getElementById('fileType' + suffix).textContent = 'Binary(?)';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
||||
window.updateUploadButtonLabel();
|
||||
ensureFileShaCached(file, 'gd32');
|
||||
return;
|
||||
}
|
||||
window.gd32File = result.file;
|
||||
@@ -208,6 +275,7 @@ export function processFile(file, chip) {
|
||||
document.getElementById('fileType' + suffix).textContent = 'Binary (HEX转换)';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(result.file.size);
|
||||
window.updateUploadButtonLabel();
|
||||
ensureFileShaCached(result.file, 'gd32');
|
||||
}).catch(function(err) {
|
||||
addLog('E', '文件', 'HEX 解析失败: ' + err.message);
|
||||
showToast('HEX 文件解析失败: ' + err.message, 'error');
|
||||
@@ -228,32 +296,21 @@ export function processFile(file, chip) {
|
||||
window.updateUploadButtonLabel();
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已选择: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
||||
showToast(chip.toUpperCase() + ' 文件已选择', 'success');
|
||||
// ESP32 文件: 计算并显示 SHA-256
|
||||
if (chip === 'esp32') {
|
||||
var shaElem = document.getElementById('fileSha256' + suffix);
|
||||
if (shaElem) {
|
||||
shaElem.textContent = '计算中...';
|
||||
file.arrayBuffer().then(function(buf) {
|
||||
return crypto.subtle.digest('SHA-256', buf);
|
||||
}).then(function(hashBuffer) {
|
||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
||||
shaElem.textContent = hashHex.toUpperCase();
|
||||
// 存储到全局变量供上传时使用
|
||||
window._esp32FileSha256 = hashHex.toUpperCase();
|
||||
addLog('I', '文件', 'ESP32 固件 SHA-256: ' + hashHex.toUpperCase());
|
||||
}).catch(function(err) {
|
||||
shaElem.textContent = '计算失败: ' + err.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
// 选文件时就把 SHA-256 算好并缓存(ESP32/GD32 都算),
|
||||
// 上传时若文件未变 ensureFileShaCached 命中直接复用。
|
||||
ensureFileShaCached(file, chip);
|
||||
}
|
||||
|
||||
/** @description 清除已选固件文件并重置对应 UI 状态。 */
|
||||
export function clearFile(chip) {
|
||||
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
||||
if (chip === 'esp32') { window.esp32File = null; window.esp32FileHandle = null; window._esp32FileSha256 = null; }
|
||||
else { window.gd32File = null; window.gd32FileHandle = null; }
|
||||
if (chip === 'esp32') {
|
||||
window.esp32File = null; window.esp32FileHandle = null;
|
||||
window._esp32FileSha256 = null; window._esp32FileShaMeta = null;
|
||||
} else {
|
||||
window.gd32File = null; window.gd32FileHandle = null;
|
||||
window._gd32FileSha256 = null; window._gd32FileShaMeta = null;
|
||||
}
|
||||
document.getElementById('fileInput' + suffix).value = '';
|
||||
document.getElementById('fileInfo' + suffix).classList.remove('show');
|
||||
var shaElem = document.getElementById('fileSha256' + suffix);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/**
|
||||
* progress.js - 折叠面板 + 刷写进度 + 阶段节点 UI
|
||||
*
|
||||
* 从 app.js 的「折叠面板」「刷写进度」段提取。
|
||||
* 操作 DOM: #esp32Stages / #gd32Stages / #stageStatusBar / #progressFill / #progressTxt / #progressPct
|
||||
*
|
||||
* 共享状态(通过 window.* 引用):
|
||||
* window.activePhase / window.currentOtaTarget / window.esp32OtaPhase
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.updateStagesFromRelayState / window.cleanupAfterOtaFailure
|
||||
* window.resumePollingAfterUpload / window.addLog
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* session.js - 前端 OTA 会话状态机
|
||||
*
|
||||
* 从 app.js 的「全局状态」+「前端 OTA 会话状态机」段提取。
|
||||
* 统一管理一次 OTA 会话的完整生命周期,消除散布的布尔标志
|
||||
* 统一管理一次 OTA 会话的完整生命周期,避免散布的布尔标志
|
||||
* (isUploading / currentOtaTarget / activeUploadChip / isEsp32Rebooting)
|
||||
* 在网络错误后被轮询错误覆盖、状态混乱的问题。
|
||||
*
|
||||
@@ -20,7 +19,7 @@
|
||||
* window.gd32CompleteAnnounced / window.relayWatchLastSeq / window.gd32RelaySessionLive
|
||||
* window.lastSysStateCode / window.lastRelayState
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.stopRelayProgressWatch / window.setProgress / window.addLog
|
||||
*/
|
||||
import { addLog } from '../common/log.js';
|
||||
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* spi.js - SPI 帧构建与发送
|
||||
*
|
||||
* 从 app.js 的「SPI 发送」段提取。
|
||||
* 操作 DOM: #spiType / #spiData / #spiTimeout / #spiHint / #spiResp / #spiRespText
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.getBaseUrl / window.showToast / window.addLog
|
||||
*/
|
||||
import { showToast } from '../common/toast.js';
|
||||
|
||||
+19
-9
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* stages.js - OTA 阶段驱动 (真实状态)
|
||||
*
|
||||
* 从 app.js 的「OTA 阶段驱动 (真实状态)」段提取。
|
||||
* 将设备端 relay_state / sys_state 映射到 GD32 阶段节点 UI,
|
||||
* 并驱动 updateSystemStatusUi 综合 UI 更新。
|
||||
*
|
||||
@@ -11,7 +10,7 @@
|
||||
* window.gd32CompleteAnnounced / window.gd32RelaySessionLive
|
||||
* window.lastSysStateCode / window.lastRelayState
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.updateHeapAndTaskStatusUi / window.updateConnectionUi / window.updateHealthUi
|
||||
* window.updateStagesFromRelayState / window.setActivePhase / window.setProgress
|
||||
* window.markGd32OtaUiComplete / window.cleanupAfterOtaFailure / window.otaSessionReset
|
||||
@@ -234,6 +233,16 @@ export function updateSystemStatusUi(data) {
|
||||
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
|
||||
window.gd32RelaySessionLive = true;
|
||||
}
|
||||
/* 包进度:进入 FINISHING/COMPLETE 时先补 100%,再走完成/收尾 UI */
|
||||
if (window.gd32RelaySessionLive && relayTotal > 0 &&
|
||||
(relaySeq >= relayTotal || rs >= 4 || isGd32Complete)) {
|
||||
if (typeof window.emitRelayProgress === 'function') {
|
||||
window.emitRelayProgress(relayTotal, relayTotal, { force100: true });
|
||||
}
|
||||
} else if (window.gd32RelaySessionLive && relayTotal > 0 && relaySeq > 0 &&
|
||||
typeof window.emitRelayProgress === 'function') {
|
||||
window.emitRelayProgress(relaySeq, relayTotal);
|
||||
}
|
||||
if (window.gd32RelaySessionLive &&
|
||||
(isGd32Complete || rs === 5 ||
|
||||
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal && rs >= 3))) {
|
||||
@@ -244,8 +253,14 @@ export function updateSystemStatusUi(data) {
|
||||
// HANDSHAKING
|
||||
setActivePhase('gd32Ota');
|
||||
setProgress(50, '握手与认证中...');
|
||||
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0 && rs !== 5)) {
|
||||
// TRANSFERRING
|
||||
} else if (rs === 4) {
|
||||
// FINISHING:转发已 100%,进度条进入收尾段(勿再落回中间百分比)
|
||||
setActivePhase('gd32Ota');
|
||||
if (!window.gd32CompleteAnnounced) {
|
||||
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
||||
}
|
||||
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0 && rs !== 5 && rs !== 4)) {
|
||||
// TRANSFERRING(不含 FINISHING)
|
||||
setActivePhase('gd32Ota');
|
||||
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
||||
var relayLabel = relayTotal > 0
|
||||
@@ -258,11 +273,6 @@ export function updateSystemStatusUi(data) {
|
||||
setProgress(55, relayTotal > 0
|
||||
? ('大小协商中(共 ' + relayTotal + ' 包)')
|
||||
: '固件大小协商中');
|
||||
} else if (rs === 4) {
|
||||
setActivePhase('gd32Ota');
|
||||
if (!window.gd32CompleteAnnounced) {
|
||||
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
||||
}
|
||||
} else if (!window.gd32RelaySessionLive) {
|
||||
setActivePhase('gd32Ota');
|
||||
setProgress(48, '已触发中继,等待设备进入握手…');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* upload-esp32.js - ESP32 本机 OTA 上传流程 + 完成升级
|
||||
*
|
||||
* 从 app.js 的「上传固件」「ESP32 OTA 完成升级」段提取。
|
||||
* 包含:startUpload 入口(处理 esp32 + gd32 共用互斥/重读逻辑)、
|
||||
* XHR 上传、SHA 校验、verifyEsp32Firmware、restartEsp32。
|
||||
*
|
||||
@@ -10,7 +9,7 @@
|
||||
* window._esp32FileSha256 / window._gd32FileSha256 / window.activeXhr
|
||||
* window.isUploading / window.uploadBtnOrigText / window.activeUploadChip
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.otaSessionActive / window.otaSessionTerminated / window.otaSessionReset
|
||||
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionTarget
|
||||
* window.cleanupAfterOtaFailure / window.pausePollingForUpload
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
startUploadWaitHeartbeat
|
||||
} from './session.js';
|
||||
import { updateEsp32StagesFromPhase, switchStageFlow, setActivePhase, setProgress, markPhaseDone, cleanupAfterOtaFailure } from './progress.js';
|
||||
import { ensureFileShaCached } from './file-handle.js';
|
||||
|
||||
function isOtaBusyError(status, detail) {
|
||||
var text = (detail || '').toLowerCase();
|
||||
@@ -104,9 +104,13 @@ export async function startUpload(chip) {
|
||||
var otaChannelName = ota.channel_name || (otaChannel === 1 ? 'BLE' : 'WIFI');
|
||||
// ESP32 RECEIVING(5) 可由同一 WiFi 通道的新上传接管;后台/休眠可能让 /ota/update 没发出,
|
||||
// 若这里提前拦截,后端 beginEsp32OtaWithRetry 的同通道清理分支永远没有机会执行。
|
||||
// ERROR(8) 也放行给后端清理底层 OTA 句柄;FINALIZING(6)/REBOOTING(7) 是不可逆忙态。
|
||||
if (ssc === 5 && otaChannel === 1 && _pi < 3) continue;
|
||||
var esp32Busy = (ssc === 6 || ssc === 7 || (ssc === 5 && otaChannel !== 0));
|
||||
// ERROR(8) 放行给后端清理;REBOOTING(7) 不可逆。
|
||||
// FINALIZING(6):真收尾时后端会 403;SHA 失败后可能假占用(core 已空),
|
||||
// status 自愈 / begin 残留清理需要请求触达设备,前端同通道不硬拦。
|
||||
if ((ssc === 5 || ssc === 6) && otaChannel === 1 && _pi < 3) continue;
|
||||
var esp32Busy = (ssc === 7 ||
|
||||
(ssc === 6 && otaChannel !== 0) ||
|
||||
(ssc === 5 && otaChannel !== 0));
|
||||
// GD32 STORING(1) 可由同一 WiFi 通道的新上传接管;RELAYING(3) 不可中断。
|
||||
var gd32Busy = (ssc === 3 || (ssc === 1 && otaChannel !== 0));
|
||||
if ((esp32Busy || gd32Busy)) {
|
||||
@@ -183,31 +187,10 @@ export async function startUpload(chip) {
|
||||
showToast('开始上传固件到 ' + chip.toUpperCase(), 'success');
|
||||
try {
|
||||
var arrayBuffer = await file.arrayBuffer();
|
||||
// 刷写时重新计算 SHA-256(ESP32 和 GD32 都算)
|
||||
if (chip === 'esp32') {
|
||||
try {
|
||||
var hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
||||
window._esp32FileSha256 = hashHex.toUpperCase();
|
||||
var shaElem = document.getElementById('fileSha256Esp32');
|
||||
if (shaElem) shaElem.textContent = window._esp32FileSha256;
|
||||
addLog('I', '文件', '刷写前重新计算 SHA-256: ' + window._esp32FileSha256);
|
||||
} catch (e) {
|
||||
addLog('W', '文件', 'SHA-256 重新计算失败: ' + e.message);
|
||||
}
|
||||
} else if (chip === 'gd32') {
|
||||
try {
|
||||
var hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
||||
window._gd32FileSha256 = hashHex.toUpperCase();
|
||||
addLog('I', '文件', 'GD32 SHA-256: ' + window._gd32FileSha256);
|
||||
} catch (e) {
|
||||
addLog('W', '文件', 'GD32 SHA-256 重新计算失败: ' + e.message);
|
||||
window._gd32FileSha256 = null;
|
||||
}
|
||||
}
|
||||
// SHA-256:processFile 阶段已算好并缓存;这里若 handle.getFile() 拿回的
|
||||
// fresh file meta 未变就直接复用(命中 30–100ms/MB 的计算);
|
||||
// 用户 rebuild 使 lastModified 变化时 ensureFileShaCached 自动 fallback 到重算。
|
||||
await ensureFileShaCached(file, chip, arrayBuffer);
|
||||
var fileSize = file.size;
|
||||
var body, url = window.getBaseUrl() + '/ota/upload';
|
||||
if (chip === 'esp32') {
|
||||
|
||||
+93
-48
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* upload-gd32.js - GD32 中继 OTA 触发 + relay 态跟踪
|
||||
*
|
||||
* 从 app.js 的「GD32 OTA 触发(带 SHA-256 校验,对齐 BLE 0xF7)」段提取。
|
||||
* 包含:relay 进度观察定时器、markGd32OtaUiComplete、triggerGd32Relay。
|
||||
*
|
||||
* 共享状态(通过 window.* 引用):
|
||||
@@ -9,7 +8,7 @@
|
||||
* window.pollingPausedForUpload / window.statusPollIntervalMs
|
||||
* window.otaSessionState / window.lastSysStateCode / window.lastRelayState
|
||||
*
|
||||
* 未拆分的函数(通过 window.* 引用):
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionActive
|
||||
* window.markPhaseDone / window.setProgress / window.updateSystemStatusUi
|
||||
* window.cleanupAfterOtaFailure / window.scheduleStatusPoll / window.pollLogs
|
||||
@@ -26,16 +25,61 @@ import { markPhaseDone, setProgress } from './progress.js';
|
||||
* GD32 中继进度观察:以 /system/status 为唯一真相源。
|
||||
* 日志环只有 64 条,中继/擦除期间 INFO 会被挤掉,时间戳还是浏览器本地时间——
|
||||
* 绝不能用「最后一条日志停在 SEND_SIZE」判断卡死。
|
||||
*
|
||||
* 时序约定(避免「设备已 FINISH / 完成,UI 还在 94%」):
|
||||
* 1) 包进度变化先刷进度条 + 日志;
|
||||
* 2) 一旦 rseq>=rtot 或 rs>=4(FINISHING),先强制打出 100% 再进收尾/完成;
|
||||
* 3) markGd32OtaUiComplete 前再兜底一次 100%。
|
||||
*/
|
||||
let relayWatchTimerId = null;
|
||||
let relayWatchInFlight = false;
|
||||
window.gd32CompleteAnnounced = false;
|
||||
window.relayWatchLastSeq = -1;
|
||||
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
|
||||
window.gd32RelaySessionLive = false;
|
||||
|
||||
/**
|
||||
* 包进度变更时更新进度条并写一条「转发」日志(同 seq 不重复)。
|
||||
* @param {number} rseq
|
||||
* @param {number} rtot
|
||||
* @param {{force100?: boolean}} [opts]
|
||||
* @returns {boolean} 是否实际输出了进度
|
||||
*/
|
||||
export function emitRelayProgress(rseq, rtot, opts) {
|
||||
opts = opts || {};
|
||||
rseq = Number(rseq) || 0;
|
||||
rtot = Number(rtot) || 0;
|
||||
if (rtot <= 0) return false;
|
||||
if (opts.force100 || rseq >= rtot) {
|
||||
rseq = rtot;
|
||||
}
|
||||
if (rseq <= 0) return false;
|
||||
if (rseq === window.relayWatchLastSeq) return false;
|
||||
window.relayWatchLastSeq = rseq;
|
||||
var pct = Math.min(100, Math.round(rseq * 100 / rtot));
|
||||
setProgress(60 + Math.round(pct * 0.30),
|
||||
'已转发 ' + rseq + ' / ' + rtot + ' 包 (' + pct + '%)');
|
||||
addLog('I', 'OTA', '转发 ' + rseq + '/' + rtot + ' (' + pct + '%)');
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 完成/收尾前:若尚未打出 100%,用 status 里的包数补一条 */
|
||||
export function ensureRelayProgressComplete() {
|
||||
var data = window.latestSystemStatus;
|
||||
var o = (data && data.ota) || {};
|
||||
var rtot = Number(o.relay_total_packets) || 0;
|
||||
var rseq = Number(o.relay_seq) || 0;
|
||||
if (rtot <= 0) return;
|
||||
/* FINISHING/DONE 时后端 seq 已到 total;status 滞后时也强制 100% */
|
||||
if (rseq < rtot && (Number(o.relay_state) >= 4 || Number(o.sys_state_code) === 4)) {
|
||||
rseq = rtot;
|
||||
}
|
||||
if (rseq >= rtot) {
|
||||
emitRelayProgress(rtot, rtot, { force100: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function markGd32OtaUiComplete(msg) {
|
||||
// 已公告过:禁止再改 className / setProgress / toast,否则每 400ms 重触发动效会「疯狂跳 ✓」
|
||||
// 已公告过:禁止再改 className / setProgress / toast,否则定时器重触发动效会「疯狂跳 ✓」
|
||||
if (window.gd32CompleteAnnounced) {
|
||||
return;
|
||||
}
|
||||
@@ -44,6 +88,8 @@ export function markGd32OtaUiComplete(msg) {
|
||||
window.otaSessionState !== OtaSessionState.FAILED) {
|
||||
return;
|
||||
}
|
||||
/* 先 100% 转发进度,再宣布刷写完成 —— 避免日志里「完成」早于「1089/1089」 */
|
||||
ensureRelayProgressComplete();
|
||||
window.gd32CompleteAnnounced = true;
|
||||
stopRelayProgressWatch();
|
||||
|
||||
@@ -67,18 +113,25 @@ export function stopRelayProgressWatch() {
|
||||
clearInterval(relayWatchTimerId);
|
||||
relayWatchTimerId = null;
|
||||
}
|
||||
relayWatchInFlight = false;
|
||||
if (window.statusPollIntervalMs < 1000) {
|
||||
window.statusPollIntervalMs = 1000;
|
||||
/* 中继结束:恢复常态 1s status 轮询 */
|
||||
if (typeof window.setStatusPollIntervalMs === 'function') {
|
||||
window.setStatusPollIntervalMs(1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中继进度观察:只读 pollStatus 写入的 latestSystemStatus,不再另起 /system/status。
|
||||
* 中继期 status 加速到 400ms,使「转发 N/M」更贴近设备真实进度。
|
||||
*/
|
||||
export function startRelayProgressWatch() {
|
||||
stopRelayProgressWatch();
|
||||
if (window.pollingPausedForUpload) {
|
||||
window.resumePollingAfterUpload();
|
||||
}
|
||||
window.statusPollIntervalMs = 400;
|
||||
/* 中继期加速 status,缩小 UI 进度相对设备日志的滞后 */
|
||||
if (typeof window.setStatusPollIntervalMs === 'function') {
|
||||
window.setStatusPollIntervalMs(400);
|
||||
}
|
||||
window.scheduleStatusPoll(0);
|
||||
window.pollLogs();
|
||||
addLog('I', 'OTA', '跟踪中继进度:看进度条与 relay_seq(/system/status),勿只看日志');
|
||||
@@ -92,46 +145,38 @@ export function startRelayProgressWatch() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (relayWatchInFlight) return;
|
||||
relayWatchInFlight = true;
|
||||
fetch(window.getBaseUrl() + '/system/status', { signal: AbortSignal.timeout(3000) })
|
||||
.then(function (resp) { return resp.ok ? resp.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!data) return;
|
||||
try { window.updateSystemStatusUi(data); } catch (e) { /* ignore */ }
|
||||
var o = data.ota || {};
|
||||
var code = Number(o.sys_state_code);
|
||||
var rstate = Number(o.relay_state);
|
||||
var alive = !!o.gd32_app_alive;
|
||||
var rseq = Number(o.relay_seq) || 0;
|
||||
var rtot = Number(o.relay_total_packets) || 0;
|
||||
if (rstate >= 1 && rstate <= 4) {
|
||||
window.gd32RelaySessionLive = true;
|
||||
}
|
||||
if (window.gd32RelaySessionLive &&
|
||||
(code === 4 || rstate === 5 ||
|
||||
(alive && rtot > 0 && rseq >= rtot && rstate >= 3))) {
|
||||
markGd32OtaUiComplete('GD32 刷写完成');
|
||||
window.pollLogs();
|
||||
return;
|
||||
}
|
||||
if (code === 8 || rstate === 255) {
|
||||
stopRelayProgressWatch();
|
||||
window.pollLogs();
|
||||
return;
|
||||
}
|
||||
// 前端进度同步:包数变化时更新进度条(updateSystemStatusUi 已做)并写一条前端日志
|
||||
// 后端不再打「转发进度」文字,只靠这里 + 进度条
|
||||
if (rtot > 0 && rseq > 0 && rseq !== window.relayWatchLastSeq) {
|
||||
window.relayWatchLastSeq = rseq;
|
||||
var pct = Math.round(rseq * 100 / rtot);
|
||||
setProgress(60 + Math.round(pct * 0.30),
|
||||
'已转发 ' + rseq + ' / ' + rtot + ' 包 (' + pct + '%)');
|
||||
addLog('I', 'OTA', '转发 ' + rseq + '/' + rtot + ' (' + pct + '%)');
|
||||
}
|
||||
})
|
||||
.catch(function () { /* 下轮继续 */ })
|
||||
.then(function () { relayWatchInFlight = false; });
|
||||
var data = window.latestSystemStatus;
|
||||
if (!data || !data.ota) return;
|
||||
var o = data.ota || {};
|
||||
var code = Number(o.sys_state_code);
|
||||
var rstate = Number(o.relay_state);
|
||||
var alive = !!o.gd32_app_alive;
|
||||
var rseq = Number(o.relay_seq) || 0;
|
||||
var rtot = Number(o.relay_total_packets) || 0;
|
||||
if (rstate >= 1 && rstate <= 4) {
|
||||
window.gd32RelaySessionLive = true;
|
||||
}
|
||||
if (code === 8 || rstate === 255) {
|
||||
stopRelayProgressWatch();
|
||||
window.pollLogs();
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* 顺序:先刷包进度(含强制 100%),再判定完成。
|
||||
* FINISHING/DONE 时若 status 滞后未到 total,仍按 total 打 100%。
|
||||
*/
|
||||
var transferDone = rtot > 0 && (rseq >= rtot || rstate >= 4 || code === 4);
|
||||
if (transferDone) {
|
||||
emitRelayProgress(rtot, rtot, { force100: true });
|
||||
} else if (rtot > 0 && rseq > 0) {
|
||||
emitRelayProgress(rseq, rtot);
|
||||
}
|
||||
if (window.gd32RelaySessionLive &&
|
||||
(code === 4 || rstate === 5 ||
|
||||
(alive && rtot > 0 && rseq >= rtot && rstate >= 3))) {
|
||||
markGd32OtaUiComplete('GD32 刷写完成');
|
||||
window.pollLogs();
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user