Der Feinstaubsensor SDS011 misst Feinstaub-Partikel mit Laserlicht. Er hat eine einfache serielle Schnittstelle, über die er Kommandos entgegennimmt und Messergebnisse liefert. Das Messprinzip ist abhängig von der Luftfeuchtigkeit, deshalb misst der Sensor auch noch die Luftfeuchtigkeit und Temperatur mit einem DHT11. (Ich weiß, der Sensor ist nicht toll, aber ich hatte keinen anderen.) Als Mikrocontroller verwende ich ein Heltec ESP32 LoRa Modul mit einem OLED-Display, den ich mit der Arduino-Umgebung programmiere.
Aufbau
Der Aufbau ist naheliegend. Der SDS011-Sensor benötigt eine serielle Schnittstelle. Da laut der Dokumentation zu der von mir ausgewählten SDS011-Bibliothek der ESP32 kein Software-Serial kann, muss der Feinstaubsensor an eine der beiden zugänglichen seriellen Schnittstellen des Heltec-Moduls angeschlossen werden. Die Schnittstelle 0 wird zum Programmieren des ESP32 benötigt, also bleibt eigentlich nur die Schnittstelle 2 übrig. Dummerweise hat Heltec den Reset des OLEDs OLED_RST
auf U2_RXD
gelegt. Glücklicherweise wird das OLED nicht benötigt, also verbinden wir TXD
des SDS011 mit U2_RXD
(GPIO16
des Heltec-Moduls) und RXD
mit U2_TXD
(GPIO17
des Heltec-Moduls). Halt! Passen denn auch die Pegel? Das Heltec-Modul verträgt nur 3,3-V-Pegel. Glücklicherweise arbeitet auch das SDS011 mit 3,3-V-Pegeln, auch wenn es 5 Volt Versorgungsspannung benötigt. Die Versorgungsspannung von den GND- und 5V-Pins des Heltec-Moduls. Das war es.
Jetzt muss nur noch der DHT11 angebunden werden. Die Versorgungsspannung ist auch hier 5 Volt, die wir uns vom Heltec-Modul holen. Spannend ist der Anschluss des Signals. Mein Modul hat einen passenden Pull-up-Widerstand. Das Signal sollte man einfach an einen Eingang anschließen können. Zuerst habe ich den GPIO02
als Anschluss für den DHT11 verwendet. Dummerweise funktioniert dann das Programmieren des ESP32 damit nicht. Verbindung ziehen, ESP32 programmieren und wieder stecken funktioniert, ist aber keine Lösung auf Dauer. Nach dem Pinout von Heltec soll man den GPIO02
nur verwenden, wenn man weiß was man tut. Ich hatte gedacht, das würde ich. Meiner Meinung nach sollte der rote Pfeil auf OLED-SDA
zeigen. GPIO02
sollte frei sein, ist es wohl aber nicht.
Programm
Das Programm basiert auf den Bibliotheken:
- IBM LMIC framework by IBM Version 1.5.0+arduino-2 (Link in Arduino-Bibliotheksverwalter zeigt nur auf eine Standard-IBM-Seite und nicht auf den Quellkode, deshalb habe ich den Link nicht übernommen)
- SDS011 sensor Library by R. Zschiegner
- Adafruit Unified Sensor by Adafruit Version 1.0.2
- DHT sensor library by Adafruit Version 1.3.0
Da ich so meine Erfahrungen mit den Versionen der LMIC-Bibliothek habe, sind oben auch die Versionen der Bibliotheken, mit denen mein Projekt funktioniert, angegeben.
Anbindung ans The Things Network
Das IBM LMIC framework sorgt für die Verbindung zum The Things Network. Das Programm basiert auf dem Beispielkode ttn-otaa
dieser Bibliothek. Damit steht schon das Grundgerüst mit Over-the-Air Authentication. Ein guter Start ist wie immer den Beispielsketch in der Arduino-IDE zu öffnen (Datei>Beispiele>IBM LMIC framework>ttn-otaa).
Der Beispielkode muss zuerst an drei Stellen angepasst werden, damit das Gerät sich bei The Things Network anmeldet (Join). Diese drei Stellen sind die Konstanten APPEUI
, DEVEUI
und APPKEY
. Die Werte dazu bekommt man aus der The Things Network Console. Dazu muss man aber schon eine Applikation im The Things Network haben oder jetzt anlegen. Wie das geht, habe ich im Artikel Neuen Sensor im The Things Network anlegen beschrieben.
OK, die Applikation existiert. Es kann weiter gehen. Wir klicken uns in der The Things Network Console über Applications>meine Applikation>Devices>mein Gerät zu unserem angelegten Gerät durch. Es erscheint der folgende Dialog.
In diesem findet man die passenden Werte. Abschreiben ist aber doof, deshalb klicken wir zuerst jeweils auf das <>-Icon. Dadurch wird der Schlüssel als Byte-Array für C angezeigt. Allerdings benötigt die LMIC-Bibliothek in LSB. Dazu muss man noch auf das Icon mit den beiden Pfeilen rechts davon klicken. Es wird dann LSB links von den Byte-Arrays angezeigt. Mit dem Kopieren-Icon kann man jetzt den Wert kopieren und in der Arduino-IDE an der passenden Stelle einfügen.
Unser Kode sollte jetzt so ähnlich wie unten aussehen.
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
// This EUI must be in little-endian format, so least-significant-byte
// first. When copying an EUI from ttnctl output, this means to reverse
// the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3,
// 0x70.
static const u1_t PROGMEM APPEUI[8]={ 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}
// This should also be in little endian format, see above.
static const u1_t PROGMEM DEVEUI[8]={ 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}
// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
// The key shown here is the semtech default key.
static const u1_t PROGMEM APPKEY[16] = { 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2,
0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C };
void os_getDevKey (u1_t* buf) { memcpy_P(buf, APPKEY, 16);}
static uint8_t mydata[] = "Hello, world!";
static osjob_t sendjob;
// hier folgt der weitere Originalkode des Beispielkodes
Jetzt sollte man schon einmal den Kode auf den ESP32 übertragen. In der The Things Network Console sollte man jetzt unter dem Data-Reiter des Geräts die Nachricht Hello, world!
sehen.
SDS011-Sensor
Auch für den SDS011 gibt es ein Beispielprogramm SDS011_HardwareSerial
, an dem wir uns orientieren und den folgenden Kode in das LMIC-Beispiel (unseren offenen Sketch) übernehmen.
#include <SDS011.h>
float p10, p25;
int sds_err;
SDS011 my_sds;
HardwareSerial port(2);
Die setup()
-Funktion erweitern wir noch um die Initialisierung der seriellen Schnittstelle.
my_sds.begin(&port);
Das Lesen der Feinstaubwerte wird jetzt zyklisch in Abhängigkeit von den LoRaWAN-Aussendungen erledigt. Dazu passen wir die Funktion do_send()
an. Zum einen müssen wir die Daten vom Sensor holen. Das erledigt die Funktion get_data()
für uns. Sie weckt den Sensor, liest die Daten und legt ihn wieder schlafen. Nach dem Aufwecken benötigt der Sensor einen Moment, bis er wach ist. (Drei Sekunden haben bei mir gereicht.) Auch wenn delay()
eine Erfingung des Teufels ist, an dieser Stelle funktioniert es. Das Gerät ist nicht auf Batterielaufzeit ausgelegt.
void get_data() {
my_sds.wakeup();
delay(3000);
sds_err = my_sds.read(&p25, &p10);
my_sds.sleep();
}
In do_send()
fügen wir noch den Aufruf von get_data()
hinzu und kopieren die Werte in unser Byte-Array zur Übertragung. Im LMIC-Beispiel wurde die Zeichenkette "Hello, World!"
übertragen. Das passen wir für unsere Daten an. Die Feinstaubwerte liefert der Sensor für die Größen 10 und 2,5 µm jeweils als float
. Der Sensor liefert seine Werte im Bereich als zwei Byte. Laut Datenblatt werden diese nach der folgenden Formel berechnet.
$$ w = (hb \cdot 256 + lb) / 10 $$
Das heißt, im Gleitkommawert ist nur eine Nachkommastelle signifikant. Da die LoRaWAN-Übertragung in Bytes erfolgt, müssen wir die Werte von float
wieder in zwei Byte umwandeln. Für pm10
und pm25
benötigen wir vier Byte in unserem Datenbereich. Für den Temperatur- und Feuchtigkeitswert sehen wir vorausschauend vier weitere Bytes vor und Ändern die Deklaration der Variablen mydata
.
static uint8_t mydata[8];
do_send()
sieht damit wie folgt aus.
void do_send(osjob_t* j){
// Check if there is not a current TX/RX job running
if (LMIC.opmode & OP_TXRXPEND) {
Serial.println(F("OP_TXRXPEND, not sending"));
} else {
get_data();
if (!sds_err) {
uint16_t tmp = p25 * 100;
mydata[0] = highByte(tmp);
mydata[1] = lowByte(tmp);
tmp = p10 * 100;
mydata[2] = highByte(tmp);
mydata[3] = lowByte(tmp);
}
else {
mydata[0] = 0xff;
mydata[1] = 0xff;
mydata[2] = 0xff;
mydata[3] = 0xff;
}
// Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(1, mydata, sizeof(mydata), 0);
Serial.println(F("Packet queued"));
}
// Next TX is scheduled after TX_COMPLETE event.
}
Wie man im Kode sieht, werden alle Bits gesetzt als Fehlerkennung übertragen.
Feuchtigkeitssensor
Der SDS011 ist von der Luftfeuchtigkeit abhängig. Damit man den Fehler erkennen und eventuell herausrechnen kann, bekommt der Sensor auch einen DHT11 als Feuchtigkeitsensor. Auch für diesen Sensor gibt es ein Beispielprogramm, aus dem wir uns bedienen. Für den Sensor fügen wir die folgenden Variablen hinzu.
float humidity;
float temperature; // in Celsius
int dht_err;
DHT my_dht(2, DHT11);
In get_data()
ergänzen wir das Auslesen der Temperatur und Feuchtigkeit und fügen die Fehlererkennung hinzu.
humidity = my_dht.readHumidity();
temperature = my_dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
dht_err = 1;
}
else {
dht_err = 0;
}
Die Umwandlung in die Bytes für das LoRaWAN erfolgt analog zu den Feinstaubwerten. Damit ist der LoRaWAN-Feinstaubsensor fertig.