Skip to content

eugenehp/mw75

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

mw75

Async Rust library and CLI tools for streaming EEG data from Master & Dynamic MW75 Neuro headphones over Bluetooth.

License: GPL-3.0

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:

  1. Enter pairing mode β€” Press and hold the power button for ~4 seconds until you hear the pairing tone
  2. Pair via OS Bluetooth settings β€” Open your system's Bluetooth settings and pair the MW75 as you would normal headphones
  3. 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

[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(())
}

CLI

# 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

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)
cargo build --no-default-features

# Build everything
cargo build --features rfcomm,audio

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

  1. BLE scan for device name containing "MW75" (case-insensitive)
  2. Connect to GATT service 00001100-d102-11e1-9b23-00025b00a5a5
  3. Subscribe to status characteristic 00001102-…
  4. 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]
  5. Verify status responses (success code 0xF1)
  6. Disconnect BLE
  7. Connect RFCOMM channel 25
  8. 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 = 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 connector
  • Mw75Handle β€” commands, feed_data(), stats
  • Mw75Device β€” discovered device info
  • Mw75ClientConfig β€” scan timeout, name pattern

rfcomm

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();

parse

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>

simulate

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 = deterministic

types

All event and data types.

  • EegPacket β€” 12-channel EEG sample with timestamp, REF, DRL
  • BatteryInfo β€” battery level (0–100%)
  • ActivationStatus β€” EEG/raw mode confirmation
  • ChecksumStats β€” valid/invalid/total packet counts + error rate
  • Mw75Event β€” Eeg, Battery, Activated, Connected, Disconnected, RawData, OtherEvent

protocol

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);

audio

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 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)
cargo test --all-features

# Run tests without hardware-dependent features
cargo test

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.

License

GPL-3.0

About

Rust client for MW75 Neuro EEG headphones over BLE + RFCOMM using btleplug

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors