Skip to content
Merged
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
2 changes: 1 addition & 1 deletion arena_studio.html
Original file line number Diff line number Diff line change
Expand Up @@ -2917,7 +2917,7 @@ <h2 id="modalTitle">Import error</h2>
</div>

<footer id="footer">
<span class="foot-left">Arena Studio v0.70 | 2026-09-04 12:14 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.71 | 2026-09-04 16:39 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
10 changes: 10 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,16 @@ 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.71 (2026-09-04) · Closed-loop apply is reset at run start and abort

- **No more error floods at the start of a run.** If FicTrac closed-loop "apply"
had been left on (Console use, or a run that aborted mid-trial), the next run
pushed ball-tracking frames into its opening Mode-2 step and the controller
rejected every one — hundreds of error lines in the first seconds (seen on
rig03-sr, 2026-09-04). The runner now forces apply off at sequence start, at
sequence end, and whenever a run aborts or the link drops. Protocol data and
timing are unchanged.

## v0.70 (2026-09-04) · Metadata pick-lists restored; course age/sex/fly# lists load; token guidance

- **The Experimenter dropdown is populated again when no data repo is signed in.**
Expand Down
20 changes: 20 additions & 0 deletions js/arena-runner-g6.js
Original file line number Diff line number Diff line change
Expand Up @@ -858,9 +858,23 @@ var ArenaRunnerG6 = (function () {
this._active = false;
this._conditionName = null;
this._clearLedActivator(); // guarded: no-op send when the link is gone
this._disarmClosedLoop();
this._emit = null;
}

/** Force the shared bridge's closed-loop apply OFF (idempotent, never
* throws). Called at sequence start, at sequence end/abort, and on
* disconnect so a stale apply=true from Console use or an aborted run
* cannot drive frames into the next run's non-Mode-3 steps. */
_disarmClosedLoop() {
if (!this._bridge || typeof this._bridge.setApply !== 'function') return;
try {
this._bridge.setApply(false);
} catch (_) {
/* best-effort */
}
}

// ---- conditional LED activation (install / teardown) --------------
// Installed by a Mode-3 trialParams carrying led_activation; driven by
// the bridge's per-frame 'applied' event; sends SET_AO_VOLTAGE only on
Expand Down Expand Up @@ -1004,6 +1018,11 @@ var ArenaRunnerG6 = (function () {

this._active = true;
this._abort = false;
// A stale closed-loop apply (left on by Console use or an aborted run)
// would push FicTrac frames into the opening Mode-2 step — the firmware
// rejects each 0x70 with status 1 and the log fills with errors
// (rig03-sr, 2026-09-04: 304 rejects in the first 3 s). Start clean.
this._disarmClosedLoop();
const summary = {
completed: false,
aborted: false,
Expand Down Expand Up @@ -1204,6 +1223,7 @@ var ArenaRunnerG6 = (function () {
this._conditionName = null;
this._resolveSleep();
this._clearLedActivator(); // LED off + stop gating on completion/abort
this._disarmClosedLoop(); // never leak apply=true into the next run
try {
if (this._link && this._link.connected) {
await this._link.send(this._wire.encodeStop());
Expand Down
126 changes: 124 additions & 2 deletions tests/test-arena-runner-g6.js
Original file line number Diff line number Diff line change
Expand Up @@ -1275,9 +1275,11 @@ async function main() {
}
});
check('bridge.connect called once', bridge.connectCalls, 1);
// Runner disarms at sequence start and again at sequence end, so the
// protocol's own true/false sits between two safety falses.
checkBool(
'apply toggled true then false',
JSON.stringify(bridge.applyStates) === JSON.stringify([true, false]),
'apply disarmed at start, toggled true then false, disarmed at end',
JSON.stringify(bridge.applyStates) === JSON.stringify([false, true, false, false]),
bridge.applyStates.join(',')
);
checkBool(
Expand All @@ -1297,6 +1299,126 @@ async function main() {
check('closed-loop timing = 2s (fictrac ops add no time)', slept, 2000);
}

console.log('\n=== FicTrac closed-loop: stale apply is disarmed before the first step ===');
{
// Regression for rig03-sr 2026-09-04: apply left ON from earlier Console use
// pushed 0x70 frames into the opening Mode-2 step (304 firmware rejects).
const order = [];
const link = makeFakeLink();
const origSend = link.send.bind(link);
link.send = (bytes) => {
order.push('send:0x' + bytes[1].toString(16));
return origSend(bytes);
};
const bridge = {
apply: true, // stale state from before the run
connect() {},
disconnect() {},
setApply(on) {
this.apply = !!on;
order.push('apply:' + this.apply);
},
setConfig() {},
log() {}
};
const runner = new Runner.ArenaRunner(link, Wire, bridge);
const steps = [{ kind: 'ref', conditionName: 'bg', label: 'bg', seqIdx: 0, dur: 1 }];
const conditionsByName = new Map([
[
'bg',
{
name: 'bg',
commands: [
{
type: 'controller',
command_name: 'trialParams',
mode: 2,
frame_rate: 10,
gain: 0,
frame_index: 0,
duration: 1,
pattern: 'p'
},
{ type: 'wait', duration: 1 }
]
}
]
]);
const summary = await runner.runSequence({
steps,
conditionsByName,
resolvePatternId: () => 1,
sleep: () => Promise.resolve()
});
check('run completed', summary.completed, true);
check('first bridge/link action is apply:false', order[0], 'apply:false');
checkBool(
'apply:false precedes the first controller send',
order.indexOf('apply:false') < order.findIndex((o) => o.startsWith('send:')),
order.slice(0, 3).join(' → ')
);
check('apply is OFF after the run', bridge.apply, false);
}

console.log('\n=== FicTrac closed-loop: abort mid-trial leaves apply OFF ===');
{
const link = makeFakeLink();
const bridge = {
apply: false,
states: [],
connect() {},
disconnect() {},
setApply(on) {
this.apply = !!on;
this.states.push(!!on);
},
setConfig() {},
log() {}
};
const runner = new Runner.ArenaRunner(link, Wire, bridge);
const steps = [{ kind: 'ref', conditionName: 'cl', label: 'cl', seqIdx: 0, dur: 20 }];
const conditionsByName = new Map([
[
'cl',
{
name: 'cl',
commands: [
{
type: 'controller',
command_name: 'trialParams',
mode: 3,
frame_rate: 0,
gain: 0,
frame_index: 0,
duration: 20,
pattern: 'p'
},
{ type: 'plugin', plugin_name: 'fictrac', command_name: 'startClosedLoop' },
{ type: 'wait', duration: 20 },
{ type: 'plugin', plugin_name: 'fictrac', command_name: 'stopClosedLoop' }
]
}
]
]);
let applyDuringWait = null;
const summary = await runner.runSequence({
steps,
conditionsByName,
resolvePatternId: () => 1,
fictracPluginNames: new Set(['fictrac']),
sleep: () => {
// Mid-closed-loop the apply must be ON; then the link drops.
applyDuringWait = bridge.apply;
runner.abort();
return Promise.resolve();
}
});
check('apply was ON during the closed-loop wait', applyDuringWait, true);
check('run reported aborted', summary.aborted, true);
check('apply is OFF after the abort', bridge.apply, false);
check('last apply state recorded is false', bridge.states[bridge.states.length - 1], false);
}

console.log('\n=== Summary ===');
console.log(`${totalChecks - failures} / ${totalChecks} checks passed`);
process.exit(failures === 0 ? 0 : 1);
Expand Down