Skip to content

Raspberry Pi example ​

A Raspberry Pi (Zero 2 W or newer) with a Pimoroni Inky display: Inky Impression (Spectra 6 or 7-colour, 4″ to 13.3″), Inky wHAT or pHAT. It's a ~100-line Python script.

The code is in examples/rpi-inky.

Since a Pi is usually plugged in, this example also uses the nudge: it tells the hub a notify_url, and the hub pokes it whenever you send a photo, so it shows up within seconds.

Set up ​

  1. Enable SPI and I2C (sudo raspi-config → Interface options) and install the Inky library:
bash
mkdir -p ~/openframe && cd ~/openframe
python3 -m venv .venv
.venv/bin/pip install "inky[rpi]" pillow requests
curl -O https://gitlab.com/yayitazale/eink-hub/-/raw/main/examples/rpi-inky/openframe.py
  1. In the hub: Add Canvas → Add manually → DIY frame with the display's resolution (e.g. 800×480). Copy the token from Connection.
  2. Try it:
bash
HUB_URL=http://192.168.1.10:8080 FRAME_TOKEN=… .venv/bin/python openframe.py
  1. Run it at boot with systemd:
ini
# systemd unit: copy to /etc/systemd/system/openframe.service, edit the two variables, then
#   sudo systemctl daemon-reload && sudo systemctl enable --now openframe
[Unit]
Description=E-Ink Hub Open Frame (Pimoroni Inky)
After=network-online.target
Wants=network-online.target

[Service]
Environment=HUB_URL=http://192.168.1.10:8080
Environment=FRAME_TOKEN=paste-the-token-here
ExecStart=/home/pi/openframe/.venv/bin/python /home/pi/openframe/openframe.py
WorkingDirectory=/home/pi/openframe
User=pi
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
bash
sudo cp openframe.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now openframe

How the code works ​

  • It asks for a full-colour PNG at the display's size and lets the inky library dither it, because the library's palettes are tuned for each panel's real inks.
  • It waits next_poll_s seconds, or less if the hub's nudge arrives on NOTIFY_PORT (8765 by default). Set NOTIFY_PORT=0 to turn the listener off.
  • On a battery-powered Pi you'd rather shut down between polls: replace the loop with a single poll() and a wake-up timer (e.g. a Witty Pi or PiSugar) set to next_poll_s.
python
#!/usr/bin/env python3
"""Open Frame example: Raspberry Pi + Pimoroni Inky (Impression, wHAT, pHAT…).

Polls E-Ink Hub, shows new pictures and sleeps for as long as the hub says. Because a Pi is
usually on mains power it also listens for the hub's nudge (notify_url), so photos you send
by hand appear within seconds instead of at the next poll.

Protocol: https://eink-hub-7ba3a3.gitlab.io/diy/protocol
"""
import io
import os
import socket
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer

import requests
from inky.auto import auto
from PIL import Image

HUB_URL = os.environ["HUB_URL"].rstrip("/")  # e.g. http://192.168.1.10:8080
TOKEN = os.environ["FRAME_TOKEN"]
NOTIFY_PORT = int(os.environ.get("NOTIFY_PORT", "8765"))  # 0 disables the nudge listener
SATURATION = float(os.environ.get("INKY_SATURATION", "0.5"))
RETRY_S = 300

AUTH = {"Authorization": f"Bearer {TOKEN}"}
wake = threading.Event()
inky = auto()
current = None  # id of the picture on screen


def lan_ip() -> str:
    # The address the hub can reach us at: the one used to route towards it.
    host = HUB_URL.split("//", 1)[1].split("/")[0].split(":")[0]
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.connect((host, 80))
        return s.getsockname()[0]


class Nudge(BaseHTTPRequestHandler):
    """The hub POSTs here (with our token) when there is something new."""

    def do_POST(self):
        ok = self.headers.get("Authorization") == AUTH["Authorization"]
        self.send_response(204 if ok else 401)
        self.end_headers()
        if ok:
            wake.set()

    def log_message(self, *args):
        pass


def poll() -> int:
    global current
    params = {
        "w": inky.width,
        "h": inky.height,
        # Full-colour PNG: the inky library dithers it with its own tuned palette.
        "format": "png",
        "fw": "rpi-inky-1.0",
    }
    if current:
        params["current"] = current
    if NOTIFY_PORT:
        params["notify_url"] = f"http://{lan_ip()}:{NOTIFY_PORT}/of/v1/notify"
    reply = requests.get(f"{HUB_URL}/of/v1/display", params=params, headers=AUTH, timeout=20).json()
    if reply["action"] == "show":
        png = requests.get(reply["image_url"], timeout=60).content
        image = Image.open(io.BytesIO(png)).convert("RGB")
        try:
            inky.set_image(image, saturation=SATURATION)
        except TypeError:  # black/white and red/yellow Inkys take no saturation
            inky.set_image(image)
        inky.show()
        current = reply["id"]
        print(f"showing {current}", flush=True)
    return int(reply["next_poll_s"])


def main():
    if NOTIFY_PORT:
        server = HTTPServer(("0.0.0.0", NOTIFY_PORT), Nudge)
        threading.Thread(target=server.serve_forever, daemon=True).start()
    while True:
        try:
            delay = poll()
        except Exception as err:  # network down, hub restarting…
            print(f"poll failed: {err}", flush=True)
            delay = RETRY_S
        wake.wait(timeout=delay)
        wake.clear()


if __name__ == "__main__":
    main()