Dies ist eine alte Version des Dokuments!


Programmcode der Sensor-Boxen

Diese Seite enthält den vollständigen Arduino-Code aller Sensor-Boxen, ausgelagert aus der Hauptanleitung und der EmonCMS-Einbindungsseite. Pin-Belegung, Funktionsprinzipien und Stolpersteine stehen weiterhin in der Hauptanleitung (Teil 3) – hier geht es nur um den Code selbst.

Netzwerk-Auswahlschema (Bereich A/B/C)

Alle Sketches auf dieser Seite nutzen dasselbe Schema, um zwischen drei Netzwerk-/Betriebsprofilen umzuschalten, ohne jedes Mal Zeilen aus- und einzukommentieren:

  • Bereich A – SAS-ARDUINO-mobil: persönliches Testnetz (z. B. Hotspot zuhause), zum Entwickeln und Testen von Sketches, bevor sie ans Schulnetz gehen
  • Bereich B – SAS-ARDUINO, Schul-Testbetrieb: isoliertes Schulnetz aus Teil 1 der Hauptanleitung, sendet mit dem TEST-API-Key (landet nicht in den „echten“ Live-Feeds)
  • Bereich C – SAS-ARDUINO, Schul-Livebetrieb: dasselbe Schulnetz, sendet mit dem LIVE-API-Key (echter Dauerbetrieb)

Im Kopf jedes Sketches wird genau eine der drei Zeilen auf true gesetzt:

#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif

Darunter folgen drei #if-Blöcke mit den jeweiligen Zugangsdaten. Der Compiler übernimmt automatisch nur den aktiven Block – die beiden anderen werden komplett ignoriert (kein Speicherverbrauch, keine doppelten Variablendefinitionen).

Wichtig bei Weitergabe an andere Schulen: Bereich A (SAS-ARDUINO-mobil)
enthält persönliche/private Zugangsdaten und sollte vor der
Veröffentlichung KOMPLETT gelöscht werden (den ganzen #if...#endif-
Block). Bereich B und C beziehen sich auf das isolierte, nach der
Hauptanleitung bei jeder Schule identisch aufgebaute Schulnetz
("SAS-ARDUINO") und können unverändert als Vorlage dienen - dort
müssen andere Schulen nur ihr eigenes WLAN-Passwort und ihre eigenen
API-Keys eintragen.

Testsensor (ohne echte Sensoren)

Dient ausschließlich dazu, die EmonCMS-Einrichtung (Inputs, Feeds, Dashboard) zu testen, bevor eine echte Sensor-Box angeschlossen ist. Es werden keine Sensoren ausgelesen – die Werte für Temperatur, Luftfeuchtigkeit und CO2 werden im Code simuliert (Temperatur/Feuchte als sanft schwankende Kurve, CO2 als leicht schwankender Zufallswert um 400 ppm).

Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT
für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle
in der Hauptanleitung).
/********************************************************************************************
 *  TESTSENSOR für ESP8266 D1 Mini
 *
 *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard).
 *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert!
 *
 *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch
 *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite).
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
 
#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden!
#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen)
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  // ---- Simulierte Werte (KEINE echten Sensoren!) ----
  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C
  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 %
  static int fakeCo2 = 400;
  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, CO2_ppm – dieselben Feldnamen wie bei den echten Boxen, sodass Sie die EmonCMS-Feeds genauso wie später bei echten Sensoren anlegen können (siehe Sensoren einbinden). Empfohlenes PHPFina-Interval für den Testsensor: 30 Sekunden (passend zu SENDEINTERVALL_MS).


Einfacher Sensor: nur BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck)

Ein echter, aber minimaler Sensor – guter nächster Schritt nach dem simulierten Testsensor, bevor die vollständigen Boxen (Klimamessbox, Raummessbox) zum Einsatz kommen. Nur ein BME280 über I²C, sonst nichts.

Verkabelung (I²C-Modus):

BME280-Pin Verbunden mit Hinweis
VCC 3,3V
GND GND
SCL D1 (GPIO5) Standard-I²C-Clock beim D1 Mini
SDA D2 (GPIO4) Standard-I²C-Data beim D1 Mini
CSB 3,3V Zwingend nötig, um den I²C-Modus zu aktivieren (statt SPI)
SDO GND Setzt die I²C-Adresse auf 0x76 – dieselbe Adresse wie bei der Klimamessbox (Teil 3), bewusst einheitlich gehalten, damit später auch die Raummessbox (aktuell noch BMP180+DHT22) beim geplanten Umstieg auf BME280 ohne Adresskonflikt eingebunden werden kann
/********************************************************************************************
 *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280
 *
 *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen
 *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus.
 *
 *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND
 *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox)
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
 
#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen!
 
// ==== TEST-EINSTELLUNG =======================================================
// 10 Sekunden statt der später üblichen 30-120 Sekunden - bewusst kurz gehalten,
// damit sich Änderungen beim Testen sofort in EmonCMS zeigen. Für Dauerbetrieb
// erhöhen (z. B. auf 300000 wie bei der Klimamessbox), um Server und WLAN nicht
// unnötig zu belasten!
#define SENDEINTERVALL_MS 10000
// ==============================================================================
 
#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77)
#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur
 
Adafruit_BME280 bme;
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Einfacher BME280-Sensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  Wire.begin();  // Standard-Pins: D1=SCL, D2=SDA
  if (!bme.begin(BME280_ADDRESS)) {
    Serial.println("FEHLER: BME280 nicht gefunden! I2C-Verkabelung und Adresse (0x76) prüfen.");
    while (true) delay(100);
  }
  Serial.println("BME280 OK");
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  float hum  = bme.readHumidity();
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;             // Pa → hPa
  float p0   = bme.seaLevelForAltitude(ALTITUDE, pressure);  // auf Meereshöhe umgerechnet
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"P_hPa\":%.1f}", temp, hum, p0);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f°C F=%.1f%% P=%.1fhPa\n", code, temp, hum, p0);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, P_hPa – dieselben Feldnamen wie später bei der Klimamessbox. Empfohlenes PHPFina-Interval: 10 Sekunden (passend zu SENDEINTERVALL_MS im Testbetrieb).


Klimamessbox (Außenbereich)

Sensoren: BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck), MH-Z19B (CO2), SDS011 (Feinstaub PM2.5/PM10), LDR (Helligkeit). Pin-Belegung und Funktionsprinzipien siehe Hauptanleitung, Teil 3.

Angepasst gegenüber der ursprünglichen Fassung: Netzwerk-Konfiguration
auf das Bereich-A/B/C-Schema umgestellt (siehe Erklärung oben auf
dieser Seite), MODE_LIVE-Umschalter entfällt dadurch (die Netzauswahl
übernimmt das jetzt). Messintervall von 120 auf 300 Sekunden (5 Min.)
geändert - schont den SDS011-Lüfter im Dauerbetrieb spürbar, ohne bei
der eher langsam schwankenden Außenluftbelastung relevant an
Aussagekraft zu verlieren.
/********************************************************************************************
 *  Außensensor für ESP8266 D1 Mini – Schulversion mit ausführlichen Erklärungen
 *
 *  Sensoren:
 *   - BME280: Temperatur, Luftfeuchtigkeit, Luftdruck (I²C)
 *   - MH-Z19B: CO2-Gehalt der Luft (UART)
 *   - SDS011: Feinstaub PM2.5 und PM10 (UART)
 *   - LDR: Helligkeit (Analog)
 *
 *  Features: Retry-Logik mit WLAN-Soft-Reset bei Upload-Fehlern, CO2_OFFSET-Kalibrierung,
 *            Abschalten der automatischen Baseline-Korrektur (ABC) für den Außeneinsatz.
 *
 *  IDE: LOLIN(WEMOS) D1 R2 & mini  (bei Boot-Abstürzen ggf. auf "Generic ESP8266 Module"
 *       mit Flash Mode: DIO, Flash Frequency: 40MHz, VTables: Flash umstellen)
 ********************************************************************************************/
 
// ========== BIBLIOTHEKEN ==========
#include <ESP8266WiFi.h>        // WLAN-Funktionen für den ESP8266
#include <ESP8266HTTPClient.h>  // HTTP-Anfragen (um Daten zu senden)
#include <WiFiClient.h>         // TCP-Client für HTTP
#include <Wire.h>               // I²C-Kommunikation (für BME280)
#include <Adafruit_BME280.h>    // BME280 Sensor-Bibliothek
#include <SoftwareSerial.h>     // Zusätzliche serielle Schnittstellen
 
// ========== KONFIGURATION (HIER NUR DIESE ZEILEN ÄNDERN!) ==========
// Jeder Sensor bekommt eine eindeutige Nummer. Diese Nummer bestimmt:
// - Den Namen des Sensors (z.B. "134")
// - Die IP-Adresse im Netzwerk (z.B. 192.168.1.134)
 
#define SENSOR_ID 134            // <<< NUR DIESE ZAHL ÄNDERN (z.B. 101, 102, 133, ...)
 
// CO2-Kalibrierungs-Offset (wie bei der Raumklima-Box): wird auf den Rohwert addiert.
// An echter Frischluft (z.B. früh morgens, wenig Verkehr) prüfen, was der Sensor anzeigt,
// und den Offset so wählen, dass ca. 420-440 ppm (aktueller globaler Referenzwert) erreicht werden.
#define CO2_OFFSET 40
 
// Gewünschtes GESAMT-Intervall zwischen zwei Sendungen an EmonCMS, in Millisekunden.
// <<< NUR DIESEN WERT ÄNDERN, um den Messtakt anzupassen (z.B. 60000 = 1 Min., 120000 = 2 Min.,
//     180000 = 3 Min., 360000 = 6 Min.). Der SDS011-Aufwachvorgang braucht davon fest ca. 10 s,
//     der Rest wird als Wartezeit am Ende jedes Zyklus eingehalten (siehe Schreibpause unten).
//     Hinweis: Das EmonCMS-Feed-Intervall (PHPFina) sollte auf denselben Wert eingestellt werden,
//     sonst entstehen unnötige Interpolationslücken in den Graphen.
#define MESSINTERVALL_MS 300000           // aktuell: alle 5 Minuten im EMONCMS entsprechend anpassen!
 
// ========== DEBUG-MODUS ==========
#define DEBUG true               // <<< Debug-Ausgaben EIN (true) oder AUS (false)
 
// ========== DEBUG-MAKROS ==========
#if DEBUG
  #define DBGPRINT(x)   Serial.print(x)
  #define DBGPRINTLN(x) Serial.println(x)
  #define DBGF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__)
#else
  #define DBGPRINT(x)
  #define DBGPRINTLN(x)
  #define DBGF(fmt, ...)
#endif
 
// ========== NETZWERK-KONFIGURATION ==========
char CLIENT[8];                                 // Speicher für den Sensor-Namen (wird später gefüllt)
IPAddress ip(192, 168, 1, SENSOR_ID);           // Feste IP-Adresse (wird aus SENSOR_ID gebildet)
 
IPAddress SERVER_IP(192, 168, 1, 2);            // EmonCMS-Server (gilt für alle drei Netzwerkprofile)
const int httpPort = 80;                        // HTTP-Port (Standard)
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
// Standard-Gateway (Router) und Subnetzmaske
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);                  // DNS nicht verwendet (0.0.0.0 = deaktiviert)
 
// ========== SENSOR-HARDWARE ==========
 
// === BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck) ===
// Der BME280 wird über I²C angesprochen (Standard-Pins D1=SCL, D2=SDA)
Adafruit_BME280 bme;                            // BME280-Objekt erstellen
#define BME280_ADDRESS 0x76                     // I²C-Adresse des BME280 (manchmal 0x77)
#define ALTITUDE 42.0                           // Höhe über Meeresspiegel in Metern (für Luftdruck-Korrektur)
 
// === MH-Z19B CO2-Sensor - an 5V angeschlossen und mit Spannungsteiler zur Datenübertragung an den D1-mini ===
// RX = D6 (GPIO12) mit Spannungsteiler 5V→3.3V (1kΩ + 2kΩ)
// TX = D5 (GPIO14)
SoftwareSerial mhzSerial(12, 14);               // RX-Pin, TX-Pin
 
// Kommando zum Auslesen des CO2-Wertes (wird zum Sensor gesendet)
byte cmdReadCO2[] = {0xFF, 0x01, 0x86, 0, 0, 0, 0, 0, 0x79};
 
// Kommando zum Abschalten der automatischen Baseline-Korrektur (ABC).
// ABC geht davon aus, dass der niedrigste 24h-Wert ~400 ppm (Frischluft) entspricht.
// Das passt für Innenraum-Sensoren, aber nicht für einen dauerhaft im Freien montierten
// Sensor in der Stadt (Hintergrundwert schwankt/ist oft dauerhaft erhöht) → würde den
// Sensor über die Zeit falsch kalibrieren. Wird einmalig beim Boot gesendet.
byte cmdABCOff[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86};
 
// === SDS011 Feinstaub-Sensor - an 5V angeschlossen aber ohne Spannungsteiler zur Datenübertragung an den D1-mini ===
// RX = D7 (GPIO13) OHNE Spannungsteiler
// TX = D8 (GPIO15)
SoftwareSerial sdsSerial(13, 15);
 
// SDS011 Sleep-Kommandos (um den Sensor ein-/auszuschalten)
byte sds011_sleep[]  = {0xAA, 0xB4, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x05, 0xAB};
byte sds011_wakeup[] = {0xAA, 0xB4, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x06, 0xAB};
 
// === LDR (Lichtsensor) ===
#define LDR_PIN A0
const int LDR_INVERTED_OFFSET = 450;            // Offset für Invertierung (hell→klein, dunkel→groß)
 
// ========== SYSTEM-PARAMETER ==========
const long schreibpauseRoh = (long)MESSINTERVALL_MS - 10000;
const int Schreibpause = (int)(schreibpauseRoh > 2000 ? schreibpauseRoh : 2000);
const uint32_t BOOT_UPLOAD_DEADLINE_MS = 180000; // 3 Minuten: Wenn in dieser Zeit kein Upload klappt → Neustart
const uint32_t WIFI_DOWN_REBOOT_MS = 120000;   // 2 Minuten: Wenn WLAN so lange weg ist → Neustart
const uint8_t WIFI_BOOT_TRIES = 3;              // Anzahl der WLAN-Verbindungsversuche beim Start
 
const float SENSOR_ERROR_VALUE = -999.0f;
const int CO2_ERROR_VALUE = -1;
 
// ========== GLOBALE VARIABLEN ==========
static bool g_firstCo2Init = true;
static bool g_uploadedOnce = false;
static bool g_lastSendFailed = false;
static uint32_t g_bootMs = 0;
static uint32_t g_wifiDownSince = 0;
static uint32_t g_lastUploadMs = 0;
 
// ========== FUNKTIONS-PROTOTYPEN ==========
bool ensureWifi(uint32_t timeoutMs = 15000);
bool wifiBootConnect();
bool netSoftReset(uint32_t connectTimeoutMs = 15000);
bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50);
bool readMHZ19B_Safe(int &ppm, int &tempC);
bool readSDS011(float &pm25, float &pm10);
void setSDS011Sleep(bool sleep);
float readLDR();
void sendData(float feuchtigkeit, float temp, float druck, float co2, float pm25, float pm10, float ldr);
 
// ==========================================
// ========== SETUP (WIRD 1x AUSGEFÜHRT) ====
// ==========================================
void setup() {
  if (DEBUG) {
    Serial.begin(115200);
    delay(200);
  }
 
  DBGPRINTLN("\n========================================");
  DBGPRINTLN("===  Außensensor ESP8266 D1 Mini    ===");
  DBGPRINTLN("========================================\n");
 
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  DBGF("Sensor-Name: %s\n", CLIENT);
  DBGF("IP-Adresse:  %s\n", ip.toString().c_str());
  DBGPRINTLN();
 
  Wire.begin();                 // Standard-Pins: D1=SCL, D2=SDA
 
  DBGPRINT("BME280 wird initialisiert... ");
  if (!bme.begin(BME280_ADDRESS)) {
    DBGPRINTLN("FEHLER!");
    DBGPRINTLN("BME280 nicht gefunden! Prüfe:");
    DBGPRINTLN("  - I²C-Verkabelung (SDA, SCL)");
    DBGPRINTLN("  - I²C-Adresse (0x76 oder 0x77?)");
    DBGPRINTLN("Sensor bleibt hier stehen.");
    while (true) delay(100);
  }
  DBGPRINTLN("OK!");
 
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::FILTER_OFF);
 
  DBGPRINTLN("MH-Z19B wird initialisiert...");
  mhzSerial.begin(9600);
  delay(50);
  mhzSerial.write(cmdABCOff, 9);
  DBGPRINTLN("  Automatische Baseline-Korrektur (ABC) abgeschaltet");
  DBGPRINTLN("  Hinweis: MH-Z19B braucht ~3 Min. zum Aufwärmen nach dem Einschalten");
 
  DBGPRINTLN("SDS011 wird initialisiert...");
  sdsSerial.begin(9600);
  setSDS011Sleep(true);
  DBGPRINTLN("  SDS011 in Sleep-Modus versetzt");
 
  DBGPRINTLN("\nWLAN-Verbindung wird aufgebaut...");
  if (!wifiBootConnect()) {
    DBGPRINTLN("[FEHLER] WLAN nicht erreichbar!");
    DBGPRINTLN("ESP8266 startet in 2 Sekunden neu...");
    delay(2000);
    ESP.restart();
  }
 
  randomSeed(ESP.getChipId());
  int startDelay = random(0, Schreibpause);
  DBGF("\nStartverzögerung: %d ms (zufällig gewählt)\n", startDelay);
  delay(startDelay);
 
  delay(3000);
  g_bootMs = millis();
 
  DBGPRINTLN("\n=== Setup abgeschlossen, Messbetrieb startet ===\n");
}
 
// ==========================================
// ========== LOOP (WIRD DAUERHAFT AUSGEFÜHRT) ====
// ==========================================
void loop() {
  DBGPRINTLN("--- Neuer Messzyklus ---");
 
  DBGPRINT("BME280 wird ausgelesen... ");
  bme.takeForcedMeasurement();
 
  float hum = bme.readHumidity();
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;
  float p0 = bme.seaLevelForAltitude(ALTITUDE, pressure);
 
  if (isnan(hum) || hum < 0 || hum > 100) hum = SENSOR_ERROR_VALUE;
  if (isnan(temp)) temp = SENSOR_ERROR_VALUE;
  if (isnan(p0) || p0 < 0) p0 = SENSOR_ERROR_VALUE;
 
  DBGPRINTLN("OK");
  DBGF("  Temperatur:      %.2f °C\n", temp);
  DBGF("  Luftfeuchtigkeit: %.1f %%\n", hum);
  DBGF("  Luftdruck:       %.1f hPa (Meereshöhe)\n", p0);
 
  DBGPRINT("MH-Z19B (CO2) wird ausgelesen... ");
  int ppm_val = CO2_ERROR_VALUE;
  int tco2 = 0;
 
  if (g_firstCo2Init) {
    uint32_t t0 = millis();
    while (!readMHZ19B_Safe(ppm_val, tco2) && millis() - t0 < 1500) {
      delay(200);
    }
    g_firstCo2Init = false;
    DBGPRINTLN("(Erstmessung)");
  } else {
    readMHZ19B_Safe(ppm_val, tco2);
  }
 
  static int ppm_uart = 0;
  if (ppm_val > 0) {
    ppm_uart = ppm_val + CO2_OFFSET;
    DBGPRINTLN("OK");
    DBGF("  CO2-Gehalt: %d ppm (Rohwert %d + Offset %d)\n", ppm_uart, ppm_val, CO2_OFFSET);
  } else {
    DBGPRINTLN("FEHLER (verwende letzten Wert)");
    DBGF("  CO2-Gehalt: %d ppm (alter Wert)\n", ppm_uart);
  }
 
  DBGPRINT("SDS011 (Feinstaub) wird ausgelesen... ");
  setSDS011Sleep(false);
  delay(10000);
 
  float pm25 = SENSOR_ERROR_VALUE;
  float pm10 = SENSOR_ERROR_VALUE;
 
  if (readSDS011(pm25, pm10)) {
    DBGPRINTLN("OK");
    DBGF("  PM2.5: %.1f µg/m³\n", pm25);
    DBGF("  PM10:  %.1f µg/m³\n", pm10);
  } else {
    DBGPRINTLN("FEHLER");
    pm25 = 0;
    pm10 = 0;
  }
 
  setSDS011Sleep(true);
 
  DBGPRINT("LDR (Helligkeit) wird ausgelesen... ");
  float ldrWert = readLDR();
  DBGPRINTLN("OK");
  DBGF("  Helligkeit: %.0f (invertiert: hell=klein, dunkel=groß)\n", ldrWert);
 
  DBGPRINTLN("\nWLAN-Verbindung wird geprüft...");
  ensureWifi(10000);
 
  DBGPRINTLN("Daten werden gesendet...");
  sendData(hum, temp, p0, (float)ppm_uart, pm25, pm10, ldrWert);
 
  uint32_t now = millis();
 
  if (!g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) {
    DBGPRINTLN("\n[WATCHDOG] Kein erfolgreicher Upload binnen 3 Minuten!");
    DBGPRINTLN("[WATCHDOG] ESP8266 startet neu...");
    delay(500);
    ESP.restart();
  }
 
  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) {
    DBGPRINTLN("\n[WATCHDOG] WLAN länger als 2 Minuten nicht erreichbar!");
    DBGPRINTLN("[WATCHDOG] ESP8266 startet neu...");
    delay(500);
    ESP.restart();
  }
 
  int jitter = random(-300, 300);
  int actualDelay = Schreibpause + jitter;
  DBGF("\nPause: %d ms (mit Jitter)\n", actualDelay);
  DBGPRINTLN("=====================================\n");
 
  delay(actualDelay);
}
 
// ==========================================
// ========== WLAN-FUNKTIONEN ==========
// ==========================================
bool wifiBootConnect() {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);
  WiFi.setOutputPower(20.5);
  WiFi.setSleepMode(WIFI_NONE_SLEEP);
 
  for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {
    DBGF("[BOOT] WLAN-Verbindungsversuch %u/%u\n", attempt, WIFI_BOOT_TRIES);
 
    WiFi.disconnect(true);
    delay(200);
    WiFi.forceSleepBegin();
    delay(200);
    WiFi.forceSleepWake();
    delay(200);
 
    WiFi.config(ip, gateway, subnet, dns);
    WiFi.begin(ssid, password);
 
    if (ensureWifi(20000)) {
      DBGF("[WIFI] Verbunden! IP: %s, RSSI: %d dBm\n",
           WiFi.localIP().toString().c_str(), WiFi.RSSI());
 
      WiFiClient poke;
      poke.connect(gateway, 80);
      poke.stop();
 
      return true;
    }
 
    delay(1000 + random(0, 2000));
  }
 
  return false;
}
 
bool ensureWifi(uint32_t timeoutMs) {
  if (WiFi.status() == WL_CONNECTED) {
    return true;
  }
 
  DBGPRINT("[WIFI] Warte auf Verbindung");
  uint32_t start = millis();
 
  while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
    delay(250);
    DBGPRINT(".");
  }
 
  if (WiFi.status() == WL_CONNECTED) {
    DBGPRINTLN(" OK");
    g_wifiDownSince = 0;
    return true;
  }
 
  DBGPRINTLN(" Timeout!");
 
  if (!g_wifiDownSince) {
    g_wifiDownSince = millis();
  }
 
  return false;
}
 
bool netSoftReset(uint32_t connectTimeoutMs) {
  DBGPRINTLN("[NET] Soft-Reset wird durchgeführt...");
 
  WiFi.disconnect(true);
  delay(200);
  WiFi.forceSleepBegin();
  delay(200);
  WiFi.forceSleepWake();
  delay(200);
 
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  return ensureWifi(connectTimeoutMs);
}
 
bool tcpPreflight(uint16_t port, uint16_t graceMs) {
  WiFiClient poke;
 
  if (!poke.connect(SERVER_IP, port)) {
    return false;
  }
 
  delay(graceMs);
  poke.stop();
 
  return true;
}
 
// ==========================================
// ========== SENSOR-FUNKTIONEN ==========
// ==========================================
bool readMHZ19B_Safe(int &ppm, int &tempC) {
  while (mhzSerial.available()) {
    mhzSerial.read();
  }
 
  mhzSerial.write(cmdReadCO2, 9);
 
  uint8_t resp[9];
  int i = 0;
  uint32_t start = millis();
 
  while (millis() - start < 200) {
    if (mhzSerial.available()) {
      resp[i++] = mhzSerial.read();
      if (i == 9) break;
    }
  }
 
  if (i != 9) {
    return false;
  }
 
  if (resp[0] != 0xFF || resp[1] != 0x86) {
    return false;
  }
 
  uint8_t sum = 0;
  for (int k = 1; k < 8; k++) {
    sum += resp[k];
  }
  sum = 0xFF - sum + 1;
 
  if (sum != resp[8]) {
    return false;
  }
 
  ppm = resp[2] * 256 + resp[3];
  tempC = resp[4] - 40;
 
  return true;
}
 
bool readSDS011(float &pm25, float &pm10) {
  uint8_t buf[10];
  int idx = 0;
  uint32_t start = millis();
 
  while (millis() - start < 2000) {
    if (sdsSerial.available()) {
      uint8_t b = sdsSerial.read();
 
      if (idx == 0 && b != 0xAA) {
        continue;
      }
 
      buf[idx++] = b;
 
      if (idx == 10) {
        if (buf[0] == 0xAA && buf[1] == 0xC0 && buf[9] == 0xAB) {
          pm25 = (buf[2] + 256 * buf[3]) / 10.0f;
          pm10 = (buf[4] + 256 * buf[5]) / 10.0f;
          return true;
        } else {
          idx = 0;
        }
      }
    }
  }
 
  return false;
}
 
void setSDS011Sleep(bool sleep) {
  if (sleep) {
    sdsSerial.write(sds011_sleep, sizeof(sds011_sleep));
  } else {
    sdsSerial.write(sds011_wakeup, sizeof(sds011_wakeup));
  }
  delay(100);
}
 
float readLDR() {
  int raw = analogRead(LDR_PIN);
  raw = LDR_INVERTED_OFFSET - raw;
 
  if (raw < 0) raw = 0;
 
  return (float)raw;
}
 
// ==========================================
// ========== DATEN SENDEN ==========
// ==========================================
void sendData(float feuchtigkeit, float temp, float druck, float co2, float pm25, float pm10, float ldr) {
  if (WiFi.status() != WL_CONNECTED) {
    g_lastSendFailed = true;
    DBGPRINTLN("[HTTP] Abbruch: WLAN nicht verbunden");
    return;
  }
 
  if (DEBUG) {
    DBGF("[WIFI] Signalstärke: %d dBm\n", WiFi.RSSI());
  }
 
  char path[128];
  snprintf(path, sizeof(path),
           "/emoncms/input/post.json?node=%s&apikey=%s",
           CLIENT, apikey);
 
  char body[256];
  snprintf(body, sizeof(body),
    "json={\"F_rel\":%.1f,\"T_C\":%.2f,\"P_hPa\":%.1f,\"CO2_ppm\":%.0f,\"PM25\":%.1f,\"PM10\":%.1f,\"H_bit\":%.0f}",
    feuchtigkeit, temp, druck, co2, pm25, pm10, ldr);
 
  if (g_lastSendFailed) {
    bool ok = tcpPreflight(httpPort, 80);
    DBGF("[NET] Preflight %s\n", ok ? "OK" : "FAIL");
  }
 
  String hostStr = SERVER_IP.toString();
 
  auto doPostOnce = [&](int attempt) -> int {
    if (WiFi.status() != WL_CONNECTED) {
      ensureWifi(8000);
      if (WiFi.status() != WL_CONNECTED) {
        DBGF("[HTTP] WLAN down (Try %d) – skip\n", attempt);
        return -10000;
      }
    }
 
    WiFiClient client;
    client.setNoDelay(true);
    client.setTimeout(15000);
 
    HTTPClient http;
    http.setTimeout(15000);
    http.setReuse(false);
 
    if (!http.begin(client, hostStr, httpPort, path, false)) {
      DBGPRINTLN("[HTTP] begin() fehlgeschlagen");
      return -2;
    }
 
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Connection", "close");
 
    int code = http.POST(body);
    String err = http.errorToString(code);
    String payload = (code > 0) ? http.getString() : String();
    http.end();
 
    DBGF("[HTTP] Versuch %d → code=%d (%s)\n", attempt, code, err.c_str());
 
    if (code == 200) {
      if (DEBUG && payload.length() > 0) {
        DBGPRINT("[HTTP] Antwort: ");
        DBGPRINTLN(payload);
      }
      DBGF("  Gesendet: F=%.1f%%, T=%.2f°C, P=%.1fhPa, CO2=%.0fppm, PM2.5=%.1fµg/m³, PM10=%.1fµg/m³, LDR=%.0f\n",
           feuchtigkeit, temp, druck, co2, pm25, pm10, ldr);
      g_uploadedOnce   = true;
      g_lastSendFailed = false;
      g_lastUploadMs   = millis();
    }
    return code;
  };
 
  int code = -9999;
 
  for (int attempt = 1; attempt <= 3; ++attempt) {
    code = doPostOnce(attempt);
    if (code == 200) return;
    delay(250 * attempt);
  }
 
  DBGPRINTLN("[NET] Soft-Reset #1 …");
  if (netSoftReset(15000)) {
    code = doPostOnce(4);
    if (code == 200) return;
  }
 
  DBGPRINTLN("[NET] Soft-Reset #2 …");
  if (netSoftReset(15000)) {
    code = doPostOnce(5);
    if (code == 200) return;
  }
 
  g_lastSendFailed = true;
  DBGF("[HTTP] FEHLGESCHLAGEN nach allen Versuchen | Node=%s | F=%.1f T=%.2f P0=%.1f CO2=%.0f PM2.5=%.1f PM10=%.1f LDR=%.0f\n",
       CLIENT, feuchtigkeit, temp, druck, co2, pm25, pm10, ldr);
}

Raummessbox (Innenbereich)

Sensoren: DHT22 (Luftfeuchte), BMP180 (Temperatur, Luftdruck), MH-Z19B (CO2), LDR (Helligkeit), NeoPixel-LED-Band (CO2-Ampel, 4 oder 5 LEDs). Pin-Belegung und Funktionsprinzipien siehe Hauptanleitung, Teil 3.

Angepasst gegenüber der ursprünglichen Fassung: Netzwerk-Konfiguration
auf das Bereich-A/B/C-Schema umgestellt (siehe Erklärung oben auf
dieser Seite). Messintervall bei den ursprünglichen 20 Sekunden
belassen (gut geeignet für ein Klassenzimmer, in dem auch kurzfristige
Schwankungen - z. B. CO2-Anstieg beim Lüften - interessant sind).
Für den EmonCMS-Feed entsprechend Interval 20 verwenden.
/********************************************************************************************
 *  Raumklima D1 mini – Schul-/Langzeit-Variante (statische IP, selbstheilend)
 *
 *  Features:
 *   - NUR HIER ÄNDERN: SENSOR_ID, NUM_LEDS, CO2_OFFSET, Netzwerk-Auswahl (Bereich A/B/C)
 *   - Statische IP (kein DHCP), robuster WLAN-Start mit 3 Versuchen + Funk-Reset
 *   - Zufällige Startverzögerung (0–30 s) + Jitter je Zyklus (±0,3 s) → entkoppelt Massenstart
 *   - Auto-Neustart, falls:
 *       • beim Boot 3 min lang kein erfolgreicher Upload
 *       • WLAN im Betrieb > 2 min am Stück weg
 *   - MH-Z19B: „sicheres" Auslesen (9 Bytes, Checksumme), sanfter Erststart
 *   - BMP180: p0 (Meereshöhe) wird gesendet
 *   - LDR: invertiert (hell → klein, dunkel → groß)
 *   - 30 s Intervall für Langzeit-Monitoring (EmonCMS PHPFina 30 s empfohlen)
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include "DHT.h"
#include <Wire.h>
#include <SFE_BMP180.h>
#include <Adafruit_NeoPixel.h>
#include <SoftwareSerial.h>
 
/****************************************  NUR HIER ÄNDERUNGEN VORNEHMEN (z. B. 111 → 112) ******************************/
//
//                                        1) Geräte-Identität
#define SENSOR_ID 111                    // eindeutige Sensor-Nummer
//
//                                        2) Bauart - Länge der LED-Anzeige
#define NUM_LEDS  5                      // 5 = Normal, 4 = Gerät mit 4 LEDs
//
//                                        3) CO2-Kalibrierung
#define CO2_OFFSET 40                    // Kalibrierung an Frischluft wird auf 400 ppm gesetzt.
//
//                                        4) Netzwerk-Auswahl siehe weiter unten (Bereich A/B/C)
//
/**************************************** AB HIER KEINE ÄNDERUNGEN VORNEHMEN *****************************************************/
 
// Aus SENSOR_ID werden automatisch Client-Name ("111") & IP 192.168.1.SENSOR_ID erzeugt:
char      CLIENT[8];                     // wird im setup() mit "111" etc. gefüllt
IPAddress ip(192, 168, 1, SENSOR_ID);    // feste IP aus SENSOR_ID
 
IPAddress SERVER_IP(192, 168, 1, 2);     // EmonCMS-Server (gilt für alle drei Netzwerkprofile)
const char* host     = "192.168.1.2";    // EmonCMS-Server (ohne http://)
const int   httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0,   0,   0,   0);
static bool g_lastSendFailed = false;  // für die WLANverbindungsqualität
 
 
/*** 3) Messintervall (Langzeitbetrieb) *****************************************************/
const int Schreibpause = 20000;          // 20 s; für 10 s im Test nur hier 10000 setzen
 
/*** 4) Sensor-Hardware *********************************************************************/
// DHT22 (Luftfeuchte/Temperatur)
#define DHTPIN   13                      // D7
#define DHTTYPE  DHT22
DHT dht(DHTPIN, DHTTYPE);
 
// BMP180 (Luftdruck/Temperatur)
#define ALTITUDE 42.0                    // Höhe [m] für p0-Korrektur
SFE_BMP180 pressure;
 
// MH-Z19B (CO₂, UART)
SoftwareSerial mhzSerial(12, 14);        // RX = D6, TX = D5
byte cmdReadCO2[] = {0xFF,0x01,0x86,0,0,0,0,0,0x79};
 
// LDR (Helligkeit, analog)
#define LDR_PIN A0
 
// LED-Band (CO₂-Ampel)
#define LED_PIN   2                      // D4
 
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
 
/*** 5) Selbstheilungs-Parameter ************************************************************/
const uint32_t BOOT_UPLOAD_DEADLINE_MS = 180000; // 3 min: bis dahin muss 1 Upload geklappt haben
const uint32_t WIFI_DOWN_REBOOT_MS     = 120000; // 2 min am Stück offline → Neustart
const uint8_t  WIFI_BOOT_TRIES         = 3;      // WLAN-Startversuche mit Funk-Reset
 
/*** 6) Zustände ****************************************************************************/
static bool     g_firstCo2Init    = true; // CO₂-Sensor im ersten Zyklus besonders behandeln
static bool     g_uploadedOnce    = false; // wurde seit Boot mindestens 1x erfolgreich gesendet?
static uint32_t g_bootMs          = 0;     // Boot-Zeitpunkt
static uint32_t g_wifiDownSince   = 0;     // Beginn einer Offline-Phase (0 = online)
 
/*** 7) Prototypen **************************************************************************/
bool ensureWifi(uint32_t timeoutMs = 15000);
bool wifiBootConnect();
void LED_Band(int co2);
float readLDR();
bool readMHZ19B_Safe(int &ppm, int &tempC);
void sendData(float feuchtigkeit, float temp, float druck, float co2, float ldr);
 
/*** 8) SETUP *******************************************************************************/
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("\n=== Boot: Raumklima-Sensor startet ===");
 
  // CLIENT-Name aus SENSOR_ID erzeugen (z. B. 111 → "111")
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("Sensor-ID = %s, IP = %s\n", CLIENT, ip.toString().c_str());
 
  // LED-Band initial
  strip.begin();
  strip.show();
 
  // Sensoren starten
  dht.begin();
  if (!pressure.begin()) {
    Serial.println("FEHLER: BMP180 nicht gefunden! (I2C prüfen: D1=SCL, D2=SDA, GND, 3V3)");
    while (true) delay(10); // „hart stehenbleiben" → Fehler sichtbar
  }
 
  // WLAN-Start: 3 saubere Versuche mit Funk-Reset & Backoff
  if (!wifiBootConnect()) {
    Serial.println("[BOOT] WLAN nicht erreichbar → Neustart");
    delay(800);
    ESP.restart();
  }
 
  // CO₂-Sensor starten (UART)
  mhzSerial.begin(9600);
  Serial.println("MH-Z19B UART bereit");
 
  // Zufällige Startverzögerung (0..Schreibpause) → entkoppelt Massenstart nach Stromausfall
  randomSeed(ESP.getChipId());
  int startDelay = random(0, Schreibpause);
  Serial.printf("Initiale Startpause: %d ms\n", startDelay);
  delay(startDelay);
 
  // kurze Sensor-Aufwärmzeit
  delay(3000);
 
  // Boot-Zeit merken
  g_bootMs = millis();
}
 
/*** 9) LOOP ********************************************************************************/
void loop() {
  /***** DHT22: Luftfeuchte (°C vom DHT kannst du bei Bedarf zusätzlich nutzen) *****/
  float hum = dht.readHumidity();
  if (isnan(hum)) hum = -1.0; // -1 = „ungültig" → in EmonCMS gut erkennbar
 
  /***** BMP180: Temperatur (T), Stationsdruck (P), p0 (auf Meereshöhe) *****/
  char status;
  double T = NAN, P = NAN, p0 = NAN;
  status = pressure.startTemperature();
  if (status) {
    delay(status);
    if (pressure.getTemperature(T)) {
      status = pressure.startPressure(3);       // Oversampling = 3 (genauer)
      if (status) {
        delay(status);
        if (pressure.getPressure(P, T)) {
          p0 = pressure.sealevel(P, ALTITUDE);  // Korrektur auf Meereshöhe
        }
      }
    }
  }
 
  /***** MH-Z19B: sanfter Erststart (max 1,5 s warten), dann normal *****/
  int ppm_val = -1, tco2 = 0;
  if (g_firstCo2Init) {
    uint32_t t0 = millis();
    while (!readMHZ19B_Safe(ppm_val, tco2) && millis() - t0 < 1500) {
      delay(200); // Sensor bekommt Zeit für erstes vollständiges Frame
    }
    g_firstCo2Init = false;
  } else {
    readMHZ19B_Safe(ppm_val, tco2);
  }
  static int ppm_uart = 0;                           // letzter gültiger CO₂-Wert (ppm)
  if (ppm_val > 0) ppm_uart = ppm_val + CO2_OFFSET;  // Offset direkt beim Schreiben eines neuen Wertes
 
/***** LDR: umgekehrt (hell → klein, dunkel → groß) *****/
  float ldrWert = readLDR();
 
  /***** LED-Ampel & WLAN-Status *****/
  LED_Band(ppm_uart);
  ensureWifi(10000);                 // ggf. Reconnect; merkt Offline-Zeitraum
 
  /***** NaN-Schutz: nie „nan" in JSON senden *****/
  if (isnan(T))  T  = -99.9;
  if (isnan(p0)) p0 = -1.0;
  if (ppm_uart < 0) ppm_uart = 0;
 
  /***** Upload zu EmonCMS *****/
  sendData(hum, (float)T, (float)p0, (float)ppm_uart, (float)ldrWert);
 
  /***** Selbstheilung / Watchdogs *****/
  uint32_t now = millis();
 
  // a) Beim Boot: wenn 3 min lang kein Upload erfolgreich → Neustart
  if (!g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) {
    Serial.println("[WD] Kein erfolgreicher Upload binnen 3 min → Reboot");
    delay(500);
    ESP.restart();
  }
 
  // b) Laufbetrieb: WLAN > 2 min am Stück weg → Neustart
  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) {
    Serial.println("[WD] WLAN > 120 s weg → Reboot");
    delay(500);
    ESP.restart();
  }
 
  // Informationen zur Signalstärke - zur Ausrichtung der Antenne...
  static uint32_t cnt = 0;
  if ((++cnt % 6) == 0) {  // alle ~3 Minuten bei 30s-Intervall
    Serial.printf("[WIFI] RSSI: %d dBm\n", WiFi.RSSI());
  }
 
  /***** Intervall + kleiner Jitter (±0,3 s) - zur WLAN und EMONCMS Lastverteilung *****/
  int jitter = random(-300, 300);
  delay(Schreibpause + jitter);
}
 
/*** ENDE von LOOP *************************************************************************/
 
/*** 10) WLAN-Helfer ***********************************************************************/
 
// Mehrstufiger WLAN-Boot: Funk hart zurücksetzen, 3 Versuche mit Backoff
bool wifiBootConnect() {
  WiFi.persistent(false);            // keine alten Credentials aus Flash verwenden
  WiFi.mode(WIFI_STA);
  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);
  WiFi.setOutputPower(20.5);         // maximale Sendeleistung
  WiFi.setSleepMode(WIFI_NONE_SLEEP);// kein Modem-Sleep während Boot
 
  for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {
    Serial.printf("[BOOT] WLAN-Versuch %u/%u\n", attempt, WIFI_BOOT_TRIES);
 
    // Funk & Assoc/ARP-Zustand hart zurücksetzen
    WiFi.disconnect(true);
    delay(200);
    WiFi.forceSleepBegin(); delay(200);
    WiFi.forceSleepWake();  delay(200);
 
    // Statische IP setzen & verbinden
    WiFi.config(ip, gateway, subnet, dns);
    WiFi.begin(ssid, password);
 
    if (ensureWifi(20000)) {
      Serial.printf("[WIFI] OK: %s  RSSI:%d dBm\n",
                    WiFi.localIP().toString().c_str(), WiFi.RSSI());
      // Optional: Gateway kurz „anticken", damit ARP im Router sitzt
      WiFiClient poke; poke.connect(gateway, 80); poke.stop();
      return true;
    }
 
    // Backoff (1.0–3.0 s) vor nächstem Versuch – verhindert Synchronsturm
    int backoff = 1000 + random(0, 2000);
    Serial.printf("[BOOT] fehlgeschlagen → Backoff %d ms\n", backoff);
    delay(backoff);
  }
  return false;
}
 
// Reconnect im Betrieb; merkt Offline-Startzeit für Watchdog
bool ensureWifi(uint32_t timeoutMs) {
  if (WiFi.status() == WL_CONNECTED) return true;
  Serial.print("[WIFI] Verbinde");
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
    delay(250);
    Serial.print(".");
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\n[WIFI] OK: %s\n", WiFi.localIP().toString().c_str());
    g_wifiDownSince = 0;
    return true;
  }
  Serial.println("\n[WIFI] Timeout");
  if (!g_wifiDownSince) g_wifiDownSince = millis(); // Offline-Phase starten
  return false;
}
 
/*** 11) Sensor-/Upload-Helfer **************************************************************/
 
// MH-Z19B „sicher" auslesen (9 Bytes, Header prüfen, Checksumme prüfen, 200 ms Timeout)
bool readMHZ19B_Safe(int &ppm, int &tempC) {
  // alten UART-Puffer leeren (verhindert Mischpakete)
  while (mhzSerial.available()) mhzSerial.read();
 
  // Anfrage senden
  mhzSerial.write(cmdReadCO2, 9);
 
  // Antwort (max. 200 ms warten)
  uint8_t resp[9]; int i = 0; uint32_t start = millis();
  while (millis() - start < 200) {
    if (mhzSerial.available()) {
      resp[i++] = mhzSerial.read();
      if (i == 9) break;  // vollständiges Paket
    }
    delay(1);
  }
 
  // Header + Länge prüfen
  if (i != 9 || resp[0] != 0xFF || resp[1] != 0x86) return false;
 
  // Checksumme prüfen
  uint8_t sum = 0; for (int k = 1; k < 8; k++) sum += resp[k];
  sum = 0xFF - sum + 1;
  if (sum != resp[8]) return false;
 
  // Werte extrahieren
  ppm   = resp[2] * 256 + resp[3];
  tempC = resp[4] - 40;
  return true;
}
 
// LDR umgekehrt: hell → großer Wert, dunkel → kleiner Wert (0..1023)
float readLDR() {
  int raw = analogRead(LDR_PIN);
  raw = 450 - raw;                                    // Umkehrung - heller wird es in der BOX nicht
  return (float)raw;
}
 
// Sanfte Netzwerk-Kur: Radio kurz aus/an + frische Assoziation mit statischer IP
bool netSoftReset(uint32_t connectTimeoutMs = 15000) {
  Serial.println("[NET] Soft-Reset: disconnect + radio cycle");
  WiFi.disconnect(true);            // Assoc/ARP löschen
  delay(200);
  WiFi.forceSleepBegin();           // Radio aus
  delay(200);
  WiFi.forceSleepWake();            // Radio an
  delay(200);
  WiFi.config(ip, gateway, subnet, dns);   // statische IP erneut setzen
  WiFi.begin(ssid, password);
  return ensureWifi(connectTimeoutMs);
}
 
// Kurzer TCP-Preflight: baut eine nackte Verbindung zum Server auf und schließt sie wieder.
// Hilft ARP/NAT/State "aufzuwecken".
bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50) {
  WiFiClient poke;
  if (!poke.connect(SERVER_IP, port)) return false;
  delay(graceMs);
  poke.stop();
  return true;
}
 
/****************************************************** SENDEN der DATEN **********************************************************************/
void sendData(float feuchtigkeit, float temp, float druck, float co2, float ldr) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[HTTP] offline – Upload übersprungen");
    g_lastSendFailed = true;
    return;
  }
 
  // Pfad/Query, weil wir die 5-Parameter-Variante mit Host-String nutzen
  String path  = "/emoncms/input/post.json";
  String query = "?node=" + String(CLIENT) + "&apikey=" + String(apikey);
 
  String body = "json={\"F_rel\":" + String(feuchtigkeit,1) +
                ",\"T_C\":"        + String(temp,2) +
                ",\"P_hPa\":"      + String(druck,1) +
                ",\"CO2_ppm\":"    + String(co2,0) +
                ",\"H_bit\":"      + String(ldr,0) + "}";
 
  // Preflight nach Fehlschlag
  if (g_lastSendFailed) {
    bool ok = tcpPreflight(httpPort, 80);
    Serial.printf("[NET] Preflight %s\n", ok ? "OK" : "FAIL");
  }
 
  // Host-String aus IPAddress erzeugen (für http.begin(...))
  String hostStr = SERVER_IP.toString();
 
  auto doPostOnce = [&](int attempt)->int {
    if (WiFi.status() != WL_CONNECTED) {
      ensureWifi(8000);
      if (WiFi.status() != WL_CONNECTED) {
        Serial.printf("[HTTP] WLAN down (Try %d) – skip\n", attempt);
        return -10000;
      }
    }
 
    WiFiClient client;
    client.setNoDelay(true);
    client.setTimeout(15000);
 
    HTTPClient http;
    http.setTimeout(15000);
    http.setReuse(false);
 
    if (!http.begin(client, hostStr, httpPort, path + query, false)) {
      Serial.println("[HTTP] begin() failed");
      return -2;
    }
 
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Connection", "close");
 
    int code = http.POST(body);
    String err = http.errorToString(code);
    String payload = (code > 0) ? http.getString() : String();
    http.end();
 
    Serial.printf("[HTTP] Try %d → code=%d (%s)\n", attempt, code, err.c_str());
    if (code == 200) {
      if (payload.length()) { Serial.print("[HTTP] resp: "); Serial.println(payload); }
      Serial.printf("Sent: F=%.1f T=%.2f P0=%.1f CO2=%.0f LDR=%.0f\n",
                    feuchtigkeit, temp, druck, co2, ldr);
      g_uploadedOnce   = true;
      g_lastSendFailed = false;  // Erfolg → Flag löschen
    }
    return code;
  };
 
  // 3 normale Versuche
  int code = -9999;
  for (int attempt = 1; attempt <= 3; ++attempt) {
    code = doPostOnce(attempt);
    if (code == 200) return;
    delay(250 * attempt);
  }
 
  // Soft-Reset + 4. Versuch
  if (netSoftReset(15000)) {
    code = doPostOnce(4);
    if (code == 200) return;
  }
 
  // Zweiter Soft-Reset + 5. Versuch
  Serial.println("[NET] Soft-Reset #2 …");
  if (netSoftReset(15000)) {
    code = doPostOnce(5);
    if (code == 200) return;
  }
 
  g_lastSendFailed = true; // alles fehlgeschlagen
  Serial.printf("[HTTP] FAILED after retries | Node=%s | F=%.1f T=%.2f P0=%.1f CO2=%.0f LDR=%.0f\n",
                CLIENT, feuchtigkeit, temp, druck, co2, ldr);
}
 
/********************************************************** ALLE DATEN GESENDET ***********************************************************/
 
/*** 12) CO₂-LED-Ampel (Balkenanzeige links → rechts) *************************************/
void LED_Band(int co2) {
 
  const uint32_t BLAU  = strip.Color(0,   0,   255);
  const uint32_t GRUEN = strip.Color(0,   255, 0  );
  const uint32_t GELB  = strip.Color(255, 255, 0  );
  const uint32_t ROT   = strip.Color(255, 0,   0  );
  const uint32_t WEISS = strip.Color(255, 255, 255);
  const uint32_t AUS   = strip.Color(0,   0,   0  );
 
  // ── NUR HIER ÄNDERN: 5 = normal, 4 = Gerät mit 4 LEDs ───────────────────
  const int LEDS = NUM_LEDS;   // nimmt den Wert aus #define NUM_LEDS oben
  // ─────────────────────────────────────────────────────────────────────────
 
  // Alarm ab 2700: Rot-Weiß blitzen, dann dauerhaft Rot
  if (co2 >= 2700) {
    strip.setBrightness(255);
    for (int i = 0; i < 5; i++) {
      strip.fill(ROT);   strip.show(); delay(200);
      strip.fill(WEISS); strip.show(); delay(200);
      strip.clear();     strip.show(); delay(200);
    }
    // Nach dem Blitzen: alle vorhandenen LEDs dauerhaft Rot
    strip.setBrightness(120);
    for (int i = 0; i < LEDS; i++) {
      strip.setPixelColor(i, ROT);
    }
    // Übrige LEDs aus (bei 4 LEDs: LED4 dunkel)
    for (int i = LEDS; i < 5; i++) {
      strip.setPixelColor(i, AUS);
    }
    strip.show();
    return;  // danach nicht erneut show()
  }
 
  // Vollständiges 5-LED-Muster (LED0=links ... LED4=rechts)
  uint32_t pat[5];
  uint8_t  brightness;
 
  //                        LED0   LED1   LED2   LED3   LED4   Helligkeit
  if      (co2 <  500) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=BLAU;  pat[3]=BLAU;  pat[4]=AUS;   brightness=2;   }
  else if (co2 <  650) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=BLAU;  pat[3]=BLAU;  pat[4]=GRUEN; brightness=2;   }
  else if (co2 <  800) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=BLAU;  pat[3]=GRUEN; pat[4]=GRUEN; brightness=2;   }
  else if (co2 <  950) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=GRUEN; pat[3]=GRUEN; pat[4]=GRUEN; brightness=3;   }
  else if (co2 < 1200) { pat[0]=BLAU;  pat[1]=GRUEN; pat[2]=GRUEN; pat[3]=GRUEN; pat[4]=GRUEN; brightness=3;   }
  else if (co2 < 1350) { pat[0]=GRUEN; pat[1]=GRUEN; pat[2]=GRUEN; pat[3]=GRUEN; pat[4]=GELB;  brightness=3;   }
  else if (co2 < 1500) { pat[0]=GRUEN; pat[1]=GRUEN; pat[2]=GRUEN; pat[3]=GELB;  pat[4]=GELB;  brightness=4;   }
  else if (co2 < 1650) { pat[0]=GRUEN; pat[1]=GRUEN; pat[2]=GELB;  pat[3]=GELB;  pat[4]=GELB;  brightness=4;   }
  else if (co2 < 1800) { pat[0]=GRUEN; pat[1]=GELB;  pat[2]=GELB;  pat[3]=GELB;  pat[4]=GELB;  brightness=4;   }
  else if (co2 < 1950) { pat[0]=GELB;  pat[1]=GELB;  pat[2]=GELB;  pat[3]=GELB;  pat[4]=ROT;   brightness=60;  }
  else if (co2 < 2100) { pat[0]=GELB;  pat[1]=GELB;  pat[2]=GELB;  pat[3]=ROT;   pat[4]=ROT;   brightness=80;  }
  else if (co2 < 2250) { pat[0]=GELB;  pat[1]=GELB;  pat[2]=ROT;   pat[3]=ROT;   pat[4]=ROT;   brightness=100; }
  else                 { pat[0]=GELB;  pat[1]=ROT;   pat[2]=ROT;   pat[3]=ROT;   pat[4]=ROT;   brightness=120; }
 
  // Nur so viele LEDs ansteuern wie vorhanden (4 oder 5)
  strip.setBrightness(brightness);
  for (int i = 0; i < LEDS; i++) {
    strip.setPixelColor(i, pat[i]);
  }
  // Übrige LEDs ausschalten (bei 4 LEDs: LED4 bleibt dunkel)
  for (int i = LEDS; i < 5; i++) {
    strip.setPixelColor(i, AUS);
  }
  strip.show();
}

—————————————————-alt - ohne RASPI-time ———————-

Programmcode der Sensor-Boxen

Diese Seite enthält den vollständigen Arduino-Code aller Sensor-Boxen, ausgelagert aus der Hauptanleitung und der EmonCMS-Einbindungsseite. Pin-Belegung, Funktionsprinzipien und Stolpersteine stehen weiterhin in der Hauptanleitung (Teil 3) – hier geht es nur um den Code selbst.

Netzwerk-Auswahlschema (Bereich A/B/C)

Alle Sketches auf dieser Seite nutzen dasselbe Schema, um zwischen drei Netzwerk-/Betriebsprofilen umzuschalten, ohne jedes Mal Zeilen aus- und einzukommentieren:

  • Bereich A – SAS-ARDUINO-mobil: persönliches Testnetz (z. B. Hotspot zuhause), zum Entwickeln und Testen von Sketches, bevor sie ans Schulnetz gehen
  • Bereich B – SAS-ARDUINO, Schul-Testbetrieb: isoliertes Schulnetz aus Teil 1 der Hauptanleitung, sendet mit dem TEST-API-Key (landet nicht in den „echten“ Live-Feeds)
  • Bereich C – SAS-ARDUINO, Schul-Livebetrieb: dasselbe Schulnetz, sendet mit dem LIVE-API-Key (echter Dauerbetrieb)

Im Kopf jedes Sketches wird genau eine der drei Zeilen auf true gesetzt:

#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif

Darunter folgen drei #if-Blöcke mit den jeweiligen Zugangsdaten. Der Compiler übernimmt automatisch nur den aktiven Block – die beiden anderen werden komplett ignoriert (kein Speicherverbrauch, keine doppelten Variablendefinitionen).

Wichtig bei Weitergabe an andere Schulen: Bereich A (SAS-ARDUINO-mobil)
enthält persönliche/private Zugangsdaten und sollte vor der
Veröffentlichung KOMPLETT gelöscht werden (den ganzen #if...#endif-
Block). Bereich B und C beziehen sich auf das isolierte, nach der
Hauptanleitung bei jeder Schule identisch aufgebaute Schulnetz
("SAS-ARDUINO") und können unverändert als Vorlage dienen - dort
müssen andere Schulen nur ihr eigenes WLAN-Passwort und ihre eigenen
API-Keys eintragen.

Testsensor (ohne echte Sensoren)

Dient ausschließlich dazu, die EmonCMS-Einrichtung (Inputs, Feeds, Dashboard) zu testen, bevor eine echte Sensor-Box angeschlossen ist. Es werden keine Sensoren ausgelesen – die Werte für Temperatur, Luftfeuchtigkeit und CO2 werden im Code simuliert (Temperatur/Feuchte als sanft schwankende Kurve, CO2 als leicht schwankender Zufallswert um 400 ppm).

Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT
für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle
in der Hauptanleitung).
/********************************************************************************************
 *  TESTSENSOR für ESP8266 D1 Mini
 *
 *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard).
 *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert!
 *
 *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch
 *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite).
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
 
#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden!
#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen)
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  // ---- Simulierte Werte (KEINE echten Sensoren!) ----
  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C
  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 %
  static int fakeCo2 = 400;
  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, CO2_ppm – dieselben Feldnamen wie bei den echten Boxen, sodass Sie die EmonCMS-Feeds genauso wie später bei echten Sensoren anlegen können (siehe Sensoren einbinden). Empfohlenes PHPFina-Interval für den Testsensor: 30 Sekunden (passend zu SENDEINTERVALL_MS).


Einfacher Sensor: nur BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck)

Ein echter, aber minimaler Sensor – guter nächster Schritt nach dem simulierten Testsensor, bevor die vollständigen Boxen (Klimamessbox, Raummessbox) zum Einsatz kommen. Nur ein BME280 über I²C, sonst nichts.

Verkabelung (I²C-Modus):

BME280-Pin Verbunden mit Hinweis
VCC 3,3V
GND GND
SCL D1 (GPIO5) Standard-I²C-Clock beim D1 Mini
SDA D2 (GPIO4) Standard-I²C-Data beim D1 Mini
CSB 3,3V Zwingend nötig, um den I²C-Modus zu aktivieren (statt SPI)
SDO GND Setzt die I²C-Adresse auf 0x76 – dieselbe Adresse wie bei der Klimamessbox (Teil 3), bewusst einheitlich gehalten, damit später auch die Raummessbox (aktuell noch BMP180+DHT22) beim geplanten Umstieg auf BME280 ohne Adresskonflikt eingebunden werden kann
/********************************************************************************************
 *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280
 *
 *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen
 *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus.
 *
 *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND
 *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox)
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
 
#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen!
 
// ==== TEST-EINSTELLUNG =======================================================
// 10 Sekunden statt der später üblichen 30-120 Sekunden - bewusst kurz gehalten,
// damit sich Änderungen beim Testen sofort in EmonCMS zeigen. Für Dauerbetrieb
// erhöhen (z. B. auf 300000 wie bei der Klimamessbox), um Server und WLAN nicht
// unnötig zu belasten!
#define SENDEINTERVALL_MS 10000
// ==============================================================================
 
#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77)
#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur
 
Adafruit_BME280 bme;
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Einfacher BME280-Sensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  Wire.begin();  // Standard-Pins: D1=SCL, D2=SDA
  if (!bme.begin(BME280_ADDRESS)) {
    Serial.println("FEHLER: BME280 nicht gefunden! I2C-Verkabelung und Adresse (0x76) prüfen.");
    while (true) delay(100);
  }
  Serial.println("BME280 OK");
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  float hum  = bme.readHumidity();
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;             // Pa → hPa
  float p0   = bme.seaLevelForAltitude(ALTITUDE, pressure);  // auf Meereshöhe umgerechnet
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"P_hPa\":%.1f}", temp, hum, p0);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f°C F=%.1f%% P=%.1fhPa\n", code, temp, hum, p0);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, P_hPa – dieselben Feldnamen wie später bei der Klimamessbox. Empfohlenes PHPFina-Interval: 10 Sekunden (passend zu SENDEINTERVALL_MS im Testbetrieb).


Klimamessbox (Außenbereich)

Sensoren: BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck), MH-Z19B (CO2), SDS011 (Feinstaub PM2.5/PM10), LDR (Helligkeit). Pin-Belegung und Funktionsprinzipien siehe Hauptanleitung, Teil 3.

Angepasst gegenüber der ursprünglichen Fassung: Netzwerk-Konfiguration
auf das Bereich-A/B/C-Schema umgestellt (siehe Erklärung oben auf
dieser Seite), MODE_LIVE-Umschalter entfällt dadurch (die Netzauswahl
übernimmt das jetzt). Messintervall von 120 auf 300 Sekunden (5 Min.)
geändert - schont den SDS011-Lüfter im Dauerbetrieb spürbar, ohne bei
der eher langsam schwankenden Außenluftbelastung relevant an
Aussagekraft zu verlieren.
/********************************************************************************************
 *  Außensensor für ESP8266 D1 Mini – Schulversion mit ausführlichen Erklärungen
 *
 *  Sensoren:
 *   - BME280: Temperatur, Luftfeuchtigkeit, Luftdruck (I²C)
 *   - MH-Z19B: CO2-Gehalt der Luft (UART)
 *   - SDS011: Feinstaub PM2.5 und PM10 (UART)
 *   - LDR: Helligkeit (Analog)
 *
 *  Features: Retry-Logik mit WLAN-Soft-Reset bei Upload-Fehlern, CO2_OFFSET-Kalibrierung,
 *            Abschalten der automatischen Baseline-Korrektur (ABC) für den Außeneinsatz.
 *
 *  IDE: LOLIN(WEMOS) D1 R2 & mini  (bei Boot-Abstürzen ggf. auf "Generic ESP8266 Module"
 *       mit Flash Mode: DIO, Flash Frequency: 40MHz, VTables: Flash umstellen)
 ********************************************************************************************/
 
// ========== BIBLIOTHEKEN ==========
#include <ESP8266WiFi.h>        // WLAN-Funktionen für den ESP8266
#include <ESP8266HTTPClient.h>  // HTTP-Anfragen (um Daten zu senden)
#include <WiFiClient.h>         // TCP-Client für HTTP
#include <Wire.h>               // I²C-Kommunikation (für BME280)
#include <Adafruit_BME280.h>    // BME280 Sensor-Bibliothek
#include <SoftwareSerial.h>     // Zusätzliche serielle Schnittstellen
 
// ========== KONFIGURATION (HIER NUR DIESE ZEILEN ÄNDERN!) ==========
// Jeder Sensor bekommt eine eindeutige Nummer. Diese Nummer bestimmt:
// - Den Namen des Sensors (z.B. "134")
// - Die IP-Adresse im Netzwerk (z.B. 192.168.1.134)
 
#define SENSOR_ID 134            // <<< NUR DIESE ZAHL ÄNDERN (z.B. 101, 102, 133, ...)
 
// CO2-Kalibrierungs-Offset (wie bei der Raumklima-Box): wird auf den Rohwert addiert.
// An echter Frischluft (z.B. früh morgens, wenig Verkehr) prüfen, was der Sensor anzeigt,
// und den Offset so wählen, dass ca. 420-440 ppm (aktueller globaler Referenzwert) erreicht werden.
#define CO2_OFFSET 40
 
// Gewünschtes GESAMT-Intervall zwischen zwei Sendungen an EmonCMS, in Millisekunden.
// <<< NUR DIESEN WERT ÄNDERN, um den Messtakt anzupassen (z.B. 60000 = 1 Min., 120000 = 2 Min.,
//     180000 = 3 Min., 360000 = 6 Min.). Der SDS011-Aufwachvorgang braucht davon fest ca. 10 s,
//     der Rest wird als Wartezeit am Ende jedes Zyklus eingehalten (siehe Schreibpause unten).
//     Hinweis: Das EmonCMS-Feed-Intervall (PHPFina) sollte auf denselben Wert eingestellt werden,
//     sonst entstehen unnötige Interpolationslücken in den Graphen.
#define MESSINTERVALL_MS 300000           // aktuell: alle 5 Minuten im EMONCMS entsprechend anpassen!
 
// ========== DEBUG-MODUS ==========
#define DEBUG true               // <<< Debug-Ausgaben EIN (true) oder AUS (false)
 
// ========== DEBUG-MAKROS ==========
#if DEBUG
  #define DBGPRINT(x)   Serial.print(x)
  #define DBGPRINTLN(x) Serial.println(x)
  #define DBGF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__)
#else
  #define DBGPRINT(x)
  #define DBGPRINTLN(x)
  #define DBGF(fmt, ...)
#endif
 
// ========== NETZWERK-KONFIGURATION ==========
char CLIENT[8];                                 // Speicher für den Sensor-Namen (wird später gefüllt)
IPAddress ip(192, 168, 1, SENSOR_ID);           // Feste IP-Adresse (wird aus SENSOR_ID gebildet)
 
IPAddress SERVER_IP(192, 168, 1, 2);            // EmonCMS-Server (gilt für alle drei Netzwerkprofile)
const int httpPort = 80;                        // HTTP-Port (Standard)
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
// Standard-Gateway (Router) und Subnetzmaske
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);                  // DNS nicht verwendet (0.0.0.0 = deaktiviert)
 
// ========== SENSOR-HARDWARE ==========
 
// === BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck) ===
// Der BME280 wird über I²C angesprochen (Standard-Pins D1=SCL, D2=SDA)
Adafruit_BME280 bme;                            // BME280-Objekt erstellen
#define BME280_ADDRESS 0x76                     // I²C-Adresse des BME280 (manchmal 0x77)
#define ALTITUDE 42.0                           // Höhe über Meeresspiegel in Metern (für Luftdruck-Korrektur)
 
// === MH-Z19B CO2-Sensor - an 5V angeschlossen und mit Spannungsteiler zur Datenübertragung an den D1-mini ===
// RX = D6 (GPIO12) mit Spannungsteiler 5V→3.3V (1kΩ + 2kΩ)
// TX = D5 (GPIO14)
SoftwareSerial mhzSerial(12, 14);               // RX-Pin, TX-Pin
 
// Kommando zum Auslesen des CO2-Wertes (wird zum Sensor gesendet)
byte cmdReadCO2[] = {0xFF, 0x01, 0x86, 0, 0, 0, 0, 0, 0x79};
 
// Kommando zum Abschalten der automatischen Baseline-Korrektur (ABC).
// ABC geht davon aus, dass der niedrigste 24h-Wert ~400 ppm (Frischluft) entspricht.
// Das passt für Innenraum-Sensoren, aber nicht für einen dauerhaft im Freien montierten
// Sensor in der Stadt (Hintergrundwert schwankt/ist oft dauerhaft erhöht) → würde den
// Sensor über die Zeit falsch kalibrieren. Wird einmalig beim Boot gesendet.
byte cmdABCOff[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86};
 
// === SDS011 Feinstaub-Sensor - an 5V angeschlossen aber ohne Spannungsteiler zur Datenübertragung an den D1-mini ===
// RX = D7 (GPIO13) OHNE Spannungsteiler
// TX = D8 (GPIO15)
SoftwareSerial sdsSerial(13, 15);
 
// SDS011 Sleep-Kommandos (um den Sensor ein-/auszuschalten)
byte sds011_sleep[]  = {0xAA, 0xB4, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x05, 0xAB};
byte sds011_wakeup[] = {0xAA, 0xB4, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x06, 0xAB};
 
// === LDR (Lichtsensor) ===
#define LDR_PIN A0
const int LDR_INVERTED_OFFSET = 450;            // Offset für Invertierung (hell→klein, dunkel→groß)
 
// ========== SYSTEM-PARAMETER ==========
const long schreibpauseRoh = (long)MESSINTERVALL_MS - 10000;
const int Schreibpause = (int)(schreibpauseRoh > 2000 ? schreibpauseRoh : 2000);
const uint32_t BOOT_UPLOAD_DEADLINE_MS = 180000; // 3 Minuten: Wenn in dieser Zeit kein Upload klappt → Neustart
const uint32_t WIFI_DOWN_REBOOT_MS = 120000;   // 2 Minuten: Wenn WLAN so lange weg ist → Neustart
const uint8_t WIFI_BOOT_TRIES = 3;              // Anzahl der WLAN-Verbindungsversuche beim Start
 
const float SENSOR_ERROR_VALUE = -999.0f;
const int CO2_ERROR_VALUE = -1;
 
// ========== GLOBALE VARIABLEN ==========
static bool g_firstCo2Init = true;
static bool g_uploadedOnce = false;
static bool g_lastSendFailed = false;
static uint32_t g_bootMs = 0;
static uint32_t g_wifiDownSince = 0;
static uint32_t g_lastUploadMs = 0;
 
// ========== FUNKTIONS-PROTOTYPEN ==========
bool ensureWifi(uint32_t timeoutMs = 15000);
bool wifiBootConnect();
bool netSoftReset(uint32_t connectTimeoutMs = 15000);
bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50);
bool readMHZ19B_Safe(int &ppm, int &tempC);
bool readSDS011(float &pm25, float &pm10);
void setSDS011Sleep(bool sleep);
float readLDR();
void sendData(float feuchtigkeit, float temp, float druck, float co2, float pm25, float pm10, float ldr);
 
// ==========================================
// ========== SETUP (WIRD 1x AUSGEFÜHRT) ====
// ==========================================
void setup() {
  if (DEBUG) {
    Serial.begin(115200);
    delay(200);
  }
 
  DBGPRINTLN("\n========================================");
  DBGPRINTLN("===  Außensensor ESP8266 D1 Mini    ===");
  DBGPRINTLN("========================================\n");
 
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  DBGF("Sensor-Name: %s\n", CLIENT);
  DBGF("IP-Adresse:  %s\n", ip.toString().c_str());
  DBGPRINTLN();
 
  Wire.begin();                 // Standard-Pins: D1=SCL, D2=SDA
 
  DBGPRINT("BME280 wird initialisiert... ");
  if (!bme.begin(BME280_ADDRESS)) {
    DBGPRINTLN("FEHLER!");
    DBGPRINTLN("BME280 nicht gefunden! Prüfe:");
    DBGPRINTLN("  - I²C-Verkabelung (SDA, SCL)");
    DBGPRINTLN("  - I²C-Adresse (0x76 oder 0x77?)");
    DBGPRINTLN("Sensor bleibt hier stehen.");
    while (true) delay(100);
  }
  DBGPRINTLN("OK!");
 
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::SAMPLING_X1,
                  Adafruit_BME280::FILTER_OFF);
 
  DBGPRINTLN("MH-Z19B wird initialisiert...");
  mhzSerial.begin(9600);
  delay(50);
  mhzSerial.write(cmdABCOff, 9);
  DBGPRINTLN("  Automatische Baseline-Korrektur (ABC) abgeschaltet");
  DBGPRINTLN("  Hinweis: MH-Z19B braucht ~3 Min. zum Aufwärmen nach dem Einschalten");
 
  DBGPRINTLN("SDS011 wird initialisiert...");
  sdsSerial.begin(9600);
  setSDS011Sleep(true);
  DBGPRINTLN("  SDS011 in Sleep-Modus versetzt");
 
  DBGPRINTLN("\nWLAN-Verbindung wird aufgebaut...");
  if (!wifiBootConnect()) {
    DBGPRINTLN("[FEHLER] WLAN nicht erreichbar!");
    DBGPRINTLN("ESP8266 startet in 2 Sekunden neu...");
    delay(2000);
    ESP.restart();
  }
 
  randomSeed(ESP.getChipId());
  int startDelay = random(0, Schreibpause);
  DBGF("\nStartverzögerung: %d ms (zufällig gewählt)\n", startDelay);
  delay(startDelay);
 
  delay(3000);
  g_bootMs = millis();
 
  DBGPRINTLN("\n=== Setup abgeschlossen, Messbetrieb startet ===\n");
}
 
// ==========================================
// ========== LOOP (WIRD DAUERHAFT AUSGEFÜHRT) ====
// ==========================================
void loop() {
  DBGPRINTLN("--- Neuer Messzyklus ---");
 
  DBGPRINT("BME280 wird ausgelesen... ");
  bme.takeForcedMeasurement();
 
  float hum = bme.readHumidity();
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;
  float p0 = bme.seaLevelForAltitude(ALTITUDE, pressure);
 
  if (isnan(hum) || hum < 0 || hum > 100) hum = SENSOR_ERROR_VALUE;
  if (isnan(temp)) temp = SENSOR_ERROR_VALUE;
  if (isnan(p0) || p0 < 0) p0 = SENSOR_ERROR_VALUE;
 
  DBGPRINTLN("OK");
  DBGF("  Temperatur:      %.2f °C\n", temp);
  DBGF("  Luftfeuchtigkeit: %.1f %%\n", hum);
  DBGF("  Luftdruck:       %.1f hPa (Meereshöhe)\n", p0);
 
  DBGPRINT("MH-Z19B (CO2) wird ausgelesen... ");
  int ppm_val = CO2_ERROR_VALUE;
  int tco2 = 0;
 
  if (g_firstCo2Init) {
    uint32_t t0 = millis();
    while (!readMHZ19B_Safe(ppm_val, tco2) && millis() - t0 < 1500) {
      delay(200);
    }
    g_firstCo2Init = false;
    DBGPRINTLN("(Erstmessung)");
  } else {
    readMHZ19B_Safe(ppm_val, tco2);
  }
 
  static int ppm_uart = 0;
  if (ppm_val > 0) {
    ppm_uart = ppm_val + CO2_OFFSET;
    DBGPRINTLN("OK");
    DBGF("  CO2-Gehalt: %d ppm (Rohwert %d + Offset %d)\n", ppm_uart, ppm_val, CO2_OFFSET);
  } else {
    DBGPRINTLN("FEHLER (verwende letzten Wert)");
    DBGF("  CO2-Gehalt: %d ppm (alter Wert)\n", ppm_uart);
  }
 
  DBGPRINT("SDS011 (Feinstaub) wird ausgelesen... ");
  setSDS011Sleep(false);
  delay(10000);
 
  float pm25 = SENSOR_ERROR_VALUE;
  float pm10 = SENSOR_ERROR_VALUE;
 
  if (readSDS011(pm25, pm10)) {
    DBGPRINTLN("OK");
    DBGF("  PM2.5: %.1f µg/m³\n", pm25);
    DBGF("  PM10:  %.1f µg/m³\n", pm10);
  } else {
    DBGPRINTLN("FEHLER");
    pm25 = 0;
    pm10 = 0;
  }
 
  setSDS011Sleep(true);
 
  DBGPRINT("LDR (Helligkeit) wird ausgelesen... ");
  float ldrWert = readLDR();
  DBGPRINTLN("OK");
  DBGF("  Helligkeit: %.0f (invertiert: hell=klein, dunkel=groß)\n", ldrWert);
 
  DBGPRINTLN("\nWLAN-Verbindung wird geprüft...");
  ensureWifi(10000);
 
  DBGPRINTLN("Daten werden gesendet...");
  sendData(hum, temp, p0, (float)ppm_uart, pm25, pm10, ldrWert);
 
  uint32_t now = millis();
 
  if (!g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) {
    DBGPRINTLN("\n[WATCHDOG] Kein erfolgreicher Upload binnen 3 Minuten!");
    DBGPRINTLN("[WATCHDOG] ESP8266 startet neu...");
    delay(500);
    ESP.restart();
  }
 
  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) {
    DBGPRINTLN("\n[WATCHDOG] WLAN länger als 2 Minuten nicht erreichbar!");
    DBGPRINTLN("[WATCHDOG] ESP8266 startet neu...");
    delay(500);
    ESP.restart();
  }
 
  int jitter = random(-300, 300);
  int actualDelay = Schreibpause + jitter;
  DBGF("\nPause: %d ms (mit Jitter)\n", actualDelay);
  DBGPRINTLN("=====================================\n");
 
  delay(actualDelay);
}
 
// ==========================================
// ========== WLAN-FUNKTIONEN ==========
// ==========================================
bool wifiBootConnect() {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);
  WiFi.setOutputPower(20.5);
  WiFi.setSleepMode(WIFI_NONE_SLEEP);
 
  for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {
    DBGF("[BOOT] WLAN-Verbindungsversuch %u/%u\n", attempt, WIFI_BOOT_TRIES);
 
    WiFi.disconnect(true);
    delay(200);
    WiFi.forceSleepBegin();
    delay(200);
    WiFi.forceSleepWake();
    delay(200);
 
    WiFi.config(ip, gateway, subnet, dns);
    WiFi.begin(ssid, password);
 
    if (ensureWifi(20000)) {
      DBGF("[WIFI] Verbunden! IP: %s, RSSI: %d dBm\n",
           WiFi.localIP().toString().c_str(), WiFi.RSSI());
 
      WiFiClient poke;
      poke.connect(gateway, 80);
      poke.stop();
 
      return true;
    }
 
    delay(1000 + random(0, 2000));
  }
 
  return false;
}
 
bool ensureWifi(uint32_t timeoutMs) {
  if (WiFi.status() == WL_CONNECTED) {
    return true;
  }
 
  DBGPRINT("[WIFI] Warte auf Verbindung");
  uint32_t start = millis();
 
  while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
    delay(250);
    DBGPRINT(".");
  }
 
  if (WiFi.status() == WL_CONNECTED) {
    DBGPRINTLN(" OK");
    g_wifiDownSince = 0;
    return true;
  }
 
  DBGPRINTLN(" Timeout!");
 
  if (!g_wifiDownSince) {
    g_wifiDownSince = millis();
  }
 
  return false;
}
 
bool netSoftReset(uint32_t connectTimeoutMs) {
  DBGPRINTLN("[NET] Soft-Reset wird durchgeführt...");
 
  WiFi.disconnect(true);
  delay(200);
  WiFi.forceSleepBegin();
  delay(200);
  WiFi.forceSleepWake();
  delay(200);
 
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  return ensureWifi(connectTimeoutMs);
}
 
bool tcpPreflight(uint16_t port, uint16_t graceMs) {
  WiFiClient poke;
 
  if (!poke.connect(SERVER_IP, port)) {
    return false;
  }
 
  delay(graceMs);
  poke.stop();
 
  return true;
}
 
// ==========================================
// ========== SENSOR-FUNKTIONEN ==========
// ==========================================
bool readMHZ19B_Safe(int &ppm, int &tempC) {
  while (mhzSerial.available()) {
    mhzSerial.read();
  }
 
  mhzSerial.write(cmdReadCO2, 9);
 
  uint8_t resp[9];
  int i = 0;
  uint32_t start = millis();
 
  while (millis() - start < 200) {
    if (mhzSerial.available()) {
      resp[i++] = mhzSerial.read();
      if (i == 9) break;
    }
  }
 
  if (i != 9) {
    return false;
  }
 
  if (resp[0] != 0xFF || resp[1] != 0x86) {
    return false;
  }
 
  uint8_t sum = 0;
  for (int k = 1; k < 8; k++) {
    sum += resp[k];
  }
  sum = 0xFF - sum + 1;
 
  if (sum != resp[8]) {
    return false;
  }
 
  ppm = resp[2] * 256 + resp[3];
  tempC = resp[4] - 40;
 
  return true;
}
 
bool readSDS011(float &pm25, float &pm10) {
  uint8_t buf[10];
  int idx = 0;
  uint32_t start = millis();
 
  while (millis() - start < 2000) {
    if (sdsSerial.available()) {
      uint8_t b = sdsSerial.read();
 
      if (idx == 0 && b != 0xAA) {
        continue;
      }
 
      buf[idx++] = b;
 
      if (idx == 10) {
        if (buf[0] == 0xAA && buf[1] == 0xC0 && buf[9] == 0xAB) {
          pm25 = (buf[2] + 256 * buf[3]) / 10.0f;
          pm10 = (buf[4] + 256 * buf[5]) / 10.0f;
          return true;
        } else {
          idx = 0;
        }
      }
    }
  }
 
  return false;
}
 
void setSDS011Sleep(bool sleep) {
  if (sleep) {
    sdsSerial.write(sds011_sleep, sizeof(sds011_sleep));
  } else {
    sdsSerial.write(sds011_wakeup, sizeof(sds011_wakeup));
  }
  delay(100);
}
 
float readLDR() {
  int raw = analogRead(LDR_PIN);
  raw = LDR_INVERTED_OFFSET - raw;
 
  if (raw < 0) raw = 0;
 
  return (float)raw;
}
 
// ==========================================
// ========== DATEN SENDEN ==========
// ==========================================
void sendData(float feuchtigkeit, float temp, float druck, float co2, float pm25, float pm10, float ldr) {
  if (WiFi.status() != WL_CONNECTED) {
    g_lastSendFailed = true;
    DBGPRINTLN("[HTTP] Abbruch: WLAN nicht verbunden");
    return;
  }
 
  if (DEBUG) {
    DBGF("[WIFI] Signalstärke: %d dBm\n", WiFi.RSSI());
  }
 
  char path[128];
  snprintf(path, sizeof(path),
           "/emoncms/input/post.json?node=%s&apikey=%s",
           CLIENT, apikey);
 
  char body[256];
  snprintf(body, sizeof(body),
    "json={\"F_rel\":%.1f,\"T_C\":%.2f,\"P_hPa\":%.1f,\"CO2_ppm\":%.0f,\"PM25\":%.1f,\"PM10\":%.1f,\"H_bit\":%.0f}",
    feuchtigkeit, temp, druck, co2, pm25, pm10, ldr);
 
  if (g_lastSendFailed) {
    bool ok = tcpPreflight(httpPort, 80);
    DBGF("[NET] Preflight %s\n", ok ? "OK" : "FAIL");
  }
 
  String hostStr = SERVER_IP.toString();
 
  auto doPostOnce = [&](int attempt) -> int {
    if (WiFi.status() != WL_CONNECTED) {
      ensureWifi(8000);
      if (WiFi.status() != WL_CONNECTED) {
        DBGF("[HTTP] WLAN down (Try %d) – skip\n", attempt);
        return -10000;
      }
    }
 
    WiFiClient client;
    client.setNoDelay(true);
    client.setTimeout(15000);
 
    HTTPClient http;
    http.setTimeout(15000);
    http.setReuse(false);
 
    if (!http.begin(client, hostStr, httpPort, path, false)) {
      DBGPRINTLN("[HTTP] begin() fehlgeschlagen");
      return -2;
    }
 
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Connection", "close");
 
    int code = http.POST(body);
    String err = http.errorToString(code);
    String payload = (code > 0) ? http.getString() : String();
    http.end();
 
    DBGF("[HTTP] Versuch %d → code=%d (%s)\n", attempt, code, err.c_str());
 
    if (code == 200) {
      if (DEBUG && payload.length() > 0) {
        DBGPRINT("[HTTP] Antwort: ");
        DBGPRINTLN(payload);
      }
      DBGF("  Gesendet: F=%.1f%%, T=%.2f°C, P=%.1fhPa, CO2=%.0fppm, PM2.5=%.1fµg/m³, PM10=%.1fµg/m³, LDR=%.0f\n",
           feuchtigkeit, temp, druck, co2, pm25, pm10, ldr);
      g_uploadedOnce   = true;
      g_lastSendFailed = false;
      g_lastUploadMs   = millis();
    }
    return code;
  };
 
  int code = -9999;
 
  for (int attempt = 1; attempt <= 3; ++attempt) {
    code = doPostOnce(attempt);
    if (code == 200) return;
    delay(250 * attempt);
  }
 
  DBGPRINTLN("[NET] Soft-Reset #1 …");
  if (netSoftReset(15000)) {
    code = doPostOnce(4);
    if (code == 200) return;
  }
 
  DBGPRINTLN("[NET] Soft-Reset #2 …");
  if (netSoftReset(15000)) {
    code = doPostOnce(5);
    if (code == 200) return;
  }
 
  g_lastSendFailed = true;
  DBGF("[HTTP] FEHLGESCHLAGEN nach allen Versuchen | Node=%s | F=%.1f T=%.2f P0=%.1f CO2=%.0f PM2.5=%.1f PM10=%.1f LDR=%.0f\n",
       CLIENT, feuchtigkeit, temp, druck, co2, pm25, pm10, ldr);
}

Raummessbox (Innenbereich)

Sensoren: DHT22 (Luftfeuchte), BMP180 (Temperatur, Luftdruck), MH-Z19B (CO2), LDR (Helligkeit), NeoPixel-LED-Band (CO2-Ampel, 4 oder 5 LEDs). Pin-Belegung und Funktionsprinzipien siehe Hauptanleitung, Teil 3.

Angepasst gegenüber der ursprünglichen Fassung: Netzwerk-Konfiguration
auf das Bereich-A/B/C-Schema umgestellt (siehe Erklärung oben auf
dieser Seite). Messintervall bei den ursprünglichen 20 Sekunden
belassen (gut geeignet für ein Klassenzimmer, in dem auch kurzfristige
Schwankungen - z. B. CO2-Anstieg beim Lüften - interessant sind).
Für den EmonCMS-Feed entsprechend Interval 20 verwenden.
/********************************************************************************************
 *  Raumklima D1 mini – Schul-/Langzeit-Variante (statische IP, selbstheilend)
 *
 *  Features:
 *   - NUR HIER ÄNDERN: SENSOR_ID, NUM_LEDS, CO2_OFFSET, Netzwerk-Auswahl (Bereich A/B/C)
 *   - Statische IP (kein DHCP), robuster WLAN-Start mit 3 Versuchen + Funk-Reset
 *   - Zufällige Startverzögerung (0–30 s) + Jitter je Zyklus (±0,3 s) → entkoppelt Massenstart
 *   - Auto-Neustart, falls:
 *       • beim Boot 3 min lang kein erfolgreicher Upload
 *       • WLAN im Betrieb > 2 min am Stück weg
 *   - MH-Z19B: „sicheres" Auslesen (9 Bytes, Checksumme), sanfter Erststart
 *   - BMP180: p0 (Meereshöhe) wird gesendet
 *   - LDR: invertiert (hell → klein, dunkel → groß)
 *   - 20 s Intervall für Langzeit-Monitoring (EmonCMS PHPFina 20 s empfohlen)
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include "DHT.h"
#include <Wire.h>
#include <SFE_BMP180.h>
#include <Adafruit_NeoPixel.h>
#include <SoftwareSerial.h>
 
/****************************************  NUR HIER ÄNDERUNGEN VORNEHMEN (z. B. 111 → 112) ******************************/
//
//                                        1) Geräte-Identität
#define SENSOR_ID 111                    // eindeutige Sensor-Nummer
//
//                                        2) Bauart - Länge der LED-Anzeige
#define NUM_LEDS  5                      // 5 = Normal, 4 = Gerät mit 4 LEDs
//
//                                        3) CO2-Kalibrierung
#define CO2_OFFSET 40                    // Kalibrierung an Frischluft wird auf 400 ppm gesetzt.
//
//                                        4) Netzwerk-Auswahl siehe weiter unten (Bereich A/B/C)
//
/**************************************** AB HIER KEINE ÄNDERUNGEN VORNEHMEN *****************************************************/
 
// Aus SENSOR_ID werden automatisch Client-Name ("111") & IP 192.168.1.SENSOR_ID erzeugt:
char      CLIENT[8];                     // wird im setup() mit "111" etc. gefüllt
IPAddress ip(192, 168, 1, SENSOR_ID);    // feste IP aus SENSOR_ID
 
IPAddress SERVER_IP(192, 168, 1, 2);     // EmonCMS-Server (gilt für alle drei Netzwerkprofile)
const char* host     = "192.168.1.2";    // EmonCMS-Server (ohne http://)
const int   httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0,   0,   0,   0);
static bool g_lastSendFailed = false;  // für die WLANverbindungsqualität
 
 
/*** 3) Messintervall (Langzeitbetrieb) *****************************************************/
const int Schreibpause = 20000;          // 20 s; für 10 s im Test nur hier 10000 setzen
 
/*** 4) Sensor-Hardware *********************************************************************/
// DHT22 (Luftfeuchte/Temperatur)
#define DHTPIN   13                      // D7
#define DHTTYPE  DHT22
DHT dht(DHTPIN, DHTTYPE);
 
// BMP180 (Luftdruck/Temperatur)
#define ALTITUDE 42.0                    // Höhe [m] für p0-Korrektur
SFE_BMP180 pressure;
 
// MH-Z19B (CO₂, UART)
SoftwareSerial mhzSerial(12, 14);        // RX = D6, TX = D5
byte cmdReadCO2[] = {0xFF,0x01,0x86,0,0,0,0,0,0x79};
 
// LDR (Helligkeit, analog)
#define LDR_PIN A0
 
// LED-Band (CO₂-Ampel)
#define LED_PIN   2                      // D4
 
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
 
/*** 5) Selbstheilungs-Parameter ************************************************************/
const uint32_t BOOT_UPLOAD_DEADLINE_MS = 180000; // 3 min: bis dahin muss 1 Upload geklappt haben
const uint32_t WIFI_DOWN_REBOOT_MS     = 120000; // 2 min am Stück offline → Neustart
const uint8_t  WIFI_BOOT_TRIES         = 3;      // WLAN-Startversuche mit Funk-Reset
 
/*** 6) Zustände ****************************************************************************/
static bool     g_firstCo2Init    = true; // CO₂-Sensor im ersten Zyklus besonders behandeln
static bool     g_uploadedOnce    = false; // wurde seit Boot mindestens 1x erfolgreich gesendet?
static uint32_t g_bootMs          = 0;     // Boot-Zeitpunkt
static uint32_t g_wifiDownSince   = 0;     // Beginn einer Offline-Phase (0 = online)
 
/*** 7) Prototypen **************************************************************************/
bool ensureWifi(uint32_t timeoutMs = 15000);
bool wifiBootConnect();
void LED_Band(int co2);
float readLDR();
bool readMHZ19B_Safe(int &ppm, int &tempC);
void sendData(float feuchtigkeit, float temp, float druck, float co2, float ldr);
 
/*** 8) SETUP *******************************************************************************/
void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("\n=== Boot: Raumklima-Sensor startet ===");
 
  // CLIENT-Name aus SENSOR_ID erzeugen (z. B. 111 → "111")
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("Sensor-ID = %s, IP = %s\n", CLIENT, ip.toString().c_str());
 
  // LED-Band initial
  strip.begin();
  strip.show();
 
  // Sensoren starten
  dht.begin();
  if (!pressure.begin()) {
    Serial.println("FEHLER: BMP180 nicht gefunden! (I2C prüfen: D1=SCL, D2=SDA, GND, 3V3)");
    while (true) delay(10); // „hart stehenbleiben" → Fehler sichtbar
  }
 
  // WLAN-Start: 3 saubere Versuche mit Funk-Reset & Backoff
  if (!wifiBootConnect()) {
    Serial.println("[BOOT] WLAN nicht erreichbar → Neustart");
    delay(800);
    ESP.restart();
  }
 
  // CO₂-Sensor starten (UART)
  mhzSerial.begin(9600);
  Serial.println("MH-Z19B UART bereit");
 
  // Zufällige Startverzögerung (0..Schreibpause) → entkoppelt Massenstart nach Stromausfall
  randomSeed(ESP.getChipId());
  int startDelay = random(0, Schreibpause);
  Serial.printf("Initiale Startpause: %d ms\n", startDelay);
  delay(startDelay);
 
  // kurze Sensor-Aufwärmzeit
  delay(3000);
 
  // Boot-Zeit merken
  g_bootMs = millis();
}
 
/*** 9) LOOP ********************************************************************************/
void loop() {
  /***** DHT22: Luftfeuchte (°C vom DHT kannst du bei Bedarf zusätzlich nutzen) *****/
  float hum = dht.readHumidity();
  if (isnan(hum)) hum = -1.0; // -1 = „ungültig" → in EmonCMS gut erkennbar
 
  /***** BMP180: Temperatur (T), Stationsdruck (P), p0 (auf Meereshöhe) *****/
  char status;
  double T = NAN, P = NAN, p0 = NAN;
  status = pressure.startTemperature();
  if (status) {
    delay(status);
    if (pressure.getTemperature(T)) {
      status = pressure.startPressure(3);       // Oversampling = 3 (genauer)
      if (status) {
        delay(status);
        if (pressure.getPressure(P, T)) {
          p0 = pressure.sealevel(P, ALTITUDE);  // Korrektur auf Meereshöhe
        }
      }
    }
  }
 
  /***** MH-Z19B: sanfter Erststart (max 1,5 s warten), dann normal *****/
  int ppm_val = -1, tco2 = 0;
  if (g_firstCo2Init) {
    uint32_t t0 = millis();
    while (!readMHZ19B_Safe(ppm_val, tco2) && millis() - t0 < 1500) {
      delay(200); // Sensor bekommt Zeit für erstes vollständiges Frame
    }
    g_firstCo2Init = false;
  } else {
    readMHZ19B_Safe(ppm_val, tco2);
  }
  static int ppm_uart = 0;                           // letzter gültiger CO₂-Wert (ppm)
  if (ppm_val > 0) ppm_uart = ppm_val + CO2_OFFSET;  // Offset direkt beim Schreiben eines neuen Wertes
 
/***** LDR: umgekehrt (hell → klein, dunkel → groß) *****/
  float ldrWert = readLDR();
 
  /***** LED-Ampel & WLAN-Status *****/
  LED_Band(ppm_uart);
  ensureWifi(10000);                 // ggf. Reconnect; merkt Offline-Zeitraum
 
  /***** NaN-Schutz: nie „nan" in JSON senden *****/
  if (isnan(T))  T  = -99.9;
  if (isnan(p0)) p0 = -1.0;
  if (ppm_uart < 0) ppm_uart = 0;
 
  /***** Upload zu EmonCMS *****/
  sendData(hum, (float)T, (float)p0, (float)ppm_uart, (float)ldrWert);
 
  /***** Selbstheilung / Watchdogs *****/
  uint32_t now = millis();
 
  // a) Beim Boot: wenn 3 min lang kein Upload erfolgreich → Neustart
  if (!g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) {
    Serial.println("[WD] Kein erfolgreicher Upload binnen 3 min → Reboot");
    delay(500);
    ESP.restart();
  }
 
  // b) Laufbetrieb: WLAN > 2 min am Stück weg → Neustart
  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) {
    Serial.println("[WD] WLAN > 120 s weg → Reboot");
    delay(500);
    ESP.restart();
  }
 
  // Informationen zur Signalstärke - zur Ausrichtung der Antenne...
  static uint32_t cnt = 0;
  if ((++cnt % 6) == 0) {  // alle ~3 Minuten bei 30s-Intervall
    Serial.printf("[WIFI] RSSI: %d dBm\n", WiFi.RSSI());
  }
 
  /***** Intervall + kleiner Jitter (±0,3 s) - zur WLAN und EMONCMS Lastverteilung *****/
  int jitter = random(-300, 300);
  delay(Schreibpause + jitter);
}
 
/*** ENDE von LOOP *************************************************************************/
 
/*** 10) WLAN-Helfer ***********************************************************************/
 
// Mehrstufiger WLAN-Boot: Funk hart zurücksetzen, 3 Versuche mit Backoff
bool wifiBootConnect() {
  WiFi.persistent(false);            // keine alten Credentials aus Flash verwenden
  WiFi.mode(WIFI_STA);
  WiFi.setAutoConnect(true);
  WiFi.setAutoReconnect(true);
  WiFi.setOutputPower(20.5);         // maximale Sendeleistung
  WiFi.setSleepMode(WIFI_NONE_SLEEP);// kein Modem-Sleep während Boot
 
  for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {
    Serial.printf("[BOOT] WLAN-Versuch %u/%u\n", attempt, WIFI_BOOT_TRIES);
 
    // Funk & Assoc/ARP-Zustand hart zurücksetzen
    WiFi.disconnect(true);
    delay(200);
    WiFi.forceSleepBegin(); delay(200);
    WiFi.forceSleepWake();  delay(200);
 
    // Statische IP setzen & verbinden
    WiFi.config(ip, gateway, subnet, dns);
    WiFi.begin(ssid, password);
 
    if (ensureWifi(20000)) {
      Serial.printf("[WIFI] OK: %s  RSSI:%d dBm\n",
                    WiFi.localIP().toString().c_str(), WiFi.RSSI());
      // Optional: Gateway kurz „anticken", damit ARP im Router sitzt
      WiFiClient poke; poke.connect(gateway, 80); poke.stop();
      return true;
    }
 
    // Backoff (1.0–3.0 s) vor nächstem Versuch – verhindert Synchronsturm
    int backoff = 1000 + random(0, 2000);
    Serial.printf("[BOOT] fehlgeschlagen → Backoff %d ms\n", backoff);
    delay(backoff);
  }
  return false;
}
 
// Reconnect im Betrieb; merkt Offline-Startzeit für Watchdog
bool ensureWifi(uint32_t timeoutMs) {
  if (WiFi.status() == WL_CONNECTED) return true;
  Serial.print("[WIFI] Verbinde");
  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
    delay(250);
    Serial.print(".");
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("\n[WIFI] OK: %s\n", WiFi.localIP().toString().c_str());
    g_wifiDownSince = 0;
    return true;
  }
  Serial.println("\n[WIFI] Timeout");
  if (!g_wifiDownSince) g_wifiDownSince = millis(); // Offline-Phase starten
  return false;
}
 
/*** 11) Sensor-/Upload-Helfer **************************************************************/
 
// MH-Z19B „sicher" auslesen (9 Bytes, Header prüfen, Checksumme prüfen, 200 ms Timeout)
bool readMHZ19B_Safe(int &ppm, int &tempC) {
  // alten UART-Puffer leeren (verhindert Mischpakete)
  while (mhzSerial.available()) mhzSerial.read();
 
  // Anfrage senden
  mhzSerial.write(cmdReadCO2, 9);
 
  // Antwort (max. 200 ms warten)
  uint8_t resp[9]; int i = 0; uint32_t start = millis();
  while (millis() - start < 200) {
    if (mhzSerial.available()) {
      resp[i++] = mhzSerial.read();
      if (i == 9) break;  // vollständiges Paket
    }
    delay(1);
  }
 
  // Header + Länge prüfen
  if (i != 9 || resp[0] != 0xFF || resp[1] != 0x86) return false;
 
  // Checksumme prüfen
  uint8_t sum = 0; for (int k = 1; k < 8; k++) sum += resp[k];
  sum = 0xFF - sum + 1;
  if (sum != resp[8]) return false;
 
  // Werte extrahieren
  ppm   = resp[2] * 256 + resp[3];
  tempC = resp[4] - 40;
  return true;
}
 
// LDR umgekehrt: hell → großer Wert, dunkel → kleiner Wert (0..1023)
float readLDR() {
  int raw = analogRead(LDR_PIN);
  raw = 450 - raw;                                    // Umkehrung - heller wird es in der BOX nicht
  return (float)raw;
}
 
// Sanfte Netzwerk-Kur: Radio kurz aus/an + frische Assoziation mit statischer IP
bool netSoftReset(uint32_t connectTimeoutMs = 15000) {
  Serial.println("[NET] Soft-Reset: disconnect + radio cycle");
  WiFi.disconnect(true);            // Assoc/ARP löschen
  delay(200);
  WiFi.forceSleepBegin();           // Radio aus
  delay(200);
  WiFi.forceSleepWake();            // Radio an
  delay(200);
  WiFi.config(ip, gateway, subnet, dns);   // statische IP erneut setzen
  WiFi.begin(ssid, password);
  return ensureWifi(connectTimeoutMs);
}
 
// Kurzer TCP-Preflight: baut eine nackte Verbindung zum Server auf und schließt sie wieder.
// Hilft ARP/NAT/State "aufzuwecken".
bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50) {
  WiFiClient poke;
  if (!poke.connect(SERVER_IP, port)) return false;
  delay(graceMs);
  poke.stop();
  return true;
}
 
/****************************************************** SENDEN der DATEN **********************************************************************/
void sendData(float feuchtigkeit, float temp, float druck, float co2, float ldr) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[HTTP] offline – Upload übersprungen");
    g_lastSendFailed = true;
    return;
  }
 
  // Pfad/Query, weil wir die 5-Parameter-Variante mit Host-String nutzen
  String path  = "/emoncms/input/post.json";
  String query = "?node=" + String(CLIENT) + "&apikey=" + String(apikey);
 
  String body = "json={\"F_rel\":" + String(feuchtigkeit,1) +
                ",\"T_C\":"        + String(temp,2) +
                ",\"P_hPa\":"      + String(druck,1) +
                ",\"CO2_ppm\":"    + String(co2,0) +
                ",\"H_bit\":"      + String(ldr,0) + "}";
 
  // Preflight nach Fehlschlag
  if (g_lastSendFailed) {
    bool ok = tcpPreflight(httpPort, 80);
    Serial.printf("[NET] Preflight %s\n", ok ? "OK" : "FAIL");
  }
 
  // Host-String aus IPAddress erzeugen (für http.begin(...))
  String hostStr = SERVER_IP.toString();
 
  auto doPostOnce = [&](int attempt)->int {
    if (WiFi.status() != WL_CONNECTED) {
      ensureWifi(8000);
      if (WiFi.status() != WL_CONNECTED) {
        Serial.printf("[HTTP] WLAN down (Try %d) – skip\n", attempt);
        return -10000;
      }
    }
 
    WiFiClient client;
    client.setNoDelay(true);
    client.setTimeout(15000);
 
    HTTPClient http;
    http.setTimeout(15000);
    http.setReuse(false);
 
    if (!http.begin(client, hostStr, httpPort, path + query, false)) {
      Serial.println("[HTTP] begin() failed");
      return -2;
    }
 
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.addHeader("Connection", "close");
 
    int code = http.POST(body);
    String err = http.errorToString(code);
    String payload = (code > 0) ? http.getString() : String();
    http.end();
 
    Serial.printf("[HTTP] Try %d → code=%d (%s)\n", attempt, code, err.c_str());
    if (code == 200) {
      if (payload.length()) { Serial.print("[HTTP] resp: "); Serial.println(payload); }
      Serial.printf("Sent: F=%.1f T=%.2f P0=%.1f CO2=%.0f LDR=%.0f\n",
                    feuchtigkeit, temp, druck, co2, ldr);
      g_uploadedOnce   = true;
      g_lastSendFailed = false;  // Erfolg → Flag löschen
    }
    return code;
  };
 
  // 3 normale Versuche
  int code = -9999;
  for (int attempt = 1; attempt <= 3; ++attempt) {
    code = doPostOnce(attempt);
    if (code == 200) return;
    delay(250 * attempt);
  }
 
  // Soft-Reset + 4. Versuch
  if (netSoftReset(15000)) {
    code = doPostOnce(4);
    if (code == 200) return;
  }
 
  // Zweiter Soft-Reset + 5. Versuch
  Serial.println("[NET] Soft-Reset #2 …");
  if (netSoftReset(15000)) {
    code = doPostOnce(5);
    if (code == 200) return;
  }
 
  g_lastSendFailed = true; // alles fehlgeschlagen
  Serial.printf("[HTTP] FAILED after retries | Node=%s | F=%.1f T=%.2f P0=%.1f CO2=%.0f LDR=%.0f\n",
                CLIENT, feuchtigkeit, temp, druck, co2, ldr);
}
 
/********************************************************** ALLE DATEN GESENDET ***********************************************************/
 
/*** 12) CO₂-LED-Ampel (Balkenanzeige links → rechts) *************************************/
void LED_Band(int co2) {
 
  const uint32_t BLAU  = strip.Color(0,   0,   255);
  const uint32_t GRUEN = strip.Color(0,   255, 0  );
  const uint32_t GELB  = strip.Color(255, 255, 0  );
  const uint32_t ROT   = strip.Color(255, 0,   0  );
  const uint32_t WEISS = strip.Color(255, 255, 255);
  const uint32_t AUS   = strip.Color(0,   0,   0  );
 
  // ── NUR HIER ÄNDERN: 5 = normal, 4 = Gerät mit 4 LEDs ───────────────────
  const int LEDS = NUM_LEDS;   // nimmt den Wert aus #define NUM_LEDS oben
  // ─────────────────────────────────────────────────────────────────────────
 
  // Alarm ab 2700: Rot-Weiß blitzen, dann dauerhaft Rot
  if (co2 >= 2700) {
    strip.setBrightness(255);
    for (int i = 0; i < 5; i++) {
      strip.fill(ROT);   strip.show(); delay(200);
      strip.fill(WEISS); strip.show(); delay(200);
      strip.clear();     strip.show(); delay(200);
    }
    // Nach dem Blitzen: alle vorhandenen LEDs dauerhaft Rot
    strip.setBrightness(120);
    for (int i = 0; i < LEDS; i++) {
      strip.setPixelColor(i, ROT);
    }
    // Übrige LEDs aus (bei 4 LEDs: LED4 dunkel)
    for (int i = LEDS; i < 5; i++) {
      strip.setPixelColor(i, AUS);
    }
    strip.show();
    return;  // danach nicht erneut show()
  }
 
  // Vollständiges 5-LED-Muster (LED0=links ... LED4=rechts)
  uint32_t pat[5];
  uint8_t  brightness;
 
  //                        LED0   LED1   LED2   LED3   LED4   Helligkeit
  if      (co2 <  500) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=BLAU;  pat[3]=BLAU;  pat[4]=AUS;   brightness=2;   }
  else if (co2 <  650) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=BLAU;  pat[3]=BLAU;  pat[4]=GRUEN; brightness=2;   }
  else if (co2 <  800) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=BLAU;  pat[3]=GRUEN; pat[4]=GRUEN; brightness=2;   }
  else if (co2 <  950) { pat[0]=BLAU;  pat[1]=BLAU;  pat[2]=GRUEN; pat[3]=GRUEN; pat[4]=GRUEN; brightness=3;   }
  else if (co2 < 1200) { pat[0]=BLAU;  pat[1]=GRUEN; pat[2]=GRUEN; pat[3]=GRUEN; pat[4]=GRUEN; brightness=3;   }
  else if (co2 < 1350) { pat[0]=GRUEN; pat[1]=GRUEN; pat[2]=GRUEN; pat[3]=GRUEN; pat[4]=GELB;  brightness=3;   }
  else if (co2 < 1500) { pat[0]=GRUEN; pat[1]=GRUEN; pat[2]=GRUEN; pat[3]=GELB;  pat[4]=GELB;  brightness=4;   }
  else if (co2 < 1650) { pat[0]=GRUEN; pat[1]=GRUEN; pat[2]=GELB;  pat[3]=GELB;  pat[4]=GELB;  brightness=4;   }
  else if (co2 < 1800) { pat[0]=GRUEN; pat[1]=GELB;  pat[2]=GELB;  pat[3]=GELB;  pat[4]=GELB;  brightness=4;   }
  else if (co2 < 1950) { pat[0]=GELB;  pat[1]=GELB;  pat[2]=GELB;  pat[3]=GELB;  pat[4]=ROT;   brightness=60;  }
  else if (co2 < 2100) { pat[0]=GELB;  pat[1]=GELB;  pat[2]=GELB;  pat[3]=ROT;   pat[4]=ROT;   brightness=80;  }
  else if (co2 < 2250) { pat[0]=GELB;  pat[1]=GELB;  pat[2]=ROT;   pat[3]=ROT;   pat[4]=ROT;   brightness=100; }
  else                 { pat[0]=GELB;  pat[1]=ROT;   pat[2]=ROT;   pat[3]=ROT;   pat[4]=ROT;   brightness=120; }
 
  // Nur so viele LEDs ansteuern wie vorhanden (4 oder 5)
  strip.setBrightness(brightness);
  for (int i = 0; i < LEDS; i++) {
    strip.setPixelColor(i, pat[i]);
  }
  // Übrige LEDs ausschalten (bei 4 LEDs: LED4 bleibt dunkel)
  for (int i = LEDS; i < 5; i++) {
    strip.setPixelColor(i, AUS);
  }
  strip.show();
}

—————————————————2026-08-12——————————————————-

Programmcode der Sensor-Boxen

Diese Seite enthält den vollständigen Arduino-Code aller Sensor-Boxen, ausgelagert aus der Hauptanleitung und der EmonCMS-Einbindungsseite. Pin-Belegung, Funktionsprinzipien und Stolpersteine stehen weiterhin in der Hauptanleitung (Teil 3) – hier geht es nur um den Code selbst.

Netzwerk-Auswahlschema (Bereich A/B/C)

Alle Sketches auf dieser Seite nutzen dasselbe Schema, um zwischen drei Netzwerk-/Betriebsprofilen umzuschalten, ohne jedes Mal Zeilen aus- und einzukommentieren:

  • Bereich A – SAS-ARDUINO-mobil: persönliches Testnetz (z. B. Hotspot zuhause), zum Entwickeln und Testen von Sketches, bevor sie ans Schulnetz gehen
  • Bereich B – SAS-ARDUINO, Schul-Testbetrieb: isoliertes Schulnetz aus Teil 1 der Hauptanleitung, sendet mit dem TEST-API-Key (landet nicht in den „echten“ Live-Feeds)
  • Bereich C – SAS-ARDUINO, Schul-Livebetrieb: dasselbe Schulnetz, sendet mit dem LIVE-API-Key (echter Dauerbetrieb)

Im Kopf jedes Sketches wird genau eine der drei Zeilen auf true gesetzt:

#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif

Darunter folgen drei #if-Blöcke mit den jeweiligen Zugangsdaten. Der Compiler übernimmt automatisch nur den aktiven Block – die beiden anderen werden komplett ignoriert (kein Speicherverbrauch, keine doppelten Variablendefinitionen).

Wichtig bei Weitergabe an andere Schulen: Bereich A (SAS-ARDUINO-mobil)
enthält persönliche/private Zugangsdaten und sollte vor der
Veröffentlichung KOMPLETT gelöscht werden (den ganzen #if...#endif-
Block). Bereich B und C beziehen sich auf das isolierte, nach der
Hauptanleitung bei jeder Schule identisch aufgebaute Schulnetz
("SAS-ARDUINO") und können unverändert als Vorlage dienen - dort
müssen andere Schulen nur ihr eigenes WLAN-Passwort und ihre eigenen
API-Keys eintragen.

Testsensor (ohne echte Sensoren)

Dient ausschließlich dazu, die EmonCMS-Einrichtung (Inputs, Feeds, Dashboard) zu testen, bevor eine echte Sensor-Box angeschlossen ist. Es werden keine Sensoren ausgelesen – die Werte für Temperatur, Luftfeuchtigkeit und CO2 werden im Code simuliert (Temperatur/Feuchte als sanft schwankende Kurve, CO2 als leicht schwankender Zufallswert um 400 ppm).

Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT
für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle
in der Hauptanleitung).
/********************************************************************************************
 *  TESTSENSOR für ESP8266 D1 Mini
 *
 *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard).
 *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert!
 *
 *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch
 *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite).
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
 
#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden!
#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen)
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  // ---- Simulierte Werte (KEINE echten Sensoren!) ----
  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C
  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 %
  static int fakeCo2 = 400;
  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, CO2_ppm – dieselben Feldnamen wie bei den echten Boxen, sodass Sie die EmonCMS-Feeds genauso wie später bei echten Sensoren anlegen können (siehe Sensoren einbinden). Empfohlenes PHPFina-Interval für den Testsensor: 30 Sekunden (passend zu SENDEINTERVALL_MS).


Einfacher Sensor: nur BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck)

Ein echter, aber minimaler Sensor – guter nächster Schritt nach dem simulierten Testsensor, bevor die vollständigen Boxen (Klimamessbox, Raummessbox) zum Einsatz kommen. Nur ein BME280 über I²C, sonst nichts.

Verkabelung (I²C-Modus):

BME280-Pin Verbunden mit Hinweis
VCC 3,3V
GND GND
SCL D1 (GPIO5) Standard-I²C-Clock beim D1 Mini
SDA D2 (GPIO4) Standard-I²C-Data beim D1 Mini
CSB 3,3V Zwingend nötig, um den I²C-Modus zu aktivieren (statt SPI)
SDO GND Setzt die I²C-Adresse auf 0x76 – dieselbe Adresse wie bei der Klimamessbox (Teil 3), bewusst einheitlich gehalten, damit später auch die Raummessbox (aktuell noch BMP180+DHT22) beim geplanten Umstieg auf BME280 ohne Adresskonflikt eingebunden werden kann
/********************************************************************************************
 *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280
 *
 *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen
 *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus.
 *
 *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND
 *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox)
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
 
#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen!
 
// ==== TEST-EINSTELLUNG =======================================================
// 10 Sekunden statt der später üblichen 30-120 Sekunden - bewusst kurz gehalten,
// damit sich Änderungen beim Testen sofort in EmonCMS zeigen. Für Dauerbetrieb
// erhöhen (z. B. auf 120000 wie bei der Klimamessbox), um Server und WLAN nicht
// unnötig zu belasten!
#define SENDEINTERVALL_MS 10000
// ==============================================================================
 
#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77)
#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur
 
Adafruit_BME280 bme;
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Einfacher BME280-Sensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  Wire.begin();  // Standard-Pins: D1=SCL, D2=SDA
  if (!bme.begin(BME280_ADDRESS)) {
    Serial.println("FEHLER: BME280 nicht gefunden! I2C-Verkabelung und Adresse (0x76) prüfen.");
    while (true) delay(100);
  }
  Serial.println("BME280 OK");
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  float hum  = bme.readHumidity();
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;             // Pa → hPa
  float p0   = bme.seaLevelForAltitude(ALTITUDE, pressure);  // auf Meereshöhe umgerechnet
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"P_hPa\":%.1f}", temp, hum, p0);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f°C F=%.1f%% P=%.1fhPa\n", code, temp, hum, p0);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, P_hPa – dieselben Feldnamen wie später bei der Klimamessbox. Empfohlenes PHPFina-Interval: 10 Sekunden (passend zu SENDEINTERVALL_MS im Testbetrieb).


Klimamessbox (Außenbereich)

(folgt im nächsten Schritt)


Raummessbox (Innenbereich)

(folgt im nächsten Schritt)

———————————————————-alt - 30 Sekunden —————————-

Programmcode der Sensor-Boxen

Diese Seite enthält den vollständigen Arduino-Code aller Sensor-Boxen, ausgelagert aus der Hauptanleitung und der EmonCMS-Einbindungsseite. Pin-Belegung, Funktionsprinzipien und Stolpersteine stehen weiterhin in der Hauptanleitung (Teil 3) – hier geht es nur um den Code selbst.

Netzwerk-Auswahlschema (Bereich A/B/C)

Alle Sketches auf dieser Seite nutzen dasselbe Schema, um zwischen drei Netzwerk-/Betriebsprofilen umzuschalten, ohne jedes Mal Zeilen aus- und einzukommentieren:

  • Bereich A – SAS-ARDUINO-mobil: persönliches Testnetz (z. B. Hotspot zuhause), zum Entwickeln und Testen von Sketches, bevor sie ans Schulnetz gehen
  • Bereich B – SAS-ARDUINO, Schul-Testbetrieb: isoliertes Schulnetz aus Teil 1 der Hauptanleitung, sendet mit dem TEST-API-Key (landet nicht in den „echten“ Live-Feeds)
  • Bereich C – SAS-ARDUINO, Schul-Livebetrieb: dasselbe Schulnetz, sendet mit dem LIVE-API-Key (echter Dauerbetrieb)

Im Kopf jedes Sketches wird genau eine der drei Zeilen auf true gesetzt:

#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif

Darunter folgen drei #if-Blöcke mit den jeweiligen Zugangsdaten. Der Compiler übernimmt automatisch nur den aktiven Block – die beiden anderen werden komplett ignoriert (kein Speicherverbrauch, keine doppelten Variablendefinitionen).

Wichtig bei Weitergabe an andere Schulen: Bereich A (SAS-ARDUINO-mobil)
enthält persönliche/private Zugangsdaten und sollte vor der
Veröffentlichung KOMPLETT gelöscht werden (den ganzen #if...#endif-
Block). Bereich B und C beziehen sich auf das isolierte, nach der
Hauptanleitung bei jeder Schule identisch aufgebaute Schulnetz
("SAS-ARDUINO") und können unverändert als Vorlage dienen - dort
müssen andere Schulen nur ihr eigenes WLAN-Passwort und ihre eigenen
API-Keys eintragen.

Testsensor (ohne echte Sensoren)

Dient ausschließlich dazu, die EmonCMS-Einrichtung (Inputs, Feeds, Dashboard) zu testen, bevor eine echte Sensor-Box angeschlossen ist. Es werden keine Sensoren ausgelesen – die Werte für Temperatur, Luftfeuchtigkeit und CO2 werden im Code simuliert (Temperatur/Feuchte als sanft schwankende Kurve, CO2 als leicht schwankender Zufallswert um 400 ppm).

Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT
für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle
in der Hauptanleitung).
/********************************************************************************************
 *  TESTSENSOR für ESP8266 D1 Mini
 *
 *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard).
 *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert!
 *
 *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch
 *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite).
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
 
#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden!
#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen)
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  // ---- Simulierte Werte (KEINE echten Sensoren!) ----
  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C
  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 %
  static int fakeCo2 = 400;
  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, CO2_ppm – dieselben Feldnamen wie bei den echten Boxen, sodass Sie die EmonCMS-Feeds genauso wie später bei echten Sensoren anlegen können (siehe Sensoren einbinden). Empfohlenes PHPFina-Interval für den Testsensor: 30 Sekunden (passend zu SENDEINTERVALL_MS).


Einfacher Sensor: nur BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck)

Ein echter, aber minimaler Sensor – guter nächster Schritt nach dem simulierten Testsensor, bevor die vollständigen Boxen (Klimamessbox, Raummessbox) zum Einsatz kommen. Nur ein BME280 über I²C, sonst nichts.

Verkabelung (I²C-Modus):

BME280-Pin Verbunden mit Hinweis
VCC 3,3V
GND GND
SCL D1 (GPIO5) Standard-I²C-Clock beim D1 Mini
SDA D2 (GPIO4) Standard-I²C-Data beim D1 Mini
CSB 3,3V Zwingend nötig, um den I²C-Modus zu aktivieren (statt SPI)
SDO GND Setzt die I²C-Adresse auf 0x76 – dieselbe Adresse wie bei der Klimamessbox (Teil 3), bewusst einheitlich gehalten, damit später auch die Raummessbox (aktuell noch BMP180+DHT22) beim geplanten Umstieg auf BME280 ohne Adresskonflikt eingebunden werden kann
/********************************************************************************************
 *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280
 *
 *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen
 *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus.
 *
 *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND
 *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox)
 ********************************************************************************************/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
 
#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen!
#define SENDEINTERVALL_MS 30000    // alle 30 Sekunden senden
 
#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77)
#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur
 
Adafruit_BME280 bme;
 
char      CLIENT[8];
IPAddress ip(192, 168, 1, SENSOR_ID);
IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile
const int httpPort = 80;
 
// ============================================================
//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
// ============================================================
#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz)
#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key)
#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)
 
#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!"
#endif
 
// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) -------------
// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN!
#if NETZ_A_MOBIL
  const char* ssid     = "SAS-ARDUINO-mobil";
  const char* password = "<MOBIL-WLAN-PASSWORT>";
  const char* apikey   = "<MOBIL-API-KEY>";
#endif
 
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<TEST-API-KEY>";
#endif
 
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
  const char* ssid     = "SAS-ARDUINO";
  const char* password = "<SCHUL-WLAN-PASSWORT>";
  const char* apikey   = "<LIVE-API-KEY>";
#endif
 
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet (255, 255, 255, 0);
IPAddress dns    (0, 0, 0, 0);
 
void setup() {
  Serial.begin(115200);
  delay(200);
  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
  Serial.printf("\n=== Einfacher BME280-Sensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str());
 
  Wire.begin();  // Standard-Pins: D1=SCL, D2=SDA
  if (!bme.begin(BME280_ADDRESS)) {
    Serial.println("FEHLER: BME280 nicht gefunden! I2C-Verkabelung und Adresse (0x76) prüfen.");
    while (true) delay(100);
  }
  Serial.println("BME280 OK");
 
  WiFi.mode(WIFI_STA);
  WiFi.config(ip, gateway, subnet, dns);
  WiFi.begin(ssid, password);
 
  Serial.print("WLAN verbinden");
  while (WiFi.status() != WL_CONNECTED) {
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str());
}
 
void loop() {
  float hum  = bme.readHumidity();
  float temp = bme.readTemperature();
  float pressure = bme.readPressure() / 100.0F;             // Pa → hPa
  float p0   = bme.seaLevelForAltitude(ALTITUDE, pressure);  // auf Meereshöhe umgerechnet
 
  char path[96];
  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey);
  char body[128];
  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"P_hPa\":%.1f}", temp, hum, p0);
 
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) {
      http.addHeader("Content-Type", "application/x-www-form-urlencoded");
      int code = http.POST(body);
      Serial.printf("[HTTP] code=%d | T=%.2f°C F=%.1f%% P=%.1fhPa\n", code, temp, hum, p0);
      http.end();
    }
  } else {
    Serial.println("[WLAN] nicht verbunden, versuche Reconnect...");
    WiFi.begin(ssid, password);
  }
 
  delay(SENDEINTERVALL_MS);
}

Gesendete Felder: T_C, F_rel, P_hPa – dieselben Feldnamen wie später bei der Klimamessbox. Empfohlenes PHPFina-Interval: 30 Sekunden (passend zu SENDEINTERVALL_MS).

serieller Monitor

13:14:53.275 -> [HTTP] code=200 | T=25.00°C F=49.4% P=1015.0hPa
13:15:23.311 -> [HTTP] code=200 | T=24.97°C F=48.7% P=1014.9hPa
13:15:53.373 -> [HTTP] code=200 | T=24.93°C F=48.5% P=1014.9hPa
13:16:23.428 -> [HTTP] code=200 | T=24.91°C F=48.4% P=1014.9hPa
13:16:53.451 -> [HTTP] code=200 | T=24.90°C F=48.5% P=1015.0hPa

Nächster Schritt: EMONCMS anpassen

Klimamessbox (Außenbereich)

(folgt im nächsten Schritt)


Raummessbox (Innenbereich)

(folgt im nächsten Schritt)

QR-Code
QR-Code www-messdatenerfassung-sas:sensor_code (erstellt für aktuelle Seite)