Skip to content

Commit

Permalink
Add ZMQConnection class (Currently only supports DEALER and SUB types)
Browse files Browse the repository at this point in the history
  • Loading branch information
OCopping committed Mar 6, 2024
1 parent 9b2414c commit 9210c5c
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 1 deletion.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ classifiers = [
]
description = "Control system agnostic framework for building Device support in Python that will work for both EPICS and Tango"
dependencies = [
"aiozmq",
"numpy",
"pydantic",
"pvi~=0.7.1",
"softioc",
"zmq",
] # Add project dependencies here, e.g. ["click", "numpy"]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down
3 changes: 2 additions & 1 deletion src/fastcs/connections/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .ip_connection import IPConnection
from .zmq_connection import ZMQConnection

__all__ = ["IPConnection"]
__all__ = ["IPConnection", "ZMQConnection"]
175 changes: 175 additions & 0 deletions src/fastcs/connections/zmq_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""ZeroMQ adapter for use in a stream device."""

import asyncio
from dataclasses import dataclass
from typing import Iterable, List, Optional

import aiozmq
import zmq


@dataclass
class ZMQConnection:
"""An adapter for a ZeroMQ data stream."""

zmq_host: str = "127.0.0.1"
zmq_port: int = 5555
zmq_type: int = zmq.DEALER
running: bool = False

def get_setup(self) -> None:
"""Print out the current configuration."""
print(

Check warning on line 22 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L22

Added line #L22 was not covered by tests
f"""
Host: {self.zmq_host}
Port: {self.zmq_port}
Type: {self.zmq_type.name}
Running: {self.running}
"""
)

async def start_stream(self) -> None:
"""Start the ZeroMQ stream."""
print("starting stream...")

Check warning on line 33 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L33

Added line #L33 was not covered by tests

self._socket = await aiozmq.create_zmq_stream(

Check warning on line 35 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L35

Added line #L35 was not covered by tests
self.zmq_type, connect=f"tcp://{self.zmq_host}:{self.zmq_port}"
) # type: ignore
if self.zmq_type == zmq.SUB:
self._socket.transport.setsockopt(zmq.SUBSCRIBE, b"")
self._socket.transport.setsockopt(zmq.LINGER, 0)

Check warning on line 40 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L38-L40

Added lines #L38 - L40 were not covered by tests

print(f"Stream started. {self._socket}")

Check warning on line 42 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L42

Added line #L42 was not covered by tests

async def close_stream(self) -> None:
"""Close the ZeroMQ stream."""
self._socket.close()

Check warning on line 46 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L46

Added line #L46 was not covered by tests

self.running = False

Check warning on line 48 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L48

Added line #L48 was not covered by tests

def send_message(self, message: List[bytes]) -> None:
"""
Send a message down the ZeroMQ stream.
Sets up an asyncio task to put the message on the message queue, before
being processed.
Args:
message (str): The message to send down the ZeroMQ stream.
"""
self._send_message_queue.put_nowait(message)

Check warning on line 60 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L60

Added line #L60 was not covered by tests

async def _read_response(self) -> Optional[bytes]:
"""
Read and return a response once received on the socket.
Returns:
Optional[bytes]: If received, a response is returned, else None
"""
if self.zmq_type is not zmq.DEALER:
try:
resp = await asyncio.wait_for(self._socket.read(), timeout=20)
return resp[0]
except asyncio.TimeoutError:
pass

Check warning on line 74 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L69-L74

Added lines #L69 - L74 were not covered by tests
else:
discard = True
while discard:
try:
multipart_resp = await asyncio.wait_for(

Check warning on line 79 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L76-L79

Added lines #L76 - L79 were not covered by tests
self._socket.read(), timeout=20
)
if multipart_resp[0] == b"":
discard = False
resp = multipart_resp[1]
return resp
except asyncio.TimeoutError:
pass
return None

Check warning on line 88 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L82-L88

Added lines #L82 - L88 were not covered by tests

async def get_response(self) -> bytes:
"""
Get response from the received message queue.
Returns:
bytes: Received response message
"""
return await self._recv_message_queue.get()

Check warning on line 97 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L97

Added line #L97 was not covered by tests

async def run_forever(self) -> None:
"""Run the ZeroMQ adapter continuously."""
self._send_message_queue: asyncio.Queue = asyncio.Queue()
self._recv_message_queue: asyncio.Queue = asyncio.Queue()

Check warning on line 102 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L101-L102

Added lines #L101 - L102 were not covered by tests

try:
if getattr(self, "_socket", None) is None:
await self.start_stream()
except Exception as e:
print("Exception when starting stream:", e)

Check warning on line 108 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L104-L108

Added lines #L104 - L108 were not covered by tests

self.running = True

Check warning on line 110 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L110

Added line #L110 was not covered by tests

if self.zmq_type == zmq.DEALER:
await asyncio.gather(

Check warning on line 113 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L112-L113

Added lines #L112 - L113 were not covered by tests
*[
self._process_message_queue(),
self._process_response_queue(),
]
)
elif self.zmq_type == zmq.SUB:
await asyncio.gather(

Check warning on line 120 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L119-L120

Added lines #L119 - L120 were not covered by tests
*[
self._process_response_queue(),
]
)

def check_if_running(self):
"""Return the running state of the adapter."""
return self.running

Check warning on line 128 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L128

Added line #L128 was not covered by tests

async def _process_message_queue(self) -> None:
"""Process message queue for sending messages over the ZeroMQ stream."""
print("Processing message queue...")
running = True
while running:
message = await self._send_message_queue.get()
await self._process_message(message)
running = self.check_if_running()

Check warning on line 137 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L132-L137

Added lines #L132 - L137 were not covered by tests

async def _process_message(self, message: Iterable[bytes]) -> None:
"""Process message to send over the ZeroMQ stream.
Args:
message (Iterable[bytes]): Message to send over the ZeroMQ stream.
"""
if message is not None:
if not self._socket._closing:
try:
if self.zmq_type is not zmq.DEALER:
self._socket.write(message)

Check warning on line 149 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L145-L149

Added lines #L145 - L149 were not covered by tests
else:
self._socket._transport._zmq_sock.send(b"", flags=zmq.SNDMORE)
self._socket.write(message)
except zmq.error.ZMQError as e:
print("ZMQ Error", e)
await asyncio.sleep(1)
except Exception as e:
print(f"Error, {e}")
print("Unable to write to ZMQ stream, trying again...")
await asyncio.sleep(1)

Check warning on line 159 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L151-L159

Added lines #L151 - L159 were not covered by tests
else:
print("Socket closed...")
await asyncio.sleep(5)

Check warning on line 162 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L161-L162

Added lines #L161 - L162 were not covered by tests
else:
print("No message")

Check warning on line 164 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L164

Added line #L164 was not covered by tests

async def _process_response_queue(self) -> None:
"""Process response message queue from the ZeroMQ stream."""
print("Processing response queue...")
running = True
while running:
resp = await self._read_response()
if resp is None:
continue
self._recv_message_queue.put_nowait(resp)
running = self.check_if_running()

Check warning on line 175 in src/fastcs/connections/zmq_connection.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/connections/zmq_connection.py#L168-L175

Added lines #L168 - L175 were not covered by tests

0 comments on commit 9210c5c

Please sign in to comment.