diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b13bebf --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'wfdb_corrosion'", + "cargo": { + "args": [ + "build", + "--bin=wfdb_corrosion", + "--package=wfdb_corrosion" + ], + "filter": { + "name": "wfdb_corrosion", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'wfdb_corrosion'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=wfdb_corrosion", + "--package=wfdb_corrosion" + ], + "filter": { + "name": "wfdb_corrosion", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/src/headproc.rs b/src/headproc.rs new file mode 100644 index 0000000..eb271c2 --- /dev/null +++ b/src/headproc.rs @@ -0,0 +1,276 @@ +use std::{u64, vec}; + +use crate::SignalFormat; + +/// Record information obtained from the header. +/// +/// Only the `name`, `signal_count`, and `sampling_freq` values are required by default. +#[derive(Debug, Clone)] +struct Record { + name: String, + seg_num: Option, + signal_count: u64, + sampling_freq: u64, + counter_freq: Option, + base_counter_val: Option, + sample_num: Option, + basetime: Option, // I dont thing we really need to care much about these + basedate: Option +} + +impl Record { + /// Attempts to generate the record information from a string + pub fn from_str(argument_line: &str) -> Result { + let args: Vec<&str> = argument_line.split(' ').collect(); + if args.len() < 2 { + return Err("error: header file provided lacks sufficient record arguments."); + } + let name = args[0].to_string(); + let seg_num: Option; + let sig_count: u64; + let sampling_freq: u64; + // Everything else is initialized with None, this is for sake of me not getting a stroke + let mut counter_freq: Option = None; + let base_counter_val: Option = None; + let sample_num: Option = None; + let basetime: Option = None; + let basedate: Option = None; + + + // Signals and segments are kept in a single argument organized as signal/segment + // segments are optional and we need to check this too + { + let seg_sig: Vec<&str> = args[1].split('/').collect(); + if seg_sig.len() == 2 { + match seg_sig[1].parse::() { + Ok(value) => { + seg_num = Some(value); + } + Err(_) => { + return Err("error: failed to parse segment number from header."); + } + } + } else { + seg_num = None; + } + match seg_sig[0].parse::() { + Ok(value) => { + sig_count = value; + } + Err(_) => { + return Err("error: failed to parse signal number from header."); + } + } + } + + // Parse everything else, if present + loop { // This is apparently the way to get safe correct goto-like behaviour + if args.len() <= 2 { // Sampling frequency and counter frequency + sampling_freq = 250; // Default value + break; + } + + { + let freq_dual: Vec<&str> = args[2].split('/').collect(); + if freq_dual.len() == 2 { + match freq_dual[1].parse::() { + Ok(value) => { + counter_freq = Some(value); + } + Err(_) => { + return Err("error: failed to parse counter frequency from header."); + } + } + } + match freq_dual[0].parse::() { + Ok(value) => { + sampling_freq = value; + } + Err(_) => { + return Err("error: failed to parse sampling frequency from header."); + } + } + } + + // Todo: Other arguments in the standard + + break; + } + Ok(Record { + name: name, + seg_num: seg_num, + signal_count: sig_count, + sampling_freq: sampling_freq, + counter_freq: counter_freq, + base_counter_val: base_counter_val, + sample_num: sample_num, + basetime: basetime, + basedate: basedate + }) + } +} + +#[derive(Debug, Clone)] +struct SignalSpec { + filename: String, + format: SignalFormat, + samples_frame: Option, + skew: Option, + offset: Option, + adc_gain: Option, + baseline: Option, + units: Option, + adc_resolution: Option, + adc_zero: Option, + initial_val: Option, + checksum: Option, + blocksize: Option, + desc: Option +} + +impl SignalSpec { + /// Attempts to generate a valid signal specification struct from a supplied string. + /// Returns a `Result` containing possible error information. + /// + /// ## Arguments + /// - `argument_line` - argument line for the signal specification, taken from header file + pub fn from_str(argument_line: &str) -> Result { + let args: Vec<&str> = argument_line.split(' ').collect(); + + if args.len() < 2 { + return Err("error: signal provided by header file lacks sufficient arguments."); + } + let name = args[0].to_string(); + let sigformat: SignalFormat; + // Optional args + let samples_frame: Option = None; + let skew: Option = None; + let offset: Option = None; + let adc_gain: Option = None; + let baseline: Option = None; + let units: Option = None; + let adc_resolution: Option = None; + let adc_zero: Option = None; + let initial_val: Option = None; + let checksum: Option = None; + let blocksize: Option = None; + let desc: Option = None; + + + match args[1].parse::() { + Ok(value) => { + sigformat = SignalSpec::parse_format(value); + }, + Err(_) => { + return Err("error: unable to parse format from signal"); + } + } + + + Ok( + SignalSpec { filename: name, + format: sigformat, + samples_frame: samples_frame, + skew: skew, offset: offset, + adc_gain: adc_gain, + baseline: baseline, + units: units, + adc_resolution: adc_resolution, + adc_zero: adc_zero, + initial_val: initial_val, + checksum: checksum, + blocksize: blocksize, + desc: desc } + ) + } + + fn parse_format(formatnum: u64) -> SignalFormat { + match formatnum { + 16 => SignalFormat::Format16, + 212 => SignalFormat::Format212, + 0..=u64::MAX => SignalFormat::Unimpl + } + } +} + +#[derive(Debug, Clone)] +pub struct Header { + record: Option, + signal_specs: Vec +} + +impl Header { + /// Creates a completely empty, not fully initialized header + /// + /// This is a workaround because I couldn't figure out how to make the compiler + /// accept non-initializing the value for the first pass of a for loop in + /// `parse_header()` + fn new() -> Header { + Header { record: None, signal_specs: vec![] } + } + + fn from_record(record: Record) -> Header { + Header { record: Some(record), signal_specs: vec![] } + } + + fn add_signal_spec(&mut self, spec: SignalSpec) { + self.signal_specs.push(spec); + } + + pub fn is_empty(&self) -> bool { + match self.record { + Some(_) => false, + None => true + } + } +} + +/// Attempts to parse the header file +pub fn parse_header(header_data: &str) -> Result { + let header_lines: Vec<&str> = header_data.split("\n").collect(); + let mut found_record: bool = false; + let mut header: Header = Header::new(); + let mut specs_read: u64 = 0; + let mut specs_max: u64 = 0; + for line in header_lines { + // Ignore commented lines + if line.starts_with("#") { + continue; + } + + if !found_record { + let possible_record = Record::from_str(line); + match possible_record { + Ok(rec) => { + specs_max = rec.signal_count; + header = Header::from_record(rec); + found_record = true; + continue; + } + Err(e) => { + return Err(e) + } + } + } + + let possible_spec = SignalSpec::from_str(line); + match possible_spec { + Ok(spec) => { + header.add_signal_spec(spec); + } + Err(e) => { + return Err(e); + } + } + + specs_read += 1; + if specs_read >= specs_max { + break; + } + } + + if header.is_empty() { + return Err("Unable to parse valid header information"); + } + Ok(header) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a9..eecae06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,47 @@ -fn main() { - println!("Hello, world!"); +use std::{env::{self}, io::Error, path::Path}; +use std::fs; + +pub mod headproc; // The HEAder PROCessing + +/// Use for handling possible formats of the WFDB data +#[derive(Debug, Clone, Copy)] +enum SignalFormat { + Format16 = 16, + Format212 = 212, + Unimpl = 0 } + +fn main() -> Result<(), Error>{ + let args: Vec = env::args().collect(); + + if args.len() <= 1 || args.contains(&"help".to_string()) { + help(); + return Ok(()); + } + + let filepath = Path::new(&args[1]); + if !filepath.is_file() { + println!("Path argument provided is not a valid file"); + return Ok(()); + } + if filepath.extension().unwrap() != "hea" { + println!("File provided is not a .hea file"); + return Ok(()); + } + + let hea_file_result = fs::read_to_string(filepath); + match hea_file_result { + Ok(file_data) => { + let header = headproc::parse_header(file_data.as_str()); + dbg!(header); + } + Err(e) => return Err(e) + } + println!("Hello, world!"); + Ok(()) +} + +fn help() { + println!("Conversion of WFDB files to a more human readable format. By default to a CSV."); + println!("\nUse in the format \"wfdb_corrosion (.hea filename)\"") +} \ No newline at end of file