ESPHome 2025.9.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 {
17namespace ld2450 {
18
19static const char *const TAG = "ld2450";
20static const char *const UNKNOWN_MAC = "unknown";
21static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
22
33
34enum ZoneType : uint8_t {
38};
39
40enum PeriodicData : uint8_t {
45};
46
47enum PeriodicDataValue : uint8_t {
48 HEADER = 0xAA,
49 FOOTER = 0x55,
50 CHECK = 0x00,
51};
52
53enum AckData : uint8_t {
56};
57
58// Memory-efficient lookup tables
59struct StringToUint8 {
60 const char *str;
61 const uint8_t value;
62};
63
64struct Uint8ToString {
65 const uint8_t value;
66 const char *str;
67};
68
69constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
70 {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
71 {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
72 {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
73};
74
75constexpr Uint8ToString DIRECTION_BY_UINT[] = {
76 {DIRECTION_APPROACHING, "Approaching"},
77 {DIRECTION_MOVING_AWAY, "Moving away"},
78 {DIRECTION_STATIONARY, "Stationary"},
79 {DIRECTION_NA, "NA"},
80};
81
82constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
83 {ZONE_DISABLED, "Disabled"},
84 {ZONE_DETECTION, "Detection"},
85 {ZONE_FILTER, "Filter"},
86};
87
88constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
89 {"Disabled", ZONE_DISABLED},
90 {"Detection", ZONE_DETECTION},
91 {"Filter", ZONE_FILTER},
92};
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_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
188 this->set_presence_timeout();
189 }
190#endif
191 this->restart_and_read_all_info();
192}
193
194void LD2450Component::dump_config() {
195 std::string mac_str =
197 std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
198 this->version_[4], this->version_[3], this->version_[2]);
199 ESP_LOGCONFIG(TAG,
200 "LD2450:\n"
201 " Firmware version: %s\n"
202 " MAC address: %s",
203 version.c_str(), mac_str.c_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 while (this->available()) {
280 this->readline_(this->read());
281 }
282}
283
284// Count targets in zone
285uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) {
286 uint8_t count = 0;
287 for (auto &index : this->target_info_) {
288 if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 &&
289 index.is_moving == is_moving) {
290 count++;
291 }
292 }
293 return count;
294}
295
296// Service reset_radar_zone
297void LD2450Component::reset_radar_zone() {
298 this->zone_type_ = 0;
299 for (auto &i : this->zone_config_) {
300 i.x1 = 0;
301 i.y1 = 0;
302 i.x2 = 0;
303 i.y2 = 0;
304 }
306}
307
308void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2,
309 int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2,
310 int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2,
311 int32_t zone3_y2) {
312 this->zone_type_ = zone_type;
313 int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
314 zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
315 for (uint8_t i = 0; i < MAX_ZONES; i++) {
316 this->zone_config_[i].x1 = zone_parameters[i * 4];
317 this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
318 this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
319 this->zone_config_[i].y2 = zone_parameters[i * 4 + 3];
320 }
322}
323
324// Set Zone on LD2450 Sensor
326 uint8_t cmd_value[26] = {};
327 uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
328 uint8_t area_config[24] = {};
329 for (uint8_t i = 0; i < MAX_ZONES; i++) {
330 int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
331 this->zone_config_[i].y2};
332 ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
333 }
334 std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes));
335 std::memcpy(cmd_value + 2, area_config, sizeof(area_config));
336 this->set_config_mode_(true);
337 this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value));
338 this->set_config_mode_(false);
339}
340
341// Check presense timeout to reset presence status
342bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
343 if (check_millis == 0) {
344 return true;
345 }
346 if (this->timeout_ == 0) {
347 this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
348 }
349 return App.get_loop_component_start_time() - check_millis >= this->timeout_;
350}
351
352// Extract, store and publish zone details LD2450 buffer
354 uint8_t index, start;
355 for (index = 0; index < MAX_ZONES; index++) {
356 start = 12 + index * 8;
357 this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start);
358 this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2);
359 this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4);
360 this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6);
361#ifdef USE_NUMBER
362 // only one null check as all coordinates are required for a single zone
363 if (this->zone_numbers_[index].x1 != nullptr) {
364 this->zone_numbers_[index].x1->publish_state(this->zone_config_[index].x1);
365 this->zone_numbers_[index].y1->publish_state(this->zone_config_[index].y1);
366 this->zone_numbers_[index].x2->publish_state(this->zone_config_[index].x2);
367 this->zone_numbers_[index].y2->publish_state(this->zone_config_[index].y2);
368 }
369#endif
370 }
371}
372
373// Read all info from LD2450 buffer
374void LD2450Component::read_all_info() {
375 this->set_config_mode_(true);
376 this->get_version_();
377 this->get_mac_();
379 this->query_zone_();
380 this->set_config_mode_(false);
381#ifdef USE_SELECT
382 const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
383 if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) {
384 this->baud_rate_select_->publish_state(baud_rate);
385 }
386 this->publish_zone_type();
387#endif
388}
389
390// Read zone info from LD2450 buffer
391void LD2450Component::query_zone_info() {
392 this->set_config_mode_(true);
393 this->query_zone_();
394 this->set_config_mode_(false);
395}
396
397// Restart LD2450 and read all info from buffer
398void LD2450Component::restart_and_read_all_info() {
399 this->set_config_mode_(true);
400 this->restart_();
401 this->set_timeout(1500, [this]() { this->read_all_info(); });
402}
403
404// Send command with values to LD2450
405void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
406 ESP_LOGV(TAG, "Sending COMMAND %02X", command);
407 // frame header bytes
408 this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER));
409 // length bytes
410 uint8_t len = 2;
411 if (command_value != nullptr) {
412 len += command_value_len;
413 }
414 // 2 length bytes (low, high) + 2 command bytes (low, high)
415 uint8_t len_cmd[] = {len, 0x00, command, 0x00};
416 this->write_array(len_cmd, sizeof(len_cmd));
417 // command value bytes
418 if (command_value != nullptr) {
419 this->write_array(command_value, command_value_len);
420 }
421 // frame footer bytes
422 this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
423
424 if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) {
425 delay(50); // NOLINT
426 }
427}
428
429// LD2450 Radar data message:
430// [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]
431// Header Target 1 Target 2 Target 3 End
433 if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
434 ESP_LOGE(TAG, "Invalid length");
435 return;
436 }
437 if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
438 this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] ||
439 this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) {
440 ESP_LOGE(TAG, "Invalid header/footer");
441 return;
442 }
443
444 int16_t target_count = 0;
445 int16_t still_target_count = 0;
446 int16_t moving_target_count = 0;
447 int16_t res = 0;
448 int16_t start = 0;
449 int16_t tx = 0;
450 int16_t ty = 0;
451 int16_t td = 0;
452 int16_t ts = 0;
453 int16_t angle = 0;
454 uint8_t index = 0;
456 bool is_moving = false;
457
458#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
459 // Loop thru targets
460 for (index = 0; index < MAX_TARGETS; index++) {
461#ifdef USE_SENSOR
462 // X
463 start = TARGET_X + index * 8;
464 is_moving = false;
465 // tx is used for further calculations, so always needs to be populated
466 tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
467 SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx);
468 // Y
469 start = TARGET_Y + index * 8;
470 ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
471 SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty);
472 // RESOLUTION
473 start = TARGET_RESOLUTION + index * 8;
474 res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
475 SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res);
476#endif
477 // SPEED
478 start = TARGET_SPEED + index * 8;
479 ts = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]);
480 if (ts) {
481 is_moving = true;
482 moving_target_count++;
483 }
484#ifdef USE_SENSOR
485 SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts);
486#endif
487 // DISTANCE
488 // Optimized: use already decoded tx and ty values, replace pow() with multiplication
489 int32_t x_squared = (int32_t) tx * tx;
490 int32_t y_squared = (int32_t) ty * ty;
491 td = (uint16_t) sqrtf(x_squared + y_squared);
492 if (td > 0) {
493 target_count++;
494 }
495#ifdef USE_SENSOR
496 SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
497 // ANGLE
498 angle = ld2450::calculate_angle(static_cast<float>(ty), static_cast<float>(td));
499 if (tx > 0) {
500 angle = angle * -1;
501 }
502 SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
503#endif
504#ifdef USE_TEXT_SENSOR
505 // DIRECTION
506 if (td == 0) {
508 } else if (ts > 0) {
510 } else if (ts < 0) {
512 } else {
514 }
515 text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
516 const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction);
517 if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) {
518 tsd->publish_state(dir_str);
519 }
520#endif
521
522 // Store target info for zone target count
523 this->target_info_[index].x = tx;
524 this->target_info_[index].y = ty;
525 this->target_info_[index].is_moving = is_moving;
526
527 } // End loop thru targets
528
529 still_target_count = target_count - moving_target_count;
530#endif
531
532#ifdef USE_SENSOR
533 // Loop thru zones
534 uint8_t zone_still_targets = 0;
535 uint8_t zone_moving_targets = 0;
536 uint8_t zone_all_targets = 0;
537 for (index = 0; index < MAX_ZONES; index++) {
538 zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
539 zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
540 zone_all_targets = zone_still_targets + zone_moving_targets;
541
542 // Publish Still Target Count in Zones
543 SAFE_PUBLISH_SENSOR(this->zone_still_target_count_sensors_[index], zone_still_targets);
544 // Publish Moving Target Count in Zones
545 SAFE_PUBLISH_SENSOR(this->zone_moving_target_count_sensors_[index], zone_moving_targets);
546 // Publish All Target Count in Zones
547 SAFE_PUBLISH_SENSOR(this->zone_target_count_sensors_[index], zone_all_targets);
548 } // End loop thru zones
549
550 // Target Count
551 SAFE_PUBLISH_SENSOR(this->target_count_sensor_, target_count);
552 // Still Target Count
553 SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count);
554 // Moving Target Count
555 SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count);
556#endif
557
558#ifdef USE_BINARY_SENSOR
559 // Target Presence
560 if (this->target_binary_sensor_ != nullptr) {
561 if (target_count > 0) {
562 this->target_binary_sensor_->publish_state(true);
563 } else {
564 if (this->get_timeout_status_(this->presence_millis_)) {
565 this->target_binary_sensor_->publish_state(false);
566 } else {
567 ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_);
568 }
569 }
570 }
571 // Moving Target Presence
572 if (this->moving_target_binary_sensor_ != nullptr) {
573 if (moving_target_count > 0) {
574 this->moving_target_binary_sensor_->publish_state(true);
575 } else {
577 this->moving_target_binary_sensor_->publish_state(false);
578 }
579 }
580 }
581 // Still Target Presence
582 if (this->still_target_binary_sensor_ != nullptr) {
583 if (still_target_count > 0) {
584 this->still_target_binary_sensor_->publish_state(true);
585 } else {
587 this->still_target_binary_sensor_->publish_state(false);
588 }
589 }
590 }
591#endif
592#ifdef USE_SENSOR
593 // For presence timeout check
594 if (target_count > 0) {
596 }
597 if (moving_target_count > 0) {
599 }
600 if (still_target_count > 0) {
602 }
603#endif
604}
605
607 ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
608 if (this->buffer_pos_ < 10) {
609 ESP_LOGE(TAG, "Invalid length");
610 return true;
611 }
612 if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
613 ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
614 return true;
615 }
616 if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
617 ESP_LOGE(TAG, "Invalid status");
618 return true;
619 }
620 if (this->buffer_data_[8] || this->buffer_data_[9]) {
621 ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
622 return true;
623 }
624
625 switch (this->buffer_data_[COMMAND]) {
626 case CMD_ENABLE_CONF:
627 ESP_LOGV(TAG, "Enable conf");
628 break;
629
630 case CMD_DISABLE_CONF:
631 ESP_LOGV(TAG, "Disabled conf");
632 break;
633
634 case CMD_SET_BAUD_RATE:
635 ESP_LOGV(TAG, "Baud rate change");
636#ifdef USE_SELECT
637 if (this->baud_rate_select_ != nullptr) {
638 ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
639 }
640#endif
641 break;
642
643 case CMD_QUERY_VERSION: {
644 std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
645 std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
646 this->version_[4], this->version_[3], this->version_[2]);
647 ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
648#ifdef USE_TEXT_SENSOR
649 if (this->version_text_sensor_ != nullptr) {
650 this->version_text_sensor_->publish_state(version);
651 }
652#endif
653 break;
654 }
655
656 case CMD_QUERY_MAC_ADDRESS: {
657 if (this->buffer_pos_ < 20) {
658 return false;
659 }
660
661 this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
662 if (this->bluetooth_on_) {
663 std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
664 }
665
666 std::string mac_str =
668 ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
669#ifdef USE_TEXT_SENSOR
670 if (this->mac_text_sensor_ != nullptr) {
671 this->mac_text_sensor_->publish_state(mac_str);
672 }
673#endif
674#ifdef USE_SWITCH
675 if (this->bluetooth_switch_ != nullptr) {
676 this->bluetooth_switch_->publish_state(this->bluetooth_on_);
677 }
678#endif
679 break;
680 }
681
682 case CMD_BLUETOOTH:
683 ESP_LOGV(TAG, "Bluetooth");
684 break;
685
686 case CMD_SINGLE_TARGET_MODE:
687 ESP_LOGV(TAG, "Single target conf");
688#ifdef USE_SWITCH
689 if (this->multi_target_switch_ != nullptr) {
690 this->multi_target_switch_->publish_state(false);
691 }
692#endif
693 break;
694
695 case CMD_MULTI_TARGET_MODE:
696 ESP_LOGV(TAG, "Multi target conf");
697#ifdef USE_SWITCH
698 if (this->multi_target_switch_ != nullptr) {
699 this->multi_target_switch_->publish_state(true);
700 }
701#endif
702 break;
703
704 case CMD_QUERY_TARGET_MODE:
705 ESP_LOGV(TAG, "Query target tracking mode");
706#ifdef USE_SWITCH
707 if (this->multi_target_switch_ != nullptr) {
708 this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02);
709 }
710#endif
711 break;
712
713 case CMD_QUERY_ZONE:
714 ESP_LOGV(TAG, "Query zone conf");
715 this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16);
716 this->publish_zone_type();
717#ifdef USE_SELECT
718 if (this->zone_type_select_ != nullptr) {
719 ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
720 }
721#endif
722 if (this->buffer_data_[10] == 0x00) {
723 ESP_LOGV(TAG, "Zone: Disabled");
724 }
725 if (this->buffer_data_[10] == 0x01) {
726 ESP_LOGV(TAG, "Zone: Area detection");
727 }
728 if (this->buffer_data_[10] == 0x02) {
729 ESP_LOGV(TAG, "Zone: Area filter");
730 }
731 this->process_zone_();
732 break;
733
734 case CMD_SET_ZONE:
735 ESP_LOGV(TAG, "Set zone conf");
736 this->query_zone_info();
737 break;
738
739 default:
740 break;
741 }
742 return true;
743}
744
745// Read LD2450 buffer data
747 if (readch < 0) {
748 return; // No data available
749 }
750
751 if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
752 this->buffer_data_[this->buffer_pos_++] = readch;
753 this->buffer_data_[this->buffer_pos_] = 0;
754 } else {
755 // We should never get here, but just in case...
756 ESP_LOGW(TAG, "Max command length exceeded; ignoring");
757 this->buffer_pos_ = 0;
758 }
759 if (this->buffer_pos_ < 4) {
760 return; // Not enough data to process yet
761 }
762 if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&
763 this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) {
764 ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
765 this->handle_periodic_data_();
766 this->buffer_pos_ = 0; // Reset position index for next frame
767 } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
768 ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
769 if (this->handle_ack_data_()) {
770 this->buffer_pos_ = 0; // Reset position index for next message
771 } else {
772 ESP_LOGV(TAG, "Ack Data incomplete");
773 }
774 }
775}
776
777// Set Config Mode - Pre-requisite sending commands
779 const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
780 const uint8_t cmd_value[2] = {0x01, 0x00};
781 this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
782}
783
784// Set Bluetooth Enable/Disable
785void LD2450Component::set_bluetooth(bool enable) {
786 this->set_config_mode_(true);
787 const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
788 this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
789 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
790}
791
792// Set Baud rate
793void LD2450Component::set_baud_rate(const std::string &state) {
794 this->set_config_mode_(true);
795 const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
796 this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
797 this->set_timeout(200, [this]() { this->restart_(); });
798}
799
800// Set Zone Type - one of: Disabled, Detection, Filter
801void LD2450Component::set_zone_type(const std::string &state) {
802 ESP_LOGV(TAG, "Set zone type: %s", state.c_str());
803 uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
804 this->zone_type_ = zone_type;
806}
807
808// Publish Zone Type to Select component
809void LD2450Component::publish_zone_type() {
810#ifdef USE_SELECT
811 std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_);
812 if (this->zone_type_select_ != nullptr) {
813 this->zone_type_select_->publish_state(zone_type);
814 }
815#endif
816}
817
818// Set Single/Multiplayer target detection
819void LD2450Component::set_multi_target(bool enable) {
820 this->set_config_mode_(true);
821 uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE;
822 this->send_command_(cmd, nullptr, 0);
823 this->set_config_mode_(false);
824}
825
826// LD2450 factory reset
827void LD2450Component::factory_reset() {
828 this->set_config_mode_(true);
829 this->send_command_(CMD_RESET, nullptr, 0);
830 this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
831}
832
833// Restart LD2450 module
834void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
835
836// Get LD2450 firmware version
837void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
838
839// Get LD2450 mac address
841 uint8_t cmd_value[2] = {0x01, 0x00};
842 this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2);
843}
844
845// Query for target tracking mode
846void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); }
847
848// Query for zone info
849void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
850
851#ifdef USE_SENSOR
852// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
853void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) {
854 this->move_x_sensors_[target] = new SensorWithDedup<int16_t>(s);
855}
856void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) {
857 this->move_y_sensors_[target] = new SensorWithDedup<int16_t>(s);
858}
859void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
860 this->move_speed_sensors_[target] = new SensorWithDedup<int16_t>(s);
861}
862void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
863 this->move_angle_sensors_[target] = new SensorWithDedup<float>(s);
864}
865void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
866 this->move_distance_sensors_[target] = new SensorWithDedup<uint16_t>(s);
867}
868void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
869 this->move_resolution_sensors_[target] = new SensorWithDedup<uint16_t>(s);
870}
871void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
872 this->zone_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
873}
874void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
875 this->zone_still_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
876}
877void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
878 this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
879}
880#endif
881#ifdef USE_TEXT_SENSOR
882void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
883 this->direction_text_sensors_[target] = s;
884}
885#endif
886
887// Send Zone coordinates data to LD2450
888#ifdef USE_NUMBER
889void LD2450Component::set_zone_coordinate(uint8_t zone) {
890 number::Number *x1sens = this->zone_numbers_[zone].x1;
891 number::Number *y1sens = this->zone_numbers_[zone].y1;
892 number::Number *x2sens = this->zone_numbers_[zone].x2;
893 number::Number *y2sens = this->zone_numbers_[zone].y2;
894 if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) {
895 return;
896 }
897 this->zone_config_[zone].x1 = static_cast<int>(x1sens->state);
898 this->zone_config_[zone].y1 = static_cast<int>(y1sens->state);
899 this->zone_config_[zone].x2 = static_cast<int>(x2sens->state);
900 this->zone_config_[zone].y2 = static_cast<int>(y2sens->state);
902}
903
904void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2,
905 number::Number *y2) {
906 if (zone < MAX_ZONES) {
907 this->zone_numbers_[zone].x1 = x1;
908 this->zone_numbers_[zone].y1 = y1;
909 this->zone_numbers_[zone].x2 = x2;
910 this->zone_numbers_[zone].y2 = y2;
911 }
912}
913#endif
914
915// Set Presence Timeout load and save from flash
916#ifdef USE_NUMBER
917void LD2450Component::set_presence_timeout() {
918 if (this->presence_timeout_number_ != nullptr) {
919 if (this->presence_timeout_number_->state == 0) {
920 float timeout = this->restore_from_flash_();
921 this->presence_timeout_number_->publish_state(timeout);
922 this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
923 }
924 if (this->presence_timeout_number_->has_state()) {
925 this->save_to_flash_(this->presence_timeout_number_->state);
926 this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
927 }
928 }
929}
930
931// Save Presence Timeout to flash
932void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); }
933
934// Load Presence Timeout from flash
936 float value;
937 if (!this->pref_.load(&value)) {
938 value = DEFAULT_PRESENCE_TIMEOUT;
939 }
940 return value;
941}
942#endif
943
944} // namespace ld2450
945} // namespace esphome
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:85
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
Definition entity_base.h:78
void save_to_flash_(float value)
Definition ld2450.cpp:932
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving)
Definition ld2450.cpp:285
std::array< SensorWithDedup< int16_t > *, MAX_TARGETS > move_speed_sensors_
Definition ld2450.h:183
std::array< SensorWithDedup< int16_t > *, MAX_TARGETS > move_x_sensors_
Definition ld2450.h:181
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len)
Definition ld2450.cpp:405
std::array< text_sensor::TextSensor *, 3 > direction_text_sensors_
Definition ld2450.h:192
std::array< SensorWithDedup< float > *, MAX_TARGETS > move_angle_sensors_
Definition ld2450.h:184
void set_config_mode_(bool enable)
Definition ld2450.cpp:778
std::array< SensorWithDedup< uint8_t > *, MAX_ZONES > zone_still_target_count_sensors_
Definition ld2450.h:188
std::array< SensorWithDedup< uint16_t > *, MAX_TARGETS > move_resolution_sensors_
Definition ld2450.h:186
std::array< SensorWithDedup< uint8_t > *, MAX_ZONES > zone_target_count_sensors_
Definition ld2450.h:187
uint8_t buffer_data_[MAX_LINE_LENGTH]
Definition ld2450.h:167
std::array< SensorWithDedup< uint8_t > *, MAX_ZONES > zone_moving_target_count_sensors_
Definition ld2450.h:189
Target target_info_[MAX_TARGETS]
Definition ld2450.h:173
Zone zone_config_[MAX_ZONES]
Definition ld2450.h:174
std::array< SensorWithDedup< uint16_t > *, MAX_TARGETS > move_distance_sensors_
Definition ld2450.h:185
ESPPreferenceObject pref_
Definition ld2450.h:177
std::array< SensorWithDedup< int16_t > *, MAX_TARGETS > move_y_sensors_
Definition ld2450.h:182
ZoneOfNumbers zone_numbers_[MAX_ZONES]
Definition ld2450.h:178
bool get_timeout_status_(uint32_t check_millis)
Definition ld2450.cpp:342
void publish_state(float state)
Definition number.cpp:9
Base-class for all sensors.
Definition sensor.h:59
std::string get_state() const
Getter-syntax for .state.
void publish_state(const std::string &state)
UARTComponent * parent_
Definition uart.h:68
void write_array(const uint8_t *data, size_t len)
Definition uart.h:21
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:75
@ DIRECTION_MOVING_AWAY
Definition ld2450.h:47
@ DIRECTION_APPROACHING
Definition ld2450.h:46
@ DIRECTION_UNDEFINED
Definition ld2450.h:50
@ DIRECTION_STATIONARY
Definition ld2450.h:48
constexpr StringToUint8 ZONE_TYPE_BY_STR[]
Definition ld2450.cpp:88
constexpr StringToUint8 BAUD_RATES_BY_STR[]
Definition ld2450.cpp:69
constexpr Uint8ToString ZONE_TYPE_BY_UINT[]
Definition ld2450.cpp:82
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
std::vector< uint8_t > bytes
Definition sml_parser.h:13
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
bool mac_address_is_valid(const uint8_t *mac)
Check if the MAC address is not all zeros or all ones.
Definition helpers.cpp:594
std::string size_t len
Definition helpers.h:279
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:280
std::string format_mac_address_pretty(const uint8_t *mac)
Definition helpers.cpp:244
std::string str_sprintf(const char *fmt,...)
Definition helpers.cpp:208
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:29
Application App
Global storage of Application pointer - only one Application can exist.
number::Number * y2
Definition ld2450.h:73
number::Number * x2
Definition ld2450.h:72
number::Number * x1
Definition ld2450.h:70
number::Number * y1
Definition ld2450.h:71