My latest project – OAQ
Several years ago I took a Raspberry Pi 2 B+ and turned it into SLRPi – the SETI listener for Raspberry Pi. It was a custom build of Debian Jessie and a custom build of BOINC for the SETI project. You can read more about that effort here.
Earlier this year it was announced that this phase of the project was ending. After 20 years of collecting data it was time for them to do some data mining and look for interesting phenomenon. With a heavy heart (because I joined that project 20 years ago!) I decided to repurpose the Pi, but wasn’t sure what exactly to do with it. 2020 has without a doubt been a unique year, bringing with it a whole host of apocalyptic things like murder hornets, incessant hurricanes, raging wildfires, and who could forget COVID-19? Needless to say, it makes life as an asthmatic more challenging, and monitoring air quality has become crucial. The last thing folks like us need is a trip to the ER for lung related problems, when there’s a lung-eating virus out there! I purchased an indoor AQI monitor which has more than paid for itself so far. This combined with our Honeywell air cleaners keep the air inside of our home very clean. However, I do like going outside, and it’s nice to know if I should wear a normal mask, an N-95 respirator, or should I just stay indoors, take Robitussin and a breathing treatment and try again tomorrow?
This sparked my next project – OAQ. The Oszakiewski Air Quality sensor. Using the same Raspberry Pi 2 B+ with a new SD card running Raspberry OS I built a Python 3 script based on this article that connects to my other project – the Weather Station Widget for Android, which reports out real time weather data from my Ambient Weather sensor array out back as a widget on my Android phone. Currently I’m reading AQI data from an EPA station roughly 5 miles away. Not too bad, unless there’s a dust storm there but not by me. Or a fire there but not by me. I’m interested if I can open my windows, go for a walk, sit on the patio, etc. What’s my air quality like?
Raspberry Pi and Sensor
I started out installing the OS the traditional way, configuring WiFi, running updates, etc. The Pi already had a WiFi dongle from the SLRPi project which made this easier. Next I installed PySerial using the following terminal command:
pip3 install pyserial
This will install the Python 3 version of the serial libraries which are needed to talk to the USB ports. The SDS011 PM sensor I purchased separately plugs into the USB port, providing an easy interface to read from. This sensor will read PM2.5 and PM10 air quality levels. A separate sensor is required for Ozone (O3). I’ll work on that one later.
Once the serial libraries were installed I created a new python project and added the following code:
import serial, time, requests
from datetime import datetime
ser = serial.Serial('/dev/ttyUSB0')
headers = {'Content-Type' : 'application/json'}
while True:
data = []
for index in range(0,10):
datum = ser.Read()
data.append(datum)
pmtwofive = int.from_bytes(b''.join(data[2:4]), byteorder='little') / 10
pmten = int.from_bytes(b''.join(data[4:6]), byteorder='little') / 10
body = {'pm25':pmtwofive,'pm10':pmten}
response = requests.post(url='API_URL', headers=headers, json=body)
print('PM2.5: ' + pmtwofive + ' PM10: ' + pmten)
print(response.status_code)
time.sleep(300)
The code first imports all necessary libraries, then establishes a connection to the USB port using a serial connection. Once connected it reads from that port as long as there’s something coming from it. The SDS011 sensor reads and reports in bytes of 10, which is why we’re reading each message from the port as a range(0,10). Once we have that chunk of data, we append it to the array we established initially (data = []). Now that we have the results as an array we need to parse it and convert the parsed pieces into integers. Array positions 2-4 is the PM2.5 value, and positions 4-6 is the PM10 value. By grabbing that, converting to an integer and dividing by 10 (again, because everything coming from the sensor is by tens), you end up with the real time sensor reading. Repeat for each for the other sensor value and you’ve got the readings!
I also ensured this loads every time the Pi restarts in case of power outages, moving the device around, etc. I did this by editing the following file
sudo nano /etc/xdg/lxsession/LXDE-pi/autostart
Adding the following entry to the bottom ensures it loads on start
@lxterminal -e python3 /home/pi/Desktop/aqi.py
API
The next step was to save it somewhere. Since I ultimately wanted to display this in my Android Weather Widget, I created a SQL table in my Azure SQL database to store the values in. Now I needed an API to push the data to. I built a .Net Core 3.1 Web API and pushed it to my Azure instance to handle these calls, and also for the weather widget to pull the data from. The API only has two endpoints: POST and GET. The POST accepts strongly-typed JSON data from the Pi and adds it to the database. The GET returns the most recent values from the database. Nice and simple!
Installation and Weatherproofing
Since this is an outdoor sensor, I wanted to make sure it can survive the weather. In Arizona it gets HOT, but it also rains on occasion and can get down to freezing at night in the winter. I already had a case for the Pi, but needed something for the SDS011 PM sensor. I found an unused Tupperware-style container with a locking lid that perfectly fit both devices side by side. I cut a hole in the front for the PM sensor laser, and another for the power cord for the Pi. You can see the result here:
Not elegant, but functional! Should allow enough air flow to keep the interior from overheating (low voltage anyway, shouldn’t have a problem with overheating) while allowing adequate air across the sensor.
Update: I published a page on our family’s Health Metrics Management site where you can view the current sensor output, updated every 5 min.
Update 8/14/2021: After having two different PM sensors die on me, it dawned on me to check the operating ranges and limits on the sensor, since it gets very hot in AZ. Summer temperatures in the afternoon can exceed 120° even in the shade. Perhaps the temperature was causing issues? Turns out the operating temperature range is 14°F – 122°F, so that’s not it. However, the idle humidity is 90%, and the operating humidity is 70%. Although AZ typically has an average humidity of 10% or less, during monsoon season it’s not uncommon to average a 40% or better humidity. Turns out lately we’ve been getting microbursts, and the humidity has been climbing above 90%. I’m sure that’s causing these failures.
To mitigate this, my daughter Kira inspired me to write another script that checks the weather station in our backyard every minute for the humidity. If it’s above 65% (or the temp is above 120°F, kill the pi. The code for that script looks like this:
import sys, json, requests, os, time
headers = {"Accept":"application/json"}
url = "https://url-to-weather-station"
while True:
response = requests.get(url, headers=headers)
json = response.json[0]
h = int(json["lastData"]["humidity"])
t = float(json["lastData"]["tempf"])
if h > 65:
logFile = open(r"log.txt","a")
logFile.write(str(datetime.now()) + " humidity is " + str(h) + ", which is > 65. Shutting down to protect the PM sensor")
logFile.close()
os.system('sudo poweroff')
elif t > 120:
logFile = open(r"log.txt","a")
logFile.write(str(datetime.now()) + " temp is " + str(h) + ", which is > 120. Shutting down to protect the PM sensor")
logFile.close()
os.system('sudo poweroff')
else:
time.sleep(60) # sleep for 1 minute
This script I saved in the same location as the main script, then added another line to the same autostart file above:
@lxterminal -e python3 /home/pi/Desktop/humidchk.py
Next Steps
Next I’ll purchase an Ozone (O3) sensor and attach that as well. Probably in a month or two. I’ll post here when that’s added with what was required and how it went.
Have you done something like this and have advice? See something that could have been done better? Interested in doing something similar? Let me know!