---
title: 'Project documentation Simon Alvhage'
disqus: hackmd
---
Air quality sensor using BME280, CCS811 and DS18X20
===
## Table of Contents
[TOC]
## Tutorial on how to build a air quality sensor
In this tutorial I will explain how to assemble and program a complex air quality sensor using 3 important sensors. I wanted to measure the quality of air that could be used both inside and outside. Since the project is using the lopy4 microcontroller which has in build Lora support this project is perfect for outside measuring.
This air quality sensor can measure a precise value of the temperature, humidity, pressure, altitude, eCO2 and tVOC. The values are then presented on a dashboard online with graphs of the sensor-values.
The project will take approximate 2 hours to complete.
My name: Simon Alvhage
User credentials: sa224ka
Objective
---
The quality of air is a very important subject since “bad” air with a lot of co2 and impurities does harm us humans and we need fresh air. Even the humidity has a large effect on us when it comes to concentration in for example studying.
I chose this subject because a wanted to get a better understanding of how the quality of air is around me. One purpose of the sensor is to be able to place it on a heavy trafficated road or maybe the road I am walking every day and be able to measure how good(or bad) the air quality is. But also in my apartment. Are the humidity values to low in where I live or not?
You can also make conclusions after an air quality measurement in your apartment if you maybe need to replace your plastic plants with some real ones.
Material
---
| IoT Thing | Specifications | Price | Link |
| -------- | -------- | -------- |-------- |
| Lopy4 | Based on Esp32 chip, 4MB RAM, 8MB FLASH, WIFI, Bluetooth, LoRa SigFox, 2 x UART, SPI, 2 x I2C, I2S.| 34.95€ |[Pycom](https://pycom.io/product/lopy4/) |
| -------- | -------- | -------- |-------- |
| Expansion Board 3.0 | USB and LiPo battery powered, LiPo battery charger, MicroSD card slot | 16€ |[Pycom](https://pycom.io/product/expansion-board-3-0/) |
| -------- | -------- | -------- |-------- |
| (LoRa & Sigfox Antenna Kit) | External antenna,RF Cable Assemblies RP-SMA (Female) JK-IPEX MHF U.FL 1.13 100MM | 9€ |[Pycom](https://pycom.io/product/lora-868mhz-915mhz-sigfox-antenna-kit/) |
| -------- | -------- | -------- |-------- |
| BME280 I2C SPI Sensor | Temperature range: -40°C to +85°C, Humidity range: 300hPa to 1100 hPa, Humidity sensor ±3%, I2C (3.4 MHz) and SPI (10 MHz)| 6.75€ |[Makershop](https://www.makershop.de/en/sensoren/temperatur/bme280/) |
| -------- | -------- | -------- |-------- |
| CJMCU-811 Gas Sensor I2C (CCS-811) | Measures CO2 and tVOC. I2C (3.4 MHz) and SPI (10 MHz)| 16.55€ |[Makershop](https://www.makershop.de/en/sensoren/gas/cjmcu-811-gas-sensor-i2c-kohlenstoff-monoxid-co-vocs-air-quality-arduino-ccs811/) |
| -------- | -------- | -------- |-------- |
| DS18B20 Temperature sensor | Supports 1 wire configuration, ±0.5°C| 3.00€ |[Makershop](https://www.makershop.de/en/sensoren/temperatur/ds18b20-digital/) |
| Breadboard | handy when prototyping| 9€ |[Kjell](https://www.kjell.com/se/produkter/el-verktyg/elektronik/elektroniklabb/kopplingsdack-400-hal-p36285) |
| Jumper Wires | handy when prototyping| 9€ |[Kjell](https://www.kjell.com/se/produkter/el-verktyg/arduino/tillbehor/luxorparts-delbar-kopplingskabel-40-pol-hane-hane-p87901) |
| 3.3kOhm resistor | used with the DS18X20 one wire temperature sensor| 1€ |[Electrokit](https://www.electrokit.com/en/product/resistor-3w-3-3kohm-5-3k3/) |
<img src="https://i.imgur.com/l91l7gX.jpg" width="300" height="200"></img>
Fig 1. Lopy4. I chose the lopy4 because this board has a lot of IoT connectivity like SigFox and LoRa. It also supports a lot of GPIOs and I2C
<img src="https://i.imgur.com/bKDgT4H.png" width="300" height="300"></img>
Fig2. Expansion Board 3.0. In this project, I have chosen to add the expansion board because it is very easy to connect sensors and jumper wires to it. Also, the micro USB power option is perfect to keep the project simple.
<img src="https://i.imgur.com/bElMv4O.png" width="300" height="300"></img>
Fig 3. LoRa & Sigfox Antenna Kit. This is the antenna needed to connect to the SigFox and LoRa networks.
<img src="https://i.imgur.com/S7utwPu.jpg" width="300" height="300"></img>
Fig4. BME280 I2C SPI Sensor. I chose this sensor because it is a sensor that measures a lot of different things. It measures temperature, humidity, pressure and calculates the altitude with a given library. I did not want to work with the DHT11 since it only measures the temperature (with a bad accuracy) and the humidity.
<img src="https://i.imgur.com/16Bcfog.jpg" width="300" height="300"></img>
Fig5. CJMCU-811 Gas Sensor I2C (CCS-811). This sensor is perfect for the project because it measures the CO2 and VOC (VOC is the Volatile organic compound).
There is more sensor like this that measures co2 but this is exremly power-efficient and accurate.
<img src="https://i.imgur.com/ihNCAQz.jpg" width="300" height="300"></img>
Fig6. DS18B20 Temperature sensor. The BME280 has a temperature sensor but since I think temperature is such an important part of this project, I picked this sensor because it has one of the best accuracies in the market (±0.5°C)
Computer setup
---
To get started with the Lopy4 and expansion board it is recommended to install Atom or Visual Studio Code. I have been using both programs before this project and both have their advantages and disadvantages. I chose to go with Atom in this project just because of the simplicity. I am using a Windows PC. These are the steps to get started
1. Download the pycom firmware updater and select the correct COM port and flash your lopy board with the latest firmware named "legacy"
2. Download Atom and PyMkr from the Atoms package website.
3. Open Atom and the program will automatically connect to the correct COM port
4. Create a project folder consisting of 2 documents; "main.py" and "boot.py".
5. Create another folder inside your project folder called "lib"
> Download Pycom Firmware Updater here: [docs.pycom.io](https://docs.pycom.io/gettingstarted/installation/firmwaretool/)
> Download Atom here: https://atom.io/
> Download PyMkr here: https://atom.io/packages/pymakr
Next step is to install the libraries for the BME280, CCS811, DS18X20 sensors.
The sensors will need libraries with functions that read the correct bytes of the sensor and converts it to readable numbers. To install the libraries you will need to download the python file and paste it in the lib folder.
>Download BME280_float library: https://github.com/robert-hh/BME280
>Download CCS811 library: https://github.com/Ledbelly2142/CCS811
>Download onewire library for DS18B20: https://github.com/pycom/pycom-libraries/tree/master/lib/onewire
>Download urequest for Ubidots: https://github.com/jotathebest/micropython-lib/tree/master/urequests
The last one is needed for establishing a connection to the dashboard on Ubidots website over Wifi. This is not necessary when using LoRa
I'm also getting the latest sea-level pressure from OpenWeatherAPI which is a free API tool. Register on the website and then use this URL to fetch the current weather data
>api.openweathermap.org/data/2.5/weather?q={city name}&appid={your api key}
API key can be found in the settings.
To flash the Lopy4 and upload the code with the including libraries you will need to plug in your lopy4, navigating to the correct project folder and then press the "Upload" button (See image below). The project code should always end up in the main.py file. The boot.py is used if you want to run some code before the main program is runned. Could be used for enabling a specific network mode for example.

The project files is now uploaded to the Lopy and it can be used without a computer nearby.
Putting everything together
---

The DS18X20 is meant for 5v but the lopy only has a 3.3V, I tried using 3.3v and the 4.7kOhm resistor that was suggested with the sensor with no luck. The sensor was only showing 25 degrees everywhere.
I later used the formula U = R * I to calculate the new resistance. With a change from 5v to 3.3v the resistor will need to be around 3.2kOhm which worked.
The BME280 and CCS811 use the same 2 data wires but since those two data wires connect to the SDA and SCL port we can establish a bus and a clock that will enable us to read both sensor readings.
Platform
---
My choice of platform is Ubidots. It has a simple dashboard view with a lot of functions. Since I'm working a lot with APIs this platform is the best. Simple POST and GET requests over the HTTP protocol can be sent to the ubidots servers. I'm using post requests to send my sensor data over wifi to ubidots but it is possible to send data to TheThingsNetwork and add an integration with Ubidots there. Since I'm 60km to the closest LoraWan gateway I'm focusing on Wifi.
I have tried multiple platforms like; losant and my own solution where I have a database server in my home. But to keep it simple and accessible for anyone I'm focusing on Ubidots free account.
Ubidots is a cloud platform, free for students and has a lot of functions. If you want to scale your product you get easily go from ubidots and send GET requests and parse your data to the device you want.
CODE
---
This is the code from the main.py file.
```python
from network import WLAN
import urequests as requests
import machine
import time
import pycom
from machine import Pin
import struct
from machine import I2C
import bme280_float as bme280
import CCS811
from onewire import DS18X20
from onewire import OneWire
import gc
pycom.heartbeat(False)
#Token for ubidots
TOKEN = "YOURUBIDOTSTOKEN"
DELAY = 1
# Assign your Wi-Fi credentials
wlan.connect("YOURWIFINAME", auth=(WLAN.WPA2, "YOURWIFIPASS"), timeout=5000)
while not wlan.isconnected ():
machine.idle()
print("Connected to Wifi\n")
# Builds the json to send the request
def build_json(variable1, value1, variable2, value2, variable3, value3,variable4, value4, variable5, value5, variable6, value6, variable7, value7):
try:
data = {variable1: {"value": value1},
variable2: {"value": value2},
variable3: {"value": value3},
variable4: {"value": value4},
variable5: {"value": value5},
variable6: {"value": value6},
variable7: {"value": value7}}
return data
except:
return None
# Sends the request.
def post_var(device, value1, value2, value3, value4, value5, value6, value7):
try:
url = "https://industrial.api.ubidots.com/"
url = url + "api/v1.6/devices/" + device
headers = {"X-Auth-Token": TOKEN, "Content-Type": "application/json"}
data = build_json("Temperature BME280", value1, "Pressure", value2, "Humidity", value3,"Altitude", value4, "CO2", value5, "tVOC", value6, "Temperature DS18X20", value7)
if data is not None:
print(data)
req = requests.post(url=url, headers=headers, json=data)
return req.json()
else:
pass
except:
pass
#Calculates the Average from the list
def Average(lst):
return sum(lst) / len(lst)
#Takes the average, loops through the list, calculates the new average without the index number we are controlling
#If the average without the index is 5% less than the old average number, the index will be changed to the new average number
#This is a way of detecting spikes in sensor values.
def FixSpikes(oldlst):
Avg = sum(oldlst) / len(oldlst)
for x in range(len(oldlst)):
avgwithoutx = (Avg*len(oldlst)) - oldlst[x]
avgwithoutx = avgwithoutx / (len(oldlst) -1)
if (avgwithoutx < (Avg * 0.95)):
oldlst[x] = avgwithoutx
return (sum(oldlst) / len(oldlst))
while 1:
#establishing a connection to the openweathermap json api and parsing out the changing sealevel pressure that is changed every hour
jsondoc = requests.get(url="https://api.openweathermap.org/data/2.5/weather?id=2708365&appid=570f383f663ad9d7b409d5481d7312bf").json()
sealevelpressure = int(jsondoc["main"]["pressure"])
x = 0
tempbme2 = []
presbme2 = []
humibme2 = []
altitude2 = []
co22 = []
voc2 = []
tempDSDONE2 = []
for x in range(10):
#reads the i2c bus on the correct address, sets the sealevelpressure and simply using the library to parse out temperature, Humidity and pressure.
#You can find your correct bus address with the scan() function.
i2c = machine.I2C()
bme = bme280.BME280(i2c=i2c, addr=118)
bme.sealevel = (sealevelpressure * 100)
print("Reading BME280")
b = ["", "", ""]
bme.read_compensated_data(b)
tempbme = b[0]
presbme = b[1]/100
humibme = b[2]
altitude = bme.altitude
i2c = I2C(0)
i2c = I2C(0, I2C.MASTER)
i2c = I2C(0, pins=('P9','P10'))
i2c.init(I2C.MASTER, baudrate=20000)
ccs = CCS811.CCS811(i2c=i2c, addr=90)
ccs.data_ready()
time.sleep(1)
print("Reading CCS811")
co2 = ccs.eCO2
voc = ccs.tVOC
#inits the onewire on pin20 and reads the values, sometimes it returns NULL so i created a while loop that only exits when the values is a boolean
ow = OneWire(Pin('P20'))
tempDS = DS18X20(ow)
print("Reading DS18B20")
tempDSDONE = tempDS.read_temp_async()
time.sleep(1)
tempDS.start_convertion()
#add every value to a list
tempbme2.append(tempbme)
presbme2.append(presbme)
humibme2.append(humibme)
altitude2.append(altitude)
co22.append(co2)
voc2.append(voc)
if isinstance(tempDSDONE, float) == True:
tempDSDONE2.append(tempDSDONE)
time.sleep(30)
#run the Spike-fixer and then send the post request to ubidots
post_var("Air-sensor", FixSpikes(tempbme2), FixSpikes(presbme2), FixSpikes(humibme2), FixSpikes(altitude2), FixSpikes(co22),FixSpikes(voc2), FixSpikes(tempDSDONE2))
#clears the memory
gc.mem_free()
```
This is the code from the Boot.py file
```python
from machine import UART
import machine
import os
from network import WLAN
#setting the clock speed
wlan = WLAN(mode=WLAN.STA)
uart = UART(0, baudrate=115200)
os.dupterm(uart)
machine.main('main.py')
```
Transmitting the data / connectivity
---
Before we can start sending data to our final destination we need to establish an internet connection first. This is done with the inbuilt function wlan.connect().
The protocol I am using is HTTP where I establish a TCP connection to the dashboard server and then sending data with the inbuilt POST method. The POST method sends data to a target with the given text format. In this case we are using JSON.
We start by getting all our sensor values to float variables. The float variables are later sent in the post_var() function which takes (in my case) 7 input parameters, which is our values. The post_var() creates a URL address to the API that is "https://industrial.api.ubidots.com/" followed by the device id of our choice. We set the headers that this message is a JSON message and the private token that we receive from the Ubidots. (Profile->API credentials -> Token) Then we need to build our JSON message.
The build_json() function is called with the name of the sensor value and the sensor value itself. The function sets the name as the key and the sensor value as the JSON value. Then we use the requests function post to post our JSON message to the server that is later displayed on the Ubidots dashboard.
The device will show up in the device settings and in our dashboard, we can assign graphs to the correct sensor value.
In my project, I chose to send the data every 5 minutes. This is perfect timing, It is very unnecessary to send it every 1 minute or so. And since I'm using transmission over wifi I am not limited by bandwidth. Sigfox has max messages per day and max size of the message.
But my projects measure the sensors every 30 seconds to later take an average that is sent every 5 minutes. This is also an effective way to eliminate spikes. I have tried sending data through Lora and also SigFox with success. I did use lora in my parent's city where there was a gateway nearby. Sigfox max limit of message size is to low for my project. 7 floats are a lot of bytes. I could trim it down to maybe one decimal but requests over wifi have no limits*. So that's why I chose Post requests over Wifi.
>*Ubidots has a limit of 4000 readings per day which is a lot.
Presenting the data
---

This is the simple dashboard I set up. This is 7 graphs that show the sensor values past 3 days in the writing moment. You can see my experiment with the altitude. The altitude is calculated with the sea-level-pressure. This is not constant and needs to be updated all the time. That's why I create a use openweather API to parse the latest pressure. That's why it crashes on some points.
To create a new graph you will need to press the plus sign at the top right corner and then choose your type of display. Could be a graph or monitor or something else. I chose a line chart. Then pick add variables and add the correct sensor. Done
The data on ubidots is saved for 1 month.
Finalizing the design
---
The final dashboard can be shown above and at the writing time the sensor values are:
Altitude: 5.7m
Co2: 407ppm
Humidity: 40.55%
Pressure: 998.32hPa
Temperature BME280: 26.77C
Temperature DS18x20: 26.57C
tVOC: 0.09mg/m^3
The project went good, it was fun and I learned a lot. One thing that could have made the project even better is that there would be a LoraWan gateway in my city so I can test it outside where i don't have wifi. Also, my original idea was to 3d print housing for the lopy and mounting solar panels on the box. The lopy would be powered by some 18650 batteries I have laying around and I would use DeepSleep to enable a very long battery life. But Lora did not work and I did not get my solar panel and battery charger module. So I kept everything simple instead.
The LoPy is a very powerful device that I will use more in the future. I think Sigfox and LoraWan is the future of IoT and I cant wait to start the next project...

###### tags: `BME280` `CCS811` `DS18X20` `Lopy4`