In preparation for a student project to monitor the CO2 levels in a number of classrooms I purchased a Grove – Carbon Dioxide Sensor(MH-Z16) for evaluation.

Arduino Uno R3 and CO2 Sensor
I downloaded the seeedstudio wiki example code, compiled and uploaded it to one of my Arduino Uno R3 devices.
I increased delay between readings to 10sec and reduced the baud rate of the serial logging to 9600baud.
/*
This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:
// #include <SoftwareSerial.h>
// SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor Serial1
*/
#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor s_serial
const unsigned char cmd_get_sensor[] =
{
0xff, 0x01, 0x86, 0x00, 0x00,
0x00, 0x00, 0x00, 0x79
};
unsigned char dataRevice[9];
int temperature;
int CO2PPM;
void setup()
{
sensor.begin(9600);
Serial.begin(9600);
Serial.println("get a 'g', begin to read from sensor!");
Serial.println("********************************************************");
Serial.println();
}
void loop()
{
if(dataRecieve())
{
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.print(" CO2: ");
Serial.print(CO2PPM);
Serial.println("");
}
delay(10000);
}
bool dataRecieve(void)
{
byte data[9];
int i = 0;
//transmit command data
for(i=0; i<sizeof(cmd_get_sensor); i++)
{
sensor.write(cmd_get_sensor[i]);
}
delay(10);
//begin reveiceing data
if(sensor.available())
{
while(sensor.available())
{
for(int i=0;i<9; i++)
{
data[i] = sensor.read();
}
}
}
for(int j=0; j<9; j++)
{
Serial.print(data[j]);
Serial.print(" ");
}
Serial.println("");
if((i != 9) || (1 + (0xFF ^ (byte)(data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]))) != data[8])
{
return false;
}
CO2PPM = (int)data[2] * 256 + (int)data[3];
temperature = (int)data[4] - 40;
return true;
}
The debug output wasn’t too promising there weren’t any C02 parts per million (ppm) values and the response payloads looked wrong. So I downloaded the MH-Z16 NDIR CO2 Sensor datasheet for some background. The datasheet didn’t mention any temperature data in the message payloads so I removed that code.
The response payload validation code was all on one line and hard to figure out what it was doing.
if((i != 9) || (1 + (0xFF ^ (byte)(data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]))) != data[8])
{
return false;
}
To make debugging easier I split the payload validation code into several steps so I could see what was failing.
/*
This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:
// #include <SoftwareSerial.h>
// SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor Serial1
*/
#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor s_serial
const unsigned char cmd_get_sensor[] =
{
0xff, 0x01, 0x86, 0x00, 0x00,
0x00, 0x00, 0x00, 0x79
};
unsigned char dataRevice[9];
int CO2PPM;
void setup()
{
sensor.begin(9600);
Serial.begin(9600);
Serial.println("get a 'g', begin to read from sensor!");
Serial.println("********************************************************");
Serial.println();
}
void loop()
{
if(dataRecieve())
{
Serial.print(" CO2: ");
Serial.print(CO2PPM);
Serial.println("");
}
delay(10000);
}
bool dataRecieve(void)
{
byte data[9];
int i = 0;
//transmit command data
for(i=0; i<sizeof(cmd_get_sensor); i++)
{
sensor.write(cmd_get_sensor[i]);
}
delay(10);
//begin reveiceing data
if(sensor.available())
{
while(sensor.available())
{
for(int i=0;i<9; i++)
{
data[i] = sensor.read();
}
}
}
for(int j=0; j<9; j++)
{
Serial.print(data[j]);
Serial.print(" ");
}
Serial.println("");
// First calculate then validate the check sum as there is no point in proceeding if the packet is corrupted. (code inspired by datasheet algorithm)
byte checksum = 0 ;
for(int j=1; j<8; j++)
{
checksum += data[j];
}
checksum=0xff-checksum;
checksum+=1;
if (checksum != data[8])
{
Serial.println("Error checksum");
return false;
}
// Then check the start byte to make sure response is what we were expecting
if ( data[0] != 0xFF )
{
Serial.println("Error start byte");
return false;
}
// Then check the command byte to make sure response is what we were expecting
if ( data[1] != 0x86 )
{
Serial.println("Error command");
return false;
}
CO2PPM = (int)data[2] * 256 + (int)data[3];
return true;
}
From these modifications I could see the payload was messed up and based on the datasheet message descriptions it looked like it was offset by a byte or two.
15:58:32.509 -> get a 'g', begin to read from sensor!
15:58:32.578 -> ********************************************************
15:58:32.612 ->
15:58:32.612 -> 255 134 6 238 76 0 0 1 255
15:58:32.647 -> Error checksum
15:58:42.631 -> 57 255 134 6 246 76 0 0 1
15:58:42.666 -> Error checksum
15:58:52.667 -> 49 255 134 5 125 76 0 0 1
15:58:52.702 -> Error checksum
15:59:02.704 -> 171 255 134 4 86 76 0 0 1
15:59:02.750 -> Error checksum
I had a look at the code and the delay(10) after sending the sensor reading request message caught my attention. I have found that often delay(x) commands are used to “tweak” the code to get it to work.
These “tweaks” often break when code is run on a different device or sensor firmware is updated changing the timing of individual bytes, or request-response processes.
I removed the delay(10) replaced it with a serial.flush() and changed the code to display the payload bytes in hexadecimal.
/*
This test code is write for Arduino AVR Series(UNO, Leonardo, Mega)
If you want to use with LinkIt ONE, please connect the module to D0/1 and modify:
// #include <SoftwareSerial.h>
// SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor Serial1
*/
#include <SoftwareSerial.h>
SoftwareSerial s_serial(2, 3); // TX, RX
#define sensor s_serial
const unsigned char cmd_get_sensor[] =
{
0xff, 0x01, 0x86, 0x00, 0x00,
0x00, 0x00, 0x00, 0x79
};
unsigned char dataRevice[9];
int CO2PPM;
void setup()
{
sensor.begin(9600);
Serial.begin(9600);
Serial.println("get a 'g', begin to read from sensor!");
Serial.println("********************************************************");
Serial.println();
}
void loop()
{
if(dataRecieve())
{
Serial.print(" CO2: ");
Serial.print(CO2PPM);
Serial.println("");
}
delay(10000);
}
bool dataRecieve(void)
{
byte data[9];
int i = 0;
//transmit command data
for(i=0; i<sizeof(cmd_get_sensor); i++)
{
sensor.write(cmd_get_sensor[i]);
}
Serial.flush();
//begin reveiceing data
if(sensor.available())
{
while(sensor.available())
{
for(int i=0;i<9; i++)
{
data[i] = sensor.read();
}
}
}
for(int j=0; j<9; j++)
{
Serial.print(data[j],HEX);
Serial.print(" ");
}
Serial.println("");
// First calculate then validate the check sum as there is no point in proceeding if the packet is corrupted. (code inspired by datasheet algorithm)
byte checksum = 0 ;
for(int j=1; j<8; j++)
{
checksum += data[j];
}
checksum=0xff-checksum;
checksum+=1;
if (checksum != data[8])
{
Serial.println("Error checksum");
return false;
}
// Then check the start byte to make sure response is what we were expecting
if ( data[0] != 0xFF )
{
Serial.println("Error start byte");
return false;
}
// Then check the command byte to make sure response is what we were expecting
if ( data[1] != 0x86 )
{
Serial.println("Error command");
return false;
}
CO2PPM = (int)data[2] * 256 + (int)data[3];
return true;
}
The initial values from the sensor were a bit high, but after leaving the device running for 3 minutes (Preheat time in the documentation) they settled down into a reasonable range
16:14:31.686 -> get a 'g', begin to read from sensor!
16:14:31.721 -> ********************************************************
16:14:31.789 ->
16:14:31.789 -> 255 134 6 224 75 0 0 1 72
16:14:31.823 -> CO2: 1760
16:14:41.824 -> 255 134 6 224 75 0 0 1 72
16:14:41.824 -> CO2: 1760
16:14:51.824 -> 255 134 5 189 75 0 0 1 108
16:14:51.858 -> CO2: 1469
16:15:01.868 -> 255 134 3 157 75 0 0 1 142
16:15:01.868 -> CO2: 925
16:15:11.857 -> 255 134 3 223 75 0 0 1 76
16:15:11.892 -> CO2: 991
16:15:21.882 -> 255 134 6 56 75 0 0 1 240
16:15:21.917 -> CO2: 1592
16:15:31.911 -> 255 134 4 186 75 0 0 1 112
16:15:31.945 -> CO2: 1210
16:15:41.927 -> 255 134 3 131 75 0 0 1 168
16:15:41.962 -> CO2: 899
16:15:51.940 -> 255 134 3 30 75 0 0 1 13
16:15:51.975 -> CO2: 798
16:16:01.986 -> 255 134 2 201 75 0 0 1 99
16:16:01.986 -> CO2: 713
16:16:11.985 -> 255 134 4 133 75 0 0 1 165
16:16:12.019 -> CO2: 1157
16:16:22.020 -> 255 134 6 62 75 0 0 1 234
16:16:22.053 -> CO2: 1598
16:16:32.041 -> 255 134 5 80 75 0 0 1 217
16:16:32.041 -> CO2: 1360
16:16:42.057 -> 255 134 3 204 75 0 0 1 95
16:16:42.092 -> CO2: 972
16:16:52.084 -> 255 134 3 191 75 0 0 1 108
16:16:52.084 -> CO2: 959
16:17:02.102 -> 255 134 2 230 75 0 0 1 70
16:17:02.102 -> CO2: 742
16:17:12.094 -> 255 134 3 106 75 0 0 1 193
16:17:12.129 -> CO2: 874
16:17:22.111 -> 255 134 2 227 75 0 0 1 73
16:17:22.145 -> CO2: 739
16:17:32.139 -> 255 134 3 225 75 0 0 1 74
16:17:32.172 -> CO2: 993
16:17:42.170 -> 255 134 3 109 75 0 0 1 190
16:17:42.204 -> CO2: 877
16:17:52.174 -> 255 134 2 188 75 0 0 1 112
16:17:52.207 -> CO2: 700
16:18:02.218 -> 255 134 2 70 75 0 0 1 230
16:18:02.253 -> CO2: 582
16:18:12.239 -> 255 134 2 163 75 0 0 1 137
16:18:12.239 -> CO2: 675
16:18:22.251 -> 255 134 2 110 75 0 0 1 190
16:18:22.285 -> CO2: 622
16:18:32.246 -> 255 134 2 83 75 0 0 1 217
16:18:32.280 -> CO2: 595
16:18:42.277 -> 255 134 2 48 75 0 0 1 252
16:18:42.312 -> CO2: 560
16:18:52.305 -> 255 134 2 62 75 0 0 1 238
16:18:52.339 -> CO2: 574
Bill of materials (prices as at Jan 2019)
- Arduino Uno R3 USD24.95 NZD54
- Grove Base shield V2 USD4.45 NZD14
- Grove – Carbon Dioxide Sensor(MH-Z16) USD89
After these tentative fixes for the MH-Z16 sensor I think going to see if there are any other libraries written by someone smarter than me available.