====== Programmcode der Sensor-Boxen ======
^ Seite ^ Inhalt | |
| [[www-messdatenerfassung-sas:startseite|Startseite]] | Übersicht | |
| [[www-messdatenerfassung-sas:klimastation|Hauptanleitung]] | Gesamtüberblick, Begriffsklärung, Teil 1–3 (FRITZ!Box, Raspberry Pi/EmonCMS, Sensor-Boxen), Ein-/Ausschalten, Zugänge-Checkliste, SENSOR_ID-Übersicht | |
| [[www-messdatenerfassung-sas:sensoren_einbinden|Sensoren in EmonCMS einbinden]] | Wiederkehrender Ablauf für jede neue Sensor-Box: API-Key, Inputs, Feeds, Dashboard | |
| [[www-messdatenerfassung-sas:sensor_code|Programmcode der Sensor-Boxen]] | **Vollständiger Arduino-Code: Testsensor, einfacher BME280-Sensor, Klimamessbox, Raummessbox** | |
| [[www-messdatenerfassung-sas:raspi_rtc_hw111|Raspberry Pi um Echtzeituhr (RTC) erweitern – HW-111]] | Nachrüstung eines DS1307-RTC-Moduls, Shutdown-Taster | |
| [[Platinenschaltung|Schaltung]] | Platine | |
| [[3D-Gehäuse|Gehäuse]] | 3D-Druck oder LASER-Cutter | |
| [[Unterrichtseinsatz|Schule]] | Einsatz im Unterricht | |
| [[Sonstiges|Sonstiges]] | dies und das.... | |
Diese Seite enthält den vollständigen Arduino-Code aller Sensor-Boxen, ausgelagert aus der [[
klimastation|Hauptanleitung]] und der [[sensoren_einbinden|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
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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|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
#include
#include
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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 // WLAN-Funktionen für den ESP8266
#include // HTTP-Anfragen (um Daten zu senden)
#include // TCP-Client für HTTP
#include // I²C-Kommunikation (für BME280)
#include // BME280 Sensor-Bibliothek
#include // 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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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
#include
#include
#include "DHT.h"
#include
#include
#include
#include
/**************************************** 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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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 [[klimastation|Hauptanleitung]] und der [[sensoren_einbinden|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
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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|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
#include
#include
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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 // WLAN-Funktionen für den ESP8266
#include // HTTP-Anfragen (um Daten zu senden)
#include // TCP-Client für HTTP
#include // I²C-Kommunikation (für BME280)
#include // BME280 Sensor-Bibliothek
#include // 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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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
#include
#include
#include "DHT.h"
#include
#include
#include
#include
/**************************************** 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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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 [[klimastation|Hauptanleitung]] und der [[sensoren_einbinden|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
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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|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
#include
#include
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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 [[klimastation|Hauptanleitung]] und der [[sensoren_einbinden|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
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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|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
#include
#include
#include
#include
#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 = "";
const char* apikey = "";
#endif
// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb -------------------------
#if NETZ_B_TEST
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#endif
// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb --------------------------
#if NETZ_C_LIVE
const char* ssid = "SAS-ARDUINO";
const char* password = "";
const char* apikey = "";
#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
----
[[sensoren_einbinden#schritt_2pruefen_ob_die_box_ankommt|Nächster Schritt: EMONCMS anpassen]]
===== Klimamessbox (Außenbereich) =====
//(folgt im nächsten Schritt)//
----
===== Raummessbox (Innenbereich) =====
//(folgt im nächsten Schritt)//