From 6331ee7c8f8134adee1e5708dbf8e502991f8467 Mon Sep 17 00:00:00 2001 From: arendst Date: Mon, 4 Dec 2017 17:25:06 +0100 Subject: [PATCH] Add experimental MH-Z19(B) support Add (experimental) support for sensor MH-Z19(B) to be enabled with define USE_MHZ19 in user_config.h (#561, #1248) --- sonoff/_releasenotes.ino | 3 +- sonoff/language/de-DE.h | 4 + sonoff/language/en-GB.h | 4 + sonoff/language/nl-NL.h | 4 + sonoff/language/pl-PL.h | 4 + sonoff/sonoff.ino | 12 +- sonoff/sonoff_template.h | 6 +- sonoff/support.ino | 15 +++ sonoff/user_config.h | 4 +- sonoff/xsns_15_mhz.ino | 277 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 319 insertions(+), 14 deletions(-) create mode 100644 sonoff/xsns_15_mhz.ino diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino index 85ba74766..76899d43a 100644 --- a/sonoff/_releasenotes.ino +++ b/sonoff/_releasenotes.ino @@ -1,6 +1,7 @@ /* 5.10.0a * Add (experimental) support for sensor SHT3x - * Add (experimental) support for iTead SI7021 temperature and humidity sensor (#735) + * Add (experimental) support for sensor MH-Z19(B) to be enabled with define USE_MHZ19 in user_config.h (#561, #1248) + * Add support for iTead SI7021 temperature and humidity sensor by consolidating DHT22 into AM2301 and using former DHT22 as SI7021 (#735) * Fix BME280 calculation (#1051) * Change ADS1115 default voltage range from +/-2V to +/-6V (#1289) * Add multipress support and more user configurable options to Sonoff Dual R2 (#1291) diff --git a/sonoff/language/de-DE.h b/sonoff/language/de-DE.h index 5ad6606fb..09d6da112 100644 --- a/sonoff/language/de-DE.h +++ b/sonoff/language/de-DE.h @@ -66,6 +66,7 @@ #define D_BUTTON "Knopf" #define D_BY "von" // Written by me #define D_CELSIUS "Celsius" +#define D_CO2 "CO2" #define D_CODE "code" // Button code #define D_COLDLIGHT "kalt" #define D_COMMAND "Befehl" @@ -473,6 +474,8 @@ #define D_SENSOR_PWM "PWM " // Suffix "1" #define D_SENSOR_COUNTER "Counter" // Suffix "1" #define D_SENSOR_IRRECV "IRRecv" +#define D_SENSOR_MHZ_RX "MHZ Rx" +#define D_SENSOR_MHZ_TX "MHZ Tx" #define D_SENSOR_SPI_CS "SPI CS" #define D_SENSOR_SPI_DC "SPI DC" #define D_SENSOR_BACKLIGHT "BLight" @@ -486,6 +489,7 @@ #define D_UNIT_MILLIAMPERE "mA" #define D_UNIT_MILLISECOND "ms" #define D_UNIT_MINUTE "min" +#define D_UNIT_PPM "ppm" #define D_UNIT_PRESSURE "hPa" #define D_UNIT_SECOND "sek" #define D_UNIT_SECTORS "Sektoren" diff --git a/sonoff/language/en-GB.h b/sonoff/language/en-GB.h index 517ab8ee3..c1a4378aa 100644 --- a/sonoff/language/en-GB.h +++ b/sonoff/language/en-GB.h @@ -66,6 +66,7 @@ #define D_BUTTON "Button" #define D_BY "by" // Written by me #define D_CELSIUS "Celsius" +#define D_CO2 "CO2" #define D_CODE "code" // Button code #define D_COLDLIGHT "Cold" #define D_COMMAND "Command" @@ -473,6 +474,8 @@ #define D_SENSOR_PWM "PWM" // Suffix "1" #define D_SENSOR_COUNTER "Counter" // Suffix "1" #define D_SENSOR_IRRECV "IRrecv" +#define D_SENSOR_MHZ_RX "MHZ Rx" +#define D_SENSOR_MHZ_TX "MHZ Tx" #define D_SENSOR_SPI_CS "SPI CS" #define D_SENSOR_SPI_DC "SPI DC" #define D_SENSOR_BACKLIGHT "BLight" @@ -486,6 +489,7 @@ #define D_UNIT_MILLIAMPERE "mA" #define D_UNIT_MILLISECOND "ms" #define D_UNIT_MINUTE "Min" +#define D_UNIT_PPM "ppm" #define D_UNIT_PRESSURE "hPa" #define D_UNIT_SECOND "sec" #define D_UNIT_SECTORS "sectors" diff --git a/sonoff/language/nl-NL.h b/sonoff/language/nl-NL.h index 4053e5210..6a6ea368e 100644 --- a/sonoff/language/nl-NL.h +++ b/sonoff/language/nl-NL.h @@ -66,6 +66,7 @@ #define D_BUTTON "DrukKnop" #define D_BY "door" // Written by me #define D_CELSIUS "Celsius" +#define D_CO2 "CO2" #define D_CODE "code" // Button code #define D_COLDLIGHT "Koud" #define D_COMMAND "Opdracht" @@ -472,6 +473,8 @@ #define D_SENSOR_LED "Led" // Suffix "1i" #define D_SENSOR_PWM "PWM" // Suffix "1" #define D_SENSOR_COUNTER "Teller" // Suffix "1" +#define D_SENSOR_MHZ_RX "MHZ Rx" +#define D_SENSOR_MHZ_TX "MHZ Tx" #define D_SENSOR_IRRECV "IRrecv" #define D_SENSOR_SPI_CS "SPI CS" #define D_SENSOR_SPI_DC "SPI DC" @@ -486,6 +489,7 @@ #define D_UNIT_MILLIAMPERE "mA" #define D_UNIT_MILLISECOND "ms" #define D_UNIT_MINUTE "Min" +#define D_UNIT_PPM "ppm" #define D_UNIT_PRESSURE "hPa" #define D_UNIT_SECOND "sec" #define D_UNIT_SECTORS "sectoren" diff --git a/sonoff/language/pl-PL.h b/sonoff/language/pl-PL.h index 71ccb7bcc..e53cc4962 100644 --- a/sonoff/language/pl-PL.h +++ b/sonoff/language/pl-PL.h @@ -66,6 +66,7 @@ #define D_BUTTON "Przycisk" #define D_BY "by" // Written by me #define D_CELSIUS "Celsiusza" +#define D_CO2 "CO2" #define D_CODE "kod" // Button code #define D_COLDLIGHT "Zimny" #define D_COMMAND "Komenda" @@ -473,6 +474,8 @@ #define D_SENSOR_PWM "PWM" // Suffix "1" #define D_SENSOR_COUNTER "Liczni" // Suffix "1" #define D_SENSOR_IRRECV "IRrecv" +#define D_SENSOR_MHZ_RX "MHZ Rx" +#define D_SENSOR_MHZ_TX "MHZ Tx" #define D_SENSOR_SPI_CS "SPI CS" #define D_SENSOR_SPI_DC "SPI DC" #define D_SENSOR_BACKLIGHT "BLight" @@ -486,6 +489,7 @@ #define D_UNIT_MILLIAMPERE "mA" #define D_UNIT_MILLISECOND "ms" #define D_UNIT_MINUTE "Min" +#define D_UNIT_PPM "ppm" #define D_UNIT_PRESSURE "hPa" #define D_UNIT_SECOND "sec" #define D_UNIT_SECTORS "sektory" diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index 0d1a7e377..78f662ea7 100644 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -2665,17 +2665,7 @@ void setup() GpioInit(); - if (Serial.baudRate() != baudrate) { - if (seriallog_level) { - snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_APPLICATION D_SET_BAUDRATE_TO " %d"), baudrate); - AddLog(LOG_LEVEL_INFO); - } - delay(100); - Serial.flush(); - Serial.begin(baudrate); - delay(10); - Serial.println(); - } + SetSerialBaudrate(baudrate); if (strstr(Settings.hostname, "%")) { strlcpy(Settings.hostname, WIFI_HOSTNAME, sizeof(Settings.hostname)); diff --git a/sonoff/sonoff_template.h b/sonoff/sonoff_template.h index a250795b2..53ae6bcdb 100644 --- a/sonoff/sonoff_template.h +++ b/sonoff/sonoff_template.h @@ -78,6 +78,8 @@ enum UserSelectablePins { GPIO_LED2_INV, GPIO_LED3_INV, GPIO_LED4_INV, + GPIO_MHZ_TXD, + GPIO_MHZ_RXD, GPIO_SENSOR_END }; // Text in webpage Module Parameters and commands GPIOS and GPIO @@ -137,7 +139,9 @@ const char kSensors[GPIO_SENSOR_END][9] PROGMEM = { D_SENSOR_LED "1i", D_SENSOR_LED "2i", D_SENSOR_LED "3i", - D_SENSOR_LED "4i" + D_SENSOR_LED "4i", + D_SENSOR_MHZ_TX, + D_SENSOR_MHZ_RX }; // Programmer selectable GPIO functionality offset by user selectable GPIOs diff --git a/sonoff/support.ino b/sonoff/support.ino index 30ba4748c..1c1300129 100644 --- a/sonoff/support.ino +++ b/sonoff/support.ino @@ -1287,6 +1287,21 @@ int GetCommandCode(char* destination, size_t destination_size, const char* needl return result; } +void SetSerialBaudrate(int baudrate) +{ + if (Serial.baudRate() != baudrate) { + if (seriallog_level) { + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_APPLICATION D_SET_BAUDRATE_TO " %d"), baudrate); + AddLog(LOG_LEVEL_INFO); + } + delay(100); + Serial.flush(); + Serial.begin(baudrate); + delay(10); + Serial.println(); + } +} + #ifndef USE_ADC_VCC /*********************************************************************************************\ * ADC support diff --git a/sonoff/user_config.h b/sonoff/user_config.h index d00c74f61..33f4345f7 100644 --- a/sonoff/user_config.h +++ b/sonoff/user_config.h @@ -186,7 +186,9 @@ #define USE_WS2812_CTYPE 1 // WS2812 Color type (0 - RGB, 1 - GRB, 2 - RGBW, 3 - GRBW) // #define USE_WS2812_DMA // DMA supports only GPIO03 (= Serial RXD) (+1k mem). When USE_WS2812_DMA is enabled expect Exceptions on Pow -#define USE_ARILUX_RF // Add code for Arilux RF remote controller (+0.8k code) +//#define USE_MHZ19 // Add support for MH-Z19 CO2 sensor using hardware serial interface at 9600 bps + +#define USE_ARILUX_RF // Add support for Arilux RF remote controller (+0.8k code) /*********************************************************************************************\ * Compile a minimal version if upgrade memory gets tight ONLY TO BE USED FOR UPGRADE STEP 1! diff --git a/sonoff/xsns_15_mhz.ino b/sonoff/xsns_15_mhz.ino new file mode 100644 index 000000000..594540b5a --- /dev/null +++ b/sonoff/xsns_15_mhz.ino @@ -0,0 +1,277 @@ +/* + xsns_15_mhz.ino - MH-Z19 CO2 sensor support for Sonoff-Tasmota + + Copyright (C) 2017 Theo Arends + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_MHZ19 +/*********************************************************************************************\ + * MH-Z19 - CO2 sensor + * + * Supported on hardware serial interface only due to lack of iram needed by SoftwareSerial + * + * Based on EspEasy plugin P049 by Dmitry (rel22 ___ inbox.ru) + * + ********************************************************************************************** + * Filter usage + * + * Select filter usage on low stability readings +\*********************************************************************************************/ + +enum Mhz19FilterOptions {MHZ19_FILTER_OFF, MHZ19_FILTER_OFF_ALLSAMPLES, MHZ19_FILTER_FAST, MHZ19_FILTER_MEDIUM, MHZ19_FILTER_SLOW}; + +#define MHZ19_FILTER_OPTION MHZ19_FILTER_FAST + +/*********************************************************************************************\ + * Source: http://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf + * + * Automatic Baseline Correction (ABC logic function) + * + * ABC logic function refers to that sensor itself do zero point judgment and automatic calibration procedure + * intelligently after a continuous operation period. The automatic calibration cycle is every 24 hours after powered on. + * + * The zero point of automatic calibration is 400ppm. + * + * This function is usually suitable for indoor air quality monitor such as offices, schools and homes, + * not suitable for greenhouse, farm and refrigeratory where this function should be off. + * + * Please do zero calibration timely, such as manual or commend calibration. +\*********************************************************************************************/ + +#define MHZ19_ABC_ENABLE 1 // Automatic Baseline Correction (0 = off, 1 = on (default)) + +/*********************************************************************************************/ + +#define MHZ19_BAUDRATE 9600 +#define MHZ19_READ_TIMEOUT 600 // Must be way less than 1000 + +const char kMhz19Types[] PROGMEM = "MHZ19|MHZ19B"; + +const byte mhz19_cmnd_read_ppm[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; +const byte mhz19_cmnd_abc_enable[9] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}; +const byte mhz19_cmnd_abc_disable[9] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}; + +uint8_t mhz19_type = 0; +uint16_t mhz19_last_ppm = 0; +uint8_t mhz19_filter = MHZ19_FILTER_OPTION; +byte mhz19_response[9]; +bool mhz19_abc_enable = MHZ19_ABC_ENABLE; +bool mhz19_abc_must_apply = false; +char mhz19_types[7]; + +bool Mhz19CheckAndApplyFilter(uint16_t ppm, uint8_t s) +{ + if (1 == s) { + return false; // S==1 => "A" version sensor bootup, do not use values. + } + if (mhz19_last_ppm < 400 || mhz19_last_ppm > 5000) { + // Prevent unrealistic values during start-up with filtering enabled. + // Just assume the entered value is correct. + mhz19_last_ppm = ppm; + return true; + } + int32_t difference = ppm - mhz19_last_ppm; + if (s > 0 && s < 64 && mhz19_filter != MHZ19_FILTER_OFF) { + // Not the "B" version of the sensor, S value is used. + // S==0 => "B" version, else "A" version + // The S value is an indication of the stability of the reading. + // S == 64 represents a stable reading and any lower value indicates (unusual) fast change. + // Now we increase the delay filter for low values of S and increase response time when the + // value is more stable. + // This will make the reading useful in more turbulent environments, + // where the sensor would report more rapid change of measured values. + difference = difference * s; + difference /= 64; + } + switch (mhz19_filter) { + case MHZ19_FILTER_OFF: { + if (s != 0 && s != 64) { + return false; + } + break; + } + // #Samples to reach >= 75% of step response + case MHZ19_FILTER_OFF_ALLSAMPLES: + break; // No Delay + case MHZ19_FILTER_FAST: + difference /= 2; + break; // Delay: 2 samples + case MHZ19_FILTER_MEDIUM: + difference /= 4; + break; // Delay: 5 samples + case MHZ19_FILTER_SLOW: + difference /= 8; + break; // Delay: 11 samples + } + mhz19_last_ppm = static_cast(mhz19_last_ppm + difference); + return true; +} + +bool Mhz19Read(uint16_t &p, float &t) +{ + bool status = false; + + p = 0; + t = NAN; + + if (mhz19_type) + { + Serial.flush(); + if (Serial.write(mhz19_cmnd_read_ppm, 9) != 9) { + return false; // Unable to send 9 bytes + } + memset(mhz19_response, 0, sizeof(mhz19_response)); + uint32_t start = millis(); + uint8_t counter = 0; + while (((millis() - start) < MHZ19_READ_TIMEOUT) && (counter < 9)) { + if (Serial.available() > 0) { + mhz19_response[counter++] = Serial.read(); + } else { + delay(10); + } + } + if (counter < 9){ + return false; // Timeout while trying to read + } + + byte crc = 0; + for (uint8_t i = 1; i < 8; i++) { + crc += mhz19_response[i]; + } + crc = 255 - crc; + crc++; + +/* + // Test data + mhz19_response[0] = 0xFF; + mhz19_response[1] = 0x86; + mhz19_response[2] = 0x12; + mhz19_response[3] = 0x86; + mhz19_response[4] = 64; +// mhz19_response[5] = 32; + mhz19_response[8] = crc; +*/ + + if (0xFF == mhz19_response[0] && 0x86 == mhz19_response[1] && mhz19_response[8] == crc) { + uint16_t u = (mhz19_response[6] << 8) | mhz19_response[7]; + if (15000 == u) { // During (and only ever at) sensor boot, 'u' is reported as 15000 + if (!mhz19_abc_enable) { + // After bootup of the sensor the ABC will be enabled. + // Thus only actively disable after bootup. + mhz19_abc_must_apply = true; + } + } else { + uint16_t ppm = (mhz19_response[2] << 8) | mhz19_response[3]; + t = ConvertTemp((float)mhz19_response[4] - 40); + uint8_t s = mhz19_response[5]; + if (s) { + mhz19_type = 1; + } else { + mhz19_type = 2; + } + if (Mhz19CheckAndApplyFilter(ppm, s)) { + p = mhz19_last_ppm; + + if (0 == s || 64 == s) { // Reading is stable. + if (mhz19_abc_must_apply) { + mhz19_abc_must_apply = false; + if (mhz19_abc_enable) { + Serial.write(mhz19_cmnd_abc_enable, 9); // Sent sensor ABC Enable + } else { + Serial.write(mhz19_cmnd_abc_disable, 9); // Sent sensor ABC Disable + } + } + } + + status = true; + } + } + } + } + return status; +} + +void Mhz19Init() +{ + SetSerialBaudrate(MHZ19_BAUDRATE); + Serial.flush(); + + seriallog_level = 0; + mhz19_type = 1; +} + +#ifdef USE_WEBSERVER +const char HTTP_SNS_CO2[] PROGMEM = + "%s{s}%s " D_CO2 "{m}%d " D_UNIT_PPM "{e}"; // {s} = , {m} = , {e} = +#endif // USE_WEBSERVER + +void Mhz19Show(boolean json) +{ + uint16_t co2; + float t; + + if (Mhz19Read(co2, t)) { + char temperature[10]; + dtostrfd(t, Settings.flag2.temperature_resolution, temperature); + GetTextIndexed(mhz19_types, sizeof(mhz19_types), mhz19_type -1, kMhz19Types); + + if (json) { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,\"%s\":{\"" D_CO2 "\":%d,\"" D_TEMPERATURE "\":%s}"), mqtt_data, mhz19_types, co2, temperature); +#ifdef USE_DOMOTICZ + DomoticzSensor(DZ_COUNT, co2); +#endif // USE_DOMOTICZ +#ifdef USE_WEBSERVER + } else { + snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_CO2, mqtt_data, mhz19_types, co2); + snprintf_P(mqtt_data, sizeof(mqtt_data), HTTP_SNS_TEMP, mqtt_data, mhz19_types, temperature, TempUnit()); +#endif // USE_WEBSERVER + } + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +#define XSNS_15 + +boolean Xsns15(byte function) +{ + boolean result = false; + + if ((pin[GPIO_MHZ_RXD] < 99) && (pin[GPIO_MHZ_TXD] < 99)) { + switch (function) { + case FUNC_XSNS_INIT: + Mhz19Init(); + break; + case FUNC_XSNS_PREP: +// Mhz19Prep(); + break; + case FUNC_XSNS_JSON_APPEND: + Mhz19Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_XSNS_WEB: + Mhz19Show(0); +// Mhz19Prep(); + break; +#endif // USE_WEBSERVER + } + } + return result; +} + +#endif // USE_MHZ19