render.lines() zerlegt den zusammengefuehrten Zustand (status.json + control.json + SSID aus der Konfigurationsdatei) in Textzeilen, ohne Pillow und ohne Hardware zu brauchen - render.compose() importiert Pillow erst innerhalb der Funktion. epdconfig.py ist eine eigene GPIO/SPI-Schicht auf gpiozero/spidev, weil RPi.GPIO auf Debian 13 mit Python 3.13 nicht mehr traegt; epd2in13_V2.py/epd2in13_V3.py sind unveraendert aus dem Waveshare-Repository uebernommen (siehe HERKUNFT.md), nur der epdconfig-Import zeigt auf unser Modul. display.py laeuft als eigener systemd-Dienst (Wants=, Restart=on-failure), fehlendes Panel/SPI/Bibliothek beendet ihn sauber mit sys.exit(0) statt einer Neustartschleife - eine Migration darf davon nie abhaengen. Verdrahtung in setup_pi.sh/deploy.sh/ninlight-station folgt in einem eigenen Durchgang.
140 lines
4.7 KiB
Python
140 lines
4.7 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 irgendeiner
|
|
Bibliotheksausnahme, damit display.py genau eine Ausnahmeklasse abfangen muss."""
|
|
global _rst_pin, _dc_pin, _busy_pin, _spi
|
|
|
|
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)?")
|
|
|
|
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 Exception as e: # noqa: BLE001 - alles hier wird zu DisplayUnavailable
|
|
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
|