feat: move to Replaced rppal with tokio-gpiod and other rust things
This commit is contained in:
parent
b2d9406831
commit
773c14e32f
6 changed files with 106 additions and 84 deletions
|
|
@ -7,8 +7,8 @@ use axum::extract::State;
|
|||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use rppal::gpio::{Gpio, Level, Trigger};
|
||||
use serde::Serialize;
|
||||
use tokio_gpiod::{Bias, Chip, Edge, EdgeDetect, Options};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
struct AppState {
|
||||
|
|
@ -178,10 +178,10 @@ async fn main() -> Result<()> {
|
|||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let gpio_pin: u8 = std::env::var("NOISEBELL_GPIO_PIN")
|
||||
let gpio_pin: u32 = std::env::var("NOISEBELL_GPIO_PIN")
|
||||
.unwrap_or_else(|_| "17".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_GPIO_PIN must be a valid u8")?;
|
||||
.context("NOISEBELL_GPIO_PIN must be a valid u32")?;
|
||||
|
||||
let debounce_secs: u64 = std::env::var("NOISEBELL_DEBOUNCE_SECS")
|
||||
.unwrap_or_else(|_| "5".into())
|
||||
|
|
@ -227,18 +227,32 @@ async fn main() -> Result<()> {
|
|||
|
||||
info!(gpio_pin, debounce_secs, port, %endpoint_url, "starting noisebell");
|
||||
|
||||
let gpio = Gpio::new().context("failed to initialize GPIO")?;
|
||||
let pin = gpio
|
||||
.get(gpio_pin)
|
||||
.context(format!("failed to get GPIO pin {gpio_pin}"))?;
|
||||
let pin = if active_low {
|
||||
pin.into_input_pullup()
|
||||
} else {
|
||||
pin.into_input_pulldown()
|
||||
};
|
||||
let chip = Chip::new("gpiochip0")
|
||||
.await
|
||||
.context("failed to open gpiochip0")?;
|
||||
|
||||
let bias = if active_low { Bias::PullUp } else { Bias::PullDown };
|
||||
|
||||
// Request the line with edge detection for monitoring
|
||||
let opts = Options::input([gpio_pin])
|
||||
.bias(bias)
|
||||
.edge(EdgeDetect::Both)
|
||||
.consumer("noisebell");
|
||||
let mut inputs = chip
|
||||
.request_lines(opts)
|
||||
.await
|
||||
.context(format!("failed to request GPIO line {gpio_pin}"))?;
|
||||
|
||||
// Read initial value
|
||||
let initial_values = inputs
|
||||
.get_values([false])
|
||||
.await
|
||||
.context("failed to read initial GPIO value")?;
|
||||
// Value is true when line is active. With Active::High (default),
|
||||
// true means the physical level is high.
|
||||
let initial_high = initial_values[0];
|
||||
let initial_open = if active_low { !initial_high } else { initial_high };
|
||||
|
||||
let open_level = if active_low { Level::Low } else { Level::High };
|
||||
let initial_open = pin.read() == open_level;
|
||||
let now = unix_timestamp();
|
||||
let commit =
|
||||
std::env::var("NOISEBELL_COMMIT").unwrap_or_else(|_| "unknown".into());
|
||||
|
|
@ -247,7 +261,7 @@ async fn main() -> Result<()> {
|
|||
is_open: AtomicBool::new(initial_open),
|
||||
last_changed: AtomicU64::new(now),
|
||||
started_at: now,
|
||||
gpio_pin,
|
||||
gpio_pin: gpio_pin as u8,
|
||||
active_low,
|
||||
commit,
|
||||
inbound_api_key,
|
||||
|
|
@ -260,31 +274,45 @@ async fn main() -> Result<()> {
|
|||
// Sync initial state with the cache on startup
|
||||
let _ = tx.send((initial_open, now));
|
||||
|
||||
let state_for_interrupt = state.clone();
|
||||
// pin must live for the entire program — rppal runs interrupts on a background
|
||||
// thread tied to the InputPin. If pin drops, the interrupt thread is joined and
|
||||
// monitoring stops. We move it into a binding that lives until main() returns.
|
||||
let mut _pin = pin;
|
||||
_pin.set_async_interrupt(
|
||||
Trigger::Both,
|
||||
Some(Duration::from_secs(debounce_secs)),
|
||||
move |event| {
|
||||
let new_open = match event.trigger {
|
||||
Trigger::FallingEdge => active_low,
|
||||
Trigger::RisingEdge => !active_low,
|
||||
_ => return,
|
||||
// Spawn async edge detection task
|
||||
let state_for_edges = state.clone();
|
||||
let edge_tx = tx.clone();
|
||||
let edge_handle = tokio::spawn(async move {
|
||||
let mut last_event_time = std::time::Instant::now();
|
||||
let debounce = Duration::from_secs(debounce_secs);
|
||||
|
||||
loop {
|
||||
let event = match inputs.read_event().await {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
error!(error = %e, "failed to read GPIO event");
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let was_open = state_for_interrupt.is_open.swap(new_open, Ordering::Relaxed);
|
||||
|
||||
// Software debounce
|
||||
if last_event_time.elapsed() < debounce {
|
||||
continue;
|
||||
}
|
||||
last_event_time = std::time::Instant::now();
|
||||
|
||||
let new_open = match event.edge {
|
||||
Edge::Falling => active_low,
|
||||
Edge::Rising => !active_low,
|
||||
};
|
||||
|
||||
let was_open = state_for_edges.is_open.swap(new_open, Ordering::Relaxed);
|
||||
if was_open != new_open {
|
||||
let timestamp = unix_timestamp();
|
||||
state_for_interrupt
|
||||
state_for_edges
|
||||
.last_changed
|
||||
.store(timestamp, Ordering::Relaxed);
|
||||
let _ = tx.send((new_open, timestamp));
|
||||
let _ = edge_tx.send((new_open, timestamp));
|
||||
}
|
||||
},
|
||||
)
|
||||
.context("failed to set GPIO interrupt")?;
|
||||
}
|
||||
});
|
||||
drop(tx); // Drop original sender so rx closes when edge_handle is dropped
|
||||
|
||||
let notify_handle = tokio::spawn(async move {
|
||||
let client = reqwest::Client::builder()
|
||||
|
|
@ -363,9 +391,7 @@ async fn main() -> Result<()> {
|
|||
.context("server error")?;
|
||||
|
||||
info!("shutting down, draining notification queue");
|
||||
// Drop the interrupt to stop producing new messages, then wait
|
||||
// for the notification task to drain remaining messages.
|
||||
drop(_pin);
|
||||
edge_handle.abort();
|
||||
let _ = notify_handle.await;
|
||||
|
||||
info!("shutdown complete");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue