first commit

This commit is contained in:
stuce-bot 2025-06-30 20:47:33 +02:00
commit 5893b00dd2
1669 changed files with 1982740 additions and 0 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,594 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_BMP280.cpp
@brief BMP280 Unit for M5UnitUnified
*/
#include "unit_BMP280.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::bmp280;
using namespace m5::unit::bmp280::command;
namespace {
constexpr uint8_t CHIP_IDENTIFIER{0x58};
constexpr uint8_t RESET_VALUE{0xB6};
constexpr uint32_t NOT_MEASURED{0x800000};
constexpr PowerMode mode_table[] = {PowerMode::Sleep, PowerMode::Forced, PowerMode::Forced, // duplicated
PowerMode::Normal
};
constexpr Oversampling osrs_table[] = {
Oversampling::Skipped, Oversampling::X1, Oversampling::X2, Oversampling::X4, Oversampling::X8, Oversampling::X16,
Oversampling::X16, // duplicated
Oversampling::X16, // duplicated
};
constexpr Oversampling osrss_table[][2] = {
// Pressure, Temperature
{Oversampling::X1, Oversampling::X1}, {Oversampling::X2, Oversampling::X1}, {Oversampling::X4, Oversampling::X1},
{Oversampling::X8, Oversampling::X1}, {Oversampling::X16, Oversampling::X2},
};
constexpr Standby standby_table[] = {
Standby::Time0_5ms, Standby::Time62_5ms, Standby::Time125ms, Standby::Time250ms,
Standby::Time500ms, Standby::Time1sec, Standby::Time2sec, Standby::Time4sec,
};
constexpr uint32_t interval_table[] = {0, 62, 125, 250, 500, 1000, 2000, 4000};
constexpr Filter filter_table[] = {
Filter::Off, Filter::Coeff2, Filter::Coeff4, Filter::Coeff8, Filter::Coeff16,
};
struct UseCaseSetting {
OversamplingSetting osrss;
Filter filter;
Standby st;
};
constexpr UseCaseSetting uc_table[] = {
{OversamplingSetting::UltraHighResolution, Filter::Coeff4, Standby::Time62_5ms},
{OversamplingSetting::StandardResolution, Filter::Coeff16, Standby::Time0_5ms},
{OversamplingSetting::UltraLowPower, Filter::Off, Standby::Time4sec},
{OversamplingSetting::StandardResolution, Filter::Coeff4, Standby::Time125ms},
{OversamplingSetting::LowPower, Filter::Off, Standby::Time0_5ms},
{OversamplingSetting::UltraHighResolution, Filter::Coeff16, Standby::Time0_5ms},
};
struct CtrlMeas {
//
inline Oversampling osrs_p() const
{
return osrs_table[(value >> 2) & 0x07];
}
inline Oversampling osrs_t() const
{
return osrs_table[(value >> 5) & 0x07];
}
inline PowerMode mode() const
{
return mode_table[value & 0x03];
}
//
inline void osrs_p(const Oversampling os)
{
value = (value & ~(0x07 << 2)) | ((m5::stl::to_underlying(os) & 0x07) << 2);
}
inline void osrs_t(const Oversampling os)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(os) & 0x07) << 5);
}
inline void mode(const PowerMode m)
{
value = (value & ~0x03) | (m5::stl::to_underlying(m) & 0x03);
}
uint8_t value{};
};
struct Config {
//
inline Standby standby() const
{
return standby_table[(value >> 5) & 0x07];
}
inline Filter filter() const
{
return filter_table[(value >> 2) & 0x07];
}
//
inline void standby(const Standby s)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(s) & 0x07) << 5);
}
inline void filter(const Filter f)
{
value = (value & ~(0x07 << 2)) | ((m5::stl::to_underlying(f) & 0x07) << 2);
}
uint8_t value{};
};
struct Calculator {
inline float temperature(const int32_t adc_P, const int32_t adc_T, const Trimming* t)
{
return t ? compensate_temperature_f(adc_T, *t) : std::numeric_limits<float>::quiet_NaN();
}
inline float pressure(const int32_t adc_P, const int32_t adc_T, const Trimming* t)
{
if (!t) {
return std::numeric_limits<float>::quiet_NaN();
}
if (t_fine == 0) {
(void)compensate_temperature_f(adc_T, *t); // For t_fine
}
return compensate_pressure_f(adc_P, *t);
}
private:
float compensate_temperature(const int32_t adc_T, const Trimming& trim)
{
int32_t var1{}, var2{};
var1 = ((((adc_T >> 3) - ((int32_t)trim.dig_T1 << 1))) * ((int32_t)trim.dig_T2)) >> 11;
var2 = (((((adc_T >> 4) - ((int32_t)trim.dig_T1)) * ((adc_T >> 4) - ((int32_t)trim.dig_T1))) >> 12) *
((int32_t)trim.dig_T3)) >>
14;
t_fine = var1 + var2; // [*1]
float T = (t_fine * 5 + 128) >> 8;
return T * 0.01f;
}
float compensate_pressure(const int32_t adc_P, const Trimming& trim)
{
int64_t var1{}, var2{}, p{};
var1 = ((int64_t)t_fine) - 128000; // (*1) using it!
var2 = var1 * var1 * (int64_t)trim.dig_P6;
var2 = var2 + ((var1 * (int64_t)trim.dig_P5) << 17);
var2 = var2 + (((int64_t)trim.dig_P4) << 35);
var1 = ((var1 * var1 * (int64_t)trim.dig_P3) >> 8) + ((var1 * (int64_t)trim.dig_P2) << 12);
var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)trim.dig_P1) >> 33;
if (var1) {
p = 1048576 - adc_P;
p = (((p << 31) - var2) * 3125) / var1;
var1 = (((int64_t)trim.dig_P9) * (p >> 13) * (p >> 13)) >> 25;
var2 = (((int64_t)trim.dig_P8) * p) >> 19;
p = ((p + var1 + var2) >> 8) + (((int64_t)trim.dig_P7) << 4);
return p / 256.f;
}
return 0.0f;
}
float compensate_temperature_f(const int32_t adc_T, const Trimming& trim)
{
float var1{}, var2{}, T{};
var1 = (((float)adc_T) / 16384.0f - ((float)trim.dig_T1) / 1024.0f) * ((float)trim.dig_T2);
var2 = ((((float)adc_T) / 131072.0f - ((float)trim.dig_T1) / 8192.0f) *
(((float)adc_T) / 131072.0f - ((float)trim.dig_T1) / 8192.0f)) *
((float)trim.dig_T3);
t_fine = (int32_t)(var1 + var2); // [*2]
T = (var1 + var2) / 5120.0f;
return T;
}
float compensate_pressure_f(const int32_t adc_P, const Trimming& trim)
{
float var1{}, var2{}, P{};
var1 = ((float)t_fine / 2.0f) - 64000.0f; // (*2)
var2 = var1 * var1 * ((float)trim.dig_P6) / 32768.0f;
var2 = var2 + var1 * ((float)trim.dig_P5) * 2.0f;
var2 = (var2 / 4.0f) + (((float)trim.dig_P4) * 65536.0f);
var1 = (((float)trim.dig_P3) * var1 * var1 / 524288.0f + ((float)trim.dig_P2) * var1) / 524288.0f;
var1 = (1.0f + var1 / 32768.0f) * ((float)trim.dig_P1);
if (var1 == 0.0f) {
return 0;
}
P = 1048576.0f - (float)adc_P;
P = (P - (var2 / 4096.0f)) * 6250.0f / var1;
var1 = ((float)trim.dig_P9) * P * P / 2147483648.0f;
var2 = P * ((float)trim.dig_P8) / 32768.0f;
P = P + (var1 + var2 + ((float)trim.dig_P7)) / 16.0f;
return P;
}
////
int32_t t_fine{};
};
} // namespace
namespace m5 {
namespace unit {
namespace bmp280 {
float Data::celsius() const
{
int32_t adc_P = (int32_t)(((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2]));
int32_t adc_T = (int32_t)(((uint32_t)raw[3] << 16) | ((uint32_t)raw[4] << 8) | ((uint32_t)raw[5]));
Calculator c{};
// adc_T is NOT_MEASURED if orsr Skipped (Not measured)
return (adc_T != NOT_MEASURED) ? c.temperature(adc_P >> 4, adc_T >> 4, trimming)
: std::numeric_limits<float>::quiet_NaN();
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::pressure() const
{
int32_t adc_P = (int32_t)(((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2]));
int32_t adc_T = (int32_t)(((uint32_t)raw[3] << 16) | ((uint32_t)raw[4] << 8) | ((uint32_t)raw[5]));
Calculator c{};
// adc_T/P is NOT_MEASURED if orsr Skipped (Not measured)
return (adc_T != NOT_MEASURED && adc_P != NOT_MEASURED) ? c.pressure(adc_P >> 4, adc_T >> 4, trimming)
: std::numeric_limits<float>::quiet_NaN();
}
} // namespace bmp280
const char UnitBMP280::name[] = "UnitBMP280";
const types::uid_t UnitBMP280::uid{"UnitBMP280"_mmh3};
const types::attr_t UnitBMP280::attr{attribute::AccessI2C};
bool UnitBMP280::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
uint8_t id{};
if (!softReset() || !readRegister8(CHIP_ID, id, 0) || id != CHIP_IDENTIFIER) {
M5_LIB_LOGE("Can not detect BMP280 %02X", id);
return false;
}
if (!read_trimming(_trimming)) {
M5_LIB_LOGE("Failed to read trimming");
return false;
}
M5_LIB_LOGV(
"Trimming\n"
"T:%u/%d/%d\n"
"P:%u/%d/%d/%d/%d/%d/%d/%d/%d",
// T
_trimming.dig_T1, _trimming.dig_T2, _trimming.dig_T3,
// P
_trimming.dig_P1, _trimming.dig_P2, _trimming.dig_P3, _trimming.dig_P4, _trimming.dig_P5, _trimming.dig_P6,
_trimming.dig_P7, _trimming.dig_P8, _trimming.dig_P9);
return _cfg.start_periodic
? startPeriodicMeasurement(_cfg.osrs_pressure, _cfg.osrs_temperature, _cfg.filter, _cfg.standby)
: true;
}
void UnitBMP280::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
Data d{};
// _updated = is_data_ready() && read_measurement(d);
_updated = read_measurement(d);
if (_updated) {
// auto dur = at - _latest;
// M5_LIB_LOGW(">DUR:%ld\n", dur);
_latest = at;
_data->push_back(d);
}
}
}
}
bool UnitBMP280::start_periodic_measurement(const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter,
const bmp280::Standby st)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
Config c{};
c.standby(st);
c.filter(filter);
CtrlMeas cm{};
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONFIG, c.value) && writeRegister8(CONTROL_MEASUREMENT, cm.value) &&
start_periodic_measurement();
}
bool UnitBMP280::start_periodic_measurement()
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
Config c{};
_periodic = readRegister8(CONFIG, c.value, 0) && writePowerMode(PowerMode::Normal);
if (_periodic) {
_latest = 0;
_interval = interval_table[m5::stl::to_underlying(c.standby())];
}
return _periodic;
}
bool UnitBMP280::stop_periodic_measurement()
{
if (inPeriodic() && writePowerMode(PowerMode::Sleep)) {
_periodic = false;
return true;
}
return false;
}
bool UnitBMP280::measureSingleshot(bmp280::Data& d, const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (osrsTemperature == Oversampling::Skipped) {
return false;
}
Config c{};
c.filter(filter);
CtrlMeas cm{};
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONFIG, c.value) && writeRegister8(CONTROL_MEASUREMENT, cm.value) && measure_singleshot(d);
}
bool UnitBMP280::measure_singleshot(bmp280::Data& d)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writePowerMode(PowerMode::Forced)) {
auto start_at = m5::utility::millis();
auto timeout_at = start_at + 2 * 1000; // 2sec
bool done{};
do {
PowerMode pm{};
done = readPowerMode(pm) && (pm == PowerMode::Sleep) && is_data_ready();
if (done) {
break;
}
m5::utility::delay(1);
} while (!done && m5::utility::millis() <= timeout_at);
return done && read_measurement(d);
}
return false;
}
bool UnitBMP280::readOversampling(Oversampling& osrsPressure, Oversampling& osrsTemperature)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
osrsPressure = cm.osrs_p();
osrsTemperature = cm.osrs_t();
return true;
}
return false;
}
bool UnitBMP280::writeOversampling(const Oversampling osrsPressure, const Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::writeOversamplingPressure(const Oversampling osrsPressure)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::writeOversamplingTemperature(const Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::writeOversampling(const bmp280::OversamplingSetting osrss)
{
auto idx = m5::stl::to_underlying(osrss);
return writeOversampling(osrss_table[idx][0], osrss_table[idx][1]);
}
bool UnitBMP280::readPowerMode(PowerMode& m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
m = cm.mode();
return true;
}
return false;
}
bool UnitBMP280::writePowerMode(const PowerMode m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.mode(m);
// Datasheet says
// If the device is currently performing ameasurement,
// execution of mode switching commands is delayed until the end of the currentlyrunning measurement period
bool can{};
auto timeout_at = m5::utility::millis() + 1000;
do {
can = is_data_ready();
if (can) {
break;
}
m5::utility::delay(1);
} while (!can && m5::utility::millis() <= timeout_at);
return can && writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::readFilter(Filter& f)
{
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
f = c.filter();
return true;
}
return false;
}
bool UnitBMP280::writeFilter(const Filter& f)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
PowerMode pm{};
if (!readPowerMode(pm) || pm != PowerMode::Sleep) {
// Datasheet says
// Writes to the config register in normal mode may be ignored. In sleep mode writes are not ignored
M5_LIB_LOGE("Invalid power mode %02X", pm);
return false;
}
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
c.filter(f);
return writeRegister8(CONFIG, c.value);
}
return false;
}
bool UnitBMP280::readStandbyTime(Standby& s)
{
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
s = c.standby();
return true;
}
return false;
}
bool UnitBMP280::writeStandbyTime(const Standby s)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
c.standby(s);
return writeRegister8(CONFIG, c.value);
}
return false;
}
bool UnitBMP280::writeUseCaseSetting(const bmp280::UseCase uc)
{
const auto& tbl = uc_table[m5::stl::to_underlying(uc)];
return writeOversampling(tbl.osrss) && writeFilter(tbl.filter) && writeStandbyTime(tbl.st);
}
bool UnitBMP280::softReset()
{
if (writeRegister8(SOFT_RESET, RESET_VALUE)) {
auto timeout_at = m5::utility::millis() + 100; // 100ms
uint8_t s{0xFF};
do {
if (readRegister8(GET_STATUS, s, 0) && (s & 0x01 /* im update */) == 0x00) {
_periodic = false;
return true;
}
} while ((s & 0x01) && m5::utility::millis() < timeout_at);
return false;
}
return false;
}
//
bool UnitBMP280::read_trimming(Trimming& t)
{
return readRegister(TRIMMING_DIG, t.value, m5::stl::size(t.value), 0);
}
bool UnitBMP280::is_data_ready()
{
uint8_t s{0xFF};
return readRegister8(GET_STATUS, s, 0) && ((s & 0x09 /* Measuring, im update */) == 0x00);
}
bool UnitBMP280::read_measurement(bmp280::Data& d)
{
d.trimming = nullptr;
// Datasheet says
// Shadowing will only work if all data registers are read in a single burst read.
// Therefore, the user must use burst reads if he does not synchronize data readout with themeasurement cycle
if (readRegister(GET_MEASUREMENT, d.raw.data(), d.raw.size(), 0)) {
d.trimming = &_trimming;
return true;
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,434 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_BMP280.hpp
@brief BMP280 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_BNP280_HPP
#define M5_UNIT_ENV_UNIT_BNP280_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
/*!
@namespace bmp280
@brief For BMP280
*/
namespace bmp280 {
/*!
@enum PowerMode
@brief Operation mode
*/
enum class PowerMode : uint8_t {
Sleep, //!< No measurements are performed
Forced, //!< Single measurements are performed
Normal = 0x03, //!< Periodic measurements are performed
};
/*!
@enum Oversampling
@brief Oversampling factor
*/
enum class Oversampling : uint8_t {
Skipped, //!< Skipped (No measurements are performed)
X1, //!< x1
X2, //!< x2
X4, //!< x4
X8, //!< x8
X16, //!< x16
};
/*!
@enum OversamplingSetting
@brief Oversampling Settings
*/
enum class OversamplingSetting : uint8_t {
UltraLowPower, //!< 16 bit / 2.62 Pa, 16 bit / 0.0050 C
LowPower, //!< 17 bit / 1.31 Pa, 16 bit / 0.0050 C
StandardResolution, //!< 18 bit / 0.66 Pa, 16 bit / 0.0050 C
HighResolution, //!< 19 bit / 0.33 Pa, 16 bit / 0.0050 C
UltraHighResolution, //!< 20 bit / 0.16 Pa, 17 bit / 0.0025 C
};
/*!
@enum Filter
@brief Filter setting
*/
enum class Filter : uint8_t {
Off, //!< Off filter
Coeff2, //!< co-efficient 2
Coeff4, //!< co-efficient 4
Coeff8, //!< co-efficient 8
Coeff16, //!< co-efficient 16
};
/*!
@enum Standby
@brief Measurement standby time for power mode Normal
*/
enum class Standby : uint8_t {
Time0_5ms, //!< 0.5 ms
Time62_5ms, //!< 62.5 ms
Time125ms, //!< 125 ms
Time250ms, //!< 250 ms
Time500ms, //!< 500 ms
Time1sec, //!< 1 second
Time2sec, //!< 2 seconds
Time4sec, //!< 4 seconds
};
/*!
@enum UseCase
@brief Preset settings
*/
enum class UseCase : uint8_t {
LowPower, //!< Handheld device low-power
Dynamic, //!< Handheld device dynamic
Weather, //!< Weather monitoring
Elevator, //!< Elevator / floor change detection
Drop, //!< Drop detection
Indoor, //!< Indoor navigation
};
/*!
@union Trimmming
@brief Trimming parameter
*/
union Trimming {
uint8_t value[12 * 2]{};
struct {
//
uint16_t dig_T1;
int16_t dig_T2;
int16_t dig_T3;
//
uint16_t dig_P1;
int16_t dig_P2;
int16_t dig_P3;
int16_t dig_P4;
int16_t dig_P5;
int16_t dig_P6;
int16_t dig_P7;
int16_t dig_P8;
int16_t dig_P9;
// uint16_t reserved;
} __attribute__((packed));
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data [0,1,2]:pressure [3,4,5]:temperature
const Trimming* trimming{}; //!< For calculate
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float pressure() const; //!< pressure (Pa)
};
} // namespace bmp280
/*!
@class UnitBMP280
@brief Pressure and temperature sensor unit
*/
class UnitBMP280 : public Component, public PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitBMP280, 0x76);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Pressure oversampling if start on begin
bmp280::Oversampling osrs_pressure{bmp280::Oversampling::X16};
//! Temperature oversampling if start on begin
bmp280::Oversampling osrs_temperature{bmp280::Oversampling::X2};
//! Filter if start on begin
bmp280::Filter filter{bmp280::Filter::Coeff16};
//! Standby time if start on begin
bmp280::Standby standby{bmp280::Standby::Time1sec};
};
explicit UnitBMP280(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<bmp280::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitBMP280()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured pressure (Pa)
inline float pressure() const
{
return !empty() ? oldest().pressure() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@param st Standby time
@return True if successful
@warning Measuring pressure requires measuring temperature
*/
inline bool startPeriodicMeasurement(const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter,
const bmp280::Standby st)
{
return PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data>::startPeriodicMeasurement(
osrsPressure, osrsTemperature, filter, st);
}
//! @brief Start periodic measurement using current settings
inline bool startPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data>::startPeriodicMeasurement();
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measuring pressure requires measuring temperature
@warning Each setting is overwritten
*/
bool measureSingleshot(bmp280::Data& d, const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter);
//! @brief Measurement single shot using current settings
inline bool measureSingleshot(bmp280::Data& d)
{
return measure_singleshot(d);
}
///@}
///@name Settings
///@{
/*!
@brief Read the oversampling conditions
@param[out] osrsPressure Oversampling for pressure
@param[out] osrsTemperature Oversampling for temperature
@return True if successful
*/
bool readOversampling(bmp280::Oversampling& osrsPressure, bmp280::Oversampling& osrsTemperature);
/*!
@brief Write the oversampling conditions
@param osrsPressure Oversampling for pressure
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const bmp280::Oversampling osrsPressure, const bmp280::Oversampling osrsTemperature);
/*!
@brief Write the oversampling conditions for pressure
@param osrsPressure Oversampling for pressure
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingPressure(const bmp280::Oversampling osrsPressure);
/*!
@brief Write the oversampling conditions for temperature
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingTemperature(const bmp280::Oversampling osrsTemperature);
/*!
@brief Write the oversampling by OversamplingSetting
@param osrss OversamplingSetting
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const bmp280::OversamplingSetting osrss);
/*!
@brief Read the IIR filter co-efficient
@param[out] f filter
@return True if successful
*/
bool readFilter(bmp280::Filter& f);
/*!
@brief Write the IIR filter co-efficient
@param f filter
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeFilter(const bmp280::Filter& f);
/*!
@brief Read the standby time
@param[out] s standby time
@return True if successful
*/
bool readStandbyTime(bmp280::Standby& s);
/*!
@brief Write the standby time
@param s standby time
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeStandbyTime(const bmp280::Standby s);
/*!
@brief Read the power mode
@param[out] m Power mode
@return True if successful
*/
bool readPowerMode(bmp280::PowerMode& m);
/*!
@brief Write the power mode
@param m Power mode
@return True if successful
@warning Note that the measurement mode is changed
@warning It is recommended to use start/stopPeriodicMeasurement or similar to change the measurement mode
*/
bool writePowerMode(const bmp280::PowerMode m);
/*!
@brief Write the settings based on use cases
@param uc UseCase
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeUseCaseSetting(const bmp280::UseCase uc);
///@}
/*!
@brief Soft reset
@return True if successful
*/
bool softReset();
protected:
bool start_periodic_measurement(const bmp280::Oversampling osrsPressure, const bmp280::Oversampling osrsTemperature,
const bmp280::Filter filter, const bmp280::Standby st);
bool start_periodic_measurement();
bool stop_periodic_measurement();
bool read_measurement(bmp280::Data& d);
bool measure_singleshot(bmp280::Data& d);
bool read_trimming(bmp280::Trimming& t);
bool is_data_ready();
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitBMP280, bmp280::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<bmp280::Data>> _data{};
config_t _cfg{};
bmp280::Trimming _trimming{};
};
///@cond
namespace bmp280 {
namespace command {
constexpr uint8_t CHIP_ID{0xD0};
// constexpr uint8_t CHIP_VERSION{0xD1};
constexpr uint8_t SOFT_RESET{0xE0};
constexpr uint8_t GET_STATUS{0xF3};
constexpr uint8_t CONTROL_MEASUREMENT{0xF4};
constexpr uint8_t CONFIG{0xF5};
constexpr uint8_t GET_MEASUREMENT{0XF7}; // 6bytes
constexpr uint8_t GET_PRESSURE{0xF7}; // 3byts
constexpr uint8_t GET_PRESSURE_MSB{0xF7}; // 7:0
constexpr uint8_t GET_PRESSURE_LSB{0xF8}; // 7:0
constexpr uint8_t GET_PRESSURE_XLSB{0xF9}; // 7:4
constexpr uint8_t GET_TEMPERATURE{0XFA}; // 3 bytes
constexpr uint8_t GET_TEMPERATURE_MSB{0XFA}; // 7:0
constexpr uint8_t GET_TEMPERATURE_LSB{0XFB}; // 7:0
constexpr uint8_t GET_TEMPERATURE_XLSB{0XFC}; // 7:4
constexpr uint8_t TRIMMING_DIG{0x88}; // 12 bytes
constexpr uint8_t TRIMMING_DIG_T1{0x88};
constexpr uint8_t TRIMMING_DIG_T2{0x8A};
constexpr uint8_t TRIMMING_DIG_T3{0x8C};
constexpr uint8_t TRIMMING_DIG_P1{0x8E};
constexpr uint8_t TRIMMING_DIG_P2{0x90};
constexpr uint8_t TRIMMING_DIG_P3{0x92};
constexpr uint8_t TRIMMING_DIG_P4{0x94};
constexpr uint8_t TRIMMING_DIG_P5{0x96};
constexpr uint8_t TRIMMING_DIG_P6{0x98};
constexpr uint8_t TRIMMING_DIG_P7{0x9A};
constexpr uint8_t TRIMMING_DIG_P8{0x9C};
constexpr uint8_t TRIMMING_DIG_P9{0x9A};
constexpr uint8_t TRIMMING_DIG_RESERVED{0xA0};
} // namespace command
} // namespace bmp280
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV3.cpp
@brief ENV III Unit for M5UnitUnified
*/
#include "unit_ENV3.hpp"
#include <M5Utility.hpp>
namespace m5 {
namespace unit {
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
const char UnitENV3::name[] = "UnitENV3";
const types::uid_t UnitENV3::uid{"UnitENV3"_mmh3};
const types::attr_t UnitENV3::attr{attribute::AccessI2C};
UnitENV3::UnitENV3(const uint8_t addr) : Component(addr)
{
// Form a parent-child relationship
auto cfg = component_config();
cfg.max_children = 2;
component_config(cfg);
_valid = add(sht30, 0) && add(qmp6988, 1);
}
std::shared_ptr<Adapter> UnitENV3::ensure_adapter(const uint8_t ch)
{
if (ch > 2) {
M5_LIB_LOGE("Invalid channel %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto unit = child(ch);
if (!unit) {
M5_LIB_LOGE("Not exists unit %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto ad = asAdapter<AdapterI2C>(Adapter::Type::I2C);
return ad ? std::shared_ptr<Adapter>(ad->duplicate(unit->address())) : std::make_shared<Adapter>();
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV3.hpp
@brief ENV III Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_ENV3_HPP
#define M5_UNIT_ENV_UNIT_ENV3_HPP
#include <M5UnitComponent.hpp>
#include "unit_SHT30.hpp"
#include "unit_QMP6988.hpp"
namespace m5 {
namespace unit {
/*!
@class UnitENV3
@brief ENV III is an environmental sensor that integrates SHT30 and QMP6988
@details This unit itself has no I/O, but holds SHT30 and QMP6988 instance
*/
class UnitENV3 : public Component {
// Must not be 0x00 for ensure and assign adapter to children
M5_UNIT_COMPONENT_HPP_BUILDER(UnitENV3, 0xFF /* Dummy address */);
public:
UnitSHT30 sht30; //!< @brief SHT30 instance
UnitQMP6988 qmp6988; //!< @brief QMP6988 instance
explicit UnitENV3(const uint8_t addr = DEFAULT_ADDRESS);
virtual ~UnitENV3()
{
}
virtual bool begin() override
{
return _valid;
}
protected:
virtual std::shared_ptr<Adapter> ensure_adapter(const uint8_t ch);
private:
bool _valid{}; // Did the constructor correctly add the child unit?
Component* _children[2]{&sht30, &qmp6988};
};
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV4.cpp
@brief ENV 4 Unit for M5UnitUnified
*/
#include "unit_ENV4.hpp"
#include <M5Utility.hpp>
namespace m5 {
namespace unit {
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
const char UnitENV4::name[] = "UnitENV4";
const types::uid_t UnitENV4::uid{"UnitENV4"_mmh3};
const types::attr_t UnitENV4::attr{attribute::AccessI2C};
UnitENV4::UnitENV4(const uint8_t addr) : Component(addr)
{
// Form a parent-child relationship
auto cfg = component_config();
cfg.max_children = 2;
component_config(cfg);
_valid = add(sht40, 0) && add(bmp280, 1);
}
std::shared_ptr<Adapter> UnitENV4::ensure_adapter(const uint8_t ch)
{
if (ch > 2) {
M5_LIB_LOGE("Invalid channel %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto unit = child(ch);
if (!unit) {
M5_LIB_LOGE("Not exists unit %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto ad = asAdapter<AdapterI2C>(Adapter::Type::I2C);
return ad ? std::shared_ptr<Adapter>(ad->duplicate(unit->address())) : std::make_shared<Adapter>();
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV4.hpp
@brief ENV IV Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_ENV4_HPP
#define M5_UNIT_ENV_UNIT_ENV4_HPP
#include <M5UnitComponent.hpp>
#include <array>
#include "unit_SHT40.hpp"
#include "unit_BMP280.hpp"
namespace m5 {
namespace unit {
/*!
@class UnitENV4
@brief ENV IV is an environmental sensor that integrates SHT40 and BMP280
@details This unit itself has no I/O, but holds SHT40 and BMP280 instance
*/
class UnitENV4 : public Component {
// Must not be 0x00 for ensure and assign adapter to children
M5_UNIT_COMPONENT_HPP_BUILDER(UnitENV4, 0xFF /* Dummy address */);
public:
UnitSHT40 sht40; //!< @brief SHT40 instance
UnitBMP280 bmp280; //!< @brief BMP280 instance
explicit UnitENV4(const uint8_t addr = DEFAULT_ADDRESS);
virtual ~UnitENV4()
{
}
virtual bool begin() override
{
return _valid;
}
protected:
virtual std::shared_ptr<Adapter> ensure_adapter(const uint8_t ch);
private:
bool _valid{}; // Did the constructor correctly add the child unit?
Component* _children[2]{&sht40, &bmp280};
};
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,594 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_QMP6988.cpp
@brief QMP6988 Unit for M5UnitUnified
*/
#include "unit_QMP6988.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <cmath>
#include <thread>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::qmp6988;
using namespace m5::unit::qmp6988::command;
namespace {
constexpr uint8_t chip_id{0x5C};
constexpr size_t calibration_length{25};
constexpr uint32_t sub_raw{8388608}; // 2^23
constexpr Oversampling osrss_table[][2] = {
// Pressure, Temperature
{Oversampling::X2, Oversampling::X1}, {Oversampling::X4, Oversampling::X1}, {Oversampling::X8, Oversampling::X1},
{Oversampling::X16, Oversampling::X2}, {Oversampling::X32, Oversampling::X4},
};
constexpr PowerMode mode_table[] = {
PowerMode::Sleep,
PowerMode::Forced,
PowerMode::Forced, // duplicated
PowerMode::Normal,
};
constexpr Filter filter_table[] = {
Filter::Off, Filter::Coeff2, Filter::Coeff4, Filter::Coeff8, Filter::Coeff16, Filter::Coeff32,
Filter::Coeff32, // duplicated
Filter::Coeff32, // duplicated
};
struct UseCaseSetting {
OversamplingSetting osrss;
Filter filter;
};
constexpr UseCaseSetting uc_table[] = {
{OversamplingSetting::HighSpeed, Filter::Off},
{OversamplingSetting::LowPower, Filter::Off},
{OversamplingSetting::Standard, Filter::Coeff4},
{OversamplingSetting::HighAccuracy, Filter::Coeff8},
{OversamplingSetting::UltraHightAccuracy, Filter::Coeff32},
};
#if 1
constexpr elapsed_time_t standby_time_table[] = {
5, 5, 50, 250, 500, 1000, 2000, 4000,
};
constexpr float ostb{4.4933f};
constexpr float oversampling_temp_time_table[] = {
0.0f, ostb * 1, ostb * 2, ostb * 4, ostb * 8, ostb * 16, ostb * 32, ostb * 64,
};
constexpr float ospb{0.5032f};
constexpr float oversampling_pressure_time_table[] = {
0.0f, ospb * 1, ospb * 2, ospb * 4, ospb * 8, ospb * 16, ospb * 32, ospb * 64,
};
constexpr float filter_time_table[] = {
0.0f, 0.3f, 0.6f, 1.2f, 2.4f, 4.8f, 9.6f, 9.6f, 9.6f,
};
#endif
constexpr uint32_t interval_table[] = {1, 5, 50, 250, 500, 1000, 2000, 4000};
int16_t convert_temperature256(const int32_t dt, const m5::unit::qmp6988::Calibration& c)
{
int64_t wk1, wk2;
int16_t temp256{};
// wk1: 60Q4 // bit size
wk1 = ((int64_t)c.a1 * (int64_t)dt); // 31Q23+24-1=54 (54Q23)
wk2 = ((int64_t)c.a2 * (int64_t)dt) >> 14; // 30Q47+24-1=53 (39Q33)
wk2 = (wk2 * (int64_t)dt) >> 10; // 39Q33+24-1=62 (52Q23)
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
temp256 = (int16_t)((c.a0 + wk2) >> 4); // 21Q4 -> 17Q0
return temp256;
}
int32_t convert_pressure16(const int32_t dp, const int16_t tx, const Calibration& c)
{
int64_t wk1, wk2, wk3;
// wk1 = 48Q16 // bit size
wk1 = ((int64_t)c.bt1 * (int64_t)tx); // 28Q15+16-1=43 (43Q15)
wk2 = ((int64_t)c.bp1 * (int64_t)dp) >> 5; // 31Q20+24-1=54 (49Q15)
wk1 += wk2; // 43,49->50Q15
wk2 = ((int64_t)c.bt2 * (int64_t)tx) >> 1; // 34Q38+16-1=49 (48Q37)
wk2 = (wk2 * (int64_t)tx) >> 8; // 48Q37+16-1=63 (55Q29)
wk3 = wk2; // 55Q29
wk2 = ((int64_t)c.b11 * (int64_t)tx) >> 4; // 28Q34+16-1=43 (39Q30)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 55,61->62Q29
wk2 = ((int64_t)c.bp2 * (int64_t)dp) >> 13; // 29Q43+24-1=52 (39Q30)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 62,61->63Q29
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
wk2 = ((int64_t)c.b12 * (int64_t)tx); // 29Q53+16-1=45 (45Q53)
wk2 = (wk2 * (int64_t)tx) >> 22; // 45Q53+16-1=61 (39Q31)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q31+24-1=62 (61Q30)
wk3 = wk2; // 61Q30
wk2 = ((int64_t)c.b21 * (int64_t)tx) >> 6; // 29Q60+16-1=45 (39Q54)
wk2 = (wk2 * (int64_t)dp) >> 23; // 39Q54+24-1=62 (39Q31)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q31+24-1=62 (61Q20)
wk3 += wk2; // 61,61->62Q30
wk2 = ((int64_t)c.bp3 * (int64_t)dp) >> 12; // 28Q65+24-1=51 (39Q53)
wk2 = (wk2 * (int64_t)dp) >> 23; // 39Q53+24-1=62 (39Q30)
wk2 = (wk2 * (int64_t)dp); // 39Q30+24-1=62 (62Q30)
wk3 += wk2; // 62,62->63Q30
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
wk1 /= 32767L;
wk1 >>= 11; // Q15 >> 7 = Q4
wk1 += c.b00; // Q4 + 20Q4
// Not shifted to set output at 16 Pa
// wk1 >>= 4; // 28Q4 -> 24Q0
int32_t p16 = (int32_t)wk1;
return p16;
}
} // namespace
struct CtrlMeas {
Oversampling osrs_t() const
{
return static_cast<Oversampling>((value >> 5) & 0x07);
}
Oversampling osrs_p() const
{
return static_cast<Oversampling>((value >> 2) & 0x07);
}
PowerMode mode() const
{
return mode_table[value & 0x03];
}
void osrs_t(const Oversampling os)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(os) & 0x07) << 5);
}
void osrs_p(const Oversampling os)
{
value = (value & ~(0x07 << 2)) | ((m5::stl::to_underlying(os) & 0x07) << 2);
}
void mode(const PowerMode m)
{
value = (value & ~0x03) | (m5::stl::to_underlying(m) & 0x03);
}
uint8_t value{};
};
struct IOSetup {
Standby standby() const
{
return static_cast<Standby>((value >> 5) & 0x07);
}
void standby(const Standby s)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(s) & 0x07) << 5);
}
uint8_t value{};
};
namespace m5 {
namespace unit {
namespace qmp6988 {
float Data::celsius() const
{
uint32_t rt = (((uint32_t)raw[3]) << 16) | (((uint32_t)raw[4]) << 8) | ((uint32_t)raw[5]);
if (calib && rt) {
int32_t dt = (int32_t)(rt - sub_raw);
int16_t t256 = convert_temperature256(dt, *calib);
return (float)t256 / 256.f;
}
return std::numeric_limits<float>::quiet_NaN();
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::pressure() const
{
uint32_t rt = (((uint32_t)raw[3]) << 16) | (((uint32_t)raw[4]) << 8) | ((uint32_t)raw[5]);
uint32_t rp = (((uint32_t)raw[0]) << 16) | (((uint32_t)raw[1]) << 8) | ((uint32_t)raw[2]);
if (calib && rt && rp) {
int32_t dt = (int32_t)(rt - sub_raw);
int16_t t256 = convert_temperature256(dt, *calib);
int32_t dp = (int32_t)(rp - sub_raw);
int32_t p16 = convert_pressure16(dp, t256, *calib);
return (float)p16 / 16.0f;
}
return std::numeric_limits<float>::quiet_NaN();
}
}; // namespace qmp6988
//
const char UnitQMP6988::name[] = "UnitQMP6988";
const types::uid_t UnitQMP6988::uid{"UnitQMP6988"_mmh3};
const types::attr_t UnitQMP6988::attr{attribute::AccessI2C};
types::elapsed_time_t calculatInterval(const Standby st, const Oversampling ost, const Oversampling osp, const Filter f)
{
// M5_LIB_LOGV("ST:%u OST:%u OSP:%u F:%u", st, ost, osp, f);
// M5_LIB_LOGV(
// "Value ST:%u OST:%u OSP:%u F:%u",
// standby_time_table[m5::stl::to_underlying(st)],
// (elapsed_time_t)std::ceil(
// oversampling_temp_time_table[m5::stl::to_underlying(ost)]),
// (elapsed_time_t)std::ceil(
// oversampling_pressure_time_table[m5::stl::to_underlying(osp)]),
// (elapsed_time_t)std::ceil(
// filter_time_table[m5::stl::to_underlying(f)]));
elapsed_time_t itv = standby_time_table[m5::stl::to_underlying(st)] +
(elapsed_time_t)std::ceil(oversampling_temp_time_table[m5::stl::to_underlying(ost)]) +
(elapsed_time_t)std::ceil(oversampling_pressure_time_table[m5::stl::to_underlying(osp)]) +
(elapsed_time_t)std::ceil(filter_time_table[m5::stl::to_underlying(f)]);
return itv;
}
bool UnitQMP6988::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
uint8_t id{};
if (!readRegister8(CHIP_ID, id, 0) || id != chip_id) {
M5_LIB_LOGE("This unit is NOT QMP6988 %x", id);
return false;
}
if (!softReset()) {
M5_LIB_LOGE("Failed to reset");
return false;
}
if (!read_calibration(_calibration)) {
M5_LIB_LOGE("Failed to read_calibration");
return false;
}
return _cfg.start_periodic
? startPeriodicMeasurement(_cfg.osrs_pressure, _cfg.osrs_temperature, _cfg.filter, _cfg.standby)
: true;
}
void UnitQMP6988::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
Data d{};
//_updated = is_data_ready() && read_measurement(d);
_updated = read_measurement(d, _only_temperature);
if (_updated) {
// auto dur = at - _latest;
// M5_LIB_LOGW(">DUR:%ld", dur);
_latest = at;
_data->push_back(d);
}
}
}
}
bool UnitQMP6988::start_periodic_measurement(const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f,
const Standby st)
{
if (inPeriodic()) {
return false;
}
// Need temperature for measure pressure (Only temperature measurement is acceptable)
if (osrsTemperature == Oversampling::Skipped) {
return false;
}
_only_temperature = (osrsPressure == Oversampling::Skipped);
return writeOversampling(osrsPressure, osrsTemperature) && writeFilter(f) && writeStandbyTime(st) &&
start_periodic_measurement();
}
bool UnitQMP6988::start_periodic_measurement()
{
if (inPeriodic()) {
return false;
}
IOSetup is{};
_periodic = readRegister8(IO_SETUP, is.value, 0) && writePowerMode(PowerMode::Normal);
if (_periodic) {
_latest = 0;
_interval = interval_table[m5::stl::to_underlying(is.standby())];
}
return _periodic;
}
bool UnitQMP6988::stop_periodic_measurement()
{
if (inPeriodic() && writePowerMode(PowerMode::Sleep)) {
_periodic = false;
return true;
}
return false;
}
bool UnitQMP6988::measureSingleshot(qmp6988::Data& d, const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
// Need temperature for measure pressure (Only temperature measurement is acceptable)
if (osrsTemperature == Oversampling::Skipped) {
return false;
}
return writeOversampling(osrsPressure, osrsTemperature) && writeFilter(f) && measureSingleshot(d);
}
bool UnitQMP6988::measureSingleshot(qmp6988::Data& d)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0) && writePowerMode(qmp6988::PowerMode::Forced)) {
auto timeout_at = m5::utility::millis() + 1 * 1000;
bool done{};
do {
done = is_data_ready();
if (done) {
break;
}
std::this_thread::yield();
// m5::utility::delay(1);
} while (!done && m5::utility::millis() <= timeout_at);
return done && read_measurement(d, cm.osrs_p() == Oversampling::Skipped);
}
return false;
}
bool UnitQMP6988::readOversampling(qmp6988::Oversampling& osrsPressure, qmp6988::Oversampling& osrsTemperature)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
osrsPressure = cm.osrs_p();
osrsTemperature = cm.osrs_t();
return true;
}
return false;
}
bool UnitQMP6988::writeOversampling(const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::writeOversamplingPressure(const qmp6988::Oversampling osrsPressure)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::writeOversamplingTemperature(const qmp6988::Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::writeOversampling(const qmp6988::OversamplingSetting osrss)
{
auto idx = m5::stl::to_underlying(osrss);
return writeOversampling(osrss_table[idx][0], osrss_table[idx][1]);
}
bool UnitQMP6988::readPowerMode(qmp6988::PowerMode& m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
m = cm.mode();
return true;
}
return false;
}
bool UnitQMP6988::writePowerMode(const qmp6988::PowerMode m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.mode(m);
// Changing mode during measurement may result in erratic data the next time
auto timeout_at = m5::utility::millis() + 1000;
bool can{};
do {
can = is_data_ready();
if (can) {
break;
}
m5::utility::delay(1);
} while (!can && m5::utility::millis() <= timeout_at);
return can && writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::readFilter(qmp6988::Filter& f)
{
uint8_t v{};
if (readRegister8(IIR_FILTER, v, 0)) {
f = filter_table[v & 0x07];
return true;
}
return false;
}
bool UnitQMP6988::writeFilter(const qmp6988::Filter f)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
return writeRegister8(IIR_FILTER, m5::stl::to_underlying(f));
}
bool UnitQMP6988::readStandbyTime(qmp6988::Standby& st)
{
IOSetup is{};
if (readRegister8(IO_SETUP, is.value, 0)) {
st = is.standby();
return true;
}
return false;
}
bool UnitQMP6988::writeStandbyTime(const qmp6988::Standby st)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
IOSetup is{};
if (readRegister8(IO_SETUP, is.value, 0)) {
is.standby(st);
return writeRegister8(IO_SETUP, is.value);
}
return false;
}
bool UnitQMP6988::writeUseCaseSetting(const qmp6988::UseCase uc)
{
const auto& tbl = uc_table[m5::stl::to_underlying(uc)];
return writeOversampling(tbl.osrss) && writeFilter(tbl.filter);
}
bool UnitQMP6988::softReset()
{
constexpr uint8_t v{0xE6}; // When inputting "E6h", a soft-reset will be occurred
auto ret = writeRegister8(SOFT_RESET, v);
M5_LIB_LOGD("Reset causes a NO ACK or timeout error, but ignore it");
(void)ret;
m5::utility::delay(10); // Need delay
if (writeRegister(SOFT_RESET, 0x00)) {
_periodic = false;
return true;
}
return false;
}
bool UnitQMP6988::is_data_ready()
{
uint8_t v{};
return readRegister8(GET_STATUS, v, 0) && ((v & 0x08 /* measure */) == 0);
}
bool UnitQMP6988::read_measurement(Data& d, const bool only_temperature)
{
if (readRegister(READ_PRESSURE, d.raw.data(), d.raw.size(), 0)) {
// If osrs_p is Skipped, but the previous pressure data is still there, so it is deleted
if (only_temperature) {
d.raw[0] = d.raw[1] = d.raw[2] = 0;
}
d.calib = &_calibration;
// M5_DUMPI(d.raw.data(), d.raw.size());
return true;
}
return false;
}
bool UnitQMP6988::read_calibration(qmp6988::Calibration& c)
{
using namespace m5::utility; // unsigned_to_signed
using namespace m5::types; // big_uint16_t
uint8_t rbuf[calibration_length]{};
if (!readRegister(READ_COMPENSATION_COEFFICIENT, rbuf, sizeof(rbuf), 0)) {
return false;
}
uint32_t b00 = ((uint32_t)(big_uint16_t(rbuf[0], rbuf[1]).get()) << 4) | ((rbuf[24] >> 4) & 0x0F);
c.b00 = unsigned_to_signed<20>(b00); // 20Q4
c.bt1 = 2982L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[2], rbuf[3]).get()) + 107370906L; // 28Q15
c.bt2 = 329854L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[4], rbuf[5]).get()) + +108083093L; // 34Q38
c.bp1 = 19923L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[6], rbuf[7]).get()) + 1133836764L; // 31Q20
c.b11 = 2406L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[8], rbuf[9]).get()) + 118215883L; // 28Q34
c.bp2 = 3079L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[10], rbuf[11]).get()) - 181579595L; // 29Q43
c.b12 = 6846L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[12], rbuf[13]).get()) + 85590281L; // 29Q53
c.b21 = 13836L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[14], rbuf[15]).get()) + 79333336L; // 29Q60
c.bp3 = 2915L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[16], rbuf[17]).get()) + 157155561L; // 28Q65
uint32_t a0 = ((uint32_t)big_uint16_t(rbuf[18], rbuf[19]).get() << 4) | (rbuf[24] & 0x0F);
c.a0 = unsigned_to_signed<20>(a0); // 20Q4
c.a1 = 3608L * (int32_t)unsigned_to_signed<16>(big_uint16_t(rbuf[20], rbuf[21]).get()) - 1731677965L; // 31Q23
c.a2 = 16889L * (int32_t)unsigned_to_signed<16>(big_uint16_t(rbuf[22], rbuf[23]).get()) - 87619360L; // 31Q47
#if 0
M5_LIB_LOGI(
"\n"
"b00:%d\n"
"bt1:%d\n"
"bt2:%lld\n"
"bp1:%d\n"
"b11:%d\n"
"bp2:%d\n"
"b12:%d\n"
"b21:%d\n"
"bp3:%d\n"
"a0:%d\n"
"a1:%d\n"
"a2:%d",
c.b00, c.bt1, c.bt2, c.bp1, c.b11, c.bp2, c.b12, c.b21, c.bp3, c.a0,
c.a1, c.a2);
#endif
return true;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,393 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_QMP6988.hpp
@brief QMP6988 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_QMP6988_HPP
#define M5_UNIT_ENV_UNIT_QMP6988_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/stl/extension.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
namespace qmp6988 {
/*!
@enum PowerMode
@brief Operation mode
*/
enum class PowerMode : uint8_t {
Sleep, //!< No measurements are performed
Forced, //!< Single measurements are performed
Normal = 0x03, //!< Periodic measurements are performed
};
/*!
@enum Oversampling
@brief Oversampling value
@warning
*/
enum class Oversampling : uint8_t {
Skipped, //!< Skipped (No measurements are performed)
X1, //!< x1
X2, //!< x2
X4, //!< x4
X8, //!< x8
X16, //!< x16
X32, //!< x32
X64, //!< x64
};
/*!
@enum OversamplingSetting
@brief Oversampling Settings
*/
enum class OversamplingSetting : uint8_t {
HighSpeed, //!< osrsP:X2 osrsT:X1
LowPower, //!< osrsP:X4 osrsT:X1
Standard, //!< osrsP:X8 osrsT:X1
HighAccuracy, //!< osrsP:X16 osrsT:X2
UltraHightAccuracy, //!< osrsP:X32 osrsT:X4
};
/*!
@enum Filter
@brief Filtter setting
*/
enum class Filter : uint8_t {
Off, //!< Off filter
Coeff2, //!< co-efficient 2
Coeff4, //!< co-efficient 4
Coeff8, //!< co-efficient 8
Coeff16, //!< co-efficient 16
Coeff32, //!< co-efficient 32
};
/*!
@enum Standby
@brief Measurement standby time for power mode Normal
*/
enum class Standby : uint8_t {
Time1ms, //!< 1 ms
Time5ms, //!< 5 ms
Time50ms, //!< 50 ms
Time250ms, //!< 250 ms
Time500ms, //!< 500 ms
Time1sec, //!< 1 seconds
Time2sec, //!< 2 seconds
Time4sec, //!< 4 seconds
};
/*!
@enum UseCase
@brief Preset settings
*/
enum class UseCase : uint8_t {
Weather, //!< Weather monitoring
Drop, //!< Drop detection
Elevator, //!< Elevator / floor change detection
Stair, //!< Stair detection
Indoor, //!< Indoor navigation
};
///@cond
struct Calibration {
int32_t b00{}, bt1{}, bp1{};
int64_t bt2{};
int32_t b11{}, bp2{}, b12{}, b21{}, bp3{}, a0{}, a1{}, a2{};
};
///@endcond
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float pressure() const; //!< pressure (Pa)
const Calibration* calib{};
};
}; // namespace qmp6988
/*!
@class UnitQMP6988
@brief Barometric pressure sensor to measure atmospheric pressure and altitude estimation
*/
class UnitQMP6988 : public Component, public PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitQMP6988, 0x70);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! pressure oversampling if start on begin
qmp6988::Oversampling osrs_pressure{qmp6988::Oversampling::X8};
//! temperature oversampling if start on begin
qmp6988::Oversampling osrs_temperature{qmp6988::Oversampling::X1};
//! Filter if start on begin
qmp6988::Filter filter{qmp6988::Filter::Coeff4};
//! Standby time if start on begin
qmp6988::Standby standby{qmp6988::Standby::Time1sec};
};
explicit UnitQMP6988(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<qmp6988::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitQMP6988()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured pressure (Pa)
inline float pressure() const
{
return !empty() ? oldest().pressure() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@param st Standby time
@return True if successful
@warning Measuring pressure requires measuring temperature
*/
inline bool startPeriodicMeasurement(const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f,
const qmp6988::Standby st)
{
return PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data>::startPeriodicMeasurement(osrsPressure,
osrsTemperature, f, st);
}
//! @brief Start periodic measurement using current settings
inline bool startPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data>::startPeriodicMeasurement();
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measuring pressure requires measuring temperature
@warning Each setting is overwritten
*/
bool measureSingleshot(qmp6988::Data& d, const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f);
//! @brief Measurement single shot using current settings
bool measureSingleshot(qmp6988::Data& d);
///@}
///@name Settings
///@{
/*!
@brief Read the oversampling conditions
@param[out] osrsPressure Oversampling for pressure
@param[out] osrsTemperature Oversampling for temperature
@return True if successful
*/
bool readOversampling(qmp6988::Oversampling& osrsPressure, qmp6988::Oversampling& osrsTemperature);
/*!
@brief Write the oversampling conditions
@param osrsPressure Oversampling for pressure
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const qmp6988::Oversampling osrsPressure, const qmp6988::Oversampling osrsTemperature);
/*!
@brief Write the oversampling conditions for pressure
@param osrsPressure Oversampling for pressure
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingPressure(const qmp6988::Oversampling osrsPressure);
/*!
@brief Write the oversampling conditions for temperature
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingTemperature(const qmp6988::Oversampling osrsTemperature);
/*!
@brief Write the oversampling by OversamplingSetting
@param osrss OversamplingSetting
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const qmp6988::OversamplingSetting osrss);
/*!
@brief Read the IIR filter co-efficient
@param[out] f filter
@return True if successful
*/
bool readFilter(qmp6988::Filter& f);
/*!
@brief Write the IIR filter co-efficient
@param f filter
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeFilter(const qmp6988::Filter f);
/*!
@brief Read the standby time
@param[out] st standby time
@return True if successful
*/
bool readStandbyTime(qmp6988::Standby& st);
/*!
@brief Write the standby time
@param st standby time
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeStandbyTime(const qmp6988::Standby st);
/*!
@brief Read the power mode
@param[out] mode PowerMode
@return True if successful
*/
bool readPowerMode(qmp6988::PowerMode& mode);
/*!
@brief Write the power mode
@param m Power mode
@return True if successful
@warning Note that the measurement mode is changed
@warning It is recommended to use start/stopPeriodicMeasurement or similar to change the measurement mode
*/
bool writePowerMode(const qmp6988::PowerMode mode);
/*!
@brief Write the settings based on use cases
@param uc UseCase
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeUseCaseSetting(const qmp6988::UseCase uc);
///@}
/*!
@brief Soft reset
@return True if successful
*/
bool softReset();
protected:
bool start_periodic_measurement();
bool start_periodic_measurement(const qmp6988::Oversampling ost, const qmp6988::Oversampling osp,
const qmp6988::Filter f, const qmp6988::Standby st);
bool stop_periodic_measurement();
bool read_calibration(qmp6988::Calibration& c);
bool read_measurement(qmp6988::Data& d, const bool only_temperature = false);
bool is_data_ready();
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitQMP6988, qmp6988::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<qmp6988::Data>> _data{};
qmp6988::Calibration _calibration{};
config_t _cfg{};
bool _only_temperature{};
};
///@cond
namespace qmp6988 {
namespace command {
constexpr uint8_t CHIP_ID{0xD1};
constexpr uint8_t READ_PRESSURE{0xF7}; // ~ F9 3bytes
constexpr uint8_t READ_TEMPERATURE{0xFA}; // ~ FC 3bytes
constexpr uint8_t IO_SETUP{0xF5};
constexpr uint8_t CONTROL_MEASUREMENT{0xF4};
constexpr uint8_t GET_STATUS{0xF3};
constexpr uint8_t IIR_FILTER{0xF1};
constexpr uint8_t SOFT_RESET{0xE0};
constexpr uint8_t READ_COMPENSATION_COEFFICIENT{0xA0}; // ~ 0xB8 25 bytes
} // namespace command
} // namespace qmp6988
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,530 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD40.cpp
@brief SCD40 Unit for M5UnitUnified
*/
#include "unit_SCD40.hpp"
#include <M5Utility.hpp>
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::scd4x;
using namespace m5::unit::scd4x::command;
namespace {
struct Temperature {
constexpr static float toFloat(const uint16_t u16)
{
return u16 * 175.f / 65536.f;
}
constexpr static uint16_t toUint16(const float f)
{
return f * 65536 / 175;
}
constexpr static float OFFSET_MIN{0.0f};
constexpr static float OFFSET_MAX{175.0f};
};
constexpr uint16_t mode_reg_table[] = {
START_PERIODIC_MEASUREMENT,
START_LOW_POWER_PERIODIC_MEASUREMENT,
};
constexpr uint32_t interval_table[] = {
5000U, // 5 Sec.
30 * 1000U, // 30 Sec.
};
const uint8_t VARIANT_VALUE[2]{0x04, 0x40}; // SCD40
} // namespace
namespace m5 {
namespace unit {
namespace scd4x {
uint16_t Data::co2() const
{
return m5::types::big_uint16_t(raw[0], raw[1]).get();
}
float Data::celsius() const
{
return -45 + Temperature::toFloat(m5::types::big_uint16_t(raw[3], raw[4]).get());
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::humidity() const
{
return 100.f * m5::types::big_uint16_t(raw[6], raw[7]).get() / 65536.f;
}
} // namespace scd4x
// class UnitSCD40
const char UnitSCD40::name[] = "UnitSCD40";
const types::uid_t UnitSCD40::uid{"UnitSCD40"_mmh3};
const types::attr_t UnitSCD40::attr{attribute::AccessI2C};
bool UnitSCD40::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
// Stop (to idle mode)
if (!writeRegister(STOP_PERIODIC_MEASUREMENT)) {
M5_LIB_LOGE("Failed to stop");
return false;
}
m5::utility::delay(STOP_PERIODIC_MEASUREMENT_DURATION);
if (!is_valid_chip()) {
return false;
}
if (!writeAutomaticSelfCalibrationEnabled(_cfg.calibration)) {
M5_LIB_LOGE("Failed to writeAutomaticSelfCalibrationEnabled");
return false;
}
// Stop
return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.mode) : true;
}
bool UnitSCD40::is_valid_chip()
{
uint8_t var[2]{};
if (!read_register(GET_SENSOR_VARIANT, var, 2) || memcmp(var, VARIANT_VALUE, 2) != 0) {
M5_LIB_LOGE("Not SCD40 %02X:%02X", var[0], var[1]);
return false;
}
return true;
}
void UnitSCD40::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
auto at = m5::utility::millis();
if (force || !_latest || at >= _latest + _interval) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = m5::utility::millis(); // Data acquisition takes time, so acquire again
_data->push_back(d);
}
}
}
}
bool UnitSCD40::start_periodic_measurement(const Mode mode)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
auto m = m5::stl::to_underlying(mode);
_periodic = writeRegister(mode_reg_table[m]);
if (_periodic) {
_interval = interval_table[m];
_latest = 0;
}
return _periodic;
}
bool UnitSCD40::stop_periodic_measurement(const uint32_t duration)
{
if (inPeriodic()) {
if (writeRegister(STOP_PERIODIC_MEASUREMENT)) {
_periodic = false;
m5::utility::delay(duration);
return true;
}
}
return false;
}
bool UnitSCD40::writeTemperatureOffset(const float offset, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (offset < Temperature::OFFSET_MIN || offset >= Temperature::OFFSET_MAX) {
M5_LIB_LOGE("offset is not a valid scope %f", offset);
return false;
}
uint8_t wbuf[2]{};
uint16_t tmp16 = Temperature::toUint16(offset);
wbuf[0] = tmp16 >> 8;
wbuf[1] = tmp16 & 0xFF;
return write_register(SET_TEMPERATURE_OFFSET, wbuf, sizeof(wbuf)) && delay_true(duration);
}
bool UnitSCD40::readTemperatureOffset(float& offset)
{
offset = 0.0f;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_TEMPERATURE_OFFSET, u16.data(), u16.size(), GET_TEMPERATURE_OFFSET_DURATION)) {
offset = Temperature::toFloat(u16.get());
return true;
}
return false;
}
bool UnitSCD40::writeSensorAltitude(const uint16_t altitude, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16(altitude);
return write_register(SET_SENSOR_ALTITUDE, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readSensorAltitude(uint16_t& altitude)
{
altitude = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_SENSOR_ALTITUDE, u16.data(), u16.size(), GET_SENSOR_ALTITUDE_DURATION)) {
altitude = u16.get();
return true;
}
return false;
}
bool UnitSCD40::writeAmbientPressure(const uint16_t pressure, const uint32_t duration)
{
constexpr uint32_t PRESSURE_MIN{700};
constexpr uint32_t PRESSURE_MAX{1200};
#if 0
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
#endif
if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) {
M5_LIB_LOGE("pressure is not a valid scope (%u - %u) %u", PRESSURE_MIN, PRESSURE_MAX, pressure);
return false;
}
m5::types::big_uint16_t u16(pressure);
return write_register(AMBIENT_PRESSURE, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readAmbientPressure(uint16_t& pressure)
{
pressure = 0;
#if 0
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
#endif
m5::types::big_uint16_t u16{};
if (read_register(AMBIENT_PRESSURE, u16.data(), u16.size(), GET_AMBIENT_PRESSURE_DURATION)) {
pressure = u16.get();
return true;
}
return false;
}
bool UnitSCD40::performForcedRecalibration(const uint16_t concentration, int16_t& correction)
{
// 1. Operate the SCD4x in the operation mode later used in normal sensor
// operation (periodic measurement, low power periodic measurement or single
// shot) for > 3 minutes in an environment with homogenous and constant CO2
// concentration.
// 2. Issue stop_periodic_measurement. Wait 500 ms for the stop command to
// complete.
correction = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16(concentration);
if (!write_register(PERFORM_FORCED_CALIBRATION, u16.data(), u16.size())) {
return false;
}
#if 0
// 3. Subsequently issue the perform_forced_recalibration command and
// optionally read out the FRC correction (i.e. the magnitude of the
// correction) after waiting for 400 ms for the command to complete.
m5::utility::delay(PERFORM_FORCED_CALIBRATION_DURATION);
std::array<uint8_t, 3> rbuf{};
if (readWithTransaction(rbuf.data(), rbuf.size()) == m5::hal::error::error_t::OK) {
m5::types::big_uint16_t u16{rbuf[0], rbuf[1]};
m5::utility::CRC8_Checksum crc{};
if (rbuf[2] == crc.range(u16.data(), u16.size()) && u16.get() != 0xFFFF) {
correction = (int16_t)(u16.get() - 0x8000);
return true;
}
}
return false;
#else
// 3. Subsequently issue the perform_forced_recalibration command and
// optionally read out the FRC correction (i.e. the magnitude of the
// correction) after waiting for 400 ms for the command to complete.
m5::utility::delay(PERFORM_FORCED_CALIBRATION_DURATION);
if (read_register(PERFORM_FORCED_CALIBRATION, u16.data(), u16.size()) && u16.get() != 0xFFFF) {
correction = (int16_t)(u16.get() - 0x8000);
return true;
}
return false;
#endif
}
bool UnitSCD40::writeAutomaticSelfCalibrationEnabled(const bool enabled, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16(enabled ? 0x0001 : 0x0000);
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readAutomaticSelfCalibrationEnabled(bool& enabled)
{
enabled = false;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION)) {
enabled = (u16.get() == 0x0001);
return true;
}
return false;
}
bool UnitSCD40::writeAutomaticSelfCalibrationTarget(const uint16_t ppm, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{ppm};
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_TARGET, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readAutomaticSelfCalibrationTarget(uint16_t& ppm)
{
ppm = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_TARGET, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION)) {
ppm = u16.get();
return true;
}
return false;
}
bool UnitSCD40::writePersistSettings(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(PERSIST_SETTINGS)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD40::readSerialNumber(char* serialNumber)
{
if (!serialNumber) {
return false;
}
*serialNumber = '\0';
uint64_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{12};
while (i--) {
*serialNumber++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*serialNumber = '\0';
return true;
}
return false;
}
bool UnitSCD40::readSerialNumber(uint64_t& serialNumber)
{
std::array<uint8_t, 9> rbuf;
serialNumber = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::utility::CRC8_Checksum crc{};
if (readRegister(GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), GET_SERIAL_NUMBER_DURATION)) {
m5::types::big_uint16_t u16[3]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}, {rbuf[6], rbuf[7]}};
if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5] &&
crc.range(u16[2].data(), u16[2].size()) == rbuf[8]) {
serialNumber = ((uint64_t)u16[0].get()) << 32 | ((uint64_t)u16[1].get()) << 16 | ((uint64_t)u16[2].get());
return true;
}
}
return false;
}
bool UnitSCD40::performSelfTest(bool& malfunction)
{
malfunction = true;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(PERFORM_SELF_TEST, u16.data(), u16.size(), PERFORM_SELF_TEST_DURATION)) {
malfunction = (u16.get() != 0);
return true;
}
return false;
}
bool UnitSCD40::performFactoryReset(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(PERFORM_FACTORY_RESET)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD40::reInit(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(REINIT)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD40::read_data_ready_status()
{
uint16_t res{};
return readRegister16BE(GET_DATA_READY_STATUS, res, GET_DATA_READY_STATUS_DURATION) ? (res & 0x07FF) != 0 : false;
}
// TH only if all is false
bool UnitSCD40::read_measurement(Data& d, const bool all)
{
if (!read_data_ready_status()) {
M5_LIB_LOGV("Not ready");
return false;
}
if (!readRegister(READ_MEASUREMENT, d.raw.data(), d.raw.size(), READ_MEASUREMENT_DURATION)) {
return false;
}
// For RHT only, previous Co2 data may be obtained and should be dismissed
if (!all) {
d.raw[0] = d.raw[1] = d.raw[2] = 0;
}
// Check CRC
m5::utility::CRC8_Checksum crc{};
for (uint_fast8_t i = all ? 0 : 1; i < 3; ++i) {
if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) {
return false;
}
}
return true;
}
bool UnitSCD40::read_register(const uint16_t reg, uint8_t* rbuf, const uint32_t rlen, const uint32_t duration)
{
uint8_t tmp[rlen + 1]{};
if (!rbuf || !rlen || !readRegister(reg, tmp, sizeof(tmp), duration)) {
return false;
}
m5::utility::CRC8_Checksum crc{};
auto crc8 = crc.range(tmp, rlen);
if (crc8 != tmp[rlen]) {
M5_LIB_LOGE("CRC8 Error:%02X, %02X", tmp[rlen], crc8);
return false;
}
memcpy(rbuf, tmp, rlen);
return true;
}
bool UnitSCD40::write_register(const uint16_t reg, uint8_t* wbuf, const uint32_t wlen)
{
uint8_t buf[wlen + 1]{};
if (!wbuf || !wlen) {
return false;
}
memcpy(buf, wbuf, wlen);
m5::utility::CRC8_Checksum crc{};
auto crc8 = crc.range(wbuf, wlen);
buf[wlen] = crc8;
return writeRegister(reg, buf, sizeof(buf));
}
bool UnitSCD40::delay_true(const uint32_t duration)
{
m5::utility::delay(duration);
return true; // Always true
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,390 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD40.hpp
@brief SCD40 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SCD40_HPP
#define M5_UNIT_ENV_UNIT_SCD40_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
/*!
@namespace scd4x
@brief For SCD40/41
*/
namespace scd4x {
/*!
@enum Mode
@brief Mode of periodic measurement
*/
enum class Mode : uint8_t {
Normal, //!< Normal (Receive data every 5 seconds)
LowPower, //!< Low power (Receive data every 30 seconds)
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 9> raw{}; //!< @brief RAW data
uint16_t co2() const; //!< @brief CO2 concentration (ppm)
//! @brief temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< @brief temperature (Celsius)
float fahrenheit() const; //!< @brief temperature (Fahrenheit)
float humidity() const; //!< @brief humidity (RH)
};
///@cond
// Max command duration(ms)
// For SCD40/41
constexpr uint16_t READ_MEASUREMENT_DURATION{1};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT_DURATION{500};
constexpr uint16_t SET_TEMPERATURE_OFFSET_DURATION{1};
constexpr uint16_t GET_TEMPERATURE_OFFSET_DURATION{1};
constexpr uint16_t SET_SENSOR_ALTITUDE_DURATION{1};
constexpr uint16_t GET_SENSOR_ALTITUDE_DURATION{1};
constexpr uint16_t SET_AMBIENT_PRESSURE_DURATION{1};
constexpr uint16_t GET_AMBIENT_PRESSURE_DURATION{1};
constexpr uint16_t PERFORM_FORCED_CALIBRATION_DURATION{400};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1};
constexpr uint16_t GET_DATA_READY_STATUS_DURATION{1};
constexpr uint16_t PERSIST_SETTINGS_DURATION{800};
constexpr uint16_t GET_SERIAL_NUMBER_DURATION{1};
constexpr uint16_t PERFORM_SELF_TEST_DURATION{10000};
constexpr uint16_t PERFORM_FACTORY_RESET_DURATION{1200};
constexpr uint16_t REINIT_DURATION{20};
/// @endcond
} // namespace scd4x
/*!
@class m5::unit::UnitSCD40
@brief SCD40 unit component
*/
class UnitSCD40 : public Component, public PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSCD40, 0x62);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Mode of periodic measurement if start on begin?
scd4x::Mode mode{scd4x::Mode::Normal};
//! Enable ASC on begin?
bool calibration{true};
};
explicit UnitSCD40(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<scd4x::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSCD40()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config() const
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t &cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured CO2 concentration (ppm)
inline uint16_t co2() const
{
return !empty() ? oldest().co2() : 0;
}
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured humidity (RH)
inline float humidity() const
{
return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param mode Measurement mode
@return True if successful
*/
inline bool startPeriodicMeasurement(const scd4x::Mode mode = scd4x::Mode::Normal)
{
return PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data>::startPeriodicMeasurement(mode);
}
/*!
@brief Start low power periodic measurement
@return True if successful
*/
inline bool startLowPowerPeriodicMeasurement()
{
return startPeriodicMeasurement(scd4x::Mode::LowPower);
}
/*!
@brief Stop periodic measurement
@param duration Max command duration(ms)
@return True if successful
*/
inline bool stopPeriodicMeasurement(const uint32_t duration = scd4x::STOP_PERIODIC_MEASUREMENT_DURATION)
{
return PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data>::stopPeriodicMeasurement(duration);
}
///@}
///@name On-Chip Output Signal Compensation
///@{
/*!
@brief Write the temperature offset
@details Define how warm the sensor is compared to ambient, so RH and T
are temperature compensated. Has no effect on the CO2 reading Default offsetis 4C
@param offset (0 <= offset < 175)
@param duration Max command duration(ms)
@return True if successful
@note Recommended temperature offset values are between 0 and 20
@warning During periodic detection runs, an error is returned
*/
bool writeTemperatureOffset(const float offset, const uint32_t duration = scd4x::SET_TEMPERATURE_OFFSET_DURATION);
/*!
@brief Read the temperature offset
@param[out] offset Offset value
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readTemperatureOffset(float &offset);
/*!
@brief Write the sensor altitude
@details Define the sensor altitude in metres above sea level, so RH and CO2 arecompensated for atmospheric
pressure Default altitude is 0m
@param altitude Sensor altitude [m]
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeSensorAltitude(const uint16_t altitude, const uint32_t duration = scd4x::SET_SENSOR_ALTITUDE_DURATION);
/*!
@brief Read the sensor altitude
@param[out] altitude Sensor altitude [m]
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readSensorAltitude(uint16_t &altitude);
/*!
@brief Write the ambient pressure
@details Define the ambient pressure in Pascals, so RH and CO2 are compensated for atmospheric pressure
setAmbientPressure overrides setSensorAltitude
@param pressure Ambient pressure [hPa]
@param duration Max command duration(ms)
@return True if successful
@warning Valid Valid input values are between 700 1200 hPa
*/
bool writeAmbientPressure(const uint16_t pressure, const uint32_t duration = scd4x::SET_AMBIENT_PRESSURE_DURATION);
/*!
@brief Read the ambient pressure
@param[out] presure Ambient pressure [hPa]
@return True if successful
*/
bool readAmbientPressure(uint16_t &pressure);
///@}
///@name Field Calibration
///@{
/*!
@brief Perform forced recalibration
@param concentration Unit:ppm
@param[out] correction The FRC correction value
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Blocked until the process is completed (about 400ms)
*/
bool performForcedRecalibration(const uint16_t concentration, int16_t &correction);
/*!
@brief Enable/disable automatic self calibration
@param enabled Enable automatic self calibration if true
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeAutomaticSelfCalibrationEnabled(
const bool enabled = true, const uint32_t duration = scd4x::SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION);
/*!
@brief Check if automatic self calibration is enabled
@param[out] enabled True if automatic self calibration is enabled
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readAutomaticSelfCalibrationEnabled(bool &enabled);
/*!
@brief Write the value of the ASC baseline target
@param ppm ASC target ppm
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeAutomaticSelfCalibrationTarget(
const uint16_t ppm, const uint32_t duration = scd4x::SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION);
/*!
@brief Read the value of the ASC baseline target
@param[out] ppm ASC target ppm
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readAutomaticSelfCalibrationTarget(uint16_t &ppm);
///@}
///@name Advanced Features
///@{
/*!
@brief Write sensor settings from RAM to EEPROM
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writePersistSettings(const uint32_t duration = scd4x::PERSIST_SETTINGS_DURATION);
/*!
@brief Read the serial number string
@param[out] serialNumber Output buffer
@return True if successful
@warning Size must be at least 13 bytes
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(char *serialNumber);
/*!
@brief Read the serial number value
@param[out] serialNumber serial number value
@return True if successful
@note The serial number is 48 bit
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(uint64_t &serialNumber);
/*!
@brief Perform self test
@param[out] True if malfunction detected
@return True if successful
@note Takes 10 seconds to complete
@warning During periodic detection runs, an error is returned
*/
bool performSelfTest(bool &malfunction);
/*!
@brief Peform factory reset
@details Reset all settings to the factory values
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measurement duration max 1200 ms
*/
bool performFactoryReset(const uint32_t duration = scd4x::PERFORM_FACTORY_RESET_DURATION);
/*!
@brief Re-initialize the sensor, load settings from EEPROM
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measurement duration max 20 ms
*/
bool reInit(const uint32_t duration = scd4x::REINIT_DURATION);
///@}
protected:
bool read_register(const uint16_t reg, uint8_t *rbuf, const uint32_t rlen, const uint32_t duration = 1);
bool write_register(const uint16_t reg, uint8_t *wbuf, const uint32_t wlen);
bool start_periodic_measurement(const scd4x::Mode mode = scd4x::Mode::Normal);
bool stop_periodic_measurement(const uint32_t duration = scd4x::STOP_PERIODIC_MEASUREMENT_DURATION);
bool read_data_ready_status();
bool read_measurement(scd4x::Data &d, const bool all = true);
virtual bool is_valid_chip();
bool delay_true(const uint32_t duration);
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSCD40, scd4x::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<scd4x::Data>> _data{};
config_t _cfg{};
};
///@cond
namespace scd4x {
namespace command {
// Basic Commands
constexpr uint16_t START_PERIODIC_MEASUREMENT{0x21b1};
constexpr uint16_t READ_MEASUREMENT{0xec05};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT{0x3f86};
// On-chip output signal compensation
constexpr uint16_t SET_TEMPERATURE_OFFSET{0x241d};
constexpr uint16_t GET_TEMPERATURE_OFFSET{0x2318};
constexpr uint16_t SET_SENSOR_ALTITUDE{0x2427};
constexpr uint16_t GET_SENSOR_ALTITUDE{0x2322};
constexpr uint16_t AMBIENT_PRESSURE{0xe000};
// Field calibration
constexpr uint16_t PERFORM_FORCED_CALIBRATION{0x362f};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED{0x2416};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED{0x2313};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET{0x243a};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET{0x233f};
// Low power
constexpr uint16_t START_LOW_POWER_PERIODIC_MEASUREMENT{0x21ac};
constexpr uint16_t GET_DATA_READY_STATUS{0xe4b8};
// Advanced features
constexpr uint16_t PERSIST_SETTINGS{0x3615};
constexpr uint16_t GET_SERIAL_NUMBER{0x3682};
constexpr uint16_t PERFORM_SELF_TEST{0x3639};
constexpr uint16_t PERFORM_FACTORY_RESET{0x3632};
constexpr uint16_t REINIT{0x3646};
//
constexpr uint16_t GET_SENSOR_VARIANT{0x202f};
} // namespace command
} // namespace scd4x
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,178 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD41.cpp
@brief SCD41 Unit for M5UnitUnified
*/
#include "unit_SCD41.hpp"
#include <M5Utility.hpp>
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::scd4x;
using namespace m5::unit::scd4x::command;
using namespace m5::unit::scd41;
using namespace m5::unit::scd41::command;
namespace {
// Max command duration(ms)
constexpr uint16_t MEASURE_SINGLE_SHOT_DURATION{5000};
constexpr uint16_t MEASURE_SINGLE_SHOT_RHT_ONLY_DURATION{50};
const uint8_t VARIANT_VALUE[2]{0x14, 0x40}; // SCD41
} // namespace
namespace m5 {
namespace unit {
// class UnitSCD41
const char UnitSCD41::name[] = "UnitSCD41";
const types::uid_t UnitSCD41::uid{"UnitSCD41"_mmh3};
const types::attr_t UnitSCD41::attr{attribute::AccessI2C};
bool UnitSCD41::is_valid_chip()
{
uint8_t var[2]{};
if (!read_register(GET_SENSOR_VARIANT, var, 2) || memcmp(var, VARIANT_VALUE, 2) != 0) {
M5_LIB_LOGE("Not SCD41 %02X:%02X", var[0], var[1]);
return false;
}
return true;
}
bool UnitSCD41::measureSingleshot(Data& d)
{
d = Data{};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(MEASURE_SINGLE_SHOT)) {
m5::utility::delay(MEASURE_SINGLE_SHOT_DURATION);
return read_measurement(d);
}
return false;
}
bool UnitSCD41::measureSingleshotRHT(Data& d)
{
d = Data{};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(MEASURE_SINGLE_SHOT_RHT_ONLY)) {
m5::utility::delay(MEASURE_SINGLE_SHOT_RHT_ONLY_DURATION);
return read_measurement(d, false);
}
return false;
}
bool UnitSCD41::powerDown(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(POWER_DOWN, nullptr, 0)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD41::wakeup()
{
constexpr uint32_t WAKE_UP_DURATION{30 + 5};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
// Note that the SCD4x does not acknowledge the wake_up command
writeRegister(WAKE_UP, nullptr, 0);
m5::utility::delay(WAKE_UP_DURATION);
// The sensors idle state after wake up can be verified by reading out the serial numbe
auto timeout_at = m5::utility::millis() + 1000;
do {
uint64_t sn{};
if (readSerialNumber(sn)) {
return true;
}
m5::utility::delay(10);
} while (m5::utility::millis() <= timeout_at);
return false;
}
bool UnitSCD41::writeAutomaticSelfCalibrationInitialPeriod(const uint16_t hours, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (hours % 4) {
M5_LIB_LOGW("Arguments are modified to multiples of 4");
}
uint16_t h = (hours >> 2) << 2;
m5::types::big_uint16_t u16{h};
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD, u16.data(), u16.size()) &&
delay_true(duration);
}
bool UnitSCD41::readAutomaticSelfCalibrationInitialPeriod(uint16_t& hours)
{
hours = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION)) {
hours = u16.get();
return true;
}
return false;
}
bool UnitSCD41::writeAutomaticSelfCalibrationStandardPeriod(const uint16_t hours, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (hours % 4) {
M5_LIB_LOGW("Arguments are modified to multiples of 4");
}
uint16_t h = (hours >> 2) << 2;
m5::types::big_uint16_t u16{h};
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD, u16.data(), u16.size()) &&
delay_true(duration);
}
bool UnitSCD41::readAutomaticSelfCalibrationStandardPeriod(uint16_t& hours)
{
hours = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION)) {
hours = u16.get();
return true;
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD41.hpp
@brief SCD41 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SCD41_HPP
#define M5_UNIT_ENV_UNIT_SCD41_HPP
#include "unit_SCD40.hpp"
namespace m5 {
namespace unit {
/*!
@namespace scd41
@brief For SCD41
*/
namespace scd41 {
///@cond
// Max command duration(ms)
// For SCD40/41
constexpr uint32_t POWER_DOWN_DURATION{1};
constexpr uint32_t GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION{1};
constexpr uint32_t SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION{1};
constexpr uint32_t GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION{1};
constexpr uint32_t SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION{1};
///@endcond
} // namespace scd41
/*!
@class m5::unit::UnitSCD41
@brief SCD41 unit component
*/
class UnitSCD41 : public UnitSCD40 {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSCD41, 0x62);
public:
explicit UnitSCD41(const uint8_t addr = DEFAULT_ADDRESS) : UnitSCD40(addr)
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSCD41()
{
}
///@name Single Shot Measurement Mode
///@{
/*!
@brief Request a single measurement
@param[out] d Measurement data
@return True if successful
@note Blocked until measurement results are acquired (5000 ms)
@warning During periodic detection runs, an error is returned
*/
bool measureSingleshot(scd4x::Data& d);
/*!
@brief Request a single measurement temperature and humidity
@param[out] d Measurement data
@return True if successful
@note Values are updated at 50 ms interval
@note Blocked until measurement results are acquired (50 ms)
@warning Information on CO2 is invalid.
@warning During periodic detection runs, an error is returned
*/
bool measureSingleshotRHT(scd4x::Data& d);
///@}
///@name Power mode
///@{
/*!
@brief Power down
@details The sensor into sleep mode
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool powerDown(const uint32_t duration = scd41::POWER_DOWN_DURATION);
/*!
@brief Wake up
@details The sensor from sleep mode into idle mode
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool wakeup();
///@}
///@name For ASC(Auto Self-Calibration)
///@{
/*!
@brief Write the duration of the initial period for ASC correction
@param hours ASC initial period
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Allowed values are integer multiples of 4 hours
*/
bool writeAutomaticSelfCalibrationInitialPeriod(
const uint16_t hours, const uint32_t duration = scd41::SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION);
/*!
@brief Read the duration of the initial period for ASC correction
@param[out] hours ASC initial period
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readAutomaticSelfCalibrationInitialPeriod(uint16_t& hours);
/*!
@brief Write the standard period for ASC correction
@param hours ASC standard period
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Allowed values are integer multiples of 4 hours
*/
bool writeAutomaticSelfCalibrationStandardPeriod(
const uint16_t hours, const uint32_t duration = scd41::SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION);
/*!
@brief Red the standard period for ASC correction
@param[iut] hours ASC standard period
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Allowed values are integer multiples of 4 hours
*/
bool readAutomaticSelfCalibrationStandardPeriod(uint16_t& hours);
///@}
protected:
virtual bool is_valid_chip() override;
};
namespace scd41 {
///@cond
namespace command {
// Low power single shot - SCD41 only
constexpr uint16_t MEASURE_SINGLE_SHOT{0x219d};
constexpr uint16_t MEASURE_SINGLE_SHOT_RHT_ONLY{0x2196};
constexpr uint16_t POWER_DOWN{0x36e0};
constexpr uint16_t WAKE_UP{0x36f6};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD{0x2445};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD{0x2340};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD{0x244e};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD{0x234b};
} // namespace command
///@endcond
} // namespace scd41
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,358 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SGP30.cpp
@brief SGP30 Unit for M5UnitUnified
*/
#include "unit_SGP30.hpp"
#include <M5Utility.hpp>
#include <array>
#include <cmath>
#include <limits>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::sgp30;
using namespace m5::unit::sgp30::command;
namespace {
// Supported lower limit version
constexpr uint8_t lower_limit_version{0x20};
// constexpr elapsed_time_t BASELINE_INTERVAL{1000 * 60 * 60}; // 1 hour (ms)
inline bool delayMeasurementDuration(const uint16_t ms)
{
m5::utility::delay(ms);
return true;
}
} // namespace
namespace m5 {
namespace unit {
namespace sgp30 {
uint16_t Data::co2eq() const
{
// M5_LIB_LOGE(">>> %x:%x => %u", raw[0], raw[1], m5::types::big_uint16_t(raw[0], raw[1]).get());
return m5::types::big_uint16_t(raw[0], raw[1]).get();
}
uint16_t Data::tvoc() const
{
return m5::types::big_uint16_t(raw[3], raw[4]).get();
}
} // namespace sgp30
// class UnitSGP30
const char UnitSGP30::name[] = "UnitSGP30";
const types::uid_t UnitSGP30::uid{"UnitSGP30"_mmh3};
const types::attr_t UnitSGP30::attr{attribute::AccessI2C};
bool UnitSGP30::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
m5::utility::delay(1);
Feature f{};
if (!readFeatureSet(f)) {
M5_LIB_LOGE("Failed to read feature");
return false;
}
if (f.productType() != 0) {
// May be SGPC3 gas sensor if value is 1
M5_LIB_LOGE("This unit is NOT SGP30");
return false;
}
_version = f.productVersion();
if (_version < lower_limit_version) {
M5_LIB_LOGE("Not enough the product version %x", _version);
return false;
}
return _cfg.start_periodic
? startPeriodicMeasurement(_cfg.baseline_co2eq, _cfg.baseline_tvoc, _cfg.humidity, _cfg.interval)
: true;
}
void UnitSGP30::update(const bool force)
{
_updated = false;
if (_periodic) {
elapsed_time_t at{m5::utility::millis()};
if (_waiting) {
_waiting = (at < _can_measure_time);
return;
}
if (force || !_latest || at >= _latest + _interval) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = at;
_data->push_back(d);
}
}
}
}
bool UnitSGP30::start_periodic_measurement(const uint16_t co2eq, const uint16_t tvoc, const uint16_t humidity,
const uint32_t interval, const uint32_t duration)
{
// Baseline and absolute humidity restoration must take place during
// this 15-second period
return start_periodic_measurement(interval, duration) && write_iaq_baseline(co2eq, tvoc) &&
writeAbsoluteHumidity(humidity);
}
bool UnitSGP30::start_periodic_measurement(const uint32_t interval, const uint32_t duration)
{
if (inPeriodic()) {
return false;
}
if (interval < sgp30::MEASURE_IAQ_DURATION) {
M5_LIB_LOGE("Interval too short %u. Must ne greater equal %u", interval, sgp30::MEASURE_IAQ_DURATION);
return false;
}
if (writeRegister(IAQ_INIT)) {
// For the first 15s after the “sgp30_iaq_init” command the sensor
// is an initialization phase during which a “sgp30_measure_iaq”
// command returns fixed values of 400 ppm CO2eq and 0 ppb TVOC.A
// new “sgp30_iaq_init” command has to be sent after every power-up
// or soft reset.
// Baseline and absolute humidity restoration must take place during
// this 15-second period
_can_measure_time = m5::utility::millis() + 15 * 1000;
_periodic = true;
_latest = 0;
_waiting = true;
_interval = interval;
m5::utility::delay(duration);
}
return _periodic;
}
bool UnitSGP30::stop_periodic_measurement()
{
_periodic = false;
return true;
}
bool UnitSGP30::readRaw(uint16_t& h2, uint16_t& ethanol)
{
std::array<uint8_t, 6> rbuf{};
h2 = ethanol = 0;
if (readRegister(MEASURE_RAW, rbuf.data(), rbuf.size(), MEASURE_RAW_DURATION)) {
m5::utility::CRC8_Checksum crc{};
if (crc.range(rbuf.data(), 2) == rbuf[2] && crc.range(rbuf.data() + 3, 2) == rbuf[5]) {
h2 = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
ethanol = m5::types::big_uint16_t(rbuf[3], rbuf[4]).get();
}
return true;
}
return false;
}
bool UnitSGP30::readRaw(float& h2, float& ethanol)
{
uint16_t hh{}, et{};
h2 = ethanol = std::numeric_limits<float>::quiet_NaN();
if (!readRaw(hh, et)) {
return false;
}
h2 = 0.5f * std::exp((13119 - hh) / 512.f);
ethanol = 0.4f * std::exp((18472 - et) / 512.f);
return true;
}
bool UnitSGP30::readIaqBaseline(uint16_t& co2eq, uint16_t& tvoc)
{
std::array<uint8_t, 6> rbuf{};
co2eq = tvoc = 0;
if (readRegister(GET_IAQ_BASELINE, rbuf.data(), rbuf.size(), GET_IAQ_BASELINE_DURATION)) {
m5::utility::CRC8_Checksum crc{};
if (crc.range(rbuf.data(), 2) == rbuf[2] && crc.range(rbuf.data() + 3, 2) == rbuf[5]) {
co2eq = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
tvoc = m5::types::big_uint16_t(rbuf[3], rbuf[4]).get();
return true;
}
}
return false;
}
bool UnitSGP30::writeAbsoluteHumidity(const uint16_t raw, const uint32_t duration)
{
m5::utility::CRC8_Checksum crc;
std::array<uint8_t, 3> buf{};
m5::types::big_uint16_t rr(raw);
std::memcpy(buf.data(), rr.data(), 2);
buf[2] = crc.range(rr.data(), 2);
return writeRegister(SET_ABSOLUTE_HUMIDITY, buf.data(), buf.size()) && delayMeasurementDuration(duration);
}
bool UnitSGP30::writeAbsoluteHumidity(const float gm3, const uint32_t duration)
{
int32_t tmp = static_cast<int32_t>(std::round(gm3 * 256.f));
if (tmp > 32767 || tmp < -32768) {
M5_LIB_LOGE("Over/underflow: %f / %d", gm3, tmp);
return false;
}
return writeAbsoluteHumidity(static_cast<uint16_t>(static_cast<int16_t>(tmp)), duration);
}
bool UnitSGP30::measureTest(uint16_t& result)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
std::array<uint8_t, 3> rbuf{};
if (readRegister(MEASURE_TEST, rbuf.data(), rbuf.size(), MEASURE_TEST_DURATION)) {
m5::utility::CRC8_Checksum crc;
if (crc.range(rbuf.data(), 2) == rbuf[2]) {
result = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
}
return false;
}
#if 0
bool UnitSGP30::readTvocInceptiveBaseline(uint16_t& inceptive_tvoc) {
if (_version < 0x21) {
M5_LIB_LOGE("Not enough the product version %x", _version);
return false;
}
std::array<uint8_t, 3> rbuf{};
if (readRegister(GET_TVOC_INCEPTIVE_BASELINE, rbuf.data(), rbuf.size(), GET_TVOC_INCEPTIVE_BASELINE_DURATION)) {
m5::utility::CRC8_Checksum crc;
if (crc.range(rbuf.data(), 2) == rbuf[2]) {
inceptive_tvoc = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
}
return false;
}
bool UnitSGP30::writeTvocInceptiveBaseline(const uint16_t inceptive_tvoc, const uint32_t duration) {
if (_version < 0x21) {
M5_LIB_LOGE("Not enough the product version %x", _version);
return false;
}
M5_LIB_LOGW(">>>>>> %u", inceptive_tvoc);
m5::utility::CRC8_Checksum crc;
std::array<uint8_t, 3> buf{};
m5::types::big_uint16_t tt(inceptive_tvoc);
std::memcpy(buf.data(), tt.data(), 2);
buf[2] = crc.range(tt.data(), 2);
return writeRegister(SET_TVOC_INCEPTIVE_BASELINE, buf.data(), buf.size()) && delayMeasurementDuration(duration);
}
#endif
bool UnitSGP30::generalReset()
{
uint8_t cmd{0x06};
if (generalCall(&cmd, 1)) {
_periodic = false;
m5::utility::delay(10);
return true;
}
return false;
}
bool UnitSGP30::readFeatureSet(sgp30::Feature& feature)
{
std::array<uint8_t, 3> rbuf{};
if (readRegister(GET_FEATURE_SET, rbuf.data(), rbuf.size(), GET_FEATURE_SET_DURATION)) {
m5::utility::CRC8_Checksum crc;
if (crc.range(rbuf.data(), 2) == rbuf[2]) {
feature.value = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
}
return false;
}
bool UnitSGP30::readSerialNumber(uint64_t& number)
{
std::array<uint8_t, 9> rbuf{};
number = 0;
if (readRegister(GET_SERIAL_ID, rbuf.data(), rbuf.size(), GET_SERIAL_ID_DURATION)) {
m5::utility::CRC8_Checksum crc;
for (uint_fast8_t i = 0; i < 3; i++) {
if (crc.range(rbuf.data() + i * 3, 2) != rbuf[i * 3 + 2]) {
return false;
}
}
for (uint_fast8_t i = 0; i < 3; ++i) {
number |= ((uint64_t)(m5::types::big_uint16_t(rbuf[i * 3], rbuf[i * 3 + 1]).get())) << (16U * (2 - i));
}
return true;
}
return false;
}
bool UnitSGP30::readSerialNumber(char* number)
{
if (!number) {
return false;
}
*number = '\0';
uint64_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{12};
while (i--) {
*number++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*number = '\0';
return true;
}
return false;
}
//
bool UnitSGP30::write_iaq_baseline(const uint16_t co2eq, const uint16_t tvoc)
{
m5::utility::CRC8_Checksum crc{};
m5::types::big_uint16_t cc(co2eq);
m5::types::big_uint16_t tt(tvoc);
std::array<uint8_t, (2 + 1) * 2> buf{};
// Note that the order is different for get and set
std::memcpy(buf.data() + 0, tt.data(), 2);
buf[2] = crc.range(tt.data(), 2);
std::memcpy(buf.data() + 3, cc.data(), 2);
buf[5] = crc.range(cc.data(), 2);
return writeRegister(SET_IAQ_BASELINE, buf.data(), buf.size());
}
bool UnitSGP30::read_measurement(Data& d)
{
if (readRegister(MEASURE_IAQ, d.raw.data(), d.raw.size(), MEASURE_IAQ_DURATION)) {
m5::utility::CRC8_Checksum crc{};
return crc.range(d.raw.data(), 2) == d.raw[2] && crc.range(d.raw.data() + 3, 2) == d.raw[5];
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,360 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SGP30.hpp
@brief SGP30 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_TVOC_UNIT_SGP30_HPP
#define M5_UNIT_TVOC_UNIT_SGP30_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <array>
namespace m5 {
namespace unit {
/*!
@namespace sgp30
@brief For SGP30
*/
namespace sgp30 {
///@cond
// Max command duration(ms)
// Max measurement duration (ms)
constexpr uint16_t IAQ_INIT_DURATION{10};
constexpr uint16_t MEASURE_IAQ_DURATION{12};
constexpr uint16_t GET_IAQ_BASELINE_DURATION{10};
constexpr uint16_t SET_IAQ_BASELINE_DURATION{10};
constexpr uint16_t SET_ABSOLUTE_HUMIDITY_DURATION{10};
constexpr uint16_t MEASURE_TEST_DURATION{220};
constexpr uint16_t GET_FEATURE_SET_DURATION{10};
constexpr uint16_t MEASURE_RAW_DURATION{25};
constexpr uint16_t GET_TVOC_INCEPTIVE_BASELINE_DURATION{10};
constexpr uint16_t SET_TVOC_INCEPTIVE_BASELINE_DURATION{10};
constexpr uint16_t GET_SERIAL_ID_DURATION{10};
///@endcond
/*!
@struct Feature
@brief Structure of the SGP feature set number
*/
struct Feature {
//! @brief product type (SGP30: 0)
uint8_t productType() const
{
return (value >> 12) & 0x0F;
}
/*!
@brief product version
@note Please note that the last 5 bits of the productversion (bits 12-16
of the LSB) are subject to change
@note This is used to track new features added tothe SGP multi-pixel
platform
*/
uint8_t productVersion() const
{
return value & 0xFF;
}
uint16_t value{};
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
uint16_t co2eq() const; //!< Co2Eq (ppm)
uint16_t tvoc() const; //!< TVOC (pbb)
};
} // namespace sgp30
/*!
@class UnitSGP30
@brief SGP30 unit
*/
class UnitSGP30 : public Component, public PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSGP30, 0x58);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin()?
bool start_periodic{true};
//! Baseline CO2eq initial value if start on begin
uint16_t baseline_co2eq{};
//! Baseline TVOC initial value if start on begin
uint16_t baseline_tvoc{};
//! Absolute humidity initiali value if start on begin
uint16_t humidity{};
/*!
Inceptive Baseline for TVOC measurements initial value if start on begin
@warning The application of this feature is solely limited to the very
first start-up period of an SGP sensor
*/
uint16_t inceptive_tvoc{};
//! Periodic measurement interval if start on begin
int32_t interval{1000};
};
explicit UnitSGP30(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<sgp30::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSGP30()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config() const
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Properties
///@{
/*!
@brief Gets the product version
@warning Calling after the call of begin()
*/
inline uint8_t productVersion() const
{
return _version;
}
/*!
@brief Can it be measured?
@return True if it can
@warning After the start of a periodic measurement, it is necessary to wait 15 seconds to obtain a valid
measurement value
*/
inline bool canMeasurePeriodic() const
{
return inPeriodic() && !_waiting;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest CO2eq (ppm)
inline uint16_t co2eq() const
{
return !empty() ? oldest().co2eq() : 0xFFFF;
}
//! @brief Oldest TVOC (ppb)
inline uint16_t tvoc() const
{
return !empty() ? oldest().tvoc() : 0xFFFF;
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@details Specify settings and measure
@param co2eq iaq baseline for CO2eq
@param tvoc iaq baseline for TVOC
@param humidity absolute humidity (disable if zero)
@param interval Measurement Interval(ms)
@param duration Max command duration(ms)
@return True if successful
@note 15 seconds wait is required before a valid measurement can be initiated (warmup)
@note In update(), waiting are taken into account
*/
inline bool startPeriodicMeasurement(const uint16_t co2eq, const uint16_t tvoc, const uint16_t humidity,
const uint32_t interval = 1000U,
const uint32_t duration = sgp30::IAQ_INIT_DURATION)
{
return PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data>::startPeriodicMeasurement(co2eq, tvoc, humidity,
interval, duration);
}
/*!
@brief Start periodic measurement
@details Measuring in the current settings
@param interval Measurement Interval(ms)
@param duration Max command duration(ms)
@return True if successful
@note 15 seconds wait is required before a valid measurement can be
initiated.
@note In update(), waiting are taken into account
*/
inline bool startPeriodicMeasurement(const uint32_t interval = 1000U,
const uint32_t duration = sgp30::IAQ_INIT_DURATION)
{
return PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data>::startPeriodicMeasurement(interval, duration);
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data>::stopPeriodicMeasurement();
}
///@}
///@name Correction
///@{
/*!
@brief Read the IAQ baseline
@param[out] co2eq iaq baseline for CO2
@param[out] tvoc iaq baseline for TVOC
@return True if successful
*/
bool readIaqBaseline(uint16_t& co2eq, uint16_t& tvoc);
/*!
@brief Write the absolute humidity
@param raw absolute humidity (disable if zero)
@param duration Max command duration(ms)
@return True if successful
@note Value is a fixed-point 8.8bit number
*/
bool writeAbsoluteHumidity(const uint16_t raw, const uint32_t duration = sgp30::SET_ABSOLUTE_HUMIDITY_DURATION);
/*!
@brief Write the absolute humidity
@param gm3 absolute humidity (g/m^3)
@param duration Max command duration(ms)
@return True if successful
*/
bool writeAbsoluteHumidity(const float gm3, const uint32_t duration = sgp30::SET_ABSOLUTE_HUMIDITY_DURATION);
#if 0
/*!
@brief Read the inceptive Basebine for TVOC
@param[out] inceptive_tvoc Inceptive baseline
@return True if successful
@warning Only available if product version is 0x21 or higher
*/
bool readTvocInceptiveBaseline(uint16_t& inceptive_tvoc);
/*!
@brief Write the inceptive Basebine for TVOC
@param inceptive_tvoc Inceptive baseline
@param duration Max command duration(ms)
@return True if successful
@warning The application of this feature is solely limited to the very
first start-up period of an SGP sensor
@warning Only available if product version is 0x21 or higher
*/
bool writeTvocInceptiveBaseline(const uint16_t inceptive_tvoc,
const uint32_t duration = sgp30::SET_TVOC_INCEPTIVE_BASELINE_DURATION);
#endif
///@}
/*!
@brief Read sensor raw signals
@param[out] h2 H2 concentration(raw)
@param[out] ethanol Ethanol concentration(raw)
@return True if successful
*/
bool readRaw(uint16_t& h2, uint16_t& ethanol);
/*!
@brief Read H2/Ethanol concentration
@param[out] h2 H2 concentration(ppm)
@param[out] ethanol Ethanol concentration(rppm
@return True if successful
@note Outputs the value calculated from the output of readRaw
*/
bool readRaw(float& h2, float& ethanol);
/*!
@brief Run the on-chip self-test
@param[out] result self-test return code
@return True if successful
@note If the test is OK, the result will be 0xd400
@warning Must not be executed after startPeriodicMeasurement
*/
bool measureTest(uint16_t& result);
/*!
@brief General reset
@details Reset using I2C general call
@warning This is a reset by General command, the command is also
sent to all devices with I2C connections
@return True if successful
*/
bool generalReset();
/*!
@brief Read the feature set
@param feature
@return True if successful
*/
bool readFeatureSet(sgp30::Feature& feature);
/*!
@brief Read the serial number
@return True if successful
@note Serial number is 48bits
*/
bool readSerialNumber(uint64_t& number);
/*!
@brief Read the serial number string
@param[out] number Output buffer
@return True if successful
@warning number must be at least 13 bytes
*/
bool readSerialNumber(char* number);
protected:
bool start_periodic_measurement(const uint16_t co2eq, const uint16_t tvoc, const uint16_t humidity,
const uint32_t interval, const uint32_t duration = sgp30::IAQ_INIT_DURATION);
bool start_periodic_measurement(const uint32_t interval, const uint32_t duration = sgp30::IAQ_INIT_DURATION);
bool stop_periodic_measurement();
bool write_iaq_baseline(const uint16_t co2eq, const uint16_t tvoc);
bool read_measurement(sgp30::Data& d);
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSGP30, sgp30::Data);
protected:
uint8_t _version{}; // Chip version
bool _waiting{};
types::elapsed_time_t _can_measure_time{};
std::unique_ptr<m5::container::CircularBuffer<sgp30::Data>> _data{};
config_t _cfg{};
};
namespace sgp30 {
namespace command {
///@cond
constexpr uint16_t IAQ_INIT{0x2003};
constexpr uint16_t MEASURE_IAQ{0x2008};
constexpr uint16_t GET_IAQ_BASELINE{0x2015};
constexpr uint16_t SET_IAQ_BASELINE{0x201E};
constexpr uint16_t SET_ABSOLUTE_HUMIDITY{0x2061};
constexpr uint16_t MEASURE_TEST{0x2032};
constexpr uint16_t GET_FEATURE_SET{0x202F};
constexpr uint16_t MEASURE_RAW{0x2050};
constexpr uint16_t GET_TVOC_INCEPTIVE_BASELINE{0x20B3};
constexpr uint16_t SET_TVOC_INCEPTIVE_BASELINE{0x2077};
constexpr uint16_t GET_SERIAL_ID{0x3682};
///@endcond
} // namespace command
} // namespace sgp30
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,350 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT30.cpp
@brief SHT30 Unit for M5UnitUnified
*/
#include "unit_SHT30.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::sht30;
using namespace m5::unit::sht30::command;
namespace {
struct Temperature {
constexpr static float toFloat(const uint16_t u16)
{
return -45 + u16 * 175 / 65535.f; // -45 + 175 * S / (2^16 - 1)
}
};
// After sending a command to the sensor a minimalwaiting time of 1ms is needed
// before another commandcan be received by the sensor.
bool delay1()
{
m5::utility::delay(1);
return true;
}
constexpr uint16_t periodic_cmd[] = {
// 0.5 mps
START_PERIODIC_MPS_HALF_HIGH,
START_PERIODIC_MPS_HALF_MEDIUM,
START_PERIODIC_MPS_HALF_LOW,
// 1 mps
START_PERIODIC_MPS_1_HIGH,
START_PERIODIC_MPS_1_MEDIUM,
START_PERIODIC_MPS_1_LOW,
// 2 mps
START_PERIODIC_MPS_2_HIGH,
START_PERIODIC_MPS_2_MEDIUM,
START_PERIODIC_MPS_2_LOW,
// 4 mps
START_PERIODIC_MPS_4_HIGH,
START_PERIODIC_MPS_4_MEDIUM,
START_PERIODIC_MPS_4_LOW,
// 10 mps
START_PERIODIC_MPS_10_HIGH,
START_PERIODIC_MPS_10_MEDIUM,
START_PERIODIC_MPS_10_LOW,
};
constexpr elapsed_time_t interval_table[] = {
2000, // 0.5
1000, // 1
500, // 2
250, // 4
100, // 10
};
} // namespace
namespace m5 {
namespace unit {
namespace sht30 {
float Data::celsius() const
{
return Temperature::toFloat(m5::types::big_uint16_t(raw[0], raw[1]).get());
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::humidity() const
{
return 100.f * m5::types::big_uint16_t(raw[3], raw[4]).get() / 65536.f;
}
} // namespace sht30
const char UnitSHT30::name[] = "UnitSHT30";
const types::uid_t UnitSHT30::uid{"UnitSHT30"_mmh3};
const types::attr_t UnitSHT30::attr{attribute::AccessI2C};
bool UnitSHT30::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
if (!stopPeriodicMeasurement()) {
M5_LIB_LOGE("Failed to stop");
return false;
}
if (!softReset()) {
M5_LIB_LOGE("Failed to reset");
return false;
}
uint32_t sn{};
if (!readSerialNumber(sn)) {
M5_LIB_LOGE("Failed to readSerialNumber %x", sn);
return false;
}
auto r = _cfg.start_heater ? startHeater() : stopHeater();
if (!r) {
M5_LIB_LOGE("Failed to heater %d", _cfg.start_heater);
return false;
}
return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.mps, _cfg.repeatability) : true;
}
void UnitSHT30::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
if (writeRegister(READ_MEASUREMENT)) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = at;
_data->push_back(d);
}
}
}
}
}
bool UnitSHT30::measureSingleshot(Data& d, const sht30::Repeatability rep, const bool stretch)
{
constexpr uint16_t cmd[] = {
// Enable clock stretching
SINGLE_SHOT_ENABLE_STRETCH_HIGH,
SINGLE_SHOT_ENABLE_STRETCH_MEDIUM,
SINGLE_SHOT_ENABLE_STRETCH_LOW,
// Disable clock stretching
SINGLE_SHOT_DISABLE_STRETCH_HIGH,
SINGLE_SHOT_DISABLE_STRETCH_MEDIUM,
SINGLE_SHOT_DISABLE_STRETCH_LOW,
};
// Latency when clock stretching is disabled
constexpr elapsed_time_t ms[] = {
15,
6,
4,
};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
uint32_t idx = m5::stl::to_underlying(rep) + (stretch ? 0 : 3);
if (idx >= m5::stl::size(cmd)) {
M5_LIB_LOGE("Invalid arg : %u", (int)rep);
return false;
}
if (writeRegister(cmd[idx])) {
m5::utility::delay(stretch ? 1 : ms[m5::stl::to_underlying(rep)]);
return read_measurement(d);
}
return false;
}
bool UnitSHT30::start_periodic_measurement(const sht30::MPS mps, const sht30::Repeatability rep)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
_periodic = writeRegister(periodic_cmd[m5::stl::to_underlying(mps) * 3 + m5::stl::to_underlying(rep)]);
if (_periodic) {
_interval = interval_table[m5::stl::to_underlying(mps)];
m5::utility::delay(16);
return true;
}
return _periodic;
}
bool UnitSHT30::stop_periodic_measurement()
{
if (writeRegister(STOP_PERIODIC_MEASUREMENT)) {
_periodic = false;
_latest = 0;
// Upon reception of the break command the sensor will abort the
// ongoing measurement and enter the single shot mode. This takes
// 1ms
return delay1();
}
return false;
}
bool UnitSHT30::writeModeAccelerateResponseTime()
{
if (!inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are NOT running");
return false;
}
if (writeRegister(ACCELERATED_RESPONSE_TIME)) {
_interval = 1000 / 4; // 4mps
m5::utility::delay(16);
return true;
}
return false;
}
bool UnitSHT30::readStatus(sht30::Status& s)
{
std::array<uint8_t, 3> rbuf{};
if (readRegister(READ_STATUS, rbuf.data(), rbuf.size(), 0) &&
m5::utility::CRC8_Checksum().range(rbuf.data(), 2) == rbuf[2]) {
s.value = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
return false;
}
bool UnitSHT30::clearStatus()
{
return writeRegister(CLEAR_STATUS) && delay1();
}
bool UnitSHT30::softReset()
{
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
if (writeRegister(SOFT_RESET)) {
// Max 1.5 ms
// Time between ACK of soft reset command and sensor entering idle
// state
m5::utility::delay(2);
return true;
}
return false;
}
bool UnitSHT30::generalReset()
{
uint8_t cmd{0x06}; // reset command
if (!clearStatus()) {
return false;
}
// Reset does not return ACK, which is an error, but should be ignored
generalCall(&cmd, 1);
m5::utility::delay(1);
auto timeout_at = m5::utility::millis() + 10;
bool done{};
do {
Status s{};
// The ALERT pin will also become active (high) after powerup and
// after resets
if (readStatus(s) && (s.reset() || s.alertPending())) {
done = true;
break;
}
m5::utility::delay(1);
} while (!done && m5::utility::millis() <= timeout_at);
return done;
}
bool UnitSHT30::startHeater()
{
return writeRegister(START_HEATER) && delay1();
}
bool UnitSHT30::stopHeater()
{
return writeRegister(STOP_HEATER) && delay1();
}
bool UnitSHT30::readSerialNumber(uint32_t& serialNumber)
{
serialNumber = 0;
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
std::array<uint8_t, 6> rbuf;
if (readRegister(GET_SERIAL_NUMBER_ENABLE_STRETCH, rbuf.data(), rbuf.size(), 0)) {
m5::types::big_uint16_t u16[2]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}};
m5::utility::CRC8_Checksum crc{};
if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5]) {
serialNumber = ((uint32_t)u16[0].get()) << 16 | ((uint32_t)u16[1].get());
return true;
}
}
return false;
}
bool UnitSHT30::readSerialNumber(char* serialNumber)
{
if (!serialNumber) {
return false;
}
*serialNumber = '\0';
uint32_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{8};
while (i--) {
*serialNumber++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*serialNumber = '\0';
return true;
}
return false;
}
bool UnitSHT30::read_measurement(Data& d)
{
if (readWithTransaction(d.raw.data(), d.raw.size()) == m5::hal::error::error_t::OK) {
m5::utility::CRC8_Checksum crc{};
for (uint_fast8_t i = 0; i < 2; ++i) {
if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) {
return false;
}
}
return true;
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,360 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT30.hpp
@brief SHT30 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SHT30_HPP
#define M5_UNIT_ENV_UNIT_SHT30_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
namespace sht30 {
/*!
@enum Repeatability
@brief Repeatability accuracy level
*/
enum class Repeatability : uint8_t {
High, //!< @brief High repeatability
Medium, //!< @brief Medium repeatability
Low //!< @brief Low repeatability
};
/*!
@enum MPS
@brief Measuring frequency
*/
enum class MPS : uint8_t {
Half, //!< @brief 0.5 measurement per second
One, //!< @brief 1 measurement per second
Two, //!< @brief 2 measurement per second
Four, //!< @brief 4 measurement per second
Ten, //!< @brief 10 measurement per second
};
/*!
@struct Status
@brief Accessor for Status
@note The order of the bit fields cannot be controlled, so bitwise
operations are used to obtain each value.
@note Items marked with (*) are subjects to clear status
*/
struct Status {
//! @brief Alert pending status (*)
inline bool alertPending() const
{
return value & (1U << 15);
}
//! @brief Heater status
inline bool heater() const
{
return value & (1U << 13);
}
//! @brief RH tracking alert (*)
inline bool trackingAlertRH() const
{
return value & (1U << 11);
}
//! @brief Tracking alert (*)
inline bool trackingAlert() const
{
return value & (1U << 10);
}
//! @brief System reset detected (*)
inline bool reset() const
{
return value & (1U << 4);
}
//! @brief Command staus
inline bool command() const
{
return value & (1U << 1);
}
//! @brief Write data checksum status
inline bool checksum() const
{
return value & (1U << 0);
}
uint16_t value{};
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float humidity() const; //!< humidity (RH)
};
} // namespace sht30
/*!
@class UnitSHT30
@brief Temperature and humidity, sensor unit
*/
class UnitSHT30 : public Component, public PeriodicMeasurementAdapter<UnitSHT30, sht30::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSHT30, 0x44);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Measuring frequency if start on begin
sht30::MPS mps{sht30::MPS::One};
//! Repeatability accuracy level if start on begin
sht30::Repeatability repeatability{sht30::Repeatability::High};
//! start heater on begin?
bool start_heater{false};
};
explicit UnitSHT30(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<sht30::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSHT30()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured humidity (RH)
inline float humidity() const
{
return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param mps Measuring frequency
@param rep Repeatability accuracy level
@return True if successful
*/
inline bool startPeriodicMeasurement(const sht30::MPS mps, const sht30::Repeatability rep)
{
return PeriodicMeasurementAdapter<UnitSHT30, sht30::Data>::startPeriodicMeasurement(mps, rep);
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitSHT30, sht30::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param rep Repeatability accuracy level
@param stretch Enable clock stretching if true
@return True if successful
@warning During periodic detection runs, an error is returned
@warning After sending a command to the sensor a minimal waiting time of 1ms is needed before another command can
be received by the sensor
*/
bool measureSingleshot(sht30::Data& d, const sht30::Repeatability rep = sht30::Repeatability::High,
const bool stretch = true);
///@}
/*!
@brief Write the mode to ART
@details After issuing the ART command the sensor will start acquiring data with a frequency of 4Hz
@return True if successful
@warning Only available during periodic measurements
*/
bool writeModeAccelerateResponseTime();
///@name Reset
///@{
/*!
@brief Soft reset
@details The sensor to reset its system controller and reloads calibration
data from the memory.
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool softReset();
/*!
@brief General reset
@details Reset using I2C general call
@return True if successful
@warning This is a reset by General command, the command is also sent to all devices with I2C connections
*/
bool generalReset();
///@}
///@name Heater
///@{
/*!
@brief Start heater
@return True if successful
*/
bool startHeater();
/*!
@brief Stop heater
@return True if successful
*/
bool stopHeater();
///@}
///@name Status
///@{
/*!
@brief Read status
@param[out] s Status
@return True if successful
*/
bool readStatus(sht30::Status& s);
/*!
@brief Clear status
@sa sht30::Status
@return True if successful
*/
bool clearStatus();
///@}
///@name Serial number
///@{
/*!
@brief Read the serial number value
@param[out] serialNumber serial number value
@return True if successful
@note The serial number is 32 bit
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(uint32_t& serialNumber);
/*!
@brief Read the serial number string
@param[out] serialNumber Output buffer
@return True if successful
@warning serialNumber must be at least 9 bytes
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(char* serialNumber);
///@}
protected:
bool start_periodic_measurement(const sht30::MPS mps, const sht30::Repeatability rep);
bool stop_periodic_measurement();
bool read_measurement(sht30::Data& d);
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSHT30, sht30::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<sht30::Data>> _data{};
config_t _cfg{};
};
///@cond
namespace sht30 {
namespace command {
// Measurement Commands for Single Shot Data Acquisition Mode
constexpr uint16_t SINGLE_SHOT_ENABLE_STRETCH_HIGH{0x2C06};
constexpr uint16_t SINGLE_SHOT_ENABLE_STRETCH_MEDIUM{0x2C0D};
constexpr uint16_t SINGLE_SHOT_ENABLE_STRETCH_LOW{0x2C10};
constexpr uint16_t SINGLE_SHOT_DISABLE_STRETCH_HIGH{0x2400};
constexpr uint16_t SINGLE_SHOT_DISABLE_STRETCH_MEDIUM{0x240B};
constexpr uint16_t SINGLE_SHOT_DISABLE_STRETCH_LOW{0x2416};
// Measurement Commands for Periodic Data Acquisition Mode
constexpr uint16_t START_PERIODIC_MPS_HALF_HIGH{0x2032};
constexpr uint16_t START_PERIODIC_MPS_HALF_MEDIUM{0x2024};
constexpr uint16_t START_PERIODIC_MPS_HALF_LOW{0x202f};
constexpr uint16_t START_PERIODIC_MPS_1_HIGH{0x2130};
constexpr uint16_t START_PERIODIC_MPS_1_MEDIUM{0x2126};
constexpr uint16_t START_PERIODIC_MPS_1_LOW{0x212D};
constexpr uint16_t START_PERIODIC_MPS_2_HIGH{0x2236};
constexpr uint16_t START_PERIODIC_MPS_2_MEDIUM{0x2220};
constexpr uint16_t START_PERIODIC_MPS_2_LOW{0x222B};
constexpr uint16_t START_PERIODIC_MPS_4_HIGH{0x2334};
constexpr uint16_t START_PERIODIC_MPS_4_MEDIUM{0x2322};
constexpr uint16_t START_PERIODIC_MPS_4_LOW{0x2329};
constexpr uint16_t START_PERIODIC_MPS_10_HIGH{0x2737};
constexpr uint16_t START_PERIODIC_MPS_10_MEDIUM{0x2721};
constexpr uint16_t START_PERIODIC_MPS_10_LOW{0x272A};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT{0x3093};
constexpr uint16_t ACCELERATED_RESPONSE_TIME{0x2B32};
constexpr uint16_t READ_MEASUREMENT{0xE000};
// Reset
constexpr uint16_t SOFT_RESET{0x30A2};
// Heater
constexpr uint16_t START_HEATER{0x306D};
constexpr uint16_t STOP_HEATER{0x3066};
// Status
constexpr uint16_t READ_STATUS{0xF32D};
constexpr uint16_t CLEAR_STATUS{0x3041};
// Serial
constexpr uint16_t GET_SERIAL_NUMBER_ENABLE_STRETCH{0x3780};
constexpr uint16_t GET_SERIAL_NUMBER_DISABLE_STRETCH{0x3682};
} // namespace command
} // namespace sht30
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT40.cpp
@brief SHT40 Unit for M5UnitUnified
*/
#include "unit_SHT40.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::sht40;
using namespace m5::unit::sht40::command;
namespace {
constexpr uint8_t periodic_cmd[] = {
// HIGH
MEASURE_HIGH_HEATER_1S,
MEASURE_HIGH_HEATER_100MS,
MEASURE_HIGH,
// MEDIUM
MEASURE_MEDIUM_HEATER_1S,
MEASURE_MEDIUM_HEATER_100MS,
MEASURE_MEDIUM,
// LOW
MEASURE_LOW_HEATER_1S,
MEASURE_LOW_HEATER_100MS,
MEASURE_LOW,
};
constexpr elapsed_time_t interval_table[] = {
// HIGH
1100, 110,
9, // 8.2
// MEDIUM
1100, 110,
5, // 4.5
// LOW
1100, 110,
2, // 1.7
};
constexpr float MAX_HEATER_DUTY{0.05f};
} // namespace
namespace m5 {
namespace unit {
namespace sht40 {
float Data::celsius() const
{
return -45 + 175 * m5::types::big_uint16_t(raw[0], raw[1]).get() / 65535.f;
}
float Data::fahrenheit() const
{
return -49 + 315 * m5::types::big_uint16_t(raw[0], raw[1]).get() / 65535.f;
// return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::humidity() const
{
return -6 + 125 * m5::types::big_uint16_t(raw[3], raw[4]).get() / 65535.f;
}
} // namespace sht40
const char UnitSHT40::name[] = "UnitSHT40";
const types::uid_t UnitSHT40::uid{"UnitSHT40"_mmh3};
const types::attr_t UnitSHT40::attr{attribute::AccessI2C};
bool UnitSHT40::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
if (!softReset()) {
M5_LIB_LOGE("Failed to reset");
return false;
}
uint32_t sn{};
if (!readSerialNumber(sn)) {
M5_LIB_LOGE("Failed to readSerialNumber %x", sn);
return false;
}
return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.precision, _cfg.heater, _cfg.heater_duty) : true;
}
void UnitSHT40::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = at;
d.heater = (_interval != _duration_heater);
_data->push_back(d);
uint8_t cmd{};
if (at >= _latest_heater + _interval_heater) {
cmd = _cmd;
_latest_heater = at;
_interval = _duration_heater;
} else {
cmd = _measureCmd;
_interval = _duration_measure;
}
if (!writeRegister(cmd)) {
M5_LIB_LOGE("Failed to write, stop periodic measurement");
_periodic = false;
}
}
}
}
}
bool UnitSHT40::start_periodic_measurement(const sht40::Precision precision, const sht40::Heater heater,
const float duty)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (duty <= 0.0f || duty > MAX_HEATER_DUTY) {
M5_LIB_LOGW("duty range is invalid %f. duty (0.0, 0.05]");
return false;
}
_cmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
_measureCmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(Heater::None)];
_periodic = writeRegister(_cmd);
if (_periodic) {
_duration_heater = interval_table[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
_duration_measure =
interval_table[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(Heater::None)];
_interval_heater = _duration_heater / duty;
_interval = _duration_heater;
_latest_heater = m5::utility::millis();
m5::utility::delay(_interval); // For first read_measurement in update
return true;
}
return _periodic;
}
bool UnitSHT40::stop_periodic_measurement()
{
if (inPeriodic()) {
// Dismissal of data to be read
Data discard{};
int64_t wait = (int64_t)(_latest + _interval) - (int64_t)m5::utility::millis();
if (wait > 0) {
m5::utility::delay(wait);
read_measurement(discard);
}
_periodic = false;
return true;
}
return false;
}
bool UnitSHT40::measureSingleshot(sht40::Data& d, const sht40::Precision precision, const sht40::Heater heater)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
uint8_t cmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
auto ms = interval_table[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
if (writeRegister(cmd)) {
m5::utility::delay(ms);
if (read_measurement(d)) {
d.heater = (heater != Heater::None);
return true;
}
}
return false;
}
bool UnitSHT40::read_measurement(sht40::Data& d)
{
if (readWithTransaction(d.raw.data(), d.raw.size()) == m5::hal::error::error_t::OK) {
m5::utility::CRC8_Checksum crc{};
for (uint_fast8_t i = 0; i < 2; ++i) {
if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) {
return false;
}
}
return true;
}
return false;
}
bool UnitSHT40::softReset()
{
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
return soft_reset();
}
bool UnitSHT40::soft_reset()
{
if (writeRegister(SOFT_RESET)) {
// Max 1 ms
// Time between ACK of soft reset command and sensor entering idle state
m5::utility::delay(1);
reset_status();
return true;
}
return false;
}
bool UnitSHT40::generalReset()
{
uint8_t cmd{0x06}; // reset command
// Reset does not return ACK, which is an error, but should be ignored
generalCall(&cmd, 1);
m5::utility::delay(1);
reset_status();
return true;
}
bool UnitSHT40::readSerialNumber(uint32_t& serialNumber)
{
serialNumber = 0;
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
std::array<uint8_t, 6> rbuf;
if (readRegister(GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), 1)) {
m5::types::big_uint16_t u16[2]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}};
m5::utility::CRC8_Checksum crc{};
if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5]) {
serialNumber = ((uint32_t)u16[0].get()) << 16 | ((uint32_t)u16[1].get());
return true;
}
}
return false;
}
bool UnitSHT40::readSerialNumber(char* serialNumber)
{
if (!serialNumber) {
return false;
}
*serialNumber = '\0';
uint32_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{8};
while (i--) {
*serialNumber++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*serialNumber = '\0';
return true;
}
return false;
}
void UnitSHT40::reset_status()
{
_interval = _latest = _interval_heater = _latest_heater = 0;
_duration_measure = _duration_heater = 0;
_cmd = _measureCmd = 0;
_periodic = false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,260 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT40.hpp
@brief SHT40 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SHT40_HPP
#define M5_UNIT_ENV_UNIT_SHT40_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
/*!
@namespace sht40
@brief For SHT40
*/
namespace sht40 {
/*!
@enum Precision
@brief precision level
*/
enum class Precision : uint8_t {
High, //!< High precision (high repeatability)
Medium, //!< Medium precision (medium repeatability)
Low //!< Lowest precision (low repeatability)
};
/*!
@enum Heater
@brief Heater behavior
*/
enum class Heater : uint8_t {
Long, //!< Activate heater for 1s
Short, //!< Activate heater for 0.1s
None //!< Not activate heater
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
bool heater{}; //!< Measured data after heater is activated if true
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float humidity() const; //!< humidity (RH)
};
} // namespace sht40
/*!
@class UnitSHT40
@brief Temperature and humidity, sensor unit
*/
class UnitSHT40 : public Component, public PeriodicMeasurementAdapter<UnitSHT40, sht40::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSHT40, 0x44);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Precision level if start on begin
sht40::Precision precision{sht40::Precision::High};
//! Heater behavior if start on begin
sht40::Heater heater{sht40::Heater::None};
//! Heater duty cycle if start on begin [~ 0.05f]
float heater_duty{0.05f};
};
explicit UnitSHT40(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<sht40::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSHT40()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured humidity (RH)
inline float humidity() const
{
return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param precision Sensor precision
@param heater Heater behavior
@param duty Duty for activate heater
@return True if successful
@note If the heater is Long or SHort, the heater will be active periodically within the specified duty
@warning Datasheet says "keepingin mind that the heater is designed for a maximal duty cycle of less than 5%"
*/
inline bool startPeriodicMeasurement(const sht40::Precision precision, const sht40::Heater heater,
const float duty = 0.05f)
{
return PeriodicMeasurementAdapter<UnitSHT40, sht40::Data>::startPeriodicMeasurement(precision, heater, duty);
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitSHT40, sht40::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param precision Sensor precision
@param heater Heater behavior
@return True if successful
@note Blocking until the process is complete
@warning During periodic detection runs, an error is returned
@warning If heater is activated, the accuracy of the returned value is not guaranteed
@sa UnitSHT40::startPeriodicMeasurement
*/
bool measureSingleshot(sht40::Data& d, const sht40::Precision precision = sht40::Precision::High,
const sht40::Heater heater = sht40::Heater::None);
///@}
///@name Reset
///@{
/*!
@brief Soft reset
@return True if successful
*/
bool softReset();
/*!
@brief General reset
@details Reset using I2C general call
@return True if successful
@warning This is a reset by General command, the command is also sent to all devices with I2C connections
*/
bool generalReset();
///@}
///@name Serial number
///@{
/*!
@brief Read the serial number value
@param[out] serialNumber serial number value
@return True if successful
@note The serial number is 32 bit
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(uint32_t& serialNumber);
/*!
@brief Read the serial number string
@param[out] serialNumber Output buffer
@return True if successful
@warning serialNumber must be at least 9 bytes
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(char* serialNumber);
///@}
protected:
bool start_periodic_measurement(const sht40::Precision precision, const sht40::Heater heater, const float duty);
bool stop_periodic_measurement();
bool read_measurement(sht40::Data& d);
void reset_status();
bool soft_reset();
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSHT40, sht40::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<sht40::Data>> _data{};
config_t _cfg{};
uint8_t _cmd{}, _measureCmd{};
types::elapsed_time_t _latest_heater{}, _interval_heater{};
uint32_t _duration_measure{}, _duration_heater{};
};
///@cond
namespace sht40 {
namespace command {
constexpr uint8_t MEASURE_HIGH_HEATER_1S{0x39};
constexpr uint8_t MEASURE_HIGH_HEATER_100MS{0x32};
constexpr uint8_t MEASURE_HIGH{0xFD};
constexpr uint8_t MEASURE_MEDIUM_HEATER_1S{0x2F};
constexpr uint8_t MEASURE_MEDIUM_HEATER_100MS{0x24};
constexpr uint8_t MEASURE_MEDIUM{0xF6};
constexpr uint8_t MEASURE_LOW_HEATER_1S{0x1E};
constexpr uint8_t MEASURE_LOW_HEATER_100MS{0x15};
constexpr uint8_t MEASURE_LOW{0xE0};
constexpr uint8_t GET_SERIAL_NUMBER{0x89};
constexpr uint8_t SOFT_RESET{0x94};
} // namespace command
} // namespace sht40
///@endcond
} // namespace unit
} // namespace m5
#endif