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","","","",
"","","",""
]]