feat: rewrite pi to be simple and nix based
This commit is contained in:
parent
b2c8d08bdc
commit
c6e726c430
28 changed files with 880 additions and 2458 deletions
2
pi/pi-service/.cargo/config.toml
Normal file
2
pi/pi-service/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
1964
pi/pi-service/Cargo.lock
generated
Normal file
1964
pi/pi-service/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
pi/pi-service/Cargo.toml
Normal file
15
pi/pi-service/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "noisebell"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
axum = "0.8"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
rppal = "0.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
62
pi/pi-service/flake.nix
Normal file
62
pi/pi-service/flake.nix
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
description = "Noisebell - GPIO door monitor service";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, crane, rust-overlay }:
|
||||
let
|
||||
forSystem = system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
crossPkgs = import nixpkgs {
|
||||
inherit system;
|
||||
crossSystem.config = "aarch64-unknown-linux-gnu";
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
targets = [ "aarch64-unknown-linux-gnu" ];
|
||||
};
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
|
||||
src = craneLib.cleanCargoSource ./.;
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
doCheck = false;
|
||||
|
||||
CARGO_BUILD_TARGET = "aarch64-unknown-linux-gnu";
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER =
|
||||
"${crossPkgs.stdenv.cc.targetPrefix}cc";
|
||||
|
||||
HOST_CC = "${pkgs.stdenv.cc.nativePrefix}cc";
|
||||
|
||||
depsBuildBuild = [ crossPkgs.stdenv.cc ];
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
noisebell = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
in
|
||||
{
|
||||
packages.aarch64-linux.default = noisebell;
|
||||
packages.aarch64-linux.noisebell = noisebell;
|
||||
};
|
||||
in
|
||||
forSystem "x86_64-linux";
|
||||
}
|
||||
116
pi/pi-service/src/main.rs
Normal file
116
pi/pi-service/src/main.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use rppal::gpio::{Gpio, Level, Trigger};
|
||||
use serde::Serialize;
|
||||
use tracing::{error, info};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StatusResponse {
|
||||
status: &'static str,
|
||||
}
|
||||
|
||||
fn status_str(is_open: bool) -> &'static str {
|
||||
if is_open {
|
||||
"open"
|
||||
} else {
|
||||
"closed"
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_status(State(is_open): State<Arc<AtomicBool>>) -> Json<StatusResponse> {
|
||||
Json(StatusResponse {
|
||||
status: status_str(is_open.load(Ordering::Relaxed)),
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let gpio_pin: u8 = std::env::var("NOISEBELL_GPIO_PIN")
|
||||
.unwrap_or_else(|_| "17".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_GPIO_PIN must be a valid u8")?;
|
||||
|
||||
let debounce_secs: u64 = std::env::var("NOISEBELL_DEBOUNCE_SECS")
|
||||
.unwrap_or_else(|_| "5".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_DEBOUNCE_SECS must be a valid u64")?;
|
||||
|
||||
let port: u16 = std::env::var("NOISEBELL_PORT")
|
||||
.unwrap_or_else(|_| "8080".into())
|
||||
.parse()
|
||||
.context("NOISEBELL_PORT must be a valid u16")?;
|
||||
|
||||
let endpoint_url =
|
||||
std::env::var("NOISEBELL_ENDPOINT_URL").context("NOISEBELL_ENDPOINT_URL is required")?;
|
||||
|
||||
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}"))?
|
||||
.into_input_pullup();
|
||||
|
||||
let is_open = Arc::new(AtomicBool::new(pin.read() == Level::Low));
|
||||
|
||||
info!(initial_status = status_str(is_open.load(Ordering::Relaxed)), "GPIO initialized");
|
||||
|
||||
// Channel to bridge sync GPIO callback -> async notification task
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<bool>();
|
||||
|
||||
// Set up async interrupt for state changes
|
||||
let state_for_interrupt = is_open.clone();
|
||||
pin.set_async_interrupt(
|
||||
Trigger::Both,
|
||||
Some(Duration::from_secs(debounce_secs)),
|
||||
move |event| {
|
||||
let new_open = match event.trigger {
|
||||
Trigger::FallingEdge => true,
|
||||
Trigger::RisingEdge => false,
|
||||
_ => return,
|
||||
};
|
||||
let was_open = state_for_interrupt.swap(new_open, Ordering::Relaxed);
|
||||
if was_open != new_open {
|
||||
let _ = tx.send(new_open);
|
||||
}
|
||||
},
|
||||
)
|
||||
.context("failed to set GPIO interrupt")?;
|
||||
|
||||
// Task that POSTs state changes to the endpoint
|
||||
tokio::spawn(async move {
|
||||
let client = reqwest::Client::new();
|
||||
while let Some(new_open) = rx.recv().await {
|
||||
let status = status_str(new_open);
|
||||
info!(status, "state changed");
|
||||
|
||||
if let Err(e) = client
|
||||
.post(&endpoint_url)
|
||||
.json(&serde_json::json!({ "status": status }))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
error!(%e, "failed to notify endpoint");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/status", get(get_status))
|
||||
.with_state(is_open);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
|
||||
.await
|
||||
.context(format!("failed to bind to port {port}"))?;
|
||||
|
||||
info!(port, "listening");
|
||||
axum::serve(listener, app).await.context("server error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue