ESPHome  2024.9.0
ltr_als_ps.cpp
Go to the documentation of this file.
1 #include "ltr_als_ps.h"
3 #include "esphome/core/log.h"
4 #include "esphome/core/helpers.h"
5 
7 
8 namespace esphome {
9 namespace ltr_als_ps {
10 
11 static const char *const TAG = "ltr_als_ps";
12 
13 static const uint8_t MAX_TRIES = 5;
14 
15 template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
16  size_t i = 0;
17  size_t idx = -1;
18  while (idx == -1 && i < size) {
19  if (array[i] == val) {
20  idx = i;
21  break;
22  }
23  i++;
24  }
25  if (idx == -1 || i + 1 >= size)
26  return val;
27  return array[i + 1];
28 }
29 
30 template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
31  size_t i = size - 1;
32  size_t idx = -1;
33  while (idx == -1 && i > 0) {
34  if (array[i] == val) {
35  idx = i;
36  break;
37  }
38  i--;
39  }
40  if (idx == -1 || i == 0)
41  return val;
42  return array[i - 1];
43 }
44 
45 static uint16_t get_itime_ms(IntegrationTime time) {
46  static const uint16_t ALS_INT_TIME[8] = {100, 50, 200, 400, 150, 250, 300, 350};
47  return ALS_INT_TIME[time & 0b111];
48 }
49 
50 static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) {
51  static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000};
52  return ALS_MEAS_RATE[rate & 0b111];
53 }
54 
55 static float get_gain_coeff(AlsGain gain) {
56  static const float ALS_GAIN[8] = {1, 2, 4, 8, 0, 0, 48, 96};
57  return ALS_GAIN[gain & 0b111];
58 }
59 
60 static float get_ps_gain_coeff(PsGain gain) {
61  static const float PS_GAIN[4] = {16, 0, 32, 64};
62  return PS_GAIN[gain & 0b11];
63 }
64 
66  ESP_LOGCONFIG(TAG, "Setting up LTR-303/329/55x/659");
67  // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive
68  this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; });
69 }
70 
72  auto get_device_type = [](LtrType typ) {
73  switch (typ) {
75  return "ALS only";
77  return "PS only";
79  return "ALS + PS";
80  default:
81  return "Unknown";
82  }
83  };
84 
85  LOG_I2C_DEVICE(this);
86  ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_));
87  if (this->is_als_()) {
88  ESP_LOGCONFIG(TAG, " Automatic mode: %s", ONOFF(this->automatic_mode_enabled_));
89  ESP_LOGCONFIG(TAG, " Gain: %.0fx", get_gain_coeff(this->gain_));
90  ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_));
91  ESP_LOGCONFIG(TAG, " Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_));
92  ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_);
93  LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_);
94  LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_);
95  LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_);
96  LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_);
97  }
98  if (this->is_ps_()) {
99  ESP_LOGCONFIG(TAG, " Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_));
100  ESP_LOGCONFIG(TAG, " Proximity cooldown time: %d s", this->ps_cooldown_time_s_);
101  ESP_LOGCONFIG(TAG, " Proximity high threshold: %d", this->ps_threshold_high_);
102  ESP_LOGCONFIG(TAG, " Proximity low threshold: %d", this->ps_threshold_low_);
103  LOG_SENSOR(" ", "Proximity counts", this->proximity_counts_sensor_);
104  }
105  LOG_UPDATE_INTERVAL(this);
106 
107  if (this->is_failed()) {
108  ESP_LOGE(TAG, "Communication with I2C LTR-303/329/55x/659 failed!");
109  }
110 }
111 
113  ESP_LOGV(TAG, "Updating");
114  if (this->is_ready() && this->state_ == State::IDLE) {
115  ESP_LOGV(TAG, "Initiating new data collection");
116 
118 
119  this->als_readings_.ch0 = 0;
120  this->als_readings_.ch1 = 0;
121  this->als_readings_.gain = this->gain_;
123  this->als_readings_.lux = 0;
125 
126  } else {
127  ESP_LOGV(TAG, "Component not ready yet");
128  }
129 }
130 
132  ErrorCode err = i2c::ERROR_OK;
133  static uint8_t tries{0};
134 
135  switch (this->state_) {
137  err = this->write(nullptr, 0);
138  if (err != i2c::ERROR_OK) {
139  ESP_LOGV(TAG, "i2c connection failed");
140  this->mark_failed();
141  }
142  this->configure_reset_();
143  if (this->is_als_()) {
144  this->configure_als_();
146  }
147  if (this->is_ps_()) {
148  this->configure_ps_();
149  }
150 
151  this->state_ = State::IDLE;
152  break;
153 
154  case State::IDLE:
155  if (this->is_ps_()) {
157  }
158  break;
159 
162  tries = 0;
163  ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
164  get_itime_ms(this->als_readings_.integration_time));
165  this->read_sensor_data_(this->als_readings_);
166  this->state_ = State::DATA_COLLECTED;
168  } else if (tries >= MAX_TRIES) {
169  ESP_LOGW(TAG, "Can't get data after several tries.");
170  tries = 0;
171  this->status_set_warning();
172  this->state_ = State::IDLE;
173  return;
174  } else {
175  tries++;
176  }
177  break;
178 
181  // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration
182  if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) {
183  this->state_ = State::ADJUSTMENT_IN_PROGRESS;
184  ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
185  get_itime_ms(this->als_readings_.integration_time));
187  this->configure_gain_(this->als_readings_.gain);
188  // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters
189  this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_),
190  [this]() { this->state_ = State::WAITING_FOR_DATA; });
191  } else {
192  this->state_ = State::READY_TO_PUBLISH;
193  }
194  break;
195 
197  // nothing to be done, just waiting for the timeout
198  break;
199 
201  this->publish_data_part_1_(this->als_readings_);
202  this->state_ = State::KEEP_PUBLISHING;
203  break;
204 
206  this->publish_data_part_2_(this->als_readings_);
207  this->status_clear_warning();
208  this->state_ = State::IDLE;
209  break;
210 
211  default:
212  break;
213  }
214 }
215 
217  static uint32_t last_high_trigger_time{0};
218  static uint32_t last_low_trigger_time{0};
219  uint16_t ps_data = this->read_ps_data_();
220  uint32_t now = millis();
221 
222  if (ps_data != this->ps_readings_) {
223  this->ps_readings_ = ps_data;
224  // Higher values - object is closer to sensor
225  if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
226  last_high_trigger_time = now;
227  ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data,
228  this->ps_threshold_high_);
229  this->on_ps_high_trigger_callback_.call();
230  } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
231  last_low_trigger_time = now;
232  ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data,
233  this->ps_threshold_low_);
234  this->on_ps_low_trigger_callback_.call();
235  }
236  }
237 }
238 
240  uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get();
241  if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID
242  ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id);
243  this->mark_failed();
244  return false;
245  }
246 
247  // Things getting not really funny here, we can't identify device type by part number ID
248  // ======================== ========= ===== =================
249  // Device Part ID Rev Capabilities
250  // ======================== ========= ===== =================
251  // Ltr-329/ltr-303 0x0a 0x00 Als 16b
252  // Ltr-553/ltr-556/ltr-556 0x09 0x02 Als 16b + Ps 11b diff nm sens
253  // Ltr-659 0x09 0x02 Ps 11b and ps gain
254  //
255  // There are other devices which might potentially work with default settings,
256  // but registers layout is different and we can't use them properly. For ex. ltr-558
257 
258  PartIdRegister part_id{0};
259  part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get();
260  if (part_id.part_number_id != 0x0a && part_id.part_number_id != 0x09) {
261  ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. It might not work properly.", part_id.part_number_id);
262  this->status_set_warning();
263  return true;
264  }
265  return true;
266 }
267 
269  ESP_LOGV(TAG, "Resetting");
270 
271  AlsControlRegister als_ctrl{0};
272  als_ctrl.sw_reset = true;
273  this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
274  delay(2);
275 
276  uint8_t tries = MAX_TRIES;
277  do {
278  ESP_LOGV(TAG, "Waiting for chip to reset");
279  delay(2);
280  als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
281  } while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting
282 
283  if (als_ctrl.sw_reset) {
284  ESP_LOGW(TAG, "Reset timed out");
285  }
286 }
287 
289  AlsControlRegister als_ctrl{0};
290 
291  als_ctrl.sw_reset = false;
292  als_ctrl.active_mode = true;
293  als_ctrl.gain = this->gain_;
294 
295  ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw);
296  this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
297  delay(5);
298 
299  uint8_t tries = MAX_TRIES;
300  do {
301  ESP_LOGV(TAG, "Waiting for device to become active...");
302  delay(2);
303  als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
304  } while (!als_ctrl.active_mode && tries--); // while active mode is not set - keep waiting
305 
306  if (!als_ctrl.active_mode) {
307  ESP_LOGW(TAG, "Failed to activate device");
308  }
309 }
310 
312  PsMeasurementRateRegister ps_meas{0};
314  this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw;
315 
316  PsControlRegister ps_ctrl{0};
317  ps_ctrl.ps_mode_active = true;
318  ps_ctrl.ps_mode_xxx = true;
319  this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw;
320 }
321 
323  AlsPsStatusRegister als_status{0};
324  als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
325  if (!als_status.ps_new_data || als_status.data_invalid) {
326  return this->ps_readings_;
327  }
328 
329  uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get();
330  PsData1Register ps_high;
331  ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get();
332 
333  uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low);
334  if (ps_high.ps_saturation_flag) {
335  return 0x7ff; // full 11 bit range
336  }
337  return val;
338 }
339 
341  AlsControlRegister als_ctrl{0};
342  als_ctrl.active_mode = true;
343  als_ctrl.gain = gain;
344  this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
345  delay(2);
346 
347  AlsControlRegister read_als_ctrl{0};
348  read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
349  if (read_als_ctrl.gain != gain) {
350  ESP_LOGW(TAG, "Failed to set gain. We will try one more time.");
351  this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
352  delay(2);
353  }
354 }
355 
357  MeasurementRateRegister meas{0};
359  meas.integration_time = time;
360  this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
361  delay(2);
362 
363  MeasurementRateRegister read_meas{0};
364  read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get();
365  if (read_meas.integration_time != time) {
366  ESP_LOGW(TAG, "Failed to set integration time. We will try one more time.");
367  this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
368  delay(2);
369  }
370 }
371 
373  AlsPsStatusRegister als_status{0};
374 
375  als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
376  if (!als_status.als_new_data)
377  return DataAvail::NO_DATA;
378 
379  if (als_status.data_invalid) {
380  ESP_LOGW(TAG, "Data available but not valid");
381  return DataAvail::BAD_DATA;
382  }
383  ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain));
384  if (data.gain != als_status.gain) {
385  ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain));
386  return DataAvail::BAD_DATA;
387  }
388  return DataAvail::DATA_OK;
389 }
390 
392  data.ch1 = 0;
393  data.ch0 = 0;
394  uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get();
395  uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get();
396  uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get();
397  uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get();
398  data.ch1 = encode_uint16(ch1_1, ch1_0);
399  data.ch0 = encode_uint16(ch0_1, ch0_0);
400 
401  ESP_LOGV(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0);
402 }
403 
405  if (!this->automatic_mode_enabled_)
406  return false;
407 
408  if (data.number_of_adjustments > 15) {
409  // sometimes sensors fail to change sensitivity. this prevents us from infinite loop
410  ESP_LOGW(TAG, "Too many sensitivity adjustments done. Apparently, sensor reconfiguration fails. Stopping.");
411  return false;
412  }
413  data.number_of_adjustments++;
414 
415  // Recommended thresholds as per datasheet
416  static const uint16_t LOW_INTENSITY_THRESHOLD = 1000;
417  static const uint16_t HIGH_INTENSITY_THRESHOLD = 30000;
418  static const AlsGain GAINS[GAINS_COUNT] = {GAIN_1, GAIN_2, GAIN_4, GAIN_8, GAIN_48, GAIN_96};
419  static const IntegrationTime INT_TIMES[TIMES_COUNT] = {
422 
423  if (data.ch0 <= LOW_INTENSITY_THRESHOLD) {
424  AlsGain next_gain = get_next(GAINS, data.gain);
425  if (next_gain != data.gain) {
426  data.gain = next_gain;
427  ESP_LOGV(TAG, "Low illuminance. Increasing gain.");
428  return true;
429  }
430  IntegrationTime next_time = get_next(INT_TIMES, data.integration_time);
431  if (next_time != data.integration_time) {
432  data.integration_time = next_time;
433  ESP_LOGV(TAG, "Low illuminance. Increasing integration time.");
434  return true;
435  }
436  } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD) {
437  AlsGain prev_gain = get_prev(GAINS, data.gain);
438  if (prev_gain != data.gain) {
439  data.gain = prev_gain;
440  ESP_LOGV(TAG, "High illuminance. Decreasing gain.");
441  return true;
442  }
443  IntegrationTime prev_time = get_prev(INT_TIMES, data.integration_time);
444  if (prev_time != data.integration_time) {
445  data.integration_time = prev_time;
446  ESP_LOGV(TAG, "High illuminance. Decreasing integration time.");
447  return true;
448  }
449  } else {
450  ESP_LOGD(TAG, "Illuminance is sufficient.");
451  return false;
452  }
453  ESP_LOGD(TAG, "Can't adjust sensitivity anymore.");
454  return false;
455 }
456 
458  if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) {
459  ESP_LOGW(TAG, "Sensors got saturated");
460  data.lux = 0.0f;
461  return;
462  }
463 
464  if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) {
465  ESP_LOGW(TAG, "Sensors blacked out");
466  data.lux = 0.0f;
467  return;
468  }
469 
470  float ch0 = data.ch0;
471  float ch1 = data.ch1;
472  float ratio = ch1 / (ch0 + ch1);
473  float als_gain = get_gain_coeff(data.gain);
474  float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f;
475  float inv_pfactor = this->glass_attenuation_factor_;
476  float lux = 0.0f;
477 
478  if (ratio < 0.45) {
479  lux = (1.7743 * ch0 + 1.1059 * ch1);
480  } else if (ratio < 0.64 && ratio >= 0.45) {
481  lux = (4.2785 * ch0 - 1.9548 * ch1);
482  } else if (ratio < 0.85 && ratio >= 0.64) {
483  lux = (0.5926 * ch0 + 0.1185 * ch1);
484  } else {
485  ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
486  lux = 0.0f;
487  }
488  lux = inv_pfactor * lux / als_gain / als_time;
489  data.lux = lux;
490 
491  ESP_LOGV(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain,
492  als_time, inv_pfactor, lux);
493 }
494 
496  if (this->proximity_counts_sensor_ != nullptr) {
498  }
499  if (this->ambient_light_sensor_ != nullptr) {
501  }
502  if (this->infrared_counts_sensor_ != nullptr) {
504  }
505  if (this->full_spectrum_counts_sensor_ != nullptr) {
507  }
508 }
509 
511  if (this->actual_gain_sensor_ != nullptr) {
512  this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain));
513  }
514  if (this->actual_integration_time_sensor_ != nullptr) {
516  }
517 }
518 } // namespace ltr_als_ps
519 } // namespace esphome
void status_set_warning(const char *message="unspecified")
Definition: component.cpp:151
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition: i2c.h:149
uint8_t get() const
returns the register value
Definition: i2c.cpp:75
bool is_failed() const
Definition: component.cpp:143
DataAvail is_als_data_ready_(AlsReadings &data)
Definition: ltr_als_ps.cpp:372
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition: component.cpp:69
T get_prev(const T(&array)[size], const T val)
Definition: ltr_als_ps.cpp:30
CallbackManager< void()> on_ps_high_trigger_callback_
Definition: ltr_als_ps.h:158
mopeka_std_values val[4]
void apply_lux_calculation_(AlsReadings &data)
Definition: ltr_als_ps.cpp:457
AlsGain501 gain
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
ErrorCode write(const uint8_t *data, size_t len, bool stop=true)
writes an array of bytes to a device using an I2CBus
Definition: i2c.h:186
bool is_ready() const
Definition: component.cpp:144
struct esphome::ltr_als_ps::LTRAlsPsComponent::AlsReadings als_readings_
No error found during execution of method.
Definition: i2c_bus.h:13
MeasurementRepeatRate repeat_rate_
Definition: ltr_als_ps.h:127
void status_clear_warning()
Definition: component.cpp:166
CallbackManager< void()> on_ps_low_trigger_callback_
Definition: ltr_als_ps.h:159
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:39
bool are_adjustments_required_(AlsReadings &data)
Definition: ltr_als_ps.cpp:404
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition: helpers.h:182
sensor::Sensor * full_spectrum_counts_sensor_
Definition: ltr_als_ps.h:139
void configure_integration_time_(IntegrationTime time)
Definition: ltr_als_ps.cpp:356
sensor::Sensor * actual_integration_time_sensor_
Definition: ltr_als_ps.h:142
void read_sensor_data_(AlsReadings &data)
Definition: ltr_als_ps.cpp:391
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
void publish_data_part_1_(AlsReadings &data)
Definition: ltr_als_ps.cpp:495
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition: i2c_bus.h:11
Gain ALS_GAIN
Definition: veml7700.h:79
T get_next(const T(&array)[size], const T val)
Definition: ltr_als_ps.cpp:15
void publish_data_part_2_(AlsReadings &data)
Definition: ltr_als_ps.cpp:510
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26