WebSocket API Documentation¶
Introduction¶
This document describes the WebSocket endpoints for the Genius Gateway. The gateway provides real-time bidirectional communication through WebSocket connections for event streaming and packet logging.
Common Connection Requirements¶
- Protocol: WebSocket (ws:// or wss://)
- Authentication: Via JWT token (see Authorization Levels below)
Authorization Levels¶
- 🔒 User - Requires valid JWT token (event subscriptions, read operations)
- 🛡️ Admin - Requires valid JWT token with admin privileges (system logs, configuration)
Authentication¶
WebSocket connections can be authenticated by including the JWT token in:
- Query parameter:
?access_token=<token> - WebSocket subprotocol: Send token in
Sec-WebSocket-Protocolheader
WebSocket Endpoints¶
Overview¶
| Endpoint | Auth | Description |
|---|---|---|
/ws/events | 🔒 | Real-time event stream for system and application events |
/ws/logger | 🛡️ | Real-time RF packet stream from CC1101 radio |
/ws/events - Event Stream¶
Connection Details¶
- URL:
/ws/events - Auth: 🔒 User
- Description: Subscribe to real-time system and application events. Clients can subscribe to specific event types and receive JSON-formatted event notifications.
Connection Flow¶
- Connect - Establish WebSocket connection with authentication
- Subscribe - Send subscription message for desired events
- Receive - Get real-time event notifications as JSON
- Unsubscribe - Optionally unsubscribe from events
- Disconnect - Close connection when done
Message Format¶
Subscribe to Event¶
Unsubscribe from Event¶
Event Notification (Server → Client)¶
Framework Events (ESP32 SvelteKit)¶
rssi¶
WiFi signal strength updates
Update Frequency: Every 500ms when connected
Data Format:
Fields:
rssi- Signal strength in dBm (negative integer)
reconnect¶
WiFi reconnection notifications
Trigger: WiFi connection attempt
Data Format:
system_health¶
System health metrics and resource usage
Update Frequency: Every 5 seconds
Data Format:
{
"event": "system_health",
"data": {
"free_heap": 200000,
"max_alloc_heap": 150000,
"min_free_heap": 180000,
"psram_size": 8388608,
"free_psram": 6000000,
"cpu_temp": 45.2,
"uptime": 3600
}
}
Fields:
free_heap- Current free heap memory (bytes)max_alloc_heap- Largest allocatable block (bytes)min_free_heap- Minimum free heap since boot (bytes)psram_size- Total PSRAM size (bytes)free_psram- Available PSRAM (bytes)cpu_temp- CPU temperature (°C, if available)uptime- System uptime (seconds)
notification¶
System notifications and alerts
Trigger: Various system events
Data Format:
{
"event": "notification",
"data": {
"type": "info",
"message": "Notification message",
"timestamp": "2025-01-15T10:30:00Z"
}
}
Fields:
type- Notification type:info,warning,error,successmessage- Notification texttimestamp- ISO 8601 timestamp
features¶
Feature flag updates
Trigger: Feature configuration changes
Data Format:
{
"event": "features",
"data": {
"features": {
"allow_broadcast": true,
"cc1101_controller": true,
"battery_monitoring": false
}
}
}
Fields:
features- Object containing feature name/enabled pairs
otastatus¶
OTA firmware update progress
Trigger: During firmware download and installation
Data Format:
{
"event": "otastatus",
"data": {
"status": "downloading",
"progress": 45,
"message": "Downloading firmware..."
}
}
Fields:
status- Update status:idle,downloading,installing,success,errorprogress- Progress percentage (0-100)message- Human-readable status message
battery¶
Battery status updates (if battery monitoring enabled)
Update Frequency: Variable based on configuration
Data Format:
Fields:
voltage- Battery voltage (V)percentage- Battery level (0-100)charging- Charging status (boolean)
analytics¶
System analytics data (if analytics enabled)
Update Frequency: Periodic based on configuration
Data Format:
Genius Gateway Events¶
alarm¶
Global alarm state notifications
Trigger: Any smoke detector enters or exits alarm state
Data Format:
{
"event": "alarm",
"data": {
"isAlarming": true,
"numAlarmingDevices": 2,
"alarmingDevices": [
{
"smokeDetectorSN": 12345678,
"location": "Living Room",
"startTime": "2025-01-15T10:30:00Z"
}
]
}
}
Fields:
isAlarming- Global alarm state (boolean)numAlarmingDevices- Number of devices currently alarmingalarmingDevices- Array of alarming device detailssmokeDetectorSN- Smoke detector serial numberlocation- Device location stringstartTime- Alarm start timestamp (ISO 8601)
alarm-state-change¶
Individual device alarm state changes
Trigger: Specific smoke detector alarm state changes
Data Format:
{
"event": "alarm-state-change",
"data": {
"smokeDetectorSN": 12345678,
"isAlarming": true,
"location": "Living Room",
"startTime": "2025-01-15T10:30:00Z"
}
}
Fields:
smokeDetectorSN- Smoke detector serial numberisAlarming- Current alarm state (boolean)location- Device locationstartTime- Alarm start time (ISO 8601, if alarming)endTime- Alarm end time (ISO 8601, if ended)endingReason- How alarm ended (0=detector, 1=manual)
genius-device-added-from-packet¶
New smoke detector auto-discovered
Trigger: New device detected from RF packet
Data Format:
{
"event": "genius-device-added-from-packet",
"data": {
"smokeDetectorSN": 12345678,
"radioModuleSN": 87654321,
"location": "Unknown location"
}
}
Fields:
smokeDetectorSN- Smoke detector serial numberradioModuleSN- Radio module serial numberlocation- Initial location (typically "Unknown location")
new-alarm-line¶
New alarm line discovered
Trigger: New alarm line detected from RF communication
Data Format:
{
"event": "new-alarm-line",
"data": {
"id": 123456789,
"name": "Alarm Line 123456789",
"created": "2025-01-15T10:30:00Z",
"acquisition": 1
}
}
Fields:
id- Unique 32-bit alarm line IDname- Auto-generated or user-defined namecreated- Discovery timestamp (ISO 8601)acquisition- How line was added (0=built-in, 1=packet, 2=manual)
alarm-line-action-finished¶
Alarm line action completion
Trigger: Line test or fire alarm transmission completed
Data Format:
{
"event": "alarm-line-action-finished",
"data": {
"alarmLineId": 123456789,
"action": "linetest",
"success": true,
"transmissionTime": 10.5
}
}
Fields:
alarmLineId- Alarm line IDaction- Action type:linetest,firealarmsuccess- Action completion status (boolean)transmissionTime- Duration in seconds
rem-alarm-block-time¶
Remaining alarm blocking time updates
Trigger: Alarm blocking active, updates every second
Data Format:
Fields:
isBlocked- Current blocking state (boolean)remainingBlockingTimeS- Seconds remaining until blocking ends
Error Handling¶
Connection Errors: - 401 Unauthorized - Invalid or missing JWT token - 403 Forbidden - Insufficient user privileges
Event Subscription Errors: - Invalid event names are silently ignored - Invalid subscription message format is silently ignored - Subscribe only to valid event names listed in the Framework Events and Genius Gateway Events sections
Best Practices¶
- Selective Subscription - Subscribe only to events you need to minimize bandwidth usage
- High-Frequency Events - Be prepared to handle frequent updates from
rssi(every 500ms) andsystem_health(every 5s) - Reconnection Strategy - Implement automatic reconnection with exponential backoff (start at 1s, max 30s)
- Resubscribe on Reconnect - Store subscriptions and resubscribe after reconnection
- Message Buffering - Consider buffering rapid events like
rssifor display updates - Resource Cleanup - Unsubscribe from events before closing connection
- Error Recovery - Handle JSON parse errors gracefully in case of malformed messages
Connection Examples¶
JavaScript (Browser)¶
const eventsWs = new WebSocket('ws://gateway.local/ws/events?access_token=' + token);
eventsWs.onopen = () => {
console.log('Connected to events stream');
// Subscribe to multiple events
['alarm', 'alarm-state-change', 'system_health'].forEach(eventName => {
eventsWs.send(JSON.stringify({
event: 'subscribe',
data: { id: eventName }
}));
});
};
eventsWs.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('Event:', msg.event, 'Data:', msg.data);
// Handle specific events
switch(msg.event) {
case 'alarm':
handleAlarm(msg.data);
break;
case 'system_health':
updateHealthDisplay(msg.data);
break;
}
};
eventsWs.onerror = (error) => {
console.error('WebSocket error:', error);
};
eventsWs.onclose = () => {
console.log('Disconnected, reconnecting...');
setTimeout(() => connectEvents(), 5000);
};
Python¶
import websocket
import json
import time
class EventsClient:
def __init__(self, token):
self.token = token
self.subscriptions = ['alarm', 'alarm-state-change', 'system_health']
self.ws = None
def on_open(self, ws):
print('Connected to events stream')
# Subscribe to events
for event_name in self.subscriptions:
ws.send(json.dumps({
'event': 'subscribe',
'data': {'id': event_name}
}))
def on_message(self, ws, message):
msg = json.loads(message)
print(f"Event: {msg['event']}, Data: {msg['data']}")
# Handle specific events
if msg['event'] == 'alarm':
self.handle_alarm(msg['data'])
elif msg['event'] == 'system_health':
self.update_health(msg['data'])
def on_error(self, ws, error):
print(f'Error: {error}')
def on_close(self, ws, close_status_code, close_msg):
print('Disconnected, reconnecting...')
time.sleep(5)
self.connect()
def connect(self):
self.ws = websocket.WebSocketApp(
f"ws://gateway.local/ws/events?access_token={self.token}",
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.ws.run_forever()
def handle_alarm(self, data):
if data.get('isAlarming'):
print(f"ALARM! {data['numAlarmingDevices']} device(s) alarming")
def update_health(self, data):
print(f"Free heap: {data['free_heap']} bytes")
# Usage
client = EventsClient(token='your_jwt_token')
client.connect()
Node.js¶
const WebSocket = require('ws');
class EventsClient {
constructor(token) {
this.token = token;
this.subscriptions = ['alarm', 'alarm-state-change', 'system_health'];
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
}
connect() {
this.ws = new WebSocket(`ws://gateway.local/ws/events?access_token=${this.token}`);
this.ws.on('open', () => {
console.log('Connected to events stream');
this.reconnectDelay = 1000; // Reset backoff
// Subscribe to events
this.subscriptions.forEach(eventName => {
this.ws.send(JSON.stringify({
event: 'subscribe',
data: { id: eventName }
}));
});
});
this.ws.on('message', (data) => {
const msg = JSON.parse(data);
console.log('Event:', msg.event, 'Data:', msg.data);
});
this.ws.on('close', () => {
console.log(`Disconnected, reconnecting in ${this.reconnectDelay}ms...`);
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
}
}
const client = new EventsClient('your_jwt_token');
client.connect();
/ws/logger - Packet Stream¶
Connection Details¶
- URL:
/ws/logger - Auth: 🛡️ Admin
- Description: Real-time binary stream of RF packets received by the CC1101 radio transceiver. Provides raw packet data for debugging, analysis, and visualization.
Connection Flow¶
- Connect - Establish WebSocket connection with admin authentication
- Receive ID - Server sends unique client ID
- Stream - Receive binary packet data continuously
- Disconnect - Close connection when done
Message Format¶
Client ID (Server → Client, on connect)¶
Packet Data (Server → Client, binary)¶
Format: Binary data structure (cc1101_packet_t)
Structure:
struct cc1101_packet {
uint64_t timestamp; // Packet reception timestamp (µs since boot)
uint8_t buffer[64]; // Raw CC1101 RX FIFO content
uint8_t* data; // Pointer to actual packet data
size_t length; // Packet data length (bytes)
}
Buffer Contents:
- First byte: Packet length
- Next N (Packt length) bytes: Packet data
- Last 2 bytes: Status (RSSI, LQI / CRC)
Binary Stream Details:
- Type:
HTTPD_WS_TYPE_BINARY - Size: 80 bytes per packet (fixed structure size)
- Frequency: Real-time as packets are received
For further details, also see WebSocket Logging Interface.
Usage Notes¶
- Only available when WebSocket logger is enabled (via
/rest/wsloggeror web frontend) - Connection filter checks admin authentication
- Multiple clients can connect simultaneously
- Each client receives all packets (broadcast)
- Binary format requires client-side parsing
- Timestamp is in microseconds since device boot
Error Handling¶
Connection Errors: - 401 Unauthorized - Invalid or missing JWT token - 403 Forbidden - Insufficient admin privileges - 503 Service Unavailable - WebSocket logger disabled via /rest/wslogger settings
Stream Errors: - Connection drops if logger is disabled during active session - Binary data parsing errors if client expects wrong packet format - Verify logger is enabled before connecting: GET /rest/wslogger
Best Practices¶
- Check Logger Status - Verify logger is enabled via
/rest/wsloggerbefore connecting - Binary Type Setting - Always set
ws.binaryType = 'arraybuffer'or'blob'for proper binary handling - Efficient Parsing - Use DataView for efficient binary data access
- High Data Rate - Prepare for high packet rates during active RF communication
- Buffer Management - Implement packet buffering if processing is slower than arrival rate
- Reconnection Strategy - Implement exponential backoff reconnection (start at 1s, max 30s)
- Resource Management - Close connection when not actively monitoring to reduce server load
- Timestamp Handling - Timestamps are microseconds since boot, convert to absolute time if needed
Connection Examples¶
JavaScript (Browser)¶
const loggerWs = new WebSocket('ws://gateway.local/ws/logger?access_token=' + token);
loggerWs.binaryType = 'arraybuffer';
loggerWs.onopen = () => {
console.log('Connected to packet logger');
};
loggerWs.onmessage = (event) => {
if (typeof event.data === 'string') {
// Client ID message
const msg = JSON.parse(event.data);
console.log('Client ID:', msg.clientId);
} else {
// Binary packet data (cc1101_packet_t structure)
const view = new DataView(event.data);
// Parse structure fields
const timestamp = view.getBigUint64(0, true); // Little endian
// Buffer starts at offset 8, first byte is length
const length = view.getUint8(8);
// Extract packet data (starts at buffer[1])
const packetData = new Uint8Array(event.data, 9, length);
// Last 2 bytes of buffer are RSSI and LQI
const rssi = view.getInt8(8 + length + 1);
const lqi = view.getUint8(8 + length + 2);
console.log('Packet:', {
timestamp: timestamp,
length: length,
data: Array.from(packetData).map(b => b.toString(16).padStart(2, '0')).join(' '),
rssi: rssi,
lqi: lqi & 0x7F,
crc_ok: (lqi & 0x80) !== 0
});
}
};
loggerWs.onerror = (error) => {
console.error('WebSocket error:', error);
};
loggerWs.onclose = () => {
console.log('Logger disconnected');
};
Python¶
import websocket
import struct
import json
class PacketLoggerClient:
def __init__(self, token):
self.token = token
self.ws = None
def on_open(self, ws):
print('Connected to packet logger')
def on_message(self, ws, message):
if isinstance(message, str):
# Client ID message
msg = json.loads(message)
print(f"Client ID: {msg['clientId']}")
else:
# Binary packet data
# Parse cc1101_packet_t structure
timestamp = struct.unpack('<Q', message[0:8])[0] # uint64_t little endian
# Buffer starts at offset 8
length = message[8] # First byte is length
packet_data = message[9:9+length]
# RSSI and LQI are last 2 bytes
rssi = struct.unpack('b', bytes([message[8 + length + 1]]))[0] # signed
lqi_byte = message[8 + length + 2]
lqi = lqi_byte & 0x7F
crc_ok = (lqi_byte & 0x80) != 0
print(f"Packet: timestamp={timestamp}µs, len={length}, "
f"data={packet_data.hex()}, rssi={rssi}dBm, "
f"lqi={lqi}, crc={crc_ok}")
def on_error(self, ws, error):
print(f'Error: {error}')
def on_close(self, ws, close_status_code, close_msg):
print('Logger disconnected')
def connect(self):
self.ws = websocket.WebSocketApp(
f"ws://gateway.local/ws/logger?access_token={self.token}",
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
self.ws.run_forever()
# Usage
client = PacketLoggerClient(token='your_jwt_token')
client.connect()
Node.js¶
const WebSocket = require('ws');
class PacketLoggerClient {
constructor(token) {
this.token = token;
}
connect() {
this.ws = new WebSocket(`ws://gateway.local/ws/logger?access_token=${this.token}`);
this.ws.binaryType = 'arraybuffer';
this.ws.on('open', () => {
console.log('Connected to packet logger');
});
this.ws.on('message', (data) => {
if (typeof data === 'string') {
// Client ID message
const msg = JSON.parse(data);
console.log('Client ID:', msg.clientId);
} else {
// Binary packet data
const buffer = Buffer.from(data);
// Parse cc1101_packet_t structure
const timestamp = buffer.readBigUInt64LE(0);
const length = buffer.readUInt8(8);
const packetData = buffer.slice(9, 9 + length);
const rssi = buffer.readInt8(8 + length + 1);
const lqiByte = buffer.readUInt8(8 + length + 2);
const lqi = lqiByte & 0x7F;
const crcOk = (lqiByte & 0x80) !== 0;
console.log('Packet:', {
timestamp: timestamp.toString(),
length: length,
data: packetData.toString('hex'),
rssi: rssi,
lqi: lqi,
crc_ok: crcOk
});
}
});
this.ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
this.ws.on('close', () => {
console.log('Logger disconnected');
});
}
}
const client = new PacketLoggerClient('your_jwt_token');
client.connect();