-
Notifications
You must be signed in to change notification settings - Fork 443
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mhz19b: add support for the MH-Z19B CO2 sensor (#179)
* 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
Showing
14 changed files
with
702 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
COMPONENT_ADD_INCLUDEDIRS = . | ||
COMPONENT_DEPENDS = log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.