Skip to content

Commit

Permalink
feat: is_pingable check method and error on closed client
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuagruenstein committed Apr 21, 2024
1 parent ea6f3a9 commit 30aa42e
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 7 deletions.
2 changes: 1 addition & 1 deletion examples/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from umodbus.functions import ReadCoils

from tcp_modbus_aio.connection import TCPModbusClient
from tcp_modbus_aio.client import TCPModbusClient


async def example() -> None:
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ style = "poetry_scripts:style"
[tool.semantic_release]
version_variables = ["tcp_modbus_aio/__init__.py:__version__"]
version_toml = ["pyproject.toml:tool.poetry.version"]
build_command = "pip install poetry && poetry build"
build_command = "pip install poetry && poetry build"

[tool.isort]
profile = "black"
52 changes: 47 additions & 5 deletions tcp_modbus_aio/connection.py → tcp_modbus_aio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def __init__(
name=f"TCPModbusClient._ping_loop_task[{self.host}:{self.port}]",
)

# Event that is set when the first ping is received
self._first_ping_event: asyncio.Event = asyncio.Event()

# List of CoilWatchStatus objects that are being logged
self._log_watches = list[CoilWatchStatus]()

Expand Down Expand Up @@ -120,9 +123,11 @@ def __repr__(self) -> str:
async def _ping_loop_task(self) -> None:
while True:
self._last_ping = await ping_ip(self.host)

if self.logger is not None:
self.logger.debug(f"[{self}][_ping_loop_task] ping ping ping")

self._first_ping_event.set()
await asyncio.sleep(self.PING_LOOP_PERIOD)

async def _get_tcp_connection(
Expand Down Expand Up @@ -204,6 +209,13 @@ async def __aexit__(
await self.close()

async def close(self) -> None:
"""
Permanent close of the TCP connection and ping loop. Only call this on final destruction of the object.
"""

if self._ping_loop is None:
return

await self.clear_tcp_connection()

if self._ping_loop is not None:
Expand All @@ -223,6 +235,9 @@ def log_watch(
is called.
"""

if self._ping_loop is None:
raise RuntimeError("Cannot log watch on closed TCPModbusClient")

for watch in self._log_watches:
if watch.memo_key == memo_key:
watch.expiry = time.perf_counter() + period
Expand Down Expand Up @@ -278,6 +293,14 @@ async def _watch_loop() -> None:
)

async def clear_tcp_connection(self) -> None:
"""
Closes the current TCP connection and clears the reader and writer objects.
On the next send_modbus_message call, a new connection will be created.
"""

if self._ping_loop is None:
raise RuntimeError("Cannot clear TCP connection on closed TCPModbusClient")

if self._writer is not None:
if self.logger is not None:
self.logger.warning(
Expand All @@ -294,9 +317,13 @@ async def test_connection(
self, timeout: float | None = DEFAULT_MODBUS_TIMEOUT_SEC
) -> None:
"""
Tests the connection to the device by sending a ReadCoil message (see TEST_CONNECTION_MESSAGE)
Uses a cached awaitable to prevent spamming the connection on this call
"""

if self._ping_loop is None:
raise RuntimeError("Cannot test connection on closed TCPModbusClient")

try:
if self._active_connection_probe is None:
self._active_connection_probe = asyncio.create_task(
Expand All @@ -308,19 +335,34 @@ async def test_connection(
finally:
self._active_connection_probe = None

async def is_pingable(self) -> bool:
"""
Returns True if the device is pingable, False if not.
Will wait for the first ping to be received (or timeout) before returning.
"""

if self._ping_loop is None:
raise RuntimeError("Cannot check pingability on closed TCPModbusClient")

if not self._first_ping_event.is_set():
await self._first_ping_event.wait()

return self._last_ping is not None

async def send_modbus_message(
self,
request_function: ModbusFunctionT,
timeout: float | None = DEFAULT_MODBUS_TIMEOUT_SEC,
retries: int = 1,
error_on_no_response: bool = True,
) -> ModbusFunctionT | None:
"""Send ADU over socket to to server and return parsed response.
:param adu: Request ADU.
:param sock: Socket instance.
:return: Parsed response from server.
"""
Sends a modbus message to the device and returns the response.
Will create a new TCP connection if one does not exist.
"""

if self._ping_loop is None:
raise RuntimeError("Cannot send modbus message on closed TCPModbusClient")

request_transaction_id = self._next_transaction_id
self._next_transaction_id = (self._next_transaction_id + 1) % MAX_TRANSACTION_ID
Expand Down

0 comments on commit 30aa42e

Please sign in to comment.