There is a newspaper on my shelf that prints three times a day, runs on a battery, and spends roughly 99 percent of its life switched off. Not asleep: off. This is the story of how it got there.
The hardware
The press itself is modest: a Raspberry Pi 4, a Waveshare 7.5 inch e-paper panel (800 by 480, the V2), and a PiSugar 3 battery hat. The panel is nominally one-bit, black or white, but the driver has a quietly documented four-level grayscale mode. Each pixel becomes one of four luminance bytes:
--ink #000 → 0x00 (black)
--gray #7a7a7a → 0x80
--gray-light #c9c9c9 → 0xc0
--paper #fff → 0xff (white)Four tones turn out to be exactly enough for a newspaper: ink, paper, a secondary text gray, and a hairline gray for rules that should whisper.
The whole thing stands in portrait on a shelf. E-paper keeps its image at zero power, which is the trick the entire architecture leans on: the display does not need the computer once the page is printed.
The power model
The Pi boots, asks a server for the current edition, writes 384,000 bytes to the panel (one byte per pixel), tells the PiSugar when to wake it next, and shuts itself down. The full cycle takes about two minutes. Between cycles the only thing drawing power is the PiSugar’s real-time clock, waiting for its alarm.
All intelligence lives in the backend, a FastAPI service in a container on my home server. The device is deliberately dumb: it obeys a single field in the API response and nothing else.
{ "scene": "night", "color_mode": "gray4", "next_wake_minutes": 480 }Scheduling logic, content, rendering, even “should I wake less often because the battery is low”: all server-side, all changeable without touching the Pi.
A browser as typesetter
I did not want to draw pixels with a graphics library. The editions are Jinja2 templates rendered by headless Chromium (Playwright) at 480 by 800, with bundled woff2 fonts so renders are deterministic and offline-safe. Real typography, flexbox, proper kerning. The screenshot then gets quantized to the panel’s four levels: templates use exact tones so they snap cleanly, photos get error-diffusion dithering instead. A server-side render on a cache miss takes about a second; the worker pre-renders each edition before its window opens, so the device usually hits warm cache.
Design before code
Before any backend existed, the editions were designed as artboards on a Paper canvas, at exact panel resolution. We argued about them like a tiny editorial board: a broadsheet with a blackletter masthead won the morning slot, a Swiss-typography date sheet lost, a calendar took the office hours, a single quote took the night.
The artboards stayed the source of truth all the way through: when the templates drifted, we compared against the canvas, not against memory.
The losing Swiss design got a second life, which I will come back to.
The schedule follows the sun
The morning edition (the broadsheet: an on-this-day lead story from Wikipedia, three year-lookback briefs, weather from the station on my roof via Home Assistant) prints at sunrise, clamped between 05:30 and 07:45. The day edition is my calendar, refreshed hourly during office hours. The night edition is a single quote from my Readwise library, printed at sunset, clamped between 18:00 and 22:30, and it holds the glass all night at zero power. Sun times come from Home Assistant’s sun.sun; if that is ever unreachable, fixed hours take over and nothing breaks.
On weekends the calendar stays home. The resurrected Swiss design (the date sheet that lost the morning slot) prints at sunrise as the weekend edition and holds until sunset: a huge date, the history briefs, the weather, one line about today’s appointments. Three wakes a day instead of twelve, which the battery appreciates.
Last night I watched the first sunset-anchored cycle land: the night edition printed at 21:27:59, sunset to the minute, and the device was told to sleep 480 minutes. That wake-up lands at 05:30, the sunrise clamp, on a Saturday. The schedule knew before I did.
A sub-editor on retainer
The weakest part of the paper was the prose. Rule-based curation cut Wikipedia text to fit character budgets, and it showed: headlines like “Air India Flight 171” (an article title, not a headline) and briefs that ended mid-phrase. So the backend now passes the curated lead and briefs through one Claude API call a day. Haiku, structured output validated against a Pydantic schema, a system prompt that carries the house voice and forbids touching facts, years, or names:
class EditedEdition(BaseModel):
lead: EditedLead # headline, deck
briefs: list[EditedBrief] # year stays the original; only text is rewritten
edition = client.messages.parse(
model="claude-haiku-4-5",
max_tokens=2048,
system=PRESS_VOICE,
messages=[{"role": "user", "content": curated}],
output_format=EditedEdition,
).parsed_output“Air India Flight 171” came back as “Air India flight crashes near Ahmedabad”. The deck went from a Wikipedia first sentence to an actual deck.
The call is never load-bearing. No API key, an outage, a timeout, a malformed response: every failure path returns the rule-based copy and the edition prints anyway. The result is cached per date, so it costs one call and about a tenth of a cent per day. Character budgets are re-enforced mechanically after editing; the model is asked to write short, the trimmer guarantees it.
Honest telemetry
The PiSugar reports charge by measuring cell voltage, and every reading happens mid-boot under load. The result is a sawtooth: 93.5, 92.2, 87.5, 90.9, 91.5 over five hours, on a battery that was actually draining about 0.7 percent per hour. The fix is boring and effective. Keep the last five readings and report the median, not the latest sample:
self._history: deque[float] = deque(maxlen=5)
def battery_display(self) -> float:
return round(statistics.median(self._history), 1)The median goes to Home Assistant and to the printed page; the raw value stays available as a sensor attribute for anyone who misses the chaos. When the battery runs low, the paper says so the way a paper would: a black banner across the top, “low ink, please charge”.
What I keep from this one
The architecture that made everything easy was the dumbest one: a device that only knows how to obey one number, and a server that owns every decision. Every feature since launch (grayscale, solar scheduling, the weekend edition, the Claude editor, battery smoothing) shipped as a backend deploy while the Pi kept waking, printing, and switching itself off, none the wiser.
Build the brain where you can reach it. Let the thing on the shelf just be a newspaper.
BUILT.