fix(station): V2/V3-Panelkapselung, wiedereintrittsfaehiges module_init, Unit-Fixes

Fixrunde 1 zu Task 6a: epd2in13_V2.init() braucht ein Pflichtargument und dessen
displayPartial() setzt selbst keine LUT (neue Panel-Klasse in display.py kapselt das statt
es zu verstreuen); epdconfig.module_init() war nicht wiedereintrittsfaehig und ein zu
breites except tarnte GPIOPinInUse als "Panel fehlt" (jetzt idempotent, gezielter
except); ninlight-display.service hatte ReadOnlyPaths=/run/ninlight-station (verhindert
den Start, wenn die Station regulaer aus ist), StartLimit* in der falschen Sektion und
SupplementaryGroups=spi gpio (harter Fehler auf reinem Debian ohne diese Gruppen); ein
fehlendes Pillow fuehrte zu ModuleNotFoundError statt zum vorgesehenen stillen Exit 0.
Dazu: Panel schlaeft nach jedem Zeichnen (Alterung), Modus-Vorrang liegt jetzt bei
RUN_DIR/mode statt bei status.json, export-Praefix in der Konfigurationsdatei wird
erkannt, MAX_COLS auf das mit Pillow nachgemessene Mass korrigiert, und build_state()
hat jetzt eigene Tests.
This commit is contained in:
Michael Sauer
2026-09-13 21:07:01 +02:00
parent 4a029a5d1c
commit 9b158c8c4f
5 changed files with 243 additions and 72 deletions
+24 -3
View File
@@ -90,10 +90,23 @@ def spi_writebyte2(data) -> None:
def module_init() -> int:
"""Oeffnet GPIO-Pins und SPI-Geraet. Wirft DisplayUnavailable statt irgendeiner
Bibliotheksausnahme, damit display.py genau eine Ausnahmeklasse abfangen muss."""
"""Oeffnet GPIO-Pins und SPI-Geraet. Wirft DisplayUnavailable statt einer rohen
Bibliotheksausnahme, damit display.py genau eine Ausnahmeklasse abfangen muss.
Idempotent: ein zweiter Aufruf, waehrend die Pins/das SPI-Geraet noch offen sind (die
Panel-Treiber rufen dies bei JEDEM epd.init() erneut auf - Vollaufbau bei Moduswechsel
oder alle 30 Minuten loest also ohne diese Absicherung einen zweiten, unnoetigen
Oeffnungsversuch aus), kehrt sofort zurueck statt gpiozero.DigitalOutputDevice(17)
ein zweites Mal anzulegen. Das wuerde mit GPIOPinInUse scheitern - vom breiten
except unten (Fixrunde 1, C2) frueher faelschlich als "Panel fehlt" verschluckt,
obwohl es ein Programmierfehler in der Aufrufreihenfolge ist. Nach module_exit()
(z.B. durch epd.sleep()) ist _spi wieder None, ein echter Neuaufbau laeuft dann
normal."""
global _rst_pin, _dc_pin, _busy_pin, _spi
if _spi is not None:
return 0
if gpiozero is None or spidev is None:
raise DisplayUnavailable("gpiozero/spidev nicht installiert")
@@ -101,6 +114,14 @@ 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).
hardware_errors: tuple[type[BaseException], ...] = (OSError, ImportError)
if gpiozero is not None:
hardware_errors = hardware_errors + (gpiozero.GPIODeviceError,)
try:
_rst_pin = gpiozero.DigitalOutputDevice(RST_PIN)
_dc_pin = gpiozero.DigitalOutputDevice(DC_PIN)
@@ -110,7 +131,7 @@ def module_init() -> int:
_spi.open(_SPI_BUS, _SPI_DEVICE)
_spi.max_speed_hz = _SPI_HZ
_spi.mode = 0b00
except Exception as e: # noqa: BLE001 - alles hier wird zu DisplayUnavailable
except hardware_errors as e:
module_exit()
raise DisplayUnavailable(f"GPIO/SPI-Initialisierung fehlgeschlagen: {e}") from e