Dies ist eine alte Version des Dokuments!
Inhaltsverzeichnis
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). Das Messintervall wird jetzt AUTOMATISCH an den gewählten Bereich gekoppelt: Bereich A (mobiles Testnetz) = 10 Sekunden für schnelles Feedback beim Entwickeln, Bereich B/C (Schulnetz, Test- und Livebetrieb) = 20 Sekunden für den Produktivbetrieb im Klassenzimmer. Beim Umschalten zwischen den Bereichen ändert sich das Intervall automatisch mit - keine zusätzliche Anpassung nötig. Für den EmonCMS-Feed entsprechend Interval 10 (Testsensor-Feeds) bzw. 20 (Produktiv-Feeds) verwenden - je nachdem, mit welchem Bereich zuletzt gemessen wurde.
/******************************************************************************************** * 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–20 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ß) * - Messintervall koppelt sich automatisch an den Netzwerk-Bereich: * Bereich A (mobil/Test) = 10 s, Bereich B/C (Schulnetz) = 20 s * (EmonCMS PHPFina-Feed-Interval entsprechend wählen) ********************************************************************************************/ #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 = NETZ_A_MOBIL ? 10000 : 20000; // A (mobil/Test) = 10 s, B/C (Schulnetz) = 20 s /*** 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(); }
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

