Skip to main content

xpanse_api/interfaces/
buttons.rs

1//! GPIO button input wrappers.
2//!
3//! Provides logical button roles that drivers can publish through the
4//! [`crate::registry::Registry`] and that apps can wait on for press events.
5//! Each role is a zero-sized marker type; the physical pin is wired to a role
6//! at startup using `pin_button` or `aliased_pin_buttons`.
7
8use alloc::{boxed::Box, sync::Arc};
9use core::future::Future;
10use core::marker::PhantomData;
11use core::pin::Pin;
12
13use embassy_rp::{
14    Peri,
15    gpio::{AnyPin, Input, Pull},
16};
17use embassy_sync::blocking_mutex::CriticalSectionMutex;
18use embassy_time::Timer;
19
20mod private {
21    pub trait Sealed {}
22}
23
24/// Marker trait for a logical button role (e.g. `A`, `B`, `Up`).
25///
26/// The type itself is zero-sized; it exists only to name a resource slot in the
27/// [`crate::registry::Registry`].
28pub trait ButtonRole: private::Sealed + 'static + Send {}
29
30macro_rules! role {
31    ($($n:ident),* $(,)?) => {
32        $(
33            pub struct $n;
34            impl private::Sealed for $n {}
35            impl ButtonRole for $n {}
36        )*
37    };
38}
39
40role!(A, B, X, Y, Up, Down, Left, Right);
41
42/// Async interface for waiting on a button press and querying its state.
43pub trait Button<R: ButtonRole>: Send {
44    /// Wait until the button transitions to the pressed state.
45    ///
46    /// The returned future resolves once a debounced press is detected.
47    fn wait_for_pressed<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
48    /// Returns `true` if the button is currently pressed.
49    fn is_pressed(&self) -> bool;
50}
51
52/// A single button backed by one GPIO input.
53pub struct SingleButton<R: ButtonRole> {
54    pin: Input<'static>,
55    _role: PhantomData<R>,
56}
57
58struct SharedButton<R: ButtonRole> {
59    pin: Arc<CriticalSectionMutex<Input<'static>>>,
60    _role: PhantomData<R>,
61}
62
63impl<R: ButtonRole> SharedButton<R> {
64    fn is_low(&self) -> bool {
65        self.pin.lock(Input::is_low)
66    }
67}
68
69impl<R: ButtonRole> SingleButton<R> {
70    /// Create a `SingleButton` from a GPIO pin configured with a pull-up.
71    pub fn new(pin: Peri<'static, AnyPin>) -> Self {
72        Self {
73            pin: Input::new(pin, Pull::Up),
74            _role: PhantomData,
75        }
76    }
77}
78
79impl<R: ButtonRole> Button<R> for SingleButton<R> {
80    fn wait_for_pressed<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
81        Box::pin(async move {
82            while self.pin.is_low() {
83                self.pin.wait_for_high().await;
84                Timer::after_millis(20).await;
85            }
86
87            loop {
88                self.pin.wait_for_low().await;
89                Timer::after_millis(20).await;
90                if self.pin.is_low() {
91                    return;
92                }
93            }
94        })
95    }
96
97    fn is_pressed(&self) -> bool {
98        self.pin.is_low()
99    }
100}
101
102impl<R: ButtonRole> Button<R> for SharedButton<R> {
103    fn wait_for_pressed<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
104        Box::pin(async move {
105            while self.is_low() {
106                Timer::after_millis(20).await;
107            }
108
109            loop {
110                Timer::after_millis(20).await;
111                if self.is_low() {
112                    return;
113                }
114            }
115        })
116    }
117
118    fn is_pressed(&self) -> bool {
119        self.is_low()
120    }
121}
122
123/// Create a boxed [`Button`] of role `R` from a single GPIO pin.
124///
125/// The pin is consumed and configured internally with a pull-up resistor.
126///
127/// # Example
128///
129/// ```ignore
130/// use embassy_rp::gpio::AnyPin;
131/// use embassy_rp::Peri;
132/// use xpanse_api::interfaces::buttons::{A, pin_button};
133/// use xpanse_api::registry::Registry;
134///
135/// # async fn example(
136/// #     pin: Peri<'static, AnyPin>,
137/// #     registry: &mut Registry,
138/// #     slot: xpanse_api::metadata::ModuleSlot,
139/// # ) {
140/// let button = pin_button::<A>(pin);
141/// // registry.register(slot, id, button);
142/// # }
143/// ```
144pub fn pin_button<R: ButtonRole>(pin: Peri<'static, AnyPin>) -> Box<dyn Button<R>> {
145    Box::new(SingleButton::<R>::new(pin))
146}
147
148/// Creates two logical button roles backed by one physical GPIO input.
149///
150/// This is useful when a single physical button (e.g. a side button) needs to
151/// serve two roles simultaneously, such as `A` and `Up`.
152pub fn aliased_pin_buttons<R: ButtonRole, Alias: ButtonRole>(
153    pin: Peri<'static, AnyPin>,
154) -> (Box<dyn Button<R>>, Box<dyn Button<Alias>>) {
155    let pin = Arc::new(CriticalSectionMutex::new(Input::new(pin, Pull::Up)));
156    (
157        Box::new(SharedButton::<R> {
158            pin: pin.clone(),
159            _role: PhantomData,
160        }),
161        Box::new(SharedButton::<Alias> {
162            pin,
163            _role: PhantomData,
164        }),
165    )
166}