2019-04-17 12:06:00 +02:00
|
|
|
#include "tcs34725.h"
|
|
|
|
#include "esphome/core/log.h"
|
2021-09-20 11:47:51 +02:00
|
|
|
#include "esphome/core/hal.h"
|
2019-04-17 12:06:00 +02:00
|
|
|
|
|
|
|
namespace esphome {
|
|
|
|
namespace tcs34725 {
|
|
|
|
|
2021-06-10 22:19:44 +02:00
|
|
|
static const char *const TAG = "tcs34725";
|
2019-04-17 12:06:00 +02:00
|
|
|
|
|
|
|
static const uint8_t TCS34725_ADDRESS = 0x29;
|
|
|
|
static const uint8_t TCS34725_COMMAND_BIT = 0x80;
|
|
|
|
static const uint8_t TCS34725_REGISTER_ID = TCS34725_COMMAND_BIT | 0x12;
|
|
|
|
static const uint8_t TCS34725_REGISTER_ATIME = TCS34725_COMMAND_BIT | 0x01;
|
|
|
|
static const uint8_t TCS34725_REGISTER_CONTROL = TCS34725_COMMAND_BIT | 0x0F;
|
|
|
|
static const uint8_t TCS34725_REGISTER_ENABLE = TCS34725_COMMAND_BIT | 0x00;
|
|
|
|
static const uint8_t TCS34725_REGISTER_CDATAL = TCS34725_COMMAND_BIT | 0x14;
|
|
|
|
static const uint8_t TCS34725_REGISTER_RDATAL = TCS34725_COMMAND_BIT | 0x16;
|
|
|
|
static const uint8_t TCS34725_REGISTER_GDATAL = TCS34725_COMMAND_BIT | 0x18;
|
|
|
|
static const uint8_t TCS34725_REGISTER_BDATAL = TCS34725_COMMAND_BIT | 0x1A;
|
|
|
|
|
|
|
|
void TCS34725Component::setup() {
|
|
|
|
ESP_LOGCONFIG(TAG, "Setting up TCS34725...");
|
|
|
|
uint8_t id;
|
2022-01-24 23:41:14 +01:00
|
|
|
if (this->read_register(TCS34725_REGISTER_ID, &id, 1) != i2c::ERROR_OK) {
|
2019-04-17 12:06:00 +02:00
|
|
|
this->mark_failed();
|
|
|
|
return;
|
|
|
|
}
|
2022-01-24 23:41:14 +01:00
|
|
|
if (this->write_config_register_(TCS34725_REGISTER_ATIME, this->integration_reg_) != i2c::ERROR_OK ||
|
|
|
|
this->write_config_register_(TCS34725_REGISTER_CONTROL, this->gain_reg_) != i2c::ERROR_OK) {
|
2019-04-17 12:06:00 +02:00
|
|
|
this->mark_failed();
|
|
|
|
return;
|
|
|
|
}
|
2022-01-24 23:41:14 +01:00
|
|
|
if (this->write_config_register_(TCS34725_REGISTER_ENABLE, 0x01) !=
|
|
|
|
i2c::ERROR_OK) { // Power on (internal oscillator on)
|
2019-04-17 12:06:00 +02:00
|
|
|
this->mark_failed();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
delay(3);
|
2022-01-24 23:41:14 +01:00
|
|
|
if (this->write_config_register_(TCS34725_REGISTER_ENABLE, 0x03) !=
|
|
|
|
i2c::ERROR_OK) { // Power on (internal oscillator on) + RGBC ADC Enable
|
2019-04-17 12:06:00 +02:00
|
|
|
this->mark_failed();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TCS34725Component::dump_config() {
|
|
|
|
ESP_LOGCONFIG(TAG, "TCS34725:");
|
|
|
|
LOG_I2C_DEVICE(this);
|
|
|
|
if (this->is_failed()) {
|
|
|
|
ESP_LOGE(TAG, "Communication with TCS34725 failed!");
|
|
|
|
}
|
|
|
|
LOG_UPDATE_INTERVAL(this);
|
|
|
|
|
|
|
|
LOG_SENSOR(" ", "Clear Channel", this->clear_sensor_);
|
|
|
|
LOG_SENSOR(" ", "Red Channel", this->red_sensor_);
|
|
|
|
LOG_SENSOR(" ", "Green Channel", this->green_sensor_);
|
|
|
|
LOG_SENSOR(" ", "Blue Channel", this->blue_sensor_);
|
|
|
|
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
|
|
|
LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_);
|
|
|
|
}
|
|
|
|
float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; }
|
2021-10-13 18:45:41 +02:00
|
|
|
|
|
|
|
/*!
|
|
|
|
* @brief Converts the raw R/G/B values to color temperature in degrees
|
|
|
|
* Kelvin using the algorithm described in DN40 from Taos (now AMS).
|
|
|
|
* @param r
|
|
|
|
* Red value
|
|
|
|
* @param g
|
|
|
|
* Green value
|
|
|
|
* @param b
|
|
|
|
* Blue value
|
|
|
|
* @param c
|
|
|
|
* Clear channel value
|
|
|
|
* @return Color temperature in degrees Kelvin
|
|
|
|
*/
|
|
|
|
void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c) {
|
|
|
|
float r2, g2, b2; /* RGB values minus IR component */
|
|
|
|
float sat; /* Digital saturation level */
|
|
|
|
float ir; /* Inferred IR content */
|
|
|
|
|
|
|
|
this->illuminance_ = 0; // Assign 0 value before calculation
|
|
|
|
this->color_temperature_ = 0;
|
|
|
|
|
|
|
|
const float ga = this->glass_attenuation_; // Glass Attenuation Factor
|
|
|
|
static const float DF = 310.f; // Device Factor
|
|
|
|
static const float R_COEF = 0.136f; //
|
|
|
|
static const float G_COEF = 1.f; // used in lux computation
|
|
|
|
static const float B_COEF = -0.444f; //
|
|
|
|
static const float CT_COEF = 3810.f; // Color Temperature Coefficient
|
|
|
|
static const float CT_OFFSET = 1391.f; // Color Temperatuer Offset
|
|
|
|
|
|
|
|
if (c == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Analog/Digital saturation:
|
|
|
|
*
|
|
|
|
* (a) As light becomes brighter, the clear channel will tend to
|
|
|
|
* saturate first since R+G+B is approximately equal to C.
|
|
|
|
* (b) The TCS34725 accumulates 1024 counts per 2.4ms of integration
|
|
|
|
* time, up to a maximum values of 65535. This means analog
|
|
|
|
* saturation can occur up to an integration time of 153.6ms
|
|
|
|
* (64*2.4ms=153.6ms).
|
|
|
|
* (c) If the integration time is > 153.6ms, digital saturation will
|
|
|
|
* occur before analog saturation. Digital saturation occurs when
|
|
|
|
* the count reaches 65535.
|
|
|
|
*/
|
|
|
|
if ((256 - this->integration_reg_) > 63) {
|
|
|
|
/* Track digital saturation */
|
|
|
|
sat = 65535.f;
|
|
|
|
} else {
|
|
|
|
/* Track analog saturation */
|
|
|
|
sat = 1024.f * (256.f - this->integration_reg_);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Ripple rejection:
|
|
|
|
*
|
|
|
|
* (a) An integration time of 50ms or multiples of 50ms are required to
|
|
|
|
* reject both 50Hz and 60Hz ripple.
|
|
|
|
* (b) If an integration time faster than 50ms is required, you may need
|
|
|
|
* to average a number of samples over a 50ms period to reject ripple
|
|
|
|
* from fluorescent and incandescent light sources.
|
|
|
|
*
|
|
|
|
* Ripple saturation notes:
|
|
|
|
*
|
|
|
|
* (a) If there is ripple in the received signal, the value read from C
|
|
|
|
* will be less than the max, but still have some effects of being
|
|
|
|
* saturated. This means that you can be below the 'sat' value, but
|
|
|
|
* still be saturating. At integration times >150ms this can be
|
|
|
|
* ignored, but <= 150ms you should calculate the 75% saturation
|
|
|
|
* level to avoid this problem.
|
|
|
|
*/
|
|
|
|
if (this->integration_time_ < 150) {
|
|
|
|
/* Adjust sat to 75% to avoid analog saturation if atime < 153.6ms */
|
|
|
|
sat -= sat / 4.f;
|
|
|
|
}
|
|
|
|
/* Check for saturation and mark the sample as invalid if true */
|
|
|
|
if (c >= sat) {
|
2022-05-12 06:53:33 +02:00
|
|
|
if (this->integration_time_auto_) {
|
|
|
|
ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing");
|
|
|
|
} else {
|
|
|
|
ESP_LOGW(
|
|
|
|
TAG,
|
|
|
|
"Saturation too high, sample with saturation %.1f and clear %d treat values carefully or use grey filter",
|
|
|
|
sat, c);
|
|
|
|
}
|
2021-10-13 18:45:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* AMS RGB sensors have no IR channel, so the IR content must be */
|
|
|
|
/* calculated indirectly. */
|
|
|
|
ir = ((r + g + b) > c) ? (r + g + b - c) / 2 : 0;
|
|
|
|
|
|
|
|
/* Remove the IR component from the raw RGB values */
|
|
|
|
r2 = r - ir;
|
|
|
|
g2 = g - ir;
|
|
|
|
b2 = b - ir;
|
|
|
|
|
2022-05-12 06:53:33 +02:00
|
|
|
// discarding super low values? not recemmonded, and avoided by using auto gain.
|
2021-10-13 18:45:41 +02:00
|
|
|
if (r2 == 0) {
|
2022-05-12 06:53:33 +02:00
|
|
|
// legacy code
|
|
|
|
if (!this->integration_time_auto_) {
|
|
|
|
ESP_LOGW(TAG,
|
|
|
|
"No light detected on red channel, switch to auto gain or adjust timing, values will be unreliable");
|
|
|
|
return;
|
|
|
|
}
|
2021-10-13 18:45:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lux Calculation (DN40 3.2)
|
|
|
|
|
|
|
|
float g1 = R_COEF * r2 + G_COEF * g2 + B_COEF * b2;
|
|
|
|
float cpl = (this->integration_time_ * this->gain_) / (ga * DF);
|
|
|
|
this->illuminance_ = g1 / cpl;
|
|
|
|
|
|
|
|
// Color Temperature Calculation (DN40)
|
|
|
|
/* A simple method of measuring color temp is to use the ratio of blue */
|
|
|
|
/* to red light, taking IR cancellation into account. */
|
|
|
|
this->color_temperature_ = (CT_COEF * b2) / /** Color temp coefficient. */
|
|
|
|
r2 +
|
|
|
|
CT_OFFSET; /** Color temp offset. */
|
|
|
|
}
|
|
|
|
|
2019-04-17 12:06:00 +02:00
|
|
|
void TCS34725Component::update() {
|
|
|
|
uint16_t raw_c;
|
|
|
|
uint16_t raw_r;
|
|
|
|
uint16_t raw_g;
|
|
|
|
uint16_t raw_b;
|
|
|
|
|
2022-01-24 23:41:14 +01:00
|
|
|
if (this->read_data_register_(TCS34725_REGISTER_CDATAL, raw_c) != i2c::ERROR_OK) {
|
2019-04-17 12:06:00 +02:00
|
|
|
this->status_set_warning();
|
|
|
|
return;
|
|
|
|
}
|
2022-01-24 23:41:14 +01:00
|
|
|
if (this->read_data_register_(TCS34725_REGISTER_RDATAL, raw_r) != i2c::ERROR_OK) {
|
|
|
|
this->status_set_warning();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this->read_data_register_(TCS34725_REGISTER_GDATAL, raw_g) != i2c::ERROR_OK) {
|
|
|
|
this->status_set_warning();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this->read_data_register_(TCS34725_REGISTER_BDATAL, raw_b) != i2c::ERROR_OK) {
|
|
|
|
this->status_set_warning();
|
|
|
|
return;
|
|
|
|
}
|
2022-05-12 06:53:33 +02:00
|
|
|
ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b);
|
2022-01-24 23:41:14 +01:00
|
|
|
|
|
|
|
float channel_c;
|
|
|
|
float channel_r;
|
|
|
|
float channel_g;
|
|
|
|
float channel_b;
|
|
|
|
// avoid division by 0 and return black if clear is 0
|
|
|
|
if (raw_c == 0) {
|
|
|
|
channel_c = channel_r = channel_g = channel_b = 0.0f;
|
|
|
|
} else {
|
|
|
|
float max_count = this->integration_time_ * 1024.0f / 2.4;
|
|
|
|
float sum = raw_c;
|
|
|
|
channel_r = raw_r / sum * 100.0f;
|
|
|
|
channel_g = raw_g / sum * 100.0f;
|
|
|
|
channel_b = raw_b / sum * 100.0f;
|
|
|
|
channel_c = raw_c / max_count * 100.0f;
|
|
|
|
}
|
2019-04-17 12:06:00 +02:00
|
|
|
|
|
|
|
if (this->clear_sensor_ != nullptr)
|
|
|
|
this->clear_sensor_->publish_state(channel_c);
|
|
|
|
if (this->red_sensor_ != nullptr)
|
|
|
|
this->red_sensor_->publish_state(channel_r);
|
|
|
|
if (this->green_sensor_ != nullptr)
|
|
|
|
this->green_sensor_->publish_state(channel_g);
|
|
|
|
if (this->blue_sensor_ != nullptr)
|
|
|
|
this->blue_sensor_->publish_state(channel_b);
|
|
|
|
|
2021-10-13 18:45:41 +02:00
|
|
|
if (this->illuminance_sensor_ || this->color_temperature_sensor_) {
|
|
|
|
calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c);
|
|
|
|
}
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2022-05-12 06:53:33 +02:00
|
|
|
// do not publish values if auto gain finding ongoing, and oversaturated
|
|
|
|
// so: publish when:
|
|
|
|
// - not auto mode
|
|
|
|
// - clear not oversaturated
|
|
|
|
// - clear oversaturated but gain and timing cannot go lower
|
|
|
|
if (!this->integration_time_auto_ || raw_c < 65530 || (this->gain_reg_ == 0 && this->integration_time_ < 200)) {
|
|
|
|
if (this->illuminance_sensor_ != nullptr)
|
|
|
|
this->illuminance_sensor_->publish_state(this->illuminance_);
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2022-05-12 06:53:33 +02:00
|
|
|
if (this->color_temperature_sensor_ != nullptr)
|
|
|
|
this->color_temperature_sensor_->publish_state(this->color_temperature_);
|
|
|
|
}
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2022-05-12 06:53:33 +02:00
|
|
|
ESP_LOGD(TAG,
|
|
|
|
"Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color "
|
|
|
|
"Temperature=%.1fK",
|
2022-01-24 23:41:14 +01:00
|
|
|
channel_r, channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_);
|
2019-04-17 12:06:00 +02:00
|
|
|
|
2022-05-12 06:53:33 +02:00
|
|
|
if (this->integration_time_auto_) {
|
|
|
|
// change integration time an gain to achieve maximum resolution an dynamic range
|
|
|
|
// calculate optimal integration time to achieve 70% satuaration
|
|
|
|
float integration_time_ideal;
|
|
|
|
integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_;
|
|
|
|
|
|
|
|
uint8_t gain_reg_val_new = this->gain_reg_;
|
|
|
|
// increase gain if less than 20% of white channel used and high integration time
|
|
|
|
// increase only if not already maximum
|
|
|
|
// do not use max gain, as ist will not get better
|
|
|
|
if (this->gain_reg_ < 3) {
|
|
|
|
if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) {
|
|
|
|
gain_reg_val_new = this->gain_reg_ + 1;
|
|
|
|
// update integration time to new situation
|
|
|
|
integration_time_ideal = integration_time_ideal / 4;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// decrease gain, if very high clear values and integration times alreadey low
|
|
|
|
if (this->gain_reg_ > 0) {
|
|
|
|
if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) {
|
|
|
|
gain_reg_val_new = this->gain_reg_ - 1;
|
|
|
|
// update integration time to new situation
|
|
|
|
integration_time_ideal = integration_time_ideal * 4;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// saturate integration times
|
|
|
|
float integration_time_next = integration_time_ideal;
|
|
|
|
if (integration_time_ideal > 2.4f * 256) {
|
|
|
|
integration_time_next = 2.4f * 256;
|
|
|
|
}
|
|
|
|
if (integration_time_ideal < 154) {
|
|
|
|
integration_time_next = 154;
|
|
|
|
}
|
|
|
|
|
|
|
|
// calculate register value from timing
|
2023-03-20 04:38:41 +01:00
|
|
|
uint8_t regval_atime = (uint8_t) (256.f - integration_time_next / 2.4f);
|
2022-05-12 06:53:33 +02:00
|
|
|
ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d gain reg: %d",
|
|
|
|
this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_);
|
|
|
|
|
|
|
|
if (this->integration_reg_ != regval_atime || gain_reg_val_new != this->gain_reg_) {
|
|
|
|
this->integration_reg_ = regval_atime;
|
|
|
|
this->gain_reg_ = gain_reg_val_new;
|
|
|
|
set_gain((TCS34725Gain) gain_reg_val_new);
|
|
|
|
if (this->write_config_register_(TCS34725_REGISTER_ATIME, this->integration_reg_) != i2c::ERROR_OK ||
|
|
|
|
this->write_config_register_(TCS34725_REGISTER_CONTROL, this->gain_reg_) != i2c::ERROR_OK) {
|
|
|
|
this->mark_failed();
|
|
|
|
ESP_LOGW(TAG, "TCS34725I update timing failed!");
|
|
|
|
} else {
|
|
|
|
this->integration_time_ = integration_time_next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-04-17 12:06:00 +02:00
|
|
|
this->status_clear_warning();
|
|
|
|
}
|
|
|
|
void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) {
|
2022-05-12 06:53:33 +02:00
|
|
|
// if an integration time is 0x100, this is auto start with 154ms as this gives best starting point
|
|
|
|
TCS34725IntegrationTime my_integration_time_regval;
|
|
|
|
|
|
|
|
if (integration_time == TCS34725_INTEGRATION_TIME_AUTO) {
|
|
|
|
this->integration_time_auto_ = true;
|
|
|
|
this->integration_reg_ = TCS34725_INTEGRATION_TIME_154MS;
|
|
|
|
my_integration_time_regval = TCS34725_INTEGRATION_TIME_154MS;
|
|
|
|
} else {
|
|
|
|
this->integration_reg_ = integration_time;
|
|
|
|
my_integration_time_regval = integration_time;
|
|
|
|
this->integration_time_auto_ = false;
|
|
|
|
}
|
|
|
|
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
|
|
|
|
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
|
2021-10-13 18:45:41 +02:00
|
|
|
}
|
|
|
|
void TCS34725Component::set_gain(TCS34725Gain gain) {
|
|
|
|
this->gain_reg_ = gain;
|
|
|
|
switch (gain) {
|
|
|
|
case TCS34725Gain::TCS34725_GAIN_1X:
|
|
|
|
this->gain_ = 1.f;
|
|
|
|
break;
|
|
|
|
case TCS34725Gain::TCS34725_GAIN_4X:
|
|
|
|
this->gain_ = 4.f;
|
|
|
|
break;
|
|
|
|
case TCS34725Gain::TCS34725_GAIN_16X:
|
|
|
|
this->gain_ = 16.f;
|
|
|
|
break;
|
|
|
|
case TCS34725Gain::TCS34725_GAIN_60X:
|
|
|
|
this->gain_ = 60.f;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this->gain_ = 1.f;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void TCS34725Component::set_glass_attenuation_factor(float ga) {
|
|
|
|
// The Glass Attenuation (FA) factor used to compensate for lower light
|
|
|
|
// levels at the device due to the possible presence of glass. The GA is
|
|
|
|
// the inverse of the glass transmissivity (T), so GA = 1/T. A transmissivity
|
|
|
|
// of 50% gives GA = 1 / 0.50 = 2. If no glass is present, use GA = 1.
|
|
|
|
// See Application Note: DN40-Rev 1.0
|
|
|
|
this->glass_attenuation_ = ga;
|
2019-04-17 12:06:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace tcs34725
|
|
|
|
} // namespace esphome
|