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
+113
View File
@@ -129,6 +129,21 @@ case "\$1" in
unit="\${!#}"
if [ "\$unit" = "ninlight-display" ]; then
code="\${FAKE_DISPLAY_SERVICE_ACTIVE_EXIT:-0}"
# Testschalter fuer den Abschlussbildschirm: der Anzeigedienst gilt fuer die
# ersten N Abfragen als AKTIV und danach als gestoppt - so, wie es beim
# Herunterfahren wirklich ablaeuft (systemd stoppt ihn, das dauert einen Moment).
# Gezaehlt wird in einer Datei, weil jeder Stub-Aufruf ein eigener Prozess ist.
if [ -n "\${FAKE_DISPLAY_ACTIVE_FOR:-}" ]; then
n=\$(cat "$rundir/.display-active-count" 2>/dev/null || echo 0)
n=\$((n + 1))
echo "\$n" >"$rundir/.display-active-count"
if [ "\$n" -le "\${FAKE_DISPLAY_ACTIVE_FOR}" ]; then
echo active
exit 0
fi
echo inactive
exit 3
fi
else
code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}"
fi
@@ -1308,6 +1323,104 @@ ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
set -e
echo "$ERR" | grep -q "keine Migrationsimages" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR"
echo
echo "=== Abschluss A: 'display final' kennt zwei verschiedene Bilder ==="
# E-Paper behaelt sein Bild dauerhaft. Beim Ausschalten darf man den Stecker ziehen, beim
# Neustart gerade nicht - deshalb zwei Schalter, nicht einer.
SBX="$TMPROOT/fin1"
setup_sandbox "$SBX"
export FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 # Anzeigedienst laeuft nicht
set +e
OUT=$(run_cli "$SBX" display final poweroff 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
grep -q "STUB display.py --final poweroff" "$SBX/cmd.log" && pass "display.py wird mit '--final poweroff' aufgerufen" || fail "falscher Aufruf: $(cat "$SBX/cmd.log")"
: >"$SBX/cmd.log"
run_cli "$SBX" display final reboot >/dev/null 2>&1
grep -q "STUB display.py --final reboot" "$SBX/cmd.log" && pass "und mit '--final reboot' fuer den Neustart" || fail "falscher Aufruf: $(cat "$SBX/cmd.log")"
grep -q "poweroff" "$SBX/cmd.log" && fail "der Neustart ruft das Ausschaltbild auf" || pass "die beiden Wege werden nicht verwechselt"
unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT
echo
echo "=== Abschluss B: ohne 'poweroff'/'reboot' passiert nichts ==="
SBX="$TMPROOT/fin2"
setup_sandbox "$SBX"
set +e
ERR=$(run_cli "$SBX" display final 2>&1 >/dev/null)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 ohne Spielart"
grep -q "STUB display.py" "$SBX/cmd.log" 2>/dev/null && fail "display.py wurde trotzdem aufgerufen" || pass "das Panel wurde nicht angefasst"
set +e
ERR=$(run_cli "$SBX" display final quatsch 2>&1 >/dev/null)
RC=$?
set -e
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 fuer eine unbekannte Spielart" || fail "Exit-Code 0 fuer 'quatsch'"
grep -q "STUB display.py" "$SBX/cmd.log" 2>/dev/null && fail "display.py wurde fuer 'quatsch' aufgerufen" || pass "und auch dafuer nicht"
echo
echo "=== Abschluss C: gezeichnet wird erst, NACHDEM der Anzeigedienst die Leitungen freigegeben hat ==="
# Zwei Prozesse auf denselben GPIO-/SPI-Leitungen enden in "lgpio.error: 'GPIO busy'" -
# in diesem Projekt schon zweimal passiert.
SBX="$TMPROOT/fin3"
setup_sandbox "$SBX"
export FAKE_DISPLAY_ACTIVE_FOR=2 # erst beim dritten Blick ist er gestoppt
set +e
OUT=$(NINLIGHT_DISPLAY_RELEASE_WAIT_S=5 run_cli "$SBX" display final poweroff 2>&1)
RC=$?
set -e
unset FAKE_DISPLAY_ACTIVE_FOR
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
grep -q "STUB display.py --final poweroff" "$SBX/cmd.log" && pass "das Abschlussbild wurde gezeichnet" || fail "gar nicht gezeichnet: $(cat "$SBX/cmd.log")"
# Die Reihenfolge in der GEMEINSAMEN Logdatei ist der Beleg: jede is-active-Abfrage steht
# vor dem display.py-Aufruf, keine danach.
# "|| true" ueberall: ohne Treffer liefert grep 1, und unter "set -e" wuerde die SUITE an
# dieser Stelle stillschweigend abbrechen, statt einen Fehler zu melden - eine Mutation,
# die das Warten ganz entfernt, waere dann scheinbar gruen (genau so passiert).
DRAW_LINE=$(grep -n "STUB display.py" "$SBX/cmd.log" 2>/dev/null | head -n1 | cut -d: -f1 || true)
LAST_CHECK=$(grep -n "systemctl is-active --quiet ninlight-display" "$SBX/cmd.log" 2>/dev/null | tail -n1 | cut -d: -f1 || true)
CHECKS=$(grep -c "systemctl is-active --quiet ninlight-display" "$SBX/cmd.log" 2>/dev/null || true)
CHECKS=${CHECKS:-0}
if [ -n "$DRAW_LINE" ] && [ -n "$LAST_CHECK" ] && [ "$LAST_CHECK" -lt "$DRAW_LINE" ]; then
pass "alle Abfragen des Anzeigedienstes stehen VOR dem Zeichnen"
else
fail "gezeichnet wurde nicht nach der Freigabe (Zeichnen Zeile '$DRAW_LINE', letzte Abfrage Zeile '$LAST_CHECK')"
fi
[ "$CHECKS" -ge 3 ] && pass "es wurde wirklich gewartet ($CHECKS Abfragen)" || fail "nur $CHECKS Abfrage(n) - es wurde gar nicht auf die Freigabe gewartet"
echo
echo "=== Abschluss D: ein Anzeigedienst, der nicht loslaesst, haelt das Herunterfahren nicht auf ==="
# Lieber kein Abschlussbild als ein haengendes Herunterfahren - und auf keinen Fall ein
# zweiter Prozess auf den belegten Leitungen.
SBX="$TMPROOT/fin4"
setup_sandbox "$SBX"
export FAKE_DISPLAY_ACTIVE_FOR=9999
set +e
ERR=$(NINLIGHT_DISPLAY_RELEASE_WAIT_S=2 run_cli "$SBX" display final poweroff 2>&1 >/dev/null)
RC=$?
set -e
unset FAKE_DISPLAY_ACTIVE_FOR
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (ein Anzeigefehler haelt das Herunterfahren nie auf)" || fail "Exit-Code $RC: $ERR"
grep -q "STUB display.py" "$SBX/cmd.log" && fail "es wurde auf belegte Leitungen gezeichnet - 'GPIO busy'" || pass "nicht gezeichnet, solange der Dienst haelt"
echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
# Und es wurde NICHT versucht, den Dienst von hier aus zu stoppen: das liefe mitten in
# der Abschalt-Transaktion.
grep -q "systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'systemctl stop' mitten im Herunterfahren" || pass "kein 'systemctl stop' aus dem Abschaltschritt heraus"
echo
echo "=== Abschluss E: ein scheiterndes display.py bleibt eine Warnung ==="
SBX="$TMPROOT/fin5"
setup_sandbox "$SBX"
export FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3
set +e
ERR=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display final reboot 2>&1 >/dev/null)
RC=$?
set -e
unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC - ein Anzeigefehler wuerde das Herunterfahren rot machen: $ERR"
echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."