Visitar URL original
Add optional inter-request delay to sync and async serial clients by Bingdom · Pull Request #3081 · pymodbus-dev/pymodbus · GitHub
Skip to content

Add optional inter-request delay to sync and async serial clients - #3081

Open
Bingdom wants to merge 14 commits into
pymodbus-dev:devfrom
Bingdom:dev
Open

Bingdom wants to merge 14 commits into
pymodbus-dev:devfrom
Bingdom:dev

Conversation

@Bingdom

@Bingdom Bingdom commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Some serial devices respond successfully but become temporarily unresponsive to an immediate follow-up request. For example, writing a relay and immediately reading its state can cause intermittent timeouts, even when normal polling succeeds.

Add inter_request_delay to ModbusSerialClient and AsyncModbusSerialClient. The value is in seconds and defaults to 0.0, preserving existing timing.

from pymodbus.client import ModbusSerialClient

with ModbusSerialClient(
    "/dev/ttyUSB0",
    baudrate=115200,
    inter_request_delay=0.020,
) as client:
    client.write_coil(0, True, device_id=1)
    client.read_coils(0, count=1, device_id=1)

Here, readback waits until at least 20 ms after the write transaction finishes. The appropriate interval depends on the device.

Spacing is enforced under the transaction lock, including retries after timeouts. The first request is immediate, existing idle time counts toward the interval, and async waits yield to the event loop.

Tests cover both clients, default behavior, response timing, retries, failures, concurrent callers, cancellation, and parameter validation.

@janiversen

Copy link
Copy Markdown
Collaborator

Before I dive into details, I want to point out that your example seems incorrect, if you write a value, the resulting value is returned directly in the response:

Captura de pantalla 2026-11-04 a las 7 48 29

However it is true that some devices need a delay between calls.

@janiversen janiversen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is quite ok, but the implementation have a side effect on other connected devices, which cannot be what you intended.

The PR also includes some general (but fundamental) changes, which for regression test reasons should be submitted as separate PRs.

Finally monkeypatch() is introduced, adding to the maintenance load, if really needed please explain why mock() is insufficient.

Comment thread pymodbus/client/serial.py
from __future__ import annotations

import contextlib
import math

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems very strange that the math is needed.

@Bingdom Bingdom Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @janiversen thank you for reviewing the code.

For the moment, we need to check if the input is a finite number. We can strip out this check if you consider this level of input validation is out of scope. However, I would suggest the lower-bound check should remain.

Comment thread pymodbus/client/serial.py Outdated
)
if framer not in [FramerType.ASCII, FramerType.RTU]:
raise TypeError("Only FramerType RTU/ASCII allowed.")
if not math.isfinite(inter_request_delay) or inter_request_delay < 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inter_request_delay is defined as a float, so just check it is a float, which probably will cause problems with the type checker.

If it was defined as "float | None", this check would make sense.

"< 0" is comparing against an int not a float ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will switch it out to float comparisons for consistency, no problem

@Bingdom Bingdom Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both NaN and infinity are valid float types. So we have the finite check there for that.

Comment thread pymodbus/client/serial.py
if framer not in [FramerType.ASCII, FramerType.RTU]:
raise TypeError("Only FramerType RTU/ASCII allowed.")
if not math.isfinite(inter_request_delay) or inter_request_delay < 0:
raise ValueError("inter_request_delay must be finite and non-negative")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it must be a float (which includes int) according to the typing

Comment thread pymodbus/client/serial.py Outdated
)
if framer not in [FramerType.ASCII, FramerType.RTU]:
raise TypeError("Only RTU/ASCII allowed.")
if not math.isfinite(inter_request_delay) or inter_request_delay < 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inter_request_delay is defined as a float, so just check it is a float, which probably will cause problems with the type checker.

If it was defined as "float | None", this check would make sense.

"< 0" is comparing against an int not a float ?

Comment thread pymodbus/transaction/transaction.py Outdated
or self._last_transaction_end is None
):
return 0.0
return max(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a lot cheaper to calculate the difference (as you do) store it in a variable and add it the the if above)

async def test_no_response_and_independent_clients(clock, monkeypatch, sync):
"""No-response sends are paced without sharing deadlines between clients."""
_, waits = clock
first, second = manager(sync, 0.02), manager(sync, 0.02)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a bad test, now add a test with 1 serial device, with 2 devices connected.

async def test_async_concurrent_callers(clock, monkeypatch):
"""Queued callers cannot send during another caller's idle interval."""
now, waits = clock
transact = manager(False, 0.02)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is wrong, different callers should be able to send to other devices.

async def test_sync_concurrent_callers(clock, monkeypatch):
"""Threads sharing a client retain spacing while queued on its lock."""
now, waits = clock
transact = manager(True, 0.02)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync pr definition cannot have concurrent callers !

sleeping.set()
await asyncio.Future()

monkeypatch.setattr(transaction_module.asyncio, "sleep", sleep)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do NOT, patch system libraries.

monkeypatch.setattr(transaction_module.asyncio, "sleep", finish_sleep)
await execute(transact, False, True)
assert send.call_count == 2
assert waits == pytest.approx([0.01])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general comment, all other tests uses mock(), not monkeypatch() so you need some convincing arguments why this is needed.

I am not against monkeypatch() but having 2 different mocks adds to the maintenance burden.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I've made it similar to existing tests.

@janiversen janiversen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed a non correct comment.

@Bingdom

Bingdom commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Tests are failing because monotonic() in <3.13 is imprecise in windows environments. I'll move my implementation to perf_counter(), and leave the existing codebase with monotonic().

I can migrate the rest of the code to perf_counter() in a separate PR if you prefer, as of right now it solves a separate issue.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants