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 -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);