feat(display): grosse Schrift, invertierter Kopf, GPIO-Uebergabe

Zwei Befunde der Hardware-Abnahme.

1) Die Schrift war zu klein: compose() zeichnete alles in
ImageFont.load_default() (rund 10 px) - auf 250x122 aus zwei Metern
nicht lesbar. blocks() vergibt jetzt Rollen (header/hero/detail/big),
compose() setzt jede Rolle eigen: Modus fett und weiss auf schwarzem
Balken, die wichtigste Zeile gross (Migration: erkannte Lampe,
Rettung: SSID), Beiwerk klein, "Station aus" gross und mittig.

Die Schrift kommt per ImageFont.truetype() aus FONT_SEARCH_PATHS mit
DejaVu (/usr/share/fonts/truetype/dejavu/) als Hauptweg - auf beiden
Pis vorhanden. load_default(size=...) scheidet aus: das gibt es erst
ab Pillow 10.1, Pi 2 hat 9.4.0. Letzter Rueckfall bleibt
load_default() ohne Argument, damit eine fehlende Schrift die Anzeige
(und damit nie eine Migration) aufhaelt. lines() bleibt ohne Pillow
importierbar, die Breitenkuerzung misst weiter in Pixeln.

2) "display test" scheiterte bei laufendem Anzeigedienst mit einem
rohen "lgpio.error: 'GPIO busy'": lgpio.error ist keine
gpiozero.GPIOZeroError und fiel durch die Ausnahmeliste in
epdconfig.module_init(). Sie ist jetzt aufgefuehrt - weiterhin
gezielt, TypeError/AttributeError/NameError bleiben Programmierfehler -
und die Meldung nennt bei belegten Leitungen den Anzeigedienst.
"display test|clear" stoppt den Dienst fuer die Dauer des Handgriffs
und startet ihn danach wieder, aber nur, wenn er vorher lief.
This commit is contained in:
Michael Sauer
2026-09-14 02:24:14 +02:00
parent f138e88f4e
commit bb39105ac4
5 changed files with 667 additions and 82 deletions
+81 -21
View File
@@ -123,12 +123,21 @@ RUN_DIR="$rundir"
UNIT_FILE="$unitfile"
case "\$1" in
is-active)
if [ "\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}" -eq 0 ]; then
# Getrennt schaltbar je Unit: der Handbetrieb am Panel fragt nach ninlight-display
# (laeuft der Anzeigedienst?), alles andere nach ninlight-station. Die Unit ist
# immer das letzte Argument ("is-active --quiet <unit>" bzw. "is-active <unit>").
unit="\${!#}"
if [ "\$unit" = "ninlight-display" ]; then
code="\${FAKE_DISPLAY_SERVICE_ACTIVE_EXIT:-0}"
else
code="\${FAKE_SYSTEMCTL_ACTIVE_EXIT:-0}"
fi
if [ "\$code" -eq 0 ]; then
echo active
exit 0
else
echo inactive
exit "\${FAKE_SYSTEMCTL_ACTIVE_EXIT}"
exit "\$code"
fi
;;
restart|stop)
@@ -137,15 +146,14 @@ case "\$1" in
# RuntimeDirectory bei JEDEM Stop/Restart, nicht nur beim allerersten Start,
# und legt es leer wieder an.
#
# ACHTUNG, Stub-Erfindung (Task 6b F1): die Regel wird hier auf JEDE
# Unit angewandt, also auch auf ninlight-display - die traegt aber gar kein
# RuntimeDirectory=, ein "stop ninlight-display" raeumt in Wirklichkeit nichts
# weg. \$UNIT_FILE ist immer ninlight-station.service, unabhaengig davon, welche
# Unit im Aufruf steht. Heute folgenlos (kein Testfall stoppt die Anzeige und
# erwartet danach noch Dateien in RUN_DIR), aber niemand darf sich darauf
# verlassen: wer kuenftig prueft, was ein "display off" mit RUN_DIR macht, muss
# den Stub vorher nach Unit unterscheiden lassen.
if ! grep -q '^RuntimeDirectoryPreserve=restart' "\$UNIT_FILE" 2>/dev/null; then
# Die Regel gilt nur fuer ninlight-station - \$UNIT_FILE ist deren Unit-Datei.
# ninlight-display traegt gar kein RuntimeDirectory=, ein "stop ninlight-display"
# raeumt in Wirklichkeit nichts weg (frueher tat der Stub das trotzdem fuer JEDE
# Unit; harmlos, solange kein Testfall die Anzeige stoppt und danach noch Dateien
# in RUN_DIR erwartet - genau das tun 'display test'/'display clear' jetzt aber,
# die den Anzeigedienst waehrend des Handgriffs stoppen).
if [ "\$2" = "ninlight-station" ] \
&& ! grep -q '^RuntimeDirectoryPreserve=restart' "\$UNIT_FILE" 2>/dev/null; then
rm -rf "\$RUN_DIR"
mkdir -p "\$RUN_DIR"
fi
@@ -274,8 +282,8 @@ OUT=$(run_cli "$SBX" migrate on 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
DOWN_LINE=$(grep -n "nmcli con down ninlight-recovery" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
UP_LINE=$(grep -n "nmcli con up qconnex-firmware" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
DOWN_LINE=$(grep -n "nmcli con down ninlight-recovery" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
UP_LINE=$(grep -n "nmcli con up qconnex-firmware" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
if [ -n "$DOWN_LINE" ] && [ -n "$UP_LINE" ] && [ "$DOWN_LINE" -lt "$UP_LINE" ]; then
pass "ninlight-recovery wird VOR qconnex-firmware heruntergefahren"
else
@@ -595,9 +603,25 @@ grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "display.py wurde mit -
# er ist wertlos, wenn er nicht die Revision aus der Konfiguration durchreicht:
# EPD_REVISION wird per ". $CONF" nur als Shell-Variable gesetzt, nicht exportiert.
grep -Fq "STUB display.py --test (EPD_REVISION=V3)" "$SBX/cmd.log" && pass "EPD_REVISION aus der Konfiguration erreicht den Python-Prozess" || fail "EPD_REVISION kam nicht durch: $(cat "$SBX/cmd.log")"
# Nicht gleichzeitig den Anzeigedienst umbringen: 'display test' ist ein Handgriff bei
# der Abnahme, kein Ausschalten.
grep -Fq "systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'display test' stoppt den Anzeigedienst - das soll nur 'display off'/'off'" || pass "'display test' laesst den Anzeigedienst in Ruhe"
# Der Anzeigedienst haelt die GPIO-Leitungen; ohne ihn kurz zu stoppen scheitert der
# Handgriff am Geraet mit "lgpio.error: 'GPIO busy'" (Hardware-Abnahme). Frueher stand
# hier die umgekehrte Zusage ("laesst den Anzeigedienst in Ruhe") - die war genau der
# Grund, warum der erste Handgriff der Abnahme nicht funktionierte.
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
TEST_LINE=$(grep -n "STUB display.py --test" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
START_LINE=$(grep -n "STUB systemctl start ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
if [ -n "$STOP_LINE" ] && [ -n "$TEST_LINE" ] && [ "$STOP_LINE" -lt "$TEST_LINE" ]; then
pass "der Anzeigedienst wird gestoppt, BEVOR display.py die GPIO-Leitungen anfasst"
else
fail "Anzeigedienst nicht (rechtzeitig) gestoppt (stop=$STOP_LINE, test=$TEST_LINE): $(cat "$SBX/cmd.log")"
fi
# Und danach wieder an: ein Abnahme-Handgriff darf die Station nicht dauerhaft ohne
# Anzeige zuruecklassen.
if [ -n "$START_LINE" ] && [ -n "$TEST_LINE" ] && [ "$TEST_LINE" -lt "$START_LINE" ]; then
pass "der Anzeigedienst laeuft danach wieder (start=$START_LINE)"
else
fail "Anzeigedienst wurde nicht wieder gestartet (test=$TEST_LINE, start=$START_LINE): $(cat "$SBX/cmd.log")"
fi
echo
echo "=== Fall D2: 'display clear' ruft display.py mit --clear auf ==="
@@ -611,6 +635,42 @@ set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "display.py wurde mit --clear aufgerufen" || fail "kein '--clear'-Aufruf im Log: $(cat "$SBX/cmd.log")"
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && fail "'display clear' hat --test aufgerufen" || pass "kein --test bei 'display clear'"
# Dieselbe GPIO-Kollision wie bei 'display test' - auch 'display clear' braucht die
# Leitungen fuer sich.
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'display clear' stoppt den Anzeigedienst fuer die Dauer" || fail "'display clear' laesst den Anzeigedienst laufen - das gibt 'GPIO busy': $(cat "$SBX/cmd.log")"
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "'display clear' startet den Anzeigedienst danach wieder" || fail "'display clear' laesst den Anzeigedienst aus: $(cat "$SBX/cmd.log")"
echo
echo "=== Fall D2a: war der Anzeigedienst aus, bleibt er nach 'display test' aus ==="
# Sonst haette ein Abnahme-Handgriff die Station nebenbei umgeschaltet: nach
# 'ninlight-station off' ist der Anzeigedienst bewusst gestoppt, und 'display test'
# darf ihn nicht wieder hochziehen.
SBX="$TMPROOT/tD2a"
setup_sandbox "$SBX"
: >"$SBX/cmd.log"
set +e
OUT=$(FAKE_DISPLAY_SERVICE_ACTIVE_EXIT=3 run_cli "$SBX" display test 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "STUB display.py --test" "$SBX/cmd.log" && pass "Testbild wurde trotzdem gezeichnet" || fail "kein '--test'-Aufruf: $(cat "$SBX/cmd.log")"
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && fail "'display test' hat den Anzeigedienst eingeschaltet, obwohl er aus war" || pass "kein 'start' fuer einen Dienst, der gar nicht lief"
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'display test' stoppt einen Dienst, der gar nicht laeuft" || pass "kein ueberfluessiges 'stop'"
echo
echo "=== Fall D2b: scheitert das Panel, meldet 'display test' das - der Dienst laeuft trotzdem wieder ==="
# Der Rueckgabewert von display.py ist das Ergebnis des Handgriffs; ein erfolgreiches
# Wiederanfahren des Dienstes darf ihn nicht in eine Erfolgsmeldung verwandeln. Und
# umgekehrt darf ein gescheitertes Testbild den Dienst nicht dauerhaft aus lassen.
SBX="$TMPROOT/tD2b"
setup_sandbox "$SBX"
: >"$SBX/cmd.log"
set +e
OUT=$(FAKE_DISPLAY_EXIT=1 run_cli "$SBX" display test 2>&1)
RC=$?
set -e
[ "$RC" -eq 1 ] && pass "Exit-Code 1 wird durchgereicht" || fail "Exit-Code $RC statt 1: $OUT"
grep -Fq "STUB systemctl start ninlight-display" "$SBX/cmd.log" && pass "Anzeigedienst laeuft trotz Panel-Fehler wieder" || fail "Anzeigedienst blieb nach dem Fehler aus: $(cat "$SBX/cmd.log")"
echo
echo "=== Fall D3 (Task 6b, Punkt 4): 'display off' stoppt den Dienst VOR dem Leeren des Panels ==="
@@ -624,8 +684,8 @@ OUT=$(run_cli "$SBX" display off 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
if [ -n "$STOP_LINE" ] && [ -n "$CLEAR_LINE" ] && [ "$STOP_LINE" -lt "$CLEAR_LINE" ]; then
pass "Anzeigedienst wird gestoppt, BEVOR das Panel geleert wird (stop=$STOP_LINE, clear=$CLEAR_LINE)"
else
@@ -649,8 +709,8 @@ set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -Fq "STUB systemctl stop ninlight-display" "$SBX/cmd.log" && pass "'off' stoppt den Anzeigedienst" || fail "'off' stoppt den Anzeigedienst nicht: $(cat "$SBX/cmd.log")"
grep -Fq "STUB display.py --clear" "$SBX/cmd.log" && pass "'off' leert das Panel" || fail "'off' leert das Panel nicht: $(cat "$SBX/cmd.log")"
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
STOP_LINE=$(grep -n "STUB systemctl stop ninlight-display" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
CLEAR_LINE=$(grep -n "STUB display.py --clear" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
if [ -n "$STOP_LINE" ] && [ -n "$CLEAR_LINE" ] && [ "$STOP_LINE" -lt "$CLEAR_LINE" ]; then
pass "auch bei 'off': erst stoppen, dann leeren"
else
@@ -658,7 +718,7 @@ else
fi
# Das Panel muss geleert sein, BEVOR ap_switch das Ergebnis von "off" verkuenden kann -
# und vor allem, bevor ap_switch bei haengendem NetworkManager abbricht (Fall D6).
UP_LINE=$(grep -n "nmcli con up home-wifi" "$SBX/cmd.log" | head -n1 | cut -d: -f1)
UP_LINE=$(grep -n "nmcli con up home-wifi" "$SBX/cmd.log" | head -n1 | cut -d: -f1 || true)
if [ -n "$UP_LINE" ] && [ "$CLEAR_LINE" -lt "$UP_LINE" ]; then
pass "Panel ist geleert, bevor wlan0 ins Heim-WLAN zurueckgeschaltet wird"
else