muse_rs/types.rs
1/// An EEG reading — one BLE notification from a single electrode.
2///
3/// # Classic firmware (Muse 1 / Muse 2 / Muse S ≤ fw 3.x)
4/// One notification per channel at 256 Hz, carrying **12 samples** each
5/// (≈ 46.9 ms of signal per packet). Samples are decoded from 12-bit
6/// big-endian packed values and scaled by 0.48828125 µV/LSB.
7///
8/// # Athena firmware (Muse S fw ≥ 4.x)
9/// All channels arrive on a single universal characteristic. Each notification
10/// is split into per-channel `EegReading`s carrying **2 samples** decoded from
11/// 14-bit little-endian packed values and scaled by 0.0885 µV/LSB.
12/// Channels 4–7 (FPz, AUX\_R, AUX\_L, AUX) are only produced by Athena hardware.
13#[derive(Debug, Clone)]
14pub struct EegReading {
15 /// Sequential packet index emitted by the headset (wraps at 0xFFFF).
16 ///
17 /// Used to reconstruct timestamps for Classic firmware.
18 /// Always `0` for Athena (Athena notifications do not carry a per-channel index).
19 pub index: u16,
20 /// Electrode channel index.
21 ///
22 /// **Classic (channels 0–4):**
23 /// * 0 = TP9 (left rear)
24 /// * 1 = AF7 (left front)
25 /// * 2 = AF8 (right front)
26 /// * 3 = TP10 (right rear)
27 /// * 4 = AUX (optional, `enable_aux: true` required)
28 ///
29 /// **Athena (channels 0–7):** same 0–3 as above, plus:
30 /// * 4 = FPz (frontal midline)
31 /// * 5 = AUX\_R
32 /// * 6 = AUX\_L
33 /// * 7 = AUX
34 pub electrode: usize,
35 /// Wall-clock timestamp in milliseconds since Unix epoch for the *first*
36 /// sample in this packet.
37 ///
38 /// **Classic:** extrapolated from `index` and the known sample rate;
39 /// accurate even under BLE jitter.
40 ///
41 /// **Athena:** always `0.0` — Athena packets do not carry a per-channel
42 /// index, so timestamp reconstruction is not possible with current data.
43 pub timestamp: f64,
44 /// Voltage samples in µV.
45 ///
46 /// **Classic:** 12 samples per packet at 256 Hz.
47 /// **Athena:** 2 samples per packet at 256 Hz.
48 pub samples: Vec<f64>,
49}
50
51/// A PPG (photoplethysmography) reading from the optical heart-rate sensor.
52///
53/// Available on Muse 2 and Muse S only. Each notification carries 6 raw
54/// 24-bit samples at 64 Hz.
55#[derive(Debug, Clone)]
56pub struct PpgReading {
57 /// Sequential packet index (wraps at 0xFFFF), same purpose as [`EegReading::index`].
58 pub index: u16,
59 /// Optical channel:
60 /// * 0 = ambient (background light subtraction)
61 /// * 1 = infrared
62 /// * 2 = red
63 pub ppg_channel: usize,
64 /// Wall-clock timestamp in milliseconds since Unix epoch for the first sample.
65 pub timestamp: f64,
66 /// Raw 24-bit ADC values (not scaled to physical units).
67 /// 6 samples per notification at 64 Hz.
68 pub samples: Vec<u32>,
69}
70
71/// Battery and housekeeping telemetry packet.
72///
73/// Sent by the headset roughly once per second on both Classic and Athena
74/// firmware, though the wire format differs:
75///
76/// | Field | Classic | Athena |
77/// |---|---|---|
78/// | `sequence_id` | from packet | always `0` |
79/// | `battery_level` | u16 BE ÷ 512 | u16 LE ÷ 512 |
80/// | `fuel_gauge_voltage` | u16 BE × 2.2 mV | always `0.0` |
81/// | `temperature` | u16 BE raw ADC | always `0` |
82#[derive(Debug, Clone)]
83pub struct TelemetryData {
84 /// Monotonically increasing packet counter (wraps at 0xFFFF).
85 /// Always `0` for Athena notifications.
86 pub sequence_id: u16,
87 /// Battery state-of-charge in percent (0–100).
88 /// Derived from the raw fuel-gauge reading divided by 512.
89 pub battery_level: f32,
90 /// Fuel-gauge terminal voltage in millivolts (Classic only).
91 /// Raw reading multiplied by 2.2. Always `0.0` for Athena.
92 pub fuel_gauge_voltage: f32,
93 /// Raw ADC temperature value (not converted to °C).
94 /// Always `0` for Athena (field not present in Athena battery packets).
95 pub temperature: u16,
96}
97
98/// A single 3-axis inertial measurement.
99#[derive(Debug, Clone, Copy)]
100pub struct XyzSample {
101 /// X-axis value in sensor-specific units (g for accelerometer, °/s for gyroscope).
102 pub x: f32,
103 /// Y-axis value.
104 pub y: f32,
105 /// Z-axis value.
106 pub z: f32,
107}
108
109/// A batch of inertial measurements from one BLE notification.
110///
111/// Both the accelerometer and gyroscope fire at ≈ 52 Hz and carry 3 XYZ
112/// samples per notification on Classic firmware. Athena packs 3 samples
113/// as well but only the first is currently forwarded (indices 1 and 2 are
114/// copies of index 0 in the Athena decoder).
115///
116/// # Wire format differences
117///
118/// | Property | Classic | Athena |
119/// |---|---|---|
120/// | Integer type | `i16` big-endian | `i16` little-endian |
121/// | Accel scale | +0.0000610352 g/LSB | +0.0000610352 g/LSB |
122/// | Gyro scale | +0.0074768 °/s/LSB | −0.0074768 °/s/LSB (negated) |
123/// | `sequence_id` | from packet | always `0` |
124#[derive(Debug, Clone)]
125pub struct ImuData {
126 /// Monotonically increasing packet counter (wraps at 0xFFFF).
127 /// Always `0` for Athena notifications.
128 pub sequence_id: u16,
129 /// Three consecutive XYZ samples; index 0 is the oldest.
130 /// For Athena, indices 1 and 2 are currently copies of index 0.
131 pub samples: [XyzSample; 3],
132}
133
134/// A time-aligned multi-channel EEG snapshot.
135///
136/// Produced by code that zips per-electrode [`EegReading`]s together so every
137/// channel has a value for the same timestamp.
138#[derive(Debug, Clone)]
139#[allow(dead_code)]
140pub struct EegSample {
141 /// Packet index from the electrode that completed this sample set.
142 pub index: u16,
143 /// Wall-clock timestamp in milliseconds since Unix epoch.
144 pub timestamp: f64,
145 /// Voltage in µV for each channel in order `[TP9, AF7, AF8, TP10, AUX]`.
146 /// Channels not yet received are `f64::NAN`.
147 pub data: Vec<f64>,
148}
149
150/// A parsed Muse control/status response decoded from the control characteristic.
151///
152/// The headset replies to `v1` (device info), `s` (status), and similar
153/// commands with a JSON object split across several BLE packets.
154/// [`crate::parse::ControlAccumulator`] reassembles the fragments; this struct
155/// carries the final result.
156#[derive(Debug, Clone)]
157pub struct ControlResponse {
158 /// The raw, un-parsed JSON string.
159 pub raw: String,
160 /// Key-value pairs from the parsed JSON object.
161 pub fields: serde_json::Map<String, serde_json::Value>,
162}
163
164/// All data events emitted by [`crate::muse_client::MuseClient`].
165///
166/// Consumers receive these values through the `mpsc::Receiver` returned by
167/// [`crate::muse_client::MuseClient::connect`] or
168/// [`crate::muse_client::MuseClient::connect_to`].
169///
170/// # Firmware availability
171///
172/// | Variant | Classic | Athena |
173/// |---|---|---|
174/// | `Eeg` (ch 0–3) | ✓ | ✓ |
175/// | `Eeg` (ch 4–7) | ✗ | ✓ |
176/// | `Ppg` | ✓ (opt-in) | ✗ |
177/// | `Accelerometer` | ✓ | ✓ |
178/// | `Gyroscope` | ✓ | ✓ |
179/// | `Telemetry` | ✓ | ✓ |
180/// | `Control` | ✓ | ✓ |
181#[derive(Debug, Clone)]
182pub enum MuseEvent {
183 /// An EEG packet from one electrode channel.
184 ///
185 /// Produced for channels 0–3 on both Classic and Athena.
186 /// Channels 4–7 (FPz, AUX\_R, AUX\_L, AUX) are Athena-only.
187 /// See [`EegReading`] for per-firmware format differences.
188 Eeg(EegReading),
189 /// A PPG (photoplethysmography) optical packet.
190 ///
191 /// Classic requires `enable_ppg: true` in [`crate::muse_client::MuseClientConfig`].
192 /// Athena optical data is always included with preset `p1045`.
193 Ppg(PpgReading),
194 /// Battery and housekeeping telemetry (~1 Hz).
195 ///
196 /// Produced by both Classic and Athena; some fields are `0` on Athena.
197 /// See [`TelemetryData`] for per-firmware field availability.
198 Telemetry(TelemetryData),
199 /// Accelerometer batch (~52 Hz).
200 ///
201 /// Produced by both Classic and Athena. The gyroscope sign convention and
202 /// byte order differ between firmwares; see [`ImuData`] for details.
203 Accelerometer(ImuData),
204 /// Gyroscope batch (~52 Hz).
205 ///
206 /// Produced by both Classic and Athena. Athena gyro values are negated
207 /// relative to Classic (−0.0074768 vs +0.0074768 °/s/LSB).
208 Gyroscope(ImuData),
209 /// A complete JSON control/status response from the headset.
210 ///
211 /// Produced by both Classic and Athena in response to commands such as
212 /// `v1` (device info) or `s` (status).
213 Control(ControlResponse),
214 /// The BLE link has been established and GATT services discovered.
215 /// The inner `String` is the advertised device name (e.g. `"Muse-AB12"`).
216 Connected(String),
217 /// The BLE link was lost (headset turned off, out of range, etc.).
218 ///
219 /// After receiving this event the channel will be closed; no further
220 /// events will arrive. The TUI and CLI both restart scanning automatically
221 /// when this event is seen.
222 Disconnected,
223}