/* XSNS_95_cm1107.ino - CM1107(B) CO2 sensor support for Tasmota Copyright (C) 2022 Maksim 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_CM110x /*********************************************************************************************\ * 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) { //0x16 - first byte in response resp_len = cm11_response[1] +3 ; // Get expected response len (according protocol desc), +3 - first byte, len and checksum } } 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(); } #ifdef ESP32 AddLog(LOG_LEVEL_DEBUG, PSTR("CM1: Serial UART%d"), CM11Serial->getUart()); #endif 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(uint32_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_CM110x