Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions arena_studio.html
Original file line number Diff line number Diff line change
Expand Up @@ -2830,6 +2830,16 @@ <h2 id="modalTitle">Import error</h2>
<button class="pill" id="cAiSweepCsv" data-cmd="caisweepcsv" disabled title="Copy the last sweep (ao_mv, ai1_mv, ai2_mv) to the clipboard as CSV">Copy CSV</button>
<span class="dim" id="cAiSweepSum"></span></div>
<div class="crow" id="cAiSweepRow" style="display:none"><table class="ai-table" id="cAiSweepTable"></table></div>
<div class="crow"><span class="lbl" title="Per-board two-point calibration (needs ai_cal firmware): with nothing on the BNC the input reads the +10 V reference; with a BNC ground cap it reads 0 V. The controller samples each point (256 averaged reads), stores the record in its EEPROM, mirrors it to the SD card, and uses it for GET_ANALOG_IN and Mode 4.">calibrate</span>
<label title="Which input to calibrate">channel <select id="cAiCalCh"><option value="1">Analog In 1</option><option value="2">Analog In 2</option></select></label>
<button class="pill" data-cmd="caicalread" title="GET_ANALOG_CAL 0xA7 + GET_ANALOG_IN_RAW 0xA5 — show the stored record for both channels and the live raw counts on both scales">Read</button>
<button class="pill" data-cmd="caicalopen" title="Step 1 — unplug the BNC so the input floats at the +10 V reference, then click: the controller averages 256 reads and stores them as the +10 V point (SET_ANALOG_CAL 0xA6 action 1)">1 · Sample +10 V (open)</button>
<button class="pill" data-cmd="caicalgnd" title="Step 2 — fit the BNC ground cap, then click: the controller averages 256 reads and stores them as the 0 V point (action 0); with both points the channel becomes calibrated">2 · Sample 0 V (ground cap)</button>
<label title="Mode 4 zero band — inputs within this many mV of the calibrated 0 V give 0 frames/s, so a static input does not creep the pattern (0–2000 mV)">deadband <input id="cAiDeadband" type="number" min="0" max="2000" step="1" value="20" style="width:56px"> mV</label>
<button class="pill" data-cmd="caicaldb" title="Store the deadband for the selected channel (SET_ANALOG_CAL 0xA6 action 2)">Set</button>
<button class="pill led-off" data-cmd="caicalclear" title="Forget the selected channel's two points — back to the nominal ±10 V scale (the deadband is kept)">Clear</button>
<span class="dim" id="cAiCalSum" title="Where the record lives and its state"></span></div>
<div class="crow" id="cAiCalTableRow" style="display:none"><table class="ai-table" id="cAiCalTable"></table></div>
<div class="crow" style="margin-bottom:0"><span class="dim" style="font-size:10.5px" title="Status of the analog-input path (docs/development/analog-input-plan.md)">±10 V front end · nominal scale, per-board calibration not yet applied · a board without the LAB-209 resistor rework reads ≈ +10 V for any input above 0 V · needs io_ext firmware</span></div>
</div>
</section>
Expand Down Expand Up @@ -2953,7 +2963,7 @@ <h2 id="modalTitle">Import error</h2>
</div>

<footer id="footer">
<span class="foot-left">Arena Studio v0.74 | 2026-09-10 16:50 ET · <a href="https://github.com/reiserlab/webDisplayTools" target="_blank" rel="noopener" title="webDisplayTools on GitHub: source, issues, and release notes (docs/development/arena-studio-release-notes.md)">GitHub</a></span>
<span class="foot-left">Arena Studio v0.75 | 2026-09-10 18:38 ET · <a href="https://github.com/reiserlab/webDisplayTools" target="_blank" rel="noopener" title="webDisplayTools on GitHub: source, issues, and release notes (docs/development/arena-studio-release-notes.md)">GitHub</a></span>
<!-- Course-repo quick-links: open protocols / logs / patterns in a new tab.
Hrefs built from the configured repo + bench id (updateGhQuickLinks). -->
<span id="ghQuickLinks" title="Open the course repo on GitHub (new tab)">
Expand Down Expand Up @@ -3721,7 +3731,9 @@ <h2 id="modalTitle">Import error</h2>
// program panels / firmware
'cispbatch', 'cfwpick', 'cfwflash',
// change controller / advanced settings
'setpanelmode', 'setrate', 'setspi', 'sysreset'
'setpanelmode', 'setrate', 'setspi', 'sysreset',
// analog-input calibration (writes the controller's EEPROM record)
'caicalopen', 'caicalgnd', 'caicaldb', 'caicalclear'
]);
// Exposed on Studio for the ConsoleView dispatcher (separate IIFE/script) —
// referencing the bare const there threw a ReferenceError, which killed EVERY
Expand Down Expand Up @@ -5010,7 +5022,7 @@ <h2 id="modalTitle">Import error</h2>
// firmware, user pause, a run in progress, or a sweep in flight.
const AI = window.StudioAnalogIn;
const aiMon = AI ? AI.createMonitor({ windowMs: 30000 }) : null;
const aiUser = { enabled: true, sweeping: false, lastSweep: null, lastFlags: undefined };
const aiUser = { enabled: true, sweeping: false, calibrating: false, lastSweep: null, lastFlags: undefined };
function aiPanelOpen() {
const p = document.querySelector('.panel[data-panel="ai"]');
return !!(p && p.classList.contains('open'));
Expand Down Expand Up @@ -5162,6 +5174,73 @@ <h2 id="modalTitle">Import error</h2>
}
}

// ---- Analog In calibration (analog-input-plan § 4 S2; firmware F2 ai_cal) ----
function aiHasCal() {
return Array.isArray(Studio.capabilities) && Studio.capabilities.includes('ai_cal');
}
function aiCalRenderTable(rec, raws) {
const row = $('cAiCalTableRow');
const tbl = $('cAiCalTable');
const sum = $('cAiCalSum');
if (!row || !tbl || !AI) return;
if (sum) sum.textContent = AI.describeCalSource(rec);
const esc = (t) => String(t).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
const f = (v, d) => (v === null || v === undefined || Number.isNaN(v) ? '—' : typeof v === 'number' ? v.toFixed(d || 0) : String(v));
let html = '<tr><th>ch</th><th>state</th><th>raw +10 V</th><th>raw 0 V</th><th>mV/count</th><th>offset mV</th><th>deadband</th><th>raw now</th><th>mV cal</th><th>mV nominal</th></tr>';
for (const r of AI.calTableRows(rec, raws)) {
html += '<tr><td>' + r.ch + '</td><td class="' + (r.valid ? '' : 'bad') + '">' + esc(AI.calStepsText(rec.channels[r.ch - 1])) + '</td><td>' + f(r.rawOpen) + '</td><td>' + f(r.rawGnd) + '</td><td>' + f(r.a, 4) + '</td><td>' + f(r.b, 0) + '</td><td>' + f(r.deadbandMv) + ' mV</td><td>' + f(r.rawNow) + '</td><td>' + f(r.mvCal, 0) + '</td><td>' + f(r.mvNominal, 0) + '</td></tr>';
}
tbl.innerHTML = html;
row.style.display = '';
}
async function aiCalRead(quiet) {
if (!session || !session.connected) { if (!quiet) clog('analog cal: not connected', 'err'); return null; }
if (!aiHasCal()) {
if (!quiet) clog('analog cal: controller lacks ai_cal (0xC2 bit 6) — needs firmware feat/ai-calibration or later', 'warn');
const sum = $('cAiCalSum'); if (sum) sum.textContent = 'needs ai_cal firmware';
return null;
}
const r1 = await send(Wire.encodeGetAnalogCal(), 'GET_ANALOG_CAL 0xA7', { timeoutMs: 2000 });
const rec = r1 && Wire.decodeAnalogCal(r1);
if (!rec) { clog('analog cal: bad GET_ANALOG_CAL reply', 'err'); return null; }
const r2 = await send(Wire.encodeGetAnalogInRaw(), 'GET_ANALOG_IN_RAW 0xA5', { timeoutMs: 2000 });
const raws = r2 && Wire.decodeAnalogInRaw(r2);
aiCalRenderTable(rec, raws);
return rec;
}
async function aiCalAction(action, mv) {
// One calibration write at a time: the transport serialises bytes, but a
// double-click would queue a second EEPROM write behind the first.
if (aiUser.calibrating) { clog('analog cal: the previous calibration action is still in progress — wait for it', 'warn'); return; }
aiUser.calibrating = true;
try { await aiCalActionInner(action, mv); } finally { aiUser.calibrating = false; }
}
async function aiCalActionInner(action, mv) {
if (!session || !session.connected) { clog('analog cal: not connected', 'err'); return; }
if (!aiHasCal()) { clog('analog cal: controller lacks ai_cal (0xC2 bit 6) — needs firmware feat/ai-calibration or later', 'warn'); return; }
const ch = Number(($('cAiCalCh') || {}).value) === 2 ? 2 : 1;
let bytes;
try { bytes = Wire.encodeSetAnalogCal(ch, action, mv); }
catch (e) { clog('analog cal: ' + (e.message || e), 'err'); return; }
const label = 'SET_ANALOG_CAL 0xA6 ch ' + ch + ' ' + action + (action === 'setDeadband' ? ' ' + mv + ' mV' : '');
// 2 s: the controller averages 256 reads (~10 ms), writes EEPROM and the SD
// mirror — a slow card must not trip the 500 ms default and leave the UI stale.
const resp = await send(bytes, label, { timeoutMs: 2000 });
const rec = resp && Wire.decodeAnalogCal(resp);
if (!rec) return;
const c = rec.channels[ch - 1];
const line = AI.calLine(c);
clog('analog cal ch ' + ch + ': ' + AI.calStepsText(c) + (line ? ' · ' + line.a.toFixed(4) + ' mV/count · offset ' + Math.round(line.b) + ' mV' : '') + ' · deadband ' + c.deadbandMv + ' mV · ' + AI.describeCalSource(rec),
c.valid || action === 'clear' || action === 'setDeadband' ? 'info' : 'warn');
if (action === 'sampleOpen' && c.rawOpen < 3000) clog('analog cal: the +10 V point reads low (' + c.rawOpen + ' counts) — is the BNC really open? On a board without the LAB-209 rework both points saturate and the channel stays invalid', 'warn');
const bridge = Studio.session && Studio.session.bridge;
if (bridge && bridge.log) bridge.log({ event: 'analog_cal', channel: ch, action: action, deadband_mv: mv, record: rec });
const r2 = await send(Wire.encodeGetAnalogInRaw(), 'GET_ANALOG_IN_RAW 0xA5', { timeoutMs: 2000 });
aiCalRenderTable(rec, r2 && Wire.decodeAnalogInRaw(r2));
aiUser.lastFlags = undefined; // force the flags text to refresh on the next sample
if (aiPoller) aiPoller.tick();
}

// ---- command dispatch ----
const handlers = {
// display essentials
Expand Down Expand Up @@ -5367,6 +5446,15 @@ <h2 id="modalTitle">Import error</h2>
clog('clipboard unavailable — sweep CSV:\n' + csv, 'warn');
}
},
// Analog In calibration (S2) — writes are advanced-only (SAFE_BLOCKED_CMDS)
caicalread: () => { aiCalRead(false); },
caicalopen: () => guardDestructive(() => { aiCalAction('sampleOpen'); }),
caicalgnd: () => guardDestructive(() => { aiCalAction('sampleGround'); }),
caicaldb: () => guardDestructive(() => { aiCalAction('setDeadband', Math.round(num('cAiDeadband'))); }),
caicalclear: () => guardDestructive(() => {
if (!confirm('Forget the selected channel\'s calibration points? GET_ANALOG_IN and Mode 4 fall back to the nominal ±10 V scale.')) return;
aiCalAction('clear');
}),
cioget: async () => {
if (!Studio.capabilities || !Studio.capabilities.includes('io_ext')) {
clog('dio roles: controller lacks io_ext (0xC2 bit 5) — needs fw feat/dio-roles-ao-modes or later', 'warn');
Expand Down Expand Up @@ -15277,7 +15365,7 @@ <h2 id="modalTitle">Import error</h2>
step: 'Load the selected pattern, then walk through it frame by frame — good for checking exactly what each frame shows.',
test: 'Built-in test figures (full field, panel map, orientation, checker) and files streamed straight from this computer.',
io: 'Read the analog input, set the analog output, and drive the two digital connectors.',
ai: 'Both analog inputs, live. Cable Analog Out to an input and run the loopback sweep to check the wiring end to end; polling pauses by itself during a run.',
ai: 'Both analog inputs, live. Cable Analog Out to an input and run the loopback sweep to check the wiring; calibrate a channel in two steps (BNC open, then ground cap) so the readings and closed loop use this board’s true scale. Polling pauses by itself during a run.',
fw: 'Check what firmware the panels run, and program new firmware into every panel from here.',
fictrac: 'Closed loop: connect the bridge, load a pattern in Step frames, set the gain, then Activate — the fly steers the pattern.'
};
Expand Down
12 changes: 12 additions & 0 deletions docs/development/arena-studio-release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ The Studio's footer used to carry the full changelog inline; it now shows one li
history lives here. Newest first. (Per-session engineering detail stays in
`arena-studio-handover.md` and the design docs — this file is the user-facing what-changed list.)

## v0.75 (2026-09-07) · Analog-input calibration from the Console

- **Calibrate the analog inputs in two clicks** (Analog In panel → calibrate; needs a
controller with the `ai_cal` firmware). Unplug the BNC and sample the +10 V point,
fit the ground cap and sample the 0 V point — the controller averages 256 reads per
point, stores the record in its own memory (mirrored to the SD card), and from then
on both the live readout and Mode 4 closed loop use the board's true scale. Read shows
the stored record with the live raw counts on both scales; Clear returns a channel to
the nominal scale; a **deadband** (default 20 mV) stops a static input from creeping
the pattern in Mode 4. Writes are advanced-mode only and are recorded in the run log.
- Calibration commands use a 2 s timeout (the controller averages 256 reads and writes EEPROM + the SD mirror) and only one calibration action runs at a time (a second click while one is in flight is refused with a note).

## v0.74 (2026-09-07) · Console "Analog In" panel — live readout + loopback self-test

- **New Console tool: Analog In.** A left-rail button opens a live readout of both
Expand Down
Loading