mirror of https://github.com/arendst/Tasmota.git
Add SCD40/41 support
This commit is contained in:
parent
f9e1ab1c90
commit
0fea60d8ee
|
@ -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
|
||||
|
|
|
@ -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, ®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));
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
name=FrogmoreScd40
|
||||
version=
|
||||
author=Frogmore42,Arnold-n
|
||||
maintainer=Arnold-n
|
||||
sentence=SCD40
|
||||
paragraph=SCD40
|
||||
category=Sensor
|
||||
url=
|
||||
architectures=esp8266,esp32
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
// 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 <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 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
|
Loading…
Reference in New Issue