# A classroom environment sensor by Magnus Dahlström (mdagy99@Linneaus University) This is a tutorial about building a room environment sensor that tracks temperature, humidity, air pressure, CO2, and VOC-levels. Further it gives a description on how to transmit and visualize the data using TTN and Ubidots. Time for completion 2-6 h depending on your skill level. ## Objective ### Motivation Sometimes I and my colleagues notice that the ventilation doesn't seem to work in the classrooms. On a number of occasions we have reported it and got told it is working normally, only to to find out after a few weeks of complaints that it was turned off after all. ### Purpose I hope by gathering data we can make our case with the building managers so that they come and check earlier. ### Insights * To find out if the ventilation is on and if so if it's properly dimensioned. * To see how the classroom environment changes over the course of a day, a week or month. * To be able to use collected data in teaching situations. ## Material | Item | Image | Specification | Store | Cost | | -------- | -------- | -------- | -------- | -------- | | 1 Pycom LoPy4 Multipack | ![](https://i.imgur.com/5QcILjq.png)| Provides 1 LoPy4 microcontroller, 1 expansion board 3.1, and 1 LoRa/Sigfox antenna |[Pycom](https://pycom.io/product/lopy4-multipack/) | €56.40 | | 1 Pycase Clear | ![](https://i.imgur.com/3P2hvPP.png) | Provides protection and easy handling of the LoPy4, expansion board and the antenna|[Pycom](https://pycom.io/product/pycase-clear/)| €8.00 | | 1 CSS811 Air Quality Sensor | ![](https://i.imgur.com/wbApnrj.jpg) | Measures CO2 and VOC levels |[Electro:kit](https://www.electrokit.com/produkt/luftkvalitetssensor-ccs811-monterad-pa-kort/) | SEK329.00 | | 1 BME680 sensor | ![](https://i.imgur.com/0Q4bfdo.jpg)| Measures air pressure, temperature, and humidity|[Electro:kit](https://www.electrokit.com/produkt/bme680-monterad-pa-kort/) | SEK279.00 | | 1 DHT11 sensor | ![](https://i.imgur.com/oEG07KX.jpg) | Measures temperature and humidity|[Electro:kit](https://www.electrokit.com/produkt/digital-temperatur-och-fuktsensor-dht11/) | SEK49.00 | | 1 Battery LiPo 3.7V 4400mAh | ![](https://i.imgur.com/9BHoEEY.jpg)| Provides power|[Electro:kit](https://www.electrokit.com/produkt/batteri-lipo-3-7v-4400mah/) | SEK249.00| |Playknowlogy Komponent-kit | ![](https://i.imgur.com/QpOud1H.jpg)| Provides breadboard and wires and sensors not used in this project|[Kjell & Company](https://www.kjell.com/se/produkter/el-verktyg/arduino/arduino-kit/playknowlogy-komponent-kit-for-experimentering-med-arduino-p87293) | SEK299.99 | ## Computer setup I used a MacBook to setup my development environment. 1. Update the firmware of the expansion board. A detailed instruction on how to do that is available at [the Pycom website](https://docs.pycom.io/pytrackpysense/installation/firmware/). 2. Connect the LoPy4 with the expansion board and the LoRa antenna. Once again [the Pycom website](https://docs.pycom.io/gettingstarted/connection/lopy4/) has instructions on how to do that. 3. Update the LoPy4 firmware and to activate it. **i.** Go to the [Pybytes website](https://pybytes.pycom.io) and register an account. **ii.** Then choose Configure Networks to add wi-fi connectability and enter the wi-fi credentials for the wi-fi network you want to use and save it. **iii.** Choose to add a device by USB and follow the guide. For this setup you need to choose both wi-fi and LoRa. **iv.** Next is activation and firmware update. In Pybytes choose your device and go to the Provisioning-tab. There you'll find a link to install the [Pycom Firmware Update](https://docs.pycom.io/gettingstarted/installation/firmwaretool/) and an activation code. Copy the code and install the Pycom Firmware Update. **v.** Run the update with your LoRa4 connected by USB to your computer. ![](https://i.imgur.com/Yj0MhU8.png) Make sure the "Force update Pybytes registration" is ticked. Follow the guide and paste the activation code when asked for it. 4. The LoPy4 is programmed with MicroPython and I chose to use [Atom](https://atom.io) as my IDE with the PyMakr-plugin for interaction with the LoPy4. **i.** When you have downloaded Atom you can find the plugin in the Settings-tab under install. ![](https://i.imgur.com/89A6iaN.png) **ii.** To upload code to your device use the upload button when it's connected to Atom. ![](https://i.imgur.com/w85hO5L.png) ## Putting everything together To connect the sensors to your LoPy4 use the breadboard with connections specified in the circuit diagram below. ![](https://i.imgur.com/f7cpVPM.jpg) What is not shown in the diagram is the battery. It is connected to the JST-connector on the expansion board. I also mounted the LoRa4, expansion board and the antenna in the Pycase. ![](https://i.imgur.com/gfxEWe7.jpg) ### Electric calculations The good thing about these sensors is that they can all run on the U = 3.3 V the expansion board can provide so you don't need to think about extra resistors for correct voltage. This setup is only recommended for development. From the data sheets you can astimate the the power consumption. The CCS811 has a max consumption of 60 mW, DHT11 has a mean current consumption of I = 0,2 mA, and thus P = UI = 0,7 mW and BME680 has a mean current consumption of I = 0,9 mA => P = 3,0 mW. The LoPy 4 has a mean current of I = 105 mA (37 ms) during transmission and I = 30 mA (300 s) otherwise, so the mean current for my setup is I = 30 mA => P = 99 mW. That is a total power of P = 162,7 mW. The battery of Q = 4400 mAh and U = 3,7 V is equivalent with W = QU =16,3 Wh. Combined this means there would be at least t = W/P = 100 h = 4 days operating time before recharging on a fully loaded battery. ## Platform I chose use a combination of [The Things Network (TTN)](https://www.thethingsnetwork.org) and [Ubidots](https://ubidots.com). Both are cloud services. The reason for this choice is three fold. * I wanted to send data with LoRa and there TTN provides the best free solution. The reason being is that the school wi-fi can't be used and we have LoRa gateways at the school. * Ubidots integrates flawlessly with TTN. See guide for setup ([TTN Setup](https://www.thethingsnetwork.org/docs/devices/) , [Ubidots Integration Setup](https://help.ubidots.com/en/articles/2362758-integrate-your-ttn-data-with-ubidots-simple-setup)) * The services are both free to use. But for Ubidots there exists other subscriptions if the need to store more data or the need to get more data arises, which could be good for scaling. The account I chose was a free STEM account. There up to 3 devises and 4000 data points stored and 50000 extracted are free for every 24 h period. There is also the possibility to have up to 3 dashboards with 10 widgets in each. The data is stored 1 month. This is enough since the 5 sensor values sent every 5 minutes generates 1440 data points every 24 h period and thus data for 34 days can be viewed once every day or more likely data for the last week viewed several times every day. If we made more sensors to put in more classrooms we would need to change to paid subscription of Ubidots. And instead of TTN maybe Sigfox could be an option. ## The code To make the sensors work I downloaded the following packages and put them in a library folder called lib. * [CCS811 - CCS811.py](https://github.com/Notthemarsian/CCS811) * [BME680 - bme680.py, constants.py and i2c.py](https://github.com/robmarkcole/bme680-mqtt-micropython/tree/master/lib) * [DHT11 - dth.py](https://github.com/JurassicPork/DHT_PyCom) Here I changed the name of the file and functions to DHT instead of keeping the authors DTH because it annoyed me. ![](https://i.imgur.com/GbzW7w7.png) ### keys.py (also saved in library folder) ```python= import ubinascii app_eui = ubinascii.unhexlify('the code from the TTN setup here') app_key = ubinascii.unhexlify('the code from the TTN setup here') ``` ### lora.py ```python= from network import LoRa import socket import time import ubinascii import keys ''' Function for connecting to a LoRaWAN network. It connects to a LoRa-network via OTAA with the keys specified in the keys.py file. It initiates a socket in the global variable s for access from other parts of the program. ''' def LoRaConnect(): # Please pick the region that matches where you are using the device: # Asia = LoRa.AS923 # Australia = LoRa.AU915 # Europe = LoRa.EU868 # United States = LoRa.US915 lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868) lora.join(activation=LoRa.OTAA, auth=(keys.app_eui, keys.app_key), timeout=0) while not lora.has_joined(): time.sleep(2.5) print('Not yet joined...') print('LoRa joined') global s # create a LoRa socket s = socket.socket(socket.AF_LORA, socket.SOCK_RAW) # set the LoRaWAN data rate s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5) ''' Function for sending a given package via LoRaWAN. It blocks the socket, sends the package and unblocks the socket. ''' def LoRaSend(package): s.setblocking(True) s.send(package) s.setblocking(False) ``` ### read.py ```python= import time from machine import Pin from machine import I2C import CCS811 # https://github.com/Notthemarsian/CCS811 import bme680 # https://github.com/robmarkcole/bme680-mqtt-micropython from i2c import I2CAdapter # Part of the above BME680-library from dht import DHT # https://github.com/JurassicPork/DHT_PyCom # Sensor setup. i2c = I2C(0) # create on bus 0 i2c = I2C(0, I2C.MASTER) # create and init as a master i2c = I2C(0, pins=('P9','P10')) # PIN assignments (P9=SDA, P10=SCL) i2c.init(I2C.MASTER, baudrate=10000) # init as a master # eCO2 (400 - 8192 ppm) and tVOC (0 to 1187 ppb) sensorCCS = CCS811.CCS811(i2c=i2c,addr=91) # Air pressure (300-1100 hpa), temperature (-40 - 85 °C) and humidity (0-100 %) i2c_dev = I2CAdapter() sensorBME = bme680.BME680(i2c_device=i2c_dev) # These oversampling settings can be tweaked to # change the balance between accuracy and noise in # the data. sensorBME.set_humidity_oversample(bme680.OS_2X) sensorBME.set_pressure_oversample(bme680.OS_4X) sensorBME.set_temperature_oversample(bme680.OS_8X) sensorBME.set_filter(bme680.FILTER_SIZE_3) # Temperature (0 - 50 °C) and humidity (20- 90 %) sensorHT = DHT(Pin('P23', mode=Pin.OPEN_DRAIN),0) # Let time pass so sensors will read values time.sleep(5) ''' Function that reads the data from the sensors, checks if they return valid results and then saves the values in variables. The double temperature and humidity values are saved as average values of the reading if they are in the range of both sensors. The values are then returned. ''' def values(): result = sensorHT.read() if result.is_valid() and sensorCCS.data_ready() and sensorBME.get_sensor_data(): co2 = sensorCCS.eCO2 voc = sensorCCS.tVOC pressure = sensorBME.data.pressure if 0<sensorBME.data.temperature<50: temperature = (result.temperature + sensorBME.data.temperature)/2 else: temperature = sensorBME.data.temperature if 20<sensorBME.data.humidity<90: humidity = (result.humidity + sensorBME.data.humidity)/2 else: humidity = sensorBME.data.humidity if co2 > 399: return co2, voc, pressure, temperature, humidity ``` ### main.py ```python= import pycom import time import struct import read from lora import * ''' Function that reads data from the sensors prints and package it, sends the package via LoRa to TTN. The FiPy LED will blink green upon success. ''' def sendValues(): try: co2, voc, pressure, temperature, humidity = read.values() package = (struct.pack('>H', int(co2)) + struct.pack('>H', int(voc)) + struct.pack('>H', int(pressure)) + struct.pack('>B', int(round((temperature+40)*2))) + struct.pack('>B', int(humidity)) ) LoRaSend(package) output = "T: {} °C, P: {} kPa, RH: {} %, CO2: {} ppm, tVOC: {} ppb".format( round(temperature, 1), round(pressure)/10, round(humidity), round(co2), round(voc)) print(output) for n in range(3): pycom.rgbled(0x00FF00) time.sleep(0.2) pycom.rgbled(0x000000) time.sleep(0.2) except (NameError, ValueError, TypeError): print("Function error") pycom.heartbeat(False) LoRaConnect() while True: sendValues() time.sleep(300) ``` ### Payload decoder for TTN ```javascript= function Decoder(bytes, port) { // Decode an uplink message from a buffer // (array) of bytes to an object of fields. var decoded = {}; decoded.co2 = (bytes[0] << 8) + bytes[1] decoded.voc = (bytes[2] << 8) + bytes[3] decoded.pressure = ((bytes[4] << 8) + bytes[5])/10 decoded.temperature = bytes[6]/2 - 40 decoded.humidity = bytes[7] return decoded; } ``` ## Transmitting the data / connectivity The data is transmitted by LoRaWAN to TTN every 5 minutes. This choice was made after discovering that the data package took 37 ms to send so in a 24 h period that amounts to 10.7 s of the limit of 30 s set by the TTN. If I were to use more units of this type I would have to redo the calculations and send data more seldom. The choice of LoRa was obvious since wi-fi isn't possible to use at my workplace and we have several LoRa gateways. This also helps with the battery life since it's a low power transmission. The data package consists of 8 bytes of unsigned binary code. This choice is based on the range of the sensors and to minimize that data load. With one byte I can send a range of 255 and with two bytes a range of 65536. The humidity is ranged between 0% and 100% so one byte suffices. I decided that same applies for temperature since the sensors range of -40°C to 85°C is a range of 125 and with half degree steps it becomes 250. Half degree steps is enough accuracy. For CO2, VOC and pressure the range of the sensors are all over 256 steps so I chose two bytes for those. I coded it with the help of the [struct package](https://docs.python.org/3/library/struct.html), I convert the data to integer values before making them into bytes. This package is sent to the TTN through LoRaWan where it is decoded by unpacking byte for byte, see code in the code section. With the smart Ubidots integration the decoded data is sent to Ubidots through MQTT. ## Presenting the data Ubidots was chosen, as stated in more detail above, for the easy integration with TTN and that it provides sufficient storage for my needs. In Ubidots the data is saved in a database for 1 month and data is received every 5 minutes (if the transmission is successful). This is sufficient for the purpose of monitoring the air quality in a classroom over time. The data can be downloaded if needed and the storage time can be expanded with a paid subscription. I have chosen to have a trigger event from Ubidots that sends an e-mail if the CO2-level is above 1000 ppm for more than 20 minutes. 1000 ppm is chosen because it's standard for good air exchange. There is also another trigger if no data is received for 30 minutes that sends an email alerting to the problem and a counter trigger if the situation reverses. From there I build a dashboard by adding a dashboard and clicking on the add widget button. I chose to do line graphs since I am interested in change over time. In the classroom I will be most interested in CO2 levels so I made that widget bigger than the other four. ![](https://i.imgur.com/eBNwEEN.png) ## Finalizing the design ![](https://i.imgur.com/gfuBGdQ.jpg) This the final version of this project. It's been powered not by the battery in this experiment setup but with USB connection. When schools reopen it will run on battery in my classroom. It's been up and running for 8 days straight when I write this. The LoPy4 is restarting about 2-4 times over a 24 h period and I loose a few packages from time to time. But on the whole it's working well. I get the data I want. If I had access to a 3D-printer I would now design some sort of case to hold the components in a much more secure way. Other ways to improve the design is to check and examine different sensors to see which are the best. I am also thinking of getting rid of the DHT11 sensor since the BME680 is measuring the humidity and temperature too in order to save power. I would also consider building a [TIG-stack](https://medium.com/@hoanglc/tig-stack-powerful-monitoring-tool-822521dce102) on a server that gets data from TTN so I own the database. I could also add sensors for monitoring sound and light levels in the classroom. A final insight is that this kind of data gathering could be used by the students when they are working with the diploma project in the Upper Secondary School.