autocore-std 3.3.30

Standard library for AutoCore control programs - shared memory, IPC, and logging utilities
Documentation
use std::time::Duration;
use super::r_trig::RTrig;
use super::simple_timer::SimpleTimer;

/// Minimum allowed on/off duration. Values below this are clamped.
const MIN_DURATION: Duration = Duration::from_millis(50);
/// Default on-time used when `t_on` is below the minimum.
const DEFAULT_ON: Duration = Duration::from_millis(750);
/// Default off-time used when `t_off` is below the minimum.
const DEFAULT_OFF: Duration = Duration::from_millis(500);

/// Audible Beeper Controller (FB_Beeper)
///
/// Controls an audible beeper through a single boolean output. On the rising
/// edge of `execute`, emits exactly `preset` beeps with configurable on/off
/// durations. Call cyclically (once per scan).
///
/// # Behavior
///
/// - Idle until a rising edge is detected on `execute`
/// - On trigger: emits `preset` beeps, alternating `q` on for `t_on` then
///   off for `t_off`
/// - When all beeps are emitted, `done` becomes `true` and `q` stays `false`
/// - Setting `execute` to `false` at any time resets the FB and silences output
/// - `t_on` / `t_off` values below 50 ms are clamped to 750 ms / 500 ms
///
/// # Example
///
/// ```
/// use autocore_std::fb::Beeper;
/// use std::time::Duration;
///
/// let mut beeper = Beeper::new();
///
/// // Idle — not yet triggered
/// beeper.call(false, 3, Duration::from_millis(100), Duration::from_millis(100));
/// assert_eq!(beeper.q, false);
/// assert_eq!(beeper.done, false);
///
/// // Rising edge triggers 3 beeps
/// beeper.call(true, 3, Duration::from_millis(100), Duration::from_millis(100));
/// // One more call enters the beeping state
/// beeper.call(true, 3, Duration::from_millis(100), Duration::from_millis(100));
/// assert_eq!(beeper.q, true); // First beep started
/// ```
///
/// # Timing Diagram (preset = 2)
///
/// ```text
/// execute: ___|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
///       q: ____|‾‾‾‾‾|_____|‾‾‾‾‾|____________________
///   count: 0   0     1     1     2
///    done: ________________________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
///              ←t_on→←t_off→←t_on→←t_off→
/// ```
///
/// # Use Cases
///
/// - Audible alarms with a fixed number of beeps
/// - Confirmation tones (single short beep)
/// - Warning patterns (rapid multi-beep)
#[derive(Debug, Clone)]
pub struct Beeper {
    /// Output: beeper pulse. Connect to the output controlling the beeper.
    pub q: bool,
    /// Output: number of beeps emitted so far.
    pub count: u16,
    /// Output: `true` when the preset number of beeps has been emitted.
    pub done: bool,

    state: i32,
    trigger: RTrig,
    timer: SimpleTimer,
}

impl Beeper {
    /// Creates a new beeper in the idle state.
    ///
    /// # Example
    ///
    /// ```
    /// use autocore_std::fb::Beeper;
    ///
    /// let beeper = Beeper::new();
    /// assert_eq!(beeper.q, false);
    /// assert_eq!(beeper.count, 0);
    /// assert_eq!(beeper.done, false);
    /// ```
    pub fn new() -> Self {
        Self {
            q: false,
            count: 0,
            done: false,
            state: 0,
            trigger: RTrig::new(),
            timer: SimpleTimer::new(Duration::ZERO),
        }
    }

    /// Executes one scan cycle of the beeper logic.
    ///
    /// Call this once per control cycle. On the rising edge of `execute`,
    /// the beeper begins emitting `preset` beeps. Setting `execute` to
    /// `false` resets the FB immediately (silences output).
    ///
    /// # Arguments
    ///
    /// * `execute` - Enable; rising edge triggers the beep sequence
    /// * `preset` - Number of beeps to emit
    /// * `t_on`   - Duration the beeper stays on per beep (min 50 ms, clamped to 750 ms)
    /// * `t_off`  - Duration the beeper stays off between beeps (min 50 ms, clamped to 500 ms)
    ///
    /// # Example
    ///
    /// ```
    /// use autocore_std::fb::Beeper;
    /// use std::time::Duration;
    ///
    /// let mut beeper = Beeper::new();
    /// let t_on  = Duration::from_millis(750);
    /// let t_off = Duration::from_millis(750);
    ///
    /// // Call cyclically in your control loop
    /// # let some_trigger_condition = false;
    /// beeper.call(some_trigger_condition, 2, t_on, t_off);
    /// // Read beeper.q to drive the physical output
    /// ```
    pub fn call(&mut self, execute: bool, preset: u16, t_on: Duration, t_off: Duration) {
        self.trigger.call(execute);

        let t_on = if t_on < MIN_DURATION { DEFAULT_ON } else { t_on };
        let t_off = if t_off < MIN_DURATION { DEFAULT_OFF } else { t_off };

        if !execute {
            self.state = 0;
        }

        match self.state {
            // Init / reset
            0 => {
                self.q = false;
                self.state = 10;
                self.timer.set_preset(Duration::from_millis(20));
                self.timer.start();
            }
            // Idle — wait for rising edge
            10 => {
                if self.trigger.q {
                    self.state = 20;
                    self.count = 0;
                    self.done = false;
                }
            }
            // Check if sequence complete, otherwise start next beep
            20 => {
                if self.count == preset {
                    self.done = true;
                    self.state = 10;
                } else {
                    self.q = true;
                    self.done = false;
                    self.timer.set_preset(t_on);
                    self.timer.start();
                    self.state = 30;
                }
            }
            // Beep on — wait for on-timer
            30 => {
                if self.timer.is_done() {
                    self.count += 1;
                    self.q = false;
                    self.timer.set_preset(t_off);
                    self.timer.start();
                    self.state = 40;
                }
            }
            // Beep off — wait for off-timer
            40 => {
                if self.timer.is_done() {
                    self.state = 20;
                }
            }
            _ => {
                self.state = 0;
            }
        }
    }
}

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

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

    /// Helper: drive the beeper through a full sequence and return it.
    fn run_sequence(preset: u16, t_on: Duration, t_off: Duration) -> Beeper {
        let mut b = Beeper::new();

        // Initialize (state 0 → 10)
        b.call(false, preset, t_on, t_off);
        assert_eq!(b.q, false);

        // Rising edge (state 10 → 20)
        b.call(true, preset, t_on, t_off);
        assert_eq!(b.count, 0);
        assert_eq!(b.done, false);

        for i in 0..preset {
            // State 20 → 30: Q goes high
            b.call(true, preset, t_on, t_off);
            assert!(b.q, "q should be true at start of beep {}", i);

            // Wait for on-timer
            std::thread::sleep(t_on + Duration::from_millis(15));

            // State 30 → 40: Q goes low, count increments
            b.call(true, preset, t_on, t_off);
            assert!(!b.q, "q should be false after beep {}", i);
            assert_eq!(b.count, i + 1);

            // Wait for off-timer
            std::thread::sleep(t_off + Duration::from_millis(15));

            // State 40 → 20
            b.call(true, preset, t_on, t_off);
        }

        // State 20: count == preset → done
        b.call(true, preset, t_on, t_off);
        assert!(b.done);
        assert!(!b.q);
        b
    }

    #[test]
    fn test_idle_state() {
        let mut b = Beeper::new();
        b.call(false, 3, Duration::from_millis(100), Duration::from_millis(100));
        assert_eq!(b.q, false);
        assert_eq!(b.done, false);
        assert_eq!(b.count, 0);
    }

    #[test]
    fn test_single_beep() {
        run_sequence(1, Duration::from_millis(50), Duration::from_millis(50));
    }

    #[test]
    fn test_multiple_beeps() {
        run_sequence(3, Duration::from_millis(50), Duration::from_millis(50));
    }

    #[test]
    fn test_execute_false_resets() {
        let mut b = Beeper::new();
        let t = Duration::from_millis(50);

        // Init + trigger
        b.call(false, 2, t, t);
        b.call(true, 2, t, t);
        b.call(true, 2, t, t);
        assert!(b.q); // Beeping

        // Pull execute low — should silence immediately
        b.call(false, 2, t, t);
        assert_eq!(b.q, false);
        assert_eq!(b.done, false);
    }

    #[test]
    fn test_zero_preset_completes_immediately() {
        let mut b = Beeper::new();
        let t = Duration::from_millis(100);

        // Init
        b.call(false, 0, t, t);
        // Rising edge
        b.call(true, 0, t, t);
        // State 20: count(0) == preset(0) → done
        b.call(true, 0, t, t);
        assert!(b.done);
        assert_eq!(b.q, false);
        assert_eq!(b.count, 0);
    }

    #[test]
    fn test_minimum_duration_clamping() {
        let mut b = Beeper::new();
        let too_short = Duration::from_millis(10);

        // Init + trigger
        b.call(false, 1, too_short, too_short);
        b.call(true, 1, too_short, too_short);
        // Start beep — internally t_on is clamped to 750ms
        b.call(true, 1, too_short, too_short);
        assert!(b.q);

        // 50ms is NOT enough (clamped to 750ms)
        std::thread::sleep(Duration::from_millis(60));
        b.call(true, 1, too_short, too_short);
        assert!(b.q, "timer should still be running (clamped to 750ms)");
    }

    #[test]
    fn test_retrigger_after_done() {
        let mut b = run_sequence(1, Duration::from_millis(50), Duration::from_millis(50));
        assert!(b.done);

        let t = Duration::from_millis(50);

        // Pull execute low then high again for a new rising edge
        b.call(false, 1, t, t);
        b.call(true, 1, t, t);
        assert_eq!(b.count, 0);
        assert_eq!(b.done, false);

        // Should start beeping again
        b.call(true, 1, t, t);
        assert!(b.q);
    }

    #[test]
    fn test_default_trait() {
        let b = Beeper::default();
        assert_eq!(b.q, false);
        assert_eq!(b.count, 0);
        assert_eq!(b.done, false);
    }
}