Project Summary
In my contest project, I documented the process of measuring air quality using Raspberry Pi and the SPS30 particle matter sensor. I shared my journey through a series of blog posts.
The first post, "Introduction," gives an overview of the project and its objectives. It explains the importance of monitoring air quality and introduces the Raspberry Pi as the central component of the measurement system. Link
In the second post, "Sensor Sensirion SPS30," I dive into the details of the Sensirion SPS30 sensor, which I used to measure various pollutants. I discuss its features, wiring, and communication with the Raspberry Pi. Link
The third post, "PCB," focuses on the design and fabrication of a custom PCB (Printed Circuit Board) for the project. I explain the PCB layout, components, and connections, highlighting the importance of a well-designed board for accurate measurements. Link
Moving on to the fourth post, "First Measurements," I share the exciting results of the initial measurements conducted using the Raspberry Pi and SPS30 sensor. I discuss the data collection process and present visualizations of the recorded air quality parameters. Link
Lastly, in the fifth post, "Further Measurements," I provide an update on the project, showcasing additional measurements and analyses. I discuss the challenges faced during the project and propose potential solutions for improving air quality based on the collected data. Link
Shopping list
here is a little shopping list, for the items you will need in this project:
- Raspberry Pi 3B+ or later ~60€
- SPS30 Particulate Matter (PM) sensor plus cable~49€
- SPS30 Interface HAT PCB (with nice purple colour) ~5€
- SMD connector parts ~5€
- Enclosure 10-20€
- optional BME680 Bosch Sensor (Adafruit Board) ~20€
Schematic and PCB
The schematic and PCB were designed in a free version of Eagle and manufactured by JLCPCB.
The schematic/PCB does not only connect the SPS30 Sensor to the Raspberry PI via the HAT connector, but I have also added QWIC connectors, so you can add your favourite Adafruit or Sparkfun board to the Raspi. In my case, I have added a BME680 Adafruit board, in order to get even more environmental data like temperature and humidity and air pressure.
My hardware setup
So my hardware setup looks like this:
And as promised you can finally see the nice purple colour of the SPS30 Interface HAT:
Python Code
I have used the Thonny IDE on the Raspberry PI. It is a very nice and helpful tool to develop and run the Python Code directly on Raspi.
external libraries:
in order not to develop the wheel myself, if have used the following external libs that need to be installed before starting the code:
SPS30 library from: https://github.com/dvsu/sps30
thingspeak library from: https://thingspeak.readthedocs.io/en/latest/
<optional> BME680 library from: https://github.com/pimoroni/bme680-python
here is the full code:
import sys import json from time import sleep from sps30 import SPS30 import bme680 import thingspeak #adjust these values here channel_id = 123456 # PUT CHANNEL ID HERE mywrite_key = 'keykeykey' # PUT YOUR WRITE KEY HERE myread_key = 'keykeykeykey' # PUT YOUR READ KEY HERE if __name__ == "__main__": channel = thingspeak.Channel(id=channel_id, api_key=mywrite_key) #BME680 Sensor try: sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY) except (RuntimeError, IOError): sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY) # These calibration data can safely be commented # out, if desired. print('Calibration data:') for name in dir(sensor.calibration_data): if not name.startswith('_'): value = getattr(sensor.calibration_data, name) if isinstance(value, int): print('{}: {}'.format(name, value)) # These oversampling settings can be tweaked to # change the balance between accuracy and noise in # the data. sensor.set_humidity_oversample(bme680.OS_2X) sensor.set_pressure_oversample(bme680.OS_4X) sensor.set_temperature_oversample(bme680.OS_8X) sensor.set_filter(bme680.FILTER_SIZE_3) sensor.set_gas_status(bme680.ENABLE_GAS_MEAS) print('\n\nInitial reading:') for name in dir(sensor.data): value = getattr(sensor.data, name) if not name.startswith('_'): print('{}: {}'.format(name, value)) sensor.set_gas_heater_temperature(320) sensor.set_gas_heater_duration(150) sensor.select_gas_heater_profile(0) # Up to 10 heater profiles can be configured, each # with their own temperature and duration. # sensor.set_gas_heater_profile(200, 150, nb_profile=1) # sensor.select_gas_heater_profile(1) #SPS30 Sensor pm_sensor = SPS30() try: print(f"Firmware version: {pm_sensor.firmware_version()}") print(f"Product type: {pm_sensor.product_type()}") print(f"Serial number: {pm_sensor.serial_number()}") print(f"Status register: {pm_sensor.read_status_register()}") print(f"Auto cleaning interval: {pm_sensor.read_auto_cleaning_interval()}s") #print(f"Set auto cleaning interval: {pm_sensor.write_auto_cleaning_interval_days(2)}s") pm_sensor.start_measurement() except: print("Exception Initialisierung") while True: try: #SPS30 SPS30_werte=pm_sensor.get_measurement() print(json.dumps(pm_sensor.get_measurement(), indent=2)) #BME680 if sensor.get_sensor_data(): output = '{0:.2f} C,{1:.2f} hPa,{2:.2f} %RH'.format( sensor.data.temperature, sensor.data.pressure, sensor.data.humidity) if sensor.data.heat_stable: print('{0},{1} Ohms'.format( output, sensor.data.gas_resistance)) else: print(output) #{'sensor_data': {'mass_density': {'pm1.0': 0.345, 'pm2.5': 0.379, 'pm4.0': 0.391, 'pm10': 0.397}, 'particle_count': {'pm0.5': 2.347, 'pm1.0': 2.726, 'pm2.5': 2.748, 'pm4.0': 2.752, 'pm10': 2.754}, 'particle_size': 0.479, 'mass_density_unit': 'ug/m3', 'particle_count_unit': '#/cm3', 'particle_size_unit': 'um'}, 'timestamp': 1678649674} if SPS30_werte: #we check if the dict is filled, this is not the case at the very beginning print(SPS30_werte['sensor_data']['mass_density']['pm1.0']) try: response = channel.update({'field1': sensor.data.temperature, 'field2': sensor.data.pressure, 'field3': sensor.data.humidity, 'field4': SPS30_werte['sensor_data']['mass_density']['pm1.0'], 'field5': SPS30_werte['sensor_data']['mass_density']['pm2.5'], 'field6': SPS30_werte['sensor_data']['mass_density']['pm4.0'], 'field7': SPS30_werte['sensor_data']['mass_density']['pm10'], 'field8': SPS30_werte['sensor_data']['particle_count']['pm2.5']}) except: print("Error while communicating with thingspeak.com") sleep(4) except KeyboardInterrupt: print("Stopping measurement...") pm_sensor.stop_measurement() sys.exit()
So what next:
Next, I will finish the enclosure for the hardware and I'll be taking more measurements this coming winter when my beloved neighbours get their wood stoves glowing and the outside air stinking again.
Another cool follow-up project would be integrating the air quality measurement system with a Switchbot system would allow for dynamic control of my existing (not smart and sensor-free) air purifier.