Move the standalone GameSense scale lighting to TypeScript ahead of the
Max for Live work, so step 3 gets type declarations at the seam.
Build emits CommonJS at ES2020 into dist/, because Node for Max loads CJS
on a Node version we do not control. bin/apex7-scale.js becomes a plain-JS
launcher so `node bin/apex7-scale.js` keeps working; the CLI itself moves
to src/cli.ts.
Two modules make previously implicit structure explicit:
- src/protocol.ts, the GameSense wire types, so the transport and the
payload construction agree on shapes neither of them owns
- src/errors.ts, since `catch (err)` binds `unknown` under strict
Typing surfaced a few real fixes:
- ScaleLighting.started was a public field the CLI set by hand to make
--off work without start(); that is now release(), with started
private behind isStarted
- readAddress() trusted JSON.parse output; the address is now checked
for being a non-empty string
- res.statusCode is number|undefined, so the old `>= 200` comparison
coerced silently; a missing status now rejects
- parseIntervals claimed to dedupe and sort, and never did (comment
corrected; pitchClassesFor is what dedupes)
Behavior is otherwise unchanged: all 18 tests pass and --once, --off,
--demo, --key-test, the resident stdin loop and the error paths were
verified against tools/fake-gamesense.ts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
steelseries-live-scale
Light the notes of a musical scale on a SteelSeries Apex 7 using the GameSense SDK, mapped to Ableton Live's computer MIDI keyboard layout.
This is build step 1 of apex7-ableton-scale-lighting.md:
a standalone script that talks to GameSense directly. Live/Max for Live is not
involved yet — you pass the scale on the command line.
TypeScript, no runtime dependencies. Node 16+.
Quick start (Windows, with SteelSeries GG running)
npm install # also compiles src\*.ts to dist\
node bin\apex7-scale.js --root C --scale major
npm install builds via the prepare script; after editing any .ts file run
npm run build (or npm run typecheck for types only).
The board dims, the C-major keys light up (A S D F G H J K), the root is
orange. The process stays resident and heartbeats so the lighting sticks; you
can type new scales at the prompt:
F# dorian
Bb minor-pentatonic
quit
Ctrl+C blanks the board and hands lighting back to SteelSeries GG.
Verify the key mapping on hardware
node bin\apex7-scale.js --key-test
Lights one note key at a time, in order, printing the note it should be. Walk it
against the table in the brief — A=C, W=C#, … J=B, K=C (octave up).
Other modes
node bin\apex7-scale.js --demo 2 # cycle scales, 2s each
node bin\apex7-scale.js --intervals 0,3,5,6,7,10 --root A # raw intervals (blues)
node bin\apex7-scale.js --once # one frame, then exit
node bin\apex7-scale.js --off # blank + deregister
node bin\apex7-scale.js --list # known scale names
node bin\apex7-scale.js --help
Colors: --bg, --color, --root-color, --off-color, all #rrggbb.
node bin\apex7-scale.js --root D --scale minor --bg "#020208" --color "#ff00d0" --root-color "#ffffff"
How the lighting works
One GameSense event (SCALE) with 14 handlers, all in context-color mode:
| Handler | Zone | Frame key |
|---|---|---|
| background | every key except the 13 note keys (93 keys) | background |
| 13 × note key | one key each, by HID code | note-a … note-k |
Because every handler pulls its color from the event's frame, the handlers are
bound once at startup and a scale change is a single POST. That is what
step 3 needs when the scale changes mid-set.
Two design notes:
- Zones, not
bitmap. The brief suggested bitmap mode. Bitmap's 22×6 grid has no documented index→key table per keyboard model, whereas HID usage codes are exact. Painting all keys with the background zone solves the same problem bitmap was suggested for: the moment a GameSense event arrives the board enters GameSense mode and any key you don't address goes black. - The octave key
Kfollows pitch class 0 (same asA) but is only tinted as the root when C actually is the root.
Testing without hardware
tools/fake-gamesense.ts stands in for the GameSense server: it accepts the
real endpoints, resolves the bound handlers against each frame, and renders the
resulting keyboard as ANSI color in the terminal.
npm run stub -- --port 51000 # terminal 1
node bin/apex7-scale.js --address 127.0.0.1:51000 --root F# --scale dorian # terminal 2
--address (or GAMESENSE_ADDRESS) skips coreProps.json discovery entirely,
so this works on Linux too. --dry-run prints the payloads without sending.
Unit tests for the scale math, key mapping, handler shape and frame colors:
npm test
Layout
bin/apex7-scale.js launcher (plain JS) — runs dist/src/cli.js
src/cli.ts argument parsing, output, the resident stdin loop
src/protocol.ts GameSense wire types (Rgb, Frame, Handler, …)
src/hid.ts USB HID usage codes; the full-board key list
src/scale.ts scale presets, root/interval parsing, pitch class -> QWERTY key
src/gamesense.ts GameSense REST client (coreProps discovery, heartbeat, cleanup)
src/lighting.ts handler + frame construction
src/errors.ts `catch (err: unknown)` helpers
src/index.ts ScaleLighting — the API step 3 will call from Node for Max
tools/fake-gamesense.ts terminal simulator of the GameSense server
test/logic.test.ts unit tests
dist/ compiled CommonJS + .d.ts (gitignored)
tsconfig.json emits CommonJS at ES2020, because Node for Max loads CJS on a
Node version we do not control.
Reusing this from Max for Live (step 3)
src/index.ts is the seam — no CLI concerns in it, and it ships type
declarations alongside the compiled JS:
import { ScaleLighting } from 'steelseries-live-scale';
// or, from a Node for Max script: require('<repo>/dist/src')
const lights = new ScaleLighting();
await lights.start(); // register + bind + heartbeat
await lights.showScale(0, [0,2,4,5,7,9,11]); // root_note + scale_intervals from the LOM
// ...
await lights.stop();
showScale(root, intervals) takes exactly what Live 12's Song object exposes
as root_note and scale_intervals.
Troubleshooting
coreProps.json not found— SteelSeries GG isn't running, or is installed somewhere unusual. Expected at%PROGRAMDATA%\SteelSeries\SteelSeries Engine 3\coreProps.json.- Nothing lights up — check GG's Engine is enabled and that no other app holds an exclusive lighting profile.
- Lighting reverts after ~15s — that's GameSense's deactivation timeout;
it only happens with
--once, since resident mode heartbeats every 5s. - A key stays dark — it may not be in
src/hid.ts. Add its HID code there; keys not addressed by any handler go black in GameSense mode. apex7-scale is not built yet— runnpm run build.