Skip to main content

device_envoy_core/
button.rs

1//! Platform-independent button types and constants.
2//!
3//! See the platform-specific crate (for example `device_envoy_rp::button` or
4//! `device_envoy_esp::button`) for the primary documentation and examples.
5
6use embassy_futures::select::{Either, select};
7use embassy_time::Duration;
8use embassy_time::Timer;
9
10// ============================================================================
11// Constants
12// ============================================================================
13
14/// Debounce delay for the button.
15// Public for cross-crate compatibility; hidden from end-user docs.
16#[doc(hidden)]
17pub const BUTTON_DEBOUNCE_DELAY: Duration = Duration::from_millis(10);
18
19/// Duration representing a long button press.
20// Public for cross-crate compatibility; hidden from end-user docs.
21#[doc(hidden)]
22pub const LONG_PRESS_DURATION: Duration = Duration::from_millis(500);
23
24/// Polling interval used by default button wait helpers.
25// Public for cross-crate compatibility; hidden from end-user docs.
26#[doc(hidden)]
27pub const BUTTON_POLL_INTERVAL: Duration = Duration::from_millis(1);
28
29/// Internal primitive methods used to build the public [`Button`] API.
30///
31/// Platform crates implement this for concrete button types.
32#[allow(async_fn_in_trait)]
33#[doc(hidden)]
34pub trait __ButtonMonitor {
35    /// Returns whether the button is currently pressed.
36    fn is_pressed_raw(&self) -> bool;
37
38    /// Wait until the sampled pressed state matches `pressed`.
39    ///
40    /// Implementations may use edge interrupts, polling, or any platform-specific mechanism.
41    async fn wait_until_pressed_state(&mut self, pressed: bool);
42}
43
44/// Platform-agnostic button contract.
45///
46/// Platform crates inherit the default debouncing and press-duration behavior from shared
47/// core logic by implementing [`__ButtonMonitor`].
48///
49/// # Hardware Requirements
50///
51/// The button can be wired in two ways:
52///
53/// - [`PressedTo::Voltage`]: Button connects pin to voltage when pressed (active-high)
54/// - [`PressedTo::Ground`]: Button connects pin to ground when pressed (active-low)
55///
56/// # Usage
57///
58/// Use [`Button::wait_for_press`] when you only need a debounced
59/// press event. It returns on the down edge and does not wait for release.
60///
61/// Use [`Button::wait_for_press_duration`] when you need to
62/// distinguish short vs. long presses. It returns as soon as it can decide, so long
63/// presses are reported before the button is released.
64///
65/// # Example
66///
67/// ```rust,no_run
68/// use device_envoy_core::button::{Button, PressDuration};
69///
70/// async fn log_button_presses(button: &mut impl Button) -> ! {
71///     // Wait for a press without measuring duration.
72///     button.wait_for_press().await;
73///
74///     // Measure press durations in a loop.
75///     loop {
76///         match button.wait_for_press_duration().await {
77///             PressDuration::Short => {
78///                 // Handle short press.
79///             }
80///             PressDuration::Long => {
81///                 // Handle long press (fires before button is released).
82///             }
83///         }
84///     }
85/// }
86///
87/// # struct ButtonMock;
88/// # impl device_envoy_core::button::__ButtonMonitor for ButtonMock {
89/// #     fn is_pressed_raw(&self) -> bool { false }
90/// #     async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
91/// # }
92/// # impl Button for ButtonMock {}
93/// # fn main() {
94/// #     let mut button = ButtonMock;
95/// #     let _future = log_button_presses(&mut button);
96/// # }
97/// ```
98#[allow(async_fn_in_trait)]
99pub trait Button: __ButtonMonitor {
100    /// Returns whether the button is currently pressed.
101    fn is_pressed(&self) -> bool {
102        <Self as __ButtonMonitor>::is_pressed_raw(self)
103    }
104
105    /// Waits for the next press (button goes down, debounced). Does not wait for release.
106    ///
107    /// See the [Button trait documentation](Self) for usage examples.
108    async fn wait_for_press(&mut self) {
109        loop {
110            <Self as __ButtonMonitor>::wait_until_pressed_state(self, false).await;
111            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
112            if !self.is_pressed() {
113                break;
114            }
115        }
116
117        loop {
118            <Self as __ButtonMonitor>::wait_until_pressed_state(self, true).await;
119            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
120            if self.is_pressed() {
121                break;
122            }
123            // otherwise it was bounce; keep waiting
124        }
125    }
126
127    /// Waits for the next press and returns whether it was short or long (debounced).
128    ///
129    /// Returns as soon as it can decide, so long presses are reported before release.
130    ///
131    /// See the [Button trait documentation](Self) for usage examples.
132    async fn wait_for_press_duration(&mut self) -> PressDuration {
133        loop {
134            <Self as __ButtonMonitor>::wait_until_pressed_state(self, false).await;
135            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
136            if !self.is_pressed() {
137                break;
138            }
139        }
140
141        loop {
142            <Self as __ButtonMonitor>::wait_until_pressed_state(self, true).await;
143            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
144            if self.is_pressed() {
145                break;
146            }
147            // otherwise it was bounce; keep waiting
148        }
149
150        let wait_for_stable_up = async {
151            loop {
152                <Self as __ButtonMonitor>::wait_until_pressed_state(self, false).await;
153                Timer::after(BUTTON_DEBOUNCE_DELAY).await;
154                if !self.is_pressed() {
155                    break;
156                }
157            }
158        };
159
160        match select(wait_for_stable_up, Timer::after(LONG_PRESS_DURATION)).await {
161            Either::First(_) => PressDuration::Short,
162            Either::Second(()) => PressDuration::Long,
163        }
164    }
165}
166
167// ============================================================================
168// PressedTo - How the button is wired
169// ============================================================================
170
171/// Describes if the button connects to voltage or ground when pressed.
172#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
173#[cfg_attr(feature = "defmt", derive(defmt::Format))]
174pub enum PressedTo {
175    /// Button connects pin to voltage (3.3V) when pressed.
176    /// Uses internal pull-down resistor. Pin reads HIGH when pressed.
177    Voltage,
178
179    /// Button connects pin to ground (GND) when pressed.
180    /// Uses internal pull-up resistor. Pin reads LOW when pressed.
181    Ground,
182}
183
184impl PressedTo {
185    /// Returns `true` when a high input level means "pressed".
186    #[must_use]
187    pub const fn pressed_is_high(self) -> bool {
188        matches!(self, Self::Voltage)
189    }
190
191    /// Evaluates whether the button is pressed for a sampled logic level.
192    #[must_use]
193    pub const fn is_pressed(self, level_is_high: bool) -> bool {
194        if self.pressed_is_high() {
195            level_is_high
196        } else {
197            !level_is_high
198        }
199    }
200}
201
202// ============================================================================
203// PressDuration - Button press type
204// ============================================================================
205
206/// Duration of a button press (short or long).
207#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
208#[cfg_attr(feature = "defmt", derive(defmt::Format))]
209pub enum PressDuration {
210    /// Button was held for less than [`LONG_PRESS_DURATION`] (500ms).
211    Short,
212    /// Button was held for at least [`LONG_PRESS_DURATION`] (500ms).
213    Long,
214}
215
216// ============================================================================
217// Tests
218// ============================================================================
219
220#[cfg(test)]
221mod tests {
222    use super::PressedTo;
223
224    #[test]
225    fn pressed_to_ground_maps_low_to_pressed() {
226        assert!(PressedTo::Ground.is_pressed(false));
227        assert!(!PressedTo::Ground.is_pressed(true));
228    }
229
230    #[test]
231    fn pressed_to_voltage_maps_high_to_pressed() {
232        assert!(!PressedTo::Voltage.is_pressed(false));
233        assert!(PressedTo::Voltage.is_pressed(true));
234    }
235}