Initial scaffold: README, project layout, and headless-chromium render loop sketch
This commit is contained in:
commit
51f5ffcd66
8 changed files with 243 additions and 0 deletions
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal 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
107
README.md
Normal 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 20–35s).
|
||||
- **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:00–13: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 20–35 s anyway, so a 5–15 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
21
pyproject.toml
Normal 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"]
|
||||
0
src/inky_impression/__init__.py
Normal file
0
src/inky_impression/__init__.py
Normal file
4
src/inky_impression/__main__.py
Normal file
4
src/inky_impression/__main__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
29
src/inky_impression/main.py
Normal file
29
src/inky_impression/main.py
Normal 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)
|
||||
45
src/inky_impression/render.py
Normal file
45
src/inky_impression/render.py
Normal 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()
|
||||
22
src/inky_impression/scenes.py
Normal file
22
src/inky_impression/scenes.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue