From 1ef08e15f1513c43da34a840cd56c05e5d34999e Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 26 Jan 2022 14:25:10 +0300 Subject: [PATCH] Squashed commit of the following: commit 5563b50abdd70806099248bea875be63b1a8acbb Author: Max Date: Wed Jan 26 12:03:21 2022 +0300 Update xsns_95_cm1107.ino commit fbcfccb9732b3b47c7e5f2554e9d9f5765aad719 Merge: a7a792ff0 62458e367 Author: Max Date: Wed Jan 26 09:44:09 2022 +0300 Merge pull request #3 from arendst/development Add command ``SspmEnergyTotal`` commit a7a792ff026872736aad991aa23702fc53e278a2 Merge: a4199127a d7664c02a Author: Max Date: Tue Jan 25 18:48:12 2022 +0300 Merge remote-tracking branch 'upstream/development' into CM11_sensor commit a4199127a178265c0eefc08a07c41716ce72f7d3 Author: Max Date: Tue Jan 25 18:38:35 2022 +0300 CM11 commit ff0c88badc83ea789b217b5d400d0660573fe64c Author: Max Date: Tue Jan 25 18:37:23 2022 +0300 Create xsns_95_cm1107.ino --- tasmota/language/af_AF.h | 2 + tasmota/language/en_GB.h | 2 + tasmota/support_features.ino | 3 + tasmota/tasmota_template.h | 8 +- tasmota/tasmota_template_legacy.h | 4 + tasmota/xsns_95_cm1107.ino | 465 ++++++++++++++++++++++++++++++ tools/lv_gpio/lv_gpio_enum.h | 2 + 7 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 tasmota/xsns_95_cm1107.ino diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h index 4b72c6837..60460a97a 100644 --- a/tasmota/language/af_AF.h +++ b/tasmota/language/af_AF.h @@ -852,6 +852,8 @@ #define D_GPIO_SHIFT595_RCLK "74x595 RCLK" #define D_GPIO_SHIFT595_OE "74x595 OE" #define D_GPIO_SHIFT595_SER "74x595 SER" +#define D_SENSOR_CM11_TX "CM110x TX" +#define D_SENSOR_CM11_RX "CM110x RX" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index 45a0ca3ca..71bd64bac 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -852,6 +852,8 @@ #define D_GPIO_SHIFT595_RCLK "74x595 RCLK" #define D_GPIO_SHIFT595_OE "74x595 OE" #define D_GPIO_SHIFT595_SER "74x595 SER" +#define D_SENSOR_CM11_TX "CM110x TX" +#define D_SENSOR_CM11_RX "CM110x RX" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/support_features.ino b/tasmota/support_features.ino index d9bbd259b..9552f25e8 100644 --- a/tasmota/support_features.ino +++ b/tasmota/support_features.ino @@ -788,6 +788,9 @@ void ResponseAppendFeatures(void) #ifdef USE_SDM230 feature8 |= 0x00100000; // xnrg_21_sdm230.ino #endif +#ifdef USE_CM1107 + feature8 |= 0x00200000; // xsns_95_cm1107.ino +#endif // feature8 |= 0x00200000; // feature8 |= 0x00400000; // feature8 |= 0x00800000; diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 86efb5c41..6a658521a 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -181,6 +181,7 @@ enum UserSelectablePins { GPIO_OPTION_E, // Emulated module GPIO_SDM230_TX, GPIO_SDM230_RX, // SDM230 Serial interface GPIO_ADC_MQ, // Analog MQ Sensor + GPIO_CM11_TXD, GPIO_CM11_RXD, // CM11 Serial interface GPIO_SENSOR_END }; enum ProgramSelectablePins { @@ -400,7 +401,8 @@ const char kSensorNames[] PROGMEM = D_SENSOR_SOLAXX1_RTS "|" D_SENSOR_OPTION " E|" D_SENSOR_SDM230_TX "|" D_SENSOR_SDM230_RX "|" - D_SENSOR_ADC_MQ + D_SENSOR_ADC_MQ "|" + D_SENSOR_CM11_TX "|" D_SENSOR_CM11_RX "|" ; const char kSensorNamesFixed[] PROGMEM = @@ -934,6 +936,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_MAX7219CS), #endif // USE_DISPLAY_MAX7219 +#ifdef USE_CM1107 + AGPIO(GPIO_CM11_TXD), // MH-Z19 Serial interface + AGPIO(GPIO_CM11_RXD), // MH-Z19 Serial interface +#endif /*-------------------------------------------------------------------------------------------*\ * ESP32 specifics \*-------------------------------------------------------------------------------------------*/ diff --git a/tasmota/tasmota_template_legacy.h b/tasmota/tasmota_template_legacy.h index 4583a9783..14d0c438b 100644 --- a/tasmota/tasmota_template_legacy.h +++ b/tasmota/tasmota_template_legacy.h @@ -87,6 +87,8 @@ enum LegacyUserSelectablePins { GPI8_LED4_INV, GPI8_MHZ_TXD, // MH-Z19 Serial interface GPI8_MHZ_RXD, // MH-Z19 Serial interface + GPI8_CM11_TXD, // MH-Z19 Serial interface + GPI8_CM11_RXD, // MH-Z19 Serial interface GPI8_PZEM0XX_TX, // PZEM0XX Serial interface GPI8_PZEM004_RX, // PZEM004T Serial interface GPI8_SAIR_TX, // SenseAir Serial interface @@ -317,6 +319,8 @@ const uint16_t kGpioConvert[] PROGMEM = { AGPIO(GPIO_LED1_INV) +3, AGPIO(GPIO_MHZ_TXD), // MH-Z19 Serial interface AGPIO(GPIO_MHZ_RXD), + AGPIO(GPIO_CM11_TXD), // MH-Z19 Serial interface + AGPIO(GPIO_CM11_RXD), AGPIO(GPIO_PZEM0XX_TX), // PZEM0XX Serial interface AGPIO(GPIO_PZEM004_RX), // PZEM004T Serial interface AGPIO(GPIO_SAIR_TX), // SenseAir Serial interface diff --git a/tasmota/xsns_95_cm1107.ino b/tasmota/xsns_95_cm1107.ino new file mode 100644 index 000000000..d5e2ffe72 --- /dev/null +++ b/tasmota/xsns_95_cm1107.ino @@ -0,0 +1,465 @@ +/* + XSNS_95_cm1107.ino - CM1107(B) CO2 sensor support for Tasmota + + Copyright (C) 2021 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_CM1107 +/*********************************************************************************************\ + * CM11xx - CO2 sensor + * https://en.gassensor.com.cn/CO2Sensor/list.html + * Adapted from Mhz19 plugin by Maksim (rekin.m ___ gmail.com) + * + * Hardware Serial will be selected if GPIO1 = [CM11 Rx] and GPIO3 = [CM11 Tx] + ********************************************************************************************** + * Filter usage + * + * Select filter usage on low stability readings + * + * ******************************************************************************************* + * Some CM11 models has manual or continuos modes - this logic not implemented. +\*********************************************************************************************/ + +#define XSNS_95 95 + +enum CM11FilterOptions {CM1107_FILTER_OFF, CM1107_FILTER_FAST, CM1107_FILTER_MEDIUM, CM1107_FILTER_MEDIUM2, CM1107_FILTER_SLOW}; + +#ifndef CM1107_FILTER_OPTION + #define CM1107_FILTER_OPTION CM1107_FILTER_FAST +#endif +/*********************************************************************************************\ + * Source: https://en.gassensor.com.cn/CO2Sensor/list.html (pdf for 1106/1107/1109 sensors) + * + * + * Automatic Baseline Correction (ABC logic function) is enabled by default but may be disabled with command + * Sensor95 0 + * and enabled again with command + * Sensor95 1 + * + * 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 first 24 hours and 7 days cycle 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 command calibration. +\*********************************************************************************************/ + +#include + +#ifndef CO2_LOW +#define CO2_LOW 800 // Below this CO2 value show green light +#endif +#ifndef CO2_HIGH +#define CO2_HIGH 1200 // Above this CO2 value show red light +#endif + +#define CM1107_READ_TIMEOUT 400 // Must be way less than 1000 but enough to read 16 bytes at 9600 bps +#define CM1107_RETRY_COUNT 8 + +TasmotaSerial *CM11Serial; + + +const char CM11_ABC_ENABLED[] = "ABC is Enabled"; +const char CM11_ABC_DISABLED[] = "ABC is Disabled"; + +//First [0] element - lenght of cmd and data +const uint8_t cmd_read[] = {0x01,0x01}; // cm11_cmnd_read_ppm +uint8_t cmd_abc_enable[] = {0x07,0x10,0x64,0x00,0x07,0x01,0x90,0x64}; // cm11_cmnd_abc_enable. Not const because can be modified +const uint8_t cmd_abc_disable[] = {0x07,0x10,0x64,0x02,0x07,0x01,0x90,0x64}; // cm11_cmnd_abc_disable +const uint8_t cmd_zeropoint[] = {0x03,0x03,0x01,0x90}; // cm11_cmnd_zeropoint_400 +const uint8_t cmd_serial[] = {0x01,0x1F}; // cm11_cmnd_read_serial +const uint8_t cmd_sw_version[] = {0x01,0x1E}; // cm11_cmnd_read_sw_version + + +enum CM11Commands { CM11_CMND_READPPM, CM11_CMND_ABCENABLE, CM11_CMND_ABCDISABLE, CM11_CMND_ZEROPOINT, CM11_CMND_SERIAL,CM11_CMND_SW_VERSION }; +const uint8_t* kCM11Commands[] PROGMEM = { + cmd_read, + cmd_abc_enable, + cmd_abc_disable, + cmd_zeropoint, + cmd_serial, + cmd_sw_version +}; + + +uint8_t cm11_type = 1; +uint16_t cm11_last_ppm = 0; +uint8_t cm11_filter = CM1107_FILTER_OPTION; +bool cm11_abc_must_apply = false; + +float cm11_temperature = 0; +uint16_t cm11_humidity = 0; +char cm11_sw_version[30] = {0}; +char cm11_serial_number[21] = {0}; +uint8_t cm11_retry = CM1107_RETRY_COUNT; +uint8_t cm11_received = 0; +uint8_t cm11_state = 0; +uint16_t ppm_low_limit = 0; +uint16_t ppm_high_limit = 5000; + +/*********************************************************************************************/ +//256-(HEAD+LEN+CMD+DATA)%256 +uint8_t CM11CalculateChecksum(uint8_t *array,uint8_t start, uint8_t len) +{ + uint8_t checksum = 0; + for (uint8_t i = start; i < len; i++) { + checksum += array[i]; + } + checksum = checksum%256; + checksum = 255 - checksum; + return (checksum +1); +} + +size_t CM11SendCmd(uint8_t command_id) +{ + uint8_t len =kCM11Commands[command_id][0]; + uint8_t cm11_send[len+3];// = {0}; //Fix length + memset( cm11_send, 0, (len+3)*sizeof(uint8_t) ); + + cm11_send[0] = 0x11; // Start byte, fixed + + memcpy_P(&cm11_send[1], kCM11Commands[command_id], (len+1) * sizeof(uint8_t)); + + cm11_send[len+2] = CM11CalculateChecksum(cm11_send,0, len+2); + +#ifdef DEBUG_TASMOTA_SENSOR + char cmdFull[len+30];// = {0}; + memset( cmdFull, 0, (len+3)*sizeof(char) ); + for(int i=0, j=0;iwrite(cm11_send, sizeof(cm11_send)); +} + +/*********************************************************************************************/ + +bool CM11CheckAndApplyFilter(uint16_t ppm, uint8_t drift) +{ +#ifdef DEBUG_TASMOTA_SENSOR + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM11 ppm: %u, last ppm: %u"),ppm, cm11_last_ppm); +#endif //DEBUG_TASMOTA_SENSOR + if (cm11_last_ppm < ppm_low_limit || cm11_last_ppm > ppm_high_limit) { + // Prevent unrealistic values during start-up with filtering enabled. + // Just assume the entered value is correct. + cm11_last_ppm = ppm; + return true; + } + int32_t difference = ppm - cm11_last_ppm; + if (drift > 0 && cm11_filter != CM1107_FILTER_OFF) { + difference >>= CM1107_FILTER_SLOW; // If drifting values -> apply slow filter + }else if (CM1107_FILTER_OFF == cm11_filter) { + if (drift != 0 ) { + return false; //Do not alarm on such unstable values + } + }else { + difference >>=cm11_filter; + } + + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM11 diff: %d"),difference); + cm11_last_ppm = static_cast(cm11_last_ppm + difference); + return true; +} + +void CM11EverySecond(void) +{ + cm11_state++; + //If more than one command was send + //Reading preffered + if (CM11Serial->available() > 0){ + cm11_received = 0; + } + + if ((8 == cm11_state && cm11_received) || 16 == cm11_state) { // Every 8 sec start a CM11 measuring cycle (which takes 1005 +5% ms) + cm11_state = 0; + + if (cm11_retry) { + cm11_retry--; + if (!cm11_retry) { + cm11_last_ppm = 0; + cm11_temperature = 0; + cm11_humidity = 0; + } + } + + CM11Serial->flush(); // Sync reception + CM11SendCmd(CM11_CMND_READPPM); + cm11_received = 0; + } + + if ((cm11_state > 2) && !cm11_received) { // Start reading response after 3 seconds every second until received + uint8_t cm11_response[50]; + unsigned long start = millis(); + uint8_t counter = 0; + uint8_t resp_len = 50; + while (((millis() - start) < CM1107_READ_TIMEOUT) && (counter < resp_len)) { + if (CM11Serial->available() > 0) { + cm11_response[counter++] = CM11Serial->read(); + if (counter ==2 && cm11_response[0] == 0x16) { + resp_len = cm11_response[1] +3 ; + } + } else { + delay(5); + } + } + + if (counter < 5) { + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM1107 timeout (command sent, no responce")); + return; + } + + uint8_t crc = CM11CalculateChecksum(cm11_response,0, cm11_response[1]+2); + if (cm11_response[cm11_response[1]+2] != crc) { + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM1107 crc error")); + return; + } + if (0x16 != cm11_response[0]) { + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM1107 bad response")); + return; + } + + cm11_received = 1; + + if (cm11_response[2]==cmd_read[1]){ //0x01 - read command + uint16_t ppm = (cm11_response[3] << 8) | cm11_response[4]; + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM11 PPM: %u"),ppm); + if (ppm ==550) { // Preheating mode, fixed value. + //DOCs says that preheating is cm11_response[5] & (1 << 0)) ==1 (first bit ==1), but mine sensor (CM1107, sw V1.07.0.02 ) + // set first bit 0 when preheating at switch to 1 then finished. + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM11 preheating")); + if (Settings->SensorBits1.mhz19b_abc_disable) { + // After bootup of the sensor the ABC will be enabled. + // Thus only actively disable after bootup. + cm11_abc_must_apply = true; + } + return; + } + if(cm11_response[1] ==13) { // CM1107T with temperature and humidity + cm11_temperature = (float)(((cm11_response[7] << 8) | cm11_response[8]) - 4685)/100.0f; + cm11_humidity = (((cm11_response[9] << 8) | cm11_response[10]) - 600)/100; + cm11_type = 2; + } + uint8_t cm11_drift = (cm11_response[5] & (1 << 7)) ? 1:0; + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_DEBUG "CM11 flags DF3: %02x"),cm11_response[5]); + + if (CM11CheckAndApplyFilter(ppm,cm11_drift)) { + cm11_retry = CM1107_RETRY_COUNT; +#ifdef USE_LIGHT + LightSetSignal(CO2_LOW, CO2_HIGH, cm11_last_ppm); +#endif // USE_LIGHT + + if (!cm11_drift) { // Measuring is stable. + if (cm11_abc_must_apply) { + cm11_abc_must_apply = false; + if (!Settings->SensorBits1.mhz19b_abc_disable) { + CM11SendCmd(CM11_CMND_ABCENABLE); + } else { + CM11SendCmd(CM11_CMND_ABCDISABLE); + } + } + } + + } + } + + if (cm11_response[2]==cmd_sw_version[1]){ //0x1E - read SW version + memcpy_P(cm11_sw_version, &cm11_response[3], cm11_response[1] * sizeof(uint8_t)); + AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_DEBUG "SW version: %s"),cm11_sw_version); + } + + if (cm11_response[2]==cmd_serial[1]){ //0x1F - read serial + // Serial num coded as 5 integers 0..9999. Each integer is uint16_t size + for (uint8_t i=0; iSensorBits1.mhz19b_abc_disable = true; + CM11SendCmd(CM11_CMND_ABCDISABLE); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, CM11_ABC_DISABLED); + break; + case 1: + Settings->SensorBits1.mhz19b_abc_disable = false; + CM11SendCmd(CM11_CMND_ABCENABLE); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, CM11_ABC_ENABLED); + break; + case 2: + CM11SendCmd(CM11_CMND_ZEROPOINT); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, D_JSON_ZERO_POINT_CALIBRATION); + break; + case 3: + CM11SendCmd(CM11_CMND_SW_VERSION); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, "CM11 sw version"); + break; + case 4: + CM11SendCmd(CM11_CMND_SERIAL); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, "CM11 serial number"); + break; + + default: + // Set ppm limits: 5,, + // ABS period cmd(with enabling ABS): 1,[1..30] + uint32_t parm[3] = { 0 }; + ParseParameters(3, parm); + switch (parm[0]) { + case 1: + if (parm[1]>=1 && parm[1]<=30){ + cmd_abc_enable[4] = parm[1]; //set uint8 from uint32 *o*, but value limited by 30 + Settings->SensorBits1.mhz19b_abc_disable = false; + CM11SendCmd(CM11_CMND_ABCENABLE); + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, CM11_ABC_ENABLED); + } else { + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, "Valid period value: [1..30]"); + } + break; + // Set sensor ppm limit. Default 0..5000, but some sensors has another range. + case 5: + if(parm[1]>=0 && parm[1] <=10000 && parm[2]>=0 && parm[2] <=10000 && parm[1]=0 && parm[1]<=4) { + cm11_filter = parm[1]; + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, "CM11 set filter mode"); + } + else { + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, "Invalid filter mode: [0..4]. 0 - Off, 1 (Fast) -> 4 (Slow)"); + } + break; + default: + Response_P(S_JSON_SENSOR_INDEX_SVALUE, XSNS_95, "Unknown command"); + break; + } + + } + + return serviced; +} + +/*********************************************************************************************/ + +void CM11Init(void) +{ + cm11_type = 0; + if (PinUsed(GPIO_CM11_RXD) && PinUsed(GPIO_CM11_TXD)) { + CM11Serial = new TasmotaSerial(Pin(GPIO_CM11_RXD), Pin(GPIO_CM11_TXD), 1); + if (CM11Serial->begin(9600)) { + if (CM11Serial->hardwareSerial()) { ClaimSerial(); } + cm11_type = 1; + CM11SendCmd(CM11_CMND_SW_VERSION); + } + } +} + +void CM11Show(bool json) +{ + if (json) { + ResponseAppend_P(PSTR(",\"CM11\":{\"" D_JSON_CO2 "\":%d,\"" D_JSON_VERSION "\":\"%s\",\"Serial number\":\"%s\""), + cm11_last_ppm, cm11_sw_version, cm11_serial_number); + if(cm11_type == 2) { // With temp and humidity + ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%*_f"), + Settings->flag2.temperature_resolution, &cm11_temperature); + } + ResponseAppend_P(PSTR("}")); +#ifdef USE_DOMOTICZ + if (0 == TasmotaGlobal.tele_period) { + DomoticzSensor(DZ_AIRQUALITY, cm11_last_ppm); + if(cm11_type == 2) { // With temp and humidity + DomoticzFloatSensor(DZ_TEMP, cm11_temperature); + } + } +#endif // USE_DOMOTICZ +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(HTTP_SNS_CO2, "CM11", cm11_last_ppm); + if(cm11_type == 2) { // With temp and humidity + WSContentSend_Temp("CM11", cm11_temperature); + } +#endif // USE_WEBSERVER + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns95(uint8_t function) +{ + bool result = false; + + if (cm11_type) { + switch (function) { + case FUNC_INIT: + CM11Init(); + break; + case FUNC_EVERY_SECOND: + CM11EverySecond(); + break; + case FUNC_COMMAND_SENSOR: + if (XSNS_95 == XdrvMailbox.index) { + result = CM11CommandSensor(); + } + break; + case FUNC_JSON_APPEND: + CM11Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + CM11Show(0); + break; +#endif // USE_WEBSERVER + } + } + return result; +} + +#endif // USE_CM1107 diff --git a/tools/lv_gpio/lv_gpio_enum.h b/tools/lv_gpio/lv_gpio_enum.h index 74f6c64f3..51e83ae7e 100644 --- a/tools/lv_gpio/lv_gpio_enum.h +++ b/tools/lv_gpio/lv_gpio_enum.h @@ -63,6 +63,8 @@ DSB_OUT = GPIO_DSB_OUT WS2812 = GPIO_WS2812 MHZ_TXD = GPIO_MHZ_TXD MHZ_RXD = GPIO_MHZ_RXD +CM11_TXD = GPIO_CM11_TXD +CM11_RXD = GPIO_CM11_RXD PZEM0XX_TX = GPIO_PZEM0XX_TX PZEM004_RX = GPIO_PZEM004_RX PZEM016_RX = GPIO_PZEM016_RX