feat: add a finite state machine for debouncing

This commit is contained in:
Jet Pham 2025-06-07 12:41:57 -07:00
parent 93f5d8e904
commit 3f519376b2
No known key found for this signature in database
2 changed files with 57 additions and 11 deletions

View file

@ -1,4 +1,4 @@
use std::time::Duration; use std::time::{Duration, Instant};
use std::fmt; use std::fmt;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -11,6 +11,14 @@ pub enum CircuitEvent {
Closed, Closed,
} }
#[derive(Debug, PartialEq)]
enum FsmState {
Idle,
DebouncingHigh,
High,
DebouncingLow,
}
impl fmt::Display for CircuitEvent { impl fmt::Display for CircuitEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -23,32 +31,68 @@ impl fmt::Display for CircuitEvent {
pub struct GpioMonitor { pub struct GpioMonitor {
pin: InputPin, pin: InputPin,
poll_interval: Duration, poll_interval: Duration,
debounce_delay: Duration,
state: FsmState,
last_potential_transition_time: Instant,
} }
impl GpioMonitor { impl GpioMonitor {
pub fn new(pin_number: u8, poll_interval: Duration) -> Result<Self> { pub fn new(pin_number: u8, poll_interval: Duration, debounce_delay: Duration) -> Result<Self> {
let gpio = Gpio::new() let gpio = Gpio::new()
.context("Failed to initialize GPIO")?; .context("Failed to initialize GPIO")?;
let pin = gpio.get(pin_number) let pin = gpio.get(pin_number)
.context(format!("Failed to get GPIO pin {}", pin_number))? .context(format!("Failed to get GPIO pin {}", pin_number))?
.into_input_pullup(); .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<F>(&mut self, mut callback: F) -> Result<()> pub async fn monitor<F>(&mut self, mut callback: F) -> Result<()>
where where
F: FnMut(CircuitEvent) + Send + 'static, F: FnMut(CircuitEvent) + Send + 'static,
{ {
let mut previous_state = self.get_current_state();
callback(previous_state); // Send initial state
loop { 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 { match self.state {
callback(current_state); FsmState::Idle => {
previous_state = current_state; 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; tokio::time::sleep(self.poll_interval).await;

View file

@ -13,6 +13,7 @@ use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
const DEFAULT_GPIO_PIN: u8 = 17; const DEFAULT_GPIO_PIN: u8 = 17;
const DEFAULT_POLL_INTERVAL_MS: u64 = 100; const DEFAULT_POLL_INTERVAL_MS: u64 = 100;
const DEFAULT_DEBOUNCE_DELAY_SECS: u64 = 5;
const LOG_DIR: &str = "logs"; const LOG_DIR: &str = "logs";
const LOG_PREFIX: &str = "noisebell"; const LOG_PREFIX: &str = "noisebell";
const LOG_SUFFIX: &str = "log"; const LOG_SUFFIX: &str = "log";
@ -54,7 +55,8 @@ async fn main() -> Result<()> {
info!("initializing gpio monitor"); info!("initializing gpio monitor");
let mut gpio_monitor = gpio::GpioMonitor::new( let mut gpio_monitor = gpio::GpioMonitor::new(
DEFAULT_GPIO_PIN, 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 // Set up the callback for state changes