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:

// 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");
  }
}