wavegrid 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,22 +10,23 @@
10
10
  </a>
11
11
  </p>
12
12
 
13
- The **brain** of the Illuminate installation. Sits between the control layer (Canvas/Simulator) and the physical hardware (BEYOND/OSC).
13
+ The **brain** of the Wavegrid installation. Sits between the control layer (Canvas/Simulator) and the physical hardware (BEYOND/OSC via `@wavegrid/osc`).
14
14
 
15
15
  ## Design Principles
16
16
 
17
- 1. **Never jolt** — runs its own independent low-pass filter on all incoming state, so even if a client disconnects mid-transition, the output always flows smoothly
18
- 2. **Always alive** — on signal loss, gracefully falls back to ambient 3D sine wave animations instead of freezing or going dark
19
- 3. **Hardware bridge** — translates HSB grid state into OSC messages for BEYOND (future phase)
17
+ 1. **Never jolt** — runs its own independent low-pass filter on all incoming state
18
+ 2. **Always alive** — on signal loss, gracefully falls back to ambient 3D sine wave animations
19
+ 3. **Pluggable** — input and output are adapters; swap them for any protocol or hardware
20
+ 4. **Lean** — no hardware dependencies in the core; OSC lives in `@wavegrid/osc`
20
21
 
21
22
  ## Architecture
22
23
 
23
24
  ```
24
- Canvas ──ws──▶ Simulator ──ws──▶ Receiver ──osc──▶ BEYOND
25
+ Canvas ──ws──▶ Simulator ──ws──▶ Receiver ──adapter──▶ Hardware
25
26
 
26
27
  own LP filter
27
28
  sine fallback
28
- health monitor
29
+ shard support
29
30
  ```
30
31
 
31
32
  ## Usage
@@ -33,16 +34,29 @@ Canvas ──ws──▶ Simulator ──ws──▶ Receiver ──osc──▶
33
34
  ```sh
34
35
  pnpm dev:receiver
35
36
  # Connects to simulator at ws://localhost:3000
36
- # Outputs state to console (or OSC when configured)
37
+ # Outputs state to console (or hardware when configured)
37
38
  ```
38
39
 
39
- ## Fallback Behavior
40
+ ### With OSC hardware (requires `@wavegrid/osc`)
40
41
 
41
- When the upstream WebSocket connection drops:
42
- - The receiver continues running its own tick loop at 60fps
43
- - Current state smoothly transitions into a 3D sine wave pattern
44
- - Sine waves sweep through hue/brightness across the 7×7 grid
45
- - When connection restores, sine wave smoothly blends back to received state
42
+ ```sh
43
+ ROUTING_CONFIG=./routing.json pnpm dev:receiver
44
+ BEYOND_HOST=192.168.50.10 pnpm dev:receiver
45
+ ```
46
+
47
+ ### Programmatic
48
+
49
+ ```typescript
50
+ import { Receiver, WebSocketInput, ConsoleOutput } from 'wavegrid';
51
+
52
+ const receiver = new Receiver({
53
+ input: new WebSocketInput({ url: 'ws://localhost:3000' }),
54
+ output: new ConsoleOutput(),
55
+ numCannons: 49,
56
+ gridColumns: 7
57
+ });
58
+ receiver.start();
59
+ ```
46
60
 
47
61
  ## Configuration
48
62
 
@@ -51,5 +65,23 @@ When the upstream WebSocket connection drops:
51
65
  | `SIMULATOR_URL` | `ws://localhost:3000` | Upstream WebSocket |
52
66
  | `RECEIVER_ALPHA` | `0.06` | Low-pass filter smoothing (lower = smoother) |
53
67
  | `FALLBACK_DELAY` | `3000` | ms before switching to sine fallback |
54
- | `OSC_HOST` | — | BEYOND OSC target host (future) |
55
- | `OSC_PORT` | — | BEYOND OSC target port (future) |
68
+ | `WS_OUTPUT_PORT` | — | Optional WebSocket relay output port |
69
+ | `SHARD_START` | — | First cannon index (inclusive) |
70
+ | `SHARD_END` | — | Last cannon index (inclusive) |
71
+ | `NUM_CANNONS` | `49` | Total cannons in the grid |
72
+ | `GRID_COLUMNS` | `7` | Number of columns in the grid |
73
+ | `ROUTING_CONFIG` | — | Path to JSON routing config (enables OSC) |
74
+ | `BEYOND_HOST` | — | Quick single-target BEYOND OSC host |
75
+ | `BEYOND_PORT` | `9000` | BEYOND OSC port |
76
+ | `FB4_HOST` | — | Quick single-target FB4 OSC host |
77
+ | `FB4_PORT` | `8000` | FB4 OSC port |
78
+
79
+ ## Built-in Adapters
80
+
81
+ | Adapter | Direction | Purpose |
82
+ |---------|-----------|---------|
83
+ | `WebSocketInput` | Input | Connects to upstream simulator/server |
84
+ | `ConsoleOutput` | Output | Logs frames to console (dev/debug) |
85
+ | `CallbackOutput` | Output | Calls your function each tick |
86
+ | `MultiOutput` | Output | Fans out to N adapters at once |
87
+ | `WebSocketOutput` | Output | Broadcasts to downstream WS clients |
package/esm/fallback.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * The waves create organic, slowly evolving color movement — like the
9
9
  * installation is breathing on its own.
10
10
  */
11
- import { GRID_SIZE, NUM_CANNONS } from './filter';
11
+ import { DEFAULT_GRID_COLUMNS } from './filter';
12
12
  export const DEFAULT_FALLBACK_CONFIG = {
13
13
  baseHue: 220,
14
14
  hueSpread: 60,
@@ -28,15 +28,18 @@ export const DEFAULT_FALLBACK_CONFIG = {
28
28
  * 2. Secondary wave moves perpendicular (brightness)
29
29
  * 3. Tertiary slow wave modulates saturation for depth
30
30
  */
31
- export function computeFallbackFrame(grid, tick, config = DEFAULT_FALLBACK_CONFIG) {
31
+ export function computeFallbackFrame(grid, tick, config = DEFAULT_FALLBACK_CONFIG, gridColumns = DEFAULT_GRID_COLUMNS) {
32
32
  const { baseHue, hueSpread, brightnessMin, brightnessMax, spatialFreq, timeFreq, timeFreq2 } = config;
33
33
  const brightRange = brightnessMax - brightnessMin;
34
- for (let i = 0; i < NUM_CANNONS; i++) {
35
- const row = Math.floor(i / GRID_SIZE);
36
- const col = i % GRID_SIZE;
34
+ const cols = Math.max(1, gridColumns);
35
+ for (let i = 0; i < grid.length; i++) {
36
+ const row = Math.floor(i / cols);
37
+ const col = i % cols;
38
+ const maxCol = Math.max(1, cols - 1);
39
+ const maxRow = Math.max(1, Math.ceil(grid.length / cols) - 1);
37
40
  // Normalize to -1..1
38
- const nx = (col / (GRID_SIZE - 1)) * 2 - 1;
39
- const ny = (row / (GRID_SIZE - 1)) * 2 - 1;
41
+ const nx = (col / maxCol) * 2 - 1;
42
+ const ny = (row / maxRow) * 2 - 1;
40
43
  // Primary diagonal wave → hue
41
44
  const wave1 = Math.sin((nx + ny) * spatialFreq * Math.PI + tick * timeFreq);
42
45
  // Secondary perpendicular wave → brightness
package/esm/filter.js CHANGED
@@ -5,11 +5,15 @@
5
5
  * smoothing so that even if the upstream connection drops mid-transition,
6
6
  * the output to hardware never jolts.
7
7
  */
8
- export const NUM_CANNONS = 49;
9
- export const GRID_SIZE = 7;
8
+ export const DEFAULT_NUM_CANNONS = 49;
9
+ export const DEFAULT_GRID_COLUMNS = 7;
10
10
  export const DEFAULT_RECEIVER_ALPHA = 0.06;
11
- export function createFilteredGrid() {
12
- return Array.from({ length: NUM_CANNONS }, () => ({
11
+ /**
12
+ * Create a filtered grid of the given size.
13
+ * Defaults to 49 cannons for the 7×7 Civic Center installation.
14
+ */
15
+ export function createFilteredGrid(numCannons = DEFAULT_NUM_CANNONS) {
16
+ return Array.from({ length: numCannons }, () => ({
13
17
  h: 220,
14
18
  s: 90,
15
19
  b: 80,
package/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { CallbackOutput, ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
2
- export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
2
+ export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
3
3
  export { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
4
4
  export { Receiver } from './receiver';
package/esm/main.js CHANGED
@@ -1,24 +1,86 @@
1
1
  /**
2
2
  * Receiver entry point.
3
3
  *
4
- * Demonstrates the adapter pattern configure input and output
5
- * adapters via environment variables, then start the receiver.
4
+ * Configure input and output adapters via environment variables:
5
+ *
6
+ * SIMULATOR_URL WebSocket upstream (default ws://localhost:3000)
7
+ * RECEIVER_ALPHA LP filter alpha (default 0.06)
8
+ * FALLBACK_DELAY Ms before sine fallback (default 3000)
9
+ * WS_OUTPUT_PORT Optional WebSocket relay port
10
+ * SHARD_START/END Optional cannon index range
11
+ * NUM_CANNONS Total cannons in grid (default 49)
12
+ * GRID_COLUMNS Number of columns (default 7)
13
+ * ROUTING_CONFIG Path to a JSON routing config file (enables OSC output)
14
+ * BEYOND_HOST/PORT Quick single-target BEYOND OSC (alternative to routing file)
15
+ * FB4_HOST/PORT Quick single-target FB4 OSC (alternative to routing file)
6
16
  */
17
+ import * as fs from 'fs';
7
18
  import { ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
19
+ import { DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS } from './filter';
8
20
  import { Receiver } from './receiver';
9
21
  const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
10
22
  const ALPHA = parseFloat(process.env.RECEIVER_ALPHA || '0.06');
11
23
  const FALLBACK_DELAY = parseInt(process.env.FALLBACK_DELAY || '3000', 10);
12
24
  const WS_OUTPUT_PORT = process.env.WS_OUTPUT_PORT ? parseInt(process.env.WS_OUTPUT_PORT, 10) : undefined;
25
+ const NUM_CANNONS = process.env.NUM_CANNONS ? parseInt(process.env.NUM_CANNONS, 10) : DEFAULT_NUM_CANNONS;
26
+ const GRID_COLUMNS = process.env.GRID_COLUMNS ? parseInt(process.env.GRID_COLUMNS, 10) : DEFAULT_GRID_COLUMNS;
27
+ let shard;
28
+ if (process.env.SHARD_START !== undefined && process.env.SHARD_END !== undefined) {
29
+ shard = {
30
+ start: parseInt(process.env.SHARD_START, 10),
31
+ end: parseInt(process.env.SHARD_END, 10)
32
+ };
33
+ }
13
34
  // ─── Input adapter ───
14
35
  const input = new WebSocketInput({ url: SIMULATOR_URL });
15
36
  // ─── Output adapter(s) ───
16
37
  const outputs = [new ConsoleOutput()];
38
+ const outputLabels = ['Console'];
39
+ // OSC adapters are in @wavegrid/osc — try to load them if env vars are set
40
+ const hasOscConfig = process.env.ROUTING_CONFIG || process.env.BEYOND_HOST || process.env.FB4_HOST;
41
+ if (hasOscConfig) {
42
+ try {
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const osc = require('@wavegrid/osc');
45
+ if (process.env.ROUTING_CONFIG) {
46
+ const raw = fs.readFileSync(process.env.ROUTING_CONFIG, 'utf8');
47
+ const routingConfig = JSON.parse(raw);
48
+ const routed = osc.createRoutedOutput(routingConfig);
49
+ routed.connect();
50
+ outputs.push(routed);
51
+ outputLabels.push(`Routed OSC → [${routed.targetNames.join(', ')}]`);
52
+ }
53
+ if (process.env.BEYOND_HOST) {
54
+ const host = process.env.BEYOND_HOST;
55
+ const port = parseInt(process.env.BEYOND_PORT || '9000', 10);
56
+ const projectorMap = {};
57
+ for (let i = 0; i < NUM_CANNONS; i++)
58
+ projectorMap[i] = i;
59
+ const beyond = new osc.BeyondOscOutput({ host, port, projectorMap });
60
+ beyond.connect();
61
+ outputs.push(beyond);
62
+ outputLabels.push(`BEYOND OSC → ${host}:${port}`);
63
+ }
64
+ if (process.env.FB4_HOST) {
65
+ const host = process.env.FB4_HOST;
66
+ const port = parseInt(process.env.FB4_PORT || '8000', 10);
67
+ console.warn(' ⚠ FB4_HOST set but no serial map — use ROUTING_CONFIG for per-cannon FB4 mapping');
68
+ const fb4 = new osc.FB4OscOutput({ host, port, serialMap: {} });
69
+ fb4.connect();
70
+ outputs.push(fb4);
71
+ outputLabels.push(`FB4 OSC → ${host}:${port}`);
72
+ }
73
+ }
74
+ catch (e) {
75
+ console.warn(' ⚠ OSC env vars set but @wavegrid/osc is not installed. Run: pnpm add @wavegrid/osc');
76
+ }
77
+ }
17
78
  let wsOutput = null;
18
79
  if (WS_OUTPUT_PORT) {
19
80
  wsOutput = new WebSocketOutput({ port: WS_OUTPUT_PORT });
20
81
  wsOutput.listen();
21
82
  outputs.push(wsOutput);
83
+ outputLabels.push(`WebSocket :${WS_OUTPUT_PORT}`);
22
84
  }
23
85
  const output = outputs.length === 1 ? outputs[0] : new MultiOutput(outputs);
24
86
  // ─── Receiver ───
@@ -26,17 +88,22 @@ const receiver = new Receiver({
26
88
  input,
27
89
  output,
28
90
  alpha: ALPHA,
29
- fallbackDelay: FALLBACK_DELAY
91
+ fallbackDelay: FALLBACK_DELAY,
92
+ shard,
93
+ numCannons: NUM_CANNONS,
94
+ gridColumns: GRID_COLUMNS
30
95
  });
31
96
  console.log('');
32
- console.log(' \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E');
33
- console.log(' \u2502 Illuminate \u00B7 Receiver \u2502');
34
- console.log(' \u2502 the brain \u2502');
35
- console.log(' \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F');
97
+ console.log(' ╭──────────────────────────────────────╮');
98
+ console.log(' Wavegrid · Receiver');
99
+ console.log(' the brain ');
100
+ console.log(' ╰──────────────────────────────────────╯');
36
101
  console.log('');
37
- console.log(` \u2192 Input: WebSocket @ ${SIMULATOR_URL}`);
38
- console.log(` \u2192 Output: Console${wsOutput ? ` + WebSocket :${WS_OUTPUT_PORT}` : ''}`);
39
- console.log(` \u2192 Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
102
+ console.log(` Input: WebSocket @ ${SIMULATOR_URL}`);
103
+ console.log(` Output: ${outputLabels.join(' + ')}`);
104
+ console.log(` Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
105
+ console.log(` → Grid: ${NUM_CANNONS} cannons (${GRID_COLUMNS} columns)`);
106
+ console.log(` → Shard: ${shard ? `cannons ${shard.start}–${shard.end} (${shard.end - shard.start + 1} of ${NUM_CANNONS})` : `all cannons (no shard)`}`);
40
107
  console.log('');
41
108
  receiver.start();
42
109
  // Graceful shutdown
package/esm/receiver.js CHANGED
@@ -12,14 +12,16 @@
12
12
  */
13
13
  import { ConsoleOutput, WebSocketInput } from './adapters';
14
14
  import { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
15
- import { applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
15
+ import { applyUpstreamState, createFilteredGrid, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
16
16
  export const DEFAULT_RECEIVER_CONFIG = {
17
17
  input: new WebSocketInput({ url: 'ws://localhost:3000' }),
18
18
  output: new ConsoleOutput(),
19
19
  alpha: DEFAULT_RECEIVER_ALPHA,
20
20
  fallbackDelay: 3000,
21
21
  fallback: DEFAULT_FALLBACK_CONFIG,
22
- tickMs: 1000 / 60
22
+ tickMs: 1000 / 60,
23
+ numCannons: DEFAULT_NUM_CANNONS,
24
+ gridColumns: DEFAULT_GRID_COLUMNS
23
25
  };
24
26
  export class Receiver {
25
27
  config;
@@ -32,17 +34,21 @@ export class Receiver {
32
34
  _running = false;
33
35
  constructor(config = {}) {
34
36
  this.config = { ...DEFAULT_RECEIVER_CONFIG, ...config };
35
- this.grid = createFilteredGrid();
37
+ this.grid = createFilteredGrid(this.config.numCannons);
36
38
  }
37
39
  get status() { return this._status; }
38
40
  get fallbackActive() { return this._fallbackActive; }
39
- /** Get the current output state (after filtering). */
41
+ /** Get the current output state (after filtering and sharding). */
40
42
  getOutputState() {
41
- return this.grid.map(c => ({
43
+ const full = this.grid.map(c => ({
42
44
  h: c.h,
43
45
  s: c.s,
44
46
  b: c.b
45
47
  }));
48
+ const shard = this.config.shard;
49
+ if (!shard)
50
+ return full;
51
+ return full.slice(shard.start, shard.end + 1);
46
52
  }
47
53
  /** Start the receiver — connects input and begins the tick loop. */
48
54
  start() {
@@ -98,7 +104,7 @@ export class Receiver {
98
104
  }
99
105
  // If fallback is active, compute sine wave targets
100
106
  if (this._fallbackActive) {
101
- computeFallbackFrame(this.grid, this.tick, this.config.fallback);
107
+ computeFallbackFrame(this.grid, this.tick, this.config.fallback, this.config.gridColumns);
102
108
  }
103
109
  // Always tick the low-pass filter — this ensures smooth output
104
110
  // whether receiving data, transitioning to fallback, or in fallback
package/fallback.d.ts CHANGED
@@ -36,4 +36,4 @@ export declare const DEFAULT_FALLBACK_CONFIG: FallbackConfig;
36
36
  * 2. Secondary wave moves perpendicular (brightness)
37
37
  * 3. Tertiary slow wave modulates saturation for depth
38
38
  */
39
- export declare function computeFallbackFrame(grid: FilteredCannon[], tick: number, config?: FallbackConfig): void;
39
+ export declare function computeFallbackFrame(grid: FilteredCannon[], tick: number, config?: FallbackConfig, gridColumns?: number): void;
package/fallback.js CHANGED
@@ -32,15 +32,18 @@ exports.DEFAULT_FALLBACK_CONFIG = {
32
32
  * 2. Secondary wave moves perpendicular (brightness)
33
33
  * 3. Tertiary slow wave modulates saturation for depth
34
34
  */
35
- function computeFallbackFrame(grid, tick, config = exports.DEFAULT_FALLBACK_CONFIG) {
35
+ function computeFallbackFrame(grid, tick, config = exports.DEFAULT_FALLBACK_CONFIG, gridColumns = filter_1.DEFAULT_GRID_COLUMNS) {
36
36
  const { baseHue, hueSpread, brightnessMin, brightnessMax, spatialFreq, timeFreq, timeFreq2 } = config;
37
37
  const brightRange = brightnessMax - brightnessMin;
38
- for (let i = 0; i < filter_1.NUM_CANNONS; i++) {
39
- const row = Math.floor(i / filter_1.GRID_SIZE);
40
- const col = i % filter_1.GRID_SIZE;
38
+ const cols = Math.max(1, gridColumns);
39
+ for (let i = 0; i < grid.length; i++) {
40
+ const row = Math.floor(i / cols);
41
+ const col = i % cols;
42
+ const maxCol = Math.max(1, cols - 1);
43
+ const maxRow = Math.max(1, Math.ceil(grid.length / cols) - 1);
41
44
  // Normalize to -1..1
42
- const nx = (col / (filter_1.GRID_SIZE - 1)) * 2 - 1;
43
- const ny = (row / (filter_1.GRID_SIZE - 1)) * 2 - 1;
45
+ const nx = (col / maxCol) * 2 - 1;
46
+ const ny = (row / maxRow) * 2 - 1;
44
47
  // Primary diagonal wave → hue
45
48
  const wave1 = Math.sin((nx + ny) * spatialFreq * Math.PI + tick * timeFreq);
46
49
  // Secondary perpendicular wave → brightness
package/filter.d.ts CHANGED
@@ -15,10 +15,14 @@ export interface FilteredCannon extends CannonState {
15
15
  targetS: number;
16
16
  targetB: number;
17
17
  }
18
- export declare const NUM_CANNONS = 49;
19
- export declare const GRID_SIZE = 7;
18
+ export declare const DEFAULT_NUM_CANNONS = 49;
19
+ export declare const DEFAULT_GRID_COLUMNS = 7;
20
20
  export declare const DEFAULT_RECEIVER_ALPHA = 0.06;
21
- export declare function createFilteredGrid(): FilteredCannon[];
21
+ /**
22
+ * Create a filtered grid of the given size.
23
+ * Defaults to 49 cannons for the 7×7 Civic Center installation.
24
+ */
25
+ export declare function createFilteredGrid(numCannons?: number): FilteredCannon[];
22
26
  /**
23
27
  * Shortest angular distance on the hue circle.
24
28
  */
package/filter.js CHANGED
@@ -7,16 +7,20 @@
7
7
  * the output to hardware never jolts.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.DEFAULT_RECEIVER_ALPHA = exports.GRID_SIZE = exports.NUM_CANNONS = void 0;
10
+ exports.DEFAULT_RECEIVER_ALPHA = exports.DEFAULT_GRID_COLUMNS = exports.DEFAULT_NUM_CANNONS = void 0;
11
11
  exports.createFilteredGrid = createFilteredGrid;
12
12
  exports.angleDelta = angleDelta;
13
13
  exports.tickFilter = tickFilter;
14
14
  exports.applyUpstreamState = applyUpstreamState;
15
- exports.NUM_CANNONS = 49;
16
- exports.GRID_SIZE = 7;
15
+ exports.DEFAULT_NUM_CANNONS = 49;
16
+ exports.DEFAULT_GRID_COLUMNS = 7;
17
17
  exports.DEFAULT_RECEIVER_ALPHA = 0.06;
18
- function createFilteredGrid() {
19
- return Array.from({ length: exports.NUM_CANNONS }, () => ({
18
+ /**
19
+ * Create a filtered grid of the given size.
20
+ * Defaults to 49 cannons for the 7×7 Civic Center installation.
21
+ */
22
+ function createFilteredGrid(numCannons = exports.DEFAULT_NUM_CANNONS) {
23
+ return Array.from({ length: numCannons }, () => ({
20
24
  h: 220,
21
25
  s: 90,
22
26
  b: 80,
package/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export type { AddressMapping, InputAdapter, MappedOutputConfig, OutputAdapter } from './adapters';
2
2
  export { CallbackOutput, ConsoleOutput, MultiOutput, WebSocketInput, WebSocketOutput } from './adapters';
3
3
  export type { CannonState, FilteredCannon } from './filter';
4
- export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
4
+ export { angleDelta, applyUpstreamState, createFilteredGrid, DEFAULT_GRID_COLUMNS, DEFAULT_NUM_CANNONS, DEFAULT_RECEIVER_ALPHA, tickFilter } from './filter';
5
5
  export type { FallbackConfig } from './fallback';
6
6
  export { computeFallbackFrame, DEFAULT_FALLBACK_CONFIG } from './fallback';
7
- export type { ReceiverConfig, ReceiverState, ReceiverStatus } from './receiver';
7
+ export type { ReceiverConfig, ReceiverState, ReceiverStatus, ShardConfig } from './receiver';
8
8
  export { Receiver } from './receiver';
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Receiver = exports.DEFAULT_FALLBACK_CONFIG = exports.computeFallbackFrame = exports.tickFilter = exports.DEFAULT_RECEIVER_ALPHA = exports.createFilteredGrid = exports.applyUpstreamState = exports.angleDelta = exports.WebSocketOutput = exports.WebSocketInput = exports.MultiOutput = exports.ConsoleOutput = exports.CallbackOutput = void 0;
3
+ exports.Receiver = exports.DEFAULT_FALLBACK_CONFIG = exports.computeFallbackFrame = exports.tickFilter = exports.DEFAULT_RECEIVER_ALPHA = exports.DEFAULT_NUM_CANNONS = exports.DEFAULT_GRID_COLUMNS = exports.createFilteredGrid = exports.applyUpstreamState = exports.angleDelta = exports.WebSocketOutput = exports.WebSocketInput = exports.MultiOutput = exports.ConsoleOutput = exports.CallbackOutput = void 0;
4
4
  var adapters_1 = require("./adapters");
5
5
  Object.defineProperty(exports, "CallbackOutput", { enumerable: true, get: function () { return adapters_1.CallbackOutput; } });
6
6
  Object.defineProperty(exports, "ConsoleOutput", { enumerable: true, get: function () { return adapters_1.ConsoleOutput; } });
@@ -11,6 +11,8 @@ var filter_1 = require("./filter");
11
11
  Object.defineProperty(exports, "angleDelta", { enumerable: true, get: function () { return filter_1.angleDelta; } });
12
12
  Object.defineProperty(exports, "applyUpstreamState", { enumerable: true, get: function () { return filter_1.applyUpstreamState; } });
13
13
  Object.defineProperty(exports, "createFilteredGrid", { enumerable: true, get: function () { return filter_1.createFilteredGrid; } });
14
+ Object.defineProperty(exports, "DEFAULT_GRID_COLUMNS", { enumerable: true, get: function () { return filter_1.DEFAULT_GRID_COLUMNS; } });
15
+ Object.defineProperty(exports, "DEFAULT_NUM_CANNONS", { enumerable: true, get: function () { return filter_1.DEFAULT_NUM_CANNONS; } });
14
16
  Object.defineProperty(exports, "DEFAULT_RECEIVER_ALPHA", { enumerable: true, get: function () { return filter_1.DEFAULT_RECEIVER_ALPHA; } });
15
17
  Object.defineProperty(exports, "tickFilter", { enumerable: true, get: function () { return filter_1.tickFilter; } });
16
18
  var fallback_1 = require("./fallback");
package/main.d.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  /**
2
2
  * Receiver entry point.
3
3
  *
4
- * Demonstrates the adapter pattern configure input and output
5
- * adapters via environment variables, then start the receiver.
4
+ * Configure input and output adapters via environment variables:
5
+ *
6
+ * SIMULATOR_URL WebSocket upstream (default ws://localhost:3000)
7
+ * RECEIVER_ALPHA LP filter alpha (default 0.06)
8
+ * FALLBACK_DELAY Ms before sine fallback (default 3000)
9
+ * WS_OUTPUT_PORT Optional WebSocket relay port
10
+ * SHARD_START/END Optional cannon index range
11
+ * NUM_CANNONS Total cannons in grid (default 49)
12
+ * GRID_COLUMNS Number of columns (default 7)
13
+ * ROUTING_CONFIG Path to a JSON routing config file (enables OSC output)
14
+ * BEYOND_HOST/PORT Quick single-target BEYOND OSC (alternative to routing file)
15
+ * FB4_HOST/PORT Quick single-target FB4 OSC (alternative to routing file)
6
16
  */
7
17
  export {};
package/main.js CHANGED
@@ -2,25 +2,120 @@
2
2
  /**
3
3
  * Receiver entry point.
4
4
  *
5
- * Demonstrates the adapter pattern configure input and output
6
- * adapters via environment variables, then start the receiver.
5
+ * Configure input and output adapters via environment variables:
6
+ *
7
+ * SIMULATOR_URL WebSocket upstream (default ws://localhost:3000)
8
+ * RECEIVER_ALPHA LP filter alpha (default 0.06)
9
+ * FALLBACK_DELAY Ms before sine fallback (default 3000)
10
+ * WS_OUTPUT_PORT Optional WebSocket relay port
11
+ * SHARD_START/END Optional cannon index range
12
+ * NUM_CANNONS Total cannons in grid (default 49)
13
+ * GRID_COLUMNS Number of columns (default 7)
14
+ * ROUTING_CONFIG Path to a JSON routing config file (enables OSC output)
15
+ * BEYOND_HOST/PORT Quick single-target BEYOND OSC (alternative to routing file)
16
+ * FB4_HOST/PORT Quick single-target FB4 OSC (alternative to routing file)
7
17
  */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
8
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
+ const fs = __importStar(require("fs"));
9
53
  const adapters_1 = require("./adapters");
54
+ const filter_1 = require("./filter");
10
55
  const receiver_1 = require("./receiver");
11
56
  const SIMULATOR_URL = process.env.SIMULATOR_URL || 'ws://localhost:3000';
12
57
  const ALPHA = parseFloat(process.env.RECEIVER_ALPHA || '0.06');
13
58
  const FALLBACK_DELAY = parseInt(process.env.FALLBACK_DELAY || '3000', 10);
14
59
  const WS_OUTPUT_PORT = process.env.WS_OUTPUT_PORT ? parseInt(process.env.WS_OUTPUT_PORT, 10) : undefined;
60
+ const NUM_CANNONS = process.env.NUM_CANNONS ? parseInt(process.env.NUM_CANNONS, 10) : filter_1.DEFAULT_NUM_CANNONS;
61
+ const GRID_COLUMNS = process.env.GRID_COLUMNS ? parseInt(process.env.GRID_COLUMNS, 10) : filter_1.DEFAULT_GRID_COLUMNS;
62
+ let shard;
63
+ if (process.env.SHARD_START !== undefined && process.env.SHARD_END !== undefined) {
64
+ shard = {
65
+ start: parseInt(process.env.SHARD_START, 10),
66
+ end: parseInt(process.env.SHARD_END, 10)
67
+ };
68
+ }
15
69
  // ─── Input adapter ───
16
70
  const input = new adapters_1.WebSocketInput({ url: SIMULATOR_URL });
17
71
  // ─── Output adapter(s) ───
18
72
  const outputs = [new adapters_1.ConsoleOutput()];
73
+ const outputLabels = ['Console'];
74
+ // OSC adapters are in @wavegrid/osc — try to load them if env vars are set
75
+ const hasOscConfig = process.env.ROUTING_CONFIG || process.env.BEYOND_HOST || process.env.FB4_HOST;
76
+ if (hasOscConfig) {
77
+ try {
78
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
79
+ const osc = require('@wavegrid/osc');
80
+ if (process.env.ROUTING_CONFIG) {
81
+ const raw = fs.readFileSync(process.env.ROUTING_CONFIG, 'utf8');
82
+ const routingConfig = JSON.parse(raw);
83
+ const routed = osc.createRoutedOutput(routingConfig);
84
+ routed.connect();
85
+ outputs.push(routed);
86
+ outputLabels.push(`Routed OSC → [${routed.targetNames.join(', ')}]`);
87
+ }
88
+ if (process.env.BEYOND_HOST) {
89
+ const host = process.env.BEYOND_HOST;
90
+ const port = parseInt(process.env.BEYOND_PORT || '9000', 10);
91
+ const projectorMap = {};
92
+ for (let i = 0; i < NUM_CANNONS; i++)
93
+ projectorMap[i] = i;
94
+ const beyond = new osc.BeyondOscOutput({ host, port, projectorMap });
95
+ beyond.connect();
96
+ outputs.push(beyond);
97
+ outputLabels.push(`BEYOND OSC → ${host}:${port}`);
98
+ }
99
+ if (process.env.FB4_HOST) {
100
+ const host = process.env.FB4_HOST;
101
+ const port = parseInt(process.env.FB4_PORT || '8000', 10);
102
+ console.warn(' ⚠ FB4_HOST set but no serial map — use ROUTING_CONFIG for per-cannon FB4 mapping');
103
+ const fb4 = new osc.FB4OscOutput({ host, port, serialMap: {} });
104
+ fb4.connect();
105
+ outputs.push(fb4);
106
+ outputLabels.push(`FB4 OSC → ${host}:${port}`);
107
+ }
108
+ }
109
+ catch (e) {
110
+ console.warn(' ⚠ OSC env vars set but @wavegrid/osc is not installed. Run: pnpm add @wavegrid/osc');
111
+ }
112
+ }
19
113
  let wsOutput = null;
20
114
  if (WS_OUTPUT_PORT) {
21
115
  wsOutput = new adapters_1.WebSocketOutput({ port: WS_OUTPUT_PORT });
22
116
  wsOutput.listen();
23
117
  outputs.push(wsOutput);
118
+ outputLabels.push(`WebSocket :${WS_OUTPUT_PORT}`);
24
119
  }
25
120
  const output = outputs.length === 1 ? outputs[0] : new adapters_1.MultiOutput(outputs);
26
121
  // ─── Receiver ───
@@ -28,17 +123,22 @@ const receiver = new receiver_1.Receiver({
28
123
  input,
29
124
  output,
30
125
  alpha: ALPHA,
31
- fallbackDelay: FALLBACK_DELAY
126
+ fallbackDelay: FALLBACK_DELAY,
127
+ shard,
128
+ numCannons: NUM_CANNONS,
129
+ gridColumns: GRID_COLUMNS
32
130
  });
33
131
  console.log('');
34
- console.log(' \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E');
35
- console.log(' \u2502 Illuminate \u00B7 Receiver \u2502');
36
- console.log(' \u2502 the brain \u2502');
37
- console.log(' \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F');
132
+ console.log(' ╭──────────────────────────────────────╮');
133
+ console.log(' Wavegrid · Receiver');
134
+ console.log(' the brain ');
135
+ console.log(' ╰──────────────────────────────────────╯');
38
136
  console.log('');
39
- console.log(` \u2192 Input: WebSocket @ ${SIMULATOR_URL}`);
40
- console.log(` \u2192 Output: Console${wsOutput ? ` + WebSocket :${WS_OUTPUT_PORT}` : ''}`);
41
- console.log(` \u2192 Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
137
+ console.log(` Input: WebSocket @ ${SIMULATOR_URL}`);
138
+ console.log(` Output: ${outputLabels.join(' + ')}`);
139
+ console.log(` Alpha: ${ALPHA} Fallback delay: ${FALLBACK_DELAY}ms`);
140
+ console.log(` → Grid: ${NUM_CANNONS} cannons (${GRID_COLUMNS} columns)`);
141
+ console.log(` → Shard: ${shard ? `cannons ${shard.start}–${shard.end} (${shard.end - shard.start + 1} of ${NUM_CANNONS})` : `all cannons (no shard)`}`);
42
142
  console.log('');
43
143
  receiver.start();
44
144
  // Graceful shutdown
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "wavegrid",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "author": "Dan Lynch <pyramation@gmail.com>",
5
- "description": "Receiver brain — resilient state engine with independent low-pass filter, 3D sine wave fallback, and OSC bridge",
5
+ "description": "Receiver brain — resilient state engine with independent low-pass filter, 3D sine wave fallback, and pluggable adapter pattern",
6
6
  "main": "index.js",
7
7
  "module": "esm/index.js",
8
8
  "types": "index.d.ts",
@@ -31,10 +31,10 @@
31
31
  },
32
32
  "keywords": [
33
33
  "laser",
34
- "osc",
35
34
  "receiver",
36
35
  "brain",
37
- "7x7"
36
+ "adapter",
37
+ "wavegrid"
38
38
  ],
39
39
  "dependencies": {
40
40
  "ws": "^8.18.0"
@@ -43,5 +43,5 @@
43
43
  "@types/ws": "^8.5.13",
44
44
  "makage": "^0.3.0"
45
45
  },
46
- "gitHead": "60ed9683c2b2003a811d0298a086ee481cd310b1"
46
+ "gitHead": "1fc162ccd34d4b7e3594d26ee20043fb24a14ec6"
47
47
  }
package/receiver.d.ts CHANGED
@@ -13,6 +13,12 @@
13
13
  import { InputAdapter, OutputAdapter } from './adapters';
14
14
  import { FallbackConfig } from './fallback';
15
15
  import { CannonState, FilteredCannon } from './filter';
16
+ export interface ShardConfig {
17
+ /** First cannon index (inclusive). */
18
+ start: number;
19
+ /** Last cannon index (inclusive). */
20
+ end: number;
21
+ }
16
22
  export interface ReceiverConfig {
17
23
  /** Input adapter — where state comes from. */
18
24
  input: InputAdapter;
@@ -26,6 +32,17 @@ export interface ReceiverConfig {
26
32
  fallback: FallbackConfig;
27
33
  /** Tick rate in ms (default 1000/60 ~ 16.67ms). */
28
34
  tickMs: number;
35
+ /**
36
+ * Optional shard — only output cannons in this index range.
37
+ * When omitted, the receiver outputs all cannons.
38
+ * The LP filter still processes the full grid; sharding only
39
+ * affects which cannons are sent to the output adapter.
40
+ */
41
+ shard?: ShardConfig;
42
+ /** Total number of cannons in the grid. Default 49. */
43
+ numCannons: number;
44
+ /** Number of columns in the grid (for fallback spatial mapping). Default 7. */
45
+ gridColumns: number;
29
46
  }
30
47
  export declare const DEFAULT_RECEIVER_CONFIG: ReceiverConfig;
31
48
  export type ReceiverStatus = 'connected' | 'reconnecting' | 'fallback';
@@ -48,7 +65,7 @@ export declare class Receiver {
48
65
  constructor(config?: Partial<ReceiverConfig>);
49
66
  get status(): ReceiverStatus;
50
67
  get fallbackActive(): boolean;
51
- /** Get the current output state (after filtering). */
68
+ /** Get the current output state (after filtering and sharding). */
52
69
  getOutputState(): CannonState[];
53
70
  /** Start the receiver — connects input and begins the tick loop. */
54
71
  start(): void;
package/receiver.js CHANGED
@@ -22,7 +22,9 @@ exports.DEFAULT_RECEIVER_CONFIG = {
22
22
  alpha: filter_1.DEFAULT_RECEIVER_ALPHA,
23
23
  fallbackDelay: 3000,
24
24
  fallback: fallback_1.DEFAULT_FALLBACK_CONFIG,
25
- tickMs: 1000 / 60
25
+ tickMs: 1000 / 60,
26
+ numCannons: filter_1.DEFAULT_NUM_CANNONS,
27
+ gridColumns: filter_1.DEFAULT_GRID_COLUMNS
26
28
  };
27
29
  class Receiver {
28
30
  config;
@@ -35,17 +37,21 @@ class Receiver {
35
37
  _running = false;
36
38
  constructor(config = {}) {
37
39
  this.config = { ...exports.DEFAULT_RECEIVER_CONFIG, ...config };
38
- this.grid = (0, filter_1.createFilteredGrid)();
40
+ this.grid = (0, filter_1.createFilteredGrid)(this.config.numCannons);
39
41
  }
40
42
  get status() { return this._status; }
41
43
  get fallbackActive() { return this._fallbackActive; }
42
- /** Get the current output state (after filtering). */
44
+ /** Get the current output state (after filtering and sharding). */
43
45
  getOutputState() {
44
- return this.grid.map(c => ({
46
+ const full = this.grid.map(c => ({
45
47
  h: c.h,
46
48
  s: c.s,
47
49
  b: c.b
48
50
  }));
51
+ const shard = this.config.shard;
52
+ if (!shard)
53
+ return full;
54
+ return full.slice(shard.start, shard.end + 1);
49
55
  }
50
56
  /** Start the receiver — connects input and begins the tick loop. */
51
57
  start() {
@@ -101,7 +107,7 @@ class Receiver {
101
107
  }
102
108
  // If fallback is active, compute sine wave targets
103
109
  if (this._fallbackActive) {
104
- (0, fallback_1.computeFallbackFrame)(this.grid, this.tick, this.config.fallback);
110
+ (0, fallback_1.computeFallbackFrame)(this.grid, this.tick, this.config.fallback, this.config.gridColumns);
105
111
  }
106
112
  // Always tick the low-pass filter — this ensures smooth output
107
113
  // whether receiving data, transitioning to fallback, or in fallback