Skip to content

Commit 30aa42e

Browse files
feat: is_pingable check method and error on closed client
1 parent ea6f3a9 commit 30aa42e

File tree

3 files changed

+52
-7
lines changed

3 files changed

+52
-7
lines changed

examples/example.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from umodbus.functions import ReadCoils
44

5-
from tcp_modbus_aio.connection import TCPModbusClient
5+
from tcp_modbus_aio.client import TCPModbusClient
66

77

88
async def example() -> None:

pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ style = "poetry_scripts:style"
3030
[tool.semantic_release]
3131
version_variables = ["tcp_modbus_aio/__init__.py:__version__"]
3232
version_toml = ["pyproject.toml:tool.poetry.version"]
33-
build_command = "pip install poetry && poetry build"
33+
build_command = "pip install poetry && poetry build"
34+
35+
[tool.isort]
36+
profile = "black"

tcp_modbus_aio/connection.py tcp_modbus_aio/client.py

+47-5
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def __init__(
8181
name=f"TCPModbusClient._ping_loop_task[{self.host}:{self.port}]",
8282
)
8383

84+
# Event that is set when the first ping is received
85+
self._first_ping_event: asyncio.Event = asyncio.Event()
86+
8487
# List of CoilWatchStatus objects that are being logged
8588
self._log_watches = list[CoilWatchStatus]()
8689

@@ -120,9 +123,11 @@ def __repr__(self) -> str:
120123
async def _ping_loop_task(self) -> None:
121124
while True:
122125
self._last_ping = await ping_ip(self.host)
126+
123127
if self.logger is not None:
124128
self.logger.debug(f"[{self}][_ping_loop_task] ping ping ping")
125129

130+
self._first_ping_event.set()
126131
await asyncio.sleep(self.PING_LOOP_PERIOD)
127132

128133
async def _get_tcp_connection(
@@ -204,6 +209,13 @@ async def __aexit__(
204209
await self.close()
205210

206211
async def close(self) -> None:
212+
"""
213+
Permanent close of the TCP connection and ping loop. Only call this on final destruction of the object.
214+
"""
215+
216+
if self._ping_loop is None:
217+
return
218+
207219
await self.clear_tcp_connection()
208220

209221
if self._ping_loop is not None:
@@ -223,6 +235,9 @@ def log_watch(
223235
is called.
224236
"""
225237

238+
if self._ping_loop is None:
239+
raise RuntimeError("Cannot log watch on closed TCPModbusClient")
240+
226241
for watch in self._log_watches:
227242
if watch.memo_key == memo_key:
228243
watch.expiry = time.perf_counter() + period
@@ -278,6 +293,14 @@ async def _watch_loop() -> None:
278293
)
279294

280295
async def clear_tcp_connection(self) -> None:
296+
"""
297+
Closes the current TCP connection and clears the reader and writer objects.
298+
On the next send_modbus_message call, a new connection will be created.
299+
"""
300+
301+
if self._ping_loop is None:
302+
raise RuntimeError("Cannot clear TCP connection on closed TCPModbusClient")
303+
281304
if self._writer is not None:
282305
if self.logger is not None:
283306
self.logger.warning(
@@ -294,9 +317,13 @@ async def test_connection(
294317
self, timeout: float | None = DEFAULT_MODBUS_TIMEOUT_SEC
295318
) -> None:
296319
"""
320+
Tests the connection to the device by sending a ReadCoil message (see TEST_CONNECTION_MESSAGE)
297321
Uses a cached awaitable to prevent spamming the connection on this call
298322
"""
299323

324+
if self._ping_loop is None:
325+
raise RuntimeError("Cannot test connection on closed TCPModbusClient")
326+
300327
try:
301328
if self._active_connection_probe is None:
302329
self._active_connection_probe = asyncio.create_task(
@@ -308,19 +335,34 @@ async def test_connection(
308335
finally:
309336
self._active_connection_probe = None
310337

338+
async def is_pingable(self) -> bool:
339+
"""
340+
Returns True if the device is pingable, False if not.
341+
Will wait for the first ping to be received (or timeout) before returning.
342+
"""
343+
344+
if self._ping_loop is None:
345+
raise RuntimeError("Cannot check pingability on closed TCPModbusClient")
346+
347+
if not self._first_ping_event.is_set():
348+
await self._first_ping_event.wait()
349+
350+
return self._last_ping is not None
351+
311352
async def send_modbus_message(
312353
self,
313354
request_function: ModbusFunctionT,
314355
timeout: float | None = DEFAULT_MODBUS_TIMEOUT_SEC,
315356
retries: int = 1,
316357
error_on_no_response: bool = True,
317358
) -> ModbusFunctionT | None:
318-
"""Send ADU over socket to to server and return parsed response.
319-
320-
:param adu: Request ADU.
321-
:param sock: Socket instance.
322-
:return: Parsed response from server.
323359
"""
360+
Sends a modbus message to the device and returns the response.
361+
Will create a new TCP connection if one does not exist.
362+
"""
363+
364+
if self._ping_loop is None:
365+
raise RuntimeError("Cannot send modbus message on closed TCPModbusClient")
324366

325367
request_transaction_id = self._next_transaction_id
326368
self._next_transaction_id = (self._next_transaction_id + 1) % MAX_TRANSACTION_ID

0 commit comments

Comments
 (0)