From 0fea60d8ee305a797f98b0f140f00c30ee55a2e1 Mon Sep 17 00:00:00 2001 From: Arnold Niessen Date: Wed, 15 Sep 2021 12:02:27 +0200 Subject: [PATCH] Add SCD40/41 support --- I2CDEVICES.md | 1 + lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.cpp | 779 +++++++++++++++++++ lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.h | 118 +++ lib/lib_i2c/FrogmoreScd40/library.properties | 9 + tasmota/xsns_92_scd40.ino | 501 ++++++++++++ 5 files changed, 1408 insertions(+) create mode 100644 lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.cpp create mode 100644 lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.h create mode 100644 lib/lib_i2c/FrogmoreScd40/library.properties create mode 100644 tasmota/xsns_92_scd40.ino diff --git a/I2CDEVICES.md b/I2CDEVICES.md index 730d24113..975a748dc 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -95,3 +95,4 @@ Index | Define | Driver | Device | Address(es) | Description 59 | USE_BM8563 | xdrv_56 | BM8563 | 0x51 | BM8563 RTC from M5Stack 60 | USE_AM2320 | xsns_88 | AM2320 | 0x5C | Temperature and Humidity sensor 61 | USE_T67XX | xsns_89 | T67XX | 0x15 | CO2 sensor + 62 | USE_SCD40 | xsns_42 | SCD40 | 0x62 | CO2 sensor Sensirion SCD40/SCD41 diff --git a/lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.cpp b/lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.cpp new file mode 100644 index 000000000..ee7e3c232 --- /dev/null +++ b/lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.cpp @@ -0,0 +1,779 @@ +/* + FrogmoreScd40.h - SCD40/SCD41 I2C CO2(+temp+RH) sensor support for Tasmota, + based on frogmore42's FrogmoreScd30.h + + Copyright (C) 2019-2021 Frogmore42, Arnold-n + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +#include +#include +#include +#include +#include + +// References are made to Sensirion datasheet at +// https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9.5_CO2/Sensirion_CO2_Sensors_SCD4x_Datasheet.pdf +// +// Basic Commands Chapter 3.5 +#define COMMAND_SCD40_START_PERIODIC_MEASUREMENT 0x21b1 +#define COMMAND_SCD40_READ_MEASUREMENT 0xec05 +#define COMMAND_SCD40_STOP_PERIODIC_MEASUREMENT 0x3f86 + +// On-chip output signal compensation Chapter 3.6 +#define COMMAND_SCD40_SET_TEMPERATURE_OFFSET 0x241d +#define COMMAND_SCD40_GET_TEMPERATURE_OFFSET 0x2318 +#define COMMAND_SCD40_SET_SENSOR_ALTITUDE 0x2427 +#define COMMAND_SCD40_GET_SENSOR_ALTITUDE 0x2322 +#define COMMAND_SCD40_SET_AMBIENT_PRESSURE 0xe000 + +// Field calibration Chapter 3.7 +#define COMMAND_SCD40_PERFORM_FORCED_RECALIBRATION 0x362f +#define COMMAND_SCD40_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED 0x2416 +#define COMMAND_SCD40_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED 0x2313 + +// Low power Chapter 3.8 +#define COMMAND_SCD40_START_LOW_POWER_PERIODIC_MEASUREMENT 0x21ac +#define COMMAND_SCD40_GET_DATA_READY_STATUS 0xe4b8 + +// Advanced features Chapter 3.9 +#define COMMAND_SCD40_PERSIST_SETTINGS 0x3615 +#define COMMAND_SCD40_GET_SERIAL_NUMBER 0x3682 +#define COMMAND_SCD40_PERFORM_SELF_TEST 0x3639 +#define COMMAND_SCD40_PERFORM_FACTORY_RESET 0x3632 +#define COMMAND_SCD40_REINIT 0x3646 + +// Low power single shot (SCD41 only) Chapter 3.10 +// only for SCD41 +#define COMMAND_SCD40_MEASURE_SINGLE_SHOT 0x219d +#define COMMAND_SCD40_MEASURE_SINGLE_SHOT_RHT_ONLY 0x2196 + +#define SCD40_DATA_REGISTER_BYTES 2 +#define SCD40_DATA_REGISTER_WITH_CRC 3 +#define SCD40_MEAS_BYTES 9 + +#ifdef SCD40_DEBUG +enum LoggingLevels {LOG_LEVEL_NONE, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_DEBUG_MORE, LOG_LEVEL_ALL}; +char scd40log_data[180]; +#define SCD40_DEBUG_LOG_LEVEL LOG_LEVEL_INFO +#endif + +// helper and private functions + +/*--------------------------------------------------------------------------- + Function : medianfilter() + In : pointer to array of SCD40_MEDIAN_FILTER_SIZE values + Out : a uint16_t which is the middle value of the array + Job : search of the median + Notice : replaced SCD30 alg by partial bubble-sort, slightly slower, but not fixed-size + ---------------------------------------------------------------------------*/ +#define PIX_SORT(a,b) { if ((a)>(b)) PIX_SWAP((a),(b)); } +#define PIX_SWAP(a,b) { uint16_t temp=(a);(a)=(b);(b)=temp; } + +uint16_t FrogmoreScd40::medianfilter(uint16_t * p) +{ + for (int8_t i = SCD40_MEDIAN_FILTER_SIZE-1; i >= (SCD40_MEDIAN_FILTER_SIZE-1)/2; i--) + { + for (uint8_t j=0; j < i; j++) + { + PIX_SORT(p[j], p[j+1]); + } + } + return(p[(SCD40_MEDIAN_FILTER_SIZE-1)/2]); +} + +#ifdef SCD40_DEBUG +void FrogmoreScd40::AddLog(uint8_t loglevel) +{ + if (loglevel <= SCD40_DEBUG_LOG_LEVEL) + { + Serial.printf("%s\r\n", scd40log_data); + } +} +#endif + +uint8_t FrogmoreScd40::computeCRC8(uint8_t data[], uint8_t len) +// Computes the CRC that the SCD40 uses +{ + uint8_t crc = 0xFF; //Init with 0xFF + + for (uint8_t x = 0 ; x < len ; x++) + { + crc ^= data[x]; // XOR-in the next input byte + for (uint8_t i = 0 ; i < 8 ; i++) + { + if ((crc & 0x80) != 0) + crc = (uint8_t)((crc << 1) ^ 0x31); + else + crc <<= 1; + } + } + return crc; //No output reflection +} + +// Sends stream of bytes to device +int FrogmoreScd40::sendBytes(void *pInput, uint8_t len) +{ + uint8_t *pBytes = (uint8_t *) pInput; + int result; + uint8_t errorBytes = 0; // number of bytes that had an error in transmission +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40SendBytes: len: %d data: 0x %02X %02X %02X | 0x %02X %02X %02X | 0x %02X %02X %02X", len, pBytes[0], pBytes[1], pBytes[2], pBytes[3], pBytes[4], pBytes[5], pBytes[6], pBytes[7], pBytes[8]); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + pWire->beginTransmission(this->i2cAddress); + errorBytes = len - (pWire->write(pBytes, len)); + result = pWire->endTransmission(); + if (errorBytes || result) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40SendBytes: errorBytes: %d | Wire.end: %d", errorBytes, result); + AddLog(LOG_LEVEL_INFO); +#endif + } + result <<= 8; // leave room for error bytes number + result |= errorBytes; // low byte has number of bytes that were not written correctly + return (result); +} + +// Gets a number of bytes from device +int FrogmoreScd40::getBytes(void *pOutput, uint8_t len) +{ + uint8_t *pBytes = (uint8_t *) pOutput; + uint8_t result; + + result = pWire->requestFrom(this->i2cAddress, len); + if (len != result) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40GetBytes: wire request expected %d got: %d", len, result); + AddLog(LOG_LEVEL_INFO); +#endif + return (ERROR_SCD40_NOT_ENOUGH_BYTES_ERROR); + } + + if (pWire->available()) + { + for (int x = 0; x < len; x++) + { + pBytes[x] = pWire->read(); + } +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40GetBytes: len: %d data: 0x %02X %02X %02X | 0x %02X %02X %02X | 0x %02X %02X %02X", len, pBytes[0], pBytes[1], pBytes[2], pBytes[3], pBytes[4], pBytes[5], pBytes[6], pBytes[7], pBytes[8]); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + return (ERROR_SCD40_NO_ERROR); + } + return (ERROR_SCD40_UNKNOWN_ERROR); +} + +//Sends just a command, no arguments, no CRC +int FrogmoreScd40::sendCommand(uint16_t command) +{ + uint8_t data[2]; + data[0] = command >> 8; + data[1] = command & 0xFF; + int error = sendBytes(data, sizeof(data)); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40SendCommand: sendBytes failed, error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + } + return (error); +} + +//Sends a command along with arguments and CRC +int FrogmoreScd40::sendCommandArguments(uint16_t command, uint16_t arguments) +{ + uint8_t data[5]; + data[0] = command >> 8; + data[1] = command & 0xFF; + data[2] = arguments >> 8; + data[3] = arguments & 0xFF; + data[4] = computeCRC8(&data[2], 2); //Calc CRC on the arguments only, not the command + int error = sendBytes(data, sizeof(data)); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40SendCommandArguments: sendBytes failed, error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + } + return (error); +} + +// Sends a command along with arguments and CRC, wait 400ms, fetch results +// NOT TESTED - not sure whether this works +int FrogmoreScd40::sendCommandArgumentsFetchResult(uint16_t command, uint16_t arguments, uint16_t* pData) +{ + sendCommandArguments(command, arguments); + delay(400); // the SCD30 uses clock stretching to give it time to prepare data, waiting here makes it work, seems this works also for SCD40 + uint8_t data[SCD40_DATA_REGISTER_WITH_CRC]; + int error = getBytes(data, sizeof(data)); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadRegister: Scd40GetBytes error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + uint16 regValue; + error = get16BitRegCheckCRC(data, ®Value); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadRegister: Scd40get16BitRegCheckCRC error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + *pData = regValue; + return (ERROR_SCD40_NO_ERROR); +} + +int FrogmoreScd40::get16BitRegCheckCRC(void* pInput, uint16_t *pData) +{ + uint8_t *pBytes = (uint8_t *) pInput; + uint8_t expectedCRC = computeCRC8(pBytes, SCD40_DATA_REGISTER_BYTES); + if (expectedCRC != pBytes[SCD40_DATA_REGISTER_BYTES]) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40get16BitRegCheckCRC: expected: 0x%02X, but got: 0x%02X", expectedCRC, pBytes[SCD40_DATA_REGISTER_BYTES]); + AddLog(LOG_LEVEL_INFO); + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40get16BitRegCheckCRC: data: 0x%02X, 0x%02X, 0x%02X", pBytes[0], pBytes[1], pBytes[2]); + AddLog(LOG_LEVEL_INFO); +#endif + return (ERROR_SCD40_CRC_ERROR); + } + *pData = (uint16_t) pBytes[0] << 8 | pBytes[1]; // data from SCD40 is Big-Endian + return (ERROR_SCD40_NO_ERROR); +} + +//Gets two bytes (and check CRC) from SCD40 +int FrogmoreScd40::readRegisterCnt(uint16_t registerAddress, uint16_t* pData, uint8_t cnt) +{ + int error = sendCommand(registerAddress); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadRegister: SendCommand error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + delay(1); // the SCD30 uses clock stretching to give it time to prepare data, waiting here makes it work, seems this works also for SCD40 + uint8_t data[SCD40_DATA_REGISTER_WITH_CRC]; + for (uint8_t c = 0; c < cnt; c++) { + error = getBytes(data, sizeof(data)); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadRegister: Scd40GetBytes error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + uint16 regValue; + error = get16BitRegCheckCRC(data, ®Value); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadRegister: Scd40get16BitRegCheckCRC error: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + pData[c] = regValue; + } + return (ERROR_SCD40_NO_ERROR); +} + +int FrogmoreScd40::readRegister(uint16_t registerAddress, uint16_t* pData) +{ + int error=readRegisterCnt(registerAddress, pData, 1); + return (error); +} + +// public functions + +void FrogmoreScd40::begin(TwoWire *pWire, uint8_t i2cAddress) +{ + this->duringMeasurement = 0; + this->i2cAddress = i2cAddress; + this->co2EAverage = 0; + if (pWire == NULL) + { + this->pWire = &Wire; + } + else + { + this->pWire = pWire; + } + co2NewDataLocation = -1; // indicates there is no data, so the 1st non-zero data point needs to fill up the median filter +#ifdef ESP8266 + this->pWire->setClockStretchLimit(200000); +#endif +} + +void FrogmoreScd40::begin(uint8_t i2cAddress) +{ + begin(NULL, i2cAddress); +} + +void FrogmoreScd40::begin(TwoWire *pWire) +{ + begin(pWire, SCD40_ADDRESS); +} + +void FrogmoreScd40::begin(void) +{ + begin(NULL, SCD40_ADDRESS); +} + +// twi_status() attempts to read out any data left that is holding SDA low, so a new transaction can take place +// something like (http://www.forward.com.au/pfod/ArduinoProgramming/I2C_ClearBus/index.html) +int FrogmoreScd40::clearI2CBus(void) +{ +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "clearI2CBus"); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif +#ifdef ESP8266 + return (twi_status()); +#else + return 0; +#endif +} + +// Function order below follows SCD40 datasheet +// Basic Commands Chapter 3.5 + +int FrogmoreScd40::startPeriodicMeasurement(void) +{ +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Start periodic measurement"); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + DuringMeasurement = 1; + return(sendCommand(COMMAND_SCD40_START_PERIODIC_MEASUREMENT)); +} + +int FrogmoreScd40::readMeasurement( + uint16 *pCO2_ppm, + uint16 *pCO2EAvg_ppm, + float *pTemperature, + float *pHumidity +) +{ +// Should only be called in DuringMeasurement mode or +// after calling measure_single_hot{,_rht_only} +// but this is currently not verified + bool isAvailable = false; + int error = 0; + uint16 tempCO2; + uint16 tempHumidity; + uint16 tempTemperature; + + error = getDataReadyStatus(&isAvailable); + if (error) + { + return (error); + } + if (!isAvailable) + { + return (ERROR_SCD40_NO_DATA); + } +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: have data"); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + error = sendCommand(COMMAND_SCD40_READ_MEASUREMENT); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: send command failed: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + delay(1); // the SCD40 uses clock streching to give it time to prepare data, waiting here makes it work + uint8_t bytes[SCD40_MEAS_BYTES]; + // there are (3) 16-bit values, each with a CRC in the measurement data + // the chip sends all of these, unless stopped by an early NACK - not supported here + error = getBytes(bytes, SCD40_MEAS_BYTES); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: Scd40GetBytes command failed: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: Scd40GetBytes data: 0x %02X %02X %02X | 0x %02X %02X %02X | 0x %02X %02X %02X", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8]); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + error = get16BitRegCheckCRC(&bytes[0], &tempCO2); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: Scd40Get16BitsCheckCRC 1st command failed: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + error = get16BitRegCheckCRC(&bytes[3], &tempTemperature); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: Scd40Get16BitsCheckCRC 2nd command failed: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + error = get16BitRegCheckCRC(&bytes[6], &tempHumidity); + if (error) + { +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: Scd40Get16BitsCheckCRC 3rd command failed: 0x%lX", error); + AddLog(LOG_LEVEL_INFO); +#endif + return (error); + } + // tempCO2 = 0 occurs after Measure_single_shot_RHT_only; no reason for error like for SCD30, but don't add to history, + // and take care to handle special case where no CO2 measurement was seen yet + if (tempCO2 > 0) + { + // add tempCO2 measurement to history + if (co2NewDataLocation < 0) + { + co2EAverage = tempCO2; + for (int x = 0; x < SCD40_MEDIAN_FILTER_SIZE; x++) + { + co2History[x] = tempCO2; + co2NewDataLocation = 1; + } + } + else + { + co2History[co2NewDataLocation++] = tempCO2; + if (co2NewDataLocation >= SCD40_MEDIAN_FILTER_SIZE) + { + co2NewDataLocation = 0; + } + } +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: co2History: %ld, %ld, %ld, %ld, %ld", co2History[0], co2History[1], co2History[2], co2History[3], co2History[4]); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + } + if ((tempCO2 > 0) || (co2NewDataLocation >= 0)) { + // find median of history; copy array since the median filter function will re-arrange it + uint16_t temp[SCD40_MEDIAN_FILTER_SIZE]; + for (int x = 0; x < SCD40_MEDIAN_FILTER_SIZE; x++) + { + temp[x] = co2History[x]; + } +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: temp: %ld, %ld, %ld, %ld, %ld", temp[0], temp[1], temp[2], temp[3], temp[4]); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + *pCO2_ppm = medianfilter(temp); + } else { + *pCO2_ppm = 0; // never seen real CO2 measurement, but need to return something: return 0 + } +#ifdef SCD40_DEBUG + snprintf_P(scd40log_data, sizeof(scd40log_data), "Scd40ReadMeasurement: CO2_ppm: %ld", *pCO2_ppm); + AddLog(LOG_LEVEL_DEBUG_MORE); +#endif + if ((pCO2EAvg_ppm) && (tempCO2 > 0)) + { + int16_t delta = (int16_t) *pCO2_ppm - (int16_t) co2EAverage; + int16_t change = delta / 32; + co2EAverage += change; + *pCO2EAvg_ppm = co2EAverage; + } + *pTemperature = (175.0 * tempTemperature) / 65536 - 45; + *pHumidity = (100.0 * tempHumidity) / 65536; + return (ERROR_SCD40_NO_ERROR); +} + +int FrogmoreScd40::forceStopPeriodicMeasurement(void) +{ + DuringMeasurement = 0; + return (sendCommand(COMMAND_SCD40_STOP_PERIODIC_MEASUREMENT)); +} + +int FrogmoreScd40::stopPeriodicMeasurement(void) +{ + if (!DuringMeasurement) { + return (ERROR_SCD40_NOT_IN_MEASUREMENT_MODE); + } + DuringMeasurement = 0; + return (sendCommand(COMMAND_SCD40_STOP_PERIODIC_MEASUREMENT)); +} + +// On-chip output signal compensation Chapter 3.6 + +int FrogmoreScd40::setTemperatureOffset(float offset_degC) +// influences RH and T readings. Does not influence CO2 measurement. Default is 4 degrees Celcius. +// to save setting to the EEPROM, call persistSetting() +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + if (offset_degC >= 0) + { + uint16_t offset_xDegC = (uint16_t) (offset_degC * 374.491); + return (sendCommandArguments(COMMAND_SCD40_SET_TEMPERATURE_OFFSET, offset_xDegC)); + } + else + { + return (ERROR_SCD40_INVALID_VALUE); + } +} + +int FrogmoreScd40::setTemperatureOffset(uint16_t offset_centiDegC) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + uint16_t offset_xDegC = (uint16_t) (offset_centiDegC * 3.74491); + return (sendCommandArguments(COMMAND_SCD40_SET_TEMPERATURE_OFFSET, offset_xDegC)); +} + +int FrogmoreScd40::getTemperatureOffset(float *pOffset_degC) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + uint16_t value; + int error = readRegister(COMMAND_SCD40_GET_TEMPERATURE_OFFSET, &value); + if (!error) + { + // result is 175 * value/2^16, need to convert to degrees + *pOffset_degC = (float) value / 374.491; + } + return (error); +} + +int FrogmoreScd40::getTemperatureOffset(uint16_t *pOffset_centiDegC) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + uint16_t value; + int error = readRegister(COMMAND_SCD40_GET_TEMPERATURE_OFFSET, &value); + if (!error) + { + // result is 175 * value/2^16, need to convert to degrees + *pOffset_centiDegC = (uint16_t) (value / 3.74491); + } + return (error); +} + +int FrogmoreScd40::setSensorAltitude(uint16_t height_meter) +// Default is 0 meter above sea-level; +// to save setting to the EEPROM, call persistSetting() +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommandArguments(COMMAND_SCD40_SET_SENSOR_ALTITUDE, height_meter)); +} + +int FrogmoreScd40::getSensorAltitude(uint16_t *pHeight_meter) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (readRegister(COMMAND_SCD40_GET_SENSOR_ALTITUDE, pHeight_meter)); +} + +int FrogmoreScd40::setAmbientPressure(uint16_t airPressure_mbar) +// Overrides any pressure compensation based on a previously set sensor altitude +{ + // allowed DuringMeasurement + return (sendCommandArguments(COMMAND_SCD40_SET_AMBIENT_PRESSURE, airPressure_mbar)); +} + +// Field calibration Chapter 3.7 + +int FrogmoreScd40::performForcedRecalibration(uint16_t co2_ppm) +// Calibrates with a CO2 reference value immediately +// Use only in planned operation mode, for >3 minutes in constant CO2 environment +// issue stop_periodic_measurement, and then wait 500ms, before calling this function +// it takes 400ms for this command to complete +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + uint16_t FRC_result; + sendCommandArgumentsFetchResult(COMMAND_SCD40_PERFORM_FORCED_RECALIBRATION, co2_ppm, &FRC_result); + if (FRC_result == 0xffff) { + // a return value of 0xffff indicates failure + return(ERROR_SCD40_FRC_FAILED); + } + return (ERROR_SCD40_NO_ERROR); +} + +int FrogmoreScd40::setAutomaticSelfCalibrationDisabled(void) +// to save setting to the EEPROM, call persistSetting() +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + bool isAuto = false; + return (setAutomaticSelfCalibrationEnabled(isAuto)); +} + +int FrogmoreScd40::setAutomaticSelfCalibrationEnabled(void) +// to save setting to the EEPROM, call persistSetting() +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + bool isAuto = true; + return (setAutomaticSelfCalibrationEnabled(isAuto)); +} + +int FrogmoreScd40::setAutomaticSelfCalibrationEnabled(bool isAuto) +// to save setting to the EEPROM, call persistSetting() +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + bool value = !!isAuto; // using NOT operator twice makes sure value is 0 or 1 + return (sendCommandArguments(COMMAND_SCD40_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED, value)); +} + +int FrogmoreScd40::getAutomaticSelfCalibrationEnabled(uint16_t *pIsAuto) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + uint16_t value = 0; + int error = readRegister(COMMAND_SCD40_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED, &value); + if (!error) + { + *pIsAuto = value != 0; + } + return (error); +} + +// Low power Chapter 3.8 + +int FrogmoreScd40::startLowPowerPeriodicMeasurement(void) +// Comment: unclear how to stop this operation mode? +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommand(COMMAND_SCD40_START_LOW_POWER_PERIODIC_MEASUREMENT)); + DuringMeasurement = 1; +} + +int FrogmoreScd40::getDataReadyStatus(bool *pIsAvailable) +{ + // allowed DuringMeasurement + uint16_t isDataAvailable = false; + int error = readRegister(COMMAND_SCD40_GET_DATA_READY_STATUS, &isDataAvailable); + if (!error) + { + *pIsAvailable = (isDataAvailable & 0x07ff) != 0; + } + return (error); +} + +// Advanced features Chapter 3.9 + +int FrogmoreScd40::persistSettings(void) +// Store configuration settings such as temperature offset, +// sensor altitude, and ASC enable/disabled parameter +// EEPROM is guaranteed to endure at least 2000 write cycles before failure. +// The field calibration history (FRC and ASC) is stored in a separate EEPROM. +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommand(COMMAND_SCD40_PERSIST_SETTINGS)); +} + +int FrogmoreScd40::getSerialNumber(uint16_t *pSerialNumberArray) +// Serialnr is 48 bits = 3 16-bit words +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + uint16_t value; + int error = readRegisterCnt(COMMAND_SCD40_GET_SERIAL_NUMBER, pSerialNumberArray, 3); + return (error); +} + +int FrogmoreScd40::performSelfTest(uint16_t *pMalfunction) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (readRegister(COMMAND_SCD40_PERFORM_SELF_TEST, pMalfunction)); +} + +int FrogmoreScd40::performFactoryReset(void) +// resets all configuration settings in EEPROM and +// erases FRC and ASC algorithm history +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommand(COMMAND_SCD40_PERFORM_FACTORY_RESET)); +} + +int FrogmoreScd40::reinit(void) +// reinitailizes sensor from EEPROM user settings +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommand(COMMAND_SCD40_REINIT)); +} + +// Low power single shot (SCD41 only) Chapter 3.10 +// (on-demand measurements) + +int FrogmoreScd40::measureSingleShot(void) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommand(COMMAND_SCD40_MEASURE_SINGLE_SHOT)); +} + +int FrogmoreScd40::measureSingleShotRhtOnly(void) +{ + if (DuringMeasurement) { + return (ERROR_SCD40_BUSY_MEASURING); + } + return (sendCommand(COMMAND_SCD40_MEASURE_SINGLE_SHOT_RHT_ONLY)); +} diff --git a/lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.h b/lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.h new file mode 100644 index 000000000..454c8ab5d --- /dev/null +++ b/lib/lib_i2c/FrogmoreScd40/FrogmoreScd40.h @@ -0,0 +1,118 @@ +/* + FrogmoreScd40.cpp - SCD40/SCD41 I2C CO2(+temp+RH) sensor support for Tasmota, + based on frogmore42's FrogmoreScd30.cpp + + Copyright (C) 2019-2021 Frogmore42, Arnold-n + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// SCD40/SCD41 code based on SCD30 code + +#pragma once + +#include "Arduino.h" + +//#define SCD40_DEBUG + +#define SCD40_ADDRESS 0x62 +#define ERROR_SCD40_NO_ERROR 0 +#define ERROR_SCD40_NO_DATA 0x80000000 +#define ERROR_SCD40_UNKNOWN_ERROR 0x1000000 +#define ERROR_SCD40_CRC_ERROR 0x2000000 +#define ERROR_SCD40_NOT_ENOUGH_BYTES_ERROR 0x3000000 +#define ERROR_SCD40_INVALID_VALUE 0x6000000 +#define ERROR_SCD40_BUSY_MEASURING 0x7000000 +#define ERROR_SCD40_NOT_IN_MEASUREMENT_MODE 0x8000000 +#define ERROR_SCD40_FRC_FAILED 0x9000000 + +#define SCD40_MEDIAN_FILTER_SIZE 1 + +class FrogmoreScd40 +{ + public: + FrogmoreScd40() {}; + // Constructors + // the SCD40 only lists a single i2c address, so not necesary to specify + // + void begin(void); + void begin(uint8_t _i2cAddress); + void begin(TwoWire *pWire); + void begin(TwoWire *pWire, uint8_t _i2cAddress); + + int reinit(void); + int clearI2CBus(void); // this is a HARD reset of the IC2 bus to restore communication, it will disrupt the bus + + int getSensorAltitude(uint16_t *pHeight_meter); + int getAutomaticSelfCalibrationEnabled(uint16_t *pIsAuto); + int getTemperatureOffset(float *pOffset_degC); + int getTemperatureOffset(uint16_t *pOffset_centiDegC); + + int setSensorAltitude(uint16_t height_meter); + int setAmbientPressure(uint16_t airPressure_mbar); + int setTemperatureOffset(float offset_degC); + int setTemperatureOffset(uint16_t offset_centiDegC); + int setAutomaticSelfCalibrationDisabled(void); + int setAutomaticSelfCalibrationEnabled(void); + int setAutomaticSelfCalibrationEnabled(bool isAuto); + int performForcedRecalibration(uint16_t co2_ppm); + int getSerialNumber(uint16_t *pSerialNumberArray); + int startLowPowerPeriodicMeasurement(void); + int persistSettings(void); + int performSelfTest(uint16_t *pMalfunction); + int performFactoryReset(void); + + int startPeriodicMeasurement(void); + int getDataReadyStatus(bool *pIsAvailable); + int readMeasurement( + uint16 *pCO2_ppm, + uint16 *pCO2EAvg_ppm, + float *pTemperature, + float *pHumidity + ); + int stopPeriodicMeasurement(void); + int forceStopPeriodicMeasurement(void); +// SCD41 only: + int measureSingleShot(void); + int measureSingleShotRhtOnly(void); + + private: + uint8_t duringMeasurement; + uint8_t i2cAddress; + TwoWire *pWire; + uint16_t co2AvgExtra; + uint16_t co2History[SCD40_MEDIAN_FILTER_SIZE]; + uint16_t co2EAverage; + int8_t co2NewDataLocation; // location to put new CO2 data for median filter + bool DuringMeasurement; + + uint8_t computeCRC8(uint8_t data[], uint8_t len); + uint16_t medianfilter(uint16_t * p); + int sendBytes(void *pInput, uint8_t len); + int getBytes(void *pOutput, uint8_t len); + int sendCommand(uint16_t command); + int sendCommandArguments(uint16_t command, uint16_t arguments); + int sendCommandArgumentsFetchResult(uint16_t command, uint16_t arguments, uint16_t* pData); + int get16BitRegCheckCRC(void* pInput, uint16_t* pData); + int readRegisterCnt(uint16_t registerAddress, uint16_t* pData, uint8_t cnt); + int readRegister(uint16_t registerAddress, uint16_t* pData); +#ifdef SCD40_DEBUG + void AddLog(uint8_t loglevel); +#endif +}; diff --git a/lib/lib_i2c/FrogmoreScd40/library.properties b/lib/lib_i2c/FrogmoreScd40/library.properties new file mode 100644 index 000000000..5dead8f63 --- /dev/null +++ b/lib/lib_i2c/FrogmoreScd40/library.properties @@ -0,0 +1,9 @@ +name=FrogmoreScd40 +version= +author=Frogmore42,Arnold-n +maintainer=Arnold-n +sentence=SCD40 +paragraph=SCD40 +category=Sensor +url= +architectures=esp8266,esp32 diff --git a/tasmota/xsns_92_scd40.ino b/tasmota/xsns_92_scd40.ino new file mode 100644 index 000000000..88ac61aea --- /dev/null +++ b/tasmota/xsns_92_scd40.ino @@ -0,0 +1,501 @@ +/* + xsns_92_scd40.ino - SCD40/SCD41 I2C CO2(+temp+RH) sensor support for Tasmota, + based on frogmore42's xsns_42_scd30.ino + + Copyright (C) 2021 Frogmore42, Arnold-n + + 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 . + +*/ + +// define USE_SCD40 to use SCD40 (or SCD41 device without additional SCD41 functions) +// define USE_SCD41 to use SCD41 device (including low-power options) +// define USE_SCD40_LOWPOWER to use low-power periodic measurement mode +// define SCD40_DEBUG (or SCD41_DEBUG) to debug + +// Console instructions supported: (errorvalue=-1 in case of error, errorvalue=0 otherwise) +// (data=-1 in case of error, value otherwise) +// (third colum: time in ms needed for execution) +// (DPM: may be executed during periodic measurements) +// +// Instruction Returns Exec(ms) DPM Function +// ------------------------------------------------------------------------------------ +// SCD40Alt data 1 no get Sensor Altitude (in m) +// SCD40Alt x errorvalue 1 no set Sensor Altitude (in m) +// SCD40Auto data 1 no get CalibrationEnabled status (bool) +// SCD40Auto x errorvalue 1 no set CalibrationEnabled status (bool) +// SCD40Toff data 1 no get Temperature offset (centigrades) +// SCD40Toff x errorvalue 1 no set Temperature offset (centigrades) (some rounding may occur) +// SCD40Pres x errorvalue 1 yes set Ambient Pressure (mbar) (overrides Sensor Altitude setting) +// SCD40Cal x errorvalue 400 no perform forced recalibration (ppm CO2) +// SCD40Test errorvalue 10000 no perform selftest +// SCD40StLp errorvalue 0 no start periodic measurement in low-power mode (1/30s) +// SCD40Strt errorvalue 0 no start periodic measurement (1/5s) +// SCD40Stop errorvalue 500 yes stop periodic measurement +// SCD40Pers errorvalue 800 no persist settings in EEPROM (2000 write cycles guaranteed) +// SCD40Rein errorvalue 20 no reinit sensor +// SCD40Fact errorvalue 1200 no factory reset sensor +// +// SCD40Sing errorvalue 5000 no (SCD41 only) measure single shot +// SCD40SRHT errorvalue 50 no (SCD41 only) measure single shot, RHT only + +//#define SCD40_DEBUG + +#ifdef USE_I2C + +#ifdef USE_SCD41 +#define USE_SCD40 +#ifdef SCD41_DEBUG +#define SCD40_DEBUG +#endif +#endif + +#ifdef USE_SCD40 + +#define XSNS_92 92 +#define XI2C_62 62 // See I2CDEVICES.md + +// #define SCD40_ADDRESS 0x62 // already defined in lib + +#define SCD40_MAX_MISSED_READS 30 // in seconds (at 1 read/second) +#define SCD40_STATE_NO_ERROR 0 +#define SCD40_STATE_ERROR_DATA_CRC 1 +#define SCD40_STATE_ERROR_READ_MEAS 2 +#define SCD40_STATE_ERROR_SOFT_RESET 3 +#define SCD40_STATE_ERROR_I2C_RESET 4 + +#include "Arduino.h" +#include + +#define D_CMND_SCD40 "SCD40" + +const char S_JSON_SCD40_COMMAND_NVALUE[] PROGMEM = "{\"" D_CMND_SCD40 "%s\":%d}"; +//const char S_JSON_SCD40_COMMAND[] PROGMEM = "{\"" D_CMND_SCD40 "%s\"}"; +const char kSCD40_Commands[] PROGMEM = "Alt|Auto|Toff|Pres|Cal|Test|StLP|Strt|Stop|Pers|Rein|Fact|Sing|SRHT"; + +/*********************************************************************************************\ + * enumerations +\*********************************************************************************************/ + +enum SCD40_Commands { // commands useable in console or rules + CMND_SCD40_ALTITUDE, + CMND_SCD40_AUTOMODE, + CMND_SCD40_TEMPOFFSET, + CMND_SCD40_PRESSURE, + CMND_SCD40_FORCEDRECALIBRATION, + CMND_SCD40_SELFTEST, + CMND_SCD40_START_MEASUREMENT_LOW_POWER, + CMND_SCD40_START_MEASUREMENT, + CMND_SCD40_STOP_MEASUREMENT, + CMND_SCD40_PERSIST, + CMND_SCD40_REINIT, + CMND_SCD40_FACTORYRESET, + CMND_SCD40_SINGLESHOT, + CMND_SCD40_SINGLESHOT_RHT_ONLY +}; + +FrogmoreScd40 scd40; + +bool scd40Found = false; +bool scd40IsDataValid = false; +int scd40ErrorState = SCD40_STATE_NO_ERROR; +int scd40Loop_count = 0; +int scd40DataNotAvailable_count = 0; +int scd40GoodMeas_count = 0; +int scd40Reset_count = 0; +int scd40CrcError_count = 0; +int i2cReset_count = 0; +uint16_t scd40_CO2 = 0; +uint16_t scd40_CO2EAvg = 0; +float scd40_Humid = 0.0; +float scd40_Temp = 0.0; + +void Scd40Detect(void) +{ + if (I2cActive(SCD40_ADDRESS)) { + AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 I2c already active in Scd40Detect()") ); + return; + } + + scd40.begin(); + + // don't stop in case of error, try to continue + delay(10); // not sure whether this is needed + int error = scd40.forceStopPeriodicMeasurement(); // after reboot, stop (if any) periodic measurement, or reinit may not work +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 force-stop error: %d"), error); +#endif + delay(550); // wait >500ms after stopPeriodicMeasurement before SCD40 allows any other command + + error = scd40.reinit(); // just in case +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 reinit error: %d"), error); +#endif + delay(20); // not sure whether this is needed + + uint16_t sn[3]; + error = scd40.getSerialNumber(sn); + AddLog(LOG_LEVEL_NONE, PSTR("SCD40 serial nr 0x%X 0x%X 0x%X") ,sn[0], sn[1], sn[2]); + + // by default, start measurements, only register device if this succeeds +#ifdef USE_SCD40_LOWPOWER + if (scd40.startLowPowerPeriodicMeasurement()) { return; } +#else + if (scd40.startPeriodicMeasurement()) { return; } +#endif + I2cSetActiveFound(SCD40_ADDRESS, "SCD40"); + scd40Found = true; +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40 found, measurements started.")); +#endif +} + +// gets data from the sensor +void Scd40Update(void) +{ + bool isAvailable; + + scd40Loop_count++; + + uint32_t error = 0; + switch (scd40ErrorState) { + case SCD40_STATE_NO_ERROR: { + error = scd40.readMeasurement(&scd40_CO2, &scd40_CO2EAvg, &scd40_Temp, &scd40_Humid); + switch (error) { + case ERROR_SCD40_NO_ERROR: + scd40Loop_count = 0; + scd40IsDataValid = true; + scd40GoodMeas_count++; + break; + + case ERROR_SCD40_NO_DATA: + scd40DataNotAvailable_count++; + AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40: no data available.")); + break; + + case ERROR_SCD40_CRC_ERROR: + scd40CrcError_count++; +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: CRC error, CRC error: %ld, good: %ld, no data: %ld, sc30_reset: %ld, i2c_reset: %ld"), + scd40CrcError_count, scd40GoodMeas_count, scd40DataNotAvailable_count, scd40Reset_count, i2cReset_count); +#endif + break; + + default: { + scd40ErrorState = SCD40_STATE_ERROR_READ_MEAS; +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: Update: ReadMeasurement error: 0x%lX, counter: %ld"), error, scd40Loop_count); +#endif + return; + } + break; + } + } + break; + + case SCD40_STATE_ERROR_READ_MEAS: { +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: (rd) in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"), + scd40ErrorState, scd40GoodMeas_count, scd40DataNotAvailable_count, scd40Reset_count, i2cReset_count); + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: not answering, sending soft reset, counter: %ld"), scd40Loop_count); +#endif + scd40Reset_count++; + error = scd40.stopPeriodicMeasurement(); + if (error) { + scd40ErrorState = SCD40_STATE_ERROR_SOFT_RESET; +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: stopPeriodicMeasurement got error: 0x%lX"), error); +#endif + } else { + error = scd40.reinit(); + if (error) { +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: resetting got error: 0x%lX"), error); +#endif + scd40ErrorState = SCD40_STATE_ERROR_SOFT_RESET; + } else { + scd40ErrorState = ERROR_SCD40_NO_ERROR; + } + } + } + break; + + case SCD40_STATE_ERROR_SOFT_RESET: { +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: (rst) in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"), + scd40ErrorState, scd40GoodMeas_count, scd40DataNotAvailable_count, scd40Reset_count, i2cReset_count); + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: clearing i2c bus")); +#endif + i2cReset_count++; + error = scd40.clearI2CBus(); + if (error) { + scd40ErrorState = SCD40_STATE_ERROR_I2C_RESET; +#ifdef SCD40_DEBUG + AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: error clearing i2c bus: 0x%lX"), error); +#endif + } else { + scd40ErrorState = ERROR_SCD40_NO_ERROR; + } + } + break; + + case SCD40_STATE_ERROR_I2C_RESET: { + // Give up + } + break; + } + + if (scd40Loop_count > SCD40_MAX_MISSED_READS) { + AddLog(LOG_LEVEL_DEBUG, PSTR("SCD40: max-missed-reads.")); + scd40IsDataValid = false; + } +} + +int Scd40GetCommand(int command_code, uint16_t *pvalue) +{ + switch (command_code) + { + case CMND_SCD40_ALTITUDE: + return scd40.getSensorAltitude(pvalue); + break; + + case CMND_SCD40_AUTOMODE: + return scd40.getAutomaticSelfCalibrationEnabled(pvalue); + break; + + case CMND_SCD40_TEMPOFFSET: + return scd40.getTemperatureOffset(pvalue); + break; + + case CMND_SCD40_SELFTEST: + return scd40.performSelfTest(pvalue); + break; + + default: + // else for Unknown command + break; + } + return 0; // Fix GCC 10.1 warning +} + +int Scd40SetCommand(int command_code, uint16_t *pvalue) +{ + switch (command_code) + { + case CMND_SCD40_ALTITUDE: + return scd40.setSensorAltitude(*pvalue); + break; + + case CMND_SCD40_AUTOMODE: + return scd40.setAutomaticSelfCalibrationEnabled((bool) (*pvalue)); + break; + + case CMND_SCD40_TEMPOFFSET: + return scd40.setTemperatureOffset(*pvalue); + break; + + case CMND_SCD40_PRESSURE: + return scd40.setAmbientPressure(*pvalue); + break; + + case CMND_SCD40_FORCEDRECALIBRATION: + return scd40.performForcedRecalibration(*pvalue); + break; + + default: + // else for Unknown command + break; + } + return 0; // Fix GCC 10.1 warning +} + +/*********************************************************************************************\ + * Command Sensor42 +\*********************************************************************************************/ + +bool Scd40CommandSensor() +{ + char command[CMDSZ]; + bool serviced = true; + uint8_t prefix_len = strlen(D_CMND_SCD40); + int error; + + if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_CMND_SCD40), prefix_len)) { // prefix + int command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic + prefix_len, kSCD40_Commands); + +// not supported here: readMeasurement, getDataReadyStatus, getSerialNumber, measure_single_shot, measure_single_shot_rht_only + + switch (command_code) { + case CMND_SCD40_ALTITUDE: + case CMND_SCD40_AUTOMODE: + case CMND_SCD40_TEMPOFFSET: + case CMND_SCD40_PRESSURE: // write-only + case CMND_SCD40_FORCEDRECALIBRATION: // write-only + case CMND_SCD40_SELFTEST: // read-only + { + uint16_t value = 0; + if (XdrvMailbox.data_len > 0) + { + if (command_code != CMND_SCD40_SELFTEST) { + value = XdrvMailbox.payload; + error = Scd40SetCommand(command_code, &value); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } else { + serviced = false; + break; + } + } + else + { + if ((command_code != CMND_SCD40_PRESSURE) && (command_code != CMND_SCD40_FORCEDRECALIBRATION)) { + error = Scd40GetCommand(command_code, &value); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:value); + } else { + serviced = false; + break; + } + } + } + break; + + case CMND_SCD40_START_MEASUREMENT_LOW_POWER: + { + error = scd40.startLowPowerPeriodicMeasurement(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + + case CMND_SCD40_START_MEASUREMENT: + { + error = scd40.startPeriodicMeasurement(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + + case CMND_SCD40_STOP_MEASUREMENT: + { + error = scd40.stopPeriodicMeasurement(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + + case CMND_SCD40_PERSIST: + { + error = scd40.persistSettings(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + + case CMND_SCD40_REINIT: + { + error = scd40.reinit(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + + case CMND_SCD40_FACTORYRESET: + { + error = scd40.performFactoryReset(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + +#ifdef USE_SCD41 + case CMND_SCD40_SINGLESHOT: + { + error = scd40.measureSingleShot(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; + + case CMND_SCD40_SINGLESHOT_RHT_ONLY: + { + error = scd40.measureSingleShotRhtOnly(); + Response_P(S_JSON_SCD40_COMMAND_NVALUE, command, error?-1:0); + } + break; +#endif + + default: + // else for Unknown command + serviced = false; + break; + } + } + return serviced; +} + +void Scd40Show(bool json) +{ + if (scd40IsDataValid) + { + float t = ConvertTemp(scd40_Temp); + float h = ConvertHumidity(scd40_Humid); + + if (json) { + ResponseAppend_P(PSTR(",\"SCD40\":{\"" D_JSON_CO2 "\":%d,\"" D_JSON_ECO2 "\":%d,"), scd40_CO2, scd40_CO2EAvg); + ResponseAppendTHD(t, h); + ResponseJsonEnd(); +#ifdef USE_DOMOTICZ + if (0 == TasmotaGlobal.tele_period) { + DomoticzSensor(DZ_AIRQUALITY, scd40_CO2); + DomoticzTempHumPressureSensor(t, h); + } +#endif // USE_DOMOTICZ +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(HTTP_SNS_CO2EAVG, "SCD40", scd40_CO2EAvg); + WSContentSend_PD(HTTP_SNS_CO2, "SCD40", scd40_CO2); + WSContentSend_THD("SCD40", t, h); +#endif // USE_WEBSERVER + } + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns92(byte function) +{ + if (!I2cEnabled(XI2C_62)) { return false; } + + bool result = false; + + if (FUNC_INIT == function) { + Scd40Detect(); + } + else if (scd40Found) { + switch (function) { + case FUNC_EVERY_SECOND: + Scd40Update(); + break; + case FUNC_COMMAND: + result = Scd40CommandSensor(); + break; + case FUNC_JSON_APPEND: + Scd40Show(1); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + Scd40Show(0); + break; +#endif // USE_WEBSERVER + } + } + return result; +} + +#endif // USE_SCD40 +#endif // USE_I2C