README.md

Pi

Rust service and deployment workflow for the Raspberry Pi at Noisebridge.

The current recommended setup is:

  1. run Raspberry Pi OS Lite on the Pi
  2. keep the Pi itself free of Nix
  3. build a static aarch64 Noisebell binary on your laptop with Nix
  4. copy the binary, secrets, and systemd service to the Pi over SSH

This avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible.

What stays on Raspberry Pi OS

  • bootloader
  • kernel
  • firmware
  • Wi-Fi and local networking
  • SSH base access
  • Tailscale package/runtime
  • Avahi package/runtime

What Nix manages

  • building a static noisebell binary for aarch64-linux
  • the exact app binary you deploy
  • encrypted secrets in the repo
  • repeatable deployment from your laptop

Initial Pi OS setup

1. Flash Raspberry Pi OS Lite

sh
1curl -L "https://downloads.raspberrypi.org/raspios_lite_arm64_latest" | xz -d -c | sudo dd of=/dev/sdb bs=16M conv=fsync status=progress && sync

2. Configure the flashed SD card

Configure it for:

  • Wi-Fi on Noisebridge
  • SSH enabled
  • serial enabled if you want a recovery console

The helper script is:

sh
1sudo scripts/configure-pios-sd.sh /run/media/jet/bootfs /run/media/jet/rootfs

This setup expects SSH key login for user pi; it does not configure a password.

3. Boot the Pi and verify SSH

After boot, verify SSH works:

sh
1ssh pi@noisebell-pi.local

Add the Pi host key to age recipients

The deploy flow decrypts secrets locally on your laptop, but the Pi host key should still be a recipient for the Pi-facing secrets so the repo stays accurate.

Grab the Pi host key:

sh
1ssh-keyscan noisebell-pi.local 2>/dev/null | grep ed25519

Add that key to secrets/secrets.nix for:

  • pi-to-cache-key.age
  • cache-to-pi-key.age
  • tailscale-auth-key.age

Then refresh recipients if needed:

sh
1cd secrets
2agenix -r

Edit secrets

sh
1cd secrets
2agenix -e pi-to-cache-key.age
3agenix -e cache-to-pi-key.age
4agenix -e tailscale-auth-key.age

These stay encrypted in git. The deploy script decrypts them locally on your laptop and copies the plaintext files to the Pi as root-only files.

Deploy to Raspberry Pi OS

From your laptop:

sh
1scripts/deploy-pios-pi.sh pi@noisebell-pi.local

If Home Assistant is on a fixed LAN IP, set that explicitly during deploy:

sh
1HOME_ASSISTANT_BASE_URL=http://10.21.0.43:8123 scripts/deploy-pios-pi.sh pi@100.66.45.36

If you only know the IP:

sh
1scripts/deploy-pios-pi.sh pi@10.21.x.x

That script:

  1. builds .#packages.aarch64-linux.noisebell-static locally
  2. builds .#packages.aarch64-linux.noisebell-relay-static locally
  3. decrypts the Pi-facing secrets locally with agenix
  4. uploads the binaries and secrets to the Pi
  5. installs Tailscale and Avahi if needed
  6. writes /etc/noisebell/noisebell.env
  7. writes /etc/noisebell/noisebell-relay.env
  8. installs noisebell.service and noisebell-relay.service
  9. runs tailscale up with the decrypted auth key
  10. installs noisebell-tailscale-only-firewall.service
  11. enables persistent journald with a 30 day retention target
  12. installs and enables prometheus-node-exporter
  13. installs noisebell-loki-journal.service to ship Pi logs to Loki on noisebell-do
  14. enables and starts the Noisebell services

Files written on the Pi

The deploy script creates:

  • /opt/noisebell/releases/<timestamp>/noisebell
  • /opt/noisebell/releases/<timestamp>/noisebell-relay
  • /opt/noisebell/current -> current release symlink
  • /etc/noisebell/pi-to-cache-key
  • /etc/noisebell/cache-to-pi-key
  • /etc/noisebell/relay-webhook-secret
  • /etc/noisebell/homeassistant-webhook-id
  • /etc/noisebell/tailscale-auth-key
  • /etc/noisebell/noisebell.env
  • /etc/noisebell/noisebell-relay.env
  • /etc/systemd/system/noisebell.service
  • /etc/systemd/system/noisebell-relay.service
  • /etc/systemd/system/noisebell-tailscale-only-firewall.service
  • /etc/systemd/system/noisebell-loki-journal.service
  • /usr/local/sbin/noisebell-tailscale-only-firewall
  • /usr/local/bin/noisebell-loki-journal
  • /etc/systemd/journald.conf.d/noisebell-persistent.conf

All secret files are root-only.

Tailscale

Tailscale is kept on Raspberry Pi OS rather than NixOS.

The deploy script:

  • installs the Tailscale package if missing
  • enables tailscaled
  • runs tailscale up --auth-key=... --hostname=noisebell-pi
  • blocks non-Tailscale TCP access to SSH (22), the Pi app (80), the relay (8090), and node exporter (9100)

So Tailscale stays part of the base OS, while its auth key is still managed as an encrypted age secret in this repo.

After the first bootstrap, deploy over Tailscale with pi@100.66.45.36 or pi@noisebell-pi. Local Wi-Fi SSH is intentionally blocked by the deploy-installed firewall.

Later updates

Normal iteration is just rerunning the deploy script:

sh
1scripts/deploy-pios-pi.sh pi@noisebell-pi.local

That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service.

Service configuration

The deployed service uses these environment variables:

VariableDefaultDescription
NOISEBELL_GPIO_PIN17GPIO pin number
NOISEBELL_DEBOUNCE_MS50Debounce delay in milliseconds
NOISEBELL_PORT80HTTP server port
NOISEBELL_ENDPOINT_URLrequiredWebhook URL to POST state changes to
NOISEBELL_RETRY_ATTEMPTS3Webhook retry count
NOISEBELL_RETRY_BASE_DELAY_SECS1Exponential backoff base delay
NOISEBELL_HTTP_TIMEOUT_SECS10Outbound request timeout
NOISEBELL_BIND_ADDRESS0.0.0.0HTTP bind address
NOISEBELL_ACTIVE_LOWtrueLow GPIO = door open

Relay service configuration

The optional relay service accepts authenticated webhooks from cache-service and forwards them to Home Assistant on the local network.

VariableDefaultDescription
NOISEBELL_RELAY_PORT8090HTTP port for the relay webhook endpoint
NOISEBELL_RELAY_BIND_ADDRESS0.0.0.0HTTP bind address
NOISEBELL_RELAY_TARGET_BASE_URLhttp://10.21.0.43:8123Base URL for Home Assistant
NOISEBELL_RELAY_TARGET_WEBHOOK_IDrequiredHome Assistant webhook ID
NOISEBELL_RELAY_INBOUND_API_KEYrequiredBearer token expected from cache-service
NOISEBELL_RELAY_RETRY_ATTEMPTS3Forward retry count
NOISEBELL_RELAY_RETRY_BASE_DELAY_SECS1Exponential backoff base delay
NOISEBELL_RELAY_HTTP_TIMEOUT_SECS10Outbound request timeout

If .local resolution is reliable on your Pi, you can override the deploy default with HOME_ASSISTANT_BASE_URL=http://homeassistant.local:8123.

The deploy default for NOISEBELL_ENDPOINT_URL is http://noisebell-do:3000/webhook, so Pi state changes go to the cache over Tailscale. Override with NOISEBELL_CACHE_WEBHOOK_URL=... only for testing or recovery.

Example cache target for the relay:

nix
1{
2 services.noisebell-cache.outboundWebhooks = [
3 {
4 url = "http://noisebell-pi.local:8090/webhook";
5 secretFile = /run/agenix/noisebell-relay-webhook-secret;
6 }
7 ];
8}

Home Assistant workflow

The working Home Assistant path is:

text
1Pi door sensor -> cache-service -> Pi relay -> Home Assistant webhook automation

This keeps cache-service as the fanout source while still letting Home Assistant stay LAN-only.

Setup summary:

  1. Pi still posts raw door events to cache via NOISEBELL_ENDPOINT_URL
  2. cache-service fans out to http://noisebell-pi:8090/webhook using relay-webhook-secret.age
  3. noisebell-relay forwards the payload to Home Assistant using homeassistant-webhook-id.age
  4. Home Assistant automation triggers on the webhook and switches devices based on trigger.json.status

Payload received by Home Assistant:

json
1{
2 "status": "open",
3 "timestamp": 1774336193
4}

Example Home Assistant automation:

yaml
1alias: noisebell
2description: ""
3triggers:
4 - trigger: webhook
5 allowed_methods:
6 - POST
7 local_only: false
8 webhook_id: "-roWWM0JVCWSispwyHXlcKtjI"
9conditions: []
10actions:
11 - if:
12 - condition: template
13 value_template: "{{ trigger.json.status == 'open' }}"
14 then:
15 - action: switch.turn_on
16 target:
17 entity_id: switch.mini_smart_plug_socket_1
18 else:
19 - if:
20 - condition: template
21 value_template: "{{ trigger.json.status == 'closed' }}"
22 then:
23 - action: switch.turn_off
24 target:
25 entity_id: switch.mini_smart_plug_socket_1
26mode: single

Important: Home Assistant webhook IDs are exact. If the automation shows a leading -, keep that same leading - in homeassistant-webhook-id.age.

API

GET / requires Authorization: Bearer <token>.

GET /

json
1{"status": "open", "timestamp": 1710000000}

GET /metrics

Prometheus metrics for local door state, raw GPIO level, debounced state-change counters, webhook delivery counters, last webhook result/status/duration, boot identity, uptime, temperature, throttling flags, Wi-Fi signal, and Tailscale state. This endpoint is unauthenticated and intended for Tailscale-only scraping by the DO Prometheus.

noisebell-relay also exposes unauthenticated Prometheus metrics at GET /metrics on port 8090, including inbound webhook count, Home Assistant forwarding counters, and last forward result/status/duration.

Routine sampled values belong in Prometheus, not logs: GPIO level, Wi-Fi signal, temperature, uptime, Tailscale state, scrape health, and webhook counters are graphed from /metrics. Journald/Loki logs are intended to stay event-oriented: startup/shutdown, initial state sync, debounced door state changes, successful state deliveries, delivery retries/failures, unauthorized requests, relay forwards, and GPIO read error/recovery events.