Files
ESP32_COMM/tools/js/common/heap-low-history.js
T
小龙虾 00b716c8db 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模式配置位置
2026-07-20 03:10:42 +08:00

196 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}