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.