Skip to main content

device_envoy/
button.rs

1//! A device abstraction for buttons with debouncing and press duration detection.
2//!
3//! This module provides two ways to monitor button presses:
4//!
5//! - [`Button`] — Simple button monitoring. Each call to `wait_for_press()`, etc. starts fresh
6//!   button monitoring.
7//! - [`button_watch!`](crate::button_watch!) — Monitors a button in a background task
8//!   so that it works even in a fast loop/select.
9//!
10
11mod button_watch;
12pub mod button_watch_generated;
13
14// Must be public for macro expansion in downstream crates, but not user-facing API.
15#[doc(hidden)]
16pub use button_watch::{ButtonWatch, ButtonWatchStatic};
17
18// Must be public for macro expansion in downstream crates, but not user-facing API.
19#[doc(hidden)]
20pub use button_watch::{button_watch_task, button_watch_task_from_input};
21
22use embassy_futures::select::{Either, select};
23use embassy_rp::Peri;
24use embassy_rp::gpio::{Input, Pull};
25use embassy_time::{Duration, Timer};
26
27// ============================================================================
28// Constants
29// ============================================================================
30
31/// Debounce delay for the button.
32pub(crate) const BUTTON_DEBOUNCE_DELAY: Duration = Duration::from_millis(10);
33
34/// Duration representing a long button press.
35pub(crate) const LONG_PRESS_DURATION: Duration = Duration::from_millis(500);
36
37// ============================================================================
38// PressedTo - How the button is wired
39// ============================================================================
40
41/// Describes if the button connects to voltage or ground when pressed.
42#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, defmt::Format)]
43pub enum PressedTo {
44    /// Button connects pin to voltage (3.3V) when pressed.
45    /// Uses internal pull-down resistor. Pin reads HIGH when pressed.
46    ///
47    /// Note: The original Pico 2 (RP2350) has a known silicon bug with pull-down resistors
48    /// that can cause pins to stay HIGH after button release. Use ToGround instead.
49    Voltage,
50
51    /// Button connects pin to ground (GND) when pressed.
52    /// Uses internal pull-up resistor. Pin reads LOW when pressed.
53    /// Recommended for Pico 2 due to pull-down resistor bug.
54    Ground,
55}
56
57// ============================================================================
58// PressDuration - Button press type
59// ============================================================================
60
61/// Duration of a button press (short or long).
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, defmt::Format)]
63pub enum PressDuration {
64    /// Button was held for less than [`LONG_PRESS_DURATION`](crate::button) (500ms).
65    Short,
66    /// Button was held for at least [`LONG_PRESS_DURATION`](crate::button) (500ms).
67    Long,
68}
69
70// ============================================================================
71// Button Virtual Device
72// ============================================================================
73
74/// A device abstraction for a button with debouncing and press duration detection.
75///
76/// # Hardware Requirements
77///
78/// The button can be wired in two ways:
79/// - [`PressedTo::Voltage`]: Button connects pin to 3.3V when pressed (uses pull-down)
80/// - [`PressedTo::Ground`]: Button connects pin to GND when pressed (uses pull-up)
81///
82/// **Important**: Pico 2 (RP2350) has a known silicon bug (erratum E9) with pull-down
83/// resistors that can leave the pin reading HIGH after release. Wire buttons to GND and
84/// use [`PressedTo::Ground`] on Pico 2.
85///
86/// # Usage
87///
88/// Use [`wait_for_press()`](Self::wait_for_press) when you only need a debounced
89/// press event. It returns on the down edge and does not wait for release.
90///
91/// Use [`wait_for_press_duration()`](Self::wait_for_press_duration) when you need to
92/// distinguish short vs. long presses. It returns as soon as it can decide, so long
93/// presses are reported before the button is released.
94///
95/// # Example
96///
97/// ```rust,no_run
98/// # #![no_std]
99/// # #![no_main]
100///
101/// use device_envoy::button::{Button, PressDuration, PressedTo};
102/// # #[panic_handler]
103/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
104///
105/// async fn example(p: embassy_rp::Peripherals) {
106///     let mut button = Button::new(p.PIN_13, PressedTo::Ground);
107///
108///     // Wait for a press without measuring duration.
109///     button.wait_for_press().await;
110///
111///     // Measure press durations in a loop
112///     loop {
113///         match button.wait_for_press_duration().await {
114///             PressDuration::Short => {
115///                 // Handle short press
116///             }
117///             PressDuration::Long => {
118///                 // Handle long press (fires before button is released)
119///             }
120///         }
121///     }
122/// }
123/// ```
124pub struct Button<'a> {
125    input: Input<'a>,
126    pressed_to: PressedTo,
127}
128
129impl<'a> Button<'a> {
130    /// Creates a new `Button` instance from a pin.
131    ///
132    /// The pin is configured based on the connection type:
133    /// - [`PressedTo::Voltage`]: Uses internal pull-down (button to 3.3V)
134    /// - [`PressedTo::Ground`]: Uses internal pull-up (button to GND)
135    #[must_use]
136    pub fn new<P: embassy_rp::gpio::Pin>(pin: Peri<'a, P>, pressed_to: PressedTo) -> Self {
137        let pull = match pressed_to {
138            PressedTo::Voltage => Pull::Down,
139            PressedTo::Ground => Pull::Up,
140        };
141        Self {
142            input: Input::new(pin, pull),
143            pressed_to,
144        }
145    }
146
147    /// Returns whether the button is currently pressed.
148    #[must_use]
149    pub fn is_pressed(&self) -> bool {
150        match self.pressed_to {
151            PressedTo::Voltage => self.input.is_high(),
152            PressedTo::Ground => self.input.is_low(),
153        }
154    }
155
156    #[inline]
157    async fn wait_for_button_up(&mut self) -> &mut Self {
158        loop {
159            if !self.is_pressed() {
160                break;
161            }
162            Timer::after(Duration::from_millis(1)).await;
163        }
164        self
165    }
166
167    #[inline]
168    async fn wait_for_button_down(&mut self) -> &mut Self {
169        loop {
170            if self.is_pressed() {
171                break;
172            }
173            Timer::after(Duration::from_millis(1)).await;
174        }
175        self
176    }
177
178    #[inline]
179    async fn wait_for_stable_down(&mut self) -> &mut Self {
180        loop {
181            self.wait_for_button_down().await;
182            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
183            if self.is_pressed() {
184                break;
185            }
186            // otherwise it was bounce; keep waiting
187        }
188        self
189    }
190
191    #[inline]
192    async fn wait_for_stable_up(&mut self) -> &mut Self {
193        loop {
194            self.wait_for_button_up().await;
195            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
196            if !self.is_pressed() {
197                break;
198            }
199        }
200        self
201    }
202    /// Waits for the next press (button goes down, debounced).
203    /// Does not wait for release.
204    ///
205    /// See [`Button`] for usage example
206    pub async fn wait_for_press(&mut self) {
207        self.wait_for_stable_up().await; // ensure edge-triggered
208        self.wait_for_stable_down().await; // return on down
209    }
210
211    /// Waits for the next press and returns whether it was short or long (debounced).
212    ///
213    /// Returns as soon as it can decide, so long presses are reported before release.
214    ///
215    /// See [`Button`] for usage example
216    pub async fn wait_for_press_duration(&mut self) -> PressDuration {
217        self.wait_for_stable_up().await;
218        self.wait_for_stable_down().await;
219
220        let press_duration =
221            match select(self.wait_for_stable_up(), Timer::after(LONG_PRESS_DURATION)).await {
222                Either::First(_) => PressDuration::Short,
223                Either::Second(()) => PressDuration::Long,
224            };
225
226        press_duration
227    }
228
229    /// Waits until the button is released (debounced).
230    pub async fn wait_for_release(&mut self) {
231        self.wait_for_stable_up().await;
232    }
233
234    /// Consumes the button and returns its internal components.
235    ///
236    /// This is useful for converting a `Button` (returned from `WifiAuto::connect`)
237    /// into a `ButtonWatch` for background monitoring.
238    ///
239    /// See the [`button_watch!`](crate::button_watch!) macro documentation for usage with `from_button()`.
240    // Must be public for macro expansion but not part of the user-facing API.
241    #[doc(hidden)]
242    #[must_use]
243    pub fn into_parts(self) -> (Input<'a>, PressedTo) {
244        (self.input, self.pressed_to)
245    }
246}
247
248#[doc(inline)]
249pub use crate::button_watch;