ESPHome 2026.2.3
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 float calculate_angle(float base, float hypotenuse) {
172 if (base < 0.0f || hypotenuse <= 0.0f) {
173 return 0.0f;
174 }
175 float angle_radians = acosf(base / hypotenuse);
176 float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v<float>);
177 return angle_degrees;
178}
179
180static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
181 return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
182}
183
185#ifdef USE_NUMBER
186 if (this->presence_timeout_number_ != nullptr) {
187 this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
188 this->set_presence_timeout();
189 }
190#endif
191 this->restart_and_read_all_info();
192}
193
194void LD2450Component::dump_config() {
195 char mac_s[18];
196 char version_s[20];
197 const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
198 ld24xx::format_version_str(this->version_, version_s);
199 ESP_LOGCONFIG(TAG,
200 "LD2450:\n"
201 " Firmware version: %s\n"
202 " MAC address: %s",
203 version_s, mac_str);
204#ifdef USE_BINARY_SENSOR
205 ESP_LOGCONFIG(TAG, "Binary Sensors:");
206 LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_);
207 LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_);
208 LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_);
209#endif
210#ifdef USE_SENSOR
211 ESP_LOGCONFIG(TAG, "Sensors:");
212 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetCount", this->moving_target_count_sensor_);
213 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetCount", this->still_target_count_sensor_);
214 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetCount", this->target_count_sensor_);
215 for (auto &s : this->move_x_sensors_) {
216 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetX", s);
217 }
218 for (auto &s : this->move_y_sensors_) {
219 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetY", s);
220 }
221 for (auto &s : this->move_angle_sensors_) {
222 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetAngle", s);
223 }
224 for (auto &s : this->move_distance_sensors_) {
225 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetDistance", s);
226 }
227 for (auto &s : this->move_resolution_sensors_) {
228 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetResolution", s);
229 }
230 for (auto &s : this->move_speed_sensors_) {
231 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetSpeed", s);
232 }
233 for (auto &s : this->zone_target_count_sensors_) {
234 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneTargetCount", s);
235 }
236 for (auto &s : this->zone_moving_target_count_sensors_) {
237 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneMovingTargetCount", s);
238 }
239 for (auto &s : this->zone_still_target_count_sensors_) {
240 LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneStillTargetCount", s);
241 }
242#endif
243#ifdef USE_TEXT_SENSOR
244 ESP_LOGCONFIG(TAG, "Text Sensors:");
245 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
246 LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_);
247 for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
248 LOG_TEXT_SENSOR(" ", "Direction", s);
249 }
250#endif
251#ifdef USE_NUMBER
252 ESP_LOGCONFIG(TAG, "Numbers:");
253 LOG_NUMBER(" ", "PresenceTimeout", this->presence_timeout_number_);
254 for (auto n : this->zone_numbers_) {
255 LOG_NUMBER(" ", "ZoneX1", n.x1);
256 LOG_NUMBER(" ", "ZoneY1", n.y1);
257 LOG_NUMBER(" ", "ZoneX2", n.x2);
258 LOG_NUMBER(" ", "ZoneY2", n.y2);
259 }
260#endif
261#ifdef USE_SELECT
262 ESP_LOGCONFIG(TAG, "Selects:");
263 LOG_SELECT(" ", "BaudRate", this->baud_rate_select_);
264 LOG_SELECT(" ", "ZoneType", this->zone_type_select_);
265#endif
266#ifdef USE_SWITCH
267 ESP_LOGCONFIG(TAG, "Switches:");
268 LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_);
269 LOG_SWITCH(" ", "MultiTarget", this->multi_target_switch_);
270#endif
271#ifdef USE_BUTTON
272 ESP_LOGCONFIG(TAG, "Buttons:");
273 LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
274 LOG_BUTTON(" ", "Restart", this->restart_button_);
275#endif
276}
277
278void LD2450Component::loop() {
279 // Read all available bytes in batches to reduce UART call overhead.
280 size_t avail = this->available();
281 uint8_t buf[MAX_LINE_LENGTH];
282 while (avail > 0) {
283 size_t to_read = std::min(avail, sizeof(buf));
284 if (!this->read_array(buf, to_read)) {
285 break;
286 }
287 avail -= to_read;
288
289 for (size_t i = 0; i < to_read; i++) {
290 this->readline_(buf[i]);
291 }
292 }
293}
294
295// Count targets in zone
296uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) {
297 uint8_t count = 0;
298 for (auto &index : this->target_info_) {
299 if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 &&
300 index.is_moving == is_moving) {
301 count++;
302 }
303 }
304 return count;
305}
306
307// Service reset_radar_zone
308void LD2450Component::reset_radar_zone() {
309 this->zone_type_ = 0;
310 for (auto &i : this->zone_config_) {
311 i.x1 = 0;
312 i.y1 = 0;
313 i.x2 = 0;
314 i.y2 = 0;
315 }
317}
318
319void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2,
320 int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2,
321 int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2,
322 int32_t zone3_y2) {
323 this->zone_type_ = zone_type;
324 int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
325 zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
326 for (uint8_t i = 0; i < MAX_ZONES; i++) {
327 this->zone_config_[i].x1 = zone_parameters[i * 4];
328 this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
329 this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
330 this->zone_config_[i].y2 = zone_parameters[i * 4 + 3];
331 }
333}
334
335// Set Zone on LD2450 Sensor
337 uint8_t cmd_value[26] = {};
338 uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
339 uint8_t area_config[24] = {};
340 for (uint8_t i = 0; i < MAX_ZONES; i++) {
341 int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
342 this->zone_config_[i].y2};
343 ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
344 }
345 std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
346 std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
347 this->set_config_mode_(true);
348 this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
349 this->set_config_mode_(false);
350}
351
352// Check presense timeout to reset presence status
353bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
354 if (check_millis == 0) {
355 return true;
356 }
357 if (this->timeout_ == 0) {
358 this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
359 }
360 return App.get_loop_component_start_time() - check_millis >= this->timeout_;
361}
362
363// Extract, store and publish zone details LD2450 buffer
365 uint8_t index, start;
366 for (index = 0; index < MAX_ZONES; index++) {
367 start = 12 + index * 8;
368 this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
369 this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
370 this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
371 this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
372#ifdef USE_NUMBER
373 // only one null check as all coordinates are required for a single zone
374 if (this->zone_numbers_[index].x1 != nullptr) {
375 this->zone_numbers_[index].x1->publish_state(this->zone_config_[index].x1);
376 this->zone_numbers_[index].y1->publish_state(this->zone_config_[index].y1);
377 this->zone_numbers_[index].x2->publish_state(this->zone_config_[index].x2);
378 this->zone_numbers_[index].y2->publish_state(this->zone_config_[index].y2);
379 }
380#endif
381 }
382}
383
384// Read all info from LD2450 buffer
385void LD2450Component::read_all_info() {
386 this->set_config_mode_(true);
387 this->get_version_();
388 this->get_mac_();
390 this->query_zone_();
391 this->set_config_mode_(false);
392#ifdef USE_SELECT
393 if (this->baud_rate_select_ != nullptr) {
394 if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) {
395 this->baud_rate_select_->publish_state(*index);
396 }
397 }
398 this->publish_zone_type();
399#endif
400}
401
402// Read zone info from LD2450 buffer
403void LD2450Component::query_zone_info() {
404 this->set_config_mode_(true);
405 this->query_zone_();
406 this->set_config_mode_(false);
407}
408
409// Restart LD2450 and read all info from buffer
410void LD2450Component::restart_and_read_all_info() {
411 this->set_config_mode_(true);
412 this->restart_();
413 this->set_timeout(1500, [this]() { this->read_all_info(); });
414}
415
416void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
417 this->data_callback_.add(std::move(callback));
418}
419
420// Send command with values to LD2450
421void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
422 ESP_LOGV(TAG, "Sending COMMAND %02X", command);
423 // frame header bytes
424 this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
425 // length bytes
426 uint8_t len = 2;
427 if (command_value != nullptr) {
428 len += command_value_len;
429 }
430 // 2 length bytes (low, high) + 2 command bytes (low, high)
431 uint8_t len_cmd[] = {len, 0x00, command, 0x00};
432 this->write_array(len_cmd, sizeof(len_cmd));
433 // command value bytes
434 if (command_value != nullptr) {
435 this->write_array(command_value, command_value_len);
436 }
437 // frame footer bytes
438 this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
439
440 if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) {
441 delay(50); // NOLINT
442 }
443}
444
445// LD2450 Radar data message:
446// [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]
447// Header Target 1 Target 2 Target 3 End
449 if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
450 ESP_LOGE(TAG, "Invalid length");
451 return;
452 }
453 if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
454 this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
455 this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
456 ESP_LOGE(TAG, "Invalid header/footer");
457 return;
458 }
459
460 int16_t target_count = 0;
461 int16_t still_target_count = 0;
462 int16_t moving_target_count = 0;
463 int16_t res = 0;
464 int16_t start = 0;
465 int16_t tx = 0;
466 int16_t ty = 0;
467 int16_t td = 0;
468 int16_t ts = 0;
469 float angle = 0;
470 uint8_t index = 0;
472 bool is_moving = false;
473
474#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
475 // Loop thru targets
476 for (index = 0; index < MAX_TARGETS; index++) {
477#ifdef USE_SENSOR
478 // X
479 start = TARGET_X + index * 8;
480 is_moving = false;
481 // tx is used for further calculations, so always needs to be populated
482 tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
483 SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx);
484 // Y
485 start = TARGET_Y + index * 8;
486 ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
487 SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty);
488 // RESOLUTION
489 start = TARGET_RESOLUTION + index * 8;
490 res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
491 SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res);
492#endif
493 // SPEED
494 start = TARGET_SPEED + index * 8;
495 ts = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
496 if (ts) {
497 is_moving = true;
498 moving_target_count++;
499 }
500#ifdef USE_SENSOR
501 SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts);
502#endif
503 // DISTANCE
504 // Optimized: use already decoded tx and ty values, replace pow() with multiplication
505 int32_t x_squared = (int32_t) tx * tx;
506 int32_t y_squared = (int32_t) ty * ty;
507 td = (uint16_t) sqrtf(x_squared + y_squared);
508 if (td > 0) {
509 target_count++;
510 }
511#ifdef USE_SENSOR
512 SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
513 // ANGLE
514 angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
515 if (tx > 0) {
516 angle = angle * -1;
517 }
518 SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
519#endif
520#ifdef USE_TEXT_SENSOR
521 // DIRECTION
522 if (td == 0) {
524 } else if (ts > 0) {
526 } else if (ts < 0) {
528 } else {
530 }
531 text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
532 const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction);
533 if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) {
534 tsd->publish_state(dir_str);
535 }
536#endif
537
538 // Store target info for zone target count
539 this->target_info_[index].x = tx;
540 this->target_info_[index].y = ty;
541 this->target_info_[index].is_moving = is_moving;
542
543 } // End loop thru targets
544
545 still_target_count = target_count - moving_target_count;
546#endif
547
548#ifdef USE_SENSOR
549 // Loop thru zones
550 uint8_t zone_still_targets = 0;
551 uint8_t zone_moving_targets = 0;
552 uint8_t zone_all_targets = 0;
553 for (index = 0; index < MAX_ZONES; index++) {
554 zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
555 zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
556 zone_all_targets = zone_still_targets + zone_moving_targets;
557
558 // Publish Still Target Count in Zones
559 SAFE_PUBLISH_SENSOR(this->zone_still_target_count_sensors_[index], zone_still_targets);
560 // Publish Moving Target Count in Zones
561 SAFE_PUBLISH_SENSOR(this->zone_moving_target_count_sensors_[index], zone_moving_targets);
562 // Publish All Target Count in Zones
563 SAFE_PUBLISH_SENSOR(this->zone_target_count_sensors_[index], zone_all_targets);
564 } // End loop thru zones
565
566 // Target Count
567 SAFE_PUBLISH_SENSOR(this->target_count_sensor_, target_count);
568 // Still Target Count
569 SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count);
570 // Moving Target Count
571 SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count);
572#endif
573
574#ifdef USE_BINARY_SENSOR
575 // Target Presence
576 if (this->target_binary_sensor_ != nullptr) {
577 if (target_count > 0) {
578 this->target_binary_sensor_->publish_state(true);
579 } else {
580 if (this->get_timeout_status_(this->presence_millis_)) {
581 this->target_binary_sensor_->publish_state(false);
582 } else {
583 ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_);
584 }
585 }
586 }
587 // Moving Target Presence
588 if (this->moving_target_binary_sensor_ != nullptr) {
589 if (moving_target_count > 0) {
590 this->moving_target_binary_sensor_->publish_state(true);
591 } else {
593 this->moving_target_binary_sensor_->publish_state(false);
594 }
595 }
596 }
597 // Still Target Presence
598 if (this->still_target_binary_sensor_ != nullptr) {
599 if (still_target_count > 0) {
600 this->still_target_binary_sensor_->publish_state(true);
601 } else {
603 this->still_target_binary_sensor_->publish_state(false);
604 }
605 }
606 }
607#endif
608#ifdef USE_SENSOR
609 // For presence timeout check
610 if (target_count > 0) {
612 }
613 if (moving_target_count > 0) {
615 }
616 if (still_target_count > 0) {
618 }
619#endif
620
621 this->data_callback_.call();
622}
623
625 ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
626 if (this->buffer_pos_ < 10) {
627 ESP_LOGE(TAG, "Invalid length");
628 return true;
629 }
630 if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
631 char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)];
632 ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE));
633 return true;
634 }
635 if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
636 ESP_LOGE(TAG, "Invalid status");
637 return true;
638 }
639 if (this->buffer_data_[8] || this->buffer_data_[9]) {
640 ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
641 return true;
642 }
643
644 switch (this->buffer_data_[COMMAND]) {
645 case CMD_ENABLE_CONF:
646 ESP_LOGV(TAG, "Enable conf");
647 break;
648
649 case CMD_DISABLE_CONF:
650 ESP_LOGV(TAG, "Disabled conf");
651 break;
652
653 case CMD_SET_BAUD_RATE:
654 ESP_LOGV(TAG, "Baud rate change");
655#ifdef USE_SELECT
656 if (this->baud_rate_select_ != nullptr) {
657 auto baud = this->baud_rate_select_->current_option();
658 ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
659 }
660#endif
661 break;
662
663 case CMD_QUERY_VERSION: {
664 std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
665 char version_s[20];
666 ld24xx::format_version_str(this->version_, version_s);
667 ESP_LOGV(TAG, "Firmware version: %s", version_s);
668#ifdef USE_TEXT_SENSOR
669 if (this->version_text_sensor_ != nullptr) {
670 this->version_text_sensor_->publish_state(version_s);
671 }
672#endif
673 break;
674 }
675
676 case CMD_QUERY_MAC_ADDRESS: {
677 if (this->buffer_pos_ < 20) {
678 return false;
679 }
680
681 this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
682 if (this->bluetooth_on_) {
683 std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
684 }
685
686 char mac_s[18];
687 const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s);
688 ESP_LOGV(TAG, "MAC address: %s", mac_str);
689#ifdef USE_TEXT_SENSOR
690 if (this->mac_text_sensor_ != nullptr) {
691 this->mac_text_sensor_->publish_state(mac_str);
692 }
693#endif
694#ifdef USE_SWITCH
695 if (this->bluetooth_switch_ != nullptr) {
696 this->bluetooth_switch_->publish_state(this->bluetooth_on_);
697 }
698#endif
699 break;
700 }
701
702 case CMD_BLUETOOTH:
703 ESP_LOGV(TAG, "Bluetooth");
704 break;
705
706 case CMD_SINGLE_TARGET_MODE:
707 ESP_LOGV(TAG, "Single target conf");
708#ifdef USE_SWITCH
709 if (this->multi_target_switch_ != nullptr) {
710 this->multi_target_switch_->publish_state(false);
711 }
712#endif
713 break;
714
715 case CMD_MULTI_TARGET_MODE:
716 ESP_LOGV(TAG, "Multi target conf");
717#ifdef USE_SWITCH
718 if (this->multi_target_switch_ != nullptr) {
719 this->multi_target_switch_->publish_state(true);
720 }
721#endif
722 break;
723
724 case CMD_QUERY_TARGET_MODE:
725 ESP_LOGV(TAG, "Query target tracking mode");
726#ifdef USE_SWITCH
727 if (this->multi_target_switch_ != nullptr) {
728 this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
729 }
730#endif
731 break;
732
733 case CMD_QUERY_ZONE:
734 ESP_LOGV(TAG, "Query zone conf");
735 this->zone_type_ = this->buffer_data_[10];
736 this->publish_zone_type();
737#ifdef USE_SELECT
738 if (this->zone_type_select_ != nullptr) {
739 auto zone = this->zone_type_select_->current_option();
740 ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.c_str());
741 }
742#endif
743 if (this->buffer_data_[10] == 0x00) {
744 ESP_LOGV(TAG, "Zone: Disabled");
745 }
746 if (this->buffer_data_[10] == 0x01) {
747 ESP_LOGV(TAG, "Zone: Area detection");
748 }
749 if (this->buffer_data_[10] == 0x02) {
750 ESP_LOGV(TAG, "Zone: Area filter");
751 }
752 this->process_zone_();
753 break;
754
755 case CMD_SET_ZONE:
756 ESP_LOGV(TAG, "Set zone conf");
757 this->query_zone_info();
758 break;
759
760 default:
761 break;
762 }
763 return true;
764}
765
766// Read LD2450 buffer data
768 if (readch < 0) {
769 return; // No data available
770 }
771
772 if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
773 this->buffer_data_[this->buffer_pos_++] = readch;
774 this->buffer_data_[this->buffer_pos_] = 0;
775 } else {
776 // We should never get here, but just in case...
777 ESP_LOGW(TAG, "Max command length exceeded; ignoring");
778 this->buffer_pos_ = 0;
779 return;
780 }
781 if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
782 return; // Not enough data to process yet
783 }
784 if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
785 this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
786#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
787 char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)];
788 ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_));
789#endif
790 this->handle_periodic_data_();
791 this->buffer_pos_ = 0; // Reset position index for next frame
792 } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
793#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
794 char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)];
795 ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_));
796#endif
797 if (this->handle_ack_data_()) {
798 this->buffer_pos_ = 0; // Reset position index for next message
799 } else {
800 ESP_LOGV(TAG, "Ack Data incomplete");
801 }
802 }
803}
804
805// Set Config Mode - Pre-requisite sending commands
807 const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
808 const uint8_t cmd_value[2] = {0x01, 0x00};
809 this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
810}
811
812// Set Bluetooth Enable/Disable
813void LD2450Component::set_bluetooth(bool enable) {
814 this->set_config_mode_(true);
815 const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
816 this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
817 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
818}
819
820// Set Baud rate
821void LD2450Component::set_baud_rate(const char *state) {
822 this->set_config_mode_(true);
823 const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
824 this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
825 this->set_timeout(200, [this]() { this->restart_(); });
826}
827
828// Set Zone Type - one of: Disabled, Detection, Filter
829void LD2450Component::set_zone_type(const char *state) {
830 ESP_LOGV(TAG, "Set zone type: %s", state);
831 uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
832 this->zone_type_ = zone_type;
834}
835
836// Publish Zone Type to Select component
837void LD2450Component::publish_zone_type() {
838#ifdef USE_SELECT
839 if (this->zone_type_select_ != nullptr) {
840 this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
841 }
842#endif
843}
844
845// Set Single/Multiplayer target detection
846void LD2450Component::set_multi_target(bool enable) {
847 this->set_config_mode_(true);
848 uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE;
849 this->send_command_(cmd, nullptr, 0);
850 this->set_config_mode_(false);
851}
852
853// LD2450 factory reset
854void LD2450Component::factory_reset() {
855 this->set_config_mode_(true);
856 this->send_command_(CMD_RESET, nullptr, 0);
857 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
858}
859
860// Restart LD2450 module
861void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
862
863// Get LD2450 firmware version
864void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
865
866// Get LD2450 mac address
868 uint8_t cmd_value[2] = {0x01, 0x00};
869 this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
870}
871
872// Query for target tracking mode
873void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); }
874
875// Query for zone info
876void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
877
878#ifdef USE_SENSOR
879// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
880void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) {
881 this->move_x_sensors_[target] = new SensorWithDedup<int16_t>(s);
882}
883void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) {
884 this->move_y_sensors_[target] = new SensorWithDedup<int16_t>(s);
885}
886void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
887 this->move_speed_sensors_[target] = new SensorWithDedup<int16_t>(s);
888}
889void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
890 this->move_angle_sensors_[target] = new SensorWithDedup<float>(s);
891}
892void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
893 this->move_distance_sensors_[target] = new SensorWithDedup<uint16_t>(s);
894}
895void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
896 this->move_resolution_sensors_[target] = new SensorWithDedup<uint16_t>(s);
897}
898void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
899 this->zone_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
900}
901void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
902 this->zone_still_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
903}
904void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
905 this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
906}
907#endif
908#ifdef USE_TEXT_SENSOR
909void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
910 this->direction_text_sensors_[target] = s;
911}
912#endif
913
914// Send Zone coordinates data to LD2450
915#ifdef USE_NUMBER
916void LD2450Component::set_zone_coordinate(uint8_t zone) {
917 number::Number *x1sens = this->zone_numbers_[zone].x1;
918 number::Number *y1sens = this->zone_numbers_[zone].y1;
919 number::Number *x2sens = this->zone_numbers_[zone].x2;
920 number::Number *y2sens = this->zone_numbers_[zone].y2;
921 if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) {
922 return;
923 }
924 this->zone_config_[zone].x1 = static_cast<int>(x1sens->state);
925 this->zone_config_[zone].y1 = static_cast<int>(y1sens->state);
926 this->zone_config_[zone].x2 = static_cast<int>(x2sens->state);
927 this->zone_config_[zone].y2 = static_cast<int>(y2sens->state);
929}
930
931void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2,
932 number::Number *y2) {
933 if (zone < MAX_ZONES) {
934 this->zone_numbers_[zone].x1 = x1;
935 this->zone_numbers_[zone].y1 = y1;
936 this->zone_numbers_[zone].x2 = x2;
937 this->zone_numbers_[zone].y2 = y2;
938 }
939}
940#endif
941
942// Set Presence Timeout load and save from flash
943#ifdef USE_NUMBER
944void LD2450Component::set_presence_timeout() {
945 if (this->presence_timeout_number_ != nullptr) {
946 if (this->presence_timeout_number_->state == 0) {
947 float timeout = this->restore_from_flash_();
948 this->presence_timeout_number_->publish_state(timeout);
949 this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
950 }
951 if (this->presence_timeout_number_->has_state()) {
952 this->save_to_flash_(this->presence_timeout_number_->state);
953 this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
954 }
955 }
956}
957
958// Save Presence Timeout to flash
959void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); }
960
961// Load Presence Timeout from flash
963 float value;
964 if (!this->pref_.load(&value)) {
965 value = DEFAULT_PRESENCE_TIMEOUT;
966 }
967 return value;
968}
969#endif
970
971} // 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.
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:429
bool save(const T *src)
Definition preferences.h:21
bool has_state() const
void save_to_flash_(float value)
Definition ld2450.cpp:959
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving)
Definition ld2450.cpp:296
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
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len)
Definition ld2450.cpp:421
std::array< text_sensor::TextSensor *, 3 > direction_text_sensors_
Definition ld2450.h:197
std::array< SensorWithDedup< float > *, MAX_TARGETS > move_angle_sensors_
Definition ld2450.h:189
void set_config_mode_(bool enable)
Definition ld2450.cpp:806
std::array< SensorWithDedup< uint8_t > *, MAX_ZONES > zone_still_target_count_sensors_
Definition ld2450.h:193
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:200
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:353
void publish_state(float state)
Definition number.cpp:22
Base-class for all sensors.
Definition sensor.h:43
const std::string & get_state() const
Getter-syntax for .state.
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:692
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:978
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:26
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