Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen RevisionVorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
www-messdatenerfassung-sas:sensor_code [2026/09/12 16:06] – [Programmcode der Sensor-Boxen] bieblwww-messdatenerfassung-sas:sensor_code [2026/10/19 20:01] (aktuell) – biebl
Zeile 1: Zeile 1:
 ====== Programmcode der Sensor-Boxen ====== ====== Programmcode der Sensor-Boxen ======
 +
 +  * [[biebl:messsystem-schule-programmcode|INTERN]]: [[raumklima:code|AG-Raumklima]]
 +
 +{{:www-fuer-alle:raumklima-105-box-geschlossen.png?200|}}{{:www-fuer-alle:klimabox-aussen-2025-05.png?200|}}
  
 ^ Seite                                                                                                ^ Inhalt                                                                                                                                                 |   | ^ Seite                                                                                                ^ Inhalt                                                                                                                                                 |   |
Zeile 13: Zeile 17:
  
  
-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.+Diese Seite enthält den vollständigen Arduino-Code aller Sensor-Boxen, ausgelagert aus der [[biebl:anleitungen:klimastation|Hauptanleitung]] und der [[biebl:anleitungen: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) ===== ===== Netzwerk-Auswahlschema (Bereich A/B/C) =====
Zeile 168: Zeile 172:
 </code> </code>
  
-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'').+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 [[biebl:anleitungen:sensoren_einbinden|Sensoren einbinden]]). Empfohlenes PHPFina-Interval für den Testsensor: 30 Sekunden (passend zu ''SENDEINTERVALL_MS'').
  
 ---- ----
Zeile 973: Zeile 977:
 Angepasst gegenüber der ursprünglichen Fassung: Netzwerk-Konfiguration Angepasst gegenüber der ursprünglichen Fassung: Netzwerk-Konfiguration
 auf das Bereich-A/B/C-Schema umgestellt (siehe Erklärung oben auf auf das Bereich-A/B/C-Schema umgestellt (siehe Erklärung oben auf
-dieser Seite). Messintervall bei den ursprünglichen 20 Sekunden +dieser Seite). Das Messintervall wird jetzt AUTOMATISCH an den 
-belassen (gut geeignet für ein Klassenzimmer, in dem auch kurzfristige +gewählten Bereich gekoppelt: Bereich A (mobiles Testnetz) = 10 
-Schwankungen - z. B. CO2-Anstieg beim Lüften - interessant sind). +Sekunden für schnelles Feedback beim Entwickeln, Bereich B/C 
-Für den EmonCMS-Feed entsprechend Interval 20 verwenden.+(Schulnetz, Test- und Livebetrieb) = 20 Sekunden für den 
 +Produktivbetrieb im Klassenzimmer. Beim Umschalten zwischen den 
 +Bereichen ändert sich das Intervall automatisch mit - keine 
 +zusätzliche Anpassung nötig. Für den EmonCMS-Feed entsprechend 
 +Interval 10 (Testsensor-Feeds) bzw. 20 (Produktiv-Feeds) verwenden - 
 +je nachdem, mit welchem Bereich zuletzt gemessen wurde.
 </code> </code>
  
Zeile 986: Zeile 995:
  *   - NUR HIER ÄNDERN: SENSOR_ID, NUM_LEDS, CO2_OFFSET, Netzwerk-Auswahl (Bereich A/B/C)  *   - 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  *   - 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+ *   - Zufällige Startverzögerung (0–20 s) + Jitter je Zyklus (±0,3 s) → entkoppelt Massenstart
  *   - Auto-Neustart, falls:  *   - Auto-Neustart, falls:
  *       • beim Boot 3 min lang kein erfolgreicher Upload  *       • beim Boot 3 min lang kein erfolgreicher Upload
Zeile 993: Zeile 1002:
  *   - BMP180: p0 (Meereshöhe) wird gesendet  *   - BMP180: p0 (Meereshöhe) wird gesendet
  *   - LDR: invertiert (hell → klein, dunkel → groß)  *   - LDR: invertiert (hell → klein, dunkel → groß)
- *   - 30 s Intervall für Langzeit-Monitoring (EmonCMS PHPFina 30 s empfohlen)+ *   - Messintervall koppelt sich automatisch an den Netzwerk-Bereich: 
 + *     Bereich A (mobil/Test) = 10 s, Bereich B/C (Schulnetz) = 20 s 
 + *     (EmonCMS PHPFina-Feed-Interval entsprechend wählen)
  ********************************************************************************************/  ********************************************************************************************/
 #include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
Zeile 1067: Zeile 1078:
  
 /*** 3) Messintervall (Langzeitbetrieb) *****************************************************/ /*** 3) Messintervall (Langzeitbetrieb) *****************************************************/
-const int Schreibpause = 20000;          // 20 s; für 10 s im Test nur hier 10000 setzen+const int Schreibpause = NETZ_A_MOBIL ? 10000 : 20000;  // A (mobil/Test) = 10 s, B/C (Schulnetz) = 20 s
  
 /*** 4) Sensor-Hardware *********************************************************************/ /*** 4) Sensor-Hardware *********************************************************************/
Zeile 1524: Zeile 1535:
 </code> </code>
  
-----------------------------------------------------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.+===== Temperatursensor Innen/Außen (2× DS18B20) =====
  
-===== Netzwerk-Auswahlschema (Bereich A/B/C) =====+Zwei DS18B20-Temperatursensoren an einem gemeinsamen Datenpin (1-Wire-Bus): einer misst die Innentemperatur (''Ti_C''), einer die Außentemperatur (''Ta_C''). Nützlich z. B. für den Vergleich zwischen Raum und Außenluft.
  
-Alle Sketches auf dieser Seite nutzen dasselbe Schema, um zwischen drei Netzwerk-/Betriebsprofilen umzuschalten, ohne jedes Mal Zeilen aus- und einzukommentieren:+**Verkabelung:**
  
-  * **Bereich A – SAS-ARDUINO-mobil:** persönliches Testnetz (z. B. Hotspot zuhause), zum Entwickeln und Testen von Sketches, bevor sie ans Schulnetz gehen +^ Anschluss ^ Verbunden mit ^ Hinweis ^ 
-  * **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) +| DS18B20 VCC (beide) | 3,3V oder 5V | | 
-  * **Bereich C – SAS-ARDUINO, Schul-Livebetrieb:** dasselbe Schulnetz, sendet mit dem LIVE-API-Key (echter Dauerbetrieb)+| DS18B20 GND (beide) | GND | | 
 +| DS18B20 DATA (beide) | D2 (GPIO4) | Beide Sensoren parallel an denselben Pin | 
 +| 4,7 kΩ Widerstand | zwischen DATA und VCC | Pull-Up, nur **ein** Widerstand für beide Sensoren |
  
-Im Kopf jedes Sketches wird **genau eine** der drei Zeilen auf ''true'' gesetzt:+**Sensor-Adressen ermitteln:** Jeder DS18B20 hat eine feste, einmalige 8-Byte-Adresse. Den Sketch zuerst mit den Beispieladressen hochladen und den seriellen Monitor (115200 Baud) öffnen – beim Start werden alle am Bus gefundenen Sensoren mit ihrer Adresse ausgegeben (Zeilen wie ''Sensor #0: 28:04:EE:71:00:00:00:9D → UNBEKANNT''). Diese Adressen anschließend bei ''SENSOR_ADDR_INNEN'' und ''SENSOR_ADDR_AUSSEN'' eintragen (Format: ''28:04:EE:71:00:00:00:9D'' wird zu ''{0x28, 0x04, 0xEE, 0x71, 0x00, 0x00, 0x00, 0x9D}''). Welcher Sensor innen und welcher außen sitzt, lässt sich durch kurzes Erwärmen eines Sensors in der Hand erkennen.
  
-<code cpp> +**Messintervall:** koppelt sich automatisch an den gewählten Netzwerk-Bereich – Bereich A (mobil/Test) = 10 Sekunden, Bereich B/C (Schulnetz) = 20 Sekunden. Der EmonCMS-Feed muss mit dem passenden Interval angelegt werden (10 bzw. 20).
-#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 +**Gesendete Felder:** ''Ti_C'' (Innen) und ''Ta_C'' (Außen), in °C. Der Wert ''-127'' bedeutet „Sensor nicht lesbar“.
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" +
-#endif +
-</code> +
- +
-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). +
- +
-<code> +
-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. +
-</code> +
- +
----- +
- +
-===== 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). +
- +
-<code> +
-Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT +
-für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle +
-in der Hauptanleitung). +
-</code>+
  
 <code cpp> <code cpp>
 /******************************************************************************************** /********************************************************************************************
- *  TESTSENSOR für ESP8266 D1 Mini+ *  D1 mini + 2 × DS18B20 – Innen/Außen Temperatur (statische IP, selbstheilend)
  *  *
- *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard). + *  Einstellen: 
- *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert!+ *   - #define SENSOR_ID 207  → Clientname "207" & IP 192.168.1.207 
 + *   - Netzwerk-Auswahl (Bereich A/B/C) weiter unten: genau EINE Zeile auf true 
 + *   - SENSOR_ADDR_INNEN und SENSOR_ADDR_AUSSEN mit echten Adressen füllen
  *  *
- *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch + *  Messung: 
- *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite).+ *   - beide Temperaturen lesen, Intervall koppelt sich an den Netzwerk-Bereich: 
 + *     Bereich A (mobil/Test) = 10 s, Bereich B/C (Schulnetz) = 20 s 
 + *   - als JSON { "Ti_C": <°C>, "Ta_C": <°C> } an EmonCMS senden 
 + *   - Ti_C = Temperatur Innen (indoor), Ta_C = Temperatur Außen (ambient) 
 + * 
 + *  Verdrahtung: 
 + *   - Beide Sensoren parallel an D2 
 + *   - VCC → 5V (oder 3V3), GND → GND, DATA → D2 
 + *   - 4,7 kΩ Pull-Up zwischen DATA und VCC (nur einer für beide!)
  ********************************************************************************************/  ********************************************************************************************/
 +
 #include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
 #include <ESP8266HTTPClient.h> #include <ESP8266HTTPClient.h>
 #include <WiFiClient.h> #include <WiFiClient.h>
 +#include <OneWire.h>
 +#include <DallasTemperature.h>
  
-#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden! +/*** 1) Identität **************************************************************************/ 
-#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen)+#define SENSOR_ID 207                    // eindeutige Sensor-Nummer → IP .207
  
 +/*** 2) Netzwerk ***************************************************************************/
 +// Aus SENSOR_ID werden Client-Name ("207") & IP 192.168.1.207:
 char      CLIENT[8]; char      CLIENT[8];
 IPAddress ip(192, 168, 1, SENSOR_ID); 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; 
  
-// ============================================================ +IPAddress SERVER_IP(192, 168, 1, 2);     // EmonCMS-Server (gilt für alle drei Netzwerkprofile)
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen +
-// ============================================================ +
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) +
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) +
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) +
- +
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 +
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" +
-#endif +
- +
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- +
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! +
-#if NETZ_A_MOBIL +
-  const char* ssid     = "SAS-ARDUINO-mobil"; +
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<MOBIL-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- +
-#if NETZ_B_TEST +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<TEST-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- +
-#if NETZ_C_LIVE +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<LIVE-API-KEY>"; +
-#endif +
- +
-IPAddress gateway(192, 168, 1, 1); +
-IPAddress subnet (255, 255, 255, 0); +
-IPAddress dns    (0, 0, 0, 0); +
- +
-void setup() { +
-  Serial.begin(115200); +
-  delay(200); +
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); +
-  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str()); +
- +
-  WiFi.mode(WIFI_STA); +
-  WiFi.config(ip, gateway, subnet, dns); +
-  WiFi.begin(ssid, password); +
- +
-  Serial.print("WLAN verbinden"); +
-  while (WiFi.status() != WL_CONNECTED) { +
-    delay(300); +
-    Serial.print("."); +
-  } +
-  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str()); +
-} +
- +
-void loop() { +
-  // ---- Simulierte Werte (KEINE echten Sensoren!) ---- +
-  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C +
-  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 % +
-  static int fakeCo2 = 400; +
-  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm +
- +
-  char path[96]; +
-  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey); +
-  char body[128]; +
-  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2); +
- +
-  if (WiFi.status() == WL_CONNECTED) { +
-    WiFiClient client; +
-    HTTPClient http; +
-    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) { +
-      http.addHeader("Content-Type", "application/x-www-form-urlencoded"); +
-      int code = http.POST(body); +
-      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2); +
-      http.end(); +
-    } +
-  } else { +
-    Serial.println("[WLAN] nicht verbunden, versuche Reconnect..."); +
-    WiFi.begin(ssid, password); +
-  } +
- +
-  delay(SENDEINTERVALL_MS); +
-} +
-</code> +
- +
-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 [[info:anleitungen: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 | +
- +
-<code cpp> +
-/******************************************************************************************** +
- *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280 +
- * +
- *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen +
- *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus. +
- * +
- *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND +
- *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox) +
- ********************************************************************************************/ +
-#include <ESP8266WiFi.h> +
-#include <ESP8266HTTPClient.h> +
-#include <WiFiClient.h> +
-#include <Wire.h> +
-#include <Adafruit_BME280.h> +
- +
-#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen! +
- +
-// ==== TEST-EINSTELLUNG ======================================================= +
-// 10 Sekunden statt der später üblichen 30-120 Sekunden - bewusst kurz gehalten, +
-// damit sich Änderungen beim Testen sofort in EmonCMS zeigen. Für Dauerbetrieb +
-// erhöhen (z. B. auf 300000 wie bei der Klimamessbox), um Server und WLAN nicht +
-// unnötig zu belasten! +
-#define SENDEINTERVALL_MS 10000 +
-// ============================================================================== +
- +
-#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77) +
-#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur +
- +
-Adafruit_BME280 bme; +
- +
-char      CLIENT[8]; +
-IPAddress ip(192, 168, 1, SENSOR_ID); +
-IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile+
 const int httpPort = 80; const int httpPort = 80;
  
Zeile 1738: Zeile 1596:
 //  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen //  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen
 // ============================================================ // ============================================================
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) +#define NETZ_A_MOBIL  true   // SAS-ARDUINO-mobil (persönliches Testnetz) 10 s 
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) +#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) 20 s 
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key)+#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) 20 s
  
 #if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 #if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1
Zeile 1770: Zeile 1628:
 IPAddress gateway(192, 168, 1, 1); IPAddress gateway(192, 168, 1, 1);
 IPAddress subnet (255, 255, 255, 0); IPAddress subnet (255, 255, 255, 0);
-IPAddress dns    (0, 0, 0, 0);+IPAddress dns    (0,   0,   0,   0); 
 +static bool g_lastSendFailed = false;    // für die WLAN-Verbindungsqualität
  
-void setup() { +/*** 3) Intervall **************************************************************************/ 
-  Serial.begin(115200); +// Pause am Ende jedes Zyklus. Messung (ca. 0,8 s) und Senden kommen noch dazu, daher 
-  delay(200); +// 1 s weniger als das gewünschte Gesamtintervall: A = 10 s, B/C = 20 s 
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); +const int SCHREIBPAUSE_MS = NETZ_A_MOBIL ? 9000 : 19000;
-  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 +/*** 4) Sensoren: 2× DS18B20 ***************************************************************/ 
-  if (!bme.begin(BME280_ADDRESS)) { +#define ONE_WIRE_BUS D2                  // DATA-Pin für beide Sensoren
-    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); +OneWire oneWire(ONE_WIRE_BUS); 
-  WiFi.config(ip, gateway, subnet, dns); +DallasTemperature sensors(&oneWire);
-  WiFi.begin(ssid, password);+
  
-  Serial.print("WLAN verbinden"); +// WICHTIG: Hier die echten Sensor-Adressen eintragen! 
-  while (WiFi.status() != WL_CONNECTED) { +// Die Adressen erscheinen beim Start im seriellen Monitor (115200 Baud): 
-    delay(300); +// "Sensor #0: 28:04:EE:71:00:00:00:9D → UNBEKANNT (Adresse nicht konfiguriert)" 
-    Serial.print("."); +// Format: {0x28, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}
-  } +
-  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str()); +
-}+
  
-void loop() { +// Sensor INNEN (Ti) 
-  float hum  = bme.readHumidity(); +DeviceAddress SENSOR_ADDR_INNEN  = {0x28, 0x04, 0xEE, 0x71, 0x00, 0x00, 0x00, 0x9D};
-  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]; +// Sensor AUSSEN (Ta) 
-  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey); +DeviceAddress SENSOR_ADDR_AUSSEN = {0x28, 0x6B, 0x31, 0x7A, 0x00, 0x00, 0x00, 0xDF};
-  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) { +// Flags, ob Sensoren gefunden wurden 
-    WiFiClient client; +bool sensorInnenOK  = false; 
-    HTTPClient http; +bool sensorAussenOK = false;
-    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); +/*** 5) Zustände / Selbstheilung ***********************************************************/ 
-} +static bool     g_uploadedOnce  = false; 
-</code> +static uint32_t g_bootMs        = 0; 
- +const  uint32_t BOOT_DEADLINE   = 180000; // 3 min: bis dahin 1 Upload 
-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). +const  uint32_t WIFI_DOWN_LIMIT = 120000; // 2 min offline → Neustart 
- +const  uint8_t  WIFI_BOOT_TRIES = 3;      // WLAN-Startversuche mit Funk-Reset
----- +
- +
-===== 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. +
- +
-<code> +
-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. +
-</code> +
- +
-<code cpp> +
-/******************************************************************************************** +
- *  Außensensor für ESP8266 D1 Mini – Schulversion mit ausführlichen Erklärungen +
- * +
- *  Sensoren: +
- *   - BME280: Temperatur, Luftfeuchtigkeit, Luftdruck (I²C) +
- *   - MH-Z19B: CO2-Gehalt der Luft (UART) +
- *   - SDS011: Feinstaub PM2.5 und PM10 (UART) +
- *   - LDR: Helligkeit (Analog) +
- * +
- *  Features: Retry-Logik mit WLAN-Soft-Reset bei Upload-Fehlern, CO2_OFFSET-Kalibrierung, +
- *            Abschalten der automatischen Baseline-Korrektur (ABC) für den Außeneinsatz. +
- * +
- *  IDE: LOLIN(WEMOS) D1 R2 & mini  (bei Boot-Abstürzen ggf. auf "Generic ESP8266 Module" +
- *       mit Flash Mode: DIO, Flash Frequency: 40MHz, VTables: Flash umstellen) +
- ********************************************************************************************/ +
- +
-// ========== BIBLIOTHEKEN ========== +
-#include <ESP8266WiFi.h>        // WLAN-Funktionen für den ESP8266 +
-#include <ESP8266HTTPClient.h>  // HTTP-Anfragen (um Daten zu senden) +
-#include <WiFiClient.h>         // TCP-Client für HTTP +
-#include <Wire.h>               // I²C-Kommunikation (für BME280) +
-#include <Adafruit_BME280.h>    // BME280 Sensor-Bibliothek +
-#include <SoftwareSerial.h>     // Zusätzliche serielle Schnittstellen +
- +
-// ========== KONFIGURATION (HIER NUR DIESE ZEILEN ÄNDERN!) ========== +
-// Jeder Sensor bekommt eine eindeutige Nummer. Diese Nummer bestimmt: +
-// - Den Namen des Sensors (z.B. "134") +
-// - Die IP-Adresse im Netzwerk (z.B. 192.168.1.134) +
- +
-#define SENSOR_ID 134            // <<< NUR DIESE ZAHL ÄNDERN (z.B. 101, 102, 133, ...) +
- +
-// CO2-Kalibrierungs-Offset (wie bei der Raumklima-Box): wird auf den Rohwert addiert. +
-// An echter Frischluft (z.B. früh morgens, wenig Verkehr) prüfen, was der Sensor anzeigt, +
-// und den Offset so wählen, dass ca. 420-440 ppm (aktueller globaler Referenzwert) erreicht werden. +
-#define CO2_OFFSET 40 +
- +
-// Gewünschtes GESAMT-Intervall zwischen zwei Sendungen an EmonCMS, in Millisekunden. +
-// <<< NUR DIESEN WERT ÄNDERN, um den Messtakt anzupassen (z.B. 60000 = 1 Min., 120000 = 2 Min., +
-//     180000 = 3 Min., 360000 = 6 Min.). Der SDS011-Aufwachvorgang braucht davon fest ca. 10 s, +
-//     der Rest wird als Wartezeit am Ende jedes Zyklus eingehalten (siehe Schreibpause unten). +
-//     Hinweis: Das EmonCMS-Feed-Intervall (PHPFina) sollte auf denselben Wert eingestellt werden, +
-//     sonst entstehen unnötige Interpolationslücken in den Graphen. +
-#define MESSINTERVALL_MS 300000           // aktuell: alle 5 Minuten im EMONCMS entsprechend anpassen! +
- +
-// ========== DEBUG-MODUS ========== +
-#define DEBUG true               // <<< Debug-Ausgaben EIN (true) oder AUS (false) +
- +
-// ========== DEBUG-MAKROS ========== +
-#if DEBUG +
-  #define DBGPRINT(x)   Serial.print(x) +
-  #define DBGPRINTLN(x) Serial.println(x) +
-  #define DBGF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) +
-#else +
-  #define DBGPRINT(x) +
-  #define DBGPRINTLN(x) +
-  #define DBGF(fmt, ...) +
-#endif +
- +
-// ========== NETZWERK-KONFIGURATION ========== +
-char CLIENT[8];                                 // Speicher für den Sensor-Namen (wird später gefüllt) +
-IPAddress ip(192, 168, 1, SENSOR_ID);           // Feste IP-Adresse (wird aus SENSOR_ID gebildet) +
- +
-IPAddress SERVER_IP(192, 168, 1, 2);            // EmonCMS-Server (gilt für alle drei Netzwerkprofile) +
-const int httpPort = 80;                        // HTTP-Port (Standard) +
- +
-// ============================================================ +
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen +
-// ============================================================ +
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) +
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) +
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) +
- +
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 +
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" +
-#endif +
- +
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- +
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! +
-#if NETZ_A_MOBIL +
-  const char* ssid     = "SAS-ARDUINO-mobil"; +
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<MOBIL-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- +
-#if NETZ_B_TEST +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<TEST-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- +
-#if NETZ_C_LIVE +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<LIVE-API-KEY>"; +
-#endif +
- +
-// Standard-Gateway (Router) und Subnetzmaske +
-IPAddress gateway(192, 168, 1, 1); +
-IPAddress subnet (255, 255, 255, 0); +
-IPAddress dns    (0, 0, 0, 0);                  // DNS nicht verwendet (0.0.0.0 = deaktiviert) +
- +
-// ========== SENSOR-HARDWARE ========== +
- +
-// === BME280 (Temperatur, Luftfeuchtigkeit, Luftdruck) === +
-// Der BME280 wird über I²C angesprochen (Standard-Pins D1=SCL, D2=SDA) +
-Adafruit_BME280 bme;                            // BME280-Objekt erstellen +
-#define BME280_ADDRESS 0x76                     // I²C-Adresse des BME280 (manchmal 0x77) +
-#define ALTITUDE 42.0                           // Höhe über Meeresspiegel in Metern (für Luftdruck-Korrektur) +
- +
-// === MH-Z19B CO2-Sensor - an 5V angeschlossen und mit Spannungsteiler zur Datenübertragung an den D1-mini === +
-// RX = D6 (GPIO12) mit Spannungsteiler 5V→3.3V (1kΩ + 2kΩ) +
-// TX = D5 (GPIO14) +
-SoftwareSerial mhzSerial(12, 14);               // RX-Pin, TX-Pin +
- +
-// Kommando zum Auslesen des CO2-Wertes (wird zum Sensor gesendet) +
-byte cmdReadCO2[] = {0xFF, 0x01, 0x86, 0, 0, 0, 0, 0, 0x79}; +
- +
-// Kommando zum Abschalten der automatischen Baseline-Korrektur (ABC). +
-// ABC geht davon aus, dass der niedrigste 24h-Wert ~400 ppm (Frischluft) entspricht. +
-// Das passt für Innenraum-Sensoren, aber nicht für einen dauerhaft im Freien montierten +
-// Sensor in der Stadt (Hintergrundwert schwankt/ist oft dauerhaft erhöht) → würde den +
-// Sensor über die Zeit falsch kalibrieren. Wird einmalig beim Boot gesendet. +
-byte cmdABCOff[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}; +
- +
-// === SDS011 Feinstaub-Sensor - an 5V angeschlossen aber ohne Spannungsteiler zur Datenübertragung an den D1-mini === +
-// RX = D7 (GPIO13) OHNE Spannungsteiler +
-// TX = D8 (GPIO15) +
-SoftwareSerial sdsSerial(13, 15); +
- +
-// SDS011 Sleep-Kommandos (um den Sensor ein-/auszuschalten) +
-byte sds011_sleep[]  = {0xAA, 0xB4, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x05, 0xAB}; +
-byte sds011_wakeup[] = {0xAA, 0xB4, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x06, 0xAB}; +
- +
-// === LDR (Lichtsensor) === +
-#define LDR_PIN A0 +
-const int LDR_INVERTED_OFFSET = 450;            // Offset für Invertierung (hell→klein, dunkel→groß) +
- +
-// ========== SYSTEM-PARAMETER ========== +
-const long schreibpauseRoh = (long)MESSINTERVALL_MS - 10000; +
-const int Schreibpause = (int)(schreibpauseRoh > 2000 ? schreibpauseRoh : 2000); +
-const uint32_t BOOT_UPLOAD_DEADLINE_MS = 180000; // 3 Minuten: Wenn in dieser Zeit kein Upload klappt → Neustart +
-const uint32_t WIFI_DOWN_REBOOT_MS = 120000;   // 2 Minuten: Wenn WLAN so lange weg ist → Neustart +
-const uint8_t WIFI_BOOT_TRIES = 3;              // Anzahl der WLAN-Verbindungsversuche beim Start +
- +
-const float SENSOR_ERROR_VALUE = -999.0f; +
-const int CO2_ERROR_VALUE = -1; +
- +
-// ========== GLOBALE VARIABLEN ========== +
-static bool g_firstCo2Init = true; +
-static bool g_uploadedOnce = false; +
-static bool g_lastSendFailed = false; +
-static uint32_t g_bootMs = 0;+
 static uint32_t g_wifiDownSince = 0; static uint32_t g_wifiDownSince = 0;
-static uint32_t g_lastUploadMs = 0; 
  
-// ========== FUNKTIONS-PROTOTYPEN ==========+/*** 6) Prototypen *************************************************************************/
 bool ensureWifi(uint32_t timeoutMs = 15000); bool ensureWifi(uint32_t timeoutMs = 15000);
 bool wifiBootConnect(); bool wifiBootConnect();
 bool netSoftReset(uint32_t connectTimeoutMs = 15000); bool netSoftReset(uint32_t connectTimeoutMs = 15000);
 bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50); bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50);
-bool readMHZ19B_Safe(int &ppm, int &tempC); +void sendData(float tempInnen, float tempAussen); 
-bool readSDS011(float &pm25, float &pm10); +void initSensors();
-void setSDS011Sleep(bool sleep); +
-float readLDR(); +
-void sendData(float feuchtigkeit, float temp, float druck, float co2, float pm25, float pm10, float ldr);+
  
-// ========================================== +/*** 7) SETUP ******************************************************************************/
-// ========== SETUP (WIRD 1x AUSGEFÜHRT) ==== +
-// ==========================================+
 void setup() { void setup() {
-  if (DEBUG) { +  Serial.begin(115200); 
-    Serial.begin(115200); +  delay(150);
-    delay(200); +
-  } +
- +
-  DBGPRINTLN("\n========================================"); +
-  DBGPRINTLN("===  Außensensor ESP8266 D1 Mini    ==="); +
-  DBGPRINTLN("========================================\n"); +
   snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);   snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID);
-  DBGF("Sensor-Name: %s\n", CLIENT); +  Serial.printf("\n=== Boot: 2x DS18B20 Innen/Außen ===\nSensor-ID=%s  IP=%s\n", 
-  DBGF("IP-Adresse:  %s\n", ip.toString().c_str()); +                CLIENT, ip.toString().c_str());
-  DBGPRINTLN();+
  
-  Wire.begin();                 // Standard-Pins: D1=SCL, D2=SDA +  // WLAN-Start: 3 saubere Versuche mit Funk-Reset & Backoff 
- +  if (!wifiBootConnect()) { 
-  DBGPRINT("BME280 wird initialisiert... "); +    Serial.println("[BOOT] WLAN nicht erreichbar → Neustart"); 
-  if (!bme.begin(BME280_ADDRESS)) { +    delay(800); 
-    DBGPRINTLN("FEHLER!"); +    ESP.restart();
-    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, +  // Beide DS18B20 Sensoren initialisieren 
-                  Adafruit_BME280::SAMPLING_X1, +  initSensors();
-                  Adafruit_BME280::SAMPLING_X1, +
-                  Adafruit_BME280::SAMPLING_X1, +
-                  Adafruit_BME280::FILTER_OFF);+
  
-  DBGPRINTLN("MH-Z19B wird initialisiert..."); +  delay(250);
-  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(); +
-  }+
  
 +  // Startverzögerung (0..Schreibpause) entkoppelt Massenstart bei vielen Geräten
   randomSeed(ESP.getChipId());   randomSeed(ESP.getChipId());
-  int startDelay = random(0, Schreibpause); +  delay(random(0, SCHREIBPAUSE_MS));
-  DBGF("\nStartverzögerung: %d ms (zufällig gewählt)\n", startDelay); +
-  delay(startDelay);+
  
-  delay(3000); 
   g_bootMs = millis();   g_bootMs = millis();
- 
-  DBGPRINTLN("\n=== Setup abgeschlossen, Messbetrieb startet ===\n"); 
 } }
  
-// ========================================== +/*** 8) LOOP ********************************************************************************/
-// ========== LOOP (WIRD DAUERHAFT AUSGEFÜHRT) ==== +
-// ==========================================+
 void loop() { void loop() {
-  DBGPRINTLN("--- Neuer Messzyklus ---");+  // Beide Sensoren gleichzeitig messen lassen 
 +  sensors.requestTemperatures(); 
 +  delay(800);  // Warten auf 12-bit Konvertierung (~750ms)
  
-  DBGPRINT("BME280 wird ausgelesen... "); +  // Temperatur INNEN auslesen 
-  bme.takeForcedMeasurement(); +  float tempInnen = -127.0;  // Standard-Fehlerwert 
- +  if (sensorInnenOK) { 
-  float hum = bme.readHumidity(); +    tempInnen = sensors.getTempC(SENSOR_ADDR_INNEN); 
-  float temp = bme.readTemperature(); +    if (tempInnen == DEVICE_DISCONNECTED_C || isnan(tempInnen) || tempInnen == -127.0) { 
-  float pressure = bme.readPressure() / 100.0F; +      tempInnen = -127.0;  // "ungültig" 
-  float p0 = bme.seaLevelForAltitude(ALTITUDE, pressure); +      Serial.println("[SENSOR] Innen-Sensor: Lesefehler!");
- +
-  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; +  // Temperatur AUSSEN auslesen 
-  if (ppm_val > 0) { +  float tempAussen = -127.0;  // Standard-Fehlerwert 
-    ppm_uart = ppm_val + CO2_OFFSET; +  if (sensorAussenOK) { 
-    DBGPRINTLN("OK"); +    tempAussen = sensors.getTempC(SENSOR_ADDR_AUSSEN); 
-    DBGF("  CO2-Gehalt: %d ppm (Rohwert %d + Offset %d)\n", ppm_uart, ppm_val, CO2_OFFSET); +    if (tempAussen == DEVICE_DISCONNECTED_C || isnan(tempAussen) || tempAussen == -127.0) { 
-  } else { +      tempAussen = -127.0;  // "ungültig" 
-    DBGPRINTLN("FEHLER (verwende letzten Wert)"); +      Serial.println("[SENSOR] Außen-Sensor: Lesefehler!"); 
-    DBGF("  CO2-Gehalt: %d ppm (alter Wert)\n", ppm_uart);+    }
   }   }
  
-  DBGPRINT("SDS011 (Feinstaub) wird ausgelesen... "); +  Serial.printf("[MESSUNG] Ti=%.1f°C  Ta=%.1f°C\n", tempInnen, tempAussen);
-  setSDS011Sleep(false); +
-  delay(10000);+
  
-  float pm25 = SENSOR_ERROR_VALUE; +  // WLAN pflegen & ggf. reconnecten
-  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);   ensureWifi(10000);
  
-  DBGPRINTLN("Daten werden gesendet..."); +  // Beide Werte an EmonCMS senden 
-  sendData(hum, temp, p0, (float)ppm_uart, pm25, pm10, ldrWert);+  sendData(tempInnen, tempAussen);
  
 +  // Selbstheilung / Watchdog
   uint32_t now = millis();   uint32_t now = millis();
- +  if (!g_uploadedOnce && (now - g_bootMs > BOOT_DEADLINE)) { 
-  if (!g_uploadedOnce && (now - g_bootMs > BOOT_UPLOAD_DEADLINE_MS)) { +    Serial.println("[WD] Kein Upload binnen 3 min → Reboot"); 
-    DBGPRINTLN("\n[WATCHDOG] Kein erfolgreicher Upload binnen 3 Minuten!"); +    delay(300); ESP.restart();
-    DBGPRINTLN("[WATCHDOG] ESP8266 startet neu..."); +
-    delay(500); +
-    ESP.restart();+
   }   }
- +  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_LIMIT)) { 
-  if (g_wifiDownSince && (now - g_wifiDownSince > WIFI_DOWN_REBOOT_MS)) { +    Serial.println("[WD] WLAN > 120 s weg → Reboot"); 
-    DBGPRINTLN("\n[WATCHDOG] WLAN länger als 2 Minuten nicht erreichbar!"); +    delay(300); ESP.restart();
-    DBGPRINTLN("[WATCHDOG] ESP8266 startet neu..."); +
-    delay(500); +
-    ESP.restart();+
   }   }
  
-  int jitter = random(-300, 300); +  // Intervall + kleiner Jitter (±0,3 s) für verteilte Last auf dem Server 
-  int actualDelay = Schreibpause + jitter; +  delay(SCHREIBPAUSE_MS + random(-300, 300));
-  DBGF("\nPause: %d ms (mit Jitter)\n", actualDelay); +
-  DBGPRINTLN("=====================================\n"); +
- +
-  delay(actualDelay);+
 } }
  
-// ========================================== +/*** 9) Sensor-Initialisierung *************************************************************/ 
-// ========== WLAN-FUNKTIONEN ========== +void initSensors() { 
-// ========================================== +  sensors.begin(); 
-bool wifiBootConnect() { +  sensors.setWaitForConversion(true);
-  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) { +  int count = sensors.getDeviceCount(); 
-    DBGF("[BOOT] WLAN-Verbindungsversuch %u/%u\n", attempt, WIFI_BOOT_TRIES);+  Serial.printf("Gefundene Sensoren am Bus: %d\n", count);
  
-    WiFi.disconnect(true); +  if (count < 2) { 
-    delay(200); +    Serial.println("[WARNUNG] Weniger als 2 Sensoren gefunden!");
-    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; +  // Alle gefundenen Sensoren durchgehen und zuordnen 
-}+  DeviceAddress tempAddr; 
 +  for (int i = 0; i < count; i++) { 
 +    if (sensors.getAddress(tempAddr, i)) {
  
-bool ensureWifi(uint32_t timeoutMs) { +      // Adresse ausgeben 
-  if (WiFi.status() == WL_CONNECTED) { +      Serial.printf("Sensor #%d: ", i); 
-    return true; +      for (uint8_t j = 0; j < 8; j++) { 
-  } +        if (tempAddr[j] < 16) Serial.print("0"); 
- +        Serial.print(tempAddr[j], HEX); 
-  DBGPRINT("[WIFI] Warte auf Verbindung"); +        if (j < 7) Serial.print(":");
-  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;+      // Prüfen, ob es der Innen- oder Außen-Sensor ist (Vergleich der kompletten Adresse) 
 +      if (memcmp(tempAddr, SENSOR_ADDR_INNEN, 8) == 0) { 
 +        Serial.println(" → INNEN (Ti)"); 
 +        sensorInnenOK = true; 
 +        sensors.setResolution(tempAddr, 12);  // 12-bit Auflösung
  
-      if (idx == 10) { +      } else if (memcmp(tempAddr, SENSOR_ADDR_AUSSEN, 8) == 0) { 
-        if (buf[0] == 0xAA && buf[1] == 0xC0 && buf[9] == 0xAB) { +        Serial.println(" → AUSSEN (Ta)"); 
-          pm25 = (buf[2] + 256 * buf[3]) / 10.0f; +        sensorAussenOK = true; 
-          pm10 = (buf[4] + 256 * buf[5]) / 10.0f; +        sensors.setResolution(tempAddr, 12);  // 12-bit Auflösung
-          return true; +
-        } else { +
-          idx = 0; +
-        } +
-      } +
-    } +
-  }+
  
-  return false; +      } else { 
-} +        Serial.println(" → UNBEKANNT (Adresse nicht konfiguriert)");
- +
-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 …"); +  Serial.println(); 
-  if (netSoftReset(15000)) { +  if (sensorInnenOK && sensorAussenOK) { 
-    code = doPostOnce(4); +    Serial.println("✓ Beide Sensoren (Innen + Außen) gefunden und konfiguriert");
-    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); +
-} +
-</code> +
- +
----- +
- +
-===== 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. +
- +
-<code> +
-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. +
-</code> +
- +
-<code cpp> +
-/******************************************************************************************** +
- *  Raumklima D1 mini – Schul-/Langzeit-Variante (statische IP, selbstheilend) +
- * +
- *  Features: +
- *   - NUR HIER ÄNDERN: SENSOR_ID, NUM_LEDS, CO2_OFFSET, Netzwerk-Auswahl (Bereich A/B/C) +
- *   - Statische IP (kein DHCP), robuster WLAN-Start mit 3 Versuchen + Funk-Reset +
- *   - Zufällige Startverzögerung (0–30 s) + Jitter je Zyklus (±0,3 s) → entkoppelt Massenstart +
- *   - Auto-Neustart, falls: +
- *       • beim Boot 3 min lang kein erfolgreicher Upload +
- *       • WLAN im Betrieb > 2 min am Stück weg +
- *   - MH-Z19B: „sicheres" Auslesen (9 Bytes, Checksumme), sanfter Erststart +
- *   - BMP180: p0 (Meereshöhe) wird gesendet +
- *   - LDR: invertiert (hell → klein, dunkel → groß) +
- *   - 20 s Intervall für Langzeit-Monitoring (EmonCMS PHPFina 20 s empfohlen) +
- ********************************************************************************************/ +
-#include <ESP8266WiFi.h> +
-#include <ESP8266HTTPClient.h> +
-#include <WiFiClient.h> +
-#include "DHT.h" +
-#include <Wire.h> +
-#include <SFE_BMP180.h> +
-#include <Adafruit_NeoPixel.h> +
-#include <SoftwareSerial.h> +
- +
-/****************************************  NUR HIER ÄNDERUNGEN VORNEHMEN (z. B. 111 → 112) ******************************/ +
-// +
-//                                        1) Geräte-Identität +
-#define SENSOR_ID 111                    // eindeutige Sensor-Nummer +
-// +
-//                                        2) Bauart - Länge der LED-Anzeige +
-#define NUM_LEDS  5                      // 5 = Normal, 4 = Gerät mit 4 LEDs +
-// +
-//                                        3) CO2-Kalibrierung +
-#define CO2_OFFSET 40                    // Kalibrierung an Frischluft wird auf 400 ppm gesetzt. +
-// +
-//                                        4) Netzwerk-Auswahl siehe weiter unten (Bereich A/B/C) +
-// +
-/**************************************** AB HIER KEINE ÄNDERUNGEN VORNEHMEN *****************************************************/ +
- +
-// Aus SENSOR_ID werden automatisch Client-Name ("111") & IP 192.168.1.SENSOR_ID erzeugt: +
-char      CLIENT[8];                     // wird im setup() mit "111" etc. gefüllt +
-IPAddress ip(192, 168, 1, SENSOR_ID);    // feste IP aus SENSOR_ID +
- +
-IPAddress SERVER_IP(192, 168, 1, 2);     // EmonCMS-Server (gilt für alle drei Netzwerkprofile) +
-const char* host     = "192.168.1.2";    // EmonCMS-Server (ohne http://) +
-const int   httpPort = 80; +
- +
-// ============================================================ +
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen +
-// ============================================================ +
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) +
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) +
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) +
- +
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 +
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" +
-#endif +
- +
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- +
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! +
-#if NETZ_A_MOBIL +
-  const char* ssid     = "SAS-ARDUINO-mobil"; +
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<MOBIL-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- +
-#if NETZ_B_TEST +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<TEST-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- +
-#if NETZ_C_LIVE +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<LIVE-API-KEY>"; +
-#endif +
- +
-IPAddress gateway(192, 168, 1, 1); +
-IPAddress subnet (255, 255, 255, 0); +
-IPAddress dns    (0,   0,   0,   0); +
-static bool g_lastSendFailed = false;  // für die WLANverbindungsqualität +
- +
- +
-/*** 3) Messintervall (Langzeitbetrieb) *****************************************************/ +
-const int Schreibpause = 20000;          // 20 s; für 10 s im Test nur hier 10000 setzen +
- +
-/*** 4) Sensor-Hardware *********************************************************************/ +
-// DHT22 (Luftfeuchte/Temperatur) +
-#define DHTPIN   13                      // D7 +
-#define DHTTYPE  DHT22 +
-DHT dht(DHTPIN, DHTTYPE); +
- +
-// BMP180 (Luftdruck/Temperatur) +
-#define ALTITUDE 42.0                    // Höhe [m] für p0-Korrektur +
-SFE_BMP180 pressure; +
- +
-// MH-Z19B (CO₂, UART) +
-SoftwareSerial mhzSerial(12, 14);        // RX = D6, TX = D5 +
-byte cmdReadCO2[] = {0xFF,0x01,0x86,0,0,0,0,0,0x79}; +
- +
-// LDR (Helligkeit, analog) +
-#define LDR_PIN A0 +
- +
-// LED-Band (CO₂-Ampel) +
-#define LED_PIN   2                      // D4 +
- +
-Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); +
- +
-/*** 5) Selbstheilungs-Parameter ************************************************************/ +
-const uint32_t BOOT_UPLOAD_DEADLINE_MS = 180000; // 3 min: bis dahin muss 1 Upload geklappt haben +
-const uint32_t WIFI_DOWN_REBOOT_MS     = 120000; // 2 min am Stück offline → Neustart +
-const uint8_t  WIFI_BOOT_TRIES         = 3;      // WLAN-Startversuche mit Funk-Reset +
- +
-/*** 6) Zustände ****************************************************************************/ +
-static bool     g_firstCo2Init    = true; // CO₂-Sensor im ersten Zyklus besonders behandeln +
-static bool     g_uploadedOnce    = false; // wurde seit Boot mindestens 1x erfolgreich gesendet? +
-static uint32_t g_bootMs          = 0;     // Boot-Zeitpunkt +
-static uint32_t g_wifiDownSince   = 0;     // Beginn einer Offline-Phase (0 = online) +
- +
-/*** 7) Prototypen **************************************************************************/ +
-bool ensureWifi(uint32_t timeoutMs = 15000); +
-bool wifiBootConnect(); +
-void LED_Band(int co2); +
-float readLDR(); +
-bool readMHZ19B_Safe(int &ppm, int &tempC); +
-void sendData(float feuchtigkeit, float temp, float druck, float co2, float ldr); +
- +
-/*** 8) SETUP *******************************************************************************/ +
-void setup() { +
-  Serial.begin(115200); +
-  delay(200); +
-  Serial.println("\n=== Boot: Raumklima-Sensor startet ==="); +
- +
-  // CLIENT-Name aus SENSOR_ID erzeugen (z. B. 111 → "111") +
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); +
-  Serial.printf("Sensor-ID = %s, IP = %s\n", CLIENT, ip.toString().c_str()); +
- +
-  // LED-Band initial +
-  strip.begin(); +
-  strip.show(); +
- +
-  // Sensoren starten +
-  dht.begin(); +
-  if (!pressure.begin()) { +
-    Serial.println("FEHLER: BMP180 nicht gefunden! (I2C prüfen: D1=SCL, D2=SDA, GND, 3V3)"); +
-    while (true) delay(10); // „hart stehenbleiben" → Fehler sichtbar +
-  } +
- +
-  // WLAN-Start: 3 saubere Versuche mit Funk-Reset & Backoff +
-  if (!wifiBootConnect()) { +
-    Serial.println("[BOOT] WLAN nicht erreichbar → Neustart"); +
-    delay(800); +
-    ESP.restart(); +
-  } +
- +
-  // CO₂-Sensor starten (UART) +
-  mhzSerial.begin(9600); +
-  Serial.println("MH-Z19B UART bereit"); +
- +
-  // Zufällige Startverzögerung (0..Schreibpause) → entkoppelt Massenstart nach Stromausfall +
-  randomSeed(ESP.getChipId()); +
-  int startDelay = random(0, Schreibpause); +
-  Serial.printf("Initiale Startpause: %d ms\n", startDelay); +
-  delay(startDelay); +
- +
-  // kurze Sensor-Aufwärmzeit +
-  delay(3000); +
- +
-  // Boot-Zeit merken +
-  g_bootMs = millis(); +
-} +
- +
-/*** 9) LOOP ********************************************************************************/ +
-void loop() { +
-  /***** DHT22: Luftfeuchte (°C vom DHT kannst du bei Bedarf zusätzlich nutzen) *****/ +
-  float hum = dht.readHumidity(); +
-  if (isnan(hum)) hum = -1.0; // -1 = „ungültig" → in EmonCMS gut erkennbar +
- +
-  /***** BMP180: Temperatur (T), Stationsdruck (P), p0 (auf Meereshöhe) *****/ +
-  char status; +
-  double T = NAN, P = NAN, p0 = NAN; +
-  status = pressure.startTemperature(); +
-  if (status) { +
-    delay(status); +
-    if (pressure.getTemperature(T)) { +
-      status = pressure.startPressure(3);       // Oversampling = 3 (genauer) +
-      if (status) { +
-        delay(status); +
-        if (pressure.getPressure(P, T)) { +
-          p0 = pressure.sealevel(P, ALTITUDE);  // Korrektur auf Meereshöhe +
-        } +
-      } +
-    } +
-  } +
- +
-  /***** MH-Z19B: sanfter Erststart (max 1,5 s warten), dann normal *****/ +
-  int ppm_val = -1, tco2 = 0; +
-  if (g_firstCo2Init) { +
-    uint32_t t0 = millis(); +
-    while (!readMHZ19B_Safe(ppm_val, tco2) && millis() - t0 < 1500) { +
-      delay(200); // Sensor bekommt Zeit für erstes vollständiges Frame +
-    } +
-    g_firstCo2Init = false;+
   } else {   } else {
-    readMHZ19B_Safe(ppm_val, tco2);+    if (!sensorInnenOK)  Serial.println("✗ FEHLER: Innen-Sensor nicht gefunden!"); 
 +    if (!sensorAussenOK) Serial.println("✗ FEHLER: Außen-Sensor nicht gefunden!"); 
 +    Serial.println("   → Sensor-Adressen im Code prüfen!");
   }   }
-  static int ppm_uart = 0;                           // letzter gültiger CO₂-Wert (ppm) +  Serial.println();
-  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 ***********************************************************************/ /*** 10) WLAN-Helfer ***********************************************************************/
Zeile 2754: Zeile 1806:
 // Mehrstufiger WLAN-Boot: Funk hart zurücksetzen, 3 Versuche mit Backoff // Mehrstufiger WLAN-Boot: Funk hart zurücksetzen, 3 Versuche mit Backoff
 bool wifiBootConnect() { bool wifiBootConnect() {
-  WiFi.persistent(false);            // keine alten Credentials aus Flash verwenden+  WiFi.persistent(false);
   WiFi.mode(WIFI_STA);   WiFi.mode(WIFI_STA);
   WiFi.setAutoConnect(true);   WiFi.setAutoConnect(true);
   WiFi.setAutoReconnect(true);   WiFi.setAutoReconnect(true);
-  WiFi.setOutputPower(20.5);         // maximale Sendeleistung +  WiFi.setOutputPower(20.5); 
-  WiFi.setSleepMode(WIFI_NONE_SLEEP);// kein Modem-Sleep während Boot+  WiFi.setSleepMode(WIFI_NONE_SLEEP);
  
   for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {   for (uint8_t attempt = 1; attempt <= WIFI_BOOT_TRIES; ++attempt) {
     Serial.printf("[BOOT] WLAN-Versuch %u/%u\n", attempt, WIFI_BOOT_TRIES);     Serial.printf("[BOOT] WLAN-Versuch %u/%u\n", attempt, WIFI_BOOT_TRIES);
  
-    // Funk & Assoc/ARP-Zustand hart zurücksetzen+    // Funkzustand hart resetten
     WiFi.disconnect(true);     WiFi.disconnect(true);
     delay(200);     delay(200);
Zeile 2775: Zeile 1827:
  
     if (ensureWifi(20000)) {     if (ensureWifi(20000)) {
-      Serial.printf("[WIFI] OK: %s  RSSI:%d dBm\n", +      WiFiClient poke; poke.connect(gateway, 80); poke.stop();  // ARP im Router "anticken"
-                    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;       return true;
     }     }
Zeile 2796: Zeile 1845:
   uint32_t start = millis();   uint32_t start = millis();
   while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {   while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
-    delay(250); +    delay(250); Serial.print(".");
-    Serial.print(".");+
   }   }
   if (WiFi.status() == WL_CONNECTED) {   if (WiFi.status() == WL_CONNECTED) {
-    Serial.printf("\n[WIFI] OK: %s\n", WiFi.localIP().toString().c_str());+    Serial.printf("\n[WIFI] OK: %s (RSSI:%d)\n", 
 +                  WiFi.localIP().toString().c_str(), WiFi.RSSI());
     g_wifiDownSince = 0;     g_wifiDownSince = 0;
     return true;     return true;
   }   }
   Serial.println("\n[WIFI] Timeout");   Serial.println("\n[WIFI] Timeout");
-  if (!g_wifiDownSince) g_wifiDownSince = millis(); // Offline-Phase starten+  if (!g_wifiDownSince) g_wifiDownSince = millis();
   return false;   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 // Sanfte Netzwerk-Kur: Radio kurz aus/an + frische Assoziation mit statischer IP
-bool netSoftReset(uint32_t connectTimeoutMs = 15000) {+bool netSoftReset(uint32_t connectTimeoutMs) {
   Serial.println("[NET] Soft-Reset: disconnect + radio cycle");   Serial.println("[NET] Soft-Reset: disconnect + radio cycle");
-  WiFi.disconnect(true);            // Assoc/ARP löschen+  WiFi.disconnect(true);
   delay(200);   delay(200);
-  WiFi.forceSleepBegin();           // Radio aus+  WiFi.forceSleepBegin();
   delay(200);   delay(200);
-  WiFi.forceSleepWake();            // Radio an+  WiFi.forceSleepWake();
   delay(200);   delay(200);
-  WiFi.config(ip, gateway, subnet, dns);   // statische IP erneut setzen+  WiFi.config(ip, gateway, subnet, dns);
   WiFi.begin(ssid, password);   WiFi.begin(ssid, password);
   return ensureWifi(connectTimeoutMs);   return ensureWifi(connectTimeoutMs);
Zeile 2865: Zeile 1873:
  
 // Kurzer TCP-Preflight: baut eine nackte Verbindung zum Server auf und schließt sie wieder. // 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, uint16_t graceMs) {
-bool tcpPreflight(uint16_t port = 80, uint16_t graceMs = 50) {+
   WiFiClient poke;   WiFiClient poke;
   if (!poke.connect(SERVER_IP, port)) return false;   if (!poke.connect(SERVER_IP, port)) return false;
Zeile 2874: Zeile 1881:
 } }
  
-/****************************************************** SENDEN der DATEN **********************************************************************/ +/*** 11) Upload an EmonCMS *****************************************************************/ 
-void sendData(float feuchtigkeit, float temp, float druck, float co2, float ldr) {+void sendData(float tempInnen, float tempAussen) {
   if (WiFi.status() != WL_CONNECTED) {   if (WiFi.status() != WL_CONNECTED) {
     Serial.println("[HTTP] offline – Upload übersprungen");     Serial.println("[HTTP] offline – Upload übersprungen");
Zeile 2882: Zeile 1889:
   }   }
  
-  // Pfad/Query, weil wir die 5-Parameter-Variante mit Host-String nutzen+  // POST zu /emoncms/input/post.json?node=...&apikey=...
   String path  = "/emoncms/input/post.json";   String path  = "/emoncms/input/post.json";
   String query = "?node=" + String(CLIENT) + "&apikey=" + String(apikey);   String query = "?node=" + String(CLIENT) + "&apikey=" + String(apikey);
  
-  String body = "json={\"F_rel\":" + String(feuchtigkeit,1) + +  // JSON mit BEIDEN Temperaturen (eine Nachkommastelle): 
-                ",\"T_C\":"        + String(temp,2) + +  // Ti_C = Temperatur Innen, Ta_C = Temperatur Außen 
-                ",\"P_hPa\":"      + String(druck,1) + +  String body = "json={\"Ti_C\":" + String(tempInnen, 1) + 
-                ",\"CO2_ppm\":"    + String(co2,0) + +                ",\"Ta_C\":"      + String(tempAussen, 1) + "}";
-                ",\"H_bit\":"      + String(ldr,0) + "}";+
  
   // Preflight nach Fehlschlag   // Preflight nach Fehlschlag
Zeile 2898: Zeile 1904:
   }   }
  
-  // Host-String aus IPAddress erzeugen (für http.begin(...)) 
   String hostStr = SERVER_IP.toString();   String hostStr = SERVER_IP.toString();
  
-  auto doPostOnce = [&](int attempt)->int {+  auto doPostOnce = [&](int attempt) -> int {
     if (WiFi.status() != WL_CONNECTED) {     if (WiFi.status() != WL_CONNECTED) {
       ensureWifi(8000);       ensureWifi(8000);
Zeile 2934: Zeile 1939:
     if (code == 200) {     if (code == 200) {
       if (payload.length()) { Serial.print("[HTTP] resp: "); Serial.println(payload); }       if (payload.length()) { Serial.print("[HTTP] resp: "); Serial.println(payload); }
-      Serial.printf("Sent: F=%.1f T=%.2f P0=%.1f CO2=%.0f LDR=%.0f\n", +      Serial.printf("Sent: Ti=%.1f°C Ta=%.1f°C\n", tempInnen, tempAussen);
-                    feuchtigkeit, temp, druck, co2, ldr);+
       g_uploadedOnce   = true;       g_uploadedOnce   = true;
       g_lastSendFailed = false;  // Erfolg → Flag löschen       g_lastSendFailed = false;  // Erfolg → Flag löschen
Zeile 2963: Zeile 1967:
   }   }
  
-  g_lastSendFailed = true; // alles fehlgeschlagen +  g_lastSendFailed = true;  // alles fehlgeschlagen 
-  Serial.printf("[HTTP] FAILED after retries | Node=%s | F=%.1f T=%.2f P0=%.1f CO2=%.0f LDR=%.0f\n", +  Serial.printf("[HTTP] FAILED after retries | Node=%s | Ti=%.1f Ta=%.1f\n", 
-                CLIENT, feuchtigkeit, temp, druck, co2, ldr);+                CLIENT, tempInnen, tempAussen);
 } }
  
-/********************************************************** 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(); 
-} 
-</code> 
- 
----------------------------------------------------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: 
- 
-<code cpp> 
-#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 
-</code> 
- 
-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). 
- 
-<code> 
-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. 
-</code> 
- 
----- 
- 
-===== 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). 
- 
-<code> 
-Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT 
-für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle 
-in der Hauptanleitung). 
-</code> 
- 
-<code cpp> 
 /******************************************************************************************** /********************************************************************************************
- *  TESTSENSOR für ESP8266 D1 Mini+ *  HINWEISE 
 + ******************************************************************************************** 
 + *  SENSOR-ADRESSEN ERMITTELN: 
 + *  1. Sketch mit den Beispieladressen hochladen 
 + *  2. Serieller Monitor (115200 Baud) zeigt beim Start alle Adressen am Bus, z. B. 
 + *       Sensor #0: 28:04:EE:71:00:00:00:9D → UNBEKANNT (Adresse nicht konfiguriert) 
 + *  3. Eintragen als: 
 + *       DeviceAddress SENSOR_ADDR_INNEN  = {0x28, 0x04, 0xEE, 0x71, 0x00, 0x00, 0x00, 0x9D};
  *  *
- *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard). + *  EMONCMS: Es erscheinen die Inputs Ti_C und Ta_C. Für jeden einen Feed anlegen 
- *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert!+ *  (PHPFina, Interval 10 s bei Bereich A bzw. 20 s bei Bereich B/C).
  *  *
- *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch + *  FEHLERSUCHE: 
- *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite).+ *  "UNBEKANNT (Adresse nicht konfiguriert)": Adressen im Code stimmen nicht mit den echten überein 
 + *  "Lesefehler!": Wackelkontakt, Sensor während der Messung getrennt oder defekt 
 + *  "Weniger als 2 Sensoren gefunden": Verkabelung und Pull-Up-Widerstand prüfen, 
 + *                                     beide Sensoren angeschlossen?
  ********************************************************************************************/  ********************************************************************************************/
-#include <ESP8266WiFi.h> 
-#include <ESP8266HTTPClient.h> 
-#include <WiFiClient.h> 
- 
-#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden! 
-#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen) 
- 
-char      CLIENT[8]; 
-IPAddress ip(192, 168, 1, SENSOR_ID); 
-IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile 
-const int httpPort = 80; 
- 
-// ============================================================ 
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen 
-// ============================================================ 
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) 
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) 
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) 
- 
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" 
-#endif 
- 
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- 
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! 
-#if NETZ_A_MOBIL 
-  const char* ssid     = "SAS-ARDUINO-mobil"; 
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; 
-  const char* apikey   = "<MOBIL-API-KEY>"; 
-#endif 
- 
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- 
-#if NETZ_B_TEST 
-  const char* ssid     = "SAS-ARDUINO"; 
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; 
-  const char* apikey   = "<TEST-API-KEY>"; 
-#endif 
- 
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- 
-#if NETZ_C_LIVE 
-  const char* ssid     = "SAS-ARDUINO"; 
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; 
-  const char* apikey   = "<LIVE-API-KEY>"; 
-#endif 
- 
-IPAddress gateway(192, 168, 1, 1); 
-IPAddress subnet (255, 255, 255, 0); 
-IPAddress dns    (0, 0, 0, 0); 
- 
-void setup() { 
-  Serial.begin(115200); 
-  delay(200); 
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); 
-  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str()); 
- 
-  WiFi.mode(WIFI_STA); 
-  WiFi.config(ip, gateway, subnet, dns); 
-  WiFi.begin(ssid, password); 
- 
-  Serial.print("WLAN verbinden"); 
-  while (WiFi.status() != WL_CONNECTED) { 
-    delay(300); 
-    Serial.print("."); 
-  } 
-  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str()); 
-} 
- 
-void loop() { 
-  // ---- Simulierte Werte (KEINE echten Sensoren!) ---- 
-  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C 
-  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 % 
-  static int fakeCo2 = 400; 
-  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm 
- 
-  char path[96]; 
-  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey); 
-  char body[128]; 
-  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2); 
- 
-  if (WiFi.status() == WL_CONNECTED) { 
-    WiFiClient client; 
-    HTTPClient http; 
-    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) { 
-      http.addHeader("Content-Type", "application/x-www-form-urlencoded"); 
-      int code = http.POST(body); 
-      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2); 
-      http.end(); 
-    } 
-  } else { 
-    Serial.println("[WLAN] nicht verbunden, versuche Reconnect..."); 
-    WiFi.begin(ssid, password); 
-  } 
- 
-  delay(SENDEINTERVALL_MS); 
-} 
 </code> </code>
  
-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 [[info:anleitungen:sensoren_einbinden|Sensoren einbinden]]). Empfohlenes PHPFina-Interval für den Testsensor: 30 Sekunden (passend zu ''SENDEINTERVALL_MS''). +====== Copyright (C) 2025 Biebl/Sankt-Ansgar-Schule Hamburg ======
- +
----- +
- +
-===== 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 | +
- +
-<code cpp> +
-/******************************************************************************************** +
- *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280 +
- * +
- *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen +
- *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus. +
- * +
- *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND +
- *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox) +
- ********************************************************************************************/ +
-#include <ESP8266WiFi.h> +
-#include <ESP8266HTTPClient.h> +
-#include <WiFiClient.h> +
-#include <Wire.h> +
-#include <Adafruit_BME280.h> +
- +
-#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen! +
- +
-// ==== TEST-EINSTELLUNG ======================================================= +
-// 10 Sekunden statt der später üblichen 30-120 Sekunden - bewusst kurz gehalten, +
-// damit sich Änderungen beim Testen sofort in EmonCMS zeigen. Für Dauerbetrieb +
-// erhöhen (z. B. auf 120000 wie bei der Klimamessbox), um Server und WLAN nicht +
-// unnötig zu belasten! +
-#define SENDEINTERVALL_MS 10000 +
-// ============================================================================== +
- +
-#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77) +
-#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur +
- +
-Adafruit_BME280 bme; +
- +
-char      CLIENT[8]; +
-IPAddress ip(192, 168, 1, SENSOR_ID); +
-IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile +
-const int httpPort = 80; +
- +
-// ============================================================ +
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen +
-// ============================================================ +
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) +
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) +
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) +
- +
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 +
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" +
-#endif +
- +
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- +
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! +
-#if NETZ_A_MOBIL +
-  const char* ssid     = "SAS-ARDUINO-mobil"; +
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<MOBIL-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- +
-#if NETZ_B_TEST +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<TEST-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- +
-#if NETZ_C_LIVE +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<LIVE-API-KEY>"; +
-#endif +
- +
-IPAddress gateway(192, 168, 1, 1); +
-IPAddress subnet (255, 255, 255, 0); +
-IPAddress dns    (0, 0, 0, 0); +
- +
-void setup() { +
-  Serial.begin(115200); +
-  delay(200); +
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); +
-  Serial.printf("\n=== Einfacher BME280-Sensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str()); +
- +
-  Wire.begin();  // Standard-Pins: D1=SCL, D2=SDA +
-  if (!bme.begin(BME280_ADDRESS)) { +
-    Serial.println("FEHLER: BME280 nicht gefunden! I2C-Verkabelung und Adresse (0x76) prüfen."); +
-    while (true) delay(100); +
-  } +
-  Serial.println("BME280 OK"); +
- +
-  WiFi.mode(WIFI_STA); +
-  WiFi.config(ip, gateway, subnet, dns); +
-  WiFi.begin(ssid, password); +
- +
-  Serial.print("WLAN verbinden"); +
-  while (WiFi.status() != WL_CONNECTED) { +
-    delay(300); +
-    Serial.print("."); +
-  } +
-  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str()); +
-} +
- +
-void loop() { +
-  float hum  = bme.readHumidity(); +
-  float temp = bme.readTemperature(); +
-  float pressure = bme.readPressure() / 100.0F;             // Pa → hPa +
-  float p0   = bme.seaLevelForAltitude(ALTITUDE, pressure);  // auf Meereshöhe umgerechnet +
- +
-  char path[96]; +
-  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey); +
-  char body[128]; +
-  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"P_hPa\":%.1f}", temp, hum, p0); +
- +
-  if (WiFi.status() == WL_CONNECTED) { +
-    WiFiClient client; +
-    HTTPClient http; +
-    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) { +
-      http.addHeader("Content-Type", "application/x-www-form-urlencoded"); +
-      int code = http.POST(body); +
-      Serial.printf("[HTTP] code=%d | T=%.2f°C F=%.1f%% P=%.1fhPa\n", code, temp, hum, p0); +
-      http.end(); +
-    } +
-  } else { +
-    Serial.println("[WLAN] nicht verbunden, versuche Reconnect..."); +
-    WiFi.begin(ssid, password); +
-  } +
- +
-  delay(SENDEINTERVALL_MS); +
-} +
-</code> +
- +
-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: +
- +
-<code cpp> +
-#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 +
-</code> +
- +
-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). +
 <code> <code>
-Wichtig bei Weitergabe an andere Schulen: Bereich A (SAS-ARDUINO-mobil) +"Klimamessstation für Schulen" von Martin Biebl/Sankt-Ansgar-Schule Hamburg, 
-enthält persönliche/private Zugangsdaten und sollte vor der +lizenziert unter CC BY-SA 4.0 
-Veröffentlichung KOMPLETT gelöscht werden (den ganzen #if...#endif- +(https://creativecommons.org/licenses/by-sa/4.0/)
-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.+
 </code> </code>
- 
----- 
- 
-===== 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). 
  
 <code> <code>
-Verwendet SENSOR_ID 999 als reservierte Test-Kennung - bitte NICHT +Diese Programme sind freie Software: Sie können es unter den 
-für echte Sensor-Boxen verwenden (siehe SENSOR_ID-Übersichtstabelle +Bedingungen der GNU General Public License, wie von der Free 
-in der Hauptanleitung). +Software Foundation veröffentlicht, weitergeben und/oder 
-</code>+modifizieren, entweder gemäß Version 3 der Lizenz oder (nach 
 +Ihrer Wahl) jeder späteren Version.
  
-<code cpp> +Die Veröffentlichung dieser Programme erfolgt in der Hoffnung, 
-/******************************************************************************************** +dass es Ihnen von Nutzen sein wird, aber OHNE JEDE GEWÄHRLEISTUNG, 
- *  TESTSENSOR für ESP8266 D1 Mini +sogar ohne die implizite Gewährleistung der MARKTFÄHIGKEIT oder 
- * +EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Details finden Sie in der 
- *  Dient NUR zum Prüfen der EmonCMS-Einrichtung (Inputs/Feeds/Dashboard). +GNU General Public License.
- *  Es sind KEINE echten Sensoren angeschlossen - alle Werte werden simuliert! +
- * +
- *  Sobald eine echte Klimamessbox oder Raummessbox verfügbar ist, diesen Sketch +
- *  durch den jeweils passenden ersetzen (siehe unten auf dieser Seite). +
- ********************************************************************************************/ +
-#include <ESP8266WiFi.h> +
-#include <ESP8266HTTPClient.h> +
-#include <WiFiClient.h>+
  
-#define SENSOR_ID 999             // Reservierte Test-ID - NICHT für echte Boxen verwenden! +Vollständiger Lizenztext: https://www.gnu.org/licenses/gpl-3.0.de.html
-#define SENDEINTERVALL_MS 30000   // Sendetakt in ms (hier kurz gehalten: alle 30 s, für schnelles Testen) +
- +
-char      CLIENT[8]; +
-IPAddress ip(192, 168, 1, SENSOR_ID); +
-IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile +
-const int httpPort = 80; +
- +
-// ============================================================ +
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen +
-// ============================================================ +
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) +
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) +
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) +
- +
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 +
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" +
-#endif +
- +
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- +
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! +
-#if NETZ_A_MOBIL +
-  const char* ssid     = "SAS-ARDUINO-mobil"; +
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<MOBIL-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- +
-#if NETZ_B_TEST +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<TEST-API-KEY>"; +
-#endif +
- +
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- +
-#if NETZ_C_LIVE +
-  const char* ssid     = "SAS-ARDUINO"; +
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; +
-  const char* apikey   = "<LIVE-API-KEY>"; +
-#endif +
- +
-IPAddress gateway(192, 168, 1, 1); +
-IPAddress subnet (255, 255, 255, 0); +
-IPAddress dns    (0, 0, 0, 0); +
- +
-void setup() { +
-  Serial.begin(115200); +
-  delay(200); +
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); +
-  Serial.printf("\n=== Testsensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str()); +
- +
-  WiFi.mode(WIFI_STA); +
-  WiFi.config(ip, gateway, subnet, dns); +
-  WiFi.begin(ssid, password); +
- +
-  Serial.print("WLAN verbinden"); +
-  while (WiFi.status() != WL_CONNECTED) { +
-    delay(300); +
-    Serial.print("."); +
-  } +
-  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str()); +
-} +
- +
-void loop() { +
-  // ---- Simulierte Werte (KEINE echten Sensoren!) ---- +
-  float temp = 20.0 + 5.0 * sin(millis() / 60000.0);    // pendelt sanft zwischen 15 und 25 °C +
-  float hum  = 45.0 + 10.0 * sin(millis() / 90000.0);   // pendelt sanft zwischen 35 und 55 % +
-  static int fakeCo2 = 400; +
-  fakeCo2 += random(-5, 6);                              // wandert leicht um 400 ppm +
- +
-  char path[96]; +
-  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey); +
-  char body[128]; +
-  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"CO2_ppm\":%d}", temp, hum, fakeCo2); +
- +
-  if (WiFi.status() == WL_CONNECTED) { +
-    WiFiClient client; +
-    HTTPClient http; +
-    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) { +
-      http.addHeader("Content-Type", "application/x-www-form-urlencoded"); +
-      int code = http.POST(body); +
-      Serial.printf("[HTTP] code=%d | T=%.2f F=%.1f CO2=%d\n", code, temp, hum, fakeCo2); +
-      http.end(); +
-    } +
-  } else { +
-    Serial.println("[WLAN] nicht verbunden, versuche Reconnect..."); +
-    WiFi.begin(ssid, password); +
-  } +
- +
-  delay(SENDEINTERVALL_MS); +
-}+
 </code> </code>
- 
-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 [[info:anleitungen: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 | 
- 
-<code cpp> 
-/******************************************************************************************** 
- *  EINFACHER SENSOR für ESP8266 D1 Mini - nur BME280 
- * 
- *  Guter Zwischenschritt zwischen dem simulierten Testsensor und den vollständigen 
- *  Sensor-Boxen (Klimamessbox, Raummessbox). Liest einen echten BME280 über I²C aus. 
- * 
- *  Verkabelung: VCC=3,3V, GND=GND, SCL=D1, SDA=D2, CSB=3,3V, SDO=GND 
- *  (SDO auf GND → I²C-Adresse 0x76, einheitlich mit der Klimamessbox) 
- ********************************************************************************************/ 
-#include <ESP8266WiFi.h> 
-#include <ESP8266HTTPClient.h> 
-#include <WiFiClient.h> 
-#include <Wire.h> 
-#include <Adafruit_BME280.h> 
- 
-#define SENSOR_ID 998              // Test-ID für diesen einfachen Sensor - vor Produktivbetrieb anpassen! 
-#define SENDEINTERVALL_MS 30000    // alle 30 Sekunden senden 
- 
-#define BME280_ADDRESS 0x76        // SDO auf GND → Adresse 0x76 (SDO auf 3,3V wäre 0x77) 
-#define ALTITUDE 42.0               // Höhe über Meeresspiegel in Metern, für Luftdruck-Korrektur 
- 
-Adafruit_BME280 bme; 
- 
-char      CLIENT[8]; 
-IPAddress ip(192, 168, 1, SENSOR_ID); 
-IPAddress SERVER_IP(192, 168, 1, 2);   // EmonCMS-Server, gilt für alle drei Netzwerkprofile 
-const int httpPort = 80; 
- 
-// ============================================================ 
-//  NETZWERK-AUSWAHL - genau EINE der drei Zeilen auf "true" setzen 
-// ============================================================ 
-#define NETZ_A_MOBIL true    // SAS-ARDUINO-mobil (persönliches Testnetz) 
-#define NETZ_B_TEST   false  // SAS-ARDUINO, Schul-Testbetrieb (TEST-API-Key) 
-#define NETZ_C_LIVE   false  // SAS-ARDUINO, Schul-Livebetrieb (LIVE-API-Key) 
- 
-#if (NETZ_A_MOBIL + NETZ_B_TEST + NETZ_C_LIVE) != 1 
-  #error "Bitte genau EINE der drei NETZ_-Zeilen oben auf true setzen!" 
-#endif 
- 
-// ---- BEREICH A: SAS-ARDUINO-mobil (persönliches Testnetz) ------------- 
-// Vor Weitergabe des Codes an andere Schulen DIESEN BLOCK LÖSCHEN! 
-#if NETZ_A_MOBIL 
-  const char* ssid     = "SAS-ARDUINO-mobil"; 
-  const char* password = "<MOBIL-WLAN-PASSWORT>"; 
-  const char* apikey   = "<MOBIL-API-KEY>"; 
-#endif 
- 
-// ---- BEREICH B: SAS-ARDUINO, Schul-Testbetrieb ------------------------- 
-#if NETZ_B_TEST 
-  const char* ssid     = "SAS-ARDUINO"; 
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; 
-  const char* apikey   = "<TEST-API-KEY>"; 
-#endif 
- 
-// ---- BEREICH C: SAS-ARDUINO, Schul-Livebetrieb -------------------------- 
-#if NETZ_C_LIVE 
-  const char* ssid     = "SAS-ARDUINO"; 
-  const char* password = "<SCHUL-WLAN-PASSWORT>"; 
-  const char* apikey   = "<LIVE-API-KEY>"; 
-#endif 
- 
-IPAddress gateway(192, 168, 1, 1); 
-IPAddress subnet (255, 255, 255, 0); 
-IPAddress dns    (0, 0, 0, 0); 
- 
-void setup() { 
-  Serial.begin(115200); 
-  delay(200); 
-  snprintf(CLIENT, sizeof(CLIENT), "%d", SENSOR_ID); 
-  Serial.printf("\n=== Einfacher BME280-Sensor startet, ID=%s, IP=%s ===\n", CLIENT, ip.toString().c_str()); 
- 
-  Wire.begin();  // Standard-Pins: D1=SCL, D2=SDA 
-  if (!bme.begin(BME280_ADDRESS)) { 
-    Serial.println("FEHLER: BME280 nicht gefunden! I2C-Verkabelung und Adresse (0x76) prüfen."); 
-    while (true) delay(100); 
-  } 
-  Serial.println("BME280 OK"); 
- 
-  WiFi.mode(WIFI_STA); 
-  WiFi.config(ip, gateway, subnet, dns); 
-  WiFi.begin(ssid, password); 
- 
-  Serial.print("WLAN verbinden"); 
-  while (WiFi.status() != WL_CONNECTED) { 
-    delay(300); 
-    Serial.print("."); 
-  } 
-  Serial.printf("\nVerbunden! IP: %s\n", WiFi.localIP().toString().c_str()); 
-} 
- 
-void loop() { 
-  float hum  = bme.readHumidity(); 
-  float temp = bme.readTemperature(); 
-  float pressure = bme.readPressure() / 100.0F;             // Pa → hPa 
-  float p0   = bme.seaLevelForAltitude(ALTITUDE, pressure);  // auf Meereshöhe umgerechnet 
- 
-  char path[96]; 
-  snprintf(path, sizeof(path), "/emoncms/input/post.json?node=%s&apikey=%s", CLIENT, apikey); 
-  char body[128]; 
-  snprintf(body, sizeof(body), "json={\"T_C\":%.2f,\"F_rel\":%.1f,\"P_hPa\":%.1f}", temp, hum, p0); 
- 
-  if (WiFi.status() == WL_CONNECTED) { 
-    WiFiClient client; 
-    HTTPClient http; 
-    if (http.begin(client, SERVER_IP.toString(), httpPort, path, false)) { 
-      http.addHeader("Content-Type", "application/x-www-form-urlencoded"); 
-      int code = http.POST(body); 
-      Serial.printf("[HTTP] code=%d | T=%.2f°C F=%.1f%% P=%.1fhPa\n", code, temp, hum, p0); 
-      http.end(); 
-    } 
-  } else { 
-    Serial.println("[WLAN] nicht verbunden, versuche Reconnect..."); 
-    WiFi.begin(ssid, password); 
-  } 
- 
-  delay(SENDEINTERVALL_MS); 
-} 
-</code> 
- 
-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 ==== 
-<code> 
-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 
-</code> 
----- 
- 
-[[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)// 
- 
QR-Code
QR-Code www-messdatenerfassung-sas:sensor_code (erstellt für aktuelle Seite)