Skip to main content

hermes_ble/
protocol.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Eugene Hauptmann, Frédéric Simard
3
4//! GATT UUIDs, sampling constants, and sensor scaling factors for Hermes V1
5//! EEG headsets.
6//!
7//! All UUIDs belong to the Hermes vendor namespace
8//! `9fa4XXXX-4967-11e5-a151-0002a5d5c51b`.
9
10use uuid::Uuid;
11
12// ── Services ──────────────────────────────────────────────────────────────────
13
14/// Primary EEG GATT service UUID.
15pub const EEG_SERVICE_UUID: Uuid =
16    Uuid::from_u128(0x9fa480e0_4967_11e5_a151_0002a5d5c51b);
17
18/// Event (button) GATT service UUID.
19pub const EVENT_SERVICE_UUID: Uuid =
20    Uuid::from_u128(0x9fa48300_4967_11e5_a151_0002a5d5c51b);
21
22// ── Characteristics ───────────────────────────────────────────────────────────
23
24/// EEG data characteristic — notifications carry packed 24-bit ADS1299 samples.
25///
26/// Wire format:
27/// ```text
28/// byte[0]      packet index  (0–127, wraps)
29/// bytes[1..]   N × 24-byte sample frames  (8 channels × 3 bytes each)
30/// ```
31///
32/// Each 3-byte channel value is a signed 24-bit big-endian integer from the
33/// ADS1299 ADC.  Convert to µV with [`ads1299_to_microvolts`].
34pub const EEG_DATA_CHARACTERISTIC: Uuid =
35    Uuid::from_u128(0x9fa480e1_4967_11e5_a151_0002a5d5c51b);
36
37/// EEG configuration characteristic — used for both writes (commands) and
38/// notifications (config responses).
39///
40/// Known commands:
41///
42/// | Bytes | Meaning |
43/// |---|---|
44/// | `[0x01, 0x01, 0x00, 0x00]` | Start streaming |
45/// | `[0x02, 0x01, 0x01, 0x95]` | Read register |
46/// | `[0x03, 0x01, 0x00, 0x00]` | Test mode |
47pub const EEG_CONFIG_CHARACTERISTIC: Uuid =
48    Uuid::from_u128(0x9fa480e2_4967_11e5_a151_0002a5d5c51b);
49
50/// Event (button press) characteristic — notifications on user interaction.
51pub const EVENT_CHARACTERISTIC: Uuid =
52    Uuid::from_u128(0x9fa48301_4967_11e5_a151_0002a5d5c51b);
53
54/// 9-DOF motion characteristic — accel + gyro + magnetometer in one notification.
55///
56/// Wire format: 9 × `i16` little-endian:
57/// `[ax, ay, az, gx, gy, gz, mx, my, mz]`
58pub const MOTION_CHARACTERISTIC: Uuid =
59    Uuid::from_u128(0x9fa48201_4967_11e5_a151_0002a5d5c51b);
60
61/// Gyroscope-only characteristic (defined by the device but not currently used
62/// — gyro data arrives via [`MOTION_CHARACTERISTIC`]).
63#[allow(dead_code)]
64pub const GYRO_CHARACTERISTIC: Uuid =
65    Uuid::from_u128(0x9fa48202_4967_11e5_a151_0002a5d5c51b);
66
67/// Compass (magnetometer-only) characteristic (defined by the device but not
68/// currently used — magnetometer data arrives via [`MOTION_CHARACTERISTIC`]).
69#[allow(dead_code)]
70pub const COMPASS_CHARACTERISTIC: Uuid =
71    Uuid::from_u128(0x9fa48203_4967_11e5_a151_0002a5d5c51b);
72
73// ── Sampling constants ────────────────────────────────────────────────────────
74
75/// EEG sample rate in Hz (250 samples per second per channel).
76pub const EEG_FREQUENCY: f64 = 250.0;
77
78/// Number of EEG channels on the ADS1299 (8 differential inputs).
79pub const EEG_NUM_CHANNELS: usize = 8;
80
81/// Bytes per single multi-channel EEG sample frame (8 channels × 3 bytes).
82pub const EEG_FRAME_BYTES: usize = EEG_NUM_CHANNELS * 3;
83
84/// Packet index range (0..=127).  The index wraps from 127 → 0.
85pub const PACKET_INDEX_MODULUS: u8 = 128;
86
87/// Human-readable EEG channel labels.
88///
89/// The ADS1299 provides 8 differential channels.  Exact electrode placement
90/// depends on the headset montage; these are generic labels.
91pub const EEG_CHANNEL_NAMES: [&str; 8] = [
92    "CH1", "CH2", "CH3", "CH4", "CH5", "CH6", "CH7", "CH8",
93];
94
95// ── IMU scaling factors ───────────────────────────────────────────────────────
96
97/// Accelerometer sensitivity: 0.061 mg/LSB → g.
98pub const ACC_SENSITIVITY: f32 = 0.061 / 1000.0;
99
100/// Gyroscope sensitivity: 8.75 mdps/LSB → dps (degrees per second).
101pub const GYRO_SENSITIVITY: f32 = 8.75 / 1000.0;
102
103/// Magnetometer sensitivity: 0.14 mgauss/LSB → gauss.
104pub const MAG_SENSITIVITY: f32 = 0.14 / 1000.0;
105
106// ── ADS1299 conversion ───────────────────────────────────────────────────────
107
108/// Default ADS1299 amplifier gain.
109pub const ADS1299_GAIN: f64 = 12.0;
110
111/// Default ADS1299 reference voltage in volts.
112pub const ADS1299_VREF: f64 = 4.5;
113
114/// Convert a raw signed 24-bit ADS1299 value to microvolts.
115///
116/// Formula: `µV = raw × (2 × Vref × 1e6) / (gain × 2²⁴)`
117///
118/// With default parameters (gain = 12, Vref = 4.5 V) one LSB ≈ 0.04470 µV.
119///
120/// # Example
121///
122/// ```
123/// # use hermes_ble::protocol::ads1299_to_microvolts;
124/// let uv = ads1299_to_microvolts(1000, 12.0, 4.5);
125/// assert!((uv - 44.703).abs() < 0.01);
126/// ```
127pub fn ads1299_to_microvolts(raw: i32, gain: f64, vref: f64) -> f64 {
128    let lsb_uv = (2.0 * vref * 1e6) / (gain * (1u64 << 24) as f64);
129    raw as f64 * lsb_uv
130}
131
132// ── Config command builders ───────────────────────────────────────────────────
133
134/// Build the 4-byte "start streaming" command.
135pub fn cmd_start_streaming() -> [u8; 4] {
136    [0x01, 0x01, 0x00, 0x00]
137}
138
139/// Build the 4-byte "read register" command (register address 0x0195).
140pub fn cmd_read_register() -> [u8; 4] {
141    [0x02, 0x01, 0x01, 0x95]
142}
143
144/// Build the 4-byte "test mode" command.
145pub fn cmd_test_mode() -> [u8; 4] {
146    [0x03, 0x01, 0x00, 0x00]
147}