Async Rust library and CLI tools for streaming EEG data from Master & Dynamic MW75 Neuro headphones over Bluetooth.
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)
| Capability | Linux | macOS | Windows |
|---|---|---|---|
| BLE activation | β (BlueZ) | β (CoreBluetooth) | β (WinRT) |
| RFCOMM streaming | β (bluer) | β (IOBluetooth) | β (WinRT) |
| A2DP audio | β (bluer + pactl) | β | β |
| Simulation | β | β | β |
| TUI | β | β | β |
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.
[dependencies]
mw75 = { version = "0.0.1", features = ["rfcomm"] }use mw75::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Mw75Client::new(Mw75ClientConfig::default());
let (mut rx, handle) = client.connect().await?;
handle.start().await?;
// Disconnect BLE, then start RFCOMM data stream
let addr = handle.peripheral_id();
handle.disconnect_ble().await?;
let handle = Arc::new(handle);
let rfcomm = start_rfcomm_stream(handle.clone(), &addr).await?;
while let Some(event) = rx.recv().await {
match event {
Mw75Event::Eeg(pkt) => {
println!("counter={} ch1={:.1} Β΅V", pkt.counter, pkt.channels[0]);
}
Mw75Event::Disconnected => break,
_ => {}
}
}
rfcomm.abort();
Ok(())
}# Headless β print EEG events to stdout
cargo run --features rfcomm
# TUI β real-time waveform viewer (hardware)
cargo run --bin mw75-tui --features rfcomm
# TUI β simulated data (no hardware needed)
cargo run --bin mw75-tui -- --simulate
# Audio β play music through MW75 headphones (Linux)
cargo run --bin mw75-audio --features audio -- music.mp3| 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)
cargo build --no-default-features
# Build everything
cargo build --features rfcomm,audioββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- 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
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
BLE scanning, connection, and activation via btleplug.
let client = Mw75Client::new(Mw75ClientConfig::default());
// Scan for all nearby MW75 devices
let devices = client.scan_all().await?;
// Or connect to the first one found
let (rx, handle) = 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
Platform-native RFCOMM data transport (requires rfcomm feature).
use mw75::rfcomm::start_rfcomm_stream;
let handle = Arc::new(handle);
handle.disconnect_ble().await?; // required before RFCOMM
// Spawns an async reader task β data arrives on the event channel
let task = start_rfcomm_stream(handle.clone(), "AA:BB:CC:DD:EE:FF").await?;
// To stop:
task.abort();Packet parsing and buffered stream processing.
use mw75::parse::{PacketProcessor, validate_checksum, parse_eeg_packet};
// Validate a raw 63-byte packet
let (valid, calc, recv) = validate_checksum(&raw_bytes);
// Parse into structured EegPacket
if let Some(pkt) = parse_eeg_packet(&raw_bytes) {
println!("{} channels, counter={}", pkt.channels.len(), pkt.counter);
}
// Continuous stream processing (handles split delivery, sync recovery)
let mut proc = PacketProcessor::new(false);
let events = proc.process_data(&chunk); // returns Vec<Mw75Event>Synthetic packet generation for testing and development.
use mw75::simulate::{build_eeg_packet, build_sim_packet, spawn_simulator};
// Random EEG packet
let pkt = build_eeg_packet(counter);
// Deterministic sinusoidal packet (alpha + beta + theta bands)
let pkt = build_sim_packet(counter, time_secs);
// Full 500 Hz simulator task
let (tx, mut rx) = tokio::sync::mpsc::channel(256);
let sim = spawn_simulator(tx, true); // true = deterministicAll 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
Wire-format constants and GATT UUIDs.
use mw75::protocol::*;
assert_eq!(SYNC_BYTE, 0xAA);
assert_eq!(PACKET_SIZE, 63);
assert_eq!(EEG_EVENT_ID, 239);
assert_eq!(NUM_EEG_CHANNELS, 12);
assert_eq!(RFCOMM_CHANNEL, 25);
assert_eq!(EEG_SCALING_FACTOR, 0.023842);
assert_eq!(EEG_CHANNEL_NAMES.len(), 12);Bluetooth A2DP audio management (Linux only, requires audio feature).
use mw75::audio::{Mw75Audio, AudioConfig};
let mut audio = Mw75Audio::new(AudioConfig::default());
let device = audio.connect().await?; // discover β pair β A2DP β set sink
audio.play_file("music.mp3").await?; // rodio playback
audio.disconnect().await?; // restore previous sinkThe 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 |
# Run all tests (85 unit + 19 doc-tests)
cargo test --all-features
# Run tests without hardware-dependent features
cargo testmw75/
βββ 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
Based on the Python mw75-streamer by Arctop / Eitan Kay.
Architecture follows muse-rs by Eugene Hauptmann.