Skip to main content

debounce_button_eng/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3
4use embedded_hal::digital::InputPin;
5
6/// Monotonic time in milliseconds.
7pub type Millis = u64;
8
9/// Electrical level that represents a pressed button.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ActiveLevel {
12    /// The button is pressed when the input pin reads high.
13    High,
14    /// The button is pressed when the input pin reads low.
15    Low,
16}
17
18impl ActiveLevel {
19    #[inline]
20    const fn state_from_high(self, is_high: bool) -> ButtonState {
21        match (self, is_high) {
22            (Self::High, true) | (Self::Low, false) => ButtonState::Pressed,
23            (Self::High, false) | (Self::Low, true) => ButtonState::Released,
24        }
25    }
26}
27
28/// Stable button state after debounce filtering.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ButtonState {
31    /// The button is pressed.
32    Pressed,
33    /// The button is released.
34    Released,
35}
36
37impl ButtonState {
38    /// Returns `true` when the state is [`ButtonState::Pressed`].
39    #[inline]
40    pub const fn is_pressed(self) -> bool {
41        matches!(self, Self::Pressed)
42    }
43
44    /// Returns `true` when the state is [`ButtonState::Released`].
45    #[inline]
46    pub const fn is_released(self) -> bool {
47        matches!(self, Self::Released)
48    }
49}
50
51/// Button timing and polarity configuration.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct ButtonConfig {
54    /// Time the raw pin state must remain unchanged before it is accepted as
55    /// the stable debounced state.
56    pub debounce_ms: Millis,
57    /// Maximum interval between two short releases that still counts as a
58    /// double click.
59    pub double_click_ms: Millis,
60    /// Time after which a continuously pressed button reports a hold event.
61    pub hold_ms: Millis,
62    /// Electrical level that represents the pressed state.
63    pub active_level: ActiveLevel,
64}
65
66impl ButtonConfig {
67    /// Default debounce time.
68    pub const DEFAULT_DEBOUNCE_MS: Millis = 20;
69    /// Default double-click interval.
70    pub const DEFAULT_DOUBLE_CLICK_MS: Millis = 300;
71    /// Default hold threshold.
72    pub const DEFAULT_HOLD_MS: Millis = 800;
73
74    /// Creates a configuration for the common pull-up wiring where the pressed
75    /// state is low.
76    #[inline]
77    pub const fn active_low() -> Self {
78        Self {
79            debounce_ms: Self::DEFAULT_DEBOUNCE_MS,
80            double_click_ms: Self::DEFAULT_DOUBLE_CLICK_MS,
81            hold_ms: Self::DEFAULT_HOLD_MS,
82            active_level: ActiveLevel::Low,
83        }
84    }
85
86    /// Creates a configuration where the pressed state is high.
87    #[inline]
88    pub const fn active_high() -> Self {
89        Self {
90            active_level: ActiveLevel::High,
91            ..Self::active_low()
92        }
93    }
94
95    /// Returns the configuration with a different debounce time.
96    #[inline]
97    pub const fn with_debounce_ms(mut self, debounce_ms: Millis) -> Self {
98        self.debounce_ms = debounce_ms;
99        self
100    }
101
102    /// Returns the configuration with a different double-click interval.
103    #[inline]
104    pub const fn with_double_click_ms(mut self, double_click_ms: Millis) -> Self {
105        self.double_click_ms = double_click_ms;
106        self
107    }
108
109    /// Returns the configuration with a different hold threshold.
110    #[inline]
111    pub const fn with_hold_ms(mut self, hold_ms: Millis) -> Self {
112        self.hold_ms = hold_ms;
113        self
114    }
115}
116
117impl Default for ButtonConfig {
118    #[inline]
119    fn default() -> Self {
120        Self::active_low()
121    }
122}
123
124/// Events produced by one [`Button::update`] call.
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126pub struct ButtonEvents {
127    /// The stable state changed to pressed.
128    pub pressed: bool,
129    /// The stable state changed to released.
130    pub released: bool,
131    /// A short single click was confirmed.
132    pub click: bool,
133    /// Two short clicks completed inside the configured interval.
134    pub double_click: bool,
135    /// The button reached the configured hold threshold.
136    pub hold: bool,
137}
138
139impl ButtonEvents {
140    /// Returns `true` if at least one event flag is set.
141    #[inline]
142    pub const fn any(self) -> bool {
143        self.pressed || self.released || self.click || self.double_click || self.hold
144    }
145}
146
147/// Result of one button update.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct ButtonUpdate {
150    /// Current stable debounced state after the update.
151    pub state: ButtonState,
152    /// Events produced by this update.
153    pub events: ButtonEvents,
154    /// Current hold time while the button is pressed.
155    pub held_ms: Option<Millis>,
156    /// Total hold time when this update released the button.
157    pub released_after_ms: Option<Millis>,
158}
159
160impl ButtonUpdate {
161    #[inline]
162    const fn idle(state: ButtonState, held_ms: Option<Millis>) -> Self {
163        Self {
164            state,
165            events: ButtonEvents {
166                pressed: false,
167                released: false,
168                click: false,
169                double_click: false,
170                hold: false,
171            },
172            held_ms,
173            released_after_ms: None,
174        }
175    }
176}
177
178/// Debounced wrapper around an `embedded-hal` input pin.
179///
180/// Call [`Button::update`] regularly and pass a monotonic millisecond
181/// timestamp. The timestamp can come from a hardware timer, a system tick, or
182/// any other counter that never goes backwards.
183pub struct Button<PIN> {
184    pin: PIN,
185    config: ButtonConfig,
186    raw_state: ButtonState,
187    raw_changed_at: Millis,
188    stable_state: ButtonState,
189    pressed_at: Option<Millis>,
190    pending_click_at: Option<Millis>,
191    hold_reported: bool,
192}
193
194impl<PIN> Button<PIN> {
195    /// Creates an active-low button with default timings.
196    #[inline]
197    pub const fn new(pin: PIN) -> Self {
198        Self::with_config(pin, ButtonConfig::active_low())
199    }
200
201    /// Creates a button with custom timings and polarity.
202    #[inline]
203    pub const fn with_config(pin: PIN, config: ButtonConfig) -> Self {
204        Self {
205            pin,
206            config,
207            raw_state: ButtonState::Released,
208            raw_changed_at: 0,
209            stable_state: ButtonState::Released,
210            pressed_at: None,
211            pending_click_at: None,
212            hold_reported: false,
213        }
214    }
215
216    /// Returns the current configuration.
217    #[inline]
218    pub const fn config(&self) -> ButtonConfig {
219        self.config
220    }
221
222    /// Replaces the whole configuration.
223    #[inline]
224    pub fn set_config(&mut self, config: ButtonConfig) {
225        self.config = config;
226    }
227
228    /// Returns the current debounce time.
229    #[inline]
230    pub const fn debounce_ms(&self) -> Millis {
231        self.config.debounce_ms
232    }
233
234    /// Sets the debounce time.
235    #[inline]
236    pub fn set_debounce_ms(&mut self, debounce_ms: Millis) {
237        self.config.debounce_ms = debounce_ms;
238    }
239
240    /// Returns the current double-click interval.
241    #[inline]
242    pub const fn double_click_ms(&self) -> Millis {
243        self.config.double_click_ms
244    }
245
246    /// Sets the double-click interval.
247    #[inline]
248    pub fn set_double_click_ms(&mut self, double_click_ms: Millis) {
249        self.config.double_click_ms = double_click_ms;
250    }
251
252    /// Returns the current hold threshold.
253    #[inline]
254    pub const fn hold_ms(&self) -> Millis {
255        self.config.hold_ms
256    }
257
258    /// Sets the hold threshold.
259    #[inline]
260    pub fn set_hold_ms(&mut self, hold_ms: Millis) {
261        self.config.hold_ms = hold_ms;
262    }
263
264    /// Returns the stable debounced state.
265    #[inline]
266    pub const fn state(&self) -> ButtonState {
267        self.stable_state
268    }
269
270    /// Returns `true` if the stable debounced state is
271    /// [`ButtonState::Pressed`].
272    #[inline]
273    pub const fn is_pressed(&self) -> bool {
274        self.stable_state.is_pressed()
275    }
276
277    /// Returns `true` if the stable debounced state is
278    /// [`ButtonState::Released`].
279    #[inline]
280    pub const fn is_released(&self) -> bool {
281        self.stable_state.is_released()
282    }
283
284    /// Returns the current debounced hold time.
285    #[inline]
286    pub fn held_ms(&self, now_ms: Millis) -> Option<Millis> {
287        self.pressed_at
288            .map(|pressed_at| elapsed(now_ms, pressed_at))
289    }
290
291    /// Returns a mutable reference to the inner pin.
292    #[inline]
293    pub fn pin_mut(&mut self) -> &mut PIN {
294        &mut self.pin
295    }
296
297    /// Returns the inner pin.
298    #[inline]
299    pub fn into_inner(self) -> PIN {
300        self.pin
301    }
302}
303
304impl<PIN> Button<PIN>
305where
306    PIN: InputPin,
307{
308    /// Reads the pin once, updates the debouncer, and returns the current state
309    /// with events.
310    ///
311    /// This method does not block and does not wait for debounce to finish. It
312    /// should be called regularly with a fresh monotonic timestamp in
313    /// `now_ms`.
314    ///
315    /// One call:
316    ///
317    /// 1. Reads the physical pin through `InputPin::is_high`.
318    /// 2. Converts the electrical level to [`ButtonState`] using
319    ///    [`ActiveLevel`].
320    /// 3. Records a raw state change and its timestamp.
321    /// 4. Accepts a raw state as stable when it has been unchanged for at least
322    ///    `debounce_ms`.
323    /// 5. Emits `events.pressed` when the stable state becomes
324    ///    [`ButtonState::Pressed`].
325    /// 6. Emits `events.released` and `released_after_ms` when the stable state
326    ///    becomes [`ButtonState::Released`].
327    /// 7. Stores a pending single click after a short release.
328    /// 8. Emits `events.double_click` if a second short click arrives inside
329    ///    `double_click_ms`, without emitting an earlier `events.click`.
330    /// 9. Emits `events.click` once if the pending click is not followed by a
331    ///    second click before `double_click_ms` expires.
332    /// 10. Updates `held_ms` while the button is pressed and emits
333    ///     `events.hold` once the hold threshold is reached.
334    ///
335    /// Event flags are valid only for the returned update. Read the stable
336    /// state from [`ButtonUpdate::state`].
337    pub fn update(&mut self, now_ms: Millis) -> Result<ButtonUpdate, PIN::Error> {
338        let raw_state = self.read_raw_state()?;
339
340        if raw_state != self.raw_state {
341            self.raw_state = raw_state;
342            self.raw_changed_at = now_ms;
343        }
344
345        let mut update = ButtonUpdate::idle(self.stable_state, self.held_ms(now_ms));
346
347        if self.stable_state != self.raw_state
348            && elapsed(now_ms, self.raw_changed_at) >= self.config.debounce_ms
349        {
350            self.accept_stable_state(now_ms, &mut update);
351        }
352
353        if self.stable_state == ButtonState::Pressed {
354            let held_ms = self.held_ms(now_ms).unwrap_or(0);
355            update.held_ms = Some(held_ms);
356
357            if !self.hold_reported && held_ms >= self.config.hold_ms {
358                update.events.hold = true;
359                self.hold_reported = true;
360            }
361        }
362
363        self.report_pending_click(now_ms, &mut update);
364        update.state = self.stable_state;
365        Ok(update)
366    }
367
368    #[inline]
369    fn read_raw_state(&mut self) -> Result<ButtonState, PIN::Error> {
370        self.pin
371            .is_high()
372            .map(|is_high| self.config.active_level.state_from_high(is_high))
373    }
374
375    fn accept_stable_state(&mut self, now_ms: Millis, update: &mut ButtonUpdate) {
376        self.stable_state = self.raw_state;
377
378        match self.stable_state {
379            ButtonState::Pressed => {
380                self.pressed_at = Some(now_ms);
381                self.hold_reported = false;
382                update.events.pressed = true;
383                update.held_ms = Some(0);
384            }
385            ButtonState::Released => {
386                let held_ms = self
387                    .pressed_at
388                    .map(|pressed_at| elapsed(now_ms, pressed_at));
389
390                self.pressed_at = None;
391                self.hold_reported = false;
392                update.events.released = true;
393                update.held_ms = None;
394                update.released_after_ms = held_ms;
395
396                if held_ms.is_some_and(|held_ms| held_ms < self.config.hold_ms) {
397                    self.register_short_release(now_ms, update);
398                }
399            }
400        }
401    }
402
403    fn register_short_release(&mut self, now_ms: Millis, update: &mut ButtonUpdate) {
404        if self.pending_click_at.is_some_and(|pending_click_at| {
405            elapsed(now_ms, pending_click_at) <= self.config.double_click_ms
406        }) {
407            update.events.double_click = true;
408            self.pending_click_at = None;
409        } else {
410            self.report_pending_click(now_ms, update);
411            self.pending_click_at = Some(now_ms);
412        }
413    }
414
415    fn report_pending_click(&mut self, now_ms: Millis, update: &mut ButtonUpdate) {
416        if self.pending_click_at.is_some_and(|pending_click_at| {
417            elapsed(now_ms, pending_click_at) > self.config.double_click_ms
418        }) {
419            update.events.click = true;
420            self.pending_click_at = None;
421        }
422    }
423}
424
425#[inline]
426const fn elapsed(now_ms: Millis, earlier_ms: Millis) -> Millis {
427    now_ms.saturating_sub(earlier_ms)
428}
429
430#[cfg(test)]
431extern crate std;
432
433#[cfg(test)]
434mod tests {
435    use std::io::ErrorKind;
436
437    use embedded_hal_mock::eh1::{
438        digital::{Mock as PinMock, State as PinState, Transaction as PinTransaction},
439        MockError,
440    };
441
442    use super::*;
443
444    fn config() -> ButtonConfig {
445        ButtonConfig::active_low()
446            .with_debounce_ms(10)
447            .with_double_click_ms(200)
448            .with_hold_ms(1000)
449    }
450
451    #[test]
452    fn filters_bounce_and_reports_press_release() {
453        let expectations = [
454            PinTransaction::get(PinState::High),
455            PinTransaction::get(PinState::Low),
456            PinTransaction::get(PinState::High),
457            PinTransaction::get(PinState::Low),
458            PinTransaction::get(PinState::Low),
459            PinTransaction::get(PinState::Low),
460            PinTransaction::get(PinState::High),
461            PinTransaction::get(PinState::High),
462            PinTransaction::get(PinState::High),
463            PinTransaction::get(PinState::High),
464        ];
465        let pin = PinMock::new(&expectations);
466        let mut button = Button::with_config(pin, config());
467
468        assert_eq!(button.update(0).unwrap().state, ButtonState::Released);
469        assert!(!button.update(1).unwrap().events.any());
470        assert!(!button.update(5).unwrap().events.any());
471        assert!(!button.update(7).unwrap().events.any());
472        assert!(!button.update(16).unwrap().events.any());
473
474        let update = button.update(17).unwrap();
475        assert_eq!(update.state, ButtonState::Pressed);
476        assert!(update.events.pressed);
477        assert_eq!(update.held_ms, Some(0));
478
479        assert!(!button.update(30).unwrap().events.any());
480        assert!(!button.update(35).unwrap().events.any());
481
482        let update = button.update(40).unwrap();
483        assert_eq!(update.state, ButtonState::Released);
484        assert!(update.events.released);
485        assert!(!update.events.click);
486        assert!(!update.events.double_click);
487        assert_eq!(update.released_after_ms, Some(23));
488
489        let update = button.update(241).unwrap();
490        assert_eq!(update.state, ButtonState::Released);
491        assert!(update.events.click);
492        assert!(!update.events.double_click);
493        assert_eq!(update.released_after_ms, None);
494
495        button.into_inner().done();
496    }
497
498    #[test]
499    fn detects_double_click() {
500        let expectations = [
501            PinTransaction::get(PinState::Low),
502            PinTransaction::get(PinState::Low),
503            PinTransaction::get(PinState::High),
504            PinTransaction::get(PinState::High),
505            PinTransaction::get(PinState::Low),
506            PinTransaction::get(PinState::Low),
507            PinTransaction::get(PinState::High),
508            PinTransaction::get(PinState::High),
509        ];
510        let pin = PinMock::new(&expectations);
511        let mut button = Button::with_config(pin, config().with_debounce_ms(5).with_hold_ms(1000));
512
513        assert!(!button.update(0).unwrap().events.any());
514        assert!(button.update(5).unwrap().events.pressed);
515
516        assert!(!button.update(20).unwrap().events.any());
517        let update = button.update(25).unwrap();
518        assert!(update.events.released);
519        assert!(!update.events.click);
520        assert!(!update.events.double_click);
521
522        assert!(!button.update(80).unwrap().events.any());
523        assert!(button.update(85).unwrap().events.pressed);
524
525        assert!(!button.update(100).unwrap().events.any());
526        let update = button.update(105).unwrap();
527        assert!(update.events.released);
528        assert!(!update.events.click);
529        assert!(update.events.double_click);
530
531        button.into_inner().done();
532    }
533
534    #[test]
535    fn reports_hold_once_and_tracks_hold_time() {
536        let expectations = [
537            PinTransaction::get(PinState::Low),
538            PinTransaction::get(PinState::Low),
539            PinTransaction::get(PinState::Low),
540            PinTransaction::get(PinState::Low),
541            PinTransaction::get(PinState::Low),
542            PinTransaction::get(PinState::High),
543            PinTransaction::get(PinState::High),
544        ];
545        let pin = PinMock::new(&expectations);
546        let mut button = Button::with_config(pin, config().with_debounce_ms(5).with_hold_ms(50));
547
548        assert!(!button.update(0).unwrap().events.any());
549        assert!(button.update(5).unwrap().events.pressed);
550
551        let update = button.update(54).unwrap();
552        assert_eq!(update.held_ms, Some(49));
553        assert!(!update.events.hold);
554
555        let update = button.update(55).unwrap();
556        assert_eq!(update.held_ms, Some(50));
557        assert!(update.events.hold);
558
559        let update = button.update(80).unwrap();
560        assert_eq!(update.held_ms, Some(75));
561        assert!(!update.events.hold);
562
563        assert!(!button.update(100).unwrap().events.any());
564        let update = button.update(105).unwrap();
565        assert!(update.events.released);
566        assert!(!update.events.click);
567        assert_eq!(update.released_after_ms, Some(100));
568
569        button.into_inner().done();
570    }
571
572    #[test]
573    fn supports_active_high_and_runtime_debounce_setting() {
574        let expectations = [
575            PinTransaction::get(PinState::Low),
576            PinTransaction::get(PinState::High),
577            PinTransaction::get(PinState::High),
578        ];
579        let pin = PinMock::new(&expectations);
580        let mut button = Button::with_config(pin, ButtonConfig::active_high());
581
582        button.set_debounce_ms(3);
583        assert_eq!(button.debounce_ms(), 3);
584
585        assert_eq!(button.update(0).unwrap().state, ButtonState::Released);
586        assert!(!button.update(1).unwrap().events.any());
587
588        let update = button.update(4).unwrap();
589        assert_eq!(update.state, ButtonState::Pressed);
590        assert!(update.events.pressed);
591
592        button.into_inner().done();
593    }
594
595    #[test]
596    fn propagates_pin_errors() {
597        let expectations =
598            [PinTransaction::get(PinState::High).with_error(MockError::Io(ErrorKind::Other))];
599        let pin = PinMock::new(&expectations);
600        let mut button = Button::new(pin);
601
602        assert!(button.update(0).is_err());
603
604        button.into_inner().done();
605    }
606}