Carbon dioxide PPM levels using the MH-Z19B sensor

I’m currently testing a non-dispersive infrared (NDIR) based sensor to detect carbon dioxide (CO2) parts per million (PPM) levels using an MH-Z19B sensor, coupled with an ESP8266 based NodeMCU V3.

MH-Z19B sensor connected to an ESP8266 NodeMCU

Compared to other sensor technologies (such as filament heating of electrochemical plates used by the MQ-135 sensor), the MH-Z19B has the following advantages:

  • is factory calibrated (does not need calibration)
  • is accurate and does not fluctuate radically
  • PPM values are already corrected for temperature dependancies
  • readily works out of the box with no 24-48 hour “burn in” period
  • is specific to CO2 PPM (filament heating of electrochemical plates are not specific to a particular gas, and respond to more than one gas)

The MH-Z19B is connected to the NodeMCU V3 as follows.

Schematic diagram showing the cable connections between the MH-Z19B sensor and the NodeMCU V3.

I have programmed the ESP8266 based NodeMCU V3 to connect to my WiFi network, and send the MH-Z19B sensor’s CO2 PPM measurements to a Thingspeak channel.

The graph below shows the previous hour of CO2 PPM measurements, updated every 15 seconds.

The gauge below shows the latest CO2 PPM reading from the MH-Z19B sensor.

The following C++ code reads the CO2 PPM (and sensor temperature) from the MH-Z19B sensor’s universal asynchronous receiver transmitter (UART) port (i.e. Rxd and Txd pins), and uploads them to my Thingspeak channel.

Note: there is no error in the C++ code below and the schematic diagram above. With serial connections, a transmit from one device must go to a receive on the other device, and the receive from one device must go to a transmit of the other device.

#include <ESP8266WiFi.h>
#include <SoftwareSerial.h>
SoftwareSerial co2Serial(D3, D4); // define MH-Z19 RX TX D3 (GPIO0) and D4 (GPIO2)
unsigned long startTime = millis();
const char* ssid     = "Your WiFi SSID";
const char* password = "Your WiFi Password";
const char* host = "api.thingspeak.com";
String apiKey = "Your Thingspeak API write key of your Thingspeak channel"; // thingspeak.com api key goes here
WiFiClient client;
void setup() {
void loop() {
  Serial.print("Time from start: ");
  Serial.print((millis() - startTime) / 1000);
  Serial.println(" s");
  int ppm_uart = readCO2UART();  
int readCO2UART() {
  byte cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
  char response[9];
  Serial.println("Sending CO2 request...");
  co2Serial.write(cmd, 9); //request PPM CO2
  // clear the buffer
  memset(response, 0, 9);
  int i = 0;
  while (co2Serial.available() == 0) {
       Serial.print("Waiting for response ");
        Serial.println(" s");
  if (co2Serial.available() > 0) {
    co2Serial.readBytes(response, 9);
  // print out the response in hexa
  for (int i = 0; i < 9; i++) {
    Serial.print(String(response[i], HEX));
    Serial.print("   ");
  // checksum
  byte check = getCheckSum(response);
  if (response[8] != check) {
    Serial.println("Checksum not OK!");
    Serial.print("Received: ");
    Serial.print("Should be: ");
  // ppm
  int ppm_uart = 256 * (int)response[2] + response[3];
  Serial.print("UART CO2 PPM: ");
  // temp
  byte temp = response[4] - 40;
  Serial.print("Sensor Temperature: ");
  // status
  byte status = response[5];
  Serial.print("Status: ");
  if (status == 0x40) {
    Serial.println("Status OK");
if (client.connect(host,80)) {
    String postStr = apiKey;
    postStr +="&field1=";
    postStr += String(ppm_uart);
    postStr +="&field2=";
    postStr += String(temp);
    postStr += "\r\n\r\n";
    client.print("POST /update HTTP/1.1\n");
    client.print("Host: api.thingspeak.com\n");
    client.print("Connection: close\n");
    client.print("X-THINGSPEAKAPIKEY: "+apiKey+"\n");
    client.print("Content-Type: application/x-www-form-urlencoded\n");
    client.print("Content-Length: ");
  Serial.print("Uploaded to Thingspeak ");
  return ppm_uart;
byte getCheckSum(char *packet) {
  byte i;
  unsigned char checksum = 0;
  for (i = 1; i < 8; i++) {
    checksum += packet[i];
  checksum = 0xff - checksum;
  checksum += 1;
  return checksum;
void connectToWiFi(){
  WiFi.begin(ssid, password);
  Serial.print("Connecting to ");
  while (WiFi.status() != WL_CONNECTED) {
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");

Note: in the C++ code above, my WiFi credentials and Thingspeak channel’s API write key have been removed.

Future updates to this C++ code can be found at https://github.com/robertwisbey/esp8266_mh-z19b_thingspeak.

In addition to embedding Thingspeak data on web pages like above, or directly viewing the Thingspeak channel data on the Thingspeak website, you can also view Thingspeak channels in a smartphone app called Thingview (iOS link or Android link).

Example iOS screenshot of the Thingview app configured to read my MH-Z19B Thingspeak channel

The great thing about Thingspeak is that you can set up a “React” App and “ThingHTTP” App, and couple them with the IFTTT (IF This Then That) platform, so it sends you notifications when the CO2 PPM level exceeds a configured threshold.

Example of IFTTT notifications coupled with Thingspeak React App threshold value of 400 PPM

Note: in the screenshot above, I intentionally set a low threshold value of 400 PPM in order to get notifications sent to my iPhone. Normally you would set a higher threshold value, such as 1000 PPM.

More information on how to configure Thingspeak with IFTTT will follow soon !