# globcon

A fully-typed Node.js / TypeScript client for the **globcon** protocol, used to control [DirectOut](https://www.directout.eu/) devices such as the **PRODIGY.MP**.

Connect over the network, read and write any device setting with full type-safety, manage crosspoint routing, drive the built-in tone generators, and subscribe to live telemetry — over a connection that heals itself.

```ts
import { GlobconClient } from "@bitfocusas/globcon";

const device = new GlobconClient({ ip: "172.27.27.225" });
await device.connect();

console.log(device.state?.device_info.name); // "PRODIGY-SPARE"

// Route SLOT1 In 1 -> NETWORK1 Out 1
await device.routing.connect(/* dest */ 128, /* source */ 384);

await device.destroy();
```

---

## Features

- **Typed end-to-end** — the entire device state tree is typed; `get`/`set` validate the path and infer the value type at compile time.
- **Self-healing connection** — connects, and on any drop/error/timeout reconnects every second, forever, until you call `destroy()`. Meter subscriptions are remembered and re-applied automatically.
- **High-level helpers** — routing, input/output labels, mic-pre gain/phantom/pad, output gain/mute, and tone generators — no magic numbers required.
- **Low-level escape hatch** — typed `get(path)` / `set(path, value)` reach any setting in the tree.
- **Live telemetry** — async `update` events (fan, status, signal-present) and raw meter frames.
- **Multi-model ready** — built around a model/schema registry. Ships with `DirectOut-Prodigy-MP`; other DirectOut boxes that speak the same protocol can be added without touching the core.
- **Zero runtime dependencies.**

## Install

```bash
npm install @bitfocusas/globcon
```

Requires **Node.js ≥ 22**. ESM only.

## Quick start

```ts
import { GlobconClient } from "@bitfocusas/globcon";

const device = new GlobconClient({
  ip: "172.27.27.225",
  // model defaults to "DirectOut-Prodigy-MP"
});

// connect() starts the resilient loop and resolves once connected.
// It also fetches the full device state into `device.state`.
await device.connect();

const state = await device.getState();
console.log(state.device_info.model);          // "PRODIGY.MP"
console.log(state.ip_config.current_address);   // "172.27.27.225"

// ... do work ...

await device.destroy(); // stops reconnecting, closes sockets, removes listeners
```

The connection keeps itself alive. If the device reboots or the network blips, the client reconnects automatically and refreshes its cached state — you don't need to do anything. Call `destroy()` when you're done so the process can exit.

## Crosspoint routing

Routing connects a **source** (an input) to a **destination** (an output). `-1` means "unrouted".

```ts
// By index:
await device.routing.connect(130, 7);  // destination 130 takes from source 7
await device.routing.disconnect(130);  // clear destination 130

// Read the current source feeding a destination (from cached state):
device.routing.source(130); // => 7, or -1 if unrouted
```

### Routing by label

Indices are stable but not memorable. Look them up from the labels:

```ts
const state = await device.getState();
const inIndex  = (name: string) => state.settings.input_labels.indexOf(name);
const outIndex = (name: string) => state.settings.output_labels.indexOf(name);

// Route SLOT1 In 1..8 to the Network 1 stream (NETWORK1 Out 1..8):
for (let ch = 1; ch <= 8; ch++) {
  await device.routing.connect(
    outIndex(`NETWORK1 Out ${ch}`),
    inIndex(`SLOT1 In ${ch}`),
  );
}
```

## Input / output labels

```ts
const state = await device.getState();

// Read (arrays indexed by port):
state.settings.input_labels[0];   // "MADI1 In 1"
state.settings.output_labels[0];  // "MADI1 Out 1"

// Convenience lookups via the routing API:
device.routing.inputLabel(384);   // "SLOT1 In 1"
device.routing.outputLabel(128);  // "NETWORK1 Out 1"

// Rename an output:
await device.output(0).setLabel("Main L");
```

## Mic-pre channels (slots)

Per-channel analog gain, 48 V phantom, and pad on the input slots:

```ts
await device.slot(0).channel(0).setGain(47);     // dB
await device.slot(0).channel(0).setPhantom(true); // 48 V on
await device.slot(0).channel(0).setPad(true);     // input pad on
```

## Outputs

```ts
await device.output(5).setGain(-3.5); // dB
await device.output(5).mute();
await device.output(5).unmute();
await device.output(5).setLabel("Booth");
```

## Tone generators

Two of each: sine, pink noise, white noise (index `0` or `1`).

```ts
// Sine (has frequency):
await device.sineGen(0).setFrequency(500); // Hz
await device.sineGen(0).setGain(-20);      // dB
await device.sineGen(0).enable();

// Noise:
await device.pinkNoise(0).setGain(-12);
await device.whiteNoise(1).disable();
```

The generators are routable sources. On the PRODIGY.MP the internal source indices are: `448` BLDS, `450` Sine 1, `451` Sine 2, `452` White 1, `453` White 2, `454` Pink 1, `455` Pink 2. So to send Sine 1 to NETWORK1 Out 1:

```ts
await device.sineGen(0).setFrequency(500);
await device.sineGen(0).enable();
await device.routing.connect(128 /* NETWORK1 Out 1 */, 450 /* Sine 1 */);
```

## Low-level typed access

Every setting is reachable with a typed path. The path is checked against the device's state tree, and the value type is inferred:

```ts
// Read one value:
const gain = await device.get(["settings", "output_gain", 0]); // number

// Write one value:
await device.set(["settings", "easy_routing", 128], 450);
await device.set(["settings", "sine_generator", 0, "f"], 1000);
```

Invalid paths are a compile error. If the device rejects a write at runtime, the promise rejects with a `GlobconError`:

```ts
import { GlobconError } from "@bitfocusas/globcon";

try {
  await device.set(["settings", "easy_routing", 128], 450);
} catch (err) {
  if (err instanceof GlobconError) console.error("device refused:", err.message);
}
```

## Events

`GlobconClient` is an `EventEmitter`:

```ts
device.on("ready", () => console.log("connected + state loaded"));
device.on("reconnecting", () => console.log("link lost, retrying…"));
device.on("reconnected", () => console.log("back online"));

// The device pushes state changes unsolicited:
device.on("update", (payload) => console.log("changed:", payload));
// e.g. { fan: { power: 27.27 } }
//      { status: { input_manager: [[0, { signals: [...] }]] } }
```

| Event | Payload | When |
|-------|---------|------|
| `connect` | – | a connection is established |
| `ready` | – | connected and initial state fetched |
| `reconnecting` | – | a retry has been scheduled |
| `reconnected` | – | a reconnect (after a prior connect) succeeded |
| `disconnect` | – | the connection dropped |
| `state` | full state | full state (re)loaded |
| `update` | partial state | device pushed a change (applied to `device.state`) |
| `meter` | `{ stream, frame }` | a meter frame arrived (see below) |
| `error` | `Error` | a transport-level error |

## Meters (advanced)

Meter frames are delivered raw as `Float32Array`s. Tell the client which channels you care about; it opens the stream, and re-subscribes automatically after a reconnect.

```ts
device.on("meter", ({ stream, frame }) => {
  // stream: 5011 | 5002 ; frame: Float32Array
});
device.meters.watch([0, 1, 2]);
device.meters.watched();  // [0, 1, 2]
device.meters.unwatch([2]);
```

> The per-frame float layout is device-specific and not fully decoded — frames are exposed as-is. For "is there signal?" the `status.input_manager.signals[].silence` flags from `update` events are simpler and reliable.

## Configuration

```ts
new GlobconClient({
  ip: "172.27.27.225",      // required
  model: "DirectOut-Prodigy-MP", // default
  retryIntervalMs: 1000,    // reconnect interval (no backoff)
  requestTimeoutMs: 5000,   // per-request ack timeout
  pingIntervalMs: 5000,     // keepalive ping
  fetchStateOnConnect: true,// fetch full state on (re)connect
});
```

## How it works

globcon speaks newline-delimited JSON over TCP (port 5003 for control). The device models its entire configuration as one big tree; this client mirrors it as a typed object, sends `set`/`get` commands addressed by path, correlates the replies, applies async `update` pushes to its local cache, and transparently re-establishes everything when the link drops.

## Limitations

- One device per `GlobconClient` instance.
- Meter frame contents are exposed raw (not semantically decoded).
- Ships the `DirectOut-Prodigy-MP` model only (more can be added).

## License

MIT

---

AI made with ❤️ by [Bitfocus](https://bitfocus.io/) (and Claude ;)
