hermes-agent/skills/creative/touchdesigner-mcp/references/external-data.md
SHL0MS c3e3a9c184 feat(skills): add Tier A references — external-data, panel-ui, replicator, dat-scripting, 3d-scene
Five additional reference docs covering common TD use cases that were not yet
documented in any reference (operators.md lists the ops, but no usage patterns).

- external-data.md: webDAT, webclientDAT, webserverDAT, websocketDAT,
  mqttClientDAT, serialDAT, tcpipDAT — auth, polling, push, JSON parsing
- panel-ui.md: custom parameter pages, button/slider/field/list COMPs,
  containerCOMP layouts, panelExecuteDAT callbacks
- replicator.md: replicatorCOMP for data-driven cloning, per-row overrides,
  recreatemissing pattern, replicator vs Python loop
- dat-scripting.md: full Execute DAT family — chopExecuteDAT, datExecuteDAT,
  parameterExecuteDAT, panelExecuteDAT, opExecuteDAT, executeDAT lifecycle
- 3d-scene.md: light types, three-point rigs, shadows, IBL/cubemaps,
  PBR materials with idiom table, multi-camera, DOF

Same conventions as existing refs: code-first, verify param names with
td_get_par_info, no token-budget impact (load on demand).
2026-04-27 19:35:18 -07:00

9 KiB

External Data Reference

Network and device I/O — HTTP requests, WebSockets, MQTT, Serial, TCP, UDP. For MIDI/OSC specifically see midi-osc.md.

Common production needs:

  • API polling / webhook ingestion
  • Real-time data streams (sensors, market data, chat)
  • IoT device control (Arduino, ESP32, smart lights)
  • Inter-application messaging
  • Hosting a tiny TD-side HTTP server for remote control

Web DAT — HTTP Requests

web = root.create(webDAT, 'api_call')
web.par.url = 'https://api.example.com/v1/status'
web.par.fetchmethod = 'get'           # 'get' | 'post' | 'put' | 'delete'
web.par.format = 'auto'                # 'auto' | 'text' | 'json'
web.par.timeout = 5.0

Triggering a request:

webDAT does NOT auto-fetch on cook. Trigger explicitly:

web.par.fetch.pulse()

Or via expression on a CHOP value-change (chopExecuteDAT — see dat-scripting.md).

Authentication headers:

Use webclientDAT (more flexible) or set webDAT headers via the headers DAT:

web_headers = root.create(tableDAT, 'headers')
web_headers.appendRow(['Authorization', 'Bearer YOUR_TOKEN'])
web_headers.appendRow(['Accept', 'application/json'])
web.par.headers = web_headers.path

Parsing JSON response:

import json

def onTableChange(dat):
    response = dat.text          # raw response body
    data = json.loads(response)
    # Update a tableDAT or store in a constantCHOP for downstream use
    op('/project1/api_status').par.value0 = data['count']
    return

Wire this in a datExecuteDAT watching the webDAT.

Polling pattern:

# timerCHOP fires every N seconds
timer = root.create(timerCHOP, 'poll_timer')
timer.par.length = 5.0
timer.par.cycle = True

# chopExecuteDAT on the timer's 'cycles' channel pulses the webDAT
def offToOn(channel, sampleIndex, val, prev):
    op('/project1/api_call').par.fetch.pulse()
    return

Web Client DAT — More Robust HTTP

webclientDAT is the modern replacement for webDAT — supports streaming responses, chunked transfer, custom auth.

client = root.create(webclientDAT, 'api')
client.par.method = 'POST'
client.par.url = 'https://api.example.com/events'
client.par.uploadtype = 'json'
client.par.uploaddata = '{"event": "scene_change", "scene": 3}'
client.par.request.pulse()

Output goes to its child webclient1_response DAT. Use a datExecuteDAT to react.


Web Server DAT — TD as HTTP Server

Hosts a tiny HTTP server inside TD. Useful for:

  • Status/health endpoints
  • Remote control from a phone or another machine
  • Webhook receivers from external services
server = root.create(webserverDAT, 'control_server')
server.par.port = 8080
server.par.active = True

# Define handler in the docked callback DAT

In the auto-created webserver1_callbacks DAT:

def onHTTPRequest(webServerDAT, request, response):
    path = request['uri']
    if path == '/status':
        response['statusCode'] = 200
        response['data'] = '{"fps": 60, "scene": "active"}'
    elif path == '/scene':
        idx = int(request['args'].get('index', 0))
        op('/project1/scene_switch').par.index = idx
        response['statusCode'] = 200
        response['data'] = 'OK'
    else:
        response['statusCode'] = 404
        response['data'] = 'Not Found'
    return response

Test from terminal: curl http://localhost:8080/status.

Security: No auth by default. Bind to localhost only or add a token check in the callback. Never expose to the public internet without auth.


WebSocket DAT — Bidirectional Real-Time

For low-latency bidirectional streams (chat, live data feeds, controllers).

Client

ws = root.create(websocketDAT, 'ws_client')
ws.par.netaddress = 'wss://api.example.com/socket'
ws.par.active = True

In the docked callbacks DAT:

def onConnect(dat):
    dat.sendText('{"action": "subscribe", "channel": "ticks"}')
    return

def onReceiveText(dat, rowIndex, message):
    # message is a string; parse JSON, dispatch to ops
    import json
    data = json.loads(message)
    op('/project1/price_chop').par.value0 = data['price']
    return

def onDisconnect(dat):
    # Optionally schedule a reconnect
    return

Server

ws = root.create(websocketDAT, 'ws_server')
ws.par.mode = 'server'
ws.par.port = 9001
ws.par.active = True

Same callback structure with an additional clientID arg.


MQTT — Pub/Sub for IoT

mqtt = root.create(mqttClientDAT, 'iot')
mqtt.par.brokeraddress = 'broker.hivemq.com'
mqtt.par.brokerport = 1883
mqtt.par.clientid = 'td_install_01'
mqtt.par.connect.pulse()

# Subscribe in callbacks DAT:
def onConnect(dat):
    dat.subscribe('home/lights/+', qos=1)
    return

def onReceive(dat, topic, payload, qos, retained, dup):
    # payload is bytes — decode if JSON
    msg = payload.decode('utf-8')
    # Dispatch by topic
    return

# Publish from anywhere:
op('iot').publish('show/scene', 'sunset', qos=0, retain=False)

For Mosquitto / HiveMQ self-hosted brokers use the same setup with tcp://192.168.x.x and your local port.


Serial DAT — Arduino, USB Devices

serial = root.create(serialDAT, 'arduino')
serial.par.port = '/dev/cu.usbmodem14101'   # macOS — check Arduino IDE
# Windows: 'COM3', 'COM4', etc.
serial.par.baudrate = 115200
serial.par.active = True

In callbacks:

def onReceive(dat, rowIndex, line):
    # Each newline-terminated line from Arduino arrives here
    parts = line.split(',')
    op('/project1/sensors').par.value0 = float(parts[0])
    op('/project1/sensors').par.value1 = float(parts[1])
    return

Send to Arduino:

op('arduino').send('LED_ON\n')

TCP/IP DAT — Custom Protocols

For talking to non-HTTP servers (game servers, custom protocols, legacy systems).

tcp = root.create(tcpipDAT, 'show_control')
tcp.par.netaddress = '192.168.1.50'
tcp.par.port = 7000
tcp.par.protocol = 'tcp'        # 'tcp' | 'udp'
tcp.par.active = True

Send / receive via callbacks similar to websocketDAT.

For UDP-only (fire-and-forget, no connection), use udpoutDAT + udpinDAT — simpler but unreliable across networks.


Common Patterns

REST API → Visual

timerCHOP (5s loop)
   → chopExecuteDAT (pulse webDAT.par.fetch on cycle)
   → webDAT (returns JSON)
   → datExecuteDAT (parse, write to constantCHOP)
   → CHOP drives glsl uniform → visuals

Webhook receiver

webserverDAT (port 8080, /webhook endpoint)
   → callback writes to a tableDAT log + triggers a scene change

Real-time stock/crypto ticker

websocketDAT (subscribe to feed)
   → onReceiveText callback parses JSON
   → writes to constantCHOP
   → drives bar chart / typography animation

IoT-controlled installation

MQTT → callback dispatches by topic
   → /lights/main → constantCHOP drives lighting render
   → /audio/volume → mathCHOP for master fader

Two-way phone control

WebSocket server in TD
   → simple HTML page on phone connects, sends slider values
   → callback writes to ops
   → TD pushes status back via dat.sendText() to phone UI

Pitfalls

  1. webDAT doesn't auto-fetch — must explicitly pulse par.fetch. Easy to forget.
  2. Blocking on slow APIswebDAT runs on the cook thread. A 30s API call freezes TD for 30s. Use webclientDAT (async) for anything potentially slow.
  3. WebSocket reconnection — TD does NOT auto-reconnect on disconnect. Implement backoff in onDisconnect.
  4. Serial port permissions on macOS — TD needs Full Disk Access OR the port needs to be unlocked via sudo chmod 666 /dev/cu.usbmodem... per session.
  5. MQTT broker connection statemqttClientDAT may show connected=true but messages don't flow if QoS is wrong or topic ACL blocks. Check broker logs.
  6. JSON parse errors crash callbacks silently — wrap parses in try/except and log to textport. Otherwise the callback just stops firing.
  7. Firewall on Windows — first time webserverDAT binds, Windows pops a firewall dialog. Approve it or the server is unreachable.
  8. CORSwebserverDAT doesn't add CORS headers by default. If serving a webapp from a different origin, add Access-Control-Allow-Origin: * in the response.
  9. Polling vs push — polling burns API quota. Always prefer WebSocket / webhook / MQTT for high-frequency data.
  10. Floating-point parsing — sensor data over Serial often comes as strings. float() will crash on '\n' or 'NaN'. Validate before converting.

Quick Recipes

Goal Op chain
Periodic API fetch timerCHOPchopExecuteDAT pulses → webDATdatExecuteDAT parses
Webhook receiver webserverDAT (port + path), callback writes to ops
Real-time stream websocketDAT client → onReceiveText → CHOP/DAT
Arduino sensor → visual serialDAT → callback → constantCHOP → expression on visual op
TD ↔ phone control websocketDAT server + simple HTML page on phone
MQTT IoT integration mqttClientDAT subscribe → callback dispatches by topic