autocore-std 3.3.40

Standard library for AutoCore control programs - shared memory, IPC, and logging utilities
Documentation
use std::time::{Duration, Instant};

/// Blink Oscillator
///
/// A simple oscillator that toggles its output `q` on and off at a fixed
/// frequency (0.5 seconds on, 0.5 seconds off) while the enable input `en` is `true`.
///
/// When `en` is `false`, the output `q` is `false` and the internal timer resets.
///
/// # Behavior
///
/// - When `en` becomes `true`, `q` immediately becomes `true`.
/// - Every 500ms thereafter, `q` toggles its state.
/// - When `en` becomes `false`, `q` immediately becomes `false`.
///
/// # Example
///
/// ```
/// use autocore_std::fb::Blink;
/// use std::time::Duration;
///
/// let mut blink = Blink::new();
///
/// // Disabled - output is false
/// assert_eq!(blink.call(false), false);
///
/// // Enabled - output is immediately true
/// assert_eq!(blink.call(true), true);
///
/// // ... after 500ms ...
/// // assert_eq!(blink.call(true), false);
/// ```
#[derive(Debug, Clone)]
pub struct Blink {
    /// Input: Enable the oscillator
    pub en: bool,
    /// Output: Toggles every 500ms while enabled
    pub q: bool,

    last_toggle: Option<Instant>,
}

impl Blink {
    /// Creates a new Blink function block with default values.
    ///
    /// # Example
    ///
    /// ```
    /// use autocore_std::fb::Blink;
    ///
    /// let blink = Blink::new();
    /// assert_eq!(blink.q, false);
    /// ```
    pub fn new() -> Self {
        Self {
            en: false,
            q: false,
            last_toggle: None,
        }
    }

    /// Executes the blink logic.
    ///
    /// Call this method once per control cycle. It uses real elapsed time,
    /// so the toggling is independent of the scan rate.
    ///
    /// # Arguments
    ///
    /// * `en` - Enable input: `true` to oscillate, `false` to reset and turn off
    ///
    /// # Returns
    ///
    /// The current state of the output `q`.
    pub fn call(&mut self, en: bool) -> bool {
        self.en = en;

        if !self.en {
            self.q = false;
            self.last_toggle = None;
        } else {
            let now = Instant::now();
            match self.last_toggle {
                None => {
                    // First cycle enabled: turn on immediately
                    self.q = true;
                    self.last_toggle = Some(now);
                }
                Some(last) => {
                    if now.duration_since(last) >= Duration::from_millis(500) {
                        self.q = !self.q;
                        
                        // Add exactly 500ms to avoid drift, instead of resetting to `now`
                        // (unless we somehow missed multiple cycles, then fallback to now)
                        let expected_next = last + Duration::from_millis(500);
                        if now.duration_since(expected_next) > Duration::from_millis(500) {
                            self.last_toggle = Some(now);
                        } else {
                            self.last_toggle = Some(expected_next);
                        }
                    }
                }
            }
        }
        
        self.q
    }
}

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

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

    #[test]
    fn test_blink_basic() {
        let mut blink = Blink::new();

        // Disabled
        assert_eq!(blink.call(false), false);

        // Enable - goes true immediately
        assert_eq!(blink.call(true), true);

        // Still true shortly after
        std::thread::sleep(Duration::from_millis(50));
        assert_eq!(blink.call(true), true);

        // Wait past 500ms
        std::thread::sleep(Duration::from_millis(460));
        assert_eq!(blink.call(true), false);

        // Disable resets it
        assert_eq!(blink.call(false), false);

        // Re-enable goes true immediately again
        assert_eq!(blink.call(true), true);
    }
}