Innenraumthermometer – Programmversion 12
Die im Vergleich zu Programmversion 0.4 vergleichsweise hohe Versionsnummer ergibt sich aus dem Umstand, dass ich die Zählweise der Programmversionen, die ich erstelle, geändert habe. Seit einiger Zeit zähle ich die Programmversionen einfach durch. Demnach ist diese Version nunr die Nummer 12.
Was hat sich geändert? Beide Innenraumthermometer verrichten nun (Stand: 23.06.2024) schon seit fünf Jahren zuverlässig ihren Dienst. Es kam nur extrem selten vor, dass der Code hängen blieb. Aber es passierte hin und wieder. Dieses Problem sollte mit der neuen Programmversion der Vergangenheit angehören. Zum einen legt der ESP 32 sich nun einfach wieder schlafen, wenn es ihm nicht gelingt, sich mit dem WLAN zu verbinden. Zum anderen legt er sich auch schlafen, wenn es ihm nicht gelingt, den BME280 zu initialisieren. Letzteres kam zuletzt bei dem Außenthermometer häufiger vor.
Darüber hinaus habe ich eine Vielzahl weiterer Verbesserungen am Code vorgenommen. Hierbei erwies sich ChatGPT als ausgesprochen hilfreich. Insbesondere hatte ChatGPT die Verwendung von Konstanten und Makros zur Vermeidung von „Magic Numbers“ und eine stärkere Modularisierung empfohlen.
Schließlich wurde auch die grafische Darstellung auf dem Display überarbeitet.
Was nuch zu überlegen wäre:
- Perspektivisch könnte die ganze NTP-Funktionalität entfernt werden. Die war nur für das Debugging interessant.
- Es könnte ab einer gewissen Schwelle der Hitzeindex auf dem Display angezeigt werden.
- Bei dieser programmversion legt sich der ESP 32 schlafen, wenn keine Verbindung zum WLAN hergestellt werden kann. Beim Außenthermometer ist dieser Programmablauf sinnvoll, weil dieses kein Display hat. Beim Innenraumthermometer könnten aber wenigstens die gemessenen Daten auf dem Display angezeigt werden, auch wenn sie nicht auf den Server übertragen werden können. Andererseits kommt das auch nur recht selten vor.
// Innenraumthermometer // Läuft auf einem Lolin D32 // Misst Temperatur, Luftfeuchtigkeit und Luftdruck // Zeigt die Daten auf einem E-Paper-Display an // Versucht sich nur einige Male mit dem WLAN zu verbinden // Versucht sich nur einige male mit dem Sensor zu verbinden // Sendet die Daten an iot.frickelpiet.de/adddata.php // Empfängt Daten von iot.frickelpiet.de/getdata.php // Kann für zwei Innenraumthermometer konfiguriert werden // Bibliotheken #include <WiFi.h> #include "time.h" #include <Wire.h> #include <SPI.h> #include <Adafruit_Sensor.h> #include <Adafruit_BME280.h> #include <GxEPD.h> #include <GxGDEH029A1/GxGDEH029A1.cpp> // Waveshare 2.9" b/w #include <GxIO/GxIO_SPI/GxIO_SPI.cpp> #include <GxIO/GxIO.cpp> #include <U8g2_for_Adafruit_GFX.h> #include <ArduinoJson.h> // Konfigurationskonstanten #define SENSORBOX_ID 3 // "2" für Wohnzimmer, "3" für Küche #define uS_TO_S_FACTOR 1000000 // Umrechnungsfaktor für Mikrosekunden zu Sekunden #define TIME_TO_SLEEP 600 // Zeit in Sekunden, die der ESP32 schlafen wird // Debug-Level //#define DEBUG 1 // Ausgabe sehr vieler Daten an die serielle Schnittstelle // Base Class and path GxIO_Class io(SPI, SS, 17, 16); GxEPD_Class display(io, 16, 4); Adafruit_BME280 bme; // I2C uint64_t chipid; U8G2_FOR_ADAFRUIT_GFX u8g2_for_adafruit_gfx; // WLAN SSDI und Passwort const char* WIFI_SSID = "XXXXXXXXXXXXXXXXXXXXXXXX"; // Name des WLAN const char* WIFI_PASSWORD = "XXXXXXXXXXXXXXXXXXXX"; // Passwort des WLAN // Webserver iot.frickelpiet.de const char SERVER_ADDRESS[] = "xxxxxxxxxxxxxxxxxx"; const int SERVER_PORT = 80; // NTP Konfiguration const char* ntpServer = "pool.ntp.org"; // NTP Server Pool const long gmtOffset_sec = 3600; const int daylightOffset_sec = 3600; // Datenübertragung auf Webserver char server[] = "XXXXXXXXXXXXXXXXXXX"; const int port = 80; WiFiClient client; #if SENSORBOX_ID == 2 const int sensorBox = 2; // Eindeutige ID dieses Thermometers. Wird von adddata.php ausgewertet #endif #if SENSORBOX_ID == 3 const int sensorBox = 3; // Eindeutige ID dieses Thermometers. Wird von adddata.php ausgewertet #endif const int SDA_PIN = 25; // SDA Pin für I2C const int SCL_PIN = 26; // SCL Pin für I2C const int ANALOG_PIN_BATTERY = 35; // Pin für die Batteriespannung const unsigned long WIFI_CONNECT_TIMEOUT = 500; // Timeout für WiFi-Verbindung in Millisekunden const int MAX_WIFI_ATTEMPTS = 20; // Maximale Anzahl der WiFi-Verbindungsversuche const int MAX_SENSOR_ATTEMPTS = 10; // Maximale Anzahl der Sensor-Initialisierungsversuche const float VOLTAGE_CONVERSION_FACTOR = 7.445; // Faktor zur Umrechnung des Spannungswerts const int RSSI_OFFSET = 100; // Offset für RSSI-Berechnung const int RSSI_MULTIPLIER = 2; // Multiplikator für RSSI-Berechnung float temperature; // interne Temperatur float humidity; // interne Luftfeuchtigkeit float pressure; // interner Luftdruck float dewpoint; // Taupunkt basierend auf den internen Daten float voltage; // Spannung der Batterie int rssi; // Signalqualität des WiFi int bars; // Anzahl der Balken für Signalqualität float externalTemperature; // Außentemperatur float externalHumidity; // Außenluftfeuchtigkeit float externalPressure; // Außenluftdruck boolean dataReceived = false; // Wird wahr, wenn Daten vom Server erfolgreich empfangen wurden String data; // String, der an den Webserver übertragen wird void debugPrint(const String &message) { #if defined DEBUG Serial.println(message); #endif } void setup () { #if defined DEBUG Serial.begin(115200); delay(1000); // Zeit zum Öffnen des Seriellen Monitors #endif esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); debugPrint("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) + " Seconds"); chipid = ESP.getEfuseMac(); // Die Chip-ID ist im Wesentlichen die MAC-Adresse (Länge: 6 Bytes). debugPrint("ESP32 Chip ID = " + String((uint16_t)(chipid >> 32), HEX) + String((uint32_t)chipid, HEX)); Wire.begin(SDA_PIN, SCL_PIN); // SDA, SCL // Verbindungsaufbau WLAN debugPrint("Connecting to " + String(WIFI_SSID)); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); int wifiCounter = 0; // Zählt die Verbindungsversuche zum WiFi while (WiFi.status() != WL_CONNECTED && wifiCounter <= MAX_WIFI_ATTEMPTS) { wifiCounter++; delay(WIFI_CONNECT_TIMEOUT); debugPrint("."); } if (WiFi.status() != WL_CONNECTED) { debugPrint("Connection to wifi failed! Going to sleep now"); esp_deep_sleep_start(); } debugPrint("Connected! IP address: " + WiFi.localIP().toString()); // init and get the time configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); printLocalTime(); // e-Paper Display Waveshare 2,9 Zoll display.init(115200); // enable diagnostic output on Serial display.setRotation(3); // Displayinhalt um 90° nach rechts drehen debugPrint("Display setup done"); // Bindet die u8g2 Prozeduren in Adafruit GFX-Bibliothek ein u8g2_for_adafruit_gfx.begin(display); // Sensor BME280 initialisieren int sensorCounter = 0; while (!bme.begin() && sensorCounter <= MAX_SENSOR_ATTEMPTS) { sensorCounter++; debugPrint("Attempting to initialize BME280 sensor..."); delay(WIFI_CONNECT_TIMEOUT); debugPrint("."); } if (sensorCounter > MAX_SENSOR_ATTEMPTS) { debugPrint("Could not find a valid BME280 sensor, check wiring! Going to sleep now"); esp_deep_sleep_start(); } bme.setSampling(Adafruit_BME280::MODE_FORCED, Adafruit_BME280::SAMPLING_X1, // temperature Adafruit_BME280::SAMPLING_X1, // pressure Adafruit_BME280::SAMPLING_X1, // humidity Adafruit_BME280::FILTER_OFF ); readSensorData(); // Daten vom Sensor abrufen calculateDewpoint(); // Taupunkt berechnen readBatteryVoltage(); // Batteriespannung abfragen getWiFiSignalQuality(); // Signalqualität WLAN abfragen getDataFromServer(); // Daten vom Server abrufen writeDisplay(); // Display schreiben if (WiFi.status() == WL_CONNECTED) { createDataString(); sendDataToServer(); } // Schickt den ESP32 in den Tiefschlaf debugPrint("Going to sleep now"); esp_deep_sleep_start(); } void loop() { // Leerer Loop, da der ESP32 in den Tiefschlaf geht. } // Funktionen void printLocalTime() { struct tm timeinfo; if(!getLocalTime(&timeinfo)){ debugPrint("Failed to obtain time"); return; } #if defined DEBUG Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S"); #endif } void displayLocalTime() { struct tm timeinfo; if(!getLocalTime(&timeinfo)){ display.print("--:--:--"); return; } display.print(&timeinfo, "%H:%M:%S"); } void readSensorData() { temperature = bme.readTemperature(); humidity = bme.readHumidity(); pressure = bme.readPressure() / 100.0F; } void calculateDewpoint() { const float a = 17.271; const float b = 237.7; float dewpointTmp = (a * temperature) / (b + temperature) + log(humidity / 100); dewpoint = (b * dewpointTmp) / (a - dewpointTmp); debugPrint("Dewpoint: " + String(dewpoint) + " °C"); } void readBatteryVoltage() { voltage = analogRead(ANALOG_PIN_BATTERY) / 4096.0 * VOLTAGE_CONVERSION_FACTOR; debugPrint("Battery voltage: " + String(voltage) + " volts"); } void getWiFiSignalQuality() { rssi = min((WiFi.RSSI() + RSSI_OFFSET) * RSSI_MULTIPLIER, 100); debugPrint("WiFi signal: " + String(rssi) + " %"); bars = getSignalQualityBars(rssi); debugPrint("Signal Quality Bars: " + String(bars)); } int getSignalQualityBars(int rssi) { if (rssi < 25) { return 0; // 0 Balken } else if (rssi < 50) { return 1; // 1 Balken } else if (rssi < 75) { return 2; // 2 Balken } else if (rssi < 100) { return 3; // 3 Balken } else { return 4; // 4 Balken } } void getDataFromServer() { if (WiFi.status() == WL_CONNECTED) { // Aktuelle Zeit von einem NTP-Server holen //configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); debugPrint("Uhrzeit: "); printLocalTime(); // Daten des Aussenthermometers von iot.frickelpiet.de/getdata.php holen if (client.connect(server, port)) { debugPrint("Server connection OK"); // Send HTTP request client.println(F("GET /getdata.php HTTP/1.0")); client.println(F("Host: iot.frickelpiet.de")); client.println(F("Connection: close")); if (client.println() == 0) { debugPrint(F("Failed to send request")); } else { // Check HTTP status char status[32] = {0}; client.readBytesUntil('\r', status, sizeof(status)); if (strcmp(status, "HTTP/1.1 200 OK") != 0) { debugPrint("Unexpected response: " + String(status)); } else { // Skip HTTP headers char endOfHeaders[] = "\r\n\r\n"; if (!client.find(endOfHeaders)) { debugPrint(F("Invalid response")); } else { // Allocate JsonBuffer // Use arduinojson.org/assistant to compute the capacity. const size_t bufferSize = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(3) + 60; DynamicJsonBuffer jsonBuffer(bufferSize); // Parse JSON object JsonArray& root = jsonBuffer.parseArray(client); if (!root.success()) { debugPrint(F("Parsing failed!")); } else { // Extract values JsonObject& root_0 = root[0]; externalTemperature = root_0["temperature"]; externalHumidity = root_0["humidity"]; externalPressure = root_0["pressure"]; debugPrint("ext. Temp.: " + String(externalTemperature)); debugPrint("ext. Hum.: " + String(externalHumidity)); debugPrint("ext. Pres.: " + String(externalPressure)); } dataReceived = true; } } } // Disconnect client.stop(); } } } void writeDisplay() { // Daten an Display senden display.fillScreen(GxEPD_WHITE); display.setTextColor(GxEPD_BLACK); // Systemschrift display.setFontMode(1); // u8g2 Fonts display.setBackgroundColor(GxEPD_WHITE); // u8g2 Fonts display.setForegroundColor(GxEPD_BLACK); // u8g2 Fonts // Temperatur (innen) display.setCursor(4, 50); display.setFont(u8g2_font_logisoso42_tf); display.print(temperature, 1); display.setCursor(101, 36); display.setFont(u8g2_font_logisoso28_tf); display.print("°C"); // Luftfeuchtigkeit (innen) display.setCursor(176, 50); display.setFont(u8g2_font_logisoso42_tf); display.print(humidity, 1); display.setCursor(276, 36); display.setFont(u8g2_font_logisoso28_tf); display.print("%"); // Trennlinie //display.drawRoundRect(108 , 64, 80, 12, 2, GxEPD_BLACK); display.setCursor(118, 74); display.setFont(u8g2_font_helvB08_tf); display.print("Innensensor"); display.drawLine(44, 75, 108, 75, GxEPD_BLACK); // Trennlinie links //display.drawRoundRect(108 , 75, 80, 12, 2, GxEPD_BLACK); display.drawRoundRect(108 , 64, 80, 24, 2, GxEPD_BLACK); // Rechteck in der Mitte display.drawLine(188, 75, 291, 75, GxEPD_BLACK); // Trennlinie rechts display.setCursor(116, 85); display.setFont(u8g2_font_helvB08_tf); display.setTextColor(GxEPD_WHITE); display.print("Außensensor"); display.setTextColor(GxEPD_BLACK); if (dataReceived == true) { // Temperatur (außen) display.setCursor(4, 122); display.setFont(u8g2_font_logisoso28_tf); display.print(externalTemperature, 1); display.setCursor(70, 111); display.setFont(u8g2_font_logisoso16_tf); display.print("°C"); // Luftdruck (außen) display.setCursor(111, 122); display.setFont(u8g2_font_logisoso16_tf); if (externalPressure >= 1000) { display.print(externalPressure, 0); } else { display.print(" "); display.print(externalPressure, 0); } display.setCursor(155, 116); display.setFont(u8g2_font_helvB10_tf); display.print("hPa"); // Luftfeuchtigkeit (außen) if (externalHumidity < 100) { display.setCursor(216, 122); } else { display.setCursor(197, 122); } display.setFont(u8g2_font_logisoso28_tf); display.print(externalHumidity, 1); display.setCursor(283, 111); display.setFont(u8g2_font_logisoso16_tf); display.print("%"); } else { display.setCursor(65, 116); display.setFont(u8g2_font_helvB10_tf); display.print("Keine Daten empfangen!"); } // Aktualisierung display.setCursor(248, 74); display.setFont(u8g2_font_open_iconic_check_1x_t); display.setFont(u8g2_font_helvR08_tf); display.print(" "); displayLocalTime(); /* // Batteriespannung display.setCursor(4, 74); display.setFont(u8g2_font_open_iconic_check_1x_t); display.setFont(u8g2_font_helvR08_tf); display.print("Bat: "); display.print(voltage, 1); display.print(" V"); */ // Batteriespannung //display.drawRoundRect(5, 118, 18, 10, 2, GxEPD_BLACK); display.drawRoundRect(5, 70, 18, 10, 2, GxEPD_BLACK); display.fillRect(24, 73, 1, 4, GxEPD_BLACK); if (voltage > 3.2) { display.fillRect(7, 72, 2, 6, GxEPD_BLACK); } if (voltage > 3.4) { display.fillRect(7, 72, 2, 6, GxEPD_BLACK); display.fillRect(10, 72, 2, 6, GxEPD_BLACK); } if (voltage > 3.6) { display.fillRect(7, 72, 2, 6, GxEPD_BLACK); display.fillRect(10, 72, 2, 6, GxEPD_BLACK); display.fillRect(13, 72, 2, 6, GxEPD_BLACK); } if (voltage > 3.8) { display.fillRect(7, 72, 2, 6, GxEPD_BLACK); display.fillRect(10, 72, 2, 6, GxEPD_BLACK); display.fillRect(13, 72, 2, 6, GxEPD_BLACK); display.fillRect(16, 72, 2, 6, GxEPD_BLACK); } if (voltage > 4.0) { display.fillRect(7, 72, 2, 6, GxEPD_BLACK); display.fillRect(10, 72, 2, 6, GxEPD_BLACK); display.fillRect(13, 72, 2, 6, GxEPD_BLACK); display.fillRect(16, 72, 2, 6, GxEPD_BLACK); display.fillRect(19, 72, 2, 6, GxEPD_BLACK); } // Signalqualität display.drawLine(30, 79, 40, 79, GxEPD_BLACK); if (bars >= 1) { display.fillRect(30, 76, 2, 2, GxEPD_BLACK); } if (bars >= 2) { display.fillRect(30, 76, 2, 2, GxEPD_BLACK); display.fillRect(33, 74, 2, 4, GxEPD_BLACK); } if (bars >= 3) { display.fillRect(30, 76, 2, 2, GxEPD_BLACK); display.fillRect(33, 74, 2, 4, GxEPD_BLACK); display.fillRect(36, 72, 2, 6, GxEPD_BLACK); } if (bars >= 4) { display.fillRect(30, 76, 2, 2, GxEPD_BLACK); display.fillRect(33, 74, 2, 4, GxEPD_BLACK); display.fillRect(36, 72, 2, 6, GxEPD_BLACK); display.fillRect(39, 70, 2, 8, GxEPD_BLACK); } // Status der Verbindung zum WLAN if (WiFi.status() != WL_CONNECTED) { // Wenn keine Verbindung zum WLAN hergestellt werden konnte, ... display.setCursor(177, 128); display.setFont(u8g2_font_helvR08_tf); display.print("Keine Verbindung zum WLAN!"); // ... erscheint eine entsprechende Meldung auf dem Display. } //*/ display.update(); } void createDataString() { data = "sensorbox=" + String(SENSORBOX_ID) + "&temperature=" + String(temperature) + "&humidity=" + String(humidity) + "&pressure=" + String(pressure) + "&dewpoint=" + String(dewpoint) + "&rssi=" + String(rssi) + "&voltage=" + String(voltage); debugPrint("Data String: " + data); } void sendDataToServer() { debugPrint("Connecting to: " + String(SERVER_ADDRESS)); if (client.connect(SERVER_ADDRESS, SERVER_PORT)) { debugPrint("Connected to: " + String(SERVER_ADDRESS)); client.print("GET /adddata.php?"); client.print(data); client.println(" HTTP/1.1"); client.print("Host: "); client.println(SERVER_ADDRESS); client.println("Connection: close"); client.println(); client.stop(); } else { debugPrint("Connection to server failed"); } }