use std::collections::BTreeSet;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, Result};
use btleplug::api::{
Central, CentralEvent, Characteristic, Manager as _, Peripheral as _, ScanFilter, WriteType,
};
use btleplug::platform::{Adapter, Manager, Peripheral};
use futures::StreamExt;
use log::{debug, info, warn};
use tokio::sync::mpsc;
use uuid::Uuid;
use crate::parse::{
decode_eeg_samples, parse_accelerometer, parse_athena_notification, parse_gyroscope,
parse_ppg_reading, parse_telemetry, ControlAccumulator,
};
use crate::protocol::{
decode_response, encode_command, ACCELEROMETER_CHARACTERISTIC, ATHENA_SENSOR_CHARACTERISTIC,
CONTROL_CHARACTERISTIC, EEG_CHARACTERISTICS, EEG_FREQUENCY, EEG_SAMPLES_PER_READING,
GYROSCOPE_CHARACTERISTIC, PPG_CHARACTERISTICS, PPG_FREQUENCY, PPG_SAMPLES_PER_READING,
TELEMETRY_CHARACTERISTIC,
};
use crate::types::{ControlResponse, EegReading, MuseEvent};
fn now_ms() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is before Unix epoch")
.as_secs_f64()
* 1000.0
}
struct TimestampTracker {
last_index: Option<u16>,
last_timestamp: Option<f64>,
}
impl TimestampTracker {
fn new() -> Self {
Self {
last_index: None,
last_timestamp: None,
}
}
fn get(&mut self, event_index: u16, samples_per_reading: usize, frequency: f64) -> f64 {
let reading_delta = 1000.0 * (1.0 / frequency) * samples_per_reading as f64;
if self.last_index.is_none() || self.last_timestamp.is_none() {
self.last_index = Some(event_index);
self.last_timestamp = Some(now_ms() - reading_delta);
}
let mut idx = event_index as i32;
let last = self.last_index.unwrap() as i32;
while last - idx > 0x1000 {
idx += 0x10000;
}
let ts = self.last_timestamp.unwrap();
if idx == last {
ts
} else if idx > last {
let new_ts = ts + reading_delta * (idx - last) as f64;
self.last_index = Some(event_index);
self.last_timestamp = Some(new_ts);
new_ts
} else {
ts - reading_delta * (last - idx) as f64
}
}
fn reset(&mut self) {
self.last_index = None;
self.last_timestamp = None;
}
}
#[derive(Clone, Debug)]
pub struct MuseDevice {
pub name: String,
pub id: String,
pub(crate) peripheral: Peripheral,
pub(crate) adapter: Adapter,
}
#[derive(Debug, Clone)]
pub struct MuseClientConfig {
pub enable_aux: bool,
pub enable_ppg: bool,
pub scan_timeout_secs: u64,
pub name_prefix: String,
}
impl Default for MuseClientConfig {
fn default() -> Self {
Self {
enable_aux: false,
enable_ppg: false,
scan_timeout_secs: 15,
name_prefix: "Muse".into(),
}
}
}
pub struct MuseClient {
config: MuseClientConfig,
}
impl MuseClient {
pub fn new(config: MuseClientConfig) -> Self {
Self { config }
}
pub async fn scan_all(&self) -> Result<Vec<MuseDevice>> {
let manager = Manager::new().await?;
let adapters = manager.adapters().await?;
let adapter = adapters
.into_iter()
.next()
.ok_or_else(|| anyhow!("No Bluetooth adapter found"))?;
#[cfg(target_os = "macos")]
{
use btleplug::api::CentralState;
let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
loop {
match adapter.adapter_state().await {
Ok(CentralState::PoweredOn) => {
info!("macOS: adapter is PoweredOn");
break;
}
Ok(state) => {
if tokio::time::Instant::now() >= deadline {
warn!("macOS: adapter still in state {state:?} after 3 s — proceeding anyway");
break;
}
debug!("macOS: adapter state = {state:?}, waiting…");
}
Err(e) => {
warn!("macOS: adapter_state() error: {e}");
break;
}
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
tokio::time::sleep(Duration::from_millis(300)).await;
}
info!(
"scan_all: scanning for {} s …",
self.config.scan_timeout_secs
);
adapter.start_scan(ScanFilter::default()).await?;
tokio::time::sleep(Duration::from_secs(self.config.scan_timeout_secs)).await;
adapter.stop_scan().await.ok();
let mut found = vec![];
for p in adapter.peripherals().await? {
if let Ok(Some(props)) = p.properties().await {
if let Some(name) = props.local_name {
if name.starts_with(&self.config.name_prefix) {
let id = p.id().to_string();
info!("scan_all: found {name} id={id}");
found.push(MuseDevice { name, id, peripheral: p, adapter: adapter.clone() });
}
}
}
}
info!("scan_all: {} device(s) found", found.len());
Ok(found)
}
pub async fn connect_to(
&self,
device: MuseDevice,
) -> Result<(mpsc::Receiver<MuseEvent>, MuseHandle)> {
self.setup_peripheral(device.peripheral, device.name, device.adapter)
.await
}
pub async fn connect(&self) -> Result<(mpsc::Receiver<MuseEvent>, MuseHandle)> {
let manager = Manager::new().await?;
let adapters = manager.adapters().await?;
let adapter = adapters
.into_iter()
.next()
.ok_or_else(|| anyhow!("No Bluetooth adapter found"))?;
#[cfg(target_os = "macos")]
{
use btleplug::api::CentralState;
let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
loop {
match adapter.adapter_state().await {
Ok(CentralState::PoweredOn) => break,
Ok(_) if tokio::time::Instant::now() >= deadline => break,
Ok(_) => {}
Err(_) => break,
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
tokio::time::sleep(Duration::from_millis(300)).await;
}
info!(
"Scanning for Muse devices (timeout: {} s) …",
self.config.scan_timeout_secs
);
adapter.start_scan(ScanFilter::default()).await?;
let peripheral = self
.find_first(&adapter, &self.config.name_prefix, self.config.scan_timeout_secs)
.await?;
adapter.stop_scan().await.ok();
let props = peripheral.properties().await?.unwrap_or_default();
let device_name = props.local_name.unwrap_or_else(|| "Unknown".into());
info!("Found device: {device_name}");
self.setup_peripheral(peripheral, device_name, adapter)
.await
}
async fn setup_peripheral(
&self,
peripheral: Peripheral,
device_name: String,
adapter: Adapter,
) -> Result<(mpsc::Receiver<MuseEvent>, MuseHandle)> {
tokio::time::timeout(Duration::from_secs(10), peripheral.connect())
.await
.map_err(|_| anyhow!("BLE connect() timed out after 10 s"))??;
#[cfg(target_os = "linux")]
tokio::time::sleep(Duration::from_millis(600)).await;
tokio::time::timeout(Duration::from_secs(15), peripheral.discover_services())
.await
.map_err(|_| anyhow!("discover_services() timed out after 15 s"))??;
info!("Connected and services discovered: {device_name}");
let chars: BTreeSet<Characteristic> = peripheral.characteristics();
let find_char = |uuid: Uuid| -> Result<Characteristic> {
chars
.iter()
.find(|c| c.uuid == uuid)
.cloned()
.ok_or_else(|| anyhow!("Characteristic {uuid} not found"))
};
let is_athena = chars.iter().any(|c| c.uuid == ATHENA_SENSOR_CHARACTERISTIC);
info!(
"{device_name}: firmware detected as {}",
if is_athena { "Athena" } else { "Classic" }
);
let control_char = find_char(CONTROL_CHARACTERISTIC)?;
peripheral.subscribe(&control_char).await?;
let (tx, rx) = mpsc::channel::<MuseEvent>(256);
let _ = tx.send(MuseEvent::Connected(device_name.clone())).await;
let disconnect_tx = tx.clone();
let peripheral_id = peripheral.id();
tokio::spawn(async move {
match adapter.events().await {
Ok(mut events) => {
while let Some(event) = events.next().await {
if let CentralEvent::DeviceDisconnected(id) = event {
if id == peripheral_id {
info!("Disconnect watcher: device {id:?} disconnected.");
let _ = disconnect_tx.send(MuseEvent::Disconnected).await;
break;
}
}
}
}
Err(e) => {
warn!("Disconnect watcher: could not subscribe to adapter events: {e}");
}
}
});
let peripheral_clone = peripheral.clone();
if is_athena {
let sensor_char = find_char(ATHENA_SENSOR_CHARACTERISTIC)?;
peripheral.subscribe(&sensor_char).await?;
tokio::spawn(async move {
let mut notifications = match peripheral_clone.notifications().await {
Ok(n) => n,
Err(e) => {
warn!("Athena: could not get notifications stream: {e}");
return;
}
};
info!("Athena: notification stream subscribed, waiting for data…");
let mut ctrl_acc = ControlAccumulator::new();
let mut notif_count: u64 = 0;
let mut sensor_count: u64 = 0;
let mut eeg_event_count: u64 = 0;
while let Some(notif) = notifications.next().await {
let data = ¬if.value;
notif_count += 1;
if notif.uuid == CONTROL_CHARACTERISTIC {
let fragment = decode_response(data);
debug!("Athena control fragment: {:?}", fragment);
if let Some(json_str) = ctrl_acc.push(&fragment) {
match serde_json::from_str::<serde_json::Value>(&json_str) {
Ok(serde_json::Value::Object(map)) => {
let _ = tx
.send(MuseEvent::Control(ControlResponse {
raw: json_str,
fields: map,
}))
.await;
}
Ok(_) => {}
Err(e) => {
warn!("Athena control JSON error: {e} | raw: {json_str}")
}
}
}
continue;
}
sensor_count += 1;
let events = parse_athena_notification(data);
let n_eeg = events.iter().filter(|e| matches!(e, MuseEvent::Eeg(_))).count();
eeg_event_count += n_eeg as u64;
if sensor_count <= 3 || sensor_count % 500 == 0 {
info!(
"Athena sensor: notif #{notif_count} sensor #{sensor_count} \
uuid={} len={} events={} eeg={} (total eeg: {eeg_event_count})",
notif.uuid,
data.len(),
events.len(),
n_eeg,
);
if sensor_count <= 3 && !data.is_empty() {
let pkt_len = data[0] as usize;
let mut tags = Vec::new();
let mut ti = 9usize; while ti < data.len() {
let t = data[ti];
tags.push(format!("0x{t:02x}@{ti}"));
let payload_start = ti + 1 + 4;
if let Some(plen) = crate::parse::athena_payload_len(t) {
if payload_start + plen <= data.len() {
ti = payload_start + plen;
} else {
break; }
} else if t == 0x88 {
ti = pkt_len.min(data.len());
} else {
ti += 1; }
}
debug!("Athena sensor tags: [{}]", tags.join(", "));
debug!(
"Athena sensor raw (first 64 bytes): {:02x?}",
&data[..data.len().min(64)]
);
}
}
for event in events {
let _ = tx.send(event).await;
}
}
info!("Athena notification stream ended – device disconnected.");
let _ = tx.send(MuseEvent::Disconnected).await;
});
} else {
let telemetry_char = find_char(TELEMETRY_CHARACTERISTIC)?;
peripheral.subscribe(&telemetry_char).await?;
let accel_char = find_char(ACCELEROMETER_CHARACTERISTIC)?;
peripheral.subscribe(&accel_char).await?;
let gyro_char = find_char(GYROSCOPE_CHARACTERISTIC)?;
peripheral.subscribe(&gyro_char).await?;
let num_eeg = if self.config.enable_aux { 5 } else { 4 };
for &eeg_uuid in &EEG_CHARACTERISTICS[..num_eeg] {
match find_char(eeg_uuid) {
Ok(c) => peripheral.subscribe(&c).await?,
Err(e) => warn!("EEG char {eeg_uuid}: {e}"),
}
}
if self.config.enable_ppg {
for &ppg_uuid in &PPG_CHARACTERISTICS {
match find_char(ppg_uuid) {
Ok(c) => peripheral.subscribe(&c).await?,
Err(e) => warn!("PPG char {ppg_uuid}: {e}"),
}
}
}
let enable_ppg = self.config.enable_ppg;
let enable_aux = self.config.enable_aux;
tokio::spawn(async move {
let mut notifications = match peripheral_clone.notifications().await {
Ok(n) => n,
Err(e) => {
warn!("Classic: could not get notifications stream: {e}");
return;
}
};
info!("Classic: notification stream subscribed, waiting for data…");
let mut notif_count: u64 = 0;
let mut eeg_ts: Vec<TimestampTracker> =
(0..5).map(|_| TimestampTracker::new()).collect();
let mut ppg_ts: Vec<TimestampTracker> =
(0..3).map(|_| TimestampTracker::new()).collect();
let mut ctrl_acc = ControlAccumulator::new();
while let Some(notif) = notifications.next().await {
let data = ¬if.value;
let uuid = notif.uuid;
notif_count += 1;
if notif_count <= 5 || notif_count % 500 == 0 {
info!(
"Classic: notif #{notif_count} uuid={uuid} len={}",
data.len()
);
}
if uuid == CONTROL_CHARACTERISTIC {
let fragment = decode_response(data);
debug!("Control fragment: {:?}", fragment);
if let Some(json_str) = ctrl_acc.push(&fragment) {
match serde_json::from_str::<serde_json::Value>(&json_str) {
Ok(serde_json::Value::Object(map)) => {
let _ = tx
.send(MuseEvent::Control(ControlResponse {
raw: json_str,
fields: map,
}))
.await;
}
Ok(_) => {}
Err(e) => {
warn!("Control JSON parse error: {e} | raw: {json_str}")
}
}
}
continue;
}
if uuid == TELEMETRY_CHARACTERISTIC {
if let Some(t) = parse_telemetry(data) {
let _ = tx.send(MuseEvent::Telemetry(t)).await;
}
continue;
}
if uuid == ACCELEROMETER_CHARACTERISTIC {
if let Some(a) = parse_accelerometer(data) {
let _ = tx.send(MuseEvent::Accelerometer(a)).await;
}
continue;
}
if uuid == GYROSCOPE_CHARACTERISTIC {
if let Some(g) = parse_gyroscope(data) {
let _ = tx.send(MuseEvent::Gyroscope(g)).await;
}
continue;
}
let num_eeg_chars = if enable_aux { 5 } else { 4 };
if let Some(electrode) = EEG_CHARACTERISTICS[..num_eeg_chars]
.iter()
.position(|&u| u == uuid)
{
if data.len() >= 2 {
let index = u16::from_be_bytes([data[0], data[1]]);
let timestamp = eeg_ts[electrode].get(
index,
EEG_SAMPLES_PER_READING,
EEG_FREQUENCY,
);
let samples = decode_eeg_samples(&data[2..]);
let _ = tx
.send(MuseEvent::Eeg(EegReading {
index,
electrode,
timestamp,
samples,
}))
.await;
}
continue;
}
if enable_ppg {
if let Some(ppg_channel) =
PPG_CHARACTERISTICS.iter().position(|&u| u == uuid)
{
if data.len() >= 2 {
let index = u16::from_be_bytes([data[0], data[1]]);
let timestamp = ppg_ts[ppg_channel].get(
index,
PPG_SAMPLES_PER_READING,
PPG_FREQUENCY,
);
if let Some(reading) =
parse_ppg_reading(data, ppg_channel, timestamp)
{
let _ = tx.send(MuseEvent::Ppg(reading)).await;
}
}
continue;
}
}
debug!("Unknown notification from {uuid}");
}
info!("Classic notification stream ended – device disconnected.");
let _ = tx.send(MuseEvent::Disconnected).await;
for t in &mut eeg_ts {
t.reset();
}
for t in &mut ppg_ts {
t.reset();
}
});
}
let handle = MuseHandle {
peripheral,
control_char,
is_athena,
};
Ok((rx, handle))
}
async fn find_first(
&self,
adapter: &btleplug::platform::Adapter,
prefix: &str,
timeout_secs: u64,
) -> Result<Peripheral> {
use tokio::time::{sleep, timeout};
let result = timeout(Duration::from_secs(timeout_secs), async {
loop {
let peripherals = adapter.peripherals().await.unwrap_or_default();
for p in peripherals {
if let Ok(Some(props)) = p.properties().await {
if let Some(name) = &props.local_name {
if name.starts_with(prefix) {
return p;
}
}
}
}
sleep(Duration::from_millis(250)).await;
}
})
.await;
result.map_err(|_| anyhow!("Timed out scanning for a Muse device after {timeout_secs} s"))
}
}
pub struct MuseHandle {
peripheral: Peripheral,
control_char: Characteristic,
pub is_athena: bool,
}
impl MuseHandle {
pub async fn send_command(&self, cmd: &str) -> Result<()> {
let payload = encode_command(cmd);
self.peripheral
.write(&self.control_char, &payload, WriteType::WithoutResponse)
.await?;
Ok(())
}
pub async fn pause(&self) -> Result<()> {
self.send_command("h").await
}
pub async fn resume(&self) -> Result<()> {
if self.is_athena {
self.send_command("dc001").await?;
self.send_command("d").await
} else {
self.send_command("d").await
}
}
pub async fn start(&self, enable_ppg: bool, enable_aux: bool) -> Result<()> {
if self.is_athena {
let delay = |ms| tokio::time::sleep(Duration::from_millis(ms));
self.send_command("v4").await?;
delay(100).await;
self.send_command("s").await?;
delay(100).await;
self.send_command("h").await?;
delay(100).await;
self.send_command("p1045").await?;
delay(100).await;
self.send_command("dc001").await?;
delay(50).await;
self.send_command("dc001").await?;
delay(50).await;
self.send_command("d").await?;
delay(100).await;
self.send_command("L1").await?;
delay(2100).await;
Ok(())
} else {
self.pause().await?;
let preset = if enable_ppg {
"p50"
} else if enable_aux {
"p20"
} else {
"p21"
};
self.send_command("s").await?;
self.send_command(preset).await?;
self.resume().await?;
Ok(())
}
}
pub async fn request_device_info(&self) -> Result<()> {
self.send_command("v1").await
}
pub async fn is_connected(&self) -> bool {
self.peripheral.is_connected().await.unwrap_or(false)
}
pub async fn disconnect(&self) -> Result<()> {
self.peripheral.disconnect().await?;
Ok(())
}
}