New screen alongside clock/weather/navidrome. Upload goes through the web UI (validated with PIL before accepting, stored under web/static/uploads/, gitignored), or point it at a URL re-fetched every 30 minutes. Cover/contain fit modes (new fit_cover/fit_contain helpers in render_utils.py, generalized from the square-only cover-art cropping already used by the Navidrome screen), plus the same contrast-boost/grayscale options that screen has.
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, 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
|
|
```
|