fix: active poll instead of wait for interupt

This commit is contained in:
Jet 2026-03-23 13:21:42 -07:00
parent 8554f7a8ad
commit f57ecd19aa
No known key found for this signature in database
4 changed files with 51 additions and 36 deletions

View file

@ -162,7 +162,7 @@ The deployed service uses these environment variables:
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `NOISEBELL_GPIO_PIN` | `17` | GPIO pin number | | `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_PORT` | `80` | HTTP server port |
| `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to | | `NOISEBELL_ENDPOINT_URL` | required | Webhook URL to POST state changes to |
| `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count | | `NOISEBELL_RETRY_ATTEMPTS` | `3` | Webhook retry count |

View file

@ -14,10 +14,10 @@ in
description = "GPIO pin number to monitor."; description = "GPIO pin number to monitor.";
}; };
debounceSecs = lib.mkOption { debounceMs = lib.mkOption {
type = lib.types.ints.positive; type = lib.types.ints.positive;
default = 5; default = 50;
description = "Debounce delay in seconds."; description = "Debounce delay in milliseconds.";
}; };
port = lib.mkOption { port = lib.mkOption {
@ -88,12 +88,15 @@ in
systemd.services.noisebell = { systemd.services.noisebell = {
description = "Noisebell GPIO door monitor"; description = "Noisebell GPIO door monitor";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "tailscaled.service" ]; after = [
"network-online.target"
"tailscaled.service"
];
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
environment = { environment = {
NOISEBELL_GPIO_PIN = toString cfg.gpioPin; NOISEBELL_GPIO_PIN = toString cfg.gpioPin;
NOISEBELL_DEBOUNCE_SECS = toString cfg.debounceSecs; NOISEBELL_DEBOUNCE_MS = toString cfg.debounceMs;
NOISEBELL_PORT = toString cfg.port; NOISEBELL_PORT = toString cfg.port;
NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts; NOISEBELL_RETRY_ATTEMPTS = toString cfg.retryAttempts;
NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs; NOISEBELL_RETRY_BASE_DELAY_SECS = toString cfg.retryBaseDelaySecs;

View file

@ -7,7 +7,7 @@ use axum::extract::State;
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::routing::get; use axum::routing::get;
use axum::{Json, Router}; use axum::{Json, Router};
use gpiod::{Bias, Chip, Edge, EdgeDetect, Options}; use gpiod::{Bias, Chip, Options};
use noisebell_common::{ use noisebell_common::{
validate_bearer, DoorStatus, PiStatusResponse, SignalLevel, WebhookPayload, validate_bearer, DoorStatus, PiStatusResponse, SignalLevel, WebhookPayload,
}; };
@ -88,10 +88,10 @@ async fn main() -> Result<()> {
.parse() .parse()
.context("NOISEBELL_GPIO_PIN must be a valid u32")?; .context("NOISEBELL_GPIO_PIN must be a valid u32")?;
let debounce_secs: u64 = std::env::var("NOISEBELL_DEBOUNCE_SECS") let debounce_ms: u64 = std::env::var("NOISEBELL_DEBOUNCE_MS")
.unwrap_or_else(|_| "5".into()) .unwrap_or_else(|_| "50".into())
.parse() .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") let port: u16 = std::env::var("NOISEBELL_PORT")
.unwrap_or_else(|_| "8080".into()) .unwrap_or_else(|_| "8080".into())
@ -133,7 +133,7 @@ async fn main() -> Result<()> {
let inbound_api_key = std::env::var("NOISEBELL_INBOUND_API_KEY") let inbound_api_key = std::env::var("NOISEBELL_INBOUND_API_KEY")
.context("NOISEBELL_INBOUND_API_KEY is required")?; .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")?; let chip = Chip::new("gpiochip0").context("failed to open gpiochip0")?;
@ -143,12 +143,13 @@ async fn main() -> Result<()> {
Bias::PullDown 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]) let opts = Options::input([gpio_pin])
.bias(bias) .bias(bias)
.edge(EdgeDetect::Both)
.consumer("noisebell"); .consumer("noisebell");
let mut inputs = chip let inputs = chip
.request_lines(opts) .request_lines(opts)
.context(format!("failed to request GPIO line {gpio_pin}"))?; .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 // Sync initial state with the cache on startup
let _ = tx.send((initial_state.as_door_status(), now)); let _ = tx.send((initial_state.as_door_status(), now));
// Spawn blocking edge detection task. The async tokio-gpiod path was // Poll the input level and debounce in software. This is less elegant than
// returning repeated EAGAIN on Raspberry Pi OS even when no real GPIO // edge-triggered reads, but it is robust on Raspberry Pi OS.
// error occurred.
let state_for_edges = state.clone(); let state_for_edges = state.clone();
let edge_tx = tx.clone(); let edge_tx = tx.clone();
let _edge_handle = std::thread::spawn(move || { let _edge_handle = std::thread::spawn(move || {
let mut last_event_time = std::time::Instant::now() - Duration::from_secs(debounce_secs); let poll_interval = Duration::from_millis(25);
let debounce = Duration::from_secs(debounce_secs); 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 { loop {
let event = match inputs.read_event() { let values = match inputs.get_values([false]) {
Ok(event) => event, Ok(values) => values,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => { 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)); std::thread::sleep(Duration::from_secs(1));
continue; continue;
} }
}; };
// Software debounce let new_raw_level = if values[0] {
if last_event_time.elapsed() < debounce { SignalLevel::High
continue; } else {
} SignalLevel::Low
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); let new_state = LocalDoorState::from_raw_level(new_raw_level, active_level);
let previous_state = if new_state != pending_state {
LocalDoorState::from_atomic(state_for_edges.door_state.swap(new_state as u8, Ordering::Relaxed)); pending_state = new_state;
if previous_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(); let timestamp = unix_timestamp();
state_for_edges state_for_edges
.last_changed .last_changed
.store(timestamp, Ordering::Relaxed); .store(timestamp, Ordering::Relaxed);
let _ = edge_tx.send((new_state.as_door_status(), timestamp)); 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 drop(tx); // Drop original sender so rx closes when edge_handle is dropped

View file

@ -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' sudo tee /etc/noisebell/noisebell.env >/dev/null <<'ENVEOF'
NOISEBELL_GPIO_PIN=17 NOISEBELL_GPIO_PIN=17
NOISEBELL_DEBOUNCE_SECS=5 NOISEBELL_DEBOUNCE_MS=50
NOISEBELL_PORT=80 NOISEBELL_PORT=80
NOISEBELL_RETRY_ATTEMPTS=3 NOISEBELL_RETRY_ATTEMPTS=3
NOISEBELL_RETRY_BASE_DELAY_SECS=1 NOISEBELL_RETRY_BASE_DELAY_SECS=1