dls-dodal 1.68.0__py3-none-any.whl → 1.69.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/METADATA +1 -31
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/RECORD +57 -49
- dodal/_version.py +2 -2
- dodal/beamlines/adsim.py +30 -23
- dodal/beamlines/i02_1.py +14 -42
- dodal/beamlines/i02_2.py +5 -11
- dodal/beamlines/i03.py +4 -1
- dodal/beamlines/i03_supervisor.py +19 -0
- dodal/beamlines/i04.py +74 -179
- dodal/beamlines/i05.py +8 -0
- dodal/beamlines/i06_1.py +24 -0
- dodal/beamlines/i09.py +53 -9
- dodal/beamlines/i09_1.py +8 -0
- dodal/beamlines/i09_2.py +4 -4
- dodal/beamlines/i16.py +11 -0
- dodal/beamlines/i20_1.py +14 -0
- dodal/beamlines/i21.py +12 -4
- dodal/beamlines/i23.py +19 -25
- dodal/beamlines/i24.py +55 -105
- dodal/beamlines/p60.py +11 -1
- dodal/common/__init__.py +2 -1
- dodal/common/maths.py +80 -0
- dodal/devices/eiger.py +29 -14
- dodal/devices/electron_analyser/base/__init__.py +3 -3
- dodal/devices/electron_analyser/base/base_controller.py +19 -8
- dodal/devices/electron_analyser/base/base_enums.py +0 -5
- dodal/devices/electron_analyser/base/base_region.py +2 -1
- dodal/devices/electron_analyser/base/energy_sources.py +27 -26
- dodal/devices/electron_analyser/specs/specs_detector.py +7 -6
- dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +7 -6
- dodal/devices/fast_shutter.py +108 -25
- dodal/devices/i04/beam_centre.py +84 -0
- dodal/devices/i04/max_pixel.py +4 -17
- dodal/devices/i04/murko_results.py +18 -3
- dodal/devices/i10/i10_apple2.py +6 -6
- dodal/devices/i17/i17_apple2.py +6 -6
- dodal/devices/i24/commissioning_jungfrau.py +9 -10
- dodal/devices/insertion_device/__init__.py +12 -8
- dodal/devices/insertion_device/apple2_controller.py +380 -0
- dodal/devices/insertion_device/apple2_undulator.py +152 -531
- dodal/devices/insertion_device/energy.py +88 -0
- dodal/devices/insertion_device/energy_motor_lookup.py +1 -1
- dodal/devices/insertion_device/lookup_table_models.py +2 -2
- dodal/devices/insertion_device/polarisation.py +36 -0
- dodal/devices/oav/oav_detector.py +66 -1
- dodal/devices/oav/utils.py +17 -0
- dodal/devices/robot.py +35 -18
- dodal/devices/selectable_source.py +38 -0
- dodal/devices/zebra/zebra.py +15 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
- dodal/testing/__init__.py +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.68.0.dist-info → dls_dodal-1.69.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/insertion_device/{id_enum.py → enum.py} +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import Movable
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
AsyncStatus,
|
|
7
|
+
Reference,
|
|
8
|
+
SignalRW,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
StandardReadableFormat,
|
|
11
|
+
soft_signal_rw,
|
|
12
|
+
)
|
|
13
|
+
from ophyd_async.epics.motor import Motor
|
|
14
|
+
|
|
15
|
+
from dodal.devices.insertion_device import MAXIMUM_MOVE_TIME, Apple2Controller
|
|
16
|
+
from dodal.log import LOGGER
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
|
|
20
|
+
"""Base class for ID energy movable device."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str = "") -> None:
|
|
23
|
+
self.energy: Reference[SignalRW[float]]
|
|
24
|
+
super().__init__(name=name)
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
@AsyncStatus.wrap
|
|
28
|
+
async def set(self, energy: float) -> None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BeamEnergy(StandardReadable, Movable[float]):
|
|
32
|
+
"""
|
|
33
|
+
Compound device to set both ID and energy motor at the same time with an option to add an offset.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
|
|
38
|
+
) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
|
|
43
|
+
id_energy: InsertionDeviceEnergy
|
|
44
|
+
An InsertionDeviceEnergy device.
|
|
45
|
+
mono: Motor
|
|
46
|
+
A Motor(energy) device.
|
|
47
|
+
name:
|
|
48
|
+
New device name.
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(name=name)
|
|
51
|
+
self._id_energy = Reference(id_energy)
|
|
52
|
+
self._mono_energy = Reference(mono)
|
|
53
|
+
|
|
54
|
+
self.add_readables(
|
|
55
|
+
[
|
|
56
|
+
self._id_energy().energy(),
|
|
57
|
+
self._mono_energy().user_readback,
|
|
58
|
+
],
|
|
59
|
+
StandardReadableFormat.HINTED_SIGNAL,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
63
|
+
self.id_energy_offset = soft_signal_rw(float, initial_value=0)
|
|
64
|
+
|
|
65
|
+
@AsyncStatus.wrap
|
|
66
|
+
async def set(self, energy: float) -> None:
|
|
67
|
+
LOGGER.info(f"Moving f{self.name} energy to {energy}.")
|
|
68
|
+
await asyncio.gather(
|
|
69
|
+
self._id_energy().set(
|
|
70
|
+
energy=energy + await self.id_energy_offset.get_value()
|
|
71
|
+
),
|
|
72
|
+
self._mono_energy().set(energy),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
|
|
77
|
+
"""Apple2 ID energy movable device."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
|
|
80
|
+
self.energy = Reference(id_controller.energy)
|
|
81
|
+
super().__init__(name=name)
|
|
82
|
+
|
|
83
|
+
self.add_readables([self.energy()], StandardReadableFormat.HINTED_SIGNAL)
|
|
84
|
+
|
|
85
|
+
@AsyncStatus.wrap
|
|
86
|
+
async def set(self, energy: float) -> None:
|
|
87
|
+
LOGGER.info(f"Setting insertion device energy to {energy}.")
|
|
88
|
+
await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
|
|
@@ -2,7 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
from daq_config_server.client import ConfigServer
|
|
4
4
|
|
|
5
|
-
from dodal.devices.insertion_device.
|
|
5
|
+
from dodal.devices.insertion_device.enum import Pol
|
|
6
6
|
from dodal.devices.insertion_device.lookup_table_models import (
|
|
7
7
|
LookupTable,
|
|
8
8
|
LookupTableColumnConfig,
|
|
@@ -39,7 +39,7 @@ from pydantic import (
|
|
|
39
39
|
field_validator,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
from dodal.devices.insertion_device.
|
|
42
|
+
from dodal.devices.insertion_device.enum import Pol
|
|
43
43
|
|
|
44
44
|
DEFAULT_POLY_DEG = [
|
|
45
45
|
"7th-order",
|
|
@@ -57,7 +57,7 @@ DEFAULT_GAP_FILE = "IDEnergy2GapCalibrations.csv"
|
|
|
57
57
|
DEFAULT_PHASE_FILE = "IDEnergy2PhaseCalibrations.csv"
|
|
58
58
|
|
|
59
59
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
60
|
-
ROW_PHASE_CIRCULAR = 15
|
|
60
|
+
ROW_PHASE_CIRCULAR = 15.0
|
|
61
61
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
62
62
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
63
63
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Locatable, Location
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
AsyncStatus,
|
|
6
|
+
Reference,
|
|
7
|
+
StandardReadable,
|
|
8
|
+
StandardReadableFormat,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from dodal.devices.insertion_device import MAXIMUM_MOVE_TIME, Apple2Controller
|
|
12
|
+
from dodal.devices.insertion_device.enum import Pol
|
|
13
|
+
from dodal.log import LOGGER
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
|
|
17
|
+
"""Apple2 ID polarisation movable device."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
|
|
20
|
+
self.polarisation = Reference(id_controller.polarisation)
|
|
21
|
+
self.polarisation_setpoint = Reference(id_controller.polarisation_setpoint)
|
|
22
|
+
super().__init__(name=name)
|
|
23
|
+
|
|
24
|
+
self.add_readables([self.polarisation()], StandardReadableFormat.HINTED_SIGNAL)
|
|
25
|
+
|
|
26
|
+
@AsyncStatus.wrap
|
|
27
|
+
async def set(self, pol: Pol) -> None:
|
|
28
|
+
LOGGER.info(f"Setting insertion device polarisation to {pol.name}")
|
|
29
|
+
await self.polarisation().set(pol, timeout=MAXIMUM_MOVE_TIME)
|
|
30
|
+
|
|
31
|
+
async def locate(self) -> Location[Pol]:
|
|
32
|
+
"""Return the current polarisation"""
|
|
33
|
+
setpoint, readback = await asyncio.gather(
|
|
34
|
+
self.polarisation_setpoint().get_value(), self.polarisation().get_value()
|
|
35
|
+
)
|
|
36
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from enum import IntEnum
|
|
2
3
|
|
|
3
4
|
from bluesky.protocols import Movable
|
|
4
5
|
from ophyd_async.core import (
|
|
5
6
|
DEFAULT_TIMEOUT,
|
|
6
7
|
AsyncStatus,
|
|
8
|
+
DeviceMock,
|
|
9
|
+
DeviceVector,
|
|
7
10
|
LazyMock,
|
|
8
11
|
SignalR,
|
|
9
12
|
SignalRW,
|
|
10
13
|
StandardReadable,
|
|
14
|
+
default_mock_class,
|
|
11
15
|
derived_signal_r,
|
|
12
16
|
soft_signal_rw,
|
|
13
17
|
)
|
|
@@ -22,6 +26,7 @@ from dodal.devices.oav.oav_parameters import (
|
|
|
22
26
|
)
|
|
23
27
|
from dodal.devices.oav.snapshots.snapshot import Snapshot
|
|
24
28
|
from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
|
|
29
|
+
from dodal.log import LOGGER
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
class Coords(IntEnum):
|
|
@@ -56,6 +61,35 @@ class NullZoomController(BaseZoomController):
|
|
|
56
61
|
await self.level.set(value, wait=True)
|
|
57
62
|
|
|
58
63
|
|
|
64
|
+
class BeamCentreForZoom(StandardReadable):
|
|
65
|
+
"""These PVs hold the beam centre on the OAV at each zoom level.
|
|
66
|
+
|
|
67
|
+
When the zoom level is changed the IOC will update the OAV overlay PVs to be at these positions."""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self, prefix: str, level_name_pv_suffix: str, centre_value_pv_suffix: str
|
|
71
|
+
) -> None:
|
|
72
|
+
self.level_name = epics_signal_r(
|
|
73
|
+
str, f"{prefix}MP:SELECT.{level_name_pv_suffix}"
|
|
74
|
+
)
|
|
75
|
+
self.x_centre = epics_signal_rw(
|
|
76
|
+
float, f"{prefix}PBCX:VAL{centre_value_pv_suffix}"
|
|
77
|
+
)
|
|
78
|
+
self.y_centre = epics_signal_rw(
|
|
79
|
+
float, f"{prefix}PBCY:VAL{centre_value_pv_suffix}"
|
|
80
|
+
)
|
|
81
|
+
super().__init__()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class InstantMovingZoom(DeviceMock["ZoomController"]):
|
|
85
|
+
"""Mock behaviour that instantly moves the zoom."""
|
|
86
|
+
|
|
87
|
+
async def connect(self, device: "ZoomController") -> None:
|
|
88
|
+
"""Mock signals to do an instant move on setpoint write."""
|
|
89
|
+
device.DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S = 0.001 # type:ignore
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@default_mock_class(InstantMovingZoom)
|
|
59
93
|
class ZoomController(BaseZoomController):
|
|
60
94
|
"""
|
|
61
95
|
Device to control the zoom level. This should be set like
|
|
@@ -63,19 +97,49 @@ class ZoomController(BaseZoomController):
|
|
|
63
97
|
oav.zoom_controller.set("1.0x")
|
|
64
98
|
|
|
65
99
|
Note that changing the zoom may change the AD wiring on the associated OAV, as such
|
|
66
|
-
you should wait on any zoom
|
|
100
|
+
you should wait on any zoom changes to finish before changing the OAV wiring.
|
|
67
101
|
"""
|
|
68
102
|
|
|
103
|
+
DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S = 2
|
|
104
|
+
|
|
69
105
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
70
106
|
self.percentage = epics_signal_rw(float, f"{prefix}ZOOMPOSCMD")
|
|
71
107
|
|
|
72
108
|
# Level is the string description of the zoom level e.g. "1.0x" or "1.0"
|
|
73
109
|
self.level = epics_signal_rw(str, f"{prefix}MP:SELECT")
|
|
110
|
+
|
|
74
111
|
super().__init__(name=name)
|
|
75
112
|
|
|
76
113
|
@AsyncStatus.wrap
|
|
77
114
|
async def set(self, value: str):
|
|
78
115
|
await self.level.set(value, wait=True)
|
|
116
|
+
LOGGER.info(
|
|
117
|
+
"Waiting {self.DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S} seconds for zoom to be noticeable"
|
|
118
|
+
)
|
|
119
|
+
await asyncio.sleep(self.DELAY_BETWEEN_MOTORS_AND_IMAGE_UPDATING_S)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ZoomControllerWithBeamCentres(ZoomController):
|
|
123
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
124
|
+
level_to_centre_mapping = [
|
|
125
|
+
("ZRST", "A"),
|
|
126
|
+
("ONST", "B"),
|
|
127
|
+
("TWST", "C"),
|
|
128
|
+
("THST", "D"),
|
|
129
|
+
("FRST", "E"),
|
|
130
|
+
("FVST", "F"),
|
|
131
|
+
("SXST", "G"),
|
|
132
|
+
("SVST", "H"),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
self.beam_centres = DeviceVector(
|
|
136
|
+
{
|
|
137
|
+
i: BeamCentreForZoom(prefix, *level_to_centre_mapping[i])
|
|
138
|
+
for i in range(len(level_to_centre_mapping))
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
super().__init__(prefix, name)
|
|
79
143
|
|
|
80
144
|
|
|
81
145
|
class OAV(StandardReadable):
|
|
@@ -118,6 +182,7 @@ class OAV(StandardReadable):
|
|
|
118
182
|
self.zoom_controller = zoom_controller
|
|
119
183
|
|
|
120
184
|
self.cam = Cam(f"{prefix}CAM:", name=name)
|
|
185
|
+
|
|
121
186
|
with self.add_children_as_readables():
|
|
122
187
|
self.grid_snapshot = SnapshotWithGrid(
|
|
123
188
|
f"{prefix}{mjpeg_prefix}:", name, mjpg_x_size_pv, mjpg_y_size_pv
|
dodal/devices/oav/utils.py
CHANGED
|
@@ -2,6 +2,7 @@ from collections.abc import Generator
|
|
|
2
2
|
from enum import IntEnum
|
|
3
3
|
|
|
4
4
|
import bluesky.plan_stubs as bps
|
|
5
|
+
import cv2
|
|
5
6
|
import numpy as np
|
|
6
7
|
from bluesky.utils import Msg
|
|
7
8
|
|
|
@@ -119,3 +120,19 @@ def wait_for_tip_to_be_found(
|
|
|
119
120
|
raise PinNotFoundError(f"No pin found after {timeout} seconds")
|
|
120
121
|
|
|
121
122
|
return Pixel((int(found_tip[0]), int(found_tip[1])))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def convert_to_gray_and_blur(data: cv2.typing.MatLike) -> cv2.typing.MatLike:
|
|
126
|
+
"""
|
|
127
|
+
Preprocess the image array data (convert to grayscale and apply a gaussian blur)
|
|
128
|
+
Image is converted to grayscale (using a weighted mean as green contributes more to brightness)
|
|
129
|
+
as we aren't interested in data relating to colour. A blur is then applied to mitigate
|
|
130
|
+
errors due to rogue hot pixels.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
# kernel size describes how many of the neighbouring pixels are used for the blur,
|
|
134
|
+
# higher kernal size means more of a blur effect
|
|
135
|
+
kernel_size = (7, 7)
|
|
136
|
+
|
|
137
|
+
gray_arr = cv2.cvtColor(data, cv2.COLOR_BGR2GRAY)
|
|
138
|
+
return cv2.GaussianBlur(gray_arr, kernel_size, 0)
|
dodal/devices/robot.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from asyncio import FIRST_COMPLETED, CancelledError, Task, wait_for
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
4
5
|
|
|
5
6
|
from bluesky.protocols import Movable
|
|
6
7
|
from ophyd_async.core import (
|
|
@@ -21,8 +22,8 @@ from ophyd_async.epics.core import (
|
|
|
21
22
|
|
|
22
23
|
from dodal.log import LOGGER
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
WAIT_FOR_BEAMLINE_DISABLE_MSG = "Waiting on beamline disable"
|
|
26
|
+
WAIT_FOR_BEAMLINE_ENABLE_MSG = "Waiting on beamline enable"
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class RobotLoadError(Exception):
|
|
@@ -43,11 +44,19 @@ class SampleLocation:
|
|
|
43
44
|
pin: int
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
SAMPLE_LOCATION_EMPTY = SampleLocation(-1, -1)
|
|
48
|
+
|
|
49
|
+
|
|
46
50
|
class PinMounted(StrictEnum):
|
|
47
51
|
NO_PIN_MOUNTED = "No Pin Mounted"
|
|
48
52
|
PIN_MOUNTED = "Pin Mounted"
|
|
49
53
|
|
|
50
54
|
|
|
55
|
+
class BeamlineStatus(IntEnum):
|
|
56
|
+
ENABLED = 0
|
|
57
|
+
DISABLED = 1
|
|
58
|
+
|
|
59
|
+
|
|
51
60
|
class ErrorStatus(Device):
|
|
52
61
|
def __init__(self, prefix: str) -> None:
|
|
53
62
|
self.str = epics_signal_r(str, prefix + "_ERR_MSG")
|
|
@@ -61,7 +70,7 @@ class ErrorStatus(Device):
|
|
|
61
70
|
raise RobotLoadError(int(error_code), error_string) from raise_from
|
|
62
71
|
|
|
63
72
|
|
|
64
|
-
class BartRobot(StandardReadable, Movable[SampleLocation
|
|
73
|
+
class BartRobot(StandardReadable, Movable[SampleLocation]):
|
|
65
74
|
"""The sample changing robot."""
|
|
66
75
|
|
|
67
76
|
# How long to wait for the robot if it is busy soaking/drying
|
|
@@ -86,6 +95,8 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
|
|
|
86
95
|
self.current_puck = epics_signal_r(float, prefix + "CURRENT_PUCK_RBV")
|
|
87
96
|
self.current_pin = epics_signal_r(float, prefix + "CURRENT_PIN_RBV")
|
|
88
97
|
|
|
98
|
+
self.beamline_disabled = epics_signal_r(int, prefix + "ROBOT_OP_16_BITS.B8")
|
|
99
|
+
|
|
89
100
|
self.next_pin = epics_signal_rw_rbv(float, prefix + "NEXT_PIN")
|
|
90
101
|
self.next_puck = epics_signal_rw_rbv(float, prefix + "NEXT_PUCK")
|
|
91
102
|
|
|
@@ -116,8 +127,8 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
|
|
|
116
127
|
)
|
|
117
128
|
super().__init__(name=name)
|
|
118
129
|
|
|
119
|
-
async def
|
|
120
|
-
"""This co-routine will finish when either the
|
|
130
|
+
async def beamline_status_or_error(self, expected_state: BeamlineStatus):
|
|
131
|
+
"""This co-routine will finish when either the beamline reaches the specified
|
|
121
132
|
state or the robot gives an error (whichever happens first). In the case where
|
|
122
133
|
there is an error a RobotLoadError error is raised.
|
|
123
134
|
"""
|
|
@@ -130,12 +141,12 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
|
|
|
130
141
|
error_msg = await self.prog_error.str.get_value()
|
|
131
142
|
raise RobotLoadError(error_code, error_msg)
|
|
132
143
|
|
|
133
|
-
async def
|
|
134
|
-
await wait_for_value(self.
|
|
144
|
+
async def wait_for_expected_state():
|
|
145
|
+
await wait_for_value(self.beamline_disabled, expected_state.value, None)
|
|
135
146
|
|
|
136
147
|
tasks = [
|
|
137
148
|
(Task(raise_if_error())),
|
|
138
|
-
(Task(
|
|
149
|
+
(Task(wait_for_expected_state())),
|
|
139
150
|
]
|
|
140
151
|
try:
|
|
141
152
|
finished, unfinished = await asyncio.wait(
|
|
@@ -171,31 +182,37 @@ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
|
|
|
171
182
|
set_and_wait_for_value(self.next_pin, sample_location.pin),
|
|
172
183
|
)
|
|
173
184
|
await self.load.trigger()
|
|
174
|
-
|
|
175
|
-
LOGGER.info(WAIT_FOR_OLD_PIN_MSG)
|
|
176
|
-
await self.pin_state_or_error(PinMounted.NO_PIN_MOUNTED)
|
|
177
|
-
LOGGER.info(WAIT_FOR_NEW_PIN_MSG)
|
|
185
|
+
await self._wait_for_beamline_enabled_after_load_or_unload()
|
|
178
186
|
|
|
179
|
-
|
|
187
|
+
async def _wait_for_beamline_enabled_after_load_or_unload(self):
|
|
188
|
+
if await self.beamline_disabled.get_value() == BeamlineStatus.ENABLED.value:
|
|
189
|
+
LOGGER.info(WAIT_FOR_BEAMLINE_DISABLE_MSG)
|
|
190
|
+
await self.beamline_status_or_error(BeamlineStatus.DISABLED)
|
|
191
|
+
|
|
192
|
+
LOGGER.info(WAIT_FOR_BEAMLINE_ENABLE_MSG)
|
|
193
|
+
await self.beamline_status_or_error(BeamlineStatus.ENABLED)
|
|
180
194
|
|
|
181
195
|
@AsyncStatus.wrap
|
|
182
|
-
async def set(self, value: SampleLocation
|
|
196
|
+
async def set(self, value: SampleLocation):
|
|
183
197
|
"""
|
|
184
198
|
Perform a sample load from the specified sample location
|
|
185
199
|
Args:
|
|
186
|
-
value: The pin and puck to load, or
|
|
200
|
+
value: The pin and puck to load, or SAMPLE_LOCATION_EMPTY to unload the sample.
|
|
187
201
|
Raises:
|
|
188
|
-
RobotLoadError if a timeout occurs, or if an error occurs loading the
|
|
202
|
+
RobotLoadError if a timeout occurs, or if an error occurs loading the sample.
|
|
189
203
|
"""
|
|
190
204
|
try:
|
|
191
|
-
if value
|
|
205
|
+
if value != SAMPLE_LOCATION_EMPTY:
|
|
192
206
|
await wait_for(
|
|
193
207
|
self._load_pin_and_puck(value),
|
|
194
208
|
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
|
|
195
209
|
)
|
|
196
210
|
else:
|
|
197
211
|
await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
|
|
198
|
-
await
|
|
212
|
+
await wait_for(
|
|
213
|
+
self._wait_for_beamline_enabled_after_load_or_unload(),
|
|
214
|
+
timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
|
|
215
|
+
)
|
|
199
216
|
except TimeoutError as e:
|
|
200
217
|
await self.prog_error.raise_if_error(e)
|
|
201
218
|
await self.controller_error.raise_if_error(e)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Movable
|
|
4
|
+
from ophyd_async.core import AsyncStatus, StandardReadable, StrictEnum, soft_signal_rw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SelectedSource(StrictEnum):
|
|
8
|
+
SOURCE1 = "source1"
|
|
9
|
+
SOURCE2 = "source2"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_obj_from_selected_source(selected_source: SelectedSource, s1: T, s2: T) -> T:
|
|
16
|
+
"""Util function that maps enum values for SelectedSource to two objects. It then
|
|
17
|
+
returns one of the objects that corrosponds to the selected_source value."""
|
|
18
|
+
match selected_source:
|
|
19
|
+
case SelectedSource.SOURCE1:
|
|
20
|
+
return s1
|
|
21
|
+
case SelectedSource.SOURCE2:
|
|
22
|
+
return s2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SourceSelector(StandardReadable, Movable[SelectedSource]):
|
|
26
|
+
"""Device that holds a selected_source signal enum of SelectedSource. Useful for
|
|
27
|
+
beamlines with multiple sources to coordinate which energy source or shutter to use."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str = ""):
|
|
30
|
+
with self.add_children_as_readables():
|
|
31
|
+
self.selected_source = soft_signal_rw(
|
|
32
|
+
SelectedSource, SelectedSource.SOURCE1
|
|
33
|
+
)
|
|
34
|
+
super().__init__(name)
|
|
35
|
+
|
|
36
|
+
@AsyncStatus.wrap
|
|
37
|
+
async def set(self, value: SelectedSource):
|
|
38
|
+
await self.selected_source.set(value)
|
dodal/devices/zebra/zebra.py
CHANGED
|
@@ -7,11 +7,15 @@ from functools import partialmethod
|
|
|
7
7
|
from bluesky.protocols import Movable
|
|
8
8
|
from ophyd_async.core import (
|
|
9
9
|
AsyncStatus,
|
|
10
|
+
DeviceMock,
|
|
10
11
|
DeviceVector,
|
|
11
12
|
SignalRW,
|
|
12
13
|
StandardReadable,
|
|
13
14
|
StrictEnum,
|
|
15
|
+
callback_on_mock_put,
|
|
16
|
+
default_mock_class,
|
|
14
17
|
observe_value,
|
|
18
|
+
set_mock_value,
|
|
15
19
|
)
|
|
16
20
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
17
21
|
|
|
@@ -88,6 +92,17 @@ class SoftInState(StrictEnum):
|
|
|
88
92
|
NO = "No"
|
|
89
93
|
|
|
90
94
|
|
|
95
|
+
class InstantArmMock(DeviceMock["ArmingDevice"]):
|
|
96
|
+
async def connect(self, device: ArmingDevice) -> None:
|
|
97
|
+
callback_on_mock_put(
|
|
98
|
+
device.arm_set, lambda *_, **__: set_mock_value(device.armed, 1)
|
|
99
|
+
)
|
|
100
|
+
callback_on_mock_put(
|
|
101
|
+
device.disarm_set, lambda *_, **__: set_mock_value(device.armed, 0)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@default_mock_class(InstantArmMock)
|
|
91
106
|
class ArmingDevice(StandardReadable, Movable[ArmDemand]):
|
|
92
107
|
"""A useful device that can abstract some of the logic of arming.
|
|
93
108
|
Allows a user to just call arm.set(ArmDemand.ARM)"""
|
|
@@ -49,6 +49,7 @@ class ZebraTTLOutputs(ZebraMappingValidations):
|
|
|
49
49
|
TTL_SHUTTER: int = Field(default=-1, ge=-1, le=4)
|
|
50
50
|
TTL_XSPRESS3: int = Field(default=-1, ge=-1, le=4)
|
|
51
51
|
TTL_PANDA: int = Field(default=-1, ge=-1, le=4)
|
|
52
|
+
TTL_JUNGFRAU: int = Field(default=-1, ge=-1, le=4)
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
class ZebraSources(ZebraMappingValidations):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|