| .vscode | ||
| data | ||
| include | ||
| src | ||
| .gitignore | ||
| LICENSE | ||
| partitions.csv | ||
| platformio.ini | ||
| README.md | ||
Sulzi Lightcontroller
Firmware fuer ein Waveshare ESP32-S3-ETH-8DI-8RO, um mit den 8 Relais potentialfreie Tastendruecke per MQTT zu emulieren. Die Netzwerkverbindung laeuft ausschliesslich ueber Ethernet mit statischer IPv4-Adresse.
Die Firmware ist fuer VS Code + PlatformIO aufgebaut und nutzt Arduino-ESP32 3.x, weil Waveshare fuer das Board Arduino 3.0.0+ nennt und der W5500-Ethernet-Support dort sauber verfuegbar ist.
Aktueller Stand:
- Ethernet only, statische IPv4, kein WLAN.
- MQTT-Steuerung fuer 8 Relaiskanaele.
- Es wird immer nur ein Relais gleichzeitig geschaltet.
- Pulszeit, Totzeit, MQTT, Netzwerk, NTP und Kanalnamen kommen aus
data/config.json. - NTP-Zeitstempel mit deutscher Zeitzone in
stateundevent. - Status-LED auf
GPIO38mit RGB-FarbreihenfolgeRGB. - OTA ist durch die Partitionierung vorbereitet, aber noch nicht als Funktion eingebaut.
Hardwarebasis
Verwendete Boarddaten:
- Relais: EXIO1 bis EXIO8 ueber TCA9554/PCA9554-IO-Expander.
- TCA9554-Adresse:
0x20. - I2C: SDA
GPIO42, SCLGPIO41. - Ethernet W5500: INT
GPIO12, MOSIGPIO13, MISOGPIO14, SCLKGPIO15, CSGPIO16. - RGB-Status-LED:
GPIO38.
Quellen: Waveshare Wiki und die oeffentliche ESPHome-Referenzkonfiguration.
Projektstruktur
platformio.ini: PlatformIO-Projektdefinition fuer ESP32-S3 + Arduino-ESP32 3.x.partitions.csv: Zwei OTA-App-Slots plus LittleFS-Partition fuer die Config.data/config.json: Lokale Geraetekonfiguration. Diese Datei ist dein produktives Setup.data/config.example.json: Beispielkonfiguration fuer neue Installationen.src/main.cpp: Netzwerk, MQTT, NTP und Hauptlogik.src/RelayController.cpp: Relais-Queue, Pulszeit, Totzeit und Fehlerzustand.src/StatusLed.cpp: Status-LED auf GPIO38.
Inbetriebnahme in VS Code
-
VS Code oeffnen und die PlatformIO-Erweiterung installieren.
-
data/config.jsonanpassen: MQTT-Broker, Topic-Basis, Zeiten, Kanalnamen. Die IP-Konfiguration besteht nur ausnetwork.ip,network.gatewayundnetwork.subnet; DNS wird nicht gesetzt.mqtt.hostmuss deshalb ebenfalls eine IPv4-Adresse sein. Fuer menschenlesbare Zeitstempeltime.ntp_serverauf einen lokalen NTP-Server oder eine erreichbare NTP-IP setzen. Standard-Zeitzone ist Deutschland. -
Dateisystem flashen:
.\.venv\Scripts\platformio.exe run -t uploadfs -
Firmware flashen:
.\.venv\Scripts\platformio.exe run -t upload -
Seriellen Monitor oeffnen:
.\.venv\Scripts\platformio.exe device monitor
Wenn PlatformIO global installiert ist, funktionieren dieselben Befehle auch mit pio statt .\.venv\Scripts\platformio.exe.
Falls deine PlatformIO-Installation inzwischen offiziell Arduino-ESP32 3.x nutzt, kannst du in platformio.ini die platform-Zeile auch auf platformio/espressif32 umstellen. Die aktuelle Konfiguration nutzt den pioarduino-Fork, damit W5500 + ESP32-S3 unter PlatformIO verlaesslich zusammenkommen.
Build und Upload
Firmware bauen:
.\.venv\Scripts\platformio.exe run
LittleFS-Image fuer data/config.json bauen:
.\.venv\Scripts\platformio.exe run -t buildfs
Nur Firmware flashen:
.\.venv\Scripts\platformio.exe run -t upload
Config/LittleFS flashen:
.\.venv\Scripts\platformio.exe run -t uploadfs
Wichtig: upload aktualisiert nur die Firmware und laesst die Config im LittleFS normalerweise unangetastet. uploadfs schreibt die Dateien aus data/ neu auf den Controller und ueberschreibt damit die dort gespeicherte config.json.
Die erzeugten Dateien liegen unter:
.pio/build/waveshare_esp32s3_eth_8di_8ro/firmware.bin
.pio/build/waveshare_esp32s3_eth_8di_8ro/firmware.factory.bin
.pio/build/waveshare_esp32s3_eth_8di_8ro/littlefs.bin
Fuer normale Firmware-Updates ist firmware.bin relevant. firmware.factory.bin ist fuer einen kompletten Erstflash inklusive Bootloader/Partitionen gedacht.
Konfiguration
Die Firmware liest beim Booten /config.json aus LittleFS. Dadurch koennen IP, MQTT, Zeiten und Kanalnamen angepasst werden, ohne den C++-Code zu aendern.
Minimal wichtige Bereiche:
{
"device": {
"id": "fbslightcontroller",
"hostname": "fbslightcontroller"
},
"network": {
"ip": "192.168.0.50",
"gateway": "192.168.0.1",
"subnet": "255.255.255.0"
},
"mqtt": {
"host": "192.168.0.10",
"base_topic": "sulzi/lightcontroller/relaybox01"
},
"time": {
"enabled": true,
"ntp_server": "192.53.103.108",
"timezone": "CET-1CEST,M3.5.0/2,M10.5.0/3"
}
}
Da DNS bewusst nicht konfiguriert wird, muessen mqtt.host und am besten auch time.ntp_server als IP-Adresse angegeben werden.
Zeitkonfiguration
Die Firmware synchronisiert nach dem Ethernet-Link die Uhr per NTP. Da DNS bewusst nicht konfiguriert wird, sollte time.ntp_server eine IP-Adresse oder ein lokal direkt aufloesbarer Server sein. Viele Router stellen NTP unter der Gateway-IP bereit; deshalb ist der Default:
"time": {
"enabled": true,
"ntp_server": "192.168.1.1",
"timezone": "CET-1CEST,M3.5.0/2,M10.5.0/3",
"sync_check_interval_ms": 30000
}
Die Zeitzone ist als POSIX-TZ-String fuer Deutschland gesetzt: MEZ/MESZ inklusive automatischer Sommerzeitumstellung. Bis zur ersten erfolgreichen NTP-Synchronisation enthalten MQTT-Nachrichten time_synced: false; danach werden unix_time, timestamp und timestamp_local mitgesendet.
Bei erfolgreicher Synchronisation wird ausserdem ein Event time_synced publiziert.
MQTT Topics
Bei mqtt.base_topic = sulzi/lightcontroller/relaybox01:
sulzi/lightcontroller/relaybox01/availabilityretained:online/offlinesulzi/lightcontroller/relaybox01/stateretained: JSON-Gesamtstatussulzi/lightcontroller/relaybox01/event: Ereignisse wiecommand_accepted,press_started,press_finished,command_rejectedsulzi/lightcontroller/relaybox01/command: zentrales Command-Topicsulzi/lightcontroller/relaybox01/channel/1/commandbis/channel/8/commandsulzi/lightcontroller/relaybox01/channel/1/statebis/channel/8/state:ONwaehrend des Tastendrucks, sonstOFF
Einfachste Node-RED-Ansteuerung
Fuer Node-RED ist pro Taste ein eigenes Topic am einfachsten. Ein MQTT-Out-Node sendet dann nur einen kurzen Text:
Topic: sulzi/lightcontroller/relaybox01/channel/1/command
Payload: PRESS
Das loest Kanal 1 mit der in data/config.json hinterlegten Pulszeit aus. Fuer die anderen Kanaele wird nur die Kanalnummer im Topic geaendert:
sulzi/lightcontroller/relaybox01/channel/2/command
...
sulzi/lightcontroller/relaybox01/channel/8/command
Akzeptierte einfache Payloads fuer einen Tastendruck:
PRESS
PULSE
ON
1
TRUE
Die Text-Payloads werden ohne Beachtung der Gross-/Kleinschreibung ausgewertet.
Zentrales Command-Topic
Alternativ gibt es ein zentrales Topic:
sulzi/lightcontroller/relaybox01/command
Hier muss der Kanal in der JSON-Payload stehen:
{"channel":3,"action":"press"}
Optional kann die Pulszeit fuer genau diesen Tastendruck ueberschrieben werden:
{"channel":3,"action":"press","pulse_ms":300,"request_id":"node-red-123"}
Akzeptierte JSON-Felder:
actionodercommand:press,pulse,on,all_off,off,stop,abort,status,statechannel:1bis8pulse_ms: Schaltzeit in Millisekunden, alternativduration_msodertime_msrequest_id: frei waehlbare ID fuer Rueckverfolgung in Events, alternativid
Sonderkommandos
Auf dem zentralen Command-Topic und den Kanal-Command-Topics:
STATUS
STATE
publiziert den aktuellen Status erneut.
ALL_OFF
OFF
0
FALSE
schaltet alle Relais aus und leert die Queue. Die Firmware haelt danach trotzdem die konfigurierte Totzeit ein.
State Topics
Der Gesamtstatus wird retained auf diesem Topic publiziert:
sulzi/lightcontroller/relaybox01/state
Beispielstruktur:
{
"device_id": "sulzi-lightcontroller-01",
"uptime_ms": 123456,
"time_synced": true,
"unix_time": 1789823456,
"timestamp": "2026-09-19T14:30:56+0200",
"timestamp_local": "2026-09-19 14:30:56 CEST",
"network": "ethernet",
"ip": "192.168.1.50",
"mqtt_connected": true,
"relay_phase": "idle",
"active_channel": 0,
"queued": 0,
"fault": false,
"channels": [
{
"id": 1,
"name": "Taste 1",
"enabled": true,
"pulse_ms": 250,
"state": "OFF"
}
]
}
Zusaetzlich gibt es pro Kanal ein retained State-Topic:
sulzi/lightcontroller/relaybox01/channel/1/state
...
sulzi/lightcontroller/relaybox01/channel/8/state
Payload:
ON
OFF
ON bedeutet: Der Relaiskontakt ist gerade fuer den Tastendruck aktiv. Nach Ablauf der Pulszeit geht der Kanal wieder auf OFF.
Availability
sulzi/lightcontroller/relaybox01/availability
Payload:
online
offline
Dieses Topic ist retained und wird auch als MQTT Last Will genutzt. Wenn der Controller ausfaellt oder die Verbindung hart abbricht, setzt der Broker den Status auf offline.
Event Topic
Ereignisse werden nicht retained publiziert:
sulzi/lightcontroller/relaybox01/event
Typische Events:
command_acceptedcommand_rejectedqueue_changedpress_startedpress_finishedemergency_offfaulttime_synced
Beispiel:
{
"device_id": "sulzi-lightcontroller-01",
"event": "press_started",
"uptime_ms": 123456,
"time_synced": true,
"unix_time": 1789823456,
"timestamp": "2026-09-19T14:30:56+0200",
"timestamp_local": "2026-09-19 14:30:56 CEST",
"channel": 3,
"channel_name": "Taste 3",
"pulse_ms": 250,
"request_id": "node-red-123"
}
Mosquitto-Beispiele
mosquitto_pub -h 192.168.1.10 -t sulzi/lightcontroller/relaybox01/channel/1/command -m PRESS
mosquitto_pub -h 192.168.1.10 -t sulzi/lightcontroller/relaybox01/command -m '{ "channel": 3, "action": "press", "pulse_ms": 300, "request_id": "test-3" }'
mosquitto_pub -h 192.168.1.10 -t sulzi/lightcontroller/relaybox01/command -m '{ "action": "all_off" }'
mosquitto_pub -h 192.168.1.10 -t sulzi/lightcontroller/relaybox01/command -m STATUS
Sicherheitslogik
- Beim Booten werden alle Relais ausgeschaltet, bevor Netzwerk/MQTT aktiv wird.
- Es wird immer nur ein Relais gleichzeitig eingeschaltet.
- Neue Tastendruecke laufen durch eine Queue.
- Zwischen zwei Tastendruecken gilt
timing.dead_time_ms. - Jeder Kanal kann einzeln aktiviert/deaktiviert und mit eigener
pulse_msversehen werden. - Bei I2C-/TCA9554-Fehlern geht der Relaiscontroller in
fault; weitere Press-Kommandos werden abgelehnt. - MQTT nutzt Last-Will auf
availability, damit ein Broker-Ausfall/Reset sichtbar wird.
Status-LED
- Weiss: Boot
- Blau blinkend: Netzwerk wird aufgebaut
- Gelb blinkend: MQTT wird verbunden
- Gruen kurzer Puls: online und idle
- Orange: Relais-Tastendruck aktiv
- Rot blinkend: Config- oder Hardware-Fehler
Die Status-LED ist eine WS2812-RGB-LED auf GPIO38. Angesteuert wird sie mit dem nativen Arduino-ESP32-RGB-Treiber. Optional kann in der Config unter hardware.status_led_color_order die Farbreihenfolge gesetzt werden, Default ist RGB. Wenn die LED leuchtet, aber Rot/Gruen vertauscht sind, kann hier z.B. GRB getestet werden.
Optionale LED-Konfiguration:
"hardware": {
"status_led_enabled": true,
"status_led_pin": 38,
"status_led_brightness": 32,
"status_led_color_order": "RGB"
}
Wenn status_led_color_order in deiner produktiven Config fehlt, verwendet die Firmware automatisch RGB.
Remote Firmware-Update
Die Partitionstabelle enthaelt bereits zwei App-Slots (app0 und app1) sowie otadata. Damit ist die Basis fuer OTA vorhanden. Eine OTA-Funktion ist in der Firmware aktuell aber noch nicht implementiert.
Moegliche spaetere Variante:
- HTTP-OTA per MQTT-Befehl.
- Controller schaltet vor dem Update alle Relais aus.
- Controller laedt
firmware.binvon einem lokalen HTTP-Server. - Nach erfolgreichem Update startet er neu.
Fuer produktive Nutzung sollte OTA nur im vertrauenswuerdigen Netz oder per VPN freigeschaltet werden.
Anschluss fuer Tastendruck-Emulation
Fuer einen Tastendruck wird ueblicherweise COM und NO eines Relais parallel zum vorhandenen Tasterkontakt angeschlossen. Vor Arbeiten an fremden Steuerungen Spannungen messen und sicherstellen, dass der Relaiskontakt nur den vorgesehenen Tasterkreis ueberbrueckt. Netzspannung und industrielle Anlagen nur durch qualifiziertes Personal verdrahten.