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#[cfg_attr(
99    feature = "host",
100    doc = "\nHost-side test double: [`crate::memory::ButtonMemory`]."
101)]
102#[cfg_attr(
103    feature = "wasm",
104    doc = "\nBrowser-simulated device: [`crate::wasm::ButtonWasm`]."
105)]
106#[allow(async_fn_in_trait)]
107pub trait Button: __ButtonMonitor {
108    /// Returns whether the button is currently pressed.
109    fn is_pressed(&self) -> bool {
110        <Self as __ButtonMonitor>::is_pressed_raw(self)
111    }
112
113    /// Waits for the next press (button goes down, debounced). Does not wait for release.
114    ///
115    /// See the [Button trait documentation](Self) for usage examples.
116    async fn wait_for_press(&mut self) {
117        loop {
118            <Self as __ButtonMonitor>::wait_until_pressed_state(self, false).await;
119            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
120            if !self.is_pressed() {
121                break;
122            }
123        }
124
125        loop {
126            <Self as __ButtonMonitor>::wait_until_pressed_state(self, true).await;
127            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
128            if self.is_pressed() {
129                break;
130            }
131            // otherwise it was bounce; keep waiting
132        }
133    }
134
135    /// Waits for the next press and returns whether it was short or long (debounced).
136    ///
137    /// Returns as soon as it can decide, so long presses are reported before release.
138    ///
139    /// See the [Button trait documentation](Self) for usage examples.
140    async fn wait_for_press_duration(&mut self) -> PressDuration {
141        loop {
142            <Self as __ButtonMonitor>::wait_until_pressed_state(self, false).await;
143            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
144            if !self.is_pressed() {
145                break;
146            }
147        }
148
149        loop {
150            <Self as __ButtonMonitor>::wait_until_pressed_state(self, true).await;
151            Timer::after(BUTTON_DEBOUNCE_DELAY).await;
152            if self.is_pressed() {
153                break;
154            }
155            // otherwise it was bounce; keep waiting
156        }
157
158        let wait_for_stable_up = async {
159            loop {
160                <Self as __ButtonMonitor>::wait_until_pressed_state(self, false).await;
161                Timer::after(BUTTON_DEBOUNCE_DELAY).await;
162                if !self.is_pressed() {
163                    break;
164                }
165            }
166        };
167
168        match select(wait_for_stable_up, Timer::after(LONG_PRESS_DURATION)).await {
169            Either::First(_) => PressDuration::Short,
170            Either::Second(()) => PressDuration::Long,
171        }
172    }
173}
174
175// ============================================================================
176// PressedTo - How the button is wired
177// ============================================================================
178
179/// Describes if the button connects to voltage or ground when pressed.
180#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
181#[cfg_attr(feature = "defmt", derive(defmt::Format))]
182pub enum PressedTo {
183    /// Button connects pin to voltage (3.3V) when pressed.
184    /// Uses internal pull-down resistor. Pin reads HIGH when pressed.
185    Voltage,
186
187    /// Button connects pin to ground (GND) when pressed.
188    /// Uses internal pull-up resistor. Pin reads LOW when pressed.
189    Ground,
190}
191
192impl PressedTo {
193    /// Returns `true` when a high input level means "pressed".
194    #[must_use]
195    pub const fn pressed_is_high(self) -> bool {
196        matches!(self, Self::Voltage)
197    }
198
199    /// Evaluates whether the button is pressed for a sampled logic level.
200    #[must_use]
201    pub const fn is_pressed(self, level_is_high: bool) -> bool {
202        if self.pressed_is_high() {
203            level_is_high
204        } else {
205            !level_is_high
206        }
207    }
208}
209
210// ============================================================================
211// PressDuration - Button press type
212// ============================================================================
213
214/// Duration of a button press (short or long).
215#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
216#[cfg_attr(feature = "defmt", derive(defmt::Format))]
217pub enum PressDuration {
218    /// Button was held for less than [`LONG_PRESS_DURATION`] (500ms).
219    Short,
220    /// Button was held for at least [`LONG_PRESS_DURATION`] (500ms).
221    Long,
222}
223
224// ============================================================================
225// Tests
226// ============================================================================
227
228#[cfg(test)]
229mod tests {
230    use super::PressedTo;
231
232    #[test]
233    fn pressed_to_ground_maps_low_to_pressed() {
234        assert!(PressedTo::Ground.is_pressed(false));
235        assert!(!PressedTo::Ground.is_pressed(true));
236    }
237
238    #[test]
239    fn pressed_to_voltage_maps_high_to_pressed() {
240        assert!(!PressedTo::Voltage.is_pressed(false));
241        assert!(PressedTo::Voltage.is_pressed(true));
242    }
243}