Fixrunde 2 zu Task 6a: der sleep()-Einbau aus Fixrunde 1 hatte drei neue Critical erzeugt. epd.sleep() schliesst ueber epdconfig.module_exit() die GPIO-Pins; ein Teilaufbau direkt danach fasste bei V3 eine bereits geschlossene Pin-Referenz an (AttributeError, ungefangen, Neustartschleife - binnen Sekunden nach dem Start, V3 ist die Vorgaberevision) und war bei V2 nach Deep Sleep ohnehin keine belastbare Sequenz (init(PART_UPDATE) allein setzt nicht die Register, die nur der FULL_UPDATE-Zweig setzt). Ausserdem setzte der V3-Vollaufbau nie den Referenzpuffer, den ein Teilaufbau zum Diffen braucht (displayPartBaseImage() statt display()). Panel bekommt ein internes _awake-Flag: draw_partial() wertet sich selbst zu einem Vollaufbau auf, wenn das Panel nicht wach ist (frisch erzeugt oder seit dem letzten Aufbau geschlafen), unabhaengig von der Aufrufreihenfolge. sleep() wird nur noch nach laengerer Ruhe (10 Minuten ohne Aenderung) gerufen, nicht mehr nach jedem Bild, und ist selbst idempotent. Dazu: epdconfig faengt jetzt gpiozero.GPIOZeroError statt nur GPIODeviceError (die PinError-Familie haengt nicht darunter), EnvironmentFile= in der Unit bekommt ein "-"-Praefix, und die Zeilenkuerzung wandert von einer Zeichenzahl-Grenze (ungueltig bei einer Proportionalschrift) zu einer echten Pixelbreitenmessung in compose() - lines() bleibt dafuer ungekuerzt und weiterhin ohne Pillow testbar.
166 lines
6.2 KiB
Python
166 lines
6.2 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). 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,)
|
|
|
|
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
|