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
+32 -32
View File
@@ -9,8 +9,9 @@ import { state, els, bleLog, currentTarget, currentPayloadSize, setConnectionSta
import { setBleStage, setBleStageText, resetBleStages } from './stages.js';
import { setProgress } from './transfer.js';
import { hideBleVerifyInfo } from './verify.js';
import { formatBytes, bytesToHex } from '../common/utils.js';
import { formatBytes } from '../common/utils.js';
import { showToast } from '../common/toast.js';
import { computeFileSha } from '../ota/file-handle.js';
/** 任一通道上传中(BLE 或 WiFi)。focus/change 等不分通道的事件用此判定。 */
function isAnyChannelUploading() {
@@ -18,11 +19,8 @@ function isAnyChannelUploading() {
}
export function updateFileInfo(file) {
// ★ 上传进行中禁止重置进度/阶段:目标切换、模式切换、syncBleFileFromSelectedTarget
// 等事件会在上传期间调用本函数,若不拦截会触发 finalizeFileInfo → setProgress(0, ...)
// 把进度条从"已确认 N/M 包"错误地刷回"等待选择文件 0%"。
// ★ 上传进行中禁止重置进度/阶段
if (isBleUploading() && file && state.selectedFile) {
// 上传中且文件未变:仅刷新元数据(文件名/大小等),不碰进度条和阶段
bleLog('FILE', '上传进行中,跳过 updateFileInfo 进度重置', 'D');
return Promise.resolve();
}
@@ -43,9 +41,9 @@ export function updateFileInfo(file) {
else els.fileInfo.classList.remove('show');
}
// ★ HEX → BIN 自动转换:复用 app.js 公共函数 window.convertHexFileToBin
// ★ HEX → BIN:必须用 window.convertHexFileToBin(本模块未 import,裸名会 ReferenceError)
if (file && file.name.toLowerCase().endsWith('.hex') && typeof window.convertHexFileToBin === 'function') {
return convertHexFileToBin(file).then(function(result) {
return window.convertHexFileToBin(file).then(function(result) {
if (!result.converted) {
bleLog('FILE', file.name + ' 扩展名为 .hex 但内容不像 HEX 格式,按原始文件处理', 'W');
finalizeAndHash(file);
@@ -55,7 +53,6 @@ export function updateFileInfo(file) {
var baseHex = '0x' + result.baseAddr.toString(16).toUpperCase().padStart(8, '0');
bleLog('FILE', 'HEX → BIN 转换完成: ' + file.name + ' → ' + result.file.name + ' (基地址 ' + baseHex + ', ' + formatBytes(result.file.size) + ')');
showToast('HEX 已自动转换为 BIN', 'success');
// 更新 UI 显示转换后的信息
els.fileName.textContent = result.file.name + ' (由 ' + file.name + ' 转换)';
els.fileType.textContent = 'Binary (HEX转换)';
els.fileSize.textContent = formatBytes(result.file.size);
@@ -72,20 +69,16 @@ export function updateFileInfo(file) {
});
}
// 非 HEX 文件直接完成
finalizeAndHash(file);
return Promise.resolve();
}
/** finalizeFileInfo + computeFileSha256 的统一出口:HEX 转换完成分支与非 HEX 分支共用。 */
function finalizeAndHash(file) {
finalizeFileInfo(file);
computeFileSha256(file);
computeBleFileSha(file);
}
function finalizeFileInfo(file) {
// ★ 上传进行中禁止重置进度条/阶段:防止目标切换等事件在上传期间
// 通过 finalizeFileInfo → setProgress(0, '等待选择文件') 覆盖上传进度。
if (isBleUploading()) {
bleLog('FILE', '上传进行中,跳过 finalizeFileInfo 进度重置', 'D');
return;
@@ -105,40 +98,43 @@ function finalizeFileInfo(file) {
}
/**
* 计算文件 SHA-256 并推进 BLE stage 流。
* stage 推进逻辑收敛在此函数内,避免与 finalizeFileInfo 隐式耦合:
* - 已连接且 file 非空:进入「计算中」态(stage 3 active + 状态栏文案)
* - 哈希算完:stage 3 done + 推进 stage 4 + 状态栏「文件就绪」
* - file 为空 / 未连接:不碰 stage(由 finalizeFileInfo 的 else 分支 + clearSelectedFile 负责)
* BLE 侧:读对应芯片槽;槽里已有则直接用,没有才算一次。
* 不跨芯片、不互切。
*/
function computeFileSha256(file) {
function computeBleFileSha(file) {
var shaElem = document.getElementById('fileSha256');
if (!file || !shaElem) {
if (!file) {
if (shaElem) shaElem.textContent = '-';
state.fileSha256 = null;
return;
}
// 已连接:进入「计算中」态(旧逻辑放在 finalizeFileInfo 里,现收敛至此)
var chip = (window.flashTarget === 'gd32') ? 'gd32' : 'esp32';
var existing = chip === 'gd32' ? window._gd32FileSha256 : window._esp32FileSha256;
if (existing) {
state.fileSha256 = existing;
if (shaElem) shaElem.textContent = existing;
if (state.server) {
setBleStage(3, 'SHA-256 已计算', null);
setBleStage(4, '', null);
setBleStageText('文件就绪,可以开始刷写');
}
return;
}
if (state.server) {
setBleStage(3, '计算哈希...', 'active');
setBleStageText('正在计算文件 SHA-256...');
}
shaElem.textContent = '计算中...';
file.arrayBuffer().then(function(buf) {
return crypto.subtle.digest('SHA-256', buf);
}).then(function(hashBuffer) {
var hashHex = bytesToHex(new Uint8Array(hashBuffer)).toUpperCase();
shaElem.textContent = hashHex;
computeFileSha(file, chip).then(function(hashHex) {
if (!hashHex) {
state.fileSha256 = null;
return;
}
state.fileSha256 = hashHex;
bleLog('FILE', 'SHA-256: ' + hashHex);
if (state.server) {
setBleStage(3, 'SHA-256 已计算', null);
setBleStage(4, '', null);
setBleStageText('文件就绪,可以开始刷写');
}
}).catch(function(err) {
shaElem.textContent = '计算失败: ' + err.message;
state.fileSha256 = null;
});
}
@@ -219,7 +215,11 @@ export async function notifyFileChange(file, handle) {
return;
}
if (file) {
if (handle) state.selectedFileHandle = handle;
/* 始终更新 handle(含 null),避免切目标后仍热重载到上一芯片的文件 */
state.selectedFileHandle = handle || null;
await updateFileInfo(file);
} else {
state.selectedFileHandle = null;
await updateFileInfo(null);
}
}
+16 -14
View File
@@ -3,8 +3,6 @@ import { TARGETS } from './constants.js';
import { addLog } from '../common/log.js';
import { updateBleStageLabels, resetBleStages } from './stages.js';
import { hideBleVerifyInfo } from './verify.js';
import { updateFileInfo } from './file.js';
export const state = {
device: null,
server: null,
@@ -28,7 +26,7 @@ export const state = {
otaResultWaiter: null,
otaRunId: 0,
lastEsp32DoneSession: null,
fileSha256: null,
fileSha256: null, // 当前 BLE 会话用的 SHA(与对应芯片槽同步)
deviceSha256: null,
expectedReboot: false, // 发送 0xFA 后置 true,重连后清
reconnecting: false, // 自动重连中
@@ -46,8 +44,6 @@ export const els = {
abortBtn: $('abortBtn'),
uploadZone: $('uploadZone'),
firmwareFile: $('firmwareFile'),
connStatus: $('connStatus'), // 共享连接指示器
connStatusTxt: $('connStatusTxt'), // 共享连接指示器文本
connState: $('connState'),
connBadge: $('connBadge'),
deviceName: $('deviceName'),
@@ -115,10 +111,12 @@ export function setReadyHint(html) {
export function setConnectionState(connected) {
const idle = connected && state.selectedFile && !state.uploading;
// 仅在 BLE 模式下更新共享连接指示器与 BLE 专用徽章,避免覆盖 WiFi 轮询的状态
// BLE 模式:连接状态只写顶栏胶囊,避免下方重复文案
if (typeof connectionMode !== 'undefined' && connectionMode === 'ble') {
if (els.connStatus) els.connStatus.className = connected ? 'conn-indicator ok' : 'conn-indicator err';
if (els.connStatusTxt) els.connStatusTxt.textContent = connected ? 'BLE 已连接' : 'BLE 未连接';
var pc = document.getElementById('pillConn');
var pct = document.getElementById('pillConnTxt');
if (pc) pc.className = connected ? 'pill online' : 'pill offline';
if (pct) pct.textContent = connected ? 'BLE 已连接' : 'BLE 未连接';
if (els.connState) {
els.connState.textContent = connected ? '已连接' : '未连接';
if (els.connState.parentElement) {
@@ -174,17 +172,21 @@ export function resetUiAfterDisconnect() {
export function refreshTargetUi() {
const cfg = currentTarget();
updateBleStageLabels(cfg);
// ★ 上传进行中不调用 updateFileInfo:避免目标切换时进度条被重置。
// 阶段标签更新(updateBleStageLabels)安全,不碰进度条。
if (state.selectedFile && !state.uploading && !state.autoFlashing) {
updateFileInfo(state.selectedFile);
/* 与 WiFi 一致:文件由 syncBleFileFromSelectedTarget 按 flashTarget 绑定;
* 此处只刷新标签/包数/阶段说明。 */
if (state.selectedFile) {
const payloadSize = currentPayloadSize();
const packets = Math.ceil(state.selectedFile.size / payloadSize);
if (els.packetCount) els.packetCount.textContent = String(packets);
}
setConnectionState(!!state.server);
// 同步目标胶囊标签
var espTab = document.getElementById('targetEsp32Tab');
var gdTab = document.getElementById('targetGd32Tab');
var isEsp = cfg.label === 'ESP32';
if (espTab) espTab.classList.toggle('active', isEsp);
if (gdTab) gdTab.classList.toggle('active', !isEsp);
bleLog('CFG', `目标切换为 ${cfg.label}: packetCmd=0x${cfg.packetCmd.toString(16).padStart(2, '0').toUpperCase()}, payload=${cfg.recommendedPayload}`);
var autoBtn = document.getElementById('autoFlashBtn');
if (autoBtn && !state.uploading && !state.autoFlashing) {
autoBtn.textContent = isEsp ? '一键刷写 ESP32' : '一键刷写 GD32';
}
}
+13 -36
View File
@@ -6,6 +6,7 @@ import { showBleVerifyInfo, hideBleVerifyInfo } from './verify.js';
import { clearOtaResultWaiter, waitForOtaResult, nextOtaSessionId } from './session.js';
import { enableNotifications } from './gatt.js';
import { syncSelectedFileFromInput, updateFileInfo } from './file.js';
import { computeFileSha } from '../ota/file-handle.js';
import { formatBytes } from '../common/utils.js';
import { showToast } from '../common/toast.js';
@@ -105,17 +106,15 @@ export function waitForAck(cmd, expectedSeq, timeoutMs = 8000, opts = {}) {
/** @description 分块上传固件到 ESP32(通过 BLE GATT 写入)。 */
export async function uploadFirmware() {
// 每次刷写前尝试重新从本地读取文件
// 热重载:有 handle 则重新读盘(本地编译会改同路径固件);否则用已选/input
if (state.selectedFileHandle) {
try {
const freshFile = await state.selectedFileHandle.getFile();
await updateFileInfo(freshFile);
var actualFile = state.selectedFile || freshFile;
bleLog('FILE', '已重新读取本地文件: ' + actualFile.name + ' (' + formatBytes(actualFile.size) + ')', 'D');
} catch (e) {
bleLog('FILE', '重新读取文件失败: ' + e.message + ',将使用缓存版本', 'W');
}
} else {
} else if (!state.selectedFile) {
await syncSelectedFileFromInput();
}
if (!state.selectedFile || !state.dataInChar) return false;
@@ -172,22 +171,14 @@ export async function uploadFirmware() {
bleLog('OTA', `⚠️ file.size(${file.size}) ≠ arrayBuffer(${totalSize}),以 arrayBuffer 为准,请确认页面已 Ctrl+F5 刷新`, 'W');
}
// SHA-256 已在文件选择时(updateFileInfo)计算并存入 state.fileSha256。
// 此处不再重复打日志(避免与 [FILE] SHA-256 重复),仅保证 fileSha256 变量有值。
var fileSha256;
if (state.fileSha256) {
fileSha256 = state.fileSha256.toLowerCase();
} else {
// 极端兜底:state.fileSha256 未初始化(理论上 updateFileInfo 总会先设置)
bleLog('OTA', '正在计算文件 SHA-256 哈希...', 'D');
const hashBuffer = await crypto.subtle.digest('SHA-256', fileBytes.buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
fileSha256 = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
state.fileSha256 = fileSha256.toUpperCase();
}
// Update SHA display in file info
/* 上传前始终按本次读到的内容重算 SHA,不沿用选文件时的旧值 */
var chipKey = (cfg.label === 'GD32') ? 'gd32' : 'esp32';
var shaHex = await computeFileSha(file, chipKey, fileBytes.buffer);
if (!shaHex) throw new Error('SHA-256 计算失败');
state.fileSha256 = shaHex;
var fileSha256 = shaHex.toLowerCase();
var shaElem = document.getElementById('fileSha256');
if (shaElem) shaElem.textContent = fileSha256.toUpperCase();
if (shaElem) shaElem.textContent = shaHex.toUpperCase();
setBleStage(4, '写入中...', null);
setBleStageText(`正在写入 ${packetTotal} 包数据...`);
@@ -464,29 +455,15 @@ export async function uploadFirmware() {
/** @description 自动刷写流程:上传 ESP32 固件 → SHA验证 → 触发 GD32 中继。 */
export async function autoFlash() {
// 每次刷写前尝试重新从本地读取文件(支持热重载:用户重新编译后无需重新选择)
/* 与 uploadFirmware 一致:有 handle 则重新读盘,SHA 在 upload 内按内容重算 */
if (state.selectedFileHandle) {
try {
const freshFile = await state.selectedFileHandle.getFile();
// 仅在文件变化时才刷新元数据并重算 SHA-256
// (name/size/lastModified 任一不同即视为变化)
// 避免对同一文件重复触发 updateFileInfo → 产生重复的 [FILE] SHA-256 日志
const cached = state.selectedFile;
// HEX 文件转换后 cached.name 是 .bin 而 freshFile.name 是 .hex,
// 因此 HEX 文件用 lastModified 判断是否变化,避免每次都重新转换
const isHex = freshFile.name.toLowerCase().endsWith('.hex');
const changed = !cached ||
(isHex ? false : (cached.name !== freshFile.name || cached.size !== freshFile.size)) ||
cached.lastModified !== freshFile.lastModified;
if (changed) {
await updateFileInfo(freshFile);
var actualFile = state.selectedFile || freshFile;
bleLog('FILE', '已重新读取本地文件(已变更): ' + actualFile.name + ' (' + formatBytes(actualFile.size) + ')', 'D');
}
await updateFileInfo(freshFile);
} catch (e) {
bleLog('FILE', '重新读取文件失败: ' + e.message + ',将使用缓存版本', 'W');
}
} else {
} else if (!state.selectedFile) {
await syncSelectedFileFromInput();
}
if (!state.selectedFile || !state.server) return;