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}