hermes_five/devices/input/
button.rs

1use std::fmt::{Display, Formatter};
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use crate::devices::{Device, Input, InputEvent};
7use crate::errors::Error;
8use crate::hardware::Hardware;
9use crate::io::{IoProtocol, PinIdOrName, PinModeId};
10use crate::pause;
11use crate::utils::{task, EventHandler, EventManager, State, TaskHandler};
12
13/// Represents a simple push button as an input of the board.
14/// <https://docs.arduino.cc/built-in-examples/digital/Button>
15///
16/// This structure is very similar to [`DigitalInput`](crate::devices::DigitalInput) but exposes convenience methods to handle two sorts of buttons:
17/// - pull-down: when the button is configured with a pin to ground by default (ie button press => pin becomes high)
18/// - pull-up: when the button is configured with a pin to +Vin by default (ie button press => pin becomes low)
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[derive(Clone, Debug)]
21pub struct Button {
22    // ########################################
23    // # Basics
24    /// The pin (id) of the [`Board`] used to read the button value.
25    pin: u8,
26    /// The current Button state.
27    #[cfg_attr(feature = "serde", serde(with = "crate::devices::arc_rwlock_serde"))]
28    state: Arc<RwLock<bool>>,
29    /// Inverts the true/false state value.
30    invert: bool,
31    /// Defines a PULL-UP mode button.
32    pullup: bool,
33
34    // ########################################
35    // # Volatile utility data.
36    #[cfg_attr(feature = "serde", serde(skip))]
37    protocol: Box<dyn IoProtocol>,
38    /// Inner handler to the task running the button value check.
39    #[cfg_attr(feature = "serde", serde(skip))]
40    handler: Arc<RwLock<Option<TaskHandler>>>,
41    /// The event manager for the button.
42    #[cfg_attr(feature = "serde", serde(skip))]
43    events: EventManager,
44}
45
46impl Button {
47    /// Creates an instance of a PULL-DOWN button attached to a given board:
48    /// <https://docs.arduino.cc/built-in-examples/digital/Button/>
49    ///
50    /// - Button pressed => pin state HIGH
51    /// - Button released => pin state LOW
52    ///
53    /// # Errors
54    /// * `UnknownPin`: this function will bail an error if the Button pin does not exist for this board.
55    /// * `IncompatiblePin`: this function will bail an error if the Button pin does not support INPUT mode.
56    pub fn new<T: Into<PinIdOrName>>(board: &dyn Hardware, pin: T) -> Result<Self, Error> {
57        Self {
58            pin: 0,
59            state: Arc::new(RwLock::new(false)),
60            invert: false,
61            pullup: false,
62            protocol: board.get_protocol(),
63            handler: Arc::new(RwLock::new(None)),
64            events: Default::default(),
65        }
66        .start_with(board, pin)
67    }
68
69    /// Creates an instance of an inverted PULL-DOWN button attached to a given board:
70    /// <https://docs.arduino.cc/built-in-examples/digital/Button/>
71    ///
72    /// /!\ The state value is inverted compared to HIGH/LOW electrical value of the pin.
73    /// - Inverted button pressed => pin state LOW
74    /// - Inverted button released => pin state HIGH
75    ///
76    /// # Errors
77    /// * `UnknownPin`: this function will bail an error if the Button pin does not exist for this board.
78    /// * `IncompatiblePin`: this function will bail an error if the Button pin does not support INPUT mode.
79    pub fn new_inverted<T: Into<PinIdOrName>>(board: &dyn Hardware, pin: T) -> Result<Self, Error> {
80        Self {
81            pin: 0,
82            state: Arc::new(RwLock::new(false)),
83            invert: true,
84            pullup: false,
85            protocol: board.get_protocol(),
86            handler: Arc::new(RwLock::new(None)),
87            events: Default::default(),
88        }
89        .start_with(board, pin)
90    }
91
92    /// Creates an instance of a PULL-UP button attached to a given board:
93    /// <https://docs.arduino.cc/tutorials/generic/digital-input-pullup/>
94    ///
95    /// - Pullup button pressed => pin state LOW
96    /// - Pullup button released => pin state HIGH
97    ///
98    /// # Errors
99    /// * `UnknownPin`: this function will bail an error if the Button pin does not exist for this board.
100    /// * `IncompatiblePin`: this function will bail an error if the Button pin does not support INPUT mode.
101    pub fn new_pullup<T: Into<PinIdOrName>>(board: &dyn Hardware, pin: T) -> Result<Self, Error> {
102        Self {
103            pin: 0,
104            state: Arc::new(RwLock::new(false)),
105            invert: false,
106            pullup: true,
107            protocol: board.get_protocol(),
108            handler: Arc::new(RwLock::new(None)),
109            events: Default::default(),
110        }
111        .start_with(board, pin)
112    }
113
114    /// Creates an instance of an inverted PULL-UP button attached to a given board:
115    /// <https://docs.arduino.cc/tutorials/generic/digital-input-pullup/>
116    ///
117    /// /!\ The state value is inverted compared to HIGH/LOW electrical value of the pin
118    /// (therefore equivalent to a standard pull-down button)
119    /// - Inverted pullup button pressed => pin state HIGH
120    /// - Inverted pullup button released => pin state LOW
121    ///
122    /// # Errors
123    /// * `UnknownPin`: this function will bail an error if the Button pin does not exist for this board.
124    /// * `IncompatiblePin`: this function will bail an error if the Button pin does not support INPUT mode.
125    pub fn new_inverted_pullup<T: Into<PinIdOrName>>(
126        board: &dyn Hardware,
127        pin: T,
128    ) -> Result<Self, Error> {
129        Self {
130            pin: 0,
131            state: Arc::new(RwLock::new(false)),
132            invert: true,
133            pullup: true,
134            protocol: board.get_protocol(),
135            handler: Arc::new(RwLock::new(None)),
136            events: Default::default(),
137        }
138        .start_with(board, pin)
139    }
140
141    /// Private helper method shared by constructors.
142    fn start_with<T: Into<PinIdOrName>>(
143        mut self,
144        board: &dyn Hardware,
145        pin: T,
146    ) -> Result<Self, Error> {
147        let pin = board.get_io().read().get_pin(pin)?.clone();
148
149        // Set pin ID and state from pin.
150        self.pin = pin.id;
151        *self.state.write() = pin.value != 0;
152
153        // Set pin mode to INPUT/PULLUP.
154        match self.pullup {
155            true => {
156                self.protocol.set_pin_mode(self.pin, PinModeId::PULLUP)?;
157                self.protocol.get_io().write().get_pin_mut(self.pin)?.value = 1;
158            }
159            false => {
160                self.protocol.set_pin_mode(self.pin, PinModeId::INPUT)?;
161            }
162        };
163
164        // Set reporting for this pin.
165        self.protocol.report_digital(self.pin, true)?;
166
167        // Create a task to listen hardware value and emit events accordingly.
168        self.attach();
169
170        Ok(self)
171    }
172
173    // ########################################
174
175    /// Returns the pin (id) used by the device.
176    pub fn get_pin(&self) -> u8 {
177        self.pin
178    }
179
180    /// Returns  if the button is configured in PULL-UP mode.
181    pub fn is_pullup(&self) -> bool {
182        self.pullup
183    }
184
185    /// Returns  if the logical button value is inverted.
186    pub fn is_inverted(&self) -> bool {
187        self.invert
188    }
189
190    // ########################################
191    // Event related functions
192
193    /// Manually attaches the button with the value change events.
194    /// This should never be needed unless you manually `detach()` the button first for some reason
195    /// and want it to start being reactive to events again.
196    pub fn attach(&self) {
197        if self.handler.read().is_none() {
198            let self_clone = self.clone();
199            *self.handler.write() = Some(
200                task::run(async move {
201                    loop {
202                        let pin_value = self_clone
203                            .protocol
204                            .get_io()
205                            .read()
206                            .get_pin(self_clone.pin)?
207                            .value
208                            != 0;
209                        let state_value = *self_clone.state.read();
210                        if pin_value != state_value {
211                            *self_clone.state.write() = pin_value;
212
213                            // Depending on logical inversion mode, pin_value is inverted.
214                            match self_clone.invert {
215                                false => self_clone.events.emit(InputEvent::OnChange, pin_value),
216                                true => self_clone.events.emit(InputEvent::OnChange, !pin_value),
217                            };
218
219                            match self_clone.pullup {
220                                true => match pin_value {
221                                    true => self_clone.events.emit(InputEvent::OnRelease, ()),
222                                    false => self_clone.events.emit(InputEvent::OnPress, ()),
223                                },
224                                false => match pin_value {
225                                    true => self_clone.events.emit(InputEvent::OnPress, ()),
226                                    false => self_clone.events.emit(InputEvent::OnRelease, ()),
227                                },
228                            };
229                        }
230
231                        // Change can only be done 10x a sec. to avoid bouncing.
232                        pause!(100);
233                    }
234                    #[allow(unreachable_code)]
235                    Ok(())
236                })
237                .unwrap(),
238            );
239        }
240    }
241
242    /// Detaches the interval associated with the button.
243    /// This means the button won't react anymore to value changes.
244    pub fn detach(&self) {
245        if let Some(handler) = self.handler.read().as_ref() {
246            handler.abort();
247        }
248        *self.handler.write() = None
249    }
250
251    /// Registers a callback to be executed on a given event on the Button.
252    ///
253    /// Available events for a button are:
254    /// - **`InputEvent::OnChange` | `change`:** Triggered when the button value changes.    
255    ///   _The callback must receive the following parameter: `|value: u16| { ... }`_
256    /// - **`InputEvent::OnRelease` | `released`:** Triggered when the button value changes.     
257    ///   _The callback must receive the void parameter: `|_:()| { ... }`_
258    /// - **`InputEvent::OnPress` | `pressed`:** Triggered when the button value changes.     
259    ///   _The callback must receive the void parameter: `|_:()| { ... }`_
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// use hermes_five::devices::{Button, InputEvent};
265    /// use hermes_five::hardware::{Board, BoardEvent};
266    ///
267    /// #[hermes_five::runtime]
268    /// async fn main() {
269    ///     let board = Board::run();
270    ///     board.on(BoardEvent::OnReady, |board: Board| async move {
271    ///
272    ///         // Register a Button on pin 2.
273    ///         let button = Button::new(&board, 2)?;
274    ///         // Triggered function when the button is pressed.
275    ///         button.on(InputEvent::OnPress, |_: ()| async move {
276    ///             println!("Push button pressed");
277    ///             Ok(())
278    ///         });
279    ///
280    ///         // The above code will run forever.
281    ///         // <do something useful>
282    ///
283    ///         // The above code will run forever runs a listener on the pin state under-the-hood.
284    ///         // It means the program will run forever listening to the InputEvent,
285    ///         // until we detach the device and close the board.
286    ///         button.detach();
287    ///         board.close();
288    ///
289    ///         Ok(())
290    ///     });
291    /// }
292    /// ```
293    pub fn on<S, F, T, Fut>(&self, event: S, callback: F) -> EventHandler
294    where
295        S: Into<String>,
296        T: 'static + Send + Sync + Clone,
297        F: FnMut(T) -> Fut + Send + 'static,
298        Fut: std::future::Future<Output = Result<(), Error>> + Send + 'static,
299    {
300        self.events.on(event, callback)
301    }
302}
303
304impl Display for Button {
305    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
306        write!(
307            f,
308            "Button (pin={}) [state={}, pullup={}, inverted={}]",
309            self.pin,
310            self.state.read(),
311            self.pullup,
312            self.invert
313        )
314    }
315}
316
317#[cfg_attr(feature = "serde", typetag::serde)]
318impl Device for Button {}
319
320#[cfg_attr(feature = "serde", typetag::serde)]
321impl Input for Button {
322    fn get_state(&self) -> State {
323        match self.invert {
324            false => State::from(*self.state.read()),
325            true => State::from(!*self.state.read()),
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use std::sync::atomic::{AtomicBool, Ordering};
333
334    use crate::hardware::Board;
335    use crate::mocks::plugin_io::MockIoProtocol;
336
337    use super::*;
338
339    #[hermes_five_macros::test]
340    fn test_new_button_creation() {
341        let board = Board::new(MockIoProtocol::default());
342        let button = Button::new(&board, 4);
343
344        assert!(button.is_ok());
345        let button = button.unwrap();
346        assert_eq!(button.get_pin(), 4);
347        assert_eq!(button.get_state().as_bool(), true);
348        assert!(!button.is_inverted());
349        assert!(!button.is_pullup());
350
351        button.detach();
352        board.close();
353    }
354
355    #[hermes_five_macros::test]
356    fn test_new_inverted_button_creation() {
357        let board = Board::new(MockIoProtocol::default());
358        let button = Button::new_inverted(&board, 4);
359
360        assert!(button.is_ok());
361        let button = button.unwrap();
362        assert_eq!(button.get_pin(), 4);
363        assert_eq!(button.get_state().as_bool(), false);
364        assert!(button.is_inverted());
365        assert!(!button.is_pullup());
366
367        button.detach();
368        board.close();
369    }
370
371    #[hermes_five_macros::test]
372    fn test_new_pullup_button_creation() {
373        let board = Board::new(MockIoProtocol::default());
374        let button = Button::new_pullup(&board, 4);
375
376        assert!(button.is_ok());
377        let button = button.unwrap();
378        assert_eq!(button.get_pin(), 4);
379        assert_eq!(button.get_state().as_bool(), true);
380        assert!(!button.is_inverted());
381        assert!(button.is_pullup());
382
383        button.detach();
384        board.close();
385    }
386
387    #[hermes_five_macros::test]
388    fn test_new_inverted_pullup_button_creation() {
389        let board = Board::new(MockIoProtocol::default());
390        let button = Button::new_inverted_pullup(&board, 4);
391
392        assert!(button.is_ok());
393        let button = button.unwrap();
394        assert_eq!(button.get_pin(), 4);
395        assert_eq!(button.get_state().as_bool(), false);
396        assert!(button.is_inverted());
397        assert!(button.is_pullup());
398
399        button.detach();
400        board.close();
401    }
402
403    #[hermes_five_macros::test]
404    fn test_button_helper() {
405        let board = Board::new(MockIoProtocol::default());
406        let button = Button::start_with(
407            Button {
408                pin: 0,
409                state: Arc::new(RwLock::new(false)),
410                invert: true,
411                pullup: false,
412                protocol: board.get_protocol(),
413                handler: Arc::new(RwLock::new(None)),
414                events: Default::default(),
415            },
416            &board,
417            13,
418        );
419        assert!(button.is_ok());
420        let button = button.unwrap();
421        assert_eq!(button.get_pin(), 13);
422        assert!(button.handler.read().is_some());
423        button.detach();
424        assert!(button.handler.read().is_none());
425        board.close();
426    }
427
428    #[hermes_five_macros::test]
429    fn test_button_inverted_state_logic() {
430        let board = Board::new(MockIoProtocol::default());
431        let button = Button::new_inverted(&board, 5).unwrap();
432        assert_eq!(button.get_state().as_bool(), true);
433
434        button.state.write().clone_from(&true); // Simulate a pressed button
435        assert_eq!(button.get_state().as_bool(), false);
436
437        button.detach();
438        board.close();
439    }
440
441    #[hermes_five_macros::test]
442    fn test_button_events() {
443        let board = Board::new(MockIoProtocol::default());
444        let button = Button::new(&board, 5).unwrap();
445
446        // CHANGE
447        let change_flag = Arc::new(AtomicBool::new(false));
448        let moved_change_flag = change_flag.clone();
449        button.on(InputEvent::OnChange, move |new_state: bool| {
450            let captured_flag = moved_change_flag.clone();
451            async move {
452                captured_flag.store(new_state, Ordering::SeqCst);
453                Ok(())
454            }
455        });
456
457        // PRESSED
458        let pressed_flag = Arc::new(AtomicBool::new(false));
459        let moved_pressed_flag = pressed_flag.clone();
460        button.on(InputEvent::OnPress, move |_: ()| {
461            let captured_flag = moved_pressed_flag.clone();
462            async move {
463                captured_flag.store(true, Ordering::SeqCst);
464                Ok(())
465            }
466        });
467
468        // RELEASED
469        let released_flag = Arc::new(AtomicBool::new(false));
470        let moved_released_flag = released_flag.clone();
471        button.on(InputEvent::OnRelease, move |_: ()| {
472            let captured_flag = moved_released_flag.clone();
473            async move {
474                captured_flag.store(true, Ordering::SeqCst);
475                Ok(())
476            }
477        });
478
479        assert!(!change_flag.load(Ordering::SeqCst));
480        assert!(!pressed_flag.load(Ordering::SeqCst));
481        assert!(!released_flag.load(Ordering::SeqCst));
482
483        // Simulate pin state change in the protocol => take value 0xFF
484        button
485            .protocol
486            .get_io()
487            .write()
488            .get_pin_mut(5)
489            .unwrap()
490            .value = 0xFF;
491
492        pause!(500);
493
494        assert!(change_flag.load(Ordering::SeqCst));
495        assert!(pressed_flag.load(Ordering::SeqCst));
496        assert!(!released_flag.load(Ordering::SeqCst));
497
498        // Simulate pin state change in the protocol => takes value 0
499        button
500            .protocol
501            .get_io()
502            .write()
503            .get_pin_mut(5)
504            .unwrap()
505            .value = 0;
506
507        pause!(500);
508
509        assert!(!change_flag.load(Ordering::SeqCst)); // change switched back to 0
510        assert!(released_flag.load(Ordering::SeqCst));
511
512        button.detach();
513        board.close();
514    }
515
516    #[hermes_five_macros::test]
517    fn test_inverted_button_events() {
518        let board = Board::new(MockIoProtocol::default());
519        let button = Button::new_inverted(&board, 5).unwrap();
520
521        // CHANGE
522        let change_flag = Arc::new(AtomicBool::new(true));
523        let moved_change_flag = change_flag.clone();
524        button.on(InputEvent::OnChange, move |new_state: bool| {
525            let captured_flag = moved_change_flag.clone();
526            async move {
527                captured_flag.store(new_state, Ordering::SeqCst);
528                Ok(())
529            }
530        });
531
532        // PRESSED
533        let pressed_flag = Arc::new(AtomicBool::new(false));
534        let moved_pressed_flag = pressed_flag.clone();
535        button.on(InputEvent::OnPress, move |_: ()| {
536            let captured_flag = moved_pressed_flag.clone();
537            async move {
538                captured_flag.store(true, Ordering::SeqCst);
539                Ok(())
540            }
541        });
542
543        // RELEASED
544        let released_flag = Arc::new(AtomicBool::new(false));
545        let moved_released_flag = released_flag.clone();
546        button.on(InputEvent::OnRelease, move |_: ()| {
547            let captured_flag = moved_released_flag.clone();
548            async move {
549                captured_flag.store(true, Ordering::SeqCst);
550                Ok(())
551            }
552        });
553
554        assert!(change_flag.load(Ordering::SeqCst)); // true by default
555        assert!(!pressed_flag.load(Ordering::SeqCst));
556        assert!(!released_flag.load(Ordering::SeqCst));
557
558        // Simulate pin state change in the protocol => take value 0xFF
559        button
560            .protocol
561            .get_io()
562            .write()
563            .get_pin_mut(5)
564            .unwrap()
565            .value = 0xFF;
566
567        pause!(500);
568
569        assert!(!change_flag.load(Ordering::SeqCst)); // changed to false
570        assert!(pressed_flag.load(Ordering::SeqCst));
571        assert!(!released_flag.load(Ordering::SeqCst));
572
573        // Simulate pin state change in the protocol => takes value 0
574        button
575            .protocol
576            .get_io()
577            .write()
578            .get_pin_mut(5)
579            .unwrap()
580            .value = 0;
581
582        pause!(500);
583
584        assert!(change_flag.load(Ordering::SeqCst)); // change switched back to true
585        assert!(released_flag.load(Ordering::SeqCst));
586
587        button.detach();
588        board.close();
589    }
590
591    // #[hermes_five_macros::test]
592    // fn test_pullup_button_events() {
593    //     let board = Board::new(MockIoProtocol::default());
594    //     let button = Button::new_pullup(&board, 5).unwrap();
595    //
596    //     // CHANGE
597    //     let change_flag = Arc::new(AtomicBool::new(true));
598    //     let moved_change_flag = change_flag.clone();
599    //     button.on(InputEvent::OnChange, move |new_state: bool| {
600    //         let captured_flag = moved_change_flag.clone();
601    //         async move {
602    //             captured_flag.store(new_state, Ordering::SeqCst);
603    //             Ok(())
604    //         }
605    //     });
606    //
607    //     // PRESSED
608    //     let pressed_flag = Arc::new(AtomicBool::new(false));
609    //     let moved_pressed_flag = pressed_flag.clone();
610    //     button.on(InputEvent::OnPress, move |_: ()| {
611    //         let captured_flag = moved_pressed_flag.clone();
612    //         async move {
613    //             captured_flag.store(true, Ordering::SeqCst);
614    //             Ok(())
615    //         }
616    //     });
617    //
618    //     // RELEASED
619    //     let released_flag = Arc::new(AtomicBool::new(false));
620    //     let moved_released_flag = released_flag.clone();
621    //     button.on(InputEvent::OnRelease, move |_: ()| {
622    //         let captured_flag = moved_released_flag.clone();
623    //         async move {
624    //             captured_flag.store(true, Ordering::SeqCst);
625    //             Ok(())
626    //         }
627    //     });
628    //
629    //     assert!(change_flag.load(Ordering::SeqCst)); // true by default
630    //     assert!(!pressed_flag.load(Ordering::SeqCst));
631    //     assert!(!released_flag.load(Ordering::SeqCst));
632    //
633    //     // Simulate pin state change in the protocol => take value 0xFF
634    //     button
635    //         .protocol
636    //         .get_io()
637    //         .write()
638    //         .get_pin_mut(5)
639    //         .unwrap()
640    //         .value = 0;
641    //
642    //     pause!(500);
643    //
644    //     assert!(!change_flag.load(Ordering::SeqCst)); // changed to false
645    //     assert!(pressed_flag.load(Ordering::SeqCst));
646    //     assert!(!released_flag.load(Ordering::SeqCst));
647    //
648    //     // Simulate pin state change in the protocol => takes value 0
649    //     button
650    //         .protocol
651    //         .get_io()
652    //         .write()
653    //         .get_pin_mut(5)
654    //         .unwrap()
655    //         .value = 0xFF;
656    //
657    //     pause!(500);
658    //
659    //     assert!(change_flag.load(Ordering::SeqCst)); // change switched back to true
660    //     assert!(released_flag.load(Ordering::SeqCst));
661    //
662    //     button.detach();
663    //     board.close();
664    // }
665
666    #[hermes_five_macros::test]
667    fn test_button_display() {
668        let board = Board::new(MockIoProtocol::default());
669        let button = Button::new(&board, 4).unwrap();
670
671        assert_eq!(
672            format!("{}", button),
673            String::from("Button (pin=4) [state=true, pullup=false, inverted=false]")
674        );
675
676        button.detach();
677        board.close();
678    }
679}