Atmo - Home-made Wetterstation - Teil 2 (Coding)

Nachdem ich die Basis von “Atmo”, meiner Wetterstation, soweit bereit habe, kann ich nun den ESP32 programmieren, um die Sensorwerte auszulesen und zentral zu übermitteln.

Im 1. Teil von “Atmo” habe ich beschrieben, wie ich die Indoor-Basistation zusammengebaut habe, worauf dieser Artikel basiert.

Was soll der ESP machen?

Das Ziel ist, dass der ESP folgendes macht:

  • Die Temperatur, Luftdruck, Luftfeuchtigkeit und Luftgüte vom Sensor BME680 auslesen
  • Den Lux Wert vom Sensor TSL2591 auslesen
  • Den Indoor Air Quality Index berechnen
  • Falls nötig Werte korrigiert/kalibriert
  • Die Daten an den MQTT-Broker übermittelt

Arduino IDE einrichten

Ich nutze für die Programmierung des ESP32 die Arduino IDE, welche jedoch noch einige Einstellungen benötigt.

Einstellungen
Als erstes sollte das ESP32 Board Arduino bekannt gemacht werden:

  • File -> Preferences -> Additional Board Manager URLs: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  • Tools -> Board -> Board Manager: Suche nach “esp32” und installieren.

Die Arduino IDE benötigt dann folgende Settings:

Libraries
Danach müssen noch folgende Libraries installiert werden:

Coding

Eine Vorbemerkungen zum Code: Die Arduino IDE ist etwas unflexibel, da ein “Sketch” ein File ist. Ich möchte verhindern dass ein File zu viele Zeilen hat, da dies den Code unübersichtlich macht. Der Eintrittspunkt ist hier immer setup() und danach loop(), was Methoden/Funktionen sind, ich aber das ganze lieber in Klassen aufteilen würde. Da das ganze in C++ geschrieben wird müssen bei vielen verschiedenen Files mit Header-Files gearbeitet werden, damit der Compiler alles findet. Ich habe versucht das ganze etwas aufzuteilen und arbeite mit Funktionen und einigen globalen Variablen - einen Fall für ein späteres Refactoring in Klassen, sollte das noch mehr wachsen…

So, jetzt let’s code. 🤓

Damit die Sensoren gefunden werden, müssen die verbundenen Pins und den Modus angegeben werden:

#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#include "Adafruit_TSL2591.h"

#define BME_CS 16                               // Pin 16 wo der CS verbunden ist.

Adafruit_BME680 bme(BME_CS);                    // hardware SPI
Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591);  // hardware I2C

Environment-Sensor

Als erstes möchte ich die Werte aus dem BME680 auslesen. Bei allen Werten kann ich angeben, wieviel mal ausgelesen werden soll (oversampling). Je höher/öfters gesampelt wird, desto ungenauer weren die Werte dafür kann Rauschen in den Werten entfernt werden.
Mit dem IIR-Filter können die Spitzen von Temperatur und Luftdruck abgefangen werden, welche z.B. durch eine zuschlagende Tür oder einen Luftzug generiert werden.

// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms

Die Werte können nun ausgelesen werden:

bme.performReading();
bme.temperature                         // C°
bme.pressure / 100.0F;                  // hPa
bme.humidity;                           // %h
bme.gas_resistance / 1000.0;            // kOhm

#define SEALEVELPRESSURE_HPA (1013.25)  // Luftdruck auf Meereshöhe
bme.readAltitude(SEALEVELPRESSURE_HPA); // m

Der Wert für die Luftqualität entsteht durch das erhitzen der Luft und Messung des Wiederstandes an der Mess-Schicht, welches mir einen Wert in Ohm zurückgibt - daraus kann der IAQ-Index (Indoor Air Quality Index) berechnet werden.

Weitere Beispiele gibt es im Git-Repo der Library “Adafruit_BME680”.

Lichtsensor

Top, nun zu dem Lichtsensor welcher etwas speziell ist, da die Adafruit Library auf der “Unified Sensors” Library von Adafruit basiert. Dadurch ist einiges abstrahiert, aber für einige (andere) Sensoren gleich anzuwenden:

sensors_event_t event;
tsl.getEvent(&event);
event.light  // lux

Weitere Beispiele gibt es im Git-Repo der “Adafruit_TSL2591_Library”.

Atmo

Um einen besseren Überblick und einfacheren Zugriff auf die Werte zu haben, lege ich sie in einem Struct ab:

struct AtmoValues_t {
  float temperature;
  float humidity;
  float pressure;
  float gas;
  int iaq;
  float altitude;
  int light;
};

Weitere Werte lege ich darin ab, um sie am Schluss via MQTT zu übermitteln. In einem Loop werden jetzt alle Werte alle paar Sekunden ausgelesen und jede Minute versendet.

  • Warum schalte ich den ESP32 nicht in den Tiefschlaf und wecke ihn nur jede Minute wieder auf um die Werte auszulesen und zu übermitteln?

Das Problem mit dem Gas Sensor ist, dass er einen “Burn-in” benötigt, also eine Laufzeit und einige Zyklen bis er sich stabilisiert hat. Das kann durchaus einige Minuten gehen, was im Schlafmodus nicht funktioniert.
Zusätzlich habe ich festgestellt, dass am besten eine “Baseline” (Durchschnitt von Messungen über eine gewisse Zeit) verwendet wird, dass einzelne Peaks abgefangen werden können, da die Messungen schwanken.
Wie ihr seht, wäre das ein Problem, welches für einen batteriebetriebenen Modus zu lösen ist.

Kalibrierung

Für einige der Werte ist eine Kalibrierung nötig, damit diese mit realen Werten übereinstimmen. Dies ist jedoch nicht ganz Einfach, da ich keinen kalibrierten “Lux”-Sensor oder Barometer besitze, daher akzeptiere ich einfach diese Werte der Sensoren.

Bei der Temperatur, Luftfeuchtigkeit und dem IAQ gibt es Optimierungsbedarf. Die gemessene Temperatur bei mir ist über dem tatsächlichen Wert liegt (habe ein Thermometer ausgegraben). Dies ist verständlich, da der ESP32 und der Erhitzer des Gas-Sensors Abwärme abgibt. Zusätzlich trocknet der Gas-Sensor die Umgebungsluft, was wiederum einen negativen Einfluss auf die Luftfeuchtigkeit hat. Das ist hier der Nachteil der “All-in-one” Lösung. Ein Vorteil hier wäre sicherlich einen zusätzlichen, weiter entfernten Temperatur und Feuchtigkeitssensor gewesen. Dann kann der BME680 jedoch nicht die Temperatur und Feuchtigkeit in die Messungen einfliessen lassen.

Eine andere Möglichkeit ist, welche ich vielleicht in Zukunft implementiere, dass der Gas-Sensor seltener genutzt wird und versetzt zur Temperatur und Luftfeuchtigkeit gemessen/aktiviert wird, damit die Wärme sich verflüchtigen und die Luft sich “regenerieren” kann.

Korrektur der Temperatur
Hier habe ich 2 Möglichkeiten:

  • den “Raw”-Wert (ADC) vom Sensor selbst auslesen und einen Offset hinzufügen.
    Dies ist jedoch durch die Library nicht möglich. Dies hätte den Vorteil, dass gemäss Dokumentation der kalibrierte Wert für die Messung der Luftfeuchtigkeit miteinbezogen wird.
  • Ermittelter C° Wert direkt dem Ergebnis abziehen.

Aktuell rechne ich einfach eine Differenz der Temperatur dazu, welcher bei mir nach der Einlaufphase aktuell -2.0°C ist.

values.temperature = bme.temperature + atmo_temperature_offset;  // -2.0 °C

Indoor Air Quality Index

Ein weiteres komplexeres Thema. Es gibt diverse Diskussionen um den IAQ Wert, und wie der für den BME680 berechnet werden soll. Die Algorithmen dazu behält Bosch für sich und kann nur mit ihrer Software verwendet werden. Nicht ganz Open Source like :(. Zusätzlich gibt es keine definitive ISO-Norm/Methode zur Berechnung es Index.

Ich habe folgendes Modell genutzt, was in diversen Foren diskutiert wurde:

  • Der Index ist eine Kombination von Luftfeuchtigkeit und Gas.
  • 21°C und Luftfeuchtigkeit von 40% und ein Gas-Wert von 50kOhm wäre der Index bei 0. Das wäre dann eine “perfekte” Luft.
  • Die Berechnung nutzt 25% Luftfeuchtigkeit und 75% Gas.

Die beiden Scores (Luftfeuchtigkeit und Gas) werden anhand der Baselines und den Anteilen berechnet:

int humidity_score = ATMO_get_humidity_score(current_humidity);
int gas_score = ATMO_get_gas_score(current_gas);

float air_quality_score = humidity_score + gas_score;     // score
int air_quality_index = (100 - air_quality_score) * 5;    // resize to IAQ index of 0-500

Für noch mehr Genauigkeit könnte zusätzlich die Temperatur berücksichtigt werden, ich gebe mich aber mit diesem Ansatz zufrieden.

MQTT

Nach diesen Berechnungen und Korrekturen sind die Daten nun in AtmoValues_t abgelegt und bereit für den Versand mit MQTT.

Ich erkläre hier nicht, wie MQTT funktioniert, dafür empfehle ich dir eine andere Lektüre.

Jede Messart (Luftfeuchtigkeit, Temperatur etc.) ist ein eigenes Topic mit dem entsprechenden Wert. Damit verschiedene Geräte/Stationen unterschieden werden können, wird der Name in den Topic-Pfad codiert. So ist dies auch erweiterbar mit einer Aussenstation oder weiteren Sensoren pro Zimmer.

Aus den aktuellen Daten resultiert folgendes:

/sensors/atmo/temperature
/sensors/atmo/humidity
/sensors/atmo/gas
/sensors/atmo/pressure
/sensors/atmo/iaq
/sensors/atmo/altitude
/sensors/atmo/light

Anstatt die Bezeichnung in den Pfad zu codieren, könnte auch ein spezielles Format für die Datenübermittlung genutzt Werten, z.B. JSON und darin ist der Gerätenamen sowie der Wert vorhanden.
Um es einfach zu halten, verzichte ich darauf.

Für den Versand gibt es auch eine Library von Adafruit. Diese wird mit einem WiFiClient oder WiFiClientSecure initialisiert. “WiFiClientSecure” verwendet für die Verbindung zum MQTT eine Transportverschlüsselung (TLS).

#include <WiFiClient.h>

WiFiClient wlan_client;
Adafruit_MQTT_Client mqtt(&wlan_client, mqtt_config.server, mqtt_config.port, mqtt_config.user, mqtt_config.password);

Somit muss nur noch ein Adafruit_MQTT_Publish Topic mit dem richtigen Pfad initialisiert werden um den Wert verschicken zu können.

/**
 * Function to send sensors readings to MQTT Broker in specified topics
 */
bool MQTT_send_values(AtmoValues_t values) {

  MQTT_connect();

  Serial.print(F("\nSending to MQTT...\n"));

  // TEMPERATURE
  topic = Adafruit_MQTT_Publish(&mqtt, String("/sensors/" + hostname + "/temperature").c_str());
  if (! topic.publish(values.temperature)) {
    Serial.println(F("Failed to send temperature"));
  }

  // ...
}

Compile & Run

Nur noch den ESP32 anschliessen, die Konfigurationen für WLAN und MQTT eintragen und Uploaden:

17:42:04.147 -> -=# Atmo Weather Station #=-
17:42:04.180 -> I'm awake.
17:42:04.180 -> BME680 detected!
17:42:07.833 -> TSL2591 detected!
17:42:07.867 -> ------------------------------------
17:42:07.900 -> Sensor:       TSL2591
17:42:07.933 -> Driver Ver:   1
17:42:07.933 -> Unique ID:    2591
17:42:07.966 -> Max Value:    88000.00 lux
17:42:07.999 -> Min Value:    0.00 lux
17:42:08.033 -> Resolution:   0.0010 lux
17:42:08.033 -> Gain:         25x (Medium)
17:42:08.066 -> Timing:       100 ms
17:42:08.099 -> ------------------------------------
17:42:08.132 ->
17:42:09.095 -> Connecting to WiFi..
17:42:09.128 -> Connected to the WiFi network
17:42:09.162 -> IP address: 192.168.1.200
17:42:09.460 ->
17:42:09.460 -> => Getting a new gas baseline
17:42:13.116 -> Gas Baseline = 409802.438
17:42:13.714 ->
17:42:13.733 -> Sensor Readings:
17:42:13.736 -> Temperature = 23.51°C
17:42:13.770 -> Pressure    = 982.12 hPa
17:42:13.770 -> Humidity    = 35.6%
17:42:13.803 -> Gas         = 337.82 kOhms
17:42:13.836 -> Air Quality Index = 15
17:42:13.869 ->
17:42:13.869 -> Sending to MQTT...
17:42:13.879 -> waiting.

Und der Empfang der Werte im MQTT-Broker:

Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/temperature', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/humidity', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/pressure', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/gas', ... (6 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/iaq', ... (3 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/altitude', ... (5 bytes))
Received PUBLISH from auto-5B1413C1-32D7-3130-51D3-E776EF7044DD (d0, q0, r0, m0, '/sensors/atmo/light', ... (2 bytes))

Juhuu. 🎉

Ergebnis

Mir ist die Komplexität der Thematik um Werte zu korrigieren und kalibrieren erst beim Ausprobieren richtig bewusst geworden. Nicht nur der Chip an sich, sondern auch die Mathematik/Physik dahinter, wie z.B. den IAQ zu berechnen oder wie die Höhe aus Druck und Temperatur errechnet werden kann.
Um die Werte weiter stabilisieren zu können, überlege ich mir noch ein DH22 Sensor auszuprobieren um die Temperatur- und Luftfeuchtigkeitswerte des BME680 zu ergänzen.

Ich möchte der Open Source Community auch etwas zurückgeben, daher ist der vollständige Code auf GitHub zu finden:

Die weiteren Komponenten, wie die Atmo-Bridge oder Dashboards werde ich im Verlauf der Serie noch beschreiben und ergänzen.