Mystik auf Bestellung: Der autonome Nebel-Wächter für Fotografen

Wer gerne mit der Kamera in der Natur unterwegs ist, sucht diese Szene: Jener Moment, in dem die Landschaft aus einem dichten Nebelmeer auftaucht. Doch oft entscheidet man sich falsch: Entweder man steht umsonst um 4 Uhr morgens in der Kälte, oder man verschläft den perfekten Moment.

Meine Lösung schaut auf die Physik: Nebel entsteht, wenn die Lufttemperatur auf den Taupunkt sinkt. Ich baue mir einen autonomen „Nebel-Wächter“ mit einem ESP32. Dieser misst kontinuierlich Temperatur, Luftdruck und Luftfeuchtigkeit, speichert historische Daten inklusive GPS-Koordinaten auf eine SD-Karte auf und weckt mich per Bluetooth, wenn Zeit und Umweltbedingungen passen.

GPS für die perfekte Zeit und Geotagging

Ein häufiges Problem in der Natur: Ein kurzer Regenschauer um 2 Uhr nachts drückt die Temperatur auf den Taupunkt und löst einen Fehlalarm aus. Für mich als Fotografen ist aber nur das Zeitfenster rund um den Sonnenaufgang relevant.

Ich nutze ein GPS-Modul, das mir weltweit nicht nur die exakte Position, sondern auch eine hochpräzise Uhrzeit liefert. Damit berechnet der Mikrocontroller den tagesaktuellen Sonnenaufgang für meinen Standort und schlägt nur im relevanten Fenster Alarm.

Der Zusatzbonus für Fotografen: Alle Messungen werden mit genauem Datum, Uhrzeit und Koordinaten auf einer SD-Karte gespeichert. Diese CSV-Daten kann man nach der Reise nutzen, um die entstandenen Fotos (z.B. über ExifTool oder Lightroom) automatisch mit Geotags zu versehen, indem das Aufnahmedatum mit dem GPS-Log abgeglichen wird.

WLAN für Daten, Bluetooth für den Alarm

Um den Komfort zu maximieren, nutze ich die volle Leistung des ESP32: Das Modul strahlt ein eigenes WLAN aus. Verbinde ich mein Smartphone damit, kann ich über den Browser ein Dashboard aufrufen und die CSV-Daten herunterladen.

Damit ich aber nicht ständig in diesem WLAN bleiben muss, schickt der ESP32 den eigentlichen Alarm via Bluetooth. So kann die Hardware draußen in der Kälte bleiben, während ich im Fahrzeug über eine Bluetooth-Terminal-App punktgenau geweckt wirst.

Das Setup: Hardware für den professionellen Einsatz

  • ESP32 Development Board (z.B. NodeMCU)
  • BME280 Sensor (Temperatur, Luftfeuchtigkeit, Luftdruck via I2C)
  • NEO-6M GPS-Modul (via UART)
  • (Micro)SD-Kartenmodul (via SPI)
  • Powerbank oder LiPo-Akku

Der Code: Webserver, GPS, Bluetooth und SD-Karte

Der folgende Code integriert alle Komponenten. Er schreibt die Daten sauber formatiert auf die SD-Karte, stellt das WLAN-Dashboard bereit und sendet den Alarm über Bluetooth.

/*
 * ESP32 Nebel-Waechter PRO (Geotagging Edition)
 * Features: GPS, BME280, SD-Karte (CSV-Logger), WebServer (AP), Bluetooth Alarm
 * Benoetigte Libs: Adafruit BME280, TinyGPSPlus, Dusk2Dawn
 */

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <SD.h>
#include <TinyGPSPlus.h>
#include <Dusk2Dawn.h>
#include "BluetoothSerial.h"

Adafruit_BME280 bme;
TinyGPSPlus gps;
WebServer server(80);
BluetoothSerial SerialBT;

// Hardware Serial 2 fuer das GPS Modul (RX=16, TX=17)
#define GPS_RX 16
#define GPS_TX 17

// CS Pin fuer das SD-Kartenmodul
const int SD_CS = 5;

// WLAN Access Point Zugangsdaten
const char* ssid = "NebelWaechter";
const char* password = "fotografie-pro";

unsigned long lastLogTime = 0;
const unsigned long logInterval = 300000; // 5 Minuten in Millisekunden
const float threshold = 1.0;

// Globale Variablen fuer GPS
float currentLat = 0.0;
float currentLon = 0.0;
int currentHour = 0;
int currentMinute = 0;

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
  SerialBT.begin("Nebel-Waechter");

  if (!bme.begin(0x76)) {
    Serial.println("BME280 nicht gefunden!");
    while (1);
  }

  if (!SD.begin(SD_CS)) {
    Serial.println("SD-Karte Mount fehlgeschlagen!");
    return;
  }

  // Access Point starten
  WiFi.softAP(ssid, password);

  // Webserver Routen
  server.on("/", handleRoot);
  server.on("/data", handleData);
  server.begin();
}

float calculateDewPoint(float temp, float humidity) {
  float a = 17.27;
  float b = 237.7;
  float alpha = ((a * temp) / (b + temp)) + log(humidity / 100.0);
  return (b * alpha) / (a - alpha);
}

void logData(float temp, float hum, float dew) {
  File file = SD.open("/geolog.csv", FILE_APPEND);
  if (!file) return;

  // Header schreiben, falls Datei leer ist
  if (file.size() == 0) {
    file.println("Date Time,Latitude,Longitude,Temperature,Humidity,DewPoint");
  }

  // Zeitstempel formatieren: YYYY-MM-DD HH:MM:SS
  char dateStr[32];
  if (gps.date.isValid() && gps.time.isValid()) {
    sprintf(dateStr, "%04d-%02d-%02d %02d:%02d:%02d",
            gps.date.year(), gps.date.month(), gps.date.day(),
            currentHour, currentMinute, gps.time.second());
  } else {
    sprintf(dateStr, "No GPS Fix");
  }

  file.print(dateStr); file.print(",");
  file.print(currentLat, 6); file.print(",");
  file.print(currentLon, 6); file.print(",");
  file.print(temp); file.print(",");
  file.print(hum); file.print(",");
  file.println(dew);
  file.close();
}

void handleRoot() {
  String html = "<!DOCTYPE html><html><head><title>Nebel-Wächter</title>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<style>body{font-family:sans-serif; padding:20px;} .box{background:#f4f4f4; padding:15px; border-radius:8px; margin-bottom:20px;}</style></head><body>";
  html += "<h1>Nebel-Wächter Dashboard</h1>";

  float temp = bme.readTemperature();
  float dew = calculateDewPoint(temp, bme.readHumidity());

  html += "<div class='box'><h2>Aktuelle Werte</h2>";
  html += "Temperatur: " + String(temp) + " °C<br>";
  html += "Taupunkt: " + String(dew) + " °C<br>";
  html += "Differenz: " + String(temp - dew) + " °C</div>";

  html += "<div class='box'><h2>GPS Status</h2>";
  if (gps.location.isValid()) {
    html += "Verbunden. Lat: " + String(currentLat, 5) + " Lon: " + String(currentLon, 5);
  } else {
    html += "Suche Satelliten...";
  }
  html += "</div>";

  html += "<h3><a href='/data'>SD-Karten Log (CSV) abrufen</a></h3>";
  html += "</body></html>";

  server.send(200, "text/html", html);
}

void handleData() {
  File file = SD.open("/geolog.csv", "r");
  if (!file) {
    server.send(404, "text/plain", "Keine Daten auf SD-Karte gefunden");
    return;
  }
  server.streamFile(file, "text/csv");
  file.close();
}

void loop() {
  while (Serial2.available() > 0) {
    gps.encode(Serial2.read());
  }

  if (gps.location.isValid() && gps.time.isValid()) {
    currentLat = gps.location.lat();
    currentLon = gps.location.lng();
    currentHour = gps.time.hour(); // Ggf. Zeitzone addieren
    currentMinute = gps.time.minute();
  }

  server.handleClient();

  if (millis() - lastLogTime > logInterval) {
    float temp = bme.readTemperature();
    float hum = bme.readHumidity();
    float dew = calculateDewPoint(temp, hum);

    logData(temp, hum, dew);

    // Alarm-Logik fuer Sonnenaufgang
    if (gps.location.isValid() && gps.date.isValid()) {
      Dusk2Dawn location(currentLat, currentLon, 2); // 2 = Zeitzone Rumaenien
      int sunriseMinutes = location.sunrise(gps.date.year(), gps.date.month(), gps.date.day(), false);
      int curMins = currentHour * 60 + currentMinute;
      
      bool isAlarmTime = (curMins >= (sunriseMinutes - 120)) && (curMins <= (sunriseMinutes + 120));
      
      if ((temp - dew) < threshold && isAlarmTime) {
         SerialBT.println("ALARM: Nebelbildung im Zeitfenster!");
      }
    }
    lastLogTime = millis();
  }
}

Auswertung im Browser und Geotagging

Wenn ich abends an einem Spot angekommen bin, positioniere ich das Gerät draußen, verbinde mein Smartphone mit dem WLAN-Netzwerk „NebelWaechter“, und kann über die IP-Adresse 192.168.4.1 die Live-Daten kontrollieren.

Die passenden Terminal-Apps für den Alarm

Um den Alarm zu empfangen, wird das Smartphone zusätzlich via Bluetooth gekoppelt. Hier sind die besten Apps für die jeweiligen Betriebssysteme:

Für Android: „Serial Bluetooth Terminal“ (von Kai Morich)
Dies ist das Standard-Werkzeug für solche Projekte und im Play Store verfügbar. In den Einstellungen der App lässt sich unter „Receive“ eine sogenannte „Receive Action“ oder ein Filter definieren. Sobald der Text „ALARM“ vom ESP32 gesendet wird, spielt die App einen lauten Benachrichtigungston ab. Das Smartphone kann dabei den Bildschirm ausschalten und stromsparend im Standby bleiben.

Für iPhone (iOS): Wichtiger technischer Hinweis
Apple blockiert im iPhone das sogenannte „Classic Bluetooth“ (SPP – Serial Port Profile), welches im obigen, schlanken C++ Code verwendet wird. Für das iPhone gibt es hervorragende Apps wie „Bluefruit Connect“ (von Adafruit) oder „BLE Terminal“. Um diese zu nutzen, muss der ESP32-Code jedoch von Classic Bluetooth auf BLE (Bluetooth Low Energy) umgeschrieben werden. Da das den Code extrem aufbläht und verkompliziert, nutzen viele Fotografen in der Praxis einen simplen Workaround: Sie nehmen einfach ein altes, ausgemustertes Android-Smartphone als reinen Alarm-Empfänger mit in den Rucksack.

Wieder zu Hause kann ich die geolog.csv direkt von der SD-Karte kopieren. Das Format (Date Time, Latitude, Longitude) ist ideal aufgebaut, um es als Tracklog in meine Fotosoftware einzuspeisen und meinen Bildern exakte Ortsdaten zuzuweisen. Ein mächtiges, kleines Werkzeug, das mich zur richtigen Zeit weckt und danach auch noch mein Archiv bereichert.