mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
[modbus] Only apply turnaround delay after broadcasts (#17209)
This commit is contained in:
@@ -92,10 +92,14 @@ int32_t Modbus::tx_delay_remaining() {
|
||||
|
||||
int32_t ModbusClientHub::tx_delay_remaining() {
|
||||
const uint32_t now = millis();
|
||||
return std::max({(int32_t) 0,
|
||||
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
|
||||
(now - this->last_send_)),
|
||||
(int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
|
||||
// Turnaround delay only applies after a broadcast: no response is expected, so we must give listening devices
|
||||
// quiet time to process it before the next request. For normal unicast request/response the received reply already
|
||||
// provides the inter-frame timing, so adding turnaround there just throttles throughput.
|
||||
const uint16_t turnaround = this->last_send_was_broadcast_ ? this->turnaround_delay_ms_ : 0;
|
||||
return std::max(
|
||||
{(int32_t) 0,
|
||||
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + turnaround - (now - this->last_send_)),
|
||||
(int32_t) (this->frame_delay_ms_ + turnaround - (now - this->last_modbus_byte_))});
|
||||
}
|
||||
|
||||
bool Modbus::tx_blocked() {
|
||||
@@ -396,6 +400,7 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
|
||||
format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_,
|
||||
now - this->last_modbus_byte_);
|
||||
this->last_send_ = now;
|
||||
this->last_send_was_broadcast_ = frame.size > 0 && frame.data[0] == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -411,7 +416,8 @@ void ModbusClientHub::send_next_frame_() {
|
||||
ModbusDeviceCommand &command = this->tx_buffer_.front();
|
||||
|
||||
if (this->send_frame_(command.frame)) {
|
||||
this->waiting_for_response_ = std::move(command);
|
||||
if (!this->last_send_was_broadcast_)
|
||||
this->waiting_for_response_ = std::move(command);
|
||||
} else {
|
||||
if (command.device)
|
||||
command.device->on_modbus_not_sent();
|
||||
|
||||
@@ -63,6 +63,7 @@ class Modbus : public uart::UARTDevice, public Component {
|
||||
uint32_t last_receive_check_{0};
|
||||
uint32_t last_send_{0};
|
||||
uint32_t last_send_tx_offset_{0};
|
||||
bool last_send_was_broadcast_{false};
|
||||
uint16_t frame_delay_ms_{5};
|
||||
uint16_t long_rx_buffer_delay_ms_{0};
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-bcast
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# No on_tx injection: a broadcast (address 0) gets no reply on a real bus.
|
||||
uart_mock:
|
||||
- id: virtual_uart
|
||||
baud_rate: 9600
|
||||
auto_start: true
|
||||
debug:
|
||||
|
||||
modbus:
|
||||
- uart_id: virtual_uart
|
||||
id: virtual_modbus
|
||||
role: client
|
||||
send_wait_time: 200ms
|
||||
turnaround_time: 10ms
|
||||
|
||||
modbus_controller:
|
||||
- address: 0
|
||||
modbus_id: virtual_modbus
|
||||
update_interval: 60s
|
||||
id: modbus_controller_bcast
|
||||
|
||||
number:
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_bcast
|
||||
id: bcast_write
|
||||
name: "bcast_write"
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
min_value: 0
|
||||
max_value: 65535
|
||||
|
||||
interval:
|
||||
- interval: 400ms
|
||||
then:
|
||||
- number.set:
|
||||
id: bcast_write
|
||||
value: 42
|
||||
@@ -330,3 +330,28 @@ async def test_uart_mock_modbus_server_controller_multiple(
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
await tracker.await_all(futures)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus_broadcast(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that broadcast writes (address 0) don't wait for a response.
|
||||
|
||||
A controller at address 0 sends broadcast writes that get no reply. The
|
||||
client must not arm the response timeout for them: otherwise every write
|
||||
blocks for send_wait_time and logs a spurious "no response from 0" warning.
|
||||
"""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected(),
|
||||
):
|
||||
# Several broadcast writes fire on the 400ms interval; send_wait_time is
|
||||
# 200ms, so the old behaviour would have warned on each one by now.
|
||||
await asyncio.sleep(3.0)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
Reference in New Issue
Block a user