Air Quality and Weather Monitor (CO2 TVOC PM2.5 PM10 Temperature Humidity Pressure)

Everybody want to breathe clean flesh air. Breathing dirty air will have detrimental effects to one’s health. Among all the measurement metrics, PM2.5, PM10, TVOC and CO2 levels are the typical ones that we should monitor. Other than that, we are interested to know about the temperature, humidity and air pressure.
In this instructables, I would introduce how to create a Weather and Air Quality Monitor by ESP8266 and various sensors. The measurement results will be displayed over an 240x320 TFT 16-bit color LED display. The device shows the current measurements as well as the trend of the past period of time. Time of which is synchronized with an NTP server over the Internet.
How to make it? Let’s get started!

Step 1: Components Required

The following list of components are used to built the Weather and Air Quality Monitor. You may also use components with equivalent functions.

  1. ESP8266 D1 Mini
  2. TFT ILI9341 240x320 SPI interface
  3. Lithium Battery
  4. Lithium Battery USB charging module
  5. 3.3V Output DC-to-DC module
  6. 5V Output DC-to-DC module
  7. Small Switch x 2
  8. HDC1080 Humidity Sensor
  9. BMP180 Temperature and Air Pressure Sensor
  10. CCS811 TVOC Sensor
  11. SenseAir S8 CO2 Sensor
  12. PM2.5/PM10 Infrared Sensor
  13. Circuit Board, Wire Wrap Tool & Wires

Step 2: Build the Power Supply Circuit

1. Wire the Lithium Battery, Battery Charger, 3.3V and 5V DC-DC converters together. You may want to add an ON / OFF switch between the Lithium Battery and the 3.3V & 5V DC-DC power converters.
2. Test whether the Lithium Battery can be charged up.
3. Confirm stable 3.3V and 5V power supplies can be got.

Step 3: Assign ESP8266 D1 Mini Pins for Various Purposes

First of all, create your design. Assign the ESP8266 D1 Mini Pins for various purposes. D1 Mini has a limited number of pins, we need to make the assignment carefully. Those that can save a pin should be saved. For example, some sensor will only send data by TTL but not receiving, then we do not need to wire the receive pin to the D1 Mini.

Here below is my pin assignment:

/* PIN Assignment<br>    A0 - Not Used
    D0 - TFT CS
    D1 - I2C CLK
    D2 - I2C SDA
    D3 - TFT C/D
    D4 - PM2.5 CS
    D5 - TFT SCK
    D6 - S8 SenseAir TX+RX tied
    D7 - TFT SDI(MOSI)
    D8 - Not Used
    TX - Debug Console
    RX - PM2.5 Tx

Step 4: Wire the D1 Mini With the 240x320 TFT by the SPI Bus

Connect the D1 Mini with the 240x320 TFT by the SPI Bus. The follow pins are used:

    D0 - TFT CS
D3 - TFT C/D

Please note that the MISO Pin is not wired. The reason is that the TFT library that we use will not obtain any data from the TFT. Therefore, we can save 1 pin for other purposes. The UTFT ESP8266 library is used to drive the TFT which support ILI9341 in SPI mode. If your TFT uses another chipset, you may need to use other TFT libraries. The key point is that you need to select an TFT which supports SPI bus. Otherwise, D1 Mini will not have sufficient pins to connect with it.

The key to work successfully with the TFT is to declare the UTFT object correctly in the correct format. Here is what we used:

// TFT Display
// UTFT::UTFT(byte model, int RS=SDI(MOSI), int WR=SCK, int CS=CS, int RST=NOTINUSE, int SER=DC)
UTFT myGLCD ( ILI9341_S5P, D7, D5, D0, NOTINUSE, D3); //ILI9341 in SPI

Step 5: Wire the Humidity Sensor to the I2C Bus

ESP8266 D1 Mini supports IIC Bus. Pin D1 is for the clock (SCL or CLK) and Pin D2 is for data (SDA). The IIC bus can support multiple devices at different addresses at the same time. You can connect the sensor with the 3.3V power supply and the SCL and SDA pubs of D1 Mini.

HDC1080 is used for sensing humidity. We use the ClosedCube Library.

<p>temperature[idx] = hdc1080.readTemperature();<br>  if ( temperature[idx] > 80.0) //Abnormal Reading, reset the chip
  Serial.print("HDC_Temp = "); Serial.print(temperature[idx]); Serial.print(" ");
  humidity[idx] = hdc1080.readHumidity();
  Serial.print("HDC_Humidity = "); Serial.print(humidity[idx]); Serial.print(" ");</p>

Step 6: Wire the Temperature and Air Pressure Sensor to the IIC Bus

ESP8266 D1 Mini supports IIC Bus. Pin D1 is for the clock (SCL or SCK) and Pin D2 is for data (SDA). The IIC bus can support multiple devices at different addresses at the same time. You can connect the sensor with the 3.3V power supply and the SCL and SDA pubs of D1 Mini.

BMP180 is used for sensing Temperature and Air Pressure. We use the Sparkfun library.

if ((status = bmp180.startTemperature()) != 0) {
delay(status); if (bmp180.getTemperature(T) != 0) { temperature[idx] = T; // Over-ride the inaccurate temperature from HDC1080 if ((status = bmp180.startPressure(3)) != 0) { delay(status); if (bmp180.getPressure(P, T) != 0) pressure[idx] = P; } } } Serial.print("bmp180Temp = "); Serial.print(temperature[idx]); Serial.print(" "); Serial.print("PRS = "); Serial.print(pressure[idx]); Serial.print(" ");

Step 7: Wire the TVOC Sensor to the IIC Bus

ESP8266 D1 Mini supports IIC Bus. Pin D1 is for the clock (SCL or SCK) and Pin D2 is for data (SDA). The IIC bus can support multiple devices at different addresses at the same time. You can connect the sensor with the 3.3V power supply and the SCL and SDA pubs of D1 Mini.

The TVOC sensor we used is CCS811, Adafruit library is available to read the data.

However, we need to be careful about the self calibration process of CCS811 in order to obtain data in a speedy way. An additional routine is added to record the BASELINE data in the EEPROM of ESP8266. The BASELINE is continuously tuned automatically by CCS811. We record the BASELINE periodically to the EEPROM. Upon the next time when the system is started up, we read in the lastly recorded BASELINE after the CCS811 is warm-up. The detail logic is available from the CCS811 datasheet.

Sample codes is as below:

// Sensor Initilization hdc1080.begin(0x40); hdc1080.reset(); bmp180.begin(); ccs811.begin(); S8Serial.begin(9600); S8_begin(&S8Serial); //==== CCS811 BASELINE MANAGEMENT ==== EEPROM.begin(512); copyCurrentTime(&curr_epoch, &curr_remain_millis); // Get the current time in Epoch if (startup_epoch == 0) // initialize startup_epoch = curr_epoch;

if (last_eeprom_write_epoch == 0) // initialize last_eeprom_write_epoch = curr_epoch; // Read all the BASELINE records and pick the best one to use score = 0; best_score = 0; lowest_score = 4294967295; //2^32-1 for (int j = 0; j < BASELINEREC_MAX; j++) { // Check all records in the EEPROM // Read the BASELINE record int baselinerec_addr_read = sizeof(uint16_t) + sizeof(struct baselinerec) * j; for (int i = 0; i < sizeof(struct baselinerec); i++) * ((uint8_t*)&_baselinerec_read + i) = (uint8_t) + i); // Calculate the score of each record if (_baselinerec_read.signature != 0xABAB) // record not initialized score = 0; else if ( (curr_epoch - _baselinerec_read.epoch) > 28 * 24 * 3600) // record too old score = 0; else if ( _baselinerec_read.uptime < 4 * 3600) // record was obtained over a too short period score = 0; else // score formula can be customized score = _baselinerec_read.uptime + ((28 * 24 * 3600 - (curr_epoch - _baselinerec_read.epoch)) / 2); Serial.print("j="); Serial.print(j); Serial.print(" signature="); Serial.print(_baselinerec_read.signature); Serial.print(" epoch="); Serial.print(_baselinerec_read.epoch); Serial.print(" uptime="); Serial.print(_baselinerec_read.uptime); Serial.print(" avg_tvoc="); Serial.print(_baselinerec_read.avg_tvoc); Serial.print(" score="); Serial.println(score); delay(100); // prevent D1 Mini software reset due to too much I/O // Select the one with the highest score for read if (score > best_score) { best_score = score; baseline_idx_read = j; } // Select the one with the highest score for write if (score < lowest_score) { lowest_score = score; baseline_idx_write = j; } } Serial.print("baseline_idx_read="); Serial.print(baseline_idx_read); show_text("baseline_idx_read=%.0f", (float)baseline_idx_read, GEN_COLOR, CHART_LEFT, CHART_TOP + 96, 0); Serial.print(" baseline_idx_write="); Serial.println(baseline_idx_write); show_text("baseline_idx_write=%.0f", (float)baseline_idx_write, GEN_COLOR, CHART_LEFT, CHART_TOP + 112, 0); // Read in the best record if (best_score != 0) { int baselinerec_addr_read = sizeof(uint16_t) + sizeof(struct baselinerec) * baseline_idx_read; for (int i = 0; i < sizeof(struct baselinerec); i++) * ((uint8_t*)&_baselinerec_read + i) = (uint8_t) + i); } // update the eeprom baseline index to record where is the current record if (eeprom_baseline_idx_updated == false) { EEPROM.write(0, baseline_idx_write >> 8); EEPROM.write(1, (baseline_idx_write << 8) >> 8); eeprom_baseline_idx_updated = true; } EEPROM.commit(); }

//The code below should be put in the loop() part of the sketch to write the lastly recorded BASELINE to CCS811

//and record the tunned BASELINE periodically.


// Get curr_epoch & uptiime for the operation below copyCurrentTime(&curr_epoch, &curr_remain_millis); uptime = curr_epoch - startup_epoch;

// write the best BASELINE to CCS811 after uptime > X minutes if (ccs811_baseline_updated == false && best_score != 0 && uptime >= 600) { ccs811.writeBaseline(_baselinerec_read.baseline); ccs811_baseline_updated = true; }

// write to EEPROM periodically - Y minutes uint8_t baseline[2]; ccs811.readBaseline(baseline); if ( curr_epoch - last_eeprom_write_epoch >= 900) { _baselinerec_write.epoch = curr_epoch; _baselinerec_write.baseline[0] = baseline[0]; _baselinerec_write.baseline[1] = baseline[1]; _baselinerec_write.uptime = uptime;

int baselinerec_addr_write = sizeof(uint16_t) + sizeof(struct baselinerec) * baseline_idx_write; for (int i = 0; i < sizeof(struct baselinerec); i++) EEPROM.write(baselinerec_addr_write + i, *((uint8_t*)&_baselinerec_write + i));

EEPROM.commit(); last_eeprom_write_epoch = curr_epoch; }

Step 8: Wire the PM2.5 / PM 10 Sensor to the TTL RX Interface

The sensor that we use is from a China manufacturer (Liudu Air 六度空氣). It provides the data over the TTL interface. PM2.5 and PM10 readings are generated per second. No command is needed to be sent to the sensor to trigger the output. So, only TX pin at the sensor and the RX pin at the D1 Mini need to be connected together.

Please also note that D1 Mini relies on its TX and RX pins for updating the sketches. Connecting the RX pin with the sensor will affect sketch download. So, a switch is implemented to break the RX connection during sketch download.

The sensor has a small fan to draw air into it for measurement. The small fan consumes power when the sensor is not in use. There is an Enable (CS) pin at the sensor to turn on / off the fan and data output. The Enable (CS) pin is connected to D4 of the D1 Mini.

Here below are the codes to obtain the data from the sensor:

// PM2.5 //Serial.println("PM Begin "); digitalWrite(PM25_CS, HIGH); delay(9000); //64 byte buffer only; 7 byte per sample; 64/7=9 max if 1 sec per sample. while (Serial.available() > 0) { do { incomingByte =; //Serial.print(incomingByte, HEX); } while (incomingByte != 0xAA);

if (incomingByte == 0xAA) { Serial.readBytes(buf, 6); if (buf[5] == 0xFF && (buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { //0xFF = term char; checksum PM2_5Value = ((buf[0] << 8) + buf[1]) / 10.0; PM10Value = ((buf[2] << 8) + buf[3]) / 10.0; } } } //Serial.println("PM End"); digitalWrite(PM25_CS, LOW);

pm25[idx] = (float)PM2_5Value; pm10[idx] = (float)PM10Value; Serial.print("PM2.5 = "); Serial.print(pm25[idx]); Serial.print(" "); Serial.print("PM10 = "); Serial.println(pm10[idx]);

Step 9: Wire the CO2 Sensor to the Software Serial

SensorAir S8 outputs CO2 concentration by TTL interface. Since that the RX pin of D1 Mini was used for the PM2.5 / PM 10 sensor, we need to use software serial for the CO2 sensor.

D1 Mini does not have a lot of pin. We nearly run out of pins. Luckily, we can tie the TX and RX pin of the CO2 sensor together and talk with it by simplex mode. This is undocumented in AirSense S8’s datasheet, but it works! So, talking with the CO2 sensor only consumes one pin at the D1 Mini.

So far, we do not see any library from the Internet for Arduino for SensorAir S8. So, software serial functions were created for the purpose. Firstly, we need to initialize the SensorAir S8 for the ABC cycle. Then, we can read the CO2 values from the sensor automatically.

Codes for Initialization:

void S8_begin(SoftwareSerialx *ss) {
//byte ch, cmd[] = {0xFE, 0x6, 0x0, 0x1F, 0x0, 0xB4, 0xAC, 0x74}; // Set ABC Period to be 180 hours //byte ch, cmd[] = {0xFE, 0x6, 0x0, 0x1F, 0x0, 0x30, 0xAC, 0x17}; // Set ABC Period to be 48 hours byte ch, cmd[] = {0xFE, 0x6, 0x0, 0x1F, 0x0, 0x18, 0xAC, 0x09}; // Set ABC Period to be 24 hours ss->enableTx(true); for (int i = 0; i < 8; i++) ss->write(cmd[i]); ss->enableTx(false); delay(250); if (ss->available()) { Serial.print("SenseAir Response:"); while (ss->available()) { ch = (byte) ss->read(); Serial.print(ch < 0x01 ? " 0" : " "); Serial.print(ch, HEX); } Serial.println(); } }

Codes for Reading:

int S8_getCO2(SoftwareSerialx *ss, uint16_t*S8_CO2, uint16_t*S8_meterstatus) {
int i; //byte ch, cmd[] = {0xFE, 0x4, 0x0, 0x03, 0x0, 0x1, 0xD5, 0xC5}; //Get CO2 Only byte ch, cmd[] = {0xFE, 0x4, 0x0, 0x0, 0x0, 0x4, 0xE5, 0xC6}; //Get MeterStatus and CO2 byte result[20]; ss->enableTx(true); for (int i = 0; i < 8; i++) ss->write(cmd[i]); ss->enableTx(false); delay(250); while (!ss->available()) // wait until data is available delay(100); while ( ss->available()) { if ( (ch = ss->read()) == 0xFE) // wait until the header is available i = 0; result[i] = ch; i++; } *S8_CO2 = result[9] * 256 + result[10]; *S8_meterstatus = result[3] * 256 + result[4]; return *S8_meterstatus; }

Step 10: Download the Sketch to D1 Mini

Download the sketch to D1 Mini and it it completed. Remember to disconnect the PM2.5 / PM10 sensor when downloading the sketch.

Step 11: Charge Up the Battery and Turn It On

It will first connect to the WiFi, then send out NTP packets and get the Internet time. Then, scan through its EEPROM to obtain the best BASELINE settings for CCS811.

Step 12: Let It Run on Its Own ...

Let it run on its own for a period of time. You will see the charts and real-time metrics measured!!!



  • Paper Contest

    Paper Contest
  • Epilog X Contest

    Epilog X Contest
  • Build a Tool Contest

    Build a Tool Contest

3 Discussions


17 days ago

I didn't understand your code, is it for Arduino IDE or for Lua? can I use this code for my Node MCU ?

DIY Hacks and How Tos

4 months ago

It would be really interesting to set all this data compiled from different places around a city.

1 reply