From 3f519376b24467ddbc544094bee77cd747d66038 Mon Sep 17 00:00:00 2001 From: Jet Pham <55770902+jetpham@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:41:57 -0700 Subject: [PATCH] feat: add a finite state machine for debouncing --- src/gpio.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 4 +++- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/gpio.rs b/src/gpio.rs index 6bdb95d..db2f630 100644 --- a/src/gpio.rs +++ b/src/gpio.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::time::{Duration, Instant}; use std::fmt; use serde::{Serialize, Deserialize}; @@ -11,6 +11,14 @@ pub enum CircuitEvent { Closed, } +#[derive(Debug, PartialEq)] +enum FsmState { + Idle, + DebouncingHigh, + High, + DebouncingLow, +} + impl fmt::Display for CircuitEvent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -23,32 +31,68 @@ impl fmt::Display for CircuitEvent { pub struct GpioMonitor { pin: InputPin, poll_interval: Duration, + debounce_delay: Duration, + state: FsmState, + last_potential_transition_time: Instant, } impl GpioMonitor { - pub fn new(pin_number: u8, poll_interval: Duration) -> Result { + pub fn new(pin_number: u8, poll_interval: Duration, debounce_delay: Duration) -> Result { let gpio = Gpio::new() .context("Failed to initialize GPIO")?; let pin = gpio.get(pin_number) .context(format!("Failed to get GPIO pin {}", pin_number))? .into_input_pullup(); - Ok(Self { pin, poll_interval }) + Ok(Self { + pin, + poll_interval, + debounce_delay, + state: FsmState::Idle, + last_potential_transition_time: Instant::now(), + }) } pub async fn monitor(&mut self, mut callback: F) -> Result<()> where F: FnMut(CircuitEvent) + Send + 'static, { - let mut previous_state = self.get_current_state(); - callback(previous_state); // Send initial state - loop { - let current_state = self.get_current_state(); + let current_switch_reading = self.get_current_state() == CircuitEvent::Closed; + let time_since_last_change = self.last_potential_transition_time.elapsed(); - if current_state != previous_state { - callback(current_state); - previous_state = current_state; + match self.state { + FsmState::Idle => { + if current_switch_reading { + self.state = FsmState::DebouncingHigh; + self.last_potential_transition_time = Instant::now(); + } + } + + FsmState::DebouncingHigh => { + if !current_switch_reading { + self.state = FsmState::Idle; + } else if time_since_last_change >= self.debounce_delay { + self.state = FsmState::High; + callback(CircuitEvent::Closed); + } + } + + FsmState::High => { + if !current_switch_reading { + self.state = FsmState::DebouncingLow; + self.last_potential_transition_time = Instant::now(); + } + } + + FsmState::DebouncingLow => { + if current_switch_reading { + self.state = FsmState::High; + } else if time_since_last_change >= self.debounce_delay { + self.state = FsmState::Idle; + callback(CircuitEvent::Open); + } + } } tokio::time::sleep(self.poll_interval).await; diff --git a/src/main.rs b/src/main.rs index bcaf0ff..9c2fa04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; const DEFAULT_GPIO_PIN: u8 = 17; const DEFAULT_POLL_INTERVAL_MS: u64 = 100; +const DEFAULT_DEBOUNCE_DELAY_SECS: u64 = 5; const LOG_DIR: &str = "logs"; const LOG_PREFIX: &str = "noisebell"; const LOG_SUFFIX: &str = "log"; @@ -54,7 +55,8 @@ async fn main() -> Result<()> { info!("initializing gpio monitor"); let mut gpio_monitor = gpio::GpioMonitor::new( DEFAULT_GPIO_PIN, - Duration::from_millis(DEFAULT_POLL_INTERVAL_MS) + Duration::from_millis(DEFAULT_POLL_INTERVAL_MS), + Duration::from_secs(DEFAULT_DEBOUNCE_DELAY_SECS) )?; // Set up the callback for state changes