ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
ld2450.cpp
Go to the documentation of this file.
1#include "ld2450.h"
2
3#ifdef USE_NUMBER
5#endif
6#ifdef USE_SENSOR
8#endif
12
13#include <cinttypes>
14#include <cmath>
15#include <numbers>
16
17namespace esphome::ld2450 {
18
19static const char *const TAG = "ld2450";
20
31
32enum ZoneType : uint8_t {
36};
37
38enum PeriodicData : uint8_t {
43};
44
45enum PeriodicDataValue : uint8_t {
46 HEADER = 0xAA,
47 FOOTER = 0x55,
48 CHECK = 0x00,
49};
50
51enum AckData : uint8_t {
54};
55
56// Memory-efficient lookup tables
57struct StringToUint8 {
58 const char *str;
59 const uint8_t value;
60};
61
62struct Uint8ToString {
63 const uint8_t value;
64 const char *str;
65};
66
67constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
68 {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
69 {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
70 {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
71};
72
73constexpr Uint8ToString DIRECTION_BY_UINT[] = {
74 {DIRECTION_APPROACHING, "Approaching"},
75 {DIRECTION_MOVING_AWAY, "Moving away"},
76 {DIRECTION_STATIONARY, "Stationary"},
77 {DIRECTION_NA, "NA"},
78};
79
80constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
81 {ZONE_DISABLED, "Disabled"},
82 {ZONE_DETECTION, "Detection"},
83 {ZONE_FILTER, "Filter"},
84};
85
86constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
87 {"Disabled", ZONE_DISABLED},
88 {"Detection", ZONE_DETECTION},
89 {"Filter", ZONE_FILTER},
90};
91
92// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup
93constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
94
95// Helper functions for lookups
96template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
97 for (const auto &entry : arr) {
98 if (str == entry.str)
99 return entry.value;
100 }
101 return 0xFF; // Not found
102}
103
104template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
105 for (const auto &entry : arr) {
106 if (value == entry.value)
107 return entry.str;
108 }
109 return ""; // Not found
110}
111
112// LD2450 UART Serial Commands
113static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
114static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
115static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
116static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
117static constexpr uint8_t CMD_RESET = 0xA2;
118static constexpr uint8_t CMD_RESTART = 0xA3;
119static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
120static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80;
121static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90;
122static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91;
123static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
124static constexpr uint8_t CMD_QUERY_ZONE = 0xC1;
125static constexpr uint8_t CMD_SET_ZONE = 0xC2;
126// Header & Footer size
127static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
128// Command Header & Footer
129static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
130static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
131// Data Header & Footer
132static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00};
133static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC};
134// MAC address the module uses when Bluetooth is disabled
135static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
136
137static inline uint32_t convert_seconds_to_ms(uint16_t value) { return (uint32_t) value * 1000; };
138
139static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
140 for (uint8_t i = 0; i < 4; i++) {
141 uint16_t val = values[i] & 0xFFFF;
142 bytes[i * 2] = val & 0xFF; // Store low byte first (little-endian)
143 bytes[i * 2 + 1] = (val >> 8) & 0xFF; // Store high byte second
144 }
145}
146
147static inline int16_t decode_coordinate(uint8_t low_byte, uint8_t high_byte) {
148 int16_t coordinate = (high_byte & 0x7F) << 8 | low_byte;
149 if ((high_byte & 0x80) == 0) {
150 coordinate = -coordinate;
151 }
152 return coordinate; // mm
153}
154
155static inline int16_t decode_speed(uint8_t low_byte, uint8_t high_byte) {
156 int16_t speed = (high_byte & 0x7F) << 8 | low_byte;
157 if ((high_byte & 0x80) == 0) {
158 speed = -speed;
159 }
160 return speed * 10; // mm/s
161}
162
163static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) {
164 uint16_t hex_val = (buffer[offset + 1] << 8) | buffer[offset];
165 int16_t dec_val = static_cast<int16_t>(hex_val);
166 if (dec_val & 0x8000) {
167 dec_val -= 65536;
168 }
169 return dec_val;
170}
171
172static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
173 return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
174}
175
177#ifdef USE_NUMBER
178 if (this->presence_timeout_number_ != nullptr) {
179 this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
180 this->set_presence_timeout();
181 }
182#endif
183 this->restart_and_read_all_info();
184}
185
186void LD2450Component::dump_config() {
187 char mac_s[18];
188 char version_s[20];
189 const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
190 ld24xx::format_version_str(this->version_, version_s);
191 ESP_LOGCONFIG(TAG,
192 "LD2450:\n"
193 " Firmware version: %s\n"
194 " MAC address: %s",
195 version_s, mac_str);
196#ifdef USE_BINARY_SENSOR
197 ESP_LOGCONFIG(TAG, "Binary Sensors:");
198 LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
199 LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_);
200 LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
201#endif
202#ifdef USE_SENSOR
203 ESP_LOGCONFIG(TAG, "Sensors:");
204 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetCount", this->moving_target_count_sensor_);
205 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetCount", this->still_target_count_sensor_);
206 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetCount", this->target_count_sensor_);
207 for (auto &s : this->move_x_sensors_) {
208 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetX", s);
209 }
210 for (auto &s : this->move_y_sensors_) {
211 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetY", s);
212 }
213 for (auto &s : this->move_angle_sensors_) {
214 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetAngle", s);
215 }
216 for (auto &s : this->move_distance_sensors_) {
217 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetDistance", s);
218 }
219 for (auto &s : this->move_resolution_sensors_) {
220 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetResolution", s);
221 }
222 for (auto &s : this->move_speed_sensors_) {
223 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetSpeed", s);
224 }
225 for (auto &s : this->zone_target_count_sensors_) {
226 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneTargetCount", s);
227 }
228 for (auto &s : this->zone_moving_target_count_sensors_) {
229 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneMovingTargetCount", s);
230 }
231 for (auto &s : this->zone_still_target_count_sensors_) {
232 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneStillTargetCount", s);
233 }
234#endif
235#ifdef USE_TEXT_SENSOR
236 ESP_LOGCONFIG(TAG, "Text Sensors:");
237 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
238 LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_);
239 for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
240 LOG_TEXT_SENSOR(" ", "Direction", s);
241 }
242#endif
243#ifdef USE_NUMBER
244 ESP_LOGCONFIG(TAG, "Numbers:");
245 LOG_NUMBER(" ", "PresenceTimeout", this->presence_timeout_number_);
246 for (auto n : this->zone_numbers_) {
247 LOG_NUMBER(" ", "ZoneX1", n.x1);
248 LOG_NUMBER(" ", "ZoneY1", n.y1);
249 LOG_NUMBER(" ", "ZoneX2", n.x2);
250 LOG_NUMBER(" ", "ZoneY2", n.y2);
251 }
252#endif
253#ifdef USE_SELECT
254 ESP_LOGCONFIG(TAG, "Selects:");
255 LOG_SELECT(" ", "BaudRate", this->baud_rate_select_);
256 LOG_SELECT(" ", "ZoneType", this->zone_type_select_);
257#endif
258#ifdef USE_SWITCH
259 ESP_LOGCONFIG(TAG, "Switches:");
260 LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_);
261 LOG_SWITCH(" ", "MultiTarget", this->multi_target_switch_);
262#endif
263#ifdef USE_BUTTON
264 ESP_LOGCONFIG(TAG, "Buttons:");
265 LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
266 LOG_BUTTON(" ", "Restart", this->restart_button_);
267#endif
268}
269
270void LD2450Component::loop() {
271 // Read all available bytes in batches to reduce UART call overhead.
272 size_t avail = this->available();
273 uint8_t buf[MAX_LINE_LENGTH];
274 while (avail > 0) {
275 size_t to_read = std::min(avail, sizeof(buf));
276 if (!this->read_array(buf, to_read)) {
277 break;
278 }
279 avail -= to_read;
280
281 for (size_t i = 0; i < to_read; i++) {
282 this->readline_(buf[i]);
283 }
284 }
285}
286
287// Count targets in zone (single pass for both still and moving)
288void LD2450Component::count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving) {
289 still = 0;
290 moving = 0;
291 for (auto &target : this->target_info_) {
292 if (target.x > zone.x1 && target.x < zone.x2 && target.y > zone.y1 && target.y < zone.y2) {
293 if (target.is_moving) {
294 moving++;
295 } else {
296 still++;
297 }
298 }
299 }
300}
301
302// Service reset_radar_zone
303void LD2450Component::reset_radar_zone() {
304 this->zone_type_ = 0;
305 for (auto &i : this->zone_config_) {
306 i.x1 = 0;
307 i.y1 = 0;
308 i.x2 = 0;
309 i.y2 = 0;
310 }
312}
313
314void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2,
315 int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2,
316 int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2,
317 int32_t zone3_y2) {
318 this->zone_type_ = zone_type;
319 int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
320 zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
321 for (uint8_t i = 0; i < MAX_ZONES; i++) {
322 this->zone_config_[i].x1 = zone_parameters[i * 4];
323 this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
324 this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
325 this->zone_config_[i].y2 = zone_parameters[i * 4 + 3];
326 }
328}
329
330// Set Zone on LD2450 Sensor
332 uint8_t cmd_value[26] = {};
333 uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
334 uint8_t area_config[24] = {};
335 for (uint8_t i = 0; i < MAX_ZONES; i++) {
336 int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
337 this->zone_config_[i].y2};
338 ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
339 }
340 std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
341 std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
342 this->set_config_mode_(true);
343 this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
344 this->set_config_mode_(false);
345}
346
347// Check presense timeout to reset presence status
349 if (check_millis == 0) {
350 return true;
351 }
352 if (this->timeout_ == 0) {
353 this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
354 }
355 return App.get_loop_component_start_time() - check_millis >= this->timeout_;
356}
357
358// Extract, store and publish zone details LD2450 buffer
360 uint8_t index, start;
361 for (index = 0; index < MAX_ZONES; index++) {
362 start = 12 + index * 8;
363 this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
364 this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
365 this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
366 this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
367#ifdef USE_NUMBER
368 // only one null check as all coordinates are required for a single zone
369 if (this->zone_numbers_[index].x1 != nullptr) {
370 this->zone_numbers_[index].x1->publish_state(this->zone_config_[index].x1);
371 this->zone_numbers_[index].y1->publish_state(this->zone_config_[index].y1);
372 this->zone_numbers_[index].x2->publish_state(this->zone_config_[index].x2);
373 this->zone_numbers_[index].y2->publish_state(this->zone_config_[index].y2);
374 }
375#endif
376 }
377}
378
379// Read all info from LD2450 buffer
380void LD2450Component::read_all_info() {
381 this->set_config_mode_(true);
382 this->get_version_();
383 this->get_mac_();
385 this->query_zone_();
386 this->set_config_mode_(false);
387#ifdef USE_SELECT
388 if (this->baud_rate_select_ != nullptr) {
389 if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) {
390 this->baud_rate_select_->publish_state(*index);
391 }
392 }
393 this->publish_zone_type();
394#endif
395}
396
397// Read zone info from LD2450 buffer
398void LD2450Component::query_zone_info() {
399 this->set_config_mode_(true);
400 this->query_zone_();
401 this->set_config_mode_(false);
402}
403
404// Restart LD2450 and read all info from buffer
405void LD2450Component::restart_and_read_all_info() {
406 this->set_config_mode_(true);
407 this->restart_();
408 this->set_timeout(1500, [this]() { this->read_all_info(); });
409}
410
411// Send command with values to LD2450
412void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
413 ESP_LOGV(TAG, "Sending COMMAND %02X", command);
414 // frame header bytes
415 this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
416 // length bytes
417 uint8_t len = 2;
418 if (command_value != nullptr) {
419 len += command_value_len;
420 }
421 // 2 length bytes (low, high) + 2 command bytes (low, high)
422 uint8_t len_cmd[] = {len, 0x00, command, 0x00};
423 this->write_array(len_cmd, sizeof(len_cmd));
424 // command value bytes
425 if (command_value != nullptr) {
426 this->write_array(command_value, command_value_len);
427 }
428 // frame footer bytes
429 this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
430
431 if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) {
432 delay(50); // NOLINT
433 }
434}
435
436// LD2450 Radar data message:
437// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
438// Header Target 1 Target 2 Target 3 End
440 if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
441 ESP_LOGE(TAG, "Invalid length");
442 return;
443 }
444 if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
445 this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
446 this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
447 ESP_LOGE(TAG, "Invalid header/footer");
448 return;
449 }
450
451 int16_t target_count = 0;
452 int16_t still_target_count = 0;
453 int16_t moving_target_count = 0;
454 int16_t res = 0;
455 int16_t start = 0;
456 int16_t tx = 0;
457 int16_t ty = 0;
458 int16_t td = 0;
459 int16_t ts = 0;
460 float angle = 0;
461 uint8_t index = 0;
463 bool is_moving = false;
464
465#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
466 // Loop thru targets
467 for (index = 0; index < MAX_TARGETS; index++) {
468#ifdef USE_SENSOR
469 // X
470 start = TARGET_X + index * 8;
471 is_moving = false;
472 // tx is used for further calculations, so always needs to be populated
473 tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
474 // Y
475 start = TARGET_Y + index * 8;
476 ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
477 // RESOLUTION
478 start = TARGET_RESOLUTION + index * 8;
479 res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
480#endif
481 // SPEED
482 start = TARGET_SPEED + index * 8;
483 ts = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
484 if (ts) {
485 is_moving = true;
486 moving_target_count++;
487 }
488 // DISTANCE
489 // Optimized: use already decoded tx and ty values, replace pow() with multiplication
490 int32_t x_squared = (int32_t) tx * tx;
491 int32_t y_squared = (int32_t) ty * ty;
492 td = (uint16_t) sqrtf(x_squared + y_squared);
493 if (td > 0) {
494 target_count++;
495 }
496#ifdef USE_SENSOR
497 if (td == 0) {
498 SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_x_sensors_[index]);
499 SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_y_sensors_[index]);
500 SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_resolution_sensors_[index]);
501 SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_speed_sensors_[index]);
502 SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_distance_sensors_[index]);
503 SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_angle_sensors_[index]);
504 } else {
505 SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx);
506 SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty);
507 SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res);
508 SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts);
509 SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
510 // ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed
511 angle = atan2f(static_cast<float>(-tx), static_cast<float>(ty)) * (180.0f / std::numbers::pi_v<float>);
512 SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
513 }
514#endif
515#ifdef USE_TEXT_SENSOR
516 // DIRECTION
517 if (td == 0) {
519 } else if (ts > 0) {
521 } else if (ts < 0) {
523 } else {
525 }
526 if (this->direction_dedup_[index].next(direction)) {
527 text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
528 if (tsd != nullptr) {
530 }
531 }
532#endif
533
534 // Store target info for zone target count. Zero out untracked targets (td==0)
535 // so stale coordinates don't produce ghost counts in count_targets_in_zone_().
536 this->target_info_[index].x = (td > 0) ? tx : 0;
537 this->target_info_[index].y = (td > 0) ? ty : 0;
538 this->target_info_[index].is_moving = (td > 0) && is_moving;
539
540 } // End loop thru targets
541
542 still_target_count = target_count - moving_target_count;
543#endif
544
545#ifdef USE_SENSOR
546 // Loop thru zones
547 uint8_t zone_still_targets = 0;
548 uint8_t zone_moving_targets = 0;
549 uint8_t zone_all_targets = 0;
550 for (index = 0; index < MAX_ZONES; index++) {
551 this->count_targets_in_zone_(this->zone_config_[index], zone_still_targets, zone_moving_targets);
552 zone_all_targets = zone_still_targets + zone_moving_targets;
553
554 // Publish Still Target Count in Zones
555 SAFE_PUBLISH_SENSOR(this->zone_still_target_count_sensors_[index], zone_still_targets);
556 // Publish Moving Target Count in Zones
557 SAFE_PUBLISH_SENSOR(this->zone_moving_target_count_sensors_[index], zone_moving_targets);
558 // Publish All Target Count in Zones
559 SAFE_PUBLISH_SENSOR(this->zone_target_count_sensors_[index], zone_all_targets);
560 } // End loop thru zones
561
562 // Target Count
563 SAFE_PUBLISH_SENSOR(this->target_count_sensor_, target_count);
564 // Still Target Count
565 SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count);
566 // Moving Target Count
567 SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count);
568
569#endif
570
571#ifdef USE_BINARY_SENSOR
572 // Target Presence
573 if (this->target_binary_sensor_ != nullptr) {
574 if (target_count > 0) {
575 this->target_binary_sensor_->publish_state(true);
576 } else {
577 if (this->get_timeout_status_(this->presence_millis_)) {
578 this->target_binary_sensor_->publish_state(false);
579 } else {
580 ESP_LOGV(TAG, "Clear presence waiting timeout: %" PRIu32, this->timeout_);
581 }
582 }
583 }
584 // Moving Target Presence
585 if (this->moving_target_binary_sensor_ != nullptr) {
586 if (moving_target_count > 0) {
587 this->moving_target_binary_sensor_->publish_state(true);
588 } else {
590 this->moving_target_binary_sensor_->publish_state(false);
591 }
592 }
593 }
594 // Still Target Presence
595 if (this->still_target_binary_sensor_ != nullptr) {
596 if (still_target_count > 0) {
597 this->still_target_binary_sensor_->publish_state(true);
598 } else {
600 this->still_target_binary_sensor_->publish_state(false);
601 }
602 }
603 }
604#endif
605#ifdef USE_SENSOR
606 // For presence timeout check
607 if (target_count > 0) {
609 }
610 if (moving_target_count > 0) {
612 }
613 if (still_target_count > 0) {
615 }
616#endif
617
618 this->data_callback_.call();
619}
620
622 ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
623 if (this->buffer_pos_ < 10) {
624 ESP_LOGE(TAG, "Invalid length");
625 return true;
626 }
627 if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
628 char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)];
629 ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE));
630 return true;
631 }
632 if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
633 ESP_LOGE(TAG, "Invalid status");
634 return true;
635 }
636 if (this->buffer_data_[8] || this->buffer_data_[9]) {
637 ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
638 return true;
639 }
640
641 switch (this->buffer_data_[COMMAND]) {
642 case CMD_ENABLE_CONF:
643 ESP_LOGV(TAG, "Enable conf");
644 break;
645
646 case CMD_DISABLE_CONF:
647 ESP_LOGV(TAG, "Disabled conf");
648 break;
649
650 case CMD_SET_BAUD_RATE:
651 ESP_LOGV(TAG, "Baud rate change");
652#ifdef USE_SELECT
653 if (this->baud_rate_select_ != nullptr) {
654 auto baud = this->baud_rate_select_->current_option();
655 ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
656 }
657#endif
658 break;
659
660 case CMD_QUERY_VERSION: {
661 std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
662 char version_s[20];
663 ld24xx::format_version_str(this->version_, version_s);
664 ESP_LOGV(TAG, "Firmware version: %s", version_s);
665#ifdef USE_TEXT_SENSOR
666 if (this->version_text_sensor_ != nullptr) {
667 this->version_text_sensor_->publish_state(version_s);
668 }
669#endif
670 break;
671 }
672
673 case CMD_QUERY_MAC_ADDRESS: {
674 if (this->buffer_pos_ < 20) {
675 return false;
676 }
677
678 this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
679 if (this->bluetooth_on_) {
680 std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
681 }
682
683 char mac_s[18];
684 const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
685 ESP_LOGV(TAG, "MAC address: %s", mac_str);
686#ifdef USE_TEXT_SENSOR
687 if (this->mac_text_sensor_ != nullptr) {
688 this->mac_text_sensor_->publish_state(mac_str);
689 }
690#endif
691#ifdef USE_SWITCH
692 if (this->bluetooth_switch_ != nullptr) {
693 this->bluetooth_switch_->publish_state(this->bluetooth_on_);
694 }
695#endif
696 break;
697 }
698
699 case CMD_BLUETOOTH:
700 ESP_LOGV(TAG, "Bluetooth");
701 break;
702
703 case CMD_SINGLE_TARGET_MODE:
704 ESP_LOGV(TAG, "Single target conf");
705#ifdef USE_SWITCH
706 if (this->multi_target_switch_ != nullptr) {
707 this->multi_target_switch_->publish_state(false);
708 }
709#endif
710 break;
711
712 case CMD_MULTI_TARGET_MODE:
713 ESP_LOGV(TAG, "Multi target conf");
714#ifdef USE_SWITCH
715 if (this->multi_target_switch_ != nullptr) {
716 this->multi_target_switch_->publish_state(true);
717 }
718#endif
719 break;
720
721 case CMD_QUERY_TARGET_MODE:
722 ESP_LOGV(TAG, "Query target tracking mode");
723#ifdef USE_SWITCH
724 if (this->multi_target_switch_ != nullptr) {
725 this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
726 }
727#endif
728 break;
729
730 case CMD_QUERY_ZONE:
731 ESP_LOGV(TAG, "Query zone conf");
732 this->zone_type_ = this->buffer_data_[10];
733 this->publish_zone_type();
734#ifdef USE_SELECT
735 if (this->zone_type_select_ != nullptr) {
736 auto zone = this->zone_type_select_->current_option();
737 ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.c_str());
738 }
739#endif
740 if (this->buffer_data_[10] == 0x00) {
741 ESP_LOGV(TAG, "Zone: Disabled");
742 }
743 if (this->buffer_data_[10] == 0x01) {
744 ESP_LOGV(TAG, "Zone: Area detection");
745 }
746 if (this->buffer_data_[10] == 0x02) {
747 ESP_LOGV(TAG, "Zone: Area filter");
748 }
749 this->process_zone_();
750 break;
751
752 case CMD_SET_ZONE:
753 ESP_LOGV(TAG, "Set zone conf");
754 this->query_zone_info();
755 break;
756
757 default:
758 break;
759 }
760 return true;
761}
762
763// Read LD2450 buffer data
765 if (readch < 0) {
766 return; // No data available
767 }
768
769 if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
770 this->buffer_data_[this->buffer_pos_++] = readch;
771 this->buffer_data_[this->buffer_pos_] = 0;
772 } else {
773 // We should never get here, but just in case...
774 ESP_LOGW(TAG, "Max command length exceeded; ignoring");
775 this->buffer_pos_ = 0;
776 return;
777 }
778 if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
779 return; // Not enough data to process yet
780 }
781 if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
782 this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
783#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
784 char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)];
785 ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_));
786#endif
787 this->handle_periodic_data_();
788 this->buffer_pos_ = 0; // Reset position index for next frame
789 } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
790#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
791 char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)];
792 ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_));
793#endif
794 if (this->handle_ack_data_()) {
795 this->buffer_pos_ = 0; // Reset position index for next message
796 } else {
797 ESP_LOGV(TAG, "Ack Data incomplete");
798 }
799 }
800}
801
802// Set Config Mode - Pre-requisite sending commands
804 const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
805 const uint8_t cmd_value[2] = {0x01, 0x00};
806 this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
807}
808
809// Set Bluetooth Enable/Disable
810void LD2450Component::set_bluetooth(bool enable) {
811 this->set_config_mode_(true);
812 const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
813 this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
814 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
815}
816
817// Set Baud rate
818void LD2450Component::set_baud_rate(const char *state) {
819 this->set_config_mode_(true);
820 const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
821 this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
822 this->set_timeout(200, [this]() { this->restart_(); });
823}
824
825// Set Zone Type - one of: Disabled, Detection, Filter
826void LD2450Component::set_zone_type(const char *state) {
827 ESP_LOGV(TAG, "Set zone type: %s", state);
828 uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
829 this->zone_type_ = zone_type;
831}
832
833// Publish Zone Type to Select component
834void LD2450Component::publish_zone_type() {
835#ifdef USE_SELECT
836 if (this->zone_type_select_ != nullptr) {
837 this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
838 }
839#endif
840}
841
842// Set Single/Multiplayer target detection
843void LD2450Component::set_multi_target(bool enable) {
844 this->set_config_mode_(true);
845 uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE;
846 this->send_command_(cmd, nullptr, 0);
847 this->set_config_mode_(false);
848}
849
850// LD2450 factory reset
851void LD2450Component::factory_reset() {
852 this->set_config_mode_(true);
853 this->send_command_(CMD_RESET, nullptr, 0);
854 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
855}
856
857// Restart LD2450 module
858void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
859
860// Get LD2450 firmware version
861void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
862
863// Get LD2450 mac address
865 uint8_t cmd_value[2] = {0x01, 0x00};
866 this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
867}
868
869// Query for target tracking mode
870void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); }
871
872// Query for zone info
873void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
874
875#ifdef USE_SENSOR
876void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) {
877 this->move_x_sensors_[target].set_sensor(s);
878}
879void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) {
880 this->move_y_sensors_[target].set_sensor(s);
881}
882void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
883 this->move_speed_sensors_[target].set_sensor(s);
884}
885void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
886 this->move_angle_sensors_[target].set_sensor(s);
887}
888void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
889 this->move_distance_sensors_[target].set_sensor(s);
890}
891void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
892 this->move_resolution_sensors_[target].set_sensor(s);
893}
894void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
895 this->zone_target_count_sensors_[zone].set_sensor(s);
896}
897void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
898 this->zone_still_target_count_sensors_[zone].set_sensor(s);
899}
900void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
901 this->zone_moving_target_count_sensors_[zone].set_sensor(s);
902}
903#endif
904#ifdef USE_TEXT_SENSOR
905void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
906 this->direction_text_sensors_[target] = s;
907}
908#endif
909
910// Send Zone coordinates data to LD2450
911#ifdef USE_NUMBER
912void LD2450Component::set_zone_coordinate(uint8_t zone) {
913 number::Number *x1sens = this->zone_numbers_[zone].x1;
914 number::Number *y1sens = this->zone_numbers_[zone].y1;
915 number::Number *x2sens = this->zone_numbers_[zone].x2;
916 number::Number *y2sens = this->zone_numbers_[zone].y2;
917 if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) {
918 return;
919 }
920 this->zone_config_[zone].x1 = static_cast<int>(x1sens->state);
921 this->zone_config_[zone].y1 = static_cast<int>(y1sens->state);
922 this->zone_config_[zone].x2 = static_cast<int>(x2sens->state);
923 this->zone_config_[zone].y2 = static_cast<int>(y2sens->state);
925}
926
927void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2,
928 number::Number *y2) {
929 if (zone < MAX_ZONES) {
930 this->zone_numbers_[zone].x1 = x1;
931 this->zone_numbers_[zone].y1 = y1;
932 this->zone_numbers_[zone].x2 = x2;
933 this->zone_numbers_[zone].y2 = y2;
934 }
935}
936#endif
937
938// Set Presence Timeout load and save from flash
939#ifdef USE_NUMBER
940void LD2450Component::set_presence_timeout() {
941 if (this->presence_timeout_number_ != nullptr) {
942 if (this->presence_timeout_number_->state == 0) {
943 float timeout = this->restore_from_flash_();
944 this->presence_timeout_number_->publish_state(timeout);
945 this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
946 }
947 if (this->presence_timeout_number_->has_state()) {
948 this->save_to_flash_(this->presence_timeout_number_->state);
949 this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
950 }
951 }
952}
953
954// Save Presence Timeout to flash
955void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); }
956
957// Load Presence Timeout from flash
959 float value;
960 if (!this->pref_.load(&value)) {
961 value = DEFAULT_PRESENCE_TIMEOUT;
962 }
963 return value;
964}
965#endif
966
967} // namespace esphome::ld2450
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.
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:89
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:510
void save_to_flash_(float value)
Definition ld2450.cpp:955
std::array< SensorWithDedup< int16_t >, MAX_TARGETS > move_y_sensors_
Definition ld2450.h:186
std::array< SensorWithDedup< uint8_t >, MAX_ZONES > zone_moving_target_count_sensors_
Definition ld2450.h:193
std::array< Deduplicator< uint8_t >, MAX_TARGETS > direction_dedup_
Definition ld2450.h:197
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len)
Definition ld2450.cpp:412
std::array< SensorWithDedup< uint8_t >, MAX_ZONES > zone_target_count_sensors_
Definition ld2450.h:191
void set_config_mode_(bool enable)
Definition ld2450.cpp:803
std::array< text_sensor::TextSensor *, MAX_TARGETS > direction_text_sensors_
Definition ld2450.h:196
void count_targets_in_zone_(const Zone &zone, uint8_t &still, uint8_t &moving)
Definition ld2450.cpp:288
std::array< SensorWithDedup< uint16_t >, MAX_TARGETS > move_resolution_sensors_
Definition ld2450.h:190
uint8_t buffer_data_[MAX_LINE_LENGTH]
Definition ld2450.h:171
Target target_info_[MAX_TARGETS]
Definition ld2450.h:177
Zone zone_config_[MAX_ZONES]
Definition ld2450.h:178
ESPPreferenceObject pref_
Definition ld2450.h:181
std::array< SensorWithDedup< uint16_t >, MAX_TARGETS > move_distance_sensors_
Definition ld2450.h:189
std::array< SensorWithDedup< uint8_t >, MAX_ZONES > zone_still_target_count_sensors_
Definition ld2450.h:192
LazyCallbackManager< void()> data_callback_
Definition ld2450.h:200
std::array< SensorWithDedup< int16_t >, MAX_TARGETS > move_speed_sensors_
Definition ld2450.h:187
std::array< SensorWithDedup< float >, MAX_TARGETS > move_angle_sensors_
Definition ld2450.h:188
ZoneOfNumbers zone_numbers_[MAX_ZONES]
Definition ld2450.h:182
std::array< SensorWithDedup< int16_t >, MAX_TARGETS > move_x_sensors_
Definition ld2450.h:185
bool get_timeout_status_(uint32_t check_millis)
Definition ld2450.cpp:348
void publish_state(float state)
Definition number.cpp:22
Base-class for all sensors.
Definition sensor.h:47
void publish_state(const std::string &state)
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
UARTComponent * parent_
Definition uart.h:73
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
FanDirection direction
Definition fan.h:5
int speed
Definition fan.h:3
bool state
Definition fan.h:2
mopeka_std_values val[3]
constexpr uint32_t BAUD_RATES[]
Definition ld2450.cpp:93
constexpr Uint8ToString DIRECTION_BY_UINT[]
Definition ld2450.cpp:73
@ DIRECTION_MOVING_AWAY
Definition ld2450.h:48
@ DIRECTION_APPROACHING
Definition ld2450.h:47
@ DIRECTION_UNDEFINED
Definition ld2450.h:51
@ DIRECTION_STATIONARY
Definition ld2450.h:49
constexpr StringToUint8 ZONE_TYPE_BY_STR[]
Definition ld2450.cpp:86
constexpr StringToUint8 BAUD_RATES_BY_STR[]
Definition ld2450.cpp:67
constexpr Uint8ToString ZONE_TYPE_BY_UINT[]
Definition ld2450.cpp:80
uint8_t find_uint8(const StringToUint8(&arr)[N], const std::string &str)
Definition ld2450.cpp:96
const char * find_str(const Uint8ToString(&arr)[N], uint8_t value)
Definition ld2450.cpp:104
void format_version_str(const uint8_t *version, std::span< char, 20 > buffer)
Definition ld24xx.h:59
const char * format_mac_str(const uint8_t *mac_address, std::span< char, 18 > buffer)
Definition ld24xx.h:49
optional< size_t > find_index(const uint32_t(&arr)[N], uint32_t value)
Definition ld24xx.h:35
std::vector< uint8_t > bytes
Definition sml_parser.h:13
std::string size_t len
Definition helpers.h:1045
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:409
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:1368
void HOT delay(uint32_t ms)
Definition core.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
number::Number * y2
Definition ld2450.h:74
number::Number * x2
Definition ld2450.h:73
number::Number * x1
Definition ld2450.h:71
number::Number * y1
Definition ld2450.h:72