Make@Thon – CO2 Ampel

Make@Thon – CO2 Ampel

Ich habe am vierten Open Photonik Make@thon teilgenommen und mit einem coolen Team eine kleine CO2 Ampel Entwickelt. Die Veranstalter haben dafür ein wenig etwas springen lassen und folgendes Paket geschnürt:

Quicklinks

Das hier ist ein längerer Beitrag, daher sind hier ein paar Quicklinks zu den entsprechenden Abschnitten:

Hintergrund

Vor dem Hintergrund der Corona-Pandemie wurde das Thema des Make@thon gewählt. Da der CO2 Gehalt in der Raumluft mit der Aerosolkonzentration korreliert, macht es Sinn diesen zu Messen und bei kritischen Grenzwerten zu Lüften. Wer mehr dazu lesen möchte, findet <hier/> einen guten Startpunkt. Dort sind auch weitere Studien und Risikobewertungen verlinkt.

Besonders Einrichtungen, bei denen viele Menschen zusammen kommen, ist das gezielte Monitoring der CO2 Konzentration in der Atemluft daher Sinnvoll. Zieleinrichtung des Wettbewerbs waren daher Schulen, da diese Gebäude häufig über keine Lüftungsanlagen, wie beispielsweise Krankenhäuser, verfügen. In Schulen und Hochschulen muss von Hand gelüftet werden. Die Veranstalter konnten sogar eine Schule in Cloppenburg als Kooperationspartner gewinnen.

Aufgabenstellung

Ziel des Wettbewerbs war es daher ein System für das effiziente und kostengünstige CO2 Monitoring zu entwickeln. Es sollte also in jedem Klassenraum eine CO2 Ampel geben, die vor Ort ein Feedback gibt, ob gelüftet werden muss oder nicht. Darüberhinaus wäre eine zentrale Datensammlung Sinnvoll, um die gemessenen Werte im Sinne von Open Science weiter auszuwerten. Außerdem wurde das Kriterium Innovation eingeführt. Hier kann man also weitere Ideen eigenständig hinzugeben, welche in einer Art Ausblick formuliert werden.

Lösungskonzept

Unser Team hat sich für den folgenden Ansatz entschieden:

  • Autarke CO2 Ampel mit direktem, aber einfachemn Feedback (kein Genauer CO2 Wert, sondern einfacher Hinweis, Lüften / nicht Lüften.
  • Übertragung der Daten via LoRa an ein zentrales Gateway in der Schule und Sammeln der Daten für die Schule.
  • Weiterleiten der gesammelten Daten via Internet an Server und lagerung in einer InfluxDB und Aufbereitung via Grafana.
  • Datenabgleich mit GeoTags und Infektionszahlen. Darstellung einer Heatmap.
  • Man könnte noch die tatsächliche Lüftungsdauer mit Fensterkontakten messen.
  • Außerdem wollten wir die Motivation der Partizipation der Schüler anregen, in dem wir einen Gamification Ansatz einbinden. Das heißt es sollte eine Art Leaderboard geben mit dem besten Klassenraum.

Weitere wichtige Kriterien für uns waren eine möglichst einfache Einrichtung, da die Nutzer nicht unbedingt IT-Spezialisten sind. Daher wollten wir ein automatisiertes SetUp Script erstellen.

Der Wettbewerb

Freitag um 18:00 ging es mit einer Zoom-Konferenz los. Alle Teilnehmer wurden gebrieft und spontan haben sich Teams entwickelt.

Ich durfte zusammen mit zwei Studierenden aus der HS Emden Leer und einem sehr aktiven Maker aus Nürnberg arbeiten. Wir haben uns in unseren Vorkenntnissen und Fertigkeiten optimal ergänzt, wie ich finde.

Im Anschluss an das Briefing haben wir im Team eine kurze Vorstellungsrunde gemacht und ein erstes Brainstorming durchgeführt. Daraus ist das oben beschriebene Lösungskonzept entstanden. Dann haben wir die Aufgaben aufgeteilt und losgelegt.

Die CO2 Ampel

Das Hirn unserer CO2 Ampel ist das Heltec Board. Es ist mit einem ESP32 ausgestattet, verfügt über ein OLED Display und kann direkt LoRa senden. Wir möchten in der Arduino IDE arbeiten. Also brauchen wir zunächst einmal die korrekten Bibliotheken und Board Daten. Wer noch nie was mit Arduino gemacht hat.. naja. Vll versteht man es ja.

Heltec in Arduino IDE

Dazu öffnen wir einmal die Voreinstellungen in der Arduino IDE (Strg + , ) und fügen den folgenden Link in das Feld für die externen Quellen ein.

https://resource.heltec.cn/download/package_heltec_esp32_index.json

Dann können wir in den Boardmanager gehen und dort im Suchfeld das Paket für die Heltec Boards suchen.

Außerdem gibt es noch eine Heltec Bibliothek. Also geh es für uns weiter in die Bibliothekverwaltung. Wir nutzen ein ESP32 Board, also installieren wir die entsprechende Bibliothek.

Damit die Arduino IDE nun auch korrekt Compiliert müssen wir das Board noch auswählen:

Nun brauchen wir noch ein Micro USB Kabel. Achtet darauf dass es nicht nur ein Ladekabel ist. Wenn das Board an geht aber der PC es nicht erkennt, ist die Wahrscheinlichkeit hoch, dass es kein Datenkabel ist. Dann kann es auch schon fast mit der Programmierung losgehen.

SCD30 in Arduino IDE

Wir möchten ja auch unseren nagelneuen SCD30 Sensor auslesen können. Dazu braucht es die entsprechende Bibliothek, die freundlicherweise von Sparkfun bereitgestellt wird. Also klicken wir uns wieder in die Bibliotheksverwaltung und suchen einfach SCD30.

Der SCD30 ist ein sogenannter NDIR Sensor.

OLED Display in Arduino IDE

Für die Ansteuerung des OLED Displays kann man verschiedene Bibliotheken verwenden. Zunächst hatten wir eine genutzt, bei der aber entweder das Display oder der Sensor genutzt werden konnte. Leider konnten wir das Problem nicht so schnell debuggen, so das wir einfach auf eine andere Bibliothek gewechselt sind. Im Wiki des Hackerspace Frankfurt gibt es weitere Infos.

Der Code – SCD30

Damit sind die Grundsteine gelegt. Als erstes wollen wir den Sensor ans Laufen bringen und mit dem Seriellen Monitor prüfen ob die Werte logisch sind. Wir fangen also mit den includes an.

//--- Include Für CO2 Sensor-----
#include "SparkFun_SCD30_Arduino_Library.h"

Mit dem SCD30 Befehl wird die Bibliothek angewisen, das Objekt scdSensor anzulegen.

//--- Initialize SCD30 ---
SCD30 scdSensor;

Dann brauchen wir noch Grenzwerte für die CO2 Konzentration in der Luft. Diese werte stammen aus den Studien. Diese können Platzsparend mit #defines festgelegt werden, weil die sich nicht ändern.

//--- Grenzwerte für CO2 Ampel ---
#define CO2_THRESHOLD_LOW 600        //ppm
#define CO2_THRESHOLD_MEDIUM 1000
#define CO2_THRESHOLD_HIGH 1500

#defines weist den Compiler an im Grunde eine “Suchen und Ersetzen” Operation durchzuführen. Überall wo das erste Argument steht, wird das zweite eingefügt.

Weiterhin haben wir im Sensor den Druckluft, ein Temperaturoffset und die Höhe über Normal Null.


//--- Sensor Config ---
#define CLOPPENBURG 40 //Höhenmeter in Cloppenburg 
#define AMBIENT_PRESSURE 1000 //Normaldruck
#define TEMP_OFFSET 0 //Offset zu 0°C

Außderdem können wir einen Messintervall nutzen. Dazu brauchen wir Timervariablen. Man kann eine Art Mutlitasking aufbauen, wenn man auf delay() verzichtet.

//--- Timer ---
const unsigned long messIntervall = 5000; //entspricht 5 sekunden
unsigned long previousMillisMeasurement; //alte Zeit

Natürlich brauchen wir noch Variablen in denen wir die Sensorwerte speichern. Hier bieten sich Fließkommazahlen an.

float co2_new, temperature_new, humidity_new;

Das SetUp des Sensors habe ich in eine eigene Funktion ausgegliedert, die in der void setup aufgerufen wird.

void scdSensorSetup() {
  Serial.println("scdSensorSetup: Initialisiere CO2 Sensor....");

  Wire.begin;
  while (scdSensor.begin) == false) {
    Serial.println("Sensor wurde nicht Erkannt. Bitte Anschluss prüfen. Stoppe...");
  }

  // Konfiguration
  scdSensor.setAltitudeCompensation(CLOPPENBURG);
  scdSensor.setAmbientPressure(AMBIENT_PRESSURE); //Current ambient pressure in mBar
  scdSensor.setTemperatureOffset(TEMP_OFFSET); //set the temperature offset to 0°C
}

Genauso auch das eigentliche lesen des Sensors. Die Serial.println() dient zum Debuggen im Seriellen Monitor, die if prüft ob der Sensor daten bereitgestellt hat und überträgt diese dann in die globalen Variablen.

void readSensor() {
  Serial.println("readSensor: Lese CO2 Sensor....");


  if (scdSensor.dataAvailable()) {
  // SCD30 Sensor abfragen
      co2_new         = scdSensor.getCO2();
      temperature_new = scdSensor.getTemperature();
      humidity_new    = scdSensor.getHumidity();
      printToSerial(co2_new, temperature_new, humidity_new);
  }

}

Die gemessenen werte gebe ich dann auf den Seriellen Monitor:

void printToSerial( float co2, float temperature, float humidity) {
  Serial.print("co2(ppm):");
  Serial.print(co2, 1);
  Serial.print(" temp(°C):");
  Serial.print(temperature, 1);
  Serial.print(" humidity(%):");
  Serial.print(humidity, 1);
  Serial.println();
}

Nun kommen die Arduino-Standard Funktionen void setup und void loop. Wir wollen den Seriellen Monitor öffnen, damit wir die Werte lesen können. Eine kleine Begrüßung hat noch nie geschadet. Und dann rufen wir noch das Sensor Setup auf.

void setup() {
  //Öffne Seriellen Monitor mit Baudrate
  Serial.begin(115200);

  //Begrüßung
  Serial.println("Make4thon CO2 Ampel: Erfasse CO2-Gehalt, Temperatur, Feuchtigkeit");

  // ---------- SCD30 Setup -------------
  scdSensorSetup();
}

In der Loop wollen wir erstmal nur immer wieder den Sensor auslesen. Aber nicht so oft, wie die Loop durchlaufen wird. Da der Sensor mindestens 2 Sekunden braucht, bis er neue Daten liefert. Daher kommt hier der Teil, der eine Art Multitasking ermöglicht.

void loop() {
   if (millis() - previousMillisMeasurement >= messIntervall) {
    previousMillisMeasurement = millis();
      readSensor();
}

Ampel / RGB LED

Als nächstes implementieren wir die RGB LED. Eine RGB LED ist im Prinzip eine LED, die aus Dreien besteht. Jeder Pin hat eine Farbe, durch PWM kann man die Helligkeit der Einzelfarben bestimmen und so alle Farben mischen. Diese soll je nach CO2-Konzentration eine Farbe darstellen.

  • Geringe CO2 Konzentration: Grün
  • Mittlere CO2 Konzentration: Gelb
  • Hohe CO2 Konzentration: Rot

Wir brauchen also die LED Pins. Hier haben wir leider nicht die freie Auswahl, da das Display Pin 16 nach dem Reset auf HIGH Benötigt. Außerdem nutzt es Pin 15 und 4. Pin 21 und 22 sind durch den SCD30 belegt. Ich habe daher die folgenden genommen.

//--- RGB LED ---
const int pinRed = 12;
const int pinGreen = 14;
const int pinBlue = 13;

Der vierte Pin der LED geht auf GND. In der Void Setup müssen wir diese Pins dann als Output Festlegen. Außerdem Teste ich gerne kurz die Funktion mit einer kleinen for Schleife. Hier wird die LED Rot, gelb weiß da die pins nacheinander high geschaltet werden. Danach lila, blau und aus.

 pinMode(pinRed, OUTPUT);
 pinMode(pinGreen, OUTPUT);
 pinMode(pinBlue, OUTPUT);

//Teste RGB LED
  for (int i = 0; i < 5; i++) {
    digitalWrite(pinRed, HIGH);
    delay(100);
    digitalWrite(pinGreen, HIGH);
    delay(100);
    digitalWrite(pinBlue, HIGH);
    delay(100);
    digitalWrite(pinRed, LOW);
    delay(100);
    digitalWrite(pinGreen, LOW);
    delay(100);
    digitalWrite(pinBlue, LOW);
    delay(100);
  }

Schön wäre nun ein analogWrite, der ist aber für esp32 Boards nicht implementiert. Es gibt einen ähnlichen Befehl ledcWrite, aber der führt bei mir immer zu einem Core-Overload und Bootloop. Daher setzte ich einfach mit digitalWrite die Pins(). So können wir nur die Hauptfarben erzeugen. Wer hier eine Lösung weiß, darf gerne in die Kommentare posten.

Wir brauchen aber noch eine Funktion mit der wir die Messwerte gegen die Grenzwerte Prüfen und dann entsprechend die LED ansteuern. Dazu brauchen wir if funktionen.

void compareToThresholds() {
  Serial.println("compareToThresholds: Vergleiche mit Grenzwerten....");

  //Für RGB LED
  if (co2_new <= CO2_THRESHOLD_LOW) { //Code Blau 0,0,255
    Serial.println("compareToThresholds: Alles super....");
    digitalWrite(pinRed, LOW);
    digitalWrite(pinGreen, LOW);
    digitalWrite(pinBlue, HIGH);
  }
  if (co2_new <= CO2_THRESHOLD_MEDIUM) { //Code Grün 0,255,0
    Serial.println("compareToThresholds: LOW Überschritten....");
    digitalWrite(pinRed, LOW);
    digitalWrite(pinGreen, HIGH);
    digitalWrite(pinBlue, LOW);
    lstatusn = 3;
  }
  else if (co2_new >= CO2_THRESHOLD_MEDIUM) { //Code Gelb 255,255,0
    Serial.println("compareToThresholds: MEDIUM Überschritten....");
    digitalWrite(pinRed, HIGH);
    digitalWrite(pinGreen, HIGH);
    digitalWrite(pinBlue, LOW);
    lstatusn = 2;
  }
  else if (co2_new >= CO2_THRESHOLD_HIGH) {  //Code ROT 255,0,0
    Serial.println("compareToThresholds: HIGH Überschritten....");
    digitalWrite(pinRed, HIGH);
    digitalWrite(pinGreen, LOW);
    digitalWrite(pinBlue, LOW);
    lstatusn = 1;
  }

}

Diese rufen wir auch in der Loop auf und ergänzen sie zu folgendem code:

void loop() {
   if (millis() - previousMillisMeasurement >= messIntervall) {
    previousMillisMeasurement = millis();
      readSensor();
      compareToThresholds();
   }
}

Das ganze können wir nun Testen. Durch die Serial.print() Befehle können wir vergleichen ob die LED das tut, was wir erwarten und ob der Code die entsprechenden Bereiche erreicht. Wenn das klappt habt ihr bereits eine Fertige CO2 Ampel gebaut. Yeah!

OLED Display

Natürlich wäre es schön direkt auf dem kleinen OLED Display die Messwerte ablesen zu können. Dazu gehen wir wieder an den Anfang des Code und ergänzen die Includes

//--- DISPLAY-----
#include <Wire.h>
#include <U8x8lib.h>
U8X8_SSD1306_128X64_NONAME_SW_I2C display(15, 4, 16);

Ich würde gerne eine Art rotierende Slides haben, um die Werte immer etwa 3 Sekunden anzuzeigen. Dann ist das Display nicht so voll mit Infos. Dazu brauche ich eine Variable für die aktuelle Seite, eine Timervariable und die Anzeigedauer:

int displayNum = 0; //Verschiedene Displays zum Switchen / Sliden
unsigned long previousMillisDisplay; //alte Zeit
const unsigned long displayRotationPeriod = 3000; // 3 Sekunden bis zum nächsten Slide

In der void Setup() müssen wir dann noch die Display initialisierung hinzufügen:

// Reset signal for display
  pinMode(16, OUTPUT); digitalWrite(16, LOW); delay(50); digitalWrite(16, HIGH);

  //Initialisiere Display
  display.begin();
  display.clear();
  display.setFont(u8x8_font_7x14B_1x2_r);
  display.drawString(0, 1, "Make4thon CO2 Ampel: Erfasse CO2-Gehalt, Temperatur, Feuchtigkeit");

Außerdem habe ich eine Funktion geschrieben die für die Display Seiten sorgt: Diese nutzt einfach einen switch-case der die gewünschte seite Anzeigt. Dafür habe ich in der void Setup auch eine Testseite:

  //  //Test Screen
  printDisplay(9);
  delay(1000);
//----------------------------------- Funktion zum User Interface ---------------------
void printDisplay(int displayNum) {
  Serial.print("printDisplay: Rendere Anzeige: ");
  switch (displayNum)
  {
    case 0:
      display.clear();
      display.setCursor(0, 0);
      display.print("Make4thon");
      display.setCursor(0, 2);
      display.print("Erfasse CO2-Gehalt, :");
      display.setCursor(0, 4);
      display.print("Feuchtigkeit,:");
      display.setCursor(0, 6);
      display.print("und Temperatur:");
      break;
    case 1: // CO2
      display.clear();
      display.setCursor(0, 0);
      display.print("CO²-Gehalt");
      display.setCursor(8, 4);
      display.print(co2_new);
      display.setCursor(12, 4);
      display.print("ppm");
      break;
    case 2: // Temperatur
      display.clear();
      display.setCursor(0, 0);
      display.print("Temperatur");
      display.setCursor(8, 4);
      display.print(temperature_new);
      display.setCursor(12, 4);
      display.print("°C");
      break;
    case 3: // Feuchtigkeit
      display.clear();
      display.setCursor(0, 0);
      display.print("Luftfeuchtigkeit");
      display.setCursor(8, 4);
      display.print(humidity_new);
      display.setCursor(12, 4);
      display.print("%");
      break;
    case 7: // Lora Failed
      display.clear();
      display.setCursor(0, 0);
      display.print("Fehler");
      display.setCursor(0, 4);
      display.print("LoRa nicht verbunden");
      break;
    case 8: // Sensor nicht erkannt
      display.clear();
      display.setCursor(0, 0);
      display.print("Fehler");
      display.setCursor(0, 4);
      display.print("Sensor nicht erkannt");
      display.setCursor(0, 4);
      display.print("Verkabelung Prüfen");
      break;
    case 9: //Boot & Test
      display.clear();
      display.setCursor(0, 0);
      display.print("Booting");
      display.setCursor(0, 4);
      display.print("Bitte warten");
      display.setCursor(0, 4);
      display.print(".....");
      break;
  }
  Serial.println(displayNum);
  delay(10);
}

Dann müssen wir die Funktion nur noch mit der Entsprechenden Seite in der void loop aufrufen. Die kleine If funktion stellt sicher, dass ich nur zwischen Seite 1 und 3 durchscrolle. Ich habe im switch-case bewusst noch ein paar Seiten freigelassen, damit man leicht Seiten hinzufügen kann.

if (millis() - previousMillisDisplay >= displayRotationPeriod) {
    previousMillisDisplay = millis();
    if (displayNum >= 3) {
      displayNum = 0;
    }
    displayNum++;
    printDisplay(displayNum);
  }

Das ganze sieht bei mir jetzt wie folgt aus:

Daten senden

Zum Daten Senden haben wir nun zwei Optionen: Im Hack@thon haben wir mit LoRa an einen Empfänger gesendet, der die Sensordaten dann ins WLAN überträgt. Wir können aber auch direkt ins WLAN. Für mein SetUp Zuhause sende ich also meine Sensordaten direkt an meinen MQTT Broker und openHAB gießt diese dann in eine InfluxDB und Grafana stellt die Messwerte als Graph dar. Dazu habe ich ja schon ein paar Beiträge formuliert.

MQTT & Wifi

Zunächst müssen wir wieder die Arduino IDE vorbereiten und zwei neue Bibliotheken spendieren. Beide findet ihr über die Bibliotheksverwaltung:

Dann binden wir beides im include Bereich ein und erzeugen direkt die notwendigen Variablen:

//--- Wifi und MQTT Stuff ---
#include <WiFi.h>
#include <PubSubClient.h>

const char* ssid = "your ssid";
const char* password = "somepassword";
const char* mqttServer = "xxx.xxx.xxx.xxx";
const char* mqttUsername = "mqttUserName";
const char* mqttPassword = "mqttUserPW";
const char* mqttDeviceId = "CO2Ampel";

const char* subTopic = "ledcontrol";     //payload[0] will control/set LED
const char* pubTopic = "ledstate";       //payload[0] will have ledState value
const char* temperatureTopic = "Temperatur";
const char* humidityTopic = "Feuchtigkeit" ;
const char* co2Topic = "CO2Gehalt";
const char* statusTopic = "Status";
char* lstatus[] = {"nicht gestartet", "Lueften!", "Achtung!", "Luft OK!"};
int lstatusn = 0;

WiFiClient CO2Ampel;
PubSubClient client(CO2Ampel);

unsigned long lastMsg = 0;
const unsigned long publishIntervall = 5000;
unsigned long previousMillisPub; //alte Zeit

Um dem ESP zu sagen, er möge sich ins WLAN einloggen brauchen wir eine Funktion dafür:

//----------------------------------- SETUP WIFI ---------------------
void setup_wifi() {
  Serial.println("setup_wifi: Started... ");
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to: ");
  Serial.println(ssid);

  display.clear();
  display.setCursor(0, 0);
  display.print("WiFi connecting");
  display.setCursor(0, 2);
  display.print("to: ");
  display.setCursor(0, 3);
  display.print(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  display.clear();
  display.setCursor(0, 0);
  display.print("WiFi connected");
  display.setCursor(0, 2);
  display.print("IP address:");
  display.setCursor(0, 4);
  display.print(WiFi.localIP());
}

Als nächstes haben wir noch drei Funktionen für MQTT. Eine zum Verbinden und Reconnect falls die Verbindung abbrechen sollte. Eine zum Abonieren von Topics und eine zum Publishen in Topics. Wir steuern die interne LED mit einem Topic an. So könnte man z.b. indirekt testen ob die Nachrichten am Broker angekommen sind.

//----------------------------------- RECONNECT MQTT ---------------------
void reconnect() {
  // Loop until we're reconnected
  Serial.println("reconnect: ");
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    
    display.clear();
    display.setCursor(0, 0);
    display.print("Attempting MQTT connection...");

    // Attempt to connect
    if (client.connect(mqttDeviceId, mqttUsername, mqttPassword)) {
      Serial.println("connected");
      
      display.clear();
      display.setCursor(0, 0);
      display.print("connected");
      
      client.subscribe(subTopic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      
      display.clear();
      display.setCursor(0, 0);
      display.print("failed, rc= ");
      display.setCursor(8, 0);
      display.print(client.state());
      display.setCursor(0, 2);
      display.print(" try again in 5 seconds");
      
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

//----------------------------------- CALLBACK MQTT ---------------------
void callback(char* topic, byte* message, unsigned int length) {
  Serial.println("callback: ");
  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");
  String messageTemp;

  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
    messageTemp += (char)message[i];
  }
  Serial.println();

  // Feel free to add more if statements to control more GPIOs with MQTT

  // If a message is received on the topic esp32/output, you check if the message is either "on" or "off".
  // Changes the output state according to the message
  if (String(topic) == "esp32/output") {
    Serial.print("Changing output to ");
    if (messageTemp == "on") {
      Serial.println("on");
      digitalWrite(ledPin, HIGH);
    }
    else if (messageTemp == "off") {
      Serial.println("off");
      digitalWrite(ledPin, LOW);
    }
  }
}

//----------------------------------- Publish MQTT ---------------------
void publishMQTT() {
  Serial.println("publishMQTT: Sende Nachrichten...");
  unsigned long now = millis();
  if (now - lastMsg > 5000) { //Sendet alle 5 Sekunden
    lastMsg = now;

    char payLoad[1];
    itoa(ledState, payLoad, 10);
    client.publish(pubTopic, payLoad);
    client.publish(co2Topic, String(co2_new).c_str());
    client.publish(temperatureTopic, String(temperature_new).c_str());
    client.publish(humidityTopic, String(humidity_new).c_str());
    client.publish(statusTopic, lstatus[lstatusn]);

  }
}

Ob das ganze funktioniert können wir zum einen direkt auf dem OLED Display sehen, andererseits können wir wieder MQTTfx bemühen um nach den Topics zu schnüffeln.

Bei mir habe ich dann im OpenHAB noch ein entsprechendes MQTTthing angelegt und die Topics und Items verknüpft.

Über die Persistence wird jede Minute ein Wert in die InfluxDB gespeichert. Dazu habe ich in Grafana ein paar Diagramme angelegt. Details wie man das einrichtet gibt es in den oben verlinkten Beiträgen.

Wie man sieht ist der Messwert ab ca.15:00 konstant. Das liegt daran, dass das Heltec Board nicht so gut ins Breadboard passt und es leider öfter mal den Kontakt zu manchen Pins verliert. Wenn ich das Gehäuse fertig habe, werde ich das aber ändern.

Gesamter Sketch (MQTT)

//SCD30 Sensor on Heltec ESP32 LoRa V2 -> MQTT & Wifi
//-----------------------------------------------------------------

//--- DISPLAY-----
#include <Wire.h>  // Only needed for Arduino 1.6.5 and earlier
//#include "SSD1306Wire.h" // legacy include: `#include "SSD1306.h"`
#include <U8x8lib.h>
//SSD1306Wire  display(0x3c, 4, 15);
U8X8_SSD1306_128X64_NONAME_SW_I2C display(15, 4, 16);

//--- Wifi und MQTT Stuff ---
#include <WiFi.h>
#include <PubSubClient.h>

const char* ssid = "FabLab Thomasblock";
const char* password = "Sc13nc3B1tch";
const char* mqttServer = "192.168.0.100";
const char* mqttUsername = "openhabian";
const char* mqttPassword = "Sc13nc3B1tch";
const char* mqttDeviceId = "CO2Ampel";

const char* subTopic = "ledcontrol";     //payload[0] will control/set LED
const char* pubTopic = "ledstate";       //payload[0] will have ledState value
const char* temperatureTopic = "Temperatur";
const char* humidityTopic = "Feuchtigkeit" ;
const char* co2Topic = "CO2Gehalt";
const char* statusTopic = "Status";
char* lstatus[] = {"nicht gestartet", "Lueften!", "Achtung!", "Luft OK!"};
int lstatusn = 0;

WiFiClient DanielsESP;
PubSubClient client(DanielsESP);

unsigned long lastMsg = 0;

//--- CO2 SENSOR-----
#include "SparkFun_SCD30_Arduino_Library.h"
SCD30 scdSensor;

int displayNum = 0; //Verschiedene Displays mit Taste zum Switchen

//--- Grenzwerte für CO2 Ampel ---
#define CO2_THRESHOLD_LOW 600
#define CO2_THRESHOLD_MEDIUM 1000
#define CO2_THRESHOLD_HIGH 1500

#define CLOPPENBURG 40 //Höhenmeter in Cloppenburg 

//--- Speichervariablen für Sensorwerte---
float co2_new, temperature_new, humidity_new;

//--- Timer ---
unsigned long previousMillisMes; //alte Zeit
unsigned long previousMillisPub; //alte Zeit
unsigned long previousMillisDisplay; //alte Zeit

const unsigned long displayRotationPeriod = 3000;
const unsigned long messIntervall = 5000;
const unsigned long publishIntervall = 5000;

//--- Interne LED (als Ersatz für RGB für Debug Zwecke) ---
const int ledPin = 25;
int ledState = 0;

//--- RGB LED ---
const int pinRed = 12;
const int pinGreen = 14;
const int pinBlue = 13;

//Test
#define TEST false

//---------------------------EIGENE FUNKTIONEN--------------------------------
//----------------------------------- SETUP WIFI ---------------------
void setup_wifi() {
  Serial.println("setup_wifi: Started... ");
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to: ");
  Serial.println(ssid);

  display.clear();
  display.setCursor(0, 0);
  display.print("WiFi connecting");
  display.setCursor(0, 2);
  display.print("to: ");
  display.setCursor(0, 3);
  display.print(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  //randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  display.clear();
  display.setCursor(0, 0);
  display.print("WiFi connected");
  display.setCursor(0, 2);
  display.print("IP address:");
  display.setCursor(0, 4);
  display.print(WiFi.localIP());
}

//----------------------------------- RECONNECT MQTT ---------------------
void reconnect() {
  // Loop until we're reconnected
  Serial.println("reconnect: ");
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    
    display.clear();
    display.setCursor(0, 0);
    display.print("Attempting MQTT connection...");

    // Attempt to connect
    if (client.connect(mqttDeviceId, mqttUsername, mqttPassword)) {
      Serial.println("connected");
      
      display.clear();
      display.setCursor(0, 0);
      display.print("connected");
      
      client.subscribe(subTopic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      
      display.clear();
      display.setCursor(0, 0);
      display.print("failed, rc= ");
      display.setCursor(8, 0);
      display.print(client.state());
      display.setCursor(0, 2);
      display.print(" try again in 5 seconds");
      
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

//----------------------------------- CALLBACK MQTT ---------------------
void callback(char* topic, byte* message, unsigned int length) {
  Serial.println("callback: ");
  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");
  String messageTemp;

  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
    messageTemp += (char)message[i];
  }
  Serial.println();

  // Feel free to add more if statements to control more GPIOs with MQTT

  // If a message is received on the topic esp32/output, you check if the message is either "on" or "off".
  // Changes the output state according to the message
  if (String(topic) == "esp32/output") {
    Serial.print("Changing output to ");
    if (messageTemp == "on") {
      Serial.println("on");
      digitalWrite(ledPin, HIGH);
    }
    else if (messageTemp == "off") {
      Serial.println("off");
      digitalWrite(ledPin, LOW);
    }
  }
}
//----------------------------------- Publish MQTT ---------------------
void publishMQTT() {
  Serial.println("publishMQTT: Sende Nachrichten...");
  unsigned long now = millis();
  if (now - lastMsg > 5000) { //Sendet alle 5 Sekunden
    lastMsg = now;

    char payLoad[1];
    itoa(ledState, payLoad, 10);
    client.publish(pubTopic, payLoad);
    client.publish(co2Topic, String(co2_new).c_str());
    client.publish(temperatureTopic, String(temperature_new).c_str());
    client.publish(humidityTopic, String(humidity_new).c_str());
    client.publish(statusTopic, lstatus[lstatusn]);

  }
}

//----------------------------------- Funktion zum Einrichten des CO² Sensor ---------------------
void scdSensorSetup() {
  Serial.println("scdSensorSetup: Initialisiere CO2 Sensor....");

  // initialize I2C
  Wire.begin();
  while (scdSensor.begin() == false) {
    Serial.println("Sensor wurde nicht Erkannt. Bitte Anschluss prüfen. Stoppe...");
    printDisplay(8);
    delay(1000);

    //FehlerCode LED Weiß
    digitalWrite(pinRed, HIGH);
    digitalWrite(pinGreen, HIGH);
    digitalWrite(pinBlue, HIGH);
  }

  // Kompensation des Luftdrucks durch Höhe über NN
  //delay(1000); //<- Braucht man den Delay wirklich?
  scdSensor.setAltitudeCompensation(CLOPPENBURG);

  float T_offset = scdSensor.getTemperatureOffset();
  Serial.print("Aktuelles Temperatur Offset: ");
  Serial.print(T_offset, 2);
  Serial.println("°C");

  // Pinbelegung beachten
  scdSensor.setTemperatureOffset(7);
}

//----------------------------------- Funktion zum Auslesen des CO² Sensor ---------------------
void readSensor() {
  Serial.println("readSensor: Lese CO2 Sensor....");

  //falls Sensor Daten hat - übergeben
  if (scdSensor.dataAvailable()) {
    Serial.println("readSensor: Daten bereit");
    // SCD30 Sensor abfragen
    co2_new         = scdSensor.getCO2();
    temperature_new = scdSensor.getTemperature();
    humidity_new    = scdSensor.getHumidity();
    printToSerial(co2_new, temperature_new, humidity_new);
  }
  else {
    Serial.println("readSensor: Noch keine Daten bereit");
  }
}


//----------------------------------- Funktion zum Ausgeben der Sensorwerte an Seriellen Monitor ---------------------
void printToSerial( float co2, float temperature, float humidity) {
  Serial.println("Daten erfasst....");
  Serial.print("co2(ppm):");
  Serial.print(co2, 1);
  Serial.print(" temp(°C):");
  Serial.print(temperature, 1);
  Serial.print(" humidity(%):");
  Serial.print(humidity, 1);
  Serial.println();
}

//----------------------------------- Funktion zum Vergleichen mit Grenzwerten ---------------------
void compareToThresholds() {
  Serial.println("compareToThresholds: Vergleiche mit Grenzwerten....");

  //Für RGB LED
  if (co2_new <= CO2_THRESHOLD_LOW) { //Code Blau 0,0,255
    digitalWrite(pinRed, LOW);
    digitalWrite(pinGreen, LOW);
    digitalWrite(pinBlue, HIGH);
  }
  if (co2_new <= CO2_THRESHOLD_MEDIUM) { //Code Grün 0,255,0
    digitalWrite(pinRed, LOW);
    digitalWrite(pinGreen, HIGH);
    digitalWrite(pinBlue, LOW);
    lstatusn = 3;
  }
  else if (co2_new >= CO2_THRESHOLD_MEDIUM) { //Code Gelb 255,255,0
    digitalWrite(pinRed, HIGH);
    digitalWrite(pinGreen, HIGH);
    digitalWrite(pinBlue, LOW);
    lstatusn = 2;
  }
  else if (co2_new >= CO2_THRESHOLD_HIGH) {  //Code ROT 255,0,0
    digitalWrite(pinRed, HIGH);
    digitalWrite(pinGreen, LOW);
    digitalWrite(pinBlue, LOW);
    lstatusn = 1;
  }

}

//----------------------------------- Funktion zum User Interface ---------------------
void printDisplay(int displayNum) {
  Serial.print("printDisplay: Rendere Anzeige: ");
  switch (displayNum)
  {
    case 0:
      display.clear();
      display.setCursor(0, 0);
      display.print("Make4thon");
      display.setCursor(0, 2);
      display.print("Erfasse CO2-Gehalt, :");
      display.setCursor(0, 4);
      display.print("Feuchtigkeit,:");
      display.setCursor(0, 6);
      display.print("und Temperatur:");
      break;
    case 1: // CO2
      display.clear();
      display.setCursor(0, 0);
      display.print("CO²-Gehalt");
      display.setCursor(8, 4);
      display.print(co2_new);
      display.setCursor(12, 4);
      display.print("ppm");
      break;
    case 2: // Temperatur
      display.clear();
      display.setCursor(0, 0);
      display.print("Temperatur");
      display.setCursor(8, 4);
      display.print(temperature_new);
      display.setCursor(12, 4);
      display.print("°C");
      break;
    case 3: // Feuchtigkeit
      display.clear();
      display.setCursor(0, 0);
      display.print("Luftfeuchtigkeit");
      display.setCursor(8, 4);
      display.print(humidity_new);
      display.setCursor(12, 4);
      display.print("%");
      break;
    case 7: // Lora Failed
      display.clear();
      display.setCursor(0, 0);
      display.print("Fehler");
      display.setCursor(0, 4);
      display.print("LoRa nicht verbunden");
      break;
    case 8: // Sensor nicht erkannt
      display.clear();
      display.setCursor(0, 0);
      display.print("Fehler");
      display.setCursor(0, 4);
      display.print("Sensor nicht erkannt");
      display.setCursor(0, 4);
      display.print("Verkabelung Prüfen");
      break;
    case 9: //Boot & Test
      display.clear();
      display.setCursor(0, 0);
      display.print("Booting");
      display.setCursor(0, 4);
      display.print("Bitte warten");
      display.setCursor(0, 4);
      display.print(".....");
      break;
  }
  Serial.println(displayNum);
  delay(10);
}

// -------------------------ARDUINO SETUP---------------------------------------

void setup() {
  //Öffne Seriellen Monitor mit Baudrate
  Serial.begin(115200);

  //Begrüßung
  Serial.println("Make4thon CO2 Ampel: Erfasse CO2-Gehalt, Temperatur, Feuchtigkeit");

  // Reset signal for display
  pinMode(16, OUTPUT); digitalWrite(16, LOW); delay(50); digitalWrite(16, HIGH);

  //Initialisiere Display
  display.begin();
  display.clear();
  display.setFont(u8x8_font_7x14B_1x2_r);
  display.drawString(0, 1, "Make4thon CO2 Ampel: Erfasse CO2-Gehalt, Temperatur, Feuchtigkeit");

  //setting up the wifi & mqtt stuff
  setup_wifi();
  client.setServer(mqttServer, 1883);
  client.setCallback(callback);

  //Init LED
  pinMode(ledPin, OUTPUT);

  //Teste LED
  for (int i = 0; i < 5; i++) {
    digitalWrite(LED, HIGH);
    delay(100);
    digitalWrite(LED, LOW);
    delay(100);
  }

  //Init RGB LED //MUss noch ggf. auf PWM Umgebaut werden
  pinMode(pinRed, OUTPUT);
  pinMode(pinGreen, OUTPUT);
  pinMode(pinBlue, OUTPUT);

  //Teste RGB LED
  for (int i = 0; i < 5; i++) {
    digitalWrite(pinRed, HIGH);
    delay(100);
    digitalWrite(pinGreen, HIGH);
    delay(100);
    digitalWrite(pinBlue, HIGH);
    delay(100);
    digitalWrite(pinRed, LOW);
    delay(100);
    digitalWrite(pinGreen, LOW);
    delay(100);
    digitalWrite(pinBlue, LOW);
    delay(100);
  }

  //  //Test Screen
  printDisplay(9);
  delay(1000);

  // SCD30 Setup
  scdSensorSetup();
}

// -----------------------------ARDUINO LOOP------------------------------
void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  
  if (millis() - previousMillisMes >= messIntervall) {
    previousMillisMes = millis();
    readSensor();
    compareToThresholds();
  }  
  if (millis() - previousMillisPub >= publishIntervall) {
    previousMillisPub = millis();
    publishMQTT();
  }
  if (millis() - previousMillisDisplay >= displayRotationPeriod) {
    previousMillisDisplay = millis();
    if (displayNum >= 3) {
      displayNum = 0;
    }
    displayNum++;
    printDisplay(displayNum);
  }   
}

LoRa

Tja, LoRa ist auch für mich absolutes Neuland. In der Biblitothek gibt es ein paar Beispielsketches die ich mir angeschaut habe. Für unseren Arduino sketch (nehmt die Version ganz unten, das ist der Fertige sketch) habe ich den MQTT und Wifi Code rausgelassen. Bei der LoRa Variante wird der MQTT Code nämlich vom LoRa Empfänger übernommen. Wir haben also gleich zwei Boards am Laufen. Einen LoRa Sender und einen LoRa Empfänger.

Diese Teilaufgabe hat im Hackathon auch jemand anderes übernommen. Ich durchdringe nicht ganz warum was passiert aber ich versuche zu erklären was was ist.

IDE

Zunächst spendieren wir der IDE wieder die entsprechende Bibliothek “LoRa”.

Diese binden wir mit einem Include, ganz oben im Sketch ein.

// Lora
#include <SPI.h>
#include <LoRa.h>

Im LoRa möchten wir unsere Daten gerne als JSON String senden. Das kenne ich schon ein bischen aus der Arbeit mit openHAB. Dazu gibt es eine Bibliothek ArduinoJson

Und wieder brauchen wir den include Befehl.

// Json parser
#include <ArduinoJson.h>

1 Kommentar

Schreibe einen Kommentar