diff --git a/BUILDS.md b/BUILDS.md index 9c02cce72305..f5767be54dbe 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -119,6 +119,7 @@ Note: `minimal` variant is not listed as it shouldn't be used outside of the [up | USE_MGS | - | - / x | - | x | - | - | | USE_SGP30 | - | - / x | - | x | - | - | | USE_SGP40 | - | - / x | - | x | - | - | +| USE_SGP4X | - | - / x | - | x | - | - | | USE_SEN5X | - | - / x | - | x | - | - | | USE_SI1145 | - | - / - | - | - | - | - | | USE_LM75AD | - | - / x | - | x | - | - | diff --git a/I2CDEVICES.md b/I2CDEVICES.md index b7e806eeae1d..2a51ad36c9fd 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -116,4 +116,5 @@ Index | Define | Driver | Device | Address(es) | Description 78 | USE_PMSA003I | xsns_104 | PMSA003I | 0x12 | PM2.5 Air Quality Sensor with I2C Interface 79 | USE_GDK101 | xsns_106 | GDK101 | 0x18 - 0x1B | Gamma Radiation Sensor 80 | USE_TC74 | xsns_108 | TC74 | 0x48 - 0x4F | Temperature sensor - 81 | USE_PCA9557 | xdrv_69 | PCA95xx | 0x18 - 0x1F | 8-bit I/O expander as virtual button/switch/relay \ No newline at end of file + 81 | USE_PCA9557 | xdrv_69 | PCA95xx | 0x18 - 0x1F | 8-bit I/O expander as virtual button/switch/relay + 82 | USE_SGP4X | xsns_109 | SGP4X | 0x59 | Gas (TVOC/NOx index) diff --git a/lib/lib_i2c/arduino-i2c-sgp41/CHANGELOG.md b/lib/lib_i2c/arduino-i2c-sgp41/CHANGELOG.md new file mode 100644 index 000000000000..b119c080dd42 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [0.1.0] - 2021-11-17 + +Initial release + +[0.1.0]: https://github.com/Sensirion/arduino-i2c-sgp41/releases/tag/0.1.0 + diff --git a/lib/lib_i2c/arduino-i2c-sgp41/LICENSE b/lib/lib_i2c/arduino-i2c-sgp41/LICENSE new file mode 100644 index 000000000000..ff20c41dfa2a --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, Sensirion AG +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/lib_i2c/arduino-i2c-sgp41/README.md b/lib/lib_i2c/arduino-i2c-sgp41/README.md new file mode 100644 index 000000000000..0a374464735d --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/README.md @@ -0,0 +1,78 @@ +# Sensirion I2C SGP41 Arduino Library + +This is the Sensirion SGP41 library for Arduino using the +modules I2C interface. + +[
](https://www.sensirion.com/en/environmental-sensors/gas-sensors/sgp41) + +Click [here](https://www.sensirion.com/en/environmental-sensors/gas-sensors/sgp41) to learn more about the SGP41 sensor. + + +# Installation + +To install, download the latest release as .zip file and add it to your +[Arduino IDE](http://www.arduino.cc/en/main/software) via + + Sketch => Include Library => Add .ZIP Library... + +Don't forget to **install the dependencies** listed below the same way via `Add +.ZIP Library` + +Note: Installation via the Arduino Library Manager is coming soon. + +# Dependencies + +* [Sensirion Core](https://github.com/Sensirion/arduino-core) + + +# Quick Start + +1. Connect the SGP41 Sensor to your Arduino board's standard + I2C bus. Check the pinout of your Arduino board to find the correct pins. + The pinout of the SGP41 Sensor board can be found in the + data sheet. + + * **VDD** of the SEK-SGP41 to the **3.3V** of your Arduino board + * **GND** of the SEK-SGP41 to the **GND** of your Arduino board + * **SCL** of the SEK-SGP41 to the **SCL** of your Arduino board + * **SDA** of the SEK-SGP41 to the **SDA** of your Arduino board + +2. Open the `exampleUsage` sample project within the Arduino IDE + + File => Examples => Sensirion I2C SGP41 => exampleUsage + +3. Click the `Upload` button in the Arduino IDE or + + Sketch => Upload + +4. When the upload process has finished, open the `Serial Monitor` or `Serial + Plotter` via the `Tools` menu to observe the measurement values. Note that + the `Baud Rate` in the corresponding window has to be set to `115200 baud`. + +# Contributing + +**Contributions are welcome!** + +We develop and test this driver using our company internal tools (version +control, continuous integration, code review etc.) and automatically +synchronize the master branch with GitHub. But this doesn't mean that we don't +respond to issues or don't accept pull requests on GitHub. In fact, you're very +welcome to open issues or create pull requests :) + +This Sensirion library uses +[`clang-format`](https://releases.llvm.org/download.html) to standardize the +formatting of all our `.cpp` and `.h` files. Make sure your contributions are +formatted accordingly: + +The `-i` flag will apply the format changes to the files listed. + +```bash +clang-format -i src/*.cpp src/*.h +``` + +Note that differences from this formatting will result in a failed build until +they are fixed. + +# License + +See [LICENSE](LICENSE). diff --git a/lib/lib_i2c/arduino-i2c-sgp41/examples/exampleUsage/exampleUsage.ino b/lib/lib_i2c/arduino-i2c-sgp41/examples/exampleUsage/exampleUsage.ino new file mode 100644 index 000000000000..3490dddb2077 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/examples/exampleUsage/exampleUsage.ino @@ -0,0 +1,124 @@ +/* + * I2C-Generator: 0.3.0 + * Yaml Version: 0.1.0 + * Template Version: 0.7.0-62-g3d691f9 + */ +/* + * Copyright (c) 2021, Sensirion AG + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +SensirionI2CSgp41 sgp41; + +// time in seconds needed for NOx conditioning +uint16_t conditioning_s = 10; + +void setup() { + + Serial.begin(115200); + while (!Serial) { + delay(100); + } + + Wire.begin(); + + uint16_t error; + char errorMessage[256]; + + sgp41.begin(Wire); + + uint16_t serialNumber[3]; + uint8_t serialNumberSize = 3; + + error = sgp41.getSerialNumber(serialNumber, serialNumberSize); + + if (error) { + Serial.print("Error trying to execute getSerialNumber(): "); + errorToString(error, errorMessage, 256); + Serial.println(errorMessage); + } else { + Serial.print("SerialNumber:"); + Serial.print("0x"); + for (size_t i = 0; i < serialNumberSize; i++) { + uint16_t value = serialNumber[i]; + Serial.print(value < 4096 ? "0" : ""); + Serial.print(value < 256 ? "0" : ""); + Serial.print(value < 16 ? "0" : ""); + Serial.print(value, HEX); + } + Serial.println(); + } + + uint16_t testResult; + error = sgp41.executeSelfTest(testResult); + if (error) { + Serial.print("Error trying to execute executeSelfTest(): "); + errorToString(error, errorMessage, 256); + Serial.println(errorMessage); + } else if (testResult != 0xD400) { + Serial.print("executeSelfTest failed with error: "); + Serial.println(testResult); + } +} + +void loop() { + uint16_t error; + char errorMessage[256]; + uint16_t defaultRh = 0x8000; + uint16_t defaultT = 0x6666; + uint16_t srawVoc = 0; + uint16_t srawNox = 0; + + delay(1000); + + if (conditioning_s > 0) { + // During NOx conditioning (10s) SRAW NOx will remain 0 + error = sgp41.executeConditioning(defaultRh, defaultT, srawVoc); + conditioning_s--; + } else { + // Read Measurement + error = sgp41.measureRawSignals(defaultRh, defaultT, srawVoc, srawNox); + } + + if (error) { + Serial.print("Error trying to execute measureRawSignals(): "); + errorToString(error, errorMessage, 256); + Serial.println(errorMessage); + } else { + Serial.print("SRAW_VOC:"); + Serial.print(srawVoc); + Serial.print("\t"); + Serial.print("SRAW_NOx:"); + Serial.println(srawNox); + } +} diff --git a/lib/lib_i2c/arduino-i2c-sgp41/images/SGP41.png b/lib/lib_i2c/arduino-i2c-sgp41/images/SGP41.png new file mode 100644 index 000000000000..a36e2d82e552 Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-sgp41/images/SGP41.png differ diff --git a/lib/lib_i2c/arduino-i2c-sgp41/keywords.txt b/lib/lib_i2c/arduino-i2c-sgp41/keywords.txt new file mode 100644 index 000000000000..13e6ba269768 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/keywords.txt @@ -0,0 +1,27 @@ +####################################### +# Syntax Coloring Map +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +SensirionI2CSgp41 KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### +executeConditioning KEYWORD2 +measureRawSignals KEYWORD2 +executeSelfTest KEYWORD2 +turnHeaterOff KEYWORD2 +getSerialNumber KEYWORD2 +####################################### +# Instances (KEYWORD2) +####################################### + +sgp41 KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### diff --git a/lib/lib_i2c/arduino-i2c-sgp41/library.properties b/lib/lib_i2c/arduino-i2c-sgp41/library.properties new file mode 100644 index 000000000000..5412354f9cf3 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/library.properties @@ -0,0 +1,10 @@ +name=Sensirion I2C SGP41 +version=0.1.0 +author=Sensirion +maintainer=Sensirion +sentence=Library for the SGP41 sensor family by Sensirion +paragraph=Enables you to use the SGP41 via I2C. +url=https://github.com/Sensirion/arduino-i2c-sgp41 +category=Sensors +depends=Sensirion Core +includes=SensirionI2CSgp41.h diff --git a/lib/lib_i2c/arduino-i2c-sgp41/src/SensirionI2CSgp4x.cpp b/lib/lib_i2c/arduino-i2c-sgp41/src/SensirionI2CSgp4x.cpp new file mode 100644 index 000000000000..7395d4a51055 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/src/SensirionI2CSgp4x.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2021, Sensirion AG + * Copyright (c) 2023, Andrew Klaus (Removing delay functions) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "SensirionI2CSgp4x.h" +#include "Arduino.h" +#include "SensirionCore.h" +#include + +#define SGP4X_I2C_ADDRESS 0x59 + +SensirionI2CSgp4x::SensirionI2CSgp4x() { +} + +void SensirionI2CSgp4x::begin(TwoWire& i2cBus) { + _i2cBus = &i2cBus; +} + +uint16_t SensirionI2CSgp4x::sendConditioningCmd(uint16_t defaultRh, + uint16_t defaultT) { + uint16_t error; + uint8_t buffer[8]; + SensirionI2CTxFrame txFrame = + SensirionI2CTxFrame::createWithUInt16Command(0x2612, buffer, 8); + + error = txFrame.addUInt16(defaultRh); + error |= txFrame.addUInt16(defaultT); + if (error) { + return error; + } + + error = SensirionI2CCommunication::sendFrame(SGP4X_I2C_ADDRESS, txFrame, + *_i2cBus); + return error; +} + +uint16_t SensirionI2CSgp4x::readConditioningValue(uint16_t& srawVoc){ + // This must run at least 50ms after initiateConditioning + uint16_t error; + uint8_t buffer[8]; + + SensirionI2CRxFrame rxFrame(buffer, 8); + error = SensirionI2CCommunication::receiveFrame(SGP4X_I2C_ADDRESS, 3, + rxFrame, *_i2cBus); + if (error) { + return error; + } + + error |= rxFrame.getUInt16(srawVoc); + return error; +} + +uint16_t SensirionI2CSgp4x::sendRawSignalsCmd(uint16_t relativeHumidity, + uint16_t temperature) { + uint16_t error; + uint8_t buffer[8]; + SensirionI2CTxFrame txFrame = + SensirionI2CTxFrame::createWithUInt16Command(0x2619, buffer, 8); + + error = txFrame.addUInt16(relativeHumidity); + error |= txFrame.addUInt16(temperature); + if (error) { + return error; + } + + error = SensirionI2CCommunication::sendFrame(SGP4X_I2C_ADDRESS, txFrame, + *_i2cBus); + return error; +} + +uint16_t SensirionI2CSgp4x::readRawSignalsValue(uint16_t& srawVoc, + uint16_t& srawNox) { + // This must run 50ms after initiateRawSignals + + uint16_t error; + uint8_t buffer[6]; + SensirionI2CRxFrame rxFrame(buffer, 6); + error = SensirionI2CCommunication::receiveFrame(SGP4X_I2C_ADDRESS, 6, + rxFrame, *_i2cBus); + if (error) { + return error; + } + + error |= rxFrame.getUInt16(srawVoc); + error |= rxFrame.getUInt16(srawNox); + return error; +} + +uint16_t SensirionI2CSgp4x::sendRawSignalCmd(uint16_t relativeHumidity, + uint16_t temperature) { + uint16_t error; + uint8_t buffer[8]; + SensirionI2CTxFrame txFrame = + SensirionI2CTxFrame::createWithUInt16Command(0x260F, buffer, 8); + + error = txFrame.addUInt16(relativeHumidity); + error |= txFrame.addUInt16(temperature); + + if (error) { + return error; + } + + error = SensirionI2CCommunication::sendFrame(SGP4X_I2C_ADDRESS, txFrame, + *_i2cBus); + return error; + +} + + +uint16_t SensirionI2CSgp4x::readRawSignalValue(uint16_t& srawVoc) { + uint16_t error; + uint8_t buffer[8]; + + SensirionI2CRxFrame rxFrame(buffer, 8); + error = SensirionI2CCommunication::receiveFrame(SGP4X_I2C_ADDRESS, 3, + rxFrame, *_i2cBus); + if (error) { + return error; + } + + error |= rxFrame.getUInt16(srawVoc); + return error; + +} + +uint16_t SensirionI2CSgp4x::sendSelfTestCmd() { + uint16_t error; + uint8_t buffer[3]; + SensirionI2CTxFrame txFrame = + SensirionI2CTxFrame::createWithUInt16Command(0x280E, buffer, 3); + + error = SensirionI2CCommunication::sendFrame(SGP4X_I2C_ADDRESS, txFrame, + *_i2cBus); + return error; +} + +uint16_t SensirionI2CSgp4x::readSelfTestValue(uint16_t& testResult) { + // Must run 320ms after initiateSelfTest + uint16_t error; + uint8_t buffer[3]; + + SensirionI2CRxFrame rxFrame(buffer, 3); + error = SensirionI2CCommunication::receiveFrame(SGP4X_I2C_ADDRESS, 3, + rxFrame, *_i2cBus); + if (error) { + return error; + } + + error |= rxFrame.getUInt16(testResult); + return error; +} + +uint16_t SensirionI2CSgp4x::turnHeaterOff() { + uint16_t error; + uint8_t buffer[2]; + SensirionI2CTxFrame txFrame = + SensirionI2CTxFrame::createWithUInt16Command(0x3615, buffer, 2); + + error = SensirionI2CCommunication::sendFrame(SGP4X_I2C_ADDRESS, txFrame, + *_i2cBus); + delay(1); + return error; +} + +uint16_t SensirionI2CSgp4x::getSerialNumber(uint16_t serialNumber[], + uint8_t serialNumberSize) { + uint16_t error; + uint8_t buffer[9]; + SensirionI2CTxFrame txFrame = + SensirionI2CTxFrame::createWithUInt16Command(0x3682, buffer, 9); + + error = SensirionI2CCommunication::sendFrame(SGP4X_I2C_ADDRESS, txFrame, + *_i2cBus); + if (error) { + return error; + } + + delay(1); + + SensirionI2CRxFrame rxFrame(buffer, 9); + error = SensirionI2CCommunication::receiveFrame(SGP4X_I2C_ADDRESS, 9, + rxFrame, *_i2cBus); + if (error) { + return error; + } + + error |= rxFrame.getUInt16(serialNumber[0]); + error |= rxFrame.getUInt16(serialNumber[1]); + error |= rxFrame.getUInt16(serialNumber[2]); + return error; +} diff --git a/lib/lib_i2c/arduino-i2c-sgp41/src/SensirionI2CSgp4x.h b/lib/lib_i2c/arduino-i2c-sgp41/src/SensirionI2CSgp4x.h new file mode 100644 index 000000000000..b03bed3f4f1f --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/src/SensirionI2CSgp4x.h @@ -0,0 +1,145 @@ +/* + * THIS FILE IS AUTOMATICALLY GENERATED + * + * I2C-Generator: 0.3.0 + * Yaml Version: 0.1.0 + * Template Version: 0.7.0-62-g3d691f9 + */ +/* + * Copyright (c) 2021, Sensirion AG + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef SENSIRIONI2CSGP4X_H +#define SENSIRIONI2CSGP4X_H + +#include + +#include + +class SensirionI2CSgp4x { + + public: + SensirionI2CSgp4x(); + /** + * begin() - Initializes the SensirionI2CSgp4x class. + * + * @param serial Arduino stream object to be communicated with. + * + */ + void begin(TwoWire& i2cBus); + + /** + * executeConditioning() - This command starts the conditioning, i.e., the + * VOC pixel will be operated at the same temperature as it is by calling + * the sgp41_measure_raw command while the NOx pixel will be operated at a + * different temperature for conditioning. This command returns only the + * measured raw signal of the VOC pixel SRAW_VOC as 2 bytes (+ 1 CRC byte). + * + * @param defaultRh Default conditions for relative humidty. + * + * @param defaultT Default conditions for temperature. + * + * @param srawVoc u16 unsigned integer directly provides the raw signal + * SRAW_VOC in ticks which is proportional to the logarithm of the + * resistance of the sensing element. + * + * @return 0 on success, an error code otherwise + */ + uint16_t sendConditioningCmd(uint16_t defaultRh, uint16_t defaultT); + + uint16_t readConditioningValue(uint16_t& srawVoc); + + /** + * measureRawSignals() - This command starts/continues the VOC+NOx + * measurement mode + * + * @param relativeHumidity Leaves humidity compensation disabled by sending + * the default value 0x8000 (50%RH) or enables humidity compensation when + * sending the relative humidity in ticks (ticks = %RH * 65535 / 100) + * + * @param temperature Leaves humidity compensation disabled by sending the + * default value 0x6666 (25 degC) or enables humidity compensation when + * sending the temperature in ticks (ticks = (degC + 45) * 65535 / 175) + * + * @param srawVoc u16 unsigned integer directly provides the raw signal + * SRAW_VOC in ticks which is proportional to the logarithm of the + * resistance of the sensing element. + * + * @param srawNox u16 unsigned integer directly provides the raw signal + * SRAW_NOX in ticks which is proportional to the logarithm of the + * resistance of the sensing element. + * + * @return 0 on success, an error code otherwise + */ + uint16_t sendRawSignalsCmd(uint16_t relativeHumidity, uint16_t temperature); + uint16_t readRawSignalsValue(uint16_t& srawVoc, uint16_t& srawNox); + + uint16_t sendRawSignalCmd(uint16_t relativeHumidity, uint16_t temperature); + uint16_t readRawSignalValue(uint16_t& srawVoc); + + /** + * executeSelfTest() - This command triggers the built-in self-test checking + * for integrity of both hotplate and MOX material and returns the result of + * this test as 2 bytes + * + * @param testResult 0xXX 0xYY: ignore most significant byte 0xXX. The four + * least significant bits of the least significant byte 0xYY provide + * information if the self-test has or has not passed for each individual + * pixel. All zero mean all tests passed successfully. Check the datasheet + * for more detailed information. + * + * @return 0 on success, an error code otherwise + */ + uint16_t sendSelfTestCmd(void); + uint16_t readSelfTestValue(uint16_t& testResult); + + /** + * turnHeaterOff() - This command turns the hotplate off and stops the + * measurement. Subsequently, the sensor enters the idle mode. + * + * @return 0 on success, an error code otherwise + */ + uint16_t turnHeaterOff(void); + + /** + * getSerialNumber() - This command provides the decimal serial number of + * the SGP41 chip by returning 3x2 bytes. + * + * @param serialNumber 48-bit unique serial number + * + * @return 0 on success, an error code otherwise + */ + uint16_t getSerialNumber(uint16_t serialNumber[], uint8_t serialNumberSize); + + private: + TwoWire* _i2cBus = nullptr; +}; + +#endif /* SENSIRIONI2CSGP4X_H */ diff --git a/lib/lib_i2c/arduino-i2c-sgp41/src/sensirion_gas_index_algorithm.c b/lib/lib_i2c/arduino-i2c-sgp41/src/sensirion_gas_index_algorithm.c new file mode 100644 index 000000000000..5ec3edeea2c3 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/src/sensirion_gas_index_algorithm.c @@ -0,0 +1,582 @@ +/* + * Copyright (c) 2022, Sensirion AG + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "sensirion_gas_index_algorithm.h" +#include + +static void GasIndexAlgorithm__init_instances(GasIndexAlgorithmParams* params); +static void GasIndexAlgorithm__mean_variance_estimator__set_parameters( + GasIndexAlgorithmParams* params); +static void GasIndexAlgorithm__mean_variance_estimator__set_states( + GasIndexAlgorithmParams* params, float mean, float std, float uptime_gamma); +static float GasIndexAlgorithm__mean_variance_estimator__get_std( + const GasIndexAlgorithmParams* params); +static float GasIndexAlgorithm__mean_variance_estimator__get_mean( + const GasIndexAlgorithmParams* params); +static bool GasIndexAlgorithm__mean_variance_estimator__is_initialized( + GasIndexAlgorithmParams* params); +static void GasIndexAlgorithm__mean_variance_estimator___calculate_gamma( + GasIndexAlgorithmParams* params); +static void GasIndexAlgorithm__mean_variance_estimator__process( + GasIndexAlgorithmParams* params, float sraw); +static void +GasIndexAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + GasIndexAlgorithmParams* params, float X0, float K); +static float GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + GasIndexAlgorithmParams* params, float sample); +static void +GasIndexAlgorithm__mox_model__set_parameters(GasIndexAlgorithmParams* params, + float SRAW_STD, float SRAW_MEAN); +static float +GasIndexAlgorithm__mox_model__process(GasIndexAlgorithmParams* params, + float sraw); +static void GasIndexAlgorithm__sigmoid_scaled__set_parameters( + GasIndexAlgorithmParams* params, float X0, float K, float offset_default); +static float +GasIndexAlgorithm__sigmoid_scaled__process(GasIndexAlgorithmParams* params, + float sample); +static void GasIndexAlgorithm__adaptive_lowpass__set_parameters( + GasIndexAlgorithmParams* params); +static float +GasIndexAlgorithm__adaptive_lowpass__process(GasIndexAlgorithmParams* params, + float sample); + +void GasIndexAlgorithm_init_with_sampling_interval( + GasIndexAlgorithmParams* params, int32_t algorithm_type, + float sampling_interval) { + params->mAlgorithm_Type = algorithm_type; + params->mSamplingInterval = sampling_interval; + if ((algorithm_type == GasIndexAlgorithm_ALGORITHM_TYPE_NOX)) { + params->mIndex_Offset = GasIndexAlgorithm_NOX_INDEX_OFFSET_DEFAULT; + params->mSraw_Minimum = GasIndexAlgorithm_NOX_SRAW_MINIMUM; + params->mGating_Max_Duration_Minutes = + GasIndexAlgorithm_GATING_NOX_MAX_DURATION_MINUTES; + params->mInit_Duration_Mean = GasIndexAlgorithm_INIT_DURATION_MEAN_NOX; + params->mInit_Duration_Variance = + GasIndexAlgorithm_INIT_DURATION_VARIANCE_NOX; + params->mGating_Threshold = GasIndexAlgorithm_GATING_THRESHOLD_NOX; + } else { + params->mIndex_Offset = GasIndexAlgorithm_VOC_INDEX_OFFSET_DEFAULT; + params->mSraw_Minimum = GasIndexAlgorithm_VOC_SRAW_MINIMUM; + params->mGating_Max_Duration_Minutes = + GasIndexAlgorithm_GATING_VOC_MAX_DURATION_MINUTES; + params->mInit_Duration_Mean = GasIndexAlgorithm_INIT_DURATION_MEAN_VOC; + params->mInit_Duration_Variance = + GasIndexAlgorithm_INIT_DURATION_VARIANCE_VOC; + params->mGating_Threshold = GasIndexAlgorithm_GATING_THRESHOLD_VOC; + } + params->mIndex_Gain = GasIndexAlgorithm_INDEX_GAIN; + params->mTau_Mean_Hours = GasIndexAlgorithm_TAU_MEAN_HOURS; + params->mTau_Variance_Hours = GasIndexAlgorithm_TAU_VARIANCE_HOURS; + params->mSraw_Std_Initial = GasIndexAlgorithm_SRAW_STD_INITIAL; + GasIndexAlgorithm_reset(params); +} + +void GasIndexAlgorithm_init(GasIndexAlgorithmParams* params, + int32_t algorithm_type) { + GasIndexAlgorithm_init_with_sampling_interval( + params, algorithm_type, GasIndexAlgorithm_DEFAULT_SAMPLING_INTERVAL); +} + +void GasIndexAlgorithm_reset(GasIndexAlgorithmParams* params) { + params->mUptime = 0.f; + params->mSraw = 0.f; + params->mGas_Index = 0; + GasIndexAlgorithm__init_instances(params); +} + +static void GasIndexAlgorithm__init_instances(GasIndexAlgorithmParams* params) { + + GasIndexAlgorithm__mean_variance_estimator__set_parameters(params); + GasIndexAlgorithm__mox_model__set_parameters( + params, GasIndexAlgorithm__mean_variance_estimator__get_std(params), + GasIndexAlgorithm__mean_variance_estimator__get_mean(params)); + if ((params->mAlgorithm_Type == GasIndexAlgorithm_ALGORITHM_TYPE_NOX)) { + GasIndexAlgorithm__sigmoid_scaled__set_parameters( + params, GasIndexAlgorithm_SIGMOID_X0_NOX, + GasIndexAlgorithm_SIGMOID_K_NOX, + GasIndexAlgorithm_NOX_INDEX_OFFSET_DEFAULT); + } else { + GasIndexAlgorithm__sigmoid_scaled__set_parameters( + params, GasIndexAlgorithm_SIGMOID_X0_VOC, + GasIndexAlgorithm_SIGMOID_K_VOC, + GasIndexAlgorithm_VOC_INDEX_OFFSET_DEFAULT); + } + GasIndexAlgorithm__adaptive_lowpass__set_parameters(params); +} + +void GasIndexAlgorithm_get_sampling_interval( + const GasIndexAlgorithmParams* params, float* sampling_interval) { + *sampling_interval = params->mSamplingInterval; +} + +void GasIndexAlgorithm_get_states(const GasIndexAlgorithmParams* params, + float* state0, float* state1) { + + *state0 = GasIndexAlgorithm__mean_variance_estimator__get_mean(params); + *state1 = GasIndexAlgorithm__mean_variance_estimator__get_std(params); + return; +} + +void GasIndexAlgorithm_set_states(GasIndexAlgorithmParams* params, float state0, + float state1) { + + GasIndexAlgorithm__mean_variance_estimator__set_states( + params, state0, state1, GasIndexAlgorithm_PERSISTENCE_UPTIME_GAMMA); + GasIndexAlgorithm__mox_model__set_parameters( + params, GasIndexAlgorithm__mean_variance_estimator__get_std(params), + GasIndexAlgorithm__mean_variance_estimator__get_mean(params)); + params->mSraw = state0; +} + +void GasIndexAlgorithm_set_tuning_parameters( + GasIndexAlgorithmParams* params, int32_t index_offset, + int32_t learning_time_offset_hours, int32_t learning_time_gain_hours, + int32_t gating_max_duration_minutes, int32_t std_initial, + int32_t gain_factor) { + + params->mIndex_Offset = ((float)(index_offset)); + params->mTau_Mean_Hours = ((float)(learning_time_offset_hours)); + params->mTau_Variance_Hours = ((float)(learning_time_gain_hours)); + params->mGating_Max_Duration_Minutes = + ((float)(gating_max_duration_minutes)); + params->mSraw_Std_Initial = ((float)(std_initial)); + params->mIndex_Gain = ((float)(gain_factor)); + GasIndexAlgorithm__init_instances(params); +} + +void GasIndexAlgorithm_get_tuning_parameters( + const GasIndexAlgorithmParams* params, int32_t* index_offset, + int32_t* learning_time_offset_hours, int32_t* learning_time_gain_hours, + int32_t* gating_max_duration_minutes, int32_t* std_initial, + int32_t* gain_factor) { + + *index_offset = ((int32_t)(params->mIndex_Offset)); + *learning_time_offset_hours = ((int32_t)(params->mTau_Mean_Hours)); + *learning_time_gain_hours = ((int32_t)(params->mTau_Variance_Hours)); + *gating_max_duration_minutes = + ((int32_t)(params->mGating_Max_Duration_Minutes)); + *std_initial = ((int32_t)(params->mSraw_Std_Initial)); + *gain_factor = ((int32_t)(params->mIndex_Gain)); + return; +} + +void GasIndexAlgorithm_process(GasIndexAlgorithmParams* params, int32_t sraw, + int32_t* gas_index) { + + if ((params->mUptime <= GasIndexAlgorithm_INITIAL_BLACKOUT)) { + params->mUptime = (params->mUptime + params->mSamplingInterval); + } else { + if (((sraw > 0) && (sraw < 65000))) { + if ((sraw < (params->mSraw_Minimum + 1))) { + sraw = (params->mSraw_Minimum + 1); + } else if ((sraw > (params->mSraw_Minimum + 32767))) { + sraw = (params->mSraw_Minimum + 32767); + } + params->mSraw = ((float)((sraw - params->mSraw_Minimum))); + } + if (((params->mAlgorithm_Type == + GasIndexAlgorithm_ALGORITHM_TYPE_VOC) || + GasIndexAlgorithm__mean_variance_estimator__is_initialized( + params))) { + params->mGas_Index = + GasIndexAlgorithm__mox_model__process(params, params->mSraw); + params->mGas_Index = GasIndexAlgorithm__sigmoid_scaled__process( + params, params->mGas_Index); + } else { + params->mGas_Index = params->mIndex_Offset; + } + params->mGas_Index = GasIndexAlgorithm__adaptive_lowpass__process( + params, params->mGas_Index); + if ((params->mGas_Index < 0.5f)) { + params->mGas_Index = 0.5f; + } + if ((params->mSraw > 0.f)) { + GasIndexAlgorithm__mean_variance_estimator__process(params, + params->mSraw); + GasIndexAlgorithm__mox_model__set_parameters( + params, + GasIndexAlgorithm__mean_variance_estimator__get_std(params), + GasIndexAlgorithm__mean_variance_estimator__get_mean(params)); + } + } + *gas_index = ((int32_t)((params->mGas_Index + 0.5f))); + return; +} + +static void GasIndexAlgorithm__mean_variance_estimator__set_parameters( + GasIndexAlgorithmParams* params) { + + params->m_Mean_Variance_Estimator___Initialized = false; + params->m_Mean_Variance_Estimator___Mean = 0.f; + params->m_Mean_Variance_Estimator___Sraw_Offset = 0.f; + params->m_Mean_Variance_Estimator___Std = params->mSraw_Std_Initial; + params->m_Mean_Variance_Estimator___Gamma_Mean = + (((GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__ADDITIONAL_GAMMA_MEAN_SCALING * + GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING) * + (params->mSamplingInterval / 3600.f)) / + (params->mTau_Mean_Hours + (params->mSamplingInterval / 3600.f))); + params->m_Mean_Variance_Estimator___Gamma_Variance = + ((GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + (params->mSamplingInterval / 3600.f)) / + (params->mTau_Variance_Hours + (params->mSamplingInterval / 3600.f))); + if ((params->mAlgorithm_Type == GasIndexAlgorithm_ALGORITHM_TYPE_NOX)) { + params->m_Mean_Variance_Estimator___Gamma_Initial_Mean = + (((GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__ADDITIONAL_GAMMA_MEAN_SCALING * + GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING) * + params->mSamplingInterval) / + (GasIndexAlgorithm_TAU_INITIAL_MEAN_NOX + + params->mSamplingInterval)); + } else { + params->m_Mean_Variance_Estimator___Gamma_Initial_Mean = + (((GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__ADDITIONAL_GAMMA_MEAN_SCALING * + GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING) * + params->mSamplingInterval) / + (GasIndexAlgorithm_TAU_INITIAL_MEAN_VOC + + params->mSamplingInterval)); + } + params->m_Mean_Variance_Estimator___Gamma_Initial_Variance = + ((GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + params->mSamplingInterval) / + (GasIndexAlgorithm_TAU_INITIAL_VARIANCE + params->mSamplingInterval)); + params->m_Mean_Variance_Estimator__Gamma_Mean = 0.f; + params->m_Mean_Variance_Estimator__Gamma_Variance = 0.f; + params->m_Mean_Variance_Estimator___Uptime_Gamma = 0.f; + params->m_Mean_Variance_Estimator___Uptime_Gating = 0.f; + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = 0.f; +} + +static void GasIndexAlgorithm__mean_variance_estimator__set_states( + GasIndexAlgorithmParams* params, float mean, float std, + float uptime_gamma) { + + params->m_Mean_Variance_Estimator___Mean = mean; + params->m_Mean_Variance_Estimator___Std = std; + params->m_Mean_Variance_Estimator___Uptime_Gamma = uptime_gamma; + params->m_Mean_Variance_Estimator___Initialized = true; +} + +static float GasIndexAlgorithm__mean_variance_estimator__get_std( + const GasIndexAlgorithmParams* params) { + + return params->m_Mean_Variance_Estimator___Std; +} + +static float GasIndexAlgorithm__mean_variance_estimator__get_mean( + const GasIndexAlgorithmParams* params) { + + return (params->m_Mean_Variance_Estimator___Mean + + params->m_Mean_Variance_Estimator___Sraw_Offset); +} + +static bool GasIndexAlgorithm__mean_variance_estimator__is_initialized( + GasIndexAlgorithmParams* params) { + + return params->m_Mean_Variance_Estimator___Initialized; +} + +static void GasIndexAlgorithm__mean_variance_estimator___calculate_gamma( + GasIndexAlgorithmParams* params) { + + float uptime_limit; + float sigmoid_gamma_mean; + float gamma_mean; + float gating_threshold_mean; + float sigmoid_gating_mean; + float sigmoid_gamma_variance; + float gamma_variance; + float gating_threshold_variance; + float sigmoid_gating_variance; + + uptime_limit = (GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__FIX16_MAX - + params->mSamplingInterval); + if ((params->m_Mean_Variance_Estimator___Uptime_Gamma < uptime_limit)) { + params->m_Mean_Variance_Estimator___Uptime_Gamma = + (params->m_Mean_Variance_Estimator___Uptime_Gamma + + params->mSamplingInterval); + } + if ((params->m_Mean_Variance_Estimator___Uptime_Gating < uptime_limit)) { + params->m_Mean_Variance_Estimator___Uptime_Gating = + (params->m_Mean_Variance_Estimator___Uptime_Gating + + params->mSamplingInterval); + } + GasIndexAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, params->mInit_Duration_Mean, + GasIndexAlgorithm_INIT_TRANSITION_MEAN); + sigmoid_gamma_mean = + GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_mean = (params->m_Mean_Variance_Estimator___Gamma_Mean + + ((params->m_Mean_Variance_Estimator___Gamma_Initial_Mean - + params->m_Mean_Variance_Estimator___Gamma_Mean) * + sigmoid_gamma_mean)); + gating_threshold_mean = + (params->mGating_Threshold + + ((GasIndexAlgorithm_GATING_THRESHOLD_INITIAL - + params->mGating_Threshold) * + GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating))); + GasIndexAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, gating_threshold_mean, + GasIndexAlgorithm_GATING_THRESHOLD_TRANSITION); + sigmoid_gating_mean = + GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->mGas_Index); + params->m_Mean_Variance_Estimator__Gamma_Mean = + (sigmoid_gating_mean * gamma_mean); + GasIndexAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, params->mInit_Duration_Variance, + GasIndexAlgorithm_INIT_TRANSITION_VARIANCE); + sigmoid_gamma_variance = + GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_variance = + (params->m_Mean_Variance_Estimator___Gamma_Variance + + ((params->m_Mean_Variance_Estimator___Gamma_Initial_Variance - + params->m_Mean_Variance_Estimator___Gamma_Variance) * + (sigmoid_gamma_variance - sigmoid_gamma_mean))); + gating_threshold_variance = + (params->mGating_Threshold + + ((GasIndexAlgorithm_GATING_THRESHOLD_INITIAL - + params->mGating_Threshold) * + GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating))); + GasIndexAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, gating_threshold_variance, + GasIndexAlgorithm_GATING_THRESHOLD_TRANSITION); + sigmoid_gating_variance = + GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->mGas_Index); + params->m_Mean_Variance_Estimator__Gamma_Variance = + (sigmoid_gating_variance * gamma_variance); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = + (params->m_Mean_Variance_Estimator___Gating_Duration_Minutes + + ((params->mSamplingInterval / 60.f) * + (((1.f - sigmoid_gating_mean) * + (1.f + GasIndexAlgorithm_GATING_MAX_RATIO)) - + GasIndexAlgorithm_GATING_MAX_RATIO))); + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes < 0.f)) { + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = 0.f; + } + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes > + params->mGating_Max_Duration_Minutes)) { + params->m_Mean_Variance_Estimator___Uptime_Gating = 0.f; + } +} + +static void GasIndexAlgorithm__mean_variance_estimator__process( + GasIndexAlgorithmParams* params, float sraw) { + + float delta_sgp; + float c; + float additional_scaling; + + if ((params->m_Mean_Variance_Estimator___Initialized == false)) { + params->m_Mean_Variance_Estimator___Initialized = true; + params->m_Mean_Variance_Estimator___Sraw_Offset = sraw; + params->m_Mean_Variance_Estimator___Mean = 0.f; + } else { + if (((params->m_Mean_Variance_Estimator___Mean >= 100.f) || + (params->m_Mean_Variance_Estimator___Mean <= -100.f))) { + params->m_Mean_Variance_Estimator___Sraw_Offset = + (params->m_Mean_Variance_Estimator___Sraw_Offset + + params->m_Mean_Variance_Estimator___Mean); + params->m_Mean_Variance_Estimator___Mean = 0.f; + } + sraw = (sraw - params->m_Mean_Variance_Estimator___Sraw_Offset); + GasIndexAlgorithm__mean_variance_estimator___calculate_gamma(params); + delta_sgp = ((sraw - params->m_Mean_Variance_Estimator___Mean) / + GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING); + if ((delta_sgp < 0.f)) { + c = (params->m_Mean_Variance_Estimator___Std - delta_sgp); + } else { + c = (params->m_Mean_Variance_Estimator___Std + delta_sgp); + } + additional_scaling = 1.f; + if ((c > 1440.f)) { + additional_scaling = ((c / 1440.f) * (c / 1440.f)); + } + params->m_Mean_Variance_Estimator___Std = + (sqrtf((additional_scaling * + (GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING - + params->m_Mean_Variance_Estimator__Gamma_Variance))) * + sqrtf( + ((params->m_Mean_Variance_Estimator___Std * + (params->m_Mean_Variance_Estimator___Std / + (GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + additional_scaling))) + + (((params->m_Mean_Variance_Estimator__Gamma_Variance * + delta_sgp) / + additional_scaling) * + delta_sgp)))); + params->m_Mean_Variance_Estimator___Mean = + (params->m_Mean_Variance_Estimator___Mean + + ((params->m_Mean_Variance_Estimator__Gamma_Mean * delta_sgp) / + GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__ADDITIONAL_GAMMA_MEAN_SCALING)); + } +} + +static void +GasIndexAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + GasIndexAlgorithmParams* params, float X0, float K) { + + params->m_Mean_Variance_Estimator___Sigmoid__K = K; + params->m_Mean_Variance_Estimator___Sigmoid__X0 = X0; +} + +static float GasIndexAlgorithm__mean_variance_estimator___sigmoid__process( + GasIndexAlgorithmParams* params, float sample) { + + float x; + + x = (params->m_Mean_Variance_Estimator___Sigmoid__K * + (sample - params->m_Mean_Variance_Estimator___Sigmoid__X0)); + if ((x < -50.f)) { + return 1.f; + } else if ((x > 50.f)) { + return 0.f; + } else { + return (1.f / (1.f + expf(x))); + } +} + +static void +GasIndexAlgorithm__mox_model__set_parameters(GasIndexAlgorithmParams* params, + float SRAW_STD, float SRAW_MEAN) { + + params->m_Mox_Model__Sraw_Std = SRAW_STD; + params->m_Mox_Model__Sraw_Mean = SRAW_MEAN; +} + +static float +GasIndexAlgorithm__mox_model__process(GasIndexAlgorithmParams* params, + float sraw) { + + if ((params->mAlgorithm_Type == GasIndexAlgorithm_ALGORITHM_TYPE_NOX)) { + return (((sraw - params->m_Mox_Model__Sraw_Mean) / + GasIndexAlgorithm_SRAW_STD_NOX) * + params->mIndex_Gain); + } else { + return (((sraw - params->m_Mox_Model__Sraw_Mean) / + (-1.f * (params->m_Mox_Model__Sraw_Std + + GasIndexAlgorithm_SRAW_STD_BONUS_VOC))) * + params->mIndex_Gain); + } +} + +static void GasIndexAlgorithm__sigmoid_scaled__set_parameters( + GasIndexAlgorithmParams* params, float X0, float K, float offset_default) { + + params->m_Sigmoid_Scaled__K = K; + params->m_Sigmoid_Scaled__X0 = X0; + params->m_Sigmoid_Scaled__Offset_Default = offset_default; +} + +static float +GasIndexAlgorithm__sigmoid_scaled__process(GasIndexAlgorithmParams* params, + float sample) { + + float x; + float shift; + + x = (params->m_Sigmoid_Scaled__K * (sample - params->m_Sigmoid_Scaled__X0)); + if ((x < -50.f)) { + return GasIndexAlgorithm_SIGMOID_L; + } else if ((x > 50.f)) { + return 0.f; + } else { + if ((sample >= 0.f)) { + if ((params->m_Sigmoid_Scaled__Offset_Default == 1.f)) { + shift = ((500.f / 499.f) * (1.f - params->mIndex_Offset)); + } else { + shift = ((GasIndexAlgorithm_SIGMOID_L - + (5.f * params->mIndex_Offset)) / + 4.f); + } + return (((GasIndexAlgorithm_SIGMOID_L + shift) / (1.f + expf(x))) - + shift); + } else { + return ((params->mIndex_Offset / + params->m_Sigmoid_Scaled__Offset_Default) * + (GasIndexAlgorithm_SIGMOID_L / (1.f + expf(x)))); + } + } +} + +static void GasIndexAlgorithm__adaptive_lowpass__set_parameters( + GasIndexAlgorithmParams* params) { + + params->m_Adaptive_Lowpass__A1 = + (params->mSamplingInterval / + (GasIndexAlgorithm_LP_TAU_FAST + params->mSamplingInterval)); + params->m_Adaptive_Lowpass__A2 = + (params->mSamplingInterval / + (GasIndexAlgorithm_LP_TAU_SLOW + params->mSamplingInterval)); + params->m_Adaptive_Lowpass___Initialized = false; +} + +static float +GasIndexAlgorithm__adaptive_lowpass__process(GasIndexAlgorithmParams* params, + float sample) { + + float abs_delta; + float F1; + float tau_a; + float a3; + + if ((params->m_Adaptive_Lowpass___Initialized == false)) { + params->m_Adaptive_Lowpass___X1 = sample; + params->m_Adaptive_Lowpass___X2 = sample; + params->m_Adaptive_Lowpass___X3 = sample; + params->m_Adaptive_Lowpass___Initialized = true; + } + params->m_Adaptive_Lowpass___X1 = + (((1.f - params->m_Adaptive_Lowpass__A1) * + params->m_Adaptive_Lowpass___X1) + + (params->m_Adaptive_Lowpass__A1 * sample)); + params->m_Adaptive_Lowpass___X2 = + (((1.f - params->m_Adaptive_Lowpass__A2) * + params->m_Adaptive_Lowpass___X2) + + (params->m_Adaptive_Lowpass__A2 * sample)); + abs_delta = + (params->m_Adaptive_Lowpass___X1 - params->m_Adaptive_Lowpass___X2); + if ((abs_delta < 0.f)) { + abs_delta = (-1.f * abs_delta); + } + F1 = expf((GasIndexAlgorithm_LP_ALPHA * abs_delta)); + tau_a = (((GasIndexAlgorithm_LP_TAU_SLOW - GasIndexAlgorithm_LP_TAU_FAST) * + F1) + + GasIndexAlgorithm_LP_TAU_FAST); + a3 = (params->mSamplingInterval / (params->mSamplingInterval + tau_a)); + params->m_Adaptive_Lowpass___X3 = + (((1.f - a3) * params->m_Adaptive_Lowpass___X3) + (a3 * sample)); + return params->m_Adaptive_Lowpass___X3; +} \ No newline at end of file diff --git a/lib/lib_i2c/arduino-i2c-sgp41/src/sensirion_gas_index_algorithm.h b/lib/lib_i2c/arduino-i2c-sgp41/src/sensirion_gas_index_algorithm.h new file mode 100644 index 000000000000..0bd584bb9376 --- /dev/null +++ b/lib/lib_i2c/arduino-i2c-sgp41/src/sensirion_gas_index_algorithm.h @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2022, Sensirion AG + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GASINDEXALGORITHM_H_ +#define GASINDEXALGORITHM_H_ + +#include + +#ifndef __cplusplus + +#if __STDC_VERSION__ >= 199901L +#include +#else + +#ifndef bool +#define bool int +#define true 1 +#define false 0 +#endif // bool + +#endif // __STDC_VERSION__ + +#endif // __cplusplus + +// Should be set by the building toolchain +#ifndef LIBRARY_VERSION_NAME +#define LIBRARY_VERSION_NAME "3.2.0" +#endif + +#define GasIndexAlgorithm_ALGORITHM_TYPE_VOC (0) +#define GasIndexAlgorithm_ALGORITHM_TYPE_NOX (1) +#define GasIndexAlgorithm_DEFAULT_SAMPLING_INTERVAL (1.f) +#define GasIndexAlgorithm_INITIAL_BLACKOUT (45.f) +#define GasIndexAlgorithm_INDEX_GAIN (230.f) +#define GasIndexAlgorithm_SRAW_STD_INITIAL (50.f) +#define GasIndexAlgorithm_SRAW_STD_BONUS_VOC (220.f) +#define GasIndexAlgorithm_SRAW_STD_NOX (2000.f) +#define GasIndexAlgorithm_TAU_MEAN_HOURS (12.f) +#define GasIndexAlgorithm_TAU_VARIANCE_HOURS (12.f) +#define GasIndexAlgorithm_TAU_INITIAL_MEAN_VOC (20.f) +#define GasIndexAlgorithm_TAU_INITIAL_MEAN_NOX (1200.f) +#define GasIndexAlgorithm_INIT_DURATION_MEAN_VOC ((3600.f * 0.75f)) +#define GasIndexAlgorithm_INIT_DURATION_MEAN_NOX ((3600.f * 4.75f)) +#define GasIndexAlgorithm_INIT_TRANSITION_MEAN (0.01f) +#define GasIndexAlgorithm_TAU_INITIAL_VARIANCE (2500.f) +#define GasIndexAlgorithm_INIT_DURATION_VARIANCE_VOC ((3600.f * 1.45f)) +#define GasIndexAlgorithm_INIT_DURATION_VARIANCE_NOX ((3600.f * 5.70f)) +#define GasIndexAlgorithm_INIT_TRANSITION_VARIANCE (0.01f) +#define GasIndexAlgorithm_GATING_THRESHOLD_VOC (340.f) +#define GasIndexAlgorithm_GATING_THRESHOLD_NOX (30.f) +#define GasIndexAlgorithm_GATING_THRESHOLD_INITIAL (510.f) +#define GasIndexAlgorithm_GATING_THRESHOLD_TRANSITION (0.09f) +#define GasIndexAlgorithm_GATING_VOC_MAX_DURATION_MINUTES ((60.f * 3.f)) +#define GasIndexAlgorithm_GATING_NOX_MAX_DURATION_MINUTES ((60.f * 12.f)) +#define GasIndexAlgorithm_GATING_MAX_RATIO (0.3f) +#define GasIndexAlgorithm_SIGMOID_L (500.f) +#define GasIndexAlgorithm_SIGMOID_K_VOC (-0.0065f) +#define GasIndexAlgorithm_SIGMOID_X0_VOC (213.f) +#define GasIndexAlgorithm_SIGMOID_K_NOX (-0.0101f) +#define GasIndexAlgorithm_SIGMOID_X0_NOX (614.f) +#define GasIndexAlgorithm_VOC_INDEX_OFFSET_DEFAULT (100.f) +#define GasIndexAlgorithm_NOX_INDEX_OFFSET_DEFAULT (1.f) +#define GasIndexAlgorithm_LP_TAU_FAST (20.0f) +#define GasIndexAlgorithm_LP_TAU_SLOW (500.0f) +#define GasIndexAlgorithm_LP_ALPHA (-0.2f) +#define GasIndexAlgorithm_VOC_SRAW_MINIMUM (20000) +#define GasIndexAlgorithm_NOX_SRAW_MINIMUM (10000) +#define GasIndexAlgorithm_PERSISTENCE_UPTIME_GAMMA ((3.f * 3600.f)) +#define GasIndexAlgorithm_TUNING_INDEX_OFFSET_MIN (1) +#define GasIndexAlgorithm_TUNING_INDEX_OFFSET_MAX (250) +#define GasIndexAlgorithm_TUNING_LEARNING_TIME_OFFSET_HOURS_MIN (1) +#define GasIndexAlgorithm_TUNING_LEARNING_TIME_OFFSET_HOURS_MAX (1000) +#define GasIndexAlgorithm_TUNING_LEARNING_TIME_GAIN_HOURS_MIN (1) +#define GasIndexAlgorithm_TUNING_LEARNING_TIME_GAIN_HOURS_MAX (1000) +#define GasIndexAlgorithm_TUNING_GATING_MAX_DURATION_MINUTES_MIN (0) +#define GasIndexAlgorithm_TUNING_GATING_MAX_DURATION_MINUTES_MAX (3000) +#define GasIndexAlgorithm_TUNING_STD_INITIAL_MIN (10) +#define GasIndexAlgorithm_TUNING_STD_INITIAL_MAX (5000) +#define GasIndexAlgorithm_TUNING_GAIN_FACTOR_MIN (1) +#define GasIndexAlgorithm_TUNING_GAIN_FACTOR_MAX (1000) +#define GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING (64.f) +#define GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__ADDITIONAL_GAMMA_MEAN_SCALING \ + (8.f) +#define GasIndexAlgorithm_MEAN_VARIANCE_ESTIMATOR__FIX16_MAX (32767.f) + +/** + * Struct to hold all parameters and states of the gas algorithm. + */ +typedef struct { + int mAlgorithm_Type; + float mSamplingInterval; + float mIndex_Offset; + int32_t mSraw_Minimum; + float mGating_Max_Duration_Minutes; + float mInit_Duration_Mean; + float mInit_Duration_Variance; + float mGating_Threshold; + float mIndex_Gain; + float mTau_Mean_Hours; + float mTau_Variance_Hours; + float mSraw_Std_Initial; + float mUptime; + float mSraw; + float mGas_Index; + bool m_Mean_Variance_Estimator___Initialized; + float m_Mean_Variance_Estimator___Mean; + float m_Mean_Variance_Estimator___Sraw_Offset; + float m_Mean_Variance_Estimator___Std; + float m_Mean_Variance_Estimator___Gamma_Mean; + float m_Mean_Variance_Estimator___Gamma_Variance; + float m_Mean_Variance_Estimator___Gamma_Initial_Mean; + float m_Mean_Variance_Estimator___Gamma_Initial_Variance; + float m_Mean_Variance_Estimator__Gamma_Mean; + float m_Mean_Variance_Estimator__Gamma_Variance; + float m_Mean_Variance_Estimator___Uptime_Gamma; + float m_Mean_Variance_Estimator___Uptime_Gating; + float m_Mean_Variance_Estimator___Gating_Duration_Minutes; + float m_Mean_Variance_Estimator___Sigmoid__K; + float m_Mean_Variance_Estimator___Sigmoid__X0; + float m_Mox_Model__Sraw_Std; + float m_Mox_Model__Sraw_Mean; + float m_Sigmoid_Scaled__K; + float m_Sigmoid_Scaled__X0; + float m_Sigmoid_Scaled__Offset_Default; + float m_Adaptive_Lowpass__A1; + float m_Adaptive_Lowpass__A2; + bool m_Adaptive_Lowpass___Initialized; + float m_Adaptive_Lowpass___X1; + float m_Adaptive_Lowpass___X2; + float m_Adaptive_Lowpass___X3; +} GasIndexAlgorithmParams; + +/** + * Initialize the gas index algorithm parameters for the specified algorithm + * type and reset its internal states. Call this once at the beginning. + * @param params Pointer to the GasIndexAlgorithmParams struct + * @param algorithm_type 0 (GasIndexAlgorithm_ALGORITHM_TYPE_VOC) for VOC or + * 1 (GasIndexAlgorithm_ALGORITHM_TYPE_NOX) for NOx + */ +void GasIndexAlgorithm_init(GasIndexAlgorithmParams* params, + int32_t algorithm_type); + +/** + * Initialize the gas index algorithm parameters for the specified algorithm + * type and reset its internal states. Call this once at the beginning. + * @param params Pointer to the GasIndexAlgorithmParams struct + * @param algorithm_type 0 (GasIndexAlgorithm_ALGORITHM_TYPE_VOC) for VOC or + * 1 (GasIndexAlgorithm_ALGORITHM_TYPE_NOX) for NOx + * @param sampling_interval The sampling interval in seconds the algorithm is + * called. Tested for 1s and 10s. + */ +void GasIndexAlgorithm_init_with_sampling_interval( + GasIndexAlgorithmParams* params, int32_t algorithm_type, + float sampling_interval); + +/** + * Reset the internal states of the gas index algorithm. Previously set tuning + * parameters are preserved. Call this when resuming operation after a + * measurement interruption. + * @param params Pointer to the GasIndexAlgorithmParams struct + */ +void GasIndexAlgorithm_reset(GasIndexAlgorithmParams* params); + +/** + * Get current algorithm states. Retrieved values can be used in + * GasIndexAlgorithm_set_states() to resume operation after a short + * interruption, skipping initial learning phase. + * NOTE: This feature can only be used for VOC algorithm type and after at least + * 3 hours of continuous operation. + * @param params Pointer to the GasIndexAlgorithmParams struct + * @param state0 State0 to be stored + * @param state1 State1 to be stored + */ +void GasIndexAlgorithm_get_states(const GasIndexAlgorithmParams* params, + float* state0, float* state1); + +/** + * Set previously retrieved algorithm states to resume operation after a short + * interruption, skipping initial learning phase. This feature should not be + * used after interruptions of more than 10 minutes. Call this once after + * GasIndexAlgorithm_init() or GasIndexAlgorithm_reset() and the optional + * GasIndexAlgorithm_set_tuning_parameters(), if desired. Otherwise, the + * algorithm will start with initial learning phase. + * NOTE: This feature can only be used for VOC algorithm type. + * @param params Pointer to the GasIndexAlgorithmParams struct + * @param state0 State0 to be restored + * @param state1 State1 to be restored + */ +void GasIndexAlgorithm_set_states(GasIndexAlgorithmParams* params, float state0, + float state1); + +/** + * Set parameters to customize the gas index algorithm. Call this once after + * GasIndexAlgorithm_init() and before optional GasIndexAlgorithm_set_states(), + * if desired. Otherwise, the default values will be used. + * + * @param params Pointer to the GasIndexAlgorithmParams + * struct + * @param index_offset Gas index representing typical (average) + * conditions. Range 1..250, + * default 100 for VOC and 1 for NOx + * @param learning_time_offset_hours Time constant of long-term estimator for + * offset. Past events will be forgotten + * after about twice the learning time. + * Range 1..1000 [hours], default 12 [hours] + * @param learning_time_gain_hours Time constant of long-term estimator for + * gain. Past events will be forgotten + * after about twice the learning time. + * Range 1..1000 [hours], default 12 [hours] + * NOTE: This value is not relevant for NOx + * algorithm type + * @param gating_max_duration_minutes Maximum duration of gating (freeze of + * estimator during high gas index signal). + * 0 (no gating) or range 1..3000 [minutes], + * default 180 [minutes] for VOC and + * 720 [minutes] for NOx + * @param std_initial Initial estimate for standard deviation. + * Lower value boosts events during initial + * learning period, but may result in larger + * device-to-device variations. + * Range 10..5000, default 50 + * NOTE: This value is not relevant for NOx + * algorithm type + * @param gain_factor Factor used to scale applied gain value + * when calculating gas index. Range 1..1000, + * default 230 + */ +void GasIndexAlgorithm_set_tuning_parameters( + GasIndexAlgorithmParams* params, int32_t index_offset, + int32_t learning_time_offset_hours, int32_t learning_time_gain_hours, + int32_t gating_max_duration_minutes, int32_t std_initial, + int32_t gain_factor); + +/** + * Get current parameters to customize the gas index algorithm. + * Refer to GasIndexAlgorithm_set_tuning_parameters() for description of the + * parameters. + */ +void GasIndexAlgorithm_get_tuning_parameters( + const GasIndexAlgorithmParams* params, int32_t* index_offset, + int32_t* learning_time_offset_hours, int32_t* learning_time_gain_hours, + int32_t* gating_max_duration_minutes, int32_t* std_initial, + int32_t* gain_factor); + +/** + * Get the sampling interval parameter used by the algorithm. + */ +void GasIndexAlgorithm_get_sampling_interval( + const GasIndexAlgorithmParams* params, float* sampling_interval); + +/** + * Calculate the gas index value from the raw sensor value. + * + * @param params Pointer to the GasIndexAlgorithmParams struct + * @param sraw Raw value from the SGP4x sensor + * @param gas_index Calculated gas index value from the raw sensor value. Zero + * during initial blackout period and 1..500 afterwards + */ +void GasIndexAlgorithm_process(GasIndexAlgorithmParams* params, int32_t sraw, + int32_t* gas_index); + +#endif /* GASINDEXALGORITHM_H_ */ \ No newline at end of file diff --git a/tasmota/include/tasmota_configurations.h b/tasmota/include/tasmota_configurations.h index 94acfddd9919..b4fabb479d88 100644 --- a/tasmota/include/tasmota_configurations.h +++ b/tasmota/include/tasmota_configurations.h @@ -99,6 +99,7 @@ #define USE_MGS // [I2cDriver17] Enable Xadow and Grove Mutichannel Gas sensor using library Multichannel_Gas_Sensor (+10k code) #define USE_SGP30 // [I2cDriver18] Enable SGP30 sensor (I2C address 0x58) (+1k1 code) #define USE_SGP40 // [I2cDriver69] Enable SGP40 sensor (I2C address 0x59) (+1k4 code) +#define USE_SGP4X // [I2cDriver69] Enable SGP40 sensor (I2C address 0x59) (+1k4 code) #define USE_SEN5X // [I2cDriver76] Enable SEN5X sensor (I2C address 0x69) (+3k code) //#define USE_SI1145 // [I2cDriver19] Enable SI1145/46/47 sensor (I2C address 0x60) (+1k code) #define USE_LM75AD // [I2cDriver20] Enable LM75AD sensor (I2C addresses 0x48 - 0x4F) (+0k5 code) diff --git a/tasmota/include/tasmota_configurations_ESP32.h b/tasmota/include/tasmota_configurations_ESP32.h index 19aa84a227ca..0b7289209d6b 100644 --- a/tasmota/include/tasmota_configurations_ESP32.h +++ b/tasmota/include/tasmota_configurations_ESP32.h @@ -376,6 +376,7 @@ //#define USE_MGS // [I2cDriver17] Enable Xadow and Grove Mutichannel Gas sensor using library Multichannel_Gas_Sensor (+10k code) //#define USE_SGP30 // [I2cDriver18] Enable SGP30 sensor (I2C address 0x58) (+1k1 code) //#define USE_SGP40 // [I2cDriver69] Enable SGP40 sensor (I2C address 0x59) (+1k4 code) +//#define USE_SGP4X // [I2cDriver69] Enable SGP4X sensor (I2C address 0x59) (+1k4 code) //#define USE_SEN5X // [I2cDriver76] Enable SEN5X sensor (I2C address 0x69) (+3k code) //#define USE_SI1145 // [I2cDriver19] Enable SI1145/46/47 sensor (I2C address 0x60) (+1k code) //#define USE_LM75AD // [I2cDriver20] Enable LM75AD sensor (I2C addresses 0x48 - 0x4F) (+0k5 code) @@ -609,6 +610,7 @@ #define USE_MGS // [I2cDriver17] Enable Xadow and Grove Mutichannel Gas sensor using library Multichannel_Gas_Sensor (+10k code) #define USE_SGP30 // [I2cDriver18] Enable SGP30 sensor (I2C address 0x58) (+1k1 code) #define USE_SGP40 // [I2cDriver69] Enable SGP40 sensor (I2C address 0x59) (+1k4 code) +#define USE_SGP4X // [I2cDriver69] Enable SGP4X sensor (I2C address 0x59) (+1k4 code) #define USE_SEN5X // [I2cDriver76] Enable SEN5X sensor (I2C address 0x69) (+3k code) //#define USE_SI1145 // [I2cDriver19] Enable SI1145/46/47 sensor (I2C address 0x60) (+1k code) #define USE_LM75AD // [I2cDriver20] Enable LM75AD sensor (I2C addresses 0x48 - 0x4F) (+0k5 code) diff --git a/tasmota/tasmota_support/support_features.ino b/tasmota/tasmota_support/support_features.ino index 690bb2d89ee8..5103f7ac21e2 100644 --- a/tasmota/tasmota_support/support_features.ino +++ b/tasmota/tasmota_support/support_features.ino @@ -897,8 +897,10 @@ void ResponseAppendFeatures(void) #if defined(USE_I2C) && defined(USE_PCA9557) feature9 |= 0x00800000; // xdrv_69_pca9557.ino #endif +#if defined(USE_I2C) && defined(USE_SGP4X) + feature9 |= 0x01000000; // xdrv_109_sgp4x.ino +#endif -// feature9 |= 0x01000000; // feature9 |= 0x02000000; // feature9 |= 0x04000000; // feature9 |= 0x08000000; diff --git a/tasmota/tasmota_xsns_sensor/xsns_109_sgp4x.ino b/tasmota/tasmota_xsns_sensor/xsns_109_sgp4x.ino new file mode 100644 index 000000000000..37d73e53852a --- /dev/null +++ b/tasmota/tasmota_xsns_sensor/xsns_109_sgp4x.ino @@ -0,0 +1,268 @@ +/* + xsns_109_sgp4x.ino - SGP4X VOC and NOx sensor support for Tasmota + + Copyright (C) 2023 Andrew Klaus + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_I2C +#ifdef USE_SGP4X +/*********************************************************************************************\ + * SGP4x - Gas (TVOC - Total Volatile Organic Compounds) and NOx + * + * Source: Sensirion Driver, with mods by Andrew Klaus + * Adaption for TASMOTA: Andrew Klaus + * + * I2C Address: 0x59 +\*********************************************************************************************/ + +#define XSNS_109 109 +#define XI2C_82 82 // See I2CDEVICES.md + +#define SGP4X_ADDRESS 0x59 + +#include "SensirionI2CSgp4x.h" + +extern "C" { +#include "sensirion_gas_index_algorithm.h" +}; + +enum SGP4X_State { + STATE_SGP4X_START, + STATE_SGP4X_SELFTEST_SENT, + STATE_SGP4X_SELFTEST_WAIT, + STATE_SGP4X_SELFTEST_DONE, + STATE_SGP4X_COND_SENT, + STATE_SGP4X_COND_DONE, + STATE_SGP4X_NORMAL, + STATE_SGP4X_FAIL, +}; +SensirionI2CSgp4x sgp4x; +SGP4X_State sgp4x_state = STATE_SGP4X_START; + +bool sgp4x_init = false; +bool sgp4x_read_pend = false; + +uint16_t srawVoc; +uint16_t srawNox; +int32_t voc_index_sgp4x; +int32_t nox_index_sgp4x; + +uint16_t conditioning_s = 10; // 10 second delay for startup + +GasIndexAlgorithmParams voc_algorithm_params; +GasIndexAlgorithmParams nox_algorithm_params; + +/********************************************************************************************/ + +void sgp4x_Init(void) +{ + if (!I2cSetDevice(SGP4X_ADDRESS)) { return; } + + uint8_t serialNumberSize = 3; + uint16_t serialNumber[serialNumberSize]; + uint16_t error; + + sgp4x.begin(Wire); + + error = sgp4x.getSerialNumber(serialNumber, serialNumberSize); + + if (error) { + Sgp4xHandleError(error); + return; + } else { + AddLog(LOG_LEVEL_INFO, PSTR("SGP4X serial nr 0x%X 0x%X 0x%X") ,serialNumber[0], serialNumber[1], serialNumber[2]); + } + + I2cSetActiveFound(SGP4X_ADDRESS, "SGP4X"); + + sgp4x_init = true; + error = sgp4x.sendSelfTestCmd(); + + if (error) { + Sgp4xHandleError(error); + sgp4x_state = STATE_SGP4X_FAIL; + return; + } + + sgp4x_state = STATE_SGP4X_SELFTEST_SENT; + + GasIndexAlgorithm_init(&nox_algorithm_params, GasIndexAlgorithm_ALGORITHM_TYPE_NOX); + GasIndexAlgorithm_init(&voc_algorithm_params, GasIndexAlgorithm_ALGORITHM_TYPE_VOC); +} + +void Sgp4xHandleError(uint16_t error) { + char errorMessage[256]; + errorToString(error, errorMessage, 256); + AddLog(LOG_LEVEL_ERROR, PSTR("SGP4X error: %s"), errorMessage); +} + +void Sgp4xSendReadCmd(void) +{ + // Check if we're already waiting for a read + // Or if we need to wait a cycle before initiating a reading + if (sgp4x_read_pend){return;} + + uint16_t error; + uint16_t rhticks = (uint16_t)((TasmotaGlobal.humidity * 65535) / 100 + 0.5); + uint16_t tempticks = (uint16_t)(((TasmotaGlobal.temperature_celsius + 45) * 65535) / 175); + + // Handle self testing + // Wait 1 cycle (at least 320ms to read selftest value) + if (sgp4x_state == STATE_SGP4X_SELFTEST_SENT) { + sgp4x_state = STATE_SGP4X_SELFTEST_WAIT; + return; + } else if (sgp4x_state == STATE_SGP4X_SELFTEST_WAIT) { + if (Sgp4xReadSelfTest()){ + sgp4x_state = STATE_SGP4X_FAIL; + return; + } else { + sgp4x_state = STATE_SGP4X_SELFTEST_DONE; + } + } + + // Initiate conditioning + if (sgp4x_state == STATE_SGP4X_SELFTEST_DONE) { + error = sgp4x.sendConditioningCmd(0x8000, 0x6666); + sgp4x_state = STATE_SGP4X_COND_SENT; + + if (error) { + Sgp4xHandleError(error); + } + return; + } + + if (sgp4x_state == STATE_SGP4X_COND_DONE) { + sgp4x_state = STATE_SGP4X_NORMAL; + } + + // Normal operation + if (sgp4x_state == STATE_SGP4X_NORMAL) { + error = sgp4x.sendRawSignalsCmd(rhticks, tempticks); + if (error) { + Sgp4xHandleError(error); + } else { + sgp4x_read_pend = true; + } + return; + } + + return; +} + +bool Sgp4xReadSelfTest() { + uint16_t testResult; + uint16_t error; + + error = sgp4x.readSelfTestValue(testResult); + if (error) { + Sgp4xHandleError(error); + return true; + } else if (testResult != 0xD400) { + AddLog(LOG_LEVEL_ERROR, PSTR("SGP4X: executeSelfTest failed with error: %s"), testResult); + return true; + } + + return false; +} + +void Sgp4xUpdate(void) +{ + uint16_t error; + + // Conditioning - NOx needs 10s to warmup + if (sgp4x_state == STATE_SGP4X_COND_SENT) { + if (conditioning_s > 0) { + conditioning_s--; + return; + } else { + sgp4x_state = STATE_SGP4X_COND_DONE; + } + } + + if (sgp4x_state == STATE_SGP4X_NORMAL && sgp4x_read_pend) { + error = sgp4x.readRawSignalsValue(srawVoc, srawNox); + sgp4x_read_pend = false; + + if (!error) { + GasIndexAlgorithm_process(&voc_algorithm_params, srawVoc, &voc_index_sgp4x); + GasIndexAlgorithm_process(&nox_algorithm_params, srawNox, &nox_index_sgp4x); + } else { + Sgp4xHandleError(error); + } + } +} + +#ifdef USE_WEBSERVER +const char HTTP_SNS_SGP4X[] PROGMEM = + "{s}SGP4X " D_TVOC "_" D_JSON_RAW "{m}%d " "{e}" // {s} = , {m} = , {e} = + "{s}SGP4X " D_NOX "_" D_JSON_RAW "{m}%d " "{e}" + "{s}SGP4X " D_TVOC "{m}%d " "{e}" + "{s}SGP4X " D_NOX "{m}%d " "{e}"; +#endif + +void Sgp4xShow(bool json) +{ + if (sgp4x_state == STATE_SGP4X_NORMAL) { + if (json) { + ResponseAppend_P(PSTR(",\"SGP4X\":{\"" D_TVOC "_" D_JSON_RAW "\":%d,\"" D_NOX "_" D_JSON_RAW "\":%d,\"" D_TVOC "\":%d,\"" D_NOX "\":%d"), srawVoc, srawNox, voc_index_sgp4x, nox_index_sgp4x); + ResponseJsonEnd(); +#ifdef USE_DOMOTICZ + if (0 == TasmotaGlobal.tele_period) DomoticzSensor(DZ_AIRQUALITY, srawVoc); +#endif // USE_DOMOTICZ +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(HTTP_SNS_SGP4X, srawVoc, srawNox, voc_index_sgp4x, nox_index_sgp4x); +#endif + } + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns109(uint32_t function) +{ + if (!I2cEnabled(XI2C_82)) { return false; } + + bool result = false; + + if (FUNC_INIT == function) { + sgp4x_Init(); + } + else if (sgp4x_init) { + switch (function) { + case FUNC_EVERY_250_MSECOND: + Sgp4xSendReadCmd(); + break; + case FUNC_EVERY_SECOND: + Sgp4xUpdate(); + break; + case FUNC_JSON_APPEND: + Sgp4xShow(1); + break; + #ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + Sgp4xShow(0); + break; + #endif // USE_WEBSERVER + } + } + return result; +} + +#endif // USE_sgp4x +#endif // USE_I2C diff --git a/tools/decode-status.py b/tools/decode-status.py index abd99ecfe767..78bc8feb7d97 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -296,7 +296,7 @@ "USE_DISPLAY_TM1650","USE_PCA9632","USE_TUYAMCUBR","USE_SEN5X", "USE_BIOPDU","USE_MCP23XXX_DRV","USE_PMSA003I","USE_LOX_O2", "USE_GDK101","USE_GM861","USE_TC74","USE_PCA9557", - "","","","", + "USE_SGP4X","","","", "","","","" ]]