# -*- coding: utf-8 -*- """ WS2812/NeoPixel Hardware-Treiber. Abstrahiert den Zugriff auf die LED-Hardware via rpi-ws281x. Auf Systemen ohne GPIO (z.B. x86-Server) degradiert der Treiber graceful — alle Methoden werden zu No-Ops. """ from __future__ import annotations from trixy_core.utils.debug import pdebug, pinfo, perror class WS2812Driver: """WS2812/NeoPixel Hardware-Treiber via rpi-ws281x. Graceful Degradation: Ohne rpi-ws281x (z.B. x86) gibt init() False zurueck. Alle Methoden sind dann No-Ops. Attributes: LED_FREQ_HZ: LED Signal-Frequenz in Hz. LED_DMA: DMA-Channel fuer die Datenuebertragung. LED_INVERT: Signal invertieren (bei Level-Shifter noetig). LED_CHANNEL: PWM-Channel (0 = GPIO 18, 1 = GPIO 13). """ LED_FREQ_HZ: int = 800_000 LED_DMA: int = 10 LED_INVERT: bool = False LED_CHANNEL: int = 0 def __init__( self, gpio_pin: int = 18, led_count: int = 1, brightness: int = 80, ) -> None: """Erstellt einen neuen WS2812-Treiber. Args: gpio_pin: GPIO-Pin fuer das Datensignal (Standard: 18/PWM0). led_count: Anzahl der LEDs in der Kette. brightness: Globale Helligkeit (0-255). """ self._gpio_pin = gpio_pin self._led_count = led_count self._brightness = min(255, max(0, brightness)) self._strip = None # rpi_ws281x.PixelStrip Instanz self._available = False @property def available(self) -> bool: """True wenn die LED-Hardware initialisiert und verfuegbar ist.""" return self._available @property def led_count(self) -> int: """Anzahl der konfigurierten LEDs.""" return self._led_count def init(self) -> bool: """Initialisiert die LED-Hardware. Versucht rpi_ws281x zu importieren und den LED-Strip zu initialisieren. Auf Nicht-Raspberry-Pi-Systemen schlaegt dies fehl und der Treiber arbeitet im No-Op-Modus weiter. Returns: True wenn die Hardware erfolgreich initialisiert wurde, False bei fehlendem Treiber oder GPIO-Zugriffsproblemen. """ try: import rpi_ws281x # type: ignore[import-untyped] self._strip = rpi_ws281x.PixelStrip( self._led_count, self._gpio_pin, self.LED_FREQ_HZ, self.LED_DMA, self.LED_INVERT, self._brightness, self.LED_CHANNEL, ) self._strip.begin() self._available = True pinfo( f"WS2812-Treiber initialisiert: {self._led_count} LEDs " f"auf GPIO {self._gpio_pin}, Helligkeit {self._brightness}" ) return True except ImportError: pdebug( "rpi_ws281x nicht installiert — " "LED-Treiber arbeitet im No-Op-Modus" ) self._available = False return False except RuntimeError as e: perror( f"LED-Hardware konnte nicht initialisiert werden: {e} — " f"GPIO-Zugriff erfordert Root-Rechte" ) self._available = False return False except Exception as e: perror(f"Unerwarteter Fehler bei LED-Initialisierung: {e}") self._available = False return False def set_pixels(self, pixels: list[tuple[int, int, int]]) -> None: """Setzt alle LEDs auf die angegebenen Farben. Die Aenderungen werden erst mit show() an die Hardware uebertragen. Das erlaubt Batch-Updates ohne Flackern. Args: pixels: Liste von RGB-Tupeln, ein Tupel pro LED. Laenge muss mit led_count uebereinstimmen. """ if not self._available or self._strip is None: return count = min(len(pixels), self._led_count) for i in range(count): r, g, b = pixels[i] self._strip.setPixelColor(i, self._rgb_to_color(r, g, b)) def set_pixel(self, index: int, r: int, g: int, b: int) -> None: """Setzt eine einzelne LED auf die angegebene Farbe. Die Aenderung wird erst mit show() an die Hardware uebertragen. Args: index: LED-Index (0-basiert). r: Rot-Anteil (0-255). g: Gruen-Anteil (0-255). b: Blau-Anteil (0-255). """ if not self._available or self._strip is None: return if 0 <= index < self._led_count: self._strip.setPixelColor(index, self._rgb_to_color(r, g, b)) def show(self) -> None: """Uebertraegt die aktuellen Pixeldaten an die Hardware. Muss nach set_pixels() oder set_pixel() aufgerufen werden, damit die Aenderungen sichtbar werden. """ if not self._available or self._strip is None: return try: self._strip.show() except Exception as e: perror(f"Fehler beim LED-Update: {e}") def clear(self) -> None: """Schaltet alle LEDs aus (schwarz). Setzt alle Pixel auf (0, 0, 0) und uebertraegt sofort. """ if not self._available or self._strip is None: return for i in range(self._led_count): self._strip.setPixelColor(i, 0) self._strip.show() def set_brightness(self, value: int) -> None: """Setzt die globale Helligkeit. Args: value: Helligkeit von 0 (aus) bis 255 (maximum). """ self._brightness = min(255, max(0, value)) if not self._available or self._strip is None: return self._strip.setBrightness(self._brightness) self._strip.show() pdebug(f"LED-Helligkeit auf {self._brightness} gesetzt") def cleanup(self) -> None: """Gibt die LED-Hardware frei und schaltet alle LEDs aus. Sollte beim Beenden der Anwendung aufgerufen werden. """ if not self._available or self._strip is None: pdebug("LED-Treiber Cleanup — keine Hardware aktiv") return pdebug("LED-Treiber Cleanup — LEDs werden ausgeschaltet") self.clear() self._available = False self._strip = None @staticmethod def _rgb_to_color(r: int, g: int, b: int) -> int: """Konvertiert RGB-Tupel in den von rpi_ws281x erwarteten Integer. rpi_ws281x erwartet Farben als 24-Bit Integer im Format 0xRRGGBB. Args: r: Rot-Anteil (0-255). g: Gruen-Anteil (0-255). b: Blau-Anteil (0-255). Returns: Farbwert als 24-Bit Integer. """ return (int(r) & 0xFF) << 16 | (int(g) & 0xFF) << 8 | (int(b) & 0xFF)