|
|
@@ -0,0 +1,479 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""
|
|
|
+LED-Test-Script fuer WS2812/NeoPixel LEDs am Raspberry Pi.
|
|
|
+
|
|
|
+Unabhaengig von Trixy — braucht nur rpi-ws281x.
|
|
|
+Testet alle Animationen fuer die drei LED-Typen:
|
|
|
+ - Breakout (1 LED)
|
|
|
+ - Jewel 7 (7 LEDs, Mitte + 6 Ring)
|
|
|
+ - Ring 12 (12 LEDs im Kreis)
|
|
|
+
|
|
|
+Verkabelung:
|
|
|
+ Pi GPIO 18 (Pin 12) ──[330Ω]── DIN (Daten)
|
|
|
+ Pi 5V (Pin 2 oder 4) ───────── VCC (Strom)
|
|
|
+ Pi GND (Pin 6) ───────── GND
|
|
|
+
|
|
|
+Ausfuehrung:
|
|
|
+ sudo pip install rpi-ws281x
|
|
|
+ sudo python3 led_test.py --leds 7 --type jewel
|
|
|
+
|
|
|
+WICHTIG: Muss als root/sudo laufen (GPIO-Zugriff)!
|
|
|
+"""
|
|
|
+
|
|
|
+import argparse
|
|
|
+import math
|
|
|
+import random
|
|
|
+import signal
|
|
|
+import sys
|
|
|
+import time
|
|
|
+
|
|
|
+try:
|
|
|
+ from rpi_ws281x import PixelStrip, Color
|
|
|
+except ImportError:
|
|
|
+ print("FEHLER: rpi-ws281x nicht installiert!")
|
|
|
+ print(" sudo pip install rpi-ws281x")
|
|
|
+ sys.exit(1)
|
|
|
+
|
|
|
+
|
|
|
+# === Hardware-Konfiguration ===
|
|
|
+LED_FREQ_HZ = 800000
|
|
|
+LED_DMA = 10
|
|
|
+LED_INVERT = False
|
|
|
+LED_CHANNEL = 0
|
|
|
+
|
|
|
+
|
|
|
+# === Farb-Helfer ===
|
|
|
+
|
|
|
+def rgb(r, g, b):
|
|
|
+ return Color(r, g, b)
|
|
|
+
|
|
|
+def hsv_to_rgb(h, s, v):
|
|
|
+ """HSV (0-1) zu RGB (0-255)."""
|
|
|
+ if s == 0:
|
|
|
+ r = g = b = int(v * 255)
|
|
|
+ return r, g, b
|
|
|
+ i = int(h * 6)
|
|
|
+ f = (h * 6) - i
|
|
|
+ p = int(v * (1 - s) * 255)
|
|
|
+ q = int(v * (1 - s * f) * 255)
|
|
|
+ t = int(v * (1 - s * (1 - f)) * 255)
|
|
|
+ v = int(v * 255)
|
|
|
+ i %= 6
|
|
|
+ if i == 0: return v, t, p
|
|
|
+ if i == 1: return q, v, p
|
|
|
+ if i == 2: return p, v, t
|
|
|
+ if i == 3: return p, q, v
|
|
|
+ if i == 4: return t, p, v
|
|
|
+ return v, p, q
|
|
|
+
|
|
|
+
|
|
|
+# === Layout-Definitionen ===
|
|
|
+
|
|
|
+class Layout:
|
|
|
+ def __init__(self, name, count, center=None, ring=None):
|
|
|
+ self.name = name
|
|
|
+ self.count = count
|
|
|
+ self.center = center # Index der Mitte (None bei Ring/Single)
|
|
|
+ self.ring = ring or list(range(count)) # Ring-Indizes
|
|
|
+
|
|
|
+LAYOUTS = {
|
|
|
+ "single": Layout("Breakout 1x", 1, center=None, ring=[0]),
|
|
|
+ "jewel": Layout("Jewel 7x", 7, center=0, ring=[1, 2, 3, 4, 5, 6]),
|
|
|
+ "ring12": Layout("Ring 12x", 12, center=None, ring=list(range(12))),
|
|
|
+ "ring16": Layout("Ring 16x", 16, center=None, ring=list(range(16))),
|
|
|
+ "ring24": Layout("Ring 24x", 24, center=None, ring=list(range(24))),
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+# === Animationen ===
|
|
|
+
|
|
|
+def clear(strip):
|
|
|
+ """Alle LEDs aus."""
|
|
|
+ for i in range(strip.numPixels()):
|
|
|
+ strip.setPixelColor(i, Color(0, 0, 0))
|
|
|
+ strip.show()
|
|
|
+
|
|
|
+
|
|
|
+def test_single_color(strip, layout, color, name, duration=2.0):
|
|
|
+ """Alle LEDs eine Farbe."""
|
|
|
+ print(f" {name}...", end="", flush=True)
|
|
|
+ for i in range(strip.numPixels()):
|
|
|
+ strip.setPixelColor(i, color)
|
|
|
+ strip.show()
|
|
|
+ time.sleep(duration)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_blink(strip, layout, color, times=3, speed=0.2):
|
|
|
+ """Blinken."""
|
|
|
+ print(f" Blink ({times}x)...", end="", flush=True)
|
|
|
+ for _ in range(times):
|
|
|
+ for i in range(strip.numPixels()):
|
|
|
+ strip.setPixelColor(i, color)
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ time.sleep(speed)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_breathe(strip, layout, color, cycles=3, steps=60):
|
|
|
+ """Sanftes Atmen (Sinus-Kurve)."""
|
|
|
+ print(f" Breathe ({cycles} Zyklen)...", end="", flush=True)
|
|
|
+ r = (color >> 16) & 0xFF
|
|
|
+ g = (color >> 8) & 0xFF
|
|
|
+ b = color & 0xFF
|
|
|
+
|
|
|
+ for cycle in range(cycles):
|
|
|
+ for step in range(steps):
|
|
|
+ brightness = (math.sin(step / steps * 2 * math.pi - math.pi / 2) + 1) / 2
|
|
|
+ cr = int(r * brightness)
|
|
|
+ cg = int(g * brightness)
|
|
|
+ cb = int(b * brightness)
|
|
|
+ for i in range(strip.numPixels()):
|
|
|
+ strip.setPixelColor(i, Color(cr, cg, cb))
|
|
|
+ strip.show()
|
|
|
+ time.sleep(0.03)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_circulate(strip, layout, color, rounds=3, speed=0.08):
|
|
|
+ """Eine LED wandert im Kreis."""
|
|
|
+ if len(layout.ring) <= 1:
|
|
|
+ print(" Circulate (uebersprungen — nur 1 LED)")
|
|
|
+ return
|
|
|
+
|
|
|
+ print(f" Circulate ({rounds} Runden)...", end="", flush=True)
|
|
|
+ center_color = Color(0, 30, 0) # Mitte gedimmt gruen
|
|
|
+
|
|
|
+ for _ in range(rounds):
|
|
|
+ for pos in range(len(layout.ring)):
|
|
|
+ clear(strip)
|
|
|
+ # Mitte (Jewel)
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, center_color)
|
|
|
+ # Aktive LED
|
|
|
+ strip.setPixelColor(layout.ring[pos], color)
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_circular_fade(strip, layout, color, rounds=3, tail=3, speed=0.06):
|
|
|
+ """Kreis mit Nachleuchten."""
|
|
|
+ ring = layout.ring
|
|
|
+ if len(ring) <= 1:
|
|
|
+ print(" Circular-Fade (uebersprungen — nur 1 LED)")
|
|
|
+ return
|
|
|
+
|
|
|
+ print(f" Circular-Fade (tail={tail})...", end="", flush=True)
|
|
|
+ r = (color >> 16) & 0xFF
|
|
|
+ g = (color >> 8) & 0xFF
|
|
|
+ b = color & 0xFF
|
|
|
+
|
|
|
+ center_color = Color(0, 20, 0)
|
|
|
+
|
|
|
+ for _ in range(rounds):
|
|
|
+ for pos in range(len(ring)):
|
|
|
+ clear(strip)
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, center_color)
|
|
|
+
|
|
|
+ # Haupt-LED
|
|
|
+ strip.setPixelColor(ring[pos], color)
|
|
|
+
|
|
|
+ # Nachleuchtende LEDs
|
|
|
+ for t in range(1, tail + 1):
|
|
|
+ fade = 1.0 - (t / (tail + 1))
|
|
|
+ fade_idx = (pos - t) % len(ring)
|
|
|
+ strip.setPixelColor(
|
|
|
+ ring[fade_idx],
|
|
|
+ Color(int(r * fade), int(g * fade), int(b * fade)),
|
|
|
+ )
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_rainbow(strip, layout, rounds=2, speed=0.02):
|
|
|
+ """Regenbogen-Rotation."""
|
|
|
+ print(f" Rainbow ({rounds} Runden)...", end="", flush=True)
|
|
|
+ ring = layout.ring
|
|
|
+
|
|
|
+ for offset in range(rounds * len(ring) * 5):
|
|
|
+ for i, led_idx in enumerate(ring):
|
|
|
+ hue = ((i / len(ring)) + (offset / (len(ring) * 5))) % 1.0
|
|
|
+ r, g, b = hsv_to_rgb(hue, 1.0, 1.0)
|
|
|
+ strip.setPixelColor(led_idx, Color(r, g, b))
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, Color(50, 50, 50))
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_chase(strip, layout, color, rounds=4, group=3, speed=0.06):
|
|
|
+ """Lauflicht (Gruppe von LEDs)."""
|
|
|
+ ring = layout.ring
|
|
|
+ if len(ring) <= 1:
|
|
|
+ print(" Chase (uebersprungen — nur 1 LED)")
|
|
|
+ return
|
|
|
+
|
|
|
+ print(f" Chase ({group} LEDs Gruppe)...", end="", flush=True)
|
|
|
+ r = (color >> 16) & 0xFF
|
|
|
+ g = (color >> 8) & 0xFF
|
|
|
+ b = color & 0xFF
|
|
|
+
|
|
|
+ for _ in range(rounds):
|
|
|
+ for pos in range(len(ring)):
|
|
|
+ clear(strip)
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, Color(0, 20, 0))
|
|
|
+ for j in range(group):
|
|
|
+ idx = (pos + j) % len(ring)
|
|
|
+ fade = 1.0 - (j / group) * 0.5
|
|
|
+ strip.setPixelColor(
|
|
|
+ ring[idx],
|
|
|
+ Color(int(r * fade), int(g * fade), int(b * fade)),
|
|
|
+ )
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_sparkle(strip, layout, color, duration=3.0, speed=0.05):
|
|
|
+ """Zufaelliges Funkeln."""
|
|
|
+ print(f" Sparkle ({duration}s)...", end="", flush=True)
|
|
|
+ r = (color >> 16) & 0xFF
|
|
|
+ g = (color >> 8) & 0xFF
|
|
|
+ b = color & 0xFF
|
|
|
+
|
|
|
+ end_time = time.monotonic() + duration
|
|
|
+ while time.monotonic() < end_time:
|
|
|
+ clear(strip)
|
|
|
+ # 1-2 zufaellige LEDs
|
|
|
+ for _ in range(random.randint(1, min(3, strip.numPixels()))):
|
|
|
+ idx = random.choice(layout.ring)
|
|
|
+ strip.setPixelColor(idx, color)
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, Color(r // 4, g // 4, b // 4))
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_wave(strip, layout, color, rounds=4, speed=0.03):
|
|
|
+ """Helligkeits-Welle (Knight Rider)."""
|
|
|
+ ring = layout.ring
|
|
|
+ if len(ring) <= 2:
|
|
|
+ print(" Wave (uebersprungen — zu wenige LEDs)")
|
|
|
+ return
|
|
|
+
|
|
|
+ print(f" Wave (Knight Rider)...", end="", flush=True)
|
|
|
+ r = (color >> 16) & 0xFF
|
|
|
+ g = (color >> 8) & 0xFF
|
|
|
+ b = color & 0xFF
|
|
|
+
|
|
|
+ positions = list(range(len(ring))) + list(range(len(ring) - 2, 0, -1))
|
|
|
+
|
|
|
+ for _ in range(rounds):
|
|
|
+ for center_pos in positions:
|
|
|
+ for i, led_idx in enumerate(ring):
|
|
|
+ dist = abs(i - center_pos)
|
|
|
+ brightness = max(0, 1.0 - dist * 0.3)
|
|
|
+ brightness = brightness ** 2 # Quadratischer Abfall
|
|
|
+ strip.setPixelColor(
|
|
|
+ led_idx,
|
|
|
+ Color(int(r * brightness), int(g * brightness), int(b * brightness)),
|
|
|
+ )
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, Color(r // 5, g // 5, b // 5))
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_fill_sequential(strip, layout, color, speed=0.1):
|
|
|
+ """LEDs nacheinander auffuellen."""
|
|
|
+ print(f" Fill Sequential...", end="", flush=True)
|
|
|
+ clear(strip)
|
|
|
+
|
|
|
+ for led_idx in layout.ring:
|
|
|
+ strip.setPixelColor(led_idx, color)
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+
|
|
|
+ if layout.center is not None:
|
|
|
+ strip.setPixelColor(layout.center, color)
|
|
|
+ strip.show()
|
|
|
+ time.sleep(speed)
|
|
|
+
|
|
|
+ time.sleep(0.5)
|
|
|
+ clear(strip)
|
|
|
+ print(" OK")
|
|
|
+
|
|
|
+
|
|
|
+def test_trixy_states(strip, layout):
|
|
|
+ """Simuliert die Trixy-Zustandsmaschine."""
|
|
|
+ print("\n=== Trixy-Zustandssimulation ===")
|
|
|
+
|
|
|
+ # Boot
|
|
|
+ print(" [BOOT] Gruen hell...")
|
|
|
+ test_single_color(strip, layout, rgb(0, 255, 0), "Boot", 1.5)
|
|
|
+
|
|
|
+ # Standby
|
|
|
+ print(" [STANDBY] Gruen gedimmt...")
|
|
|
+ test_single_color(strip, layout, rgb(0, 60, 0), "Standby", 2.0)
|
|
|
+
|
|
|
+ # Wakeword
|
|
|
+ print(" [WAKEWORD] 2x Blau blinken...")
|
|
|
+ test_blink(strip, layout, rgb(0, 0, 255), times=2, speed=0.15)
|
|
|
+
|
|
|
+ # Listening
|
|
|
+ print(" [LISTENING] Rot (Mikrofon aktiv)...")
|
|
|
+ test_single_color(strip, layout, rgb(255, 0, 0), "Listening", 2.0)
|
|
|
+
|
|
|
+ # Thinking
|
|
|
+ print(" [THINKING] Orange pulsierend...")
|
|
|
+ test_breathe(strip, layout, rgb(255, 165, 0).intColor if hasattr(rgb(255, 165, 0), 'intColor') else Color(255, 165, 0), cycles=2, steps=30)
|
|
|
+
|
|
|
+ # Speaking
|
|
|
+ print(" [SPEAKING] Blau atmend...")
|
|
|
+ test_breathe(strip, layout, Color(0, 100, 255), cycles=2, steps=60)
|
|
|
+
|
|
|
+ # Error
|
|
|
+ print(" [ERROR] 3x Rot blinken...")
|
|
|
+ test_blink(strip, layout, rgb(255, 0, 0), times=3, speed=0.15)
|
|
|
+
|
|
|
+ # Disconnect
|
|
|
+ print(" [DISCONNECT] Rot pulsierend...")
|
|
|
+ test_breathe(strip, layout, Color(255, 0, 0), cycles=2, steps=40)
|
|
|
+
|
|
|
+ # Zurueck zu Standby
|
|
|
+ print(" [STANDBY] Gruen gedimmt...")
|
|
|
+ test_single_color(strip, layout, rgb(0, 60, 0), "Standby", 1.0)
|
|
|
+
|
|
|
+ clear(strip)
|
|
|
+ print(" Simulation fertig!")
|
|
|
+
|
|
|
+
|
|
|
+# === Hauptprogramm ===
|
|
|
+
|
|
|
+def main():
|
|
|
+ parser = argparse.ArgumentParser(
|
|
|
+ description="WS2812/NeoPixel LED-Test fuer Raspberry Pi",
|
|
|
+ formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
+ epilog="""
|
|
|
+Beispiele:
|
|
|
+ sudo python3 led_test.py --type jewel # Jewel 7x testen
|
|
|
+ sudo python3 led_test.py --leds 12 --type ring12 # Ring 12x
|
|
|
+ sudo python3 led_test.py --leds 1 --type single # Breakout 1x
|
|
|
+ sudo python3 led_test.py --type jewel --test states # Nur Trixy-Simulation
|
|
|
+ sudo python3 led_test.py --type jewel --brightness 50 # Niedrigere Helligkeit
|
|
|
+ sudo python3 led_test.py --pin 12 --type ring12 # Anderer GPIO Pin
|
|
|
+
|
|
|
+Verkabelung:
|
|
|
+ Pi GPIO 18 (Pin 12) --[330 Ohm]-- DIN (Daten-Eingang)
|
|
|
+ Pi 5V (Pin 2 oder 4) ------------ VCC (Strom)
|
|
|
+ Pi GND (Pin 6) ------------ GND
|
|
|
+
|
|
|
+WICHTIG: Muss als root/sudo ausgefuehrt werden!
|
|
|
+ """,
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--type", choices=list(LAYOUTS.keys()), default="jewel",
|
|
|
+ help="LED-Typ (default: jewel)",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--leds", type=int, default=0,
|
|
|
+ help="Anzahl LEDs (ueberschreibt --type Default)",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--pin", type=int, default=18,
|
|
|
+ help="GPIO BCM Pin (default: 18 = PWM0)",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--brightness", type=int, default=80,
|
|
|
+ help="Helligkeit 0-255 (default: 80)",
|
|
|
+ )
|
|
|
+ parser.add_argument(
|
|
|
+ "--test", choices=["all", "colors", "animations", "states"],
|
|
|
+ default="all",
|
|
|
+ help="Test-Modus (default: all)",
|
|
|
+ )
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ layout = LAYOUTS[args.type]
|
|
|
+ led_count = args.leds if args.leds > 0 else layout.count
|
|
|
+
|
|
|
+ print(f"╔═══════════════════════════════════════════╗")
|
|
|
+ print(f"║ WS2812 LED Test — Trixy ║")
|
|
|
+ print(f"╠═══════════════════════════════════════════╣")
|
|
|
+ print(f"║ Typ: {layout.name:28s} ║")
|
|
|
+ print(f"║ LEDs: {led_count:<28d} ║")
|
|
|
+ print(f"║ GPIO Pin: {args.pin:<28d} ║")
|
|
|
+ print(f"║ Helligkeit: {args.brightness:<28d} ║")
|
|
|
+ print(f"╚═══════════════════════════════════════════╝")
|
|
|
+ print()
|
|
|
+
|
|
|
+ # LED-Strip initialisieren
|
|
|
+ strip = PixelStrip(
|
|
|
+ led_count, args.pin,
|
|
|
+ LED_FREQ_HZ, LED_DMA, LED_INVERT,
|
|
|
+ args.brightness, LED_CHANNEL,
|
|
|
+ )
|
|
|
+ strip.begin()
|
|
|
+
|
|
|
+ # SIGINT abfangen → LEDs aus
|
|
|
+ def cleanup(sig, frame):
|
|
|
+ print("\n\nAbbruch — LEDs werden ausgeschaltet...")
|
|
|
+ clear(strip)
|
|
|
+ sys.exit(0)
|
|
|
+ signal.signal(signal.SIGINT, cleanup)
|
|
|
+
|
|
|
+ try:
|
|
|
+ if args.test in ("all", "colors"):
|
|
|
+ print("=== Farbtest ===")
|
|
|
+ test_single_color(strip, layout, rgb(255, 0, 0), "Rot")
|
|
|
+ test_single_color(strip, layout, rgb(0, 255, 0), "Gruen")
|
|
|
+ test_single_color(strip, layout, rgb(0, 0, 255), "Blau")
|
|
|
+ test_single_color(strip, layout, rgb(255, 255, 0), "Gelb")
|
|
|
+ test_single_color(strip, layout, rgb(255, 0, 255), "Magenta")
|
|
|
+ test_single_color(strip, layout, rgb(0, 255, 255), "Cyan")
|
|
|
+ test_single_color(strip, layout, rgb(255, 255, 255), "Weiss")
|
|
|
+ test_single_color(strip, layout, rgb(255, 165, 0), "Orange")
|
|
|
+ print()
|
|
|
+
|
|
|
+ if args.test in ("all", "animations"):
|
|
|
+ print("=== Animationen ===")
|
|
|
+ test_blink(strip, layout, rgb(0, 0, 255), times=3)
|
|
|
+ test_breathe(strip, layout, Color(0, 100, 255), cycles=2)
|
|
|
+ test_circulate(strip, layout, rgb(0, 255, 0))
|
|
|
+ test_circular_fade(strip, layout, rgb(0, 0, 255), tail=3)
|
|
|
+ test_chase(strip, layout, rgb(255, 165, 0), group=3)
|
|
|
+ test_rainbow(strip, layout, rounds=2)
|
|
|
+ test_wave(strip, layout, rgb(255, 0, 0))
|
|
|
+ test_sparkle(strip, layout, rgb(255, 255, 255), duration=3.0)
|
|
|
+ test_fill_sequential(strip, layout, rgb(0, 255, 0))
|
|
|
+ print()
|
|
|
+
|
|
|
+ if args.test in ("all", "states"):
|
|
|
+ test_trixy_states(strip, layout)
|
|
|
+
|
|
|
+ print("\n✓ Alle Tests abgeschlossen!")
|
|
|
+
|
|
|
+ finally:
|
|
|
+ clear(strip)
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|