117 lines
5.5 KiB
Markdown
117 lines
5.5 KiB
Markdown
# 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`.
|