ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ld2412.cpp
Go to the documentation of this file.
1#include "ld2412.h"
2
3#ifdef USE_NUMBER
5#endif
6#ifdef USE_SENSOR
8#endif
9
12
13#include <algorithm>
14#include <cmath>
15
16namespace esphome::ld2412 {
17
18ESPHOME_LOG_TAG(TAG, "ld2412");
19
30
36
42
43enum OutPinLevel : uint8_t {
46};
47
48/*
49Data Type: 6th byte
50Target states: 9th byte
51 Moving target distance: 10~11th bytes
52 Moving target energy: 12th byte
53 Still target distance: 13~14th bytes
54 Still target energy: 15th byte
55 Detect distance: 16~17th bytes
56*/
71
72enum PeriodicDataValue : uint8_t {
73 HEADER = 0XAA,
74 FOOTER = 0x55,
75 CHECK = 0x00,
76};
77
78enum AckData : uint8_t {
79 FRAME_LENGTH = 4, // little-endian count of the bytes between it and the footer
83};
84
85// Memory-efficient lookup tables
86struct StringToUint8 {
87 const char *str;
88 const uint8_t value;
89};
90
91struct Uint8ToString {
92 const uint8_t value;
93 const char *str;
94};
95
96constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
97 {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
98 {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
99 {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
100};
101
102constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
103 {"0.2m", DISTANCE_RESOLUTION_0_2},
104 {"0.5m", DISTANCE_RESOLUTION_0_5},
105 {"0.75m", DISTANCE_RESOLUTION_0_75},
106};
107
108constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
109 {DISTANCE_RESOLUTION_0_2, "0.2m"},
110 {DISTANCE_RESOLUTION_0_5, "0.5m"},
111 {DISTANCE_RESOLUTION_0_75, "0.75m"},
112};
113
114constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
115 {"off", LIGHT_FUNCTION_OFF},
116 {"below", LIGHT_FUNCTION_BELOW},
117 {"above", LIGHT_FUNCTION_ABOVE},
118};
119
120constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
121 {LIGHT_FUNCTION_OFF, "off"},
122 {LIGHT_FUNCTION_BELOW, "below"},
123 {LIGHT_FUNCTION_ABOVE, "above"},
124};
125
126constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
127 {"low", OUT_PIN_LEVEL_LOW},
128 {"high", OUT_PIN_LEVEL_HIGH},
129};
130
131constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
132 {OUT_PIN_LEVEL_LOW, "low"},
133 {OUT_PIN_LEVEL_HIGH, "high"},
134};
135
136constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
137
138// Helper functions for lookups
139template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
140 for (const auto &entry : arr) {
141 if (strcmp(str, entry.str) == 0) {
142 return entry.value;
143 }
144 }
145 return 0xFF; // Not found
146}
147
148template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
149 for (const auto &entry : arr) {
150 if (value == entry.value) {
151 return entry.str;
152 }
153 }
154 return ""; // Not found
155}
156
157static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined
158// Commands
159static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
160static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
161static constexpr uint8_t CMD_ENABLE_ENG = 0x62;
162static constexpr uint8_t CMD_DISABLE_ENG = 0x63;
163static constexpr uint8_t CMD_QUERY_BASIC_CONF = 0x12;
164static constexpr uint8_t CMD_BASIC_CONF = 0x02;
165static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
166static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x11;
167static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x01;
168static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0x1C;
169static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0x0C;
170static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
171static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
172static constexpr uint8_t CMD_FACTORY_RESET = 0xA2;
173static constexpr uint8_t CMD_RESTART = 0xA3;
174static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
175static constexpr uint8_t CMD_DYNAMIC_BACKGROUND_CORRECTION = 0x0B;
176static constexpr uint8_t CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION = 0x1B;
177static constexpr uint8_t CMD_MOTION_GATE_SENS = 0x03;
178static constexpr uint8_t CMD_QUERY_MOTION_GATE_SENS = 0x13;
179static constexpr uint8_t CMD_STATIC_GATE_SENS = 0x04;
180static constexpr uint8_t CMD_QUERY_STATIC_GATE_SENS = 0x14;
181static constexpr uint8_t CMD_NONE = 0x00;
182// Commands values
183static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00;
184static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01;
185static constexpr uint8_t CMD_DURATION_VALUE = 0x02;
186// Bitmasks for target states
187static constexpr uint8_t MOVE_BITMASK = 0x01;
188static constexpr uint8_t STILL_BITMASK = 0x02;
189// Header & Footer size
190static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
191// Command Header & Footer
192static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
193static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
194// Data Header & Footer
195static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
196static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
197// MAC address the module uses when Bluetooth is disabled
198static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
199static constexpr uint8_t GATE_SENS_ACK_SIZE = ACK_PAYLOAD + TOTAL_GATES + HEADER_FOOTER_SIZE;
200// The length field counts the command word, the status word and the gates
201static constexpr uint16_t GATE_SENS_ACK_LENGTH = GATE_SENS_ACK_SIZE - COMMAND - HEADER_FOOTER_SIZE;
202
203static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
204 return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
205}
206
207void LD2412Component::dump_config() {
208 char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
209 char version_s[20];
210 const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
211 ld24xx::format_version_str(this->version_, version_s);
212 ESP_LOGCONFIG(TAG,
213 "LD2412:\n"
214 " Firmware version: %s\n"
215 " MAC address: %s",
216 version_s, mac_str);
217#ifdef USE_BINARY_SENSOR
218 ESP_LOGCONFIG(TAG, "Binary Sensors:");
219 LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus",
220 this->dynamic_background_correction_status_binary_sensor_);
221 LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
222 LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_);
223 LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
224#endif
225#ifdef USE_SENSOR
226 ESP_LOGCONFIG(TAG, "Sensors:");
227 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_);
228 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_);
229 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_);
230 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_);
231 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_);
232 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_);
233 for (auto &s : this->gate_still_sensors_) {
234 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s);
235 }
236 for (auto &s : this->gate_move_sensors_) {
237 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s);
238 }
239#endif
240#ifdef USE_TEXT_SENSOR
241 ESP_LOGCONFIG(TAG, "Text Sensors:");
242 LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_);
243 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
244#endif
245#ifdef USE_NUMBER
246 ESP_LOGCONFIG(TAG, "Numbers:");
247 LOG_NUMBER(" ", "LightThreshold", this->light_threshold_number_);
248 LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_);
249 LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_);
250 LOG_NUMBER(" ", "Timeout", this->timeout_number_);
251 for (number::Number *n : this->gate_move_thresholds_.numbers) {
252 LOG_NUMBER(" ", "Move Thresholds", n);
253 }
254 for (number::Number *n : this->gate_still_thresholds_.numbers) {
255 LOG_NUMBER(" ", "Still Thresholds", n);
256 }
257#endif
258#ifdef USE_SELECT
259 ESP_LOGCONFIG(TAG, "Selects:");
260 LOG_SELECT(" ", "BaudRate", this->baud_rate_select_);
261 LOG_SELECT(" ", "DistanceResolution", this->distance_resolution_select_);
262 LOG_SELECT(" ", "LightFunction", this->light_function_select_);
263 LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_);
264#endif
265#ifdef USE_SWITCH
266 ESP_LOGCONFIG(TAG, "Switches:");
267 LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_);
268 LOG_SWITCH(" ", "EngineeringMode", this->engineering_mode_switch_);
269#endif
270#ifdef USE_BUTTON
271 ESP_LOGCONFIG(TAG, "Buttons:");
272 LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
273 LOG_BUTTON(" ", "Query", this->query_button_);
274 LOG_BUTTON(" ", "Restart", this->restart_button_);
275 LOG_BUTTON(" ", "StartDynamicBackgroundCorrection", this->start_dynamic_background_correction_button_);
276#endif
277}
278
280 ESP_LOGCONFIG(TAG, "Running setup");
281 this->read_all_info();
282}
283
284void LD2412Component::read_all_info() {
285 this->set_config_mode_(true);
286 this->get_version_();
287 delay(10); // NOLINT
288 this->get_mac_();
289 delay(10); // NOLINT
291 delay(10); // NOLINT
292 this->query_parameters_();
293 delay(10); // NOLINT
295 delay(10); // NOLINT
296 this->query_light_control_();
297 delay(10); // NOLINT
298#ifdef USE_NUMBER
299 this->get_gate_threshold();
300 delay(10); // NOLINT
301#endif
302 this->set_config_mode_(false);
303#ifdef USE_SELECT
304 if (this->baud_rate_select_ != nullptr) {
305 if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) {
306 this->baud_rate_select_->publish_state(*index);
307 }
308 }
309#endif
310}
311
312void LD2412Component::restart_and_read_all_info() {
313 this->set_config_mode_(true);
314 this->restart_();
315 this->set_timeout(1000, [this]() { this->read_all_info(); });
316}
317
318void LD2412Component::loop() {
319 // Read all available bytes in batches to reduce UART call overhead.
320 size_t avail = this->available();
321 uint8_t buf[MAX_LINE_LENGTH];
322 while (avail > 0) {
323 size_t to_read = std::min(avail, sizeof(buf));
324 if (!this->read_array(buf, to_read)) {
325 break;
326 }
327 avail -= to_read;
328
329 for (size_t i = 0; i < to_read; i++) {
330 this->readline_(buf[i]);
331 }
332 }
333}
334
335void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
336 ESP_LOGV(TAG, "Sending COMMAND %02X", command);
337 // frame header bytes
338 this->write_array(CMD_FRAME_HEADER, HEADER_FOOTER_SIZE);
339 // length bytes
340 uint8_t len = 2;
341 if (command_value != nullptr) {
342 len += command_value_len;
343 }
344 // 2 length bytes (low, high) + 2 command bytes (low, high)
345 uint8_t len_cmd[] = {len, 0x00, command, 0x00};
346 this->write_array(len_cmd, sizeof(len_cmd));
347
348 // command value bytes
349 if (command_value != nullptr) {
350 this->write_array(command_value, command_value_len);
351 }
352 // frame footer bytes
353 this->write_array(CMD_FRAME_FOOTER, HEADER_FOOTER_SIZE);
354
355 if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) {
356 delay(30); // NOLINT
357 }
358 delay(20); // NOLINT
359}
360
362 // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes
363 // data header=0xAA, data footer=0x55, crc=0x00
364 if (this->buffer_pos_ < 12 || !ld2412::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
365 this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER) {
366 return;
367 }
368 /*
369 Data Type: 7th
370 0x01: Engineering mode
371 0x02: Normal mode
372 */
373 bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01;
374#ifdef USE_SWITCH
375 if (this->engineering_mode_switch_ != nullptr) {
376 this->engineering_mode_switch_->publish_state(engineering_mode);
377 }
378#endif
379
380#ifdef USE_BINARY_SENSOR
381 /*
382 Target states: 9th
383 0x00 = No target
384 0x01 = Moving targets
385 0x02 = Still targets
386 0x03 = Moving+Still targets
387 */
388 char target_state = this->buffer_data_[TARGET_STATES];
389 if (this->target_binary_sensor_ != nullptr) {
390 this->target_binary_sensor_->publish_state(target_state != 0x00);
391 }
392 if (this->moving_target_binary_sensor_ != nullptr) {
393 this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK);
394 }
395 if (this->still_target_binary_sensor_ != nullptr) {
396 this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK);
397 }
398#endif
399 /*
400 Moving target distance: 10~11th bytes
401 Moving target energy: 12th byte
402 Still target distance: 13~14th bytes
403 Still target energy: 15th byte
404 Detect distance: 16~17th bytes
405 */
406#ifdef USE_SENSOR
407 SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
409 SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
410 SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
412 SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
413 if (this->detection_distance_sensor_.has_sensor()) {
414 int new_detect_distance = 0;
415 if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
416 new_detect_distance =
417 encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]);
418 } else if (target_state != 0x00) {
419 new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]);
420 }
421 this->detection_distance_sensor_.publish_state_if_not_dup(new_detect_distance);
422 }
423 if (engineering_mode) {
424 // Engineering mode needs at least LIGHT_SENSOR + 1 bytes
425 if (this->buffer_pos_ < LIGHT_SENSOR + 1) {
426 ESP_LOGW(TAG, "Engineering mode packet too short: %u", this->buffer_pos_);
427 } else {
428 /*
429 Moving distance range: 18th byte
430 Still distance range: 19th byte
431 Moving energy: 20~28th bytes
432 */
433 for (uint8_t i = 0; i < TOTAL_GATES; i++) {
434 SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
435 }
436 /*
437 Still energy: 29~37th bytes
438 */
439 for (uint8_t i = 0; i < TOTAL_GATES; i++) {
440 SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
441 }
442 /*
443 Light sensor value
444 */
445 SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
446 }
447 } else {
448 for (auto &gate_move_sensor : this->gate_move_sensors_) {
449 SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
450 }
451 for (auto &gate_still_sensor : this->gate_still_sensors_) {
452 SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
453 }
454 SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
455 }
456#endif
457 // the radar module won't tell us when it's done, so we just have to keep polling...
459 this->set_config_mode_(true);
461 this->set_config_mode_(false);
462 }
463}
464
465#ifdef USE_NUMBER
466void set_number_value(number::Number *n, float value) {
467 if (n != nullptr && (!n->has_state() || n->state != value)) {
468 n->publish_state(value);
469 }
470}
471
472// NaN marks a value the module never received
473static inline bool has_value(number::Number *n) { return n != nullptr && n->has_state() && !std::isnan(n->state); }
474
475// Timeout ids 0 (still) and 1 (motion) for a write waiting on a re-query
476static constexpr uint32_t GATE_QUERY_TIMEOUT_ID = 0;
477static constexpr uint32_t GATE_QUERY_TIMEOUT_MS = 2000;
478#endif
479
481 ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
482 if (this->buffer_pos_ < 10) {
483 ESP_LOGW(TAG, "Invalid length");
484 return true;
485 }
486 if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
487 char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)];
488 ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE));
489 return true;
490 }
491 if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
492 ESP_LOGW(TAG, "Invalid status");
493 return true;
494 }
495 if (this->buffer_data_[8] || this->buffer_data_[9]) {
496 ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
497 return true;
498 }
499
500 switch (this->buffer_data_[COMMAND]) {
501 case CMD_ENABLE_CONF:
502 ESP_LOGV(TAG, "Enable conf");
503 break;
504
505 case CMD_DISABLE_CONF:
506 ESP_LOGV(TAG, "Disabled conf");
507 break;
508
509 case CMD_SET_BAUD_RATE:
510 ESP_LOGV(TAG, "Baud rate change");
511#ifdef USE_SELECT
512 if (this->baud_rate_select_ != nullptr) {
513 auto baud = this->baud_rate_select_->current_option();
514 ESP_LOGW(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
515 }
516#endif
517 break;
518
519 case CMD_QUERY_VERSION: {
520 if (this->buffer_pos_ < 12 + sizeof(this->version_)) {
521 return false;
522 }
523 std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
524 char version_s[20];
525 ld24xx::format_version_str(this->version_, version_s);
526 ESP_LOGV(TAG, "Firmware version: %s", version_s);
527#ifdef USE_TEXT_SENSOR
528 if (this->version_text_sensor_ != nullptr) {
529 this->version_text_sensor_->publish_state(version_s);
530 }
531#endif
532 break;
533 }
534 case CMD_QUERY_DISTANCE_RESOLUTION: {
535 const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
536 ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
537#ifdef USE_SELECT
538 if (this->distance_resolution_select_ != nullptr) {
539 this->distance_resolution_select_->publish_state(distance_resolution);
540 }
541#endif
542 break;
543 }
544
545 case CMD_QUERY_LIGHT_CONTROL: {
546 this->light_function_ = this->buffer_data_[10];
547 this->light_threshold_ = this->buffer_data_[11];
548 const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
549 ESP_LOGV(TAG, "Light function: %s, threshold: %u", light_function_str, this->light_threshold_);
550#ifdef USE_SELECT
551 if (this->light_function_select_ != nullptr) {
552 this->light_function_select_->publish_state(light_function_str);
553 }
554#endif
555#ifdef USE_NUMBER
556 if (this->light_threshold_number_ != nullptr) {
557 this->light_threshold_number_->publish_state(static_cast<float>(this->light_threshold_));
558 }
559#endif
560 break;
561 }
562
563 case CMD_QUERY_MAC_ADDRESS: {
564 if (this->buffer_pos_ < 20) {
565 return false;
566 }
567
568 this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
569 if (this->bluetooth_on_) {
570 std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
571 }
572
573 char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
574 const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
575 ESP_LOGV(TAG, "MAC address: %s", mac_str);
576#ifdef USE_TEXT_SENSOR
577 if (this->mac_text_sensor_ != nullptr) {
578 this->mac_text_sensor_->publish_state(mac_str);
579 }
580#endif
581#ifdef USE_SWITCH
582 if (this->bluetooth_switch_ != nullptr) {
583 this->bluetooth_switch_->publish_state(this->bluetooth_on_);
584 }
585#endif
586 break;
587 }
588
589 case CMD_SET_DISTANCE_RESOLUTION:
590 ESP_LOGV(TAG, "Handled set distance resolution command");
591 break;
592
593 case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: {
594 ESP_LOGV(TAG, "Handled query dynamic background correction");
595 bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00);
596#ifdef USE_BINARY_SENSOR
597 if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) {
598 this->dynamic_background_correction_status_binary_sensor_->publish_state(dynamic_background_correction_active);
599 }
600#endif
601 this->dynamic_background_correction_active_ = dynamic_background_correction_active;
602 break;
603 }
604
605 case CMD_BLUETOOTH:
606 ESP_LOGV(TAG, "Handled bluetooth command");
607 break;
608
609 case CMD_SET_LIGHT_CONTROL:
610 ESP_LOGV(TAG, "Handled set light control command");
611 break;
612
613 case CMD_QUERY_MOTION_GATE_SENS:
614 case CMD_QUERY_STATIC_GATE_SENS: {
615#ifdef USE_NUMBER
616 const uint16_t declared = encode_uint16(this->buffer_data_[FRAME_LENGTH + 1], this->buffer_data_[FRAME_LENGTH]);
617 if (declared != GATE_SENS_ACK_LENGTH || this->buffer_pos_ > GATE_SENS_ACK_SIZE) {
618 // A truncated answer merged with the next frame, or a length this driver does not know
619 ESP_LOGW(TAG, "Dropping gate threshold answer of %u bytes", this->buffer_pos_);
620 return true;
621 }
622 if (this->buffer_pos_ < GATE_SENS_ACK_SIZE) {
623 return false; // the footer pattern appeared inside the payload, keep reading
624 }
625 const bool motion = this->buffer_data_[COMMAND] == CMD_QUERY_MOTION_GATE_SENS;
626 GateThresholds &group = this->gate_thresholds_(motion);
627 // True only while a held-back write waits for this answer
628 const bool pending = this->cancel_timeout(GATE_QUERY_TIMEOUT_ID + motion);
629 for (size_t i = 0; i < TOTAL_GATES; i++) {
630 group.last_read[i] = this->buffer_data_[ACK_PAYLOAD + i];
631 // The pending write's user values must survive the answer
632 if (!pending || !has_value(group.numbers[i])) {
633 set_number_value(group.numbers[i], group.last_read[i]);
634 }
635 }
636 group.read = true;
637 if (pending) {
638 this->set_config_mode_(true);
639 this->send_gate_thresholds_(motion);
640 this->set_config_mode_(false);
641 }
642#endif
643 break;
644 }
645
646 case CMD_QUERY_BASIC_CONF: // Query parameters response
647 {
648#ifdef USE_NUMBER
649 if (this->buffer_pos_ < 15) {
650 return false;
651 }
652 /*
653 Moving distance range: 9th byte
654 Still distance range: 10th byte
655 */
656 set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]);
657 set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1);
658 ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10],
659 this->buffer_data_[11]);
660 /*
661 None Duration: 11~12th bytes
662 */
663 set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
664 ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
665 /*
666 Output pin configuration: 13th bytes
667 */
668 this->out_pin_level_ = this->buffer_data_[14];
669#ifdef USE_SELECT
670 const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
671 if (this->out_pin_level_select_ != nullptr) {
672 this->out_pin_level_select_->publish_state(out_pin_level_str);
673 }
674#endif
675#endif
676 } break;
677 default:
678 break;
679 }
680
681 return true;
682}
683
685 if (readch < 0) {
686 return; // No data available
687 }
688 if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] &&
689 readch != CMD_FRAME_HEADER[this->buffer_pos_]) {
690 this->buffer_pos_ = 0;
691 return;
692 }
693 if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
694 this->buffer_data_[this->buffer_pos_++] = readch;
695 this->buffer_data_[this->buffer_pos_] = 0;
696 } else {
697 // We should never get here, but just in case...
698 ESP_LOGW(TAG, "Max command length exceeded; ignoring");
699 this->buffer_pos_ = 0;
700 }
701 if (this->buffer_pos_ < 4) {
702 return; // Not enough data to process yet
703 }
704 if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
705 ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Periodic Data: %s");
706 this->handle_periodic_data_();
707 this->buffer_pos_ = 0; // Reset position index for next message
708 } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
709 ESP_LOGV_HEX(TAG, this->buffer_data_, this->buffer_pos_, MAX_LINE_LENGTH, "Handling Ack Data: %s");
710 if (this->handle_ack_data_()) {
711 this->buffer_pos_ = 0; // Reset position index for next message
712 } else {
713 ESP_LOGV(TAG, "Ack Data incomplete");
714 }
715 }
716}
717
719 const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
720 const uint8_t cmd_value[2] = {0x01, 0x00};
721 this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
722}
723
724void LD2412Component::set_bluetooth(bool enable) {
725 this->set_config_mode_(true);
726 const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
727 this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
728 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
729}
730
731void LD2412Component::set_distance_resolution(const char *state) {
732 this->set_config_mode_(true);
733 const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00};
734 this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
735 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
736}
737
738void LD2412Component::set_baud_rate(const char *state) {
739 this->set_config_mode_(true);
740 const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
741 this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
742 this->set_timeout(200, [this]() { this->restart_(); });
743}
744
746 this->send_command_(CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0);
747}
748
749void LD2412Component::start_dynamic_background_correction() {
751 return; // Already in progress
752 }
753#ifdef USE_BINARY_SENSOR
754 if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) {
755 this->dynamic_background_correction_status_binary_sensor_->publish_state(true);
756 }
757#endif
759 this->set_config_mode_(true);
760 this->send_command_(CMD_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0);
761 this->set_config_mode_(false);
762}
763
764void LD2412Component::set_engineering_mode(bool enable) {
765 const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG;
766 this->set_config_mode_(true);
767 this->send_command_(cmd, nullptr, 0);
768 this->set_config_mode_(false);
769}
770
771void LD2412Component::factory_reset() {
772 this->set_config_mode_(true);
773 this->send_command_(CMD_FACTORY_RESET, nullptr, 0);
774#ifdef USE_NUMBER
775 // Reset discards the thresholds; the next write must wait for the re-read
776 this->gate_move_thresholds_.read = false;
777 this->gate_still_thresholds_.read = false;
778#endif
779 this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); });
780}
781
782void LD2412Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
783
784void LD2412Component::query_parameters_() { this->send_command_(CMD_QUERY_BASIC_CONF, nullptr, 0); }
785
786void LD2412Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
787
789 const uint8_t cmd_value[2] = {0x01, 0x00};
790 this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value));
791}
792
793void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); }
794
795void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
796
797void LD2412Component::set_basic_config() {
798 uint8_t min_gate = 1;
799 uint8_t max_gate = TOTAL_GATES;
800 uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
801 uint8_t out_pin_level = 0x01;
802
803#ifdef USE_NUMBER
804 if (this->min_distance_gate_number_ != nullptr) {
805 if (!this->min_distance_gate_number_->has_state())
806 return;
807 min_gate = static_cast<int>(this->min_distance_gate_number_->state);
808 }
809 if (this->max_distance_gate_number_ != nullptr) {
810 if (!this->max_distance_gate_number_->has_state())
811 return;
812 max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1;
813 }
814 if (this->timeout_number_ != nullptr) {
815 if (!this->timeout_number_->has_state())
816 return;
817 timeout = static_cast<int>(this->timeout_number_->state);
818 }
819#endif
820#ifdef USE_SELECT
821 if (this->out_pin_level_select_ != nullptr) {
822 if (!this->out_pin_level_select_->has_state())
823 return;
824 out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
825 }
826#endif
827
828 uint8_t value[5] = {
829 lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
830 };
831 this->set_config_mode_(true);
832 this->send_command_(CMD_BASIC_CONF, value, sizeof(value));
833 this->set_config_mode_(false);
834}
835
836#ifdef USE_NUMBER
838 GateThresholds &group = this->gate_thresholds_(motion);
839 const uint8_t command = motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS;
840 uint8_t value[TOTAL_GATES];
841 uint8_t configured = 0;
842 for (uint8_t i = 0; i < TOTAL_GATES; i++) {
843 number::Number *n = group.numbers[i];
844 if (has_value(n)) {
845 value[i] = static_cast<uint8_t>(n->state);
846 configured++;
847 } else {
848 value[i] = group.last_read[i];
849 }
850 }
851 if (configured < TOTAL_GATES && !group.read) {
852 // Unread gates would go out as zero, which is maximum sensitivity
853 ESP_LOGW(TAG, "Command %02X held back until the module reports its gate thresholds", command);
854 this->send_command_(motion ? CMD_QUERY_MOTION_GATE_SENS : CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
855 this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, GATE_QUERY_TIMEOUT_MS,
856 [this, motion]() { this->gate_query_timeout_(motion); });
857 return;
858 }
859 this->send_command_(command, value, sizeof(value));
860}
861
863 ESP_LOGE(TAG, "Command %02X not sent, the module did not report its gate thresholds",
864 motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS);
865 // The values on hand never reached the module or are stale, so forgetting them loses nothing
866 for (number::Number *n : this->gate_thresholds_(motion).numbers) {
867 if (n != nullptr) {
868 n->publish_state(NAN);
869 }
870 }
871}
872
873void LD2412Component::set_gate_threshold(number::Number *n) {
874 // Found by pointer: 14 compares per write beat a flag per number
875 const auto &motion_numbers = this->gate_move_thresholds_.numbers;
876 const bool motion = std::find(motion_numbers.begin(), motion_numbers.end(), n) != motion_numbers.end();
877 this->set_config_mode_(true);
878 this->send_gate_thresholds_(motion);
879 this->set_config_mode_(false);
880}
881
882void LD2412Component::get_gate_threshold() {
883 this->send_command_(CMD_QUERY_MOTION_GATE_SENS, nullptr, 0);
884 this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
885}
886
887#endif
888
889void LD2412Component::set_light_out_control() {
890#ifdef USE_NUMBER
891 if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) {
892 this->light_threshold_ = static_cast<uint8_t>(this->light_threshold_number_->state);
893 }
894#endif
895#ifdef USE_SELECT
896 if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
897 this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
898 }
899#endif
900 uint8_t value[2] = {this->light_function_, this->light_threshold_};
901 this->set_config_mode_(true);
902 this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value));
903 this->query_light_control_();
904 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
905}
906
907#ifdef USE_SENSOR
908void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
909 this->gate_move_sensors_[gate].set_sensor(s);
910}
911void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
912 this->gate_still_sensors_[gate].set_sensor(s);
913}
914#endif
915
916} // namespace esphome::ld2412
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
bool cancel_timeout(const char *name)
Cancel a timeout function.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
bool has_state() const
void set_config_mode_(bool enable)
Definition ld2412.cpp:718
void send_gate_thresholds_(bool motion)
Definition ld2412.cpp:837
std::array< SensorWithDedup< uint8_t >, TOTAL_GATES > gate_move_sensors_
Definition ld2412.h:151
GateThresholds & gate_thresholds_(bool motion)
Definition ld2412.h:130
void gate_query_timeout_(bool motion)
Definition ld2412.cpp:862
GateThresholds gate_still_thresholds_
Definition ld2412.h:148
std::array< SensorWithDedup< uint8_t >, TOTAL_GATES > gate_still_sensors_
Definition ld2412.h:152
uint8_t buffer_data_[MAX_LINE_LENGTH]
Definition ld2412.h:141
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len)
Definition ld2412.cpp:335
uint8_t mac_address_[MAC_ADDRESS_SIZE]
Definition ld2412.h:142
GateThresholds gate_move_thresholds_
Definition ld2412.h:147
Base-class for all numbers.
Definition number.h:29
void publish_state(float state)
Definition number.cpp:20
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:43
UARTComponent * parent_
Definition uart.h:134
void write_array(const uint8_t *data, size_t len)
Definition uart.h:30
bool state
Definition fan.h:2
uint32_t buffer
Definition gif.cpp:50
uint8_t find_uint8(const StringToUint8(&arr)[N], const char *str)
Definition ld2412.cpp:139
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[]
Definition ld2412.cpp:131
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[]
Definition ld2412.cpp:120
@ DISTANCE_RESOLUTION_0_5
Definition ld2412.cpp:33
@ DISTANCE_RESOLUTION_0_2
Definition ld2412.cpp:32
@ DISTANCE_RESOLUTION_0_75
Definition ld2412.cpp:34
ESPHOME_LOG_TAG(TAG, "ld2412")
const char * find_str(const Uint8ToString(&arr)[N], uint8_t value)
Definition ld2412.cpp:148
constexpr uint32_t BAUD_RATES[]
Definition ld2412.cpp:136
constexpr StringToUint8 BAUD_RATES_BY_STR[]
Definition ld2412.cpp:96
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[]
Definition ld2412.cpp:114
constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[]
Definition ld2412.cpp:102
void set_number_value(number::Number *n, float value)
Definition ld2412.cpp:466
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[]
Definition ld2412.cpp:108
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[]
Definition ld2412.cpp:126
void format_version_str(const uint8_t *version, std::span< char, 20 > buffer)
Definition ld24xx.h:51
const char * format_mac_str(const uint8_t *mac_address, std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buffer)
Definition ld24xx.h:41
optional< size_t > find_index(const uint32_t(&arr)[N], uint32_t value)
Definition ld24xx.h:28
value[LAYOUT.day_index()]
const void size_t len
Definition hal.h:68
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:498
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1609
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:998
void HOT delay(uint32_t ms)
Definition hal.cpp:85
static void uint32_t
std::array< uint8_t, TOTAL_GATES > last_read
Definition ld2412.h:127
std::array< number::Number *, TOTAL_GATES > numbers
Definition ld2412.h:126