Docs menu
Extend

Module SDK

Write a Tangle device module in JavaScript, TypeScript or Python: manifest, settings, controls, protocol, SDK APIs, testing and installing.

A device module teaches Tangle to control one kind of device. It is a folder with a manifest (tangle-module.json) and a program that tangle-core starts as a child process and talks to over stdin and stdout. This page shows how to write one with the JavaScript/TypeScript SDK (@tangle/module-sdk) or the Python SDK (tangle-module-sdk), test it, and install it.

What a module is

  • One folder per module, named after the module id.
  • tangle-module.json: the manifest. It says how to start the module, which capabilities it uses, which settings a device needs (configSchema), and which ready-made dashboard controls it offers.
  • An entry file: index.js, main.py or a native executable.
  • tangle-core starts one process per module and sends it every device that uses it. The module owns the connection to each device (TCP, UDP, HTTP, WebSocket...). The SDK handles the protocol, reconnects, polling, timeouts and status reporting, so you write only the device logic.

Start from one of the reference modules in the Tangle source tree: tangle-contract/examples/virtual-light-js, virtual-light-ts and virtual-light-py (the same simulated light in three languages), or tangle-connect/modules/misc.pjlink.projector, a complete networked module with password, polling and discovery.

Naming and folder

Module ids follow <manufacturer or misc>.<product or protocol>.<category>[.<version>]:

  • manufacturer: lowercase brand without spaces (barco, qsc), or misc for open protocols and generic devices.
  • product or protocol: the product line (watchout, pulse) or the protocol (pjlink, artnet).
  • category: one of timecode, projector, display, mediaserver, engine, player, computer, io, dsp, lighting, console, switcher, control.
  • version (optional): a protocol or product generation when they are not compatible (v6, v7, ma2). It is not the module's own version.

Use only a-z, 0-9 and - inside a part. When the manifest declares a category, the id's third part must equal it and the folder must be named exactly like the id: tangle module add refuses a folder my-projector holding barco.pulse.projector. A manifest without a category installs with a warning.

Python package names cannot contain dots, so put Python code in a normally named package inside the folder (misc.ssh.computer/ssh_computer/). Only the folder name matters.

The manifest

tangle-module.json is validated against ModuleManifest in tangle-contract/schema/module.schema.json.

FieldRequiredMeaning
idyesModule id, as above
nameyesDisplay name
versionyesModule version, 1.2.3 (optional -suffix)
publisheryes{ "name": "...", "url": "https://..." }
channelyesofficial or community
contractyesSemver range of the Tangle contract supported, for example "^0.2.0"
runtimeyes{ "kind": "node", "minVersion": "20.0.0" }, { "kind": "python", "minVersion": "3.11" } or { "kind": "native" }
entryyesEntry file, relative to the folder
descriptionOne or two sentences
categoryDevice category (see above)
devicesSupported models, [{ "manufacturer": "Epson", "model": "EB-L1100U" }]. Discovery uses this list to suggest the module
capabilitiesStandard capabilities used, for example ["tangle.power", "tangle.input"]
customCapabilitiesCapabilities this module defines (see below)
configSchemaJSON Schema for one device's settings
permissionsAny of network, serial, usb, midi, filesystem. Shown to the operator for consent at install
discoverytrue if the module implements discover
controls, controlLayoutsReady-made dashboard controls and arrangements
configuratorsDevice-side setup steps
processNative modules only: modules with the same process name share one process

While the contract is 0.x, "^0.2.0" does not match 0.3.0. Tangle refuses to install a module whose contract range does not include its own contract version.

Capabilities

A capability is a set of state keys and actions that lets dashboards, scenes and timelines control any device the same way. Standard capabilities:

CapabilityStateActions
tangle.poweronon, off, toggle
tangle.inputinputselect (input)
tangle.levellevel (0..1)set (level, fadeMs)
tangle.mutemutedmute, unmute, toggle
tangle.presetcurrentrecall (preset)
tangle.transportstatus, positionMs, clipplay, pause, stop, seek (positionMs), load (clip)
tangle.timecodetimecode, fps, running, signalstart, stop, locate (timecode)

Define your own in customCapabilities, with an id <module prefix>.<name>:

{
  "id": "pjlink.lamp",
  "version": 1,
  "description": "Lamp use (first lamp).",
  "state": { "hours": { "type": "number" }, "on": { "type": "boolean" } },
  "actions": {}
}

State values and action parameters are typed: boolean, number (min, max, step, unit), string, enum (values) or json.

Settings: configSchema

configSchema is a JSON Schema for one device's config. Tangle validates device settings against it, and the designer builds the device's settings form from it: strings, numbers, integers, booleans and enums become fields, default becomes a placeholder, required fields are marked, and anything else is edited as JSON.

Mark passwords, PINs, tokens and keys with "x-secret": true. Tangle stores them in the OS credential store, never in the project file, and fills them in before sending the device to your module.

"configSchema": {
  "type": "object",
  "properties": {
    "host": { "type": "string", "minLength": 1, "description": "IP address or host name of the projector." },
    "port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 4352 },
    "password": { "type": "string", "x-secret": true, "description": "PJLink password, if the projector has one." }
  },
  "required": ["host"],
  "additionalProperties": false
}

DMX modules put the universe a device uses in an integer universe setting. Licensing counts distinct universe values to apply the edition's universe limit.

Controls and layouts

controls are ready-made widgets for this module's devices, offered by the dashboard builder. Bindings and actions apply to whichever device the control is used with.

FieldMeaning
id, label, descriptionIdentity and text
widgetbutton, toggle, slider, meter, value, source, scenes, status, transport, cue, clock, label, image or group
propsWidget properties, each bound to a constant { "kind": "const", "value": ... }, a state value { "kind": "state", "capability": "...", "key": "..." } or the device status { "kind": "status" }
actionIntent sent when used: { "kind": "intent", "name": "level.set", "params": { "level": "{{value}}" } }. {{value}} (or the older $value) is replaced by the widget's value
offActionIntent for the off position of a toggle
confirmnone, confirm or hold
sizeDefault size in grid cells, { "w": 3, "h": 2 }

controlLayouts arrange controls into named sets (compact, full...) that the builder offers when a device is dragged onto a dashboard. Each placement gives control, x, y, w, h in grid cells. See Dashboards.

"controls": [
  {
    "id": "power", "label": "Power", "widget": "toggle",
    "props": { "value": { "kind": "state", "capability": "tangle.power", "key": "on" } },
    "action": { "kind": "intent", "name": "power.on" },
    "offAction": { "kind": "intent", "name": "power.off" },
    "size": { "w": 3, "h": 2 }
  },
  { "id": "status", "label": "Status", "widget": "status", "props": { "value": { "kind": "status" } }, "size": { "w": 3, "h": 1 } }
],
"controlLayouts": [
  { "id": "compact", "label": "Compact", "controls": [
    { "control": "power", "x": 0, "y": 0, "w": 3, "h": 2 },
    { "control": "status", "x": 0, "y": 2, "w": 3, "h": 1 }
  ] }
]

Configurators

configurators describe setup needed on the device side, such as a script to copy to a player or remote access to enable on a PC. Each has an id, a name, optional platforms (macos, windows, linux, device) and ordered steps of kind:

  • info: text only (Markdown in text);
  • download: files your module generates, to copy to the device;
  • script: a script your module generates, for the operator to run on the target;
  • check: runs an action (capability, action) to confirm the setup worked.

Your module builds the files and script from a device's configuration when the host calls configurator.run (see the SDK APIs below).

Note: The designer wizard that shows configurator steps is not available yet.

A minimal module in JavaScript

A simulated dimmable light, as in tangle-contract/examples/virtual-light-js. Folder misc.virtual-light.lighting/:

tangle-module.json:

{
  "id": "misc.virtual-light.lighting",
  "name": "Virtual light",
  "version": "0.1.0",
  "description": "Simulated dimmable light.",
  "category": "lighting",
  "publisher": { "name": "Your name" },
  "channel": "community",
  "contract": "^0.2.0",
  "runtime": { "kind": "node", "minVersion": "20.0.0" },
  "entry": "index.js",
  "devices": [{ "manufacturer": "Tangle", "model": "Virtual light" }],
  "capabilities": ["tangle.power", "tangle.level"],
  "configSchema": {
    "type": "object",
    "properties": { "startOn": { "type": "boolean", "default": false } },
    "additionalProperties": false
  },
  "permissions": []
}

package.json:

{
  "name": "misc.virtual-light.lighting",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "dependencies": { "@tangle/module-sdk": "workspace:*" }
}

index.js:

import { runModule } from "@tangle/module-sdk";

runModule({
  version: "0.1.0",
  createDriver(ctx) {
    let on = ctx.config.startOn === true;
    let level = 1;

    return {
      async connect() {
        // A real module opens its connection here.
        ctx.setState("tangle.power", { on });
        ctx.setState("tangle.level", { level });
      },

      async execute(cmd) {
        const key = `${cmd.capability}.${cmd.action}`;
        switch (key) {
          case "tangle.power.on":
          case "tangle.power.off":
          case "tangle.power.toggle":
            on = cmd.action === "toggle" ? !on : cmd.action === "on";
            ctx.setState("tangle.power", { on });
            return;
          case "tangle.level.set":
            level = Math.min(1, Math.max(0, Number(cmd.params?.level)));
            ctx.setState("tangle.level", { level });
            return;
          default:
            throw new Error(`unsupported action: ${key}`);
        }
      },
    };
  },
});

The SDK lives in the Tangle source tree at tangle-contract/sdk/js. The workspace:* dependency works when the module folder is inside the Tangle pnpm workspace, for example in tangle-connect/modules/. Build the SDK once:

pnpm install
pnpm --filter @tangle/module-sdk build

In TypeScript, import the types too: import { runModule, type ActionCommand, type DeviceContext, type DeviceDriver } from "@tangle/module-sdk"; and compile to the entry file named in the manifest (see examples/virtual-light-ts).

A minimal module in Python

The same light in Python, as in tangle-contract/examples/virtual-light-py. Folder misc.virtual-light-py.lighting/, with the same manifest except "id": "misc.virtual-light-py.lighting", "runtime": { "kind": "python", "minVersion": "3.11" } and "entry": "main.py".

main.py:

"""A simulated dimmable light, written in Python."""

from tangle_module_sdk import DeviceDriver, run_module


class VirtualLight(DeviceDriver):
    async def connect(self):
        # A real module opens its connection here.
        self.on = self.ctx.config.get("startOn", False)
        self.level = 1.0
        self.ctx.set_state("tangle.power", {"on": self.on})
        self.ctx.set_state("tangle.level", {"level": self.level})

    async def execute(self, cmd):
        key = f"{cmd['capability']}.{cmd['action']}"
        if key in ("tangle.power.on", "tangle.power.off", "tangle.power.toggle"):
            self.on = (not self.on) if cmd["action"] == "toggle" else cmd["action"] == "on"
            self.ctx.set_state("tangle.power", {"on": self.on})
        elif key == "tangle.level.set":
            self.level = min(1.0, max(0.0, float(cmd.get("params", {}).get("level", 0))))
            self.ctx.set_state("tangle.level", {"level": self.level})
        else:
            raise ValueError(f"unsupported action: {key}")


if __name__ == "__main__":
    run_module(VirtualLight, version="0.1.0")

The Python SDK uses the standard library only and needs Python 3.11 or later. For development, install it from the source tree:

python3 -m venv .venv && source .venv/bin/activate     # Windows: .venv\Scripts\activate
pip install -e tangle-contract/sdk/python

For an installed module, the SDK must be importable when Tangle runs main.py: copy tangle-contract/sdk/python/tangle_module_sdk next to the entry file, which is how the official Python modules are packaged.

Talking to a real device

Open the connection in connect() with a transport from the device context. The SDK watches it: if it closes, the device goes offline and connect() runs again with backoff. Use poll() to read state regularly.

This module drives output 1 of a 4x2 video matrix that answers 3*1! with Out1 In3 All and 1! with the current input. The simulator's example config has such a matrix on TCP port 5023, so you can try it without hardware.

import { runModule } from "@tangle/module-sdk";

runModule({
  version: "0.1.0",
  createDriver(ctx) {
    const output = Number(ctx.config.output ?? 1);
    let conn;

    const readInput = async () => {
      const answer = await conn.request(`${output}!\r\n`, { match: (f) => /^\d+$/.test(f) });
      ctx.setState("tangle.input", { input: answer });
    };

    return {
      async connect() {
        conn = await ctx.tcp({ host: ctx.config.host, port: Number(ctx.config.port ?? 5023), delimiter: "\r\n" });
        await readInput();
      },
      poll: readInput,
      async execute(cmd) {
        if (cmd.capability !== "tangle.input" || cmd.action !== "select") {
          throw new Error(`unsupported action: ${cmd.capability}.${cmd.action}`);
        }
        const input = String(cmd.params?.input ?? "");
        if (!/^[1-4]$/.test(input)) throw new Error("input must be 1 to 4");
        await conn.request(`${input}*${output}!\r\n`, { match: (f) => f.startsWith(`Out${output} `) });
        ctx.setState("tangle.input", { input });
      },
    };
  },
});

The same in Python:

import re
from tangle_module_sdk import DeviceDriver, run_module


class Matrix(DeviceDriver):
    async def connect(self):
        cfg = self.ctx.config
        self.output = int(cfg.get("output", 1))
        self.conn = await self.ctx.tcp(cfg["host"], int(cfg.get("port", 5023)), delimiter="\r\n")
        await self.poll()

    async def poll(self):
        answer = await self.conn.request(f"{self.output}!\r\n", match=lambda f: f.isdigit())
        self.ctx.set_state("tangle.input", {"input": answer})

    async def execute(self, cmd):
        if (cmd["capability"], cmd["action"]) != ("tangle.input", "select"):
            raise ValueError(f"unsupported action: {cmd['capability']}.{cmd['action']}")
        value = str(cmd.get("params", {}).get("input", ""))
        if not re.fullmatch(r"[1-4]", value):
            raise ValueError("input must be 1 to 4")
        await self.conn.request(f"{value}*{self.output}!\r\n", match=lambda f: f.startswith(f"Out{self.output} "))
        self.ctx.set_state("tangle.input", {"input": value})


if __name__ == "__main__":
    run_module(Matrix, version="0.1.0")

Its manifest lists "capabilities": ["tangle.input"], "category": "switcher", "permissions": ["network"], and a configSchema with host (required), port and output.

SDK APIs

Module definition

JavaScript (runModule({...}))Python (run_module(Driver, version, ...))Purpose
versionversionModule version reported to the host; use the manifest's
createDriver(ctx)the DeviceDriver subclassOne driver instance per device
discover({ timeoutMs, log })discover=async fn(timeout_seconds)Return devices found on the network
configurators: { id: fn }configurators={"id": async fn}Build configurator files and scripts
timing: { ... }timing=Timing(...)Change timeouts and delays

Driver methods

MethodWhen it runs
connect()When the device is added, and again after a lost connection. Open the connection and send the initial state. Throwing marks the device offline and retries with backoff
execute(cmd)For each action. cmd has deviceId, capability, action, optional params and at (epoch ms, for timeline pre-roll; the SDK waits until then). Return nothing for success, or throw
poll()Every 10 s while online, never at the same time as an action. Read the device and set what changed
disconnect()When the device is removed or reconnected
pollIntervalMs (JS), poll_interval (Python, seconds)Per-device poll interval

Errors to throw:

  • RetryableError: the action may work later (device busy, warming up). Core may retry it.
  • ConfigError: from connect(), when retrying cannot help (password rejected, setting missing). The device shows error and is not retried.
  • TimeoutError (JS: exported by the SDK; Python: the built-in one): from connect() or poll() when the device did not answer. The device shows timeout instead of offline.
  • Anything else from execute() fails the action, not retryable.

Device context

JavaScript (ctx)Python (self.ctx)Purpose
ctx.device, ctx.configctx.device, ctx.config, ctx.device_idThe device and its settings, secrets filled in
ctx.dataDirctx.data_dirFolder the module may write to
ctx.setState(capability, values)ctx.set_state(capability, values)Report changed state. Send the initial state after connecting, then every change, including changes made outside Tangle
ctx.setStatus(status, detail)ctx.set_status(status, detail)Report a status yourself, for example degraded with an alarm text
ctx.statusctx.statusCurrent status
ctx.emit(name, data)ctx.emit(name, data)Device event (a button press, an alarm) for automations
ctx.log.info(msg, data) (also debug, warn, error)ctx.log("info", msg, **data)Logs, written to stderr
ctx.connectionLost(reason, "offline" or "timeout")ctx.connection_lost(reason, status)For transports you manage yourself: reconnect with backoff
ctx.signalctx.stoppedAborted (JS) or set (Python) when the device is removed

Transports

JavaScriptPythonWhat it is
ctx.tcp({ host, port, delimiter })ctx.tcp(host, port, delimiter=...)TCP with framing (default delimiter \r), one request at a time with a timeout (default 3 s), answer matching with match, unsolicited frames to onFrame / on_frame. A closed connection reconnects the device
udpSearch({ port, message, timeoutMs }), udpSend(...)udp_search(port, message, timeout), udp_send(...)Broadcast a search and collect answers (for discover), or send one datagram
ctx.udp({ host, port })UDP socket for one device, with request and onMessage
ctx.http({ baseUrl })REST client: get, post, put, patch, delete. Timeouts become TimeoutError, 503 and 429 are retryable
ctx.websocket({ url })WebSocket (Node.js 22+). A closed socket reconnects the device
ctx.osc({ host, port })OSC over UDP, with encodeOsc / decodeOsc
JsonRpcClientJSON-RPC over any of the above
ctx.watch(transport)Watch a transport you opened yourself

TCP options also include frameLength (custom framing for binary or length-prefixed protocols such as Modbus TCP), jsonFrameLength (JSON objects sent back to back), encoding ("binary" for Buffers), requestTimeoutMs and sendIntervalMs (minimum time between writes). UDP cannot see a device go away: implement poll() so a silent device becomes timeout. @tangle/module-sdk/testing has a small WebSocket server for writing simulators.

Note: A serial (RS-232) transport is not available yet in the SDKs. Use a serial-to-IP adapter with the TCP helper.

Timing

JavaScript (ms)Python (Timing, seconds)Default
actionTimeoutMsaction_timeout10 s
connectTimeoutMsconnect_timeout10 s
reconnectMinMsreconnect_min1 s, doubling up to the maximum
reconnectMaxMsreconnect_max30 s
pollIntervalMspoll_interval10 s

Discovery

Set "discovery": true in the manifest and implement discover. The host calls it during network scans, in a separate short-lived process, never the project's running one. Search for at most the given time, then return a list of { address, name, manufacturer, model, config } (only address is required). config pre-fills the device settings when the operator adds it.

import { runModule, udpSearch } from "@tangle/module-sdk";

runModule({
  version: "0.1.0",
  createDriver: (ctx) => ({ /* ... */ }),
  async discover({ timeoutMs }) {
    const answers = await udpSearch({ port: 4352, message: "%2SRCH\r", timeoutMs: timeoutMs - 500 });
    return answers.map((a) => ({ address: a.address, config: { host: a.address } }));
  },
});

Configurators

runModule({
  version: "0.1.0",
  createDriver: (ctx) => ({ /* ... */ }),
  configurators: {
    "enable-remoting": ({ config }) => ({
      files: [],
      script: { language: "powershell", content: "Enable-PSRemoting -Force" },
    }),
  },
});

In Python: run_module(Driver, version="0.1.0", configurators={"enable-remoting": build}) where build is async def build(config, device_id) returning a dict with files (each name, content as text or bytes, optional executable), script (language, content) and instructions. Script languages are powershell, bash, shell, brightscript and text.

Statuses

The SDK reports device status for you:

SituationStatus
First connection in progressconnecting
connect() succeededonline
One failed polldegraded
connect() failed, or the connection closedoffline, then retry after 1 s, 2 s, 4 s... up to 30 s
TimeoutError, or two failed polls without an answertimeout, then retry
ConfigErrorerror, no retry

Every module must report at least online, offline and timeout. Actions for an offline or timeout device fail at once with retryable: true. Actions sent while the first connection is being made wait for it, up to the connect timeout. Report degraded yourself with setStatus when the device answers but has a problem (a lamp warning, an alarm).

The module protocol

The SDKs implement the protocol, so you only need this to debug or to write a module in another language. The full reference is tangle-contract/docs/module-protocol.md; message shapes are in tangle-contract/schema/module-protocol.schema.json.

  • The host starts the module with the runtime from the manifest (node <entry>, python <entry>, or the native executable), with the module folder as working directory.
  • stdin carries host-to-module messages, stdout module-to-host messages: JSON-RPC 2.0, one JSON object per line, UTF-8. stderr is for logs. Never print logs to stdout. The SDKs redirect console.log to stderr.
  • When stdin closes, the module must clean up and exit.

Methods (host to module):

MethodPurpose
initializeFirst call, with contractVersion, host, moduleId, dataDir. Answer within 5 s with moduleId, version, sdk
device.addA device to drive, secrets filled in. Answer as soon as it is registered; report progress with device.status
device.removeDisconnect and forget a device
device.executeRun an action. The result is { "ok": true } or { "ok": false, "error": "...", "retryable": true }; failures are results, not JSON-RPC errors. Actions for one device run one at a time, in order
discoverSearch the network for timeoutMs (only with "discovery": true)
configurator.runBuild a configurator's files and script for one device configuration
pingAnswer { "now": <epoch ms> }. 3 missed pings and the host restarts the module
shutdownDisconnect every device, answer, exit with code 0

Notifications (module to host): device.status, device.state (changed values for one capability) and device.event.

{"jsonrpc":"2.0","id":4,"method":"device.execute","params":{"deviceId":"proj-1","capability":"tangle.power","action":"on"}}
{"jsonrpc":"2.0","id":4,"result":{"ok":false,"error":"lamp warming up","retryable":true}}
{"jsonrpc":"2.0","method":"device.state","params":{"deviceId":"proj-1","capability":"tangle.power","values":{"on":true}}}

Error codes: -32700 invalid JSON, -32601 unknown method (also discover in a module without it), -32602 bad params (unknown configurator), -32603 internal error. The host fails an action not answered within 15 s, without restarting the module.

Test your module

Conformance kit

The conformance kit starts your module the way Tangle does and checks it: answers initialize within 5 s, answers ping, rejects unknown methods and invalid JSON, handles actions for unknown devices, answers discover in time (when declared), runs a device scenario, validates every message against the contract, keeps stdout clean, shuts down within 5 s and exits when stdin closes.

pnpm exec tangle-conformance path/to/module           # inside the Tangle workspace
node tangle-contract/conformance/run.mjs path/to/module

For Python modules, set PYTHON to choose the interpreter. Add a conformance.json to the module folder to run a device scenario:

{
  "device": { "id": "light-1", "name": "Test light", "moduleId": "misc.virtual-light.lighting", "config": {} },
  "steps": [
    {
      "execute": { "capability": "tangle.power", "action": "on" },
      "expect": { "ok": true, "state": { "capability": "tangle.power", "values": { "on": true } } }
    },
    { "execute": { "capability": "tangle.power", "action": "explode" }, "expect": { "ok": false } }
  ]
}

The device must come online within 5 s, so point it at something that answers (a simulator).

Against a simulated device

Test against a virtual device before you have hardware. The simulator runs PJLink projectors, any line-based TCP or UDP device described by rules (ascii type), and Art-Net/sACN receivers, with faults to switch on: offline, silent, latencyMs, fail, rejectAuth, drop and reboot. Check that your module reports offline for an offline device, timeout for a silent one, and online again when the fault is cleared. Most official modules also ship their own sim.js or sim.py that you can use as a model.

In a running project

Run tangle-core with an extra modules folder (one sub-folder per module), without installing:

tangle-core run --project show.yaml -m ./my-modules
tangle-core modules -m ./my-modules        # list the modules found

Then test a device with tangle device test or Test connection in the designer.

Package and install

To install a module on a Tangle machine:

tangle module add ./misc.virtual-light.lighting

The folder is copied into the Tangle modules folder (tangle module list shows it as local and unsigned) and becomes the active version. Before installing, make sure:

  • JavaScript: the folder runs on its own. Either keep node_modules with the runtime dependencies (symlinks are followed when copying; development dependencies are skipped), or bundle the entry into one file. The official packages bundle with esbuild.
  • TypeScript: compile first; entry points to the compiled JavaScript.
  • Python: list third-party dependencies in requirements.txt. At install, Tangle creates a virtual environment for the module (.venv) and installs them, from a wheels/ folder when present (so it works offline). Ship the tangle_module_sdk package next to the entry file.
  • Permissions: declare what the module needs. The operator confirms them at install and again when an update adds one.

See Devices for updates, rollback and per-project version locks.

Note: Publishing modules to a marketplace, signed packages and managed Node.js/Python runtimes are not available yet.

Native modules

Timing-critical modules (timecode, MIDI, DMX) are native Rust programs built with the Rust SDK (tangle-native-sdk, in tangle-connect/crates/native-sdk), which behaves like the JS and Python SDKs. Their manifest uses "runtime": { "kind": "native" }, and entry names the executable, looked up in the module folder (with .exe on Windows), then in TANGLE_NATIVE_DIR, then next to the host executable. Modules with the same process name share one process, which receives the devices of all of them; each device.add carries the device's own moduleId.