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}