[ld2460] Preserve buffered frames across overflow (#21123)

This commit is contained in:
Igor Stadnyk
2026-10-19 08:23:31 -10:00
committed by GitHub
parent fd3c1fad45
commit a5fbd34a1e
2 changed files with 66 additions and 2 deletions
+27 -2
View File
@@ -37,6 +37,22 @@ static bool matches(const uint8_t *bytes, const uint8_t *pattern) {
static constexpr uint8_t FRAME_PREFIX_SIZE = 7;
static uint16_t frame_length(const uint8_t *frame) { return encode_uint16(frame[6], frame[5]); }
// First offset after 0 whose bytes, as far as they go, match a header; len when there is none (len >= 1)
static uint8_t next_header_start(const uint8_t *buffer, uint8_t len) {
uint8_t data_header[HEADER_FOOTER_SIZE];
uint8_t cmd_header[HEADER_FOOTER_SIZE];
progmem_memcpy(data_header, DATA_FRAME_HEADER, sizeof(data_header));
progmem_memcpy(cmd_header, CMD_FRAME_HEADER, sizeof(cmd_header));
uint8_t start = 1;
for (; start < len; start++) {
const size_t n = std::min<size_t>(len - start, HEADER_FOOTER_SIZE);
if (std::memcmp(&buffer[start], data_header, n) == 0 || std::memcmp(&buffer[start], cmd_header, n) == 0) {
break;
}
}
return start;
}
static constexpr float RADIANS_TO_DEGREES = 180.0f / std::numbers::pi_v<float>;
void LD2460Component::setup() { this->send_query_(); }
@@ -155,8 +171,17 @@ void LD2460Component::handle_periodic_data_() {
void LD2460Component::readline_(uint8_t readch) {
if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
ESP_LOGW(TAG, "Max frame length exceeded; ignoring");
this->buffer_pos_ = 0;
// Keep the bytes from the next offset that could start a header, so a frame already partly
// buffered behind stray bytes survives; it is still fully validated once its footer arrives
const uint8_t start = next_header_start(this->buffer_data_, this->buffer_pos_);
this->buffer_pos_ -= start;
std::memmove(this->buffer_data_, &this->buffer_data_[start], this->buffer_pos_);
// Header-shaped noise keeps a whole header; warning only otherwise bounds the warning rate
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
ESP_LOGW(TAG, "Max frame length exceeded; ignoring");
} else {
ESP_LOGV(TAG, "Max frame length exceeded; kept %u bytes", this->buffer_pos_);
}
}
this->buffer_data_[this->buffer_pos_++] = readch;
@@ -180,4 +180,43 @@ TEST_F(LD2460ReadlineTest, DataCallbackOncePerValidFrame) {
EXPECT_EQ(calls, 2);
}
TEST_F(LD2460ReadlineTest, TruncatedPrefixBeforeLargeFrames) {
// Three targets still fit behind the truncated prefix; four and five overflow the buffer
const std::vector<Point> targets = {{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}};
for (int n = 3; n <= MAX_TARGETS; n++) {
SCOPED_TRACE(n);
this->ld2460_.feed({0x00, 0xF4, 0xF3, 0xF2, 0xF1, 0x04});
this->ld2460_.feed(make_periodic_frame(std::vector<Point>(targets.begin(), targets.begin() + n)));
EXPECT_FLOAT_EQ(this->count_.state, n);
EXPECT_NEAR(this->x_[n - 1].state, (2 * n - 1) * 0.1f, 0.001f);
}
}
TEST_F(LD2460ReadlineTest, LostFooterTwoTargetResync) {
// A two-target frame missing its last byte; the next frame overflows the buffer
auto broken = make_periodic_frame({{10, 10}, {20, 20}});
broken.pop_back();
this->ld2460_.feed(broken);
this->ld2460_.feed(make_periodic_frame({{30, 40}, {50, 60}}));
EXPECT_FLOAT_EQ(this->count_.state, 2);
EXPECT_NEAR(this->x_[1].state, 5.0f, 0.001f);
this->ld2460_.feed(make_periodic_frame({{70, 80}}));
EXPECT_FLOAT_EQ(this->count_.state, 1);
EXPECT_NEAR(this->x_[0].state, 7.0f, 0.001f);
}
TEST_F(LD2460ReadlineTest, HeaderLikeGarbageThenMaxTargets) {
// Header first bytes and whole command headers in the noise do not hold back a real frame
this->ld2460_.feed(std::vector<uint8_t>(70, 0xF4));
this->ld2460_.feed(make_periodic_frame({{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}}));
EXPECT_FLOAT_EQ(this->count_.state, 5);
std::vector<uint8_t> garbage;
for (int i = 0; i < 40; i++) {
garbage.insert(garbage.end(), {0xFD, 0xFC, 0xFB, 0xFA});
}
this->ld2460_.feed(garbage);
this->ld2460_.feed(make_periodic_frame({{11, 12}, {3, 4}, {5, 6}, {7, 8}, {9, 10}}));
EXPECT_NEAR(this->x_[0].state, 1.1f, 0.001f);
}
} // namespace esphome::ld2460::testing