8000
Skip to content

[ld6002b] Add target sensors (2/5) - #17820

Merged
swoboda1337 merged 13 commits into
esphome:devfrom
hepter:ld6002b-p2
Aug 6, 2026
Merged

[ld6002b] Add target sensors (2/5)#17820
swoboda1337 merged 13 commits into
esphome:devfrom
hepter:ld6002b-p2

Conversation

@hepter
@hepter hepter commented Jul 23, 2026
Copy link
Copy Markdown
Contributor

What does this implement/fix?

Part 2 of 5 of the HLK-LD6002B 60GHz presence radar component (split from #13532). Depends on #17819.

Adds the sensor platform on top of the hub:

  • per-target x / y / z coordinates, doppler index and cluster id (up to 3 targets)
  • target_count
  • default timeout + throttle_with_priority filters on the value sensors, matching ld2450 (NaN transitions publish immediately)

Stacked on #17819 — the diff includes its commits until it merges. Reviewable scope: the last commit.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • New developer-facing feature (adds functionality for component developers; no end-user configuration change)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Developer breaking change (an API change that could break external components)
  • Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on)
  • Code quality improvements to existing code or addition of tests
  • Other

Pull request in esphome.io with documentation (if applicable):

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040/RP2350
  • BK72xx
  • RTL87xx
  • LN882x
  • nRF52840

Example entry for config.yaml:

ld6002b:
  id: ld6002b_radar

sensor:
  - platform: ld6002b
    ld6002b_id: ld6002b_radar
    target_count:
      name: Target Count
    target_1:
      x:
        name: Target-1 X
      y:
        name: Target-1 Y
      z:
        name: Target-1 Z

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

@esphome
esphome Bot commented Jul 23, 2026
Copy link
Copy Markdown
Contributor

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://pr#17820
    components: [ld6002b]
    refresh: 1h

(Added by the PR bot)

esphome[bot]
esphome Bot previously requested changes Jul 23, 2026
@esphome esphome Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📦 Pull Request Size

Hey @hepter, thanks for the contribution! Just a heads up, this PR is on the large side (1104 line changes excluding tests), which makes it harder for maintainers to review.

Smaller, focused PRs tend to be reviewed much faster since they fit into the short gaps between other maintainer work; large ones often have to wait for a rare long uninterrupted block of time. If you can break this up into smaller pieces that can be reviewed independently, it will almost certainly land faster overall.

Before putting more time in, it's also worth popping into #devs on Discord so we can help you scope things and flag anything already in flight.

For more details (including how to split the work up), see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions

@codecov
codecov Bot commented Jul 23, 2026
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.28%. Comparing base (ba86685) to head (371970d).
⚠️ Report is 2 commits behind head on dev.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##              dev   #17820   +/-   ##
=======================================
  Coverage   87.28%   87.28%           
=======================================
  Files          64       64           
  Lines       14697    14697           
  Branches     2217     2217           
=======================================
  Hits        12828    12828           
  Misses       1560     1560           
  Partials      309      309           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Protocol (per the HLK-LD6002B communication protocol V1.2):
- Rework command ACK correlation. The module acknowledges with a frame
  carrying its own counter (frame IDs are per-peer and every message
  type is unidirectional), so matching our TX id could never succeed.
  ACKs are matched by type in transmission order: an ACK arriving while
  its command is still active completes it, and leftover ACKs from
  superseded retry attempts are absorbed so they cannot complete the
  next command.
- Give the first attempt a wider ACK budget (600 ms): the first frame
  sent to a sleeping module doubles as its wake-up and is consumed
  unanswered, so the wake must not cascade into extra retries.
- Verify the header checksum before acting on the length field when
  discarding oversized frames; a corrupted length byte now costs 8
  bytes of resync instead of up to 65537.

Reliability and RAM:
- Allocate the RX buffer once in setup() via RAMAllocator with a real
  failure check.
- Stop heap-allocating per command send in the wake-up path; stage
  untracked payloads in a fixed scratch buffer.
- Right-size command slots to the largest protocol payload (28 bytes)
  and document why the queue depth stays at 16.
- Drop manual publish de-duplication (binary_sensor already does it),
  clamp the wire-reported target count, and bounds-check indexed
  setters.

Config and logging:
- Rename wakeup_pulse_ms to wakeup_pulse and require wakeup_pin when
  auto_wake or wakeup_pulse is configured.
- Move max_data_len (and helpers only used by later parts) into the
  parts that use them; cap max_data_len at 8192.
- Cap low_power_sleep_time at 500 ms per the datasheet recommendation.
- Remove the boot log line; expand dump_config with the wake pin,
  pulse, resolved buffer size and per-target sensors.
- Log retries for non-control command types too.

Tests:
- Include common.yaml via dict-style packages in the test fixtures.

All of the above validated on real hardware (ESP32 intercom + LD6002B):
full settings matrix, area write/readback flows, reboot soak with the
boot sequence sending each command exactly once, and zero component
warnings or errors in the captured logs.
Move the DISCARD decrement out of the condition and apply De Morgan
to the stale-ACK guard. Behaviour unchanged.
@esphome
esphome Bot dismissed their stale review August 6, 2026 02:20

Review dismissed: All requirements have been met

@swoboda1337
swoboda1337 requested a lite review from Copilot August 6, 2026 02:25
@swoboda1337
Copy link
Copy Markdown
Member

@esphbot review

@github-actions
github-actions Bot commented Aug 6, 2026
Copy link
8BD0 Copy Markdown
Contributor

Memory Impact Analysis

Components: ld6002b
Platform: esp8266-ard

Metric Target Branch This PR Change
RAM 30,268 bytes 31,940 bytes 📈 🚨 +1,672 bytes (+5.52%)
Flash 274,627 bytes 279,751 bytes 📈 🚨 +5,124 bytes (+1.87%)
📊 Component Memory Breakdown
Component Target Flash PR Flash Change
[esphome]sensor 0 bytes 1,883 bytes 📈 🔸 +1,883 bytes (0.00%)
[esphome]core 8,102 bytes 9,919 bytes 📈 🚨 +1,817 bytes (+22.43%)
[esphome]ld6002b 3,538 bytes 4,129 bytes 📈 🚨 +591 bytes (+16.70%)
wifi_stack 40,648 bytes 40,308 bytes 📉 -340 bytes (-0.84%)
power_mgmt 6,800 bytes 7,140 bytes 📈 +340 bytes (+5.00%)
exception_handling 2,488 bytes 2,531 bytes 📈 +43 bytes (+1.73%)
[esphome]binary_sensor 278 bytes 312 bytes 📈 🚨 +34 bytes (+12.23%)
cpp_runtime 53 bytes 29 bytes 📉 -24 bytes (-45.28%)
wifi_phy_pp 2,926 bytes 2,948 bytes 📈 +22 bytes (+0.75%)
esp_system 5,605 bytes 5,583 bytes 📉 -22 bytes (-0.39%)
freertos 35 bytes 41 bytes 📈 +6 bytes (+17.14%)
other 7,118 bytes 7,112 bytes 📉 -6 bytes (-0.08%)
🔍 Symbol-Level Changes (click to expand)

Changed Symbols

Symbol Target Size PR Size Change
setup 608 bytes 1,943 bytes 📈 +1,335 bytes (+219.57%)
esphome::ld6002b::LD6002BComponent::handle_target_report_(unsigned char const*, unsigned short) 396 bytes 760 bytes 📈 +364 bytes (+91.92%)
esphome::App 132 bytes 248 bytes 📈 +116 bytes (+87.88%)
esphome::ld6002b::LD6002BComponent::dump_config() 131 bytes 227 bytes 📈 +96 bytes (+73.28%)
ld6002b__ld6002b_radar__pstorage 688 bytes 776 bytes 📈 +88 bytes (+12.79%)
std::_Function_handler<void (), esphome::ld6002b::LD6002BComponent::setup()::{lambda()#1}>::_M_in...std::_Function_handler<void (), esphome::ld6002b::LD6002BComponent::setup()::{lambda()#1}>::_M_invoke(std::_Any_data const&)
55 bytes 102 bytes 📈 +47 bytes (+85.45%)
esphome::entity_device_class_lookup(unsigned char) 24 bytes 32 bytes 📈 +8 bytes (+33.33%)
esphome::EntityBase::configure_entity_(char const*, unsigned int, unsigned int) 168 bytes 174 bytes 📈 +6 bytes (+3.57%)
esphome::ld6002b::LD6002BComponent::process_command_queue_() 424 bytes 428 bytes 📈 +4 bytes (+0.94%)
esphome::ENTITY_DC_TABLE 4 bytes 8 bytes 📈 +4 bytes (+100.00%)
esphome::ld6002b::LD6002BComponent::send_command_internal_(unsigned short, unsigned char const*, ...esphome::ld6002b::LD6002BComponent::send_command_internal_(unsigned short, unsigned char const*, unsigned char, bool)
261 bytes 257 bytes 📉 -4 bytes (-1.53%)
esphome::SchedulerNameLog::format(esphome::Scheduler::NameType, char const*, unsigned int)::__pstr__ 8 bytes 12 bytes 📈 +4 bytes (+50.00%)
esphome::ld6002b::LD6002BComponent::queue_command_(unsigned short, unsigned char const*, unsigned...esphome::ld6002b::LD6002BComponent::queue_command_(unsigned short, unsigned char const*, unsigned char)
140 bytes 144 bytes 📈 +4 bytes (+2.86%)
esphome::ld6002b::LD6002BComponent::parse_byte_(unsigned char) 429 bytes 433 bytes 📈 +4 bytes (+0.93%)
esphome::COMP_SRC_TABLE 16 bytes 20 bytes 📈 +4 bytes (+25.00%)
esphome::ld6002b::LD6002BComponent::loop() 76 bytes 73 bytes 📉 -3 bytes (-3.95%)
esphome::Application::register_component_impl_(esphome::Component*, bool) 35 bytes 38 bytes 📈 +3 bytes (+8.57%)
esphome::ld6002b::LD6002BComponent::handle_frame_(unsigned short, unsigned char const*, unsigned ...esphome::ld6002b::LD6002BComponent::handle_frame_(unsigned short, unsigned char const*, unsigned short)
283 bytes 281 bytes 📉 -2 bytes (-0.71%)
esphome::ENTITY_DC_TABLE_STR_0 10 bytes 9 bytes 📉 -1 bytes (-10.00%)
std::Function_handler<void (), esphome::ld6002b::LD6002BComponent::send_command_internal(unsign...std::Function_handler<void (), esphome::ld6002b::LD6002BComponent::send_command_internal(unsigned short, unsigned char const*, unsigned char, bool)::{lambda()#1}>::_M_manager(std::_Any_data&, std::Function_handler<void (), esphome::ld6002b::LD6002BComponent::send_command_internal(unsigned short, unsigned char const*, unsigned char, bool)::{lambda()#1}> const&, std::_Manager_operation)
81 bytes 82 bytes 📈 +1 bytes (+1.23%)
esphome::ld6002b::LD6002BComponent::reset_parser_() 28 bytes 29 bytes 📈 +1 bytes (+3.57%)

New Symbols (top 15)

Symbol Size
pinMode 711 bytes
esphome::sensor::log_sensor(char const*, char const*, char const*, esphome::sensor::Sensor*) 174 bytes
fpm_close 133 bytes
fpm_open 127 bytes
esphome::sensor::Filter::output(float) 113 bytes
_vsniprintf_r 112 bytes
esphome::sensor::Sensor::internal_send_state_to_frontend(float) 111 bytes
esphome::sensor::Filter::input(float) 91 bytes
__esp_delay 91 bytes
esphome::sensor::Sensor::publish_state(float) 88 bytes
esphome::Component::disable_loop() 83 bytes
esphome::sensor::Sensor::add_filter(esphome::sensor::Filter*) 82 bytes
esphome::sensor::ThrottleWithPriorityNanFilter::new_value(float) 81 bytes
vtable for esphome::sensor::TimeoutFilterLast 76 bytes
vtable for esphome::sensor::TimeoutFilterBase 76 bytes
98 more new symbols... Total: 4,997 bytes

Removed Symbols (top 15)

Symbol Size
__pinMode 711 bytes
wifi_fpm_close 133 bytes
wifi_fpm_open 127 bytes
_vsnprintf_r 112 bytes
esp_delay 91 bytes
wifi_fpm_do_wakeup 73 bytes
digitalRead 54 bytes
__preloop_update_frequency 38 bytes
vsnprintf 32 bytes
system_soft_wdt_feed 22 bytes
__loop_end 19 bytes
CSWTCH$63 16 bytes
attachInterruptArg 11 bytes
wifi_fpm_set_wakeup_cb 7 bytes
z2EapFree 6 bytes
3 more removed symbols... Total: 1,460 bytes

Note: This analysis measures static RAM and Flash usage only (compile-time allocation).
Dynamic memory (heap) cannot be measured automatically.
⚠️ You must test this PR on a real device to measure free heap and ensure no runtime memory issues.

This analysis runs automatically when components change. Memory usage is measured from a representative test configuration.

@swoboda1337
swoboda1337 marked this pull request as ready for review August 6, 2026 02:26
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the sensor platform and supporting hub plumbing for the new ld6002b UART presence radar component, enabling per-target coordinate-related sensors plus target/point counts and point-cloud stream handling.

Changes:

  • Add ld6002b sensor platform schema/codegen for target sensors (x/y/z/doppler/cluster) and target_count / point_count.
  • Extend the hub C++ implementation to publish the new sensors, handle point-cloud report frames, and adjust UART frame buffer sizing based on point-cloud usage.
  • Update component test YAML to exercise the new sensors.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/components/ld6002b/common.yaml Adds sensor platform configuration to the component’s test setup.
esphome/components/ld6002b/sensor.py New sensor platform schema and codegen wiring to the hub setters.
esphome/components/ld6002b/ld6002b.h Adds sensor pointers/state tracking, throttling fields, and point-cloud handler declarations.
esphome/components/ld6002b/ld6002b.cpp Implements publish de-dup/throttling, buffer sizing, target sensor publishes, and point-cloud count handling.
esphome/components/ld6002b/const.py Adds component-local config keys used by the new sensor platform and hub options.
esphome/components/ld6002b/init.py Adds max_data_len and throttle hub options and passes them into the C++ component.

Comment thread esphome/components/ld6002b/__init__.py Outdated
Comment on lines 45 to 49
cv.Optional(CONF_MAX_DATA_LEN): cv.int_range(min=256, max=8192),
cv.Optional(
CONF_THROTTLE, default="1000ms"
): cv.positive_time_period_milliseconds,
}
Comment thread esphome/components/ld6002b/ld6002b.cpp Outdated
Comment on lines +319 to +323
if (this->should_throttle_stream_(this->last_point_publish_)) {
return;
}
this->handle_point_cloud_(data, len);
break;
@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@esphbot esphbot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Important issues found.

  • Hub-level throttle re-implements sensor filters the project removed from ld2450
  • Configuring point_count turns on a full point-cloud stream (4 KB frames on a 115200 link) to publish one integer

@hepter hepter changed the title [ld6002b] Add target and point cloud sensors (2/5) [ld6002b] Add target sensors (2/5) Aug 6, 2026
@hepter
hepter commented Aug 6, 2026
Copy link
Copy Markdown
Contributor Author

Reworked, throttle is gone in favor of the ld2450 style filter defaults and point cloud moved to part 3 behind its switch. Kept KEY_TARGET_COUNT as is, the CONF_ rename trips the repo lint since ld2450 and rd03d already define the same constant, moving all three to the shared const file is a separate PR.

@hepter
hepter requested a review from esphbot August 6, 2026 07:40
@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

@hepter: Reworked, throttle is gone in favor of the ld2450 style filter defaults and point cloud moved to par...

Rework confirmed. Checked filter.cpp:270-275ThrottleWithPriorityNanFilter passes NaN immediately, so one-shot NAN clear still bypasses throttle. TimeoutFilterLast self-disables, no NAN loop.

On KEY_TARGET_COUNT: your reasoning checks out. script/ci-custom.py:544 errors at 3+ definitions; ld2450/sensor.py:24 and rd03d/sensor.py:22 already hold 2. Adding to esphome/components/const/__init__.py would also count — regex includes *.py, excludes only esphome/const.py. So a prep PR moving all three remains the only clean path. Not re-raising.

Remaining items all non-blocking. One worth doing before release: rename dop_idx to doppler_index — public key, deprecation cycle later. Also update the PR body, still documents throttle: 1s.

@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@esphbot esphbot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

No blocking issues found — ready to merge.

@swoboda1337
Copy link
Copy Markdown
Member

Reworked, throttle is gone in favor of the ld2450 style filter defaults and point cloud moved to part 3 behind its switch. Kept KEY_TARGET_COUNT as is, the CONF_ rename trips the repo lint since ld2450 and rd03d already define the same constant, moving all three to the shared const file is a separate PR.

Lets create that PR then.

@swoboda1337
Copy link
Copy Markdown
Member

@esphbot review

@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

@swoboda1337: > Reworked, throttle is gone in favor of the ld2450 style filter defaults and point cloud moved to p...

Done, landed already. #18120 moved CONF_TARGET_COUNT into esphome/components/const/__init__.py (commit ba86685361, now on dev). sensor.py here imports from there, so no third definition and no lint hit.

One housekeeping note: part 1 (#17819) also merged, so the "stacked on #17819" line in the body reads stale now. A rebase onto dev drops the carried commits and the size-bot warning with them.

@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

PR Review — [ld6002b] Add target sensors (2/5)

Merge-ready. Every finding from the previous round is fixed, and the new code holds up under a fresh trace of the frame path.

What is genuinely well done here:

  • The USE_SENSOR guards are now correct in both directions. The frame decode (offset, x/y/z/dop_idx, cluster_id) moved inside the guard, the four de-dup members in ld6002b.h are wrapped, and the last_target_presence_[i] = has_target write moved with them — so a binary_sensor:-only build no longer pays flash, RAM, or warnings for sensors it does not have.
  • slot_wire[] indexing is sound. I re-derived it: the clear-unseen loop runs between the cluster-match loop and the new-slot assign loop, so every slot left occupied has slot_wire[s] written this frame, and slot_wire[i] < count <= available keeps 4 + slot_wire[i] * 20 + 20 inside the length-validated payload. No stale index, no out-of-bounds read.
  • The timeout + throttle_with_priority chain does what the description claims. ThrottleWithPriorityNanFilter (filter.cpp:270-277) passes NaN unthrottled, so the one-shot NAN on target loss reaches Home Assistant immediately; TimeoutFilterLast (filter.cpp:321-346) disables its own loop after firing, so an absent target is not re-NAN'd forever.
  • Slot handover is right: a slot whose occupant changes from cluster 5 to cluster 9 publishes 9 with no intervening NAN, and last_cluster_id_valid_ is cleared on slot release so a repeated id after a gap still publishes.
  • read_f32_le / read_int32_le go through std::memcpy rather than a type-punned cast.
  • CONF_TARGET_COUNT now imports from esphome/components/const/__init__.py — the follow-up PR @swoboda1337 asked for landed as [const] Move CONF_TARGET_COUNT to components/const #18120, so the lint objection is gone rather than worked around.
  • Sharing one _VALUE_SENSOR_FILTERS list across four sensor_schema() calls is safe: validate_registry_entry copies before it mutates (config_validation.py:2214), and GenerateID uses a callable default, so reusing the single TARGET_SCHEMA object for all three targets also cannot alias IDs.

Remaining points, all non-blocking:

  • The per-target publish/clear block is 54 lines of near-repetition and leaves an if/else with two empty bodies in non-sensor builds — two small helpers plus a braced-init loop for the NAN clears would cut it to three lines.
  • last_cluster_id_ / last_cluster_id_valid_ / last_target_count_{0xFFFFFFFF} re-implement Deduplicator<T> (core/helpers.h:1900), which would drop the sentinel — at a cost of roughly 13 bytes per instance, so it is a judgement call, not a defect.
  • The C++ side still says dop_idx after the config key became doppler_index; worth aligning while the setters are still un-released.
  • The comment explaining why CMD_POINT_CLOUD_OFF is sent unconditionally was deleted along with the point-cloud code, leaving a bare side-effecting command.
  • Housekeeping: part 1 ([ld6002b] Add LD6002B 60GHz presence radar (1/5) #17819) is now on dev, so the "Stacked on [ld6002b] Add LD6002B 60GHz presence radar (1/5) #17819 — the diff includes its commits" note in the PR body is stale and a rebase would also retire the size-bot warning.

✅ Resolved since last review (4)

Previously-flagged issues verified fixed
  • esphome/components/ld6002b/ld6002b.cpp:339 Frame decode runs outside #ifdef USE_SENSOR, producing unused-variable warnings in binary_sensor-only builds
  • esphome/components/ld6002b/sensor.py:30 The nine-line filter list is repeated verbatim four times
  • esphome/components/ld6002b/const.py:3 dop_idx is a cryptic name for a public config key
  • esphome/components/ld6002b/ld6002b.cpp:128 dump_config logs only target_count, not the 15 per-target sensors

🟢 Suggestions

1. Per-target publish/clear block is 54 lines of repetition with an empty if/else in non-sensor builds
esphome/components/ld6002b/ld6002b.cpp:344-397

The correctness of this block checks out — I re-traced slot_wire[] on the post-diff code and every slot with slot_occupied_[i] == true has had slot_wire[s] written this frame (matched slots in the cluster-match loop at 261-269, new slots in the assign loop at 275-286, and the clear loop at 270-274 runs between them), so 4 + slot_wire[i] * 20 + 20 <= len always holds. The five-way NAN clear and the last_cluster_id_valid_ reset are also correct across slot handover (cluster 5 → cluster 9 in the same slot publishes 9 without an intervening NAN, which is what you want).

What's awkward is the shape. if (has_target) { #ifdef USE_SENSOR ... #endif } else { #ifdef USE_SENSOR ... #endif } leaves an if (x) {} else {} with two empty bodies in a binary_sensor:-only build. It compiles away, but a reader has to scan 54 lines to work out that the whole branch exists only for sensors, and the two TargetSensors &target declarations in sibling scopes read as a mistake at first glance.

Why it matters: this is the hot path (a frame every ~100 ms) and the one place a future part (zones, point cloud) will have to edit. Two named helpers make the USE_SENSOR dependency obvious and shrink the loop body to three lines.

Suggested shape:

  for (uint8_t i = 0; i < MAX_TARGETS; i++) {
    const bool has_target = this->slot_occupied_[i];
#ifdef USE_SENSOR
    if (has_target) {
      this->publish_target_sensors_(i, data + 4 + (slot_wire[i] * TARGET_DATA_LEN));
    } else if (this->last_target_presence_[i]) {
      this->clear_target_sensors_(i);
    }
    this->last_target_presence_[i] = has_target;
#endif
#ifdef USE_BINARY_SENSOR
    ...
#endif
  }

And inside clear_target_sensors_, the five identical NAN publishes collapse to one loop:

  for (auto *s : {target.x, target.y, target.z, target.dop_idx, target.cluster_id}) {
    if (s != nullptr)
      s->publish_state(NAN);
  }
    bool has_target = this->slot_occupied_[i];
    if (has_target) {
#ifdef USE_SENSOR
      uint16_t offset = 4 + (slot_wire[i] * TARGET_DATA_LEN);
2. Hand-rolled de-dup state re-implements `Deduplicator` from core helpers
esphome/components/ld6002b/ld6002b.h:182-186

last_cluster_id_ + last_cluster_id_valid_ and last_target_count_{0xFFFFFFFF} reproduce what Deduplicator<T> in esphome/core/helpers.h:1900 already provides: next(value) returns false on a repeat, next_unknown() returns true only on the first transition to unknown, and has_value_ removes the need for a sentinel. The sibling radar family wraps exactly this in esphome/components/ld24xx/ld24xx.h:66 (SensorWithDedup<T>, used by ld2450).

Why it matters, mildly: the 0xFFFFFFFF sentinel is the kind of thing that is only safe because of a bound established elsewhere. It happens to hold — reported = min(target_num, available) and available = (len - 4) / 20 with len a uint16_t, so reported <= 3276 — but nothing at the declaration says so, and a future part that feeds a different count through the same member would break it silently. Deduplicator has no such coupling.

Honest tradeoff, since this repo weighs RAM: Deduplicator<int32_t> is 4 + 2 bools padded to 8 bytes, so three of them cost 24 bytes against the 15 you spend now (12 + 3), and Deduplicator<uint32_t> for the count costs 8 against 4. Roughly +13 bytes per instance with MULTI_CONF = True. Your current layout — one presence bool per target covering all five sensors — is genuinely tighter. So this is a judgement call, not a defect: take it if you value the shared helper and losing the sentinel, skip it if the bytes matter more.

  std::array<bool, MAX_TARGETS> last_target_presence_{};  // one-shot NAN clear for target sensors
  // A cluster id names a person, so like the counts it is published on change, not per frame.
  std::array<int32_t, MAX_TARGETS> last_cluster_id_{};
  std::array<bool, MAX_TARGETS> last_cluster_id_valid_{};
  uint32_t last_target_count_{0xFFFFFFFF};
3. C++ still says `dop_idx` after the config key became `doppler_index`
esphome/components/ld6002b/ld6002b.h:64-68

The user-facing rename landed — const.py now has CONF_DOPPLER_INDEX = "doppler_index", which was the important half. But the C++ side kept the old spelling: set_target_dop_idx_sensor(), TargetSensors::dop_idx, the dop_idx local in handle_target_report_, and TARGET_DATA_LEN's comment.

Why it matters: a user reading doppler_index: in their YAML and then grepping the component source for it finds nothing. The setters are config plumbing, so per AGENTS.md they are not public API and renaming them costs nothing now, whereas after release someone's lambda may reference the field.

Suggested fix: set_target_doppler_index_sensor() / TargetSensors::doppler_index. Purely mechanical — five call sites in sensor.py and ld6002b.cpp.

  void set_target_dop_idx_sensor(uint8_t target, sensor::Sensor *sensor) {
    if (target >= MAX_TARGETS)
      return;
    this->targets_[target].dop_idx = sensor;
  }
4. Dropped the comment that explained the unconditional `CMD_POINT_CLOUD_OFF`
esphome/components/ld6002b/ld6002b.cpp:115

The diff removes // Point-cloud streaming is introduced in a later part; make sure it is off. but keeps the command, leaving a bare unconditional write with nothing to explain why a feature this component does not implement gets turned off at every boot.

Why it matters: that line is the only defence against a module left in point-cloud mode by earlier firmware or another integration flooding the 115200 link. Someone reading setup() in part 3 or 4 has no way to tell whether it is load-bearing or leftover, and an unexplained side-effecting command is exactly what gets "cleaned up" in a later refactor.

The comment is still accurate — point cloud is part 3 per the PR body — so the simplest fix is to keep it, or reword it to state the reason rather than the roadmap, e.g. // The module may still be streaming point clouds from a previous session; that saturates the link.

    this->send_control_command_(CMD_POINT_CLOUD_OFF);

Checklist

  • No out-of-bounds reads in the frame decode path (slot_wire indexing)
  • Input validation at the protocol boundary (frame length vs. claimed target count)
  • Conditional compilation guards correct (USE_SENSOR / USE_BINARY_SENSOR)
  • Publish/NAN-clear semantics correct across target loss and slot handover
  • No hardcoded secrets or unsafe deserialization
  • Home Assistant entity metadata correct (device class, state class, units)
  • Shared schema and filter-list objects are safe to reuse across keys
  • Embedded memory cost justified — suggestion #2
  • Test coverage for the new config paths
  • Public config option names durable before release — suggestion #3
  • Hot-path code readable and maintainable for later parts — suggestion #1, suggestion #4
  • PR description matches the diff

Automated review by Kōan (Claude) HEAD=371970d 9 min 1s

@esphbot esphbot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

No blocking issues found — ready to merge.

@swoboda1337
swoboda1337 merged commit 53b1b3a into esphome:dev Aug 6, 2026
44 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

0