You've already forked M5Unit-HEART
mirror of
https://github.com/m5stack/M5Unit-HEART.git
synced 2026-05-20 11:28:19 -07:00
894 lines
31 KiB
C++
894 lines
31 KiB
C++
/*
|
|
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
/*
|
|
UnitTest for UnitMAX30102
|
|
*/
|
|
#include <gtest/gtest.h>
|
|
#include <Wire.h>
|
|
#include <M5Unified.h>
|
|
#include <M5UnitUnified.hpp>
|
|
#include <googletest/test_template.hpp>
|
|
#include <googletest/test_helper.hpp>
|
|
#include <unit/unit_MAX30102.hpp>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <esp_random.h>
|
|
|
|
using namespace m5::unit::googletest;
|
|
using namespace m5::unit;
|
|
using namespace m5::unit::max30102;
|
|
using namespace m5::unit::max30102::command;
|
|
|
|
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};
|
|
}
|
|
}
|
|
|
|
} // namespace hat
|
|
|
|
class TestMAX30102 : public I2CComponentTestBase<UnitMAX30102> {
|
|
protected:
|
|
virtual bool begin() override
|
|
{
|
|
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);
|
|
wire.end();
|
|
wire.begin(pins.sda, pins.scl, unit->component_config().clock);
|
|
return Units.add(*unit, wire) && Units.begin();
|
|
}
|
|
|
|
virtual UnitMAX30102* get_instance() override
|
|
{
|
|
auto ptr = new m5::unit::UnitMAX30102();
|
|
if (ptr) {
|
|
auto ccfg = ptr->component_config();
|
|
ptr->component_config(ccfg);
|
|
}
|
|
return ptr;
|
|
}
|
|
};
|
|
|
|
namespace {
|
|
// esp_random() used instead of std::default_random_engine
|
|
|
|
constexpr uint32_t STORED_SIZE{4};
|
|
|
|
// Same as unit_MAX30102.cpp
|
|
constexpr uint8_t spo2_table[] = {
|
|
// LSB:50 MSB:3200
|
|
0x0F, 0x0F, 0x0F, 0x0F, 0x07, 0x03, 0x01, 0x00,
|
|
};
|
|
constexpr uint8_t hr_table[] = {
|
|
// LSB:50 MSB:3200
|
|
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x07, 0x01,
|
|
};
|
|
constexpr uint8_t none_table[] = {
|
|
// LSB:50 MSB:3200
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
|
|
constexpr const uint8_t* allowed_setting_table[] = {
|
|
none_table, none_table, hr_table, spo2_table, none_table, none_table, none_table, spo2_table,
|
|
};
|
|
|
|
constexpr uint32_t adc_resolution_bits_table[] = {
|
|
0x007FFF, // 15 bits
|
|
0x00FFFF, // 16 bits
|
|
0x01FFFF, // 17 bits
|
|
0x03FFFF, // 18 bits
|
|
};
|
|
|
|
bool is_allowed_settings(const Mode mode, const Sampling rate, const LEDPulse pw)
|
|
{
|
|
return allowed_setting_table[m5::stl::to_underlying(mode)][m5::stl::to_underlying(rate)] &
|
|
(1U << m5::stl::to_underlying(pw));
|
|
}
|
|
|
|
constexpr Mode mode_table[] = {
|
|
Mode::SpO2,
|
|
Mode::HROnly,
|
|
Mode::MultiLED,
|
|
};
|
|
|
|
constexpr ADC range_table[] = {
|
|
ADC::Range2048nA,
|
|
ADC::Range4096nA,
|
|
ADC::Range8192nA,
|
|
ADC::Range16384nA,
|
|
};
|
|
|
|
constexpr Sampling sr_table[] = {
|
|
Sampling::Rate50, Sampling::Rate100, Sampling::Rate200, Sampling::Rate400,
|
|
Sampling::Rate800, Sampling::Rate1000, Sampling::Rate1600, Sampling::Rate3200,
|
|
};
|
|
|
|
constexpr LEDPulse pw_table[] = {
|
|
LEDPulse::Width69,
|
|
LEDPulse::Width118,
|
|
LEDPulse::Width215,
|
|
LEDPulse::Width411,
|
|
};
|
|
|
|
constexpr FIFOSampling fs_table[] = {
|
|
FIFOSampling::Average1, FIFOSampling::Average2, FIFOSampling::Average4,
|
|
FIFOSampling::Average8, FIFOSampling::Average16, FIFOSampling::Average32,
|
|
};
|
|
|
|
constexpr std::array<Slot, 2> slots_table[] = {
|
|
//
|
|
{Slot::None, Slot::None},
|
|
//
|
|
{Slot::IR, Slot::None},
|
|
{Slot::Red, Slot::None},
|
|
//
|
|
{Slot::IR, Slot::IR},
|
|
{Slot::IR, Slot::Red},
|
|
{Slot::Red, Slot::IR},
|
|
{Slot::Red, Slot::Red},
|
|
};
|
|
|
|
constexpr std::array<Slot, 2> invalid_slots_table[] = {
|
|
{Slot::None, Slot::IR},
|
|
{Slot::None, Slot::Red},
|
|
};
|
|
|
|
void test_spo2_config(UnitMAX30102* unit, const Mode mode)
|
|
{
|
|
EXPECT_TRUE(unit->writeMode(mode));
|
|
|
|
for (auto& rg : range_table) {
|
|
for (auto&& sr : sr_table) {
|
|
for (auto&& pw : pw_table) {
|
|
auto s = m5::utility::formatString("Mode:%u RNG:%u Rate:%u Width:%u", mode, rg, sr, pw);
|
|
SCOPED_TRACE(s);
|
|
|
|
ADC range{};
|
|
Sampling rate{};
|
|
LEDPulse width{};
|
|
|
|
if (is_allowed_settings(mode, sr, pw)) {
|
|
EXPECT_TRUE(unit->writeSpO2Configuration(rg, sr, pw));
|
|
|
|
EXPECT_TRUE(unit->readSpO2Configuration(range, rate, width));
|
|
EXPECT_EQ(range, rg);
|
|
EXPECT_EQ(rate, sr);
|
|
EXPECT_EQ(width, pw);
|
|
} else {
|
|
EXPECT_TRUE(unit->readSpO2Configuration(range, rate, width));
|
|
|
|
EXPECT_FALSE(unit->writeSpO2Configuration(rg, sr, pw));
|
|
|
|
ADC range2{};
|
|
Sampling rate2{};
|
|
LEDPulse width2{};
|
|
EXPECT_TRUE(unit->readSpO2Configuration(range2, rate2, width2));
|
|
EXPECT_EQ(range2, range);
|
|
EXPECT_EQ(rate2, rate);
|
|
EXPECT_EQ(width2, width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_TRUE(unit->writeMode(mode));
|
|
EXPECT_TRUE(unit->writeSpO2Configuration(ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width69));
|
|
|
|
// ADC range
|
|
for (auto& rg : range_table) {
|
|
ADC range{};
|
|
EXPECT_TRUE(unit->writeSpO2ADCRange(rg));
|
|
EXPECT_TRUE(unit->readSpO2ADCRange(range));
|
|
EXPECT_EQ(range, rg);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
template <class U>
|
|
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<uint32_t>(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());
|
|
|
|
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);
|
|
}
|
|
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());
|
|
|
|
EXPECT_EQ(unit->ir(), 0);
|
|
EXPECT_EQ(unit->red(), 0);
|
|
}
|
|
|
|
void test_periodic_spo2(UnitMAX30102* unit)
|
|
{
|
|
// Pairwise
|
|
constexpr std::tuple<ADC, Sampling, LEDPulse, FIFOSampling> cond_table[] = {
|
|
{ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width118, FIFOSampling::Average1},
|
|
{ADC::Range8192nA, Sampling::Rate100, LEDPulse::Width215, FIFOSampling::Average16},
|
|
{ADC::Range4096nA, Sampling::Rate100, LEDPulse::Width411, FIFOSampling::Average4},
|
|
{ADC::Range4096nA, Sampling::Rate200, LEDPulse::Width215, FIFOSampling::Average8},
|
|
{ADC::Range8192nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range4096nA, Sampling::Rate800, LEDPulse::Width69, FIFOSampling::Average1},
|
|
{ADC::Range16384nA, Sampling::Rate400, LEDPulse::Width118, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate200, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range16384nA, Sampling::Rate800, LEDPulse::Width215, FIFOSampling::Average32},
|
|
{ADC::Range16384nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average1},
|
|
{ADC::Range8192nA, Sampling::Rate200, LEDPulse::Width411, FIFOSampling::Average1},
|
|
{ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width411, FIFOSampling::Average2},
|
|
{ADC::Range8192nA, Sampling::Rate800, LEDPulse::Width118, FIFOSampling::Average4},
|
|
{ADC::Range2048nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average4},
|
|
{ADC::Range4096nA, Sampling::Rate1000, LEDPulse::Width118, FIFOSampling::Average32},
|
|
{ADC::Range4096nA, Sampling::Rate50, LEDPulse::Width215, FIFOSampling::Average4},
|
|
{ADC::Range8192nA, Sampling::Rate400, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range2048nA, Sampling::Rate800, LEDPulse::Width215, FIFOSampling::Average8},
|
|
{ADC::Range16384nA, Sampling::Rate100, LEDPulse::Width411, FIFOSampling::Average1},
|
|
{ADC::Range16384nA, Sampling::Rate1000, LEDPulse::Width118, FIFOSampling::Average16},
|
|
{ADC::Range8192nA, Sampling::Rate200, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range16384nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average4},
|
|
{ADC::Range4096nA, Sampling::Rate400, LEDPulse::Width215, FIFOSampling::Average1},
|
|
{ADC::Range4096nA, Sampling::Rate800, LEDPulse::Width215, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate100, LEDPulse::Width118, FIFOSampling::Average8},
|
|
{ADC::Range16384nA, Sampling::Rate50, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range8192nA, Sampling::Rate50, LEDPulse::Width411, FIFOSampling::Average16},
|
|
{ADC::Range8192nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range2048nA, Sampling::Rate400, LEDPulse::Width411, FIFOSampling::Average8},
|
|
{ADC::Range4096nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average1},
|
|
{ADC::Range2048nA, Sampling::Rate100, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range4096nA, Sampling::Rate800, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range16384nA, Sampling::Rate200, LEDPulse::Width411, FIFOSampling::Average32},
|
|
{ADC::Range2048nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate100, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range16384nA, Sampling::Rate50, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range2048nA, Sampling::Rate400, LEDPulse::Width69, FIFOSampling::Average4},
|
|
{ADC::Range2048nA, Sampling::Rate400, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate200, LEDPulse::Width118, FIFOSampling::Average4},
|
|
};
|
|
|
|
for (auto&& cond : cond_table) {
|
|
ADC range{};
|
|
Sampling rate{};
|
|
LEDPulse width{};
|
|
FIFOSampling avg{};
|
|
std::tie(range, rate, width, avg) = cond;
|
|
|
|
auto s = m5::utility::formatString("SPO2 RNG:%u SR:%u WID:%u AVG:%u", range, rate, width, avg);
|
|
SCOPED_TRACE(s);
|
|
|
|
uint32_t mask = adc_resolution_bits_table[m5::stl::to_underlying(width)];
|
|
EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::SpO2, range, rate, width, avg, 0x1f, 0x1f));
|
|
collect_and_verify(unit, STORED_SIZE, true, true, mask);
|
|
}
|
|
}
|
|
|
|
void test_periodic_hr(UnitMAX30102* unit)
|
|
{
|
|
// Pairwise
|
|
constexpr std::tuple<ADC, Sampling, LEDPulse, FIFOSampling> cond_table[] = {
|
|
{ADC::Range8192nA, Sampling::Rate3200, LEDPulse::Width69, FIFOSampling::Average4},
|
|
{ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width118, FIFOSampling::Average1},
|
|
{ADC::Range8192nA, Sampling::Rate100, LEDPulse::Width215, FIFOSampling::Average16},
|
|
{ADC::Range4096nA, Sampling::Rate100, LEDPulse::Width411, FIFOSampling::Average4},
|
|
{ADC::Range4096nA, Sampling::Rate200, LEDPulse::Width215, FIFOSampling::Average8},
|
|
{ADC::Range8192nA, Sampling::Rate1000, LEDPulse::Width118, FIFOSampling::Average8},
|
|
{ADC::Range4096nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average1},
|
|
{ADC::Range16384nA, Sampling::Rate400, LEDPulse::Width118, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate200, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range8192nA, Sampling::Rate800, LEDPulse::Width411, FIFOSampling::Average32},
|
|
{ADC::Range16384nA, Sampling::Rate1000, LEDPulse::Width215, FIFOSampling::Average32},
|
|
{ADC::Range2048nA, Sampling::Rate1600, LEDPulse::Width215, FIFOSampling::Average1},
|
|
{ADC::Range16384nA, Sampling::Rate200, LEDPulse::Width411, FIFOSampling::Average1},
|
|
{ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width411, FIFOSampling::Average2},
|
|
{ADC::Range8192nA, Sampling::Rate400, LEDPulse::Width215, FIFOSampling::Average1},
|
|
{ADC::Range8192nA, Sampling::Rate200, LEDPulse::Width118, FIFOSampling::Average32},
|
|
{ADC::Range4096nA, Sampling::Rate1600, LEDPulse::Width118, FIFOSampling::Average4},
|
|
{ADC::Range4096nA, Sampling::Rate3200, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range4096nA, Sampling::Rate50, LEDPulse::Width215, FIFOSampling::Average4},
|
|
{ADC::Range2048nA, Sampling::Rate800, LEDPulse::Width118, FIFOSampling::Average4},
|
|
{ADC::Range4096nA, Sampling::Rate800, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range16384nA, Sampling::Rate800, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range16384nA, Sampling::Rate3200, LEDPulse::Width69, FIFOSampling::Average1},
|
|
{ADC::Range2048nA, Sampling::Rate3200, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range16384nA, Sampling::Rate100, LEDPulse::Width118, FIFOSampling::Average1},
|
|
{ADC::Range4096nA, Sampling::Rate1000, LEDPulse::Width411, FIFOSampling::Average16},
|
|
{ADC::Range8192nA, Sampling::Rate200, LEDPulse::Width215, FIFOSampling::Average2},
|
|
{ADC::Range16384nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average4},
|
|
{ADC::Range16384nA, Sampling::Rate1600, LEDPulse::Width118, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate800, LEDPulse::Width215, FIFOSampling::Average1},
|
|
{ADC::Range2048nA, Sampling::Rate400, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range4096nA, Sampling::Rate400, LEDPulse::Width411, FIFOSampling::Average8},
|
|
{ADC::Range2048nA, Sampling::Rate100, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range16384nA, Sampling::Rate50, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range8192nA, Sampling::Rate50, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range8192nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range2048nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate800, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate1000, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate100, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate1600, LEDPulse::Width69, FIFOSampling::Average32},
|
|
{ADC::Range2048nA, Sampling::Rate3200, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate400, LEDPulse::Width69, FIFOSampling::Average4},
|
|
{ADC::Range2048nA, Sampling::Rate100, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range2048nA, Sampling::Rate3200, LEDPulse::Width69, FIFOSampling::Average2},
|
|
{ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width69, FIFOSampling::Average8},
|
|
{ADC::Range2048nA, Sampling::Rate400, LEDPulse::Width69, FIFOSampling::Average16},
|
|
{ADC::Range2048nA, Sampling::Rate200, LEDPulse::Width69, FIFOSampling::Average4},
|
|
};
|
|
|
|
for (auto&& cond : cond_table) {
|
|
ADC range{};
|
|
Sampling rate{};
|
|
LEDPulse width{};
|
|
FIFOSampling avg{};
|
|
std::tie(range, rate, width, avg) = cond;
|
|
|
|
auto s = m5::utility::formatString("HR RNG:%u SR:%u WID:%u AVG:%u", range, rate, width, avg);
|
|
SCOPED_TRACE(s);
|
|
|
|
uint32_t mask = adc_resolution_bits_table[m5::stl::to_underlying(width)];
|
|
EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::HROnly, range, rate, width, avg, 0x1f, 0x1f));
|
|
collect_and_verify(unit, STORED_SIZE, true, false, mask);
|
|
}
|
|
}
|
|
|
|
void test_periodic_multi(UnitMAX30102* unit)
|
|
{
|
|
constexpr std::tuple<Slot, Slot> cond_table[] = {
|
|
{Slot::IR, Slot::Red},
|
|
{Slot::Red, Slot::Red},
|
|
{Slot::IR, Slot::None},
|
|
{Slot::Red, Slot::None},
|
|
};
|
|
|
|
for (auto&& cond : cond_table) {
|
|
Slot slot1{}, slot2{};
|
|
std::tie(slot1, slot2) = cond;
|
|
|
|
auto s = m5::utility::formatString("Multi %u/%u", slot1, slot2);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_TRUE(unit->writeMultiLEDModeControl(slot1, slot2));
|
|
EXPECT_TRUE(unit->startPeriodicMeasurement());
|
|
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_F(TestMAX30102, Mode)
|
|
{
|
|
constexpr bool bool_table[] = {true, false};
|
|
|
|
SCOPED_TRACE(ustr);
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
|
|
// Failed if in periodic
|
|
for (auto&& m : mode_table) {
|
|
EXPECT_FALSE(unit->writeMode(m));
|
|
}
|
|
for (auto&& shdn : bool_table) {
|
|
EXPECT_FALSE(unit->writeShutdownControl(shdn));
|
|
}
|
|
|
|
//
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
// Mode
|
|
for (auto&& m : mode_table) {
|
|
EXPECT_TRUE(unit->writeMode(m));
|
|
Mode m2{};
|
|
EXPECT_TRUE(unit->readMode(m2));
|
|
EXPECT_EQ(m2, m);
|
|
}
|
|
|
|
// SHDN
|
|
for (auto&& shdn : bool_table) {
|
|
EXPECT_TRUE(unit->writeShutdownControl(shdn));
|
|
bool shdn2{};
|
|
EXPECT_TRUE(unit->readShutdownControl(shdn2));
|
|
EXPECT_EQ(shdn2, shdn);
|
|
}
|
|
}
|
|
|
|
TEST_F(TestMAX30102, SpO2Configuration)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
// Failed if in periodic
|
|
EXPECT_FALSE(unit->writeSpO2Configuration(ADC::Range2048nA, Sampling::Rate50, LEDPulse::Width69));
|
|
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
test_spo2_config(unit.get(), Mode::SpO2);
|
|
test_spo2_config_each(unit.get(), Mode::SpO2);
|
|
|
|
test_spo2_config(unit.get(), Mode::HROnly);
|
|
test_spo2_config_each(unit.get(), Mode::HROnly);
|
|
|
|
test_spo2_config(unit.get(), Mode::MultiLED);
|
|
test_spo2_config_each(unit.get(), Mode::MultiLED);
|
|
}
|
|
|
|
TEST_F(TestMAX30102, LEDCurrent)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
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));
|
|
|
|
uint8_t raw{};
|
|
EXPECT_TRUE(unit->readLEDCurrent(raw, 0));
|
|
EXPECT_EQ(raw, cur);
|
|
EXPECT_TRUE(unit->readLEDCurrent(raw, 1));
|
|
EXPECT_EQ(raw, cur);
|
|
|
|
float mA = (255 - cur) * 0.2f;
|
|
EXPECT_TRUE(unit->writeLEDCurrent(0, mA));
|
|
EXPECT_TRUE(unit->writeLEDCurrent(1, mA));
|
|
|
|
float f{};
|
|
EXPECT_TRUE(unit->readLEDCurrent(f, 0));
|
|
EXPECT_FLOAT_EQ(f, mA);
|
|
EXPECT_TRUE(unit->readLEDCurrent(f, 1));
|
|
EXPECT_FLOAT_EQ(f, mA);
|
|
}
|
|
|
|
// Out-of-range (float)
|
|
EXPECT_FALSE(unit->writeLEDCurrent(0, -0.01f));
|
|
EXPECT_FALSE(unit->writeLEDCurrent(1, -0.01f));
|
|
|
|
EXPECT_FALSE(unit->writeLEDCurrent(0, 51.01f));
|
|
EXPECT_FALSE(unit->writeLEDCurrent(1, 51.01f));
|
|
}
|
|
|
|
TEST_F(TestMAX30102, MultiLEDMode)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
// MultLED mode
|
|
EXPECT_TRUE(unit->writeMode(Mode::MultiLED));
|
|
|
|
// valid
|
|
Slot slot1{}, slot2{};
|
|
for (auto&& slots : slots_table) {
|
|
auto s = m5::utility::formatString("0:%u 1:%u", slots[0], slots[1]);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_TRUE(unit->writeMultiLEDModeControl(slots[0], slots[1]));
|
|
|
|
EXPECT_TRUE(unit->readMultiLEDModeControl(slot1, slot2));
|
|
EXPECT_EQ(slot1, slots[0]);
|
|
EXPECT_EQ(slot2, slots[1]);
|
|
}
|
|
|
|
// invalid
|
|
for (auto&& slots : invalid_slots_table) {
|
|
auto s = m5::utility::formatString("0:%u 1:%u", slots[0], slots[1]);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_FALSE(unit->writeMultiLEDModeControl(slots[0], slots[1]));
|
|
|
|
Slot s1{}, s2{};
|
|
EXPECT_TRUE(unit->readMultiLEDModeControl(s1, s2));
|
|
EXPECT_EQ(s1, slot1);
|
|
EXPECT_EQ(s2, slot2);
|
|
}
|
|
|
|
// All invalid (slots can be set for MultiLED mode only).
|
|
constexpr Mode m_table[] = {
|
|
Mode::SpO2,
|
|
Mode::HROnly,
|
|
};
|
|
for (auto&& mode : m_table) {
|
|
auto s = m5::utility::formatString("mode:%u", mode);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_TRUE(unit->writeMode(mode));
|
|
|
|
for (auto&& slots : slots_table) {
|
|
auto s = m5::utility::formatString("0:%u 1:%u", slots[0], slots[1]);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_FALSE(unit->writeMultiLEDModeControl(slots[0], slots[1]));
|
|
Slot s1{}, s2{};
|
|
EXPECT_TRUE(unit->readMultiLEDModeControl(s1, s2));
|
|
EXPECT_EQ(s1, slot1);
|
|
EXPECT_EQ(s2, slot2);
|
|
}
|
|
for (auto&& slots : invalid_slots_table) {
|
|
auto s = m5::utility::formatString("0:%u 1:%u", slots[0], slots[1]);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_FALSE(unit->writeMultiLEDModeControl(slots[0], slots[1]));
|
|
Slot s1{}, s2{};
|
|
EXPECT_TRUE(unit->readMultiLEDModeControl(s1, s2));
|
|
EXPECT_EQ(s1, slot1);
|
|
EXPECT_EQ(s2, slot2);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(TestMAX30102, FIFOConfiguration)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
// Failed if in periodic
|
|
EXPECT_FALSE(unit->writeFIFOConfiguration(FIFOSampling::Average1, true, 15));
|
|
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
for (auto&& fs : fs_table) {
|
|
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));
|
|
|
|
FIFOSampling avg{};
|
|
bool rollover{};
|
|
uint8_t almostFull{};
|
|
EXPECT_TRUE(unit->readFIFOConfiguration(avg, rollover, almostFull));
|
|
EXPECT_EQ(avg, fs);
|
|
EXPECT_EQ(rollover, ro);
|
|
EXPECT_EQ(almostFull, af);
|
|
}
|
|
}
|
|
|
|
TEST_F(TestMAX30102, Temperature)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
for (auto&& m : mode_table) {
|
|
auto s = m5::utility::formatString("Mode:%u", m);
|
|
SCOPED_TRACE(s);
|
|
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement()); // to Power-save mode
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
EXPECT_TRUE(unit->writeMode(m));
|
|
|
|
TemperatureData td{};
|
|
uint32_t cnt{4};
|
|
|
|
// Does not work in power-save mode
|
|
while (cnt--) {
|
|
EXPECT_FALSE(unit->measureTemperatureSingleshot(td));
|
|
EXPECT_FALSE(std::isfinite(td.celsius()));
|
|
EXPECT_FALSE(std::isfinite(td.fahrenheit()));
|
|
}
|
|
|
|
// Work in not power-save mode
|
|
EXPECT_TRUE(unit->writeShutdownControl(false));
|
|
cnt = 4;
|
|
while (cnt--) {
|
|
EXPECT_TRUE(unit->measureTemperatureSingleshot(td));
|
|
EXPECT_TRUE(std::isfinite(td.celsius()));
|
|
EXPECT_TRUE(std::isfinite(td.fahrenheit()));
|
|
// M5_LOGI("TempS>C:%f F:%f", td.celsius(), td.fahrenheit());
|
|
}
|
|
|
|
// Measurement is possible during periodic measurements
|
|
EXPECT_TRUE(unit->startPeriodicMeasurement());
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
cnt = 4;
|
|
while (cnt--) {
|
|
EXPECT_TRUE(unit->measureTemperatureSingleshot(td));
|
|
EXPECT_TRUE(std::isfinite(td.celsius()));
|
|
EXPECT_TRUE(std::isfinite(td.fahrenheit()));
|
|
// M5_LOGI("TempS>C:%f F:%f", td.celsius(), td.fahrenheit());
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
uint8_t rev{};
|
|
EXPECT_TRUE(unit->readRevisionID(rev));
|
|
EXPECT_NE(rev, 0);
|
|
}
|
|
|
|
TEST_F(TestMAX30102, Reset)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
//
|
|
EXPECT_TRUE(unit->writeMode(Mode::MultiLED));
|
|
EXPECT_TRUE(unit->writeSpO2Configuration(ADC::Range16384nA, Sampling::Rate400, LEDPulse::Width411));
|
|
EXPECT_TRUE(unit->writeLEDCurrent(0, 255));
|
|
EXPECT_TRUE(unit->writeLEDCurrent(1, 255));
|
|
EXPECT_TRUE(unit->writeMultiLEDModeControl(Slot::IR, Slot::Red));
|
|
EXPECT_TRUE(unit->writeFIFOConfiguration(FIFOSampling::Average16, true, 8));
|
|
|
|
EXPECT_TRUE(unit->writeFIFOReadPointer(1));
|
|
EXPECT_TRUE(unit->writeFIFOWritePointer(1));
|
|
EXPECT_TRUE(unit->writeFIFOOverflowCounter(1));
|
|
|
|
EXPECT_TRUE(unit->startPeriodicMeasurement());
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
|
|
//
|
|
EXPECT_TRUE(unit->reset());
|
|
|
|
//
|
|
Mode mode{};
|
|
EXPECT_TRUE(unit->readMode(mode));
|
|
EXPECT_EQ(mode, Mode::None);
|
|
|
|
ADC range{};
|
|
Sampling rate{};
|
|
LEDPulse width{};
|
|
EXPECT_TRUE(unit->readSpO2Configuration(range, rate, width));
|
|
EXPECT_EQ(range, ADC::Range2048nA);
|
|
EXPECT_EQ(rate, Sampling::Rate50);
|
|
EXPECT_EQ(width, LEDPulse::Width69);
|
|
|
|
uint8_t led1{}, led2{};
|
|
EXPECT_TRUE(unit->readLEDCurrent(led1, 0));
|
|
EXPECT_TRUE(unit->readLEDCurrent(led2, 1));
|
|
EXPECT_EQ(led1, 0);
|
|
EXPECT_EQ(led2, 0);
|
|
|
|
Slot slot1{}, slot2{};
|
|
EXPECT_TRUE(unit->readMultiLEDModeControl(slot1, slot2));
|
|
EXPECT_EQ(slot1, Slot::None);
|
|
EXPECT_EQ(slot2, Slot::None);
|
|
|
|
FIFOSampling avg{};
|
|
bool rollover{};
|
|
uint8_t almostFull{};
|
|
EXPECT_TRUE(unit->readFIFOConfiguration(avg, rollover, almostFull));
|
|
EXPECT_EQ(avg, FIFOSampling::Average1);
|
|
EXPECT_FALSE(rollover);
|
|
EXPECT_EQ(almostFull, 15); // In the datasheet the POR state is 0, but...
|
|
|
|
uint8_t rptr{0xFF}, wptr{0xFF}, cnt{0xFF};
|
|
EXPECT_TRUE(unit->readFIFOReadPointer(rptr));
|
|
EXPECT_TRUE(unit->readFIFOWritePointer(wptr));
|
|
EXPECT_TRUE(unit->readFIFOOverflowCounter(cnt));
|
|
EXPECT_EQ(rptr, 0U);
|
|
EXPECT_EQ(wptr, 0U);
|
|
EXPECT_EQ(cnt, 0U);
|
|
}
|
|
|
|
TEST_F(TestMAX30102, Periodic)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
// 100 sps
|
|
EXPECT_TRUE(unit->startPeriodicMeasurement(Mode::SpO2, ADC::Range4096nA, Sampling::Rate100, LEDPulse::Width411,
|
|
FIFOSampling::Average1, 0x1F, 0x1F));
|
|
|
|
// Wait first updated
|
|
auto start_at = m5::utility::millis();
|
|
do {
|
|
unit->update();
|
|
std::this_thread::yield();
|
|
} while (!unit->updated() && m5::utility::millis() - start_at <= 1000);
|
|
EXPECT_TRUE(unit->updated());
|
|
|
|
EXPECT_FALSE(unit->full());
|
|
EXPECT_FALSE(unit->empty());
|
|
EXPECT_GT(unit->available(), 0U);
|
|
|
|
while (unit->available()) {
|
|
EXPECT_EQ(unit->ir(), unit->oldest().ir());
|
|
EXPECT_EQ(unit->red(), unit->oldest().red());
|
|
unit->discard();
|
|
}
|
|
|
|
// Sampling about 10 times (not overflow)
|
|
m5::utility::delay(100);
|
|
|
|
unit->update();
|
|
EXPECT_TRUE(unit->updated());
|
|
|
|
EXPECT_GE(unit->available(), 10U);
|
|
auto retrieved = unit->retrieved();
|
|
EXPECT_GT(retrieved, 0U);
|
|
EXPECT_FALSE(unit->full());
|
|
EXPECT_FALSE(unit->empty());
|
|
|
|
EXPECT_NE(unit->ir(), 0U);
|
|
EXPECT_NE(unit->red(), 0U);
|
|
EXPECT_EQ(unit->ir(), unit->oldest().ir());
|
|
EXPECT_EQ(unit->red(), unit->oldest().red());
|
|
unit->flush();
|
|
|
|
EXPECT_EQ(unit->available(), 0U);
|
|
EXPECT_EQ(unit->retrieved(), retrieved); // Not clear on flush
|
|
EXPECT_FALSE(unit->full());
|
|
EXPECT_TRUE(unit->empty());
|
|
|
|
// Sampling about 40 times (overflow!)
|
|
m5::utility::delay(400);
|
|
|
|
unit->update();
|
|
EXPECT_TRUE(unit->updated());
|
|
|
|
EXPECT_EQ(unit->available(), MAX_FIFO_DEPTH);
|
|
EXPECT_EQ(unit->retrieved(), MAX_FIFO_DEPTH);
|
|
EXPECT_TRUE(unit->full());
|
|
EXPECT_FALSE(unit->empty());
|
|
EXPECT_GT(unit->overflow(), 0U);
|
|
|
|
while (unit->available()) {
|
|
// M5_LOGI("IR:%u RED:%u", unit->ir(), unit->red());
|
|
EXPECT_NE(unit->ir(), 0U);
|
|
EXPECT_NE(unit->red(), 0U);
|
|
EXPECT_EQ(unit->ir(), unit->oldest().ir());
|
|
EXPECT_EQ(unit->red(), unit->oldest().red());
|
|
unit->discard();
|
|
}
|
|
}
|
|
|
|
TEST_F(TestMAX30102, Periodic_SPO2)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
test_periodic_spo2(unit.get());
|
|
}
|
|
|
|
TEST_F(TestMAX30102, Periodic_HR)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
test_periodic_hr(unit.get());
|
|
}
|
|
|
|
TEST_F(TestMAX30102, Periodic_MultiLED)
|
|
{
|
|
SCOPED_TRACE(ustr);
|
|
|
|
EXPECT_TRUE(unit->inPeriodic());
|
|
EXPECT_TRUE(unit->stopPeriodicMeasurement());
|
|
EXPECT_FALSE(unit->inPeriodic());
|
|
|
|
EXPECT_TRUE(unit->writeMode(Mode::MultiLED));
|
|
EXPECT_TRUE(unit->writeSpO2Configuration(ADC::Range4096nA, Sampling::Rate400, LEDPulse::Width411));
|
|
EXPECT_TRUE(unit->writeFIFOConfiguration(FIFOSampling::Average4, true, 15));
|
|
EXPECT_TRUE(unit->writeLEDCurrent(0, 0x40));
|
|
EXPECT_TRUE(unit->writeLEDCurrent(1, 0x1F));
|
|
|
|
test_periodic_multi(unit.get());
|
|
}
|