Momentan bietet IKEA für knapp EUR 10.- einen Luftqualitätssensor an, der mit drei verschiedenfarbigen LEDs die aktuelle Qualität der Raumlauft anzeigt. Dabei wird eine Partikelanalyse (PM2,5) durchgeführt und das entsprechende Resultat angezeigt.
Typ | E2014 VINDRIKTNING |
---|---|
Eingang | 5,0V DC; 2,0A; 0,5W; USB-C |
Partikelerkennungsbereich | 0~1000µg/m³ |
Max. USB-Kabellänge | 3,0m |
Betriebstemperatur | 0°C bis 40°C |
Betriebsfeuchtigkeit | 0 bis 95% RH (empfohlen: 40-60%) |
Laut einiger Artikel im WWW scheint der hier verwendete Partikelsensor eng verwandt mit dem
Cubic PM1006k zu sein.
Das Vindriktning bietet auf der Platine einige Test-Ausgänge, die man für weitere Analysen
verwenden kann. Der gemessene Wert der Luftqualität wird im Folgenden mit einem ESP32
ausgelesen, verarbeitet und auf einem kleinen OLED angezeigt.
Im folgenden Bild wird gezeigt, wie die Schaltung aufgebaut ist. Zu beachten ist, dass
die Eingänge des ESP32 nicht 5V-tolerant sind, d.h. mit 3,3V betrieben werden müssen.
Daher wird hier ein einfacher Spannungsteiler benutzt. (es ist auch möglich einen
Pegelwandler zu verwenden). Der Ausgang REST
des Vindriktning wird am Eingang GPIO16 (U2_RXD)
des ESP32 angeschlossen und kann
als serielle Schnittstelle (9600 Baud, 8N1) ausgelesen werden.
In der Arduino-IDE wird (wie in Beitrag ESP32 beschrieben)
auf das ESP32-Board umgestellt und die Libraries Adafruit_GFX, Adafruit_SSD1306
und EspSoftwareSerial über den Bibliotheks-Manager installiert.
Die Daten werden über eine serielle Schnittstelle ausgelesen, wobei ich mich nahe am Code
von Hypfer/esp8266-vindriktning-particle-sensor
orientiert habe. Der Sketch bindet noch zwei lokale header-Dateien ein, die im selben Ordner wie
der Sketch liegen müssen.
Zum Übertragen des Codes wird der ESP32 zunächst ohne den Vindriktning per Mikro-USB-Kabel an den PC
verbunden. Dieses Kabel nach der erfolgreichen Programmierung entfernt und dann der gesamte Aufbau per
USB-C-Kabel vom Vindriktning mit Strom versorgt.
struct SensorState
{
uint16_t avgPM25 = 0;
uint16_t measurements[5] = {0, 0, 0, 0, 0};
uint8_t measurementIdx = 0;
boolean valid = false;
};
#include <SoftwareSerial.h>
#include "Types.h"
namespace SerialCom
{
constexpr static const uint8_t PIN_UART_RX = 16; // GPIO16 = U2_RXD (ESP32)
constexpr static const uint8_t PIN_UART_TX = 17; // UNUSED
SoftwareSerial sensorSerial(PIN_UART_RX, PIN_UART_TX);
uint8_t serialRxBuf[255];
uint8_t rxBufIdx = 0;
void setup()
{
sensorSerial.begin(9600);
}
void clearRxBuf()
{
// Clear everything for the next message
memset(serialRxBuf, 0, sizeof(serialRxBuf));
rxBufIdx = 0;
}
void parseState(SensorState& state)
{
/**
* MSB DF 3 DF 4 LSB
* uint16_t = xxxxxxxx xxxxxxxx
*/
const uint16_t pm25 = (serialRxBuf[5] << 8) | serialRxBuf[6];
Serial.printf("Received PM 2.5 reading: %d\n", pm25);
state.measurements[state.measurementIdx] = pm25;
state.measurementIdx = (state.measurementIdx + 1) % 5;
if (state.measurementIdx == 0) {
float avgPM25 = 0.0f;
for (uint8_t i = 0; i < 5; ++i) {
avgPM25 += state.measurements[i] / 5.0f;
}
state.avgPM25 = avgPM25;
state.valid = true;
Serial.printf("New Avg PM25: %d\n", state.avgPM25);
}
clearRxBuf();
}
bool isValidHeader()
{
bool headerValid = serialRxBuf[0] == 0x16 && serialRxBuf[1] == 0x11 && serialRxBuf[2] == 0x0B;
if (!headerValid) {
Serial.println("Received message with invalid header.");
}
return headerValid;
}
bool isValidChecksum()
{
uint8_t checksum = 0;
for (uint8_t i = 0; i < 20; i++) {
checksum += serialRxBuf[i];
}
if (checksum != 0) {
Serial.printf("Received message with invalid checksum. Expected: 0. Actual: %d\n", checksum);
}
return checksum == 0;
}
void handleUart(SensorState& state)
{
if (!sensorSerial.available()) {
return;
}
Serial.print("Receiving:");
while (sensorSerial.available()) {
serialRxBuf[rxBufIdx++] = sensorSerial.read();
Serial.print(".");
// Without this delay, receiving data breaks for reasons that are beyond me
delay(15);
if (rxBufIdx >= 64) {
clearRxBuf();
}
}
Serial.println("Done.");
if (isValidHeader() && isValidChecksum()) {
parseState(state);
Serial.printf(
"Current measurements: %d, %d, %d, %d, %d\n",
state.measurements[0],
state.measurements[1],
state.measurements[2],
state.measurements[3],
state.measurements[4]
);
} else {
clearRxBuf();
}
}
}
Der eigentliche Sketch initialisiert die softwareseitige, serielle Schnittstelle sowie
das OLED über I²C und zeigt dann im loop()
alle 10 Sekunden den vom
Sensor gemessenen Wert der Luftqualität auf dem OLED an.
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "SerialCom.h"
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
SensorState state;
uint32_t next_report_millis = 0, t = 0;
void setup()
{
SerialCom::setup();
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
while (true);
}
display.clearDisplay();
display.display();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Air quality");
display.display();
delay(2000);
}
void loop()
{
SerialCom::handleUart(state);
t = millis();
if (t > next_report_millis) {
display.clearDisplay();
display.setCursor(0, 0);
next_report_millis = t + 10000;
if (state.valid) {
display.println(state.avgPM25);
} else {
display.println("?");
}
display.display();
}
}
Um nicht nur die aktuelle Qualität der Raumluft zu messen und analysieren, sondern die Sensordaten über die Zeit zu sammeln und somit eine Historie der Luftqualität eines Raumes zu erstellen, wird im Folgenden zunächst das bestehende Modul angezapft, die Daten an eine ESP32-Modul weitergeleitet und dann über WLAN an den IoT-Service ThingSpeak gesendet. (siehe dazu den vorhergehenden Beitrag "Sensordaten auslesen und nach ThingSpeak senden".
Der Aufbau der Schaltung ist dem verherigen sehr ähnlich, jedoch hab ich nicht mehr das Vindriktning über
USB-Anschluss mit Spannung versorgt, sondern den ESP32. Das Vindriktning wurde dann über +5V
auf VIN
an die Stromversorgung gehängt. Diese Anpassung habe ich gemacht, da der ESP32
dauern neugestartet ist, denn anscheinend ich die Stromstärke durch den Sensor zu wenig für die zusätzliche
Verwendung des WiFi-Moduls auf dem ESP32.
Außerdem habe ich den Spannungsteiler hier weggelassen, denn das Auslesen der Werte über die serielle
Schnittstelle hat hier Probleme bereitet. Vermutlich werde ich bei einem längeren Lauf des Sensors
einen Pegelwandler verwenden und hoffe, dass die Sensorwerte dann
stabil über die serielle Schnittstelle ausgelesen werden können.
struct SensorState
{
uint16_t avgPM25 = 0;
uint16_t measurements[5] = {0, 0, 0, 0, 0};
uint8_t measurementIdx = 0;
boolean valid = false;
};
Das anscheinend das WiFi-Modul nicht gleichzeitig mit dem SoftwareSerial zusammenarbeiten will, habe ich hier auf die zweite hardwareseitige Serial-Schnittstelle umgestellt. Dies hat ohne Probleme funktioniert. Außerdem sind die Debug-Ausgabe der ersten seriellen Schnittstelle rausgefallen.
#include "Types.h"
namespace SerialCom
{
constexpr static const uint8_t PIN_UART_RX = 16; // GPIO16 = U2_RXD (ESP32)
constexpr static const uint8_t PIN_UART_TX = 17; // UNUSED
uint8_t serialRxBuf[255];
uint8_t rxBufIdx = 0;
void setup()
{
Serial2.begin(9600, SERIAL_8N1, PIN_UART_RX, PIN_UART_TX);
}
void clearRxBuf()
{
memset(serialRxBuf, 0, sizeof(serialRxBuf));
rxBufIdx = 0;
}
void parseState(SensorState& state)
{
const uint16_t pm25 = (serialRxBuf[5] << 8) | serialRxBuf[6];
state.measurements[state.measurementIdx] = pm25;
state.measurementIdx = (state.measurementIdx + 1) % 5;
if (state.measurementIdx == 0) {
float avgPM25 = 0.0f;
for (uint8_t i = 0; i < 5; ++i) {
avgPM25 += state.measurements[i] / 5.0f;
}
state.avgPM25 = avgPM25;
state.valid = true;
}
clearRxBuf();
}
bool isValidHeader()
{
bool headerValid = serialRxBuf[0] == 0x16 && serialRxBuf[1] == 0x11 && serialRxBuf[2] == 0x0B;
return headerValid;
}
bool isValidChecksum()
{
uint8_t checksum = 0;
for (uint8_t i = 0; i < 20; i++) {
checksum += serialRxBuf[i];
}
return checksum == 0;
}
void handleUart(SensorState& state)
{
if (!Serial2.available()) {
return;
}
while (Serial2.available()) {
serialRxBuf[rxBufIdx++] = Serial2.read();
// Without this delay, receiving data breaks for reasons that are beyond me
delay(15);
if (rxBufIdx >= 64) {
clearRxBuf();
}
}
if (isValidHeader() && isValidChecksum()) {
parseState(state);
} else {
clearRxBuf();
}
}
}
Der Sketch ist im Vergleich zum Vorherigen nur um die WiFi- und HTTPClient-Library erweitert worden und es wird nun in regelmäßigen Abständen (alle 30 Sekunden) der Luftqualitätswert an ThingSpeak verschickt. Das OLED habe ich nur zur Überprüfung beibehalten und kann auch ohne Probleme weggelassen werden.
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "SerialCom.h"
#define OLED_WIDTH 128
#define OLED_HEIGHT 32
#define OLED_PIN_RESET 4
#define OLED_ADDRESS 0x3C
#define REPORT_DELAY 30 // in seconds
// WiFi settings
const char* WIFI_SSID = "xxxxxxxxxxx";
const char* WIFI_PASSWORD = "xxxxxxxxxxx";
// ThingSpeak settings
const char* THINGSPEAK_URL = "http://api.thingspeak.com/update";
const char* WRITE_API_KEY = "xxxxxxxxxxx";
#define lmillis() ((long)millis())
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_PIN_RESET);
SensorState state;
long nextReport = 0;
void setup()
{
SerialCom::setup();
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
while (true);
}
display.clearDisplay();
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Init...");
display.display();
nextReport = lmillis() + REPORT_DELAY * 1000;
}
void loop()
{
SerialCom::handleUart(state);
if (lmillis() - nextReport >= 0) {
nextReport = lmillis() + REPORT_DELAY * 1000;
display.clearDisplay();
display.setCursor(0, 0);
if (state.valid) {
display.println(state.avgPM25);
requestIot();
} else {
display.println("-");
}
display.display();
}
}
void connectWiFi()
{
if (WiFi.status() == WL_CONNECTED) {
return;
}
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void requestIot()
{
connectWiFi();
String request = String(THINGSPEAK_URL) + "?api_key=" + String(WRITE_API_KEY) + "&field1=" + String(state.avgPM25);
HTTPClient http;
http.begin(request.c_str());
http.GET();
http.end();
WiFi.disconnect();
}
Um den Sensor und die übertragenen Werte zu testen habe ich in dem Raum verschiedene Aktionen durchgeführt, die sich im nachfolgenden Diagram anhand der gemessenen Luftpartikelwerte wiederfinden lassen:
21:05 | Ein japanisches Räucherstäbchen wird entzündet |
---|---|
21:20 | Räucherstäbchen ist abgebrannt; Raum wird gelüftet |
21:25 | Ein Räucherkegel (Weihrauch) wird entzündet |
21:50 | Räucherkegel erloschen; Raum wird gelüftet |
Man sieht, dass das Räucherstäbchen die Luftpartikelwerte nicht einmal auf 100µg/m³ hochgehen lässt und durch das Lüften schnell wieder die Werte abfallen. Der Räucherkegel lässt aber die Partikel schnell bis über 320µg/m³ ansteigen.
Hinweis zum Stromverbrauch: Ohne irgendwelche Optimierung bzgl. DeepSleep o.ä. beläuft sich die durchschnittliche Stromaufnahme auf ca. 150mA bis 190mA, wobei davon etwa 100mA für das Vindriktning und 50-90mA auf den ESP32 entfallen. Will man also eine längere Zeit die Raumluft analysieren, so sollte man entweder eine kräftige Powerbank oder besser einen 5V-Netzadapter verwenden.