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:
+75
-66
@@ -10,7 +10,7 @@
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.updateUploadButtonLabel / window.showToast / window.addLog
|
||||
*/
|
||||
import { formatBytes } from '../common/utils.js';
|
||||
import { formatBytes, bytesToHex } from '../common/utils.js';
|
||||
import { showToast } from '../common/toast.js';
|
||||
import { addLog } from '../common/log.js';
|
||||
|
||||
@@ -33,24 +33,31 @@ var IDB_STORE = 'file_handles';
|
||||
* 刷新后权限常为 prompt,requestPermission 必须在用户手势里才成功。
|
||||
* 此处暂存待授权 handle,点击上传区时再 requestPermission 并直接 getFile。
|
||||
*/
|
||||
window._pendingFwHandles = window._pendingFwHandles || { esp32: null, gd32: null };
|
||||
window._pendingFwHandles = { esp32: null, gd32: null };
|
||||
|
||||
/** 模块级 IndexedDB 连接缓存,避免每次 put/get 都重开 */
|
||||
var _idbDbPromise = null;
|
||||
function openHandleDb() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (!window.indexedDB) {
|
||||
reject(new Error('indexedDB unavailable'));
|
||||
return;
|
||||
}
|
||||
var req = indexedDB.open(IDB_NAME, 1);
|
||||
req.onupgradeneeded = function () {
|
||||
var db = req.result;
|
||||
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
||||
db.createObjectStore(IDB_STORE);
|
||||
if (!_idbDbPromise) {
|
||||
_idbDbPromise = new Promise(function (resolve, reject) {
|
||||
if (!window.indexedDB) {
|
||||
reject(new Error('indexedDB unavailable'));
|
||||
return;
|
||||
}
|
||||
};
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
req.onerror = function () { reject(req.error || new Error('idb open failed')); };
|
||||
});
|
||||
var req = indexedDB.open(IDB_NAME, 1);
|
||||
req.onupgradeneeded = function () {
|
||||
var db = req.result;
|
||||
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
||||
db.createObjectStore(IDB_STORE);
|
||||
}
|
||||
};
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
req.onerror = function () { reject(req.error || new Error('idb open failed')); };
|
||||
});
|
||||
// 失败时清空缓存,下次允许重试
|
||||
_idbDbPromise.catch(function () { _idbDbPromise = null; });
|
||||
}
|
||||
return _idbDbPromise;
|
||||
}
|
||||
|
||||
function idbPutHandle(chip, handle) {
|
||||
@@ -97,8 +104,8 @@ export function loadPersistedFlashTarget() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存 FSAPI handle,刷新后可默认恢复该文件 */
|
||||
export function persistFileHandle(chip, handle) {
|
||||
/** 保存 FSAPI handle,刷新后可默认恢复该文件(仅模块内使用,不导出) */
|
||||
function persistFileHandle(chip, handle) {
|
||||
if (chip !== 'esp32' && chip !== 'gd32') return Promise.resolve();
|
||||
return idbPutHandle(chip, handle || null);
|
||||
}
|
||||
@@ -136,6 +143,13 @@ function loadFileMeta(chip) {
|
||||
return map[chip] || null;
|
||||
}
|
||||
|
||||
/** 安全调用 window.updateUploadButtonLabel(恢复路径可能在 app.js 初始化前触发) */
|
||||
function safeUpdateUploadButtonLabel() {
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
}
|
||||
|
||||
/** 仅 query,不 request(刷新无用户手势时 request 会失败) */
|
||||
async function queryHandlePermission(handle) {
|
||||
if (!handle || typeof handle.getFile !== 'function') return 'denied';
|
||||
@@ -157,7 +171,7 @@ async function queryHandlePermission(handle) {
|
||||
*/
|
||||
export async function resumePersistedFirmwareWithGesture(chip) {
|
||||
if (chip !== 'esp32' && chip !== 'gd32') return false;
|
||||
var handle = (window._pendingFwHandles && window._pendingFwHandles[chip]) ||
|
||||
var handle = (window._pendingFwHandles[chip]) ||
|
||||
(chip === 'esp32' ? window.esp32FileHandle : window.gd32FileHandle) ||
|
||||
null;
|
||||
if (!handle) {
|
||||
@@ -183,7 +197,7 @@ export async function resumePersistedFirmwareWithGesture(chip) {
|
||||
}
|
||||
var file = await handle.getFile();
|
||||
applyRestoredFile(chip, handle, file, { silent: true });
|
||||
if (window._pendingFwHandles) window._pendingFwHandles[chip] = null;
|
||||
window._pendingFwHandles[chip] = null;
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已重新授权并恢复: ' + file.name);
|
||||
showToast(chip.toUpperCase() + ' 已恢复上次固件', 'success');
|
||||
return true;
|
||||
@@ -208,16 +222,14 @@ function applyRestoredFile(chip, handle, file, options) {
|
||||
if (typeof window.syncBleFileFromSelectedTarget === 'function') {
|
||||
window.syncBleFileFromSelectedTarget();
|
||||
}
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
safeUpdateUploadButtonLabel();
|
||||
}
|
||||
|
||||
/** 无权限时仍展示上次文件名,提示点上传区一键恢复 */
|
||||
function showPendingReauthUi(chip, handle, meta) {
|
||||
if (chip === 'esp32') window.esp32FileHandle = handle;
|
||||
else window.gd32FileHandle = handle;
|
||||
if (window._pendingFwHandles) window._pendingFwHandles[chip] = handle;
|
||||
window._pendingFwHandles[chip] = handle;
|
||||
if (window.sharedFirmwareFiles) {
|
||||
window.sharedFirmwareFiles[chip] = { file: null, handle: handle };
|
||||
}
|
||||
@@ -234,9 +246,7 @@ function showPendingReauthUi(chip, handle, meta) {
|
||||
if (sizeEl) sizeEl.textContent = meta && meta.size ? formatBytes(meta.size) : '--';
|
||||
var shaEl = document.getElementById('fileSha256' + suffix);
|
||||
if (shaEl) shaEl.textContent = '刷新后需点击授权';
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
safeUpdateUploadButtonLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,7 +270,7 @@ export async function restorePersistedFirmwareSelection() {
|
||||
if (perm === 'granted') {
|
||||
var file = await handle.getFile();
|
||||
applyRestoredFile(chip, handle, file, { silent: true, skipHandlePersist: true });
|
||||
if (window._pendingFwHandles) window._pendingFwHandles[chip] = null;
|
||||
window._pendingFwHandles[chip] = null;
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已恢复刷新前的固件: ' + file.name);
|
||||
continue;
|
||||
}
|
||||
@@ -281,9 +291,7 @@ export async function restorePersistedFirmwareSelection() {
|
||||
if (typeof window.syncBleFileFromSelectedTarget === 'function') {
|
||||
window.syncBleFileFromSelectedTarget();
|
||||
}
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
safeUpdateUploadButtonLabel();
|
||||
}
|
||||
|
||||
export function handleFileSelect(event, chip) {
|
||||
@@ -292,7 +300,7 @@ export function handleFileSelect(event, chip) {
|
||||
/* 传统 input 无 handle:清掉旧 FSAPI 句柄,避免热重载读到过期路径 */
|
||||
if (chip === 'esp32') window.esp32FileHandle = null;
|
||||
else window.gd32FileHandle = null;
|
||||
if (window._pendingFwHandles) window._pendingFwHandles[chip] = null;
|
||||
window._pendingFwHandles[chip] = null;
|
||||
persistFileHandle(chip, null);
|
||||
processFile(file, chip);
|
||||
}
|
||||
@@ -303,7 +311,7 @@ export async function pickFileViaFSAPI(chip) {
|
||||
* 仅在「刷新后待授权」或「尚无已加载 File」时尝试一键恢复。
|
||||
* 已有可用文件时再点上传区 → 打开选文件对话框(可换文件)。
|
||||
*/
|
||||
var pending = window._pendingFwHandles && window._pendingFwHandles[chip];
|
||||
var pending = window._pendingFwHandles[chip];
|
||||
var hasLoadedFile = !!(chip === 'esp32' ? window.esp32File : window.gd32File);
|
||||
if (pending || !hasLoadedFile) {
|
||||
var resumed = await resumePersistedFirmwareWithGesture(chip);
|
||||
@@ -321,7 +329,7 @@ export async function pickFileViaFSAPI(chip) {
|
||||
? { 'application/octet-stream': ['.bin'] }
|
||||
: { 'application/octet-stream': ['.bin', '.hex'] };
|
||||
var prevHandle = chip === 'esp32' ? window.esp32FileHandle : window.gd32FileHandle;
|
||||
if (!prevHandle && window._pendingFwHandles) {
|
||||
if (!prevHandle) {
|
||||
prevHandle = window._pendingFwHandles[chip] || null;
|
||||
}
|
||||
var opts = {
|
||||
@@ -339,8 +347,8 @@ export async function pickFileViaFSAPI(chip) {
|
||||
var file = await handle.getFile();
|
||||
if (chip === 'esp32') window.esp32FileHandle = handle;
|
||||
else window.gd32FileHandle = handle;
|
||||
if (window._pendingFwHandles) window._pendingFwHandles[chip] = null;
|
||||
processFile(file, chip);
|
||||
window._pendingFwHandles[chip] = null;
|
||||
await processFile(file, chip);
|
||||
persistFileHandle(chip, handle);
|
||||
persistFileMeta(chip, file);
|
||||
persistFlashTarget(chip);
|
||||
@@ -382,7 +390,7 @@ export function setupDropZone(zoneId, chip) {
|
||||
* @returns {{ data: Uint8Array, baseAddr: number, endAddr: number }}
|
||||
*/
|
||||
export function parseIntelHex(hexText) {
|
||||
var baseAddr = 0; // 当前扩展地址
|
||||
var baseAddr = 0; // 当前扩展地址(uint32)
|
||||
var segments = []; // { offset, data } 列表
|
||||
var minAddr = 0xFFFFFFFF;
|
||||
var maxAddr = 0;
|
||||
@@ -391,13 +399,13 @@ export function parseIntelHex(hexText) {
|
||||
var line = lines[i].trim();
|
||||
if (!line || line[0] !== ':') continue;
|
||||
// 解析行: :LLAAAATT[DD...]CC
|
||||
var byteCount = parseInt(line.substr(1, 2), 16);
|
||||
var addr16 = parseInt(line.substr(3, 4), 16);
|
||||
var recType = parseInt(line.substr(7, 2), 16);
|
||||
var byteCount = parseInt(line.slice(1, 3), 16);
|
||||
var addr16 = parseInt(line.slice(3, 7), 16);
|
||||
var recType = parseInt(line.slice(7, 9), 16);
|
||||
// 校验和验证
|
||||
var sum = 0;
|
||||
for (var j = 1; j < line.length; j += 2) {
|
||||
sum += parseInt(line.substr(j, 2), 16);
|
||||
sum += parseInt(line.slice(j, j + 2), 16);
|
||||
}
|
||||
if ((sum & 0xFF) !== 0) {
|
||||
throw new Error('HEX 行 ' + (i + 1) + ' 校验和错误: ' + line);
|
||||
@@ -406,17 +414,17 @@ export function parseIntelHex(hexText) {
|
||||
// EOF
|
||||
break;
|
||||
} else if (recType === 0x04) {
|
||||
// 扩展线性地址 (upper 16 bits)
|
||||
baseAddr = parseInt(line.substr(9, 4), 16) << 16;
|
||||
// 扩展线性地址 (upper 16 bits),>>>0 归整为 uint32
|
||||
baseAddr = (parseInt(line.slice(9, 13), 16) << 16) >>> 0;
|
||||
} else if (recType === 0x02) {
|
||||
// 扩展段地址
|
||||
baseAddr = (parseInt(line.substr(9, 4), 16) << 4) & 0xFFFFF;
|
||||
// 扩展段地址:seg << 4,不加掩码(避免截断 0x10FFEF 等合法地址)
|
||||
baseAddr = (parseInt(line.slice(9, 13), 16) << 4) >>> 0;
|
||||
} else if (recType === 0x00) {
|
||||
// 数据记录
|
||||
var absAddr = baseAddr + addr16;
|
||||
var absAddr = (baseAddr + addr16) >>> 0;
|
||||
var data = new Uint8Array(byteCount);
|
||||
for (var k = 0; k < byteCount; k++) {
|
||||
data[k] = parseInt(line.substr(9 + k * 2, 2), 16);
|
||||
data[k] = parseInt(line.slice(9 + k * 2, 11 + k * 2), 16);
|
||||
}
|
||||
segments.push({ offset: absAddr, data: data });
|
||||
if (absAddr < minAddr) minAddr = absAddr;
|
||||
@@ -459,15 +467,15 @@ export function isIntelHexText(text) {
|
||||
* 公共 HEX→BIN 转换:如果文件是 .hex 格式,自动解析为 bin。
|
||||
* BLE / WiFi 共用此函数,避免转换逻辑重复。
|
||||
* @param {File} file 原始文件
|
||||
* @returns {Promise<{file: File, converted: boolean, baseAddr: number|null}>}
|
||||
* @returns {Promise<{file: File, converted: boolean, baseAddr: number|null, endAddr: number|null}>}
|
||||
*/
|
||||
export async function convertHexFileToBin(file) {
|
||||
if (!file || !file.name.toLowerCase().endsWith('.hex')) {
|
||||
return { file: file, converted: false, baseAddr: null };
|
||||
return { file: file, converted: false, baseAddr: null, endAddr: null };
|
||||
}
|
||||
var text = await file.text();
|
||||
if (!isIntelHexText(text)) {
|
||||
return { file: file, converted: false, baseAddr: null };
|
||||
return { file: file, converted: false, baseAddr: null, endAddr: null };
|
||||
}
|
||||
var result = parseIntelHex(text);
|
||||
var binName = file.name.replace(/\.hex$/i, '.bin');
|
||||
@@ -498,8 +506,7 @@ function _computeAndCacheFileSha(file, chip, optArrayBuffer) {
|
||||
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();
|
||||
var hashHex = bytesToHex(new Uint8Array(hashBuffer)).toUpperCase();
|
||||
if (shaElem) shaElem.textContent = hashHex;
|
||||
var meta = { size: file.size, lastModified: file.lastModified, name: file.name };
|
||||
if (chip === 'esp32') {
|
||||
@@ -546,10 +553,13 @@ export function ensureFileShaCached(file, chip, optArrayBuffer) {
|
||||
// ==================== 文件处理 ====================
|
||||
/**
|
||||
* @description 处理用户选择的固件文件(HEX→BIN 转换、SHA-256 计算、文件校验)。
|
||||
* 对 GD32 .hex 走异步转换;返回 Promise 以便调用方 await
|
||||
* (避免上游用 setTimeout 硬等,见 upload-esp32.js)。
|
||||
* @param {File} file
|
||||
* @param {'esp32'|'gd32'} chip
|
||||
* @param {{silent?: boolean, skipHandlePersist?: boolean}=} options
|
||||
* silent: 恢复场景不弹 toast;skipHandlePersist: 已从 IDB 读出时不再写回
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function processFile(file, chip, options) {
|
||||
var opts = options || {};
|
||||
@@ -565,7 +575,7 @@ export function processFile(file, chip, options) {
|
||||
}
|
||||
if (chip === 'gd32' && isHexByName) {
|
||||
// GD32 固件: .hex 文件自动转换为 .bin,然后替换文件对象
|
||||
convertHexFileToBin(file).then(function(result) {
|
||||
return convertHexFileToBin(file).then(function(result) {
|
||||
if (!result.converted) {
|
||||
// 文件名 .hex 但内容不像,直接用原始文件
|
||||
addLog('W', '文件', file.name + ' 扩展名为 .hex 但内容不像 HEX 格式,按原始文件处理');
|
||||
@@ -574,9 +584,8 @@ export function processFile(file, chip, options) {
|
||||
document.getElementById('fileName' + suffix).textContent = file.name;
|
||||
document.getElementById('fileType' + suffix).textContent = 'Binary(?)';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
||||
window.updateUploadButtonLabel();
|
||||
ensureFileShaCached(file, 'gd32');
|
||||
return;
|
||||
safeUpdateUploadButtonLabel();
|
||||
return ensureFileShaCached(file, 'gd32');
|
||||
}
|
||||
window.gd32File = result.file;
|
||||
var baseHex = '0x' + result.baseAddr.toString(16).toUpperCase().padStart(8, '0');
|
||||
@@ -587,16 +596,16 @@ export function processFile(file, chip, options) {
|
||||
document.getElementById('fileName' + suffix).textContent = result.file.name + ' (由 ' + file.name + ' 转换)';
|
||||
document.getElementById('fileType' + suffix).textContent = 'Binary (HEX转换)';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(result.file.size);
|
||||
window.updateUploadButtonLabel();
|
||||
ensureFileShaCached(result.file, 'gd32');
|
||||
safeUpdateUploadButtonLabel();
|
||||
return ensureFileShaCached(result.file, 'gd32');
|
||||
}).catch(function(err) {
|
||||
addLog('E', '文件', 'HEX 解析失败: ' + err.message);
|
||||
if (!silent) showToast('HEX 文件解析失败: ' + err.message, 'error');
|
||||
window.gd32File = null;
|
||||
document.getElementById('fileInfo' + suffix).classList.remove('show');
|
||||
window.updateUploadButtonLabel();
|
||||
safeUpdateUploadButtonLabel();
|
||||
throw err; // 让上游 await 感知失败
|
||||
});
|
||||
return; // 异步,回调里设置 gd32File
|
||||
}
|
||||
// .bin 文件或 ESP32 文件: 直接使用
|
||||
if (chip === 'esp32') window.esp32File = file;
|
||||
@@ -606,7 +615,7 @@ export function processFile(file, chip, options) {
|
||||
document.getElementById('fileType' + suffix).textContent =
|
||||
isHexByName ? 'Intel HEX' : 'Binary';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
||||
window.updateUploadButtonLabel();
|
||||
safeUpdateUploadButtonLabel();
|
||||
if (silent) {
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已恢复: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
||||
} else {
|
||||
@@ -615,7 +624,7 @@ export function processFile(file, chip, options) {
|
||||
}
|
||||
// 选文件时就把 SHA-256 算好并缓存(ESP32/GD32 都算),
|
||||
// 上传时若文件未变 ensureFileShaCached 命中直接复用。
|
||||
ensureFileShaCached(file, chip);
|
||||
return Promise.resolve(ensureFileShaCached(file, chip));
|
||||
}
|
||||
|
||||
/** @description 清除已选固件文件并重置对应 UI 状态。 */
|
||||
@@ -628,13 +637,13 @@ export function clearFile(chip) {
|
||||
window.gd32File = null; window.gd32FileHandle = null;
|
||||
window._gd32FileSha256 = null; window._gd32FileShaMeta = null;
|
||||
}
|
||||
if (window._pendingFwHandles) window._pendingFwHandles[chip] = null;
|
||||
window._pendingFwHandles[chip] = null;
|
||||
persistFileHandle(chip, null);
|
||||
persistFileMeta(chip, null);
|
||||
document.getElementById('fileInput' + suffix).value = '';
|
||||
document.getElementById('fileInfo' + suffix).classList.remove('show');
|
||||
var shaElem = document.getElementById('fileSha256' + suffix);
|
||||
if (shaElem) shaElem.textContent = '-';
|
||||
window.updateUploadButtonLabel();
|
||||
var shaEl = document.getElementById('fileSha256' + suffix);
|
||||
if (shaEl) shaEl.textContent = '-';
|
||||
safeUpdateUploadButtonLabel();
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 文件已清除');
|
||||
}
|
||||
+64
-34
@@ -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');
|
||||
});
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* spi.js - SPI 帧构建与发送
|
||||
*
|
||||
* 操作 DOM: #spiType / #spiData / #spiTimeout / #spiHint / #spiResp / #spiRespText
|
||||
*
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.getBaseUrl / window.showToast / window.addLog
|
||||
*/
|
||||
import { showToast } from '../common/toast.js';
|
||||
import { addLog } from '../common/log.js';
|
||||
|
||||
function updateSpiHint() {
|
||||
var typeHex = document.getElementById('spiType').value.trim();
|
||||
var dataHex = document.getElementById('spiData').value.trim().replace(/\s/g, '');
|
||||
var hint = document.getElementById('spiHint');
|
||||
if (!typeHex || !/^[0-9a-fA-F]{1,2}$/.test(typeHex)) {
|
||||
hint.className = 'helper-note err';
|
||||
hint.innerHTML = 'type 需填 1 字节 hex(如 01, B0)。';
|
||||
return;
|
||||
}
|
||||
if (dataHex && !/^[0-9a-fA-F]*$/.test(dataHex)) {
|
||||
hint.className = 'helper-note err';
|
||||
hint.innerHTML = '数据包含非 HEX 字符。';
|
||||
return;
|
||||
}
|
||||
if (dataHex && dataHex.length % 2 !== 0) {
|
||||
hint.className = 'helper-note err';
|
||||
hint.innerHTML = 'HEX 长度必须为偶数。';
|
||||
return;
|
||||
}
|
||||
var bytes = dataHex ? dataHex.length / 2 : 0;
|
||||
var totalOnWire = 5 + bytes;
|
||||
hint.className = 'helper-note ok';
|
||||
hint.innerHTML = 'type=<code>0x' + typeHex.toUpperCase() + '</code>' +
|
||||
(bytes > 0 ? ',payload: <b>' + bytes + '</b>B' : ',无 payload') +
|
||||
',线上 <b>' + totalOnWire + '</b>B(SOF+TYPE+LEN+PAYLOAD+CRC)';
|
||||
}
|
||||
|
||||
export async function sendSpiFrame() {
|
||||
var typeHex = document.getElementById('spiType').value.trim();
|
||||
var dataStr = document.getElementById('spiData').value.trim();
|
||||
var timeout = parseInt(document.getElementById('spiTimeout').value) || 1000;
|
||||
if (!typeHex || !/^[0-9a-fA-F]{1,2}$/.test(typeHex)) {
|
||||
showToast('type 需填 1 字节 hex', 'error');
|
||||
return;
|
||||
}
|
||||
var dataHex = dataStr.replace(/\s/g, '');
|
||||
if (dataHex && !/^[0-9a-fA-F]*$/.test(dataHex)) {
|
||||
showToast('数据包含非 HEX 字符', 'error');
|
||||
return;
|
||||
}
|
||||
if (dataHex && dataHex.length % 2 !== 0) {
|
||||
showToast('HEX 数据长度必须为偶数', 'error');
|
||||
return;
|
||||
}
|
||||
var txBytes = dataHex ? dataHex.length / 2 : 0;
|
||||
addLog('I', 'SPI', 'type=0x' + typeHex.toUpperCase() +
|
||||
(txBytes > 0 ? ', payload=' + dataHex.toUpperCase() : ', 无负载'));
|
||||
var body = { type: typeHex, data: dataHex.toUpperCase(), timeout_ms: timeout };
|
||||
try {
|
||||
var resp = await fetch(window.getBaseUrl() + '/spi/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(timeout + 2000)
|
||||
});
|
||||
var result = await resp.json();
|
||||
if (result.status === 'ok') {
|
||||
var recvHex = result.recv || '';
|
||||
var respTypeName = {'80': 'ACK', '81': 'WAIT', '82': 'ERROR'};
|
||||
var respTypeLabel = respTypeName[result.resp_type] || ('0x' + result.resp_type);
|
||||
addLog('I', 'SPI', '→ ' + respTypeLabel +
|
||||
(recvHex ? ' 0x' + recvHex : '') + ' (' + (result.resp_len || 0) + 'B)');
|
||||
var rd = document.getElementById('spiResp');
|
||||
rd.style.display = 'block';
|
||||
document.getElementById('spiRespText').textContent =
|
||||
'type=0x' + result.type + ' payload=' + result.payload_len + 'B → ' + respTypeLabel +
|
||||
(recvHex ? ' 0x' + recvHex : '') +
|
||||
' (' + (result.resp_len || 0) + 'B)';
|
||||
showToast('SPI 发送成功', 'success');
|
||||
} else {
|
||||
addLog('E', 'SPI', '发送失败: code=' + result.code + ' ' + (result.message || ''));
|
||||
var rd = document.getElementById('spiResp');
|
||||
rd.style.display = 'block';
|
||||
document.getElementById('spiRespText').textContent =
|
||||
'错误: code=' + result.code + ' ' + (result.message || '');
|
||||
showToast('SPI 发送失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
addLog('E', 'SPI', '发送失败: ' + e.message);
|
||||
showToast('SPI 发送失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export function clearSpiFields() {
|
||||
document.getElementById('spiType').value = '01';
|
||||
document.getElementById('spiData').value = '';
|
||||
document.getElementById('spiTimeout').value = '1000';
|
||||
document.getElementById('spiResp').style.display = 'none';
|
||||
updateSpiHint();
|
||||
}
|
||||
|
||||
// 导出 updateSpiHint 供入口挂载 window.updateSpiHint
|
||||
export { updateSpiHint };
|
||||
+131
-87
@@ -4,28 +4,31 @@
|
||||
* 将设备端 relay_state / sys_state 映射到 GD32 阶段节点 UI,
|
||||
* 并驱动 updateSystemStatusUi 综合 UI 更新。
|
||||
*
|
||||
* 结构:
|
||||
* applyDashboardUi — 堆/连接/版本/温湿度等常驻面板
|
||||
* applyGd32OtaSessionUi — GD32 会话进度/完成/失败(唯一 UI 应用路径)
|
||||
* updateSystemStatusUi — 入口:仪表盘 →(非 BLE/非 ESP32-OTA)会话
|
||||
*
|
||||
* 共享状态(通过 window.* 引用):
|
||||
* window.latestSystemStatus / window.currentOtaTarget / window.esp32OtaPhase
|
||||
* window.otaSessionState / window.otaSessionTarget / window.isUploading
|
||||
* window.gd32CompleteAnnounced / window.gd32RelaySessionLive
|
||||
* window.lastSysStateCode / window.lastRelayState
|
||||
*
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.updateHeapAndTaskStatusUi / window.updateConnectionUi / window.updateHealthUi
|
||||
* window.updateStagesFromRelayState / window.setActivePhase / window.setProgress
|
||||
* window.setActivePhase / window.setProgress
|
||||
* window.markGd32OtaUiComplete / window.cleanupAfterOtaFailure / window.otaSessionReset
|
||||
* window.stopRelayProgressWatch / window.pollLogs / window.formatBytes / window.formatUptime
|
||||
* window.stopRelayProgressWatch / window.pollLogs / window.emitRelayProgress
|
||||
*/
|
||||
import { formatBytes, formatUptime } from '../common/utils.js';
|
||||
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi } from '../common/connection.js';
|
||||
import { setActivePhase, setProgress, markPhaseDone } from './progress.js';
|
||||
import {
|
||||
otaSessionActive, otaSessionReset, otaSessionTransition, OtaSessionState
|
||||
} from './session.js';
|
||||
import { setActivePhase, setProgress } from './progress.js';
|
||||
import { otaSessionReset, OtaSessionState } from './session.js';
|
||||
|
||||
// Relay state comes from relay_state_t in main/relay/ota_relay.h:
|
||||
// 0=IDLE, 1=HANDSHAKING, 2=SIZE_NEG, 3=TRANSFERRING,
|
||||
// 4=FINISHING, 5=DONE, 255=ERROR.
|
||||
// 与 index.html #gd32Stages s1..s6 一一对应(rs→节点序号):
|
||||
// 1→s1, 2→s2, 3→s3, 4→s4, 5→s1..s6 全 done。
|
||||
const relayStateDetailMap = {
|
||||
0: '待机',
|
||||
1: '认证握手',
|
||||
@@ -36,48 +39,56 @@ const relayStateDetailMap = {
|
||||
255: '错误'
|
||||
};
|
||||
|
||||
/** 上次刷到阶段条的 displayRelayState,未变则跳过 DOM 重建 */
|
||||
var lastDisplayedRelayState = undefined;
|
||||
|
||||
/**
|
||||
* 将 relay_state 映射到 GD32 阶段节点 (s1..s6)。
|
||||
* @param {number|null|undefined} rs
|
||||
*/
|
||||
export function updateStagesFromRelayState(rs) {
|
||||
var ids = ['s1', 's2', 's3', 's4', 's5', 's6'];
|
||||
var detailIds = ['sd1', 'sd2', 'sd3', 'sd4', 'sd5', 'sd6'];
|
||||
var all = ids.map(function(id) { return document.getElementById(id); });
|
||||
var details = detailIds.map(function(id) { return document.getElementById(id); });
|
||||
// reset all
|
||||
all.forEach(function (s) { if (s) s.className = 'stage-node'; });
|
||||
details.forEach(function (d) { if (d) d.textContent = ''; });
|
||||
// status bar
|
||||
var statusBar = document.getElementById('stageStatusBar');
|
||||
var statusText = document.getElementById('stageStatusText');
|
||||
if (rs === undefined || rs === null) {
|
||||
if (statusBar) statusBar.className = 'stage-status-bar';
|
||||
if (statusText) statusText.textContent = '待机';
|
||||
lastDisplayedRelayState = rs;
|
||||
return;
|
||||
}
|
||||
if (rs === 255) {
|
||||
all.forEach(function (s) { if (s) s.className = 'stage-node err'; });
|
||||
if (statusBar) statusBar.className = 'stage-status-bar err';
|
||||
if (statusText) statusText.textContent = '错误';
|
||||
lastDisplayedRelayState = 255;
|
||||
return;
|
||||
}
|
||||
/* rs → 节点:1..4 直接对应 s1..s4;rs=5 DONE → 点亮完成链(s1..s6 全 done) */
|
||||
var activeStage = 0;
|
||||
if (rs === 1) activeStage = 1;
|
||||
else if (rs === 2) activeStage = 2;
|
||||
else if (rs === 3) activeStage = 3;
|
||||
else if (rs === 4) activeStage = 5;
|
||||
else if (rs === 4) activeStage = 4;
|
||||
else if (rs === 5) activeStage = 6;
|
||||
for (var i = 0; i < activeStage - 1; i++) {
|
||||
if (all[i]) all[i].className = 'stage-node done';
|
||||
}
|
||||
if (rs === 5) {
|
||||
/* DONE:s1..s5 已在上方标 done,s5/s6 一并完成 */
|
||||
if (all[4]) all[4].className = 'stage-node done';
|
||||
if (all[5]) all[5].className = 'stage-node done';
|
||||
if (details[5]) details[5].textContent = relayStateDetailMap[5];
|
||||
} else if (activeStage >= 1 && activeStage <= 6) {
|
||||
if (all[activeStage - 1]) all[activeStage - 1].className = 'stage-node live';
|
||||
if (details[activeStage - 1]) {
|
||||
details[activeStage - 1].textContent = relayStateDetailMap[rs] || '';
|
||||
}
|
||||
}
|
||||
// Show detail text for the active stage
|
||||
var detailText = relayStateDetailMap[rs] || '';
|
||||
if (activeStage >= 1 && activeStage <= 6 && details[activeStage - 1]) {
|
||||
details[activeStage - 1].textContent = detailText;
|
||||
}
|
||||
// Update status bar
|
||||
if (statusBar && statusText) {
|
||||
if (rs === 0) {
|
||||
statusBar.className = 'stage-status-bar';
|
||||
@@ -87,27 +98,33 @@ export function updateStagesFromRelayState(rs) {
|
||||
statusText.textContent = (relayStateDetailMap[rs] || '未知') + ' (' + rs + ')';
|
||||
}
|
||||
}
|
||||
lastDisplayedRelayState = rs;
|
||||
}
|
||||
|
||||
export function updateSystemStatusUi(data) {
|
||||
if (!data) return;
|
||||
window.latestSystemStatus = data;
|
||||
function setDisplayedRelayState(rs) {
|
||||
if (lastDisplayedRelayState === rs) return;
|
||||
updateStagesFromRelayState(rs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 常驻仪表盘:堆、连接、健康、版本、温湿度、IP/RSSI、OTA cache。
|
||||
* @param {object} data /system/status JSON
|
||||
* @param {object} ota data.ota
|
||||
* @param {object} wifi data.wifi
|
||||
*/
|
||||
function applyDashboardUi(data, ota, wifi) {
|
||||
updateHeapAndTaskStatusUi(data);
|
||||
var wifi = data.wifi || {};
|
||||
var ota = data.ota || {};
|
||||
var healthCode = data.health;
|
||||
// Use global relayStateDetailMap defined in updateStagesFromRelayState section
|
||||
var connected = !!wifi.connected;
|
||||
var connText = connected ? (wifi.mode || 'STA') + ' 已连接' : (wifi.mode || 'STA') + ' 未连接';
|
||||
updateConnectionUi(connected, connText);
|
||||
updateHealthUi(healthCode, data.free_heap);
|
||||
updateHealthUi(data.health, data.free_heap);
|
||||
|
||||
if (data.version) {
|
||||
var pillVerEl = document.getElementById('pillVer');
|
||||
if (pillVerEl) {
|
||||
pillVerEl.textContent = 'v' + data.version;
|
||||
pillVerEl.dataset.tooltip = data.build_time ? ("编译: " + data.build_time) : '';
|
||||
pillVerEl.dataset.tooltip = data.build_time ? ('编译: ' + data.build_time) : '';
|
||||
}
|
||||
// GD32 版本
|
||||
var mGd32App = document.getElementById('mGd32App');
|
||||
var mGd32Boot = document.getElementById('mGd32Boot');
|
||||
if (mGd32App) {
|
||||
@@ -124,24 +141,26 @@ export function updateSystemStatusUi(data) {
|
||||
mGd32Boot.textContent = ota.gd32_boot;
|
||||
mGd32Boot.className = 'v ok';
|
||||
} else {
|
||||
mGd32Boot.textContent = ota.gd32_app_alive ? '未知' : '离线';
|
||||
/* BOOT 版本与 APP alive 无直接关系,无版本时统一「未知」 */
|
||||
mGd32Boot.textContent = '未知';
|
||||
mGd32Boot.className = 'v';
|
||||
}
|
||||
}
|
||||
}
|
||||
// temperature
|
||||
|
||||
var mTempEl = document.getElementById('mTemp');
|
||||
if (mTempEl) mTempEl.textContent =
|
||||
Number.isFinite(data.temperature) ? data.temperature.toFixed(1) + '\u00B0C' : '--';
|
||||
// uptime
|
||||
if (mTempEl) {
|
||||
mTempEl.textContent =
|
||||
Number.isFinite(data.temperature) ? data.temperature.toFixed(1) + '\u00B0C' : '--';
|
||||
}
|
||||
var uptimeEl = document.getElementById('pillUptime');
|
||||
if (uptimeEl) uptimeEl.textContent = '\u23F1 ' + formatUptime(data.uptime_sec);
|
||||
// wifi / signal
|
||||
var pillIPEl = document.getElementById('pillIP');
|
||||
if (pillIPEl) pillIPEl.textContent = wifi.ip || '--';
|
||||
var rssi = (wifi.rssi !== undefined && wifi.rssi !== -127) ? wifi.rssi + ' dBm' : '--';
|
||||
var pillRSSIEl = document.getElementById('pillRSSI');
|
||||
if (pillRSSIEl) pillRSSIEl.textContent = rssi;
|
||||
|
||||
var mOtaCache = document.getElementById('mOtaCache');
|
||||
var otaStored = ota.stored_size || 0;
|
||||
if (mOtaCache) {
|
||||
@@ -153,19 +172,14 @@ export function updateSystemStatusUi(data) {
|
||||
mOtaCache.className = 'v';
|
||||
}
|
||||
}
|
||||
// 共享进度条按当前连接模式归属:BLE 模式下由 ble_ota.js 的设备 ACK
|
||||
// 驱动包数、阶段和完成状态,HTTP 状态轮询不能覆盖这套实时进度。
|
||||
if (window.connectionMode === 'ble') {
|
||||
return;
|
||||
}
|
||||
// relay state - skip GD32 relay UI when ESP32 OTA is active
|
||||
if (window.currentOtaTarget === 'esp32' && window.esp32OtaPhase > 0) {
|
||||
return;
|
||||
}
|
||||
// ★ 状态机守卫:本会话已 FAILED 时,禁止后端状态覆盖失败 UI。
|
||||
// 后端可能仍在跑 GD32 中继(网络断开不会停止设备端流程),
|
||||
// 若不拦截,会把进度条/阶段从"网络错误"错误地刷回"数据转发"。
|
||||
// 只有设备真正回 IDLE 才允许自动复位状态机(见下方 else 分支)。
|
||||
}
|
||||
|
||||
/**
|
||||
* GD32 OTA 会话 UI:阶段节点 + 进度条 + 完成/失败。
|
||||
* 门控逻辑保持与历史实现一致,勿随意放宽完成条件。
|
||||
* @param {object} ota data.ota
|
||||
*/
|
||||
function applyGd32OtaSessionUi(ota) {
|
||||
var rs = ota.relay_state;
|
||||
var sysState = ota.sys_state || '';
|
||||
var sysStateCode = Number.isFinite(ota.sys_state_code) ? ota.sys_state_code : -1;
|
||||
@@ -173,42 +187,41 @@ export function updateSystemStatusUi(data) {
|
||||
var isGd32Relaying = sysState === 'GD32_RELAYING' || sysStateCode === 3;
|
||||
var isGd32Complete = sysState === 'GD32_COMPLETE' || sysStateCode === 4;
|
||||
var isGd32Error = sysState === 'ERROR' || sysStateCode === 8 || rs === 255;
|
||||
// OTA cache progress (GD32 upload to ESP32 flash)
|
||||
|
||||
var total = ota.total_size || 0;
|
||||
var stored = ota.stored_size || 0;
|
||||
var storageProgress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
|
||||
// GD32 relay forwarding progress — 以 /system/status 的 relay_seq 为准,不依赖日志
|
||||
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;
|
||||
var gd32AppAlive = ota.gd32_app_alive;
|
||||
|
||||
/* ★ FAILED 守卫:禁止后端状态覆盖失败 UI;设备回 IDLE 才自动复位 */
|
||||
if (window.otaSessionState === OtaSessionState.FAILED) {
|
||||
var _deviceIdle = (sysStateCode === 0 || sysStateCode === 2 || sysStateCode === 4) &&
|
||||
(rs === 0 || rs === 5);
|
||||
if (!_deviceIdle) {
|
||||
// 会话已失败,但设备可能仍在中继:继续用 relay_seq 刷进度,避免“假卡死”
|
||||
var deviceIdle = (sysStateCode === 0 || sysStateCode === 2 || sysStateCode === 4) &&
|
||||
(rs === 0 || rs === 5);
|
||||
if (!deviceIdle) {
|
||||
if ((rs === 3 || isGd32Relaying) && relayTotal > 0) {
|
||||
setActivePhase('gd32Ota');
|
||||
var failRelayOverall = 60 + Math.round(relayProgress * 0.30);
|
||||
setProgress(failRelayOverall,
|
||||
'已转发 ' + relaySeq + ' / ' + relayTotal + ' 包(设备仍在跑)');
|
||||
updateStagesFromRelayState(3);
|
||||
setDisplayedRelayState(3);
|
||||
} else {
|
||||
updateStagesFromRelayState(255);
|
||||
setDisplayedRelayState(255);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 设备已回 IDLE —— 自动复位状态机,恢复正常 UI
|
||||
otaSessionReset();
|
||||
window.stopRelayProgressWatch();
|
||||
// 落到下方正常分支刷新 UI
|
||||
/* 落到下方正常分支刷新 UI */
|
||||
}
|
||||
|
||||
var displayRelayState = isGd32Complete ? 5 : (isGd32Relaying && rs === 0 ? 1 : rs);
|
||||
if (window.currentOtaTarget !== 'esp32') { updateStagesFromRelayState(displayRelayState); }
|
||||
if (window.currentOtaTarget !== 'esp32') {
|
||||
setDisplayedRelayState(displayRelayState);
|
||||
}
|
||||
|
||||
/* 本会话正在 GD32 路径(含仅 RELAYING 而 target 被清的情况) */
|
||||
var gd32Session =
|
||||
window.otaSessionState === OtaSessionState.RELAYING ||
|
||||
window.currentOtaTarget === 'gd32' ||
|
||||
@@ -225,25 +238,28 @@ export function updateSystemStatusUi(data) {
|
||||
window.cleanupAfterOtaFailure(failDetail);
|
||||
window.pollLogs();
|
||||
return;
|
||||
} else if (isGd32Error) {
|
||||
// 非当前 OTA 会话的错误:仅标记阶段指示器,不触发完整清理
|
||||
updateStagesFromRelayState(255);
|
||||
}
|
||||
|
||||
if (isGd32Error) {
|
||||
setDisplayedRelayState(255);
|
||||
var sbErr = document.getElementById('stageStatusBar');
|
||||
var stErr = document.getElementById('stageStatusText');
|
||||
if (sbErr) sbErr.className = 'stage-status-bar err';
|
||||
if (stErr) stErr.textContent = 'GD32 错误(待机)';
|
||||
} else if (window.otaSessionState === OtaSessionState.UPLOADING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.otaSessionState === OtaSessionState.UPLOADING) {
|
||||
/* 上传进行中:不要用旧缓存/旧完成态覆盖进度条 */
|
||||
} else if (window.otaSessionState === OtaSessionState.RELAYING) {
|
||||
/* 必须先看到本会话真实中继(HANDSHAKE~FINISHING),才允许 DONE;
|
||||
* 禁止一进 RELAYING 就吃掉上一次的 rs=5 / app_alive。 */
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.otaSessionState === OtaSessionState.RELAYING) {
|
||||
/* 必须先看到本会话真实中继,才允许 DONE */
|
||||
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
|
||||
window.gd32RelaySessionLive = true;
|
||||
}
|
||||
/*
|
||||
* 设备侧 ERROR 常被 status 软清为 IDLE,前端收不到 code=8。
|
||||
* 已见真实中继、设备已回 IDLE、且未宣布完成 → 按失败收尾,释放会话。
|
||||
*/
|
||||
|
||||
var deviceIdleAfterRelay =
|
||||
(sysStateCode === 0 || sysStateCode === 2) &&
|
||||
(rs === 0 || rs === 5) &&
|
||||
@@ -257,7 +273,11 @@ export function updateSystemStatusUi(data) {
|
||||
window.pollLogs();
|
||||
return;
|
||||
}
|
||||
/* 包进度:进入 FINISHING/COMPLETE 时先补 100%,再走完成/收尾 UI */
|
||||
|
||||
/*
|
||||
* 包进度/完成:统一走 emitRelayProgress + markGd32OtaUiComplete。
|
||||
* FINISHING 时 force100 后只改文案,禁止 setProgress(95) 回跳。
|
||||
*/
|
||||
if (window.gd32RelaySessionLive && relayTotal > 0 &&
|
||||
(relaySeq >= relayTotal || rs >= 4 || isGd32Complete)) {
|
||||
if (typeof window.emitRelayProgress === 'function') {
|
||||
@@ -267,32 +287,29 @@ export function updateSystemStatusUi(data) {
|
||||
typeof window.emitRelayProgress === 'function') {
|
||||
window.emitRelayProgress(relaySeq, relayTotal);
|
||||
}
|
||||
|
||||
if (window.gd32RelaySessionLive &&
|
||||
(isGd32Complete || rs === 5 ||
|
||||
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal && rs >= 3))) {
|
||||
if (!window.gd32CompleteAnnounced) {
|
||||
window.markGd32OtaUiComplete('GD32 刷写完成');
|
||||
}
|
||||
} else if (rs === 1) {
|
||||
// HANDSHAKING
|
||||
return;
|
||||
}
|
||||
|
||||
if (rs === 1) {
|
||||
setActivePhase('gd32Ota');
|
||||
setProgress(50, '握手与认证中...');
|
||||
} else if (rs === 4) {
|
||||
// FINISHING:转发已 100%,进度条进入收尾段(勿再落回中间百分比)
|
||||
/* 收尾:保持 100%,只更新文案(避免 100→95 回跳) */
|
||||
setActivePhase('gd32Ota');
|
||||
if (!window.gd32CompleteAnnounced) {
|
||||
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
||||
setProgress(100, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
||||
}
|
||||
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0 && rs !== 5 && rs !== 4)) {
|
||||
// TRANSFERRING(不含 FINISHING)
|
||||
} else if (rs === 3 && relaySeq <= 0) {
|
||||
setActivePhase('gd32Ota');
|
||||
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
||||
var relayLabel = relayTotal > 0
|
||||
? ('已转发 ' + relaySeq + ' / ' + relayTotal + ' 包 (' + relayProgress + '%)')
|
||||
: '数据包传输中...';
|
||||
setProgress(relayOverall, relayLabel);
|
||||
setProgress(60, '数据包传输中...');
|
||||
} else if (rs === 2 || (isGd32Relaying && relaySeq === 0)) {
|
||||
// SIZE_NEG
|
||||
setActivePhase('gd32Ota');
|
||||
setProgress(55, relayTotal > 0
|
||||
? ('大小协商中(共 ' + relayTotal + ' 包)')
|
||||
@@ -301,13 +318,40 @@ export function updateSystemStatusUi(data) {
|
||||
setActivePhase('gd32Ota');
|
||||
setProgress(48, '已触发中继,等待设备进入握手…');
|
||||
}
|
||||
} else if (isGd32Storing || (total > 0 && stored < total && !window.isUploading &&
|
||||
window.otaSessionState !== OtaSessionState.DONE &&
|
||||
window.otaSessionState !== OtaSessionState.IDLE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGd32Storing || (total > 0 && stored < total && !window.isUploading &&
|
||||
window.otaSessionState !== OtaSessionState.DONE &&
|
||||
window.otaSessionState !== OtaSessionState.IDLE)) {
|
||||
setActivePhase('gd32Ota');
|
||||
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
|
||||
if (!window.isUploading) {
|
||||
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total) + '');
|
||||
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /system/status → UI 总入口。
|
||||
* @param {object} data
|
||||
*/
|
||||
export function updateSystemStatusUi(data) {
|
||||
if (!data) return;
|
||||
window.latestSystemStatus = data;
|
||||
var wifi = data.wifi || {};
|
||||
var ota = data.ota || {};
|
||||
|
||||
applyDashboardUi(data, ota, wifi);
|
||||
|
||||
/* BLE:仪表盘可更新,进度/阶段由 ble 路径独占 */
|
||||
if (window.connectionMode === 'ble') {
|
||||
return;
|
||||
}
|
||||
/* ESP32 OTA 进行中:勿用 GD32 relay 覆盖阶段条 */
|
||||
if (window.currentOtaTarget === 'esp32' && window.esp32OtaPhase > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyGd32OtaSessionUi(ota);
|
||||
}
|
||||
|
||||
@@ -162,8 +162,9 @@ export async function startUpload(chip) {
|
||||
file = freshFile;
|
||||
} else {
|
||||
if (freshFile.name.toLowerCase().endsWith('.hex')) {
|
||||
window.processFile(freshFile, 'gd32');
|
||||
await new Promise(function(resolve) { setTimeout(resolve, 200); });
|
||||
// processFile 现已返回 Promise,await 即可拿到转换后的 gd32File,
|
||||
// 不再用 setTimeout 硬等(避免 HEX 稍大时拿到 null)。
|
||||
await window.processFile(freshFile, 'gd32');
|
||||
file = window.gd32File;
|
||||
} else {
|
||||
window.gd32File = freshFile;
|
||||
|
||||
+34
-113
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* upload-gd32.js - GD32 中继 OTA 触发 + relay 态跟踪
|
||||
* upload-gd32.js - GD32 中继 OTA 触发 + 进度辅助
|
||||
*
|
||||
* 包含:relay 进度观察定时器、markGd32OtaUiComplete、triggerGd32Relay。
|
||||
* 进度/完成/失败 UI 的唯一应用路径:pollStatus → updateSystemStatusUi(stages.js)。
|
||||
* 本文件只负责:
|
||||
* - 触发 /ota/relay
|
||||
* - 中继期加速 status 轮询(400ms)
|
||||
* - emitRelayProgress / markGd32OtaUiComplete 等被 stages 调用的辅助函数
|
||||
*
|
||||
* 共享状态(通过 window.* 引用):
|
||||
* window.gd32CompleteAnnounced / window.relayWatchLastSeq / window.gd32RelaySessionLive
|
||||
@@ -9,7 +13,7 @@
|
||||
* window.otaSessionState / window.lastSysStateCode / window.lastRelayState
|
||||
*
|
||||
* 复用的函数(通过 window.* 引用):
|
||||
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionActive
|
||||
* window.otaSessionTransition / window.OtaSessionState
|
||||
* window.markPhaseDone / window.setProgress / window.updateSystemStatusUi
|
||||
* window.cleanupAfterOtaFailure / window.scheduleStatusPoll / window.pollLogs
|
||||
* window.resumePollingAfterUpload / window.getBaseUrl / window.showToast / window.addLog
|
||||
@@ -17,28 +21,22 @@
|
||||
import { showToast } from '../common/toast.js';
|
||||
import { addLog } from '../common/log.js';
|
||||
import {
|
||||
otaSessionTransition, otaSessionActive, OtaSessionState
|
||||
otaSessionTransition, OtaSessionState
|
||||
} from './session.js';
|
||||
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;
|
||||
/** 中继期 status 轮询间隔(ms);结束后由 stopRelayProgressWatch 恢复 1000 */
|
||||
const RELAY_STATUS_POLL_MS = 400;
|
||||
const NORMAL_STATUS_POLL_MS = 1000;
|
||||
|
||||
window.gd32CompleteAnnounced = false;
|
||||
window.relayWatchLastSeq = -1;
|
||||
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
|
||||
window.gd32RelaySessionLive = false;
|
||||
|
||||
/**
|
||||
* 包进度变更时更新进度条并写一条「转发」日志(同 seq 不重复)。
|
||||
* 包进度变更时更新进度条(同 seq 不重复)。
|
||||
* 由 stages.js 在 status 轮询路径调用;勿在本文件再起独立定时器刷进度。
|
||||
* @param {number} rseq
|
||||
* @param {number} rtot
|
||||
* @param {{force100?: boolean}} [opts]
|
||||
@@ -79,7 +77,7 @@ export function ensureRelayProgressComplete() {
|
||||
}
|
||||
|
||||
export function markGd32OtaUiComplete(msg) {
|
||||
// 已公告过:禁止再改 className / setProgress / toast,否则定时器重触发动效会「疯狂跳 ✓」
|
||||
// 已公告过:禁止再改 className / setProgress / toast,否则 status 重触发动效会「疯狂跳 ✓」
|
||||
if (window.gd32CompleteAnnounced) {
|
||||
return;
|
||||
}
|
||||
@@ -111,106 +109,33 @@ export function markGd32OtaUiComplete(msg) {
|
||||
showToast(msg || 'GD32 刷写完成', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束中继加速轮询,恢复常态 1s status 间隔。
|
||||
* 不再持有独立 setInterval(进度/完成/失败统一由 stages 在 pollStatus 路径处理)。
|
||||
*/
|
||||
export function stopRelayProgressWatch() {
|
||||
if (relayWatchTimerId) {
|
||||
clearInterval(relayWatchTimerId);
|
||||
relayWatchTimerId = null;
|
||||
}
|
||||
/* 中继结束:恢复常态 1s status 轮询 */
|
||||
if (typeof window.setStatusPollIntervalMs === 'function') {
|
||||
window.setStatusPollIntervalMs(1000);
|
||||
window.setStatusPollIntervalMs(NORMAL_STATUS_POLL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中继进度观察:只读 pollStatus 写入的 latestSystemStatus,不再另起 /system/status。
|
||||
* 中继期 status 加速到 400ms,使「转发 N/M」更贴近设备真实进度。
|
||||
* 中继进度跟踪:只加速 /system/status 轮询,不再另起 400ms 定时器。
|
||||
* UI 真相源仍是 pollStatus → updateSystemStatusUi(stages.js)。
|
||||
*/
|
||||
export function startRelayProgressWatch() {
|
||||
stopRelayProgressWatch();
|
||||
if (window.pollingPausedForUpload) {
|
||||
window.resumePollingAfterUpload();
|
||||
}
|
||||
/* 中继期加速 status,缩小 UI 进度相对设备日志的滞后 */
|
||||
if (typeof window.setStatusPollIntervalMs === 'function') {
|
||||
window.setStatusPollIntervalMs(400);
|
||||
window.setStatusPollIntervalMs(RELAY_STATUS_POLL_MS);
|
||||
}
|
||||
if (typeof window.scheduleStatusPoll === 'function') {
|
||||
window.scheduleStatusPoll(0);
|
||||
}
|
||||
if (typeof window.pollLogs === 'function') {
|
||||
window.pollLogs();
|
||||
}
|
||||
window.scheduleStatusPoll(0);
|
||||
window.pollLogs();
|
||||
addLog('I', 'OTA', '跟踪中继进度:看进度条与 relay_seq(/system/status),勿只看日志');
|
||||
relayWatchTimerId = setInterval(function () {
|
||||
var active = window.otaSessionState === OtaSessionState.RELAYING ||
|
||||
(window.otaSessionState === OtaSessionState.FAILED &&
|
||||
(window.lastSysStateCode === 3 || window.lastRelayState >= 1 && window.lastRelayState <= 4));
|
||||
if (!active) {
|
||||
if (window.otaSessionState === OtaSessionState.DONE) {
|
||||
stopRelayProgressWatch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
/* 中继失败:必须把前端会话从 RELAYING 拉到 FAILED,否则无法新开上传 */
|
||||
if (code === 8 || rstate === 255) {
|
||||
stopRelayProgressWatch();
|
||||
window.pollLogs();
|
||||
if (window.otaSessionState === OtaSessionState.RELAYING ||
|
||||
window.otaSessionState === OtaSessionState.UPLOADING) {
|
||||
var failMsg = 'GD32 中继失败(relay_state=' + rstate +
|
||||
', sys=' + (o.sys_state || code) + ')';
|
||||
if (typeof window.cleanupAfterOtaFailure === 'function') {
|
||||
window.cleanupAfterOtaFailure(failMsg);
|
||||
} else {
|
||||
otaSessionTransition(OtaSessionState.FAILED, failMsg);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* ERROR 可能在 status 路径被软清为 IDLE,前端收不到 code=8。
|
||||
* 本会话已见过真实中继,设备却已回 IDLE 且未宣布完成 → 按失败清理。
|
||||
*/
|
||||
var idleAfterRelay = (code === 0 || code === 2) &&
|
||||
(rstate === 0 || rstate === 5) &&
|
||||
window.gd32RelaySessionLive &&
|
||||
!window.gd32CompleteAnnounced &&
|
||||
window.otaSessionState === OtaSessionState.RELAYING;
|
||||
if (idleAfterRelay) {
|
||||
stopRelayProgressWatch();
|
||||
window.pollLogs();
|
||||
if (typeof window.cleanupAfterOtaFailure === 'function') {
|
||||
window.cleanupAfterOtaFailure('GD32 中继已结束但未成功(设备已回 IDLE)');
|
||||
} else {
|
||||
otaSessionTransition(OtaSessionState.FAILED, 'GD32 中继已结束但未成功');
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/** @description 触发 GD32 中继 OTA 流程(通过 HTTP 接口)。 */
|
||||
@@ -222,8 +147,7 @@ export async function triggerGd32Relay() {
|
||||
window.cleanupAfterOtaFailure('SHA-256 缺失');
|
||||
return;
|
||||
}
|
||||
addLog('I', 'OTA', 'GD32 SHA-256: ' + sha256 + ' → 触发中继...');
|
||||
showToast('正在触发 GD32 OTA', 'success');
|
||||
addLog('I', 'OTA', '触发 GD32 中继…');
|
||||
setProgress(48, '校验 SHA 并触发 GD32 中继…');
|
||||
try {
|
||||
// 仅触发中继(设备端异步跑),不阻塞到刷写结束;SHA 读分区可能数秒
|
||||
@@ -235,11 +159,7 @@ export async function triggerGd32Relay() {
|
||||
});
|
||||
var text = await resp.text();
|
||||
if (resp.ok) {
|
||||
try {
|
||||
var data = JSON.parse(text);
|
||||
if (data.sha256) addLog('I', 'OTA', '设备端 SHA-256: ' + data.sha256);
|
||||
} catch (_) {}
|
||||
addLog('I', 'OTA', 'GD32 OTA 已触发;进度看状态栏包数(勿只看日志时间戳)');
|
||||
addLog('I', 'OTA', 'GD32 OTA 已触发;进度看状态栏包数');
|
||||
showToast('GD32 OTA 已触发', 'success');
|
||||
setProgress(50, '中继已启动,同步进度…');
|
||||
window.gd32CompleteAnnounced = false;
|
||||
@@ -250,7 +170,8 @@ export async function triggerGd32Relay() {
|
||||
try {
|
||||
var err = JSON.parse(text);
|
||||
if (err.device) {
|
||||
addLog('E', 'OTA', 'GD32 SHA-256 不匹配: 设备=' + err.device + ' 前端=' + sha256);
|
||||
addLog('E', 'OTA', 'GD32 SHA-256 不匹配: 设备=' + err.device +
|
||||
' 前端=' + sha256);
|
||||
showToast('SHA-256 不匹配', 'error');
|
||||
window.cleanupAfterOtaFailure('SHA-256 不匹配');
|
||||
return;
|
||||
@@ -287,4 +208,4 @@ export async function triggerGd32Relay() {
|
||||
showToast('GD32 触发失败', 'error');
|
||||
window.cleanupAfterOtaFailure('触发异常');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user