# Muxit for AI assistants The pages in this section are written for an AI assistant, not for a person. Each one is self-contained, states the rules the Muxit runtime actually enforces, shows complete files instead of fragments, lists the mistakes assistants make most often, and ends with a checklist. Hand them to whatever assistant you already use, and it can write Muxit code that loads first time without having seen Muxit before. | Page | Use it when you want the assistant to write… | |---|---| | [Writing Connectors](/for-ai/connectors.md) | a `workspace/connectors/.js` file: wiring an instrument, Arduino, sensor or other device into Muxit, or an in-memory store for dashboards | | [Writing Scripts](/for-ai/scripts.md) | a `workspace/scripts/.js` automation: measurement runs, sweeps, monitoring loops, data logging, alarms | | [Writing Drivers](/for-ai/drivers.md) | a JavaScript, Python or C# driver, for a device no built-in protocol can talk to | Most devices need only a connector. The assistant can tell from the connector page whether yours needs a driver. ## For people: how to use these pages 1. **Give the assistant the page.** Pick whichever fits your assistant: - **Paste it.** Every page on this site has **Copy page as Markdown** at the top. Paste the result into the chat before your request. - **Attach one file.** [`llms-full.txt`](/llms-full.txt) holds all the pages in this section. Attach it, or save it into your project as context for a coding agent (Claude Code, Cursor, Copilot). - **Give it the link.** An assistant that can browse can start from `https://docs.muxit.io/llms.txt`, an index of every page as raw markdown. 2. **Tell it about your setup.** The make and model, how it is connected (serial port and baud rate, IP address and port, USB), what you want to read, set or automate, and — for an instrument — the command table from its programming manual. The assistant should not guess device commands, and the pages tell it not to. 3. **Put the file where it belongs.** Connectors go in `workspace/connectors/`, scripts in `workspace/scripts/`, drivers in `workspace/drivers/`. The file name matters: `psu.js` becomes the connector `psu`. 4. **Load it and check it.** For a connector, click **Reload** in Muxit's **Connectors** panel (and tick the connector if it shows as disabled); for a driver, restart Muxit. A connector that fails to load shows the error and line number on its card — paste that back to the assistant. Scripts run from the editor's **Run** button. A request that works well: ```text I'm attaching the Muxit reference for AI assistants. Follow it exactly: use only the APIs it documents, no Node.js APIs, and don't invent device commands. Write a Muxit connector for my . Connection: I want to: Command reference from the manual: Give me the complete file with its file name, then go through the checklist at the end of the reference and tell me anything you could not verify. ``` **Other ways to get there.** Muxit's own chat assistant can probe a connected instrument and write the connector for you — see [AI-assisted SCPI setup](/guides/ai-scpi-authoring.md). And an assistant that supports MCP (Claude Desktop, Claude Code) can connect to Muxit's [MCP server](/guides/ai.md#_2-mcp-server) and read schemas, write connector and script files, and run scripts itself; these pages still tell it how to write them. --- ## For AI assistants: start here You are writing code for **Muxit**, a hardware orchestration platform. A Muxit server (C#) runs on the user's machine and talks to lab hardware: power supplies, multimeters, oscilloscopes, robots, CNC machines, cameras, Arduinos, sensors. Everything a user automates is a JavaScript file in their *workspace* folder, executed by the server in a sandboxed V8 engine — not in Node.js and not in a browser. ### What you can write | You write | File | What it is | Reference | |---|---|---|---| | **Connector** | `workspace/connectors/.js` | One per device. Says which backend talks to the device (a built-in protocol, a driver, or nothing for an in-memory store), how it is connected, and optionally adds computed properties, helper methods, polling, safety limits and AI instructions. The file name is the device name. | [Writing Connectors](/for-ai/connectors.md) | | **Script** | `workspace/scripts/.js` | Automation. Plain top-level JavaScript, run on demand from the UI, by the AI, or at server start (`workspace/startup/`). Reads and writes connector properties synchronously. | [Writing Scripts](/for-ai/scripts.md) | | **Driver** | `workspace/drivers/…` | A backend for a device no built-in protocol can talk to: vendor SDK, native library, complex binary protocol. JavaScript, Python or C#. A driver on its own does nothing — a connector selects it. | [Writing Drivers](/for-ai/drivers.md) | ### Decide what to write - The user wants to **use a device** in Muxit → a **connector**. Most instruments need no driver: the built-in `Scpi`, `LineText` and `BinaryStream` protocols cover SCPI instruments, anything that prints text lines, and fixed-layout binary frames (Modbus RTU included). - The user wants to **do something with devices that already have connectors** → a **script**. - The device speaks something none of the protocols can express — a vendor DLL or Python package, a stateful handshake, a multi-step binary protocol → a **driver**, plus a connector that uses it. When in doubt, write a connector first. A driver is the last resort, not the default. ### Rules for everything you write 1. **Use only the APIs these pages document.** Connectors, scripts and JavaScript drivers run in a sandbox: there is no `require`, no `fs`, no `process`, no `fetch`, no Node.js and no npm packages, and each kind of file has its own short list of globals. If something the user asks for needs an API that is not on these pages, say so instead of inventing one — an invented global or method fails at load time or, worse, silently returns `undefined`. 2. **Never invent device commands.** SCPI commands, line formats, register offsets and baud rates come from the device's manual or from the user. If you have to guess, say which values are guesses so the user can check them before the code touches hardware. 3. **This is real hardware.** Ask for the safe ranges before writing anything that sets voltages, currents, temperatures, speeds or positions. Encode them as `safety.limits` in the connector *and* check them in your own code — the `safety` block is only enforced on Muxit's Lab tier. Do not switch outputs on, move axes or write to non-volatile memory as a side effect of reading something. 4. **Deliver whole files.** Give the complete file, its exact path and name, and nothing to fill in except values only the user knows (port, IP address), which you call out. 5. **Finish with the checklist** at the end of the page you used, and tell the user anything you could not check. ### Version note These pages describe the Muxit release they were built with. If the user's Muxit is older and something here fails to load, ask for the error message — it names the problem — rather than reaching for an older or invented syntax. ### Further reading Every page of the documentation is available as raw markdown; [`llms.txt`](/llms.txt) lists them. The ones you are most likely to need next: - [Concepts — Driver, Protocol, Engine](/reference/concepts.md) - [Scpi protocol](/reference/drivers/scpi.md), [LineText](/reference/drivers/linetext.md), [BinaryStream](/reference/drivers/binary-stream.md), [Transport](/reference/drivers/transport.md) - [Built-in drivers](/reference/drivers/index.md) - [Script API](/reference/script-api.md) and the [Script guide](/guides/scripts.md) - [Safety](/guides/safety.md) - [Driver SDK](/reference/driver-sdk.md) and [Driver manifest](/reference/driver-manifest.md) # Writing Muxit connectors *A reference for AI assistants. It is self-contained: you can write a correct connector from this page alone. Read [Rules the runtime enforces](#rules-the-runtime-enforces) before anything else, and finish with the [checklist](#checklist).* A **connector** is one JavaScript file that brings one device into Muxit. It names the backend that talks to the device, says how the device is connected, and can add computed properties, helper methods, safety limits and instructions for Muxit's own AI. Scripts, dashboards and the AI then use the device by the connector's name: the file `workspace/connectors/psu.js` is the connector `psu`, reached from a script as `connector("psu")`. ## The deliverable - **One file**, `workspace/connectors/.js`. The file name without `.js` is the connector name; use lowercase letters, digits, `-` and `_` (`psu`, `bench-dmm`, `arduino_env`). - **An ES module with a single `export default { … }`.** Plain JavaScript, no TypeScript. - It runs in a **sandboxed V8 engine inside the Muxit server**: not Node.js, not a browser. There are no npm packages and no Node or browser APIs. - Give the user the whole file, its path, and the values only they can fill in (serial port, IP address), called out. ## Rules the runtime enforces Breaking any of these either stops the file from loading or makes it misbehave quietly. 1. **Pick exactly one backend keyword**: `protocol:` (a built-in wire protocol: `"Scpi"`, `"LineText"`, `"BinaryStream"`), `driver:` (a driver that owns its connection), or `engine:` (compute over another connector). Leave all three out only for an in-memory [Virtual](#virtual-no-hardware) connector. 2. **Top-level keys are fixed.** The recognised keys are `driver`, `protocol`, `engine`, `config`, `methods`, `properties`, `poll`, `ai`, `hide`, `expose`, `safety`, `allowedConnectors`, `allowedHosts`. Any *other* top-level key (`connection`, `source`, a driver option) is handed to the driver as configuration, and `config: { … }` overrides it. In a file without a backend keyword, an unrecognised key is a load error. 3. **These strings may not appear anywhere in the file, comments and string literals included:** `require(`, `import(`, `eval(`, `Function(`. The loader rejects the file on a text match. Do not write `import` statements either: a default import (`import x from "…"`) is an error, and the named form is stripped and does nothing — everything you can use is already a global. 4. **Inside a connector, every `driver.` read and every `driver.(…)` call returns a Promise.** Make the getter or method `async` and `await` each one. Without `await`, arithmetic gives `NaN` and template strings show `[object Promise]`. A write, `driver. = value`, starts immediately but cannot be awaited — see [The `driver` object](#the-driver-object). 5. **`driver` has only the names the backend declares.** Anything else throws `ReferenceError: Driver has no property or action ''`. Your own custom properties and methods are *not* on `driver`. 6. **There is no `this` and no `this.state`.** Keep state in module-level `let` variables above `export default`. 7. **Return plain values** — numbers, strings, booleans, `null`, arrays, plain objects. Never `{ value, unit }`: the unit goes in the `description` (or `unit` field), and dashboards and scripts expect a bare number. 8. **Every readable property the backend declares is polled automatically**, each on its own loop every 200 ms (server setting `polling.connectorRate`). The `poll` key only switches polling on for *your* custom properties. A backend property stops being polled only when a custom property of the same name replaces it, so keep protocol schemas to the properties the user needs. 9. **Never invent device commands, line formats or register offsets.** Take them from the manual or the user, and say which ones you could not verify. ## Choose the backend | The device… | Backend | Section | |---|---|---| | answers SCPI (`*IDN?` works): benchtop PSUs, DMMs, scopes, generators, loads — Rigol, Keysight, Siglent, Tektronix, R&S, Keithley, BK Precision, Owon… | `protocol: "Scpi"` | [Scpi](#protocol-scpi) | | prints text lines (`TEMP: 25.5`, `V=12.0`): Arduino and ESP32 sketches, hobby supplies, loggers, anything with a serial console | `protocol: "LineText"` | [LineText](#protocol-linetext) | | sends or answers fixed-layout binary frames: Modbus RTU, custom binary sensors | `protocol: "BinaryStream"` | [BinaryStream](#protocol-binarystream) | | is a camera, file store, MQTT broker, notifier, label printer, or has an installed Muxit driver (Fairino robot, Avantes spectrometer, G-code machine…) | `driver: ""` | [Drivers](#using-a-driver) | | is processed from another connector's output (object detection on a camera feed) | `engine: ""` | [Engines](#engines) | | does not exist: shared values for dashboards and scripts | no keyword | [Virtual](#virtual-no-hardware) | | fits none of these: vendor SDK, stateful or multi-step protocol | a new driver | [Writing Drivers](/for-ai/drivers.md) | Use SCPI when the vendor documents SCPI commands, even if the cable is USB or serial. Use LineText for text that is not SCPI. BinaryStream is only for byte-level frames. ## File anatomy ```js // workspace/connectors/bench_psu.js let lastSetpoint = null; // module-level state, if you need any export default { protocol: "Scpi", // 1. backend: protocol | driver | engine connection: { type: "tcp", host: "192.168.1.50", port: 5025 }, // 2. transport (protocols only) config: { // 3. options for the backend terminator: "\n", schema: { // protocols: what the device understands properties: { voltage: { cmd: "VOLT", type: "float", unit: "V", description: "Set voltage" } }, methods: { beep: { cmd: "SYST:BEEP", description: "Beep" } }, }, }, properties: { /* computed properties — see Custom properties */ }, methods: { /* helper methods — see Custom methods */ }, poll: [ /* names of custom properties to poll */ ], safety: { limits: { voltage: { min: 0, max: 30 } } }, // 4. hard limits, confirmations ai: { instructions: "Bench PSU, 0–30 V. Confirm before enabling the output." }, // 5. for Muxit's AI }; ``` ## `connection:` — how the bytes move Protocol connectors need a top-level `connection:` block. Drivers usually take their own options in `config` instead — see the driver's reference. `type` must be `"serial"`, `"tcp"` or `"usbtmc"`; anything else fails to load. **serial** | Field | Default | Notes | |---|---|---| | `port` | — | `"COM4"` on Windows, `"/dev/ttyUSB0"` / `"/dev/ttyACM0"` on Linux, `"/dev/tty.usbserial-XXXX"` on macOS. Required. | | `baudRate` | `9600` | Must match the device. | | `dataBits` | `8` | | | `parity` | `"none"` | `"none"`, `"odd"`, `"even"`, `"mark"`, `"space"`. | | `stopBits` | `"1"` | `"1"`, `"1.5"`, `"2"`. | | `flowControl` | `"none"` | `"none"`, `"xon-xoff"`, `"rts-cts"`, `"both"`. | **tcp** | Field | Default | Notes | |---|---|---| | `host` | `"127.0.0.1"` | IP address or host name. | | `port` | `5025` | 5025 is the usual raw-SCPI (LXI) port. | | `timeoutMs` | `5000` | Connect timeout. | **usbtmc** — USB instruments that enumerate as USB Test & Measurement class. Works with `Scpi` only; LineText and BinaryStream never receive data over it. | Field | Default | Notes | |---|---|---| | `vendorId`, `productId` | — | Required. Number (`0x1AB1`) or string (`"0x1AB1"`). | | `serial` | — | Pick one of several identical instruments by serial number. | | `interfaceNumber` | `0` | | | `endpointNumber` | `1` | Bulk OUT endpoint; IN is `0x80 | n`. | On Windows a USBTMC instrument needs the WinUSB driver bound (Zadig); on Linux, a udev rule. ## Protocol: Scpi For instruments that speak SCPI. You declare each property once — the command and its type — and the protocol does the query, the write and the parsing. **`config` options** | Option | Default | Meaning | |---|---|---| | `terminator` | `"\n"` | Appended to every command; a reply ends at the first terminator (a trailing `\r` is stripped). Some instruments need `"\r\n"` or `"\r"`. | | `timeoutMs` | `5000` | How long a query waits for its reply. | | `commandIntervalMs` | `0` | Minimum gap between any two commands. Set 20–100 for slow instruments that drop commands. | | `autoConnect` | `false` | When `true`, sends `*IDN?` at start-up to fill the `identity` property. The transport opens either way. | | `schema` | — | `{ properties, methods }`, below. | **`schema.properties`** — one entry per value: ```js voltage: { cmd: "VOLT", type: "float", unit: "V", description: "Programmed voltage" }, output: { cmd: "OUTP", type: "bool", description: "Output enable" }, measured_voltage: { cmd: "MEAS:VOLT?", type: "float", unit: "V", description: "Measured voltage" }, ``` | Field | Meaning | |---|---| | `cmd` | Required. Without a trailing `?`, the property is read with `cmd + "?"` and written with `cmd + " " + value`. With a trailing `?`, the command is sent as written and the property is read-only. | | `type` | `"float"`, `"int"`, `"bool"` or `"string"` (the default). | | `unit`, `description` | Shown in the UI and to Muxit's AI. | | `readonly` | `true` makes a property without `?` read-only. | No other fields exist. `min`, `max`, `enum`, `scale`, `format` and `details` are ignored — ranges belong in [`safety.limits`](#safety). Parsing on read: `float` and `int` must parse as numbers or the read fails; `bool` is true for `1`, `ON` or `TRUE` and false for anything else. On write, `bool` is sent as `ON` / `OFF` — if the instrument wants `1` / `0`, declare the property as `type: "int"`. **`schema.methods`** — commands that do something and return nothing: ```js beep: { cmd: "SYST:BEEP", description: "Beep once" }, save: { cmd: "*SAV", arg: "int", description: "Save settings to slot N" }, // psu.save(3) → "*SAV 3" ``` `arg` is a type (`"float"`, `"int"`, `"bool"`, `"string"`) or `{ type, name }`. The method sends `cmd` or `cmd + " " + value` and returns `"OK"`; it never reads a reply. Always pass the argument a method declares: without it a script call sends a placeholder (`0`, `OFF` or nothing) and a call from connector code fails. Do not declare an `arg` the command does not take. **Built in** — always present, do not redeclare: | Name | Kind | What it does | |---|---|---| | `connected`, `identity`, `lastCommand`, `lastResponse`, `queryCount` | read-only properties | Status; no I/O. | | `connect()`, `disconnect()` | actions | Open / close the transport. `connect` also queries `*IDN?`. | | `identify()` | action | `*IDN?` | | `reset()` | action | `*RST` | | `clear()` | action | `*CLS` | | `query(command)` | action | Send any command, return the reply line as a string. | | `send(command)` | action | Send any command, no reply. | Do not name schema entries after the built-ins: a schema property called `identity` is never queried, and a schema method called `query` or `send` hides the raw one. **What the schema cannot express — use a custom property or method with `driver.query`:** - Queries with parameters (`MEAS:VOLT? CH1`, `:MEAS:ITEM? VPP,CHAN1`). The schema always sends `cmd + "?"`. - One reply carrying several values (`U=12.00 I=0.103`, `5.012,0.103,12.4`). Never map several properties to the same `cmd`. Query once and parse — see the [multi-value example](#multi-value-replies). - Multi-step sequences, or commands whose reply needs custom parsing. **I/O cost.** Every schema property is queried on every poll cycle — five properties at the default 200 ms is 25 queries a second — and the protocol has no cache: each `await driver.voltage` inside your own code is one more query. All queries share one line: a query the instrument never answers holds it for the full `timeoutMs` on every poll cycle and delays every other property, and two timeouts in a row trip a breaker that fails all queries until the instrument answers again. So declare only properties the user wants, and make any command without a query form a method, never a property. The legacy `driver: "GenericScpi"` with a flat config and a `config.scpi` block still loads; do not write new connectors in that shape. ## Protocol: LineText For devices that print lines of text. Each property recognises a line and keeps the last value it saw; reading a property never sends anything. **`config` options:** `terminator` (default `"\n"`; a trailing `\r` on received lines is stripped), `timeoutMs` (default `5000`, used by `query`), `schema: { properties, methods }`. **`schema.properties`** | Field | Meaning | |---|---| | `prefix` | Case-sensitive "line starts with". The rest of the line, trimmed, is the value. `"TEMP: "` matches `TEMP: 25.5`. | | `match` | A regular expression **string** (no flags), when the line needs structure. Backslashes are doubled inside a JS string: `"^V=([-+]?\\d+(?:\\.\\d+)?)$"`. An invalid regex stops the connector loading. | | `group` | Capture group holding the value, default `1`; `0` is the whole match. A `match` with no capture group never updates. | | `type` | `"float"`, `"int"`, `"bool"`, `"string"` (default). `float` tolerates trailing text (`25.5 C` → 25.5); `bool` is true for `1`, `ON`, `TRUE`. A value that fails to parse becomes `null`. | | `unit`, `description` | Labels. | Give each property a `prefix` or a `match`; with neither it stays `null`. One line can update several properties. All schema properties are read-only. **`schema.methods`** — `{ cmd, arg?, description }`. Sends `cmd` or `cmd + " " + value` plus the terminator and returns `"OK"`. `bool` arguments are sent as `ON` / `OFF`. **Built in:** properties `lastLine` (every received line) and `connected`; actions `send(command)` and `query(command)`. `query` sends a line and returns the **next line received, whatever it is** — on a device that streams lines unprompted, that may be a streamed line rather than the answer. Do not name schema methods `send` or `query`. Received bytes are decoded one byte per character, so only plain ASCII survives intact. ## Protocol: BinaryStream For devices that exchange binary frames. You describe how to find a frame in the byte stream, where each field sits in it, and — for request/response devices — the request to send. **`schema.framing`** — byte strings are hex; spaces, `_` and `-` are ignored (`"01 03 00 00"`). | Field | Default | Meaning | |---|---|---| | `header` | — | Bytes every frame starts with; used to resync. **Never on its own** — combine with `size` or a length field, or no frame is ever delivered. | | `size` | — | Fixed total frame length in bytes (header included). | | `lengthOffset`, `lengthWidth` | — | Where the length byte(s) sit. Frame length = `lengthOffset + lengthWidth + ` (+2 with Modbus CRC). Use both or neither. | | `lengthEndian` | `"little"` | `"little"` or `"big"`. | | `crc` | `"none"` | `"modbus"` appends a CRC-16 to everything sent and checks it on received frames (only with `size` or length framing). Write commands *without* the CRC. Any other value means none. | | `quietMs` | `50` | Without `header`, `size` or a length field, a frame ends after this many ms of silence. | | `request` | — | Hex bytes sent to ask for a fresh frame. Required for devices that only answer when asked (Modbus). | | `maxStaleMs` | `100` | A read older than this sends `request` again. Set it to at least `200` (the poll interval) for about one request per poll cycle. | **`schema.properties`** | Field | Default | Meaning | |---|---|---| | `offset` | `0` | Byte offset from the start of the frame, **header included**. | | `width` | — | Bytes: 1, 2 or 4 (8 for float64). **Required** — without it the value is always 0. | | `type` | `"uint"` | `"uint"`, `"int"` (two's complement — use it for anything that can go negative), `"float"` (width 4 = float32, 8 = float64), `"bool"`. | | `endian` | `"little"` | Modbus registers are `"big"`. | | `scale` | `1` | Multiplier, as a **number** (`0.1`, not `"0.1"`). | | `unit`, `description` | | Labels. | Unsigned 4-byte values of 2³¹ and above decode as negative numbers; avoid relying on them. All schema properties are read-only. Without `request`, a read returns the last frame received; with it, a stale read sends the request and waits for the reply. **`schema.methods`** — `{ command, argOffset?, argWidth?, argType?, argEndian?, description }`. `command` is a hex template; with `argOffset` **and** `argWidth`, the argument is packed into it at that offset. Arguments are packed as integers only (a float is truncated). Returns `"OK"`. **Built in:** properties `connected` and `lastFrame` (hex string); actions `sendHex(hex)` and `queryHex(hex)` (returns the next frame as uppercase hex). ## Using a driver `driver: ""` picks a backend that owns its connection. Driver names are case-insensitive. Each driver has its own `config` options and its own properties and actions; take them from the driver's reference page or its starter template (shown when the user adds a connector for that driver in Muxit). **Do not guess option names** — an unknown option is silently ignored. | Driver | What it is | Reference | |---|---|---| | `TestDevice` | Simulated device for trying things out (temperature, counters, streams, video). | [test-device](/reference/drivers/test-device.md) | | `Webcam` | USB / built-in camera: live stream, `snapshot()`, recording. | [webcam](/reference/drivers/webcam.md) | | `Onvif` | IP camera: stream, snapshot, PTZ. | [onvif](/reference/drivers/onvif.md) | | `FileAccess` | Read and write files in a workspace folder. | [file-access](/reference/drivers/file-access.md) | | `MqttBridge` | Publish and subscribe to an MQTT broker. | [mqtt-bridge](/reference/drivers/mqtt-bridge.md) | | `WebhookNotifier` | HTTP notifications: ntfy, Discord, Slack, generic JSON. | [webhook-notifier](/reference/drivers/webhook-notifier.md) | | `EmailNotifier` | E-mail over SMTP. | [email-notifier](/reference/drivers/email-notifier.md) | | `LabelPrinter` | Label printing from templates. | [label-printer](/reference/drivers/label-printer.md) | | `Virtual` | In-memory store; usually written without a keyword. | [Virtual](#virtual-no-hardware) | | installed drivers | e.g. `Fairino`, `Avantes`, `AudioSynth`, `Midi`, `Gcode`, `Python` | the driver's own page, or its template | ```js // workspace/connectors/webcam.js export default { driver: "Webcam", config: { device: 0 }, // option names come from the driver's reference ai: { instructions: "Bench camera looking at the fixture." }, }; ``` Wrap a driver's raw API in friendlier methods, and use `hide` to keep the raw items out of the UI and Muxit's AI (they stay callable from scripts): ```js export default { driver: "Fairino", config: { ip: "192.168.58.2" }, hide: ["moveL", "moveJ"], methods: { moveTo: { fn: (x, y, z) => driver.moveL({ pose: [x, y, z, 180, 0, 90] }), description: "Linear move to x, y, z in mm, tool pointing down", }, }, }; ``` ## Engines An engine processes another connector's output instead of talking to hardware. Name the source connector with `source`: ```js export default { engine: "Vision", source: "webcam" }; ``` ## Virtual (no hardware) A file with no `driver`, `protocol` or `engine` key — and only the recognised top-level keys — is an in-memory store. State lives in module-level variables and is lost on restart. Use it for setpoints a dashboard slider adjusts and a script reads, status text, counters and flags. ```js // workspace/connectors/store.js let setpoint = 20; let status = "Idle"; export default { properties: { setpoint: { get: () => setpoint, set: (v) => { setpoint = Number(v); }, description: "Target (°C)" }, status: { get: () => status, set: (v) => { status = String(v); }, description: "Status line" }, }, methods: { reset: [() => { setpoint = 20; status = "Idle"; }, "Back to defaults"], }, poll: ["setpoint", "status"], // so changes made inside reset() reach dashboards }; ``` A direct write (`store.setpoint = 25` from a script or a dashboard) is broadcast at once. A value changed *inside a method* is only noticed by polling, hence the `poll` list. ## Custom properties Computed or wrapped values. Three equivalent forms: ```js properties: { uptime: () => Date.now() - startedAt, // bare getter power: [async () => (await driver.voltage) * (await driver.current), // [getter, description, options] "Output power (W)", { poll: true }], target: { // full form get: () => target, set: (v) => { target = Number(v); }, description: "Target temperature (°C)", poll: true, }, }, ``` - **`get` is required.** An entry with only `set` is ignored. - **No `set` means read-only** in the UI and for Muxit's AI. - **`poll: true`** (or the name in the top-level `poll: [...]`) re-reads it every 200 ms and pushes changes to dashboards. Only poll what a dashboard shows live; every poll runs your getter, and every `await driver.x` in it is a device query. - **Getters run on every read** from a script, dashboard or AI. Keep them quick. - **There is no `type` or `unit` field.** The type is inferred from the value; put the unit in the `description`. - **Setter writes are queued**: the UI shows the new value at once, the setter runs at most once per 100 ms with the latest value, and an error it throws only reaches the server log. Writing the value a property already has does nothing. - A custom property with the **same name as a backend property replaces it** (and the backend property is then only polled if you set `poll: true`). Inside it, `driver.` still reaches the backend property. ## Custom methods Helper actions callable as `connector("psu").rampTo(12)`. Three equivalent forms: ```js methods: { home: () => driver.home(), // bare function park: [async () => { await driver.send("G0 Z5"); }, "Raise Z"], // [fn, description] rampTo: { fn: (target, stepMs) => rampTo(target, stepMs), // { fn, description } (or `call`) description: "Ramp voltage to target (V) — rampTo(target, stepMs = 100)" }, }, ``` **How arguments arrive.** A script call `dev.rampTo(12, 50)` calls `fn(12, 50)`; one argument arrives as that value; no arguments as `fn()`. A single array is spread: `dev.load([1, 2, 3])` calls `fn(1, 2, 3)`. **Muxit's AI and MCP clients pass one object** — `{ "target": 12 }` arrives as `fn({ target: 12 })` — because named arguments are not mapped onto parameters. Accept both: ```js fn: (target, stepMs) => { if (typeof target === "object" && target !== null) ({ target, stepMs } = target); return rampTo(Number(target), Number(stepMs ?? 100)); }, ``` **How the argument list is shown.** Muxit reads the parameter names from the function source to show the arguments in the Connectors panel and to Muxit's AI — but only for `function (a, b)`, `(a, b) =>` and `a =>`. An `async (a) =>` or `async function` shows *no* arguments, and the panel then runs it with none. So keep the exposed function a plain arrow and put asynchronous work in a module-level `async function`, as `rampTo` above does. Describe the expected arguments in the `description` as well. **Return** a plain value (or nothing). **Throw** `new Error("…")` to fail — the message reaches the caller. Keep methods short — seconds, not minutes: each call holds the connector's engine, so other custom calls and custom polls on the same connector wait for it. Long sequences belong in a [script](/for-ai/scripts.md). A method with the same name as a backend action replaces it. ## The `driver` object Inside getters, setters and methods, `driver` is the backend. | You write | What happens | |---|---| | `await driver.voltage` | Live read from the device (not the poll cache). Without `await` you get a Promise. | | `driver.voltage = 5` | Starts the write and returns at once; you cannot wait for it, and an error in it is not reported to you. | | `await driver.reset()` | Calls the action and waits for its result. | | `driver.foo` (not declared by the backend) | Throws `ReferenceError`. | - **When a later step depends on a write having happened**, use an action you can await instead. On `Scpi`: `await driver.send(\`VOLT ${v}\`)`. - **Action arguments inside a connector are not mapped to names.** Pass driver actions an object with the declared argument names: `await driver.moveL({ pose: [x, y, z] })`. `Scpi`, `LineText` and `BinaryStream` also accept a bare value: `await driver.query("SYST:ERR?")`. - **Calls made through `driver` skip Muxit's safety gate.** A method that writes `driver.voltage = 100` is not stopped by `safety.limits`. Check ranges in your own code. - `driver` holds the backend's names only — never your own custom properties or methods. To reuse logic, write a module-level function and call it from both places. ## Globals inside a connector | Global | Notes | |---|---| | `driver` | The backend, above. | | `log.info(…)`, `log.warn(…)`, `log.error(…)`, `log.debug(…)`; `console.log` / `warn` / `error` | Server log, tagged with the connector name. | | `await delay(ms)` | Async sleep, **capped at 30 000 ms**. It must be awaited, inside an `async` function. | | `timestamp()` | ISO 8601 UTC string. | | `connector(name)` | Another connector — only when listed in `allowedConnectors`. Returns Promises like `driver`. | | `"$ENV{NAME}"` | A string that is *exactly* this is replaced with the environment variable `NAME` — for API keys and passwords. `"Bearer $ENV{TOKEN}"` is not replaced. | | `createTcpTransport(host, port, opts)`, `createSerialTransport(path, opts)` | Raw transports, for drivers that take a pre-built `config._transport`. Not needed with `protocol:`. | | `emit(event, data)` | Publishes `connector:.`. Scripts receive it with `on("connector:.", …)` or `waitFor(…)`. Every call is delivered, even with unchanged data. | Plus standard JavaScript (`Math`, `JSON`, `Date`, `Map`, `RegExp`, `Promise`, …). **Not available:** `setTimeout`, `setInterval`, `fetch`, `require`, `process`, `Buffer`, `eval`, and the script-only globals `on`, `stream`, `ai`, `say`, `ask` and `script`. **Top-level code runs at load time without these globals.** The file is evaluated once to read its configuration before any device exists, so code outside functions must not call `driver`, `connector`, `log`, `delay` or `timestamp`. Declare variables and helper functions at the top level; call things only inside getters, setters and methods. ## Talking to other connectors Connector files are denied access to other connectors unless they list them: ```js export default { driver: "Virtual", allowedConnectors: ["psu", "dmm"], properties: { efficiency: { get: async () => { const pin = await connector("psu").power; const pout = await connector("dmm").power; return pin > 0 ? pout / pin : null; }, description: "Output / input power", }, }, }; ``` Do not call `connector()` at the top level (other connectors may not be up yet), and do not call your own connector's custom members through `connector("")` — that waits on itself forever. Scripts are not subject to `allowedConnectors`. ## Network hosts `createTcpTransport` may only reach `localhost`, loopback and private addresses (`10.x`, `172.16–31.x`, `192.168.x`, `169.254.x`). Anything else — a public address, or any other host *name* — is refused unless: - the user turns on `security.allowExternalTransports` in `workspace/config/server.json` (server-wide), or - the transport is created inside one of this connector's getters, setters or methods and the host is listed in `allowedHosts: ["api.vendor.com"]`. A transport built at the top level, such as `config: { _transport: createTcpTransport(…) }`, is created while the file loads, before `allowedHosts` applies, so it needs the server-wide setting. ## Visibility: `hide` and `expose` `hide: ["rawCmd", …]` removes backend or custom items from the Connectors panel, Muxit's AI and MCP; `expose: [...]` keeps only the listed items. Use one or the other, never both (load error). Hidden items are still callable from scripts and still polled. ## AI instructions ```js ai: { instructions: "60 V / 2.5 A bench supply feeding a DUT rated 12 V. Never set more than 13 V. Ask before enabling the output." }, ``` Muxit's own assistant sees this next to the device in every conversation. Put safety rules, units, what the device is connected to, and how to use it there. Keep it to a few sentences. ## Safety ```js safety: { limits: { voltage: { min: 0, max: 13 }, current: { min: 0, max: 2.5 } }, confirm: { output: "once", reset: "always" }, rate: { voltage: 10 }, }, ``` | Key | Meaning | |---|---| | `limits` | `{ prop: { min, max, largeChangeFraction? } }`. Writes outside `min`–`max` are blocked (except at the Unrestricted safety level). A change bigger than `largeChangeFraction` (default 0.25) of the range asks for confirmation at the Assisted level. | | `confirm` | `{ item: "once" \| "always" \| "never" \| "typed" \| "onLargeChange" }` per property or action. `always` / `typed` also mark an action as destructive. Any other value is a load error. | | `rate` | `{ item: n }` — at most `n` operations per second (in practice rounded up: values below 1 allow one per second). | | `overrideLevel` | Pin this connector to `"Observe"`, `"Assisted"`, `"Active"` or `"Unrestricted"` regardless of the global level. | Know the limits of the block: - **It is enforced only on Muxit's Lab tier.** On other tiers it is ignored (with a warning in the server log) and only the global safety level applies. Write it anyway — it documents the safe range and protects Lab users — but also check ranges in your own methods and throw. - It applies to writes and actions coming *into* the connector (scripts, dashboards, AI). Calls your own code makes through `driver` are not checked. - `interlock` and `safeState` are accepted but not enforced yet; do not rely on them. - Drivers that opt out of the safety gate (Virtual, FileAccess, Webcam, Onvif, WebhookNotifier) skip it entirely, so a `safety` block on them does nothing. ## Loading and reloading Muxit does not watch connector files. After the file is saved, the user clicks **Reload** in the Connectors panel (or restarts the server). If their workspace has an explicit list of enabled connectors, a new file starts disabled until they tick it in the Connectors panel. A file that fails to load shows the error, with a line number, on its card. ## Multi-value replies When one SCPI query returns several values (`U=12.00 I=0.103 P=1.236`), query once per poll cycle into a module-level variable and parse it in cheap getters: ```js let raw = ""; const field = (key) => { const m = raw.match(new RegExp(`${key}=([-+]?[\\d.]+(?:[eE][-+]?\\d+)?)`)); return m ? Number(m[1]) : null; }; export default { protocol: "Scpi", connection: { type: "serial", port: "COM3", baudRate: 9600 }, config: { terminator: "\r\n" }, properties: { reading: { get: async () => (raw = await driver.query("VAL?")), poll: true, description: "Raw VAL? reply" }, voltage: { get: () => field("U"), poll: true, description: "Voltage (V), parsed from VAL?" }, current: { get: () => field("I"), poll: true, description: "Current (A), parsed from VAL?" }, }, }; ``` ## Complete examples **SCPI bench supply over LAN** ```js // workspace/connectors/psu.js const MAX_V = 30, MAX_A = 3; async function applySetpoint(volts, amps) { if (!(volts >= 0 && volts <= MAX_V)) throw new Error(`voltage must be 0–${MAX_V} V`); if (!(amps >= 0 && amps <= MAX_A)) throw new Error(`current must be 0–${MAX_A} A`); await driver.send(`APPL ${volts},${amps}`); return { volts, amps }; } export default { protocol: "Scpi", connection: { type: "tcp", host: "192.168.1.50", port: 5025 }, // ← the user's IP config: { terminator: "\n", timeoutMs: 2000, autoConnect: true, schema: { properties: { voltage: { cmd: "VOLT", type: "float", unit: "V", description: "Programmed voltage" }, current: { cmd: "CURR", type: "float", unit: "A", description: "Programmed current limit" }, output: { cmd: "OUTP", type: "bool", description: "Output enable" }, measured_voltage: { cmd: "MEAS:VOLT?", type: "float", unit: "V", description: "Measured voltage" }, measured_current: { cmd: "MEAS:CURR?", type: "float", unit: "A", description: "Measured current" }, }, methods: { save: { cmd: "*SAV", arg: "int", description: "Save settings to memory slot N" }, recall: { cmd: "*RCL", arg: "int", description: "Recall settings from memory slot N" }, }, }, }, properties: { power: { get: async () => { const v = await driver.measured_voltage; const i = await driver.measured_current; return Math.round(v * i * 1000) / 1000; }, description: "Output power (W), computed", poll: true, }, }, methods: { apply: { fn: (volts, amps) => { if (typeof volts === "object" && volts !== null) ({ volts, amps } = volts); return applySetpoint(Number(volts), Number(amps)); }, description: "Set voltage (V) and current limit (A) in one step: apply(volts, amps)", }, last_error: [() => driver.query("SYST:ERR?"), "Read the next entry of the error queue"], }, safety: { limits: { voltage: { min: 0, max: MAX_V }, current: { min: 0, max: MAX_A } }, confirm: { output: "once", apply: "always" }, }, ai: { instructions: "Bench PSU, 0–30 V / 0–3 A. Ask before switching the output on or raising the voltage." }, }; ``` **Arduino sketch printing text lines** ```js // workspace/connectors/env_sensor.js — the sketch prints "TEMP: 23.4" and "HUM=41.0%" lines export default { protocol: "LineText", connection: { type: "serial", port: "/dev/ttyACM0", baudRate: 115200 }, // ← the user's port config: { terminator: "\n", schema: { properties: { temperature: { prefix: "TEMP: ", type: "float", unit: "°C", description: "Last temperature line" }, humidity: { match: "^HUM=([-+]?\\d+(?:\\.\\d+)?)%?$", type: "float", unit: "%", description: "Last humidity line" }, }, methods: { set_led: { cmd: "LED", arg: "bool", description: "LED on/off — sends LED ON / LED OFF" }, }, }, }, ai: { instructions: "Arduino environment sensor. Values are the last line the sketch printed." }, }; ``` **Modbus RTU sensor** ```js // workspace/connectors/th_sensor.js — slave 1, holding registers 0 (temp ×10) and 1 (RH ×10) export default { protocol: "BinaryStream", connection: { type: "serial", port: "/dev/ttyUSB0", baudRate: 9600 }, config: { timeoutMs: 1000, schema: { framing: { header: "01 03", // slave address + function code of the reply lengthOffset: 2, lengthWidth: 1, // byte count follows the header crc: "modbus", request: "01 03 00 00 00 02", // read 2 registers from 0; CRC is appended maxStaleMs: 250, }, properties: { temperature: { offset: 3, width: 2, type: "int", endian: "big", scale: 0.1, unit: "°C", description: "Register 0" }, humidity: { offset: 5, width: 2, type: "uint", endian: "big", scale: 0.1, unit: "%", description: "Register 1" }, }, }, }, }; ``` ## Common mistakes | Mistake | Instead | |---|---| | `const v = driver.voltage; return v * 2;` | `async` getter, `const v = await driver.voltage;` | | `this.state.x`, `this.driver` | Module-level variables; the `driver` global. | | `require(…)`, `import fs from "fs"`, `fetch(…)`, `setInterval(…)` | None exist. Polling replaces timers; drivers or connectors replace I/O. | | The words `require(`, `eval(`, `Function(` in a comment | The loader rejects the file on a text match; reword the comment. | | `module.exports = …`, `export const …` | One `export default { … }`. | | Two schema properties with the same `cmd` | One query, parsed — [multi-value replies](#multi-value-replies). | | A property for a command that has no query form (`*RST`, `OUTP:PROT:CLE`) | A method. | | `min`, `max`, `scale`, `enum` on a SCPI schema property | `safety.limits`, or custom properties. | | `scale: "0.1"` on a BinaryStream field | `scale: 0.1` | | `header` with no `size` or length field | Add `size` or `lengthOffset` + `lengthWidth`. | | Returning `{ value: 12.1, unit: "V" }` | `12.1`, with the unit in the description. | | `poll: ["voltage"]` to make a schema property update | Already polled; `poll` is for custom properties only. | | `driver.myCustomMethod()` | Call a module-level helper function. | | `async (target) => …` as a method that takes arguments | `(target) => helper(target)` so the arguments are visible. | | Calling `log`, `driver` or `connector` at the top level | Only inside getters, setters and methods. | | Long loops inside a method | A script. | | Relying on `safety` as the only range check | Also validate in your methods. | | Invented SCPI commands or register maps | From the manual or the user; flag what is unverified. | ## Checklist - [ ] File `workspace/connectors/.js`, one `export default { … }`, no `import` / `require`, and none of `require(`, `import(`, `eval(`, `Function(` anywhere in the text. - [ ] Exactly one of `protocol` / `driver` / `engine` (or none, for a Virtual store with only recognised top-level keys). - [ ] Protocol connectors have a `connection` block with the right `type` and the user's port, host or USB ids — called out as values to check. - [ ] Every command, line format, register offset and baud rate comes from the manual or the user; anything unverified is listed. - [ ] SCPI schema: one `cmd` per property, `?` only on query-only commands, no unsupported fields; commands without a query form are methods. - [ ] Every getter or method that reads `driver.*` is `async` and `await`s each read. - [ ] Methods that take arguments are plain arrows (or `function`), accept positional arguments *and* a single object, describe their arguments, validate ranges and throw on bad input. - [ ] No `this`, no timers, no Node or browser APIs, nothing but declarations at the top level. - [ ] Only properties the user needs, since every readable backend property is polled. - [ ] `safety.limits` for every setpoint with a known safe range, `confirm` on outputs and destructive actions, and `ai.instructions` with the device's safety rules. - [ ] Other connectors used from this file are in `allowedConnectors`. - [ ] Told the user to save the file, click **Reload** in the Connectors panel, and enable the connector if it shows as disabled. # Writing Muxit scripts *A reference for AI assistants. It is self-contained: you can write a correct script from this page alone. Read [How a script runs](#how-a-script-runs) first and finish with the [checklist](#checklist).* A **script** is automation: a measurement run, a sweep, a monitoring loop, a data logger, an alarm. It uses devices that already have connectors, by name — `connector("psu")` is the connector defined in `workspace/connectors/psu.js`. If a device has no connector yet, write that first ([Writing Connectors](/for-ai/connectors.md)). ## The deliverable - **One file**, `workspace/scripts/.js`. Sub-folders are fine (`workspace/scripts/tests/iv-curve.js`). - **Plain top-level JavaScript.** No `export`, no `import`, no `main()` — the file body is the program. A top-level `return value;` ends the script and hands `value` back to whoever ran it. - Scripts in `workspace/startup/` (top level only, alphabetical order) start automatically when the server starts, after connectors are up. Auto-start needs a Maker licence or higher. - There are **no script parameters**. Put settings in constants at the top of the file, read them from a connector (a [Virtual](/for-ai/connectors.md#virtual-no-hardware) store a dashboard can edit), or ask the operator with [`ask.*`](#operator-interaction). ## You need the connector names and their properties A script can only use names the connector actually has; anything else throws `ReferenceError: Connector 'psu' has no property or method 'voltag'`. Do not guess names. Ask the user for the connector file, or for the property and action list shown when the connector is expanded in Muxit's **Connectors** panel. If you are connected to Muxit over MCP, call `list_connectors` / `get_connector_schema`. Know which names are **properties** (read with `dev.x`, write with `dev.x = v`) and which are **actions** (call with `dev.x(…)`). Reading an action without parentheses gives you a function, not a value — a camera's `snapshot`, for example, is an action: `cam.snapshot()`. ## How a script runs ```js const psu = connector("psu"); psu.voltage = 5; // write: blocks until the device has it psu.output = true; delay(500); // let it settle const v = psu.measured_voltage; // read: a plain value, no await log.info(`Measured ${v} V`); return { v }; // optional result ``` 1. **Everything is synchronous.** Property reads, writes, action calls, `delay`, `ai` and `ask` block the script until they finish and return plain values. Do not write `await`; it is unnecessary (it does no harm on a value, but `await dev.x = 5` is a syntax error). 2. **Loops must call `delay(ms)`.** A loop without one spins a CPU core until stopped. `delay` caps at 300 000 ms (5 minutes) per call. 3. **There are no timers.** `setTimeout`, `setInterval` and `requestAnimationFrame` do not exist. Use a `while` loop with `delay`. 4. **Stop is a hard kill.** When the user presses Stop, the engine is interrupted wherever it is: code after the loop does not run and **`finally` blocks do not run**. Put setup before the loop, leave the hardware in a safe state at every iteration, and never rely on cleanup code at the end. A script that should run until stopped is simply `while (true) { …; delay(ms); }` — there is no `script.running` flag. 5. **An uncaught error ends the script**, logged with its line number. Wrap device calls you expect to fail in `try` / `catch`. 6. Different scripts run concurrently; the same script cannot be started twice. The Free tier allows three running scripts at once. ## Device access ```js const dev = connector("name"); // device("name") is an alias ``` `connector()` throws if the connector does not exist or is disabled. **Reading** `dev.prop` returns the latest polled value. Muxit polls every readable property the device backend declares every 200 ms, so a read costs no I/O and can be up to about 200 ms old. Properties the connector computes itself are recalculated on every read. **Writing** `dev.prop = value` sends the value to the device and waits for the write to complete. The value you read back immediately afterwards is still the *previous polled* value; to confirm a setting, `delay(300)` and read again (or read a measured property after the device has settled). Assigning `null`, `undefined`, `NaN` or `[]` does nothing. A property the *connector file* defines with its own setter is the exception: the write is queued (at most one per 100 ms) and an error in it only reaches the server log. **Actions** are called with positional arguments in the documented order: ```js dev.reset(); // no arguments psu.save(3); // one argument wh.notify("Run done", "12 points", "success"); // several dev.moveTo({ x: 10, y: 20 }); // or one object with named arguments ``` For a driver action, positional arguments are matched to the declared argument names, and an object is passed through as named arguments. **Do not pass a lone array** — `dev.load([1, 2, 3])` is treated exactly like `dev.load(1, 2, 3)`. Wrap it: `dev.load({ values: [1, 2, 3] })`. What comes back is converted to plain JavaScript: numbers, strings, booleans, `null`, arrays and objects. ## Globals | Global | What it does | |---|---| | `connector(name)`, `device(name)` | Device proxy, as above. | | `log.info(…)`, `log.warn(…)`, `log.error(…)`, `log.debug(…)` | Log to the script output panel. Any number of arguments, joined with spaces; plain objects are JSON-formatted, arrays are not — `JSON.stringify` them yourself. | | `console.log`, `console.warn`, `console.error` | Aliases of `log.info` / `log.warn` / `log.error`. There is no `console.info` or `console.debug`. | | `delay(ms)` | Sleep, up to 300 000 ms. Cut short when the script is stopped. | | `timestamp()` | Current time as an ISO 8601 UTC string. | | `emit(event, data)` | Send an event to other scripts. Each call is delivered, even with the same data twice. | | `on(event, handler)`, `waitFor(event, opts?)` | Receive events. See [Events](#events). | | `stream(connectorName, streamName, data)` | Push data to dashboard widgets subscribed to that stream (a terminal, a chart, a canvas). | | `say(text, opts?)` | Show `text` in the chat panel, spoken aloud when the user has script speech on. `opts.emotion`: `"excited"`, `"serious"`, `"whisper"`, … | | `ask.confirm(msg, opts?)`, `ask.choose(msg, choices, opts?)`, `ask.text(msg, opts?)` | Ask the operator and wait for the answer. See [Operator interaction](#operator-interaction). | | `ai(prompt, image?, options?)` | One LLM call, returns the answer as a string. See [AI](#ai). | | `script.name` | This script's name: its path under `scripts/` without `.js` (`tests/iv-curve`), or `startup/`. | Standard JavaScript is available: `Math`, `JSON`, `Date`, `Number`, `String`, `Array`, `Object`, `Map`, `Set`, `RegExp`, `Promise`, `Proxy`, `Reflect`, `globalThis`, template strings, destructuring, classes and helper functions. **Not available:** `require`, `import`, `fetch`, `XMLHttpRequest`, `WebSocket`, `fs`, `process`, `Buffer`, `atob` / `btoa`, `TextEncoder`, `setTimeout`, `setInterval`, `eval`, `new Function`, and every Node.js or browser API. Files go through a FileAccess connector; HTTP goes through a WebhookNotifier connector; scheduling is a loop with `delay`. ## Events Scripts can signal each other, and react to property changes, without polling. ```js emit("unit-done", { serial, pass }); // script A on("unit-done", (u) => log.info(u.serial)); // script B: runs for every event const u = waitFor("unit-done", { timeout: 60000 }); // or block for the next one ``` - A bare name is another script's `emit()`. `"state:."` fires when that property's value changes; an unchanged value fires nothing. `"connector:."` fires on every connector-config `emit()`, JS-driver `__emitEvent`, and every matching line of a LineText property with `emit:`, repeats included: use it for barcode scans. `"dashboard:"` fires each time a dashboard widget with `config.emit: ""` is used; its data is `{ value, user, widget }`. A trailing `*` matches by prefix (`"state:psu.*"`); `meta.event` is the actual name. - `waitFor` options: `timeout` (ms), `default` (returned on timeout; without it the call throws an error with `code === "EVENT_TIMEOUT"`), `where` (filter function). After the first `waitFor(name)`, later events of that name are buffered until taken, so a loop around `waitFor` loses nothing while it is busy. - Events are delivered only while the script waits, in `delay()`, `waitFor()` or `ask.*`. A loop that never calls either never runs a handler. - A script that ends its top-level code with `on()` handlers registered stays running until stopped. Don't add a `while (true)` loop just to keep it alive. - Never use `on()` for safety: a handler runs at the next wait, not instantly. Limits belong in the connector's `safety` block. ## Files The sandbox has no file system. Files go through a connector using the built-in **FileAccess** driver, sandboxed to a folder of the workspace (by default `workspace/data/`). A new workspace ships one named **`fileaccess`** — confirm the name with the user; if they have none, they add a connector `{ driver: "FileAccess", config: { basePath: "data" } }` first. ```js const fa = connector("fileaccess"); const f = fa.openText("runs/iv-curve.csv"); // path relative to workspace/data/ f.write("time,voltage_v,current_a\n"); // overwrite (creates folders) f.append([timestamp(), 5.0, 0.103]); // array → one CSV row, quoted as needed f.append("a plain line\n"); // strings are written as-is const text = f.read(); // whole file as a string const lines = text.split("\n"); f.exists; // property: true / false f.size; // property: bytes f.rename("archive/iv-curve.csv"); // moves the file; the handle follows f.delete(); // false if it was already gone const tsv = fa.openText("out.tsv", "\t"); // another delimiter for array rows const { files, dirs } = fa.list("runs"); // one folder, not recursive fa.mkdir("runs/2026"); const img = fa.openBinary("snapshots/part.jpg"); img.write(connector("webcam").snapshot()); // binary handles read and write base64 ``` Paths are always relative to the sandbox folder: `..` that escapes it is refused, and a leading `/` is not an absolute path (`"/tmp/x.csv"` lands in `data/tmp/x.csv`). Files are limited to 10 MB by default. Prefer `read().split("\n")` over `readLines()`, and avoid `info()`: both return .NET objects rather than plain JavaScript ones. ## Operator interaction ```js if (!ask.confirm("Output will switch on at 12 V. Continue?", { timeout: 60000, default: false })) { return "cancelled"; } const port = ask.choose("Which fixture?", ["A", "B", "C"], { default: "A", timeout: 120000 }); const label = ask.text("Batch label?", { timeout: 300000 }); ``` - The script waits until a dashboard user answers. - `timeout` is in milliseconds and at least 1000. Without it the script waits indefinitely. - On timeout the script gets `default`; with no `default` it throws `PROMPT_TIMEOUT`. - `requireObserver: true` pauses the countdown while no dashboard is open. ## AI ```js const verdict = ai("Reply PASS or FAIL only: is 85 °C acceptable for a motor rated 90 °C?"); const seen = ai("Is the part seated in the fixture? Answer yes or no.", connector("webcam").snapshot()); ``` `ai()` returns a string (up to about 1024 tokens) and has no memory between calls and no tools. The second argument is a base64 JPEG, or an array of up to 8 images (strings or `{ image, label }`). It uses the AI provider configured in Muxit: text works on every tier with a local model and needs Maker on the Muxit-managed service; any call with an image needs Maker. When the tier does not allow it, `ai()` throws — wrap it in `try` / `catch` if the script must keep going. ## Safety Writes and actions a script makes go through Muxit's safety gate, using the safety level the user has chosen and — on the Lab tier — the connector's `safety` block. (Connectors on drivers with no hardware to protect — Virtual, FileAccess, Webcam, Onvif, WebhookNotifier — skip the gate.) - A blocked operation — out of range, or over a rate limit — throws an error with code `SAFETY_BLOCKED` and a message such as `limit.outOfRange`; the hardware never sees it. - An operation that needs confirmation **pauses the script** until someone approves it in a dashboard. A denial, or no answer within 30 seconds of a dashboard being open, throws (`user.denied`); with no dashboard open the script waits. - At the **Observe** level, ordinary writes are simulated without error and ordinary actions return `null`. So do not catch and ignore safety errors in a loop; let the script stop, or stop it yourself. ## Patterns **Sweep with CSV logging** ```js // workspace/scripts/iv-curve.js — step the PSU, log V and I. const psu = connector("psu"); const csv = connector("fileaccess").openText(`iv/${timestamp().replace(/[:.]/g, "-")}.csv`); const START_V = 0, STOP_V = 5, STEP_V = 0.25, SETTLE_MS = 400; csv.write("set_v,measured_v,measured_a\n"); psu.voltage = START_V; psu.output = true; for (let set = START_V; set <= STOP_V + 1e-9; set += STEP_V) { psu.voltage = Number(set.toFixed(3)); delay(SETTLE_MS); // settle + at least one poll cycle csv.append([set.toFixed(3), psu.measured_voltage, psu.measured_current]); } psu.output = false; // a natural end; Stop would skip this psu.voltage = 0; return "done"; ``` **Monitor with an alarm and hysteresis** ```js const sensor = connector("thermocouple"); const alarm = connector("webhook"); // a WebhookNotifier connector const LIMIT = 80, HYSTERESIS = 5; let armed = true; while (true) { const t = sensor.temperature; if (t === null || t === undefined) { log.warn("no reading"); delay(1000); continue; } if (armed && t > LIMIT) { alarm.notify("Over temperature", `${t.toFixed(1)} °C`, "error"); armed = false; } else if (!armed && t < LIMIT - HYSTERESIS) { armed = true; } delay(1000); } ``` **Wait for a condition, with a timeout** ```js function waitFor(check, timeoutMs, pollMs = 250) { const end = Date.now() + timeoutMs; while (Date.now() < end) { if (check()) return true; delay(pollMs); } return false; } const robot = connector("robot"); robot.moveTo({ x: 100, y: 50, z: 20 }); if (!waitFor(() => robot.idle, 30000)) throw new Error("move did not finish in 30 s"); ``` **Hand values to a dashboard**: write them to a Virtual connector's properties (`connector("store").status = "Running step 3/10"`); widgets bound to `store.status` update at once. ## Common mistakes | Mistake | Instead | |---|---| | `await dev.voltage`, `async function main()`, `main()` | Plain top-level code; reads return values directly. | | `setTimeout` / `setInterval` | `while (true) { …; delay(ms); }` | | `require("fs")`, `fetch(…)` | A FileAccess connector; a WebhookNotifier connector. | | Polling a value in a loop to notice that something happened | `waitFor("state:.")`, or `emit()` / `waitFor()` between scripts. | | Cleanup after the loop or in `finally` | Safe state at every iteration; stop naturally with `break` when cleanup matters. | | Loop without `delay` | Always `delay(ms)` inside loops. | | Reading a value right after writing it and expecting the new value | `delay(300)` first, or read a measured property. | | `dev.snapshot` for an action | `dev.snapshot()` — know which names are actions. | | `connector("files")` | Use the user's FileAccess connector name (`fileaccess` in a new workspace). | | Passing an array as the only argument | Wrap it in an object with the argument name. | | Invented property or connector names | Ask for the connector's schema. | | `read_property(…)`, `call_action(…)` inside a script | Those are AI tools, not script functions. Use the proxy. | ## Checklist - [ ] The file goes in `workspace/scripts/` (or `workspace/startup/` to auto-start) and is plain top-level code: no `import`, `export`, `require` or `main()`. - [ ] Every connector, property and action name came from the user or a schema, and actions are called with `()`. - [ ] No `await`, no `setTimeout` / `setInterval`, no `on()` / `emit()`, no Node or browser APIs. - [ ] Every loop calls `delay(ms)`. - [ ] Nothing important happens after the loop or in a `finally` block; the hardware is safe at every point the script could be stopped. - [ ] Setpoints are within limits the user confirmed; outputs are not switched on, and axes not moved, without the user asking for it. - [ ] Reads that must reflect a just-written value wait at least one poll cycle. - [ ] File access goes through the user's FileAccess connector with paths relative to its folder. - [ ] Anything you could not verify (connector names, units, safe ranges) is listed for the user. # Writing Muxit drivers *A reference for AI assistants. It is self-contained: you can write a correct driver from this page alone. Read [Before you write a driver](#before-you-write-a-driver) and [The contract every driver follows](#the-contract-every-driver-follows) first, then the section for your language, and finish with the [checklist](#checklist).* A **driver** is a backend that talks to one kind of device. On its own it does nothing: a [connector](/for-ai/connectors.md) selects it with `driver: ""`, passes it configuration, and gives the device a name. Scripts, dashboards and Muxit's AI then use the properties and actions the driver declares. ## Before you write a driver Most devices do not need one. A connector using a built-in protocol covers SCPI instruments (`Scpi`), anything that prints text lines (`LineText`) and fixed-layout binary frames such as Modbus RTU (`BinaryStream`) — see [Writing Connectors](/for-ai/connectors.md). If the user only wants to call some Python functions, the generic `Python` driver may be enough (see [Python connectors](/guides/python-connectors.md)). Write a driver when the device needs a vendor SDK or native library, a Python-only stack (numpy, pyvisa, a vendor wheel), a stateful handshake or session, or a protocol the three built-in ones cannot express. | Language | Choose it when | Runs as | |---|---|---| | **JavaScript** (`.driver.js`) | Serial or TCP devices with a text or simple binary protocol. Quickest to write; nothing to install. | Sandboxed V8 inside the Muxit server. | | **Python** (`.driver.py`) | The device has a Python SDK, or you need numpy / pyserial / pyvisa / sockets. | A separate Python 3.10+ process per connector. Needs Python on the user's machine. | | **C#** (`.dll` in a `.muxdriver`) | A .NET or native vendor SDK, high throughput, or full .NET. | Loaded into the server. Needs the Muxit driver SDK assembly to build. | ## The contract every driver follows Every driver, in every language, has the same shape: - **A name** (`MyDevice`) — what connectors write in `driver: "MyDevice"`. It may not equal a built-in name (`Scpi`, `Webcam`, `TestDevice`, `Virtual`, …); comparison ignores case. - **A group**: `instruments`, `motion`, `communication` or `utilities`. - **Properties**, each `{ name, type, access, unit?, description, details? }`. - **Actions**, each `{ name, description, args?, details? }`. - **Streams** (optional): names of continuous outputs (lines, frames, spectra) that dashboard widgets subscribe to. - **Lifecycle**: `init(config)` once when the connector starts (open the connection here, never at load time); `get(property)`, `set(property, value)`, `execute(action, args)` while running; `shutdown()` when it stops. **`access`** is exactly `"R"`, `"W"` or `"R/W"` — uppercase, with the slash. **Every `"R"` and `"R/W"` property is read every 200 ms** for as long as the connector runs, so `get` must be cheap: return a cached value, or do one short query. Spellings such as `"r"` or `"rw"` are never polled, so their values never reach dashboards on their own. **`type`** is free text used for display and for Muxit's AI; use one of `"string"`, `"int"`, `"double"`, `"bool"`, `"double[]"`, `"int[]"`, `"string[]"`, `"object"`. **`description`** is one line and is always shown — in the UI, in IntelliSense, and in Muxit's AI prompt. **`details`** is optional markdown fetched only on demand: put argument enums, units, side effects, timeouts and failure modes there, not in the description. **`config`** is the connector's configuration: the connector's `config: { … }` plus any other non-reserved top-level keys. Read every option with a default and validate required ones in `init`, throwing a clear error (`'MyDevice needs config.port, e.g. "COM3"'`). **`args`** arrive as an object keyed by the declared argument names. A script call `dev.move(10, 20)` arrives as `{ x: 10, y: 20 }` when the action declares `x` then `y`; a call with one object passes that object. Declare arguments in the order a caller would naturally pass them. **Errors**: throw (or raise) with a message a user can act on; it reaches the script, dashboard or AI that made the call. **Safety**: the user's safety gate checks every write and action made on a connector that uses your driver. Third-party drivers cannot opt out — `requiresSafetyGates: false` is ignored for JavaScript and Python drivers and for unsigned C# drivers. ## JavaScript drivers One file, `.driver.js`, with a single `export default` object. ```js // workspace/drivers/mydevice.driver.js // Helpers may be declared at the top level, but never *call* host globals (log, // createSerialTransport, setTimeout) there: the file is also evaluated at scan time without them. function openSerial(config) { if (!config.port) throw new Error('MyDevice needs config.port, e.g. "COM3" or "/dev/ttyUSB0"'); return createSerialTransport(config.port, { baudRate: config.baudRate ?? 115200, delimiter: config.delimiter ?? "\n", timeout: config.timeout ?? 5000, }); } export default { meta: { name: "MyDevice", version: "1.0.0", group: "instruments", description: "Line-based serial instrument with a setpoint, a measurement and a line stream.", properties: { connected: { type: "bool", access: "R", description: "Serial port is open" }, setpoint: { type: "double", access: "R/W", unit: "V", description: "Output setpoint", details: "Writing sends `SETP ` and waits up to 2 s for the reply." }, measurement: { type: "double", access: "R", unit: "V", description: "Last measured value" }, }, actions: { measure: { description: "Take a fresh measurement and return it" }, send: { description: "Send a raw line and return the reply", args: { text: "string" }, details: "Appends `\\n`. Fails after 2 s without a reply line." }, }, streams: ["output"], }, // State lives on the object; the lifecycle methods are called with `this` bound to it. _transport: null, _pending: [], _setpoint: 0, _measurement: null, async init(config) { this._pending = []; this._transport = openSerial(config); this._transport.onData = (line) => { // set before connect() const waiter = this._pending.shift(); if (waiter) waiter.resolve(line); __emitStream("output", line); // stream data must be a string }; if (!this._transport.connected) await this._transport.connect(); }, async get(property) { // polled every 200 ms: cached values only switch (property) { case "connected": return this._transport?.connected ?? false; case "setpoint": return this._setpoint; case "measurement": return this._measurement; default: throw new Error(`Unknown property: ${property}`); } }, async set(property, value) { if (property !== "setpoint") throw new Error(`Property '${property}' is read-only`); const v = Number(value); if (!Number.isFinite(v)) throw new Error(`setpoint must be a number, got ${value}`); await this._request(`SETP ${v}`); this._setpoint = v; }, async execute(action, args) { switch (action) { case "measure": { this._measurement = Number(await this._request("MEAS?")); return this._measurement; } case "send": return this._request(String(args?.text ?? "")); default: throw new Error(`Unknown action: ${action}`); } }, async shutdown() { for (const waiter of this._pending) waiter.reject(new Error("Driver shut down")); this._pending = []; if (this._transport) { this._transport.onData = null; this._transport.close(); } this._transport = null; }, // One request, one reply line. Replies are matched through onData, not transport.query(). _request(text, timeoutMs = 2000) { if (!this._transport?.connected) return Promise.reject(new Error("Not connected")); return new Promise((resolve, reject) => { const waiter = {}; const timer = setTimeout(() => { this._pending = this._pending.filter((w) => w !== waiter); reject(new Error(`No reply to '${text}' within ${timeoutMs} ms`)); }, timeoutMs); waiter.resolve = (line) => { clearTimeout(timer); resolve(line); }; waiter.reject = (err) => { clearTimeout(timer); reject(err); }; this._pending.push(waiter); this._transport.write(text + "\n"); }); }, }; ``` **Rules for JavaScript drivers** - The file contains `export default` and a `meta` object. No other `export`, no `import` (stripped), and the strings `require(` and `import(` may not appear anywhere, comments included. `eval` and `new Function` throw. - Write the lifecycle functions as methods (`async init(config) { … }`), not arrow functions — they need `this`. - `meta` fields read: `name`, `version`, `description`, `group`, `properties`, `actions`, `streams`. In `actions`, `args` maps each name to a **type string** (`{ x: "double" }`); arguments have no descriptions, so explain them in the action's `details`. - Calls to one connector's driver run one at a time: a slow `execute` delays the `get` polls behind it. - Returning `undefined` or `""` reaches the caller as `null`. **Globals in a JavaScript driver:** `log.info/warn/error/debug`, `console.log/warn/error` (server console only), `timestamp()`, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `createSerialTransport(path, { baudRate, delimiter, timeout })`, `createTcpTransport(host, port, { delimiter, timeout })`, `__emitStream(name, string)`, and `__emitEvent(name, data)`, which publishes `connector:.` for scripts' `on()` / `waitFor()` on every call (use it for occurrences, such as a scan, that may repeat the same value). There is no `delay` (use `new Promise((r) => setTimeout(r, ms))`), no `emit`, no `Buffer`, no `fetch`, no `require`, no Node or browser API. **Transports.** `createSerialTransport` honours only `baudRate`, `delimiter` and `timeout` (8 data bits, no parity, 1 stop bit). `createTcpTransport` may only reach `localhost`, loopback and private network addresses; anything else needs the user to turn on `security.allowExternalTransports` in `workspace/config/server.json` (a connector's `allowedHosts` does not reach drivers). The transport object has `connect()`, `write(string)`, `close()`, `connected` and `onData = (line) => …` (one call per received line, split on `delimiter`); serial transports also have `setRawMode(true)` with `onRawData = (hex, ts) => …` (each received chunk as an uppercase hex string) for byte-level protocols. `write()` sends text as UTF-8, so bytes ≥ `0x80` become two bytes on the wire; to send binary use `writeRaw([0xFE, 0xFF, …])` (a byte array, `Uint8Array`, single number, or a string whose char codes are all ≤ `0xFF`), which sends exactly those bytes. **`connect()`, `write()` and `query()` return .NET tasks that `await` does not unwrap**: `await transport.query(cmd)` does not give you the reply, so match replies to requests through `onData`, as `_request` does above. A serial `connect()` has opened the port when it returns (or thrown); a TCP `connect()` returns before the connection is up, so wait until `transport.connected` is true, with a timeout. ## Python drivers One file, `.driver.py`, defining a subclass of `muxit_driver.Driver`. Muxit provides the `muxit_driver` package; do not install or vendor it yourself for a loose driver file. ```python # workspace/drivers/mydevice.driver.py from __future__ import annotations from typing import Any from muxit_driver import Driver, run class MyDevice(Driver): META = { "name": "MyDevice", "version": "1.0.0", "description": "Serial instrument via pyserial: setpoint, measurement and a reading stream.", "group": "instruments", "properties": { "connected": {"type": "bool", "access": "R", "description": "Serial port is open"}, "setpoint": {"type": "double", "access": "R/W", "unit": "V", "description": "Output setpoint"}, "measurement": {"type": "double", "access": "R", "unit": "V", "description": "Last measured value"}, }, "actions": { "measure": { "description": "Take N readings and return them", "args": {"count": {"type": "int", "description": "Number of readings (default 1)"}}, "details": "Each reading is also emitted on the `readings` stream.", }, }, "streams": ["readings"], } def __init__(self) -> None: self._ser = None self._setpoint = 0.0 self._measurement: float | None = None def init(self, config: dict[str, Any] | None) -> None: config = config or {} import serial # third-party imports go here, never at module level port = config.get("port") if not port: raise ValueError('MyDevice needs config.port, e.g. "COM3" or "/dev/ttyUSB0"') self._ser = serial.Serial(str(port), int(config.get("baudRate", 115200)), timeout=2) self.log(f"opened {port}") def _query(self, command: str) -> str: self._ser.write((command + "\n").encode()) reply = self._ser.readline().decode(errors="replace").strip() if not reply: raise TimeoutError(f"no reply to {command!r}") return reply def get(self, property: str) -> Any: # polled every 200 ms: cached values only if property == "connected": return bool(self._ser is not None and self._ser.is_open) if property == "setpoint": return self._setpoint if property == "measurement": return self._measurement raise KeyError(f"unknown property: {property}") def set(self, property: str, value: Any) -> None: if property != "setpoint": raise KeyError(f"property '{property}' is read-only") self._query(f"SETP {float(value)}") self._setpoint = float(value) def execute(self, action: str, args: dict[str, Any] | None) -> Any: args = args or {} if action == "measure": values = [] for _ in range(int(args.get("count", 1))): self._measurement = float(self._query("MEAS?")) values.append(self._measurement) self.emit("readings", {"v": self._measurement}) return values raise KeyError(f"unknown action: {action}") def shutdown(self) -> None: if self._ser is not None: self._ser.close() if __name__ == "__main__": run(MyDevice) ``` **Rules for Python drivers** - The file imports `Driver` and `run` from `muxit_driver`, defines `META`, and ends with `if __name__ == "__main__": run(YourClass)`. - **Action arguments in `META` are dicts**: `{"count": {"type": "int", "description": "…"}}`. A bare string (`{"count": "int"}`) breaks the driver scan and the driver does not appear. - **Import third-party packages inside `init`.** Muxit runs the module with `--scan` to read `META` on every start-up, where heavy or missing packages would slow or break the scan. - **Never `print()`**: stdout is the channel to Muxit. Use `self.log(message, level="info")`, `self.emit(stream, data)` (any JSON-serialisable data), `self.emit_progress(stage, line)`, or write to stderr. - Methods may be plain or `async def`. Requests are handled one at a time; `get`, `set` and `execute` time out after 30 seconds, `init` after 30 minutes (for first-run installs). - Return JSON-serialisable values; numpy arrays are converted with `.tolist()`. - Dependencies go in a `requirements.txt` next to the `.driver.py`. Muxit creates a virtual environment beside it and installs them on first use. There is no SDK transport helper — use `pyserial`, `socket`, `pyvisa` or the vendor package directly. - Test without Muxit: `python3 mydevice.driver.py --scan` prints the parsed `META`; `python3 -u mydevice.driver.py --instance dev` reads JSON-RPC lines such as `{"id": 1, "method": "init", "params": {"port": "COM3"}}` from stdin. Outside Muxit, `muxit_driver` must be importable (`PYTHONPATH` pointing at a copy of it). ## C# drivers A .NET class library implementing `Muxit.Driver.Sdk.IConnectorDriver`, shipped in a `.muxdriver` package. Build it against the **Muxit.Driver.Sdk** assembly, which is not on NuGet: it ships with Muxit (as a file in the Linux and macOS install folders) and is the `drivers/Muxit.Driver.Sdk` project in Muxit's source. Reference it without copying it into your output, and never put it in the package — the server always supplies its own copy. ```xml net8.0 enable enable MyDevice path/to/Muxit.Driver.Sdk.dll false ``` ```csharp // MyDeviceDriver.cs using Muxit.Driver.Sdk; using static Muxit.Driver.Sdk.DriverConfig; [assembly: DriverGroup(DriverGroup.Instruments)] // assembly-level, before the namespace namespace MyCompany.MyDevice; public sealed class MyDeviceDriver : IConnectorDriver { public string Name => "MyDevice"; public string? Version => "1.0.0"; public string? Description => "Example instrument with a setpoint and a measurement."; public Action? StreamEmitter { get; set; } // required, even without streams private readonly object _lock = new(); private string _host = ""; private double _setpoint; public MyDeviceDriver() { } // public, parameterless, no I/O: also constructed at scan time public Task InitAsync(Dictionary? config) { _host = GetString(config, "host", ""); if (_host == "") throw new ArgumentException("MyDevice needs config.host, e.g. \"192.168.1.50\""); // open the connection here return Task.CompletedTask; } public Task ShutdownAsync() // also called on an instance that was never initialised { // close the connection if it is open return Task.CompletedTask; } public IEnumerable GetProperties() => [ new PropertyDescriptor("setpoint", "double", "R/W", "V", "Output setpoint", Details: "Range 0–30 V."), new PropertyDescriptor("host", "string", "R", Description: "Device address"), ]; public IEnumerable GetActions() => [ new ActionDescriptor("reset", "Reset to defaults"), new ActionDescriptor("ramp", "Ramp the setpoint to a target", [new ArgDescriptor("target", "double", "Target in V"), new ArgDescriptor("rate", "double", "V per second")]), ]; public Task GetAsync(string property) => Task.FromResult(property switch { "setpoint" => _setpoint, "host" => _host, _ => throw new ArgumentException($"Unknown property: {property}"), }); public Task SetAsync(string property, object? value) { if (property != "setpoint") throw new ArgumentException($"Property '{property}' is read-only"); var v = ToDouble(value); if (v is < 0 or > 30) throw new ArgumentOutOfRangeException(nameof(value), "setpoint must be 0–30 V"); lock (_lock) _setpoint = v; return Task.CompletedTask; } public Task ExecuteAsync(string action, object? args) { switch (action) { case "reset": lock (_lock) _setpoint = 0; return Task.FromResult("OK"); case "ramp": var target = ArgDouble(args, "target", _setpoint); // ... ramp at ArgDouble(args, "rate", 1.0) V/s ... lock (_lock) _setpoint = target; return Task.FromResult(target); default: throw new ArgumentException($"Unknown action: {action}"); } } } ``` **Rules for C# drivers** - Implement `IConnectorDriver` (there is no base class). Required members: `Name`, `InitAsync`, `ShutdownAsync`, `GetProperties`, `GetActions`, `GetAsync`, `SetAsync`, `ExecuteAsync`, and the `StreamEmitter` property. Optional: `Version`, `Description`, `SupportsStreaming`, `GetStreams`. - Descriptors are records: `PropertyDescriptor(Name, Type, Access, Unit = "", Description = "", Details = "")`, `ActionDescriptor(Name, Description = "", Args = null, Details = "")`, `ArgDescriptor(Name, Type, Description = "")`. Named arguments use the capitalised parameter names (`Description:`, `Details:`). - `[assembly: DriverGroup(...)]` is an **assembly** attribute: put it above the `namespace` line, not on the class. - Read configuration and arguments with the `DriverConfig` helpers (`GetString`, `GetInt`, `GetDouble`, `GetBool`; `ArgString`, `ArgInt`, `ArgDouble`, `ArgBool`, `ArgDoubleArray`; `ToDouble`, `ToInt`, `ToBool`). - **Calls are not serialised.** Polling, scripts and the UI call into the driver concurrently — guard shared state and the connection. - Streams: return `true` from `SupportsStreaming`, list names in `GetStreams()`, and call `StreamEmitter?.Invoke("data", json)` with a JSON string. The emitter is only set when `SupportsStreaming` is true. - Native libraries the vendor SDK needs go next to your DLL in the package root. ## Installing a driver **Loose file (JavaScript and Python).** Put `mydevice.driver.js` or `mydevice.driver.py` directly in `workspace/drivers/` — the top level, not a sub-folder. No manifest is needed. Muxit reads drivers **only when the server starts**, so the user restarts Muxit, then creates a connector: ```js // workspace/connectors/bench_device.js export default { driver: "MyDevice", config: { port: "/dev/ttyUSB0", baudRate: 115200 }, }; ``` **Package (`.muxdriver`, required for C#).** A ZIP file with these files at its root — no folder inside: | Type | Package contents | |---|---| | JavaScript | `manifest.json`, `.driver.js`, `template.js`, optional `README.md`, `CHANGELOG.md` | | Python | `manifest.json`, `.driver.py`, `template.js`, other `.py` files it imports, `requirements.txt` | | C# | `manifest.json`, `template.js`, `.dll`, its dependency DLLs and native libraries — **not** `Muxit.Driver.Sdk.dll` | Name it after the id and version (`yourname-mydevice-v1.0.0.muxdriver`) and put it directly in `workspace/drivers/` (C# and JavaScript packages can also be installed from Muxit's Extensions panel; Python packages only by copying the file). Restart the server. **`manifest.json`** ```json { "formatVersion": 1, "id": "yourname/mydevice", "name": "MyDevice", "version": "1.0.0", "description": "Line-based serial instrument driver with setpoint control and a line stream.", "author": { "name": "Your Name" }, "tier": 1, "category": "free", "group": "instruments", "entryPoint": "mydevice.driver.js", "tags": ["serial"], "license": "MIT" } ``` | Field | Rule | |---|---| | `formatVersion` | `1`. | | `id` | `publisher/name` (the `publisher/` part is optional): lowercase letters, digits and `-`, starting with a letter or digit; publisher 2–40 characters, name 3–40. | | `name` | The driver name connectors use; must match the code's name. | | `version` | `x.y.z`, no suffix. Match the code's version. | | `tier` | `1` JavaScript, `2` Python, `3` C#. | | `category` | `"free"`. (Premium drivers are signed by Muxit.) | | `entryPoint` | The file name only: `mydevice.driver.js`, `mydevice.driver.py`, or `.dll`. | | `group` | `instruments`, `motion`, `communication` or `utilities`. | | `description` | At least 40 characters. | | `author` | If present, must have `name`. | | `tags` | 1–8 of: `spectroscopy`, `motion`, `camera`, `robotics`, `power`, `daq`, `io`, `serial`, `networking`, `cnc`, `plotter`, `laser`, `monitor`, `terminal`, `arduino`, `esp32`, `gcode`, `grbl`, `drawing`, `canvas`, `pen`, `axidraw`, `audio`, `synth`, `notification`, `midi`, `printer`, `label`, `barcode`, `other`. | **`template.js`** is the starter connector Muxit offers when the user adds a connector for your driver — required in a package. It is an ordinary connector file: ```js // template.js export default { driver: "MyDevice", config: { port: "/dev/ttyUSB0", // "COM3" on Windows baudRate: 115200, }, ai: { instructions: "MyDevice bench instrument. Setpoint range 0–30 V." }, }; ``` Add a `CHANGELOG.md` with a `## 1.0.0` heading per version if the driver will be shared. ## Common mistakes | Mistake | Instead | |---|---| | `access: "rw"` or `"r"` | `"R/W"`, `"R"`, `"W"` exactly. | | Slow I/O in `get` | Cache values; update them in `execute`, from `onData`, or a background loop. `get` runs every 200 ms per readable property. | | Opening the port in the constructor or at module level | In `init(config)`. | | JavaScript: arrow functions for `init` / `get` / … | Methods, so `this` works. | | JavaScript: `await transport.query(cmd)` | Match replies through `onData`. | | JavaScript: `delay(ms)`, `Buffer.from`, `require("serialport")` | `new Promise((r) => setTimeout(r, ms))`, strings, the built-in transports. | | JavaScript: `__emitStream("data", { v: 1 })` | `__emitStream("data", JSON.stringify({ v: 1 }))` | | Python: `print(...)` | `self.log(...)` or stderr. | | Python: `"args": {"count": "int"}` | `"args": {"count": {"type": "int", "description": "…"}}` | | Python: `import numpy` at the top of the file | Inside `init`. | | C#: `[DriverGroup(...)]` on the class | `[assembly: DriverGroup(...)]` above the namespace. | | C#: no `StreamEmitter` property | Declare it even without streams. | | C#: shipping `Muxit.Driver.Sdk.dll` | Reference it with `false`; leave it out. | | `requiresSafetyGates: false` | Not available to third-party drivers; leave it out. | | A driver for a device a protocol already handles | A connector with `Scpi`, `LineText` or `BinaryStream`. | | Expecting the server to pick up a new driver file | Restart Muxit. | ## Checklist - [ ] A built-in protocol really cannot do this, and the language fits the device's SDK. - [ ] The driver name is unique, not a built-in name, and identical in code, manifest and `template.js`. - [ ] Every property has `type`, `access` (`"R"`, `"W"` or `"R/W"`) and a one-line `description`; actions describe their arguments; enums, units and side effects are in `details`. - [ ] `get` returns quickly from cached state; the connection opens in `init`, closes in `shutdown`, and `shutdown` is safe when `init` never ran. - [ ] `init` validates required configuration and throws a message that names the option. - [ ] Errors are thrown with actionable messages; nothing fails silently. - [ ] Every device command comes from the manual or the user; unverified ones are listed. - [ ] JavaScript: `export default` with `meta`, methods not arrows, replies via `onData`, no `require(` / `import(` text, stream data as strings. - [ ] Python: `META` action args are dicts, third-party imports inside `init`, no `print`, `run(...)` at the bottom, `requirements.txt` for dependencies. - [ ] C#: implements `IConnectorDriver` with `StreamEmitter`, assembly-level `DriverGroup`, thread-safe, SDK referenced but not shipped. - [ ] Told the user where the file goes, to restart Muxit, and gave them a connector that uses the driver.