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. When autoHop is true, the Leader will periodically scan for the quietest Wi-Fi channel and migrate the entire network automatically (every 30 seconds).

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);
  }
}