Light Logo Dark Logo

LEADER

OPT-OUT

Brussels 2026

High-Performance OSC over ESP-NOW System

Build v1.3.0 Native USB Enabled CNMAT Compatible

00. Download - Install

ESP32 Hardware Compatibility:

  • 100% Variant Agnostic: Native ESP-NOW architecture enables LEADER to run cleanly on all chips (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6).

Get the latest release directly from GitHub:

Download Last Release

Install via the Arduino IDE Library Manager (Sketch > Include Library > Add .ZIP Library).

01. Architecture

LEADER is a specialized networking library for ESP32. It facilitates a zero-config bridge between Serial SLIP OSC (Pure Data/MaxMSP) and Wireless ESP-NOW. Developed by OPT-OUT for ultra-low-latency artistic environments.

Computer A
Pure Data
USB SLIP
LEADER
ESP-NOW ≈≈≈≈≈≈≈>
Follower BATTERY
-> Sensors
-> LEDs
Follower USB
-> Computer B
(Resolume)

The Leader

Stationary master node. Plugged into the main PC via USB. Translates SLIP OSC into high-speed radio broadcasts. Dictates channel hopping and telemetry.

Follower (Sensor)

Standalone, battery-powered node. Reads physical sensors/pins and beams data directly to the Leader. Uses zero USB CPU overhead.

Follower (Tethered)

Plugged into a secondary PC via USB. Acts as a flawless two-way bridge between computers with zero Wi-Fi router latency.

02. OSCLeader Class

void begin(Stream& port, long baud, uint8_t channel, bool autoHop)

Initializes ESP-NOW and starts the Serial-to-Radio bridge. Usually attached to Serial at 1000000 baud.

bool update()

Processes the Serial SLIP buffer and handles wireless synchronization. Must be called in every loop.

void setIndicator(int pin)

Assigns a hardware LED pin to flash dynamically on successful packet transmission/reception.

void sendNodeRegistry()

Transmits the current registry of active nodes to the Host Computer.

03. OSCFollower Class

void begin(uint8_t channel, bool enableUSB, long baudRate)

Starts as a wireless client. Set enableUSB to true to activate Tethered Bridge mode and set your desired baudRate.

void send(const uint8_t *data, int len)

Broadcasts raw formatted OSC byte arrays out to the wireless network.

void enableHeartbeat(uint32_t ms, uint32_t id)

Starts an automated /sys/pong service. The id is sent as a lightweight integer to keep network traffic minimal.

04. MiniOSC Engine

While user data relies on standard OSC libraries, the LEADER system's internal telemetry is powered by MiniOSC. It is a ruthlessly efficient, zero-dynamic-memory parser running entirely in the background.

Background Telemetry

It handles core network commands like /sys/ping, /leader/hop, and heartbeats. By strictly restricting its payloads to 32-bit integers, it keeps network administrative traffic microscopic (often just 4 to 8 bytes per packet) to prevent airwave congestion.

// Internal Pack Function
MiniOSC::pack(buffer, address, inArray, argCount);

// Internal Extract Function
MiniOSC::extract(data, len, address, outArray, maxArgs);

05. CNMAT Integration

Standard CNMAT OSC libraries cannot natively transmit over ESP-NOW. CNMAT is designed to stream data byte-by-byte (via the Arduino Print class), whereas ESP-NOW demands strictly formatted, pre-assembled memory arrays.

The OSCBuffer Adaptor

We built the OSCBuffer class to act as the universal adaptor. It safely catches the streaming bytes from CNMAT, compiles them into a contiguous radio-ready array, and explicitly pads the tail to mathematically perfect 4-byte multiples (a strict requirement for Pure Data's SLIP decoder).

// 1. Create your standard CNMAT message
OSCMessage msg("/sensor/dial");
msg.add(3.14);

// 2. Pour the stream into the OPT-OUT Buffer
OSCBuffer myBuffer;
msg.send(myBuffer);

// 3. SECURE THE PADDING (Crucial for Pure Data)
myBuffer.end(); 

// 4. Beam the compiled array to the Leader
follower.send(myBuffer.buffer, myBuffer.length);

06. System Commands

Address Args Description
/leader/ping - Returns telemetry: Channel, Uptime, Heap, Sent, Dropped.
/leader/hop - Leader forces network to find cleanest channel and migrate.
/leader/nodes - Requests Leader to transmit the registry of all active connected nodes.
/sys/node int, int Leader reply containing Follower Node ID and milliseconds since last seen.
/sys/ping int Sent from Leader. Sets heartbeat MS for all Followers (0 = OFF).
/sys/pong int Automatic Follower reply containing its unique node ID.

07. Implementation

Example: The Leader Bridge

Plugged into the master computer. Its the DIRECTOR. it sends to every other device and receives from every other device, passing everything to your computer.

#include <LEADER.h>
OSCLeader leader;

#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif

void setup() {
  Serial.begin(230400); // enough is enough. pd[comport] limit seems to be
                        // 230400
  // Initialize the LEADER on Channel 1, autoHop = false
  leader.begin(Serial, 230400, 1, false);
  // Enable the built-in LED, blink for 40ms, using active-LOW (true for
  // XIAO...) blink when LEADER is sending data to FOLLOWERS
  leader.setIndicator(LED_BUILTIN, 40, true);
}

void loop() {
  // The library handles all the data, routing, and LED blinking internally!
  leader.update();

  // CIAO! :O)
}

Example: Tethered Follower

Plugged into a secondary computer. Acts as a flawless two-way SLIP-to-Radio bridge. This device can also have sensors and actuators

#include <LEADER.h>

OSCFollower theothercomputer;

void setup() {
  // Channel 1, USB SLIP = true, Baud = 1000000 is max...
  theothercomputer.begin(1, true, 230400); 
}

void loop() {
  // Silently routes USB to Radio and Radio to USB
  theothercomputer.update();

  // CIAO :O)
}

Example: Standalone Follower

Standalone... not plugged to a computer (or plugged just for power) read sensor and/or play with actuators.

#include <LEADER.h>
#include <OSCMessage.h>

OSCFollower node;
const int LED_PIN = 2; // Standard ESP32 built-in LED

// 1. The function that runs when "/test" is received
void controlLED(OSCMessage &msg) {
  // Check if Pure Data sent a 1 or a 0
  if (msg.isInt(0)) {
    int state = msg.getInt(0);
    digitalWrite(LED_PIN, state > 0 ? HIGH : LOW);
  }
}

// 2. The callback that catches all incoming radio traffic
void onRadioData(const uint8_t *data, int len) {
  OSCMessage msg;
  msg.fill(const_cast<uint8_t *>(data),
           len); // Pour the raw radio array into CNMAT

  if (!msg.hasError()) {
    // If the address is "/test", trigger the controlLED function
    msg.dispatch("/test", controlLED);
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);

  // Channel 1, USB SLIP = false (Battery Mode)
  node.begin(1, false);
  node.enableHeartbeat(1000, 42);

  // Attach the listener
  node.onReceive(onRadioData);
}

void loop() {
  node.update();

  // Send a sensor reading every 50ms
  static unsigned long lastSend = 0;
  if (millis() - lastSend > 50) {
    lastSend = millis();

    OSCMessage msg("/sensor/pot");
    msg.add((int32_t)analogRead(34));

    OSCBuffer buf;
    msg.send(buf);
    buf.end(); // Assemble and pad the array!

    node.send(buf.buffer, buf.length);
  }
}