@@ -61,13 +61,18 @@ def __init__(
61
61
host : str ,
62
62
port : int = 502 ,
63
63
slave_id : int = 1 ,
64
+ * ,
64
65
logger : logging .Logger | None = None ,
66
+ enforce_pingable : bool = True ,
65
67
) -> None :
66
68
self .host = host
67
69
self .port = port
68
70
self .slave_id = slave_id
69
71
self .logger = logger
70
72
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
+
71
76
# Unique identifier for this client (used only for logging)
72
77
self ._id = uuid .uuid4 ()
73
78
@@ -377,6 +382,11 @@ async def send_modbus_message(
377
382
if self ._ping_loop is None :
378
383
raise RuntimeError ("Cannot send modbus message on closed TCPModbusClient" )
379
384
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
+
380
390
request_transaction_id = self ._next_transaction_id
381
391
self ._next_transaction_id = (self ._next_transaction_id + 1 ) % MAX_TRANSACTION_ID
382
392
@@ -397,17 +407,35 @@ async def send_modbus_message(
397
407
f"[{ self } ][send_modbus_message] sending request { msg_str } : { request_adu = } "
398
408
)
399
409
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 :
401
422
if self .logger is not None :
402
423
self .logger .debug (
403
424
f"[{ self } ][send_modbus_message] acquired lock to send { msg_str } "
404
425
)
405
426
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
407
432
408
433
try :
409
434
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
411
439
412
440
if self .logger is not None :
413
441
self .logger .debug (f"[{ self } ][send_modbus_message] wrote { msg_str } " )
@@ -422,6 +450,9 @@ async def send_modbus_message(
422
450
423
451
await self .clear_tcp_connection ()
424
452
453
+ # release the lock before retrying (so we can re-get it)
454
+ self ._comms_lock .release ()
455
+
425
456
return await self .send_modbus_message (
426
457
request_function ,
427
458
timeout = timeout ,
@@ -439,9 +470,12 @@ async def send_modbus_message(
439
470
try :
440
471
seen_response_transaction_ids = []
441
472
while True :
473
+ last_time = time .perf_counter ()
442
474
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 ,
444
477
)
478
+ time_budget_remaining -= time .perf_counter () - last_time
445
479
446
480
response_pdu = response_adu [MODBUS_MBAP_SIZE :]
447
481
response_mbap_header = response_adu [:MODBUS_MBAP_SIZE ]
@@ -507,6 +541,8 @@ async def send_modbus_message(
507
541
)
508
542
509
543
return None
544
+ finally :
545
+ self ._comms_lock .release ()
510
546
511
547
mismatch = response_transaction_id != request_transaction_id
512
548
0 commit comments