Files
Paperwall/README.md
T
Honey 8c9b3e1bfa Add Departures screen (train departure board)
New screens/departures.py using the free, unofficial db-vendo-client REST
API (v6.db.transport.rest) -- no key needed, same "no API key" philosophy
as Weather's Open-Meteo integration. Community-run infra though, not
official DB, and it was actually down (503, then timing out entirely) for
the whole time I was building this -- couldn't live-verify against real
data the way Open-Meteo was earlier, so I built the parsing against the
project's documented example response and stress-tested render() with
mocked requests.get() covering on-time/delayed/cancelled/missing-platform/
very-long-direction cases instead. check() fails soft (logs a warning,
returns None) on any request error, matching the "service might be down"
reality of this API -- the screen shows a plain "not available" message
rather than erroring out.

Settings: station name (resolved to a station ID via /locations, same
pattern as Weather's city->coordinates geocoding), how many departures to
show (3/5/8), and its own configurable update interval via the
get_poll_interval() mechanism built for Weather (defaults to 2 min --
departure boards go stale much faster than weather does).

Caught two real layout bugs while stress-testing render() before shipping:
line names ("ICE 1234") and delayed/cancelled time text ("Fällt aus") were
both wider than the columns I'd budgeted for them, with the second one
overlapping the direction text when right-aligned past its column's own
left edge. Fixed by measuring actual worst-case widths at the chosen font
size instead of guessing, and truncating every field to its column as a
defensive backstop regardless.
2026-07-22 21:38:20 +02:00

125 lines
5.4 KiB
Markdown

# Paperwall
A standalone dashboard for one Waveshare `epd4in0e` (4", 6-color) e-ink panel:
Navidrome now-playing, clock, weather, train departures, and pictures
(upload or URL), switchable/controllable from a local web UI. Started as an InkyPi
replacement, but doesn't depend on InkyPi at all -- own venv, own vendored
copy of the Waveshare driver files.
Confirmed by reading the vendor driver directly: this panel has no
partial-refresh mode at all -- every update is a full flash. So the design
here isn't about avoiding that flash, it's about only triggering it when
something real actually changed (song change, minute tick on the clock,
weather update), via hash-comparing each rendered frame against what's
currently on screen.
## Local development (no hardware needed)
```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python3 main.py --mock --port 8080
```
`--mock` saves each frame to `mock_output/` as a PNG instead of writing to
real hardware. The web UI (http://localhost:8080) works identically either
way -- same status page, same settings forms, same "switch screen now"
buttons. `requirements-pi.txt` (gpiozero/lgpio/RPi.GPIO) is only needed on
the real Pi, so local dev doesn't need it -- and those generally won't even
build off a Pi anyway.
## Installing on the Pi
```bash
git clone https://gitea.honeyy.duckdns.org/Honey/Paperwall.git /root/paperwall
cd /root/paperwall
sudo bash install.sh
```
That's the whole thing. It installs the few system packages needed to build
`lgpio`/`spidev` (`liblgpio-dev`, `swig`, `python3-dev`), enables SPI/I2C in
`config.txt` if not already on, creates its own venv right next to the
project and installs everything into it (`requirements.txt` +
`requirements-pi.txt`), and installs+starts `paperwall.service`. If it finds
InkyPi (or anything else) already running, it stops it first -- two things
can't drive the same panel at once -- but nothing is copied from or reused
out of it. Safe to re-run.
Then open `http://<pi-ip>:8080`, fill in Navidrome credentials under
**Navidrome** and a city under **Weather**. Everything saves immediately and
takes effect within a few seconds -- no restart needed.
## Updating
```bash
cd /root/paperwall
git pull
sudo systemctl restart paperwall.service
```
`venv/` and `config.json` are gitignored, so `git pull` never touches your
installed dependencies or your Navidrome/weather settings -- just the code.
A plain restart is enough for most changes. Only re-run `sudo bash
install.sh` when `requirements.txt` or `requirements-pi.txt` changed (new
dependency) or you need SPI/I2C re-checked -- it's idempotent, safe to run
any time.
## Uninstalling
```bash
sudo bash uninstall.sh
```
Stops the service, removes the systemd unit and the venv `install.sh`
created. Leaves the project folder and `config.json` alone -- delete those
yourself if you want it fully gone. Doesn't touch SPI/I2C or anything else
on the system.
(If you're migrating away from InkyPi specifically and want to remove *that*
too, see `uninstall_inkypi.sh` -- separate script, since it's InkyPi's
install that needs reversing, not Paperwall's.)
## Deploying to a different e-ink screen
Everything panel-specific lives behind the `Display` protocol in
`display/__init__.py` (`init()`, `show(image)`, `sleep()`) -- screens never
talk to hardware directly, they just return a `PIL.Image` sized to
`display.resolution`. To support a different panel:
1. **Waveshare panel, different model** (e.g. a monochrome one for fast
partial refresh): vendor that model's driver files into
`display/waveshare_epd/` the same way (check the Waveshare `e-Paper`
GitHub repo for whether it's in the standard
`RaspberryPi_JetsonNano/python/lib/waveshare_epd/` path or, like
`epd4in0e`, needs the "Separate Program" path instead). Write a new
driver class mirroring `display/epd4in0e_driver.py`, with the panel's
real pixel resolution. If the model exposes `displayPartial()` (common
on B/W and B/W/red panels, unlike this 6-color one), that's a real
opportunity: add a `supports_partial` flag and an optional
`show_partial(image, region)` to the `Display` protocol, and have the
scheduler use it for screens that only change a small region (e.g. just
the clock's time text) instead of always doing a full flash.
2. **Pimoroni Inky panel**: add `inky` to `requirements-pi.txt` and write a
driver using `inky.auto.auto()` instead of the Waveshare EPD classes --
simpler, since Pimoroni's library handles the hardware differences for
you.
3. Either way, update `resolution` in the new driver, and swap which driver
`main.py`'s `build_display()` constructs.
Nothing in `scheduler.py`, `config.py`, `web/`, or any screen needs to
change -- that's the point of the `Display` boundary.
## Layout
```
main.py entrypoint: init display, load config, start scheduler + web app
config.py config.json load/save
scheduler.py rotation / Navidrome-preemption / hash-dedup logic
display/ Display protocol + epd4in0e driver (vendored) + mock driver for dev
screens/ one module per screen (navidrome.py, clock.py, weather.py)
web/ Flask status page + settings forms
install.sh sets up venv + service on the Pi, standalone
uninstall.sh removes what install.sh set up
uninstall_inkypi.sh separate: removes InkyPi itself, if you were migrating from it
```