diff --git a/doc/spelling_wordlist.txt b/doc/spelling_wordlist.txt index 1a83487..7316673 100644 --- a/doc/spelling_wordlist.txt +++ b/doc/spelling_wordlist.txt @@ -1,6 +1,7 @@ accessible accessibles bool +dataclass datainfo datatype datatypes diff --git a/examples/epics_ca.py b/examples/epics_ca.py index 19d4de5..d6f2e89 100644 --- a/examples/epics_ca.py +++ b/examples/epics_ca.py @@ -8,13 +8,11 @@ from fastcs.connections import IPConnectionSettings from fastcs.launch import FastCS from fastcs.logging import LogLevel, configure_logging +from fastcs.transports.epics.ca import EpicsCATransport -from fastcs_secop import SecopController, SecopQuirks +from fastcs_secop import SecopController, SecopControllerSettings, SecopQuirks if __name__ == "__main__": - from fastcs.transports import EpicsIOCOptions - from fastcs.transports.epics.ca import EpicsCATransport - parser = argparse.ArgumentParser(description="Demo PVA ioc") parser.add_argument("-i", "--ip", type=str, default="127.0.0.1", help="IP to connect to") parser.add_argument("-p", "--port", type=int, help="Port to connect to", required=True) @@ -25,9 +23,6 @@ asyncio.get_event_loop().slow_callback_duration = 1000 - epics_options = EpicsIOCOptions(pv_prefix=f"TE:{socket.gethostname().upper()}:SECOP") - epics_ca = EpicsCATransport(epicsca=epics_options) - quirks = SecopQuirks( raw_tuple=True, raw_struct=True, @@ -40,12 +35,16 @@ ) controller = SecopController( - settings=IPConnectionSettings(ip=args.ip, port=args.port), - quirks=quirks, + SecopControllerSettings( + connection=IPConnectionSettings(ip=args.ip, port=args.port), + quirks=quirks, + ) ) + controller.set_path(["TE", socket.gethostname(), "SECOP"]) + fastcs = FastCS( controller, - [epics_ca], + [EpicsCATransport()], ) fastcs.run(interactive=True) diff --git a/examples/epics_pva.py b/examples/epics_pva.py index f90e7b3..2d43d8e 100644 --- a/examples/epics_pva.py +++ b/examples/epics_pva.py @@ -8,13 +8,11 @@ from fastcs.connections import IPConnectionSettings from fastcs.launch import FastCS from fastcs.logging import LogLevel, configure_logging +from fastcs.transports.epics.pva import EpicsPVATransport -from fastcs_secop import SecopController, SecopQuirks +from fastcs_secop import SecopController, SecopControllerSettings, SecopQuirks if __name__ == "__main__": - from fastcs.transports import EpicsIOCOptions - from fastcs.transports.epics.pva import EpicsPVATransport - parser = argparse.ArgumentParser(description="Demo PVA ioc") parser.add_argument("-i", "--ip", type=str, default="127.0.0.1", help="IP to connect to") parser.add_argument("-p", "--port", type=int, help="Port to connect to", required=True) @@ -25,9 +23,6 @@ asyncio.get_event_loop().slow_callback_duration = 1000 - epics_options = EpicsIOCOptions(pv_prefix=f"TE:{socket.gethostname().upper()}:SECOP") - epics_pva = EpicsPVATransport(epicspva=epics_options) - quirks = SecopQuirks( raw_accessibles=[ ("valve_controller", "_domains_to_extract"), @@ -36,12 +31,16 @@ ) controller = SecopController( - settings=IPConnectionSettings(ip=args.ip, port=args.port), - quirks=quirks, + SecopControllerSettings( + connection=IPConnectionSettings(ip=args.ip, port=args.port), + quirks=quirks, + ) ) + controller.set_path(["TE", socket.gethostname(), "SECOP"]) + fastcs = FastCS( controller, - [epics_pva], + [EpicsPVATransport()], ) fastcs.run(interactive=True) diff --git a/pyproject.toml b/pyproject.toml index 0128cf4..d96b575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] dependencies = [ - "fastcs==0.12.0", + "fastcs==0.14.0", "orjson", ] diff --git a/src/fastcs_secop/__init__.py b/src/fastcs_secop/__init__.py index ab1725a..17e4603 100644 --- a/src/fastcs_secop/__init__.py +++ b/src/fastcs_secop/__init__.py @@ -1,11 +1,17 @@ """SECoP support using FastCS.""" -from fastcs_secop._controllers import SecopCommandController, SecopController, SecopModuleController +from fastcs_secop._controllers import ( + SecopCommandController, + SecopController, + SecopControllerSettings, + SecopModuleController, +) from fastcs_secop._util import SecopError, SecopQuirks __all__ = [ "SecopCommandController", "SecopController", + "SecopControllerSettings", "SecopError", "SecopModuleController", "SecopQuirks", diff --git a/src/fastcs_secop/_controllers.py b/src/fastcs_secop/_controllers.py index 3acce4c..3e595c5 100644 --- a/src/fastcs_secop/_controllers.py +++ b/src/fastcs_secop/_controllers.py @@ -1,6 +1,7 @@ """FastCS controllers for SECoP nodes.""" import asyncio +import dataclasses import typing import uuid from logging import getLogger @@ -24,6 +25,21 @@ logger = getLogger(__name__) +@dataclasses.dataclass +class SecopControllerSettings: + """Top-level settings dataclass for a SECoP controller.""" + + connection: IPConnectionSettings + """ + The communication settings (e.g. IP address, port) at which the SECoP node is reachable. + """ + + quirks: SecopQuirks | None = None + """ + :py:obj:`~fastcs_secop.SecopQuirks` that affect how attributes are processed in this controller. + """ + + class SecopCommandController(Controller): """SECoP command controller.""" @@ -229,19 +245,21 @@ async def initialise(self) -> None: class SecopController(Controller): """FastCS Controller for a SECoP node.""" - def __init__(self, settings: IPConnectionSettings, quirks: SecopQuirks | None = None) -> None: + def __init__(self, settings: SecopControllerSettings) -> None: """FastCS Controller for a SECoP node. The intended usage is via :py:obj:`fastcs.control_system.FastCS`: .. code-block:: python - from fastcs_secop import SecopController, SecopQuirks + from fastcs_secop import SecopController, SecopQuirks, SecopControllerSettings from fastcs.control_system import FastCS controller = SecopController( - settings=IPConnectionSettings(ip="127.0.0.1", port=1234), - quirks=SecopQuirks(...), + SecopControllerSettings( + connection=IPConnectionSettings(ip="127.0.0.1", port=1234), + quirks=SecopQuirks(...), + ) ) transports = [...] @@ -256,14 +274,12 @@ def __init__(self, settings: IPConnectionSettings, quirks: SecopQuirks | None = :ref:`example_ca_ioc` and :ref:`example_pva_ioc` for examples of full configurations Args: - settings: The communication settings (e.g. IP address, port) at which - the SECoP node is reachable. - quirks: :py:obj:`~fastcs_secop.SecopQuirks` that affects how attributes are processed. + settings: SECoP device settings (see :py:obj:`SecopControllerSettings` for details) """ - self._ip_settings = settings + self._ip_settings = settings.connection self._connection = IPConnection() - self._quirks = quirks or SecopQuirks() + self._quirks = settings.quirks or SecopQuirks() super().__init__() diff --git a/tests/test_against_emulator.py b/tests/test_against_emulator.py index a79080f..0d45d65 100644 --- a/tests/test_against_emulator.py +++ b/tests/test_against_emulator.py @@ -12,7 +12,7 @@ from fastcs.connections import IPConnectionSettings from fastcs.logging import LogLevel, configure_logging -from fastcs_secop import SecopController +from fastcs_secop import SecopController, SecopControllerSettings configure_logging(level=LogLevel.TRACE) @@ -43,9 +43,11 @@ def emulator(): @pytest.fixture async def controller(): controller = SecopController( - settings=IPConnectionSettings( - ip="127.0.0.1", - port=57677, + SecopControllerSettings( + connection=IPConnectionSettings( + ip="127.0.0.1", + port=57677, + ) ), ) diff --git a/tests/test_controller.py b/tests/test_controller.py index a8e02f6..0fa7370 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -8,6 +8,7 @@ from fastcs_secop import ( SecopCommandController, SecopController, + SecopControllerSettings, SecopError, SecopModuleController, SecopQuirks, @@ -17,9 +18,11 @@ @pytest.fixture def controller(): return SecopController( - settings=IPConnectionSettings( - ip="127.0.0.1", - port=65535, + SecopControllerSettings( + connection=IPConnectionSettings( + ip="127.0.0.1", + port=65535, + ), ) ) @@ -54,7 +57,7 @@ async def test_ping_bad_response(controller): async def test_check_idn(): - controller = SecopController(settings=IPConnectionSettings("127.0.0.1", 0)) + controller = SecopController(SecopControllerSettings(IPConnectionSettings("127.0.0.1", 0))) controller._connection = AsyncMock() controller._connection.send_query.return_value = "ISSE&SINE2020,SECoP,foo,bar" @@ -78,8 +81,10 @@ async def test_check_idn(): async def test_create_modules(): controller = SecopController( - settings=IPConnectionSettings("127.0.0.1", 0), - quirks=SecopQuirks(skip_modules="a_skipped_module"), + SecopControllerSettings( + connection=IPConnectionSettings("127.0.0.1", 0), + quirks=SecopQuirks(skip_modules="a_skipped_module"), + ) ) controller._connection = AsyncMock() controller._connection.send_query.return_value = ( @@ -106,8 +111,10 @@ async def test_create_modules(): async def test_create_modules_bad_description(): controller = SecopController( - settings=IPConnectionSettings("127.0.0.1", 0), - quirks=SecopQuirks(skip_modules="a_skipped_module"), + SecopControllerSettings( + connection=IPConnectionSettings("127.0.0.1", 0), + quirks=SecopQuirks(skip_modules="a_skipped_module"), + ) ) controller._connection = AsyncMock() controller._connection.send_query.return_value = "a huge pile of nonsense\n" @@ -143,7 +150,7 @@ async def test_secop_module_controller_initialise(): async def test_cannot_connect_to_controller_at_startup(): controller = SecopController( - settings=IPConnectionSettings("127.0.0.1", 0), + SecopControllerSettings(IPConnectionSettings("127.0.0.1", 0)), ) with ( pytest.raises(SecopError, match=r"Could not connect to SECoP node at FastCS startup"), @@ -154,7 +161,7 @@ async def test_cannot_connect_to_controller_at_startup(): async def test_reconnect_does_nothing_if_already_connected(): controller = SecopController( - settings=IPConnectionSettings("127.0.0.1", 0), + SecopControllerSettings(IPConnectionSettings("127.0.0.1", 0)), ) with patch.object(controller, "connect", AsyncMock()) as mock_connect: controller._connected = True