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}