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}