webui: calibrate CSV export, trigger threshold and unit display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4908c039a5
commit
3cb998c5da
@@ -706,14 +706,25 @@ function onBinaryData(buf) {
|
|||||||
function wsSend(obj) {
|
function wsSend(obj) {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
// trig.threshold is held in calibrated units. The hub's comparator runs on raw
|
||||||
|
// samples, so invert on the way out: raw = (calibrated - offset) / scale.
|
||||||
function sendTrigConfig() {
|
function sendTrigConfig() {
|
||||||
|
const cal = trig.signal ? calForKey(trig.signal) : Calib.IDENTITY;
|
||||||
wsSend({
|
wsSend({
|
||||||
type: 'setTrigger', signal: trig.signal, edge: trig.edge,
|
type: 'setTrigger', signal: trig.signal, edge: trig.edge,
|
||||||
threshold: trig.threshold, windowSec: trig.windowSec,
|
threshold: Calib.invertCal(trig.threshold, cal), windowSec: trig.windowSec,
|
||||||
prePercent: trig.prePercent, mode: trig.mode,
|
prePercent: trig.prePercent, mode: trig.mode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rewrite the threshold input and its unit hint from trig.threshold.
|
||||||
|
function refreshTrigThresholdField() {
|
||||||
|
const el = document.getElementById('trig-threshold');
|
||||||
|
if (document.activeElement !== el) el.value = trig.threshold;
|
||||||
|
const u = trig.signal ? unitForKey(trig.signal) : '';
|
||||||
|
el.title = u ? 'Threshold in ' + u : 'Threshold in the signal\u2019s raw units';
|
||||||
|
}
|
||||||
|
|
||||||
// Hub FSM broadcast: {state:"idle|armed|collecting|triggered", mode, stopped[, trigTime]}
|
// Hub FSM broadcast: {state:"idle|armed|collecting|triggered", mode, stopped[, trigTime]}
|
||||||
function onTriggerState(msg) {
|
function onTriggerState(msg) {
|
||||||
const st = msg.state || 'idle';
|
const st = msg.state || 'idle';
|
||||||
@@ -2423,7 +2434,11 @@ function showHoverReadout(p, e) {
|
|||||||
p.traces.forEach((key, idx) => {
|
p.traces.forEach((key, idx) => {
|
||||||
const vNorm = interpAtTime(p.uplot, idx + 1, t);
|
const vNorm = interpAtTime(p.uplot, idx + 1, t);
|
||||||
const name = key.includes(':') ? key.slice(key.indexOf(':') + 1) : key;
|
const name = key.includes(':') ? key.slice(key.indexOf(':') + 1) : key;
|
||||||
const val = vNorm === null ? '—' : _fmtVal(rawFromNorm(p, key, vNorm));
|
// rawFromNorm inverts the vscale transform, which Task 7 made operate on
|
||||||
|
// calibrated values — so this is already in calibrated units.
|
||||||
|
const unit = unitForKey(key);
|
||||||
|
const val = vNorm === null ? '—'
|
||||||
|
: (_fmtVal(rawFromNorm(p, key, vNorm)) + (unit ? ' ' + unit : ''));
|
||||||
html += '<div class="hov-row"><span class="hov-dot" style="background:' +
|
html += '<div class="hov-row"><span class="hov-dot" style="background:' +
|
||||||
escHtml(getSigStyle(key).color) + '"></span>' +
|
escHtml(getSigStyle(key).color) + '"></span>' +
|
||||||
'<span class="hov-name">' + escHtml(name) + '</span>' +
|
'<span class="hov-name">' + escHtml(name) + '</span>' +
|
||||||
@@ -2612,6 +2627,7 @@ function buildTrigSignalSelect() {
|
|||||||
// Restore selection: match base key so array element "sig[3]" selects "sig" option.
|
// Restore selection: match base key so array element "sig[3]" selects "sig" option.
|
||||||
if (curBase && [...sel.options].some(o => o.value === curBase)) sel.value = curBase;
|
if (curBase && [...sel.options].some(o => o.value === curBase)) sel.value = curBase;
|
||||||
// Do NOT overwrite trig.signal here — an array element selection must be preserved.
|
// Do NOT overwrite trig.signal here — an array element selection must be preserved.
|
||||||
|
refreshTrigThresholdField();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ════════════════════════════════════════════════════════════════
|
/* ════════════════════════════════════════════════════════════════
|
||||||
@@ -2660,19 +2676,20 @@ function buildSidebar() {
|
|||||||
const n = numElements(sig), temporal = isTemporal(sig);
|
const n = numElements(sig), temporal = isTemporal(sig);
|
||||||
const typeName = _typeNames[sig.typeCode] || '?';
|
const typeName = _typeNames[sig.typeCode] || '?';
|
||||||
const globalKey = prefix + sig.name;
|
const globalKey = prefix + sig.name;
|
||||||
|
const effUnit = unitForKey(globalKey);
|
||||||
if (n === 1 || temporal) {
|
if (n === 1 || temporal) {
|
||||||
grp.appendChild(makeDraggable(globalKey, sig.name, temporal ? '[' + n + '] ' + typeName : typeName, sig.unit || ''));
|
grp.appendChild(makeDraggable(globalKey, sig.name, temporal ? '[' + n + '] ' + typeName : typeName, effUnit));
|
||||||
} else {
|
} else {
|
||||||
const group = document.createElement('div'); group.className = 'array-group';
|
const group = document.createElement('div'); group.className = 'array-group';
|
||||||
const header = document.createElement('div'); header.className = 'array-header';
|
const header = document.createElement('div'); header.className = 'array-header';
|
||||||
header.innerHTML = '<span class="array-arrow">▶</span><span class="sig-name">' + escHtml(sig.name) + '</span>'
|
header.innerHTML = '<span class="array-arrow">▶</span><span class="sig-name">' + escHtml(sig.name) + '</span>'
|
||||||
+ (sig.unit ? '<span class="sig-unit">' + escHtml(sig.unit) + '</span>' : '')
|
+ (effUnit ? '<span class="sig-unit">' + escHtml(effUnit) + '</span>' : '')
|
||||||
+ '<span class="type-badge">[' + n + '] ' + typeName + '</span>';
|
+ '<span class="type-badge">[' + n + '] ' + typeName + '</span>';
|
||||||
header.addEventListener('click', () => header.classList.toggle('open'));
|
header.addEventListener('click', () => header.classList.toggle('open'));
|
||||||
const children = document.createElement('div'); children.className = 'array-children';
|
const children = document.createElement('div'); children.className = 'array-children';
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const key = globalKey + '[' + i + ']';
|
const key = globalKey + '[' + i + ']';
|
||||||
const child = makeDraggable(key, sig.name + '[' + i + ']', typeName, sig.unit || '');
|
const child = makeDraggable(key, sig.name + '[' + i + ']', typeName, effUnit);
|
||||||
child.className = 'array-child'; children.appendChild(child);
|
child.className = 'array-child'; children.appendChild(child);
|
||||||
}
|
}
|
||||||
group.appendChild(header); group.appendChild(children); grp.appendChild(group);
|
group.appendChild(header); group.appendChild(children); grp.appendChild(group);
|
||||||
@@ -3028,11 +3045,19 @@ async function exportAllCSV() {
|
|||||||
return m;
|
return m;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Strip "sourceId:" prefix from column headers for readability.
|
// Strip "sourceId:" prefix from column headers for readability, and append
|
||||||
const displayKeys = keys.map(k => (k.includes(':') ? k.split(':').slice(1).join(':') : k));
|
// the effective unit. These values come straight from the ring/history/
|
||||||
|
// snapshot and never pass through applyVScaleNorm, so calibrate them here.
|
||||||
|
const cals = keys.map(k => calForKey(k));
|
||||||
|
const displayKeys = keys.map(k => {
|
||||||
|
const name = k.includes(':') ? k.split(':').slice(1).join(':') : k;
|
||||||
|
const u = unitForKey(k);
|
||||||
|
return u ? name + ' [' + u + ']' : name;
|
||||||
|
});
|
||||||
const hdr = [(inTrigMode ? 'time_rel_s' : 'time_s'), ...displayKeys].join(',');
|
const hdr = [(inTrigMode ? 'time_rel_s' : 'time_s'), ...displayKeys].join(',');
|
||||||
const rows = sortedT.map(t =>
|
const rows = sortedT.map(t =>
|
||||||
[t.toFixed(9), ...lookups.map(lk => (lk.has(t) ? lk.get(t) : ''))].join(',')
|
[t.toFixed(9), ...lookups.map((lk, i) =>
|
||||||
|
lk.has(t) ? Calib.applyCal(lk.get(t), cals[i]) : '')].join(',')
|
||||||
);
|
);
|
||||||
const blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' });
|
const blob = new Blob([hdr + '\n' + rows.join('\n')], { type: 'text/csv' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@@ -3441,7 +3466,9 @@ function applyCalibrationChanged() {
|
|||||||
buildSidebar(); // unit badges
|
buildSidebar(); // unit badges
|
||||||
plots.forEach(p => { p.needsRedraw = true; });
|
plots.forEach(p => { p.needsRedraw = true; });
|
||||||
if (typeof refreshVScaleMenu === 'function') refreshVScaleMenu(); // Task 8
|
if (typeof refreshVScaleMenu === 'function') refreshVScaleMenu(); // Task 8
|
||||||
if (typeof sendTrigConfig === 'function') sendTrigConfig(); // Task 9
|
// The threshold is held in calibrated units, so a calibration change alters
|
||||||
|
// the raw value the hub must compare against — resend it.
|
||||||
|
if (trig.signal) { refreshTrigThresholdField(); sendTrigConfig(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replaced in Task 10 with the Sources & Config status renderer.
|
// Replaced in Task 10 with the Sources & Config status renderer.
|
||||||
|
|||||||
@@ -40,7 +40,9 @@
|
|||||||
var offset = obj.offset === undefined ? 0 : obj.offset;
|
var offset = obj.offset === undefined ? 0 : obj.offset;
|
||||||
if (!isFiniteNum(scale) || scale === 0) return null;
|
if (!isFiniteNum(scale) || scale === 0) return null;
|
||||||
if (!isFiniteNum(offset)) return null;
|
if (!isFiniteNum(offset)) return null;
|
||||||
var unit = String(obj.unit == null ? '' : obj.unit).trim();
|
// Commas and quotes would corrupt the CSV export header; drop them here so
|
||||||
|
// every consumer sees an already-safe unit.
|
||||||
|
var unit = String(obj.unit == null ? '' : obj.unit).replace(/[",]/g, '').trim();
|
||||||
// Cap at MAX_UNIT_LEN UTF-8 bytes, matching both hubs' Normalise() exactly.
|
// Cap at MAX_UNIT_LEN UTF-8 bytes, matching both hubs' Normalise() exactly.
|
||||||
// TextEncoder/TextDecoder are available natively in all modern browsers and
|
// TextEncoder/TextDecoder are available natively in all modern browsers and
|
||||||
// Node v11+; no build step or bundler is needed.
|
// Node v11+; no build step or bundler is needed.
|
||||||
|
|||||||
@@ -150,3 +150,7 @@ test('CalTable.list is sorted by source then signal', () => {
|
|||||||
assert.deepStrictEqual(t.list().map(e => e.source + '/' + e.signal),
|
assert.deepStrictEqual(t.list().map(e => e.source + '/' + e.signal),
|
||||||
['a/b', 'a/z', 'z/a']);
|
['a/b', 'a/z', 'z/a']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('normaliseCal strips characters that would corrupt a CSV header', () => {
|
||||||
|
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: 'k,V"'}).unit, 'kV');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user