Skip to content
Open
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
17 changes: 14 additions & 3 deletions src/dodal/beamlines/i21.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from daq_config_server import ConfigClient

from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.common.enums import OpenClosed
from dodal.device_manager import DeviceManager
from dodal.devices.beamlines.i21 import (
FastShutterWithLateralMotor,
Grating,
I21SampleManipulatorStage,
ToolPointMotion,
Expand All @@ -24,9 +26,7 @@
from dodal.devices.insertion_device.lookup_table_models import LookupTableColumnConfig
from dodal.devices.pgm import PlaneGratingMonochromator
from dodal.devices.synchrotron import Synchrotron
from dodal.devices.temperture_controller import (
Lakeshore336,
)
from dodal.devices.temperture_controller import Lakeshore336
from dodal.log import set_beamline as set_log_beamline
from dodal.utils import BeamlinePrefix, get_beamline_name

Expand All @@ -50,6 +50,17 @@ def synchrotron() -> Synchrotron:
return Synchrotron()


@devices.factory
def fs1() -> FastShutterWithLateralMotor[OpenClosed]:
return FastShutterWithLateralMotor[OpenClosed](
prefix=PREFIX.beamline_prefix,
shutter_suffix="-OP-SHTR-01:CON",
lateral_suffix="-MO-SHTR-01:LAT",
open_state=OpenClosed.OPEN,
close_state=OpenClosed.CLOSED,
)


@devices.factory()
def pgm() -> PlaneGratingMonochromator:
return PlaneGratingMonochromator(
Expand Down
6 changes: 6 additions & 0 deletions src/dodal/common/enums.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from ophyd_async.core import StrictEnum


class OpenClosed(StrictEnum):
OPEN = "Open"
CLOSED = "Closed"


# Any capitalised enums needs to be removed and replaced with ones from ophyd-async.core
# https://github.com/DiamondLightSource/dodal/issues/1416

Expand Down
2 changes: 2 additions & 0 deletions src/dodal/devices/beamlines/i21/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .enums import Grating
from .fast_shutter import FastShutterWithLateralMotor
from .i21_motors import I21SampleManipulatorStage
from .toolpoint_motion import (
ToolPointMotion,
Expand All @@ -8,6 +9,7 @@

__all__ = [
"Grating",
"FastShutterWithLateralMotor",
"I21SampleManipulatorStage",
"ToolPointMotion",
"UVWTiltAzimuthMotorPositions",
Expand Down
34 changes: 34 additions & 0 deletions src/dodal/devices/beamlines/i21/fast_shutter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Generic

from ophyd_async.epics.motor import Motor

from dodal.devices.fast_shutter import EnumTypesT, FastShutter


class FastShutterWithLateralMotor(FastShutter[EnumTypesT], Generic[EnumTypesT]):
Copy link
Copy Markdown
Contributor

@Relm-Arrowny Relm-Arrowny Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to ask @fajinyuan how this fast shutter actually should behave, looking at it as an outsider, it look to me this is a externally triggered fast shutter. Couple of things concern me:

  1. The ERIO output is the status we trying to change as if it is a standard shutter, I am not 100% sure open will mean open or it just set the default state to open and a trigger signal will close it.
  2. There are 4 more outputs and a source signals on that fast shutter controller we likely want to be able to access them depending of the experimental condition.

"""Implementation of fast shutter that connects to an epics enum pv. This controls
the open and close state of the shutter. This also has the lateral motor x.

Args:
pv (str): The pv to connect to the shutter device.
open_state (EnumTypesT): The enum value that corresponds with opening the
shutter.
close_state (EnumTypesT): The enum value that corresponds with closing the
shutter.
shutter_suffix (str, optional): Shutter suffix state. Defaults to CON.
lateral_suffix (str, optional): Lateral motor suffix. Defaults to LAT.
name (str, optional): The name of the shutter.
"""

def __init__(
self,
prefix: str,
open_state: EnumTypesT,
close_state: EnumTypesT,
shutter_suffix: str = "CON",
lateral_suffix: str = "LAT",
name: str = "",
):
with self.add_children_as_readables():
self.x = Motor(prefix + lateral_suffix)
super().__init__(prefix + shutter_suffix, open_state, close_state, name)
11 changes: 2 additions & 9 deletions src/dodal/devices/detector/detector_motion.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
from ophyd_async.core import StrictEnum
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
from ophyd_async.epics.motor import Motor

from dodal.common.enums import OpenClosed
from dodal.devices.motors import XYZStage


class ShutterState(StrictEnum):
CLOSED = "Closed"
OPEN = "Open"


class DetectorMotion(XYZStage):
def __init__(self, device_prefix: str, pmac_prefix: str, name: str = ""):
self.upstream_x = Motor(f"{device_prefix}UPSTREAMX")
self.downstream_x = Motor(f"{device_prefix}DOWNSTREAMX")
self.yaw = Motor(f"{device_prefix}YAW")

self.shutter = epics_signal_rw(
ShutterState, f"{device_prefix}SET_SHUTTER_STATE"
)
self.shutter = epics_signal_rw(OpenClosed, f"{device_prefix}SET_SHUTTER_STATE")
self.shutter_closed_lim = epics_signal_r(
float, f"{device_prefix}CLOSE_LIMIT"
) # on limit = 1, off = 0
Expand Down
3 changes: 1 addition & 2 deletions src/dodal/devices/insertion_device/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
ConfigServerEnergyMotorLookup,
EnergyMotorLookup,
)
from .enum import Pol, UndulatorGateStatus
from .enum import Pol
from .lookup_table_models import (
EnergyCoverage,
LookupTable,
Expand Down Expand Up @@ -60,7 +60,6 @@
"Pol",
"DEFAULT_MOTOR_MIN_TIMEOUT",
"EnabledDisabledUpper",
"UndulatorGateStatus",
"Apple2LockedPhasesVal",
"EnergyMotorLookup",
"ConfigServerEnergyMotorLookup",
Expand Down
19 changes: 8 additions & 11 deletions src/dodal/devices/insertion_device/apple2_undulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
from ophyd_async.epics.motor import Motor

from dodal.common.enums import EnabledDisabledUpper
from dodal.devices.insertion_device.enum import UndulatorGateStatus
from dodal.common.enums import EnabledDisabledUpper, OpenClosed
from dodal.log import LOGGER

T = TypeVar("T")
Expand Down Expand Up @@ -71,7 +70,7 @@ class UndulatorBase(abc.ABC, Device, Generic[T]):

def __init__(self, name: str = ""):
# Gate keeper open when move is requested, closed when move is completed
self.gate: SignalR[UndulatorGateStatus]
self.gate: SignalR[OpenClosed]
self.status: SignalR[EnabledDisabledUpper]
super().__init__(name=name)

Expand All @@ -86,7 +85,7 @@ async def get_timeout(self) -> float:
async def raise_if_cannot_move(self) -> None:
if await self.status.get_value() is EnabledDisabledUpper.DISABLED:
raise RuntimeError(f"{self.name} is DISABLED and cannot move.")
if await self.gate.get_value() is UndulatorGateStatus.OPEN:
if await self.gate.get_value() is OpenClosed.OPEN:
raise RuntimeError(f"{self.name} is already in motion.")


Expand All @@ -97,7 +96,7 @@ class SafeUndulatorMover(StandardReadable, UndulatorBase, Generic[T]):

def __init__(self, set_move: SignalW, prefix: str, name: str = ""):
# Gate keeper open when move is requested, closed when move is completed
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
self.gate = epics_signal_r(OpenClosed, prefix + "BLGATE")
self.status = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
self.set_move = set_move
super().__init__(name)
Expand All @@ -110,7 +109,7 @@ async def set(self, value: T) -> None:
timeout = await self.get_timeout()
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
await self.set_move.set(value=1, timeout=timeout)
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
await wait_for_value(self.gate, OpenClosed.CLOSED, timeout=timeout)


class UnstoppableMotor(Motor):
Expand All @@ -129,7 +128,7 @@ class GapSafeMotorNoStop(UnstoppableMotor, UndulatorBase[float]):

def __init__(self, set_move: SignalW[int], prefix: str, name: str = ""):
# Gate keeper open when move is requested, closed when move is completed
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
self.gate = epics_signal_r(OpenClosed, prefix + "BLGATE")
self.status = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
self.set_move = set_move
super().__init__(prefix=prefix + "BLGAPMTR", name=name)
Expand All @@ -153,7 +152,7 @@ async def set(self, new_position: float, timeout=DEFAULT_TIMEOUT):

await self.set_move.set(value=1, timeout=timeout)
move_status = AsyncStatus(
wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
wait_for_value(self.gate, OpenClosed.CLOSED, timeout=timeout)
)

async for current_position in observe_value(
Expand Down Expand Up @@ -402,6 +401,4 @@ async def set(self, id_motor_values: Apple2Val) -> None:
self.phase().set_move.set(value=1, timeout=timeout),
)

await wait_for_value(
self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
)
await wait_for_value(self.gap().gate, OpenClosed.CLOSED, timeout=timeout)
5 changes: 0 additions & 5 deletions src/dodal/devices/insertion_device/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,3 @@ class Pol(StrictEnum):
LA = "la"
LH3 = "lh3"
LV3 = "lv3"


class UndulatorGateStatus(StrictEnum):
OPEN = "Open"
CLOSE = "Closed"
10 changes: 5 additions & 5 deletions src/dodal/testing/fixtures/devices/apple2.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
set_mock_value,
)

from dodal.common.enums import OpenClosed
from dodal.devices.insertion_device import (
EnabledDisabledUpper,
UndulatorGap,
UndulatorGateStatus,
UndulatorJawPhase,
UndulatorPhaseAxes,
)
Expand Down Expand Up @@ -40,7 +40,7 @@ async def mock_id_gap(prefix: str = "BLXX-EA-DET-007:") -> UndulatorGap:
async with init_devices(mock=True):
mock_id_gap = UndulatorGap(prefix, "mock_id_gap")
assert mock_id_gap.name == "mock_id_gap"
set_mock_value(mock_id_gap.gate, UndulatorGateStatus.CLOSE)
set_mock_value(mock_id_gap.gate, OpenClosed.CLOSED)
set_mock_value(mock_id_gap.velocity, 1)
set_mock_value(mock_id_gap.user_readback, 1)
set_mock_value(mock_id_gap.user_setpoint, "1")
Expand All @@ -59,7 +59,7 @@ async def mock_phase_axes(prefix: str = "BLXX-EA-DET-007:") -> UndulatorPhaseAxe
btm_inner="RPQ4",
)
assert mock_phase_axes.name == "mock_phase_axes"
set_mock_value(mock_phase_axes.gate, UndulatorGateStatus.CLOSE)
set_mock_value(mock_phase_axes.gate, OpenClosed.CLOSED)
set_mock_value(mock_phase_axes.top_outer.velocity, 2)
set_mock_value(mock_phase_axes.top_inner.velocity, 2)
set_mock_value(mock_phase_axes.btm_outer.velocity, 2)
Expand All @@ -74,7 +74,7 @@ async def mock_jaw_phase(prefix: str = "BLXX-EA-DET-007:") -> UndulatorJawPhase:
mock_jaw_phase = UndulatorJawPhase(
prefix=prefix, move_pv="RPQ1", jaw_phase="JAW"
)
set_mock_value(mock_jaw_phase.gate, UndulatorGateStatus.CLOSE)
set_mock_value(mock_jaw_phase.gate, OpenClosed.CLOSED)
set_mock_value(mock_jaw_phase.jaw_phase.velocity, 2)
set_mock_value(mock_jaw_phase.jaw_phase.user_readback, 0)
set_mock_value(mock_jaw_phase.jaw_phase.user_setpoint_readback, 0)
Expand All @@ -93,7 +93,7 @@ async def mock_locked_phase_axes(
btm_inner="RPQ4",
)
assert mock_phase_axes.name == "mock_phase_axes"
set_mock_value(mock_phase_axes.gate, UndulatorGateStatus.CLOSE)
set_mock_value(mock_phase_axes.gate, OpenClosed.CLOSED)
set_mock_value(mock_phase_axes.top_outer.velocity, 2)
set_mock_value(mock_phase_axes.btm_inner.velocity, 2)
set_mock_value(mock_phase_axes.top_outer.user_readback, 2)
Expand Down
8 changes: 4 additions & 4 deletions tests/devices/beamlines/i10/test_i10_apple2.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from ophyd_async.testing import assert_emitted

from dodal.common.enums import OpenClosed
from dodal.devices.beamlines.i10.i10_apple2 import (
DEFAULT_JAW_PHASE_POLY_PARAMS,
I10Apple2,
Expand All @@ -31,7 +32,6 @@
InsertionDevicePolarisation,
Pol,
UndulatorGap,
UndulatorGateStatus,
UndulatorJawPhase,
UndulatorPhaseAxes,
)
Expand Down Expand Up @@ -253,7 +253,7 @@ async def test_fail_i10_apple2_controller_set_id_not_ready(
set_mock_value(
mock_id_controller.apple2().gap().status, EnabledDisabledUpper.ENABLED
)
set_mock_value(mock_id_controller.apple2().gap().gate, UndulatorGateStatus.OPEN)
set_mock_value(mock_id_controller.apple2().gap().gate, OpenClosed.OPEN)
with pytest.raises(RuntimeError) as e:
await mock_id_controller.energy.set(600)
assert (
Expand Down Expand Up @@ -629,11 +629,11 @@ async def test_linear_arbitrary_run_engine_scan(
assert_emitted(run_engine_documents, start=1, descriptor=1, event=num_point, stop=1)
set_mock_value(
mock_id_controller.apple2().gap().gate,
UndulatorGateStatus.CLOSE,
OpenClosed.CLOSED,
)
set_mock_value(
mock_id_controller.apple2().phase().gate,
UndulatorGateStatus.CLOSE,
OpenClosed.CLOSED,
)
jaw_phase = get_mock_put(
mock_id_controller.apple2().jaw_phase().jaw_phase.user_setpoint
Expand Down
27 changes: 27 additions & 0 deletions tests/devices/beamlines/i21/test_fast_shutter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pytest
from ophyd_async.core import init_devices
from ophyd_async.testing import assert_reading, partial_reading

from dodal.common.enums import OpenClosed
from dodal.devices.beamlines.i21 import FastShutterWithLateralMotor


@pytest.fixture
def fs_with_lateral_motor() -> FastShutterWithLateralMotor:
with init_devices(mock=True):
fs_with_lateral_motor = FastShutterWithLateralMotor[OpenClosed](
"TESt", open_state=OpenClosed.OPEN, close_state=OpenClosed.CLOSED
)
return fs_with_lateral_motor


async def test_fs_with_lateral_motor_read(
fs_with_lateral_motor: FastShutterWithLateralMotor,
) -> None:
await assert_reading(
fs_with_lateral_motor,
{
"fs_with_lateral_motor-shutter_state": partial_reading(OpenClosed.OPEN),
"fs_with_lateral_motor-x": partial_reading(0),
},
)
4 changes: 2 additions & 2 deletions tests/devices/insertion_device/test_apple2_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
set_mock_value,
)

from dodal.common.enums import OpenClosed
from dodal.devices.insertion_device import (
Apple2,
Apple2Controller,
Expand All @@ -14,7 +15,6 @@
EnabledDisabledUpper,
EnergyMotorConvertor,
Pol,
UndulatorGateStatus,
UndulatorLockedPhaseAxes,
)

Expand All @@ -34,7 +34,7 @@ async def mock_locked_phase_axes(
btm_inner="RPQ4",
)
assert mock_phase_axes.name == "mock_phase_axes"
set_mock_value(mock_phase_axes.gate, UndulatorGateStatus.CLOSE)
set_mock_value(mock_phase_axes.gate, OpenClosed.CLOSED)
set_mock_value(mock_phase_axes.top_outer.velocity, 2)
set_mock_value(mock_phase_axes.btm_inner.velocity, 2)
set_mock_value(mock_phase_axes.top_outer.user_readback, 2)
Expand Down
Loading
Loading