mirror of
https://github.com/letscontrolit/ESPEasy.git
synced 2026-07-28 04:07:47 +00:00
Merge pull request #3854 from tonhuisman/feature/p117-scd30-co2-humidity-temperature-sensor
[P117] Add SCD30 CO2, humidity and temperature sensor
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
.. include:: ../Plugin/_plugin_substitutions_p11x.repl
|
||||
.. _P117_page:
|
||||
|
||||
|P117_typename|
|
||||
==================================================
|
||||
|
||||
|P117_shortinfo|
|
||||
|
||||
Plugin details
|
||||
--------------
|
||||
|
||||
Type: |P117_type|
|
||||
|
||||
Name: |P117_name|
|
||||
|
||||
Status: |P117_status|
|
||||
|
||||
GitHub: |P117_github|_
|
||||
|
||||
Maintainer: |P117_maintainer|
|
||||
|
||||
Used libraries: |P117_usedlibraries|
|
||||
|
||||
Description
|
||||
-----------
|
||||
|
||||
The SCD30 is a CO2 sensor unit that also provides Humidity and Temperature measurement, supports auto-calibration, and has temperature and altitude compensation settings.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
.. image:: P117_DeviceConfiguration.png
|
||||
:alt: Device configuration
|
||||
|
||||
|
||||
* **Name** Required by ESPEasy, must be unique among the list of available devices/tasks.
|
||||
|
||||
* **Enabled** The device can be disabled or enabled. When not enabled the device should not use any resources.
|
||||
|
||||
I2C options
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. note:: According to manufacturer specification, the I2C ClockStretchLimit setting should be configured at a minimum value of 20 milliseconds, and can take up to 150 msec once a day. For ESP8266 and ESP32 there are somewhat different values to fill, ESP8266 expects microseconds (usec) there, 20000 to 150000, and ESP32 needs 1/80 usec, 1600000 to 12000000, so a factor 80 difference.
|
||||
|
||||
The available settings here depend on the build used. At least the **Force Slow I2C speed** option is available, but selections for the I2C Multiplexer can also be shown. For details see the :ref:`Hardware_page`
|
||||
|
||||
.. note:: The SCD30 sensor supports a max. I2C Clock Speed of 100 kHz, so **Force Slow I2C speed** should be checked! (ESPEasy has a default setting of 100 kHz for I2C Slow device Clock Speed).
|
||||
|
||||
Device Settings
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
* **Altitude** Set the altitude above sealevel in meters, where the sensor is placed. The range is 0..2000 meter.
|
||||
|
||||
* **Temp offset** Offset for temperature correction. Can be set to compensate any self-heating of the device, by comparing the temperature measurement with another calibrated temperature measured close to the device. The difference can be set here, and is (also) stored in the sensor (but re-written every time ESPEasy initializes the sensor).
|
||||
|
||||
The Data Acquisition, Send to Controller and Interval settings are standard available configuration items. Send to Controller only when one or more Controllers are configured.
|
||||
|
||||
**Interval** By default, Interval will be set to 60 sec. The data will be measured and optionally sent to any configured controllers using this interval.
|
||||
|
||||
Values
|
||||
^^^^^^
|
||||
|
||||
The used library provides an averaged CO2 value of the last 5 measurements in value ``CO2``. The last single CO2 measurement is also available as ``CO2raw``.
|
||||
|
||||
``Humidity`` shows the relative humidity RH in a range of 0..100%.
|
||||
|
||||
``Temperature`` shows the temperature of the sensor in degrees Celcius, with the **Temp offset** applied.
|
||||
|
||||
Commands available
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. include:: P117_commands.repl
|
||||
|
||||
Change log
|
||||
----------
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
...
|
||||
|
||||
|added|
|
||||
2021-11-20 Initial release version.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,31 @@
|
||||
.. csv-table::
|
||||
:header: "Command Syntax", "Extra information"
|
||||
:widths: 20, 30
|
||||
|
||||
"
|
||||
|
||||
| ``scdgetabc``
|
||||
|
||||
","
|
||||
|
||||
| Shows automatic calibration period in days, 0 = disable.
|
||||
|
||||
"
|
||||
"
|
||||
|
||||
| ``scdgetalt``
|
||||
|
||||
","
|
||||
|
||||
| Shows altitude compensation configuration in meters above sea level.
|
||||
|
||||
"
|
||||
"
|
||||
|
||||
| ``scdgettmp``
|
||||
|
||||
","
|
||||
|
||||
| Shows temperature offset in degrees C.
|
||||
|
||||
"
|
||||
@@ -140,6 +140,7 @@ There are different released versions of ESP Easy:
|
||||
":ref:`P113_page`","|P113_status|","P113"
|
||||
":ref:`P114_page`","|P114_status|","P114"
|
||||
":ref:`P115_page`","|P115_status|","P115"
|
||||
":ref:`P117_page`","|P117_status|","P117"
|
||||
|
||||
|
||||
Internal GPIO handling
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
.. |Plugin_Energy_Heat| replace:: :ref:`P088_page`, :ref:`P093_page`
|
||||
.. |Plugin_Environment| replace:: :ref:`P004_page`, :ref:`P005_page`, :ref:`P006_page`, :ref:`P014_page`, :ref:`P024_page`, :ref:`P028_page`, :ref:`P030_page`, :ref:`P031_page`, :ref:`P032_page`, :ref:`P034_page`, :ref:`P039_page`, :ref:`P047_page`, :ref:`P051_page`, :ref:`P068_page`, :ref:`P069_page`, :ref:`P072_page`, :ref:`P103_page`, :ref:`P104_page`, :ref:`P105_page`, :ref:`P106_page`
|
||||
.. |Plugin_Extra_IO| replace:: :ref:`P011_page`, :ref:`P022_page`
|
||||
.. |Plugin_Gases| replace:: :ref:`P049_page`, :ref:`P052_page`, :ref:`P083_page`, :ref:`P090_page`
|
||||
.. |Plugin_Gases| replace:: :ref:`P049_page`, :ref:`P052_page`, :ref:`P083_page`, :ref:`P090_page`, :ref:`P117_page`
|
||||
.. |Plugin_Generic| replace:: :ref:`P003_page`, :ref:`P026_page`, :ref:`P033_page`, :ref:`P037_page`, :ref:`P081_page`, :ref:`P100_page`
|
||||
.. |Plugin_Gesture| replace:: :ref:`P064_page`
|
||||
.. |Plugin_Gyro| replace:: :ref:`P045_page`
|
||||
|
||||
@@ -62,3 +62,16 @@
|
||||
.. |P115_maintainer| replace:: `TD-er`
|
||||
.. |P115_compileinfo| replace:: `.`
|
||||
.. |P115_usedlibraries| replace:: `https://github.com/sparkfun/SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library`
|
||||
|
||||
.. |P117_name| replace:: :cyan:`SCD30`
|
||||
.. |P117_type| replace:: :cyan:`Gases`
|
||||
.. |P117_typename| replace:: :cyan:`Gases - SCD30 CO2, Humidity, Temperature`
|
||||
.. |P117_porttype| replace:: `.`
|
||||
.. |P117_status| replace:: :yellow:`TESTING D`
|
||||
.. |P117_github| replace:: _P117_SCD30.ino
|
||||
.. _P117_github: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P117_SCD30.ino
|
||||
.. |P117_usedby| replace:: `.`
|
||||
.. |P117_shortinfo| replace:: `SCD30 CO2 sensor`
|
||||
.. |P117_maintainer| replace:: `tonhuisman`
|
||||
.. |P117_compileinfo| replace:: `.`
|
||||
.. |P117_usedlibraries| replace:: `https://github.com/Frogmore42/Sonoff-Tasmota/tree/development/lib/FrogmoreScd30 (corrected local copy)`
|
||||
|
||||
@@ -663,3 +663,9 @@ P115 :ref:`P115_page`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. include:: ../Plugin/P115_commands.repl
|
||||
|
||||
|
||||
P117 :ref:`P117_page`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. include:: ../Plugin/P117_commands.repl
|
||||
|
||||
@@ -0,0 +1,743 @@
|
||||
/*
|
||||
# Copyright (c) 2019 Frogmore42
|
||||
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 <FrogmoreScd30.h>
|
||||
|
||||
#define COMMAND_SCD30_CONTINUOUS_MEASUREMENT 0x0010
|
||||
#define COMMAND_SCD30_MEASUREMENT_INTERVAL 0x4600
|
||||
#define COMMAND_SCD30_GET_DATA_READY 0x0202
|
||||
#define COMMAND_SCD30_READ_MEASUREMENT 0x0300
|
||||
#define COMMAND_SCD30_CALIBRATION_TYPE 0x5306
|
||||
#define COMMAND_SCD30_FORCED_RECALIBRATION_FACTOR 0x5204
|
||||
#define COMMAND_SCD30_TEMPERATURE_OFFSET 0x5403
|
||||
#define COMMAND_SCD30_ALTITUDE_COMPENSATION 0x5102
|
||||
#define COMMAND_SCD30_SOFT_RESET 0xD304
|
||||
#define COMMAND_SCD30_GET_FW_VERSION 0xD100
|
||||
#define COMMAND_SCD30_STOP_MEASUREMENT 0x0104
|
||||
|
||||
#define SCD30_DATA_REGISTER_BYTES 2
|
||||
#define SCD30_DATA_REGISTER_WITH_CRC 3
|
||||
#define SCD30_MEAS_BYTES 18
|
||||
|
||||
#ifdef SCD30_DEBUG
|
||||
enum LoggingLevels { LOG_LEVEL_NONE, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_DEBUG_MORE, LOG_LEVEL_ALL };
|
||||
char scd30log_data[180];
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
void FrogmoreScd30::begin(TwoWire *pWire, uint8_t i2cAddress)
|
||||
{
|
||||
this->i2cAddress = i2cAddress;
|
||||
|
||||
if (pWire == NULL)
|
||||
{
|
||||
this->pWire = &Wire;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->pWire = pWire;
|
||||
}
|
||||
|
||||
co2NewDataLocation = -1; // indicates there is no data, so the 1st data point needs to fill up the median filter
|
||||
// this->pWire->setClockStretchLimit(200000); // should be set via Tools->Advanced->I2C ClockStretchLimit
|
||||
this->ambientPressure = 0;
|
||||
}
|
||||
|
||||
void FrogmoreScd30::begin(uint8_t i2cAddress)
|
||||
{
|
||||
begin(NULL, i2cAddress);
|
||||
}
|
||||
|
||||
void FrogmoreScd30::begin(TwoWire *pWire)
|
||||
{
|
||||
begin(pWire, SCD30_ADDRESS);
|
||||
}
|
||||
|
||||
void FrogmoreScd30::begin(void)
|
||||
{
|
||||
begin(NULL, SCD30_ADDRESS);
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------
|
||||
Function : opt_med5() In : pointer to array of 5 values
|
||||
Out : a uint16_t which is the middle value of the sorted array
|
||||
Job : optimized search of the median of 5 values
|
||||
Notice : found on sci.image.processing cannot go faster unless assumptions are made on the nature of the input signal.
|
||||
---------------------------------------------------------------------------*/
|
||||
#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 opt_med5(uint16_t *p)
|
||||
{
|
||||
PIX_SORT(p[0], p[1]);
|
||||
PIX_SORT(p[3], p[4]);
|
||||
PIX_SORT(p[0], p[3]);
|
||||
PIX_SORT(p[1], p[4]);
|
||||
PIX_SORT(p[1], p[2]);
|
||||
PIX_SORT(p[2], p[3]);
|
||||
PIX_SORT(p[1], p[2]);
|
||||
return p[2];
|
||||
}
|
||||
|
||||
// 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 FrogmoreScd30::clearI2CBus(void)
|
||||
// {
|
||||
// #ifdef SCD30_DEBUG
|
||||
// snprintf_P(scd30log_data, sizeof(scd30log_data), "clearI2CBus");
|
||||
// AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
// #endif // ifdef SCD30_DEBUG
|
||||
// return twi_status();
|
||||
// }
|
||||
|
||||
#ifdef SCD30_DEBUG
|
||||
void FrogmoreScd30::AddLog(uint8_t loglevel)
|
||||
{
|
||||
if (loglevel <= LOG_LEVEL_INFO)
|
||||
{
|
||||
Serial.printf("%s\r\n", scd30log_data);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
uint8_t FrogmoreScd30::computeCRC8(uint8_t data[], uint8_t len)
|
||||
|
||||
// Computes the CRC that the SCD30 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 FrogmoreScd30::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 SCD30_DEBUG
|
||||
snprintf_P(scd30log_data,
|
||||
sizeof(scd30log_data),
|
||||
"Scd30SendBytes: data: 0x %02X %02X %02X | 0x %02X %02X %02X | 0x %02X %02X %02X",
|
||||
pBytes[0],
|
||||
pBytes[1],
|
||||
pBytes[2],
|
||||
pBytes[3],
|
||||
pBytes[4],
|
||||
pBytes[5],
|
||||
pBytes[6],
|
||||
pBytes[7],
|
||||
pBytes[8]);
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
pWire->beginTransmission(this->i2cAddress);
|
||||
errorBytes = len - (pWire->write(pBytes, len));
|
||||
result = pWire->endTransmission();
|
||||
|
||||
if (errorBytes || result)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30SendBytes: errorBytes: %d | Wire.end: %d", errorBytes, result);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
}
|
||||
|
||||
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 FrogmoreScd30::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 SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30GetBytes: wire request expected %d got: %d", len, result);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return ERROR_SCD30_NOT_ENOUGH_BYTES_ERROR;
|
||||
}
|
||||
|
||||
if (pWire->available())
|
||||
{
|
||||
for (int x = 0; x < len; x++)
|
||||
{
|
||||
pBytes[x] = pWire->read();
|
||||
}
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30GetBytes: data: 0x %02X %02X %02X | 0x %02X %02X %02X | 0x %02X %02X %02X",
|
||||
pBytes[0], pBytes[1], pBytes[2], pBytes[3], pBytes[4], pBytes[5], pBytes[6], pBytes[7], pBytes[8]);
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return ERROR_SCD30_NO_ERROR;
|
||||
}
|
||||
|
||||
return ERROR_SCD30_UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
// Sends just a command, no arguments, no CRC
|
||||
int FrogmoreScd30::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 SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30SendCommand: Scd30SendBytes failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
// Sends a command along with arguments and CRC
|
||||
int FrogmoreScd30::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 SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30SendCommandArguments: Scd30SendBytes failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::get16BitRegCheckCRC(void *pInput, uint16_t *pData)
|
||||
{
|
||||
uint8_t *pBytes = (uint8_t *)pInput;
|
||||
uint8_t expectedCRC = computeCRC8(pBytes, SCD30_DATA_REGISTER_BYTES);
|
||||
|
||||
if (expectedCRC != pBytes[SCD30_DATA_REGISTER_BYTES])
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30get16BitRegCheckCRC: expected: 0x%02X, but got: 0x%02X", expectedCRC,
|
||||
pBytes[SCD30_DATA_REGISTER_BYTES]);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30get16BitRegCheckCRC: data: 0x%02X, 0x%02X, 0x%02X", pBytes[0], pBytes[1],
|
||||
pBytes[2]);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return ERROR_SCD30_CRC_ERROR;
|
||||
}
|
||||
*pData = (uint16_t)pBytes[0] << 8 | pBytes[1]; // data from SCD30 is Big-Endian
|
||||
return ERROR_SCD30_NO_ERROR;
|
||||
}
|
||||
|
||||
// gets 32 bits, (2) 16-bit chunks, and validates the CRCs
|
||||
//
|
||||
int FrogmoreScd30::get32BitRegCheckCRC(void *pInput, float *pData)
|
||||
{
|
||||
uint16_t tempU16High;
|
||||
uint16_t tempU16Low;
|
||||
uint8_t *pBytes = (uint8_t *)pInput;
|
||||
uint32_t rawInt = 0;
|
||||
|
||||
int error = get16BitRegCheckCRC(pBytes, &tempU16High);
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
error = get16BitRegCheckCRC(pBytes + SCD30_DATA_REGISTER_WITH_CRC, &tempU16Low);
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// data from SCD is Big-Endian
|
||||
rawInt |= tempU16High;
|
||||
rawInt <<= 16;
|
||||
rawInt |= tempU16Low;
|
||||
|
||||
*pData = *(float *)&rawInt;
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "get32BitRegCheckCRC: got: tempUs 0x%lX, %lX", tempU16High, tempU16Low);
|
||||
AddLog(LOG_LEVEL_DEBUG);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
if (isnan(*pData) || isinf(*pData))
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "get32BitRegCheckCRC: not a floating point number: rawInt 0x%lX", rawInt);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return ERROR_SCD30_NOT_A_NUMBER_ERROR;
|
||||
}
|
||||
|
||||
return ERROR_SCD30_NO_ERROR;
|
||||
}
|
||||
|
||||
// Gets two bytes (and check CRC) from SCD30
|
||||
int FrogmoreScd30::readRegister(uint16_t registerAddress, uint16_t *pData)
|
||||
{
|
||||
int error = sendCommand(registerAddress);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadRegister: SendCommand error: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
delay(1); // the SCD30 uses clock streching to give it time to prepare data, waiting here makes it work
|
||||
uint8_t data[SCD30_DATA_REGISTER_WITH_CRC];
|
||||
error = getBytes(data, sizeof(data));
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadRegister: Scd30GetBytes error: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
uint16_t regValue;
|
||||
error = get16BitRegCheckCRC(data, ®Value);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadRegister: Scd30get16BitRegCheckCRC error: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
|
||||
*pData = regValue;
|
||||
return ERROR_SCD30_NO_ERROR;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::softReset(void)
|
||||
{
|
||||
return sendCommand(COMMAND_SCD30_SOFT_RESET);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getAltitudeCompensation(uint16_t *pHeight_meter)
|
||||
{
|
||||
return readRegister(COMMAND_SCD30_ALTITUDE_COMPENSATION, pHeight_meter);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getAmbientPressure(uint16_t *pAirPressure_mbar)
|
||||
{
|
||||
*pAirPressure_mbar = ambientPressure;
|
||||
return ERROR_SCD30_NO_ERROR;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getCalibrationType(uint16_t *pIsAuto)
|
||||
{
|
||||
uint16_t value = 0;
|
||||
int error = readRegister(COMMAND_SCD30_CALIBRATION_TYPE, &value);
|
||||
|
||||
if (!error)
|
||||
{
|
||||
*pIsAuto = value != 0;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getFirmwareVersion(uint8_t *pMajor, uint8_t *pMinor)
|
||||
{
|
||||
uint16_t value;
|
||||
int error = readRegister(COMMAND_SCD30_GET_FW_VERSION, &value);
|
||||
|
||||
if (!error)
|
||||
{
|
||||
*pMajor = value >> 8;
|
||||
*pMinor = value & 0xFF;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getForcedRecalibrationFactor(uint16_t *pCo2_ppm)
|
||||
{
|
||||
return readRegister(COMMAND_SCD30_FORCED_RECALIBRATION_FACTOR, pCo2_ppm);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getMeasurementInterval(uint16_t *pTime_sec)
|
||||
{
|
||||
return readRegister(COMMAND_SCD30_MEASUREMENT_INTERVAL, pTime_sec);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getTemperatureOffset(float *pOffset_degC)
|
||||
{
|
||||
uint16_t value;
|
||||
int error = readRegister(COMMAND_SCD30_TEMPERATURE_OFFSET, &value);
|
||||
|
||||
if (!error)
|
||||
{
|
||||
// result is in centi-degrees, need to convert to degrees
|
||||
*pOffset_degC = (float)value / 100.0;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::getTemperatureOffset(uint16_t *pOffset_centiDegC)
|
||||
{
|
||||
uint16_t value;
|
||||
int error = readRegister(COMMAND_SCD30_TEMPERATURE_OFFSET, &value);
|
||||
|
||||
if (!error)
|
||||
{
|
||||
// result is in centi-degrees, need to convert to degrees
|
||||
*pOffset_centiDegC = value;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setAltitudeCompensation(uint16_t height_meter)
|
||||
{
|
||||
return sendCommandArguments(COMMAND_SCD30_ALTITUDE_COMPENSATION, height_meter);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setAmbientPressure(uint16_t airPressure_mbar)
|
||||
{
|
||||
ambientPressure = airPressure_mbar;
|
||||
return beginMeasuring(ambientPressure);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setAutoSelfCalibration(void)
|
||||
{
|
||||
bool isAuto = true;
|
||||
|
||||
return setCalibrationType(isAuto);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setCalibrationType(bool isAuto)
|
||||
{
|
||||
bool value = !!isAuto; // using NOT operator twice makes sure value is 0 or 1
|
||||
|
||||
return sendCommandArguments(COMMAND_SCD30_CALIBRATION_TYPE, value);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setForcedRecalibrationFactor(uint16_t co2_ppm)
|
||||
{
|
||||
return sendCommandArguments(COMMAND_SCD30_FORCED_RECALIBRATION_FACTOR, co2_ppm);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setManualCalibration(void)
|
||||
{
|
||||
bool isAuto = false;
|
||||
|
||||
return setCalibrationType(isAuto);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setMeasurementInterval(uint16_t time_sec)
|
||||
{
|
||||
if (time_sec < 2) { time_sec = 2; }
|
||||
|
||||
if (time_sec > 1800) { time_sec = 1800; }
|
||||
return sendCommandArguments(COMMAND_SCD30_MEASUREMENT_INTERVAL, time_sec);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setTemperatureOffset(float offset_degC)
|
||||
{
|
||||
uint16_t offset_centiDegC;
|
||||
|
||||
if (offset_degC >= 0)
|
||||
{
|
||||
offset_centiDegC = (uint16_t)offset_degC * 100;
|
||||
return sendCommandArguments(COMMAND_SCD30_TEMPERATURE_OFFSET, offset_centiDegC);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ERROR_SCD30_INVALID_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
int FrogmoreScd30::setTemperatureOffset(uint16_t offset_centiDegC)
|
||||
{
|
||||
return sendCommandArguments(COMMAND_SCD30_TEMPERATURE_OFFSET, offset_centiDegC);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::beginMeasuring(void)
|
||||
{
|
||||
return beginMeasuring(ambientPressure);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::beginMeasuring(uint16_t airPressure_mbar)
|
||||
{
|
||||
ambientPressure = airPressure_mbar;
|
||||
return sendCommandArguments(COMMAND_SCD30_CONTINUOUS_MEASUREMENT, ambientPressure);
|
||||
}
|
||||
|
||||
int FrogmoreScd30::isDataAvailable(bool *pIsAvailable)
|
||||
{
|
||||
uint16_t isDataAvailable = false;
|
||||
int error = readRegister(COMMAND_SCD30_GET_DATA_READY, &isDataAvailable);
|
||||
|
||||
if (!error)
|
||||
{
|
||||
*pIsAvailable = isDataAvailable != 0;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
uint32_t FrogmoreScd30::readMeasurement(
|
||||
uint16_t *pCO2_ppm,
|
||||
uint16_t *pCO2EAvg_ppm,
|
||||
float *pTemperature,
|
||||
float *pHumidity
|
||||
)
|
||||
{
|
||||
bool isAvailable = false;
|
||||
int error = 0;
|
||||
float tempCO2;
|
||||
float tempHumidity;
|
||||
float tempTemperature;
|
||||
|
||||
error = isDataAvailable(&isAvailable);
|
||||
|
||||
if (error)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!isAvailable)
|
||||
{
|
||||
return ERROR_SCD30_NO_DATA;
|
||||
}
|
||||
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: have data");
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
error = sendCommand(COMMAND_SCD30_READ_MEASUREMENT);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: send command failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
delay(1); // the SCD30 uses clock streching to give it time to prepare data, waiting here makes it work
|
||||
|
||||
uint8_t bytes[SCD30_MEAS_BYTES];
|
||||
|
||||
// there are (6) 16-bit values, each with a CRC in the measurement data
|
||||
// the chip does not seem to like sending this data, except all at once
|
||||
error = getBytes(bytes, SCD30_MEAS_BYTES);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: Scd30GetBytes command failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data,
|
||||
sizeof(scd30log_data),
|
||||
"Scd30ReadMeasurement: Scd30GetBytes 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);
|
||||
snprintf_P(scd30log_data,
|
||||
sizeof(scd30log_data),
|
||||
"Scd30ReadMeasurement: Scd30GetBytes data: 0x %02X %02X %02X | 0x %02X %02X %02X | 0x %02X %02X %02X",
|
||||
bytes[9],
|
||||
bytes[10],
|
||||
bytes[11],
|
||||
bytes[12],
|
||||
bytes[13],
|
||||
bytes[14],
|
||||
bytes[15],
|
||||
bytes[16],
|
||||
bytes[17]);
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
error = get32BitRegCheckCRC(&bytes[0], &tempCO2);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: Scd30Get32BitsCheckCRC 1st command failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
|
||||
error = get32BitRegCheckCRC(&bytes[6], &tempTemperature);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: Scd30Get32BitsCheckCRC 2nd command failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
|
||||
error = get32BitRegCheckCRC(&bytes[12], &tempHumidity);
|
||||
|
||||
if (error)
|
||||
{
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: Scd30Get32BitsCheckCRC 3rd command failed: 0x%lX", error);
|
||||
AddLog(LOG_LEVEL_INFO);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
return error;
|
||||
}
|
||||
|
||||
if (tempCO2 == 0)
|
||||
{
|
||||
return ERROR_SCD30_CO2_ZERO;
|
||||
}
|
||||
|
||||
if (co2NewDataLocation < 0)
|
||||
{
|
||||
co2EAverage = tempCO2;
|
||||
|
||||
for (int x = 0; x < SCD30_MEDIAN_FILTER_SIZE; x++)
|
||||
{
|
||||
co2History[x] = tempCO2;
|
||||
co2NewDataLocation = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
co2History[co2NewDataLocation++] = tempCO2;
|
||||
|
||||
if (co2NewDataLocation >= SCD30_MEDIAN_FILTER_SIZE)
|
||||
{
|
||||
co2NewDataLocation = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data,
|
||||
sizeof(scd30log_data),
|
||||
"Scd30ReadMeasurement: co2History: %ld, %ld, %ld, %ld, %ld",
|
||||
co2History[0],
|
||||
co2History[1],
|
||||
co2History[2],
|
||||
co2History[3],
|
||||
co2History[4]);
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
// copy array since the median filter function will re-arrange it
|
||||
uint16_t temp[SCD30_MEDIAN_FILTER_SIZE];
|
||||
|
||||
for (int x = 0; x < SCD30_MEDIAN_FILTER_SIZE; x++)
|
||||
{
|
||||
temp[x] = co2History[x];
|
||||
}
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data,
|
||||
sizeof(scd30log_data),
|
||||
"Scd30ReadMeasurement: temp: %ld, %ld, %ld, %ld, %ld",
|
||||
temp[0],
|
||||
temp[1],
|
||||
temp[2],
|
||||
temp[3],
|
||||
temp[4]);
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
*pCO2_ppm = opt_med5(temp);
|
||||
#ifdef SCD30_DEBUG
|
||||
snprintf_P(scd30log_data, sizeof(scd30log_data), "Scd30ReadMeasurement: CO2_ppm: %ld", *pCO2_ppm);
|
||||
AddLog(LOG_LEVEL_DEBUG_MORE);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
|
||||
if (pCO2EAvg_ppm)
|
||||
{
|
||||
int16_t delta = (int16_t)*pCO2_ppm - (int16_t)co2EAverage;
|
||||
int16_t change = delta / 32;
|
||||
co2EAverage += change;
|
||||
#if 0
|
||||
uint16_t remain = co2EAverage % 5;
|
||||
uint16_t dividend = co2EAverage / 5;
|
||||
uint16_t co2EAReported = dividend * 5;
|
||||
|
||||
if (remain > 2)
|
||||
{
|
||||
co2EAReported += 5;
|
||||
}
|
||||
*pCO2EAvg_ppm = co2EAReported;
|
||||
#else // if 0
|
||||
*pCO2EAvg_ppm = co2EAverage;
|
||||
#endif // if 0
|
||||
}
|
||||
|
||||
*pTemperature = tempTemperature;
|
||||
*pHumidity = tempHumidity;
|
||||
return ERROR_SCD30_NO_ERROR;
|
||||
}
|
||||
|
||||
int FrogmoreScd30::stopMeasuring(void)
|
||||
{
|
||||
return sendCommand(COMMAND_SCD30_STOP_MEASUREMENT);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
# Copyright (c) 2019 Frogmore42
|
||||
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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
// #define SCD30_DEBUG
|
||||
|
||||
#define SCD30_ADDRESS 0x61
|
||||
#define ERROR_SCD30_NO_ERROR 0
|
||||
#define ERROR_SCD30_NO_DATA 0x80000000
|
||||
#define ERROR_SCD30_CO2_ZERO 0x90000000
|
||||
#define ERROR_SCD30_UNKNOWN_ERROR 0x1000000
|
||||
#define ERROR_SCD30_CRC_ERROR 0x2000000
|
||||
#define ERROR_SCD30_NOT_ENOUGH_BYTES_ERROR 0x3000000
|
||||
#define ERROR_SCD30_NOT_FOUND_ERROR 0x4000000
|
||||
#define ERROR_SCD30_NOT_A_NUMBER_ERROR 0x5000000
|
||||
#define ERROR_SCD30_INVALID_VALUE 0x6000000
|
||||
|
||||
#define SCD30_MEDIAN_FILTER_SIZE 5
|
||||
|
||||
class FrogmoreScd30 {
|
||||
public:
|
||||
|
||||
FrogmoreScd30() {}
|
||||
|
||||
// Constructors
|
||||
// the SCD30 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 softReset(void);
|
||||
|
||||
// int clearI2CBus(void); // this is a HARD reset of the IC2 bus to restore communication, it will disrupt the bus
|
||||
|
||||
int getAltitudeCompensation(uint16_t *pHeight_meter);
|
||||
int getAmbientPressure(uint16_t *pAirPressure_mbar);
|
||||
int getCalibrationType(uint16_t *pIsAuto);
|
||||
int getFirmwareVersion(uint8_t *pMajor,
|
||||
uint8_t *pMinor);
|
||||
int getForcedRecalibrationFactor(uint16_t *pCo2_ppm);
|
||||
int getMeasurementInterval(uint16_t *pTime_sec);
|
||||
int getTemperatureOffset(float *pOffset_degC);
|
||||
int getTemperatureOffset(uint16_t *pOffset_centiDegC);
|
||||
|
||||
int setAltitudeCompensation(uint16_t height_meter);
|
||||
int setAmbientPressure(uint16_t airPressure_mbar);
|
||||
int setAutoSelfCalibration(void);
|
||||
int setCalibrationType(bool isAuto);
|
||||
int setForcedRecalibrationFactor(uint16_t co2_ppm);
|
||||
int setManualCalibration(void);
|
||||
int setMeasurementInterval(uint16_t time_sec);
|
||||
int setTemperatureOffset(float offset_degC);
|
||||
int setTemperatureOffset(uint16_t offset_centiDegC);
|
||||
|
||||
int beginMeasuring(void);
|
||||
int beginMeasuring(uint16_t airPressure_mbar); // also sets ambient pressure offset in mbar/hPascal
|
||||
int isDataAvailable(bool *pIsAvailable);
|
||||
uint32_t readMeasurement(uint16_t *pCO2_ppm,
|
||||
uint16_t *pCO2EAvg_ppm,
|
||||
float *pTemperature,
|
||||
float *pHumidity);
|
||||
int stopMeasuring(void);
|
||||
|
||||
private:
|
||||
|
||||
uint8_t i2cAddress;
|
||||
TwoWire *pWire;
|
||||
uint16_t ambientPressure;
|
||||
uint16_t co2AvgExtra;
|
||||
uint16_t co2History[SCD30_MEDIAN_FILTER_SIZE];
|
||||
uint16_t co2EAverage;
|
||||
int8_t co2NewDataLocation; // location to put new CO2 data for median filter
|
||||
|
||||
uint8_t computeCRC8(uint8_t data[],
|
||||
uint8_t len);
|
||||
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 get16BitRegCheckCRC(void *pInput,
|
||||
uint16_t *pData);
|
||||
int get32BitRegCheckCRC(void *pInput,
|
||||
float *pData);
|
||||
int readRegister(uint16_t registerAddress,
|
||||
uint16_t *pData);
|
||||
#ifdef SCD30_DEBUG
|
||||
void AddLog(uint8_t loglevel);
|
||||
#endif // ifdef SCD30_DEBUG
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
name=FrogmoreScd30
|
||||
version=1.0
|
||||
author=Frogmore
|
||||
maintainer=Frogmore
|
||||
sentence=
|
||||
paragraph=
|
||||
category=Environment sensor
|
||||
url=
|
||||
architectures=esp8266,esp32
|
||||
@@ -10,7 +10,7 @@ lib_ignore = ESP8266WiFi, ESP8266Ping, ESP8266WebServer, ESP8266H
|
||||
|
||||
[esp32_common]
|
||||
extends = common, core_esp32_3_3_0
|
||||
lib_deps = td-er/ESPeasySerial @ 2.0.8, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ArduinoOTA, ESP32HTTPUpdateServer
|
||||
lib_deps = td-er/ESPeasySerial @ 2.0.8, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ArduinoOTA, ESP32HTTPUpdateServer, FrogmoreScd30
|
||||
lib_ignore = ${esp32_always.lib_ignore}, ESP32_ping, IRremoteESP8266, HeatpumpIR
|
||||
board_build.f_flash = 80000000L
|
||||
board_build.flash_mode = dout
|
||||
|
||||
@@ -52,7 +52,7 @@ extends = common
|
||||
board_build.f_cpu = 80000000L
|
||||
build_flags = ${debug_flags.build_flags} ${mqtt_flags.build_flags} -DHTTPCLIENT_1_1_COMPATIBLE=0
|
||||
build_unflags = -DDEBUG_ESP_PORT
|
||||
lib_deps = td-er/ESPeasySerial @ 2.0.8, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, bblanchon/ArduinoJson @ ^6.17.2, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/RABurton ESP8266 Mutex @ ^1.0.2, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ESP8266HTTPUpdateServer
|
||||
lib_deps = td-er/ESPeasySerial @ 2.0.8, adafruit/Adafruit ILI9341 @ ^1.5.6, Adafruit GFX Library, LOLIN_EPD, Adafruit BusIO, bblanchon/ArduinoJson @ ^6.17.2, VL53L0X @ 1.3.0, SparkFun VL53L1X 4m Laser Distance Sensor @ 1.2.9, td-er/RABurton ESP8266 Mutex @ ^1.0.2, td-er/SparkFun MAX1704x Fuel Gauge Arduino Library @ ^1.0.1, ESP8266HTTPUpdateServer, FrogmoreScd30
|
||||
lib_ignore = ${esp82xx_defaults.lib_ignore}, IRremoteESP8266, HeatpumpIR, LittleFS(esp8266), ServoESP32, TinyWireM
|
||||
board = esp12e
|
||||
monitor_filters = esp8266_exception_decoder
|
||||
|
||||
@@ -377,6 +377,7 @@ static const char DATA_ESPEASY_DEFAULT_MIN_CSS[] PROGMEM = {
|
||||
// #define USES_P113 // VL53L1X ToF
|
||||
// #define USES_P114 // VEML6075
|
||||
// #define USES_P115 // MAX1704x
|
||||
// #define USES_P117 // SCD30
|
||||
// #define USES_P119 // ITG3205 Gyro
|
||||
// #define USES_P120 // ADXL345 Acceleration / Gravity
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "_Plugin_Helper.h"
|
||||
#ifdef USES_P117
|
||||
|
||||
// #######################################################################################################
|
||||
// ############ Plugin 117 SCD30 I2C CO2, Humidity and Temperature Sensor ################################
|
||||
// #######################################################################################################
|
||||
// development version
|
||||
// by: V0JT4
|
||||
// this plugin is based on the Frogmore42 library
|
||||
// written based code from https://github.com/Frogmore42/Sonoff-Tasmota/tree/development/lib/FrogmoreScd30
|
||||
|
||||
// Changelog:
|
||||
//
|
||||
// 2021-11-20 tonhuisman: Implement multi-instance support (using PluginStruct)
|
||||
// 2021-09 tonhuisman: Moved from ESPEasyPluginPlayground to main repository
|
||||
|
||||
// Commands:
|
||||
// SCDGETABC - shows automatic calibration period in days, 0 = disable
|
||||
// SCDGETALT - shows altitude compensation configuration in meters above sea level
|
||||
// SCDGETTMP - shows temperature offset in degrees C
|
||||
|
||||
# define PLUGIN_117
|
||||
# define PLUGIN_ID_117 117
|
||||
# define PLUGIN_NAME_117 "Gases - CO2 SCD30 [TESTING]"
|
||||
# define PLUGIN_VALUENAME1_117 "CO2"
|
||||
# define PLUGIN_VALUENAME2_117 "Humidity"
|
||||
# define PLUGIN_VALUENAME3_117 "Temperature"
|
||||
# define PLUGIN_VALUENAME4_117 "CO2raw"
|
||||
|
||||
# include "./src/PluginStructs/P117_data_struct.h"
|
||||
|
||||
boolean Plugin_117(uint8_t function, struct EventStruct *event, String& string)
|
||||
{
|
||||
boolean success = false;
|
||||
|
||||
switch (function)
|
||||
{
|
||||
case PLUGIN_DEVICE_ADD:
|
||||
{
|
||||
Device[++deviceCount].Number = PLUGIN_ID_117;
|
||||
Device[deviceCount].Type = DEVICE_TYPE_I2C;
|
||||
Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_QUAD;
|
||||
Device[deviceCount].Ports = 0;
|
||||
Device[deviceCount].PullUpOption = false;
|
||||
Device[deviceCount].InverseLogicOption = false;
|
||||
Device[deviceCount].FormulaOption = true;
|
||||
Device[deviceCount].ValueCount = 4;
|
||||
Device[deviceCount].SendDataOption = true;
|
||||
Device[deviceCount].TimerOption = true;
|
||||
Device[deviceCount].GlobalSyncOption = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_GET_DEVICENAME:
|
||||
{
|
||||
string = F(PLUGIN_NAME_117);
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_GET_DEVICEVALUENAMES:
|
||||
{
|
||||
strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[0], PSTR(PLUGIN_VALUENAME1_117));
|
||||
strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[1], PSTR(PLUGIN_VALUENAME2_117));
|
||||
strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[2], PSTR(PLUGIN_VALUENAME3_117));
|
||||
strcpy_P(ExtraTaskSettings.TaskDeviceValueNames[3], PSTR(PLUGIN_VALUENAME4_117));
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_I2C_HAS_ADDRESS:
|
||||
{
|
||||
success = event->Par1 == 0x61;
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_SET_DEFAULTS:
|
||||
{
|
||||
ExtraTaskSettings.TaskDeviceValueDecimals[0] = 0; // CO2 values are integers
|
||||
ExtraTaskSettings.TaskDeviceValueDecimals[3] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_WEBFORM_SHOW_I2C_PARAMS:
|
||||
{
|
||||
html_TR_TD();
|
||||
html_TD();
|
||||
addHtml(F("<span style=\"color:red\">Tools->Advanced->I2C ClockStretchLimit should be set in range 20 to 150 msec.</span>"));
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_WEBFORM_LOAD:
|
||||
{
|
||||
addFormNumericBox(F("Altitude"), F("plugin_117_SCD30_alt"), PCONFIG(0), 0, 2000);
|
||||
addUnit(F("0..2000 m"));
|
||||
addFormTextBox(F("Temp offset"), F("plugin_117_SCD30_tmp"), String(PCONFIG_FLOAT(0), 2), 5);
|
||||
addUnit(F("°C"));
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case PLUGIN_WEBFORM_SAVE:
|
||||
{
|
||||
uint16_t alt = getFormItemInt(F("plugin_117_SCD30_alt"));
|
||||
|
||||
if (alt > 2000) { alt = 2000; }
|
||||
PCONFIG(0) = alt;
|
||||
PCONFIG_FLOAT(0) = getFormItemFloat(F("plugin_117_SCD30_tmp"));
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
case PLUGIN_INIT:
|
||||
{
|
||||
initPluginTaskData(event->TaskIndex, new (std::nothrow) P117_data_struct(PCONFIG(0), PCONFIG_FLOAT(0)));
|
||||
P117_data_struct *P117_data = static_cast<P117_data_struct *>(getPluginTaskData(event->TaskIndex));
|
||||
|
||||
if (nullptr == P117_data) {
|
||||
return success;
|
||||
}
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
case PLUGIN_READ:
|
||||
{
|
||||
P117_data_struct *P117_data = static_cast<P117_data_struct *>(getPluginTaskData(event->TaskIndex));
|
||||
|
||||
if (nullptr == P117_data) {
|
||||
return success;
|
||||
}
|
||||
|
||||
uint16_t scd30_CO2 = 0u;
|
||||
uint16_t scd30_CO2EAvg = 0u;
|
||||
float scd30_Humid = 0.0f;
|
||||
float scd30_Temp = 0.0f;
|
||||
|
||||
switch (P117_data->read_sensor(&scd30_CO2, &scd30_CO2EAvg, &scd30_Temp, &scd30_Humid))
|
||||
{
|
||||
case ERROR_SCD30_NO_ERROR:
|
||||
UserVar[event->BaseVarIndex] = scd30_CO2EAvg;
|
||||
UserVar[event->BaseVarIndex + 1] = scd30_Humid;
|
||||
UserVar[event->BaseVarIndex + 2] = scd30_Temp;
|
||||
UserVar[event->BaseVarIndex + 3] = scd30_CO2;
|
||||
|
||||
if (scd30_CO2EAvg > 5000)
|
||||
{
|
||||
addLog(LOG_LEVEL_INFO, F("SCD30: Sensor saturated! > 5000 ppm"));
|
||||
}
|
||||
break;
|
||||
case ERROR_SCD30_NO_DATA:
|
||||
case ERROR_SCD30_CRC_ERROR:
|
||||
case ERROR_SCD30_CO2_ZERO:
|
||||
break;
|
||||
default:
|
||||
{
|
||||
P117_data->softReset();
|
||||
break;
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
case PLUGIN_WRITE:
|
||||
{
|
||||
P117_data_struct *P117_data = static_cast<P117_data_struct *>(getPluginTaskData(event->TaskIndex));
|
||||
|
||||
if (nullptr == P117_data) {
|
||||
return success;
|
||||
}
|
||||
String command = parseString(string, 1);
|
||||
uint16_t value = 0;
|
||||
String log;
|
||||
float temp;
|
||||
|
||||
if (command.equals(F("scdgetabc")))
|
||||
{
|
||||
P117_data->getCalibrationType(&value);
|
||||
log += F("ABC: ");
|
||||
log += value;
|
||||
success = true;
|
||||
} else if (command.equals(F("scdgetalt")))
|
||||
{
|
||||
P117_data->getAltitudeCompensation(&value);
|
||||
log += F("Altitude: ");
|
||||
log += value;
|
||||
success = true;
|
||||
} else if (command.equals(F("scdgettmp")))
|
||||
{
|
||||
P117_data->getTemperatureOffset(&temp);
|
||||
log += F("Temp offset: ");
|
||||
log += String(temp, 2);
|
||||
success = true;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
SendStatus(event, log);
|
||||
} else {
|
||||
addLog(LOG_LEVEL_ERROR, F("SCD30: Unknown command."));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
#endif // ifdef USES_P117
|
||||
@@ -1137,6 +1137,7 @@ To create/register a plugin, you have to :
|
||||
#define USES_P094 // CUL Reader
|
||||
#define USES_P114 // VEML6075 UVA/UVB sensor
|
||||
#define USES_P115 // Fuel Gauge MAX1704x
|
||||
#define USES_P117 // SCD30
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1419,7 +1420,7 @@ To create/register a plugin, you have to :
|
||||
#define USES_P116 //
|
||||
#endif
|
||||
#ifndef USES_P117
|
||||
#define USES_P117 //
|
||||
#define USES_P117 // SCD30
|
||||
#endif
|
||||
#ifndef USES_P118
|
||||
#define USES_P118 //
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "../PluginStructs/P117_data_struct.h"
|
||||
|
||||
#ifdef USES_P117
|
||||
|
||||
// **************************************************************************/
|
||||
// Constructor
|
||||
// **************************************************************************/
|
||||
P117_data_struct::P117_data_struct(uint16_t altitude, float temperatureOffset)
|
||||
: _altitude(altitude), _temperatureOffset(temperatureOffset) {}
|
||||
|
||||
|
||||
// **************************************************************************/
|
||||
// Initialize sensor and read data from SCD30
|
||||
// **************************************************************************/
|
||||
uint32_t P117_data_struct::read_sensor(uint16_t *scd30_CO2, uint16_t *scd30_CO2EAvg, float *scd30_Temp, float *scd30_Humid) {
|
||||
if (!initialised) {
|
||||
initialised = init_sensor(); // Check id device is present
|
||||
}
|
||||
|
||||
if (initialised) {
|
||||
return scd30.readMeasurement(scd30_CO2, scd30_CO2EAvg, scd30_Temp, scd30_Humid);
|
||||
}
|
||||
return ERROR_SCD30_NO_DATA;
|
||||
}
|
||||
|
||||
// **************************************************************************/
|
||||
// Check SCD30 presence and initialize
|
||||
// **************************************************************************/
|
||||
bool P117_data_struct::softReset() {
|
||||
if (initialised) {
|
||||
scd30.softReset();
|
||||
}
|
||||
return initialised;
|
||||
}
|
||||
|
||||
// **************************************************************************/
|
||||
// Check SCD30 presence and initialize
|
||||
// **************************************************************************/
|
||||
bool P117_data_struct::init_sensor() {
|
||||
if (!initialised) {
|
||||
scd30.begin();
|
||||
uint16_t calibration = 0;
|
||||
scd30.getCalibrationType(&calibration);
|
||||
|
||||
if (calibration) {
|
||||
scd30.setManualCalibration();
|
||||
}
|
||||
scd30.beginMeasuring();
|
||||
scd30.setAltitudeCompensation(_altitude);
|
||||
scd30.setTemperatureOffset(_temperatureOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
return initialised;
|
||||
}
|
||||
|
||||
#endif // ifdef USES_P117
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef PLUGINSTRUCTS_P117_DATA_STRUCT_H
|
||||
#define PLUGINSTRUCTS_P117_DATA_STRUCT_H
|
||||
|
||||
#include "../../_Plugin_Helper.h"
|
||||
#ifdef USES_P117
|
||||
# include <FrogmoreScd30.h>
|
||||
|
||||
struct P117_data_struct : public PluginTaskData_base {
|
||||
public:
|
||||
|
||||
P117_data_struct(uint16_t altitude,
|
||||
float temperatureOffset);
|
||||
|
||||
P117_data_struct() = delete;
|
||||
|
||||
uint32_t read_sensor(uint16_t *scd30_CO2,
|
||||
uint16_t *scd30_CO2EAvg,
|
||||
float *scd30_Temp,
|
||||
float *scd30_Humid);
|
||||
bool softReset();
|
||||
void getCalibrationType(uint16_t *abc) {
|
||||
if (initialised) {
|
||||
scd30.getCalibrationType(abc);
|
||||
}
|
||||
}
|
||||
|
||||
void getAltitudeCompensation(uint16_t *altitude) {
|
||||
if (initialised) {
|
||||
scd30.getAltitudeCompensation(altitude);
|
||||
}
|
||||
}
|
||||
|
||||
void getTemperatureOffset(float *temperature) {
|
||||
if (initialised) {
|
||||
scd30.getTemperatureOffset(temperature);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
FrogmoreScd30 scd30;
|
||||
|
||||
bool init_sensor();
|
||||
|
||||
uint16_t _altitude;
|
||||
float _temperatureOffset;
|
||||
|
||||
bool initialised = false;
|
||||
};
|
||||
|
||||
#endif // ifdef USES_P117
|
||||
#endif // ifndef PLUGINSTRUCTS_P117_DATA_STRUCT_H
|
||||
@@ -265,7 +265,7 @@ String getKnownI2Cdevice(uint8_t address) {
|
||||
result += F("Adafruit Motorshield v2,SI1145");
|
||||
break;
|
||||
case 0x61:
|
||||
result += F("Atlas EZO DO");
|
||||
result += F("Atlas EZO DO,SCD30");
|
||||
break;
|
||||
case 0x62:
|
||||
result += F("Atlas EZO ORP");
|
||||
|
||||
Reference in New Issue
Block a user