mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 07:20:24 +00:00
[modbus] Drop a deferred server reply when another frame arrives first (#19534)
This commit is contained in:
@@ -203,6 +203,12 @@ void ModbusClientHub::parse_modbus_frames() {
|
||||
|
||||
void ModbusServerHub::parse_modbus_frames() {
|
||||
while (!this->rx_buffer_.empty()) {
|
||||
if (this->deferred_payload_len_ != 0) {
|
||||
// Another frame arrived before the deferred reply went out, so the client has moved on.
|
||||
this->cancel_timeout("deferred_send");
|
||||
ESP_LOGD(TAG, "Dropped deferred reply to %" PRIu8 ": a new frame arrived first", this->deferred_payload_[0]);
|
||||
this->deferred_payload_len_ = 0;
|
||||
}
|
||||
size_t size = this->rx_buffer_.size();
|
||||
ESP_LOGVV(TAG, "Parsing frames buffer size = %" PRIu32, size);
|
||||
bool retry_as_client = false;
|
||||
@@ -1213,6 +1219,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
|
||||
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
|
||||
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
|
||||
this->deferred_payload_len_ - 1);
|
||||
this->deferred_payload_len_ = 0;
|
||||
if (!this->send_frame_(frame)) {
|
||||
ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked");
|
||||
}
|
||||
|
||||
@@ -26,6 +26,20 @@ uart_mock:
|
||||
rx_timeout: 2
|
||||
auto_start: false
|
||||
debug:
|
||||
# Each burst-case reply from device 1 (single-register FC 0x03, told apart by its register
|
||||
# value) fires its own sensor when it reaches the wire. One sensor per reply, because the API
|
||||
# merges updates to the same entity that land within its batching window.
|
||||
on_tx:
|
||||
- then:
|
||||
- lambda: |-
|
||||
if (data.size() != 7 || data[0] != 0x01 || data[1] != 0x03 || data[3] != 0x00)
|
||||
return;
|
||||
switch (data[4]) {
|
||||
case 0xA1: id(burst_tx_a).publish_state(1); break;
|
||||
case 0xB2: id(burst_tx_b).publish_state(1); break;
|
||||
case 0xC3: id(burst_tx_before_peer).publish_state(1); break;
|
||||
case 0xD4: id(burst_tx_probe).publish_state(1); break;
|
||||
}
|
||||
injections:
|
||||
- delay: 100ms
|
||||
inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read)
|
||||
@@ -52,6 +66,21 @@ uart_mock:
|
||||
- delay: 100ms
|
||||
inject_rx:
|
||||
[0x01, 0x17, 0x00, 0x06, 0x00, 0x01, 0x00, 0x06, 0x00, 0x01, 0x02, 0x56, 0x78, 0x8B, 0x55]
|
||||
# Two reads of device 1 (regs 0x0B then 0x0C) in one injection so both land in the rx buffer
|
||||
# together. The reply to 0x0B is deferred because 0x0C is still queued behind it, and must be
|
||||
# dropped once 0x0C is parsed: only the reply to 0x0C may reach the wire (burst_read_a/b).
|
||||
- delay: 100ms
|
||||
inject_rx: [0x01, 0x03, 0x00, 0x0B, 0x00, 0x01, 0xF5, 0xC8,
|
||||
0x01, 0x03, 0x00, 0x0C, 0x00, 0x01, 0x44, 0x09]
|
||||
# Read of device 1 (reg 0x0D) followed in the same injection by a read of device 2. The client
|
||||
# has moved on to another device, so the deferred reply to 0x0D must never be sent
|
||||
# (burst_read_before_peer).
|
||||
- delay: 100ms
|
||||
inject_rx: [0x01, 0x03, 0x00, 0x0D, 0x00, 0x01, 0x15, 0xC9,
|
||||
0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8]
|
||||
# Plain read of device 1 (reg 0x0E) whose reply on the wire marks the burst cases as settled.
|
||||
- delay: 100ms
|
||||
inject_rx: [0x01, 0x03, 0x00, 0x0E, 0x00, 0x01, 0xE5, 0xC9]
|
||||
|
||||
globals:
|
||||
- id: stored_1
|
||||
@@ -110,6 +139,24 @@ modbus_server:
|
||||
read_lambda: |-
|
||||
id(read_after_peer_timeout).publish_state(1);
|
||||
return 1;
|
||||
- address: 0x0B
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(burst_read_a).publish_state(1);
|
||||
return 0xA1;
|
||||
- address: 0x0C
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(burst_read_b).publish_state(1);
|
||||
return 0xB2;
|
||||
- address: 0x0D
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(burst_read_before_peer).publish_state(1);
|
||||
return 0xC3;
|
||||
- address: 0x0E
|
||||
value_type: U_WORD
|
||||
read_lambda: return 0xD4;
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
@@ -136,6 +183,27 @@ sensor:
|
||||
- platform: template
|
||||
name: "rw_read_3"
|
||||
id: rw_read_3
|
||||
- platform: template
|
||||
name: "burst_read_a"
|
||||
id: burst_read_a
|
||||
- platform: template
|
||||
name: "burst_read_b"
|
||||
id: burst_read_b
|
||||
- platform: template
|
||||
name: "burst_read_before_peer"
|
||||
id: burst_read_before_peer
|
||||
- platform: template
|
||||
name: "burst_tx_a"
|
||||
id: burst_tx_a
|
||||
- platform: template
|
||||
name: "burst_tx_b"
|
||||
id: burst_tx_b
|
||||
- platform: template
|
||||
name: "burst_tx_before_peer"
|
||||
id: burst_tx_before_peer
|
||||
- platform: template
|
||||
name: "burst_tx_probe"
|
||||
id: burst_tx_probe
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
|
||||
@@ -260,11 +260,71 @@ async def test_uart_mock_modbus_server_read_write(
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
# The FC 0x17 injections fire last, behind four earlier 100ms delays
|
||||
# The FC 0x17 injections fire behind four earlier 100ms delays
|
||||
await tracker.await_all(futures, timeout=4.0)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
|
||||
@pytest.mark.shared_yaml("uart_mock_modbus_server_injected")
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus_server_burst(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that a server reply deferred behind a queued frame is dropped.
|
||||
|
||||
Two requests are injected as one chunk so both sit in the rx buffer at
|
||||
once. The reply to the first is deferred because the second is still
|
||||
queued, and must be discarded once the second frame is parsed:
|
||||
* device 1 reg 0x0B then device 1 reg 0x0C -- only the 0x0C reply is sent;
|
||||
* device 1 reg 0x0D then a device 2 request -- nothing is sent.
|
||||
The fixture's on_tx hook fires a sensor per burst reply that reaches the
|
||||
wire, and a final plain read marks both cases settled once its reply is
|
||||
seen.
|
||||
"""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
tracker = SensorTracker(
|
||||
[
|
||||
"burst_read_a",
|
||||
"burst_read_b",
|
||||
"burst_read_before_peer",
|
||||
"burst_tx_a",
|
||||
"burst_tx_b",
|
||||
"burst_tx_before_peer",
|
||||
"burst_tx_probe",
|
||||
]
|
||||
)
|
||||
futures = tracker.expect_all(
|
||||
{
|
||||
"burst_read_a": 1,
|
||||
"burst_read_b": 1,
|
||||
"burst_read_before_peer": 1,
|
||||
"burst_tx_b": 1,
|
||||
"burst_tx_probe": 1,
|
||||
}
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
# Every request is parsed and served by its read_lambda regardless of
|
||||
# whether its reply reaches the wire.
|
||||
await tracker.await_all(futures, timeout=4.0)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
assert not tracker.sensor_states["burst_tx_a"], (
|
||||
"reply to reg 0x0B must be dropped, a later request was queued behind it"
|
||||
)
|
||||
assert not tracker.sensor_states["burst_tx_before_peer"], (
|
||||
"reply to reg 0x0D must be dropped, the client moved on to device 2"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus_server_read_write_invalid(
|
||||
yaml_config: str,
|
||||
|
||||
Reference in New Issue
Block a user