fix: active poll instead of wait for interupt
This commit is contained in:
parent
8554f7a8ad
commit
f57ecd19aa
4 changed files with 51 additions and 36 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue