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 connect command gains control

  • Control 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 message

  • type - 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 disconnect

  • log_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 enabled

  • log_level - Current log level filter

  • battery_level - Device battery percentage (0-100), or null if not available

  • has_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 message

  • timestamp - Unix timestamp of status update

  • battery_level - Device battery percentage (0-100), or null if not available

Connection States:

  • idle - No device connection

  • connecting - Attempting device connection

  • connected - Device connected and streaming

  • disconnecting - Disconnecting from device

  • disconnected - Device disconnected

  • reconnecting - Auto-reconnect in progress

  • error - 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 data

  • counter - 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 ch12

  • feature_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 information

  • INFO - General informational messages

  • WARNING - 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 received

  • INVALID_MESSAGE - Message structure invalid

  • MISSING_TYPE - Message missing ‘type’ field

  • UNKNOWN_COMMAND - Unknown command type

  • INVALID_LOG_LEVEL - Invalid log level specified

  • ALREADY_CONNECTED - Device already connected

  • BLE_ACTIVATION_FAILED - MW75 device not found or BLE activation failed

  • RFCOMM_CONNECTION_FAILED - RFCOMM data connection failed

  • CONNECTION_FAILED - Device connection failed

  • DISCONNECT_ERROR - Error during disconnection

  • RECONNECT_FAILED - Auto-reconnect attempt failed

  • RECONNECT_EXHAUSTED - Max reconnection attempts reached

  • DEVICE_ERROR - Device-level error

  • MESSAGE_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 heartbeat

  • battery_level - Device battery percentage (0-100), or null if 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 above

  • WARNING - Warnings and errors only

  • ERROR - 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 null when device is not connected or battery level unavailable

Access Battery Level:

  1. Query Status: Send status command to get current battery level

  2. Status Updates: Battery level included in all state change notifications

  3. 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 connect gains control

  • Other clients attempting connect or disconnect receive DEVICE_CONTROL_TAKEN error

  • Control 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:

  1. Client connects to WebSocket server

  2. Server sends welcome status message

  3. Client sends connect command (gains control)

  4. Server acknowledges and begins device connection

  5. Device connects, EEG data flows to client

  6. Client sends disconnect or closes WebSocket

  7. Server disconnects device and cleans up

Multiple Clients:

  1. Client A connects, sends connect command (gains control)

  2. Device connects, EEG data flows to Client A

  3. Client B connects while device is connected

  4. Client B receives current device status and EEG data

  5. Client A disconnects (control released, device stays connected)

  6. Client B can now send connect or disconnect commands

  7. When 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: Enum

MW75 device connection states

IDLE = 'idle'
CONNECTING = 'connecting'
CONNECTED = 'connected'
DISCONNECTING = 'disconnecting'
DISCONNECTED = 'disconnected'
ERROR = 'error'
class mw75_streamer.server.ws_server.WebSocketLogHandler(server)[source]

Bases: Handler

Custom logging handler that sends logs to all WebSocket clients

emit(record)[source]

Send log record to all WebSocket clients

Return type:

None

class mw75_streamer.server.ws_server.MW75WebSocketServer(host='localhost', port=8080, use_mock=False)[source]

Bases: object

WebSocket server for remote MW75 device control

async start()[source]

Start the WebSocket server

Return type:

None

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:

  1. Ensure server is running:

    uv run -m mw75_streamer.server --port 8080
    
  2. Check port availability:

    lsof -i :8080
    
  3. Verify firewall settings allow connections

  4. Try localhost first: ws://localhost:8080

Device Control Taken

Problem: “DEVICE_CONTROL_TAKEN” error when trying to connect or disconnect

Solutions:

  1. Another client currently has device control

  2. Wait for controlling client to disconnect

  3. Check status with status command to see has_control field

  4. Only the client with control can send connect or disconnect commands

Device Won’t Connect

Problem: Status stays in “connecting” state

Solutions:

  1. Ensure MW75 headphones are paired and powered on

  2. Check Bluetooth connection in System Preferences

  3. Run server with --verbose to see detailed logs

  4. See Troubleshooting Guide for device-specific issues

No EEG Data

Problem: Connected but no eeg_data messages

Solutions:

  1. Check device state with status command

  2. Ensure device successfully connected (status = “connected”)

  3. Monitor for error messages

  4. Check log messages for connection issues

Security Considerations

The WebSocket server provides no built-in authentication or encryption:

Recommendations:

  • Bind to localhost only for local applications

  • Use 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