feat(station): Abschlussbild beim Herunterfahren und beim Neustart

E-Paper behaelt sein Bild dauerhaft, auch stromlos. Faehrt der Pi
herunter, wird der Anzeigedienst mit abgeraeumt und das zuletzt
gezeichnete Bild bleibt fuer immer stehen - bei der Abnahme stand dort
die Rueckfrage des Tastermenues, und der ausgeschaltete Pi sah aus, als
warte er noch auf eine Eingabe.

Zwei neue Units, ninlight-poweroff.service (WantedBy=poweroff.target)
und ninlight-reboot.service (WantedBy=reboot.target). Sie haengen am
System, nicht am Tasterdienst: so greifen sie bei jedem Herunterfahren -
Menue, "poweroff" per SSH, "reboot" - und nicht nur auf einem von drei
Wegen. Zwei Units statt einer Fallunterscheidung, weil systemd einem
ExecStart nicht sagt, wohin die Reise geht.

Die Texte unterscheiden sich: "AUSGESCHALTET / Strom kann getrennt
werden" gegen "NEUSTART / Strom NICHT trennen!" samt Hinweis auf die
rund zwei Minuten, die der Zero W bis zum Anzeigedienst braucht. An
diesem Pi hat ein Stromzug in genau dieser Wartezeit schon eine
SD-Karte gekostet.

Reihenfolge: die Units ordnen sich per After=ninlight-display.service
hinter dessen Stop ein, und "ninlight-station display final <kind>"
wartet zusaetzlich, bis systemctl is-active nicht mehr anschlaegt -
dasselbe Muster wie display_handover(), nur ohne Wiederanfahren. Gibt
der Dienst die Leitungen nicht frei, wird nicht gezeichnet (GPIO busy),
sondern gewarnt: ein Anzeigefehler haelt das Herunterfahren nie auf.

Nebenbei behoben: display_run() reichte nur "$1" an display.py weiter
und haette "--final poweroff" halbiert.
This commit is contained in:
Michael Sauer
2026-09-15 03:02:21 +02:00
parent 300edafc3e
commit 4e1a965cbb
12 changed files with 572 additions and 4 deletions
+42
View File
@@ -0,0 +1,42 @@
[Unit]
Description=ninlight-Station: "Ausgeschaltet"-Bild aufs E-Paper
Documentation=file:/opt/ninlight-station/README.md
# E-Paper ist bistabil: das zuletzt gezeichnete Bild bleibt fuer immer stehen, auch
# stromlos. Faehrt der Pi herunter, wird der Anzeigedienst mit abgeraeumt - ohne diese
# Unit behauptet das Panel danach dauerhaft den Zustand von vorher. Bei der Abnahme stand
# dort die Rueckfrage des Tastermenues, und der ausgeschaltete Pi sah aus, als warte er
# noch auf eine Eingabe.
#
# Sie haengt am SYSTEM, nicht am Tasterdienst: so greift sie bei jedem Herunterfahren -
# ueber das Tastermenue (systemctl poweroff), ueber "poweroff" per SSH, ueber jeden
# anderen Weg nach poweroff.target. Am Taster allein haetten wir die Anzeige nur auf
# einem von drei Wegen.
#
# Die Gegenstueck-Unit ninlight-reboot.service macht dasselbe fuer den Neustart, mit
# ANDEREM Text: beim Neustart darf man den Stecker gerade nicht ziehen.
DefaultDependencies=no
# Erst zeichnen, wenn der Anzeigedienst die GPIO-/SPI-Leitungen losgelassen hat: zwei
# Prozesse darauf enden in "lgpio.error: 'GPIO busy'" (in diesem Projekt schon zweimal
# passiert). "After=" auf eine Unit, die gerade gestoppt wird, ordnet deren Stop VOR den
# Start dieser hier - genau die gesuchte Reihenfolge. Das CLI wartet zusaetzlich, bis
# "systemctl is-active ninlight-display" nicht mehr anschlaegt (display_final()).
After=ninlight-display.service ninlight-button.service
# ... und noch BEVOR das System die Leitungen verliert: nach umount.target gibt es weder
# /opt/ninlight-station noch die Schriftdateien, aus denen das Bild entsteht.
Before=umount.target shutdown.target final.target
[Service]
Type=oneshot
# Das Bild entsteht ueber das vorhandene CLI, nicht durch nachgebaute Logik: nur dort
# stehen EPD_REVISION aus /etc/ninlight-station.conf, die Uebergabe der GPIO-Leitungen
# und die Zusage "ein Fehler an der Anzeige haelt das Herunterfahren nie auf" (das CLI
# beendet sich hier immer mit 0).
ExecStart=/usr/local/bin/ninlight-station display final poweroff
# Ein Bildaufbau auf diesem Panel dauert Sekunden, nicht Minuten; dazu die Wartezeit auf
# die Freigabe der Leitungen. Der Deckel ist da, damit ein haengendes Panel das
# Herunterfahren nicht dauerhaft aufhaelt - systemds Vorgabe von 90 s waere hier eine
# sehr lange Zeit vor einem dunklen Geraet.
TimeoutStartSec=30
[Install]
WantedBy=poweroff.target