Implemented history writer
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user