Connector Guide
Connectors are the bridge between your hardware drivers and the rest of Muxit. They wrap a driver with custom methods, computed properties, and configuration — giving each physical device a friendly, high-level API.
Connector Limits & Enable/Disable
Your license tier determines how many connectors can be active at once (e.g., Free tier allows 3). The TestDevice connector is always enabled and doesn't count against this limit.
All connectors are visible in the Hardware panel — disabled ones appear greyed out with a reason. You can choose which connectors to enable:
- Open the Hardware panel (plug icon in the activity bar)
- Check/uncheck connectors to enable or disable them (within your tier's limit)
- Click Save to persist your selection
- Restart the server for changes to take effect
Your selection is saved in workspace/config/server.json under connectors.enabled. If no selection exists (first run), the first N connectors are auto-selected.
Quick Reference
// Minimal connector — just a driver and config
export default {
driver: "MyDriver",
config: { port: "/dev/ttyUSB0" },
};// The proxy object (c) in methods and properties
await c.voltage() // read property
await c.voltage(12) // write property
await c.reset() // execute actionTips:
- Filename = connector name (e.g.,
psu.js→connector("psu")) - Driver names are case-insensitive
- Use
poll: truesparingly — each polled property generates I/O - Test with the REPL:
schema("psu")to verify your schema
What Connectors Do
A driver provides raw hardware access (read voltage, send SCPI command, move motor). A connector adds:
- Configuration — pass settings like default voltage, serial port, IP address to the driver
- Custom methods — compose multiple driver calls into higher-level operations (e.g.,
rampTo,safeReset) - Computed properties — derive values from driver properties (e.g.,
power = voltage * current) - Reactive polling — automatically poll properties and push changes to dashboards
File Location
Connector configs live in workspace/connectors/ as JavaScript ES modules:
workspace/connectors/
├── psu.js
├── robot.js
├── spectrometer.js
└── panel.jsThe filename (without .js) becomes the connector name.
Security Sandbox
Connector configs run in a sandboxed V8 engine (ClearScript). They do not have access to Node.js APIs:
- Blocked:
fs,child_process,process,net,http,require(), dynamicimport() - Allowed: Standard JS globals (
Math,JSON,Date,Promise, etc.), transport factories, and script globals (see below)
Script Globals
Custom methods and properties have access to the same globals as user scripts:
| Global | Description |
|---|---|
connector(name) / device(name) | Access another connector by name (returns a proxy) |
log.debug/info/warn/error(...) | Structured logging (appears in server output and EventBus) |
console.log/warn/error | Aliases for log.info/warn/error |
emit(event, data) | Publish event to EventBus as connector:{event} |
delay(ms) | Async sleep (max 30 seconds) |
timestamp() | Returns ISO 8601 UTC timestamp |
createTcpTransport(host, port, opts) | Create TCP transport for driver config |
createSerialTransport(path, opts) | Create serial transport for driver config |
Example using globals in a custom method:
methods: {
rampTemp: {
fn: async (c, args) => {
const target = args?.target ?? 30;
log.info(`Ramping to ${target}°C`);
await c.temperature(target);
emit("temp.changed", { target });
return await c.temperature();
},
description: "Ramp temperature with logging",
},
},Basic Format
// workspace/connectors/psu.js
export default {
driver: "MockInstrument",
config: { defaultVoltage: 0, defaultCurrent: 1.0 },
poll: ["power"],
methods: {
rampTo: [async (c, target) => {
const current = await c.voltage();
const step = (target - current) / 10;
for (let i = 1; i <= 10; i++) await c.voltage(current + step * i);
return await c.voltage();
}, "Ramp voltage to target in 10 steps"],
},
properties: {
power: [async (c) => {
const v = await c.measured_voltage();
const i = await c.measured_current();
return Math.round(v * i * 1000) / 1000;
}, "Calculated power (W)"],
},
};AI Instructions
You can embed AI instructions directly in your connector config. These appear in the AI system prompt alongside the device listing, giving the AI context about how to safely interact with this device.
export default {
driver: "Fairino",
ip: "192.168.2.222",
ai: {
instructions: "Max speed = 29 when homing. Always ask for confirmation before moves.",
},
methods: { /* ... */ },
};The ai.instructions string appears in the system prompt right after the device name, before its properties and actions. Use it for:
- Safety rules — speed limits, force limits, no-go zones
- Confirmation requirements — "always confirm before enabling output"
- Device context — what the device is, units, coordinate systems
- Usage hints — "take a dark reference before measuring"
This is additive with per-property/per-action notes configured in server.json under ai.access:
| Location | Scope | Use case |
|---|---|---|
Connector ai.instructions | Whole device | Safety rules, general behavior, device context |
server.json ai.access | Per property/action | Hide items from AI, add per-item notes |
Shorthand Syntax
Methods and properties each accept three definition forms. All forms are normalized to the object form at init time.
Methods
methods: {
// 1. Bare function — no description
goHome: c => c.home(),
// 2. Array tuple — [fn, description]
moveTo: [(c, x, y, z) => c.moveL({ pose: [x, y, z] }), "Linear move"],
// 3. Object — full form (use `fn` or `call` as the function key)
rampTo: {
fn: async (c, target) => { /* ... */ },
description: "Ramp voltage to target",
},
// Equivalent using `call`:
scan: {
call: async (c) => { /* ... */ },
description: "Take a measurement",
},
}Properties
properties: {
// 1. Bare function — no description, no poll
position: c => c.tcpPos(),
// 2. Array tuple — [getter, description, options?]
power: [async (c) => (await c.v()) * (await c.i()), "Power (W)", { poll: true }],
// 3. Object — full form
idle: {
get: c => c.motionDone(),
poll: true,
description: "Robot is idle",
},
}The optional third element in the array tuple is an options object that can include poll, set, or any other property options.
Top-level poll shorthand
Instead of setting poll: true on each property, list polled property names at the top level:
export default {
driver: "Fairino",
poll: ["position", "joints", "idle"],
properties: {
position: c => c.tcpPos(),
joints: c => c.jointPos(),
idle: c => c.motionDone(),
},
};Fields
driver (required)
The name of the driver to use. Must match a registered driver name (case-insensitive lookup).
driver: "MockInstrument" // Tier 1: JS driver
driver: "Keithley2400" // Tier 2: SCPI definition
driver: "Fairino" // Tier 3: C# native driverconfig (optional)
Configuration object passed to driver.init(). Contents depend on the driver.
config: {
ip: "192.168.1.100",
port: 5000,
defaultVoltage: 0,
}methods (optional)
Custom methods that compose driver calls. The function receives a driver proxy c as its first argument, followed by the caller's arguments.
properties (optional)
Computed properties that derive values from driver properties. Object fields: get(c) (getter), set(c, value) (optional setter), poll (enable reactive polling), description.
The Driver Proxy (c)
In methods.fn and properties.get/set, the first argument c is a Proxy object:
await c.voltage() // → driver.get("voltage")
await c.voltage(12) // → driver.set("voltage", 12)
await c.reset() // → driver.execute("reset")Rules:
- Known property, no args →
driver.get(name) - Known property, one arg →
driver.set(name, value) - Known action →
driver.execute(name, args) - Fallback: tries property get/set, then action
Polling and State Updates
Properties with poll: true are automatically read at a fixed interval. When the value changes, it's broadcast to WebSocket subscribers via the state.batch message.
The poll interval defaults to 200ms, configurable per connector:
config: {
pollRate: 500, // poll every 500ms
},Only changed values trigger updates (deep equality check).
Driver Properties vs Connector Properties
When you expand a connector in the Hardware pane, you see two kinds of properties:
- Driver properties — declared by the driver itself (e.g.,
jointPos,tcpPos,forceSensor,emergencyStop). These have typed metadata including type, access level (R or R/W), unit, and description. - Custom properties — defined in your connector config's
propertiesblock (e.g.,position,joints,idle). These typically wrap or compute values from driver properties.
Both appear together in the Hardware pane. Each property is tagged with a source field ("driver" or "custom") so you can tell them apart.
How they merge
The connector schema includes all driver properties plus all custom properties. If a custom property has the same name as a driver property, the custom version takes priority and the driver version is hidden. This lets you override a driver property with custom logic (e.g., unit conversion, formatting).
Since custom properties usually have different names (e.g., position wrapping tcpPos), both appear side by side:
Properties (from robot connector + Fairino driver):
position [custom] — Current TCP position (your connector wrapper)
joints [custom] — Current joint angles (your connector wrapper)
idle [custom] — Motion idle flag (your connector wrapper)
tSpeed [custom] — Computed total tool speed
jointPos [driver] — Raw joint angles from driver
tcpPos [driver] — Raw TCP pose from driver
forceSensor [driver] — Force/torque sensor data
emergencyStop [driver] — E-stop status
... (all other driver properties)Same pattern for methods
Methods work identically — driver actions and custom methods both appear. Custom methods with the same name as a driver action override the driver version.
Accessing driver properties from scripts
Both custom and driver properties are accessible through the connector proxy in scripts:
let r = connector("robot");
await r.position(); // custom property (defined in robot.js)
await r.jointTorques(); // driver property (from Fairino driver directly)
await r.forceSensor(); // driver propertyVirtual Store Connector Pattern
The GUI driver creates virtual connectors — in-memory data stores with no hardware. These bridge scripts and dashboards:
// workspace/connectors/store.js
export default {
driver: "GUI",
config: {
widgets: [
{ name: "value1", type: "gauge", label: "Value 1", min: 0, max: 100 },
{ name: "input", type: "slider", label: "Input", min: 0, max: 100, step: 1, value: 50 },
{ name: "running", type: "light", label: "Running", value: false },
{ name: "status", type: "text", label: "Status", value: "Idle" },
{ name: "display", type: "canvas", label: "Display" },
],
},
};Data flow — property path: Script → store.value1(42) → driver.set() → polling → state.batch → dashboard gauge
Data flow — stream path: Script → stream("store", "display", base64) → EventBus → stream.subscribe → dashboard canvas
Transports
Drivers that communicate over a network or serial port need a transport object passed via config._transport.
TCP Transport
import { createTcpTransport } from "createTcpTransport";
export default {
driver: "Keithley2400",
config: {
_transport: createTcpTransport("192.168.1.100", 5000, {
timeout: 5000,
delimiter: "\n",
}),
},
};Serial Transport
DLL extension drivers (like GenericScpi) manage their own transport internally via config options:
export default {
driver: "GenericScpi",
config: {
transport: "serial",
serialPort: "/dev/ttyUSB0",
baudRate: 9600,
terminator: "\n",
timeout: 3000,
autoConnect: true,
},
};JS drivers use createSerialTransport instead:
export default {
driver: "MyJsDriver",
config: {
_transport: createSerialTransport("/dev/ttyUSB0", {
baudRate: 9600,
delimiter: "\n",
timeout: 3000,
}),
},
};Both transports expose: connect(), write(data), query(command), close(), and connected (getter). The serial transport also supports writeRaw(data) for raw bytes and an onData callback for streaming protocols.
Note: Transport imports in connector configs are validated against an allowlist — only
createSerialTransportandcreateTcpTransportare permitted.
Schema Output
Connectors automatically generate a schema from driver metadata plus custom methods and properties:
{
"name": "psu",
"driver": "MockInstrument",
"properties": {
"voltage": { "type": "number", "access": "rw", "unit": "V", "description": "Output voltage" },
"power": { "type": "object", "access": "r", "description": "Calculated power (W)" }
},
"actions": {
"reset": { "description": "Reset to defaults" },
"rampTo": { "description": "Ramp voltage to target in 10 steps" }
},
"streams": []
}Real Hardware Examples
BK Precision 9122A (USB-Serial PSU)
Uses GenericScpi driver with instrument-specific properties defined as computed connector properties:
export default {
driver: "GenericScpi",
config: {
transport: "serial",
serialPort: "/dev/ttyUSB0",
baudRate: 9600,
terminator: "\n",
timeout: 3000,
autoConnect: true,
initCommands: "SYST:REM",
},
poll: ["measured_voltage", "measured_current"],
methods: {
on: [c => c.output(true), "Turn output ON"],
off: [c => c.output(false), "Turn output OFF"],
setOutput: {
fn: async (c, args) => {
if (args?.voltage != null) await c.voltage(args.voltage);
if (args?.current != null) await c.current(args.current);
},
description: "Set voltage and current: { voltage, current }",
},
},
properties: {
voltage: {
get: c => c.query({ command: "VOLT?" }).then(parseFloat),
set: (c, v) => c.send({ command: `VOLT ${Number(v)}` }),
description: "Programmed voltage (V)",
},
measured_voltage: {
get: c => c.query({ command: "MEAS:VOLT?" }).then(parseFloat),
description: "Measured output voltage (V)",
},
measured_current: {
get: c => c.query({ command: "MEAS:CURR?" }).then(parseFloat),
description: "Measured output current (A)",
},
output: {
get: async c => (await c.query({ command: "OUTP?" })).trim() === "1",
set: (c, v) => c.send({ command: `OUTP ${v ? "ON" : "OFF"}` }),
description: "Output enable/disable",
},
power: {
get: async (c) => {
const v = await c.measured_voltage();
const i = await c.measured_current();
return Math.round(v * i * 1000) / 1000;
},
description: "Calculated power (W)",
},
},
};Serial Monitor (Terminal)
import { createSerialTransport } from "createSerialTransport";
export default {
driver: "SerialMonitor",
config: {
_transport: createSerialTransport("/dev/ttyUSB0", { baudRate: 9600 }),
},
methods: {
sendLine: {
fn: async (c, args) => {
const text = typeof args === "string" ? args : args?.text ?? "";
return c.sendLine({ text });
},
description: "Send a line of text to the serial port",
},
},
};G-code / GRBL CNC
import { createSerialTransport } from "createSerialTransport";
export default {
driver: "Gcode",
config: {
_transport: createSerialTransport("/dev/ttyUSB0", { baudRate: 115200 }),
statusInterval: 250,
},
poll: ["status", "position", "progress"],
methods: {
park: {
fn: async (c) => {
await c.send({ command: "G0 Z5 F500" });
await c.send({ command: "G0 X0 Y0 F1000" });
},
description: "Raise Z and park at origin",
},
},
};