diff --git a/pi/README.md b/pi/README.md index fe555c7..16ca81a 100644 --- a/pi/README.md +++ b/pi/README.md @@ -162,7 +162,7 @@ The deployed service uses these environment variables: | Variable | Default | Description | |---|---|---| | `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number | -| `NOISEBELL_DEBOUNCE_MS` | `50` | Debounce delay in milliseconds | +| `NOISEBELL_DEBOUNCE_SECS` | `5` | Debounce delay in seconds | | `NOISEBELL_PORT` | `80` | HTTP server port | | `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to | | `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count | diff --git a/pi/module.nix b/pi/module.nix index b8d6309..b539712 100644 --- a/pi/module.nix +++ b/pi/module.nix @@ -14,10 +14,10 @@ in description = "GPIO pin number to monitor."; }; - debounceMs = lib.mkOption { + debounceSecs = lib.mkOption { type = lib.types.ints.positive; - default = 50; - description = "Debounce delay in milliseconds."; + default = 5; + description = "Debounce delay in seconds."; }; port = lib.mkOption { @@ -88,15 +88,12 @@ in systemd.services.noisebell = { description = "Noisebell GPIO door monitor"; wantedBy = [ "multi-user.target" ]; - after = [ - "network-online.target" - "tailscaled.service" - ]; + after = [ "network-online.target" "tailscaled.service" ]; wants = [ "network-online.target" ]; environment = { NOISEBELL_GPIO_PIN = toString cfg.gpioPin; - NOISEBELL_DEBOUNCE_MS = toString cfg.debounceMs; + NOISEBELL_DEBOUNCE_SECS = toString cfg.debounceSecs; NOISEBELL_PORT = toString cfg.port; NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts; NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs; diff --git a/pi/pi-service/src/main.rs b/pi/pi-service/src/main.rs index ca984cf..780d6b7 100644 --- a/pi/pi-service/src/main.rs +++ b/pi/pi-service/src/main.rs @@ -7,7 +7,7 @@ use axum::extract::State; use axum::http::{HeaderMap, StatusCode}; use axum::routing::get; use axum::{Json, Router}; -use gpiod::{Bias, Chip, Options}; +use gpiod::{Bias, Chip, Edge, EdgeDetect, Options}; use noisebell_common::{ validate_bearer, DoorStatus, PiStatusResponse, SignalLevel, WebhookPayload, }; @@ -88,10 +88,10 @@ async fn main() -> Result<()> { .parse() .context("NOISEBELL_GPIO_PIN must be a valid u32")?; - let debounce_ms: u64 = std::env::var("NOISEBELL_DEBOUNCE_MS") - .unwrap_or_else(|_| "50".into()) + let debounce_secs: u64 = std::env::var("NOISEBELL_DEBOUNCE_SECS") + .unwrap_or_else(|_| "5".into()) .parse() - .context("NOISEBELL_DEBOUNCE_MS must be a valid u64")?; + .context("NOISEBELL_DEBOUNCE_SECS must be a valid u64")?; let port: u16 = std::env::var("NOISEBELL_PORT") .unwrap_or_else(|_| "8080".into()) @@ -133,7 +133,7 @@ async fn main() -> Result<()> { let inbound_api_key = std::env::var("NOISEBELL_INBOUND_API_KEY") .context("NOISEBELL_INBOUND_API_KEY is required")?; - info!(gpio_pin, debounce_ms, port, %endpoint_url, "starting noisebell"); + info!(gpio_pin, debounce_secs, port, %endpoint_url, "starting noisebell"); let chip = Chip::new("gpiochip0").context("failed to open gpiochip0")?; @@ -143,13 +143,12 @@ async fn main() -> Result<()> { Bias::PullDown }; - // Keep the line requested and poll its value. Edge-triggered reads have - // proven unreliable on Raspberry Pi OS even though the raw line level is - // correct, so we debounce from sampled levels instead. + // Request the line with edge detection for monitoring let opts = Options::input([gpio_pin]) .bias(bias) + .edge(EdgeDetect::Both) .consumer("noisebell"); - let inputs = chip + let mut inputs = chip .request_lines(opts) .context(format!("failed to request GPIO line {gpio_pin}"))?; @@ -184,58 +183,47 @@ async fn main() -> Result<()> { // Sync initial state with the cache on startup let _ = tx.send((initial_state.as_door_status(), now)); - // Poll the input level and debounce in software. This is less elegant than - // edge-triggered reads, but it is robust on Raspberry Pi OS. + // Spawn blocking edge detection task. The async tokio-gpiod path was + // returning repeated EAGAIN on Raspberry Pi OS even when no real GPIO + // error occurred. let state_for_edges = state.clone(); let edge_tx = tx.clone(); let _edge_handle = std::thread::spawn(move || { - let poll_interval = Duration::from_millis(25); - let debounce = Duration::from_millis(debounce_ms); - let mut current_state = initial_state; - let mut pending_state = current_state; - let mut pending_since = std::time::Instant::now(); + let mut last_event_time = std::time::Instant::now() - Duration::from_secs(debounce_secs); + let debounce = Duration::from_secs(debounce_secs); loop { - let values = match inputs.get_values([false]) { - Ok(values) => values, + let event = match inputs.read_event() { + Ok(event) => event, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, Err(e) => { - error!(error = %e, "failed to read GPIO value"); + error!(error = %e, "failed to read GPIO event"); std::thread::sleep(Duration::from_secs(1)); continue; } }; - let new_raw_level = if values[0] { - SignalLevel::High - } else { - SignalLevel::Low + // Software debounce + if last_event_time.elapsed() < debounce { + continue; + } + last_event_time = std::time::Instant::now(); + + let new_raw_level = match event.edge { + Edge::Falling => SignalLevel::Low, + Edge::Rising => SignalLevel::High, }; let new_state = LocalDoorState::from_raw_level(new_raw_level, active_level); - if new_state != pending_state { - pending_state = new_state; - pending_since = std::time::Instant::now(); - } else if new_state != current_state && pending_since.elapsed() >= debounce { - current_state = new_state; - let previous_state = LocalDoorState::from_atomic( - state_for_edges - .door_state - .swap(new_state as u8, Ordering::Relaxed), - ); - - if previous_state == new_state { - std::thread::sleep(poll_interval); - continue; - } - + let previous_state = + LocalDoorState::from_atomic(state_for_edges.door_state.swap(new_state as u8, Ordering::Relaxed)); + if previous_state != new_state { let timestamp = unix_timestamp(); state_for_edges .last_changed .store(timestamp, Ordering::Relaxed); let _ = edge_tx.send((new_state.as_door_status(), timestamp)); } - - std::thread::sleep(poll_interval); } }); drop(tx); // Drop original sender so rx closes when edge_handle is dropped diff --git a/remote/discord-bot/src/main.rs b/remote/discord-bot/src/main.rs index 8b27a8c..39083fb 100644 --- a/remote/discord-bot/src/main.rs +++ b/remote/discord-bot/src/main.rs @@ -52,7 +52,6 @@ fn build_embed(status: DoorStatus, timestamp: u64, image_base_url: &str) -> Crea .title(title) .description(description) .colour(colour) - .field("Since", format_timestamp(timestamp), true) .thumbnail(image_url) .timestamp( serenity::model::Timestamp::from_unix_timestamp(timestamp as i64) diff --git a/scripts/deploy-pios-pi.sh b/scripts/deploy-pios-pi.sh index 11242a1..d1edbaa 100755 --- a/scripts/deploy-pios-pi.sh +++ b/scripts/deploy-pios-pi.sh @@ -84,7 +84,7 @@ sudo chmod 600 /etc/noisebell/pi-to-cache-key /etc/noisebell/cache-to-pi-key /et sudo tee /etc/noisebell/noisebell.env >/dev/null <<'ENVEOF' NOISEBELL_GPIO_PIN=17 -NOISEBELL_DEBOUNCE_MS=50 +NOISEBELL_DEBOUNCE_SECS=5 NOISEBELL_PORT=80 NOISEBELL_RETRY_ATTEMPTS=3 NOISEBELL_RETRY_BASE_DELAY_SECS=1