ESP8266 with co2 sensor and MQTT client

28 Nov 2021

I just have to write about my latest project. A bit more hardware related and no Linux stuff. The airquality in my basement where I have my computer feels bad, so I just had to upgrade my home made co2 sensor.

Hardware

The hardware I am using, I bought everything from ebay.

  • Wemos D1 mini
  • 0.96" Oled with 128x64 resolution
  • CCS811 sensor board, my version includes a HDC1080 temp & humidity sensor

Wiring

This is a quick sketch I made in KiCAD, great software IMO.

There is not much wiring needed, I have everything hooked up on a breadboard at the momemt.

Programming

I’m using the Arduino IDE because it is easy to use. But because we are using an ESP8266 we can’t just start writing code and program. We need to install some board stuff so the Arduino IDE know how to program our microcontroller.

Installing ESP8266 to Arduino IDE

There are plenty of guides already, so I will just write a short version.

Go to preferences (file -> preferences)

Add this URL

https://arduino.esp8266.com/stable/package_esp8266com_index.json

to the Additional Boards manager URLs: and click OK

Go to Tools -> Board -> Boards manager...

Search for ESP8266 and press install button for the “ESP8266 by ESP8266 Community“

When the installation is complete you will be able to find the ESP8266 boards under Tools -> Board -> ESP8266

There is a Blink test sketch under File -> Examples if you want to test your board. There are lots of other examples there as well.

Installing libraries

Before we continue we need some libraries for our sensors and oled display.

Go to Tools -> Manage libraries...

Libraries needed

  • Adafruit GFX Library
  • Adafruit SSD1306
  • Adafruit CCS811 Library
  • PubSubClient
  • ClosedCube HDC1080, for the extra HDC1080 temp sensor on my CCS811 board

Start coding

finally we can start write some code.

Lets start with the includes we need

/*
 * AirQuality Sensor (AQS)
 * 
 * Written for D1 Mini (ESP8266), Adafruit_SSD1306 compatible display, ccs811 VOC & co2 sensor and HDC1080 temp & humidity sensor.
 * 
 * */
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_CCS811.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ClosedCube_HDC1080.h>

Lets define some local sensor data

const char *SSID = "YOUR_WIFI";
const char *PASSWORD = "YOUR_WIFI_PASSWORD";

const char *mqtt_server = "192.168.30.27"; // Change this to your MQTT server IP
const int mqtt_port = 1883; // Default is 1883
const int mqtt_delay = 10;  // Publish MQTT approx every 10 minutes

const char *hostname = "AQS_1"; // The units LAN hostname
const char *mqtt_str = "AQS/1"; // The MQTT prefix string

Then I like to create a struct to hold my sensor values

    /*
     *  All the data from the sensor.
     */
    struct AQS_SensorData {
      uint16_t eCO2;
      uint16_t TVOC;
      float temp;
      float hum;
    
      // make sure the values are 0
      AQS_SensorData() : eCO2(0), TVOC(0), temp(0), hum(0) {}
    };

Now lets define some global variables, just because I’m lazy when I write code for my Arduino projects.

/* 
 * Global Variables 
 */
AQS_SensorData data;
 
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     0 // Reset pin # (or -1 if sharing Arduino reset pin)

Adafruit_SSD1306 display = Adafruit_SSD1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Adafruit_CCS811 ccs811;
ClosedCube_HDC1080 hdc1080;

IPAddress ip;

// Initializes the espClient. You should change the espClient name if you have multiple ESPs running in your home automation system
WiFiClient espClient;
PubSubClient mqtt(espClient);

I don’t like to write everything into my loop() function, so I have created some functions to be called from my loop(). Because I have the actual function after my loop() in my code I have to define them first.

void read_ccs811();   // Read CCS811 sensor data
void read_hdc1080();  // Read HDC1080 sensor data
void mqtt_connect();  // Connect to MQTT server
void mqtt_publish();  // Publish sensor data to MQTT
void updateDisplay(); // Update display with sensor data
void dumpToSerial();  // Print sensor data to serial port

That’s why I have the global AQS_SensorData data; variable. Then my read_ccs811() function will update the global variable and my updateDisplay() and mqtt_publish() will read the latest values and just publish or display them.

This makes it easy to switch over to use interrupt timers for some parts of the code if you want to.

Finally we are at the setup() function

void setup() {

  Serial.begin(115200);
  delay(500); Serial.println();

  // Print current settings, for debug purpose
  Serial.print("Hostname: "); Serial.println(hostname);
  Serial.print("Mqtt str: "); Serial.println(mqtt_str);
  Serial.print("WIFI: "); Serial.println(SSID);
  Serial.print("Password: "); Serial.println(PASSWORD);
  Serial.print("MQTT srv: "); Serial.println(mqtt_server);
  Serial.print("MQTT port: "); Serial.println(mqtt_port);
  Serial.print("MQTT Delay: "); Serial.println(mqtt_delay);

  // by default, we'll generate the high voltage from the 3.3v line internally! (neat!)
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x32)

  // hdc1080 info
  hdc1080.begin(0x40);
  delay(20);
  Serial.println("------------------------");
  Serial.print("Manufacturer ID=0x");
  Serial.println(hdc1080.readManufacturerId(), HEX); // 0x5449 ID of Texas Instruments  
  delay(20);
  Serial.print("Device ID=0x");
  Serial.println(hdc1080.readDeviceId(), HEX); // 0x1050 ID of the device
  delay(20);
  Serial.println("------------------------");
  Serial.print("setup: ccs811 lib  version: "); Serial.println(CCS811_HW_VERSION);
  Serial.println("------------------------");

  Wire.begin();
  
  // Enable CCS811
  if ( !ccs811.begin() ) {
    Serial.println("setup: CCS811 sensor start error.");
  }

  // Show image buffer on the display hardware.
  // Since the buffer is intialized with an Adafruit splashscreen
  // internally, this will display the splashscreen.
  display.display();
  delay(500);

  // Clear the buffer.
  display.clearDisplay();
  display.display();

  // text display tests
  display.setTextSize(1);
  display.setTextColor(WHITE);

  display.print("Connecting to ");
  display.print(ssid);
  display.display();

  // Connect to WIFI
  WiFi.hostname(hostname);
  WiFi.begin(SSID, PASSWORD);
  while( WiFi.status() != WL_CONNECTED) {
    delay(500);
    display.print(".");
    display.display();
  }
  ip = WiFi.localIP();

  // init MQTT
  mqtt.setServer(mqtt_server, mqtt_port);
}

And then we have the loop() function

void loop() {
  // Update sensor values in our global data variable
  read_hdc1080();
  read_ccs811();

  // Use the global data variable to publish or print
  mqtt_publish();
  dumpToSerial();
  updateDisplay();
 
  // Just delay 5s
  delay(5000);
}

Now we have all the other functions

void read_hdc1080() {
  data.temp = hdc1080.readTemperature();
  data.hum = hdc1080.readHumidity();
}

void read_ccs811() {
  if ( ccs811.available() ) {
    if ( !ccs811.readData() ) {
      data.eCO2 = ccs811.geteCO2();
      data.TVOC = ccs811.getTVOC();
    } else {
      // readData failed
      Serial.println("ccs811 error, Failed to readData.");
    }
  }
}

void mqtt_connect() {
  // Loop until we're reconnected
  while (!mqtt.connected()) {
    Serial.print("Attempting MQTT connection... ");
    if ( mqtt.connect(cfg.get_hostname()) ) {
      Serial.println("MQTT connected");
      mqtt.publish(mqtt_str, "online");
    } else {
      Serial.print("failed, rc=");
      Serial.print(mqtt.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void mqtt_publish() {
  if (!mqtt.connected()) {
    mqtt_connect();
  } else {
    // Call mqtt.loop() to do some background stuff, keep-alive etc.
   if(!mqtt.loop()) {
      Serial.println("mqtt.loop failed");
    }
  }

  if ( mqtt.connected() && mqtt_counter <= 1 ) {
    char buffer[16];
    char msg[128];
    dtostrf(data.eCO2, 1, 4, buffer);
    sprintf(msg, "%s/eCO2", mqtt_str);
    mqtt.publish(msg, buffer);
   
    dtostrf(data.TVOC, 1, 4, buffer);
    sprintf(msg, "%s/TVOC", mqtt_str);
    mqtt.publish(msg, buffer);
    
    dtostrf(data.temp, 1, 4, buffer);
    sprintf(msg, "%s/Temperature", mqtt_str);
    mqtt.publish(msg, buffer);
    
    dtostrf(data.hum, 1, 4, buffer);
    sprintf(msg, "%s/Humidity", mqtt_str);
    mqtt.publish(msg, buffer);

    // The loop() has a 5 sec delay, so it will run approx 60/5 times per minute
    mqtt_counter = (60 / 5) * cfg.get_mqtt_delay();
  } else {
    mqtt_counter--;
  }
}

Just upload and hold your breath :)

Conclusion

My code is very dirty, I feel itchy and it needs a good amount of cleanup before I am satisfied.

But my dirty code is not the biggest problem. The HDC1080 sensor is a huge failure. Sometimes it just works other times it just reports -40 or 125 degrees C. I have read over at the Github repo for ClosedCube_HDC1080 that you need to change the delay() in the readData() function. I have tried without success, today the sensor just works. Few days ago I rebooted and re-uploaded my code like 100 times but the sensor just spit out -40 or 125 degrees C.

So I’m about to throw away my combined CCS811 & HDC1080 sensor board. I just placed an order for a better CO2 sensor, SenseAir S8. And an SHT30 temperature sensor board.