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