Initial scaffold: README, project layout, and headless-chromium render loop sketch

This commit is contained in:
Padreug 2026-05-16 12:17:27 +02:00
commit 51f5ffcd66
8 changed files with 243 additions and 0 deletions

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
.env
.envrc
.DS_Store
# Local cached renders
out/
cache/
# Reference clones (keep tracked source-of-truth elsewhere)
reference/

107
README.md Normal file
View file

@ -0,0 +1,107 @@
# InkyImpression
Display server for a 13.3" Pimoroni Inky Impression on a Raspberry Pi Zero 2 W.
## Hardware
- **Display**: [Inky Impression 13.3"](https://shop.pimoroni.com/products/inky-impression) — 1600×1200, 6 colors (Spectra 6: red/green/blue/yellow/black/white), ~12s refresh (real-world 2035s).
- **Host**: Raspberry Pi Zero 2 W (quad-core ARMv7, 512 MB RAM — enough for headless Chromium on a single page).
- **Buttons**: 4 tactile buttons on the back. For the 13.3" variant they map to BCM GPIO **5, 6, 25, 24** (note: 25, not 16 — see `reference/inky/examples/spectra6/buttons.py`).
- **Mounting**: Pi Zero plugs straight into the socket header on the back. USB ports face down; text on Pi and Inky reads the same way up. **Power off before attaching/removing.**
## What this project does
Drives the Inky Impression to cycle through display "scenes":
1. **Tasks** — pulled from the webapp tasks module (`~/dev/webapp/src/modules/tasks`).
2. **Restaurant menu** — shown during the lunch window (12:0013:00).
3. **Announcements / events** — cycled via the 4 hardware buttons.
## Architecture
**Current strategy (this branch): headless Chromium on the Pi.**
The webapp already renders the scenes we want (`tasks.ariege.io`, etc.), so the Pi runs `chromium --headless --screenshot` against the live URL, then pushes the PNG through PIL into the `inky` library. E-ink refresh is 2035 s anyway, so a 515 s screenshot is comfortably within budget. Inky's `set_image()` handles dithering to the 6-color Spectra 6 palette.
Other strategies we may try on branches later:
- **Webapp JSON → PIL**: lighter, no browser, but layouts have to be redrawn by hand.
- **External render service**: only worth it if multiple Pis share renders, or if Chromium-on-Pi proves too slow/flaky.
## Initial Pi setup
Flash **Raspberry Pi OS Bookworm or later** (the "with desktop" image is recommended by Pimoroni — pulls in the dependencies, but Lite works if you install them manually).
On first boot:
```bash
sudo raspi-config
# Interfacing Options → enable SPI and I2C
sudo apt update && sudo apt upgrade -y
sudo apt install -y git python3-venv python3-pip chromium-browser
```
Install the Inky library via Pimoroni's installer (creates `~/.virtualenvs/pimoroni`):
```bash
git clone https://github.com/pimoroni/inky
cd inky
./install.sh
source ~/.virtualenvs/pimoroni/bin/activate
```
Verify the display is detected:
```bash
python3 -c "from inky.auto import auto; i = auto(verbose=True); print(i.resolution, i.colour)"
```
Quick smoke test using the bundled example:
```bash
cd ~/inky/examples/spectra6
python3 image.py --file images/spectra6-1600x1200.png
```
Then clone this repo on the Pi and install into the same venv:
```bash
git clone <this-repo-url> ~/InkyImpression
cd ~/InkyImpression
pip install -e .
inky-impression # runs the main loop
```
## Project layout
```
.
├── README.md
├── pyproject.toml
├── src/inky_impression/
│ ├── __init__.py
│ ├── __main__.py # `python -m inky_impression`
│ ├── main.py # scheduler loop
│ ├── render.py # chromium screenshot → PIL → Inky
│ └── scenes.py # which URL to show at what time
└── reference/inky/ # cloned Pimoroni library (gitignored)
```
## Reference
- Pimoroni library (cloned for reference): `reference/inky/`
- `inky/inky_el133uf1.py` — driver for the 13.3" Spectra 6 panel
- `examples/spectra6/image.py` — minimal "draw an image" example
- `examples/spectra6/buttons.py` — button GPIO setup (remember: SW_C = 25 for 13.3")
- [Pimoroni getting-started article](https://learn.pimoroni.com/article/getting-started-with-inky-impression)
- Webapp tasks module: `~/dev/webapp/src/modules/tasks`
## Version control
This repo uses [jj](https://github.com/martinvonz/jj) (colocated with git):
```bash
jj st # status
jj describe # set commit message for working copy
jj new # start a new change
```

21
pyproject.toml Normal file
View file

@ -0,0 +1,21 @@
[project]
name = "inky-impression"
version = "0.1.0"
description = "Display server for a 13.3\" Pimoroni Inky Impression on a Raspberry Pi Zero 2 W"
requires-python = ">=3.11"
dependencies = [
"inky[example-depends]>=2.0.0",
"Pillow>=10.0.0",
"gpiod>=2.0.0",
"gpiodevice>=0.0.5",
]
[project.scripts]
inky-impression = "inky_impression.main:main"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]

View file

View file

@ -0,0 +1,4 @@
from .main import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,29 @@
import logging
import sys
import time
from .render import show_url
from .scenes import current_scene
REFRESH_INTERVAL_S = 300
log = logging.getLogger("inky_impression")
def main() -> int:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
last_url: str | None = None
while True:
scene = current_scene()
if scene.url != last_url:
log.info("scene change → %s (%s)", scene.name, scene.url)
try:
show_url(scene.url)
last_url = scene.url
except Exception:
log.exception("render failed for %s", scene.url)
time.sleep(REFRESH_INTERVAL_S)
if __name__ == "__main__":
sys.exit(main() or 0)

View file

@ -0,0 +1,45 @@
import pathlib
import shutil
import subprocess
import tempfile
from PIL import Image
from inky.auto import auto
CHROMIUM_BIN = shutil.which("chromium") or shutil.which("chromium-browser")
def screenshot_url(url: str, width: int, height: int, out: pathlib.Path) -> None:
if not CHROMIUM_BIN:
raise RuntimeError("chromium not found on PATH; apt install chromium-browser")
subprocess.run(
[
CHROMIUM_BIN,
"--headless=new",
"--disable-gpu",
"--no-sandbox",
"--hide-scrollbars",
"--virtual-time-budget=5000",
f"--window-size={width},{height}",
f"--screenshot={out}",
url,
],
check=True,
# chromium writes the PNG to CWD when --screenshot is bare; pass a full
# path and run with cwd=parent so the file lands where we expect.
cwd=out.parent,
)
def show_url(url: str, *, saturation: float = 0.5) -> None:
inky = auto(ask_user=False, verbose=False)
w, h = inky.resolution
with tempfile.TemporaryDirectory() as tmp:
out = pathlib.Path(tmp) / "screen.png"
screenshot_url(url, w, h, out)
img = Image.open(out).convert("RGB").resize((w, h))
try:
inky.set_image(img, saturation=saturation)
except TypeError:
inky.set_image(img)
inky.show()

View file

@ -0,0 +1,22 @@
from dataclasses import dataclass
from datetime import datetime, time
@dataclass(frozen=True)
class Scene:
name: str
url: str
TASKS = Scene("tasks", "https://tasks.ariege.io")
MENU = Scene("menu", "https://menu.ariege.io") # TODO: real URL
LUNCH_START = time(12, 0)
LUNCH_END = time(13, 0)
def current_scene(now: datetime | None = None) -> Scene:
now = now or datetime.now()
if LUNCH_START <= now.time() < LUNCH_END:
return MENU
return TASKS