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