diff --git a/code/espurna/config/sensors.h b/code/espurna/config/sensors.h index 396d68cc..a4a6a5be 100644 --- a/code/espurna/config/sensors.h +++ b/code/espurna/config/sensors.h @@ -148,6 +148,22 @@ #define BH1750_ADDRESS 0x00 // 0x00 means auto #endif +#ifndef BH1750_ACCURACY +#define BH1750_ACCURACY 1.2 // RAW value conversion ratio + // Allowed values are 0.96...1.44 +#endif + +#ifndef BH1750_SENSITIVITY +#define BH1750_SENSITIVITY 1.0 // Measurement sensitivity; value is derived from 'MTreg CURRENT' + // `SENSITIVITY = MTreg CURRENT / MTreg DEFAULT` (up to 2 decimal places) + // e.g. for MTreg allowed values of 31...254 + // * 31 -> 0.45 (min) + // * 69 -> 1.0 + // * 138 -> 2.0 + // * 207 -> 3.0 + // * 254 -> 3.68 (max) +#endif + #ifndef BH1750_MODE #define BH1750_MODE BH1750_CONTINUOUS_HIGH_RES_MODE #endif diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp index 93bf7668..1852d4c4 100644 --- a/code/espurna/sensor.cpp +++ b/code/espurna/sensor.cpp @@ -2043,6 +2043,8 @@ void load() { { auto* sensor = new BH1750Sensor(); sensor->setAddress(BH1750_ADDRESS); + sensor->setAccuracy(BH1750_ACCURACY); + sensor->setSensitivity(BH1750_SENSITIVITY); sensor->setMode(BH1750_MODE); add(sensor); } diff --git a/code/espurna/sensors/BH1750Sensor.h b/code/espurna/sensors/BH1750Sensor.h index d6eb64e4..2018abc9 100644 --- a/code/espurna/sensors/BH1750Sensor.h +++ b/code/espurna/sensors/BH1750Sensor.h @@ -9,31 +9,96 @@ #include "I2CSensor.h" -#define BH1750_CONTINUOUS_HIGH_RES_MODE 0x10 // Start measurement at 1lx resolution. Measurement time is approx 120ms. -#define BH1750_CONTINUOUS_HIGH_RES_MODE_2 0x11 // Start measurement at 0.5lx resolution. Measurement time is approx 120ms. -#define BH1750_CONTINUOUS_LOW_RES_MODE 0x13 // Start measurement at 4lx resolution. Measurement time is approx 16ms. -#define BH1750_ONE_TIME_HIGH_RES_MODE 0x20 // Start measurement at 1lx resolution. Measurement time is approx 120ms. - // Device is automatically set to Power Down after measurement. -#define BH1750_ONE_TIME_HIGH_RES_MODE_2 0x21 // Start measurement at 0.5lx resolution. Measurement time is approx 120ms. - // Device is automatically set to Power Down after measurement. -#define BH1750_ONE_TIME_LOW_RES_MODE 0x23 // Start measurement at 1lx resolution. Measurement time is approx 120ms. - // Device is automatically set to Power Down after measurement. - -static constexpr bool bh1750_is_mode2(unsigned char mode) { - return (mode == BH1750_CONTINUOUS_HIGH_RES_MODE_2) || (mode == BH1750_ONE_TIME_HIGH_RES_MODE_2); -} +// Start measurement at 1lx resolution. Measurement time is approx 120ms. +#define BH1750_CONTINUOUS_HIGH_RES_MODE BH1750Sensor::Mode::ContinuousHighRes + +// Start measurement at 0.5lx resolution. Measurement time is approx 120ms. +#define BH1750_CONTINUOUS_HIGH_RES_MODE_2 BH1750Sensor::Mode::ContinuousHighRes2 + +// Start measurement at 4lx resolution. Measurement time is approx 16ms. +#define BH1750_CONTINUOUS_LOW_RES_MODE BH1750Sensor::Mode::ContinuousLowRes + +// -//- as the above, but device is automatically set to Power Down after measurement. +#define BH1750_ONE_TIME_HIGH_RES_MODE BH1750Sensor::Mode::OneTimeHighRes +#define BH1750_ONE_TIME_HIGH_RES_MODE_2 BH1750Sensor::Mode::OneTimeHighRes2 +#define BH1750_ONE_TIME_LOW_RES_MODE BH1750Sensor::Mode::OneTimeLowRes class BH1750Sensor : public I2CSensor<> { public: + enum class Mode { + ContinuousHighRes, + ContinuousHighRes2, + ContinuousLowRes, + OneTimeHighRes, + OneTimeHighRes2, + OneTimeLowRes, + }; + + private: + static constexpr bool is_mode2(Mode mode) { + return (mode == Mode::ContinuousHighRes2) + || (mode == Mode::OneTimeHighRes2); + } + + static constexpr uint8_t mode_to_reg(Mode mode) { + return (mode == Mode::ContinuousHighRes) + ? 0b00010000 + : (mode == Mode::ContinuousHighRes2) + ? 0b00010001 + : (mode == Mode::ContinuousLowRes) + ? 0b00010011 + : (mode == Mode::OneTimeHighRes) + ? 0b00100000 + : (mode == Mode::OneTimeHighRes2) + ? 0b00100001 + : (mode == Mode::OneTimeLowRes) + ? 0b00100011 + : 0; + } + + static uint8_t sensitivity_to_mtime(double value) { + static constexpr double Default { 69.0 }; + value = std::nearbyint(Default * value); + + static constexpr double Min { 31.0 }; + static constexpr double Max { 254.0 }; + value = std::clamp(value, Min, Max); + + return static_cast(value); + } + + struct MeasurementTime { + uint8_t low; + uint8_t high; + }; + + auto mtime_to_reg(uint8_t time) -> MeasurementTime { + MeasurementTime out; + + static constexpr uint8_t Low { 0b01000000 }; + out.low = (time >> 5) | Low; // aka MT[7,6,5] - void setMode(unsigned char mode) { - if (_mode == mode) return; + static constexpr uint8_t High { 0b01100000 }; + out.high = (time & 0b11111) | High; // aka MT[4,3,2,1,0] + + return out; + } + + public: + void setMode(Mode mode) { _mode = mode; - _dirty = true; } - unsigned char getMode() const { - return _mode; + void setSensitivity(double sensitivity) { + static constexpr double Min { 0.45 }; + static constexpr double Max { 3.68 }; + _sensitivity = std::clamp(sensitivity, Min, Max); + } + + void setAccuracy(double accuracy) { + static constexpr double Min { 0.96 }; + static constexpr double Max { 1.44 }; + _accuracy = std::clamp(accuracy, Min, Max); } // --------------------------------------------------------------------- @@ -50,23 +115,24 @@ class BH1750Sensor : public I2CSensor<> { // Initialization method, must be idempotent void begin() override { - if (!_dirty) { return; } - // I2C auto-discover static constexpr uint8_t addresses[] {0x23, 0x5C}; auto address = findAndLock(addresses); if (address == 0) { return; } - // Run configuration on next update - _run_configure = true; + _mtreg = mtime_to_reg(sensitivity_to_mtime(_sensitivity)); + _modereg = mode_to_reg(_mode); + + _init(); + _wait(); + _ready = true; _dirty = false; - } // Descriptive name of the sensor @@ -79,25 +145,67 @@ class BH1750Sensor : public I2CSensor<> { // Type for slot # index unsigned char type(unsigned char index) const override { - if (index == 0) return MAGNITUDE_LUX; + if (index == 0) { + return MAGNITUDE_LUX; + } + return MAGNITUDE_NONE; } + // Loop-like method, call it in your main loop + virtual void tick() { + if (_wait_reading && (TimeSource::now() - _wait_start) > _wait_duration) { + _wait_reading = false; + } + } + // Pre-read hook (usually to populate registers with up-to-date data) void pre() override { _error = SENSOR_ERROR_OK; - _lux = _read(lockedAddress()); + if (_wait_reading) { + _error = SENSOR_ERROR_NOT_READY; + return; + } + + const auto lux = _read_lux(lockedAddress()); + if (!lux.ok) { + _error = SENSOR_ERROR_NOT_READY; + _init(); + _wait(); + return; + } + + _lux = 0; + if (lux.value > 0) { + _lux = _value(lux.value); + } + + // repeatedly update mode b/c sensor + // is powering down after each reading + switch (_mode) { + case Mode::OneTimeHighRes: + case Mode::OneTimeHighRes2: + case Mode::OneTimeLowRes: + _init(); + _wait(); + break; + default: + break; + } } // Current value for slot # index double value(unsigned char index) override { - if (index == 0) return _lux; + if (index == 0) { + return _lux; + } + return 0; } // Number of decimals for a unit (or -1 for default) signed char decimals(espurna::sensor::Unit unit) const { - if (bh1750_is_mode2(_mode)) { + if (is_mode2(_mode)) { return 2; } @@ -105,45 +213,86 @@ class BH1750Sensor : public I2CSensor<> { } protected: + void _init(uint8_t address) { + i2c_write_uint8(address, _mtreg.low); + i2c_write_uint8(address, _mtreg.high); + i2c_write_uint8(address, _modereg); + } + + void _init() { + _init(lockedAddress()); + } + + // Make sure to wait maximum amount of time specified at + // Electrical Characteristics (VCC 3.0V, DVI 3.0V, Ta 25C) pg. 2/17 + // We take the maximum time values, not typival ones. + void _wait() { + double wait = 0; + switch (_mode) { + case Mode::ContinuousHighRes: + case Mode::ContinuousHighRes2: + case Mode::OneTimeHighRes: + case Mode::OneTimeHighRes2: + wait = 180.0; + break; + case Mode::ContinuousLowRes: + case Mode::OneTimeLowRes: + wait = 24.0; + break; + } - double _read(uint8_t address) { - // For one-shot modes reconfigure sensor & wait for conversion - if (_run_configure) { + wait *= _sensitivity; + wait = std::nearbyint(wait); - // Configure mode - i2c_write_uint8(address, _mode); + _wait_duration = TimeSource::duration( + static_cast(wait)); + _wait_start = TimeSource::now(); + _wait_reading = true; + } - // According to datasheet - // conversion time is ~16ms for low resolution - // and ~120 for high resolution - // but more time is needed - espurna::time::blockingDelay( - espurna::duration::Milliseconds { (_mode & 0x02) ? 24 : 180 }); + struct Lux { + uint16_t value; + bool ok; + }; - // Keep on running configure each time if one-shot mode - _run_configure = (_mode & 0x20) > 0; + Lux _read_lux(uint8_t address) { + Lux out; + out.value = i2c_read_uint16(address); + out.ok = out.value != 0xffff; - } + return out; + } - uint16_t level = i2c_read_uint16(address); - if (level == 0xFFFF) { - _error = SENSOR_ERROR_CRC; - _run_configure = true; - return 0; + // pg. 11/17 + // > The below formula is to calculate illuminance per 1 count. + // > H-reslution mode : Illuminance per 1 count ( lx / count ) = 1 / 1.2 *( 69 / X ) + // > H-reslution mode2 : Illuminance per 1 count ( lx / count ) = 1 / 1.2 *( 69 / X ) / 2 + // - 1.2 is default accuracy; we allow a custom value + // - `69 / X` is substituted by `1.0 / ACCURACY` + // - optional division by two when in H-resolution MODE2 + double _value(uint16_t raw) { + auto value = static_cast(raw) / _accuracy; + value *= (1.0 / _sensitivity); + if (is_mode2(_mode)) { + value /= 2.0; } - // When using HIGH Mode2, value is halved - const auto multiplier = bh1750_is_mode2(_mode) ? 0.5 : 1.0; - - // TODO also * MTreg? - return ((double)level / 1.2) * multiplier; + return value; } - unsigned char _mode; - bool _run_configure = false; + using TimeSource = espurna::time::CoreClock; + TimeSource::time_point _wait_start; + TimeSource::duration _wait_duration; + bool _wait_reading = false; + + MeasurementTime _mtreg; + uint8_t _modereg; - double _lux = 0; + Mode _mode; + double _sensitivity; + double _accuracy; + double _lux; }; #endif // SENSOR_SUPPORT && BH1750_SUPPORT