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;