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
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
remote/target
|
remote/target
|
||||||
|
pi/pi-service/target
|
||||||
.direnv
|
.direnv
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,8 @@
|
||||||
# Decrypted at runtime by agenix
|
# Decrypted at runtime by agenix
|
||||||
age.secrets.tailscale-auth-key.file = ./secrets/tailscale-auth-key.age;
|
age.secrets.tailscale-auth-key.file = ./secrets/tailscale-auth-key.age;
|
||||||
|
|
||||||
age.secrets.api-key = {
|
age.secrets.api-key.file = ./secrets/api-key.age;
|
||||||
file = ./secrets/api-key.age;
|
age.secrets.inbound-api-key.file = ./secrets/inbound-api-key.age;
|
||||||
owner = "noisebell";
|
|
||||||
};
|
|
||||||
age.secrets.inbound-api-key = {
|
|
||||||
file = ./secrets/inbound-api-key.age;
|
|
||||||
owner = "noisebell";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.noisebell = {
|
services.noisebell = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
|
||||||
27
pi/flake.nix
27
pi/flake.nix
|
|
@ -97,19 +97,6 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
users.users.noisebell = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "noisebell";
|
|
||||||
extraGroups = [ "gpio" ];
|
|
||||||
};
|
|
||||||
users.groups.noisebell = {};
|
|
||||||
users.groups.gpio = {};
|
|
||||||
|
|
||||||
services.udev.extraRules = ''
|
|
||||||
KERNEL=="gpiomem", GROUP="gpio", MODE="0660"
|
|
||||||
KERNEL=="gpiochip[0-9]*", GROUP="gpio", MODE="0660"
|
|
||||||
'';
|
|
||||||
|
|
||||||
systemd.services.noisebell = let
|
systemd.services.noisebell = let
|
||||||
bin = "${noisebell.packages.aarch64-linux.default}/bin/noisebell";
|
bin = "${noisebell.packages.aarch64-linux.default}/bin/noisebell";
|
||||||
in {
|
in {
|
||||||
|
|
@ -144,20 +131,6 @@
|
||||||
WatchdogSec = cfg.watchdogSecs;
|
WatchdogSec = cfg.watchdogSecs;
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = cfg.restartDelaySecs;
|
RestartSec = cfg.restartDelaySecs;
|
||||||
User = "noisebell";
|
|
||||||
Group = "noisebell";
|
|
||||||
AmbientCapabilities = lib.optionals (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
DevicePolicy = "closed";
|
|
||||||
DeviceAllow = [ "char-gpiomem rw" "char-gpiochip rw" ];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
50
pi/pi-service/Cargo.lock
generated
50
pi/pi-service/Cargo.lock
generated
|
|
@ -81,6 +81,12 @@ version = "0.22.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
|
@ -217,6 +223,15 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpiod-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15a60e3beb5444643d049a3f8769b47ce246ec1f57e6cd1aed1e417d57a47110"
|
||||||
|
dependencies = [
|
||||||
|
"nix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|
@ -526,6 +541,17 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "noisebell"
|
name = "noisebell"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -534,11 +560,11 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"libc",
|
"libc",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rppal",
|
|
||||||
"sd-notify",
|
"sd-notify",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-gpiod",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
@ -771,15 +797,6 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rppal"
|
|
||||||
version = "0.22.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c1ce3b019009cff02cb6b0e96e7cc2e5c5b90187dc1a490f8ef1521d0596b026"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -1068,6 +1085,17 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-gpiod"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce15fa0021a7acacd2be506f72aeb5044a0a8b53d684963f133b37ace5c57f47"
|
||||||
|
dependencies = [
|
||||||
|
"gpiod-core",
|
||||||
|
"libc",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -1111,7 +1139,7 @@ version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.11.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ anyhow = "1.0"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
rppal = "0.22"
|
|
||||||
sd-notify = "0.4"
|
sd-notify = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal", "time"] }
|
||||||
|
tokio-gpiod = "0.3.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ 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 rppal::gpio::{Gpio, Level, Trigger};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use tokio_gpiod::{Bias, Chip, Edge, EdgeDetect, Options};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
|
|
@ -178,10 +178,10 @@ async fn main() -> Result<()> {
|
||||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
.init();
|
.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())
|
.unwrap_or_else(|_| "17".into())
|
||||||
.parse()
|
.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")
|
let debounce_secs: u64 = std::env::var("NOISEBELL_DEBOUNCE_SECS")
|
||||||
.unwrap_or_else(|_| "5".into())
|
.unwrap_or_else(|_| "5".into())
|
||||||
|
|
@ -227,18 +227,32 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
info!(gpio_pin, debounce_secs, port, %endpoint_url, "starting noisebell");
|
info!(gpio_pin, debounce_secs, port, %endpoint_url, "starting noisebell");
|
||||||
|
|
||||||
let gpio = Gpio::new().context("failed to initialize GPIO")?;
|
let chip = Chip::new("gpiochip0")
|
||||||
let pin = gpio
|
.await
|
||||||
.get(gpio_pin)
|
.context("failed to open gpiochip0")?;
|
||||||
.context(format!("failed to get GPIO pin {gpio_pin}"))?;
|
|
||||||
let pin = if active_low {
|
let bias = if active_low { Bias::PullUp } else { Bias::PullDown };
|
||||||
pin.into_input_pullup()
|
|
||||||
} else {
|
// Request the line with edge detection for monitoring
|
||||||
pin.into_input_pulldown()
|
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 now = unix_timestamp();
|
||||||
let commit =
|
let commit =
|
||||||
std::env::var("NOISEBELL_COMMIT").unwrap_or_else(|_| "unknown".into());
|
std::env::var("NOISEBELL_COMMIT").unwrap_or_else(|_| "unknown".into());
|
||||||
|
|
@ -247,7 +261,7 @@ async fn main() -> Result<()> {
|
||||||
is_open: AtomicBool::new(initial_open),
|
is_open: AtomicBool::new(initial_open),
|
||||||
last_changed: AtomicU64::new(now),
|
last_changed: AtomicU64::new(now),
|
||||||
started_at: now,
|
started_at: now,
|
||||||
gpio_pin,
|
gpio_pin: gpio_pin as u8,
|
||||||
active_low,
|
active_low,
|
||||||
commit,
|
commit,
|
||||||
inbound_api_key,
|
inbound_api_key,
|
||||||
|
|
@ -260,31 +274,45 @@ async fn main() -> Result<()> {
|
||||||
// Sync initial state with the cache on startup
|
// Sync initial state with the cache on startup
|
||||||
let _ = tx.send((initial_open, now));
|
let _ = tx.send((initial_open, now));
|
||||||
|
|
||||||
let state_for_interrupt = state.clone();
|
// Spawn async edge detection task
|
||||||
// pin must live for the entire program — rppal runs interrupts on a background
|
let state_for_edges = state.clone();
|
||||||
// thread tied to the InputPin. If pin drops, the interrupt thread is joined and
|
let edge_tx = tx.clone();
|
||||||
// monitoring stops. We move it into a binding that lives until main() returns.
|
let edge_handle = tokio::spawn(async move {
|
||||||
let mut _pin = pin;
|
let mut last_event_time = std::time::Instant::now();
|
||||||
_pin.set_async_interrupt(
|
let debounce = Duration::from_secs(debounce_secs);
|
||||||
Trigger::Both,
|
|
||||||
Some(Duration::from_secs(debounce_secs)),
|
loop {
|
||||||
move |event| {
|
let event = match inputs.read_event().await {
|
||||||
let new_open = match event.trigger {
|
Ok(event) => event,
|
||||||
Trigger::FallingEdge => active_low,
|
Err(e) => {
|
||||||
Trigger::RisingEdge => !active_low,
|
error!(error = %e, "failed to read GPIO event");
|
||||||
_ => return,
|
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 {
|
if was_open != new_open {
|
||||||
let timestamp = unix_timestamp();
|
let timestamp = unix_timestamp();
|
||||||
state_for_interrupt
|
state_for_edges
|
||||||
.last_changed
|
.last_changed
|
||||||
.store(timestamp, Ordering::Relaxed);
|
.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 notify_handle = tokio::spawn(async move {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
|
|
@ -363,9 +391,7 @@ async fn main() -> Result<()> {
|
||||||
.context("server error")?;
|
.context("server error")?;
|
||||||
|
|
||||||
info!("shutting down, draining notification queue");
|
info!("shutting down, draining notification queue");
|
||||||
// Drop the interrupt to stop producing new messages, then wait
|
edge_handle.abort();
|
||||||
// for the notification task to drain remaining messages.
|
|
||||||
drop(_pin);
|
|
||||||
let _ = notify_handle.await;
|
let _ = notify_handle.await;
|
||||||
|
|
||||||
info!("shutdown complete");
|
info!("shutdown complete");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue