Skip to main content

elata_muse_proto/
lib.rs

1//! Muse EEG Headband BLE Protocol
2//!
3//! This crate provides constants and utilities for the Muse BLE protocol,
4//! enabling both real Muse device communication and synthetic emulation.
5
6pub mod athena;
7pub mod classic;
8pub mod utils;
9
10/// Muse device variant
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MuseVariant {
13    Classic,
14    Athena,
15}
16
17/// Muse BLE service UUID
18pub const SERVICE_UUID: &str = "0000fe8d-0000-1000-8000-00805f9b34fb";
19pub const SERVICE_UUID_U128: u128 = 0x0000fe8d_0000_1000_8000_00805f9b34fb;
20
21/// Muse characteristic UUIDs
22pub mod characteristic {
23    /// Command characteristic - for sending control commands
24    pub const COMMAND: &str = "273e0001-4c4d-454d-96be-f03bac821358";
25    pub const COMMAND_U128: u128 = 0x273e0001_4c4d_454d_96be_f03bac821358;
26
27    /// EEG channel TP9 (left ear)
28    pub const TP9: &str = "273e0003-4c4d-454d-96be-f03bac821358";
29    pub const TP9_U128: u128 = 0x273e0003_4c4d_454d_96be_f03bac821358;
30
31    /// EEG channel AF7 (left forehead)
32    pub const AF7: &str = "273e0004-4c4d-454d-96be-f03bac821358";
33    pub const AF7_U128: u128 = 0x273e0004_4c4d_454d_96be_f03bac821358;
34
35    /// EEG channel AF8 (right forehead)
36    pub const AF8: &str = "273e0005-4c4d-454d-96be-f03bac821358";
37    pub const AF8_U128: u128 = 0x273e0005_4c4d_454d_96be_f03bac821358;
38
39    /// EEG channel TP10 (right ear)
40    pub const TP10: &str = "273e0006-4c4d-454d-96be-f03bac821358";
41    pub const TP10_U128: u128 = 0x273e0006_4c4d_454d_96be_f03bac821358;
42
43    /// Right auxiliary electrode
44    pub const RIGHT_AUX: &str = "273e0007-4c4d-454d-96be-f03bac821358";
45    pub const RIGHT_AUX_U128: u128 = 0x273e0007_4c4d_454d_96be_f03bac821358;
46
47    /// Gyroscope data
48    pub const GYRO: &str = "273e0009-4c4d-454d-96be-f03bac821358";
49    pub const GYRO_U128: u128 = 0x273e0009_4c4d_454d_96be_f03bac821358;
50
51    /// Accelerometer data
52    pub const ACCEL: &str = "273e000a-4c4d-454d-96be-f03bac821358";
53    pub const ACCEL_U128: u128 = 0x273e000a_4c4d_454d_96be_f03bac821358;
54
55    /// Telemetry data (battery, etc.)
56    pub const TELEMETRY: &str = "273e000b-4c4d-454d-96be-f03bac821358";
57    pub const TELEMETRY_U128: u128 = 0x273e000b_4c4d_454d_96be_f03bac821358;
58
59    /// PPG channel 1 (IR)
60    pub const PPG1: &str = "273e000f-4c4d-454d-96be-f03bac821358";
61    pub const PPG1_U128: u128 = 0x273e000f_4c4d_454d_96be_f03bac821358;
62
63    /// PPG channel 2 (Near-IR)
64    pub const PPG2: &str = "273e0010-4c4d-454d-96be-f03bac821358";
65    pub const PPG2_U128: u128 = 0x273e0010_4c4d_454d_96be_f03bac821358;
66
67    /// PPG channel 3 (Red)
68    pub const PPG3: &str = "273e0011-4c4d-454d-96be-f03bac821358";
69    pub const PPG3_U128: u128 = 0x273e0011_4c4d_454d_96be_f03bac821358;
70
71    /// All EEG channel UUIDs in order
72    pub const EEG_CHANNELS: [u128; 4] = [TP9_U128, AF7_U128, AF8_U128, TP10_U128];
73    pub const EEG_CHANNEL_NAMES: [&str; 4] = ["TP9", "AF7", "AF8", "TP10"];
74
75    /// All PPG channel UUIDs in order
76    pub const PPG_CHANNELS: [u128; 3] = [PPG1_U128, PPG2_U128, PPG3_U128];
77    pub const PPG_CHANNEL_NAMES: [&str; 3] = ["PPG1", "PPG2", "PPG3"];
78}
79
80/// Muse device specifications
81pub mod spec {
82    /// EEG sample rate in Hz
83    pub const SAMPLE_RATE: u16 = 256;
84    /// Number of EEG channels
85    pub const CHANNEL_COUNT: usize = 4;
86    /// Samples per EEG packet (20 bytes = 2 header + 18 data = 12 samples)
87    pub const SAMPLES_PER_PACKET: usize = 12;
88    /// EEG packet size in bytes
89    pub const PACKET_SIZE: usize = 20;
90
91    /// PPG sample rate in Hz (Muse S/Muse 2)
92    pub const PPG_SAMPLE_RATE: u16 = 64;
93    /// Number of PPG channels (IR, near-IR, red)
94    pub const PPG_CHANNEL_COUNT: usize = 3;
95    /// Samples per PPG packet (20 bytes = 2 header + 18 data = 6 samples of 24 bits each)
96    pub const PPG_SAMPLES_PER_PACKET: usize = 6;
97    /// PPG packet size in bytes
98    pub const PPG_PACKET_SIZE: usize = 20;
99}
100
101/// Muse control commands
102pub mod command {
103    /// Set preset v1 (default EEG mode)
104    pub const SET_PRESET: &str = "v1";
105    /// Enable PPG/aux channels
106    pub const ENABLE_AUX: &str = "p21";
107    /// Start data streaming
108    pub const START_STREAM: &str = "d";
109    /// Stop data streaming
110    pub const STOP_STREAM: &str = "h";
111    /// Request device info
112    pub const DEVICE_INFO: &str = "?";
113
114    /// Encode a command for BLE transmission
115    /// Format: [length byte] [command string] [newline]
116    pub fn encode(cmd: &str) -> Vec<u8> {
117        let mut bytes = Vec::with_capacity(cmd.len() + 2);
118        bytes.push((cmd.len() + 1) as u8); // Length includes newline
119        bytes.extend_from_slice(cmd.as_bytes());
120        bytes.push(0x0A); // Newline
121        bytes
122    }
123
124    /// Parse a received command (strips length prefix and newline)
125    pub fn decode(data: &[u8]) -> Option<&str> {
126        if data.is_empty() {
127            return None;
128        }
129        let len = data[0] as usize;
130        if data.len() < len + 1 {
131            return None;
132        }
133        // Skip length byte, take command bytes (exclude newline)
134        let cmd_end = if data.len() > 1 && data[len] == 0x0A {
135            len
136        } else {
137            len.min(data.len() - 1)
138        };
139        std::str::from_utf8(&data[1..cmd_end]).ok()
140    }
141}
142
143/// Encode 12 EEG samples (in microvolts) into a 20-byte Muse packet
144///
145/// Packet format:
146/// - Bytes 0-1: Packet header/sequence number
147/// - Bytes 2-19: 12 samples packed as 12-bit values (18 bytes)
148///
149/// Each pair of 12-bit samples is packed into 3 bytes:
150/// [b0][b1][b2] where sample1 = b0<<4 | b1>>4, sample2 = (b1&0x0F)<<8 | b2
151pub fn encode_eeg_packet(sequence: u16, samples: &[f32]) -> [u8; 20] {
152    let mut packet = [0u8; 20];
153
154    // Header: 2-byte sequence number
155    packet[0] = (sequence >> 8) as u8;
156    packet[1] = (sequence & 0xFF) as u8;
157
158    // Convert samples to 12-bit ADC values and pack
159    // ADC is centered at 0x800 (2048), scale: 256.0 / 125.0 LSB per uV
160    let mut byte_idx = 2;
161    let total_samples = spec::SAMPLES_PER_PACKET;
162    for i in (0..total_samples).step_by(2) {
163        let s1 = samples.get(i).copied().unwrap_or(0.0);
164        let s2 = samples.get(i + 1).copied().unwrap_or(0.0);
165
166        // Convert from microvolts to 12-bit ADC value
167        let v1 = uv_to_adc(s1);
168        let v2 = uv_to_adc(s2);
169
170        // Pack two 12-bit values into 3 bytes
171        packet[byte_idx] = (v1 >> 4) as u8;
172        packet[byte_idx + 1] = ((v1 & 0x0F) << 4 | (v2 >> 8)) as u8;
173        packet[byte_idx + 2] = (v2 & 0xFF) as u8;
174
175        byte_idx += 3;
176    }
177
178    packet
179}
180
181/// Decode a 20-byte Muse EEG packet into samples (in microvolts)
182pub fn decode_eeg_packet(packet: &[u8]) -> (u16, Vec<f32>) {
183    if packet.len() < 20 {
184        return (0, Vec::new());
185    }
186
187    // Extract sequence number
188    let sequence = ((packet[0] as u16) << 8) | (packet[1] as u16);
189
190    let mut samples = Vec::with_capacity(12);
191
192    // Decode packed 12-bit samples
193    for i in (2..20).step_by(3) {
194        if i + 2 >= packet.len() {
195            break;
196        }
197
198        let b0 = packet[i] as u16;
199        let b1 = packet[i + 1] as u16;
200        let b2 = packet[i + 2] as u16;
201
202        // First 12-bit value: b0[7:0] + b1[7:4]
203        let v1_raw = (b0 << 4) | (b1 >> 4);
204        // Second 12-bit value: b1[3:0] + b2[7:0]
205        let v2_raw = ((b1 & 0x0F) << 8) | b2;
206
207        samples.push(adc_to_uv(v1_raw));
208        samples.push(adc_to_uv(v2_raw));
209    }
210
211    (sequence, samples)
212}
213
214/// Decoded PPG frame with 6 samples.
215///
216/// Each PPG characteristic (PPG1, PPG2, PPG3) represents a different LED:
217/// - PPG1: IR (infrared)
218/// - PPG2: Near-IR (near infrared)
219/// - PPG3: Red
220///
221/// Each packet contains 6 consecutive 24-bit samples of that LED type.
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct PpgFrame {
224    pub sequence: u16,
225    pub samples: [u32; 6],
226}
227
228/// Encode 6 24-bit PPG samples into a 20-byte Muse packet.
229///
230/// Packet format:
231/// - Bytes 0-1: Packet header/sequence number (big-endian)
232/// - Bytes 2-19: 6 samples as 24-bit big-endian values (18 bytes)
233pub fn encode_ppg_packet(sequence: u16, samples: &[u32; 6]) -> [u8; 20] {
234    let mut packet = [0u8; 20];
235    packet[0] = (sequence >> 8) as u8;
236    packet[1] = (sequence & 0xFF) as u8;
237
238    for (i, &sample) in samples.iter().enumerate() {
239        let sample = sample & 0x00FF_FFFF; // 24-bit clamp
240        let idx = 2 + i * 3;
241        packet[idx] = (sample >> 16) as u8;
242        packet[idx + 1] = (sample >> 8) as u8;
243        packet[idx + 2] = sample as u8;
244    }
245
246    packet
247}
248
249/// Decode a 20-byte Muse PPG packet into a PpgFrame.
250///
251/// Each packet contains 6 consecutive 24-bit samples (big-endian).
252pub fn decode_ppg_packet(packet: &[u8]) -> Option<PpgFrame> {
253    if packet.len() < 20 {
254        return None;
255    }
256
257    let sequence = ((packet[0] as u16) << 8) | (packet[1] as u16);
258    let data = &packet[2..20];
259
260    let mut samples = [0u32; 6];
261    for (i, sample) in samples.iter_mut().enumerate() {
262        let idx = i * 3;
263        if idx + 2 >= data.len() {
264            return None;
265        }
266        *sample =
267            ((data[idx] as u32) << 16) | ((data[idx + 1] as u32) << 8) | (data[idx + 2] as u32);
268    }
269
270    Some(PpgFrame { sequence, samples })
271}
272
273/// Convert microvolts to 12-bit ADC value
274fn uv_to_adc(uv: f32) -> u16 {
275    // ADC centered at 0x800, scale factor: 256.0 / 125.0 LSB per uV
276    let adc = (uv * 256.0 / 125.0) + 2048.0;
277    (adc.clamp(0.0, 4095.0) as u16) & 0x0FFF
278}
279
280/// Convert 12-bit ADC value to microvolts
281fn adc_to_uv(adc: u16) -> f32 {
282    // Reverse: (adc - 2048) * 125.0 / 256.0
283    ((adc as i16 - 0x800) as f32) * 125.0 / 256.0
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_adc_conversion_roundtrip() {
292        let test_values = [0.0, 10.0, -10.0, 100.0, -100.0, 200.0, -200.0];
293        for &uv in &test_values {
294            let adc = uv_to_adc(uv);
295            let back = adc_to_uv(adc);
296            // Allow small error due to 12-bit quantization
297            assert!((uv - back).abs() < 0.5, "uv={uv}, adc={adc}, back={back}");
298        }
299    }
300
301    #[test]
302    fn test_packet_roundtrip() {
303        let samples: Vec<f32> = (0..12).map(|i| (i as f32 - 6.0) * 10.0).collect();
304        let packet = encode_eeg_packet(42, &samples);
305        let (seq, decoded) = decode_eeg_packet(&packet);
306
307        assert_eq!(seq, 42);
308        assert_eq!(decoded.len(), 12);
309
310        for (i, (&orig, &dec)) in samples.iter().zip(decoded.iter()).enumerate() {
311            assert!(
312                (orig - dec).abs() < 0.5,
313                "sample {i}: orig={orig}, decoded={dec}"
314            );
315        }
316    }
317
318    #[test]
319    fn test_command_encode_decode() {
320        let cmd = "d";
321        let encoded = command::encode(cmd);
322        assert_eq!(encoded, vec![2, b'd', 0x0A]);
323
324        let decoded = command::decode(&encoded);
325        assert_eq!(decoded, Some("d"));
326    }
327
328    #[test]
329    fn test_command_decode_rejects_short_payload() {
330        let data = vec![3, b'd', 0x0A];
331        let decoded = command::decode(&data);
332        assert_eq!(decoded, None);
333    }
334
335    #[test]
336    fn test_decode_short_packet_returns_empty() {
337        let (seq, samples) = decode_eeg_packet(&[0u8; 10]);
338        assert_eq!(seq, 0);
339        assert!(samples.is_empty());
340    }
341
342    #[test]
343    fn test_packet_with_short_sample_input_zero_fills() {
344        let samples = vec![50.0, -25.0];
345        let packet = encode_eeg_packet(1, &samples);
346        let (_seq, decoded) = decode_eeg_packet(&packet);
347
348        assert_eq!(decoded.len(), 12);
349        assert!((decoded[0] - 50.0).abs() < 0.5);
350        assert!((decoded[1] + 25.0).abs() < 0.5);
351        for value in decoded.iter().skip(2) {
352            assert!(value.abs() < 0.5);
353        }
354    }
355
356    #[test]
357    fn test_packet_clamps_out_of_range_samples() {
358        let samples = [1_000_000.0, -1_000_000.0].repeat(6);
359        let packet = encode_eeg_packet(7, &samples);
360        let (_seq, decoded) = decode_eeg_packet(&packet);
361
362        let max_uv = ((0x0FFFu16 as i16 - 0x800) as f32) * 125.0 / 256.0;
363        let min_uv = ((0u16 as i16 - 0x800) as f32) * 125.0 / 256.0;
364
365        assert!((decoded[0] - max_uv).abs() < 0.5);
366        assert!((decoded[1] - min_uv).abs() < 0.5);
367    }
368
369    #[test]
370    fn test_ppg_packet_roundtrip() {
371        // 6 samples of 24-bit values
372        let samples = [0x000000, 0x000001, 0xABCDEF, 0xFFFFFF, 0x123456, 0x654321];
373        let packet = encode_ppg_packet(9, &samples);
374        let decoded = decode_ppg_packet(&packet).expect("ppg decode");
375
376        assert_eq!(decoded.sequence, 9);
377        assert_eq!(decoded.samples, samples);
378    }
379}