Skip to content

Commit 11ae8c9

Browse files
committed
Add generic G6 3x10 and 4x10 rigs
1 parent a1a0ee1 commit 11ae8c9

6 files changed

Lines changed: 209 additions & 3 deletions

File tree

configs/rigs/g6_3x10.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# G6 rig configuration — 3x10 (full 360°)
2+
#
3+
# General-purpose counterpart of cshl_g6_2x10.yaml for a three-row arena.
4+
# The browser drives the G6 controller directly over USB/serial. No external
5+
# camera, tracker, backlight, or temperature plugins are assumed.
6+
7+
format_version: "1.0"
8+
name: "G6 3x10"
9+
description: "Generic G6 3x10 arena (full 360°), controller over USB/serial, no external plugins"
10+
11+
arena: "G6_3x10"
12+
13+
controller:
14+
host: "0.0.0.0" # not used for serial/USB
15+
port: 62222
16+
17+
plugins: {}
18+
# AO (analog-out) is controller-native, not a rig plugin.
19+
20+
# Match the CSHL G6 arena-only controller I/O defaults.
21+
io:
22+
dio:
23+
- port: 1
24+
role: out_debug_framescan # HIGH during each frame's SPI transmission
25+
default: 0
26+
- port: 2
27+
role: off # declare in_trigger when a trigger source is actually cabled
28+
default: 0
29+
ai:
30+
role: off
31+
ao:
32+
role: programmable
33+
default: 5.0 # volts — this hardware idles at 5 V

configs/rigs/g6_4x10.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# G6 rig configuration — 4x10 (full 360°)
2+
#
3+
# General-purpose counterpart of cshl_g6_2x10.yaml for a four-row arena.
4+
# The browser drives the G6 controller directly over USB/serial. No external
5+
# camera, tracker, backlight, or temperature plugins are assumed.
6+
7+
format_version: "1.0"
8+
name: "G6 4x10"
9+
description: "Generic G6 4x10 arena (full 360°), controller over USB/serial, no external plugins"
10+
11+
arena: "G6_4x10"
12+
13+
controller:
14+
host: "0.0.0.0" # not used for serial/USB
15+
port: 62222
16+
17+
plugins: {}
18+
# AO (analog-out) is controller-native, not a rig plugin.
19+
20+
# Match the CSHL G6 arena-only controller I/O defaults.
21+
io:
22+
dio:
23+
- port: 1
24+
role: out_debug_framescan # HIGH during each frame's SPI transmission
25+
default: 0
26+
- port: 2
27+
role: off # declare in_trigger when a trigger source is actually cabled
28+
default: 0
29+
ai:
30+
role: off
31+
ao:
32+
role: programmable
33+
default: 5.0 # volts — this hardware idles at 5 V

configs/rigs/index.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
"version": 1,
44
"description": "Built-in rigs offered in the v3 Experiment Designer's Settings → Rig dropdown. Add a rig YAML alongside this file and a matching entry here.",
55
"rigs": [
6+
{
7+
"name": "g6_3x10",
8+
"label": "G6 — 3×10 (360°)",
9+
"path": "./configs/rigs/g6_3x10.yaml",
10+
"arena": "G6_3x10"
11+
},
12+
{
13+
"name": "g6_4x10",
14+
"label": "G6 — 4×10 (360°)",
15+
"path": "./configs/rigs/g6_4x10.yaml",
16+
"arena": "G6_4x10"
17+
},
618
{
719
"name": "cshl_g6_2x10_ball",
820
"label": "CSHL G6 — 2×10 fly-on-ball (FicTrac)",

js/arena-configs.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Arena Configurations
33
* Auto-generated from maDisplayTools/configs/arenas/ and arena_registry/
4-
* Last updated: 2026-07-05T00:22:25.965Z
4+
* Last updated: 2026-07-13T19:38:14.361Z
55
*
66
* DO NOT EDIT MANUALLY - regenerate with: node scripts/generate-arena-configs.js
77
*/
@@ -42,6 +42,19 @@ const STANDARD_CONFIGS = {
4242
"angle_offset_deg": 0
4343
}
4444
},
45+
"G6_3x10": {
46+
"label": "G6 (3×10) - 360°",
47+
"description": "Full G6 arena, 3 rows x 10 columns, 360 degree coverage",
48+
"arena": {
49+
"generation": "G6",
50+
"num_rows": 3,
51+
"num_cols": 10,
52+
"columns_installed": null,
53+
"orientation": "normal",
54+
"column_order": "cw",
55+
"angle_offset_deg": 0
56+
}
57+
},
4558
"G6_3x12of18": {
4659
"label": "G6 (3×18) - 240°",
4760
"description": "G6 arena, 3 rows, 12 of 18 columns installed (240 degree coverage)",
@@ -81,6 +94,19 @@ const STANDARD_CONFIGS = {
8194
"angle_offset_deg": 0
8295
}
8396
},
97+
"G6_4x10": {
98+
"label": "G6 (4×10) - 360°",
99+
"description": "Full G6 arena, 4 rows x 10 columns, 360 degree coverage",
100+
"arena": {
101+
"generation": "G6",
102+
"num_rows": 4,
103+
"num_cols": 10,
104+
"columns_installed": null,
105+
"orientation": "normal",
106+
"column_order": "cw",
107+
"angle_offset_deg": 0
108+
}
109+
},
84110
"G41_2x12_cw": {
85111
"label": "G4.1 (2×12) - 360°",
86112
"description": "G4.1 arena, 2 rows x 12 columns, 360 degree, CW, c0 at south",
@@ -188,7 +214,7 @@ const GENERATIONS = {
188214
const ARENA_REGISTRY = {
189215
'G4': { 1: 'G4_4x12', 2: 'G4_3x12of18' },
190216
'G4.1': { 1: 'G41_2x12_cw' },
191-
'G6': { 1: 'G6_2x10', 2: 'G6_2x8of10', 3: 'G6_3x12of18', 4: 'G6_3x16_full' }
217+
'G6': { 1: 'G6_2x10', 2: 'G6_2x8of10', 3: 'G6_3x12of18', 4: 'G6_3x16_full', 5: 'G6_3x10', 6: 'G6_4x10' }
192218
};
193219

194220
/**

pixi.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ sim = "python fictrac-bridge/fictrac_sim.py"
1515
# The full test suite. yaml is vendored (js/vendor/yaml), so no package install.
1616
# The v3 suite pulls in ESM modules that `import 'yaml'`; --import maps that bare
1717
# specifier to the vendored copy (tests/vendor-yaml.register.mjs).
18-
test = "node tests/validate-arena-calculations.js && node tests/test-protocol-roundtrip.js && node --import ./tests/vendor-yaml.register.mjs tests/test-protocol-roundtrip-v3.js && node tests/test-plugin-registry.js && node tests/test-arena-wire-g6.js && node tests/test-arena-link.js && node tests/test-arena-runner-g6.js && node tests/test-arena-session.js && node tests/test-fictrac-bridge-client.js && node tests/test-run-log.js && node tests/test-bin-classifier.js && node tests/test-pattern-set.js && node tests/test-pat-preview.js && node tests/test-studio-meta.js && node tests/test-studio-url-state.js && node tests/test-studio-runlog-adapter.js && node tests/test-studio-github.js && node tests/test-studio-canmutate.js && node tests/test-studio-protocols-index.js && node tests/test-kinematics.js && node tests/test-runlog-replay.js && python tests/test-bridge-behavior.py"
18+
test = "node tests/validate-arena-calculations.js && node tests/test-protocol-roundtrip.js && node --import ./tests/vendor-yaml.register.mjs tests/test-protocol-roundtrip-v3.js && node tests/test-plugin-registry.js && node tests/test-arena-wire-g6.js && node tests/test-arena-link.js && node tests/test-arena-runner-g6.js && node tests/test-arena-session.js && node tests/test-fictrac-bridge-client.js && node tests/test-run-log.js && node tests/test-bin-classifier.js && node tests/test-pattern-set.js && node tests/test-pat-preview.js && node tests/test-studio-meta.js && node tests/test-studio-url-state.js && node tests/test-studio-runlog-adapter.js && node tests/test-studio-github.js && node tests/test-studio-canmutate.js && node tests/test-studio-protocols-index.js && node tests/test-g6-tall-rigs.js && node tests/test-kinematics.js && node tests/test-runlog-replay.js && python tests/test-bridge-behavior.py"
1919
format = "prettier --write \"**/*.js\""
2020
format-check = "prettier --check \"**/*.js\""
2121

tests/test-g6-tall-rigs.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env node
2+
3+
'use strict';
4+
5+
const fs = require('fs');
6+
const path = require('path');
7+
const YAML = require('../js/vendor/yaml/browser/dist/index.js');
8+
const { getConfig, getArenaId, getArenaName } = require('../js/arena-configs.js');
9+
const PatEncoder = require('../js/pat-encoder.js');
10+
const parserModule = require('../js/pat-parser.js');
11+
const PatParser = parserModule.default || parserModule;
12+
13+
const ROOT = path.resolve(__dirname, '..');
14+
let passed = 0;
15+
let failed = 0;
16+
17+
function check(label, condition, detail = '') {
18+
if (condition) {
19+
passed++;
20+
console.log(` PASS ${label}${detail ? ` — ${detail}` : ''}`);
21+
} else {
22+
failed++;
23+
console.error(` FAIL ${label}${detail ? ` — ${detail}` : ''}`);
24+
}
25+
}
26+
27+
function readYaml(relativePath) {
28+
return YAML.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8'));
29+
}
30+
31+
function sharedRigFields(rig) {
32+
return {
33+
format_version: rig.format_version,
34+
controller: rig.controller,
35+
plugins: rig.plugins,
36+
io: rig.io
37+
};
38+
}
39+
40+
function strictRoundTrip(rows, arenaId) {
41+
const cols = 10;
42+
const pixelRows = rows * 20;
43+
const pixelCols = cols * 20;
44+
const frame = new Uint8Array(pixelRows * pixelCols);
45+
frame[0] = 1;
46+
frame[pixelCols - 1] = 2;
47+
frame[(pixelRows - 1) * pixelCols] = 3;
48+
frame[frame.length - 1] = 4;
49+
const encoded = PatEncoder.encode({
50+
generation: 'G6',
51+
gs_val: 16,
52+
numFrames: 1,
53+
rowCount: rows,
54+
colCount: cols,
55+
pixelRows,
56+
pixelCols,
57+
frames: [frame],
58+
stretchValues: [128],
59+
arena_id: arenaId,
60+
observer_id: 0
61+
});
62+
const parsed = PatParser.parsePatFile(encoded, { strict: true });
63+
return (
64+
parsed.rowCount === rows &&
65+
parsed.colCount === cols &&
66+
parsed.arena_id === arenaId &&
67+
Buffer.compare(Buffer.from(parsed.frames[0]), Buffer.from(frame)) === 0
68+
);
69+
}
70+
71+
console.log('=== G6 tall arena + rig proposal ===');
72+
73+
const base = readYaml('configs/rigs/cshl_g6_2x10.yaml');
74+
const index = JSON.parse(fs.readFileSync(path.join(ROOT, 'configs/rigs/index.json'), 'utf8'));
75+
76+
for (const rows of [3, 4]) {
77+
const arenaName = `G6_${rows}x10`;
78+
const rigName = `g6_${rows}x10`;
79+
const rigPath = `configs/rigs/${rigName}.yaml`;
80+
const expectedArenaId = rows + 2; // 3x10 -> 5; 4x10 -> 6
81+
const rig = readYaml(rigPath);
82+
const config = getConfig(arenaName);
83+
const entry = index.rigs.find((candidate) => candidate.name === rigName);
84+
85+
check(`${arenaName}: registered`, Boolean(config));
86+
check(`${arenaName}: geometry`, config?.arena?.num_rows === rows && config?.arena?.num_cols === 10);
87+
check(`${arenaName}: full grid`, config?.arena?.columns_installed === null);
88+
check(`${arenaName}: ID`, getArenaId('G6', arenaName) === expectedArenaId, String(expectedArenaId));
89+
check(`${arenaName}: ID round-trip`, getArenaName('G6', expectedArenaId) === arenaName);
90+
check(`${rigName}: arena reference`, rig.arena === arenaName);
91+
check(
92+
`${rigName}: CSHL capability parity`,
93+
JSON.stringify(sharedRigFields(rig)) === JSON.stringify(sharedRigFields(base))
94+
);
95+
check(`${rigName}: index entry`, Boolean(entry));
96+
check(`${rigName}: index arena`, entry?.arena === arenaName);
97+
check(`${rigName}: index path`, entry?.path === `./${rigPath}`);
98+
check(`${arenaName}: strict pattern round-trip`, strictRoundTrip(rows, expectedArenaId));
99+
}
100+
101+
console.log(`\n${passed} passed, ${failed} failed`);
102+
if (failed) process.exit(1);

0 commit comments

Comments
 (0)