MH-Z19-like NDIR CO2 Sensor HC8 Found And Explored

While on the search for an alternative to directly buying the fairly expensive MH-Z19 CO2 sensor, [spezifisch] came across a ‘BreeRainz’ branded gadget (also found under other brands) that claimed to use an NDIR (Non-Dispersive Infrared) sensor for measuring CO2 levels, while costing only €25. This type of sensor allows for CO2 levels to be measured directly, rather than inferred, making them significantly more precise.

The BreeRainz DM1308A device cracked open.
The BreeRainz DM1308A device cracked open.

After cracking the gadget open (literally, due to the hidden screws), the CO2 sensor is clearly visible. While superficially identical to an MH-Z19, the NDIR sensor is actually called ‘HC8’, is produced by 广州海谷电子科技有限公司 (Guangzhou Haigu Electronic Technology Co., Ltd.). While being pin-compatible with the MH-Z19, its UART protocol is not the same. Fortunately there is a datasheet to help with implementing it, which is what [spezifisch] did.

This raises the question of whether harvesting NDIR CO2 sensors like this is worth it to save a few Euros. A quick look on German Amazon shows that the device in question currently costs €35, while a genuine MH-Z19 can be bought for €25 or less. There are also many MH-Z19 models (B, C and D), which cover an even wider price range. All of which points to finding an NDIR sensor-containing device can be interesting when it’s on sale, but if all you care about is the sensor itself, it’s probably best to just buy them directly.

7 thoughts on “MH-Z19-like NDIR CO2 Sensor HC8 Found And Explored

  1. I still have some trouble finding hidden screws, but over the decades I’ve learned a few of the tricks that are implemented. But, I’m still surprised to read that someone doesn’t know about them.

  2. He found the mysterious device specs by googling the command’s byte sequence “64 69 03 5E 4E” that he sniffed on its RX line ! Five bytes and Google gets it all, bang! This era is amazing.

  3. /**
    HC8 sensor code for arduino Uno. I wrote this with help from OG python script,
    from translated chinese docs, with some help from GPT.
    By default in active output mode, sensor will produce CO2 value every second.
    This script will store last 60 values and each minute will calculate the average.

    Data format in “active output” mode:

    The output format is 16BYTE.
    Data header: BYTE0 = 0X42; BYTE1=4D
    BYTE6 data is high, BYTE7 data is low, indicating CO2 concentration.
    BYTE15, data checksum. BYTE15= BYTE0+BYTE1+…….+BYTE13;

    Example: 42 4D 0C 51 09 A2 07 2B 01 35 05 81 20 08 20 AD;
    CO2 concentration = BYTE6 X 256 + BYTE7 = 07X256 + 2B = 1853;


    // Define the CO2 sensor serial interface
    SoftwareSerial sensorSerial(2, 3); // RX, TX

    // Define the number of bytes in a data packet
    #define DATA_PACKET_SIZE 16

    // Define the size of the CO2 buffer in “element-seconds”
    // The CO2 sensor sends data every second,
    // and the CO2_BUFFER_SIZE determines the number of measurements stored.
    // It is also used as the interval (in seconds) for printing the average value.
    #define CO2_BUFFER_SIZE 60
    unsigned long intervalPrintTime = CO2_BUFFER_SIZE * 1000UL;

    // Initialize an array to store CO2 values
    uint16_t co2Values[CO2_BUFFER_SIZE];
    // Variable to keep track of the current index in the buffer
    int co2Index = 0;
    // Variable to keep track of the number of valid measurements in the buffer
    int validMeasurements = 0;
    // Variable to keep track of the last print time
    unsigned long lastPrintTime = 0;

    void setup() {
    // Start the serial communication with the CO2 sensor
    Serial.begin(9600); // You can change this baud rate based on your needs

    void printAverage() {
    // Calculate the average of the valid CO2 values in the buffer
    uint32_t sum = 0;
    for (int i = 0; i < validMeasurements; i++) {
    sum += co2Values[i];
    uint16_t average = static_cast(sum / validMeasurements);

    // Print the average CO2 concentration
    Serial.print(“Average CO2 Concentration: “);

    void loop() {
    if (sensorSerial.available() >= DATA_PACKET_SIZE) {
    // Read the data packet from the CO2 sensor
    uint8_t dataPacket[DATA_PACKET_SIZE];
    sensorSerial.readBytes(dataPacket, DATA_PACKET_SIZE);

    // Check if the data packet has a valid header
    if (dataPacket[0] == 0x42 && dataPacket[1] == 0x4D) {
    // Extract CO2 concentration from the data packet
    uint16_t co2Concentration = (dataPacket[6] << 8) | dataPacket[7];

    // Verify the checksum
    uint8_t checksum = 0;
    for (int i = 0; i = intervalPrintTime) || (millis() < lastPrintTime)) {
    lastPrintTime = millis();
    } else {
    // Checksum error
    Serial.println("Checksum error!");
    } else {
    // Invalid header
    Serial.println("Invalid data packet header!");

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

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