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
dodal/devices/oav/utils.py
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from enum import IntEnum
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import Generator, Tuple
|
|
4
3
|
|
|
5
4
|
import bluesky.plan_stubs as bps
|
|
6
5
|
import numpy as np
|
|
7
6
|
from bluesky.utils import Msg
|
|
8
|
-
from PIL.Image import Image
|
|
9
7
|
|
|
10
8
|
from dodal.devices.oav.oav_calculations import camera_coordinates_to_xyz
|
|
11
|
-
from dodal.devices.oav.
|
|
9
|
+
from dodal.devices.oav.oav_detector import OAVConfigParams
|
|
12
10
|
from dodal.devices.oav.pin_image_recognition import PinTipDetection
|
|
13
11
|
from dodal.devices.smargon import Smargon
|
|
14
|
-
from dodal.log import LOGGER
|
|
15
12
|
|
|
16
|
-
Pixel =
|
|
13
|
+
Pixel = tuple[int, int]
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
class PinNotFoundException(Exception):
|
|
@@ -110,14 +107,3 @@ def wait_for_tip_to_be_found(
|
|
|
110
107
|
raise PinNotFoundException(f"No pin found after {timeout} seconds")
|
|
111
108
|
|
|
112
109
|
return found_tip # type: ignore
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def save_thumbnail(full_file_path: Path, full_image: Image, new_height=192):
|
|
116
|
-
"""Scales an image down to have the height specified in new_height and saves it
|
|
117
|
-
to the same location as the full image with a t appended to the filename"""
|
|
118
|
-
thumbnail_path = full_file_path.with_stem(full_file_path.stem + "t")
|
|
119
|
-
LOGGER.info(f"Saving thumbnail to {thumbnail_path}")
|
|
120
|
-
full_size = full_image.size
|
|
121
|
-
new_width = (new_height / full_size[1]) * full_size[0]
|
|
122
|
-
full_image.thumbnail((new_width, new_height))
|
|
123
|
-
full_image.save(thumbnail_path.as_posix())
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import Device
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_rw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SampleAngleStage(Device):
|
|
8
|
+
def __init__(self, prefix: str, name: str):
|
|
9
|
+
self.theta = epics_signal_rw(
|
|
10
|
+
float, prefix + "WRITETHETA:RBV", prefix + "WRITETHETA"
|
|
11
|
+
)
|
|
12
|
+
self.roll = epics_signal_rw(
|
|
13
|
+
float, prefix + "WRITEROLL:RBV", prefix + "WRITEROLL"
|
|
14
|
+
)
|
|
15
|
+
self.pitch = epics_signal_rw(
|
|
16
|
+
float, prefix + "WRITEPITCH:RBV", prefix + "WRITEPITCH"
|
|
17
|
+
)
|
|
18
|
+
super().__init__(name=name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class p99StageSelections(str, Enum):
|
|
22
|
+
Empty = "Empty"
|
|
23
|
+
Mn5um = "Mn 5um"
|
|
24
|
+
Fe = "Fe (empty)"
|
|
25
|
+
Co5um = "Co 5um"
|
|
26
|
+
Ni5um = "Ni 5um"
|
|
27
|
+
Cu5um = "Cu 5um"
|
|
28
|
+
Zn5um = "Zn 5um"
|
|
29
|
+
Zr = "Zr (empty)"
|
|
30
|
+
Mo = "Mo (empty)"
|
|
31
|
+
Rh = "Rh (empty)"
|
|
32
|
+
Pd = "Pd (empty)"
|
|
33
|
+
Ag = "Ag (empty)"
|
|
34
|
+
Cd25um = "Cd 25um"
|
|
35
|
+
W = "W (empty)"
|
|
36
|
+
Pt = "Pt (empty)"
|
|
37
|
+
User = "User"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FilterMotor(Device):
|
|
41
|
+
def __init__(self, prefix: str, name: str):
|
|
42
|
+
self.user_setpoint = epics_signal_rw(p99StageSelections, prefix)
|
|
43
|
+
super().__init__(name=name)
|
dodal/devices/robot.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from asyncio import FIRST_COMPLETED, Task
|
|
2
|
+
from asyncio import FIRST_COMPLETED, CancelledError, Task
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from enum import Enum
|
|
5
5
|
|
|
@@ -45,6 +45,9 @@ class BartRobot(StandardReadable, Movable):
|
|
|
45
45
|
LOAD_TIMEOUT = 60
|
|
46
46
|
NO_PIN_ERROR_CODE = 25
|
|
47
47
|
|
|
48
|
+
# How far the gonio position can be out before loading will fail
|
|
49
|
+
LOAD_TOLERANCE_MM = 0.02
|
|
50
|
+
|
|
48
51
|
def __init__(
|
|
49
52
|
self,
|
|
50
53
|
name: str,
|
|
@@ -74,19 +77,28 @@ class BartRobot(StandardReadable, Movable):
|
|
|
74
77
|
await wait_for_value(self.error_code, self.NO_PIN_ERROR_CODE, None)
|
|
75
78
|
raise RobotLoadFailed(self.NO_PIN_ERROR_CODE, "Pin was not detected")
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
async def wfv():
|
|
81
|
+
await wait_for_value(self.gonio_pin_sensor, PinMounted.PIN_MOUNTED, None)
|
|
82
|
+
|
|
83
|
+
tasks = [
|
|
84
|
+
(Task(raise_if_no_pin())),
|
|
85
|
+
(Task(wfv())),
|
|
86
|
+
]
|
|
87
|
+
try:
|
|
88
|
+
finished, unfinished = await asyncio.wait(
|
|
89
|
+
tasks,
|
|
90
|
+
return_when=FIRST_COMPLETED,
|
|
91
|
+
)
|
|
92
|
+
for task in unfinished:
|
|
93
|
+
task.cancel()
|
|
94
|
+
for task in finished:
|
|
95
|
+
await task
|
|
96
|
+
except CancelledError:
|
|
97
|
+
# If the outer enclosing task cancels after LOAD_TIMEOUT, this causes CancelledError to be raised
|
|
98
|
+
# in the current task, when it propagates to here we should cancel all pending tasks before bubbling up
|
|
99
|
+
for task in tasks:
|
|
100
|
+
task.cancel()
|
|
101
|
+
raise
|
|
90
102
|
|
|
91
103
|
async def _load_pin_and_puck(self, sample_location: SampleLocation):
|
|
92
104
|
LOGGER.info(f"Loading pin {sample_location}")
|
|
@@ -108,12 +120,12 @@ class BartRobot(StandardReadable, Movable):
|
|
|
108
120
|
await self.pin_mounted_or_no_pin_found()
|
|
109
121
|
|
|
110
122
|
@AsyncStatus.wrap
|
|
111
|
-
async def set(self,
|
|
123
|
+
async def set(self, value: SampleLocation):
|
|
112
124
|
try:
|
|
113
125
|
await asyncio.wait_for(
|
|
114
|
-
self._load_pin_and_puck(
|
|
126
|
+
self._load_pin_and_puck(value), timeout=self.LOAD_TIMEOUT
|
|
115
127
|
)
|
|
116
|
-
except asyncio.TimeoutError:
|
|
128
|
+
except asyncio.TimeoutError as e:
|
|
117
129
|
error_code = await self.error_code.get_value()
|
|
118
130
|
error_string = await self.error_str.get_value()
|
|
119
|
-
raise RobotLoadFailed(error_code, error_string)
|
|
131
|
+
raise RobotLoadFailed(int(error_code), error_string) from e
|
dodal/devices/scintillator.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
from ophyd_async.epics.motion import Motor
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class Scintillator(
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
class Scintillator(StandardReadable):
|
|
6
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
7
|
+
with self.add_children_as_readables():
|
|
8
|
+
self.y = Motor(prefix + "-MO-SCIN-01:Y")
|
|
9
|
+
self.z = Motor(prefix + "-MO-SCIN-01:Z")
|
|
10
|
+
super().__init__(name)
|
dodal/devices/smargon.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from collections.abc import Generator
|
|
1
|
+
from collections.abc import Collection, Generator
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from math import isclose
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import cast
|
|
6
6
|
|
|
7
7
|
from bluesky import plan_stubs as bps
|
|
8
8
|
from bluesky.utils import Msg
|
|
@@ -87,7 +87,7 @@ class XYZLimits:
|
|
|
87
87
|
def position_valid(self, pos: Collection[float]) -> bool:
|
|
88
88
|
return all(
|
|
89
89
|
axis_limits.contains(value)
|
|
90
|
-
for axis_limits, value in zip([self.x, self.y, self.z], pos)
|
|
90
|
+
for axis_limits, value in zip([self.x, self.y, self.z], pos, strict=False)
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
dodal/devices/status.py
CHANGED
|
@@ -1,41 +1,12 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
3
|
from ophyd.status import SubscriptionStatus
|
|
4
4
|
|
|
5
|
-
T = TypeVar("T")
|
|
6
|
-
|
|
7
5
|
|
|
8
6
|
def await_value(
|
|
9
|
-
subscribable: Any, expected_value:
|
|
7
|
+
subscribable: Any, expected_value: object, timeout: None | int = None
|
|
10
8
|
) -> SubscriptionStatus:
|
|
11
9
|
def value_is(value, **_):
|
|
12
10
|
return value == expected_value
|
|
13
11
|
|
|
14
12
|
return SubscriptionStatus(subscribable, value_is, timeout=timeout)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def await_value_in_list(
|
|
18
|
-
subscribable: Any, expected_value: list, timeout: None | int = None
|
|
19
|
-
) -> SubscriptionStatus:
|
|
20
|
-
"""Returns a status which is completed when the subscriptable contains a value
|
|
21
|
-
within the expected_value list"""
|
|
22
|
-
|
|
23
|
-
def value_is(value, **_):
|
|
24
|
-
return value in expected_value
|
|
25
|
-
|
|
26
|
-
if not isinstance(expected_value, list):
|
|
27
|
-
raise TypeError(f"expected value {expected_value} is not a list")
|
|
28
|
-
else:
|
|
29
|
-
return SubscriptionStatus(subscribable, value_is, timeout=timeout)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def await_approx_value(
|
|
33
|
-
subscribable: Any,
|
|
34
|
-
expected_value: T,
|
|
35
|
-
deadband: float = 1e-09,
|
|
36
|
-
timeout: None | int = None,
|
|
37
|
-
) -> SubscriptionStatus:
|
|
38
|
-
def value_is_approx(value, **_):
|
|
39
|
-
return abs(value - expected_value) <= deadband
|
|
40
|
-
|
|
41
|
-
return SubscriptionStatus(subscribable, value_is_approx, timeout=timeout)
|
dodal/devices/tetramm.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import Sequence
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import Sequence
|
|
4
4
|
|
|
5
5
|
from bluesky.protocols import Hints
|
|
6
6
|
from ophyd_async.core import (
|
|
@@ -115,7 +115,7 @@ class TetrammController(DetectorControl):
|
|
|
115
115
|
async def arm(
|
|
116
116
|
self,
|
|
117
117
|
num: int,
|
|
118
|
-
trigger: DetectorTrigger,
|
|
118
|
+
trigger: DetectorTrigger = DetectorTrigger.edge_trigger,
|
|
119
119
|
exposure: float | None = None,
|
|
120
120
|
) -> AsyncStatus:
|
|
121
121
|
if exposure is None:
|
|
@@ -132,7 +132,7 @@ class TetrammController(DetectorControl):
|
|
|
132
132
|
self._drv.averaging_time.set(exposure), self.set_exposure(exposure)
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
-
status = await set_and_wait_for_value(self._drv.acquire,
|
|
135
|
+
status = await set_and_wait_for_value(self._drv.acquire, True)
|
|
136
136
|
|
|
137
137
|
return status
|
|
138
138
|
|
|
@@ -150,7 +150,7 @@ class TetrammController(DetectorControl):
|
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
async def disarm(self):
|
|
153
|
-
await stop_busy_record(self._drv.acquire,
|
|
153
|
+
await stop_busy_record(self._drv.acquire, False, timeout=1)
|
|
154
154
|
|
|
155
155
|
async def set_exposure(self, exposure: float):
|
|
156
156
|
"""Tries to set the exposure time of a single frame.
|
dodal/devices/thawer.py
CHANGED
|
@@ -15,7 +15,7 @@ class ThawerStates(str, Enum):
|
|
|
15
15
|
ON = "On"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class ThawingTimer(Device):
|
|
18
|
+
class ThawingTimer(Device, Stoppable):
|
|
19
19
|
def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
|
|
20
20
|
self._control_signal = control_signal
|
|
21
21
|
self._thawing_task: Task | None = None
|
|
@@ -32,7 +32,8 @@ class ThawingTimer(Device):
|
|
|
32
32
|
finally:
|
|
33
33
|
await self._control_signal.set(ThawerStates.OFF)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
@AsyncStatus.wrap
|
|
36
|
+
async def stop(self, *args, **kwargs):
|
|
36
37
|
if self._thawing_task:
|
|
37
38
|
self._thawing_task.cancel()
|
|
38
39
|
|
|
@@ -43,6 +44,7 @@ class Thawer(StandardReadable, Stoppable):
|
|
|
43
44
|
self.thaw_for_time_s = ThawingTimer(self.control)
|
|
44
45
|
super().__init__(name)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
@AsyncStatus.wrap
|
|
48
|
+
async def stop(self, *args, **kwargs):
|
|
47
49
|
await self.thaw_for_time_s.stop()
|
|
48
50
|
await self.control.set(ThawerStates.OFF)
|
dodal/devices/undulator_dcm.py
CHANGED
|
@@ -74,14 +74,12 @@ class UndulatorDCM(StandardReadable, Movable):
|
|
|
74
74
|
daq_configuration_path + "/domain/beamlineParameters"
|
|
75
75
|
)["DCM_Perp_Offset_FIXED"]
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return AsyncStatus(_set())
|
|
77
|
+
@AsyncStatus.wrap
|
|
78
|
+
async def set(self, value: float):
|
|
79
|
+
await asyncio.gather(
|
|
80
|
+
self._set_dcm_energy(value),
|
|
81
|
+
self._set_undulator_gap_if_required(value),
|
|
82
|
+
)
|
|
85
83
|
|
|
86
84
|
async def _set_dcm_energy(self, energy_kev: float) -> None:
|
|
87
85
|
access_level = await self.undulator.gap_access.get_value()
|
|
@@ -3,10 +3,10 @@ All the methods in this module return a bluesky plan generator that adjusts a va
|
|
|
3
3
|
according to some criteria either via feedback, preset positions, lookup tables etc.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable, Generator
|
|
7
7
|
|
|
8
8
|
from bluesky import plan_stubs as bps
|
|
9
|
-
from bluesky.
|
|
9
|
+
from bluesky.utils import Msg
|
|
10
10
|
from ophyd.epics_motor import EpicsMotor
|
|
11
11
|
from ophyd_async.epics.motion import Motor
|
|
12
12
|
|
dodal/devices/util/epics_util.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
1
2
|
from functools import partial
|
|
2
|
-
from typing import Callable
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Movable
|
|
5
5
|
from ophyd import Component, EpicsSignal
|
|
@@ -26,7 +26,7 @@ def epics_signal_put_wait(pv_name: str, wait: float = 3.0) -> Component[EpicsSig
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def run_functions_without_blocking(
|
|
29
|
-
functions_to_chain:
|
|
29
|
+
functions_to_chain: Sequence[Callable[[], StatusBase]],
|
|
30
30
|
timeout: float = 60.0,
|
|
31
31
|
associated_obj: OphydDevice | None = None,
|
|
32
32
|
) -> Status:
|
|
@@ -61,9 +61,9 @@ def run_functions_without_blocking(
|
|
|
61
61
|
# Wrap each function by first checking the previous status and attaching a callback
|
|
62
62
|
# to the next function in the chain
|
|
63
63
|
def wrap_func(
|
|
64
|
-
old_status: Status, current_func: Callable[[], StatusBase], next_func
|
|
64
|
+
old_status: Status | None, current_func: Callable[[], StatusBase], next_func
|
|
65
65
|
):
|
|
66
|
-
if old_status.exception() is not None:
|
|
66
|
+
if old_status is not None and old_status.exception() is not None:
|
|
67
67
|
set_global_exception_and_log(old_status)
|
|
68
68
|
return
|
|
69
69
|
|
|
@@ -96,7 +96,7 @@ def run_functions_without_blocking(
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
# Wrap each function in reverse
|
|
99
|
-
for
|
|
99
|
+
for func in list(reversed(functions_to_chain))[1:-1]:
|
|
100
100
|
wrapped_funcs.append(
|
|
101
101
|
partial(
|
|
102
102
|
wrap_func,
|
|
@@ -105,10 +105,8 @@ def run_functions_without_blocking(
|
|
|
105
105
|
)
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
starting_status = Status(done=True, success=True)
|
|
109
|
-
|
|
110
108
|
# Initiate the chain of functions
|
|
111
|
-
wrap_func(
|
|
109
|
+
wrap_func(None, functions_to_chain[0], wrapped_funcs[-1])
|
|
112
110
|
return full_status
|
|
113
111
|
|
|
114
112
|
|
|
@@ -3,9 +3,8 @@ All the public methods in this module return a lookup table of some kind that
|
|
|
3
3
|
converts the source value s to a target value t for different values of s.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from collections.abc import Sequence
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
7
|
from io import StringIO
|
|
8
|
-
from typing import Callable
|
|
9
8
|
|
|
10
9
|
import aiofiles
|
|
11
10
|
import numpy as np
|
|
@@ -36,7 +35,7 @@ async def energy_distance_table(lookup_table_path: str) -> np.ndarray:
|
|
|
36
35
|
def linear_interpolation_lut(filename: str) -> Callable[[float], float]:
|
|
37
36
|
"""Returns a callable that converts values by linear interpolation of lookup table values"""
|
|
38
37
|
LOGGER.info(f"Using lookup table {filename}")
|
|
39
|
-
s_and_t_vals = zip(*loadtxt(filename, comments=["#", "Units"]))
|
|
38
|
+
s_and_t_vals = zip(*loadtxt(filename, comments=["#", "Units"]), strict=False)
|
|
40
39
|
|
|
41
40
|
s_values: Sequence
|
|
42
41
|
t_values: Sequence
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from bluesky.run_engine import RunEngine
|
|
9
|
+
from ophyd_async.core import Device, save_device
|
|
10
|
+
from ophyd_async.panda import phase_sorter
|
|
11
|
+
|
|
12
|
+
from dodal.beamlines import module_name_for_beamline
|
|
13
|
+
from dodal.utils import make_device
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str]):
|
|
17
|
+
"""CLI Utility to save the panda configuration."""
|
|
18
|
+
parser = ArgumentParser(description="Save an ophyd_async panda to yaml")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--beamline", help="beamline to save from e.g. i03. Defaults to BEAMLINE"
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--device-name",
|
|
24
|
+
help='name of the device. The default is "panda"',
|
|
25
|
+
default="panda",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-f",
|
|
29
|
+
"--force",
|
|
30
|
+
action=argparse.BooleanOptionalAction,
|
|
31
|
+
help="Force overwriting an existing file",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("output_file", help="output filename")
|
|
34
|
+
|
|
35
|
+
# this exit()s with message/help unless args parsed successfully
|
|
36
|
+
args = parser.parse_args(argv[1:])
|
|
37
|
+
|
|
38
|
+
beamline = args.beamline
|
|
39
|
+
device_name = args.device_name
|
|
40
|
+
output_file = args.output_file
|
|
41
|
+
force = args.force
|
|
42
|
+
|
|
43
|
+
if beamline:
|
|
44
|
+
os.environ["BEAMLINE"] = beamline
|
|
45
|
+
else:
|
|
46
|
+
beamline = os.environ.get("BEAMLINE", None)
|
|
47
|
+
|
|
48
|
+
if not beamline:
|
|
49
|
+
sys.stderr.write("BEAMLINE not set and --beamline not specified\n")
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
if Path(output_file).exists() and not force:
|
|
53
|
+
sys.stderr.write(
|
|
54
|
+
f"Output file {output_file} already exists and --force not specified."
|
|
55
|
+
)
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
_save_panda(beamline, device_name, output_file)
|
|
59
|
+
|
|
60
|
+
print("Done.")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _save_panda(beamline, device_name, output_file):
|
|
65
|
+
RE = RunEngine()
|
|
66
|
+
print("Creating devices...")
|
|
67
|
+
module_name = module_name_for_beamline(beamline)
|
|
68
|
+
try:
|
|
69
|
+
devices = make_device(f"dodal.beamlines.{module_name}", device_name)
|
|
70
|
+
except Exception as error:
|
|
71
|
+
sys.stderr.write(f"Couldn't create device {device_name}: {error}\n")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
panda = devices[device_name]
|
|
75
|
+
print(f"Saving to {output_file} from {device_name} on {beamline}...")
|
|
76
|
+
_save_panda_to_file(RE, cast(Device, panda), output_file)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _save_panda_to_file(RE: RunEngine, panda: Device, path: str):
|
|
80
|
+
def save_to_file():
|
|
81
|
+
yield from save_device(panda, path, sorter=phase_sorter)
|
|
82
|
+
|
|
83
|
+
RE(save_to_file())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__": # pragma: no cover
|
|
87
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
callback_on_mock_put,
|
|
3
|
+
set_mock_value,
|
|
4
|
+
)
|
|
5
|
+
from ophyd_async.epics.motion import Motor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def patch_motor(motor: Motor, initial_position=0):
|
|
9
|
+
set_mock_value(motor.user_setpoint, initial_position)
|
|
10
|
+
set_mock_value(motor.user_readback, initial_position)
|
|
11
|
+
set_mock_value(motor.deadband, 0.001)
|
|
12
|
+
set_mock_value(motor.motor_done_move, 1)
|
|
13
|
+
set_mock_value(motor.velocity, 3)
|
|
14
|
+
return callback_on_mock_put(
|
|
15
|
+
motor.user_setpoint,
|
|
16
|
+
lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
|
|
17
|
+
)
|
dodal/devices/webcam.py
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import io
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import aiofiles
|
|
5
4
|
from aiohttp import ClientSession
|
|
6
5
|
from bluesky.protocols import Triggerable
|
|
7
|
-
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_rw
|
|
8
|
-
from PIL import Image
|
|
6
|
+
from ophyd_async.core import AsyncStatus, HintedSignal, StandardReadable, soft_signal_rw
|
|
9
7
|
|
|
10
|
-
from dodal.devices.oav.utils import save_thumbnail
|
|
11
8
|
from dodal.log import LOGGER
|
|
12
9
|
|
|
13
10
|
|
|
@@ -18,7 +15,7 @@ class Webcam(StandardReadable, Triggerable):
|
|
|
18
15
|
self.directory = soft_signal_rw(str, name="directory")
|
|
19
16
|
self.last_saved_path = soft_signal_rw(str, name="last_saved_path")
|
|
20
17
|
|
|
21
|
-
self.
|
|
18
|
+
self.add_readables([self.last_saved_path], wrapper=HintedSignal)
|
|
22
19
|
super().__init__(name=name)
|
|
23
20
|
|
|
24
21
|
async def _write_image(self, file_path: str):
|
|
@@ -26,10 +23,8 @@ class Webcam(StandardReadable, Triggerable):
|
|
|
26
23
|
async with session.get(self.url) as response:
|
|
27
24
|
response.raise_for_status()
|
|
28
25
|
LOGGER.info(f"Saving webcam image from {self.url} to {file_path}")
|
|
29
|
-
data = await response.read()
|
|
30
26
|
async with aiofiles.open(file_path, "wb") as file:
|
|
31
|
-
await file.write(
|
|
32
|
-
save_thumbnail(Path(file_path), Image.open(io.BytesIO(data)))
|
|
27
|
+
await file.write(await response.read())
|
|
33
28
|
|
|
34
29
|
@AsyncStatus.wrap
|
|
35
30
|
async def trigger(self) -> None:
|
dodal/devices/xbpm_feedback.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
-
import ophyd
|
|
4
3
|
from bluesky.protocols import Triggerable
|
|
5
|
-
from ophyd import Component, EpicsSignal, EpicsSignalRO
|
|
6
|
-
from ophyd.status import StatusBase, SubscriptionStatus
|
|
7
4
|
from ophyd_async.core import Device, observe_value
|
|
8
5
|
from ophyd_async.core.async_status import AsyncStatus
|
|
9
6
|
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
@@ -31,23 +28,3 @@ class XBPMFeedback(Device, Triggerable):
|
|
|
31
28
|
async for value in observe_value(self.pos_stable):
|
|
32
29
|
if value:
|
|
33
30
|
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class XBPMFeedbackI04(ophyd.Device):
|
|
37
|
-
"""The I04 version of this device has a slightly different trigger method"""
|
|
38
|
-
|
|
39
|
-
# Values to set to pause_feedback
|
|
40
|
-
PAUSE = 0
|
|
41
|
-
RUN = 1
|
|
42
|
-
|
|
43
|
-
pos_ok = Component(EpicsSignalRO, "-EA-FDBK-01:XBPM2POSITION_OK")
|
|
44
|
-
pause_feedback = Component(EpicsSignal, "-EA-FDBK-01:FB_PAUSE")
|
|
45
|
-
x = Component(EpicsSignalRO, "-EA-XBPM-02:PosX:MeanValue_RBV")
|
|
46
|
-
y = Component(EpicsSignalRO, "-EA-XBPM-02:PosY:MeanValue_RBV")
|
|
47
|
-
|
|
48
|
-
def trigger(self) -> StatusBase:
|
|
49
|
-
return SubscriptionStatus(
|
|
50
|
-
self.pos_ok,
|
|
51
|
-
lambda *, old_value, value, **kwargs: value == 1,
|
|
52
|
-
timeout=60,
|
|
53
|
-
)
|
dodal/devices/zebra.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from functools import partialmethod
|
|
6
|
-
from typing import List
|
|
7
6
|
|
|
8
7
|
from ophyd_async.core import (
|
|
9
8
|
AsyncStatus,
|
|
@@ -95,7 +94,7 @@ class ArmingDevice(StandardReadable):
|
|
|
95
94
|
"""A useful device that can abstract some of the logic of arming.
|
|
96
95
|
Allows a user to just call arm.set(ArmDemand.ARM)"""
|
|
97
96
|
|
|
98
|
-
TIMEOUT = 3
|
|
97
|
+
TIMEOUT: float = 3
|
|
99
98
|
|
|
100
99
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
101
100
|
self.arm_set = epics_signal_rw(float, prefix + "PC_ARM")
|
|
@@ -110,10 +109,9 @@ class ArmingDevice(StandardReadable):
|
|
|
110
109
|
if reading == demand.value:
|
|
111
110
|
return
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
112
|
+
@AsyncStatus.wrap
|
|
113
|
+
async def set(self, demand: ArmDemand):
|
|
114
|
+
await asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
|
|
117
115
|
|
|
118
116
|
|
|
119
117
|
class PositionCompare(StandardReadable):
|
|
@@ -166,7 +164,7 @@ class ZebraOutputPanel(StandardReadable):
|
|
|
166
164
|
super().__init__(name)
|
|
167
165
|
|
|
168
166
|
|
|
169
|
-
def boolean_array_to_integer(values:
|
|
167
|
+
def boolean_array_to_integer(values: list[bool]) -> int:
|
|
170
168
|
"""Converts a boolean array to integer by interpretting it in binary with LSB 0 bit
|
|
171
169
|
numbering.
|
|
172
170
|
|
|
@@ -245,8 +243,8 @@ class LogicGateConfiguration:
|
|
|
245
243
|
NUMBER_OF_INPUTS = 4
|
|
246
244
|
|
|
247
245
|
def __init__(self, input_source: int, invert: bool = False) -> None:
|
|
248
|
-
self.sources:
|
|
249
|
-
self.invert:
|
|
246
|
+
self.sources: list[int] = []
|
|
247
|
+
self.invert: list[bool] = []
|
|
250
248
|
self.add_input(input_source, invert)
|
|
251
249
|
|
|
252
250
|
def add_input(
|
|
@@ -271,7 +269,9 @@ class LogicGateConfiguration:
|
|
|
271
269
|
|
|
272
270
|
def __str__(self) -> str:
|
|
273
271
|
input_strings = []
|
|
274
|
-
for input, (source, invert) in enumerate(
|
|
272
|
+
for input, (source, invert) in enumerate(
|
|
273
|
+
zip(self.sources, self.invert, strict=False)
|
|
274
|
+
):
|
|
275
275
|
input_strings.append(f"INP{input+1}={'!' if invert else ''}{source}")
|
|
276
276
|
|
|
277
277
|
return ", ".join(input_strings)
|
|
@@ -29,10 +29,10 @@ class ZebraShutter(StandardReadable, Movable):
|
|
|
29
29
|
super().__init__(name=name)
|
|
30
30
|
|
|
31
31
|
@AsyncStatus.wrap
|
|
32
|
-
async def set(self,
|
|
33
|
-
await self.position_setpoint.set(
|
|
32
|
+
async def set(self, value: ZebraShutterState):
|
|
33
|
+
await self.position_setpoint.set(value)
|
|
34
34
|
return await wait_for_value(
|
|
35
35
|
signal=self.position_readback,
|
|
36
|
-
match=
|
|
36
|
+
match=value,
|
|
37
37
|
timeout=DEFAULT_TIMEOUT,
|
|
38
38
|
)
|