dls-dodal 1.29.3__py3-none-any.whl → 1.30.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.29.3.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +28 -43
- dls_dodal-1.30.0.dist-info/RECORD +132 -0
- {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
- dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
- dodal/__init__.py +1 -4
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +3 -1
- dodal/beamlines/i03.py +28 -23
- dodal/beamlines/i04.py +34 -12
- dodal/beamlines/i13_1.py +66 -0
- dodal/beamlines/i22.py +5 -5
- dodal/beamlines/i24.py +15 -1
- dodal/beamlines/p38.py +7 -7
- dodal/beamlines/p45.py +7 -5
- dodal/beamlines/p99.py +61 -0
- dodal/cli.py +6 -3
- dodal/common/beamlines/beamline_parameters.py +2 -2
- dodal/common/beamlines/beamline_utils.py +6 -5
- dodal/common/maths.py +1 -3
- dodal/common/types.py +2 -3
- dodal/common/udc_directory_provider.py +14 -3
- dodal/common/visit.py +2 -3
- dodal/devices/CTAB.py +22 -17
- dodal/devices/aperturescatterguard.py +114 -136
- dodal/devices/areadetector/adaravis.py +8 -6
- dodal/devices/areadetector/adsim.py +2 -3
- dodal/devices/areadetector/adutils.py +20 -12
- dodal/devices/areadetector/plugins/MJPG.py +0 -4
- dodal/devices/attenuator.py +4 -4
- dodal/devices/cryostream.py +19 -7
- dodal/devices/detector/__init__.py +13 -2
- dodal/devices/detector/det_dim_constants.py +2 -2
- dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
- dodal/devices/detector/detector.py +8 -7
- dodal/devices/detector/detector_motion.py +38 -31
- dodal/devices/eiger.py +23 -23
- dodal/devices/eiger_odin.py +12 -13
- dodal/devices/fast_grid_scan.py +4 -3
- dodal/devices/fluorescence_detector_motion.py +13 -4
- dodal/devices/focusing_mirror.py +66 -66
- dodal/devices/hutch_shutter.py +4 -4
- dodal/devices/i22/dcm.py +4 -3
- dodal/devices/i22/fswitch.py +4 -4
- dodal/devices/i22/nxsas.py +23 -32
- dodal/devices/i24/dcm.py +42 -0
- dodal/devices/i24/pmac.py +47 -8
- dodal/devices/ipin.py +7 -4
- dodal/devices/linkam3.py +11 -5
- dodal/devices/logging_ophyd_device.py +1 -1
- dodal/devices/motors.py +31 -5
- dodal/devices/oav/grid_overlay.py +1 -0
- dodal/devices/oav/microns_for_zoom_levels.json +1 -1
- dodal/devices/oav/oav_detector.py +2 -0
- dodal/devices/oav/oav_parameters.py +18 -10
- dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
- dodal/devices/oav/pin_image_recognition/__init__.py +19 -17
- dodal/devices/oav/pin_image_recognition/utils.py +5 -6
- dodal/devices/oav/utils.py +3 -17
- dodal/devices/p99/__init__.py +0 -0
- dodal/devices/p99/sample_stage.py +43 -0
- dodal/devices/robot.py +30 -18
- dodal/devices/scintillator.py +8 -5
- dodal/devices/smargon.py +3 -3
- dodal/devices/status.py +2 -31
- dodal/devices/tetramm.py +4 -4
- dodal/devices/thawer.py +5 -3
- dodal/devices/undulator_dcm.py +6 -8
- dodal/devices/util/adjuster_plans.py +2 -2
- dodal/devices/util/epics_util.py +6 -8
- dodal/devices/util/lookup_tables.py +2 -3
- dodal/devices/util/save_panda.py +87 -0
- dodal/devices/util/test_utils.py +17 -0
- dodal/devices/webcam.py +3 -8
- dodal/devices/xbpm_feedback.py +0 -23
- dodal/devices/zebra.py +10 -10
- dodal/devices/zebra_controlled_shutter.py +3 -3
- dodal/devices/zocalo/zocalo_interaction.py +10 -2
- dodal/devices/zocalo/zocalo_results.py +31 -18
- dodal/log.py +14 -5
- dodal/plans/data_session_metadata.py +1 -0
- dodal/plans/motor_util_plans.py +117 -0
- dodal/utils.py +74 -26
- dls_dodal-1.29.3.dist-info/RECORD +0 -124
- dls_dodal-1.29.3.dist-info/entry_points.txt +0 -2
- dodal/devices/qbpm1.py +0 -8
- {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.29.3.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
|
@@ -1,37 +1,44 @@
|
|
|
1
|
-
from enum import
|
|
1
|
+
from enum import Enum
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from ophyd_async.core import Device
|
|
4
|
+
from ophyd_async.epics.motion import Motor
|
|
5
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class ShutterState(
|
|
8
|
-
CLOSED =
|
|
9
|
-
OPEN =
|
|
8
|
+
class ShutterState(str, Enum):
|
|
9
|
+
CLOSED = "Closed"
|
|
10
|
+
OPEN = "Open"
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class DetectorMotion(Device):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
15
|
+
device_prefix = "-MO-DET-01:"
|
|
16
|
+
pmac_prefix = "-MO-PMAC-02:"
|
|
17
|
+
|
|
18
|
+
self.upstream_x = Motor(f"{prefix}{device_prefix}UPSTREAMX")
|
|
19
|
+
self.downstream_x = Motor(f"{prefix}{device_prefix}DOWNSTREAMX")
|
|
20
|
+
self.x = Motor(f"{prefix}{device_prefix}X")
|
|
21
|
+
self.y = Motor(f"{prefix}{device_prefix}Y")
|
|
22
|
+
self.z = Motor(f"{prefix}{device_prefix}Z")
|
|
23
|
+
self.yaw = Motor(f"{prefix}{device_prefix}YAW")
|
|
24
|
+
|
|
25
|
+
self.shutter = epics_signal_rw(
|
|
26
|
+
ShutterState, f"{prefix}{device_prefix}SET_SHUTTER_STATE"
|
|
27
|
+
)
|
|
28
|
+
self.shutter_closed_lim = epics_signal_r(
|
|
29
|
+
float, f"{prefix}{device_prefix}CLOSE_LIMIT"
|
|
30
|
+
) # on limit = 1, off = 0
|
|
31
|
+
self.shutter_open_lim = epics_signal_r(
|
|
32
|
+
float, f"{prefix}{device_prefix}OPEN_LIMIT"
|
|
33
|
+
) # on limit = 1, off = 0
|
|
34
|
+
self.z_disabled = epics_signal_r(
|
|
35
|
+
float, f"{prefix}{device_prefix}Z:DISABLED"
|
|
36
|
+
) # robot interlock, 0=ok to move, 1=blocked
|
|
37
|
+
self.crate_power = epics_signal_r(
|
|
38
|
+
float, f"{prefix}{pmac_prefix}CRATE2_HEALTHY"
|
|
39
|
+
) # returns 0 if no power
|
|
40
|
+
self.in_robot_load_safe_position = epics_signal_r(
|
|
41
|
+
int, f"{prefix}{pmac_prefix}GPIO_INP_BITS.B2"
|
|
42
|
+
) # returns 1 if safe
|
|
43
|
+
|
|
44
|
+
super().__init__(name)
|
dodal/devices/eiger.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
# type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
|
|
1
2
|
from enum import Enum
|
|
2
3
|
|
|
3
4
|
from ophyd import Component, Device, EpicsSignalRO, Signal
|
|
4
5
|
from ophyd.areadetector.cam import EigerDetectorCam
|
|
5
|
-
from ophyd.status import AndStatus, Status,
|
|
6
|
+
from ophyd.status import AndStatus, Status, StatusBase
|
|
6
7
|
|
|
7
8
|
from dodal.devices.detector import DetectorParams, TriggerMode
|
|
8
9
|
from dodal.devices.eiger_odin import EigerOdin
|
|
@@ -12,10 +13,6 @@ from dodal.log import LOGGER
|
|
|
12
13
|
|
|
13
14
|
FREE_RUN_MAX_IMAGES = 1000000
|
|
14
15
|
|
|
15
|
-
# TODO present for testing purposes, remove
|
|
16
|
-
TEST_1169_FIX = True
|
|
17
|
-
TEST_1169_INJECT = False
|
|
18
|
-
|
|
19
16
|
|
|
20
17
|
class InternalEigerTriggerMode(Enum):
|
|
21
18
|
INTERNAL_SERIES = 0
|
|
@@ -27,6 +24,7 @@ class InternalEigerTriggerMode(Enum):
|
|
|
27
24
|
class EigerDetector(Device):
|
|
28
25
|
class ArmingSignal(Signal):
|
|
29
26
|
def set(self, value, *, timeout=None, settle_time=None, **kwargs):
|
|
27
|
+
assert isinstance(self.parent, EigerDetector)
|
|
30
28
|
return self.parent.async_stage()
|
|
31
29
|
|
|
32
30
|
do_arm = Component(ArmingSignal)
|
|
@@ -38,10 +36,12 @@ class EigerDetector(Device):
|
|
|
38
36
|
|
|
39
37
|
STALE_PARAMS_TIMEOUT = 60
|
|
40
38
|
GENERAL_STATUS_TIMEOUT = 10
|
|
39
|
+
# Long timeout for meta file to compensate for filesystem issues
|
|
40
|
+
META_FILE_READY_TIMEOUT = 30
|
|
41
41
|
ALL_FRAMES_TIMEOUT = 120
|
|
42
42
|
ARMING_TIMEOUT = 60
|
|
43
43
|
|
|
44
|
-
filewriters_finished:
|
|
44
|
+
filewriters_finished: StatusBase
|
|
45
45
|
|
|
46
46
|
detector_params: DetectorParams | None = None
|
|
47
47
|
|
|
@@ -53,17 +53,15 @@ class EigerDetector(Device):
|
|
|
53
53
|
cls,
|
|
54
54
|
params: DetectorParams,
|
|
55
55
|
name: str = "EigerDetector",
|
|
56
|
-
*args,
|
|
57
|
-
**kwargs,
|
|
58
56
|
):
|
|
59
|
-
det = cls(name=name
|
|
57
|
+
det = cls(name=name)
|
|
60
58
|
det.set_detector_parameters(params)
|
|
61
59
|
return det
|
|
62
60
|
|
|
63
61
|
def set_detector_parameters(self, detector_params: DetectorParams):
|
|
64
62
|
self.detector_params = detector_params
|
|
65
63
|
if self.detector_params is None:
|
|
66
|
-
raise
|
|
64
|
+
raise ValueError("Parameters for scan must be specified")
|
|
67
65
|
|
|
68
66
|
to_check = [
|
|
69
67
|
(
|
|
@@ -100,7 +98,7 @@ class EigerDetector(Device):
|
|
|
100
98
|
|
|
101
99
|
def stage(self):
|
|
102
100
|
self.wait_on_arming_if_started()
|
|
103
|
-
if
|
|
101
|
+
if not self.is_armed():
|
|
104
102
|
LOGGER.info("Eiger not armed, arming")
|
|
105
103
|
|
|
106
104
|
self.async_stage().wait(timeout=self.ARMING_TIMEOUT)
|
|
@@ -155,7 +153,7 @@ class EigerDetector(Device):
|
|
|
155
153
|
def enable_roi_mode(self):
|
|
156
154
|
return self.change_roi_mode(True)
|
|
157
155
|
|
|
158
|
-
def change_roi_mode(self, enable: bool) ->
|
|
156
|
+
def change_roi_mode(self, enable: bool) -> StatusBase:
|
|
159
157
|
assert self.detector_params is not None
|
|
160
158
|
detector_dimensions = (
|
|
161
159
|
self.detector_params.detector_size_constants.roi_size_pixels
|
|
@@ -206,7 +204,7 @@ class EigerDetector(Device):
|
|
|
206
204
|
)
|
|
207
205
|
return status
|
|
208
206
|
|
|
209
|
-
def set_odin_pvs(self) ->
|
|
207
|
+
def set_odin_pvs(self) -> StatusBase:
|
|
210
208
|
assert self.detector_params is not None
|
|
211
209
|
file_prefix = self.detector_params.full_filename
|
|
212
210
|
status = self.odin.file_writer.file_path.set(
|
|
@@ -264,7 +262,7 @@ class EigerDetector(Device):
|
|
|
264
262
|
status.set_finished()
|
|
265
263
|
return status
|
|
266
264
|
|
|
267
|
-
def set_num_triggers_and_captures(self) ->
|
|
265
|
+
def set_num_triggers_and_captures(self) -> StatusBase:
|
|
268
266
|
"""Sets the number of triggers and the number of images for the Eiger to capture
|
|
269
267
|
during the datacollection. The number of images is the number of images per
|
|
270
268
|
trigger.
|
|
@@ -295,7 +293,7 @@ class EigerDetector(Device):
|
|
|
295
293
|
|
|
296
294
|
return status
|
|
297
295
|
|
|
298
|
-
def _wait_for_odin_status(self) ->
|
|
296
|
+
def _wait_for_odin_status(self) -> StatusBase:
|
|
299
297
|
self.forward_bit_depth_to_filewriter()
|
|
300
298
|
await_value(self.odin.meta.active, 1).wait(self.GENERAL_STATUS_TIMEOUT)
|
|
301
299
|
|
|
@@ -304,18 +302,20 @@ class EigerDetector(Device):
|
|
|
304
302
|
)
|
|
305
303
|
LOGGER.info("Eiger staging: awaiting odin metadata")
|
|
306
304
|
status &= await_value(
|
|
307
|
-
self.odin.meta.ready, 1, timeout=self.
|
|
305
|
+
self.odin.meta.ready, 1, timeout=self.META_FILE_READY_TIMEOUT
|
|
308
306
|
)
|
|
309
307
|
return status
|
|
310
308
|
|
|
311
|
-
def _wait_fan_ready(self) ->
|
|
309
|
+
def _wait_fan_ready(self) -> StatusBase:
|
|
312
310
|
self.filewriters_finished = self.odin.create_finished_status()
|
|
313
311
|
LOGGER.info("Eiger staging: awaiting odin fan ready")
|
|
314
312
|
return await_value(self.odin.fan.ready, 1, self.GENERAL_STATUS_TIMEOUT)
|
|
315
313
|
|
|
316
314
|
def _finish_arm(self) -> Status:
|
|
317
315
|
LOGGER.info("Eiger staging: Finishing arming")
|
|
318
|
-
|
|
316
|
+
status = Status()
|
|
317
|
+
status.set_finished()
|
|
318
|
+
return status
|
|
319
319
|
|
|
320
320
|
def forward_bit_depth_to_filewriter(self):
|
|
321
321
|
bit_depth = self.bit_depth.get()
|
|
@@ -332,11 +332,16 @@ class EigerDetector(Device):
|
|
|
332
332
|
|
|
333
333
|
def do_arming_chain(self) -> Status:
|
|
334
334
|
functions_to_do_arm = []
|
|
335
|
+
assert self.detector_params
|
|
335
336
|
detector_params: DetectorParams = self.detector_params
|
|
336
337
|
if detector_params.use_roi_mode:
|
|
337
338
|
functions_to_do_arm.append(self.enable_roi_mode)
|
|
338
339
|
|
|
339
340
|
arming_sequence_funcs = [
|
|
341
|
+
# If a beam dump occurs after arming the eiger but prior to eiger staging,
|
|
342
|
+
# the odin may timeout which will cause the arming sequence to be retried;
|
|
343
|
+
# if this previously completed successfully we must reset the odin first
|
|
344
|
+
self.odin.stop,
|
|
340
345
|
lambda: self.change_dev_shm(detector_params.enable_dev_shm),
|
|
341
346
|
lambda: self.set_detector_threshold(detector_params.expected_energy_ev),
|
|
342
347
|
self.set_cam_pvs,
|
|
@@ -350,11 +355,6 @@ class EigerDetector(Device):
|
|
|
350
355
|
self._wait_fan_ready,
|
|
351
356
|
self._finish_arm,
|
|
352
357
|
]
|
|
353
|
-
if TEST_1169_FIX:
|
|
354
|
-
# If a beam dump occurs after arming the eiger but prior to eiger staging,
|
|
355
|
-
# the odin may timeout which will cause the arming sequence to be retried;
|
|
356
|
-
# if this previously completed successfully we must reset the odin first
|
|
357
|
-
arming_sequence_funcs.insert(0, self.odin.stop)
|
|
358
358
|
|
|
359
359
|
functions_to_do_arm.extend(arming_sequence_funcs)
|
|
360
360
|
|
dodal/devices/eiger_odin.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
|
|
3
2
|
from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
|
4
3
|
from ophyd.areadetector.plugins import HDF5Plugin_V22
|
|
5
4
|
from ophyd.sim import NullStatus
|
|
6
|
-
from ophyd.status import
|
|
5
|
+
from ophyd.status import StatusBase
|
|
7
6
|
|
|
8
7
|
from dodal.devices.status import await_value
|
|
9
8
|
|
|
@@ -59,12 +58,12 @@ class OdinNodesStatus(Device):
|
|
|
59
58
|
node_3 = Component(OdinNode, "OD4:")
|
|
60
59
|
|
|
61
60
|
@property
|
|
62
|
-
def nodes(self) ->
|
|
61
|
+
def nodes(self) -> list[OdinNode]:
|
|
63
62
|
return [self.node_0, self.node_1, self.node_2, self.node_3]
|
|
64
63
|
|
|
65
64
|
def check_node_frames_from_attr(
|
|
66
65
|
self, node_get_func, error_message_verb: str
|
|
67
|
-
) ->
|
|
66
|
+
) -> tuple[bool, str]:
|
|
68
67
|
nodes_frames_values = [0] * len(self.nodes)
|
|
69
68
|
frames_details = []
|
|
70
69
|
for node_number, node_pv in enumerate(self.nodes):
|
|
@@ -75,17 +74,17 @@ class OdinNodesStatus(Device):
|
|
|
75
74
|
bad_frames = any(v != 0 for v in nodes_frames_values)
|
|
76
75
|
return bad_frames, "\n".join(frames_details)
|
|
77
76
|
|
|
78
|
-
def check_frames_timed_out(self) ->
|
|
77
|
+
def check_frames_timed_out(self) -> tuple[bool, str]:
|
|
79
78
|
return self.check_node_frames_from_attr(
|
|
80
79
|
lambda node: node.frames_timed_out.get(), "timed out"
|
|
81
80
|
)
|
|
82
81
|
|
|
83
|
-
def check_frames_dropped(self) ->
|
|
82
|
+
def check_frames_dropped(self) -> tuple[bool, str]:
|
|
84
83
|
return self.check_node_frames_from_attr(
|
|
85
84
|
lambda node: node.frames_dropped.get(), "dropped"
|
|
86
85
|
)
|
|
87
86
|
|
|
88
|
-
def get_error_state(self) ->
|
|
87
|
+
def get_error_state(self) -> tuple[bool, str]:
|
|
89
88
|
is_error = []
|
|
90
89
|
error_messages = []
|
|
91
90
|
for node_number, node_pv in enumerate(self.nodes):
|
|
@@ -99,7 +98,7 @@ class OdinNodesStatus(Device):
|
|
|
99
98
|
|
|
100
99
|
def get_init_state(self) -> bool:
|
|
101
100
|
is_initialised = []
|
|
102
|
-
for
|
|
101
|
+
for node_pv in self.nodes:
|
|
103
102
|
is_initialised.append(node_pv.fr_initialised.get())
|
|
104
103
|
is_initialised.append(node_pv.fp_initialised.get())
|
|
105
104
|
return all(is_initialised)
|
|
@@ -120,7 +119,7 @@ class EigerOdin(Device):
|
|
|
120
119
|
meta = Component(OdinMetaListener, "OD:META:")
|
|
121
120
|
nodes = Component(OdinNodesStatus, "")
|
|
122
121
|
|
|
123
|
-
def create_finished_status(self) ->
|
|
122
|
+
def create_finished_status(self) -> StatusBase:
|
|
124
123
|
writing_finished = await_value(self.meta.ready, 0)
|
|
125
124
|
for node_pv in self.nodes.nodes:
|
|
126
125
|
writing_finished &= await_value(node_pv.writing, 0)
|
|
@@ -132,7 +131,7 @@ class EigerOdin(Device):
|
|
|
132
131
|
frames_timed_out, frames_timed_out_details = self.nodes.check_frames_timed_out()
|
|
133
132
|
|
|
134
133
|
if not is_initialised:
|
|
135
|
-
raise
|
|
134
|
+
raise RuntimeError(error_message)
|
|
136
135
|
if frames_dropped:
|
|
137
136
|
self.log.error(f"Frames dropped: {frames_dropped_details}")
|
|
138
137
|
if frames_timed_out:
|
|
@@ -140,7 +139,7 @@ class EigerOdin(Device):
|
|
|
140
139
|
|
|
141
140
|
return is_initialised and not frames_dropped and not frames_timed_out
|
|
142
141
|
|
|
143
|
-
def check_odin_initialised(self) ->
|
|
142
|
+
def check_odin_initialised(self) -> tuple[bool, str]:
|
|
144
143
|
is_error_state, error_messages = self.nodes.get_error_state()
|
|
145
144
|
to_check = [
|
|
146
145
|
(not self.fan.consumers_connected.get(), "EigerFan is not connected"),
|
|
@@ -157,7 +156,7 @@ class EigerOdin(Device):
|
|
|
157
156
|
|
|
158
157
|
return not errors, "\n".join(errors)
|
|
159
158
|
|
|
160
|
-
def stop(self) ->
|
|
159
|
+
def stop(self) -> StatusBase:
|
|
161
160
|
"""Stop odin manually"""
|
|
162
161
|
status = self.file_writer.capture.set(0)
|
|
163
162
|
status &= self.meta.stop_writing.set(1)
|
dodal/devices/fast_grid_scan.py
CHANGED
|
@@ -126,7 +126,7 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
|
|
|
126
126
|
:return: The motor position this corresponds to.
|
|
127
127
|
:raises: IndexError if the desired position is outside the grid."""
|
|
128
128
|
for position, axis in zip(
|
|
129
|
-
grid_position, [self.x_axis, self.y_axis, self.z_axis]
|
|
129
|
+
grid_position, [self.x_axis, self.y_axis, self.z_axis], strict=False
|
|
130
130
|
):
|
|
131
131
|
if not axis.is_within(position):
|
|
132
132
|
raise IndexError(f"{grid_position} is outside the bounds of the grid")
|
|
@@ -191,9 +191,10 @@ class MotionProgram(Device):
|
|
|
191
191
|
class ExpectedImages(SignalR[int]):
|
|
192
192
|
def __init__(self, parent: "FastGridScanCommon") -> None:
|
|
193
193
|
super().__init__(SoftSignalBackend(int))
|
|
194
|
-
self.parent
|
|
194
|
+
self.parent = parent
|
|
195
195
|
|
|
196
|
-
async def get_value(self):
|
|
196
|
+
async def get_value(self, cached: bool | None = None):
|
|
197
|
+
assert isinstance(self.parent, FastGridScanCommon)
|
|
197
198
|
x = await self.parent.x_steps.get_value()
|
|
198
199
|
y = await self.parent.y_steps.get_value()
|
|
199
200
|
z = await self.parent.z_steps.get_value()
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
from
|
|
2
|
-
from ophyd import Device, EpicsSignal
|
|
1
|
+
from enum import Enum
|
|
3
2
|
|
|
3
|
+
from ophyd_async.core import StandardReadable
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_r
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
class FluorescenceDetectorControlState(Enum):
|
|
6
8
|
OUT = 0
|
|
7
9
|
IN = 1
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
class FluorescenceDetector(StandardReadable):
|
|
13
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
14
|
+
with self.add_children_as_readables():
|
|
15
|
+
self.pos = epics_signal_r(
|
|
16
|
+
FluorescenceDetectorControlState, prefix + "-EA-FLU-01:CTRL"
|
|
17
|
+
)
|
|
18
|
+
super().__init__(name)
|
dodal/devices/focusing_mirror.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
ConfigSignal,
|
|
6
|
+
Device,
|
|
7
|
+
DeviceVector,
|
|
8
|
+
HintedSignal,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
observe_value,
|
|
11
|
+
)
|
|
7
12
|
from ophyd_async.core.signal import soft_signal_r_and_setter
|
|
8
13
|
from ophyd_async.epics.motion import Motor
|
|
9
14
|
from ophyd_async.epics.signal import (
|
|
15
|
+
epics_signal_r,
|
|
10
16
|
epics_signal_rw,
|
|
11
17
|
epics_signal_x,
|
|
12
18
|
)
|
|
@@ -32,11 +38,11 @@ class MirrorStripe(str, Enum):
|
|
|
32
38
|
PLATINUM = "Platinum"
|
|
33
39
|
|
|
34
40
|
|
|
35
|
-
class MirrorVoltageDemand(
|
|
36
|
-
N_A =
|
|
37
|
-
OK =
|
|
38
|
-
FAIL =
|
|
39
|
-
SLEW =
|
|
41
|
+
class MirrorVoltageDemand(str, Enum):
|
|
42
|
+
N_A = "N/A"
|
|
43
|
+
OK = "OK"
|
|
44
|
+
FAIL = "FAIL"
|
|
45
|
+
SLEW = "SLEW"
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
class MirrorVoltageDevice(Device):
|
|
@@ -44,14 +50,16 @@ class MirrorVoltageDevice(Device):
|
|
|
44
50
|
the demanded voltage setpoint is accepted, without blocking the caller as this process can take significant time.
|
|
45
51
|
"""
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
def __init__(self, name: str = "", prefix: str = ""):
|
|
54
|
+
self._actual_v = epics_signal_r(int, prefix + "R")
|
|
55
|
+
self._setpoint_v = epics_signal_rw(int, prefix + "D")
|
|
56
|
+
self._demand_accepted = epics_signal_r(MirrorVoltageDemand, prefix + "DSEV")
|
|
57
|
+
super().__init__(name=name)
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
@AsyncStatus.wrap
|
|
60
|
+
async def set(self, value, *args, **kwargs):
|
|
52
61
|
"""Combine the following operations into a single set:
|
|
53
62
|
1. apply the value to the setpoint PV
|
|
54
|
-
2. Return to the caller with a Status future
|
|
55
63
|
3. Wait until demand is accepted
|
|
56
64
|
4. When either demand is accepted or DEFAULT_SETTLE_TIME expires, signal the result on the Status
|
|
57
65
|
"""
|
|
@@ -59,66 +67,60 @@ class MirrorVoltageDevice(Device):
|
|
|
59
67
|
setpoint_v = self._setpoint_v
|
|
60
68
|
demand_accepted = self._demand_accepted
|
|
61
69
|
|
|
62
|
-
if demand_accepted.
|
|
70
|
+
if await demand_accepted.get_value() != MirrorVoltageDemand.OK:
|
|
63
71
|
raise AssertionError(
|
|
64
72
|
f"Attempted to set {setpoint_v.name} when demand is not accepted."
|
|
65
73
|
)
|
|
66
74
|
|
|
67
|
-
if setpoint_v.
|
|
75
|
+
if await setpoint_v.get_value() == value:
|
|
68
76
|
LOGGER.debug(f"{setpoint_v.name} already at {value} - skipping set")
|
|
69
|
-
return
|
|
77
|
+
return
|
|
70
78
|
|
|
71
79
|
LOGGER.debug(f"setting {setpoint_v.name} to {value}")
|
|
72
|
-
demand_accepted_status = Status(self, DEFAULT_SETTLE_TIME_S)
|
|
73
|
-
|
|
74
|
-
subscription: dict[str, Any] = {"handle": None}
|
|
75
|
-
|
|
76
|
-
def demand_check_callback(old_value, value, **kwargs):
|
|
77
|
-
LOGGER.debug(f"Got event old={old_value} new={value} for {setpoint_v.name}")
|
|
78
|
-
if old_value != MirrorVoltageDemand.OK and value == MirrorVoltageDemand.OK:
|
|
79
|
-
LOGGER.debug(f"Demand accepted for {setpoint_v.name}")
|
|
80
|
-
subs_handle = subscription.pop("handle", None)
|
|
81
|
-
if subs_handle is None:
|
|
82
|
-
raise AssertionError("Demand accepted before set attempted")
|
|
83
|
-
demand_accepted.unsubscribe(subs_handle)
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
# Register an observer up front to ensure we don't miss events after we
|
|
82
|
+
# perform the set
|
|
83
|
+
demand_accepted_iterator = observe_value(
|
|
84
|
+
demand_accepted, timeout=DEFAULT_SETTLE_TIME_S
|
|
85
|
+
)
|
|
86
|
+
# discard the current value (OK) so we can await a subsequent change
|
|
87
|
+
await anext(demand_accepted_iterator)
|
|
88
|
+
await setpoint_v.set(value)
|
|
89
|
+
|
|
90
|
+
# The set should always change to SLEW regardless of whether we are
|
|
91
|
+
# already at the set point, then change back to OK/FAIL depending on
|
|
92
|
+
# success
|
|
93
|
+
accepted_value = await anext(demand_accepted_iterator)
|
|
94
|
+
assert accepted_value == MirrorVoltageDemand.SLEW
|
|
95
|
+
LOGGER.debug(
|
|
96
|
+
f"Demand not accepted for {setpoint_v.name}, waiting for acceptance..."
|
|
97
|
+
)
|
|
98
|
+
while MirrorVoltageDemand.SLEW == (
|
|
99
|
+
accepted_value := await anext(demand_accepted_iterator)
|
|
100
|
+
):
|
|
101
|
+
pass
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
if accepted_value != MirrorVoltageDemand.OK:
|
|
104
|
+
raise AssertionError(
|
|
105
|
+
f"Voltage slew failed for {setpoint_v.name}, new state={accepted_value}"
|
|
106
|
+
)
|
|
92
107
|
|
|
93
108
|
|
|
94
|
-
class VFMMirrorVoltages(
|
|
95
|
-
def __init__(
|
|
96
|
-
|
|
109
|
+
class VFMMirrorVoltages(StandardReadable):
|
|
110
|
+
def __init__(
|
|
111
|
+
self, name: str, prefix: str, *args, daq_configuration_path: str, **kwargs
|
|
112
|
+
):
|
|
97
113
|
self.voltage_lookup_table_path = (
|
|
98
114
|
daq_configuration_path + "/json/mirrorFocus.json"
|
|
99
115
|
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
_channel21_voltage_device = Component(MirrorVoltageDevice, "BM:V21")
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def voltage_channels(self) -> list[MirrorVoltageDevice]:
|
|
112
|
-
return [
|
|
113
|
-
self._channel14_voltage_device,
|
|
114
|
-
self._channel15_voltage_device,
|
|
115
|
-
self._channel16_voltage_device,
|
|
116
|
-
self._channel17_voltage_device,
|
|
117
|
-
self._channel18_voltage_device,
|
|
118
|
-
self._channel19_voltage_device,
|
|
119
|
-
self._channel20_voltage_device,
|
|
120
|
-
self._channel21_voltage_device,
|
|
121
|
-
]
|
|
116
|
+
with self.add_children_as_readables():
|
|
117
|
+
self.voltage_channels = DeviceVector(
|
|
118
|
+
{
|
|
119
|
+
i - 14: MirrorVoltageDevice(prefix=f"{prefix}BM:V{i}")
|
|
120
|
+
for i in range(14, 22)
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
super().__init__(*args, name=name, **kwargs)
|
|
122
124
|
|
|
123
125
|
|
|
124
126
|
class FocusingMirror(StandardReadable):
|
|
@@ -144,10 +146,8 @@ class FocusingMirror(StandardReadable):
|
|
|
144
146
|
# regardless of orientation of the mirror
|
|
145
147
|
self.incident_angle = Motor(prefix + "PITCH")
|
|
146
148
|
|
|
147
|
-
self.
|
|
148
|
-
|
|
149
|
-
config=[self.type],
|
|
150
|
-
)
|
|
149
|
+
self.add_readables([self.incident_angle.user_readback], wrapper=HintedSignal)
|
|
150
|
+
self.add_readables([self.type], wrapper=ConfigSignal)
|
|
151
151
|
super().__init__(name)
|
|
152
152
|
|
|
153
153
|
|
dodal/devices/hutch_shutter.py
CHANGED
|
@@ -74,20 +74,20 @@ class HutchShutter(StandardReadable, Movable):
|
|
|
74
74
|
super().__init__(name)
|
|
75
75
|
|
|
76
76
|
@AsyncStatus.wrap
|
|
77
|
-
async def set(self,
|
|
77
|
+
async def set(self, value: ShutterDemand):
|
|
78
78
|
interlock_state = await self.interlock.shutter_safe_to_operate()
|
|
79
79
|
if not interlock_state:
|
|
80
80
|
raise ShutterNotSafeToOperateError(
|
|
81
81
|
"The hutch has not been locked, not operating shutter."
|
|
82
82
|
)
|
|
83
|
-
if
|
|
83
|
+
if value == ShutterDemand.OPEN:
|
|
84
84
|
await self.control.set(ShutterDemand.RESET, wait=True)
|
|
85
|
-
await self.control.set(
|
|
85
|
+
await self.control.set(value, wait=True)
|
|
86
86
|
return await wait_for_value(
|
|
87
87
|
self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
|
|
88
88
|
)
|
|
89
89
|
else:
|
|
90
|
-
await self.control.set(
|
|
90
|
+
await self.control.set(value, wait=True)
|
|
91
91
|
return await wait_for_value(
|
|
92
92
|
self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
|
|
93
93
|
)
|
dodal/devices/i22/dcm.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import time
|
|
2
|
+
from collections.abc import Sequence
|
|
2
3
|
from dataclasses import dataclass
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
from bluesky.protocols import Reading
|
|
6
7
|
from event_model.documents.event_descriptor import DataKey
|
|
@@ -127,7 +128,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
127
128
|
|
|
128
129
|
super().__init__(name)
|
|
129
130
|
|
|
130
|
-
async def describe(self) ->
|
|
131
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
131
132
|
default_describe = await super().describe()
|
|
132
133
|
return {
|
|
133
134
|
f"{self.name}-wavelength": DataKey(
|
|
@@ -139,7 +140,7 @@ class DoubleCrystalMonochromator(StandardReadable):
|
|
|
139
140
|
**default_describe,
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
async def read(self) ->
|
|
143
|
+
async def read(self) -> dict[str, Reading]:
|
|
143
144
|
default_reading = await super().read()
|
|
144
145
|
energy: float = default_reading[f"{self.name}-energy"]["value"]
|
|
145
146
|
if energy > 0.0:
|
dodal/devices/i22/fswitch.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
3
|
from enum import Enum
|
|
4
|
-
from typing import Dict
|
|
5
4
|
|
|
6
|
-
from bluesky.protocols import
|
|
5
|
+
from bluesky.protocols import Reading
|
|
6
|
+
from event_model import DataKey
|
|
7
7
|
from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
|
|
8
8
|
from ophyd_async.core.device import DeviceVector
|
|
9
9
|
from ophyd_async.epics.signal import epics_signal_r
|
|
@@ -74,7 +74,7 @@ class FSwitch(StandardReadable):
|
|
|
74
74
|
|
|
75
75
|
super().__init__(name)
|
|
76
76
|
|
|
77
|
-
async def describe(self) ->
|
|
77
|
+
async def describe(self) -> dict[str, DataKey]:
|
|
78
78
|
default_describe = await super().describe()
|
|
79
79
|
return {
|
|
80
80
|
FSwitch.NUM_LENSES_FIELD_NAME: DataKey(
|
|
@@ -83,7 +83,7 @@ class FSwitch(StandardReadable):
|
|
|
83
83
|
**default_describe,
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
async def read(self) ->
|
|
86
|
+
async def read(self) -> dict[str, Reading]:
|
|
87
87
|
result = await asyncio.gather(
|
|
88
88
|
*(filter.get_value() for filter in self.filters.values())
|
|
89
89
|
)
|