ESPHome 2026.9.1
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
11static const char *const 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 turn_on()/turn_off() are what turn 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 if (state) {
309 } else {
311 }
312 }
313#endif
314 if (!target_display_controlled) {
315 // No switch: the stream follows its consumers. With none, nothing is sent
316 // and the module's own default stands -- but the reports are gated out
317 // regardless, because there is nothing configured for them to feed.
318 this->target_display_enabled_ = want_target_stream;
319 if (want_target_stream) {
320 this->send_control_command_(CMD_TARGET_DISPLAY_ON);
321 }
322 }
323
324 bool point_cloud_controlled = false;
325#ifdef USE_SWITCH
326 if (this->point_cloud_switch_ != nullptr) {
327 point_cloud_controlled = true;
328 // The switch owns the stream, so it is also what applies the restored state:
329 // driving it rather than the module keeps the entity's inversion in the path.
330 const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false);
331 if (state) {
333 } else {
335 }
336 }
337#endif
338 if (!point_cloud_controlled) {
339 // No switch: the stream follows the sensor that reads it, which is also what
340 // the frame buffer above was sized for.
341 bool want_point_cloud = false;
342#ifdef USE_SENSOR
343 want_point_cloud = this->point_count_sensor_ != nullptr;
344#endif
345 this->send_control_command_(want_point_cloud ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
346 this->point_cloud_enabled_ = want_point_cloud;
347 }
348
349#ifdef USE_SELECT
350 if (this->sensitivity_select_ != nullptr) {
351 this->send_control_command_(CMD_GET_SENSITIVITY);
352 }
353 if (this->trigger_speed_select_ != nullptr) {
354 this->send_control_command_(CMD_GET_TRIGGER);
355 }
356 if (this->installation_select_ != nullptr) {
357 this->send_control_command_(CMD_GET_INSTALLATION);
358 }
359#endif
360#ifdef USE_NUMBER
361 if (this->z_min_number_ != nullptr || this->z_max_number_ != nullptr) {
362 this->send_control_command_(CMD_GET_Z_RANGE);
363 }
364 if (this->low_power_sleep_number_ != nullptr) {
365 this->send_control_command_(CMD_GET_LOW_POWER_SLEEP);
366 }
367 if (this->hold_delay_number_ != nullptr) {
368 this->send_control_command_(CMD_GET_DELAY);
369 }
370#endif
371#ifdef USE_SWITCH
372 bool want_low_power = this->low_power_switch_ != nullptr;
373 if (want_low_power) {
374 // The module reports this one back, so the query below confirms what it took.
375 // Driving the switch applies its inversion; it also marks the restored value
376 // as reported, so the work mode fallback runs on that until the query lands.
377 const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false);
378 if (state) {
379 this->low_power_switch_->turn_on();
380 } else {
382 }
383 }
384#else
385 bool want_low_power = false;
386#endif
387#ifdef USE_TEXT_SENSOR
388 want_low_power = want_low_power || this->work_mode_text_sensor_ != nullptr;
389#endif
390 if (want_low_power) {
391 this->send_control_command_(CMD_GET_LOW_POWER);
392 }
393
394 bool want_area_report = false;
395#ifdef USE_SENSOR
396 for (const auto &area : this->interference_areas_) {
397 if (area.x_min != nullptr || area.x_max != nullptr || area.y_min != nullptr || area.y_max != nullptr ||
398 area.z_min != nullptr || area.z_max != nullptr) {
399 want_area_report = true;
400 break;
401 }
402 }
403 if (!want_area_report) {
404 for (const auto &area : this->detection_areas_) {
405 if (area.x_min != nullptr || area.x_max != nullptr || area.y_min != nullptr || area.y_max != nullptr ||
406 area.z_min != nullptr || area.z_max != nullptr) {
407 want_area_report = true;
408 break;
409 }
410 }
411 }
412#endif
413#ifdef USE_NUMBER
414 if (this->area_x_min_number_ != nullptr || this->area_x_max_number_ != nullptr ||
415 this->area_y_min_number_ != nullptr || this->area_y_max_number_ != nullptr ||
416 this->area_z_min_number_ != nullptr || this->area_z_max_number_ != nullptr) {
417 want_area_report = true;
418 }
419#endif
420 if (want_area_report) {
421 this->send_control_command_(CMD_GET_AREAS);
422 }
423
424 this->init_area_id_pref_();
425 this->init_version_pref_();
426
427#ifdef USE_TEXT_SENSOR
428 if (this->ota_version_text_sensor_ != nullptr) {
429 this->queue_command_(TYPE_QUERY_VERSION, VERSION_QUERY_DATA, sizeof(VERSION_QUERY_DATA));
430 }
431#endif
432 });
433}
434
436 ESP_LOGCONFIG(TAG,
437 "HLK-LD6002B:\n"
438 " Auto wake: %s\n"
439 " Max data length: %u",
440 this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
441 static_cast<unsigned>(this->max_data_len_));
442 if (this->wakeup_pin_ != nullptr) {
443 LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
444 ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_);
445 }
446#ifdef USE_SENSOR
447 LOG_SENSOR(" ", "Target Count", this->target_count_sensor_);
448 LOG_SENSOR(" ", "Point Count", this->point_count_sensor_);
449 for (auto &target : this->targets_) {
450 LOG_SENSOR(" ", "Target X", target.x);
451 LOG_SENSOR(" ", "Target Y", target.y);
452 LOG_SENSOR(" ", "Target Z", target.z);
453 LOG_SENSOR(" ", "Target Doppler Index", target.dop_idx);
454 LOG_SENSOR(" ", "Target Cluster ID", target.cluster_id);
455 }
456 for (auto &area : this->interference_areas_) {
457 LOG_SENSOR(" ", "Interference Area X Min", area.x_min);
458 LOG_SENSOR(" ", "Interference Area X Max", area.x_max);
459 LOG_SENSOR(" ", "Interference Area Y Min", area.y_min);
460 LOG_SENSOR(" ", "Interference Area Y Max", area.y_max);
461 LOG_SENSOR(" ", "Interference Area Z Min", area.z_min);
462 LOG_SENSOR(" ", "Interference Area Z Max", area.z_max);
463 }
464 for (auto &area : this->detection_areas_) {
465 LOG_SENSOR(" ", "Detection Area X Min", area.x_min);
466 LOG_SENSOR(" ", "Detection Area X Max", area.x_max);
467 LOG_SENSOR(" ", "Detection Area Y Min", area.y_min);
468 LOG_SENSOR(" ", "Detection Area Y Max", area.y_max);
469 LOG_SENSOR(" ", "Detection Area Z Min", area.z_min);
470 LOG_SENSOR(" ", "Detection Area Z Max", area.z_max);
471 }
472#endif
473#ifdef USE_BINARY_SENSOR
474 LOG_BINARY_SENSOR(" ", "Presence", this->presence_binary_sensor_);
475 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
476 LOG_BINARY_SENSOR(" ", "Target Presence", this->target_presence_[i]);
477 }
478 for (uint8_t i = 0; i < AREA_COUNT; i++) {
479 LOG_BINARY_SENSOR(" ", "Detection Area Presence", this->area_presence_[i]);
480 }
481#endif
482#ifdef USE_TEXT_SENSOR
483 LOG_TEXT_SENSOR(" ", "Work Mode", this->work_mode_text_sensor_);
484 LOG_TEXT_SENSOR(" ", "OTA Version", this->ota_version_text_sensor_);
485#endif
486#ifdef USE_NUMBER
487 LOG_NUMBER(" ", "Hold Delay", this->hold_delay_number_);
488 LOG_NUMBER(" ", "Z Min", this->z_min_number_);
489 LOG_NUMBER(" ", "Z Max", this->z_max_number_);
490 LOG_NUMBER(" ", "Low Power Sleep", this->low_power_sleep_number_);
491 LOG_NUMBER(" ", "Area X Min", this->area_x_min_number_);
492 LOG_NUMBER(" ", "Area X Max", this->area_x_max_number_);
493 LOG_NUMBER(" ", "Area Y Min", this->area_y_min_number_);
494 LOG_NUMBER(" ", "Area Y Max", this->area_y_max_number_);
495 LOG_NUMBER(" ", "Area Z Min", this->area_z_min_number_);
496 LOG_NUMBER(" ", "Area Z Max", this->area_z_max_number_);
497#endif
498#ifdef USE_SWITCH
499 LOG_SWITCH(" ", "Low Power", this->low_power_switch_);
500 LOG_SWITCH(" ", "Point Cloud", this->point_cloud_switch_);
501 LOG_SWITCH(" ", "Target Display", this->target_display_switch_);
502#endif
503#ifdef USE_SELECT
504 LOG_SELECT(" ", "Sensitivity", this->sensitivity_select_);
505 LOG_SELECT(" ", "Trigger Speed", this->trigger_speed_select_);
506 LOG_SELECT(" ", "Installation Mode", this->installation_select_);
507 LOG_SELECT(" ", "Area ID", this->area_id_select_);
508#endif
509}
510
512 while (this->available()) {
513 uint8_t byte = this->read();
514 this->parse_byte_(byte);
515 }
517}
518
521 this->header_pos_ = 0;
522 this->header_xor_ = 0;
523 this->data_len_ = 0;
524 this->data_pos_ = 0;
525 this->data_xor_ = 0;
526 this->discard_remaining_ = 0;
527 this->frame_oversize_ = false;
528}
529
531 switch (this->parse_state_) {
533 // discard_remaining_ is unsigned: an unguarded decrement at zero would swallow 4 GB of stream.
534 if (this->discard_remaining_ > 0) {
535 this->discard_remaining_--;
536 }
537 if (this->discard_remaining_ == 0) {
538 this->reset_parser_();
539 }
540 return;
541 case ParseState::SOF:
542 if (byte != TF_SOF)
543 return;
544 this->header_pos_ = 0;
545 this->header_xor_ = 0;
546 this->header_xor_ ^= byte;
548 return;
550 if (this->header_pos_ < 6) {
551 this->data_buf_[this->header_pos_] = byte;
552 this->header_xor_ ^= byte;
553 this->header_pos_++;
554 if (this->header_pos_ == 6) {
555 this->frame_id_ = read_u16_be(this->data_buf_);
556 this->data_len_ = read_u16_be(this->data_buf_ + 2);
557 this->frame_type_ = read_u16_be(this->data_buf_ + 4);
558 // The length is only trustworthy once the header checksum has been verified, so just
559 // remember that the frame is oversized and let the HCK state act on it.
560 this->frame_oversize_ = this->data_len_ > this->max_data_len_;
562 }
563 }
564 return;
565 case ParseState::HCK: {
566 uint8_t expected = static_cast<uint8_t>(~this->header_xor_);
567 if (byte != expected) {
568 ESP_LOGV(TAG, "Header checksum mismatch");
569 this->reset_parser_();
570 return;
571 }
572 if (this->frame_oversize_) {
573 ESP_LOGW(TAG, "Frame too large: %u", this->data_len_);
574 // The header is verified, so the length can be trusted: skip the payload and its checksum.
575 this->discard_remaining_ = static_cast<uint32_t>(this->data_len_) + 1;
577 return;
578 }
579 if (this->data_len_ == 0) {
580 this->handle_frame_(this->frame_type_, nullptr, 0);
581 this->reset_parser_();
582 } else {
583 this->data_pos_ = 0;
584 this->data_xor_ = 0;
586 }
587 return;
588 }
589 case ParseState::DATA:
590 this->data_buf_[this->data_pos_++] = byte;
591 this->data_xor_ ^= byte;
592 if (this->data_pos_ >= this->data_len_) {
594 }
595 return;
596 case ParseState::DCK: {
597 uint8_t expected = static_cast<uint8_t>(~this->data_xor_);
598 if (byte == expected) {
599 this->handle_frame_(this->frame_type_, this->data_buf_, this->data_len_);
600 } else {
601 ESP_LOGV(TAG, "Data checksum mismatch");
602 }
603 this->reset_parser_();
604 return;
605 }
606 }
607}
608
609void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_t len) {
610 this->last_traffic_ms_ = millis();
611 if (this->stale_ack_count_ > 0 && millis() - this->stale_ack_ms_ > STALE_ACK_MAX_AGE_MS) {
612 this->stale_ack_count_ = 0;
613 }
614 // ACKs carry no id and arrive in send order: debt from earlier attempts is paid before the active command.
615 if (len == 0 && this->stale_ack_count_ > 0 && this->stale_ack_type_ == type) {
616 this->stale_ack_count_--;
617 ESP_LOGV(TAG, "Ignoring ACK for command 0x%04X from an earlier attempt (module frame 0x%04X)", type,
618 this->frame_id_);
619 return;
620 }
621 if (len == 0 && this->command_active_ && this->command_sent_ && type == this->active_command_.type) {
622 ESP_LOGV(TAG, "ACK for command 0x%04X (module frame 0x%04X)", type, this->frame_id_);
623 const bool refresh_areas = (type == TYPE_SET_AREA) && this->area_write_in_flight_;
624 // This settles one expected reply; the rest stay owed and become the debt for the next command.
625 this->send_generation_++;
626 this->stale_ack_type_ = type;
627 this->stale_ack_count_ = this->acks_expected_ > 0 ? static_cast<uint8_t>(this->acks_expected_ - 1) : 0;
628 this->stale_ack_ms_ = millis();
629 this->command_active_ = false;
630 this->command_sent_ = false;
631 this->last_send_ms_ = 0;
633 if (refresh_areas) {
634 this->area_write_in_flight_ = false;
635 this->set_timeout(AREA_REFRESH_TIMEOUT, 50, [this]() { this->send_control_command_(CMD_GET_AREAS); });
636 }
637 return;
638 }
639
640#ifdef ESPHOME_LOG_HAS_VERBOSE
641 const uint32_t active_control_command =
642 (this->command_active_ && this->active_command_.type == TYPE_CONTROL && this->active_command_.len >= 4)
643 ? read_u32_le(this->active_command_.data.data())
644 : 0;
645 if (active_control_command != 0 && is_expected_control_report(active_control_command, type)) {
646 ESP_LOGV(TAG, "Received %s (0x%04X) while waiting for %s (0x%02" PRIX32 ") ACK", frame_type_name(type), type,
647 control_command_name(active_control_command), active_control_command);
648 }
649#endif
650
651 switch (type) {
652 case TYPE_REPORT_TARGET:
653 this->handle_target_report_(data, len);
654 break;
655 case TYPE_REPORT_POINT_CLOUD:
656 this->handle_point_cloud_(data, len);
657 break;
658 case TYPE_REPORT_AREA_PRESENCE:
659 this->handle_area_presence_(data, len);
660 break;
661 case TYPE_REPORT_INTERFERENCE_AREAS:
662 this->handle_area_report_(true, data, len);
663 break;
664 case TYPE_REPORT_DETECTION_AREAS:
665 this->handle_area_report_(false, data, len);
666 break;
667 case TYPE_REPORT_DELAY:
668 this->handle_delay_report_(data, len);
669 break;
670 case TYPE_REPORT_SENSITIVITY:
671 this->handle_sensitivity_report_(data, len);
672 break;
673 case TYPE_REPORT_TRIGGER:
675 break;
676 case TYPE_REPORT_Z_RANGE:
677 this->handle_z_range_report_(data, len);
678 break;
679 case TYPE_REPORT_INSTALLATION:
680 this->handle_installation_report_(data, len);
681 break;
682 case TYPE_REPORT_LOW_POWER:
683 this->handle_low_power_report_(data, len);
684 break;
685 case TYPE_REPORT_LOW_POWER_SLEEP:
687 break;
688 case TYPE_REPORT_WORK_MODE:
689 this->handle_work_mode_report_(data, len);
690 break;
691 case TYPE_QUERY_VERSION:
692 this->handle_version_report_(data, len);
693 break;
694 default:
695 break;
696 }
697}
698
699void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len) {
700 // The module stops streaming when it acts on the command, not when the command
701 // is queued, so trailing frames after an off must not repopulate what
702 // set_switch_state just cleared.
703 if (!this->target_display_enabled_) {
704 return;
705 }
706 if (len < 4)
707 return;
708
709 uint32_t target_num = read_u32_le(data);
710 uint16_t available = (len - 4) / TARGET_DATA_LEN;
711 // Un-narrowed: a report of e.g. 256 targets must not truncate to 0 and read as "absent".
712 const uint32_t reported = std::min<uint32_t>(target_num, available);
713 uint8_t count = static_cast<uint8_t>(std::min<uint32_t>(reported, MAX_TARGETS));
714
715 // The module re-sorts its array by cluster id, so slots key on the id to track the person.
716 std::array<int32_t, MAX_TARGETS> wire_cluster{};
717 std::array<bool, MAX_TARGETS> wire_placed{};
718 std::array<bool, MAX_TARGETS> slot_seen{};
719 std::array<uint8_t, MAX_TARGETS> slot_wire{};
720 for (uint8_t i = 0; i < count; i++) {
721 uint16_t cluster_offset = 4 + (i * TARGET_DATA_LEN) + 16;
722 wire_cluster[i] = static_cast<int32_t>(read_u32_le(data + cluster_offset));
723 }
724 for (uint8_t i = 0; i < count; i++) {
725 for (uint8_t s = 0; s < MAX_TARGETS; s++) {
726 if (this->slot_occupied_[s] && !slot_seen[s] && this->slot_cluster_[s] == wire_cluster[i]) {
727 slot_seen[s] = true;
728 wire_placed[i] = true;
729 slot_wire[s] = i;
730 break;
731 }
732 }
733 }
734 for (uint8_t s = 0; s < MAX_TARGETS; s++) {
735 if (!slot_seen[s]) {
736 this->slot_occupied_[s] = false;
737 }
738 }
739 for (uint8_t i = 0; i < count; i++) {
740 if (wire_placed[i]) {
741 continue;
742 }
743 for (uint8_t s = 0; s < MAX_TARGETS; s++) {
744 if (!this->slot_occupied_[s]) {
745 this->slot_occupied_[s] = true;
746 this->slot_cluster_[s] = wire_cluster[i];
747 slot_wire[s] = i;
748 break;
749 }
750 }
751 }
752
753#ifdef USE_SENSOR
754 if (this->target_count_sensor_ != nullptr) {
755 if (reported != this->last_target_count_) {
756 this->target_count_sensor_->publish_state(reported);
757 this->last_target_count_ = reported;
758 }
759 }
760#endif
761
762 this->target_presence_any_ = (reported > 0);
763#ifdef USE_BINARY_SENSOR
764 bool presence = this->target_presence_any_ || this->area_presence_any_;
765 if (this->presence_binary_sensor_ != nullptr) {
766 this->presence_binary_sensor_->publish_state(presence);
767 }
768#endif
770
771 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
772 bool has_target = this->slot_occupied_[i];
773 if (has_target) {
774#ifdef USE_SENSOR
775 uint16_t offset = 4 + (slot_wire[i] * TARGET_DATA_LEN);
776 float x = read_f32_le(data + offset + 0);
777 float y = read_f32_le(data + offset + 4);
778 float z = read_f32_le(data + offset + 8);
779 int32_t dop_idx = read_int32_le(data + offset + 12);
780 int32_t cluster_id = this->slot_cluster_[i];
781 TargetSensors &target = this->targets_[i];
782 if (target.x != nullptr) {
783 target.x->publish_state(x);
784 }
785 if (target.y != nullptr) {
786 target.y->publish_state(y);
787 }
788 if (target.z != nullptr) {
789 target.z->publish_state(z);
790 }
791 if (target.dop_idx != nullptr) {
792 target.dop_idx->publish_state(static_cast<float>(dop_idx));
793 }
794 if (target.cluster_id != nullptr) {
795 if (!this->last_cluster_id_valid_[i] || cluster_id != this->last_cluster_id_[i]) {
796 target.cluster_id->publish_state(static_cast<float>(cluster_id));
797 this->last_cluster_id_[i] = cluster_id;
798 this->last_cluster_id_valid_[i] = true;
799 }
800 }
801#endif
802 } else {
803#ifdef USE_SENSOR
804 this->clear_target_slot_(i);
805#endif
806 }
807#ifdef USE_BINARY_SENSOR
808 if (this->target_presence_[i] != nullptr) {
809 // publish_state() already skips unchanged states, no manual de-dup needed.
810 this->target_presence_[i]->publish_state(has_target);
811 }
812#endif
813#ifdef USE_SENSOR
814 this->last_target_presence_[i] = has_target;
815#endif
816 }
817}
818
819void LD6002BComponent::handle_point_cloud_(const uint8_t *data, uint16_t len) {
820 // Same window as the target stream: a frame already in flight must not put the
821 // count back after the switch cleared it.
822 if (!this->point_cloud_enabled_) {
823 return;
824 }
825 if (len < 4)
826 return;
827
828#ifdef USE_SENSOR
829 uint32_t point_num = read_u32_le(data);
830 if (this->point_count_sensor_ != nullptr) {
831 if (point_num != this->last_point_count_) {
832 this->point_count_sensor_->publish_state(point_num);
833 this->last_point_count_ = point_num;
834 }
835 }
836#endif
837}
838
839// 0x0A0A carries one uint32 per detection area -- the protocol names the four
840// fields detection_state_area0..3 -- so this covers area ids 4..7 only. The
841// interference areas have no presence report: a target inside one is what they
842// exist to suppress.
843void LD6002BComponent::handle_area_presence_(const uint8_t *data, uint16_t len) {
844 const uint16_t needed = AREA_COUNT * AREA_PRESENCE_ENTRY_LEN;
845 if (len < needed)
846 return;
847
848 this->area_presence_any_ = false;
849 for (uint8_t i = 0; i < AREA_COUNT; i++) {
850 uint32_t state = read_u32_le(data + (i * AREA_PRESENCE_ENTRY_LEN));
851 bool present = state != 0;
852 this->area_presence_any_ = this->area_presence_any_ || present;
853#ifdef USE_BINARY_SENSOR
854 if (this->area_presence_[i] != nullptr) {
855 this->area_presence_[i]->publish_state(present);
856 }
857#endif
858 }
859
860#ifdef USE_BINARY_SENSOR
861 bool presence = this->target_presence_any_ || this->area_presence_any_;
862 if (this->presence_binary_sensor_ != nullptr) {
863 this->presence_binary_sensor_->publish_state(presence);
864 }
865#endif
867}
868
869void LD6002BComponent::handle_area_report_(bool interference, const uint8_t *data, uint16_t len) {
870 uint16_t needed = AREA_COUNT * AREA_DATA_LEN;
871 if (len < needed)
872 return;
873
874 for (uint8_t i = 0; i < AREA_COUNT; i++) {
875 uint16_t offset = i * AREA_DATA_LEN;
876 float x_min = read_f32_le(data + offset + 0);
877 float x_max = read_f32_le(data + offset + 4);
878 float y_min = read_f32_le(data + offset + 8);
879 float y_max = read_f32_le(data + offset + 12);
880 float z_min = read_f32_le(data + offset + 16);
881 float z_max = read_f32_le(data + offset + 20);
882
883#ifdef USE_SENSOR
884 AreaSensors &area = interference ? this->interference_areas_[i] : this->detection_areas_[i];
885 if (area.x_min != nullptr)
886 area.x_min->publish_state(x_min);
887 if (area.x_max != nullptr)
888 area.x_max->publish_state(x_max);
889 if (area.y_min != nullptr)
890 area.y_min->publish_state(y_min);
891 if (area.y_max != nullptr)
892 area.y_max->publish_state(y_max);
893 if (area.z_min != nullptr)
894 area.z_min->publish_state(z_min);
895 if (area.z_max != nullptr)
896 area.z_max->publish_state(z_max);
897#endif
898
899 AreaConfig &store = interference ? this->interference_area_values_[i] : this->detection_area_values_[i];
900 store.x_min = x_min;
901 store.x_max = x_max;
902 store.y_min = y_min;
903 store.y_max = y_max;
904 store.z_min = z_min;
905 store.z_max = z_max;
906
907 uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
908 bool selected_interference = selected_id < AREA_COUNT;
909 uint8_t selected_index = selected_interference ? selected_id : static_cast<uint8_t>(selected_id - AREA_COUNT);
910 if (selected_interference == interference && selected_index == i) {
911 this->update_area_numbers_(store);
912 }
913 }
914 this->try_apply_pending_area_(interference);
915}
916
917void LD6002BComponent::handle_delay_report_(const uint8_t *data, uint16_t len) {
918 if (len < 4)
919 return;
920#ifdef USE_NUMBER
921 uint32_t delay = read_u32_le(data);
923#endif
924}
925
926void LD6002BComponent::handle_sensitivity_report_(const uint8_t *data, uint16_t len) {
927 if (len < 1)
928 return;
929#ifdef USE_SELECT
930 if (this->sensitivity_select_ == nullptr)
931 return;
932 uint8_t value = data[0];
933 if (value <= 2) {
935 }
936#endif
937}
938
939void LD6002BComponent::handle_trigger_speed_report_(const uint8_t *data, uint16_t len) {
940 if (len < 1)
941 return;
942#ifdef USE_SELECT
943 if (this->trigger_speed_select_ == nullptr)
944 return;
945 uint8_t value = data[0];
946 if (value <= 2) {
948 }
949#endif
950}
951
952void LD6002BComponent::handle_z_range_report_(const uint8_t *data, uint16_t len) {
953 if (len < 8)
954 return;
955 float z_min = read_f32_le(data);
956 float z_max = read_f32_le(data + 4);
957 this->z_min_ = z_min;
958 this->z_max_ = z_max;
959#ifdef USE_NUMBER
960 this->publish_number_clamped_(this->z_min_number_, z_min);
961 this->publish_number_clamped_(this->z_max_number_, z_max);
962#endif
963}
964
965void LD6002BComponent::handle_installation_report_(const uint8_t *data, uint16_t len) {
966 if (len < 1)
967 return;
968#ifdef USE_SELECT
969 if (this->installation_select_ == nullptr)
970 return;
971 uint8_t value = data[0];
972 if (value <= 1) {
974 }
975#endif
976}
977
978void LD6002BComponent::handle_low_power_report_(const uint8_t *data, uint16_t len) {
979 if (len < 1)
980 return;
981 bool enabled = data[0] != 0;
982 this->low_power_enabled_ = enabled;
983 this->low_power_reported_ = true;
984#ifdef USE_SWITCH
985 if (this->low_power_switch_ != nullptr) {
986 this->low_power_switch_->publish_state(enabled);
987 }
988#endif
990}
991
992void LD6002BComponent::handle_low_power_sleep_report_(const uint8_t *data, uint16_t len) {
993 if (len < 4)
994 return;
995#ifdef USE_NUMBER
996 uint32_t sleep_ms = read_u32_le(data);
998#endif
999}
1000
1001void LD6002BComponent::handle_work_mode_report_(const uint8_t *data, uint16_t len) {
1002 if (len < 1)
1003 return;
1004 // Zero is the unattended half of this transition. Read outside the text sensor's
1005 // ifdef because the area sensors do not need one configured to have gone stale.
1006 const bool low_power = (data[0] == 0);
1007#ifdef USE_TEXT_SENSOR
1008 if (this->work_mode_text_sensor_ != nullptr) {
1009 this->work_mode_reported_ = true;
1010 this->publish_work_mode_(low_power);
1011 }
1012#endif
1013 // Protocol V1.2 section 2.1.17: this message is sent only on the transition
1014 // between the unattended low-power mode and normal operation, so a zero is the
1015 // module stating that nobody is in any area. Not while a target is still being
1016 // tracked, though: the reset_unattended command is undocumented on whether it
1017 // forces this report, and where two statements from the module disagree the live
1018 // one wins.
1019 if (low_power && !this->target_presence_any_) {
1020 this->clear_area_presence_();
1021 }
1022}
1023
1025#ifdef USE_TEXT_SENSOR
1026 if (this->work_mode_text_sensor_ == nullptr || this->work_mode_reported_) {
1027 return;
1028 }
1029 if (!this->low_power_reported_) {
1030 return;
1031 }
1032 // Target presence is only meaningful while the stream that maintains it runs.
1033 // Area presence keeps its own report, so it still counts with the target stream
1034 // off and low power alone decides only when neither half has anything to say.
1035 const bool presence = (this->target_display_enabled_ && this->target_presence_any_) || this->area_presence_any_;
1036 this->publish_work_mode_(this->low_power_enabled_ && !presence);
1037#endif
1038}
1039
1041#ifdef USE_TEXT_SENSOR
1042 if (this->work_mode_text_sensor_ == nullptr) {
1043 return;
1044 }
1045 if (this->last_work_mode_valid_ && this->last_work_mode_low_power_ == low_power) {
1046 return;
1047 }
1048 this->work_mode_text_sensor_->publish_state(low_power ? "low_power" : "normal");
1049 this->last_work_mode_valid_ = true;
1050 this->last_work_mode_low_power_ = low_power;
1051#endif
1052}
1053
1054#ifdef USE_NUMBER
1056 if (number == nullptr)
1057 return;
1058 if (std::isnan(value)) {
1059 // NAN is this component's "the module has not told us yet". Publishing it on an
1060 // entity that has never had a state would report a nan where unknown is the
1061 // truth; on one that already shows a value it is the only way to say that value
1062 // no longer describes the selected area.
1063 if (number->has_state()) {
1064 number->publish_state(value);
1065 }
1066 return;
1067 }
1068 const float min_value = number->traits.get_min_value();
1069 const float max_value = number->traits.get_max_value();
1070 // Outside the declared range the user cannot write the value back, so publish
1071 // what they can reach and say what the module actually sent.
1072 if (value < min_value || value > max_value) {
1073 ESP_LOGW(TAG, "'%s': module reported %.1f, clamped to %.1f..%.1f", number->get_name().c_str(), value, min_value,
1074 max_value);
1075 value = std::clamp(value, min_value, max_value);
1076 }
1077 number->publish_state(value);
1078}
1079#endif
1080
1081void LD6002BComponent::handle_version_report_(const uint8_t *data, uint16_t len) {
1082 if (len < 4)
1083 return;
1084#ifdef USE_TEXT_SENSOR
1085 if (this->ota_version_text_sensor_ == nullptr)
1086 return;
1087 uint8_t project = data[0];
1088 uint8_t major = data[1];
1089 uint8_t minor = data[2];
1090 uint8_t patch = data[3];
1091 char buf[32];
1092 if (project == 0) {
1093 std::snprintf(buf, sizeof(buf), "%u.%u.%u", major, minor, patch);
1094 } else {
1095 std::snprintf(buf, sizeof(buf), "p%u %u.%u.%u", project, major, minor, patch);
1096 }
1098 this->save_version_pref_(buf);
1099#endif
1100}
1101
1102bool LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_t len) {
1103 if (len > CMD_MAX_DATA_LEN) {
1104 ESP_LOGW(TAG, "Command data too large: %u", len);
1105 return false;
1106 }
1107 if (this->cmd_count_ >= CMD_QUEUE_SIZE) {
1108 ESP_LOGW(TAG, "Command queue full, dropping command 0x%04X", type);
1109 return false;
1110 }
1111
1112 PendingCommand &cmd = this->cmd_queue_[this->cmd_tail_];
1113 cmd.type = type;
1114 cmd.len = len;
1115 if (len > 0 && data != nullptr) {
1116 std::memcpy(cmd.data.data(), data, len);
1117 }
1118
1119 this->cmd_tail_ = (this->cmd_tail_ + 1) % CMD_QUEUE_SIZE;
1120 this->cmd_count_++;
1121 this->process_command_queue_();
1122 return true;
1123}
1124
1126 uint32_t now = millis();
1127 if (this->command_active_) {
1128 // A sleeping module consumes the opening attempt as its wake-up instead of answering it.
1129 const uint32_t ack_timeout = this->attempts_sent_ <= 1 ? CMD_FIRST_ACK_TIMEOUT_MS : CMD_ACK_TIMEOUT_MS;
1130 if (this->command_sent_ && now - this->last_send_ms_ >= ack_timeout) {
1131 const uint32_t active_control_command =
1132 (this->active_command_.type == TYPE_CONTROL && this->active_command_.len >= 4)
1133 ? read_u32_le(this->active_command_.data.data())
1134 : 0;
1135 if (this->retries_left_ > 0) {
1136#ifdef ESPHOME_LOG_HAS_VERBOSE
1137 if (active_control_command != 0) {
1138 ESP_LOGV(TAG, "Retrying %s (0x%02" PRIX32 "), %u attempt(s) remaining",
1139 control_command_name(active_control_command), active_control_command, this->retries_left_);
1140 } else {
1141 // Writes without a control subcommand (hold delay, z-range) had no retry trace at all.
1142 ESP_LOGV(TAG, "Retrying command 0x%04X, %u attempt(s) remaining", this->active_command_.type,
1143 this->retries_left_);
1144 }
1145#endif
1146 this->command_sent_ = false;
1147 this->last_send_ms_ = 0;
1148 this->send_command_(this->active_command_.type, this->active_command_.data.data(), this->active_command_.len);
1149 this->retries_left_--;
1150 } else {
1151 if (active_control_command != 0) {
1152 ESP_LOGW(TAG, "Command 0x%04X subcommand 0x%02" PRIX32 " timed out", this->active_command_.type,
1153 active_control_command);
1154 } else {
1155 ESP_LOGW(TAG, "Command 0x%04X timed out", this->active_command_.type);
1156 }
1157 if (this->active_command_.type == TYPE_SET_AREA) {
1158 this->area_write_in_flight_ = false;
1159 }
1160 // The deferred apply is waiting on the report this command would have
1161 // brought back, and nothing else re-arms it. Dropping it here is the
1162 // difference between one apply lost to a timeout and one that rides in on
1163 // an unrelated area report later, writing bounds the user has moved on from.
1164 if (active_control_command == CMD_GET_AREAS && this->deferred_apply_pending_) {
1165 this->deferred_apply_pending_ = false;
1167 ESP_LOGW(TAG, "Area read timed out, dropping deferred area apply");
1168 }
1169 // A reply may still be in flight for the attempt we just gave up on, so carry one over as
1170 // debt rather than clearing the ledger, or that late ACK would retire the successor. Only
1171 // one: reaching this point means nothing was answered at all, so the older attempts are
1172 // speculative, and carrying them would swallow the successor's own replies.
1173 const uint16_t owed = (this->stale_ack_type_ == this->active_command_.type ? this->stale_ack_count_ : 0) +
1174 (this->acks_expected_ > 0 ? 1 : 0);
1175 this->stale_ack_type_ = this->active_command_.type;
1176 this->stale_ack_count_ = static_cast<uint8_t>(std::min<uint16_t>(owed, 255));
1177 this->stale_ack_ms_ = now;
1178 this->send_generation_++;
1179 this->command_active_ = false;
1180 this->command_sent_ = false;
1181 this->last_send_ms_ = 0;
1182 }
1183 }
1184 return;
1185 }
1186
1187 if (this->cmd_count_ == 0)
1188 return;
1189
1190 this->active_command_ = this->cmd_queue_[this->cmd_head_];
1191 this->cmd_head_ = (this->cmd_head_ + 1) % CMD_QUEUE_SIZE;
1192 this->cmd_count_--;
1193
1194 this->send_generation_++;
1196 this->command_active_ = true;
1197 this->command_sent_ = false;
1198 this->last_send_ms_ = 0;
1199 this->attempts_sent_ = 0;
1200 this->acks_expected_ = 0;
1201 if (this->stale_ack_type_ != this->active_command_.type) {
1202 this->stale_ack_count_ = 0;
1203 }
1204 this->send_command_(this->active_command_.type, this->active_command_.data.data(), this->active_command_.len);
1205}
1206
1207void LD6002BComponent::send_command_(uint16_t type, const uint8_t *data, uint8_t len) {
1208 this->send_command_internal_(type, data, len, true);
1209}
1210
1211void LD6002BComponent::send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track) {
1212 if (len > CMD_MAX_DATA_LEN) {
1213 ESP_LOGW(TAG, "Command data too large: %u", len);
1214 if (track) {
1215 // Release the slot: an unwritten command is never acked and never times out.
1216 this->command_active_ = false;
1217 this->command_sent_ = false;
1218 this->last_send_ms_ = 0;
1219 }
1220 return;
1221 }
1222
1223 // Anonymous timeouts never replace each other; with a pulse already pending the module is waking anyway.
1224 if (this->auto_wake_ && this->wakeup_pin_ != nullptr && !this->wake_pulse_pending_) {
1225 // Snapshot the payload: the deferred write must not depend on state a completing command changes.
1226 if (len > 0 && data != nullptr) {
1227 std::memcpy(this->wake_scratch_.data(), data, len);
1228 }
1229 // A button pulse must not raise the pin in the middle of this one.
1231 this->wake_pulse_pending_ = true;
1232 this->wakeup_pin_->digital_write(false);
1233 const uint8_t generation = this->send_generation_;
1234 this->set_timeout(this->wakeup_pulse_ms_, [this, type, len, track, generation]() {
1235 this->wakeup_pin_->digital_write(true);
1236 this->wake_pulse_pending_ = false;
1237 // Anonymous timeouts are never cancelled, so a tracked pulse whose command has since been
1238 // retired must not transmit: the frame would land after its successor and be booked to it.
1239 if (track && generation != this->send_generation_) {
1240 return;
1241 }
1242 this->write_frame_(type, (len > 0) ? this->wake_scratch_.data() : nullptr, len, track);
1243 });
1244 return;
1245 }
1246
1247 this->write_frame_(type, data, len, track);
1248}
1249
1250void LD6002BComponent::write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track) {
1251 uint16_t frame_id = this->next_frame_id_++ & 0x7FFF;
1252 frame_id |= 0x8000;
1253
1254 uint8_t header_xor = 0;
1255 auto write_header = [&](uint8_t b) {
1256 this->write_byte(b);
1257 header_xor ^= b;
1258 };
1259
1260 write_header(TF_SOF);
1261 write_header((frame_id >> 8) & 0xFF);
1262 write_header(frame_id & 0xFF);
1263 write_header((len >> 8) & 0xFF);
1264 write_header(len & 0xFF);
1265 write_header((type >> 8) & 0xFF);
1266 write_header(type & 0xFF);
1267
1268 this->write_byte(static_cast<uint8_t>(~header_xor));
1269
1270 if (len > 0 && data != nullptr) {
1271 uint8_t data_xor = 0;
1272 for (uint8_t i = 0; i < len; i++) {
1273 this->write_byte(data[i]);
1274 data_xor ^= data[i];
1275 }
1276 this->write_byte(static_cast<uint8_t>(~data_xor));
1277 }
1278 const uint32_t now = millis();
1279 if (track) {
1280 // A frame sent to a module that has had time to fall asleep is its wake-up, and goes unanswered.
1281 if (this->last_traffic_ms_ != 0 && now - this->last_traffic_ms_ < MODULE_AWAKE_MS) {
1282 this->acks_expected_++;
1283 }
1284 this->last_send_ms_ = now;
1285 this->command_sent_ = true;
1286 this->attempts_sent_++;
1287 }
1288 this->last_traffic_ms_ = now;
1289}
1290
1292 uint8_t data[4];
1293 write_u32_le(data, command);
1294 return this->queue_command_(TYPE_CONTROL, data, sizeof(data));
1295}
1296
1298 // One frame carries both bounds, so half a range cannot be written.
1299 if (std::isnan(this->z_min_) || std::isnan(this->z_max_)) {
1300 ESP_LOGW(TAG, "Z range not written, other bound unknown");
1301 return;
1302 }
1303 // Both bounds are known and crossed; the frame has no way to say that.
1304 if (this->z_min_ > this->z_max_) {
1305 ESP_LOGW(TAG, "Z range not written, min above max");
1306 return;
1307 }
1308 uint8_t data[8];
1309 write_f32_le(data, this->z_min_);
1310 write_f32_le(data + 4, this->z_max_);
1311 this->queue_command_(TYPE_SET_Z_RANGE, data, sizeof(data));
1312}
1313
1315 if (!this->area_id_set_) {
1316 ESP_LOGW(TAG, "Area ID not selected; ignoring apply");
1317 return;
1318 }
1319 if (this->area_id_ >= AREA_ID_COUNT) {
1320 ESP_LOGW(TAG, "Invalid area id: %u", this->area_id_);
1321 return;
1322 }
1323
1324 const bool interference = this->area_id_ < AREA_COUNT;
1325 const uint8_t index = interference ? this->area_id_ : static_cast<uint8_t>(this->area_id_ - AREA_COUNT);
1326 AreaConfig desired = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1327 if (!std::isnan(this->area_x_min_))
1328 desired.x_min = this->area_x_min_;
1329 if (!std::isnan(this->area_x_max_))
1330 desired.x_max = this->area_x_max_;
1331 if (!std::isnan(this->area_y_min_))
1332 desired.y_min = this->area_y_min_;
1333 if (!std::isnan(this->area_y_max_))
1334 desired.y_max = this->area_y_max_;
1335 if (!std::isnan(this->area_z_min_))
1336 desired.z_min = this->area_z_min_;
1337 if (!std::isnan(this->area_z_max_))
1338 desired.z_max = this->area_z_max_;
1339
1340 if (std::isnan(desired.x_min) || std::isnan(desired.x_max) || std::isnan(desired.y_min) ||
1341 std::isnan(desired.y_max) || std::isnan(desired.z_min) || std::isnan(desired.z_max)) {
1342 // Ask first: a read that never reached the queue would leave a deferral waiting
1343 // on a report nobody requested, with the user's values already retired for it.
1344 if (!this->send_control_command_(CMD_GET_AREAS)) {
1345 ESP_LOGW(TAG, "Area read not queued; area config left unapplied");
1346 return;
1347 }
1348 this->deferred_apply_pending_ = true;
1349 this->pending_area_id_ = this->area_id_;
1350 // The ledger, not the mirror: the mirror also carries whatever the module last
1351 // reported for the axes the user never touched, and staging those would hand them
1352 // back later wearing the user's badge -- a module value the next report is then
1353 // kept away from. Staging only what was actually typed is also what makes the
1354 // replay's overlay right: the untouched axes come from the fresh report. An
1355 // empty ledger is a meaning rather than a gap, then: an apply with nothing
1356 // staged rewrites the area exactly as the report just described it, which is
1357 // what a direct apply with nothing staged already does.
1358 this->pending_area_updates_ = this->area_edits_;
1359 // Staged above, so they are the deferred apply's values now rather than an
1360 // unsent edit. Anything typed from here belongs to whatever the user does
1361 // next, which may well be a different area.
1362 this->area_edits_ = AreaConfig{};
1363 ESP_LOGI(TAG, "Area config incomplete; requesting current areas before applying");
1364 return;
1365 }
1366 // Only a write the module will actually see retires them.
1367 if (this->queue_area_config_(this->area_id_, desired)) {
1368 this->area_edits_ = AreaConfig{};
1369 }
1370}
1371
1373 // A command's own pulse raises the pin and writes after it, so ride along instead of
1374 // claiming the flag: claiming it would send that command down the immediate-write path
1375 // with the pin still low.
1376 if (this->wakeup_pin_ == nullptr || this->wake_pulse_pending_)
1377 return;
1378 this->wakeup_pin_->digital_write(false);
1379 this->set_timeout(WAKE_BUTTON_TIMEOUT, this->wakeup_pulse_ms_, [this]() { this->wakeup_pin_->digital_write(true); });
1380}
1381
1383 switch (type) {
1385 uint32_t delay = static_cast<uint32_t>(value);
1386 uint8_t data[4];
1387 write_u32_le(data, delay);
1388 this->queue_command_(TYPE_SET_HOLD_DELAY, data, sizeof(data));
1389 break;
1390 }
1391 case NumberType::Z_MIN:
1392 this->z_min_ = value;
1393 this->send_z_range_();
1394 break;
1395 case NumberType::Z_MAX:
1396 this->z_max_ = value;
1397 this->send_z_range_();
1398 break;
1400 uint32_t sleep_ms = static_cast<uint32_t>(value);
1401 uint8_t data[4];
1402 write_u32_le(data, sleep_ms);
1403 this->queue_command_(TYPE_SET_LOW_POWER_SLEEP, data, sizeof(data));
1404 break;
1405 }
1407 this->area_x_min_ = value;
1408 this->area_edits_.x_min = value;
1409 break;
1411 this->area_x_max_ = value;
1412 this->area_edits_.x_max = value;
1413 break;
1415 this->area_y_min_ = value;
1416 this->area_edits_.y_min = value;
1417 break;
1419 this->area_y_max_ = value;
1420 this->area_edits_.y_max = value;
1421 break;
1423 this->area_z_min_ = value;
1424 this->area_edits_.z_min = value;
1425 break;
1427 this->area_z_max_ = value;
1428 this->area_edits_.z_max = value;
1429 break;
1430 }
1431}
1432
1434 switch (type) {
1436 if (index == 0) {
1437 this->send_control_command_(CMD_SENSITIVITY_LOW);
1438 } else if (index == 1) {
1439 this->send_control_command_(CMD_SENSITIVITY_MEDIUM);
1440 } else if (index == 2) {
1441 this->send_control_command_(CMD_SENSITIVITY_HIGH);
1442 }
1443 break;
1445 if (index == 0) {
1446 this->send_control_command_(CMD_TRIGGER_SLOW);
1447 } else if (index == 1) {
1448 this->send_control_command_(CMD_TRIGGER_MEDIUM);
1449 } else if (index == 2) {
1450 this->send_control_command_(CMD_TRIGGER_FAST);
1451 }
1452 break;
1454 if (index == 0) {
1455 this->send_control_command_(CMD_INSTALL_TOP);
1456 } else if (index == 1) {
1457 this->send_control_command_(CMD_INSTALL_SIDE);
1458 }
1459 break;
1461 this->area_id_ = static_cast<uint8_t>(index);
1462 this->area_id_set_ = true;
1464 this->save_area_id_pref_(this->area_id_);
1465 break;
1466 }
1467}
1468
1470 // A report refreshes every axis the user is not in the middle of changing. An
1471 // unapplied edit is the one value here the module cannot know about, so taking
1472 // the report over it would discard what the user typed with nothing to show for it.
1473 const AreaConfig &edits = this->area_edits_;
1474 if (std::isnan(edits.x_min))
1475 this->area_x_min_ = area.x_min;
1476 if (std::isnan(edits.x_max))
1477 this->area_x_max_ = area.x_max;
1478 if (std::isnan(edits.y_min))
1479 this->area_y_min_ = area.y_min;
1480 if (std::isnan(edits.y_max))
1481 this->area_y_max_ = area.y_max;
1482 if (std::isnan(edits.z_min))
1483 this->area_z_min_ = area.z_min;
1484 if (std::isnan(edits.z_max))
1485 this->area_z_max_ = area.z_max;
1486 this->publish_area_numbers_();
1487}
1488
1489// The mirror, not the report: an axis a report was kept away from has to keep its
1490// displayed value too, or the entity and the value the next apply sends disagree.
1501
1503 if (area_id >= AREA_ID_COUNT)
1504 return;
1505 const bool interference = area_id < AREA_COUNT;
1506 const uint8_t index = interference ? area_id : static_cast<uint8_t>(area_id - AREA_COUNT);
1507 const AreaConfig &area = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1508 // The edits belonged to the area being navigated away from.
1509 this->area_edits_ = AreaConfig{};
1510 this->update_area_numbers_(area);
1511}
1512
1513bool LD6002BComponent::queue_area_config_(uint8_t area_id, const AreaConfig &desired) {
1514 // One frame carries all three pairs and cannot express a crossed one; the module
1515 // would keep a box nothing can ever be inside. Both callers arrive with the six
1516 // bounds resolved, so this is the last place that can say no -- and the return
1517 // value is how saying no reaches the caller, which must not then retire the edits
1518 // the user still has to fix.
1519 if (desired.x_min > desired.x_max || desired.y_min > desired.y_max || desired.z_min > desired.z_max) {
1520 ESP_LOGW(TAG, "Area %u not written, min above max", area_id);
1521 return false;
1522 }
1523 uint8_t data[AREA_CONFIG_LEN];
1524 write_int32_le(data, static_cast<int32_t>(area_id));
1525 write_f32_le(data + 4, desired.x_min);
1526 write_f32_le(data + 8, desired.x_max);
1527 write_f32_le(data + 12, desired.y_min);
1528 write_f32_le(data + 16, desired.y_max);
1529 write_f32_le(data + 20, desired.z_min);
1530 write_f32_le(data + 24, desired.z_max);
1531
1532 if (!this->queue_command_(TYPE_SET_AREA, data, sizeof(data))) {
1533 // Nothing is on its way, so the cache must not claim these bounds, the ack
1534 // refresh must not be armed for an ack that cannot come, and the values stay
1535 // the user's unsent edit.
1536 return false;
1537 }
1538 this->area_write_in_flight_ = true;
1539
1540 const bool interference = area_id < AREA_COUNT;
1541 const uint8_t index = interference ? area_id : static_cast<uint8_t>(area_id - AREA_COUNT);
1542 AreaConfig &store = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1543 store = desired;
1544 // The six numbers show one area at a time, and a deferred apply can land here for
1545 // an area the user has navigated away from. Same question handle_area_report_
1546 // asks before it touches them.
1547 const uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
1548 if (area_id == selected_id) {
1549 this->update_area_numbers_(store);
1550 }
1551 return true;
1552}
1553
1554void LD6002BComponent::try_apply_pending_area_(bool reported_interference) {
1555 if (!this->deferred_apply_pending_) {
1556 return;
1557 }
1558 if (this->pending_area_id_ >= AREA_ID_COUNT) {
1559 this->deferred_apply_pending_ = false;
1560 return;
1561 }
1562 const bool interference = this->pending_area_id_ < AREA_COUNT;
1563 const uint8_t index =
1564 interference ? this->pending_area_id_ : static_cast<uint8_t>(this->pending_area_id_ - AREA_COUNT);
1565 AreaConfig desired = interference ? this->interference_area_values_[index] : this->detection_area_values_[index];
1566
1567 if (!std::isnan(this->pending_area_updates_.x_min))
1568 desired.x_min = this->pending_area_updates_.x_min;
1569 if (!std::isnan(this->pending_area_updates_.x_max))
1570 desired.x_max = this->pending_area_updates_.x_max;
1571 if (!std::isnan(this->pending_area_updates_.y_min))
1572 desired.y_min = this->pending_area_updates_.y_min;
1573 if (!std::isnan(this->pending_area_updates_.y_max))
1574 desired.y_max = this->pending_area_updates_.y_max;
1575 if (!std::isnan(this->pending_area_updates_.z_min))
1576 desired.z_min = this->pending_area_updates_.z_min;
1577 if (!std::isnan(this->pending_area_updates_.z_max))
1578 desired.z_max = this->pending_area_updates_.z_max;
1579
1580 if (std::isnan(desired.x_min) || std::isnan(desired.x_max) || std::isnan(desired.y_min) ||
1581 std::isnan(desired.y_max) || std::isnan(desired.z_min) || std::isnan(desired.z_max)) {
1582 // Only the report covering this area's half can still fill it in, and there is
1583 // exactly one of those per read. Once it has landed with a bound still unknown,
1584 // nothing further is coming and waiting means waiting forever.
1585 if (reported_interference == interference) {
1586 this->deferred_apply_pending_ = false;
1588 ESP_LOGW(TAG, "Dropping deferred area apply, area report incomplete");
1589 }
1590 return;
1591 }
1592
1593 const uint8_t area_id = this->pending_area_id_;
1594 this->deferred_apply_pending_ = false;
1595 if (!this->queue_area_config_(area_id, desired)) {
1596 // Nothing was queued, so this is a drop like the other two: hand the staged
1597 // values back rather than leaving them with no ledger to protect them.
1599 }
1600}
1601
1603#ifdef USE_SELECT
1604 if (this->area_id_select_ == nullptr) {
1605 return;
1606 }
1607 this->area_id_pref_ = this->area_id_select_->make_entity_preference<uint8_t>();
1608 this->area_id_pref_initialized_ = true;
1609
1610 uint8_t value = 0;
1611 if (!this->area_id_pref_.load(&value) || value >= AREA_ID_COUNT) {
1612 // No stored selection. The numbers are about to display this area either way,
1613 // so select it for real: a displayed area that apply_area then refuses to write
1614 // is the one combination the user cannot make sense of.
1615 value = AREA_ID_DEFAULT;
1616 }
1617 this->area_id_select_->publish_state(value);
1618 this->area_id_ = value;
1619 this->area_id_set_ = true;
1620 this->update_area_numbers_for_id_(value);
1621#endif
1622}
1623
1625#ifdef USE_SELECT
1626 if (!this->area_id_pref_initialized_) {
1627 return;
1628 }
1629 this->area_id_pref_.save(&value);
1630#endif
1631}
1632
1634#ifdef USE_TEXT_SENSOR
1635 if (this->ota_version_text_sensor_ == nullptr) {
1636 return;
1637 }
1639 this->version_pref_initialized_ = true;
1640
1641 VersionPref pref{};
1642 if (this->version_pref_.load(&pref) && pref.value[0] != '\0') {
1643 pref.value[sizeof(pref.value) - 1] = '\0';
1644 this->ota_version_text_sensor_->publish_state(pref.value);
1645 }
1646#endif
1647}
1648
1650#ifdef USE_TEXT_SENSOR
1651 if (!this->version_pref_initialized_) {
1652 return;
1653 }
1654 VersionPref pref{};
1655 std::strncpy(pref.value, value, sizeof(pref.value) - 1);
1656 pref.value[sizeof(pref.value) - 1] = '\0';
1657 this->version_pref_.save(&pref);
1658#endif
1659}
1660
1661#ifdef USE_SENSOR
1663 if (!this->last_target_presence_[index]) {
1664 return;
1665 }
1666 TargetSensors &target = this->targets_[index];
1667 if (target.x != nullptr) {
1668 target.x->publish_state(NAN);
1669 }
1670 if (target.y != nullptr) {
1671 target.y->publish_state(NAN);
1672 }
1673 if (target.z != nullptr) {
1674 target.z->publish_state(NAN);
1675 }
1676 if (target.dop_idx != nullptr) {
1677 target.dop_idx->publish_state(NAN);
1678 }
1679 if (target.cluster_id != nullptr) {
1680 target.cluster_id->publish_state(NAN);
1681 }
1682 // The slot is free: the next person's id is new even when it repeats this one.
1683 this->last_cluster_id_valid_[index] = false;
1684}
1685#endif
1686
1688 // The staged values become an unsent edit again, but only for the user who is
1689 // still looking at the area they were staged for; anyone else's ledger belongs to
1690 // the area they are on now.
1691 const uint8_t selected_id = this->area_id_set_ ? this->area_id_ : AREA_ID_DEFAULT;
1692 if (this->pending_area_id_ != selected_id) {
1693 return;
1694 }
1695 // Axis by axis rather than a whole-struct assignment: the user can have edited
1696 // another bound while the deferral was in flight, and that edit is newer than
1697 // anything the deferral staged. Assigning over the ledger would drop it back to
1698 // NaN and let the next report take the value away. A live edit wins; only an axis
1699 // with nothing in the ledger takes its staged value back.
1700 //
1701 // The mirror moves with the ledger, because on the report path handle_area_report_
1702 // ran update_area_numbers_ before the replay, with the ledger still empty -- so the
1703 // mirror already holds the module's bounds and both the entities and the next apply
1704 // would build on them. On the timeout path no report arrived, the mirror still
1705 // holds the staged values, and this is an identity.
1706 const AreaConfig &staged = this->pending_area_updates_;
1707 if (std::isnan(this->area_edits_.x_min) && !std::isnan(staged.x_min)) {
1708 this->area_edits_.x_min = staged.x_min;
1709 this->area_x_min_ = staged.x_min;
1710 }
1711 if (std::isnan(this->area_edits_.x_max) && !std::isnan(staged.x_max)) {
1712 this->area_edits_.x_max = staged.x_max;
1713 this->area_x_max_ = staged.x_max;
1714 }
1715 if (std::isnan(this->area_edits_.y_min) && !std::isnan(staged.y_min)) {
1716 this->area_edits_.y_min = staged.y_min;
1717 this->area_y_min_ = staged.y_min;
1718 }
1719 if (std::isnan(this->area_edits_.y_max) && !std::isnan(staged.y_max)) {
1720 this->area_edits_.y_max = staged.y_max;
1721 this->area_y_max_ = staged.y_max;
1722 }
1723 if (std::isnan(this->area_edits_.z_min) && !std::isnan(staged.z_min)) {
1724 this->area_edits_.z_min = staged.z_min;
1725 this->area_z_min_ = staged.z_min;
1726 }
1727 if (std::isnan(this->area_edits_.z_max) && !std::isnan(staged.z_max)) {
1728 this->area_edits_.z_max = staged.z_max;
1729 this->area_z_max_ = staged.z_max;
1730 }
1731 this->publish_area_numbers_();
1732}
1733
1735 if (!this->area_presence_any_) {
1736 return;
1737 }
1738 // Nothing else corrects this: 0x0A0A carries no period the protocol states and no
1739 // command stops it, so the module going unattended is the only moment the
1740 // component can know a stored "occupied" has stopped being true.
1741 this->area_presence_any_ = false;
1742#ifdef USE_BINARY_SENSOR
1743 for (uint8_t i = 0; i < AREA_COUNT; i++) {
1744 if (this->area_presence_[i] != nullptr) {
1745 this->area_presence_[i]->publish_state(false);
1746 }
1747 }
1748 const bool presence = this->target_presence_any_ || this->area_presence_any_;
1749 if (this->presence_binary_sensor_ != nullptr) {
1750 this->presence_binary_sensor_->publish_state(presence);
1751 }
1752#endif
1753}
1754
1756 // Nothing corrects any of this until the stream comes back. The slot table goes
1757 // with it: slots key on cluster ids, which only track a person while reports are
1758 // arriving, and the room can empty and refill across the gap -- so the next
1759 // report starts from an empty table and fills slots in wire order, rather than
1760 // handing one back to whoever last held that id.
1761 for (uint8_t i = 0; i < MAX_TARGETS; i++) {
1762#ifdef USE_SENSOR
1763 this->clear_target_slot_(i);
1764 this->last_target_presence_[i] = false;
1765#endif
1766 if (this->slot_occupied_[i]) {
1767 this->slot_occupied_[i] = false;
1768#ifdef USE_BINARY_SENSOR
1769 if (this->target_presence_[i] != nullptr) {
1770 this->target_presence_[i]->publish_state(false);
1771 }
1772#endif
1773 }
1774 }
1775#ifdef USE_SENSOR
1776 if (this->last_target_count_ != 0xFFFFFFFF) {
1777 if (this->target_count_sensor_ != nullptr) {
1779 }
1780 this->last_target_count_ = 0xFFFFFFFF;
1781 }
1782#endif
1783 if (this->target_presence_any_) {
1784 this->target_presence_any_ = false;
1785#ifdef USE_BINARY_SENSOR
1786 bool presence = this->target_presence_any_ || this->area_presence_any_;
1787 if (this->presence_binary_sensor_ != nullptr) {
1788 this->presence_binary_sensor_->publish_state(presence);
1789 }
1790#endif
1792 }
1793}
1794
1796 switch (type) {
1798 this->low_power_enabled_ = state;
1799 this->low_power_reported_ = true;
1800 this->send_control_command_(state ? CMD_LOW_POWER_ON : CMD_LOW_POWER_OFF);
1802 break;
1805 this->send_control_command_(state ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
1806#ifdef USE_SENSOR
1807 // The count only moves while the stream runs, so the last one would stand as
1808 // a live reading. The dedup sentinel is cleared with it: the same count is
1809 // new again when the stream comes back.
1810 if (!state && this->point_count_sensor_ != nullptr && this->last_point_count_ != 0xFFFFFFFF) {
1812 this->last_point_count_ = 0xFFFFFFFF;
1813 }
1814#endif
1815 break;
1818 this->send_control_command_(state ? CMD_TARGET_DISPLAY_ON : CMD_TARGET_DISPLAY_OFF);
1819 if (!state) {
1820 // Every target entity is fed by the reports this just stopped.
1821 this->clear_target_state_();
1822 }
1823 break;
1824 }
1825}
1826
1828 switch (type) {
1830 this->apply_area_config_();
1831 break;
1833 this->send_control_command_(CMD_AUTO_INTERFERENCE);
1834 // The module recomputes the interference areas without reporting them.
1835 this->send_control_command_(CMD_GET_AREAS);
1836 break;
1838 this->send_control_command_(CMD_GET_AREAS);
1839 break;
1841 this->send_control_command_(CMD_CLEAR_INTERFERENCE);
1842 // The module rewrites the areas but does not report them, so ask for the new geometry the
1843 // way the apply_area ack path does; the queue keeps it behind the command above.
1844 this->send_control_command_(CMD_GET_AREAS);
1845 break;
1847 this->send_control_command_(CMD_RESET_DETECTION_AREA);
1848 this->send_control_command_(CMD_GET_AREAS);
1849 break;
1851 this->send_control_command_(CMD_GET_DELAY);
1852 break;
1854 this->send_control_command_(CMD_GET_SENSITIVITY);
1855 break;
1857 this->send_control_command_(CMD_GET_TRIGGER);
1858 break;
1860 this->send_control_command_(CMD_GET_Z_RANGE);
1861 break;
1863 this->send_control_command_(CMD_GET_INSTALLATION);
1864 break;
1866 this->send_control_command_(CMD_GET_LOW_POWER);
1867 break;
1869 this->send_control_command_(CMD_GET_LOW_POWER_SLEEP);
1870 break;
1872 this->send_control_command_(CMD_RESET_UNATTENDED);
1873 break;
1874 case ButtonType::WAKE:
1875 this->wake_();
1876 break;
1877 }
1878}
1879
1880} // 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:2142
T * allocate(size_t n)
Definition helpers.h:2169
constexpr const char * c_str() const
Definition string_ref.h:73
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:1513
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:699
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
static constexpr const char * WAKE_BUTTON_TIMEOUT
Definition ld6002b.h:418
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:1433
void save_area_id_pref_(uint8_t value)
Definition ld6002b.cpp:1624
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:965
void set_switch_state(SwitchType type, bool state)
Definition ld6002b.cpp:1795
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:1382
void handle_trigger_speed_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:939
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:1649
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:1502
void update_area_numbers_(const AreaConfig &area)
Definition ld6002b.cpp:1469
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:1081
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:1211
static uint16_t read_u16_be(const uint8_t *data)
Definition ld6002b.cpp:206
static constexpr const char * AREA_REFRESH_TIMEOUT
Definition ld6002b.h:421
void handle_area_presence_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:843
void publish_number_clamped_(number::Number *number, float value)
Definition ld6002b.cpp:1055
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:869
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:978
std::array< AreaConfig, AREA_COUNT > detection_area_values_
Definition ld6002b.h:468
static constexpr uint32_t CMD_ACK_TIMEOUT_MS
Definition ld6002b.h:410
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:917
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:926
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:609
void send_command_(uint16_t type, const uint8_t *data, uint8_t len)
Definition ld6002b.cpp:1207
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:1040
void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track)
Definition ld6002b.cpp:1250
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:992
void handle_work_mode_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:1001
void press_button(ButtonType type)
Definition ld6002b.cpp:1827
number::Number * area_x_min_number_
Definition ld6002b.h:364
void try_apply_pending_area_(bool reported_interference)
Definition ld6002b.cpp:1554
bool queue_command_(uint16_t type, const uint8_t *data, uint8_t len)
Definition ld6002b.cpp:1102
void clear_target_slot_(uint8_t index)
Definition ld6002b.cpp:1662
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:1291
void handle_point_cloud_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:819
void handle_z_range_report_(const uint8_t *data, uint16_t len)
Definition ld6002b.cpp:952
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:68
void turn_on()
Turn this switch on.
Definition switch.cpp:20
void turn_off()
Turn this switch off.
Definition switch.cpp:24
void publish_state(bool state)
Publish a state to the front-end from the back-end.
Definition switch.cpp:57
optional< bool > get_initial_state_with_restore_mode()
Returns the initial state of the switch, after applying restore mode rules.
Definition switch.cpp:43
void publish_state(const std::string &state)
Definition text_sensor.h:40
void write_byte(uint8_t data)
Definition uart.h:19
uint16_t type
bool state
Definition fan.h:2
bool z
Definition msa3xx.h:1
const void size_t len
Definition hal.h:64
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