mw75
Async Rust library and CLI tools for streaming EEG data from Master & Dynamic MW75 Neuro headphones over Bluetooth.
Overview
The MW75 Neuro headphones contain a 12-channel EEG sensor array developed by Arctop. Data is streamed at 500 Hz over Bluetooth Classic (RFCOMM channel 25) after an initial BLE activation handshake.
This crate provides:
- BLE activation — scan, connect, enable EEG & raw mode, query battery
- RFCOMM transport — platform-native Bluetooth Classic data streaming
- Packet parsing — sync-byte alignment, checksum validation, 12-channel EEG decoding
- Simulation — synthetic 500 Hz EEG packets (random or deterministic sinusoidal)
- TUI — real-time 4-channel waveform viewer with smooth overlay and auto-scale
- Audio — automatic A2DP pairing, sink routing, and file playback (Linux)
Platform support
| Capability | Linux | macOS | Windows |
|---|---|---|---|
| BLE activation | ✓ (BlueZ) | ✓ (CoreBluetooth) | ✓ (WinRT) |
| RFCOMM streaming | ✓ (bluer) | ✓ (IOBluetooth) | ✓ (WinRT) |
| A2DP audio | ✓ (bluer + pactl) | — | — |
| Simulation | ✓ | ✓ | ✓ |
| TUI | ✓ | ✓ | ✓ |
Pairing
Before using this software, pair the MW75 Neuro headphones with your computer:
- Enter pairing mode — Press and hold the power button for ~4 seconds until you hear the pairing tone
- Pair via OS Bluetooth settings — Open your system's Bluetooth settings and pair the MW75 as you would normal headphones
- Verify connection — The headphones should appear as a paired audio device
Once paired, this library uses:
- BLE (GATT) — for control commands (enable EEG, query battery, etc.)
- Bluetooth Classic (RFCOMM) — for streaming EEG data at 500 Hz
Note: The headphones must be paired at the OS level first. BLE activation commands only work after the device is paired and connected as a standard Bluetooth audio device.
Quick start
Library
[]
= { = "0.0.1", = ["rfcomm"] }
use *;
use Arc;
async
CLI
# Headless — print EEG events to stdout
# TUI — real-time waveform viewer (hardware)
# TUI — simulated data (no hardware needed)
# Audio — play music through MW75 headphones (Linux)
Cargo features
| Feature | Default | Description |
|---|---|---|
tui |
✓ | Terminal UI binary (mw75-tui) with ratatui + crossterm |
rfcomm |
RFCOMM data transport (Linux: BlueZ, macOS: IOBluetooth, Windows: WinRT) | |
audio |
Bluetooth A2DP audio + rodio playback (Linux only) |
# Build only the library (no extras)
# Build everything
Architecture
┌──────────────────────────────────────────────────────────────┐
│ BLE Activation (btleplug) │
│ scan → connect → enable EEG → enable raw mode → battery │
└──────────────────┬───────────────────────────────────────────┘
│ disconnect BLE
▼
┌──────────────────────────────────────────────────────────────┐
│ RFCOMM Transport (rfcomm feature) │
│ Linux: bluer::rfcomm::Stream │
│ macOS: IOBluetoothDevice.openRFCOMMChannelSync │
│ Windows: StreamSocket + RfcommDeviceService │
│ │
│ async read loop → Mw75Handle::feed_data() │
└──────────────────┬───────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ PacketProcessor │
│ 63-byte packet framing · sync recovery · checksum · f32 LE │
│ 12 × EEG channels scaled to µV (×0.023842) │
└──────────────────┬───────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ Mw75Event::Eeg(EegPacket) → mpsc::Receiver │
│ 500 Hz · 12 channels · REF · DRL · feature status │
└──────────────────────────────────────────────────────────────┘
Protocol
Connection flow
- BLE scan for device name containing
"MW75"(case-insensitive) - Connect to GATT service
00001100-d102-11e1-9b23-00025b00a5a5 - Subscribe to status characteristic
00001102-… - Write activation commands to command characteristic
00001101-…:ENABLE_EEG→[0x09, 0x9A, 0x03, 0x60, 0x01]ENABLE_RAW_MODE→[0x09, 0x9A, 0x03, 0x41, 0x01]BATTERY→[0x09, 0x9A, 0x03, 0x14, 0xFF]
- Verify status responses (success code
0xF1) - Disconnect BLE
- Connect RFCOMM channel 25
- Read 63-byte packets at 500 Hz
Packet format (63 bytes)
Offset Size Field
────── ──── ─────
0 1 Sync byte (0xAA)
1 1 Event ID (239 = EEG)
2 1 Data length (0x3C = 60)
3 1 Counter (0–255, wrapping)
4 4 REF electrode (f32 LE)
8 4 DRL electrode (f32 LE)
12 48 12 × EEG channels (f32 LE, raw ADC)
60 1 Feature status byte
61 2 Checksum (u16 LE = sum of bytes[0..61] & 0xFFFF)
Channel values: µV = raw_adc × 0.023842
Modules
mw75_client
BLE scanning, connection, and activation via btleplug.
let client = new;
// Scan for all nearby MW75 devices
let devices = client.scan_all.await?;
// Or connect to the first one found
let = client.connect.await?;
handle.start.await?; // activation sequence
handle.stop.await?; // disable sequence
handle.disconnect.await?;
Key types:
Mw75Client— scanner and connectorMw75Handle— commands,feed_data(), statsMw75Device— discovered device infoMw75ClientConfig— scan timeout, name pattern
rfcomm
Platform-native RFCOMM data transport (requires rfcomm feature).
use start_rfcomm_stream;
let handle = new;
handle.disconnect_ble.await?; // required before RFCOMM
// Spawns an async reader task — data arrives on the event channel
let task = start_rfcomm_stream.await?;
// To stop:
task.abort;
parse
Packet parsing and buffered stream processing.
use ;
// Validate a raw 63-byte packet
let = validate_checksum;
// Parse into structured EegPacket
if let Some = parse_eeg_packet
// Continuous stream processing (handles split delivery, sync recovery)
let mut proc = new;
let events = proc.process_data; // returns Vec<Mw75Event>
simulate
Synthetic packet generation for testing and development.
use ;
// Random EEG packet
let pkt = build_eeg_packet;
// Deterministic sinusoidal packet (alpha + beta + theta bands)
let pkt = build_sim_packet;
// Full 500 Hz simulator task
let = channel;
let sim = spawn_simulator; // true = deterministic
types
All event and data types.
EegPacket— 12-channel EEG sample with timestamp, REF, DRLBatteryInfo— battery level (0–100%)ActivationStatus— EEG/raw mode confirmationChecksumStats— valid/invalid/total packet counts + error rateMw75Event—Eeg,Battery,Activated,Connected,Disconnected,RawData,OtherEvent
protocol
Wire-format constants and GATT UUIDs.
use *;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
audio
Bluetooth A2DP audio management (Linux only, requires audio feature).
use ;
let mut audio = new;
let device = audio.connect.await?; // discover → pair → A2DP → set sink
audio.play_file.await?; // rodio playback
audio.disconnect.await?; // restore previous sink
TUI
The mw75-tui binary provides a real-time EEG waveform viewer:
MW75 EEG Monitor │ ● MW75 Neuro │ Bat 85% │ 500 Hz │ ±200 µV │ 42K smp │ 0 drop
┌─ Ch1 min:-45.2 max:+52.1 rms: 28.3 µV [SMOOTH] ──────────────────────────────┐
│ ⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀│
│ ⠀⠀⠁⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀│
├─ Ch2 ... ┤
├─ Ch3 ... ┤
├─ Ch4 ... ┤
└──────────────────────────────────────────────────────────────────────────────────┘
[+/-]Scale [a]Auto [v]Smooth [p]Pause [r]Resume [c]Clear [q]Quit
Keys:
| Key | Action |
|---|---|
+ / = |
Zoom out (increase µV scale) |
- |
Zoom in (decrease µV scale) |
a |
Auto-scale Y axis to peak amplitude |
v |
Toggle smooth overlay (moving average) |
p / r |
Pause / Resume streaming |
c |
Clear waveform buffers |
q / Esc |
Quit |
Testing
# Run all tests (85 unit + 19 doc-tests)
# Run tests without hardware-dependent features
Project structure
mw75/
├── Cargo.toml
├── README.md
├── src/
│ ├── lib.rs # Module declarations, prelude, crate docs
│ ├── protocol.rs # GATT UUIDs, BLE commands, wire-format constants
│ ├── types.rs # EegPacket, Mw75Event, BatteryInfo, ChecksumStats
│ ├── parse.rs # Checksum validation, packet parsing, PacketProcessor
│ ├── mw75_client.rs # BLE scanning, connection, activation (btleplug)
│ ├── rfcomm.rs # RFCOMM transport: Linux/macOS/Windows (rfcomm feature)
│ ├── simulate.rs # Synthetic packet generator + 500 Hz simulator task
│ ├── audio.rs # A2DP audio: BlueZ + pactl + rodio (audio feature)
│ ├── main.rs # Headless CLI binary
│ └── bin/
│ ├── tui.rs # Real-time EEG waveform TUI (tui feature)
│ └── audio.rs # Audio playback CLI binary (audio feature)
└── audio.mp3 # Sample audio file for testing
Credits
Based on the Python mw75-streamer by Arctop / Eitan Kay.
Architecture follows muse-rs by Eugene Hauptmann.