Your First Connector
This walkthrough guides you through creating a connector configuration that wraps a hardware driver with custom methods and computed properties.
Prerequisites
- Muxit server running (
node start.js server) - A driver available (we'll use the built-in TestDevice driver, no hardware needed)
Step 1: Create the Config File
Create workspace/connectors/my-device.js:
export default {
driver: "TestDevice",
config: {},
};That's a valid connector. Restart the server (or use hot-reload) and you'll see "my-device" in the Connector Browser.
The filename (my-device.js) becomes the connector name — use it with connector("my-device") in scripts.
Step 2: Add Configuration
Pass settings to the driver via config:
export default {
driver: "TestDevice",
config: {
defaultVoltage: 5,
defaultCurrent: 1.0,
},
};What goes in config depends on the driver. Check the driver documentation for supported keys.
Step 3: Add Custom Methods
Custom methods compose multiple driver calls into higher-level operations. Inside each method, the driver is in scope as the closure variable driver. Function signatures mirror the script-side call exactly — dev.rampTo(5) ↔ (target) => …:
export default {
driver: "TestDevice",
config: {
defaultVoltage: 5,
defaultCurrent: 1.0,
},
methods: {
// Shorthand: bare function
on: () => { driver.output = true; },
// Shorthand: [function, description] tuple
off: [() => { driver.output = false; }, "Turn output OFF"],
// Full form: object with fn and description
rampTo: {
fn: async (target) => {
const current = driver.voltage;
const step = (target - current) / 10;
for (let i = 1; i <= 10; i++) {
driver.voltage = current + step * i;
await delay(50);
}
return driver.voltage;
},
description: "Ramp voltage to target in 10 steps",
},
},
};The driver closure variable is a Proxy:
driver.voltage→ reads the "voltage" propertydriver.voltage = 12→ writes the "voltage" propertydriver.reset()→ executes the "reset" action
Step 4: Add Computed Properties
Computed properties derive values from driver properties:
export default {
driver: "TestDevice",
config: { defaultVoltage: 5, defaultCurrent: 1.0 },
methods: {
on: () => { driver.output = true; },
off: [() => { driver.output = false; }, "Turn output OFF"],
rampTo: {
fn: async (target) => {
const current = driver.voltage;
const step = (target - current) / 10;
for (let i = 1; i <= 10; i++) {
driver.voltage = current + step * i;
await delay(50);
}
return driver.voltage;
},
description: "Ramp voltage to target in 10 steps",
},
},
properties: {
power: {
get: () => {
const v = driver.measured_voltage;
const i = driver.measured_current;
return Math.round(v * i * 1000) / 1000;
},
description: "Calculated power (W)",
},
},
};Step 5: Enable Polling
Make properties update automatically by adding poll:
export default {
driver: "TestDevice",
config: { defaultVoltage: 5, defaultCurrent: 1.0 },
poll: ["power", "measured_voltage", "measured_current"],
methods: {
on: () => { driver.output = true; },
off: [() => { driver.output = false; }, "Turn output OFF"],
rampTo: {
fn: async (target) => {
const current = driver.voltage;
const step = (target - current) / 10;
for (let i = 1; i <= 10; i++) {
driver.voltage = current + step * i;
await delay(50);
}
return driver.voltage;
},
description: "Ramp voltage to target in 10 steps",
},
},
properties: {
power: {
get: () => {
const v = driver.measured_voltage;
const i = driver.measured_current;
return Math.round(v * i * 1000) / 1000;
},
description: "Calculated power (W)",
},
},
};The poll array lists property names that should be read automatically (default every 200ms). Changed values are pushed to dashboard widgets via WebSocket.
Step 6: Use It in a Script
const dev = connector("my-device");
dev.on();
dev.rampTo(12);
const power = dev.power;
log.info(`Power: ${power}W`);
dev.off();Step 7: Connect Real Hardware
For real devices, use the GenericScpi driver with a declarative schema — each instrument property/method is described once, and the driver handles the {cmd: "VOLT?"} → parseFloat and VOLT {value} boilerplate. Here's a serial PSU example:
export default {
driver: "GenericScpi",
config: {
transport: "serial",
serialPort: "COM3",
baudRate: 9600,
terminator: "\n",
timeout: 3000,
autoConnect: true,
initCommands: "SYST:REM",
// Declarative schema — driver synthesizes get/set/dispatch for each entry.
// Trailing "?" on cmd marks the entry as read-only.
scpi: {
properties: {
voltage: { cmd: "VOLT", type: "float", unit: "V" },
current: { cmd: "CURR", type: "float", unit: "A" },
output: { cmd: "OUTP", type: "bool" },
measured_voltage: { cmd: "MEAS:VOLT?", type: "float", unit: "V" },
measured_current: { cmd: "MEAS:CURR?", type: "float", unit: "A" },
},
},
},
poll: ["measured_voltage", "measured_current"],
methods: {
on: [() => { driver.output = true; }, "Turn output ON"],
off: [() => { driver.output = false; }, "Turn output OFF"],
},
properties: {
// Computed property — pure JS on top of declared measurements.
power: {
get: () => {
const v = driver.measured_voltage;
const i = driver.measured_current;
return Math.round(v * i * 1000) / 1000;
},
description: "Calculated power (W)",
},
},
};Common commands like *RST, *IDN?, and SYST:ERR? are already exposed as driver actions (driver.reset(), driver.identify(), driver.readErrors()) — no need to redeclare them. For instruments with vendor-specific quirks (odd response formats, multi-step commands), hand-write just those entries in the top-level properties/methods blocks; they override the declared schema of the same name. See the SCPI driver reference for the full list of declarative fields.
Connector Config Cheat Sheet
export default {
driver: "DriverName", // Required — which driver to use
config: { /* ... */ }, // Optional — passed to driver.init()
poll: ["prop1", "prop2"], // Optional — auto-poll these properties
methods: {
name: fn, // Bare function
name: [fn, "description"], // Tuple
name: { fn, description }, // Full form
},
properties: {
name: fn, // Bare getter
name: [fn, "description", { poll: true }], // Tuple with options
name: { get: fn, set: fn, poll, description }, // Full form
},
};Inside every fn, get, and set, the driver is available as the closure variable driver. Function signatures match the script-side call exactly — no leading proxy parameter.
Next Steps
- See the full Connector Guide for transports, virtual stores, and real hardware examples
- Learn about dashboard widgets that bind to connector properties
- Build a custom driver for your specific hardware