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:
小龙虾
2026-07-20 03:10:42 +08:00
parent b807645a20
commit 00b716c8db
66 changed files with 4803 additions and 1074 deletions
+111 -71
View File
@@ -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
View File
@@ -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);
+1 -2
View File
@@ -156,8 +156,7 @@ export function cleanupAfterOtaFailure(reason) {
window.clearOtaSessionResiduals({
keepErrorStages: true,
statusText: reason || '失败',
errorStyle: true,
clearCacheDisplay: true
errorStyle: true
});
// 2. 阶段指示器标错
+1 -12
View File
@@ -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
View File
@@ -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。 */
}
/**
+17 -23
View File
@@ -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') {
+1 -1
View File
@@ -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');
}