Files
ninlight-w600/tools/station/display/epdconfig.py
T
Michael Sauer 9b158c8c4f 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.
2026-09-13 21:07:01 +02:00

161 lines
5.8 KiB
Python

"""GPIO/SPI-Unterbau fuer die vendored Waveshare-Panel-Treiber (epd2in13_V2.py,
epd2in13_V3.py), geschrieben gegen gpiozero + spidev statt gegen Waveshares eigenes
epdconfig.py, das RPi.GPIO voraussetzt - ein Modul, das auf Debian 13 (trixie) mit
Python 3.13 nicht mehr baut/importiert. gpiozero, lgpio (gpiozero's Pin-Factory auf
trixie) und spidev sind auf dem Ziel-Pi vorhanden.
Stellt genau die Schnittstelle bereit, die die beiden Treiber erwarten: die Pin-Konstanten
RST_PIN/DC_PIN/CS_PIN/BUSY_PIN sowie digital_write, digital_read, delay_ms, spi_writebyte,
spi_writebyte2, module_init und module_exit als Modulfunktionen (kein Klassen-Wrapper -
die Treiber greifen direkt auf `epdconfig.<name>` zu).
CS_PIN wird NICHT als GPIO geschaltet: spidev.SpiDev(0, 0) steuert die Hardware-CS0-Leitung
selbst bei jeder Übertragung, ein zusaetzliches Software-Toggle wuerde nur mit der Hardware
um die Leitung konkurrieren. digital_write()/digital_read() sind fuer CS_PIN deshalb
No-ops - genau das Muster, das Waveshares eigene, neuere epdconfig.py-Fassung fuer
Raspberry Pi inzwischen ebenfalls verwendet (dort ist die GPIO-Instanziierung fuer CS_PIN
auskommentiert).
Ein fehlender Import (gpiozero/spidev nicht installiert) oder ein fehlendes
/dev/spidev0.0 wirft DisplayUnavailable - display.py faengt das ab, protokolliert eine
Zeile und beendet sich sauber (sys.exit(0)), statt dass ein fehlendes Panel eine Migration
verhindern koennte (siehe Modul-Docstring von display.py)."""
from __future__ import annotations
import os
import time
try:
import gpiozero
except ImportError: # pragma: no cover - auf dem Pi immer vorhanden, hier nur defensiv
gpiozero = None
try:
import spidev
except ImportError: # pragma: no cover - dito
spidev = None
class DisplayUnavailable(Exception):
"""Kein Panel ansprechbar: Bibliothek fehlt, /dev/spidev0.0 fehlt, oder der
Hardwarezugriff selbst ist fehlgeschlagen (z.B. Berechtigung, Pin belegt)."""
# Dieselben Pin-Nummern wie in Waveshares eigenem epdconfig.py fuer den Raspberry Pi.
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
_SPI_BUS = 0
_SPI_DEVICE = 0
_SPI_HZ = 4_000_000
_rst_pin = None
_dc_pin = None
_busy_pin = None
_spi = None
def digital_write(pin: int, value: int) -> None:
if pin == RST_PIN:
_rst_pin.value = bool(value)
elif pin == DC_PIN:
_dc_pin.value = bool(value)
elif pin == CS_PIN:
pass # Hardware-CS ueber spidev, siehe Modul-Docstring.
# Andere Pins fragen die Treiber nicht ab - kein else-Zweig noetig.
def digital_read(pin: int) -> int:
if pin == BUSY_PIN:
return int(bool(_busy_pin.value))
if pin == RST_PIN:
return int(bool(_rst_pin.value))
if pin == DC_PIN:
return int(bool(_dc_pin.value))
return 0
def delay_ms(ms: int) -> None:
time.sleep(ms / 1000.0)
def spi_writebyte(data) -> None:
_spi.writebytes(data)
def spi_writebyte2(data) -> None:
_spi.writebytes2(data)
def module_init() -> int:
"""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")
spi_path = f"/dev/spidev{_SPI_BUS}.{_SPI_DEVICE}"
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)
_busy_pin = gpiozero.DigitalInputDevice(BUSY_PIN, pull_up=False)
_spi = spidev.SpiDev()
_spi.open(_SPI_BUS, _SPI_DEVICE)
_spi.max_speed_hz = _SPI_HZ
_spi.mode = 0b00
except hardware_errors as e:
module_exit()
raise DisplayUnavailable(f"GPIO/SPI-Initialisierung fehlgeschlagen: {e}") from e
return 0
def module_exit() -> None:
"""Gibt GPIO-Pins und SPI-Geraet frei. Verzeiht, wenn module_init() nie (vollstaendig)
gelaufen ist - display.py ruft das u.a. im Fehlerpfad von module_init() selbst auf."""
global _rst_pin, _dc_pin, _busy_pin, _spi
if _spi is not None:
try:
_spi.close()
except Exception: # noqa: BLE001 - Aufraeumen darf nicht seinerseits scheitern
pass
_spi = None
for attr_name in ("_rst_pin", "_dc_pin", "_busy_pin"):
device = globals()[attr_name]
if device is not None:
try:
device.close()
except Exception: # noqa: BLE001
pass
globals()[attr_name] = None