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 |
|---|---|---|
| `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 |

View file

@ -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;

View file

@ -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

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'
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