fleascope-rs 0.2.1

Library to interact with a Fleascope
Documentation
use crate::flea_scope::CaptureConfigError;

pub struct StringifiedTriggerConfig {
    trigger_fields: String,
}

impl StringifiedTriggerConfig {
    pub fn into_string(self) -> String {
        self.trigger_fields
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BitState {
    High,
    Low,
    DontCare,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DigitalTriggerBehavior {
    Auto,
    While,
    Start,
    Stop,
}

impl DigitalTriggerBehavior {
    pub fn as_str(&self) -> &'static str {
        match self {
            DigitalTriggerBehavior::Auto => "~",
            DigitalTriggerBehavior::While => "",
            DigitalTriggerBehavior::Start => "+",
            DigitalTriggerBehavior::Stop => "-",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnalogTriggerBehavior {
    Auto,
    Level,
    Rising,
    Falling,
}

impl AnalogTriggerBehavior {
    pub fn as_str(&self) -> &'static str {
        match self {
            AnalogTriggerBehavior::Auto => "~",
            AnalogTriggerBehavior::Level => "",
            AnalogTriggerBehavior::Rising => "+",
            AnalogTriggerBehavior::Falling => "-",
        }
    }
}

#[derive(Debug)]
pub struct BitTriggerBuilder {
    bit_states: [BitState; 9],
}

impl BitTriggerBuilder {
    pub fn new() -> Self {
        Self {
            bit_states: [BitState::DontCare; 9],
        }
    }

    pub fn set_bit(mut self, bit: usize, state: BitState) -> Self {
        if bit >= self.bit_states.len() {
            panic!(
                "Bit index {} out of range, must be between 0 and {}",
                bit,
                self.bit_states.len() - 1
            );
        }
        self.bit_states[bit] = state;
        self
    }

    pub fn bit0(self, state: BitState) -> Self {
        self.set_bit(0, state)
    }

    pub fn bit1(self, state: BitState) -> Self {
        self.set_bit(1, state)
    }

    pub fn bit2(self, state: BitState) -> Self {
        self.set_bit(2, state)
    }

    pub fn bit3(self, state: BitState) -> Self {
        self.set_bit(3, state)
    }

    pub fn bit4(self, state: BitState) -> Self {
        self.set_bit(4, state)
    }

    pub fn bit5(self, state: BitState) -> Self {
        self.set_bit(5, state)
    }

    pub fn bit6(self, state: BitState) -> Self {
        self.set_bit(6, state)
    }

    pub fn bit7(self, state: BitState) -> Self {
        self.set_bit(7, state)
    }

    pub fn bit8(self, state: BitState) -> Self {
        self.set_bit(8, state)
    }

    pub fn is_matching(self) -> DigitalTrigger {
        DigitalTrigger::new(self.bit_states, DigitalTriggerBehavior::While)
    }

    pub fn starts_matching(self) -> DigitalTrigger {
        DigitalTrigger::new(self.bit_states, DigitalTriggerBehavior::Start)
    }

    pub fn stops_matching(self) -> DigitalTrigger {
        DigitalTrigger::new(self.bit_states, DigitalTriggerBehavior::Stop)
    }

    /// Same as is_matching, but will also trigger when the bits did not match within 100ms.
    pub fn auto(self) -> DigitalTrigger {
        DigitalTrigger::new(self.bit_states, DigitalTriggerBehavior::Auto)
    }
}

impl Default for BitTriggerBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Debug, Clone)]
pub struct DigitalTrigger {
    pub bit_states: [BitState; 9],
    pub behavior: DigitalTriggerBehavior,
}

impl DigitalTrigger {
    pub fn new(bit_states: [BitState; 9], behavior: DigitalTriggerBehavior) -> Self {
        Self {
            bit_states,
            behavior,
        }
    }

    pub fn start_capturing_when() -> BitTriggerBuilder {
        BitTriggerBuilder::new()
    }

    pub fn into_trigger_fields(&self) -> StringifiedTriggerConfig {
        let mut relevant_bits = 0u32;
        for (i, state) in self.bit_states.iter().enumerate() {
            if *state != BitState::DontCare {
                relevant_bits |= 1 << i;
            }
        }

        let mut active_bits = 0u32;
        for (i, state) in self.bit_states.iter().enumerate() {
            if *state == BitState::High {
                active_bits |= 1 << i;
            }
        }

        let trigger_behavior_flag = self.behavior.as_str();
        StringifiedTriggerConfig {
            trigger_fields: format!(
                "{}0x{:02x} 0x{:02x}",
                trigger_behavior_flag, active_bits, relevant_bits
            ),
        }
    }
}

#[derive(Debug)]
pub struct AnalogTriggerBuilder;

impl AnalogTriggerBuilder {
    pub fn rising_edge(&self, volts: f64) -> AnalogTrigger {
        AnalogTrigger::new(volts, AnalogTriggerBehavior::Rising)
    }

    pub fn falling_edge(&self, volts: f64) -> AnalogTrigger {
        AnalogTrigger::new(volts, AnalogTriggerBehavior::Falling)
    }

    pub fn level(&self, volts: f64) -> AnalogTrigger {
        AnalogTrigger::new(volts, AnalogTriggerBehavior::Level)
    }

    /// Same as level, but will also trigger when the voltage did not match within 100ms.
    pub fn auto(&self, volts: f64) -> AnalogTrigger {
        AnalogTrigger::new(volts, AnalogTriggerBehavior::Auto)
    }
}

#[derive(Debug, Clone)]
pub struct AnalogTrigger {
    pub level: f64,
    pub behavior: AnalogTriggerBehavior,
}

impl AnalogTrigger {
    pub fn new(level: f64, behavior: AnalogTriggerBehavior) -> Self {
        Self { level, behavior }
    }

    pub fn start_capturing_when() -> AnalogTriggerBuilder {
        AnalogTriggerBuilder {}
    }

    pub fn into_trigger_fields<F>(
        &self,
        voltage_to_raw: F,
    ) -> Result<StringifiedTriggerConfig, CaptureConfigError>
    where
        F: Fn(f64) -> f64,
    {
        let trigger_behavior_flag = self.behavior.as_str();
        let raw_level = (voltage_to_raw(self.level) / 4.0 + 0.5) as i32;

        if !(-1023..=1023).contains(&raw_level) {
            return Err(CaptureConfigError::VoltageOutOfRange);
        }

        Ok(StringifiedTriggerConfig {
            trigger_fields: format!("{}{} 0", trigger_behavior_flag, raw_level),
        })
    }
}

/// A unified trigger type that can represent both analog and digital triggers.
/// This allows treating all triggers uniformly in the API.
#[derive(Debug, Clone)]
pub enum Trigger {
    Analog(AnalogTrigger),
    Digital(DigitalTrigger),
}

impl From<AnalogTrigger> for Trigger {
    fn from(trigger: AnalogTrigger) -> Self {
        Self::Analog(trigger)
    }
}

impl From<DigitalTrigger> for Trigger {
    fn from(trigger: DigitalTrigger) -> Self {
        Self::Digital(trigger)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_analog_trigger_out_of_range() {
        let trigger = AnalogTrigger::start_capturing_when().level(100.0);

        let voltage_to_raw = |v: f64| v * 100.0; // This will create a value out of range
        let result = trigger.into_trigger_fields(voltage_to_raw);
        assert!(result.is_err());
    }

    #[test]
    #[should_panic(expected = "Bit index 9 out of range")]
    fn test_digital_trigger_builder_panic_on_invalid_bit() {
        DigitalTrigger::start_capturing_when().set_bit(9, BitState::High); // This should panic since valid range is 0-8
    }
}