From 4e1a965cbb448020e25ef660109ac93a143f9d30 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Tue, 15 Sep 2026 03:02:21 +0200 Subject: [PATCH] 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 " 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. --- tools/station/README.md | 32 ++++++ tools/station/deploy.sh | 10 +- tools/station/display/display.py | 23 +++++ tools/station/display/render.py | 31 ++++++ tools/station/ninlight-poweroff.service | 42 ++++++++ tools/station/ninlight-reboot.service | 30 ++++++ tools/station/ninlight-station | 60 +++++++++++- tools/station/setup_pi.sh | 28 +++++- tools/station/tests/test_cli.sh | 113 ++++++++++++++++++++++ tools/station/tests/test_deploy.sh | 31 ++++++ tools/station/tests/test_render.py | 123 ++++++++++++++++++++++++ tools/station/tests/test_setup_pi.sh | 53 ++++++++++ 12 files changed, 572 insertions(+), 4 deletions(-) create mode 100644 tools/station/ninlight-poweroff.service create mode 100644 tools/station/ninlight-reboot.service diff --git a/tools/station/README.md b/tools/station/README.md index b046325..5670bd8 100644 --- a/tools/station/README.md +++ b/tools/station/README.md @@ -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 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 ` 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 Build-Parametern `NINLIGHT_RECOVERY_SSID` und `NINLIGHT_RECOVERY_PASSWORD` aus `config.local.cmake` passen. Weicht eines ab, findet die Lampe die Station nicht. diff --git a/tools/station/deploy.sh b/tools/station/deploy.sh index 46f4148..ee64044 100755 --- a/tools/station/deploy.sh +++ b/tools/station/deploy.sh @@ -255,6 +255,7 @@ echo "== uebertragen ==" 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" \ "$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HERE/ninlight-button.service" \ + "$HERE/ninlight-poweroff.service" "$HERE/ninlight-reboot.service" \ "$HOST:/tmp/" # Das Anzeigeverzeichnis als Ganzes (render.py, display.py, epdconfig.py, die beiden # 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} DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.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" 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 rsync -a -c --delete /tmp/ninlight-button/ "$ROOT_DIR/button/" 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 \ - /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 if systemctl is-active --quiet ninlight-station; then if sudo systemctl restart ninlight-station; then diff --git a/tools/station/display/display.py b/tools/station/display/display.py index fb1e531..dd4f66c 100644 --- a/tools/station/display/display.py +++ b/tools/station/display/display.py @@ -560,6 +560,24 @@ def run_test(revision: str) -> None: 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 " 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: panel = Panel(revision) panel.clear() @@ -634,6 +652,9 @@ def main() -> None: group = parser.add_mutually_exclusive_group() 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("--final", choices=sorted(render.FINAL_LABELS), + help="Abschlussbild zeichnen und beenden (beim Herunterfahren " + "bzw. Neustart; die Texte unterscheiden sich)") args = parser.parse_args() revision = os.environ.get("EPD_REVISION", "V3") @@ -643,6 +664,8 @@ def main() -> None: run_test(revision) elif args.clear: run_clear(revision) + elif args.final: + run_final(revision, args.final) else: run_daemon(revision) except (DisplayUnavailable, ImportError) as e: diff --git a/tools/station/display/render.py b/tools/station/display/render.py index b123c07..7323add 100644 --- a/tools/station/display/render.py +++ b/tools/station/display/render.py @@ -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. diff --git a/tools/station/ninlight-poweroff.service b/tools/station/ninlight-poweroff.service new file mode 100644 index 0000000..9df449a --- /dev/null +++ b/tools/station/ninlight-poweroff.service @@ -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 diff --git a/tools/station/ninlight-reboot.service b/tools/station/ninlight-reboot.service new file mode 100644 index 0000000..d6ed04e --- /dev/null +++ b/tools/station/ninlight-reboot.service @@ -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 diff --git a/tools/station/ninlight-station b/tools/station/ninlight-station index 1b47493..490a5ea 100755 --- a/tools/station/ninlight-station +++ b/tools/station/ninlight-station @@ -674,7 +674,11 @@ migrate_on() { # an den Python-Prozess durch. "sudo", weil /dev/spidev0.0 und die GPIO-Leitungen root # gehoeren (der Anzeigedienst laeuft aus demselben Grund als root). 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 @@ -779,6 +783,44 @@ display_off() { 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() { # 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 @@ -1028,6 +1070,12 @@ display_cmd() { test) display_handover --test ;; clear) display_handover --clear ;; 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 ;; esac } @@ -1061,6 +1109,14 @@ usage() { echo " Ein laufender Anzeigedienst wird dafuer kurz gestoppt und" >&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 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 " der Station brauchen das nicht - der Anzeigedienst laeuft" >&2 echo " unabhaengig davon durch und zeichnet den Zustand selbst." >&2 @@ -1114,7 +1170,7 @@ case "$_cmd" in autooff_fire ;; display) - display_cmd "${1:-}" + display_cmd "${1:-}" "${2:-}" ;; off) full_off diff --git a/tools/station/setup_pi.sh b/tools/station/setup_pi.sh index c46cdb4..b568be0 100755 --- a/tools/station/setup_pi.sh +++ b/tools/station/setup_pi.sh @@ -94,6 +94,7 @@ rm -rf "$STAGE_DIR/ninlight-display" "$STAGE_DIR/ninlight-button" REMOTE "$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-poweroff.service" "$HERE/ninlight-reboot.service" \ "$HOST:$STAGE_DIR/" # Das Anzeigeverzeichnis als Ganzes: render.py, display.py, epdconfig.py, die beiden # 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} DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.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 # (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" \ "$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.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 @@ -215,6 +219,9 @@ for f in "$ROOT_DIR"/button/*; do fi done 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. sudo systemctl daemon-reload echo " installiert" @@ -400,6 +407,25 @@ sudo systemctl restart ninlight-button.service >/dev/null 2>&1 || \ echo " Tasterdienst aktiviert" 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 ==" # Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf: # - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht diff --git a/tools/station/tests/test_cli.sh b/tools/station/tests/test_cli.sh index aa802f6..b7c9977 100755 --- a/tools/station/tests/test_cli.sh +++ b/tools/station/tests/test_cli.sh @@ -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." diff --git a/tools/station/tests/test_deploy.sh b/tools/station/tests/test_deploy.sh index ce5722b..c5fc25d 100755 --- a/tools/station/tests/test_deploy.sh +++ b/tools/station/tests/test_deploy.sh @@ -53,6 +53,8 @@ setup_sandbox() { echo "# dummy" >"$sandbox/tools/station/ninlight-station.service" echo "# dummy" >"$sandbox/tools/station/ninlight-display.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 # ist, dass die beiden uebernommenen Panel-Treiber UND der Herkunftsnachweis # 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_DISPLAY_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-display.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" \ "$sandbox/tools/station/deploy.sh" "$@" } @@ -1240,6 +1244,33 @@ set -e && pass "der Altbestand images/migrate wurde entfernt" \ || 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 if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden." diff --git a/tools/station/tests/test_render.py b/tools/station/tests/test_render.py index 0294222..3146573 100644 --- a/tools/station/tests/test_render.py +++ b/tools/station/tests/test_render.py @@ -1865,3 +1865,126 @@ def test_the_display_and_the_server_agree_on_the_set_names(): spec.loader.exec_module(server) assert display.IMAGESET_DIRS == server.IMAGE_SET_DIRS 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 diff --git a/tools/station/tests/test_setup_pi.sh b/tools/station/tests/test_setup_pi.sh index cc877fc..cd9e109 100755 --- a/tools/station/tests/test_setup_pi.sh +++ b/tools/station/tests/test_setup_pi.sh @@ -146,6 +146,13 @@ echo "STUB systemctl \$*" >>"$log" # ein Szenario NUR ihn scheitern lassen kann, ohne die Anzeige mitzureissen - sonst # liesse sich nicht zeigen, dass ein fehlgeschlagenes enable/start des Tasters fuer # 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 *ninlight-button*) case "\$1" in @@ -217,6 +224,8 @@ run_setup() { NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \ NINLIGHT_DISPLAY_UNIT="$state/etc/systemd/system/ninlight-display.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_BOOT_CONFIG_NEW="$state/boot/firmware/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" 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 if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden."