Dies ist eine alte Version des Dokuments!


Programmcode der Sensor-Boxen

Seite Inhalt
Startseite Übersicht
Hauptanleitung Gesamtüberblick, Begriffsklärung, Teil 1–3 (FRITZ!Box, Raspberry Pi/EmonCMS, Sensor-Boxen), Ein-/Ausschalten, Zugänge-Checkliste, SENSOR_ID-Übersicht
Sensoren in EmonCMS einbinden Wiederkehrender Ablauf für jede neue Sensor-Box: API-Key, Inputs, Feeds, Dashboard
Programmcode der Sensor-Boxen Vollständiger Arduino-Code: Testsensor, einfacher BME280-Sensor, Klimamessbox, Raummessbox
Raspberry Pi um Echtzeituhr (RTC) erweitern – HW-111 Nachrüstung eines DS1307-RTC-Moduls, Shutdown-Taster
Schaltung Platine
Gehäuse 3D-Druck oder LASER-Cutter
Schule Einsatz im Unterricht
Sonstiges dies und das….

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)

Copyright (C) 2025 Biebl/Sankt-Ansgar-Schule Hamburg

"Klimamessstation für Schulen" von Martin Biebl/Sankt-Ansgar-Schule Hamburg,
lizenziert unter CC BY-SA 4.0
(https://creativecommons.org/licenses/by-sa/4.0/)
Diese Programme sind freie Software: Sie können es unter den
Bedingungen der GNU General Public License, wie von der Free
Software Foundation veröffentlicht, weitergeben und/oder
modifizieren, entweder gemäß Version 3 der Lizenz oder (nach
Ihrer Wahl) jeder späteren Version.

Die Veröffentlichung dieser Programme erfolgt in der Hoffnung,
dass es Ihnen von Nutzen sein wird, aber OHNE JEDE GEWÄHRLEISTUNG,
sogar ohne die implizite Gewährleistung der MARKTFÄHIGKEIT oder
EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Details finden Sie in der
GNU General Public License.

Vollständiger Lizenztext: https://www.gnu.org/licenses/gpl-3.0.de.html
QR-Code
QR-Code www-messdatenerfassung-sas:sensor_code (erstellt für aktuelle Seite)