Skip to main content

emotiv_cortex_v2/protocol/
headset.rs

1//! Headset discovery, metadata, and config-mapping protocol types.
2
3use std::collections::HashMap;
4
5use serde::Deserialize;
6
7/// Options for the `queryHeadsets` method.
8#[derive(Debug, Clone, Default)]
9pub struct QueryHeadsetsOptions {
10    /// Filter by a specific headset id.
11    pub id: Option<String>,
12    /// Include flex mapping details in the response payload.
13    pub include_flex_mappings: bool,
14}
15
16/// Headset info returned by `queryHeadsets`.
17#[derive(Debug, Clone, Deserialize)]
18pub struct HeadsetInfo {
19    /// Headset ID (e.g., "INSIGHT-A1B2C3D4").
20    pub id: String,
21
22    /// Connection status: "discovered", "connecting", "connected".
23    pub status: String,
24
25    /// How the headset is connected: "dongle", "bluetooth", "usb cable".
26    #[serde(rename = "connectedBy")]
27    pub connected_by: Option<String>,
28
29    /// Dongle serial number, if connected via USB dongle.
30    #[serde(rename = "dongle")]
31    pub dongle_serial: Option<String>,
32
33    /// Firmware version string.
34    pub firmware: Option<String>,
35
36    /// Motion sensor names available on this headset.
37    #[serde(rename = "motionSensors")]
38    pub motion_sensors: Option<Vec<String>>,
39
40    /// EEG sensor/channel names available on this headset.
41    pub sensors: Option<Vec<String>>,
42
43    /// Device-specific settings.
44    pub settings: Option<serde_json::Value>,
45
46    /// Mapping of EEG channels to headset sensor locations (EPOC Flex).
47    ///
48    /// The Cortex docs and payloads have used both `flexMappings` and
49    /// `flexMapping` over time; we accept either for compatibility.
50    #[serde(rename = "flexMappings", alias = "flexMapping")]
51    pub flex_mapping: Option<serde_json::Value>,
52
53    /// Headband position (EPOC X).
54    #[serde(rename = "headbandPosition")]
55    pub headband_position: Option<String>,
56
57    /// Custom name of the headset, if set by the user.
58    #[serde(rename = "customName")]
59    pub custom_name: Option<String>,
60
61    /// Virtual headset flag (true for virtual devices)
62    #[serde(rename = "isVirtual")]
63    pub is_virtual: Option<bool>,
64
65    /// Device mode (for example, "EPOC", "EPOC+", "EPOC X").
66    pub mode: Option<String>,
67
68    /// Battery percentage in range [0, 100].
69    #[serde(rename = "batteryPercent")]
70    pub battery_percent: Option<u32>,
71
72    /// Signal strength indicator.
73    #[serde(rename = "signalStrength")]
74    pub signal_strength: Option<u32>,
75
76    /// Power status.
77    pub power: Option<String>,
78
79    /// ID for virtual headset pairing.
80    #[serde(rename = "virtualHeadsetId")]
81    pub virtual_headset_id: Option<String>,
82
83    /// User-friendly firmware display string.
84    #[serde(rename = "firmwareDisplay")]
85    pub firmware_display: Option<String>,
86
87    /// Whether the device is in DFU mode.
88    #[serde(rename = "isDfuMode")]
89    pub is_dfu_mode: Option<bool>,
90
91    /// Supported DFU update types.
92    #[serde(rename = "dfuTypes")]
93    pub dfu_types: Option<Vec<String>>,
94
95    /// System uptime value reported by Cortex.
96    #[serde(rename = "systemUpTime")]
97    pub system_up_time: Option<u64>,
98
99    /// Device uptime value reported by Cortex.
100    pub uptime: Option<u64>,
101
102    /// Bluetooth uptime value reported by Cortex.
103    #[serde(rename = "bluetoothUpTime")]
104    pub bluetooth_up_time: Option<u64>,
105
106    /// Internal device counter value.
107    pub counter: Option<u64>,
108
109    /// Forward-compatible storage for new optional fields.
110    #[serde(flatten)]
111    pub extra: HashMap<String, serde_json::Value>,
112}
113
114/// Result payload from `syncWithHeadsetClock`.
115#[derive(Debug, Clone, Deserialize)]
116pub struct HeadsetClockSyncResult {
117    /// Clock adjustment reported by Cortex.
118    pub adjustment: f64,
119    /// Headset id associated with the sync result.
120    pub headset: String,
121}
122
123/// Type of operation requested for `configMapping`.
124#[derive(Debug, Clone, Copy)]
125pub enum ConfigMappingMode {
126    Create,
127    Get,
128    Read,
129    Update,
130    Delete,
131}
132
133impl ConfigMappingMode {
134    /// Returns the Cortex API status string for this mode.
135    #[must_use]
136    pub fn as_str(&self) -> &'static str {
137        match self {
138            ConfigMappingMode::Create => "create",
139            ConfigMappingMode::Get => "get",
140            ConfigMappingMode::Read => "read",
141            ConfigMappingMode::Update => "update",
142            ConfigMappingMode::Delete => "delete",
143        }
144    }
145}
146
147/// Request variants for `configMapping`.
148#[derive(Debug, Clone)]
149pub enum ConfigMappingRequest {
150    /// Create a new mapping configuration.
151    Create {
152        name: String,
153        mappings: serde_json::Value,
154    },
155    /// Retrieve all mapping configurations.
156    Get,
157    /// Read a specific mapping configuration.
158    Read { uuid: String },
159    /// Update a mapping configuration.
160    Update {
161        uuid: String,
162        name: Option<String>,
163        mappings: Option<serde_json::Value>,
164    },
165    /// Delete a mapping configuration.
166    Delete { uuid: String },
167}
168
169impl ConfigMappingRequest {
170    /// Returns the operation mode for this request.
171    #[must_use]
172    pub fn mode(&self) -> ConfigMappingMode {
173        match self {
174            ConfigMappingRequest::Create { .. } => ConfigMappingMode::Create,
175            ConfigMappingRequest::Get => ConfigMappingMode::Get,
176            ConfigMappingRequest::Read { .. } => ConfigMappingMode::Read,
177            ConfigMappingRequest::Update { .. } => ConfigMappingMode::Update,
178            ConfigMappingRequest::Delete { .. } => ConfigMappingMode::Delete,
179        }
180    }
181}
182
183/// Mapping object returned for create/read/update operations.
184#[derive(Debug, Clone, Deserialize)]
185pub struct ConfigMappingValue {
186    /// Optional mapping label metadata.
187    pub label: Option<serde_json::Value>,
188    /// EEG sensor mapping pairs.
189    pub mappings: serde_json::Value,
190    /// Mapping name.
191    pub name: String,
192    /// Mapping UUID.
193    pub uuid: String,
194    /// Forward-compatible storage for additional fields.
195    #[serde(flatten)]
196    pub extra: HashMap<String, serde_json::Value>,
197}
198
199/// Value payload returned by the `get` mode of `configMapping`.
200#[derive(Debug, Clone, Deserialize)]
201pub struct ConfigMappingListValue {
202    /// Available mapping configurations.
203    #[serde(default)]
204    pub config: Vec<ConfigMappingValue>,
205    /// Last update timestamp.
206    pub updated: Option<String>,
207    /// Version identifier.
208    pub version: Option<String>,
209    /// Forward-compatible storage for additional fields.
210    #[serde(flatten)]
211    pub extra: HashMap<String, serde_json::Value>,
212}
213
214/// Parsed response variants for `configMapping`.
215#[derive(Debug, Clone)]
216pub enum ConfigMappingResponse {
217    /// Response for create/read/update modes.
218    Value {
219        message: String,
220        value: ConfigMappingValue,
221    },
222    /// Response for get mode.
223    List {
224        message: String,
225        value: ConfigMappingListValue,
226    },
227    /// Response for delete mode.
228    Deleted { message: String, uuid: String },
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_deserialize_headset_info() {
237        let json = r#"{
238            "id": "INSIGHT-A1B2C3D4",
239            "dongle": "6ff",
240            "firmware": "925",
241            "status": "connected",
242            "connectedBy": "dongle",
243            "motionSensors": ["GYROX", "GYROY", "GYROZ", "ACCX", "ACCY", "ACCZ"],
244            "sensors": ["AF3", "AF4", "T7", "T8", "Pz"]
245        }"#;
246
247        let info: HeadsetInfo = serde_json::from_str(json).unwrap();
248        assert_eq!(info.id, "INSIGHT-A1B2C3D4");
249        assert_eq!(info.status, "connected");
250        assert_eq!(info.sensors.as_ref().unwrap().len(), 5);
251        assert!(info.extra.is_empty());
252    }
253
254    #[test]
255    fn test_deserialize_headset_info_flex_mappings_aliases() {
256        let new_json = r#"{
257            "id": "EPOCFLEX-ABCD1234",
258            "status": "connected",
259            "flexMappings": {"AF3":"C1"}
260        }"#;
261        let old_json = r#"{
262            "id": "EPOCFLEX-ABCD1234",
263            "status": "connected",
264            "flexMapping": {"AF3":"C1"}
265        }"#;
266
267        let new_info: HeadsetInfo = serde_json::from_str(new_json).unwrap();
268        let old_info: HeadsetInfo = serde_json::from_str(old_json).unwrap();
269
270        assert!(new_info.flex_mapping.is_some());
271        assert!(old_info.flex_mapping.is_some());
272    }
273
274    #[test]
275    fn test_deserialize_headset_info_extended_fields() {
276        let json = r#"{
277            "id": "EPOCX-11223344",
278            "status": "connected",
279            "mode": "EPOC X",
280            "batteryPercent": 87,
281            "signalStrength": 2,
282            "power": "on",
283            "virtualHeadsetId": "VH-001",
284            "firmwareDisplay": "3.7.1",
285            "isDfuMode": false,
286            "dfuTypes": ["firmware"],
287            "systemUpTime": 12345,
288            "uptime": 12300,
289            "bluetoothUpTime": 12000,
290            "counter": 91,
291            "futureField": "future"
292        }"#;
293
294        let info: HeadsetInfo = serde_json::from_str(json).unwrap();
295        assert_eq!(info.mode.as_deref(), Some("EPOC X"));
296        assert_eq!(info.battery_percent, Some(87));
297        assert_eq!(info.signal_strength, Some(2));
298        assert_eq!(info.power.as_deref(), Some("on"));
299        assert_eq!(info.virtual_headset_id.as_deref(), Some("VH-001"));
300        assert_eq!(info.firmware_display.as_deref(), Some("3.7.1"));
301        assert_eq!(info.is_dfu_mode, Some(false));
302        assert_eq!(info.dfu_types, Some(vec!["firmware".to_string()]));
303        assert_eq!(info.system_up_time, Some(12345));
304        assert_eq!(info.uptime, Some(12300));
305        assert_eq!(info.bluetooth_up_time, Some(12000));
306        assert_eq!(info.counter, Some(91));
307        assert_eq!(
308            info.extra.get("futureField"),
309            Some(&serde_json::json!("future"))
310        );
311    }
312
313    #[test]
314    fn test_deserialize_headset_clock_sync_result() {
315        let json = r#"{
316            "adjustment": 0.0123,
317            "headset": "INSIGHT-A1B2C3D4"
318        }"#;
319
320        let sync: HeadsetClockSyncResult = serde_json::from_str(json).unwrap();
321        assert!((sync.adjustment - 0.0123).abs() < f64::EPSILON);
322        assert_eq!(sync.headset, "INSIGHT-A1B2C3D4");
323    }
324    #[test]
325    fn test_deserialize_config_mapping_value_response_shapes() {
326        let value_json = r#"{
327            "message": "Create flex mapping config successful",
328            "value": {
329                "label": {},
330                "mappings": {"CMS":"TP9"},
331                "name": "config1",
332                "uuid": "4416dc1b-3a7c-4d20-9ec6-aacdb9930071"
333            }
334        }"#;
335        let list_json = r#"{
336            "message": "Get flex mapping config successful",
337            "value": {
338                "config": [{
339                    "label": {},
340                    "mappings": {"CMS":"TP10"},
341                    "name": "Default Configuration",
342                    "uuid": "f4296b2d-d6e7-45cf-9569-7bc2a1bd56e4"
343                }],
344                "updated": "2025-10-08T06:16:30.521+07:00",
345                "version": "2018-05-08"
346            }
347        }"#;
348        let delete_json = r#"{
349            "message": "Delete flex mapping config successful",
350            "uuid": "effa621f-49d6-4c46-95f3-28f43813a6e9"
351        }"#;
352
353        #[derive(serde::Deserialize)]
354        struct ValueEnvelope {
355            message: String,
356            value: ConfigMappingValue,
357        }
358        #[derive(serde::Deserialize)]
359        struct ListEnvelope {
360            message: String,
361            value: ConfigMappingListValue,
362        }
363        #[derive(serde::Deserialize)]
364        struct DeleteEnvelope {
365            message: String,
366            uuid: String,
367        }
368
369        let value: ValueEnvelope = serde_json::from_str(value_json).unwrap();
370        assert_eq!(value.message, "Create flex mapping config successful");
371        assert_eq!(value.value.name, "config1");
372
373        let list: ListEnvelope = serde_json::from_str(list_json).unwrap();
374        assert_eq!(list.message, "Get flex mapping config successful");
375        assert_eq!(list.value.config.len(), 1);
376        assert_eq!(list.value.version.as_deref(), Some("2018-05-08"));
377
378        let deleted: DeleteEnvelope = serde_json::from_str(delete_json).unwrap();
379        assert_eq!(deleted.message, "Delete flex mapping config successful");
380        assert_eq!(deleted.uuid, "effa621f-49d6-4c46-95f3-28f43813a6e9");
381    }
382}