refactor(debug): 重构调试日志环形缓冲区管理
- 将日志环形缓冲区从静态数组改为动态堆分配
- 实现启动时大容量缓冲区和运行时小容量缓冲区的切换机制
- 添加日志环满时优先保留错误和警告信息的清理策略
- 优化内存使用,在日志为空时避免不必要的内存分配
- 移除调试日志中的冗余字段显示
feat(api): 更新CAN接口为批量发送模式
- 将 `/can/send` 接口替换为 `/can/send_batch`,支持1到N帧的批量发送
- 更新API文档中的接口描述和使用示例
- 修改请求参数格式为 `{"ch":0,"frames":[{...}]}` 返回格式为 `{"status":"ok","results":[...]}`
- 单帧发送也需要通过send_batch接口实现
feat(monitor): 新增GD32 ADC调试反馈处理功能
- 添加gd32_adc_feedback组件用于解析0xC4类型的SPI反馈
- 实现ADC数据解析包括温度、电压等参数的计算和转换
- 在can_feedback_task中集成ADC反馈分发逻辑
- 添加线程安全的数据获取和年龄检查接口
refactor(config): 优化任务栈大小配置并调整OTA块大小
- 重新评估各任务栈使用情况,优化HTTP、WiFi、SPI等工作任务的栈大小
- 将HTTP服务器栈从8KB调整至7KB,WiFi OTA任务栈从4KB调整至3KB
- OTA块大小从2KB调整回4KB以提升传输吞吐性能
- 重新整理配置文件结构,移动WiFi模式配置位置
This commit is contained in:
+111
-71
@@ -94,33 +94,41 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
window.flashTarget = target === 'gd32' ? 'gd32' : 'esp32';
|
||||
persistFlashTarget(window.flashTarget);
|
||||
applySharedModeUi();
|
||||
syncBleFileFromSelectedTarget();
|
||||
/* 与 WiFi 一致:两路文件独立;切换只换「当前目标」面板 + 绑定该路文件到会话 */
|
||||
if (window.connectionMode === 'ble') {
|
||||
window.switchStageFlow('ble');
|
||||
if (typeof setActivePhase === 'function') {
|
||||
setActivePhase(target === 'gd32' ? 'gd32Ota' : 'esp32Ota');
|
||||
setActivePhase(window.flashTarget === 'gd32' ? 'gd32Ota' : 'esp32Ota');
|
||||
}
|
||||
if (window.bleOta && typeof window.bleOta.refreshTargetUi === 'function') {
|
||||
window.bleOta.refreshTargetUi();
|
||||
}
|
||||
/* 把当前目标对应的 esp32/gd32 固件绑到 BLE 会话(无文件则清空,避免误刷另一芯片) */
|
||||
syncBleFileFromSelectedTarget();
|
||||
} else {
|
||||
window.switchStageFlow(window.flashTarget);
|
||||
}
|
||||
if (window.bleOta && typeof window.bleOta.refreshTargetUi === 'function') {
|
||||
window.bleOta.refreshTargetUi();
|
||||
}
|
||||
};
|
||||
|
||||
window.handleSharedFileSelect = function (event, chip) {
|
||||
var file = event && event.target && event.target.files && event.target.files[0] ? event.target.files[0] : null;
|
||||
if (file) {
|
||||
/* 传统 input 无持久 handle,刷新后无法自动恢复该次选择 */
|
||||
window.sharedFirmwareFiles[chip] = { file: file, handle: null };
|
||||
}
|
||||
if (typeof window.handleFileSelect === 'function') {
|
||||
window.handleFileSelect(event, chip);
|
||||
}
|
||||
window.flashTarget = chip === 'gd32' ? 'gd32' : 'esp32';
|
||||
persistFlashTarget(window.flashTarget);
|
||||
applySharedModeUi();
|
||||
setTimeout(syncBleFileFromSelectedTarget, 0);
|
||||
/* 等 processFile(含 HEX→BIN)完成后再写入 shared 并 sync */
|
||||
var done = (typeof window.handleFileSelect === 'function')
|
||||
? Promise.resolve(window.handleFileSelect(event, chip))
|
||||
: Promise.resolve();
|
||||
done.then(function () {
|
||||
var prepared = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||||
if (prepared) {
|
||||
window.sharedFirmwareFiles[chip] = { file: prepared, handle: null };
|
||||
}
|
||||
syncBleFileFromSelectedTarget();
|
||||
}).catch(function (err) {
|
||||
if (typeof window.addLog === 'function') {
|
||||
window.addLog('W', '文件', chip.toUpperCase() + ' 选择失败: ' + (err && err.message ? err.message : err));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.pickSharedFirmware = function (chip) {
|
||||
@@ -137,22 +145,25 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
});
|
||||
};
|
||||
Promise.resolve(window.pickFileViaFSAPI(chip)).then(function () {
|
||||
window.showOpenFilePicker = nativePicker;
|
||||
if (!capturedHandle || typeof capturedHandle.getFile !== 'function') {
|
||||
syncBleFileFromSelectedTarget();
|
||||
return;
|
||||
/* 用 processFile 后的结果(GD32 HEX→BIN 后的 prepared),不要再塞回原始 .hex */
|
||||
var prepared = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||||
var handle = (chip === 'gd32' ? window.gd32FileHandle : window.esp32FileHandle) || capturedHandle;
|
||||
if (prepared) {
|
||||
window.sharedFirmwareFiles[chip] = { file: prepared, handle: handle || null };
|
||||
} else if (capturedHandle) {
|
||||
window.sharedFirmwareFiles[chip] = {
|
||||
file: window.sharedFirmwareFiles[chip] && window.sharedFirmwareFiles[chip].file,
|
||||
handle: capturedHandle
|
||||
};
|
||||
}
|
||||
capturedHandle.getFile().then(function (file) {
|
||||
window.sharedFirmwareFiles[chip] = { file: file, handle: capturedHandle };
|
||||
syncBleFileFromSelectedTarget();
|
||||
}).catch(function () {
|
||||
syncBleFileFromSelectedTarget();
|
||||
});
|
||||
syncBleFileFromSelectedTarget();
|
||||
}).catch(function (err) {
|
||||
window.showOpenFilePicker = nativePicker;
|
||||
if (err && err.name !== 'AbortError' && typeof window.addLog === 'function') {
|
||||
window.addLog('W', '文件', 'FSAPI 选择失败: ' + err.message);
|
||||
window.addLog('W', '文件', 'FSAPI 选择失败: ' + (err.message || err));
|
||||
}
|
||||
}).then(function () {
|
||||
/* 无论成败都还原原生 picker,避免二次选择失效 */
|
||||
window.showOpenFilePicker = nativePicker;
|
||||
});
|
||||
} else {
|
||||
var input = document.getElementById(chip === 'gd32' ? 'fileInputGd32' : 'fileInputEsp32');
|
||||
@@ -184,60 +195,83 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 按 flashTarget 把「该芯片一路」固件绑到 BLE 会话(复刻 WiFi:两路独立、切换不串文件)。
|
||||
* 优先 window.esp32File/gd32File(含 HEX→BIN 结果),其次 sharedFirmwareFiles / input。
|
||||
* 当前目标无文件时清空 BLE 会话,禁止继续显示/上传另一芯片固件。
|
||||
*/
|
||||
window.syncBleFileFromSelectedTarget = function () {
|
||||
if (window.connectionMode !== 'ble' || !window.bleOta || typeof window.bleOta.notifyFileChange !== 'function') {
|
||||
if (window.connectionMode !== 'ble' || !window.bleOta) {
|
||||
return;
|
||||
}
|
||||
var isGd32 = window.flashTarget === 'gd32';
|
||||
var chip = isGd32 ? 'gd32' : 'esp32';
|
||||
var suffix = isGd32 ? 'Gd32' : 'Esp32';
|
||||
var stored = window.sharedFirmwareFiles[chip];
|
||||
if (stored && stored.handle && typeof stored.handle.getFile === 'function') {
|
||||
stored.handle.getFile().then(function (file) {
|
||||
stored.file = file;
|
||||
setBleFirmwareInputFile(file);
|
||||
window.bleOta.notifyFileChange(file, stored.handle);
|
||||
}).catch(function () {
|
||||
if (stored.file) {
|
||||
setBleFirmwareInputFile(stored.file);
|
||||
window.bleOta.notifyFileChange(stored.file, stored.handle);
|
||||
if (window.bleOta.isUploading && window.bleOta.isUploading()) {
|
||||
return;
|
||||
}
|
||||
var chip = window.flashTarget === 'gd32' ? 'gd32' : 'esp32';
|
||||
var file = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||||
var handle = chip === 'gd32' ? window.gd32FileHandle : window.esp32FileHandle;
|
||||
var stored = window.sharedFirmwareFiles && window.sharedFirmwareFiles[chip]
|
||||
? window.sharedFirmwareFiles[chip]
|
||||
: null;
|
||||
if (!handle && stored && stored.handle) handle = stored.handle;
|
||||
if (!file && stored && stored.file) file = stored.file;
|
||||
|
||||
function bindBle(f, h) {
|
||||
setBleFirmwareInputFile(f);
|
||||
if (typeof window.bleOta.notifyFileChange === 'function') {
|
||||
window.bleOta.notifyFileChange(f, h || null);
|
||||
}
|
||||
/* 会话 SHA 与对应芯片槽对齐(不跨芯片) */
|
||||
var slot = chip === 'gd32' ? window._gd32FileSha256 : window._esp32FileSha256;
|
||||
if (window.bleOta.state) {
|
||||
if (slot) window.bleOta.state.fileSha256 = slot;
|
||||
}
|
||||
}
|
||||
function clearBleSession() {
|
||||
if (typeof window.bleOta.clearFile === 'function') {
|
||||
window.bleOta.clearFile();
|
||||
} else if (typeof window.bleOta.notifyFileChange === 'function') {
|
||||
window.bleOta.notifyFileChange(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (file) {
|
||||
bindBle(file, handle);
|
||||
return;
|
||||
}
|
||||
if (handle && typeof handle.getFile === 'function') {
|
||||
handle.getFile().then(function (fresh) {
|
||||
if (chip === 'gd32' && fresh.name.toLowerCase().endsWith('.hex') &&
|
||||
typeof window.processFile === 'function') {
|
||||
return Promise.resolve(window.processFile(fresh, 'gd32', {
|
||||
silent: true,
|
||||
skipHandlePersist: true
|
||||
})).then(function () {
|
||||
if (window.gd32File) bindBle(window.gd32File, handle);
|
||||
else bindBle(fresh, handle);
|
||||
});
|
||||
}
|
||||
if (chip === 'esp32') window.esp32File = fresh;
|
||||
else window.gd32File = fresh;
|
||||
if (stored) stored.file = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||||
bindBle(chip === 'gd32' ? window.gd32File : window.esp32File, handle);
|
||||
}).catch(function () {
|
||||
if (stored && stored.file) bindBle(stored.file, handle);
|
||||
else clearBleSession();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (stored && stored.file) {
|
||||
setBleFirmwareInputFile(stored.file);
|
||||
window.bleOta.notifyFileChange(stored.file, stored.handle || null);
|
||||
return;
|
||||
}
|
||||
var suffix = chip === 'gd32' ? 'Gd32' : 'Esp32';
|
||||
var input = document.getElementById('fileInput' + suffix);
|
||||
var info = document.getElementById('fileInfo' + suffix);
|
||||
var hasInfo = info && info.classList.contains('show');
|
||||
var file = input && input.files && input.files[0] ? input.files[0] : null;
|
||||
if (file) {
|
||||
window.sharedFirmwareFiles[chip] = { file: file, handle: null };
|
||||
setBleFirmwareInputFile(file);
|
||||
window.bleOta.notifyFileChange(file, null);
|
||||
var inputFile = input && input.files && input.files[0] ? input.files[0] : null;
|
||||
if (inputFile) {
|
||||
if (window.sharedFirmwareFiles) {
|
||||
window.sharedFirmwareFiles[chip] = { file: inputFile, handle: null };
|
||||
}
|
||||
bindBle(inputFile, null);
|
||||
return;
|
||||
}
|
||||
if (!hasInfo) {
|
||||
return;
|
||||
}
|
||||
var nameEl = document.getElementById('fileName' + suffix);
|
||||
var typeEl = document.getElementById('fileType' + suffix);
|
||||
var sizeEl = document.getElementById('fileSize' + suffix);
|
||||
var bleInfo = document.getElementById('fileInfo');
|
||||
var bleName = document.getElementById('fileName');
|
||||
var bleType = document.getElementById('fileType');
|
||||
var bleSize = document.getElementById('fileSize');
|
||||
if (bleInfo) bleInfo.classList.add('show');
|
||||
if (bleName && nameEl) bleName.textContent = nameEl.textContent;
|
||||
if (bleType && typeEl) bleType.textContent = typeEl.textContent;
|
||||
if (bleSize && sizeEl) bleSize.textContent = sizeEl.textContent;
|
||||
var packetCount = document.getElementById('packetCount');
|
||||
var sentCount = document.getElementById('sentCount');
|
||||
if (packetCount) packetCount.textContent = '-';
|
||||
if (sentCount) sentCount.textContent = '0';
|
||||
clearBleSession();
|
||||
};
|
||||
|
||||
window.applySharedModeUi = function () {
|
||||
@@ -260,9 +294,15 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
if (el) el.style.display = ids[id] ? '' : 'none';
|
||||
});
|
||||
var hint = document.getElementById('readyHint');
|
||||
var autoBtn = document.getElementById('autoFlashBtn');
|
||||
if (autoBtn && isBle) {
|
||||
autoBtn.textContent = isGd32 ? '一键刷写 GD32' : '一键刷写 ESP32';
|
||||
}
|
||||
if (hint) {
|
||||
if (isBle) {
|
||||
hint.innerHTML = 'BLE 模式:连接设备 → 选择固件 → 一键刷写。';
|
||||
hint.innerHTML = isGd32
|
||||
? 'BLE 模式 · 目标 GD32:连接设备 → 选择 GD32 固件 → 一键刷写。'
|
||||
: 'BLE 模式 · 目标 ESP32:连接设备 → 选择 ESP32 固件 → 一键刷写。';
|
||||
} else if (isGd32) {
|
||||
hint.innerHTML = 'WiFi 模式:上传 GD32 固件后自动执行 SPI OTA。';
|
||||
} else {
|
||||
|
||||
+111
-103
@@ -20,6 +20,9 @@ window.esp32File = null;
|
||||
window.gd32File = null;
|
||||
window.esp32FileHandle = null; // FileSystemFileHandle for hot reload
|
||||
window.gd32FileHandle = null; // FileSystemFileHandle for hot reload
|
||||
/** 两路各自一份 SHA:选文件时写入,上传时读取;互不切换、不共享 */
|
||||
window._esp32FileSha256 = null;
|
||||
window._gd32FileSha256 = null;
|
||||
|
||||
/** localStorage:升级目标等轻量偏好(刷新后恢复) */
|
||||
var LS_FLASH_TARGET = 'dcu_ota_flash_target';
|
||||
@@ -196,7 +199,7 @@ export async function resumePersistedFirmwareWithGesture(chip) {
|
||||
return false;
|
||||
}
|
||||
var file = await handle.getFile();
|
||||
applyRestoredFile(chip, handle, file, { silent: true });
|
||||
await applyRestoredFile(chip, handle, file, { silent: true });
|
||||
window._pendingFwHandles[chip] = null;
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已重新授权并恢复: ' + file.name);
|
||||
showToast(chip.toUpperCase() + ' 已恢复上次固件', 'success');
|
||||
@@ -218,11 +221,14 @@ function applyRestoredFile(chip, handle, file, options) {
|
||||
if (window.sharedFirmwareFiles) {
|
||||
window.sharedFirmwareFiles[chip] = { file: file, handle: handle };
|
||||
}
|
||||
processFile(file, chip, Object.assign({ skipHandlePersist: true }, options || {}));
|
||||
if (typeof window.syncBleFileFromSelectedTarget === 'function') {
|
||||
window.syncBleFileFromSelectedTarget();
|
||||
}
|
||||
safeUpdateUploadButtonLabel();
|
||||
/* 等 SHA 写入对应槽后再 sync BLE,避免二次重算 */
|
||||
return Promise.resolve(processFile(file, chip, Object.assign({ skipHandlePersist: true }, options || {})))
|
||||
.then(function () {
|
||||
if (typeof window.syncBleFileFromSelectedTarget === 'function') {
|
||||
window.syncBleFileFromSelectedTarget();
|
||||
}
|
||||
safeUpdateUploadButtonLabel();
|
||||
});
|
||||
}
|
||||
|
||||
/** 无权限时仍展示上次文件名,提示点上传区一键恢复 */
|
||||
@@ -269,7 +275,7 @@ export async function restorePersistedFirmwareSelection() {
|
||||
var perm = await queryHandlePermission(handle);
|
||||
if (perm === 'granted') {
|
||||
var file = await handle.getFile();
|
||||
applyRestoredFile(chip, handle, file, { silent: true, skipHandlePersist: true });
|
||||
await applyRestoredFile(chip, handle, file, { silent: true, skipHandlePersist: true });
|
||||
window._pendingFwHandles[chip] = null;
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已恢复刷新前的固件: ' + file.name);
|
||||
continue;
|
||||
@@ -296,13 +302,13 @@ export async function restorePersistedFirmwareSelection() {
|
||||
|
||||
export function handleFileSelect(event, chip) {
|
||||
var file = event.target.files[0];
|
||||
if (!file) return;
|
||||
if (!file) return Promise.resolve();
|
||||
/* 传统 input 无 handle:清掉旧 FSAPI 句柄,避免热重载读到过期路径 */
|
||||
if (chip === 'esp32') window.esp32FileHandle = null;
|
||||
else window.gd32FileHandle = null;
|
||||
window._pendingFwHandles[chip] = null;
|
||||
persistFileHandle(chip, null);
|
||||
processFile(file, chip);
|
||||
return processFile(file, chip);
|
||||
}
|
||||
|
||||
// 通过 File System Access API 选择文件(支持热重载)
|
||||
@@ -325,9 +331,14 @@ export async function pickFileViaFSAPI(chip) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
/* GD32 常为 .hex:同时声明 octet-stream / text,避免对话框里灰掉选不了 */
|
||||
var acceptMap = chip === 'esp32'
|
||||
? { 'application/octet-stream': ['.bin'] }
|
||||
: { 'application/octet-stream': ['.bin', '.hex'] };
|
||||
: {
|
||||
'application/octet-stream': ['.bin', '.hex'],
|
||||
'text/plain': ['.hex'],
|
||||
'application/x-intel-hex': ['.hex']
|
||||
};
|
||||
var prevHandle = chip === 'esp32' ? window.esp32FileHandle : window.gd32FileHandle;
|
||||
if (!prevHandle) {
|
||||
prevHandle = window._pendingFwHandles[chip] || null;
|
||||
@@ -341,7 +352,18 @@ export async function pickFileViaFSAPI(chip) {
|
||||
if (prevHandle) {
|
||||
opts.startIn = prevHandle;
|
||||
}
|
||||
var handles = await window.showOpenFilePicker(opts);
|
||||
var handles;
|
||||
try {
|
||||
handles = await window.showOpenFilePicker(opts);
|
||||
} catch (pickErr) {
|
||||
/* 失效 handle 作 startIn 时部分浏览器直接抛错,去掉后重试 */
|
||||
if (opts.startIn && pickErr && pickErr.name !== 'AbortError') {
|
||||
delete opts.startIn;
|
||||
handles = await window.showOpenFilePicker(opts);
|
||||
} else {
|
||||
throw pickErr;
|
||||
}
|
||||
}
|
||||
if (handles && handles.length > 0) {
|
||||
var handle = handles[0];
|
||||
var file = await handle.getFile();
|
||||
@@ -356,8 +378,8 @@ export async function pickFileViaFSAPI(chip) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') {
|
||||
addLog('W', '文件', 'FSAPI 选择失败: ' + e.message);
|
||||
if (e && e.name !== 'AbortError') {
|
||||
addLog('W', '文件', 'FSAPI 选择失败: ' + (e.message || e));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -484,81 +506,58 @@ 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 信号)。
|
||||
// ==================== SHA-256:ESP32 / GD32 各一份,互不共用、不互切 ====================
|
||||
function paintShaUi(chip, hashHex, statusText) {
|
||||
var t = statusText != null ? statusText : (hashHex || '-');
|
||||
if (chip === 'esp32') {
|
||||
var e = document.getElementById('fileSha256Esp32');
|
||||
if (e) e.textContent = t;
|
||||
} else if (chip === 'gd32') {
|
||||
var g = document.getElementById('fileSha256Gd32');
|
||||
if (g) g.textContent = t;
|
||||
}
|
||||
/* BLE 面板只显示当前目标槽,避免算 ESP32 时盖掉 GD32 的展示 */
|
||||
var cur = (window.flashTarget === 'gd32') ? 'gd32' : 'esp32';
|
||||
if (chip === cur) {
|
||||
var ble = document.getElementById('fileSha256');
|
||||
if (ble) ble.textContent = t;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = '计算中...';
|
||||
export function computeFileSha(file, chip, optArrayBuffer) {
|
||||
if (!file || (chip !== 'esp32' && chip !== 'gd32')) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
paintShaUi(chip, null, '计算中...');
|
||||
var bufPromise = optArrayBuffer ? Promise.resolve(optArrayBuffer) : file.arrayBuffer();
|
||||
return bufPromise.then(function(buf) {
|
||||
return bufPromise.then(function (buf) {
|
||||
return crypto.subtle.digest('SHA-256', buf);
|
||||
}).then(function(hashBuffer) {
|
||||
}).then(function (hashBuffer) {
|
||||
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') {
|
||||
window._esp32FileSha256 = hashHex;
|
||||
window._esp32FileShaMeta = meta;
|
||||
} else {
|
||||
window._gd32FileSha256 = hashHex;
|
||||
window._gd32FileShaMeta = meta;
|
||||
}
|
||||
if (chip === 'esp32') window._esp32FileSha256 = hashHex;
|
||||
else window._gd32FileSha256 = hashHex;
|
||||
paintShaUi(chip, hashHex);
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 固件 SHA-256: ' + hashHex);
|
||||
return hashHex;
|
||||
}).catch(function(err) {
|
||||
if (shaElem) shaElem.textContent = '计算失败: ' + err.message;
|
||||
}).catch(function (err) {
|
||||
paintShaUi(chip, null, '计算失败: ' + err.message);
|
||||
if (chip === 'esp32') window._esp32FileSha256 = null;
|
||||
else window._gd32FileSha256 = null;
|
||||
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 计算、文件校验)。
|
||||
* 对 GD32 .hex 走异步转换;返回 Promise 以便调用方 await
|
||||
* (避免上游用 setTimeout 硬等,见 upload-esp32.js)。
|
||||
* @description 处理用户选择的固件文件(HEX→BIN、算 SHA 写入对应芯片槽)。
|
||||
* @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) {
|
||||
@@ -569,52 +568,61 @@ export function processFile(file, chip, options) {
|
||||
if (!opts.skipHandlePersist) {
|
||||
persistFlashTarget(chip);
|
||||
}
|
||||
/* 元数据始终更新,便于刷新后展示「上次文件名」 */
|
||||
if (file && file.name) {
|
||||
persistFileMeta(chip, file);
|
||||
}
|
||||
|
||||
function paintFileInfo(name, type, sizeText) {
|
||||
var info = document.getElementById('fileInfo' + suffix);
|
||||
var nameEl = document.getElementById('fileName' + suffix);
|
||||
var typeEl = document.getElementById('fileType' + suffix);
|
||||
var sizeEl = document.getElementById('fileSize' + suffix);
|
||||
if (info) info.classList.add('show');
|
||||
if (nameEl) nameEl.textContent = name;
|
||||
if (typeEl) typeEl.textContent = type;
|
||||
if (sizeEl) sizeEl.textContent = sizeText;
|
||||
}
|
||||
|
||||
if (chip === 'gd32' && isHexByName) {
|
||||
// GD32 固件: .hex 文件自动转换为 .bin,然后替换文件对象
|
||||
return convertHexFileToBin(file).then(function(result) {
|
||||
return convertHexFileToBin(file).then(function (result) {
|
||||
if (!result.converted) {
|
||||
// 文件名 .hex 但内容不像,直接用原始文件
|
||||
addLog('W', '文件', file.name + ' 扩展名为 .hex 但内容不像 HEX 格式,按原始文件处理');
|
||||
window.gd32File = file;
|
||||
document.getElementById('fileInfo' + suffix).classList.add('show');
|
||||
document.getElementById('fileName' + suffix).textContent = file.name;
|
||||
document.getElementById('fileType' + suffix).textContent = 'Binary(?)';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
||||
paintFileInfo(file.name, 'Binary(?)', formatBytes(file.size));
|
||||
safeUpdateUploadButtonLabel();
|
||||
return ensureFileShaCached(file, 'gd32');
|
||||
return computeFileSha(file, 'gd32');
|
||||
}
|
||||
window.gd32File = result.file;
|
||||
var baseHex = '0x' + result.baseAddr.toString(16).toUpperCase().padStart(8, '0');
|
||||
addLog('I', '文件', 'HEX → BIN 转换完成: ' + file.name + ' → ' + result.file.name);
|
||||
addLog('I', '文件', '基地址: ' + baseHex + ', 数据大小: ' + formatBytes(result.file.size));
|
||||
if (!silent) showToast('HEX 已自动转换为 BIN', 'success');
|
||||
document.getElementById('fileInfo' + suffix).classList.add('show');
|
||||
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);
|
||||
paintFileInfo(
|
||||
result.file.name + ' (由 ' + file.name + ' 转换)',
|
||||
'Binary (HEX转换)',
|
||||
formatBytes(result.file.size)
|
||||
);
|
||||
safeUpdateUploadButtonLabel();
|
||||
return ensureFileShaCached(result.file, 'gd32');
|
||||
}).catch(function(err) {
|
||||
addLog('E', '文件', 'HEX 解析失败: ' + err.message);
|
||||
if (!silent) showToast('HEX 文件解析失败: ' + err.message, 'error');
|
||||
return computeFileSha(result.file, 'gd32');
|
||||
}).catch(function (err) {
|
||||
addLog('E', '文件', 'HEX 解析失败: ' + (err && err.message ? err.message : err));
|
||||
if (!silent) showToast('HEX 文件解析失败: ' + (err && err.message ? err.message : err), 'error');
|
||||
window.gd32File = null;
|
||||
document.getElementById('fileInfo' + suffix).classList.remove('show');
|
||||
window._gd32FileSha256 = null;
|
||||
var info = document.getElementById('fileInfo' + suffix);
|
||||
if (info) info.classList.remove('show');
|
||||
safeUpdateUploadButtonLabel();
|
||||
throw err; // 让上游 await 感知失败
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
// .bin 文件或 ESP32 文件: 直接使用
|
||||
|
||||
if (chip === 'esp32') window.esp32File = file;
|
||||
else window.gd32File = file;
|
||||
document.getElementById('fileInfo' + suffix).classList.add('show');
|
||||
document.getElementById('fileName' + suffix).textContent = file.name;
|
||||
document.getElementById('fileType' + suffix).textContent =
|
||||
isHexByName ? 'Intel HEX' : 'Binary';
|
||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
||||
paintFileInfo(
|
||||
file.name,
|
||||
isHexByName ? 'Intel HEX' : 'Binary',
|
||||
formatBytes(file.size)
|
||||
);
|
||||
safeUpdateUploadButtonLabel();
|
||||
if (silent) {
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已恢复: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
||||
@@ -622,20 +630,20 @@ export function processFile(file, chip, options) {
|
||||
addLog('I', '文件', chip.toUpperCase() + ' 已选择: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
||||
showToast(chip.toUpperCase() + ' 文件已选择', 'success');
|
||||
}
|
||||
// 选文件时就把 SHA-256 算好并缓存(ESP32/GD32 都算),
|
||||
// 上传时若文件未变 ensureFileShaCached 命中直接复用。
|
||||
return Promise.resolve(ensureFileShaCached(file, chip));
|
||||
return Promise.resolve(computeFileSha(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; window._esp32FileShaMeta = null;
|
||||
window.esp32File = null;
|
||||
window.esp32FileHandle = null;
|
||||
window._esp32FileSha256 = null;
|
||||
} else {
|
||||
window.gd32File = null; window.gd32FileHandle = null;
|
||||
window._gd32FileSha256 = null; window._gd32FileShaMeta = null;
|
||||
window.gd32File = null;
|
||||
window.gd32FileHandle = null;
|
||||
window._gd32FileSha256 = null;
|
||||
}
|
||||
window._pendingFwHandles[chip] = null;
|
||||
persistFileHandle(chip, null);
|
||||
|
||||
@@ -156,8 +156,7 @@ export function cleanupAfterOtaFailure(reason) {
|
||||
window.clearOtaSessionResiduals({
|
||||
keepErrorStages: true,
|
||||
statusText: reason || '失败',
|
||||
errorStyle: true,
|
||||
clearCacheDisplay: true
|
||||
errorStyle: true
|
||||
});
|
||||
|
||||
// 2. 阶段指示器标错
|
||||
|
||||
+1
-12
@@ -228,16 +228,6 @@ export function clearOtaSessionResiduals(opts) {
|
||||
: '待机';
|
||||
}
|
||||
|
||||
// 设备侧缓存展示:失败后不要继续显示“已满缓存像成功”
|
||||
var mOtaCache = document.getElementById('mOtaCache');
|
||||
if (mOtaCache && opts.clearCacheDisplay) {
|
||||
mOtaCache.textContent = '0 / 216 KB';
|
||||
mOtaCache.className = 'v';
|
||||
}
|
||||
var sOtaStored = document.getElementById('sOtaStored');
|
||||
if (sOtaStored && opts.clearCacheDisplay) {
|
||||
sOtaStored.textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 将会话从 FAILED/DONE 复位到 IDLE(用户发起新上传或设备空闲时调用)。 */
|
||||
@@ -245,8 +235,7 @@ export function otaSessionReset() {
|
||||
clearOtaSessionResiduals({
|
||||
keepErrorStages: false,
|
||||
statusText: '待机',
|
||||
errorStyle: false,
|
||||
clearCacheDisplay: false
|
||||
errorStyle: false
|
||||
});
|
||||
if (otaSessionState !== OtaSessionState.IDLE) {
|
||||
otaSessionState = OtaSessionState.IDLE;
|
||||
|
||||
+85
-29
@@ -19,7 +19,7 @@
|
||||
* window.markGd32OtaUiComplete / window.cleanupAfterOtaFailure / window.otaSessionReset
|
||||
* window.stopRelayProgressWatch / window.pollLogs / window.emitRelayProgress
|
||||
*/
|
||||
import { formatBytes, formatUptime } from '../common/utils.js';
|
||||
import { formatUptime } from '../common/utils.js';
|
||||
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi } from '../common/connection.js';
|
||||
import { setActivePhase, setProgress } from './progress.js';
|
||||
import { otaSessionReset, OtaSessionState } from './session.js';
|
||||
@@ -107,7 +107,7 @@ function setDisplayedRelayState(rs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 常驻仪表盘:堆、连接、健康、版本、温湿度、IP/RSSI、OTA cache。
|
||||
* 常驻仪表盘:堆、连接、健康、版本、温度、IP/RSSI。
|
||||
* @param {object} data /system/status JSON
|
||||
* @param {object} ota data.ota
|
||||
* @param {object} wifi data.wifi
|
||||
@@ -150,8 +150,87 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
|
||||
var mTempEl = document.getElementById('mTemp');
|
||||
if (mTempEl) {
|
||||
mTempEl.textContent =
|
||||
Number.isFinite(data.temperature) ? data.temperature.toFixed(1) + '\u00B0C' : '--';
|
||||
/* 与固件 app_config:TEMP_WARNING=80 / TEMP_CRITICAL=100 对齐 */
|
||||
if (Number.isFinite(data.temperature)) {
|
||||
var tC = data.temperature;
|
||||
mTempEl.textContent = tC.toFixed(1) + '\u00B0C';
|
||||
if (tC >= 100) {
|
||||
mTempEl.className = 'v err';
|
||||
} else if (tC >= 80) {
|
||||
mTempEl.className = 'v warn';
|
||||
} else {
|
||||
mTempEl.className = 'v ok';
|
||||
}
|
||||
mTempEl.title = 'ESP32 片内温度传感器(警告≥80°C,危险≥100°C)';
|
||||
} else {
|
||||
mTempEl.textContent = '--';
|
||||
mTempEl.className = 'v';
|
||||
mTempEl.title = '温度无效或未采样';
|
||||
}
|
||||
}
|
||||
/* GD32 ADC:enabled 来自 HELLO_ACK 功能位(GD32 编译开关),ESP32 无本地开关 */
|
||||
var ga = data.gd32_adc || null;
|
||||
var feat = data.gd32_features || null;
|
||||
var mGd32TempEl = document.getElementById('mGd32Temp');
|
||||
var mVccEl = document.getElementById('mVcc');
|
||||
var mVddaEl = document.getElementById('mVdda');
|
||||
|
||||
function setMetricUi(el, text, cls, title) {
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.className = cls || 'v';
|
||||
if (title) el.title = title;
|
||||
}
|
||||
|
||||
if (mGd32TempEl || mVccEl || mVddaEl) {
|
||||
if (ga && ga.enabled === false) {
|
||||
var offTitle = 'GD32 CONFIG_SPI_ADC_FEEDBACK_ENABLE=0(HELLO/PING 身份)';
|
||||
setMetricUi(mGd32TempEl, '关闭', 'v', offTitle);
|
||||
setMetricUi(mVccEl, '关闭', 'v', offTitle);
|
||||
setMetricUi(mVddaEl, '关闭', 'v', offTitle);
|
||||
} else if (ga && Number.isFinite(ga.temperature) && ga.fresh) {
|
||||
setMetricUi(mGd32TempEl, ga.temperature.toFixed(1) + '\u00B0C',
|
||||
ga.valid ? 'v ok' : 'v warn',
|
||||
'片内温度 · seq=' + (ga.seq || 0) + ' age=' + (ga.age_ms || 0) + 'ms'
|
||||
+ (ga.valid ? '' : ' (valid=0)'));
|
||||
/* VCC:status 已带 vcc;旧固件仅 vin_adc 时本地 *11(47k/4.7k) */
|
||||
var vcc = Number.isFinite(ga.vcc) ? ga.vcc
|
||||
: (Number.isFinite(ga.vin_adc) ? ga.vin_adc * 11.0 : NaN);
|
||||
if (Number.isFinite(vcc) && vcc > 0.5) {
|
||||
setMetricUi(mVccEl, vcc.toFixed(1) + 'V', ga.valid ? 'v ok' : 'v warn',
|
||||
'输入母线 VCC(PA6 分压反推)· vin_adc='
|
||||
+ (Number.isFinite(ga.vin_adc) ? ga.vin_adc.toFixed(3) + 'V' : '--'));
|
||||
} else {
|
||||
setMetricUi(mVccEl, '--', 'v', '无 VIN 采样');
|
||||
}
|
||||
if (Number.isFinite(ga.vdda) && ga.vdda > 0.5) {
|
||||
setMetricUi(mVddaEl, ga.vdda.toFixed(2) + 'V', ga.valid ? 'v ok' : 'v warn',
|
||||
'MCU 模拟供电 VDDA(片内 VREFINT 反推)');
|
||||
} else {
|
||||
setMetricUi(mVddaEl, '--', 'v', '无 VDDA 采样');
|
||||
}
|
||||
} else if (ga && Number.isFinite(ga.temperature)) {
|
||||
setMetricUi(mGd32TempEl, ga.temperature.toFixed(1) + '\u00B0C (旧)', 'v warn', '超过 5s 未更新');
|
||||
var vccOld = Number.isFinite(ga.vcc) ? ga.vcc
|
||||
: (Number.isFinite(ga.vin_adc) ? ga.vin_adc * 11.0 : NaN);
|
||||
setMetricUi(mVccEl, Number.isFinite(vccOld) ? (vccOld.toFixed(1) + 'V (旧)') : '--',
|
||||
'v warn', '超过 5s 未更新');
|
||||
setMetricUi(mVddaEl,
|
||||
Number.isFinite(ga.vdda) ? (ga.vdda.toFixed(2) + 'V (旧)') : '--',
|
||||
'v warn', '超过 5s 未更新');
|
||||
} else if (ga && ga.enabled === true) {
|
||||
var waitTitle = 'GD32 已开 0xC4,等待首帧';
|
||||
setMetricUi(mGd32TempEl, '等待…', 'v', waitTitle);
|
||||
setMetricUi(mVccEl, '等待…', 'v', waitTitle);
|
||||
setMetricUi(mVddaEl, '等待…', 'v', waitTitle);
|
||||
} else {
|
||||
var idleTitle = feat && feat.known === false
|
||||
? '尚未 HELLO 身份或旧 GD32(无 feat_map)'
|
||||
: '等待 HELLO / 0xC4';
|
||||
setMetricUi(mGd32TempEl, '--', 'v', idleTitle);
|
||||
setMetricUi(mVccEl, '--', 'v', idleTitle);
|
||||
setMetricUi(mVddaEl, '--', 'v', idleTitle);
|
||||
}
|
||||
}
|
||||
var uptimeEl = document.getElementById('pillUptime');
|
||||
if (uptimeEl) uptimeEl.textContent = '\u23F1 ' + formatUptime(data.uptime_sec);
|
||||
@@ -160,18 +239,6 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
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) {
|
||||
if (otaStored > 0) {
|
||||
mOtaCache.textContent = formatBytes(otaStored) + ' / 216 KB';
|
||||
mOtaCache.className = 'v ok';
|
||||
} else {
|
||||
mOtaCache.textContent = '0 / 216 KB';
|
||||
mOtaCache.className = 'v';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,14 +250,11 @@ 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;
|
||||
var isGd32Storing = sysState === 'GD32_STORING' || sysStateCode === 1;
|
||||
var isGd32Relaying = sysState === 'GD32_RELAYING' || sysStateCode === 3;
|
||||
var isGd32Complete = sysState === 'GD32_COMPLETE' || sysStateCode === 4;
|
||||
var isGd32Error = sysState === 'ERROR' || sysStateCode === 8 || rs === 255;
|
||||
|
||||
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;
|
||||
/* 进度以 relay_seq/relay_total 为准;不再用 stored_size 做「OTA 缓存」展示 */
|
||||
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;
|
||||
@@ -321,15 +385,7 @@ function applyGd32OtaSessionUi(ota) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
/* 不再用 stored_size 做「OTA 缓存」展示;上传走 XHR,中继走 relay_seq。 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
startUploadWaitHeartbeat
|
||||
} from './session.js';
|
||||
import { updateEsp32StagesFromPhase, switchStageFlow, setActivePhase, setProgress, markPhaseDone, cleanupAfterOtaFailure } from './progress.js';
|
||||
import { ensureFileShaCached } from './file-handle.js';
|
||||
import { computeFileSha } from './file-handle.js';
|
||||
|
||||
function isOtaBusyError(status, detail) {
|
||||
var text = (detail || '').toLowerCase();
|
||||
@@ -151,40 +151,30 @@ export async function startUpload(chip) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 每次上传前尝试重新从本地读取文件
|
||||
// 热重载:有 FSAPI handle 则重新读盘(本地编译会改同路径固件)
|
||||
var handle = chip === 'esp32' ? window.esp32FileHandle : window.gd32FileHandle;
|
||||
if (handle) {
|
||||
// 有 FileSystemFileHandle,调用 getFile() 获取最新内容
|
||||
try {
|
||||
var freshFile = await handle.getFile();
|
||||
if (chip === 'esp32') {
|
||||
window.esp32File = freshFile;
|
||||
file = freshFile;
|
||||
if (chip === 'gd32' && freshFile.name.toLowerCase().endsWith('.hex')) {
|
||||
/* HEX 必须按最新源文件再转 BIN,不能用选文件时的旧 prepared */
|
||||
await window.processFile(freshFile, 'gd32', { silent: true, skipHandlePersist: true });
|
||||
file = window.gd32File;
|
||||
} else {
|
||||
if (freshFile.name.toLowerCase().endsWith('.hex')) {
|
||||
// processFile 现已返回 Promise,await 即可拿到转换后的 gd32File,
|
||||
// 不再用 setTimeout 硬等(避免 HEX 稍大时拿到 null)。
|
||||
await window.processFile(freshFile, 'gd32');
|
||||
file = window.gd32File;
|
||||
} else {
|
||||
window.gd32File = freshFile;
|
||||
file = freshFile;
|
||||
}
|
||||
if (chip === 'esp32') window.esp32File = freshFile;
|
||||
else window.gd32File = freshFile;
|
||||
file = freshFile;
|
||||
}
|
||||
addLog('I', '文件', '已重新读取本地文件: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
||||
} catch (e) {
|
||||
addLog('W', '文件', '重新读取文件失败: ' + e.message + ',将使用缓存版本');
|
||||
}
|
||||
} else {
|
||||
// 没有 handle,尝试直接读取当前 File 对象
|
||||
try {
|
||||
await file.arrayBuffer();
|
||||
} catch (e) {
|
||||
// File 引用已失效,提示用户重新选择
|
||||
addLog('E', '文件', '本地文件已变更或失效,请重新选择文件');
|
||||
showToast('文件已变更,请重新选择', 'error');
|
||||
window.clearFile(chip);
|
||||
// ★ 状态机:文件失效属于会话失败
|
||||
cleanupAfterOtaFailure('本地文件失效');
|
||||
return;
|
||||
}
|
||||
@@ -209,10 +199,14 @@ export async function startUpload(chip) {
|
||||
showToast('开始上传固件到 ' + chip.toUpperCase(), 'success');
|
||||
try {
|
||||
var arrayBuffer = await file.arrayBuffer();
|
||||
// SHA-256:processFile 阶段已算好并缓存;这里若 handle.getFile() 拿回的
|
||||
// fresh file meta 未变就直接复用(命中 30–100ms/MB 的计算);
|
||||
// 用户 rebuild 使 lastModified 变化时 ensureFileShaCached 自动 fallback 到重算。
|
||||
await ensureFileShaCached(file, chip, arrayBuffer);
|
||||
/* 上传前始终按本次内容重算 SHA(本地固件会改,不能沿用选文件时的旧值) */
|
||||
var savedSha = await computeFileSha(file, chip, arrayBuffer);
|
||||
if (!savedSha) {
|
||||
addLog('E', '文件', chip.toUpperCase() + ' SHA-256 计算失败');
|
||||
showToast('SHA-256 计算失败', 'error');
|
||||
cleanupAfterOtaFailure('SHA-256 计算失败');
|
||||
return;
|
||||
}
|
||||
var fileSize = file.size;
|
||||
var body, url = window.getBaseUrl() + '/ota/upload';
|
||||
if (chip === 'esp32') {
|
||||
|
||||
@@ -105,7 +105,7 @@ export function markGd32OtaUiComplete(msg) {
|
||||
if (typeof window.updateUploadButtonLabel === 'function') {
|
||||
window.updateUploadButtonLabel();
|
||||
}
|
||||
addLog('I', 'OTA', msg || 'GD32 刷写完成');
|
||||
/* 完成文案以设备 [I][OTA] GD32 OTA完成 为准,前端只 toast/进度,避免日志双份 */
|
||||
showToast(msg || 'GD32 刷写完成', 'success');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user