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:
@@ -35,6 +35,15 @@ try:
|
||||
except ImportError: # pragma: no cover - dito
|
||||
spidev = None
|
||||
|
||||
# lgpio ist die Pin-Factory, die gpiozero auf beiden Pis benutzt (Debian 13 trixie wie
|
||||
# Bookworm). Sie meldet einen belegten Pin nicht als gpiozero-Ausnahme, sondern als
|
||||
# lgpio.error - siehe _hardware_error_types(). Auf der Entwicklungsmaschine fehlt das
|
||||
# Modul; dann bleibt es None und die Fehlerliste kommt ohne es aus.
|
||||
try:
|
||||
import lgpio
|
||||
except ImportError: # pragma: no cover - auf dem Pi immer vorhanden
|
||||
lgpio = None
|
||||
|
||||
|
||||
class DisplayUnavailable(Exception):
|
||||
"""Kein Panel ansprechbar: Bibliothek fehlt, /dev/spidev0.0 fehlt, oder der
|
||||
@@ -89,6 +98,52 @@ def spi_writebyte2(data) -> None:
|
||||
_spi.writebytes2(data)
|
||||
|
||||
|
||||
def _busy_hint(error: BaseException) -> str:
|
||||
"""Zusatz fuer die Fehlermeldung, wenn die Leitungen belegt sind. "GPIO busy" heisst
|
||||
auf dieser Station praktisch immer "der Anzeigedienst haelt die Pins" - und trifft
|
||||
ausgerechnet den Befehl, den der Hilfetext als ersten Handgriff der Abnahme nennt.
|
||||
Die Meldung soll den Bediener also nicht zum Suchen schicken, sondern sagen, was los
|
||||
ist. Leer, wenn es nicht danach aussieht - ein erfundener Hinweis waere schlimmer als
|
||||
keiner."""
|
||||
if "busy" in str(error).lower():
|
||||
return (" - haelt der Anzeigedienst die Leitungen? "
|
||||
"Pruefen mit 'systemctl status ninlight-display'")
|
||||
return ""
|
||||
|
||||
|
||||
def _hardware_error_types() -> tuple:
|
||||
"""Die Ausnahmen, die als "keine/blockierte Hardware" gelten und damit zu
|
||||
DisplayUnavailable werden. Bewusst eine ENUMERATION, kein `except Exception`: ein
|
||||
echter Programmierfehler an anderer Stelle (TypeError, AttributeError, NameError)
|
||||
soll weiterhin als das erkennbar bleiben, was er ist, statt hier als "Panel fehlt"
|
||||
getarnt zu werden (Fixrunde 1, C2, zweiter Teil).
|
||||
|
||||
gpiozero.GPIOZeroError (nicht nur GPIODeviceError, Fixrunde 2, N4): die PinError-
|
||||
Familie (PinInvalidPin, PinUnsupported, ... - z.B. bei falscher/fehlender
|
||||
Pin-Factory-Zuordnung) haengt direkt unter GPIOZeroError, nicht unter
|
||||
GPIODeviceError - per `gpiozero.PinInvalidPin.__mro__` nachgeprueft. BadPinFactory
|
||||
erbt zusaetzlich von ImportError und war dadurch schon abgedeckt.
|
||||
|
||||
lgpio.error (Hardware-Abnahme): laeuft der Anzeigedienst, haelt er die GPIO-Leitungen,
|
||||
und ein gleichzeitiges "ninlight-station display test" bekommt beim Anlegen des ersten
|
||||
Pins ein `lgpio.error: 'GPIO busy'`. Das ist KEINE gpiozero.GPIOZeroError - gpiozero
|
||||
reicht die Ausnahme seiner Pin-Factory unveraendert durch - und fiel deshalb durch
|
||||
diese Liste hindurch: statt der vorgesehenen einen Protokollzeile gab es einen rohen
|
||||
Stapelabzug und rc=1. lgpio.error erbt direkt von Exception, die drei
|
||||
Programmierfehler oben bleiben davon also unberuehrt.
|
||||
|
||||
Wird bei JEDEM Aufruf neu gebaut statt einmal beim Import: nur so wirkt ein
|
||||
monkeypatch.setattr(epdconfig, "lgpio", ...) im Test ueberhaupt (dasselbe Muster wie
|
||||
bei gpiozero/spidev in tools/station/tests/test_render.py)."""
|
||||
types: tuple = (OSError, ImportError)
|
||||
if gpiozero is not None:
|
||||
types = types + (gpiozero.GPIOZeroError,)
|
||||
lgpio_error = getattr(lgpio, "error", None) if lgpio is not None else None
|
||||
if isinstance(lgpio_error, type) and issubclass(lgpio_error, BaseException):
|
||||
types = types + (lgpio_error,)
|
||||
return types
|
||||
|
||||
|
||||
def module_init() -> int:
|
||||
"""Oeffnet GPIO-Pins und SPI-Geraet. Wirft DisplayUnavailable statt einer rohen
|
||||
Bibliotheksausnahme, damit display.py genau eine Ausnahmeklasse abfangen muss.
|
||||
@@ -114,18 +169,7 @@ def module_init() -> int:
|
||||
if not os.path.exists(spi_path):
|
||||
raise DisplayUnavailable(f"{spi_path} fehlt - ist SPI aktiviert (raspi-config)?")
|
||||
|
||||
# Nur Fehler, die tatsaechlich von einer fehlenden/blockierten Hardware kommen, werden
|
||||
# zu DisplayUnavailable - alles andere (ein echter Programmierfehler an anderer
|
||||
# Stelle) soll weiterhin als das erkennbar bleiben, was es ist, statt hier als
|
||||
# "Panel fehlt" getarnt zu werden (Fixrunde 1, C2, zweiter Teil). gpiozero.GPIOZeroError
|
||||
# (nicht nur GPIODeviceError, Fixrunde 2, N4): die PinError-Familie (PinInvalidPin,
|
||||
# PinUnsupported, ... - z.B. bei falscher/fehlender Pin-Factory-Zuordnung) haengt
|
||||
# direkt unter GPIOZeroError, nicht unter GPIODeviceError - per
|
||||
# `gpiozero.PinInvalidPin.__mro__` nachgeprueft. BadPinFactory erbt zusaetzlich von
|
||||
# ImportError und war dadurch schon abgedeckt.
|
||||
hardware_errors: tuple[type[BaseException], ...] = (OSError, ImportError)
|
||||
if gpiozero is not None:
|
||||
hardware_errors = hardware_errors + (gpiozero.GPIOZeroError,)
|
||||
hardware_errors = _hardware_error_types()
|
||||
|
||||
try:
|
||||
_rst_pin = gpiozero.DigitalOutputDevice(RST_PIN)
|
||||
@@ -138,7 +182,8 @@ def module_init() -> int:
|
||||
_spi.mode = 0b00
|
||||
except hardware_errors as e:
|
||||
module_exit()
|
||||
raise DisplayUnavailable(f"GPIO/SPI-Initialisierung fehlgeschlagen: {e}") from e
|
||||
raise DisplayUnavailable(
|
||||
f"GPIO/SPI-Initialisierung fehlgeschlagen: {e}{_busy_hint(e)}") from e
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user