No description
Find a file
2026-05-23 13:59:57 +02:00
.vscode First Version 2026-05-19 23:18:53 +02:00
data Udpate LED 2026-05-23 13:59:57 +02:00
include Udpate LED 2026-05-23 13:59:57 +02:00
src Udpate LED 2026-05-23 13:59:57 +02:00
.gitignore First Version 2026-05-19 23:18:53 +02:00
LICENSE Initial commit 2026-05-19 22:10:50 +02:00
partitions.csv First Version 2026-05-19 23:18:53 +02:00
platformio.ini Udpate LED 2026-05-23 13:59:57 +02:00
README.md Udpate LED 2026-05-23 13:59:57 +02:00

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 state und event.
  • Status-LED auf GPIO38 mit RGB-Farbreihenfolge RGB.
  • 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, SCL GPIO41.
  • Ethernet W5500: INT GPIO12, MOSI GPIO13, MISO GPIO14, SCLK GPIO15, CS GPIO16.
  • 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

  1. VS Code oeffnen und die PlatformIO-Erweiterung installieren.

  2. data/config.json anpassen: MQTT-Broker, Topic-Basis, Zeiten, Kanalnamen. Die IP-Konfiguration besteht nur aus network.ip, network.gateway und network.subnet; DNS wird nicht gesetzt. mqtt.host muss deshalb ebenfalls eine IPv4-Adresse sein. Fuer menschenlesbare Zeitstempel time.ntp_server auf einen lokalen NTP-Server oder eine erreichbare NTP-IP setzen. Standard-Zeitzone ist Deutschland.

  3. Dateisystem flashen:

    .\.venv\Scripts\platformio.exe run -t uploadfs
    
  4. Firmware flashen:

    .\.venv\Scripts\platformio.exe run -t upload
    
  5. 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/availability retained: online / offline
  • sulzi/lightcontroller/relaybox01/state retained: JSON-Gesamtstatus
  • sulzi/lightcontroller/relaybox01/event: Ereignisse wie command_accepted, press_started, press_finished, command_rejected
  • sulzi/lightcontroller/relaybox01/command: zentrales Command-Topic
  • sulzi/lightcontroller/relaybox01/channel/1/command bis /channel/8/command
  • sulzi/lightcontroller/relaybox01/channel/1/state bis /channel/8/state: ON waehrend des Tastendrucks, sonst OFF

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:

  • action oder command: press, pulse, on, all_off, off, stop, abort, status, state
  • channel: 1 bis 8
  • pulse_ms: Schaltzeit in Millisekunden, alternativ duration_ms oder time_ms
  • request_id: frei waehlbare ID fuer Rueckverfolgung in Events, alternativ id

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_accepted
  • command_rejected
  • queue_changed
  • press_started
  • press_finished
  • emergency_off
  • fault
  • time_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_ms versehen 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.bin von 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.