mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
[ld2420] Add module restart regression test
Presses the restart button while the module is mid transmission: the in-flight frame tail bytes arriving right after the restart command must not count as proof the module is up, and the setup handshake must only run again after the module's first post-boot frame.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-restart-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
|
||||
|
||||
# Exercises the module restart path. The restart command hits the module while
|
||||
# it is mid transmission, so a few tail bytes of the in-flight frame arrive
|
||||
# right after the restart. The module is then silent for 2 seconds while it
|
||||
# boots, and it locks up until power cycled if it receives any data in that
|
||||
# window. The component must not treat the tail bytes as proof the module is
|
||||
# up, and must only re-run its setup handshake after the module's first
|
||||
# post-boot frame.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
injections:
|
||||
# Initial wake-up frame (t=700ms): energy frame (presence=1, distance=100).
|
||||
# Delay=700ms keeps it outside the component's 500ms listen settle window,
|
||||
# which is measured from boot and ignores earlier reception.
|
||||
- delay: 700ms
|
||||
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,
|
||||
]
|
||||
|
||||
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,
|
||||
]
|
||||
|
||||
# The module restart command (CMD_RESTART, 0x0068) gets no reply; a real
|
||||
# module goes silent and reboots. Matching it here prevents the catch-all
|
||||
# below from answering it.
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x68, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx: []
|
||||
|
||||
# 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,
|
||||
]
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Restart Module"
|
||||
on_press:
|
||||
- lambda: 'id(ld2420_dev).restart_module_action();'
|
||||
# Tail of the energy frame the module was transmitting when the restart
|
||||
# command hit it; must not count as proof the module is up
|
||||
- uart_mock.inject_rx:
|
||||
id: mock_uart
|
||||
data: [0x00, 0x00, 0x00, 0xF8, 0xF7, 0xF6, 0xF5]
|
||||
# The module's first frame after its ~2s boot (presence=1, distance=100)
|
||||
- uart_mock.inject_rx:
|
||||
id: mock_uart
|
||||
delay: 2000ms
|
||||
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,
|
||||
]
|
||||
|
||||
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
|
||||
@@ -27,6 +27,14 @@ test_uart_mock_ld2420_delayed_boot (module boots slower than the ESP):
|
||||
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.
|
||||
|
||||
test_uart_mock_ld2420_restart_button (module restart action):
|
||||
Presses the restart button after setup. The restart hits the module mid
|
||||
transmission, so a few tail bytes of the in-flight frame arrive right after
|
||||
the restart command, then the module is silent for 2 seconds while it
|
||||
boots. The component must not treat the tail bytes as proof the module is
|
||||
up and must only re-run its handshake after the module's first post-boot
|
||||
frame; transmitting into the boot window locks up real hardware.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -308,6 +316,94 @@ async def test_uart_mock_ld2420_delayed_boot(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_restart_button(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Restart action must not transmit into the module's boot window."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
setup_complete_count = 0
|
||||
first_setup_complete = loop.create_future()
|
||||
second_setup_complete = loop.create_future()
|
||||
restart_seen = False
|
||||
module_frame_after_restart_seen = False
|
||||
tx_into_boot_window = False
|
||||
failure_lines: list[str] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
nonlocal setup_complete_count, restart_seen, module_frame_after_restart_seen
|
||||
nonlocal tx_into_boot_window
|
||||
if "Module setup complete; firmware v2.0.0" in line:
|
||||
setup_complete_count += 1
|
||||
if setup_complete_count == 1 and not first_setup_complete.done():
|
||||
first_setup_complete.set_result(True)
|
||||
elif setup_complete_count == 2 and not second_setup_complete.done():
|
||||
second_setup_complete.set_result(True)
|
||||
if "[ld2420" in line and "Restarting" in line:
|
||||
restart_seen = True
|
||||
if restart_seen and "RX inject 45 bytes" in line:
|
||||
# The module's first frame after its simulated 2 s boot
|
||||
module_frame_after_restart_seen = True
|
||||
if (
|
||||
restart_seen
|
||||
and not module_frame_after_restart_seen
|
||||
and "uart_mock" in line
|
||||
and "TX " in line
|
||||
and "FF:00:02:00" in line
|
||||
):
|
||||
# Config mode enable transmitted before the module's first
|
||||
# post-boot frame; on real hardware this locks the module up
|
||||
tx_into_boot_window = True
|
||||
if "marked FAILED" in line or "Communication failed" in line:
|
||||
failure_lines.append(line)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(lambda s: None))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Wait for the initial startup handshake to finish
|
||||
try:
|
||||
await asyncio.wait_for(first_setup_complete, timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for the initial 'Module setup complete'")
|
||||
|
||||
# Restart the module; the button automation also injects the in-flight
|
||||
# frame tail immediately and the module's first frame 2 s later
|
||||
restart_btn = find_entity(entities, "restart_module", ButtonInfo)
|
||||
assert restart_btn is not None, "Restart Module button not found"
|
||||
client.button_command(restart_btn.key)
|
||||
|
||||
# The handshake must complete again after the module comes back
|
||||
try:
|
||||
await asyncio.wait_for(second_setup_complete, timeout=15.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Timeout waiting for 'Module setup complete' after the restart. "
|
||||
"The component did not recover from the module restart."
|
||||
)
|
||||
|
||||
assert not tx_into_boot_window, (
|
||||
"Component transmitted the config handshake into the module's "
|
||||
"boot window after a restart; the in-flight frame tail bytes must "
|
||||
"not count as proof the module is up"
|
||||
)
|
||||
|
||||
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