Files
Bewaesserung/watering/README.md
T
admin 33c07c7b42 neu: Steuerung über MQTT
Behoben: Keine automatische Abschaltuk bei t<2 t jetzt limitiert auf 1-120
Dokumentation angelegt
2026-08-26 12:36:39 +02:00

342 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Gartenbewässerung ESP32 Ventilsteuerung
Firmware für eine ESP32-basierte Steuerung motorisierter Gartenventile (Kugelhähne), bedienbar über eine Weboberfläche (WebSocket + REST) und MQTT. Es existieren zwei baugleiche Steuerungen im Einsatz:
| Gerät (Hostname) | Standort / Zonen | Belegte Ventilkanäle |
|------------------------|--------------------------------------------|-----------------------|
| `Gartenwasser_vorn` | Tröge + Garten vorn | Kanal 1 (Tröge), Kanal 6 (Garten vorn) |
| `Gartenwasser_hinten` | Hochbeete | Kanal 6 (Hochbeete) |
Beide laufen mit derselben Firmware, unterscheiden sich nur in der über den WiFiManager-Captive-Portal gespeicherten Konfiguration (Hostname, MQTT-Topic, `numValves`, …), die in den ESP32-NVS-Preferences abgelegt wird.
## Inhaltsverzeichnis
- [Systemüberblick](#systemüberblick)
- [Hardware](#hardware)
- [Ventil-Zustandsautomat](#ventil-zustandsautomat)
- [Software-Architektur](#software-architektur)
- [Kommunikationswege](#kommunikationswege)
- [Automatik-Modi](#automatik-modi)
- [Konfiguration & Erstinbetriebnahme](#konfiguration--erstinbetriebnahme)
- [Boot-Ablauf](#boot-ablauf)
- [Logging](#logging)
- [Firmware-Updates (OTA)](#firmware-updates-ota)
- [Projektstruktur](#projektstruktur)
- [Bekannte Einschränkungen / TODO](#bekannte-einschränkungen--todo)
## Systemüberblick
```mermaid
flowchart LR
Browser[Web-Browser<br/>index.html] -- HTTP + WebSocket /ws --> ESP
App[MQTT-Client / Home-Automation] -- MQTT --> Broker[(MQTT-Broker<br/>nas.local)]
Broker <-- MQTT --> ESP
NTPsrv[(NTP-Server<br/>nas.local)] --> ESP
Syslog[(Syslog-Server<br/>NAS, UDP 13377)] <-- UDP --> ESP
Dev[Entwickler-PC] -- ArduinoOTA / espota --> ESP
subgraph ESP["ESP32 Steuerung"]
FW[Firmware<br/>main.cpp / MQTT.cpp]
end
ESP -- SPI, Latch GPIO4 --> SR[3x 74HC595<br/>Schieberegister, 24 Bit]
SR -- Transistor + bistabiles Relais<br/>G5RL-K1-E --> Valves[(Ventile Kanal 1-6<br/>12V motorisierte Kugelhähne)]
ESP -- WS2812, GPIO12 --> Ring[NeoPixel-Ring<br/>12 LEDs, Schaltschrank]
```
Die Firmware übernimmt drei Rollen gleichzeitig:
1. **Webserver** (`ESPAsyncWebServer`) liefert die Bedienoberfläche (`data/index.html`, `data/simple.css` aus SPIFFS) und eine REST-Schnittstelle.
2. **WebSocket-Server** (`/ws`) für Live-Status/Steuerung der Weboberfläche.
3. **MQTT-Client** (`PubSubClient`) für Steuerung/Status-Publishing aus der Hausautomation.
Zusätzlich: `WiFiManager` für WLAN-Konfiguration per Captive Portal, `ArduinoOTA` für Firmware-Updates über das Netzwerk, NTP für die Systemzeit (u. a. für die Bewässerungs-Logs), und ein einfacher UDP-Syslog-Client fürs zentrale Logging.
## Hardware
### Steuerplatine (`ventilsteuerung2.sch`, 2 Blätter)
Der vollständige Schaltplan liegt unter [`docs/hardware/ventilsteuerung2.pdf`](docs/hardware/ventilsteuerung2.pdf) (Rendering), [`docs/hardware/ventilsteuerung2.sch`](docs/hardware/ventilsteuerung2.sch) (Eagle-Schaltplan) und [`docs/hardware/ventilsteuerung2.brd`](docs/hardware/ventilsteuerung2.brd) (Eagle-Platinenlayout). Die folgende Beschreibung basiert auf dem Schaltplan und der Firmware.
**Sheet 1 Steuerlogik:**
- **MCU:** ESP32 Dev-Board (im Schaltplan auf einem Arduino-Nano-kompatiblen Sockel). Die Firmware ist für `esp32dev` (Espressif32-Plattform, Arduino-Framework) gebaut.
- **Ausgangserweiterung:** 2× 74HC595-Schieberegister in Reihe, per Hardware-SPI angesprochen (`SpiShiftRegisterChain`, Latch-Pin GPIO4). Zusammen 24 Bit, wovon 21 genutzt werden.
- **7 identische Relais-Treiberkanäle** (Kanal 06), je bestehend aus:
- 1× Transistor-Treiberstufe für die **SET-Spule** (oberer Block im Schaltplan)
- 1× Transistor-Treiberstufe für die **RESET-Spule** (unterer Block im Schaltplan)
- 1× **bistabiles Relais Omron G5RL-K1-E** (2-Spulen-Stromstoßrelais, 12V) mit Freilaufdiode (Schottky, BAT85) je Spule zum Schutz der Treibertransistoren
- 1× Status-LED je Kanal (direkt am Schieberegister-Ausgang)
- 3-poliger Steckverbinder zur Ventilverkabelung
- Da das Relais bistabil ist, genügt ein kurzer Steuerimpuls, um die Spulenposition dauerhaft (ohne Haltestrom) zu wechseln — daher `KEEP_V(x)` in der Firmware, das beide Spulen wieder stromlos schaltet, nachdem ein Impuls gesetzt wurde.
**Sheet 2 Spannungsversorgung:**
- Eingangsklemme für die 12V-Versorgung mit Verpolungs-/Filterschaltung (Drossel + Eingangskondensatoren).
- Schaltregler erzeugt eine **5V-Schiene** (Logik/Relais-Treiber) und eine **3,3V-Schiene** (ESP32) aus der 12V-Eingangsspannung.
### Bit-Belegung des Schieberegisters (`glblData.h`)
Kanal-Index `i` läuft von 06 (7 Kanäle = 3 Byte = 24 Bit, davon 21 genutzt):
| Funktion | Bit-Offset | Bits (Kanal 06) |
|-------------------|-----------:|-------------------|
| SET-Spule | `i` | 06 |
| RESET-Spule | `i + 8` | 814 |
| Status-LED | `i + 16` | 1622 |
**Kanal 0 ist kein Gartenventil**, sondern schaltet die gemeinsame 12V-Motorspannung für alle Ventile ein/aus (`SET_V(0)` / `RST_V(0)` / `KEEP_V(0)` in `main.cpp`). Kanäle 16 sind die eigentlichen Ventile: Ihr SET/RESET-Impuls wählt die Laufrichtung (auf/zu) des jeweiligen Ventilaktors, bevor die gemeinsame Motorspannung (Kanal 0) für die Dauer der Ventillaufzeit angelegt wird.
```
#define RST_V(x) { RESET-Spule(x)=an; SET-Spule(x)=aus; LED(x)=aus; }
#define SET_V(x) { RESET-Spule(x)=aus; SET-Spule(x)=an; LED(x)=an; }
#define KEEP_V(x){ SET-Spule(x)=aus; RESET-Spule(x)=aus; } // beide Spulen stromlos
```
`MOTOR_POWER_TIME` = 15 s (als 30 Ticks à 500 ms) ist die Laufzeit, für die die gemeinsame Motorspannung je Öffnungs-/Schließvorgang anliegt.
### NeoPixel-Ring
12× WS2812-LEDs an GPIO12, montiert am Schaltschrank. Zeigt die verbleibende Bewässerungsrestzeit des länger laufenden der beiden aktiven Timer (`glblData.timers[1]`/`timers[6]`) als umlaufenden Ring an (Minuten als blauer Fortschrittsring, Sekunden als grün/rot abklingender "Kometenschweif", Stunden als rote Segmente).
## Ventil-Zustandsautomat
Jedes Ventil `i` (16) durchläuft beim Öffnen/Schließen denselben zweistufigen Ablauf: zuerst Richtungsauswahl (Kanal `i`), danach gemeinsame Motorspannung (Kanal 0) für 15 s. `valveState()` in `main.cpp` wird alle 500 ms über einen `Ticker` aufgerufen.
```mermaid
stateDiagram-v2
[*] --> VALVE_CLOSED
VALVE_CLOSED --> VALVE_OPENING_COILPWR: setValveState[i]=OPEN\nSET_V(i)
VALVE_OPENING_COILPWR --> VALVE_OPENING_POWER: KEEP_V(i)
VALVE_OPENING_POWER --> VALVE_OPENING_POWER_COILPWR: SET_V(0)
VALVE_OPENING_POWER_COILPWR --> VALVE_OPEN_MOTORIZED: KEEP_V(0)\nv_powered=0
VALVE_OPEN_MOTORIZED --> VALVE_OPEN_COILPWR: 15s vergangen\nRST_V(0)
VALVE_OPEN_COILPWR --> VALVE_OPEN: KEEP_V(0)
VALVE_OPEN --> VALVE_CLOSING_COILPWR: setValveState[i]=CLOSE\nRST_V(i)
VALVE_CLOSING_COILPWR --> VALVE_CLOSING_POWER: KEEP_V(i)
VALVE_CLOSING_POWER --> VALVE_CLOSING_POWER_COILPWR: SET_V(0)
VALVE_CLOSING_POWER_COILPWR --> VALVE_CLOSED_MOTORIZED: KEEP_V(0)\nv_powered=0
VALVE_CLOSED_MOTORIZED --> VALVE_CLOSED_COILPWR: 15s vergangen\nRST_V(0)
VALVE_CLOSED_COILPWR --> VALVE_CLOSED: KEEP_V(0)
```
Nebenbei protokolliert die Firmware pro Ventil die letzten 5 Bewässerungsvorgänge (Startzeit + Dauer in 2er-Schritten, da der Timer-Tick alle 500 ms läuft) als Ringpuffer (`wateringLog`, `wateringLogDuration`, `logIndex`) für die Anzeige im Web-UI.
## Software-Architektur
| Datei | Verantwortung |
|--------------------|----------------|
| `main.cpp` | Setup/Loop, Ventil-Zustandsautomat (`valveState`), Relais-Initialisierung (`init_relays`/`closeValves_tick`), Automatik-Modi (`autoMode_tick`), NeoPixel-Ring (`updateTimerRing`) |
| `MQTT.h` / `.cpp` | Sammelklasse `MQTT` (erbt von `PubSubClient`, enthält zusätzlich einen `PubSubClient`-Member `psclient`) — bündelt WLAN (`WiFiManager`), Webserver (`ESPAsyncWebServer`), WebSocket, MQTT, OTA, NTP und UDP-Syslog |
| `glblData.h` / `.cpp` | Globaler Zustand (`dataset_t glblData`): Soll-/Ist-Zustand je Ventil, Timer, Automatikmodus, Watering-Log |
```mermaid
classDiagram
class MQTT {
+begin()
+loop()
+publish(msg)
+publish_sub(subtopic, msg)
-handleMqttMessage(topic, payload, length)
-handleWebSocket(...)
-handleWebSocketMessage(...)
-applyValveJson(doc) bool
-reconnect()
-saveConfigToFlash()
+psclient : PubSubClient
+wm : WiFiManager
+prefs : Preferences
}
class dataset_t {
+setValveState[7] : bool
+valveState[7] : valvestate_t
+timers[7] : uint32
+pauseTimers[7] : int32
+autoMode : automode_t
+statusChange : bool
+timerChange : bool
+wateringLog[7][5]
}
MQTT ..> dataset_t : liest/schreibt glblData (extern)
main ..> MQTT : connections.begin()/loop()
main ..> dataset_t : valveState(), autoMode_tick()
```
`glblData` ist eine globale, von `main.cpp` und `MQTT.cpp` gemeinsam genutzte Datenstruktur — der eigentliche "Shared State" zwischen dem Ventil-Zustandsautomaten (der die Hardware treibt) und den Kommunikationsschnittstellen (die Sollwerte setzen und Ist-Werte auslesen).
## Kommunikationswege
### WebSocket (`/ws`)
Beim Verbindungsaufbau sendet der Server sofort den aktuellen Status (`makeStatusJSON`) und die Timer (`makeTimerJSON`). Änderungen werden alle 1 s geprüft und bei Bedarf per Broadcast an alle Clients gesendet (`glblData.statusChange` / `glblData.timerChange`).
Eingehende Nachrichten (JSON) werden von `handleWebSocketMessage` verarbeitet:
```json
{ "valve": 6, "set": "open", "timer": 30 }
{ "valve": 6, "set": "close" }
{ "auto": "Hoch" }
```
Enthält die Nachricht kein gültiges `valve`-Feld, wird stattdessen `auto` ausgewertet (`"Hoch"`/`"Vorn"``AUTO_HOCH`, `"Trog"``AUTO_TROG`, alles andere → `AUTO_CLOSE`).
### Ventilnummern (`valve` / `N`)
Der numerische Ventilkanal in WebSocket-, REST- und MQTT-`/set`-Befehlen entspricht der physischen Kanalnummer am Schieberegister (siehe [Bit-Belegung](#hardware)). Aktuell sind pro Gerät nur folgende Kanäle tatsächlich verkabelt:
| Kanal-Nr. | Zone | `Gartenwasser_vorn` | `Gartenwasser_hinten` |
|---|---|---|---|
| 1 | Tröge | verkabelt | unbenutzt |
| 6 | Garten vorn / Hochbeete | verkabelt (Garten vorn) | verkabelt (Hochbeete) |
| 25 | — | unbenutzt (Hardware-Reserve) | unbenutzt (Hardware-Reserve) |
Kanal 0 ist kein ansteuerbares Ventil, sondern die gemeinsame Motorspannung (siehe [Hardware](#hardware)) und darf nicht als `valve`-Wert verwendet werden.
### REST
| Endpunkt | Beschreibung |
|-------------------------|--------------|
| `GET /` | Liefert `index.html` (mit Platzhaltern `%HOSTNAME%`, `%NAME_VALVE1%`, `%NAME_VALVE2%`) |
| `GET /simple.css` | Stylesheet |
| `GET /data` | Aktueller Status als JSON (wie `makeStatusJSON`) |
| `GET /setValve?valve=N&set=open\|close&timer=M` | Ventil N öffnen/schließen, optional mit Laufzeit in Minuten |
### MQTT
Basis-Topic ist konfigurierbar (`mqtt_topic`, z. B. `Gartenwasser/vorn`).
**Publish (alle 1s bei Änderung, bzw. beim Verbindungsaufbau):**
| Topic (relativ zu `mqtt_topic`) | Inhalt |
|---|---|
| `/status` | JSON mit Ist-/Soll-Ventilzuständen |
| `/hostname`, `/IP`, `/numValves`, `/defaultTimerMins`, `/NTP-Server` | Gerätestatus |
| `/Timers` | JSON mit Restlaufzeiten je Ventil + Automatikmodus |
**Subscribe:**
| Topic | Payload | Wirkung |
|---|---|---|
| `/auto` | `"Hoch"` / `"Vorn"` / `"Trog"` / sonst | setzt `autoMode` (`AUTO_HOCH` / `AUTO_TROG` / `AUTO_CLOSE`), case-insensitive |
| `/set` | `{"valve":N,"set":"open"\|"close","timer":M}` | steuert ein einzelnes Ventil, analog zur WebSocket-/REST-API |
`timer` (Minuten) ist bei `"set":"open"` optional: Fehlt das Feld oder liegt der Wert außerhalb 1120, greift als Fallback `defaultTimerMins` (Gerätekonfiguration) bzw. wird der Wert auf 1120 geklemmt — ein Ventil wird also nie ohne Restlaufzeit geöffnet (kein unbegrenztes Offenbleiben). Dieselbe Absicherung gilt auch für den REST-Endpunkt `/setValve`.
```mermaid
sequenceDiagram
participant U as MQTT-Client (Hausautomation)
participant B as MQTT-Broker (nas.local)
participant ESP as ESP32 Firmware
participant HW as Ventil-Hardware
U->>B: publish Gartenwasser/vorn/set<br/>{"valve":6,"set":"open","timer":30}
B->>ESP: handleMqttMessage(topic, payload)
ESP->>ESP: applyValveJson()<br/>setValveState[6]=OPEN, timers[6]=1800
loop alle 500ms
ESP->>HW: Zustandsautomat treibt Relais
end
ESP-->>B: publish .../status, .../Timers
ESP-->>U: WebSocket-Broadcast an Weboberfläche
```
## Automatik-Modi
Zwei vordefinierte Bewässerungsprogramme, gesteuert über `glblData.autoMode` und einen minütlichen `Ticker` (`autoMode_tick`):
```mermaid
sequenceDiagram
participant T as autoModeTicker (60s-Takt)
participant V6 as Ventil 6 (Hoch/Vorn)
participant V1 as Ventil 1 (Tröge)
Note over T,V6: AUTO_HOCH
T->>V6: t=0 min: OPEN, 30 min
T->>V6: t=30 min: CLOSE, Modus beendet
Note over T,V1: AUTO_TROG
T->>V1: t=0 min: OPEN, 5 min
T->>V1: t=5 min: CLOSE, Pause 10 min
T->>V1: t=15 min: OPEN, 3 min
T->>V1: t=18 min: CLOSE, Modus beendet
```
`AUTO_TROG` fährt also einen Quell-/Einsickerzyklus: kurz befüllen, ausgiebig ziehen lassen, kurz nachlegen.
## Konfiguration & Erstinbetriebnahme
Beim ersten Start (bzw. wenn kein WLAN erreichbar ist) öffnet `WiFiManager` einen Access-Point **`AutoConnectAP`** mit Konfigurationsportal. Dort werden neben den WLAN-Zugangsdaten folgende Parameter abgefragt und in den ESP32-NVS-Preferences (Namespace `settings`) gespeichert:
| Parameter (Portal) | Preferences-Key | Bedeutung | Default |
|---|---|---|---|
| Hostname | `HOST` | Gerätename (WLAN, mDNS, OTA) | `Gartenwasser_vorn` |
| mqtt server | `MQTT_SERVER` | MQTT-Broker-Adresse | `nas.local` |
| mqtt port | `MQTT_PORT` | MQTT-Broker-Port | `1883` |
| topic | `MQTT_TOPIC` | Basis-MQTT-Topic | `Gartenwasser/vorn` |
| Default ON Time (min) | `DEF_TIME_MIN` | Standard-Bewässerungsdauer | `60` |
| Available Valves | `NUM_VALVES` | Anzahl im Web-UI angezeigter Zonen (1 oder 2) | `1` |
| NTP Server | `NTP_SERVER` | Zeitserver | `nas.local` |
`numValves` steuert **nur die Web-UI-Beschriftung/Sichtbarkeit** ("Hochbeete" bei 1 Zone, "Garten Vorn"/"Tröge" bei 2 Zonen) — die Hardware unterstützt unabhängig davon bis zu 6 Ventilkanäle.
Nach dem Speichern trennt die Firmware die MQTT-Verbindung, um die neuen Einstellungen zu übernehmen (`psclient.disconnect()`); WLAN-Hostname, mDNS und OTA-Hostname werden aktuell nur beim Boot gesetzt — eine Hostname-Änderung wird daher erst nach einem Neustart vollständig wirksam.
Solange das Gerät verbunden ist, bleibt das Konfigurationsportal zusätzlich unter Port **8080** erreichbar (die eigentliche Anwendung belegt Port 80).
## Boot-Ablauf
```mermaid
flowchart TD
A[Start] --> B[Preferences laden<br/>SPIFFS mounten]
B --> C[WiFiManager: autoConnect]
C -->|verbunden| D[NTP-Zeit synchronisieren]
C -->|nicht verbunden, 1. Versuch| E[5s warten, erneut versuchen]
E -->|verbunden| D
E -->|weiterhin offline| F[Nur Config-Portal starten<br/>kein Anwendungsbetrieb]
D --> G[ArduinoOTA einrichten]
G --> H[Webserver-Routen registrieren<br/>/, /simple.css, /data, /setValve]
H --> I[mDNS starten: hostname.local]
I --> J[WebSocket-Handler registrieren]
J --> K[MQTT verbinden + Topics abonnieren]
K --> L[Relais initialisieren<br/>closeValves_tick 0..4]
L --> M[Ventil-Zustandsautomat startet<br/>500ms Ticker]
```
Die Relais-Initialisierung (`init_relays`/`closeValves_tick`) fährt beim Start einmalig alle Ausgänge in einen definierten (stromlosen) Zustand, bevor der reguläre Zustandsautomat übernimmt.
## Logging
`MQTT::remoteLog*` sendet Log-Nachrichten im BSD-Syslog-ähnlichen Format per UDP an einen zentralen Log-Server im lokalen Netz (aktuell `192.168.179.174:13377`, bereitgestellt vom NAS). Genutzt für Ventilschaltungen, Automatik-Moduswechsel und Verbindungsstatus. Es gibt aktuell keine Bestätigung/Fehlerbehandlung, falls der Log-Server nicht erreichbar ist (Fire-and-forget per UDP).
## Firmware-Updates (OTA)
`upload_protocol = espota` in `platformio.ini` lädt neue Firmware über WLAN auf das per `upload_port` (`<hostname>.local`) adressierte Gerät hoch — kein serieller Zugriff nötig, sofern das Gerät bereits im WLAN und per mDNS erreichbar ist. Beide Geräte müssen daher beim Flashen über den jeweils passenden Hostnamen adressiert werden (`upload_port` in `platformio.ini` anpassen, aktuell `gartenwasser_hinten.local`).
## Projektstruktur
```
src/
main.cpp Setup/Loop, Ventil-Zustandsautomat, Automatik-Modi, LED-Ring
MQTT.h / .cpp WLAN/Web/WebSocket/MQTT/OTA-Sammelklasse
glblData.h / .cpp Globaler Anwendungszustand
data/
index.html Weboberfläche (SPIFFS)
simple.css Stylesheet (SPIFFS)
lib/
SpiShiftRegisterChain Ansteuerung der 74HC595-Kette
PubSubClient MQTT-Client
WiFiManager WLAN-Provisioning / Captive Portal
ESPAsyncWebServer, AsyncTCP Webserver + WebSocket
ArduinoJson JSON (de)serialisierung
Adafruit_NeoPixel WS2812-Ansteuerung (Statusring)
platformio.ini Board esp32dev, Framework Arduino, espressif32@6.10.0
```
## Bekannte Einschränkungen / TODO
- Hostname-/mDNS-/OTA-Namensänderungen über das Konfigurationsportal werden erst nach einem manuellen Neustart vollständig wirksam (werden nur in `begin()` gesetzt).
- Laut Code-Kommentar in `MQTT::begin()` ist die Art, wie der Webserver bei Bedarf neu aufgesetzt wird, "not the safest way" — mögliche Abstürze durch bereits registrierte Callbacks auf dem geteilten `AsyncWebServer`-Pointer sind nicht ausgeschlossen.
- Der Syslog-Ziel-Host (`192.168.179.174:13377`) ist aktuell fest im Code hinterlegt statt konfigurierbar.
- Die Weboberfläche (`index.html`) geht fest von Ventil 1 ("Tröge") und Ventil 6 ("Garten vorn"/"Hochbeete") aus; eine allgemeinere Zuordnung (z. B. für mehr als 2 sichtbare Zonen) ist nicht vorgesehen.