Air Quality Monitor using PMS7003, BME688 and Raspberry Pi Zero

View of Air Monitor in Case, lid open

After recently purchasing an HEPA Air Purifier I decided it would be an interesting project to read and log air quality in my home.

The original Raspberry Pi Zero W I had spare which is a fairly low power computer by modern standards, but it's got enough resources to read sensors, log the results to the database and expose the results via a web page (and API). It's a decent choice for this project.

After some research, the sensors I would buy for this project are the PMS7003 by PLANTOWER, which can read PM1.0, PM2.5 and PM10 particle density in µg/m³ and the BME688 sensor from Bosch Sensortec to measure gas, pressure, temperature and humidity. I did initially start with the AHT25 for temperature and humidity, but results were poor.

The two sensors and the conveniently small Pi Zero fits in a cheap project box measuring 10cm x 6cm x 2.5cm. No screen is present because I'm nearly always close to one in my own home (PC, laptop, Smartphone) which do excellent jobs of displaying a webpage. There's also no buttons or LEDs - the interaction is purely web based.

Being web based, this allows me to set up the UI as I wish, and I used the open source chart.js for graphing the results, and influxdb as the database (as measurements are timestamped). As I run my own home based VPN too, I can even connect from (almost) anywhere in the world with Internet access and monitor my home air quality, if I wish!

Python code runs as a service to read the sensor data and write to the database. This runs independently of separate Python code which exposes a simple web framework that reads and displays values.

So, here is my implementation:

View of Air Monitor

View of Air Monitor

About Air Quality

My interest from this came about when reading news articles about air quality and how poor it can be in cities especially and impact your health. I'd also caught a few comments recommending a HEPA air purifier as a life changing purchase.

So, I did. But the purifier doesn't tell you the quality of the air, so I started to look at ways to monitor it to see how bad my indoor air quality really is. In particular interest is the Particulate Matter (PM), with some governments taking an interest in reading PM10 and PM2.5 based on the latest evidence on the effects to health.

For PM10, a 24 hour average of 50 µg/m³ is considered moderate by the UK. For PM2.5, it's 35µg/m³ average over 24 hours.

PM10 can include dust and dirt, smoke, combustion, pollen and mould. You might be able to smell these things, but sometimes not. It's less likely than PM2.5 to enter the bloodstream through your lungs, however, it can cause irritation to eyes, nose and throat and long term exposure can affect lungs (lung cancer) and heart.

PM2.5 is more dangerous to health as it is more easily absorbed through the lungs and into your blood. Lung damage can cause respiratory illnesses such as asthma, bronchitis, and emphysema. Blood gets pumped around your body through the heart (leading to cardiovascular problems) and also reaches the brain (stroke, amongst other bad effects). PM2.5 can include dust, soot, smoke (inc. smoking), combustion emissions (i.e., from engines, vehicle or otherwise), construction and demolition and even organic sources such as soil.

Other than PM, bad pollutants also involve (but not limited to) Nitrogen dioxide (NO2), Ozone (O3), Carbon monoxide (CO), Sulphur Dioxide (SO2), Lead (Pb) and Formaldehyde from Volatile Organic Compounds (VOCs). These also can cause a number of serious health issues.

Hardware

View of Air Monitor Case, open lid 2

Below is the hardware I'd use for the project:

  • Raspberry Pi Zero W - the brains of the operation, a cheap ARM computer developed here in the UK, but has become extremely popular. I need the 'W' version with wireless so I can access the data remotely. Designed to run Linux (and I'll use the default Raspberry Pi OS OS distro). Requires a separate Micro SD card.
  • PMS7003 Particle Sensor - (aka PMSA003A) a sensor that uses a fan and laser to measure suspended small particles in the air. This will measure the PM10, PM2.5 and PM1.0 in the air, in µg/m³.
  • BME688 gas, pressure, temperature & humidity sensor (Pimoroni module) - for measuring temperature, pressure and humidity, but can also be a 'digital nose' set up to react to changes in Volatile Organic Compounds (VOCs), Volatile Sulphur Compounds (VSCs) and the presence of Carbon Monoxide and Hydrogen.
  • USB Power Supply - 5V 2A or better.
  • Homemade PCB for mounting the PMS7003 connector (separate adapters can be purchased if you want to avoid PCB etching)
  • 2.54mm headers for Pi, 'Dupont' connectors and housings (best get a kit), cable, PCB, nylon screws, nuts.
  • Project case to house it all.

There is no screen, buttons or battery. It is viewed on a PC or Smartphone and is designed to run continuously to log data.

The sensors are the greatest expense for the project, followed by the Pi. The rest of the hardware shouldn't add much to the cost.

The below wiring diagram is how the hardware is connected together:

Pi Zero Air Monitor Wiring

Wiring is simple. Only the sensors are connected via the power, UART and I2C headers of the Pi. The Pi receives its power via the micro USB port connected to a 5V phone charger.

The complication was the header for the PMS7003 is Samtec TFC/TMF header with 1.27mm (0.05 inch) spacing instead of the usual 2.54mm (0.1 inch) spacing, making it incompatible with any of my typical headers. A PCB mount header comes with the sensor but it would be awkward to solder directly to. This drove me to make and etch my own PCB with offcuts of copper board I already had. It would allow me to adapt the PMS7003 connections via wires to standard 2.54mm DuPont headers for plugging into the Raspberry Pi. I also designed the board for the spacing of the AHT25 sensor, which I later abandoned for the much better (separate) BME688 board.

Below is a picture of my layout, as well as the PCB. These images will not be true size. The PCB was built for the AHT25, so how the BME688 sensor board is placed is an after-thought that shows! Get the sensor as exposed as possible (whilst still being protected from accidents).

PCB template

PCB after etching

PCB top after soldering

PCB bottom after soldering

Below is a picture of the built layout including case lid which has gentle padding to keep PMS7003 in place. Note that the Pi Zero slides under the PMS7003 in this case / layout so don't order the Pi with soldered GPIO headers if you plan on doing the same!

View of Air Monitor Case, open lid 2

Adjusting the case took a little time, but it was done purely with cheap tools - drill and drill bits + needle files. The first picture is the openings for the Pi ports (even though two I've never used), the second is the sensor openings.

View of Case, Pi Ports

View of Case, Sensor openings

Software

My files file downloading / clone are available at github.com.

Operating System (OS)

Raspberry Pi OS Logo

For the OS, I'm using the standard Raspberry Pi OS Lite 32-bit bullseye version. That's because the UI is web based, meaning no GUI is needed for the OS. Please see my Raspberry Pi OS Tweaks page for installation recommendation and tweaks.

After installing and updating, some initial configuration adjustments are required. We'll need to enable I2C for the BME688 and UART for the PMS7003. I've also disabled audio since it needs no speaker.

sudo nano /boot/config.txt

Add, change or uncomment the following options:

...
# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=on
...
# Enable audio (loads snd_bcm2835)
dtparam=audio=off
...
# Enable UART
enable_uart=1

To enable the i2c driver, we also need put i2c-dev into /etc/modules. To do this quickly, run:

sudo sh -c "echo i2c-dev >> /etc/modules"

Alternatively, I2C and UART can be enabled by running raspi-config.

We'll install git cmake gcc and g++ for building the projects we'll use:

sudo apt install -y git cmake gcc g++

Influx DB

For a small scale project like this, the choice of database isn't too critical, and I was considering using Maria DB (a fork of MySQL, which I've used about 17 years ago!). However, I did some research about time/sensor logging databases and found influxdb is a recommendation, so I decided to use that instead.

It is supported on the Pi, and the little computer has enough power to host the database for local read and write purposes which is all I need (it could suit remote purposes too but wouldn't be able to handle many clients).

Installing it is easy enough - head to https://www.influxdata.com/downloads/. At time of writing, I picked version InfluxDB v2.7.5 and platform is "Ubuntu & Debian" or "Ubuntu & Debian (ARM 64-bit)" - both options will grab the binaries for the older version 1.8.10 of InfluxDB for ARMv6 32-bit anyway as that's all the Pi will support.

The client for Python is also required:

sudo apt install python3-influxdb

Once we have the Python client, we should be able to run python3 and then run line-by-line statements to connect and create a database:

>>> from influxdb import InfluxDBClient
>>> client = InfluxDBClient(host='localhost', port=8086)
>>> client.create_database('airq')
>>> client.get_list_database()
[{'name': '_internal'}, {'name': 'airq'}]
>>> exit()

There's no need to create a 'table', because that's what a measurement is in influxdb. We'll write points to the DB after reading our sensors and influxdb will store them appropriately with a timestamp against them automatically.

PMS7003

The PMS7003 implementation is simple, and it took me less than an hour to get it up and running.

However, it's not recommended to keep the PMS7003 awake 24/7 and read it constantly. For a better lifespan, it should be put to sleep and woken up at infrequently intervals to read data.

The approach that helps the sensor's operational life is to:

  1. Wakes up the PMS7003 - this will cause its fan to start spinning
  2. Sleeps for 60 seconds - it is recommended to wait at least 30 seconds after waking up to get stable data
  3. Read the sensor - collecting measurements returned as a dictionary
  4. Puts the sensor to sleep again - this stops the fan spinning

I decided to do this every 15 minutes.

The python read service depends on pms7003.py which is from Tomasz Lewicki's code on github.com. It works nicely and I picked this code as it implemented the sleep and wakeup commands which I rely on.

BME688

The BME688 sensor also has libraries available. It's not out-of-the-box perfect however and for many measurements it needs to be 'trained'. Bosch Sensortec's BSEC software (closed source) allows for measuring the IAQ (Indoor Air Quality) with ability to train for specific gases.

I downloaded and used the older 2.0.6.1 version of BSEC. You can download it from Bosch Sensortec here. This is both compatible with the pi3g library and does not seem to have a 'business use only' license (the latest restricts private use).

The library is the pi3g bme68x-python-library (gihub.com). On the Pi itself, you can clone this by running:

cd ~
git clone https://github.com/pi3g/bme68x-python-library

Put the BSEC zip in the bme68x-python-library directory that is created and unzip it there (unzip bsec_2-0-6-1_generic_release_04302021.zip). The folder BSEC_2.0.6.1_Generic_Release_04302021 should be created inside bme68x-python-library. Then run:

cd ~/bme68x-python-library
sudo python3 setup.py install

Before using it in my project, I used the mcalisterkm - p-sensors code to create a burn-in configuration, which I'll use in my web service to improve calibration time.

cd ~
git clone https://github.com/mcalisterkm/p-sensors

You can run the burn in python code over the next 24 hours to create the conf files.

cd ~/p-sensors/src/1.3.0/BurnIn/
nohup python3 burn_in.py &

Use tail -f nohup.out to view and follow the output.

Wait 24 hours. During that time, put hand cleanser gel (60 / 70% alcohol) in front of sensor for a short while. After 24 hours, you'll get two new conf_ and state_ txt files in the conf subdirectory. Copy these to a conf folder in your Pi's home directory.

Sensor reading service

I split the tasks of reading data and viewing data into two separate independent pieces of code, deployed as systemd services.

This first one is used to read the sensors periodically. Regardless of whether I'm viewing the data, I want the results to be logged regularly so it makes sense to disconnect the reading process from the displaying of the data.

So, this service handled by a Python script airqread.py. The script is scheduled in a systemd service (systemctl airqread.service) which is set to start on boot.

My code then does the following, which is scheduled every 15 minutes using the schedule library - at 14, 29, 44 and 59 minutes past the hour (this is because there is a minute sleep before taking the air reading).

  1. Connects to the database, PMS7003 sensor and BME688 sensor (initialising with a stored state)
  2. Starts a thread to read the BME688 continuously. This continuously updates a global dictionary. Most readings are discarded but the latest one is used when writing to the database.
  3. Setup two schedules that run four times an hour:
    1. One schedule starts at 13, 28, 43 and 58 past the hour - this handles reading the PMS7003 data. At these times, the code will wait 50 seconds, wakeup the PMS7003, wait another 60 seconds, read the PMS7003 data, then sleep. This task finishes just under 10 seconds before...
    2. The second schedule starts at 0, 15, 30 and 45 past the hour - this writes the global dictionaries for the PMS7003 and BME688 outputs to the InfluxDB database.

The idea is I get readings every 15 minutes past the hour regularly with the correct timestamp in the database. The separate schedule for the PMS7003 ensures I read this sensor no more than the 4x the hour, as close to the database write as I dare. The BME688 is updated every few seconds however so that the BSEC library learns far more quickly from the sensor readings, but only the readings closest to the database write is actually used.

Sensor reading service - Setup

To setup the Python script for reading the sensors, first we'll need some dependencies:

sudo apt install -y python3-pip python3-influxdb python3-schedule python3-serial python3-busio python3-smbus2

You'll need a conf sub directory with your saved burn in data from above. The python file airqread.py can be in the home directory. It must be edited to put the filename of your burn-in state (edit line starting state_file_name=).

To set up a systemd service for starting the sensor read service on boot, you can use my airqread.service.

sudo cp ~/services/system/airqread.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable airqread.service
sudo systemctl start airqread.service
sudo systemctl status airqread.service

Web Interface (Python Bottle)

Now I'm recording the results, I wanted a web interface to see them with a graph over time. Being web based is convenient as I can see the results on my Smartphone, PC or Laptop, and even away from home if I connect to my VPN.

Screenshot of main interface page with graphs

Above: Screenshot of the main interface. Latest values are displayed and two 24h graphs. You may notice the spike in PM particles and air quality - that's cooking, followed by incense, which just goes to show how easily a small two bedroom flat of my own on a cold winters day gets saturated with particles. The spike of temperature drop/humidity increase is me taking the case lid off for the above photos!

Python is used here again- airqweb.py. The Bottle Web Framework is ideal for a simple service such as this as I can have all the code simply in one file and host a lightweight web service.

It's not just Python at work here though - HTML, CSS, JavaScript (with asynchronous / json based AJAX), and a big benefit from the JavaScript based chart.js.

It's a simple web page, showing a line chart over time, and latest values at the top for an 'at a glance' check of what I'm breathing in! It is also responsive, so displays on a Smartphone or Tablet quite well.

The Python based web server starts up on port 8080 and serves the static html page airqweb.html by default. This has linked JavaScript that will post to an /action end point with json request parameters. The only action that is really implemented is query_data, which takes an optional parameter for the number of days to go back. The JavaScript hard codes one day, so the graph displays a 24 hour period.

The JavaScript then translates the data received back from python into three arrays that can be used by chart.js to display the data. It also captures the last value and updates the three span tags with the result.

The page queries the data on load, and also on focus, so if you leave the tab open it doesn't consume resources until you switch to it.

The main interface only shows PM10, PM2.5, PM1.0, Air Quality, Temperature and Humidity (and latest Air Pressure value). To see all data, I created a separate web page for this (go to http://x.x.x.x/data) which queries all columns in the database and uses JavaScript DOM to dynamically make a table to display it. It only displays one day at a time, but the html control allows any day to be picked.

I may add more functionality in time (or by kind request) as it's just software changes. If you wished, you could also install Grafana instead and get the data out of influxdb via that interface.

Screenshot of main interface page with graphs

Web Interface - Setup

To set up, copy my files airqweb.html, airqweb.css, airqweb.js, airqweb.py and shutdown.html into your Pi's home directory.

sudo apt install -y python3-bottle

You can use my bottleweb.service to set up the web service for the camera.

sudo cp ~/services/system/bottleweb.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable bottleweb.service
sudo systemctl start bottleweb.service
sudo systemctl status bottleweb.service

The interface should be on port 8080 (try http://x.x.x.x:8080).

Reverse Proxy (nginx)

Rather than connect directly to the Python web service on port 8080 (or start that directly on port 80), I've installed nginx to use as a reverse proxy.

NGINX is a popular web server that is extremely popular now in the corporate as well as amateur worlds, and it's light enough to work well on the Pi Zero for the couple of users who would be accessing it.

The benefit of nginx is I can password protect, SSL enable or cache or many other ideas if I want, but for now it's just a simple reverse proxy.

To set up nginx:

sudo apt install -y nginx

The nginx default configuration is in the file /etc/nginx/sites-available/default. Edit that using:

sudo nano /etc/nginx/sites-available/default

Below is an extract of the added configuration lines (including the commented out original location):

location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_set_header X-NginX-Proxy true;
    proxy_pass http://127.0.0.1:8080/;
    proxy_redirect http://127.0.0.1:8080/ https://$server_name/;
}

#location / {
    # First attempt to serve request as file, then
    # as directory, then fall back to displaying a 404.
    #	try_files $uri $uri/ =404;
#}
  

Further tweaks

I've written a page of Raspberry Pi OS Tweaks that you should consider for this air quality monitoring project. Most of those tweaks have been applied to this project.

If you apply my recommendations, you'll need to go a step further with influxdb to reduce SD card writes:

sudo nano /etc/influxdb/influxdb.conf
...
  query-log-enabled = false
...
  cache-max-memory-size = "100m"
...
  cache-snapshot-memory-size = "25m"
...
  cache-snapshot-write-cold-duration = "1h"
...
[monitor]
  # Whether to record statistics internally.
  store-enabled = false
...

Then restart InfluxDB.

sudo systemctl restart influxd.service

These tweaks both reduced disk writing, but also reduced CPU usage.

Conclusion

This project was born out of having an interest in air quality and having a spare Pi Zero now unused. The Raspberry Pi and accessories are a great way that isn't too expensive to build your own projects. It's powered by USB and only needs one wire and can sit out of the way (as long as it can access the general room air).

In terms of hours of work, it's been a quick project. A few evenings here and there and a bit of weekends for the cutting/filing of the case and PCB build. I don't think there is anything commercially equivalent and it might cost more if so, so I'm pleased with the result.

References and more reading: