Indoor Air Quality Monitoring with Dashboard


In this simple tutorial you will:

  1. Use the Arduino IDE to program an ESP32
  2. Interface temperature, humidity and eCO2 modules to the ESP32
  3. Output data on both a physical display, and an IOT cloud service with integrated dashboard functionality


The ESP32 in all its 32 bit glory.
The ESP32 module, on its most common dev board form factor


The ESP32 runs at 240MHz stock, is dirt cheap, and has both WiFi and Bluetooth.

The CCS811 TVOC/eCO2 sensor module.
A generic purple CCS811 module – mind the WAKE signal, you’ll need to pull this to ground to communicate with the sensor

TVOC/eCO2 Sensor

The CCS811 is a hot plate based gas sensor which detects Volatile Organic Compounds (VOCs) levels and provides an equivalent carbon dioxide estimate (eCO2) over I2C.

eCO2 you say! Why, doesn’t that mean we could use this thing to detect CO2 levels outdoors as well?

Alas, the eCO2 reading is an estimate derived from the proven correlation between CO2 levels and VOC levels indoors. The CCS811 does not directly measure CO2 and as a result, wouldn’t be usable as part of say – a green house gas sensor array.

The datasheet for this sensor recommends that you apply a “burn in” period, by running it for at least 48 hours before use, which should add some semblance of stability to variations in sensitivity levels.

You’ll also want to let the sensor run for at least 20 minutes on boot before trusting the readings. It takes a while for the MOX to heat up.

The SHT3X temperature/humidity sensor module.
This cute little SHT3X module is also of the generic purple variety.

Temperature and Humidity Sensor

The SHT30-D is one of the faster temperature and humidity sensors out there, outpacing dinosaurs like the hobbyist favorite – the DHT22, in terms of both accuracy and response time, for roughly the same cost.

An SSD1306 based OLED module.
This small SSD1306 based OLED module can pack quite a bit of information in a relatively tight space.


The SSD1306 is a popular driver IC which is typically used to control the wide array of low cost OLED modules that are now available. OLEDs typically have lower power consumption relative to TFT LCD displays.

See also  GPS Fundamentals with UBLOX NEO-6M GPS Chip


Message Queuing Telemetry Transport or MQTT is a popular, lightweight messaging protocol. It’s based on a publish-subscribe model, called so because clients can either publish or subscribe to the server, or “broker” using the protocol’s terminology.

Data is organized according to topics. Clients can then publish data to a topic or multiple topics, and subscribe to several topics at the same time. The simplicity of MQTT has made it incredibly popular for IOT applications.


A wiring diagram.
Friends don’t let friends use fritzing, unless it’s for wiring diagrams.


The CCS811, SHT30-D and SSD1306 modules are all 3.3V or 5V compatible, so directly connecting the 3.3V output pin on the ESP32 dev board to the VCC input of each module should pose no issue.


The SDA and SCL pins on the ESP32, which are pins 21 and 22 respectively, are connected to the SDA and SCL pins of the CCS811, SHT30-D and SSD1306 modules. They all use I2C to communicate, so they will share a common bus for their data and clock signals. The WAKE signal on the CCS811 module must be pulled down to ground in order to establish communications.


Here are the libraries you would want to have installed to get these devices up and running quickly.

Go to Sketch->Include Library->Manage Libraries to find these libraries.

Software Setup

To use the Arduino IDE to program an ESP32, you’re going to need to add the ESP32 to the board manager of the Arduino IDE. You can accomplish this by:

  1. Opening the preferences window in the Arduino IDE.
  2. Entering into the Additional Board Manager URLs field.
  3. Open Boards Manager from Tools > Board menu and install esp32 platform
See also  Dynamic OLED Animation Display with Arduino

Additionally you’ll need to change the WiFi and Adafruit IO credentials in the code to make it work.

  4. AIO_KEY

If you want to use a different cloud service, you’ll also have to change these – consult the service provider website to determine the values you’ll use:



#include "WiFi.h"
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"
#include "Adafruit_CCS811.h"
#include "Adafruit_SHT31.h"
#include "SSD1306Wire.h"

/************************* WiFi Access Point *********************************/

#define WLAN_SSID       "YOUR_WIFI_SSID"

/************************* Setup *********************************/

#define AIO_SERVER      ""
#define AIO_SERVERPORT  1883                   // use 8883 for SSL
#define AIO_KEY         "INSERT_YOUR_KEY_HERE"

/************************* Setup *********************************/
WiFiClient client;

/****************************** Feeds ***************************************/
Adafruit_MQTT_Publish eCO2_feed = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/eco2");
Adafruit_MQTT_Publish TVOC_feed = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/tvoc");
Adafruit_MQTT_Publish temp_feed = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/temp");

Adafruit_CCS811 ccs;
Adafruit_SHT31 sht31 = Adafruit_SHT31();
SSD1306Wire display(0x3c, 21, 22);

void MQTT_connect();

void setup() {


  while (WiFi.status() != WL_CONNECTED) {
  Serial.println("CCS811 test");
    Serial.println("Couldn't find SHT31");

  Serial.println("SHT31 test");
  if (! sht31.begin(0x44)) {   // Set to 0x45 for alternate i2c addr
    Serial.println("Couldn't find SHT31");
    while (1) delay(1);


  //calibrate temperature sensor
  float temp = sht31.readTemperature();
  ccs.setTempOffset(temp - 25.0);

int counter = 1;

void loop() {
    float temp = sht31.readTemperature();
      Serial.print("eCO2: ");
      float eCO2 = ccs.geteCO2();
      display.drawString(0,0,"eCO2: " + String(eCO2) + "ppm");
      if(counter%5 == 0) //limit data usage 
      Serial.print("ppm, TVOC: ");
      float TVOC = ccs.getTVOC();
      display.drawString(0,12,"TVOC: " + String(TVOC) + "ppb");
      if(counter%6 == 0) //limit data usage 
      Serial.print("ppb   Temp:");
      display.drawString(0,24,"Temp: " + String(temp));
      if(counter%7 == 0) //limit data usage 
  if(counter > 11)
    counter = 1;

void MQTT_connect() {
  int8_t ret;

  // Stop if already connected.
  if (mqtt.connected()) {

  Serial.print("Connecting to MQTT... ");

  uint8_t retries = 3;
  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
       Serial.println("Retrying MQTT connection in 5 seconds...");
       delay(5000);  // wait 5 seconds
       if (retries == 0) {
         // basically die and wait for WDT to reset me
         while (1);
  Serial.println("MQTT Connected!");

Cloud Service

You’ll want to sign up for an Adafruit IO account to follow this tutorial. A free account provides up to 30 data points per minute for 10 feeds and 5 dashboards. The code included in this tutorial limits the data point output rate to allow even free accounts to work well.

See also  ESP32 NTP Time Synchronization with LCD Display


After signing up for a free account, you should create at least 3 feeds (what Adafruit chose to call their topics), one for each sensor module. In our code, we specified three feeds:

  1. eco2
  2. tvoc
  3. temp

You can create a feed by hitting feeds > actions > create a new feed > create


Now that you have setup some feeds, you’ll want a dashboard to visualize that data.

You can do this by going to dashboards > actions > create a new dashboard > create

This will create an empty dashboard – now add some blocks by entering your dashboard, and clicking on create a new block

Choose any appropriate block type to display the data you will be receiving – I’d personally recommend a line chart in this situation.

Pick the feed you want this chart to visualize. Set labels as well as maximum and minimum values for your newly created chart, or leave them as default (you can modify this to your satisfaction later).

You’ll end up with a dashboard that looks like this.

eCO2, Temperature and TVOC feeds visualized.
The spikes on eCO2 and TVOC were created by breathing onto the sensor.

That’s all folks!

Interfacing the modules on perfboard should only take a few minutes – I’d suggest to use headers so you can remove and re-use the modules for other projects.

The logical next step, for the more adventurous, might be to step off the perfboard and run the ICs these modules use on a PCB you would design!

Good luck, and stay creative!

Leave a Reply