easy_rs/
info.rs

1use anyhow::Result;
2use chrono::{DateTime, MappedLocalTime, TimeZone, Utc};
3use regex::Regex;
4use std::collections::HashMap;
5use std::fs::File;
6use std::io::{self, BufRead};
7
8/// Struct holding device information for EEG data.
9#[derive(Debug)]
10pub struct DeviceInfo {
11    pub version: String,
12    pub start_date: Option<DateTime<Utc>>,
13    pub device_class: String,
14    pub communication_type: String,
15    pub device_id: String,
16    pub software_version: String,
17    pub firmware_version: String,
18    pub os: String,
19    pub sdcard_filename: String,
20    pub additional_channel: String,
21}
22
23/// Struct for EEG settings including sampling rate, filters, and montage.
24#[derive(Debug)]
25pub struct EEGSettings {
26    pub total_channels: usize,
27    pub eeg_channels: usize,
28    pub records: usize,
29    pub sampling_rate: f32,
30    pub configured_duration: u32,
31    pub packets_lost: usize,
32    pub line_filter: bool,
33    pub fir_filter: bool,
34    pub eog_correction: bool,
35    pub reference_filter: bool,
36    pub eeg_units: String,
37    pub montage: HashMap<usize, String>,
38    pub accelerometer: Option<AccelerometerData>,
39}
40
41/// Struct for accelerometer data.
42#[derive(Debug)]
43pub struct AccelerometerData {
44    pub channels: usize,
45    pub sampling_rate: f32,
46    pub units: String,
47}
48
49/// Struct for trigger information in EEG data.
50#[derive(Debug)]
51pub struct TriggerInfo {
52    pub triggers: HashMap<u32, String>,
53}
54
55/// Main struct representing EEG data, including device, settings, and trigger info.
56#[derive(Debug)]
57pub struct EEGData {
58    pub device_info: DeviceInfo,
59    pub eeg_settings: EEGSettings,
60    pub trigger_info: TriggerInfo,
61}
62
63impl EEGData {
64    /// Creates a new, empty EEGData struct.
65    pub fn new() -> Self {
66        EEGData {
67            device_info: DeviceInfo {
68                version: String::new(),
69                start_date: None,
70                device_class: String::new(),
71                communication_type: String::new(),
72                device_id: String::new(),
73                software_version: String::new(),
74                firmware_version: String::new(),
75                os: String::new(),
76                sdcard_filename: String::new(),
77                additional_channel: String::new(),
78            },
79            eeg_settings: EEGSettings {
80                total_channels: 0,
81                eeg_channels: 0,
82                records: 0,
83                sampling_rate: 0.0,
84                configured_duration: 0,
85                packets_lost: 0,
86                line_filter: false,
87                fir_filter: false,
88                eog_correction: false,
89                reference_filter: false,
90                eeg_units: String::new(),
91                montage: HashMap::new(),
92                accelerometer: None,
93            },
94            trigger_info: TriggerInfo {
95                triggers: HashMap::new(),
96            },
97        }
98    }
99
100    /// Parses an EEG data file and returns an EEGData struct.
101    pub fn parse_file(filename: &str) -> Result<Self> {
102        let file = File::open(filename)?;
103        let reader = io::BufReader::new(file);
104        let mut data = EEGData::new();
105        let mut current_section = None;
106
107        for line in reader.lines() {
108            let line = line?;
109
110            if line.contains("Step Details") {
111                current_section = Some("Step Details");
112            } else if line.contains("EEG Settings") {
113                current_section = Some("EEG Settings");
114            } else if line.contains("Trigger information") {
115                current_section = Some("Trigger information");
116            }
117
118            match current_section.as_deref() {
119                Some("Step Details") => Self::parse_step_details(&line, &mut data),
120                Some("EEG Settings") => Self::parse_eeg_settings(&line, &mut data),
121                Some("Trigger information") => Self::parse_trigger_info(&line, &mut data),
122                _ => continue,
123            }
124        }
125        Ok(data)
126    }
127
128    /// Parses the 'Step Details' section of the file.
129    fn parse_step_details(line: &str, data: &mut EEGData) {
130        if line.contains("Info Version") {
131            data.device_info.version = line.split(':').nth(1).unwrap_or("").trim().to_string();
132        } else if line.contains("StartDate") {
133            let timestamp: i64 = line
134                .split(':')
135                .nth(1)
136                .unwrap_or("")
137                .trim()
138                .parse()
139                .unwrap_or(0);
140
141            data.device_info.start_date = match Utc.timestamp_millis_opt(timestamp) {
142                MappedLocalTime::Single(dt) => Some(dt),
143                MappedLocalTime::Ambiguous(early, _late) => Some(early),
144                MappedLocalTime::None => None,
145            }
146        } else if line.contains("Device class") {
147            data.device_info.device_class = line.split(':').nth(1).unwrap_or("").trim().to_string();
148        } else if line.contains("Communication type") {
149            data.device_info.communication_type =
150                line.split(':').nth(1).unwrap_or("").trim().to_string();
151        } else if line.contains("Device ID") {
152            data.device_info.device_id = line.split(':').nth(1).unwrap_or("").trim().to_string();
153        } else if line.contains("Software's version") {
154            data.device_info.software_version =
155                line.split(':').nth(1).unwrap_or("").trim().to_string();
156        } else if line.contains("Firmware's version") {
157            data.device_info.firmware_version =
158                line.split(':').nth(1).unwrap_or("").trim().to_string();
159        } else if line.contains("Operative system") {
160            data.device_info.os = line.split(':').nth(1).unwrap_or("").trim().to_string();
161        } else if line.contains("SDCard Filename") {
162            data.device_info.sdcard_filename =
163                line.split(':').nth(1).unwrap_or("").trim().to_string();
164        } else if line.contains("Additional channel") {
165            data.device_info.additional_channel =
166                line.split(':').nth(1).unwrap_or("").trim().to_string();
167        }
168    }
169
170    /// Parses the 'EEG Settings' section of the file.
171    fn parse_eeg_settings(line: &str, data: &mut EEGData) {
172        if line.contains("Total number of channels") {
173            data.eeg_settings.total_channels = line
174                .split(':')
175                .nth(1)
176                .unwrap_or("")
177                .trim()
178                .parse()
179                .unwrap_or(0);
180        } else if line.contains("Number of EEG channels") {
181            data.eeg_settings.eeg_channels = line
182                .split(':')
183                .nth(1)
184                .unwrap_or("")
185                .trim()
186                .parse()
187                .unwrap_or(0);
188        } else if line.contains("Number of records of EEG") {
189            data.eeg_settings.records = line
190                .split(':')
191                .nth(1)
192                .unwrap_or("")
193                .trim()
194                .parse()
195                .unwrap_or(0);
196        } else if line.contains("EEG sampling rate") {
197            let re = Regex::new(r"(\d+)\s*Samples/second").unwrap();
198
199            // Try to find a match in the input string
200            let sample_rate = if let Some(captures) = re.captures(&line) {
201                // Extract the first capture group and convert it to a f64
202                captures[1].parse::<f32>().ok()
203            } else {
204                None
205            };
206
207            if sample_rate.is_some() {
208                data.eeg_settings.sampling_rate = sample_rate.unwrap();
209            }
210        } else if line.contains("EEG recording configured duration") {
211            data.eeg_settings.configured_duration = line
212                .split(':')
213                .nth(1)
214                .unwrap_or("")
215                .trim()
216                .parse()
217                .unwrap_or(0);
218        } else if line.contains("Number of packets lost") {
219            data.eeg_settings.packets_lost = line
220                .split(':')
221                .nth(1)
222                .unwrap_or("")
223                .trim()
224                .parse()
225                .unwrap_or(0);
226        } else if line.contains("Line filter status") {
227            data.eeg_settings.line_filter = line.contains("ON");
228        } else if line.contains("FIR filter status") {
229            data.eeg_settings.fir_filter = line.contains("ON");
230        } else if line.contains("EOG correction filter status") {
231            data.eeg_settings.eog_correction = line.contains("ON");
232        } else if line.contains("Reference filter status") {
233            data.eeg_settings.reference_filter = line.contains("ON");
234        } else if line.contains("EEG units") {
235            data.eeg_settings.eeg_units = line.split(':').nth(1).unwrap_or("").trim().to_string();
236        } else if line.contains("Accelerometer data") {
237            if line.contains("ON") {
238                let accelerometer = AccelerometerData {
239                    channels: 3,
240                    sampling_rate: 100.0,
241                    units: "mm/s^2".to_string(),
242                };
243                data.eeg_settings.accelerometer = Some(accelerometer);
244            }
245        } else if line.contains("Channel") {
246            let parts: Vec<&str> = line.split(':').collect();
247            let channel_number = parts[0]
248                .split_whitespace()
249                .nth(1)
250                .unwrap_or("")
251                .trim()
252                .parse()
253                .unwrap_or(0);
254            let electrode = parts[1].trim().to_string();
255            data.eeg_settings.montage.insert(channel_number, electrode);
256        }
257    }
258
259    /// Parses the 'Trigger information' section of the file.
260    fn parse_trigger_info(line: &str, data: &mut EEGData) {
261        // Skip header if found.
262        if line.contains("Code") && line.contains("Description") {
263            return;
264        }
265
266        // Parse trigger code and description.
267        let parts: Vec<&str> = line.split_whitespace().collect();
268
269        if parts.len() >= 2 {
270            if let Ok(code) = parts[0].parse::<u32>() {
271                let description = parts[1..].join(" ");
272                data.trigger_info.triggers.insert(code, description);
273            }
274        }
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use chrono::TimeZone;
282
283    // Helper function to create a sample EEG file.
284    fn create_sample_file() -> String {
285        let file_content = r#"
286        Step Details
287        Info Version: 1.0
288        StartDate: 1609459200000
289        Device class: EEG
290        Communication type: Bluetooth
291        Device ID: 123456
292        Software's version: 1.0.0
293        Firmware's version: 1.0.1
294        Operative system: Linux
295        SDCard Filename: eegd_data.txt
296        Additional channel: Channel_Extra
297
298        EEG Settings
299        Total number of channels: 8
300        Number of EEG channels: 4
301        Number of records of EEG: 1000
302        EEG sampling rate: 250.0
303        EEG recording configured duration: 3600
304        Number of packets lost: 0
305        Line filter status: ON
306        FIR filter status: OFF
307        EOG correction filter status: ON
308        Reference filter status: OFF
309        EEG units: µV
310        Accelerometer data: ON
311        Channel 1: Fp1
312        Channel 2: Fp2
313        Channel 3: F3
314        Channel 4: F4
315
316        Trigger information
317        Code Description
318        1 Start of EEG
319        2 End of EEG
320        "#;
321
322        let filename = "sample_eeg_data.txt";
323        std::fs::write(filename, file_content).unwrap();
324        filename.to_string()
325    }
326
327    // Test for parsing EEG data file
328    #[test]
329    fn test_parse_file() {
330        let filename = create_sample_file();
331        let eeg_data = EEGData::parse_file(&filename).unwrap();
332
333        // Test Device Info parsing
334        assert_eq!(eeg_data.device_info.version, "1.0");
335        assert_eq!(eeg_data.device_info.device_class, "EEG");
336        assert_eq!(eeg_data.device_info.communication_type, "Bluetooth");
337        assert_eq!(eeg_data.device_info.device_id, "123456");
338        assert_eq!(eeg_data.device_info.software_version, "1.0.0");
339        assert_eq!(eeg_data.device_info.firmware_version, "1.0.1");
340        assert_eq!(eeg_data.device_info.os, "Linux");
341        assert_eq!(eeg_data.device_info.sdcard_filename, "eegd_data.txt");
342        assert_eq!(eeg_data.device_info.additional_channel, "Channel_Extra");
343
344        // Test EEG Settings parsing
345        assert_eq!(eeg_data.eeg_settings.total_channels, 8);
346        assert_eq!(eeg_data.eeg_settings.eeg_channels, 4);
347        assert_eq!(eeg_data.eeg_settings.records, 1000);
348        assert_eq!(eeg_data.eeg_settings.sampling_rate, 250.0);
349        assert_eq!(eeg_data.eeg_settings.configured_duration, 3600);
350        assert_eq!(eeg_data.eeg_settings.packets_lost, 0);
351        assert!(eeg_data.eeg_settings.line_filter);
352        assert!(!eeg_data.eeg_settings.fir_filter);
353        assert!(eeg_data.eeg_settings.eog_correction);
354        assert!(!eeg_data.eeg_settings.reference_filter);
355        assert_eq!(eeg_data.eeg_settings.eeg_units, "µV");
356
357        // Test Accelerometer data
358        assert!(eeg_data.eeg_settings.accelerometer.is_some());
359        let accelerometer = eeg_data.eeg_settings.accelerometer.as_ref().unwrap();
360        assert_eq!(accelerometer.channels, 3);
361        assert_eq!(accelerometer.sampling_rate, 100.0);
362        assert_eq!(accelerometer.units, "mm/s^2");
363
364        // Test Montage parsing
365        assert_eq!(
366            eeg_data.eeg_settings.montage.get(&1),
367            Some(&"Fp1".to_string())
368        );
369        assert_eq!(
370            eeg_data.eeg_settings.montage.get(&2),
371            Some(&"Fp2".to_string())
372        );
373        assert_eq!(
374            eeg_data.eeg_settings.montage.get(&3),
375            Some(&"F3".to_string())
376        );
377        assert_eq!(
378            eeg_data.eeg_settings.montage.get(&4),
379            Some(&"F4".to_string())
380        );
381
382        // Test Trigger Information parsing
383        assert_eq!(eeg_data.trigger_info.triggers.len(), 2);
384        assert_eq!(
385            eeg_data.trigger_info.triggers.get(&1),
386            Some(&"Start of EEG".to_string())
387        );
388        assert_eq!(
389            eeg_data.trigger_info.triggers.get(&2),
390            Some(&"End of EEG".to_string())
391        );
392
393        // Clean up the sample file
394        std::fs::remove_file(filename).unwrap();
395    }
396
397    // Test parsing when the file is empty
398    #[test]
399    fn test_parse_empty_file() {
400        let filename = "empty_file.txt";
401        std::fs::write(filename, "").unwrap();
402
403        let eeg_data = EEGData::parse_file(filename).unwrap();
404        assert_eq!(eeg_data.device_info.version, "");
405        assert_eq!(eeg_data.eeg_settings.total_channels, 0);
406        assert_eq!(eeg_data.trigger_info.triggers.len(), 0);
407
408        std::fs::remove_file(filename).unwrap();
409    }
410
411    // Test parsing when a field is missing (e.g., missing "StartDate" in the Step Details section)
412    #[test]
413    fn test_parse_missing_field() {
414        let file_content = r#"
415        Step Details
416        Info Version: 1.0
417        Device class: EEG
418        Communication type: Bluetooth
419        Device ID: 123456
420        Software's version: 1.0.0
421        Firmware's version: 1.0.1
422        Operative system: Linux
423        SDCard Filename: eegd_data.txt
424        Additional channel: Channel_Extra
425
426        EEG Settings
427        Total number of channels: 8
428        Number of EEG channels: 4
429        Number of records of EEG: 1000
430        EEG sampling rate: 250.0
431        EEG recording configured duration: 3600
432        Number of packets lost: 0
433        Line filter status: ON
434        FIR filter status: OFF
435        EOG correction filter status: ON
436        Reference filter status: OFF
437        EEG units: µV
438        Accelerometer data: ON
439
440        Trigger information
441        Code Description
442        1 Start of EEG
443        2 End of EEG
444        "#;
445
446        let filename = "missing_start_date.txt";
447        std::fs::write(filename, file_content).unwrap();
448
449        let eeg_data = EEGData::parse_file(filename).unwrap();
450        assert!(eeg_data.device_info.start_date.is_none());
451
452        std::fs::remove_file(filename).unwrap();
453    }
454}