skills/creative/touchdesigner-mcp/references/external-data.md
Network and device I/O — HTTP requests, WebSockets, MQTT, Serial, TCP, UDP. For MIDI/OSC specifically see midi-osc.md.
Common production needs:
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
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.
Hosts a tiny HTTP server inside TD. Useful for:
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.
For low-latency bidirectional streams (chat, live data feeds, controllers).
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
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 = 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 = 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')
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.
timerCHOP (5s loop)
→ chopExecuteDAT (pulse webDAT.par.fetch on cycle)
→ webDAT (returns JSON)
→ datExecuteDAT (parse, write to constantCHOP)
→ CHOP drives glsl uniform → visuals
webserverDAT (port 8080, /webhook endpoint)
→ callback writes to a tableDAT log + triggers a scene change
websocketDAT (subscribe to feed)
→ onReceiveText callback parses JSON
→ writes to constantCHOP
→ drives bar chart / typography animation
MQTT → callback dispatches by topic
→ /lights/main → constantCHOP drives lighting render
→ /audio/volume → mathCHOP for master fader
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
webDAT doesn't auto-fetch — must explicitly pulse par.fetch. Easy to forget.webDAT runs on the cook thread. A 30s API call freezes TD for 30s. Use webclientDAT (async) for anything potentially slow.onDisconnect.sudo chmod 666 /dev/cu.usbmodem... per session.mqttClientDAT may show connected=true but messages don't flow if QoS is wrong or topic ACL blocks. Check broker logs.webserverDAT binds, Windows pops a firewall dialog. Approve it or the server is unreachable.webserverDAT doesn't add CORS headers by default. If serving a webapp from a different origin, add Access-Control-Allow-Origin: * in the response.float() will crash on '\n' or 'NaN'. Validate before converting.| Goal | Op chain |
|---|---|
| Periodic API fetch | timerCHOP → chopExecuteDAT pulses → webDAT → datExecuteDAT 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 |