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;