fix: update the readmes to be more clear
This commit is contained in:
parent
dc7b8cbadd
commit
574a3a103e
6 changed files with 216 additions and 147 deletions
116
README.md
116
README.md
|
|
@ -1,114 +1,20 @@
|
||||||
# Noisebell
|
# Noisebell
|
||||||
|
|
||||||
Door status monitor for [Noisebridge](https://www.noisebridge.net). A Raspberry Pi watches a door sensor and reports open/closed state; remote services cache the data and fan it out to Discord and an Atom feed.
|
Monitors the door at [Noisebridge](https://www.noisebridge.net) and tells you whether it's open or closed.
|
||||||
|
|
||||||
## Architecture
|
A Raspberry Pi reads a magnetic sensor on the door and pushes state changes to a cache server. The cache fans updates out to Discord and an Atom feed.
|
||||||
|
|
||||||
```
|
```
|
||||||
Pi (door sensor) ──webhook──▸ Cache Service ──webhook──▸ Discord Bot
|
Pi (door sensor) ──webhook──> Cache ──webhook──> Discord
|
||||||
│ │
|
|
|
||||||
polls Pi ◂──┘ Atom feed ◂──┘
|
polls Pi <-+ RSS reads from Cache
|
||||||
RSS Service
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The **Pi** runs a small HTTP service that reads GPIO and exposes `GET /` (status) and `GET /info`. It also pushes state changes to the cache service via webhook.
|
## Layout
|
||||||
|
|
||||||
The **Cache Service** is the central hub. It polls the Pi for status and info, stores everything in SQLite, and forwards state changes to downstream webhooks (Discord, etc.). It exposes a read API for consumers.
|
| Directory | What it is |
|
||||||
|
|-----------|------------|
|
||||||
|
| [`pi/`](pi/) | NixOS config + Rust service for the Pi |
|
||||||
|
| [`remote/`](remote/) | Server-side services (cache, discord bot, rss feed) |
|
||||||
|
|
||||||
The **Discord Bot** receives webhooks from the cache service and posts embeds to a Discord channel.
|
Each directory has its own README with setup and configuration details.
|
||||||
|
|
||||||
The **RSS Service** fetches history from the cache service and serves an Atom feed of door events from the last 7 days.
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
| Service | Crate | Default Port | Description |
|
|
||||||
|---------|-------|-------------|-------------|
|
|
||||||
| Cache | `noisebell-cache` | 3000 | Polls Pi, caches state in SQLite, forwards webhooks |
|
|
||||||
| Discord | `noisebell-discord` | 3001 | Posts door status embeds to Discord |
|
|
||||||
| RSS | `noisebell-rss` | 3002 | Serves Atom feed of recent door events |
|
|
||||||
|
|
||||||
## Cache API
|
|
||||||
|
|
||||||
| Method | Path | Auth | Description |
|
|
||||||
|--------|------|------|-------------|
|
|
||||||
| `GET` | `/status` | None | Current door status (`status`, `timestamp`, `last_seen`) |
|
|
||||||
| `GET` | `/info` | None | Pi system info (JSON blob) |
|
|
||||||
| `GET` | `/history` | None | Last 100 state changes |
|
|
||||||
| `POST` | `/webhook` | Bearer token | Inbound webhook from Pi |
|
|
||||||
| `GET` | `/health` | None | Returns `200 OK` |
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
All remote services live in a Cargo workspace under `remote/`.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd remote
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
Or with Nix:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd remote
|
|
||||||
nix build .#noisebell-cache
|
|
||||||
nix build .#noisebell-discord
|
|
||||||
nix build .#noisebell-rss
|
|
||||||
```
|
|
||||||
|
|
||||||
## NixOS Configuration
|
|
||||||
|
|
||||||
The flake at `remote/flake.nix` exports NixOS modules. Example configuration:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, noisebell, ... }: {
|
|
||||||
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
noisebell.nixosModules.default
|
|
||||||
({ ... }: {
|
|
||||||
services.noisebell-cache = {
|
|
||||||
enable = true;
|
|
||||||
domain = "cache.noisebell.example.com";
|
|
||||||
piAddress = "http://noisebell-pi:80";
|
|
||||||
piApiKeyFile = "/run/secrets/noisebell-pi-api-key";
|
|
||||||
inboundApiKeyFile = "/run/secrets/noisebell-inbound-api-key";
|
|
||||||
outboundWebhooks = [
|
|
||||||
{
|
|
||||||
url = "http://localhost:3001/webhook";
|
|
||||||
secretFile = "/run/secrets/noisebell-discord-webhook-secret";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.noisebell-discord = {
|
|
||||||
enable = true;
|
|
||||||
domain = "discord.noisebell.example.com";
|
|
||||||
discordTokenFile = "/run/secrets/noisebell-discord-token";
|
|
||||||
channelId = "123456789012345678";
|
|
||||||
webhookSecretFile = "/run/secrets/noisebell-discord-webhook-secret";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.noisebell-rss = {
|
|
||||||
enable = true;
|
|
||||||
domain = "rss.noisebell.example.com";
|
|
||||||
cacheUrl = "http://localhost:3000";
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Secrets
|
|
||||||
|
|
||||||
| Secret | Used by | Description |
|
|
||||||
|--------|---------|-------------|
|
|
||||||
| `piApiKeyFile` | Cache | Bearer token for authenticating to Pi GET endpoints |
|
|
||||||
| `inboundApiKeyFile` | Cache | Bearer token the Pi uses when POSTing to `/webhook` |
|
|
||||||
| `outboundWebhooks[].secretFile` | Cache | Bearer token sent with outbound webhook POSTs |
|
|
||||||
| `discordTokenFile` | Discord | Discord bot token |
|
|
||||||
| `webhookSecretFile` | Discord | Must match the cache's outbound webhook secret |
|
|
||||||
|
|
|
||||||
84
pi/README.md
84
pi/README.md
|
|
@ -1,39 +1,41 @@
|
||||||
# noisebell
|
# Pi
|
||||||
|
|
||||||
Monitors a GPIO pin on a Raspberry Pi to detect door open/close events. State changes get POSTed to a webhook endpoint. Current state is available over HTTP.
|
Rust service and NixOS config for the Raspberry Pi at Noisebridge. Reads a magnetic door sensor via GPIO, serves the current state over HTTP, and pushes changes to the cache service.
|
||||||
|
|
||||||
Runs on NixOS with Tailscale for networking and agenix for secrets.
|
Runs NixOS with Tailscale for remote access and agenix for secrets.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The service watches a GPIO pin for rising/falling edges with a configurable debounce. When the door state changes, it:
|
||||||
|
|
||||||
|
1. Updates in-memory state (atomics)
|
||||||
|
2. POSTs `{"status": "open", "timestamp": ...}` to the cache service with a Bearer token
|
||||||
|
3. Retries with exponential backoff on failure
|
||||||
|
|
||||||
|
On startup it also syncs the initial state.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### 1. Bootstrap
|
### 1. Flash the SD card
|
||||||
|
|
||||||
Build the SD image, flash it, and boot the Pi:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage
|
nix build .#nixosConfigurations.bootstrap.config.system.build.sdImage
|
||||||
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress
|
dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress
|
||||||
```
|
```
|
||||||
|
|
||||||
Insert the SD card into the Pi and power it on. It will connect to the Noisebridge WiFi network automatically.
|
Boot the Pi. It connects to the Noisebridge WiFi automatically.
|
||||||
|
|
||||||
### 2. Find the Pi
|
### 2. Find the Pi
|
||||||
|
|
||||||
Once booted, find the Pi on the network:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Scan the local subnet
|
|
||||||
nmap -sn 192.168.1.0/24
|
nmap -sn 192.168.1.0/24
|
||||||
|
# or
|
||||||
# Or check ARP table
|
|
||||||
arp -a
|
arp -a
|
||||||
|
|
||||||
# Or check your router's DHCP leases
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Get SSH host key
|
### 3. SSH host key
|
||||||
|
|
||||||
Grab the Pi's ed25519 host key and put it in `secrets/secrets.nix`:
|
Grab the key and add it to `secrets/secrets.nix`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ssh-keyscan <pi-ip> | grep ed25519
|
ssh-keyscan <pi-ip> | grep ed25519
|
||||||
|
|
@ -42,7 +44,7 @@ ssh-keyscan <pi-ip> | grep ed25519
|
||||||
```nix
|
```nix
|
||||||
# secrets/secrets.nix
|
# secrets/secrets.nix
|
||||||
let
|
let
|
||||||
pi = "ssh-ed25519 AAAA..."; # paste the key here
|
pi = "ssh-ed25519 AAAA...";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
"api-key.age".publicKeys = [ pi ];
|
"api-key.age".publicKeys = [ pi ];
|
||||||
|
|
@ -51,20 +53,18 @@ in
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Secrets
|
### 4. Create secrets
|
||||||
|
|
||||||
Create the encrypted secret files:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd secrets
|
cd secrets
|
||||||
agenix -e api-key.age # paste API key for the cache endpoint
|
agenix -e api-key.age # key for POSTing to the cache
|
||||||
agenix -e inbound-api-key.age # paste API key that the cache uses to poll the Pi
|
agenix -e inbound-api-key.age # key the cache uses to poll us
|
||||||
agenix -e tailscale-auth-key.age # paste Tailscale auth key
|
agenix -e tailscale-auth-key.age # tailscale auth key
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Add SSH key
|
### 5. SSH access
|
||||||
|
|
||||||
Add your SSH public key to `configuration.nix`:
|
Add your public key to `configuration.nix`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
users.users.root.openssh.authorizedKeys.keys = [
|
users.users.root.openssh.authorizedKeys.keys = [
|
||||||
|
|
@ -84,29 +84,31 @@ Options under `services.noisebell` in `flake.nix`:
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `endpointUrl` | — | Webhook endpoint URL to POST state changes to |
|
| `endpointUrl` | required | Webhook URL to POST state changes to |
|
||||||
| `apiKeyFile` | — | Path to file containing outbound API key (agenix secret) |
|
| `apiKeyFile` | required | Outbound API key file (agenix secret) |
|
||||||
| `inboundApiKeyFile` | — | Path to file containing inbound API key for GET endpoint auth (agenix secret) |
|
| `inboundApiKeyFile` | required | Inbound API key file for GET auth |
|
||||||
| `gpioPin` | 17 | GPIO pin to monitor |
|
| `gpioPin` | `17` | GPIO pin number |
|
||||||
| `debounceSecs` | 5 | Debounce delay |
|
| `debounceSecs` | `5` | Debounce delay in seconds |
|
||||||
| `port` | 8080 | HTTP status port |
|
| `port` | `8080` | HTTP server port |
|
||||||
| `retryAttempts` | 3 | Webhook retry count |
|
| `retryAttempts` | `3` | Webhook retry count |
|
||||||
| `retryBaseDelaySecs` | 1 | Base delay for exponential backoff |
|
| `retryBaseDelaySecs` | `1` | Exponential backoff base delay |
|
||||||
| `httpTimeoutSecs` | 10 | Timeout for outbound webhook requests |
|
| `httpTimeoutSecs` | `10` | Outbound request timeout |
|
||||||
| `bindAddress` | `0.0.0.0` | Address to bind the HTTP server to |
|
| `bindAddress` | `0.0.0.0` | HTTP bind address |
|
||||||
| `activeLow` | `true` | Whether low GPIO level means open (depends on wiring) |
|
| `activeLow` | `true` | Low GPIO = door open (depends on wiring) |
|
||||||
| `restartDelaySecs` | 5 | Seconds before systemd restarts on failure |
|
| `restartDelaySecs` | `5` | systemd restart delay on failure |
|
||||||
| `watchdogSecs` | 30 | Watchdog timeout — service is restarted if unresponsive |
|
| `watchdogSecs` | `30` | systemd watchdog timeout |
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
`GET /` — current door state:
|
All endpoints require `Authorization: Bearer <token>`.
|
||||||
|
|
||||||
|
**`GET /`** — door state
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"status": "open", "timestamp": 1710000000}
|
{"status": "open", "timestamp": 1710000000}
|
||||||
```
|
```
|
||||||
|
|
||||||
`GET /info` — system health and GPIO config:
|
**`GET /info`** — system health + GPIO config
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -129,5 +131,3 @@ Options under `services.noisebell` in `flake.nix`:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
State changes (and initial state on startup) are POSTed to the configured endpoint in the same format as `GET /`, with an `Authorization: Bearer <api-key>` header.
|
|
||||||
|
|
|
||||||
70
remote/README.md
Normal file
70
remote/README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Remote Services
|
||||||
|
|
||||||
|
Cargo workspace with the server-side pieces of Noisebell. Runs on any Linux box.
|
||||||
|
|
||||||
|
| Service | Port | What it does |
|
||||||
|
|---------|------|--------------|
|
||||||
|
| [`cache-service/`](cache-service/) | 3000 | Polls the Pi, stores history in SQLite, fans out webhooks |
|
||||||
|
| [`discord-bot/`](discord-bot/) | 3001 | Posts door status to a Discord channel |
|
||||||
|
| [`rss-service/`](rss-service/) | 3002 | Serves an Atom feed of door events |
|
||||||
|
| [`noisebell-common/`](noisebell-common/) | — | Shared types and helpers |
|
||||||
|
|
||||||
|
See each service's README for configuration and API docs.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Nix:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix build .#noisebell-cache
|
||||||
|
nix build .#noisebell-discord
|
||||||
|
nix build .#noisebell-rss
|
||||||
|
```
|
||||||
|
|
||||||
|
## NixOS deployment
|
||||||
|
|
||||||
|
The flake exports NixOS modules. Each service runs as a hardened systemd unit behind Caddy.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.noisebell.url = "git+https://git.extremist.software/jet/noisebell?dir=remote";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, noisebell, ... }: {
|
||||||
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
noisebell.nixosModules.default
|
||||||
|
({ ... }: {
|
||||||
|
services.noisebell-cache = {
|
||||||
|
enable = true;
|
||||||
|
domain = "cache.noisebell.example.com";
|
||||||
|
piAddress = "http://noisebell-pi:80";
|
||||||
|
piApiKeyFile = "/run/secrets/noisebell-pi-api-key";
|
||||||
|
inboundApiKeyFile = "/run/secrets/noisebell-inbound-api-key";
|
||||||
|
outboundWebhooks = [{
|
||||||
|
url = "http://localhost:3001/webhook";
|
||||||
|
secretFile = "/run/secrets/noisebell-discord-webhook-secret";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
services.noisebell-discord = {
|
||||||
|
enable = true;
|
||||||
|
domain = "discord.noisebell.example.com";
|
||||||
|
discordTokenFile = "/run/secrets/noisebell-discord-token";
|
||||||
|
channelId = "123456789012345678";
|
||||||
|
webhookSecretFile = "/run/secrets/noisebell-discord-webhook-secret";
|
||||||
|
};
|
||||||
|
services.noisebell-rss = {
|
||||||
|
enable = true;
|
||||||
|
domain = "rss.noisebell.example.com";
|
||||||
|
cacheUrl = "http://localhost:3000";
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
45
remote/cache-service/README.md
Normal file
45
remote/cache-service/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Cache Service
|
||||||
|
|
||||||
|
The central hub. Sits between the Pi and everything else.
|
||||||
|
|
||||||
|
It does two things: polls the Pi on a timer to keep a local copy of the door state, and receives push webhooks from the Pi when the state actually changes. Either way, updates get written to SQLite and forwarded to downstream services (Discord, etc.) via outbound webhooks.
|
||||||
|
|
||||||
|
If the Pi stops responding to polls (configurable threshold, default 3 misses), the cache marks it as offline and notifies downstream.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|--------|------|------|-------------|
|
||||||
|
| `GET` | `/status` | — | Current door status (`status`, `timestamp`, `last_seen`) |
|
||||||
|
| `GET` | `/info` | — | Cached Pi system info |
|
||||||
|
| `GET` | `/history` | — | Last 100 state changes |
|
||||||
|
| `POST` | `/webhook` | Bearer | Inbound webhook from the Pi |
|
||||||
|
| `GET` | `/health` | — | Health check |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
NixOS options under `services.noisebell-cache`:
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `domain` | required | Caddy virtual host domain |
|
||||||
|
| `piAddress` | required | Pi URL (e.g. `http://noisebell:80`) |
|
||||||
|
| `piApiKeyFile` | required | Key file for authenticating to the Pi |
|
||||||
|
| `inboundApiKeyFile` | required | Key file for validating inbound webhooks |
|
||||||
|
| `port` | `3000` | Listen port |
|
||||||
|
| `statusPollIntervalSecs` | `60` | How often to poll `GET /` on the Pi |
|
||||||
|
| `infoPollIntervalSecs` | `300` | How often to poll `GET /info` on the Pi |
|
||||||
|
| `offlineThreshold` | `3` | Consecutive failed polls before marking offline |
|
||||||
|
| `retryAttempts` | `3` | Outbound webhook retry count |
|
||||||
|
| `retryBaseDelaySecs` | `1` | Exponential backoff base delay |
|
||||||
|
| `httpTimeoutSecs` | `10` | HTTP request timeout |
|
||||||
|
| `dataDir` | `/var/lib/noisebell-cache` | SQLite database location |
|
||||||
|
| `outboundWebhooks` | `[]` | List of `{url, secretFile}` for downstream services |
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
| Secret | What it's for |
|
||||||
|
|--------|---------------|
|
||||||
|
| `piApiKeyFile` | Bearer token to poll the Pi's GET endpoints |
|
||||||
|
| `inboundApiKeyFile` | Bearer token the Pi sends when POSTing to `/webhook` |
|
||||||
|
| `outboundWebhooks[].secretFile` | Bearer token sent with outbound webhook POSTs |
|
||||||
28
remote/discord-bot/README.md
Normal file
28
remote/discord-bot/README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Discord Bot
|
||||||
|
|
||||||
|
Receives webhooks from the cache service and posts door status updates to a Discord channel as embeds.
|
||||||
|
|
||||||
|
- Green embed = door open
|
||||||
|
- Red embed = door closed
|
||||||
|
- Gray embed = Pi offline
|
||||||
|
|
||||||
|
Validates inbound webhooks with a Bearer token. Maintains a persistent Discord gateway connection with automatic reconnection.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Path | Auth | Description |
|
||||||
|
|--------|------|------|-------------|
|
||||||
|
| `POST` | `/webhook` | Bearer | Status update from the cache service |
|
||||||
|
| `GET` | `/health` | — | Health check |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
NixOS options under `services.noisebell-discord`:
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `domain` | required | Caddy virtual host domain |
|
||||||
|
| `discordTokenFile` | required | Discord bot token file |
|
||||||
|
| `channelId` | required | Discord channel ID to post to |
|
||||||
|
| `webhookSecretFile` | required | Shared secret with the cache service |
|
||||||
|
| `port` | `3001` | Listen port |
|
||||||
20
remote/rss-service/README.md
Normal file
20
remote/rss-service/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# RSS Service
|
||||||
|
|
||||||
|
Serves an Atom feed of door status history. Stateless — it fetches from the cache service's `/history` endpoint on each request and renders the last 7 days as Atom XML.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `GET` | `/feed` | Atom feed |
|
||||||
|
| `GET` | `/health` | Health check |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
NixOS options under `services.noisebell-rss`:
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `domain` | required | Caddy virtual host domain |
|
||||||
|
| `cacheUrl` | required | Cache service URL (e.g. `http://localhost:3000`) |
|
||||||
|
| `port` | `3002` | Listen port |
|
||||||
Loading…
Add table
Add a link
Reference in a new issue