ESPHome 2025.5.0
Loading...
Searching...
No Matches
ltr501.cpp
Go to the documentation of this file.
1#include "ltr501.h"
3#include "esphome/core/log.h"
5
7
8namespace esphome {
9namespace ltr501 {
10
11static const char *const TAG = "ltr501";
12
13static const uint8_t MAX_TRIES = 5;
14static const uint8_t MAX_SENSITIVITY_ADJUSTMENTS = 10;
15
16struct GainTimePair {
17 AlsGain501 gain;
19};
20
21bool operator==(const GainTimePair &lhs, const GainTimePair &rhs) {
22 return lhs.gain == rhs.gain && lhs.time == rhs.time;
23}
24
25bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) {
26 return lhs.gain != rhs.gain || lhs.time != rhs.time;
27}
28
29template<typename T, size_t size> T get_next(const T (&array)[size], const T val) {
30 size_t i = 0;
31 size_t idx = -1;
32 while (idx == -1 && i < size) {
33 if (array[i] == val) {
34 idx = i;
35 break;
36 }
37 i++;
38 }
39 if (idx == -1 || i + 1 >= size)
40 return val;
41 return array[i + 1];
42}
43
44template<typename T, size_t size> T get_prev(const T (&array)[size], const T val) {
45 size_t i = size - 1;
46 size_t idx = -1;
47 while (idx == -1 && i > 0) {
48 if (array[i] == val) {
49 idx = i;
50 break;
51 }
52 i--;
53 }
54 if (idx == -1 || i == 0)
55 return val;
56 return array[i - 1];
57}
58
59static uint16_t get_itime_ms(IntegrationTime501 time) {
60 static const uint16_t ALS_INT_TIME[4] = {100, 50, 200, 400};
61 return ALS_INT_TIME[time & 0b11];
62}
63
64static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) {
65 static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000};
66 return ALS_MEAS_RATE[rate & 0b111];
67}
68
69static float get_gain_coeff(AlsGain501 gain) { return gain == AlsGain501::GAIN_1 ? 1.0f : 150.0f; }
70
71static float get_ps_gain_coeff(PsGain501 gain) {
72 static const float PS_GAIN[4] = {1, 4, 8, 16};
73 return PS_GAIN[gain & 0b11];
74}
75
77 ESP_LOGCONFIG(TAG, "Setting up LTR-501/301/558");
78 // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive
79 this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; });
80}
81
83 auto get_device_type = [](LtrType typ) {
84 switch (typ) {
86 return "ALS only";
88 return "PS only";
90 return "Als + PS";
91 default:
92 return "Unknown";
93 }
94 };
95
96 LOG_I2C_DEVICE(this);
97 ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_));
98 ESP_LOGCONFIG(TAG, " Automatic mode: %s", ONOFF(this->automatic_mode_enabled_));
99 ESP_LOGCONFIG(TAG, " Gain: %.0fx", get_gain_coeff(this->gain_));
100 ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_));
101 ESP_LOGCONFIG(TAG, " Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_));
102 ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_);
103 ESP_LOGCONFIG(TAG, " Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_));
104 ESP_LOGCONFIG(TAG, " Proximity cooldown time: %d s", this->ps_cooldown_time_s_);
105 ESP_LOGCONFIG(TAG, " Proximity high threshold: %d", this->ps_threshold_high_);
106 ESP_LOGCONFIG(TAG, " Proximity low threshold: %d", this->ps_threshold_low_);
107
108 LOG_UPDATE_INTERVAL(this);
109
110 LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_);
111 LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_);
112 LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_);
113 LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_);
114
115 if (this->is_failed()) {
116 ESP_LOGE(TAG, "Communication with I2C LTR-501/301/558 failed!");
117 }
118}
119
121 if (!this->is_als_()) {
122 ESP_LOGW(TAG, "Update. ALS data not available. Change configuration to ALS or ALS_PS.");
123 return;
124 }
125 if (this->is_ready() && this->is_als_() && this->state_ == State::IDLE) {
126 ESP_LOGV(TAG, "Update. Initiating new ALS data collection.");
127
129
130 this->als_readings_.ch0 = 0;
131 this->als_readings_.ch1 = 0;
132 this->als_readings_.gain = this->gain_;
134 this->als_readings_.lux = 0;
136
137 } else {
138 ESP_LOGV(TAG, "Update. Component not ready yet.");
139 }
140}
141
144 static uint8_t tries{0};
145
146 switch (this->state_) {
148 err = this->write(nullptr, 0);
149 if (err != i2c::ERROR_OK) {
150 ESP_LOGW(TAG, "i2c connection failed");
151 this->mark_failed();
152 }
153 this->configure_reset_();
154 if (this->is_als_()) {
155 this->configure_als_();
157 }
158 if (this->is_ps_()) {
159 this->configure_ps_();
160 }
161
162 this->state_ = State::IDLE;
163 break;
164
165 case State::IDLE:
166 if (this->is_ps_()) {
167 this->check_and_trigger_ps_();
168 }
169 break;
170
173 tries = 0;
174 ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms",
175 get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time));
176 this->read_sensor_data_(this->als_readings_);
178 this->state_ = State::DATA_COLLECTED;
179 } else if (tries >= MAX_TRIES) {
180 ESP_LOGW(TAG, "Can't get data after several tries. Aborting.");
181 tries = 0;
182 this->status_set_warning();
183 this->state_ = State::IDLE;
184 return;
185 } else {
186 tries++;
187 }
188 break;
189
192 // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration
193 if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) {
194 this->state_ = State::ADJUSTMENT_IN_PROGRESS;
195 ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
196 get_itime_ms(this->als_readings_.integration_time));
199 // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters
200 this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_),
201 [this]() { this->state_ = State::WAITING_FOR_DATA; });
202 } else {
203 this->state_ = State::READY_TO_PUBLISH;
204 }
205 break;
206
208 // nothing to be done, just waiting for the timeout
209 break;
210
213 this->state_ = State::KEEP_PUBLISHING;
214 break;
215
218 this->status_clear_warning();
219 this->state_ = State::IDLE;
220 break;
221
222 default:
223 break;
224 }
225}
226
228 static uint32_t last_high_trigger_time{0};
229 static uint32_t last_low_trigger_time{0};
230 uint16_t ps_data = this->read_ps_data_();
231 uint32_t now = millis();
232
233 if (ps_data != this->ps_readings_) {
234 this->ps_readings_ = ps_data;
235 // Higher values - object is closer to sensor
236 if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
237 last_high_trigger_time = now;
238 ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data,
239 this->ps_threshold_high_);
240 this->on_ps_high_trigger_callback_.call();
241 } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
242 last_low_trigger_time = now;
243 ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data,
244 this->ps_threshold_low_);
245 this->on_ps_low_trigger_callback_.call();
246 }
247 }
248}
249
251 uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get();
252 if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID
253 ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id);
254 this->mark_failed();
255 return false;
256 }
257
258 // Things getting not really funny here, we can't identify device type by part number ID
259 // ======================== ========= ===== =================
260 // Device Part ID Rev Capabilities
261 // ======================== ========= ===== =================
262 // ltr-558als 0x08 0 als + ps
263 // ltr-501als 0x08 0 als + ps
264 // ltr-301als - 0x08 0 als only
265
266 PartIdRegister part_id{0};
267 part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get();
268 if (part_id.part_number_id != 0x08) {
269 ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. LTR-501/301 shall have 0x08. It might not work properly.",
270 part_id.part_number_id);
271 this->status_set_warning();
272 return true;
273 }
274 return true;
275}
276
278 ESP_LOGV(TAG, "Resetting");
279
280 AlsControlRegister501 als_ctrl{0};
281 als_ctrl.sw_reset = true;
282 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
283 delay(2);
284
285 uint8_t tries = MAX_TRIES;
286 do {
287 ESP_LOGV(TAG, "Waiting chip to reset");
288 delay(2);
289 als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
290 } while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting
291
292 if (als_ctrl.sw_reset) {
293 ESP_LOGW(TAG, "Reset failed");
294 }
295}
296
298 AlsControlRegister501 als_ctrl{0};
299 als_ctrl.sw_reset = false;
300 als_ctrl.als_mode_active = true;
301 als_ctrl.gain = this->gain_;
302
303 ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw);
304 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
305 delay(5);
306
307 uint8_t tries = MAX_TRIES;
308 do {
309 ESP_LOGV(TAG, "Waiting for ALS device to become active...");
310 delay(2);
311 als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
312 } while (!als_ctrl.als_mode_active && tries--); // while active mode is not set - keep waiting
313
314 if (!als_ctrl.als_mode_active) {
315 ESP_LOGW(TAG, "Failed to activate ALS device");
316 }
317}
318
320 PsMeasurementRateRegister ps_meas{0};
322 this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw;
323
324 PsControlRegister501 ps_ctrl{0};
325 ps_ctrl.ps_mode_active = true;
326 ps_ctrl.ps_mode_xxx = true;
327 this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw;
328}
329
331 AlsPsStatusRegister als_status{0};
332 als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
333 if (!als_status.ps_new_data) {
334 return this->ps_readings_;
335 }
336
337 uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get();
338 PsData1Register ps_high;
339 ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get();
340
341 uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low);
342 return val;
343}
344
346 AlsControlRegister501 als_ctrl{0};
347 als_ctrl.als_mode_active = true;
348 als_ctrl.gain = gain;
349 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
350 delay(2);
351
352 AlsControlRegister501 read_als_ctrl{0};
353 read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get();
354 if (read_als_ctrl.gain != gain) {
355 ESP_LOGW(TAG, "Failed to set gain. We will try one more time.");
356 this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw;
357 delay(2);
358 }
359}
360
364 meas.integration_time = time;
365 this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
366 delay(2);
367
368 MeasurementRateRegister501 read_meas{0};
369 read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get();
370 if (read_meas.integration_time != time) {
371 ESP_LOGW(TAG, "Failed to set integration time. We will try one more time.");
372 this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw;
373 delay(2);
374 }
375}
376
378 AlsPsStatusRegister als_status{0};
379 als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get();
380 if (!als_status.als_new_data)
381 return DataAvail::NO_DATA;
382 ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain));
383 if (data.gain != als_status.gain) {
384 ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain));
385 return DataAvail::BAD_DATA;
386 }
387 data.gain = als_status.gain;
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_LOGD(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 // sometimes sensors fail to change sensitivity. this prevents us from infinite loop
409 if (data.number_of_adjustments++ > MAX_SENSITIVITY_ADJUSTMENTS) {
410 ESP_LOGW(TAG, "Too many sensitivity adjustments done. Something wrong with the sensor. Stopping.");
411 return false;
412 }
413
414 ESP_LOGV(TAG, "Adjusting sensitivity, run #%d", data.number_of_adjustments);
415
416 // available combinations of gain and integration times:
417 static const GainTimePair GAIN_TIME_PAIRS[] = {
421 };
422
423 GainTimePair current_pair = {data.gain, data.integration_time};
424
425 // Here comes funky business with this sensor. it has no internal error checking mechanism
426 // as in later versions (LTR-303/329/559/..) and sensor gets overwhelmed when saturated
427 // and readings are strange. We only check high sensitivity mode for now.
428 // Nothing is documented and it is a result of real-world testing.
429 if (data.gain == AlsGain501::GAIN_150) {
430 // when sensor is saturated it returns various crazy numbers
431 // CH1 = 1, CH0 = 0
432 if (data.ch1 == 1 && data.ch0 == 0) {
433 ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = 1, CH0 = 0, Gain 150x");
434 // fake saturation
435 data.ch0 = 0xffff;
436 data.ch1 = 0xffff;
437 } else if (data.ch1 == 65535 && data.ch0 == 0) {
438 ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = 65535, CH0 = 0, Gain 150x");
439 data.ch0 = 0xffff;
440 } else if (data.ch1 > 1000 && data.ch0 == 0) {
441 ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = %d, CH0 = 0, Gain 150x", data.ch1);
442 data.ch0 = 0xffff;
443 }
444 }
445
446 static const uint16_t LOW_INTENSITY_THRESHOLD_1 = 100;
447 static const uint16_t LOW_INTENSITY_THRESHOLD_200 = 2000;
448 static const uint16_t HIGH_INTENSITY_THRESHOLD = 25000;
449
450 if (data.ch0 <= (data.gain == AlsGain501::GAIN_1 ? LOW_INTENSITY_THRESHOLD_1 : LOW_INTENSITY_THRESHOLD_200) ||
451 (data.gain == AlsGain501::GAIN_1 && data.lux < 320)) {
452 GainTimePair next_pair = get_next(GAIN_TIME_PAIRS, current_pair);
453 if (next_pair != current_pair) {
454 data.gain = next_pair.gain;
455 data.integration_time = next_pair.time;
456 ESP_LOGV(TAG, "Low illuminance. Increasing sensitivity.");
457 return true;
458 }
459
460 } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD || data.ch1 >= HIGH_INTENSITY_THRESHOLD) {
461 GainTimePair prev_pair = get_prev(GAIN_TIME_PAIRS, current_pair);
462 if (prev_pair != current_pair) {
463 data.gain = prev_pair.gain;
464 data.integration_time = prev_pair.time;
465 ESP_LOGV(TAG, "High illuminance. Decreasing sensitivity.");
466 return true;
467 }
468 } else {
469 ESP_LOGD(TAG, "Illuminance is good enough.");
470 return false;
471 }
472 ESP_LOGD(TAG, "Can't adjust sensitivity anymore.");
473 return false;
474}
475
477 if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) {
478 ESP_LOGW(TAG, "Sensors got saturated");
479 data.lux = 0.0f;
480 return;
481 }
482
483 if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) {
484 ESP_LOGW(TAG, "Sensors blacked out");
485 data.lux = 0.0f;
486 return;
487 }
488
489 float ch0 = data.ch0;
490 float ch1 = data.ch1;
491 float ratio = ch1 / (ch0 + ch1);
492 float als_gain = get_gain_coeff(data.gain);
493 float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f;
494 float inv_pfactor = this->glass_attenuation_factor_;
495 float lux = 0.0f;
496
497 // method from
498 // https://github.com/fards/Ainol_fire_kernel/blob/83832cf8a3082fd8e963230f4b1984479d1f1a84/customer/drivers/lightsensor/ltr501als.c#L295
499
500 if (ratio < 0.45) {
501 lux = 1.7743 * ch0 + 1.1059 * ch1;
502 } else if (ratio < 0.64) {
503 lux = 3.7725 * ch0 - 1.3363 * ch1;
504 } else if (ratio < 0.85) {
505 lux = 1.6903 * ch0 - 0.1693 * ch1;
506 } else {
507 ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio");
508 lux = 0.0f;
509 }
510
511 lux = inv_pfactor * lux / als_gain / als_time;
512 data.lux = lux;
513
514 ESP_LOGD(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain,
515 als_time, inv_pfactor, lux);
516}
517
519 if (this->proximity_counts_sensor_ != nullptr) {
521 }
522 if (this->ambient_light_sensor_ != nullptr) {
524 }
525 if (this->infrared_counts_sensor_ != nullptr) {
527 }
528 if (this->full_spectrum_counts_sensor_ != nullptr) {
530 }
531}
532
534 if (this->actual_gain_sensor_ != nullptr) {
535 this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain));
536 }
537 if (this->actual_integration_time_sensor_ != nullptr) {
539 }
540}
541} // namespace ltr501
542} // namespace esphome
virtual void mark_failed()
Mark this component as failed.
bool is_failed() const
bool is_ready() const
void status_set_warning(const char *message="unspecified")
void status_clear_warning()
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:72
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:190
I2CRegister reg(uint8_t a_register)
calls the I2CRegister constructor
Definition i2c.h:153
uint8_t get() const
returns the register value
Definition i2c.cpp:75
void configure_gain_(AlsGain501 gain)
Definition ltr501.cpp:345
void publish_data_part_1_(AlsReadings &data)
Definition ltr501.cpp:518
void publish_data_part_2_(AlsReadings &data)
Definition ltr501.cpp:533
void configure_integration_time_(IntegrationTime501 time)
Definition ltr501.cpp:361
sensor::Sensor * actual_integration_time_sensor_
Definition ltr501.h:142
CallbackManager< void()> on_ps_high_trigger_callback_
Definition ltr501.h:158
IntegrationTime501 integration_time_
Definition ltr501.h:126
struct esphome::ltr501::LTRAlsPs501Component::AlsReadings als_readings_
void read_sensor_data_(AlsReadings &data)
Definition ltr501.cpp:391
MeasurementRepeatRate repeat_rate_
Definition ltr501.h:127
CallbackManager< void()> on_ps_low_trigger_callback_
Definition ltr501.h:159
void apply_lux_calculation_(AlsReadings &data)
Definition ltr501.cpp:476
sensor::Sensor * proximity_counts_sensor_
Definition ltr501.h:143
sensor::Sensor * infrared_counts_sensor_
Definition ltr501.h:138
bool are_adjustments_required_(AlsReadings &data)
Definition ltr501.cpp:404
DataAvail is_als_data_ready_(AlsReadings &data)
Definition ltr501.cpp:377
sensor::Sensor * full_spectrum_counts_sensor_
Definition ltr501.h:139
sensor::Sensor * actual_gain_sensor_
Definition ltr501.h:141
sensor::Sensor * ambient_light_sensor_
Definition ltr501.h:140
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:39
AlsGain501 gain
mopeka_std_values val[4]
ErrorCode
Error codes returned by I2CBus and I2CDevice methods.
Definition i2c_bus.h:11
@ ERROR_OK
No error found during execution of method.
Definition i2c_bus.h:13
@ LTR_TYPE_ALS_AND_PS
Definition ltr501.h:20
@ LTR_TYPE_ALS_ONLY
Definition ltr501.h:18
bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs)
Definition ltr501.cpp:25
T get_next(const T(&array)[size], const T val)
Definition ltr501.cpp:29
bool operator==(const GainTimePair &lhs, const GainTimePair &rhs)
Definition ltr501.cpp:21
T get_prev(const T(&array)[size], const T val)
Definition ltr501.cpp:44
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
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:191
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:28
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:27