A tiny HTTP server that lets you upload an image and have it displayed on a Pimoroni Inky Impression e-ink panel attached to a Raspberry Pi.

POST an image, it's fitted to the panel and shown. Built for the slow
reality of e-ink: one refresh at a time, bursts collapse to the most recent
image, and a configurable cooldown protects panel life.
No authentication. Intended for a trusted LAN / internal network. Put it behind a reverse proxy or firewall if you need access control.
POST /image endpoint — multipart file field or a raw image body.inky library (works on any Inky
Impression that inky.auto supports).Raspberry Pi with a Pimoroni Inky Impression connected.
Raspberry Pi OS with I2C and SPI enabled (sudo raspi-config →
Interface Options, or sudo raspi-config nonint do_i2c 0 && sudo raspi-config nonint do_spi 0,
then reboot).
Python 3.9+.
The Pimoroni inky library and its system dependencies. The easiest path is
Pimoroni's installer, which also creates a virtualenv:
git clone https://github.com/pimoroni/inky
cd inky && ./install.sh
This creates ~/.virtualenvs/pimoroni. Use that environment below.
git clone https://github.com/koryk/inky-impression-server
cd inky-impression-server
# Use the Pimoroni virtualenv (or any venv where `inky` works):
source ~/.virtualenvs/pimoroni/bin/activate
pip install .
inky-impression-server
or without installing:
python -m inky_impression_server
It listens on 0.0.0.0:8080 by default.
Upload an image (replace the host with your Pi's address):
curl -X POST -F "file=@photo.jpg" http://raspberrypi.local:8080/image
Raw body also works:
curl -X POST --data-binary @photo.jpg http://raspberrypi.local:8080/image
Response is immediate (202); the e-ink refresh then takes ~25–40s:
{"status":"accepted","seq":4,"note":"rendering now"}
| Method | Path | Purpose |
|---|---|---|
POST | /image | Upload an image (multipart file or raw body). Returns 202 with a seq. |
GET | /status | Worker/render state as JSON. |
GET | / | Plain-text usage. |
GET /status:
{"busy":true,"pending":false,"pending_seq":4,"rendered_seq":3,
"last_error":null,"panel_size":[1600,1200],"min_interval_seconds":30}
busy — a refresh is physically happening now.pending — an image is waiting to render next.rendered_seq — seq currently on the panel; when it reaches your upload's
seq, your image is showing.last_error — string if the last render failed, else null.All via environment variables:
| Variable | Default | Meaning |
|---|---|---|
INKY_HOST | 0.0.0.0 | Bind address. |
INKY_PORT | 8080 | Listen port. |
INKY_MIN_INTERVAL | 30 | Minimum seconds between the end of one refresh and the start of the next. |
INKY_SATURATION | 0.5 | Dithering saturation, 0.0–1.0. |
INKY_MAX_UPLOAD_MB | 16 | Reject uploads larger than this. |
Example:
INKY_PORT=9000 INKY_MIN_INTERVAL=60 inky-impression-server
A unit template is in packaging/inky-impression-server.service.
Edit User= and the venv path in ExecStart=, then:
sudo cp packaging/inky-impression-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now inky-impression-server
systemctl status inky-impression-server
journalctl -u inky-impression-server -f
The HTTP handler never touches the display. It validates and fits the upload,
then drops it into a single "pending" slot and returns 202. One worker
thread loops: wait for a pending image → render (~30s, blocking) → enforce the
minimum interval → repeat. Because the slot is single-valued, a burst of
uploads during a render collapses to "show the most recent one once" — earlier
pending uploads are dropped, not queued.
Issues and PRs welcome. Keep the concurrency model intact: only the worker thread may touch the Inky device, and the pending slot stays latest-wins.
3 commits
Python
100.0%
A tiny HTTP server that lets you upload an image and have it displayed on a Pimoroni Inky Impression e-ink panel attached to a Raspberry Pi.

POST an image, it's fitted to the panel and shown. Built for the slow
reality of e-ink: one refresh at a time, bursts collapse to the most recent
image, and a configurable cooldown protects panel life.
No authentication. Intended for a trusted LAN / internal network. Put it behind a reverse proxy or firewall if you need access control.
POST /image endpoint — multipart file field or a raw image body.inky library (works on any Inky
Impression that inky.auto supports).Raspberry Pi with a Pimoroni Inky Impression connected.
Raspberry Pi OS with I2C and SPI enabled (sudo raspi-config →
Interface Options, or sudo raspi-config nonint do_i2c 0 && sudo raspi-config nonint do_spi 0,
then reboot).
Python 3.9+.
The Pimoroni inky library and its system dependencies. The easiest path is
Pimoroni's installer, which also creates a virtualenv:
git clone https://github.com/pimoroni/inky
cd inky && ./install.sh
This creates ~/.virtualenvs/pimoroni. Use that environment below.
git clone https://github.com/koryk/inky-impression-server
cd inky-impression-server
# Use the Pimoroni virtualenv (or any venv where `inky` works):
source ~/.virtualenvs/pimoroni/bin/activate
pip install .
inky-impression-server
or without installing:
python -m inky_impression_server
It listens on 0.0.0.0:8080 by default.
Upload an image (replace the host with your Pi's address):
curl -X POST -F "file=@photo.jpg" http://raspberrypi.local:8080/image
Raw body also works:
curl -X POST --data-binary @photo.jpg http://raspberrypi.local:8080/image
Response is immediate (202); the e-ink refresh then takes ~25–40s:
{"status":"accepted","seq":4,"note":"rendering now"}
| Method | Path | Purpose |
|---|---|---|
POST | /image | Upload an image (multipart file or raw body). Returns 202 with a seq. |
GET | /status | Worker/render state as JSON. |
GET | / | Plain-text usage. |
GET /status:
{"busy":true,"pending":false,"pending_seq":4,"rendered_seq":3,
"last_error":null,"panel_size":[1600,1200],"min_interval_seconds":30}
busy — a refresh is physically happening now.pending — an image is waiting to render next.rendered_seq — seq currently on the panel; when it reaches your upload's
seq, your image is showing.last_error — string if the last render failed, else null.All via environment variables:
| Variable | Default | Meaning |
|---|---|---|
INKY_HOST | 0.0.0.0 | Bind address. |
INKY_PORT | 8080 | Listen port. |
INKY_MIN_INTERVAL | 30 | Minimum seconds between the end of one refresh and the start of the next. |
INKY_SATURATION | 0.5 | Dithering saturation, 0.0–1.0. |
INKY_MAX_UPLOAD_MB | 16 | Reject uploads larger than this. |
Example:
INKY_PORT=9000 INKY_MIN_INTERVAL=60 inky-impression-server
A unit template is in packaging/inky-impression-server.service.
Edit User= and the venv path in ExecStart=, then:
sudo cp packaging/inky-impression-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now inky-impression-server
systemctl status inky-impression-server
journalctl -u inky-impression-server -f
The HTTP handler never touches the display. It validates and fits the upload,
then drops it into a single "pending" slot and returns 202. One worker
thread loops: wait for a pending image → render (~30s, blocking) → enforce the
minimum interval → repeat. Because the slot is single-valued, a burst of
uploads during a render collapses to "show the most recent one once" — earlier
pending uploads are dropped, not queued.
Issues and PRs welcome. Keep the concurrency model intact: only the worker thread may touch the Inky device, and the pending slot stays latest-wins.
3 commits
Python
100.0%