mirror of
https://github.com/esphome/esphome.git
synced 2026-10-21 13:34:12 +00:00
[ld2420] Resume UART reads after a silent command timeout (#21131)
This commit is contained in:
@@ -683,6 +683,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
this->handle_cmd_error(this->cmd_reply_.error);
|
||||
}
|
||||
}
|
||||
this->cmd_active_ = false; // Resend and give-up are done; let loop() read the UART again
|
||||
return error;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-config-exit
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
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
|
||||
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: false
|
||||
# Energy reports with distance 100, then 101, more than the 1 s report interval apart.
|
||||
# Each delay counts from the previous injection.
|
||||
injections:
|
||||
- delay: 1200ms
|
||||
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]
|
||||
- delay: 1500ms
|
||||
inject_rx: [0xF4, 0xF3, 0xF2, 0xF1, 0x23, 0x00, 0x01, 0x65, 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]
|
||||
on_tx:
|
||||
- then:
|
||||
- lambda: |-
|
||||
// Rebuild command frames however the writes are split, and answer each by its command byte.
|
||||
// Reply to config exit (0xFE): 0 none, 1 failure status, 2 too short, 3 success
|
||||
static constexpr int MODE = EXIT_REPLY_MODE;
|
||||
static std::vector<uint8_t> buf;
|
||||
static int exit_sends = 0;
|
||||
buf.insert(buf.end(), data.begin(), data.end());
|
||||
while (buf.size() >= 6) {
|
||||
if (buf[0] != 0xFD || buf[1] != 0xFC || buf[2] != 0xFB || buf[3] != 0xFA) {
|
||||
buf.erase(buf.begin());
|
||||
continue;
|
||||
}
|
||||
size_t len = buf[4] | (buf[5] << 8);
|
||||
if (buf.size() < len + 10)
|
||||
return;
|
||||
uint8_t cmd = buf[6];
|
||||
size_t params = len >= 2 ? (len - 2) / 2 : 0;
|
||||
buf.erase(buf.begin(), buf.begin() + len + 10);
|
||||
std::vector<uint8_t> body = {cmd, 0x01, 0x00, 0x00}; // reply word, status
|
||||
if (cmd == 0x68) {
|
||||
continue; // restart has no reply
|
||||
} else if (cmd == 0x00) {
|
||||
body.insert(body.end(), {0x06, 0x00, 0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30}); // "v2.0.0"
|
||||
} else if (cmd == 0x08) {
|
||||
body.insert(body.end(), params * 4, 0x00);
|
||||
} else if (cmd == 0xFE) {
|
||||
ESP_LOGI("ld2420_test", "Config exit sent %d", ++exit_sends);
|
||||
if (MODE == 0)
|
||||
continue;
|
||||
if (MODE == 1)
|
||||
body[2] = 0x01;
|
||||
if (MODE == 2)
|
||||
body.resize(1);
|
||||
}
|
||||
std::vector<uint8_t> reply = {0xFD, 0xFC, 0xFB, 0xFA, static_cast<uint8_t>(body.size()), 0x00};
|
||||
reply.insert(reply.end(), body.begin(), body.end());
|
||||
reply.insert(reply.end(), {0x04, 0x03, 0x02, 0x01});
|
||||
id(mock_uart).inject_to_rx_buffer(reply);
|
||||
}
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
on_press:
|
||||
- lambda: 'id(mock_uart).start_scenario();'
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Integration test for LD2420 reports after the last config exit command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from aioesphomeapi import ButtonInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, SensorStateCollector, require_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
EXIT_SENT = re.compile(r"Config exit sent (\d+)")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "expected_sends"),
|
||||
[
|
||||
pytest.param(0, 3, id="silent"),
|
||||
pytest.param(1, 3, id="nack"),
|
||||
pytest.param(2, 3, id="short"),
|
||||
pytest.param(3, 1, id="ack"),
|
||||
],
|
||||
)
|
||||
async def test_uart_mock_ld2420_config_exit(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
mode: int,
|
||||
expected_sends: int,
|
||||
) -> None:
|
||||
"""Test both energy reports arrive after the last config exit, and a failed exit is still sent 3 times."""
|
||||
exit_sends: list[int] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if match := EXIT_SENT.search(line):
|
||||
exit_sends.append(int(match[1]))
|
||||
|
||||
collector = SensorStateCollector(sensor_names=["moving_distance"])
|
||||
yaml_config = yaml_config.replace("EXIT_REPLY_MODE", str(mode))
|
||||
|
||||
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")
|
||||
|
||||
start_btn = require_entity(entities, "start_scenario", ButtonInfo)
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
resumed = collector.add_waiter(
|
||||
lambda: len(collector.sensor_states["moving_distance"]) >= 2
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(resumed, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Energy reports did not resume after the last config exit.\n"
|
||||
f" exit sends: {exit_sends}\n"
|
||||
f" sensor_states: {collector.sensor_states}"
|
||||
)
|
||||
|
||||
# The sensor skips repeated values, so each distinct distance is one report
|
||||
assert collector.sensor_states["moving_distance"] == pytest.approx(
|
||||
[100.0, 101.0]
|
||||
)
|
||||
# A failed exit is resent and then given up; it is never treated as answered
|
||||
assert exit_sends == list(range(1, expected_sends + 1))
|
||||
Reference in New Issue
Block a user