[ld6002b] Add switch, number and text sensor platforms (3/5) (#17821)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
Mustafa KURU
2026-08-07 11:11:29 -04:00
committed by GitHub
co-authored by Jonathan Swoboda
parent 1bbe8b415f
commit b802740997
12 changed files with 949 additions and 27 deletions
+10
View File
@@ -1,8 +1,18 @@
CONF_AUTO_WAKE = "auto_wake"
CONF_CLUSTER_ID = "cluster_id"
CONF_DOPPLER_INDEX = "doppler_index"
CONF_HOLD_DELAY = "hold_delay"
CONF_LD6002B_ID = "ld6002b_id"
CONF_LOW_POWER = "low_power"
CONF_LOW_POWER_SLEEP_TIME = "low_power_sleep_time"
CONF_OTA_VERSION = "ota_version"
CONF_POINT_CLOUD = "point_cloud"
CONF_POINT_COUNT = "point_count"
CONF_TARGET_DISPLAY = "target_display"
CONF_WAKEUP_PULSE = "wakeup_pulse"
CONF_WORK_MODE = "work_mode"
CONF_Z = "z"
CONF_Z_MAX = "z_max"
CONF_Z_MIN = "z_min"
MAX_TARGETS = 3
+564 -27
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <cstdio>
#include <cstring>
namespace esphome::ld6002b {
@@ -14,20 +15,40 @@ static constexpr uint32_t SETUP_DELAY_MS = 100;
// Command/message types
static constexpr uint16_t TYPE_CONTROL = 0x0201;
static constexpr uint16_t TYPE_SET_HOLD_DELAY = 0x0203;
static constexpr uint16_t TYPE_SET_Z_RANGE = 0x0204;
static constexpr uint16_t TYPE_SET_LOW_POWER_SLEEP = 0x0205;
static constexpr uint16_t TYPE_REPORT_TARGET = 0x0A04;
static constexpr uint16_t TYPE_REPORT_POINT_CLOUD = 0x0A08;
static constexpr uint16_t TYPE_REPORT_DELAY = 0x0A0D;
static constexpr uint16_t TYPE_REPORT_Z_RANGE = 0x0A10;
static constexpr uint16_t TYPE_REPORT_LOW_POWER = 0x0A12;
static constexpr uint16_t TYPE_REPORT_LOW_POWER_SLEEP = 0x0A13;
static constexpr uint16_t TYPE_REPORT_WORK_MODE = 0x0A14;
static constexpr uint16_t TYPE_QUERY_VERSION = 0xFFFF;
// Control command values for TYPE_CONTROL
static constexpr uint32_t CMD_GET_DELAY = 0x05;
static constexpr uint32_t CMD_POINT_CLOUD_ON = 0x06;
static constexpr uint32_t CMD_POINT_CLOUD_OFF = 0x07;
static constexpr uint32_t CMD_TARGET_DISPLAY_ON = 0x08;
static constexpr uint32_t CMD_TARGET_DISPLAY_OFF = 0x09;
static constexpr uint32_t CMD_GET_Z_RANGE = 0x12;
static constexpr uint32_t CMD_LOW_POWER_ON = 0x16;
static constexpr uint32_t CMD_LOW_POWER_OFF = 0x17;
static constexpr uint32_t CMD_GET_LOW_POWER = 0x18;
static constexpr uint32_t CMD_GET_LOW_POWER_SLEEP = 0x19;
static constexpr uint16_t TARGET_DATA_LEN = 20; // x,y,z,dop_idx,cluster_id
static constexpr uint8_t VERSION_QUERY_DATA[] = {0x01, 0x01, 0x00, 0x00};
#ifdef ESPHOME_LOG_HAS_VERBOSE
static const char *control_command_name(uint32_t command) {
switch (command) {
case CMD_GET_DELAY:
return "get_delay";
case CMD_POINT_CLOUD_ON:
return "point_cloud_on";
case CMD_POINT_CLOUD_OFF:
@@ -36,10 +57,68 @@ static const char *control_command_name(uint32_t command) {
return "target_display_on";
case CMD_TARGET_DISPLAY_OFF:
return "target_display_off";
case CMD_GET_Z_RANGE:
return "get_z_range";
case CMD_LOW_POWER_ON:
return "low_power_on";
case CMD_LOW_POWER_OFF:
return "low_power_off";
case CMD_GET_LOW_POWER:
return "get_low_power";
case CMD_GET_LOW_POWER_SLEEP:
return "get_low_power_sleep";
default:
return "unknown";
}
}
static const char *frame_type_name(uint16_t type) {
switch (type) {
case TYPE_CONTROL:
return "control";
case TYPE_SET_HOLD_DELAY:
return "set_hold_delay";
case TYPE_SET_Z_RANGE:
return "set_z_range";
case TYPE_SET_LOW_POWER_SLEEP:
return "set_low_power_sleep";
case TYPE_REPORT_TARGET:
return "report_target";
case TYPE_REPORT_POINT_CLOUD:
return "report_point_cloud";
case TYPE_REPORT_DELAY:
return "report_delay";
case TYPE_REPORT_Z_RANGE:
return "report_z_range";
case TYPE_REPORT_LOW_POWER:
return "report_low_power";
case TYPE_REPORT_LOW_POWER_SLEEP:
return "report_low_power_sleep";
case TYPE_REPORT_WORK_MODE:
return "report_work_mode";
case TYPE_QUERY_VERSION:
return "query_version";
default:
return "unknown";
}
}
static bool is_expected_control_report(uint32_t command, uint16_t type) {
switch (command) {
case CMD_GET_DELAY:
return type == TYPE_REPORT_DELAY;
case CMD_GET_Z_RANGE:
return type == TYPE_REPORT_Z_RANGE;
case CMD_GET_LOW_POWER:
case CMD_LOW_POWER_ON:
case CMD_LOW_POWER_OFF:
return type == TYPE_REPORT_LOW_POWER;
case CMD_GET_LOW_POWER_SLEEP:
return type == TYPE_REPORT_LOW_POWER_SLEEP;
default:
return false;
}
}
#endif
uint16_t LD6002BComponent::read_u16_be(const uint8_t *data) { return (static_cast<uint16_t>(data[0]) << 8) | data[1]; }
@@ -70,10 +149,25 @@ void LD6002BComponent::write_u32_le(uint8_t *data, uint32_t value) {
data[3] = (value >> 24) & 0xFF;
}
void LD6002BComponent::write_f32_le(uint8_t *data, float value) {
uint32_t raw;
std::memcpy(&raw, &value, sizeof(raw));
write_u32_le(data, raw);
}
void LD6002BComponent::setup() {
// Only the point cloud stream needs the larger frame; nothing resizes the buffer after setup.
bool point_cloud_configured = false;
#ifdef USE_SENSOR
point_cloud_configured = point_cloud_configured || this->point_count_sensor_ != nullptr;
#endif
#ifdef USE_SWITCH
point_cloud_configured = point_cloud_configured || this->point_cloud_switch_ != nullptr;
#endif
this->max_data_len_ = point_cloud_configured ? DEFAULT_MAX_DATA_LEN_POINT_CLOUD : DEFAULT_MAX_DATA_LEN;
// One allocation for the component lifetime; the parser reuses it for the header and every payload.
RAMAllocator<uint8_t> allocator;
this->data_buf_ = allocator.allocate(DEFAULT_MAX_DATA_LEN);
this->data_buf_ = allocator.allocate(this->max_data_len_);
if (this->data_buf_ == nullptr) {
this->mark_failed(LOG_STR("Failed to allocate frame buffer"));
return;
@@ -108,25 +202,120 @@ void LD6002BComponent::setup() {
}
}
#endif
if (want_target_stream) {
this->send_control_command_(CMD_TARGET_DISPLAY_ON);
#ifdef USE_TEXT_SENSOR
// The work mode fallback reads presence off this stream, so it counts as a
// consumer of it here. This only feeds the automatic branch below: with a
// target_display switch configured that switch still decides, and the
// fallback weighs no presence at all while the stream is off.
want_target_stream = want_target_stream || this->work_mode_text_sensor_ != nullptr;
#endif
bool target_display_controlled = false;
#ifdef USE_SWITCH
if (this->target_display_switch_ != nullptr) {
target_display_controlled = true;
// Nothing reports this switch back, so its restored state is the only state
// there is. Restoring through the switch keeps its inversion in the path:
// the restored value is logical, and turn_on()/turn_off() are what turn it
// into the raw command, the published state and the stream flag.
const bool state = this->target_display_switch_->get_initial_state_with_restore_mode().value_or(true);
if (state) {
this->target_display_switch_->turn_on();
} else {
this->target_display_switch_->turn_off();
}
}
#endif
if (!target_display_controlled) {
// No switch: the stream follows its consumers. With none, nothing is sent
// and the module's own default stands -- but the reports are gated out
// regardless, because there is nothing configured for them to feed.
this->target_display_enabled_ = want_target_stream;
if (want_target_stream) {
this->send_control_command_(CMD_TARGET_DISPLAY_ON);
}
}
this->send_control_command_(CMD_POINT_CLOUD_OFF);
bool point_cloud_controlled = false;
#ifdef USE_SWITCH
if (this->point_cloud_switch_ != nullptr) {
point_cloud_controlled = true;
// The switch owns the stream, so it is also what applies the restored state:
// driving it rather than the module keeps the entity's inversion in the path.
const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false);
if (state) {
this->point_cloud_switch_->turn_on();
} else {
this->point_cloud_switch_->turn_off();
}
}
#endif
if (!point_cloud_controlled) {
// No switch: the stream follows the sensor that reads it, which is also what
// the frame buffer above was sized for.
bool want_point_cloud = false;
#ifdef USE_SENSOR
want_point_cloud = this->point_count_sensor_ != nullptr;
#endif
this->send_control_command_(want_point_cloud ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
this->point_cloud_enabled_ = want_point_cloud;
}
#ifdef USE_NUMBER
if (this->z_min_number_ != nullptr || this->z_max_number_ != nullptr) {
this->send_control_command_(CMD_GET_Z_RANGE);
}
if (this->low_power_sleep_number_ != nullptr) {
this->send_control_command_(CMD_GET_LOW_POWER_SLEEP);
}
if (this->hold_delay_number_ != nullptr) {
this->send_control_command_(CMD_GET_DELAY);
}
#endif
#ifdef USE_SWITCH
bool want_low_power = this->low_power_switch_ != nullptr;
if (want_low_power) {
// The module reports this one back, so the query below confirms what it took.
// Driving the switch applies its inversion; it also marks the restored value
// as reported, so the work mode fallback runs on that until the query lands.
const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false);
if (state) {
this->low_power_switch_->turn_on();
} else {
this->low_power_switch_->turn_off();
}
}
#else
bool want_low_power = false;
#endif
#ifdef USE_TEXT_SENSOR
want_low_power = want_low_power || this->work_mode_text_sensor_ != nullptr;
#endif
if (want_low_power) {
this->send_control_command_(CMD_GET_LOW_POWER);
}
this->init_version_pref_();
#ifdef USE_TEXT_SENSOR
if (this->ota_version_text_sensor_ != nullptr) {
this->queue_command_(TYPE_QUERY_VERSION, VERSION_QUERY_DATA, sizeof(VERSION_QUERY_DATA));
}
#endif
});
}
void LD6002BComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"HLK-LD6002B:\n"
" Auto wake: %s",
this->auto_wake_ ? "true" : "false");
" Auto wake: %s\n"
" Max data length: %u",
this->auto_wake_ ? "true" : "false", static_cast<unsigned>(this->max_data_len_));
if (this->wakeup_pin_ != nullptr) {
LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
ESP_LOGCONFIG(TAG, " Wake Pulse: %ums", this->wakeup_pulse_ms_);
}
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Target Count", this->target_count_sensor_);
LOG_SENSOR(" ", "Point Count", this->point_count_sensor_);
for (auto &target : this->targets_) {
LOG_SENSOR(" ", "Target X", target.x);
LOG_SENSOR(" ", "Target Y", target.y);
@@ -141,6 +330,21 @@ void LD6002BComponent::dump_config() {
LOG_BINARY_SENSOR(" ", "Target Presence", this->target_presence_[i]);
}
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Work Mode", this->work_mode_text_sensor_);
LOG_TEXT_SENSOR(" ", "OTA Version", this->ota_version_text_sensor_);
#endif
#ifdef USE_NUMBER
LOG_NUMBER(" ", "Hold Delay", this->hold_delay_number_);
LOG_NUMBER(" ", "Z Min", this->z_min_number_);
LOG_NUMBER(" ", "Z Max", this->z_max_number_);
LOG_NUMBER(" ", "Low Power Sleep", this->low_power_sleep_number_);
#endif
#ifdef USE_SWITCH
LOG_SWITCH(" ", "Low Power", this->low_power_switch_);
LOG_SWITCH(" ", "Point Cloud", this->point_cloud_switch_);
LOG_SWITCH(" ", "Target Display", this->target_display_switch_);
#endif
}
void LD6002BComponent::loop() {
@@ -192,7 +396,7 @@ void LD6002BComponent::parse_byte_(uint8_t byte) {
this->frame_type_ = read_u16_be(this->data_buf_ + 4);
// The length is only trustworthy once the header checksum has been verified, so just
// remember that the frame is oversized and let the HCK state act on it.
this->frame_oversize_ = this->data_len_ > DEFAULT_MAX_DATA_LEN;
this->frame_oversize_ = this->data_len_ > this->max_data_len_;
this->parse_state_ = ParseState::HCK;
}
}
@@ -267,16 +471,54 @@ void LD6002BComponent::handle_frame_(uint16_t type, const uint8_t *data, uint16_
return;
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
const uint32_t active_control_command =
(this->command_active_ && this->active_command_.type == TYPE_CONTROL && this->active_command_.len >= 4)
? read_u32_le(this->active_command_.data.data())
: 0;
if (active_control_command != 0 && is_expected_control_report(active_control_command, type)) {
ESP_LOGV(TAG, "Received %s (0x%04X) while waiting for %s (0x%02" PRIX32 ") ACK", frame_type_name(type), type,
control_command_name(active_control_command), active_control_command);
}
#endif
switch (type) {
case TYPE_REPORT_TARGET:
this->handle_target_report_(data, len);
break;
case TYPE_REPORT_POINT_CLOUD:
this->handle_point_cloud_(data, len);
break;
case TYPE_REPORT_DELAY:
this->handle_delay_report_(data, len);
break;
case TYPE_REPORT_Z_RANGE:
this->handle_z_range_report_(data, len);
break;
case TYPE_REPORT_LOW_POWER:
this->handle_low_power_report_(data, len);
break;
case TYPE_REPORT_LOW_POWER_SLEEP:
this->handle_low_power_sleep_report_(data, len);
break;
case TYPE_REPORT_WORK_MODE:
this->handle_work_mode_report_(data, len);
break;
case TYPE_QUERY_VERSION:
this->handle_version_report_(data, len);
break;
default:
break;
}
}
void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len) {
// The module stops streaming when it acts on the command, not when the command
// is queued, so trailing frames after an off must not repopulate what
// set_switch_state just cleared.
if (!this->target_display_enabled_) {
return;
}
if (len < 4)
return;
@@ -339,6 +581,7 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
this->presence_binary_sensor_->publish_state(this->target_presence_any_);
}
#endif
this->update_work_mode_fallback_();
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
bool has_target = this->slot_occupied_[i];
@@ -373,26 +616,7 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
#endif
} else {
#ifdef USE_SENSOR
TargetSensors &target = this->targets_[i];
if (this->last_target_presence_[i]) {
if (target.x != nullptr) {
target.x->publish_state(NAN);
}
if (target.y != nullptr) {
target.y->publish_state(NAN);
}
if (target.z != nullptr) {
target.z->publish_state(NAN);
}
if (target.dop_idx != nullptr) {
target.dop_idx->publish_state(NAN);
}
if (target.cluster_id != nullptr) {
target.cluster_id->publish_state(NAN);
}
// The slot is free: the next person's id is new even when it repeats this one.
this->last_cluster_id_valid_[i] = false;
}
this->clear_target_slot_(i);
#endif
}
#ifdef USE_BINARY_SENSOR
@@ -407,6 +631,150 @@ void LD6002BComponent::handle_target_report_(const uint8_t *data, uint16_t len)
}
}
void LD6002BComponent::handle_point_cloud_(const uint8_t *data, uint16_t len) {
// Same window as the target stream: a frame already in flight must not put the
// count back after the switch cleared it.
if (!this->point_cloud_enabled_) {
return;
}
if (len < 4)
return;
#ifdef USE_SENSOR
uint32_t point_num = read_u32_le(data);
if (this->point_count_sensor_ != nullptr) {
if (point_num != this->last_point_count_) {
this->point_count_sensor_->publish_state(point_num);
this->last_point_count_ = point_num;
}
}
#endif
}
void LD6002BComponent::handle_delay_report_(const uint8_t *data, uint16_t len) {
if (len < 4)
return;
#ifdef USE_NUMBER
uint32_t delay = read_u32_le(data);
this->publish_number_clamped_(this->hold_delay_number_, delay);
#endif
}
void LD6002BComponent::handle_z_range_report_(const uint8_t *data, uint16_t len) {
if (len < 8)
return;
float z_min = read_f32_le(data);
float z_max = read_f32_le(data + 4);
this->z_min_ = z_min;
this->z_max_ = z_max;
#ifdef USE_NUMBER
this->publish_number_clamped_(this->z_min_number_, z_min);
this->publish_number_clamped_(this->z_max_number_, z_max);
#endif
}
void LD6002BComponent::handle_low_power_report_(const uint8_t *data, uint16_t len) {
if (len < 1)
return;
bool enabled = data[0] != 0;
this->low_power_enabled_ = enabled;
this->low_power_reported_ = true;
#ifdef USE_SWITCH
if (this->low_power_switch_ != nullptr) {
this->low_power_switch_->publish_state(enabled);
}
#endif
this->update_work_mode_fallback_();
}
void LD6002BComponent::handle_low_power_sleep_report_(const uint8_t *data, uint16_t len) {
if (len < 4)
return;
#ifdef USE_NUMBER
uint32_t sleep_ms = read_u32_le(data);
this->publish_number_clamped_(this->low_power_sleep_number_, sleep_ms);
#endif
}
void LD6002BComponent::handle_work_mode_report_(const uint8_t *data, uint16_t len) {
if (len < 1)
return;
#ifdef USE_TEXT_SENSOR
const bool low_power = (data[0] == 0);
if (this->work_mode_text_sensor_ != nullptr) {
this->work_mode_reported_ = true;
this->publish_work_mode_(low_power);
}
#endif
}
void LD6002BComponent::update_work_mode_fallback_() {
#ifdef USE_TEXT_SENSOR
if (this->work_mode_text_sensor_ == nullptr || this->work_mode_reported_) {
return;
}
if (!this->low_power_reported_) {
return;
}
// Presence is only meaningful while the stream that maintains it runs; with it
// off there is nothing to weigh and low power alone decides.
const bool presence = this->target_display_enabled_ && this->target_presence_any_;
this->publish_work_mode_(this->low_power_enabled_ && !presence);
#endif
}
void LD6002BComponent::publish_work_mode_(bool low_power) {
#ifdef USE_TEXT_SENSOR
if (this->work_mode_text_sensor_ == nullptr) {
return;
}
if (this->last_work_mode_valid_ && this->last_work_mode_low_power_ == low_power) {
return;
}
this->work_mode_text_sensor_->publish_state(low_power ? "low_power" : "normal");
this->last_work_mode_valid_ = true;
this->last_work_mode_low_power_ = low_power;
#endif
}
#ifdef USE_NUMBER
void LD6002BComponent::publish_number_clamped_(number::Number *number, float value) {
if (number == nullptr)
return;
const float min_value = number->traits.get_min_value();
const float max_value = number->traits.get_max_value();
// Outside the declared range the user cannot write the value back, so publish
// what they can reach and say what the module actually sent.
if (value < min_value || value > max_value) {
ESP_LOGW(TAG, "'%s': module reported %.1f, clamped to %.1f..%.1f", number->get_name().c_str(), value, min_value,
max_value);
value = std::clamp(value, min_value, max_value);
}
number->publish_state(value);
}
#endif
void LD6002BComponent::handle_version_report_(const uint8_t *data, uint16_t len) {
if (len < 4)
return;
#ifdef USE_TEXT_SENSOR
if (this->ota_version_text_sensor_ == nullptr)
return;
uint8_t project = data[0];
uint8_t major = data[1];
uint8_t minor = data[2];
uint8_t patch = data[3];
char buf[32];
if (project == 0) {
std::snprintf(buf, sizeof(buf), "%u.%u.%u", major, minor, patch);
} else {
std::snprintf(buf, sizeof(buf), "p%u %u.%u.%u", project, major, minor, patch);
}
this->ota_version_text_sensor_->publish_state(buf);
this->save_version_pref_(buf);
#endif
}
void LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_t len) {
if (len > CMD_MAX_DATA_LEN) {
ESP_LOGW(TAG, "Command data too large: %u", len);
@@ -587,4 +955,173 @@ void LD6002BComponent::send_control_command_(uint32_t command) {
this->queue_command_(TYPE_CONTROL, data, sizeof(data));
}
void LD6002BComponent::send_z_range_() {
// One frame carries both bounds, so half a range cannot be written.
if (std::isnan(this->z_min_) || std::isnan(this->z_max_)) {
ESP_LOGW(TAG, "Z range not written, other bound unknown");
return;
}
// Both bounds are known and crossed; the frame has no way to say that.
if (this->z_min_ > this->z_max_) {
ESP_LOGW(TAG, "Z range not written, min above max");
return;
}
uint8_t data[8];
write_f32_le(data, this->z_min_);
write_f32_le(data + 4, this->z_max_);
this->queue_command_(TYPE_SET_Z_RANGE, data, sizeof(data));
}
void LD6002BComponent::set_number_value(NumberType type, float value) {
switch (type) {
case NumberType::HOLD_DELAY: {
uint32_t delay = static_cast<uint32_t>(value);
uint8_t data[4];
write_u32_le(data, delay);
this->queue_command_(TYPE_SET_HOLD_DELAY, data, sizeof(data));
break;
}
case NumberType::Z_MIN:
this->z_min_ = value;
this->send_z_range_();
break;
case NumberType::Z_MAX:
this->z_max_ = value;
this->send_z_range_();
break;
case NumberType::LOW_POWER_SLEEP: {
uint32_t sleep_ms = static_cast<uint32_t>(value);
uint8_t data[4];
write_u32_le(data, sleep_ms);
this->queue_command_(TYPE_SET_LOW_POWER_SLEEP, data, sizeof(data));
break;
}
}
}
void LD6002BComponent::init_version_pref_() {
#ifdef USE_TEXT_SENSOR
if (this->ota_version_text_sensor_ == nullptr) {
return;
}
this->version_pref_ = this->ota_version_text_sensor_->make_entity_preference<VersionPref>();
this->version_pref_initialized_ = true;
VersionPref pref{};
if (this->version_pref_.load(&pref) && pref.value[0] != '\0') {
pref.value[sizeof(pref.value) - 1] = '\0';
this->ota_version_text_sensor_->publish_state(pref.value);
}
#endif
}
void LD6002BComponent::save_version_pref_(const char *value) {
#ifdef USE_TEXT_SENSOR
if (!this->version_pref_initialized_) {
return;
}
VersionPref pref{};
std::strncpy(pref.value, value, sizeof(pref.value) - 1);
pref.value[sizeof(pref.value) - 1] = '\0';
this->version_pref_.save(&pref);
#endif
}
#ifdef USE_SENSOR
void LD6002BComponent::clear_target_slot_(uint8_t index) {
if (!this->last_target_presence_[index]) {
return;
}
TargetSensors &target = this->targets_[index];
if (target.x != nullptr) {
target.x->publish_state(NAN);
}
if (target.y != nullptr) {
target.y->publish_state(NAN);
}
if (target.z != nullptr) {
target.z->publish_state(NAN);
}
if (target.dop_idx != nullptr) {
target.dop_idx->publish_state(NAN);
}
if (target.cluster_id != nullptr) {
target.cluster_id->publish_state(NAN);
}
// The slot is free: the next person's id is new even when it repeats this one.
this->last_cluster_id_valid_[index] = false;
}
#endif
void LD6002BComponent::clear_target_state_() {
// Nothing corrects any of this until the stream comes back. The slot table goes
// with it: slots key on cluster ids, which only track a person while reports are
// arriving, and the room can empty and refill across the gap -- so the next
// report starts from an empty table and fills slots in wire order, rather than
// handing one back to whoever last held that id.
for (uint8_t i = 0; i < MAX_TARGETS; i++) {
#ifdef USE_SENSOR
this->clear_target_slot_(i);
this->last_target_presence_[i] = false;
#endif
if (this->slot_occupied_[i]) {
this->slot_occupied_[i] = false;
#ifdef USE_BINARY_SENSOR
if (this->target_presence_[i] != nullptr) {
this->target_presence_[i]->publish_state(false);
}
#endif
}
}
#ifdef USE_SENSOR
if (this->last_target_count_ != 0xFFFFFFFF) {
if (this->target_count_sensor_ != nullptr) {
this->target_count_sensor_->publish_state(NAN);
}
this->last_target_count_ = 0xFFFFFFFF;
}
#endif
if (this->target_presence_any_) {
this->target_presence_any_ = false;
#ifdef USE_BINARY_SENSOR
if (this->presence_binary_sensor_ != nullptr) {
this->presence_binary_sensor_->publish_state(this->target_presence_any_);
}
#endif
this->update_work_mode_fallback_();
}
}
void LD6002BComponent::set_switch_state(SwitchType type, bool state) {
switch (type) {
case SwitchType::LOW_POWER:
this->low_power_enabled_ = state;
this->low_power_reported_ = true;
this->send_control_command_(state ? CMD_LOW_POWER_ON : CMD_LOW_POWER_OFF);
this->update_work_mode_fallback_();
break;
case SwitchType::POINT_CLOUD:
this->point_cloud_enabled_ = state;
this->send_control_command_(state ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
#ifdef USE_SENSOR
// The count only moves while the stream runs, so the last one would stand as
// a live reading. The dedup sentinel is cleared with it: the same count is
// new again when the stream comes back.
if (!state && this->point_count_sensor_ != nullptr && this->last_point_count_ != 0xFFFFFFFF) {
this->point_count_sensor_->publish_state(NAN);
this->last_point_count_ = 0xFFFFFFFF;
}
#endif
break;
case SwitchType::TARGET_DISPLAY:
this->target_display_enabled_ = state;
this->send_control_command_(state ? CMD_TARGET_DISPLAY_ON : CMD_TARGET_DISPLAY_OFF);
if (!state) {
// Every target entity is fed by the reports this just stopped.
this->clear_target_state_();
}
break;
}
}
} // namespace esphome::ld6002b
+105
View File
@@ -11,16 +11,41 @@
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/core/preferences.h"
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <array>
#include <cmath>
namespace esphome::ld6002b {
static constexpr uint8_t MAX_TARGETS = 3;
static constexpr size_t DEFAULT_MAX_DATA_LEN = 1024;
static constexpr size_t DEFAULT_MAX_DATA_LEN_POINT_CLOUD = 4096;
// Largest protocol payload is TYPE_SET_AREA: int32 area id + 6 floats = 28 bytes.
static constexpr size_t CMD_MAX_DATA_LEN = 28;
enum class NumberType : uint8_t {
HOLD_DELAY,
Z_MIN,
Z_MAX,
LOW_POWER_SLEEP,
};
enum class SwitchType : uint8_t {
LOW_POWER,
POINT_CLOUD,
TARGET_DISPLAY,
};
#ifdef USE_SENSOR
struct TargetSensors {
sensor::Sensor *x{nullptr};
@@ -32,6 +57,10 @@ struct TargetSensors {
#endif
struct VersionPref {
char value[20];
};
class LD6002BComponent : public Component, public uart::UARTDevice {
public:
void setup() override;
@@ -45,6 +74,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; }
void set_point_count_sensor(sensor::Sensor *sensor) { this->point_count_sensor_ = sensor; }
void set_target_x_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
@@ -82,6 +112,27 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
}
#endif
#ifdef USE_TEXT_SENSOR
void set_work_mode_text_sensor(text_sensor::TextSensor *sensor) { this->work_mode_text_sensor_ = sensor; }
void set_ota_version_text_sensor(text_sensor::TextSensor *sensor) { this->ota_version_text_sensor_ = sensor; }
#endif
#ifdef USE_NUMBER
void set_hold_delay_number(number::Number *number) { this->hold_delay_number_ = number; }
void set_z_min_number(number::Number *number) { this->z_min_number_ = number; }
void set_z_max_number(number::Number *number) { this->z_max_number_ = number; }
void set_low_power_sleep_number(number::Number *number) { this->low_power_sleep_number_ = number; }
#endif
#ifdef USE_SWITCH
void set_low_power_switch(switch_::Switch *sw) { this->low_power_switch_ = sw; }
void set_point_cloud_switch(switch_::Switch *sw) { this->point_cloud_switch_ = sw; }
void set_target_display_switch(switch_::Switch *sw) { this->target_display_switch_ = sw; }
#endif
void set_number_value(NumberType type, float value);
void set_switch_state(SwitchType type, bool state);
protected:
enum class ParseState : uint8_t { SOF, HEADER, HCK, DATA, DCK, DISCARD };
@@ -95,6 +146,25 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
void reset_parser_();
void handle_frame_(uint16_t type, const uint8_t *data, uint16_t len);
void handle_target_report_(const uint8_t *data, uint16_t len);
void handle_point_cloud_(const uint8_t *data, uint16_t len);
void handle_delay_report_(const uint8_t *data, uint16_t len);
void handle_z_range_report_(const uint8_t *data, uint16_t len);
void handle_low_power_report_(const uint8_t *data, uint16_t len);
void handle_low_power_sleep_report_(const uint8_t *data, uint16_t len);
void handle_work_mode_report_(const uint8_t *data, uint16_t len);
void handle_version_report_(const uint8_t *data, uint16_t len);
void update_work_mode_fallback_();
void publish_work_mode_(bool low_power);
// Drops every target-derived reading and the slot table they are indexed by.
void clear_target_state_();
#ifdef USE_SENSOR
void clear_target_slot_(uint8_t index);
#endif
#ifdef USE_NUMBER
void publish_number_clamped_(number::Number *number, float value);
#endif
void init_version_pref_();
void save_version_pref_(const char *value);
void queue_command_(uint16_t type, const uint8_t *data, uint8_t len);
void process_command_queue_();
@@ -102,21 +172,41 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
void send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
void send_control_command_(uint32_t command);
void send_z_range_();
static uint16_t read_u16_be(const uint8_t *data);
static uint32_t read_u32_le(const uint8_t *data);
static int32_t read_int32_le(const uint8_t *data);
static float read_f32_le(const uint8_t *data);
static void write_u32_le(uint8_t *data, uint32_t value);
static void write_f32_le(uint8_t *data, float value);
#ifdef USE_SENSOR
std::array<TargetSensors, MAX_TARGETS> targets_{};
sensor::Sensor *target_count_sensor_{nullptr};
sensor::Sensor *point_count_sensor_{nullptr};
#endif
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *presence_binary_sensor_{nullptr};
std::array<binary_sensor::BinarySensor *, MAX_TARGETS> target_presence_{};
#endif
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *work_mode_text_sensor_{nullptr};
text_sensor::TextSensor *ota_version_text_sensor_{nullptr};
ESPPreferenceObject version_pref_{};
bool version_pref_initialized_{false};
#endif
#ifdef USE_NUMBER
number::Number *hold_delay_number_{nullptr};
number::Number *z_min_number_{nullptr};
number::Number *z_max_number_{nullptr};
number::Number *low_power_sleep_number_{nullptr};
#endif
#ifdef USE_SWITCH
switch_::Switch *low_power_switch_{nullptr};
switch_::Switch *point_cloud_switch_{nullptr};
switch_::Switch *target_display_switch_{nullptr};
#endif
GPIOPin *wakeup_pin_{nullptr};
uint32_t wakeup_pulse_ms_{50};
@@ -132,6 +222,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
uint8_t data_xor_{0};
uint32_t discard_remaining_{0};
bool frame_oversize_{false};
size_t max_data_len_{0};
uint8_t *data_buf_{nullptr};
uint16_t next_frame_id_{0};
@@ -173,11 +264,24 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
// Bumped whenever the active command changes, so a deferred send can tell it was retired.
uint8_t send_generation_{0};
float z_min_{NAN};
float z_max_{NAN};
// Which person owns each target_N slot, so a slot survives the module re-sorting its array.
std::array<int32_t, MAX_TARGETS> slot_cluster_{};
std::array<bool, MAX_TARGETS> slot_occupied_{};
bool target_presence_any_{false};
// What the switches and setup asked the module for, which is not the same as
// what it is doing yet: a stream keeps sending until it acts on the command.
// The report handlers read these and drop anything a stopped stream still emits.
bool target_display_enabled_{false};
bool point_cloud_enabled_{false};
bool work_mode_reported_{false};
bool low_power_enabled_{false};
bool low_power_reported_{false};
bool last_work_mode_valid_{false};
bool last_work_mode_low_power_{false};
#ifdef USE_SENSOR
std::array<bool, MAX_TARGETS> last_target_presence_{}; // one-shot NAN clear for target sensors
@@ -185,6 +289,7 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
std::array<int32_t, MAX_TARGETS> last_cluster_id_{};
std::array<bool, MAX_TARGETS> last_cluster_id_valid_{};
uint32_t last_target_count_{0xFFFFFFFF};
uint32_t last_point_count_{0xFFFFFFFF};
#endif
};
@@ -0,0 +1,82 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_DURATION,
ENTITY_CATEGORY_CONFIG,
UNIT_METER,
UNIT_MILLISECOND,
UNIT_SECOND,
)
from .. import LD6002BComponent, ld6002b_ns
from ..const import (
CONF_HOLD_DELAY,
CONF_LD6002B_ID,
CONF_LOW_POWER_SLEEP_TIME,
CONF_Z_MAX,
CONF_Z_MIN,
)
DEPENDENCIES = ["ld6002b"]
LD6002BNumber = ld6002b_ns.class_("LD6002BNumber", number.Number)
NumberType = ld6002b_ns.enum("NumberType", is_class=True)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_HOLD_DELAY): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_SECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MAX): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_LOW_POWER_SLEEP_TIME): number.number_schema(
LD6002BNumber,
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_LD6002B_ID])
for key, number_type, setter, min_value, max_value, step in (
(CONF_HOLD_DELAY, NumberType.HOLD_DELAY, "set_hold_delay_number", 0, 65535, 1),
(CONF_Z_MIN, NumberType.Z_MIN, "set_z_min_number", -10, 10, 0.1),
(CONF_Z_MAX, NumberType.Z_MAX, "set_z_max_number", -10, 10, 0.1),
# 0x0205 carries a uint32 of milliseconds; the vendor documents 500 ms as
# the default and no upper bound, so the range ends at a minute rather
# than at a default the module is free to be sleeping past.
(
CONF_LOW_POWER_SLEEP_TIME,
NumberType.LOW_POWER_SLEEP,
"set_low_power_sleep_number",
0,
60000,
100,
),
):
if conf := config.get(key):
n = await number.new_number(
conf, number_type, min_value=min_value, max_value=max_value, step=step
)
await cg.register_parented(n, config[CONF_LD6002B_ID])
cg.add(getattr(hub, setter)(n))
@@ -0,0 +1,10 @@
#include "ld6002b_number.h"
namespace esphome::ld6002b {
void LD6002BNumber::control(float value) {
this->publish_state(value);
this->parent_->set_number_value(this->type_, value);
}
} // namespace esphome::ld6002b
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld6002b.h"
namespace esphome::ld6002b {
class LD6002BNumber : public number::Number, public Parented<LD6002BComponent> {
public:
explicit LD6002BNumber(NumberType type) : type_(type) {}
protected:
void control(float value) override;
NumberType type_;
};
} // namespace esphome::ld6002b
+9
View File
@@ -15,6 +15,7 @@ from .const import (
CONF_CLUSTER_ID,
CONF_DOPPLER_INDEX,
CONF_LD6002B_ID,
CONF_POINT_COUNT,
CONF_Z,
MAX_TARGETS,
)
@@ -75,6 +76,10 @@ CONFIG_SCHEMA = cv.Schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POINT_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)})
@@ -86,6 +91,10 @@ async def to_code(config):
sens = await sensor.new_sensor(target_count_config)
cg.add(hub.set_target_count_sensor(sens))
if point_count_config := config.get(CONF_POINT_COUNT):
sens = await sensor.new_sensor(point_count_config)
cg.add(hub.set_point_count_sensor(sens))
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
if x_config := target_config.get(CONF_X):
@@ -0,0 +1,60 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG
from .. import LD6002BComponent, ld6002b_ns
from ..const import (
CONF_LD6002B_ID,
CONF_LOW_POWER,
CONF_POINT_CLOUD,
CONF_TARGET_DISPLAY,
)
DEPENDENCIES = ["ld6002b"]
LD6002BSwitch = ld6002b_ns.class_("LD6002BSwitch", switch.Switch)
SwitchType = ld6002b_ns.enum("SwitchType", is_class=True)
# None of these three carry an inversion. They name what the module is doing, not
# how something is wired to it, so an inverted one would only report the opposite
# of the truth -- and the boot restore, which applies a state nothing reports back,
# is where that would be hardest to spot.
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_LOW_POWER): switch.switch_schema(
LD6002BSwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_POINT_CLOUD): switch.switch_schema(
LD6002BSwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_TARGET_DISPLAY): switch.switch_schema(
LD6002BSwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="RESTORE_DEFAULT_ON",
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_LD6002B_ID])
for key, switch_type, setter in (
(CONF_LOW_POWER, SwitchType.LOW_POWER, "set_low_power_switch"),
(CONF_POINT_CLOUD, SwitchType.POINT_CLOUD, "set_point_cloud_switch"),
(CONF_TARGET_DISPLAY, SwitchType.TARGET_DISPLAY, "set_target_display_switch"),
):
if conf := config.get(key):
s = await switch.new_switch(conf, switch_type)
await cg.register_parented(s, config[CONF_LD6002B_ID])
cg.add(getattr(hub, setter)(s))
@@ -0,0 +1,10 @@
#include "ld6002b_switch.h"
namespace esphome::ld6002b {
void LD6002BSwitch::write_state(bool state) {
this->parent_->set_switch_state(this->type_, state);
this->publish_state(state);
}
} // namespace esphome::ld6002b
@@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld6002b.h"
namespace esphome::ld6002b {
class LD6002BSwitch : public switch_::Switch, public Parented<LD6002BComponent> {
public:
explicit LD6002BSwitch(SwitchType type) : type_(type) {}
protected:
void write_state(bool state) override;
SwitchType type_;
};
} // namespace esphome::ld6002b
+31
View File
@@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC
from . import LD6002BComponent
from .const import CONF_LD6002B_ID, CONF_OTA_VERSION, CONF_WORK_MODE
DEPENDENCIES = ["ld6002b"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_WORK_MODE): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_OTA_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_LD6002B_ID])
if work_mode_config := config.get(CONF_WORK_MODE):
sens = await text_sensor.new_text_sensor(work_mode_config)
cg.add(hub.set_work_mode_text_sensor(sens))
if ota_config := config.get(CONF_OTA_VERSION):
sens = await text_sensor.new_text_sensor(ota_config)
cg.add(hub.set_ota_version_text_sensor(sens))
+32
View File
@@ -7,6 +7,8 @@ sensor:
ld6002b_id: ld6002b_radar
target_count:
name: Target Count
point_count:
name: Point Count
target_1:
x:
name: Target-1 X
@@ -48,3 +50,33 @@ binary_sensor:
name: Presence
target_1:
name: Target-1 Presence
text_sensor:
- platform: ld6002b
ld6002b_id: ld6002b_radar
work_mode:
name: Work Mode
ota_version:
name: OTA Version
number:
- platform: ld6002b
ld6002b_id: ld6002b_radar
hold_delay:
name: Hold Delay
z_min:
name: Z Min
z_max:
name: Z Max
low_power_sleep_time:
name: Low Power Sleep
switch:
- platform: ld6002b
ld6002b_id: ld6002b_radar
low_power:
name: Low Power
point_cloud:
name: Point Cloud
target_display:
name: Target Display