Previously check() re-fetched the configured image URL over HTTP on every poll (every 30 min by default). If the remote image ever came back even slightly byte-different between fetches -- common with dynamically-generated images -- that forced a real e-ink flash for no visible reason, since this panel has no partial refresh. Now the URL is downloaded exactly once, server-side in the settings route, and saved into web/static/uploads/current.<ext> the same way a direct upload already was -- clearing any previous upload/download first so switching sources doesn't accumulate orphaned files. check() just reads that local file regardless of whether it came from an upload or a URL, with zero network calls. Also added a browser User-Agent to the download request -- confirmed live that Wikimedia (plausibly a common source people would link to) 403s the default requests/python User-Agent as bot traffic, same class of issue as the EFA transit endpoints handled earlier. Verified end-to-end against real URLs: download+cache works, a second URL replaces rather than accumulates, and a settings-only save (no new URL entered) no longer wipes the cached picture.
Paperwall
A standalone dashboard for one Waveshare epd4in0e (4", 6-color) e-ink panel:
Navidrome now-playing, clock, weather, departure boards, a fixed-route
journey planner, 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)
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
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>, 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
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
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:
- 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 Wavesharee-PaperGitHub repo for whether it's in the standardRaspberryPi_JetsonNano/python/lib/waveshare_epd/path or, likeepd4in0e, needs the "Separate Program" path instead). Write a new driver class mirroringdisplay/epd4in0e_driver.py, with the panel's real pixel resolution. If the model exposesdisplayPartial()(common on B/W and B/W/red panels, unlike this 6-color one), that's a real opportunity: add asupports_partialflag and an optionalshow_partial(image, region)to theDisplayprotocol, 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. - Pimoroni Inky panel: add
inkytorequirements-pi.txtand write a driver usinginky.auto.auto()instead of the Waveshare EPD classes -- simpler, since Pimoroni's library handles the hardware differences for you. - Either way, update
resolutionin the new driver, and swap which drivermain.py'sbuild_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