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!
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.
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?
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!
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 (–).
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!
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.
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 |
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:
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.
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!
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…