In meinem ersten Post habe ich erklärt, wie ich eine LED-Matrix mit einem winzigen ESP32-Computer zum Leuchten gebracht habe. Das war toll – aber jedes Mal zum Handy greifen um das Licht zu wechseln ist auf Dauer nervig. Deshalb habe ich ein richtiges Bedienfeld gebaut: “Blau”.
Was ist Blau?
Blau ist ein zweiter ESP32, der nur eine Aufgabe hat: auf Knöpfe und Drehknöpfe hören und Befehle an die LED-Matrix schicken. Kein Display, keine LEDs – nur Bedienung. Dafür aber mit allem was das Bastlerherz begehrt: zwei Endlos-Drehknöpfe, sechs bunte Taster, und ein cleverer Chip der aus 2 Drähten 8 Eingänge macht.
Die Einkaufsliste
| Teil | Wozu? | Ungefährer Preis |
|---|---|---|
| ESP32-WROOM32 Entwicklungsboard | Der Computer | ~5 € |
| 2× Rotary Encoder mit Taster | Drehknöpfe | ~3 € |
| PCF8574 Breakout-Board | 8 zusätzliche Eingänge | ~2 € |
| Taster in Rot, Grün, Blau, Gelb, Weiß, Schwarz | Bunte Knöpfe | ~4 € |
| WAGO 221-415 Klemmen (5-Leiter) | Kabelverbindungen ohne Löten | ~3 € |
| Gehäuse, Kabel | Gehäuse und Verbindungen | ~5 € |
Gesamt: ca. 22 €
Die drei spannenden Bauteile
Rotary Encoder – der Endlos-Drehknopf
Ein Rotary Encoder sieht aus wie ein normaler Drehknopf, funktioniert aber völlig anders. Ein normales Poti (Potentiometer) ändert einen Widerstandswert je nach Position – man kann es also nicht endlos drehen. Ein Rotary Encoder dagegen dreht sich endlos und meldet dem Computer bei jedem Klick: “ich wurde einen Schritt nach rechts gedreht” oder “einen Schritt nach links”.
Wie funktioniert das? Innen sind zwei Kontakte, die beim Drehen abwechselnd schließen. Je nachdem welcher zuerst schließt, weiß der Computer in welche Richtung gedreht wurde. Außerdem hat jeder Encoder einen eingebauten Taster – man kann ihn auch drücken!
Ich habe zwei Encoder:
- Encoder Oben: steuert die Helligkeit der LEDs
- Encoder Unten: wechselt das aktuelle Lichtprogramm
PCF8574 – der I2C-Porterweiterter
Hier kommt etwas wirklich Cleveres! Ein ESP32 hat viele Anschluss-Pins, aber wenn man viele Taster anschließen will, werden sie schnell knapp. Die Encoder brauchen bereits 4 Pins (2 pro Encoder) – und dann kommen noch 6 Taster dazu…
Die Lösung heißt PCF8574. Das ist ein kleiner Chip, der 8 zusätzliche Ein-/Ausgänge über nur 2 Drähte bereitstellt! Diese zwei Drähte nennt man I2C-Bus (gesprochen “I-Quadrat-C”).
Wie funktioniert I2C? Stell dir vor, du hast ein Haus mit vielen Zimmern, aber nur einen Korridor. Jedes Zimmer hat eine Nummer (die “Adresse”). Wenn du mit Zimmer 0x20 sprechen willst, rufst du in den Korridor: “Hey 0x20, was ist dein aktueller Zustand?” Zimmer 0x20 antwortet, und alle anderen Zimmer ignorieren die Frage. So kann man viele Geräte über nur 2 Drähte verbinden!
Mein PCF8574 hat die Adresse 0x20 (eine Hexadezimalzahl – das “0x” zeigt an, dass es keine normale Dezimalzahl ist).
Warum können die Encoder-Taster nicht auch am PCF8574 hängen?
Encoder drehen sich sehr schnell – zwischen zwei Abfragen über I2C könnten Signale verloren gehen. Direkte GPIO-Pins reagieren sofort auf jede Änderung. Taster dagegen sind langsam und funktionieren problemlos über den PCF8574.
WAGO-Klemmen – Verbinden ohne Löten
Bei diesem Projekt laufen viele Kabel zusammen: der ESP32, die zwei Encoder, der PCF8574, sechs Taster – alle brauchen 3,3V und GND. Das wären sehr viele Lötstellen, und ein Fehler dabei kann das ganze Projekt lahmlegen.
Die Lösung: WAGO 221-Klemmen. Das sind kleine orangefarbene Verbinder, in die man Kabel einfach einklemmt – kein Löten, kein Werkzeug. Man klappt den orangen Hebel hoch, steckt das Kabel rein, klappt zu. Fertig. Und das Beste: Man kann die Verbindung jederzeit wieder öffnen wenn man etwas ändern will!
Ich verwende die 221-415 (2/3/5-Leiter-Varianten): daran klemme ich 3,3V vom ESP32 an und verteile es von dort auf alle anderen Bauteile. Eine Klemme für Plus, eine für Minus – und schon ist die Stromversorgung für alle Komponenten geregelt.
WAGO vs. Löten: Für ein Projekt das noch in Entwicklung ist – also wo man noch ausprobiert, umbaut, Fehler sucht – sind WAGO-Klemmen unschlagbar. Für ein fertig eingeplantes Gerät das nie mehr aufgemacht wird, ist Löten langfristig zuverlässiger. Für mein Bedienfeld: WAGO ohne Frage.
Pin-Belegung – was hängt wo?
Direkt am ESP32
| GPIO-Pin | Angeschlossen an |
|---|---|
| GPIO26 | Encoder Oben – Signal A |
| GPIO25 | Encoder Oben – Signal B |
| GPIO33 | Encoder Unten – Signal A |
| GPIO32 | Encoder Unten – Signal B |
| GPIO21 | I2C Datenleitung (SDA) zum PCF8574 |
| GPIO22 | I2C Taktleitung (SCL) zum PCF8574 |
Über den PCF8574
| P-Pin | Taster |
|---|---|
| P0 | Taster Rot |
| P1 | Taster Grün |
| P2 | Taster Blau |
| P3 | Taster Gelb |
| P4 | Taster Weiß |
| P5 | Taster Schwarz |
| P6 | Encoder Unten Taster |
| P7 | Encoder Oben Taster |
Zusammenbauen – Schritt für Schritt
1. Stromversorgung mit WAGO verteilen
Als erstes klemme ich je eine WAGO 221-415 für 3,3V und GND ab. Der ESP32 liefert am 3V3-Pin genug Strom für alle Bauteile. Von der WAGO-Klemme geht dann je ein Kabel zu jedem Bauteil das Strom braucht: PCF8574, Encoder (die brauchen keinen eigenen Strom, aber die Pull-up-Widerstände nutzen 3,3V), Taster-LED falls vorhanden.
2. Encoder anschließen
Jeder Encoder hat 5 Pins: GND, +, SW (Taster), DT und CLK. GND und + kommen von der WAGO-Klemme. SW kommt an den PCF8574. DT und CLK kommen direkt an die GPIO-Pins des ESP32 – wegen der Geschwindigkeit (siehe oben).
3. PCF8574 anschließen
SDA → GPIO21, SCL → GPIO22, VCC → WAGO 3,3V, GND → WAGO GND. Dann die 6 Farbtaster und die 2 Encoder-Taster an P0–P7. Jeder Taster hat ein Ende am PCF8574-Pin, das andere Ende an GND.
4. Testen vor dem Einbauen
Bevor alles ins Gehäuse kommt: Firmware flashen und jeden Knopf einzeln testen! Nichts ist ärgerlicher als ein schlecht erreichbarer Wackelkontakt nach dem Zusammenbau.
Was machen die Knöpfe?
| Bedienelement | Aktion |
|---|---|
| Encoder Oben drehen | Helligkeit ändern (sofort sichtbar!) |
| Encoder Unten drehen | Lichtprogramm wechseln |
| Taster Rot/Grün/Blau/Gelb | Basisfarbe setzen |
| Taster Weiß (kurz) | Seite auf dem Display wechseln |
| Taster Weiß (2 Sek. halten) | Display um 180° drehen |
| Taster Schwarz | Alles aus |
Was ist eine Basisfarbe? Manche Lichtprogramme wie Knight Rider oder Lauflicht können in verschiedenen Farben leuchten. Mit den Farbtastern wählt man die Farbe aus – drückt man Blau, fährt der Knight Rider in Blau hin und her!
Wie spricht Blau mit Bunt?
Hier kommt wieder ESP-NOW ins Spiel, das ich schon im ersten Post erklärt habe. Wenn ich am Encoder Oben drehe, passiert folgendes:
- Der ESP32 erkennt: “Encoder wurde 1 Schritt nach rechts gedreht”
- Er berechnet die neue Helligkeit: “war 60%, jetzt 65%”
- Er sendet per ESP-NOW:
h:65an das Gelb-Display - Gelb empfängt und leitet weiter:
h:65an Bunt - Bunt setzt sofort die neue Helligkeit
Das alles passiert in weniger als einer Millisekunde – für uns Menschen fühlt es sich absolut sofort an!
Das Besondere: Blau sendet nicht direkt an Bunt, sondern immer über Gelb. So weiß das Display immer was gerade eingestellt ist und kann es anzeigen.
Langer Tastendruck – ein cleverer Trick
Manchmal will man einem Knopf zwei verschiedene Funktionen geben. Bei meinem weißen Taster:
- Kurz drücken → Seite auf dem Display wechseln
- 2 Sekunden halten → Display drehen
Wie funktioniert das? Beim Drücken speichert der ESP32 die aktuelle Uhrzeit. Beim Loslassen schaut er wie lange es her ist:
beim Drücken:
merke aktuelle Zeit
beim Loslassen:
berechne: (jetzt - gemerkte Zeit)
wenn kürzer als 2 Sekunden:
Tab wechseln
wenn länger als 2 Sekunden:
Display drehen
So einfach kann Programmieren sein!
Die Firmware
Die Konfigurationsdatei für Blau ist viel kürzer als die von Bunt, weil Blau keine komplizierten LED-Animationen braucht – nur Eingaben lesen und Nachrichten schicken.
esphome:
name: blau
friendly_name: Blau
esp32:
board: esp32dev
framework:
type: esp-idf
# ---------------------------------------------------------------------------
# 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: "NeoPixel-Clock Fallback"
password: !secret wifi1_password
logger:
level: DEBUG
captive_portal:
api:
encryption:
key: !secret blaukey
ota:
- platform: esphome
password: !secret blaupassword
# ---------------------------------------------------------------------------
# I2C – PCF8574
# ---------------------------------------------------------------------------
i2c:
sda: GPIO21
scl: GPIO22
pcf8574:
- id: pcf8574_hub
address: 0x20
pcf8575: false
# ---------------------------------------------------------------------------
# ESP-NOW
# ---------------------------------------------------------------------------
espnow:
peers:
- !secret gelbmac
# ---------------------------------------------------------------------------
# GLOBALS
# ---------------------------------------------------------------------------
globals:
- id: brightness_pct
type: int
initial_value: '100'
- id: weiss_press_time
type: uint32_t
initial_value: '0'
# ---------------------------------------------------------------------------
# ENCODER OBEN – Helligkeit
# ---------------------------------------------------------------------------
sensor:
- platform: rotary_encoder
id: enc_oben
name: "Encoder Oben"
pin_a:
number: GPIO26
mode: INPUT_PULLUP
pin_b:
number: GPIO25
mode: INPUT_PULLUP
resolution: 1
on_clockwise:
then:
- lambda: |-
id(brightness_pct) = std::min(100, id(brightness_pct) + 5);
- espnow.send:
address: !secret gelbmac
data: !lambda |-
static std::string s;
s = "h:" + std::to_string(id(brightness_pct));
return std::vector<uint8_t>(s.begin(), s.end());
on_anticlockwise:
then:
- lambda: |-
id(brightness_pct) = std::max(0, id(brightness_pct) - 5);
- espnow.send:
address: !secret gelbmac
data: !lambda |-
static std::string s;
s = "h:" + std::to_string(id(brightness_pct));
return std::vector<uint8_t>(s.begin(), s.end());
# ---------------------------------------------------------------------------
# ENCODER UNTEN – Menü scrollen
# ---------------------------------------------------------------------------
- platform: rotary_encoder
id: enc_unten
name: "Encoder Unten"
pin_a:
number: GPIO33
mode: INPUT_PULLUP
pin_b:
number: GPIO32
mode: INPUT_PULLUP
resolution: 1
on_clockwise:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "v:+1";
return std::vector<uint8_t>(s.begin(), s.end());
on_anticlockwise:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "v:-1";
return std::vector<uint8_t>(s.begin(), s.end());
# ---------------------------------------------------------------------------
# SCRIPTS – ESP-NOW Hilfsskripte für Taster Weiß
# ---------------------------------------------------------------------------
script:
- id: espnow_send_tab
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "b:1";
return std::vector<uint8_t>(s.begin(), s.end());
- id: espnow_send_flip
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "f:flip";
return std::vector<uint8_t>(s.begin(), s.end());
# ---------------------------------------------------------------------------
# TASTER
# ---------------------------------------------------------------------------
binary_sensor:
# ── Encoder Oben Taster – Reserve ───────────────────────────────────
- platform: gpio
name: "Encoder Oben Taster"
pin:
pcf8574: pcf8574_hub
number: 7
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- lambda: 'ESP_LOGI("blau", "Encoder Oben Taster: Reserve");'
# ── Encoder Unten Taster – Reserve ──────────────────────────────────
- platform: gpio
name: "Encoder Unten Taster"
pin:
pcf8574: pcf8574_hub
number: 6
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- lambda: 'ESP_LOGI("blau", "Encoder Unten Taster: Reserve");'
# ── Taster Rot – Basisfarbe Rot ─────────────────────────────────────
- platform: gpio
name: "Taster Rot"
pin:
pcf8574: pcf8574_hub
number: 0
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "k:0";
return std::vector<uint8_t>(s.begin(), s.end());
# ── Taster Grün – Basisfarbe Grün ───────────────────────────────────
- platform: gpio
name: "Taster Gruen"
pin:
pcf8574: pcf8574_hub
number: 1
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "k:1";
return std::vector<uint8_t>(s.begin(), s.end());
# ── Taster Blau – Basisfarbe Blau ───────────────────────────────────
- platform: gpio
name: "Taster Blau"
pin:
pcf8574: pcf8574_hub
number: 2
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "k:2";
return std::vector<uint8_t>(s.begin(), s.end());
# ── Taster Gelb – Basisfarbe Gelb/Orange ────────────────────────────
- platform: gpio
name: "Taster Gelb"
pin:
pcf8574: pcf8574_hub
number: 3
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "k:3";
return std::vector<uint8_t>(s.begin(), s.end());
# ── Taster Weiß – kurz = Tab wechseln, lang (2s) = Display drehen ──
- platform: gpio
name: "Taster Weiss"
pin:
pcf8574: pcf8574_hub
number: 4
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- lambda: 'id(weiss_press_time) = millis();'
on_release:
then:
- lambda: |-
uint32_t held = millis() - id(weiss_press_time);
ESP_LOGI("blau", "Weiss losgelassen nach %d ms", (int)held);
if (held >= 2000) {
ESP_LOGI("blau", "→ sende f:flip");
id(espnow_send_flip).execute();
} else {
ESP_LOGI("blau", "→ sende b:1");
id(espnow_send_tab).execute();
}
# ── Taster Schwarz – Alles aus ──────────────────────────────────────
- platform: gpio
name: "Taster Schwarz"
pin:
pcf8574: pcf8574_hub
number: 5
mode: INPUT
inverted: true
filters:
- delayed_on: 50ms
on_press:
then:
- espnow.send:
address: !secret gelbmac
data: !lambda |-
std::string s = "x:0";
return std::vector<uint8_t>(s.begin(), s.end());
Was ich dabei gelernt habe
WAGO spart Nerven. Beim ersten Aufbau hatte ich alles gelötet – und nach dem dritten Umbau war ich froh, auf WAGO umgestiegen zu sein. Der Zeitvorteil beim Debuggen ist enorm.
I2C ist überall. Das Konzept “viele Geräte, zwei Drähte, jeder hat eine Adresse” findet man nicht nur beim PCF8574 – Temperatursensoren, Displays, Uhrenchips, alles nutzt I2C. Wer es einmal verstanden hat, baut damit für immer.
Testen vor dem Einbauen. Klingt selbstverständlich, habe ich beim ersten Mal trotzdem ignoriert. Die Stunde Fehlersuche danach hat mich es nie wieder vergessen lassen.
Fazit und Ausblick
Mit Blau habe ich gelernt, dass man nicht alles direkt an den Hauptcomputer anschließen muss. Der I2C-Bus erlaubt es, mit nur 2 Drähten viele Geräte anzusteuern – ein Konzept das man überall in der Elektronik wiederfindet. Und WAGO-Klemmen machen das Ganze so zugänglich, dass man sich wirklich auf das Wesentliche konzentrieren kann: das Gerät zum Laufen zu bringen.
Im nächsten Post zeige ich das dritte Gerät meines Systems: Gelb – ein Gerät mit Touchscreen-Display, das alles zusammenhält und auch mit Home Assistant verbunden ist.