Merge pull request #13139 from Arnold-n/development+SCD40

Add SCD40/41 support
This commit is contained in:
Theo Arends 2021-09-23 14:42:20 +02:00 committed by GitHub
commit 608ab64d05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1408 additions and 2 deletions

View File

@ -123,6 +123,7 @@ m = minimal, l = lite, t = tasmota, k = knx, s = sensors, i = ir, d = display
| USE_MGC3130 | - | - | - / - | - | - | - | - |
| USE_MAX44009 | - | - | - / - | - | - | - | - |
| USE_SCD30 | - | - | - / x | - | x | - | - |
| USE_SCD40 | - | - | - / x | - | - | - | - |
| USE_SPS30 | - | - | - / - | - | - | - | - |
| USE_ADE7953 | - | - | x / x | x | x | - | x |
| USE_VL53L0X | - | - | - / x | - | x | - | - |

View File

@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Crash recorder ``Status 12`` for ESP32/ESP32S2/ESP32C3, supporting Esp-idf 3.3/4.4
- Support for ESP32/ESP32S2 DAC gpio via Berry
- Berry support for Serial
- Support for Sensirion SCD40/SCD41 CO2 sensor
- Support for BL0939 energy monitor as used in ESP32 based Sonoff Dual R3 V2 Pow (#13195)
### Changed

View File

@ -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_92 | SCD40 | 0x62 | CO2 sensor Sensirion SCD40/SCD41

View File

@ -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 <Wire.h>
#include <math.h>
#include <stdio.h>
#include <twi.h>
#include <FrogmoreScd40.h>
// 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, &regValue);
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, &regValue);
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));
}

View File

@ -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
};

View File

@ -0,0 +1,9 @@
name=FrogmoreScd40
version=
author=Frogmore42,Arnold-n
maintainer=Arnold-n
sentence=SCD40
paragraph=SCD40
category=Sensor
url=
architectures=esp8266,esp32

View File

@ -607,6 +607,7 @@
// #define USE_MGC3130 // [I2cDriver27] Enable MGC3130 Electric Field Effect Sensor (I2C address 0x42) (+2k7 code, 0k3 mem)
// #define USE_MAX44009 // [I2cDriver28] Enable MAX44009 Ambient Light sensor (I2C addresses 0x4A and 0x4B) (+0k8 code)
// #define USE_SCD30 // [I2cDriver29] Enable Sensiron SCd30 CO2 sensor (I2C address 0x61) (+3k3 code)
// #define USE_SCD40 // [I2cDriver62] Enable Sensiron SCd40/Scd41 CO2 sensor (I2C address 0x62) (+3k5 code)
// #define USE_SPS30 // [I2cDriver30] Enable Sensiron SPS30 particle sensor (I2C address 0x69) (+1.7 code)
#define USE_ADE7953 // [I2cDriver7] Enable ADE7953 Energy monitor as used on Shelly 2.5 (I2C address 0x38) (+1k5)
// #define USE_VL53L0X // [I2cDriver31] Enable VL53L0x time of flight sensor (I2C address 0x29) (+4k code)

View File

@ -768,7 +768,9 @@ void ResponseAppendFeatures(void)
#ifdef USE_VINDRIKTNING
feature8 |= 0x00002000; // xsns_91_vindriktning.ino
#endif
// feature8 |= 0x00004000;
#if defined(USE_I2C) && defined(USE_SCD40)
feature8 |= 0x00004000; // xsns_92_scd40.ino
#endif
// feature8 |= 0x00008000;
// feature8 |= 0x00010000;

View File

@ -107,6 +107,7 @@
//#define USE_MGC3130 // [I2cDriver27] Enable MGC3130 Electric Field Effect Sensor (I2C address 0x42) (+2k7 code, 0k3 mem)
//#define USE_MAX44009 // [I2cDriver28] Enable MAX44009 Ambient Light sensor (I2C addresses 0x4A and 0x4B) (+0k8 code)
#define USE_SCD30 // [I2cDriver29] Enable Sensiron SCd30 CO2 sensor (I2C address 0x61) (+3k3 code)
//#define USE_SCD40 // [I2cDriver62] Enable Sensiron SCd40 CO2 sensor (I2C address 0x62) (+3k5 code)
//#define USE_SPS30 // [I2cDriver30] Enable Sensiron SPS30 particle sensor (I2C address 0x69) (+1.7 code)
#define USE_ADE7953 // [I2cDriver7] Enable ADE7953 Energy monitor as used on Shelly 2.5 (I2C address 0x38) (+1k5)
#define USE_VL53L0X // [I2cDriver31] Enable VL53L0x time of flight sensor (I2C address 0x29) (+4k code)

View File

@ -281,6 +281,7 @@
//#define USE_MGC3130 // [I2cDriver27] Enable MGC3130 Electric Field Effect Sensor (I2C address 0x42) (+2k7 code, 0k3 mem)
//#define USE_MAX44009 // [I2cDriver28] Enable MAX44009 Ambient Light sensor (I2C addresses 0x4A and 0x4B) (+0k8 code)
//#define USE_SCD30 // [I2cDriver29] Enable Sensiron SCd30 CO2 sensor (I2C address 0x61) (+3k3 code)
//#define USE_SCD40 // [I2cDriver62] Enable Sensiron SCd40 CO2 sensor (I2C address 0x62) (+3k3 code)
//#define USE_SPS30 // [I2cDriver30] Enable Sensiron SPS30 particle sensor (I2C address 0x69) (+1.7 code)
//#define USE_ADE7953 // [I2cDriver7] Enable ADE7953 Energy monitor as used on Shelly 2.5 (I2C address 0x38) (+1k5)
//#define USE_VL53L0X // [I2cDriver31] Enable VL53L0x time of flight sensor (I2C address 0x29) (+4k code)
@ -415,6 +416,7 @@
//#define USE_MGC3130 // [I2cDriver27] Enable MGC3130 Electric Field Effect Sensor (I2C address 0x42) (+2k7 code, 0k3 mem)
//#define USE_MAX44009 // [I2cDriver28] Enable MAX44009 Ambient Light sensor (I2C addresses 0x4A and 0x4B) (+0k8 code)
#define USE_SCD30 // [I2cDriver29] Enable Sensiron SCd30 CO2 sensor (I2C address 0x61) (+3k3 code)
#define USE_SCD40 // [I2cDriver62] Enable Sensiron SCd40 CO2 sensor (I2C address 0x62) (+3k5 code)
//#define USE_SPS30 // [I2cDriver30] Enable Sensiron SPS30 particle sensor (I2C address 0x69) (+1.7 code)
#define USE_ADE7953 // [I2cDriver7] Enable ADE7953 Energy monitor as used on Shelly 2.5 (I2C address 0x38) (+1k5)
#define USE_VL53L0X // [I2cDriver31] Enable VL53L0x time of flight sensor (I2C address 0x29) (+4k code)

491
tasmota/xsns_92_scd40.ino Normal file
View File

@ -0,0 +1,491 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
// define USE_SCD40 to use SCD40 or SCD41 device; use SCD41-functions only if you use SCD41 sensor
// define USE_SCD40_LOWPOWER to use low-power periodic measurement mode (both SCD40 and SCD41)
// define SCD40_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_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 <FrogmoreScd40.h>
#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 scd40i2cReset_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, scd40i2cReset_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, scd40i2cReset_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, scd40i2cReset_count);
AddLog(LOG_LEVEL_ERROR, PSTR("SCD40: clearing i2c bus"));
#endif
scd40i2cReset_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;
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;
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

View File

@ -256,7 +256,7 @@ a_features = [[
"USE_MPU_ACCEL","USE_TFMINIPLUS","USE_CSE7761","USE_BERRY",
"USE_BM8563","USE_ENERGY_DUMMY","USE_AM2320","USE_T67XX",
"USE_MCP2515","USE_TASMESH","USE_WIFI_RANGE_EXTENDER","USE_INFLUXDB",
"USE_HRG15","USE_VINDRIKTNING","","",
"USE_HRG15","USE_VINDRIKTNING","USE_SCD40","",
"","","","",
"","","","",
"","","","",