Skip to content

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:

  1. Open the Hardware panel (plug icon in the activity bar)
  2. Check/uncheck connectors to enable or disable them (within your tier's limit)
  3. Click Save to persist your selection
  4. 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

javascript
// Minimal connector — just a driver and config
export default {
  driver: "MyDriver",
  config: { port: "/dev/ttyUSB0" },
};
javascript
// The proxy object (c) in methods and properties
await c.voltage()        // read property
await c.voltage(12)      // write property
await c.reset()          // execute action

Tips:

  • Filename = connector name (e.g., psu.jsconnector("psu"))
  • Driver names are case-insensitive
  • Use poll: true sparingly — 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.js

The 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(), dynamic import()
  • 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:

GlobalDescription
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/errorAliases 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:

javascript
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

javascript
// 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.

javascript
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:

LocationScopeUse case
Connector ai.instructionsWhole deviceSafety rules, general behavior, device context
server.json ai.accessPer property/actionHide 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

javascript
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

javascript
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:

javascript
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).

javascript
driver: "MockInstrument"     // Tier 1: JS driver
driver: "Keithley2400"       // Tier 2: SCPI definition
driver: "Fairino"            // Tier 3: C# native driver

config (optional)

Configuration object passed to driver.init(). Contents depend on the driver.

javascript
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:

javascript
await c.voltage()        // → driver.get("voltage")
await c.voltage(12)      // → driver.set("voltage", 12)
await c.reset()          // → driver.execute("reset")

Rules:

  1. Known property, no args → driver.get(name)
  2. Known property, one arg → driver.set(name, value)
  3. Known action → driver.execute(name, args)
  4. 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:

javascript
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 properties block (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:

javascript
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 property

Virtual Store Connector Pattern

The GUI driver creates virtual connectors — in-memory data stores with no hardware. These bridge scripts and dashboards:

javascript
// 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

javascript
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:

javascript
export default {
  driver: "GenericScpi",
  config: {
    transport: "serial",
    serialPort: "/dev/ttyUSB0",
    baudRate: 9600,
    terminator: "\n",
    timeout: 3000,
    autoConnect: true,
  },
};

JS drivers use createSerialTransport instead:

javascript
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 createSerialTransport and createTcpTransport are permitted.


Schema Output

Connectors automatically generate a schema from driver metadata plus custom methods and properties:

json
{
  "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:

javascript
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)

javascript
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

javascript
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",
    },
  },
};

Muxit — Hardware Orchestration Platform