diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52e4dfe81..f20a6fd68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
### Breaking Changed
### Changed
+- SCD30 library FrogmoreScd30 to Sensirion arduino-i2c-scd30 v1.1.1
- SCD4x library FrogmoreScd40 to Sensirion arduino-i2c-scd4x v1.1.0
- SPS30 library Sensirion arduino-i2c-sps30 v1.0.1
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index d8cee5b8f..daf97f317 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -135,6 +135,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
### Changed
- ESP32 Platform from 2025.04.30 to 2026.05.50, Framework (Arduino Core) from v3.1.11 to v3.3.8.260506 and IDF from v5.3.4.260127 to v5.5.4.260407 [#24718](https://github.com/arendst/Tasmota/issues/24718)
+- SCD30 library FrogmoreScd30 to Sensirion arduino-i2c-scd30 v1.1.1
- SCD4x library FrogmoreScd40 to Sensirion arduino-i2c-scd4x v1.1.0
- SPS30 library Sensirion arduino-i2c-sps30 v1.0.1
- Increase security by inverting state of `define DISABLE_REFERER_CHK` controlling remote HTTP access which is now default off
diff --git a/lib/default/TasmotaWire/src/Wire.cpp b/lib/default/TasmotaWire/src/Wire.cpp
index 90226adcd..5b43deb99 100644
--- a/lib/default/TasmotaWire/src/Wire.cpp
+++ b/lib/default/TasmotaWire/src/Wire.cpp
@@ -77,6 +77,10 @@ void TwoWire::setClockStretchLimit(uint32_t limit) {
twi.setClockStretchLimit(limit);
}
+uint8_t TwoWire::status(void) {
+ return twi.status();
+}
+
size_t TwoWire::requestFrom(uint8_t address, size_t size, bool sendStop) {
if (size > BUFFER_LENGTH) {
size = BUFFER_LENGTH;
diff --git a/lib/default/TasmotaWire/src/Wire.h b/lib/default/TasmotaWire/src/Wire.h
index 3c6f924d5..229bee43e 100644
--- a/lib/default/TasmotaWire/src/Wire.h
+++ b/lib/default/TasmotaWire/src/Wire.h
@@ -60,6 +60,7 @@ public:
void begin();
void setClock(uint32_t);
void setClockStretchLimit(uint32_t);
+ uint8_t status();
void beginTransmission(uint8_t);
void beginTransmission(int);
uint8_t endTransmission(void);
diff --git a/lib/lib_i2c/arduino-i2c-scd30/.clang-format b/lib/lib_i2c/arduino-i2c-scd30/.clang-format
new file mode 100644
index 000000000..047f2adf1
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/.clang-format
@@ -0,0 +1,14 @@
+---
+Language: Cpp
+BasedOnStyle: LLVM
+IndentWidth: 4
+AlignAfterOpenBracket: Align
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: false
+IndentCaseLabels: true
+SpacesBeforeTrailingComments: 2
+PointerAlignment: Left
+AlignEscapedNewlines: Left
+ForEachMacros: ['TEST_GROUP', 'TEST']
+...
diff --git a/lib/lib_i2c/arduino-i2c-scd30/CHANGELOG.md b/lib/lib_i2c/arduino-i2c-scd30/CHANGELOG.md
new file mode 100644
index 000000000..46dc9db23
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/CHANGELOG.md
@@ -0,0 +1,33 @@
+# CHANGELOG
+
+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).
+
+## [Unreleased]
+
+## [1.1.1] - 2026-4-27
+
+### Added
+
+- Added `read_serial_number` to example usage
+## [1.1.0] - 2026-4-24
+
+### Added
+
+- Added `read_serial_number` command to read out the sensor's serial number
+## [1.0.0] - 2025-8-25
+
+### Changed
+
+- Updated to latest driver framework
+## [0.1.0] - 2022-4-07
+
+### Added
+
+- Initial SCD30 driver release
+
+[Unreleased]: https://github.com/Sensirion/arduino-i2c-scd30/compare/1.1.1...HEAD
+[1.1.1]: https://github.com/Sensirion/arduino-i2c-scd30/compare/1.1.0...1.1.1
+[1.1.0]: https://github.com/Sensirion/arduino-i2c-scd30/compare/1.0.0...1.1.0
+[1.0.0]: https://github.com/Sensirion/arduino-i2c-scd30/compare/0.1.0...1.0.0
+[0.1.0]: https://github.com/Sensirion/arduino-i2c-scd30/releases/tag/0.1.0
\ No newline at end of file
diff --git a/lib/lib_i2c/arduino-i2c-scd30/LICENSE b/lib/lib_i2c/arduino-i2c-scd30/LICENSE
new file mode 100644
index 000000000..8fe04edd1
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, 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-scd30/README.md b/lib/lib_i2c/arduino-i2c-scd30/README.md
new file mode 100644
index 000000000..7b507b707
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/README.md
@@ -0,0 +1,221 @@
+# Sensirion I²C SCD30 Arduino Library
+
+This is the Sensirion SCD30 library for Arduino allowing you to
+communicate with a SCD30 sensor
+over I²C.
+
+
+
+Click [here](https://sensirion.com/products/catalog/SCD30/) to learn more about the Sensirion SCD30 sensor.
+
+
+
+The default I²C address of [SCD30](https://sensirion.com/products/catalog/SCD30/) is **0x61**.
+
+
+
+## Installation of the library
+
+This library can be installed using the Arduino Library manager:
+Start the [Arduino IDE](http://www.arduino.cc/en/main/software) and open
+the Library Manager via
+
+`Sketch` ➔ `Include Library` ➔ `Manage Libraries...`
+
+Search for the `Sensirion I2C SCD30` library in the `Filter
+your search...` field and install it by clicking the `install` button.
+
+If you cannot find it in the library manager, 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 library
+manager or `Add .ZIP Library`
+
+#### Dependencies
+* [Sensirion Core](https://github.com/Sensirion/arduino-core)
+
+## Connect the sensor
+
+Use the following pin description to connect your SCD30 to the standard I²C bus of your Arduino board:
+
+
+
+| *Pin* | *Cable Color* | *Name* | *Description* | *Comments* |
+|-------|---------------|:------:|----------------|------------|
+| 1 | red | VDD | Supply Voltage | 3.3V to 5.5V
+| 2 | black | GND | Ground |
+| 3 | yellow | SCL | I2C: Serial clock input |
+| 4 | green | SDA | I2C: Serial data input / output |
+| 5 | | RDY | | High when data is available - do not connect
+| 6 | | PWM | | do not connect
+| 7 | blue | SEL | Interface select | Pull to ground or floating for I2C
+
+
+
+
+The recommended voltage is 3.3V.
+
+### Board specific wiring
+You will find pinout schematics for recommended board models below:
+
+
+
+Arduino Uno
+
+
+| *SCD30* | *SCD30 Pin* | *Cable Color* | *Board Pin* |
+| :---: | --- | --- | --- |
+| VDD | 1 | red | 3.3V |
+| GND | 2 | black | GND |
+| SCL | 3 | yellow | D19/SCL |
+| SDA | 4 | green | D18/SDA |
+| RDY | 5 | | |
+| PWM | 6 | | |
+| SEL | 7 | blue | GND |
+
+
+
+
+
+
+
+
+
+
+Arduino Nano
+
+
+| *SCD30* | *SCD30 Pin* | *Cable Color* | *Board Pin* |
+| :---: | --- | --- | --- |
+| VDD | 1 | red | 3.3V |
+| GND | 2 | black | GND |
+| SCL | 3 | yellow | A5 |
+| SDA | 4 | green | A4 |
+| RDY | 5 | | |
+| PWM | 6 | | |
+| SEL | 7 | blue | GND |
+
+
+
+
+
+
+
+
+
+
+Arduino Micro
+
+
+| *SCD30* | *SCD30 Pin* | *Cable Color* | *Board Pin* |
+| :---: | --- | --- | --- |
+| VDD | 1 | red | 3.3V |
+| GND | 2 | black | GND |
+| SCL | 3 | yellow | ~D3/SCL |
+| SDA | 4 | green | D2/SDA |
+| RDY | 5 | | |
+| PWM | 6 | | |
+| SEL | 7 | blue | GND |
+
+
+
+
+
+
+
+
+
+
+Arduino Mega 2560
+
+
+| *SCD30* | *SCD30 Pin* | *Cable Color* | *Board Pin* |
+| :---: | --- | --- | --- |
+| VDD | 1 | red | 3.3V |
+| GND | 2 | black | GND |
+| SCL | 3 | yellow | D21/SCL |
+| SDA | 4 | green | D20/SDA |
+| RDY | 5 | | |
+| PWM | 6 | | |
+| SEL | 7 | blue | GND |
+
+
+
+
+
+
+
+
+
+
+ESP32 DevKitC
+
+
+| *SCD30* | *SCD30 Pin* | *Cable Color* | *Board Pin* |
+| :---: | --- | --- | --- |
+| VDD | 1 | red | 3V3 |
+| GND | 2 | black | GND |
+| SCL | 3 | yellow | GPIO 22 |
+| SDA | 4 | green | GPIO 21 |
+| RDY | 5 | | |
+| PWM | 6 | | |
+| SEL | 7 | blue | GND |
+
+
+
+
+
+
+
+
+
+## Quick Start
+
+1. Install the libraries and dependencies according to [Installation of the library](#installation-of-the-library)
+
+2. Connect the SCD30 sensor to your Arduino as explained in [Connect the sensor](#connect-the-sensor)
+
+3. Open the `exampleUsage` sample project within the Arduino IDE:
+
+ `File` ➔ `Examples` ➔ `Sensirion I2C SCD30` ➔ `exampleUsage`
+
+
+
+4. Click the `Upload` button in the Arduino IDE or `Sketch` ➔ `Upload`
+
+5. 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 used tool has to be set to `115200 baud`.
+
+## Contributing
+
+**Contributions are welcome!**
+
+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.
+:
+
+## Known issues
+
+* *softReset()*:
+ After using the ``softReset()`` function on an Arduino MKR WIFI 1010, subsequent commands are no longer acknowledged.
+ The I2C line remains low after receiving the first command byte.
+
+ To make the provided example work on the Arduino MKR WIFI 1010, the call to ``softReset()`` and the subsequent ``delay()`` can be removed.
+
+## License
+
+See [LICENSE](LICENSE).
\ No newline at end of file
diff --git a/lib/lib_i2c/arduino-i2c-scd30/examples/exampleUsage/exampleUsage.ino b/lib/lib_i2c/arduino-i2c-scd30/examples/exampleUsage/exampleUsage.ino
new file mode 100644
index 000000000..1ac0be02c
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/examples/exampleUsage/exampleUsage.ino
@@ -0,0 +1,127 @@
+/*
+ * THIS FILE IS AUTOMATICALLY GENERATED
+ *
+ * Generator: sensirion-driver-generator 1.6.1
+ * Product: scd30
+ * Model-Version: 1.1.1
+ */
+/*
+ * Copyright (c) 2026, 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
+
+// macro definitions
+// make sure that we use the proper definition of NO_ERROR
+#ifdef NO_ERROR
+#undef NO_ERROR
+#endif
+#define NO_ERROR 0
+
+SensirionI2cScd30 sensor;
+
+static char errorMessage[64];
+static int16_t error;
+
+void setup() {
+
+ Serial.begin(115200);
+ while (!Serial) {
+ delay(100);
+ }
+ Wire.begin();
+ sensor.begin(Wire, SCD30_I2C_ADDR_61);
+
+ sensor.stopPeriodicMeasurement();
+ sensor.softReset();
+ delay(2000);
+ int8_t serialNumber[32] = {0};
+
+ error = sensor.readSerialNumber(serialNumber, 32);
+ if (error != NO_ERROR) {
+ Serial.print("Error trying to execute readSerialNumber(): ");
+ errorToString(error, errorMessage, sizeof errorMessage);
+ Serial.println(errorMessage);
+ return;
+ }
+ Serial.print("serialNumber: ");
+ Serial.print((const char*)serialNumber);
+ Serial.println();
+ uint8_t major = 0;
+ uint8_t minor = 0;
+
+ error = sensor.readFirmwareVersion(major, minor);
+ if (error != NO_ERROR) {
+ Serial.print("Error trying to execute readFirmwareVersion(): ");
+ errorToString(error, errorMessage, sizeof errorMessage);
+ Serial.println(errorMessage);
+ return;
+ }
+ Serial.print("major: ");
+ Serial.print(major);
+ Serial.print("\t");
+ Serial.print("minor: ");
+ Serial.print(minor);
+ Serial.println();
+ error = sensor.startPeriodicMeasurement(0);
+ if (error != NO_ERROR) {
+ Serial.print("Error trying to execute startPeriodicMeasurement(): ");
+ errorToString(error, errorMessage, sizeof errorMessage);
+ Serial.println(errorMessage);
+ return;
+ }
+}
+
+void loop() {
+
+ float co2Concentration = 0.0;
+ float temperature = 0.0;
+ float humidity = 0.0;
+
+ delay(1500);
+ error = sensor.blockingReadMeasurementData(co2Concentration, temperature,
+ humidity);
+ if (error != NO_ERROR) {
+ Serial.print("Error trying to execute blockingReadMeasurementData(): ");
+ errorToString(error, errorMessage, sizeof errorMessage);
+ Serial.println(errorMessage);
+ return;
+ }
+ Serial.print("co2Concentration: ");
+ Serial.print(co2Concentration);
+ Serial.print("\t");
+ Serial.print("temperature: ");
+ Serial.print(temperature);
+ Serial.print("\t");
+ Serial.print("humidity: ");
+ Serial.print(humidity);
+ Serial.println();
+}
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Mega-2560-Rev3-i2c-pinout-3.3V-SEL.png b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Mega-2560-Rev3-i2c-pinout-3.3V-SEL.png
new file mode 100644
index 000000000..f31661f39
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Mega-2560-Rev3-i2c-pinout-3.3V-SEL.png differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Micro-i2c-pinout-3.3V-SEL.png b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Micro-i2c-pinout-3.3V-SEL.png
new file mode 100644
index 000000000..366c3088f
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Micro-i2c-pinout-3.3V-SEL.png differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Nano-i2c-pinout-3.3V-SEL.png b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Nano-i2c-pinout-3.3V-SEL.png
new file mode 100644
index 000000000..a6638a93a
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Nano-i2c-pinout-3.3V-SEL.png differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Uno-Rev3-i2c-pinout-3.3V-SEL.png b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Uno-Rev3-i2c-pinout-3.3V-SEL.png
new file mode 100644
index 000000000..74dee2839
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/Arduino-Uno-Rev3-i2c-pinout-3.3V-SEL.png differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/esp32-devkitc-i2c-pinout-3.3V-SEL.png b/lib/lib_i2c/arduino-i2c-scd30/images/esp32-devkitc-i2c-pinout-3.3V-SEL.png
new file mode 100644
index 000000000..3ac7eec29
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/esp32-devkitc-i2c-pinout-3.3V-SEL.png differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/scd30_pinout.jpg b/lib/lib_i2c/arduino-i2c-scd30/images/scd30_pinout.jpg
new file mode 100644
index 000000000..8d7cb506c
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/scd30_pinout.jpg differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/images/sensor_scd30_image.jpg b/lib/lib_i2c/arduino-i2c-scd30/images/sensor_scd30_image.jpg
new file mode 100644
index 000000000..a7d583d41
Binary files /dev/null and b/lib/lib_i2c/arduino-i2c-scd30/images/sensor_scd30_image.jpg differ
diff --git a/lib/lib_i2c/arduino-i2c-scd30/keywords.txt b/lib/lib_i2c/arduino-i2c-scd30/keywords.txt
new file mode 100644
index 000000000..fdbd4d448
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/keywords.txt
@@ -0,0 +1,43 @@
+#######################################
+# Syntax Coloring Map
+#######################################
+
+#######################################
+# Datatypes (KEYWORD1)
+#######################################
+
+SensirionI2cScd30 KEYWORD1
+
+#######################################
+# Methods and Functions (KEYWORD2)
+#######################################
+
+awaitDataReady KEYWORD2
+blockingReadMeasurementData KEYWORD2
+startPeriodicMeasurement KEYWORD2
+stopPeriodicMeasurement KEYWORD2
+setMeasurementInterval KEYWORD2
+getMeasurementInterval KEYWORD2
+getDataReady KEYWORD2
+readMeasurementData KEYWORD2
+activateAutoCalibration KEYWORD2
+getAutoCalibrationStatus KEYWORD2
+forceRecalibration KEYWORD2
+getForceRecalibrationStatus KEYWORD2
+setTemperatureOffset KEYWORD2
+getTemperatureOffset KEYWORD2
+getAltitudeCompensation KEYWORD2
+setAltitudeCompensation KEYWORD2
+readFirmwareVersion KEYWORD2
+softReset KEYWORD2
+readSerialNumber KEYWORD2
+
+#######################################
+# Instances (KEYWORD2)
+#######################################
+
+sensor KEYWORD2
+
+#######################################
+# Constants (LITERAL1)
+#######################################
\ No newline at end of file
diff --git a/lib/lib_i2c/arduino-i2c-scd30/library.properties b/lib/lib_i2c/arduino-i2c-scd30/library.properties
new file mode 100644
index 000000000..c19b05f90
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/library.properties
@@ -0,0 +1,11 @@
+name=Sensirion I2C SCD30
+version=1.1.1
+author=Sensirion
+maintainer=Sensirion
+sentence=Library for the SCD30 sensor by Sensirion
+paragraph=Enables you to use the SCD30 sensor via I2C.
+url=https://github.com/Sensirion/arduino-i2c-scd30
+category=Sensors
+architectures=*
+depends=Sensirion Core
+includes=SensirionI2cScd30.h
diff --git a/lib/lib_i2c/arduino-i2c-scd30/metadata.yml b/lib/lib_i2c/arduino-i2c-scd30/metadata.yml
new file mode 100644
index 000000000..462e14ae3
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/metadata.yml
@@ -0,0 +1,7 @@
+# driver generation metadata
+generator_version: 1.6.1
+model_version: 1.1.1
+dg_status: released
+is_manually_modified: true
+first_generated: '2022-04-07 17:45'
+last_generated: '2026-04-27 08:44'
diff --git a/lib/lib_i2c/arduino-i2c-scd30/src/SensirionI2cScd30.cpp b/lib/lib_i2c/arduino-i2c-scd30/src/SensirionI2cScd30.cpp
new file mode 100644
index 000000000..1e329cafe
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/src/SensirionI2cScd30.cpp
@@ -0,0 +1,436 @@
+/*
+ * THIS FILE IS AUTOMATICALLY GENERATED
+ *
+ * Generator: sensirion-driver-generator 1.6.1
+ * Product: scd30
+ * Model-Version: 1.1.1
+ */
+/*
+ * Copyright (c) 2026, 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 "SensirionI2cScd30.h"
+#include
+
+// make sure that we use the proper definition of NO_ERROR
+#ifdef NO_ERROR
+#undef NO_ERROR
+#endif
+#define NO_ERROR 0
+
+static uint8_t communication_buffer[48] = {0};
+
+SensirionI2cScd30::SensirionI2cScd30() {
+}
+
+int16_t SensirionI2cScd30::awaitDataReady(void) {
+ uint16_t dataReady = 0;
+ int16_t localError = 0;
+
+ localError = getDataReady(dataReady);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ while (dataReady == 0) {
+ delay(100);
+ localError = getDataReady(dataReady);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ }
+ return localError;
+}
+
+int16_t SensirionI2cScd30::blockingReadMeasurementData(float& co2Concentration,
+ float& temperature,
+ float& humidity) {
+ int16_t localError = 0;
+
+ localError = awaitDataReady();
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError = readMeasurementData(co2Concentration, temperature, humidity);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::startPeriodicMeasurement(uint16_t ambientPressure) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_START_PERIODIC_MEASUREMENT_CMD_ID, buffer_ptr, 5);
+ localError |= txFrame.addUInt16(ambientPressure);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::stopPeriodicMeasurement(void) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_STOP_PERIODIC_MEASUREMENT_CMD_ID, buffer_ptr, 2);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::setMeasurementInterval(uint16_t interval) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_SET_MEASUREMENT_INTERVAL_CMD_ID, buffer_ptr, 5);
+ localError |= txFrame.addUInt16(interval);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::getMeasurementInterval(uint16_t& interval) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_GET_MEASUREMENT_INTERVAL_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt16(interval);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::getDataReady(uint16_t& dataReadyFlag) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_GET_DATA_READY_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt16(dataReadyFlag);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::readMeasurementData(float& co2Concentration,
+ float& temperature,
+ float& humidity) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_READ_MEASUREMENT_DATA_CMD_ID, buffer_ptr, 18);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 18);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 18,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getFloat(co2Concentration);
+ localError |= rxFrame.getFloat(temperature);
+ localError |= rxFrame.getFloat(humidity);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::activateAutoCalibration(uint16_t doActivate) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_ACTIVATE_AUTO_CALIBRATION_CMD_ID, buffer_ptr, 5);
+ localError |= txFrame.addUInt16(doActivate);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::getAutoCalibrationStatus(uint16_t& isActive) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_GET_AUTO_CALIBRATION_STATUS_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt16(isActive);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::forceRecalibration(uint16_t co2RefConcentration) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_FORCE_RECALIBRATION_CMD_ID, buffer_ptr, 5);
+ localError |= txFrame.addUInt16(co2RefConcentration);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t
+SensirionI2cScd30::getForceRecalibrationStatus(uint16_t& co2RefConcentration) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_GET_FORCE_RECALIBRATION_STATUS_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt16(co2RefConcentration);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::setTemperatureOffset(uint16_t temperatureOffset) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_SET_TEMPERATURE_OFFSET_CMD_ID, buffer_ptr, 5);
+ localError |= txFrame.addUInt16(temperatureOffset);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::getTemperatureOffset(uint16_t& temperatureOffset) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_GET_TEMPERATURE_OFFSET_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt16(temperatureOffset);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::getAltitudeCompensation(uint16_t& altitude) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_GET_ALTITUDE_COMPENSATION_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt16(altitude);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::setAltitudeCompensation(uint16_t altitude) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_SET_ALTITUDE_COMPENSATION_CMD_ID, buffer_ptr, 5);
+ localError |= txFrame.addUInt16(altitude);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::readFirmwareVersion(uint8_t& major, uint8_t& minor) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_READ_FIRMWARE_VERSION_CMD_ID, buffer_ptr, 3);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 3);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 3,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getUInt8(major);
+ localError |= rxFrame.getUInt8(minor);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::softReset(void) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_SOFT_RESET_CMD_ID, buffer_ptr, 2);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(2000);
+ return localError;
+}
+
+int16_t SensirionI2cScd30::readSerialNumber(int8_t serialNumber[],
+ uint16_t serialNumberSize) {
+ int16_t localError = NO_ERROR;
+ uint8_t* buffer_ptr = communication_buffer;
+
+ SensirionI2CTxFrame txFrame = SensirionI2CTxFrame::createWithUInt16Command(
+ SCD30_READ_SERIAL_NUMBER_CMD_ID, buffer_ptr, 48);
+ localError =
+ SensirionI2CCommunication::sendFrame(_i2cAddress, txFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ delay(10);
+ SensirionI2CRxFrame rxFrame(buffer_ptr, 48);
+ localError = SensirionI2CCommunication::receiveFrame(_i2cAddress, 48,
+ rxFrame, *_i2cBus);
+ if (localError != NO_ERROR) {
+ return localError;
+ }
+ localError |= rxFrame.getBytes((uint8_t*)serialNumber, serialNumberSize);
+ return localError;
+}
+
+void SensirionI2cScd30::begin(TwoWire& i2cBus, uint8_t i2cAddress) {
+ _i2cBus = &i2cBus;
+ _i2cAddress = i2cAddress;
+}
diff --git a/lib/lib_i2c/arduino-i2c-scd30/src/SensirionI2cScd30.h b/lib/lib_i2c/arduino-i2c-scd30/src/SensirionI2cScd30.h
new file mode 100644
index 000000000..6785d98cf
--- /dev/null
+++ b/lib/lib_i2c/arduino-i2c-scd30/src/SensirionI2cScd30.h
@@ -0,0 +1,463 @@
+/*
+ * THIS FILE IS AUTOMATICALLY GENERATED
+ *
+ * Generator: sensirion-driver-generator 1.6.1
+ * Product: scd30
+ * Model-Version: 1.1.1
+ */
+/*
+ * Copyright (c) 2026, 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 SENSIRIONI2CSCD30_H
+#define SENSIRIONI2CSCD30_H
+
+#include
+#include
+
+#define SCD30_I2C_ADDR_61 0x61
+
+typedef enum {
+ SCD30_START_PERIODIC_MEASUREMENT_CMD_ID = 0x10,
+ SCD30_STOP_PERIODIC_MEASUREMENT_CMD_ID = 0x104,
+ SCD30_SET_MEASUREMENT_INTERVAL_CMD_ID = 0x4600,
+ SCD30_GET_MEASUREMENT_INTERVAL_CMD_ID = 0x4600,
+ SCD30_GET_DATA_READY_CMD_ID = 0x202,
+ SCD30_READ_MEASUREMENT_DATA_CMD_ID = 0x300,
+ SCD30_ACTIVATE_AUTO_CALIBRATION_CMD_ID = 0x5306,
+ SCD30_GET_AUTO_CALIBRATION_STATUS_CMD_ID = 0x5306,
+ SCD30_FORCE_RECALIBRATION_CMD_ID = 0x5204,
+ SCD30_GET_FORCE_RECALIBRATION_STATUS_CMD_ID = 0x5204,
+ SCD30_SET_TEMPERATURE_OFFSET_CMD_ID = 0x5403,
+ SCD30_GET_TEMPERATURE_OFFSET_CMD_ID = 0x5403,
+ SCD30_GET_ALTITUDE_COMPENSATION_CMD_ID = 0x5102,
+ SCD30_SET_ALTITUDE_COMPENSATION_CMD_ID = 0x5102,
+ SCD30_READ_FIRMWARE_VERSION_CMD_ID = 0xd100,
+ SCD30_SOFT_RESET_CMD_ID = 0xd304,
+ SCD30_READ_SERIAL_NUMBER_CMD_ID = 0xd033,
+} SCD30CmdId;
+
+class SensirionI2cScd30 {
+ public:
+ SensirionI2cScd30();
+ /**
+ * @brief Initializes the SCD30 class.
+ *
+ * @param i2cBus Arduino stream object to be used for communication.
+ */
+ void begin(TwoWire& i2cBus, uint8_t i2cAddress);
+
+ /**
+ * @brief Poll the data ready flag.
+ *
+ * Repeatedly call the get_data_ready() until the ready flag is set to 1. If
+ * the minimal measurement interval is 2s we iterate at most 200 times. Note
+ * that this is blocking the system for a considerable amount of time!
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t awaitDataReady(void);
+
+ /**
+ * @brief Block until data is available and return measurement results.
+ *
+ * This is a convenience method that combines polling the data ready flag
+ * and reading out the data. Note that this is blocking the system for a
+ * considerable amount of time!
+ *
+ * @param[out] co2Concentration
+ * @param[out] temperature
+ * @param[out] humidity
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t blockingReadMeasurementData(float& co2Concentration,
+ float& temperature, float& humidity);
+
+ /**
+ * @brief Starts continuous measurement of CO₂, relative humidity and
+ * temperature.
+ *
+ * Starts continuous measurement of the SCD30 to measure CO₂ concentration,
+ * humidity and temperature. Measurement data which is not read from the
+ * sensor will be overwritten. The CO₂ measurement value can be compensated
+ * for ambient pressure by feeding the pressure value in mBar to the sensor.
+ * Setting the ambient pressure will overwrite previous settings of altitude
+ * compensation. Setting the argument to zero will deactivate the ambient
+ * pressure compensation (default ambient pressure = 1013.25 mBar). For
+ * setting a new ambient pressure when continuous measurement is running the
+ * whole command has to be written to SCD30.
+ *
+ * @param[in] ambientPressure Ambient pressure in millibar.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ *
+ * Example:
+ * --------
+ *
+ * @code{.cpp}
+ *
+ * int16_t localError = 0;
+ *
+ * localError = sensor.startPeriodicMeasurement(0);
+ * if (localError != NO_ERROR) {
+ * return;
+ * }
+ *
+ * @endcode
+ *
+ */
+ int16_t startPeriodicMeasurement(uint16_t ambientPressure);
+
+ /**
+ * @brief Stops continuous measurement of the sensor.
+ *
+ * Stops the continuous measurement of the SCD30.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t stopPeriodicMeasurement(void);
+
+ /**
+ * @brief Sets the interval used to measure in continuous measurement mode.
+ *
+ * Sets the interval used by the SCD30 sensor to measure in continuous
+ * measurement mode. Initial value is 2s. The chosen measurement interval is
+ * saved in non-volatile memory and thus is not reset to its initial value
+ * after power up.
+ *
+ * @param[in] interval Measurement interval in seconds.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ *
+ * Example:
+ * --------
+ *
+ * @code{.cpp}
+ *
+ * int16_t localError = 0;
+ *
+ * localError = sensor.setMeasurementInterval(4);
+ * if (localError != NO_ERROR) {
+ * return;
+ * }
+ *
+ * @endcode
+ *
+ */
+ int16_t setMeasurementInterval(uint16_t interval);
+
+ /**
+ * @brief Read the configured measurement interval.
+ *
+ * Reads out the active measurement interval.
+ *
+ * @param[out] interval Configured measurement interval
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t getMeasurementInterval(uint16_t& interval);
+
+ /**
+ * @brief Queries if data is ready for readout.
+ *
+ * Data ready command is used to determine if a measurement can be read from
+ * the sensor’s buffer. Whenever there is a measurement available from the
+ * internal buffer this command returns 1 and 0 otherwise. As soon as the
+ * measurement has been read by SCD30 the return value changes to 0.
+ *
+ * @param[out] dataReadyFlag Data ready flag
+ *
+ * @note The read header should be sent with a delay of >3ms following the
+ * write sequence.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t getDataReady(uint16_t& dataReadyFlag);
+
+ /**
+ * @brief Reads out the measurement values.
+ *
+ * Allows to read new measurement data if data is available.
+ *
+ * @param[out] co2Concentration
+ * @param[out] temperature
+ * @param[out] humidity
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t readMeasurementData(float& co2Concentration, float& temperature,
+ float& humidity);
+
+ /**
+ * @brief Activates or deactivates continuous automatic self-calibration.
+ *
+ * Continuous automatic self-calibration (ASC) can be (de-)activated with
+ * this command. When activated for the first time a period of minimum 7
+ * days is needed so that the algorithm can find its initial parameter set
+ * for ASC. The sensor has to be exposed to fresh air for at least 1 hour
+ * every day. Also during that period, the sensor may not be disconnected
+ * from the power supply. Otherwise the procedure to find calibration
+ * parameters is aborted and has to be restarted from the beginning. The
+ * successfully calculated parameters are stored in non-volatile memory of
+ * the SCD30 having the effect that after a restart the previously found
+ * parameters for ASC are still present.
+ *
+ * @param[in] doActivate Set activate flag.
+ *
+ * @note Note that the most recently found self-calibration parameters will
+ * be actively used for self-calibration disregarding the status of this
+ * feature. Finding a new parameter set by the here described method will
+ * always overwrite the settings from external recalibration and vice-versa.
+ * The feature is switched off by default. To work properly SCD30 has to see
+ * fresh air on a regular basis. Optimal working conditions are given when
+ * the sensor sees fresh air for one hour every day so that ASC can
+ * constantly re-calibrate. ASC only works in continuous measurement mode.
+ * ASC status is saved in non-volatile memory. When the sensor is powered
+ * down while ASC is activated SCD30 will continue with automatic
+ * self-calibration after repowering without sending the command.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ *
+ * Example:
+ * --------
+ *
+ * @code{.cpp}
+ *
+ * int16_t localError = 0;
+ *
+ * localError = sensor.activateAutoCalibration(1);
+ * if (localError != NO_ERROR) {
+ * return;
+ * }
+ *
+ * @endcode
+ *
+ */
+ int16_t activateAutoCalibration(uint16_t doActivate);
+
+ /**
+ * @brief Gets the status of auto calibration.
+ *
+ * Read out the status of the active self calibration.
+ *
+ * @param[out] isActive Indication if automatic calibration is active
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t getAutoCalibrationStatus(uint16_t& isActive);
+
+ /**
+ * @brief Forces recalibration with a new value for the CO₂ concentration.
+ *
+ * Forced recalibration (FRC) is used to compensate for sensor drifts when a
+ * reference value of the CO₂ concentration in close proximity to the SCD30
+ * is available. For best results, the sensor has to be run in a stable
+ * environment in continuous mode at a measurement rate of 2s for at least
+ * two minutes before applying the FRC command and sending the reference
+ * value. Setting a reference CO₂ concentration by the method described here
+ * will always supersede corrections from the ASC (see command
+ * activate_auto_calibration) and vice-versa. The reference CO₂
+ * concentration has to be within the range 400 ppm ≤ cref(CO₂) ≤ 2000 ppm.
+ * The FRC method imposes a permanent update of the CO₂ calibration curve
+ * which persists after repowering the sensor. The most recently used
+ * reference value is retained in volatile memory and can be read out with
+ * the command sequence given below. After repowering the sensor, the
+ * command will return the standard reference value of 400 ppm.
+ *
+ * @param[in] co2RefConcentration New CO2 reference concentration.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ *
+ * Example:
+ * --------
+ *
+ * @code{.cpp}
+ *
+ * int16_t localError = 0;
+ *
+ * localError = sensor.forceRecalibration(500);
+ * if (localError != NO_ERROR) {
+ * return;
+ * }
+ *
+ * @endcode
+ *
+ */
+ int16_t forceRecalibration(uint16_t co2RefConcentration);
+
+ /**
+ * @brief Gets the force recalibration status.
+ *
+ * Read out the CO₂ reference concentration.
+ *
+ * @param[out] co2RefConcentration Currently used CO2 reference
+ * concentration.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t getForceRecalibrationStatus(uint16_t& co2RefConcentration);
+
+ /**
+ * @brief Set the temperature offset. Unit ℃ * 100.
+ *
+ * The on-board RH/T sensor is influenced by thermal self-heating of SCD30
+ * and other electrical components. Design-in alters the thermal properties
+ * of SCD30 such that temperature and humidity offsets may occur when
+ * operating the sensor in end-customer devices. Compensation of those
+ * effects is achievable by writing the temperature offset found in
+ * continuous operation of the device into the sensor. Temperature offset
+ * value is saved in non-volatile memory. The last set value will be used
+ * for temperature offset compensation after repowering.
+ *
+ * @param[in] temperatureOffset
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ *
+ * Example:
+ * --------
+ *
+ * @code{.cpp}
+ *
+ * int16_t localError = 0;
+ *
+ * localError = sensor.setTemperatureOffset(2000);
+ * if (localError != NO_ERROR) {
+ * return;
+ * }
+ *
+ * @endcode
+ *
+ */
+ int16_t setTemperatureOffset(uint16_t temperatureOffset);
+
+ /**
+ * @brief Get the temperature offset. Unit ℃ * 100.
+ *
+ * Read out the actual temperature offset. The result can be converted to ℃
+ * by dividing it by 100.
+ *
+ * @param[out] temperatureOffset
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t getTemperatureOffset(uint16_t& temperatureOffset);
+
+ /**
+ * @brief Get the configured altitude (height over sea level in m).
+ *
+ * Read out the configured altitude (height in [m] over sea level).
+ *
+ * @param[out] altitude
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t getAltitudeCompensation(uint16_t& altitude);
+
+ /**
+ * @brief Set a new value for altitude.
+ *
+ * Measurements of CO₂ concentration based on the NDIR principle are
+ * influenced by altitude. SCD30 offers to compensate deviations due to
+ * altitude by using this command. Setting altitude is disregarded when an
+ * ambient pressure is given to the sensor (see command
+ * start_periodic_measurement). Altitude value is saved in non-volatile
+ * memory. The last set value will be used for altitude compensation after
+ * repowering.
+ *
+ * @param[in] altitude
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ *
+ * Example:
+ * --------
+ *
+ * @code{.cpp}
+ *
+ * int16_t localError = 0;
+ *
+ * localError = sensor.setAltitudeCompensation(440);
+ * if (localError != NO_ERROR) {
+ * return;
+ * }
+ *
+ * @endcode
+ *
+ */
+ int16_t setAltitudeCompensation(uint16_t altitude);
+
+ /**
+ * @brief Read the version of the current firmware.
+ *
+ * Read the version of the current firmware.
+ *
+ * @param[out] major Major version number.
+ * @param[out] minor Minor version number.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t readFirmwareVersion(uint8_t& major, uint8_t& minor);
+
+ /**
+ * @brief Performs a soft reset of the sensor. The device will be
+ * unavailable for 2 seconds.
+ *
+ * The SCD30 provides a soft reset mechanism that forces the sensor into the
+ * same state as after powering up without the need for removing the
+ * power-supply. It does so by restarting its system controller. After soft
+ * reset the sensor will reload all calibrated data. However, it is worth
+ * noting that the sensor reloads calibration data prior to every
+ * measurement by default. This includes previously set reference values
+ * from ASC or FRC as well as temperature offset values last setting. The
+ * sensor is able to receive the command at any time, regardless of its
+ * internal state.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t softReset(void);
+
+ /**
+ * @brief Read the serial number of the sensor.
+ *
+ * Null-terminated ASCII string containing the serial number. Up to 32
+ * characters can be read from the device.
+ *
+ * @param[out] serialNumber Serial number of the sensor.
+ *
+ * @return error_code 0 on success, an error code otherwise.
+ */
+ int16_t readSerialNumber(int8_t serialNumber[], uint16_t serialNumberSize);
+
+ private:
+ TwoWire* _i2cBus = nullptr;
+ uint8_t _i2cAddress = 0;
+};
+
+#endif // SENSIRIONI2CSCD30_H
\ No newline at end of file
diff --git a/tasmota/tasmota_support/support_a_i2c.ino b/tasmota/tasmota_support/support_a_i2c.ino
index 868bb92aa..8d19889df 100644
--- a/tasmota/tasmota_support/support_a_i2c.ino
+++ b/tasmota/tasmota_support/support_a_i2c.ino
@@ -93,6 +93,28 @@ bool I2cSetClock(uint32_t frequency, uint32_t bus) {
return true;
}
+uint8_t I2cClearBus(uint32_t bus = 0);
+uint8_t I2cClearBus(uint32_t bus) {
+#ifdef ESP8266
+ /**
+ * twi_status() attempts to read out any data left that is holding SDA low, so a new transaction can take place
+ * something like (http://www.forward.com.au/pfod/ArduinoProgramming/I2C_ClearBus/index.html)
+ *
+ * Returns:
+ * 0 - No error
+ * 1 - SCL held low by another device, no procedure available to recover
+ * 2 - I2C bus error. SCL held low beyond slave clock stretch time
+ * 3 - I2C bus error. SDA line held low by slave/another_master after n bits
+ */
+ TwoWire& myWire = I2cGetWire(bus);
+ if (&myWire == nullptr) { return 0; } // No valid I2c bus
+
+ return myWire.status();
+#else
+ return 0;
+#endif
+}
+
/*-------------------------------------------------------------------------------------------*\
* Return code: 0 = Error, 1 = OK
\*-------------------------------------------------------------------------------------------*/
diff --git a/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino b/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino
index cf34e8477..80a757484 100644
--- a/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino
+++ b/tasmota/tasmota_xsns_sensor/xsns_42_scd30.ino
@@ -1,7 +1,7 @@
/*
- xsns_42_scd30.ino - SCD30 CO2 sensor support for Tasmota
+ xsns_42_scd30.ino - Sensirion SCD30 CO2 sensor support for Tasmota
- Copyright (C) 2021 Frogmore42
+ Copyright (C) 2021 Frogmore42, Theo Arends
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
@@ -21,325 +21,483 @@
#ifdef USE_SCD30
/*********************************************************************************************\
* SCD30 NDIR CO2 Temperature and Humidity sensor
+ *
+ * I2C Address: 0x61
\*********************************************************************************************/
-#define XSNS_42 42
-#define XI2C_29 29 // See I2CDEVICES.md
+#define XSNS_42 42
+#define XI2C_29 29 // See I2CDEVICES.md
-//#define SCD30_DEBUG
+#define SCD30_MAX_MISSED_READS 3
+#define SCD30_I2C_BUS_SPEED 50000 // Sensirion recommends to operate the SCD30 at a baud rate of 50 kHz or smaller
-#define SCD30_ADDRESS 0x61
-
-#define SCD30_MAX_MISSED_READS 3
-#define SCD30_STATE_NO_ERROR 0
-#define SCD30_STATE_ERROR_DATA_CRC 1
-#define SCD30_STATE_ERROR_READ_MEAS 2
-#define SCD30_STATE_ERROR_SOFT_RESET 3
-#define SCD30_STATE_ERROR_I2C_RESET 4
-#define SCD30_STATE_ERROR_UNKNOWN 5
-
-const char kScd30Commands[] PROGMEM = "Scd30|" // Prefix
- "Alt|Auto|Cal|FW|Int|Pres|TOff";
-void (*const kScd30Command[])(void) PROGMEM = {
- &CmndScd30Altitude, &CmndScd30AutoMode, &CmndScd30Calibrate, &CmndScd30Firmware, &CmndScd30Interval, &CmndScd30Pressure, &CmndScd30TempOffset };
+//#define SENSIRION_DEBUG // Adds 1k2 to code size
/********************************************************************************************/
-#include
+#include
-FrogmoreScd30 scd30;
+SensirionI2cScd30 scd30;
-struct {
- float humidity = 0.0f;
- float temperature = 0.0f;
- int error_state = SCD30_STATE_NO_ERROR;
- int loop_count = 0;
- int data_not_available_count = 0;
- int good_measure_count = 0;
- int reset_count = 0;
- int error_count = 0;
- int co2_zero_count = 0;
- int i2c_reset_count = 0;
+struct SCD30DATA_s {
+ float humidity;
+ float temperature;
+ uint16_t co2;
+ uint16_t pressure;
uint16_t interval;
- uint16_t co2 = 0;
- uint16_t co2e_avg = 0;
- uint8_t bus;
- bool init_once;
- bool found = false;
- bool data_valid = false;
-} Scd30;
+ uint8_t loop_count;
+ bool data_valid;
+} *SCD30DATA = nullptr;
-void Scd30BusSpeed(bool set) {
- I2cSetClock((set) ? 50000 : 0, Scd30.bus);
+uint8_t scd30_bus = 0;
+bool scd30_init_once = false;
+
+/********************************************************************************************/
+
+bool Scd30Error(const char* func, int error) {
+ bool result = (error != 0);
+ if (result) {
+#ifdef SENSIRION_DEBUG
+ char error_msg[64];
+ errorToString(error, error_msg, sizeof(error_msg));
+ AddLog(LOG_LEVEL_DEBUG, PSTR("SCD: %s error %d %s"), func, error, error_msg);
+#else
+ AddLog(LOG_LEVEL_DEBUG, PSTR("SCD: %s error %d"), func, error);
+#endif
+ }
+ return result;
}
-void Scd30Detect(void) {
- for (Scd30.bus = 0; Scd30.bus < MAX_I2C; Scd30.bus++) {
- Scd30BusSpeed(1);
- if (I2cSetDevice(SCD30_ADDRESS, Scd30.bus)) {
- scd30.begin(&I2cGetWire(Scd30.bus));
+void Scd30BusSpeed(uint32_t bus_speed) {
+ I2cSetClock(bus_speed, scd30_bus);
+}
+
+void Scd30ClearI2CBus(void) {
+#ifdef ESP8266
+ /**
+ * SCD30 driver known issues:
+ *
+ * *softReset()*:
+ * After using the ``softReset()`` function on an Arduino MKR WIFI 1010 (software I2C),
+ * subsequent commands are no longer acknowledged. The I2C line remains low after
+ * receiving the first command byte.
+ *
+ * To make the provided example work on the Arduino MKR WIFI 1010, the call to
+ * ``softReset()`` and the subsequent ``delay()`` can be removed.
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ Scd30Error("ClearI2cBus", I2cClearBus(scd30_bus));
+ Scd30BusSpeed(0);
+#endif
+}
+
+/********************************************************************************************/
+
+void Scd30Init(void) {
+ /**
+ * Maximal I2C speed is 100 kHz and the master has to support clock stretching.
+ * Sensirion recommends to operate the SCD30 at a baud rate of 50 kHz or smaller.
+ * Clock stretching period in write- and read-frames is 30 ms, however, due to
+ * internal calibration processes a maximal clock stretching of 150 ms may occur
+ * once per day.
+ */
+ PowerOnDelay(2000); // Sensor startup time (Time after power-on until I2C communication can be started)
+ bool quit = false;
+ for (scd30_bus = 0; scd30_bus < MAX_I2C; scd30_bus++) {
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if (I2cSetDevice(SCD30_I2C_ADDR_61, scd30_bus)) {
+ scd30.begin(I2cGetWire(scd30_bus), SCD30_I2C_ADDR_61);
+
+ // Don't stop in case of error, try to continue
+ Scd30Error("StopMeasurement", scd30.stopPeriodicMeasurement()); // Performs delay(10)
+ Scd30Error("ReInit", scd30.softReset()); // Performs delay(2000)
+
uint8_t major;
uint8_t minor;
- if (!scd30.getFirmwareVersion(&major, &minor)) {
- if (!scd30.getMeasurementInterval(&Scd30.interval)) {
- if (!scd30.beginMeasuring()) {
- I2cSetActiveFound(SCD30_ADDRESS, "SCD30", Scd30.bus);
- AddLog(LOG_LEVEL_DEBUG, PSTR("SCD: SCD30 v%d.%d"), major, minor);
- Scd30.found = true;
+ if (!Scd30Error("Version", scd30.readFirmwareVersion(major, minor))) {
+
+ int8_t serial_number[32] = { 0 };
+ if (!Scd30Error("Serialnumber", scd30.readSerialNumber(serial_number, sizeof(serial_number)))) {
+
+ uint16_t interval;
+ if (!Scd30Error("GetInterval", scd30.getMeasurementInterval(interval))) {
+
+ if (!Scd30Error("StartMeasurement", scd30.startPeriodicMeasurement(0))) {
+
+ SCD30DATA = (SCD30DATA_s *)calloc(1, sizeof(struct SCD30DATA_s));
+ if (SCD30DATA != nullptr) {
+ SCD30DATA->interval = interval;
+
+ I2cSetActiveFound(SCD30_I2C_ADDR_61, "SCD30", scd30_bus);
+ AddLog(LOG_LEVEL_DEBUG, PSTR("SCD: SCD30 serialnumber %s v%d.%d"), serial_number, major, minor);
+ }
+ quit = true;
+ }
}
}
}
}
Scd30BusSpeed(0);
- if (Scd30.found) { break; }
+ if (quit) { break; }
}
}
// gets data from the sensor every 3 seconds or so to give the sensor time to gather new data
void Scd30Update(void) {
- Scd30.loop_count++;
- if (Scd30.loop_count > (Scd30.interval - 1)) {
- Scd30BusSpeed(1);
- uint32_t error = 0;
- switch (Scd30.error_state) {
- case SCD30_STATE_NO_ERROR: {
- error = scd30.readMeasurement(&Scd30.co2, &Scd30.co2e_avg, &Scd30.temperature, &Scd30.humidity);
- switch (error) {
- case ERROR_SCD30_NO_ERROR:
- Scd30.loop_count = 0;
- Scd30.data_valid = true;
- Scd30.good_measure_count++;
-#ifdef USE_LIGHT
- LightSetSignal(CO2_LOW, CO2_HIGH, Scd30.co2);
-#endif // USE_LIGHT
- break;
+ if (SCD30DATA->loop_count > (SCD30DATA->interval)) {
+ uint16_t data_ready;
+ bool error = false;
- case ERROR_SCD30_NO_DATA:
- Scd30.data_not_available_count++;
- break;
-
- case ERROR_SCD30_CRC_ERROR:
- Scd30.error_state = SCD30_STATE_ERROR_DATA_CRC;
- Scd30.error_count++;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: CRC error, CRC error: %ld, CO2 zero: %ld, good: %ld, no data: %ld, sc30_reset: %ld, i2c_reset: %ld"),
- Scd30.error_count, Scd30.co2_zero_count, Scd30.good_measure_count, Scd30.data_not_available_count, Scd30.reset_count, Scd30.i2c_reset_count);
-#endif
- break;
-
- case ERROR_SCD30_CO2_ZERO:
- Scd30.co2_zero_count++;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: CO2 zero, CRC error: %ld, CO2 zero: %ld, good: %ld, no data: %ld, sc30_reset: %ld, i2c_reset: %ld"),
- Scd30.error_count, Scd30.co2_zero_count, Scd30.good_measure_count, Scd30.data_not_available_count, Scd30.reset_count, Scd30.i2c_reset_count);
-#endif
- break;
-
- default: {
- Scd30.error_state = SCD30_STATE_ERROR_READ_MEAS;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: Update: ReadMeasurement error: 0x%lX, counter: %ld"), error, Scd30.loop_count);
-#endif
- Scd30BusSpeed(0);
- return;
- }
- break;
- }
- }
- break;
-
- case SCD30_STATE_ERROR_DATA_CRC: {
- //Scd30.data_valid = false;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
- Scd30.error_state, Scd30.good_measure_count, Scd30.data_not_available_count, Scd30.reset_count, Scd30.i2c_reset_count);
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: got CRC error, try again, counter: %ld"), Scd30.loop_count);
-#endif
- Scd30.error_state = ERROR_SCD30_NO_ERROR;
- }
- break;
-
- case SCD30_STATE_ERROR_READ_MEAS: {
- //Scd30.data_valid = false;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
- Scd30.error_state, Scd30.good_measure_count, Scd30.data_not_available_count, Scd30.reset_count, Scd30.i2c_reset_count);
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: not answering, sending soft reset, counter: %ld"), Scd30.loop_count);
-#endif
- Scd30.reset_count++;
- error = scd30.softReset();
- if (error) {
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: resetting got error: 0x%lX"), error);
-#endif
- error >>= 8;
- if (error == 4) {
- Scd30.error_state = SCD30_STATE_ERROR_SOFT_RESET;
- } else {
- Scd30.error_state = SCD30_STATE_ERROR_UNKNOWN;
- }
- } else {
- Scd30.error_state = ERROR_SCD30_NO_ERROR;
- }
- }
- break;
-
- case SCD30_STATE_ERROR_SOFT_RESET: {
- //Scd30.data_valid = false;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: in error state: %d, good: %ld, no data: %ld, sc30 reset: %ld, i2c reset: %ld"),
- Scd30.error_state, Scd30.good_measure_count, Scd30.data_not_available_count, Scd30.reset_count, Scd30.i2c_reset_count);
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: clearing i2c bus"));
-#endif
- Scd30.i2c_reset_count++;
- error = scd30.clearI2CBus();
- if (error) {
- Scd30.error_state = SCD30_STATE_ERROR_I2C_RESET;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: error clearing i2c bus: 0x%lX"), error);
-#endif
- } else {
- Scd30.error_state = ERROR_SCD30_NO_ERROR;
- }
- }
- break;
-
- default: {
- //Scd30.data_valid = false;
-#ifdef SCD30_DEBUG
- AddLog(LOG_LEVEL_ERROR, PSTR("SCD30: unknown error state: 0x%lX"), Scd30.error_state);
-#endif
- Scd30.error_state = SCD30_STATE_ERROR_SOFT_RESET; // try again
- }
+ /**
+ * @brief Queries if data is ready for readout.
+ *
+ * Data ready command is used to determine if a measurement can be read from
+ * the sensor’s buffer. Whenever there is a measurement available from the
+ * internal buffer this command returns 1 and 0 otherwise. As soon as the
+ * measurement has been read by SCD30 the return value changes to 0.
+ *
+ * @param[out] dataReadyFlag Data ready flag
+ *
+ * @note The read header should be sent with a delay of >3ms following the
+ * write sequence.
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if (scd30.getDataReady(data_ready)) {
+ delay(100);
+ error = Scd30Error("DataReady", scd30.getDataReady(data_ready));
}
- Scd30BusSpeed(0);
-
- if (Scd30.loop_count > (SCD30_MAX_MISSED_READS * Scd30.interval)) {
- Scd30.data_valid = false;
+ if (!error && data_ready) {
+ /**
+ * @brief Reads out the measurement values.
+ *
+ * Allows to read new measurement data if data is available.
+ *
+ * @param[out] co2Concentration
+ * @param[out] temperature
+ * @param[out] humidity
+ */
+ float co2;
+ float temperature;
+ float humidity;
+ if (scd30.readMeasurementData(co2, temperature, humidity)) {
+ delay(150);
+ error = Scd30Error("Measurement", scd30.readMeasurementData(co2, temperature, humidity));
+ }
+ if (!error) {
+ SCD30DATA->co2 = (uint16_t)co2;
+ SCD30DATA->temperature = temperature;
+ SCD30DATA->humidity = humidity;
+#ifdef USE_LIGHT
+ LightSetSignal(CO2_LOW, CO2_HIGH, SCD30DATA->co2);
+#endif // USE_LIGHT
+ SCD30DATA->loop_count = 0;
+ SCD30DATA->data_valid = true;
+ }
}
}
+
+ SCD30DATA->loop_count++;
+ if (SCD30DATA->loop_count > (SCD30_MAX_MISSED_READS * SCD30DATA->interval)) {
+ SCD30DATA->data_valid = false;
+ AddLog(LOG_LEVEL_DEBUG, PSTR("SCD: Reinit"));
+ Scd30Error("StopMeasurement", scd30.stopPeriodicMeasurement()); // Performs delay(10)
+ Scd30Error("ReInit", scd30.softReset()); // Performs delay(2000)
+ Scd30ClearI2CBus();
+ Scd30Error("Measurement", scd30.startPeriodicMeasurement(SCD30DATA->pressure));
+ SCD30DATA->loop_count = 0;
+ }
+ Scd30BusSpeed(0);
}
/*********************************************************************************************\
- * Command Scd30
+ * Commands
\*********************************************************************************************/
+bool CmndScd30Error(int error) {
+ bool result = (error != 0);
+ if (result) {
+ ResponseCmnd();
+#ifdef SENSIRION_DEBUG
+ char error_msg[64];
+ errorToString(error, error_msg, sizeof(error_msg));
+ ResponseAppend_P(PSTR("{\"Error\":\"%d %s\"}"), error, error_msg);
+#else
+ ResponseAppend_P(PSTR("{\"Error\":%d}"), error);
+#endif
+ }
+ return result;
+}
+
+const char kScd30Commands[] PROGMEM = "Scd30|" // Prefix
+ "Alt|Auto|Cal|Int|Pres|TOff";
+
+void (* const kScd30Command[])(void) PROGMEM = {
+ &CmndScd30Altitude, &CmndScd30AutoMode, &CmndScd30Calibrate,
+ &CmndScd30Interval, &CmndScd30Pressure, &CmndScd30TempOffset };
+
void CmndScd30Altitude(void) {
- uint16_t value = 0;
- Scd30BusSpeed(1);
- if (XdrvMailbox.data_len > 0) {
- value = XdrvMailbox.payload;
- scd30.setAltitudeCompensation(value);
- } else {
- scd30.getAltitudeCompensation(&value);
+ /**
+ * Scd30Alt 440
+ *
+ * @brief Set a new value for altitude.
+ *
+ * Measurements of CO₂ concentration based on the NDIR principle are
+ * influenced by altitude. SCD30 offers to compensate deviations due to
+ * altitude by using this command. Setting altitude is disregarded when an
+ * ambient pressure is given to the sensor (see command
+ * start_periodic_measurement). Altitude value is saved in non-volatile
+ * memory. The last set value will be used for altitude compensation after
+ * repowering.
+ *
+ * @param[in] altitude
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if (XdrvMailbox.payload >= 0) {
+ scd30.setAltitudeCompensation(XdrvMailbox.payload);
+ }
+ /**
+ * Scd30Alt
+ *
+ * @brief Get the configured altitude (height over sea level in m).
+ *
+ * Read out the configured altitude (height in [m] over sea level).
+ *
+ * @param[out] altitude
+ */
+ uint16_t altitude;
+ if (!CmndScd30Error(scd30.getAltitudeCompensation(altitude))) {
+ ResponseCmndNumber(altitude);
}
Scd30BusSpeed(0);
- ResponseCmndNumber(value);
};
void CmndScd30AutoMode(void) {
- uint16_t value = 0;
- Scd30BusSpeed(1);
- if (XdrvMailbox.data_len > 0) {
- value = XdrvMailbox.payload;
- scd30.setCalibrationType(value);
- } else {
- scd30.getCalibrationType(&value);
+ /**
+ * Scd30Auto 1
+ *
+ * @brief Activates or deactivates continuous automatic self-calibration.
+ *
+ * Continuous automatic self-calibration (ASC) can be (de-)activated with
+ * this command. When activated for the first time a period of minimum 7
+ * days is needed so that the algorithm can find its initial parameter set
+ * for ASC. The sensor has to be exposed to fresh air for at least 1 hour
+ * every day. Also during that period, the sensor may not be disconnected
+ * from the power supply. Otherwise the procedure to find calibration
+ * parameters is aborted and has to be restarted from the beginning. The
+ * successfully calculated parameters are stored in non-volatile memory of
+ * the SCD30 having the effect that after a restart the previously found
+ * parameters for ASC are still present.
+ *
+ * @param[in] doActivate Set activate flag.
+ *
+ * @note Note that the most recently found self-calibration parameters will
+ * be actively used for self-calibration disregarding the status of this
+ * feature. Finding a new parameter set by the here described method will
+ * always overwrite the settings from external recalibration and vice-versa.
+ * The feature is switched off by default. To work properly SCD30 has to see
+ * fresh air on a regular basis. Optimal working conditions are given when
+ * the sensor sees fresh air for one hour every day so that ASC can
+ * constantly re-calibrate. ASC only works in continuous measurement mode.
+ * ASC status is saved in non-volatile memory. When the sensor is powered
+ * down while ASC is activated SCD30 will continue with automatic
+ * self-calibration after repowering without sending the command.
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) {
+ scd30.activateAutoCalibration(XdrvMailbox.payload);
+ }
+ /**
+ * Scd30Auto
+ *
+ * @brief Gets the status of auto calibration.
+ *
+ * Read out the status of the active self calibration.
+ *
+ * @param[out] isActive Indication if automatic calibration is active
+ */
+ uint16_t state;
+ if (!CmndScd30Error(scd30.getAutoCalibrationStatus(state))) {
+ ResponseCmndStateText(state);
}
Scd30BusSpeed(0);
- ResponseCmndNumber(value);
};
void CmndScd30Calibrate(void) {
- uint16_t value = 0;
- Scd30BusSpeed(1);
- if (XdrvMailbox.data_len > 0) {
- value = XdrvMailbox.payload;
- scd30.setForcedRecalibrationFactor(value);
- } else {
- scd30.getForcedRecalibrationFactor(&value);
+ /**
+ * Scd30Cal 420
+ *
+ * @brief Forces recalibration with a new value for the CO₂ concentration.
+ *
+ * Forced recalibration (FRC) is used to compensate for sensor drifts when a
+ * reference value of the CO₂ concentration in close proximity to the SCD30
+ * is available. For best results, the sensor has to be run in a stable
+ * environment in continuous mode at a measurement rate of 2s for at least
+ * two minutes before applying the FRC command and sending the reference
+ * value. Setting a reference CO₂ concentration by the method described here
+ * will always supersede corrections from the ASC (see command
+ * activate_auto_calibration) and vice-versa. The reference CO₂
+ * concentration has to be within the range 400 ppm ≤ cref(CO₂) ≤ 2000 ppm.
+ * The FRC method imposes a permanent update of the CO₂ calibration curve
+ * which persists after repowering the sensor. The most recently used
+ * reference value is retained in volatile memory and can be read out with
+ * the command sequence given below. After repowering the sensor, the
+ * command will return the standard reference value of 400 ppm.
+ *
+ * @param[in] co2RefConcentration New CO2 reference concentration.
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if ((XdrvMailbox.payload >= 400) && (XdrvMailbox.payload <= 2000)) {
+ CmndScd30Error(scd30.forceRecalibration(XdrvMailbox.payload)); // Performs internal delay(10)
+ }
+ /**
+ * Scd30Cal
+ *
+ * @brief Gets the force recalibration status.
+ *
+ * Read out the CO₂ reference concentration.
+ *
+ * @param[out] co2RefConcentration Currently used CO2 reference
+ * concentration (default 400).
+ */
+ uint16_t co2RefConcentration;
+ if (!CmndScd30Error(scd30.getForceRecalibrationStatus(co2RefConcentration))) {
+ ResponseCmndNumber(co2RefConcentration);
}
Scd30BusSpeed(0);
- ResponseCmndNumber(value);
-};
-
-void CmndScd30Firmware(void) {
- uint8_t major = 0;
- uint8_t minor = 0;
- Scd30BusSpeed(1);
- int error = scd30.getFirmwareVersion(&major, &minor);
- Scd30BusSpeed(0);
- if (!error) {
- float firmware = major + ((float)minor / 100);
- ResponseCmndFloat(firmware, 2);
- }
};
void CmndScd30Interval(void) {
- uint16_t value = 0;
- Scd30BusSpeed(1);
- if (XdrvMailbox.data_len > 0) {
- value = XdrvMailbox.payload;
- int error = scd30.setMeasurementInterval(value);
- if (!error) {
- Scd30.interval = value;
+ /**
+ * Scd30Int 4
+ *
+ * @brief Sets the interval used to measure in continuous measurement mode.
+ *
+ * Sets the interval used by the SCD30 sensor to measure in continuous
+ * measurement mode. Initial value is 2s. The chosen measurement interval is
+ * saved in non-volatile memory and thus is not reset to its initial value
+ * after power up.
+ *
+ * @param[in] interval Measurement interval in seconds.
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if ((XdrvMailbox.payload >= 2) && (XdrvMailbox.payload <= 1800)) {
+ if (!CmndScd30Error(scd30.setMeasurementInterval(XdrvMailbox.payload))) {
+ SCD30DATA->interval = XdrvMailbox.payload;
}
}
- scd30.getMeasurementInterval(&value);
+ /**
+ * Scd30Int
+ *
+ * @brief Read the configured measurement interval.
+ *
+ * Reads out the active measurement interval.
+ *
+ * @param[out] interval Configured measurement interval
+ */
+ uint16_t interval;
+ if (!CmndScd30Error(scd30.getMeasurementInterval(interval))) {
+ ResponseCmndNumber(interval);
+ }
Scd30BusSpeed(0);
- ResponseCmndNumber(value);
};
void CmndScd30Pressure(void) {
- uint16_t value = 0;
- Scd30BusSpeed(1);
- if (XdrvMailbox.data_len > 0) {
- value = XdrvMailbox.payload;
- scd30.setAmbientPressure(value);
- } else {
- scd30.getAmbientPressure(&value);
+ // Scd30Pres 1013
+ // Scd30Pres
+ if ((0 == XdrvMailbox.payload) || ((XdrvMailbox.payload >= 700) && (XdrvMailbox.payload <= 1400))) {
+ if (XdrvMailbox.payload != SCD30DATA->pressure) {
+ /**
+ * @brief Starts continuous measurement of CO₂, relative humidity and
+ * temperature.
+ *
+ * Starts continuous measurement of the SCD30 to measure CO₂ concentration,
+ * humidity and temperature. Measurement data which is not read from the
+ * sensor will be overwritten. The CO₂ measurement value can be compensated
+ * for ambient pressure by feeding the pressure value in mBar to the sensor.
+ * Setting the ambient pressure will overwrite previous settings of altitude
+ * compensation. Setting the argument to zero will deactivate the ambient
+ * pressure compensation (default ambient pressure = 1013.25 mBar). For
+ * setting a new ambient pressure when continuous measurement is running the
+ * whole command has to be written to SCD30.
+ *
+ * @param[in] ambientPressure Ambient pressure in millibar (0, 700 to 1400).
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if (!CmndScd30Error(scd30.startPeriodicMeasurement(XdrvMailbox.payload))) {
+ SCD30DATA->pressure = XdrvMailbox.payload;
+ }
+ Scd30BusSpeed(0);
+ }
}
- Scd30BusSpeed(0);
- ResponseCmndNumber(value);
+ ResponseCmndNumber(SCD30DATA->pressure);
};
void CmndScd30TempOffset(void) {
- uint16_t value = 0;
- Scd30BusSpeed(1);
- if (XdrvMailbox.data_len > 0) {
- value = XdrvMailbox.payload;
- scd30.setTemperatureOffset(value);
- } else {
- scd30.getTemperatureOffset(&value);
+ /**
+ * Scd30TOff 4.2
+ *
+ * @brief Set the temperature offset. Unit ℃ * 100.
+ *
+ * The on-board RH/T sensor is influenced by thermal self-heating of SCD30
+ * and other electrical components. Design-in alters the thermal properties
+ * of SCD30 such that temperature and humidity offsets may occur when
+ * operating the sensor in end-customer devices. Compensation of those
+ * effects is achievable by writing the temperature offset found in
+ * continuous operation of the device into the sensor. Temperature offset
+ * value is saved in non-volatile memory. The last set value will be used
+ * for temperature offset compensation after repowering.
+ *
+ * @param[in] temperatureOffset Temperature in ℃ * 100 (0 to 2000).
+ */
+ Scd30BusSpeed(SCD30_I2C_BUS_SPEED);
+ if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 20)) {
+ float offset_f = CharToFloat(XdrvMailbox.data); // 0 to 20.00
+ uint16_t offset = offset_f * 100.0;
+ scd30.setTemperatureOffset(offset);
+ }
+ /**
+ * Scd30TOff
+ *
+ * @brief Get the temperature offset. Unit ℃ * 100.
+ *
+ * Read out the actual temperature offset. The result can be converted to ℃
+ * by dividing it by 100.
+ *
+ * @param[out] temperatureOffset
+ */
+ uint16_t offset;
+ if (!CmndScd30Error(scd30.getTemperatureOffset(offset))) {
+ float offset_f = offset / 100.0;
+ ResponseCmndFloat(offset_f, Settings->flag2.temperature_resolution); // TempRes
}
Scd30BusSpeed(0);
- ResponseCmndNumber(value);
};
-/********************************************************************************************/
+/*********************************************************************************************\
+ * Presentation
+\*********************************************************************************************/
void Scd30Show(bool json) {
- if (Scd30.data_valid) {
- float t = ConvertTemp(Scd30.temperature);
- float h = ConvertHumidity(Scd30.humidity);
+ if (!SCD30DATA->data_valid) { return; }
- if (json) {
- ResponseAppend_P(PSTR(",\"SCD30\":{\"" D_JSON_CO2 "\":%d,\"" D_JSON_ECO2 "\":%d,"), Scd30.co2, Scd30.co2e_avg);
- ResponseAppendTHD(t, h);
- ResponseJsonEnd();
+ float t = ConvertTemp(SCD30DATA->temperature);
+ float h = ConvertHumidity(SCD30DATA->humidity);
+
+ if (json) {
+ ResponseAppend_P(PSTR(",\"SCD30\":{\"" D_JSON_CO2 "\":%d,"), SCD30DATA->co2);
+ ResponseAppendTHD(t, h);
+ ResponseJsonEnd();
#ifdef USE_DOMOTICZ
- if (0 == TasmotaGlobal.tele_period) {
- DomoticzSensor(DZ_AIRQUALITY, Scd30.co2);
- DomoticzTempHumPressureSensor(t, h);
- }
+ if (0 == TasmotaGlobal.tele_period) {
+ DomoticzSensor(DZ_AIRQUALITY, SCD30DATA->co2);
+ DomoticzTempHumPressureSensor(t, h);
+ }
#endif // USE_DOMOTICZ
#ifdef USE_WEBSERVER
- } else {
- WSContentSend_PD(HTTP_SNS_CO2EAVG, "SCD30", Scd30.co2e_avg);
- WSContentSend_PD(HTTP_SNS_CO2, "SCD30", Scd30.co2);
- WSContentSend_THD("SCD30", t, h);
+ } else {
+ WSContentSend_PD(HTTP_SNS_CO2, "SCD30", SCD30DATA->co2);
+ WSContentSend_THD("SCD30", t, h);
#endif // USE_WEBSERVER
- }
}
}
@@ -358,18 +516,15 @@ bool Xsns42(uint32_t function) {
Scd30Detect();
}
*/
- if (!Scd30.init_once && (FUNC_EVERY_SECOND == function) && (TasmotaGlobal.uptime > 3)) {
- Scd30.init_once = true;
- Scd30Detect();
+ if (!scd30_init_once && (FUNC_EVERY_SECOND == function) && (TasmotaGlobal.uptime > 3)) {
+ scd30_init_once = true;
+ Scd30Init();
}
- else if (Scd30.found) {
+ else if (SCD30DATA) {
switch (function) {
case FUNC_EVERY_SECOND:
Scd30Update();
break;
- case FUNC_COMMAND:
- result = DecodeCommand(kScd30Commands, kScd30Command);
- break;
case FUNC_JSON_APPEND:
Scd30Show(1);
break;
@@ -378,6 +533,9 @@ bool Xsns42(uint32_t function) {
Scd30Show(0);
break;
#endif // USE_WEBSERVER
+ case FUNC_COMMAND:
+ result = DecodeCommand(kScd30Commands, kScd30Command);
+ break;
}
}
return result;