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}