WebSocket Client - Usage Examples
This document provides practical examples for implementing the WebSocket client management features in LedFx.
Migrating from Legacy System? If you’re updating code that uses the old
POST /api/clientssync action orclient_syncevents, see the Client Sync Migration Guide for step-by-step migration instructions. 📊 Lifecycle Diagrams: This document includes Mermaid sequence diagrams throughout to visualize key interaction patterns.
Table of Contents
Basic Client Setup
Lifecycle: Client Connection & Registration
sequenceDiagram
participant Client
participant WebSocket
participant Server
Client->>WebSocket: Connect to ws://localhost:8888/api/websocket
WebSocket->>Server: WebSocket Connection Established
Server->>WebSocket: client_id event {client_id: "abc-123"}
WebSocket->>Client: onmessage: client_id received
Note over Client: Store client_id
Client->>WebSocket: set_client_info {name, type, device_id}
Server->>Server: Validate & store metadata
Server->>WebSocket: client_info_updated event
WebSocket->>Client: onmessage: Registration confirmed
Note over Client: Client is now registered
Server->>Server: Fire ClientsUpdatedEvent
Server-->>WebSocket: clients_updated event (to subscribers)
const websocket = new WebSocket('ws://localhost:8888/api/websocket');
let myClientId = null;
websocket.onopen = () => {
console.log('WebSocket connected');
};
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
// First message will be our client ID
if (data.event_type === 'client_id') {
myClientId = data.client_id;
console.log('Received client ID:', myClientId);
// Now set our metadata
setClientInfo();
}
};
function setClientInfo() {
websocket.send(JSON.stringify({
id: 1,
type: 'set_client_info',
data: {
name: 'My Application',
type: 'controller',
device_id: 'my-device-123'
}
}));
}
Metadata Management
Lifecycle: Name Conflict Resolution
sequenceDiagram
participant Client1
participant Client2
participant Server
Note over Client1: Already registered as "Display"
Client2->>Server: set_client_info {name: "Display"}
Server->>Server: Check name uniqueness
Server->>Server: Name exists! Append " (2)"
Server->>Client2: client_info_updated {name: "Display (2)", name_conflict: true}
Note over Client2: Notify user of name change
alt Another client with same name
Client2->>Server: set_client_info {name: "Display"}
Server->>Server: "Display" and "Display (2)" exist
Server->>Server: Append " (3)"
Server->>Client2: client_info_updated {name: "Display (3)", name_conflict: true}
end
Setting Client Info with Error Handling
// Helper: Generate unique message IDs
let messageIdCounter = 1;
function getNextMessageId() {
return messageIdCounter++;
}
function setClientInfo(name, type, deviceId = null) {
const messageId = getNextMessageId();
websocket.send(JSON.stringify({
id: messageId,
type: 'set_client_info',
data: {
name: name,
type: type,
device_id: deviceId
}
}));
// Handle response
return new Promise((resolve, reject) => {
const handler = (event) => {
const data = JSON.parse(event.data);
if (data.id === messageId) {
if (data.event_type === 'client_info_updated') {
console.log('Metadata set:', data);
if (data.name_conflict) {
console.warn(`Name conflict: "${name}" changed to "${data.name}"`);
}
cleanup();
resolve(data);
} else if (data.success === false) {
console.error('Failed to set metadata:', data.error);
cleanup();
reject(data.error);
}
}
};
const closeHandler = () => {
cleanup();
reject(new Error('WebSocket connection closed'));
};
const cleanup = () => {
websocket.removeEventListener('message', handler);
websocket.removeEventListener('close', closeHandler);
clearTimeout(timeoutId);
};
// Timeout after 5 seconds
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error('Request timeout'));
}, 5000);
websocket.addEventListener('message', handler);
websocket.addEventListener('close', closeHandler);
});
}
// Usage
setClientInfo('Living Room Display', 'visualiser', 'esp32-001')
.then(result => {
console.log('Successfully registered as:', result.name);
})
.catch(error => {
console.error('Registration failed:', error);
});
Updating Client Metadata
You can update client name and/or type after initial connection:
function updateClientInfo(newName, newType) {
const messageId = getNextMessageId();
const data = {};
if (newName !== undefined) {
data.name = newName;
}
if (newType !== undefined) {
data.type = newType;
}
websocket.send(JSON.stringify({
id: messageId,
type: 'update_client_info',
data: data
}));
}
// Example: Update name only
updateClientInfo('New Display Name', undefined);
// Example: Update type only
updateClientInfo(undefined, 'visualiser');
// Example: Update both name and type
updateClientInfo('Bedroom Display', 'display');
// Example: Update name when user changes it in UI
document.getElementById('nameInput').addEventListener('change', (e) => {
updateClientInfo(e.target.value, undefined);
});
// Example: Update type when user selects from dropdown
document.getElementById('typeSelect').addEventListener('change', (e) => {
updateClientInfo(undefined, e.target.value);
});
Fetching Client List
async function getConnectedClients() {
const response = await fetch('http://localhost:8888/api/clients');
const data = await response.json();
// data.result contains the client metadata dictionary
const clients = data.result;
Object.entries(clients).forEach(([uuid, metadata]) => {
console.log(`Client ${uuid}:`, {
name: metadata.name,
type: metadata.type,
ip: metadata.ip,
connected: new Date(metadata.connected_at * 1000).toLocaleString()
});
});
return clients;
}
// Usage
getConnectedClients().then(clients => {
console.log(`${Object.keys(clients).length} clients connected`);
});
Broadcasting Messages
Lifecycle: Broadcasting to All Clients
sequenceDiagram
participant Sender
participant Server
participant Client1
participant Client2
participant Client3
Sender->>Server: broadcast {broadcast_type: "scene_sync", target: {mode: "all"}, payload: {...}}
Server->>Server: Validate broadcast Derive sender identity from WebSocket
Server->>Server: Generate broadcast_id
Server->>Sender: broadcast_sent {targets_matched: 3, target_uuids: [...]}
par Broadcast to all subscribers
Server->>Client1: client_broadcast event {sender_uuid, sender_name, payload}
Server->>Client2: client_broadcast event {sender_uuid, sender_name, payload}
Server->>Client3: client_broadcast event {sender_uuid, sender_name, payload}
end
Note over Client1,Client3: Each client filters: 1. Check target_uuids includes them 2. Skip if sender_uuid == own client_id
Broadcasting to All Clients
function broadcastToAll(messageType, payload) {
const messageId = getNextMessageId();
websocket.send(JSON.stringify({
id: messageId,
type: 'broadcast',
data: {
broadcast_type: messageType,
target: {
mode: 'all'
},
payload: payload
}
}));
}
// Example: Broadcast scene change
broadcastToAll('scene_sync', {
scene_id: 'party-mode',
action: 'activate'
});
Lifecycle: Targeted Broadcasting (By Type)
sequenceDiagram
participant Controller
participant Server
participant Visualiser1
participant Visualiser2
participant Mobile
Note over Visualiser1: type: "visualiser"
Note over Visualiser2: type: "visualiser"
Note over Mobile: type: "mobile"
Controller->>Server: broadcast {broadcast_type: "visualiser_control", target: {mode: "type", value: "visualiser"}}
Server->>Server: Filter clients by type="visualiser"
Server->>Controller: broadcast_sent {targets_matched: 2}
par Broadcast to matching type only
Server->>Visualiser1: client_broadcast event
Server->>Visualiser2: client_broadcast event
end
Note over Mobile: No broadcast received (type doesn't match)
Broadcasting to Specific Client Types
function broadcastToVisualisers(payload) {
websocket.send(JSON.stringify({
id: getNextMessageId(),
type: 'broadcast',
data: {
broadcast_type: 'visualiser_control',
target: {
mode: 'type',
value: 'visualiser'
},
payload: payload
}
}));
}
// Example: Send color palette to all visualisers
broadcastToVisualisers({
colors: ['#FF0000', '#00FF00', '#0000FF'],
transition: 'fade',
duration: 2000
});
Broadcasting to Specific Clients by Name
function broadcastToNamedClients(names, payload) {
websocket.send(JSON.stringify({
id: getNextMessageId(),
type: 'broadcast',
data: {
broadcast_type: 'custom',
target: {
mode: 'names',
names: names
},
payload: payload
}
}));
}
// Example: Send command to specific displays
broadcastToNamedClients(
['Living Room Display', 'Bedroom Display'],
{
brightness: 80,
effect: 'rainbow'
}
);
Broadcasting with Response Handling
function broadcastWithConfirmation(broadcastType, target, payload) {
const messageId = getNextMessageId();
websocket.send(JSON.stringify({
id: messageId,
type: 'broadcast',
data: {
broadcast_type: broadcastType,
target: target,
payload: payload
}
}));
return new Promise((resolve, reject) => {
const handler = (event) => {
const data = JSON.parse(event.data);
if (data.id === messageId) {
if (data.event_type === 'broadcast_sent') {
console.log(`Broadcast ${data.broadcast_id} sent to ${data.targets_matched} clients`);
cleanup();
resolve(data);
} else if (data.success === false) {
console.error('Broadcast failed:', data.error);
cleanup();
reject(data.error);
}
}
};
const closeHandler = () => {
cleanup();
reject(new Error('WebSocket connection closed'));
};
const cleanup = () => {
websocket.removeEventListener('message', handler);
websocket.removeEventListener('close', closeHandler);
clearTimeout(timeoutId);
};
// Timeout after 5 seconds
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error('Request timeout'));
}, 5000);
websocket.addEventListener('message', handler);
websocket.addEventListener('close', closeHandler);
});
}
// Usage
broadcastWithConfirmation('scene_sync', {mode: 'all'}, {scene: 'chill'})
.then(result => {
console.log(`Delivered to ${result.target_uuids.length} clients`);
})
.catch(error => {
console.error('Broadcast error:', error.message);
});
Event Handling
Lifecycle: Client List Change Notification
sequenceDiagram
participant ClientA
participant Server
participant ClientB
participant NewClient
Note over ClientA,ClientB: Already connected & subscribed to clients_updated
NewClient->>Server: WebSocket Connect
Server->>NewClient: client_id event
NewClient->>Server: set_client_info
Server->>Server: Update client metadata
Server->>Server: Fire ClientsUpdatedEvent
par Notify all subscribers
Server->>ClientA: clients_updated event
Server->>ClientB: clients_updated event
end
Note over ClientA: Refresh client list via GET /api/clients
Note over ClientB: Update UI with new client
ClientA->>Server: GET /api/clients
Server->>ClientA: Full client list with metadata
Lifecycle: Receiving and Filtering Broadcasts
sequenceDiagram
participant Sender
participant Server
participant Receiver
participant OtherClient
Note over Receiver: client_id: "abc-123" Subscribed to client_broadcast
Note over OtherClient: client_id: "xyz-789" Subscribed to client_broadcast
Sender->>Server: broadcast {target: {mode: "uuids", uuids: ["abc-123"]}}
Server->>Server: Determine targets for audit & response
par Broadcast to ALL subscribers
Server->>Receiver: client_broadcast {target_uuids: ["abc-123"]}
Server->>OtherClient: client_broadcast {target_uuids: ["abc-123"]}
end
Receiver->>Receiver: Check: "abc-123" in target_uuids? yes
Receiver->>Receiver: Check: sender_uuid != "abc-123"? yes
Note over Receiver: Process broadcast payload
OtherClient->>OtherClient: Check: "xyz-789" in target_uuids? no
Note over OtherClient: Discard (not in target list)
alt Sender receives their own broadcast
Sender->>Sender: Check: sender_uuid == own client_id?
Note over Sender: Skip processing (own broadcast)
end
Subscribing to Client Events
// Subscribe to clients_updated event
websocket.send(JSON.stringify({
id: getNextMessageId(),
type: 'subscribe_event',
event_type: 'clients_updated'
}));
// Subscribe to client_broadcast event
websocket.send(JSON.stringify({
id: getNextMessageId(),
type: 'subscribe_event',
event_type: 'client_broadcast'
}));
Handling Client List Changes
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event_type === 'clients_updated') {
console.log('Client list changed, refreshing...');
refreshClientList();
}
};
async function refreshClientList() {
const clients = await getConnectedClients();
updateClientListUI(clients);
}
function updateClientListUI(clients) {
const listElement = document.getElementById('clientList');
listElement.innerHTML = '';
Object.entries(clients).forEach(([uuid, metadata]) => {
const item = document.createElement('div');
item.className = 'client-item';
// Safely create name element
const nameElement = document.createElement('strong');
nameElement.textContent = metadata.name;
item.appendChild(nameElement);
// Safely create type badge
const typeElement = document.createElement('span');
typeElement.className = 'badge';
typeElement.textContent = metadata.type;
item.appendChild(typeElement);
// Safely create IP element
const ipElement = document.createElement('span');
ipElement.className = 'ip';
ipElement.textContent = metadata.ip;
item.appendChild(ipElement);
listElement.appendChild(item);
});
}
Receiving Broadcasts
Important: Broadcast events are sent to ALL subscribers of client_broadcast. You MUST filter by target_uuids to determine if the message is intended for you. This is client-side filtering - the server does not selectively send to specific connections.
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event_type === 'client_broadcast') {
// REQUIRED: Check if this broadcast is for us
// All subscribers receive this event - we must filter client-side
if (!Array.isArray(data.target_uuids) || !data.target_uuids.includes(myClientId)) {
return; // Not for us - discard
}
// OPTIONAL: Filter out our own broadcasts
if (data.sender_uuid === myClientId) {
return; // We sent this - discard
}
console.log(`Broadcast from ${data.sender_name}:`, data.payload);
// Handle based on broadcast type
switch (data.broadcast_type) {
case 'scene_sync':
handleSceneSync(data.payload);
break;
case 'color_palette':
handleColorPalette(data.payload);
break;
case 'visualiser_control':
handleVisualiserControl(data.payload);
break;
case 'custom':
handleCustomBroadcast(data.payload);
break;
}
}
};
function handleSceneSync(payload) {
console.log('Scene sync received:', payload.scene_id);
// Activate the scene
activateScene(payload.scene_id);
}
function handleColorPalette(payload) {
console.log('Color palette received:', payload.colors);
// Update color scheme
updateColors(payload.colors);
}
Complete Example Applications
Lifecycle: End-to-End Scene Sync (Controller → Visualisers)
sequenceDiagram
participant Controller
participant Server
participant Visualiser1
participant Visualiser2
Note over Controller,Visualiser2: Initial Setup
Controller->>Server: Connect & set_client_info (type: "controller")
Visualiser1->>Server: Connect & set_client_info (type: "visualiser")
Visualiser2->>Server: Connect & set_client_info (type: "visualiser")
Controller->>Server: subscribe_event: clients_updated
Visualiser1->>Server: subscribe_event: client_broadcast
Visualiser2->>Server: subscribe_event: client_broadcast
Note over Controller,Visualiser2: User Activates Scene in Controller
Controller->>Server: POST /api/scenes/party-mode/activate
Server->>Controller: Scene activated successfully
Controller->>Server: broadcast { broadcast_type: "scene_sync", target: {mode: "type", value: "visualiser"}, payload: {scene_id: "party-mode", action: "activate"} }
Server->>Server: Filter clients by type="visualiser"
Server->>Server: Derive sender from WebSocket (Controller)
Server->>Controller: broadcast_sent {targets_matched: 2}
par Notify visualisers
Server->>Visualiser1: client_broadcast { sender_name: "Scene Controller", sender_type: "controller", broadcast_type: "scene_sync", payload: {scene_id: "party-mode"} }
Server->>Visualiser2: client_broadcast { sender_name: "Scene Controller", sender_type: "controller", broadcast_type: "scene_sync", payload: {scene_id: "party-mode"} }
end
Visualiser1->>Visualiser1: Check broadcast_type == "scene_sync"
Visualiser1->>Visualiser1: activateScene("party-mode")
Note over Visualiser1: Display updates to party mode
Visualiser2->>Visualiser2: Check broadcast_type == "scene_sync"
Visualiser2->>Visualiser2: activateScene("party-mode")
Note over Visualiser2: Display updates to party mode
Controller Application
A controller app that manages scenes and broadcasts to visualisers:
class LedFxController {
constructor(wsUrl = 'ws://localhost:8888/api/websocket') {
this.wsUrl = wsUrl;
this.websocket = null;
this.clientId = null;
this.messageId = 1;
this.registrationMessageId = null;
this.connectedClients = {};
}
connect() {
this.websocket = new WebSocket(this.wsUrl);
this.websocket.onopen = () => {
console.log('Controller connected');
};
this.websocket.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
this.websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
handleMessage(data) {
switch (data.event_type) {
case 'client_id':
this.clientId = data.client_id;
this.registerAsController();
this.subscribeToEvents();
break;
case 'client_info_updated':
if (data.id === this.registrationMessageId) {
console.log('Registered as controller:', data.name);
this.refreshClientList().catch(err =>
console.error('Failed to refresh client list:', err)
);
}
break;
case 'clients_updated':
this.refreshClientList().catch(err =>
console.error('Failed to refresh client list:', err)
);
break;
case 'broadcast_sent':
console.log(`Broadcast delivered to ${data.targets_matched} clients`);
break;
}
}
registerAsController() {
this.registrationMessageId = this.messageId;
this.send({
id: this.messageId++,
type: 'set_client_info',
data: {
name: 'Scene Controller',
type: 'controller'
}
});
}
subscribeToEvents() {
this.send({
id: this.messageId++,
type: 'subscribe_event',
event_type: 'clients_updated'
});
}
async refreshClientList() {
const response = await fetch('http://localhost:8888/api/clients');
const data = await response.json();
this.connectedClients = data.result;
console.log('Connected clients:', Object.keys(this.connectedClients).length);
}
activateScene(sceneId) {
console.log(`Activating scene: ${sceneId}`);
// Broadcast to all visualisers
this.send({
id: this.messageId++,
type: 'broadcast',
data: {
broadcast_type: 'scene_sync',
target: {
mode: 'type',
value: 'visualiser'
},
payload: {
scene_id: sceneId,
action: 'activate',
timestamp: Date.now()
}
}
});
}
updateColorPalette(colors) {
this.send({
id: this.messageId++,
type: 'broadcast',
data: {
broadcast_type: 'color_palette',
target: { mode: 'all' },
payload: { colors: colors }
}
});
}
send(message) {
this.websocket.send(JSON.stringify(message));
}
}
// Usage
const controller = new LedFxController();
controller.connect();
// Later: activate a scene
setTimeout(() => {
controller.activateScene('party-mode');
}, 5000);
Visualiser Application
A visualiser app that receives broadcasts and updates display:
class LedFxVisualiser {
constructor(name, wsUrl = 'ws://localhost:8888/api/websocket') {
this.name = name;
this.wsUrl = wsUrl;
this.websocket = null;
this.clientId = null;
this.messageId = 1;
}
connect() {
this.websocket = new WebSocket(this.wsUrl);
this.websocket.onopen = () => {
console.log('Visualiser connected');
};
this.websocket.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
}
handleMessage(data) {
switch (data.event_type) {
case 'client_id':
this.clientId = data.client_id;
this.registerAsVisualiser();
this.subscribeToBroadcasts();
break;
case 'client_info_updated':
console.log('Registered as:', data.name);
break;
case 'client_broadcast':
this.handleBroadcast(data);
break;
}
}
registerAsVisualiser() {
this.send({
id: this.messageId++,
type: 'set_client_info',
data: {
name: this.name,
type: 'visualiser'
}
});
}
subscribeToBroadcasts() {
this.send({
id: this.messageId++,
type: 'subscribe_event',
event_type: 'client_broadcast'
});
}
handleBroadcast(data) {
// REQUIRED: Client-side filtering
// All subscribers receive this event - we must check target_uuids
if (!Array.isArray(data.target_uuids) || !data.target_uuids.includes(this.clientId)) {
return; // Not intended for us
}
// OPTIONAL: Ignore our own broadcasts
if (data.sender_uuid === this.clientId) {
return;
}
console.log(`Broadcast from ${data.sender_name}:`, data.broadcast_type);
switch (data.broadcast_type) {
case 'scene_sync':
this.activateScene(data.payload.scene_id);
break;
case 'color_palette':
this.updateColors(data.payload.colors);
break;
case 'visualiser_control':
this.handleControl(data.payload);
break;
}
}
activateScene(sceneId) {
console.log(`Activating scene: ${sceneId}`);
// Implementation: activate the scene on this visualiser
}
updateColors(colors) {
console.log('Updating color palette:', colors);
// Implementation: update display colors
}
handleControl(command) {
console.log('Control command:', command);
// Implementation: handle visualiser control
}
send(message) {
this.websocket.send(JSON.stringify(message));
}
}
// Usage
const visualiser = new LedFxVisualiser('Living Room Display');
visualiser.connect();
Best Practices
1. Always Set Client Metadata
Always call set_client_info after receiving your client ID to enable proper identification.
2. Handle Name Conflicts Gracefully
Check the name_conflict flag in responses and inform users if their chosen name was modified.
3. REQUIRED: Filter Broadcasts Client-Side
Critical: Broadcast events are sent to ALL subscribers. You MUST check if your UUID is in target_uuids before processing. This is not optional - the server does not restrict delivery to specific connections. Always filter out your own broadcasts as well.
4. Validate Payload Size
Keep broadcast payloads under 2048 bytes. For larger data, use REST API endpoints and broadcast just references.
5. Use Appropriate Broadcast Types
Choose the correct broadcast_type to help receivers handle messages appropriately:
scene_sync- Scene activation/coordinationcolor_palette- Color scheme updatesvisualiser_control- Display control commandscustom- Application-specific messages
6. Subscribe to events
Subscribe to clients_updated to keep your client list synchronized.
7. Implement Reconnection Logic
function connectWithRetry(maxRetries = 5, delay = 1000) {
let retries = 0;
function connect() {
const ws = new WebSocket('ws://localhost:8888/api/websocket');
ws.onclose = () => {
if (retries < maxRetries) {
retries++;
// Exponential backoff: 1s, 2s, 4s, 8s, 16s...
const backoffDelay = delay * Math.pow(2, retries - 1);
console.log(`Reconnecting in ${backoffDelay}ms... (${retries}/${maxRetries})`);
setTimeout(connect, backoffDelay);
} else {
console.error('Max reconnection attempts reached');
}
};
return ws;
}
return connect();
}
Lifecycle: Reconnection with Exponential Backoff
sequenceDiagram
participant Client
participant WebSocket
participant Server
Client->>WebSocket: Initial Connection
WebSocket->>Server: Connected
Server->>Client: client_id event
Note over Client: Store client_id, register metadata
Note over Client,Server: Connection Active
Server--xWebSocket: Connection Lost
WebSocket->>Client: onclose event
Note over Client: Retry 1: Wait 1s
Client->>WebSocket: Reconnect Attempt 1
WebSocket--xClient: Connection Failed
WebSocket->>Client: onclose event
Note over Client: Retry 2: Wait 2s
Client->>WebSocket: Reconnect Attempt 2
WebSocket--xClient: Connection Failed
WebSocket->>Client: onclose event
Note over Client: Retry 3: Wait 4s
Client->>WebSocket: Reconnect Attempt 3
WebSocket->>Server: Connected
Server->>Client: NEW client_id event
Note over Client: Store new client_id, re-register metadata, re-subscribe to events
Note over Client: Connection Restored
Troubleshooting
Problem: Not Receiving Broadcasts
Solution: Ensure you’ve subscribed to the client_broadcast event:
websocket.send(JSON.stringify({
id: 1,
type: 'subscribe_event',
event_type: 'client_broadcast'
}));
Problem: Name Conflict Every Time
Solution: Check if another client is using your name. Consider adding a unique suffix:
const name = `My App ${Math.random().toString(36).substr(2, 4)}`;
Problem: Broadcast Fails with “No targets matched”
Solution: Verify target specification is correct and target clients exist. Fetch client list to confirm:
const clients = await getConnectedClients();
console.log('Available clients:', Object.values(clients).map(c => c.name));
Problem: Payload Too Large Error
Solution: Reduce payload size or split into multiple broadcasts:
// Instead of sending large data
const largePayload = { image: base64ImageData }; // Too large!
// Upload via REST and broadcast reference
const uploadResponse = await uploadImage(imageData);
broadcastToAll('custom', { image_id: uploadResponse.id });