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
+31
View File
@@ -200,6 +200,23 @@ IMAGESET_LABELS = {
}
IMAGESET_MISSING_SUFFIX = " - SATZ FEHLT"
# Der Abschlussbildschirm: was auf dem Panel stehen bleibt, wenn der Pi heruntergefaehrt
# oder neu gestartet wird. E-Paper ist bistabil - das zuletzt gezeichnete Bild bleibt fuer
# immer stehen, auch stromlos. Ohne diesen Schritt behauptet das Panel nach dem
# Ausschalten weiter den Zustand von vorher (bei der Abnahme stand dort die RUECKFRAGE,
# und der Pi sah aus, als warte er noch auf eine Eingabe).
#
# Die beiden Texte unterscheiden sich, und das ist der Punkt: beim Ausschalten DARF man
# den Stecker ziehen, beim Neustart NICHT - ein Zero W braucht gut anderthalb Minuten, bis
# der Anzeigedienst wieder zeichnet, und an genau diesem Pi hat ein Stromzug in dieser
# Wartezeit schon eine SD-Karte gekostet.
FINAL_LABELS = {
"poweroff": ("AUSGESCHALTET", "Strom kann getrennt werden", []),
"reboot": ("NEUSTART", "Strom NICHT trennen!",
["Der Pi kommt in rund 2 min wieder.",
"Solange bleibt dieses Bild stehen."]),
}
# Woraus die Station die Signiervariante abgeleitet hat (products.classify_variant):
# "-dev" kann in der Produkt-ID stehen, in der Version, oder in beiden. Die Quelle steht
# mit auf dem Panel, weil genau hier am 2026-09-15 ein falsch signiertes Image
@@ -250,6 +267,20 @@ def blocks(state: dict) -> list[tuple[str, str]]:
mode = state.get("mode")
out: list[tuple[str, str]] = []
if mode == "final":
# Abschlussbildschirm - KEINE Fusszeile und keine weiteren Zeilen: die Station ist
# im Begriff zu verschwinden, und die einzige Auskunft, auf die es jetzt ankommt,
# ist "darf ich den Stecker ziehen?".
label = FINAL_LABELS.get(state.get("final"))
if label is None:
# Unbekannte Spielart - lieber ehrlich nichts behaupten als "ausgeschaltet"
# zu sagen, wenn gleich wieder gebootet wird.
return [("Faehrt herunter", ROLE_BIG)]
title, hero, details = label
return ([(title, ROLE_HEADER), (hero, ROLE_HERO)]
+ [(d, ROLE_DETAIL) for d in details])
# Die Fusszeile haengt an JEDEM Statusbild - auch an "Station aus" und an "Status
# unbekannt", die beide sofort zurueckkehren. Sie wird deshalb hier gebaut und unten
# bei jedem Rueckgabepunkt angehaengt.