tekhsi_rs 0.1.1

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

/// Digital sample payloads in native i8 or i16 form.
///
/// ```rust
/// use tekhsi_rs::data::DigitalSamples;
///
/// let samples = DigitalSamples::I8(vec![1, 2]);
/// assert_eq!(samples.len(), 2);
/// ```
#[derive(Debug, Clone)]
pub enum DigitalSamples {
    I8(Vec<i8>),
    I16(Vec<i16>),
}

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

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

    /// Extract a single bitstream from the packed samples.
    ///
    /// ```rust
    /// use tekhsi_rs::data::DigitalSamples;
    ///
    /// let samples = DigitalSamples::I8(vec![-95]); // 0xA1
    /// let bit0 = samples.get_nth_bitstream(0).unwrap();
    /// assert_eq!(bit0, vec![1]);
    /// ```
    pub fn get_nth_bitstream(&self, bitstream_number: usize) -> Option<Vec<u8>> {
        fn extract_bitstream<T>(
            values: &[T],
            bitstream_number: usize,
            width_bits: usize,
        ) -> Option<Vec<u8>>
        where
            T: Copy + Into<i64>,
        {
            if bitstream_number >= width_bits {
                return None;
            }

            let byte_index = bitstream_number / 8;
            let bit_in_byte = bitstream_number % 8;
            let mut output = Vec::with_capacity(values.len());

            for &value in values {
                let val_i64: i64 = value.into();
                let byte = (val_i64 >> (8 * byte_index)) & 0xFF;
                let bit = (byte >> (7 - bit_in_byte)) & 1;
                output.push(bit as u8);
            }

            Some(output)
        }

        match self {
            DigitalSamples::I8(values) => extract_bitstream(values, bitstream_number, 8),
            DigitalSamples::I16(values) => extract_bitstream(values, bitstream_number, 16),
        }
    }

    /// Map i16 samples into the scope's 8-bit digital ordering (MSO 5 series).
    pub fn as_scope_digital8(&self) -> Option<Vec<u8>> {
        const BIT_MAP: [usize; 8] = [7, 5, 3, 1, 15, 13, 11, 9]; // Experimentally observed on my MSO 5 series
        let values = match self {
            DigitalSamples::I16(values) => values,
            _ => return None,
        };

        let mut mapped = Vec::with_capacity(values.len());
        for value in values {
            let bytes = value.to_le_bytes();
            let mut out = 0u8;
            for (d_index, &bit) in BIT_MAP.iter().enumerate() {
                let byte_index = bit / 8;
                let bit_in_byte = bit % 8;
                let raw_byte = bytes[byte_index];
                let bit_value = (raw_byte >> (7 - bit_in_byte)) & 1;
                out |= bit_value << (7 - d_index);
            }
            mapped.push(out);
        }
        Some(mapped)
    }

    /// Expand each sample into per-bit values (MSB first).
    #[inline]
    pub fn iter_normalized_values(&self) -> Box<dyn ExactSizeIterator<Item = Vec<u8>> + '_> {
        match self {
            DigitalSamples::I8(values) => Box::new(values.iter().map(|&value| {
                let byte = value as u8;
                (0..8).map(|bit| (byte >> (7 - bit)) & 1).collect()
            })),
            DigitalSamples::I16(values) => Box::new(values.iter().map(|&value| {
                let value = value as u32;
                let mut bits = Vec::with_capacity(16);
                for bit in 0..8 {
                    bits.push(((value & 0xFF) as u8 >> (7 - bit)) & 1);
                }
                for bit in 0..8 {
                    bits.push((((value >> 8) & 0xFF) as u8 >> (7 - bit)) & 1);
                }
                bits
            })),
        }
    }

    /// Expand each sample into per-bit values (MSB first) as a flat iterator.
    #[inline]
    pub fn iter_normalized_values_flat(&self) -> Box<dyn Iterator<Item = u8> + '_> {
        match self {
            DigitalSamples::I8(values) => Box::new(values.iter().flat_map(|&value| {
                let byte = value as u8;
                (0..8).map(move |bit| (byte >> (7 - bit)) & 1)
            })),
            DigitalSamples::I16(values) => Box::new(values.iter().flat_map(|&value| {
                let value = value as u32;
                let low_bits = (0..8).map(move |bit| ((value & 0xFF) as u8 >> (7 - bit)) & 1);
                let high_bits =
                    (0..8).map(move |bit| (((value >> 8) & 0xFF) as u8 >> (7 - bit)) & 1);
                low_bits.chain(high_bits)
            })),
        }
    }
}

/// Decoded digital waveform with axis metadata.
/// Decoded digital waveform with axis metadata.
///
/// ```rust
/// use tekhsi_rs::data::{DigitalSamples, DigitalWaveform};
/// use smol_str::SmolStr;
///
/// let waveform = DigitalWaveform {
///     source_name: SmolStr::new("d0"),
///     y_axis_bytes: DigitalSamples::I8(vec![0x01]),
///     y_axis_units: SmolStr::new("NONE"),
///     x_axis_spacing: 1.0,
///     x_axis_units: SmolStr::new("s"),
///     trigger_index: 0.0,
/// };
/// assert_eq!(waveform.record_length(), 1);
/// ```
#[derive(Debug, Clone)]
pub struct DigitalWaveform {
    /// Channel or waveform identifier (e.g., "d0", "dall").
    pub source_name: SmolStr,
    /// Raw digital sample values (packed bits).
    pub y_axis_bytes: DigitalSamples,
    /// Unit string for vertical axis (typically "NONE" for digital).
    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 DigitalWaveform {
    /// Returns the number of samples in this waveform.
    pub fn record_length(&self) -> usize {
        self.y_axis_bytes.len()
    }

    /// Expand samples into per-bit values (MSB first).
    #[inline]
    pub fn iter_normalized_values(&self) -> impl ExactSizeIterator<Item = Vec<u8>> {
        self.y_axis_bytes.iter_normalized_values()
    }

    /// Expand samples into per-bit values (MSB first) as a flat iterator.
    #[inline]
    pub fn iter_normalized_values_flat(&self) -> impl Iterator<Item = u8> {
        self.y_axis_bytes.iter_normalized_values_flat()
    }

    /// Map i16 samples into the scope's 8-bit digital ordering (tested with MSO 5 series).
    pub fn as_scope_digital8(&self) -> Option<Vec<u8>> {
        self.y_axis_bytes.as_scope_digital8()
    }

    /// Extract a single bitstream from the packed samples.
    pub fn get_nth_bitstream(&self, bitstream_number: usize) -> Option<Vec<u8>> {
        self.y_axis_bytes.get_nth_bitstream(bitstream_number)
    }
}

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

    #[test]
    fn record_length_digital_uses_sample_len() {
        let waveform = DigitalWaveform {
            source_name: SmolStr::new("d0"),
            y_axis_bytes: DigitalSamples::I8(vec![1, 2, 3, 4]),
            y_axis_units: SmolStr::new("NONE"),
            x_axis_spacing: 1.0,
            x_axis_units: SmolStr::new("s"),
            trigger_index: 0.0,
        };
        assert_eq!(waveform.record_length(), 4);
        assert_eq!(Waveform::Digital(waveform).record_length(), 4);
    }

    #[test]
    fn digital_normalized_vertical_values_unpack_bits() {
        let waveform = DigitalWaveform {
            source_name: SmolStr::new("d0"),
            y_axis_bytes: DigitalSamples::I8(vec![-95]),
            y_axis_units: SmolStr::new("NONE"),
            x_axis_spacing: 1.0,
            x_axis_units: SmolStr::new("s"),
            trigger_index: 0.0,
        };
        let values: Vec<Vec<u8>> = waveform.iter_normalized_values().collect();
        assert_eq!(values, vec![vec![1, 0, 1, 0, 0, 0, 0, 1]]);
    }

    #[test]
    fn digital_normalized_vertical_values_unpack_bits_i16() {
        let waveform = DigitalWaveform {
            source_name: SmolStr::new("dall"),
            y_axis_bytes: DigitalSamples::I16(vec![0x1234]),
            y_axis_units: SmolStr::new("NONE"),
            x_axis_spacing: 1.0,
            x_axis_units: SmolStr::new("s"),
            trigger_index: 0.0,
        };
        let values: Vec<Vec<u8>> = waveform.iter_normalized_values().collect();
        assert_eq!(
            values,
            vec![vec![0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0]]
        );
    }

    #[test]
    fn digital_i16_maps_to_scope_d0_d7() {
        let samples = DigitalSamples::I16(vec![0x0010]);
        let mapped = samples.as_scope_digital8().expect("mapping expected");
        assert_eq!(mapped, vec![0x20]);
    }

    #[test]
    fn digital_i8_get_nth_bitstream() {
        let samples = DigitalSamples::I8(vec![-95]); // byte pattern as 0xA1u8
        let bit0 = samples.get_nth_bitstream(0).expect("bitstream expected");
        let bit1 = samples.get_nth_bitstream(1).expect("bitstream expected");
        let bit7 = samples.get_nth_bitstream(7).expect("bitstream expected");
        assert_eq!(bit0, vec![1]);
        assert_eq!(bit1, vec![0]);
        assert_eq!(bit7, vec![1]);
    }

    #[test]
    fn digital_i16_get_nth_bitstream() {
        let samples = DigitalSamples::I16(vec![0x1234]);
        let bit2 = samples.get_nth_bitstream(2).expect("bitstream expected");
        let bit8 = samples.get_nth_bitstream(8).expect("bitstream expected");
        assert_eq!(bit2, vec![1]);
        assert_eq!(bit8, vec![0]);
    }

    #[test]
    fn digital_get_nth_bitstream_out_of_range() {
        let samples = DigitalSamples::I8(vec![0]);
        assert!(samples.get_nth_bitstream(8).is_none());
        let samples = DigitalSamples::I16(vec![0]);
        assert!(samples.get_nth_bitstream(16).is_none());
    }

    #[test]
    fn digital_normalized_values_flat_i8_matches_nested() {
        let samples = DigitalSamples::I8(vec![-95]); // 0xA1
        let flat: Vec<u8> = samples.iter_normalized_values_flat().collect();
        let nested_flat: Vec<u8> = samples
            .iter_normalized_values()
            .into_iter()
            .flatten()
            .collect();
        assert_eq!(flat, nested_flat);
    }

    #[test]
    fn digital_normalized_values_flat_i16_matches_nested() {
        let samples = DigitalSamples::I16(vec![0x1234]);
        let flat: Vec<u8> = samples.iter_normalized_values_flat().collect();
        let nested_flat: Vec<u8> = samples
            .iter_normalized_values()
            .into_iter()
            .flatten()
            .collect();
        assert_eq!(flat, nested_flat);
    }

    #[test]
    fn digital_samples_is_empty_covers_i8_and_i16() {
        assert!(DigitalSamples::I8(Vec::new()).is_empty());
        assert!(DigitalSamples::I16(Vec::new()).is_empty());
        assert!(!DigitalSamples::I8(vec![1]).is_empty());
        assert!(!DigitalSamples::I16(vec![1]).is_empty());
    }

    #[test]
    fn digital_waveform_iterators_and_scope_mapping() {
        let waveform = DigitalWaveform {
            source_name: SmolStr::new("d0"),
            y_axis_bytes: DigitalSamples::I8(vec![0x01]),
            y_axis_units: SmolStr::new("NONE"),
            x_axis_spacing: 1.0,
            x_axis_units: SmolStr::new("s"),
            trigger_index: 0.0,
        };

        let flat: Vec<u8> = waveform.iter_normalized_values_flat().collect();
        assert_eq!(flat.len(), 8);
        assert!(waveform.as_scope_digital8().is_none());
        assert!(waveform.get_nth_bitstream(0).is_some());
    }
}