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:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* heap-low-history.js - 堆"最低空闲创新低"记录的浏览器端持久化
|
||||
*
|
||||
* 目的:设备 RAM 只保留最新 1 条创新低;长时间挂机早期记录会被覆盖。
|
||||
* 前端定时拉 /system/heap_low_log,发现新记录就追加到 IndexedDB,
|
||||
* 弹窗里合并显示「设备当前 1 条 + 浏览器累积历史」,掉电/重启不丢本地史。
|
||||
*
|
||||
* 存储设计:
|
||||
* - 单 objectStore 'heap_low_records',keyPath 'id'(自增)
|
||||
* - 每条记录附前端写入时的时间戳 saved_at_ms(wall clock)
|
||||
* - 设备重启会重置 ts_ms(启动后毫秒)和 min_free_heap(水位清零),
|
||||
* 所以去重 key 用 (ts_ms, min_free_heap, free_heap, alloc_count) 组合,
|
||||
* 重启后第一条 min_free_heap 必然高于上一次会话最低值 → 自然不会误并入。
|
||||
* - 上限 HEAP_HISTORY_MAX 条,超出按 saved_at_ms 升序删最早的。
|
||||
*/
|
||||
|
||||
var IDB_NAME = 'esp_dcu_heap_low';
|
||||
var IDB_STORE = 'heap_low_records';
|
||||
var IDB_VERSION = 1; // 独立库,与 file-handle.js 的 dcu_ota_fw_v1 不冲突
|
||||
/** 浏览器可保存的创新低条数上限(设备仅 1 槽;多条全靠这里)。 */
|
||||
export var HEAP_HISTORY_MAX = 500;
|
||||
|
||||
var _dbPromise = null;
|
||||
|
||||
function openDb() {
|
||||
if (!_dbPromise) {
|
||||
_dbPromise = new Promise(function (resolve, reject) {
|
||||
if (!window.indexedDB) {
|
||||
reject(new Error('indexedDB unavailable'));
|
||||
return;
|
||||
}
|
||||
var req = indexedDB.open(IDB_NAME, IDB_VERSION);
|
||||
req.onupgradeneeded = function () {
|
||||
var db = req.result;
|
||||
if (!db.objectStoreNames.contains(IDB_STORE)) {
|
||||
var store = db.createObjectStore(IDB_STORE, { keyPath: 'id', autoIncrement: true });
|
||||
// 去重查询索引:同一会话内 (ts_ms,min_free_heap,free_heap,alloc_count) 唯一
|
||||
store.createIndex('dedup', ['ts_ms', 'min_free_heap', 'free_heap', 'alloc_count'], { unique: false });
|
||||
store.createIndex('saved_at', 'saved_at_ms', { unique: false });
|
||||
}
|
||||
};
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
req.onerror = function () { reject(req.error || new Error('idb open failed')); };
|
||||
});
|
||||
_dbPromise.catch(function () { _dbPromise = null; });
|
||||
}
|
||||
return _dbPromise;
|
||||
}
|
||||
|
||||
/** 计算单条记录的去重 key 字符串(用于 in-memory 比对,避免每次都走 IDB 索引) */
|
||||
function dedupKey(r) {
|
||||
return r.ts_ms + '|' + r.min_free_heap + '|' + r.free_heap + '|' + (r.alloc_count || 0);
|
||||
}
|
||||
|
||||
/** 把设备拉到的一批 records(最新在前)增量写入 IndexedDB,去重。
|
||||
* 返回 Promise<number>:本次实际新增的条数。 */
|
||||
export function appendHeapLowRecords(records) {
|
||||
if (!Array.isArray(records) || !records.length) return Promise.resolve(0);
|
||||
return openDb().then(function (db) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tx = db.transaction(IDB_STORE, 'readwrite');
|
||||
var store = tx.objectStore(IDB_STORE);
|
||||
var idx = store.index('dedup');
|
||||
var newCount = 0;
|
||||
var savedAt = Date.now();
|
||||
|
||||
// 顺序处理:每条先查 dedup 索引,不存在则 put
|
||||
var i = 0;
|
||||
function next() {
|
||||
if (i >= records.length) {
|
||||
// 全部处理完后再裁剪到上限
|
||||
pruneToMax(store, function () {
|
||||
tx.oncomplete ? null : null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
var r = records[i++];
|
||||
var key = [r.ts_ms, r.min_free_heap, r.free_heap, r.alloc_count || 0];
|
||||
var getReq = idx.get(key);
|
||||
getReq.onsuccess = function () {
|
||||
if (!getReq.result) {
|
||||
// 新记录:写入
|
||||
var rec = Object.assign({}, r, { saved_at_ms: savedAt });
|
||||
var putReq = store.put(rec);
|
||||
putReq.onsuccess = function () { newCount++; next(); };
|
||||
putReq.onerror = function () { next(); }; // 单条失败不阻断
|
||||
} else {
|
||||
next(); // 已存在跳过
|
||||
}
|
||||
};
|
||||
getReq.onerror = function () { next(); };
|
||||
}
|
||||
next();
|
||||
|
||||
tx.oncomplete = function () { resolve(newCount); };
|
||||
tx.onerror = function () { reject(tx.error); };
|
||||
tx.onabort = function () { reject(tx.error || new Error('tx aborted')); };
|
||||
});
|
||||
}).catch(function (e) {
|
||||
// IndexedDB 不可用(无痕模式等)静默降级
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/** 删最早的记录直到条数 <= HEAP_HISTORY_MAX。需在 readwrite tx 内调用。 */
|
||||
function pruneToMax(store, done) {
|
||||
var countReq = store.count();
|
||||
countReq.onsuccess = function () {
|
||||
var total = countReq.result;
|
||||
if (total <= HEAP_HISTORY_MAX) { done(); return; }
|
||||
var toRemove = total - HEAP_HISTORY_MAX;
|
||||
// 按 saved_at 索引升序游标删前 toRemove 条
|
||||
var idx = store.index('saved_at');
|
||||
var curReq = idx.openCursor();
|
||||
var removed = 0;
|
||||
curReq.onsuccess = function (ev) {
|
||||
var cursor = ev.target.result;
|
||||
if (!cursor || removed >= toRemove) { done(); return; }
|
||||
var delReq = store.delete(cursor.primaryKey);
|
||||
delReq.onsuccess = function () { removed++; cursor.continue(); };
|
||||
delReq.onerror = function () { cursor.continue(); };
|
||||
};
|
||||
curReq.onerror = function () { done(); };
|
||||
};
|
||||
countReq.onerror = function () { done(); };
|
||||
}
|
||||
|
||||
/** 读全部累积历史,按设备时间 ts_ms 升序(旧的在前),便于弹窗"历史段"显示。
|
||||
* 返回 Promise<Array>。IndexedDB 不可用时返回 []。 */
|
||||
export function loadHeapLowHistory() {
|
||||
return openDb().then(function (db) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tx = db.transaction(IDB_STORE, 'readonly');
|
||||
var store = tx.objectStore(IDB_STORE);
|
||||
var all = [];
|
||||
var curReq = store.openCursor();
|
||||
curReq.onsuccess = function (ev) {
|
||||
var cursor = ev.target.result;
|
||||
if (!cursor) return;
|
||||
all.push(cursor.value);
|
||||
cursor.continue();
|
||||
};
|
||||
curReq.onerror = function () { reject(curReq.error); };
|
||||
tx.oncomplete = function () {
|
||||
// 按 ts_ms 升序;同 ts_ms 时按 saved_at 升序(保证稳定)
|
||||
all.sort(function (a, b) {
|
||||
if (a.ts_ms !== b.ts_ms) return a.ts_ms - b.ts_ms;
|
||||
return (a.saved_at_ms || 0) - (b.saved_at_ms || 0);
|
||||
});
|
||||
resolve(all);
|
||||
};
|
||||
tx.onerror = function () { reject(tx.error); };
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
/** 清空全部历史(用于"清空"按钮,可选实现)。返回 Promise<boolean>。 */
|
||||
export function clearHeapLowHistory() {
|
||||
return openDb().then(function (db) {
|
||||
return new Promise(function (resolve) {
|
||||
var tx = db.transaction(IDB_STORE, 'readwrite');
|
||||
var req = tx.objectStore(IDB_STORE).clear();
|
||||
req.onsuccess = function () { resolve(true); };
|
||||
req.onerror = function () { resolve(false); };
|
||||
});
|
||||
}).catch(function () { return false; });
|
||||
}
|
||||
|
||||
/** 返回当前累积条数(仅用于 UI 提示,不需要数据)。返回 Promise<number>。 */
|
||||
export function countHeapLowHistory() {
|
||||
return openDb().then(function (db) {
|
||||
return new Promise(function (resolve) {
|
||||
var tx = db.transaction(IDB_STORE, 'readonly');
|
||||
var req = tx.objectStore(IDB_STORE).count();
|
||||
req.onsuccess = function () { resolve(req.result || 0); };
|
||||
req.onerror = function () { resolve(0); };
|
||||
});
|
||||
}).catch(function () { return 0; });
|
||||
}
|
||||
|
||||
/** 计算给定 records 数组(设备拉到的最新在前)与参考集合(已存历史)相比的新条数。
|
||||
* 纯内存比对,不写库,用于先判断是否需要写。 */
|
||||
export function diffNewCount(records, existingHistory) {
|
||||
if (!Array.isArray(records) || !records.length) return 0;
|
||||
var seen = {};
|
||||
if (Array.isArray(existingHistory)) {
|
||||
for (var i = 0; i < existingHistory.length; ++i) {
|
||||
seen[dedupKey(existingHistory[i])] = true;
|
||||
}
|
||||
}
|
||||
var n = 0;
|
||||
for (var j = 0; j < records.length; ++j) {
|
||||
if (!seen[dedupKey(records[j])]) ++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
Reference in New Issue
Block a user