Skip to main content

device_envoy/button/
button_watch.rs

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