Skip to main content

device_envoy_esp/button/
button_watch.rs

1//! A device abstraction for background button monitoring with a spawned task.
2//!
3//! See [`button_watch!`](crate::button_watch!) for usage.
4
5#[cfg(target_os = "none")]
6use core::sync::atomic::{AtomicBool, Ordering};
7#[cfg(target_os = "none")]
8use device_envoy_core::button::{Button, __ButtonMonitor};
9#[cfg(target_os = "none")]
10use embassy_executor::Spawner;
11#[cfg(target_os = "none")]
12use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
13#[cfg(target_os = "none")]
14use embassy_time::Timer;
15
16#[cfg(target_os = "none")]
17use super::{PressDuration, PressedTo};
18#[cfg(target_os = "none")]
19use crate::{Error, Result};
20
21/// Static resources for [`ButtonWatchEsp`].
22#[cfg(target_os = "none")]
23// Public only because `button_watch!` macro expansions reference this type in
24// downstream crates.
25#[doc(hidden)]
26pub struct ButtonWatchStaticEsp {
27    signal: Signal<CriticalSectionRawMutex, PressDuration>,
28    state_signal: Signal<CriticalSectionRawMutex, bool>,
29    state_changed_signal: Signal<CriticalSectionRawMutex, ()>,
30    initialized_signal: Signal<CriticalSectionRawMutex, ()>,
31    is_pressed: AtomicBool,
32    initialized: AtomicBool,
33}
34
35#[cfg(target_os = "none")]
36impl ButtonWatchStaticEsp {
37    const fn new() -> Self {
38        Self {
39            signal: Signal::new(),
40            state_signal: Signal::new(),
41            state_changed_signal: Signal::new(),
42            initialized_signal: Signal::new(),
43            is_pressed: AtomicBool::new(false),
44            initialized: AtomicBool::new(false),
45        }
46    }
47}
48
49/// A device abstraction for background button monitoring.
50///
51/// Use this type when you need press detection that is not disrupted by other
52/// fast loops/tasks that repeatedly cancel futures.
53#[cfg(target_os = "none")]
54// Public only because `button_watch!` macro expansions reference this type in
55// downstream crates.
56#[doc(hidden)]
57pub struct ButtonWatchEsp<'a> {
58    signal: &'a Signal<CriticalSectionRawMutex, PressDuration>,
59    state_signal: &'a Signal<CriticalSectionRawMutex, bool>,
60    state_changed_signal: &'a Signal<CriticalSectionRawMutex, ()>,
61    initialized_signal: &'a Signal<CriticalSectionRawMutex, ()>,
62    is_pressed: &'a AtomicBool,
63    initialized: &'a AtomicBool,
64}
65
66#[cfg(target_os = "none")]
67impl ButtonWatchEsp<'_> {
68    /// Create static resources for [`ButtonWatchEsp::new_from_pin`].
69    #[must_use]
70    pub const fn new_static() -> ButtonWatchStaticEsp {
71        ButtonWatchStaticEsp::new()
72    }
73
74    /// Create a background button monitor directly from a pin.
75    #[must_use = "Must be used to manage the spawned task"]
76    pub async fn new_from_pin(
77        button_watch_static: &'static ButtonWatchStaticEsp,
78        button_pin: impl esp_hal::gpio::InputPin + 'static,
79        pressed_to: PressedTo,
80        spawner: Spawner,
81    ) -> Result<Self> {
82        let pull = match pressed_to {
83            PressedTo::Voltage => esp_hal::gpio::Pull::Down,
84            PressedTo::Ground => esp_hal::gpio::Pull::Up,
85        };
86        let input = esp_hal::gpio::Input::new(
87            button_pin,
88            esp_hal::gpio::InputConfig::default().with_pull(pull),
89        );
90        let token = button_watch_task_from_pin(
91            input,
92            pressed_to,
93            &button_watch_static.signal,
94            &button_watch_static.state_signal,
95            &button_watch_static.state_changed_signal,
96            &button_watch_static.initialized_signal,
97            &button_watch_static.is_pressed,
98            &button_watch_static.initialized,
99        );
100        spawner.spawn(token).map_err(Error::TaskSpawn)?;
101        let button_watch = Self {
102            signal: &button_watch_static.signal,
103            state_signal: &button_watch_static.state_signal,
104            state_changed_signal: &button_watch_static.state_changed_signal,
105            initialized_signal: &button_watch_static.initialized_signal,
106            is_pressed: &button_watch_static.is_pressed,
107            initialized: &button_watch_static.initialized,
108        };
109        button_watch.wait_until_initialized().await;
110        Ok(button_watch)
111    }
112
113    /// Wait until the first sampled state has been captured by the background task.
114    pub async fn wait_until_initialized(&self) {
115        if self.initialized.load(Ordering::Acquire) {
116            return;
117        }
118        self.initialized_signal.wait().await;
119    }
120
121    /// Wait for any button state transition (press or release).
122    pub async fn wait_for_state_change(&self) {
123        self.state_changed_signal.wait().await;
124    }
125}
126
127#[cfg(target_os = "none")]
128impl __ButtonMonitor for ButtonWatchEsp<'_> {
129    fn is_pressed_raw(&self) -> bool {
130        self.is_pressed.load(Ordering::Relaxed)
131    }
132
133    async fn wait_until_pressed_state(&mut self, pressed: bool) {
134        if self.is_pressed.load(Ordering::Relaxed) == pressed {
135            return;
136        }
137
138        loop {
139            let state = self.state_signal.wait().await;
140            if state == pressed {
141                return;
142            }
143        }
144    }
145}
146
147#[cfg(target_os = "none")]
148impl Button for ButtonWatchEsp<'_> {
149    async fn wait_for_press_duration(&mut self) -> PressDuration {
150        self.signal.wait().await
151    }
152}
153
154#[embassy_executor::task]
155#[cfg(target_os = "none")]
156async fn button_watch_task_from_pin(
157    mut input: esp_hal::gpio::Input<'static>,
158    pressed_to: PressedTo,
159    signal: &'static Signal<CriticalSectionRawMutex, PressDuration>,
160    state_signal: &'static Signal<CriticalSectionRawMutex, bool>,
161    state_changed_signal: &'static Signal<CriticalSectionRawMutex, ()>,
162    initialized_signal: &'static Signal<CriticalSectionRawMutex, ()>,
163    is_pressed: &'static AtomicBool,
164    initialized: &'static AtomicBool,
165) -> ! {
166    let mut input_button = InputButton {
167        input: &mut input,
168        pressed_to,
169    };
170    signal_press_durations(
171        &mut input_button,
172        signal,
173        state_signal,
174        state_changed_signal,
175        initialized_signal,
176        is_pressed,
177        initialized,
178    )
179    .await
180}
181
182#[cfg(target_os = "none")]
183struct InputButton<'a> {
184    input: &'a mut esp_hal::gpio::Input<'static>,
185    pressed_to: PressedTo,
186}
187
188#[cfg(target_os = "none")]
189impl __ButtonMonitor for InputButton<'_> {
190    fn is_pressed_raw(&self) -> bool {
191        self.pressed_to.is_pressed(self.input.is_high())
192    }
193
194    async fn wait_until_pressed_state(&mut self, pressed: bool) {
195        match (pressed, self.pressed_to) {
196            (true, PressedTo::Voltage) | (false, PressedTo::Ground) => {
197                self.input.wait_for_high().await;
198            }
199            (true, PressedTo::Ground) | (false, PressedTo::Voltage) => {
200                self.input.wait_for_low().await;
201            }
202        }
203    }
204}
205
206#[cfg(target_os = "none")]
207async fn signal_press_durations<B: __ButtonMonitor>(
208    button: &mut B,
209    signal: &'static Signal<CriticalSectionRawMutex, PressDuration>,
210    state_signal: &'static Signal<CriticalSectionRawMutex, bool>,
211    state_changed_signal: &'static Signal<CriticalSectionRawMutex, ()>,
212    initialized_signal: &'static Signal<CriticalSectionRawMutex, ()>,
213    is_pressed: &'static AtomicBool,
214    initialized: &'static AtomicBool,
215) -> ! {
216    let initial_pressed = <B as __ButtonMonitor>::is_pressed_raw(button);
217    is_pressed.store(initial_pressed, Ordering::Relaxed);
218    state_signal.signal(initial_pressed);
219    initialized.store(true, Ordering::Release);
220    initialized_signal.signal(());
221
222    loop {
223        <B as __ButtonMonitor>::wait_until_pressed_state(button, false).await;
224
225        <B as __ButtonMonitor>::wait_until_pressed_state(button, true).await;
226        is_pressed.store(true, Ordering::Relaxed);
227        state_signal.signal(true);
228        state_changed_signal.signal(());
229
230        Timer::after(device_envoy_core::button::BUTTON_DEBOUNCE_DELAY).await;
231        if !<B as __ButtonMonitor>::is_pressed_raw(button) {
232            is_pressed.store(false, Ordering::Relaxed);
233            state_signal.signal(false);
234            state_changed_signal.signal(());
235            continue;
236        }
237
238        let press_duration = embassy_futures::select::select(
239            <B as __ButtonMonitor>::wait_until_pressed_state(button, false),
240            Timer::after(device_envoy_core::button::LONG_PRESS_DURATION),
241        )
242        .await;
243
244        match press_duration {
245            embassy_futures::select::Either::First(()) => {
246                is_pressed.store(false, Ordering::Relaxed);
247                state_signal.signal(false);
248                state_changed_signal.signal(());
249                signal.signal(PressDuration::Short);
250            }
251            embassy_futures::select::Either::Second(()) => {
252                signal.signal(PressDuration::Long);
253                <B as __ButtonMonitor>::wait_until_pressed_state(button, false).await;
254                is_pressed.store(false, Ordering::Relaxed);
255                state_signal.signal(false);
256                state_changed_signal.signal(());
257            }
258        }
259    }
260}
261
262/// Creates a button monitoring device abstraction with a background task.
263///
264/// This macro creates a button monitor that runs in a dedicated background task,
265/// providing continuous monitoring without interruption.
266///
267/// See [`ButtonWatchGenerated`](crate::button::button_watch_generated::ButtonWatchGenerated)
268/// for a sample of what the macro generates.
269///
270/// **Syntax:**
271///
272/// ```text
273/// button_watch! {
274///     [<attributes>]
275///     [<visibility>] <Name> {
276///         pin: <pin_ident>,
277///     }
278/// }
279/// ```
280///
281/// # Constructors
282///
283/// - [`new()`](crate::button::button_watch_generated::ButtonWatchGenerated::new) — Create from a pin
284///
285/// # Use Cases
286///
287/// Use `button_watch!` instead of [`ButtonEsp`](super::ButtonEsp) when you need continuous monitoring
288/// that works even in fast loops or `select()` operations. [`ButtonEsp`](super::ButtonEsp) starts
289/// fresh monitoring on each call to `wait_for_press()`, which can miss events in busy loops.
290///
291/// # Parameters
292///
293/// - `name`: The struct name for the button watch device
294/// - `pin`: The GPIO pin connected to the button
295///
296/// Optional:
297/// - `vis`: Visibility modifier (default: private)
298///
299/// # Example
300///
301/// ```rust,no_run
302/// # #![no_std]
303/// # #![no_main]
304/// use device_envoy_esp::{
305///     Result,
306///     button::{Button as _, PressDuration, PressedTo},
307///     button_watch,
308/// };
309/// use embassy_executor::Spawner;
310/// # use esp_backtrace as _;
311/// # #[panic_handler]
312/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
313///
314/// button_watch! {
315///     ButtonWatch13 {
316///         pin: GPIO13,
317///     }
318/// }
319///
320/// async fn example(
321///     p: esp_hal::peripherals::Peripherals,
322///     spawner: Spawner,
323/// ) -> Result<()> {
324///     // Create the button monitor (spawns background task automatically)
325///     let mut button_watch13 = ButtonWatch13::new(p.GPIO13, PressedTo::Ground, spawner)
326///         .await?;
327///
328///     loop {
329///         // Wait for button press - never misses events even if this loop is slow
330///         match button_watch13.wait_for_press_duration().await {
331///             PressDuration::Short => {
332///                 // Handle short press
333/// #               break;
334///             }
335///             PressDuration::Long => {
336///                 // Handle long press
337/// #               break;
338///             }
339///         }
340///     }
341///     Ok(())
342/// }
343/// ```
344#[doc(hidden)]
345#[macro_export]
346macro_rules! button_watch {
347    ($($tt:tt)*) => { $crate::__button_watch_impl! { $($tt)* } };
348}
349
350/// Implementation macro for [`button_watch!`].
351///
352/// Do not call directly - use [`button_watch!`](crate::button_watch!) instead.
353#[doc(hidden)]
354#[macro_export]
355macro_rules! __button_watch_impl {
356    (
357        $(#[$meta:meta])*
358        $vis:vis $name:ident {
359            pin: $pin:ident,
360        }
361    ) => {
362        $crate::__button_watch_impl! {
363            @impl
364            meta: [$(#[$meta])*],
365            vis: $vis,
366            name: $name,
367            pin: $pin
368        }
369    };
370    (
371        $(#[$meta:meta])*
372        $name:ident {
373            pin: $pin:ident,
374        }
375    ) => {
376        $crate::__button_watch_impl! {
377            @impl
378            meta: [$(#[$meta])*],
379            vis: ,
380            name: $name,
381            pin: $pin
382        }
383    };
384    (
385        @impl
386        meta: [$(#[$meta:meta])*],
387        vis: $vis:vis,
388        name: $name:ident,
389        pin: $pin:ident
390    ) => {
391        ::paste::paste! {
392            $(#[$meta])*
393            #[doc = concat!(
394                "Button monitor generated by [`button_watch!`].\n\n",
395                "Monitors button presses in a background task. ",
396                "See the [button module documentation](mod@$crate::button) for usage."
397            )]
398            $vis struct $name {
399                button_watch: $crate::button::ButtonWatchEsp<'static>,
400            }
401
402            impl $name {
403                /// Creates a new button monitor and spawns its background task.
404                ///
405                /// # Parameters
406                ///
407                /// - `button_pin`: GPIO pin for the button
408                /// - `pressed_to`: How the button is wired ([`PressedTo::Ground`] or [`PressedTo::Voltage`])
409                /// - `spawner`: Task spawner for background operations
410                ///
411                /// # Errors
412                ///
413                /// Returns an error if the background task cannot be spawned.
414                pub async fn new(
415                    button_pin: $crate::esp_hal::peripherals::$pin<'static>,
416                    pressed_to: $crate::button::PressedTo,
417                    spawner: ::embassy_executor::Spawner,
418                ) -> $crate::Result<&'static mut Self> {
419                    static BUTTON_WATCH_STATIC: $crate::button::ButtonWatchStaticEsp =
420                        $crate::button::ButtonWatchEsp::new_static();
421                    static BUTTON_WATCH_CELL: ::static_cell::StaticCell<$name> =
422                        ::static_cell::StaticCell::new();
423
424                    let button_watch = $crate::button::ButtonWatchEsp::new_from_pin(
425                        &BUTTON_WATCH_STATIC,
426                        button_pin,
427                        pressed_to,
428                        spawner,
429                    )
430                    .await?;
431
432                    let instance = BUTTON_WATCH_CELL.init($name { button_watch });
433                    Ok(instance)
434                }
435            }
436
437            impl ::core::ops::Deref for $name {
438                type Target = $crate::button::ButtonWatchEsp<'static>;
439
440                fn deref(&self) -> &Self::Target {
441                    &self.button_watch
442                }
443            }
444
445            impl $crate::button::__ButtonMonitor for $name {
446                fn is_pressed_raw(&self) -> bool {
447                    <$crate::button::ButtonWatchEsp<'static> as $crate::button::Button>::is_pressed(
448                        &self.button_watch,
449                    )
450                }
451
452                async fn wait_until_pressed_state(&mut self, pressed: bool) {
453                    <$crate::button::ButtonWatchEsp<'static> as $crate::button::__ButtonMonitor>::wait_until_pressed_state(
454                        &mut self.button_watch,
455                        pressed,
456                    )
457                    .await
458                }
459            }
460
461            impl $crate::button::Button for $name {
462                async fn wait_for_press_duration(&mut self) -> $crate::button::PressDuration {
463                    <$crate::button::ButtonWatchEsp<'static> as $crate::button::Button>::wait_for_press_duration(
464                        &mut self.button_watch,
465                    )
466                    .await
467                }
468            }
469        }
470    };
471}