Carbon Dioxide Sensor(MH-Z16) library comparison

The first library I looked at was for the DFRobot Gravity: UART Infrared CO2 Sensor (0-50000ppm). There was sample code provided on the associated wiki page. The code worked first time I ran it but I didn’t use this library due to the lack of checksum & packet header/footer validation.

/***************************************************
* Infrared CO2 Sensor 0-50000ppm(Wide Range)
* ****************************************************
* The follow example is used to detect CO2 concentration.
  
* @author lg.gang(lg.gang@qq.com)
* @version  V1.0
* @date  2016-6-6
  
* GNU Lesser General Public License.
* See <http://www.gnu.org/licenses/> for details.
* All above must be included in any redistribution
* ****************************************************/ 
#include <SoftwareSerial.h>
SoftwareSerial mySerial(10, 11); // RX, TX
unsigned char hexdata[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //Read the gas density command /Don't change the order
void setup() {
  
  Serial.begin(9600);
  while (!Serial) {

  }
  mySerial.begin(9600);

}

void loop() {
   mySerial.write(hexdata,9);
   delay(500);

 for(int i=0,j=0;i<9;i++)
 {
  if (mySerial.available()>0)
  {
     long hi,lo,CO2;
     int ch=mySerial.read();

    if(i==2){     hi=ch;   }   //High concentration
    if(i==3){     lo=ch;   }   //Low concentration
    if(i==8) {
               CO2=hi*256+lo;  //CO2 concentration
      Serial.print("CO2 concentration: ");
      Serial.print(CO2);
      Serial.println("ppm");      
      }
    }   
  } 
}

After some GitHub searching the second library I looked at was abbozza_CO2_MHZ16_arduino by Michael Brinkmeier. This library appears to be “plug-in” module for the abbozza! framework. I didn’t use this library due to the lack of checksum & packet header/footer validation.

/**
 * @license
 * abbozza! Calliope plugin for the MH-Z16 CO2 sensor
 * 
 * The sensor has to be connected to a serial connection with 9600 baud.
 *
 * Copyright 2015 Michael Brinkmeier ( michael.brinkmeier@uni-osnabrueck.de )
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "SoftwareSerial.h"
#include "MHZ16.h"
#include "Arduino.h"

MHZ16::MHZ16(int tx, int rx) {
    this->serial = new SoftwareSerial(rx,tx,false);
    this->serial->begin(9600);
}


void MHZ16::calibrate() {
    int idx;
    for (idx = 0; idx < 9; idx++) {
        serial->write(cal[idx]);
    }
    delay(10);
}

void MHZ16::doMeasurement() {
    int idx;
    int bu;

    for (idx = 0; idx < 9; idx++) {
        serial->write(cmd[idx]);
    }
    delay(10);

    while (serial->available()) {
        do {
            bu = serial->read();
        } while (bu != 255);
        buf[0] = bu;

        idx = 1;
        while (serial->available() && (idx < 9)) {
            bu = serial->read();
            buf[idx] = bu;
            idx++;
        }

        if (idx == 9) {
            PPM = ((int) buf[2]) *256 + ((int) buf[3]);
        }
    }
}

int MHZ16::getPPM() {
    return PPM;
}

The third library was produced by Sandbox electronics for their selection of 10,000PPM thru 100,000PPM MH-Z16 sensors. Their datasheet looked similar(maybe newer?) to the Seeedstudio one and the packet format was the same.

Their library had checksum & packet header/footer validation but I didn’t use it because the carbon dioxide concentration was calculated using 4 bytes (maybe this was to support the different range sensors?)

/*
Description:
This is a example code for Sandbox Electronics NDIR CO2 sensor module.
You can get one of those products on
http://sandboxelectronics.com

Version:
V1.2

Release Date:
2019-01-10

Author:
Tiequan Shao          support@sandboxelectronics.com

Lisence:
CC BY-NC-SA 3.0

Please keep the above information when you use this code in your project.
*/

#include <SoftwareSerial.h>
#include <NDIR_SoftwareSerial.h>
#define  RECEIVE_TIMEOUT  (100)

#if ARDUINO >= 100
    #include "Arduino.h"
#else
    #include "WProgram.h"
#endif

class SoftwareSerial;

uint8_t NDIR_SoftwareSerial::cmd_measure[9]                = {0xFF,0x01,0x9C,0x00,0x00,0x00,0x00,0x00,0x63};
uint8_t NDIR_SoftwareSerial::cmd_calibrateZero[9]          = {0xFF,0x01,0x87,0x00,0x00,0x00,0x00,0x00,0x78};
uint8_t NDIR_SoftwareSerial::cmd_enableAutoCalibration[9]  = {0xFF,0x01,0x79,0xA0,0x00,0x00,0x00,0x00,0xE6};
uint8_t NDIR_SoftwareSerial::cmd_disableAutoCalibration[9] = {0xFF,0x01,0x79,0x00,0x00,0x00,0x00,0x00,0x86};

NDIR_SoftwareSerial::NDIR_SoftwareSerial(uint8_t rx_pin, uint8_t tx_pin) : serial(rx_pin, tx_pin, false)
{
}


uint8_t NDIR_SoftwareSerial::begin()
{
    serial.begin(9600);

    if (measure()) {
        return true;
    } else {
        return false;
    }
}

uint8_t NDIR_SoftwareSerial::measure()
{
    uint8_t i;
    uint8_t buf[9];
    uint32_t start = millis();

    serial.flush();

    for (i=0; i<9; i++) {
        serial.write(cmd_measure[i]);
    }

    for (i=0; i<9;) {
        if (serial.available()) {
            buf[i++] = serial.read();
        }

        if (millis() - start > RECEIVE_TIMEOUT) {
            return false;
        }
    }

    if (parse(buf)) {
        return true;
    }

    return false;
}


void NDIR_SoftwareSerial::calibrateZero()
{
    uint8_t i;

    for (i=0; i<9; i++) {
        serial.write(cmd_calibrateZero[i]);
    }
}


void NDIR_SoftwareSerial::enableAutoCalibration()
{
    uint8_t i;

    for (i=0; i<9; i++) {
        serial.write(cmd_enableAutoCalibration[i]);
    }
}


void NDIR_SoftwareSerial::disableAutoCalibration()
{
    uint8_t i;

    for (i=0; i<9; i++) {
        serial.write(cmd_disableAutoCalibration[i]);
    }
}


uint8_t NDIR_SoftwareSerial::parse(uint8_t *pbuf)
{
    uint8_t i;
    uint8_t checksum = 0;

    for (i=0; i<9; i++) {
        checksum += pbuf[i];
    }

    if (pbuf[0] == 0xFF && pbuf[1] == 0x9C && checksum == 0xFF) {
        ppm = (uint32_t)pbuf[2] << 24 | (uint32_t)pbuf[3] << 16 | (uint32_t)pbuf[4] << 8 | pbuf[5];
        return true;
    } else {
        return false;
    }
}

The forth library I looked at was MHZ-Z-C02-Sensors by Tobias Schürg this library was for different series of MHZ sensors. With re-synching, configurable timeouts and checksum validation it looked like the code could easily be adapted for the MH-Z16.

/* MHZ library

    By Tobias Schürg
*/

#include "MHZ.h"

const int MHZ14A = 14;
const int MHZ19B = 19;

const int MHZ14A_RESPONSE_TIME = 60;
const int MHZ19B_RESPONSE_TIME = 120;

const int STATUS_NO_RESPONSE = -2;
const int STATUS_CHECKSUM_MISMATCH = -3;
const int STATUS_INCOMPLETE = -4;
const int STATUS_NOT_READY = -5;

unsigned long lastRequest = 0;

MHZ::MHZ(uint8_t rxpin, uint8_t txpin, uint8_t pwmpin, uint8_t type)
    : co2Serial(rxpin, txpin) {
  _rxpin = rxpin;
  _txpin = txpin;
  _pwmpin = pwmpin;
  _type = type;

  co2Serial.begin(9600);
}

/**
 * Enables or disables the debug mode (more logging).
 */
void MHZ::setDebug(boolean enable) {
  debug = enable;
  if (debug) {
    Serial.println(F("MHZ: debug mode ENABLED"));
  } else {
    Serial.println(F("MHZ: debug mode DISABLED"));
  }
}

boolean MHZ::isPreHeating() {
  if (_type == MHZ14A) {
    return millis() < (3 * 60 * 1000);
  } else if (_type == MHZ19B) {
    return millis() < (3 * 60 * 1000);
  } else {
    Serial.println(F("MHZ::isPreHeating() => UNKNOWN SENSOR"));
    return false;
  }
}

boolean MHZ::isReady() {
  if (isPreHeating()) return false;
  if (_type == MHZ14A)
    return lastRequest < millis() - MHZ14A_RESPONSE_TIME;
  else if (_type == MHZ19B)
    return lastRequest < millis() - MHZ19B_RESPONSE_TIME;
  else {
    Serial.print(F("MHZ::isReady() => UNKNOWN SENSOR \""));
    Serial.print(_type);
    Serial.println(F("\""));
    return true;
  }
}

int MHZ::readCO2UART() {
  if (!isReady()) return STATUS_NOT_READY;
  if (debug) Serial.println(F("-- read CO2 uart ---"));
  byte cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
  byte response[9];  // for answer

  if (debug) Serial.print(F("  >> Sending CO2 request"));
  co2Serial.write(cmd, 9);  // request PPM CO2
  lastRequest = millis();

  // clear the buffer
  memset(response, 0, 9);

  int waited = 0;
  while (co2Serial.available() == 0) {
    if (debug) Serial.print(".");
    delay(100);  // wait a short moment to avoid false reading
    if (waited++ > 10) {
      if (debug) Serial.println(F("No response after 10 seconds"));
      co2Serial.flush();
      return STATUS_NO_RESPONSE;
    }
  }
  if (debug) Serial.println();

  // The serial stream can get out of sync. The response starts with 0xff, try
  // to resync.
  // TODO: I think this might be wrong any only happens during initialization?
  boolean skip = false;
  while (co2Serial.available() > 0 && (unsigned char)co2Serial.peek() != 0xFF) {
    if (!skip) {
      Serial.print(F("MHZ: - skipping unexpected readings:"));
      skip = true;
    }
    Serial.print(" ");
    Serial.print(co2Serial.peek(), HEX);
    co2Serial.read();
  }
  if (skip) Serial.println();

  if (co2Serial.available() > 0) {
    int count = co2Serial.readBytes(response, 9);
    if (count < 9) {
      co2Serial.flush();
      return STATUS_INCOMPLETE;
    }
  } else {
    co2Serial.flush();
    return STATUS_INCOMPLETE;
  }

  if (debug) {
    // print out the response in hexa
    Serial.print(F("  << "));
    for (int i = 0; i < 9; i++) {
      Serial.print(response[i], HEX);
      Serial.print(F("  "));
    }
    Serial.println(F(""));
  }

  // checksum
  byte check = getCheckSum(response);
  if (response[8] != check) {
    Serial.println(F("MHZ: Checksum not OK!"));
    Serial.print(F("MHZ: Received: "));
    Serial.println(response[8], HEX);
    Serial.print(F("MHZ: Should be: "));
    Serial.println(check, HEX);
    temperature = STATUS_CHECKSUM_MISMATCH;
    co2Serial.flush();
    return STATUS_CHECKSUM_MISMATCH;
  }

  int ppm_uart = 256 * (int)response[2] + response[3];

  temperature = response[4] - 44;  // - 40;

  byte status = response[5];
  if (debug) {
    Serial.print(F(" # PPM UART: "));
    Serial.println(ppm_uart);
    Serial.print(F(" # Temperature? "));
    Serial.println(temperature);
  }

  // Is always 0 for version 14a  and 19b
  // Version 19a?: status != 0x40
  if (debug && status != 0) {
    Serial.print(F(" ! Status maybe not OK ! "));
    Serial.println(status, HEX);
  } else if (debug) {
    Serial.print(F(" Status  OK: "));
    Serial.println(status, HEX);
  }

  co2Serial.flush();
  return ppm_uart;
}

uint8_t MHZ::getLastTemperature() {
  if (isPreHeating()) return STATUS_NOT_READY;
  return temperature;
}

byte MHZ::getCheckSum(byte* packet) {
  if (debug) Serial.println(F("  getCheckSum()"));
  byte i;
  unsigned char checksum = 0;
  for (i = 1; i < 8; i++) {
    checksum += packet[i];
  }
  checksum = 0xff - checksum;
  checksum += 1;
  return checksum;
}

int MHZ::readCO2PWM() {
  // if (!isReady()) return STATUS_NOT_READY; not needed?
  if (debug) Serial.print(F("-- reading CO2 from pwm "));
  unsigned long th, tl, ppm_pwm = 0;
  do {
    if (debug) Serial.print(".");
    th = pulseIn(_pwmpin, HIGH, 1004000) / 1000;
    tl = 1004 - th;
    ppm_pwm = 5000 * (th - 2) / (th + tl - 4);
  } while (th == 0);
  if (debug) {
    Serial.print(F("\n # PPM PWM: "));
    Serial.println(ppm_pwm);
  }
  return ppm_pwm;
}

The forth library I looked at was MHZ16_uart by Intar it had been updated recently, was quite lightweight, had timeouts, checksum & packet header/footer validation.

/*
  MHZ16_uart.cpp - MH-Z16 CO2 sensor library for ESP-32
  by Intar BV
  version 0.1
  
  License MIT
*/

#include "MHZ16_uart.h"
#include "Arduino.h"


#define WAIT_READ_TIMES	100
#define WAIT_READ_DELAY	10

// public

MHZ16_uart::MHZ16_uart(){
}
MHZ16_uart::MHZ16_uart(int rx, int tx){
	begin(rx,tx);
}

MHZ16_uart::~MHZ16_uart(){
}

#ifdef ARDUINO_ARCH_ESP32
void MHZ16_uart::begin(int rx, int tx, int s){
	_rx_pin = rx;
	_tx_pin = tx;
	_start_millis = millis();
	_serialno = s;
}
#else
void MHZ16_uart::begin(int rx, int tx){
	_rx_pin = rx;
	_start_millis = millis();
	_tx_pin = tx;
}
#endif

void MHZ16_uart::calibrateZero() {
	writeCommand( zerocalib );
}

void MHZ16_uart::calibrateSpan(int ppm) {
	if( ppm < 1000 )	return;

	uint8_t com[MHZ16_uart::REQUEST_CNT];
	for(int i=0; i<MHZ16_uart::REQUEST_CNT; i++) {
		com[i] = spancalib[i];
	}
	com[3] = (uint8_t)(ppm/256);
	com[4] = (uint8_t)(ppm%256);
	writeCommand( com );
}

int MHZ16_uart::getPPM() {
	return getSerialData();
}

boolean MHZ16_uart::isWarming(){
	return millis() <= _start_millis + PREHEAT_MS;
}

//protected
void MHZ16_uart::writeCommand(uint8_t cmd[]) {
	writeCommand(cmd,NULL);
}

void MHZ16_uart::writeCommand(uint8_t cmd[], uint8_t* response) {
#ifdef ARDUINO_ARCH_ESP32
	HardwareSerial hserial(_serialno);
	hserial.begin(9600, SERIAL_8N1, _rx_pin, _tx_pin);
#else
	SoftwareSerial hserial(_rx_pin, _tx_pin);
	hserial.begin(9600);
#endif
    hserial.write(cmd, REQUEST_CNT);
	hserial.write(MHZ16_checksum(cmd));
	hserial.flush();
	
	if (response != NULL) {
		int i = 0;
		while(hserial.available() <= 0) {
			if( ++i > WAIT_READ_TIMES ) {
				Serial.println("error: can't get MH-Z16 response.");
				return;
			}
			yield();
			delay(WAIT_READ_DELAY);
		}
		hserial.readBytes(response, MHZ16_uart::RESPONSE_CNT);
	}

}

//private

int MHZ16_uart::getSerialData() {
	uint8_t buf[MHZ16_uart::RESPONSE_CNT];
	for( int i=0; i<MHZ16_uart::RESPONSE_CNT; i++){
		buf[i]=0x0;
	}

	writeCommand(getppm, buf);
	int co2 = 0, co2temp = 0, co2status =  0;

	// parse
	if (buf[0] == 0xff && buf[1] == 0x86 && MHZ16_checksum(buf) == buf[MHZ16_uart::RESPONSE_CNT-1]) {
		co2 = buf[2] * 256 + buf[3];
	} else {
		co2 = co2temp = co2status = -1;
	}
	return co2;
}	

uint8_t MHZ16_uart::MHZ16_checksum( uint8_t com[] ) {
	uint8_t sum = 0x00;
	for ( int i = 1; i < MHZ16_uart::REQUEST_CNT; i++) {
		sum += com[i];
	}
	sum = 0xff - sum + 0x01;
	return sum;
}

It ran second time on one of my Arduino devices (after I figured out how to configure the serial port pins) and though intended for an ESP8266 device this is the library I will field test.

#include <MHZ16_uart.h>

//Select 2 digital pins as SoftwareSerial's Rx and Tx. For example, Rx=2 Tx=3
MHZ16_uart mySensor(4,5);

void setup()
{
  Serial.begin(9600);

  mySensor.begin(4,5); 
}


void loop() 
{
  if ( !mySensor.isWarming())
  {
    Serial.print("CO2 Concentration is ");
    Serial.print(mySensor.getPPM());
    Serial.println("ppm");
  }
  else
{
    Serial.println("isWarming");
  }
  
  delay(10000);
}

This was just a sample of the libraries I found on GitHub if I missed a good a library contact me via the comments.

3 thoughts on “Carbon Dioxide Sensor(MH-Z16) library comparison

  1. Hello,

    Your trail code, for the MH-Z16, worked very well for us (students working on project). We are trying to use this sensor with 3 others and a SIM900 GSM shield on the same Arduino Uno R3 board. With your trial code, the sensor works great… by itself. However, when we replace your “void loop” in the trial code with a function name and place it into a new void loop to be called with other sensors, it doesnt work. It keeps giving us a “Error Checksum.” It’s weird because it works well on its own. We even added in a few lines to get temperature data (which we don’t need from this sensor), and that worked with the stand alone sensor. We are really unsure why the sensor is giving us a great deal of trouble when combinded with others.

    The work you have done so far has been extremely helpful for our student project. We would really appreciate any insight or advice you could offer.

    Thanks,

    Josh V

  2. Hi

    Make sure you have the software serial PINs correct. i.e. not colliding with serial port for modem or possibly its CTR/RTS lines.

    What values are displayed in the serial monitor for the 9 byte packets returned by the CO2 sensor?

    Do they look valid or are they random junk characters?

    @KiwiBryn

  3. Hello, I am another student involved with this project. This is what the sensor is giving for the 9 byte feedback: “55 1 4B 46 7F FF B 10 D0”. The other day when we ran it, we got this 9 byte packet: “FE FF FF FF FF FF FF FF FF”. It is more common for us to get the first packet. Some times we get a 56 for the first byte instead of 55 in the first packet. Please let us know if you have any insight. We are pulling our hair out at the moment. Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.