Skip to content

Commit

Permalink
mhz19b: add support for the MH-Z19B CO2 sensor (#179)
Browse files Browse the repository at this point in the history
* mhz19b: add support for the MH-Z19B CO2 sensor

* mhz19b: brought to repo standard, add docs, fix build on ESP-IDF < 4.4

* mhz19b: fix build for esp8266

* mhz19b: fix build for ESP8266

* mhz19b: fix build for ESP-IDF v < 4.0

* mhz19b: fix example

Co-authored-by: UncleRus <unclerus@gmail.com>
  • Loading branch information
douardda and UncleRus authored Apr 27, 2021
1 parent 8e3a39b commit 222aafa
Show file tree
Hide file tree
Showing 14 changed files with 702 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ See [GitHub examples](https://github.com/UncleRus/esp-idf-lib/tree/master/exampl
|----------------|-------------------------------------------------------------------------|---------|---------|---------------
| **sgp40** | SGP40 Indoor Air Quality Sensor for VOC Measurements | BSD | Yes | Yes
| **ccs811** | Driver for AMS CCS811 digital gas sensor | BSD | Yes | Yes
| **mhz19b** | Driver for MH-Z19B NDIR CO2 sensor | BSD | Yes | *No*

### ADC/DAC

Expand Down Expand Up @@ -234,3 +235,5 @@ See [GitHub examples](https://github.com/UncleRus/esp-idf-lib/tree/master/exampl
- [Lucio Tarantino](https://github.com/dianlight), developer of ADS111x driver
- [Julian Dörner](https://github.com/juliandoerner), developer of TSL2591 driver
- [FastLED community](https://github.com/FastLED), developers of `lib8tion`, `color` and `noise` libraries
- [Erriez](https://github.com/Erriez), developer of MH-Z19B driver
- [David Douard](https://github.com/douardda), developer of MH-Z19B driver
6 changes: 6 additions & 0 deletions components/mhz19b/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
set(COMPONENT_SRCDIRS .)
set(COMPONENT_ADD_INCLUDEDIRS .)

set(COMPONENT_REQUIRES log esp_idf_lib_helpers)

register_component()
26 changes: 26 additions & 0 deletions components/mhz19b/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Copyright (C) 2021 David Douard <david.douard@sdfa3.org>

Redistribution and use in source and binary forms, with or woithout
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of itscontributors
may be used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
62 changes: 62 additions & 0 deletions components/mhz19b/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Driver for the MH-Z19B NDIR CO2 sensor

This driver is heavily inspired from [Erriez MH-Z19B CO2 sensor library for
Arduino](https://github.com/Erriez/ErriezMHZ19B).

It uses an UART serial port to communicate with the sensor. Since the UART must
configured a specific way (9600 8N1), the `mhz19b_init()` takes care of configuring it.
It will however not start detecting/reading from the sensor. This must be done. Typical
usage would be:


```C
[...]
#include "esp_log.h"
#include "mhz19b.h"

#define MHZ19B_TX 12
#define MHZ19B_RX 13

void app_main(void)
{
int16_t co2;
mhz19b_dev_t dev;
char version[6];
uint16_t range;
bool autocal;

mhz19b_init(&dev, UART_NUM_1, MHZ19B_TX, MHZ19B_RX);

while (!mhz19b_detect(&dev))
{
ESP_LOGI(TAG, "MHZ-19B not detected, waiting...");
vTaskDelay(1000 / portTICK_RATE_MS);
}

mhz19b_get_version(&dev, version, 5);
ESP_LOGI(TAG, "MHZ-19B firmware version: %s", version);
ESP_LOGI(TAG, "MHZ-19B set range and autocal");

mhz19b_set_range(&dev, MHZ19B_RANGE_5000);
mhz19b_set_auto_calibration(&dev, false);

mhz19b_get_range(&dev, &range);
ESP_LOGI(TAG, " range: %d", range);

mhz19b_get_auto_calibration(&dev, &autocal);
ESP_LOGI(TAG, " autocal: %s", autocal ? "ON" : "OFF");

while (mhz19b_is_warming_up(&dev))
{
ESP_LOGI(TAG, "MHZ-19B is warming up");
vTaskDelay(1000 / portTICK_RATE_MS);
}

while (1) {
mhz19b_read_CO2(&dev, &co2);
ESP_LOGI(TAG, "CO2: %d", co2);
vTaskDelay(5000 / portTICK_RATE_MS);
}
}

```
2 changes: 2 additions & 0 deletions components/mhz19b/component.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
COMPONENT_ADD_INCLUDEDIRS = .
COMPONENT_DEPENDS = log
269 changes: 269 additions & 0 deletions components/mhz19b/mhz19b.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/**
* @file mhz19b.c
*
* ESP-IDF driver for MH-Z19B NDIR CO2 sensor connected to UART
*
* Inspired from https://github.com/Erriez/ErriezMHZ19B
*
* Copyright (C) 2020 Erriez <https://github.com/Erriez>
* Copyright (C) 2021 David Douard <david.douard@sdfa3.org>
*
* BSD Licensed as described in the file LICENSE
*/
#include <string.h>
#include <esp_idf_lib_helpers.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

#include "mhz19b.h"

static const char *TAG = "mhz19b";

#define CHECK(x) do { esp_err_t __; if ((__ = x) != ESP_OK) return __; } while (0)
#define CHECK_ARG(VAL) do { if (!(VAL)) return ESP_ERR_INVALID_ARG; } while (0)

esp_err_t mhz19b_init(mhz19b_dev_t *dev, uart_port_t uart_port, gpio_num_t tx_gpio, gpio_num_t rx_gpio)
{
CHECK_ARG(dev);

uart_config_t uart_config = {
.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)
.source_clk = UART_SCLK_APB,
#endif
};
CHECK(uart_driver_install(uart_port, MHZ19B_SERIAL_BUF_LEN * 2, 0, 0, NULL, 0));
CHECK(uart_param_config(uart_port, &uart_config));
#if HELPER_TARGET_IS_ESP32
CHECK(uart_set_pin(uart_port, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
#endif

dev->uart_port = uart_port;
// buffer for the incoming data
dev->buf = malloc(MHZ19B_SERIAL_BUF_LEN);
if (!dev->buf)
return ESP_ERR_NO_MEM;
dev->last_value = -1;
dev->last_ts = esp_timer_get_time();
return ESP_OK;
}

esp_err_t mhz19b_free(mhz19b_dev_t *dev)
{
CHECK_ARG(dev && dev->buf);

free(dev->buf);
dev->buf = NULL;
return ESP_OK;
}

bool mhz19b_detect(mhz19b_dev_t *dev)
{
CHECK_ARG(dev);

uint16_t range;
// Check valid PPM range
if ((mhz19b_get_range(dev, &range) == ESP_OK) && (range > 0))
return true;

// Sensor not detected, or invalid range returned
// Try recover by calling setRange(MHZ19B_RANGE_5000);
return false;
}

bool mhz19b_is_warming_up(mhz19b_dev_t *dev, bool smart_warming_up)
{
CHECK_ARG(dev);

// Wait at least 3 minutes after power-on
if (esp_timer_get_time() < MHZ19B_WARMING_UP_TIME_US)
{
if (smart_warming_up)
{
ESP_LOGI(TAG, "Using smart warming up detection ");

int16_t co2, last_co2;
last_co2 = dev->last_value;
// Sensor returns valid data after CPU reset and keep sensor powered
if (mhz19b_read_co2(dev, &co2) != ESP_OK)
return false;
if ((last_co2 != -1) && (last_co2 != co2))
// CO2 value changed since last read, no longer warming-up
return false;
}
// Warming-up
return true;
}

// Not warming-up
return false;
}

bool mhz19b_is_ready(mhz19b_dev_t *dev)
{
if (!dev) return false;

// Minimum CO2 read interval (Built-in LED flashes)
if ((esp_timer_get_time() - dev->last_ts) > MHZ19B_READ_INTERVAL_MS) {
return true;
}

return false;
}

esp_err_t mhz19b_read_co2(mhz19b_dev_t *dev, int16_t *co2)
{
CHECK_ARG(dev && co2);

// Send command "Read CO2 concentration"
CHECK(mhz19b_send_command(dev, MHZ19B_CMD_READ_CO2, 0, 0, 0, 0, 0));

// 16-bit CO2 value in response Bytes 2 and 3
*co2 = (dev->buf[2] << 8) | dev->buf[3];
dev->last_ts = esp_timer_get_time();
dev->last_value = *co2;

return ESP_OK;
}

esp_err_t mhz19b_get_version(mhz19b_dev_t *dev, char *version)
{
CHECK_ARG(dev && version);

// Clear version
memset(version, 0, 5);

// Send command "Read firmware version" (NOT DOCUMENTED)
CHECK(mhz19b_send_command(dev, MHZ19B_CMD_GET_VERSION, 0, 0, 0, 0, 0));

// Copy 4 ASCII characters to version array like "0443"
for (uint8_t i = 0; i < 4; i++) {
// Version in response Bytes 2..5
version[i] = dev->buf[i + 2];
}

return ESP_OK;
}

esp_err_t mhz19b_set_range(mhz19b_dev_t *dev, mhz19b_range_t range)
{
CHECK_ARG(dev);

// Send "Set range" command
return mhz19b_send_command(dev, MHZ19B_CMD_SET_RANGE,
0x00, 0x00, 0x00, (range >> 8), (range & 0xff));
}

esp_err_t mhz19b_get_range(mhz19b_dev_t *dev, uint16_t *range)
{
CHECK_ARG(dev && range);

// Send command "Read range" (NOT DOCUMENTED)
CHECK(mhz19b_send_command(dev, MHZ19B_CMD_GET_RANGE, 0, 0, 0, 0, 0));

// Range is in Bytes 4 and 5
*range = (dev->buf[4] << 8) | dev->buf[5];

// Check range according to documented specification
if ((*range != MHZ19B_RANGE_2000) && (*range != MHZ19B_RANGE_5000))
return ESP_ERR_INVALID_RESPONSE;

return ESP_OK;
}

esp_err_t mhz19b_set_auto_calibration(mhz19b_dev_t *dev, bool calibration_on)
{
CHECK_ARG(dev);

// Send command "Set Automatic Baseline Correction (ABC logic function)"
return mhz19b_send_command(dev, MHZ19B_CMD_SET_AUTO_CAL, (calibration_on ? 0xA0 : 0x00), 0, 0, 0, 0);
}

esp_err_t mhz19b_get_auto_calibration(mhz19b_dev_t *dev, bool *calibration_on)
{
CHECK_ARG(dev && calibration_on);

// Send command "Get Automatic Baseline Correction (ABC logic function)" (NOT DOCUMENTED)
CHECK(mhz19b_send_command(dev, MHZ19B_CMD_GET_AUTO_CAL, 0, 0, 0, 0, 0));

// Response is located in Byte 7: 0 = off, 1 = on
*calibration_on = dev->buf[7] & 0x01;

return ESP_OK;
}

esp_err_t mhz19b_start_calibration(mhz19b_dev_t *dev)
{
CHECK_ARG(dev);

// Send command "Zero Point Calibration"
return mhz19b_send_command(dev, MHZ19B_CMD_CAL_ZERO_POINT, 0, 0, 0, 0, 0);
}

esp_err_t mhz19b_send_command(mhz19b_dev_t *dev, uint8_t cmd, uint8_t b3, uint8_t b4, uint8_t b5, uint8_t b6, uint8_t b7)
{
CHECK_ARG(dev && dev->buf);

uint8_t txBuffer[MHZ19B_SERIAL_RX_BYTES] = { 0xFF, 0x01, cmd, b3, b4, b5, b6, b7, 0x00 };

// Check initialized
#if HELPER_TARGET_IS_ESP32 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0)
if (!uart_is_driver_installed(dev->uart_port))
return ESP_ERR_INVALID_STATE;
#endif

// Add CRC Byte
txBuffer[8] = mhz19b_calc_crc(txBuffer);

// Clear receive buffer
uart_flush(dev->uart_port);

// Write serial data
uart_write_bytes(dev->uart_port, (char *) txBuffer, sizeof(txBuffer));

// Clear receive buffer
memset(dev->buf, 0, MHZ19B_SERIAL_BUF_LEN);

// Read response from serial buffer
int len = uart_read_bytes(dev->uart_port, dev->buf,
MHZ19B_SERIAL_RX_BYTES,
MHZ19B_SERIAL_RX_TIMEOUT_MS / portTICK_RATE_MS);
if (len < 9)
return ESP_ERR_TIMEOUT;

// Check received Byte[0] == 0xFF and Byte[1] == transmit command
if ((dev->buf[0] != 0xFF) || (dev->buf[1] != cmd))
return ESP_ERR_INVALID_RESPONSE;

// Check received Byte[8] CRC
if (dev->buf[8] != mhz19b_calc_crc(dev->buf))
return ESP_ERR_INVALID_CRC;

// Return result
return ESP_OK;
}

// ----------------------------------------------------------------------------
// Private functions
// ----------------------------------------------------------------------------

uint8_t mhz19b_calc_crc(uint8_t *data)
{
uint8_t crc = 0;

// Calculate CRC on 8 data Bytes
for (uint8_t i = 1; i < 8; i++)
crc += data[i];

crc = 0xFF - crc;
crc++;

// Return calculated CRC
return crc;
}
Loading

0 comments on commit 222aafa

Please sign in to comment.