Skip to main content

m5stack_core/io/
buttons.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Unified front-panel button events.
3//!
4//! Both input flavours of the M5Stack Core family emit the same
5//! [`ButtonEvent`]: the Fire27's three physical buttons ([`Buttons`], feature
6//! `buttons`) and the CoreS3's touch-strip emulation
7//! ([`crate::io::touch_buttons::TouchButtons`]). An application maps these to
8//! its own events in one place, identically for both boards.
9
10use esp_hal::gpio::AnyPin;
11
12/// Which of the three front-panel positions fired. On the Fire27 these are
13/// buttons A/B/C; on the CoreS3, the left/center/right touch zones.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ButtonId {
16    Left,
17    Center,
18    Right,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ButtonAction {
23    /// Short press(es); the count is the number of consecutive taps.
24    Short(usize),
25    Long,
26}
27
28/// A generic, positional input event — the #32 I1/I6 contract: it carries only
29/// *where* (`id`) and *what kind* (`action`), never app semantics (no
30/// Inc/Dec/Ok, no nav). Both the Fire27 physical buttons and the CoreS3 touch
31/// zones emit this same type, so a consumer's input router maps it to meaning;
32/// the BSP never does. Do not add app-vocabulary variants here.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct ButtonEvent {
35    pub id: ButtonId,
36    pub action: ButtonAction,
37}
38
39/// Press-decoding timing shared by **both** front-panel drivers — the Fire27
40/// physical buttons (`ButtonResources::into_buttons_with_timing`, feature
41/// `buttons`) and the CoreS3 touch strip
42/// ([`TouchButtonsConfig::with_timing`](crate::io::touch_buttons::TouchButtonsConfig::with_timing)).
43/// Milliseconds (the touch driver's native unit; the Fire27 `async-button`
44/// `Duration`s are built from these).
45///
46/// The knob that matters for latency is `multi_tap_ms`: both drivers count
47/// consecutive taps, which delays *every* single press by that window. Set it to
48/// `0` (see [`immediate_single_press`](Self::immediate_single_press)) for
49/// latency-sensitive input like an encoder — a downstream accumulator can still
50/// sum immediate `Short(1)` events into multi-step (#58).
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct ButtonTiming {
53    /// Hold time before a press becomes [`ButtonAction::Long`].
54    pub long_press_ms: u64,
55    /// Max gap between consecutive taps still counted as one multi-tap sequence.
56    /// `0` disables counting: each press emits [`Short(1)`](ButtonAction::Short)
57    /// as soon as it is debounced, with no counting delay.
58    pub multi_tap_ms: u64,
59}
60
61impl ButtonTiming {
62    /// Count multi-taps (the historical behaviour): a single press is delayed by
63    /// the `multi_tap_ms` window so consecutive taps can be counted. 500 ms
64    /// long-press, 300 ms multi-tap window.
65    pub const fn multi_tap() -> Self {
66        Self { long_press_ms: 500, multi_tap_ms: 300 }
67    }
68
69    /// Report every press immediately — multi-tap counting disabled
70    /// (`multi_tap_ms = 0`). For latency-sensitive input (e.g. an encoder). Keeps
71    /// the [`multi_tap`](Self::multi_tap) long-press threshold.
72    pub const fn immediate_single_press() -> Self {
73        Self { long_press_ms: Self::multi_tap().long_press_ms, multi_tap_ms: 0 }
74    }
75}
76
77/// The unified opt-in baseline is [`multi_tap`](Self::multi_tap). Note the legacy
78/// entry points (`ButtonResources::into_buttons`,
79/// [`TouchButtons::new`](crate::io::touch_buttons::TouchButtons::new) with
80/// [`TouchButtonsConfig::default`](crate::io::touch_buttons::TouchButtonsConfig::default))
81/// keep each driver's *original* per-board timings for backward compatibility;
82/// this default is only used where a `ButtonTiming` is taken explicitly.
83impl Default for ButtonTiming {
84    fn default() -> Self {
85        Self::multi_tap()
86    }
87}
88
89/// The three front-panel button pins (Fire27: A=GPIO39, B=GPIO38, C=GPIO37,
90/// active-low). Wired by [`crate::board::fire27::Board::split`].
91pub struct ButtonResources<'a> {
92    pub left: AnyPin<'a>,
93    pub center: AnyPin<'a>,
94    pub right: AnyPin<'a>,
95}
96
97#[cfg(feature = "buttons")]
98mod physical {
99    use async_button::{Button, ButtonConfig, ButtonEvent as AsyncButtonEvent, Mode};
100    use embassy_futures::select::{Either3, select3};
101    use embassy_time_04::Duration;
102    use esp_hal::gpio::{Input, InputConfig, Pull};
103
104    use super::{ButtonAction, ButtonEvent, ButtonId, ButtonResources, ButtonTiming};
105
106    /// Map the cross-driver [`ButtonTiming`] onto `async-button`'s [`ButtonConfig`]:
107    /// `multi_tap_ms → double_click`, `long_press_ms → long_press`. Debounce keeps
108    /// async-button's default; `mode` is [`Mode::PullUp`] — the BSP owns the
109    /// active-low + pull-up wiring, the caller owns only the timings. The
110    /// `Duration`s are `embassy-time` 0.4 (async-button's pin), built here from ms.
111    fn config_from_timing(timing: ButtonTiming) -> ButtonConfig {
112        ButtonConfig {
113            double_click: Duration::from_millis(timing.multi_tap_ms),
114            long_press: Duration::from_millis(timing.long_press_ms),
115            mode: Mode::PullUp,
116            ..ButtonConfig::default()
117        }
118    }
119
120    impl ButtonResources<'static> {
121        /// Wrap the pins in debounced [`async_button`] drivers with async-button's
122        /// default timings (pull-ups on). A single press is delayed by the multi-tap
123        /// window (~350 ms) so consecutive taps can be counted; for a different
124        /// window — or immediate single-press — use
125        /// [`into_buttons_with_timing`](Self::into_buttons_with_timing).
126        pub fn into_buttons(self) -> Buttons<'static> {
127            self.make_buttons(ButtonConfig::default())
128        }
129
130        /// Like [`into_buttons`](Self::into_buttons) but driven by the cross-driver
131        /// [`ButtonTiming`] — the same type and presets the CoreS3 touch strip takes
132        /// ([`TouchButtonsConfig::with_timing`](crate::io::touch_buttons::TouchButtonsConfig::with_timing)).
133        /// Pass [`ButtonTiming::immediate_single_press`] to trade the multi-tap
134        /// counting delay for instant response, or a custom window / long-press.
135        pub fn into_buttons_with_timing(self, timing: ButtonTiming) -> Buttons<'static> {
136            self.make_buttons(config_from_timing(timing))
137        }
138
139        fn make_buttons(self, config: ButtonConfig) -> Buttons<'static> {
140            let make = |pin| {
141                Button::new(
142                    Input::new(pin, InputConfig::default().with_pull(Pull::Up)),
143                    config,
144                )
145            };
146            Buttons {
147                left: make(self.left),
148                center: make(self.center),
149                right: make(self.right),
150            }
151        }
152    }
153
154    /// The three debounced front-panel buttons.
155    pub struct Buttons<'a> {
156        left: Button<Input<'a>>,
157        center: Button<Input<'a>>,
158        right: Button<Input<'a>>,
159    }
160
161    impl Buttons<'_> {
162        /// Wait for the next debounced event on any of the three buttons.
163        /// No polling — the underlying driver awaits pin edges.
164        pub async fn next_event(&mut self) -> ButtonEvent {
165            let (id, event) = match select3(
166                self.left.update(),
167                self.center.update(),
168                self.right.update(),
169            )
170            .await
171            {
172                Either3::First(e) => (ButtonId::Left, e),
173                Either3::Second(e) => (ButtonId::Center, e),
174                Either3::Third(e) => (ButtonId::Right, e),
175            };
176            let action = match event {
177                AsyncButtonEvent::ShortPress { count } => ButtonAction::Short(count),
178                AsyncButtonEvent::LongPress => ButtonAction::Long,
179            };
180            ButtonEvent { id, action }
181        }
182    }
183}
184
185#[cfg(feature = "buttons")]
186pub use physical::Buttons;