diff --git a/packages/cli/README.md b/packages/cli/README.md index 068f273..87d9c28 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -97,6 +97,32 @@ than emitted. See [`docs/light-indexing.md`](../../docs/light-indexing.md). A one-laptop show skips all of it — `wavegrid projects osc` points straight at BEYOND or FB4. +### `wavegrid signals` — debugging what reaches Pangolin + +The show sends a whole grid 30 times a second, which is the wrong instrument for +"did BEYOND get anything at all?" and "which zone is fixture 7?". These send by +hand, over the project's configured OSC target (or `--host` / `--port`): + +```sh +wavegrid signals send /beyond/zone/0/livecontrol/red 255 # one message +wavegrid signals probe --zones 0-11 --hold 500 # light one zone at a time +wavegrid signals listen --port 7001 # print what arrives +``` + +`send` arguments are floats unless tagged (`i:3` integer, `s:text` string), since +an int where the receiver expects a float tends to be dropped silently. `probe` +uses the same encoders as the show and blacks out when it finishes; `listen` +binds a port, so pointing the show at `--host 127.0.0.1 --port ` shows the +exact stream the hardware would receive. + +For BEYOND to act on any of it, its OSC server must be enabled (BEYOND's +settings — check the port there rather than assuming; `projects osc` defaults to +7001 for BEYOND, 8000 for FB4) and the zone has to be under live control. + +This is aimed output on a configured target, unrelated to +[`tools/traffic`](../../tools/traffic), which stays passive — it observes +BEYOND ⇄ FB4 traffic and never transmits. + ### `wavegrid config` (or `wavegrid --print-config`) Resolves the configuration and prints it with per-key provenance so it is obvious diff --git a/packages/cli/__tests__/osc-signals.test.ts b/packages/cli/__tests__/osc-signals.test.ts new file mode 100644 index 0000000..b751851 --- /dev/null +++ b/packages/cli/__tests__/osc-signals.test.ts @@ -0,0 +1,44 @@ +import { resolveTarget } from '../src/commands/osc-signals'; + +const noTargets = { osc: {} }; + +describe('resolveTarget', () => { + it('uses the project BEYOND target, so a probe exercises the show path', () => { + const target = resolveTarget({}, { osc: { beyond: { host: '192.168.1.50', port: 7001 } } }); + expect(target).toEqual({ + kind: 'beyond', + host: '192.168.1.50', + port: 7001, + origin: 'project config (BEYOND)' + }); + }); + + it('falls back to FB4 when no BEYOND is configured', () => { + const target = resolveTarget({}, { osc: { fb4: { host: '192.168.1.77' } } }); + expect(target).toMatchObject({ kind: 'fb4', host: '192.168.1.77', port: 8000 }); + }); + + it('prefers flags over config, and defaults the port per kind', () => { + const config = { osc: { beyond: { host: '192.168.1.50', port: 7001 } } }; + expect(resolveTarget({ host: '127.0.0.1' }, config)).toMatchObject({ + kind: 'beyond', + host: '127.0.0.1', + port: 7001, + origin: 'flags' + }); + expect(resolveTarget({ host: '127.0.0.1', kind: 'fb4' }, config)).toMatchObject({ + kind: 'fb4', + port: 8000 + }); + expect(resolveTarget({ host: '127.0.0.1', port: '9000' }, config)).toMatchObject({ port: 9000 }); + }); + + it('overrides only the port when the host comes from config', () => { + const config = { osc: { beyond: { host: '192.168.1.50', port: 7001 } } }; + expect(resolveTarget({ port: 8000 }, config)).toMatchObject({ host: '192.168.1.50', port: 8000 }); + }); + + it('refuses to guess a target', () => { + expect(() => resolveTarget({}, noTargets)).toThrow(/No OSC target/); + }); +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index ee2bda6..5913dfe 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -43,6 +43,7 @@ "@wavegrid/discovery": "workspace:*", "@wavegrid/doctor": "workspace:*", "@wavegrid/layout": "workspace:*", + "@wavegrid/osc": "workspace:*", "@wavegrid/receiver": "workspace:*", "@wavegrid/server": "workspace:*", "@wavegrid/settings": "workspace:*", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index e2f92aa..a19b429 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -9,6 +9,12 @@ import { runInit } from './commands/init'; import { runKeysEnabled, runKeysList, runKeysNew, runKeysRemove } from './commands/keys'; import { pickCommand, pickSubcommand, printSubcommands, type SubCommand } from './commands/menu'; import { runOscSetup } from './commands/osc'; +import { + runSignalsListen, + runSignalsProbe, + runSignalsSend, + SIGNALS_USAGE +} from './commands/osc-signals'; import { runPrintConfig } from './commands/print-config'; import { runProjectsExport, runProjectsImport } from './commands/project-io'; import { runProjects, runUse } from './commands/projects'; @@ -63,6 +69,11 @@ ${c.bold('Run')} receiver Run a receiver only — connects to a brain, drives its shard doctor Diagnose this laptop + the whole installation +${c.bold('Signals')} — hand-driven OSC for debugging Pangolin + signals send [args] Send one OSC message to the configured target + signals probe [--zones 0-11] Light one zone/fixture at a time, to find the mapping + signals listen [--port 7001] Print every OSC message arriving on a port + ${c.bold('Receiver options')} --server Brain to connect to (e.g. ws://192.168.1.42:3333) --shard Cannon range this receiver drives (e.g. 0-24) @@ -84,7 +95,14 @@ const COMMANDS: SubCommand[] = [ { value: 'start', description: 'Run the active project — server + UI + receiver (one laptop)' }, { value: 'server', description: 'Run the brain only — server + UI + API + WebSocket (no receiver)' }, { value: 'receiver', description: 'Run a receiver only — connects to a brain, drives its shard' }, - { value: 'doctor', description: 'Diagnose this laptop + the whole installation' } + { value: 'doctor', description: 'Diagnose this laptop + the whole installation' }, + { value: 'signals', description: 'Hand-driven OSC: send one message, probe zones, listen to a port' } +]; + +const SIGNALS_SUBS: SubCommand[] = [ + { value: 'send', description: 'Send one OSC message to the configured target (or --host/--port)' }, + { value: 'probe', description: 'Light one zone/fixture at a time to find which laser is which' }, + { value: 'listen', description: 'Print every OSC message arriving on a port' } ]; const PROJECTS_SUBS: SubCommand[] = [ @@ -203,7 +221,8 @@ const KNOWN_COMMANDS = [ 'keys', 'devices', 'env', - 'doctor' + 'doctor', + 'signals' ]; /** @@ -338,6 +357,21 @@ async function dispatchDevices( } else unknownSub('devices', sub); } +async function dispatchSignals( + args: string[], + flags: Flags, + prompter: Inquirerer, + nonInteractive: boolean +): Promise { + const sub = (await resolveSub(args[0], 'signals', SIGNALS_SUBS, prompter, nonInteractive)) ?? undefined; + if (sub == null) return; + if (sub === 'send') await runSignalsSend(args.slice(1), flags); + else if (sub === 'probe' || sub === 'walk') await runSignalsProbe(flags); + else if (sub === 'listen' || sub === 'watch') await runSignalsListen(flags); + else if (sub === 'help') console.log(SIGNALS_USAGE); + else unknownSub('signals', sub); +} + function dispatchEnv(args: string[], flags: Flags): void { const sub = args[0]; // `env` has a single action (export); a bare `env` runs it deliberately. @@ -511,6 +545,12 @@ export async function run(argvInput: string[] = process.argv.slice(2)): Promise< case 'doctor': await runDoctor(flags); break; + case 'signals': + // `listen` runs until Ctrl-C; keep the prompter attached so the signal + // handling isn't fighting a half-closed stdin. + keepOpen = !nonInteractive && positionals[1] === 'listen'; + await dispatchSignals(positionals.slice(1), flags, prompter, nonInteractive); + break; default: console.log(c.red(`Unknown command: ${command}`)); console.log(HELP); diff --git a/packages/cli/src/commands/osc-signals.ts b/packages/cli/src/commands/osc-signals.ts new file mode 100644 index 0000000..0856a87 --- /dev/null +++ b/packages/cli/src/commands/osc-signals.ts @@ -0,0 +1,248 @@ +/** + * `wavegrid signals` — hand-driven OSC, for debugging what actually reaches + * Pangolin. + * + * The show sends a whole grid 30 times a second, which is the wrong instrument + * for "did BEYOND receive anything at all?" and "which zone is fixture 7?". + * These send one message at a time, walk fixtures one at a time, and can sit on + * a port and print the stream the hardware would see. + * + * Aiming: the project's configured OSC target by default (so this debugs the + * same path the show uses), or `--host/--port` for a one-off. + */ +import { loadWavegridConfig } from '@wavegrid/layout'; +import { + BeyondOscOutput, + type CannonState, + FB4OscOutput, + listenForOsc, + type OutputAdapter, + parseIndexRange, + parseOscArg, + probeGrid, + sendOscMessage +} from '@wavegrid/osc'; +import c from 'yanse'; + +import { type Flags, getStore, resolveProjectName } from '../project'; + +export const SIGNALS_USAGE = [ + ' Usage:', + ' wavegrid signals send /beyond/zone/0/livecontrol/red 255', + ' wavegrid signals send /FB4-12345/color_red 100 --host 192.168.1.50 --port 8000', + ' wavegrid signals probe [--zones 0-11] [--hold 500] [--hue 40]', + ' wavegrid signals listen [--port 7001]', + '', + ' Options:', + ' --host Override the project\'s OSC host', + ' --port Override the port (BEYOND 7001, FB4 8000)', + ' --dry-run Print what would be sent, send nothing', + '', + ' Arguments are floats unless tagged: `i:3` integer, `s:text` string.', + ' BEYOND needs its OSC server enabled (Settings \u2192 OSC) and the zone under', + ' live-control for these to have any visible effect.' +].join('\n'); + +interface Target { + kind: 'beyond' | 'fb4'; + host: string; + port: number; + /** Where the target came from, for the line printed before sending. */ + origin: string; +} + +function str(flags: Flags, key: string): string | undefined { + const v = flags[key]; + return typeof v === 'string' && v !== '' ? v : undefined; +} + +function num(flags: Flags, key: string): number | undefined { + const v = flags[key]; + if (typeof v === 'number') return v; + if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v))) return Number(v); + return undefined; +} + +/** + * Where to aim. Flags win, then the project's configured target — so a probe + * with no arguments exercises exactly what the show would drive. + */ +export function resolveTarget(flags: Flags, config: { osc: { beyond?: { host: string; port: number }; fb4?: { host: string; port?: number } } }): Target { + const host = str(flags, 'host'); + const port = num(flags, 'port'); + const kindFlag = str(flags, 'kind'); + + if (host) { + const kind = kindFlag === 'fb4' ? 'fb4' : 'beyond'; + return { kind, host, port: port ?? (kind === 'fb4' ? 8000 : 7001), origin: 'flags' }; + } + if (config.osc.beyond) { + return { + kind: 'beyond', + host: config.osc.beyond.host, + port: port ?? config.osc.beyond.port, + origin: 'project config (BEYOND)' + }; + } + if (config.osc.fb4) { + return { + kind: 'fb4', + host: config.osc.fb4.host, + port: port ?? config.osc.fb4.port ?? 8000, + origin: 'project config (FB4)' + }; + } + throw new Error( + 'No OSC target. Set one with `wavegrid projects osc`, or pass --host (and --port).' + ); +} + +function targetLine(target: Target): string { + return c.gray(` → ${target.kind.toUpperCase()} ${target.host}:${target.port} (${target.origin})`); +} + +function projectConfig(flags: Flags) { + const store = getStore(); + // Resolve the project so an explicit --project is honoured and a missing one + // fails loudly rather than debugging the wrong installation. + resolveProjectName(store, flags); + return loadWavegridConfig().config; +} + +export async function runSignalsSend(args: string[], flags: Flags): Promise { + const address = args[0]; + if (!address || !address.startsWith('/')) { + console.log(c.red(' An OSC address is required, e.g. /beyond/zone/0/livecontrol/red')); + console.log(SIGNALS_USAGE); + process.exitCode = 1; + return; + } + const target = resolveTarget(flags, projectConfig(flags)); + const oscArgs = args.slice(1).map(parseOscArg); + const rendered = oscArgs.map((a) => `${a.type[0]}:${a.value}`).join(' '); + + console.log(''); + console.log(` ${c.bold(address)} ${rendered}`); + console.log(targetLine(target)); + if (flags['dry-run']) { + console.log(c.yellow(' dry run — nothing sent')); + console.log(''); + return; + } + await sendOscMessage(target.host, target.port, address, oscArgs); + console.log(c.green(' ✓ sent')); + console.log(c.gray(' UDP is fire-and-forget: `wavegrid signals listen` on the far side is the')); + console.log(c.gray(' only proof it arrived.')); + console.log(''); +} + +/** + * One adapter for the whole walk, mapping each index to itself (BEYOND zone) or + * to the given serial (FB4) — the same encoders the show uses, so the bytes on + * the wire are the show's bytes. + */ +function probeAdapter( + target: Target, + zones: number[], + serial: string | undefined +): OutputAdapter & { connect: () => void } { + if (target.kind === 'fb4') { + if (!serial) throw new Error('FB4 addressing needs a 5-digit serial: --serial 12345'); + return new FB4OscOutput({ + host: target.host, + port: target.port, + serialMap: Object.fromEntries(zones.map((z) => [z, serial])), + sendEveryNFrames: 1 + }); + } + return new BeyondOscOutput({ + host: target.host, + port: target.port, + projectorMap: Object.fromEntries(zones.map((z) => [z, z])), + sendEveryNFrames: 1 + }); +} + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +/** Time given to the UDP socket to flush before it is closed. */ +const FLUSH_MS = 100; + +/** + * Light one fixture at a time so the operator can watch which physical laser + * answers to which index — the mapping question no capture can answer. + */ +export async function runSignalsProbe(flags: Flags): Promise { + const config = projectConfig(flags); + const target = resolveTarget(flags, config); + const zones = parseIndexRange(str(flags, 'zones') ?? '0-11'); + const hold = num(flags, 'hold') ?? 500; + const color: CannonState = { + h: num(flags, 'hue') ?? 40, + s: num(flags, 'sat') ?? 100, + b: num(flags, 'bright') ?? 100 + }; + const serial = str(flags, 'serial'); + const count = Math.max(...zones) + 1; + + console.log(''); + console.log(` Walking ${zones.length} ${target.kind === 'fb4' ? 'fixtures' : 'zones'}, ${hold}ms each`); + console.log(targetLine(target)); + if (flags['dry-run']) { + console.log(c.yellow(` dry run — would light ${zones.join(', ')} then blackout`)); + console.log(''); + return; + } + console.log(c.yellow(' Lasers will output. Make sure the room is safe.')); + console.log(''); + + const adapter = probeAdapter(target, zones, serial); + adapter.connect(); + try { + for (const zone of zones) { + console.log(` ${c.bold(String(zone))} on`); + adapter.send(probeGrid(count, zone, color)); + await sleep(hold); + } + adapter.send(probeGrid(count, null, color)); + // UDP sends are queued on the socket; closing it immediately drops the + // blackout frame, which is the one frame that must not be lost. + await sleep(FLUSH_MS); + } finally { + adapter.close(); + } + console.log(''); + console.log(c.green(' ✓ done — everything blacked out')); + console.log(''); +} + +/** Sit on a port and print what arrives. Ctrl-C to stop. */ +export async function runSignalsListen(flags: Flags): Promise { + const port = num(flags, 'port') ?? 7001; + const host = str(flags, 'host') ?? '0.0.0.0'; + let seen = 0; + + const listener = await listenForOsc(port, host, ({ address, args, from }) => { + seen += 1; + const rendered = args.map((a) => (typeof a === 'number' ? a.toFixed(2) : String(a))).join(' '); + console.log(` ${c.gray(from.padEnd(21))} ${c.bold(address)} ${rendered}`); + }); + + console.log(''); + console.log(` Listening for OSC on ${host}:${port} — Ctrl-C to stop`); + console.log(c.gray(' Point the show at this (`wavegrid projects osc beyond --host 127.0.0.1')); + console.log(c.gray(` --port ${port}`) + c.gray(') to see the exact stream the hardware gets.')); + console.log(''); + + await new Promise((resolve) => { + const stop = () => { + void listener.close().then(() => { + console.log(''); + console.log(c.green(` ✓ ${seen} message${seen === 1 ? '' : 's'} seen`)); + resolve(); + }); + }; + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + }); +} diff --git a/packages/osc/__tests__/debug.test.ts b/packages/osc/__tests__/debug.test.ts new file mode 100644 index 0000000..72f09a6 --- /dev/null +++ b/packages/osc/__tests__/debug.test.ts @@ -0,0 +1,75 @@ +import { listenForOsc, parseIndexRange, parseOscArg, probeGrid, sendOscMessage } from '../src/debug'; + +describe('parseOscArg', () => { + it('defaults numbers to float, because BEYOND ignores ints on float addresses', () => { + expect(parseOscArg('255')).toEqual({ type: 'float', value: 255 }); + expect(parseOscArg('0.5')).toEqual({ type: 'float', value: 0.5 }); + }); + + it('honours explicit type tags', () => { + expect(parseOscArg('i:3')).toEqual({ type: 'integer', value: 3 }); + expect(parseOscArg('i:3.7')).toEqual({ type: 'integer', value: 4 }); + expect(parseOscArg('f:2')).toEqual({ type: 'float', value: 2 }); + expect(parseOscArg('s:12')).toEqual({ type: 'string', value: '12' }); + }); + + it('treats non-numeric tokens as strings', () => { + expect(parseOscArg('on')).toEqual({ type: 'string', value: 'on' }); + }); + + it('rejects a tagged number that is not a number', () => { + expect(() => parseOscArg('i:red')).toThrow(/Not a number/); + }); +}); + +describe('parseIndexRange', () => { + it('expands ranges, lists, and singles', () => { + expect(parseIndexRange('0-3')).toEqual([0, 1, 2, 3]); + expect(parseIndexRange('0,3,7')).toEqual([0, 3, 7]); + expect(parseIndexRange('5')).toEqual([5]); + expect(parseIndexRange('0-2,9')).toEqual([0, 1, 2, 9]); + }); + + it('walks a descending range in reverse', () => { + expect(parseIndexRange('3-1')).toEqual([3, 2, 1]); + }); + + it('rejects nonsense', () => { + expect(() => parseIndexRange('')).toThrow(/No indices/); + expect(() => parseIndexRange('a')).toThrow(/Not an index/); + }); +}); + +describe('probeGrid', () => { + it('lights exactly one fixture and leaves the rest dark', () => { + const grid = probeGrid(3, 1, { h: 40, s: 100, b: 90 }); + expect(grid).toEqual([ + { h: 40, s: 100, b: 0 }, + { h: 40, s: 100, b: 90 }, + { h: 40, s: 100, b: 0 } + ]); + }); + + it('blacks everything out when nothing is lit', () => { + expect(probeGrid(2, null, { h: 40, s: 100, b: 90 }).every((c) => c.b === 0)).toBe(true); + }); +}); + +describe('sendOscMessage / listenForOsc', () => { + it('delivers a message over UDP with its arguments intact', async () => { + const received: Array<{ address: string; args: unknown[] }> = []; + const listener = await listenForOsc(41234, '127.0.0.1', (msg) => { + received.push({ address: msg.address, args: msg.args }); + }); + + await sendOscMessage('127.0.0.1', 41234, '/beyond/zone/0/livecontrol/red', [ + { type: 'float', value: 255 } + ]); + await new Promise((r) => setTimeout(r, 100)); + await listener.close(); + + expect(received).toHaveLength(1); + expect(received[0].address).toBe('/beyond/zone/0/livecontrol/red'); + expect(received[0].args).toEqual([255]); + }); +}); diff --git a/packages/osc/src/debug.ts b/packages/osc/src/debug.ts new file mode 100644 index 0000000..bcdfaa3 --- /dev/null +++ b/packages/osc/src/debug.ts @@ -0,0 +1,135 @@ +/** + * Hand-driven OSC, for debugging what actually reaches Pangolin. + * + * The adapters in this package send a whole grid, 30 times a second, which is + * exactly wrong for answering "did BEYOND get anything at all?" and "which zone + * is fixture 7?". These are the one-shot primitives: send a single message, + * light one fixture at a time, and listen to a port to see the stream as the + * hardware would see it. + * + * Output only ever goes where it is aimed — nothing here discovers or + * broadcasts to hardware on its own. + */ +import { Client, Message, Server } from 'node-osc'; + +import type { CannonState } from './osc-adapters'; + +/** A typed OSC argument. The adapters here send floats, so that is the default + * for anything numeric: an int where the receiver expects a float tends to be + * dropped without complaint, which is miserable to debug. */ +export interface OscArg { + type: 'float' | 'integer' | 'string'; + value: number | string; +} + +/** + * Parse a command-line token into a typed OSC argument. + * + * `255` → float 255 (BEYOND/FB4 want floats) + * `i:3` → integer 3 + * `f:0.5` → float 0.5 + * `s:hello` → string + * `hello` → string + */ +export function parseOscArg(token: string): OscArg { + const typed = /^([ifs]):(.*)$/.exec(token); + const raw = typed ? typed[2] : token; + const tag = typed?.[1]; + + if (tag === 's') return { type: 'string', value: raw }; + if (tag === 'i') { + const n = Number(raw); + if (!Number.isFinite(n)) throw new Error(`Not a number: ${token}`); + return { type: 'integer', value: Math.round(n) }; + } + if (tag === 'f') { + const n = Number(raw); + if (!Number.isFinite(n)) throw new Error(`Not a number: ${token}`); + return { type: 'float', value: n }; + } + const n = Number(raw); + if (raw.trim() !== '' && Number.isFinite(n)) return { type: 'float', value: n }; + return { type: 'string', value: raw }; +} + +/** + * Expand an index spec into indices: `0-11`, `0,3,7`, `5`, or a mix. + * Used for "which zones / fixtures should the probe walk". + */ +export function parseIndexRange(spec: string): number[] { + const out: number[] = []; + for (const part of spec.split(',')) { + const piece = part.trim(); + if (piece === '') continue; + const range = /^(\d+)\s*-\s*(\d+)$/.exec(piece); + if (range) { + const from = Number(range[1]); + const to = Number(range[2]); + const step = from <= to ? 1 : -1; + for (let i = from; step > 0 ? i <= to : i >= to; i += step) out.push(i); + continue; + } + const n = Number(piece); + if (!Number.isInteger(n) || n < 0) throw new Error(`Not an index or range: ${piece}`); + out.push(n); + } + if (out.length === 0) throw new Error(`No indices in "${spec}"`); + return out; +} + +/** A grid with exactly one fixture lit — the probe's frame. Passing `lit: null` + * gives the blackout frame the probe finishes on. */ +export function probeGrid(count: number, lit: number | null, color: CannonState): CannonState[] { + const dark: CannonState = { h: color.h, s: color.s, b: 0 }; + return Array.from({ length: count }, (_, i) => (i === lit ? { ...color } : { ...dark })); +} + +/** Send one OSC message and close the socket. */ +export async function sendOscMessage( + host: string, + port: number, + address: string, + args: OscArg[] +): Promise { + const client = new Client(host, port); + const msg = new Message(address); + for (const arg of args) msg.append(arg); + try { + await client.send(msg); + } finally { + await client.close(); + } +} + +export interface OscListener { + close: () => Promise; +} + +/** What a received message carries: address, arguments, and who sent it. */ +export interface ReceivedOsc { + address: string; + args: unknown[]; + from: string; +} + +/** + * Listen on a UDP port and report every OSC message that arrives. Point the + * receiver's OSC target at this to see the exact stream wavegrid emits, or bind + * the port BEYOND replies on to see what it says back. + */ +export async function listenForOsc( + port: number, + host: string, + onMessage: (msg: ReceivedOsc) => void +): Promise { + const server = new Server(port, host); + await new Promise((resolve, reject) => { + server.once('listening', resolve); + server.once('error', reject); + }); + server.on('message', (msg: unknown[], rinfo: { address: string; port: number }) => { + const [address, ...args] = msg; + onMessage({ address: String(address), args, from: `${rinfo.address}:${rinfo.port}` }); + }); + return { close: () => Promise.resolve(server.close()) }; +} diff --git a/packages/osc/src/index.ts b/packages/osc/src/index.ts index aa2bf2f..8606eea 100644 --- a/packages/osc/src/index.ts +++ b/packages/osc/src/index.ts @@ -1,3 +1,7 @@ +// Hand-driven OSC for debugging what reaches the hardware +export type { OscArg, OscListener, ReceivedOsc } from './debug'; +export { listenForOsc, parseIndexRange, parseOscArg, probeGrid, sendOscMessage } from './debug'; + // Color conversion export type { RGB } from './color'; export { hsbToRgb, hsbToRgb100, hsbToRgb255 } from './color'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25d7fa5..40f464a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: '@wavegrid/layout': specifier: workspace:* version: link:../layout/dist + '@wavegrid/osc': + specifier: workspace:* + version: link:../osc/dist '@wavegrid/receiver': specifier: workspace:* version: link:../receiver/dist