From 7d538f1942d9115567668ee126f222fa283249fe Mon Sep 17 00:00:00 2001 From: Jet Date: Mon, 23 Mar 2026 01:48:49 -0700 Subject: [PATCH] feat: update ot synchronous gpio and rotate keys --- Cargo.lock | 22 ++++++++++------------ pi/README.md | 10 +++++----- pi/configuration.nix | 2 +- pi/pi-service/Cargo.toml | 2 +- pi/pi-service/src/main.rs | 22 ++++++++++------------ remote/cache-service/module.nix | 4 +++- scripts/configure-pios-sd.sh | 2 +- scripts/deploy-full-pi.sh | 2 +- scripts/deploy-pios-pi.sh | 26 ++++++++++++++++++++------ secrets/tailscale-auth-key.age | 17 +++++++++-------- 10 files changed, 61 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 001b2fb..0a20e9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,6 +399,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gpiod" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f851b1c607b36b4a493448ef80d8693bf74145712074c667a008a58264f8da49" +dependencies = [ + "gpiod-core", +] + [[package]] name = "gpiod-core" version = "0.3.0" @@ -783,6 +792,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "gpiod", "libc", "noisebell-common", "reqwest", @@ -790,7 +800,6 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-gpiod", "tracing", "tracing-subscriber", ] @@ -1560,17 +1569,6 @@ dependencies = [ "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]] name = "tokio-macros" version = "2.6.1" diff --git a/pi/README.md b/pi/README.md index edf2640..16ca81a 100644 --- a/pi/README.md +++ b/pi/README.md @@ -57,7 +57,7 @@ This setup expects SSH key login for user `pi`; it does not configure a password After boot, verify SSH works: ```sh -ssh pi@noisebridge-pi.local +ssh pi@noisebell-pi.local ``` ## Add the Pi host key to age recipients @@ -67,7 +67,7 @@ The deploy flow decrypts secrets locally on your laptop, but the Pi host key sho Grab the Pi host key: ```sh -ssh-keyscan noisebridge-pi.local 2>/dev/null | grep ed25519 +ssh-keyscan noisebell-pi.local 2>/dev/null | grep ed25519 ``` Add that key to `secrets/secrets.nix` for: @@ -99,7 +99,7 @@ These stay encrypted in git. The deploy script decrypts them locally on your lap From your laptop: ```sh -scripts/deploy-pios-pi.sh pi@noisebridge-pi.local +scripts/deploy-pios-pi.sh pi@noisebell-pi.local ``` If you only know the IP: @@ -141,7 +141,7 @@ The deploy script: - installs the Tailscale package if missing - enables `tailscaled` -- runs `tailscale up --auth-key=... --hostname=noisebridge-pi` +- runs `tailscale up --auth-key=... --hostname=noisebell-pi` So Tailscale stays part of the base OS, while its auth key is still managed as an encrypted `age` secret in this repo. @@ -150,7 +150,7 @@ So Tailscale stays part of the base OS, while its auth key is still managed as a Normal iteration is just rerunning the deploy script: ```sh -scripts/deploy-pios-pi.sh pi@noisebridge-pi.local +scripts/deploy-pios-pi.sh pi@noisebell-pi.local ``` That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service. diff --git a/pi/configuration.nix b/pi/configuration.nix index cc61965..92799bf 100644 --- a/pi/configuration.nix +++ b/pi/configuration.nix @@ -3,7 +3,7 @@ { system.stateVersion = "24.11"; - networking.hostName = "noisebridge-pi"; + networking.hostName = "noisebell-pi"; networking.wireless = { enable = true; diff --git a/pi/pi-service/Cargo.toml b/pi/pi-service/Cargo.toml index 47cd18a..10f8bf7 100644 --- a/pi/pi-service/Cargo.toml +++ b/pi/pi-service/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0" axum = "0.8" +gpiod = "0.3.0" libc = "0.2" noisebell-common = { path = "../../remote/noisebell-common" } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } @@ -13,6 +14,5 @@ sd-notify = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "signal", "time"] } -tokio-gpiod = "0.3.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/pi/pi-service/src/main.rs b/pi/pi-service/src/main.rs index b26dfda..780d6b7 100644 --- a/pi/pi-service/src/main.rs +++ b/pi/pi-service/src/main.rs @@ -7,10 +7,10 @@ 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 noisebell_common::{ validate_bearer, DoorStatus, PiStatusResponse, SignalLevel, WebhookPayload, }; -use tokio_gpiod::{Bias, Chip, Edge, EdgeDetect, Options}; use tracing::{error, info, warn}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -135,9 +135,7 @@ async fn main() -> Result<()> { info!(gpio_pin, debounce_secs, port, %endpoint_url, "starting noisebell"); - let chip = Chip::new("gpiochip0") - .await - .context("failed to open gpiochip0")?; + let chip = Chip::new("gpiochip0").context("failed to open gpiochip0")?; let bias = if active_level == SignalLevel::Low { Bias::PullUp @@ -152,13 +150,11 @@ async fn main() -> Result<()> { .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. @@ -187,19 +183,22 @@ async fn main() -> Result<()> { // Sync initial state with the cache on startup let _ = tx.send((initial_state.as_door_status(), now)); - // Spawn async edge detection task + // 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. 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 _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); loop { - let event = match inputs.read_event().await { + let event = match inputs.read_event() { Ok(event) => event, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, Err(e) => { error!(error = %e, "failed to read GPIO event"); - tokio::time::sleep(Duration::from_secs(1)).await; + std::thread::sleep(Duration::from_secs(1)); continue; } }; @@ -303,7 +302,6 @@ async fn main() -> Result<()> { .context("server error")?; info!("shutting down, draining notification queue"); - edge_handle.abort(); let _ = notify_handle.await; info!("shutdown complete"); diff --git a/remote/cache-service/module.nix b/remote/cache-service/module.nix index 02f753e..12bcae6 100644 --- a/remote/cache-service/module.nix +++ b/remote/cache-service/module.nix @@ -99,7 +99,9 @@ in let idx = toString (i - 1); in - ''export NOISEBELL_CACHE_WEBHOOK_${idx}_URL="${wh.url}"'' + '' + export NOISEBELL_CACHE_WEBHOOK_${idx}_URL="${wh.url}" + '' + lib.optionalString (wh.secretFile != null) '' export NOISEBELL_CACHE_WEBHOOK_${idx}_SECRET="$(cat ${wh.secretFile})" '' diff --git a/scripts/configure-pios-sd.sh b/scripts/configure-pios-sd.sh index 43aca2c..e8fc274 100755 --- a/scripts/configure-pios-sd.sh +++ b/scripts/configure-pios-sd.sh @@ -3,7 +3,7 @@ set -euo pipefail BOOTFS=${1:-/run/media/jet/bootfs} ROOTFS=${2:-/run/media/jet/rootfs} -HOSTNAME=noisebridge-pi +HOSTNAME=noisebell-pi WIFI_SSID=Noisebridge WIFI_PASSWORD=noisebridge PI_USERNAME=pi diff --git a/scripts/deploy-full-pi.sh b/scripts/deploy-full-pi.sh index 84b70f2..9b8261b 100755 --- a/scripts/deploy-full-pi.sh +++ b/scripts/deploy-full-pi.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -TARGET_HOST=${1:-root@noisebridge-pi.local} +TARGET_HOST=${1:-root@noisebell-pi.local} exec nixos-rebuild switch --flake ".#pi" --target-host "$TARGET_HOST" diff --git a/scripts/deploy-pios-pi.sh b/scripts/deploy-pios-pi.sh index 3a2a1b4..d1edbaa 100755 --- a/scripts/deploy-pios-pi.sh +++ b/scripts/deploy-pios-pi.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -TARGET_HOST=${1:-pi@noisebridge-pi.local} +TARGET_HOST=${1:-pi@noisebell-pi.local} +DEPLOY_HOSTNAME=${DEPLOY_HOSTNAME:-noisebell-pi} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd) RELEASE_ID=${RELEASE_ID:-$(date +%Y%m%d-%H%M%S)} @@ -52,12 +53,23 @@ scp "${SSH_OPTS[@]}" "$TMP_DIR/cache-to-pi-key" "$TARGET_HOST:$REMOTE_TMP_DIR/ca scp "${SSH_OPTS[@]}" "$TMP_DIR/tailscale-auth-key" "$TARGET_HOST:$REMOTE_TMP_DIR/tailscale-auth-key" echo "Installing service and Tailscale on $TARGET_HOST..." -ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "REMOTE_RELEASE_DIR='$REMOTE_RELEASE_DIR' REMOTE_CURRENT_LINK='$REMOTE_CURRENT_LINK' REMOTE_TMP_DIR='$REMOTE_TMP_DIR' bash -s" <<'EOF' +ssh "${SSH_OPTS[@]}" "$TARGET_HOST" "DEPLOY_HOSTNAME='$DEPLOY_HOSTNAME' REMOTE_RELEASE_DIR='$REMOTE_RELEASE_DIR' REMOTE_CURRENT_LINK='$REMOTE_CURRENT_LINK' REMOTE_TMP_DIR='$REMOTE_TMP_DIR' bash -s" <<'EOF' set -euo pipefail sudo apt-get update sudo apt-get install -y curl rsync avahi-daemon +sudo hostnamectl set-hostname "$DEPLOY_HOSTNAME" +sudo tee /etc/hostname >/dev/null <<<"$DEPLOY_HOSTNAME" +sudo tee /etc/hosts >/dev/null </dev/null 2>&1; then curl -fsSL https://tailscale.com/install.sh | sh fi @@ -84,7 +96,7 @@ RUST_LOG=info ENVEOF sudo chmod 600 /etc/noisebell/noisebell.env -sudo tee /etc/systemd/system/noisebell.service >/dev/null </dev/null <<'UNITEOF' [Unit] Description=Noisebell GPIO door monitor After=network-online.target tailscaled.service @@ -94,7 +106,7 @@ Wants=network-online.target Type=notify NotifyAccess=all EnvironmentFile=/etc/noisebell/noisebell.env -ExecStart=/bin/bash -lc 'export NOISEBELL_API_KEY="$$(cat /etc/noisebell/pi-to-cache-key)"; export NOISEBELL_INBOUND_API_KEY="$$(cat /etc/noisebell/cache-to-pi-key)"; exec ${REMOTE_CURRENT_LINK}/noisebell' +ExecStart=/bin/bash -lc 'export NOISEBELL_API_KEY="$$(cat /etc/noisebell/pi-to-cache-key)"; export NOISEBELL_INBOUND_API_KEY="$$(cat /etc/noisebell/cache-to-pi-key)"; exec /opt/noisebell/current/noisebell' Restart=on-failure RestartSec=5 WatchdogSec=30 @@ -105,9 +117,11 @@ UNITEOF sudo ln -sfn "$REMOTE_RELEASE_DIR" "$REMOTE_CURRENT_LINK" sudo systemctl daemon-reload -sudo systemctl enable --now noisebell.service +sudo systemctl enable noisebell.service +sudo systemctl restart noisebell.service +sudo systemctl restart avahi-daemon -sudo tailscale up --auth-key="$(sudo cat /etc/noisebell/tailscale-auth-key)" --hostname=noisebridge-pi || true +sudo tailscale up --auth-key="$(sudo cat /etc/noisebell/tailscale-auth-key)" --hostname=noisebell-pi || true rmdir "$REMOTE_TMP_DIR" 2>/dev/null || true diff --git a/secrets/tailscale-auth-key.age b/secrets/tailscale-auth-key.age index 355816b..cb0917b 100644 --- a/secrets/tailscale-auth-key.age +++ b/secrets/tailscale-auth-key.age @@ -1,9 +1,10 @@ age-encryption.org/v1 --> ssh-ed25519 Ziw7aw uacUhdU1sHIkFBWH8Fs05kA7Jq8UDuaGajVcnl6x8Xw -ypVbx8PvWHrzgt0kfsOiTFxf/QBQS75SgFusWFdbNQw --> ssh-ed25519 NFB4qA Eux3ByEKoh7oDDob17R6q+nBevoOVt+Rll24+3O9l2Y -hgRHNLvRSLzHHtnGIdLESQNYgJqhjk1nDVp2rxlCu+A --> X25519 gTGkOm0qOJzOcPSsXDh4x7mYVAwB8ImhZNwJuNisCQg -TmHdBGkyFtAK+SEWYU97GKK75LnobLOwIt6r15NQB2o ---- k+U0bzyk8gTo0tcBgrrSRpPqM6OdvrDmDV4AeAuzQl0 -'5Y/:$tIsLE4tDEaDٹ5Q&kz{DMAf|o##Mt5(` \ No newline at end of file +-> ssh-ed25519 Ziw7aw Wx6m6fWrZstI1M3mFySXEtCEeiYOK3EB8xUVLKe8my4 +T0Evdcs7+hsWYU0M2AEWbGCtdOwHNHgk/bBXZ0jpPg4 +-> ssh-ed25519 NFB4qA KlrsRc4Us/7WCoCk3hYNVvmeNYvfMH4hOuXAkLFipkw +y/rCNHka6HDr5HdfMazlqaebcBO0K50rzcb3igcMxpw +-> X25519 XTXs2qhJK1noZZtCHCol6IlN48s3nDOqIHX86PmQo2o +eHxpTg3QsTd3EzLUQAecNtGI7+NvP3zxFhUd8zHTuvQ +--- mFSpkYW6U5vQaH+a3fqVW5/ODOZwounsybqkLQoLqY0 +yqٶG +ƵMV=XIc|,QQ|ɁĎy=  +.xRl4Y_N &0TV,X@47 \ No newline at end of file