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 |
|
| 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 |
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue