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

  1. Flash NixOS aarch64 SD image onto a card and boot the Pi.
  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:
    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:
    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 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:

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 -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.
S
Description
No description provided
Readme 66 KiB
Languages
Python 79.8%
Nix 10.8%
TypeScript 4.6%
Svelte 3%
CSS 1%
Other 0.8%