Inkwell


A Go dashboard engine for e-ink panels, built to be fully testable without touching hardware.

Started 2026 · Active Go · Raspberry Pi · SPI · E-Paper

Inkwell drives a Waveshare 7.5" e-paper panel from a Raspberry Pi Zero 2 W. It composes widgets into a frame, packs that frame down to what the display can actually show, and pushes it over SPI. Out of the box it renders a calendar and weather dashboard from iCal feeds and an Open-Meteo ensemble.

The Inkwell dashboard as the panel renders it: a seven-day calendar with weather, an inverted column for today, temperature lines and precipitation bars

The reason it exists is less about e-ink and more about the state of the software around it. The Waveshare sample code is Python, written to demonstrate that the hardware works rather than to be built on, and it isn’t testable in any way I recognized. I wanted a dashboard I could keep changing for years without being afraid of it.

Adding a display is a data problem

The thing I’m happiest with is that the driver has no per-display code in it. Everything particular to a panel is data: how big it is, what colours it can manage, and the sequences it expects over the wire. Supporting a new panel means describing it and registering it, and the driver doesn’t change.

That was a goal from the first design document rather than something I noticed afterwards, and it survived contact with a second colour mode, which is usually where this kind of abstraction falls over.

Testing a thing you can’t plug in

The panel is one of four places Inkwell can draw. It can drive the real hardware through periph.io, write PNGs, record what it would have sent, or serve a live browser preview, and the dashboard code doesn’t know which one it got.

The preview is the one worth explaining. It doesn’t re-render the frame through a parallel drawing path, which would let it drift from the device. It captures the bytes going out over the wire and reconstructs the image from those, so the browser shows what the panel would receive. It defaults to that device view rather than the smooth source canvas, because the smooth version is a design aid and a liar.

The same dashboard as the source design canvas, in smooth grayscale before packing

The same dashboard packed to 1-bit black and white

None of that needs the panel plugged in, so I can change the rendering on a laptop and see what it would look like on the wall. The alternative is carrying a Raspberry Pi around to find out whether a refactor worked.

Refreshing without driving yourself mad

E-paper refresh is its own subsystem, and it took me longer to get right than the rendering did. Two separate questions: which waveform to use (skip, fast, full, or 4-gray), and when a change is allowed to push at all.

A full refresh flashes the whole panel to clear ghosting. It has to happen periodically, and it’s ugly, so it’s on an hourly cadence that is deliberately not user-configurable. It’s a property of the panel, not something a widget author should be reasoning about.

Per-widget refresh intervals are mandatory, with no default, so a config that forgets one fails to load rather than guessing on my behalf. They’re aligned to the wall clock, so two widgets on a five minute interval both land on :00, :05, :10 and coalesce into one panel update rather than flashing independently. The version of this I tried first let each widget refresh on its own schedule from whenever it started, and the panel never sat still.

There’s also a negative result recorded in the repo. I built a flicker-free version that refreshed only what changed, used it on real hardware, and found it left the changed area inverted. So it’s abandoned, with a note next to it, because writing that down is cheaper than rediscovering it in a year.

Getting it onto the desk

The panel lives on my desk, in a stand I 3D printed for it.

Releases are automated from conventional commits through to multi-arch tarballs, with the version demoted one level so a feat: doesn’t accidentally cut a 1.0 before I’m ready for one. On the Pi itself:

1
2
3
4
inkwell --version
sudo inkwell self-update --check
sudo inkwell self-update
sudo systemctl restart inkwell

A failed update leaves the running binary alone, so a bad release doesn’t take the dashboard off the wall.

Almost everything Inkwell needs is written out rather than pulled in. That wasn’t a purity exercise, it’s that each piece is small and specific enough that bending a general library would have been more work than writing it. Ask me again when someone sends me a calendar with a timezone I haven’t thought about.