Skip to main content

pamoja_kit/
debounce.rs

1//! Debouncing a chattering on/off signal.
2
3/// Cleans a noisy boolean signal by requiring it to hold steady before reporting a change.
4///
5/// A mechanical switch, a relay contact, or a reading crossing a threshold does not flip
6/// cleanly: for a few milliseconds it chatters between states. A [`Debounce`] reports a
7/// change only after the new value has been seen for a set number of consecutive samples,
8/// so a button press, a float-switch trip, or a threshold crossing reads as one clean
9/// event. This is the standard counter debounce: N stable samples accept a change, and any
10/// contrary sample resets the count. At a fixed sample rate, N samples is the debounce time
11/// - sampling every 5 ms with `samples` of `4` debounces over 20 ms.
12///
13/// # Examples
14///
15/// ```
16/// use pamoja_kit::Debounce;
17///
18/// // A button needs three stable samples to register.
19/// let mut button = Debounce::new(3, false);
20/// assert!(!button.update(true)); // first press sample
21/// assert!(!button.update(false)); // the contact bounced back
22/// assert!(!button.update(true)); // counting restarts
23/// assert!(!button.update(true));
24/// assert!(button.update(true)); // three in a row: pressed
25/// ```
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Debounce {
28    state: bool,
29    candidate: bool,
30    count: u16,
31    samples: u16,
32}
33
34impl Debounce {
35    /// Creates a debouncer.
36    ///
37    /// # Arguments
38    ///
39    /// * `samples` - consecutive stable samples required to accept a change. `0` and `1`
40    ///   both accept a change on the first contrary sample.
41    /// * `initial` - the starting debounced state.
42    ///
43    /// # Returns
44    ///
45    /// A debouncer reporting `initial` until a change is confirmed.
46    pub fn new(samples: u16, initial: bool) -> Self {
47        Self {
48            state: initial,
49            candidate: initial,
50            count: 0,
51            samples,
52        }
53    }
54
55    /// Feeds a raw sample and returns the debounced state.
56    ///
57    /// # Arguments
58    ///
59    /// * `raw` - the latest raw signal value.
60    ///
61    /// # Returns
62    ///
63    /// The debounced state after this sample. It changes only once a contrary value has
64    /// held for the required number of samples.
65    pub fn update(&mut self, raw: bool) -> bool {
66        if raw == self.state {
67            self.count = 0;
68            self.candidate = self.state;
69        } else {
70            if raw == self.candidate {
71                self.count = self.count.saturating_add(1);
72            } else {
73                self.candidate = raw;
74                self.count = 1;
75            }
76            if self.count >= self.samples {
77                self.state = raw;
78                self.candidate = raw;
79                self.count = 0;
80            }
81        }
82        self.state
83    }
84
85    /// Returns the current debounced state.
86    pub fn state(&self) -> bool {
87        self.state
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn change_is_accepted_after_enough_stable_samples() {
97        let mut debounce = Debounce::new(3, false);
98        assert!(!debounce.update(true));
99        assert!(!debounce.update(true));
100        assert!(debounce.update(true)); // the third stable sample
101        assert!(debounce.state());
102    }
103
104    #[test]
105    fn chatter_resets_the_count() {
106        let mut debounce = Debounce::new(3, false);
107        debounce.update(true);
108        debounce.update(true);
109        assert!(!debounce.update(false)); // bounce back to the held state
110        assert!(!debounce.update(true)); // counting restarts
111        assert!(!debounce.update(true));
112        assert!(debounce.update(true)); // now three clean in a row
113    }
114
115    #[test]
116    fn one_sample_flips_immediately() {
117        let mut debounce = Debounce::new(1, false);
118        assert!(debounce.update(true));
119    }
120
121    #[test]
122    fn a_single_contrary_sample_does_not_flip() {
123        let mut debounce = Debounce::new(2, true);
124        assert!(debounce.update(false)); // one low sample: still high
125        assert!(!debounce.update(false)); // a second low sample: now low
126    }
127}