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.
161 lines
5.8 KiB
Python
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
|