use crate::{Error, Result, Sample, SignalFormat};
type FormatFieldComponents = (SignalFormat, Option<u32>, Option<u32>, Option<u64>);
struct OptionalFields {
adc_gain: Option<f64>,
baseline: Option<i32>,
units: Option<String>,
adc_resolution: Option<u8>,
adc_zero: Option<i32>,
initial_value: Option<Sample>,
checksum: Option<i32>,
block_size: Option<i32>,
description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum FieldType {
Gain,
Resolution,
AdcZero,
InitialValue,
Checksum,
BlockSize,
Description,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
enum ParseState {
Start,
AfterGain,
AfterResolution,
AfterZero,
AfterInitial,
AfterChecksum,
AfterBlockSize,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalInfo {
pub file_name: String,
pub format: SignalFormat,
pub samples_per_frame: Option<u32>,
pub skew: Option<u32>,
pub byte_offset: Option<u64>,
pub adc_gain: Option<f64>,
pub baseline: Option<i32>,
pub units: Option<String>,
pub adc_resolution: Option<u8>,
pub adc_zero: Option<i32>,
pub initial_value: Option<Sample>,
pub checksum: Option<i32>,
pub block_size: Option<i32>,
pub description: Option<String>,
}
impl SignalInfo {
pub const DEFAULT_ADC_GAIN: f64 = 200.0;
pub const DEFAULT_ADC_RESOLUTION_AMPLITUDE: u8 = 12;
pub const DEFAULT_ADC_RESOLUTION_DIFFERENCE: u8 = 10;
pub const DEFAULT_ADC_ZERO: i32 = 0;
pub const DEFAULT_BASELINE: i32 = Self::DEFAULT_ADC_ZERO;
pub const DEFAULT_UNITS: &'static str = "mV";
pub const DEFAULT_SAMPLES_PER_FRAME: u32 = 1;
pub const DEFAULT_SKEW: u32 = 0;
pub const DEFAULT_BYTE_OFFSET: u64 = 0;
pub const DEFAULT_BLOCK_SIZE: i32 = 0;
pub fn from_signal_line(line: &str) -> Result<Self> {
let line = line.trim();
let mut parts = line.split_whitespace();
let file_name = parts
.next()
.ok_or_else(|| Error::InvalidHeader("Missing file name".to_string()))?
.to_string();
let format_field = parts
.next()
.ok_or_else(|| Error::InvalidHeader("Missing format field".to_string()))?;
let (format, samples_per_frame, skew, byte_offset) =
Self::parse_format_field(format_field)?;
let remaining: Vec<&str> = parts.collect();
let optional_fields = Self::parse_optional_fields(&remaining)?;
Ok(Self {
file_name,
format,
samples_per_frame,
skew,
byte_offset,
adc_gain: optional_fields.adc_gain,
baseline: optional_fields.baseline,
units: optional_fields.units,
adc_resolution: optional_fields.adc_resolution,
adc_zero: optional_fields.adc_zero,
initial_value: optional_fields.initial_value,
checksum: optional_fields.checksum,
block_size: optional_fields.block_size,
description: optional_fields.description,
})
}
fn parse_format_field(field: &str) -> Result<FormatFieldComponents> {
let mut format_str = field;
let mut samples_per_frame = None;
let mut skew = None;
let mut byte_offset = None;
if let Some(plus_pos) = format_str.find('+') {
let offset_str = &format_str[plus_pos + 1..];
byte_offset = Some(
offset_str
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid byte offset: {e}")))?,
);
format_str = &format_str[..plus_pos];
}
if let Some(colon_pos) = format_str.find(':') {
let skew_str = &format_str[colon_pos + 1..];
skew = Some(
skew_str
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid skew: {e}")))?,
);
format_str = &format_str[..colon_pos];
}
if let Some(x_pos) = format_str.find('x') {
let spf_str = &format_str[x_pos + 1..];
samples_per_frame =
Some(spf_str.parse().map_err(|e| {
Error::InvalidHeader(format!("Invalid samples per frame: {e}"))
})?);
if let Some(spf) = samples_per_frame
&& spf == 0
{
return Err(Error::InvalidHeader(
"Samples per frame must be greater than zero".to_string(),
));
}
format_str = &format_str[..x_pos];
}
let format_code: u16 = format_str
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid format code: {e}")))?;
let format = SignalFormat::try_from(format_code).map_err(|_| {
Error::InvalidHeader(format!("Unsupported signal format: {format_code}"))
})?;
Ok((format, samples_per_frame, skew, byte_offset))
}
fn parse_optional_fields(fields: &[&str]) -> Result<OptionalFields> {
if fields.is_empty() {
return Ok(OptionalFields {
adc_gain: None,
baseline: None,
units: None,
adc_resolution: None,
adc_zero: None,
initial_value: None,
checksum: None,
block_size: None,
description: None,
});
}
let mut adc_gain = None;
let mut baseline = None;
let mut units = None;
let mut adc_resolution = None;
let mut adc_zero = None;
let mut initial_value = None;
let mut checksum = None;
let mut block_size = None;
let mut description = None;
let mut state = ParseState::Start;
for (field_idx, field) in fields.iter().enumerate() {
let field_type = Self::detect_field_type(field, state)?;
match field_type {
FieldType::Gain => {
(adc_gain, baseline, units) = Self::parse_gain_field(field)?;
state = ParseState::AfterGain;
}
FieldType::Resolution => {
adc_resolution = Some(Self::parse_resolution(field)?);
state = ParseState::AfterResolution;
}
FieldType::AdcZero => {
adc_zero = Some(Self::parse_adc_zero(field)?);
state = ParseState::AfterZero;
}
FieldType::InitialValue => {
initial_value = Some(Self::parse_initial_value(field)?);
state = ParseState::AfterInitial;
}
FieldType::Checksum => {
checksum = Some(Self::parse_checksum(field)?);
state = ParseState::AfterChecksum;
}
FieldType::BlockSize => {
block_size = Some(Self::parse_block_size(field)?);
state = ParseState::AfterBlockSize;
}
FieldType::Description => {
description = Some(Self::join_description(&fields[field_idx..]));
break;
}
}
}
Ok(OptionalFields {
adc_gain,
baseline,
units,
adc_resolution,
adc_zero,
initial_value,
checksum,
block_size,
description,
})
}
fn detect_field_type(field: &str, state: ParseState) -> Result<FieldType> {
match state {
ParseState::Start => {
if field.contains('/') || field.contains('(') {
if Self::parse_gain_field(field).is_ok() {
Ok(FieldType::Gain)
} else {
Ok(FieldType::Description)
}
} else if let Ok(val) = field.parse::<f64>() {
if val > 0.0 {
Ok(FieldType::Gain)
} else if field.parse::<i32>().is_ok() {
Ok(FieldType::BlockSize)
} else {
Ok(FieldType::Description)
}
} else if field.parse::<i32>().is_ok() {
Ok(FieldType::BlockSize)
} else {
Ok(FieldType::Description)
}
}
ParseState::AfterGain => {
if field.parse::<u8>().is_ok() {
Ok(FieldType::Resolution)
} else {
Err(Error::InvalidHeader(format!(
"Expected ADC resolution after gain, found '{field}'"
)))
}
}
ParseState::AfterResolution => {
if field.parse::<i32>().is_ok() {
Ok(FieldType::AdcZero)
} else {
Err(Error::InvalidHeader(format!(
"Expected ADC zero after resolution, found '{field}'"
)))
}
}
ParseState::AfterZero => {
if field.parse::<i32>().is_ok() {
Ok(FieldType::InitialValue)
} else {
Err(Error::InvalidHeader(format!(
"Expected initial value after ADC zero, found '{field}'"
)))
}
}
ParseState::AfterInitial => {
if field.parse::<i32>().is_ok() {
Ok(FieldType::Checksum)
} else {
Err(Error::InvalidHeader(format!(
"Expected checksum after initial value, found '{field}'"
)))
}
}
ParseState::AfterChecksum => {
if field.parse::<i32>().is_ok() {
Ok(FieldType::BlockSize)
} else {
Err(Error::InvalidHeader(format!(
"Expected block size after checksum, found '{field}'"
)))
}
}
ParseState::AfterBlockSize => Ok(FieldType::Description),
}
}
fn parse_resolution(field: &str) -> Result<u8> {
field
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid ADC resolution: {e}")))
}
fn parse_adc_zero(field: &str) -> Result<i32> {
field
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid ADC zero: {e}")))
}
fn parse_initial_value(field: &str) -> Result<Sample> {
field
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid initial value: {e}")))
}
fn parse_checksum(field: &str) -> Result<i32> {
field
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid checksum: {e}")))
}
fn parse_block_size(field: &str) -> Result<i32> {
field
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid block size: {e}")))
}
fn parse_gain_field(field: &str) -> Result<(Option<f64>, Option<i32>, Option<String>)> {
let mut gain_part = field;
let mut units = None;
if let Some(slash_pos) = field.find('/') {
let units_str = &field[slash_pos + 1..];
if units_str.is_empty() {
return Err(Error::InvalidHeader("Units field is empty".to_string()));
}
units = Some(units_str.to_string());
gain_part = &field[..slash_pos];
}
let mut baseline = None;
if let Some(paren_start) = gain_part.find('(') {
let paren_end = gain_part.find(')').ok_or_else(|| {
Error::InvalidHeader("Missing closing parenthesis in baseline".to_string())
})?;
baseline = Some(
gain_part[paren_start + 1..paren_end]
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid baseline value: {e}")))?,
);
gain_part = &gain_part[..paren_start];
}
if gain_part.is_empty() {
return Err(Error::InvalidHeader("ADC gain is empty".to_string()));
}
let gain = Some(
gain_part
.parse()
.map_err(|e| Error::InvalidHeader(format!("Invalid ADC gain: {e}")))?,
);
if let Some(g) = gain
&& g <= 0.0
{
return Err(Error::InvalidHeader(format!(
"ADC gain must be greater than zero, got {g}"
)));
}
Ok((gain, baseline, units))
}
fn join_description(fields: &[&str]) -> String {
fields.join(" ")
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub const fn format(&self) -> SignalFormat {
self.format
}
#[must_use]
pub fn samples_per_frame(&self) -> u32 {
self.samples_per_frame
.unwrap_or(Self::DEFAULT_SAMPLES_PER_FRAME)
}
#[must_use]
pub fn skew(&self) -> u32 {
self.skew.unwrap_or(Self::DEFAULT_SKEW)
}
#[must_use]
pub fn byte_offset(&self) -> u64 {
self.byte_offset.unwrap_or(Self::DEFAULT_BYTE_OFFSET)
}
#[must_use]
pub fn adc_gain(&self) -> f64 {
self.adc_gain.unwrap_or(Self::DEFAULT_ADC_GAIN)
}
#[must_use]
pub fn baseline(&self) -> i32 {
self.baseline.unwrap_or_else(|| self.adc_zero())
}
#[must_use]
pub fn units(&self) -> &str {
self.units.as_deref().unwrap_or(Self::DEFAULT_UNITS)
}
#[must_use]
pub const fn adc_resolution(&self) -> u8 {
if let Some(res) = self.adc_resolution {
res
} else if matches!(self.format, SignalFormat::Format8) {
Self::DEFAULT_ADC_RESOLUTION_DIFFERENCE
} else {
Self::DEFAULT_ADC_RESOLUTION_AMPLITUDE
}
}
#[must_use]
pub fn adc_zero(&self) -> i32 {
self.adc_zero.unwrap_or(Self::DEFAULT_ADC_ZERO)
}
#[must_use]
pub fn initial_value(&self) -> Sample {
self.initial_value.unwrap_or_else(|| self.adc_zero())
}
#[must_use]
pub const fn checksum(&self) -> Option<i32> {
self.checksum
}
#[must_use]
pub fn block_size(&self) -> i32 {
self.block_size.unwrap_or(Self::DEFAULT_BLOCK_SIZE)
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}