Skip to content
Draft
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
20 changes: 20 additions & 0 deletions src/dodal/beamlines/i15_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
from dodal.devices.beamlines.i15_1.attenuator import Attenuator
from dodal.devices.beamlines.i15_1.puck_detector import PuckDetect
from dodal.devices.beamlines.i15_1.robot import Robot
from dodal.devices.hutch_shutter import (
HutchInterlock,
InterlockedHutchShutter,
PLCShutterInterlock,
)
from dodal.devices.motors import XYPhiStage, XYStage, YZStage
from dodal.devices.slits import Slits
from dodal.devices.synchrotron import Synchrotron
Expand Down Expand Up @@ -176,3 +181,18 @@ def puck_detect() -> PuckDetect:
@devices.factory()
def attenuator() -> Attenuator:
return Attenuator(f"{PREFIX.beamline_prefix}-OP-ATTN-02:")


@devices.factory()
def hutch_interlock() -> HutchInterlock:
return HutchInterlock(bl_prefix="BL15I", interlock_suffix="-PS-IOC-02:M11:LOP")


@devices.factory()
def hutch_shutter() -> InterlockedHutchShutter:
return InterlockedHutchShutter(
bl_prefix=PREFIX.beamline_prefix,
interlock=PLCShutterInterlock(
bl_prefix=PREFIX.beamline_prefix, interlock_suffix="-PS-SHTR-01:ILKSTA"
),
)
40 changes: 40 additions & 0 deletions src/dodal/devices/hutch_shutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ class ShutterState(StrictEnum):
CLOSING = "Closing"


class InterlockState(StrictEnum):
FAILED = "Failed"
RUN_ILKS_OK = "Run Ilks Ok"
OK = "OK"
DISARMED = "Disarmed"


class BaseHutchInterlock(ABC, StandardReadable):
status: SignalR[float | EnumTypes]
bl_prefix: str
Expand Down Expand Up @@ -95,6 +102,39 @@ async def shutter_safe_to_operate(self) -> bool:
return isclose(float(interlock_state), HUTCH_SAFE_FOR_OPERATIONS, abs_tol=5e-2)


class PLCShutterInterlock(BaseHutchInterlock):
"""Device to check the interlock state of the shutter using PLC pv."""

def __init__(
self,
bl_prefix: str,
shtr_infix: str = "",
interlock_suffix: str = EXP_SHUTTER_1_INFIX,
name: str = "",
) -> None:
super().__init__(
signal_type=InterlockState,
bl_prefix=bl_prefix,
interlock_infix=shtr_infix,
interlock_suffix=interlock_suffix,
name=name,
)

# TODO replace with read
# See https://github.com/DiamondLightSource/dodal/issues/651
# TODO check if shutter only opens when Ilks are "OK" or if "Run Ilks Ok" is also healthy
async def shutter_safe_to_operate(self) -> bool:
"""If the status value is OK, shutter is safe to operate.

If the status value is not OK (Failed or Disarmed), the shutter cannot be
operated.
"""
interlock_state = await self.status.get_value()
return (interlock_state == InterlockState.OK) | (
interlock_state == InterlockState.RUN_ILKS_OK
)


class BaseHutchShutter(ABC, StandardReadable, Movable[ShutterDemand]):
"""Device to operate the hutch shutter.

Expand Down
56 changes: 56 additions & 0 deletions tests/devices/test_hutch_shutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
HutchInterlock,
HutchShutter,
InterlockedHutchShutter,
InterlockState,
PLCShutterInterlock,
ShutterDemand,
ShutterNotSafeToOperateError,
ShutterState,
Expand All @@ -29,6 +31,13 @@ async def interlock() -> HutchInterlock:
return interlock


@pytest.fixture
async def plc_interlock() -> PLCShutterInterlock:
async with init_devices(mock=True):
interlock = PLCShutterInterlock(bl_prefix="TEST")
return interlock


def _apply_status_setter(abstract_shutter: BaseHutchShutter):
def set_status(value: ShutterDemand, *args, **kwargs):
value_sta = ShutterState.OPEN if value == "Open" else ShutterState.CLOSED
Expand All @@ -51,6 +60,20 @@ async def fake_interlocked_shutter(
return interlocked_shutter


@pytest.fixture
async def fake_plc_interlocked_shutter(
plc_interlock: PLCShutterInterlock,
) -> InterlockedHutchShutter:
async with init_devices(mock=True):
interlocked_shutter = InterlockedHutchShutter(
bl_prefix="TEST", interlock=plc_interlock
)

_apply_status_setter(interlocked_shutter)

return interlocked_shutter


@pytest.fixture
async def fake_shutter_without_interlock() -> HutchShutter:
async with init_devices(mock=True):
Expand All @@ -70,6 +93,12 @@ def test_interlocked_shutter_can_be_created(
assert isinstance(fake_interlocked_shutter, BaseHutchShutter)


def test_plc_interlocked_shutter_can_be_created(
fake_plc_interlocked_shutter: InterlockedHutchShutter,
):
assert isinstance(fake_plc_interlocked_shutter, BaseHutchShutter)


async def test_interlock_is_readable(interlock: HutchInterlock):
await assert_reading(
interlock,
Expand All @@ -79,6 +108,15 @@ async def test_interlock_is_readable(interlock: HutchInterlock):
)


async def test_plc_interlock_is_readable(plc_interlock: PLCShutterInterlock):
await assert_reading(
plc_interlock,
{
f"{plc_interlock.name}-status": partial_reading(InterlockState.FAILED),
},
)


@pytest.mark.parametrize(
"readback, expected_state",
[
Expand All @@ -96,6 +134,24 @@ async def test_hutch_interlock_safe_to_operate_logic(
assert await interlock.shutter_safe_to_operate() is expected_state


@pytest.mark.parametrize(
"readback, expected_state",
[
(InterlockState.OK, True),
(InterlockState.RUN_ILKS_OK, True),
(InterlockState.DISARMED, False),
(InterlockState.FAILED, False),
],
)
async def test_plc_shutter_interlock_safe_to_operate_logic(
plc_interlock: PLCShutterInterlock,
readback: float,
expected_state: bool,
):
set_mock_value(plc_interlock.status, readback)
assert await plc_interlock.shutter_safe_to_operate() is expected_state


async def test_shutter_readable(fake_shutter_without_interlock: HutchShutter):
result = {
f"{fake_shutter_without_interlock.name}-status": partial_reading(
Expand Down
Loading