{"id":50549285,"url":"https://github.com/eugenehp/mw75-rs","last_synced_at":"2026-06-04T01:30:50.232Z","repository":{"id":344049293,"uuid":"1180225708","full_name":"eugenehp/mw75-rs","owner":"eugenehp","description":"Rust client for MW75 Neuro EEG headphones over BLE + RFCOMM using btleplug","archived":false,"fork":false,"pushed_at":"2026-03-12T21:06:56.000Z","size":1040,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-13T03:35:28.995Z","etag":null,"topics":["audio","bci","ble","eeg","exg","rfcomm"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/mw75","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eugenehp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-12T20:39:37.000Z","updated_at":"2026-03-12T22:06:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/eugenehp/mw75-rs","commit_stats":null,"previous_names":["eugenehp/mw75-rs"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/eugenehp/mw75-rs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eugenehp%2Fmw75-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eugenehp%2Fmw75-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eugenehp%2Fmw75-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eugenehp%2Fmw75-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eugenehp","download_url":"https://codeload.github.com/eugenehp/mw75-rs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eugenehp%2Fmw75-rs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33886160,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","bci","ble","eeg","exg","rfcomm"],"created_at":"2026-06-04T01:30:49.619Z","updated_at":"2026-06-04T01:30:50.224Z","avatar_url":"https://github.com/eugenehp.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mw75\n\nAsync Rust library and CLI tools for streaming EEG data from\n[Master \u0026 Dynamic MW75 Neuro](https://www.masterdynamic.com/) headphones\nover Bluetooth.\n\n[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue.svg)](LICENSE)\n\n## Overview\n\nThe MW75 Neuro headphones contain a 12-channel EEG sensor array developed by\n[Arctop](https://arctop.com). Data is streamed at **500 Hz** over Bluetooth\nClassic (RFCOMM channel 25) after an initial BLE activation handshake.\n\nThis crate provides:\n\n- **BLE activation** — scan, connect, enable EEG \u0026 raw mode, query battery\n- **RFCOMM transport** — platform-native Bluetooth Classic data streaming\n- **Packet parsing** — sync-byte alignment, checksum validation, 12-channel EEG decoding\n- **Simulation** — synthetic 500 Hz EEG packets (random or deterministic sinusoidal)\n- **TUI** — real-time 4-channel waveform viewer with smooth overlay and auto-scale\n- **Audio** — automatic A2DP pairing, sink routing, and file playback (Linux)\n\n## Platform support\n\n| Capability | Linux | macOS | Windows |\n|-----------|-------|-------|---------|\n| BLE activation | ✓ (BlueZ) | ✓ (CoreBluetooth) | ✓ (WinRT) |\n| RFCOMM streaming | ✓ (bluer) | ✓ (IOBluetooth) | ✓ (WinRT) |\n| A2DP audio | ✓ (bluer + pactl) | — | — |\n| Simulation | ✓ | ✓ | ✓ |\n| TUI | ✓ | ✓ | ✓ |\n\n## Quick start\n\n### Library\n\n```toml\n[dependencies]\nmw75 = { version = \"0.0.1\", features = [\"rfcomm\"] }\n```\n\n```rust\nuse mw75::prelude::*;\nuse std::sync::Arc;\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let client = Mw75Client::new(Mw75ClientConfig::default());\n    let (mut rx, handle) = client.connect().await?;\n    handle.start().await?;\n\n    // Disconnect BLE, then start RFCOMM data stream\n    let addr = handle.peripheral_id();\n    handle.disconnect_ble().await?;\n    let handle = Arc::new(handle);\n    let rfcomm = start_rfcomm_stream(handle.clone(), \u0026addr).await?;\n\n    while let Some(event) = rx.recv().await {\n        match event {\n            Mw75Event::Eeg(pkt) =\u003e {\n                println!(\"counter={} ch1={:.1} µV\", pkt.counter, pkt.channels[0]);\n            }\n            Mw75Event::Disconnected =\u003e break,\n            _ =\u003e {}\n        }\n    }\n\n    rfcomm.abort();\n    Ok(())\n}\n```\n\n### CLI\n\n```bash\n# Headless — print EEG events to stdout\ncargo run --features rfcomm\n\n# TUI — real-time waveform viewer (hardware)\ncargo run --bin mw75-tui --features rfcomm\n\n# TUI — simulated data (no hardware needed)\ncargo run --bin mw75-tui -- --simulate\n\n# Audio — play music through MW75 headphones (Linux)\ncargo run --bin mw75-audio --features audio -- music.mp3\n```\n\n## Cargo features\n\n| Feature | Default | Description |\n|---------|---------|-------------|\n| `tui` | ✓ | Terminal UI binary (`mw75-tui`) with ratatui + crossterm |\n| `rfcomm` | | RFCOMM data transport (Linux: BlueZ, macOS: IOBluetooth, Windows: WinRT) |\n| `audio` | | Bluetooth A2DP audio + rodio playback (Linux only) |\n\n```bash\n# Build only the library (no extras)\ncargo build --no-default-features\n\n# Build everything\ncargo build --features rfcomm,audio\n```\n\n## Architecture\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│  BLE Activation (btleplug)                                   │\n│  scan → connect → enable EEG → enable raw mode → battery    │\n└──────────────────┬───────────────────────────────────────────┘\n                   │ disconnect BLE\n                   ▼\n┌──────────────────────────────────────────────────────────────┐\n│  RFCOMM Transport (rfcomm feature)                           │\n│  Linux: bluer::rfcomm::Stream                                │\n│  macOS: IOBluetoothDevice.openRFCOMMChannelSync              │\n│  Windows: StreamSocket + RfcommDeviceService                 │\n│                                                              │\n│  async read loop → Mw75Handle::feed_data()                   │\n└──────────────────┬───────────────────────────────────────────┘\n                   ▼\n┌──────────────────────────────────────────────────────────────┐\n│  PacketProcessor                                             │\n│  63-byte packet framing · sync recovery · checksum · f32 LE │\n│  12 × EEG channels scaled to µV (×0.023842)                 │\n└──────────────────┬───────────────────────────────────────────┘\n                   ▼\n┌──────────────────────────────────────────────────────────────┐\n│  Mw75Event::Eeg(EegPacket)  →  mpsc::Receiver               │\n│  500 Hz · 12 channels · REF · DRL · feature status           │\n└──────────────────────────────────────────────────────────────┘\n```\n\n## Protocol\n\n### Connection flow\n\n1. BLE scan for device name containing `\"MW75\"` (case-insensitive)\n2. Connect to GATT service `00001100-d102-11e1-9b23-00025b00a5a5`\n3. Subscribe to status characteristic `00001102-…`\n4. Write activation commands to command characteristic `00001101-…`:\n   - `ENABLE_EEG` → `[0x09, 0x9A, 0x03, 0x60, 0x01]`\n   - `ENABLE_RAW_MODE` → `[0x09, 0x9A, 0x03, 0x41, 0x01]`\n   - `BATTERY` → `[0x09, 0x9A, 0x03, 0x14, 0xFF]`\n5. Verify status responses (success code `0xF1`)\n6. Disconnect BLE\n7. Connect RFCOMM channel 25\n8. Read 63-byte packets at 500 Hz\n\n### Packet format (63 bytes)\n\n```\nOffset  Size  Field\n──────  ────  ─────\n  0       1   Sync byte (0xAA)\n  1       1   Event ID (239 = EEG)\n  2       1   Data length (0x3C = 60)\n  3       1   Counter (0–255, wrapping)\n  4       4   REF electrode (f32 LE)\n  8       4   DRL electrode (f32 LE)\n 12      48   12 × EEG channels (f32 LE, raw ADC)\n 60       1   Feature status byte\n 61       2   Checksum (u16 LE = sum of bytes[0..61] \u0026 0xFFFF)\n```\n\nChannel values: `µV = raw_adc × 0.023842`\n\n## Modules\n\n### `mw75_client`\n\nBLE scanning, connection, and activation via btleplug.\n\n```rust\nlet client = Mw75Client::new(Mw75ClientConfig::default());\n\n// Scan for all nearby MW75 devices\nlet devices = client.scan_all().await?;\n\n// Or connect to the first one found\nlet (rx, handle) = client.connect().await?;\nhandle.start().await?;    // activation sequence\nhandle.stop().await?;     // disable sequence\nhandle.disconnect().await?;\n```\n\nKey types:\n- `Mw75Client` — scanner and connector\n- `Mw75Handle` — commands, `feed_data()`, stats\n- `Mw75Device` — discovered device info\n- `Mw75ClientConfig` — scan timeout, name pattern\n\n### `rfcomm`\n\nPlatform-native RFCOMM data transport (requires `rfcomm` feature).\n\n```rust\nuse mw75::rfcomm::start_rfcomm_stream;\n\nlet handle = Arc::new(handle);\nhandle.disconnect_ble().await?;  // required before RFCOMM\n\n// Spawns an async reader task — data arrives on the event channel\nlet task = start_rfcomm_stream(handle.clone(), \"AA:BB:CC:DD:EE:FF\").await?;\n\n// To stop:\ntask.abort();\n```\n\n### `parse`\n\nPacket parsing and buffered stream processing.\n\n```rust\nuse mw75::parse::{PacketProcessor, validate_checksum, parse_eeg_packet};\n\n// Validate a raw 63-byte packet\nlet (valid, calc, recv) = validate_checksum(\u0026raw_bytes);\n\n// Parse into structured EegPacket\nif let Some(pkt) = parse_eeg_packet(\u0026raw_bytes) {\n    println!(\"{} channels, counter={}\", pkt.channels.len(), pkt.counter);\n}\n\n// Continuous stream processing (handles split delivery, sync recovery)\nlet mut proc = PacketProcessor::new(false);\nlet events = proc.process_data(\u0026chunk);  // returns Vec\u003cMw75Event\u003e\n```\n\n### `simulate`\n\nSynthetic packet generation for testing and development.\n\n```rust\nuse mw75::simulate::{build_eeg_packet, build_sim_packet, spawn_simulator};\n\n// Random EEG packet\nlet pkt = build_eeg_packet(counter);\n\n// Deterministic sinusoidal packet (alpha + beta + theta bands)\nlet pkt = build_sim_packet(counter, time_secs);\n\n// Full 500 Hz simulator task\nlet (tx, mut rx) = tokio::sync::mpsc::channel(256);\nlet sim = spawn_simulator(tx, true);  // true = deterministic\n```\n\n### `types`\n\nAll event and data types.\n\n- `EegPacket` — 12-channel EEG sample with timestamp, REF, DRL\n- `BatteryInfo` — battery level (0–100%)\n- `ActivationStatus` — EEG/raw mode confirmation\n- `ChecksumStats` — valid/invalid/total packet counts + error rate\n- `Mw75Event` — `Eeg`, `Battery`, `Activated`, `Connected`, `Disconnected`, `RawData`, `OtherEvent`\n\n### `protocol`\n\nWire-format constants and GATT UUIDs.\n\n```rust\nuse mw75::protocol::*;\n\nassert_eq!(SYNC_BYTE, 0xAA);\nassert_eq!(PACKET_SIZE, 63);\nassert_eq!(EEG_EVENT_ID, 239);\nassert_eq!(NUM_EEG_CHANNELS, 12);\nassert_eq!(RFCOMM_CHANNEL, 25);\nassert_eq!(EEG_SCALING_FACTOR, 0.023842);\nassert_eq!(EEG_CHANNEL_NAMES.len(), 12);\n```\n\n### `audio`\n\nBluetooth A2DP audio management (Linux only, requires `audio` feature).\n\n```rust\nuse mw75::audio::{Mw75Audio, AudioConfig};\n\nlet mut audio = Mw75Audio::new(AudioConfig::default());\nlet device = audio.connect().await?;   // discover → pair → A2DP → set sink\naudio.play_file(\"music.mp3\").await?;   // rodio playback\naudio.disconnect().await?;             // restore previous sink\n```\n\n## TUI\n\nThe `mw75-tui` binary provides a real-time EEG waveform viewer:\n\n```\n MW75 EEG Monitor │ ● MW75 Neuro │ Bat 85% │ 500 Hz │ ±200 µV │ 42K smp │ 0 drop\n┌─ Ch1  min:-45.2  max:+52.1  rms: 28.3 µV [SMOOTH] ──────────────────────────────┐\n│  ⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀⠀⠀⠀⣀⠀⠀⠀⢀⠀⠀⠀⡀│\n│  ⠀⠀⠁⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀⠀⠁⠀⠀│\n├─ Ch2 ...                                                                         ┤\n├─ Ch3 ...                                                                         ┤\n├─ Ch4 ...                                                                         ┤\n└──────────────────────────────────────────────────────────────────────────────────┘\n [+/-]Scale  [a]Auto  [v]Smooth  [p]Pause  [r]Resume  [c]Clear  [q]Quit\n```\n\n**Keys:**\n\n| Key | Action |\n|-----|--------|\n| `+` / `=` | Zoom out (increase µV scale) |\n| `-` | Zoom in (decrease µV scale) |\n| `a` | Auto-scale Y axis to peak amplitude |\n| `v` | Toggle smooth overlay (moving average) |\n| `p` / `r` | Pause / Resume streaming |\n| `c` | Clear waveform buffers |\n| `q` / `Esc` | Quit |\n\n## Testing\n\n```bash\n# Run all tests (85 unit + 19 doc-tests)\ncargo test --all-features\n\n# Run tests without hardware-dependent features\ncargo test\n```\n\n## Project structure\n\n```\nmw75/\n├── Cargo.toml\n├── README.md\n├── src/\n│   ├── lib.rs              # Module declarations, prelude, crate docs\n│   ├── protocol.rs         # GATT UUIDs, BLE commands, wire-format constants\n│   ├── types.rs            # EegPacket, Mw75Event, BatteryInfo, ChecksumStats\n│   ├── parse.rs            # Checksum validation, packet parsing, PacketProcessor\n│   ├── mw75_client.rs      # BLE scanning, connection, activation (btleplug)\n│   ├── rfcomm.rs           # RFCOMM transport: Linux/macOS/Windows (rfcomm feature)\n│   ├── simulate.rs         # Synthetic packet generator + 500 Hz simulator task\n│   ├── audio.rs            # A2DP audio: BlueZ + pactl + rodio (audio feature)\n│   ├── main.rs             # Headless CLI binary\n│   └── bin/\n│       ├── tui.rs          # Real-time EEG waveform TUI (tui feature)\n│       └── audio.rs        # Audio playback CLI binary (audio feature)\n└── audio.mp3               # Sample audio file for testing\n```\n\n## Credits\n\nBased on the Python [mw75-streamer](https://github.com/arctop/mw75-streamer) by Arctop / Eitan Kay.\n\nArchitecture follows [muse-rs](https://github.com/eugenehp/muse-rs) by Eugene Hauptmann.\n\n## License\n\n[GPL-3.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feugenehp%2Fmw75-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feugenehp%2Fmw75-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feugenehp%2Fmw75-rs/lists"}