feat(station): E-Paper-Anzeige fuer Modus, Lampe und Auslieferungen
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.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user