Simulator
Run virtual projectors, media players and servers, OSC and line-based devices, serial gateways and DMX receivers with tangle-sim to build and test a show without hardware.
tangle-simulator runs virtual devices for testing Tangle without hardware. Each virtual device speaks its real protocol on a real network port, so the real Tangle modules (and any other controller) connect to it exactly as they would to the device. Core needs no special mode and does not know it is talking to a simulator.
Use it to program and demo a show before the gear is on site, and to rehearse failures: a projector that goes offline mid-show, a hung device, a wrong password.
Get it
The simulator is a free tool, with no license check. It comes in three forms:
| Form | Needs | Start it with |
|---|---|---|
Standalone executable tangle-sim (Node.js built in). On macOS it is also in the Tangle .dmg next to Tangle.app | Nothing | ./tangle-sim run examples/demo.json |
Package tangle-simulator-<version>.tar.gz (one script, web UI, examples) | Node.js 20 or later | node tangle-sim.mjs run examples/demo.json |
Source, in the tangle-simulator folder of the Tangle source tree | Node.js 22.6 or later, pnpm | see below |
From source:
pnpm install
cd tangle-simulator
node bin/tangle-sim.mjs init # writes tangle-sim.json: example devices and scenarios
node bin/tangle-sim.mjs run # starts them
pnpm dev # runs examples/demo.json directly
On this page, tangle-sim stands for whichever form you use.
When it starts, the simulator prints a table of devices with the address and port each one listens on, then the address of the web UI: http://127.0.0.1:4380.
Note: On macOS, the first run asks whether the program may accept incoming connections. Allow it if tangle-core runs on another machine.
Connect Tangle to it
Add a device in Tangle with the matching module and point it at the simulator: host = 127.0.0.1 (or the simulator machine's IP address), port = the port shown at start. Tutorial: a gallery installation does this in the designer for two projectors and a VLC player. On the command line, with the PJLink module:
tangle device add -p show.yaml pj-1 -m misc.pjlink.projector --set host=127.0.0.1 --set port=4352
tangle device test -p show.yaml pj-1
Virtual devices are also found by Tangle discovery (tangle discover, or Scan the network in the designer): PJLink class 2 search, mDNS advertisements, SSDP answers and Art-Net ArtPoll replies. Each virtual device reports manufacturer Tangle and a stable fake MAC address derived from its id.
Device types
tangle-sim types lists every type and its options.
| Type | What it is | Default port |
|---|---|---|
pjlink | PJLink projector: power with warm-up and cool-down, inputs, picture and audio mute, lamp hours, error status, password, class 2 search | TCP 4352 |
ascii | Any line-based device over TCP or UDP, described by rules (regular expression match, set state, reply template) | 5000 |
osc | OSC device over UDP, or TCP (SLIP or length-prefixed framing): keeps the last arguments at each address and answers a message without arguments with them | UDP 8000 |
moxa.nport.gateway | IP-to-serial gateway (Moxa NPort style) with a line-based device on its serial port (rules, as for ascii). mode: tcp (raw socket), rfc2217 (the client sets the line; a client with the wrong baud rate, parity, data or stop bits gets noise, as on a real line) or udp. Answers the Moxa search on UDP 4800 | TCP/UDP 4001 |
dmx | Art-Net and sACN receiver: shows the 512 levels per universe, the source and frames per second; answers ArtPoll | UDP 6454 / 5568 |
videolan.vlc.player | VLC: Lua HTTP interface (password secret by default) with a playlist (Intro, Loop, Credits), and the RC / telnet console (rcPort, rcPassword) | TCP 8080 |
resolume.arena.mediaserver | Resolume Arena / Avenue: REST webserver and OSC input (oscPort) | TCP 8080 |
obs.obs.switcher | OBS Studio over obs-websocket 5 (password); fail = stream reconnecting | TCP 4455 |
disguise.d3.mediaserver | disguise director: HTTP API, OSC transport (oscPort 7400) with feedback to feedbackPort (7401), JSON Machine Control (machinePort 9864); fail = health problem | TCP 8180 |
greenhippo.hippotizer.mediaserver | Hippotizer V4 REST API component and an OSC input (oscPort) | TCP 40512 |
avstumpfl.pixera.mediaserver | Pixera JSON/TCP API: timelines Main (60 fps) and Pre-show (25 fps) with cues; the state shows each timeline's timecode | TCP 1400 |
The player and media server types run the simulators that ship with their modules, so tangle-sim needs the tangle-connect folder next to it when run from source (the release package includes them). Their state is read-only in the web UI: drive them through their protocol. Their faults are offline, silent, fail (where noted), drop and reboot; latencyMs and rejectAuth have no effect on them. Extra ports left empty are picked at random and shown under ports. examples/video-servers.json starts all five media servers, and examples/serial-gateways.json a projector behind a gateway in each mode (in Tangle, use the generic serial module with tcp://127.0.0.1:4001, rfc2217://127.0.0.1:4002 at 19200 baud, even parity, or udp://127.0.0.1:4003?listen=4103).
pjlink
Implements PJLink class 1 commands (POWR, INPT, INST, AVMT, LAMP, ERST, NAME, INF1, INF2, INFO, CLSS) with the standard answers and errors (ERR3 for inputs while in standby, for example), the MD5 password check, and class 2 search on UDP. Lamp hours increase while the lamp is on. Idle connections close after 30 s, like a real projector.
| Option | Default |
|---|---|
password | none |
inputs | ["11","12","31","32","51"] |
warmSeconds, coolSeconds | 5 |
lampSpeed | 1 (lamp hours per real hour) |
class | 2 |
search, searchPort | true, 4352 |
idleSeconds | 30 |
manufacturer, model | Tangle, PJLink Simulator |
serial, otherInfo |
State keys: power (off, warming, on, cooling), input, videoMute, audioMute, lampHours, errors (6 digits: fan, lamp, temperature, cover, filter, other; 0 ok, 1 warning, 2 error), projectorName.
ascii
Simulates a line-based controller protocol (a matrix switcher, a serial-over-IP device) from rules. The first rule whose match matches a received line wins: set changes state keys (captures $1, $2... are allowed in keys and values), and reply is sent back, with captures and {stateKey} values filled in. Unmatched lines get unknownReply.
| Option | Default |
|---|---|
transport | tcp (or udp) |
delimiter | "\r\n", added after every answer |
inputDelimiter | CR, LF or CRLF |
greeting | sent when a client connects (TCP) |
rules | [{match, flags?, set?, reply?}] |
unknownReply | "ERR" (null for no answer) |
failReply | "ERR FAIL", the answer to everything while the fail fault is on |
push | {stateKey: "template"}, sent to every client when that key changes |
mdns | tangle-sim (service type without _), false to not advertise |
idleSeconds | never |
manufacturer, model | shown in discovery and the UI |
dmx
Receives Art-Net (ArtDmx) and sACN (E1.31, joining the multicast group of each universe) and shows, per universe, the 512 levels, the source address and name, the sequence and the frames per second. It answers ArtPoll with an ArtPollReply naming the device and its universes.
| Option | Default |
|---|---|
artnet, artnetUniverses | true, [0] (Art-Net port-addresses) |
sacn, sacnUniverses | true, [1] |
artnetPort, sacnPort | 6454, 5568 |
manufacturer, model | shown in the UI |
The config file
The simulator reads a JSON file, tangle-sim.json by default. tangle-sim init [file] writes a working example with two PJLink projectors, a video matrix (ASCII over TCP), a DMX receiver and three scenarios.
{
"ui": { "host": "127.0.0.1", "port": 4380 },
"discovery": { "mdns": true, "ssdp": true },
"devices": [
{ "id": "pj-1", "type": "pjlink", "name": "Hall projector", "warmSeconds": 5 },
{ "id": "pj-2", "type": "pjlink", "name": "Stage projector", "password": "secret", "state": { "lampHours": 1790 } },
{
"id": "matrix",
"type": "ascii",
"name": "Video matrix 4x2",
"port": 5023,
"state": { "out1": 1, "out2": 1 },
"rules": [
{ "match": "^([1-4])\\*([12])!$", "set": { "out$2": "$1" }, "reply": "Out$2 In$1 All" },
{ "match": "^([12])!$", "reply": "{out$1}" }
],
"unknownReply": "E10"
},
{ "id": "dimmers", "type": "dmx", "name": "Dimmer rack", "artnetUniverses": [0, 1], "sacnUniverses": [1] }
],
"scenarios": {
"stage-pj-dies": {
"description": "Stage projector turns on, then goes offline 10 s later",
"steps": [
{ "after": 0, "device": "pj-2", "state": { "power": "on" } },
{ "after": 10000, "device": "pj-2", "faults": { "offline": true } },
{ "after": 30000, "device": "pj-2", "faults": { "offline": false } }
]
}
}
}
Every device has an id (letters, digits, ., _, -), a type, and optionally name, address (IPv4, default 0.0.0.0), port, state (initial state), faults, discovery (default true) and the options of its type.
- Ports. Devices without a
portget their protocol's port, or the next free one: tenpjlinkdevices without ports start on 4352 to 4361. - Separate addresses. For discovery to list several devices of one type separately, give each its own
address. On Linux every127.x.y.zworks already. On macOS add an alias first:sudo ifconfig lo0 alias 127.0.0.2, then"address": "127.0.0.2". - Errors. A bad config stops the start with the path of the wrong field, for example
devices[3].port: must be an integer from 1 to 65535. A device whose port is taken by another program shows statuserrorwith the reason; the other devices keep running.
Faults
Every device has faults you can switch on from the web UI, the CLI, the API, the config file or a scenario. They apply at once.
| Fault | Effect | What a Tangle module reports |
|---|---|---|
offline | Connections refused, open ones closed, UDP ignored | offline |
silent | Accepts and reads, never answers (a hung device) | timeout |
latencyMs | Every answer delayed by this many milliseconds, in order (a slow device or network) | slow actions, timeout past the limits |
fail | Device failure. PJLink: lamp error and ERR4, projector switches off; ascii: failReply; dmx: stops updating levels | depends on the module, often degraded |
rejectAuth | Password always rejected. PJLink: PJLINK ERRA | error |
Two actions act once:
drop: cut every open connection; the device stays up.reboot(seconds, default 5): drop connections, stay offline for that time, then come back with the initial state from the config (a projector comes back off).
Scenarios
A scenario is a named list of timed steps. Each step has after (milliseconds from the scenario start), a device, and one or more of state, faults or action (drop or reboot, with seconds for reboot). Optional scenario fields: description, autoStart: true (start when the simulator starts) and loop: true (start again after the last step).
"slow-network": {
"description": "Every device answers 800 ms late for 30 s",
"steps": [
{ "after": 0, "device": "pj-1", "faults": { "latencyMs": 800 } },
{ "after": 30000, "device": "pj-1", "faults": { "latencyMs": 0 } }
]
}
Start and stop scenarios from the web UI, with tangle-sim scenario <name> and tangle-sim scenario <name> stop, or through the API. Step timing is within about 10 ms: good for testing, not a show clock.
Web UI
Open http://127.0.0.1:4380. The page shows one card per device with its type, id, address and port, status, live state, faults, actions and protocol log (the last 200 lines). For dmx devices the card shows the channel levels. Changes appear without reloading, so you can watch a Tangle dashboard turn a projector on and see %1POWR 1 arrive in its log.
From a card you can:
- edit the device's state;
- toggle Offline, Silent, Fail and Reject auth, and set Latency in ms;
- Drop connections or Reboot 5 s;
- Edit or Remove the device.
The Scenarios section lists the scenarios with buttons to start and stop them. A Skin menu switches between the Console and Lobby looks; the page follows the OS light or dark mode.
Add and edit devices
Click Add device, pick a Type, then fill in ID, Name, Address, Port (empty for automatic), Discoverable, the type's Settings (with help text and example rules) and an optional Initial state as JSON. Edit opens the same form with the device's settings as written in the config file; saving restarts the device with its id and log kept.
Every add, edit or remove is saved to the config file, keeping its other settings. Automatic ports are not written, so a restart gives the same devices. Without a config file, tangle-sim run starts with no devices and creates tangle-sim.json on the first change.
The form checks values like the config file does: an id already in use shows id: "pj-1" is already used and the form stays open. A device used by a scenario cannot be removed; the message names the scenario.
Command line
tangle-sim run [config.json] # default tangle-sim.json
tangle-sim init [file] # write the example config
tangle-sim types # device types and their options
run options:
| Option | Effect |
|---|---|
--ui-host <ip>, --ui-port <n> | Where the web UI and API listen (default 127.0.0.1:4380) |
--no-mdns, --no-ssdp | Do not advertise devices |
-q, --quiet | Do not print protocol lines |
Control a running simulator (these call its API; --url or TANGLE_SIM_URL changes the address, default http://127.0.0.1:4380):
tangle-sim status # devices, ports, status and faults
tangle-sim show pj-1 # full state and recent log
tangle-sim set pj-1 lampHours=1990 errors=010000 # change state
tangle-sim fault pj-1 offline # offline=off to clear; silent, fail, rejectAuth, latency=500
tangle-sim clear pj-1 # remove every fault
tangle-sim drop pj-1
tangle-sim reboot pj-2 10
tangle-sim add pjlink pj-3 name="Lobby PJ" password=secret
tangle-sim remove pj-3
tangle-sim scenario # list scenarios
tangle-sim scenario stage-pj-dies # start one
HTTP API
The web UI and the CLI use a local HTTP API, handy for test scripts.
| Method | Path | What it does |
|---|---|---|
| GET | /api/devices | Every device |
| GET | /api/devices/{id} | One device, with its log |
| GET | /api/devices/{id}/config | Its settings as written in the config file |
| GET | /api/types | Device types and their options |
| POST | /api/devices | Add a device |
| PUT | /api/devices/{id} | Replace its settings (restarts it) |
| DELETE | /api/devices/{id} | Remove it |
| PATCH | /api/devices/{id}/state | Change state (JSON body) |
| PATCH | /api/devices/{id}/faults | Change faults (JSON body) |
| POST | /api/devices/{id}/actions/drop | Cut connections |
| POST | /api/devices/{id}/actions/reboot | Reboot, body {"seconds": 5} |
| GET | /api/scenarios | Scenarios and whether they run |
| POST | /api/scenarios/{name}/start, /api/scenarios/{name}/stop | Start or stop a scenario |
| GET | /api/events | Server-Sent Events: device, log, scenarios |
curl -X PATCH http://127.0.0.1:4380/api/devices/pj-1/faults -H 'content-type: application/json' -d '{"offline": true}'
Warning: The API has no authentication. It listens on 127.0.0.1 unless you pass
--ui-host; only do that on a trusted network.
Limits
- Local serial (RS-232) ports are not simulated;
moxa.nport.gatewaycovers devices behind IP-to-serial gateways. - Timecode generators and video or audio streams are not simulated. Media servers report their timelines' timecode, which Tangle timelines can chase.
- New device types are classes extending
VirtualDevicein the simulator source (src/device.ts, registered insrc/types/index.ts).