ESP8266 with new co2 sensor

05 Jul 2022

Finally I have an update on my ESP8266 based Co2 sensor. In my first build I used a CCS811 sensor board, but it did not work properly in my opinion. So now I have removed the CCS811 and replaced it with the SenseAir S8 co2 sensor and SHT31 temp sensor.

Hardware

  • Wemos D1 mini
  • 0.96" Oled with 128x64 resolution
  • SenseAir S8 co2 sensor (I’m using the 004-0-0053 version)
  • SHT31 i2c temp and humidity sensor, AFAIK the SHT30, SHT31 or SHT3x is pretty much the same

Wiring

Preparing the Arduino IDE

I’m still using Arduino IDE, it’s easy to use but we need to install some libraries to use our hardware.

Installing ESP8266 support

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 SenseAir S8 libraries

The SenseAir S8 libraries I’m using comes from jcomas/S8_UART with good install instructions already.

Installing libraries

To install the libraries go to Tools -> Manage libraries...

Libraries needed

  • Adafruit GFX Library, Graphics stuff for the display
  • Adafruit SSD1306, For the display
  • Adafruit SHT31, Temp and humidity sensor
  • PubSubClient, Mqtt client

The Code

You can find the complete code at the bottom, I will only go through the changes I have made to my code. Most of the Display and MQTT code is pretty much the same

Functions for old sensors removed

because I have removed the crappy eCO2 sensor I was using, I have to remove all the code related to that sensor.

But because I had split my code into sub-routines for reading my sensor and others for updating display or sending to my MQTT server etc. I quickly just removed those two functions

void read_ccs811();   // Read CCS811 sensor data
void read_hdc1080();  // Read HDC1080 sensor data

Just remember to remove the actual function as well, not just the definition at the top.

Removed code from setup()

Some lines of code to initialize the old sensors was removed from the setup() function

  // 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.");
  }

New and updated functions for new sensors

New code added for the new sensors

Sensor initialization in setup()

First we need to initialize the sensors in the setup() function

  // initialize the SenseAir S8 co2 sensor
  S8_Serial.begin(S8_BAUDRATE);
  uartS8 = new S8_UART(S8_Serial);

  // Try to get the firmware version as a way to verify that we have a sensor attached
  uartS8->get_firmware_version(sensorS8.firm_version);
  int len = strlen(sensorS8.firm_version);
  if(len==0) {
    Serial.println("no SenseAir S8");
  }

  // just print some info to serial for debugging purpose
  sensorS8.sensor_type_id = uartS8->get_sensor_type_ID();
  sensorS8.abc_period = uartS8->get_ABC_period(); // ABC period is the self calibration delay
  Serial.print("SenseAir S8 ID: 0x"); printIntToHex(sensorS8.sensor_type_id, 3); Serial.println("");
  Serial.print("SenseAir S8 ABC Period: "); Serial.print(sensorS8.abc_period); Serial.println("");

  // Initialize the SHT31 sensor
  if( ! sht31.begin(0x44)) {
    Serial.println("no SHT3x sensor");
  }

New sensor read function

void read_sensorS8() {
  sensorS8.co2 = uartS8->get_co2();

  // keep old value if the value from the sensor is 0 or lower
  if (sensorS8.co2 > 0 ) {
    data.CO2 = sensorS8.co2;
  }
}

void read_SHT31() {
  float t = sht31.readTemperature();
  float h = sht31.readHumidity();

  data.temp = t;
  data.hum = h;  
}

Changed code segments

Because the new sensors is using other types my AQS_SensorData has to change

/*
 *  Sensor data
 */
struct AQS_SensorData {
  int16_t CO2;
  float temp;
  float hum;

  AQS_SensorData() : CO2(0), temp(0), hum(0) {}
};

Because the AQS_SensorData was changed the mqtt_publish(), dumpToSerial() and updateDisplay() was updated to reflect those changes

The new updated loop() function

void loop() {
  read_sensorS8();
  read_SHT31();
  
  mqtt_publish();
  dumpToSerial();
  updateDisplay();
  delay(5000);
}

Complete code

/*
 * Air Quality Sensor (AQS)
 * 
 * Written for D1 Mini (ESP8266), Adafruit_SSD1306 compatible display, Senseair S8 C02 sensor.
 * 
 * */
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_SHT31.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include "AQS_Config.h"
#include <SoftwareSerial.h>
#include "s8_uart.h"        // https://github.com/jcomas/S8_UART

// SenseAir S8
#define RX_PIN D5
#define TX_PIN D6

SoftwareSerial S8_Serial(RX_PIN, TX_PIN);
S8_UART *uartS8;
S8_sensor sensorS8;

// SHT3x i2c temp sensor
Adafruit_SHT31 sht31 = Adafruit_SHT31();


/*
 *  Sensor data
 */
struct AQS_SensorData {
  int16_t CO2;
  float temp;
  float hum;

  AQS_SensorData() : CO2(0), temp(0), hum(0) {}
};

/*
 * Global variables for the sensor
 */
AQS_Config cfg;           // AirQuality Sensor settings, wifi, name etc.
AQS_SensorData data;      // AirQuality Sensor global data storage for sensor input

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

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

void mqtt_connect();  // Connect to MQTT server
void mqtt_callback(char* topic, byte* payload, unsigned int length);  // receive messages
void mqtt_publish();  // Publish sensor data to MQTT
void updateDisplay(); // Update display with sensor data
void dumpToSerial();  // Print sensor data to serial port

// Somewhere to store a MQTT message
char mqtt_msg[64];
char new_mqtt_msg = false;

void setup() {
  // Local settings for this sensor
  cfg.set_name("AQS");
  cfg.set_id(3);
  cfg.set_ssid("BadassWIFI");
  cfg.set_wifi_password("CrazyPassword");
  cfg.set_mqtt_server("192.168.0.41");
  cfg.set_mqtt_delay(2);

  // Pin state
  pinMode(D7, OUTPUT);
  digitalWrite(D7, HIGH);

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

  Serial.print("Hostname: "); Serial.println(cfg.get_hostname());
  Serial.print("Mqtt str: "); Serial.println(cfg.get_mqtt_str());
  Serial.print("WIFI: "); Serial.println(cfg.get_ssid());
  Serial.print("Password: "); Serial.println(cfg.get_wifi_password());
  Serial.print("MQTT srv: "); Serial.println(cfg.get_mqtt_server());
  Serial.print("MQTT port: "); Serial.println(cfg.get_mqtt_port());
  Serial.print("MQTT Delay: "); Serial.println(cfg.get_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)

  Wire.begin(); // WHY?

  // initialize the SenseAir S8 co2 sensor
  S8_Serial.begin(S8_BAUDRATE);
  uartS8 = new S8_UART(S8_Serial);

  uartS8->get_firmware_version(sensorS8.firm_version);
  int len = strlen(sensorS8.firm_version);
  if(len==0) {
    Serial.println("no SenseAir S8");
  }

  sensorS8.sensor_type_id = uartS8->get_sensor_type_ID();
  sensorS8.abc_period = uartS8->get_ABC_period();

  Serial.print("SenseAir S8 ID: 0x"); printIntToHex(sensorS8.sensor_type_id, 3); Serial.println("");
  Serial.print("SenseAir S8 ABC Period: "); Serial.print(sensorS8.abc_period); Serial.println("");
 
  if( ! sht31.begin(0x44)) {
    Serial.println("no SHT3x sensor");
  }


  // 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(cfg.get_ssid());
  display.display();

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

  // init MQTT
  mqtt.setServer(cfg.get_mqtt_server(), cfg.get_mqtt_port());
  mqtt.setCallback(mqtt_callback);

}

// Manual calibrate Sensair S8 sensor, only do this when Co2 is known to be approx 400ppm (outside)
void calibrate_sensorS8() {
  Serial.println("Manual calibration triggered");
      
  display.setCursor(0,0);
  display.clearDisplay();
  
  display.println("Calibration in progress");
  display.display();

  // Now trigger manual calibration
  digitalWrite(D7, LOW);
  
  // delay for 5 seconds in a loop to update the display while waiting
  for( int i=0; i<10; i++ ) {
    delay(500);
    
    display.print(".");
    display.display();
  }

  // End manual calibration
  digitalWrite(D7, HIGH);
}

void read_sensorS8() {
  sensorS8.co2 = uartS8->get_co2();

  // keep old value if the value from the sensor is 0 or lower
  if (sensorS8.co2 > 0 ) {
    data.CO2 = sensorS8.co2;
  }
}

void read_SHT31() {
  float t = sht31.readTemperature();
  float h = sht31.readHumidity();

  data.temp = t;
  data.hum = h;
  
}

uint32_t mqtt_counter = 0;

void loop() {

  if( new_mqtt_msg ) {
    Serial.print("MQTT msg: ");
    Serial.println(mqtt_msg);

    if ( strcmp(mqtt_msg, "calibrate") == 0 ) {
      calibrate_sensorS8();
    }
    
    new_mqtt_msg = false;
  }

  read_sensorS8();
  read_SHT31();
  
  mqtt_publish();
  dumpToSerial();
  updateDisplay();
  delay(5000);
}

void dumpToSerial() {
  Serial.print("CO2: ");
  Serial.print(data.CO2);
  Serial.print("ppm, ");
    
  Serial.print("Temp:");
  Serial.print(data.temp);
  Serial.print("C, ");
    
  Serial.print("Humidity:");
  Serial.print(data.hum);
  Serial.print("%");

  Serial.println("");
}

void updateDisplay() {
  display.setCursor(0,0);
  display.clearDisplay();
  
  display.print("Air Quality Sensor #");
  display.print(cfg.get_id());
  display.println("\n");
  
  display.print("CO2: ");
  display.print(data.CO2);
  display.print(" ppm\n");
  
  display.print("Temp: ");
  display.print(data.temp);
  display.print("C\n");
  
  display.print("Humidity: ");
  display.print(data.hum);
  display.print("%\n");
  
  display.print("\n\nIP: ");
  display.println(ip);
  
  display.display();
}

void mqtt_publish() {
  if (!mqtt.connected()) {
    mqtt_connect();
  } else {
    if(!mqtt.loop()) {
      Serial.println("mqtt.loop failed");
    }
  }
    
  if ( mqtt.connected() && mqtt_counter <= 1 ) {
    Serial.println("Publish MQTT message");
    
    char buffer[16];
    char msg[128];
    dtostrf(data.CO2, 1, 4, buffer);
    sprintf(msg, "%s/CO2", cfg.get_mqtt_str());
    mqtt.publish(msg, buffer);
    
    dtostrf(data.temp, 1, 4, buffer);
    sprintf(msg, "%s/Temperature", cfg.get_mqtt_str());
    mqtt.publish(msg, buffer);
    
    dtostrf(data.hum, 1, 4, buffer);
    sprintf(msg, "%s/Humidity", cfg.get_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--;
  }
}

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(cfg.get_mqtt_str(), "online");
      
      char msg[50];
      sprintf(msg, "%s/cfg/#", cfg.get_mqtt_str());
      mqtt.subscribe(msg);
    } 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_callback(char* topic, byte* payload, unsigned int length) {
  //Serial.print("Message arrived in topic: ");
  //Serial.println(topic);
 
  //Serial.print("Message:");
  int i = 0;
  for (i = 0; i < length; i++) {
    mqtt_msg[i] = payload[i];
    //Serial.print((char)payload[i]);
  }
  mqtt_msg[i] = '\0'; // terminate mqtt message string
  new_mqtt_msg = true;
 
  //Serial.println();
  //Serial.println("-----------------------");
}