Hardware integration

OSC over WiFi / Ethernet — wire ESP32 or Arduino devices into Grafer's Live mode to drive the conductor from buttons and sensors, or to control lights, motors, and robotics from cell graphs.

How OSC flows in Grafer

              ┌─────────────────────────┐
              │       Grafer app        │
   sensors   ─┼──► OSC In  (UDP listen) │  Phase 3: triggers cell
   pedals     │      port: 7400         │  transitions / pause / etc.
   ESP32     ─┼──►                      │  Phase 5: fires reactive
              │                         │           osc.in → out.chord
              │                         │
              │   score.osc-send  ──────┼──► robotics / synths
              │   (per-cell node)       │    Phase 1
              └─────────────────────────┘

Two independent flows. Sending OSC into Grafer is the Live page's OSC input feature. Receiving OSC from Grafer is the score.osc-send graph node — drop it inside any cell and wire a Voice into it.

You don't need both — pick whichever side(s) your project needs.

Library

For Arduino-flavoured platforms (ESP32, ESP8266, Uno + Ethernet shield, Teensy, etc.), use the CNMAT OSC library — the de-facto standard, maintained, supports both sending and receiving:

All four sketches below use this library. If you're on a non-Arduino platform (Raspberry Pi, MaxMSP, SuperCollider, Pd, TouchOSC), use whatever native OSC support that platform has — Grafer's side is plain OSC over UDP, no proprietary extras.

Network setup

1. Same network

Hardware and the Grafer host (your computer or VPS) must reach each other over IP. The simplest setup is both on the same WiFi router, or both on the same wired LAN. If your router has client isolation enabled (common on guest networks), disable it for the network you're using.

2. Find Grafer's IP

On the machine running Grafer:

OSCommand
macOSipconfig getifaddr en0 (or en1)
Linuxhostname -I
Windowsipconfig (look for IPv4 Address)

Or on the Live → OSC input panel, the address you'll see (<host>:7400) shows the host as the browser's hostname. If that's localhost you're testing on the same machine; otherwise it's the LAN/VPS hostname your hardware should target.

3. Find the OSC In port

Open Grafer's Live page → expand the OSC input panel → click Listen. The panel shows the allocated port (default range 7400-7463). Each user gets their own port — share that exact number with your hardware. The port persists for ~5 minutes after you close the panel, so a quick page reload doesn't re-allocate.

4. Pick a port for hardware to listen on

For Grafer → hardware (the score.osc-send direction), the hardware chooses. Anything in the user-port range works; common choices are 9000, 9001, etc. Configure that port both in your sketch (Udp.begin(9000)) and in the cell's score.osc-send node (port: 9000, host: <ESP32's IP>).

5. Firewall

Most home routers don't block UDP between LAN clients. Local OS firewalls (Windows Defender, macOS Application Firewall, ufw) might — allow UDP on the chosen port if traffic isn't getting through.

Hardware → Grafer: triggering the conductor

Add OSC mappings in Live → OSC input → Score-level mappings:

AddressAction
/grafer/nextnext
/grafer/skipskip cell
/grafer/pausepause
/grafer/resumeresume
/grafer/panicstop

Or set per-edge triggerAddress on a rhizome edge — that edge wins the next boundary when the matching packet arrives. Or wire osc.inout.chord inside a cell graph — the chord layers in mid-cell when the matching packet arrives.

On the hardware, just send a UDP packet:

OSCMessage msg("/grafer/next");
Udp.beginPacket(graferHost, graferPort);
msg.send(Udp);
Udp.endPacket();
msg.empty();

Grafer → Hardware: driving lights / motors / robotics

Inside any cell graph, drop a score.osc-send node:

Wire a Voice into the node and Grafer fires one OSC message per note:

<address> <onset_seconds> <duration_seconds> <pitch_midicents> <velocity_0..1>

Your sketch handles each address however you want — flash an LED, spin a motor, trigger a sample.

Example sketches

Each sketch is paste-credentials-and-run. Right-click each link to download, or open it to read the source in your browser.

esp32_send_osc.ino

Single button: short press fires /grafer/next, long press fires /grafer/skip. Debounced, reconnect-tolerant.

esp32_receive_osc.ino

LED handlers for /lights/flash (with note duration), /lights/level (PWM brightness), /motor/spin.

esp32_bidirectional.ino

Three buttons (next / skip / pause-toggle) + analog pot at 50 Hz with dead-band + the same three output handlers. The realistic foot-controller skeleton.

arduino_uno_ethernet.ino

Wired version with DHCP + lease maintenance for long-running sessions. For when WiFi isn't reliable enough.

Latency

Round-trip latency on a typical home network sits around 5-15 ms for a single packet — well under the threshold a performer would notice as "delayed". Two things to watch:

Troubleshooting

Nothing happens when I press the button.

  1. Check the serial monitor — is the ESP32 actually firing? It should log Sent /grafer/next on every press.
  2. Wrong IP. Verify the Grafer host's IP again — did your laptop's IP change? Re-check with ipconfig / ifconfig.
  3. Wrong port. Re-check the OSC input panel — did the listener restart and pick a different port? It's stable while open but reallocates after a reload past the 5-minute grace.
  4. Firewall blocking UDP on that port. Test with oscsend from the Grafer host's terminal first:
    # macOS / Linux (from `liblo` or `oscsend` package)
    oscsend localhost 7400 /grafer/next
    
    # Windows (PowerShell — install: pip install python-osc)
    python -c "from pythonosc.udp_client import SimpleUDPClient; SimpleUDPClient('127.0.0.1', 7400).send_message('/grafer/next', [])"
    If oscsend works but the ESP32 doesn't, it's a network/firewall issue, not Grafer.

Recent packets monitor shows my packets but the conductor isn't reacting.

Either the address doesn't match a mapping/edge, or the conductor isn't running. Check:

  1. Is the address spelled exactly the same on both sides? /Grafer/next/grafer/next (case-sensitive).
  2. Is the conductor actually playing (Stop button visible, not Play)?
  3. Score-level mappings fire at cell boundaries, not mid-cell — there's a delay until the current cell finishes. Use a per-edge triggerAddress or an in-cell osc.in for instant response.

Receiver sketch sees no packets from Grafer.

  1. Wrong host IP in the score.osc-send node — has your ESP32's IP changed? Print it to serial on every boot.
  2. The cell isn't actually playing (the node only fires when the cell evaluates as part of the playing voice).
  3. Check the node's "Last send" status field — Grafer logs the most recent send result there.

OSC input panel says "Stream not reaching tab" / mobile says "composer's tab not subscribed".

The composer's browser can't keep an open Server-Sent Events connection to the API server. Almost always a reverse-proxy / hosting-layer issue. Common fixes per platform:

PlatformFix
IIS + ARR In web.config on the <proxy> element: responseBufferLimit="0" + timeout="01:00:00". ARR buffers responses by default, which deadlocks SSE.
nginx In the /api/ location: proxy_buffering off; proxy_cache off; proxy_read_timeout 1h;
Cloudflare Enable WebSockets on the zone (Network → WebSockets). Cloudflare's SSE pass-through rides the same flag.
Apache (mod_proxy) Add SetEnv proxy-sendchunked and ProxyTimeout 3600 for the API location.

You can verify the SSE endpoint directly with curl: curl -N https://your-host/api/osc/in/stream?token=YOUR_JWT. A working stream prints retry: 3000 immediately and a hello event within ~100ms. If curl hangs with no output for >1s, the proxy is buffering.

Going further

Build something cool? The example sketches are deliberately minimal so you've got space to grow them into your own hardware setup.