ESPHome LD2450: coherent, timestamped frames for trajectory tracking

TL;DR: The usual LD2450 X, Y, and speed entities are fine for occupancy and
dashboards, but they are a poor substrate for trajectory math: they arrive as
separate Home Assistant state changes, use receive timing, and may deduplicate
unchanged values. I now publish all three target slots as one coherent state with
an ESP device timestamp:

t_ms|x1,y1,v1|x2,y2,v2|x3,y3,v3

The clean-room ESPHome package, parser, optional Pyscript CSV logger, BLE-presence
add-on, tests, and build examples are here:

Why one atomic frame?

Trajectory calculations care about small timing and coordinate errors. If X from
one radar frame is combined with Y from the next, or Home Assistant receive time
is used as the sample clock, velocity and heading estimates can move noticeably.

The package uses ESPHome’s native ld2450.on_data callback, after a complete
periodic frame has been parsed. It publishes raw millimetres, raw speed quanta,
all three target slots, and millis() from the ESP in one text state. Because
the timestamp changes every frame, repeated dwell coordinates are not silently
lost as duplicate states.

This does not make the LD2450 itself more accurate. It also does not solve
multipath ghosts, unstable target-slot identity, association across gaps, or
origin/destination classification. It provides a cleaner input stream on which
those later stages can operate.

The timestamp is device uptime, not wall-clock time. It wraps after about 49.7
days and resets on reboot; the included parser handles rollover and treats a
probable reboot as a discontinuity.

SCREEK 2A context

I developed and physically exercised these changes as modifications to my
private customized SCREEK Human Sensor 2A firmware. The public repository does
not contain SCREEK source, YAML, or binaries. It independently demonstrates the
same hardware capabilities using ESPHome’s public native components because the
SCREEK source repository currently has no declared redistribution license.

The public examples compile with ESPHome 2026.6.1, but have not yet been flashed
as complete replacement firmware. They are not official SCREEK updates, patches
to apply over a vendor image, or drop-in replacements for all vendor features.
Keep a recovery path before flashing experimental firmware.

Optional BLE presence

The repository also includes an optional passive iBeacon presence package for
the SCREEK 2A’s ESP32-C3 radio. BLE presence is a coarse, seconds-scale signal,
so Home Assistant arrival time is sufficient there; it does not need the radar
stream’s device-time precision.

The example listens for 100 ms in every 200 ms interval, so advertisements can
be missed by design. The ESP32-C3 also time-shares one radio between BLE and
Wi-Fi. Repeated advertisements plus a presence timeout tolerate occasional
misses, but this is not a delivery guarantee or an access-control signal.

The ESP32 scanner and the LD2450 module’s own Bluetooth radio are separate. The
radar radio is not needed for beacon scanning and should be disabled after setup
when unused. That security finding and procedure are covered here:

Recorder and privacy

An atomic frame can update several times per second. Exclude the text sensor from
Recorder when a live consumer is sufficient, or use the included buffered CSV
logger deliberately. Radar captures reveal movement patterns; beacon identifiers
and presence history are also tracking data. Keep real captures, geometry, and
identifiers private.

This is part of a series

This post is the transport/data-foundation part of a longer set of LD2450
writeups. Dates are TBD, and the order may change as the reusable code is tested.

  1. LD2450 Bluetooth hardening - published. It separates the ESP32 and radar
    radios, shows how the open radar configuration interface can affect dependent
    automations, and covers disablement and read-back.

  2. Atomic radar frames - this post. Coherent target slots, device timing,
    parsing, logging, rollover handling, and the clean-room SCREEK 2A demo.

  3. Radar lab setup and tuning - planned. A privacy-safe replay/visualization
    lab with synthetic fixtures, placement checks, native region/filter tuning,
    multipath diagnosis, and a repeatable way to compare parameter changes.

  4. Trajectory handling on lightweight Home Assistant hardware - planned.
    This will include two paths:

    • a practical starting path with documented constant-velocity Kalman defaults,
      gap coasting, outlier/association gates, and manually drawn
      origin/destination portals
    • a lab path for replaying captures, inspecting residuals and track splits,
      tuning noise/gates, and measuring CPU/memory cost

    The defaults will be starting points with explicit units and cadence
    assumptions, not universal magic values. Target slots are not person IDs, so
    cross-slot association must come before filtering/classification.

  5. Publishing from the lab to Home Assistant - planned. A neutral, versioned
    rule/config export; a lightweight HA/Pyscript adapter; reload and rollback;
    binary/event sensors with confidence and reason fields; and observability when
    input cadence or a classifier becomes unhealthy.

Rich mobile notifications fit in that fifth part rather than in the core tracker:
once HA has a stable event contract, notifications can add trajectory summaries,
confidence/reason text, cross-sensor corroboration, grouping, cooldowns, and an
action for marking a false alert without coupling notification UX to the tracking
algorithm.

A possible later evaluation writeup would cover ground-truth annotations,
synthetic regression cases, confusion matrices, and optional corroboration from
door contacts, PIR, or coarse BLE presence. Those sensors should validate or
identify a passage; they should not be hidden prerequisites for the basic LD2450
pipeline.

Feedback on the frame contract and on what would make the quick-start trajectory
path useful on modest HA hardware is welcome.

2 Likes

Update, 2026-08-28: SCREEK Workshop responded to the license request by
adding an MIT license and welcoming modified versions. Thank you to SCREEK for
making the terms explicit. This supersedes the original post’s statement that
the SCREEK repository had no declared redistribution license; Discourse no
longer permits me to edit that first post.

The repository now includes a full, attributed SCREEK 2A derivative much closer
to the firmware used for the hardware tests:

It retains the SCREEK UART parser, entities, software zones, illuminance support,
and PCB LED wiring, and adds the atomic frame stream, direct LD2450 Bluetooth /
mode / native-region commands with read-back, runtime publish throttling,
optional two-beacon BLE presence, and performance/restart diagnostics.

Before publishing it I replaced all identifiers and credentials, removed MAC
publication and MAC-derived naming, enabled safe mode, hid Bluetooth-enable and
factory-reset controls by default, pinned the exact SCREEK source revision, and
preserved SCREEK’s MIT notice. CI validates and compiles the clean-room examples
and the full derivative.

The same caveat remains: the feature lineage is physically exercised, but this
exact sanitized public file has not yet been flashed. Treat its first install as
USB/serial unless you have independently verified OTA partition compatibility.

1 Like

Confirming your premise with numbers from a different room, in case it is useful.

Same sensor, four days, one living room, LD2450 over ESPHome into a PostgreSQL recorder. The two entities do not line up:

target_1_x 225,074 rows 221,839 distinct seconds
target_1_y 221,621 rows 218,556 distinct seconds

Joining them on the second keeps 203,460 seconds, 91.7% of the x samples. The other 8.3% are seconds where x changed and y did not, or the reverse. On top of that, 1.1% of seconds carry two or more samples, so a second-granularity join can pair the wrong x with the wrong y.

I should be clear about where this does not matter. I use these coordinates for dwell-time density, not trajectory, and losing 8% of samples barely moves a four-day heatmap. For velocity and heading it clearly would, which is your point.

The dedup behaviour is the part I had not thought about. Someone sitting still produces the same coordinate repeatedly, and those are exactly the frames a dwell analysis wants, so I am probably losing the stillest periods without seeing it. I will check that against your atomic frame format.