mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 17:00:21 +00:00
[ld2420] Wait for data from the module before configuring it
The LD2420 locks up until power cycled if it receives any data before it has sent its first frame after powering on. Setup previously transmitted immediately, blocking for up to 3 seconds and marking the component failed on the first timeout. Setup is now a non blocking state machine that listens for the module before transmitting, retries the handshake, and degrades to a warning instead of failing.
This commit is contained in:
@@ -50,19 +50,10 @@ uart_mock:
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all response: match any command footer (04 03 02 01).
|
||||
# Returns a generic ACK with cmd=0xFF (CMD_ENABLE_CONF case in switch).
|
||||
# All commands get unblocked via cmd_reply_.ack = true.
|
||||
# Data fields stay zeroed (min_gate=0, max_gate=0, timeout=0, thresholds=0).
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 04 00 = length 4
|
||||
# [6] FF = cmd (handled as CMD_ENABLE_CONF)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10-13] 04 03 02 01 = footer
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
# TX = FD FC FB FA 04 00 FF 00 02 00 04 03 02 01
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
@@ -72,6 +63,53 @@ uart_mock:
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the remaining commands, which are all CMD_READ_ABD_PARAM
|
||||
# (0x0008) reads: min/max/timeout limits and the 16 gate threshold reads.
|
||||
# The reply carries three zeroed uint32 values (data length 16 = 4 + 12),
|
||||
# so limits and thresholds all read as 0.
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 10 00 = length 16
|
||||
# [6] 08 = cmd (CMD_READ_ABD_PARAM)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10-21] 00 x12 = three zeroed uint32 data values
|
||||
# [22-25] 04 03 02 01 = footer
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2420 energy mode data frame - happy path
|
||||
# Buffer is clean (buffer_pos_=0). This frame should parse correctly.
|
||||
@@ -98,13 +136,15 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# Phase 2 (t=800ms): Garbage bytes
|
||||
# LD2420's readline_ does NOT check frame headers at position 0 (unlike LD2412),
|
||||
# so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7.
|
||||
- delay: 200ms
|
||||
# Delay=700ms leaves time for the setup handshake (triggered by Phase 1,
|
||||
# the first data seen from the module) to finish first.
|
||||
- delay: 700ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=400ms): Truncated energy frame WITH footer (13 bytes)
|
||||
# Phase 3 (t=900ms): Truncated energy frame WITH footer (13 bytes)
|
||||
# This tests PR #14458 bug #3: missing length validation in handle_energy_mode_.
|
||||
# The 7 garbage bytes from Phase 2 are still in the buffer (buffer_pos_=7).
|
||||
# These 13 bytes are appended at positions 7-19 (buffer_pos_=20).
|
||||
@@ -126,7 +166,7 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 4 (t=600ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# Phase 4 (t=1100ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# After Phase 3, buffer_pos_=0 (reset after energy footer detection).
|
||||
# 49 bytes fill positions 0-48 (buffer_pos_=49), 50th byte triggers overflow.
|
||||
# Logs "Max command length exceeded; ignoring", buffer_pos_=0.
|
||||
@@ -143,8 +183,8 @@ uart_mock:
|
||||
# Phase 5 (t=1500ms): Valid frame after overflow - recovery test
|
||||
# Buffer was reset by overflow. This valid frame should parse correctly.
|
||||
# Presence: 1 (target), Distance: 50cm
|
||||
# Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 900ms
|
||||
# Delay=400ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 400ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-boot-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Simulates a cold boot where the LD2420 module boots slower than the ESP:
|
||||
# the module is silent for 2 seconds and then sends its first energy frame.
|
||||
# The module locks up until power cycled if it receives any data before it
|
||||
# has sent its first frame, so the component must stay quiet for the full
|
||||
# 2 seconds and only start its setup handshake after the first frame.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
injections:
|
||||
# Module boot finished (t=2000ms): first energy frame
|
||||
# (presence=1, distance=100). Any TX from the component before this
|
||||
# point would have locked up real hardware.
|
||||
- delay: 2000ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Post-setup frame (t=3300ms): distance=50 proves streaming still works
|
||||
# after the setup handshake. Delay=1300ms keeps >1000ms publish throttle
|
||||
# gap from the first frame.
|
||||
- delay: 1300ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x32, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -22,10 +22,24 @@ uart_mock:
|
||||
baud_rate: 115200
|
||||
auto_start: false
|
||||
responses:
|
||||
# Catch-all response only (no version-specific response).
|
||||
# Without a version response, firmware_ver_ stays at default "v0.0.0".
|
||||
# get_firmware_int("v0.0.0") = 0 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE).
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
# Version response with an old firmware version "v1.5.3".
|
||||
# get_firmware_int("v1.5.3") = 153 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE).
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x31, 0x2E, 0x35, 0x2E, 0x33,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
@@ -35,10 +49,47 @@ uart_mock:
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = simple (0x0064)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid simple mode text frame - happy path
|
||||
# "ON Range 0100\r\n" → presence=true, distance=100
|
||||
# Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_.
|
||||
# Phase 0 (t=100ms): Wake-up frame. The component listens for data from the
|
||||
# module before transmitting anything, so this frame starts the setup
|
||||
# handshake. It is not parsed as simple mode because the component's system
|
||||
# mode is only switched to simple after the firmware version is read.
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[
|
||||
@@ -47,12 +98,24 @@ uart_mock:
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# Phase 1 (t=800ms): Valid simple mode text frame - happy path
|
||||
# "ON Range 0100\r\n" → presence=true, distance=100
|
||||
# Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_.
|
||||
# Delay=700ms leaves time for the setup handshake to finish first.
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x31, 0x30, 0x30,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 2 (t=1000ms): Garbage bytes
|
||||
# LD2420's readline_ stores all bytes regardless of header. buffer_pos_ = 7.
|
||||
- delay: 200ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=500ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# Phase 3 (t=1200ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# buffer_pos_ starts at 7 (from Phase 2 garbage).
|
||||
# Positions 7-48 fill (42 bytes), byte 43 triggers overflow (buffer_pos_=49).
|
||||
# After overflow: buffer_pos_=0, remaining 7 bytes fill positions 0-6.
|
||||
@@ -67,13 +130,13 @@ uart_mock:
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 4 (t=1400ms): Recovery after overflow
|
||||
# Phase 4 (t=1900ms): Recovery after overflow
|
||||
# buffer_pos_ = 7 (from overflow remainder). These 15 bytes fill positions 7-21.
|
||||
# At position 21 (0x0A), \r\n detected → handle_simple_mode_(buffer, 22).
|
||||
# Parser skips 0xFF bytes at positions 0-6, finds "ON" at positions 7-8,
|
||||
# parses digits "0050" → distance=50.
|
||||
# Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 900ms
|
||||
# Delay=700ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
@@ -81,7 +144,7 @@ uart_mock:
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 5 (t=2500ms): 16-digit distance - tests PR #14458 bug #1
|
||||
# Phase 5 (t=3000ms): 16-digit distance - tests PR #14458 bug #1
|
||||
# "ON Range 0000000000000000\r\n" has 16 digit characters.
|
||||
# handle_simple_mode_ outbuf is 16 bytes, can hold 15 digits (index 0-14).
|
||||
#
|
||||
@@ -100,7 +163,7 @@ uart_mock:
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 6 (t=3700ms): Post-bug-trigger recovery
|
||||
# Phase 6 (t=4200ms): Post-bug-trigger recovery
|
||||
# If Phase 5 didn't hang, this frame should parse correctly.
|
||||
# "ON Range 0025\r\n" → distance=25
|
||||
# Delay=1200ms ensures >1000ms gap from Phase 5 for throttle.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-warm-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Simulates a warm restart: the ESP rebooted but the LD2420 module stayed
|
||||
# powered and keeps streaming energy frames from the moment the firmware
|
||||
# starts. The component must not transmit anything until it has seen data
|
||||
# from the module, then run its setup handshake against the live stream.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
# Module streams a valid energy frame (presence=1, distance=100) continuously
|
||||
periodic_rx:
|
||||
- interval: 250ms
|
||||
data:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -15,6 +15,18 @@ test_uart_mock_ld2420_simple (simple mode):
|
||||
3. Buffer overflow recovery
|
||||
4. 16-digit distance triggers infinite loop pre-fix (PR #14458 bug #1)
|
||||
5. Post-bug-trigger recovery proves the parser survived
|
||||
|
||||
test_uart_mock_ld2420_warm_restart (module streaming at boot):
|
||||
Simulates a warm restart where the module stayed powered and streams energy
|
||||
frames from the moment the firmware starts. Asserts the component never
|
||||
transmits before receiving data from the module, completes setup against
|
||||
the live stream, and publishes sensor data.
|
||||
|
||||
test_uart_mock_ld2420_delayed_boot (module boots slower than the ESP):
|
||||
Simulates a cold boot where the module is silent for 2 seconds. The module
|
||||
locks up until power cycled if it receives data before sending its first
|
||||
frame, so the component must stay quiet until the module talks, then
|
||||
complete setup and keep parsing the stream.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -160,6 +172,208 @@ async def test_uart_mock_ld2420(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_warm_restart(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Module streams from boot; component must listen first, then set up."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
setup_complete = loop.create_future()
|
||||
rx_seen = False
|
||||
tx_before_rx = False
|
||||
failure_lines: list[str] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
nonlocal rx_seen, tx_before_rx
|
||||
if "uart_mock" in line:
|
||||
if "RX inject" in line or "Injecting" in line:
|
||||
rx_seen = True
|
||||
elif "TX " in line and not rx_seen:
|
||||
tx_before_rx = True
|
||||
if (
|
||||
"Module setup complete; firmware v2.0.0" in line
|
||||
and not setup_complete.done()
|
||||
):
|
||||
setup_complete.set_result(True)
|
||||
if (
|
||||
"marked FAILED" in line
|
||||
or "Communication failed" in line
|
||||
or "No data received from the module" in line
|
||||
):
|
||||
failure_lines.append(line)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
binary_sensor_names=["has_target"],
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
collector.build_key_mapping(entities)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(
|
||||
initial_state_helper.on_state_wrapper(collector.on_state)
|
||||
)
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Setup handshake must complete against the live stream
|
||||
try:
|
||||
await asyncio.wait_for(setup_complete, timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Timeout waiting for 'Module setup complete' log line. "
|
||||
"The startup state machine did not finish its handshake."
|
||||
)
|
||||
|
||||
# Sensor data must flow from the stream
|
||||
try:
|
||||
await collector.wait_for_all(timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for sensor data. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
)
|
||||
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
assert collector.binary_states["has_target"][0] is True
|
||||
|
||||
# The component must never transmit before the module has talked;
|
||||
# real hardware locks up until power cycled if it does.
|
||||
assert not tx_before_rx, (
|
||||
"Component transmitted on the UART before receiving any data "
|
||||
"from the module; this locks up real LD2420 hardware"
|
||||
)
|
||||
|
||||
assert not failure_lines, f"Unexpected failure log lines: {failure_lines}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_delayed_boot(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Module silent for 2 s; component must not transmit into the boot window."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
setup_complete = loop.create_future()
|
||||
rx_seen = False
|
||||
tx_before_rx = False
|
||||
failure_lines: list[str] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
nonlocal rx_seen, tx_before_rx
|
||||
if "uart_mock" in line:
|
||||
if "RX inject" in line or "Injecting" in line:
|
||||
rx_seen = True
|
||||
elif "TX " in line and not rx_seen:
|
||||
tx_before_rx = True
|
||||
if (
|
||||
"Module setup complete; firmware v2.0.0" in line
|
||||
and not setup_complete.done()
|
||||
):
|
||||
setup_complete.set_result(True)
|
||||
if (
|
||||
"marked FAILED" in line
|
||||
or "Communication failed" in line
|
||||
or "No data received from the module" in line
|
||||
):
|
||||
failure_lines.append(line)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
binary_sensor_names=["has_target"],
|
||||
)
|
||||
|
||||
# The second injected frame (distance=50) proves streaming works post-setup
|
||||
post_setup_received = collector.add_waiter(
|
||||
lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"]
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
collector.build_key_mapping(entities)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(
|
||||
initial_state_helper.on_state_wrapper(collector.on_state)
|
||||
)
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Module's first frame arrives at t=2000ms; handshake follows
|
||||
try:
|
||||
await asyncio.wait_for(setup_complete, timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Timeout waiting for 'Module setup complete' log line. "
|
||||
"The startup state machine did not finish its handshake."
|
||||
)
|
||||
|
||||
try:
|
||||
await collector.wait_for_all(timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for sensor data. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
)
|
||||
|
||||
# First frame (t=2000ms) publishes distance=100
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
assert collector.binary_states["has_target"][0] is True
|
||||
|
||||
# Second frame (t=3300ms, after setup) publishes distance=50
|
||||
try:
|
||||
await asyncio.wait_for(post_setup_received, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for post-setup frame (distance=50). Received:\n"
|
||||
f" moving_distance: {collector.sensor_states['moving_distance']}"
|
||||
)
|
||||
|
||||
# The component must have stayed quiet for the module's whole 2 s boot
|
||||
# window; transmitting into it locks up real LD2420 hardware.
|
||||
assert not tx_before_rx, (
|
||||
"Component transmitted on the UART before the module sent its "
|
||||
"first frame; this locks up real LD2420 hardware"
|
||||
)
|
||||
|
||||
assert not failure_lines, f"Unexpected failure log lines: {failure_lines}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_simple(
|
||||
yaml_config: str,
|
||||
|
||||
Reference in New Issue
Block a user