dls-dodal 1.29.4__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.
Files changed (85) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +27 -42
  2. dls_dodal-1.30.0.dist-info/RECORD +132 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamlines/__init__.py +3 -1
  8. dodal/beamlines/i03.py +28 -23
  9. dodal/beamlines/i04.py +34 -12
  10. dodal/beamlines/i13_1.py +66 -0
  11. dodal/beamlines/i22.py +5 -5
  12. dodal/beamlines/i24.py +1 -1
  13. dodal/beamlines/p38.py +7 -7
  14. dodal/beamlines/p45.py +7 -5
  15. dodal/beamlines/p99.py +61 -0
  16. dodal/cli.py +6 -3
  17. dodal/common/beamlines/beamline_parameters.py +2 -2
  18. dodal/common/beamlines/beamline_utils.py +6 -5
  19. dodal/common/maths.py +1 -3
  20. dodal/common/types.py +2 -3
  21. dodal/common/udc_directory_provider.py +14 -3
  22. dodal/common/visit.py +2 -3
  23. dodal/devices/CTAB.py +22 -17
  24. dodal/devices/aperturescatterguard.py +114 -136
  25. dodal/devices/areadetector/adaravis.py +8 -6
  26. dodal/devices/areadetector/adsim.py +2 -3
  27. dodal/devices/areadetector/adutils.py +20 -12
  28. dodal/devices/cryostream.py +19 -7
  29. dodal/devices/detector/__init__.py +13 -2
  30. dodal/devices/detector/det_dim_constants.py +2 -2
  31. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  32. dodal/devices/detector/detector.py +5 -5
  33. dodal/devices/detector/detector_motion.py +38 -31
  34. dodal/devices/eiger.py +11 -15
  35. dodal/devices/eiger_odin.py +9 -10
  36. dodal/devices/fast_grid_scan.py +4 -3
  37. dodal/devices/fluorescence_detector_motion.py +13 -4
  38. dodal/devices/focusing_mirror.py +4 -4
  39. dodal/devices/hutch_shutter.py +4 -4
  40. dodal/devices/i22/dcm.py +4 -3
  41. dodal/devices/i22/fswitch.py +4 -4
  42. dodal/devices/i22/nxsas.py +23 -32
  43. dodal/devices/i24/pmac.py +47 -8
  44. dodal/devices/ipin.py +7 -4
  45. dodal/devices/linkam3.py +11 -5
  46. dodal/devices/logging_ophyd_device.py +1 -1
  47. dodal/devices/motors.py +31 -5
  48. dodal/devices/oav/grid_overlay.py +1 -0
  49. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  50. dodal/devices/oav/oav_detector.py +2 -1
  51. dodal/devices/oav/oav_parameters.py +18 -10
  52. dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
  53. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  54. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  55. dodal/devices/oav/utils.py +2 -2
  56. dodal/devices/p99/__init__.py +0 -0
  57. dodal/devices/p99/sample_stage.py +43 -0
  58. dodal/devices/robot.py +30 -18
  59. dodal/devices/scintillator.py +8 -5
  60. dodal/devices/smargon.py +3 -3
  61. dodal/devices/status.py +2 -31
  62. dodal/devices/tetramm.py +4 -4
  63. dodal/devices/thawer.py +5 -3
  64. dodal/devices/undulator_dcm.py +6 -8
  65. dodal/devices/util/adjuster_plans.py +2 -2
  66. dodal/devices/util/epics_util.py +5 -7
  67. dodal/devices/util/lookup_tables.py +2 -3
  68. dodal/devices/util/save_panda.py +87 -0
  69. dodal/devices/util/test_utils.py +17 -0
  70. dodal/devices/webcam.py +3 -3
  71. dodal/devices/xbpm_feedback.py +0 -23
  72. dodal/devices/zebra.py +10 -10
  73. dodal/devices/zebra_controlled_shutter.py +3 -3
  74. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  75. dodal/devices/zocalo/zocalo_results.py +31 -18
  76. dodal/log.py +14 -5
  77. dodal/plans/data_session_metadata.py +1 -0
  78. dodal/plans/motor_util_plans.py +117 -0
  79. dodal/utils.py +65 -22
  80. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  81. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  82. dodal/devices/qbpm1.py +0 -8
  83. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
  84. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
  85. /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
dodal/devices/motors.py CHANGED
@@ -3,8 +3,34 @@ from ophyd_async.epics.motion import Motor
3
3
 
4
4
 
5
5
  class XYZPositioner(Device):
6
- def __init__(self, prefix: str, name: str):
7
- self.x = Motor(prefix + "X")
8
- self.y = Motor(prefix + "Y")
9
- self.z = Motor(prefix + "Z")
10
- super().__init__(name)
6
+ """
7
+
8
+ Standard ophyd_async xyz motor stage, by combining 3 Motors,
9
+ with added infix for extra flexibliy to allow different axes other than x,y,z.
10
+
11
+ Parameters
12
+ ----------
13
+ prefix:
14
+ EPICS PV (Common part up to and including :).
15
+ name:
16
+ name for the stage.
17
+ infix:
18
+ EPICS PV, default is the ["X", "Y", "Z"].
19
+ Notes
20
+ -----
21
+ Example usage::
22
+ async with DeviceCollector():
23
+ xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:")
24
+ Or::
25
+ with DeviceCollector():
26
+ xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:", suffix = ["A", "B", "C"])
27
+
28
+ """
29
+
30
+ def __init__(self, prefix: str, name: str, infix: list[str] | None = None):
31
+ if infix is None:
32
+ infix = ["X", "Y", "Z"]
33
+ self.x = Motor(prefix + infix[0])
34
+ self.y = Motor(prefix + infix[1])
35
+ self.z = Motor(prefix + infix[2])
36
+ super().__init__(name=name)
@@ -1,3 +1,4 @@
1
+ # type: ignore # OAV will soon be ophyd-async, see https://github.com/DiamondLightSource/dodal/issues/716
1
2
  from enum import Enum
2
3
  from functools import partial
3
4
  from os.path import join as path_join
@@ -52,4 +52,4 @@
52
52
  "micronsPerXPixel": 0.227,
53
53
  "micronsPerYPixel": 0.314
54
54
  }
55
- }
55
+ }
@@ -1,3 +1,4 @@
1
+ # type: ignore # OAV will soon be ophyd-async, see https://github.com/DiamondLightSource/dodal/issues/716
1
2
  from functools import partial
2
3
 
3
4
  from ophyd import ADComponent as ADC
@@ -46,7 +47,7 @@ class ZoomController(Device):
46
47
  sxst = Component(EpicsSignal, "MP:SELECT.SXST")
47
48
 
48
49
  def set_flatfield_on_zoom_level_one(self, value):
49
- self.parent: "OAV"
50
+ self.parent: OAV
50
51
  flat_applied = self.parent.proc.port_name.get()
51
52
  no_flat_applied = self.parent.cam.port_name.get()
52
53
  return self.parent.grid_snapshot.input_plugin.set(
@@ -1,7 +1,8 @@
1
1
  import json
2
- import xml.etree.cElementTree as et
2
+ import xml.etree.ElementTree as et
3
3
  from collections import ChainMap
4
- from typing import Any, Tuple
4
+ from typing import Any
5
+ from xml.etree.ElementTree import Element
5
6
 
6
7
  from dodal.devices.oav.oav_errors import (
7
8
  OAVError_BeamPositionNotFound,
@@ -20,6 +21,13 @@ OAV_CONFIG_JSON = (
20
21
  )
21
22
 
22
23
 
24
+ def _get_element_as_float(node: Element, element_name: str) -> float:
25
+ element = node.find(element_name)
26
+ assert element is not None, f"{element_name} not found in {node}"
27
+ assert element.text
28
+ return float(element.text)
29
+
30
+
23
31
  class OAVParameters:
24
32
  """
25
33
  The parameters to set up the OAV depending on the context.
@@ -65,11 +73,11 @@ class OAVParameters:
65
73
  try:
66
74
  param = param_type(param)
67
75
  return param
68
- except AssertionError:
76
+ except AssertionError as e:
69
77
  raise TypeError(
70
78
  f"OAV param {name} from the OAV centring params json file has the "
71
79
  f"wrong type, should be {param_type} but is {type(param)}."
72
- )
80
+ ) from e
73
81
 
74
82
  self.exposure: float = update("exposure", float)
75
83
  self.acquire_period: float = update("acqPeriod", float)
@@ -134,14 +142,14 @@ class OAVConfigParams:
134
142
  root = tree.getroot()
135
143
  levels = root.findall(".//zoomLevel")
136
144
  for node in levels:
137
- if float(node.find("level").text) == zoom:
145
+ if _get_element_as_float(node, "level") == zoom:
138
146
  self.micronsPerXPixel = (
139
- float(node.find("micronsPerXPixel").text)
147
+ _get_element_as_float(node, "micronsPerXPixel")
140
148
  * DEFAULT_OAV_WINDOW[0]
141
149
  / xsize
142
150
  )
143
151
  self.micronsPerYPixel = (
144
- float(node.find("micronsPerYPixel").text)
152
+ _get_element_as_float(node, "micronsPerYPixel")
145
153
  * DEFAULT_OAV_WINDOW[1]
146
154
  / ysize
147
155
  )
@@ -155,7 +163,7 @@ class OAVConfigParams:
155
163
 
156
164
  def get_beam_position_from_zoom(
157
165
  self, zoom: float, xsize: int, ysize: int
158
- ) -> Tuple[int, int]:
166
+ ) -> tuple[int, int]:
159
167
  """
160
168
  Extracts the beam location in pixels `xCentre` `yCentre`, for a requested zoom \
161
169
  level. The beam location is manually inputted by the beamline operator on GDA \
@@ -164,7 +172,7 @@ class OAVConfigParams:
164
172
  """
165
173
  crosshair_x_line = None
166
174
  crosshair_y_line = None
167
- with open(self.display_config, "r") as f:
175
+ with open(self.display_config) as f:
168
176
  file_lines = f.readlines()
169
177
  for i in range(len(file_lines)):
170
178
  if file_lines[i].startswith("zoomLevel = " + str(zoom)):
@@ -188,7 +196,7 @@ class OAVConfigParams:
188
196
 
189
197
  def calculate_beam_distance(
190
198
  self, horizontal_pixels: int, vertical_pixels: int
191
- ) -> Tuple[int, int]:
199
+ ) -> tuple[int, int]:
192
200
  """
193
201
  Calculates the distance between the beam centre and the given (horizontal, vertical).
194
202
 
@@ -0,0 +1,100 @@
1
+ import asyncio
2
+ import io
3
+ import pickle
4
+ import uuid
5
+
6
+ import numpy as np
7
+ from aiohttp import ClientResponse, ClientSession
8
+ from bluesky.protocols import Flyable
9
+ from ophyd_async.core import AsyncStatus, StandardReadable
10
+ from ophyd_async.core.signal import soft_signal_r_and_setter
11
+ from ophyd_async.epics.signal import epics_signal_r
12
+ from PIL import Image
13
+ from redis.asyncio import StrictRedis
14
+
15
+ from dodal.log import LOGGER
16
+
17
+
18
+ async def get_next_jpeg(response: ClientResponse) -> bytes:
19
+ JPEG_START_BYTE = b"\xff\xd8"
20
+ JPEG_STOP_BYTE = b"\xff\xd9"
21
+ while True:
22
+ line = await response.content.readline()
23
+ if line.startswith(JPEG_START_BYTE):
24
+ return line + await response.content.readuntil(JPEG_STOP_BYTE)
25
+
26
+
27
+ class OAVToRedisForwarder(StandardReadable, Flyable):
28
+ """Forwards OAV image data to redis. To use call:
29
+
30
+ > bps.kickoff(oav_forwarder)
31
+ > bps.monitor(oav_forwarder.uuid)
32
+ > bps.complete(oav_forwarder)
33
+
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ prefix: str,
39
+ redis_host: str,
40
+ redis_password: str,
41
+ redis_db: int = 0,
42
+ name: str = "",
43
+ redis_key: str = "test-image",
44
+ ) -> None:
45
+ """Reads image data from the MJPEG stream on an OAV and forwards it into a
46
+ redis database. This is currently only used for murko integration.
47
+
48
+ Arguments:
49
+ prefix: str the PV prefix of the OAV
50
+ redis_host: str the host where the redis database is running
51
+ redis_password: str the password for the redis database
52
+ redis_db: int which redis database to connect to, defaults to 0
53
+ name: str the name of this device
54
+ redis_key: str the key to store data in, defaults to "test-image"
55
+ """
56
+ self.stream_url = epics_signal_r(str, f"{prefix}-DI-OAV-01:MJPG:HOST_RBV")
57
+
58
+ with self.add_children_as_readables():
59
+ self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
60
+
61
+ self.forwarding_task = None
62
+ self.redis_client = StrictRedis(
63
+ host=redis_host, password=redis_password, db=redis_db
64
+ )
65
+
66
+ self.redis_key = redis_key
67
+
68
+ # The uuid that images are being saved under, this should be monitored for
69
+ # callbacks to correlate the data
70
+ self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
71
+
72
+ super().__init__(name=name)
73
+
74
+ async def _get_frame_and_put_to_redis(self, response: ClientResponse):
75
+ """Converts the data that comes in as a jpeg byte stream into a numpy array of
76
+ RGB values, pickles this array then writes it to redis.
77
+ """
78
+ jpeg_bytes = await get_next_jpeg(response)
79
+ self.uuid_setter(image_uuid := str(uuid.uuid4()))
80
+ img = Image.open(io.BytesIO(jpeg_bytes))
81
+ image_data = pickle.dumps(np.asarray(img))
82
+ await self.redis_client.hset(self.redis_key, image_uuid, image_data) # type: ignore
83
+ LOGGER.debug(f"Sent frame to redis key {self.redis_key} with uuid {image_uuid}")
84
+
85
+ async def _stream_to_redis(self):
86
+ stream_url = await self.stream_url.get_value()
87
+ async with ClientSession() as session:
88
+ async with session.get(stream_url) as response:
89
+ while True:
90
+ await self._get_frame_and_put_to_redis(response)
91
+ await asyncio.sleep(0.01)
92
+
93
+ @AsyncStatus.wrap
94
+ async def kickoff(self):
95
+ self.forwarding_task = asyncio.create_task(self._stream_to_redis())
96
+
97
+ @AsyncStatus.wrap
98
+ async def complete(self):
99
+ assert self.forwarding_task, "Device not kicked off"
100
+ self.forwarding_task.cancel()
@@ -5,6 +5,7 @@ import numpy as np
5
5
  from numpy.typing import NDArray
6
6
  from ophyd_async.core import (
7
7
  AsyncStatus,
8
+ HintedSignal,
8
9
  StandardReadable,
9
10
  observe_value,
10
11
  soft_signal_r_and_setter,
@@ -77,12 +78,13 @@ class PinTipDetection(StandardReadable):
77
78
  self.min_tip_height = soft_signal_rw(int, 5, name="min_tip_height")
78
79
  self.validity_timeout = soft_signal_rw(float, 5.0, name="validity_timeout")
79
80
 
80
- self.set_readable_signals(
81
- read=[
81
+ self.add_readables(
82
+ [
82
83
  self.triggered_tip,
83
84
  self.triggered_top_edge,
84
85
  self.triggered_bottom_edge,
85
86
  ],
87
+ wrapper=HintedSignal,
86
88
  )
87
89
 
88
90
  super().__init__(name=name)
@@ -134,9 +136,7 @@ class PinTipDetection(StandardReadable):
134
136
  location = sample_detection.processArray(array_data)
135
137
  end_time = time.time()
136
138
  LOGGER.debug(
137
- "Sample location detection took {}ms".format(
138
- (end_time - start_time) * 1000.0
139
- )
139
+ f"Sample location detection took {(end_time - start_time) * 1000.0}ms"
140
140
  )
141
141
  return location
142
142
 
@@ -154,7 +154,7 @@ class PinTipDetection(StandardReadable):
154
154
  location = await self._get_tip_and_edge_data(value)
155
155
  self._set_triggered_values(location)
156
156
  except Exception as e:
157
- LOGGER.warn(
157
+ LOGGER.warning(
158
158
  f"Failed to detect pin-tip location, will retry with next image: {e}"
159
159
  )
160
160
  else:
@@ -1,6 +1,7 @@
1
+ from collections.abc import Callable
1
2
  from dataclasses import dataclass
2
3
  from enum import Enum
3
- from typing import Callable, Final, Tuple
4
+ from typing import Final
4
5
 
5
6
  import cv2
6
7
  import numpy as np
@@ -103,7 +104,7 @@ class SampleLocation:
103
104
  edge_bottom: np.ndarray
104
105
 
105
106
 
106
- class MxSampleDetect(object):
107
+ class MxSampleDetect:
107
108
  def __init__(
108
109
  self,
109
110
  *,
@@ -161,7 +162,7 @@ class MxSampleDetect(object):
161
162
  @staticmethod
162
163
  def _first_and_last_nonzero_by_columns(
163
164
  arr: np.ndarray,
164
- ) -> Tuple[np.ndarray, np.ndarray]:
165
+ ) -> tuple[np.ndarray, np.ndarray]:
165
166
  """
166
167
  Finds the indexes of the first & last non-zero values by column in a 2d array.
167
168
 
@@ -243,9 +244,7 @@ class MxSampleDetect(object):
243
244
  bottom[x + 1 :] = NONE_VALUE
244
245
 
245
246
  LOGGER.info(
246
- "pin-tip detection: Successfully located pin tip at (x={}, y={})".format(
247
- tip_x, tip_y
248
- )
247
+ f"pin-tip detection: Successfully located pin tip at (x={tip_x}, y={tip_y})"
249
248
  )
250
249
  return SampleLocation(
251
250
  tip_x=tip_x, tip_y=tip_y, edge_bottom=bottom, edge_top=top
@@ -1,5 +1,5 @@
1
+ from collections.abc import Generator
1
2
  from enum import IntEnum
2
- from typing import Generator, Tuple
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
5
  import numpy as np
@@ -10,7 +10,7 @@ from dodal.devices.oav.oav_detector import OAVConfigParams
10
10
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
11
11
  from dodal.devices.smargon import Smargon
12
12
 
13
- Pixel = Tuple[int, int]
13
+ Pixel = tuple[int, int]
14
14
 
15
15
 
16
16
  class PinNotFoundException(Exception):
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
- finished, unfinished = await asyncio.wait(
78
- [
79
- Task(raise_if_no_pin()),
80
- Task(
81
- wait_for_value(self.gonio_pin_sensor, PinMounted.PIN_MOUNTED, None)
82
- ),
83
- ],
84
- return_when=FIRST_COMPLETED,
85
- )
86
- for task in unfinished:
87
- task.cancel()
88
- for task in finished:
89
- await task
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, sample_location: SampleLocation):
123
+ async def set(self, value: SampleLocation):
112
124
  try:
113
125
  await asyncio.wait_for(
114
- self._load_pin_and_puck(sample_location), timeout=self.LOAD_TIMEOUT
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
@@ -1,7 +1,10 @@
1
- from ophyd import Component as Cpt
2
- from ophyd import Device, EpicsMotor
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.motion import Motor
3
3
 
4
4
 
5
- class Scintillator(Device):
6
- y = Cpt(EpicsMotor, "-MO-SCIN-01:Y")
7
- z = Cpt(EpicsMotor, "-MO-SCIN-01:Z")
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 Collection, cast
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, TypeVar
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: T, timeout: None | int = None
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, 1)
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, 0, timeout=1)
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
- async def stop(self):
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
- async def stop(self):
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)
@@ -74,14 +74,12 @@ class UndulatorDCM(StandardReadable, Movable):
74
74
  daq_configuration_path + "/domain/beamlineParameters"
75
75
  )["DCM_Perp_Offset_FIXED"]
76
76
 
77
- def set(self, value: float) -> AsyncStatus:
78
- async def _set():
79
- await asyncio.gather(
80
- self._set_dcm_energy(value),
81
- self._set_undulator_gap_if_required(value),
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 typing import Callable, Generator
6
+ from collections.abc import Callable, Generator
7
7
 
8
8
  from bluesky import plan_stubs as bps
9
- from bluesky.run_engine import Msg
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