From 5d9293a24ab967b4c1712655097787233cb8a5a2 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 20:28:15 +0900 Subject: [PATCH 01/22] Fixes SGP30 readRaw CRC bug, SHT30 humidity formula, Doxygen comments and typos --- src/M5UnitUnifiedENV.hpp | 8 ++--- src/idf_component.yml | 2 -- src/idf_component.yml.orig | 2 -- src/unit/unit_BME688.hpp | 70 +++++++++++++++++++++++++++++--------- src/unit/unit_BMP280.hpp | 16 ++++----- src/unit/unit_QMP6988.cpp | 2 +- src/unit/unit_QMP6988.hpp | 10 ++++-- src/unit/unit_SCD40.hpp | 2 +- src/unit/unit_SGP30.cpp | 2 +- src/unit/unit_SHT30.cpp | 2 +- src/unit/unit_SHT30.hpp | 6 +++- src/unit/unit_SHT40.hpp | 2 +- 12 files changed, 82 insertions(+), 42 deletions(-) delete mode 100644 src/idf_component.yml delete mode 100644 src/idf_component.yml.orig diff --git a/src/M5UnitUnifiedENV.hpp b/src/M5UnitUnifiedENV.hpp index 2b2e14f..ee12b2c 100644 --- a/src/M5UnitUnifiedENV.hpp +++ b/src/M5UnitUnifiedENV.hpp @@ -45,10 +45,10 @@ namespace m5 { */ namespace unit { -using UnitCO2 = m5::unit::UnitSCD40; -using UnitCO2L = m5::unit::UnitSCD41; -using UnitENVPro = m5::unit::UnitBME688; -using UnitTVOC = m5::unit::UnitSGP30; +using UnitCO2 = m5::unit::UnitSCD40; //!< Product name alias for UnitSCD40 +using UnitCO2L = m5::unit::UnitSCD41; //!< Product name alias for UnitSCD41 +using UnitENVPro = m5::unit::UnitBME688; //!< Product name alias for UnitBME688 +using UnitTVOC = m5::unit::UnitSGP30; //!< Product name alias for UnitSGP30 } // namespace unit } // namespace m5 diff --git a/src/idf_component.yml b/src/idf_component.yml deleted file mode 100644 index d752765..0000000 --- a/src/idf_component.yml +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - idf: '>=5.1' diff --git a/src/idf_component.yml.orig b/src/idf_component.yml.orig deleted file mode 100644 index d752765..0000000 --- a/src/idf_component.yml.orig +++ /dev/null @@ -1,2 +0,0 @@ -dependencies: - idf: '>=5.1' diff --git a/src/unit/unit_BME688.hpp b/src/unit/unit_BME688.hpp index df81c2e..71e2e3c 100644 --- a/src/unit/unit_BME688.hpp +++ b/src/unit/unit_BME688.hpp @@ -37,6 +37,10 @@ namespace m5 { namespace unit { +/*! + @namespace bme688 + @brief For BME688 + */ namespace bme688 { /*! @@ -107,14 +111,14 @@ enum class Oversampling : uint8_t { @brief IIR Filter setting */ enum class Filter : uint8_t { - None, - Coeff_1, - Coeff_3, - Coeff_7, - Coeff_15, - Coeff_31, - Coeff_63, - Coeff_127, + None, //!< No filter + Coeff_1, //!< co-efficient 1 + Coeff_3, //!< co-efficient 3 + Coeff_7, //!< co-efficient 7 + Coeff_15, //!< co-efficient 15 + Coeff_31, //!< co-efficient 31 + Coeff_63, //!< co-efficient 63 + Coeff_127, //!< co-efficient 127 }; /*! @@ -122,9 +126,9 @@ enum class Filter : uint8_t { @brief bme68xConf::odr settings (standbytime Unit:ms) */ enum class ODR : uint8_t { - MS_0_59, //< 0.59 ms - MS_62_5, //< 62.5 ms - MS_125, //< 125 ms + MS_0_59, //!< 0.59 ms + MS_62_5, //!< 62.5 ms + MS_125, //!< 125 ms MS_250, //!< 250 ms MS_500, //!< 500 ms MS_1000, //!< 1000 ms @@ -149,10 +153,12 @@ struct GasWait { enum class Factor { x1, x4, x16, x64 }; ///@name Getter ///@{ + //! @brief Gets the step value (0-63) inline uint8_t step() const { return value & 0x3F; } + //! @brief Gets the multiplication factor inline Factor factor() const { return static_cast((value >> 6) & 0x03); @@ -161,10 +167,12 @@ struct GasWait { ///@name Setter ///@{ + //! @brief Sets the step value (0-63) inline void step(const uint8_t s) { value = (value & ~0x3F) | (s & 0x3F); } + //! @brief Sets the multiplication factor inline void factor(const Factor f) { value = (value & ~(0x03 << 6)) | (m5::stl::to_underlying(f) << 6); @@ -258,108 +266,135 @@ struct Data { #if defined(UNIT_BME688_USING_BSEC2) bsecOutputs raw_outputs{}; + //! @brief Gets the value of the specified virtual sensor output float get(const bsec_virtual_sensor_t vs) const; + //! @brief Gets the IAQ (Indoor Air Quality) value inline float iaq() const { return get(BSEC_OUTPUT_IAQ); } + //! @brief Gets the static IAQ value inline float static_iaq() const { return get(BSEC_OUTPUT_STATIC_IAQ); } + //! @brief Gets the CO2 equivalent value inline float co2() const { return get(BSEC_OUTPUT_CO2_EQUIVALENT); } + //! @brief Gets the breath VOC equivalent value inline float voc() const { return get(BSEC_OUTPUT_BREATH_VOC_EQUIVALENT); } + //! @brief Gets the raw temperature value inline float temperature() const { return get(BSEC_OUTPUT_RAW_TEMPERATURE); } + //! @brief Gets the raw pressure value inline float pressure() const { return get(BSEC_OUTPUT_RAW_PRESSURE); } + //! @brief Gets the raw humidity value inline float humidity() const { return get(BSEC_OUTPUT_RAW_HUMIDITY); } + //! @brief Gets the raw gas resistance value inline float gas() const { return get(BSEC_OUTPUT_RAW_GAS); } + //! @brief Gets the gas sensor stabilization status inline bool gas_stabilization() const { return get(BSEC_OUTPUT_STABILIZATION_STATUS) == 1.0f; } + //! @brief Gets the gas sensor run-in status inline bool gas_run_in_status() const { return get(BSEC_OUTPUT_RUN_IN_STATUS) == 1.0f; } + //! @brief Gets the heat compensated temperature value inline float heat_compensated_temperature() const { return get(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE); } + //! @brief Gets the heat compensated humidity value inline float heat_compensated_humidity() const { return get(BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY); } + //! @brief Gets the gas percentage value inline float gas_percentage() const { return get(BSEC_OUTPUT_GAS_PERCENTAGE); } + //! @brief Gets the gas estimate 1 value inline float gas_estimate_1() const { return get(BSEC_OUTPUT_GAS_ESTIMATE_1); } + //! @brief Gets the gas estimate 2 value inline float gas_estimate_2() const { return get(BSEC_OUTPUT_GAS_ESTIMATE_2); } + //! @brief Gets the gas estimate 3 value inline float gas_estimate_3() const { return get(BSEC_OUTPUT_GAS_ESTIMATE_3); } + //! @brief Gets the gas estimate 4 value inline float gas_estimate_4() const { return get(BSEC_OUTPUT_GAS_ESTIMATE_4); } + //! @brief Gets the raw gas index value inline uint32_t gas_index() const { return get(BSEC_OUTPUT_RAW_GAS_INDEX); } + //! @brief Gets the regression estimate 1 value inline float regression_estimate_1() const { return get(BSEC_OUTPUT_REGRESSION_ESTIMATE_1); } + //! @brief Gets the regression estimate 2 value inline float regression_estimate_2() const { return get(BSEC_OUTPUT_REGRESSION_ESTIMATE_2); } + //! @brief Gets the regression estimate 3 value inline float regression_estimate_3() const { return get(BSEC_OUTPUT_REGRESSION_ESTIMATE_3); } + //! @brief Gets the regression estimate 4 value inline float regression_estimate_4() const { return get(BSEC_OUTPUT_REGRESSION_ESTIMATE_4); } #endif + //! @brief Gets the raw temperature from bme68xData inline float raw_temperature() const { return raw.temperature; } + //! @brief Gets the raw pressure from bme68xData inline float raw_pressure() const { return raw.pressure; } + //! @brief Gets the raw humidity from bme68xData inline float raw_humidity() const { return raw.humidity; } + //! @brief Gets the raw gas resistance from bme68xData inline float raw_gas() const { return raw.gas_resistance; @@ -403,7 +438,7 @@ class UnitBME688 : public Component, public PeriodicMeasurementAdapter Date: Mon, 23 Mar 2026 20:28:20 +0900 Subject: [PATCH 02/22] Fixes test comments, typos and removes unused includes --- test/embedded/scd4x_test.inl | 2 +- test/embedded/test_bme688/bme688_test.cpp | 20 +++++++++++--------- test/embedded/test_bmp280/bmp280_test.cpp | 1 - test/embedded/test_qmp6988/qmp6988_test.cpp | 1 - test/embedded/test_sht30/sht30_test.cpp | 6 +++--- test/embedded/test_sht40/sht40_test.cpp | 5 ++--- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/test/embedded/scd4x_test.inl b/test/embedded/scd4x_test.inl index 92758af..f544a92 100644 --- a/test/embedded/scd4x_test.inl +++ b/test/embedded/scd4x_test.inl @@ -8,7 +8,7 @@ */ namespace { -// float t uu int16 (temperature) same as library +// float to uint16 (temperature) same as library constexpr uint16_t float_to_uint16(const float f) { return f * 65536 / 175; diff --git a/test/embedded/test_bme688/bme688_test.cpp b/test/embedded/test_bme688/bme688_test.cpp index cab7d21..7f9df1b 100644 --- a/test/embedded/test_bme688/bme688_test.cpp +++ b/test/embedded/test_bme688/bme688_test.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include using namespace m5::unit::googletest; @@ -77,7 +77,6 @@ constexpr uint8_t bsec_config[] = { }; #endif -auto rng = std::default_random_engine{}; void check_measurement_values(UnitBME688* u) { @@ -160,13 +159,16 @@ TEST_F(TestBME688, Settings) uint32_t cnt{10}; while (cnt--) { bme68xConf tph = unit->tphSetting(); - tph.os_temp = rng() % 0x06; - tph.os_pres = rng() % 0x06; - tph.os_hum = rng() % 0x06; - tph.filter = rng() % 0x07; - - // M5_LOGW("%u/%u/%u/%u", tph.os_temp, tph.os_pres, tph.os_hum, - // tph.filter); + tph.os_temp = esp_random() % 0x06; + tph.os_pres = esp_random() % 0x06; + tph.os_hum = esp_random() % 0x06; + tph.filter = esp_random() % 0x07; + + { + auto s = m5::utility::formatString("Random TPH: %u/%u/%u/%u", tph.os_temp, tph.os_pres, tph.os_hum, + tph.filter); + SCOPED_TRACE(s); + } EXPECT_TRUE(unit->writeTPHSetting(tph)); EXPECT_EQ(unit->tphSetting().os_temp, tph.os_temp); diff --git a/test/embedded/test_bmp280/bmp280_test.cpp b/test/embedded/test_bmp280/bmp280_test.cpp index 43fc4da..f1c0b60 100644 --- a/test/embedded/test_bmp280/bmp280_test.cpp +++ b/test/embedded/test_bmp280/bmp280_test.cpp @@ -16,7 +16,6 @@ #include #include #include -#include using namespace m5::unit::googletest; using namespace m5::unit; diff --git a/test/embedded/test_qmp6988/qmp6988_test.cpp b/test/embedded/test_qmp6988/qmp6988_test.cpp index 33a6a7d..0544ee0 100644 --- a/test/embedded/test_qmp6988/qmp6988_test.cpp +++ b/test/embedded/test_qmp6988/qmp6988_test.cpp @@ -16,7 +16,6 @@ #include #include #include -#include using namespace m5::unit::googletest; using namespace m5::unit; diff --git a/test/embedded/test_sht30/sht30_test.cpp b/test/embedded/test_sht30/sht30_test.cpp index 79c9ca9..92df44a 100644 --- a/test/embedded/test_sht30/sht30_test.cpp +++ b/test/embedded/test_sht30/sht30_test.cpp @@ -39,7 +39,7 @@ class TestSHT30 : public I2CComponentTestBase { }; namespace { -// flot t uu int16 (temperature) +// float to uint16 (temperature) constexpr uint16_t float_to_uint16(const float f) { return f * 65536 / 175; @@ -115,11 +115,11 @@ TEST_F(TestSHT30, Periodic) {"2Medium", MPS::Two, Repeatability::Medium}, {"2Low", MPS::Two, Repeatability::Low}, // - {"4fHigh", MPS::Four, Repeatability::High}, + {"4High", MPS::Four, Repeatability::High}, {"4Medium", MPS::Four, Repeatability::Medium}, {"4Low", MPS::Four, Repeatability::Low}, // - {"10fHigh", MPS::Ten, Repeatability::High}, + {"10High", MPS::Ten, Repeatability::High}, {"10Medium", MPS::Ten, Repeatability::Medium}, {"10Low", MPS::Ten, Repeatability::Low}, }; diff --git a/test/embedded/test_sht40/sht40_test.cpp b/test/embedded/test_sht40/sht40_test.cpp index d836122..f7957cf 100644 --- a/test/embedded/test_sht40/sht40_test.cpp +++ b/test/embedded/test_sht40/sht40_test.cpp @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ /* - UnitTest for UnitSHT30 + UnitTest for UnitSHT40 */ #include #include @@ -16,7 +16,6 @@ #include #include #include -#include using namespace m5::unit::googletest; using namespace m5::unit; @@ -47,7 +46,7 @@ std::tuple sm_table[] = { {"HighNone", Precision::High, Heater::None, 9}, // {"MediumLong", Precision::Medium, Heater::Long, 5}, - {"MediumSHort", Precision::Medium, Heater::Short, 5}, + {"MediumShort", Precision::Medium, Heater::Short, 5}, {"MediumNone", Precision::Medium, Heater::None, 5}, // {"LowLong", Precision::Low, Heater::Long, 2}, From 1bcdc3f1531e8aa187298b165ecdacfda7571df6 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 20:28:26 +0900 Subject: [PATCH 03/22] Fixes workflow paths and adds intelhex installation --- .github/workflows/arduino-esp-v2-build-check.yml | 4 ++-- .github/workflows/arduino-esp-v3-build-check.yml | 4 ++-- .github/workflows/arduino-m5-build-check.yml | 4 ++-- .github/workflows/clang-format-check.yml | 4 ++-- .github/workflows/platformio-build-check.yml | 16 ++++++++++++---- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index 4ce8b52..dfabe01 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -21,7 +21,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**arduino-esp-v2-build-check.yml' + - '.github/workflows/arduino-esp-v2-build-check.yml' pull_request: paths: - 'src/unit/**.cpp' @@ -33,7 +33,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**arduino-esp-v2-build-check.yml' + - '.github/workflows/arduino-esp-v2-build-check.yml' workflow_dispatch: defaults: diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index bfbad5b..7f295f4 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -21,7 +21,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**arduino-esp-v3-build-check.yml' + - '.github/workflows/arduino-esp-v3-build-check.yml' pull_request: paths: - 'src/unit/**.cpp' @@ -33,7 +33,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**arduino-esp-v3-build-check.yml' + - '.github/workflows/arduino-esp-v3-build-check.yml' workflow_dispatch: defaults: diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index 7ce94ec..f08eabf 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -21,7 +21,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**arduino-m5-build-check.yml' + - '.github/workflows/arduino-m5-build-check.yml' pull_request: paths: - 'src/unit/**.cpp' @@ -33,7 +33,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**arduino-m5-build-check.yml' + - '.github/workflows/arduino-m5-build-check.yml' workflow_dispatch: defaults: diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 9fbec79..9a9dec6 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -17,7 +17,7 @@ on: - '**.h' - '**.c' - '**.inl' - - '**clang-format-check.yml' + - '.github/workflows/clang-format-check.yml' - '**.clang-format' pull_request: paths: @@ -27,7 +27,7 @@ on: - '**.h' - '**.c' - '**.inl' - - '**clang-format-check.yml' + - '.github/workflows/clang-format-check.yml' - '**.clang-format' workflow_dispatch: diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 0b8c48f..688a420 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -17,8 +17,8 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**/platformio-build-check.yml' - - '**platformio.ini' + - '.github/workflows/platformio-build-check.yml' + - 'platformio.ini' pull_request: paths: - 'src/unit/**.cpp' @@ -30,8 +30,8 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '**/platformio-build-check.yml' - - '**platformio.ini' + - '.github/workflows/platformio-build-check.yml' + - 'platformio.ini' workflow_dispatch: defaults: @@ -105,6 +105,14 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install intelhex + run: pip install intelhex + - name: Build examples uses: karniv00l/platformio-run-action@v1 with: From 99fed1d48175a067989ff5317b4c6741365072a9 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 20:28:30 +0900 Subject: [PATCH 04/22] Bump M5UnitUnified dependency to >=0.4.4 and align project files with template --- .gitignore | 52 +++++++++++++++++++++++++++++++++++++++++----- library.json | 14 +++++++------ library.properties | 4 ++-- platformio.ini | 23 ++------------------ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 89cc49c..49576fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,47 @@ -.pio -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/launch.json -.vscode/ipch +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Doxygen +docs/html/ + +# PlatformIO +.pio/ + +# IDE +.vscode/ + +# macOS +.DS_Store + +# Temporary +tmp/ diff --git a/library.json b/library.json index 1b87428..ec7b2e1 100644 --- a/library.json +++ b/library.json @@ -1,17 +1,17 @@ { "name": "M5Unit-ENV", - "description": "Library for M5Stack UNIT ENV", - "keywords": "M5Stack UNIT ENV,M5UnitUnified", + "description": "Library for M5Stack UNIT ENV using M5UnitUnified", + "keywords": "m5stack, m5unitunified, env, scd40, scd41, sht30, sht40, qmp6988, bmp280, bme688, sgp30, co2, temperature, humidity, pressure, tvoc, i2c", "authors": { "name": "M5Stack", - "url": "http://www.m5stack.com" + "url": "https://www.m5stack.com" }, "repository": { "type": "git", "url": "https://github.com/m5stack/M5Unit-ENV.git" }, "dependencies": { - "m5stack/M5UnitUnified": ">=0.4.1", + "m5stack/M5UnitUnified": ">=0.4.4", "boschsensortec/BME68x Sensor library": ">=1.3.40408", "boschsensortec/bsec2": ">=1.10.2610" }, @@ -23,11 +23,13 @@ "M5UnitENV.h", "M5UnitUnifiedENV.h" ], - "platforms": "espressif32", + "platforms": [ + "espressif32" + ], "license": "MIT", "export": { "exclude": [ "docs/html" ] } -} \ No newline at end of file +} diff --git a/library.properties b/library.properties index bcdc5ad..ef12519 100644 --- a/library.properties +++ b/library.properties @@ -2,8 +2,8 @@ name=M5Unit-ENV version=1.3.2 author=M5Stack maintainer=M5Stack -sentence=Library for M5Stack UNIT ENV -paragraph=See more on https://docs.m5stack.com/en/unit/envIII +sentence=Library for M5Stack UNIT ENV using M5UnitUnified +paragraph=Supports ENV3(SHT30+QMP6988), ENV4(SHT40+BMP280), CO2(SCD40/41), ENVPro(BME688), TVOC(SGP30). category=Device Control url=https://github.com/m5stack/M5Unit-ENV architectures=esp32 diff --git a/platformio.ini b/platformio.ini index 3402975..25ced38 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,7 +11,7 @@ lib_ldf_mode = deep test_framework = googletest test_build_src = true lib_deps=m5stack/M5Unified - m5stack/M5UnitUnified@>=0.4.1 + m5stack/M5UnitUnified@>=0.4.4 boschsensortec/BME68x Sensor library@>=1.3.40408 ; -------------------------------- @@ -50,7 +50,7 @@ lib_deps = ${env.lib_deps} ${bsec2.lib_deps} [StampS3] -;include M5Capsule,M5DinMeter +;include M5Capsule, DinMeter extends = m5base board = m5stack-stamps3 lib_deps = ${env.lib_deps} @@ -177,21 +177,6 @@ lib_deps = ${env.lib_deps} platform = espressif32 @ 6.12.0 framework = arduino -[arduino_6_8_1] -platform = espressif32 @ 6.8.1 -framework = arduino - -[arduino_6_6_0] -platform = espressif32 @ 6.6.0 -framework = arduino - -[arduino_6_0_1] -platform = espressif32 @ 6.0.1 -framework = arduino - -;[arduino_3_5_0] -;platform = espressif32 @ 3.5.0 -;framework = arduino [nanoc6_latest] platform = espressif32 @ 6.12.0 @@ -205,10 +190,6 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ framework = arduino -[esp-idf] -platform = espressif32 @ 6.8.1 -framework = espidf - ; -------------------------------- ;Choose build options [option_release] From 985c9cdbeed758447f92750b448f1f13cca318a5 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 20:31:46 +0900 Subject: [PATCH 05/22] Cosmetic change --- src/unit/unit_BME688.hpp | 16 ++++++++-------- test/embedded/test_bme688/bme688_test.cpp | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/unit/unit_BME688.hpp b/src/unit/unit_BME688.hpp index 71e2e3c..a1967e1 100644 --- a/src/unit/unit_BME688.hpp +++ b/src/unit/unit_BME688.hpp @@ -111,14 +111,14 @@ enum class Oversampling : uint8_t { @brief IIR Filter setting */ enum class Filter : uint8_t { - None, //!< No filter - Coeff_1, //!< co-efficient 1 - Coeff_3, //!< co-efficient 3 - Coeff_7, //!< co-efficient 7 - Coeff_15, //!< co-efficient 15 - Coeff_31, //!< co-efficient 31 - Coeff_63, //!< co-efficient 63 - Coeff_127, //!< co-efficient 127 + None, //!< No filter + Coeff_1, //!< co-efficient 1 + Coeff_3, //!< co-efficient 3 + Coeff_7, //!< co-efficient 7 + Coeff_15, //!< co-efficient 15 + Coeff_31, //!< co-efficient 31 + Coeff_63, //!< co-efficient 63 + Coeff_127, //!< co-efficient 127 }; /*! diff --git a/test/embedded/test_bme688/bme688_test.cpp b/test/embedded/test_bme688/bme688_test.cpp index 7f9df1b..4b9c7e1 100644 --- a/test/embedded/test_bme688/bme688_test.cpp +++ b/test/embedded/test_bme688/bme688_test.cpp @@ -77,7 +77,6 @@ constexpr uint8_t bsec_config[] = { }; #endif - void check_measurement_values(UnitBME688* u) { auto latest = u->latest(); @@ -165,8 +164,8 @@ TEST_F(TestBME688, Settings) tph.filter = esp_random() % 0x07; { - auto s = m5::utility::formatString("Random TPH: %u/%u/%u/%u", tph.os_temp, tph.os_pres, tph.os_hum, - tph.filter); + auto s = + m5::utility::formatString("Random TPH: %u/%u/%u/%u", tph.os_temp, tph.os_pres, tph.os_hum, tph.filter); SCOPED_TRACE(s); } From f7a9d11c3802c917aa4cd05f07673d98e90b8b93 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 20:34:45 +0900 Subject: [PATCH 06/22] Cosmetic change --- src/M5UnitUnifiedENV.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/M5UnitUnifiedENV.hpp b/src/M5UnitUnifiedENV.hpp index ee12b2c..bfab988 100644 --- a/src/M5UnitUnifiedENV.hpp +++ b/src/M5UnitUnifiedENV.hpp @@ -46,9 +46,9 @@ namespace m5 { namespace unit { using UnitCO2 = m5::unit::UnitSCD40; //!< Product name alias for UnitSCD40 -using UnitCO2L = m5::unit::UnitSCD41; //!< Product name alias for UnitSCD41 -using UnitENVPro = m5::unit::UnitBME688; //!< Product name alias for UnitBME688 -using UnitTVOC = m5::unit::UnitSGP30; //!< Product name alias for UnitSGP30 +using UnitCO2L = m5::unit::UnitSCD41; //!< Product name alias for UnitSCD41 +using UnitENVPro = m5::unit::UnitBME688; //!< Product name alias for UnitBME688 +using UnitTVOC = m5::unit::UnitSGP30; //!< Product name alias for UnitSGP30 } // namespace unit } // namespace m5 From 8f43e98faebee2c1def3fb6907657e6c99dcd697 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:55:29 +0900 Subject: [PATCH 07/22] Fixes BMP280 filter_table OOB and interval_table --- src/unit/unit_BMP280.cpp | 14 +++++++++++--- src/unit/unit_BMP280.hpp | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/unit/unit_BMP280.cpp b/src/unit/unit_BMP280.cpp index 379e881..488b0d6 100644 --- a/src/unit/unit_BMP280.cpp +++ b/src/unit/unit_BMP280.cpp @@ -46,10 +46,18 @@ constexpr Standby standby_table[] = { }; -constexpr uint32_t interval_table[] = {0, 62, 125, 250, 500, 1000, 2000, 4000}; +constexpr uint32_t interval_table[] = {1, 62, 125, 250, 500, 1000, 2000, 4000}; constexpr Filter filter_table[] = { - Filter::Off, Filter::Coeff2, Filter::Coeff4, Filter::Coeff8, Filter::Coeff16, + Filter::Off, + Filter::Coeff2, + Filter::Coeff4, + Filter::Coeff8, + Filter::Coeff16, + // duplicated [5,6,7] + Filter::Coeff16, + Filter::Coeff16, + Filter::Coeff16, }; struct UseCaseSetting { @@ -143,7 +151,7 @@ struct Calculator { ((int32_t)trim.dig_T3)) >> 14; t_fine = var1 + var2; // [*1] - float T = (t_fine * 5 + 128) >> 8; + float T = static_cast((t_fine * 5 + 128) >> 8); return T * 0.01f; } diff --git a/src/unit/unit_BMP280.hpp b/src/unit/unit_BMP280.hpp index 985c899..064a309 100644 --- a/src/unit/unit_BMP280.hpp +++ b/src/unit/unit_BMP280.hpp @@ -265,7 +265,7 @@ class UnitBMP280 : public Component, public PeriodicMeasurementAdapter Date: Thu, 26 Mar 2026 18:55:44 +0900 Subject: [PATCH 08/22] Fixes BME688 strict aliasing violation in calibration --- src/unit/unit_BME688.cpp | 95 ++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/unit/unit_BME688.cpp b/src/unit/unit_BME688.cpp index 3c43954..b5a5217 100644 --- a/src/unit/unit_BME688.cpp +++ b/src/unit/unit_BME688.cpp @@ -37,6 +37,25 @@ static_assert(std::is_same::type, decltype(m5::unit static_assert(std::is_same::type, uint8_t>::value, "Illegal type"); namespace { + +// Read a little-endian integer from a byte buffer without aliasing violation +template +inline T read_le(const uint8_t* src) +{ + m5::types::EndianInt v; + memcpy(&v, src, sizeof(T)); + return v.get(); +} + +// Write a little-endian integer to a byte buffer without aliasing violation +template +inline void write_le(uint8_t* dst, const T val) +{ + m5::types::EndianInt v; + v.set(val); + memcpy(dst, &v, sizeof(T)); +} + constexpr Oversampling oversampling_table[8] = { Oversampling::None, Oversampling::x1, Oversampling::x2, Oversampling::x4, Oversampling::x8, Oversampling::x16, Oversampling::x16, Oversampling::x16, @@ -446,36 +465,36 @@ bool UnitBME688::readCalibration(bme688::bme68xCalibration& c) } // temperature - c.par_t1 = *(uint16_t*)(array1.data() + (CALIBRATION_TEMPERATURE_1_LOW - CALIBRATION_GROUP_1)); - c.par_t2 = *(int16_t*)(array0.data() + (CALIBRATION_TEMPERATURE_2_LOW - CALIBRATION_GROUP_0)); + c.par_t1 = read_le(array1.data() + (CALIBRATION_TEMPERATURE_1_LOW - CALIBRATION_GROUP_1)); + c.par_t2 = read_le(array0.data() + (CALIBRATION_TEMPERATURE_2_LOW - CALIBRATION_GROUP_0)); c.par_t3 = array0[CALIBRATION_TEMPERATURE_3 - CALIBRATION_GROUP_0]; // pressure - c.par_p1 = *(uint16_t*)(array0.data() + (CALIBRATION_PRESSURE_1_LOW - CALIBRATION_GROUP_0)); - c.par_p2 = *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_2_LOW - CALIBRATION_GROUP_0)); - c.par_p3 = (int8_t)array0[CALIBRATION_PRESSURE_3 - CALIBRATION_GROUP_0]; - c.par_p4 = *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_4_LOW - CALIBRATION_GROUP_0)); - c.par_p5 = *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_5_LOW - CALIBRATION_GROUP_0)); - c.par_p6 = (int8_t)array0[CALIBRATION_PRESSURE_6 - CALIBRATION_GROUP_0]; - c.par_p7 = (int8_t)array0[CALIBRATION_PRESSURE_7 - CALIBRATION_GROUP_0]; - c.par_p8 = *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_8_LOW - CALIBRATION_GROUP_0)); - c.par_p9 = *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_9_LOW - CALIBRATION_GROUP_0)); + c.par_p1 = read_le(array0.data() + (CALIBRATION_PRESSURE_1_LOW - CALIBRATION_GROUP_0)); + c.par_p2 = read_le(array0.data() + (CALIBRATION_PRESSURE_2_LOW - CALIBRATION_GROUP_0)); + c.par_p3 = static_cast(array0[CALIBRATION_PRESSURE_3 - CALIBRATION_GROUP_0]); + c.par_p4 = read_le(array0.data() + (CALIBRATION_PRESSURE_4_LOW - CALIBRATION_GROUP_0)); + c.par_p5 = read_le(array0.data() + (CALIBRATION_PRESSURE_5_LOW - CALIBRATION_GROUP_0)); + c.par_p6 = static_cast(array0[CALIBRATION_PRESSURE_6 - CALIBRATION_GROUP_0]); + c.par_p7 = static_cast(array0[CALIBRATION_PRESSURE_7 - CALIBRATION_GROUP_0]); + c.par_p8 = read_le(array0.data() + (CALIBRATION_PRESSURE_8_LOW - CALIBRATION_GROUP_0)); + c.par_p9 = read_le(array0.data() + (CALIBRATION_PRESSURE_9_LOW - CALIBRATION_GROUP_0)); c.par_p10 = array0[CALIBRATION_PRESSURE_10 - CALIBRATION_GROUP_0]; // humidity c.par_h1 = (array1[CALIBRATION_HUMIDITY_12 - CALIBRATION_GROUP_1] & 0X0F) | - (((uint16_t)array1[CALIBRATION_HUMIDITY_1_HIGH - CALIBRATION_GROUP_1]) << 4); + (static_cast(array1[CALIBRATION_HUMIDITY_1_HIGH - CALIBRATION_GROUP_1]) << 4); c.par_h2 = ((array1[CALIBRATION_HUMIDITY_12 - CALIBRATION_GROUP_1] >> 4) & 0X0F) | - (((uint16_t)array1[CALIBRATION_HUMIDITY_2_HIGH - CALIBRATION_GROUP_1]) << 4); - c.par_h3 = (int8_t)array1[CALIBRATION_HUMIDITY_3 - CALIBRATION_GROUP_1]; - c.par_h4 = (int8_t)array1[CALIBRATION_HUMIDITY_4 - CALIBRATION_GROUP_1]; - c.par_h5 = (int8_t)array1[CALIBRATION_HUMIDITY_5 - CALIBRATION_GROUP_1]; + (static_cast(array1[CALIBRATION_HUMIDITY_2_HIGH - CALIBRATION_GROUP_1]) << 4); + c.par_h3 = static_cast(array1[CALIBRATION_HUMIDITY_3 - CALIBRATION_GROUP_1]); + c.par_h4 = static_cast(array1[CALIBRATION_HUMIDITY_4 - CALIBRATION_GROUP_1]); + c.par_h5 = static_cast(array1[CALIBRATION_HUMIDITY_5 - CALIBRATION_GROUP_1]); c.par_h6 = array1[CALIBRATION_HUMIDITY_6 - CALIBRATION_GROUP_1]; - c.par_h7 = (int8_t)array1[CALIBRATION_HUMIDITY_7 - CALIBRATION_GROUP_1]; + c.par_h7 = static_cast(array1[CALIBRATION_HUMIDITY_7 - CALIBRATION_GROUP_1]); // gas - c.par_gh1 = (int8_t)array1[CALIBRATION_GAS_1 - CALIBRATION_GROUP_1]; - c.par_gh2 = *(int16_t*)(array1.data() + (CALIBRATION_GAS_2_LOW - CALIBRATION_GROUP_1)); - c.par_gh3 = (int8_t)array1[CALIBRATION_GAS_3 - CALIBRATION_GROUP_1]; + c.par_gh1 = static_cast(array1[CALIBRATION_GAS_1 - CALIBRATION_GROUP_1]); + c.par_gh2 = read_le(array1.data() + (CALIBRATION_GAS_2_LOW - CALIBRATION_GROUP_1)); + c.par_gh3 = static_cast(array1[CALIBRATION_GAS_3 - CALIBRATION_GROUP_1]); c.res_heat_range = (array2[CALIBRATION_RES_HEAT_RANGE - CALIBRATION_GROUP_2] >> 4) & 0x03; - c.res_heat_val = (int8_t)array2[CALIBRATION_RES_HEAT_VAL - CALIBRATION_GROUP_2]; + c.res_heat_val = static_cast(array2[CALIBRATION_RES_HEAT_VAL - CALIBRATION_GROUP_2]); return true; } @@ -494,22 +513,22 @@ bool UnitBME688::writeCalibration(const bme688::bme68xCalibration& c) } // temperature - *(uint16_t*)(array1.data() + (CALIBRATION_TEMPERATURE_1_LOW - CALIBRATION_GROUP_1)) = c.par_t1; - *(int16_t*)(array0.data() + (CALIBRATION_TEMPERATURE_2_LOW - CALIBRATION_GROUP_0)) = c.par_t2; - array0[CALIBRATION_TEMPERATURE_3 - CALIBRATION_GROUP_0] = c.par_t3; + write_le(array1.data() + (CALIBRATION_TEMPERATURE_1_LOW - CALIBRATION_GROUP_1), c.par_t1); + write_le(array0.data() + (CALIBRATION_TEMPERATURE_2_LOW - CALIBRATION_GROUP_0), c.par_t2); + array0[CALIBRATION_TEMPERATURE_3 - CALIBRATION_GROUP_0] = c.par_t3; // pressure - *(uint16_t*)(array0.data() + (CALIBRATION_PRESSURE_1_LOW - CALIBRATION_GROUP_0)) = c.par_p1; - *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_2_LOW - CALIBRATION_GROUP_0)) = c.par_p2; - array0[CALIBRATION_PRESSURE_3 - CALIBRATION_GROUP_0] = c.par_p3; - *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_4_LOW - CALIBRATION_GROUP_0)) = c.par_p4; - *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_5_LOW - CALIBRATION_GROUP_0)) = c.par_p5; - array0[CALIBRATION_PRESSURE_6 - CALIBRATION_GROUP_0] = c.par_p6; - array0[CALIBRATION_PRESSURE_7 - CALIBRATION_GROUP_0] = c.par_p7; - *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_8_LOW - CALIBRATION_GROUP_0)) = c.par_p8; - *(int16_t*)(array0.data() + (CALIBRATION_PRESSURE_9_LOW - CALIBRATION_GROUP_0)) = c.par_p9; - array0[CALIBRATION_PRESSURE_10 - CALIBRATION_GROUP_0] = c.par_p10; + write_le(array0.data() + (CALIBRATION_PRESSURE_1_LOW - CALIBRATION_GROUP_0), c.par_p1); + write_le(array0.data() + (CALIBRATION_PRESSURE_2_LOW - CALIBRATION_GROUP_0), c.par_p2); + array0[CALIBRATION_PRESSURE_3 - CALIBRATION_GROUP_0] = c.par_p3; + write_le(array0.data() + (CALIBRATION_PRESSURE_4_LOW - CALIBRATION_GROUP_0), c.par_p4); + write_le(array0.data() + (CALIBRATION_PRESSURE_5_LOW - CALIBRATION_GROUP_0), c.par_p5); + array0[CALIBRATION_PRESSURE_6 - CALIBRATION_GROUP_0] = c.par_p6; + array0[CALIBRATION_PRESSURE_7 - CALIBRATION_GROUP_0] = c.par_p7; + write_le(array0.data() + (CALIBRATION_PRESSURE_8_LOW - CALIBRATION_GROUP_0), c.par_p8); + write_le(array0.data() + (CALIBRATION_PRESSURE_9_LOW - CALIBRATION_GROUP_0), c.par_p9); + array0[CALIBRATION_PRESSURE_10 - CALIBRATION_GROUP_0] = c.par_p10; // humidity - uint8_t h12{(uint8_t)((c.par_h1 & 0x0F) | ((c.par_h2 & 0x0F) << 4))}; + uint8_t h12{static_cast((c.par_h1 & 0x0F) | ((c.par_h2 & 0x0F) << 4))}; array1[CALIBRATION_HUMIDITY_12 - CALIBRATION_GROUP_1] = h12; array1[CALIBRATION_HUMIDITY_1_HIGH - CALIBRATION_GROUP_1] = (c.par_h1 >> 4) & 0xFF; array1[CALIBRATION_HUMIDITY_2_HIGH - CALIBRATION_GROUP_1] = (c.par_h2 >> 4) & 0xFF; @@ -519,9 +538,9 @@ bool UnitBME688::writeCalibration(const bme688::bme68xCalibration& c) array1[CALIBRATION_HUMIDITY_6 - CALIBRATION_GROUP_1] = c.par_h6; array1[CALIBRATION_HUMIDITY_7 - CALIBRATION_GROUP_1] = c.par_h7; // gas - array1[CALIBRATION_GAS_1 - CALIBRATION_GROUP_1] = c.par_gh1; - *(int16_t*)(array1.data() + (CALIBRATION_GAS_2_LOW - CALIBRATION_GROUP_1)) = c.par_gh2; - array1[CALIBRATION_GAS_3 - CALIBRATION_GROUP_1] = c.par_gh3; + array1[CALIBRATION_GAS_1 - CALIBRATION_GROUP_1] = c.par_gh1; + write_le(array1.data() + (CALIBRATION_GAS_2_LOW - CALIBRATION_GROUP_1), c.par_gh2); + array1[CALIBRATION_GAS_3 - CALIBRATION_GROUP_1] = c.par_gh3; array2[CALIBRATION_RES_HEAT_RANGE - CALIBRATION_GROUP_2] &= ~(0x03 << 4); array2[CALIBRATION_RES_HEAT_RANGE - CALIBRATION_GROUP_2] |= (c.res_heat_range & 0x03) << 4; array2[CALIBRATION_RES_HEAT_VAL - CALIBRATION_GROUP_2] = c.res_heat_val; From 31bd2c9a380e26c31ed721207baa46af4373e08f Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:55:57 +0900 Subject: [PATCH 09/22] Remove unused calculatInterval and fixes typos in QMP6988 --- src/unit/unit_QMP6988.cpp | 42 +-------------------------------------- src/unit/unit_QMP6988.hpp | 2 +- 2 files changed, 2 insertions(+), 42 deletions(-) diff --git a/src/unit/unit_QMP6988.cpp b/src/unit/unit_QMP6988.cpp index 700aeef..c4dc009 100644 --- a/src/unit/unit_QMP6988.cpp +++ b/src/unit/unit_QMP6988.cpp @@ -54,26 +54,6 @@ constexpr UseCaseSetting uc_table[] = { {OversamplingSetting::UltraHighAccuracy, Filter::Coeff32}, }; -#if 1 -constexpr elapsed_time_t standby_time_table[] = { - 5, 5, 50, 250, 500, 1000, 2000, 4000, -}; - -constexpr float ostb{4.4933f}; -constexpr float oversampling_temp_time_table[] = { - 0.0f, ostb * 1, ostb * 2, ostb * 4, ostb * 8, ostb * 16, ostb * 32, ostb * 64, -}; - -constexpr float ospb{0.5032f}; -constexpr float oversampling_pressure_time_table[] = { - 0.0f, ospb * 1, ospb * 2, ospb * 4, ospb * 8, ospb * 16, ospb * 32, ospb * 64, -}; - -constexpr float filter_time_table[] = { - 0.0f, 0.3f, 0.6f, 1.2f, 2.4f, 4.8f, 9.6f, 9.6f, 9.6f, -}; -#endif - constexpr uint32_t interval_table[] = {1, 5, 50, 250, 500, 1000, 2000, 4000}; int16_t convert_temperature256(const int32_t dt, const m5::unit::qmp6988::Calibration& c) @@ -121,7 +101,7 @@ int32_t convert_pressure16(const int32_t dp, const int16_t tx, const Calibration wk3 += wk2; // 62,62->63Q30 wk1 += wk3 >> 15; // Q30 >> 15 = Q15 wk1 /= 32767L; - wk1 >>= 11; // Q15 >> 7 = Q4 + wk1 >>= 11; // Q15 >> 11 = Q4 wk1 += c.b00; // Q4 + 20Q4 // Not shifted to set output at 16 Pa // wk1 >>= 4; // 28Q4 -> 24Q0 @@ -212,26 +192,6 @@ const char UnitQMP6988::name[] = "UnitQMP6988"; const types::uid_t UnitQMP6988::uid{"UnitQMP6988"_mmh3}; const types::attr_t UnitQMP6988::attr{attribute::AccessI2C}; -types::elapsed_time_t calculatInterval(const Standby st, const Oversampling ost, const Oversampling osp, const Filter f) -{ - // M5_LIB_LOGV("ST:%u OST:%u OSP:%u F:%u", st, ost, osp, f); - // M5_LIB_LOGV( - // "Value ST:%u OST:%u OSP:%u F:%u", - // standby_time_table[m5::stl::to_underlying(st)], - // (elapsed_time_t)std::ceil( - // oversampling_temp_time_table[m5::stl::to_underlying(ost)]), - // (elapsed_time_t)std::ceil( - // oversampling_pressure_time_table[m5::stl::to_underlying(osp)]), - // (elapsed_time_t)std::ceil( - // filter_time_table[m5::stl::to_underlying(f)])); - - elapsed_time_t itv = standby_time_table[m5::stl::to_underlying(st)] + - (elapsed_time_t)std::ceil(oversampling_temp_time_table[m5::stl::to_underlying(ost)]) + - (elapsed_time_t)std::ceil(oversampling_pressure_time_table[m5::stl::to_underlying(osp)]) + - (elapsed_time_t)std::ceil(filter_time_table[m5::stl::to_underlying(f)]); - return itv; -} - bool UnitQMP6988::begin() { auto ssize = stored_size(); diff --git a/src/unit/unit_QMP6988.hpp b/src/unit/unit_QMP6988.hpp index 834ec93..a02564e 100644 --- a/src/unit/unit_QMP6988.hpp +++ b/src/unit/unit_QMP6988.hpp @@ -335,7 +335,7 @@ class UnitQMP6988 : public Component, public PeriodicMeasurementAdapter Date: Thu, 26 Mar 2026 18:56:12 +0900 Subject: [PATCH 10/22] Fixes SCD40 narrowing conversion --- src/unit/unit_SCD40.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unit/unit_SCD40.cpp b/src/unit/unit_SCD40.cpp index 4b899b4..eeddc9e 100644 --- a/src/unit/unit_SCD40.cpp +++ b/src/unit/unit_SCD40.cpp @@ -24,7 +24,7 @@ struct Temperature { } constexpr static uint16_t toUint16(const float f) { - return f * 65536 / 175; + return static_cast(f * 65536 / 175); } constexpr static float OFFSET_MIN{0.0f}; constexpr static float OFFSET_MAX{175.0f}; From bc17850192777f1a2e20dc46acace3ceee857c14 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:56:24 +0900 Subject: [PATCH 11/22] Add HeaterPower/HeaterDuration API and fixes SHT40 timing --- src/unit/unit_SHT40.cpp | 59 ++++--- src/unit/unit_SHT40.hpp | 64 +++++++- test/embedded/test_sht40/sht40_test.cpp | 208 +++++++++++++++++++++++- 3 files changed, 308 insertions(+), 23 deletions(-) diff --git a/src/unit/unit_SHT40.cpp b/src/unit/unit_SHT40.cpp index dec63b1..7a75720 100644 --- a/src/unit/unit_SHT40.cpp +++ b/src/unit/unit_SHT40.cpp @@ -33,16 +33,22 @@ constexpr uint8_t periodic_cmd[] = { MEASURE_LOW, }; +// Heater commands always perform high precision measurement (datasheet 4.9) +// Total time = heater duration (max) + high precision measurement (max 8.3ms) +constexpr elapsed_time_t heater_long = 1100; // max 1.1s +constexpr elapsed_time_t heater_short = 110; // max 0.11s +constexpr elapsed_time_t measure_high = 9; // max 8.3ms + constexpr elapsed_time_t interval_table[] = { // HIGH - 1100, 110, - 9, // 8.2 + heater_long + measure_high, heater_short + measure_high, + 9, // 8.3 max // MEDIUM - 1100, 110, - 5, // 4.5 + heater_long + measure_high, heater_short + measure_high, + 5, // 4.5 max // LOW - 1100, 110, - 2, // 1.7 + heater_long + measure_high, heater_short + measure_high, + 2, // 1.6 max }; constexpr float MAX_HEATER_DUTY{0.05f}; @@ -97,8 +103,15 @@ bool UnitSHT40::begin() } _precision = _cfg.precision; - _heater = _cfg.heater; _duty = _cfg.heater_duty; + if (_cfg.use_heater) { + // New API: HeaterPower + HeaterDuration + _heater = static_cast(_cfg.heater_duration); + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.heater_power, _cfg.heater_duration, _cfg.heater_duty) + : true; + } + // Legacy API: Precision + Heater (still functional for backward compatibility) + _heater = _cfg.heater; return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.precision, _cfg.heater, _cfg.heater_duty) : true; } @@ -112,23 +125,25 @@ void UnitSHT40::update(const bool force) _updated = read_measurement(d); if (_updated) { - _latest = at; - d.heater = (_interval != _duration_heater); + // heater flag reflects the PREVIOUS command (whose result we just read) + d.heater = (_heater != Heater::None && _interval == _duration_heater); _data->push_back(d); - uint8_t cmd{}; - if (at >= _latest_heater + _interval_heater) { - cmd = _cmd; - _latest_heater = at; - _interval = _duration_heater; - } else { - cmd = _measureCmd; - _interval = _duration_measure; + uint8_t cmd = _measureCmd; + if (_heater != Heater::None) { + if (at >= _latest_heater + _interval_heater) { + cmd = _cmd; + _latest_heater = at; + _interval = _duration_heater; + } else { + _interval = _duration_measure; + } } if (!writeRegister(cmd)) { M5_LIB_LOGE("Failed to write, stop periodic measurement"); _periodic = false; } + _latest = m5::utility::millis(); } } } @@ -148,8 +163,7 @@ bool UnitSHT40::start_periodic_measurement(const sht40::Precision precision, con _cmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)]; _measureCmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(Heater::None)]; - - _periodic = writeRegister(_cmd); + _periodic = writeRegister(_cmd); if (_periodic) { _precision = precision; _heater = heater; @@ -204,6 +218,11 @@ bool UnitSHT40::measureSingleshot(sht40::Data& d, const sht40::Precision precisi return false; } +bool UnitSHT40::measureSingleshot(sht40::Data& d, const sht40::HeaterPower power, const sht40::HeaterDuration duration) +{ + return measureSingleshot(d, static_cast(power), static_cast(duration)); +} + bool UnitSHT40::read_measurement(sht40::Data& d) { if (readWithTransaction(d.raw.data(), d.raw.size()) == m5::hal::error::error_t::OK) { @@ -260,7 +279,7 @@ bool UnitSHT40::readSerialNumber(uint32_t& serialNumber) } std::array rbuf; - if (readRegister(GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), 1)) { + if (readRegister(GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), 10)) { m5::types::big_uint16_t u16[2]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}}; m5::utility::CRC8_Checksum crc{}; if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5]) { diff --git a/src/unit/unit_SHT40.hpp b/src/unit/unit_SHT40.hpp index 0da1e73..6f62cc1 100644 --- a/src/unit/unit_SHT40.hpp +++ b/src/unit/unit_SHT40.hpp @@ -36,6 +36,9 @@ enum class Precision : uint8_t { /*! @enum Heater @brief Heater behavior + @deprecated Use HeaterPower and HeaterDuration instead. + When heater is active, the Precision parameter selects heater power, not measurement precision. + All heater commands perform high precision measurement regardless of Precision setting. */ enum class Heater : uint8_t { Long, //!< Activate heater for 1s @@ -43,6 +46,26 @@ enum class Heater : uint8_t { None //!< Not activate heater }; +/*! + @enum HeaterPower + @brief Heater power level + @note All heater commands perform high precision measurement regardless of power level + */ +enum class HeaterPower : uint8_t { + High = 0, //!< Highest heater power (typ. 200mW @ 3.3V) + Medium = 1, //!< Medium heater power (typ. 110mW @ 3.3V) + Low = 2, //!< Lowest heater power (typ. 20mW @ 3.3V) +}; + +/*! + @enum HeaterDuration + @brief Heater activation duration + */ +enum class HeaterDuration : uint8_t { + Long = 0, //!< Activate heater for 1s + Short = 1, //!< Activate heater for 0.1s +}; + /*! @struct Data @brief Measurement data group @@ -80,10 +103,16 @@ class UnitSHT40 : public Component, public PeriodicMeasurementAdapter::startPeriodicMeasurement(precision, heater, duty); } + /*! + @brief Start periodic measurement with heater + @param power Heater power level + @param duration Heater activation duration + @param duty Duty for activate heater [~ 0.05f] + @return True if successful + @note Always performs high precision measurement regardless of heater power level + @warning Datasheet says "keeping in mind that the heater is designed for a maximal duty cycle of less than 5%" + */ + inline bool startPeriodicMeasurement(const sht40::HeaterPower power, const sht40::HeaterDuration duration, + const float duty = 0.05f) + { + return PeriodicMeasurementAdapter::startPeriodicMeasurement( + static_cast(power), static_cast(duration), duty); + } /*! @brief Start periodic measurement using previous settings @return True if successful @@ -184,10 +231,25 @@ class UnitSHT40 : public Component, public PeriodicMeasurementAdapterstopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + // Test new HeaterPower/HeaterDuration API + std::tuple hp_table[] = { + {"High/Long", HeaterPower::High, HeaterDuration::Long}, + {"High/Short", HeaterPower::High, HeaterDuration::Short}, + {"Medium/Long", HeaterPower::Medium, HeaterDuration::Long}, + {"Medium/Short", HeaterPower::Medium, HeaterDuration::Short}, + {"Low/Long", HeaterPower::Low, HeaterDuration::Long}, + {"Low/Short", HeaterPower::Low, HeaterDuration::Short}, + }; + + for (auto&& e : hp_table) { + const char* s{}; + HeaterPower power{}; + HeaterDuration duration{}; + std::tie(s, power, duration) = e; + + SCOPED_TRACE(s); + + sht40::Data d{}; + EXPECT_TRUE(unit->measureSingleshot(d, power, duration)); + EXPECT_TRUE(std::isfinite(d.temperature())); + EXPECT_TRUE(std::isfinite(d.humidity())); + EXPECT_TRUE(d.heater); // All heater commands should set heater flag + } +} + TEST_F(TestSHT40, Periodic) { SCOPED_TRACE(ustr); @@ -157,9 +190,9 @@ TEST_F(TestSHT40, Periodic) std::tie(s, p, h, tm) = e; SCOPED_TRACE(s); - M5_LOGI("Periodic: %s interval:%lu ms", s, (unsigned long)tm); EXPECT_TRUE(unit->startPeriodicMeasurement(p, h)); + M5_LOGI("Periodic: %s interval:%lu ms", s, (unsigned long)unit->interval()); EXPECT_TRUE(unit->inPeriodic()); // Cannot call all singleshot in periodic @@ -175,7 +208,6 @@ TEST_F(TestSHT40, Periodic) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); - // EXPECT_TRUE(unit->startPeriodicMeasurement(p, h)); EXPECT_TRUE(unit->inPeriodic()); @@ -197,6 +229,13 @@ TEST_F(TestSHT40, Periodic) EXPECT_FALSE(unit->empty()); EXPECT_TRUE(unit->full()); + // Verify heater flag: first sample uses heater command, rest use measure command + if (h == Heater::None) { + EXPECT_FALSE(unit->oldest().heater); + } else { + EXPECT_TRUE(unit->oldest().heater); + } + uint32_t cnt{2}; while (cnt-- && unit->available()) { EXPECT_TRUE(std::isfinite(unit->temperature())); @@ -242,4 +281,169 @@ TEST_F(TestSHT40, Periodic) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); } + + // startPeriodicMeasurement with HeaterPower/HeaterDuration + { + EXPECT_TRUE(unit->startPeriodicMeasurement(HeaterPower::Low, HeaterDuration::Short)); + EXPECT_TRUE(unit->inPeriodic()); + + uint32_t timeout3 = is_bus ? std::max(unit->interval(), 500) * (STORED_SIZE + 1) * 4 + : unit->interval() * (STORED_SIZE + 1); + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout3); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + } +} + +#if 0 +TEST_F(TestSHT40, PeriodicWithSoftResetBetweenCases) +{ + SCOPED_TRACE(ustr); + + auto ad = unit->asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_TRUE(unit->softReset()); + + for (auto&& e : sm_table) { + const char* s{}; + Precision p{}; + Heater h{}; + elapsed_time_t tm{}; + std::tie(s, p, h, tm) = e; + + SCOPED_TRACE(s); + + EXPECT_TRUE(unit->startPeriodicMeasurement(p, h)); + EXPECT_TRUE(unit->inPeriodic()); + + uint32_t cycle = std::max(tm, unit->interval()); + uint32_t timeout = is_bus ? std::max(cycle, (uint32_t)500) * (STORED_SIZE + 1) * 4 : cycle * (STORED_SIZE + 1); + uint32_t tolerance = is_bus ? 5 : 1; + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + EXPECT_LE(r.median(), r.expected_interval + tolerance); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_TRUE(unit->softReset()); + + unit->flush(); + } +} + +TEST_F(TestSHT40, PeriodicLowHeaterOnly) +{ + SCOPED_TRACE(ustr); + + auto ad = unit->asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + std::tuple low_table[] = { + {"LowLong", Precision::Low, Heater::Long, 2}, + {"LowShort", Precision::Low, Heater::Short, 2}, + }; + + for (auto&& e : low_table) { + const char* s{}; + Precision p{}; + Heater h{}; + elapsed_time_t tm{}; + std::tie(s, p, h, tm) = e; + + SCOPED_TRACE(s); + + EXPECT_TRUE(unit->softReset()); + unit->flush(); + + EXPECT_TRUE(unit->startPeriodicMeasurement(p, h)); + EXPECT_TRUE(unit->inPeriodic()); + M5_LOGI("PeriodicLowHeaterOnly: %s interval:%lu ms", s, (unsigned long)unit->interval()); + + uint32_t cycle = std::max(tm, unit->interval()); + uint32_t timeout = is_bus ? std::max(cycle, (uint32_t)500) * (STORED_SIZE + 1) * 4 : cycle * (STORED_SIZE + 1); + uint32_t tolerance = is_bus ? 5 : 1; + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + EXPECT_LE(r.median(), r.expected_interval + tolerance); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + } +} + +TEST_F(TestSHT40, PeriodicLowNoneOnly) +{ + SCOPED_TRACE(ustr); + + auto ad = unit->asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_TRUE(unit->softReset()); + unit->flush(); + + EXPECT_TRUE(unit->startPeriodicMeasurement(Precision::Low, Heater::None)); + EXPECT_TRUE(unit->inPeriodic()); + M5_LOGI("PeriodicLowNoneOnly interval:%lu ms", (unsigned long)unit->interval()); + + uint32_t cycle = std::max(2, unit->interval()); + uint32_t timeout = is_bus ? std::max(cycle, (uint32_t)500) * (STORED_SIZE + 1) * 4 : cycle * (STORED_SIZE + 1); + uint32_t tolerance = is_bus ? 5 : 1; + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + EXPECT_LE(r.median(), r.expected_interval + tolerance); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); +} + +TEST_F(TestSHT40, PeriodicLowLongThenLowNone) +{ + SCOPED_TRACE(ustr); + + auto ad = unit->asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + auto run_periodic = [&](const char* label, const Precision p, const Heater h, const elapsed_time_t tm) { + SCOPED_TRACE(label); + + EXPECT_TRUE(unit->startPeriodicMeasurement(p, h)); + EXPECT_TRUE(unit->inPeriodic()); + M5_LOGI("PeriodicLowLongThenLowNone: %s interval:%lu ms", label, (unsigned long)unit->interval()); + + uint32_t cycle = std::max(tm, unit->interval()); + uint32_t timeout = is_bus ? std::max(cycle, (uint32_t)500) * (STORED_SIZE + 1) * 4 : cycle * (STORED_SIZE + 1); + uint32_t tolerance = is_bus ? 5 : 1; + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + EXPECT_LE(r.median(), r.expected_interval + tolerance); + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + unit->flush(); + }; + + EXPECT_TRUE(unit->softReset()); + run_periodic("LowLong", Precision::Low, Heater::Long, 2); + + EXPECT_TRUE(unit->softReset()); + run_periodic("LowNone", Precision::Low, Heater::None, 2); } +#endif From f65642eda3cf9496c0d835b817f0d62bf3b94101 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:56:38 +0900 Subject: [PATCH 12/22] Add HatENV3 support --- .../UnitENVIII/PlotToSerial/PlotToSerial.ino | 10 +- .../PlotToSerial/main/PlotToSerial.cpp | 108 ++++++++---- src/M5UnitUnifiedENV.hpp | 3 +- test/embedded/embedded_main.cpp | 10 +- test/embedded/test_qmp6988/qmp6988_test.cpp | 37 +++++ test/embedded/test_sht30/sht30_test.cpp | 37 +++++ unit_env3_env.ini | 155 +++++++++++++++++- 7 files changed, 323 insertions(+), 37 deletions(-) diff --git a/examples/UnitUnified/UnitENVIII/PlotToSerial/PlotToSerial.ino b/examples/UnitUnified/UnitENVIII/PlotToSerial/PlotToSerial.ino index f6e458d..ddccb66 100644 --- a/examples/UnitUnified/UnitENVIII/PlotToSerial/PlotToSerial.ino +++ b/examples/UnitUnified/UnitENVIII/PlotToSerial/PlotToSerial.ino @@ -4,8 +4,16 @@ * SPDX-License-Identifier: MIT */ /* - Example using M5UnitUnified for UnitENVIII + Example using M5UnitUnified for Unit/HatENVIII Required - M5Unified: https://github.com/m5stack/M5Unified */ +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_ENV3) && !defined(USING_HAT_ENV3) +#define USING_UNIT_ENV3 // Default: UnitENV3 (GROVE port) +// #define USING_HAT_ENV3 +#endif +// ************************************************************* #include "main/PlotToSerial.cpp" diff --git a/examples/UnitUnified/UnitENVIII/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/UnitENVIII/PlotToSerial/main/PlotToSerial.cpp index a2f39d7..20ed759 100644 --- a/examples/UnitUnified/UnitENVIII/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/UnitENVIII/PlotToSerial/main/PlotToSerial.cpp @@ -4,41 +4,76 @@ * SPDX-License-Identifier: MIT */ /* - Example using M5UnitUnified for UnitENVIII + Example using M5UnitUnified for Unit/HatENVIII */ #include #include #include #include // For NessoN1 -// Using combined unit if defined -#define USING_ENV3 +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_ENV3) && !defined(USING_HAT_ENV3) +// For UnitENV3 +// #define USING_UNIT_ENV3 +// For HatENV3 +// #define USING_HAT_ENV3 +#endif +// ************************************************************* namespace { auto& lcd = M5.Display; m5::unit::UnitUnified Units; -#if defined(USING_ENV3) -#pragma message "Using combined unit(ENV3)" -m5::unit::UnitENV3 unitENV3; +#if defined(USING_UNIT_ENV3) +#pragma message "Using UnitENV3" +m5::unit::UnitENV3 unit; +#elif defined(USING_HAT_ENV3) +#pragma message "Using HatENV3" +m5::unit::HatENV3 unit; #else -#pragma message "Using each unit" -m5::unit::UnitSHT30 unitSHT30; -m5::unit::UnitQMP6988 unitQMP6988; +#error "Choose unit or hat" #endif -#if defined(USING_ENV3) -auto& sht30 = unitENV3.sht30; -auto& qmp6988 = unitENV3.qmp6988; -#else -auto& sht30 = unitSHT30; -auto& qmp6988 = unitQMP6988; +auto& sht30 = unit.sht30; +auto& qmp6988 = unit.qmp6988; + +#if defined(USING_HAT_ENV3) +struct HatPins { + int sda, scl; +}; +HatPins get_hat_pins(const m5::board_t board) +{ + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + return {0, 26}; + case m5::board_t::board_M5StickS3: + return {8, 0}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7}; + default: + return {-1, -1}; + } +} #endif + } // namespace void setup() { - M5.begin(); + auto m5cfg = M5.config(); +#if defined(USING_HAT_ENV3) + m5cfg.pmic_button = false; // Disable BtnPWR + m5cfg.internal_imu = false; // Disable internal IMU + m5cfg.internal_rtc = false; // Disable internal RTC +#endif + M5.begin(m5cfg); + M5.setTouchButtonHeightByRatio(100); // The screen shall be in landscape mode if (lcd.height() > lcd.width()) { @@ -47,6 +82,27 @@ void setup() auto board = M5.getBoard(); +#if defined(USING_HAT_ENV3) + const auto pins = get_hat_pins(board); + M5_LOGI("getHatPin: SDA:%d SCL:%d", pins.sda, pins.scl); + if (pins.sda < 0 || pins.scl < 0) { + M5_LOGE("Unsupported board for HatENV3"); + lcd.fillScreen(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, 400 * 1000U); + if (!Units.add(unit, wire) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.fillScreen(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } +#else // NessoN1: Arduino Wire (I2C_NUM_0) cannot be used for GROVE port. // Wire is used by M5Unified In_I2C for internal devices (IOExpander etc.). // Solution: Use SoftwareI2C via M5HAL (bit-banging) for the GROVE port. @@ -64,31 +120,18 @@ void setup() i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); M5_LOGI("Bus:%d", i2c_bus.has_value()); -#if defined(USING_ENV3) - unit_ready = Units.add(unitENV3, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); -#else - unit_ready = Units.add(unitSHT30, i2c_bus ? i2c_bus.value() : nullptr) && - Units.add(unitQMP6988, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); -#endif + unit_ready = Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); } else if (board == m5::board_t::board_M5NanoC6) { // NanoC6: Use M5.Ex_I2C (m5::I2C_Class, not Arduino Wire) M5_LOGI("Using M5.Ex_I2C"); -#if defined(USING_ENV3) - unit_ready = Units.add(unitENV3, M5.Ex_I2C) && Units.begin(); -#else - unit_ready = Units.add(unitSHT30, M5.Ex_I2C) && Units.add(unitQMP6988, M5.Ex_I2C) && Units.begin(); -#endif + unit_ready = Units.add(unit, M5.Ex_I2C) && Units.begin(); } else { 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); Wire.end(); Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U); -#if defined(USING_ENV3) - unit_ready = Units.add(unitENV3, Wire) && Units.begin(); -#else - unit_ready = Units.add(unitSHT30, Wire) && Units.add(unitQMP6988, Wire) && Units.begin(); -#endif + unit_ready = Units.add(unit, Wire) && Units.begin(); } if (!unit_ready) { M5_LOGE("Failed to begin"); @@ -98,6 +141,7 @@ void setup() m5::utility::delay(10000); } } +#endif M5_LOGI("M5UnitUnified has been begun"); M5_LOGI("%s", Units.debugInfo().c_str()); diff --git a/src/M5UnitUnifiedENV.hpp b/src/M5UnitUnifiedENV.hpp index bfab988..7cefe76 100644 --- a/src/M5UnitUnifiedENV.hpp +++ b/src/M5UnitUnifiedENV.hpp @@ -35,7 +35,7 @@ /*! @namespace m5 - @brief Top level namespace of M5stack + @brief Top level namespace of M5Stack */ namespace m5 { @@ -49,6 +49,7 @@ using UnitCO2 = m5::unit::UnitSCD40; //!< Product name alias for UnitSCD40 using UnitCO2L = m5::unit::UnitSCD41; //!< Product name alias for UnitSCD41 using UnitENVPro = m5::unit::UnitBME688; //!< Product name alias for UnitBME688 using UnitTVOC = m5::unit::UnitSGP30; //!< Product name alias for UnitSGP30 +using HatENV3 = m5::unit::UnitENV3; //!< Product name alias for HatENVIII } // namespace unit } // namespace m5 diff --git a/test/embedded/embedded_main.cpp b/test/embedded/embedded_main.cpp index 8a8695e..3b6c3d4 100644 --- a/test/embedded/embedded_main.cpp +++ b/test/embedded/embedded_main.cpp @@ -32,7 +32,15 @@ void setup() { delay(1500); +#if defined(USING_HAT_SHT30) || defined(USING_HAT_QMP6988) + auto m5cfg = M5.config(); + m5cfg.pmic_button = false; // Disable BtnPWR + m5cfg.internal_imu = false; // Disable internal IMU + m5cfg.internal_rtc = false; // Disable internal RTC + M5.begin(m5cfg); +#else M5.begin(); +#endif M5_LOGI("CPP %ld", __cplusplus); M5_LOGI("ESP-IDF Version %d.%d.%d", (ESP_IDF_VERSION >> 16) & 0xFF, (ESP_IDF_VERSION >> 8) & 0xFF, @@ -40,7 +48,7 @@ void setup() M5_LOGI("BOARD:%X", M5.getBoard()); M5_LOGI("Heap: %u", esp_get_free_heap_size()); - lcd.clear(TFT_DARKGRAY); + lcd.fillScreen(TFT_DARKGRAY); ::testing::InitGoogleTest(); #ifdef GTEST_FILTER diff --git a/test/embedded/test_qmp6988/qmp6988_test.cpp b/test/embedded/test_qmp6988/qmp6988_test.cpp index 0544ee0..d01ee1c 100644 --- a/test/embedded/test_qmp6988/qmp6988_test.cpp +++ b/test/embedded/test_qmp6988/qmp6988_test.cpp @@ -25,6 +25,32 @@ using m5::unit::types::elapsed_time_t; constexpr uint32_t STORED_SIZE{8}; +#if defined(USING_HAT_QMP6988) +namespace hat { +struct I2cPins { + int sda, scl; +}; + +I2cPins get_hat_pins(const m5::board_t board) +{ + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + return {0, 26}; + case m5::board_t::board_M5StickS3: + return {8, 0}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7}; + default: + return {-1, -1}; + } +} +} // namespace hat +#endif + class TestQMP6988 : public I2CComponentTestBase { protected: virtual UnitQMP6988* get_instance() override @@ -35,6 +61,17 @@ class TestQMP6988 : public I2CComponentTestBase { ptr->component_config(ccfg); return ptr; } +#if defined(USING_HAT_QMP6988) + virtual bool begin() override + { + auto board = M5.getBoard(); + const auto pins = hat::get_hat_pins(board); + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, unit->component_config().clock); + return Units.add(*unit, wire) && Units.begin(); + } +#endif }; namespace { diff --git a/test/embedded/test_sht30/sht30_test.cpp b/test/embedded/test_sht30/sht30_test.cpp index 92df44a..75e7e67 100644 --- a/test/embedded/test_sht30/sht30_test.cpp +++ b/test/embedded/test_sht30/sht30_test.cpp @@ -26,6 +26,32 @@ using namespace m5::unit::sht30::command; constexpr size_t STORED_SIZE{4}; +#if defined(USING_HAT_SHT30) +namespace hat { +struct I2cPins { + int sda, scl; +}; + +I2cPins get_hat_pins(const m5::board_t board) +{ + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + return {0, 26}; + case m5::board_t::board_M5StickS3: + return {8, 0}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7}; + default: + return {-1, -1}; + } +} +} // namespace hat +#endif + class TestSHT30 : public I2CComponentTestBase { protected: virtual UnitSHT30* get_instance() override @@ -36,6 +62,17 @@ class TestSHT30 : public I2CComponentTestBase { ptr->component_config(ccfg); return ptr; } +#if defined(USING_HAT_SHT30) + virtual bool begin() override + { + auto board = M5.getBoard(); + const auto pins = hat::get_hat_pins(board); + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, unit->component_config().clock); + return Units.add(*unit, wire) && Units.begin(); + } +#endif }; namespace { diff --git a/unit_env3_env.ini b/unit_env3_env.ini index 8460862..9dc9caf 100644 --- a/unit_env3_env.ini +++ b/unit_env3_env.ini @@ -231,85 +231,236 @@ lib_deps = ${NessoN1.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_qmp6988 +; Hat port +;SHT30 +[env:test_Hat_SHT30_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht30 +build_flags = ${option_release.build_flags} + -D USING_HAT_SHT30 + +[env:test_Hat_SHT30_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht30 +build_flags = ${option_release.build_flags} + -D USING_HAT_SHT30 + +[env:test_Hat_SHT30_StickS3] +extends=StickS3, option_release, arduino_latest +lib_deps = ${StickS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht30 +build_flags = ${StickS3.build_flags} + ${option_release.build_flags} + -D USING_HAT_SHT30 + +[env:test_Hat_SHT30_CoreInk] +extends=CoreInk, option_release, arduino_latest +lib_deps = ${CoreInk.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht30 +build_flags = ${option_release.build_flags} + -D USING_HAT_SHT30 + +[env:test_Hat_SHT30_NessoN1] +extends=NessoN1, option_release, pioarduino_latest +lib_deps = ${NessoN1.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht30 +build_flags = ${option_release.build_flags} + -D USING_HAT_SHT30 + +;QMP6988 +[env:test_Hat_QMP6988_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_qmp6988 +build_flags = ${option_release.build_flags} + -D USING_HAT_QMP6988 + +[env:test_Hat_QMP6988_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_qmp6988 +build_flags = ${option_release.build_flags} + -D USING_HAT_QMP6988 + +[env:test_Hat_QMP6988_StickS3] +extends=StickS3, option_release, arduino_latest +lib_deps = ${StickS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_qmp6988 +build_flags = ${StickS3.build_flags} + ${option_release.build_flags} + -D USING_HAT_QMP6988 + +[env:test_Hat_QMP6988_CoreInk] +extends=CoreInk, option_release, arduino_latest +lib_deps = ${CoreInk.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_qmp6988 +build_flags = ${option_release.build_flags} + -D USING_HAT_QMP6988 + +[env:test_Hat_QMP6988_NessoN1] +extends=NessoN1, option_release, pioarduino_latest +lib_deps = ${NessoN1.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_qmp6988 +build_flags = ${option_release.build_flags} + -D USING_HAT_QMP6988 + + ;Examples ;;Unit ENVIII [env:UnitENVIII_PlotToSerial_Core_Arduino_latest] extends=Core, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Core2_Arduino_latest] extends=Core2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_CoreS3_Arduino_latest] extends=CoreS3, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_StampS3_Arduino_latest] extends=StampS3, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Atom_Arduino_latest] extends=Atom, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_AtomS3_Arduino_latest] extends=AtomS3, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_AtomS3R_Arduino_latest] extends=AtomS3R, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Dial_Arduino_latest] extends=Dial, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_NanoC6_Arduino_latest] extends=NanoC6, option_release, nanoc6_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_StickCPlus_Arduino_latest] extends=StickCPlus, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_StickS3_Arduino_latest] extends=StickS3, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> build_flags = ${StickS3.build_flags} ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Paper_Arduino_latest] extends=Paper, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_CoreInk_Arduino_latest] extends=CoreInk, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Fire_Arduino_latest] extends=Fire, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Cardputer_Arduino_latest] extends=Cardputer, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> build_flags = ${Cardputer.build_flags} ${option_release.build_flags} -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_Tab5_Arduino_latest] extends=Tab5, option_release, pioarduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> build_flags = ${Tab5.build_flags} ${option_release.build_flags} -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> + -D USING_UNIT_ENV3 [env:UnitENVIII_PlotToSerial_NessoN1_Arduino_latest] extends=NessoN1, option_release, pioarduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_ENV3 + +;HatENV3 +[env:HatENVIII_PlotToSerial_StickCPlus_Arduino_latest] +extends=StickCPlus, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_ENV3 + +[env:HatENVIII_PlotToSerial_StickCPlus2_Arduino_latest] +extends=StickCPlus2, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_ENV3 + +[env:HatENVIII_PlotToSerial_StickS3_Arduino_latest] +extends=StickS3, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${StickS3.build_flags} + ${option_release.build_flags} + -D USING_HAT_ENV3 + +[env:HatENVIII_PlotToSerial_CoreInk_Arduino_latest] +extends=CoreInk, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_ENV3 + +[env:HatENVIII_PlotToSerial_NessoN1_Arduino_latest] +extends=NessoN1, option_release, pioarduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitENVIII/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_ENV3 ; This example is for the SHT30 sensor built into the M5Paper [env:BuiltinSHT30_Paper_Paper_Arduino_latest] From afa16daea8ae26ac1c10e92848e86e900d247166 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:56:51 +0900 Subject: [PATCH 13/22] Fixes examples and simplifies ENVIV --- .../PlotToSerial/main/PlotToSerial.cpp | 47 ++++--------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/examples/UnitUnified/UnitENVIV/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/UnitENVIV/PlotToSerial/main/PlotToSerial.cpp index 44fedd5..045126a 100644 --- a/examples/UnitUnified/UnitENVIV/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/UnitENVIV/PlotToSerial/main/PlotToSerial.cpp @@ -12,35 +12,17 @@ #include // For NessoN1 #include -// Using combined unit if defined -#define USING_ENV4 - namespace { auto& lcd = M5.Display; m5::unit::UnitUnified Units; +m5::unit::UnitENV4 unit; +auto& sht40 = unit.sht40; +auto& bmp280 = unit.bmp280; -#if defined(USING_ENV4) -#pragma message "Using combined unit(ENV4)" -m5::unit::UnitENV4 unitENV4; -#else -#pragma message "Using each unit" -m5::unit::UnitSHT40 unitSHT40; -m5::unit::UnitBMP280 unitBMP280; -#endif - -#if defined(USING_ENV4) -auto& sht40 = unitENV4.sht40; -auto& bmp280 = unitENV4.bmp280; -#else -auto& sht40 = unitSHT40; -auto& bmp280 = unitBMP280; -#endif - -float calculate_altitude(const float pressure, const float seaLvhPa = 1013.25f) +float calculate_altitude(const float pressure, const float seaLevelHPa = 1013.25f) { - return 44330.f * (1.0f - pow((pressure / 100.f) / seaLvhPa, 0.1903f)); + return 44330.f * (1.0f - pow((pressure / 100.f) / seaLevelHPa, 0.1903f)); } - } // namespace void setup() @@ -81,31 +63,18 @@ void setup() i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); M5_LOGI("Bus:%d", i2c_bus.has_value()); -#if defined(USING_ENV4) - unit_ready = Units.add(unitENV4, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); -#else - unit_ready = Units.add(unitSHT40, i2c_bus ? i2c_bus.value() : nullptr) && - Units.add(unitBMP280, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); -#endif + unit_ready = Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); } else if (board == m5::board_t::board_M5NanoC6) { // NanoC6: Use M5.Ex_I2C (m5::I2C_Class, not Arduino Wire) M5_LOGI("Using M5.Ex_I2C"); -#if defined(USING_ENV4) - unit_ready = Units.add(unitENV4, M5.Ex_I2C) && Units.begin(); -#else - unit_ready = Units.add(unitSHT40, M5.Ex_I2C) && Units.add(unitBMP280, M5.Ex_I2C) && Units.begin(); -#endif + unit_ready = Units.add(unit, M5.Ex_I2C) && Units.begin(); } else { 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); Wire.end(); Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U); -#if defined(USING_ENV4) - unit_ready = Units.add(unitENV4, Wire) && Units.begin(); -#else - unit_ready = Units.add(unitSHT40, Wire) && Units.add(unitBMP280, Wire) && Units.begin(); -#endif + unit_ready = Units.add(unit, Wire) && Units.begin(); } if (!unit_ready) { M5_LOGE("Failed to begin"); From 3adcbcda80fa2f2ca08a99ed36d6432d70db7a9e Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:57:07 +0900 Subject: [PATCH 14/22] Fixes workflows and adds ENVIII-specific Arduino build check --- .../arduino-env3-esp-v3-build-check.yml | 135 ++++++++++++++++ .../workflows/arduino-env3-m5-build-check.yml | 146 ++++++++++++++++++ .../workflows/arduino-esp-v2-build-check.yml | 33 ++-- .../workflows/arduino-esp-v3-build-check.yml | 33 ++-- .github/workflows/arduino-m5-build-check.yml | 33 ++-- .github/workflows/platformio-build-check.yml | 40 ++++- platformio.ini | 1 + 7 files changed, 356 insertions(+), 65 deletions(-) create mode 100644 .github/workflows/arduino-env3-esp-v3-build-check.yml create mode 100644 .github/workflows/arduino-env3-m5-build-check.yml diff --git a/.github/workflows/arduino-env3-esp-v3-build-check.yml b/.github/workflows/arduino-env3-esp-v3-build-check.yml new file mode 100644 index 0000000..00ec544 --- /dev/null +++ b/.github/workflows/arduino-env3-esp-v3-build-check.yml @@ -0,0 +1,135 @@ +name: Build ENVIII(arduino-esp32:3.x) + +env: + SKETCH_NAMES_FIND_START: ./examples/UnitUnified/UnitENVIII + REQUIRED_LIBRARIES: M5Unified,M5UnitUnified,BME68x Sensor library,bsec2 + +on: + push: + tags-ignore: + - '*.*.*' + - 'v*.*.*' + branches: + - '*' + paths: + - 'src/**' + - 'examples/UnitUnified/UnitENVIII/**.ino' + - 'examples/UnitUnified/UnitENVIII/**.cpp' + - 'examples/UnitUnified/UnitENVIII/**.hpp' + - 'examples/UnitUnified/UnitENVIII/**.h' + - 'examples/UnitUnified/UnitENVIII/**.c' + - '.github/workflows/arduino-env3-esp-v3-build-check.yml' + pull_request: + paths: + - 'src/**' + - 'examples/UnitUnified/UnitENVIII/**.ino' + - 'examples/UnitUnified/UnitENVIII/**.cpp' + - 'examples/UnitUnified/UnitENVIII/**.hpp' + - 'examples/UnitUnified/UnitENVIII/**.h' + - 'examples/UnitUnified/UnitENVIII/**.c' + - '.github/workflows/arduino-env3-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.build-properties }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} + runs-on: ubuntu-latest + timeout-minutes: 12 + + strategy: + fail-fast: false + max-parallel: 20 + matrix: + platform-url: + - https://espressif.github.io/arduino-esp32/package_esp32_index.json + + sketch: + - PlotToSerial + + board: + - arduino_nesso_n1 + - m5stack_atom + - m5stack_atoms3 + - m5stack_capsule + - m5stack_cardputer + - m5stack_core + - m5stack_core2 + - m5stack_coreink + - m5stack_cores3 + - m5stack_dial + - m5stack_dinmeter + - m5stack_fire + - m5stack_nanoc6 + - m5stack_paper + - m5stack_stamp_s3 + - m5stack_stickc_plus + - m5stack_stickc_plus2 + - m5stack_tab5 + + platform-version: + - 3.3.6 + + platform: + - esp32 + + archi: + - esp32 + + build-properties: + - "-DUSING_UNIT_ENV3" + + include: + # HatENV3 + - sketch: PlotToSerial + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform-version: 3.3.6 + platform: esp32 + archi: esp32 + board: m5stack_stickc_plus + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform-version: 3.3.6 + platform: esp32 + archi: esp32 + board: m5stack_stickc_plus2 + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform-version: 3.3.6 + platform: esp32 + archi: esp32 + board: m5stack_coreink + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform-version: 3.3.6 + platform: esp32 + archi: esp32 + board: arduino_nesso_n1 + build-properties: "-DUSING_HAT_ENV3" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Build + - name: Compile examples + uses: ArminJo/arduino-test-compile@master + with: + arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} + platform-url: ${{ matrix.platform-url }} + required-libraries: ${{ env.REQUIRED_LIBRARIES }} + extra-arduino-cli-args: ${{ matrix.cli-args }} + build-properties: ${{ matrix.build-properties }} + sketch-names: ${{ matrix.sketch }}.ino + sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ diff --git a/.github/workflows/arduino-env3-m5-build-check.yml b/.github/workflows/arduino-env3-m5-build-check.yml new file mode 100644 index 0000000..500efc7 --- /dev/null +++ b/.github/workflows/arduino-env3-m5-build-check.yml @@ -0,0 +1,146 @@ +name: Build ENVIII(arduino-m5stack) + +env: + SKETCH_NAMES_FIND_START: ./examples/UnitUnified/UnitENVIII + REQUIRED_LIBRARIES: M5Unified,M5UnitUnified,BME68x Sensor library,bsec2 + +on: + push: + tags-ignore: + - '*.*.*' + - 'v*.*.*' + branches: + - '*' + paths: + - 'src/**' + - 'examples/UnitUnified/UnitENVIII/**.ino' + - 'examples/UnitUnified/UnitENVIII/**.cpp' + - 'examples/UnitUnified/UnitENVIII/**.hpp' + - 'examples/UnitUnified/UnitENVIII/**.h' + - 'examples/UnitUnified/UnitENVIII/**.c' + - '.github/workflows/arduino-env3-m5-build-check.yml' + pull_request: + paths: + - 'src/**' + - 'examples/UnitUnified/UnitENVIII/**.ino' + - 'examples/UnitUnified/UnitENVIII/**.cpp' + - 'examples/UnitUnified/UnitENVIII/**.hpp' + - 'examples/UnitUnified/UnitENVIII/**.h' + - 'examples/UnitUnified/UnitENVIII/**.c' + - '.github/workflows/arduino-env3-m5-build-check.yml' + workflow_dispatch: + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: ${{ matrix.build-properties }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} + runs-on: ubuntu-latest + timeout-minutes: 12 + + strategy: + fail-fast: false + max-parallel: 20 + matrix: + platform-url: + - https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + + sketch: + - PlotToSerial + + board: + - arduino_nesso_n1 + - m5stack_atom + - m5stack_atoms3 + - m5stack_atoms3r + - m5stack_capsule + - m5stack_cardputer + - m5stack_core + - m5stack_core2 + - m5stack_coreink + - m5stack_cores3 + - m5stack_dial + - m5stack_dinmeter + - m5stack_fire + - m5stack_nano_c6 + - m5stack_paper + - m5stack_papers3 + - m5stack_stamp_s3 + - m5stack_stickc + - m5stack_stickc_plus + - m5stack_stickc_plus2 + - m5stack_sticks3 + - m5stack_tab5 + + platform-version: + - 3.2.5 + + platform: + - m5stack + + archi: + - esp32 + + build-properties: + - "-DUSING_UNIT_ENV3" + + include: + # HatENV3 + - sketch: PlotToSerial + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform-version: 3.2.5 + platform: m5stack + archi: esp32 + board: m5stack_stickc_plus + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform-version: 3.2.5 + platform: m5stack + archi: esp32 + board: m5stack_stickc_plus2 + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform-version: 3.2.5 + platform: m5stack + archi: esp32 + board: m5stack_sticks3 + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform-version: 3.2.5 + platform: m5stack + archi: esp32 + board: m5stack_coreink + build-properties: "-DUSING_HAT_ENV3" + - sketch: PlotToSerial + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform-version: 3.2.5 + platform: m5stack + archi: esp32 + board: arduino_nesso_n1 + build-properties: "-DUSING_HAT_ENV3" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Build + - name: Compile examples + uses: ArminJo/arduino-test-compile@master + with: + arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} + platform-url: ${{ matrix.platform-url }} + required-libraries: ${{ env.REQUIRED_LIBRARIES }} + extra-arduino-cli-args: ${{ matrix.cli-args }} + build-properties: ${{ matrix.build-properties }} + sketch-names: ${{ matrix.sketch }}.ino + sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index dfabe01..2b485d1 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -12,27 +12,23 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' - - 'examples/UnitUnified/**.ino' - - 'examples/UnitUnified/**.cpp' - - 'examples/UnitUnified/**.hpp' - - 'examples/UnitUnified/**.h' - - 'examples/UnitUnified/**.c' + - 'src/**' + - 'examples/UnitUnified/Paper/**' + - 'examples/UnitUnified/UnitCO2/**' + - 'examples/UnitUnified/UnitCO2L/**' + - 'examples/UnitUnified/UnitENVIV/**' + - 'examples/UnitUnified/UnitENVPro/**' + - 'examples/UnitUnified/UnitTVOC/**' - '.github/workflows/arduino-esp-v2-build-check.yml' pull_request: paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' - - 'examples/UnitUnified/**.ino' - - 'examples/UnitUnified/**.cpp' - - 'examples/UnitUnified/**.hpp' - - 'examples/UnitUnified/**.h' - - 'examples/UnitUnified/**.c' + - 'src/**' + - 'examples/UnitUnified/Paper/**' + - 'examples/UnitUnified/UnitCO2/**' + - 'examples/UnitUnified/UnitCO2L/**' + - 'examples/UnitUnified/UnitENVIV/**' + - 'examples/UnitUnified/UnitENVPro/**' + - 'examples/UnitUnified/UnitTVOC/**' - '.github/workflows/arduino-esp-v2-build-check.yml' workflow_dispatch: @@ -63,7 +59,6 @@ jobs: unit: - UnitCO2 - UnitCO2L - - UnitENVIII - UnitENVIV - UnitTVOC - UnitENVPro diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 7f295f4..06de514 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -12,27 +12,23 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' - - 'examples/UnitUnified/**.ino' - - 'examples/UnitUnified/**.cpp' - - 'examples/UnitUnified/**.hpp' - - 'examples/UnitUnified/**.h' - - 'examples/UnitUnified/**.c' + - 'src/**' + - 'examples/UnitUnified/Paper/**' + - 'examples/UnitUnified/UnitCO2/**' + - 'examples/UnitUnified/UnitCO2L/**' + - 'examples/UnitUnified/UnitENVIV/**' + - 'examples/UnitUnified/UnitENVPro/**' + - 'examples/UnitUnified/UnitTVOC/**' - '.github/workflows/arduino-esp-v3-build-check.yml' pull_request: paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' - - 'examples/UnitUnified/**.ino' - - 'examples/UnitUnified/**.cpp' - - 'examples/UnitUnified/**.hpp' - - 'examples/UnitUnified/**.h' - - 'examples/UnitUnified/**.c' + - 'src/**' + - 'examples/UnitUnified/Paper/**' + - 'examples/UnitUnified/UnitCO2/**' + - 'examples/UnitUnified/UnitCO2L/**' + - 'examples/UnitUnified/UnitENVIV/**' + - 'examples/UnitUnified/UnitENVPro/**' + - 'examples/UnitUnified/UnitTVOC/**' - '.github/workflows/arduino-esp-v3-build-check.yml' workflow_dispatch: @@ -63,7 +59,6 @@ jobs: unit: - UnitCO2 - UnitCO2L - - UnitENVIII - UnitENVIV - UnitTVOC - UnitENVPro diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index f08eabf..6581eb8 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -12,27 +12,23 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' - - 'examples/UnitUnified/**.ino' - - 'examples/UnitUnified/**.cpp' - - 'examples/UnitUnified/**.hpp' - - 'examples/UnitUnified/**.h' - - 'examples/UnitUnified/**.c' + - 'src/**' + - 'examples/UnitUnified/Paper/**' + - 'examples/UnitUnified/UnitCO2/**' + - 'examples/UnitUnified/UnitCO2L/**' + - 'examples/UnitUnified/UnitENVIV/**' + - 'examples/UnitUnified/UnitENVPro/**' + - 'examples/UnitUnified/UnitTVOC/**' - '.github/workflows/arduino-m5-build-check.yml' pull_request: paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' - - 'examples/UnitUnified/**.ino' - - 'examples/UnitUnified/**.cpp' - - 'examples/UnitUnified/**.hpp' - - 'examples/UnitUnified/**.h' - - 'examples/UnitUnified/**.c' + - 'src/**' + - 'examples/UnitUnified/Paper/**' + - 'examples/UnitUnified/UnitCO2/**' + - 'examples/UnitUnified/UnitCO2L/**' + - 'examples/UnitUnified/UnitENVIV/**' + - 'examples/UnitUnified/UnitENVPro/**' + - 'examples/UnitUnified/UnitTVOC/**' - '.github/workflows/arduino-m5-build-check.yml' workflow_dispatch: @@ -63,7 +59,6 @@ jobs: unit: - UnitCO2 - UnitCO2L - - UnitENVIII - UnitENVIV - UnitTVOC - UnitENVPro diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 688a420..6ef16b3 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -8,10 +8,7 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -19,12 +16,11 @@ on: - 'examples/UnitUnified/**.c' - '.github/workflows/platformio-build-check.yml' - 'platformio.ini' + - 'unit_*_env.ini' + - 'extra_script.py' pull_request: paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - 'examples/UnitUnified/**.hpp' @@ -32,6 +28,8 @@ on: - 'examples/UnitUnified/**.c' - '.github/workflows/platformio-build-check.yml' - 'platformio.ini' + - 'unit_*_env.ini' + - 'extra_script.py' workflow_dispatch: defaults: @@ -99,6 +97,32 @@ jobs: board: Paper framework: Arduino espressif32: latest + # HatENV3 + - example: PlotToSerial + unit: HatENVIII + board: StickCPlus + framework: Arduino + espressif32: latest + - example: PlotToSerial + unit: HatENVIII + board: StickCPlus2 + framework: Arduino + espressif32: latest + - example: PlotToSerial + unit: HatENVIII + board: StickS3 + framework: Arduino + espressif32: latest + - example: PlotToSerial + unit: HatENVIII + board: CoreInk + framework: Arduino + espressif32: latest + - example: PlotToSerial + unit: HatENVIII + board: NessoN1 + framework: Arduino + espressif32: latest steps: diff --git a/platformio.ini b/platformio.ini index 25ced38..0ed8514 100644 --- a/platformio.ini +++ b/platformio.ini @@ -79,6 +79,7 @@ lib_deps = ${env.lib_deps} ; Using ./boards/m5stack-atoms3r.json [AtomS3R] +;include AtomEchoS3R extends = m5base board = m5stack-atoms3r lib_deps = ${env.lib_deps} From 2eeb8562bd117937219b92b10049ae59a8ba1a0b Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 18:57:21 +0900 Subject: [PATCH 15/22] Fixes typos and cosmetic changes --- README.md | 15 +++++++++------ src/M5UnitUnifiedENV.h | 2 +- test/embedded/test_sgp30/sgp30_test.cpp | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c8f9269..f716ac2 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,19 @@ ## Overview -### SKU:U001 & U001-B & U001-C & U090 & U053-B & U053-D & U103 +### SKU:U001 & U001-B & U001-C & U090 & U090-B & U053-B & U053-D & U103 Contains M5Stack-**UNIT ENV & Hat ENV & UNIT BPS & UNIT CO2** series related case programs. -ENV is an environmental sensor with integrated SHT30 and QMP6988 internally to detect temperature, humidity, and atmospheric pressure data. +Unit ENV is an environmental sensor that integrates DHT12 and BMP280 internally to detect temperature, humidity, and atmospheric pressure data. -BPS is a barometer unit, which integrates the Bosch BMP280 pressure sensor to measure atmospheric pressure and estimate the altitude. +Unit ENV II integrates SHT30 and BMP280. Unit ENV III integrates SHT30 and QMP6988. -BPS(QMP6988) Unit is a barometer unit that uses QMP6988 barometric pressure sensor to measure atmospheric pressure and altitude estimation +Unit Mini BPS uses the Bosch BMP280 pressure sensor. Unit Mini BPS v1.1 uses QMP6988 to measure atmospheric pressure and altitude estimation. -CO2 is a photoacoustic Carbon Dioxide (CO2) Unit that will tell you the CO2 PPM (parts-per-million) composition of ambient air. +Hat ENV II and Hat ENV III are Hat form factor versions of ENV II and ENV III for M5StickC series. + +Unit CO2 is a digital air CO2 concentration detection unit, built-in with Sensirion's SCD40 sensor. ## Related Link @@ -44,13 +46,14 @@ 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. -### Supported units +### 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) +- Hat ENVIII (SKU:U053-D) ### SKU:U088 diff --git a/src/M5UnitUnifiedENV.h b/src/M5UnitUnifiedENV.h index 47bd536..f115ad5 100644 --- a/src/M5UnitUnifiedENV.h +++ b/src/M5UnitUnifiedENV.h @@ -5,7 +5,7 @@ */ /*! @file M5UnitUnifiedENV.h - @brief Main header of M5UnitENV using M5UnitUnfied + @brief Main header of M5UnitENV using M5UnitUnified */ #ifndef M5_UNIT_UNIFIED_ENV_H #define M5_UNIT_UNIFIED_ENV_H diff --git a/test/embedded/test_sgp30/sgp30_test.cpp b/test/embedded/test_sgp30/sgp30_test.cpp index 1660617..325904d 100644 --- a/test/embedded/test_sgp30/sgp30_test.cpp +++ b/test/embedded/test_sgp30/sgp30_test.cpp @@ -75,7 +75,7 @@ TEST_F(TestSGP30, FeatureSet) EXPECT_EQ(unit->productVersion(), f.productVersion()); } -TEST_F(TestSGP30, selfTest) +TEST_F(TestSGP30, SelfTest) { SCOPED_TRACE(ustr); @@ -84,7 +84,7 @@ TEST_F(TestSGP30, selfTest) EXPECT_EQ(result, 0xD400); } -TEST_F(TestSGP30, serialNumber) +TEST_F(TestSGP30, SerialNumber) { SCOPED_TRACE(ustr); // Read direct [MSB] SNB_3, SNB_2, CRC, SNB_1, SNB_0, CRC [LSB] @@ -119,7 +119,7 @@ TEST_F(TestSGP30, serialNumber) EXPECT_STREQ(s.c_str(), ssno); } -TEST_F(TestSGP30, generalReset) +TEST_F(TestSGP30, GeneralReset) { SCOPED_TRACE(ustr); From 131a6292f2352f154ac6b95fffd1a79edc2e7554 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 19:02:47 +0900 Subject: [PATCH 16/22] Fixes workflow paths --- .github/workflows/platformio-build-check.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 6ef16b3..b037a7d 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -17,7 +17,6 @@ on: - '.github/workflows/platformio-build-check.yml' - 'platformio.ini' - 'unit_*_env.ini' - - 'extra_script.py' pull_request: paths: - 'src/**' @@ -29,7 +28,6 @@ on: - '.github/workflows/platformio-build-check.yml' - 'platformio.ini' - 'unit_*_env.ini' - - 'extra_script.py' workflow_dispatch: defaults: From 10ea133b0f721c0bd8f8f9cc876416e7f62e158f Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 23:23:29 +0900 Subject: [PATCH 17/22] Add HatYun support --- .../workflows/arduino-esp-v3-build-check.yml | 17 + .github/workflows/arduino-m5-build-check.yml | 24 + .github/workflows/platformio-build-check.yml | 13 + .../HatYunEnvironmentGlow.ino | 12 + .../main/HatYunEnvironmentGlow.cpp | 412 ++++++++++++++++++ hat_yun_env.ini | 184 ++++++++ platformio.ini | 2 +- src/M5UnitUnifiedENV.hpp | 3 + src/unit/unit_HatYun.cpp | 201 +++++++++ src/unit/unit_HatYun.hpp | 184 ++++++++ src/unit/unit_SHT20.cpp | 297 +++++++++++++ src/unit/unit_SHT20.hpp | 237 ++++++++++ test/embedded/embedded_main.cpp | 3 +- test/embedded/test_bmp280/bmp280_test.cpp | 37 ++ test/embedded/test_hatyun/hatyun_test.cpp | 152 +++++++ test/embedded/test_sht20/sht20_test.cpp | 262 +++++++++++ 16 files changed, 2038 insertions(+), 2 deletions(-) create mode 100644 examples/UnitUnified/HatYun/HatYunEnvironmentGlow/HatYunEnvironmentGlow.ino create mode 100644 examples/UnitUnified/HatYun/HatYunEnvironmentGlow/main/HatYunEnvironmentGlow.cpp create mode 100644 hat_yun_env.ini create mode 100644 src/unit/unit_HatYun.cpp create mode 100644 src/unit/unit_HatYun.hpp create mode 100644 src/unit/unit_SHT20.cpp create mode 100644 src/unit/unit_SHT20.hpp create mode 100644 test/embedded/test_hatyun/hatyun_test.cpp create mode 100644 test/embedded/test_sht20/sht20_test.cpp diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 06de514..b0a6473 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -19,6 +19,7 @@ on: - 'examples/UnitUnified/UnitENVIV/**' - 'examples/UnitUnified/UnitENVPro/**' - 'examples/UnitUnified/UnitTVOC/**' + - 'examples/UnitUnified/HatYun/**' - '.github/workflows/arduino-esp-v3-build-check.yml' pull_request: paths: @@ -29,6 +30,7 @@ on: - 'examples/UnitUnified/UnitENVIV/**' - 'examples/UnitUnified/UnitENVPro/**' - 'examples/UnitUnified/UnitTVOC/**' + - 'examples/UnitUnified/HatYun/**' - '.github/workflows/arduino-esp-v3-build-check.yml' workflow_dispatch: @@ -111,6 +113,21 @@ jobs: platform-version: 3.3.6 board: m5stack_paper unit: '' + # HatYun (StickCPlus / StickCPlus2) + - sketch: HatYunEnvironmentGlow + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform: esp32 + archi: esp32 + platform-version: 3.3.6 + board: m5stack_stickc_plus + unit: HatYun + - sketch: HatYunEnvironmentGlow + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform: esp32 + archi: esp32 + platform-version: 3.3.6 + board: m5stack_stickc_plus2 + unit: HatYun steps: - name: Checkout diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index 6581eb8..ec523fd 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -19,6 +19,7 @@ on: - 'examples/UnitUnified/UnitENVIV/**' - 'examples/UnitUnified/UnitENVPro/**' - 'examples/UnitUnified/UnitTVOC/**' + - 'examples/UnitUnified/HatYun/**' - '.github/workflows/arduino-m5-build-check.yml' pull_request: paths: @@ -29,6 +30,7 @@ on: - 'examples/UnitUnified/UnitENVIV/**' - 'examples/UnitUnified/UnitENVPro/**' - 'examples/UnitUnified/UnitTVOC/**' + - 'examples/UnitUnified/HatYun/**' - '.github/workflows/arduino-m5-build-check.yml' workflow_dispatch: @@ -119,6 +121,28 @@ jobs: platform-version: 3.2.5 board: m5stack_paper unit: '' + # HatYun (StickC / StickCPlus / StickCPlus2) + - sketch: HatYunEnvironmentGlow + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform: m5stack + archi: esp32 + platform-version: 3.2.5 + board: m5stack_stickc + unit: HatYun + - sketch: HatYunEnvironmentGlow + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform: m5stack + archi: esp32 + platform-version: 3.2.5 + board: m5stack_stickc_plus + unit: HatYun + - sketch: HatYunEnvironmentGlow + platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json + platform: m5stack + archi: esp32 + platform-version: 3.2.5 + board: m5stack_stickc_plus2 + unit: HatYun steps: - name: Checkout diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index b037a7d..721aefb 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -17,6 +17,7 @@ on: - '.github/workflows/platformio-build-check.yml' - 'platformio.ini' - 'unit_*_env.ini' + - 'hat_*_env.ini' pull_request: paths: - 'src/**' @@ -28,6 +29,7 @@ on: - '.github/workflows/platformio-build-check.yml' - 'platformio.ini' - 'unit_*_env.ini' + - 'hat_*_env.ini' workflow_dispatch: defaults: @@ -121,6 +123,17 @@ jobs: board: NessoN1 framework: Arduino espressif32: latest + # HatYun (StickCPlus / StickCPlus2 only) + - example: HatYunEnvironmentGlow + unit: HatYun + board: StickCPlus + framework: Arduino + espressif32: latest + - example: HatYunEnvironmentGlow + unit: HatYun + board: StickCPlus2 + framework: Arduino + espressif32: latest steps: diff --git a/examples/UnitUnified/HatYun/HatYunEnvironmentGlow/HatYunEnvironmentGlow.ino b/examples/UnitUnified/HatYun/HatYunEnvironmentGlow/HatYunEnvironmentGlow.ino new file mode 100644 index 0000000..b049018 --- /dev/null +++ b/examples/UnitUnified/HatYun/HatYunEnvironmentGlow/HatYunEnvironmentGlow.ino @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + HatYunEnvironmentGlow — Hat Yun exclusive example + Supported boards: M5StickC / M5StickCPlus / M5StickCPlus2 + Required + - M5Unified: https://github.com/m5stack/M5Unified +*/ +#include "main/HatYunEnvironmentGlow.cpp" diff --git a/examples/UnitUnified/HatYun/HatYunEnvironmentGlow/main/HatYunEnvironmentGlow.cpp b/examples/UnitUnified/HatYun/HatYunEnvironmentGlow/main/HatYunEnvironmentGlow.cpp new file mode 100644 index 0000000..001b577 --- /dev/null +++ b/examples/UnitUnified/HatYun/HatYunEnvironmentGlow/main/HatYunEnvironmentGlow.cpp @@ -0,0 +1,412 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + HatYunEnvironmentGlow — Hat Yun exclusive example + Supported boards: M5StickC / M5StickCPlus / M5StickCPlus2 + + LED layout: 14 LEDs, index 0 = bottom-left, counter-clockwise + LED 0-6: temperature (blue=cold -> green -> red=hot) + LED 7-13: humidity (dark=dry -> cyan=humid) + Light sensor auto-dims LEDs in dark environments + BtnA toggles between environment mode and rainbow mode +*/ +#include +#include +#include +#include + +namespace { +auto& lcd = M5.Display; +M5Canvas canvas(&lcd); +m5::unit::UnitUnified Units; +m5::unit::HatYun unit; + +auto& sht20 = unit.sht20; +auto& bmp280 = unit.bmp280; + +// LED modes: 0=glow, 1=rainbow, 2=off +uint8_t led_mode{}; +uint8_t rainbow_offset{}; + +// LED state cache to avoid flicker — only write changed LEDs +constexpr uint8_t NUM_LEDS = m5::unit::hatyun::NUM_LEDS; +uint8_t led_r[NUM_LEDS]{}; +uint8_t led_g[NUM_LEDS]{}; +uint8_t led_b[NUM_LEDS]{}; + +inline float clampf(const float v, const float lo, const float hi) +{ + return (v < lo) ? lo : (v > hi) ? hi : v; +} + +// Write LED array without flicker: set each LED individually, skip unchanged +void apply_leds(const uint8_t* r, const uint8_t* g, const uint8_t* b) +{ + for (uint8_t i = 0; i < NUM_LEDS; ++i) { + if (r[i] != led_r[i] || g[i] != led_g[i] || b[i] != led_b[i]) { + unit.writeLED(i, r[i], g[i], b[i]); + led_r[i] = r[i]; + led_g[i] = g[i]; + led_b[i] = b[i]; + } + } +} + +// HSV to RGB (hue 0-255, sat 255, val = brightness) +void hsv_to_rgb(const uint8_t hue, const uint8_t val, uint8_t& r, uint8_t& g, uint8_t& b) +{ + uint8_t region = hue / 43; + uint8_t frac = (hue - region * 43) * 6; + uint8_t q = (val * (255 - frac)) >> 8; + uint8_t t = (val * frac) >> 8; + switch (region) { + case 0: + r = val; + g = t; + b = 0; + break; + case 1: + r = q; + g = val; + b = 0; + break; + case 2: + r = 0; + g = val; + b = t; + break; + case 3: + r = 0; + g = q; + b = val; + break; + case 4: + r = t; + g = 0; + b = val; + break; + default: + r = val; + g = 0; + b = q; + break; + } +} + +// Temperature to hue: 15C=170(blue), 25C=85(green), 35C=0(red) +uint8_t temp_to_hue(const float temp) +{ + return static_cast(170.f - (clampf(temp, 15.f, 35.f) - 15.f) * (170.f / 20.f)); +} + +// Temperature to RGB565 for display +uint16_t temp_to_color565(const float temp) +{ + uint8_t r, g, b; + hsv_to_rgb(temp_to_hue(temp), 255, r, g, b); + return lcd.color565(r, g, b); +} + +// Arc gauge constants: 135° (7:30) to 405° (4:30) = 270° sweep, bottom open +constexpr float ARC_START = 135.f; +constexpr float ARC_RANGE = 270.f; +constexpr uint16_t COL_BG = 0x2104; // dark gray track +constexpr uint16_t COL_DIM = 0x4208; // dim label + +void draw_arc_gauge(const int32_t cx, const int32_t cy, const int32_t ri, const int32_t ro, const float ratio, + const uint16_t fg, const uint16_t bg = COL_BG) +{ + canvas.fillArc(cx, cy, ri, ro, ARC_START, ARC_START + ARC_RANGE, bg); + if (ratio > 0.001f) { + canvas.fillArc(cx, cy, ri, ro, ARC_START, ARC_START + ARC_RANGE * clampf(ratio, 0.f, 1.f), fg); + } +} + +// Temperature arc with gradient segments +void draw_temp_arc(const int32_t cx, const int32_t cy, const int32_t ri, const int32_t ro, const float ratio) +{ + canvas.fillArc(cx, cy, ri, ro, ARC_START, ARC_START + ARC_RANGE, COL_BG); + if (ratio <= 0.001f) { + return; + } + float end_angle = ARC_START + ARC_RANGE * clampf(ratio, 0.f, 1.f); + float seg = ARC_START; + while (seg < end_angle) { + float seg_end = seg + 10.f; + if (seg_end > end_angle) { + seg_end = end_angle; + } + float seg_temp = 15.f + ((seg - ARC_START) / ARC_RANGE) * 20.f; + uint16_t col = temp_to_color565(seg_temp); + canvas.fillArc(cx, cy, ri, ro, seg, seg_end, col); + seg = seg_end; + } +} + +// Tick marks around arc +void draw_ticks(const int32_t cx, const int32_t cy, const int32_t ro, const uint8_t n, const int32_t len, + const uint16_t col) +{ + for (uint8_t i = 0; i <= n; ++i) { + float rad = (ARC_START + ARC_RANGE * i / n) * 3.14159265f / 180.f; + float cs = cosf(rad); + float sn = sinf(rad); + canvas.drawLine(cx + (int32_t)((ro + 1) * cs), cy + (int32_t)((ro + 1) * sn), + cx + (int32_t)((ro + 1 + len) * cs), cy + (int32_t)((ro + 1 + len) * sn), col); + } +} + +// Needle indicator +void draw_needle(const int32_t cx, const int32_t cy, const int32_t len, const float ratio, const uint16_t col) +{ + float rad = (ARC_START + ARC_RANGE * clampf(ratio, 0.f, 1.f)) * 3.14159265f / 180.f; + int32_t x1 = cx + (int32_t)(len * cosf(rad)); + int32_t y1 = cy + (int32_t)(len * sinf(rad)); + canvas.drawLine(cx, cy, x1, y1, col); + canvas.drawLine(cx + 1, cy, x1 + 1, y1, col); + canvas.fillCircle(cx, cy, 3, col); + canvas.fillCircle(cx, cy, 1, TFT_WHITE); +} + +void draw_display(const float temp, const float humi, const float pres, const float bmp_temp, const uint16_t light) +{ + const int16_t H = canvas.height(); + + canvas.fillScreen(TFT_BLACK); + + char buf[16]; + + // === Left: Temperature hero gauge === + { + constexpr int32_t cx = 68, cy = 68, ro = 52, ri = 42; + + float t_ratio = std::isfinite(temp) ? (clampf(temp, 15.f, 35.f) - 15.f) / 20.f : 0.f; + + draw_temp_arc(cx, cy, ri, ro, t_ratio); + draw_ticks(cx, cy, ro, 4, 4, COL_DIM); + draw_needle(cx, cy, ri - 4, t_ratio, TFT_WHITE); + + canvas.setTextDatum(middle_center); + if (std::isfinite(temp)) { + canvas.setFont(&fonts::Orbitron_Light_24); + canvas.setTextColor(temp_to_color565(temp)); + snprintf(buf, sizeof(buf), "%.1f", temp); + canvas.drawString(buf, cx, cy - 2); + canvas.setFont(&fonts::FreeSans9pt7b); + canvas.setTextColor(0x8410); + canvas.drawString("C", cx + 2, cy + 16); + } + + canvas.setFont(&fonts::FreeSans9pt7b); + canvas.setTextColor(COL_DIM); + canvas.setTextDatum(top_center); + canvas.drawString("TEMP", cx, H - 16); + } + + // Vertical separator + canvas.drawFastVLine(136, 4, H - 8, COL_BG); + + // === Right top: Humidity mini gauge === + { + constexpr int32_t cx = 188, cy = 30, ro = 24, ri = 18; + + float h_ratio = std::isfinite(humi) ? clampf(humi, 0.f, 100.f) / 100.f : 0.f; + draw_arc_gauge(cx, cy, ri, ro, h_ratio, TFT_CYAN); + + canvas.setTextDatum(middle_center); + if (std::isfinite(humi)) { + canvas.setFont(&fonts::Orbitron_Light_24); + canvas.setTextColor(TFT_WHITE); + snprintf(buf, sizeof(buf), "%.0f", humi); + canvas.drawString(buf, cx, cy - 2); + canvas.setFont(&fonts::Font0); + canvas.setTextColor(0x8410); + canvas.drawString("%", cx, cy + 10); + } + canvas.setFont(&fonts::Font0); + canvas.setTextColor(TFT_CYAN); + canvas.setTextDatum(top_center); + canvas.drawString("HUMI", cx, cy + ro + 2); + } + + // === Right bottom: Pressure mini gauge === + { + constexpr int32_t cx = 188, cy = 88, ro = 24, ri = 18; + + float p_ratio = std::isfinite(pres) ? (clampf(pres, 950.f, 1050.f) - 950.f) / 100.f : 0.f; + draw_arc_gauge(cx, cy, ri, ro, p_ratio, TFT_ORANGE); + + canvas.setTextDatum(middle_center); + if (std::isfinite(pres)) { + canvas.setFont(&fonts::Orbitron_Light_24); + canvas.setTextColor(TFT_WHITE); + snprintf(buf, sizeof(buf), "%.0f", pres); + canvas.drawString(buf, cx, cy - 2); + canvas.setFont(&fonts::Font0); + canvas.setTextColor(0x8410); + canvas.drawString("hPa", cx, cy + 10); + } + canvas.setFont(&fonts::Font0); + canvas.setTextColor(TFT_ORANGE); + canvas.setTextDatum(top_center); + canvas.drawString("PRES", cx, cy + ro + 2); + } + + // === Bottom strip: BMP temp | mode | Light === + { + constexpr const char* mode_names[] = {"Glow", "Rainbow", "Off"}; + canvas.setFont(&fonts::Font0); + canvas.setTextColor(0x8410); + + canvas.setTextDatum(bottom_left); + if (std::isfinite(bmp_temp)) { + snprintf(buf, sizeof(buf), "%.1fC", bmp_temp); + canvas.drawString(buf, 140, H - 1); + } + + canvas.setTextDatum(bottom_center); + canvas.setTextColor(led_mode == 0 ? TFT_GREEN : led_mode == 1 ? (uint16_t)TFT_MAGENTA : (uint16_t)0x8410); + canvas.drawString(mode_names[led_mode], 188, H - 1); + + canvas.setTextDatum(bottom_right); + canvas.setTextColor(TFT_YELLOW); + snprintf(buf, sizeof(buf), "%u", light); + canvas.drawString(buf, canvas.width() - 2, H - 1); + } + + canvas.pushSprite(0, 0); +} + +struct HatPins { + int sda, scl; +}; +HatPins get_hat_pins(const m5::board_t board) +{ + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + return {0, 26}; + default: + return {-1, -1}; + } +} + +} // namespace + +void setup() +{ + auto m5cfg = M5.config(); + m5cfg.pmic_button = false; + m5cfg.internal_imu = false; + m5cfg.internal_rtc = false; + M5.begin(m5cfg); + + if (lcd.height() > lcd.width()) { + lcd.setRotation(1); + } + + canvas.setColorDepth(8); + canvas.setPsram(false); + canvas.createSprite(lcd.width(), lcd.height()); + + auto board = M5.getBoard(); + const auto pins = get_hat_pins(board); + if (pins.sda < 0 || pins.scl < 0) { + M5_LOGE("Unsupported board for HatYun"); + lcd.fillScreen(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + Wire.end(); + Wire.begin(pins.sda, pins.scl, 400 * 1000U); + if (!Units.add(unit, Wire) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.fillScreen(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + + lcd.fillScreen(TFT_BLACK); + M5_LOGI("%s", Units.debugInfo().c_str()); +} + +void loop() +{ + M5.update(); + Units.update(); + + // BtnA: cycle glow -> rainbow -> off -> glow... + if (M5.BtnA.wasClicked()) { + led_mode = (led_mode + 1) % 3; + } + + // Brightness from ambient light (0-1000 -> 4-64) + uint16_t ambient = unit.light(); + uint8_t brightness = static_cast(4.f + clampf(ambient, 0.f, 1000.f) / 1000.f * 60.f); + + // Prepare new LED state + uint8_t nr[NUM_LEDS]{}, ng[NUM_LEDS]{}, nb[NUM_LEDS]{}; + + if (led_mode == 0) { + // Glow: LED 0-6 temperature, LED 7-13 humidity + float temp = sht20.temperature(); + float humi = sht20.humidity(); + + if (std::isfinite(temp)) { + uint8_t hue = temp_to_hue(temp); + uint8_t r, g, b; + hsv_to_rgb(hue, brightness, r, g, b); + for (uint8_t i = 0; i < 7; ++i) { + nr[i] = r; + ng[i] = g; + nb[i] = b; + } + } + if (std::isfinite(humi)) { + float h_ratio = clampf(humi, 0.f, 100.f) / 100.f; + uint8_t hb = static_cast(brightness * h_ratio); + if (hb < 1) { + hb = 1; + } + for (uint8_t i = 7; i < NUM_LEDS; ++i) { + nr[i] = 0; + ng[i] = hb; + nb[i] = hb; + } + } + } else if (led_mode == 1) { + // Rainbow animation + for (uint8_t i = 0; i < NUM_LEDS; ++i) { + uint8_t hue = rainbow_offset + (i * 256 / NUM_LEDS); + hsv_to_rgb(hue, brightness, nr[i], ng[i], nb[i]); + } + ++rainbow_offset; + } + // led_mode == 2: all zeros (off) + + // Apply LEDs without flicker (only write changed) + apply_leds(nr, ng, nb); + + // Teleplot output per sensor + if (sht20.updated()) { + M5.Log.printf(">SHT20_Temp:%2.2f\n>SHT20_Humidity:%2.2f\n", sht20.temperature(), sht20.humidity()); + } + if (bmp280.updated()) { + M5.Log.printf(">BMP280_Temp:%2.2f\n>BMP280_Pressure:%.2f\n", bmp280.temperature(), bmp280.pressure() * 0.01f); + } + if (unit.updated()) { + M5.Log.printf(">Light:%u\n", unit.light()); + } + + // LCD update via sprite (flicker-free) + if (sht20.updated() || bmp280.updated() || unit.updated()) { + draw_display(sht20.temperature(), sht20.humidity(), bmp280.pressure() * 0.01f, bmp280.temperature(), + unit.light()); + } +} diff --git a/hat_yun_env.ini b/hat_yun_env.ini new file mode 100644 index 0000000..19cff93 --- /dev/null +++ b/hat_yun_env.ini @@ -0,0 +1,184 @@ +; For Hat Yun +; Note: Hat Yun physically connects only to StickC / StickCPlus / StickCPlus2 + +;UnitTest +; SHT20 (GROVE — all boards, prepared for future standalone SHT20 unit use) +[env:test_SHT20_Core] +extends=Core, option_release, arduino_latest +lib_deps = ${Core.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Core2] +extends=Core2, option_release, arduino_latest +lib_deps = ${Core2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_CoreS3] +extends=CoreS3, option_release, arduino_latest +lib_deps = ${CoreS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Fire] +extends=Fire, option_release, arduino_latest +lib_deps = ${Fire.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_StampS3] +extends=StampS3, option_release, arduino_latest +lib_deps = ${StampS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Dial] +extends=Dial, option_release, arduino_latest +lib_deps = ${Dial.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Atom] +extends=Atom, option_release, arduino_latest +lib_deps = ${Atom.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_AtomS3] +extends=AtomS3, option_release, arduino_latest +lib_deps = ${AtomS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_AtomS3R] +extends=AtomS3R, option_release, arduino_latest +lib_deps = ${AtomS3R.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_NanoC6] +extends=NanoC6, option_release, nanoc6_latest +lib_deps = ${NanoC6.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_StickS3] +extends=StickS3, option_release, arduino_latest +build_flags = ${StickS3.build_flags} + ${option_release.build_flags} +lib_deps = ${StickS3.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Paper] +extends=Paper, option_release, arduino_latest +lib_deps = ${Paper.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_CoreInk] +extends=CoreInk, option_release, arduino_latest +lib_deps = ${CoreInk.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Cardputer] +extends=Cardputer, option_release, arduino_latest +build_flags = ${Cardputer.build_flags} + ${option_release.build_flags} +lib_deps = ${Cardputer.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_Tab5] +extends=Tab5, option_release, pioarduino_latest +build_flags = ${Tab5.build_flags} + ${option_release.build_flags} +lib_deps = ${Tab5.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +[env:test_SHT20_NessoN1] +extends=NessoN1, option_release, pioarduino_latest +lib_deps = ${NessoN1.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 + +; Hat port (StickCPlus / StickCPlus2 only) +;SHT20 +[env:test_Hat_SHT20_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 +build_flags = ${option_release.build_flags} + -D USING_HAT_SHT20 + +[env:test_Hat_SHT20_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_sht20 +build_flags = ${option_release.build_flags} + -D USING_HAT_SHT20 + +;HatYun (LED/Light — STM32 at 0x38) +[env:test_HatYun_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_hatyun +build_flags = ${option_release.build_flags} + -D USING_HAT_YUN + +[env:test_HatYun_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_hatyun +build_flags = ${option_release.build_flags} + -D USING_HAT_YUN + +;BMP280 (Hat port) +[env:test_Hat_BMP280_StickCPlus] +extends=StickCPlus, option_release, arduino_latest +lib_deps = ${StickCPlus.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_bmp280 +build_flags = ${option_release.build_flags} + -D USING_HAT_BMP280 + +[env:test_Hat_BMP280_StickCPlus2] +extends=StickCPlus2, option_release, arduino_latest +lib_deps = ${StickCPlus2.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_bmp280 +build_flags = ${option_release.build_flags} + -D USING_HAT_BMP280 + +;Examples +;HatYun +[env:HatYun_HatYunEnvironmentGlow_StickCPlus_Arduino_latest] +extends=StickCPlus, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/HatYun/HatYunEnvironmentGlow> +build_flags = ${option_release.build_flags} + -D USING_HAT_YUN + +[env:HatYun_HatYunEnvironmentGlow_StickCPlus2_Arduino_latest] +extends=StickCPlus2, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/HatYun/HatYunEnvironmentGlow> +build_flags = ${option_release.build_flags} + -D USING_HAT_YUN diff --git a/platformio.ini b/platformio.ini index 0ed8514..6eea2cb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -3,7 +3,7 @@ ; For UnitTest and examples (Using M5UnitUnified) ;----------------------------------------------------------------------- [platformio] -extra_configs = unit_co2_env.ini, unit_env3_env.ini, unit_env4_env.ini, unit_envpro_env.ini, unit_tvoc_env.ini +extra_configs = unit_co2_env.ini, unit_env3_env.ini, unit_env4_env.ini, unit_envpro_env.ini, unit_tvoc_env.ini, hat_yun_env.ini [env] build_flags =-Wall -Wextra -Wreturn-local-addr -Werror=format -Werror=return-local-addr diff --git a/src/M5UnitUnifiedENV.hpp b/src/M5UnitUnifiedENV.hpp index 7cefe76..92f2184 100644 --- a/src/M5UnitUnifiedENV.hpp +++ b/src/M5UnitUnifiedENV.hpp @@ -32,6 +32,9 @@ #include "unit/unit_SHT40.hpp" #include "unit/unit_BMP280.hpp" #include "unit/unit_ENV4.hpp" +// HatYun +#include "unit/unit_SHT20.hpp" +#include "unit/unit_HatYun.hpp" /*! @namespace m5 diff --git a/src/unit/unit_HatYun.cpp b/src/unit/unit_HatYun.cpp new file mode 100644 index 0000000..5467c4d --- /dev/null +++ b/src/unit/unit_HatYun.cpp @@ -0,0 +1,201 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/*! + @file unit_HatYun.cpp + @brief Hat Yun for M5UnitUnified +*/ +#include "unit_HatYun.hpp" +#include + +namespace m5 { +namespace unit { + +using namespace m5::utility::mmh3; +using namespace m5::unit::types; +using namespace m5::unit::hatyun; +using namespace m5::unit::hatyun::command; + +const char HatYun::name[] = "HatYun"; +const types::uid_t HatYun::uid{"HatYun"_mmh3}; +const types::attr_t HatYun::attr{attribute::AccessI2C}; + +HatYun::HatYun(const uint8_t addr) : Component(addr), _data{new m5::container::CircularBuffer(1)} +{ + auto cfg = component_config(); + cfg.max_children = 2; + component_config(cfg); + _valid = add(sht20, 0) && add(bmp280, 1); +} + +bool HatYun::begin() +{ + if (!_valid) { + return false; + } + + auto ssize = stored_size(); + assert(ssize && "stored_size must be greater than zero"); + if (ssize != _data->capacity()) { + _data.reset(new m5::container::CircularBuffer(ssize)); + if (!_data) { + M5_LIB_LOGE("Failed to allocate"); + return false; + } + } + + // Verify STM32 communication by reading light sensor + uint16_t val{}; + if (!readLight(val)) { + M5_LIB_LOGE("Failed to read light sensor"); + return false; + } + + // Turn off all LEDs on init + writeAllLED(0, 0, 0); + + return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.periodic_interval) : true; +} + +void HatYun::update(const bool force) +{ + _updated = false; + if (inPeriodic()) { + elapsed_time_t at{m5::utility::millis()}; + if (force || !_latest || at >= _latest + _interval) { + hatyun::Data d{}; + _updated = read_measurement(d); + if (_updated) { + _latest = at; + _data->push_back(d); + } + } + } +} + +bool HatYun::start_periodic_measurement(const uint32_t interval) +{ + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + _interval = interval < 10 ? 10 : interval; + _latest = 0; + _periodic = true; + return true; +} + +bool HatYun::stop_periodic_measurement() +{ + _periodic = false; + _latest = 0; + return true; +} + +bool HatYun::read_measurement(hatyun::Data& d) +{ + return readLight(d.light); +} + +bool HatYun::writeLED(const uint8_t num, const uint8_t r, const uint8_t g, const uint8_t b) +{ + if (num >= NUM_LEDS) { + return false; + } + uint8_t buf[5] = {LED_REG, num, r, g, b}; + return writeWithTransaction(buf, sizeof(buf)) == m5::hal::error::error_t::OK; +} + +bool HatYun::writeAllLED(const uint8_t r, const uint8_t g, const uint8_t b) +{ + bool ok = true; + for (uint8_t i = 0; i < NUM_LEDS; ++i) { + if (!writeLED(i, r, g, b)) { + ok = false; + } + } + return ok; +} + +bool HatYun::writeRainbow(const uint8_t offset, const uint8_t brightness) +{ + bool ok = true; + for (uint8_t i = 0; i < NUM_LEDS; ++i) { + uint8_t hue = offset + (i * 256 / NUM_LEDS); // Intentional uint8_t wrap-around for hue cycling + uint8_t region = hue / 43; + uint8_t frac = (hue - region * 43) * 6; + uint8_t q = (brightness * (255 - frac)) >> 8; + uint8_t t = (brightness * frac) >> 8; + uint8_t r, g, b; + switch (region) { + case 0: + r = brightness; + g = t; + b = 0; + break; + case 1: + r = q; + g = brightness; + b = 0; + break; + case 2: + r = 0; + g = brightness; + b = t; + break; + case 3: + r = 0; + g = q; + b = brightness; + break; + case 4: + r = t; + g = 0; + b = brightness; + break; + default: + r = brightness; + g = 0; + b = q; + break; + } + if (!writeLED(i, r, g, b)) { + ok = false; + } + } + return ok; +} + +bool HatYun::readLight(uint16_t& value) +{ + uint8_t cmd = LIGHT_REG; + if (writeWithTransaction(&cmd, 1) != m5::hal::error::error_t::OK) { + return false; + } + uint8_t buf[2]{}; + if (readWithTransaction(buf, 2) != m5::hal::error::error_t::OK) { + return false; + } + value = (static_cast(buf[1]) << 8) | buf[0]; + return true; +} + +std::shared_ptr HatYun::ensure_adapter(const uint8_t ch) +{ + if (ch >= 2) { + M5_LIB_LOGE("Invalid channel %u", ch); + return std::make_shared(); + } + auto unit = child(ch); + if (!unit) { + M5_LIB_LOGE("Not exists unit %u", ch); + return std::make_shared(); + } + auto ad = asAdapter(Adapter::Type::I2C); + return ad ? std::shared_ptr(ad->duplicate(unit->address())) : std::make_shared(); +} + +} // namespace unit +} // namespace m5 diff --git a/src/unit/unit_HatYun.hpp b/src/unit/unit_HatYun.hpp new file mode 100644 index 0000000..9197a23 --- /dev/null +++ b/src/unit/unit_HatYun.hpp @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/*! + @file unit_HatYun.hpp + @brief Hat Yun for M5UnitUnified +*/ +#ifndef M5_UNIT_ENV_UNIT_HATYUN_HPP +#define M5_UNIT_ENV_UNIT_HATYUN_HPP + +#include +#include +#include "unit_SHT20.hpp" +#include "unit_BMP280.hpp" + +namespace m5 { +namespace unit { + +/*! + @namespace hatyun + @brief For HatYun + */ +namespace hatyun { +constexpr uint8_t NUM_LEDS{14}; //!< Number of RGB LEDs on Hat Yun + +/*! + @struct Data + @brief Light sensor measurement data + */ +struct Data { + uint16_t light{}; //!< Raw light sensor value +}; + +} // namespace hatyun + +/*! + @class HatYun + @brief Hat Yun is an environmental sensor hat that integrates SHT20, BMP280, and STM32 LED/Light controller + @details Holds SHT20 (temp/humidity) and BMP280 (pressure) as child components. + The STM32-based LED/light controller (I2C 0x38) is accessed directly by this class. + Supported boards: M5StickC / M5StickCPlus / M5StickCPlus2 + */ +class HatYun : public Component, public PeriodicMeasurementAdapter { + M5_UNIT_COMPONENT_HPP_BUILDER(HatYun, 0x38); + +public: + /*! + @struct config_t + @brief Settings for begin + */ + struct config_t { + //! Start periodic measurement on begin? + bool start_periodic{true}; + //! Periodic measurement interval (ms) + uint32_t periodic_interval{1000}; + }; + + UnitSHT20 sht20; //!< @brief SHT20 instance (temperature/humidity) + UnitBMP280 bmp280; //!< @brief BMP280 instance (pressure) + + explicit HatYun(const uint8_t addr = DEFAULT_ADDRESS); + virtual ~HatYun() + { + } + + virtual bool begin() override; + virtual void update(const bool force = false) override; + + ///@name Settings for begin + ///@{ + /*! @brief Gets the configuration */ + inline config_t config() const + { + return _cfg; + } + //! @brief Set the configuration + inline void config(const config_t& cfg) + { + _cfg = cfg; + } + ///@} + + ///@name Measurement data by periodic + ///@{ + //! @brief Oldest light sensor value + inline uint16_t light() const + { + return !empty() ? oldest().light : 0; + } + ///@} + + ///@name Periodic measurement (light sensor polling) + ///@{ + /*! + @brief Start periodic light sensor polling + @param interval Polling interval in ms (default 1000) + @return True if successful + */ + inline bool startPeriodicMeasurement(const uint32_t interval = 1000) + { + return PeriodicMeasurementAdapter::startPeriodicMeasurement(interval); + } + /*! + @brief Stop periodic light sensor polling + @return True if successful + */ + inline bool stopPeriodicMeasurement() + { + return PeriodicMeasurementAdapter::stopPeriodicMeasurement(); + } + ///@} + + ///@note LED heat affects SHT20/BMP280 temperature readings due to the compact Hat form factor. + /// Consider lowering brightness or turning off LEDs when accurate temperature measurement is required. + ///@name LED control + ///@{ + /*! + @brief Write a single LED color + @param num LED index (0-13) + @param r Red (0-255) + @param g Green (0-255) + @param b Blue (0-255) + @return True if successful + */ + bool writeLED(const uint8_t num, const uint8_t r, const uint8_t g, const uint8_t b); + /*! + @brief Write all LEDs to the same color + @param r Red (0-255) + @param g Green (0-255) + @param b Blue (0-255) + @return True if successful + */ + bool writeAllLED(const uint8_t r, const uint8_t g, const uint8_t b); + /*! + @brief Write rainbow pattern across all LEDs + @param offset Hue offset (0-255) for animation + @param brightness Brightness (0-255) + @return True if successful + */ + bool writeRainbow(const uint8_t offset = 0, const uint8_t brightness = 255); + ///@} + + ///@name Light sensor + ///@{ + /*! + @brief Read light sensor value + @param[out] value Light sensor value (16-bit) + @return True if successful + */ + bool readLight(uint16_t& value); + ///@} + +protected: + bool start_periodic_measurement(const uint32_t interval); + bool stop_periodic_measurement(); + bool read_measurement(hatyun::Data& d); + + M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(HatYun, hatyun::Data); + + virtual std::shared_ptr ensure_adapter(const uint8_t ch); + +protected: + std::unique_ptr> _data{}; + config_t _cfg{}; + +private: + bool _valid{}; // Did the constructor correctly add the child unit? + Component* _children[2]{&sht20, &bmp280}; +}; + +///@cond +namespace hatyun { +namespace command { +constexpr uint8_t LIGHT_REG{0x00}; +constexpr uint8_t LED_REG{0x01}; +} // namespace command +} // namespace hatyun +///@endcond + +} // namespace unit +} // namespace m5 +#endif diff --git a/src/unit/unit_SHT20.cpp b/src/unit/unit_SHT20.cpp new file mode 100644 index 0000000..8b275f1 --- /dev/null +++ b/src/unit/unit_SHT20.cpp @@ -0,0 +1,297 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/*! + @file unit_SHT20.cpp + @brief SHT20 Unit for M5UnitUnified + */ +#include "unit_SHT20.hpp" +#include +#include + +using namespace m5::utility::mmh3; +using namespace m5::unit::types; +using namespace m5::unit::sht20; +using namespace m5::unit::sht20::command; + +namespace { + +// SHT20 CRC-8: polynomial 0x31, init 0x00 (differs from SHT30/SHT40 which use init 0xFF) +m5::utility::CRC8 crc_sht20(0x00, 0x31, false, false, 0x00); + +// Measurement duration max (ms) by resolution — Table 7 +// Index: 0=RH12_T14, 1=RH8_T12, 2=RH10_T13, 3=RH11_T11 +constexpr elapsed_time_t temp_duration[] = { + 85, // 14-bit + 22, // 12-bit + 43, // 13-bit + 11, // 11-bit +}; +constexpr elapsed_time_t humd_duration[] = { + 29, // 12-bit + 4, // 8-bit + 9, // 10-bit + 15, // 11-bit +}; + +uint8_t resolution_index(Resolution res) +{ + switch (res) { + case Resolution::RH12_T14: + return 0; + case Resolution::RH8_T12: + return 1; + case Resolution::RH10_T13: + return 2; + case Resolution::RH11_T11: + return 3; + default: + return 0; + } +} + +} // namespace + +namespace m5 { +namespace unit { +namespace sht20 { + +float Data::celsius() const +{ + // Temperature: -46.85 + 175.72 * S_T / 2^16 + // raw[0..2] = MSB, LSB, CRC (status bits in LSB[1:0] must be masked) + uint16_t raw_val = (static_cast(raw[0]) << 8) | (raw[1] & 0xFC); + return -46.85f + 175.72f * raw_val / 65536.f; +} + +float Data::fahrenheit() const +{ + return celsius() * 9.0f / 5.0f + 32.f; +} + +float Data::humidity() const +{ + // Humidity: -6 + 125 * S_RH / 2^16 + // raw[3..5] = MSB, LSB, CRC (status bits in LSB[1:0] must be masked) + uint16_t raw_val = (static_cast(raw[3]) << 8) | (raw[4] & 0xFC); + return -6.f + 125.f * raw_val / 65536.f; +} + +} // namespace sht20 + +const char UnitSHT20::name[] = "UnitSHT20"; +const types::uid_t UnitSHT20::uid{"UnitSHT20"_mmh3}; +const types::attr_t UnitSHT20::attr{attribute::AccessI2C}; + +bool UnitSHT20::begin() +{ + auto ssize = stored_size(); + assert(ssize && "stored_size must be greater than zero"); + if (ssize != _data->capacity()) { + _data.reset(new m5::container::CircularBuffer(ssize)); + if (!_data) { + M5_LIB_LOGE("Failed to allocate"); + return false; + } + } + + if (!softReset()) { + M5_LIB_LOGE("Failed to reset"); + return false; + } + + // Verify communication by reading user register + uint8_t reg{}; + if (!read_user_register(reg)) { + M5_LIB_LOGE("Failed to read user register"); + return false; + } + + // Soft reset does NOT clear heater bit (datasheet: "except heater bit") + auto r = _cfg.start_heater ? startHeater() : stopHeater(); + if (!r) { + M5_LIB_LOGE("Failed to %s heater", _cfg.start_heater ? "start" : "stop"); + return false; + } + + if (!writeResolution(_cfg.resolution)) { + M5_LIB_LOGE("Failed to set resolution"); + return false; + } + + return _cfg.start_periodic ? startPeriodicMeasurement() : true; +} + +void UnitSHT20::update(const bool force) +{ + _updated = false; + if (inPeriodic()) { + elapsed_time_t at{m5::utility::millis()}; + if (force || !_latest || at >= _latest + _interval) { + sht20::Data d{}; + _updated = read_measurement(d); + if (_updated) { + _latest = at; + _data->push_back(d); + } + } + } +} + +bool UnitSHT20::measureSingleshot(sht20::Data& d) +{ + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + return read_measurement(d); +} + +bool UnitSHT20::start_periodic_measurement() +{ + if (inPeriodic()) { + M5_LIB_LOGD("Periodic measurements are running"); + return false; + } + + uint8_t idx = resolution_index(_cfg.resolution); + elapsed_time_t minimum = temp_duration[idx] + humd_duration[idx] + 10; // margin + _interval = m5::stl::clamp(static_cast(_cfg.periodic_interval), minimum, + static_cast(60000)); + _periodic = true; + return true; +} + +bool UnitSHT20::stop_periodic_measurement() +{ + _periodic = false; + _latest = 0; + return true; +} + +bool UnitSHT20::read_measurement(sht20::Data& d) +{ + uint16_t temp_raw{}, humd_raw{}; + + // Trigger temperature measurement (no hold master) + if (!trigger_measurement(TRIGGER_TEMP_NOHOLD, temp_raw)) { + return false; + } + d.raw[0] = static_cast(temp_raw >> 8); + d.raw[1] = static_cast(temp_raw & 0xFF); + d.raw[2] = 0; // CRC already verified in trigger_measurement + + // Trigger humidity measurement (no hold master) + if (!trigger_measurement(TRIGGER_HUMD_NOHOLD, humd_raw)) { + return false; + } + d.raw[3] = static_cast(humd_raw >> 8); + d.raw[4] = static_cast(humd_raw & 0xFF); + d.raw[5] = 0; // CRC already verified in trigger_measurement + + return true; +} + +bool UnitSHT20::trigger_measurement(const uint8_t cmd, uint16_t& raw) +{ + // Send command + if (writeWithTransaction(&cmd, 1) != m5::hal::error::error_t::OK) { + return false; + } + + // Determine wait time based on command and resolution + uint8_t idx = resolution_index(_cfg.resolution); + elapsed_time_t delay = + (cmd == TRIGGER_TEMP_NOHOLD || cmd == TRIGGER_TEMP_HOLD) ? temp_duration[idx] : humd_duration[idx]; + + m5::utility::delay(delay); + + // Read 3 bytes: MSB, LSB, CRC + std::array buf{}; + if (readWithTransaction(buf.data(), buf.size()) != m5::hal::error::error_t::OK) { + return false; + } + + // Verify CRC + if (crc_sht20.range(buf.data(), 2) != buf[2]) { + M5_LIB_LOGE("CRC mismatch"); + return false; + } + + raw = (static_cast(buf[0]) << 8) | buf[1]; + return true; +} + +bool UnitSHT20::readResolution(sht20::Resolution& res) +{ + uint8_t reg{}; + if (!read_user_register(reg)) { + return false; + } + res = static_cast(reg & REG_RESOLUTION_MASK); + return true; +} + +bool UnitSHT20::writeResolution(const sht20::Resolution res) +{ + uint8_t reg{}; + if (!read_user_register(reg)) { + return false; + } + reg = (reg & ~REG_RESOLUTION_MASK) | static_cast(res); + return write_user_register(reg); +} + +bool UnitSHT20::startHeater() +{ + uint8_t reg{}; + if (!read_user_register(reg)) { + return false; + } + reg |= REG_HEATER_BIT; + return write_user_register(reg); +} + +bool UnitSHT20::stopHeater() +{ + uint8_t reg{}; + if (!read_user_register(reg)) { + return false; + } + reg &= ~REG_HEATER_BIT; + return write_user_register(reg); +} + +bool UnitSHT20::softReset() +{ + uint8_t cmd = SOFT_RESET; + if (writeWithTransaction(&cmd, 1) != m5::hal::error::error_t::OK) { + return false; + } + // Soft reset takes max 15ms + m5::utility::delay(15); + return true; +} + +bool UnitSHT20::read_user_register(uint8_t& reg) +{ + uint8_t cmd = READ_USER_REG; + if (writeWithTransaction(&cmd, 1) != m5::hal::error::error_t::OK) { + return false; + } + if (readWithTransaction(®, 1) != m5::hal::error::error_t::OK) { + return false; + } + return true; +} + +bool UnitSHT20::write_user_register(const uint8_t reg) +{ + uint8_t buf[2] = {WRITE_USER_REG, reg}; + return writeWithTransaction(buf, 2) == m5::hal::error::error_t::OK; +} + +} // namespace unit +} // namespace m5 diff --git a/src/unit/unit_SHT20.hpp b/src/unit/unit_SHT20.hpp new file mode 100644 index 0000000..46184b1 --- /dev/null +++ b/src/unit/unit_SHT20.hpp @@ -0,0 +1,237 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/*! + @file unit_SHT20.hpp + @brief SHT20 Unit for M5UnitUnified + */ +#ifndef M5_UNIT_ENV_UNIT_SHT20_HPP +#define M5_UNIT_ENV_UNIT_SHT20_HPP + +#include +#include +#include // NaN + +namespace m5 { +namespace unit { + +/*! + @namespace sht20 + @brief For SHT20 + */ +namespace sht20 { + +/*! + @enum Resolution + @brief Measurement resolution (humidity bits / temperature bits) + */ +enum class Resolution : uint8_t { + RH12_T14 = 0x00, //!< RH 12 bit, T 14 bit (default) + RH8_T12 = 0x01, //!< RH 8 bit, T 12 bit + RH10_T13 = 0x80, //!< RH 10 bit, T 13 bit + RH11_T11 = 0x81, //!< RH 11 bit, T 11 bit +}; + +/*! + @struct Data + @brief Measurement data group + */ +struct Data { + std::array raw{}; //!< RAW data (3 bytes temperature + 3 bytes humidity) + //! temperature (Celsius) + inline float temperature() const + { + return celsius(); + } + float celsius() const; //!< temperature (Celsius) + float fahrenheit() const; //!< temperature (Fahrenheit) + float humidity() const; //!< humidity (RH) +}; + +} // namespace sht20 + +/*! + @class UnitSHT20 + @brief Temperature and humidity sensor unit (SHT20) + @details Sensirion SHT20 — I2C address 0x40, 1-byte commands, CRC-8 +*/ +class UnitSHT20 : public Component, public PeriodicMeasurementAdapter { + M5_UNIT_COMPONENT_HPP_BUILDER(UnitSHT20, 0x40); + +public: + /*! + @struct config_t + @brief Settings for begin + */ + struct config_t { + //! Start periodic measurement on begin? + bool start_periodic{true}; + //! Measurement resolution + sht20::Resolution resolution{sht20::Resolution::RH12_T14}; + //! Start heater on begin? + bool start_heater{false}; + //! Periodic measurement interval (ms). Clamped to resolution minimum + uint32_t periodic_interval{1000}; + }; + + explicit UnitSHT20(const uint8_t addr = DEFAULT_ADDRESS) + : Component(addr), _data{new m5::container::CircularBuffer(1)} + { + auto ccfg = component_config(); + ccfg.clock = 400 * 1000U; + component_config(ccfg); + } + virtual ~UnitSHT20() + { + } + + virtual bool begin() override; + virtual void update(const bool force = false) override; + + ///@name Settings for begin + ///@{ + /*! @brief Gets the configuration */ + inline config_t config() const + { + return _cfg; + } + //! @brief Set the configuration + inline void config(const config_t& cfg) + { + _cfg = cfg; + } + ///@} + + ///@name Measurement data by periodic + ///@{ + //! @brief Oldest measured temperature (Celsius) + inline float temperature() const + { + return !empty() ? oldest().temperature() : std::numeric_limits::quiet_NaN(); + } + //! @brief Oldest measured temperature (Celsius) + inline float celsius() const + { + return !empty() ? oldest().celsius() : std::numeric_limits::quiet_NaN(); + } + //! @brief Oldest measured temperature (Fahrenheit) + inline float fahrenheit() const + { + return !empty() ? oldest().fahrenheit() : std::numeric_limits::quiet_NaN(); + } + //! @brief Oldest measured humidity (RH) + inline float humidity() const + { + return !empty() ? oldest().humidity() : std::numeric_limits::quiet_NaN(); + } + ///@} + + ///@name Periodic measurement + ///@{ + /*! + @brief Start periodic measurement + @return True if successful + */ + inline bool startPeriodicMeasurement() + { + return PeriodicMeasurementAdapter::startPeriodicMeasurement(); + } + /*! + @brief Stop periodic measurement + @return True if successful + */ + inline bool stopPeriodicMeasurement() + { + return PeriodicMeasurementAdapter::stopPeriodicMeasurement(); + } + ///@} + + ///@name Single shot measurement + ///@{ + /*! + @brief Measurement single shot + @param[out] d Measured data + @return True if successful + @warning During periodic detection runs, an error is returned + */ + bool measureSingleshot(sht20::Data& d); + ///@} + + ///@name Resolution + ///@{ + /*! + @brief Read measurement resolution + @param[out] res Resolution + @return True if successful + */ + bool readResolution(sht20::Resolution& res); + /*! + @brief Write measurement resolution + @param res Resolution + @return True if successful + */ + bool writeResolution(const sht20::Resolution res); + ///@} + + ///@name Heater + ///@{ + /*! + @brief Enable on-chip heater + @return True if successful + */ + bool startHeater(); + /*! + @brief Disable on-chip heater + @return True if successful + */ + bool stopHeater(); + ///@} + + ///@name Reset + ///@{ + /*! + @brief Soft reset + @return True if successful + */ + bool softReset(); + ///@} + +protected: + bool start_periodic_measurement(); + bool stop_periodic_measurement(); + bool read_measurement(sht20::Data& d); + + M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSHT20, sht20::Data); + +protected: + std::unique_ptr> _data{}; + config_t _cfg{}; + +private: + bool read_user_register(uint8_t& reg); + bool write_user_register(const uint8_t reg); + bool trigger_measurement(const uint8_t cmd, uint16_t& raw); +}; + +///@cond +namespace sht20 { +namespace command { +constexpr uint8_t TRIGGER_TEMP_HOLD{0xE3}; +constexpr uint8_t TRIGGER_HUMD_HOLD{0xE5}; +constexpr uint8_t TRIGGER_TEMP_NOHOLD{0xF3}; +constexpr uint8_t TRIGGER_HUMD_NOHOLD{0xF5}; +constexpr uint8_t WRITE_USER_REG{0xE6}; +constexpr uint8_t READ_USER_REG{0xE7}; +constexpr uint8_t SOFT_RESET{0xFE}; + +constexpr uint8_t REG_RESOLUTION_MASK{0x81}; +constexpr uint8_t REG_HEATER_BIT{0x04}; +} // namespace command +} // namespace sht20 +///@endcond + +} // namespace unit +} // namespace m5 +#endif diff --git a/test/embedded/embedded_main.cpp b/test/embedded/embedded_main.cpp index 3b6c3d4..b26c88d 100644 --- a/test/embedded/embedded_main.cpp +++ b/test/embedded/embedded_main.cpp @@ -32,7 +32,8 @@ void setup() { delay(1500); -#if defined(USING_HAT_SHT30) || defined(USING_HAT_QMP6988) +#if defined(USING_HAT_SHT30) || defined(USING_HAT_QMP6988) || defined(USING_HAT_SHT20) || defined(USING_HAT_BMP280) \ + || defined(USING_HAT_YUN) auto m5cfg = M5.config(); m5cfg.pmic_button = false; // Disable BtnPWR m5cfg.internal_imu = false; // Disable internal IMU diff --git a/test/embedded/test_bmp280/bmp280_test.cpp b/test/embedded/test_bmp280/bmp280_test.cpp index f1c0b60..dfacf47 100644 --- a/test/embedded/test_bmp280/bmp280_test.cpp +++ b/test/embedded/test_bmp280/bmp280_test.cpp @@ -25,6 +25,32 @@ using m5::unit::types::elapsed_time_t; constexpr uint32_t STORED_SIZE{8}; +#if defined(USING_HAT_BMP280) +namespace hat { +struct I2cPins { + int sda, scl; +}; + +I2cPins get_hat_pins(const m5::board_t board) +{ + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + return {0, 26}; + case m5::board_t::board_M5StickS3: + return {8, 0}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7}; + default: + return {-1, -1}; + } +} +} // namespace hat +#endif + class TestBMP280 : public I2CComponentTestBase { protected: virtual UnitBMP280* get_instance() override @@ -35,6 +61,17 @@ class TestBMP280 : public I2CComponentTestBase { ptr->component_config(ccfg); return ptr; } +#if defined(USING_HAT_BMP280) + virtual bool begin() override + { + auto board = M5.getBoard(); + const auto pins = hat::get_hat_pins(board); + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, unit->component_config().clock); + return Units.add(*unit, wire) && Units.begin(); + } +#endif void print_ctrl_measurement(const char* msg = "") { diff --git a/test/embedded/test_hatyun/hatyun_test.cpp b/test/embedded/test_hatyun/hatyun_test.cpp new file mode 100644 index 0000000..c5eda4c --- /dev/null +++ b/test/embedded/test_hatyun/hatyun_test.cpp @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + UnitTest for HatYun (STM32 LED/Light controller at 0x38) + Requires physical Hat Yun connected to StickC / StickCPlus / StickCPlus2 +*/ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace m5::unit::googletest; +using namespace m5::unit; +using namespace m5::unit::hatyun; +using namespace m5::unit::hatyun::command; + +constexpr size_t STORED_SIZE{4}; + +class TestHatYun : public I2CComponentTestBase { +protected: + virtual HatYun* get_instance() override + { + auto ptr = new m5::unit::HatYun(); + auto ccfg = ptr->component_config(); + ccfg.stored_size = STORED_SIZE; + ptr->component_config(ccfg); + return ptr; + } + virtual bool begin() override + { + auto board = M5.getBoard(); + int sda = -1, scl = -1; + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + sda = 0; + scl = 26; + break; + default: + break; + } + if (sda < 0 || scl < 0) { + return false; + } + Wire.end(); + Wire.begin(sda, scl, 400 * 1000U); + return Units.add(*unit, Wire) && Units.begin(); + } +}; + +TEST_F(TestHatYun, ReadLight) +{ + SCOPED_TRACE(ustr); + + uint16_t value{}; + EXPECT_TRUE(unit->readLight(value)); + M5_LOGI("Light: %u", value); +} + +TEST_F(TestHatYun, WriteLED) +{ + SCOPED_TRACE(ustr); + + // Set each LED individually + for (uint8_t i = 0; i < NUM_LEDS; ++i) { + EXPECT_TRUE(unit->writeLED(i, 32, 0, 0)); // dim red + } + m5::utility::delay(200); + + // All green + EXPECT_TRUE(unit->writeAllLED(0, 32, 0)); + m5::utility::delay(200); + + // All blue + EXPECT_TRUE(unit->writeAllLED(0, 0, 32)); + m5::utility::delay(200); + + // Turn off + EXPECT_TRUE(unit->writeAllLED(0, 0, 0)); +} + +TEST_F(TestHatYun, WriteRainbow) +{ + SCOPED_TRACE(ustr); + + for (uint8_t offset = 0; offset < 64; offset += 8) { + EXPECT_TRUE(unit->writeRainbow(offset, 32)); + m5::utility::delay(50); + } + + EXPECT_TRUE(unit->writeAllLED(0, 0, 0)); +} + +TEST_F(TestHatYun, Periodic) +{ + SCOPED_TRACE(ustr); + + auto ad = unit->asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + // Start with 100ms interval for testing + EXPECT_TRUE(unit->startPeriodicMeasurement(100)); + EXPECT_TRUE(unit->inPeriodic()); + + M5_LOGI("Periodic: interval:%lu ms", (unsigned long)unit->interval()); + + { + uint32_t timeout = is_bus ? std::max(unit->interval(), 500) * (STORED_SIZE + 1) * 4 + : unit->interval() * (STORED_SIZE + 1); + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + EXPECT_LE(r.median(), r.expected_interval + (is_bus ? 5U : 1U)); + } + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + EXPECT_EQ(unit->available(), STORED_SIZE); + EXPECT_FALSE(unit->empty()); + EXPECT_TRUE(unit->full()); + + // Verify light values are readable + uint32_t cnt{2}; + while (cnt-- && unit->available()) { + M5_LOGI("Light: %u", unit->oldest().light); + EXPECT_FALSE(unit->empty()); + unit->discard(); + } + EXPECT_EQ(unit->available(), STORED_SIZE - 2); + + unit->flush(); + EXPECT_EQ(unit->available(), 0); + EXPECT_TRUE(unit->empty()); + + // Verify restart works + EXPECT_TRUE(unit->startPeriodicMeasurement()); + EXPECT_TRUE(unit->inPeriodic()); + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); +} diff --git a/test/embedded/test_sht20/sht20_test.cpp b/test/embedded/test_sht20/sht20_test.cpp new file mode 100644 index 0000000..2c76621 --- /dev/null +++ b/test/embedded/test_sht20/sht20_test.cpp @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + UnitTest for UnitSHT20 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace m5::unit::googletest; +using namespace m5::unit; +using namespace m5::unit::sht20; +using namespace m5::unit::sht20::command; + +constexpr size_t STORED_SIZE{4}; + +#if defined(USING_HAT_SHT20) +namespace hat { +struct I2cPins { + int sda, scl; +}; + +I2cPins get_hat_pins(const m5::board_t board) +{ + switch (board) { + case m5::board_t::board_M5StickC: + case m5::board_t::board_M5StickCPlus: + case m5::board_t::board_M5StickCPlus2: + return {0, 26}; + case m5::board_t::board_M5StickS3: + return {8, 0}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7}; + default: + return {-1, -1}; + } +} +} // namespace hat +#endif + +class TestSHT20 : public I2CComponentTestBase { +protected: + virtual UnitSHT20* get_instance() override + { + auto ptr = new m5::unit::UnitSHT20(); + auto ccfg = ptr->component_config(); + ccfg.stored_size = STORED_SIZE; + ptr->component_config(ccfg); + return ptr; + } +#if defined(USING_HAT_SHT20) + virtual bool begin() override + { + auto board = M5.getBoard(); + const auto pins = hat::get_hat_pins(board); + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, unit->component_config().clock); + return Units.add(*unit, wire) && Units.begin(); + } +#endif +}; + +namespace { + +// SHT20 CRC-8 reference implementation for test verification +// polynomial x^8 + x^5 + x^4 + 1 = 0x131, init = 0x00 +uint8_t crc8_sht20(const uint8_t* data, size_t len) +{ + uint8_t crc = 0x00; + for (size_t i = 0; i < len; ++i) { + crc ^= data[i]; + for (uint_fast8_t bit = 0; bit < 8; ++bit) { + crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1); + } + } + return crc; +} + +void check_measurement_values(UnitSHT20* u) +{ + EXPECT_TRUE(std::isfinite(u->latest().celsius())); + EXPECT_TRUE(std::isfinite(u->latest().fahrenheit())); + EXPECT_TRUE(std::isfinite(u->latest().humidity())); +} + +} // namespace + +TEST_F(TestSHT20, SingleShot) +{ + SCOPED_TRACE(ustr); + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + uint32_t cnt{10}; // repeat 10 times + while (cnt--) { + sht20::Data d{}; + EXPECT_TRUE(unit->measureSingleshot(d)); + EXPECT_TRUE(std::isfinite(d.temperature())); + EXPECT_TRUE(std::isfinite(d.humidity())); + + // Sanity range check: temperature -40..125, humidity 0..100 + EXPECT_GE(d.celsius(), -40.f); + EXPECT_LE(d.celsius(), 125.f); + EXPECT_GE(d.humidity(), 0.f); + EXPECT_LE(d.humidity(), 100.f); + } +} + +TEST_F(TestSHT20, Periodic) +{ + SCOPED_TRACE(ustr); + + auto ad = unit->asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + EXPECT_TRUE(unit->startPeriodicMeasurement()); + EXPECT_TRUE(unit->inPeriodic()); + + M5_LOGI("Periodic: interval:%lu ms", (unsigned long)unit->interval()); + + // Cannot call singleshot in periodic + { + sht20::Data d{}; + EXPECT_FALSE(unit->measureSingleshot(d)); + } + + { + uint32_t timeout = is_bus ? std::max(unit->interval(), 500) * (STORED_SIZE + 1) * 4 + : unit->interval() * (STORED_SIZE + 1); + auto r = collect_periodic_measurements(unit.get(), STORED_SIZE, timeout, check_measurement_values); + EXPECT_FALSE(r.timed_out); + EXPECT_EQ(r.update_count, STORED_SIZE); + EXPECT_LE(r.median(), r.expected_interval + (is_bus ? 5U : 1U)); + } + + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + EXPECT_EQ(unit->available(), STORED_SIZE); + EXPECT_FALSE(unit->empty()); + EXPECT_TRUE(unit->full()); + + uint32_t cnt{2}; + while (cnt-- && unit->available()) { + EXPECT_TRUE(std::isfinite(unit->temperature())); + EXPECT_TRUE(std::isfinite(unit->humidity())); + EXPECT_FLOAT_EQ(unit->temperature(), unit->oldest().temperature()); + 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_FALSE(std::isfinite(unit->temperature())); + EXPECT_FALSE(std::isfinite(unit->humidity())); +} + +TEST_F(TestSHT20, Resolution) +{ + SCOPED_TRACE(ustr); + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + + constexpr std::tuple table[] = { + {"RH12_T14", Resolution::RH12_T14}, + {"RH8_T12", Resolution::RH8_T12}, + {"RH10_T13", Resolution::RH10_T13}, + {"RH11_T11", Resolution::RH11_T11}, + }; + + for (auto&& e : table) { + const char* s{}; + Resolution res{}; + std::tie(s, res) = e; + SCOPED_TRACE(s); + + EXPECT_TRUE(unit->writeResolution(res)); + + Resolution readback{}; + EXPECT_TRUE(unit->readResolution(readback)); + EXPECT_EQ(readback, res); + + // Verify measurement works at each resolution + sht20::Data d{}; + EXPECT_TRUE(unit->measureSingleshot(d)); + EXPECT_TRUE(std::isfinite(d.temperature())); + EXPECT_TRUE(std::isfinite(d.humidity())); + } + + // Restore default + EXPECT_TRUE(unit->writeResolution(Resolution::RH12_T14)); +} + +TEST_F(TestSHT20, Heater) +{ + SCOPED_TRACE(ustr); + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + + EXPECT_TRUE(unit->startHeater()); + + // Read user register to verify heater is on + uint8_t reg{}; + uint8_t cmd = READ_USER_REG; + EXPECT_EQ(unit->writeWithTransaction(&cmd, 1), m5::hal::error::error_t::OK); + EXPECT_EQ(unit->readWithTransaction(®, 1), m5::hal::error::error_t::OK); + EXPECT_TRUE(reg & REG_HEATER_BIT); + + EXPECT_TRUE(unit->stopHeater()); + + EXPECT_EQ(unit->writeWithTransaction(&cmd, 1), m5::hal::error::error_t::OK); + EXPECT_EQ(unit->readWithTransaction(®, 1), m5::hal::error::error_t::OK); + EXPECT_FALSE(reg & REG_HEATER_BIT); +} + +TEST_F(TestSHT20, SoftReset) +{ + SCOPED_TRACE(ustr); + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + + // Change resolution and enable heater, then reset + EXPECT_TRUE(unit->writeResolution(Resolution::RH8_T12)); + EXPECT_TRUE(unit->startHeater()); + EXPECT_TRUE(unit->softReset()); + + // After reset, resolution returns to default (RH12_T14) + Resolution res{}; + EXPECT_TRUE(unit->readResolution(res)); + EXPECT_EQ(res, Resolution::RH12_T14); + + // Datasheet: "Reinitializes to default settings (except heater bit)" + // Heater bit is NOT cleared by soft reset + uint8_t reg{}; + uint8_t cmd = READ_USER_REG; + EXPECT_EQ(unit->writeWithTransaction(&cmd, 1), m5::hal::error::error_t::OK); + EXPECT_EQ(unit->readWithTransaction(®, 1), m5::hal::error::error_t::OK); + EXPECT_TRUE(reg & REG_HEATER_BIT); + + // Clean up: disable heater + EXPECT_TRUE(unit->stopHeater()); +} From 0da85abbe26574f1416a1f959c9eaefdf89311f7 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 26 Mar 2026 23:40:46 +0900 Subject: [PATCH 18/22] Cosmetic change --- test/embedded/embedded_main.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/embedded/embedded_main.cpp b/test/embedded/embedded_main.cpp index b26c88d..072a103 100644 --- a/test/embedded/embedded_main.cpp +++ b/test/embedded/embedded_main.cpp @@ -30,10 +30,8 @@ void test() void setup() { - delay(1500); - -#if defined(USING_HAT_SHT30) || defined(USING_HAT_QMP6988) || defined(USING_HAT_SHT20) || defined(USING_HAT_BMP280) \ - || defined(USING_HAT_YUN) +#if defined(USING_HAT_SHT30) || defined(USING_HAT_QMP6988) || defined(USING_HAT_SHT20) || defined(USING_HAT_BMP280) || \ + defined(USING_HAT_YUN) auto m5cfg = M5.config(); m5cfg.pmic_button = false; // Disable BtnPWR m5cfg.internal_imu = false; // Disable internal IMU From c54dd6b53d76cdd3164aad626eaf30d27c253c66 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 14:23:23 +0900 Subject: [PATCH 19/22] Migrate Arduino build workflow to arduino/compile-sketches@v1 --- .../arduino-env3-esp-v3-build-check.yml | 45 ++++++++---- .../workflows/arduino-env3-m5-build-check.yml | 44 ++++++++---- .../workflows/arduino-esp-v2-build-check.yml | 66 ++++++++++------- .../workflows/arduino-esp-v3-build-check.yml | 70 +++++++++++-------- .github/workflows/arduino-m5-build-check.yml | 68 +++++++++++------- 5 files changed, 189 insertions(+), 104 deletions(-) diff --git a/.github/workflows/arduino-env3-esp-v3-build-check.yml b/.github/workflows/arduino-env3-esp-v3-build-check.yml index 00ec544..f5ff239 100644 --- a/.github/workflows/arduino-env3-esp-v3-build-check.yml +++ b/.github/workflows/arduino-env3-esp-v3-build-check.yml @@ -12,7 +12,10 @@ on: branches: - '*' paths: - - 'src/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/UnitENVIII/**.ino' - 'examples/UnitUnified/UnitENVIII/**.cpp' - 'examples/UnitUnified/UnitENVIII/**.hpp' @@ -21,7 +24,10 @@ on: - '.github/workflows/arduino-env3-esp-v3-build-check.yml' pull_request: paths: - - 'src/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/UnitENVIII/**.ino' - 'examples/UnitUnified/UnitENVIII/**.cpp' - 'examples/UnitUnified/UnitENVIII/**.hpp' @@ -58,6 +64,7 @@ jobs: - arduino_nesso_n1 - m5stack_atom - m5stack_atoms3 + - m5stack_atoms3r - m5stack_capsule - m5stack_cardputer - m5stack_core @@ -121,15 +128,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Build + - name: Prepare libraries list + id: libs + run: | + { + echo "yaml<> "$GITHUB_OUTPUT" + - name: Compile examples - uses: ArminJo/arduino-test-compile@master + uses: arduino/compile-sketches@v1 with: - arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} - arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} - platform-url: ${{ matrix.platform-url }} - required-libraries: ${{ env.REQUIRED_LIBRARIES }} - extra-arduino-cli-args: ${{ matrix.cli-args }} - build-properties: ${{ matrix.build-properties }} - sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ + fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + platforms: | + - name: ${{ matrix.platform }}:${{ matrix.archi }} + source-url: ${{ matrix.platform-url }} + version: ${{ matrix.platform-version }} + libraries: ${{ steps.libs.outputs.yaml }} + sketch-paths: | + - ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.sketch }} + cli-compile-flags: | + - --build-property + - build.extra_flags=${{ matrix.build-properties }} diff --git a/.github/workflows/arduino-env3-m5-build-check.yml b/.github/workflows/arduino-env3-m5-build-check.yml index 500efc7..78f504b 100644 --- a/.github/workflows/arduino-env3-m5-build-check.yml +++ b/.github/workflows/arduino-env3-m5-build-check.yml @@ -12,7 +12,10 @@ on: branches: - '*' paths: - - 'src/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/UnitENVIII/**.ino' - 'examples/UnitUnified/UnitENVIII/**.cpp' - 'examples/UnitUnified/UnitENVIII/**.hpp' @@ -21,7 +24,10 @@ on: - '.github/workflows/arduino-env3-m5-build-check.yml' pull_request: paths: - - 'src/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/UnitENVIII/**.ino' - 'examples/UnitUnified/UnitENVIII/**.cpp' - 'examples/UnitUnified/UnitENVIII/**.hpp' @@ -132,15 +138,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Build + - name: Prepare libraries list + id: libs + run: | + { + echo "yaml<> "$GITHUB_OUTPUT" + - name: Compile examples - uses: ArminJo/arduino-test-compile@master + uses: arduino/compile-sketches@v1 with: - arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} - arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} - platform-url: ${{ matrix.platform-url }} - required-libraries: ${{ env.REQUIRED_LIBRARIES }} - extra-arduino-cli-args: ${{ matrix.cli-args }} - build-properties: ${{ matrix.build-properties }} - sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ + fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + platforms: | + - name: ${{ matrix.platform }}:${{ matrix.archi }} + source-url: ${{ matrix.platform-url }} + version: ${{ matrix.platform-version }} + libraries: ${{ steps.libs.outputs.yaml }} + sketch-paths: | + - ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.sketch }} + cli-compile-flags: | + - --build-property + - build.extra_flags=${{ matrix.build-properties }} diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index 2b485d1..1b189b4 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -12,23 +12,29 @@ on: branches: - '*' paths: - - 'src/**' - - 'examples/UnitUnified/Paper/**' - - 'examples/UnitUnified/UnitCO2/**' - - 'examples/UnitUnified/UnitCO2L/**' - - 'examples/UnitUnified/UnitENVIV/**' - - 'examples/UnitUnified/UnitENVPro/**' - - 'examples/UnitUnified/UnitTVOC/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' + - 'examples/UnitUnified/**.ino' + - 'examples/UnitUnified/**.cpp' + - 'examples/UnitUnified/**.hpp' + - 'examples/UnitUnified/**.h' + - 'examples/UnitUnified/**.c' + - '!examples/UnitUnified/UnitENVIII/**' - '.github/workflows/arduino-esp-v2-build-check.yml' pull_request: paths: - - 'src/**' - - 'examples/UnitUnified/Paper/**' - - 'examples/UnitUnified/UnitCO2/**' - - 'examples/UnitUnified/UnitCO2L/**' - - 'examples/UnitUnified/UnitENVIV/**' - - 'examples/UnitUnified/UnitENVPro/**' - - 'examples/UnitUnified/UnitTVOC/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' + - 'examples/UnitUnified/**.ino' + - 'examples/UnitUnified/**.cpp' + - 'examples/UnitUnified/**.hpp' + - 'examples/UnitUnified/**.h' + - 'examples/UnitUnified/**.c' + - '!examples/UnitUnified/UnitENVIII/**' - '.github/workflows/arduino-esp-v2-build-check.yml' workflow_dispatch: @@ -85,16 +91,26 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Build + - name: Prepare libraries list + id: libs + run: | + { + echo "yaml<> "$GITHUB_OUTPUT" + - name: Compile examples - uses: ArminJo/arduino-test-compile@master + uses: arduino/compile-sketches@v1 with: - arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} - arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} - platform-url: ${{ matrix.platform-url }} - required-libraries: ${{ env.REQUIRED_LIBRARIES }} - extra-arduino-cli-args: ${{ matrix.cli-args }} - #build-properties: ${{ toJson(matrix.build-properties) }} - sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }} - #sketches-exclude: ${{ matrix.sketches-exclude }} + fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + platforms: | + - name: ${{ matrix.platform }}:${{ matrix.archi }} + source-url: ${{ matrix.platform-url }} + version: ${{ matrix.platform-version }} + libraries: ${{ steps.libs.outputs.yaml }} + sketch-paths: | + - ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }}/${{ matrix.sketch }} diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index b0a6473..599691a 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -12,25 +12,29 @@ on: branches: - '*' paths: - - 'src/**' - - 'examples/UnitUnified/Paper/**' - - 'examples/UnitUnified/UnitCO2/**' - - 'examples/UnitUnified/UnitCO2L/**' - - 'examples/UnitUnified/UnitENVIV/**' - - 'examples/UnitUnified/UnitENVPro/**' - - 'examples/UnitUnified/UnitTVOC/**' - - 'examples/UnitUnified/HatYun/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' + - 'examples/UnitUnified/**.ino' + - 'examples/UnitUnified/**.cpp' + - 'examples/UnitUnified/**.hpp' + - 'examples/UnitUnified/**.h' + - 'examples/UnitUnified/**.c' + - '!examples/UnitUnified/UnitENVIII/**' - '.github/workflows/arduino-esp-v3-build-check.yml' pull_request: paths: - - 'src/**' - - 'examples/UnitUnified/Paper/**' - - 'examples/UnitUnified/UnitCO2/**' - - 'examples/UnitUnified/UnitCO2L/**' - - 'examples/UnitUnified/UnitENVIV/**' - - 'examples/UnitUnified/UnitENVPro/**' - - 'examples/UnitUnified/UnitTVOC/**' - - 'examples/UnitUnified/HatYun/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' + - 'examples/UnitUnified/**.ino' + - 'examples/UnitUnified/**.cpp' + - 'examples/UnitUnified/**.hpp' + - 'examples/UnitUnified/**.h' + - 'examples/UnitUnified/**.c' + - '!examples/UnitUnified/UnitENVIII/**' - '.github/workflows/arduino-esp-v3-build-check.yml' workflow_dispatch: @@ -69,6 +73,7 @@ jobs: - arduino_nesso_n1 - m5stack_atom - m5stack_atoms3 + - m5stack_atoms3r - m5stack_capsule - m5stack_cardputer - m5stack_core @@ -133,17 +138,26 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Build + - name: Prepare libraries list + id: libs + run: | + { + echo "yaml<> "$GITHUB_OUTPUT" + - name: Compile examples - uses: ArminJo/arduino-test-compile@master + uses: arduino/compile-sketches@v1 with: - arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} - arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} - platform-url: ${{ matrix.platform-url }} - required-libraries: ${{ env.REQUIRED_LIBRARIES }} -# required-libraries: ${{ matrix.board == 'm5stack_nanoc6' && env.REQUIRED_LIBRARIES_NANOC6 || env.REQUIRED_LIBRARIES }} - extra-arduino-cli-args: ${{ matrix.cli-args }} - #build-properties: ${{ toJson(matrix.build-properties) }} - sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }} - #sketches-exclude: ${{ matrix.sketches-exclude }} + fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + platforms: | + - name: ${{ matrix.platform }}:${{ matrix.archi }} + source-url: ${{ matrix.platform-url }} + version: ${{ matrix.platform-version }} + libraries: ${{ steps.libs.outputs.yaml }} + sketch-paths: | + - ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }}/${{ matrix.sketch }} diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index ec523fd..56377f5 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -12,25 +12,29 @@ on: branches: - '*' paths: - - 'src/**' - - 'examples/UnitUnified/Paper/**' - - 'examples/UnitUnified/UnitCO2/**' - - 'examples/UnitUnified/UnitCO2L/**' - - 'examples/UnitUnified/UnitENVIV/**' - - 'examples/UnitUnified/UnitENVPro/**' - - 'examples/UnitUnified/UnitTVOC/**' - - 'examples/UnitUnified/HatYun/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' + - 'examples/UnitUnified/**.ino' + - 'examples/UnitUnified/**.cpp' + - 'examples/UnitUnified/**.hpp' + - 'examples/UnitUnified/**.h' + - 'examples/UnitUnified/**.c' + - '!examples/UnitUnified/UnitENVIII/**' - '.github/workflows/arduino-m5-build-check.yml' pull_request: paths: - - 'src/**' - - 'examples/UnitUnified/Paper/**' - - 'examples/UnitUnified/UnitCO2/**' - - 'examples/UnitUnified/UnitCO2L/**' - - 'examples/UnitUnified/UnitENVIV/**' - - 'examples/UnitUnified/UnitENVPro/**' - - 'examples/UnitUnified/UnitTVOC/**' - - 'examples/UnitUnified/HatYun/**' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' + - 'examples/UnitUnified/**.ino' + - 'examples/UnitUnified/**.cpp' + - 'examples/UnitUnified/**.hpp' + - 'examples/UnitUnified/**.h' + - 'examples/UnitUnified/**.c' + - '!examples/UnitUnified/UnitENVIII/**' - '.github/workflows/arduino-m5-build-check.yml' workflow_dispatch: @@ -148,17 +152,27 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Build + - name: Prepare libraries list + id: libs + run: | + { + echo "yaml<> "$GITHUB_OUTPUT" + - name: Compile examples - uses: ArminJo/arduino-test-compile@master + uses: arduino/compile-sketches@v1 with: - arduino-board-fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} - arduino-platform: ${{ matrix.platform }}:${{ matrix.archi }}@${{ matrix.platform-version }} - platform-url: ${{ matrix.platform-url }} - required-libraries: ${{ env.REQUIRED_LIBRARIES }} - extra-arduino-cli-args: ${{ matrix.cli-args }} - #build-properties: ${{ toJson(matrix.build-properties) }} - sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }} - #sketches-exclude: ${{ matrix.sketches-exclude }} + fqbn: ${{ matrix.platform }}:${{ matrix.archi }}:${{ matrix.board }} + platforms: | + - name: ${{ matrix.platform }}:${{ matrix.archi }} + source-url: ${{ matrix.platform-url }} + version: ${{ matrix.platform-version }} + libraries: ${{ steps.libs.outputs.yaml }} + sketch-paths: | + - ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }}/${{ matrix.sketch }} From d8b5ead16e657e970b45362f9ab8e460845400ed Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 14:25:28 +0900 Subject: [PATCH 20/22] Fixes Paper example EPD rendering --- examples/UnitUnified/Paper/main/Paper.cpp | 39 +++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/examples/UnitUnified/Paper/main/Paper.cpp b/examples/UnitUnified/Paper/main/Paper.cpp index e5e94af..15dbc8c 100644 --- a/examples/UnitUnified/Paper/main/Paper.cpp +++ b/examples/UnitUnified/Paper/main/Paper.cpp @@ -19,8 +19,9 @@ float latest_temperature{}; float latest_humidity{}; bool has_measurement{}; uint32_t last_lcd_update_ms{}; +constexpr uint32_t lcd_update_interval_ms{60 * 1000U}; -void draw_dashboard(const float temperature, const float humidity) +void draw_dashboard(const float temperature, const float humidity, const bool force_full_refresh = false) { char temp_text[32]; char hum_text[32]; @@ -30,13 +31,23 @@ void draw_dashboard(const float temperature, const float humidity) const int w = lcd.width(); const int h = lcd.height(); - constexpr int value_x = 220; - constexpr int temp_value_y = 130; - const int humid_value_y = h / 2 + 58; + constexpr int value_x{220}; + constexpr int temp_value_y{130}; + constexpr uint8_t full_refresh_cycle{5}; + const int humid_value_y = h / 2 + 58; static bool initialized{}; - lcd.startWrite(); - if (!initialized) { + static uint8_t partial_count{}; + + // Set EPD mode before any drawing to avoid mid-frame refresh + if (force_full_refresh || ++partial_count >= full_refresh_cycle) { + lcd.setEpdMode(m5gfx::epd_mode_t::epd_quality); + partial_count = 0; + } else { + lcd.setEpdMode(m5gfx::epd_mode_t::epd_text); + } + + if (!initialized || force_full_refresh) { lcd.fillScreen(TFT_WHITE); lcd.drawRoundRect(12, 12, w - 24, h - 24, 16, TFT_BLACK); lcd.drawFastHLine(28, 88, w - 56, TFT_BLACK); @@ -58,13 +69,13 @@ void draw_dashboard(const float temperature, const float humidity) initialized = true; } + lcd.waitDisplay(); lcd.setTextColor(TFT_BLACK, TFT_WHITE); lcd.setTextSize(3.5f); - lcd.setCursor(value_x, temp_value_y); - lcd.print(temp_text); - lcd.setCursor(value_x, humid_value_y); - lcd.print(hum_text); - lcd.endWrite(); + lcd.setTextPadding(w - value_x - 28); + lcd.drawString(temp_text, value_x, temp_value_y); + lcd.drawString(hum_text, value_x, humid_value_y); + lcd.setTextPadding(0); } } // namespace @@ -127,8 +138,10 @@ void loop() M5.Log.printf(">SHT30Temp:%2.2f\n>Humidity:%2.2f\n", latest_temperature, latest_humidity); } - if (has_measurement && (now - last_lcd_update_ms >= 60 * 1000U || touch_redraw)) { - draw_dashboard(latest_temperature, latest_humidity); + if (has_measurement && (now - last_lcd_update_ms >= lcd_update_interval_ms || touch_redraw)) { + lcd.startWrite(); + draw_dashboard(latest_temperature, latest_humidity, touch_redraw); last_lcd_update_ms = now; + lcd.endWrite(); } } From d61e0fc5d0208c4519c0a80b8d8b2a6257a60c9b Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 14:31:37 +0900 Subject: [PATCH 21/22] Raise version to 1.4.0 --- library.json | 4 ++-- library.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library.json b/library.json index ec7b2e1..444ae09 100644 --- a/library.json +++ b/library.json @@ -15,7 +15,7 @@ "boschsensortec/BME68x Sensor library": ">=1.3.40408", "boschsensortec/bsec2": ">=1.10.2610" }, - "version": "1.3.2", + "version": "1.4.0", "frameworks": [ "arduino" ], @@ -32,4 +32,4 @@ "docs/html" ] } -} +} \ No newline at end of file diff --git a/library.properties b/library.properties index ef12519..5bf8c85 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unit-ENV -version=1.3.2 +version=1.4.0 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack UNIT ENV using M5UnitUnified From 1fcc47cba4875be589a115281263e43098e189ff Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 15:52:23 +0900 Subject: [PATCH 22/22] Fixes workflow --- .github/workflows/arduino-env3-esp-v3-build-check.yml | 1 - .github/workflows/arduino-esp-v3-build-check.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/arduino-env3-esp-v3-build-check.yml b/.github/workflows/arduino-env3-esp-v3-build-check.yml index f5ff239..9740ee0 100644 --- a/.github/workflows/arduino-env3-esp-v3-build-check.yml +++ b/.github/workflows/arduino-env3-esp-v3-build-check.yml @@ -64,7 +64,6 @@ jobs: - arduino_nesso_n1 - m5stack_atom - m5stack_atoms3 - - m5stack_atoms3r - m5stack_capsule - m5stack_cardputer - m5stack_core diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 599691a..9fa298e 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -73,7 +73,6 @@ jobs: - arduino_nesso_n1 - m5stack_atom - m5stack_atoms3 - - m5stack_atoms3r - m5stack_capsule - m5stack_cardputer - m5stack_core