====== Innenraumluftqualitätsmonitor: Programmversion 0.1 ====== Diese erste Programmversion fokussiert auf die grundlegendsten Funktionen: * Das Display wird angesteuert. * Die Sensoren SGP30 und BME280 werden ausgelesen. * Von allen Sensoren werden die Daten über einen Zeitraum von sechs Stunden in Arrays angelegt. * Die Daten einiger Sensoren werden im Zeitverlauf auf dem Display dargestellt. Weil die Fonts der Adafruit GFX Bibliothek viel zu groß sind, habe ich die Bibliothek [[https://github.com/olikraus/U8g2_for_Adafruit_GFX|U8g2_for_Adafruit_GFX]] eingebunden. Damit lassen sich zahllose, auch relativ kleine Fonts verwenden, die dennoch dem Auge schmeicheln. // Innenraumluftqualitätsmonitor // Ab dieser Version wird anstatt eines Teensy 3.6 ein Lolin D32 verwendet // include library, include base class, make path known #include #include #include #include #include "Adafruit_SGP30.h" #include #include // 7.5" b/w #include #include #include #include "RunningAverage.h" U8G2_FOR_ADAFRUIT_GFX u8g2_for_adafruit_gfx; /* * Anschluss des Waveshare 7,5 Zoll e-Paper Display am Lolin D32 * DIN (blau): Pin 23 * CLK (gelb): Pin 18 * CS (orange): Pin 5 * DC (grün): Pin 17 * RST (weiß): Pin 16 * BUSY (violett): Pin 4 */ GxIO_Class io(SPI, SS, 17, 16); GxEPD_Class display(io, 16, 4); Adafruit_SGP30 sgp; Adafruit_BME280 bme; // I2C RunningAverage averageCO2(60); // RunningAverage-Objekt für Glättung der CO2-Messwerte RunningAverage averageTVOC(60); // RunningAverage-Objekt für Glättung der TVOC-Messwerte RunningAverage averageTEMP(1); // RunningAverage-Objekt für Glättung der Temperatur-Messwerte (Muss später auf eine sinnvolle Länge geändert werden.) RunningAverage averageHUMI(1); // RunningAverage-Objekt für Glättung der Luftfeuchtigkeits-Messwerte (Muss später auf eine sinnvolle Länge geändert werden.) RunningAverage averagePRES(1); // RunningAverage-Objekt für Glättung der Luftdruck-Messwerte (Muss später auf eine sinnvolle Länge geändert werden.) // Sechs-Stunden-Arrays unsigned int arrayCO2_6H[291]; // Messwerte für ECO2 unsigned int arrayTVOC_6H[291]; // Messwerte für TVOC float arrayTEMP_6H[290]; // Messerte für Temperatur float arrayHUMI_6H[290]; // Messwerte für Luftfeuchtigkeit float arrayPRES_6H[290]; // Messwerte für Luftdruck int validMeasures6H = 0; // Zählt den "Füllstand" der Sechs-Stunden Arrays. int counter = 0; // Definiert die Tracking-Variablen für die IF-Abfragen unsigned long previousMillisBME280 = 0; // Adafruit BME280 unsigned long previousMillisSGP30 = 0; // Adafruit SGP30 unsigned long previousMillisArrays = 0; // Arrays unsigned long previousMillisDisplay = 0; // Display // Definiert die Intervalle für die IF-Abfragen in Millisekunden const unsigned long intervalBME280 = 78000; // konstanter Delay für Adafruit BME280 const unsigned long intervalSGP30 = 1000; // konstanter Delay für Adafruit SGP30 (Der Sensor muss einmal pro Sekunde abgefragt werden, damit die automatische Kalibrierung funktioniert.) const unsigned long intervalArrays = 78000; // konstanter Delay für Arrays const unsigned long intervalDisplay = 78000; // konstanter Delay für Display void setup () { Serial.begin(115200); delay(1000); Wire.begin(25, 26); // SDA, SCL // e-Paper Display Waveshare 7,5 Zoll Serial.println(); Serial.println("Setup Display"); display.init(115200); // enable diagnostic output on Serial display.setRotation(0); Serial.println("Setup done"); // Air Quality Sensor SGP30 if (! sgp.begin()){ Serial.println("Sensor not found :("); while (1); } Serial.print("SGP30 serial number "); Serial.print(sgp.serialnumber[0], HEX); Serial.print(sgp.serialnumber[1], HEX); Serial.println(sgp.serialnumber[2], HEX); // If you have a baseline measurement from before you can assign it to start, to 'self-calibrate' sgp.setIAQBaseline(0x899D, 0x8D36); // Temperature, Humidity, Pressure Sensor BME280 bool status; status = bme.begin(); if (!status) { Serial.println("Could not find a valid BME280 sensor, check wiring!"); while (1); } 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 ); // Leeren der Arrays der RunningAverage-Objekte averageCO2.clear(); averageTVOC.clear(); averageTEMP.clear(); averageHUMI.clear(); averagePRES.clear(); u8g2_for_adafruit_gfx.begin(display); // connect u8g2 procedures to Adafruit GFX // Startbildschirm 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 displayStatus(); displayGrid (0, 99, 400, 480, "Kohlendioxid", arrayCO2_6H[0], "ppm"); displayGrid (0, 254, 0, 80, "Organische Verbindungen", arrayTVOC_6H[0], "ppb"); displayGrid (320, 99, 00, 40, "Feinstaub 10", arrayCO2_6H[0], "ug/m3"); displayGrid (320, 254, 0, 40, "Feinstaub 2.5", arrayTVOC_6H[0], "ug/m3"); display.update(); } void loop() { // Aktuelle Zeit abfragen unsigned long currentMillis = millis(); // Adafruit SGP30 abfragen if ((unsigned long)(currentMillis - previousMillisSGP30) >= intervalSGP30) { // Werte für Temperatur und Luftfeuchtigkeit werden an den Sensor übergeben float temperature, humidity; if (millis() < 78000) { // Gültige Messwerte liegen erst nach 78 Sekunden vor temperature = 25.0; // [°C] humidity = 50.0; // [%RH] } else { temperature = averageTEMP.getAverage(); // [°C] humidity = averageHUMI.getAverage(); // [%RH] } sgp.setHumidity(getAbsoluteHumidity(temperature, humidity)); // Daten auslesen und an die Glättung übergeben if (! sgp.IAQmeasure()) { Serial.println("Measurement failed"); return; } //Serial.print("TVOC "); Serial.print(sgp.TVOC); Serial.print(" ppb\t"); //Serial.print("eCO2 "); Serial.print(sgp.eCO2); Serial.println(" ppm"); averageCO2.addValue(sgp.eCO2); // Fügt dem Objekt einen neuen Wert hinzu averageTVOC.addValue(sgp.TVOC); // Fügt dem Objekt einen neuen Wert hinzu Serial.print("."); counter = counter + 1; if (counter >= 71) { Serial.println(); counter = 0; } //Speichere die aktuelle Zeit in die zughörige Variable previousMillisSGP30 = currentMillis; } // Adafruit BME280 abfragen if ((unsigned long)(currentMillis - previousMillisBME280) >= intervalBME280) { bme.takeForcedMeasurement(); averageTEMP.addValue(bme.readTemperature()); averageHUMI.addValue(bme.readHumidity()); averagePRES.addValue(bme.readPressure() / 100.0F); //Speichere die aktuelle Zeit in die zughörige Variable previousMillisBME280 = currentMillis; } // Arrays mit Daten füllen if ((unsigned long)(currentMillis - previousMillisArrays) >= intervalArrays) { // Die Sechs-Stunden-Arrays werden mit Daten befüllt. Die aktuelle Messung steht immer ganz links im Array. Ältere Messungen werden automatisch verschoben. addUnsignedIntegerToArray (arrayCO2_6H, averageCO2.getAverage()); addUnsignedIntegerToArray (arrayTVOC_6H, averageTVOC.getAverage()); addFloatToArray (arrayTEMP_6H, averageTEMP.getAverage()); addFloatToArray (arrayHUMI_6H, averageHUMI.getAverage()); addFloatToArray (arrayPRES_6H, averagePRES.getAverage()); // Der "Füllstand" der Sechs-Stunden-Arrays wird gezählt validMeasures6H = validMeasures6H + 1; if (validMeasures6H >= 290) { validMeasures6H = 290; } Serial.print("validMeasures6H: "); Serial.println(validMeasures6H); //Speichere die aktuelle Zeit in die zughörige Variable previousMillisArrays = currentMillis; } // Display if ((unsigned long)(currentMillis - previousMillisDisplay) >= intervalDisplay) { display.fillScreen(GxEPD_WHITE); // Der Displayinhalt wird gelösch display.setTextColor(GxEPD_BLACK); // Statusleiste displayStatus (); // Temperatur, Luftfeuchtigkeit und Luftdruck displayValue (0, 14, "Temperatur", arrayTEMP_6H[0], "°C"); displayValue (150, 14, "Luftfeuchtigkeit", arrayHUMI_6H[0], "%rH"); displayValue (300, 14, "Luftdruck", arrayPRES_6H[0], "hPa"); // Anzeige der CO2-Werte unsigned int minCO2 = minValue(arrayCO2_6H, validMeasures6H); // Berechnet den Minimalwert im Array für alle "gültigen" Messwerte) Serial.print("minCO2: "); Serial.println(minCO2); unsigned int maxCO2 = maxValue(arrayCO2_6H, validMeasures6H); // Berechnet den Minimalwert im Array für alle "gültigen" Messwerte) if (maxCO2 <= 480) { // maxCO2 muss immer größer als minCO2 sein, sonst stürzt der ESP32 ab maxCO2 = 480; } Serial.print("maxCO2: "); Serial.println(maxCO2); // Zeigt das Sechs-Stunden-Array für eCO2 an if (validMeasures6H == 1) { display.drawPixel((314), (map(arrayCO2_6H[0], 400, maxCO2, 218, 98)), GxEPD_BLACK); } else { // Es müssen zwei Messwerte vorliegen, um den Graph zeihenen zu können for (int i=0; i < validMeasures6H - 1; i++) { display.drawLine((314 - i), (map(arrayCO2_6H[i], 400, maxCO2, 218, 98)), (314 - i - 1), (map(arrayCO2_6H[i + 1], 400, maxCO2, 218, 98)), GxEPD_BLACK); } } // Zeichnet das Grid displayGrid (0, 99, 400, maxCO2, "Kohlendioxid", arrayCO2_6H[0], "ppm"); // (offsetX, offsetY, minCO2, maxCO2, Titel, aktueller Messwert, Einheit) // Anzeige der TVOC-Werte unsigned int minTVOC = minValue(arrayTVOC_6H, validMeasures6H); // Berechnet den Minimalwert im Array für die ersten 290 Elemente) Serial.print("minTVOC: "); Serial.println(minTVOC); unsigned int maxTVOC = maxValue(arrayTVOC_6H, validMeasures6H); // Berechnet den Minimalwert im Array für die ersten 290 Elemente) if (maxTVOC <= 80) { // maxCO2 muss immer größer als minCO2 sein, sonst stürzt der ESP32 ab maxTVOC = 80; } Serial.print("maxTVOC: "); Serial.println(maxTVOC); // Zeigt das Sechs-Stunden-Array für TVOC an if (validMeasures6H == 1) { // Der erste Messwert wird nur als Punkt dargestellt display.drawPixel((314), (map(arrayTVOC_6H[0], 0, maxTVOC, 373, 253)), GxEPD_BLACK); } else { // Ab dem zweiten Messwert werden alle Werte mit Linien verbunden for (int i=0; i < validMeasures6H - 1; i++) { display.drawLine((314 - i), (map(arrayTVOC_6H[i], 0, maxTVOC, 373, 253)), (314 - i - 1), (map(arrayTVOC_6H[i + 1], 0, maxTVOC, 373, 253)), GxEPD_BLACK); } } // Zeichnet das Grid displayGrid (0, 254, 0, maxTVOC, "Organische Verbindungen", arrayTVOC_6H[0], "ppb"); // (offsetX, offsetY, minTVOC, maxTVOC, Titel, aktueller Messwert, Einheit) // Anzeige der Feinstaubwerte 10 ug/m3 displayGrid (320, 99, 00, 40, "Feinstaub 10", arrayCO2_6H[0], "ug/m3"); // Anzeige der Feinstaubwerte 2,5 ug/m3 displayGrid (320, 254, 0, 40, "Feinstaub 2.5", arrayTVOC_6H[0], "ug/m3"); display.update(); //Speichere die aktuelle Zeit in die zughörige Variable previousMillisDisplay = currentMillis; } } //######################################### Functions ############################################## // Richtet Ziffern rechtsbündig aus, indem Leerzeichen vorangestellt werden void displayprintAlignRight(int digits) { if(digits < 10) { display.print(" "); } else if (digits < 100) { display.print(" "); } else if (digits < 1000) { display.print(" "); } display.print(digits); } // Verschiebt alle Elemente in dem Array um eine Position nach rechts und fügt links einen neuen Unsigned-Integer-Wert hinzu void addUnsignedIntegerToArray (unsigned int arrayName[], unsigned int value) { for (int i = 289; i >= 1; i--) { // Länge des Arrays - 1! arrayName[i] = arrayName[i - 1]; } arrayName[0] = value; Serial.print("Array Test: "); for (int i=0; i < 290; i++) { Serial.print(arrayName[i]); Serial.print("; "); } Serial.println(); } // Verschiebt alle Elemente in dem Array um eine Position nach rechts und fügt links einen neuen Floating-Point-Wert hinzu void addFloatToArray (float arrayName[], float value) { for (int i = 289; i >= 1; i--) { // Länge des Arrays - 1! arrayName[i] = arrayName[i - 1]; } arrayName[0] = value; Serial.print("Array Test: "); for (int i=0; i < 290; i++) { Serial.print(arrayName[i]); Serial.print("; "); } Serial.println(); } // Bestimmt den Maximalwert in einem Array: "scope" gibt den Bereich an (beginnend bei 0) unsigned int maxValue (unsigned int arrayName[], int scope) { unsigned int maxValue; unsigned int kmax = 0; unsigned int max = 0; for (int k = 0; k <= scope; k++) { if (arrayName[k] > max) { max = arrayName[k]; kmax = k; } } return maxValue = arrayName[kmax]; } // Bestimmt den Minimalwert in einem Array: "scope" gibt den Bereich an (beginnend bei 0) unsigned int minValue (unsigned int arrayName[], int scope) { unsigned int minValue; unsigned int kmin = 0; unsigned int min = 60000; for (int k = 0; k < scope; k++) { if (arrayName[k] < min) { min = arrayName[k]; kmin = k; } } return minValue = arrayName[kmin]; } // Berechnet einen Korrekturwert für den Sensor SGP30 uint32_t getAbsoluteHumidity(float temperature, float humidity) { // approximation formula from Sensirion SGP30 Driver Integration chapter 3.15 const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3] const uint32_t absoluteHumidityScaled = static_cast(1000.0f * absoluteHumidity); // [mg/m^3] return absoluteHumidityScaled; } // Statusleiste void displayStatus () { display.setFontMode(1); display.setBackgroundColor(GxEPD_WHITE); display.setForegroundColor(GxEPD_BLACK); display.setFont(u8g2_font_helvR08_tf); display.setCursor(1, 10); display.print("IP: 000.000.000.000"); display.setCursor(255, 10); display.print("Sonntag, der 01:01:2018"); if (validMeasures6H < 1) { display.setCursor(536, 10); display.print("Erste Messung läuft ..."); } else { display.setCursor(505, 10); display.print("Aktualisiert um 00:00:00 Uhr"); } display.drawLine(0, 12, 640, 12, GxEPD_BLACK); } // Gibt Einzelwerte aus void displayValue (int offsetX, int offsetY, String title, float currentValue, String unit) { display.setFontMode(1); display.setBackgroundColor(GxEPD_WHITE); display.setForegroundColor(GxEPD_BLACK); display.setCursor(offsetX + 1, offsetY + 10); display.setFont(u8g2_font_helvB08_tf); display.print(title); display.setCursor(offsetX + 1, offsetY + 29); display.setFont(u8g2_font_helvB14_tf); display.print(currentValue, 1); if (currentValue > 0.0) { display.setCursor(offsetX + 30, offsetY + 24); Serial.println("currentValue > 0"); } if (currentValue > 10.0) { display.setCursor(offsetX + 40, offsetY + 24); Serial.println("currentValue > 10"); } if (currentValue > 100.0) { display.setCursor(offsetX + 50, offsetY + 24); Serial.println("currentValue > 100"); } if (currentValue > 1000.0) { display.setCursor(offsetX + 60, offsetY + 24); Serial.println("currentValue > 1000"); } display.setFont(u8g2_font_helvB08_tf); display.print(unit); } // Zeichnet das Grid, Hilfslinien und die Skalierung void displayGrid (int offsetX, int offsetY, unsigned int minY, unsigned int maxY, String title, unsigned int currentValue, String unit) { // offsetX bezieht sich auf das erste Pixel von links; offsetY auf die oberste Linie des Rahmens // Überschrift display.setForegroundColor(GxEPD_BLACK); display.setFont(u8g2_font_7x13B_mr); display.setCursor(offsetX + 25, offsetY - 5); display.print(title); display.setCursor(offsetX + 248, offsetY - 5); displayprintAlignRight(currentValue); display.print(unit); // Rahmen display.drawRect(offsetX + 25, offsetY, 290, 120, GxEPD_BLACK); // Kasten //Hilfslinien for (int i = offsetX + 30; i < offsetX + 310; i = i + 6) { display.drawLine(i, offsetY + 30, (i + 3), offsetY + 30, GxEPD_BLACK); // Hilfslinie 75% display.drawLine(i, offsetY + 60, (i + 3), offsetY + 60, GxEPD_BLACK); // Hilfslinie 50% display.drawLine(i, offsetY + 90, (i + 3), offsetY + 90, GxEPD_BLACK); // Hilfslinie 25% } // Beschriftung y-Achse display.setFont(); display.setTextSize(1); display.setCursor(offsetX, offsetY- 4); // Beschriftung 100% displayprintAlignRight(maxY); display.setCursor(offsetX, offsetY + 26); // Beschriftung 75% displayprintAlignRight(((maxY - minY) * 0.75) + minY); display.setCursor(offsetX, offsetY + 56); // Beschriftung 75% displayprintAlignRight(((maxY - minY) * 0.5) + minY); display.setCursor(offsetX, offsetY + 86); // Beschriftung 50% displayprintAlignRight(((maxY - minY) * 0.25) + minY); display.setCursor(offsetX, offsetY + 116); // Beschriftung 0% displayprintAlignRight(minY); // Beschriftung x-Achse display.setFont(); display.setTextSize(1); display.setCursor(offsetX + 25, offsetY + 123); display.println("Stunden"); display.setCursor(offsetX + 120, offsetY + 123); display.println("-4"); display.setCursor(offsetX + 216, offsetY + 123); display.println("-2"); display.setCursor(offsetX + 312, offsetY + 123); display.println("0"); }