From f57ecd19aa2f2d9d50c9125a51e80f83312a6e8c Mon Sep 17 00:00:00 2001 From: Jet Date: Mon, 23 Mar 2026 13:21:42 -0700 Subject: [PATCH] fix: active poll instead of wait for interupt --- pi/README.md | 2 +- pi/module.nix | 13 +++++--- pi/pi-service/src/main.rs | 70 +++++++++++++++++++++++---------------- scripts/deploy-pios-pi.sh | 2 +- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/pi/README.md b/pi/README.md index 16ca81a..fe555c7 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_SECS` | `5` | Debounce delay in seconds | +| `NOISEBELL_DEBOUNCE_MS` | `50` | Debounce delay in milliseconds | | `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 b539712..b8d6309 100644 --- a/pi/module.nix +++ b/pi/module.nix @@ -14,10 +14,10 @@ in description = "GPIO pin number to monitor."; }; - debounceSecs = lib.mkOption { + debounceMs = lib.mkOption { type = lib.types.ints.positive; - default = 5; - description = "Debounce delay in seconds."; + default = 50; + description = "Debounce delay in milliseconds."; }; port = lib.mkOption { @@ -88,12 +88,15 @@ 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_SECS = toString cfg.debounceSecs; + NOISEBELL_DEBOUNCE_MS = toString cfg.debounceMs; 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 780d6b7..ca984cf 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, Edge, EdgeDetect, Options}; +use gpiod::{Bias, Chip, 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_secs: u64 = std::env::var("NOISEBELL_DEBOUNCE_SECS") - .unwrap_or_else(|_| "5".into()) + let debounce_ms: u64 = std::env::var("NOISEBELL_DEBOUNCE_MS") + .unwrap_or_else(|_| "50".into()) .parse() - .context("NOISEBELL_DEBOUNCE_SECS must be a valid u64")?; + .context("NOISEBELL_DEBOUNCE_MS 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_secs, port, %endpoint_url, "starting noisebell"); + info!(gpio_pin, debounce_ms, port, %endpoint_url, "starting noisebell"); let chip = Chip::new("gpiochip0").context("failed to open gpiochip0")?; @@ -143,12 +143,13 @@ async fn main() -> Result<()> { Bias::PullDown }; - // Request the line with edge detection for monitoring + // 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. let opts = Options::input([gpio_pin]) .bias(bias) - .edge(EdgeDetect::Both) .consumer("noisebell"); - let mut inputs = chip + let inputs = chip .request_lines(opts) .context(format!("failed to request GPIO line {gpio_pin}"))?; @@ -183,47 +184,58 @@ async fn main() -> Result<()> { // Sync initial state with the cache on startup let _ = tx.send((initial_state.as_door_status(), now)); - // 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. + // Poll the input level and debounce in software. This is less elegant than + // edge-triggered reads, but it is robust on Raspberry Pi OS. let state_for_edges = state.clone(); let edge_tx = tx.clone(); let _edge_handle = std::thread::spawn(move || { - let mut last_event_time = std::time::Instant::now() - Duration::from_secs(debounce_secs); - let debounce = Duration::from_secs(debounce_secs); + 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(); loop { - let event = match inputs.read_event() { - Ok(event) => event, - Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + let values = match inputs.get_values([false]) { + Ok(values) => values, Err(e) => { - error!(error = %e, "failed to read GPIO event"); + error!(error = %e, "failed to read GPIO value"); std::thread::sleep(Duration::from_secs(1)); continue; } }; - // 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_raw_level = if values[0] { + SignalLevel::High + } else { + SignalLevel::Low }; let new_state = LocalDoorState::from_raw_level(new_raw_level, active_level); - let previous_state = - LocalDoorState::from_atomic(state_for_edges.door_state.swap(new_state as u8, Ordering::Relaxed)); - if previous_state != new_state { + 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 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/scripts/deploy-pios-pi.sh b/scripts/deploy-pios-pi.sh index d1edbaa..11242a1 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_SECS=5 +NOISEBELL_DEBOUNCE_MS=50 NOISEBELL_PORT=80 NOISEBELL_RETRY_ATTEMPTS=3 NOISEBELL_RETRY_BASE_DELAY_SECS=1