Advanced OOCSI: Designing Multi-Device Systems
So, you have worked through the OOCSI basics and intermediate guides. You know how to send messages between devices, share variables, and filter data on channels.
Now comes the exciting part of interactive design: designing systems of connected products.
When you prototype an interactive room, an escape game, an exhibition pavilion, or a collection of wearable devices, you are no longer connecting just one sensor to one computer. You are orchestrating a group of independent physical devices into a single, cohesive user experience.
The 5 System Design Patterns
flowchart TD
subgraph S1["1. Distributed State Machine"]
COORD["State Coordinator"] -->|room_state: 'SUCCESS'| B1["OOCSI Broker"] --> ACT1["Smart Lights, Audio & Lock"]
end
subgraph S2["2. Shared Heartbeats"]
CONDUCTOR["Master Conductor"] -->|beat: 60 BPM| B2["OOCSI Broker"] --> ACT2["Pulsing Lamps & Wearables"]
end
subgraph S3["3. Dynamic Role Allocation"]
ROSTER["Table Coordinator"] -->|assign: 'Drums'| B3["OOCSI Broker"] --> ACT3["Modular Smart Blocks"]
end
subgraph S4["4. Collective Sensing"]
SENSORS["Table Sensors (1..3)"] -->|raw readings| AGG["Room Aggregator"] --> CHANDELIER["Ambient Chandelier"]
end
subgraph S5["5. Group Consensus & Voting"]
VOTERS["Voter Stations (1..3)"] -->|submit ballots| TALLY["Poll Coordinator"] --> ALL["Synchronized Room Devices"]
end
In the sections below, we explore five foundational patterns that help you tackle common multi-device challenges:
- Distributed State Machine: Keeping entire rooms and spaces synchronized across different interactive phases.
- Shared Heartbeats & Synchronization: Keeping lamps, wearables, and sounds pulsing in unison without drifting apart.
- Dynamic Role Allocation: Letting identical modular objects automatically discover each other and assign roles.
- Collective Sensing & Data Aggregation: Combining data from multiple dispersed sensors into a single ambient summary.
- Group Consensus & Voting: Allowing autonomous devices to propose, vote, and agree on a collective choice.
1. Distributed State Machine: Orchestrating Room Phases
The Design Scenario
Imagine you are building an interactive escape room, a museum exhibition, or an interactive smart meeting space. The experience moves through clear story phases:
STANDBY: Ambient blue light, gentle background music, sensors waiting for visitors.EXPLORATION: Main room lights turn on, puzzle props become active, visitors interact with buttons and sensors.SUCCESS: The puzzle is solved! Room lights pulse green, a celebratory chime sounds, and an electric magnetic lock opens a secret compartment.RESET: The installation smoothly returns to standby for the next team.
When the room changes phase, you do not want to send individual instructions to dozens of lamps, speakers, and motors one by one. You want one global announcement that makes every device in the space adapt immediately.
stateDiagram-v2
[*] --> STANDBY: System Powered On
STANDBY --> EXPLORATION: Visitor triggers entry sensor
EXPLORATION --> SUCCESS: Puzzle sensors solved
SUCCESS --> RESET: Timeout (30s) or facilitator click
RESET --> STANDBY: Ready for next visitors
The Design Pattern
A central device—such as a laptop, a Raspberry Pi, or a control tablet running Python or JavaScript—acts as the State Coordinator. It listens for puzzle completion events, decides when a state change happens, and broadcasts the new state name on a dedicated channel (e.g. room/state).
sequenceDiagram
autonumber
actor Player as Visitors
participant Puzzle as Puzzle Sensor (ESP32)
participant Coord as State Coordinator (Python)
participant Broker as OOCSI Broker
participant Actuators as Room Actuators (Lights, Audio, Lock)
Player->>Puzzle: Solve final puzzle step
Puzzle->>Broker: Send {"puzzle_solved": true} to 'room/events'
Broker->>Coord: Forward event
Note over Coord: Transition:<br/>EXPLORATION -> SUCCESS
Coord->>Broker: Broadcast {"state": "SUCCESS", "_RETAIN": 3600} to 'room/state'
par Coordinated Phase Transition
Broker->>Actuators: Deliver to Lights -> Pulse green glow
and
Broker->>Actuators: Deliver to Audio -> Play victory fanfare
and
Broker->>Actuators: Deliver to Lock -> Release door magnet
end
The Prototyping Secret: Retained Messages (_RETAIN)
What happens if one of your ESP32 lamps loses power or reboots halfway through the game? If you only send the state transition once, the rebooted lamp will wake up not knowing what phase the room is in!
By adding " _RETAIN": 3600 (retained for 1 hour) to your state message, the OOCSI server remembers the current state. The instant any rebooted device reconnects and subscribes to room/state, the server immediately hands it the latest state message. Your devices never lose track of reality.
Code Examples
State Coordinator (Python)
This script manages the room phases and broadcasts retained updates:
from oocsi import OOCSI
import time
# Connect to OOCSI with a unique handle
oocsi = OOCSI('room_coordinator_####', 'localhost')
# Available room states
STATES = ['STANDBY', 'EXPLORATION', 'SUCCESS', 'RESET']
current_state = 'STANDBY'
def broadcast_state(new_state):
global current_state
current_state = new_state
print(f"[*] Transitioning room state to: {current_state}")
# Broadcast to 'room/state' with _RETAIN so rebooted nodes catch up immediately
oocsi.send('room/state', {
'state': current_state,
'_RETAIN': 3600 # Remember for 1 hour on server
})
def handle_room_events(sender, recipient, data):
# React to sensors in the room
if data.get('action') == 'player_entered' and current_state == 'STANDBY':
broadcast_state('EXPLORATION')
elif data.get('puzzle_solved') == True and current_state == 'EXPLORATION':
broadcast_state('SUCCESS')
elif data.get('action') == 'force_reset':
broadcast_state('STANDBY')
# Subscribe to incoming sensor triggers
oocsi.subscribe('room/events', handle_room_events)
# Publish initial state on startup
broadcast_state('STANDBY')
# Keep running
while True:
time.sleep(1)
Interactive Prop Node (ESP32 / Arduino C++)
This microcontroller controls an ambient indicator LED and an electric lock servo, listening for room state updates:
#include "OOCSI.h"
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* hostserver = "oocsi.example.com";
const char* OOCSIName = "door_prop_####";
OOCSI oocsi = OOCSI();
// Pin definitions
const int LED_PIN = 2;
// Callback triggered whenever room state changes (or immediately on reconnect via _RETAIN!)
void handleStateChange() {
String state = oocsi.getString("state", "STANDBY");
Serial.print("Prop received new room state: ");
Serial.println(state);
if (state == "STANDBY") {
// Dim light, lock mechanism
digitalWrite(LED_PIN, LOW);
} else if (state == "EXPLORATION") {
// Normal ambient light, armed puzzle
digitalWrite(LED_PIN, HIGH);
} else if (state == "SUCCESS") {
// Unlock door and flash light
for (int i = 0; i < 5; i++) {
digitalWrite(LED_PIN, HIGH); delay(100);
digitalWrite(LED_PIN, LOW); delay(100);
}
digitalWrite(LED_PIN, HIGH);
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// Connect to WiFi and OOCSI
oocsi.connect(OOCSIName, hostserver, ssid, password, handleStateChange);
// Subscribe to the shared state channel
oocsi.subscribe("room/state");
}
void loop() {
// Service OOCSI network events
oocsi.check();
delay(20);
}
2. Shared Heartbeats & Synchronization: Marching to the Same Beat
The Design Scenario
Imagine you are designing five ambient lamps that gently pulse together like a calm collective heartbeat, or a set of wearable wristbands that buzz in unison during a group meditation workshop.
If you program each microcontroller with a simple loop like delay(1000); blink();, you will encounter a classic physical computing problem: clock drift. The tiny crystal timing oscillators on microcontrollers are never 100% identical. Even a difference of 0.05% means that after just three minutes, the lights will visibly fall out of step and flash haphazardly.
sequenceDiagram
autonumber
participant Conductor as Master Conductor (Python)
participant Broker as OOCSI Broker ('tempo/pulse')
participant Lamp as Ambient Lamps (ESP32)
participant Wearable as Haptic Wearables (ESP32)
Note over Conductor: 60 BPM = 1 beat every 1000ms
loop Periodic Heartbeat
Conductor->>Broker: Broadcast {"beat": 1, "bpm": 60}
par Synchronized Realignment
Broker->>Lamp: Pulse arrived -> Snap animation phase to 0
and
Broker->>Wearable: Pulse arrived -> Trigger haptic tap in rhythm
end
end
The Design Pattern (The Conductor)
Instead of letting each device count time independently, designate one device as the Conductor (or use a server-generated timer).
- The Conductor broadcasts a short pulse message at regular musical intervals (e.g. 60 BPM = 1 pulse per second) to
tempo/pulse. - Each follower device runs its own smooth local animation loop (e.g. fading an LED brightness smoothly using a sine wave).
- The trick: Every time an OOCSI beat message arrives, the follower snaps its animation counter back to 0. If a device was running slightly too fast or slow, it gently realigns on every beat. All devices stay marching to the exact same rhythm indefinitely!
Code Examples
Master Conductor (Python)
Broadcasts steady, rhythmic heartbeat pulses at a configurable tempo (BPM):
from oocsi import OOCSI
import time
oocsi = OOCSI('conductor_clock_####', 'localhost')
BPM = 60 # 60 beats per minute
INTERVAL = 60.0 / BPM # 1.0 second per beat
beat_counter = 0
print(f"[*] Conductor running at {BPM} BPM (1 beat every {INTERVAL:.2f}s)")
while True:
beat_counter += 1
# Broadcast beat pulse
oocsi.send('tempo/pulse', {
'beat': beat_counter,
'bpm': BPM,
'timestamp': int(time.time() * 1000)
})
print(f" -> Beat {beat_counter} sent")
time.sleep(INTERVAL)
Synchronized Follower Lamp (ESP32 / Arduino C++)
Smoothly animates its light and resets its animation phase whenever a pulse arrives:
#include "OOCSI.h"
#include <math.h>
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* hostserver = "oocsi.example.com";
const char* OOCSIName = "sync_lamp_####";
OOCSI oocsi = OOCSI();
const int LED_PIN = 2;
unsigned long lastBeatTime = 0;
float beatPeriodMs = 1000.0; // Default 1000ms (60 BPM)
// Callback triggered whenever the Conductor sends a beat
void onBeatPulse() {
int beat = oocsi.getInt("beat", 0);
int bpm = oocsi.getInt("bpm", 60);
beatPeriodMs = (60.0 / bpm) * 1000.0;
// The secret: reset local timing phase to match the conductor!
lastBeatTime = millis();
Serial.print("Beat in step: ");
Serial.println(beat);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
oocsi.connect(OOCSIName, hostserver, ssid, password, onBeatPulse);
oocsi.subscribe("tempo/pulse");
}
void loop() {
oocsi.check();
// Calculate where we are in the current breath cycle (0.0 to 1.0)
unsigned long elapsed = millis() - lastBeatTime;
if (elapsed > beatPeriodMs) elapsed = beatPeriodMs;
float progress = (float)elapsed / beatPeriodMs;
// Smooth sinusoidal breathing curve: 0 -> 1 -> 0
float brightness = (sin(progress * 2.0 * PI - (PI / 2.0)) + 1.0) / 2.0;
// Drive LED (PWM / analog output)
analogWrite(LED_PIN, (int)(brightness * 255));
delay(15);
}
3. Dynamic Role Allocation: Interchangeable Smart Objects
The Design Scenario
Suppose you build four identical smart wooden blocks with an RGB LED and an accelerometer for a tangible music sequencer table.
- Rather than permanently flashing “Block #1 is Drums, Block #2 is Bass, Block #3 is Melody” into their chips, you want the blocks to be completely interchangeable.
- If a visitor picks up any block and places it on the table, it automatically gets assigned the next available role.
- If a block runs out of battery or is removed from the table, its role is freed up for another block.
sequenceDiagram
autonumber
actor User as Designer / User
participant Block as New Smart Block (ESP32)
participant Broker as OOCSI Broker ('blocks/lobby')
participant Manager as Table Manager (Python)
User->>Block: Powers on block
Block->>Broker: Connects as 'block_7842' & subscribes to 'blocks/lobby'
Broker->>Manager: Channel presence event (JOIN: 'block_7842')
Note over Manager: Check available roles:<br/>['DRUMS', 'BASS', 'LEAD']<br/>Assign next: 'DRUMS'
Manager->>Broker: Send direct message to 'block_7842':<br/>{"role": "DRUMS", "color": [255, 0, 0]}
Broker->>Block: Deliver private role message
Note over Block: Adopt 'DRUMS' role!<br/>Turn LED Red & activate drum samples
Block->>Broker: Send to 'blocks/lobby':<br/>{"status": "READY", "role": "DRUMS"}
The Design Pattern
- Every device connects with random digits in its handle (e.g.
block_####), preventing connection name collisions. - All devices subscribe to a shared meeting channel (e.g.
blocks/lobby). - A Table Manager listens for channel presence events (
joinandleave). - When a new node joins, the manager picks an available role and sends a private, direct OOCSI message specifically to that client handle (
oocsi.message(client_handle, {"role": "..."})). - When a node leaves or disconnects, the manager returns that role to the available pool.
Code Examples
Table Manager (Python)
Tracks available roles and assigns them dynamically to incoming smart blocks:
from oocsi import OOCSI
import time
oocsi = OOCSI('table_manager_####', 'localhost')
# Available roles to distribute
AVAILABLE_ROLES = {
'DRUMS': {'color': 'red', 'assigned_to': None},
'BASS': {'color': 'blue', 'assigned_to': None},
'LEAD': {'color': 'green', 'assigned_to': None},
'CHORDS': {'color': 'amber', 'assigned_to': None}
}
def assign_role_to_client(client_handle):
# Find the first unassigned role
for role_name, info in AVAILABLE_ROLES.items():
if info['assigned_to'] is None:
info['assigned_to'] = client_handle
print(f"[*] Assigning role '{role_name}' to client: {client_handle}")
# Send private direct message to the specific device
oocsi.send(client_handle, {
'action': 'assign_role',
'role': role_name,
'color': info['color']
})
return
print(f"[!] No roles left for client: {client_handle}")
def handle_lobby_events(sender, recipient, data):
# Handle presence join events or manual registration greetings
if data.get('action') == 'request_role':
assign_role_to_client(sender)
oocsi.subscribe('blocks/lobby', handle_lobby_events)
print("[*] Table Manager listening for new smart blocks on 'blocks/lobby'...")
while True:
time.sleep(1)
Modular Smart Block (ESP32 / Arduino C++)
Powers on without a fixed identity, requests a role, and adapts its personality:
#include "OOCSI.h"
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* hostserver = "oocsi.example.com";
// Use #### so the server assigns random digits: e.g. "block_4821"
const char* OOCSIName = "block_####";
OOCSI oocsi = OOCSI();
String myRole = "UNASSIGNED";
// Callback for direct messages sent privately to THIS block
void onDirectMessage() {
if (oocsi.has("role")) {
myRole = oocsi.getString("role", "UNASSIGNED");
String color = oocsi.getString("color", "white");
Serial.print("[*] Role assigned! I am now: ");
Serial.println(myRole);
Serial.print(" LED color configured to: ");
Serial.println(color);
}
}
void setup() {
Serial.begin(115200);
// Connect and set onDirectMessage as the personal message handler
oocsi.connect(OOCSIName, hostserver, ssid, password, onDirectMessage);
// Join the lobby channel and request a role from the manager
oocsi.subscribe("blocks/lobby");
oocsi.newMessage("blocks/lobby");
oocsi.addString("action", "request_role");
oocsi.sendMessage();
}
void loop() {
oocsi.check();
// If we have an assigned role, perform role-specific interactions
if (myRole == "DRUMS") {
// Read accelerometer tap -> send drum beat
} else if (myRole == "BASS") {
// Read tilt angle -> send bass note
}
delay(50);
}
4. Collective Sensing & Data Aggregation: Room-Wide Awareness
The Design Scenario
Consider an interactive office, study lounge, or cafe where you place small sensor pucks on each table measuring noise and ambient light. Overhead in the center of the ceiling hangs a large dynamic chandelier.
- When the whole space is quiet and dimly lit, the chandelier emits a gentle warm amber glow.
- When multiple tables become noisy and active, the chandelier brightens and shifts to an invigorating crisp white.
flowchart TD
subgraph Sensors["1. Distributed Table Sensors (Data Producers)"]
T1["Table Puck 1 (ESP32)"]
T2["Table Puck 2 (ESP32)"]
T3["Table Puck 3 (ESP32)"]
end
subgraph Intake["2. Raw Intake Channel"]
RAW["Channel: 'space/tables/raw'<br/>Frequent individual sensor bursts"]
end
subgraph Pipeline["3. Data Aggregator (Python / OOCSI Filter)"]
AVG["Rolling 10s Window Buffer<br/>Calculates mean noise & light across room"]
end
subgraph Summary["4. Clean Summary Channel"]
SUMM["Channel: 'space/room/summary'<br/>One calm, smoothed room metric every 5s"]
end
subgraph Actuators["5. Ambient Output (Consumers)"]
CHAND["Ambient Chandelier (LEDs)"]
DASH["Facilitator Web Dashboard"]
end
T1 & T2 & T3 -->|Post raw readings every 3s| RAW
RAW --> AVG
AVG -->|Publish smoothed average| SUMM
SUMM --> CHAND & DASH
The Design Pattern
If every table sensor sent messages directly to the chandelier, the chandelier would suffer from message flooding and jittery light flickering.
Instead, separate sensing from acting:
- All sensor nodes publish their raw data to an intake channel (
space/tables/raw). - An Aggregator (which can be a Python script or an OOCSI server-side sliding window transform) collects readings from all tables over a rolling 10-second window.
- The Aggregator calculates smooth, room-wide averages and publishes a single, tidy message to
space/room/summary. - The chandelier only listens to the summary channel, creating calm, stable, organic ambient lighting.
Code Examples
Table Sensor Node (ESP32 / Arduino C++)
Periodically measures local sound and light and posts to the raw stream:
#include "OOCSI.h"
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* hostserver = "oocsi.example.com";
const char* OOCSIName = "table_sensor_####";
OOCSI oocsi = OOCSI();
unsigned long lastSend = 0;
void setup() {
Serial.begin(115200);
oocsi.connect(OOCSIName, hostserver, ssid, password);
}
void loop() {
oocsi.check();
// Publish sensor reading every 2 seconds
if (millis() - lastSend > 2000) {
lastSend = millis();
int soundLevel = analogRead(34); // Microphone sensor pin
int lightLevel = analogRead(35); // Light sensor pin
oocsi.newMessage("space/tables/raw");
oocsi.addInt("sound", soundLevel);
oocsi.addInt("light", lightLevel);
oocsi.sendMessage();
Serial.println("[*] Table reading sent");
}
}
Ambient Aggregator & Chandelier Driver (Python)
Collects all table readings, calculates a smoothed average, and drives the room experience:
from oocsi import OOCSI
import time
oocsi = OOCSI('room_aggregator_####', 'localhost')
# Storage for recent readings across all tables
recent_sound_readings = []
recent_light_readings = []
WINDOW_SIZE = 20 # Keep latest 20 readings
def handle_raw_sensor(sender, recipient, data):
global recent_sound_readings, recent_light_readings
sound = data.get('sound')
light = data.get('light')
if sound is not None and light is not None:
recent_sound_readings.append(sound)
recent_light_readings.append(light)
# Keep window bounded
if len(recent_sound_readings) > WINDOW_SIZE:
recent_sound_readings.pop(0)
recent_light_readings.pop(0)
oocsi.subscribe('space/tables/raw', handle_raw_sensor)
print("[*] Aggregator calculating room environment...")
while True:
if recent_sound_readings:
avg_sound = sum(recent_sound_readings) / len(recent_sound_readings)
avg_light = sum(recent_light_readings) / len(recent_light_readings)
# Determine ambient lighting mood
mood = 'CALM_WARM' if avg_sound < 1500 else 'ENERGETIC_WHITE'
# Publish clean summary for the chandelier
oocsi.send('space/room/summary', {
'avg_sound': round(avg_sound, 1),
'avg_light': round(avg_light, 1),
'mood': mood
})
print(f" -> Room mood: {mood} (Sound: {avg_sound:.1f}, Light: {avg_light:.1f})")
time.sleep(3) # Publish smooth summary every 3 seconds
5. Group Consensus & Negotiation: Taking a Group Vote
The Design Scenario
Imagine you are building a collaborative music jukebox, an installation where four visitors sit on interactive swings to agree on an atmosphere, or an interactive voting pedestal in an exhibition.
- No single visitor or device gets to dictate what happens next.
- The installation periodically prompts participants to cast a vote (e.g. choose between
FOREST,OCEAN, orDESERT). - Once the voting countdown ends, the votes are tallied, and every device in the installation adopts the winning choice together!
sequenceDiagram
autonumber
participant Coord as Poll Coordinator (Python)
participant Broker as OOCSI Broker
participant Stations as Voting Stations (ESP32)
participant Audio as Sound & Light Installation
Coord->>Broker: Broadcast: {"action": "OPEN_POLL", "poll_id": 42, "timeout": 5}
Broker->>Stations: Prompt visitors to vote
Note over Stations,Coord: Voting Window (5 seconds)
Stations->>Broker: Post ballots: {"poll_id": 42, "vote": "OCEAN"}
Broker->>Coord: Collect ballots
Note over Coord: Tally ballots:<br/>Winner: "OCEAN"
Coord->>Broker: Broadcast: {"action": "WINNER", "choice": "OCEAN"}
par Coordinated Experience Update
Broker->>Stations: Flash Cyan (Confirmation glow)
and
Broker->>Audio: Crossfade into Ocean soundscape
end
The Design Pattern
- Poll Invitation: A coordinator starts a voting round by broadcasting
{"action": "OPEN_POLL", "poll_id": 42, "timeout": 5}to a shared channel. - Voting Window: For the next 5 seconds, participant devices post ballots containing the
poll_idand their chosen option tovoting/ballots. - Tallying & Tie-Breaking: When the countdown timer expires, the coordinator tallies the votes. If there is a tie, it can use a random pick or keep the previous state.
- Consensus Announcement: The coordinator announces the winning outcome (
{"action": "WINNER", "choice": "OCEAN"}). - Harmonious Action: Every station and actuator updates its display, sound, and lighting to reflect the winning collective decision.
Code Examples
Voting Coordinator (Python)
Opens polls, collects ballots during a deadline, and announces the winner:
from oocsi import OOCSI
from collections import Counter
import time
oocsi = OOCSI('voting_coordinator_####', 'localhost')
current_poll_id = 0
ballots = []
voting_open = False
def handle_ballot(sender, recipient, data):
global ballots
if voting_open and data.get('poll_id') == current_poll_id:
choice = data.get('vote')
if choice:
ballots.append(choice)
print(f"[*] Received ballot from {sender}: {choice}")
oocsi.subscribe('voting/ballots', handle_ballot)
def run_voting_round(options=['OCEAN', 'FOREST', 'DESERT'], duration=5):
global current_poll_id, ballots, voting_open
current_poll_id += 1
ballots = []
voting_open = True
print(f"\n[===] STARTING VOTE #{current_poll_id} for {duration} seconds [===]")
oocsi.send('voting/channel', {
'action': 'OPEN_POLL',
'poll_id': current_poll_id,
'options': options,
'timeout': duration
})
# Wait for the voting window to elapse
time.sleep(duration)
voting_open = False
# Tally votes
if ballots:
tally = Counter(ballots)
winner = tally.most_common(1)[0][0]
print(f"[*] Vote complete! Tallies: {dict(tally)} -> Winner: {winner}")
else:
winner = options[0] # Default if nobody voted
print(f"[*] No votes cast. Defaulting to: {winner}")
# Broadcast winning consensus to everyone
oocsi.send('voting/channel', {
'action': 'WINNER',
'poll_id': current_poll_id,
'choice': winner
})
# Run a sample poll every 20 seconds
while True:
run_voting_round()
time.sleep(15)
Participant Voting Station (ESP32 / Arduino C++)
Allows visitors to press a button to vote, and responds when the winner is announced:
#include "OOCSI.h"
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* hostserver = "oocsi.example.com";
const char* OOCSIName = "vote_station_####";
OOCSI oocsi = OOCSI();
int currentPollId = -1;
bool canVote = false;
const int BUTTON_PIN = 0; // Boot button on most ESP32 boards
const int LED_PIN = 2;
void onVoteEvent() {
String action = oocsi.getString("action", "");
if (action == "OPEN_POLL") {
currentPollId = oocsi.getInt("poll_id", 0);
canVote = true;
digitalWrite(LED_PIN, HIGH); // Turn on LED: Voting is open!
Serial.println("[*] Poll is open! Press button to vote.");
} else if (action == "WINNER") {
canVote = false;
digitalWrite(LED_PIN, LOW);
String winner = oocsi.getString("choice", "");
Serial.print("[*] Group consensus reached! Winner is: ");
Serial.println(winner);
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
oocsi.connect(OOCSIName, hostserver, ssid, password, onVoteEvent);
oocsi.subscribe("voting/channel");
}
void loop() {
oocsi.check();
// If voting is open and user presses the button, cast a vote!
if (canVote && digitalRead(BUTTON_PIN) == LOW) {
oocsi.newMessage("voting/ballots");
oocsi.addInt("poll_id", currentPollId);
oocsi.addString("vote", "OCEAN"); // This station's vote
oocsi.sendMessage();
Serial.println(" -> Ballot cast: OCEAN");
canVote = false; // Vote cast, prevent duplicate clicks
digitalWrite(LED_PIN, LOW);
delay(300);
}
delay(20);
}
Prototyping Checklist for Design Students
When building multi-device installations, keep these five golden rules in mind:
- Always use
#hashes in client names (lamp_####): Adding four hashes guarantees that every time your ESP32 reboots or re-flashes, the OOCSI server gives it a fresh, unique name. This avoids “client handle already taken” errors. - Use
_RETAINfor persistent room state: If an experience has phases, colors, or modes that a newcomer needs to know immediately upon plugging in, always send it with_RETAIN. - Pace your messages gracefully: Never put
oocsi.sendMessage()inside an un-throttled loop without a delay! Send messages when an input changes, or at human-scale intervals (50–100ms for continuous sliders, 1–5 seconds for ambient room sensors). - Design graceful fallbacks: Always ask yourself: “If the WiFi disconnects for 5 seconds, what will the user see?” Keep local animations running gracefully so your prototypes feel dependable even during network glitches.
- Use OOCSI debugging tools to see what’s happening: When multiple devices are talking, you cannot guess who sent what. Open the web browser tools:
- View channels in real time with the OOCSI tools.
- Graph room-wide sensor streams on the Data Canvas.
Video Lecture: System Design with OOCSI
Watch Mathias’ lecture on multi-device systems from the Technologies for Connectivity course at Eindhoven University of Technology:
Next Steps
Now that you know how to architect multi-device interactive systems, explore:
- OOCSI Tools & Debugging Suite: Monitor channels, inspect connections, and test prototypes.
- Data Canvas: Visualize spatial sensor data streams and explore interactive system behavior.
- OOCSI Clients Reference: Full API references for Java, Python, JavaScript, and Embedded microcontrollers.