Rust service and deployment workflow for the Raspberry Pi at Noisebridge.
The current recommended setup is:
aarch64 Noisebell binary on your laptop with NixThis avoids the Raspberry Pi Zero 2 W NixOS boot issues while still keeping the application build reproducible.
noisebell binary for aarch64-linux| 1 | curl -L "https://downloads.raspberrypi.org/raspios_lite_arm64_latest" | xz -d -c | sudo dd of=/dev/sdb bs=16M conv=fsync status=progress && sync |
Configure it for:
NoisebridgeThe helper script is:
| 1 | sudo 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.
After boot, verify SSH works:
| 1 | ssh pi@noisebell-pi.local |
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:
| 1 | ssh-keyscan noisebell-pi.local 2>/dev/null | grep ed25519 |
Add that key to secrets/secrets.nix for:
pi-to-cache-key.agecache-to-pi-key.agetailscale-auth-key.ageThen refresh recipients if needed:
| 1 | cd secrets |
| 2 | agenix -r |
| 1 | cd secrets |
| 2 | agenix -e pi-to-cache-key.age |
| 3 | agenix -e cache-to-pi-key.age |
| 4 | agenix -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.
From your laptop:
| 1 | scripts/deploy-pios-pi.sh pi@noisebell-pi.local |
If Home Assistant is on a fixed LAN IP, set that explicitly during deploy:
| 1 | HOME_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:
| 1 | scripts/deploy-pios-pi.sh pi@10.21.x.x |
That script:
.#packages.aarch64-linux.noisebell-static locally.#packages.aarch64-linux.noisebell-relay-static locallyagenix/etc/noisebell/noisebell.env/etc/noisebell/noisebell-relay.envnoisebell.service and noisebell-relay.servicetailscale up with the decrypted auth keynoisebell-tailscale-only-firewall.serviceprometheus-node-exporternoisebell-loki-journal.service to ship Pi logs to Loki on noisebell-doThe 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.confAll secret files are root-only.
Tailscale is kept on Raspberry Pi OS rather than NixOS.
The deploy script:
tailscaledtailscale up --auth-key=... --hostname=noisebell-pi22), 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.
Normal iteration is just rerunning the deploy script:
| 1 | scripts/deploy-pios-pi.sh pi@noisebell-pi.local |
That rebuilds the binary locally, uploads a new release, refreshes secrets, and restarts the service.
The deployed service uses these environment variables:
| Variable | Default | Description |
|---|---|---|
NOISEBELL_GPIO_PIN | 17 | GPIO pin number |
NOISEBELL_DEBOUNCE_MS | 50 | Debounce delay in milliseconds |
NOISEBELL_PORT | 80 | HTTP server port |
NOISEBELL_ENDPOINT_URL | required | Webhook URL to POST state changes to |
NOISEBELL_RETRY_ATTEMPTS | 3 | Webhook retry count |
NOISEBELL_RETRY_BASE_DELAY_SECS | 1 | Exponential backoff base delay |
NOISEBELL_HTTP_TIMEOUT_SECS | 10 | Outbound request timeout |
NOISEBELL_BIND_ADDRESS | 0.0.0.0 | HTTP bind address |
NOISEBELL_ACTIVE_LOW | true | Low GPIO = door open |
The optional relay service accepts authenticated webhooks from cache-service and forwards them to Home Assistant on the local network.
| Variable | Default | Description |
|---|---|---|
NOISEBELL_RELAY_PORT | 8090 | HTTP port for the relay webhook endpoint |
NOISEBELL_RELAY_BIND_ADDRESS | 0.0.0.0 | HTTP bind address |
NOISEBELL_RELAY_TARGET_BASE_URL | http://10.21.0.43:8123 | Base URL for Home Assistant |
NOISEBELL_RELAY_TARGET_WEBHOOK_ID | required | Home Assistant webhook ID |
NOISEBELL_RELAY_INBOUND_API_KEY | required | Bearer token expected from cache-service |
NOISEBELL_RELAY_RETRY_ATTEMPTS | 3 | Forward retry count |
NOISEBELL_RELAY_RETRY_BASE_DELAY_SECS | 1 | Exponential backoff base delay |
NOISEBELL_RELAY_HTTP_TIMEOUT_SECS | 10 | Outbound 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:
| 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 | } |
The working Home Assistant path is:
| 1 | Pi 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:
NOISEBELL_ENDPOINT_URLhttp://noisebell-pi:8090/webhook using relay-webhook-secret.agenoisebell-relay forwards the payload to Home Assistant using homeassistant-webhook-id.agetrigger.json.statusPayload received by Home Assistant:
| 1 | { |
| 2 | "status": "open", |
| 3 | "timestamp": 1774336193 |
| 4 | } |
Example Home Assistant automation:
| 1 | alias: noisebell |
| 2 | description: "" |
| 3 | triggers: |
| 4 | - trigger: webhook |
| 5 | allowed_methods: |
| 6 | - POST |
| 7 | local_only: false |
| 8 | webhook_id: "-roWWM0JVCWSispwyHXlcKtjI" |
| 9 | conditions: [] |
| 10 | actions: |
| 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 |
| 26 | mode: single |
Important: Home Assistant webhook IDs are exact. If the automation shows a leading -, keep that same leading - in homeassistant-webhook-id.age.
GET / requires Authorization: Bearer <token>.
GET /
| 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.