Skip to content

Commit 12c9f62

Browse files
fix: more correct accounting for timeouts
1 parent bc8efc7 commit 12c9f62

File tree

1 file changed

+40
-4
lines changed

1 file changed

+40
-4
lines changed

tcp_modbus_aio/client.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,18 @@ def __init__(
6161
host: str,
6262
port: int = 502,
6363
slave_id: int = 1,
64+
*,
6465
logger: logging.Logger | None = None,
66+
enforce_pingable: bool = True,
6567
) -> None:
6668
self.host = host
6769
self.port = port
6870
self.slave_id = slave_id
6971
self.logger = logger
7072

73+
# If True, will throw an exception if attempting to send a request and the device is not pingable
74+
self.enforce_pingable = enforce_pingable
75+
7176
# Unique identifier for this client (used only for logging)
7277
self._id = uuid.uuid4()
7378

@@ -377,6 +382,11 @@ async def send_modbus_message(
377382
if self._ping_loop is None:
378383
raise RuntimeError("Cannot send modbus message on closed TCPModbusClient")
379384

385+
if self.enforce_pingable and not await self.is_pingable():
386+
raise ModbusNotConnectedError(
387+
f"Cannot send modbus message to {self.host} because it is not pingable"
388+
)
389+
380390
request_transaction_id = self._next_transaction_id
381391
self._next_transaction_id = (self._next_transaction_id + 1) % MAX_TRANSACTION_ID
382392

@@ -397,17 +407,35 @@ async def send_modbus_message(
397407
f"[{self}][send_modbus_message] sending request {msg_str}: {request_adu=}"
398408
)
399409

400-
async with self._comms_lock:
410+
time_budget_remaining = timeout if timeout is not None else float("inf")
411+
412+
last_time = time.perf_counter()
413+
try:
414+
await asyncio.wait_for(self._comms_lock.acquire(), time_budget_remaining)
415+
except asyncio.TimeoutError:
416+
raise ModbusCommunicationFailureError(
417+
f"Failed to acquire lock to send request {msg_str} to modbus device {self.host}"
418+
)
419+
time_budget_remaining -= time.perf_counter() - last_time
420+
421+
try:
401422
if self.logger is not None:
402423
self.logger.debug(
403424
f"[{self}][send_modbus_message] acquired lock to send {msg_str}"
404425
)
405426

406-
reader, writer = await self._get_tcp_connection(timeout=timeout)
427+
last_time = time.perf_counter()
428+
reader, writer = await self._get_tcp_connection(
429+
timeout=time_budget_remaining
430+
)
431+
time_budget_remaining -= time.perf_counter() - last_time
407432

408433
try:
409434
writer.write(request_adu)
410-
await asyncio.wait_for(writer.drain(), timeout)
435+
436+
last_time = time.perf_counter()
437+
await asyncio.wait_for(writer.drain(), time_budget_remaining)
438+
time_budget_remaining -= time.perf_counter() - last_time
411439

412440
if self.logger is not None:
413441
self.logger.debug(f"[{self}][send_modbus_message] wrote {msg_str}")
@@ -422,6 +450,9 @@ async def send_modbus_message(
422450

423451
await self.clear_tcp_connection()
424452

453+
# release the lock before retrying (so we can re-get it)
454+
self._comms_lock.release()
455+
425456
return await self.send_modbus_message(
426457
request_function,
427458
timeout=timeout,
@@ -439,9 +470,12 @@ async def send_modbus_message(
439470
try:
440471
seen_response_transaction_ids = []
441472
while True:
473+
last_time = time.perf_counter()
442474
response_adu = await asyncio.wait_for(
443-
reader.read(expected_response_size), timeout=timeout
475+
reader.read(expected_response_size),
476+
timeout=time_budget_remaining,
444477
)
478+
time_budget_remaining -= time.perf_counter() - last_time
445479

446480
response_pdu = response_adu[MODBUS_MBAP_SIZE:]
447481
response_mbap_header = response_adu[:MODBUS_MBAP_SIZE]
@@ -507,6 +541,8 @@ async def send_modbus_message(
507541
)
508542

509543
return None
544+
finally:
545+
self._comms_lock.release()
510546

511547
mismatch = response_transaction_id != request_transaction_id
512548

0 commit comments

Comments
 (0)