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