From 228862cd956df3e1bf382801d1f7d5678b669b9b Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 16:09:20 +0900 Subject: [PATCH 01/40] Consolidate the examples, Add support NesssoN1 --- .../DualSensor/main/DualSensor.cpp | 83 +++++-- .../UnitUnified/DualSensor/src/ui_plotter.cpp | 3 +- .../GraphicalMeter/GraphicalMeter.ino | 16 ++ .../GraphicalMeter/main/GraphicalMeter.cpp | 191 +++++++++++++++ .../GraphicalMeter/src/meter.hpp | 0 .../GraphicalMeter/src/ui_plotter.cpp | 3 +- .../GraphicalMeter/src/ui_plotter.hpp | 0 .../GraphicalMeter/src/view.hpp | 0 .../GraphicalMeter/GraphicalMeter.ino | 9 - .../GraphicalMeter/main/GraphicalMeter.cpp | 112 --------- .../GraphicalMeter/src/ui_plotter.cpp | 96 -------- .../HatHeart/PlotToSerial/PlotToSerial.ino | 9 - .../PlotToSerial/main/PlotToSerial.cpp | 133 ----------- .../UnitUnified/PlotToSerial/PlotToSerial.ino | 16 ++ .../PlotToSerial/main/PlotToSerial.cpp | 218 ++++++++++++++++++ .../GraphicalMeter/GraphicalMeter.ino | 10 - .../GraphicalMeter/main/GraphicalMeter.cpp | 82 ------- .../UnitHeart/GraphicalMeter/src/meter.hpp | 43 ---- .../GraphicalMeter/src/ui_plotter.hpp | 116 ---------- .../UnitHeart/GraphicalMeter/src/view.hpp | 69 ------ .../UnitHeart/PlotToSerial/PlotToSerial.ino | 9 - .../PlotToSerial/main/PlotToSerial.cpp | 103 --------- platformio.ini | 152 ++++++++---- src/utility/pulse_monitor.cpp | 10 + 24 files changed, 624 insertions(+), 859 deletions(-) create mode 100644 examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino create mode 100644 examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp rename examples/UnitUnified/{HatHeart => }/GraphicalMeter/src/meter.hpp (100%) rename examples/UnitUnified/{UnitHeart => }/GraphicalMeter/src/ui_plotter.cpp (96%) rename examples/UnitUnified/{HatHeart => }/GraphicalMeter/src/ui_plotter.hpp (100%) rename examples/UnitUnified/{HatHeart => }/GraphicalMeter/src/view.hpp (100%) delete mode 100644 examples/UnitUnified/HatHeart/GraphicalMeter/GraphicalMeter.ino delete mode 100644 examples/UnitUnified/HatHeart/GraphicalMeter/main/GraphicalMeter.cpp delete mode 100644 examples/UnitUnified/HatHeart/GraphicalMeter/src/ui_plotter.cpp delete mode 100644 examples/UnitUnified/HatHeart/PlotToSerial/PlotToSerial.ino delete mode 100644 examples/UnitUnified/HatHeart/PlotToSerial/main/PlotToSerial.cpp create mode 100644 examples/UnitUnified/PlotToSerial/PlotToSerial.ino create mode 100644 examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp delete mode 100644 examples/UnitUnified/UnitHeart/GraphicalMeter/GraphicalMeter.ino delete mode 100644 examples/UnitUnified/UnitHeart/GraphicalMeter/main/GraphicalMeter.cpp delete mode 100644 examples/UnitUnified/UnitHeart/GraphicalMeter/src/meter.hpp delete mode 100644 examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.hpp delete mode 100644 examples/UnitUnified/UnitHeart/GraphicalMeter/src/view.hpp delete mode 100644 examples/UnitUnified/UnitHeart/PlotToSerial/PlotToSerial.ino delete mode 100644 examples/UnitUnified/UnitHeart/PlotToSerial/main/PlotToSerial.cpp diff --git a/examples/UnitUnified/DualSensor/main/DualSensor.cpp b/examples/UnitUnified/DualSensor/main/DualSensor.cpp index 49c923b..b5b5ace 100644 --- a/examples/UnitUnified/DualSensor/main/DualSensor.cpp +++ b/examples/UnitUnified/DualSensor/main/DualSensor.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "../src/view.hpp" namespace { @@ -20,6 +21,29 @@ m5::unit::HatHeart hat; View* view[2]{}; +struct I2cPins { + int sda; + int scl; +}; + +I2cPins get_hat_i2c_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 using namespace m5::unit::max30102; @@ -32,10 +56,12 @@ void setup() m5cfg.internal_imu = false; // Disable internal IMU m5cfg.internal_rtc = false; // Disable internal RTC M5.begin(m5cfg); + const auto board_type = M5.getBoard(); - auto board = M5.getBoard(); - if (board != m5::board_t::board_M5StickCPlus && board != m5::board_t::board_M5StickCPlus2) { - M5_LOGE("Example for StickCPlus/CPlus2"); + const auto pins = get_hat_i2c_pins(board_type); + M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); + if (pins.sda < 0 || pins.scl < 0) { + M5_LOGE("HatHeart requires Wire1-capable boards"); lcd.clear(TFT_RED); while (true) { m5::utility::delay(10000); @@ -48,25 +74,44 @@ void setup() } // Setup required to use HatHEART - pinMode(25, INPUT_PULLUP); - pinMode(26, OUTPUT); + pinMode(pins.scl, OUTPUT); - // Wire settings + // HatHeart on Wire1 + Wire1.end(); + Wire1.begin(pins.sda, pins.scl, 400 * 1000U); + + // UnitHeart wire settings 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); - Wire.end(); - Wire.begin(pin_num_sda, pin_num_scl, 400 * 1000U); - - Wire1.end(); - Wire1.begin(0, 26, 400 * 1000U); - - // UnitHeart connected to GROOVE with Wire - // HatHeart connected to PIN sockect with Wire1 - if (!Units.add(unit, Wire) || !Units.add(hat, Wire1) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); + // For NessoN1 GROVE + if (board_type == m5::board_t::board_ArduinoNessoN1) { + // Wire is used internally, so SoftwareI2C handles the unit + pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); + pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); + M5_LOGI("getPin(NessoN1): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + m5::hal::bus::I2CBusConfig i2c_cfg; + i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); + i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); + auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); + M5_LOGI("Bus:%d", i2c_bus.has_value()); + if (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.add(hat, Wire1) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + } else { + // UnitHeart on Wire + 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 (!Units.add(unit, Wire) || !Units.add(hat, Wire1) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } } } diff --git a/examples/UnitUnified/DualSensor/src/ui_plotter.cpp b/examples/UnitUnified/DualSensor/src/ui_plotter.cpp index 603d8e2..a3d0a79 100644 --- a/examples/UnitUnified/DualSensor/src/ui_plotter.cpp +++ b/examples/UnitUnified/DualSensor/src/ui_plotter.cpp @@ -83,7 +83,8 @@ void Plotter::push(LovyanGFX* dst, const int32_t x, const int32_t y) left += _wid - sz; } - while (it != itend && left < _wid) { + const int32_t right = x + _wid; + while (it != itend && left < right) { int32_t s{*it}, e{*(++it)}; dst->drawLine(left, y + hh - hh * (s - _min) / range, left + 1, y + hh - hh * (e - _min) / range, _lineClr); ++left; diff --git a/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino b/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino new file mode 100644 index 0000000..bd403a0 --- /dev/null +++ b/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Graphical meter example for UnitHeart / HatHeart +*/ +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// #define USING_UNIT_HEART +// #define USING_HAT_HEART +#endif +#include "main/GraphicalMeter.cpp" diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp new file mode 100644 index 0000000..f4a23db --- /dev/null +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Graphical meter example for UnitHeart / HatHeart + The core must be equipped with LCD +*/ +// #define USING_M5HAL // When using M5HAL (UnitHeart only) + +#include +#include +#include +#include +#include +#include "../src/view.hpp" + +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// #define USING_UNIT_HEART +// #define USING_HAT_HEART +#endif + +#if defined(USING_UNIT_HEART) +#pragma message "Using UnitHeart" +using namespace m5::unit::max30100; +#elif defined(USING_HAT_HEART) +#pragma message "Using HatHeart" +using namespace m5::unit::max30102; +#else +#error Please choose unit! +#endif + +namespace { +auto& lcd = M5.Display; +m5::unit::UnitUnified Units; +#if defined(USING_UNIT_HEART) +m5::unit::UnitHeart heart; +#elif defined(USING_HAT_HEART) +m5::unit::HatHeart heart; +#endif + +View* view{}; + +#if defined(USING_HAT_HEART) +struct I2cPins { + int sda; + int scl; + bool use_wire1; +}; + +I2cPins get_hat_i2c_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, true}; + case m5::board_t::board_M5StickS3: + return {8, 0, true}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26, true}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7, true}; + default: + return {-1, -1, false}; + } +} +#endif + +} // namespace + +void setup() +{ + auto m5cfg = M5.config(); +#if defined(USING_HAT_HEART) + 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); + + const auto board = M5.getBoard(); + + // The screen shall be in landscape mode + if (lcd.height() > lcd.width()) { + lcd.setRotation(1); + } + +#if defined(USING_HAT_HEART) + const auto pins = get_hat_i2c_pins(board); + M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); + if (pins.sda < 0 || pins.scl < 0) { + M5_LOGE("Illegal pin number"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + + // Setup required to use HatHEART + pinMode(pins.scl, OUTPUT); + + TwoWire& wire = pins.use_wire1 ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, 400 * 1000U); + if (!Units.add(heart, wire) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } +#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); + // For NessoN1 GROVE + if (board == m5::board_t::board_ArduinoNessoN1) { + // Wire is used internally, so SoftwareI2C handles the unit + pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); + pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); + M5_LOGI("getPin(NessoN1): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + m5::hal::bus::I2CBusConfig i2c_cfg; + i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); + i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); + auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); + M5_LOGI("Bus:%d", i2c_bus.has_value()); + if (!Units.add(heart, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + } else { + // Using TwoWire + M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + Wire.end(); + Wire.begin(pin_num_sda, pin_num_scl, 400000U); + if (!Units.add(heart, Wire) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + } +#endif + + M5_LOGI("M5UnitUnified has been begun"); + M5_LOGI("%s", Units.debugInfo().c_str()); + + lcd.clear(0); + +#if defined(USING_UNIT_HEART) + view = new View(lcd.width(), lcd.height(), true); +#else + view = new View(lcd.width(), lcd.height(), false); +#endif + view->_monitor.setSamplingRate(heart.caluculateSamplingRate()); + view->push(&lcd, 0, 0); + + M5_LOGI("periodic:%d", heart.inPeriodic()); +} + +void loop() +{ + M5.update(); + Units.update(); + + if (heart.updated()) { + if (heart.overflow()) { + M5_LOGW("OVERFLOW:%u", heart.overflow()); + } + while (heart.available()) { + view->push_back(heart.ir(), heart.red()); + view->update(); + heart.discard(); + } + view->render(); + view->push(&lcd, 0, 0); + } + + if (M5.BtnA.wasClicked()) { + view->clear(); + } +} diff --git a/examples/UnitUnified/HatHeart/GraphicalMeter/src/meter.hpp b/examples/UnitUnified/GraphicalMeter/src/meter.hpp similarity index 100% rename from examples/UnitUnified/HatHeart/GraphicalMeter/src/meter.hpp rename to examples/UnitUnified/GraphicalMeter/src/meter.hpp diff --git a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.cpp b/examples/UnitUnified/GraphicalMeter/src/ui_plotter.cpp similarity index 96% rename from examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.cpp rename to examples/UnitUnified/GraphicalMeter/src/ui_plotter.cpp index 603d8e2..a3d0a79 100644 --- a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.cpp +++ b/examples/UnitUnified/GraphicalMeter/src/ui_plotter.cpp @@ -83,7 +83,8 @@ void Plotter::push(LovyanGFX* dst, const int32_t x, const int32_t y) left += _wid - sz; } - while (it != itend && left < _wid) { + const int32_t right = x + _wid; + while (it != itend && left < right) { int32_t s{*it}, e{*(++it)}; dst->drawLine(left, y + hh - hh * (s - _min) / range, left + 1, y + hh - hh * (e - _min) / range, _lineClr); ++left; diff --git a/examples/UnitUnified/HatHeart/GraphicalMeter/src/ui_plotter.hpp b/examples/UnitUnified/GraphicalMeter/src/ui_plotter.hpp similarity index 100% rename from examples/UnitUnified/HatHeart/GraphicalMeter/src/ui_plotter.hpp rename to examples/UnitUnified/GraphicalMeter/src/ui_plotter.hpp diff --git a/examples/UnitUnified/HatHeart/GraphicalMeter/src/view.hpp b/examples/UnitUnified/GraphicalMeter/src/view.hpp similarity index 100% rename from examples/UnitUnified/HatHeart/GraphicalMeter/src/view.hpp rename to examples/UnitUnified/GraphicalMeter/src/view.hpp diff --git a/examples/UnitUnified/HatHeart/GraphicalMeter/GraphicalMeter.ino b/examples/UnitUnified/HatHeart/GraphicalMeter/GraphicalMeter.ino deleted file mode 100644 index b9fe561..0000000 --- a/examples/UnitUnified/HatHeart/GraphicalMeter/GraphicalMeter.ino +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Graphical meter example for HatHeart -*/ -#include "main/GraphicalMeter.cpp" diff --git a/examples/UnitUnified/HatHeart/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/HatHeart/GraphicalMeter/main/GraphicalMeter.cpp deleted file mode 100644 index c99a4cd..0000000 --- a/examples/UnitUnified/HatHeart/GraphicalMeter/main/GraphicalMeter.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Graphical meter example for HatHeart -*/ -#include -#include -#include -#include -#include "../src/view.hpp" - -namespace { -auto& lcd = M5.Display; -m5::unit::UnitUnified Units; -m5::unit::HatHeart hat; -m5::heart::PulseMonitor monitor{}; - -constexpr bool using_wire1{false}; // Using Wire1 if true - -View* view{}; - -} // namespace - -using namespace m5::unit::max30102; - -void setup() -{ - // Configuration if using Wire1 - auto m5cfg = M5.config(); - if (using_wire1) { - m5cfg.pmic_button = false; // Disable BtnPWR - m5cfg.internal_imu = false; // Disable internal IMU - m5cfg.internal_rtc = false; // Disable internal RTC - } - M5.begin(m5cfg); - - auto board = M5.getBoard(); - if (board != m5::board_t::board_M5StickCPlus && board != m5::board_t::board_M5StickCPlus2) { - M5_LOGE("HatHeart for StickCPlus/CPlus2"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - - // The screen shall be in landscape mode - if (lcd.height() > lcd.width()) { - lcd.setRotation(1); - } - - // Setup required to use HatHEART - pinMode(25, INPUT_PULLUP); - pinMode(26, OUTPUT); - - // Using TwoWire - if (using_wire1) { - Wire1.end(); - Wire1.begin(0, 26, 400 * 1000U); - if (!Units.add(hat, Wire1) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - } else { - Wire.end(); - Wire.begin(0, 26, 400 * 1000U); - if (!Units.add(hat, Wire) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - } - - M5_LOGI("M5UnitUnified has been begun"); - M5_LOGI("%s", Units.debugInfo().c_str()); - - lcd.clear(0); - - view = new View(lcd.width(), lcd.height(), false); - view->_monitor.setSamplingRate(hat.caluculateSamplingRate()); - view->push(&lcd, 0, 0); -} - -void loop() -{ - M5.update(); - Units.update(); - - if (hat.updated()) { - if (hat.overflow()) { - M5_LOGW("OVERFLOW:%u", hat.overflow()); - } - while (hat.available()) { - view->push_back(hat.ir(), hat.red()); - view->update(); - hat.discard(); - } - view->render(); - view->push(&lcd, 0, 0); - } - - if (M5.BtnA.wasClicked()) { - view->clear(); - } -} diff --git a/examples/UnitUnified/HatHeart/GraphicalMeter/src/ui_plotter.cpp b/examples/UnitUnified/HatHeart/GraphicalMeter/src/ui_plotter.cpp deleted file mode 100644 index 603d8e2..0000000 --- a/examples/UnitUnified/HatHeart/GraphicalMeter/src/ui_plotter.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/*! - @file ui_plotter.cpp - @brief Plotter - */ -#include "ui_plotter.hpp" -#include -#include - -namespace m5 { -namespace ui { - -Plotter::Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t wid, const int32_t hgt, - const int32_t coefficient) - : _parent(parent), _wid{wid}, _hgt{hgt}, _coefficient(coefficient), _data(maxPlot), _autoScale{true} -{ -} - -Plotter::Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t minimum, const int32_t maximum, - const int32_t wid, const int32_t hgt, const int32_t coefficient) - : _parent(parent), - _min{minimum}, - _max{maximum}, - _wid{wid}, - _hgt{hgt}, - _coefficient(coefficient), - _data(maxPlot), - _autoScale{false} -{ -} - -void Plotter::push_back(const float val) -{ - push_back((int32_t)(val * _coefficient)); -} - -void Plotter::push_back(const int32_t val) -{ - auto v = _autoScale ? val : std::min(std::max(val, _min), _max); - _data.push_back(v); - - if (_autoScale && _data.size() >= 2) { - auto it = std::minmax_element(_data.cbegin(), _data.cend()); - _min = *(it.first); - _max = *(it.second); - if (_min == _max) { - ++_max; - } - } -} - -void Plotter::push(LovyanGFX* dst, const int32_t x, const int32_t y) -{ - dst->setClipRect(x, y, width(), height()); - - // gauge - dst->drawFastHLine(x, y, _wid, _gaugeClr); - dst->drawFastHLine(x, y + (_hgt >> 1), _wid, _gaugeClr); - dst->drawFastHLine(x, y + (_hgt >> 2), _wid, _gaugeClr); - dst->drawFastHLine(x, y + (_hgt >> 2) * 3, _wid, _gaugeClr); - dst->drawFastHLine(x, y + _hgt - 1, _wid, _gaugeClr); - - if (_data.size() >= 2) { - auto it = _data.cbegin(); - auto itend = --_data.cend(); - auto sz = _data.size(); - const float range{_max > _min ? (float)_max - _min : 1.0f}; - const int32_t hh{_hgt - 1}; - int32_t left{x}; - - // plot latest - if (sz > _wid) { - auto cnt{sz - _wid}; - while (cnt--) { - ++it; // Bidirectional iterator, so only ++/-- is available. - } - } - if (sz < _wid) { - left += _wid - sz; - } - - while (it != itend && left < _wid) { - int32_t s{*it}, e{*(++it)}; - dst->drawLine(left, y + hh - hh * (s - _min) / range, left + 1, y + hh - hh * (e - _min) / range, _lineClr); - ++left; - } - } - dst->clearClipRect(); -} - -} // namespace ui -} // namespace m5 diff --git a/examples/UnitUnified/HatHeart/PlotToSerial/PlotToSerial.ino b/examples/UnitUnified/HatHeart/PlotToSerial/PlotToSerial.ino deleted file mode 100644 index ee0887f..0000000 --- a/examples/UnitUnified/HatHeart/PlotToSerial/PlotToSerial.ino +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Example using M5UnitUnified for HatHeart -*/ -#include "main/PlotToSerial.cpp" diff --git a/examples/UnitUnified/HatHeart/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/HatHeart/PlotToSerial/main/PlotToSerial.cpp deleted file mode 100644 index b7ccc55..0000000 --- a/examples/UnitUnified/HatHeart/PlotToSerial/main/PlotToSerial.cpp +++ /dev/null @@ -1,133 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Example using M5UnitUnified for HatHeart -*/ -#include -#include -#include -#include - -namespace { -auto& lcd = M5.Display; -m5::unit::UnitUnified Units; -m5::unit::HatHeart hat; -m5::heart::PulseMonitor monitor{}; - -constexpr bool using_multi_led_mode{false}; // Using multiLED mode if true -constexpr bool using_wire1{false}; // Using Wire1 if true - -} // namespace - -using namespace m5::unit::max30102; - -void setup() -{ - // Configuration if using Wire1 - auto m5cfg = M5.config(); - if (using_wire1) { - m5cfg.pmic_button = false; // Disable BtnPWR - m5cfg.internal_imu = false; // Disable internal IMU - m5cfg.internal_rtc = false; // Disable internal RTC - } - M5.begin(m5cfg); - - auto board = M5.getBoard(); - if (board != m5::board_t::board_M5StickCPlus && board != m5::board_t::board_M5StickCPlus2) { - M5_LOGE("HatHeart for StickCPlus/CPlus2"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - - // Setup required to use HatHEART - pinMode(25, INPUT_PULLUP); - pinMode(26, OUTPUT); - - // Using MultiLED mode - // In MultiLED mode, you need to set and start them yourself - if (using_multi_led_mode) { - auto cfg = hat.config(); - cfg.start_periodic = false; // Ignore auto start - hat.config(cfg); - } - - // Using TwoWire - if (using_wire1) { - Wire1.end(); - Wire1.begin(0, 26, 400 * 1000U); - if (!Units.add(hat, Wire1) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - } else { - Wire.end(); - Wire.begin(0, 26, 400 * 1000U); - if (!Units.add(hat, Wire) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - } - - M5_LOGI("M5UnitUnified has been begun"); - M5_LOGI("%s", Units.debugInfo().c_str()); - - // In MultiLED mode, you need to set and start them yourself - if (using_multi_led_mode) { - hat.writeMode(Mode::MultiLED); - hat.writeSpO2Configuration(ADC::Range4096nA, Sampling::Rate400, LEDPulse::Width411); - hat.writeFIFOConfiguration(FIFOSampling::Average4, true, 15); - // hat.writeMultiLEDModeControl(Slot::Red, Slot::IR); // (A) - hat.writeMultiLEDModeControl(Slot::IR, Slot::Red); // (B) - hat.writeLEDCurrent(0, 0x1F); // Red if (A), IR if (B) - hat.writeLEDCurrent(1, 0x1F); // IR if (A), Red if (B) - hat.startPeriodicMeasurement(); - } - lcd.clear(TFT_DARKGREEN); - - monitor.setSamplingRate(hat.caluculateSamplingRate()); -} - -void loop() -{ - M5.update(); - Units.update(); - - if (hat.updated()) { - // WARNING - // If overflow is occurring, the sampling rate should be reduced because the processing is not up to par - if (hat.overflow()) { - M5_LOGW("OVERFLOW:%u", hat.overflow()); - } - - bool beat{}; - // MAX30102 is equipped with a FIFO, so multiple data may be stored - while (hat.available()) { - M5.Log.printf(">IR:%u\n>RED:%u\n", hat.ir(), hat.red()); - monitor.push_back(hat.ir(), hat.red()); // Push back the oldest data - M5.Log.printf(">MIR:%f\n", monitor.latestIR()); - monitor.update(); - beat |= monitor.isBeat(); - hat.discard(); // Discard the oldest data - } - M5.Log.printf(">BPM:%f\n>SpO2:%f\n>BEAT:%u\n", monitor.bpm(), monitor.SpO2(), beat); - } - - // Measure tempeature - if (M5.BtnA.wasClicked()) { - TemperatureData td{}; - if (hat.measureTemperatureSingleshot(td)) { - M5.Log.printf(">Temp:%f\n", td.celsius()); - } - } -} diff --git a/examples/UnitUnified/PlotToSerial/PlotToSerial.ino b/examples/UnitUnified/PlotToSerial/PlotToSerial.ino new file mode 100644 index 0000000..b295344 --- /dev/null +++ b/examples/UnitUnified/PlotToSerial/PlotToSerial.ino @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Example using M5UnitUnified for UnitHeart / HatHeart +*/ +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// #define USING_UNIT_HEART +// #define USING_HAT_HEART +#endif +#include "main/PlotToSerial.cpp" diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp new file mode 100644 index 0000000..69bbd39 --- /dev/null +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -0,0 +1,218 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +/* + Example using M5UnitUnified for UnitHeart / HatHeart +*/ +// #define USING_M5HAL // When using M5HAL (UnitHeart only) + +#include +#include +#include +#include +#include // For NessoN1 + +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// #define USING_UNIT_HEART +// #define USING_HAT_HEART +#endif + +#if defined(USING_UNIT_HEART) +#pragma message "Using UnitHeart" +using namespace m5::unit::max30100; +#elif defined(USING_HAT_HEART) +#pragma message "Using HatHeart" +using namespace m5::unit::max30102; +#else +#error Please choose unit! +#endif + +namespace { +auto& lcd = M5.Display; +m5::unit::UnitUnified Units; +#if defined(USING_UNIT_HEART) +m5::unit::UnitHeart unit; +#elif defined(USING_HAT_HEART) +m5::unit::HatHeart unit; +#endif + +m5::heart::PulseMonitor monitor; + +#if defined(USING_HAT_HEART) +constexpr bool using_multi_led_mode{false}; // Using multiLED mode if true + +struct I2cPins { + int sda; + int scl; + bool use_wire1; +}; + +I2cPins get_hat_i2c_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, true}; + case m5::board_t::board_M5StickS3: + return {8, 0, true}; + case m5::board_t::board_M5StackCoreInk: + return {25, 26, true}; + case m5::board_t::board_ArduinoNessoN1: + return {6, 7, true}; + default: + return {-1, -1, false}; + } +} +#endif + +} // namespace + +void setup() +{ + delay(1500); + + auto m5cfg = M5.config(); +#if defined(USING_HAT_HEART) + 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()) { + lcd.setRotation(1); + } + +#if defined(USING_HAT_HEART) + const auto pins = get_hat_i2c_pins(board); + M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); + if (pins.sda < 0 || pins.scl < 0) { + M5_LOGE("Illegal pin number"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + + // Setup required to use HatHEART + pinMode(pins.scl, OUTPUT); + + // Using MultiLED mode + // In MultiLED mode, you need to set and start them yourself + if (using_multi_led_mode) { + auto cfg = unit.config(); + cfg.start_periodic = false; // Ignore auto start + unit.config(cfg); + } + + auto& wire = pins.use_wire1 ? 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.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } +#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); + // For NessoN1 GROVE + if (M5.getBoard() == m5::board_t::board_ArduinoNessoN1) { + // Port A of the NessoN1 is QWIIC, then use portB (GROVE) + pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); + pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); + M5_LOGI("getPin(NessoN1): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + // Wire is used internally, so SoftwareI2C handles the unit + m5::hal::bus::I2CBusConfig i2c_cfg; + i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); + i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); + auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); + M5_LOGI("Bus:%d", i2c_bus.has_value()); + if (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + } else { + // Using TwoWire + 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 (!Units.add(unit, Wire) || !Units.begin()) { + M5_LOGE("Failed to begin"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + } +#endif + + M5_LOGI("M5UnitUnified has been begun"); + M5_LOGI("%s", Units.debugInfo().c_str()); + +#if defined(USING_HAT_HEART) + // In MultiLED mode, you need to set and start them yourself + if (using_multi_led_mode) { + M5_LOGI("MultiLED mode"); + unit.writeMode(Mode::MultiLED); + unit.writeSpO2Configuration(ADC::Range4096nA, Sampling::Rate400, LEDPulse::Width411); + unit.writeFIFOConfiguration(FIFOSampling::Average4, true, 15); + // unit.writeMultiLEDModeControl(Slot::Red, Slot::IR); // (A) + unit.writeMultiLEDModeControl(Slot::IR, Slot::Red); // (B) + unit.writeLEDCurrent(0, 0x1F); // Red if (A), IR if (B) + unit.writeLEDCurrent(1, 0x1F); // IR if (A), Red if (B) + unit.startPeriodicMeasurement(); + } +#endif + + monitor.setSamplingRate(unit.caluculateSamplingRate()); + lcd.clear(TFT_DARKGREEN); +} + +void loop() +{ + M5.update(); + Units.update(); + + if (unit.updated()) { + // WARNING + // If overflow is occurring, the sampling rate should be reduced because the processing is not up to par + if (unit.overflow()) { + M5_LOGW("OVERFLOW:%u", unit.overflow()); + } + + bool beat{}; + // MAX30100/02 is equipped with a FIFO, so multiple data may be stored + while (unit.available()) { + M5.Log.printf(">IR:%u\n>RED:%u\n", unit.ir(), unit.red()); + monitor.push_back(unit.ir(), unit.red()); // Push back the oldest data + M5.Log.printf(">MIR:%f\n", monitor.latestIR()); + monitor.update(); + beat |= monitor.isBeat(); + unit.discard(); // Discard the oldest data + } + M5.Log.printf(">BPM:%f\n>SpO2:%f\n>BEAT:%u\n", monitor.bpm(), monitor.SpO2(), beat); + } + + // Measure tempeature + if (M5.BtnA.wasClicked()) { + TemperatureData td{}; + if (unit.measureTemperatureSingleshot(td)) { + M5.Log.printf(">Temp:%f\n", td.celsius()); + } + } +} diff --git a/examples/UnitUnified/UnitHeart/GraphicalMeter/GraphicalMeter.ino b/examples/UnitUnified/UnitHeart/GraphicalMeter/GraphicalMeter.ino deleted file mode 100644 index 1784b04..0000000 --- a/examples/UnitUnified/UnitHeart/GraphicalMeter/GraphicalMeter.ino +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Graphical meter example for UnitHeart - The core must be equipped with LCD -*/ -#include "main/GraphicalMeter.cpp" diff --git a/examples/UnitUnified/UnitHeart/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/UnitHeart/GraphicalMeter/main/GraphicalMeter.cpp deleted file mode 100644 index b3e9706..0000000 --- a/examples/UnitUnified/UnitHeart/GraphicalMeter/main/GraphicalMeter.cpp +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Graphical meter example for UnitHeart - The core must be equipped with LCD -*/ -#include -#include -#include -#include -#include "../src/view.hpp" - -namespace { -auto& lcd = M5.Display; -m5::unit::UnitUnified Units; -m5::unit::UnitHeart unit; -View* view{}; - -} // namespace - -using namespace m5::unit::max30102; - -void setup() -{ - M5.begin(); - - // The screen shall be in landscape mode - if (lcd.height() > lcd.width()) { - lcd.setRotation(1); - } - - 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, 400000U); - if (!Units.add(unit, Wire) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - - M5_LOGI("M5UnitUnified has been begun"); - M5_LOGI("%s", Units.debugInfo().c_str()); - - lcd.clear(0); - - view = new View(lcd.width(), lcd.height()); - view->_monitor.setSamplingRate(unit.caluculateSamplingRate()); - view->push(&lcd, 0, 0); -} - -void loop() -{ - M5.update(); - auto touch = M5.Touch.getDetail(); - - Units.update(); - - if (unit.updated()) { - if (unit.overflow()) { - M5_LOGW("OVERFLOW:%u", unit.overflow()); - } - while (unit.available()) { - view->push_back(unit.ir(), unit.red()); - view->update(); - unit.discard(); - } - view->render(); - view->push(&lcd, 0, 0); - } - - if (M5.BtnA.wasClicked() || touch.wasClicked()) { - view->clear(); - } -} diff --git a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/meter.hpp b/examples/UnitUnified/UnitHeart/GraphicalMeter/src/meter.hpp deleted file mode 100644 index 1f8b506..0000000 --- a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/meter.hpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -#ifndef M5_UNIT_HEART_EXAMPLE_METER_HPP -#define M5_UNIT_HEART_EXAMPLE_METER_HPP - -#include -#include "ui_plotter.hpp" -#include - -class Meter { -public: - Meter(const uint32_t left, const uint32_t top, const uint32_t wid, const uint32_t hgt, m5gfx::rgb565_t tcolor) - : _left(left), _top(top), _theme_color(tcolor) - { - _plotter = new m5::ui::Plotter(nullptr, wid, wid, hgt); - _plotter->setGaugeTextDatum(textdatum_t::top_right); - _plotter->setLineColor(_theme_color); - } - - inline void push_back(const float value) - { - _plotter->push_back(value); - } - - inline void push(LovyanGFX* target, const uint32_t x, const uint32_t y) - { - _plotter->push(target, _left, _top); - } - - inline void clear() - { - _plotter->clear(); - } - -private: - uint32_t _left{}, _top{}; - m5::ui::Plotter* _plotter{}; - m5gfx::rgb565_t _theme_color{}; -}; -#endif diff --git a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.hpp b/examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.hpp deleted file mode 100644 index f504cf9..0000000 --- a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/ui_plotter.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ - -#ifndef M5_UNIT_HEART_EXAMPLE_UI_PLOTTER_HPP -#define M5_UNIT_HEART_EXAMPLE_UI_PLOTTER_HPP -#include -#include - -namespace m5 { -namespace ui { - -class Plotter { -public: - Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t wid, const int32_t hgt, - const int32_t coefficient = 1); - Plotter(LovyanGFX* parent, const size_t maxPlot, const int32_t minimum, const int32_t maximum, const int32_t wid, - const int32_t hgt, const int32_t coefficient = 1); - ~Plotter() - { - } - - inline int32_t width() const - { - return _wid; - } - inline int32_t height() const - { - return _hgt; - } - inline int32_t minimum() const - { - return _min; - } - inline int32_t maximum() const - { - return _max; - } - inline uint32_t size() const - { - return _data.size(); - } - inline int32_t latest() const - { - return !_data.empty() ? *(--_data.cend()) : 0; - } - - template - void setLineColor(const T& clr) - { - _lineClr = clr; - } - template - void setGaugeColor(const T& clr) - { - _gaugeClr = clr; - } - template - void setBackgroundColor(const T& clr) - { - _bgClr = clr; - } - - inline void setUnitString(const char* s) - { - _ustr = s; - } - inline void setGaugeTextDatum(const textdatum_t datum) - { - _tdatum = static_cast(m5::stl::to_underlying(datum) & 0x03); // only horizon - } - - void push_back(const float val); - void push_back(const int32_t val); - inline void clear() - { - _data.clear(); - } - - inline void push(const int32_t x, const int32_t y) - { - push(_parent, x, y); - } - void push(LovyanGFX* dst, const int32_t x, const int32_t y); - -protected: - m5gfx::rgb565_t lineColor() const - { - return _lineClr; - } - m5gfx::rgb565_t gaugeColor() const - { - return _gaugeClr; - } - m5gfx::rgb565_t backgroundColor() const - { - return _bgClr; - } - -protected: -private: - LovyanGFX* _parent{}; - int32_t _min{}, _max{}, _wid{}, _hgt{}, _coefficient{}; - m5::container::CircularBuffer _data; - - m5gfx::rgb565_t _lineClr{TFT_WHITE}, _gaugeClr{TFT_DARKGRAY}, _bgClr{TFT_BLACK}; - textdatum_t _tdatum{textdatum_t::top_left}; - const char* _ustr{}; - bool _autoScale{}; -}; - -} // namespace ui -} // namespace m5 -#endif diff --git a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/view.hpp b/examples/UnitUnified/UnitHeart/GraphicalMeter/src/view.hpp deleted file mode 100644 index 6d4201f..0000000 --- a/examples/UnitUnified/UnitHeart/GraphicalMeter/src/view.hpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -#ifndef M5_UNIT_HEART_EXAMPLE_VIEW_HPP -#define M5_UNIT_HEART_EXAMPLE_VIEW_HPP - -#include "meter.hpp" - -struct View { - View(const uint32_t wid, const uint32_t hgt) - { - constexpr RGBColor palettes[4] = {RGBColor(0, 0, 0), RGBColor(0, 0, 255), RGBColor(255, 0, 0), - RGBColor(255, 255, 255)}; - _sprite.setColorDepth(2); // 4 colors - _sprite.createSprite(wid, hgt); - _sprite.setFont(&fonts::FreeSansBold9pt7b); - auto pal = _sprite.getPalette(); - for (auto&& p : palettes) { - *pal++ = p; - } - _meter = new Meter(0, hgt * 2 / 3, wid, hgt / 3, 1); - } - - inline void push_back(const float ir, const float red) - { - _monitor.push_back(ir, red); - } - - void update() - { - if (_beat) { - --_beat; - } - _meter->push_back(_monitor.latestIR()); - _monitor.update(); - _beat += _monitor.isBeat() * 8; - } - - void render() - { - _sprite.clear(); - _sprite.drawString("Unit", 0, 0); - _sprite.setCursor(0, 24); - _sprite.printf("BPM: %3.2f\nSpO2:%3.2f", _monitor.bpm(), _monitor.SpO2()); - _sprite.fillCircle(_sprite.width() - 12, 24 * 3, 7, _beat ? 2 : 3); - - _meter->push(&_sprite, 0, _sprite.height() >> 1); - } - - void clear() - { - _monitor.clear(); - _meter->clear(); - } - - void push(LovyanGFX* target, const uint32_t x, const uint32_t y) - { - _sprite.pushSprite(target, x, y); - } - - LGFX_Sprite _sprite{}; - m5::heart::PulseMonitor _monitor{100, 2}; - Meter* _meter{}; - uint32_t _left{}, _top{}, _beat{}; - bool _type{}; -}; -#endif diff --git a/examples/UnitUnified/UnitHeart/PlotToSerial/PlotToSerial.ino b/examples/UnitUnified/UnitHeart/PlotToSerial/PlotToSerial.ino deleted file mode 100644 index e97160d..0000000 --- a/examples/UnitUnified/UnitHeart/PlotToSerial/PlotToSerial.ino +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Example using M5UnitUnified for UnitHeart -*/ -#include "main/PlotToSerial.cpp" diff --git a/examples/UnitUnified/UnitHeart/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/UnitHeart/PlotToSerial/main/PlotToSerial.cpp deleted file mode 100644 index 615713a..0000000 --- a/examples/UnitUnified/UnitHeart/PlotToSerial/main/PlotToSerial.cpp +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - */ -/* - Example using M5UnitUnified for UnitHeart -*/ -// #define USING_M5HAL // When using M5HAL - -#include -#include -#include -#if !defined(USING_M5HAL) -#include -#endif - -namespace { -auto& lcd = M5.Display; -m5::unit::UnitUnified Units; -m5::unit::UnitHeart unit; -m5::heart::PulseMonitor monitor; - -} // namespace - -using namespace m5::unit::max30100; - -void setup() -{ - M5.begin(); - - auto pin_num_sda = M5.getPin(m5::pin_name_t::port_a_sda); - auto pin_num_scl = M5.getPin(m5::pin_name_t::port_a_scl); - M5_LOGI("getPin: SDA:%u SCL:%u", pin_num_sda, pin_num_scl); - -#if defined(USING_M5HAL) -#pragma message "Using M5HAL" - // Using M5HAL - m5::hal::bus::I2CBusConfig i2c_cfg; - i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); - i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); - auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); - M5_LOGI("Bus:%d", i2c_bus.has_value()); - if (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } -#else -#pragma message "Using Wire" - // Using TwoWire - Wire.begin(pin_num_sda, pin_num_scl, 400000U); - if (!Units.add(unit, Wire) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } -#endif - - M5_LOGI("M5UnitUnified has been begun"); - M5_LOGI("%s", Units.debugInfo().c_str()); - - monitor.setSamplingRate(unit.caluculateSamplingRate()); - - lcd.clear(TFT_DARKGREEN); -} - -void loop() -{ - M5.update(); - Units.update(); - if (unit.updated()) { - // WARNING - // If overflow is occurring, the sampling rate should be reduced because the processing is not up to par - if (unit.overflow()) { - M5_LOGW("OVERFLOW:%u", unit.overflow()); - } - - bool beat{}; - // MAX30100 is equipped with a FIFO, so multiple data may be stored - while (unit.available()) { - M5.Log.printf(">IR:%u\n>RED:%u\n", unit.ir(), unit.red()); - monitor.push_back(unit.ir(), unit.red()); // Push back the oldest data - M5.Log.printf(">MIR:%f\n", monitor.latestIR()); - monitor.update(); - beat |= monitor.isBeat(); - unit.discard(); // Discard the oldest data - } - M5.Log.printf(">BPM:%f\n>SpO2:%f\n>BEAT:%u\n", monitor.bpm(), monitor.SpO2(), beat); - } - - // Measure tempeature - if (M5.BtnA.wasClicked()) { - TemperatureData td{}; - if (unit.measureTemperatureSingleshot(td)) { - M5.Log.printf(">Temp:%f\n", td.celsius()); - } - } -} diff --git a/platformio.ini b/platformio.ini index 6d85f7d..205d412 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,15 +10,14 @@ lib_ldf_mode = deep test_framework = googletest test_build_src = true lib_deps=m5stack/M5Unified - m5stack/M5UnitUnified@>=0.1.0 + m5stack/M5UnitUnified@>=0.3.0 -; -------------------------------- [m5base] monitor_speed = 115200 monitor_filters = esp32_exception_decoder, time upload_speed = 1500000 test_speed = 115200 -test_ignore= native/* +test_ignore= native/* [Core] extends = m5base @@ -102,6 +101,11 @@ extends = m5base board = m5stack-coreink lib_deps = ${env.lib_deps} +[NessoN1] +extends = m5base +board = arduino_nesso_n1 +lib_deps = ${env.lib_deps} + [sdl] build_flags = -O3 -xc++ -std=c++14 -lSDL2 -arch arm64 ; for arm mac @@ -117,9 +121,14 @@ lib_deps = ${env.lib_deps} ; -------------------------------- ;Choose framework [arduino_latest] +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 @@ -136,9 +145,9 @@ framework = arduino platform = espressif32 @ 4.4.0 framework = arduino -;[arduino_3_5_0] -;platform = espressif32 @ 3.5.0 -;framework = arduino +[pioarduino_latest] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip@55.3.36 +framework = arduino [esp-idf] platform = espressif32 @ 6.8.1 @@ -241,7 +250,7 @@ lib_deps = ${AtomS3R.lib_deps} test_filter= embedded/test_max30100 [env:test_UnitHeart_NanoC6] -extends=NanoC6, option_release, arduino_latest +extends=NanoC6, option_release lib_deps = ${NanoC6.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30100 @@ -292,91 +301,126 @@ test_filter= embedded/test_max30102 ;PlotToSerail [env:UnitHeart_PlotToSerial_Core_Arduino_latest] extends=Core, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Core_Arduino_5_4_0] extends=Core, option_release, arduino_5_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Core_Arduino_4_4_0] extends=Core, option_release, arduino_4_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Core2_Arduino_latest] extends=Core2, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Core2_Arduino_5_4_0] extends=Core2, option_release, arduino_5_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Core2_Arduino_4_4_0] extends=Core2, option_release, arduino_4_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_CoreS3_Arduino_latest] extends=CoreS3, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_StampS3_Arduino_latest] extends=StampS3, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_AtomMatrix_Arduino_latest] extends=AtomMatrix, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_AtomS3_Arduino_latest] extends=AtomS3, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_AtomS3R_Arduino_latest] extends=AtomS3R, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Dial_Arduino_latest] extends=Dial, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_NanoC6_Arduino_latest] -extends=NanoC6, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +extends=NanoC6, option_release +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_StickCPlus_Arduino_latest] extends=StickCPlus, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Paper_Arduino_latest] extends=Paper, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_CoreInk_Arduino_latest] extends=CoreInk, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Fire_Arduino_latest] extends=Fire, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART -[env:UnitHeart_PlotToSerial_Fire_Arduino_5_4_0] -extends=Fire, option_release, arduino_5_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> - -[env:UnitHeart_PlotToSerial_Fire_Arduino_4_4_0] -extends=Fire, option_release, arduino_4_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/PlotToSerial> +[env:UnitHeart_PlotToSerial_NessoN1_Arduino_latest] +extends=NessoN1, option_release, pioarduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART ;GraphicalMeter ; For thisg samples, please refer to PlotToSerial to create env for your own environment ; こののサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください -; [env:UnitHeart_GraphicalMeter_Core_Arduino_latest] extends=Core, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/GraphicalMeter> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMeter> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART ;-------------------------------- ;HatHeart @@ -384,27 +428,41 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/UnitHeart/Gr ;PlotToSerial [env:HatHeart_PlotToSerial_StickCPlus_Arduino_latest] extends=StickCPlus, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/HatHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_HEART [env:HatHeart_PlotToSerial_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/HatHeart/PlotToSerial> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_HEART + +[env:HatHeart_PlotToSerial_CoreInk_Arduino_latest] +extends=CoreInk, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_HEART + +[env:HatHeart_PlotToSerial_NessoN1_Arduino_latest] +extends=NessoN1, option_release, pioarduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_HAT_HEART ;GraphicalMeter -[env:HatHeart_GraphicalMeter_StickCPlus_Arduino_latest] -extends=StickCPlus, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/HatHeart/GraphicalMeter> - +; For thisg samples, please refer to PlotToSerial to create env for your own environment +; こののサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください [env:HatHeart_GraphicalMeter_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/HatHeart/GraphicalMeter> +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMeter> +build_flags = ${option_release.build_flags} + -D USING_HAT_HEART -;DualSensor +;-------------------------------- +;DualSensor (Using Unit and Hat) +;-------------------------------- ;Example of using M5UnitUnified to connect both UnitHeart and HatHeart -[env:DualSensor_StickCPlus_Arduino_latest] -extends=StickCPlus, option_release, arduino_latest -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> - [env:DualSensor_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> diff --git a/src/utility/pulse_monitor.cpp b/src/utility/pulse_monitor.cpp index 362ac6a..0e9821b 100644 --- a/src/utility/pulse_monitor.cpp +++ b/src/utility/pulse_monitor.cpp @@ -54,6 +54,12 @@ void PulseMonitor::push_back(const float ir, const float red) _sumredrms += (red - _avered) * (red - _avered); _sumirrms += (ir - _aveir) * (ir - _aveir); if (++_count == (uint32_t)_sampling_rate) { + const float eps = 1e-6f; + if (std::fabs(_avered) < eps || std::fabs(_aveir) < eps) { + _sumredrms = _sumirrms = 0; + _count = 0; + return; + } float R = (std::sqrt(_sumredrms) / _avered) / (std::sqrt(_sumirrms) / _aveir); _SpO2 = -23.3f * (R - 0.4f) + 100; _SpO2 = std::fmax(std::fmin(100.0f, _SpO2), 80.0f); // clamp 80-100 @@ -69,6 +75,10 @@ void PulseMonitor::update() float PulseMonitor::calculate_bpm() { + if (_dataIR.size() < 3) { + _beat = false; + return 0.0f; + } std::vector peaks; float threshold = 50.f; bool negatived{}; From ba75339f82c2cacc5d7a1f74604e044e1148ef15 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 16:09:47 +0900 Subject: [PATCH 02/40] Fixes workflow --- .../workflows/arduino-esp-v2-build-check.yml | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index cb55c34..8b51659 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -46,7 +46,7 @@ concurrency: jobs: build: - name: ${{ matrix.unit }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} + name: ${{ matrix.build-properties }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest timeout-minutes: 5 @@ -60,8 +60,8 @@ jobs: sketch: - PlotToSerial - unit: - - UnitHeart + build-properties: + - "-DUSING_UNIT_HEART" board: - m5stack-atom @@ -84,12 +84,26 @@ jobs: include: # Specific sketches - sketch: GraphicalMeter - unit: UnitHeart platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 platform-version: 2.0.17 board: m5stack-core-esp32 + build-properties:"-DUSING_UNIT_HEART" + - sketch: PlotToSerial + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform: esp32 + archi: esp32 + platform-version: 2.0.17 + board: m5stack-coreink + build-properties:"-DUSING_HAT_HEART" + - sketch: DualSensor + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform: esp32 + archi: esp32 + platform-version: 2.0.17 + board: m5stack-coreink + steps: - name: Checkout @@ -106,7 +120,7 @@ jobs: platform-url: ${{ matrix.platform-url }} required-libraries: ${{ env.REQUIRED_LIBRARIES }} extra-arduino-cli-args: ${{ matrix.cli-args }} - #build-properties: ${{ toJson(matrix.build-properties) }} + build-properties: ${{ matrix.build-properties) } sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }} + sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ #sketches-exclude: ${{ matrix.sketches-exclude }} From 6b64fbaa95e0a19b0a16401595e4599fe9470b64 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 16:13:23 +0900 Subject: [PATCH 03/40] Fixes workflow --- .github/workflows/arduino-esp-v2-build-check.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index 8b51659..6339ee2 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -48,11 +48,11 @@ jobs: build: name: ${{ matrix.build-properties }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 12 strategy: fail-fast: false - #max-parallel: 1 + max-parallel: 20 matrix: platform-url: - https://espressif.github.io/arduino-esp32/package_esp32_index.json @@ -89,14 +89,14 @@ jobs: archi: esp32 platform-version: 2.0.17 board: m5stack-core-esp32 - build-properties:"-DUSING_UNIT_HEART" + build-properties: "-DUSING_UNIT_HEART" - sketch: PlotToSerial platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 platform-version: 2.0.17 board: m5stack-coreink - build-properties:"-DUSING_HAT_HEART" + build-properties: "-DUSING_HAT_HEART" - sketch: DualSensor platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 @@ -108,8 +108,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} # Build - name: Compile examples @@ -120,7 +118,7 @@ jobs: platform-url: ${{ matrix.platform-url }} required-libraries: ${{ env.REQUIRED_LIBRARIES }} extra-arduino-cli-args: ${{ matrix.cli-args }} - build-properties: ${{ matrix.build-properties) } + build-properties: ${{ matrix.build-properties }} sketch-names: ${{ matrix.sketch }}.ino sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ #sketches-exclude: ${{ matrix.sketches-exclude }} From 7437a2409a71f21e05186ddee5eb25399b6248cf Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 16:17:52 +0900 Subject: [PATCH 04/40] Fixes compile error --- examples/UnitUnified/DualSensor/main/DualSensor.cpp | 1 + examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp | 4 ++-- examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/UnitUnified/DualSensor/main/DualSensor.cpp b/examples/UnitUnified/DualSensor/main/DualSensor.cpp index b5b5ace..830264c 100644 --- a/examples/UnitUnified/DualSensor/main/DualSensor.cpp +++ b/examples/UnitUnified/DualSensor/main/DualSensor.cpp @@ -56,6 +56,7 @@ void setup() m5cfg.internal_imu = false; // Disable internal IMU m5cfg.internal_rtc = false; // Disable internal RTC M5.begin(m5cfg); + const auto board_type = M5.getBoard(); const auto pins = get_hat_i2c_pins(board_type); diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp index f4a23db..751da5b 100644 --- a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -84,13 +84,13 @@ void setup() M5.begin(m5cfg); M5.setTouchButtonHeightByRatio(100); - const auto board = M5.getBoard(); - // The screen shall be in landscape mode if (lcd.height() > lcd.width()) { lcd.setRotation(1); } + auto board = M5.getBoard(); + #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index 69bbd39..4b71986 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -92,6 +92,8 @@ void setup() lcd.setRotation(1); } + auto board = M5.getBoard(); + #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); @@ -128,7 +130,7 @@ void setup() 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); // For NessoN1 GROVE - if (M5.getBoard() == m5::board_t::board_ArduinoNessoN1) { + if (board == m5::board_t::board_ArduinoNessoN1) { // Port A of the NessoN1 is QWIIC, then use portB (GROVE) pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); From c4cfd459b20b83f7a987cbc0bdf2233249f52b53 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 16:50:44 +0900 Subject: [PATCH 05/40] Fixes workflows --- .../workflows/arduino-esp-v3-build-check.yml | 72 ++++++++------- .github/workflows/arduino-m5-build-check.yml | 89 +++++++++++-------- .github/workflows/platformio-build-check.yml | 63 ++++--------- 3 files changed, 108 insertions(+), 116 deletions(-) diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 345d533..0ae0ee2 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -46,13 +46,13 @@ concurrency: jobs: build: - name: ${{ matrix.unit }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} + name: ${{ matrix.build-properties }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 12 strategy: fail-fast: false - #max-parallel: 1 + max-parallel: 20 matrix: platform-url: - https://espressif.github.io/arduino-esp32/package_esp32_index.json @@ -60,19 +60,21 @@ jobs: sketch: - PlotToSerial - unit: - - UnitHeart + build-properties: + - "-DUSING_UNIT_HEART" board: + - arduino_nesso_n1 - m5stack_atom - m5stack_atoms3 - m5stack_capsule -# - m5stack_cardputer + - m5stack_cardputer - m5stack_core - m5stack_core2 - m5stack_coreink - m5stack_cores3 - m5stack_dial + - m5stack-dinmeter - m5stack_fire - m5stack_nanoc6 - m5stack_paper @@ -84,13 +86,14 @@ jobs: # - m5stack_stickc - m5stack_stickc_plus - m5stack_stickc_plus2 + - m5stack_tab5 # - m5stack_timer_cam # - m5stack_tough # - m5stack_unit_cam # - m5stack_unit_cams3 platform-version: - - 3.0.4 + - 3.3.6 platform: - esp32 @@ -100,59 +103,60 @@ jobs: include: # Specific sketches + # HAT - sketch: PlotToSerial - unit: HatHeart platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 - platform-version: 3.0.4 + platform-version: 3.3.6 board: m5stack_stickc_plus + build-properties: "-DUSING_HAT_HEART" - sketch: PlotToSerial - unit: HatHeart platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 - platform-version: 3.0.4 + platform-version: 3.3.6 board: m5stack_stickc_plus2 - - sketch: GraphicalMeter - unit: UnitHeart + build-properties: "-DUSING_HAT_HEART" + - sketch: PlotToSerial platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 - platform-version: 3.0.4 + platform-version: 3.3.6 + board: m5stack_coreink + build-properties: "-DUSING_HAT_HEART" + - sketch: PlotToSerial + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform: esp32 + archi: esp32 + platform-version: 3.3.6 + board: arduino_nesso_n1 + build-properties: "-DUSING_HAT_HEART" + #GraphicalMeter + - sketch: GraphicalMeter + platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platform: esp32 + archi: esp32 + platform-version: 3.3.6 board: m5stack_core + build-properties: "-DUSING_UNIT_HEART" - sketch: GraphicalMeter - unit: HatHeart platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 - platform-version: 3.0.4 - board: m5stack_stickc_plus - - sketch: GraphicalMeter - unit: HatHeart - platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json - platform: esp32 - archi: esp32 - platform-version: 3.0.4 + platform-version: 3.3.6 board: m5stack_stickc_plus2 + build-properties: "-DUSING_HAT_HEART" - sketch: DualSensor platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json platform: esp32 archi: esp32 - platform-version: 3.0.4 - board: m5stack_stickc_plus - - sketch: DualSensor - platform-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json - platform: esp32 - archi: esp32 - platform-version: 3.0.4 + platform-version: 3.3.6 board: m5stack_stickc_plus2 steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} # Build - name: Compile examples @@ -163,7 +167,7 @@ jobs: platform-url: ${{ matrix.platform-url }} required-libraries: ${{ env.REQUIRED_LIBRARIES }} extra-arduino-cli-args: ${{ matrix.cli-args }} - #build-properties: ${{ toJson(matrix.build-properties) }} + build-properties: ${{ matrix.build-properties }} sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }} + sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ #sketches-exclude: ${{ matrix.sketches-exclude }} diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index f779b63..de297a3 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -46,13 +46,13 @@ concurrency: jobs: build: - name: ${{ matrix.unit }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} + name: ${{ matrix.build-properties }}:${{ matrix.sketch }}:${{matrix.board}}@${{matrix.platform-version}} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 12 strategy: fail-fast: false -# max-parallel: 1 + max-parallel: 20 matrix: platform-url: - https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json @@ -60,15 +60,17 @@ jobs: sketch: - PlotToSerial - unit: - - UnitHeart + build-properties: + - "-DUSING_UNIT_HAERT" board: + - arduino_nesso_n1 - m5stack_atom - m5stack_atoms3 - m5stack_atoms3r - m5stack_capsule -# - m5stack_cardputer + - m5stack_cardputer +# - m5stack_chain_dualkey - m5stack_core - m5stack_core2 - m5stack_coreink @@ -76,22 +78,30 @@ jobs: - m5stack_dial - m5stack_dinmeter - m5stack_fire + - m5stack_nano_c6 +# - m5stack_nano_h2 - m5stack_paper +# - m5stack_papers3 # - m5stack_poe_cam +# - m5stack_powerhub # - m5stack_stamp_c3 # - m5stack_stamp_pico - m5stack_stamp_s3 +# - m5stack_stamplc # - m5stack_station -# - m5stack_stickc + - m5stack_stickc - m5stack_stickc_plus - m5stack_stickc_plus2 + - m5stack_sticks3 + - m5stack_tab5 # - m5stack_timer_cam # - m5stack_tough +# - m5stack_unit_c6l # - m5stack_unit_cam # - m5stack_unit_cams3 platform-version: - - 2.1.2 + - 3.2.5 platform: - m5stack @@ -101,59 +111,68 @@ jobs: include: # Specific sketches + # HAT - sketch: PlotToSerial - unit: HatHeart platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json platform: m5stack archi: esp32 - platform-version: 2.1.2 + platform-version: 3.2.5 board: m5stack_stickc_plus + build-properties: "-DUSING_HAT_HEART" - sketch: PlotToSerial - unit: HatHeart platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json platform: m5stack archi: esp32 - platform-version: 2.1.2 + platform-version: 3.2.5 board: m5stack_stickc_plus2 - - sketch: GraphicalMeter - unit: UnitHeart + build-properties: "-DUSING_HAT_HEART" + - sketch: PlotToSerial platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json platform: m5stack archi: esp32 - platform-version: 2.1.2 + platform-version: 3.2.5 + board: m5stack_sticks3 + build-properties: "-DUSING_HAT_HEART" + - sketch: PlotToSerial + 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_coreink + build-properties: "-DUSING_HAT_HEART" + - sketch: PlotToSerial + 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: arduino_nesso_n1 + build-properties: "-DUSING_HAT_HEART" + # GraphicalMeter + - sketch: GraphicalMeter + 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_core + build-properties: "-DUSING_UNIT_HEART" - sketch: GraphicalMeter - unit: HatHeart platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json platform: m5stack archi: esp32 - platform-version: 2.1.2 - board: m5stack_stickc_plus - - sketch: GraphicalMeter - unit: HatHeart - platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json - platform: m5stack - archi: esp32 - platform-version: 2.1.2 + platform-version: 3.2.5 board: m5stack_stickc_plus2 + build-properties: "-DUSING_HAT_HEART" + # DualSensor - sketch: DualSensor platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json platform: m5stack archi: esp32 - platform-version: 2.1.2 - board: m5stack_stickc_plus - - sketch: DualSensor - platform-url: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json - platform: m5stack - archi: esp32 - platform-version: 2.1.2 + platform-version: 3.2.5 board: m5stack_stickc_plus2 steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} # Build - name: Compile examples @@ -164,8 +183,8 @@ jobs: platform-url: ${{ matrix.platform-url }} required-libraries: ${{ env.REQUIRED_LIBRARIES }} extra-arduino-cli-args: ${{ matrix.cli-args }} - #build-properties: ${{ toJson(matrix.build-properties) }} + build-properties: ${{ matrix.build-properties }} sketch-names: ${{ matrix.sketch }}.ino - sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/${{ matrix.unit }} + sketch-names-find-start: ${{ env.SKETCH_NAMES_FIND_START }}/ #sketches-exclude: ${{ matrix.sketches-exclude }} diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 6332066..181854f 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -17,7 +17,7 @@ on: - 'examples/UnitUnified/**.hpp' - 'examples/UnitUnified/**.h' - 'examples/UnitUnified/**.c' - - '/platformio-build-check.yml' + - '**/platformio-build-check.yml' - '**platformio.ini' pull_request: paths: @@ -46,11 +46,11 @@ jobs: build: name: ${{ matrix.unit }}:${{ matrix.example }}@${{ matrix.board }}:${{ matrix.framework }}:${{ matrix.espressif32 }} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 12 strategy: fail-fast: false - #max-parallel: 1 + max-parallel: 20 matrix: example: @@ -80,41 +80,10 @@ jobs: espressif32: - latest -# - '5_4_0' -# - '4_4_0' - -# exclude: -# - board: CoreS3 -# espressif32: '5_4_0' -# - board: CoreS3 -# espressif32: '4_4_0' -# - board: StampS3 -# espressif32: '5_4_0' -# - board: StampS3 -# espressif32: '4_4_0' -# - board: AtomS3 -# espressif32: '5_4_0' -# - board: AtomS3 -# espressif32: '4_4_0' -# - board: Dial -# espressif32: '5_4_0' -# - board: Dial -# espressif32: '4_4_0' -# - board: NanoC6 -# espressif32: '5_4_0' -# - board: NanoC6 -# espressif32: '4_4_0' -# - board: StickCPlus -# espressif32: '5_4_0' -# - board: StickCPlus -# espressif32: '4_4_0' -# - board: Paper -# espressif32: '5_4_0' -# - board: Paper -# espressif32: '4_4_0' include: # Specific sketches + # Hat - example: PlotToSerial unit: HatHeart board: StickCPlus @@ -125,36 +94,36 @@ jobs: board: StickCPlus2 framework: Arduino espressif32: latest + - example: PlotToSerial + unit: HatHeart + board: CoreInk + framework: Arduino + espressif32: latest + - example: PlotToSerial + unit: HatHeart + board: NessoN1 + framework: Arduino + espressif32: latest + # GraphicalMeter - example: GraphicalMeter unit: UnitHeart board: Core framework: Arduino espressif32: latest - - example: GraphicalMeter - unit: HatHeart - board: StickCPlus - framework: Arduino - espressif32: latest - example: GraphicalMeter unit: HatHeart board: StickCPlus2 framework: Arduino espressif32: latest - - example: DualSensor - board: StickCPlus - framework: Arduino - espressif32: latest + # DualSensor - example: DualSensor board: StickCPlus2 framework: Arduino espressif32: latest - steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - name: Build examples uses: karniv00l/platformio-run-action@v1 From e27c105914c93da2eda8ed969446a6af844acb72 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 17:26:44 +0900 Subject: [PATCH 06/40] Fixes typo --- .../DualSensor/main/DualSensor.cpp | 4 +- .../GraphicalMeter/main/GraphicalMeter.cpp | 2 +- .../PlotToSerial/main/PlotToSerial.cpp | 4 +- src/unit/unit_MAX30100.cpp | 18 ++++----- src/unit/unit_MAX30100.hpp | 32 +++++++++++---- src/unit/unit_MAX30102.cpp | 20 +++++----- src/unit/unit_MAX30102.hpp | 40 +++++++++++++------ src/utility/pulse_monitor.hpp | 14 +++---- test/embedded/test_max30100/max30100_test.cpp | 18 ++++----- test/embedded/test_max30102/max30102_test.cpp | 20 +++++----- 10 files changed, 102 insertions(+), 70 deletions(-) diff --git a/examples/UnitUnified/DualSensor/main/DualSensor.cpp b/examples/UnitUnified/DualSensor/main/DualSensor.cpp index 830264c..54fa228 100644 --- a/examples/UnitUnified/DualSensor/main/DualSensor.cpp +++ b/examples/UnitUnified/DualSensor/main/DualSensor.cpp @@ -123,8 +123,8 @@ void setup() view[0] = new View(lcd.width() >> 1, lcd.height(), true); view[1] = new View(lcd.width() >> 1, lcd.height(), false); - view[0]->_monitor.setSamplingRate(unit.caluculateSamplingRate()); - view[1]->_monitor.setSamplingRate(hat.caluculateSamplingRate()); + view[0]->_monitor.setSamplingRate(unit.calculateSamplingRate()); + view[1]->_monitor.setSamplingRate(hat.calculateSamplingRate()); view[0]->push(&lcd, lcd.width() >> 1, 0); view[1]->push(&lcd, 0, 0); } diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp index 751da5b..1c78661 100644 --- a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -161,7 +161,7 @@ void setup() #else view = new View(lcd.width(), lcd.height(), false); #endif - view->_monitor.setSamplingRate(heart.caluculateSamplingRate()); + view->_monitor.setSamplingRate(heart.calculateSamplingRate()); view->push(&lcd, 0, 0); M5_LOGI("periodic:%d", heart.inPeriodic()); diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index 4b71986..c44ac23 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -181,7 +181,7 @@ void setup() } #endif - monitor.setSamplingRate(unit.caluculateSamplingRate()); + monitor.setSamplingRate(unit.calculateSamplingRate()); lcd.clear(TFT_DARKGREEN); } @@ -210,7 +210,7 @@ void loop() M5.Log.printf(">BPM:%f\n>SpO2:%f\n>BEAT:%u\n", monitor.bpm(), monitor.SpO2(), beat); } - // Measure tempeature + // Measure temperature if (M5.BtnA.wasClicked()) { TemperatureData td{}; if (unit.measureTemperatureSingleshot(td)) { diff --git a/src/unit/unit_MAX30100.cpp b/src/unit/unit_MAX30100.cpp index d4adb20..be34e13 100644 --- a/src/unit/unit_MAX30100.cpp +++ b/src/unit/unit_MAX30100.cpp @@ -55,7 +55,7 @@ inline bool is_allowed_settings(const Mode mode, const Sampling rate, const LEDP } // Calculate the interval per data -inline uint32_t caluculate_interval_time(const Sampling rate) +inline uint32_t calculate_interval_time(const Sampling rate) { return std::floor(1000.f / sr_table[m5::stl::to_underlying(rate)]); } @@ -219,7 +219,7 @@ bool UnitMAX30100::start_periodic_measurement() _periodic = writeShutdownControl(false) && resetFIFO(); if (_periodic) { _latest = 0; - _interval = caluculate_interval_time(rate); + _interval = calculate_interval_time(rate); // M5_LIB_LOGE(">>>>R: Rate:%u IT:%u", rate, _interval); // _mask = adc_resolution_bits_table[m5::stl::to_underlying(width)]; return true; @@ -435,7 +435,7 @@ bool UnitMAX30100::reset() if (read_register8(MODE_CONFIGURATION, mc.value) && !mc.reset()) { _periodic = false; _mode = mc.mode(); - _retrived = _overflow = 0; + _retrieved = _overflow = 0; return true; } m5::utility::delay(1); @@ -448,7 +448,7 @@ bool UnitMAX30100::reset() bool UnitMAX30100::read_FIFO() { uint8_t wptr{}, rptr{}; - _retrived = _overflow = 0; + _retrieved = _overflow = 0; if (!read_register8(FIFO_WRITE_POINTER, wptr) || !read_register8(FIFO_READ_POINTER, rptr) || !read_register8(FIFO_OVERFLOW_COUNTER, _overflow)) { @@ -493,7 +493,7 @@ bool UnitMAX30100::read_FIFO() } left -= batch_len; } - _retrived = readCount; + _retrieved = readCount; } #else @@ -504,10 +504,10 @@ bool UnitMAX30100::read_FIFO() return false; } _data->push_back(d); - ++_retrived; + ++_retrieved; } #endif - return (_retrived != 0); + return (_retrieved != 0); } bool UnitMAX30100::read_measurement_temperature(max30100::TemperatureData& td) @@ -521,11 +521,11 @@ bool UnitMAX30100::readRevisionID(uint8_t& rev) return read_register8(READ_REVISION_ID, rev); } -uint32_t UnitMAX30100::caluculateSamplingRate() +uint32_t UnitMAX30100::calculateSamplingRate() { Sampling rate{}; if (readSpO2SamplingRate(rate)) { - return 1000 / caluculate_interval_time(rate); + return 1000 / calculate_interval_time(rate); } return 0; } diff --git a/src/unit/unit_MAX30100.hpp b/src/unit/unit_MAX30100.hpp index a597133..d0baa32 100644 --- a/src/unit/unit_MAX30100.hpp +++ b/src/unit/unit_MAX30100.hpp @@ -199,12 +199,12 @@ public: ///@name Settings for begin ///@{ - /*! @brief Gets the configration */ + /*! @brief Gets the configuration */ inline config_t config() { return _cfg; } - //! @brief Set the configration + //! @brief Set the configuration inline void config(const config_t& cfg) { _cfg = cfg; @@ -229,9 +229,17 @@ public: accumulated @sa available() */ - inline uint8_t retrived() const + inline uint8_t retrieved() const { - return _retrived; + return _retrieved; + } + /*! + @brief Deprecated alias of retrieved() + @deprecated Use retrieved() instead. + */ + [[deprecated("Please use retrieved()")]] inline uint8_t retrived() const + { + return retrieved(); } /*! @brief The number of samples lost @@ -248,7 +256,15 @@ public: @return >= 0 Sampling rate @note Calculate by SpO2 sampling rate */ - uint32_t caluculateSamplingRate(); + uint32_t calculateSamplingRate(); + /*! + @brief Deprecated alias of calculateSamplingRate() + @deprecated Use calculateSamplingRate() instead. + */ + [[deprecated("Please use calculateSamplingRate()")]] inline uint32_t caluculateSamplingRate() + { + return calculateSamplingRate(); + } ///@name Periodic measurement ///@{ @@ -407,7 +423,7 @@ public: ///@name LED Configuration ///@{ /*! - @brief Read the LED curremt + @brief Read the LED current @param[out] ir_current IR current @param[out] red_current Red current @return True if successful @@ -425,7 +441,7 @@ public: ///@name Measurement temperature ///@{ /*! - @brief Measure tempeature single shot + @brief Measure temperature single shot @param[out] td TemperatureData @return True if successful @warning Blocking until measured about 29 ms @@ -509,7 +525,7 @@ protected: protected: max30100::Mode _mode{max30100::Mode::None}; - uint8_t _retrived{}, _overflow{}; + uint8_t _retrieved{}, _overflow{}; std::unique_ptr> _data{}; config_t _cfg{}; diff --git a/src/unit/unit_MAX30102.cpp b/src/unit/unit_MAX30102.cpp index 5185397..b5e3b60 100644 --- a/src/unit/unit_MAX30102.cpp +++ b/src/unit/unit_MAX30102.cpp @@ -138,7 +138,7 @@ constexpr uint32_t adc_resolution_bits_table[] = { }; // Calculate the interval per data -inline uint32_t caluculate_interval_time(const FIFOSampling avg, const Sampling rate) +inline uint32_t calculate_interval_time(const FIFOSampling avg, const Sampling rate) { float freq = sampling_rate_table[m5::stl::to_underlying(rate)] / (float)average_table[m5::stl::to_underlying(avg)]; @@ -261,7 +261,7 @@ void UnitMAX30102::update(const bool force) if (inPeriodic()) { auto at = m5::utility::millis(); if (force || !_latest || at >= _latest + _interval) { - _updated = (read_FIFO() && _retrived); + _updated = (read_FIFO() && _retrieved); if (_updated) { _latest = m5::utility::millis(); } @@ -287,7 +287,7 @@ bool UnitMAX30102::start_periodic_measurement() writeShutdownControl(false) && resetFIFO(); if (_periodic) { _latest = 0; - _interval = caluculate_interval_time(avg, rate); + _interval = calculate_interval_time(avg, rate); _mask = adc_resolution_bits_table[m5::stl::to_underlying(width)]; // M5_LIB_LOGI(">>> AVG:%u SR:%u => interval:%u WID:%u => mask:%0x", avg, rate, _interval, width, _mask); @@ -606,7 +606,7 @@ bool UnitMAX30102::reset_FIFO(const bool circling_read_ptr) bool UnitMAX30102::read_FIFO() { uint8_t rptr{}, wptr{}; - _retrived = _overflow = 0; + _retrieved = _overflow = 0; if (!readFIFOReadPointer(rptr) || !readFIFOWritePointer(wptr) || !readFIFOOverflowCounter(_overflow)) { M5_LIB_LOGE("Failed to read ptrs"); @@ -673,7 +673,7 @@ bool UnitMAX30102::read_FIFO() } left -= batch_len; } - _retrived = readCount; + _retrieved = readCount; } #else while (readCount--) { @@ -683,11 +683,11 @@ bool UnitMAX30102::read_FIFO() return false; } _data->push_back(d); - ++_retrived; + ++_retrieved; } #endif - return (_retrived != 0); + return (_retrieved != 0); } bool UnitMAX30102::reset() @@ -700,7 +700,7 @@ bool UnitMAX30102::reset() if (read_register8(MODE_CONFIGURATION, mc.value) && !mc.reset()) { _periodic = false; _mode = mc.mode(); - _retrived = _overflow = 0; + _retrieved = _overflow = 0; _slot[0] = _slot[1] = Slot::None; return true; } @@ -716,7 +716,7 @@ bool UnitMAX30102::readRevisionID(uint8_t& rev) return read_register8(READ_REVISION_ID, rev); } -uint32_t UnitMAX30102::caluculateSamplingRate() +uint32_t UnitMAX30102::calculateSamplingRate() { FIFOSampling avg{}; bool rollover{}; @@ -724,7 +724,7 @@ uint32_t UnitMAX30102::caluculateSamplingRate() Sampling rate{}; if (readFIFOConfiguration(avg, rollover, almostFull) && readSpO2SamplingRate(rate)) { - return 1000 / caluculate_interval_time(avg, rate); + return 1000 / calculate_interval_time(avg, rate); } return 0; } diff --git a/src/unit/unit_MAX30102.hpp b/src/unit/unit_MAX30102.hpp index 8097d5a..473fb9f 100644 --- a/src/unit/unit_MAX30102.hpp +++ b/src/unit/unit_MAX30102.hpp @@ -233,12 +233,12 @@ public: ///@name Settings for begin ///@{ - /*! @brief Gets the configration */ + /*! @brief Gets the configuration */ inline config_t config() { return _cfg; } - //! @brief Set the configration + //! @brief Set the configuration inline void config(const config_t& cfg) { _cfg = cfg; @@ -262,9 +262,17 @@ public: @note The number of data retrieved by the latest update, not all data accumulated @sa available() */ - inline uint8_t retrived() const + inline uint8_t retrieved() const { - return _retrived; + return _retrieved; + } + /*! + @brief Deprecated alias of retrieved() + @deprecated Use retrieved() instead. + */ + [[deprecated("Please use retrieved()")]] inline uint8_t retrived() const + { + return retrieved(); } /*! @brief The number of samples lost @@ -281,7 +289,15 @@ public: @return >= 0 Sampling rate @note Calculate by FIFO average and SpO2 sampling rate */ - uint32_t caluculateSamplingRate(); + uint32_t calculateSamplingRate(); + /*! + @brief Deprecated alias of calculateSamplingRate() + @deprecated Use calculateSamplingRate() instead. + */ + [[deprecated("Please use calculateSamplingRate()")]] inline uint32_t caluculateSamplingRate() + { + return calculateSamplingRate(); + } ///@name Periodic measurement ///@{ @@ -383,7 +399,7 @@ public: ///@{ /*! @brief Read the SpO2 configuration - @param[out] range ADC rRange + @param[out] range ADC range @param[out] rate Sampling rate @param[out] width LED pulse width @return True if successful @@ -412,7 +428,7 @@ public: } /*! @brief Write the SpO2 configuration - @param range ADC rRange + @param range ADC range @param rate Sampling rate @param width LED pulse width @return True if successful @@ -483,7 +499,7 @@ public: ///@name Multi-LED Mode Control ///@{ /*! - @brief Read the the MultiLED Mode form Slot 1-2 + @brief Read the MultiLED Mode from Slot 1-2 @param[out] slot1 Slot1 mode @param[out] slot2 Slot2 mode @return True if successful @@ -503,7 +519,7 @@ public: ///@name Measurement temperature ///@{ /*! - @brief Measure tempeature single shot + @brief Measure temperature single shot @param[out] td TemperatureData @return True if successful @warning Blocking until measured about 29 ms @@ -518,7 +534,7 @@ public: /*! @brief Read the FIFO configuration @param[out] avg FIFO sampling average - @param[out] rolllover FIFO Rolls on Full if true + @param[out] rollover FIFO Rolls on Full if true @param[out] almostFull FIFO Almost Full Value for interrupt @return True if successful */ @@ -526,7 +542,7 @@ public: /*! @brief Write the FIFO configuration @param avg FIFO sampling average - @param rolllover FIFO Rolls on Full if true + @param rollover FIFO Rolls on Full if true @param almostFull FIFO Almost Full Value for interrupt @return True if successful @warning During periodic detection runs, an error is returned @@ -618,7 +634,7 @@ protected: protected: std::unique_ptr> _data{}; max30102::Mode _mode{}; - uint8_t _retrived{}, _overflow{}; + uint8_t _retrieved{}, _overflow{}; uint32_t _mask{}; // Valid bits based on ADC range max30102::Slot _slot[2]{}; config_t _cfg{}; diff --git a/src/utility/pulse_monitor.hpp b/src/utility/pulse_monitor.hpp index 86cb60c..78f10ba 100644 --- a/src/utility/pulse_monitor.hpp +++ b/src/utility/pulse_monitor.hpp @@ -18,8 +18,8 @@ namespace m5 { /*! - @namepsace heart - @brief Unit-HEART releated + @namespace heart + @brief Unit-HEART related */ namespace heart { @@ -96,7 +96,7 @@ private: class PulseMonitor { public: /*! - @brief Costructor + @brief Constructor @param samplingRate sampling rate @param sec Seconds of data to be stored */ @@ -106,7 +106,7 @@ public: _max_samples{(size_t)samplingRate * sec}, _filterIR(5.0f, samplingRate) { - assert(sec >= 1 && "sec must be greater or eaual than 1"); + assert(sec >= 1 && "sec must be greater or equal than 1"); assert(samplingRate >= 1.0f && "SamplingRate must be greater or equal than 1.0f"); } @@ -145,20 +145,20 @@ public: @brief Push back IR and RED @param ir IR data @param red RED data - @note Calclate SpO2 + @note Calculate SpO2 */ void push_back(const float ir, const float red); /*! @brief Update status - @note Calclate BPM + @note Calculate BPM */ void update(); //! @brief Clear inner data void clear(); - //! @brief Filterd latest ir value + //! @brief Filtered latest ir value inline float latestIR() const { return !_dataIR.empty() ? _dataIR.back() : std::numeric_limits::quiet_NaN(); diff --git a/test/embedded/test_max30100/max30100_test.cpp b/test/embedded/test_max30100/max30100_test.cpp index 7283f81..9135780 100644 --- a/test/embedded/test_max30100/max30100_test.cpp +++ b/test/embedded/test_max30100/max30100_test.cpp @@ -288,8 +288,8 @@ void test_periodic_spo2(UnitMAX30100* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrived:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), - // unit->retrived(), unit->overflow()); + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) EXPECT_FALSE(unit->empty()); @@ -378,8 +378,8 @@ void test_periodic_hr(UnitMAX30100* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrived:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), - // unit->retrived(), unit->overflow()); + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) EXPECT_FALSE(unit->empty()); @@ -618,7 +618,7 @@ TEST_P(TestMAX30100, Periodic) } while (!unit->updated() && m5::utility::millis() - start_at <= 1000); EXPECT_TRUE(unit->updated()); - // M5_LOGW("%u %u", unit->retrived(), unit->available()); + // M5_LOGW("%u %u", unit->retrieved(), unit->available()); EXPECT_FALSE(unit->full()); EXPECT_FALSE(unit->empty()); @@ -637,8 +637,8 @@ TEST_P(TestMAX30100, Periodic) EXPECT_TRUE(unit->updated()); EXPECT_GE(unit->available(), 10U); - auto retrived = unit->retrived(); - EXPECT_GT(retrived, 0U); + auto retrieved = unit->retrieved(); + EXPECT_GT(retrieved, 0U); EXPECT_FALSE(unit->full()); EXPECT_FALSE(unit->empty()); @@ -649,7 +649,7 @@ TEST_P(TestMAX30100, Periodic) unit->flush(); EXPECT_EQ(unit->available(), 0U); - EXPECT_EQ(unit->retrived(), retrived); // Not clear on flush + EXPECT_EQ(unit->retrieved(), retrieved); // Not clear on flush EXPECT_FALSE(unit->full()); EXPECT_TRUE(unit->empty()); @@ -660,7 +660,7 @@ TEST_P(TestMAX30100, Periodic) EXPECT_TRUE(unit->updated()); EXPECT_EQ(unit->available(), MAX_FIFO_DEPTH); - EXPECT_EQ(unit->retrived(), MAX_FIFO_DEPTH); + EXPECT_EQ(unit->retrieved(), MAX_FIFO_DEPTH); EXPECT_TRUE(unit->full()); EXPECT_FALSE(unit->empty()); EXPECT_GT(unit->overflow(), 0U); diff --git a/test/embedded/test_max30102/max30102_test.cpp b/test/embedded/test_max30102/max30102_test.cpp index 91d8cac..4982535 100644 --- a/test/embedded/test_max30102/max30102_test.cpp +++ b/test/embedded/test_max30102/max30102_test.cpp @@ -364,8 +364,8 @@ void test_periodic_spo2(UnitMAX30102* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrived:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), - // unit->retrived(), unit->overflow()); + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) EXPECT_FALSE(unit->empty()); @@ -482,8 +482,8 @@ void test_periodic_hr(UnitMAX30102* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrived:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), - // unit->retrived(), unit->overflow()); + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) EXPECT_FALSE(unit->empty()); @@ -554,8 +554,8 @@ void test_periodic_multi(UnitMAX30102* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrived:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), - // unit->retrived(), unit->overflow()); + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) EXPECT_FALSE(unit->empty()); @@ -952,8 +952,8 @@ TEST_P(TestMAX30102, Periodic) EXPECT_TRUE(unit->updated()); EXPECT_GE(unit->available(), 10U); - auto retrived = unit->retrived(); - EXPECT_GT(retrived, 0U); + auto retrieved = unit->retrieved(); + EXPECT_GT(retrieved, 0U); EXPECT_FALSE(unit->full()); EXPECT_FALSE(unit->empty()); @@ -964,7 +964,7 @@ TEST_P(TestMAX30102, Periodic) unit->flush(); EXPECT_EQ(unit->available(), 0U); - EXPECT_EQ(unit->retrived(), retrived); // Not clear on flush + EXPECT_EQ(unit->retrieved(), retrieved); // Not clear on flush EXPECT_FALSE(unit->full()); EXPECT_TRUE(unit->empty()); @@ -975,7 +975,7 @@ TEST_P(TestMAX30102, Periodic) EXPECT_TRUE(unit->updated()); EXPECT_EQ(unit->available(), MAX_FIFO_DEPTH); - EXPECT_EQ(unit->retrived(), MAX_FIFO_DEPTH); + EXPECT_EQ(unit->retrieved(), MAX_FIFO_DEPTH); EXPECT_TRUE(unit->full()); EXPECT_FALSE(unit->empty()); EXPECT_GT(unit->overflow(), 0U); From 090decc04829a35db30b22ef85f387f342e3fb6d Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 17:58:53 +0900 Subject: [PATCH 07/40] Fixes workflows --- .github/workflows/arduino-esp-v3-build-check.yml | 2 +- .github/workflows/arduino-m5-build-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 0ae0ee2..ed20213 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -74,7 +74,7 @@ jobs: - m5stack_coreink - m5stack_cores3 - m5stack_dial - - m5stack-dinmeter + - m5stack_dinmeter - m5stack_fire - m5stack_nanoc6 - m5stack_paper diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index de297a3..fec74fa 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -61,7 +61,7 @@ jobs: - PlotToSerial build-properties: - - "-DUSING_UNIT_HAERT" + - "-DUSING_UNIT_HEART" board: - arduino_nesso_n1 From fb53971c50fdf69deb73f53a5159e960cdf09645 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 18:31:19 +0900 Subject: [PATCH 08/40] Cosmetic change --- examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp | 2 -- src/unit/unit_MAX30100.cpp | 4 ++-- src/unit/unit_MAX30102.cpp | 4 ++-- test/embedded/test_max30100/max30100_test.cpp | 6 ++++-- test/embedded/test_max30102/max30102_test.cpp | 9 ++++++--- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index c44ac23..5f80d19 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -75,8 +75,6 @@ I2cPins get_hat_i2c_pins(const m5::board_t board) void setup() { - delay(1500); - auto m5cfg = M5.config(); #if defined(USING_HAT_HEART) m5cfg.pmic_button = false; // Disable BtnPWR diff --git a/src/unit/unit_MAX30100.cpp b/src/unit/unit_MAX30100.cpp index be34e13..7f31f61 100644 --- a/src/unit/unit_MAX30100.cpp +++ b/src/unit/unit_MAX30100.cpp @@ -433,8 +433,8 @@ bool UnitMAX30100::reset() auto timeout_at = m5::utility::millis() + 1000; do { if (read_register8(MODE_CONFIGURATION, mc.value) && !mc.reset()) { - _periodic = false; - _mode = mc.mode(); + _periodic = false; + _mode = mc.mode(); _retrieved = _overflow = 0; return true; } diff --git a/src/unit/unit_MAX30102.cpp b/src/unit/unit_MAX30102.cpp index b5e3b60..6351ce2 100644 --- a/src/unit/unit_MAX30102.cpp +++ b/src/unit/unit_MAX30102.cpp @@ -698,8 +698,8 @@ bool UnitMAX30102::reset() auto timeout_at = m5::utility::millis() + 1000; do { if (read_register8(MODE_CONFIGURATION, mc.value) && !mc.reset()) { - _periodic = false; - _mode = mc.mode(); + _periodic = false; + _mode = mc.mode(); _retrieved = _overflow = 0; _slot[0] = _slot[1] = Slot::None; return true; diff --git a/test/embedded/test_max30100/max30100_test.cpp b/test/embedded/test_max30100/max30100_test.cpp index 9135780..45310ee 100644 --- a/test/embedded/test_max30100/max30100_test.cpp +++ b/test/embedded/test_max30100/max30100_test.cpp @@ -288,7 +288,8 @@ void test_periodic_spo2(UnitMAX30100* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * + // unit->interval(), // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) @@ -378,7 +379,8 @@ void test_periodic_hr(UnitMAX30100* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * + // unit->interval(), // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) diff --git a/test/embedded/test_max30102/max30102_test.cpp b/test/embedded/test_max30102/max30102_test.cpp index 4982535..cf21bc1 100644 --- a/test/embedded/test_max30102/max30102_test.cpp +++ b/test/embedded/test_max30102/max30102_test.cpp @@ -364,7 +364,8 @@ void test_periodic_spo2(UnitMAX30102* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * + // unit->interval(), // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) @@ -482,7 +483,8 @@ void test_periodic_hr(UnitMAX30102* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * + // unit->interval(), // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) @@ -554,7 +556,8 @@ void test_periodic_multi(UnitMAX30102* unit) EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * + // unit->interval(), // unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) From 3c1dd745b117f386a3259cbea4173349be2fd107 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 20:17:51 +0900 Subject: [PATCH 09/40] Fixes examples --- examples/UnitUnified/DualSensor/src/meter.hpp | 9 +- examples/UnitUnified/DualSensor/src/view.hpp | 21 ++++- .../GraphicalMeter/main/GraphicalMeter.cpp | 7 +- .../UnitUnified/GraphicalMeter/src/meter.hpp | 9 +- .../UnitUnified/GraphicalMeter/src/view.hpp | 86 ++++++++++++++++--- 5 files changed, 109 insertions(+), 23 deletions(-) diff --git a/examples/UnitUnified/DualSensor/src/meter.hpp b/examples/UnitUnified/DualSensor/src/meter.hpp index 1f8b506..0e50041 100644 --- a/examples/UnitUnified/DualSensor/src/meter.hpp +++ b/examples/UnitUnified/DualSensor/src/meter.hpp @@ -12,12 +12,15 @@ class Meter { public: - Meter(const uint32_t left, const uint32_t top, const uint32_t wid, const uint32_t hgt, m5gfx::rgb565_t tcolor) - : _left(left), _top(top), _theme_color(tcolor) + Meter(const uint32_t left, const uint32_t top, const uint32_t wid, const uint32_t hgt, m5gfx::rgb565_t tcolor, + m5gfx::rgb565_t gcolor = TFT_DARKGRAY, m5gfx::rgb565_t bcolor = TFT_BLACK) + : _left(left), _top(top), _theme_color(tcolor), _gauge_color(gcolor), _background_color(bcolor) { _plotter = new m5::ui::Plotter(nullptr, wid, wid, hgt); _plotter->setGaugeTextDatum(textdatum_t::top_right); _plotter->setLineColor(_theme_color); + _plotter->setGaugeColor(_gauge_color); + _plotter->setBackgroundColor(_background_color); } inline void push_back(const float value) @@ -39,5 +42,7 @@ private: uint32_t _left{}, _top{}; m5::ui::Plotter* _plotter{}; m5gfx::rgb565_t _theme_color{}; + m5gfx::rgb565_t _gauge_color{TFT_DARKGRAY}; + m5gfx::rgb565_t _background_color{TFT_BLACK}; }; #endif diff --git a/examples/UnitUnified/DualSensor/src/view.hpp b/examples/UnitUnified/DualSensor/src/view.hpp index 0186581..37443f3 100644 --- a/examples/UnitUnified/DualSensor/src/view.hpp +++ b/examples/UnitUnified/DualSensor/src/view.hpp @@ -11,9 +11,17 @@ struct View { View(const uint32_t wid, const uint32_t hgt, bool unit) : _type{unit} { + _theme_color = unit ? TFT_BLUE : TFT_YELLOW; + _sprite.setPsram(false); + _sprite.setColorDepth(2); _sprite.createSprite(wid, hgt); + _sprite.setPaletteColor(palette_black, TFT_BLACK); + _sprite.setPaletteColor(palette_white, TFT_WHITE); + _sprite.setPaletteColor(palette_theme, _theme_color); + _sprite.setPaletteColor(palette_gauge, TFT_DARKGRAY); + _sprite.setTextColor(palette_white, palette_black); _sprite.setFont(&fonts::FreeSansBold9pt7b); - _meter = new Meter(0, hgt * 2 / 3, wid, hgt / 3, unit ? TFT_BLUE : TFT_YELLOW); + _meter = new Meter(0, hgt * 2 / 3, wid, hgt / 3, palette_theme, palette_gauge, palette_black); } inline void push_back(const float ir, const float red) @@ -33,11 +41,11 @@ struct View { void render() { - _sprite.clear(); + _sprite.clear(palette_black); _sprite.drawString(_type ? "Unit" : "Hat", 0, 0); _sprite.setCursor(0, 24); _sprite.printf("BPM: %3.2f\nSpO2:%3.2f", _monitor.bpm(), _monitor.SpO2()); - _sprite.fillCircle(_sprite.width() - 12, 24 * 3, 7, _beat ? TFT_RED : TFT_WHITE); + _sprite.fillCircle(_sprite.width() - 12, 24 * 3, 7, _beat ? palette_theme : palette_white); _meter->push(&_sprite, 0, _sprite.height() >> 1); } @@ -53,10 +61,17 @@ struct View { _sprite.pushSprite(target, x, y); } + enum : uint8_t { + palette_black = 0, + palette_white = 1, + palette_theme = 2, + palette_gauge = 3, + }; LGFX_Sprite _sprite{}; m5::heart::PulseMonitor _monitor{100, 2}; Meter* _meter{}; uint32_t _left{}, _top{}, _beat{}; bool _type{}; + m5gfx::rgb565_t _theme_color{}; }; #endif diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp index 1c78661..8efe12d 100644 --- a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -84,13 +84,12 @@ void setup() M5.begin(m5cfg); M5.setTouchButtonHeightByRatio(100); - // The screen shall be in landscape mode - if (lcd.height() > lcd.width()) { + auto board = M5.getBoard(); + // The screen shall be in landscape mode (skip for M5Tab5) + if (board != m5::board_t::board_M5Tab5 && lcd.height() > lcd.width()) { lcd.setRotation(1); } - auto board = M5.getBoard(); - #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); diff --git a/examples/UnitUnified/GraphicalMeter/src/meter.hpp b/examples/UnitUnified/GraphicalMeter/src/meter.hpp index 1f8b506..0e50041 100644 --- a/examples/UnitUnified/GraphicalMeter/src/meter.hpp +++ b/examples/UnitUnified/GraphicalMeter/src/meter.hpp @@ -12,12 +12,15 @@ class Meter { public: - Meter(const uint32_t left, const uint32_t top, const uint32_t wid, const uint32_t hgt, m5gfx::rgb565_t tcolor) - : _left(left), _top(top), _theme_color(tcolor) + Meter(const uint32_t left, const uint32_t top, const uint32_t wid, const uint32_t hgt, m5gfx::rgb565_t tcolor, + m5gfx::rgb565_t gcolor = TFT_DARKGRAY, m5gfx::rgb565_t bcolor = TFT_BLACK) + : _left(left), _top(top), _theme_color(tcolor), _gauge_color(gcolor), _background_color(bcolor) { _plotter = new m5::ui::Plotter(nullptr, wid, wid, hgt); _plotter->setGaugeTextDatum(textdatum_t::top_right); _plotter->setLineColor(_theme_color); + _plotter->setGaugeColor(_gauge_color); + _plotter->setBackgroundColor(_background_color); } inline void push_back(const float value) @@ -39,5 +42,7 @@ private: uint32_t _left{}, _top{}; m5::ui::Plotter* _plotter{}; m5gfx::rgb565_t _theme_color{}; + m5gfx::rgb565_t _gauge_color{TFT_DARKGRAY}; + m5gfx::rgb565_t _background_color{TFT_BLACK}; }; #endif diff --git a/examples/UnitUnified/GraphicalMeter/src/view.hpp b/examples/UnitUnified/GraphicalMeter/src/view.hpp index 0186581..29a3509 100644 --- a/examples/UnitUnified/GraphicalMeter/src/view.hpp +++ b/examples/UnitUnified/GraphicalMeter/src/view.hpp @@ -11,9 +11,54 @@ struct View { View(const uint32_t wid, const uint32_t hgt, bool unit) : _type{unit} { - _sprite.createSprite(wid, hgt); - _sprite.setFont(&fonts::FreeSansBold9pt7b); - _meter = new Meter(0, hgt * 2 / 3, wid, hgt / 3, unit ? TFT_BLUE : TFT_YELLOW); + _theme_color = unit ? TFT_BLUE : TFT_YELLOW; + _screen_w = wid; + _screen_h = hgt; + _plot_top = hgt * 2 / 3; + _plot_hgt = hgt / 3; + const uint32_t spr_w = wid; + const uint32_t spr_h = _plot_hgt; + _plot_sprite.setPsram(false); + _plot_sprite.setColorDepth(2); + _plot_sprite.createSprite(spr_w, spr_h); + _plot_sprite.setPaletteColor(palette_black, TFT_BLACK); + _plot_sprite.setPaletteColor(palette_white, TFT_WHITE); + _plot_sprite.setPaletteColor(palette_theme, _theme_color); + _plot_sprite.setPaletteColor(palette_gauge, TFT_DARKGRAY); + _plot_sprite.setTextColor(palette_white, palette_black); + _plot_sprite.setFont(&fonts::FreeSansBold9pt7b); + _text_sprite.setPsram(false); + _text_sprite.setColorDepth(2); + _text_sprite.setPaletteColor(palette_black, TFT_BLACK); + _text_sprite.setPaletteColor(palette_white, TFT_WHITE); + _text_sprite.setPaletteColor(palette_theme, _theme_color); + _text_sprite.setPaletteColor(palette_gauge, TFT_DARKGRAY); + _text_sprite.setTextColor(palette_white, palette_black); + _text_sprite.setFont(&fonts::FreeSansBold9pt7b); + const int32_t line_h = _text_sprite.fontHeight(); + const int32_t title_w = _text_sprite.textWidth("Unit"); + const int32_t bpm_w = _text_sprite.textWidth("BPM: 000.00"); + const int32_t spo2_w = _text_sprite.textWidth("SpO2:100.00"); + _text_w = std::max(title_w, std::max(bpm_w, spo2_w)); + _text_h = line_h * 3; + _text_sprite.createSprite(_text_w, _text_h); + _text_sprite.setPaletteColor(palette_black, TFT_BLACK); + _text_sprite.setPaletteColor(palette_white, TFT_WHITE); + _text_sprite.setPaletteColor(palette_theme, _theme_color); + _text_sprite.setPaletteColor(palette_gauge, TFT_DARKGRAY); + _text_sprite.setTextColor(palette_white, palette_black); + _text_sprite.setFont(&fonts::FreeSansBold9pt7b); + _beat_size = 16; + _beat_x = _screen_w - _beat_size - 2; + _beat_y = (line_h * 2) + (line_h / 2) - (_beat_size / 2); + _beat_sprite.setPsram(false); + _beat_sprite.setColorDepth(2); + _beat_sprite.createSprite(_beat_size, _beat_size); + _beat_sprite.setPaletteColor(palette_black, TFT_BLACK); + _beat_sprite.setPaletteColor(palette_white, TFT_WHITE); + _beat_sprite.setPaletteColor(palette_theme, _theme_color); + _beat_sprite.setPaletteColor(palette_gauge, TFT_DARKGRAY); + _meter = new Meter(0, 0, spr_w, spr_h, palette_theme, palette_gauge, palette_black); } inline void push_back(const float ir, const float red) @@ -33,30 +78,47 @@ struct View { void render() { - _sprite.clear(); - _sprite.drawString(_type ? "Unit" : "Hat", 0, 0); - _sprite.setCursor(0, 24); - _sprite.printf("BPM: %3.2f\nSpO2:%3.2f", _monitor.bpm(), _monitor.SpO2()); - _sprite.fillCircle(_sprite.width() - 12, 24 * 3, 7, _beat ? TFT_RED : TFT_WHITE); - - _meter->push(&_sprite, 0, _sprite.height() >> 1); + _plot_sprite.clear(palette_black); + _meter->push(&_plot_sprite, 0, 0); + _text_sprite.clear(palette_black); + _text_sprite.drawString(_type ? "Unit" : "Hat", 0, 0); + _text_sprite.setCursor(0, _text_sprite.fontHeight()); + _text_sprite.printf("BPM: %3.2f\nSpO2:%3.2f", _monitor.bpm(), _monitor.SpO2()); + _beat_sprite.clear(palette_black); + _beat_sprite.fillCircle(_beat_size / 2, _beat_size / 2, 7, _beat ? palette_theme : palette_white); } void clear() { _monitor.clear(); _meter->clear(); + _plot_sprite.clear(palette_black); + _text_sprite.clear(palette_black); + _beat_sprite.clear(palette_black); } void push(LovyanGFX* target, const uint32_t x, const uint32_t y) { - _sprite.pushSprite(target, x, y); + _text_sprite.pushSprite(target, x, y); + _beat_sprite.pushSprite(target, x + _beat_x, y + _beat_y); + _plot_sprite.pushSprite(target, x, y + _plot_top); } - LGFX_Sprite _sprite{}; + enum : uint8_t { + palette_black = 0, + palette_white = 1, + palette_theme = 2, + palette_gauge = 3, + }; + LGFX_Sprite _plot_sprite{}; + LGFX_Sprite _text_sprite{}; + LGFX_Sprite _beat_sprite{}; m5::heart::PulseMonitor _monitor{100, 2}; Meter* _meter{}; uint32_t _left{}, _top{}, _beat{}; + uint32_t _screen_w{}, _screen_h{}, _plot_top{}, _plot_hgt{}; + int32_t _text_w{}, _text_h{}, _beat_x{}, _beat_y{}, _beat_size{}; bool _type{}; + m5gfx::rgb565_t _theme_color{}; }; #endif From 9b1ca370ecf7ab73e9978b9ff2953dc136c1ce97 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 20:18:17 +0900 Subject: [PATCH 10/40] Add more devices --- platformio.ini | 58 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/platformio.ini b/platformio.ini index 205d412..391353d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,6 +53,7 @@ board = m5stack-stamps3 lib_deps = ${env.lib_deps} m5stack/M5Dial +;include AtomLite [AtomMatrix] extends = m5base board = m5stack-atom @@ -64,6 +65,7 @@ board = m5stack-atoms3 lib_deps = ${env.lib_deps} ; Using ./boards/m5stack-atoms3r.json +;include もじと AtomEchoS3R [AtomS3R] extends = m5base board = m5stack-atoms3r @@ -73,12 +75,8 @@ lib_deps = ${env.lib_deps} [NanoC6] extends = m5base board = m5stack-nanoc6 -platform = https://github.com/platformio/platform-espressif32.git -platform_packages = - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.7 - platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1 -board_build.partitions = default.csv lib_deps = ${env.lib_deps} +board_build.partitions = default.csv [StickCPlus] extends = m5base @@ -101,11 +99,31 @@ extends = m5base board = m5stack-coreink lib_deps = ${env.lib_deps} +[Cardputer] +extends = m5base +board = esp32-s3-devkitc-1 +build_flags = + -DESP32S3 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 +lib_deps = ${env.lib_deps} + +[Tab5] +extends = m5base +board = esp32-p4-evboard +board_build.mcu = esp32p4 +board_build.flash_mode = qio +build_flags = + -DBOARD_HAS_PSRAM + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + [NessoN1] extends = m5base board = arduino_nesso_n1 lib_deps = ${env.lib_deps} + [sdl] build_flags = -O3 -xc++ -std=c++14 -lSDL2 -arch arm64 ; for arm mac @@ -128,7 +146,6 @@ framework = arduino platform = espressif32 @ 6.8.1 framework = arduino - [arduino_6_6_0] platform = espressif32 @ 6.6.0 framework = arduino @@ -145,14 +162,17 @@ framework = arduino platform = espressif32 @ 4.4.0 framework = arduino +[nanoc6_latest] +platform = espressif32 @ 6.12.0 +platform_packages = + platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.7 + platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1 +framework = arduino + [pioarduino_latest] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip@55.3.36 framework = arduino -[esp-idf] -platform = espressif32 @ 6.8.1 -framework = espidf - ; -------------------------------- ;Choose build options [option_release] @@ -250,7 +270,7 @@ lib_deps = ${AtomS3R.lib_deps} test_filter= embedded/test_max30100 [env:test_UnitHeart_NanoC6] -extends=NanoC6, option_release +extends=NanoC6, option_release, nanoc6_latest lib_deps = ${NanoC6.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30100 @@ -372,7 +392,7 @@ build_flags = ${option_release.build_flags} -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_NanoC6_Arduino_latest] -extends=NanoC6, option_release +extends=NanoC6, option_release, nanoc6_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> build_flags = ${option_release.build_flags} -D USING_UNIT_HEART @@ -407,6 +427,18 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial build_flags = ${option_release.build_flags} -D USING_UNIT_HEART +[env:UnitHeart_PlotToSerial_Cardputer_Arduino_latest] +extends=Cardputer, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART + +[env:UnitHeart_PlotToSerial_Tab5_Arduino_latest] +extends=Tab5, option_release, pioarduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${option_release.build_flags} + -D USING_UNIT_HEART + [env:UnitHeart_PlotToSerial_NessoN1_Arduino_latest] extends=NessoN1, option_release, pioarduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> @@ -422,6 +454,7 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMet build_flags = ${option_release.build_flags} -D USING_UNIT_HEART + ;-------------------------------- ;HatHeart ;-------------------------------- @@ -466,4 +499,3 @@ build_flags = ${option_release.build_flags} [env:DualSensor_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> - From 0cfea77dee0b35d3844f89bec8a878adb6025fed Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Feb 2026 20:18:32 +0900 Subject: [PATCH 11/40] Fixes workflow --- .github/workflows/platformio-build-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 181854f..917059a 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -74,6 +74,8 @@ jobs: - StickCPlus2 - Paper - CoreInk + - Cardputer + - Tab5 framework: - Arduino From fd1bdaa9ab4bf9321f14d4345ac9c7f9002806d3 Mon Sep 17 00:00:00 2001 From: GOB Date: Tue, 10 Feb 2026 13:31:58 +0900 Subject: [PATCH 12/40] Fixes test --- platformio.ini | 38 +++++++++++- test/embedded/embedded_main.cpp | 6 +- test/embedded/test_max30100/max30100_test.cpp | 31 ++++++++-- test/embedded/test_max30102/max30102_test.cpp | 58 +++++++++++++++---- 4 files changed, 112 insertions(+), 21 deletions(-) diff --git a/platformio.ini b/platformio.ini index 391353d..e76067d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -53,8 +53,8 @@ board = m5stack-stamps3 lib_deps = ${env.lib_deps} m5stack/M5Dial -;include AtomLite [AtomMatrix] +;include AtomLite extends = m5base board = m5stack-atom lib_deps = ${env.lib_deps} @@ -65,8 +65,8 @@ board = m5stack-atoms3 lib_deps = ${env.lib_deps} ; Using ./boards/m5stack-atoms3r.json -;include もじと AtomEchoS3R [AtomS3R] +;include AtomEchoS3R extends = m5base board = m5stack-atoms3r lib_deps = ${env.lib_deps} @@ -117,13 +117,13 @@ build_flags = -DBOARD_HAS_PSRAM -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 +lib_deps = ${env.lib_deps} [NessoN1] extends = m5base board = arduino_nesso_n1 lib_deps = ${env.lib_deps} - [sdl] build_flags = -O3 -xc++ -std=c++14 -lSDL2 -arch arm64 ; for arm mac @@ -299,6 +299,24 @@ lib_deps = ${CoreInk.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30100 +[env:test_UnitHeart_Cardputer] +extends=Cardputer, option_release, arduino_latest +lib_deps = ${Cardputer.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_max30100 + +[env:test_UnitHeart_Tab5] +extends=Tab5, option_release, pioarduino_latest +lib_deps = ${Tab5.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_max30100 + +[env:test_UnitHeart_NessoN1] +extends=NessoN1, option_release, pioarduino_latest +lib_deps = ${NessoN1.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_max30100 + ; HatHeart [env:test_HatHeart_StickCPlus] extends=StickCPlus, option_release, arduino_latest @@ -312,6 +330,20 @@ lib_deps = ${StickCPlus2.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30102 +[env:test_HatHeart_CoreInk] +extends=CoreInk, option_release, arduino_latest +lib_deps = ${CoreInk.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_max30102 + +[env:test_HatHeart_NessoN1] +extends=NessoN1, option_release, pioarduino_latest +lib_deps = ${NessoN1.lib_deps} + ${test_fw.lib_deps} +test_filter= embedded/test_max30102 + + + ;-------------------------------- ;Examples by M5UnitUnified ;-------------------------------- diff --git a/test/embedded/embedded_main.cpp b/test/embedded/embedded_main.cpp index 28f708c..879b14d 100644 --- a/test/embedded/embedded_main.cpp +++ b/test/embedded/embedded_main.cpp @@ -30,7 +30,11 @@ void test() void setup() { - M5.begin(); + 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); M5_LOGI("CPP %ld", __cplusplus); M5_LOGI("ESP-IDF Version %d.%d.%d", (ESP_IDF_VERSION >> 16) & 0xFF, (ESP_IDF_VERSION >> 8) & 0xFF, diff --git a/test/embedded/test_max30100/max30100_test.cpp b/test/embedded/test_max30100/max30100_test.cpp index 45310ee..9cda2dd 100644 --- a/test/embedded/test_max30100/max30100_test.cpp +++ b/test/embedded/test_max30100/max30100_test.cpp @@ -28,6 +28,30 @@ const ::testing::Environment* global_fixture = ::testing::AddGlobalTestEnvironme class TestMAX30100 : public ComponentTestBase { protected: + virtual bool begin() override + { + // NessoN1 GROVE must use HAL + auto board = M5.getBoard(); + if (board == m5::board_t::board_ArduinoNessoN1 || is_using_hal()) { + // Using M5HAL + 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); + if (board == m5::board_t::board_ArduinoNessoN1) { + pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); + pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); + } + // M5_LOGI("pin:%d,%d", pin_num_sda, pin_num_scl); + + m5::hal::bus::I2CBusConfig i2c_cfg; + i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); + i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); + auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); + return Units.add(*unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); + } + + // Using TwoWire + return Units.add(*unit, Wire) && Units.begin(); + } virtual UnitMAX30100* get_instance() override { auto ptr = new m5::unit::UnitMAX30100(); @@ -286,7 +310,6 @@ void test_periodic_spo2(UnitMAX30100* unit) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); - EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * // unit->interval(), @@ -377,11 +400,9 @@ void test_periodic_hr(UnitMAX30100* unit) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); - EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * - // unit->interval(), - // unit->retrieved(), unit->overflow()); + M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), + unit->retrieved(), unit->overflow()); EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) EXPECT_FALSE(unit->empty()); diff --git a/test/embedded/test_max30102/max30102_test.cpp b/test/embedded/test_max30102/max30102_test.cpp index cf21bc1..1332e11 100644 --- a/test/embedded/test_max30102/max30102_test.cpp +++ b/test/embedded/test_max30102/max30102_test.cpp @@ -22,10 +22,30 @@ using namespace m5::unit::max30102; using namespace m5::unit::max30102::command; using m5::unit::types::elapsed_time_t; -#if !defined(M5STACK_M5STICK_CPLUS2) && !defined(ARDUINO_M5Stick_C) -#error This test for M5StckCPlus or M5StckCPlus2 -#else namespace hat { +struct I2cPins { + int sda; + int scl; +}; + +I2cPins get_hat_i2c_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}; + } +} + template class GlobalFixture : public ::testing::Environment { static_assert(WNUM < 2, "Wire number must be lesser than 2"); @@ -33,24 +53,41 @@ class GlobalFixture : public ::testing::Environment { public: void SetUp() override { - // Setup required to use HatHEART - pinMode(25, INPUT_PULLUP); - pinMode(26, OUTPUT); + const auto pins = get_hat_i2c_pins(M5.getBoard()); + // M5_LOGI("pin:%d %d", pins.sda, pins.scl); + + pinMode(pins.scl, OUTPUT); TwoWire* w[2] = {&Wire, &Wire1}; if (WNUM < m5::stl::size(w) && i2cIsInit(WNUM)) { M5_LOGW("Already inititlized Wire %d. Terminate and restart FREQ %u", WNUM, FREQ); w[WNUM]->end(); } - w[WNUM]->begin(0, 26, FREQ); + w[WNUM]->begin(pins.sda, pins.scl, FREQ); } }; + } // namespace hat -const ::testing::Environment* global_fixture = ::testing::AddGlobalTestEnvironment(new hat::GlobalFixture<400000U>()); -#endif +const ::testing::Environment* global_fixture = + ::testing::AddGlobalTestEnvironment(new hat::GlobalFixture<400000U, 1 /* Wire1 */>()); class TestMAX30102 : public ComponentTestBase { protected: + virtual bool begin() override + { + if (is_using_hal()) { + const auto pins = hat::get_hat_i2c_pins(M5.getBoard()); + m5::hal::bus::I2CBusConfig i2c_cfg; + i2c_cfg.pin_sda = m5::hal::gpio::getPin(pins.sda); + i2c_cfg.pin_scl = m5::hal::gpio::getPin(pins.scl); + auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); + return Units.add(*unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); + } + + // Using TwoWire + return Units.add(*unit, Wire1) && Units.begin(); + } + virtual UnitMAX30102* get_instance() override { auto ptr = new m5::unit::UnitMAX30102(); @@ -362,7 +399,6 @@ void test_periodic_spo2(UnitMAX30102* unit) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); - EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * // unit->interval(), @@ -481,7 +517,6 @@ void test_periodic_hr(UnitMAX30102* unit) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); - EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * // unit->interval(), @@ -554,7 +589,6 @@ void test_periodic_multi(UnitMAX30102* unit) EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); - EXPECT_NE(elapsed, 0); EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * // unit->interval(), From 11edbe6dc461a1a2ee5df17e1e18068633e414aa Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 12 Feb 2026 13:01:25 +0900 Subject: [PATCH 13/40] Add more device --- .github/workflows/arduino-m5-build-check.yml | 2 +- .github/workflows/clang-format-check.yml | 2 - .github/workflows/doxygen-gh-pages.yml | 2 + .github/workflows/platformio-build-check.yml | 2 +- library.json | 4 +- platformio.ini | 62 ++++++++++++++++---- 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index fec74fa..81b47f2 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -81,7 +81,7 @@ jobs: - m5stack_nano_c6 # - m5stack_nano_h2 - m5stack_paper -# - m5stack_papers3 + - m5stack_papers3 # - m5stack_poe_cam # - m5stack_powerhub # - m5stack_stamp_c3 diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 53d196d..ffc5917 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -58,8 +58,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - name: Run clang-format style check for C/C++/Protobuf programs. uses: jidicula/clang-format-action@v4.10.2 # Using include-regex 10.x or later diff --git a/.github/workflows/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml index 83c1c96..9554f74 100644 --- a/.github/workflows/doxygen-gh-pages.yml +++ b/.github/workflows/doxygen-gh-pages.yml @@ -3,6 +3,8 @@ on: [release, workflow_dispatch] # branches: # - main # - master +permissions: + contents: write defaults: run: shell: bash diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 917059a..20a7a15 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -66,7 +66,7 @@ jobs: - Fire - StampS3 - Dial - - AtomMatrix + - Atom - AtomS3 - AtomS3R - NanoC6 diff --git a/library.json b/library.json index 06d796b..320a4d5 100644 --- a/library.json +++ b/library.json @@ -11,7 +11,7 @@ "url": "https://github.com/m5stack/M5Unit-HEART.git" }, "dependencies": { - "m5stack/M5UnitUnified": ">=0.1.0" + "m5stack/M5UnitUnified": ">=0.3.0" }, "version": "0.2.0", "frameworks": [ @@ -27,4 +27,4 @@ "docs/html" ] } -} \ No newline at end of file +} diff --git a/platformio.ini b/platformio.ini index e76067d..5cf7ff6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,9 +21,7 @@ test_ignore= native/* [Core] extends = m5base -board = m5stack-grey -;m5stack-core-esp32-16M ;;6.8.0 or later -;m5stack-core-esp32 +board = m5stack-core-esp32-16M lib_deps = ${env.lib_deps} [Core2] @@ -53,13 +51,14 @@ board = m5stack-stamps3 lib_deps = ${env.lib_deps} m5stack/M5Dial -[AtomMatrix] -;include AtomLite +[Atom] +;include AtomMatrix,AtomLite,AtomU extends = m5base board = m5stack-atom lib_deps = ${env.lib_deps} [AtomS3] +;include AtomEchoS3R,AtomS3Lite,AtomS3U extends = m5base board = m5stack-atoms3 lib_deps = ${env.lib_deps} @@ -89,6 +88,19 @@ extends = m5base board = m5stick-cplus2 lib_deps = ${env.lib_deps} +[StickS3] +extends = m5base +board = esp32-s3-devkitc-1 +board_build.arduino.partitions = default_8MB.csv +board_build.arduino.memory_type = qio_opi +build_flags = + -DESP32S3 + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 +lib_deps = ${env.lib_deps} + [Paper] extends = m5base board = m5stack-fire @@ -251,9 +263,9 @@ lib_deps = ${Dial.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30100 -[env:test_UnitHeart_AtomMatrix] -extends=AtomMatrix, option_release, arduino_latest -lib_deps = ${AtomMatrix.lib_deps} +[env:test_UnitHeart_Atom] +extends=Atom, option_release, arduino_latest +lib_deps = ${Atom.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30100 @@ -287,6 +299,14 @@ lib_deps = ${StickCPlus2.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30100 +[env:test_UnitHeart_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_max30100 + [env:test_UnitHeart_Paper] extends=Paper, option_release, arduino_latest lib_deps = ${Paper.lib_deps} @@ -330,6 +350,14 @@ lib_deps = ${StickCPlus2.lib_deps} ${test_fw.lib_deps} test_filter= embedded/test_max30102 +[env:test_HatHeart_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_max30102 + [env:test_HatHeart_CoreInk] extends=CoreInk, option_release, arduino_latest lib_deps = ${CoreInk.lib_deps} @@ -399,8 +427,8 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial build_flags = ${option_release.build_flags} -D USING_UNIT_HEART -[env:UnitHeart_PlotToSerial_AtomMatrix_Arduino_latest] -extends=AtomMatrix, option_release, arduino_latest +[env:UnitHeart_PlotToSerial_Atom_Arduino_latest] +extends=Atom, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> build_flags = ${option_release.build_flags} -D USING_UNIT_HEART @@ -441,6 +469,13 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial build_flags = ${option_release.build_flags} -D USING_UNIT_HEART +[env:UnitHeart_PlotToSerial_StickS3_Arduino_latest] +extends=StickS3, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${StickS3.build_flags} + ${option_release.build_flags} + -D USING_UNIT_HEART + [env:UnitHeart_PlotToSerial_Paper_Arduino_latest] extends=Paper, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> @@ -503,6 +538,13 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial build_flags = ${option_release.build_flags} -D USING_HAT_HEART +[env:HatHeart_PlotToSerial_StickS3_Arduino_latest] +extends=StickS3, option_release, arduino_latest +build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> +build_flags = ${StickS3.build_flags} + ${option_release.build_flags} + -D USING_HAT_HEART + [env:HatHeart_PlotToSerial_CoreInk_Arduino_latest] extends=CoreInk, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> From cb082fb1423ebabbcc361b826ab048d2004ab454 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 12 Feb 2026 13:20:15 +0900 Subject: [PATCH 14/40] Fixes pin down the pioarduino version --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 5cf7ff6..4de85b0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -182,7 +182,7 @@ platform_packages = framework = arduino [pioarduino_latest] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip@55.3.36 +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip framework = arduino ; -------------------------------- From 2db7b1849515daa2dc59f7f3414bfeafc40bd4b4 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 12 Feb 2026 14:20:49 +0900 Subject: [PATCH 15/40] Add more device --- .github/workflows/platformio-build-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 20a7a15..49224f3 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -76,6 +76,7 @@ jobs: - CoreInk - Cardputer - Tab5 + - NessoN1 framework: - Arduino From 57603f72c81eee409a6316188ec46326c8d8f25e Mon Sep 17 00:00:00 2001 From: GOB Date: Fri, 13 Feb 2026 14:49:24 +0900 Subject: [PATCH 16/40] Some tweaks --- .github/workflows/platformio-build-check.yml | 6 ++++ .../DualSensor/main/DualSensor.cpp | 1 + .../GraphicalMeter/main/GraphicalMeter.cpp | 2 -- .../PlotToSerial/main/PlotToSerial.cpp | 4 +-- platformio.ini | 32 ------------------- 5 files changed, 8 insertions(+), 37 deletions(-) diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 49224f3..9433b23 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -72,6 +72,7 @@ jobs: - NanoC6 - StickCPlus - StickCPlus2 + - StickS3 - Paper - CoreInk - Cardputer @@ -97,6 +98,11 @@ jobs: board: StickCPlus2 framework: Arduino espressif32: latest + - example: PlotToSerial + unit: HatHeart + board: StickS3 + framework: Arduino + espressif32: latest - example: PlotToSerial unit: HatHeart board: CoreInk diff --git a/examples/UnitUnified/DualSensor/main/DualSensor.cpp b/examples/UnitUnified/DualSensor/main/DualSensor.cpp index 54fa228..a57cd3e 100644 --- a/examples/UnitUnified/DualSensor/main/DualSensor.cpp +++ b/examples/UnitUnified/DualSensor/main/DualSensor.cpp @@ -56,6 +56,7 @@ void setup() m5cfg.internal_imu = false; // Disable internal IMU m5cfg.internal_rtc = false; // Disable internal RTC M5.begin(m5cfg); + M5.setTouchButtonHeightByRatio(100); const auto board_type = M5.getBoard(); diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp index 8efe12d..6aa40fb 100644 --- a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -7,8 +7,6 @@ Graphical meter example for UnitHeart / HatHeart The core must be equipped with LCD */ -// #define USING_M5HAL // When using M5HAL (UnitHeart only) - #include #include #include diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index 5f80d19..571b3c7 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -6,8 +6,6 @@ /* Example using M5UnitUnified for UnitHeart / HatHeart */ -// #define USING_M5HAL // When using M5HAL (UnitHeart only) - #include #include #include @@ -180,7 +178,7 @@ void setup() #endif monitor.setSamplingRate(unit.calculateSamplingRate()); - lcd.clear(TFT_DARKGREEN); + lcd.fillScreen(TFT_DARKGREEN); } void loop() diff --git a/platformio.ini b/platformio.ini index 4de85b0..990dee8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -166,14 +166,6 @@ framework = arduino platform = espressif32 @ 6.0.1 framework = arduino -[arduino_5_4_0] -platform = espressif32 @ 5.4.0 -framework = arduino - -[arduino_4_4_0] -platform = espressif32 @ 4.4.0 -framework = arduino - [nanoc6_latest] platform = espressif32 @ 6.12.0 platform_packages = @@ -385,36 +377,12 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial build_flags = ${option_release.build_flags} -D USING_UNIT_HEART -[env:UnitHeart_PlotToSerial_Core_Arduino_5_4_0] -extends=Core, option_release, arduino_5_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> -build_flags = ${option_release.build_flags} - -D USING_UNIT_HEART - -[env:UnitHeart_PlotToSerial_Core_Arduino_4_4_0] -extends=Core, option_release, arduino_4_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> -build_flags = ${option_release.build_flags} - -D USING_UNIT_HEART - [env:UnitHeart_PlotToSerial_Core2_Arduino_latest] extends=Core2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> build_flags = ${option_release.build_flags} -D USING_UNIT_HEART -[env:UnitHeart_PlotToSerial_Core2_Arduino_5_4_0] -extends=Core2, option_release, arduino_5_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> -build_flags = ${option_release.build_flags} - -D USING_UNIT_HEART - -[env:UnitHeart_PlotToSerial_Core2_Arduino_4_4_0] -extends=Core2, option_release, arduino_4_4_0 -build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> -build_flags = ${option_release.build_flags} - -D USING_UNIT_HEART - [env:UnitHeart_PlotToSerial_CoreS3_Arduino_latest] extends=CoreS3, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> From 9d7daa27535decd8750e4acc8125797e270c7d79 Mon Sep 17 00:00:00 2001 From: GOB Date: Fri, 13 Feb 2026 15:24:41 +0900 Subject: [PATCH 17/40] Update clang-format-action to v4.16.0 --- .github/workflows/clang-format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index ffc5917..65f0f20 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v4 - name: Run clang-format style check for C/C++/Protobuf programs. - uses: jidicula/clang-format-action@v4.10.2 # Using include-regex 10.x or later + uses: jidicula/clang-format-action@v4.16.0 with: clang-format-version: '13' check-path: ${{ matrix.path['check'] }} From e0653c89a29c856041c6e0b750d353f57a18aa8c Mon Sep 17 00:00:00 2001 From: GOB Date: Fri, 20 Feb 2026 17:41:19 +0900 Subject: [PATCH 18/40] Fixes HatHeart examples to always use Wire1 for I2C --- .../GraphicalMeter/main/GraphicalMeter.cpp | 20 +++++++++---------- .../PlotToSerial/main/PlotToSerial.cpp | 20 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp index 6aa40fb..879d1f2 100644 --- a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -47,7 +47,6 @@ View* view{}; struct I2cPins { int sda; int scl; - bool use_wire1; }; I2cPins get_hat_i2c_pins(const m5::board_t board) @@ -56,15 +55,15 @@ I2cPins get_hat_i2c_pins(const m5::board_t board) case m5::board_t::board_M5StickC: case m5::board_t::board_M5StickCPlus: case m5::board_t::board_M5StickCPlus2: - return {0, 26, true}; + return {0, 26}; case m5::board_t::board_M5StickS3: - return {8, 0, true}; + return {8, 0}; case m5::board_t::board_M5StackCoreInk: - return {25, 26, true}; + return {25, 26}; case m5::board_t::board_ArduinoNessoN1: - return {6, 7, true}; + return {6, 7}; default: - return {-1, -1, false}; + return {-1, -1}; } } #endif @@ -90,7 +89,7 @@ void setup() #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); - M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); + M5_LOGI("getHatPin: SDA:%u SCL:%u Wire1", pins.sda, pins.scl); if (pins.sda < 0 || pins.scl < 0) { M5_LOGE("Illegal pin number"); lcd.clear(TFT_RED); @@ -102,10 +101,9 @@ void setup() // Setup required to use HatHEART pinMode(pins.scl, OUTPUT); - TwoWire& wire = pins.use_wire1 ? Wire1 : Wire; - wire.end(); - wire.begin(pins.sda, pins.scl, 400 * 1000U); - if (!Units.add(heart, wire) || !Units.begin()) { + Wire1.end(); + Wire1.begin(pins.sda, pins.scl, 400 * 1000U); + if (!Units.add(heart, Wire1) || !Units.begin()) { M5_LOGE("Failed to begin"); lcd.clear(TFT_RED); while (true) { diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index 571b3c7..cdf37cb 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -47,7 +47,6 @@ constexpr bool using_multi_led_mode{false}; // Using multiLED mode if true struct I2cPins { int sda; int scl; - bool use_wire1; }; I2cPins get_hat_i2c_pins(const m5::board_t board) @@ -56,15 +55,15 @@ I2cPins get_hat_i2c_pins(const m5::board_t board) case m5::board_t::board_M5StickC: case m5::board_t::board_M5StickCPlus: case m5::board_t::board_M5StickCPlus2: - return {0, 26, true}; + return {0, 26}; case m5::board_t::board_M5StickS3: - return {8, 0, true}; + return {8, 0}; case m5::board_t::board_M5StackCoreInk: - return {25, 26, true}; + return {25, 26}; case m5::board_t::board_ArduinoNessoN1: - return {6, 7, true}; + return {6, 7}; default: - return {-1, -1, false}; + return {-1, -1}; } } #endif @@ -92,7 +91,7 @@ void setup() #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); - M5_LOGI("getHatPin: SDA:%u SCL:%u %s", pins.sda, pins.scl, pins.use_wire1 ? "Wire1" : "Wire"); + M5_LOGI("getHatPin: SDA:%u SCL:%u Wire1", pins.sda, pins.scl); if (pins.sda < 0 || pins.scl < 0) { M5_LOGE("Illegal pin number"); lcd.clear(TFT_RED); @@ -112,10 +111,9 @@ void setup() unit.config(cfg); } - auto& wire = pins.use_wire1 ? Wire1 : Wire; - wire.end(); - wire.begin(pins.sda, pins.scl, 400 * 1000U); - if (!Units.add(unit, wire) || !Units.begin()) { + Wire1.end(); + Wire1.begin(pins.sda, pins.scl, 400 * 1000U); + if (!Units.add(unit, Wire1) || !Units.begin()) { M5_LOGE("Failed to begin"); lcd.clear(TFT_RED); while (true) { From efe3aeb962e66dc6209df41078698757d0c84cbf Mon Sep 17 00:00:00 2001 From: GOB Date: Fri, 20 Feb 2026 20:02:02 +0900 Subject: [PATCH 19/40] Fixes HatHeart I2C bus selection and EPD serial output in PlotToSerial and DualSensor --- .../DualSensor/main/DualSensor.cpp | 120 ++++++++++++------ .../PlotToSerial/main/PlotToSerial.cpp | 10 +- 2 files changed, 87 insertions(+), 43 deletions(-) diff --git a/examples/UnitUnified/DualSensor/main/DualSensor.cpp b/examples/UnitUnified/DualSensor/main/DualSensor.cpp index a57cd3e..b542757 100644 --- a/examples/UnitUnified/DualSensor/main/DualSensor.cpp +++ b/examples/UnitUnified/DualSensor/main/DualSensor.cpp @@ -20,6 +20,7 @@ m5::unit::UnitHeart unit; m5::unit::HatHeart hat; View* view[2]{}; +bool is_epd_panel{}; struct I2cPins { int sda; @@ -57,36 +58,50 @@ void setup() m5cfg.internal_rtc = false; // Disable internal RTC M5.begin(m5cfg); M5.setTouchButtonHeightByRatio(100); + is_epd_panel = lcd.isEPD(); const auto board_type = M5.getBoard(); - const auto pins = get_hat_i2c_pins(board_type); - M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); - if (pins.sda < 0 || pins.scl < 0) { - M5_LOGE("HatHeart requires Wire1-capable boards"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } - // The screen shall be in landscape mode if (lcd.height() > lcd.width()) { lcd.setRotation(1); } + const auto pins = get_hat_i2c_pins(board_type); + M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); + if (pins.sda < 0 || pins.scl < 0) { + M5_LOGE("Hat port not exists"); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); + } + } + // Setup required to use HatHEART pinMode(pins.scl, OUTPUT); - // HatHeart on Wire1 - Wire1.end(); - Wire1.begin(pins.sda, pins.scl, 400 * 1000U); + // HatHeart settings + bool hat_ready{}; + if (board_type == m5::board_t::board_ArduinoNessoN1) { + // Using wire1 + Wire1.end(); + Wire1.begin(pins.sda, pins.scl, 400 * 1000U); + hat_ready = Units.add(hat, Wire1); + } else { + // SoftwareI2C for Hat + m5::hal::bus::I2CBusConfig hat_i2c_cfg; + hat_i2c_cfg.pin_sda = m5::hal::gpio::getPin(pins.sda); + hat_i2c_cfg.pin_scl = m5::hal::gpio::getPin(pins.scl); + auto hat_i2c_bus = m5::hal::bus::i2c::getBus(hat_i2c_cfg); + hat_ready = Units.add(hat, hat_i2c_bus ? hat_i2c_bus.value() : nullptr); + } - // UnitHeart wire settings + // UnitHeart settings 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); - // For NessoN1 GROVE + bool unit_ready{}; if (board_type == m5::board_t::board_ArduinoNessoN1) { + // For NessoN1 GROVE // Wire is used internally, so SoftwareI2C handles the unit pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); @@ -96,38 +111,35 @@ 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 (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.add(hat, Wire1) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } + unit_ready = Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr); } else { // UnitHeart on Wire 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 (!Units.add(unit, Wire) || !Units.add(hat, Wire1) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } + unit_ready = Units.add(unit, Wire); + } + + if (!hat_ready || !unit_ready || !Units.begin()) { + M5_LOGE("Failed to begin %u/%u", hat_ready, unit_ready); + M5_LOGE("%s", Units.debugInfo().c_str()); + lcd.clear(TFT_RED); + while (true) { + m5::utility::delay(10000); } } M5_LOGI("M5UnitUnified has been begun"); M5_LOGI("%s", Units.debugInfo().c_str()); - lcd.startWrite(); - view[0] = new View(lcd.width() >> 1, lcd.height(), true); view[1] = new View(lcd.width() >> 1, lcd.height(), false); view[0]->_monitor.setSamplingRate(unit.calculateSamplingRate()); view[1]->_monitor.setSamplingRate(hat.calculateSamplingRate()); - view[0]->push(&lcd, lcd.width() >> 1, 0); - view[1]->push(&lcd, 0, 0); + if (!is_epd_panel) { + view[0]->push(&lcd, lcd.width() >> 1, 0); + view[1]->push(&lcd, 0, 0); + } } void loop() @@ -135,34 +147,66 @@ void loop() M5.update(); Units.update(); + if (!is_epd_panel) { + lcd.startWrite(); + } if (unit.updated()) { if (unit.overflow()) { M5_LOGW("OVERFLOW U:%u", unit.overflow()); } + bool beat{}; while (unit.available()) { - view[0]->push_back(unit.ir(), unit.red()); + const auto ir = unit.ir(); + const auto red = unit.red(); + view[0]->push_back(ir, red); view[0]->update(); + if (is_epd_panel) { + M5.Log.printf(">UIR:%u\n>URED:%u\n", static_cast(ir), static_cast(red)); + M5.Log.printf(">UMIR:%f\n", view[0]->_monitor.latestIR()); + } + beat |= view[0]->_monitor.isBeat(); unit.discard(); } - view[0]->render(); - view[0]->push(&lcd, lcd.width() >> 1, 0); + if (is_epd_panel) { + M5.Log.printf(">UBPM:%f\n>USpO2:%f\n>UBEAT:%u\n", view[0]->_monitor.bpm(), view[0]->_monitor.SpO2(), beat); + } + if (!is_epd_panel) { + view[0]->render(); + view[0]->push(&lcd, lcd.width() >> 1, 0); + } } if (hat.updated()) { if (hat.overflow()) { M5_LOGW("OVERFLOW H:%u", hat.overflow()); } + bool beat{}; while (hat.available()) { - view[1]->push_back(hat.ir(), hat.red()); + const auto ir = hat.ir(); + const auto red = hat.red(); + view[1]->push_back(ir, red); view[1]->update(); + if (is_epd_panel) { + M5.Log.printf(">HIR:%u\n>HRED:%u\n", static_cast(ir), static_cast(red)); + M5.Log.printf(">HMIR:%f\n", view[1]->_monitor.latestIR()); + } + beat |= view[1]->_monitor.isBeat(); hat.discard(); } - view[1]->render(); - view[1]->push(&lcd, 0, 0); + if (is_epd_panel) { + M5.Log.printf(">HBPM:%f\n>HSpO2:%f\n>HBEAT:%u\n", view[1]->_monitor.bpm(), view[1]->_monitor.SpO2(), beat); + } + if (!is_epd_panel) { + view[1]->render(); + view[1]->push(&lcd, 0, 0); + } } if (M5.BtnA.wasClicked()) { view[0]->clear(); view[1]->clear(); } + if (!is_epd_panel) { + lcd.endWrite(); + } } diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index cdf37cb..23d55d6 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -91,7 +91,7 @@ void setup() #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); - M5_LOGI("getHatPin: SDA:%u SCL:%u Wire1", pins.sda, pins.scl); + M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); if (pins.sda < 0 || pins.scl < 0) { M5_LOGE("Illegal pin number"); lcd.clear(TFT_RED); @@ -110,10 +110,10 @@ void setup() cfg.start_periodic = false; // Ignore auto start unit.config(cfg); } - - Wire1.end(); - Wire1.begin(pins.sda, pins.scl, 400 * 1000U); - if (!Units.add(unit, Wire1) || !Units.begin()) { + 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.clear(TFT_RED); while (true) { From 075aaabc80d8dc95853386753f25ffb872ab0a24 Mon Sep 17 00:00:00 2001 From: GOB Date: Wed, 4 Mar 2026 19:38:14 +0900 Subject: [PATCH 20/40] Fixes reset handling in begin() and adds post-reset delay for register settling --- src/unit/unit_MAX30100.cpp | 9 +++------ src/unit/unit_MAX30102.cpp | 1 + src/unit/unit_MAX30102.hpp | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/unit/unit_MAX30100.cpp b/src/unit/unit_MAX30100.cpp index 7f31f61..ccfd48f 100644 --- a/src/unit/unit_MAX30100.cpp +++ b/src/unit/unit_MAX30100.cpp @@ -179,14 +179,10 @@ bool UnitMAX30100::begin() return false; } -#if 0 - // Clear interrupt status - uint8_t it{}; - if (!read_register8(READ_INTERRUPT_STATUS, it)) { - M5_LIB_LOGE("Failed to read INTERRUPT_STATUS"); + if (!reset()) { + M5_LIB_LOGE("Failed to reset"); return false; } -#endif return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.mode, _cfg.rate, _cfg.width, _cfg.ir_current, _cfg.high_resolution, _cfg.red_current) @@ -436,6 +432,7 @@ bool UnitMAX30100::reset() _periodic = false; _mode = mc.mode(); _retrieved = _overflow = 0; + m5::utility::delay(10); // Wait for registers to settle after POR return true; } m5::utility::delay(1); diff --git a/src/unit/unit_MAX30102.cpp b/src/unit/unit_MAX30102.cpp index 6351ce2..8d6c4e4 100644 --- a/src/unit/unit_MAX30102.cpp +++ b/src/unit/unit_MAX30102.cpp @@ -702,6 +702,7 @@ bool UnitMAX30102::reset() _mode = mc.mode(); _retrieved = _overflow = 0; _slot[0] = _slot[1] = Slot::None; + m5::utility::delay(10); // Wait for registers to settle after POR return true; } m5::utility::delay(1); diff --git a/src/unit/unit_MAX30102.hpp b/src/unit/unit_MAX30102.hpp index 473fb9f..38ec187 100644 --- a/src/unit/unit_MAX30102.hpp +++ b/src/unit/unit_MAX30102.hpp @@ -191,7 +191,7 @@ public: /*! @struct config_t @brief Settings for begin - @warning Note that some combinations of sampling_rate and pulse_width are invalid. See alse SpO2 configuration + @warning Note that some combinations of sampling_rate and pulse_width are invalid. See also SpO2 configuration */ struct config_t { /*! From 76b7b4e31580074a6d950c0f79e1bdd60dd11647 Mon Sep 17 00:00:00 2001 From: GOB Date: Wed, 4 Mar 2026 19:38:48 +0900 Subject: [PATCH 21/40] Migrate tests to I2CComponentTestBase for M5UnitUnified 0.4.0 --- test/embedded/test_max30100/max30100_test.cpp | 296 ++++--------- test/embedded/test_max30102/max30102_test.cpp | 390 +++++------------- 2 files changed, 179 insertions(+), 507 deletions(-) diff --git a/test/embedded/test_max30100/max30100_test.cpp b/test/embedded/test_max30100/max30100_test.cpp index 9cda2dd..054dd80 100644 --- a/test/embedded/test_max30100/max30100_test.cpp +++ b/test/embedded/test_max30100/max30100_test.cpp @@ -22,36 +22,9 @@ using namespace m5::unit::googletest; using namespace m5::unit; using namespace m5::unit::max30100; using namespace m5::unit::max30100::command; -using m5::unit::types::elapsed_time_t; -const ::testing::Environment* global_fixture = ::testing::AddGlobalTestEnvironment(new GlobalFixture<400000U>()); - -class TestMAX30100 : public ComponentTestBase { +class TestMAX30100 : public I2CComponentTestBase { protected: - virtual bool begin() override - { - // NessoN1 GROVE must use HAL - auto board = M5.getBoard(); - if (board == m5::board_t::board_ArduinoNessoN1 || is_using_hal()) { - // Using M5HAL - 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); - if (board == m5::board_t::board_ArduinoNessoN1) { - pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); - pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); - } - // M5_LOGI("pin:%d,%d", pin_num_sda, pin_num_scl); - - m5::hal::bus::I2CBusConfig i2c_cfg; - i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); - i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); - auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); - return Units.add(*unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); - } - - // Using TwoWire - return Units.add(*unit, Wire) && Units.begin(); - } virtual UnitMAX30100* get_instance() override { auto ptr = new m5::unit::UnitMAX30100(); @@ -61,18 +34,8 @@ protected: } return ptr; } - virtual bool is_using_hal() const override - { - return GetParam(); - }; }; -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestMAX30100, -// ::testing::Values(false, true)); -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestMAX30100, -// ::testing::Values(true)); -INSTANTIATE_TEST_SUITE_P(ParamValues, TestMAX30100, ::testing::Values(false)); - namespace { constexpr uint32_t STORED_SIZE{6}; @@ -160,6 +123,7 @@ void test_spo2_config(UnitMAX30100* unit, const Mode mode) } } +// Verify individual SpO2 parameter read/write APIs (roundtrip) void test_spo2_config_each(UnitMAX30100* unit, const Mode mode) { auto s = m5::utility::formatString("Mode:%u", mode); @@ -168,99 +132,83 @@ void test_spo2_config_each(UnitMAX30100* unit, const Mode mode) EXPECT_TRUE(unit->writeMode(mode)); EXPECT_TRUE(unit->writeSpO2Configuration(false, Sampling::Rate50, LEDPulse::Width200)); + // Resolution for (auto& res : res_table) { - auto s = m5::utility::formatString("RES:%u", res); - SCOPED_TRACE(s); - bool resolution{}; EXPECT_TRUE(unit->writeSpO2HighResolution(res)); EXPECT_TRUE(unit->readSpO2HighResolution(resolution)); EXPECT_EQ(resolution, res); + } - for (auto&& sr : sr_table) { - auto s = m5::utility::formatString("Rate:%u", sr); - SCOPED_TRACE(s); - - if (is_allowed_settings(mode, sr, LEDPulse::Width200)) { - Sampling rate{}; - - EXPECT_TRUE(unit->writeSpO2SamplingRate(sr)); - - EXPECT_TRUE(unit->readSpO2SamplingRate(rate)); - EXPECT_EQ(rate, sr); - - for (auto&& pw : pw_table) { - auto s = m5::utility::formatString("Width:%u", pw); - SCOPED_TRACE(s); - LEDPulse width{}; - - if (is_allowed_settings(mode, sr, pw)) { - EXPECT_TRUE(unit->writeSpO2LEDPulseWidth(pw)); - - EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width)); - EXPECT_EQ(width, pw); - } else { - LEDPulse width2{}; - EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width)); - - EXPECT_FALSE(unit->writeSpO2LEDPulseWidth(pw)); - - EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width2)); - EXPECT_EQ(width2, width); - } - } - EXPECT_TRUE(unit->writeSpO2LEDPulseWidth(LEDPulse::Width200)); - } else { - Sampling rate{}, rate2{}; - EXPECT_TRUE(unit->readSpO2SamplingRate(rate)); - - EXPECT_FALSE(unit->writeSpO2SamplingRate(sr)); - - EXPECT_TRUE(unit->readSpO2SamplingRate(rate2)); - EXPECT_EQ(rate2, rate); - } + // Sampling rate (allowed settings only) + for (auto&& sr : sr_table) { + if (is_allowed_settings(mode, sr, LEDPulse::Width200)) { + Sampling rate{}; + EXPECT_TRUE(unit->writeSpO2SamplingRate(sr)); + EXPECT_TRUE(unit->readSpO2SamplingRate(rate)); + EXPECT_EQ(rate, sr); + } + } + EXPECT_TRUE(unit->writeSpO2SamplingRate(Sampling::Rate50)); + + // LED pulse width (allowed settings only) + for (auto&& pw : pw_table) { + if (is_allowed_settings(mode, Sampling::Rate50, pw)) { + LEDPulse width{}; + EXPECT_TRUE(unit->writeSpO2LEDPulseWidth(pw)); + EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width)); + EXPECT_EQ(width, pw); } - EXPECT_TRUE(unit->writeSpO2Configuration(res, Sampling::Rate50, LEDPulse::Width200)); } } template -elapsed_time_t test_periodic(U* unit, const uint32_t times, const uint32_t measure_duration = 0) +void collect_and_verify(U* unit, uint32_t count, bool expect_ir, bool expect_red) { - auto tm = unit->interval(); - auto timeout_at = m5::utility::millis() + 10 * 1000; + auto timeout = std::max(unit->interval() * (count + 1) * 2, 2000); + auto result = collect_periodic_measurements(unit, count, timeout); - do { - unit->update(); - if (unit->updated()) { - break; - } - std::this_thread::yield(); - } while (!unit->updated() && m5::utility::millis() <= timeout_at); - // timeout - if (!unit->updated()) { - return 0; + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_FALSE(result.timed_out); + + EXPECT_GE(unit->available(), count); + EXPECT_FALSE(unit->empty()); + + uint32_t cnt{unit->available() / 2}; + uint32_t left = unit->available() - cnt; + uint32_t air{}, ared{}; + while (cnt-- && unit->available()) { + air += unit->ir(); + ared += unit->red(); + EXPECT_EQ(unit->oldest().ir(), unit->ir()); + EXPECT_EQ(unit->oldest().red(), unit->red()); + EXPECT_FALSE(unit->empty()); + unit->discard(); } + if (expect_ir) { + EXPECT_NE(air, 0); + } else { + EXPECT_EQ(air, 0); + } + if (expect_red) { + EXPECT_NE(ared, 0); + } else { + EXPECT_EQ(ared, 0); + } + + EXPECT_EQ(unit->available(), left); + EXPECT_FALSE(unit->empty()); + EXPECT_FALSE(unit->full()); + unit->flush(); + EXPECT_EQ(unit->available(), 0); + EXPECT_TRUE(unit->empty()); + EXPECT_FALSE(unit->full()); - // - uint32_t measured{}; - auto start_at = m5::utility::millis(); - timeout_at = start_at + (times * (tm + measure_duration) * 2); - - do { - unit->update(); - measured += unit->updated() ? 1 : 0; - if (measured >= times) { - break; - } - // std::this_thread::yield(); - m5::utility::delay(1); - - } while (measured < times && m5::utility::millis() <= timeout_at); - return (measured == times) ? m5::utility::millis() - start_at : 0; - // return (measured == times) ? unit->updatedMillis() - start_at : 0; + EXPECT_EQ(unit->ir(), 0); + EXPECT_EQ(unit->red(), 0); } void test_periodic_spo2(UnitMAX30100* unit) @@ -303,52 +251,7 @@ void test_periodic_spo2(UnitMAX30100* unit) SCOPED_TRACE(s); EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::SpO2, rate, width, LED::Current27_1, res, LED::Current27_1)); - auto it = unit->interval() ? unit->interval() : 1; - - auto elapsed = test_periodic(unit, STORED_SIZE, it); - - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - EXPECT_FALSE(unit->inPeriodic()); - - EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * - // unit->interval(), - // unit->retrieved(), unit->overflow()); - - EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) - EXPECT_FALSE(unit->empty()); - if (unit->available() == MAX_FIFO_DEPTH) { - EXPECT_TRUE(unit->full()); - } else { - EXPECT_FALSE(unit->full()); - } - - uint32_t cnt{unit->available() / 2}; - uint32_t left = unit->available() - cnt; - uint32_t air{}, ared{}; - while (cnt-- && unit->available()) { - air += unit->ir(); - ared += unit->red(); - EXPECT_EQ(unit->oldest().ir(), unit->ir()); - EXPECT_EQ(unit->oldest().red(), unit->red()); - - EXPECT_FALSE(unit->empty()); - unit->discard(); - } - EXPECT_NE(air, 0); - EXPECT_NE(ared, 0); - - EXPECT_EQ(unit->available(), left); - EXPECT_FALSE(unit->empty()); - EXPECT_FALSE(unit->full()); - - unit->flush(); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - - EXPECT_EQ(unit->ir(), 0); - EXPECT_EQ(unit->red(), 0); + collect_and_verify(unit, STORED_SIZE, true, true); } } @@ -393,57 +296,13 @@ void test_periodic_hr(UnitMAX30100* unit) SCOPED_TRACE(s); EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::HROnly, rate, width, LED::Current27_1, res)); - auto it = unit->interval() ? unit->interval() : 1; - - auto elapsed = test_periodic(unit, STORED_SIZE, it); - - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - EXPECT_FALSE(unit->inPeriodic()); - - EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * unit->interval(), - unit->retrieved(), unit->overflow()); - - EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) - EXPECT_FALSE(unit->empty()); - if (unit->available() == MAX_FIFO_DEPTH) { - EXPECT_TRUE(unit->full()); - } else { - EXPECT_FALSE(unit->full()); - } - - uint32_t cnt{unit->available() / 2}; - uint32_t left = unit->available() - cnt; - uint32_t air{}, ared{}; - while (cnt-- && unit->available()) { - air += unit->ir(); - ared += unit->red(); - EXPECT_EQ(unit->oldest().ir(), unit->ir()); - EXPECT_EQ(unit->oldest().red(), unit->red()); - - EXPECT_FALSE(unit->empty()); - unit->discard(); - } - EXPECT_NE(air, 0); - EXPECT_EQ(ared, 0); - - EXPECT_EQ(unit->available(), left); - EXPECT_FALSE(unit->empty()); - EXPECT_FALSE(unit->full()); - - unit->flush(); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - - EXPECT_EQ(unit->ir(), 0); - EXPECT_EQ(unit->red(), 0); + collect_and_verify(unit, STORED_SIZE, true, false); } } } // namespace -TEST_P(TestMAX30100, Mode) +TEST_F(TestMAX30100, Mode) { constexpr bool bool_table[] = {true, false}; @@ -480,7 +339,7 @@ TEST_P(TestMAX30100, Mode) } } -TEST_P(TestMAX30100, SpO2Configuration) +TEST_F(TestMAX30100, SpO2Configuration) { SCOPED_TRACE(ustr); @@ -497,12 +356,17 @@ TEST_P(TestMAX30100, SpO2Configuration) test_spo2_config_each(unit.get(), Mode::HROnly); } -TEST_P(TestMAX30100, LEDCurrent) +TEST_F(TestMAX30100, LEDCurrent) { SCOPED_TRACE(ustr); - for (auto&& ir : cur_table) { - for (auto&& red : cur_table) { + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + // Boundary values: min, mid, max + constexpr LED boundary[] = {LED::Current0_0, LED::Current24_0, LED::Current50_0}; + for (auto&& ir : boundary) { + for (auto&& red : boundary) { auto s = m5::utility::formatString("IR:%u Red:%u", ir, red); SCOPED_TRACE(s); EXPECT_TRUE(unit->writeLEDCurrent(ir, red)); @@ -515,7 +379,7 @@ TEST_P(TestMAX30100, LEDCurrent) } } -TEST_P(TestMAX30100, Temperature) +TEST_F(TestMAX30100, Temperature) { SCOPED_TRACE(ustr); @@ -561,7 +425,7 @@ TEST_P(TestMAX30100, Temperature) } } -TEST_P(TestMAX30100, Revision) +TEST_F(TestMAX30100, Revision) { SCOPED_TRACE(ustr); @@ -571,7 +435,7 @@ TEST_P(TestMAX30100, Revision) // M5_LOGI("Rev:%02X", rev); } -TEST_P(TestMAX30100, Reset) +TEST_F(TestMAX30100, Reset) { SCOPED_TRACE(ustr); @@ -621,7 +485,7 @@ TEST_P(TestMAX30100, Reset) EXPECT_EQ(cnt, 0U); } -TEST_P(TestMAX30100, Periodic) +TEST_F(TestMAX30100, Periodic) { SCOPED_TRACE(ustr); @@ -698,7 +562,7 @@ TEST_P(TestMAX30100, Periodic) } } -TEST_P(TestMAX30100, Periodic_SPO2) +TEST_F(TestMAX30100, Periodic_SPO2) { SCOPED_TRACE(ustr); @@ -709,7 +573,7 @@ TEST_P(TestMAX30100, Periodic_SPO2) test_periodic_spo2(unit.get()); } -TEST_P(TestMAX30100, Periodic_HR) +TEST_F(TestMAX30100, Periodic_HR) { SCOPED_TRACE(ustr); diff --git a/test/embedded/test_max30102/max30102_test.cpp b/test/embedded/test_max30102/max30102_test.cpp index 1332e11..d3e7e8c 100644 --- a/test/embedded/test_max30102/max30102_test.cpp +++ b/test/embedded/test_max30102/max30102_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,7 +21,6 @@ using namespace m5::unit::googletest; using namespace m5::unit; using namespace m5::unit::max30102; using namespace m5::unit::max30102::command; -using m5::unit::types::elapsed_time_t; namespace hat { struct I2cPins { @@ -46,45 +46,16 @@ I2cPins get_hat_i2c_pins(const m5::board_t board) } } -template -class GlobalFixture : public ::testing::Environment { - static_assert(WNUM < 2, "Wire number must be lesser than 2"); - -public: - void SetUp() override - { - const auto pins = get_hat_i2c_pins(M5.getBoard()); - // M5_LOGI("pin:%d %d", pins.sda, pins.scl); - - pinMode(pins.scl, OUTPUT); - - TwoWire* w[2] = {&Wire, &Wire1}; - if (WNUM < m5::stl::size(w) && i2cIsInit(WNUM)) { - M5_LOGW("Already inititlized Wire %d. Terminate and restart FREQ %u", WNUM, FREQ); - w[WNUM]->end(); - } - w[WNUM]->begin(pins.sda, pins.scl, FREQ); - } -}; - } // namespace hat -const ::testing::Environment* global_fixture = - ::testing::AddGlobalTestEnvironment(new hat::GlobalFixture<400000U, 1 /* Wire1 */>()); -class TestMAX30102 : public ComponentTestBase { +class TestMAX30102 : public I2CComponentTestBase { protected: virtual bool begin() override { - if (is_using_hal()) { - const auto pins = hat::get_hat_i2c_pins(M5.getBoard()); - m5::hal::bus::I2CBusConfig i2c_cfg; - i2c_cfg.pin_sda = m5::hal::gpio::getPin(pins.sda); - i2c_cfg.pin_scl = m5::hal::gpio::getPin(pins.scl); - auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); - return Units.add(*unit, i2c_bus ? i2c_bus.value() : nullptr) && Units.begin(); - } - - // Using TwoWire + const auto pins = hat::get_hat_i2c_pins(M5.getBoard()); + pinMode(pins.scl, OUTPUT); + Wire1.end(); + Wire1.begin(pins.sda, pins.scl, unit->component_config().clock); return Units.add(*unit, Wire1) && Units.begin(); } @@ -97,16 +68,8 @@ protected: } return ptr; } - virtual bool is_using_hal() const override - { - return GetParam(); - }; }; -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestMAX30102, ::testing::Values(false, true)); -// INSTANTIATE_TEST_SUITE_P(ParamValues, TestMAX30102, ::testing::Values(true)); -INSTANTIATE_TEST_SUITE_P(ParamValues, TestMAX30102, ::testing::Values(false)); - namespace { auto rng = std::default_random_engine{}; @@ -230,6 +193,7 @@ void test_spo2_config(UnitMAX30102* unit, const Mode mode) } } +// Verify individual SpO2 parameter read/write APIs (roundtrip) void test_spo2_config_each(UnitMAX30102* unit, const Mode mode) { auto s = m5::utility::formatString("Mode:%u", mode); @@ -238,99 +202,87 @@ void test_spo2_config_each(UnitMAX30102* unit, const Mode mode) EXPECT_TRUE(unit->writeMode(mode)); EXPECT_TRUE(unit->writeSpO2Configuration(ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width69)); + // ADC range for (auto& rg : range_table) { - auto s = m5::utility::formatString("Range:%u", rg); - SCOPED_TRACE(s); - ADC range{}; EXPECT_TRUE(unit->writeSpO2ADCRange(rg)); EXPECT_TRUE(unit->readSpO2ADCRange(range)); EXPECT_EQ(range, rg); + } - for (auto&& sr : sr_table) { - auto s = m5::utility::formatString("Rate:%u", sr); - SCOPED_TRACE(s); - - if (is_allowed_settings(mode, sr, LEDPulse::Width69)) { - Sampling rate{}; - - EXPECT_TRUE(unit->writeSpO2SamplingRate(sr)); - - EXPECT_TRUE(unit->readSpO2SamplingRate(rate)); - EXPECT_EQ(rate, sr); - - for (auto&& pw : pw_table) { - auto s = m5::utility::formatString("Width:%u", pw); - SCOPED_TRACE(s); - LEDPulse width{}; - - if (is_allowed_settings(mode, sr, pw)) { - EXPECT_TRUE(unit->writeSpO2LEDPulseWidth(pw)); - - EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width)); - EXPECT_EQ(width, pw); - } else { - LEDPulse width2{}; - EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width)); - - EXPECT_FALSE(unit->writeSpO2LEDPulseWidth(pw)); - - EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width2)); - EXPECT_EQ(width2, width); - } - } - EXPECT_TRUE(unit->writeSpO2LEDPulseWidth(LEDPulse::Width69)); - } else { - Sampling rate{}, rate2{}; - EXPECT_TRUE(unit->readSpO2SamplingRate(rate)); - - EXPECT_FALSE(unit->writeSpO2SamplingRate(sr)); - - EXPECT_TRUE(unit->readSpO2SamplingRate(rate2)); - EXPECT_EQ(rate2, rate); - } + // Sampling rate (allowed settings only) + for (auto&& sr : sr_table) { + if (is_allowed_settings(mode, sr, LEDPulse::Width69)) { + Sampling rate{}; + EXPECT_TRUE(unit->writeSpO2SamplingRate(sr)); + EXPECT_TRUE(unit->readSpO2SamplingRate(rate)); + EXPECT_EQ(rate, sr); + } + } + EXPECT_TRUE(unit->writeSpO2SamplingRate(Sampling::Rate50)); + + // LED pulse width (allowed settings only) + for (auto&& pw : pw_table) { + if (is_allowed_settings(mode, Sampling::Rate50, pw)) { + LEDPulse width{}; + EXPECT_TRUE(unit->writeSpO2LEDPulseWidth(pw)); + EXPECT_TRUE(unit->readSpO2LEDPulseWidth(width)); + EXPECT_EQ(width, pw); } - EXPECT_TRUE(unit->writeSpO2Configuration(rg, Sampling::Rate50, LEDPulse::Width69)); } } template -elapsed_time_t test_periodic(U* unit, const uint32_t times, const uint32_t measure_duration = 0) +void collect_and_verify(U* unit, uint32_t count, bool expect_ir, bool expect_red, uint32_t mask = 0) { - auto tm = unit->interval(); - auto timeout_at = m5::utility::millis() + 10 * 1000; + auto timeout = std::max(unit->interval() * (count + 1) * 2, 2000); + auto result = collect_periodic_measurements(unit, count, timeout); - do { - unit->update(); - if (unit->updated()) { - break; + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + EXPECT_FALSE(result.timed_out); + + EXPECT_GE(unit->available(), count); + EXPECT_FALSE(unit->empty()); + + uint32_t cnt{unit->available() / 2}; + uint32_t left = unit->available() - cnt; + uint32_t air{}, ared{}; + while (cnt-- && unit->available()) { + air += unit->ir(); + ared += unit->red(); + if (mask) { + EXPECT_LE(unit->ir(), mask); + EXPECT_LE(unit->red(), mask); } - std::this_thread::yield(); - } while (!unit->updated() && m5::utility::millis() <= timeout_at); - // timeout - if (!unit->updated()) { - return 0; + EXPECT_EQ(unit->oldest().ir(), unit->ir()); + EXPECT_EQ(unit->oldest().red(), unit->red()); + EXPECT_FALSE(unit->empty()); + unit->discard(); } + if (expect_ir) { + EXPECT_NE(air, 0); + } else { + EXPECT_EQ(air, 0); + } + if (expect_red) { + EXPECT_NE(ared, 0); + } else { + EXPECT_EQ(ared, 0); + } + + EXPECT_EQ(unit->available(), left); + EXPECT_FALSE(unit->empty()); + EXPECT_FALSE(unit->full()); + unit->flush(); + EXPECT_EQ(unit->available(), 0); + EXPECT_TRUE(unit->empty()); + EXPECT_FALSE(unit->full()); - // - uint32_t measured{}; - auto start_at = m5::utility::millis(); - timeout_at = start_at + (times * (tm + measure_duration) * 2); - - do { - unit->update(); - measured += unit->updated() ? 1 : 0; - if (measured >= times) { - break; - } - // std::this_thread::yield(); - m5::utility::delay(1); - - } while (measured < times && m5::utility::millis() <= timeout_at); - return (measured == times) ? m5::utility::millis() - start_at : 0; - // return (measured == times) ? unit->updatedMillis() - start_at : 0; + EXPECT_EQ(unit->ir(), 0); + EXPECT_EQ(unit->red(), 0); } void test_periodic_spo2(UnitMAX30102* unit) @@ -391,57 +343,9 @@ void test_periodic_spo2(UnitMAX30102* unit) auto s = m5::utility::formatString("SPO2 RNG:%u SR:%u WID:%u AVG:%u", range, rate, width, avg); SCOPED_TRACE(s); - EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::SpO2, range, rate, width, avg, 0x1f, 0x1f)); - auto it = unit->interval() ? unit->interval() : 1; - - auto elapsed = test_periodic(unit, STORED_SIZE, it); - - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - EXPECT_FALSE(unit->inPeriodic()); - - EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * - // unit->interval(), - // unit->retrieved(), unit->overflow()); - - EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) - EXPECT_FALSE(unit->empty()); - if (unit->available() == MAX_FIFO_DEPTH) { - EXPECT_TRUE(unit->full()); - } else { - EXPECT_FALSE(unit->full()); - } - uint32_t mask = adc_resolution_bits_table[m5::stl::to_underlying(width)]; - - uint32_t cnt{unit->available() / 2}; - uint32_t left = unit->available() - cnt; - uint32_t air{}, ared{}; - while (cnt-- && unit->available()) { - air += unit->ir(); - ared += unit->red(); - EXPECT_LE(unit->ir(), mask); - EXPECT_LE(unit->red(), mask); - EXPECT_EQ(unit->oldest().ir(), unit->ir()); - EXPECT_EQ(unit->oldest().red(), unit->red()); - - EXPECT_FALSE(unit->empty()); - unit->discard(); - } - EXPECT_NE(air, 0); - EXPECT_NE(ared, 0); - - EXPECT_EQ(unit->available(), left); - EXPECT_FALSE(unit->empty()); - EXPECT_FALSE(unit->full()); - - unit->flush(); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - - EXPECT_EQ(unit->ir(), 0); - EXPECT_EQ(unit->red(), 0); + EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::SpO2, range, rate, width, avg, 0x1f, 0x1f)); + collect_and_verify(unit, STORED_SIZE, true, true, mask); } } @@ -509,57 +413,9 @@ void test_periodic_hr(UnitMAX30102* unit) auto s = m5::utility::formatString("HR RNG:%u SR:%u WID:%u AVG:%u", range, rate, width, avg); SCOPED_TRACE(s); - EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::HROnly, range, rate, width, avg, 0x1f, 0x1f)); - auto it = unit->interval() ? unit->interval() : 1; - - auto elapsed = test_periodic(unit, STORED_SIZE, it); - - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - EXPECT_FALSE(unit->inPeriodic()); - - EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * - // unit->interval(), - // unit->retrieved(), unit->overflow()); - - EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) - EXPECT_FALSE(unit->empty()); - if (unit->available() == MAX_FIFO_DEPTH) { - EXPECT_TRUE(unit->full()); - } else { - EXPECT_FALSE(unit->full()); - } - uint32_t mask = adc_resolution_bits_table[m5::stl::to_underlying(width)]; - - uint32_t cnt{unit->available() / 2}; - uint32_t left = unit->available() - cnt; - uint32_t air{}, ared{}; - while (cnt-- && unit->available()) { - air += unit->ir(); - ared += unit->red(); - EXPECT_LE(unit->ir(), mask); - EXPECT_LE(unit->red(), mask); - EXPECT_EQ(unit->oldest().ir(), unit->ir()); - EXPECT_EQ(unit->oldest().red(), unit->red()); - - EXPECT_FALSE(unit->empty()); - unit->discard(); - } - EXPECT_NE(air, 0); - EXPECT_EQ(ared, 0); - - EXPECT_EQ(unit->available(), left); - EXPECT_FALSE(unit->empty()); - EXPECT_FALSE(unit->full()); - - unit->flush(); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - - EXPECT_EQ(unit->ir(), 0); - EXPECT_EQ(unit->red(), 0); + EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::HROnly, range, rate, width, avg, 0x1f, 0x1f)); + collect_and_verify(unit, STORED_SIZE, true, false, mask); } } @@ -580,73 +436,16 @@ void test_periodic_multi(UnitMAX30102* unit) SCOPED_TRACE(s); EXPECT_TRUE(unit->writeMultiLEDModeControl(slot1, slot2)); - EXPECT_TRUE(unit->startPeriodicMeasurement()); - auto it = unit->interval() ? unit->interval() : 1; - - auto elapsed = test_periodic(unit, STORED_SIZE, it); - - EXPECT_TRUE(unit->stopPeriodicMeasurement()); - EXPECT_FALSE(unit->inPeriodic()); - - EXPECT_GE(elapsed, STORED_SIZE * unit->interval()); - // M5_LOGI(">>> %s>elapsed: %ld/%u retrieved:%u overflow:%u", s.c_str(), elapsed, STORED_SIZE * - // unit->interval(), - // unit->retrieved(), unit->overflow()); - - EXPECT_GE(unit->available(), STORED_SIZE); // Check GE not EQ! (because FIFO) - EXPECT_FALSE(unit->empty()); - if (unit->available() == MAX_FIFO_DEPTH) { - EXPECT_TRUE(unit->full()); - } else { - EXPECT_FALSE(unit->full()); - } - - uint32_t mask = 0x3FFFF; - - uint32_t cnt{unit->available() / 2}; - uint32_t left = unit->available() - cnt; - uint32_t air{}, ared{}; - while (cnt-- && unit->available()) { - air += unit->ir(); - ared += unit->red(); - EXPECT_LE(unit->ir(), mask); - EXPECT_LE(unit->red(), mask); - EXPECT_EQ(unit->oldest().ir(), unit->ir()); - EXPECT_EQ(unit->oldest().red(), unit->red()); - - EXPECT_FALSE(unit->empty()); - unit->discard(); - } - if (slot1 == Slot::IR || slot2 == Slot::IR) { - EXPECT_NE(air, 0); - } else { - EXPECT_EQ(air, 0); - } - - if (slot1 == Slot::Red || slot2 == Slot::Red) { - EXPECT_NE(ared, 0); - } else { - EXPECT_EQ(ared, 0); - } - - EXPECT_EQ(unit->available(), left); - EXPECT_FALSE(unit->empty()); - EXPECT_FALSE(unit->full()); - - unit->flush(); - EXPECT_EQ(unit->available(), 0); - EXPECT_TRUE(unit->empty()); - EXPECT_FALSE(unit->full()); - - EXPECT_EQ(unit->ir(), 0); - EXPECT_EQ(unit->red(), 0); + bool has_ir = (slot1 == Slot::IR || slot2 == Slot::IR); + bool has_red = (slot1 == Slot::Red || slot2 == Slot::Red); + collect_and_verify(unit, STORED_SIZE, has_ir, has_red, 0x3FFFF); } } } // namespace -TEST_P(TestMAX30102, Mode) +TEST_F(TestMAX30102, Mode) { constexpr bool bool_table[] = {true, false}; @@ -682,7 +481,7 @@ TEST_P(TestMAX30102, Mode) } } -TEST_P(TestMAX30102, SpO2Configuration) +TEST_F(TestMAX30102, SpO2Configuration) { SCOPED_TRACE(ustr); @@ -702,11 +501,19 @@ TEST_P(TestMAX30102, SpO2Configuration) test_spo2_config_each(unit.get(), Mode::MultiLED); } -TEST_P(TestMAX30102, LEDCurrent) +TEST_F(TestMAX30102, LEDCurrent) { SCOPED_TRACE(ustr); - for (uint16_t cur = 0; cur < 256; ++cur) { + EXPECT_TRUE(unit->stopPeriodicMeasurement()); + EXPECT_FALSE(unit->inPeriodic()); + + // Boundary values: min, near-min, mid, near-max, max + constexpr uint8_t boundary[] = {0, 1, 127, 128, 254, 255}; + for (auto cur : boundary) { + auto s = m5::utility::formatString("cur:%u", cur); + SCOPED_TRACE(s); + EXPECT_TRUE(unit->writeLEDCurrent(0, cur)); EXPECT_TRUE(unit->writeLEDCurrent(1, cur)); @@ -727,6 +534,7 @@ TEST_P(TestMAX30102, LEDCurrent) EXPECT_FLOAT_EQ(f, mA); } + // Out-of-range (float) EXPECT_FALSE(unit->writeLEDCurrent(0, -0.01f)); EXPECT_FALSE(unit->writeLEDCurrent(1, -0.01f)); @@ -734,7 +542,7 @@ TEST_P(TestMAX30102, LEDCurrent) EXPECT_FALSE(unit->writeLEDCurrent(1, 51.01f)); } -TEST_P(TestMAX30102, MultiLEDMode) +TEST_F(TestMAX30102, MultiLEDMode) { SCOPED_TRACE(ustr); @@ -805,7 +613,7 @@ TEST_P(TestMAX30102, MultiLEDMode) } } -TEST_P(TestMAX30102, FIFOConfiguration) +TEST_F(TestMAX30102, FIFOConfiguration) { SCOPED_TRACE(ustr); @@ -832,7 +640,7 @@ TEST_P(TestMAX30102, FIFOConfiguration) } } -TEST_P(TestMAX30102, Temperature) +TEST_F(TestMAX30102, Temperature) { SCOPED_TRACE(ustr); @@ -877,7 +685,7 @@ TEST_P(TestMAX30102, Temperature) } } -TEST_P(TestMAX30102, Revision) +TEST_F(TestMAX30102, Revision) { SCOPED_TRACE(ustr); @@ -886,7 +694,7 @@ TEST_P(TestMAX30102, Revision) EXPECT_NE(rev, 0); } -TEST_P(TestMAX30102, Reset) +TEST_F(TestMAX30102, Reset) { SCOPED_TRACE(ustr); @@ -952,7 +760,7 @@ TEST_P(TestMAX30102, Reset) EXPECT_EQ(cnt, 0U); } -TEST_P(TestMAX30102, Periodic) +TEST_F(TestMAX30102, Periodic) { SCOPED_TRACE(ustr); @@ -1027,7 +835,7 @@ TEST_P(TestMAX30102, Periodic) } } -TEST_P(TestMAX30102, Periodic_SPO2) +TEST_F(TestMAX30102, Periodic_SPO2) { SCOPED_TRACE(ustr); @@ -1038,7 +846,7 @@ TEST_P(TestMAX30102, Periodic_SPO2) test_periodic_spo2(unit.get()); } -TEST_P(TestMAX30102, Periodic_HR) +TEST_F(TestMAX30102, Periodic_HR) { SCOPED_TRACE(ustr); @@ -1049,7 +857,7 @@ TEST_P(TestMAX30102, Periodic_HR) test_periodic_hr(unit.get()); } -TEST_P(TestMAX30102, Periodic_MultiLED) +TEST_F(TestMAX30102, Periodic_MultiLED) { SCOPED_TRACE(ustr); From c19351009b3eb0118b13cbf13daacf73e3a5d65c Mon Sep 17 00:00:00 2001 From: GOB Date: Wed, 4 Mar 2026 19:39:21 +0900 Subject: [PATCH 22/40] Fixes examples I2C 3-way branching and lcd.clear to fillScreen --- .../DualSensor/main/DualSensor.cpp | 4 +- .../GraphicalMeter/main/GraphicalMeter.cpp | 63 ++++++++++--------- .../PlotToSerial/main/PlotToSerial.cpp | 53 +++++++++------- 3 files changed, 66 insertions(+), 54 deletions(-) diff --git a/examples/UnitUnified/DualSensor/main/DualSensor.cpp b/examples/UnitUnified/DualSensor/main/DualSensor.cpp index b542757..b8b8150 100644 --- a/examples/UnitUnified/DualSensor/main/DualSensor.cpp +++ b/examples/UnitUnified/DualSensor/main/DualSensor.cpp @@ -71,7 +71,7 @@ void setup() M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); if (pins.sda < 0 || pins.scl < 0) { M5_LOGE("Hat port not exists"); - lcd.clear(TFT_RED); + lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } @@ -123,7 +123,7 @@ void setup() if (!hat_ready || !unit_ready || !Units.begin()) { M5_LOGE("Failed to begin %u/%u", hat_ready, unit_ready); M5_LOGE("%s", Units.debugInfo().c_str()); - lcd.clear(TFT_RED); + lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } diff --git a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp index 879d1f2..a346956 100644 --- a/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp +++ b/examples/UnitUnified/GraphicalMeter/main/GraphicalMeter.cpp @@ -89,10 +89,10 @@ void setup() #if defined(USING_HAT_HEART) const auto pins = get_hat_i2c_pins(board); - M5_LOGI("getHatPin: SDA:%u SCL:%u Wire1", pins.sda, pins.scl); + M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); if (pins.sda < 0 || pins.scl < 0) { M5_LOGE("Illegal pin number"); - lcd.clear(TFT_RED); + lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } @@ -101,47 +101,54 @@ void setup() // Setup required to use HatHEART pinMode(pins.scl, OUTPUT); - Wire1.end(); - Wire1.begin(pins.sda, pins.scl, 400 * 1000U); - if (!Units.add(heart, Wire1) || !Units.begin()) { + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; + wire.end(); + wire.begin(pins.sda, pins.scl, 400 * 1000U); + if (!Units.add(heart, wire) || !Units.begin()) { M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); + lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } } #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); - // For NessoN1 GROVE + // NessoN1: Arduino Wire (I2C_NUM_0) cannot be used for GROVE port. + // Wire is used by M5Unified In_I2C for internal devices (IOExpander etc.). + // Wire1 exists but is reserved for HatPort — cannot be used for GROVE. + // Reconfiguring Wire to GROVE pins breaks In_I2C, causing ESP_ERR_INVALID_STATE in M5.update(). + // Solution: Use SoftwareI2C via M5HAL (bit-banging) for the GROVE port. + // NanoC6: Wire.begin() on GROVE pins conflicts with m5::I2C_Class registered by Ex_I2C.setPort() + // on the same I2C_NUM_0, causing sporadic NACK errors. + // Solution: Use M5.Ex_I2C (m5::I2C_Class) directly instead of Arduino Wire. + bool unit_ready{}; if (board == m5::board_t::board_ArduinoNessoN1) { - // Wire is used internally, so SoftwareI2C handles the unit - pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); - pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); - M5_LOGI("getPin(NessoN1): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); + // NessoN1: GROVE is on port_b (GPIO 5/4), not port_a (which maps to Wire pins 8/10) + auto pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); + auto pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); + M5_LOGI("getPin(M5HAL): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); m5::hal::bus::I2CBusConfig i2c_cfg; i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); M5_LOGI("Bus:%d", i2c_bus.has_value()); - if (!Units.add(heart, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } + unit_ready = Units.add(heart, 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"); + unit_ready = Units.add(heart, M5.Ex_I2C) && Units.begin(); } else { - // Using TwoWire + 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, 400000U); - if (!Units.add(heart, Wire) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } + unit_ready = Units.add(heart, Wire) && Units.begin(); + } + if (!unit_ready) { + M5_LOGE("Failed to begin"); + lcd.fillScreen(TFT_RED); + while (true) { + m5::utility::delay(10000); } } #endif @@ -149,7 +156,7 @@ void setup() M5_LOGI("M5UnitUnified has been begun"); M5_LOGI("%s", Units.debugInfo().c_str()); - lcd.clear(0); + lcd.fillScreen(0); #if defined(USING_UNIT_HEART) view = new View(lcd.width(), lcd.height(), true); diff --git a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp index 23d55d6..f7e027b 100644 --- a/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp +++ b/examples/UnitUnified/PlotToSerial/main/PlotToSerial.cpp @@ -94,7 +94,7 @@ void setup() M5_LOGI("getHatPin: SDA:%u SCL:%u", pins.sda, pins.scl); if (pins.sda < 0 || pins.scl < 0) { M5_LOGE("Illegal pin number"); - lcd.clear(TFT_RED); + lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } @@ -115,44 +115,49 @@ void setup() wire.begin(pins.sda, pins.scl, 400 * 1000U); if (!Units.add(unit, wire) || !Units.begin()) { M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); + lcd.fillScreen(TFT_RED); while (true) { m5::utility::delay(10000); } } #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); - // For NessoN1 GROVE + // NessoN1: Arduino Wire (I2C_NUM_0) cannot be used for GROVE port. + // Wire is used by M5Unified In_I2C for internal devices (IOExpander etc.). + // Wire1 exists but is reserved for HatPort — cannot be used for GROVE. + // Reconfiguring Wire to GROVE pins breaks In_I2C, causing ESP_ERR_INVALID_STATE in M5.update(). + // Solution: Use SoftwareI2C via M5HAL (bit-banging) for the GROVE port. + // NanoC6: Wire.begin() on GROVE pins conflicts with m5::I2C_Class registered by Ex_I2C.setPort() + // on the same I2C_NUM_0, causing sporadic NACK errors. + // Solution: Use M5.Ex_I2C (m5::I2C_Class) directly instead of Arduino Wire. + bool unit_ready{}; if (board == m5::board_t::board_ArduinoNessoN1) { - // Port A of the NessoN1 is QWIIC, then use portB (GROVE) - pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); - pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); - M5_LOGI("getPin(NessoN1): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); - // Wire is used internally, so SoftwareI2C handles the unit + // NessoN1: GROVE is on port_b (GPIO 5/4), not port_a (which maps to Wire pins 8/10) + auto pin_num_sda = M5.getPin(m5::pin_name_t::port_b_out); + auto pin_num_scl = M5.getPin(m5::pin_name_t::port_b_in); + M5_LOGI("getPin(M5HAL): SDA:%u SCL:%u", pin_num_sda, pin_num_scl); m5::hal::bus::I2CBusConfig i2c_cfg; i2c_cfg.pin_sda = m5::hal::gpio::getPin(pin_num_sda); i2c_cfg.pin_scl = m5::hal::gpio::getPin(pin_num_scl); auto i2c_bus = m5::hal::bus::i2c::getBus(i2c_cfg); M5_LOGI("Bus:%d", i2c_bus.has_value()); - if (!Units.add(unit, i2c_bus ? i2c_bus.value() : nullptr) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } - } + 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"); + unit_ready = Units.add(unit, M5.Ex_I2C) && Units.begin(); } else { - // Using TwoWire + 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 (!Units.add(unit, Wire) || !Units.begin()) { - M5_LOGE("Failed to begin"); - lcd.clear(TFT_RED); - while (true) { - m5::utility::delay(10000); - } + unit_ready = Units.add(unit, Wire) && Units.begin(); + } + if (!unit_ready) { + M5_LOGE("Failed to begin"); + lcd.fillScreen(TFT_RED); + while (true) { + m5::utility::delay(10000); } } #endif From cdc7d9691ee2ad35abad3b1be1d66bcf384f7e55 Mon Sep 17 00:00:00 2001 From: GOB Date: Wed, 4 Mar 2026 19:40:15 +0900 Subject: [PATCH 23/40] Bump M5UnitUnified dependency to 0.4.0 and fix platformio.ini build flags --- library.json | 2 +- platformio.ini | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/library.json b/library.json index 320a4d5..703f5b4 100644 --- a/library.json +++ b/library.json @@ -11,7 +11,7 @@ "url": "https://github.com/m5stack/M5Unit-HEART.git" }, "dependencies": { - "m5stack/M5UnitUnified": ">=0.3.0" + "m5stack/M5UnitUnified": ">=0.4.0" }, "version": "0.2.0", "frameworks": [ diff --git a/platformio.ini b/platformio.ini index 990dee8..3ba5ef4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ lib_ldf_mode = deep test_framework = googletest test_build_src = true lib_deps=m5stack/M5Unified - m5stack/M5UnitUnified@>=0.3.0 + m5stack/M5UnitUnified@>=0.4.0 [m5base] monitor_speed = 115200 @@ -211,7 +211,7 @@ build_flags = ${env.build_flags} -DM5_LOG_LEVEL=0 -Wl,-Map,output.map -; Require at leaset C++14 after 1.13.0 +; Require at least C++14 after 1.13.0 [test_fw] lib_deps = google/googletest@1.12.1 @@ -313,12 +313,16 @@ test_filter= embedded/test_max30100 [env:test_UnitHeart_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_max30100 [env:test_UnitHeart_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_max30100 @@ -465,13 +469,15 @@ build_flags = ${option_release.build_flags} [env:UnitHeart_PlotToSerial_Cardputer_Arduino_latest] extends=Cardputer, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> -build_flags = ${option_release.build_flags} +build_flags = ${Cardputer.build_flags} + ${option_release.build_flags} -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_Tab5_Arduino_latest] extends=Tab5, option_release, pioarduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> -build_flags = ${option_release.build_flags} +build_flags = ${Tab5.build_flags} + ${option_release.build_flags} -D USING_UNIT_HEART [env:UnitHeart_PlotToSerial_NessoN1_Arduino_latest] @@ -536,8 +542,11 @@ build_flags = ${option_release.build_flags} ;-------------------------------- ;DualSensor (Using Unit and Hat) -;-------------------------------- +; For thisg samples, please refer to PlotToSerial to create env for your own environment +; こののサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください ;Example of using M5UnitUnified to connect both UnitHeart and HatHeart [env:DualSensor_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> + + From c2f92f1f4054e96a30b8cd8b19da0445f1f86a63 Mon Sep 17 00:00:00 2001 From: GOB Date: Wed, 4 Mar 2026 19:40:45 +0900 Subject: [PATCH 24/40] Fixes Doxyfile quoting and typos --- docs/Doxyfile | 2 +- docs/doxy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index c820920..232301f 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -42,7 +42,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = M5Unit-HEART +PROJECT_NAME = "M5Unit-HEART" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version diff --git a/docs/doxy.sh b/docs/doxy.sh index 064edd0..d4492c9 100755 --- a/docs/doxy.sh +++ b/docs/doxy.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Please execute on repositry root +# Please execute on repository root ## Get version from library.properties ## Get git rev of HEAD From 556ed1294346440caa95bb91eb3365e6b655b26c Mon Sep 17 00:00:00 2001 From: GOB Date: Wed, 4 Mar 2026 19:41:34 +0900 Subject: [PATCH 25/40] Fixes README typos and links --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bcf7b88..bb430ca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Overview -Library for UnitHEART [M5UnitUnified](https://github.com/m5stack/M5UnitUnified). +Library for UnitHEART using [M5UnitUnified](https://github.com/m5stack/M5UnitUnified). M5UnitUnified is a library for unified handling of various M5 units products. ### SKU:U029 @@ -15,13 +15,13 @@ The MAX30100 provides very small total solution size without sacrificing optical ### SKU:U118 -HEART RATE HAT is a blood oxygen heart rate sensor. Integrate MAX30102 to provide a complete pulse oximeter and heart rate sensor system solution. This is a non-pluggable blood oxygen heart rate sensor. The sensor uses I2C communication interface, internally integrates infrared light-emitting diodes, photo-detectors, optical components and low-noise electronic equipment. A certain amount of ambient light suppression function can make the measurement results more accurate. . +HEART RATE HAT is a blood oxygen heart rate sensor. Integrate MAX30102 to provide a complete pulse oximeter and heart rate sensor system solution. This is a non-pluggable blood oxygen heart rate sensor. The sensor uses I2C communication interface, internally integrates infrared light-emitting diodes, photo-detectors, optical components and low-noise electronic equipment. A certain amount of ambient light suppression function can make the measurement results more accurate. ## Related Link See also examples using conventional methods here. -- [Unit Heart & Datasheet](https://docs.m5stack.com/ja/unit/heart) +- [Unit Heart & Datasheet](https://docs.m5stack.com/en/unit/heart) - [Hat Heart & Datasheet](https://docs.m5stack.com/en/hat/hat_heart_rate) ## Required Libraries: @@ -32,7 +32,7 @@ See also examples using conventional methods here. ## License -- [M5Unit-HEART- MIT](LICENSE) +- [M5Unit-HEART - MIT](LICENSE) ## Examples @@ -51,6 +51,6 @@ It will output it under docs/html If you want to output Git commit hashes to html, do it for the git cloned folder. ### Required -- [Doxyegn](https://www.doxygen.nl/) +- [Doxygen](https://www.doxygen.nl/) - [pcregrep](https://formulae.brew.sh/formula/pcre2) - [Git](https://git-scm.com/) (Output commit hash to html) From d98a87557b93d8c5516c61de6d739db4cdfe09b9 Mon Sep 17 00:00:00 2001 From: GOB Date: Fri, 6 Mar 2026 12:00:03 +0900 Subject: [PATCH 26/40] Bump M5UnitUnified dependency to 0.4.1 --- library.json | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library.json b/library.json index 703f5b4..2175f75 100644 --- a/library.json +++ b/library.json @@ -11,7 +11,7 @@ "url": "https://github.com/m5stack/M5Unit-HEART.git" }, "dependencies": { - "m5stack/M5UnitUnified": ">=0.4.0" + "m5stack/M5UnitUnified": ">=0.4.1" }, "version": "0.2.0", "frameworks": [ diff --git a/platformio.ini b/platformio.ini index 3ba5ef4..17119ea 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ lib_ldf_mode = deep test_framework = googletest test_build_src = true lib_deps=m5stack/M5Unified - m5stack/M5UnitUnified@>=0.4.0 + m5stack/M5UnitUnified@>=0.4.1 [m5base] monitor_speed = 115200 From 8e9ea727c198472a660f404b2d85ca282d31c319 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 9 Mar 2026 17:20:24 +0900 Subject: [PATCH 27/40] Fixes MAX30102 test bugs and NessoN1 Hat port I2C selection --- test/embedded/test_max30102/max30102_test.cpp | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/embedded/test_max30102/max30102_test.cpp b/test/embedded/test_max30102/max30102_test.cpp index d3e7e8c..5d9cfce 100644 --- a/test/embedded/test_max30102/max30102_test.cpp +++ b/test/embedded/test_max30102/max30102_test.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include using namespace m5::unit::googletest; using namespace m5::unit; @@ -52,11 +52,14 @@ class TestMAX30102 : public I2CComponentTestBase { protected: virtual bool begin() override { - const auto pins = hat::get_hat_i2c_pins(M5.getBoard()); + auto board = M5.getBoard(); + const auto pins = hat::get_hat_i2c_pins(board); + // NessoN1: Wire is used by M5Unified In_I2C; use Wire1 for Hat port + auto& wire = (board == m5::board_t::board_ArduinoNessoN1) ? Wire1 : Wire; pinMode(pins.scl, OUTPUT); - Wire1.end(); - Wire1.begin(pins.sda, pins.scl, unit->component_config().clock); - return Units.add(*unit, Wire1) && Units.begin(); + wire.end(); + wire.begin(pins.sda, pins.scl, unit->component_config().clock); + return Units.add(*unit, wire) && Units.begin(); } virtual UnitMAX30102* get_instance() override @@ -71,7 +74,7 @@ protected: }; namespace { -auto rng = std::default_random_engine{}; +// esp_random() used instead of std::default_random_engine constexpr uint32_t STORED_SIZE{4}; @@ -185,8 +188,8 @@ void test_spo2_config(UnitMAX30102* unit, const Mode mode) LEDPulse width2{}; EXPECT_TRUE(unit->readSpO2Configuration(range2, rate2, width2)); EXPECT_EQ(range2, range); - EXPECT_EQ(rate2, rate2); - EXPECT_EQ(width2, width2); + EXPECT_EQ(rate2, rate); + EXPECT_EQ(width2, width); } } } @@ -561,7 +564,6 @@ TEST_F(TestMAX30102, MultiLEDMode) EXPECT_TRUE(unit->writeMultiLEDModeControl(slots[0], slots[1])); EXPECT_TRUE(unit->readMultiLEDModeControl(slot1, slot2)); - ; EXPECT_EQ(slot1, slots[0]); EXPECT_EQ(slot2, slots[1]); } @@ -579,7 +581,7 @@ TEST_F(TestMAX30102, MultiLEDMode) EXPECT_EQ(s2, slot2); } - // All invalid (slots can be set for MutiLED mode only). + // All invalid (slots can be set for MultiLED mode only). constexpr Mode m_table[] = { Mode::SpO2, Mode::HROnly, @@ -624,8 +626,8 @@ TEST_F(TestMAX30102, FIFOConfiguration) EXPECT_FALSE(unit->inPeriodic()); for (auto&& fs : fs_table) { - bool ro = rng() % 1; - uint8_t af = rng() % 0x0F; + bool ro = esp_random() % 2; + uint8_t af = esp_random() % 0x0F; auto s = m5::utility::formatString("FS:%u RO:%u AF:%u", fs, ro, af); SCOPED_TRACE(s); EXPECT_TRUE(unit->writeFIFOConfiguration(fs, ro, af)); From 4d2320255065482af93334fbd6ec7a3e5edecf59 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 18:08:38 +0900 Subject: [PATCH 28/40] Fixes Doxygen comments, typos, reserved identifier, and narrowing conversions --- src/M5UnitUnifiedHEART.h | 2 +- src/M5UnitUnifiedHEART.hpp | 2 +- src/unit/unit_MAX30100.cpp | 4 ++-- src/unit/unit_MAX30100.hpp | 20 +++++++++++++------- src/unit/unit_MAX30102.cpp | 5 +++-- src/unit/unit_MAX30102.hpp | 14 +++++++++----- src/utility/pulse_monitor.cpp | 10 +++++----- src/utility/pulse_monitor.hpp | 21 ++++++++++++++++++--- 8 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/M5UnitUnifiedHEART.h b/src/M5UnitUnifiedHEART.h index 070a00e..eb33c1c 100644 --- a/src/M5UnitUnifiedHEART.h +++ b/src/M5UnitUnifiedHEART.h @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ /*! - @file M5UnitHEART.h + @file M5UnitUnifiedHEART.h */ #ifndef M5_UNIT_UNIFIED_HEART_H #define M5_UNIT_UNIFIED_HEART_H diff --git a/src/M5UnitUnifiedHEART.hpp b/src/M5UnitUnifiedHEART.hpp index 21909e8..dab524e 100644 --- a/src/M5UnitUnifiedHEART.hpp +++ b/src/M5UnitUnifiedHEART.hpp @@ -19,7 +19,7 @@ /*! @namespace m5 - @brief Top level namespace of M5stack + @brief Top level namespace of M5Stack */ namespace m5 { diff --git a/src/unit/unit_MAX30100.cpp b/src/unit/unit_MAX30100.cpp index ccfd48f..271b54e 100644 --- a/src/unit/unit_MAX30100.cpp +++ b/src/unit/unit_MAX30100.cpp @@ -163,7 +163,7 @@ const types::attr_t UnitMAX30100::attr{attribute::AccessI2C}; bool UnitMAX30100::begin() { auto ssize = stored_size(); - assert(ssize >= max30100::MAX_FIFO_DEPTH && "stored_size must be greater than MAX_FIFO_DEPT"); + assert(ssize >= max30100::MAX_FIFO_DEPTH && "stored_size must be greater than MAX_FIFO_DEPTH"); if (ssize != _data->capacity()) { _data.reset(new m5::container::CircularBuffer(ssize)); if (!_data) { @@ -482,7 +482,7 @@ bool UnitMAX30100::read_FIFO() return false; } - for (uint_fast8_t i = 0; i < batch_count; ++i) { + for (uint32_t i = 0; i < batch_count; ++i) { Data d; // Unlike MAX30102, the length of data per session does not change even in HROnly memcpy(d.raw.data(), rbuf + 4 * i, 4); diff --git a/src/unit/unit_MAX30100.hpp b/src/unit/unit_MAX30100.hpp index d0baa32..d322d30 100644 --- a/src/unit/unit_MAX30100.hpp +++ b/src/unit/unit_MAX30100.hpp @@ -64,13 +64,13 @@ enum class LEDPulse : uint8_t { @brief LED current control */ enum class LED : uint8_t { - Current0_0, //!< 0,0 mA - Current4_4, //!< 4,4 mA + Current0_0, //!< 0.0 mA + Current4_4, //!< 4.4 mA Current7_6, //!< 7.6 mA Current11_0, //!< 11.0 mA Current14_2, //!< 14.2 mA Current17_4, //!< 17.4 mA - Current20_8, //!< 20,8 mA + Current20_8, //!< 20.8 mA Current24_0, //!< 24.0 mA Current27_1, //!< 27.1 mA Current30_6, //!< 30.6 mA @@ -89,11 +89,13 @@ constexpr uint8_t MAX_FIFO_DEPTH{16}; //!< @brief FIFO depth @brief Measurement data group */ struct Data { - std::array raw{}; // [0...1]:IR [2...3]:Red + std::array raw{}; //!< Raw data [0...1]:IR [2...3]:Red + //! @brief Gets the IR value inline uint16_t ir() const { return m5::types::big_uint16_t(raw[0], raw[1]).get(); } + //! @brief Gets the Red value inline uint16_t red() const { return m5::types::big_uint16_t(raw[2], raw[3]).get(); @@ -182,6 +184,8 @@ public: m5::unit::max30100::LED red_current{max30100::LED::Current27_1}; }; + /*! @brief Constructor + @param addr I2C address */ explicit UnitMAX30100(const uint8_t addr = DEFAULT_ADDRESS) : Component(addr), _data{new m5::container::CircularBuffer(max30100::MAX_FIFO_DEPTH)} { @@ -194,7 +198,9 @@ public: { } + //! @brief Begin the unit virtual bool begin() override; + //! @brief Update the unit virtual void update(const bool force = false) override; ///@name Settings for begin @@ -407,7 +413,7 @@ public: } //! @brief Write the sampling rate bool writeSpO2SamplingRate(const max30100::Sampling rate); - //! @brief Write LED pulse width + //! @brief Read LED pulse width inline bool readSpO2LEDPulseWidth(max30100::LEDPulse& width) { bool enabled{}; @@ -419,7 +425,7 @@ public: ///@} ///@warning In the heart-rate only mode, the red LED is inactive - // @warning and only the IR LED is used to capture optical data and determine the heart rate + /// @warning and only the IR LED is used to capture optical data and determine the heart rate ///@name LED Configuration ///@{ /*! @@ -446,7 +452,7 @@ public: @return True if successful @warning Blocking until measured about 29 ms @warning Does not work in power-save mode - @sa m5::unit::MAX30100::readShutdownControl + @sa m5::unit::UnitMAX30100::readShutdownControl */ bool measureTemperatureSingleshot(max30100::TemperatureData& td); ///@} diff --git a/src/unit/unit_MAX30102.cpp b/src/unit/unit_MAX30102.cpp index 8d6c4e4..9b2136e 100644 --- a/src/unit/unit_MAX30102.cpp +++ b/src/unit/unit_MAX30102.cpp @@ -12,6 +12,7 @@ #include // NaN #include #include +#include using namespace m5::utility::mmh3; using namespace m5::unit::types; @@ -473,7 +474,7 @@ bool UnitMAX30102::write_led_current(const uint8_t idx, const float mA) M5_LIB_LOGE("Valid range 0.0 - 51.0 (0.2 increments) %f", mA); return false; } - uint8_t raw = (uint8_t)(mA * 5); // / 0.2f + uint8_t raw = static_cast(std::lround(mA * 5)); // / 0.2f return write_led_current(idx, raw); } @@ -649,7 +650,7 @@ bool UnitMAX30102::read_FIFO() return false; } - for (uint_fast8_t i = 0; i < batch_count; ++i) { + for (uint32_t i = 0; i < batch_count; ++i) { Data d; d.mask = _mask; switch (_mode) { diff --git a/src/unit/unit_MAX30102.hpp b/src/unit/unit_MAX30102.hpp index 38ec187..fd0ac20 100644 --- a/src/unit/unit_MAX30102.hpp +++ b/src/unit/unit_MAX30102.hpp @@ -106,14 +106,14 @@ constexpr uint8_t MAX_FIFO_DEPTH{32}; //!< @brief FIFO depth @brief Measurement data group */ struct Data { - std::array raw{}; // [0...2]:Red [3...5]:IR - uint32_t mask{0x3FFFF}; // Valid bits based on ADC range - //! IR value + std::array raw{}; //!< Raw data [0...2]:Red [3...5]:IR + uint32_t mask{0x3FFFF}; //!< Valid bits based on ADC range + //! @brief Gets the IR value inline uint32_t ir() const { return mask & (((uint32_t)raw[3] << 16) | ((uint32_t)raw[4] << 8) | ((uint32_t)raw[5])); } - //! Red value + //! @brief Gets the Red value inline uint32_t red() const { return mask & (((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2])); @@ -216,6 +216,8 @@ public: max30102::FIFOSampling fifo_sampling_average{max30102::FIFOSampling::Average4}; }; + /*! @brief Constructor + @param addr I2C address */ explicit UnitMAX30102(const uint8_t addr = DEFAULT_ADDRESS) : Component(addr), _data{new m5::container::CircularBuffer(max30102::MAX_FIFO_DEPTH)} { @@ -228,7 +230,9 @@ public: { } + //! @brief Begin the unit virtual bool begin() override; + //! @brief Update the unit virtual void update(const bool force = false) override; ///@name Settings for begin @@ -524,7 +528,7 @@ public: @return True if successful @warning Blocking until measured about 29 ms @warning Does not work in power-save mode - @sa m5::unit::MAX30102::readShutdownControl + @sa m5::unit::UnitMAX30102::readShutdownControl */ bool measureTemperatureSingleshot(max30102::TemperatureData& td); ///@} diff --git a/src/utility/pulse_monitor.cpp b/src/utility/pulse_monitor.cpp index 0e9821b..fdb4497 100644 --- a/src/utility/pulse_monitor.cpp +++ b/src/utility/pulse_monitor.cpp @@ -20,7 +20,7 @@ void PulseMonitor::setSamplingRate(const uint32_t samplingRate) return; } _sampling_rate = (float)samplingRate; - _max_samples = (size_t)samplingRate * _range; + _max_samples = static_cast(samplingRate) * _range; _filterIR.setSamplingRate(5.0f, samplingRate); clear(); @@ -30,7 +30,7 @@ void PulseMonitor::clear() { _dataIR.clear(); _beat = false; - _bpm = _SpO2 = 0.0f; + _bpm = _spo2 = 0.0f; _count = 0; _avered = _aveir = _sumredrms = _sumirrms = 0; @@ -53,7 +53,7 @@ void PulseMonitor::push_back(const float ir, const float red) _aveir = _aveir * 0.95f + ir * (1.0f - 0.95f); _sumredrms += (red - _avered) * (red - _avered); _sumirrms += (ir - _aveir) * (ir - _aveir); - if (++_count == (uint32_t)_sampling_rate) { + if (++_count == static_cast(_sampling_rate)) { const float eps = 1e-6f; if (std::fabs(_avered) < eps || std::fabs(_aveir) < eps) { _sumredrms = _sumirrms = 0; @@ -61,8 +61,8 @@ void PulseMonitor::push_back(const float ir, const float red) return; } float R = (std::sqrt(_sumredrms) / _avered) / (std::sqrt(_sumirrms) / _aveir); - _SpO2 = -23.3f * (R - 0.4f) + 100; - _SpO2 = std::fmax(std::fmin(100.0f, _SpO2), 80.0f); // clamp 80-100 + _spo2 = -23.3f * (R - 0.4f) + 100; + _spo2 = std::fmax(std::fmin(100.0f, _spo2), 80.0f); // clamp 80-100 _sumredrms = _sumirrms = 0; _count = 0; } diff --git a/src/utility/pulse_monitor.hpp b/src/utility/pulse_monitor.hpp index 78f10ba..5a0d6f7 100644 --- a/src/utility/pulse_monitor.hpp +++ b/src/utility/pulse_monitor.hpp @@ -29,15 +29,21 @@ namespace heart { */ class EMA { public: + /*! @brief Constructor + @param factor Smoothing factor (0.0 - 1.0) */ explicit EMA(float factor) : _alpha(factor) { } + //! @brief Clear the stored value inline void clear() { _ema_value = std::numeric_limits::quiet_NaN(); } + /*! @brief Update with a new value and return the smoothed result + @param new_value New input value + @return Smoothed value */ inline float update(float new_value) { if (!std::isnan(_ema_value)) { @@ -58,11 +64,17 @@ private: */ class Filter { public: + /*! @brief Constructor + @param cutoff Cutoff frequency in Hz + @param sampling_rate Sampling rate in Hz */ Filter(const float cutoff, const float sampling_rate) { setSamplingRate(cutoff, sampling_rate); } + /*! @brief Set the sampling rate and reset filter state + @param cutoff Cutoff frequency in Hz + @param sampling_rate Sampling rate in Hz */ void setSamplingRate(const float cutoff, const float sampling_rate) { _cutoff = cutoff; @@ -74,6 +86,9 @@ public: _ema.clear(); } + /*! @brief Process a sample through the filter + @param value Input sample + @return Filtered and inverted output */ float process(const float value) { float out = _ema.update(_alpha * (_prevOut + value - _prevIn)); @@ -103,7 +118,7 @@ public: explicit PulseMonitor(const uint32_t samplingRate = 100, const uint32_t sec = 5) : _range{sec}, _sampling_rate{(float)samplingRate}, - _max_samples{(size_t)samplingRate * sec}, + _max_samples{static_cast(samplingRate) * sec}, _filterIR(5.0f, samplingRate) { assert(sec >= 1 && "sec must be greater or equal than 1"); @@ -126,7 +141,7 @@ public: */ inline float SpO2() const { - return _SpO2; + return _spo2; } /*! @@ -177,7 +192,7 @@ private: bool _beat{}; float _bpm{}; - float _SpO2{}; + float _spo2{}; uint32_t _count{}; float _avered{}, _aveir{}; From 67e5f53e5d6cd6ca1f452633f2ba4a250d0396ac Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 18:08:42 +0900 Subject: [PATCH 29/40] Fixes MAX30100 test self-comparison bug and removes unused includes --- test/embedded/embedded_main.cpp | 2 +- test/embedded/test_max30100/max30100_test.cpp | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/test/embedded/embedded_main.cpp b/test/embedded/embedded_main.cpp index 879b14d..84427b0 100644 --- a/test/embedded/embedded_main.cpp +++ b/test/embedded/embedded_main.cpp @@ -42,7 +42,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_max30100/max30100_test.cpp b/test/embedded/test_max30100/max30100_test.cpp index 054dd80..5d3a9d6 100644 --- a/test/embedded/test_max30100/max30100_test.cpp +++ b/test/embedded/test_max30100/max30100_test.cpp @@ -15,8 +15,6 @@ #include #include #include -#include -#include using namespace m5::unit::googletest; using namespace m5::unit; @@ -48,7 +46,7 @@ constexpr uint8_t hr_table[] = { 0x0F, 0x0F, 0x07, 0x07, 0x03, 0x03, 0x03, 0x03, }; constexpr uint8_t none_table[] = { - // LSB:200 MSB::1600 + // LSB:200 MSB:1600 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; constexpr const uint8_t* allowed_setting_table[] = {none_table, none_table, hr_table, spo2_table}; @@ -115,8 +113,8 @@ void test_spo2_config(UnitMAX30100* unit, const Mode mode) LEDPulse width2{}; EXPECT_TRUE(unit->readSpO2Configuration(resolution2, rate2, width2)); EXPECT_EQ(resolution2, resolution); - EXPECT_EQ(rate2, rate2); - EXPECT_EQ(width2, width2); + EXPECT_EQ(rate2, rate); + EXPECT_EQ(width2, width); } } } From 6d7c8aab1049237009f755a89cd5db59fc83b7fd Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 18:08:46 +0900 Subject: [PATCH 30/40] Fixes examples --- examples/UnitUnified/DualSensor/src/meter.hpp | 2 +- examples/UnitUnified/DualSensor/src/view.hpp | 2 +- examples/UnitUnified/GraphicalMeter/src/meter.hpp | 2 +- examples/UnitUnified/GraphicalMeter/src/view.hpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/UnitUnified/DualSensor/src/meter.hpp b/examples/UnitUnified/DualSensor/src/meter.hpp index 0e50041..bc56477 100644 --- a/examples/UnitUnified/DualSensor/src/meter.hpp +++ b/examples/UnitUnified/DualSensor/src/meter.hpp @@ -28,7 +28,7 @@ public: _plotter->push_back(value); } - inline void push(LovyanGFX* target, const uint32_t x, const uint32_t y) + inline void push(LovyanGFX* target) { _plotter->push(target, _left, _top); } diff --git a/examples/UnitUnified/DualSensor/src/view.hpp b/examples/UnitUnified/DualSensor/src/view.hpp index 37443f3..ba8f418 100644 --- a/examples/UnitUnified/DualSensor/src/view.hpp +++ b/examples/UnitUnified/DualSensor/src/view.hpp @@ -47,7 +47,7 @@ struct View { _sprite.printf("BPM: %3.2f\nSpO2:%3.2f", _monitor.bpm(), _monitor.SpO2()); _sprite.fillCircle(_sprite.width() - 12, 24 * 3, 7, _beat ? palette_theme : palette_white); - _meter->push(&_sprite, 0, _sprite.height() >> 1); + _meter->push(&_sprite); } void clear() diff --git a/examples/UnitUnified/GraphicalMeter/src/meter.hpp b/examples/UnitUnified/GraphicalMeter/src/meter.hpp index 0e50041..bc56477 100644 --- a/examples/UnitUnified/GraphicalMeter/src/meter.hpp +++ b/examples/UnitUnified/GraphicalMeter/src/meter.hpp @@ -28,7 +28,7 @@ public: _plotter->push_back(value); } - inline void push(LovyanGFX* target, const uint32_t x, const uint32_t y) + inline void push(LovyanGFX* target) { _plotter->push(target, _left, _top); } diff --git a/examples/UnitUnified/GraphicalMeter/src/view.hpp b/examples/UnitUnified/GraphicalMeter/src/view.hpp index 29a3509..60bc7aa 100644 --- a/examples/UnitUnified/GraphicalMeter/src/view.hpp +++ b/examples/UnitUnified/GraphicalMeter/src/view.hpp @@ -79,7 +79,7 @@ struct View { void render() { _plot_sprite.clear(palette_black); - _meter->push(&_plot_sprite, 0, 0); + _meter->push(&_plot_sprite); _text_sprite.clear(palette_black); _text_sprite.drawString(_type ? "Unit" : "Hat", 0, 0); _text_sprite.setCursor(0, _text_sprite.fontHeight()); From a6de890f8416dc29926024982c0faf86f6011a51 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 18:08:51 +0900 Subject: [PATCH 31/40] Fixes workflow paths and adds intelhex dependency --- .../workflows/arduino-esp-v2-build-check.yml | 20 +++++++------- .../workflows/arduino-esp-v3-build-check.yml | 20 +++++++------- .github/workflows/arduino-m5-build-check.yml | 20 +++++++------- .github/workflows/clang-format-check.yml | 4 +-- .github/workflows/doxygen-gh-pages.yml | 2 +- .github/workflows/platformio-build-check.yml | 27 ++++++++++--------- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index 6339ee2..eb6e9cc 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -12,28 +12,28 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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 ed20213..9941313 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -12,28 +12,28 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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 81b47f2..8c038d3 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -12,28 +12,28 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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 65f0f20..b941809 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/doxygen-gh-pages.yml b/.github/workflows/doxygen-gh-pages.yml index 9554f74..59c8172 100644 --- a/.github/workflows/doxygen-gh-pages.yml +++ b/.github/workflows/doxygen-gh-pages.yml @@ -1,4 +1,4 @@ -name: Deploy Doxygen docuemnt on GitHub Pages +name: Deploy Doxygen document on GitHub Pages on: [release, workflow_dispatch] # branches: # - main diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 9433b23..4ff7854 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -8,30 +8,30 @@ on: branches: - '*' paths: - - 'src/unit/**.cpp' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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' - - 'src/unit/**.hpp' - - 'src/unit/**.h' - - 'src/unit/**.c' + - 'src/**.cpp' + - 'src/**.hpp' + - 'src/**.h' + - 'src/**.c' - 'examples/UnitUnified/**.ino' - 'examples/UnitUnified/**.cpp' - '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: @@ -134,6 +134,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install intelhex + run: pip install intelhex + - name: Build examples uses: karniv00l/platformio-run-action@v1 with: From 43afe362cc89d40a0f57a72404d6e77238363792 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 18:08:57 +0900 Subject: [PATCH 32/40] Bump M5UnitUnified dependency to 0.4.4 and some tweaks --- .gitignore | 15 +++++++++++++++ library.json | 6 +++--- library.properties | 2 +- platformio.ini | 28 ++++++++-------------------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 259148f..49576fb 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,18 @@ *.exe *.out *.app + +# Doxygen +docs/html/ + +# PlatformIO +.pio/ + +# IDE +.vscode/ + +# macOS +.DS_Store + +# Temporary +tmp/ diff --git a/library.json b/library.json index 2175f75..a917239 100644 --- a/library.json +++ b/library.json @@ -1,17 +1,17 @@ { "name": "M5Unit-HEART", "description": "Library for M5Stack UNIT HEART Using M5UnitUnified", - "keywords": "M5Stack UNIT HEART, M5UnitUnified", + "keywords": "m5stack, m5unitunified, heart, max30100, max30102, pulse-oximeter, heart-rate, spo2, i2c", "authors": { "name": "M5Stack", - "url": "http://www.m5stack.com" + "url": "https://www.m5stack.com" }, "repository": { "type": "git", "url": "https://github.com/m5stack/M5Unit-HEART.git" }, "dependencies": { - "m5stack/M5UnitUnified": ">=0.4.1" + "m5stack/M5UnitUnified": ">=0.4.4" }, "version": "0.2.0", "frameworks": [ diff --git a/library.properties b/library.properties index c86bc25..caead0a 100644 --- a/library.properties +++ b/library.properties @@ -3,7 +3,7 @@ version=0.2.0 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack UNIT HEART using M5UnitUnified -paragraph= +paragraph=Supports MAX30100 (UnitHeart, SKU:U029) and MAX30102 (HatHeart, SKU:U118) for pulse oximetry and heart rate monitoring. category=Device Control url=https://github.com/m5stack/M5Unit-HEART architectures=esp32 diff --git a/platformio.ini b/platformio.ini index 17119ea..2d22d8f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,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 [m5base] monitor_speed = 115200 @@ -154,18 +154,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 - [nanoc6_latest] platform = espressif32 @ 6.12.0 platform_packages = @@ -374,7 +362,7 @@ test_filter= embedded/test_max30102 ;-------------------------------- ;UnitHeart ;-------------------------------- -;PlotToSerail +;PlotToSerial [env:UnitHeart_PlotToSerial_Core_Arduino_latest] extends=Core, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/PlotToSerial> @@ -487,8 +475,8 @@ build_flags = ${option_release.build_flags} -D USING_UNIT_HEART ;GraphicalMeter -; For thisg samples, please refer to PlotToSerial to create env for your own environment -; こののサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください +; For this sample, please refer to PlotToSerial to create env for your own environment +; このサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください [env:UnitHeart_GraphicalMeter_Core_Arduino_latest] extends=Core, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMeter> @@ -532,8 +520,8 @@ build_flags = ${option_release.build_flags} -D USING_HAT_HEART ;GraphicalMeter -; For thisg samples, please refer to PlotToSerial to create env for your own environment -; こののサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください +; For this sample, please refer to PlotToSerial to create env for your own environment +; このサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください [env:HatHeart_GraphicalMeter_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMeter> @@ -542,8 +530,8 @@ build_flags = ${option_release.build_flags} ;-------------------------------- ;DualSensor (Using Unit and Hat) -; For thisg samples, please refer to PlotToSerial to create env for your own environment -; こののサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください +; For this sample, please refer to PlotToSerial to create env for your own environment +; このサンプルは、PlotToSerial を参考にして、自分の環境にあった env を作成してください ;Example of using M5UnitUnified to connect both UnitHeart and HatHeart [env:DualSensor_StickCPlus2_Arduino_latest] extends=StickCPlus2, option_release, arduino_latest From 97e8c4edce6a4f84554bd5cb1b762eaf4f2f0e16 Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 23 Mar 2026 19:27:11 +0900 Subject: [PATCH 33/40] Fixes workflow --- .github/workflows/platformio-build-check.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/platformio-build-check.yml b/.github/workflows/platformio-build-check.yml index 4ff7854..8f8714c 100644 --- a/.github/workflows/platformio-build-check.yml +++ b/.github/workflows/platformio-build-check.yml @@ -134,6 +134,11 @@ 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 From e0590c9395a8bd016b79deca6b21d411c3e9378e Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 30 Mar 2026 15:32:28 +0900 Subject: [PATCH 34/40] Fixes TemperatureData sentinel collision, removes dead code, and adds regression tests --- src/unit/unit_MAX30100.cpp | 14 ------------ src/unit/unit_MAX30100.hpp | 9 ++++---- src/unit/unit_MAX30102.cpp | 18 +++------------ src/unit/unit_MAX30102.hpp | 15 ++++++++----- src/utility/pulse_monitor.cpp | 3 ++- src/utility/pulse_monitor.hpp | 3 ++- test/embedded/test_max30100/max30100_test.cpp | 22 +++++++++++++++++-- test/embedded/test_max30102/max30102_test.cpp | 16 ++++++++++++++ 8 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/unit/unit_MAX30100.cpp b/src/unit/unit_MAX30100.cpp index 271b54e..4728bce 100644 --- a/src/unit/unit_MAX30100.cpp +++ b/src/unit/unit_MAX30100.cpp @@ -197,7 +197,6 @@ void UnitMAX30100::update(const bool force) if (force || !_latest || at >= _latest + _interval) { _updated = read_FIFO(); if (_updated) { - // _latest = at; _latest = m5::utility::millis(); } } @@ -462,7 +461,6 @@ bool UnitMAX30100::read_FIFO() assert(readCount <= MAX_FIFO_DEPTH); -#if 1 if (readCount) { uint8_t reg{FIFO_DATA_REGISTER}; if (writeWithTransaction(®, 1) != m5::hal::error::error_t::OK) { @@ -492,18 +490,6 @@ bool UnitMAX30100::read_FIFO() } _retrieved = readCount; } - -#else - while (readCount--) { - Data d{}; - if (!read_register(FIFO_DATA_REGISTER, d.raw.data(), d.raw.size())) { - M5_LIB_LOGE("Failed to read"); - return false; - } - _data->push_back(d); - ++_retrieved; - } -#endif return (_retrieved != 0); } diff --git a/src/unit/unit_MAX30100.hpp b/src/unit/unit_MAX30100.hpp index d322d30..7bafc95 100644 --- a/src/unit/unit_MAX30100.hpp +++ b/src/unit/unit_MAX30100.hpp @@ -107,7 +107,7 @@ struct Data { @brief Measurement data group for temperature */ struct TemperatureData { - std::array raw{0xFF, 0xFF}; // [0]:integer [1]:fraction + std::array raw{0x80, 0x00}; // [0]:integer [1]:fraction sentinel:0x80(-128) is outside operating range //! @brief Temperature (Celsius) inline float temperature() const { @@ -116,9 +116,10 @@ struct TemperatureData { //! @brief Temperature (Celsius) inline float celsius() const { - return (raw[0] != 0xFF) ? (int8_t)raw[0] + raw[1] * 0.0625f : std::numeric_limits::quiet_NaN(); + return (raw[0] != 0x80) ? static_cast(raw[0]) + raw[1] * 0.0625f + : std::numeric_limits::quiet_NaN(); } - //! @brief temperature (Fahrenheit) + //! @brief Temperature (Fahrenheit) inline float fahrenheit() const { return celsius() * 9.0f / 5.0f + 32.f; @@ -425,7 +426,7 @@ public: ///@} ///@warning In the heart-rate only mode, the red LED is inactive - /// @warning and only the IR LED is used to capture optical data and determine the heart rate + ///@warning and only the IR LED is used to capture optical data and determine the heart rate ///@name LED Configuration ///@{ /*! diff --git a/src/unit/unit_MAX30102.cpp b/src/unit/unit_MAX30102.cpp index 9b2136e..13472f1 100644 --- a/src/unit/unit_MAX30102.cpp +++ b/src/unit/unit_MAX30102.cpp @@ -141,7 +141,8 @@ constexpr uint32_t adc_resolution_bits_table[] = { // Calculate the interval per data inline uint32_t calculate_interval_time(const FIFOSampling avg, const Sampling rate) { - float freq = sampling_rate_table[m5::stl::to_underlying(rate)] / (float)average_table[m5::stl::to_underlying(avg)]; + float freq = sampling_rate_table[m5::stl::to_underlying(rate)] / + static_cast(average_table[m5::stl::to_underlying(avg)]); // M5_LIB_LOGE(">>>>>>>>>> avg:%u %u rate:%u %u => %f %f", avg, average_table[m5::stl::to_underlying(avg)], rate, // sampling_rate_table[m5::stl::to_underlying(rate)], freq, std::ceil(1000.f / freq)); @@ -465,7 +466,7 @@ bool UnitMAX30102::read_led_current(const uint8_t idx, float& mA) bool UnitMAX30102::write_led_current(const uint8_t idx, const uint8_t raw) { - return (idx < 2) ? writeRegister8((uint8_t)(LED_CONFIGURATION_1 + idx), raw) : false; + return (idx < 2) ? writeRegister8(static_cast(LED_CONFIGURATION_1 + idx), raw) : false; } bool UnitMAX30102::write_led_current(const uint8_t idx, const float mA) @@ -623,7 +624,6 @@ bool UnitMAX30102::read_FIFO() assert(readCount <= MAX_FIFO_DEPTH); -#if 1 uint32_t dlen = (_mode == Mode::HROnly) ? 3 : (_mode == Mode::SpO2) ? 6 : (_mode == Mode::MultiLED) ? 3 * ((_slot[0] != Slot::None) + (_slot[1] != Slot::None)) @@ -676,18 +676,6 @@ bool UnitMAX30102::read_FIFO() } _retrieved = readCount; } -#else - while (readCount--) { - Data d{}; - if (!read_register(FIFO_DATA_REGISTER, d.raw.data(), d.raw.size())) { - M5_LIB_LOGE("Failed to read"); - return false; - } - _data->push_back(d); - ++_retrieved; - } - -#endif return (_retrieved != 0); } diff --git a/src/unit/unit_MAX30102.hpp b/src/unit/unit_MAX30102.hpp index fd0ac20..9eb1348 100644 --- a/src/unit/unit_MAX30102.hpp +++ b/src/unit/unit_MAX30102.hpp @@ -111,12 +111,14 @@ struct Data { //! @brief Gets the IR value inline uint32_t ir() const { - return mask & (((uint32_t)raw[3] << 16) | ((uint32_t)raw[4] << 8) | ((uint32_t)raw[5])); + return mask & ((static_cast(raw[3]) << 16) | (static_cast(raw[4]) << 8) | + static_cast(raw[5])); } //! @brief Gets the Red value inline uint32_t red() const { - return mask & (((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2])); + return mask & ((static_cast(raw[0]) << 16) | (static_cast(raw[1]) << 8) | + static_cast(raw[2])); } }; @@ -125,7 +127,7 @@ struct Data { @brief Measurement data group for temperature */ struct TemperatureData { - std::array raw{0xFF, 0xFF}; // [0]:integer [1]:fraction + std::array raw{0x80, 0x00}; // [0]:integer [1]:fraction sentinel:0x80(-128) is outside operating range //! @brief Temperature (Celsius) inline float temperature() const { @@ -134,9 +136,10 @@ struct TemperatureData { //! @brief Temperature (Celsius) inline float celsius() const { - return (raw[0] != 0xFF) ? (int8_t)raw[0] + raw[1] * 0.0625f : std::numeric_limits::quiet_NaN(); + return (raw[0] != 0x80) ? static_cast(raw[0]) + raw[1] * 0.0625f + : std::numeric_limits::quiet_NaN(); } - //! @brief temperature (Fahrenheit) + //! @brief Temperature (Fahrenheit) inline float fahrenheit() const { return celsius() * 9.0f / 5.0f + 32.f; @@ -494,7 +497,7 @@ public: template ::value, std::nullptr_t>::type = nullptr> inline bool writeLEDCurrent(const uint8_t slot, const T mA) { - return write_led_current(slot, (float)mA); + return write_led_current(slot, static_cast(mA)); } ///@} diff --git a/src/utility/pulse_monitor.cpp b/src/utility/pulse_monitor.cpp index fdb4497..79380f6 100644 --- a/src/utility/pulse_monitor.cpp +++ b/src/utility/pulse_monitor.cpp @@ -60,7 +60,8 @@ void PulseMonitor::push_back(const float ir, const float red) _count = 0; return; } - float R = (std::sqrt(_sumredrms) / _avered) / (std::sqrt(_sumirrms) / _aveir); + float R = (std::sqrt(_sumredrms) / _avered) / (std::sqrt(_sumirrms) / _aveir); + // Empirical SpO2 approximation from the red/IR RMS to DC ratio. _spo2 = -23.3f * (R - 0.4f) + 100; _spo2 = std::fmax(std::fmin(100.0f, _spo2), 80.0f); // clamp 80-100 _sumredrms = _sumirrms = 0; diff --git a/src/utility/pulse_monitor.hpp b/src/utility/pulse_monitor.hpp index 5a0d6f7..65483ed 100644 --- a/src/utility/pulse_monitor.hpp +++ b/src/utility/pulse_monitor.hpp @@ -77,11 +77,12 @@ public: @param sampling_rate Sampling rate in Hz */ void setSamplingRate(const float cutoff, const float sampling_rate) { + constexpr float pi{3.14159265358979323846f}; _cutoff = cutoff; _samplingRate = sampling_rate; _prevIn = _prevOut = 0.0f; auto dt = 1.0f / _samplingRate; - auto RC = 1.0f / (2.0f * M_PI * _cutoff); + auto RC = 1.0f / (2.0f * pi * _cutoff); _alpha = RC / (RC + dt); _ema.clear(); } diff --git a/test/embedded/test_max30100/max30100_test.cpp b/test/embedded/test_max30100/max30100_test.cpp index 5d3a9d6..567b660 100644 --- a/test/embedded/test_max30100/max30100_test.cpp +++ b/test/embedded/test_max30100/max30100_test.cpp @@ -163,12 +163,17 @@ void test_spo2_config_each(UnitMAX30100* unit, const Mode mode) template void collect_and_verify(U* unit, uint32_t count, bool expect_ir, bool expect_red) { - auto timeout = std::max(unit->interval() * (count + 1) * 2, 2000); - auto result = collect_periodic_measurements(unit, count, timeout); + auto ad = unit->template asAdapter(m5::unit::Adapter::Type::I2C); + bool is_bus = ad && ad->implType() == m5::unit::AdapterI2C::ImplType::Bus; + uint32_t limit = is_bus ? 3U : 1U; + auto timeout = std::max(unit->interval() * (count + 1) * 2, 2000); + auto result = collect_periodic_measurements(unit, count, timeout); EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); EXPECT_FALSE(result.timed_out); + EXPECT_EQ(result.update_count, count); + EXPECT_LE(result.median(), result.expected_interval + limit); EXPECT_GE(unit->available(), count); EXPECT_FALSE(unit->empty()); @@ -423,6 +428,19 @@ TEST_F(TestMAX30100, Temperature) } } +TEST_F(TestMAX30100, TemperatureDataSentinel) +{ + TemperatureData td{}; + + td.raw = {0x80, 0x00}; + EXPECT_FALSE(std::isfinite(td.celsius())); + EXPECT_FALSE(std::isfinite(td.fahrenheit())); + + td.raw = {0xFF, 0x00}; + EXPECT_FLOAT_EQ(td.celsius(), -1.0f); + EXPECT_FLOAT_EQ(td.fahrenheit(), 30.2f); +} + TEST_F(TestMAX30100, Revision) { SCOPED_TRACE(ustr); diff --git a/test/embedded/test_max30102/max30102_test.cpp b/test/embedded/test_max30102/max30102_test.cpp index 5d9cfce..45dd326 100644 --- a/test/embedded/test_max30102/max30102_test.cpp +++ b/test/embedded/test_max30102/max30102_test.cpp @@ -238,12 +238,15 @@ void test_spo2_config_each(UnitMAX30102* unit, const Mode mode) template void collect_and_verify(U* unit, uint32_t count, bool expect_ir, bool expect_red, uint32_t mask = 0) { + constexpr uint32_t limit{3U}; auto timeout = std::max(unit->interval() * (count + 1) * 2, 2000); auto result = collect_periodic_measurements(unit, count, timeout); EXPECT_TRUE(unit->stopPeriodicMeasurement()); EXPECT_FALSE(unit->inPeriodic()); EXPECT_FALSE(result.timed_out); + EXPECT_EQ(result.update_count, count); + EXPECT_LE(result.median(), result.expected_interval + limit); EXPECT_GE(unit->available(), count); EXPECT_FALSE(unit->empty()); @@ -687,6 +690,19 @@ TEST_F(TestMAX30102, Temperature) } } +TEST_F(TestMAX30102, TemperatureDataSentinel) +{ + TemperatureData td{}; + + td.raw = {0x80, 0x00}; + EXPECT_FALSE(std::isfinite(td.celsius())); + EXPECT_FALSE(std::isfinite(td.fahrenheit())); + + td.raw = {0xFF, 0x00}; + EXPECT_FLOAT_EQ(td.celsius(), -1.0f); + EXPECT_FLOAT_EQ(td.fahrenheit(), 30.2f); +} + TEST_F(TestMAX30102, Revision) { SCOPED_TRACE(ustr); From 9828d1fd083af95e03be834d26b4bdf79482a88a Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 30 Mar 2026 15:32:36 +0900 Subject: [PATCH 35/40] Fixes README descriptions to match SKU pages --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bb430ca..772ec7a 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,10 @@ Library for UnitHEART using [M5UnitUnified](https://github.com/m5stack/M5UnitUni M5UnitUnified is a library for unified handling of various M5 units products. ### SKU:U029 - -HEART is built using the MAX30100 chipset. - -MAX30100 is a complete pulse oximetry and heart-rate sensor system solution designed for the demanding requirements of wearable devices. - -The MAX30100 provides very small total solution size without sacrificing optical or electrical performance. Minimal external hardware components are needed for integration into a wearable device. +Unit Heart is a blood oxygen and heart rate sensor. Integrated MAX30100, it offers a complete pulse oximeter and heart rate sensor system solution. This is a non-invasive blood oxygen and heart rate sensor, integrating two infrared light-emitting diodes and a photodetector. The detection principle is to use infrared LEDs to illuminate and detect the proportion of red blood cells carrying oxygen versus those not carrying oxygen, thereby obtaining the oxygen content. ### SKU:U118 - -HEART RATE HAT is a blood oxygen heart rate sensor. Integrate MAX30102 to provide a complete pulse oximeter and heart rate sensor system solution. This is a non-pluggable blood oxygen heart rate sensor. The sensor uses I2C communication interface, internally integrates infrared light-emitting diodes, photo-detectors, optical components and low-noise electronic equipment. A certain amount of ambient light suppression function can make the measurement results more accurate. +Hat Heart is a blood oxygen and heart rate sensor. It integrates MAX30102, providing a complete pulse oximeter and heart rate sensor system solution. This is a non-invasive blood oxygen and heart rate sensor. Its detection principle is based on infrared LED light illumination, which detects the ratio of oxygenated to deoxygenated red blood cells to determine blood oxygen levels. The sensor uses an I2C communication interface and integrates infrared LEDs, photodetectors, optical components, and low-noise electronics, with some ambient light suppression for more accurate measurements. ## Related Link From 681bdc533aeca7a996e6fea368fd7d65982fe2ee Mon Sep 17 00:00:00 2001 From: GOB Date: Mon, 30 Mar 2026 15:32:42 +0900 Subject: [PATCH 36/40] Some tweaks --- .gitignore | 6 ++++++ platformio.ini | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/.gitignore b/.gitignore index 49576fb..cd234a6 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,9 @@ docs/html/ # Temporary tmp/ + +# Secrets +.env* +*.pem +*.key +*.cert diff --git a/platformio.ini b/platformio.ini index 2d22d8f..d09b5c4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -528,6 +528,12 @@ build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMet build_flags = ${option_release.build_flags} -D USING_HAT_HEART +; [env:HatHeart_GraphicalMeter_StickS3_Arduino_latest] +; extends=StickS3, option_release, arduino_latest +; build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/GraphicalMeter> +; build_flags = ${option_release.build_flags} +; -D USING_HAT_HEART + ;-------------------------------- ;DualSensor (Using Unit and Hat) ; For this sample, please refer to PlotToSerial to create env for your own environment @@ -537,4 +543,15 @@ build_flags = ${option_release.build_flags} extends=StickCPlus2, option_release, arduino_latest build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> +; [env:DualSensor_StickS3_Arduino_latest] +; extends=StickS3, option_release, arduino_latest +; build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> + +; [env:DualSensor_NessoN1_Arduino_latest] +; extends=NessoN1, option_release, pioarduino_latest +; build_src_filter = +<*> -<.git/> -<.svn/> +<../examples/UnitUnified/DualSensor> + + + + From 4bca56b5cf33641c4bc9093af48bcd6bdc50e379 Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 13:39:26 +0900 Subject: [PATCH 37/40] Migrate Arduino build workflow to arduino/compile-sketches@v1 --- .../workflows/arduino-esp-v2-build-check.yml | 35 +++++++++++++------ .../workflows/arduino-esp-v3-build-check.yml | 35 +++++++++++++------ .github/workflows/arduino-m5-build-check.yml | 35 +++++++++++++------ 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/.github/workflows/arduino-esp-v2-build-check.yml b/.github/workflows/arduino-esp-v2-build-check.yml index eb6e9cc..66fcf6b 100644 --- a/.github/workflows/arduino-esp-v2-build-check.yml +++ b/.github/workflows/arduino-esp-v2-build-check.yml @@ -109,16 +109,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 }}/ - #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.sketch }} + cli-compile-flags: | + - --build-property + - build.extra_flags=${{ matrix.build-properties }} diff --git a/.github/workflows/arduino-esp-v3-build-check.yml b/.github/workflows/arduino-esp-v3-build-check.yml index 9941313..fe2a9e8 100644 --- a/.github/workflows/arduino-esp-v3-build-check.yml +++ b/.github/workflows/arduino-esp-v3-build-check.yml @@ -158,16 +158,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 }}/ - #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.sketch }} + cli-compile-flags: | + - --build-property + - build.extra_flags=${{ matrix.build-properties }} diff --git a/.github/workflows/arduino-m5-build-check.yml b/.github/workflows/arduino-m5-build-check.yml index 8c038d3..4c8126e 100644 --- a/.github/workflows/arduino-m5-build-check.yml +++ b/.github/workflows/arduino-m5-build-check.yml @@ -174,17 +174,30 @@ 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 }}/ - #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.sketch }} + cli-compile-flags: | + - --build-property + - build.extra_flags=${{ matrix.build-properties }} From d4f4aeb882213670905482a118ab0d5c433edf9a Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 13:40:10 +0900 Subject: [PATCH 38/40] Some tweaks --- examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino | 2 ++ examples/UnitUnified/PlotToSerial/PlotToSerial.ino | 2 ++ 2 files changed, 4 insertions(+) diff --git a/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino b/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino index bd403a0..f2fa438 100644 --- a/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino +++ b/examples/UnitUnified/GraphicalMeter/GraphicalMeter.ino @@ -10,7 +10,9 @@ // Choose one define symbol to match the unit you are using // ************************************************************* #if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// For UnitHeart (U029) // #define USING_UNIT_HEART +// For HatHeart (U118) // #define USING_HAT_HEART #endif #include "main/GraphicalMeter.cpp" diff --git a/examples/UnitUnified/PlotToSerial/PlotToSerial.ino b/examples/UnitUnified/PlotToSerial/PlotToSerial.ino index b295344..71c8dd8 100644 --- a/examples/UnitUnified/PlotToSerial/PlotToSerial.ino +++ b/examples/UnitUnified/PlotToSerial/PlotToSerial.ino @@ -10,7 +10,9 @@ // Choose one define symbol to match the unit you are using // ************************************************************* #if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// For UnitHeart (U029) // #define USING_UNIT_HEART +// For HatHeart (U118) // #define USING_HAT_HEART #endif #include "main/PlotToSerial.cpp" From 4fa4c8203b2026ed359dcb0bdcc3ba11d38ece0b Mon Sep 17 00:00:00 2001 From: GOB Date: Thu, 2 Apr 2026 13:42:38 +0900 Subject: [PATCH 39/40] Add ArduinoIDE define guide --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 772ec7a..776548d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,23 @@ See also examples using conventional methods here. ## Examples See also [examples/UnitUnified](examples/UnitUnified) +### For ArduinoIDE settings +You must choose a define symbol for the unit you will use. +(Rewrite source or specify with compile options) + +- PlotToSerial / GraphicalMeter +```cpp +// ************************************************************* +// Choose one define symbol to match the unit you are using +// ************************************************************* +#if !defined(USING_UNIT_HEART) && !defined(USING_HAT_HEART) +// For UnitHeart (U029) +// #define USING_UNIT_HEART +// For HatHeart (U118) +// #define USING_HAT_HEART +#endif +``` + ## Doxygen document [GitHub Pages](https://m5stack.github.io/M5Unit-HEART/) From 057e6a78b9d4efee97239ac688ee6aa54035b2c7 Mon Sep 17 00:00:00 2001 From: GOB Date: Fri, 3 Apr 2026 10:37:35 +0900 Subject: [PATCH 40/40] Raise version to 0.2.1 --- library.json | 4 ++-- library.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/library.json b/library.json index a917239..afd05d4 100644 --- a/library.json +++ b/library.json @@ -13,7 +13,7 @@ "dependencies": { "m5stack/M5UnitUnified": ">=0.4.4" }, - "version": "0.2.0", + "version": "0.2.1", "frameworks": [ "arduino" ], @@ -27,4 +27,4 @@ "docs/html" ] } -} +} \ No newline at end of file diff --git a/library.properties b/library.properties index caead0a..6b1f3e8 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unit-HEART -version=0.2.0 +version=0.2.1 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack UNIT HEART using M5UnitUnified