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
+32
View File
@@ -99,6 +99,38 @@ den Tasterdienst wie den Anzeigedienst; `deploy.sh` liefert `button/` bei jedem
aus. Ein fehlgeschlagenes Aktivieren/Starten ist dabei nur eine Warnung — eine Station aus. Ein fehlgeschlagenes Aktivieren/Starten ist dabei nur eine Warnung — eine Station
ohne Taster muss vollständig benutzbar bleiben. ohne Taster muss vollständig benutzbar bleiben.
## Abschlussbildschirm beim Herunterfahren und beim Neustart
E-Paper behält sein Bild **dauerhaft**, auch stromlos. Fährt der Pi herunter, wird der
Anzeigedienst mit abgeräumt, und das zuletzt gezeichnete Bild bleibt für immer stehen —
bei der Abnahme stand dort die **Rückfrage** des Tastermenüs, und der ausgeschaltete Pi
sah aus, als warte er noch auf eine Eingabe.
Dafür gibt es zwei systemd-Units, `ninlight-poweroff.service` (`WantedBy=poweroff.target`)
und `ninlight-reboot.service` (`WantedBy=reboot.target`). Sie hängen am **System**, nicht
am Tasterdienst: so greifen sie bei jedem Herunterfahren — über das Menü, über `poweroff`
per SSH, über `reboot` — und nicht nur auf einem von drei Wegen.
**Die Texte unterscheiden sich, und das ist der Punkt:**
| | Panel |
|---|---|
| Herunterfahren | „AUSGESCHALTET — Strom kann getrennt werden" |
| Neustart | „NEUSTART — Strom NICHT trennen!" plus der Hinweis, dass es rund 2 min dauert |
Ein Zero W braucht gut anderthalb Minuten, bis der Anzeigedienst wieder zeichnet. Stünde
dort währenddessen „Strom kann getrennt werden", wäre das eine Aufforderung zu genau dem
Griff, der an diesem Pi schon eine SD-Karte gekostet hat.
**Reihenfolge:** der Anzeigedienst hält die GPIO-/SPI-Leitungen; zwei Prozesse darauf
ergeben `lgpio.error: 'GPIO busy'`. Die Units ordnen sich deshalb per
`After=ninlight-display.service` hinter dessen Stop ein, und `ninlight-station display
final <poweroff|reboot>` wartet zusätzlich, bis `systemctl is-active ninlight-display`
nicht mehr anschlägt — dasselbe Muster wie `display_handover()` bei `display test`, nur
ohne Wiederanfahren. Gibt der Dienst die Leitungen nicht frei, wird **nicht** gezeichnet,
sondern eine Zeile ins Journal geschrieben: ein Fehler an der Anzeige darf das
Herunterfahren nie aufhalten.
SSID und Passwort stehen in `/etc/ninlight-station.conf` auf dem Pi und müssen zu den SSID und Passwort stehen in `/etc/ninlight-station.conf` auf dem Pi und müssen zu den
Build-Parametern `NINLIGHT_RECOVERY_SSID` und `NINLIGHT_RECOVERY_PASSWORD` aus Build-Parametern `NINLIGHT_RECOVERY_SSID` und `NINLIGHT_RECOVERY_PASSWORD` aus
`config.local.cmake` passen. Weicht eines ab, findet die Lampe die Station nicht. `config.local.cmake` passen. Weicht eines ab, findet die Lampe die Station nicht.
+9 -1
View File
@@ -255,6 +255,7 @@ echo "== uebertragen =="
rsync -a -c --delete -e "$SSH_BIN" "$STAGE/" "$HOST:/tmp/ninlight-images/" rsync -a -c --delete -e "$SSH_BIN" "$STAGE/" "$HOST:/tmp/ninlight-images/"
rsync -a -e "$SSH_BIN" "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \ rsync -a -e "$SSH_BIN" "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \
"$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HERE/ninlight-button.service" \ "$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HERE/ninlight-button.service" \
"$HERE/ninlight-poweroff.service" "$HERE/ninlight-reboot.service" \
"$HOST:/tmp/" "$HOST:/tmp/"
# Das Anzeigeverzeichnis als Ganzes (render.py, display.py, epdconfig.py, die beiden # Das Anzeigeverzeichnis als Ganzes (render.py, display.py, epdconfig.py, die beiden
# uebernommenen Panel-Treiber und HERKUNFT.md - der Herkunftsnachweis gehoert mit auf # uebernommenen Panel-Treiber und HERKUNFT.md - der Herkunftsnachweis gehoert mit auf
@@ -277,6 +278,8 @@ BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station}
UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service} UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service}
DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service} DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service}
BUTTON_UNIT_PATH=${NINLIGHT_BUTTON_UNIT:-/etc/systemd/system/ninlight-button.service} BUTTON_UNIT_PATH=${NINLIGHT_BUTTON_UNIT:-/etc/systemd/system/ninlight-button.service}
POWEROFF_UNIT_PATH=${NINLIGHT_POWEROFF_UNIT:-/etc/systemd/system/ninlight-poweroff.service}
REBOOT_UNIT_PATH=${NINLIGHT_REBOOT_UNIT:-/etc/systemd/system/ninlight-reboot.service}
sudo install -d -m 0755 "$ROOT_DIR/images" sudo install -d -m 0755 "$ROOT_DIR/images"
if [ "$FULL_SYNC" -eq 1 ]; then if [ "$FULL_SYNC" -eq 1 ]; then
@@ -430,8 +433,13 @@ sudo install -m 0644 /tmp/ninlight-display.service "$DISPLAY_UNIT_PATH"
sudo install -d -m 0755 "$ROOT_DIR/button" sudo install -d -m 0755 "$ROOT_DIR/button"
sudo rsync -a -c --delete /tmp/ninlight-button/ "$ROOT_DIR/button/" sudo rsync -a -c --delete /tmp/ninlight-button/ "$ROOT_DIR/button/"
sudo install -m 0644 /tmp/ninlight-button.service "$BUTTON_UNIT_PATH" sudo install -m 0644 /tmp/ninlight-button.service "$BUTTON_UNIT_PATH"
# Abschlussbild beim Herunterfahren/Neustart. Nur die Dateien - aktiviert werden sie in
# setup_pi.sh (ein Deploy soll keine Units einschalten, die vorher aus waren).
sudo install -m 0644 /tmp/ninlight-poweroff.service "$POWEROFF_UNIT_PATH"
sudo install -m 0644 /tmp/ninlight-reboot.service "$REBOOT_UNIT_PATH"
rm -rf /tmp/ninlight-images /tmp/server.py /tmp/products.py /tmp/ninlight-station /tmp/ninlight-station.service \ rm -rf /tmp/ninlight-images /tmp/server.py /tmp/products.py /tmp/ninlight-station /tmp/ninlight-station.service \
/tmp/ninlight-display /tmp/ninlight-display.service /tmp/ninlight-button /tmp/ninlight-button.service /tmp/ninlight-display /tmp/ninlight-display.service /tmp/ninlight-button /tmp/ninlight-button.service \
/tmp/ninlight-poweroff.service /tmp/ninlight-reboot.service
sudo systemctl daemon-reload sudo systemctl daemon-reload
if systemctl is-active --quiet ninlight-station; then if systemctl is-active --quiet ninlight-station; then
if sudo systemctl restart ninlight-station; then if sudo systemctl restart ninlight-station; then
+23
View File
@@ -560,6 +560,24 @@ def run_test(revision: str) -> None:
log.info("Testbild gezeichnet (EPD_REVISION=%s)", revision) log.info("Testbild gezeichnet (EPD_REVISION=%s)", revision)
def run_final(revision: str, kind: str) -> None:
"""Zeichnet den Abschlussbildschirm ("poweroff"/"reboot") und legt das Panel schlafen.
Das ist das Bild, das nach dem Herunterfahren DAUERHAFT stehen bleibt - E-Paper ist
bistabil, es haelt sein Bild auch ohne Strom. Bewusst ein VOLLAUFBAU: ein Teilaufbau
liesse Reste des vorigen Bildes stehen, und dieses eine Bild ueberdauert alle anderen.
Aufgerufen wird das ausschliesslich ueber "ninlight-station display final <kind>" aus
einer systemd-Unit heraus, und zwar ERST, nachdem der Anzeigedienst gestoppt ist - er
haelt sonst die GPIO-/SPI-Leitungen ("lgpio.error: 'GPIO busy'"). Diese Uebergabe
macht das CLI (display_final()), nicht diese Funktion."""
panel = Panel(revision)
panel.clear()
panel.draw_full(render.compose({"mode": "final", "final": kind}))
panel.sleep()
log.info("Abschlussbild gezeichnet (%s, EPD_REVISION=%s)", kind, revision)
def run_clear(revision: str) -> None: def run_clear(revision: str) -> None:
panel = Panel(revision) panel = Panel(revision)
panel.clear() panel.clear()
@@ -634,6 +652,9 @@ def main() -> None:
group = parser.add_mutually_exclusive_group() group = parser.add_mutually_exclusive_group()
group.add_argument("--test", action="store_true", help="Testbild zeichnen und beenden") group.add_argument("--test", action="store_true", help="Testbild zeichnen und beenden")
group.add_argument("--clear", action="store_true", help="Panel leeren und beenden") group.add_argument("--clear", action="store_true", help="Panel leeren und beenden")
group.add_argument("--final", choices=sorted(render.FINAL_LABELS),
help="Abschlussbild zeichnen und beenden (beim Herunterfahren "
"bzw. Neustart; die Texte unterscheiden sich)")
args = parser.parse_args() args = parser.parse_args()
revision = os.environ.get("EPD_REVISION", "V3") revision = os.environ.get("EPD_REVISION", "V3")
@@ -643,6 +664,8 @@ def main() -> None:
run_test(revision) run_test(revision)
elif args.clear: elif args.clear:
run_clear(revision) run_clear(revision)
elif args.final:
run_final(revision, args.final)
else: else:
run_daemon(revision) run_daemon(revision)
except (DisplayUnavailable, ImportError) as e: except (DisplayUnavailable, ImportError) as e:
+31
View File
@@ -200,6 +200,23 @@ IMAGESET_LABELS = {
} }
IMAGESET_MISSING_SUFFIX = " - SATZ FEHLT" 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): # 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 # "-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 # 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") mode = state.get("mode")
out: list[tuple[str, str]] = [] 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 # 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 # unbekannt", die beide sofort zurueckkehren. Sie wird deshalb hier gebaut und unten
# bei jedem Rueckgabepunkt angehaengt. # bei jedem Rueckgabepunkt angehaengt.
+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
+30
View File
@@ -0,0 +1,30 @@
[Unit]
Description=ninlight-Station: "Neustart"-Bild aufs E-Paper
Documentation=file:/opt/ninlight-station/README.md
# Das Gegenstueck zu ninlight-poweroff.service, mit einem ANDEREN Text - und das ist hier
# kein Schoenheitsfehler, sondern der Zweck der zweiten Unit:
#
# Herunterfahren -> "AUSGESCHALTET / Strom kann getrennt werden"
# Neustart -> "NEUSTART / Strom NICHT trennen!"
#
# Ein Raspberry Pi Zero W braucht gut anderthalb Minuten, bis der Anzeigedienst wieder
# zeichnet. Stuende dort waehrenddessen "Strom kann getrennt werden", waere das eine
# Aufforderung zu genau dem Griff, der an diesem Pi schon eine SD-Karte gekostet hat.
#
# Zwei Units statt einer mit Fallunterscheidung: systemd sagt einem ExecStop/ExecStart
# nicht, wohin die Reise geht - man muesste "systemctl list-jobs" nach "reboot.target"
# durchsuchen. Ueber WantedBy= ist es dagegen der Abschalt-Zieleinheit selbst entnommen
# und damit nicht zu verwechseln.
DefaultDependencies=no
# Reihenfolge und Begruendung wie in ninlight-poweroff.service (GPIO-Uebergabe, und noch
# vor umount.target zeichnen).
After=ninlight-display.service ninlight-button.service
Before=umount.target shutdown.target final.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ninlight-station display final reboot
TimeoutStartSec=30
[Install]
WantedBy=reboot.target
+58 -2
View File
@@ -674,7 +674,11 @@ migrate_on() {
# an den Python-Prozess durch. "sudo", weil /dev/spidev0.0 und die GPIO-Leitungen root # an den Python-Prozess durch. "sudo", weil /dev/spidev0.0 und die GPIO-Leitungen root
# gehoeren (der Anzeigedienst laeuft aus demselben Grund als root). # gehoeren (der Anzeigedienst laeuft aus demselben Grund als root).
display_run() { display_run() {
sudo env EPD_REVISION="$EPD_REVISION" python3 "$STATION_LIB/display/display.py" "$1" # "$@", nicht "$1": das Abschlussbild wird mit ZWEI Argumenten gerufen
# ("--final poweroff"), und ein verschlucktes zweites waere hier besonders teuer -
# argparse erzwingt bei --final eine Auswahl, der Aufruf scheiterte also, und auf dem
# Panel bliebe die letzte Rueckfrage stehen.
sudo env EPD_REVISION="$EPD_REVISION" python3 "$STATION_LIB/display/display.py" "$@"
} }
# Handbetrieb am Panel ("display test"/"display clear"), waehrend der Anzeigedienst # Handbetrieb am Panel ("display test"/"display clear"), waehrend der Anzeigedienst
@@ -779,6 +783,44 @@ display_off() {
display_run --clear || echo "Warnung: Panel konnte nicht geleert werden - Anzeige pruefen" >&2 display_run --clear || echo "Warnung: Panel konnte nicht geleert werden - Anzeige pruefen" >&2
} }
# --- Abschlussbildschirm 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, und der ausgeschaltete Pi sah aus, als warte
# er noch auf eine Eingabe.
#
# Dieser Handgriff gehoert deshalb ins System (ninlight-poweroff.service /
# ninlight-reboot.service), nicht in den Tasterdienst: so greift er bei JEDEM
# Herunterfahren - ueber das Menue, ueber "poweroff" per SSH, ueber "reboot" - und nicht
# nur auf einem von drei Wegen.
#
# Wie lange auf die Freigabe der GPIO-/SPI-Leitungen gewartet wird. Die Units ordnen sich
# per "After=ninlight-display.service" HINTER dessen Stop ein; systemd erledigt das also
# schon. Die Wartezeit ist das Netz darunter (und der Weg fuer einen Handaufruf zum
# Ausprobieren): zwei Prozesse auf denselben Leitungen enden in "lgpio.error: 'GPIO busy'",
# und das ist in diesem Projekt schon zweimal passiert.
DISPLAY_RELEASE_WAIT_S=${NINLIGHT_DISPLAY_RELEASE_WAIT_S:-10}
# $1 = poweroff|reboot. Beendet IMMER mit 0: ein Fehler an der Anzeige darf das
# Herunterfahren nie aufhalten (dieselbe Zusage wie ueberall sonst).
display_final() {
_waited=0
while systemctl is-active --quiet ninlight-display 2>/dev/null; do
if [ "$_waited" -ge "$DISPLAY_RELEASE_WAIT_S" ]; then
# Bewusst KEIN "systemctl stop" von hier aus: das liefe mitten in der
# Abschalt-Transaktion und koennte sich mit ihr verhaken. Lieber eine ehrliche
# Zeile im Journal als ein haengendes Herunterfahren.
echo "Warnung: Anzeigedienst laeuft nach ${DISPLAY_RELEASE_WAIT_S}s noch - kein Abschlussbild (GPIO belegt)" >&2
return 0
fi
sleep 1
_waited=$((_waited + 1))
done
display_run --final "$1" || echo "Warnung: Abschlussbild konnte nicht gezeichnet werden - Anzeige pruefen" >&2
return 0
}
full_off() { full_off() {
# ZUERST: ein noch vorgemerkter Zeitschalter muss mit weg, sonst feuert er spaeter in # ZUERST: ein noch vorgemerkter Zeitschalter muss mit weg, sonst feuert er spaeter in
# eine Station hinein, die laengst aus ist - oder, schlimmer, in eine, die inzwischen # eine Station hinein, die laengst aus ist - oder, schlimmer, in eine, die inzwischen
@@ -1028,6 +1070,12 @@ display_cmd() {
test) display_handover --test ;; test) display_handover --test ;;
clear) display_handover --clear ;; clear) display_handover --clear ;;
off) display_off ;; off) display_off ;;
final)
case "${2:-}" in
poweroff|reboot) display_final "$2" ;;
*) echo "Fehler: 'display final' braucht 'poweroff' oder 'reboot'" >&2; exit 2 ;;
esac
;;
*) usage; exit 2 ;; *) usage; exit 2 ;;
esac esac
} }
@@ -1061,6 +1109,14 @@ usage() {
echo " Ein laufender Anzeigedienst wird dafuer kurz gestoppt und" >&2 echo " Ein laufender Anzeigedienst wird dafuer kurz gestoppt und" >&2
echo " danach wieder gestartet (er haelt sonst die GPIO-Leitungen)." >&2 echo " danach wieder gestartet (er haelt sonst die GPIO-Leitungen)." >&2
echo " display clear Panel leeren (ebenfalls mit kurzer Unterbrechung des Dienstes)." >&2 echo " display clear Panel leeren (ebenfalls mit kurzer Unterbrechung des Dienstes)." >&2
echo " display final {poweroff|reboot}" >&2
echo " Abschlussbild aufs Panel. Kein Handgriff fuer Menschen: das" >&2
echo " rufen ninlight-poweroff.service/ninlight-reboot.service beim" >&2
echo " Herunterfahren bzw. Neustart auf, nachdem der Anzeigedienst" >&2
echo " die GPIO-Leitungen freigegeben hat. E-Paper behaelt sein Bild" >&2
echo " dauerhaft - ohne diesen Schritt bliebe die letzte Rueckfrage" >&2
echo " auf dem Panel stehen. Die Texte unterscheiden sich: beim" >&2
echo " Neustart darf man den Stecker gerade NICHT ziehen." >&2
echo " display off Anzeigedienst dauerhaft stoppen und das Panel leeren. 'on'/'off'" >&2 echo " display off Anzeigedienst dauerhaft stoppen und das Panel leeren. 'on'/'off'" >&2
echo " der Station brauchen das nicht - der Anzeigedienst laeuft" >&2 echo " der Station brauchen das nicht - der Anzeigedienst laeuft" >&2
echo " unabhaengig davon durch und zeichnet den Zustand selbst." >&2 echo " unabhaengig davon durch und zeichnet den Zustand selbst." >&2
@@ -1114,7 +1170,7 @@ case "$_cmd" in
autooff_fire autooff_fire
;; ;;
display) display)
display_cmd "${1:-}" display_cmd "${1:-}" "${2:-}"
;; ;;
off) off)
full_off full_off
+27 -1
View File
@@ -94,6 +94,7 @@ rm -rf "$STAGE_DIR/ninlight-display" "$STAGE_DIR/ninlight-button"
REMOTE REMOTE
"$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \ "$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \
"$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HERE/ninlight-button.service" \ "$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HERE/ninlight-button.service" \
"$HERE/ninlight-poweroff.service" "$HERE/ninlight-reboot.service" \
"$HOST:$STAGE_DIR/" "$HOST:$STAGE_DIR/"
# Das Anzeigeverzeichnis als Ganzes: render.py, display.py, epdconfig.py, die beiden # Das Anzeigeverzeichnis als Ganzes: render.py, display.py, epdconfig.py, die beiden
# uebernommenen Panel-Treiber und HERKUNFT.md (der Herkunftsnachweis gehoert mit auf den # uebernommenen Panel-Treiber und HERKUNFT.md (der Herkunftsnachweis gehoert mit auf den
@@ -115,6 +116,8 @@ BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station}
UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service} UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service}
DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service} DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service}
BUTTON_UNIT_PATH=${NINLIGHT_BUTTON_UNIT:-/etc/systemd/system/ninlight-button.service} BUTTON_UNIT_PATH=${NINLIGHT_BUTTON_UNIT:-/etc/systemd/system/ninlight-button.service}
POWEROFF_UNIT_PATH=${NINLIGHT_POWEROFF_UNIT:-/etc/systemd/system/ninlight-poweroff.service}
REBOOT_UNIT_PATH=${NINLIGHT_REBOOT_UNIT:-/etc/systemd/system/ninlight-reboot.service}
# Die Ablage wird in JEDEM Fall abgeraeumt, auch wenn dieser Block vorzeitig aussteigt # Die Ablage wird in JEDEM Fall abgeraeumt, auch wenn dieser Block vorzeitig aussteigt
# (Task 6b F2, N1). Vorher stand das "rm -rf" nur am Blockende und wurde beim Abbruch # (Task 6b F2, N1). Vorher stand das "rm -rf" nur am Blockende und wurde beim Abbruch
@@ -124,7 +127,8 @@ cleanup_stage() {
rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \ rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \
"$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \ "$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \
"$STAGE_DIR/ninlight-display" "$STAGE_DIR/ninlight-button.service" \ "$STAGE_DIR/ninlight-display" "$STAGE_DIR/ninlight-button.service" \
"$STAGE_DIR/ninlight-button" "$STAGE_DIR/ninlight-button" "$STAGE_DIR/ninlight-poweroff.service" \
"$STAGE_DIR/ninlight-reboot.service"
} }
trap cleanup_stage EXIT trap cleanup_stage EXIT
@@ -215,6 +219,9 @@ for f in "$ROOT_DIR"/button/*; do
fi fi
done done
sudo install -m 0644 "$STAGE_DIR/ninlight-button.service" "$BUTTON_UNIT_PATH" sudo install -m 0644 "$STAGE_DIR/ninlight-button.service" "$BUTTON_UNIT_PATH"
# Abschlussbild beim Herunterfahren und beim Neustart (zwei Units, zwei Texte).
sudo install -m 0644 "$STAGE_DIR/ninlight-poweroff.service" "$POWEROFF_UNIT_PATH"
sudo install -m 0644 "$STAGE_DIR/ninlight-reboot.service" "$REBOOT_UNIT_PATH"
# Das Aufraeumen der Ablage macht der EXIT-Trap oben - auf beiden Wegen aus diesem Block. # Das Aufraeumen der Ablage macht der EXIT-Trap oben - auf beiden Wegen aus diesem Block.
sudo systemctl daemon-reload sudo systemctl daemon-reload
echo " installiert" echo " installiert"
@@ -400,6 +407,25 @@ sudo systemctl restart ninlight-button.service >/dev/null 2>&1 || \
echo " Tasterdienst aktiviert" echo " Tasterdienst aktiviert"
REMOTE REMOTE
echo "== Abschlussbild aktivieren =="
# Zwei Units, zwei Texte: beim Herunterfahren "Strom kann getrennt werden", beim Neustart
# "Strom NICHT trennen!". Ohne sie bliebe auf dem E-Paper fuer immer das letzte Bild
# stehen - bei der Abnahme war das die Rueckfrage des Tastermenues.
#
# Dieselbe weiche Behandlung wie Anzeige und Taster: ein fehlgeschlagenes "enable" ist
# eine Warnung, kein Abbruch. Eine Station, deren Panel nach dem Ausschalten das falsche
# Bild zeigt, ist aergerlich; eine Station, die sich gar nicht einrichten laesst, ist
# unbrauchbar. "enable" allein, kein "start": diese Units tun beim Systemstart NICHTS,
# sie gehoeren zur Abschalt-Transaktion.
"$SSH_BIN" "$HOST" bash -s <<'REMOTE'
set -e
for unit in ninlight-poweroff.service ninlight-reboot.service; do
sudo systemctl enable "$unit" >/dev/null 2>&1 || \
echo " Warnung: 'systemctl enable $unit' fehlgeschlagen - das E-Paper behaelt nach dem Herunterfahren sein letztes Bild" >&2
done
echo " Abschlussbild aktiviert (Herunterfahren und Neustart)"
REMOTE
echo "== NetworkManager-Profile ==" echo "== NetworkManager-Profile =="
# Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf: # Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf:
# - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht # - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht
+113
View File
@@ -129,6 +129,21 @@ case "\$1" in
unit="\${!#}" unit="\${!#}"
if [ "\$unit" = "ninlight-display" ]; then if [ "\$unit" = "ninlight-display" ]; then
code="\${FAKE_DISPLAY_SERVICE_ACTIVE_EXIT:-0}" 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 else
code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}" code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}"
fi fi
@@ -1308,6 +1323,104 @@ ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
set -e 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 "$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 echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden." echo "Alle Tests bestanden."
+31
View File
@@ -53,6 +53,8 @@ setup_sandbox() {
echo "# dummy" >"$sandbox/tools/station/ninlight-station.service" echo "# dummy" >"$sandbox/tools/station/ninlight-station.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-display.service" echo "# dummy" >"$sandbox/tools/station/ninlight-display.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-button.service" echo "# dummy" >"$sandbox/tools/station/ninlight-button.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-poweroff.service"
echo "# dummy" >"$sandbox/tools/station/ninlight-reboot.service"
# display/ wird ECHT kopiert, nicht durch Attrappen ersetzt: die Zusage aus Task 6b # display/ wird ECHT kopiert, nicht durch Attrappen ersetzt: die Zusage aus Task 6b
# ist, dass die beiden uebernommenen Panel-Treiber UND der Herkunftsnachweis # ist, dass die beiden uebernommenen Panel-Treiber UND der Herkunftsnachweis
# HERKUNFT.md mit auf den Pi gehen. Mit selbst erfundenen Dateinamen im Sandbox-Repo # HERKUNFT.md mit auf den Pi gehen. Mit selbst erfundenen Dateinamen im Sandbox-Repo
@@ -185,6 +187,8 @@ run_deploy() {
NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \ NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \
NINLIGHT_DISPLAY_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" \ NINLIGHT_DISPLAY_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-display.service" \
NINLIGHT_BUTTON_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-button.service" \ NINLIGHT_BUTTON_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-button.service" \
NINLIGHT_POWEROFF_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-poweroff.service" \
NINLIGHT_REBOOT_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-reboot.service" \
NINLIGHT_SIGN_DIR="$sign_dir" \ NINLIGHT_SIGN_DIR="$sign_dir" \
"$sandbox/tools/station/deploy.sh" "$@" "$sandbox/tools/station/deploy.sh" "$@"
} }
@@ -1240,6 +1244,33 @@ set -e
&& pass "der Altbestand images/migrate wurde entfernt" \ && pass "der Altbestand images/migrate wurde entfernt" \
|| fail "images/migrate liegt noch da" || fail "images/migrate liegt noch da"
echo
echo "=== Szenario 32: die beiden Abschlussbild-Units werden mit ausgeliefert ==="
# Ohne sie bliebe auf dem E-Paper nach dem Herunterfahren fuer immer das letzte Bild
# stehen - bei der Abnahme war das die Rueckfrage des Tastermenues.
SANDBOX="$TMPROOT/s32"; STUBBIN="$TMPROOT/bin32"; LOG="$TMPROOT/s32.log"; SANDBOX_STATE="$TMPROOT/s32-state"
setup_sandbox "$SANDBOX"
setup_stubbin "$STUBBIN" "$LOG"
mkdir -p "$SANDBOX/build/foo"
echo "foo-image" >"$SANDBOX/build/foo/ninlight_foo_ota.img"
: >"$LOG"
set +e
OUT=$(run_deploy "$SANDBOX" pi-test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
for u in ninlight-poweroff ninlight-reboot; do
[ -f "$SANDBOX_STATE/etc/systemd/system/$u.service" ] \
&& pass "$u.service installiert" || fail "$u.service fehlt - sie waere toter Text im Repo"
done
# Ein Deploy darf keine Unit EINSCHALTEN, die vorher aus war - das macht setup_pi.sh.
grep -Fq "systemctl enable ninlight-poweroff" "$LOG" \
&& fail "der Deploy aktiviert die Abschlussbild-Unit" \
|| pass "der Deploy aktiviert sie nicht (das ist setup_pi.sh's Aufgabe)"
# Und die Ablage ist hinterher leer.
[ ! -e /tmp/ninlight-poweroff.service ] && [ ! -e /tmp/ninlight-reboot.service ] \
&& pass "die Ablage wurde aufgeraeumt" || fail "Unit-Dateien blieben unter /tmp liegen"
echo echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden." echo "Alle Tests bestanden."
+123
View File
@@ -1865,3 +1865,126 @@ def test_the_display_and_the_server_agree_on_the_set_names():
spec.loader.exec_module(server) spec.loader.exec_module(server)
assert display.IMAGESET_DIRS == server.IMAGE_SET_DIRS assert display.IMAGESET_DIRS == server.IMAGE_SET_DIRS
assert display.IMAGESET_DEFAULT == server.DEFAULT_IMAGE_SET assert display.IMAGESET_DEFAULT == server.DEFAULT_IMAGE_SET
# --- Abschlussbildschirm beim Herunterfahren und beim Neustart -----------------------
#
# E-Paper ist bistabil: das zuletzt gezeichnete Bild bleibt fuer immer stehen, auch
# stromlos. Ohne dieses Bild behauptet das Panel nach dem Ausschalten weiter den Zustand
# von vorher - bei der Abnahme stand dort die Rueckfrage des Tastermenues, und der Pi sah
# aus, als warte er noch auf eine Eingabe.
def _final_text(kind):
return " ".join(render.lines({"mode": "final", "final": kind}))
def test_the_shutdown_and_the_reboot_screen_differ():
"""Der wichtigste Unterschied im ganzen Bild: beim Ausschalten DARF man den Stecker
ziehen, beim Neustart NICHT (an diesem Pi hat ein Stromzug schon eine SD-Karte
gekostet)."""
off, reboot = _final_text("poweroff"), _final_text("reboot")
assert off != reboot
assert "AUSGESCHALTET" in off and "NEUSTART" in reboot
# Ausschalten: erlaubt das Trennen ausdruecklich.
assert "kann getrennt werden" in off
# Neustart: verbietet es ebenso ausdruecklich - und sagt nirgends das Gegenteil.
assert "NICHT trennen" in reboot
assert "kann getrennt werden" not in reboot
def test_the_reboot_screen_says_how_long_it_stays():
"""Ein Zero W braucht gut anderthalb Minuten bis zum Anzeigedienst - ohne diesen Satz
sieht das stehende Bild nach "haengt" aus, und genau dann greift jemand zum Stecker."""
assert "min" in _final_text("reboot")
def test_the_final_screen_carries_nothing_else():
"""Kein Bildsatz, keine Fusszeile, kein Zaehler: die Station ist im Begriff zu
verschwinden, und die einzige Auskunft, auf die es jetzt ankommt, ist "Stecker?"."""
state = {"mode": "final", "final": "poweroff", "imageset": "mit-wlan",
"imageset_ok": True, "served": 7}
blocks = render.blocks(state)
assert not any(role == render.ROLE_FOOTER for _, role in blocks)
assert not any("Bildsatz" in text or "Ausgeliefert" in text for text, _ in blocks)
def test_an_unknown_final_kind_claims_neither():
"""Lieber gar nichts behaupten als "ausgeschaltet" zu sagen, wenn gleich wieder
gebootet wird."""
text = " ".join(render.lines({"mode": "final", "final": "was-auch-immer"}))
assert "AUSGESCHALTET" not in text
assert "getrennt" not in text
def test_both_final_screens_are_actually_drawable():
for kind in ("poweroff", "reboot"):
image = render.compose({"mode": "final", "final": kind})
assert image.size == (render.PANEL_WIDTH_PX, render.PANEL_HEIGHT_PX)
assert _ink_box(image) is not None, kind
def test_run_final_draws_fully_and_sleeps(monkeypatch):
"""Ueber die ECHTE display.run_final(): Vollaufbau (kein Teilaufbau - dieses eine Bild
ueberdauert alle anderen und darf keine Reste des vorigen tragen) und danach schlafen
legen, damit das Panel nicht unter Bias in den Ruhezustand geht."""
calls = []
class _Panel:
def __init__(self, revision):
calls.append(("init", revision))
def clear(self):
calls.append(("clear", None))
def draw_full(self, image):
calls.append(("full", image))
def draw_partial(self, image):
calls.append(("partial", image))
return False
def sleep(self):
calls.append(("sleep", None))
monkeypatch.setattr(display, "Panel", _Panel)
display.run_final("V3", "reboot")
kinds = [k for k, _ in calls]
assert kinds == ["init", "clear", "full", "sleep"], kinds
# ... und gezeichnet wurde wirklich das Neustart-Bild, nicht irgendeines.
image = dict((k, v) for k, v in calls)["full"]
expected = render.compose({"mode": "final", "final": "reboot"})
assert list(image.getdata()) == list(expected.getdata())
assert list(image.getdata()) != list(
render.compose({"mode": "final", "final": "poweroff"}).getdata())
def test_display_main_passes_the_final_kind_through(monkeypatch):
"""Die Verdrahtung in display.main() selbst: "--final reboot" muss beim Neustartbild
landen. Ohne diese Zusage liesse sich die Spielart in main() durch eine feste
ersetzen, ohne dass ein Test rot wuerde - und auf dem Panel stuende beim Neustart
"Strom kann getrennt werden"."""
drawn = []
class _Panel:
def __init__(self, revision):
pass
def clear(self):
pass
def draw_full(self, image):
drawn.append(image)
def sleep(self):
pass
monkeypatch.setattr(display, "Panel", _Panel)
monkeypatch.setenv("EPD_REVISION", "V3")
for kind in ("reboot", "poweroff"):
drawn.clear()
monkeypatch.setattr(sys, "argv", ["display.py", "--final", kind])
display.main()
assert len(drawn) == 1
expected = render.compose({"mode": "final", "final": kind})
assert list(drawn[0].getdata()) == list(expected.getdata()), kind
+53
View File
@@ -146,6 +146,13 @@ echo "STUB systemctl \$*" >>"$log"
# ein Szenario NUR ihn scheitern lassen kann, ohne die Anzeige mitzureissen - sonst # ein Szenario NUR ihn scheitern lassen kann, ohne die Anzeige mitzureissen - sonst
# liesse sich nicht zeigen, dass ein fehlgeschlagenes enable/start des Tasters fuer # liesse sich nicht zeigen, dass ein fehlgeschlagenes enable/start des Tasters fuer
# sich genommen keinen Abbruch ausloest. # sich genommen keinen Abbruch ausloest.
# Gezielt EINE Unit scheitern lassen (Abschlussbild): FAKE_ENABLE_FAIL_SUBSTR nennt den
# Teilstring des Unit-Namens, dessen "enable" fehlschlaegt.
if [ -n "\${FAKE_ENABLE_FAIL_SUBSTR:-}" ] && [ "\$1" = "enable" ]; then
case "\$*" in
*"\$FAKE_ENABLE_FAIL_SUBSTR"*) exit 1 ;;
esac
fi
case "\$*" in case "\$*" in
*ninlight-button*) *ninlight-button*)
case "\$1" in case "\$1" in
@@ -217,6 +224,8 @@ run_setup() {
NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \ NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \
NINLIGHT_DISPLAY_UNIT="$state/etc/systemd/system/ninlight-display.service" \ NINLIGHT_DISPLAY_UNIT="$state/etc/systemd/system/ninlight-display.service" \
NINLIGHT_BUTTON_UNIT="$state/etc/systemd/system/ninlight-button.service" \ NINLIGHT_BUTTON_UNIT="$state/etc/systemd/system/ninlight-button.service" \
NINLIGHT_POWEROFF_UNIT="$state/etc/systemd/system/ninlight-poweroff.service" \
NINLIGHT_REBOOT_UNIT="$state/etc/systemd/system/ninlight-reboot.service" \
NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \ NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \
NINLIGHT_BOOT_CONFIG_NEW="$state/boot/firmware/config.txt" \ NINLIGHT_BOOT_CONFIG_NEW="$state/boot/firmware/config.txt" \
NINLIGHT_BOOT_CONFIG_OLD="$state/boot/config.txt" \ NINLIGHT_BOOT_CONFIG_OLD="$state/boot/config.txt" \
@@ -1080,6 +1089,50 @@ for d in migrate-wlan migrate-nocreds; do
|| fail "images/$d wurde leer angelegt" || fail "images/$d wurde leer angelegt"
done done
echo
echo "=== Szenario: der Abschlussbildschirm wird installiert UND aktiviert ==="
# Der Abschlussbildschirm gehoert ins System, nicht in den Tasterdienst: so greift er bei
# jedem Herunterfahren (Menue, "poweroff" per SSH, Neustart) und nicht nur auf einem von
# drei Wegen. Zwei Units, weil sich die Texte unterscheiden muessen.
STATE="$TMPROOT/sfin-state"; STUBBIN="$TMPROOT/binfin"; LOG="$TMPROOT/sfin.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
for u in ninlight-poweroff ninlight-reboot; do
[ -f "$STATE/etc/systemd/system/$u.service" ] \
&& pass "$u.service installiert" || fail "$u.service fehlt"
grep -Fq "systemctl enable $u.service" "$LOG" \
&& pass "$u.service aktiviert" || fail "kein 'systemctl enable $u' im Log"
done
# "enable", aber KEIN "start": diese Units tun beim Systemstart nichts, sie gehoeren zur
# Abschalt-Transaktion. Ein "start" wuerde das Abschlussbild mitten im Betrieb zeichnen.
grep -Fq "systemctl restart ninlight-poweroff" "$LOG" \
&& fail "die Abschlussbild-Unit wird beim Einrichten gestartet - das Bild kaeme sofort" \
|| pass "sie wird nicht gestartet, nur aktiviert"
grep -Fq "systemctl start ninlight-poweroff" "$LOG" \
&& fail "die Abschlussbild-Unit wird beim Einrichten gestartet" \
|| pass "auch kein 'systemctl start'"
echo
echo "=== Szenario: ein fehlgeschlagenes enable des Abschlussbilds warnt nur ==="
# Eine Station, deren Panel nach dem Ausschalten das falsche Bild zeigt, ist aergerlich;
# eine, die sich gar nicht einrichten laesst, ist unbrauchbar.
STATE="$TMPROOT/sfin2-state"; STUBBIN="$TMPROOT/binfin2"; LOG="$TMPROOT/sfin2.log"
setup_state "$STATE"
setup_stubbin "$STUBBIN" "$LOG"
: >"$LOG"
set +e
OUT=$(FAKE_ENABLE_FAIL_SUBSTR=ninlight-poweroff run_setup "$STATE" "$STUBBIN" 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlgeschlagenem enable" || fail "Exit-Code $RC: $OUT"
echo "$OUT" | grep -qi "warnung" && pass "Warnung in der Ausgabe" || fail "keine Warnung: $OUT"
echo echo
if [ "$FAILED" -eq 0 ]; then if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden." echo "Alle Tests bestanden."