Skip to content

[ld2450] Add frame header synchronization to readline_()#14135

Merged
swoboda1337 merged 2 commits intoesphome:devfrom
swoboda1337:fix/ld2450-frame-sync
Feb 20, 2026
Merged

[ld2450] Add frame header synchronization to readline_()#14135
swoboda1337 merged 2 commits intoesphome:devfrom
swoboda1337:fix/ld2450-frame-sync

Conversation

@swoboda1337
Copy link
Member

@swoboda1337 swoboda1337 commented Feb 20, 2026

What does this implement/fix?

After upgrading to ESPHome 2026.2.0, LD2450 radar devices fail to initialize and continuously log Max command length exceeded; ignoring. This is caused by the UART parser lacking frame header synchronization — when it starts reading mid-frame (e.g. after the LD2450 module restarts during setup()), accumulated bytes never form a valid frame, the buffer overflows, resets, and the cycle repeats indefinitely.

This PR adds header validation for the first 4 bytes of each frame in readline_(). The parser now only starts accumulating data when it sees a valid DATA_FRAME_HEADER (AA FF 03 00) or CMD_FRAME_HEADER (FD FC FB FA), ensuring it always begins at a frame boundary. If a header mismatch occurs mid-header, the parser resets and checks if the mismatched byte could start a new frame.

Additionally:

  • Added return after buffer overflow reset to avoid processing stale data
  • Replaced magic number 4 with existing HEADER_FOOTER_SIZE constant
  • Added missing #include "esphome/core/automation.h" in ld2450.h (needed for Trigger<> base class)
  • Added 14 Google Test unit tests covering normal frame parsing, garbage rejection, header sync, overflow recovery, and simulated restart scenarios

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • 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)
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

Pull request in esphome-docs with documentation (if applicable):

  • N/A

Test Environment

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

Example entry for config.yaml:

# No config changes required. Existing LD2450 configurations work as before.
uart:
  tx_pin: GPIO01
  rx_pin: GPIO03
  baud_rate: 115200

ld2450:

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:

  • N/A - no user-facing changes.

Copilot AI review requested due to automatic review settings February 20, 2026 02:39
@github-actions
Copy link
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#14135
    components: [ld2450]
    refresh: 1h

(Added by the PR bot)

@github-actions
Copy link
Contributor

👋 Hi there! This PR modifies 2 file(s) with codeowners.

@hareeshmu - As codeowner(s) of the affected files, your review would be appreciated! 🙏

Note: Automatic review request may have failed, but you're still welcome to review.

@codecov-commenter
Copy link

codecov-commenter commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.11%. Comparing base (c1265a9) to head (f103acc).
⚠️ Report is 3 commits behind head on dev.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev   #14135   +/-   ##
=======================================
  Coverage   74.11%   74.11%           
=======================================
  Files          55       55           
  Lines       11590    11590           
  Branches     1578     1578           
=======================================
  Hits         8590     8590           
  Misses       2598     2598           
  Partials      402      402           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical regression in ESPHome 2026.2.0 where LD2450 radar devices fail to initialize due to UART frame synchronization issues. The root cause is that the UART parser lacks header validation, causing it to accumulate mid-frame garbage data indefinitely when starting to read mid-stream (e.g., after module restart).

Changes:

  • Added byte-by-byte frame header synchronization to validate that incoming data starts with a valid DATA_FRAME_HEADER (AA FF 03 00) or CMD_FRAME_HEADER (FD FC FB FA)
  • Added explicit return after buffer overflow to prevent processing stale data
  • Added missing #include "esphome/core/automation.h" for Trigger<> base class

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
esphome/components/ld2450/ld2450.h Added missing include for automation.h (needed for Trigger<> template)
esphome/components/ld2450/ld2450.cpp Implemented frame header synchronization in readline_(), added return after overflow, replaced magic number with HEADER_FOOTER_SIZE constant
tests/components/ld2450/common.h Added test infrastructure with MockUARTComponent, TestableLD2450 wrapper, and frame builder helpers
tests/components/ld2450/ld2450_readline.cpp Added 14 comprehensive unit tests covering normal parsing, garbage rejection, header sync, overflow recovery, and restart scenarios

…ssion

The batch UART read change in esphome#13818 exposed a latent bug where the
UART parser could start accumulating bytes mid-frame after module
restart, causing an infinite cycle of "Max command length exceeded"
warnings that prevented initialization.

Add header validation for the first 4 bytes of each frame, ensuring
the parser only accumulates data starting from a valid DATA_FRAME_HEADER
(AA FF 03 00) or CMD_FRAME_HEADER (FD FC FB FA). Non-matching bytes are
discarded until a valid frame start is found.

Fixes esphome#14131

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@swoboda1337 swoboda1337 force-pushed the fix/ld2450-frame-sync branch from 3dcaa55 to 4ecf81a Compare February 20, 2026 02:45
@swoboda1337 swoboda1337 enabled auto-merge (squash) February 20, 2026 02:53
@swoboda1337
Copy link
Member Author

Thanks

swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
Add frame header validation to prevent the parser from getting stuck
in an overflow loop when it loses sync with the UART byte stream
(e.g. after module restart or UART noise). This is the same latent
bug fixed in ld2450 (PR esphome#14135) and present in ld2410.

The fix validates the first 4 bytes of each frame match a known
header (CMD or ENERGY) before accumulating data. In simple mode,
frames are text lines without binary headers, so the check is
skipped. Also adds a return after buffer overflow reset to prevent
immediately re-accumulating the overflowed byte.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@swoboda1337 swoboda1337 merged commit b67b2cc into esphome:dev Feb 20, 2026
29 checks passed
swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
Add frame header validation to prevent the parser from getting stuck
in an overflow loop when it loses sync with the UART byte stream
(e.g. after module restart or UART noise). This is the same latent
bug fixed in ld2450 (PR esphome#14135) and present in ld2420.

The fix validates the first 4 bytes of each frame match a known
header (DATA or CMD) before accumulating data. On header byte
mismatch, the buffer resets and checks if the mismatched byte starts
a new frame. Also adds a return after buffer overflow reset to
prevent immediately re-accumulating the overflowed byte.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@swoboda1337 swoboda1337 changed the title [ld2450] Add frame header synchronization to fix initialization regression [ld2450] Add frame header synchronization to readline_() Feb 20, 2026
swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
…me resync

Reverts the frame header synchronization added in esphome#14135 and esphome#14136
in favor of a simpler fix: increasing MAX_LINE_LENGTH so that the
existing footer-based resynchronization can recover after losing sync.

Both components already check for frame footers at every byte position,
which naturally resyncs the parser. The problem was that the buffers
were sized exactly to fit the largest frame, so a desynced parser's
footer could land at the overflow boundary and get discarded. Increasing
the buffer by 4 bytes (footer size) ensures the footer always lands
inside the buffer.

- ld2450: 41 -> 45 (zone query response = 40 bytes + 1 null + 4 footer)
- ld2410: 46 -> 50 (engineering data frame = 45 bytes + 1 null + 4 footer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
…me resync

Reverts the frame header synchronization added in esphome#14135 and esphome#14136
in favor of a simpler fix: increasing MAX_LINE_LENGTH so that the
existing footer-based resynchronization can recover after losing sync.

Both components already check for frame footers at every byte position,
which naturally resyncs the parser. The problem was that the buffers
were sized exactly to fit the largest frame, so a desynced parser's
footer could land at the overflow boundary and get discarded. Increasing
the buffer by 4 bytes (footer size) ensures the footer always lands
inside the buffer.

- ld2450: 41 -> 45 (zone query response = 40 bytes + 1 null + 4 footer)
- ld2410: 46 -> 50 (engineering data frame = 45 bytes + 1 null + 4 footer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
…me resync

Reverts the frame header synchronization added in esphome#14135 and esphome#14136
in favor of a simpler fix: increasing MAX_LINE_LENGTH so that the
existing footer-based resynchronization can recover after losing sync.

Both components already check for frame footers at every byte position,
which naturally resyncs the parser. The problem was that the buffers
were sized exactly to fit the largest frame, so a desynced parser's
footer could land at the overflow boundary and get discarded. Increasing
the buffer by 4 bytes (footer size) ensures the footer always lands
inside the buffer.

- ld2450: 41 -> 45 (zone query response = 40 bytes + 1 null + 4 footer)
- ld2410: 46 -> 50 (engineering data frame = 45 bytes + 1 null + 4 footer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
…me resync

Reverts the frame header synchronization added in esphome#14135 and esphome#14136
in favor of a simpler fix: increasing MAX_LINE_LENGTH so that the
existing footer-based resynchronization can recover after losing sync.

Both components already check for frame footers at every byte position,
which naturally resyncs the parser. The problem was that the buffers
were sized exactly to fit the largest frame, so a desynced parser's
footer could land at the overflow boundary and get discarded. Increasing
the buffer by 4 bytes (footer size) ensures the footer always lands
inside the buffer.

- ld2450: 41 -> 45 (zone query response = 40 bytes + 1 null + 4 footer)
- ld2410: 46 -> 50 (engineering data frame = 45 bytes + 1 null + 4 footer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
swoboda1337 added a commit to swoboda1337/esphome that referenced this pull request Feb 20, 2026
…me resync

Reverts the frame header synchronization added in esphome#14135 and esphome#14136
in favor of a simpler fix: increasing MAX_LINE_LENGTH so that the
existing footer-based resynchronization can recover after losing sync.

Both components already check for frame footers at every byte position,
which naturally resyncs the parser. The problem was that the buffers
were sized exactly to fit the largest frame, so a desynced parser's
footer could land at the overflow boundary and get discarded. Increasing
the buffer by 4 bytes (footer size) ensures the footer always lands
inside the buffer.

- ld2450: 41 -> 45 (zone query response = 40 bytes + 1 null + 4 footer)
- ld2410: 46 -> 50 (engineering data frame = 45 bytes + 1 null + 4 footer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
swoboda1337 added a commit that referenced this pull request Feb 20, 2026
…ssion (#14135)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
@swoboda1337 swoboda1337 mentioned this pull request Feb 20, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Feb 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

4 participants