diff --git a/api/src/opentrons/hardware_control/emulation/module_server/__init__.py b/api/src/opentrons/hardware_control/emulation/module_server/__init__.py index fd9f7229550..082295dbfc7 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/__init__.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/__init__.py @@ -1,8 +1,8 @@ +"""Package for the module status server.""" from .server import ModuleStatusServer -from .client import ModuleServerClient - +from .client import ModuleStatusClient __all__ = [ "ModuleStatusServer", - "ModuleServerClient", + "ModuleStatusClient", ] diff --git a/api/src/opentrons/hardware_control/emulation/module_server/client.py b/api/src/opentrons/hardware_control/emulation/module_server/client.py index 4749ef68f10..b1051861fc6 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/client.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/client.py @@ -11,7 +11,7 @@ class ModuleServerClientError(Exception): pass -class ModuleServerClient: +class ModuleStatusClient: """A module server client.""" def __init__( @@ -28,7 +28,7 @@ async def connect( port: int, retries: int = 3, interval_seconds: float = 0.1, - ) -> ModuleServerClient: + ) -> ModuleStatusClient: """Connect to the module server. Args: @@ -52,7 +52,7 @@ async def connect( await asyncio.sleep(interval_seconds) if r is not None and w is not None: - return ModuleServerClient(reader=r, writer=w) + return ModuleStatusClient(reader=r, writer=w) else: raise IOError( f"Failed to connect to module_server at after {retries} retries." diff --git a/api/src/opentrons/hardware_control/emulation/module_server/helpers.py b/api/src/opentrons/hardware_control/emulation/module_server/helpers.py index cd075a91727..ad4e7ffbabe 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/helpers.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/helpers.py @@ -1,9 +1,11 @@ +"""Utililty methods and classes for interacting with the Module Status Server.""" + import asyncio from typing import Sequence, Set, Callable, List, Awaitable from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.hardware_control.emulation.module_server.client import ( - ModuleServerClient, + ModuleStatusClient, ModuleServerClientError, ) from opentrons.hardware_control.emulation.module_server.models import Message @@ -20,7 +22,7 @@ async def listen_module_connection(callback: NotifyMethod) -> None: """Listen for module emulator connections.""" settings = Settings() try: - client = await ModuleServerClient.connect( + client = await ModuleStatusClient.connect( host=settings.module_server.host, port=settings.module_server.port, interval_seconds=1.0, @@ -34,7 +36,7 @@ async def listen_module_connection(callback: NotifyMethod) -> None: class ModuleListener: """Provide a callback for listening for new and removed module connections.""" - def __init__(self, client: ModuleServerClient, notify_method: NotifyMethod) -> None: + def __init__(self, client: ModuleStatusClient, notify_method: NotifyMethod) -> None: """Constructor. Args: @@ -88,7 +90,7 @@ def _next_index() -> int: async def wait_emulators( - client: ModuleServerClient, + client: ModuleStatusClient, modules: Sequence[ModuleType], timeout: float, ) -> None: @@ -105,7 +107,7 @@ async def wait_emulators( asyncio.TimeoutError on timeout. """ - async def _wait_modules(cl: ModuleServerClient, module_set: Set[str]) -> None: + async def _wait_modules(cl: ModuleStatusClient, module_set: Set[str]) -> None: """Read messages from module server waiting for modules in module_set to be connected.""" while module_set: diff --git a/api/src/opentrons/hardware_control/emulation/module_server/models.py b/api/src/opentrons/hardware_control/emulation/module_server/models.py index f2c4b0c598e..05363bfc109 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/models.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/models.py @@ -2,19 +2,30 @@ from typing_extensions import Literal -from pydantic import BaseModel +from pydantic import BaseModel, Field -class Connection(BaseModel): +class ModuleConnection(BaseModel): """Model a single module connection.""" - url: str - module_type: str - identifier: str + url: str = Field( + ..., + description="The url (port) value the module driver should connect to. " + "For example: socket://host:port", + ) + module_type: str = Field( + ..., description="What kind of module this connection emulates." + ) + identifier: str = Field(..., description="Unique id for this emulator.") class Message(BaseModel): """A message sent to module server clients.""" - status: Literal["connected", "disconnected", "dump"] - connections: List[Connection] + status: Literal["connected", "disconnected", "dump"] = Field( + ..., + description="`dump` includes a complete list of connected emulators. " + "`connected` for new connections. `disconnected` for emulators " + "that have disconnected. ", + ) + connections: List[ModuleConnection] diff --git a/api/src/opentrons/hardware_control/emulation/module_server/server.py b/api/src/opentrons/hardware_control/emulation/module_server/server.py index 13da6410392..5a3d696eb7b 100644 --- a/api/src/opentrons/hardware_control/emulation/module_server/server.py +++ b/api/src/opentrons/hardware_control/emulation/module_server/server.py @@ -1,9 +1,10 @@ +"""Server notifying of module connections.""" import asyncio import logging from typing import Dict, Set from opentrons.hardware_control.emulation.module_server.models import ( - Connection, + ModuleConnection, Message, ) from opentrons.hardware_control.emulation.proxy import ProxyListener @@ -16,7 +17,11 @@ class ModuleStatusServer(ProxyListener): - """Server notifying of module connections.""" + """The module status server is the emulator equivalent of inotify. A client + can know when an emulated module connects or disconnects. + + Clients connect and read JSON messages (See models module). + """ def __init__(self, settings: ModuleServerSettings) -> None: """Constructor @@ -25,7 +30,7 @@ def __init__(self, settings: ModuleServerSettings) -> None: settings: app settings """ self._settings = settings - self._connections: Dict[str, Connection] = {} + self._connections: Dict[str, ModuleConnection] = {} self._clients: Set[asyncio.StreamWriter] = set() def on_server_connected( @@ -42,7 +47,7 @@ def on_server_connected( """ log.info(f"On connected {server_type} {client_uri} {identifier}") - connection = Connection( + connection = ModuleConnection( module_type=server_type, url=client_uri, identifier=identifier ) self._connections[identifier] = connection @@ -87,6 +92,7 @@ async def _handle_connection( """Handle a client connection to the server.""" log.info("Client connected to module server.") + # A client connected. Send a dump of all connected modules. m = Message(status="dump", connections=list(self._connections.values())) writer.write(m.json().encode()) diff --git a/api/src/opentrons/hardware_control/emulation/proxy.py b/api/src/opentrons/hardware_control/emulation/proxy.py index 3ebc94a961a..0dfbb2e2eb6 100644 --- a/api/src/opentrons/hardware_control/emulation/proxy.py +++ b/api/src/opentrons/hardware_control/emulation/proxy.py @@ -1,3 +1,4 @@ +"""The Proxy class module.""" from __future__ import annotations import asyncio import logging @@ -14,7 +15,7 @@ @dataclass(frozen=True) class Connection: - """A connection.""" + """Attributes of a client connected on the server port (module emulator).""" identifier: str reader: asyncio.StreamReader @@ -22,6 +23,9 @@ class Connection: class ProxyListener(ABC): + """Interface defining an object needing to know when a server (module emulator) + connected/disconnected.""" + @abstractmethod def on_server_connected( self, server_type: str, client_uri: str, identifier: str diff --git a/api/tests/opentrons/hardware_control/emulation/module_server/test_helpers.py b/api/tests/opentrons/hardware_control/emulation/module_server/test_helpers.py index a8852860db6..441917b8236 100644 --- a/api/tests/opentrons/hardware_control/emulation/module_server/test_helpers.py +++ b/api/tests/opentrons/hardware_control/emulation/module_server/test_helpers.py @@ -5,7 +5,7 @@ from opentrons.drivers.rpi_drivers.types import USBPort from opentrons.hardware_control.emulation.module_server import ( helpers, - ModuleServerClient, + ModuleStatusClient, ) from opentrons.hardware_control.emulation.module_server import models from opentrons.hardware_control.modules import ModuleAtPort @@ -20,7 +20,7 @@ def mock_callback() -> AsyncMock: @pytest.fixture def mock_client() -> AsyncMock: """Mock client.""" - return AsyncMock(spec=ModuleServerClient) + return AsyncMock(spec=ModuleStatusClient) @pytest.fixture @@ -30,10 +30,10 @@ def subject(mock_callback: AsyncMock, mock_client: AsyncMock) -> helpers.ModuleL @pytest.fixture -def connections() -> List[models.Connection]: +def connections() -> List[models.ModuleConnection]: """Connection models.""" return [ - models.Connection( + models.ModuleConnection( url=f"url{i}", module_type=f"module_type{i}", identifier=f"identifier{i}" ) for i in range(5) @@ -65,7 +65,7 @@ async def test_handle_message_connected_empty( async def test_handle_message_connected_one( subject: helpers.ModuleListener, mock_callback: AsyncMock, - connections: List[models.Connection], + connections: List[models.ModuleConnection], modules_at_port: List[ModuleAtPort], ) -> None: """It should call the call back with the correct modules to add.""" @@ -77,7 +77,7 @@ async def test_handle_message_connected_one( async def test_handle_message_connected_many( subject: helpers.ModuleListener, mock_callback: AsyncMock, - connections: List[models.Connection], + connections: List[models.ModuleConnection], modules_at_port: List[ModuleAtPort], ) -> None: """It should call the call back with the correct modules to add.""" @@ -98,7 +98,7 @@ async def test_handle_message_disconnected_empty( async def test_handle_message_disconnected_one( subject: helpers.ModuleListener, mock_callback: AsyncMock, - connections: List[models.Connection], + connections: List[models.ModuleConnection], modules_at_port: List[ModuleAtPort], ) -> None: """It should call the call back with the correct modules to remove.""" @@ -110,7 +110,7 @@ async def test_handle_message_disconnected_one( async def test_handle_message_disconnected_many( subject: helpers.ModuleListener, mock_callback: AsyncMock, - connections: List[models.Connection], + connections: List[models.ModuleConnection], modules_at_port: List[ModuleAtPort], ) -> None: """It should call the call back with the correct modules to remove.""" @@ -131,7 +131,7 @@ async def test_handle_message_dump_empty( async def test_handle_message_dump_one( subject: helpers.ModuleListener, mock_callback: AsyncMock, - connections: List[models.Connection], + connections: List[models.ModuleConnection], modules_at_port: List[ModuleAtPort], ) -> None: """It should call the call back with the correct modules to load.""" @@ -143,7 +143,7 @@ async def test_handle_message_dump_one( async def test_handle_message_dump_many( subject: helpers.ModuleListener, mock_callback: AsyncMock, - connections: List[models.Connection], + connections: List[models.ModuleConnection], modules_at_port: List[ModuleAtPort], ) -> None: """It should call the call back with the correct modules to load.""" diff --git a/api/tests/opentrons/hardware_control/integration/conftest.py b/api/tests/opentrons/hardware_control/integration/conftest.py index 6590766fa34..742413da920 100644 --- a/api/tests/opentrons/hardware_control/integration/conftest.py +++ b/api/tests/opentrons/hardware_control/integration/conftest.py @@ -4,7 +4,7 @@ import pytest import asyncio -from opentrons.hardware_control.emulation.module_server import ModuleServerClient +from opentrons.hardware_control.emulation.module_server import ModuleStatusClient from opentrons.hardware_control.emulation.module_server.helpers import wait_emulators from opentrons.hardware_control.emulation.scripts import run_app from opentrons.hardware_control.emulation.types import ModuleType @@ -35,7 +35,7 @@ def _run_app() -> None: asyncio.run(run_app.run(emulator_settings, modules=[m.value for m in modules])) async def _wait_ready() -> None: - c = await ModuleServerClient.connect( + c = await ModuleStatusClient.connect( host="localhost", port=emulator_settings.module_server.port, interval_seconds=1,