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.

Das fertige Blau-Bedienfeld von oben
Das fertige Blau-Bedienfeld von oben

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”.

Rotary Encoder – der Endlos-Drehknopf
Rotary Encoder – der Endlos-Drehknopf

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”).

PCF8574 Breakout-Board mit beschrifteten Pins
PCF8574 Breakout-Board mit beschrifteten Pins

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.

WAGO 221-415 Klemmen – orange Hebel öffnen, Kabel rein, Hebel schließen
WAGO 221-415 Klemmen – orange Hebel öffnen, Kabel rein, Hebel schließen

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.

Das kann alles wiederverwertet werden 😉
Das kann alles wiederverwertet werden 😉

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
Alle Taster am PCF8574 angeschlossen
Alle Taster am PCF8574 angeschlossen

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!

Farbtaster ändern die Basisfarbe des Knight Rider

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:

  1. Der ESP32 erkennt: “Encoder wurde 1 Schritt nach rechts gedreht”
  2. Er berechnet die neue Helligkeit: “war 60%, jetzt 65%”
  3. Er sendet per ESP-NOW: h:65 an das Gelb-Display
  4. Gelb empfängt und leitet weiter: h:65 an Bunt
  5. 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.

blau.yaml 308 Zeilen ⬇ Download
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.