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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user