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.
125 lines
5.4 KiB
Markdown
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
|
|
```
|