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). 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
Klimamessbox (Außenbereich)
(folgt im nächsten Schritt)
Raummessbox (Innenbereich)
(folgt im nächsten Schritt)