Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/spelling_wordlist.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
accessible
accessibles
bool
dataclass
datainfo
datatype
datatypes
Expand Down
19 changes: 9 additions & 10 deletions examples/epics_ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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)
19 changes: 9 additions & 10 deletions examples/epics_pva.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"),
Expand All @@ -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)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [
]

dependencies = [
"fastcs==0.12.0",
"fastcs==0.14.0",
"orjson",
]

Expand Down
8 changes: 7 additions & 1 deletion src/fastcs_secop/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
34 changes: 25 additions & 9 deletions src/fastcs_secop/_controllers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""FastCS controllers for SECoP nodes."""

import asyncio
import dataclasses
import typing
import uuid
from logging import getLogger
Expand All @@ -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."""

Expand Down Expand Up @@ -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 = [...]
Expand All @@ -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__()

Expand Down
10 changes: 6 additions & 4 deletions tests/test_against_emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
)
),
)

Expand Down
27 changes: 17 additions & 10 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from fastcs_secop import (
SecopCommandController,
SecopController,
SecopControllerSettings,
SecopError,
SecopModuleController,
SecopQuirks,
Expand All @@ -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,
),
)
)

Expand Down Expand Up @@ -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"
Expand All @@ -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 = (
Expand All @@ -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"
Expand Down Expand Up @@ -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"),
Expand All @@ -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
Expand Down
Loading