ESP32, PMS5003, BME280, MICS6814 Sensor Build

Marshall feature image


This build consists of an ESP32 with a PMS5003 particle sensor, a BME280 temperature sensor, and a MICS6814 gas sensor for environment monitoring. This is the second sensor array in the system I call AirPatrol. For reasons I explained previously I call this sensor array Marshall. It will monitor environment conditions in my living room. This sensor array looks a lot like Chase and some of the building blocks are the same. Therefore, these building blocks will not be described in as much detail in this post. I will, however, make sure to drop links whenever appropriate.


Let me start out with a few words on the choices of components before presenting lists of materials and tools used.

PMS5003 is a particle sensor. It uses laser to measure the concentration of particles – PM1.0, PM2.5, and PM10.0. These represent particles of sizes 0.3-1.0, 1.0-2.5, and 2.5-10 μm respectively. The sensor uses a fan to circulate the air. This fan must run for 30-60 seconds before measurements are reliable. The laser diode in this sensor has a lifetime of 8000 hours. Thus, I need to take measures to ensure the sensor doesn’t ware out in about a year.

The BME280 temperature, humidity, and pressure sensor was also used for Chase. I’m happy with that sensor and will use it again for this build.

I’m not satisfied with the CCS811 gas sensor I used for Chase. As a consequence I’ve been on the look-out for alternatives. The choice fell on the MICS6814 which measures carbon monoxide, nitrogen dioxide, and ammonia. As opposed to the other sensors this one is analog.

Which leads me to the choice of main board. The ESP32 has more ADC-pins than the ESP8266. And since the MICS6814 has three analog output pins the natural choice is the ESP32.



  • Wire cutters.
  • Soldering iron.
  • Hot glue gun.



A few design sketches has never hurt anyone. First PMS5003 wiring to the ESP32. I followed wiring diagram from the PMS5003 specification. Note that the sensor requires 5V to operate, while the signalling is 3.3V.

PMS5003 ESP32 wiring diagram

Then wiring of BME280 to ESP32 – which was also presented for Chase.

BME280 ESP32 Wiring Diagram

Finally, the wiring of MICS6814 to ESP32. Like with the PMS5003 this sensor requires 5V supply while the analog signals max out at 3.3V.

MICS6814 ESP32 Wiring Diagram


Some of the sensors were tricky to get working. Here are some of the details.


First off an overview of the colour coding of the PMS5003 connector wires. These colours vary from sensor to sensor so make sure to double check your own.

PMS5003 wires

I had the sensor wired up and running on a breadboard for a while before doing the assembly. As mentioned earlier this sensor wears out after 8000 hours of operation. Pulling the set pin low will disable the sensor. My idea was to keep the set pin low for 9-10 minutes, pull it high and let it run for one minute and then do my measurements. The rest of the system measures every minute.

In the beginning I did get measurements, but not as expected. The sensor has two modes: Active mode and Passive mode. The default mode is Active and the sensor automatically sends readings every second or so. In Passive mode a command is sent to the sensor which then transmits one reading.

My intention was for the operation sequence to be like this for start up:

  • Power on sensor.
  • Put sensor in Passive mode.

And for this when it was running:

  • Wait one minute.
  • Request data.
  • Read Rx pin until data received.
  • Pull SET pin low.
  • Wait 9 minutes.
  • Pull SET pin high.
  • Repeat from the top of the list.

With this setup I frequently experienced data not available on the Rx pin – which was running with a one second timeout. It was time to put my Saleae Logic 16 logic analyser to work. A logic analyser is a debug tool that makes it possible to “listen in” on the traffic on the wires.

PMS5003 Logic analyser debugging

Unfortunately, I didn’t take the appropriate screenshots during the process. But these are my finding.

In the first iteration I didn’t have the ESP32 Tx pin wired up correctly, so the PMS5003 didn’t receive my commands. It would stay in active mode and transmit data every second. My read timeout being one second sometimes due to timing I would not receive all the data before timing out. Doubling my timeout value made the issue of data not available go away.

Second iteration it did receive my commands and the first attempt to read data did go as expected: passive mode, request data, read data, set pin low. But when the set pin was pulled high again the sensor would have cleared the passive mode setting and gone back to active mode. I tried putting it back into passive mode right after pulling the set pin high, but the sensor ignored commands at this point. It probably needs a bit of time before being ready. But at this point I reached the conclusion:

If it ain’t broken, then don’t fix it.

I did get data from the sensor. I did put it to sleep between readings to extend it’s lifetime. Active mode would work just fine for me. Below are the final trace of data on the wire. The sleep and active periods are reduced in the trace for debugging purpose.

PMS5003 logic analyser trace


During the build process I needed to test the MICS6814. First I connected the sensor directly to a power supply and a voltmeter to test the readings. I used a small glass with ethanol to test the sensor.

MICS6814 ethanol testing

The MICS6814 is an analog gas sensor, so it needs pull up resistors to give valid readings. Took me a while and some frustration to get right. The ADC on the ESP32 will convert 0-3.3V to digital values. Therefore, I needed to make sure the readings were inside this range. I did this through the values of the resistors. I mounted all the pull up resistors for this build on the same piece of veroboard.

Mounting of pull up resistors

When I had that working I wired the sensor up on a breadboard before final assembly and soldering.

Final assembly

After getting everything running on a breadboard I soldered everything together.

ESP32 PMS5003 BME280 MICS6814 testing

Double checked that everything was working after assembly. And the finally mounted it all in a box. I had cut a venting hole in the bottom of the box to allow both PMS5003 and MICS6814 access to fresh air. The hole is covered with a mesh to prevent spiders and similar to move into the box.

Final ESP32 PMS5003 BME280 MICS6814 assembly

Sensor box temporarily mounted on the wall in the corner of the living room.

Sensor wall mounted


The software makes use of components I already had from Chase. For software for the BME280 sensor and for the data publisher, see ESP8266, BME280, CCS811 Sensor Build.

PMS5003 Software

A few libraries exist for controlling the PMS5003. I downloaded a few and played around with them. I ended up using the fu-hsi/pms library. The sensor is reset if data cannot be read from it. As mentioned earlier the sensor will run for one “tick” before doing a reading. Then it will sleep for nine “ticks” before repeating. In my system a tick is one minute.

  1. #include <Arduino.h>
  2. #include "PMS.h"
  4. #define PMS_SET_PIN 26U
  5. #define PMS_RST_PIN 25U
  6. #define PMS_READ_INTERVAL 9U
  7. #define PMS_READ_DELAY 1U
  8. uint8_t pms_tick_count = PMS_READ_INTERVAL;
  9. PMS pms(Serial2);
  11. /***************************************************/
  12. static void setup_pins(void) {
  13.   pinMode(PMS_RST_PIN, OUTPUT);
  14.   digitalWrite(PMS_RST_PIN, HIGH);
  15.   pinMode(PMS_SET_PIN, OUTPUT);
  16.   digitalWrite(PMS_SET_PIN, HIGH);
  17. }
  19. /***************************************************/
  20. static void toggle_set(bool sleep) {
  21.   if (sleep) {
  22.     digitalWrite(PMS_SET_PIN, LOW);
  23.   } else {
  24.     digitalWrite(PMS_SET_PIN, HIGH);
  25.   }
  26.   delay(500U);
  27. }
  29. /***************************************************/
  30. static void toggle_reset(void) {
  31.   digitalWrite(PMS_RST_PIN, LOW);
  32.   delay(500U);
  33.   digitalWrite(PMS_RST_PIN, HIGH);
  34.   delay(500U);
  35. }
  37. /***************************************************/
  38. bool pms5003_init(void) {
  39.   setup_pins();
  40.   Serial2.begin(PMS::BAUD_RATE, SERIAL_8N1, 16U, 17U);
  41.   delay(1000U);
  42.   pms_tick_count = PMS_READ_INTERVAL;
  43.   return true;
  44. }
  46. /***************************************************/
  47. bool pms5003_read(uint16_t *pmSp1_0, uint16_t *pmSp2_5, uint16_t *pmSp10_0,
  48.                   uint16_t *pmAe1_0, uint16_t *pmAe2_5, uint16_t *pmAe10_0) {
  49.   bool result = false;
  51.   if ((NULL == pmSp1_0) || (NULL == pmSp2_5) || (NULL == pmSp10_0)
  52.   || (NULL == pmAe1_0) || (NULL == pmAe2_5) || (NULL == pmAe10_0)) {
  53.     result = false;
  54.   }
  55.   else {
  56.     pms_tick_count++;
  57.     if (pms_tick_count == PMS_READ_DELAY) {
  58.       PMS::DATA data;
  59.       while (Serial2.available())
  60.       {
  62.       }
  64.       if (pms.readUntil(data, 2U*PMS::SINGLE_RESPONSE_TIME))
  65.       {
  66.         *pmSp1_0 = data.PM_AE_UG_1_0;
  67.         *pmSp2_5 = data.PM_AE_UG_2_5;
  68.         *pmSp10_0 = data.PM_AE_UG_10_0;
  69.         *pmAe1_0 = data.PM_SP_UG_1_0;
  70.         *pmAe2_5 = data.PM_SP_UG_2_5;
  71.         *pmAe1_0 = data.PM_SP_UG_10_0;
  72.         result = true;
  73.       }
  74.       else
  75.       {
  76.         toggle_reset();
  77.       }
  78.       toggle_set(true);
  79.     } else if (pms_tick_count >= PMS_READ_INTERVAL) {
  80.       toggle_set(false);
  81.       pms_tick_count = 0U;
  82.     }
  83.   }
  85.   return result;
  86. }

MICS6814 Software

Analog reading are prone to noise. To mitigate this I average three readings from each pin for the final results. Like with the CCS811 this sensor has a warm up period before readings are accurate. Don’t trust the readings for the first 20 minutes or so. There is no guard for this in the software.

  1. #include <Arduino.h>
  2. #include <driver/adc.h>
  4. #define ADC_MAX_VALUE 4095U
  5. #define ADC_MIN_VALUE 0U
  6. #define ADC_SAMPLES 3U
  7. #define ADC_SAMPLE_DELAY 100U
  9. /***************************************************/
  10. bool mics6814_init(void) {
  11.   adc1_config_width(ADC_WIDTH_BIT_12);
  12.   adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  13.   adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_DB_11);
  14.   adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11);
  15.   return true;
  16. }
  18. /***************************************************/
  19. bool mics6814_read(uint16_t* no2, uint16_t* nh3, uint16_t* co) {
  20.   bool result = true;
  22.   if ((NULL == no2) || (NULL == nh3) || (NULL == co)) {
  23.     result = false;
  24.   }
  26.   uint16_t tempNo2 = 0U;
  27.   uint16_t tempNh3 = 0U;
  28.   uint16_t tempCo = 0U;
  29.   uint8_t count = 0U;
  30.   while ((true == result) && (count < ADC_SAMPLES)) {
  31.     if (count > 0U) {
  32.       delay(ADC_SAMPLE_DELAY);
  33.     }
  35.     int temp = adc1_get_raw(ADC1_CHANNEL_6);
  36.     if ((temp >= ADC_MIN_VALUE) && (temp <= ADC_MAX_VALUE)) {
  37.       tempNo2 += (uint16_t)temp;
  38.     } else {
  39.       result = false;
  40.     }
  42.     if (true == result) {
  43.       temp = adc1_get_raw(ADC1_CHANNEL_7);
  44.       if ((temp >= ADC_MIN_VALUE) && (temp <= ADC_MAX_VALUE)) {
  45.         tempNh3 += (uint16_t)temp;
  46.       } else {
  47.         result = false;
  48.       }
  49.     }
  51.     if (true == result) {
  52.       temp = adc1_get_raw(ADC1_CHANNEL_4);
  53.       if ((temp >= ADC_MIN_VALUE) && (temp <= ADC_MAX_VALUE)) {
  54.         tempCo += (uint16_t)temp;
  55.         count++;
  56.       } else {
  57.         result = false;
  58.       }
  59.     }
  60.   }
  62.   if (true == result) {
  63.     *no2 = ADC_MAX_VALUE - (tempNo2 / ADC_SAMPLES);
  64.     *nh3 = ADC_MAX_VALUE - (tempNh3 / ADC_SAMPLES);
  65.     *co = ADC_MAX_VALUE - (tempCo / ADC_SAMPLES);
  66.   }
  68.   return result;
  69. }

main.cpp Software

The main program will consist of the two sensor softwares just presented and of the BME280 and Publisher softwares from Chase.

Measurements will be stored in an InfluxDB as described in Collect, Present, Alert. Therefore, the sensor reading as stored in a string called line as InfluxDB Line Protocol.

The format of the InfluxDB Line Protocol is in short:

  • The name of the measurement.
  • A tag which in this case is location as I’ll collect the same kind of measurements from different locations.
  • A field called value which holds the measured value.
  1. /***************************************************/
  2. /********************** SETUP **********************/
  3. /***************************************************/
  4. void setup()
  5. {
  6.   /* Setup serial communication. */
  7.   Serial.begin(9600U);
  8.   delay(1000U);
  10.   /* Perform initialisations. */
  11.   if (!publisher_init()) {
  12.     publisher_sendSyslog(SYSLOG_ALARM, "Failed to start publisher - check WiFi.");
  13.     while (1);
  14.   }
  16.   if (!bme280_init()) {
  17.     publisher_sendSyslog(SYSLOG_CRITICAL, "Failed to start BME280 temp/hum/bar sensor - check wiring.");
  18.     while (1);
  19.   }
  21.   if ( !pms5003_init() ) {
  22.     publisher_sendSyslog(SYSLOG_CRITICAL, "Failed to start PMS5003 particle sensor - check wiring.");
  23.     while(1);
  24.   }
  26.   if ( !mics6814_init() ) {
  27.     publisher_sendSyslog(SYSLOG_CRITICAL, "Failed to start MICS6814 gas sensor - check wiring.");
  28.     while(1);
  29.   }
  31.   publisher_sendSyslog(SYSLOG_DEBUG, "Setup successful.");
  32. }
  34. /***************************************************/
  35. /**********************  LOOP  *********************/
  36. /***************************************************/
  37. void loop() {
  38.   String line = "";
  40.   float temp = NAN;
  41.   float hum = NAN;
  42.   float pres = NAN;
  43.   if (bme280_read(&temp, &hum, &pres)) {
  44.     line = String(line + "temperature,location=" + String(LOCATION) + " value=" + String(temp) + "\n");
  45.     line = String(line + "humidity,location=" + String(LOCATION) + " value=" + String(hum) + "\n");
  46.     line = String(line + "pressure,location=" + String(LOCATION) + " value=" + String(pres) + "\n");
  47.   }
  49.   uint16_t pmSp1_0 = 0U;
  50.   uint16_t pmSp2_5 = 0U;
  51.   uint16_t pmSp10_0 = 0U;
  52.   uint16_t pmAe1_0 = 0U;
  53.   uint16_t pmAe2_5 = 0U;
  54.   uint16_t pmAe10_0 = 0U;
  55.   if (pms5003_read(&pmSp1_0, &pmSp2_5, &pmSp10_0, &pmAe1_0, &pmAe2_5, &pmAe10_0)) {
  56.     line = String(line + "particle,location=" + String(LOCATION) + " pm_ae_1_0=" + String(pmSp1_0) + ",pm_ae_2_5=" + String(pmSp2_5) + ",pm_ae_10_0=" + String(pmSp10_0) + ",pm_sp_1_0=" + String(pmAe1_0) + ",pm_sp_2_5=" + String(pmAe2_5) + ",pm_sp_10_0=" + String(pmAe10_0) + "\n");
  57.   }
  59.   uint16_t no2 = 0U;
  60.   uint16_t nh3 = 0U;
  61.   uint16_t co = 0U;
  62.   if (mics6814_read(&no2, &nh3, &co)) {
  63.     line = String(line + "gas,location=" + String(LOCATION) + " no2=" + String(no2) + ",nh3=" + String(nh3) + ",co=" + String(co) + "\n");
  64.   }
  66.   if (line != "") {
  67.     Serial.println(line);
  68.     publisher_sendData(line);
  69.   } else {
  70.     publisher_sendSyslog(SYSLOG_INFO, "Nothing to transmitted.");
  71.   }
  73.   // Now sleep
  74.   delay(60000U);
  75. }


With everything up and running and a new dashboard setup in Grafana the sensor build was done.

ESP32, PMS5003, BME280, MICS6814 Grafana Dashboard


After the sensor array was done I was quick to mount it in the corner of the living room – close to the floor.

As the data started to tick in I noticed a higher temperature and a lower humidity than I had seen before. I blamed the location and tried moving to where I had it during the build and testing. Same results. Even removing the lid did not lower the temperature.

The graph below show the result before and after the build was done. And with Chase as a reference. A temperature increase of 6-8 degrees Celsius.

MICS6814 effect on BME280 temperature

I had noticed MICS6814 could get warm, so I decided to take a thermal image of the setup. Sure enough, MICS6814 is heating everything around it.

MICS6814 thermal image

Haven’t we all lived in an apartment with a noisy neighbour? Well, this is just too much and the neighbour has to go. I need to find another gas sensor, that does not disturb the temperature readings. I’m thinking BME680 but that will be the topic of another post.