tekhsi_rs 0.1.1

High-performance client for Tektronix TekHSI enabled oscilloscopes
Documentation
use smol_str::SmolStr;

/// Analog sample payloads in native or floating-point form.
///
/// ```rust
/// use tekhsi_rs::data::AnalogSamples;
///
/// let samples = AnalogSamples::I16(vec![1, 2, 3]);
/// assert_eq!(samples.len(), 3);
/// ```
#[derive(Debug, Clone)]
pub enum AnalogSamples {
    I8(Vec<i8>),
    I16(Vec<i16>),
    F32(Vec<f32>),
    F64(Vec<f64>),
}

impl AnalogSamples {
    /// Number of samples in the payload.
    pub fn len(&self) -> usize {
        match self {
            AnalogSamples::I8(values) => values.len(),
            AnalogSamples::I16(values) => values.len(),
            AnalogSamples::F32(values) => values.len(),
            AnalogSamples::F64(values) => values.len(),
        }
    }

    /// Returns true when the payload is empty.
    pub fn is_empty(&self) -> bool {
        match self {
            AnalogSamples::I8(values) => values.is_empty(),
            AnalogSamples::I16(values) => values.is_empty(),
            AnalogSamples::F32(values) => values.is_empty(),
            AnalogSamples::F64(values) => values.is_empty(),
        }
    }

    /// Iterate over raw values as `f64` without applying scaling.
    pub fn iter_raw(&self) -> Box<dyn ExactSizeIterator<Item = f64> + '_> {
        match self {
            Self::I8(v) => Box::new(v.iter().map(|&x| x as f64)),
            Self::I16(v) => Box::new(v.iter().map(|&x| x as f64)),
            Self::F32(v) => Box::new(v.iter().map(|&x| x as f64)),
            Self::F64(v) => Box::new(v.iter().copied()),
        }
    }
}

/// Decoded analog waveform with axis metadata.
///
/// ```rust
/// use tekhsi_rs::data::{AnalogSamples, AnalogWaveform};
/// use smol_str::SmolStr;
///
/// let waveform = AnalogWaveform {
///     source_name: SmolStr::new("ch1"),
///     y_axis_values: AnalogSamples::I8(vec![1, 2]),
///     y_axis_spacing: 1.0,
///     y_axis_offset: 0.0,
///     y_axis_units: SmolStr::new("V"),
///     x_axis_spacing: 1.0,
///     x_axis_units: SmolStr::new("s"),
///     trigger_index: 0.0,
/// };
/// assert_eq!(waveform.record_length(),2);
/// ```
#[derive(Debug, Clone)]
pub struct AnalogWaveform {
    /// Channel or waveform identifier (e.g., "ch1").
    pub source_name: SmolStr,
    /// Raw sample values from oscilloscope.
    pub y_axis_values: AnalogSamples,
    /// Vertical scale factor applied to raw samples.
    pub y_axis_spacing: f64,
    /// Vertical offset applied to scaled samples.
    pub y_axis_offset: f64,
    /// Unit string for vertical axis (typically "V").
    pub y_axis_units: SmolStr,
    /// Sample period in seconds (time per sample).
    pub x_axis_spacing: f64,
    /// Unit string for horizontal axis (typically "s").
    pub x_axis_units: SmolStr,
    /// Trigger location expressed as sample index from trigger.
    pub trigger_index: f64,
}

impl AnalogWaveform {
    /// Returns the number of samples in this waveform.
    pub fn record_length(&self) -> usize {
        self.y_axis_values.len()
    }

    /// Iterate over normalized floating-point values.
    ///
    /// ```rust
    /// use tekhsi_rs::data::{AnalogSamples, AnalogWaveform};
    /// use smol_str::SmolStr;
    ///
    /// let waveform = AnalogWaveform {
    ///     source_name: SmolStr::new("ch1"),
    ///     y_axis_values: AnalogSamples::I16(vec![2, -2]),
    ///     y_axis_spacing: 0.5,
    ///     y_axis_offset: 1.0,
    ///     y_axis_units: SmolStr::new("V"),
    ///     x_axis_spacing: 1.0,
    ///     x_axis_units: SmolStr::new("s"),
    ///     trigger_index: 0.0,
    /// };
    /// let values: Vec<f64> = waveform.iter_normalized_values().collect();
    /// assert_eq!(values, vec![2.0, 0.0]);
    /// ```
    #[inline]
    pub fn iter_normalized_values(&self) -> impl ExactSizeIterator<Item = f64> {
        self.y_axis_values
            .iter_raw()
            .map(|v| v * self.y_axis_spacing + self.y_axis_offset)
    }
}

#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
    use super::{AnalogSamples, AnalogWaveform};
    use crate::data::Waveform;
    use smol_str::SmolStr;

    #[test]
    fn record_length_analog_uses_sample_len() {
        let waveform = AnalogWaveform {
            source_name: SmolStr::new("ch1"),
            y_axis_values: AnalogSamples::I16(vec![1, 2, 3]),
            y_axis_spacing: 1.0,
            y_axis_offset: 0.0,
            y_axis_units: SmolStr::new("V"),
            x_axis_spacing: 1.0,
            x_axis_units: SmolStr::new("s"),
            trigger_index: 0.0,
        };
        assert_eq!(waveform.record_length(), 3);
        assert_eq!(Waveform::Analog(waveform).record_length(), 3);
    }

    #[test]
    fn analog_normalized_vertical_values_applies_scale_offset() {
        let waveform = AnalogWaveform {
            source_name: SmolStr::new("ch1"),
            y_axis_values: AnalogSamples::I16(vec![2, -2]),
            y_axis_spacing: 0.5,
            y_axis_offset: 1.0,
            y_axis_units: SmolStr::new("V"),
            x_axis_spacing: 1.0,
            x_axis_units: SmolStr::new("s"),
            trigger_index: 0.0,
        };
        let values: Vec<f64> = waveform.iter_normalized_values().collect();
        assert_eq!(values, vec![2.0, 0.0]);
    }

    #[test]
    fn analog_samples_len_and_empty_for_f64() {
        let samples = AnalogSamples::F64(vec![1.0, 2.0]);
        assert_eq!(samples.len(), 2);
        assert!(!samples.is_empty());

        let empty = AnalogSamples::F64(Vec::new());
        assert_eq!(empty.len(), 0);
        assert!(empty.is_empty());
    }

    #[test]
    fn analog_samples_iter_raw_for_f32_and_f64() {
        let f32_samples = AnalogSamples::F32(vec![1.5, -2.5]);
        let f32_values: Vec<f64> = f32_samples.iter_raw().collect();
        assert_eq!(f32_values, vec![1.5, -2.5]);

        let f64_samples = AnalogSamples::F64(vec![3.0, -4.0]);
        let f64_values: Vec<f64> = f64_samples.iter_raw().collect();
        assert_eq!(f64_values, vec![3.0, -4.0]);
    }
}