@bitfocusas/globcon
v0.1.0
Published
Typed Node.js client for the globcon protocol (DirectOut devices)
Readme
globcon
A fully-typed Node.js / TypeScript client for the globcon protocol, used to control DirectOut 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.
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/setvalidate 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
updateevents (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
npm install @bitfocusas/globconRequires Node.js ≥ 22. ESM only.
Quick start
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 listenersThe 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".
// 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 unroutedRouting by label
Indices are stable but not memorable. Look them up from the labels:
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
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:
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 onOutputs
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).
// 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:
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:
// 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:
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:
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 Float32Arrays. Tell the client which channels you care about; it opens the stream, and re-subscribes automatically after a reconnect.
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[].silenceflags fromupdateevents are simpler and reliable.
Configuration
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
GlobconClientinstance. - Meter frame contents are exposed raw (not semantically decoded).
- Ships the
DirectOut-Prodigy-MPmodel only (more can be added).
License
MIT
AI made with ❤️ by Bitfocus (and Claude ;)
