Wir messen an unserer Schule regelmäßig den Feinstaubgehalt in der Luft. Besonders zwischen Februar und Mai sind die Werte hier in Süd-Ost Asien sehr hoch, da es in dieser Zeit kaum regnet, es sehr trocken und heiß ist und viele Wiesen und Felder abgebrannt werden. Das alles wirkt sich negativ auf die Qualität der Luft aus. Heute möchte ich zeigen, wie man den Feinstaub mit einem Raspberry Pi messen kann. Doch vorher möchte ich kurz auf die Frage eingehen: Was ist Feinstaub überhaupt und was wollen wir messen?
Was ist Feinstaub?
Ganz grob kann man sagen, dass es sich bei Feinstaub um sehr kleine Partikel / Teilchen in der Luft handelt. Man unterscheidet dabei zwischen PM10 und PM2.5 (Particulate Matter, auch Feststoffteilchen). Bei PM10 handelt es sich dabei um alle Partikel in der Luft, die kleiner als 10µm sind, bei PM2.5 entsprechend alle Teilchen, die kleiner als 2.5µm sind. Je kleiner die Teilchen, also v.a. alles kleiner als 2.5µm, desto gefährlicher sind sie für die Gesundheit, da sie bis in die Lungenbläschen eindringen können.
Die WHO empfiehlt z.B. folgende Grenzwerte:
- Jahresmittel PM10 20 µg/m³
- Jahresmittel PM2,5 10 µg/m³
- Tagesmittel PM10 50 µg/m³ ohne zulässige Tage, an denen eine Überschreitung möglich ist.
- Tagesmittel PM2,5 25 µg/m³ ohne zulässige Tage, an denen eine Überschreitung möglich ist.
Diese Werte liegen unter den in den meisten Ländern festgelegten Grenzen. In EU ist ein Jahresmittel für PM10 von 40 µg/m³ zulässig.
Was ist der Air Quality Index (AQI)?
Ausgehend von dem Feinstaubgehalt in der Luft kann man den Air Quality Index berechnen. Er gibt an, wie „gut“ oder „schlecht“ die Luft gerade ist. Leider gibt es hier keinen einheitlichen Standard, da verschiedene Länder diesen jeweils anders berechnen oder andere Skalen haben. Der Wikipedia-Artikel zum Air Quality Index liefert dafür einen gute Übersicht. An unserer Schule richten wir uns nach der Einteilung wie sie von der EPA (United States Environmental Protection Agency) festgelegt wurde.
Soweit ein kurzer Abriss zum Thema Feinstaub und AQI.
Was brauchen wir für die Feinstaubmessung?
Eigentlich braucht es nur zwei Dinge:
- einen Raspberry Pi (jedes Modell geht, am besten ein Modell mit WLAN)
- Feinstaubsensor SDS011
Das war es schon 🙂 Wer sich für einen Raspberry Pi Zero W entscheidet, benötigt noch ein Adapterkabel auf einen Standard-USB Anschluss, da der Zero nur Micro-USB hat. Den Sensor bekommt man am besten bei Aliexpress. Dort kostet er um die 17-20$. Der Sensor hat eine USB-Adapter für die serielle Schnittstelle mit dabei.
Installation
Für unseren Raspberry Pi laden wir uns das entsprechende Raspbian Lite Image herunter und schreiben es auf die Micro-SD Karte. Dokumentiert ist das z.B. hier. Auf die Einrichtung der WLAN-Verbindung gehe ich an dieser Stelle nicht ein. Dazu gibt es viele Tutorials im Netz.
Wer nach dem Booten gleich SSH aktiviert haben möchte, muss in der boot-Partition einen leere Datei mit dem Namen ssh
anlegen. Die IP des Raspberry Pis bekommt man am besten über den eigenen Router / DHCP Server heraus. Danach kann man sich per SSH anmelden (Standardpasswort ist raspberry):
$ ssh pi@192.168.1.5
Für unseren Sensor verwenden wir eine kleines Python-Modul, was uns einiges an Arbeit abnimmt. Dazu müssen wir noch einige Pakete installieren und das Repository auf den Pi klonen.
$ sudo apt install python-serial python-enum lighttpd
Alle benötigten Pakete sollten jetzt installiert sein und das Modul für unseren Sensor befindet sich in /home/pi/sds011. Unter /home/pi/sds011/Code/text.py gibt es ein Testprogramm, mit dem man den Sensor testen kann. Bevor wir das Programm aufrufen können, müssen wir noch wissen, an welchem seriellen Port der USB-Adapter steckt. dmesg hilft uns weiter:
$ dmesg [ 5.559802] usbcore: registered new interface driver usbserial [ 5.559930] usbcore: registered new interface driver usbserial_generic [ 5.560049] usbserial: USB Serial support registered for generic [ 5.569938] usbcore: registered new interface driver ch341 [ 5.570079] usbserial: USB Serial support registered for ch341-uart [ 5.570217] ch341 1-1.4:1.0: ch341-uart converter detected [ 5.575686] usb 1-1.4: ch341-uart converter now attached to ttyUSB0
In der letzten Zeile steht unsere Schnittstelle: ttyUSB0. Wir brauchen jetzt noch zwei Dinge: einmal ein kleines Python-Skript, welches die Daten ausliest und in eine JSON-Datei speichert und dann werden wir eine kleine HTML-Seite basteln, die diese Daten ausliest und darstellt.
Daten auf dem Raspberry Pi auslesen [Update]
Wir erstellen zuerst eine Instanz des Sensors und lesen dann alle 5 Minuten für 30 Sekunden den Sensor aus. Diese Werte können natürlich angepasst werden. Zwischen den Messintervallen versetzen wir den Sensor in einen Schlafmodus, um die Lebensdauer zu erhöhen (Lebensdauer beträgt laut Hersteller ca. 8000 Stunden).
[Update, 27.02.2018] Ich habe festgestellt, das meine Python3 Version des Skripts die serielle Schnittstellen nicht ansprechen konnte, z.B. nach einem Reboot. Erst nach mehrmaligem Starten des Skripts konnten dann die Daten gelesen werden. Woran das liegt, habe ich leider nicht herausgefunden, aber mit Python2.7 funktioniert es ohne Probleme. Deshalb hier eine neue Version des Skripts, welche auf Python2.7 basiert.
Im Homeverzeichnis legen wir eine Datei mit dem Namen aqi.py an und kopieren den folgenden Inhalt hinein:
#!/usr/bin/python # coding=utf-8 # "DATASHEET": http://cl.ly/ekot # https://gist.github.com/kadamski/92653913a53baf9dd1a8 from __future__ import print_function import serial, struct, sys, time, json DEBUG = 0 CMD_MODE = 2 CMD_QUERY_DATA = 4 CMD_DEVICE_ID = 5 CMD_SLEEP = 6 CMD_FIRMWARE = 7 CMD_WORKING_PERIOD = 8 MODE_ACTIVE = 0 MODE_QUERY = 1 ser = serial.Serial() ser.port = "/dev/ttyUSB0" ser.baudrate = 9600 ser.open() ser.flushInput() byte, data = 0, "" def dump(d, prefix=''): print(prefix + ' '.join(x.encode('hex') for x in d)) def construct_command(cmd, data=[]): assert len(data) <= 12 data += [0,]*(12-len(data)) checksum = (sum(data)+cmd-2)%256 ret = "\xaa\xb4" + chr(cmd) ret += ''.join(chr(x) for x in data) ret += "\xff\xff" + chr(checksum) + "\xab" if DEBUG: dump(ret, '> ') return ret def process_data(d): r = struct.unpack('<HHxxBB', d[2:]) pm25 = r[0]/10.0 pm10 = r[1]/10.0 checksum = sum(ord(v) for v in d[2:8])%256 return [pm25, pm10] #print("PM 2.5: {} μg/m^3 PM 10: {} μg/m^3 CRC={}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK")) def process_version(d): r = struct.unpack('<BBBHBB', d[3:]) checksum = sum(ord(v) for v in d[2:8])%256 print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK")) def read_response(): byte = 0 while byte != "\xaa": byte = ser.read(size=1) d = ser.read(size=9) if DEBUG: dump(d, '< ') return byte + d def cmd_set_mode(mode=MODE_QUERY): ser.write(construct_command(CMD_MODE, [0x1, mode])) read_response() def cmd_query_data(): ser.write(construct_command(CMD_QUERY_DATA)) d = read_response() values = [] if d[1] == "\xc0": values = process_data(d) return values def cmd_set_sleep(sleep=1): mode = 0 if sleep else 1 ser.write(construct_command(CMD_SLEEP, [0x1, mode])) read_response() def cmd_set_working_period(period): ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period])) read_response() def cmd_firmware_ver(): ser.write(construct_command(CMD_FIRMWARE)) d = read_response() process_version(d) def cmd_set_id(id): id_h = (id>>8) % 256 id_l = id % 256 ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h])) read_response() if __name__ == "__main__": while True: cmd_set_sleep(0) cmd_set_mode(1); for t in range(15): values = cmd_query_data(); if values is not None: print("PM2.5: ", values[0], ", PM10: ", values[1]) time.sleep(2) # open stored data with open('/var/www/html/aqi.json') as json_data: data = json.load(json_data) # check if length is more than 100 and delete first element if len(data) > 100: data.pop(0) # append new values data.append({'pm25': values[0], 'pm10': values[1], 'time': time.strftime("%d.%m.%Y %H:%M:%S")}) # save it with open('/var/www/html/aqi.json', 'w') as outfile: json.dump(data, outfile) print("Going to sleep for 5min...") cmd_set_mode(0); cmd_set_sleep() time.sleep(300)
Schneller geht es mit:
$ wget -O /home/pi/aqi.py https://raw.githubusercontent.com/zefanja/aqi/master/python/aqi.py
Damit das Skript fehlerfrei durchläuft sind noch zwei kleine Dinge nötig:
$ sudo chown pi:pi /var/www/html/ $ echo [] > /var/www/html/aqi.json
Nun kann man das Skript starten:
$ chmod +x aqi.py $ ./aqi.py PM2.5: 55.3 , PM10: 47.5 PM2.5: 55.5 , PM10: 47.7 PM2.5: 55.7 , PM10: 47.8 PM2.5: 53.9 , PM10: 47.6 PM2.5: 53.6 , PM10: 47.4 PM2.5: 54.2 , PM10: 47.3 ...
Skript automatisch starten lassen [Update]
Damit wir nicht jedes mal das Skript von Hand starten müssen, können wir es mit einem Cronjob z.B. bei jedem Neustart des Raspberry Pis mit starten lassen. Dazu öffnen wir die crontab-Datei
$ crontab -e
und ergänzen am Ende die nachfolgende Zeile:
@reboot cd /home/pi/ && ./aqi.py
Nun startet unser Skript automatisch bei jedem Neustart mit.
HTML-Seite für Anzeige der Messwerte und AQI
Weiter oben haben wir bereits einen leichtgewichtigen Webserver lighttpd
installiert. Unsere HTML-, Javascript- und CSS-Datei müssen wir also im Verzeichnis /var/www/html/ speichern, damit wir von einen anderem Computer / Smartphone auf die Daten zugreifen können. Mit den nächsten drei Befehlen laden wir die entsprechenden Dateien einfach herunter:
$ wget -O /var/www/html/index.html https://raw.githubusercontent.com/zefanja/aqi/master/html/index.html $ wget -O /var/www/html/aqi.js https://raw.githubusercontent.com/zefanja/aqi/master/html/aqi.js $ wget -O /var/www/html/style.css https://raw.githubusercontent.com/zefanja/aqi/master/html/style.css
Die Hauptarbeit findet in der Javascript-Datei statt, die unsere JSON-Datei öffnet, den letzten Wert nimmt und anhand dieses Wertes den AQI berechnet. Dann werden noch die Hintergrundfarben anhand der Skala der EPA angepasst.
Nun man die Adresse des Raspberry Pis einfach im Browser aufrufen und die aktuellen Feinstaubwerte betrachten, z.B. http://192.168.1.5:
Die Seite ist sehr einfach gestaltet und kann noch erweitert werden, z.B. um eine Grafik, die den Verlauf der letzten Stunden anzeigt usw. Pull Requests sind willkommen 🙂 Der komplette Quellcode befindet sich auf Github.
Fazit
Für relativ wenig Geld können wir jetzt den Feinstaub mit einem Raspberry Pi messen. Ob draußen fest installiert oder als mobiles Messgerät – Einsatzmöglichkeiten gibt es viele. An unserer Schule haben wir beides im Einsatz. Einerseits einen Sensor der Tag und Nacht die Werte im Freien misst und einen mobilen Sensor, mit dem wir die Effektivität unserer Luftfilter in den Klassenzimmern überprüfen.
So sieht das ganze bei uns in der Schule aus:
Unter http://luftdaten.info gibt es noch eine andere Möglichkeit einen ähnlichen Sensor zu bauen. Da bekommt man die Software bereits fertig geliefert und das Messgerät ist noch mal kompakter, da kein Raspberry Pi verwendet wird. Tolles Projekt!
Ein Feinstaubsensor ist ein Projekt, was sich auch gut mit Schülern im Informatikunterricht oder einer Arbeitsgemeinschaft umsetzen lässt!
Wofür setzt du einen Raspberry Pi ein?
Interessanter Artikel – vielen Dank! Lädst Du die Daten zusätzlich bei Luftdaten.info oder einem ähnlichen Service hoch?
Ja. In Asien beliebt ist v.a. https://aqicn.org.
Danke für den klasse Artikel. Mich würde noch das Gehäuse für den fest installierten Pi/Sensor interessieren und wie der mit Strom versorgt wird.
@Tom: Wir haben alles in einer Plastikbox, in die wir die Teile geklebt oder geschraubt haben. Von außen wir ein Schlauch durch die Wand am Sensor angeschlossen. Ich lade später mal noch ein Bild hoch.
Sehr hilfreich. Danke. BTW: Wie kann ich Text Dateien, die nicht mit txt enden sondern z.B. mit conf oder json in der Vorschau anzeigen?
@Josioo: Welche Vorschau meinst du? Unter Windows / Ubuntu / … ?
Danke für die TOP Anleitung. Würde mir noch ein Script für eine RRDtool DB und eine Grafische Auswertung wünschen über bestimmt Zeitpunkte wünschen.
@Hanz: Ja, an eine grafische Auswertung habe ich schon gedacht, aber noch nicht umsetzen können. Das sollte aber keine großes Problem sein.
Hi, danke fürs Script, die Sache mit der grafischen, zeitlichen Visualisierung möchte ich nochmal aufgreifen: Gibt’s evtl. schon etwas fertiges im Netz? Man muss ja das Rad nicht immer neu erfinden.
Zum Thema grafische Auswertung… Schaut euch doch mal das Projekt auf Luftdaten.info genauer an. (z.B. auch was im Git gehostet ist)
Die NodeMCU’s senden ihre Werte an eine API von Luftdaten.info. Das sollte auch mit dem Raspberry funktionieren und ihr nehmt dann auch am gemeinsamen Sensor Netzwerk teil… Als Bonus sozusagen bekommt ihr die grafischen Auswertungen. (Und ihr seid auch auf der Karte mit dabei) Es gibt dort auch weitere Informationen und Projekte. z.B. kann man sich über das Webinterface die Daten auch an eine eigene (private) API’s schicken lassen… Sprich ein PHP Skript auf einem Server… Das Projekt ist vom OK Lab und ich gehe davon aus, dass alles Open Source ist. PS: ich habe an einem Workshop einen Feinstaub Sensor mit denen gebastelt, war auch sonst schon auf ein paar Veranstalltungen… Bin aber nicht weiter OK Lab Verbandelt… Die „Werbung“ mache ich aus Begeisterung für das Projekt. Hier gelandet bin ich, weil ich die NodeMCU Lösung um einen Raspberry PI ergänzen will. (BTW. Top Anleitung ich freu mich schon, wenn der neue Sensor kommt)
Gibt es einen speziellen Grund, dass du es immer 30 Sek. laufen lässt?
Ich tendiere dazu es 1x alle 5 Min laufen zu lassen und dann die Werte nach Munin oder was ähnlichem zu schreiben.
Hi. Danke für die tolle Anleitung.
Leider stockt das Skript bei immer mal wieder (1h schaffts fast nie). Ctrl-C im Putty gibt zurück:
pi@raspberrypi:~ $ ^CTraceback (most recent call last):
-bash: :s^CTraceback (most recent call last):: substitution failed
File „./aqi.py“, line 62, in read_response
byte = ser.read(size=1)
File „/usr/lib/python2.7/dist-packages/serial/serialposix.py“ , line 472, in read
ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left())
KeyboardInterrupt
Kannst du dir was darunter vorstellen? lg
Vielen Dank für das Skript! Ich hatte als absoluter Newbie das Problem, dass mit modifiziertem Skript nach einigen Messzyklen des SDS011 an einem RasPi Zero WH per cronjob (statt sleep) plötzlich keine Werte mehr ausgelesen worden waren, nur ein reboot oder Abziehen des USB-Adapters schaffte jeweils vorübergehend Abhilfe. Ich habe bzgl. serial im Skript das flushInput() auf das neuere reset_input_buffer() geändert (analog ggf. reset_output_buffer() ), seitdem läuft alles wie gewünscht als cronjob. Eine weitere Visualisierung hatte ich über thingspeak.com hinzugefügt.
Vielen Dank für das tolle Script und die Arbeit mit der schönen Website. Ich möchte es gerne stündlich per Cronjob starten. Dazu habe ich die Ausgaben auf Standardoutput und die while Schleife entfernt. Manchmal funktioniert es, manchmal bekomme ich aber auch keine Werte oder ich bekomme keine Werte und der Sensor geht auch nicht in den Schlafmodus. Die Änderung von Bernhard habe ich gemacht, hilft aber leider nicht. Hat jemand eine Idee?
[…] ca. einem Jahr habe ich beschrieben, wie man mit einem Raspberry Pi und einem Feinstaubsensor die Luftqualität messen kann. Das Setup setzen wir so seit einigen Jahren bei uns in der Schule […]
Die Änderung von Bernhard sollte noch mit eingebaut werden, da ich die selben Symptome hatte und mit dem genannten Einzeiler dann das gewünschte Ergebnis bekommen habe.
Hallo würde gerne auch so eine tolle Grafik wie auf der Seite https://aqicn.org/ haben.
Was brauche ich an Komponenten für das Messen von Ozon, Schwefeldioxid, Stickstoffdioxid, Kohlenmonoxid, Temperatur, Luftdruck relativer Luftdruck und Wind?
Sorry, dann bin ich ehrlich gesagt überfragt. Vielleicht hast du mehr Erfolg, wenn du in einem der Rapsberry Pi Foren fragst.
Hallo danke für das Skript.
ich habe das Problem gehabt das die ttyUSB0 nicht anerkannt ist und hab alles mögliche versucht aber es klappt leider nicht.
kann mir Jemand vllt irgendwie helfen?
ich bekomme folgendes raus
$ dmseg
atabase
[ 4.874669] cfg80211: Loaded X.509 cert ’sforshee: 00b28ddf47aef9cea7′
[ 4.952295] brcmfmac: F1 signature read @0x18000000=0x1541a9a6
[ 4.970403] brcmfmac: brcmf_fw_alloc_request: using brcm/brcmfmac43430-sdio f or chip BCM43430/1
[ 4.970672] usbcore: registered new interface driver brcmfmac
[ 5.082929] smsc95xx 1-1.1:1.0 enxb827eb5f867c: renamed from eth0
[ 5.174412] brcmfmac: brcmf_fw_alloc_request: using brcm/brcmfmac43430-sdio f or chip BCM43430/1
[ 5.174515] brcmfmac: brcmf_c_process_clm_blob: no clm_blob available (err=-2 ), device may have limited channels available
[ 5.175342] brcmfmac: brcmf_c_preinit_dcmds: Firmware: BCM43430/1 wl0: Oct 23 2017 03:55:53 version 7.45.98.38 (r674442 CY) FWID 01-e58d219f
@Jone: Was sagt lsusb?
Gibt es einen Weg, dieses Skript auch in Python 3 zu benutzen?
Dieser Codeteil :
def construct_command(cmd, data=[]):
assert len(data) ‚)
return ret
ergibt bei mir in Python3 den Fehler :
Traceback (most recent call last):
File „/home/pi/AirMonitoring/aqi3.py“, line 150, in
cmd_set_sleep(0)
File „/home/pi/AirMonitoring/aqi3.py“, line 116, in cmd_set_sleep
ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
File „/usr/lib/python3/dist-packages/serial/serialposix.py“, line 532, in write
d = to_bytes(data)
File „/usr/lib/python3/dist-packages/serial/serialutil.py“, line 63, in to_bytes
raise TypeError(‚unicode strings are not supported, please encode to bytes: {!r}‘.format(seq))
TypeError: unicode strings are not supported, please encode to bytes: ‚ª´\x06\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ÿÿ\x06«‘
in Python 2.7 läuft das ganze jedoch problemlos
Hallo,
ich habe alles installiert, wie beschrieben. Das aqi.py Script läuft und zeigt mir im Terminal die Werte an.
Ich erhalte aber die Medlung „127.1.1.1“ hat die Verbindung abgelehnt, wenn ich die echte IP-Adresse angebe, das Gleiche.
Was kann ich da tun? Muß noch ein Port (wie?) geöffnet werden, oder einfach der richtige angegeben werden?
mfg
Wolfgang
Manchmal ist der Reboot die Lösung und wir haben früher immer den Witz bei Windows gemacht: „Mouse move occurred – please reboot to see change“
mfg
Wolfgang
Wenn ich den RaspberryPi neu starte, so wird das Script vom cron gestartet, beendet sich aber nach 2-3 Durchläufen. Beim Start im Terminal läuft es stundenlang perfekt.
In der syslog ist kein Eintrag.
Woran kann das liegen? Soll ich den crontab-Eintrag mit „> /dev/null“ erweitern?
mfg
Wolfgang
Jetzt ist das Script abgestürzt, nachdem es einige Stunden gelaufen ist:
Traceback (most recent call last):
File „./aqi.py“, line 105, in
print(„PM2.5: „, values[0], „, PM10: „, values[])
IndexError: list index out of range
[1]+ Exit 1 ./aqi.py
Wie sag ich dem, daß er weitermachen soll, statt abzustürzen, bzw. wie kann es neu starten?
mfg
Wolfgang
Ich habe mir dazu einen systemd Service geschrieben:
[Unit]
Description=AQI Service
[Service]
Type=simple
Restart=always
RestartSec=10
ExecStart=/home/pi/aqi/aqi.py
[Install]
WantedBy=multi-user.target
@Martin: Ja, der Code ist für Python 2.7. Mittlerweile verwende ich lieber ein anderes Projekt, um Feinstaub zu messen: luftdaten.info. Ist billiger und funktioniert sehr zuverlässig.
Hallo alle miteinander!
Erstmal Danke für deinen Artikel und eine Tolle Idee mit dem Bildschirm!
Es gibt mindestens schon zwei Varianten für Python 3. Eine davon ist meine:
https://github.com/JuriPospelow/Klima-in-der-Stube
Sie ist ohne mqtt und ohne history, aber mit einem Sensor zur Messung von Temperatur und Druck BMP280 .
Leider lässt sich das Skript mittlerweile nicht mehr verwenden, in den neuen Raspian Versionen gibt es keine Möglichkeit mehr python-serial und python-enum zu installieren.
Hallo,
gibt es vielleicht eine Doku zum Code?
Ich verstehe die Funktion einiger methoden nicht, wie “ cmd_firmware_ver()“, „cmd_set_id(id)“, „cmd_set_mode(mode=MODE_QUERY)“
Vielen Dank