Implemented history writer

This commit is contained in:
Martino Ferrari
2026-06-14 14:09:51 +02:00
parent f25bd7f08e
commit dd7dd22cb0
27 changed files with 1537 additions and 104 deletions
+106 -10
View File
@@ -344,6 +344,8 @@ function connectWS() {
else if (msg.type === 'data') onData(msg);
else if (msg.type === 'stats') onStats(msg);
else if (msg.type === 'zoom') onZoomReply(msg);
else if (msg.type === 'historyZoom') onHistoryZoomReply(msg);
else if (msg.type === 'historyInfo') onHistoryInfo(msg);
else if (msg.type === 'triggerState') onTriggerState(msg);
};
}
@@ -372,6 +374,56 @@ function onZoomReply(msg) {
p.resolve(msg.signals || {});
}
/* ════════════════════════════════════════════════════════════════
History (disk-backed data from the hub)
════════════════════════════════════════════════════════════════ */
let historyMeta = null; // {enabled, durationHours, decimation, signals:{key:{t0,t1,count,capacity}}}
const historyData = {}; // plotId → {signals:{key:{t,v}}, t0, t1}
const _histPending = new Map();
function wsHistoryZoomRequest(t0, t1, n, keys) {
return new Promise((resolve, reject) => {
if (!ws || ws.readyState !== WebSocket.OPEN) { reject(new Error('ws not open')); return; }
const reqId = ++_zoomReqId;
const timer = setTimeout(() => {
_histPending.delete(reqId);
reject(new Error('historyZoom timeout'));
}, 10000);
_histPending.set(reqId, { resolve, timer });
ws.send(JSON.stringify({ type: 'historyZoom', reqId, t0, t1, n, signals: keys.join(',') }));
});
}
function onHistoryZoomReply(msg) {
const p = _histPending.get(msg.reqId);
if (!p) return;
clearTimeout(p.timer);
_histPending.delete(msg.reqId);
p.resolve(msg.signals || {});
}
function onHistoryInfo(msg) {
historyMeta = {
enabled: msg.enabled || false,
durationHours: msg.durationHours || 0,
decimation: msg.decimation || 1,
signals: msg.signals || {},
};
updateHistoryUI();
}
function updateHistoryUI() {
const badge = document.getElementById('history-badge');
if (!badge) return;
if (historyMeta && historyMeta.enabled) {
const nSigs = Object.keys(historyMeta.signals).length;
badge.textContent = `History: ${historyMeta.durationHours}h, ${nSigs} signals`;
badge.style.display = '';
} else {
badge.style.display = 'none';
}
}
/* ════════════════════════════════════════════════════════════════
Status LED
════════════════════════════════════════════════════════════════ */
@@ -935,18 +987,62 @@ async function doZoomFetch(t0, t1) {
if (allKeys.size === 0) return;
const targetPts = Math.max(400, (plots.find(p => p.uplot)?.uplot?.width || 600) * 2);
let sigs;
try {
sigs = await wsZoomRequest(t0, t1, targetPts, [...allKeys]);
} catch (e) { console.warn('zoom fetch:', e); return; }
if (!sigs) return;
// Convert arrays from JSON to Float64Array and store per-plot.
// Fire regular zoom + history zoom in parallel
const promises = [];
// Regular zoom (in-memory ring)
promises.push(wsZoomRequest(t0, t1, targetPts, [...allKeys]).catch(e => {
console.warn('zoom fetch:', e); return null;
}));
// History zoom (disk) — only if history is enabled
if (historyMeta && historyMeta.enabled) {
promises.push(wsHistoryZoomRequest(t0, t1, targetPts, [...allKeys]).catch(e => {
console.warn('history zoom fetch:', e); return null;
}));
}
const [sigs, histSigs] = await Promise.all(promises);
// Merge: history data first (older), regular zoom overlays (newer wins)
const merged = {};
Object.entries(sigs).forEach(([k, sd]) => {
if (!sd || !sd.t || !sd.v) return;
merged[k] = { t: Float64Array.from(sd.t), v: Float64Array.from(sd.v) };
});
if (histSigs) {
Object.entries(histSigs).forEach(([k, sd]) => {
if (!sd || !sd.t || !sd.v) return;
merged[k] = { t: Float64Array.from(sd.t), v: Float64Array.from(sd.v) };
});
}
if (sigs) {
Object.entries(sigs).forEach(([k, sd]) => {
if (!sd || !sd.t || !sd.v || sd.t.length === 0) return;
const zt = Float64Array.from(sd.t), zv = Float64Array.from(sd.v);
if (merged[k] && merged[k].t.length > 0) {
// Merge: use history for t < ring oldest, ring for the rest
const ht = merged[k].t, hv = merged[k].v;
const ringOldest = zt[0];
// Find cutoff in history data
let cutIdx = ht.length;
for (let i = 0; i < ht.length; i++) {
if (ht[i] >= ringOldest) { cutIdx = i; break; }
}
if (cutIdx > 0) {
// Prepend history data before ring data
const mt = new Float64Array(cutIdx + zt.length);
const mv = new Float64Array(cutIdx + zv.length);
mt.set(ht.subarray(0, cutIdx));
mv.set(hv.subarray(0, cutIdx));
mt.set(zt, cutIdx);
mv.set(zv, cutIdx);
merged[k] = { t: mt, v: mv };
} else {
merged[k] = { t: zt, v: zv };
}
} else {
merged[k] = { t: zt, v: zv };
}
});
}
plots.forEach(p => {
// Only store if this plot's range still matches what we fetched.