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) {
|
||||
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() {
|
||||
const cal = trig.signal ? calForKey(trig.signal) : Calib.IDENTITY;
|
||||
wsSend({
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// 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]}
|
||||
function onTriggerState(msg) {
|
||||
const st = msg.state || 'idle';
|
||||
@@ -2423,7 +2434,11 @@ function showHoverReadout(p, e) {
|
||||
p.traces.forEach((key, idx) => {
|
||||
const vNorm = interpAtTime(p.uplot, idx + 1, t);
|
||||
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:' +
|
||||
escHtml(getSigStyle(key).color) + '"></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.
|
||||
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.
|
||||
refreshTrigThresholdField();
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════
|
||||
@@ -2660,19 +2676,20 @@ function buildSidebar() {
|
||||
const n = numElements(sig), temporal = isTemporal(sig);
|
||||
const typeName = _typeNames[sig.typeCode] || '?';
|
||||
const globalKey = prefix + sig.name;
|
||||
const effUnit = unitForKey(globalKey);
|
||||
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 {
|
||||
const group = document.createElement('div'); group.className = 'array-group';
|
||||
const header = document.createElement('div'); header.className = 'array-header';
|
||||
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>';
|
||||
header.addEventListener('click', () => header.classList.toggle('open'));
|
||||
const children = document.createElement('div'); children.className = 'array-children';
|
||||
for (let i = 0; i < n; 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);
|
||||
}
|
||||
group.appendChild(header); group.appendChild(children); grp.appendChild(group);
|
||||
@@ -3028,11 +3045,19 @@ async function exportAllCSV() {
|
||||
return m;
|
||||
});
|
||||
|
||||
// Strip "sourceId:" prefix from column headers for readability.
|
||||
const displayKeys = keys.map(k => (k.includes(':') ? k.split(':').slice(1).join(':') : k));
|
||||
// Strip "sourceId:" prefix from column headers for readability, and append
|
||||
// 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 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 a = document.createElement('a');
|
||||
@@ -3441,7 +3466,9 @@ function applyCalibrationChanged() {
|
||||
buildSidebar(); // unit badges
|
||||
plots.forEach(p => { p.needsRedraw = true; });
|
||||
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.
|
||||
|
||||
@@ -40,7 +40,9 @@
|
||||
var offset = obj.offset === undefined ? 0 : obj.offset;
|
||||
if (!isFiniteNum(scale) || scale === 0) 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.
|
||||
// TextEncoder/TextDecoder are available natively in all modern browsers and
|
||||
// Node v11+; no build step or bundler is needed.
|
||||
|
||||
Reference in New Issue
Block a user