#![no_std]
#![doc = include_str!("../README.md")]
use embedded_hal::digital::InputPin;
pub type Millis = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActiveLevel {
High,
Low,
}
impl ActiveLevel {
#[inline]
const fn state_from_high(self, is_high: bool) -> ButtonState {
match (self, is_high) {
(Self::High, true) | (Self::Low, false) => ButtonState::Pressed,
(Self::High, false) | (Self::Low, true) => ButtonState::Released,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonState {
Pressed,
Released,
}
impl ButtonState {
#[inline]
pub const fn is_pressed(self) -> bool {
matches!(self, Self::Pressed)
}
#[inline]
pub const fn is_released(self) -> bool {
matches!(self, Self::Released)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ButtonConfig {
pub debounce_ms: Millis,
pub double_click_ms: Millis,
pub hold_ms: Millis,
pub multi_click_count: u8,
pub multi_click_timeout_ms: Millis,
pub active_level: ActiveLevel,
}
impl ButtonConfig {
pub const DEFAULT_DEBOUNCE_MS: Millis = 20;
pub const DEFAULT_DOUBLE_CLICK_MS: Millis = 300;
pub const DEFAULT_HOLD_MS: Millis = 800;
pub const DEFAULT_MULTI_CLICK_COUNT: u8 = 0;
pub const DEFAULT_MULTI_CLICK_TIMEOUT_MS: Millis = 3_000;
#[inline]
pub const fn active_low() -> Self {
Self {
debounce_ms: Self::DEFAULT_DEBOUNCE_MS,
double_click_ms: Self::DEFAULT_DOUBLE_CLICK_MS,
hold_ms: Self::DEFAULT_HOLD_MS,
multi_click_count: Self::DEFAULT_MULTI_CLICK_COUNT,
multi_click_timeout_ms: Self::DEFAULT_MULTI_CLICK_TIMEOUT_MS,
active_level: ActiveLevel::Low,
}
}
#[inline]
pub const fn active_high() -> Self {
Self {
active_level: ActiveLevel::High,
..Self::active_low()
}
}
#[inline]
pub const fn with_debounce_ms(mut self, debounce_ms: Millis) -> Self {
self.debounce_ms = debounce_ms;
self
}
#[inline]
pub const fn with_double_click_ms(mut self, double_click_ms: Millis) -> Self {
self.double_click_ms = double_click_ms;
self
}
#[inline]
pub const fn with_hold_ms(mut self, hold_ms: Millis) -> Self {
self.hold_ms = hold_ms;
self
}
#[inline]
pub const fn with_multi_click_count(mut self, multi_click_count: u8) -> Self {
self.multi_click_count = multi_click_count;
self
}
#[inline]
pub const fn with_multi_click_timeout_ms(mut self, multi_click_timeout_ms: Millis) -> Self {
self.multi_click_timeout_ms = multi_click_timeout_ms;
self
}
#[inline]
pub const fn with_multi_click(
mut self,
multi_click_count: u8,
multi_click_timeout_ms: Millis,
) -> Self {
self.multi_click_count = multi_click_count;
self.multi_click_timeout_ms = multi_click_timeout_ms;
self
}
}
impl Default for ButtonConfig {
#[inline]
fn default() -> Self {
Self::active_low()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ButtonEvents {
pub pressed: bool,
pub released: bool,
pub click: bool,
pub double_click: bool,
pub multi_click: bool,
pub hold: bool,
}
impl ButtonEvents {
#[inline]
pub const fn any(self) -> bool {
self.pressed
|| self.released
|| self.click
|| self.double_click
|| self.multi_click
|| self.hold
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ButtonUpdate {
pub state: ButtonState,
pub events: ButtonEvents,
pub held_ms: Option<Millis>,
pub released_after_ms: Option<Millis>,
pub multi_click_count: u8,
}
impl ButtonUpdate {
#[inline]
const fn idle(state: ButtonState, held_ms: Option<Millis>, multi_click_count: u8) -> Self {
Self {
state,
events: ButtonEvents {
pressed: false,
released: false,
click: false,
double_click: false,
multi_click: false,
hold: false,
},
held_ms,
released_after_ms: None,
multi_click_count,
}
}
}
pub struct Button<PIN> {
pin: PIN,
config: ButtonConfig,
raw_state: ButtonState,
raw_changed_at: Millis,
stable_state: ButtonState,
pressed_at: Option<Millis>,
pending_click_at: Option<Millis>,
multi_click_seen: u8,
multi_click_last_at: Option<Millis>,
hold_reported: bool,
}
impl<PIN> Button<PIN> {
#[inline]
pub const fn new(pin: PIN) -> Self {
Self::with_config(pin, ButtonConfig::active_low())
}
#[inline]
pub const fn with_config(pin: PIN, config: ButtonConfig) -> Self {
Self {
pin,
config,
raw_state: ButtonState::Released,
raw_changed_at: 0,
stable_state: ButtonState::Released,
pressed_at: None,
pending_click_at: None,
multi_click_seen: 0,
multi_click_last_at: None,
hold_reported: false,
}
}
#[inline]
pub const fn config(&self) -> ButtonConfig {
self.config
}
#[inline]
pub fn set_config(&mut self, config: ButtonConfig) {
self.config = config;
if config.multi_click_count < 2 {
self.reset_multi_click();
}
}
#[inline]
pub const fn debounce_ms(&self) -> Millis {
self.config.debounce_ms
}
#[inline]
pub fn set_debounce_ms(&mut self, debounce_ms: Millis) {
self.config.debounce_ms = debounce_ms;
}
#[inline]
pub const fn double_click_ms(&self) -> Millis {
self.config.double_click_ms
}
#[inline]
pub fn set_double_click_ms(&mut self, double_click_ms: Millis) {
self.config.double_click_ms = double_click_ms;
}
#[inline]
pub const fn hold_ms(&self) -> Millis {
self.config.hold_ms
}
#[inline]
pub fn set_hold_ms(&mut self, hold_ms: Millis) {
self.config.hold_ms = hold_ms;
}
#[inline]
pub const fn multi_click_count(&self) -> u8 {
self.config.multi_click_count
}
#[inline]
pub fn set_multi_click_count(&mut self, multi_click_count: u8) {
self.config.multi_click_count = multi_click_count;
if multi_click_count < 2 {
self.reset_multi_click();
}
}
#[inline]
pub const fn multi_click_timeout_ms(&self) -> Millis {
self.config.multi_click_timeout_ms
}
#[inline]
pub fn set_multi_click_timeout_ms(&mut self, multi_click_timeout_ms: Millis) {
self.config.multi_click_timeout_ms = multi_click_timeout_ms;
}
#[inline]
pub fn reset_multi_click(&mut self) {
self.multi_click_seen = 0;
self.multi_click_last_at = None;
}
#[inline]
fn visible_multi_click_count(&self) -> u8 {
if self.config.multi_click_count < 2 {
0
} else {
self.multi_click_seen
}
}
#[inline]
pub const fn state(&self) -> ButtonState {
self.stable_state
}
#[inline]
pub const fn is_pressed(&self) -> bool {
self.stable_state.is_pressed()
}
#[inline]
pub const fn is_released(&self) -> bool {
self.stable_state.is_released()
}
#[inline]
pub fn held_ms(&self, now_ms: Millis) -> Option<Millis> {
self.pressed_at
.map(|pressed_at| elapsed(now_ms, pressed_at))
}
#[inline]
pub fn pin_mut(&mut self) -> &mut PIN {
&mut self.pin
}
#[inline]
pub fn into_inner(self) -> PIN {
self.pin
}
}
impl<PIN> Button<PIN>
where
PIN: InputPin,
{
pub fn update(&mut self, now_ms: Millis) -> Result<ButtonUpdate, PIN::Error> {
let raw_state = self.read_raw_state()?;
self.reset_expired_multi_click(now_ms);
if raw_state != self.raw_state {
self.raw_state = raw_state;
self.raw_changed_at = now_ms;
}
let mut update = ButtonUpdate::idle(
self.stable_state,
self.held_ms(now_ms),
self.visible_multi_click_count(),
);
if self.stable_state != self.raw_state
&& elapsed(now_ms, self.raw_changed_at) >= self.config.debounce_ms
{
self.accept_stable_state(now_ms, &mut update);
}
if self.stable_state == ButtonState::Pressed {
let held_ms = self.held_ms(now_ms).unwrap_or(0);
update.held_ms = Some(held_ms);
if !self.hold_reported && held_ms >= self.config.hold_ms {
update.events.hold = true;
self.hold_reported = true;
}
}
self.report_pending_click(now_ms, &mut update);
update.state = self.stable_state;
Ok(update)
}
#[inline]
fn read_raw_state(&mut self) -> Result<ButtonState, PIN::Error> {
self.pin
.is_high()
.map(|is_high| self.config.active_level.state_from_high(is_high))
}
fn accept_stable_state(&mut self, now_ms: Millis, update: &mut ButtonUpdate) {
self.stable_state = self.raw_state;
match self.stable_state {
ButtonState::Pressed => {
self.pressed_at = Some(now_ms);
self.hold_reported = false;
update.events.pressed = true;
update.held_ms = Some(0);
}
ButtonState::Released => {
let held_ms = self
.pressed_at
.map(|pressed_at| elapsed(now_ms, pressed_at));
self.pressed_at = None;
self.hold_reported = false;
update.events.released = true;
update.held_ms = None;
update.released_after_ms = held_ms;
if held_ms.is_some_and(|held_ms| held_ms < self.config.hold_ms) {
self.register_short_release(now_ms, update);
} else {
self.reset_multi_click();
}
}
}
}
fn register_short_release(&mut self, now_ms: Millis, update: &mut ButtonUpdate) {
let multi_click_fired = self.register_multi_click(now_ms, update);
if self.pending_click_at.is_some_and(|pending_click_at| {
elapsed(now_ms, pending_click_at) <= self.config.double_click_ms
}) {
update.events.double_click = true;
self.pending_click_at = None;
} else if multi_click_fired {
self.pending_click_at = None;
} else {
self.report_pending_click(now_ms, update);
self.pending_click_at = Some(now_ms);
}
}
fn report_pending_click(&mut self, now_ms: Millis, update: &mut ButtonUpdate) {
if self.has_active_multi_click_sequence(now_ms) {
return;
}
if self.pending_click_at.is_some_and(|pending_click_at| {
elapsed(now_ms, pending_click_at) > self.config.double_click_ms
}) {
update.events.click = true;
self.pending_click_at = None;
}
}
fn register_multi_click(&mut self, now_ms: Millis, update: &mut ButtonUpdate) -> bool {
if self.config.multi_click_count < 2 {
update.multi_click_count = 0;
return false;
}
self.reset_expired_multi_click(now_ms);
self.multi_click_seen = self.multi_click_seen.saturating_add(1);
self.multi_click_last_at = Some(now_ms);
update.multi_click_count = self.multi_click_seen;
if self.multi_click_seen >= self.config.multi_click_count {
update.events.multi_click = true;
update.multi_click_count = self.config.multi_click_count;
self.reset_multi_click();
return true;
}
false
}
fn reset_expired_multi_click(&mut self, now_ms: Millis) {
if self
.multi_click_last_at
.is_some_and(|last_at| elapsed(now_ms, last_at) > self.config.multi_click_timeout_ms)
{
self.reset_multi_click();
}
}
fn has_active_multi_click_sequence(&self, now_ms: Millis) -> bool {
self.config.multi_click_count >= 2
&& self.multi_click_seen > 0
&& self.multi_click_last_at.is_some_and(|last_at| {
elapsed(now_ms, last_at) <= self.config.multi_click_timeout_ms
})
}
}
#[inline]
const fn elapsed(now_ms: Millis, earlier_ms: Millis) -> Millis {
now_ms.saturating_sub(earlier_ms)
}
#[cfg(test)]
extern crate std;
#[cfg(test)]
mod tests {
use std::io::ErrorKind;
use embedded_hal_mock::eh1::{
digital::{Mock as PinMock, State as PinState, Transaction as PinTransaction},
MockError,
};
use super::*;
fn config() -> ButtonConfig {
ButtonConfig::active_low()
.with_debounce_ms(10)
.with_double_click_ms(200)
.with_hold_ms(1000)
}
#[test]
fn filters_bounce_and_reports_press_release() {
let expectations = [
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(pin, config());
assert_eq!(button.update(0).unwrap().state, ButtonState::Released);
assert!(!button.update(1).unwrap().events.any());
assert!(!button.update(5).unwrap().events.any());
assert!(!button.update(7).unwrap().events.any());
assert!(!button.update(16).unwrap().events.any());
let update = button.update(17).unwrap();
assert_eq!(update.state, ButtonState::Pressed);
assert!(update.events.pressed);
assert_eq!(update.held_ms, Some(0));
assert!(!button.update(30).unwrap().events.any());
assert!(!button.update(35).unwrap().events.any());
let update = button.update(40).unwrap();
assert_eq!(update.state, ButtonState::Released);
assert!(update.events.released);
assert!(!update.events.click);
assert!(!update.events.double_click);
assert_eq!(update.released_after_ms, Some(23));
let update = button.update(241).unwrap();
assert_eq!(update.state, ButtonState::Released);
assert!(update.events.click);
assert!(!update.events.double_click);
assert_eq!(update.released_after_ms, None);
button.into_inner().done();
}
#[test]
fn detects_double_click() {
let expectations = [
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(pin, config().with_debounce_ms(5).with_hold_ms(1000));
assert!(!button.update(0).unwrap().events.any());
assert!(button.update(5).unwrap().events.pressed);
assert!(!button.update(20).unwrap().events.any());
let update = button.update(25).unwrap();
assert!(update.events.released);
assert!(!update.events.click);
assert!(!update.events.double_click);
assert!(!button.update(80).unwrap().events.any());
assert!(button.update(85).unwrap().events.pressed);
assert!(!button.update(100).unwrap().events.any());
let update = button.update(105).unwrap();
assert!(update.events.released);
assert!(!update.events.click);
assert!(update.events.double_click);
button.into_inner().done();
}
#[test]
fn detects_configured_multi_click_count() {
let expectations = [
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(
pin,
config()
.with_debounce_ms(5)
.with_hold_ms(1000)
.with_multi_click(5, 3_000),
);
assert_eq!(button.multi_click_count(), 5);
assert_eq!(button.multi_click_timeout_ms(), 3_000);
assert!(!button.update(0).unwrap().events.any());
assert!(button.update(5).unwrap().events.pressed);
assert!(!button.update(20).unwrap().events.any());
let update = button.update(25).unwrap();
assert!(update.events.released);
assert_eq!(update.multi_click_count, 1);
assert!(!update.events.multi_click);
assert!(!button.update(80).unwrap().events.any());
assert!(button.update(85).unwrap().events.pressed);
assert!(!button.update(100).unwrap().events.any());
let update = button.update(105).unwrap();
assert!(update.events.released);
assert_eq!(update.multi_click_count, 2);
assert!(!update.events.multi_click);
assert!(!button.update(160).unwrap().events.any());
assert!(button.update(165).unwrap().events.pressed);
assert!(!button.update(180).unwrap().events.any());
let update = button.update(185).unwrap();
assert!(update.events.released);
assert_eq!(update.multi_click_count, 3);
assert!(!update.events.multi_click);
assert!(!button.update(240).unwrap().events.any());
assert!(button.update(245).unwrap().events.pressed);
assert!(!button.update(260).unwrap().events.any());
let update = button.update(265).unwrap();
assert!(update.events.released);
assert_eq!(update.multi_click_count, 4);
assert!(!update.events.multi_click);
assert!(!button.update(320).unwrap().events.any());
assert!(button.update(325).unwrap().events.pressed);
assert!(!button.update(340).unwrap().events.any());
let update = button.update(345).unwrap();
assert!(update.events.released);
assert_eq!(update.multi_click_count, 5);
assert!(update.events.multi_click);
button.into_inner().done();
}
#[test]
fn resets_multi_click_count_after_timeout() {
let expectations = [
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(
pin,
config()
.with_debounce_ms(5)
.with_hold_ms(1000)
.with_multi_click(3, 3_000),
);
assert!(!button.update(0).unwrap().events.any());
assert!(button.update(5).unwrap().events.pressed);
assert!(!button.update(20).unwrap().events.any());
let update = button.update(25).unwrap();
assert_eq!(update.multi_click_count, 1);
assert!(!update.events.multi_click);
assert!(!button.update(80).unwrap().events.any());
assert!(button.update(85).unwrap().events.pressed);
assert!(!button.update(100).unwrap().events.any());
let update = button.update(105).unwrap();
assert_eq!(update.multi_click_count, 2);
assert!(!update.events.multi_click);
let update = button.update(3106).unwrap();
assert!(!update.events.any());
assert_eq!(update.multi_click_count, 0);
assert!(!button.update(3200).unwrap().events.any());
assert!(button.update(3205).unwrap().events.pressed);
assert!(!button.update(3220).unwrap().events.any());
let update = button.update(3225).unwrap();
assert_eq!(update.multi_click_count, 1);
assert!(!update.events.multi_click);
button.into_inner().done();
}
#[test]
fn configurable_double_click_can_use_multi_click_timeout() {
let expectations = [
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(
pin,
config()
.with_debounce_ms(5)
.with_double_click_ms(200)
.with_hold_ms(1000)
.with_multi_click(2, 3_000),
);
assert!(!button.update(0).unwrap().events.any());
assert!(button.update(5).unwrap().events.pressed);
assert!(!button.update(20).unwrap().events.any());
let update = button.update(25).unwrap();
assert!(update.events.released);
assert_eq!(update.multi_click_count, 1);
assert!(!update.events.click);
assert!(!update.events.double_click);
assert!(!update.events.multi_click);
assert!(!button.update(1000).unwrap().events.any());
assert!(button.update(1005).unwrap().events.pressed);
assert!(!button.update(1020).unwrap().events.any());
let update = button.update(1025).unwrap();
assert!(update.events.released);
assert!(!update.events.click);
assert!(!update.events.double_click);
assert_eq!(update.multi_click_count, 2);
assert!(update.events.multi_click);
button.into_inner().done();
}
#[test]
fn reports_hold_once_and_tracks_hold_time() {
let expectations = [
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(pin, config().with_debounce_ms(5).with_hold_ms(50));
assert!(!button.update(0).unwrap().events.any());
assert!(button.update(5).unwrap().events.pressed);
let update = button.update(54).unwrap();
assert_eq!(update.held_ms, Some(49));
assert!(!update.events.hold);
let update = button.update(55).unwrap();
assert_eq!(update.held_ms, Some(50));
assert!(update.events.hold);
let update = button.update(80).unwrap();
assert_eq!(update.held_ms, Some(75));
assert!(!update.events.hold);
assert!(!button.update(100).unwrap().events.any());
let update = button.update(105).unwrap();
assert!(update.events.released);
assert!(!update.events.click);
assert_eq!(update.released_after_ms, Some(100));
button.into_inner().done();
}
#[test]
fn supports_active_high_and_runtime_debounce_setting() {
let expectations = [
PinTransaction::get(PinState::Low),
PinTransaction::get(PinState::High),
PinTransaction::get(PinState::High),
];
let pin = PinMock::new(&expectations);
let mut button = Button::with_config(pin, ButtonConfig::active_high());
button.set_debounce_ms(3);
assert_eq!(button.debounce_ms(), 3);
assert_eq!(button.update(0).unwrap().state, ButtonState::Released);
assert!(!button.update(1).unwrap().events.any());
let update = button.update(4).unwrap();
assert_eq!(update.state, ButtonState::Pressed);
assert!(update.events.pressed);
button.into_inner().done();
}
#[test]
fn propagates_pin_errors() {
let expectations =
[PinTransaction::get(PinState::High).with_error(MockError::Io(ErrorKind::Other))];
let pin = PinMock::new(&expectations);
let mut button = Button::new(pin);
assert!(button.update(0).is_err());
button.into_inner().done();
}
}