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#[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#[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#[derive(Debug)]
43pub struct AccelerometerData {
44 pub channels: usize,
45 pub sampling_rate: f32,
46 pub units: String,
47}
48
49#[derive(Debug)]
51pub struct TriggerInfo {
52 pub triggers: HashMap<u32, String>,
53}
54
55#[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 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 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 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 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 let sample_rate = if let Some(captures) = re.captures(&line) {
201 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 fn parse_trigger_info(line: &str, data: &mut EEGData) {
261 if line.contains("Code") && line.contains("Description") {
263 return;
264 }
265
266 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 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]
329 fn test_parse_file() {
330 let filename = create_sample_file();
331 let eeg_data = EEGData::parse_file(&filename).unwrap();
332
333 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 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 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 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 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 std::fs::remove_file(filename).unwrap();
395 }
396
397 #[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]
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}