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;