Vom ATmega32 zum ESP8266: Mikrocontroller damals und heute

Zwei stilisierte Zebra-Avatare blicken von links und rechts auf ein Mikrocontroller-Board mit vielen ESP-Modulen, das einen klassischen ATmega32 verdeckt.

In diesem Beitrag zeigen wir, wie sich ein einfaches Projekt – eine Temperaturanzeige mit LED-Ausgabe – über die letzten zwei Jahrzehnte entwickelt hat. Dabei geht es nicht nur um die Programmierung, sondern auch um die rasante Entwicklung der Hardware: Vom großen DIP-AVR mit externem Programmer bis hin zu kompakten All-in-One-Boards wie dem ESP8266 oder ESP32.

Was macht das Projekt?

Ein DHT11-Sensor misst die Temperatur. Zwei LEDs zeigen den Wert an – entweder in Helligkeitsstufen oder als An/Aus-Anzeige. Die Helligkeit wird per PWM geregelt, sodass die Temperatur visuell erkennbar ist.

Variante 1: AVR-C auf dem Pollin ATmega32-Board

Technikstand: ca. 2000–2005
Diese Variante kommt ohne Bibliotheken aus und verwendet direkte Registerzugriffe. Die PWM wird über Timer0 realisiert, der DHT11 per Bit-Banging abgefragt.

Hardware: Pollin ATmega32 Evaluationsboard mit 16 MHz Quarz, DHT11 an PC0, LED an PD6 (OC0)

#define F_CPU 16000000UL

#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#include <stdbool.h>

#define DHT_DDR  DDRC
#define DHT_PORT PORTC
#define DHT_PIN  PINC
#define DHT_INPUTPIN PC0

uint8_t read_dht11(uint8_t *temperature) {
    uint8_t bits[5] = {0};
    uint8_t i, j;

    // Start signal
    DHT_DDR |= (1 << DHT_INPUTPIN);
    DHT_PORT &= ~(1 << DHT_INPUTPIN);
    _delay_ms(20); // >18ms
    DHT_PORT |= (1 << DHT_INPUTPIN);
    _delay_us(40);
    DHT_DDR &= ~(1 << DHT_INPUTPIN);

    // Wait for DHT11 response
    _delay_us(80);
    if (DHT_PIN & (1 << DHT_INPUTPIN)) return 1;
    _delay_us(80);
    if (!(DHT_PIN & (1 << DHT_INPUTPIN))) return 2;

    // Read 5 bytes
    for (j = 0; j < 5; j++) {
        for (i = 0; i < 8; i++) {
            while (!(DHT_PIN & (1 << DHT_INPUTPIN))); // wait for high
            _delay_us(30);
            if (DHT_PIN & (1 << DHT_INPUTPIN))
                bits[j] |= (1 << (7 - i));
            while (DHT_PIN & (1 << DHT_INPUTPIN)); // wait for low
        }
    }

    // Checksum
    if ((uint8_t)(bits[0] + bits[1] + bits[2] + bits[3]) != bits[4]) return 3;

    *temperature = bits[2]; // whole °C from byte 3
    return 0;
}

void init_pwm_timer0(void) {
    DDRD |= (1 << PD6);               // OC0 (PD6) als Ausgang
    TCCR0 = (1 << WGM00) | (1 << WGM01) // Fast PWM
          | (1 << COM01);              // Non-inverting
    TCCR0 |= (1 << CS01);              // Prescaler 8 → 16MHz/8 = 2MHz
}

int main(void) {
    uint8_t temp = 0;

    init_pwm_timer0();

    while (1) {
        if (read_dht11(&temp) == 0) {
            // Begrenzung des Temperaturbereichs auf 20–30°C
            if (temp < 20) temp = 20;
            if (temp > 30) temp = 30;

            // Mapping 20°C–30°C auf PWM 0–255
            OCR0 = (temp - 20) * 25;
        } else {
            OCR0 = 0; // Fehler → LED aus
        }

        _delay_ms(1000); // Messrate ca. 1 Hz
    }
}

Diese Art der Mikrocontroller-Programmierung war damals Standard und bot maximale Kontrolle bei minimaler Abstraktion. Externe Programmer wie LPT-Adapter waren damals die einzige Option – der heute beliebte USBasp kam erst später auf den Markt, ab etwa 2005. In den frühen 2000ern wurden oft selbst gelötete Parallelport-Programmieradapter eingesetzt, die direkt über PonyProg oder einfache Bit-Banging-Tools angesteuert wurden., um den Code überhaupt auf den Chip zu bekommen. Komfort: Fehlanzeige.

Pollin ATmega32 Evaluationsboard mit drei modernen ESP-Modulen im Vergleich, nebeneinander auf einem weißen Hintergrund.

Variante 2: ESP8266 mit Arduino-C++

Technikstand: 2015 bis heute
Hier nutzen wir moderne Bibliotheken wie DHT.h von Adafruit, komfortable Methoden wie analogWrite() und deutlich mehr Rechenleistung.

Hardware: ESP8266 (z. B. NodeMCU oder Wemos D1 Mini), DHT11 an D4 (GPIO2), LED an D5 (GPIO14)

#include <DHT.h>

#define DHTPIN D4      // GPIO2
#define LEDPIN D5      // GPIO14
#define DHTTYPE DHT11

DHT dht(DHTPIN, DHTTYPE);

void setup() {
  pinMode(LEDPIN, OUTPUT);
  analogWriteRange(255);  // Standard für PWM auf ESP8266
  dht.begin();
}

void loop() {
  float temp = dht.readTemperature();

  if (!isnan(temp)) {
    // Mapping 20–30°C → 0–255 PWM
    int pwm = constrain((temp - 20) * 25, 0, 255);
    analogWrite(LEDPIN, pwm);
  } else {
    analogWrite(LEDPIN, 0); // Sensorfehler → LED aus
  }

  delay(1000);
}

Die Programmierung erfolgt per USB, ohne externen Programmer. Flashen, Debuggen und sogar OTA-Updates (Over The Air) sind möglich.

Variante 3: ESP8266 mit MicroPython

Technikstand: Minimalismus trifft Moderne
In nur wenigen Zeilen Python erreichen wir dasselbe Ergebnis. Ideal für alle, die lieber in Skriptsprachen arbeiten. Die MicroPython-Firmware wird per USB oder OTA aufgespielt und kann direkt über REPL oder Webinterface gesteuert werden.

Hardware: wie Variante 2

import dht
import machine
import time

sensor = dht.DHT11(machine.Pin(2))  # GPIO2 = D4
led = machine.PWM(machine.Pin(14))  # GPIO14 = D5
led.freq(1000)

while True:
    try:
        sensor.measure()
        temp = sensor.temperature()

        pwm = int((temp - 20) * 25)
        pwm = max(0, min(255, pwm))

        led.duty(pwm)
    except OSError:
        led.duty(0)  # Fehler → LED aus

    time.sleep(1)

Lesbarkeit und Einfachheit stehen hier im Vordergrund. Mehr zur MicroPython-Umgebung auf der offiziellen Seite: micropython.org

Variante 4: Assembler auf dem ATmega32

Technikstand: Nerd-Level over 9000
Wer maximale Kontrolle will (und den Schmerz nicht scheut), kann das Ganze auch in Assembler schreiben. Hier definieren wir die Bitmuster für Timer, LED und Sensorzugriff direkt.

.include "m32def.inc"     ; Register-Definitionen für ATmega32

; --------------------------------------------------------
; OC0 (PD6) = PWM-Ausgang für LED
; --------------------------------------------------------

        .org 0x00
        rjmp RESET

; --------------------------------------------------------
; Reset-Vektor
; --------------------------------------------------------

RESET:
        ; Stackpointer setzen
        ldi r16, high(RAMEND)
        out SPH, r16
        ldi r16, low(RAMEND)
        out SPL, r16

        ; PD6 (OC0) als Ausgang
        sbi DDRD, PD6

        ; Timer0: Fast PWM, non-inverting, prescaler 64
        ldi r16, (1&lt;&lt;WGM00)|(1&lt;&lt;WGM01)|(1&lt;&lt;COM01)|(1&lt;&lt;CS01)|(1&lt;&lt;CS00)
        out TCCR0, r16

        ; Initiale Helligkeit
        ldi r16, 0
        out OCR0, r16

MAIN:
        ; Erhöhe PWM-Wert langsam
        ldi r17, 0        ; Zähler

LOOP:
        mov r16, r17
        out OCR0, r16     ; PWM-Wert setzen

        rcall DELAY

        inc r17
        cpi r17, 255
        brne LOOP

        rjmp MAIN

; --------------------------------------------------------
; Einfache Delay-Routine (Software-Delay)
; --------------------------------------------------------

DELAY:
        ldi r18, 20
DELAY1: ldi r19, 255
DELAY2: dec r19
        brne DELAY2
        dec r18
        brne DELAY1
        ret

Diese Variante ist nichts für Einsteiger, aber hervorragend geeignet, um zu verstehen, wie Mikrocontroller intern arbeiten. Eine gute Referenz für Assembler-Einsteiger bietet AVR Freaks und die Original-Datenblätter vom Microchip.

BONUS: DHT11 in Assembler auslesen (Hardcore)

Ein kompletter DHT11-Bit-Banging-Reader in Assembler erfordert feingranulare Kontrolle von Mikrosekunden-Timing, Schleifen mit Timeout und Bitmanipulation. Die Umsetzung wäre ein eigener Beitrag für sich. Wer es wagen will, braucht:

  • Zyklenexakte WAIT_US-Schleifen
  • Schleifen zur HIGH-Phasen-Messung (für 0 vs. 1)
  • 5 Bytes Speicher (40 Bit) zum Zusammenbauen

Tipp: DHT11-Datenblatt genau studieren, dort stehen alle Timings. Wer sich daran wagt, verdient das Nerdabzeichen mit Goldrand.

Von der Backstein-Platine zum All-in-One-Modul

Der ursprüngliche ATmega32 war ein echter Brocken: 40 Pins, DIP-Gehäuse, externe Taktquelle, externer Programmer, keine Kommunikation ohne zusätzliche Schaltung. Im Kontrast dazu stehen moderne Boards wie der ESP32-S3 DevKit* (bezahlter Link), die WLAN, Bluetooth, USB-OTG, mehrere ADCs, DACs und SPI-/I2C-Interfaces auf kleinstem Raum vereinen – oft in der Größe eines USB-Sticks. Manche Boards, wie der LilyGO T-Display, haben sogar ein OLED- oder TFT-Display direkt mit auf dem PCB.

Was früher ein großes Steckbrett voller Kabel war, ist heute ein einziges Modul mit USB-C-Anschluss. Plug-and-play statt Kabelsalat.

Fazit: Vier Wege zur Temperaturanzeige

Ob du lieber klassisch mit direktem Registerzugriff arbeitest oder moderne Skriptsprachen bevorzugst – dieses Projekt zeigt, wie vielseitig Mikrocontroller heute einsetzbar sind. Und wer die Technik von damals noch kennt, weiß die heutigen Komfortfunktionen umso mehr zu schätzen. Aber weißt du, was noch älter als diese Schreibweise ist? XML und XSLT – ein Relikt aus der Zeit, als man Webseiten noch mit Stylesheets statt mit JavaScript animieren wollte. Wenn du denkst, Assembler sei archaisch, warte ab, bis du versuchst, eine Tabelle mit XSLT zu formatieren.

Und du?

Hast du selbst noch alte AVR-Boards in der Schublade? Oder bastelst du schon mit ESP32 und OLED direkt am Gerät?
Lass es mich wissen – kommentiere unter dem Beitrag oder schick mir deinen Aufbau per Mastodon oder E-Mail!

💬 Diskutiere mit auf nerdculture.de/@prokrastinerd

WaterMeV2 – Feuchtigkeitssensor mit ESP8266 und OLED-Display

Einleitung

Der WaterMeV2 – Feuchtigkeitssensor mit ESP8266 und OLED-Display kann die Bodenfeuchtigkeit in Echtzeit überwachen und die Daten direkt per MQTT an dein Smart Home System senden. Dieses Upgrade unseres vorherigen WaterMe Sensors bietet Verbesserungen in der Hardware und Software, eine optimierte Reset-Funktion sowie ein ansprechendes Gehäuse, das du kostenlos auf meinem Cults3D-Account herunterladen kannst (hier klicken).

WaterMeV2 Gehäuse

Vorteile von WaterMev2

  • Echtzeitüberwachung der Bodenfeuchtigkeit
  • Anbindung an MQTT für Smart Home Integration
  • WiFi-Manager für einfache WLAN-Konfiguration
  • OLED-Display für lokale Anzeige
  • 5-Sekunden-Reset-Taster für eine einfache Neukonfiguration
  • Kostenloses 3D-gedrucktes Gehäuse zum Schutz der Hardware

Benötigte Komponenten

Für dieses Projekt benötigst du folgende Komponenten (mit Affiliate-Links):

Aufbau der Hardware

Der Aufbau ist einfach und erfordert nur wenige Kabelverbindungen:

  1. ESP8266 mit dem OLED-Display verbinden:
    • SDA → D2 (GPIO4)
    • SCL → D1 (GPIO5)
    • VCC → 3.3V
    • GND → GND
  2. Feuchtigkeitssensor anschließen:
    • Signal → A0 (Analog-Pin)
    • VCC → 3.3V
    • GND → GND
  3. Taster für den Reset:
    • Ein Pin an GPIO0 (D3)
    • Der andere Pin an GND

Software – Der Code von WaterMev2

Der folgende Code enthält alle wichtigen Funktionen für den Sensor:

1. Bibliotheken und Variablen

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiManager.h>
#include <ESP8266WebServer.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <SSD1306Wire.h>
#include <LittleFS.h>
#include <ArduinoJson.h>

#define SENSOR_PIN A0
#define RESET_PIN 0
#define RESET_TIME 5000

Hier laden wir alle wichtigen Bibliotheken und definieren die Pins für den Sensor und den Reset-Taster.

2. Reset-Funktion mit 5-Sekunden-Haltezeit

void checkResetButton() {
    if (digitalRead(RESET_PIN) == LOW) {
        if (!buttonPressed) {
            buttonPressStart = millis();
            buttonPressed = true;
        }
        if (millis() - buttonPressStart >= RESET_TIME) {
            Serial.println("Reset-Taster 5 Sekunden gehalten! Setze WiFiManager-Einstellungen zurück...");
            WiFiManager wifiManager;
            wifiManager.resetSettings();
            delay(1000);
            WiFi.disconnect(true);
            delay(1000);
            ESP.restart();
        }
    } else {
        buttonPressed = false;
    }
}

Diese Funktion sorgt dafür, dass der WiFiManager nur zurückgesetzt wird, wenn der Taster mindestens 5 Sekunden gedrückt wird. Dadurch wird ein versehentliches Zurücksetzen verhindert.

3. Verbindung zum WLAN mit WiFiManager

void setup_wifi() {
    WiFiManager wifiManager;
    wifiManager.setTimeout(180);
    if (!wifiManager.autoConnect("WaterMev2")) {
        ESP.restart();
    }
}

Der WiFiManager ermöglicht eine einfache Einrichtung des WLANs über einen Access Point.

4. Sensor-Daten auslesen und per MQTT senden

void updateSensorData() {
    int sensorValue = analogRead(SENSOR_PIN);
    float moisture = map(sensorValue, 1024, 0, 0, 100);
    client.publish("WaterMev2/feuchtigkeit", String(moisture).c_str());
}

Hier wird die Feuchtigkeit gemessen und an den MQTT-Server gesendet.

5. Loop-Funktion mit Reset-Check

void loop() {
    server.handleClient();
    checkResetButton();
    updateSensorData();
    delay(30000);
}

Die loop()-Funktion überprüft den Taster und aktualisiert die Sensordaten alle 30 Sekunden.

6. Vollständiger Code

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiManager.h>  // WiFiManager Library für AP-Konfiguration
#include <ESP8266WebServer.h>
#include <time.h>        // Für configTime()
#include <SSD1306Wire.h>        // legacy: #include "SSD1306.h"
#include <LittleFS.h>
#include <ArduinoJson.h>
 
#define SENSOR_PIN A0  // Sensor an A0 angeschlossen
#define RESET_PIN 0  // GPIO0 (D3 beim ESP8266)
#define RESET_TIME 5000  // Zeit in Millisekunden (5 Sekunden)

unsigned long buttonPressStart = 0;
bool buttonPressed = false;

unsigned long previousMillis = 0;  // Speichert den Zeitpunkt des letzten MQTT-Sendevorgangs
const long interval = 30000;       // Zeitintervall für das Senden der MQTT-Daten
 
bool resetActive = false; // verhindert, dass andere Funktionen das Display während Reset stören

char mqtt_server[40] = "";
char mqtt_port[6] = "";
char mqtt_user[40] = "";
char mqtt_pass[40] = "";
char mqtt_topic_prefix[40] = "WaterMe"; // Benutzerdefiniertes Präfix
char ntp_server[40] = "pool.ntp.org";  // Standard NTP Server
 
WiFiClient espClient;
PubSubClient client(espClient);
ESP8266WebServer server(80);
SSD1306Wire display(0x3c, SDA, SCL);
 
void saveConfig() {
    Serial.println("Speichere Konfiguration in LittleFS...");
    File configFile = LittleFS.open("/config.json", "w");
    if (!configFile) {
        Serial.println("Fehler beim Speichern der Konfiguration!");
        return;
    }

    DynamicJsonDocument doc(128);
    doc["mqtt_server"] = mqtt_server;
    doc["mqtt_port"] = mqtt_port;
    doc["mqtt_user"] = mqtt_user;
    doc["mqtt_pass"] = mqtt_pass;
    doc["mqtt_topic_prefix"] = mqtt_topic_prefix;
    doc["ntp_server"] = ntp_server;

    serializeJson(doc, configFile);
    configFile.close();
    Serial.println("Konfiguration gespeichert.");
}

void loadConfig() {
    Serial.println("Lade Konfiguration aus LittleFS...");

    File configFile = LittleFS.open("/config.json", "r");
    if (!configFile) {
        Serial.println("Keine gespeicherte Konfiguration gefunden.");
        return;
    }

    DynamicJsonDocument doc(128);
    DeserializationError error = deserializeJson(doc, configFile);
    if (error) {
        Serial.println("Fehler beim Lesen der Konfigurationsdatei!");
        return;
    }

    strlcpy(mqtt_server, doc["mqtt_server"] | "", sizeof(mqtt_server));
    strlcpy(mqtt_port, doc["mqtt_port"] | "", sizeof(mqtt_port));
    strlcpy(mqtt_user, doc["mqtt_user"] | "", sizeof(mqtt_user));
    strlcpy(mqtt_pass, doc["mqtt_pass"] | "", sizeof(mqtt_pass));
    strlcpy(mqtt_topic_prefix, doc["mqtt_topic_prefix"] | "", sizeof(mqtt_topic_prefix));
    strlcpy(ntp_server, doc["ntp_server"] | "", sizeof(ntp_server));

    Serial.println("Konfiguration geladen.");
}
 
void setup_wifi() {
    Serial.println("Starte WiFiManager...");

    uint8_t mac[6];
    WiFi.macAddress(mac);

    char hostname[32];
    snprintf(hostname, sizeof(hostname), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    WiFiManager wifiManager;
    wifiManager.setTimeout(180); // 3 Minuten warten statt unendlich

    //wifiManager.resetSettings();

    WiFiManagerParameter custom_mqtt_server("server", "MQTT Server", mqtt_server, 40);
    WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", mqtt_port, 6);
    WiFiManagerParameter custom_mqtt_topic("topic", "MQTT Topic Prefix", mqtt_topic_prefix, 40);
    WiFiManagerParameter custom_ntp_server("ntp_server", "NTP Server", ntp_server, 40);
 
    wifiManager.addParameter(&custom_mqtt_server);
    wifiManager.addParameter(&custom_mqtt_port);
    wifiManager.addParameter(&custom_mqtt_topic);
    wifiManager.addParameter(&custom_ntp_server);

    if (!wifiManager.autoConnect(hostname)) {
        Serial.println("Fehlgeschlagen! Neustart in 5 Sekunden...");
        delay(5000);
        ESP.restart();
    }

    WiFi.hostname(hostname);

    strncpy(mqtt_server, custom_mqtt_server.getValue(), sizeof(mqtt_server) - 1);
    strncpy(mqtt_port, custom_mqtt_port.getValue(), sizeof(mqtt_port) - 1);
    strncpy(mqtt_topic_prefix, custom_mqtt_topic.getValue(), sizeof(mqtt_topic_prefix) - 1);
    strncpy(ntp_server, custom_ntp_server.getValue(), sizeof(ntp_server) - 1);
    saveConfig();
    Serial.println("WLAN verbunden!");
    Serial.print("IP-Adresse: ");
    Serial.println(WiFi.localIP());
}
 
void handleRoot() {
    Serial.println("Web-Oberfläche aufgerufen");
    String htmlContent = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>WaterMe Einstellungen</title>"
                         "<style>"
                         "body { font-family: Arial, sans-serif; margin: 40px; background-color: #333; color: #fff; }"
                         "h1 { color: #fff; }"
                         "form { background-color: #222; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px 0 rgba(0,0,0,0.5); }"
                         "label { display: block; margin-top: 20px; margin-bottom: 5px; color: #ccc; }"
                         "input[type='text'], input[type='submit'] { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #555; border-radius: 4px; }"
                         "input[type='text'] { background-color: #555; color: #ddd; }"
                         "input[type='submit'] { background-color: #008CBA; color: white; cursor: pointer; }"
                         "input[type='submit']:hover { background-color: #005f7a; }"
                         "</style>"
                         "</head><body>"
                         "<h1>WaterMe Einstellungen</h1>"
                         "<form action='/save' method='POST'>"
                         "<label for='server'>MQTT Server:</label> <input type='text' id='server' name='server' value='" + String(mqtt_server) + "'><br>"
                         "<label for='port'>MQTT Port:</label> <input type='text' id='port' name='port' value='" + String(mqtt_port) + "'><br>"
                         "<label for='user'>MQTT Benutzer:</label> <input type='text' id='user' name='user' value='" + String(mqtt_user) + "'><br>"
                         "<label for='pass'>MQTT Passwort:</label> <input type='password' id='pass' name='pass' value='" + String(mqtt_pass) + "'><br>"
                         "<label for='topic'>MQTT Topic Prefix:</label> <input type='text' id='topic' name='topic' value='" + String(mqtt_topic_prefix) + "'><br>"
                         "<label for='ntp_server'>NTP Server:</label> <input type='text' id='ntp_server' name='ntp_server' value='" + String(ntp_server) + "'><br>"
                         "<input type='submit' value='Speichern'>"
                         "</form></body></html>";
    server.send(200, "text/html", htmlContent);
}
 
void handleSave() {
    Serial.println("Speichere neue Einstellungen...");
    if (server.hasArg("server")) strncpy(mqtt_server, server.arg("server").c_str(), sizeof(mqtt_server) - 1);
    if (server.hasArg("port")) strncpy(mqtt_port, server.arg("port").c_str(), sizeof(mqtt_port) - 1);
    if (server.hasArg("user")) strlcpy(mqtt_user, server.arg("user").c_str(), sizeof(mqtt_user));
    if (server.hasArg("pass")) strlcpy(mqtt_pass, server.arg("pass").c_str(), sizeof(mqtt_pass));
    if (server.hasArg("topic")) strncpy(mqtt_topic_prefix, server.arg("topic").c_str(), sizeof(mqtt_topic_prefix) - 1);
    if (server.hasArg("ntp_server")) strncpy(ntp_server, server.arg("ntp_server").c_str(), sizeof(ntp_server) - 1);
    saveConfig();
    server.send(200, "text/html", "<html><body><h1>Gespeichert! Neustart...</h1></body></html>");
    delay(3000);
    ESP.restart();
}
 
void checkResetButton() {
    static int lastSecondsLeft = -1;

    if (digitalRead(RESET_PIN) == LOW) {
        if (!buttonPressed) {
            buttonPressStart = millis();
            buttonPressed = true;
            resetActive = true;
            lastSecondsLeft = -1;
        }

        unsigned long heldTime = millis() - buttonPressStart;
        int secondsLeft = (RESET_TIME - heldTime) / 1000;

        if (secondsLeft != lastSecondsLeft && heldTime < RESET_TIME) {
            lastSecondsLeft = secondsLeft;

            display.clear();
            display.setFont(ArialMT_Plain_16);
            display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
            display.drawString(display.getWidth() / 2, 20, "Reset in");
            display.setFont(ArialMT_Plain_24);
            display.drawString(display.getWidth() / 2, 44, String(secondsLeft));
            display.display();
        }

        if (heldTime >= RESET_TIME) {
            display.clear();
            display.setFont(ArialMT_Plain_16);
            display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
            display.drawString(display.getWidth() / 2, 28, "RESET!");
            display.display();
            delay(1000);

            Serial.println("Reset-Taster 5 Sekunden gehalten! Setze WiFiManager-Einstellungen zurück...");
            WiFiManager wifiManager;
            wifiManager.resetSettings();
            delay(500);
            WiFi.disconnect(true);
            delay(500);
            ESP.restart();
        }

    } else {
        if (buttonPressed) {
            buttonPressed = false;
            resetActive = false;

            // <<< Hier neu ergänzen:
            previousMillis = millis() - interval;  
            // <<< Dadurch wird beim nächsten loop() direkt aktualisiert!
        }
    }
}

void setup() {
    pinMode(RESET_PIN, INPUT_PULLUP);
    Serial.begin(115200);
    Serial.println("WaterMe startet...");

    if (!LittleFS.begin()) {
        Serial.println("LittleFS-Fehler");
    } else {
        loadConfig();
    }

    // *** Display direkt am Anfang initialisieren und "AP" anzeigen ***
    display.init();
    display.flipScreenVertically();
    display.clear();
    display.setFont(ArialMT_Plain_24);
    display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
    display.drawString(display.getWidth() / 2, 28, "AP");
    display.display();
    delay(500);  // Kurze Verzögerung (optional), damit man es sicher sieht

    setup_wifi();
 
    if (strlen(mqtt_server) > 0 && strlen(mqtt_port) > 0) {
        client.setServer(mqtt_server, atoi(mqtt_port));
        Serial.println("MQTT-Konfiguration geladen.");
    } else {
        Serial.println("MQTT-Daten fehlen! Webinterface zur Konfiguration nutzen.");
    }
 
    server.on("/", handleRoot);
    server.on("/save", HTTP_POST, handleSave);
    server.begin();
    Serial.println("Webserver gestartet!");
    Serial.print("Rufe auf: http://");
    Serial.println(WiFi.localIP());

    // Sofortiges erstes Update erzwingen
    previousMillis = millis() - interval;
    handleMQTTAndDisplayUpdate(millis());

    // Sommerzeitregel für Mitteleuropa: letzte So im März + Okt
    configTime("CET-1CEST,M3.5.0/2,M10.5.0/3", ntp_server);

    Serial.print("Warte auf Zeit...");
    time_t now = time(nullptr);
    while (now < 100000) {
        delay(100);
        Serial.print(".");
        now = time(nullptr);
    }
    Serial.println(" Zeit empfangen.");
}
 
void reconnect() {
    static unsigned long lastReconnectAttempt = 0;
    uint8_t mac[6];
    WiFi.macAddress(mac);
    char clientId[32];
    snprintf(clientId, sizeof(clientId), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    if (!client.connected() && millis() - lastReconnectAttempt > 5000) {
        lastReconnectAttempt = millis();
        Serial.print("Verbindungsversuch mit MQTT-Broker als ");
        Serial.println(clientId);

        if (client.connect(clientId, mqtt_user, mqtt_pass)) {
            Serial.println("MQTT-Verbindung hergestellt!");
        } else {
            Serial.print("MQTT-Verbindung fehlgeschlagen, Fehlercode: ");
            Serial.print(client.state());
            Serial.println(" - Neuer Versuch in 5 Sekunden...");
        }
    }
}

void loop() {
    checkResetButton();

    if (resetActive) {
        return;
    }

    server.handleClient();

    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    handleMQTTAndDisplayUpdate(millis());
}

void handleMQTTAndDisplayUpdate(unsigned long currentMillis) {
    if (resetActive) return;

    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        if (!client.connected()) {
            reconnect();
        }
        updateSensorData();
    }
}

void updateSensorData() {
    if (resetActive) return;
    int sensorValue = analogRead(SENSOR_PIN);
    float moisture = map(sensorValue, 1024, 0, 0, 100);

    time_t now = time(nullptr);
    struct tm *timeinfo = localtime(&now);
    char buffer[20];
    strftime(buffer, sizeof(buffer), "%H:%M:%S", timeinfo);
    String timestamp = String(buffer);

    updateDisplay(moisture, timestamp);
    sendMQTTData(moisture, timestamp);
}

void updateDisplay(float moisture, String timestamp) {
    Serial.println("Aktualisiere Display...");
    Serial.print("Feuchtigkeit: "); Serial.println(moisture);
    Serial.print("Timestamp: "); Serial.println(timestamp);

    String text = String(moisture, 0) + "%";
    display.clear();
    display.setFont(ArialMT_Plain_24);
    display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
    display.drawString(display.getWidth() / 2, 28, text);
    display.drawLine(0, 42, 128, 42);
    display.setFont(ArialMT_Plain_16);
    display.drawString(display.getWidth() / 2, 54, timestamp);
    display.display();
}

void sendMQTTData(float moisture, const String& timestamp) {
    char topic[50], message[50];

    snprintf(topic, sizeof(topic), "%s/feuchtigkeit", mqtt_topic_prefix);
    snprintf(message, sizeof(message), "%.2f", moisture);
    client.publish(topic, message);

    snprintf(topic, sizeof(topic), "%s/timestamp", mqtt_topic_prefix);
    client.publish(topic, timestamp.c_str());
}

Gehäuse zum 3D-Drucken

Damit die Elektronik geschützt ist, kannst du dir ein passendes Gehäuse für WaterMev2 kostenlos auf meinem Cults3D-Account herunterladen: Hier geht’s zum Gehäuse.

WaterMeV2 Gehäuse mit ESP8266 und OLED Display

Fazit

Mit dem WaterMeV2 – Feuchtigkeitssensor mit ESP8266 und OLED-Display kannst du ganz einfach Werte überwachen und in dein Smart Home System integrieren. Dank der MQTT-Integration kannst du die Daten bequem weiterverarbeiten und mit dem OLED-Display behältst du stets den Überblick. Baue dein eigenes WaterMeV2 jetzt nach und lade dir das passende Gehäuse herunter!


Hast du Fragen oder Verbesserungsvorschläge? Schreib sie in die Kommentare!

WaterMe – DIY Bewässerungssystem mit ESP8266 und Gehäuse aus dem 3D-Drucker

Für alle, die gleichzeitig Technikfreaks und Hobbygärtner sind, habe ich hier etwas Spannendes: das WaterMe DIY Bewässerungssystem. Ein DIY-Bewässerungssystem, das auf dem cleveren ESP8266 Mikrocontroller basiert, um deine Pflanzen optimal und smart zu versorgen.

Die Bauteile des WaterMe DIY Bewässerungssystem

1. Der Bodenfeuchtesensor: Kernstück des Systems ist der kapazitive Bodenfeuchtesensor* (bezahlter Link), der dank seiner Technologie dauerhaft und zuverlässig die Feuchtigkeit im Boden misst.

2. Der D1 Mini ESP8266 Entwicklungsboard: Der D1 Mini* (bezahlter Link) ist ein kleines Kraftpaket mit WLAN-Fähigkeit, ideal für alle IoT-Projekte.

3. Das 3D-gedruckte Gehäuse: Mein selbst entworfenes Gehäuse ist auf Cults3D kostenlos erhältlich und nutzt Gewindeeinsätze* (bezahlter Link). Hier findet Ihr passende Mikro-Schrauben* (bezahlter Link).

WaterMe – DIY Bewässerungssystem mit 3D gedrucktem Gehäuse

Erweiterte Funktionen und technische Details

Das „WaterMe“ System nutzt einen AP für die WLAN-Verbindung, MQTT und NTP, um die Funktionalität über das reine Messen der Bodenfeuchtigkeit hinaus zu erweitern.

MQTT (Message Queuing Telemetry Transport): Dieses leichte und effiziente Protokoll ermöglicht es dem System, Messdaten über das Internet zu versenden. Im Code werden MQTT-Einstellungen konfiguriert, um Sensorwerte an einen Server zu senden, der diese dann für Monitoring oder automatische Bewässerungsaktionen verwenden kann.

NTP (Network Time Protocol): Die Integration von NTP hilft dabei, die exakte Zeit für das Logging der Sensorwerte zu erhalten. Dies ist besonders nützlich, um zu bestimmen, wann die Pflanzen zuletzt gegossen wurden und wann sie wieder Wasser benötigen.

Anschluss des ESP

Der Anschluss des ESP ist sehr einfach. Es muss lediglich die rote Plus-Leitung an 5V, die schwarze Minus-Leitung an GND und die gelbe Signalleitung an A0 des ESP angeschlossen werden.

WaterMe – DIY Bewässerungssystem mit ESP8266

Arduino Code Erklärung

Der untenstehende Arduino-Sketch ist das Herzstück von des WaterMe DIY Bewässerungssystem. Er verbindet den D1 Mini mit deinem WLAN, misst die Bodenfeuchtigkeit und sendet diese Daten über MQTT. Den kompletten Code findest du nachfolgend:

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiManager.h>  // WiFiManager Library für AP-Konfiguration
#include <ESP8266WebServer.h>
#include <NTPClient.h>  // Bibliothek für NTP-Client
#include <WiFiUdp.h>          // Only needed for Arduino 1.6.5 and earlier
#include <LittleFS.h>
#include <ArduinoJson.h>
 
#define SENSOR_PIN A0  // Sensor an A0 angeschlossen

unsigned long previousMillis = 0;  // Speichert den Zeitpunkt des letzten MQTT-Sendevorgangs
const long interval = 30000;       // Zeitintervall für das Senden der MQTT-Daten
 
char mqtt_server[40] = "";
char mqtt_port[6] = "";
char mqtt_topic_prefix[40] = "WaterMe"; // Benutzerdefiniertes Präfix
char ntp_server[40] = "pool.ntp.org";  // Standard NTP Server
 
WiFiClient espClient;
PubSubClient client(espClient);
ESP8266WebServer server(80);
WiFiUDP udp;
NTPClient timeClient(udp, ntp_server, 3600, 60000);  // Zeitzone +1 für Deutschland
 
void saveConfig() {
    Serial.println("Speichere Konfiguration in LittleFS...");
    File configFile = LittleFS.open("/config.json", "w");
    if (!configFile) {
        Serial.println("Fehler beim Speichern der Konfiguration!");
        return;
    }

    DynamicJsonDocument doc(128);
    doc["mqtt_server"] = mqtt_server;
    doc["mqtt_port"] = mqtt_port;
    doc["mqtt_topic_prefix"] = mqtt_topic_prefix;
    doc["ntp_server"] = ntp_server;

    serializeJson(doc, configFile);
    configFile.close();
    Serial.println("Konfiguration gespeichert.");
}

void loadConfig() {
    Serial.println("Lade Konfiguration aus LittleFS...");

    File configFile = LittleFS.open("/config.json", "r");
    if (!configFile) {
        Serial.println("Keine gespeicherte Konfiguration gefunden.");
        return;
    }

    DynamicJsonDocument doc(128);
    DeserializationError error = deserializeJson(doc, configFile);
    if (error) {
        Serial.println("Fehler beim Lesen der Konfigurationsdatei!");
        return;
    }

    strlcpy(mqtt_server, doc["mqtt_server"] | "", sizeof(mqtt_server));
    strlcpy(mqtt_port, doc["mqtt_port"] | "", sizeof(mqtt_port));
    strlcpy(mqtt_topic_prefix, doc["mqtt_topic_prefix"] | "", sizeof(mqtt_topic_prefix));
    strlcpy(ntp_server, doc["ntp_server"] | "", sizeof(ntp_server));

    Serial.println("Konfiguration geladen.");
}
 
void setup_wifi() {
    Serial.println("Starte WiFiManager...");

    uint8_t mac[6];
    WiFi.macAddress(mac);

    char hostname[32];
    snprintf(hostname, sizeof(hostname), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    WiFiManager wifiManager;
    wifiManager.setTimeout(180); // 3 Minuten warten statt unendlich

    //wifiManager.resetSettings();

    WiFiManagerParameter custom_mqtt_server("server", "MQTT Server", mqtt_server, 40);
    WiFiManagerParameter custom_mqtt_port("port", "MQTT Port", mqtt_port, 6);
    WiFiManagerParameter custom_mqtt_topic("topic", "MQTT Topic Prefix", mqtt_topic_prefix, 40);
    WiFiManagerParameter custom_ntp_server("ntp_server", "NTP Server", ntp_server, 40);
 
    wifiManager.addParameter(&custom_mqtt_server);
    wifiManager.addParameter(&custom_mqtt_port);
    wifiManager.addParameter(&custom_mqtt_topic);
    wifiManager.addParameter(&custom_ntp_server);

    if (!wifiManager.autoConnect(hostname)) {
        Serial.println("Fehlgeschlagen! Neustart in 5 Sekunden...");
        delay(5000);
        ESP.restart();
    }

    WiFi.hostname(hostname);

    strncpy(mqtt_server, custom_mqtt_server.getValue(), sizeof(mqtt_server) - 1);
    strncpy(mqtt_port, custom_mqtt_port.getValue(), sizeof(mqtt_port) - 1);
    strncpy(mqtt_topic_prefix, custom_mqtt_topic.getValue(), sizeof(mqtt_topic_prefix) - 1);
    strncpy(ntp_server, custom_ntp_server.getValue(), sizeof(ntp_server) - 1);
    saveConfig();
    Serial.println("WLAN verbunden!");
    Serial.print("IP-Adresse: ");
    Serial.println(WiFi.localIP());
}
 
void handleRoot() {
    Serial.println("Web-Oberfläche aufgerufen");
    String htmlContent = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>WaterMe Einstellungen</title>"
                         "<style>"
                         "body { font-family: Arial, sans-serif; margin: 40px; background-color: #333; color: #fff; }"
                         "h1 { color: #fff; }"
                         "form { background-color: #222; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px 0 rgba(0,0,0,0.5); }"
                         "label { display: block; margin-top: 20px; margin-bottom: 5px; color: #ccc; }"
                         "input[type='text'], input[type='submit'] { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #555; border-radius: 4px; }"
                         "input[type='text'] { background-color: #555; color: #ddd; }"
                         "input[type='submit'] { background-color: #008CBA; color: white; cursor: pointer; }"
                         "input[type='submit']:hover { background-color: #005f7a; }"
                         "</style>"
                         "</head><body>"
                         "<h1>WaterMe Einstellungen</h1>"
                         "<form action='/save' method='POST'>"
                         "<label for='server'>MQTT Server:</label> <input type='text' id='server' name='server' value='" + String(mqtt_server) + "'><br>"
                         "<label for='port'>MQTT Port:</label> <input type='text' id='port' name='port' value='" + String(mqtt_port) + "'><br>"
                         "<label for='topic'>MQTT Topic Prefix:</label> <input type='text' id='topic' name='topic' value='" + String(mqtt_topic_prefix) + "'><br>"
                         "<label for='ntp_server'>NTP Server:</label> <input type='text' id='ntp_server' name='ntp_server' value='" + String(ntp_server) + "'><br>"
                         "<input type='submit' value='Speichern'>"
                         "</form></body></html>";
    server.send(200, "text/html", htmlContent);
}
 
void handleSave() {
    Serial.println("Speichere neue Einstellungen...");
    if (server.hasArg("server")) strncpy(mqtt_server, server.arg("server").c_str(), sizeof(mqtt_server) - 1);
    if (server.hasArg("port")) strncpy(mqtt_port, server.arg("port").c_str(), sizeof(mqtt_port) - 1);
    if (server.hasArg("topic")) strncpy(mqtt_topic_prefix, server.arg("topic").c_str(), sizeof(mqtt_topic_prefix) - 1);
    if (server.hasArg("ntp_server")) strncpy(ntp_server, server.arg("ntp_server").c_str(), sizeof(ntp_server) - 1);
    saveConfig();
    server.send(200, "text/html", "<html><body><h1>Gespeichert! Neustart...</h1></body></html>");
    delay(3000);
    ESP.restart();
}
 
void setup() {
    Serial.begin(115200);
    Serial.println("WaterMe startet...");

    if (!LittleFS.begin()) {
        Serial.println("LittleFS-Fehler");
    } else {
        loadConfig();
    }

    setup_wifi();
    timeClient.begin();
 
    if (strlen(mqtt_server) > 0 && strlen(mqtt_port) > 0) {
        client.setServer(mqtt_server, atoi(mqtt_port));
        Serial.println("MQTT-Konfiguration geladen.");
    } else {
        Serial.println("MQTT-Daten fehlen! Webinterface zur Konfiguration nutzen.");
    }
 
    server.on("/", handleRoot);
    server.on("/save", HTTP_POST, handleSave);
    server.begin();
    Serial.println("Webserver gestartet!");
    Serial.print("Rufe auf: http://");
    Serial.println(WiFi.localIP());

    // Sofortiges erstes Update erzwingen
    previousMillis = millis() - interval;  // Simuliert ein vergangenes Intervall
    handleMQTTUpdate(millis());
}
 
void reconnect() {
    static unsigned long lastReconnectAttempt = 0;
    uint8_t mac[6];
    WiFi.macAddress(mac);
    char clientId[32];
    snprintf(clientId, sizeof(clientId), "WaterMe_%02X%02X%02X", mac[3], mac[4], mac[5]);

    if (!client.connected() && millis() - lastReconnectAttempt > 5000) {
        lastReconnectAttempt = millis();
        Serial.print("Verbindungsversuch mit MQTT-Broker als ");
        Serial.println(clientId);

        if (client.connect(clientId)) {
            Serial.println("MQTT-Verbindung hergestellt!");

            // Falls du Topics abonnieren willst, füge sie hier hinzu:
            // client.subscribe("WaterMe/steuerung");  
            // client.subscribe("WaterMe/config");

        } else {
            Serial.print("MQTT-Verbindung fehlgeschlagen, Fehlercode: ");
            Serial.print(client.state());  // Gibt den Fehlercode aus
            Serial.println(" - Neuer Versuch in 5 Sekunden...");
        }
    }
}

void loop() {
    server.handleClient();
    timeClient.update();

    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    handleMQTTUpdate(millis());
}

void handleMQTTUpdate(unsigned long currentMillis) {
    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        if (!client.connected()) {
            reconnect();
        }
        updateSensorData();
    }
}

void updateSensorData() {
    int sensorValue = analogRead(SENSOR_PIN);  // Nur einmal aufrufen
    float moisture = map(sensorValue, 1024, 0, 0, 100);
    String timestamp = timeClient.getFormattedTime();

    sendMQTTData(moisture, timestamp);
}

void sendMQTTData(float moisture, const String& timestamp) {
    char topic[50], message[50];

    snprintf(topic, sizeof(topic), "%s/feuchtigkeit", mqtt_topic_prefix);
    snprintf(message, sizeof(message), "%.2f", moisture);
    client.publish(topic, message);

    snprintf(topic, sizeof(topic), "%s/timestamp", mqtt_topic_prefix);
    client.publish(topic, timestamp.c_str());
}