diff --git a/Client/udpstreamer/static/app.js b/Client/udpstreamer/static/app.js
index 42c2783..159215e 100644
--- a/Client/udpstreamer/static/app.js
+++ b/Client/udpstreamer/static/app.js
@@ -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 += '
' +
'' + escHtml(name) + '' +
@@ -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 = '▶' + escHtml(sig.name) + ''
- + (sig.unit ? '' + escHtml(sig.unit) + '' : '')
+ + (effUnit ? '' + escHtml(effUnit) + '' : '')
+ '[' + n + '] ' + typeName + '';
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.
diff --git a/Client/udpstreamer/static/calibration.js b/Client/udpstreamer/static/calibration.js
index 1c3c557..8756153 100644
--- a/Client/udpstreamer/static/calibration.js
+++ b/Client/udpstreamer/static/calibration.js
@@ -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.
diff --git a/Client/udpstreamer/test/calibration.test.js b/Client/udpstreamer/test/calibration.test.js
index 9a108d7..c1bed6d 100644
--- a/Client/udpstreamer/test/calibration.test.js
+++ b/Client/udpstreamer/test/calibration.test.js
@@ -150,3 +150,7 @@ test('CalTable.list is sorted by source then signal', () => {
assert.deepStrictEqual(t.list().map(e => e.source + '/' + e.signal),
['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');
+});