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