ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ld6002b.cpp
Go to the documentation of this file.
1#include "ld6002b.h"
2#include "esphome/core/log.h"
3#include <algorithm>
4#include <cinttypes>
5#include <cmath>
6#include <cstdio>
7#include <cstring>
8
9namespace esphome::ld6002b {
10
11ESPHOME_LOG_TAG(TAG, "ld6002b");
12
13static constexpr uint8_t TF_SOF = 0x01;
14static constexpr uint32_t SETUP_DELAY_MS = 100;
15
16// Command/message types
17static constexpr uint16_t TYPE_CONTROL = 0x0201;
18static constexpr uint16_t TYPE_SET_AREA = 0x0202;
19static constexpr uint16_t TYPE_SET_HOLD_DELAY = 0x0203;
20static constexpr uint16_t TYPE_SET_Z_RANGE = 0x0204;
21static constexpr uint16_t TYPE_SET_LOW_POWER_SLEEP = 0x0205;
22
23static constexpr uint16_t TYPE_REPORT_TARGET = 0x0A04;
24static constexpr uint16_t TYPE_REPORT_POINT_CLOUD = 0x0A08;
25static constexpr uint16_t TYPE_REPORT_AREA_PRESENCE = 0x0A0A;
26static constexpr uint16_t TYPE_REPORT_INTERFERENCE_AREAS = 0x0A0B;
27static constexpr uint16_t TYPE_REPORT_DETECTION_AREAS = 0x0A0C;
28static constexpr uint16_t TYPE_REPORT_DELAY = 0x0A0D;
29static constexpr uint16_t TYPE_REPORT_SENSITIVITY = 0x0A0E;
30static constexpr uint16_t TYPE_REPORT_TRIGGER = 0x0A0F;
31static constexpr uint16_t TYPE_REPORT_Z_RANGE = 0x0A10;
32static constexpr uint16_t TYPE_REPORT_INSTALLATION = 0x0A11;
33static constexpr uint16_t TYPE_REPORT_LOW_POWER = 0x0A12;
34static constexpr uint16_t TYPE_REPORT_LOW_POWER_SLEEP = 0x0A13;
35static constexpr uint16_t TYPE_REPORT_WORK_MODE = 0x0A14;
36static constexpr uint16_t TYPE_QUERY_VERSION = 0xFFFF;
37
38// Control command values for TYPE_CONTROL
39static constexpr uint32_t CMD_AUTO_INTERFERENCE = 0x01;
40static constexpr uint32_t CMD_GET_AREAS = 0x02;
41static constexpr uint32_t CMD_CLEAR_INTERFERENCE = 0x03;
42static constexpr uint32_t CMD_RESET_DETECTION_AREA = 0x04;
43static constexpr uint32_t CMD_GET_DELAY = 0x05;
44static constexpr uint32_t CMD_POINT_CLOUD_ON = 0x06;
45static constexpr uint32_t CMD_POINT_CLOUD_OFF = 0x07;
46static constexpr uint32_t CMD_TARGET_DISPLAY_ON = 0x08;
47static constexpr uint32_t CMD_TARGET_DISPLAY_OFF = 0x09;
48static constexpr uint32_t CMD_SENSITIVITY_LOW = 0x0A;
49static constexpr uint32_t CMD_SENSITIVITY_MEDIUM = 0x0B;
50static constexpr uint32_t CMD_SENSITIVITY_HIGH = 0x0C;
51static constexpr uint32_t CMD_GET_SENSITIVITY = 0x0D;
52static constexpr uint32_t CMD_TRIGGER_SLOW = 0x0E;
53static constexpr uint32_t CMD_TRIGGER_MEDIUM = 0x0F;
54static constexpr uint32_t CMD_TRIGGER_FAST = 0x10;
55static constexpr uint32_t CMD_GET_TRIGGER = 0x11;
56static constexpr uint32_t CMD_GET_Z_RANGE = 0x12;
57static constexpr uint32_t CMD_INSTALL_TOP = 0x13;
58static constexpr uint32_t CMD_INSTALL_SIDE = 0x14;
59static constexpr uint32_t CMD_GET_INSTALLATION = 0x15;
60static constexpr uint32_t CMD_LOW_POWER_ON = 0x16;
61static constexpr uint32_t CMD_LOW_POWER_OFF = 0x17;
62static constexpr uint32_t CMD_GET_LOW_POWER = 0x18;
63static constexpr uint32_t CMD_GET_LOW_POWER_SLEEP = 0x19;
64static constexpr uint32_t CMD_RESET_UNATTENDED = 0x1A;
65
66static constexpr uint16_t TARGET_DATA_LEN = 20; // x,y,z,dop_idx,cluster_id
67static constexpr uint16_t AREA_DATA_LEN = 24; // 6 floats
68static constexpr uint16_t AREA_CONFIG_LEN = 28; // int32 + 6 floats
69static constexpr uint16_t AREA_PRESENCE_ENTRY_LEN = 4; // uint32 per detection area
70
71static constexpr uint8_t AREA_ID_DEFAULT = 4; // detection_area_0 for initial display
72
73static constexpr uint8_t VERSION_QUERY_DATA[] = {0x01, 0x01, 0x00, 0x00};
74
75#ifdef ESPHOME_LOG_HAS_VERBOSE
76static const char *control_command_name(uint32_t command) {
77 switch (command) {
78 case CMD_AUTO_INTERFERENCE:
79 return "auto_interference";
80 case CMD_GET_AREAS:
81 return "get_areas";
82 case CMD_CLEAR_INTERFERENCE:
83 return "clear_interference";
84 case CMD_RESET_DETECTION_AREA:
85 return "reset_detection_area";
86 case CMD_GET_DELAY:
87 return "get_delay";
88 case CMD_POINT_CLOUD_ON:
89 return "point_cloud_on";
90 case CMD_POINT_CLOUD_OFF:
91 return "point_cloud_off";
92 case CMD_TARGET_DISPLAY_ON:
93 return "target_display_on";
94 case CMD_TARGET_DISPLAY_OFF:
95 return "target_display_off";
96 case CMD_SENSITIVITY_LOW:
97 return "sensitivity_low";
98 case CMD_SENSITIVITY_MEDIUM:
99 return "sensitivity_medium";
100 case CMD_SENSITIVITY_HIGH:
101 return "sensitivity_high";
102 case CMD_GET_SENSITIVITY:
103 return "get_sensitivity";
104 case CMD_TRIGGER_SLOW:
105 return "trigger_slow";
106 case CMD_TRIGGER_MEDIUM:
107 return "trigger_medium";
108 case CMD_TRIGGER_FAST:
109 return "trigger_fast";
110 case CMD_GET_TRIGGER:
111 return "get_trigger";
112 case CMD_GET_Z_RANGE:
113 return "get_z_range";
114 case CMD_INSTALL_TOP:
115 return "install_top";
116 case CMD_INSTALL_SIDE:
117 return "install_side";
118 case CMD_GET_INSTALLATION:
119 return "get_installation";
120 case CMD_LOW_POWER_ON:
121 return "low_power_on";
122 case CMD_LOW_POWER_OFF:
123 return "low_power_off";
124 case CMD_GET_LOW_POWER:
125 return "get_low_power";
126 case CMD_GET_LOW_POWER_SLEEP:
127 return "get_low_power_sleep";
128 case CMD_RESET_UNATTENDED:
129 return "reset_unattended";
130 default:
131 return "unknown";
132 }
133}
134
135static const char *frame_type_name(uint16_t type) {
136 switch (type) {
137 case TYPE_CONTROL:
138 return "control";
139 case TYPE_SET_AREA:
140 return "set_area";
141 case TYPE_SET_HOLD_DELAY:
142 return "set_hold_delay";
143 case TYPE_SET_Z_RANGE:
144 return "set_z_range";
145 case TYPE_SET_LOW_POWER_SLEEP:
146 return "set_low_power_sleep";
147 case TYPE_REPORT_TARGET:
148 return "report_target";
149 case TYPE_REPORT_POINT_CLOUD:
150 return "report_point_cloud";
151 case TYPE_REPORT_AREA_PRESENCE:
152 return "report_area_presence";
153 case TYPE_REPORT_INTERFERENCE_AREAS:
154 return "report_interference_areas";
155 case TYPE_REPORT_DETECTION_AREAS:
156 return "report_detection_areas";
157 case TYPE_REPORT_DELAY:
158 return "report_delay";
159 case TYPE_REPORT_SENSITIVITY:
160 return "report_sensitivity";
161 case TYPE_REPORT_TRIGGER:
162 return "report_trigger";
163 case TYPE_REPORT_Z_RANGE:
164 return "report_z_range";
165 case TYPE_REPORT_INSTALLATION:
166 return "report_installation";
167 case TYPE_REPORT_LOW_POWER:
168 return "report_low_power";
169 case TYPE_REPORT_LOW_POWER_SLEEP:
170 return "report_low_power_sleep";
171 case TYPE_REPORT_WORK_MODE:
172 return "report_work_mode";
173 case TYPE_QUERY_VERSION:
174 return "query_version";
175 default:
176 return "unknown";
177 }
178}
179
180static bool is_expected_control_report(uint32_t command, uint16_t type) {
181 switch (command) {
182 case CMD_GET_AREAS:
183 return type == TYPE_REPORT_INTERFERENCE_AREAS || type == TYPE_REPORT_DETECTION_AREAS;
184 case CMD_GET_DELAY:
185 return type == TYPE_REPORT_DELAY;
186 case CMD_GET_SENSITIVITY:
187 return type == TYPE_REPORT_SENSITIVITY;
188 case CMD_GET_TRIGGER:
189 return type == TYPE_REPORT_TRIGGER;
190 case CMD_GET_Z_RANGE:
191 return type == TYPE_REPORT_Z_RANGE;
192 case CMD_GET_INSTALLATION:
193 return type == TYPE_REPORT_INSTALLATION;
194 case CMD_GET_LOW_POWER:
195 case CMD_LOW_POWER_ON:
196 case CMD_LOW_POWER_OFF:
197 return type == TYPE_REPORT_LOW_POWER;
198 case CMD_GET_LOW_POWER_SLEEP:
199 return type == TYPE_REPORT_LOW_POWER_SLEEP;
200 default:
201 return false;
202 }
203}
204#endif
205
206uint16_t LD6002BComponent::read_u16_be(const uint8_t *data) { return (static_cast<uint16_t>(data[0]) << 8) | data[1]; }
207
209 return static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
210 (static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
211}
212
213int32_t LD6002BComponent::read_int32_le(const uint8_t *data) {
214 uint32_t raw = read_u32_le(data);
215 int32_t value;
216 std::memcpy(&value, &raw, sizeof(value));
217 return value;
218}
219
220float LD6002BComponent::read_f32_le(const uint8_t *data) {
221 uint32_t raw = read_u32_le(data);
222 float value;
223 std::memcpy(&value, &raw, sizeof(value));
224 return value;
225}
226
227void LD6002BComponent::write_u32_le(uint8_t *data, uint32_t value) {
228 data[0] = value & 0xFF;
229 data[1] = (value >> 8) & 0xFF;
230 data[2] = (value >> 16) & 0xFF;
231 data[3] = (value >> 24) & 0xFF;
232}
233
234void LD6002BComponent::write_int32_le(uint8_t *data, int32_t value) {
235 write_u32_le(data, static_cast<uint32_t>(value));
236}
237
238void LD6002BComponent::write_f32_le(uint8_t *data, float value) {
240 std::memcpy(&raw, &value, sizeof(raw));
241 write_u32_le(data, raw);
242}
243
245 // Only the point cloud stream needs the larger frame; nothing resizes the buffer after setup.
246 bool point_cloud_configured = false;
247#ifdef USE_SENSOR
248 point_cloud_configured = point_cloud_configured || this->point_count_sensor_ != nullptr;
249#endif
250#ifdef USE_SWITCH
251 point_cloud_configured = point_cloud_configured || this->point_cloud_switch_ != nullptr;
252#endif
253 this->max_data_len_ = point_cloud_configured ? DEFAULT_MAX_DATA_LEN_POINT_CLOUD : DEFAULT_MAX_DATA_LEN;
254 // One allocation for the component lifetime; the parser reuses it for the header and every payload.
255 RAMAllocator<uint8_t> allocator;
256 this->data_buf_ = allocator.allocate(this->max_data_len_);
257 if (this->data_buf_ == nullptr) {
258 this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
259 return;
260 }
261 if (this->wakeup_pin_ != nullptr) {
262 this->wakeup_pin_->setup();
263 this->wakeup_pin_->digital_write(true);
264 }
265
266 this->set_timeout(SETUP_DELAY_MS, [this]() {
267 bool want_target_stream = false;
268#ifdef USE_SENSOR
269 want_target_stream = want_target_stream || this->target_count_sensor_ != nullptr;
270 if (!want_target_stream) {
271 for (const auto &target : this->targets_) {
272 if (target.x != nullptr || target.y != nullptr || target.z != nullptr || target.dop_idx != nullptr ||
273 target.cluster_id != nullptr) {
274 want_target_stream = true;
275 break;
276 }
277 }
278 }
279#endif
280#ifdef USE_BINARY_SENSOR
281 want_target_stream = want_target_stream || this->presence_binary_sensor_ != nullptr;
282 if (!want_target_stream) {
283 for (auto *sensor : this->target_presence_) {
284 if (sensor != nullptr) {
285 want_target_stream = true;
286 break;
287 }
288 }
289 }
290#endif
291#ifdef USE_TEXT_SENSOR
292 // The work mode fallback reads presence off this stream, so it counts as a
293 // consumer of it here. This only feeds the automatic branch below: with a
294 // target_display switch configured that switch still decides, and the
295 // fallback weighs no presence at all while the stream is off.
296 want_target_stream = want_target_stream || this->work_mode_text_sensor_ != nullptr;
297#endif
298 bool target_display_controlled = false;
299#ifdef USE_SWITCH
300 if (this->target_display_switch_ != nullptr) {
301 target_display_controlled = true;
302 // Nothing reports this switch back, so its restored state is the only state
303 // there is. Restoring through the switch keeps its inversion in the path:
304 // the restored value is logical, and driving the switch is what turns it
305 // into the raw command, the published state and the stream flag.
306 const bool state = this->target_display_switch_->get_initial_state_with_restore_mode().value_or(true);
307 this->target_display_switch_->control(state);
308 }
309#endif
310 if (!target_display_controlled) {
311 // No switch: the stream follows its consumers. With none, nothing is sent
312 // and the module's own default stands -- but the reports are gated out
313 // regardless, because there is nothing configured for them to feed.
314 this->target_display_enabled_ = want_target_stream;
315 if (want_target_stream) {
316 this->send_control_command_(CMD_TARGET_DISPLAY_ON);
317 }
318 }
319
320 bool point_cloud_controlled = false;
321#ifdef USE_SWITCH
322 if (this->point_cloud_switch_ != nullptr) {
323 point_cloud_controlled = true;
324 // The switch owns the stream, so it is also what applies the restored state:
325 // driving it rather than the module keeps the entity's inversion in the path.
326 const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false);
327 this->point_cloud_switch_->control(state);
328 }
329#endif
330 if (!point_cloud_controlled) {
331 // No switch: the stream follows the sensor that reads it, which is also what
332 // the frame buffer above was sized for.
333 bool want_point_cloud = false;
334#ifdef USE_SENSOR
335 want_point_cloud = this->point_count_sensor_ != nullptr;
336#endif
337 this->send_control_command_(want_point_cloud ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
338 this->point_cloud_enabled_ = want_point_cloud;
339 }
340
341#ifdef USE_SELECT
342 if (this->sensitivity_select_ != nullptr) {
343 this->send_control_command_(CMD_GET_SENSITIVITY);
344 }
345 if (this->trigger_speed_select_ != nullptr) {
346 this->send_control_command_(CMD_GET_TRIGGER);
347 }
348 if (this->installation_select_ != nullptr) {
349 this->send_control_command_(CMD_GET_INSTALLATION);
350 }
351#endif
352#ifdef USE_NUMBER
353 if (this->z_min_number_ != nullptr || this->z_max_number_ != nullptr) {
354 this->send_control_command_(CMD_GET_Z_RANGE);
355 }
356 if (this->low_power_sleep_number_ != nullptr) {
357 this->send_control_command_(CMD_GET_LOW_POWER_SLEEP);
358 }
359 if (this->hold_delay_number_ != nullptr) {
360 this->send_control_command_(CMD_GET_DELAY);
361 }
362#endif
363#ifdef USE_SWITCH
364 bool want_low_power = this->low_power_switch_ != nullptr;
365 if (want_low_power) {
366 // The module reports this one back, so the query below confirms what it took.
367 // Driving the switch applies its inversion; it also marks the restored value
368 // as reported, so the work mode fallback runs on that until the query lands.
369 const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false);
370 this->low_power_switch_->control(state);
371 }
372#else
373 bool want_low_power = false;
374#endif
375#ifdef USE_TEXT_SENSOR
376 want_low_power = want_low_power || this->work_mode_text_sensor_ != nullptr;
377#endif
378 if (want_low_power) {
379 this->send_control_command_(CMD_GET_LOW_POWER);
380 }
381
382 bool want_area_report = false;
383#ifdef USE_SENSOR
384 for (const auto &area : this->interference_areas_) {
385 if (area.x_min != nullptr || area.x_max != nullptr || area.y_min != nullptr || area.y_max != nullptr ||
386 area.z_min != nullptr || area.z_max != nullptr) {
387 want_area_report = true;
388 break;
389 }
390 }
391 if (!want_area_report) {
392 for (const auto &area : this->detection_areas_) {
393 if (area.x_min != nullptr || area.x_max != nullptr || area.y_min != nullptr || area.y_max != nullptr ||
394 area.z_min != nullptr || area.z_max != nullptr) {
395 want_area_report = true;
396 break;
397 }
398 }
399 }
400#endif
401#ifdef USE_NUMBER
402 if (this->area_x_min_number_ != nullptr || this->area_x_max_number_ != nullptr ||
403 this->area_y_min_number_ != nullptr || this->area_y_max_number_ != nullptr ||
404 this->area_z_min_number_ != nullptr || this->area_z_max_number_ != nullptr) {
405 want_area_report = true;
406 }
407#endif
408 if (want_area_report) {
409 this->send_control_command_(CMD_GET_AREAS);
410 }
411
412 this->init_area_id_pref_();
413 this->init_version_pref_();
414
415#ifdef USE_TEXT_SENSOR
416 if (this->ota_version_text_sensor_ != nullptr) {
417 this->queue_command_(TYPE_QUERY_VERSION, VERSION_QUERY_DATA, sizeof(VERSION_QUERY_DATA));
418 }
419#endif
420 });
421}
422
424 ESP_LOGCONFIG(TAG,
425 "HLK-LD6002B:\n"
426 " Auto wake: %s\n"
427 " Max data length: %u",
428 this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
429 static_cast<unsigned>(this->max_data_len_));
430 if (this->wakeup_pin_ != nullptr) {
431 LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
432 ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_);
433 }
434#ifdef USE_SENSOR
435 LOG_SENSOR(" ", "Target Count", this->target_count_sensor_);
436 LOG_SENSOR(" ", "Point Count", this->point_count_sensor_);
437 for (auto &target : this->targets_) {
438 LOG_SENSOR(" ", "Target X", target.x);
439 LOG_SENSOR(" ", "Target Y", target.y);
440 LOG_SENSOR(" ", "Target Z", target.z);
441 LOG_SENSOR(" ", "Target Doppler Index", target.dop_idx);
442 LOG_SENSOR(" ", "Target Cluster ID", target.cluster_id);
443 }
444 for (auto &area : this->interference_areas_) {
445 LOG_SENSOR(" ", "Interference Area X Min", area.x_min);
446 LOG_SENSOR(" ", "Interference Area X Max", area.x_max);
447 LOG_SENSOR(" ", "Interference Area Y Min", area.y_min);
448 LOG_SENSOR(" ", "Interference Area Y Max", area.y_max);
449 LOG_SENSOR(" ", "Interference Area Z Min", area.z_min);
450 LOG_SENSOR(" ", "Interference Area Z Max", area.z_max);
451 }
452 for (auto &area : this->detection_areas_) {
453 LOG_SENSOR(" ", "Detection Area X Min", area.x_min);
454 LOG_SENSOR(" ", "Detection Area X Max", area.x_max);
455 LOG_SENSOR(" ", "Detection Area Y Min", area.y_min);
456 LOG_SENSOR(" ", "Detection Area Y Max", area.y_max);
457 LOG_SENSOR(" ", "Detection Area Z Min", area.z_min);
458 LOG_SENSOR(" ", "Detection Area Z Max", area.z_max);
459 }
460#endif
461#ifdef USE_BINARY_SENSOR
462 LOG_BINARY_SENSOR(" ", "Presence", this->presence_binary_sensor_);
463 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
464 LOG_BINARY_SENSOR(" ", "Target Presence", this->target_presence_[i]);
465 }
466 for (uint8_t i = 0; i < AREA_COUNT; i++) {
467 LOG_BINARY_SENSOR(" ", "Detection Area Presence", this->area_presence_[i]);
468 }
469#endif
470#ifdef USE_TEXT_SENSOR
471 LOG_TEXT_SENSOR(" ", "Work Mode", this->work_mode_text_sensor_);
472 LOG_TEXT_SENSOR(" ", "OTA Version", this->ota_version_text_sensor_);
473#endif
474#ifdef USE_NUMBER
475 LOG_NUMBER(" ", "Hold Delay", this->hold_delay_number_);
476 LOG_NUMBER(" ", "Z Min", this->z_min_number_);
477 LOG_NUMBER(" ", "Z Max", this->z_max_number_);
478 LOG_NUMBER(" ", "Low Power Sleep", this->low_power_sleep_number_);
479 LOG_NUMBER(" ", "Area X Min", this->area_x_min_number_);
480 LOG_NUMBER(" ", "Area X Max", this->area_x_max_number_);
481 LOG_NUMBER(" ", "Area Y Min", this->area_y_min_number_);
482 LOG_NUMBER(" ", "Area Y Max", this->area_y_max_number_);
483 LOG_NUMBER(" ", "Area Z Min", this->area_z_min_number_);
484 LOG_NUMBER(" ", "Area Z Max", this->area_z_max_number_);
485#endif
486#ifdef USE_SWITCH
487 LOG_SWITCH(" ", "Low Power", this->low_power_switch_);
488 LOG_SWITCH(" ", "Point Cloud", this->point_cloud_switch_);
489 LOG_SWITCH(" ", "Target Display", this->target_display_switch_);
490#endif
491#ifdef USE_SELECT
492 LOG_SELECT(" ", "Sensitivity", this->sensitivity_select_);
493 LOG_SELECT(" ", "Trigger Speed", this->trigger_speed_select_);
494 LOG_SELECT(" ", "Installation Mode", this->installation_select_);
495 LOG_SELECT(" ", "Area ID", this->area_id_select_);
496#endif
497}
498
500 while (this->available()) {
501 uint8_t byte = this->read();
502 this->parse_byte_(byte);
503 }
505}
506
509 this->header_pos_ = 0;
510 this->header_xor_ = 0;
511 this->data_len_ = 0;
512 this->data_pos_ = 0;
513 this->data_xor_ = 0;
514 this->discard_remaining_ = 0;
515 this->frame_oversize_ = false;
516}
517
519 switch (this->parse_state_) {
521 // discard_remaining_ is unsigned: an unguarded decrement at zero would swallow 4 GB of stream.
522 if (this->discard_remaining_ > 0) {
523 this->discard_remaining_--;
524 }
525 if (this->discard_remaining_ == 0) {
526 this->reset_parser_();
527 }
528 return;
529 case ParseState::SOF:
530 if (byte != TF_SOF)
531 return;
532 this->header_pos_ = 0;
533 this->header_xor_ = 0;
534 this->header_xor_ ^= byte;
536 return;
538 if (this->header_pos_ < 6) {
539 this->data_buf_[this->header_pos_] = byte;
540 this->header_xor_ ^= byte;
541 this->header_pos_++;
542 if (this->header_pos_ == 6) {
543 this->frame_id_ = read_u16_be(this->data_buf_);
544 this->data_len_ = read_u16_be(this->data_buf_ + 2);
545 this->frame_type_ = read_u16_be(this->data_buf_ + 4);
546 // The length is only trustworthy once the header checksum has been verified, so just
547 // remember that the frame is oversized and let the HCK state act on it.
548 this->frame_oversize_ = this->data_len_ > this->max_data_len_;
550 }
551 }
552 return;
553 case ParseState::HCK: {
554 uint8_t expected = static_cast<uint8_t>(~this->header_xor_);
555 if (byte != expected) {
556 ESP_LOGV(TAG, "Header checksum mismatch");
557 this->reset_parser_();
558 return;
559 }
560 if (this->frame_oversize_) {
561 ESP_LOGW(TAG, "Frame too large: %u", this->data_len_);
562 // The header is verified, so the length can be trusted: skip the payload and its checksum.
563 this->discard_remaining_ = static_cast<uint32_t>(this->data_len_) + 1;
565 return;
566 }
567 if (this->data_len_ == 0) {
568 this->handle_frame_(this->frame_type_, nullptr, 0);
569 this->reset_parser_();
570 } else {
571 this->data_pos_ = 0;
572 this->data_xor_ = 0;
574 }
575 return;
576 }
577 case ParseState::DATA:
578 this->data_buf_[this->data_pos_++] = byte;
579 this->data_xor_ ^= byte;
580 if (this->data_pos_ >= this->data_len_) {
582 }
583 return;
584 case ParseState::DCK: {
585 uint8_t expected = static_cast<uint8_t>(~this->data_xor_);
586 if (byte == expected) {
587 this->handle_frame_(this->frame_type_, this->data_buf_, this->data_len_);
588 } else {
589 ESP_LOGV(TAG, "Data checksum mismatch");
590 }
591 this->reset_parser_();
592 return;
593 }
594 }
595}
596
597void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_t len) {
598 this->last_traffic_ms_ = millis();
599 if (this->stale_ack_count_ > 0 && millis() - this->stale_ack_ms_ > STALE_ACK_MAX_AGE_MS) {
600 this->stale_ack_count_ = 0;
601 }
602 // ACKs carry no id and arrive in send order: debt from earlier attempts is paid before the active command.
603 if (len == 0 && this->stale_ack_count_ > 0 && this->stale_ack_type_ == type) {
604 this->stale_ack_count_--;
605 ESP_LOGV(TAG, "Ignoring ACK for command 0x%04X from an earlier attempt (module frame 0x%04X)", type,
606 this->frame_id_);
607 return;
608 }
609 if (len == 0 && this->command_active_ && this->command_sent_ && type == this->active_command_.type) {
610 ESP_LOGV(TAG, "ACK for command 0x%04X (module frame 0x%04X)", type, this->frame_id_);
611 const bool refresh_areas = (type == TYPE_SET_AREA) && this->area_write_in_flight_;
612 // This settles one expected reply; the rest stay owed and become the debt for the next command.
613 this->send_generation_++;
614 this->stale_ack_type_ = type;
615 this->stale_ack_count_ = this->acks_expected_ > 0 ? static_cast<uint8_t>(this->acks_expected_ - 1) : 0;
616 this->stale_ack_ms_ = millis();
617 this->command_active_ = false;
618 this->command_sent_ = false;
619 this->last_send_ms_ = 0;
621 if (refresh_areas) {
622 this->area_write_in_flight_ = false;
623 this->set_timeout(AREA_REFRESH_TIMEOUT_ID, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
624 }
625 return;
626 }
627
628#ifdef ESPHOME_LOG_HAS_VERBOSE
629 const uint32_t active_control_command =
630 (this->command_active_ && this->active_command_.type == TYPE_CONTROL && this->active_command_.len >= 4)
631 ? read_u32_le(this->active_command_.data.data())
632 : 0;
633 if (active_control_command != 0 && is_expected_control_report(active_control_command, type)) {
634 ESP_LOGV(TAG, "Received %s (0x%04X) while waiting for %s (0x%02" PRIX32 ") ACK", frame_type_name(type), type,
635 control_command_name(active_control_command), active_control_command);
636 }
637#endif
638
639 switch (type) {
640 case TYPE_REPORT_TARGET:
641 this->handle_target_report_(data, len);
642 break;
643 case TYPE_REPORT_POINT_CLOUD:
644 this->handle_point_cloud_(data, len);
645 break;
646 case TYPE_REPORT_AREA_PRESENCE:
647 this->handle_area_presence_(data, len);
648 break;
649 case TYPE_REPORT_INTERFERENCE_AREAS:
650 this->handle_area_report_(true, data, len);
651 break;
652 case TYPE_REPORT_DETECTION_AREAS:
653 this->handle_area_report_(false, data, len);
654 break;
655 case TYPE_REPORT_DELAY:
656 this->handle_delay_report_(data, len);
657 break;
658 case TYPE_REPORT_SENSITIVITY:
659 this->handle_sensitivity_report_(data, len);
660 break;
661 case TYPE_REPORT_TRIGGER:
663 break;
664 case TYPE_REPORT_Z_RANGE:
665 this->handle_z_range_report_(data, len);
666 break;
667 case TYPE_REPORT_INSTALLATION:
668 this->handle_installation_report_(data, len);
669 break;
670 case TYPE_REPORT_LOW_POWER:
671 this->handle_low_power_report_(data, len);
672 break;
673 case TYPE_REPORT_LOW_POWER_SLEEP:
675 break;
676 case TYPE_REPORT_WORK_MODE:
677 this->handle_work_mode_report_(data, len);
678 break;
679 case TYPE_QUERY_VERSION:
680 this->handle_version_report_(data, len);
681 break;
682 default:
683 break;
684 }
685}
686
687void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len) {
688 // The module stops streaming when it acts on the command, not when the command
689 // is queued, so trailing frames after an off must not repopulate what
690 // set_switch_state just cleared.
691 if (!this->target_display_enabled_) {
692 return;
693 }
694 if (len < 4)
695 return;
696
697 uint32_t target_num = read_u32_le(data);
698 uint16_t available = (len - 4) / TARGET_DATA_LEN;
699 // Un-narrowed: a report of e.g. 256 targets must not truncate to 0 and read as "absent".
700 const uint32_t reported = std::min<uint32_t>(target_num, available);
701 uint8_t count = static_cast<uint8_t>(std::min<uint32_t>(reported, MAX_TARGETS));
702
703 // The module re-sorts its array by cluster id, so slots key on the id to track the person.
704 std::array<int32_t, MAX_TARGETS> wire_cluster{};
705 std::array<bool, MAX_TARGETS> wire_placed{};
706 std::array<bool, MAX_TARGETS> slot_seen{};
707 std::array<uint8_t, MAX_TARGETS> slot_wire{};
708 for (uint8_t i = 0; i < count; i++) {
709 uint16_t cluster_offset = 4 + (i * TARGET_DATA_LEN) + 16;
710 wire_cluster[i] = static_cast<int32_t>(read_u32_le(data + cluster_offset));
711 }
712 for (uint8_t i = 0; i < count; i++) {
713 for (uint8_t s = 0; s < MAX_TARGETS; s++) {
714 if (this->slot_occupied_[s] && !slot_seen[s] && this->slot_cluster_[s] == wire_cluster[i]) {
715 slot_seen[s] = true;
716 wire_placed[i] = true;
717 slot_wire[s] = i;
718 break;
719 }
720 }
721 }
722 for (uint8_t s = 0; s < MAX_TARGETS; s++) {
723 if (!slot_seen[s]) {
724 this->slot_occupied_[s] = false;
725 }
726 }
727 for (uint8_t i = 0; i < count; i++) {
728 if (wire_placed[i]) {
729 continue;
730 }
731 for (uint8_t s = 0; s < MAX_TARGETS; s++) {
732 if (!this->slot_occupied_[s]) {
733 this->slot_occupied_[s] = true;
734 this->slot_cluster_[s] = wire_cluster[i];
735 slot_wire[s] = i;
736 break;
737 }
738 }
739 }
740
741#ifdef USE_SENSOR
742 if (this->target_count_sensor_ != nullptr) {
743 if (reported != this->last_target_count_) {
744 this->target_count_sensor_->publish_state(reported);
745 this->last_target_count_ = reported;
746 }
747 }
748#endif
749
750 this->target_presence_any_ = (reported > 0);
751#ifdef USE_BINARY_SENSOR
752 bool presence = this->target_presence_any_ || this->area_presence_any_;
753 if (this->presence_binary_sensor_ != nullptr) {
754 this->presence_binary_sensor_->publish_state(presence);
755 }
756#endif
758
759 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
760 bool has_target = this->slot_occupied_[i];
761 if (has_target) {
762#ifdef USE_SENSOR
763 uint16_t offset = 4 + (slot_wire[i] * TARGET_DATA_LEN);
764 float x = read_f32_le(data + offset + 0);
765 float y = read_f32_le(data + offset + 4);
766 float z = read_f32_le(data + offset + 8);
767 int32_t dop_idx = read_int32_le(data + offset + 12);
768 int32_t cluster_id = this->slot_cluster_[i];
769 TargetSensors &target = this->targets_[i];
770 if (target.x != nullptr) {
771 target.x->publish_state(x);
772 }
773 if (target.y != nullptr) {
774 target.y->publish_state(y);
775 }
776 if (target.z != nullptr) {
777 target.z->publish_state(z);
778 }
779 if (target.dop_idx != nullptr) {
780 target.dop_idx->publish_state(static_cast<float>(dop_idx));
781 }
782 if (target.cluster_id != nullptr) {
783 if (!this->last_cluster_id_valid_[i] || cluster_id != this->last_cluster_id_[i]) {
784 target.cluster_id->publish_state(static_cast<float>(cluster_id));
785 this->last_cluster_id_[i] = cluster_id;
786 this->last_cluster_id_valid_[i] = true;
787 }
788 }
789#endif
790 } else {
791#ifdef USE_SENSOR
792 this->clear_target_slot_(i);
793#endif
794 }
795#ifdef USE_BINARY_SENSOR
796 if (this->target_presence_[i] != nullptr) {
797 // publish_state() already skips unchanged states, no manual de-dup needed.
798 this->target_presence_[i]->publish_state(has_target);
799 }
800#endif
801#ifdef USE_SENSOR
802 this->last_target_presence_[i] = has_target;
803#endif
804 }
805}
806
807void LD6002BComponent::handle_point_cloud_(const uint8_t *data, uint16_t len) {
808 // Same window as the target stream: a frame already in flight must not put the
809 // count back after the switch cleared it.
810 if (!this->point_cloud_enabled_) {
811 return;
812 }
813 if (len < 4)
814 return;
815
816#ifdef USE_SENSOR
817 uint32_t point_num = read_u32_le(data);
818 if (this->point_count_sensor_ != nullptr) {
819 if (point_num != this->last_point_count_) {
820 this->point_count_sensor_->publish_state(point_num);
821 this->last_point_count_ = point_num;
822 }
823 }
824#endif
825}
826
827// 0x0A0A carries one uint32 per detection area -- the protocol names the four
828// fields detection_state_area0..3 -- so this covers area ids 4..7 only. The
829// interference areas have no presence report: a target inside one is what they
830// exist to suppress.
831void LD6002BComponent::handle_area_presence_(const uint8_t *data, uint16_t len) {
832 const uint16_t needed = AREA_COUNT * AREA_PRESENCE_ENTRY_LEN;
833 if (len < needed)
834 return;
835
836 this->area_presence_any_ = false;
837 for (uint8_t i = 0; i < AREA_COUNT; i++) {
838 uint32_t state = read_u32_le(data + (i * AREA_PRESENCE_ENTRY_LEN));
839 bool present = state != 0;
840 this->area_presence_any_ = this->area_presence_any_ || present;
841#ifdef USE_BINARY_SENSOR
842 if (this->area_presence_[i] != nullptr) {
843 this->area_presence_[i]->publish_state(present);
844 }
845#endif
846 }
847
848#ifdef USE_BINARY_SENSOR
849 bool presence = this->target_presence_any_ || this->area_presence_any_;
850 if (this->presence_binary_sensor_ != nullptr) {
851 this->presence_binary_sensor_->publish_state(presence);
852 }
853#endif
855}
856
857void LD6002BComponent::handle_area_report_(bool interference, const uint8_t *data, uint16_t len) {
858 uint16_t needed = AREA_COUNT * AREA_DATA_LEN;
859 if (len < needed)
860 return;
861
862 for (uint8_t i = 0; i < AREA_COUNT; i++) {
863 uint16_t offset = i * AREA_DATA_LEN;
864 float x_min = read_f32_le(data + offset + 0);
865 float x_max = read_f32_le(data + offset + 4);
866 float y_min = read_f32_le(data + offset + 8);
867 float y_max = read_f32_le(data + offset + 12);
868 float z_min = read_f32_le(data + offset + 16);
869 float z_max = read_f32_le(data + offset + 20);
870
871#ifdef USE_SENSOR
872 AreaSensors &area = interference ? this->interference_areas_[i] : this->detection_areas_[i];
873 if (area.x_min != nullptr)
874 area.x_min->publish_state(x_min);
875 if (area.x_max != nullptr)
876 area.x_max->publish_state(x_max);
877 if (area.y_min != nullptr)
878 area.y_min->publish_state(y_min);
879 if (area.y_max != nullptr)
880 area.y_max->publish_state(y_max);
881 if (area.z_min != nullptr)
882 area.z_min->publish_state(z_min);
883 if (area.z_max != nullptr)
884 area.z_max->publish_state(z_max);
885#endif
886
887 AreaConfig &store = interference ? this->interference_area_values_[i] : this->detection_area_values_[i];
888 store.x_min = x_min;
889 store.x_max = x_max;
890 store.y_min = y_min;
891 store.y_max = y_max;
892 store.z_min = z_min;
893 store.z_max = z_max;
894
895 uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
896 bool selected_interference = selected_id < AREA_COUNT;
897 uint8_t selected_index = selected_interference ? selected_id : static_cast<uint8_t>(selected_id - AREA_COUNT);
898 if (selected_interference == interference && selected_index == i) {
899 this->update_area_numbers_(store);
900 }
901 }
902 this->try_apply_pending_area_(interference);
903}
904
905void LD6002BComponent::handle_delay_report_(const uint8_t *data, uint16_t len) {
906 if (len < 4)
907 return;
908#ifdef USE_NUMBER
909 uint32_t delay = read_u32_le(data);
911#endif
912}
913
914void LD6002BComponent::handle_sensitivity_report_(const uint8_t *data, uint16_t len) {
915 if (len < 1)
916 return;
917#ifdef USE_SELECT
918 if (this->sensitivity_select_ == nullptr)
919 return;
920 uint8_t value = data[0];
921 if (value <= 2) {
923 }
924#endif
925}
926
927void LD6002BComponent::handle_trigger_speed_report_(const uint8_t *data, uint16_t len) {
928 if (len < 1)
929 return;
930#ifdef USE_SELECT
931 if (this->trigger_speed_select_ == nullptr)
932 return;
933 uint8_t value = data[0];
934 if (value <= 2) {
936 }
937#endif
938}
939
940void LD6002BComponent::handle_z_range_report_(const uint8_t *data, uint16_t len) {
941 if (len < 8)
942 return;
943 float z_min = read_f32_le(data);
944 float z_max = read_f32_le(data + 4);
945 this->z_min_ = z_min;
946 this->z_max_ = z_max;
947#ifdef USE_NUMBER
948 this->publish_number_clamped_(this->z_min_number_, z_min);
949 this->publish_number_clamped_(this->z_max_number_, z_max);
950#endif
951}
952
953void LD6002BComponent::handle_installation_report_(const uint8_t *data, uint16_t len) {
954 if (len < 1)
955 return;
956#ifdef USE_SELECT
957 if (this->installation_select_ == nullptr)
958 return;
959 uint8_t value = data[0];
960 if (value <= 1) {
962 }
963#endif
964}
965
966void LD6002BComponent::handle_low_power_report_(const uint8_t *data, uint16_t len) {
967 if (len < 1)
968 return;
969 bool enabled = data[0] != 0;
970 this->low_power_enabled_ = enabled;
971 this->low_power_reported_ = true;
972#ifdef USE_SWITCH
973 if (this->low_power_switch_ != nullptr) {
974 this->low_power_switch_->publish_state(enabled);
975 }
976#endif
978}
979
980void LD6002BComponent::handle_low_power_sleep_report_(const uint8_t *data, uint16_t len) {
981 if (len < 4)
982 return;
983#ifdef USE_NUMBER
984 uint32_t sleep_ms = read_u32_le(data);
986#endif
987}
988
989void LD6002BComponent::handle_work_mode_report_(const uint8_t *data, uint16_t len) {
990 if (len < 1)
991 return;
992 // Zero is the unattended half of this transition. Read outside the text sensor's
993 // ifdef because the area sensors do not need one configured to have gone stale.
994 const bool low_power = (data[0] == 0);
995#ifdef USE_TEXT_SENSOR
996 if (this->work_mode_text_sensor_ != nullptr) {
997 this->work_mode_reported_ = true;
998 this->publish_work_mode_(low_power);
999 }
1000#endif
1001 // Protocol V1.2 section 2.1.17: this message is sent only on the transition
1002 // between the unattended low-power mode and normal operation, so a zero is the
1003 // module stating that nobody is in any area. Not while a target is still being
1004 // tracked, though: the reset_unattended command is undocumented on whether it
1005 // forces this report, and where two statements from the module disagree the live
1006 // one wins.
1007 if (low_power && !this->target_presence_any_) {
1008 this->clear_area_presence_();
1009 }
1010}
1011
1013#ifdef USE_TEXT_SENSOR
1014 if (this->work_mode_text_sensor_ == nullptr || this->work_mode_reported_) {
1015 return;
1016 }
1017 if (!this->low_power_reported_) {
1018 return;
1019 }
1020 // Target presence is only meaningful while the stream that maintains it runs.
1021 // Area presence keeps its own report, so it still counts with the target stream
1022 // off and low power alone decides only when neither half has anything to say.
1023 const bool presence = (this->target_display_enabled_ && this->target_presence_any_) || this->area_presence_any_;
1024 this->publish_work_mode_(this->low_power_enabled_ && !presence);
1025#endif
1026}
1027
1029#ifdef USE_TEXT_SENSOR
1030 if (this->work_mode_text_sensor_ == nullptr) {
1031 return;
1032 }
1033 if (this->last_work_mode_valid_ && this->last_work_mode_low_power_ == low_power) {
1034 return;
1035 }
1036 this->work_mode_text_sensor_->publish_state(low_power ? "low_power" : "normal");
1037 this->last_work_mode_valid_ = true;
1038 this->last_work_mode_low_power_ = low_power;
1039#endif
1040}
1041
1042#ifdef USE_NUMBER
1044 if (number == nullptr)
1045 return;
1046 if (std::isnan(value)) {
1047 // NAN is this component's "the module has not told us yet". Publishing it on an
1048 // entity that has never had a state would report a nan where unknown is the
1049 // truth; on one that already shows a value it is the only way to say that value
1050 // no longer describes the selected area.
1051 if (number->has_state()) {
1052 number->publish_state(value);
1053 }
1054 return;
1055 }
1056 const float min_value = number->traits.get_min_value();
1057 const float max_value = number->traits.get_max_value();
1058 // Outside the declared range the user cannot write the value back, so publish
1059 // what they can reach and say what the module actually sent.
1060 if (value < min_value || value > max_value) {
1061 ESP_LOGW(TAG, "'%s': module reported %.1f, clamped to %.1f..%.1f", number->get_name().c_str(), value, min_value,
1062 max_value);
1063 value = std::clamp(value, min_value, max_value);
1064 }
1065 number->publish_state(value);
1066}
1067#endif
1068
1069void LD6002BComponent::handle_version_report_(const uint8_t *data, uint16_t len) {
1070 if (len < 4)
1071 return;
1072#ifdef USE_TEXT_SENSOR
1073 if (this->ota_version_text_sensor_ == nullptr)
1074 return;
1075 uint8_t project = data[0];
1076 uint8_t major = data[1];
1077 uint8_t minor = data[2];
1078 uint8_t patch = data[3];
1079 char buf[32];
1080 if (project == 0) {
1081 std::snprintf(buf, sizeof(buf), "%u.%u.%u", major, minor, patch);
1082 } else {
1083 std::snprintf(buf, sizeof(buf), "p%u %u.%u.%u", project, major, minor, patch);
1084 }
1086 this->save_version_pref_(buf);
1087#endif
1088}
1089
1090bool LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_t len) {
1091 if (len > CMD_MAX_DATA_LEN) {
1092 ESP_LOGW(TAG, "Command data too large: %u", len);
1093 return false;
1094 }
1095 if (this->cmd_count_ >= CMD_QUEUE_SIZE) {
1096 ESP_LOGW(TAG, "Command queue full, dropping command 0x%04X", type);
1097 return false;
1098 }
1099
1100 PendingCommand &cmd = this->cmd_queue_[this->cmd_tail_];
1101 cmd.type = type;
1102 cmd.len = len;
1103 if (len > 0 && data != nullptr) {
1104 std::memcpy(cmd.data.data(), data, len);
1105 }
1106
1107 this->cmd_tail_ = (this->cmd_tail_ + 1) % CMD_QUEUE_SIZE;
1108 this->cmd_count_++;
1109 this->process_command_queue_();
1110 return true;
1111}
1112
1114 uint32_t now = millis();
1115 if (this->command_active_) {
1116 // A sleeping module consumes the opening attempt as its wake-up instead of answering it.
1117 const uint32_t ack_timeout = this->attempts_sent_ <= 1 ? CMD_FIRST_ACK_TIMEOUT_MS : CMD_ACK_TIMEOUT_MS;
1118 if (this->command_sent_ && now - this->last_send_ms_ >= ack_timeout) {
1119 const uint32_t active_control_command =
1120 (this->active_command_.type == TYPE_CONTROL && this->active_command_.len >= 4)
1121 ? read_u32_le(this->active_command_.data.data())
1122 : 0;
1123 if (this->retries_left_ > 0) {
1124#ifdef ESPHOME_LOG_HAS_VERBOSE
1125 if (active_control_command != 0) {
1126 ESP_LOGV(TAG, "Retrying %s (0x%02" PRIX32 "), %u attempt(s) remaining",
1127 control_command_name(active_control_command), active_control_command, this->retries_left_);
1128 } else {
1129 // Writes without a control subcommand (hold delay, z-range) had no retry trace at all.
1130 ESP_LOGV(TAG, "Retrying command 0x%04X, %u attempt(s) remaining", this->active_command_.type,
1131 this->retries_left_);
1132 }
1133#endif
1134 this->command_sent_ = false;
1135 this->last_send_ms_ = 0;
1136 this->send_command_(this->active_command_.type, this->active_command_.data.data(), this->active_command_.len);
1137 this->retries_left_--;
1138 } else {
1139 if (active_control_command != 0) {
1140 ESP_LOGW(TAG, "Command 0x%04X subcommand 0x%02" PRIX32 " timed out", this->active_command_.type,
1141 active_control_command);
1142 } else {
1143 ESP_LOGW(TAG, "Command 0x%04X timed out", this->active_command_.type);
1144 }
1145 if (this->active_command_.type == TYPE_SET_AREA) {
1146 this->area_write_in_flight_ = false;
1147 }
1148 // The deferred apply is waiting on the report this command would have
1149 // brought back, and nothing else re-arms it. Dropping it here is the
1150 // difference between one apply lost to a timeout and one that rides in on
1151 // an unrelated area report later, writing bounds the user has moved on from.
1152 if (active_control_command == CMD_GET_AREAS && this->deferred_apply_pending_) {
1153 this->deferred_apply_pending_ = false;
1155 ESP_LOGW(TAG, "Area read timed out, dropping deferred area apply");
1156 }
1157 // A reply may still be in flight for the attempt we just gave up on, so carry one over as
1158 // debt rather than clearing the ledger, or that late ACK would retire the successor. Only
1159 // one: reaching this point means nothing was answered at all, so the older attempts are
1160 // speculative, and carrying them would swallow the successor's own replies.
1161 const uint16_t owed = (this->stale_ack_type_ == this->active_command_.type ? this->stale_ack_count_ : 0) +
1162 (this->acks_expected_ > 0 ? 1 : 0);
1163 this->stale_ack_type_ = this->active_command_.type;
1164 this->stale_ack_count_ = static_cast<uint8_t>(std::min<uint16_t>(owed, 255));
1165 this->stale_ack_ms_ = now;
1166 this->send_generation_++;
1167 this->command_active_ = false;
1168 this->command_sent_ = false;
1169 this->last_send_ms_ = 0;
1170 }
1171 }
1172 return;
1173 }
1174
1175 if (this->cmd_count_ == 0)
1176 return;
1177
1178 this->active_command_ = this->cmd_queue_[this->cmd_head_];
1179 this->cmd_head_ = (this->cmd_head_ + 1) % CMD_QUEUE_SIZE;
1180 this->cmd_count_--;
1181
1182 this->send_generation_++;
1184 this->command_active_ = true;
1185 this->command_sent_ = false;
1186 this->last_send_ms_ = 0;
1187 this->attempts_sent_ = 0;
1188 this->acks_expected_ = 0;
1189 if (this->stale_ack_type_ != this->active_command_.type) {
1190 this->stale_ack_count_ = 0;
1191 }
1192 this->send_command_(this->active_command_.type, this->active_command_.data.data(), this->active_command_.len);
1193}
1194
1195void LD6002BComponent::send_command_(uint16_t type, const uint8_t *data, uint8_t len) {
1196 this->send_command_internal_(type, data, len, true);
1197}
1198
1199void LD6002BComponent::send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track) {
1200 if (len > CMD_MAX_DATA_LEN) {
1201 ESP_LOGW(TAG, "Command data too large: %u", len);
1202 if (track) {
1203 // Release the slot: an unwritten command is never acked and never times out.
1204 this->command_active_ = false;
1205 this->command_sent_ = false;
1206 this->last_send_ms_ = 0;
1207 }
1208 return;
1209 }
1210
1211 // Anonymous timeouts never replace each other; with a pulse already pending the module is waking anyway.
1212 if (this->auto_wake_ && this->wakeup_pin_ != nullptr && !this->wake_pulse_pending_) {
1213 // Snapshot the payload: the deferred write must not depend on state a completing command changes.
1214 if (len > 0 && data != nullptr) {
1215 std::memcpy(this->wake_scratch_.data(), data, len);
1216 }
1217 // A button pulse must not raise the pin in the middle of this one.
1219 this->wake_pulse_pending_ = true;
1220 this->wakeup_pin_->digital_write(false);
1221 const uint8_t generation = this->send_generation_;
1222 this->set_timeout(this->wakeup_pulse_ms_, [this, type, len, track, generation]() {
1223 this->wakeup_pin_->digital_write(true);
1224 this->wake_pulse_pending_ = false;
1225 // Anonymous timeouts are never cancelled, so a tracked pulse whose command has since been
1226 // retired must not transmit: the frame would land after its successor and be booked to it.
1227 if (track && generation != this->send_generation_) {
1228 return;
1229 }
1230 this->write_frame_(type, (len > 0) ? this->wake_scratch_.data() : nullptr, len, track);
1231 });
1232 return;
1233 }
1234
1235 this->write_frame_(type, data, len, track);
1236}
1237
1238void LD6002BComponent::write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track) {
1239 uint16_t frame_id = this->next_frame_id_++ & 0x7FFF;
1240 frame_id |= 0x8000;
1241
1242 uint8_t header_xor = 0;
1243 auto write_header = [&](uint8_t b) {
1244 this->write_byte(b);
1245 header_xor ^= b;
1246 };
1247
1248 write_header(TF_SOF);
1249 write_header((frame_id >> 8) & 0xFF);
1250 write_header(frame_id & 0xFF);
1251 write_header((len >> 8) & 0xFF);
1252 write_header(len & 0xFF);
1253 write_header((type >> 8) & 0xFF);
1254 write_header(type & 0xFF);
1255
1256 this->write_byte(static_cast<uint8_t>(~header_xor));
1257
1258 if (len > 0 && data != nullptr) {
1259 uint8_t data_xor = 0;
1260 for (uint8_t i = 0; i < len; i++) {
1261 this->write_byte(data[i]);
1262 data_xor ^= data[i];
1263 }
1264 this->write_byte(static_cast<uint8_t>(~data_xor));
1265 }
1266 const uint32_t now = millis();
1267 if (track) {
1268 // A frame sent to a module that has had time to fall asleep is its wake-up, and goes unanswered.
1269 if (this->last_traffic_ms_ != 0 && now - this->last_traffic_ms_ < MODULE_AWAKE_MS) {
1270 this->acks_expected_++;
1271 }
1272 this->last_send_ms_ = now;
1273 this->command_sent_ = true;
1274 this->attempts_sent_++;
1275 }
1276 this->last_traffic_ms_ = now;
1277}
1278
1280 uint8_t data[4];
1281 write_u32_le(data, command);
1282 return this->queue_command_(TYPE_CONTROL, data, sizeof(data));
1283}
1284
1286 // One frame carries both bounds, so half a range cannot be written.
1287 if (std::isnan(this->z_min_) || std::isnan(this->z_max_)) {
1288 ESP_LOGW(TAG, "Z range not written, other bound unknown");
1289 return;
1290 }
1291 // Both bounds are known and crossed; the frame has no way to say that.
1292 if (this->z_min_ > this->z_max_) {
1293 ESP_LOGW(TAG, "Z range not written, min above max");
1294 return;
1295 }
1296 uint8_t data[8];
1297 write_f32_le(data, this->z_min_);
1298 write_f32_le(data + 4, this->z_max_);
1299 this->queue_command_(TYPE_SET_Z_RANGE, data, sizeof(data));
1300}
1301
1303 if (!this->area_id_set_) {
1304 ESP_LOGW(TAG, "Area ID not selected; ignoring apply");
1305 return;
1306 }
1307 if (this->area_id_ >= AREA_ID_COUNT) {
1308 ESP_LOGW(TAG, "Invalid area id: %u", this->area_id_);
1309 return;
1310 }
1311
1312 const bool interference = this->area_id_ < AREA_COUNT;
1313 const uint8_t index = interference ? this->area_id_ : static_cast<uint8_t>(this->area_id_ - AREA_COUNT);
1314 AreaConfig desired = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1315 if (!std::isnan(this->area_x_min_))
1316 desired.x_min = this->area_x_min_;
1317 if (!std::isnan(this->area_x_max_))
1318 desired.x_max = this->area_x_max_;
1319 if (!std::isnan(this->area_y_min_))
1320 desired.y_min = this->area_y_min_;
1321 if (!std::isnan(this->area_y_max_))
1322 desired.y_max = this->area_y_max_;
1323 if (!std::isnan(this->area_z_min_))
1324 desired.z_min = this->area_z_min_;
1325 if (!std::isnan(this->area_z_max_))
1326 desired.z_max = this->area_z_max_;
1327
1328 if (std::isnan(desired.x_min) || std::isnan(desired.x_max) || std::isnan(desired.y_min) ||
1329 std::isnan(desired.y_max) || std::isnan(desired.z_min) || std::isnan(desired.z_max)) {
1330 // Ask first: a read that never reached the queue would leave a deferral waiting
1331 // on a report nobody requested, with the user's values already retired for it.
1332 if (!this->send_control_command_(CMD_GET_AREAS)) {
1333 ESP_LOGW(TAG, "Area read not queued; area config left unapplied");
1334 return;
1335 }
1336 this->deferred_apply_pending_ = true;
1337 this->pending_area_id_ = this->area_id_;
1338 // The ledger, not the mirror: the mirror also carries whatever the module last
1339 // reported for the axes the user never touched, and staging those would hand them
1340 // back later wearing the user's badge -- a module value the next report is then
1341 // kept away from. Staging only what was actually typed is also what makes the
1342 // replay's overlay right: the untouched axes come from the fresh report. An
1343 // empty ledger is a meaning rather than a gap, then: an apply with nothing
1344 // staged rewrites the area exactly as the report just described it, which is
1345 // what a direct apply with nothing staged already does.
1346 this->pending_area_updates_ = this->area_edits_;
1347 // Staged above, so they are the deferred apply's values now rather than an
1348 // unsent edit. Anything typed from here belongs to whatever the user does
1349 // next, which may well be a different area.
1350 this->area_edits_ = AreaConfig{};
1351 ESP_LOGI(TAG, "Area config incomplete; requesting current areas before applying");
1352 return;
1353 }
1354 // Only a write the module will actually see retires them.
1355 if (this->queue_area_config_(this->area_id_, desired)) {
1356 this->area_edits_ = AreaConfig{};
1357 }
1358}
1359
1361 // A command's own pulse raises the pin and writes after it, so ride along instead of
1362 // claiming the flag: claiming it would send that command down the immediate-write path
1363 // with the pin still low.
1364 if (this->wakeup_pin_ == nullptr || this->wake_pulse_pending_)
1365 return;
1366 this->wakeup_pin_->digital_write(false);
1368 [this]() { this->wakeup_pin_->digital_write(true); });
1369}
1370
1372 switch (type) {
1374 uint32_t delay = static_cast<uint32_t>(value);
1375 uint8_t data[4];
1376 write_u32_le(data, delay);
1377 this->queue_command_(TYPE_SET_HOLD_DELAY, data, sizeof(data));
1378 break;
1379 }
1380 case NumberType::Z_MIN:
1381 this->z_min_ = value;
1382 this->send_z_range_();
1383 break;
1384 case NumberType::Z_MAX:
1385 this->z_max_ = value;
1386 this->send_z_range_();
1387 break;
1389 uint32_t sleep_ms = static_cast<uint32_t>(value);
1390 uint8_t data[4];
1391 write_u32_le(data, sleep_ms);
1392 this->queue_command_(TYPE_SET_LOW_POWER_SLEEP, data, sizeof(data));
1393 break;
1394 }
1396 this->area_x_min_ = value;
1397 this->area_edits_.x_min = value;
1398 break;
1400 this->area_x_max_ = value;
1401 this->area_edits_.x_max = value;
1402 break;
1404 this->area_y_min_ = value;
1405 this->area_edits_.y_min = value;
1406 break;
1408 this->area_y_max_ = value;
1409 this->area_edits_.y_max = value;
1410 break;
1412 this->area_z_min_ = value;
1413 this->area_edits_.z_min = value;
1414 break;
1416 this->area_z_max_ = value;
1417 this->area_edits_.z_max = value;
1418 break;
1419 }
1420}
1421
1423 switch (type) {
1425 if (index == 0) {
1426 this->send_control_command_(CMD_SENSITIVITY_LOW);
1427 } else if (index == 1) {
1428 this->send_control_command_(CMD_SENSITIVITY_MEDIUM);
1429 } else if (index == 2) {
1430 this->send_control_command_(CMD_SENSITIVITY_HIGH);
1431 }
1432 break;
1434 if (index == 0) {
1435 this->send_control_command_(CMD_TRIGGER_SLOW);
1436 } else if (index == 1) {
1437 this->send_control_command_(CMD_TRIGGER_MEDIUM);
1438 } else if (index == 2) {
1439 this->send_control_command_(CMD_TRIGGER_FAST);
1440 }
1441 break;
1443 if (index == 0) {
1444 this->send_control_command_(CMD_INSTALL_TOP);
1445 } else if (index == 1) {
1446 this->send_control_command_(CMD_INSTALL_SIDE);
1447 }
1448 break;
1450 this->area_id_ = static_cast<uint8_t>(index);
1451 this->area_id_set_ = true;
1453 this->save_area_id_pref_(this->area_id_);
1454 break;
1455 }
1456}
1457
1459 // A report refreshes every axis the user is not in the middle of changing. An
1460 // unapplied edit is the one value here the module cannot know about, so taking
1461 // the report over it would discard what the user typed with nothing to show for it.
1462 const AreaConfig &edits = this->area_edits_;
1463 if (std::isnan(edits.x_min))
1464 this->area_x_min_ = area.x_min;
1465 if (std::isnan(edits.x_max))
1466 this->area_x_max_ = area.x_max;
1467 if (std::isnan(edits.y_min))
1468 this->area_y_min_ = area.y_min;
1469 if (std::isnan(edits.y_max))
1470 this->area_y_max_ = area.y_max;
1471 if (std::isnan(edits.z_min))
1472 this->area_z_min_ = area.z_min;
1473 if (std::isnan(edits.z_max))
1474 this->area_z_max_ = area.z_max;
1475 this->publish_area_numbers_();
1476}
1477
1478// The mirror, not the report: an axis a report was kept away from has to keep its
1479// displayed value too, or the entity and the value the next apply sends disagree.
1490
1492 if (area_id >= AREA_ID_COUNT)
1493 return;
1494 const bool interference = area_id < AREA_COUNT;
1495 const uint8_t index = interference ? area_id : static_cast<uint8_t>(area_id - AREA_COUNT);
1496 const AreaConfig &area = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1497 // The edits belonged to the area being navigated away from.
1498 this->area_edits_ = AreaConfig{};
1499 this->update_area_numbers_(area);
1500}
1501
1502bool LD6002BComponent::queue_area_config_(uint8_t area_id, const AreaConfig &desired) {
1503 // One frame carries all three pairs and cannot express a crossed one; the module
1504 // would keep a box nothing can ever be inside. Both callers arrive with the six
1505 // bounds resolved, so this is the last place that can say no -- and the return
1506 // value is how saying no reaches the caller, which must not then retire the edits
1507 // the user still has to fix.
1508 if (desired.x_min > desired.x_max || desired.y_min > desired.y_max || desired.z_min > desired.z_max) {
1509 ESP_LOGW(TAG, "Area %u not written, min above max", area_id);
1510 return false;
1511 }
1512 uint8_t data[AREA_CONFIG_LEN];
1513 write_int32_le(data, static_cast<int32_t>(area_id));
1514 write_f32_le(data + 4, desired.x_min);
1515 write_f32_le(data + 8, desired.x_max);
1516 write_f32_le(data + 12, desired.y_min);
1517 write_f32_le(data + 16, desired.y_max);
1518 write_f32_le(data + 20, desired.z_min);
1519 write_f32_le(data + 24, desired.z_max);
1520
1521 if (!this->queue_command_(TYPE_SET_AREA, data, sizeof(data))) {
1522 // Nothing is on its way, so the cache must not claim these bounds, the ack
1523 // refresh must not be armed for an ack that cannot come, and the values stay
1524 // the user's unsent edit.
1525 return false;
1526 }
1527 this->area_write_in_flight_ = true;
1528
1529 const bool interference = area_id < AREA_COUNT;
1530 const uint8_t index = interference ? area_id : static_cast<uint8_t>(area_id - AREA_COUNT);
1531 AreaConfig &store = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1532 store = desired;
1533 // The six numbers show one area at a time, and a deferred apply can land here for
1534 // an area the user has navigated away from. Same question handle_area_report_
1535 // asks before it touches them.
1536 const uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
1537 if (area_id == selected_id) {
1538 this->update_area_numbers_(store);
1539 }
1540 return true;
1541}
1542
1543void LD6002BComponent::try_apply_pending_area_(bool reported_interference) {
1544 if (!this->deferred_apply_pending_) {
1545 return;
1546 }
1547 if (this->pending_area_id_ >= AREA_ID_COUNT) {
1548 this->deferred_apply_pending_ = false;
1549 return;
1550 }
1551 const bool interference = this->pending_area_id_ < AREA_COUNT;
1552 const uint8_t index =
1553 interference ? this->pending_area_id_ : static_cast<uint8_t>(this->pending_area_id_ - AREA_COUNT);
1554 AreaConfig desired = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1555
1556 if (!std::isnan(this->pending_area_updates_.x_min))
1557 desired.x_min = this->pending_area_updates_.x_min;
1558 if (!std::isnan(this->pending_area_updates_.x_max))
1559 desired.x_max = this->pending_area_updates_.x_max;
1560 if (!std::isnan(this->pending_area_updates_.y_min))
1561 desired.y_min = this->pending_area_updates_.y_min;
1562 if (!std::isnan(this->pending_area_updates_.y_max))
1563 desired.y_max = this->pending_area_updates_.y_max;
1564 if (!std::isnan(this->pending_area_updates_.z_min))
1565 desired.z_min = this->pending_area_updates_.z_min;
1566 if (!std::isnan(this->pending_area_updates_.z_max))
1567 desired.z_max = this->pending_area_updates_.z_max;
1568
1569 if (std::isnan(desired.x_min) || std::isnan(desired.x_max) || std::isnan(desired.y_min) ||
1570 std::isnan(desired.y_max) || std::isnan(desired.z_min) || std::isnan(desired.z_max)) {
1571 // Only the report covering this area's half can still fill it in, and there is
1572 // exactly one of those per read. Once it has landed with a bound still unknown,
1573 // nothing further is coming and waiting means waiting forever.
1574 if (reported_interference == interference) {
1575 this->deferred_apply_pending_ = false;
1577 ESP_LOGW(TAG, "Dropping deferred area apply, area report incomplete");
1578 }
1579 return;
1580 }
1581
1582 const uint8_t area_id = this->pending_area_id_;
1583 this->deferred_apply_pending_ = false;
1584 if (!this->queue_area_config_(area_id, desired)) {
1585 // Nothing was queued, so this is a drop like the other two: hand the staged
1586 // values back rather than leaving them with no ledger to protect them.
1588 }
1589}
1590
1592#ifdef USE_SELECT
1593 if (this->area_id_select_ == nullptr) {
1594 return;
1595 }
1596 this->area_id_pref_ = this->area_id_select_->make_entity_preference<uint8_t>();
1597 this->area_id_pref_initialized_ = true;
1598
1599 uint8_t value = 0;
1600 if (!this->area_id_pref_.load(&value) || value >= AREA_ID_COUNT) {
1601 // No stored selection. The numbers are about to display this area either way,
1602 // so select it for real: a displayed area that apply_area then refuses to write
1603 // is the one combination the user cannot make sense of.
1604 value = AREA_ID_DEFAULT;
1605 }
1606 this->area_id_select_->publish_state(value);
1607 this->area_id_ = value;
1608 this->area_id_set_ = true;
1609 this->update_area_numbers_for_id_(value);
1610#endif
1611}
1612
1614#ifdef USE_SELECT
1615 if (!this->area_id_pref_initialized_) {
1616 return;
1617 }
1618 this->area_id_pref_.save(&value);
1619#endif
1620}
1621
1623#ifdef USE_TEXT_SENSOR
1624 if (this->ota_version_text_sensor_ == nullptr) {
1625 return;
1626 }
1628 this->version_pref_initialized_ = true;
1629
1630 VersionPref pref{};
1631 if (this->version_pref_.load(&pref) && pref.value[0] != '\0') {
1632 pref.value[sizeof(pref.value) - 1] = '\0';
1633 this->ota_version_text_sensor_->publish_state(pref.value);
1634 }
1635#endif
1636}
1637
1639#ifdef USE_TEXT_SENSOR
1640 if (!this->version_pref_initialized_) {
1641 return;
1642 }
1643 VersionPref pref{};
1644 std::strncpy(pref.value, value, sizeof(pref.value) - 1);
1645 pref.value[sizeof(pref.value) - 1] = '\0';
1646 this->version_pref_.save(&pref);
1647#endif
1648}
1649
1650#ifdef USE_SENSOR
1652 if (!this->last_target_presence_[index]) {
1653 return;
1654 }
1655 TargetSensors &target = this->targets_[index];
1656 if (target.x != nullptr) {
1657 target.x->publish_state(NAN);
1658 }
1659 if (target.y != nullptr) {
1660 target.y->publish_state(NAN);
1661 }
1662 if (target.z != nullptr) {
1663 target.z->publish_state(NAN);
1664 }
1665 if (target.dop_idx != nullptr) {
1666 target.dop_idx->publish_state(NAN);
1667 }
1668 if (target.cluster_id != nullptr) {
1669 target.cluster_id->publish_state(NAN);
1670 }
1671 // The slot is free: the next person's id is new even when it repeats this one.
1672 this->last_cluster_id_valid_[index] = false;
1673}
1674#endif
1675
1677 // The staged values become an unsent edit again, but only for the user who is
1678 // still looking at the area they were staged for; anyone else's ledger belongs to
1679 // the area they are on now.
1680 const uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
1681 if (this->pending_area_id_ != selected_id) {
1682 return;
1683 }
1684 // Axis by axis rather than a whole-struct assignment: the user can have edited
1685 // another bound while the deferral was in flight, and that edit is newer than
1686 // anything the deferral staged. Assigning over the ledger would drop it back to
1687 // NaN and let the next report take the value away. A live edit wins; only an axis
1688 // with nothing in the ledger takes its staged value back.
1689 //
1690 // The mirror moves with the ledger, because on the report path handle_area_report_
1691 // ran update_area_numbers_ before the replay, with the ledger still empty -- so the
1692 // mirror already holds the module's bounds and both the entities and the next apply
1693 // would build on them. On the timeout path no report arrived, the mirror still
1694 // holds the staged values, and this is an identity.
1695 const AreaConfig &staged = this->pending_area_updates_;
1696 if (std::isnan(this->area_edits_.x_min) && !std::isnan(staged.x_min)) {
1697 this->area_edits_.x_min = staged.x_min;
1698 this->area_x_min_ = staged.x_min;
1699 }
1700 if (std::isnan(this->area_edits_.x_max) && !std::isnan(staged.x_max)) {
1701 this->area_edits_.x_max = staged.x_max;
1702 this->area_x_max_ = staged.x_max;
1703 }
1704 if (std::isnan(this->area_edits_.y_min) && !std::isnan(staged.y_min)) {
1705 this->area_edits_.y_min = staged.y_min;
1706 this->area_y_min_ = staged.y_min;
1707 }
1708 if (std::isnan(this->area_edits_.y_max) && !std::isnan(staged.y_max)) {
1709 this->area_edits_.y_max = staged.y_max;
1710 this->area_y_max_ = staged.y_max;
1711 }
1712 if (std::isnan(this->area_edits_.z_min) && !std::isnan(staged.z_min)) {
1713 this->area_edits_.z_min = staged.z_min;
1714 this->area_z_min_ = staged.z_min;
1715 }
1716 if (std::isnan(this->area_edits_.z_max) && !std::isnan(staged.z_max)) {
1717 this->area_edits_.z_max = staged.z_max;
1718 this->area_z_max_ = staged.z_max;
1719 }
1720 this->publish_area_numbers_();
1721}
1722
1724 if (!this->area_presence_any_) {
1725 return;
1726 }
1727 // Nothing else corrects this: 0x0A0A carries no period the protocol states and no
1728 // command stops it, so the module going unattended is the only moment the
1729 // component can know a stored "occupied" has stopped being true.
1730 this->area_presence_any_ = false;
1731#ifdef USE_BINARY_SENSOR
1732 for (uint8_t i = 0; i < AREA_COUNT; i++) {
1733 if (this->area_presence_[i] != nullptr) {
1734 this->area_presence_[i]->publish_state(false);
1735 }
1736 }
1737 const bool presence = this->target_presence_any_ || this->area_presence_any_;
1738 if (this->presence_binary_sensor_ != nullptr) {
1739 this->presence_binary_sensor_->publish_state(presence);
1740 }
1741#endif
1742}
1743
1745 // Nothing corrects any of this until the stream comes back. The slot table goes
1746 // with it: slots key on cluster ids, which only track a person while reports are
1747 // arriving, and the room can empty and refill across the gap -- so the next
1748 // report starts from an empty table and fills slots in wire order, rather than
1749 // handing one back to whoever last held that id.
1750 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
1751#ifdef USE_SENSOR
1752 this->clear_target_slot_(i);
1753 this->last_target_presence_[i] = false;
1754#endif
1755 if (this->slot_occupied_[i]) {
1756 this->slot_occupied_[i] = false;
1757#ifdef USE_BINARY_SENSOR
1758 if (this->target_presence_[i] != nullptr) {
1759 this->target_presence_[i]->publish_state(false);
1760 }
1761#endif
1762 }
1763 }
1764#ifdef USE_SENSOR
1765 if (this->last_target_count_ != 0xFFFFFFFF) {
1766 if (this->target_count_sensor_ != nullptr) {
1768 }
1769 this->last_target_count_ = 0xFFFFFFFF;
1770 }
1771#endif
1772 if (this->target_presence_any_) {
1773 this->target_presence_any_ = false;
1774#ifdef USE_BINARY_SENSOR
1775 bool presence = this->target_presence_any_ || this->area_presence_any_;
1776 if (this->presence_binary_sensor_ != nullptr) {
1777 this->presence_binary_sensor_->publish_state(presence);
1778 }
1779#endif
1781 }
1782}
1783
1785 switch (type) {
1787 this->low_power_enabled_ = state;
1788 this->low_power_reported_ = true;
1789 this->send_control_command_(state ? CMD_LOW_POWER_ON : CMD_LOW_POWER_OFF);
1791 break;
1794 this->send_control_command_(state ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
1795#ifdef USE_SENSOR
1796 // The count only moves while the stream runs, so the last one would stand as
1797 // a live reading. The dedup sentinel is cleared with it: the same count is
1798 // new again when the stream comes back.
1799 if (!state && this->point_count_sensor_ != nullptr && this->last_point_count_ != 0xFFFFFFFF) {
1801 this->last_point_count_ = 0xFFFFFFFF;
1802 }
1803#endif
1804 break;
1807 this->send_control_command_(state ? CMD_TARGET_DISPLAY_ON : CMD_TARGET_DISPLAY_OFF);
1808 if (!state) {
1809 // Every target entity is fed by the reports this just stopped.
1810 this->clear_target_state_();
1811 }
1812 break;
1813 }
1814}
1815
1817 switch (type) {
1819 this->apply_area_config_();
1820 break;
1822 this->send_control_command_(CMD_AUTO_INTERFERENCE);
1823 // The module recomputes the interference areas without reporting them.
1824 this->send_control_command_(CMD_GET_AREAS);
1825 break;
1827 this->send_control_command_(CMD_GET_AREAS);
1828 break;
1830 this->send_control_command_(CMD_CLEAR_INTERFERENCE);
1831 // The module rewrites the areas but does not report them, so ask for the new geometry the
1832 // way the apply_area ack path does; the queue keeps it behind the command above.
1833 this->send_control_command_(CMD_GET_AREAS);
1834 break;
1836 this->send_control_command_(CMD_RESET_DETECTION_AREA);
1837 this->send_control_command_(CMD_GET_AREAS);
1838 break;
1840 this->send_control_command_(CMD_GET_DELAY);
1841 break;
1843 this->send_control_command_(CMD_GET_SENSITIVITY);
1844 break;
1846 this->send_control_command_(CMD_GET_TRIGGER);
1847 break;
1849 this->send_control_command_(CMD_GET_Z_RANGE);
1850 break;
1852 this->send_control_command_(CMD_GET_INSTALLATION);
1853 break;
1855 this->send_control_command_(CMD_GET_LOW_POWER);
1856 break;
1858 this->send_control_command_(CMD_GET_LOW_POWER_SLEEP);
1859 break;
1861 this->send_control_command_(CMD_RESET_UNATTENDED);
1862 break;
1863 case ButtonType::WAKE:
1864 this->wake_();
1865 break;
1866 }
1867}
1868
1869} // namespace esphome::ld6002b
uint8_t raw[35]
Definition bl0939.h:0
void mark_failed()
Mark this component as failed.
bool cancel_timeout(const char *name)
Cancel a timeout function.
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
const StringRef & get_name() const
Definition entity_base.h:71
ESPPreferenceObject make_entity_preference(uint32_t version=0)
Create a preference object for storing this entity's state/settings.
bool has_state() const
virtual void setup()=0
virtual void digital_write(bool value)=0
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2222
T * allocate(size_t n)
Definition helpers.h:2249
constexpr const char * c_str() const
Definition string_ref.h:78
void publish_state(bool new_state)
Publish a new state to the front-end.
text_sensor::TextSensor * work_mode_text_sensor_
Definition ld6002b.h:353
select::Select * area_id_select_
Definition ld6002b.h:375
bool queue_area_config_(uint8_t area_id, const AreaConfig &desired)
Definition ld6002b.cpp:1502
binary_sensor::BinarySensor * presence_binary_sensor_
Definition ld6002b.h:348
number::Number * area_z_min_number_
Definition ld6002b.h:368
void handle_target_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:687
text_sensor::TextSensor * ota_version_text_sensor_
Definition ld6002b.h:354
std::array< uint8_t, CMD_MAX_DATA_LEN > wake_scratch_
Definition ld6002b.h:433
static constexpr uint8_t CMD_QUEUE_SIZE
Definition ld6002b.h:409
number::Number * area_z_max_number_
Definition ld6002b.h:369
static void write_int32_le(uint8_t *data, int32_t value)
Definition ld6002b.cpp:234
void set_select_value(SelectType type, size_t index)
Definition ld6002b.cpp:1422
void save_area_id_pref_(uint8_t value)
Definition ld6002b.cpp:1613
static void write_u32_le(uint8_t *data, uint32_t value)
Definition ld6002b.cpp:227
std::array< PendingCommand, CMD_QUEUE_SIZE > cmd_queue_
Definition ld6002b.h:425
static float read_f32_le(const uint8_t *data)
Definition ld6002b.cpp:220
ESPPreferenceObject version_pref_
Definition ld6002b.h:355
void handle_installation_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:953
void set_switch_state(SwitchType type, bool state)
Definition ld6002b.cpp:1784
std::array< bool, MAX_TARGETS > last_cluster_id_valid_
Definition ld6002b.h:497
std::array< int32_t, MAX_TARGETS > last_cluster_id_
Definition ld6002b.h:496
void set_number_value(NumberType type, float value)
Definition ld6002b.cpp:1371
void handle_trigger_speed_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:927
select::Select * installation_select_
Definition ld6002b.h:374
std::array< AreaSensors, AREA_COUNT > detection_areas_
Definition ld6002b.h:345
select::Select * trigger_speed_select_
Definition ld6002b.h:373
static uint32_t read_u32_le(const uint8_t *data)
Definition ld6002b.cpp:208
void save_version_pref_(const char *value)
Definition ld6002b.cpp:1638
number::Number * area_x_max_number_
Definition ld6002b.h:365
sensor::Sensor * point_count_sensor_
Definition ld6002b.h:343
static constexpr uint8_t CMD_MAX_RETRIES
Definition ld6002b.h:415
void update_area_numbers_for_id_(uint8_t area_id)
Definition ld6002b.cpp:1491
void update_area_numbers_(const AreaConfig &area)
Definition ld6002b.cpp:1458
std::array< AreaConfig, AREA_COUNT > interference_area_values_
Definition ld6002b.h:467
void handle_version_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:1069
static constexpr uint32_t WAKE_BUTTON_TIMEOUT_ID
Definition ld6002b.h:418
number::Number * area_y_max_number_
Definition ld6002b.h:367
switch_::Switch * target_display_switch_
Definition ld6002b.h:382
void send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track)
Definition ld6002b.cpp:1199
static uint16_t read_u16_be(const uint8_t *data)
Definition ld6002b.cpp:206
void handle_area_presence_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:831
void publish_number_clamped_(number::Number *number, float value)
Definition ld6002b.cpp:1043
select::Select * sensitivity_select_
Definition ld6002b.h:372
number::Number * hold_delay_number_
Definition ld6002b.h:359
static constexpr uint32_t MODULE_AWAKE_MS
Definition ld6002b.h:414
static constexpr uint32_t CMD_FIRST_ACK_TIMEOUT_MS
Definition ld6002b.h:412
static int32_t read_int32_le(const uint8_t *data)
Definition ld6002b.cpp:213
void handle_area_report_(bool interference, const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:857
std::array< bool, MAX_TARGETS > slot_occupied_
Definition ld6002b.h:474
void handle_low_power_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:966
std::array< AreaConfig, AREA_COUNT > detection_area_values_
Definition ld6002b.h:468
static constexpr uint32_t CMD_ACK_TIMEOUT_MS
Definition ld6002b.h:410
static constexpr uint32_t AREA_REFRESH_TIMEOUT_ID
Definition ld6002b.h:421
std::array< int32_t, MAX_TARGETS > slot_cluster_
Definition ld6002b.h:473
std::array< binary_sensor::BinarySensor *, AREA_COUNT > area_presence_
Definition ld6002b.h:350
void handle_delay_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:905
std::array< AreaSensors, AREA_COUNT > interference_areas_
Definition ld6002b.h:344
void handle_sensitivity_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:914
std::array< TargetSensors, MAX_TARGETS > targets_
Definition ld6002b.h:341
void handle_frame_(uint16_t type, const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:597
void send_command_(uint16_t type, const uint8_t *data, uint8_t len)
Definition ld6002b.cpp:1195
switch_::Switch * low_power_switch_
Definition ld6002b.h:380
sensor::Sensor * target_count_sensor_
Definition ld6002b.h:342
switch_::Switch * point_cloud_switch_
Definition ld6002b.h:381
std::array< bool, MAX_TARGETS > last_target_presence_
Definition ld6002b.h:494
number::Number * area_y_min_number_
Definition ld6002b.h:366
void publish_work_mode_(bool low_power)
Definition ld6002b.cpp:1028
void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track)
Definition ld6002b.cpp:1238
std::array< binary_sensor::BinarySensor *, MAX_TARGETS > target_presence_
Definition ld6002b.h:349
void handle_low_power_sleep_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:980
void handle_work_mode_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:989
void press_button(ButtonType type)
Definition ld6002b.cpp:1816
number::Number * area_x_min_number_
Definition ld6002b.h:364
void try_apply_pending_area_(bool reported_interference)
Definition ld6002b.cpp:1543
bool queue_command_(uint16_t type, const uint8_t *data, uint8_t len)
Definition ld6002b.cpp:1090
void clear_target_slot_(uint8_t index)
Definition ld6002b.cpp:1651
number::Number * low_power_sleep_number_
Definition ld6002b.h:362
static constexpr uint32_t STALE_ACK_MAX_AGE_MS
Definition ld6002b.h:423
ESPPreferenceObject area_id_pref_
Definition ld6002b.h:376
bool send_control_command_(uint32_t command)
Definition ld6002b.cpp:1279
void handle_point_cloud_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:807
void handle_z_range_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:940
static void write_f32_le(uint8_t *data, float value)
Definition ld6002b.cpp:238
Base-class for all numbers.
Definition number.h:29
void publish_state(float state)
Definition number.cpp:22
NumberTraits traits
Definition number.h:41
void publish_state(const std::string &state)
Definition select.h:36
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
void publish_state(bool state)
Publish a state to the front-end from the back-end.
Definition switch.cpp:56
optional< bool > get_initial_state_with_restore_mode()
Returns the initial state of the switch, after applying restore mode rules.
Definition switch.cpp:42
void control(bool target_state)
Control this switch using a boolean state value.
Definition switch.cpp:12
void publish_state(const std::string &state)
Definition text_sensor.h:41
void write_byte(uint8_t data)
Definition uart.h:19
uint8_t type
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
bool z
Definition msa3xx.h:1
ESPHOME_LOG_TAG(TAG, "ld6002b")
const void size_t len
Definition hal.h:68
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
sensor::Sensor * z_max
Definition ld6002b.h:101
sensor::Sensor * z_min
Definition ld6002b.h:100
sensor::Sensor * x_min
Definition ld6002b.h:96
sensor::Sensor * y_max
Definition ld6002b.h:99
sensor::Sensor * y_min
Definition ld6002b.h:98
sensor::Sensor * x_max
Definition ld6002b.h:97
std::array< uint8_t, CMD_MAX_DATA_LEN > data
Definition ld6002b.h:278
sensor::Sensor * cluster_id
Definition ld6002b.h:92
sensor::Sensor * dop_idx
Definition ld6002b.h:91
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6