The display adapter (Documentation Index
Fetch the complete documentation index at: https://mintlify.com/chamilonster/Piumy/llms.txt
Use this file to discover all available pages before exploring further.
adapters/display/service.py) is a Python service that polls status.json and renders the pwnagotchi-style kaomoji face. It runs as the pimywa-display systemd service on the Pi, or as a one-shot script for development. The service never freezes — it drives a dynamic idle animation ticker that keeps the face alive between real status changes, lerping from a fast post-interaction cadence back to a slow, low-power idle blink as quiet time accumulates.
Backends
Three backends are available. Select one with thePIMYWA_DISPLAY environment variable.
file
Renders the face to a PNG file (path controlled by PIMYWA_DISPLAY_OUT, default display.png). No hardware required. Use this backend on a development PC to preview every mood, test layout changes, and verify the full face catalog before touching a real panel.
epaper-waveshare
Drives the Waveshare 2.13” V4 e-paper panel via spidev and libgpiod directly — no Waveshare vendor SDK or waveshare_epd Python package is needed. The epd2in13_V4 register protocol is implemented entirely in epaper/backend.py. On a Raspberry Pi, this is the production backend.
none
No output. The service starts and runs its status-polling loop normally, but nothing is sent to any display. Use this for headless operation where the binary must run (e.g. to keep the face.json sidecar updated) but no physical panel exists.
Installation
Install system packages
fonts-dejavu-core installs DejaVuSans.ttf at /usr/share/fonts/truetype/dejavu/. The renderer bundles its own copy in adapters/display/fonts/ and tries that first, so font rendering is identical on a dev PC and on the Pi.Install Python packages
requirements.txt requires Pillow>=10 and qrcode[pil]>=7. The qrcode dependency is only exercised when the core sets mood=qr.Running the file backend (development)
Thefile backend lets you render any mood to a PNG on a dev PC without any hardware. The standalone script lives in adapters/display/file/:
render.py reads PIMYWA_STATUS to find status.json. Set a mood first using the core:
render.py to see the updated face. The output path defaults to display.png; override it with PIMYWA_DISPLAY_OUT.
Environment variables
All knobs are env-driven (zero hardcode). Set them in/opt/pimywa/.env (loaded via the systemd EnvironmentFile= directive) or export them directly for development.
Service / animator
| Variable | Default | Description |
|---|---|---|
PIMYWA_STATUS | /opt/pimywa/data/status.json | Path to status.json the core writes |
PIMYWA_DISPLAY | file | Backend: file | epaper-waveshare | none |
PIMYWA_DISPLAY_OUT | display.png | Output PNG path (file backend only) |
PIMYWA_POLL_INTERVAL | 2.0 | Seconds between status.json mtime checks |
PIMYWA_ANIM_FAST_SEC | 25.0 | Fast idle-tick interval right after a real status change |
PIMYWA_ANIM_SLOW_SEC | 60.0 | Slow idle-tick interval once PIMYWA_ANIM_RAMP_SEC of quiet has passed |
PIMYWA_ANIM_RAMP_SEC | 180.0 | Seconds of quiet to ramp FAST → SLOW |
PIMYWA_FACE_MIN_SEC | 3.0 | Minimum seconds between face advances when piggybacking a data-change refresh |
PIMYWA_LOW_BATT | 15 | Battery % threshold for the battery-saver throttle |
PIMYWA_BATTERY_SAVER | 0 | Enable battery-saver mode (1): slows idle animation 4× when battery is below PIMYWA_LOW_BATT |
PIMYWA_EPAPER_FULL_REFRESH | 0 | Allow full-refresh flashes during operation (1): gates first-content-frame and big-mood-transition flashes; off by default |
PIMYWA_SLEEP_ON_EXIT | 1 | Put e-paper to sleep on service exit (0 to skip) |
PIMYWA_FACE_FILE | /run/pimywa/face.json | Path the service writes the face sidecar to |
PIMYWA_LOG_LEVEL | INFO | Python logging level (DEBUG | INFO | WARNING | ERROR) |
E-paper hardware variables
These variables configure theepaper-waveshare backend. The defaults match the Waveshare 2.13” V4 HAT wiring for a Raspberry Pi.
| Variable | Default | Description |
|---|---|---|
PIMYWA_EPAPER_RST_PIN | 17 | BCM GPIO pin for RESET |
PIMYWA_EPAPER_DC_PIN | 25 | BCM GPIO pin for Data/Command |
PIMYWA_EPAPER_BUSY_PIN | 24 | BCM GPIO pin for BUSY |
PIMYWA_EPAPER_PWR_PIN | 18 | BCM GPIO pin for power enable (set to empty string to skip) |
PIMYWA_EPAPER_GPIOCHIP | gpiochip0 | GPIO chip name or /dev/ path |
PIMYWA_EPAPER_SPI_DEV | /dev/spidev0.0 | SPI device path |
PIMYWA_EPAPER_SPI_SPEED | 4000000 | SPI clock speed in Hz |
PIMYWA_EPAPER_GHOST_PARTIALS | 40 | Full refresh every N partials (anti-ghosting); 0 = disabled |
GPIO pin mapping (Waveshare 2.13” HAT defaults)
| Signal | BCM GPIO | HAT pin | Env var |
|---|---|---|---|
| RST | 17 | 11 | PIMYWA_EPAPER_RST_PIN |
| DC | 25 | 22 | PIMYWA_EPAPER_DC_PIN |
| BUSY | 24 | 18 | PIMYWA_EPAPER_BUSY_PIN |
| PWR | 18 | 12 | PIMYWA_EPAPER_PWR_PIN |
| MOSI | 10 | 19 | (kernel SPI driver) |
| SCLK | 11 | 23 | (kernel SPI driver) |
| CS0 | 8 | 24 | PIMYWA_EPAPER_SPI_DEV |
Refresh policy
The adapter minimizes panel flashing to preserve e-paper lifetime and avoid visual interruption.| Event | Refresh type | Notes |
|---|---|---|
| Boot | Full refresh (unconditional) | One-time init: init() + Clear(0xFF) + base image. Performed by the backend itself before the service loop starts. Not gated by PIMYWA_EPAPER_FULL_REFRESH. |
| First content frame | Full refresh (opt-in) | Drawn from the known-blank boot baseline. Gated by PIMYWA_EPAPER_FULL_REFRESH (default 0 / off); partial if the flag is off. |
| Normal animation tick | Partial | No flash; only changed pixels update. |
| Minor mood change | Partial | No flash. |
| Major mood change | Full refresh (opt-in) | Entering or leaving qr, error, or sleeping. Gated by PIMYWA_EPAPER_FULL_REFRESH (default 0 / off); partial if the flag is off. |
| Identical image | Skipped entirely | No pointless partial update sent to the panel. |
| Anti-ghosting | Full refresh | Fires automatically every PIMYWA_EPAPER_GHOST_PARTIALS partials (default 40) to clear accumulated ghost pixels. |
Face sidecar
The display adapter writes the current kaomoji string to aface.json sidecar file (path controlled by PIMYWA_FACE_FILE, default /run/pimywa/face.json). The core merges it into status.json on its own heartbeat, so the web dashboard can mirror the live e-paper face without duplicating the face-selection logic.
The sidecar is written atomically (write to .tmp, then os.replace) on every rendered frame. For mood qr, the panel shows a full-canvas QR code and no kaomoji face string is drawn; face.json reflects this with "face": null.