diff --git a/.github/workflows/Arduino-Lint-Check.yml b/.github/workflows/Arduino-Lint-Check.yml index ee49e2f..603bcdc 100644 --- a/.github/workflows/Arduino-Lint-Check.yml +++ b/.github/workflows/Arduino-Lint-Check.yml @@ -1,17 +1,27 @@ name: Arduino Lint Check on: push: - branches: [ master ] + branches: [ master, main ] pull_request: - branches: [ master ] + branches: [ master, main ] + workflow_dispatch: + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: lint: name: Lint Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: arduino/arduino-lint-action@v1 + - uses: actions/checkout@v4 + - uses: arduino/arduino-lint-action@v2 with: library-manager: update compliance: strict - project-type: all \ No newline at end of file + project-type: all diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index 971a61e..313a864 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -16,10 +16,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -32,10 +28,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -44,10 +36,19 @@ on: - '**arduino-esp-v2-build-check.yml' workflow_dispatch: +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.unit }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest + timeout-minutes: 5 strategy: fail-fast: false @@ -61,6 +62,7 @@ jobs: unit: - UnitCO2 + - UnitCO2L - UnitENVIII - UnitENVIV - UnitTVOC diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 755487e..b75ebf8 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -16,10 +16,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -32,10 +28,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -44,10 +36,19 @@ on: - '**arduino-esp-v3-build-check.yml' workflow_dispatch: +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.unit }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest + timeout-minutes: 5 strategy: fail-fast: false @@ -61,6 +62,7 @@ jobs: unit: - UnitCO2 + - UnitCO2L - UnitENVIII - UnitENVIV - UnitTVOC diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index d6be848..54b8e8a 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -16,10 +16,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -32,10 +28,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -44,10 +36,19 @@ on: - '**arduino-m5-build-check.yml' workflow_dispatch: +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.unit }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest + timeout-minutes: 5 strategy: fail-fast: false @@ -61,6 +62,7 @@ jobs: unit: - UnitCO2 + - UnitCO2L - UnitENVIII - UnitENVIV - UnitTVOC diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 1bf4498..3453b52 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -31,6 +31,14 @@ on: - '**.clang-format' workflow_dispatch: +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: formatting-check: name: Formatting Check diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml index 0a1ee95..83c1c96 100644 --- a/.github/workflows/doxygen-gh-pages.yml +++ b/.github/workflows/doxygen-gh-pages.yml @@ -3,6 +3,14 @@ on: [release, workflow_dispatch] # branches: # - main # - master +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 6be49f7..41f67fc 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -12,10 +12,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -29,10 +25,6 @@ on: - 'src/unit/**.hpp' - 'src/unit/**.h' - 'src/unit/**.c' - - 'test/**.cpp' - - 'test/**.hpp' - - 'test/**.h' - - 'test/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -42,10 +34,19 @@ on: - '**platformio.ini' workflow_dispatch: +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.unit }}:${{ matrix.example }}@${{ matrix.board }}:${{ matrix.framework }}:${{ matrix.espressif32 }} runs-on: ubuntu-latest + timeout-minutes: 5 strategy: fail-fast: false @@ -57,6 +58,7 @@ jobs: unit: - UnitCO2 + - UnitCO2L - UnitENVIII - UnitENVIV - UnitTVOC diff --git a/README.md b/README.md index 4818aaf..feb26b4 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,13 @@ CO2 is a photoacoustic Carbon Dioxide (CO2) Unit that will tell you the CO2 PPM Library for Unit ENV using [M5UnitUnified](https://github.com/m5stack/M5UnitUnified). M5UnitUnified is a library for unified handling of various M5 units products. -M5UnitUnified currently supports the following units SKU:U001-C, SKU:U103, SKU:U169, and SKU:U088. +### Supported units +- Unit CO2 (SKU:U103) +- Unit CO2L (SKU:U104) +- Unit ENVIII (SKU:U001-C) +- Unit ENVIV (SKU:U001-D) +- Unit ENVPro (SKU:U169) +- Unit TVOC (SKU:U088) ### SKU:U088 @@ -67,12 +73,6 @@ See also examples using conventional methods here. #include // For UnitUnified //#include // When using M5UnitUnified, do not use it at the same time as conventional libraries ``` -### Supported units -- Unit CO2 -- Unit ENVIII -- Unit ENVPro -- Unit TVOC - Supported units will be added in the future. ### Required Libraries: diff --git a/examples/UnitUnified/UnitCO2/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/UnitCO2/PlotToSerial/main/PlotToSerial.cpp index 1c269e7..00c06f6 100644 --- a/examples/UnitUnified/UnitCO2/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/UnitCO2/PlotToSerial/main/PlotToSerial.cpp @@ -42,6 +42,7 @@ void setup() #else #pragma message "Using Wire" // Using TwoWire + Wire.end(); Wire.begin(pin_num_sda, pin_num_scl, 400000U); if (!Units.add(unit, Wire) || !Units.begin()) { M5_LOGE("Failed to begin"); @@ -54,6 +55,35 @@ void setup() M5_LOGI("M5UnitUnified has been begun"); M5_LOGI("%s", Units.debugInfo().c_str()); + { + auto ret = unit.stopPeriodicMeasurement(); + float offset{}; + ret &= unit.readTemperatureOffset(offset); + uint16_t altitude{}; + ret &= unit.readSensorAltitude(altitude); + uint16_t pressure{}; + ret &= unit.readAmbientPressure(pressure); + bool asc{}; + ret &= unit.readAutomaticSelfCalibrationEnabled(asc); + uint16_t ppm{}; + ret &= unit.readAutomaticSelfCalibrationTarget(ppm); + ret &= unit.startPeriodicMeasurement(); + + M5.Log.printf( + " temp offset:%f\n" + " sensor altitude:%u\n" + "ambient pressure:%u\n" + " ASC enabled:%u\n" + " ASC target:%u\n", + offset, altitude, pressure, asc, ppm); + + if (!ret) { + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(1000); + } + } + } lcd.clear(TFT_DARKGREEN); } @@ -63,6 +93,7 @@ void loop() Units.update(); if (unit.updated()) { // Can be checked e.g. by serial plotters - M5_LOGI("\n>CO2:%u\n>Temperature:%2.2f\n>Humidity:%2.2f", unit.co2(), unit.temperature(), unit.humidity()); + M5.Log.printf(">CO2:%u\n>Temperature:%2.2f\n>Humidity:%2.2f\n", unit.co2(), unit.temperature(), + unit.humidity()); } } diff --git a/examples/UnitUnified/UnitCO2L/PlotToSerial/PlotToSerial.ino b/examples/UnitUnified/UnitCO2L/PlotToSerial/PlotToSerial.ino new file mode 100644 index 0000000..15b19df --- /dev/null +++ b/examples/UnitUnified/UnitCO2L/PlotToSerial/PlotToSerial.ino @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Example using M5UnitUnified for UnitCO2L + Required + - M5Unified: https://github.com/m5stack/M5Unified +*/ +#include "main/PlotToSerial.cpp" diff --git a/examples/UnitUnified/UnitCO2L/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/UnitCO2L/PlotToSerial/main/PlotToSerial.cpp new file mode 100644 index 0000000..34e4f9e --- /dev/null +++ b/examples/UnitUnified/UnitCO2L/PlotToSerial/main/PlotToSerial.cpp @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Example using M5UnitUnified for UnitCO2L +*/ +// #define USING_M5HAL // When using M5HAL +#include +#include +#include + +namespace { +auto& lcd = M5.Display; +m5::unit::UnitUnified Units; +// m5::unit::UnitCO2 unit; +m5::unit::UnitCO2L unit; + +} // namespace + +using namespace m5::unit::scd4x; + +void setup() +{ + M5.begin(); + + auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda); + auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl); + M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + +#if defined(USING_M5HAL) +#pragma message "Using M5HAL" + // Using M5HAL + m5::hal::bus::I2CBusConfig i2c_cfg; + i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); + i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); + auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); + if (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } +#else +#pragma message "Using Wire" + // Using TwoWire + Wire.end(); + Wire.begin(pin_num_sda, pin_num_scl, 400000U); + if (!Units.add(unit, Wire) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } +#endif + M5_LOGI("M5UnitUnified has been begun"); + M5_LOGI("%s", Units.debugInfo().c_str()); + + { + auto ret = unit.stopPeriodicMeasurement(); + float offset{}; + ret &= unit.readTemperatureOffset(offset); + uint16_t altitude{}; + ret &= unit.readSensorAltitude(altitude); + uint16_t pressure{}; + ret &= unit.readAmbientPressure(pressure); + bool asc{}; + ret &= unit.readAutomaticSelfCalibrationEnabled(asc); + uint16_t ppm{}; + ret &= unit.readAutomaticSelfCalibrationTarget(ppm); + uint16_t initialPeriod{}, standardPeriod{}; + ret &= unit.readAutomaticSelfCalibrationInitialPeriod(initialPeriod); + ret &= unit.readAutomaticSelfCalibrationStandardPeriod(standardPeriod); + + ret &= unit.startPeriodicMeasurement(); + + M5.Log.printf( + " temp offset:%f\n" + " sensor altitude:%u\n" + "ambient pressure:%u\n" + " ASC enabled:%u\n" + " ASC target:%u\n" + " initial period:%u\n" + " standard period:%u\n", + offset, altitude, pressure, asc, ppm, initialPeriod, standardPeriod); + + if (!ret) { + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(1000); + } + } + } + lcd.clear(TFT_DARKGREEN); +} + +void loop() +{ + M5.update(); + auto touch = M5.Touch.getDetail(); + + // Periodic + Units.update(); + if (unit.updated()) { + // Can be checked e.g. by serial plotters + M5.Log.printf(">CO2:%u\n>Temperature:%2.2f\n>Humidity:%2.2f\n", unit.co2(), unit.temperature(), + unit.humidity()); + } + + // Single shot + if (M5.BtnA.wasClicked() || touch.wasClicked()) { + static bool all{}; // false: RHT only + all = !all; + M5.Log.printf("Try single shot %u, waiting measurement...\n", all); + unit.stopPeriodicMeasurement(); + Data d{}; + if (all) { + if (unit.measureSingleshot(d)) { + M5.Log.printf(" SingleAll: %u/%2.2f/%2.2f\n", d.co2(), d.temperature(), d.humidity()); + } + } else { + if (unit.measureSingleshotRHT(d)) { + M5.Log.printf(" SingleRHT: %2.2f/%2.2f", d.temperature(), d.humidity()); + } + } + unit.startPeriodicMeasurement(); + } +} diff --git a/library.json b/library.json index a05ed01..081053b 100644 --- a/library.json +++ b/library.json @@ -15,10 +15,14 @@ "BME68x Sensor library": "https://github.com/boschsensortec/Bosch-BME68x-Library.git", "bsec2": "https://github.com/boschsensortec/Bosch-BSEC2-Library" }, - "version": "1.2.0", + "version": "1.2.1", "frameworks": [ "arduino" ], + "headers": [ + "M5UnitENV.h", + "M5UnitUnifiedENV.h" + ], "platforms": "espressif32", "license": "MIT", "export": { @@ -26,4 +30,4 @@ "docs/html" ] } -} \ No newline at end of file +} diff --git a/library.properties b/library.properties index 58d8f06..db08c4a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unit-ENV -version=1.2.0 +version=1.2.1 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack UNIT ENV @@ -8,4 +8,4 @@ category=Device Control url=https://github.com/m5stack/M5Unit-ENV architectures=esp32 includes=M5UnitENV.h, M5UnitUnifiedENV.h -depends=BME68x Sensor library,M5UnitUnified,M5Utility,M5HAL,bsec2 +depends=M5UnitUnified,M5Utility,M5HAL,bsec2,BME68x Sensor library diff --git a/platformio.ini b/platformio.ini index 162d185..91c2c9f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -91,7 +91,7 @@ extends = m5base board = m5stack-nanoc6 platform = https://github.com/platformio/platform-espressif32.git platform_packages = - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git + platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.7 platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1 board_build.partitions = default.csv lib_deps = ${env.lib_deps} diff --git a/src/M5UnitUnifiedENV.hpp b/src/M5UnitUnifiedENV.hpp index ff251dd..2b2e14f 100644 --- a/src/M5UnitUnifiedENV.hpp +++ b/src/M5UnitUnifiedENV.hpp @@ -46,6 +46,7 @@ namespace m5 { namespace unit { using UnitCO2 = m5::unit::UnitSCD40; +using UnitCO2L = m5::unit::UnitSCD41; using UnitENVPro = m5::unit::UnitBME688; using UnitTVOC = m5::unit::UnitSGP30; diff --git a/src/unit/unit_BME688.cpp b/src/unit/unit_BME688.cpp index 3daf988..ff3ab3b 100644 --- a/src/unit/unit_BME688.cpp +++ b/src/unit/unit_BME688.cpp @@ -127,7 +127,7 @@ float Data::get(const bsec_virtual_sensor_t vs) const // const char UnitBME688::name[] = "UnitBME688"; const types::uid_t UnitBME688::uid{"UnitBME688"_mmh3}; -const types::uid_t UnitBME688::attr{0}; +const types::attr_t UnitBME688::attr{}; // I2C accessor int8_t UnitBME688::read_function(uint8_t reg_addr, uint8_t* reg_data, uint32_t length, void* intf_ptr) diff --git a/src/unit/unit_BMP280.cpp b/src/unit/unit_BMP280.cpp index d63ee27..2457ce0 100644 --- a/src/unit/unit_BMP280.cpp +++ b/src/unit/unit_BMP280.cpp @@ -240,7 +240,7 @@ float Data::pressure() const const char UnitBMP280::name[] = "UnitBMP280"; const types::uid_t UnitBMP280::uid{"UnitBMP280"_mmh3}; -const types::uid_t UnitBMP280::attr{0}; +const types::attr_t UnitBMP280::attr{}; bool UnitBMP280::begin() { diff --git a/src/unit/unit_ENV3.cpp b/src/unit/unit_ENV3.cpp index 76b96f6..7b64502 100644 --- a/src/unit/unit_ENV3.cpp +++ b/src/unit/unit_ENV3.cpp @@ -17,7 +17,7 @@ using namespace m5::utility::mmh3; const char UnitENV3::name[] = "UnitENVIII"; const types::uid_t UnitENV3::uid{"UnitENVIII"_mmh3}; -const types::uid_t UnitENV3::attr{0}; +const types::attr_t UnitENV3::attr{0}; UnitENV3::UnitENV3(const uint8_t addr) : Component(addr) { diff --git a/src/unit/unit_ENV4.cpp b/src/unit/unit_ENV4.cpp index cf19ddd..49f6836 100644 --- a/src/unit/unit_ENV4.cpp +++ b/src/unit/unit_ENV4.cpp @@ -17,7 +17,7 @@ using namespace m5::utility::mmh3; const char UnitENV4::name[] = "UnitENVIV"; const types::uid_t UnitENV4::uid{"UnitENVIV"_mmh3}; -const types::uid_t UnitENV4::attr{0}; +const types::attr_t UnitENV4::attr{0}; UnitENV4::UnitENV4(const uint8_t addr) : Component(addr) { diff --git a/src/unit/unit_QMP6988.cpp b/src/unit/unit_QMP6988.cpp index 6bcb99e..47684d7 100644 --- a/src/unit/unit_QMP6988.cpp +++ b/src/unit/unit_QMP6988.cpp @@ -210,7 +210,7 @@ float Data::pressure() const // const char UnitQMP6988::name[] = "UnitQMP6988"; const types::uid_t UnitQMP6988::uid{"UnitQMP6988"_mmh3}; -const types::uid_t UnitQMP6988::attr{0}; +const types::attr_t UnitQMP6988::attr{}; types::elapsed_time_t calculatInterval(const Standby st, const Oversampling ost, const Oversampling osp, const Filter f) { @@ -259,6 +259,7 @@ bool UnitQMP6988::begin() M5_LIB_LOGE("Failed to read_calibration"); return false; } + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.osrs_pressure, _cfg.osrs_temperature, _cfg.filter, _cfg.standby) : true; diff --git a/src/unit/unit_SCD40.cpp b/src/unit/unit_SCD40.cpp index ca1ab3e..2312e1e 100644 --- a/src/unit/unit_SCD40.cpp +++ b/src/unit/unit_SCD40.cpp @@ -40,6 +40,8 @@ constexpr uint32_t interval_table[] = { 30 * 1000U, // 30 Sec. }; +const uint8_t VARIANT_VALUE[2]{0x04, 0x40}; // SCD40 + } // namespace namespace m5 { @@ -71,7 +73,7 @@ float Data::humidity() const // class UnitSCD40 const char UnitSCD40::name[] = "UnitSCD40"; const types::uid_t UnitSCD40::uid{"UnitSCD40"_mmh3}; -const types::uid_t UnitSCD40::attr{0}; +const types::attr_t UnitSCD40::attr{0}; bool UnitSCD40::begin() { @@ -85,30 +87,48 @@ bool UnitSCD40::begin() } } - if (!stopPeriodicMeasurement()) { + // Stop (to idle mode) + if (!writeRegister(STOP_PERIODIC_MEASUREMENT)) { M5_LIB_LOGE("Failed to stop"); return false; } + m5::utility::delay(STOP_PERIODIC_MEASUREMENT_DURATION); + + if (!is_valid_chip()) { + return false; + } if (!writeAutomaticSelfCalibrationEnabled(_cfg.calibration)) { - M5_LIB_LOGE("Failed to write automatic calibration"); + M5_LIB_LOGE("Failed to writeAutomaticSelfCalibrationEnabled"); return false; } + // Stop + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.mode) : true; } +bool UnitSCD40::is_valid_chip() +{ + uint8_t var[2]{}; + if (!read_register(GET_SENSOR_VARIANT, var, 2) || memcmp(var, VARIANT_VALUE, 2) != 0) { + M5_LIB_LOGE("Not SCD40 %02X:%02X", var[0], var[1]); + return false; + } + return true; +} + void UnitSCD40::update(const bool force) { _updated = false; if (inPeriodic()) { - unsigned long at{m5::utility::millis()}; + auto at = m5::utility::millis(); if (force || !_latest || at >= _latest + _interval) { Data d{}; _updated = read_measurement(d); if (_updated) { + _latest = m5::utility::millis(); // Data acquisition takes time, so acquire again _data->push_back(d); - _latest = at; } } } @@ -131,10 +151,12 @@ bool UnitSCD40::start_periodic_measurement(const Mode mode) bool UnitSCD40::stop_periodic_measurement(const uint32_t duration) { - if (writeRegister(STOP_PERIODIC_MEASUREMENT)) { - _periodic = false; - m5::utility::delay(duration); - return true; + if (inPeriodic()) { + if (writeRegister(STOP_PERIODIC_MEASUREMENT)) { + _periodic = false; + m5::utility::delay(duration); + return true; + } } return false; } @@ -150,14 +172,11 @@ bool UnitSCD40::writeTemperatureOffset(const float offset, const uint32_t durati return false; } - m5::types::big_uint16_t u16(Temperature::toUint16(offset)); - m5::utility::CRC8_Checksum crc{}; - std::array buf{u16.u8[0], u16.u8[1], crc.range(u16.data(), u16.size())}; - if (writeRegister(SET_TEMPERATURE_OFFSET, buf.data(), buf.size())) { - m5::utility::delay(duration); - return true; - } - return false; + uint8_t wbuf[2]{}; + uint16_t tmp16 = Temperature::toUint16(offset); + wbuf[0] = tmp16 >> 8; + wbuf[1] = tmp16 & 0xFF; + return write_register(SET_TEMPERATURE_OFFSET, wbuf, sizeof(wbuf)) && delay_true(duration); } bool UnitSCD40::readTemperatureOffset(float& offset) @@ -168,10 +187,12 @@ bool UnitSCD40::readTemperatureOffset(float& offset) return false; } - uint16_t u16{}; - auto ret = readRegister16(GET_TEMPERATURE_OFFSET, u16, GET_TEMPERATURE_OFFSET_DURATION); - offset = Temperature::toFloat(u16); - return ret; + m5::types::big_uint16_t u16{}; + if (read_register(GET_TEMPERATURE_OFFSET, u16.data(), u16.size(), GET_TEMPERATURE_OFFSET_DURATION)) { + offset = Temperature::toFloat(u16.get()); + return true; + } + return false; } bool UnitSCD40::writeSensorAltitude(const uint16_t altitude, const uint32_t duration) @@ -182,36 +203,56 @@ bool UnitSCD40::writeSensorAltitude(const uint16_t altitude, const uint32_t dura } m5::types::big_uint16_t u16(altitude); - m5::utility::CRC8_Checksum crc{}; - std::array buf{u16.u8[0], u16.u8[1], crc.range(u16.data(), u16.size())}; - if (writeRegister(SET_SENSOR_ALTITUDE, buf.data(), buf.size())) { - m5::utility::delay(duration); + return write_register(SET_SENSOR_ALTITUDE, u16.data(), u16.size()) && delay_true(duration); +} + +bool UnitSCD40::readSensorAltitude(uint16_t& altitude) +{ + altitude = 0; + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + + m5::types::big_uint16_t u16{}; + if (read_register(GET_SENSOR_ALTITUDE, u16.data(), u16.size(), GET_SENSOR_ALTITUDE_DURATION)) { + altitude = u16.get(); return true; } return false; } -bool UnitSCD40::readSensorAltitude(uint16_t& altitude) +bool UnitSCD40::writeAmbientPressure(const uint16_t pressure, const uint32_t duration) { - altitude = 0; + constexpr uint32_t PRESSURE_MIN{700}; + constexpr uint32_t PRESSURE_MAX{1200}; + +#if 0 if (inPeriodic()) { M5_LIB_LOGD("Periodic measurements are running"); return false; } - return readRegister16(GET_SENSOR_ALTITUDE, altitude, GET_SENSOR_ALTITUDE_DURATION); +#endif + if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) { + M5_LIB_LOGE("pressure is not a valid scope (%u - %u) %u", PRESSURE_MIN, PRESSURE_MAX, pressure); + return false; + } + m5::types::big_uint16_t u16(pressure); + return write_register(AMBIENT_PRESSURE, u16.data(), u16.size()) && delay_true(duration); } -bool UnitSCD40::writeAmbientPressure(const float pressure, const uint32_t duration) +bool UnitSCD40::readAmbientPressure(uint16_t& pressure) { - if (pressure < 0.0f || pressure > 65535.f * 100) { - M5_LIB_LOGE("pressure is not a valid scope %f", pressure); + pressure = 0; +#if 0 + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); return false; } - m5::types::big_uint16_t u16((uint16_t)(pressure / 100)); - m5::utility::CRC8_Checksum crc{}; - std::array buf{u16.u8[0], u16.u8[1], crc.range(u16.data(), u16.size())}; - if (writeRegister(SET_AMBIENT_PRESSURE, buf.data(), buf.size())) { - m5::utility::delay(duration); +#endif + m5::types::big_uint16_t u16{}; + if (read_register(AMBIENT_PRESSURE, u16.data(), u16.size(), GET_AMBIENT_PRESSURE_DURATION)) { + pressure = u16.get(); return true; } return false; @@ -230,12 +271,12 @@ bool UnitSCD40::performForcedRecalibration(const uint16_t concentration, int16_t M5_LIB_LOGD("Periodic measurements are running"); return false; } + m5::types::big_uint16_t u16(concentration); - m5::utility::CRC8_Checksum crc{}; - std::array buf{u16.u8[0], u16.u8[1], crc.range(u16.data(), u16.size())}; - if (!writeRegister(PERFORM_FORCED_CALIBRATION, buf.data(), buf.size())) { + if (!write_register(PERFORM_FORCED_CALIBRATION, u16.data(), u16.size())) { return false; } +#if 0 // 3. Subsequently issue the perform_forced_recalibration command and // optionally read out the FRC correction (i.e. the magnitude of the @@ -252,6 +293,19 @@ bool UnitSCD40::performForcedRecalibration(const uint16_t concentration, int16_t } } return false; +#else + + // 3. Subsequently issue the perform_forced_recalibration command and + // optionally read out the FRC correction (i.e. the magnitude of the + // correction) after waiting for 400 ms for the command to complete. + m5::utility::delay(PERFORM_FORCED_CALIBRATION_DURATION); + + if (read_register(PERFORM_FORCED_CALIBRATION, u16.data(), u16.size()) && u16.get() != 0xFFFF) { + correction = (int16_t)(u16.get() - 0x8000); + return true; + } + return false; +#endif } bool UnitSCD40::writeAutomaticSelfCalibrationEnabled(const bool enabled, const uint32_t duration) @@ -261,13 +315,7 @@ bool UnitSCD40::writeAutomaticSelfCalibrationEnabled(const bool enabled, const u return false; } m5::types::big_uint16_t u16(enabled ? 0x0001 : 0x0000); - m5::utility::CRC8_Checksum crc{}; - std::array buf{u16.u8[0], u16.u8[1], crc.range(u16.data(), u16.size())}; - if (writeRegister(SET_AUTOMATIC_SELF_CALIBRATION_ENABLED, buf.data(), buf.size())) { - m5::utility::delay(duration); - return true; - } - return false; + return write_register(SET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16.data(), u16.size()) && delay_true(duration); } bool UnitSCD40::readAutomaticSelfCalibrationEnabled(bool& enabled) @@ -277,18 +325,39 @@ bool UnitSCD40::readAutomaticSelfCalibrationEnabled(bool& enabled) M5_LIB_LOGD("Periodic measurements are running"); return false; } - uint16_t u16{}; - if (readRegister16(GET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16, 1)) { - enabled = (u16 == 0x0001); + m5::types::big_uint16_t u16{}; + if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16.data(), u16.size(), + GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION)) { + enabled = (u16.get() == 0x0001); return true; } return false; } -bool UnitSCD40::read_data_ready_status() +bool UnitSCD40::writeAutomaticSelfCalibrationTarget(const uint16_t ppm, const uint32_t duration) { - uint16_t res{}; - return readRegister16(GET_DATA_READY_STATUS, res, GET_DATA_READY_STATUS_DURATION) ? (res & 0x07FF) != 0 : false; + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + m5::types::big_uint16_t u16{ppm}; + return write_register(SET_AUTOMATIC_SELF_CALIBRATION_TARGET, u16.data(), u16.size()) && delay_true(duration); +} + +bool UnitSCD40::readAutomaticSelfCalibrationTarget(uint16_t& ppm) +{ + ppm = 0; + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + m5::types::big_uint16_t u16{}; + if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_TARGET, u16.data(), u16.size(), + GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION)) { + ppm = u16.get(); + return true; + } + return false; } bool UnitSCD40::writePersistSettings(const uint32_t duration) @@ -347,14 +416,16 @@ bool UnitSCD40::readSerialNumber(uint64_t& serialNumber) bool UnitSCD40::performSelfTest(bool& malfunction) { + malfunction = true; + if (inPeriodic()) { M5_LIB_LOGD("Periodic measurements are running"); return false; } - uint16_t response{}; - if (readRegister16(PERFORM_SELF_TEST, response, PERFORM_SELF_TEST_DURATION)) { - malfunction = (response != 0); + m5::types::big_uint16_t u16{}; + if (read_register(PERFORM_SELF_TEST, u16.data(), u16.size(), PERFORM_SELF_TEST_DURATION)) { + malfunction = (u16.get() != 0); return true; } return false; @@ -387,7 +458,13 @@ bool UnitSCD40::reInit(const uint32_t duration) return false; } -// TH only if false +bool UnitSCD40::read_data_ready_status() +{ + uint16_t res{}; + return readRegister16BE(GET_DATA_READY_STATUS, res, GET_DATA_READY_STATUS_DURATION) ? (res & 0x07FF) != 0 : false; +} + +// TH only if all is false bool UnitSCD40::read_measurement(Data& d, const bool all) { if (!read_data_ready_status()) { @@ -397,6 +474,13 @@ bool UnitSCD40::read_measurement(Data& d, const bool all) if (!readRegister(READ_MEASUREMENT, d.raw.data(), d.raw.size(), READ_MEASUREMENT_DURATION)) { return false; } + + // For RHT only, previous Co2 data may be obtained and should be dismissed + if (!all) { + d.raw[0] = d.raw[1] = d.raw[2] = 0; + } + + // Check CRC m5::utility::CRC8_Checksum crc{}; for (uint_fast8_t i = all ? 0 : 1; i < 3; ++i) { if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) { @@ -405,5 +489,42 @@ bool UnitSCD40::read_measurement(Data& d, const bool all) } return true; } + +bool UnitSCD40::read_register(const uint16_t reg, uint8_t* rbuf, const uint32_t rlen, const uint32_t duration) +{ + uint8_t tmp[rlen + 1]{}; + if (!rbuf || !rlen || !readRegister(reg, tmp, sizeof(tmp), duration)) { + return false; + } + + m5::utility::CRC8_Checksum crc{}; + auto crc8 = crc.range(tmp, rlen); + if (crc8 != tmp[rlen]) { + M5_LIB_LOGE("CRC8 Error:%02X, %02X", tmp[rlen], crc8); + return false; + } + memcpy(rbuf, tmp, rlen); + return true; +} + +bool UnitSCD40::write_register(const uint16_t reg, uint8_t* wbuf, const uint32_t wlen) +{ + uint8_t buf[wlen + 1]{}; + if (!wbuf || !wlen) { + return false; + } + memcpy(buf, wbuf, wlen); + m5::utility::CRC8_Checksum crc{}; + auto crc8 = crc.range(wbuf, wlen); + buf[wlen] = crc8; + return writeRegister(reg, buf, sizeof(buf)); +} + +bool UnitSCD40::delay_true(const uint32_t duration) +{ + m5::utility::delay(duration); + return true; // Always true +} + } // namespace unit } // namespace m5 diff --git a/src/unit/unit_SCD40.hpp b/src/unit/unit_SCD40.hpp index 5c748f3..59acc4f 100644 --- a/src/unit/unit_SCD40.hpp +++ b/src/unit/unit_SCD40.hpp @@ -58,9 +58,12 @@ constexpr uint16_t GET_TEMPERATURE_OFFSET_DURATION{1}; constexpr uint16_t SET_SENSOR_ALTITUDE_DURATION{1}; constexpr uint16_t GET_SENSOR_ALTITUDE_DURATION{1}; constexpr uint16_t SET_AMBIENT_PRESSURE_DURATION{1}; +constexpr uint16_t GET_AMBIENT_PRESSURE_DURATION{1}; constexpr uint16_t PERFORM_FORCED_CALIBRATION_DURATION{400}; constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1}; constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1}; +constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1}; +constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1}; constexpr uint16_t GET_DATA_READY_STATUS_DURATION{1}; constexpr uint16_t PERSIST_SETTINGS_DURATION{800}; constexpr uint16_t GET_SERIAL_NUMBER_DURATION{1}; @@ -72,7 +75,7 @@ constexpr uint16_t REINIT_DURATION{20}; } // namespace scd4x /*! - @class UnitSCD40 + @class m5::unit::UnitSCD40 @brief SCD40 unit component */ class UnitSCD40 : public Component, public PeriodicMeasurementAdapter { @@ -88,7 +91,7 @@ class UnitSCD40 : public Component, public PeriodicMeasurementAdapter= 0.0f) + @param pressure Ambient pressure [hPa] @param duration Max command duration(ms) @return True if successful + @warning Valid Valid input values are between 700 – 1200 hPa + */ + bool writeAmbientPressure(const uint16_t pressure, const uint32_t duration = scd4x::SET_AMBIENT_PRESSURE_DURATION); + /*! + @brief Read the ambient pressure + @param[out] presure Ambient pressure [hPa] + @return True if successful */ - bool writeAmbientPressure(const float pressure, const uint32_t duration = scd4x::SET_AMBIENT_PRESSURE_DURATION); + bool readAmbientPressure(uint16_t &pressure); ///@} ///@name Field Calibration @@ -242,6 +251,7 @@ class UnitSCD40 : public Component, public PeriodicMeasurementAdapter> 2) << 2; + m5::types::big_uint16_t u16{h}; + return write_register(SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD, u16.data(), u16.size()) && + delay_true(duration); +} + +bool UnitSCD41::readAutomaticSelfCalibrationInitialPeriod(uint16_t& hours) +{ + hours = 0; + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + m5::types::big_uint16_t u16{}; + if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD, u16.data(), u16.size(), + GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION)) { + hours = u16.get(); + return true; + } + return false; +} + +bool UnitSCD41::writeAutomaticSelfCalibrationStandardPeriod(const uint16_t hours, const uint32_t duration) +{ + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + if (hours % 4) { + M5_LIB_LOGW("Arguments are modified to multiples of 4"); + } + uint16_t h = (hours >> 2) << 2; + m5::types::big_uint16_t u16{h}; + return write_register(SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD, u16.data(), u16.size()) && + delay_true(duration); +} + +bool UnitSCD41::readAutomaticSelfCalibrationStandardPeriod(uint16_t& hours) +{ + hours = 0; + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + m5::types::big_uint16_t u16{}; + if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD, u16.data(), u16.size(), + GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION)) { + hours = u16.get(); + return true; + } + return false; } } // namespace unit diff --git a/src/unit/unit_SCD41.hpp b/src/unit/unit_SCD41.hpp index 9bbad30..70b3623 100644 --- a/src/unit/unit_SCD41.hpp +++ b/src/unit/unit_SCD41.hpp @@ -14,9 +14,25 @@ namespace m5 { namespace unit { +/*! + @namespace scd41 + @brief For SCD41 + */ +namespace scd41 { +///@cond +// Max command duration(ms) +// For SCD40/41 +constexpr uint32_t POWER_DOWN_DURATION{1}; +constexpr uint32_t GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION{1}; +constexpr uint32_t SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION{1}; +constexpr uint32_t GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION{1}; +constexpr uint32_t SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION{1}; + +///@endcond +} // namespace scd41 /*! - @class UnitSCD41 + @class m5::unit::UnitSCD41 @brief SCD41 unit component */ class UnitSCD41 : public UnitSCD40 { @@ -29,41 +45,109 @@ class UnitSCD41 : public UnitSCD40 { ccfg.clock = 400 * 1000U; component_config(ccfg); } + virtual ~UnitSCD41() + { + } - ///@name Low power single shot (SCD41) + ///@name Single Shot Measurement Mode ///@{ /*! @brief Request a single measurement + @param[out] d Measurement data @return True if successful - @note Values are updated at 5000 ms interval + @note Blocked until measurement results are acquired (5000 ms) @warning During periodic detection runs, an error is returned */ - bool measureSingleshot(scd4x::Data &d); + bool measureSingleshot(scd4x::Data& d); /*! @brief Request a single measurement temperature and humidity + @param[out] d Measurement data @return True if successful @note Values are updated at 50 ms interval + @note Blocked until measurement results are acquired (50 ms) @warning Information on CO2 is invalid. @warning During periodic detection runs, an error is returned */ - bool measureSingleshotRHT(scd4x::Data &d); + bool measureSingleshotRHT(scd4x::Data& d); + ///@} + + ///@name Power mode + ///@{ + /*! + @brief Power down + @details The sensor into sleep mode + @param duration Max command duration(ms) + @return True if successful + @warning During periodic detection runs, an error is returned + */ + bool powerDown(const uint32_t duration = scd41::POWER_DOWN_DURATION); + /*! + @brief Wake up + @details The sensor from sleep mode into idle mode + @param duration Max command duration(ms) + @return True if successful + @warning During periodic detection runs, an error is returned + */ + bool wakeup(); + ///@} + + ///@name For ASC(Auto Self-Calibration) + ///@{ + /*! + @brief Write the duration of the initial period for ASC correction + @param hours ASC initial period + @param duration Max command duration(ms) + @return True if successful + @warning During periodic detection runs, an error is returned + @warning Allowed values are integer multiples of 4 hours + */ + bool writeAutomaticSelfCalibrationInitialPeriod( + const uint16_t hours, const uint32_t duration = scd41::SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION); + /*! + @brief Read the duration of the initial period for ASC correction + @param[out] hours ASC initial period + @param duration Max command duration(ms) + @return True if successful + @warning During periodic detection runs, an error is returned + */ + bool readAutomaticSelfCalibrationInitialPeriod(uint16_t& hours); + /*! + @brief Write the standard period for ASC correction + @param hours ASC standard period + @param duration Max command duration(ms) + @return True if successful + @warning During periodic detection runs, an error is returned + @warning Allowed values are integer multiples of 4 hours + */ + bool writeAutomaticSelfCalibrationStandardPeriod( + const uint16_t hours, const uint32_t duration = scd41::SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION); + /*! + @brief Red the standard period for ASC correction + @param[iut] hours ASC standard period + @return True if successful + @warning During periodic detection runs, an error is returned + @warning Allowed values are integer multiples of 4 hours + */ + bool readAutomaticSelfCalibrationStandardPeriod(uint16_t& hours); ///@} + +protected: + virtual bool is_valid_chip() override; }; -/*! - @namespace scd41 - @brief For SCD41 - */ namespace scd41 { ///@cond -// Max command duration(ms) -constexpr uint16_t MEASURE_SINGLE_SHOT_DURATION{5000}; -constexpr uint16_t MEASURE_SINGLE_SHOT_RHT_ONLY_DURATION{50}; - namespace command { // Low power single shot - SCD41 only constexpr uint16_t MEASURE_SINGLE_SHOT{0x219d}; constexpr uint16_t MEASURE_SINGLE_SHOT_RHT_ONLY{0x2196}; +constexpr uint16_t POWER_DOWN{0x36e0}; +constexpr uint16_t WAKE_UP{0x36f6}; +constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD{0x2445}; +constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD{0x2340}; +constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD{0x244e}; +constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD{0x234b}; + } // namespace command ///@endcond } // namespace scd41 diff --git a/src/unit/unit_SGP30.cpp b/src/unit/unit_SGP30.cpp index 47c3537..cb3c483 100644 --- a/src/unit/unit_SGP30.cpp +++ b/src/unit/unit_SGP30.cpp @@ -50,7 +50,7 @@ uint16_t Data::tvoc() const // class UnitSGP30 const char UnitSGP30::name[] = "UnitSGP30"; const types::uid_t UnitSGP30::uid{"UnitSGP30"_mmh3}; -const types::uid_t UnitSGP30::attr{0}; +const types::attr_t UnitSGP30::attr{}; bool UnitSGP30::begin() { @@ -81,6 +81,7 @@ bool UnitSGP30::begin() M5_LIB_LOGE("Not enough the product version %x", _version); return false; } + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.baseline_co2eq, _cfg.baseline_tvoc, _cfg.humidity, _cfg.interval) : true; diff --git a/src/unit/unit_SHT30.cpp b/src/unit/unit_SHT30.cpp index 5b16379..67dc85b 100644 --- a/src/unit/unit_SHT30.cpp +++ b/src/unit/unit_SHT30.cpp @@ -87,7 +87,7 @@ float Data::humidity() const const char UnitSHT30::name[] = "UnitSHT30"; const types::uid_t UnitSHT30::uid{"UnitSHT30"_mmh3}; -const types::uid_t UnitSHT30::attr{0}; +const types::attr_t UnitSHT30::attr{}; bool UnitSHT30::begin() { @@ -111,6 +111,12 @@ bool UnitSHT30::begin() return false; } + uint32_t sn{}; + if (!readSerialNumber(sn)) { + M5_LIB_LOGE("Failed to readSerialNumber %x", sn); + return false; + } + auto r = _cfg.start_heater ? startHeater() : stopHeater(); if (!r) { M5_LIB_LOGE("Failed to heater %d", _cfg.start_heater); diff --git a/src/unit/unit_SHT40.cpp b/src/unit/unit_SHT40.cpp index 00d576d..c0f4f95 100644 --- a/src/unit/unit_SHT40.cpp +++ b/src/unit/unit_SHT40.cpp @@ -71,7 +71,7 @@ float Data::humidity() const const char UnitSHT40::name[] = "UnitSHT40"; const types::uid_t UnitSHT40::uid{"UnitSHT40"_mmh3}; -const types::uid_t UnitSHT40::attr{0}; +const types::attr_t UnitSHT40::attr{}; bool UnitSHT40::begin() { @@ -90,6 +90,12 @@ bool UnitSHT40::begin() return false; } + uint32_t sn{}; + if (!readSerialNumber(sn)) { + M5_LIB_LOGE("Failed to readSerialNumber %x", sn); + return false; + } + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.precision, _cfg.heater, _cfg.heater_duty) : true; } diff --git a/test/embedded/scd4x_test.inl b/test/embedded/scd4x_test.inl new file mode 100644 index 0000000..9ce601c --- /dev/null +++ b/test/embedded/scd4x_test.inl @@ -0,0 +1,347 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Common parts of SCD40/41 test + */ + +namespace { +// float t uu int16 (temperature) same as library +constexpr uint16_t float_to_uint16(const float f) +{ + return f * 65536 / 175; +} + +constexpr Mode mode_table[] = {Mode::Normal, Mode::LowPower}; +constexpr uint32_t interval_table[] = { + 5 * 1000, + 30 * 1000, +}; + +template +elapsed_time_t test_periodic(U* unit, const uint32_t times, const uint32_t measure_duration = 0) +{ + auto tm = unit->interval(); + auto timeout_at = m5::utility::millis() + 10 * 1000; + + do { + unit->update(); + if (unit->updated()) { + break; + } + std::this_thread::yield(); + } while (!unit->updated() && m5::utility::millis() <= timeout_at); + // timeout + if (!unit->updated()) { + return 0; + } + + // + uint32_t measured{}; + auto start_at = m5::utility::millis(); + timeout_at = start_at + (times * (tm + measure_duration) * 2); + + do { + unit->update(); + measured += unit->updated() ? 1 : 0; + if (measured >= times) { + break; + } + m5::utility::delay(1); + + } while (measured < times && m5::utility::millis() <= timeout_at); + return (measured == times) ? m5::utility::millis() - start_at : 0; +} +} // namespace + +TEST_P(TestSCD4x, BasicCommand) +{ + SCOPED_TRACE(ustr); + + EXPECT_FALSE(unit->inPeriodic()); + + for (auto&& m : mode_table) { + auto s = m5::utility::formatString("Mode:%u", m); + SCOPED_TRACE(s); + + // Return False if already stopped + EXPECT_FALSE(unit->stopPeriodicMeasurement()); + + EXPECT_TRUE(unit->startPeriodicMeasurement(m)); + // Return False if already started + EXPECT_FALSE(unit->startPeriodicMeasurement(m)); + EXPECT_FALSE(unit->startLowPowerPeriodicMeasurement()); + + EXPECT_TRUE(unit->inPeriodic()); + + // These APIs result in an error during periodic detection + { + EXPECT_FALSE(unit->writeTemperatureOffset(0)); + float offset{}; + EXPECT_FALSE(unit->readTemperatureOffset(offset)); + + EXPECT_FALSE(unit->writeSensorAltitude(0)); + uint16_t altitude{}; + EXPECT_FALSE(unit->readSensorAltitude(altitude)); + + int16_t correction{}; + EXPECT_FALSE(unit->performForcedRecalibration(0, correction)); + + EXPECT_FALSE(unit->writeAutomaticSelfCalibrationEnabled(true)); + bool enabled{}; + EXPECT_FALSE(unit->readAutomaticSelfCalibrationEnabled(enabled)); + + EXPECT_FALSE(unit->writeAutomaticSelfCalibrationTarget(0)); + uint16_t ppm{}; + EXPECT_FALSE(unit->readAutomaticSelfCalibrationTarget(ppm)); + + EXPECT_FALSE(unit->writePersistSettings()); + + uint64_t sno{}; + EXPECT_FALSE(unit->readSerialNumber(sno)); + + bool malfunction{}; + EXPECT_FALSE(unit->performSelfTest(malfunction)); + + EXPECT_FALSE(unit->performFactoryReset()); + + EXPECT_FALSE(unit->reInit()); + } + + EXPECT_TRUE(unit->writeAmbientPressure(1013)); + uint16_t pressure{}; + EXPECT_TRUE(unit->readAmbientPressure(pressure)); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + } +} + +TEST_P(TestSCD4x, OnChipOutputSignalCompensation) +{ + SCOPED_TRACE(ustr); + + { + constexpr float OFFSET{5.4f}; + EXPECT_TRUE(unit->writeTemperatureOffset(OFFSET)); + float offset{}; + EXPECT_TRUE(unit->readTemperatureOffset(offset)); + EXPECT_EQ(float_to_uint16(offset), float_to_uint16(OFFSET)) << "offset:" << offset << " OFFSET:" << OFFSET; + } + + { + constexpr uint16_t ALTITUDE{3776}; + EXPECT_TRUE(unit->writeSensorAltitude(ALTITUDE)); + uint16_t altitude{}; + EXPECT_TRUE(unit->readSensorAltitude(altitude)); + EXPECT_EQ(altitude, ALTITUDE); + } + + { + constexpr uint16_t PRESSURE{1111}; + EXPECT_TRUE(unit->writeAmbientPressure(PRESSURE)); + uint16_t pressure{}; + EXPECT_TRUE(unit->readAmbientPressure(pressure)); + EXPECT_EQ(pressure, PRESSURE); + + EXPECT_TRUE(unit->writeAmbientPressure(700)); + EXPECT_TRUE(unit->readAmbientPressure(pressure)); + EXPECT_EQ(pressure, 700); + + EXPECT_TRUE(unit->writeAmbientPressure(1200)); + EXPECT_TRUE(unit->readAmbientPressure(pressure)); + EXPECT_EQ(pressure, 1200); + + EXPECT_FALSE(unit->writeAmbientPressure(699)); + EXPECT_FALSE(unit->writeAmbientPressure(1201)); + } +} + +TEST_P(TestSCD4x, FieldCalibration) +{ + SCOPED_TRACE(ustr); + + { + int16_t correction{}; + EXPECT_TRUE(unit->performForcedRecalibration(1234, correction)); + } + + { + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(false)); + bool enabled{}; + EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); + EXPECT_FALSE(enabled); + + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(true)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); + EXPECT_TRUE(enabled); + } + + { + constexpr uint16_t PPM{12345}; + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationTarget(PPM)); + uint16_t ppm{}; + EXPECT_TRUE(unit->readAutomaticSelfCalibrationTarget(ppm)); + EXPECT_EQ(ppm, PPM); + } +} + +TEST_P(TestSCD4x, AdvancedFeatures) +{ + SCOPED_TRACE(ustr); + + { + // Read direct [MSB] SNB_3, SNB_2, CRC, SNB_1, SNB_0, CRC [LSB] + std::array rbuf{}; + EXPECT_TRUE(unit->readRegister(m5::unit::scd4x::command::GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), 1)); + + // M5_LOGI("%02x%02x%02x%02x%02x%02x", rbuf[0], rbuf[1], rbuf[3], + // rbuf[4], + // rbuf[6], rbuf[7]); + + m5::types::big_uint16_t w0(rbuf[0], rbuf[1]); + m5::types::big_uint16_t w1(rbuf[3], rbuf[4]); + m5::types::big_uint16_t w2(rbuf[6], rbuf[7]); + uint64_t d_sno = (((uint64_t)w0.get()) << 32) | (((uint64_t)w1.get()) << 16) | ((uint64_t)w2.get()); + + // M5_LOGI("d_sno[%llX]", d_sno); + + // + uint64_t sno{}; + char ssno[13]{}; + EXPECT_TRUE(unit->readSerialNumber(sno)); + EXPECT_TRUE(unit->readSerialNumber(ssno)); + + // M5_LOGI("s:[%s] uint64:[%x]", ssno, sno); + + EXPECT_EQ(sno, d_sno); + + std::stringstream stream; + stream << std::uppercase << std::setw(12) << std::hex << std::setfill('0') << sno; + std::string s(stream.str()); + EXPECT_STREQ(s.c_str(), ssno); + } + + // Set + constexpr float OFFSET{1.234f}; + EXPECT_TRUE(unit->writeTemperatureOffset(OFFSET)); + constexpr uint16_t ALTITUDE{3776}; + EXPECT_TRUE(unit->writeSensorAltitude(ALTITUDE)); + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(false)); + constexpr uint16_t PPM{12345}; + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationTarget(PPM)); + + EXPECT_TRUE(unit->writePersistSettings()); // Save EEPROM + + // Overwrite settings + EXPECT_TRUE(unit->writeTemperatureOffset(OFFSET * 2)); + EXPECT_TRUE(unit->writeSensorAltitude(ALTITUDE * 2)); + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(true)); + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationTarget(PPM * 2)); + + float off{}; + uint16_t alt{}, ppm{}; + bool enabled{}; + + EXPECT_TRUE(unit->readTemperatureOffset(off)); + EXPECT_TRUE(unit->readSensorAltitude(alt)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationTarget(ppm)); + + EXPECT_EQ(float_to_uint16(off), float_to_uint16(OFFSET * 2)); + EXPECT_EQ(alt, ALTITUDE * 2); + EXPECT_EQ(ppm, PPM * 2); + EXPECT_TRUE(enabled); + + EXPECT_TRUE(unit->reInit()); // Load EEPROM + + // Check saved settings + EXPECT_TRUE(unit->readTemperatureOffset(off)); + EXPECT_TRUE(unit->readSensorAltitude(alt)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationTarget(ppm)); + + EXPECT_EQ(float_to_uint16(off), float_to_uint16(OFFSET)); + EXPECT_EQ(alt, ALTITUDE); + EXPECT_EQ(ppm, PPM); + EXPECT_FALSE(enabled); + + bool malfunction{}; + EXPECT_TRUE(unit->performSelfTest(malfunction)); + + EXPECT_TRUE(unit->performFactoryReset()); // Reset EEPROM + + EXPECT_TRUE(unit->readTemperatureOffset(off)); + EXPECT_TRUE(unit->readSensorAltitude(alt)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationTarget(ppm)); + + EXPECT_NE(float_to_uint16(off), float_to_uint16(OFFSET)); + EXPECT_NE(alt, ALTITUDE); + EXPECT_NE(ppm, PPM); + EXPECT_TRUE(enabled); +} + +TEST_P(TestSCD4x, Periodic) +{ + SCOPED_TRACE(ustr); + + EXPECT_TRUE(unit->performFactoryReset()); // Reset EEPROM + + uint32_t idx{}; + for (auto&& m : mode_table) { + auto s = m5::utility::formatString("Mode:%u", m); + SCOPED_TRACE(s); + + EXPECT_FALSE(unit->inPeriodic()); + + EXPECT_TRUE(unit->startPeriodicMeasurement(m)); + EXPECT_TRUE(unit->inPeriodic()); + EXPECT_EQ(unit->updatedMillis(), 0); + + auto it = interval_table[idx]; + auto elapsed = test_periodic(unit.get(), STORED_SIZE, it); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + EXPECT_NE(elapsed, 0); + EXPECT_GE(elapsed, STORED_SIZE * it); + + // + EXPECT_EQ(unit->available(), STORED_SIZE); + EXPECT_FALSE(unit->empty()); + EXPECT_TRUE(unit->full()); + + uint32_t cnt{STORED_SIZE / 2}; + while (cnt-- && unit->available()) { + EXPECT_NE(unit->co2(), 0); + EXPECT_TRUE(std::isfinite(unit->celsius())); + EXPECT_TRUE(std::isfinite(unit->fahrenheit())); + EXPECT_TRUE(std::isfinite(unit->humidity())); + + EXPECT_EQ(unit->co2(), unit->oldest().co2()); + EXPECT_FLOAT_EQ(unit->celsius(), unit->oldest().celsius()); + EXPECT_FLOAT_EQ(unit->fahrenheit(), unit->oldest().fahrenheit()); + EXPECT_FLOAT_EQ(unit->humidity(), unit->oldest().humidity()); + + EXPECT_FALSE(unit->empty()); + unit->discard(); + } + EXPECT_EQ(unit->available(), STORED_SIZE / 2); + EXPECT_FALSE(unit->empty()); + EXPECT_FALSE(unit->full()); + + unit->flush(); + EXPECT_EQ(unit->available(), 0); + EXPECT_TRUE(unit->empty()); + EXPECT_FALSE(unit->full()); + + EXPECT_EQ(unit->co2(), 0); + EXPECT_FALSE(std::isfinite(unit->celsius())); + EXPECT_FALSE(std::isfinite(unit->fahrenheit())); + EXPECT_FALSE(std::isfinite(unit->humidity())); + } +} diff --git a/test/embedded/test_scd40/scd40_test.cpp b/test/embedded/test_scd40/scd40_test.cpp index c59c318..8304d28 100644 --- a/test/embedded/test_scd40/scd40_test.cpp +++ b/test/embedded/test_scd40/scd40_test.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ /* - UnitTest for UnitSCD40/41 + UnitTest for UnitSCD40 */ #include #include @@ -13,49 +13,26 @@ #include #include #include -#include #include #include -// #define UNIT_TEST_SCD41 - using namespace m5::unit::googletest; using namespace m5::unit; using namespace m5::unit::scd4x; +using m5::unit::types::elapsed_time_t; const ::testing::Environment* global_fixture = ::testing::AddGlobalTestEnvironment(new GlobalFixture<400000U>()); -class TestSCD40 : public ComponentTestBase { -protected: - virtual UnitSCD40* get_instance() override - { -#if defined(UNIT_TEST_SCD41) - auto ptr = new m5::unit::UnitSCD41(); -#else - auto ptr = new m5::unit::UnitSCD40(); -#endif - if (ptr) { - auto ccfg = ptr->component_config(); - ccfg.stored_size = 3; - ptr->component_config(ccfg); - - auto cfg = ptr->config(); - cfg.start_periodic = false; - ptr->config(cfg); - } - return ptr; - } - virtual bool is_using_hal() const override - { - return GetParam(); - }; -}; +constexpr uint32_t STORED_SIZE{4}; -class TestSCD41 : public ComponentTestBase { +class TestSCD4x : public ComponentTestBase { protected: - virtual UnitSCD41* get_instance() override + virtual UnitSCD40* get_instance() override { - auto ptr = new m5::unit::UnitSCD41(0x62); + auto ptr = new m5::unit::UnitSCD40(); + auto ccfg = ptr->component_config(); + ccfg.stored_size = STORED_SIZE; + ptr->component_config(ccfg); auto cfg = ptr->config(); cfg.start_periodic = false; ptr->config(cfg); @@ -67,333 +44,11 @@ class TestSCD41 : public ComponentTestBase { }; }; -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD40, -// ::testing::Values(false, true)); -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD40, ::testing::Values(true)); -INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD40, ::testing::Values(false)); - -#if defined(UNIT_TEST_SCD41) -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD41, -// ::testing::Values(false, true)); -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD41, ::testing::Values(true)); -INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD41, ::testing::Values(false)); -#endif +// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD4x, ::testing::Values(false, true)); +// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD4x, ::testing::Values(true)); +INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD4x, ::testing::Values(false)); namespace { -// flot t uu int16 (temperature) -constexpr uint16_t float_to_uint16(const float f) -{ - return f * 65536 / 175; -} - -struct ModeParams { - const char* s; - Mode mode; - uint32_t tolerance; -}; -constexpr ModeParams mode_table[] = { - // {"Normal", Mode::Normal, 1}, - {"LowPower", Mode::LowPower, 500}, -}; - -void check_measurement_values(UnitSCD40* u) -{ - EXPECT_NE(u->co2(), 0); - EXPECT_TRUE(std::isfinite(u->latest().celsius())); - EXPECT_TRUE(std::isfinite(u->latest().fahrenheit())); - EXPECT_TRUE(std::isfinite(u->latest().humidity())); -} } // namespace -TEST_P(TestSCD40, BasicCommand) -{ - SCOPED_TRACE(ustr); - - EXPECT_FALSE(unit->inPeriodic()); - - for (auto&& m : mode_table) { - SCOPED_TRACE(m.s); - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - EXPECT_FALSE(unit->inPeriodic()); - - EXPECT_TRUE(unit->startPeriodicMeasurement(m.mode)); - // Return False if already started - EXPECT_FALSE(unit->startPeriodicMeasurement(m.mode)); - - EXPECT_TRUE(unit->inPeriodic()); - - // These APIs result in an error during periodic detection - { - EXPECT_FALSE(unit->writeTemperatureOffset(0)); - float offset{}; - EXPECT_FALSE(unit->readTemperatureOffset(offset)); - - EXPECT_FALSE(unit->writeSensorAltitude(0)); - uint16_t altitude{}; - EXPECT_FALSE(unit->readSensorAltitude(altitude)); - - int16_t correction{}; - EXPECT_FALSE(unit->performForcedRecalibration(0, correction)); - - EXPECT_FALSE(unit->writeAutomaticSelfCalibrationEnabled(true)); - bool enabled{}; - EXPECT_FALSE(unit->readAutomaticSelfCalibrationEnabled(enabled)); - - EXPECT_FALSE(unit->startLowPowerPeriodicMeasurement()); - - EXPECT_FALSE(unit->writePersistSettings()); - - uint64_t sno{}; - EXPECT_FALSE(unit->readSerialNumber(sno)); - - bool malfunction{}; - EXPECT_FALSE(unit->performSelfTest(malfunction)); - - EXPECT_FALSE(unit->performFactoryReset()); - - EXPECT_FALSE(unit->reInit()); - } - // These APIs can be used during periodic detection - EXPECT_TRUE(unit->writeAmbientPressure(0.0f)); - } -} - -TEST_P(TestSCD40, Periodic) -{ - SCOPED_TRACE(ustr); - - // Empty - EXPECT_EQ(unit->co2(), 0U); - EXPECT_FALSE(std::isfinite(unit->temperature())); - EXPECT_FALSE(std::isfinite(unit->celsius())); - EXPECT_FALSE(std::isfinite(unit->fahrenheit())); - EXPECT_FALSE(std::isfinite(unit->humidity())); - - // - uint32_t idx{}; - for (auto&& m : mode_table) { - auto s = m5::utility::formatString("Mode:%s", m.s); - SCOPED_TRACE(s.c_str()); - - EXPECT_TRUE(unit->startPeriodicMeasurement(m.mode)); - EXPECT_TRUE(unit->inPeriodic()); - EXPECT_EQ(unit->updatedMillis(), 0); - test_periodic_measurement(unit.get(), 3, m.tolerance, check_measurement_values); - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - - EXPECT_EQ(unit->available(), 3); - EXPECT_FALSE(unit->empty()); - EXPECT_TRUE(unit->full()); - - if (idx & 1) { - uint32_t cnt{}; - while (unit->available()) { - ++cnt; - - EXPECT_NE(unit->co2(), 0U); - EXPECT_TRUE(std::isfinite(unit->celsius())); - EXPECT_TRUE(std::isfinite(unit->fahrenheit())); - EXPECT_TRUE(std::isfinite(unit->humidity())); - EXPECT_EQ(unit->co2(), unit->oldest().co2()); - EXPECT_FLOAT_EQ(unit->celsius(), unit->oldest().celsius()); - EXPECT_FLOAT_EQ(unit->fahrenheit(), unit->oldest().fahrenheit()); - EXPECT_FLOAT_EQ(unit->humidity(), unit->oldest().humidity()); - - unit->discard(); - } - EXPECT_EQ(cnt, 2); - - EXPECT_EQ(unit->co2(), 0U); - EXPECT_TRUE(std::isnan(unit->celsius())); - EXPECT_TRUE(std::isnan(unit->fahrenheit())); - EXPECT_TRUE(std::isnan(unit->humidity())); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - } else { - EXPECT_NE(unit->co2(), 0U); - EXPECT_TRUE(std::isfinite(unit->celsius())); - EXPECT_TRUE(std::isfinite(unit->fahrenheit())); - EXPECT_TRUE(std::isfinite(unit->humidity())); - EXPECT_EQ(unit->co2(), unit->oldest().co2()); - EXPECT_FLOAT_EQ(unit->celsius(), unit->oldest().celsius()); - EXPECT_FLOAT_EQ(unit->fahrenheit(), unit->oldest().fahrenheit()); - EXPECT_FLOAT_EQ(unit->humidity(), unit->oldest().humidity()); - - unit->flush(); - EXPECT_EQ(unit->co2(), 0U); - EXPECT_TRUE(std::isnan(unit->celsius())); - EXPECT_TRUE(std::isnan(unit->fahrenheit())); - EXPECT_TRUE(std::isnan(unit->humidity())); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - } - ++idx; - } -} - -TEST_P(TestSCD40, OnChipOutputSignalCompensation) -{ - SCOPED_TRACE(ustr); - - { - constexpr float OFFSET{1.234f}; - EXPECT_TRUE(unit->writeTemperatureOffset(OFFSET)); - float offset{}; - EXPECT_TRUE(unit->readTemperatureOffset(offset)); - EXPECT_EQ(float_to_uint16(offset), float_to_uint16(OFFSET)) << "offset:" << offset << " OFFSET:" << OFFSET; - } - - { - constexpr uint16_t ALTITUDE{3776}; - EXPECT_TRUE(unit->writeSensorAltitude(ALTITUDE)); - uint16_t altitude{}; - EXPECT_TRUE(unit->readSensorAltitude(altitude)); - EXPECT_EQ(altitude, ALTITUDE); - } -} - -TEST_P(TestSCD40, FieldCalibration) -{ - SCOPED_TRACE(ustr); - - { - int16_t correction{}; - EXPECT_TRUE(unit->performForcedRecalibration(1234, correction)); - } - - { - EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(false)); - bool enabled{}; - EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); - EXPECT_FALSE(enabled); - - EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(true)); - EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); - EXPECT_TRUE(enabled); - } -} - -TEST_P(TestSCD40, AdvancedFeatures) -{ - SCOPED_TRACE(ustr); - - { - // Read direct [MSB] SNB_3, SNB_2, CRC, SNB_1, SNB_0, CRC [LSB] - std::array rbuf{}; - EXPECT_TRUE(unit->readRegister(m5::unit::scd4x::command::GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), 1)); - - // M5_LOGI("%02x%02x%02x%02x%02x%02x", rbuf[0], rbuf[1], rbuf[3], - // rbuf[4], - // rbuf[6], rbuf[7]); - - m5::types::big_uint16_t w0(rbuf[0], rbuf[1]); - m5::types::big_uint16_t w1(rbuf[3], rbuf[4]); - m5::types::big_uint16_t w2(rbuf[6], rbuf[7]); - uint64_t d_sno = (((uint64_t)w0.get()) << 32) | (((uint64_t)w1.get()) << 16) | ((uint64_t)w2.get()); - - // M5_LOGI("d_sno[%llX]", d_sno); - - // - uint64_t sno{}; - char ssno[13]{}; - EXPECT_TRUE(unit->readSerialNumber(sno)); - EXPECT_TRUE(unit->readSerialNumber(ssno)); - - // M5_LOGI("s:[%s] uint64:[%x]", ssno, sno); - - EXPECT_EQ(sno, d_sno); - - std::stringstream stream; - stream << std::uppercase << std::setw(12) << std::hex << std::setfill('0') << sno; - std::string s(stream.str()); - EXPECT_STREQ(s.c_str(), ssno); - } - - // Set - constexpr float OFFSET{1.234f}; - EXPECT_TRUE(unit->writeTemperatureOffset(OFFSET)); - constexpr uint16_t ALTITUDE{3776}; - EXPECT_TRUE(unit->writeSensorAltitude(ALTITUDE)); - EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(false)); - - EXPECT_TRUE(unit->writePersistSettings()); // Save EEPROM - - // Overwrite settings - EXPECT_TRUE(unit->writeTemperatureOffset(OFFSET * 2)); - EXPECT_TRUE(unit->writeSensorAltitude(ALTITUDE * 2)); - EXPECT_TRUE(unit->writeAutomaticSelfCalibrationEnabled(true)); - - float off{}; - uint16_t alt{}; - bool enabled{}; - - EXPECT_TRUE(unit->readTemperatureOffset(off)); - EXPECT_TRUE(unit->readSensorAltitude(alt)); - EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); - EXPECT_EQ(float_to_uint16(off), float_to_uint16(OFFSET * 2)); - EXPECT_EQ(alt, ALTITUDE * 2); - EXPECT_TRUE(enabled); - - EXPECT_TRUE(unit->reInit()); // Load EEPROM - - // Check saved settings - EXPECT_TRUE(unit->readTemperatureOffset(off)); - EXPECT_TRUE(unit->readSensorAltitude(alt)); - EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); - EXPECT_EQ(float_to_uint16(off), float_to_uint16(OFFSET)); - EXPECT_EQ(alt, ALTITUDE); - EXPECT_FALSE(enabled); - - bool malfunction{}; - EXPECT_TRUE(unit->performSelfTest(malfunction)); - - EXPECT_TRUE(unit->performFactoryReset()); // Reset EEPROM - - EXPECT_TRUE(unit->readTemperatureOffset(off)); - EXPECT_TRUE(unit->readSensorAltitude(alt)); - EXPECT_TRUE(unit->readAutomaticSelfCalibrationEnabled(enabled)); - EXPECT_NE(float_to_uint16(off), float_to_uint16(OFFSET)); - EXPECT_NE(alt, ALTITUDE); - EXPECT_TRUE(enabled); -} - -#if defined(UNIT_TEST_SCD41) -TEST_P(TestSCD41, LowPowerSingleshot) -{ - SCOPED_TRACE(ustr); - - { - UnitSCD40::Data d{}; - EXPECT_TRUE(unit->measureSingleshot(d)); - EXPECT_NE(d.co2(), 0); - EXPECT_TRUE(std::isfinite(d.temperature())); - EXPECT_TRUE(std::isfinite(d.humidity())); - - EXPECT_FALSE(unit->measureSingleshot(d)); - m5::utility::delay(5000); - - EXPECT_TRUE(unit->measureSingleshot(d)); - EXPECT_NE(d.co2(), 0); - EXPECT_TRUE(std::isfinite(d.temperature())); - EXPECT_TRUE(std::isfinite(d.humidity())); - } - - { - UnitSCD40::Data d{}; - EXPECT_TRUE(unit->measureSingleshotRHT(d)); - EXPECT_EQ(d.co2(), 0); - EXPECT_TRUE(std::isfinite(d.temperature())); - EXPECT_TRUE(std::isfinite(d.humidity())); - - EXPECT_FALSE(unit->measureSingleshotRHT(d)); - m5::utility::delay(50); - - EXPECT_TRUE(unit->measureSingleshotRHT(d)); - EXPECT_EQ(d.co2(), 0); - EXPECT_TRUE(std::isfinite(d.temperature())); - EXPECT_TRUE(std::isfinite(d.humidity())); - } -} -#endif +#include "../scd4x_test.inl" diff --git a/test/embedded/test_scd41/scd41_test.cpp b/test/embedded/test_scd41/scd41_test.cpp new file mode 100644 index 0000000..741c7b0 --- /dev/null +++ b/test/embedded/test_scd41/scd41_test.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + UnitTest for UnitSCD41 +*/ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace m5::unit::googletest; +using namespace m5::unit; +using namespace m5::unit::scd4x; +using m5::unit::types::elapsed_time_t; + +const ::testing::Environment* global_fixture = ::testing::AddGlobalTestEnvironment(new GlobalFixture<400000U>()); + +constexpr uint32_t STORED_SIZE{4}; + +class TestSCD4x : public ComponentTestBase { +protected: + virtual UnitSCD41* get_instance() override + { + auto ptr = new m5::unit::UnitSCD41(); + auto ccfg = ptr->component_config(); + ccfg.stored_size = STORED_SIZE; + ptr->component_config(ccfg); + auto cfg = ptr->config(); + cfg.start_periodic = false; + ptr->config(cfg); + return ptr; + } + virtual bool is_using_hal() const override + { + return GetParam(); + }; +}; + +// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD4x, ::testing::Values(false, true)); +// INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD4x, ::testing::Values(true)); +INSTANTIATE_TEST_SUITE_P(ParamValues, TestSCD4x, ::testing::Values(false)); + +namespace { +} // namespace + +#include "../scd4x_test.inl" + +TEST_P(TestSCD4x, Singleshot) +{ + SCOPED_TRACE(ustr); + { + Data d{}; + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_TRUE(unit->measureSingleshot(d)); + EXPECT_NE(d.co2(), 0); + EXPECT_TRUE(std::isfinite(d.temperature())); + EXPECT_TRUE(std::isfinite(d.humidity())); + + EXPECT_TRUE(unit->startPeriodicMeasurement()); + + EXPECT_TRUE(unit->inPeriodic()); + EXPECT_FALSE(unit->measureSingleshot(d)); + EXPECT_EQ(d.co2(), 0); + EXPECT_FLOAT_EQ(d.temperature(), -45.f); + EXPECT_FLOAT_EQ(d.humidity(), 0.0f); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + } + { + Data d{}; + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_TRUE(unit->measureSingleshotRHT(d)); + EXPECT_EQ(d.co2(), 0); + EXPECT_TRUE(std::isfinite(d.temperature())); + EXPECT_TRUE(std::isfinite(d.humidity())); + + EXPECT_TRUE(unit->startPeriodicMeasurement()); + + EXPECT_TRUE(unit->inPeriodic()); + EXPECT_FALSE(unit->measureSingleshotRHT(d)); + EXPECT_EQ(d.co2(), 0); + EXPECT_FLOAT_EQ(d.temperature(), -45.f); + EXPECT_FLOAT_EQ(d.humidity(), 0.0f); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + } +} + +TEST_P(TestSCD4x, PowerMode) +{ + SCOPED_TRACE(ustr); + + EXPECT_FALSE(unit->inPeriodic()); + + uint32_t count{8}; + while (count--) { + EXPECT_TRUE(unit->powerDown()) << count; + EXPECT_TRUE(unit->wakeup()) << count; + } + + EXPECT_TRUE(unit->startPeriodicMeasurement()); + EXPECT_TRUE(unit->inPeriodic()); + + EXPECT_FALSE(unit->powerDown()); + EXPECT_FALSE(unit->wakeup()); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_TRUE(unit->reInit()); +} + +TEST_P(TestSCD4x, ASC) +{ + SCOPED_TRACE(ustr); + + constexpr uint16_t hours_table[] = {0, 32768, 65535}; + for (auto&& h : hours_table) { + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationInitialPeriod(h)); + EXPECT_TRUE(unit->writeAutomaticSelfCalibrationStandardPeriod(h)); + + uint16_t ih{}, sh{}; + + EXPECT_TRUE(unit->readAutomaticSelfCalibrationInitialPeriod(ih)); + EXPECT_TRUE(unit->readAutomaticSelfCalibrationStandardPeriod(sh)); + + EXPECT_EQ(ih, (h >> 2) << 2); + EXPECT_EQ(sh, (h >> 2) << 2); + } + + EXPECT_TRUE(unit->startPeriodicMeasurement()); + EXPECT_TRUE(unit->inPeriodic()); + for (auto&& h : hours_table) { + EXPECT_FALSE(unit->writeAutomaticSelfCalibrationInitialPeriod(h)); + EXPECT_FALSE(unit->writeAutomaticSelfCalibrationStandardPeriod(h)); + } +} diff --git a/unit_co2_env.ini b/unit_co2_env.ini index 9e88391..429e069 100644 --- a/unit_co2_env.ini +++ b/unit_co2_env.ini @@ -87,7 +87,94 @@ lib_deps = ${CoreInk.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_scd40 +; SCD41 +[env:test_SCD41_Core] +extends=Core, option_release, arduino_latest +lib_deps = ${Core.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_Core2] +extends=Core2, option_release, arduino_latest +lib_deps = ${Core2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_CoreS3] +extends=CoreS3, option_release, arduino_latest +lib_deps = ${CoreS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_Fire] +extends=Fire, option_release, arduino_latest +lib_deps = ${Fire.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_StampS3] +extends=StampS3, option_release, arduino_latest +lib_deps = ${StampS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_Dial] +extends=StampS3, option_release, arduino_latest +lib_deps = ${StampS3.lib_deps} + ${test_fw.lib_deps} + m5stack/M5Dial +test_filter= embedded/test_scd41 + +[env:test_SCD41_AtomMatrix] +extends=AtomMatrix, option_release, arduino_latest +lib_deps = ${AtomMatrix.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_AtomS3] +extends=AtomS3, option_release, arduino_latest +lib_deps = ${AtomS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_AtomS3R] +extends=AtomS3R, option_release, arduino_latest +lib_deps = ${AtomS3R.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_NanoC6] +extends=NanoC6, option_release, arduino_latest +lib_deps = ${NanoC6.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_Paper] +extends=Paper, option_release, arduino_latest +lib_deps = ${Paper.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + +[env:test_SCD41_CoreInk] +extends=CoreInk, option_release, arduino_latest +lib_deps = ${CoreInk.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_scd41 + ;Examples +; UnitCO2 [env:UnitCO2_PlotToSerial_Core_Arduino_latest] extends=Core, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2/PlotToSerial> @@ -167,3 +254,84 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2/Plot [env:UnitCO2_PlotToSerial_Fire_Arduino_4_4_0] extends=Fire, option_release, arduino_4_4_0 build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2/PlotToSerial> + +; UnitCO2L +[env:UnitCO2L_PlotToSerial_Core_Arduino_latest] +extends=Core, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Core_Arduino_5_4_0] +extends=Core, option_release, arduino_5_4_0 +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Core_Arduino_4_4_0] +extends=Core, option_release, arduino_4_4_0 +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Core2_Arduino_latest] +extends=Core2, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Core2_Arduino_5_4_0] +extends=Core2, option_release, arduino_5_4_0 +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Core2_Arduino_4_4_0] +extends=Core2, option_release, arduino_4_4_0 +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_CoreS3_Arduino_latest] +extends=CoreS3, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_StampS3_Arduino_latest] +extends=StampS3, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_AtomMatrix_Arduino_latest] +extends=AtomMatrix, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_AtomS3_Arduino_latest] +extends=AtomS3, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_AtomS3R_Arduino_latest] +extends=AtomS3R, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Dial_Arduino_latest] +extends=Dial, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_NanoC6_Arduino_latest] +extends=NanoC6, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_StickCPlus_Arduino_latest] +extends=StickCPlus, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_StickCPlus2_Arduino_latest] +extends=StickCPlus2, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Paper_Arduino_latest] +extends=Paper, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_CoreInk_Arduino_latest] +extends=CoreInk, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Fire_Arduino_latest] +extends=Fire, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Fire_Arduino_5_4_0] +extends=Fire, option_release, arduino_5_4_0 +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial> + +[env:UnitCO2L_PlotToSerial_Fire_Arduino_4_4_0] +extends=Fire, option_release, arduino_4_4_0 +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitCO2L/PlotToSerial>