ESPHome 2026.3.0
Loading...
Searching...
No Matches
ld2420.cpp
Go to the documentation of this file.
1#include "ld2420.h"
4
5/*
6Configure commands - little endian
7
8No command can exceed 64 bytes, otherwise they would need be to be split up into multiple sends.
9
10All send command frames will have:
11 Header = FD FC FB FA, Bytes 0 - 3, uint32_t 0xFAFBFCFD
12 Length, bytes 4 - 5, uint16_t 0x0002, must be at least 2 for the command byte if no addon data.
13 Command bytes 6 - 7, uint16_t
14 Footer = 04 03 02 01 - uint32_t 0x01020304, Always last 4 Bytes.
15Receive
16 Error bytes 8-9 uint16_t, 0 = success, all other positive values = error
17
18Enable config mode:
19Send:
20 UART Tx: FD FC FB FA 04 00 FF 00 02 00 04 03 02 01
21 Command = FF 00 - uint16_t 0x00FF
22 Protocol version = 02 00, can be 1 or 2 - uint16_t 0x0002
23Reply:
24 UART Rx: FD FC FB FA 06 00 FF 01 00 00 02 00 04 03 02 01
25
26Disable config mode:
27Send:
28 UART Tx: FD FC FB FA 02 00 FE 00 04 03 02 01
29 Command = FE 00 - uint16_t 0x00FE
30Receive:
31 UART Rx: FD FC FB FA 04 00 FE 01 00 00 04 03 02 01
32
33Configure system parameters:
34
35UART Tx: FD FC FB FA 08 00 12 00 00 00 64 00 00 00 04 03 02 01 Set system parms
36Command = 12 00 - uint16_t 0x0012, Param
37There are three documented parameters for modes:
38 00 64 = Basic status mode
39 This mode outputs text as presence "ON" or "OFF" and "Range XXXX"
40 where XXXX is a decimal value for distance in cm
41 00 04 = Energy output mode
42 This mode outputs detailed signal energy values for each gate and the target distance.
43 The data format consist of the following.
44 Header HH, Length LL, Presence PP, Distance DD, 16 Gate Energies EE, Footer FF
45 HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF
46 F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5
47 00 00 = debug output mode
48 This mode outputs detailed values consisting of 20 Dopplers, 16 Ranges for a total 20 * 16 * 4 bytes
49 The data format consist of the following.
50 Header HH, Doppler DD, Range RR, Footer FF
51 HH HH HH HH DD DD DD DD .. 20x .. RR RR RR RR .. 16x .. FF FF FF FF
52 AA BF 10 14 00 00 00 00 .. .. .. .. 00 00 00 00 .. .. .. .. FD FC FB FA
53
54Configure gate sensitivity parameters:
55UART Tx: FD FC FB FA 0E 00 07 00 10 00 60 EA 00 00 20 00 60 EA 00 00 04 03 02 01
56Command = 12 00 - uint16_t 0x0007
57Gate 0 high thresh = 10 00 uint16_t 0x0010, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60
58Gate 0 low thresh = 20 00 uint16_t 0x0020, Threshold value = 60 EA 00 00 uint32_t 0x0000EA60
59*/
60
61namespace esphome::ld2420 {
62
63static const char *const TAG = "ld2420";
64
65// Local const's
66static constexpr uint16_t REFRESH_RATE_MS = 1000;
67
68// Command sets
69static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE;
70static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF;
71static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012;
72static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021;
73static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002;
74static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008;
75static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020;
76static constexpr uint16_t CMD_READ_REGISTER = 0x0002;
77static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011;
78static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013;
79static constexpr uint16_t CMD_READ_VERSION = 0x0000;
80static constexpr uint16_t CMD_RESTART = 0x0068;
81static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000;
82static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003;
83static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001;
84static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064;
85static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000;
86static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004;
87static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002;
88static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007;
89static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001;
90static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012;
91
92static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04;
93static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A;
94static constexpr uint8_t CMD_MAX_BYTES = 0x64;
95static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02;
96
97static constexpr uint8_t LD2420_ERROR_NONE = 0x00;
98static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02;
99static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01;
100
101// Register address values
102static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000;
103static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001;
104static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004;
105static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015,
106 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B,
107 0x001C, 0x001D, 0x001E, 0x001F};
108static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025,
109 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B,
110 0x002C, 0x002D, 0x002E, 0x002F};
111static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250,
112 250, 250, 250, 250, 250, 250, 250, 250};
113static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150,
114 150, 100, 100, 100, 100, 100, 100, 100};
115static constexpr uint16_t FACTORY_TIMEOUT = 120;
116static constexpr uint16_t FACTORY_MIN_GATE = 1;
117static constexpr uint16_t FACTORY_MAX_GATE = 12;
118
119// COMMAND_BYTE Header & Footer
120static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304;
121static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD;
122static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD;
123static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA;
124static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8;
125static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4;
126static constexpr int CALIBRATE_VERSION_MIN = 154;
127static constexpr uint8_t CMD_FRAME_COMMAND = 6;
128static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4;
129static constexpr uint8_t CMD_FRAME_STATUS = 7;
130static constexpr uint8_t CMD_ERROR_WORD = 8;
131static constexpr uint8_t ENERGY_SENSOR_START = 9;
132static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4;
133static const char *const OP_NORMAL_MODE_STRING = "Normal";
134static const char *const OP_SIMPLE_MODE_STRING = "Simple";
135
136// Memory-efficient lookup tables
137struct StringToUint8 {
138 const char *str;
139 const uint8_t value;
140};
141
142static constexpr StringToUint8 OP_MODE_BY_STR[] = {
143 {"Normal", OP_NORMAL_MODE},
144 {"Calibrate", OP_CALIBRATE_MODE},
145 {"Simple", OP_SIMPLE_MODE},
146};
147
148static constexpr const char *ERR_MESSAGE[] = {
149 "None",
150 "Unknown",
151 "Timeout",
152};
153
154// Helper function for lookups
155template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
156 for (const auto &entry : arr) {
157 if (str == entry.str) {
158 return entry.value;
159 }
160 }
161 return 0xFF; // Not found
162}
163
164static uint8_t calc_checksum(void *data, size_t size) {
165 uint8_t checksum = 0;
166 uint8_t *data_bytes = (uint8_t *) data;
167 for (size_t i = 0; i < size; i++) {
168 checksum ^= data_bytes[i]; // XOR operation
169 }
170 return checksum;
171}
172
173static int get_firmware_int(const char *version_string) {
174 std::string version_str = version_string;
175 if (version_str[0] == 'v') {
176 version_str.erase(0, 1);
177 }
178 version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end());
179 int version_integer = stoi(version_str);
180 return version_integer;
181}
182
184
186 ESP_LOGCONFIG(TAG,
187 "LD2420:\n"
188 " Firmware version: %7s",
189 this->firmware_ver_);
190#ifdef USE_NUMBER
191 ESP_LOGCONFIG(TAG, "Number:");
192 LOG_NUMBER(" ", "Gate Timeout:", this->gate_timeout_number_);
193 LOG_NUMBER(" ", "Gate Max Distance:", this->max_gate_distance_number_);
194 LOG_NUMBER(" ", "Gate Min Distance:", this->min_gate_distance_number_);
195 LOG_NUMBER(" ", "Gate Select:", this->gate_select_number_);
196 for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
197 LOG_NUMBER(" ", "Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]);
198 LOG_NUMBER(" ", "Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]);
199 }
200#endif
201#ifdef USE_BUTTON
202 LOG_BUTTON(" ", "Apply Config:", this->apply_config_button_);
203 LOG_BUTTON(" ", "Revert Edits:", this->revert_config_button_);
204 LOG_BUTTON(" ", "Factory Reset:", this->factory_reset_button_);
205 LOG_BUTTON(" ", "Restart Module:", this->restart_module_button_);
206#endif
207#ifdef USE_SELECT
208 ESP_LOGCONFIG(TAG, "Select:");
209 LOG_SELECT(" ", "Operating Mode", this->operating_selector_);
210#endif
211 if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
212 ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
213 }
214}
215
217 if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
218 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
219 this->mark_failed();
220 return;
221 }
223#ifdef USE_NUMBER
225#endif
226 this->get_firmware_version_();
227 const char *pfw = this->firmware_ver_;
228 std::string fw_str(pfw);
229
230 for (auto &listener : this->listeners_) {
231 listener->on_fw_version(fw_str);
232 }
233
234 for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
236 this->get_gate_threshold_(gate);
237 }
238
239 memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
240 if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
241 this->set_operating_mode(OP_SIMPLE_MODE_STRING);
242#ifdef USE_SELECT
243 if (this->operating_selector_ != nullptr) {
244 this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
245 }
246#endif
247 this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
248 ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
249 } else {
250 this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
251#ifdef USE_SELECT
252 if (this->operating_selector_ != nullptr) {
253 this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
254 }
255#endif
256 }
257#ifdef USE_NUMBER
259#endif
260 this->set_system_mode(this->system_mode_);
261 this->set_config_mode(false);
262}
263
265 const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config));
266 if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) {
267 ESP_LOGD(TAG, "No configuration change detected");
268 return;
269 }
270 ESP_LOGD(TAG, "Reconfiguring");
271 if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
272 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
273 this->mark_failed();
274 return;
275 }
276 this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout);
277 for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
279 this->set_gate_threshold(gate);
280 }
281 memcpy(&current_config, &new_config, sizeof(new_config));
282#ifdef USE_NUMBER
284#endif
285 this->set_system_mode(this->system_mode_);
286 this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm
287 this->set_operating_mode(OP_NORMAL_MODE_STRING);
288}
289
291 ESP_LOGD(TAG, "Setting factory defaults");
292 if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
293 ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
294 this->mark_failed();
295 return;
296 }
297 this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT);
298#ifdef USE_NUMBER
299 this->gate_timeout_number_->state = FACTORY_TIMEOUT;
300 this->min_gate_distance_number_->state = FACTORY_MIN_GATE;
301 this->max_gate_distance_number_->state = FACTORY_MAX_GATE;
302#endif
303 for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
304 this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate];
305 this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate];
307 this->set_gate_threshold(gate);
308 }
309 memcpy(&this->current_config, &this->new_config, sizeof(this->new_config));
310 this->set_system_mode(this->system_mode_);
311 this->set_config_mode(false);
312#ifdef USE_NUMBER
315#endif
316}
317
319 ESP_LOGD(TAG, "Restarting");
320 this->send_module_restart();
321 this->set_timeout(250, [this]() {
322 this->set_config_mode(true);
323 this->set_system_mode(this->system_mode_);
324 this->set_config_mode(false);
325 });
326}
327
329 memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
330#ifdef USE_NUMBER
332#endif
333 ESP_LOGD(TAG, "Reverted config number edits");
334}
335
337 // If there is a active send command do not process it here, the send command call will handle it.
338 if (this->cmd_active_) {
339 return;
340 }
341 this->read_batch_(this->buffer_data_);
342}
343
344void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
345 for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) {
346 this->radar_data[gate][sample_number] = gate_energy[gate];
347 }
349}
350
352 // Calculate average and peak values for each gate
353 const float move_factor = gate_move_sensitivity_factor + 1;
354 const float still_factor = (gate_still_sensitivity_factor / 2) + 1;
355 for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) {
356 uint32_t sum = 0;
357 uint16_t peak = 0;
358
359 for (uint8_t sample_number = 0; sample_number < CALIBRATE_SAMPLES; ++sample_number) {
360 // Calculate average
361 sum += this->radar_data[gate][sample_number];
362
363 // Calculate max value
364 if (this->radar_data[gate][sample_number] > peak) {
365 peak = this->radar_data[gate][sample_number];
366 }
367 }
368
369 // Store average and peak values
370 this->gate_avg[gate] = sum / CALIBRATE_SAMPLES;
371 if (this->gate_peak[gate] < peak) {
372 this->gate_peak[gate] = peak;
373 }
374
375 uint32_t calculated_value =
376 (static_cast<uint32_t>(this->gate_peak[gate]) + (move_factor * static_cast<uint32_t>(this->gate_peak[gate])));
377 this->new_config.move_thresh[gate] = static_cast<uint16_t>(calculated_value <= 65535 ? calculated_value : 65535);
378 calculated_value =
379 (static_cast<uint32_t>(this->gate_peak[gate]) + (still_factor * static_cast<uint32_t>(this->gate_peak[gate])));
380 this->new_config.still_thresh[gate] = static_cast<uint16_t>(calculated_value <= 65535 ? calculated_value : 65535);
381 }
382}
383
385 for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) {
386 // Output results
387 ESP_LOGI(TAG, "Gate: %2d Avg: %5d Peak: %5d", gate, this->gate_avg[gate], this->gate_peak[gate]);
388 }
389 ESP_LOGI(TAG, "Total samples: %d", this->total_sample_number_counter);
390}
391
393 // If unsupported firmware ignore mode select
394 if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) {
395 this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state);
396 // Entering Auto Calibrate we need to clear the previous data collection
397#ifdef USE_SELECT
398 if (this->operating_selector_ != nullptr) {
400 }
401#endif
403 this->set_calibration_(true);
404 for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
405 this->gate_avg[gate] = 0;
406 this->gate_peak[gate] = 0;
407 for (uint8_t i = 0; i < CALIBRATE_SAMPLES; i++) {
408 this->radar_data[gate][i] = 0;
409 }
411 }
412 } else {
413 // Set the current data back so we don't have new data that can be applied in error.
414 if (this->get_calibration_()) {
415 memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
416 }
417 this->set_calibration_(false);
418 }
419 } else {
421#ifdef USE_SELECT
422 if (this->operating_selector_ != nullptr) {
423 this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
424 }
425#endif
426 }
427}
428
429void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) {
430 if (rx_data < 0) {
431 return; // No data available
432 }
433 if (this->buffer_pos_ < len - 1) {
434 buffer[this->buffer_pos_++] = rx_data;
435 buffer[this->buffer_pos_] = 0;
436 } else {
437 // We should never get here, but just in case...
438 ESP_LOGW(TAG, "Max command length exceeded; ignoring");
439 this->buffer_pos_ = 0;
440 }
441 if (this->buffer_pos_ < 4) {
442 return; // Not enough data to process yet
443 }
444 if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) {
445 this->cmd_active_ = false; // Set command state to inactive after response
446 this->handle_ack_data_(buffer, this->buffer_pos_);
447 this->buffer_pos_ = 0;
448 } else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) &&
449 (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) {
450 this->handle_simple_mode_(buffer, this->buffer_pos_);
451 this->buffer_pos_ = 0;
452 } else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) &&
453 (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) {
454 this->handle_energy_mode_(buffer, this->buffer_pos_);
455 this->buffer_pos_ = 0;
456 }
457}
458
459void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) {
460 uint8_t index = 6; // Start at presence byte position
461 uint16_t range;
462 const uint8_t elements = sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]);
463 if (len < static_cast<int>(index + 1 + sizeof(range) + elements * sizeof(this->gate_energy_[0]))) {
464 ESP_LOGW(TAG, "Energy frame too short: %d bytes", len);
465 return;
466 }
467 this->set_presence_(buffer[index]);
468 index++;
469 memcpy(&range, &buffer[index], sizeof(range));
470 index += sizeof(range);
471 this->set_distance_(range);
472 for (uint8_t i = 0; i < elements; i++) { // NOLINT
473 memcpy(&this->gate_energy_[i], &buffer[index], sizeof(this->gate_energy_[0]));
474 index += sizeof(this->gate_energy_[0]);
475 }
476
479 this->sample_number_counter++;
480 if (this->sample_number_counter >= CALIBRATE_SAMPLES) {
481 this->sample_number_counter = 0;
482 }
483 }
484
485 // Resonable refresh rate for home assistant database size health
486 const int32_t current_millis = App.get_loop_component_start_time();
487 if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) {
488 return;
489 }
490 this->last_periodic_millis = current_millis;
491 for (auto &listener : this->listeners_) {
492 listener->on_distance(this->get_distance_());
493 listener->on_presence(this->get_presence_());
494 listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]));
495 }
496
499 if (current_millis - this->report_periodic_millis > REFRESH_RATE_MS * CALIBRATE_REPORT_INTERVAL) {
500 this->report_periodic_millis = current_millis;
501 this->report_gate_data();
502 }
503 }
504}
505
506void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
507 const uint8_t bufsize = 16;
508 uint8_t index{0};
509 uint8_t pos{0};
510 char *endptr{nullptr};
511 char outbuf[bufsize]{0};
512 while (true) {
513 if (pos >= 2 && inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') {
514 this->set_presence_(false);
515 } else if (pos >= 1 && inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') {
516 this->set_presence_(true);
517 }
518 if (inbuf[pos] >= '0' && inbuf[pos] <= '9') {
519 if (index < bufsize - 1) {
520 outbuf[index++] = inbuf[pos];
521 }
522 }
523 if (pos < len - 1) {
524 pos++;
525 } else {
526 break;
527 }
528 }
529 outbuf[index] = '\0';
530 if (index > 1) {
531 this->set_distance_(strtol(outbuf, &endptr, 10));
532 }
533
534 if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
535 // Resonable refresh rate for home assistant database size health
536 const int32_t current_millis = App.get_loop_component_start_time();
537 if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) {
538 return;
539 }
540 this->last_normal_periodic_millis = current_millis;
541 for (auto &listener : this->listeners_)
542 listener->on_distance(this->get_distance_());
543 for (auto &listener : this->listeners_)
544 listener->on_presence(this->get_presence_());
545 }
546}
547
548void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
549 // Read all available bytes in batches to reduce UART call overhead.
550 size_t avail = this->available();
551 uint8_t buf[MAX_LINE_LENGTH];
552 while (avail > 0) {
553 size_t to_read = std::min(avail, sizeof(buf));
554 if (!this->read_array(buf, to_read)) {
555 break;
556 }
557 avail -= to_read;
558
559 for (size_t i = 0; i < to_read; i++) {
560 this->readline_(buf[i], buffer.data(), buffer.size());
561 }
562 }
563}
564
565void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
566 this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
567 this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
568 uint16_t data_pos = 0;
569 if (this->cmd_reply_.length > CMD_MAX_BYTES) {
570 ESP_LOGW(TAG, "Reply frame too long");
571 return;
572 } else if (this->cmd_reply_.length < 2) {
573 ESP_LOGW(TAG, "Command frame too short");
574 return;
575 }
576 memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error));
577 const char *result = this->cmd_reply_.error ? "failure" : "success";
578 if (this->cmd_reply_.error > 0) {
579 return;
580 };
581 this->cmd_reply_.ack = true;
582 switch ((uint16_t) this->cmd_reply_.command) {
583 case (CMD_ENABLE_CONF):
584 ESP_LOGV(TAG, "Set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result);
585 break;
586 case (CMD_DISABLE_CONF):
587 ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result);
588 break;
589 case (CMD_READ_REGISTER): {
590 ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result);
591 // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file
592 data_pos = 0x0A;
593 uint16_t reg_count = std::min<uint16_t>((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_REG_DATA_REPLY_SIZE,
594 sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0]));
595 for (uint16_t i = 0; i < reg_count; i++) {
596 memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_REG_DATA_REPLY_SIZE], CMD_REG_DATA_REPLY_SIZE);
597 }
598 break;
599 }
600 case (CMD_WRITE_REGISTER):
601 ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result);
602 break;
603 case (CMD_WRITE_ABD_PARAM):
604 ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result);
605 break;
606 case (CMD_READ_ABD_PARAM): {
607 ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result);
608 data_pos = CMD_ABD_DATA_REPLY_START;
609 uint16_t abd_count = std::min<uint16_t>((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE,
610 sizeof(this->cmd_reply_.data) / sizeof(this->cmd_reply_.data[0]));
611 for (uint16_t i = 0; i < abd_count; i++) {
612 memcpy(&this->cmd_reply_.data[i], &buffer[data_pos + i * CMD_ABD_DATA_REPLY_SIZE],
613 sizeof(this->cmd_reply_.data[i]));
614 }
615 break;
616 }
617 case (CMD_WRITE_SYS_PARAM):
618 ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result);
619 break;
620 case (CMD_READ_VERSION): {
621 uint8_t ver_len = std::min<uint8_t>(buffer[10], sizeof(this->firmware_ver_) - 1);
622 memcpy(this->firmware_ver_, &buffer[12], ver_len);
623 this->firmware_ver_[ver_len] = '\0';
624 ESP_LOGV(TAG, "Firmware version: %s %s", this->firmware_ver_, result);
625 break;
626 }
627 default:
628 break;
629 }
630}
631
633 uint32_t start_millis = millis();
634 uint8_t error = 0;
635 uint8_t ack_buffer[MAX_LINE_LENGTH];
636 uint8_t cmd_buffer[MAX_LINE_LENGTH];
637 this->cmd_reply_.ack = false;
638 if (frame.command != CMD_RESTART) {
639 this->cmd_active_ = true;
640 } // Restart does not reply, thus no ack state required
641 uint8_t retry = 3;
642 while (retry) {
643 frame.length = 0;
644 uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size
645
646 memcpy(&cmd_buffer[frame.length], &frame.header, sizeof(frame.header));
647 frame.length += sizeof(frame.header);
648
649 memcpy(&cmd_buffer[frame.length], &frame_data_bytes, sizeof(frame.data_length));
650 frame.length += sizeof(frame.data_length);
651
652 memcpy(&cmd_buffer[frame.length], &frame.command, sizeof(frame.command));
653 frame.length += sizeof(frame.command);
654
655 for (uint16_t index = 0; index < frame.data_length; index++) {
656 memcpy(&cmd_buffer[frame.length], &frame.data[index], sizeof(frame.data[index]));
657 frame.length += sizeof(frame.data[index]);
658 }
659
660 memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
661 frame.length += sizeof(frame.footer);
662 this->write_array(cmd_buffer, frame.length);
663
664 error = 0;
665 if (frame.command == CMD_RESTART) {
666 return 0; // restart does not reply exit now
667 }
668
669 while (!this->cmd_reply_.ack) {
670 while (this->available()) {
671 this->readline_(this->read(), ack_buffer, sizeof(ack_buffer));
672 }
674 // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
675 if ((millis() - start_millis) > 1000) {
676 start_millis = millis();
677 error = LD2420_ERROR_TIMEOUT;
678 retry--;
679 break;
680 }
681 }
682 if (this->cmd_reply_.ack) {
683 retry = 0;
684 }
685 if (this->cmd_reply_.error > 0) {
686 this->handle_cmd_error(error);
687 }
688 }
689 return error;
690}
691
693 CmdFrameT cmd_frame;
694 cmd_frame.data_length = 0;
695 cmd_frame.header = CMD_FRAME_HEADER;
696 cmd_frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
697 if (enable) {
698 memcpy(&cmd_frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER));
699 cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
700 }
701 cmd_frame.footer = CMD_FRAME_FOOTER;
702 ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
703 return this->send_cmd_from_array(cmd_frame);
704}
705
706// Sends a restart and set system running mode to normal
708
710 CmdFrameT cmd_frame;
711 cmd_frame.data_length = 0;
712 cmd_frame.header = CMD_FRAME_HEADER;
713 cmd_frame.command = CMD_RESTART;
714 cmd_frame.footer = CMD_FRAME_FOOTER;
715 ESP_LOGV(TAG, "Sending restart command: %2X", cmd_frame.command);
716 this->send_cmd_from_array(cmd_frame);
717}
718
720 CmdFrameT cmd_frame;
721 cmd_frame.data_length = 0;
722 cmd_frame.header = CMD_FRAME_HEADER;
723 cmd_frame.command = CMD_READ_REGISTER;
724 cmd_frame.data[1] = reg;
725 cmd_frame.data_length += 2;
726 cmd_frame.footer = CMD_FRAME_FOOTER;
727 ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command);
728 this->send_cmd_from_array(cmd_frame);
729}
730
731void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
732 CmdFrameT cmd_frame;
733 cmd_frame.data_length = 0;
734 cmd_frame.header = CMD_FRAME_HEADER;
735 cmd_frame.command = CMD_WRITE_REGISTER;
736 memcpy(&cmd_frame.data[cmd_frame.data_length], &reg, CMD_REG_DATA_REPLY_SIZE);
737 cmd_frame.data_length += 2;
738 memcpy(&cmd_frame.data[cmd_frame.data_length], &value, CMD_REG_DATA_REPLY_SIZE);
739 cmd_frame.data_length += 2;
740 cmd_frame.footer = CMD_FRAME_FOOTER;
741 ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value);
742 this->send_cmd_from_array(cmd_frame);
743}
744
745void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGE(TAG, "Command failed: %s", ERR_MESSAGE[error]); }
746
748 uint8_t error;
749 CmdFrameT cmd_frame;
750 cmd_frame.data_length = 0;
751 cmd_frame.header = CMD_FRAME_HEADER;
752 cmd_frame.command = CMD_READ_ABD_PARAM;
753 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate]));
754 cmd_frame.data_length += 2;
755 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
756 cmd_frame.data_length += 2;
757 cmd_frame.footer = CMD_FRAME_FOOTER;
758 ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command);
759 error = this->send_cmd_from_array(cmd_frame);
760 if (error == 0) {
761 this->current_config.move_thresh[gate] = cmd_reply_.data[0];
763 }
764 return error;
765}
766
768 uint8_t error;
769 CmdFrameT cmd_frame;
770 cmd_frame.data_length = 0;
771 cmd_frame.header = CMD_FRAME_HEADER;
772 cmd_frame.command = CMD_READ_ABD_PARAM;
773 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG,
774 sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number
775 cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG);
776 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG,
777 sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number
778 cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG);
779 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG,
780 sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
781 cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
782 cmd_frame.footer = CMD_FRAME_FOOTER;
783 ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command);
784 error = this->send_cmd_from_array(cmd_frame);
785 if (error == 0) {
786 this->current_config.min_gate = (uint16_t) cmd_reply_.data[0];
787 this->current_config.max_gate = (uint16_t) cmd_reply_.data[1];
788 this->current_config.timeout = (uint16_t) cmd_reply_.data[2];
789 }
790 return error;
791}
792
794 CmdFrameT cmd_frame;
795 uint16_t unknown_parm = 0x0000;
796 cmd_frame.data_length = 0;
797 cmd_frame.header = CMD_FRAME_HEADER;
798 cmd_frame.command = CMD_WRITE_SYS_PARAM;
799 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE));
800 cmd_frame.data_length += sizeof(CMD_SYSTEM_MODE);
801 memcpy(&cmd_frame.data[cmd_frame.data_length], &mode, sizeof(mode));
802 cmd_frame.data_length += sizeof(mode);
803 memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm));
804 cmd_frame.data_length += sizeof(unknown_parm);
805 cmd_frame.footer = CMD_FRAME_FOOTER;
806 ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
807 if (this->send_cmd_from_array(cmd_frame) == 0) {
808 this->set_mode_(mode);
809 }
810}
811
813 CmdFrameT cmd_frame;
814 cmd_frame.data_length = 0;
815 cmd_frame.header = CMD_FRAME_HEADER;
816 cmd_frame.command = CMD_READ_VERSION;
817 cmd_frame.footer = CMD_FRAME_FOOTER;
818
819 ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command);
820 this->send_cmd_from_array(cmd_frame);
821}
822
823void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, // NOLINT
824 uint32_t timeout) {
825 // Header H, Length L, Register R, Value V, Footer F
826 // |Min Gate |Max Gate |Timeout |
827 // HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF
828 // FD FC FB FA 14 00 07 00 00 00 01 00 00 00 01 00 09 00 00 00 04 00 0A 00 00 00 04 03 02 01 e.g.
829
830 CmdFrameT cmd_frame;
831 cmd_frame.data_length = 0;
832 cmd_frame.header = CMD_FRAME_HEADER;
833 cmd_frame.command = CMD_WRITE_ABD_PARAM;
834 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG,
835 sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number
836 cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG);
837 memcpy(&cmd_frame.data[cmd_frame.data_length], &min_gate_distance, sizeof(min_gate_distance));
838 cmd_frame.data_length += sizeof(min_gate_distance);
839 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG,
840 sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number
841 cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG);
842 memcpy(&cmd_frame.data[cmd_frame.data_length], &max_gate_distance, sizeof(max_gate_distance));
843 cmd_frame.data_length += sizeof(max_gate_distance);
844 memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG,
845 sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
846 cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
847 memcpy(&cmd_frame.data[cmd_frame.data_length], &timeout, sizeof(timeout));
848 ;
849 cmd_frame.data_length += sizeof(timeout);
850 cmd_frame.footer = CMD_FRAME_FOOTER;
851
852 ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command);
853 this->send_cmd_from_array(cmd_frame);
854}
855
857 // Header H, Length L, Command C, Register R, Value V, Footer F
858 // HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF
859 // FD FC FB FA 14 00 07 00 10 00 00 FF 00 00 00 01 00 0F 00 00 04 03 02 01
860
861 uint16_t move_threshold_gate = CMD_GATE_MOVE_THRESH[gate];
862 uint16_t still_threshold_gate = CMD_GATE_STILL_THRESH[gate];
863 CmdFrameT cmd_frame;
864 cmd_frame.data_length = 0;
865 cmd_frame.header = CMD_FRAME_HEADER;
866 cmd_frame.command = CMD_WRITE_ABD_PARAM;
867 memcpy(&cmd_frame.data[cmd_frame.data_length], &move_threshold_gate, sizeof(move_threshold_gate));
868 cmd_frame.data_length += sizeof(move_threshold_gate);
869 memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.move_thresh[gate],
870 sizeof(this->new_config.move_thresh[gate]));
871 cmd_frame.data_length += sizeof(this->new_config.move_thresh[gate]);
872 memcpy(&cmd_frame.data[cmd_frame.data_length], &still_threshold_gate, sizeof(still_threshold_gate));
873 cmd_frame.data_length += sizeof(still_threshold_gate);
874 memcpy(&cmd_frame.data[cmd_frame.data_length], &this->new_config.still_thresh[gate],
875 sizeof(this->new_config.still_thresh[gate]));
876 cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]);
877 cmd_frame.footer = CMD_FRAME_FOOTER;
878 ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command);
879 this->send_cmd_from_array(cmd_frame);
880}
881
882#ifdef USE_NUMBER
884 if (this->gate_timeout_number_ != nullptr) {
885 this->gate_timeout_number_->publish_state(static_cast<uint16_t>(this->current_config.timeout));
886 }
887 if (this->gate_select_number_ != nullptr) {
889 }
890 if (this->min_gate_distance_number_ != nullptr) {
891 this->min_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.min_gate));
892 }
893 if (this->max_gate_distance_number_ != nullptr) {
894 this->max_gate_distance_number_->publish_state(static_cast<uint16_t>(this->current_config.max_gate));
895 }
896 if (this->gate_move_sensitivity_factor_number_ != nullptr) {
898 }
899 if (this->gate_still_sensitivity_factor_number_ != nullptr) {
901 }
902 for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
903 if (this->gate_still_threshold_numbers_[gate] != nullptr) {
904 this->gate_still_threshold_numbers_[gate]->publish_state(
905 static_cast<uint16_t>(this->current_config.still_thresh[gate]));
906 }
907 if (this->gate_move_threshold_numbers_[gate] != nullptr) {
908 this->gate_move_threshold_numbers_[gate]->publish_state(
909 static_cast<uint16_t>(this->current_config.move_thresh[gate]));
910 }
911 }
912}
913
919
920#endif
921
922} // namespace esphome::ld2420
BedjetMode mode
BedJet operating mode.
uint8_t checksum
Definition bl0906.h:3
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:451
void readline_(int rx_data, uint8_t *buffer, int len)
Definition ld2420.cpp:429
void set_system_mode(uint16_t mode)
Definition ld2420.cpp:793
void handle_ack_data_(uint8_t *buffer, int len)
Definition ld2420.cpp:565
std::vector< number::Number * > gate_still_threshold_numbers_
Definition ld2420.h:182
number::Number * gate_select_number_
Definition ld2420.h:177
uint8_t set_config_mode(bool enable)
Definition ld2420.cpp:692
button::Button * factory_reset_button_
Definition ld2420.h:144
void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number)
Definition ld2420.cpp:344
float get_setup_priority() const override
Definition ld2420.cpp:183
void set_distance_(uint16_t distance)
Definition ld2420.h:166
void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout)
Definition ld2420.cpp:823
void set_gate_threshold(uint8_t gate)
Definition ld2420.cpp:856
void handle_energy_mode_(uint8_t *buffer, int len)
Definition ld2420.cpp:459
button::Button * revert_config_button_
Definition ld2420.h:142
void set_calibration_(bool state)
Definition ld2420.h:172
number::Number * min_gate_distance_number_
Definition ld2420.h:178
button::Button * apply_config_button_
Definition ld2420.h:141
uint16_t gate_peak[TOTAL_GATES]
Definition ld2420.h:130
std::vector< LD2420Listener * > listeners_
Definition ld2420.h:196
std::vector< number::Number * > gate_move_threshold_numbers_
Definition ld2420.h:183
void set_mode_(uint16_t mode)
Definition ld2420.h:162
uint16_t gate_avg[TOTAL_GATES]
Definition ld2420.h:129
void read_batch_(std::span< uint8_t, MAX_LINE_LENGTH > buffer)
Definition ld2420.cpp:548
uint8_t buffer_data_[MAX_LINE_LENGTH]
Definition ld2420.h:190
int get_gate_threshold_(uint8_t gate)
Definition ld2420.cpp:747
number::Number * gate_still_sensitivity_factor_number_
Definition ld2420.h:181
void handle_cmd_error(uint8_t error)
Definition ld2420.cpp:745
int send_cmd_from_array(CmdFrameT cmd_frame)
Definition ld2420.cpp:632
void get_reg_value_(uint16_t reg)
Definition ld2420.cpp:719
uint16_t gate_energy_[TOTAL_GATES]
Definition ld2420.h:188
void handle_simple_mode_(const uint8_t *inbuf, int len)
Definition ld2420.cpp:506
number::Number * max_gate_distance_number_
Definition ld2420.h:179
select::Select * operating_selector_
Definition ld2420.h:138
button::Button * restart_module_button_
Definition ld2420.h:143
number::Number * gate_timeout_number_
Definition ld2420.h:176
void set_operating_mode(const char *state)
Definition ld2420.cpp:392
uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]
Definition ld2420.h:128
void set_presence_(bool presence)
Definition ld2420.h:164
number::Number * gate_move_sensitivity_factor_number_
Definition ld2420.h:180
void set_reg_value(uint16_t reg, uint16_t value)
Definition ld2420.cpp:731
void publish_state(float state)
Definition number.cpp:22
void publish_state(const std::string &state)
Definition select.cpp:11
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
bool state
Definition fan.h:2
Range range
Definition msa3xx.h:0
@ OP_CALIBRATE_MODE
Definition ld2420.h:31
uint8_t find_uint8(const StringToUint8(&arr)[N], const std::string &str)
Definition ld2420.cpp:155
constexpr float BUS
For communication buses like i2c/spi.
Definition component.h:25
std::string size_t len
Definition helpers.h:892
size_t size
Definition helpers.h:929
size_t size_t pos
Definition helpers.h:929
void IRAM_ATTR HOT delay_microseconds_safe(uint32_t us)
Delay for the given amount of microseconds, possibly yielding to other processes during the wait.
Definition helpers.cpp:843
int stoi(const StringRef &str, size_t *pos=nullptr, int base=10)
Definition string_ref.h:227
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t