WebSocket Server (Remote Control)
The MW75 WebSocket Server provides a remote control interface for third-party applications to manage MW75 device connections and receive real-time EEG data over WebSocket.
Overview
Unlike the WebSocket client mode (--ws), which connects to an existing WebSocket server, the server mode creates a WebSocket server that external applications can connect to for remote control.
Use Cases:
Web applications controlling MW75 devices
Mobile apps requiring EEG data
Distributed systems with centralized device management
Remote monitoring and control scenarios
Starting the Server
Basic Usage
# Start on default port 8080
uv run -m mw75_streamer.server
# Start on custom port
uv run -m mw75_streamer.server --port 9000
# Listen on all interfaces (for remote access)
uv run -m mw75_streamer.server --host 0.0.0.0 --port 8080
# Enable verbose logging
uv run -m mw75_streamer.server --port 8080 --verbose
Connection Behavior
Server starts idle (no device connection)
Multiple clients can connect simultaneously
Only one client can control device connection at a time
First client to send
connectcommand gains controlControl is released when controlling client disconnects
All clients receive EEG data, status updates, and logs
Clients can broadcast messages to each other
Device stays connected when controlling client disconnects (if other clients remain)
Message Protocol
All messages use JSON format with this structure:
{
"id": "unique-message-id",
"type": "message-type",
"data": { }
}
Message Fields:
id- Unique identifier (UUID) for the messagetype- Message type (see below)data- Type-specific data payload
Client Commands
Commands sent from client to server:
Connect Command
Request connection to MW75 device:
{
"id": "msg-1",
"type": "connect",
"data": {
"auto_reconnect": true,
"log_level": "ERROR"
}
}
Parameters:
auto_reconnect(boolean, default: false) - Enable automatic reconnection on disconnectlog_level(string, default: “ERROR”) - Log level: “DEBUG”, “INFO”, “WARNING”, or “ERROR”
Response: Server sends command_ack followed by status updates
Disconnect Command
Request disconnection from MW75 device:
{
"id": "msg-2",
"type": "disconnect",
"data": {}
}
Response: Server sends command_ack and disconnects from device
Status Query
Query current connection status:
{
"id": "msg-3",
"type": "status",
"data": {}
}
Response: Server sends current status information:
{
"id": "msg-3",
"type": "status",
"data": {
"device_state": "connected",
"auto_reconnect": true,
"log_level": "ERROR",
"battery_level": 85,
"has_control": true,
"total_clients": 3
}
}
Status Fields:
device_state- Current connection state (see Connection States below)auto_reconnect- Whether auto-reconnect is enabledlog_level- Current log level filterbattery_level- Device battery percentage (0-100), ornullif not availablehas_control- Whether this client currently has device control (boolean)total_clients- Number of connected clients (integer)
Ping/Pong
Keepalive mechanism (optional, server sends automatic heartbeats):
{
"id": "msg-4",
"type": "ping",
"data": {}
}
Response: Server sends pong message
Broadcast Command
Send a message to all other connected clients:
{
"id": "msg-5",
"type": "broadcast",
"data": {
"custom_field": "your data",
"another_field": 123
}
}
Parameters:
data- Any JSON object to broadcast to other clients
Response: Server sends command_ack to sender, and forwards the broadcast to all other clients with sender information
Received by other clients:
{
"id": "msg-5",
"type": "broadcast",
"data": {
"from": "192.168.1.100:54321",
"data": {
"custom_field": "your data",
"another_field": 123
},
"timestamp": 1234567890.123
}
}
Use Cases:
Client-to-client communication
Coordinating multiple viewers
Sharing analysis results between clients
Custom application-specific messaging
Server Messages
Messages sent from server to client:
Command Acknowledgement
Confirms command receipt:
{
"id": "msg-1",
"type": "command_ack",
"data": {
"command": "connect",
"message": "Connect command received, initiating device connection",
"auto_reconnect": true,
"log_level": "INFO"
}
}
Status Updates
Connection state changes:
{
"id": "uuid",
"type": "status",
"data": {
"state": "connected",
"message": "Successfully connected to MW75 device",
"timestamp": 1234567890.123,
"battery_level": 85
}
}
Status Update Fields:
state- Current connection state (see Connection States below)message- Human-readable status messagetimestamp- Unix timestamp of status updatebattery_level- Device battery percentage (0-100), ornullif not available
Connection States:
idle- No device connectionconnecting- Attempting device connectionconnected- Device connected and streamingdisconnecting- Disconnecting from devicedisconnected- Device disconnectedreconnecting- Auto-reconnect in progresserror- Error state
EEG Data
Real-time EEG packets (sent at ~500Hz when connected):
{
"id": "uuid",
"type": "eeg_data",
"data": {
"timestamp": 1234567890.123,
"event_id": 239,
"counter": 42,
"ref": 123.45,
"drl": 67.89,
"channels": {
"ch1": 10.5,
"ch2": 12.3,
"ch3": -5.2,
"ch4": 8.7,
"ch5": -2.1,
"ch6": 15.3,
"ch7": 4.8,
"ch8": -7.6,
"ch9": 11.2,
"ch10": 3.4,
"ch11": -9.8,
"ch12": 6.1
},
"feature_status": 0
}
}
Fields:
timestamp- Unix timestamp (seconds with microsecond precision)event_id- Always 239 for EEG datacounter- Sequence counter (0-255, wraps around)ref- Reference electrode value (µV)drl- Driven Right Leg electrode value (µV)channels- 12 EEG channels (µV), named ch1 through ch12feature_status- Device feature status flags
Log Messages
Filtered log messages (based on client-requested log level):
{
"id": "uuid",
"type": "log",
"data": {
"level": "ERROR",
"message": "Connection error: timeout",
"logger": "mw75_streamer.device",
"timestamp": 1234567890.123
}
}
Log Levels:
DEBUG- Detailed diagnostic informationINFO- General informational messagesWARNING- Warning messages (potential issues)ERROR- Error messages (failures)
Error Messages
Error notifications:
{
"id": "uuid",
"type": "error",
"data": {
"code": "CONNECTION_FAILED",
"message": "Failed to connect to device",
"timestamp": 1234567890.123
}
}
Common Error Codes:
DEVICE_CONTROL_TAKEN- Another client currently has device control (cannot connect or disconnect)INVALID_JSON- Malformed JSON receivedINVALID_MESSAGE- Message structure invalidMISSING_TYPE- Message missing ‘type’ fieldUNKNOWN_COMMAND- Unknown command typeINVALID_LOG_LEVEL- Invalid log level specifiedALREADY_CONNECTED- Device already connectedBLE_ACTIVATION_FAILED- MW75 device not found or BLE activation failedRFCOMM_CONNECTION_FAILED- RFCOMM data connection failedCONNECTION_FAILED- Device connection failedDISCONNECT_ERROR- Error during disconnectionRECONNECT_FAILED- Auto-reconnect attempt failedRECONNECT_EXHAUSTED- Max reconnection attempts reachedDEVICE_ERROR- Device-level errorMESSAGE_PROCESSING_ERROR- Error processing message
Heartbeat Messages
Automatic keepalive (sent every 30 seconds):
{
"id": "uuid",
"type": "heartbeat",
"data": {
"timestamp": 1234567890.123,
"battery_level": 85
}
}
Heartbeat Fields:
timestamp- Unix timestamp of heartbeatbattery_level- Device battery percentage (0-100), ornullif not available
These heartbeats help monitor connection health and provide periodic battery level updates. The WebSocket library automatically handles connection liveness and will close the connection if it becomes unresponsive.
Features
Auto-Reconnect
When enabled via the connect command:
Monitors device connection health
Automatically attempts reconnection on disconnect
Uses exponential backoff (1, 2, 4, 8, 16, 30 seconds max)
Maximum 10 reconnection attempts
Sends status updates during reconnection
Stops on client disconnect command
Log Level Filtering
Client can request specific log levels in the connect command:
DEBUG- All logs (very verbose)INFO- Informational and aboveWARNING- Warnings and errors onlyERROR- Errors only (default)
Logs are captured from the entire mw75_streamer package and filtered before sending to client.
Battery Monitoring
The server automatically retrieves and reports the MW75 device battery level:
Battery level is obtained during device connection via BLE
Reported as percentage (0-100)
Included in all status messages
Periodically updated via heartbeat messages (every 30 seconds)
Returns
nullwhen device is not connected or battery level unavailable
Access Battery Level:
Query Status: Send
statuscommand to get current battery levelStatus Updates: Battery level included in all state change notifications
Heartbeats: Periodic updates every 30 seconds while connected
Multi-Client Support
Multiple clients can connect simultaneously with the following behavior:
Connection:
Any number of clients can connect to the WebSocket server
All clients receive EEG data, status updates, and logs
Device Control:
Only one client can control the device (connect/disconnect commands)
First client to send
connectgains controlOther clients attempting
connectordisconnectreceiveDEVICE_CONTROL_TAKENerrorControl is automatically released when controlling client disconnects
Any remaining client can then take control
Broadcasting:
Clients can send broadcast messages to communicate with each other
Broadcasts are forwarded to all clients except the sender
Sender information is included in forwarded broadcasts
Device Persistence:
Device connection persists when controlling client disconnects (if other clients remain)
Device only disconnects when explicitly requested or when all clients disconnect
Connection Lifecycle
Single Client:
Client connects to WebSocket server
Server sends welcome status message
Client sends
connectcommand (gains control)Server acknowledges and begins device connection
Device connects, EEG data flows to client
Client sends
disconnector closes WebSocketServer disconnects device and cleans up
Multiple Clients:
Client A connects, sends
connectcommand (gains control)Device connects, EEG data flows to Client A
Client B connects while device is connected
Client B receives current device status and EEG data
Client A disconnects (control released, device stays connected)
Client B can now send
connectordisconnectcommandsWhen last client disconnects, device disconnects
Python Client Example
Complete Example
import asyncio
import json
import uuid
import websockets
async def mw75_client():
uri = "ws://localhost:8080"
async with websockets.connect(uri) as ws:
# Send connect command
await ws.send(json.dumps({
"id": str(uuid.uuid4()),
"type": "connect",
"data": {
"auto_reconnect": True,
"log_level": "INFO"
}
}))
# Receive and process messages
async for message in ws:
data = json.loads(message)
msg_type = data.get("type")
if msg_type == "eeg_data":
channels = data["data"]["channels"]
print(f"Ch1: {channels['ch1']:.1f} µV")
elif msg_type == "status":
state = data["data"]["state"]
battery = data["data"].get("battery_level")
battery_str = f" (Battery: {battery}%)" if battery else ""
print(f"Status: {state}{battery_str}")
elif msg_type == "heartbeat":
battery = data["data"].get("battery_level")
if battery:
print(f"Heartbeat - Battery: {battery}%")
elif msg_type == "error":
code = data["data"]["code"]
msg = data["data"]["message"]
print(f"Error [{code}]: {msg}")
elif msg_type == "log":
level = data["data"]["level"]
msg = data["data"]["message"]
print(f"[{level}] {msg}")
asyncio.run(mw75_client())
See examples/websocket_server_client.py for a complete working example.
JavaScript Client Example
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
// Connect to device
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type: 'connect',
data: {
auto_reconnect: true,
log_level: 'ERROR'
}
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'eeg_data':
const ch1 = data.data.channels.ch1;
console.log(`Ch1: ${ch1.toFixed(1)} µV`);
break;
case 'status':
const battery = data.data.battery_level;
const batteryStr = battery ? ` (Battery: ${battery}%)` : '';
console.log(`Status: ${data.data.state}${batteryStr}`);
break;
case 'heartbeat':
if (data.data.battery_level) {
console.log(`Battery: ${data.data.battery_level}%`);
}
break;
case 'error':
console.error(`Error: ${data.data.message}`);
break;
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
Testing
Integration Test
Test with actual MW75 device:
# Terminal 1: Start server
uv run -m mw75_streamer.server --port 8080 --verbose
# Terminal 2: Run example client
python examples/websocket_server_client.py
The example client will connect, start device streaming, receive EEG packets, and disconnect.
API Reference
Server Class
WebSocket Server for Remote MW75 Control
Provides a WebSocket server that allows third-party applications to: - Connect/disconnect to MW75 device remotely - Receive real-time EEG data - Get status updates and logs - Configure auto-reconnect behavior
- class mw75_streamer.server.ws_server.DeviceState(value)[source]
Bases:
EnumMW75 device connection states
- IDLE = 'idle'
- CONNECTING = 'connecting'
- CONNECTED = 'connected'
- DISCONNECTING = 'disconnecting'
- DISCONNECTED = 'disconnected'
- ERROR = 'error'
Comparison: Client vs Server Mode
The package offers two WebSocket modes:
- Client Mode (
--ws) Streamer connects to existing WebSocket server
Sends EEG data to server
Simple one-way data streaming
Use for: sending data to existing infrastructure
- Server Mode (
-m mw75_streamer.server) Streamer is the WebSocket server
Clients connect to control device and receive data
Two-way command/response protocol
Use for: remote control applications
Both modes can coexist - you can run the server mode and also use --ws to forward data elsewhere simultaneously.
Troubleshooting
Connection Refused
Problem: Client cannot connect to server
Solutions:
Ensure server is running:
uv run -m mw75_streamer.server --port 8080
Check port availability:
lsof -i :8080
Verify firewall settings allow connections
Try localhost first:
ws://localhost:8080
Device Control Taken
Problem: “DEVICE_CONTROL_TAKEN” error when trying to connect or disconnect
Solutions:
Another client currently has device control
Wait for controlling client to disconnect
Check status with
statuscommand to seehas_controlfieldOnly the client with control can send
connectordisconnectcommands
Device Won’t Connect
Problem: Status stays in “connecting” state
Solutions:
Ensure MW75 headphones are paired and powered on
Check Bluetooth connection in System Preferences
Run server with
--verboseto see detailed logsSee Troubleshooting Guide for device-specific issues
No EEG Data
Problem: Connected but no eeg_data messages
Solutions:
Check device state with status command
Ensure device successfully connected (status = “connected”)
Monitor for error messages
Check log messages for connection issues
Security Considerations
The WebSocket server provides no built-in authentication or encryption:
Recommendations:
Bind to
localhostonly for local applicationsUse a reverse proxy (nginx, Apache) for remote access
Implement TLS/SSL at proxy level for encryption
Add authentication at application level if needed
Run on isolated network for sensitive applications
Never expose directly to public internet
For production deployments, consider wrapping the server with proper security infrastructure.
Next Steps
See Quick Start Guide for basic MW75 usage
Read Protocol Documentation for technical details
Check Troubleshooting Guide for common issues
Review
examples/websocket_server_client.pyfor complete client implementation