BME680 Air Quality low?

Yeah, run the example for a day or two, perhaps keeping note of when you open/close windows, and save all the data to a text file, then we can have a look at it. It’ll be a useful exercise to help to improve the example too!

1 Like

Ok, sounds like a plan @sandyjmacdonald.

I’ve just restarted my logging on the Pi with the BME680 attached, and also created a new dashboard on Beebotte that contains all the data (https://beebotte.com/dash/d1f81350-c942-11e7-bfef-6f68fef5ca14#.Wgr0uRO0NN0). You should also be able to visit the page as it is public, but hopefully that amount of data will help.

Due to API limitations on Beebotte, the readings are set to 5 minute intervals - you can disregard any readings taken before today as this was used when I started testing out my script.

1 Like

Pardon me but may I ask for some guidance here. I am up and running with the BME680 module thanks to technical assistance from gadgetoid. Having worked with DHTXX and related sensors for quite some time going down the well trodden telemetry logging track, I must confess that I am delighted to see Pimoroni make this BME680 available so “craftily” with the pin placements.

My question is how can I brush up on the VOC calculations to gain a better understanding of the monitoring. I’ve looked at the Bosch SDK (for the full module) but I am at a loss to convert the “resistance ohms” readings to some air quality index. Is the resistance value simply an indicator of the %VOC? Is dust a factor in these equations?

Looking forward to being educated! Thanks again.

Hey @baqwas, I had a similar confusion myself. That is, until I saw the example called indoor-air-quality.py which is referred to in the getting started guide for the BME680 board.

After @sandyjmacdonald’s reply above, I’m currently collecting more data in the hope we can increase the accuracy of that script to provide a more user-friendly air quality output.

Hope this helps?

Yes, your response definitely helps. I’ll step through indoor-air-quality.py in due course. To be honest, the acronym VOC had me salivating because a few decades ago, I had the unpleasant temporary job to log all VOC readings in a small plant to satisfy regulatory compliance. I was unnecessarily thinking of complicated measurements in the current context. Thx.

Fair warning! This is a long and somewhat rambling post, but it’s to try to illustrate the complexity of this whole thing…

The volatiles detected by the BME680 encompass a wide range of different compounds, everything from solvents to carbon monoxide. The trouble is that you can’t be much more specific than that, i.e. it’s not tuned, or not possible to tune it, to any one of these volatiles, so what you’re detecting is the overall mix. Fortunately, most of what we deem to be “air pollution” will fall under the umbrella of volatile organic compounds.

The gas resistance readings can be very simply interpreted as higher resistance = higher air quality and lower resistance = lower air quality. The problem then becomes how to translate that into something more meaningful. Here’s a bit of an aside about some of the issues we should bear in mind…


In a perfect situation, how would we work out the 100% air quality point?

There’s a few options:

The first would be to have a completely bare room, lined with material that gives off no VOCs, like stainless steel, with absolutely nothing in it, and filled with pure air. Not a very realistic or achievable scenario.

More realistic would be a “normal” room, painted, with some furniture, and fixtures and fittings in it. All of those things will release an amount of VOCs that would be comparable to a “normal” environment.

A “normal” room would probably have some people in it, right? So we need to factor that in too. Humans breath out VOCs, so their presence will affect the air quality.

There are also a bunch of other things to factor in, like boilers and gas hobs and ovens that will emit VOCs too. So you probably don’t want to set your baseline in a room that contains any of those.

If you live next to a busy road, then pollutants from passing vehicles will negatively affect the air quality in adjacent rooms, and air quality may actually drop when you open the windows. In more rural spots, the air quality should increase when you open the windows, because lack of ventilation is known to result in significant drops in indoor air quality.

Humidity is also part of indoor air quality. It’s thought that optimal indoor humidity is around 40%, and anything either side of that point negatively affects air quality.

In terms of the BME680 sensor itself, there will be some variation between sensors, so the baseline should be calculated and set for each sensor separately. The gas readings also take some time to settle once set running, and require some burn-in time (possibly up to 24 hours according to Adafruit, but we’ve found anecdotally that about 20 minutes should suffice).


Suggestions for setting your baseline

  1. A room that isn’t your kitchen (or attached to your kitchen) and doesn’t contain a boiler. Your bathroom probably isn’t ideal either due to the abnormal humidity levels. Perhaps a bedroom or living room?

  2. At a time when there are a “normal” number of people in the room in which you’re calibrating.

  3. In a well-ventilated room, but not at a time when there are a lot of cars passing any windows, e.g. at rush hours, if you live in a busy area. If you do live in a busy area, then it might be best to calibrate it with the windows closed!

  4. Not too close to any radiators or heat sources that will affect the humidity in the immediate vicinity.


So how does my example work?

It assumes that indoor air quality is comprised of 25% relative humidity, and 75% gas resistance reading. The humidity is clamped to a baseline of 40% relative humidity, i.e. 40% RH will add 25% to the overall score, and 0% RH would add 0% to the overall score and 100% RH would also add 0% to the overall score (in other words, it’s scaled differently either side of the 40% RH baseline).

Because the gas resistance is a somewhat arbitrary scale, it’s much harder to set the baseline. The approach that I took was to take 5 minutes of readings every second or so (assuming that there had been an at least 20 minute burn-in run the first time that the sensor was used) and assume that the first chunk of those 5 minutes worth of readings would be unreliable because it would still be stabilising. An average of the last 50 values taken (so the last minute or so of the 5 minutes) would be the 100% air quality baseline, i.e. adding 75% to the overall score. Let’s say that the baseline is set at 50,000 Ohms, for the sake of argument. A reading of 0 Ohms would then add 0% to the overall score, a reading of 50,000 Ohms would add 75%, a reading of 25,000 Ohms would add 36.5%, and so on… Any readings higher than 50,000 would still score 75%.


Problems with calculating indoor air quality

The first, and biggest issue, is that it’s a completely arbitrary thing. It’s like converting indoor air temperature to a fixed scale from 0 to 100%, it doesn’t make much sense, and is incredibly awkward to do. I mean, you could do something like saying that the optimal temperature is 21 degrees Celsius and it’s never going to go lower than 15 degrees or higher than 30 degrees, but they’re all assumptions.

No two people’s homes are the same, no two rooms within anyone’s home will be the same, and a particular room’s air quality will vary at different times of the day, on different days, and depending on outside variables like weather, traffic, and so on.

How should the air quality score be comprised? The 25/75% score that I came up with was completely arbitrary and probably not correct. I suppose to more correctly set that balance you would have to come up with a measure of the relative effects of humidity and VOCs on human health.


Ways in which my example could be improved

The baseline should probably change through time. In other words, if readings higher than the baseline are found for a period of time, then shift the baseline up to that new level.

To account for short transient spikes, a moving average might be a better way to score the air quality, to smooth out those spikes, although you lose a small amount of precision doing this.

Perhaps a nice way to set the baseline would be to take 5 minutes worth of readings in each room in your home and then use the aggregate of those readings to better set the baseline?


Other approaches

As humans, I think we like to assign numbers and scores to things, even if they’re things that don’t make sense when numbers are attached. We’re trying to turn something that is inherently qualitative into something quantitative.

The much easier, but perhaps less easy to interpret, way to do all of this is just to plot the raw resistance readings and humidity readings and then look at the trends and interpret falls and rises. For instance, you look at a day’s worth of readings and see that there was a rise in gas resistance readings when you came home in the evening and opened the bedroom window, or there was a rise in humidity during the night when you were lying in bed sweating and breathing, and so on… Less easy to instantly interpret, but probably more informative.

Perhaps the ultimate approach is to do both, and then compare the indoor air quality score through time to the raw readings and events, and see how much sense they make…?


Sorry for the long and rambling nature of that! I hope some of it might be useful though?

5 Likes

Thanks for this very detailed explanation @sandyjmacdonald, this really helps shed light on what you’ve done, and how it can be interpreted.

Whilst this is quite lengthy, do you think it could be worth linking to in someway on the “Getting started” guide for the BME680? I know you refer to air quality in there, and the difficulties faced when estimating this, but it may be useful for those who want to know a little more about how you came up with your example.

I’m still collecting data on mine, and still happy to share if you think it could help. For me, I would love to have an example on how to calculate an indoor quality test based on the calibration data obtained, then I wouldn’t need to get in touch as often for help. However, I realise this is probably a more advanced way of doing things.

1 Like

I might try to wrangle it into a blog post and then cross-link it from the product page and the tutorial.

I think from a very basic point of view, run the sensor for 24 hours reading the raw resistance values, as you are, look at the data, and then decide where to set the baseline. That’s a more sensible way than relying on the way that my script does it.

So, essentially, it would be:

#!/usr/bin/env python

import bme680
import time

sensor = bme680.BME680()

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)

sensor.set_gas_heater_temperature(320)
sensor.set_gas_heater_duration(150)
sensor.select_gas_heater_profile(0)

start_time = time.time()
curr_time = time.time()

try:
    gas_baseline = INSERT_BASELINE_VALUE_HERE
    hum_weighting = 0.25

    while True:
        if sensor.get_sensor_data() and sensor.data.heat_stable:
            gas = sensor.data.gas_resistance
            gas_offset = gas_baseline - gas

            hum = sensor.data.humidity
            hum_offset = hum - hum_baseline

            if hum_offset > 0:
                hum_score = (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * (hum_weighting * 100)

            else:
                hum_score = (hum_baseline + hum_offset) / hum_baseline * (hum_weighting * 100)

            if gas_offset > 0:
                gas_score = (gas / gas_baseline) * (100 - (hum_weighting * 100))

            else:
                gas_score = 100 - (hum_weighting * 100)

            air_quality_score = hum_score + gas_score

            print("Gas: {0:.2f} Ohms,humidity: {1:.2f} %RH,air quality: {2:.2f}".format(gas, hum, air_quality_score))
            time.sleep(1)

except KeyboardInterrupt:
    pass
2 Likes

Thanks, sounds straight forward. Now just need to wait for the data… I’ll share my findings when I have news.

Ok then @sandyjmacdonald and @baqwas, I have been collecting data using the read-all.py script and averaged out the readings for the gas resistance, then I added them to the script Sandy provided above and… It threw an error (hum_baseline is not defined), so a quick scan through the code and I added hum_baseline = 40.0 as per the original code, and then hey-presto, it worked!

So here is the code I used (minus the average gas resistance readings, as you would need to use your own value here) - when I ran the code with my calibrated value, it reported an air quality of 98.49%, which seems more accurate than the 30 - 40% I was getting before:

#!/usr/bin/env python

import bme680
import time

# Define the BME680 sensor
sensor = bme680.BME680()

# 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)

sensor.set_gas_heater_temperature(320)
sensor.set_gas_heater_duration(150)
sensor.select_gas_heater_profile(0)

start_time = time.time()
curr_time = time.time()

try:
    # Set the gas resistance baseline. It is recommended
    # to run 'read-all.py' for at least 24 hours 
    # and then average the resuls for this value.
    gas_baseline = INSERT_BASELINE_VALUE_HERE
    
    # This sets the balance between humidity and gas reading in the 
    # calculation of air_quality_score (25:75, humidity:gas)
    hum_weighting = 0.25
    
    # Set the humidity baseline to 40%, an optimal indoor humidity.
    hum_baseline = 40.0

    while True:
        if sensor.get_sensor_data() and sensor.data.heat_stable:
            gas = sensor.data.gas_resistance
            gas_offset = gas_baseline - gas

            hum = sensor.data.humidity
            hum_offset = hum - hum_baseline

            # Calculate hum_score as the distance from the hum_baseline.
            if hum_offset > 0:
                hum_score = (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * (hum_weighting * 100)

            else:
                hum_score = (hum_baseline + hum_offset) / hum_baseline * (hum_weighting * 100)

            # Calculate gas_score as the distance from the gas_baseline.
            if gas_offset > 0:
                gas_score = (gas / gas_baseline) * (100 - (hum_weighting * 100))

            else:
                gas_score = 100 - (hum_weighting * 100)

            # Caclulate air_quality score.
            air_quality_score = hum_score + gas_score

            print("Gas: {0:.2f} Ohms,humidity: {1:.2f} %RH,air quality: {2:.2f}".format(gas, hum, air_quality_score))
            time.sleep(1)

except KeyboardInterrupt:
    pass

I’m now just going to re-add the relevant code to send the data to my Beebotte dashboard to provide a visual representation.

EDIT: Dashboard on Beebotte can be viewed here for those who are interested - it will still show the “old” values as I couldn’t be bothered to setup an entire new channel and dashboard, but you can now clearly see an improvement in the air quality as a result of the calibration.

2 Likes

Fantastic! \o/ \o/ \o/

The detailed explanation is truly useful. I have a “dust” sensor and a gas sensor that I will rig up for comparative assessment (over the weekend). Thanks again for taking the time to explain something that IMHO Bosch should have done with its documentation (or perhaps its audience is already familiar with the concepts).
The stability of the other readings (temp, humidity, etc.) with BME680 is remarkable given my experience with the DHTXX family and PiSenseHAT.

Regards.

This is a great thread and has given some insite, what would be interesting is to know what the average baseline for gas most people are getting. i am using mine as part of and ESP8266 feeding node red on a pi so code is a little different and would like just to use a guideline range for good and bad air quality

ta

I suspect another factor was causing the issue for me too - due to API limitations on Beebotte, I had to limit the times that the BME680 sensor was used to every 5 minutes (as opposed to every second).

I later suspected that the sensor needs to be warmed up in order to calculate the gas quality, and as I was only running this every 5 minutes, the sensor was not warmed up enough to calculate an accurate reading.

I’ve since changed my script so that it outputs the reading to a CSV file (every second), and then implemented a secondary script that sends the last read line from the CSV file to Beebotte every 10 minutes to overcome the API limitation.

I’ve updated the link in my previous post to the dashboard showing these results if anybody is interested.

Hi,

So I’ve started working with the BME680 but with a feather huzzah. I’m using the codes provided by bosch. I’m trying to get use out of the sensor.gas to get VOC values, but they never stabilizes. I ran it for 5 days and the value continued to get larger and larger. I’m re-running it now but I started at gas value of 969020ohm, and an hour later I’m at 1113411ohms and four more hours after its 1214995ohms. I’m using I2c and I’m powering through usb. Can you help decipher what is going wrong? the IAQ score from the bsec libaray prints a reasonable value, but is a hassle having to re-expose the sensor to bad gas for calibration.

Just to continue on the interesting discussion I wanted to share some interesting trend I notice.

I used to just plot the last 24h data with python and not to store any data, until I finally decided to use the InFlux database + Grafana combo to store the sensor data and be able to explore larger trends.

It looks to me that to calibrate the sensor maybe temperature should also be taken into account


You can see in fact a nice anti-correlation between temperature and gas sensor. True, correlation does not imply causation, so you might argue that the temperature goes down at night because the heatings are off and the air quality goes up because we leave the room with the sensor empty (kitchen + living room), we stop cooking and we go to the bedroom so that the VOC also lowers and one does not have to affect the other. However the two-days average seems to have the same anti-correlation, which goes beyond the day-night cycle.

3-4 days is not enough to say anything definitive, so I’ll keep an eye on it.

1 Like

Ive found that when this happens, the cause is typically the script running twice resulting in the hot plate getting to twice the targeted temperature.

For example, If my baseline is 130,000 ohms, the read-all script calls for the hot plate to heat to 300, but if I have nodered running in the background and it is polling the sensor because I forgot to delete a configuration, the read-all script would also call for the hot plate to heat to 300, resulting in the hot plate actually heating to 600, and the readings being either unusable or significantly higher than what they should be, for me it was 260,000+ ohms. I did have it continuously heat several times, sending the resistance to the moon.

I purchased my BME680 when they first came out after playing around with the BME280. I largely stopped my project because I had spent a lot of time learning python on PI, and moved on to the next project. Ive picked it up again in the past week now that a RPI library has been released. (Maybe I Somehow missed it at the time?)

Blockquote
The much easier, but perhaps less easy to interpret, way to do all of this is just to plot the raw resistance readings and humidity readings and then look at the trends and interpret falls and rises. For instance, you look at a day’s worth of readings and see that there was a rise in gas resistance readings when you came home in the evening and opened the bedroom window, or there was a rise in humidity during the night when you were lying in bed sweating and breathing, and so on… Less easy to instantly interpret, but probably more informative.

Perhaps the ultimate approach is to do both, and then compare the indoor air quality score through time to the raw readings and events, and see how much sense they make…?

Blockquote

This is pretty close to how Bosch is solving this problem. Theyre about to release an array of bme680s on an adafruit esp board preloaded with BSEC AI. The sample program is able to detect the different types of coffee beans.

I’ve had a BME680 for some time and would really like to be able to use it with MicroPython and the display on my Pico Explorer.
Any chance of a suitable driver soon?

I’ve tried to edit the Adafruit module but I get stuck on the line:

super().__init__(refresh_rate=refresh_rate)

which falls over with a OS Error 5 ???

I thought we had a solution here with Adafruit Blinka et al built into the Pimoroni UF2 v 2.1 but I am still having a problem.

# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
from pimoroni_i2c import PimoroniI2C


PINS_PICO_EXPLORER = {"sda": 20, "scl": 21}
#i2c = PimoroniI2C(**PINS_BREAKOUT_GARDEN)

import time
import board
import adafruit_bme680
import busio

# Create sensor object, communicating over the board's default I2C bus
#i2c = board.I2C()  # uses board.SCL and board.SDA
i2c = PimoroniI2C(**PINS_PICO_EXPLORER)
bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c, debug=False)

# change this to match the location's pressure (hPa) at sea level
bme680.sea_level_pressure = 1013.25

# You will usually have to add an offset to account for the temperature of
# the sensor. This is usually around 5 degrees but varies by use. Use a
# separate temperature sensor to calibrate this one.
temperature_offset = -5

while True:
    print("\nTemperature: %0.1f C" % (bme680.temperature + temperature_offset))
    print("Gas: %d ohm" % bme680.gas)
    print("Humidity: %0.1f %%" % bme680.relative_humidity)
    print("Pressure: %0.3f hPa" % bme680.pressure)
    print("Altitude = %0.2f meters" % bme680.altitude)

    time.sleep(1)

MicroPython v1.15 on 2021-05-20; Raspberry Pi Pico with RP2040

Type “help()” for more information.

%Run -c $EDITOR_CONTENT
Traceback (most recent call last):
File “”, line 17, in
File “adafruit_bme680.py”, line 430, in init
ImportError: no module named ‘adafruit_bus_device’

It appears to need the full strength "adafruit_bus_device" rather than busio

I’ve plugged the BME680 into the SDA and SCL black sockets on the Pico Explorer. The full sized bme680.py has been installed on the Pico.

Help, please.