Initial scaffold: NixOS + SvelteKit dashboard + Python renderer

This commit is contained in:
Ruben Hensen
2026-06-07 18:15:06 +02:00
commit 50256fa384
25 changed files with 4491 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
# eink
NixOS-managed Raspberry Pi 4 driving a Waveshare 12.48" e-paper panel as a
calendar / day-planner. CalDAV data comes from the household Stalwart at
`mail.rubenhensen.nl`. The Pi pulls every commit on `main` via
[comin](https://github.com/nlewo/comin) — never touch the SD card after the
first flash.
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Raspberry Pi 4 (NixOS aarch64, hostname: dayplanner) │
│ │
│ systemd: eink-dashboard.service systemd: eink-renderer.service │
│ ┌──────────────────────────┐ ┌──────────────────────────────┐ │
│ │ SvelteKit (Node adapter) │◄──────│ Playwright → Pillow dither │ │
│ │ :3000 │ HTTP │ → Waveshare 12.48" driver │ │
│ │ fetches CalDAV every load│ │ (every REFRESH_SECONDS) │ │
│ └────────────┬─────────────┘ └───────────────┬──────────────┘ │
│ │ │ SPI │
│ ▼ ▼ │
│ mail.rubenhensen.nl Waveshare 12.48" HAT │
│ (Stalwart CalDAV) (1304×984, 4 sub-panels) │
└─────────────────────────────────────────────────────────────────────────┘
Deploy path: laptop → git push → Gitea → comin (60s poll) → Pi
```
## Layout
```
eink/
├── flake.nix # nixpkgs + comin → nixosConfigurations.dayplanner
├── hosts/dayplanner/ # this Pi's NixOS config (users, comin, hardware)
├── modules/ # eink-dashboard.nix + eink-renderer.nix
├── dashboard/ # SvelteKit app rendered headlessly at 1304×984
└── renderer/ # Python loop: screenshot → dither → push
```
## One-time bootstrap
1. **Flash NixOS aarch64 SD image** onto a card and boot the Pi.
- Image: <https://hydra.nixos.org/job/nixos/release-25.11/nixos.sd_image.aarch64-linux/latest>
- On first boot, set a `nixos` user password via the serial console or
monitor + keyboard. Enable SSH manually (`sudo systemctl start sshd`)
and note the Pi's IP.
2. **Add your SSH public key** to `hosts/dayplanner/default.nix` under
`users.users.ruben.openssh.authorizedKeys.keys`.
3. **Push this repo to a Git remote** the Pi can reach (e.g. your Gitea).
Update `services.comin.remotes[0].url` in `hosts/dayplanner/default.nix`
if the URL differs.
4. **First synchronous deploy from your laptop:**
```sh
nix run nixpkgs#nixos-rebuild -- switch \
--flake .#dayplanner \
--target-host nixos@<pi-ip> \
--build-host nixos@<pi-ip> \
--use-remote-sudo
```
(Building on the Pi itself avoids needing a cross-compile setup on your
Mac. Slow but one-time.)
5. **Drop the CalDAV password** on the Pi:
```sh
ssh ruben@<pi-ip> 'sudo install -d -m 700 /var/lib/eink/secrets'
ssh ruben@<pi-ip> 'sudo tee /var/lib/eink/secrets/caldav-password >/dev/null' \
<<< 'your-caldav-app-password'
ssh ruben@<pi-ip> 'sudo chmod 600 /var/lib/eink/secrets/caldav-password'
```
(Long-term replace this with [sops-nix](https://github.com/Mic92/sops-nix)
so the secret lives encrypted in this repo.)
6. **Vendor the Waveshare driver** (see `renderer/waveshare/README.md`).
After this, the steady-state workflow is just:
```sh
git push origin main # comin picks it up within ~60s and switches the Pi
```
## Iterating on the dashboard locally
You don't need the Pi to develop the UI — just open the SvelteKit dev
server in a browser sized to 1304×984:
```sh
cd dashboard
npm install
CALDAV_PASSWORD=... npm run dev -- --host
# open http://localhost:5173 in a browser with viewport 1304×984
```
Chrome devtools → "Toggle device toolbar" → set to 1304×984 to preview
exactly what the panel will see.
## Refresh cadence & panel wear
`eink.renderer.refreshSeconds = 300` (5 min) is the default. The 12.48"
panel has **no partial refresh** — every update is a full flash that
takes ~8s and consumes some panel life. Below 180 s is abusive; 300-600
s is comfortable. Consider scheduling a nightly invert to spread wear.
## Panel variant
Set `eink.renderer.panel = "bwr"` in `hosts/dayplanner/default.nix` if
you have the **12.48inch e-Paper Module (B)** (B/W/Red) instead of the
plain B/W version. The dashboard's red channel is whatever CSS color
matches `rgb(>180, <100, <100)` — see `renderer/render.py:to_bwr`.
## Troubleshooting
- **Comin not pulling:** `journalctl -u comin.service -f` on the Pi.
- **Renderer dry-running:** check `/tmp/eink-last.png` — if it exists,
the screenshot+dither pipeline works and the Waveshare driver is the
remaining piece (see `renderer/waveshare/README.md`).
- **Dashboard 500:** `journalctl -u eink-dashboard.service -f`. Most
common cause: empty `/var/lib/eink/secrets/caldav-password`.