Stell dir vor: Du hast einen Computer, der kleiner als eine Streichholzschachtel ist. Kein Bildschirm, keine Tastatur – nur ein winziger Chip mit WiFi. Und du programmierst ihn so, dass er 768 bunte LEDs gleichzeitig steuert, auf Befehle aus dem ganzen Haus hört und sich sogar mit anderen Geräten unterhält. Klingt nach Science-Fiction? Das ist mein Projekt “Bunt”!

Was ist das überhaupt?

Eine LED-Matrix ist ein Raster aus vielen kleinen Farblämpchen. Jede einzelne LED kann eine beliebige Farbe leuchten – Rot, Grün, Blau, oder jede Mischung davon. Meine Matrix hat 32 Spalten und 8 Zeilen, also 256 LEDs pro Modul. Ich kann bis zu 3 solcher Module hintereinander schalten – dann leuchten 768 LEDs gleichzeitig! Natürlich wären noch viel mehr möglich!

Die LED-Matrix
Die LED-Matrix

Der kleine Computer dahinter heißt M5 Atom Lite – ein ESP32-Chip in einem Gehäuse so groß wie ein Würfelzucker. Er hat WiFi eingebaut und kann eigenständig Programme ausführen, ohne dass ein Computer angesteckt sein muss.

Der M5 Atom Lite – kleiner als ein Würfelzucker
Der M5 Atom Lite – kleiner als ein Würfelzucker

Die Einkaufsliste

Das brauchst du für den Nachbau:

Teil Wozu? Ungefährer Preis
M5Stack Atom Lite Der “Gehirn”-Computer ~8 €
WS2812B 8×32 Matrix Die LED-Tafel ~15 €
Printnetzteil 5V/6A Strom für alles ~10 €
470 Ω Widerstand Schützt die Datenleitung ~0,10 €
Kabel, Stecker Verbindungen ~3 €

Gesamt: ca. 36 € – und du bekommst dafür ein programmierbares Licht, das man beliebig erweitern kann.

⚠️ Angebot: Die ersten 5. Neffen von mir, die sich bei mir melden können mein Zeug verwenden, das ich eh hier habe 😉

Warum ein Widerstand? Der 470 Ω Widerstand sitzt zwischen dem ESP32 und der LED-Matrix. Ohne ihn könnten kurze Spannungsspitzen beim Einschalten die erste LED beschädigen. So ein kleines Bauteil, so wichtig!

Wie hängt das zusammen?

Verdrahtung: Netzteil, ESP32 und Matrix
Verdrahtung: Netzteil, ESP32 und Matrix

Das Netzteil liefert 5 Volt Gleichstrom – ungefähr so viel wie ein USB-Ladegerät, aber mit mehr Strom. Diese 5 Volt gehen gleichzeitig:
- An die LED-Matrix (die braucht viel Strom wenn viele LEDs leuchten)
- An den M5 Atom (der braucht nur wenig Strom)

Der M5 Atom schickt dann über eine einzige Datenleitung die Befehle an alle 768 LEDs. Das ist das Tolle an WS2812B LEDs: jede LED hat einen kleinen Chip eingebaut, der den Befehl entgegennimmt, seine Farbe setzt, und den Rest des Befehls an die nächste LED weiterleitet. Wie eine Telefonkette!

Alle Teile zusammengebaut
Alle Teile zusammengebaut

Zusammenbauen – Schritt für Schritt

1. Netzteil vorbereiten

Das Printnetzteil hat auf einer Seite den 230V-Eingang (Vorsicht – das ist gefährlich, nur Erwachsene!), auf der anderen Seite kommen saubere 5V raus. Dort schraubst du zwei Kabel an: Plus (+) und Minus/GND (–).

Das Netzteil
Das Netzteil

2. Matrix anschließen

Die LED-Matrix hat drei Anschlüsse:
- 5V – Stromversorgung
- GND – Minus/Masse
- DIN – Dateneingang

5V und GND kommen vom Netzteil. DIN kommt vom M5 Atom – aber über den 470 Ω Widerstand!

Anschlüsse an der LED-Matrix
Anschlüsse an der LED-Matrix

3. M5 Atom anschließen

Der M5 Atom bekommt auch 5V und GND vom Netzteil. Der Datenpin für die LED-Matrix ist GPIO32 – das ist einer der kleinen Metallkontakte am Anschluss des Boards.

Anschluss des M5
Anschluss des M5

4. Fertig!

Wenn du alles richtig angeschlossen hast und Strom gibst, passiert erstmal nichts – der M5 wartet auf seine Firmware. Die kommt im nächsten Schritt.

Die Software – wie funktioniert das?

Hier wird es spannend! Ich programmiere den M5 Atom nicht mit normalen Programmierbefehlen, sondern mit ESPHome – einem System, bei dem man in einer einfachen Textdatei beschreibt, was der Chip tun soll.

Meine Konfigurationsdatei heißt bunt.yaml. “YAML” ist ein Dateiformat wo man Einstellungen einfach untereinander schreibt – wie eine To-Do-Liste für den Computer.

Was kann das Programm?

Der M5 Atom hat 11 verschiedene Lichtprogramme eingebaut:

Nr. Name Beschreibung
0 Knight Rider Ein Lichtpunkt flitzt hin und her (wie das Auto in der alten TV-Serie!)
1 Regenbogen Alle Farben des Regenbogens laufen durch
2 Regenbogen Diagonal Wie Regenbogen, aber schräg
3 Feuer Eine Feuer-Simulation mit Hitzewellen
4 Matrix-Regen Grüne Tropfen fallen herunter (wie im Film “Matrix”)
5 Farbwellen Sanfte Farbwellen, die ineinanderfließen
6 Sternenhimmel Kleine Sterne blinken zufällig
7 Lauflicht Ein Lichtkegel gleitet hin und her
8 Demo Wechselt automatisch zwischen allen Programmen
9 Knight Rider Feuer Knight Rider, aber mit Feuerschweif!
10 Laufschrift Texte scrollen über die Matrix
Knight Rider – ein Klassiker
Laufschrift in grün

Wie kommunizieren die Geräte?

Das ist das Coolste am ganzen Projekt! Ich habe drei ESP32-Geräte, die miteinander sprechen:

  • Bunt – der mit den LEDs (da sind wir gerade)
  • Gelb – ein Display-Gerät mit Touchscreen zur Steuerung
  • Blau – ein Bedienfeld mit Drehknöpfen und Tastern

Die drei Geräte nutzen ESP-NOW – ein spezielles WiFi-Protokoll, das direkt von Gerät zu Gerät sendet, ohne einen Router dazwischen. Das ist so schnell, dass die Reaktion sofort passiert wenn man einen Knopf drückt!

Wenn Bunt startet, schickt er eine Liste seiner Programme an alle anderen Geräte. Die können dann sagen: “Spiel Programm Nummer 3!” – und Bunt fängt sofort an, das Feuer-Programm abzuspielen.

Der Code (vereinfacht erklärt)

Jedes Lichtprogramm ist ein sogenannter “Effekt” in der Software. So funktioniert zum Beispiel der Regenbogen-Effekt in vereinfachter Form:

Für jeden Schritt:
  Für jede LED:
    Berechne eine Farbe basierend auf Position + Zeit
    Setze die LED auf diese Farbe
  Warte 50 Millisekunden
  Gehe zum nächsten Schritt

Der Computer führt das 20 Mal pro Sekunde aus – das sieht dann für unser Auge wie eine flüssige Bewegung aus, genau wie bei einem Film!

Hier die echte Konfigurationsdatei:

bunt.yaml 673 Zeilen ⬇ Download
esphome:
  name: bunt
  friendly_name: bunt
  on_boot:
    priority: -10
    then:
      - delay: 3s
      - script.execute: send_program_list
      - delay: 3s
      - lambda: |-
          id(current_program) = 0;
          id(play_program).execute();

esp32:
  board: m5stack-atom
  framework:
    type: esp-idf

# ---------------------------------------------------------------------------
# LOGGING & API
# ---------------------------------------------------------------------------
logger:
  level: DEBUG

api:
  encryption:
    key: !secret buntkey

ota:
  - platform: esphome
    password: !secret buntpassword

captive_portal:

# ---------------------------------------------------------------------------
# NETZWERK
# ---------------------------------------------------------------------------
wifi:
  power_save_mode: none
  fast_connect: true
  networks:
    - ssid: !secret wifi10_ssid
      password: !secret wifi10_password
      priority: 100
    - ssid: !secret wifi2_ssid
      password: !secret wifi2_password
      priority: 50
    - ssid: !secret wifi3_ssid
      password: !secret wifi2_password
      priority: 50
    - ssid: !secret wifi4_ssid
      password: !secret wifi4_password
      priority: 10
  ap:
    ssid: "Bunt Fallback Hotspot"
    password: !secret wifi10_password
  on_connect:
    then:
      - delay: 2s
      - script.execute: send_program_list

# ---------------------------------------------------------------------------
# ESP-NOW
# ---------------------------------------------------------------------------
espnow:
  auto_add_peer: true
  peers:
    - !secret gelbmac
    - !secret orangemac
  on_receive:
    then:
      - lambda: |-
          std::string data_str((const char*)data, size);
          if (data_str.length() < 2) return;
          std::string prefix = data_str.substr(0, 2);
          std::string val_str = data_str.substr(2);

          if (prefix == "p:") {
            int prog = atoi(val_str.c_str());
            if (prog == -1) {
              id(matrix_light).turn_off().perform();
              id(current_program) = -1;
            } else {
              id(current_program) = prog;
              id(play_program).execute();
            }
          }
          else if (data_str == "r:list") {
            id(send_program_list).execute();
          }
          else if (prefix == "h:") {
            int pct = atoi(val_str.c_str());
            const float MAX_CAP = 0.6f;
            id(max_brightness) = (pct / 100.0f) * MAX_CAP;
            if (id(current_program) >= 0) {
              auto call = id(matrix_light).turn_on();
              call.set_brightness(id(max_brightness));
              call.perform();
            }
          }
          else if (prefix == "k:") {
            id(base_color) = atoi(val_str.c_str());
            if (id(current_program) >= 0) {
              id(play_program).execute();
            }
          }
          else if (prefix == "m:") {
            size_t colon = val_str.find(':');
            if (colon != std::string::npos) {
              id(scroll_variant) = atoi(val_str.substr(0, colon).c_str());
              id(scroll_text)    = val_str.substr(colon + 1);
            }
            id(current_program) = 10;
            id(play_program).execute();
          }
          else if (prefix == "s:") {
            id(scroll_variant) = atoi(val_str.c_str());
          }
          else if (prefix == "x:") {
            id(matrix_light).turn_off().perform();
            id(current_program) = -1;
          }
          // ── Matrix-Breite setzen ──────────────────────────────────────
          else if (prefix == "w:") {
            int w = atoi(val_str.c_str());
            if (w >= 8 && w <= 96) {
              id(matrix_width) = w;
              ESP_LOGI("bunt", "Matrix-Breite: %d", w);
              // Laufendes Programm neu starten damit Größe sofort wirkt
              if (id(current_program) >= 0) {
                id(play_program).execute();
              }
            }
          }

# ---------------------------------------------------------------------------
# GLOBALS
# ---------------------------------------------------------------------------
globals:
  - id: current_program
    type: int
    initial_value: '-1'
  - id: max_brightness
    type: float
    initial_value: '0.6'
  - id: demo_index
    type: int
    initial_value: '0'
  - id: scroll_text
    type: std::string
    initial_value: '"Laufschrift"'
  - id: scroll_variant
    type: int
    initial_value: '1'
  - id: base_color
    type: int
    initial_value: '0'
  # Matrix-Breite wird im NVS gespeichert
  - id: matrix_width
    type: int
    restore_value: true
    initial_value: '32'

# ---------------------------------------------------------------------------
# INTERVAL  –  Demo-Modus
# ---------------------------------------------------------------------------
interval:
  - interval: 30s
    then:
      - lambda: |-
          if (id(current_program) != 8) return;
          const char* effects[] = {
            "Knight Rider", "Regenbogen", "Regenbogen Diagonal", "Feuer",
            "Matrix-Regen", "Farbwellen", "Sternenhimmel", "Lauflicht",
            "Knight Rider Feuer"
          };
          const int NUM_EFFECTS = 9;
          id(demo_index) = (id(demo_index) + 1) % NUM_EFFECTS;
          auto call = id(matrix_light).turn_on();
          call.set_brightness(id(max_brightness));
          call.set_effect(effects[id(demo_index)]);
          call.perform();

# ---------------------------------------------------------------------------
# NEOPIXEL  –  max. 96x8 = 768 LEDs, aktive Breite via matrix_width
# ---------------------------------------------------------------------------
light:
  - platform: esp32_rmt_led_strip
    id: matrix_light
    pin: GPIO32
    num_leds: 768
    chipset: WS2812
    rgb_order: GRB
    name: "Bunt Matrix"
    default_transition_length: 0s
    restore_mode: ALWAYS_OFF

    effects:

      # ── 1. Knight Rider (Basisfarbe) ─────────────────────────────────
      - addressable_lambda:
          name: "Knight Rider"
          update_interval: 30ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static int pos=0, dir=1;
            static float tr[96]={0};
            const int W = id(matrix_width), H = 8;
            // Nicht verwendete Pixel löschen
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for (int x=0;x<W;x++) tr[x]*=0.75f;
            if (pos >= W) pos = W-1;
            tr[pos]=255.0f;
            int bc = id(base_color);
            for(int y=0;y<H;y++) for(int x=0;x<W;x++) {
              uint8_t b=(uint8_t)tr[x]; Color c;
              if      (bc==0) c=Color(b,0,0);
              else if (bc==1) c=Color(0,b,0);
              else if (bc==2) c=Color(0,0,b);
              else            c=Color(b,b/2,0);
              it[XY(x,y,W,H)]=c;
            }
            pos+=dir;
            if(pos>=W-1){pos=W-1;dir=-1;} if(pos<=0){pos=0;dir=1;}

      # ── 2. Regenbogen ─────────────────────────────────────────────────
      - addressable_lambda:
          name: "Regenbogen"
          update_interval: 50ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static uint16_t offset = 0;
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for (int y=0;y<H;y++)
              for (int x=0;x<W;x++)
                it[XY(x,y,W,H)] = ESPHSVColor((uint8_t)((offset + x*8) & 0xFF), 255, 220);
            offset = (offset + 2) & 0xFF;

      # ── 3. Regenbogen Diagonal ────────────────────────────────────────
      - addressable_lambda:
          name: "Regenbogen Diagonal"
          update_interval: 50ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static uint16_t offset = 0;
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for (int y=0;y<H;y++)
              for (int x=0;x<W;x++)
                it[XY(x,y,W,H)] = ESPHSVColor((uint8_t)((offset + x*6 + y*12) & 0xFF), 255, 200);
            offset = (offset + 2) & 0xFF;

      # ── 4. Feuer ───────────────────────────────────────────────────────
      - addressable_lambda:
          name: "Feuer"
          update_interval: 50ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static uint8_t heat[768] = {0};
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for (int i=0;i<W*H;i++) { uint8_t c=esp_random()%3; heat[i]=heat[i]>c?heat[i]-c:0; }
            for (int y=0;y<=H-3;y++) for (int x=0;x<W;x++)
              heat[y*W+x]=(heat[(y+1)*W+x]+heat[(y+2)*W+x]+heat[(y+1)*W+(x>0?x-1:x)])/3;
            for (int x=0;x<W;x++) if((esp_random()%4)<3) heat[(H-1)*W+x]=160+(esp_random()%95);
            for (int y=0;y<H;y++) for (int x=0;x<W;x++) {
              uint8_t h=heat[y*W+x]; Color c;
              if(h<85) c=Color(h*3,0,0); else if(h<170) c=Color(255,(h-85)*3,0); else c=Color(255,255,(h-170)*3);
              it[XY(x,y,W,H)]=c;
            }

      # ── 5. Matrix-Regen (immer grün) ──────────────────────────────────
      - addressable_lambda:
          name: "Matrix-Regen"
          update_interval: 80ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static uint8_t dp[96]={0}, don[96]={0}, br[768]={0};
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for(int i=0;i<W*H;i++) br[i]=br[i]>25?br[i]-25:0;
            for(int x=0;x<W;x++) {
              if(don[x]){br[dp[x]*W+x]=255;dp[x]++;if(dp[x]>=H){dp[x]=0;don[x]=0;}}
              else if((esp_random()%6)==0){don[x]=1;dp[x]=0;}
            }
            for(int y=0;y<H;y++) for(int x=0;x<W;x++)
              it[XY(x,y,W,H)] = Color(0, br[y*W+x], 0);

      # ── 6. Farbwellen ─────────────────────────────────────────────────
      - addressable_lambda:
          name: "Farbwellen"
          update_interval: 50ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static float t=0.0f;
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for(int y=0;y<H;y++) for(int x=0;x<W;x++) {
              float wave=sinf(x*0.35f+t)*sinf(y*0.7f+t*0.6f);
              it[XY(x,y,W,H)]=ESPHSVColor((uint8_t)(((int)(x*7+y*15+t*18))&0xFF),240,(uint8_t)((wave+1.0f)*100.0f)+30);
            }
            t+=0.08f; if(t>1000.0f) t=0.0f;

      # ── 7. Sternenhimmel ───────────────────────────────────────────────
      - addressable_lambda:
          name: "Sternenhimmel"
          update_interval: 80ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static uint8_t br[768]={0}; static int8_t dir[768]={0};
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            if(initial_run) for(int i=0;i<W*H;i++){br[i]=esp_random()%60;dir[i]=(esp_random()%2)?3:-3;}
            for(int y=0;y<H;y++) for(int x=0;x<W;x++) {
              int i=y*W+x; int16_t nb=(int16_t)br[i]+dir[i];
              if(nb>=220){nb=220;dir[i]=-(int8_t)(3+esp_random()%3);}
              if(nb<=0){nb=0;dir[i]=(int8_t)(2+esp_random()%4);if((esp_random()%3)==0)nb=esp_random()%10;}
              br[i]=(uint8_t)nb; uint8_t b=br[i];
              it[XY(x,y,W,H)]=Color(b,b,b<40?0:b);
            }

      # ── 8. Lauflicht (Basisfarbe) ─────────────────────────────────────
      - addressable_lambda:
          name: "Lauflicht"
          update_interval: 40ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static int pos=0; static bool fwd=true; static uint8_t tr[96]={0};
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for(int x=0;x<W;x++) tr[x]=tr[x]>18?tr[x]-18:0;
            if(pos>=W) pos=W-1;
            tr[pos]=255;
            int bc=id(base_color);
            for(int y=0;y<H;y++) for(int x=0;x<W;x++) {
              uint8_t b=tr[x]; Color c;
              if      (bc==0) c=Color(b,0,0);
              else if (bc==1) c=Color(0,b,0);
              else if (bc==2) c=Color(0,0,b);
              else            c=Color(b,b/2,0);
              it[XY(x,y,W,H)]=c;
            }
            if(fwd){pos++;if(pos>=W){pos=W-1;fwd=false;}} else{pos--;if(pos<0){pos=0;fwd=true;}}

      # ── 9. Demo ───────────────────────────────────────────────────────
      - addressable_lambda:
          name: "Demo"
          update_interval: 50ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static uint16_t offset = 0;
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for(int y=0;y<H;y++)
              for(int x=0;x<W;x++)
                it[XY(x,y,W,H)] = ESPHSVColor((uint8_t)((offset + x*8) & 0xFF), 255, 220);
            offset = (offset + 2) & 0xFF;

      # ── 10. Knight Rider Feuer (Basisfarbe) ───────────────────────────
      - addressable_lambda:
          name: "Knight Rider Feuer"
          update_interval: 30ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static int pos=0, dir=1;
            static float tr[8][96]={{0}};
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            for(int y=0;y<H;y++) for(int x=0;x<W;x++){
              if(tr[y][x]>10.0f&&(esp_random()%4)==0) tr[y][x]*=1.1f;
              tr[y][x]*=0.80f; if(tr[y][x]<1.0f) tr[y][x]=0.0f;
            }
            if(pos>=W) pos=W-1;
            for(int y=0;y<H;y++){
              int off=(int)(((float)(H-1-y)/(float)(H-1))*2.0f+0.5f);
              int fx=pos+(dir>0?off:-off); fx=std::max(0,std::min(W-1,fx));
              tr[y][fx]=255.0f; int fx2=fx+dir; if(fx2>=0&&fx2<W) tr[y][fx2]=180.0f;
            }
            int bc=id(base_color);
            for(int y=0;y<H;y++) for(int x=0;x<W;x++){
              uint8_t b=(uint8_t)tr[y][x]; Color c;
              if(bc==0){if(b>200)c=Color(255,200,b-200);else if(b>120)c=Color(255,b-120,0);else c=Color(b,0,0);}
              else if(bc==1){if(b>200)c=Color(200,255,b-200);else if(b>120)c=Color(b-120,255,0);else c=Color(0,b,0);}
              else if(bc==2){if(b>200)c=Color(b-200,200,255);else if(b>120)c=Color(0,b-120,255);else c=Color(0,0,b);}
              else{if(b>200)c=Color(255,255,b-200);else if(b>120)c=Color(255,b-120,0);else c=Color(b,b/3,0);}
              it[XY(x,y,W,H)]=c;
            }
            pos+=dir;
            if(pos>=W-1){pos=W-1;dir=-1;} if(pos<=0){pos=0;dir=1;}

      # ── 11. Laufschrift ───────────────────────────────────────────────
      - addressable_lambda:
          name: "Laufschrift"
          update_interval: 40ms
          lambda: |-
            auto XY = [](int x, int y, int W, int H) -> int {
              if (x % 2 == 0) return x * H + y;
              else            return x * H + (H - 1 - y);
            };
            static const uint8_t F[][5] = {
              {0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5f,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},
              {0x14,0x7f,0x14,0x7f,0x14},{0x24,0x2a,0x7f,0x2a,0x12},{0x23,0x13,0x08,0x64,0x62},
              {0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},{0x00,0x1c,0x22,0x41,0x00},
              {0x00,0x41,0x22,0x1c,0x00},{0x14,0x08,0x3e,0x08,0x14},{0x08,0x08,0x3e,0x08,0x08},
              {0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},
              {0x20,0x10,0x08,0x04,0x02},{0x3e,0x51,0x49,0x45,0x3e},{0x00,0x42,0x7f,0x40,0x00},
              {0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4b,0x31},{0x18,0x14,0x12,0x7f,0x10},
              {0x27,0x45,0x45,0x45,0x39},{0x3c,0x4a,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},
              {0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1e},{0x00,0x36,0x36,0x00,0x00},
              {0x00,0x56,0x36,0x00,0x00},{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},
              {0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},{0x32,0x49,0x79,0x41,0x3e},
              {0x7e,0x11,0x11,0x11,0x7e},{0x7f,0x49,0x49,0x49,0x36},{0x3e,0x41,0x41,0x41,0x22},
              {0x7f,0x41,0x41,0x22,0x1c},{0x7f,0x49,0x49,0x49,0x41},{0x7f,0x09,0x09,0x09,0x01},
              {0x3e,0x41,0x49,0x49,0x7a},{0x7f,0x08,0x08,0x08,0x7f},{0x00,0x41,0x7f,0x41,0x00},
              {0x20,0x40,0x41,0x3f,0x01},{0x7f,0x08,0x14,0x22,0x41},{0x7f,0x40,0x40,0x40,0x40},
              {0x7f,0x02,0x0c,0x02,0x7f},{0x7f,0x04,0x08,0x10,0x7f},{0x3e,0x41,0x41,0x41,0x3e},
              {0x7f,0x09,0x09,0x09,0x06},{0x3e,0x41,0x51,0x21,0x5e},{0x7f,0x09,0x19,0x29,0x46},
              {0x46,0x49,0x49,0x49,0x31},{0x01,0x01,0x7f,0x01,0x01},{0x3f,0x40,0x40,0x40,0x3f},
              {0x1f,0x20,0x40,0x20,0x1f},{0x3f,0x40,0x38,0x40,0x3f},{0x63,0x14,0x08,0x14,0x63},
              {0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43},{0x00,0x7f,0x41,0x41,0x00},
              {0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x7f,0x00},{0x04,0x02,0x01,0x02,0x04},
              {0x40,0x40,0x40,0x40,0x40},{0x00,0x01,0x02,0x04,0x00},{0x20,0x54,0x54,0x54,0x78},
              {0x7f,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},{0x38,0x44,0x44,0x48,0x7f},
              {0x38,0x54,0x54,0x54,0x18},{0x08,0x7e,0x09,0x01,0x02},{0x0c,0x52,0x52,0x52,0x3e},
              {0x7f,0x08,0x04,0x04,0x78},{0x00,0x44,0x7d,0x40,0x00},{0x20,0x40,0x44,0x3d,0x00},
              {0x7f,0x10,0x28,0x44,0x00},{0x00,0x41,0x7f,0x40,0x00},{0x7c,0x04,0x18,0x04,0x78},
              {0x7c,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},{0x7c,0x14,0x14,0x14,0x08},
              {0x08,0x14,0x14,0x18,0x7c},{0x7c,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20},
              {0x04,0x3f,0x44,0x40,0x20},{0x3c,0x40,0x40,0x20,0x7c},{0x1c,0x20,0x40,0x20,0x1c},
              {0x3c,0x40,0x30,0x40,0x3c},{0x44,0x28,0x10,0x28,0x44},{0x0c,0x50,0x50,0x50,0x3c},
              {0x44,0x64,0x54,0x4c,0x44},{0x00,0x08,0x36,0x41,0x00},{0x00,0x00,0x7f,0x00,0x00},
              {0x00,0x41,0x36,0x08,0x00},{0x10,0x08,0x08,0x10,0x08},
              {0x20,0x55,0x54,0x55,0x78},{0x38,0x45,0x44,0x45,0x38},{0x3c,0x41,0x40,0x21,0x7c},
              {0x7e,0x12,0x11,0x12,0x7e},{0x3e,0x42,0x41,0x42,0x3e},{0x3f,0x41,0x40,0x41,0x3f},
              {0x7e,0x09,0x49,0x36,0x00},
            };
            const int W = id(matrix_width), H = 8;
            for (int i = W*H; i < 768; i++) it[i] = Color(0,0,0);
            static std::string last_txt = "";
            static int scroll_pos = 32;
            std::string& txt = id(scroll_text);
            int var = id(scroll_variant);
            if (txt != last_txt) { last_txt = txt; scroll_pos = W; }
            int char_count = 0;
            for (int i = 0; i < (int)txt.size(); i++) {
              uint8_t b = (uint8_t)txt[i];
              if (b == 0xC3 && i+1 < (int)txt.size()) { i++; char_count++; }
              else if (b >= 0x20 && b <= 0x7e) char_count++;
            }
            int txt_px = char_count * 6;
            it.all() = Color(0, 0, 0);
            Color tc;
            if      (var==0) tc=Color(0,100,255);
            else if (var==1) tc=Color(0,255,0);
            else if (var==2) tc=Color(255,140,0);
            else             tc=Color(255,0,0);
            int xc = scroll_pos;
            for (int i = 0; i < (int)txt.size() && xc < W; i++) {
              uint8_t b = (uint8_t)txt[i];
              int fi = 0;
              if (b == 0xC3 && i+1 < (int)txt.size()) {
                uint8_t n = (uint8_t)txt[++i];
                if(n==0xA4)fi=95; else if(n==0xB6)fi=96; else if(n==0xBC)fi=97;
                else if(n==0x84)fi=98; else if(n==0x96)fi=99; else if(n==0x9C)fi=100;
                else if(n==0x9F)fi=101; else fi=0x3f-0x20;
              } else if (b>=0x20&&b<=0x7e) { fi=b-0x20; } else continue;
              for (int col=0;col<5;col++) {
                int px=xc+col;
                if (px>=0&&px<W) {
                  uint8_t cd=F[fi][col];
                  for (int row=0;row<7;row++)
                    if(cd&(1<<row)) it[XY(px,row,W,H)]=tc;
                }
              }
              xc+=6;
            }
            scroll_pos--;
            if (scroll_pos < -(txt_px+4)) scroll_pos = W;

# ---------------------------------------------------------------------------
# SCRIPT: Programmliste an Gelb senden
# ---------------------------------------------------------------------------
script:
  - id: send_program_list
    then:
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:0:Knight Rider";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:0:Knight Rider";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:1:Regenbogen";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:1:Regenbogen";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:2:Regenbogen Diagonal";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:2:Regenbogen Diagonal";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:3:Feuer";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:3:Feuer";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:4:Matrix-Regen";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:4:Matrix-Regen";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:5:Farbwellen";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:5:Farbwellen";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:6:Sternenhimmel";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:6:Sternenhimmel";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:7:Lauflicht";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:7:Lauflicht";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:8:Demo";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:8:Demo";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:9:Knight Rider Feuer";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:9:Knight Rider Feuer";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:10:Laufschrift";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:10:Laufschrift";
            return std::vector<uint8_t>(s.begin(), s.end());
      - delay: 200ms
      - espnow.send:
          address: !secret gelbmac
          data: !lambda |-
            std::string s = "n:END";
            return std::vector<uint8_t>(s.begin(), s.end());
      - espnow.send:
          address: !secret orangemac
          data: !lambda |-
            std::string s = "n:END";
            return std::vector<uint8_t>(s.begin(), s.end());

  - id: play_program
    then:
      - lambda: |-
          auto call = id(matrix_light).turn_on();
          call.set_brightness(id(max_brightness));
          switch (id(current_program)) {
            case 0:  call.set_effect("Knight Rider");        break;
            case 1:  call.set_effect("Regenbogen");          break;
            case 2:  call.set_effect("Regenbogen Diagonal"); break;
            case 3:  call.set_effect("Feuer");               break;
            case 4:  call.set_effect("Matrix-Regen");        break;
            case 5:  call.set_effect("Farbwellen");          break;
            case 6:  call.set_effect("Sternenhimmel");       break;
            case 7:  call.set_effect("Lauflicht");           break;
            case 8:
              id(demo_index) = 0;
              call.set_effect("Knight Rider");
              break;
            case 9:  call.set_effect("Knight Rider Feuer"); break;
            case 10: call.set_effect("Laufschrift");        break;
            default:
              id(matrix_light).turn_off().perform();
              return;
          }
          call.perform();

Diese Datei wird dann vom Programm ESPHome in die Sprache des M5 übersetzt (–> bunt.bin) und muss dann auf diesen geflasht werden. Aber keine Sorge, wenn man es richtig konfiguriert funktioniert das ganz einfach übers WLAN.

bunt.bin bunt.bin — 914.2 KB

Sicherheit – was man beachten muss

⚠️ Wichtig für junge Maker: Das Netzteil arbeitet mit 230V Wechselstrom – das ist lebensgefährlich! Den 230V-Teil immer von einem Erwachsenen anschließen lassen. Die 5V-Seite (die LEDs und der ESP32) sind dagegen völlig harmlos.

Noch etwas: LEDs können sehr hell werden und viel Strom brauchen. Mein Programm begrenzt die maximale Helligkeit auf 60% – das schützt das Netzteil und spart Strom. Bei voller Weißhelligkeit würde die Matrix über 30 Watt verbrauchen!

Strommessung – wie viel Strom braucht das?

Ich habe den Stromverbrauch der verschiedenen Programme genau gemessen. Das Ergebnis war überraschend: Die animierten Programme (Knight Rider, Lauflicht) brauchen viel weniger Strom als eine statische weiße Farbe – weil immer nur ein Teil der LEDs leuchtet!

🌐 Stromverbrauch aller Programme
▶ Klicken zum Öffnen

Was man noch damit machen kann

Das ist erst der Anfang! Mit diesem Setup kann man zum Beispiel:

  • Home Automation: Die Matrix leuchtet rot wenn die Wohnungstür offen ist
  • Benachrichtigungen: “Waschmaschine fertig!” scrollt als Text über die Matrix
  • Wecker: Morgens langsam aufhellen wie ein Sonnenaufgang
  • Spiele: Einfache Pixel-Spiele auf der Matrix programmieren
  • Wetterstation: Farbe zeigt an ob es regnet oder die Sonne scheint

Das Tolle an ESPHome und Home Assistant ist: Man kann das alles ohne tiefe Programmierkenntnisse machen. Man beschreibt einfach was passieren soll, und das System kümmert sich um das wie.

Fazit

Für ca. 36 Euro und ein Wochenende Bastelzeit bekommt man ein Licht, das man beliebig programmieren kann. Kein fertiges Produkt, sondern etwas, das genau so funktioniert wie man es selbst will – und das man immer weiter verbessern kann.

Der nächste Schritt für mich: Eine zweite und dritte Matrix, verteilt in verschiedenen Zimmern, alle gesteuert von einer einzigen Bedieneinheit. Aber das ist eine andere Geschichte…

Alle drei Geräte
Alle drei Geräte