driver.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # -*- coding: utf-8 -*-
  2. """
  3. WS2812/NeoPixel Hardware-Treiber.
  4. Abstrahiert den Zugriff auf die LED-Hardware via rpi-ws281x.
  5. Auf Systemen ohne GPIO (z.B. x86-Server) degradiert der Treiber
  6. graceful — alle Methoden werden zu No-Ops.
  7. """
  8. from __future__ import annotations
  9. from trixy_core.utils.debug import pdebug, pinfo, perror
  10. class WS2812Driver:
  11. """WS2812/NeoPixel Hardware-Treiber via rpi-ws281x.
  12. Graceful Degradation: Ohne rpi-ws281x (z.B. x86) gibt init() False
  13. zurueck. Alle Methoden sind dann No-Ops.
  14. Attributes:
  15. LED_FREQ_HZ: LED Signal-Frequenz in Hz.
  16. LED_DMA: DMA-Channel fuer die Datenuebertragung.
  17. LED_INVERT: Signal invertieren (bei Level-Shifter noetig).
  18. LED_CHANNEL: PWM-Channel (0 = GPIO 18, 1 = GPIO 13).
  19. """
  20. LED_FREQ_HZ: int = 800_000
  21. LED_DMA: int = 10
  22. LED_INVERT: bool = False
  23. LED_CHANNEL: int = 0
  24. def __init__(
  25. self,
  26. gpio_pin: int = 18,
  27. led_count: int = 1,
  28. brightness: int = 80,
  29. ) -> None:
  30. """Erstellt einen neuen WS2812-Treiber.
  31. Args:
  32. gpio_pin: GPIO-Pin fuer das Datensignal (Standard: 18/PWM0).
  33. led_count: Anzahl der LEDs in der Kette.
  34. brightness: Globale Helligkeit (0-255).
  35. """
  36. self._gpio_pin = gpio_pin
  37. self._led_count = led_count
  38. self._brightness = min(255, max(0, brightness))
  39. self._strip = None # rpi_ws281x.PixelStrip Instanz
  40. self._available = False
  41. @property
  42. def available(self) -> bool:
  43. """True wenn die LED-Hardware initialisiert und verfuegbar ist."""
  44. return self._available
  45. @property
  46. def led_count(self) -> int:
  47. """Anzahl der konfigurierten LEDs."""
  48. return self._led_count
  49. def init(self) -> bool:
  50. """Initialisiert die LED-Hardware.
  51. Versucht rpi_ws281x zu importieren und den LED-Strip zu
  52. initialisieren. Auf Nicht-Raspberry-Pi-Systemen schlaegt
  53. dies fehl und der Treiber arbeitet im No-Op-Modus weiter.
  54. Returns:
  55. True wenn die Hardware erfolgreich initialisiert wurde,
  56. False bei fehlendem Treiber oder GPIO-Zugriffsproblemen.
  57. """
  58. try:
  59. import rpi_ws281x # type: ignore[import-untyped]
  60. self._strip = rpi_ws281x.PixelStrip(
  61. self._led_count,
  62. self._gpio_pin,
  63. self.LED_FREQ_HZ,
  64. self.LED_DMA,
  65. self.LED_INVERT,
  66. self._brightness,
  67. self.LED_CHANNEL,
  68. )
  69. self._strip.begin()
  70. self._available = True
  71. pinfo(
  72. f"WS2812-Treiber initialisiert: {self._led_count} LEDs "
  73. f"auf GPIO {self._gpio_pin}, Helligkeit {self._brightness}"
  74. )
  75. return True
  76. except ImportError:
  77. pdebug(
  78. "rpi_ws281x nicht installiert — "
  79. "LED-Treiber arbeitet im No-Op-Modus"
  80. )
  81. self._available = False
  82. return False
  83. except RuntimeError as e:
  84. perror(
  85. f"LED-Hardware konnte nicht initialisiert werden: {e} — "
  86. f"GPIO-Zugriff erfordert Root-Rechte"
  87. )
  88. self._available = False
  89. return False
  90. except Exception as e:
  91. perror(f"Unerwarteter Fehler bei LED-Initialisierung: {e}")
  92. self._available = False
  93. return False
  94. def set_pixels(self, pixels: list[tuple[int, int, int]]) -> None:
  95. """Setzt alle LEDs auf die angegebenen Farben.
  96. Die Aenderungen werden erst mit show() an die Hardware
  97. uebertragen. Das erlaubt Batch-Updates ohne Flackern.
  98. Args:
  99. pixels: Liste von RGB-Tupeln, ein Tupel pro LED.
  100. Laenge muss mit led_count uebereinstimmen.
  101. """
  102. if not self._available or self._strip is None:
  103. return
  104. count = min(len(pixels), self._led_count)
  105. for i in range(count):
  106. r, g, b = pixels[i]
  107. self._strip.setPixelColor(i, self._rgb_to_color(r, g, b))
  108. def set_pixel(self, index: int, r: int, g: int, b: int) -> None:
  109. """Setzt eine einzelne LED auf die angegebene Farbe.
  110. Die Aenderung wird erst mit show() an die Hardware uebertragen.
  111. Args:
  112. index: LED-Index (0-basiert).
  113. r: Rot-Anteil (0-255).
  114. g: Gruen-Anteil (0-255).
  115. b: Blau-Anteil (0-255).
  116. """
  117. if not self._available or self._strip is None:
  118. return
  119. if 0 <= index < self._led_count:
  120. self._strip.setPixelColor(index, self._rgb_to_color(r, g, b))
  121. def show(self) -> None:
  122. """Uebertraegt die aktuellen Pixeldaten an die Hardware.
  123. Muss nach set_pixels() oder set_pixel() aufgerufen werden,
  124. damit die Aenderungen sichtbar werden.
  125. """
  126. if not self._available or self._strip is None:
  127. return
  128. try:
  129. self._strip.show()
  130. except Exception as e:
  131. perror(f"Fehler beim LED-Update: {e}")
  132. def clear(self) -> None:
  133. """Schaltet alle LEDs aus (schwarz).
  134. Setzt alle Pixel auf (0, 0, 0) und uebertraegt sofort.
  135. """
  136. if not self._available or self._strip is None:
  137. return
  138. for i in range(self._led_count):
  139. self._strip.setPixelColor(i, 0)
  140. self._strip.show()
  141. def set_brightness(self, value: int) -> None:
  142. """Setzt die globale Helligkeit.
  143. Args:
  144. value: Helligkeit von 0 (aus) bis 255 (maximum).
  145. """
  146. self._brightness = min(255, max(0, value))
  147. if not self._available or self._strip is None:
  148. return
  149. self._strip.setBrightness(self._brightness)
  150. self._strip.show()
  151. pdebug(f"LED-Helligkeit auf {self._brightness} gesetzt")
  152. def cleanup(self) -> None:
  153. """Gibt die LED-Hardware frei und schaltet alle LEDs aus.
  154. Sollte beim Beenden der Anwendung aufgerufen werden.
  155. """
  156. if not self._available or self._strip is None:
  157. pdebug("LED-Treiber Cleanup — keine Hardware aktiv")
  158. return
  159. pdebug("LED-Treiber Cleanup — LEDs werden ausgeschaltet")
  160. self.clear()
  161. self._available = False
  162. self._strip = None
  163. @staticmethod
  164. def _rgb_to_color(r: int, g: int, b: int) -> int:
  165. """Konvertiert RGB-Tupel in den von rpi_ws281x erwarteten Integer.
  166. rpi_ws281x erwartet Farben als 24-Bit Integer im Format 0xRRGGBB.
  167. Args:
  168. r: Rot-Anteil (0-255).
  169. g: Gruen-Anteil (0-255).
  170. b: Blau-Anteil (0-255).
  171. Returns:
  172. Farbwert als 24-Bit Integer.
  173. """
  174. return (int(r) & 0xFF) << 16 | (int(g) & 0xFF) << 8 | (int(b) & 0xFF)