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
26 changes: 26 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n>` 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
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/__tests__/osc-signals.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
44 changes: 42 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <addr> [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 <ws-url> Brain to connect to (e.g. ws://192.168.1.42:3333)
--shard <a-b> Cannon range this receiver drives (e.g. 0-24)
Expand All @@ -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[] = [
Expand Down Expand Up @@ -203,7 +221,8 @@ const KNOWN_COMMANDS = [
'keys',
'devices',
'env',
'doctor'
'doctor',
'signals'
];

/**
Expand Down Expand Up @@ -338,6 +357,21 @@ async function dispatchDevices(
} else unknownSub('devices', sub);
}

async function dispatchSignals(
args: string[],
flags: Flags,
prompter: Inquirerer,
nonInteractive: boolean
): Promise<void> {
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.
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading