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 --gui) - 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:
export default {
driver: "TestDevice",
config: {
defaultVoltage: 5,
defaultCurrent: 1.0,
},
methods: {
// Shorthand: bare function
on: (c) => c.output(true),
// Shorthand: [function, description] tuple
off: [(c) => c.output(false), "Turn output OFF"],
// Full form: object with fn and description
rampTo: {
fn: 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);
await delay(50);
}
return await c.voltage();
},
description: "Ramp voltage to target in 10 steps",
},
},
};The first argument c is a proxy to the driver. Use it like:
await c.voltage()→ reads the "voltage" propertyawait c.voltage(12)→ writes the "voltage" propertyawait c.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: (c) => c.output(true),
off: [(c) => c.output(false), "Turn output OFF"],
rampTo: {
fn: 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);
await delay(50);
}
return await c.voltage();
},
description: "Ramp voltage to target in 10 steps",
},
},
properties: {
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)",
},
},
};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: (c) => c.output(true),
off: [(c) => c.output(false), "Turn output OFF"],
rampTo: {
fn: 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);
await delay(50);
}
return await c.voltage();
},
description: "Ramp voltage to target in 10 steps",
},
},
properties: {
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)",
},
},
};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");
await dev.on();
await dev.rampTo(12);
const power = await dev.power();
log.info(`Power: ${power}W`);
await dev.off();Step 7: Connect Real Hardware
For real devices, use the GenericScpi driver with instrument-specific properties in the connector. Here's a serial PSU example:
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"],
},
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)",
},
},
};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
},
};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