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 ReleaseInstall 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.
Pure Data
-> LEDs
(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);
}
}