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