Docs menu
Guides

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:

FormNeedsStart it with
Standalone executable tangle-sim (Node.js built in). On macOS it is also in the Tangle .dmg next to Tangle.appNothing./tangle-sim run examples/demo.json
Package tangle-simulator-<version>.tar.gz (one script, web UI, examples)Node.js 20 or laternode tangle-sim.mjs run examples/demo.json
Source, in the tangle-simulator folder of the Tangle source treeNode.js 22.6 or later, pnpmsee 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.

TypeWhat it isDefault port
pjlinkPJLink projector: power with warm-up and cool-down, inputs, picture and audio mute, lamp hours, error status, password, class 2 searchTCP 4352
asciiAny line-based device over TCP or UDP, described by rules (regular expression match, set state, reply template)5000
oscOSC device over UDP, or TCP (SLIP or length-prefixed framing): keeps the last arguments at each address and answers a message without arguments with themUDP 8000
moxa.nport.gatewayIP-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 4800TCP/UDP 4001
dmxArt-Net and sACN receiver: shows the 512 levels per universe, the source and frames per second; answers ArtPollUDP 6454 / 5568
videolan.vlc.playerVLC: Lua HTTP interface (password secret by default) with a playlist (Intro, Loop, Credits), and the RC / telnet console (rcPort, rcPassword)TCP 8080
resolume.arena.mediaserverResolume Arena / Avenue: REST webserver and OSC input (oscPort)TCP 8080
obs.obs.switcherOBS Studio over obs-websocket 5 (password); fail = stream reconnectingTCP 4455
disguise.d3.mediaserverdisguise director: HTTP API, OSC transport (oscPort 7400) with feedback to feedbackPort (7401), JSON Machine Control (machinePort 9864); fail = health problemTCP 8180
greenhippo.hippotizer.mediaserverHippotizer V4 REST API component and an OSC input (oscPort)TCP 40512
avstumpfl.pixera.mediaserverPixera JSON/TCP API: timelines Main (60 fps) and Pre-show (25 fps) with cues; the state shows each timeline's timecodeTCP 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).

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.

OptionDefault
passwordnone
inputs["11","12","31","32","51"]
warmSeconds, coolSeconds5
lampSpeed1 (lamp hours per real hour)
class2
search, searchPorttrue, 4352
idleSeconds30
manufacturer, modelTangle, 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.

OptionDefault
transporttcp (or udp)
delimiter"\r\n", added after every answer
inputDelimiterCR, LF or CRLF
greetingsent 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
mdnstangle-sim (service type without _), false to not advertise
idleSecondsnever
manufacturer, modelshown 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.

OptionDefault
artnet, artnetUniversestrue, [0] (Art-Net port-addresses)
sacn, sacnUniversestrue, [1]
artnetPort, sacnPort6454, 5568
manufacturer, modelshown 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 port get their protocol's port, or the next free one: ten pjlink devices 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 every 127.x.y.z works 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 status error with 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.

FaultEffectWhat a Tangle module reports
offlineConnections refused, open ones closed, UDP ignoredoffline
silentAccepts and reads, never answers (a hung device)timeout
latencyMsEvery answer delayed by this many milliseconds, in order (a slow device or network)slow actions, timeout past the limits
failDevice failure. PJLink: lamp error and ERR4, projector switches off; ascii: failReply; dmx: stops updating levelsdepends on the module, often degraded
rejectAuthPassword always rejected. PJLink: PJLINK ERRAerror

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:

OptionEffect
--ui-host <ip>, --ui-port <n>Where the web UI and API listen (default 127.0.0.1:4380)
--no-mdns, --no-ssdpDo not advertise devices
-q, --quietDo 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.

MethodPathWhat it does
GET/api/devicesEvery device
GET/api/devices/{id}One device, with its log
GET/api/devices/{id}/configIts settings as written in the config file
GET/api/typesDevice types and their options
POST/api/devicesAdd a device
PUT/api/devices/{id}Replace its settings (restarts it)
DELETE/api/devices/{id}Remove it
PATCH/api/devices/{id}/stateChange state (JSON body)
PATCH/api/devices/{id}/faultsChange faults (JSON body)
POST/api/devices/{id}/actions/dropCut connections
POST/api/devices/{id}/actions/rebootReboot, body {"seconds": 5}
GET/api/scenariosScenarios and whether they run
POST/api/scenarios/{name}/start, /api/scenarios/{name}/stopStart or stop a scenario
GET/api/eventsServer-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.gateway covers 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 VirtualDevice in the simulator source (src/device.ts, registered in src/types/index.ts).