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 — 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
- 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
nixosuser password via the serial console or monitor + keyboard. Enable SSH manually (sudo systemctl start sshd) and note the Pi's IP.
- Add your SSH public key to
hosts/dayplanner/default.nixunderusers.users.ruben.openssh.authorizedKeys.keys. - Push this repo to a Git remote the Pi can reach (e.g. your Gitea).
Update
services.comin.remotes[0].urlinhosts/dayplanner/default.nixif the URL differs. - First synchronous deploy from your laptop:
(Building on the Pi itself avoids needing a cross-compile setup on your Mac. Slow but one-time.)
nix run nixpkgs#nixos-rebuild -- switch \ --flake .#dayplanner \ --target-host nixos@<pi-ip> \ --build-host nixos@<pi-ip> \ --use-remote-sudo - Drop the CalDAV password on the Pi:
(Long-term replace this with sops-nix so the secret lives encrypted in this repo.)
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' - Vendor the Waveshare driver (see
renderer/waveshare/README.md).
After this, the steady-state workflow is just:
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:
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 -fon 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 (seerenderer/waveshare/README.md). - Dashboard 500:
journalctl -u eink-dashboard.service -f. Most common cause: empty/var/lib/eink/secrets/caldav-password.