A calm e-ink display that shows one thought per hour — an idea, a question, or a poem — on a Waveshare 7.5" panel driven by a Raspberry Pi.
Mirror is deliberately not a dashboard. No news, no metrics, no notifications. Every card is built around a single piece of curated content, rendered in big type with a lot of whitespace, and replaced once an hour. The design goal is a surface you glance at while walking past, that occasionally hands you something worth turning over for the rest of the day.
┌─────────────────────────────────────────────┐
│ THIS MORNING │
│ │
│ What would you attempt if the │
│ attempt itself were the reward? │
│ │
│ ─────────────────────────────── │
│ Clear, 61F now · high 78, low 55 │
│ Mirror · hourly rotation │
└─────────────────────────────────────────────┘
Background on how the project came to be: huanyu.io.
Two content paths share one render pipeline:
- Editions (3×/day — 07:00, 12:00, 18:00): an LLM agent receives an offer of 2–3 candidate items assembled by a script, picks exactly one, and writes the card around it. The agent never invents content and never blends items.
- Interludes (every other hour, 08:00–23:00): a pure script composes the entire payload from the curated pools. No model runs at all. Most of what the panel shows involves no LLM.
Three daypart layouts: a morning card (question / aphorism / observation), a midday card (a mental model, a small vignette, or a word history), and an evening card (a poem, presented plainly — no interpretation, no framing).
Everything shown is drawn from three JSONL files curated into the repo — ~130 poems (public
domain, incl. classical Chinese kept in the original), ~213 ideas, ~102 morning fragments — plus a
live weather line (Open-Meteo). A validator (pools.py --validate) enforces schema, length caps,
and no-emoji rules (emoji render as tofu boxes on the panel's fonts).
Every render appends one row of telemetry, and all anti-repetition windows are derived from that
log rather than stored: a poem can't reappear within 14 days, a topic within 21, an idea domain
within 5, a fragment within 14. There is no separate state file to corrupt or drift — replaying
the log reproduces the system's memory exactly. A 30-day simulation (--simulate) replays the
whole scheme against the real pools and is the launch gate in the test suite.
About twice a week, the evening card may quote one verbatim line the owner wrote at least 30 days ago (mined from their notes vault), placed beside an unrelated poem or idea at moderate conceptual distance — with the connection deliberately never stated. The research behind this (reflective photo displays, technology-mediated reflection, serendipity in recommenders) says asserted connections read as horoscope material, while unstated juxtaposition lets the viewer complete the link themselves. Personal material is otherwise absent by design: the default card knows nothing about its owner.
A weekly job may also distill "threads" (sustained life patterns) from calendar/notes evidence, behind a hard evidence gate — at least 3 distinct days, 2 evidence kinds, 1 non-agent source. Zero threads passing in a given week is the expected outcome. This exists because v1 of this project taught the opposite lesson: mining an agent's operational logs for "signals" produces repetitive, over-indexed, forced-feeling cards.
- 800×480, 1-bit monochrome. Fixed type scale (title 44px / hero 34px / body 23px / small 16px).
- Poems are never re-wrapped — the poet's line breaks are the layout. The whole block shrinks (32px → 18px) until the widest line fits.
- Icons are drawn as PIL vector primitives (four of them, total). No glyph fonts, no emoji.
- Payloads are sanitized against per-layout character caps with a deterministic fallback chain (payload → last-good state → static default), then pushed over SSH and atomically swapped before render.
| Path | What it is |
|---|---|
scripts/next_context.py |
Hour routing, offers, interlude payload composition, --simulate |
scripts/ledger.py |
Telemetry-derived repetition windows |
scripts/pools.py |
Pool loading + validation CLI |
scripts/resonance.py |
The personal lane: artifact eligibility, seeded scheduling |
scripts/build_artifacts.py |
Notes-vault → verbatim artifact registry (weekly) |
scripts/distill_threads.py |
Evidence gate for proposed personal threads (weekly) |
scripts/sanitize_payload.py |
Limits, fallbacks, telemetry identity |
scripts/render_mirror.py |
Pillow renderer (runs on the Pi; --dry-run --png-out locally) |
scripts/push_payload.sh |
Sanitize → SCP → atomic swap → render → telemetry |
data/*.jsonl |
The curated content pools |
references/ |
Architecture notes, runbook, cron policy |
tests/ |
Plain-python suites, no framework; test_simulation.py is the launch gate |
The renderer and content pipeline are portable; the scheduling assumes an agent workspace with a
cron registry and an agent that can follow SKILL.md (this install uses OpenClaw). What runs
standalone today:
# build a context/payload for the current hour
python3 scripts/next_context.py --emit-payload --json-path /tmp/ctx.json --payload-path /tmp/payload.json
# preview the payload as a PNG, no hardware needed
python3 scripts/render_mirror.py --payload /tmp/payload.json --dry-run --png-out /tmp/p.png
# replay 30 days of scheduling without touching state
python3 scripts/next_context.py --simulate 30 --start-date 2026-08-01
# validate the pools
python3 scripts/pools.py --validate poems --path data/poems.jsonlRequirements: Python 3.11+ (stdlib only for the pipeline), Pillow for the renderer, a Raspberry
Pi + Waveshare epd7in5_V2 for the panel itself. Local previews need DejaVu fonts — point
MIRROR_FONT_DIR at a directory containing them, or the preview falls back to a bitmap font.
Personal configuration (coordinates for weather, SSH host, vault path) lives in a gitignored
.env.local — copy .env.local.example and fill it in. The tracked tree contains no
location, identity, or host information by design.
for f in tests/test_*.py; do python3 "$f" || break; doneEach suite is a standalone script that exits non-zero on failure. test_simulation.py replays 30
days against the real pools and asserts no repetition window was violated and no pool ran dry.
- One thought per screen
- Big type, low ornament
- Ideas by default; personal material is rare and earned
- Monochrome iconography — never emoji glyphs
- Respect e-ink rhythm: slow, deliberate, stable
- Deterministic fallbacks over ad hoc failure handling
- Telemetry for every render — it is the ledger, not a log
