3 Webserver für Gießanlage auf ESP8266
3.1 Beschreibung
Um die Steuerung der Bewässerungsanlage über das Netzwerk zu ermöglichen, wird auf dem ESP8266 zusätzlich ein einfacher Webserver gehostet. Hierüber wird der aktuelle Feuchtigkeitswert, sowie der Status der Pumpe angezeigt. Dazu bietet die Oberfläche auch die Möglichkeit, die Pumpe manuell ein- und auszuschalten und sich einen anschaulichen Graphen über den Verlauf der Feuchtigkeitswerte über die Zeit anzuzeigen.
3.2 Implementierung
3.2.1 Abhängigkeiten
Zusätzlich zu den bereits bekannten importierten Bibliotheken müssen drei weitere Bibliotheken genutzt werden:
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <LittleFS.h>Da der Webserver aus dem lokalen Netzwerk erreichbar sein kann, muss sich der Mikrocontroller über WiFi damit verbinden können. Dies ermöglicht die Bibliothek ESP8266WiFi.
ESP8266WebServer ermöglicht es, einen simplen Webserver zu hosten.
Um die Daten von vergangenen Feuchtigkeitsmessungen in einer CSV Datei abspeichern zu können, wird ebenfalls die Bibliothek LittleFS benötigt. Diese ermöglicht es, auf dem Flash Speicher Dateien abzuspeichern.
3.2.2 WLAN
Um sich über WiFi in das lokale Netzwerk einwählen zu können, sind zunächst die entsprechenden Anmeldedaten des Netzwerks notwendig:
// Wi-Fi credentials
const char* ssid = "SSID";
const char* password = "PASSWORD";Hierbei darf nicht vergessen werden, dass SSID und Passwort des Netzwerkes eingefügt werden müssen, bevor die Firmware auf den Mikrocontroller geladen wird.
Diese Anmeldedaten werden darauf in der setup() Funktion genutzt, um sich mit dem Netzwerk zu verbinden:
// Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected. IP address: ");
Serial.println(WiFi.localIP());Sobald die Verbindung hergestellt ist, wird die vom DHCP zugeteilte IP Adresse über die Debug Console ausgegeben, um sich auf den Webserver verbinden zu können.
3.2.3 Webserver
Um den Server zu hosten, muss zunächst ein entsprechendes Objekt der Klasse ESP8266WebServer erstellt werden. Dieses ermöglicht es, entsprechende Anfragen über HTTP zu handlen:
ESP8266WebServer server(80);Die Hauptseite, welche die Grundfunktionen des Webservers bietet, wird über eine eigene Funktion zurückgegeben:
void handleRoot() {
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>Plant Monitor</title></head><body>";
html += "<h1>Soil Moisture: " + String(moisturePercentage) + "%</h1>";
html += "<h2>Pump Active: " + String(pumpActive ? "Yes" : "No") + "</h2>";
html += "<form action='/pump' method='POST'><button type='submit'>Activate Pump</button></form>";
html += "</body></html>";
server.send(200, "text/html", html);
}Dazu bestimmt eine weitere Funktion handlePumpRequest() die Funktion der manuellen Steuerung der Pumpe über den entsprechenden Button:
void handlePumpRequest() {
manualPumpTrigger = true;
server.sendHeader("Location", "/");
server.send(303);
}Der Server selbst wird darauf in der setup() Funktion in Betrieb genommen:
// Set up web server routes
server.on("/", handleRoot);
server.on("/pump", HTTP_POST, handlePumpRequest);
server.on("/chart", handleChart);
server.begin();
Serial.println("Web server started");Die resultierende HTML Seite ist durchaus sehr rudimentär. Dies soll lediglich als Proof-Of-Concept für eine HTML Seite auf dem ESP8266 dienen. Die gerenderte HTML der Hauptseite sieht wie folgt aus:

3.2.4 Chart
Teil der Web-UI ist auch ein Graph, welcher den bisherigen Verlauf der vom Feuchtigkeitssensor gemessenen Werte darstellt. Dazu werden diese Werte in einer CSV Datei zwischengespeichert und beim Laden der Seite abgerufen.
Das Abrufen der Datei, sowie das Zeichnen des Graphen wird innerhalb einer eigenen Funktion übernommen:
void handleChart() {
File file = LittleFS.open(LOG_FILE, "r");
if (!file) {
server.send(500, "text/plain", "Error while accesing CSV-File");
return;
}
const int maxEntries = 30;
std::vector<int> allValues;
String line = file.readStringUntil('\n');
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int comma1 = line.indexOf(',');
int comma2 = line.indexOf(',', comma1 + 1);
if (comma1 < 0 || comma2 < 0) continue;
int moisture = line.substring(comma1 + 1, comma2).toInt();
allValues.push_back(moisture);
}
file.close();
// Use only newest Entries
int count = min(maxEntries, (int)allValues.size());
int offset = allValues.size() - count;
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'><title>Moisture History</title></head><body>";
html += "<h2>Moisture History </h2><svg width='360' height='150' style='border:1px solid #ccc;'>";
int barWidth = 10, spacing = 4, chartHeight = 100, yBase = 130;
for (int i = 0; i < count; i++) {
int value = allValues[offset + i];
int barHeight = map(value, 0, 100, 0, chartHeight);
int x = 10 + i * (barWidth + spacing);
int y = yBase - barHeight;
String color = value < 30 ? "#f44336" : (value < 60 ? "#FFC107" : "#4CAF50");
html += "<rect x='" + String(x) + "' y='" + String(y) + "' width='" + String(barWidth) + "' height='" + String(barHeight) + "' fill='" + color + "'>";
html += "<title>" + String(value) + "%</title></rect>";
}
html += "</svg><br><a href='/'>back</a></body></html>";
server.send(200, "text/html", html);
}Diese Funktion wird, wie oben bereits gezeigt, beim Setup ausgeführt.
Eine weitere Funktion logDataToCSVB() übernimmt die Aufgabe, die Feuchtigkeits- und Pumpenstatuswerte mitzuloggen:
void logDataToCSV(int moisture, bool pump) {
File logFile = LittleFS.open(LOG_FILE, "r");
size_t size = logFile.size();
logFile.close();
if (size > MAX_LOG_SIZE) {
trimLogFile();
}
File file = LittleFS.open(LOG_FILE, "a");
if (!file) {
Serial.println("Error opening LOG-File");
return;
}
unsigned long timeNow = millis() / 1000;
file.printf("%lu,%d,%s\n", timeNow, moisture, pump ? "Yes" : "No");
file.close();
}Diese wird in der loop() Funktion ausgeführt, um die aktuellsten Daten abzuspeichern:
if (currentMillis - lastLogTime >= logInterval) {
logDataToCSV(moisturePercentage, pumpActive);
lastLogTime = currentMillis;
}Dies passiert alle 5 Minuten, was zu Beginn in entsprechenden Konstanten festgehalten wird:
//Log values
const size_t MAX_LOG_SIZE = 10240;
const size_t MAX_LOG_LINES = 50;
unsigned long lastLogTime = 0;
const unsigned long logInterval = 5UL * 60UL * 1000UL; // 5 MinutenZusätzlich wird über diese Konstanten auch die maximale Größe der Logs festgehalten.
Die resultierende Seite sieht im Browser dann wie folgt aus:

3.3 Performance
Der ESP8266 des D1 Minis ist leistungsstärker als man zunächst annimmt, jedoch fällt bei Anwendung wie einem Webserver die verhältnismäßig geringere Leistung dennoch auf. Das zeigt sich vor allem in relativ langen Ladezeiten der einzelnen Seiten. So zeigt Safari eine Wartezeit von knapp 3,2 Sekunden beim Laden einer beliebigen Seite an.

3.4 Code
Hier ist noch einmal der gesamte Code der auf dem ESP8266 laufenden Anwendung:
#include <Wire.h>
#include <U8g2lib.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <LittleFS.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define I2C_ADDRESS 0x3C
#define LOG_FILE "moisture_log.csv"
//Log values
const size_t MAX_LOG_SIZE = 10240;
const size_t MAX_LOG_LINES = 50;
unsigned long lastLogTime = 0;
const unsigned long logInterval = 5UL * 60UL * 1000UL; // 5 Minuten
// Wi-Fi credentials
const char* ssid = "SSID";
const char* password = "PASSWORD";
// OLED display
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// Calibration values
const int AirValue = 1024;
const int WaterValue = 590;
const int PumpThreshold = 50;
int moistureValue = 0;
int moisturePercentage = 0;
bool pumpActive = false;
bool manualPumpTrigger = false;
unsigned long lastPumpTime = 0;
const unsigned long pumpCooldown = 15UL * 60UL * 1000UL; // 15 minutes
ESP8266WebServer server(80);
void handleRoot() {
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>Plant Monitor</title></head><body>";
html += "<h1>Soil Moisture: " + String(moisturePercentage) + "%</h1>";
html += "<h2>Pump Active: " + String(pumpActive ? "Yes" : "No") + "</h2>";
html += "<form action='/pump' method='POST'><button type='submit'>Activate Pump</button></form>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handlePumpRequest() {
manualPumpTrigger = true;
server.sendHeader("Location", "/");
server.send(303);
}
void handleChart() {
File file = LittleFS.open(LOG_FILE, "r");
if (!file) {
server.send(500, "text/plain", "Error while accesing CSV-File");
return;
}
const int maxEntries = 30;
std::vector<int> allValues;
String line = file.readStringUntil('\n');
while (file.available()) {
String line = file.readStringUntil('\n');
line.trim();
if (line.length() == 0) continue;
int comma1 = line.indexOf(',');
int comma2 = line.indexOf(',', comma1 + 1);
if (comma1 < 0 || comma2 < 0) continue;
int moisture = line.substring(comma1 + 1, comma2).toInt();
allValues.push_back(moisture);
}
file.close();
// Use only newest Entries
int count = min(maxEntries, (int)allValues.size());
int offset = allValues.size() - count;
String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'><title>Moisture History</title></head><body>";
html += "<h2>Moisture History </h2><svg width='360' height='150' style='border:1px solid #ccc;'>";
int barWidth = 10, spacing = 4, chartHeight = 100, yBase = 130;
for (int i = 0; i < count; i++) {
int value = allValues[offset + i];
int barHeight = map(value, 0, 100, 0, chartHeight);
int x = 10 + i * (barWidth + spacing);
int y = yBase - barHeight;
String color = value < 30 ? "#f44336" : (value < 60 ? "#FFC107" : "#4CAF50");
html += "<rect x='" + String(x) + "' y='" + String(y) + "' width='" + String(barWidth) + "' height='" + String(barHeight) + "' fill='" + color + "'>";
html += "<title>" + String(value) + "%</title></rect>";
}
html += "</svg><br><a href='/'>back</a></body></html>";
server.send(200, "text/html", html);
}
void trimLogFile() {
File file = LittleFS.open(LOG_FILE, "r");
if (!file) return;
std::vector<String> lines;
while (file.available()) {
lines.push_back(file.readStringUntil('\n'));
}
file.close();
int start = lines.size() > MAX_LOG_LINES ? lines.size() - MAX_LOG_LINES : 0;
File newFile = LittleFS.open(LOG_FILE, "w");
if (!newFile) return;
for (int i = start; i < lines.size(); i++) {
newFile.println(lines[i]);
}
newFile.close();
}
void logDataToCSV(int moisture, bool pump) {
File logFile = LittleFS.open(LOG_FILE, "r");
size_t size = logFile.size();
logFile.close();
if (size > MAX_LOG_SIZE) {
trimLogFile();
}
File file = LittleFS.open(LOG_FILE, "a");
if (!file) {
Serial.println("Error opening LOG-File");
return;
}
unsigned long timeNow = millis() / 1000;
file.printf("%lu,%d,%s\n", timeNow, moisture, pump ? "Yes" : "No");
file.close();
}
void setup() {
Serial.begin(115200);
pinMode(D4, OUTPUT);
digitalWrite(D4, HIGH);
u8g2.begin();
u8g2.setFont(u8g2_font_ncenB14_tr);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected. IP address: ");
Serial.println(WiFi.localIP());
// Create Log-File
if (!LittleFS.begin()) {
Serial.println("LittleFS Startup Error");
return;
}
if (!LittleFS.exists(LOG_FILE)) {
File file = LittleFS.open(LOG_FILE, "w");
if (file) {
file.println("Time (s),Moisture (%),Pump");
file.close();
}
}
// Set up web server routes
server.on("/", handleRoot);
server.on("/pump", HTTP_POST, handlePumpRequest);
server.on("/chart", handleChart);
server.begin();
Serial.println("Web server started");
}
void loop() {
server.handleClient();
moistureValue = analogRead(A0);
moisturePercentage = map(moistureValue, AirValue, WaterValue, 0, 100);
moisturePercentage = constrain(moisturePercentage, 0, 100);
unsigned long currentMillis = millis();
bool cooldownActive = (currentMillis - lastPumpTime) < pumpCooldown;
unsigned long remainingCooldown = 0;
if (cooldownActive) {
remainingCooldown = (pumpCooldown - (currentMillis - lastPumpTime)) / 1000UL;
}
// Pump control logic
if ((moisturePercentage < PumpThreshold && !cooldownActive) || manualPumpTrigger) {
manualPumpTrigger = false;
pumpActive = true;
digitalWrite(D4, LOW);
delay(4000); // Pump ON for 4 seconds
digitalWrite(D4, HIGH);
pumpActive = false;
lastPumpTime = millis(); // Reset cooldown
} else {
pumpActive = false;
digitalWrite(D4, HIGH);
}
if (currentMillis - lastLogTime >= logInterval) {
logDataToCSV(moisturePercentage, pumpActive);
lastLogTime = currentMillis;
}
// Display data on OLED
char moistureStr[32], pumpStr[32], cooldownStr[32];
sprintf(moistureStr, "Moist.: %d%%", moisturePercentage);
if (cooldownActive) {
sprintf(pumpStr, "Cooldown:");
sprintf(cooldownStr, "%lu sec", remainingCooldown);
} else {
sprintf(pumpStr, "Active: %d", pumpActive);
sprintf(cooldownStr, "");
}
u8g2.clearBuffer();
u8g2.drawStr(5, 20, moistureStr);
u8g2.drawStr(5, 40, pumpStr);
u8g2.drawStr(5, 60, cooldownStr);
u8g2.sendBuffer();
delay(1000);
}