- 将日志环形缓冲区从静态数组改为动态堆分配
- 实现启动时大容量缓冲区和运行时小容量缓冲区的切换机制
- 添加日志环满时优先保留错误和警告信息的清理策略
- 优化内存使用,在日志为空时避免不必要的内存分配
- 移除调试日志中的冗余字段显示
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模式配置位置
196 lines
7.8 KiB
JavaScript
196 lines
7.8 KiB
JavaScript
/**
|
||
* 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;
|
||
} |