dls-dodal 1.30.0__py3-none-any.whl → 1.31.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 (65) hide show
  1. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/METADATA +4 -4
  2. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/RECORD +64 -62
  3. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamline_specific_utils/i03.py +1 -4
  6. dodal/beamlines/__init__.py +4 -0
  7. dodal/beamlines/i03.py +8 -8
  8. dodal/beamlines/i04.py +10 -9
  9. dodal/beamlines/i13_1.py +7 -7
  10. dodal/beamlines/i22.py +18 -18
  11. dodal/beamlines/p38.py +14 -14
  12. dodal/beamlines/p45.py +11 -11
  13. dodal/beamlines/training_rig.py +64 -0
  14. dodal/common/beamlines/beamline_parameters.py +5 -4
  15. dodal/common/beamlines/beamline_utils.py +9 -9
  16. dodal/common/types.py +4 -2
  17. dodal/common/udc_directory_provider.py +29 -22
  18. dodal/common/visit.py +59 -60
  19. dodal/devices/CTAB.py +1 -1
  20. dodal/devices/aperture.py +1 -1
  21. dodal/devices/aperturescatterguard.py +140 -188
  22. dodal/devices/areadetector/plugins/MJPG.py +2 -1
  23. dodal/devices/backlight.py +12 -1
  24. dodal/devices/dcm.py +1 -1
  25. dodal/devices/detector/detector.py +31 -30
  26. dodal/devices/detector/detector_motion.py +1 -1
  27. dodal/devices/fast_grid_scan.py +14 -24
  28. dodal/devices/focusing_mirror.py +2 -2
  29. dodal/devices/i22/dcm.py +1 -1
  30. dodal/devices/i22/fswitch.py +6 -2
  31. dodal/devices/i22/nxsas.py +32 -11
  32. dodal/devices/i24/aperture.py +1 -1
  33. dodal/devices/i24/beamstop.py +1 -1
  34. dodal/devices/i24/dcm.py +1 -1
  35. dodal/devices/i24/i24_detector_motion.py +1 -1
  36. dodal/devices/i24/pmac.py +24 -8
  37. dodal/devices/linkam3.py +1 -1
  38. dodal/devices/motors.py +1 -1
  39. dodal/devices/oav/oav_to_redis_forwarder.py +46 -17
  40. dodal/devices/robot.py +1 -2
  41. dodal/devices/scatterguard.py +1 -1
  42. dodal/devices/scintillator.py +1 -1
  43. dodal/devices/slits.py +1 -1
  44. dodal/devices/smargon.py +1 -1
  45. dodal/devices/tetramm.py +20 -16
  46. dodal/devices/training_rig/__init__.py +0 -0
  47. dodal/devices/training_rig/sample_stage.py +10 -0
  48. dodal/devices/turbo_slit.py +1 -1
  49. dodal/devices/undulator.py +1 -1
  50. dodal/devices/util/adjuster_plans.py +1 -1
  51. dodal/devices/util/save_panda.py +1 -1
  52. dodal/devices/util/test_utils.py +1 -1
  53. dodal/devices/xbpm_feedback.py +1 -2
  54. dodal/devices/xspress3/xspress3.py +1 -1
  55. dodal/devices/zebra.py +5 -0
  56. dodal/devices/zebra_controlled_shutter.py +24 -9
  57. dodal/devices/zocalo/zocalo_results.py +6 -2
  58. dodal/log.py +32 -10
  59. dodal/plans/check_topup.py +65 -10
  60. dodal/plans/data_session_metadata.py +8 -10
  61. dodal/plans/motor_util_plans.py +1 -1
  62. dodal/devices/beamstop.py +0 -8
  63. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
  64. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/entry_points.txt +0 -0
  65. {dls_dodal-1.30.0.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
dodal/devices/i22/dcm.py CHANGED
@@ -6,7 +6,7 @@ from typing import Literal
6
6
  from bluesky.protocols import Reading
7
7
  from event_model.documents.event_descriptor import DataKey
8
8
  from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
9
- from ophyd_async.epics.motion import Motor
9
+ from ophyd_async.epics.motor import Motor
10
10
  from ophyd_async.epics.signal import epics_signal_r
11
11
 
12
12
  # Conversion constant for energy and wavelength, taken from the X-Ray data booklet
@@ -4,8 +4,12 @@ from enum import Enum
4
4
 
5
5
  from bluesky.protocols import Reading
6
6
  from event_model import DataKey
7
- from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
8
- from ophyd_async.core.device import DeviceVector
7
+ from ophyd_async.core import (
8
+ ConfigSignal,
9
+ DeviceVector,
10
+ StandardReadable,
11
+ soft_signal_r_and_setter,
12
+ )
9
13
  from ophyd_async.epics.signal import epics_signal_r
10
14
 
11
15
 
@@ -1,12 +1,33 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Iterable
1
3
  from dataclasses import dataclass, fields
4
+ from typing import TypeVar
2
5
 
3
6
  from bluesky.protocols import Reading
4
7
  from event_model.documents.event_descriptor import DataKey
5
- from ophyd_async.core import DirectoryProvider, merge_gathered_dicts
6
- from ophyd_async.epics.areadetector import AravisDetector, PilatusDetector
7
- from ophyd_async.epics.areadetector.aravis import AravisController
8
+ from ophyd_async.core import PathProvider
9
+ from ophyd_async.epics.adaravis import AravisController, AravisDetector
10
+ from ophyd_async.epics.adpilatus import PilatusDetector
8
11
 
9
12
  ValueAndUnits = tuple[float, str]
13
+ T = TypeVar("T")
14
+
15
+
16
+ # TODO: Remove this file as part of github.com/DiamondLightSource/dodal/issues/595
17
+ # Until which, temporarily duplicated non-public method from ophyd_async
18
+ async def _merge_gathered_dicts(
19
+ coros: Iterable[Awaitable[dict[str, T]]],
20
+ ) -> dict[str, T]:
21
+ """Merge dictionaries produced by a sequence of coroutines.
22
+
23
+ Can be used for merging ``read()`` or ``describe``. For instance::
24
+
25
+ combined_read = await merge_gathered_dicts(s.read() for s in signals)
26
+ """
27
+ ret: dict[str, T] = {}
28
+ for result in await asyncio.gather(*coros):
29
+ ret.update(result)
30
+ return ret
10
31
 
11
32
 
12
33
  @dataclass
@@ -80,7 +101,7 @@ class NXSasPilatus(PilatusDetector):
80
101
  def __init__(
81
102
  self,
82
103
  prefix: str,
83
- directory_provider: DirectoryProvider,
104
+ path_provider: PathProvider,
84
105
  drv_suffix: str,
85
106
  hdf_suffix: str,
86
107
  metadata_holder: NXSasMetadataHolder,
@@ -93,7 +114,7 @@ class NXSasPilatus(PilatusDetector):
93
114
  Writes hdf5 files."""
94
115
  super().__init__(
95
116
  prefix,
96
- directory_provider,
117
+ path_provider,
97
118
  drv_suffix=drv_suffix,
98
119
  hdf_suffix=hdf_suffix,
99
120
  name=name,
@@ -101,7 +122,7 @@ class NXSasPilatus(PilatusDetector):
101
122
  self._metadata_holder = metadata_holder
102
123
 
103
124
  async def read_configuration(self) -> dict[str, Reading]:
104
- return await merge_gathered_dicts(
125
+ return await _merge_gathered_dicts(
105
126
  r
106
127
  for r in (
107
128
  super().read_configuration(),
@@ -110,7 +131,7 @@ class NXSasPilatus(PilatusDetector):
110
131
  )
111
132
 
112
133
  async def describe_configuration(self) -> dict[str, DataKey]:
113
- return await merge_gathered_dicts(
134
+ return await _merge_gathered_dicts(
114
135
  r
115
136
  for r in (
116
137
  super().describe_configuration(),
@@ -123,7 +144,7 @@ class NXSasOAV(AravisDetector):
123
144
  def __init__(
124
145
  self,
125
146
  prefix: str,
126
- directory_provider: DirectoryProvider,
147
+ path_provider: PathProvider,
127
148
  drv_suffix: str,
128
149
  hdf_suffix: str,
129
150
  metadata_holder: NXSasMetadataHolder,
@@ -137,7 +158,7 @@ class NXSasOAV(AravisDetector):
137
158
  Writes hdf5 files."""
138
159
  super().__init__(
139
160
  prefix,
140
- directory_provider,
161
+ path_provider,
141
162
  drv_suffix=drv_suffix,
142
163
  hdf_suffix=hdf_suffix,
143
164
  name=name,
@@ -146,7 +167,7 @@ class NXSasOAV(AravisDetector):
146
167
  self._metadata_holder = metadata_holder
147
168
 
148
169
  async def read_configuration(self) -> dict[str, Reading]:
149
- return await merge_gathered_dicts(
170
+ return await _merge_gathered_dicts(
150
171
  r
151
172
  for r in (
152
173
  super().read_configuration(),
@@ -155,7 +176,7 @@ class NXSasOAV(AravisDetector):
155
176
  )
156
177
 
157
178
  async def describe_configuration(self) -> dict[str, DataKey]:
158
- return await merge_gathered_dicts(
179
+ return await _merge_gathered_dicts(
159
180
  r
160
181
  for r in (
161
182
  super().describe_configuration(),
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from ophyd_async.core import StandardReadable
4
- from ophyd_async.epics.motion import Motor
4
+ from ophyd_async.epics.motor import Motor
5
5
  from ophyd_async.epics.signal import epics_signal_rw
6
6
 
7
7
 
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from ophyd_async.core import StandardReadable
4
- from ophyd_async.epics.motion import Motor
4
+ from ophyd_async.epics.motor import Motor
5
5
  from ophyd_async.epics.signal import epics_signal_rw
6
6
 
7
7
 
dodal/devices/i24/dcm.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
  from ophyd_async.epics.signal import epics_signal_r
4
4
 
5
5
 
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class DetectorMotion(StandardReadable):
dodal/devices/i24/pmac.py CHANGED
@@ -2,12 +2,18 @@ from enum import Enum, IntEnum
2
2
  from typing import SupportsFloat
3
3
 
4
4
  from bluesky.protocols import Triggerable
5
- from ophyd_async.core import AsyncStatus, StandardReadable, wait_for_value
6
- from ophyd_async.core.signal import CalculateTimeout, SignalR, SignalRW
7
- from ophyd_async.core.signal_backend import SignalBackend
8
- from ophyd_async.core.soft_signal_backend import SoftSignalBackend
9
- from ophyd_async.core.utils import DEFAULT_TIMEOUT
10
- from ophyd_async.epics.motion import Motor
5
+ from ophyd_async.core import (
6
+ DEFAULT_TIMEOUT,
7
+ AsyncStatus,
8
+ CalculateTimeout,
9
+ SignalBackend,
10
+ SignalR,
11
+ SignalRW,
12
+ SoftSignalBackend,
13
+ StandardReadable,
14
+ wait_for_value,
15
+ )
16
+ from ophyd_async.epics.motor import Motor
11
17
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
12
18
 
13
19
  HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
@@ -79,7 +85,12 @@ class PMACStringLaser(SignalRW):
79
85
  super().__init__(backend, timeout, name)
80
86
 
81
87
  @AsyncStatus.wrap
82
- async def set(self, value: LaserSettings, wait=True, timeout=CalculateTimeout):
88
+ async def set(
89
+ self,
90
+ value: LaserSettings,
91
+ wait=True,
92
+ timeout=CalculateTimeout,
93
+ ):
83
94
  await self.signal.set(value.value, wait, timeout)
84
95
 
85
96
 
@@ -97,7 +108,12 @@ class PMACStringEncReset(SignalRW):
97
108
  super().__init__(backend, timeout, name)
98
109
 
99
110
  @AsyncStatus.wrap
100
- async def set(self, value: EncReset, wait=True, timeout=CalculateTimeout):
111
+ async def set(
112
+ self,
113
+ value: EncReset,
114
+ wait=True,
115
+ timeout=CalculateTimeout,
116
+ ):
101
117
  await self.signal.set(value.value, wait, timeout)
102
118
 
103
119
 
dodal/devices/linkam3.py CHANGED
@@ -8,9 +8,9 @@ from ophyd_async.core import (
8
8
  HintedSignal,
9
9
  StandardReadable,
10
10
  WatchableAsyncStatus,
11
+ WatcherUpdate,
11
12
  observe_value,
12
13
  )
13
- from ophyd_async.core.utils import WatcherUpdate
14
14
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
15
15
 
16
16
 
dodal/devices/motors.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import Device
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class XYZPositioner(Device):
@@ -2,12 +2,18 @@ import asyncio
2
2
  import io
3
3
  import pickle
4
4
  import uuid
5
+ from collections.abc import Awaitable, Callable
6
+ from datetime import timedelta
5
7
 
6
8
  import numpy as np
7
9
  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
10
+ from bluesky.protocols import Flyable, Stoppable
11
+ from ophyd_async.core import (
12
+ AsyncStatus,
13
+ StandardReadable,
14
+ soft_signal_r_and_setter,
15
+ soft_signal_rw,
16
+ )
11
17
  from ophyd_async.epics.signal import epics_signal_r
12
18
  from PIL import Image
13
19
  from redis.asyncio import StrictRedis
@@ -24,7 +30,7 @@ async def get_next_jpeg(response: ClientResponse) -> bytes:
24
30
  return line + await response.content.readuntil(JPEG_STOP_BYTE)
25
31
 
26
32
 
27
- class OAVToRedisForwarder(StandardReadable, Flyable):
33
+ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
28
34
  """Forwards OAV image data to redis. To use call:
29
35
 
30
36
  > bps.kickoff(oav_forwarder)
@@ -33,6 +39,8 @@ class OAVToRedisForwarder(StandardReadable, Flyable):
33
39
 
34
40
  """
35
41
 
42
+ DATA_EXPIRY_DAYS = 7
43
+
36
44
  def __init__(
37
45
  self,
38
46
  prefix: str,
@@ -40,7 +48,6 @@ class OAVToRedisForwarder(StandardReadable, Flyable):
40
48
  redis_password: str,
41
49
  redis_db: int = 0,
42
50
  name: str = "",
43
- redis_key: str = "test-image",
44
51
  ) -> None:
45
52
  """Reads image data from the MJPEG stream on an OAV and forwards it into a
46
53
  redis database. This is currently only used for murko integration.
@@ -51,9 +58,8 @@ class OAVToRedisForwarder(StandardReadable, Flyable):
51
58
  redis_password: str the password for the redis database
52
59
  redis_db: int which redis database to connect to, defaults to 0
53
60
  name: str the name of this device
54
- redis_key: str the key to store data in, defaults to "test-image"
55
61
  """
56
- self.stream_url = epics_signal_r(str, f"{prefix}-DI-OAV-01:MJPG:HOST_RBV")
62
+ self.stream_url = epics_signal_r(str, f"{prefix}MJPG:MJPG_URL_RBV")
57
63
 
58
64
  with self.add_children_as_readables():
59
65
  self.uuid, self.uuid_setter = soft_signal_r_and_setter(str)
@@ -63,7 +69,9 @@ class OAVToRedisForwarder(StandardReadable, Flyable):
63
69
  host=redis_host, password=redis_password, db=redis_db
64
70
  )
65
71
 
66
- self.redis_key = redis_key
72
+ self._stop_flag = False
73
+
74
+ self.sample_id = soft_signal_rw(int, initial_value=0)
67
75
 
68
76
  # The uuid that images are being saved under, this should be monitored for
69
77
  # callbacks to correlate the data
@@ -79,22 +87,43 @@ class OAVToRedisForwarder(StandardReadable, Flyable):
79
87
  self.uuid_setter(image_uuid := str(uuid.uuid4()))
80
88
  img = Image.open(io.BytesIO(jpeg_bytes))
81
89
  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):
90
+ sample_id = str(await self.sample_id.get_value())
91
+ await self.redis_client.hset(sample_id, image_uuid, image_data) # type: ignore
92
+ await self.redis_client.expire(sample_id, timedelta(days=self.DATA_EXPIRY_DAYS))
93
+ LOGGER.debug(f"Sent frame to redis key {sample_id} with uuid {image_uuid}")
94
+
95
+ async def _open_connection_and_do_function(
96
+ self, function_to_do: Callable[[ClientResponse, str | None], Awaitable]
97
+ ):
86
98
  stream_url = await self.stream_url.get_value()
87
99
  async with ClientSession() as session:
88
100
  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)
101
+ await function_to_do(response, stream_url)
102
+
103
+ async def _stream_to_redis(self, response, _):
104
+ while not self._stop_flag:
105
+ await self._get_frame_and_put_to_redis(response)
106
+ await asyncio.sleep(0.01)
107
+
108
+ async def _confirm_mjpg_stream(self, response, stream_url):
109
+ if response.content_type != "multipart/x-mixed-replace":
110
+ raise ValueError(f"{stream_url} is not an MJPG stream")
92
111
 
93
112
  @AsyncStatus.wrap
94
113
  async def kickoff(self):
95
- self.forwarding_task = asyncio.create_task(self._stream_to_redis())
114
+ self._stop_flag = False
115
+ await self._open_connection_and_do_function(self._confirm_mjpg_stream)
116
+ self.forwarding_task = asyncio.create_task(
117
+ self._open_connection_and_do_function(self._stream_to_redis)
118
+ )
96
119
 
97
120
  @AsyncStatus.wrap
98
121
  async def complete(self):
99
122
  assert self.forwarding_task, "Device not kicked off"
100
- self.forwarding_task.cancel()
123
+ await self.stop()
124
+
125
+ @AsyncStatus.wrap
126
+ async def stop(self, success=True):
127
+ if self.forwarding_task:
128
+ self._stop_flag = True
129
+ await self.forwarding_task
dodal/devices/robot.py CHANGED
@@ -10,8 +10,7 @@ from ophyd_async.core import (
10
10
  set_and_wait_for_value,
11
11
  wait_for_value,
12
12
  )
13
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_x
14
- from ophyd_async.epics.signal.signal import epics_signal_rw_rbv
13
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv, epics_signal_x
15
14
 
16
15
  from dodal.log import LOGGER
17
16
 
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class Scatterguard(StandardReadable):
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class Scintillator(StandardReadable):
dodal/devices/slits.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import StandardReadable
2
- from ophyd_async.epics.motion import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class Slits(StandardReadable):
dodal/devices/smargon.py CHANGED
@@ -7,7 +7,7 @@ from typing import cast
7
7
  from bluesky import plan_stubs as bps
8
8
  from bluesky.utils import Msg
9
9
  from ophyd_async.core import AsyncStatus, Device, StandardReadable, wait_for_value
10
- from ophyd_async.epics.motion import Motor
10
+ from ophyd_async.epics.motor import Motor
11
11
  from ophyd_async.epics.signal import epics_signal_r
12
12
 
13
13
  from dodal.devices.util.epics_util import SetWhenEnabled
dodal/devices/tetramm.py CHANGED
@@ -1,23 +1,24 @@
1
1
  import asyncio
2
- from collections.abc import Sequence
3
2
  from enum import Enum
4
3
 
5
4
  from bluesky.protocols import Hints
6
5
  from ophyd_async.core import (
7
6
  AsyncStatus,
7
+ DatasetDescriber,
8
8
  DetectorControl,
9
9
  DetectorTrigger,
10
10
  Device,
11
- DirectoryProvider,
12
- ShapeProvider,
11
+ PathProvider,
13
12
  StandardDetector,
14
13
  set_and_wait_for_value,
15
14
  soft_signal_r_and_setter,
16
15
  )
17
- from ophyd_async.epics.areadetector.utils import stop_busy_record
18
- from ophyd_async.epics.areadetector.writers import HDFWriter, NDFileHDF
19
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
20
- from ophyd_async.epics.signal.signal import epics_signal_rw_rbv
16
+ from ophyd_async.epics.adcore import ADHDFWriter, NDFileHDFIO, stop_busy_record
17
+ from ophyd_async.epics.signal import (
18
+ epics_signal_r,
19
+ epics_signal_rw,
20
+ epics_signal_rw_rbv,
21
+ )
21
22
 
22
23
 
23
24
  class TetrammRange(str, Enum):
@@ -108,7 +109,7 @@ class TetrammController(DetectorControl):
108
109
  self.minimum_values_per_reading = minimum_values_per_reading
109
110
  self.readings_per_frame = readings_per_frame
110
111
 
111
- def get_deadtime(self, exposure: float) -> float:
112
+ def get_deadtime(self, exposure: float | None) -> float:
112
113
  # 2 internal clock cycles. Best effort approximation
113
114
  return 2 / self.base_sample_rate
114
115
 
@@ -204,14 +205,17 @@ class TetrammController(DetectorControl):
204
205
  )
205
206
 
206
207
 
207
- class TetrammShapeProvider(ShapeProvider):
208
+ class TetrammDatasetDescriber(DatasetDescriber):
208
209
  max_channels = 11
209
210
 
210
211
  def __init__(self, controller: TetrammController) -> None:
211
212
  self.controller = controller
212
213
 
213
- async def __call__(self) -> Sequence[int]:
214
- return [self.max_channels, self.controller.readings_per_frame]
214
+ async def np_datatype(self) -> str:
215
+ return "<f8" # IEEE 754 double precision floating point
216
+
217
+ async def shape(self) -> tuple[int, int]:
218
+ return (self.max_channels, self.controller.readings_per_frame)
215
219
 
216
220
 
217
221
  # TODO: Support MeanValue signals https://github.com/DiamondLightSource/dodal/issues/337
@@ -219,13 +223,13 @@ class TetrammDetector(StandardDetector):
219
223
  def __init__(
220
224
  self,
221
225
  prefix: str,
222
- directory_provider: DirectoryProvider,
226
+ path_provider: PathProvider,
223
227
  name: str,
224
228
  type: str | None = None,
225
229
  **scalar_sigs: str,
226
230
  ) -> None:
227
231
  self.drv = TetrammDriver(prefix + "DRV:")
228
- self.hdf = NDFileHDF(prefix + "HDF5:")
232
+ self.hdf = NDFileHDFIO(prefix + "HDF5:")
229
233
  controller = TetrammController(self.drv)
230
234
  config_signals = [
231
235
  self.drv.values_per_reading,
@@ -239,11 +243,11 @@ class TetrammDetector(StandardDetector):
239
243
  self.type = None
240
244
  super().__init__(
241
245
  controller,
242
- HDFWriter(
246
+ ADHDFWriter(
243
247
  self.hdf,
244
- directory_provider,
248
+ path_provider,
245
249
  lambda: self.name,
246
- TetrammShapeProvider(controller),
250
+ TetrammDatasetDescriber(controller),
247
251
  **scalar_sigs,
248
252
  ),
249
253
  config_signals,
File without changes
@@ -0,0 +1,10 @@
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.motor import Motor
3
+
4
+
5
+ class TrainingRigSampleStage(StandardReadable):
6
+ def __init__(self, prefix: str, name: str = ""):
7
+ with self.add_children_as_readables():
8
+ self.x = Motor(prefix + "X")
9
+ self.theta = Motor(prefix + "A")
10
+ super().__init__(name)
@@ -1,5 +1,5 @@
1
1
  from ophyd_async.core import Device
2
- from ophyd_async.epics.motion.motor import Motor
2
+ from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
5
  class TurboSlit(Device):
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
4
- from ophyd_async.epics.motion import Motor
4
+ from ophyd_async.epics.motor import Motor
5
5
  from ophyd_async.epics.signal import epics_signal_r
6
6
 
7
7
  # The acceptable difference, in mm, between the undulator gap and the DCM
@@ -8,7 +8,7 @@ from collections.abc import Callable, Generator
8
8
  from bluesky import plan_stubs as bps
9
9
  from bluesky.utils import Msg
10
10
  from ophyd.epics_motor import EpicsMotor
11
- from ophyd_async.epics.motion import Motor
11
+ from ophyd_async.epics.motor import Motor
12
12
 
13
13
  from dodal.log import LOGGER
14
14
 
@@ -7,7 +7,7 @@ from typing import cast
7
7
 
8
8
  from bluesky.run_engine import RunEngine
9
9
  from ophyd_async.core import Device, save_device
10
- from ophyd_async.panda import phase_sorter
10
+ from ophyd_async.fastcs.panda import phase_sorter
11
11
 
12
12
  from dodal.beamlines import module_name_for_beamline
13
13
  from dodal.utils import make_device
@@ -2,7 +2,7 @@ from ophyd_async.core import (
2
2
  callback_on_mock_put,
3
3
  set_mock_value,
4
4
  )
5
- from ophyd_async.epics.motion import Motor
5
+ from ophyd_async.epics.motor import Motor
6
6
 
7
7
 
8
8
  def patch_motor(motor: Motor, initial_position=0):
@@ -1,8 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from bluesky.protocols import Triggerable
4
- from ophyd_async.core import Device, observe_value
5
- from ophyd_async.core.async_status import AsyncStatus
4
+ from ophyd_async.core import AsyncStatus, Device, observe_value
6
5
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
7
6
 
8
7
 
@@ -9,7 +9,7 @@ from ophyd_async.core import (
9
9
  DeviceVector,
10
10
  wait_for_value,
11
11
  )
12
- from ophyd_async.epics.signal.signal import (
12
+ from ophyd_async.epics.signal import (
13
13
  epics_signal_r,
14
14
  epics_signal_rw,
15
15
  epics_signal_rw_rbv,
dodal/devices/zebra.py CHANGED
@@ -37,6 +37,11 @@ TTL_SHUTTER = 2
37
37
  TTL_XSPRESS3 = 3
38
38
  TTL_PANDA = 4
39
39
 
40
+ # The AND gate that controls the automatic shutter
41
+ AUTO_SHUTTER_GATE = 2
42
+ # The input that triggers the automatic shutter
43
+ AUTO_SHUTTER_INPUT = 1
44
+
40
45
 
41
46
  class ArmSource(str, Enum):
42
47
  SOFT = "Soft"
@@ -7,7 +7,7 @@ from ophyd_async.core import (
7
7
  StandardReadable,
8
8
  wait_for_value,
9
9
  )
10
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_w
10
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_w
11
11
 
12
12
 
13
13
  class ZebraShutterState(str, Enum):
@@ -15,22 +15,37 @@ class ZebraShutterState(str, Enum):
15
15
  OPEN = "Open"
16
16
 
17
17
 
18
+ class ZebraShutterControl(str, Enum):
19
+ MANUAL = "Manual"
20
+ AUTO = "Auto"
21
+
22
+
18
23
  class ZebraShutter(StandardReadable, Movable):
24
+ """The shutter on most MX beamlines is controlled by the zebra.
25
+
26
+ Internally in the zebra there are two AND gates, one for manual control and one for
27
+ automatic control. A soft input (aliased to control_mode) will switch between
28
+ which of these AND gates to use. For the manual gate the shutter is then controlled
29
+ by a different soft input (aliased to _manual_position_setpoint). Both these AND
30
+ gates then feed into an OR gate, which then feeds to the shutter."""
31
+
19
32
  def __init__(self, prefix: str, name: str):
20
- self.position_setpoint = epics_signal_w(
21
- write_pv=prefix + "CTRL2",
22
- datatype=ZebraShutterState,
33
+ self._manual_position_setpoint = epics_signal_w(
34
+ ZebraShutterState, prefix + "CTRL2"
23
35
  )
36
+ self.control_mode = epics_signal_rw(ZebraShutterControl, prefix + "CTRL1")
37
+
24
38
  with self.add_children_as_readables():
25
- self.position_readback = epics_signal_r(
26
- read_pv=prefix + "STA",
27
- datatype=ZebraShutterState,
28
- )
39
+ self.position_readback = epics_signal_r(ZebraShutterState, prefix + "STA")
29
40
  super().__init__(name=name)
30
41
 
31
42
  @AsyncStatus.wrap
32
43
  async def set(self, value: ZebraShutterState):
33
- await self.position_setpoint.set(value)
44
+ if await self.control_mode.get_value() == ZebraShutterControl.AUTO:
45
+ raise UserWarning(
46
+ f"Tried to set shutter to {value.value} but the shutter is in auto mode."
47
+ )
48
+ await self._manual_position_setpoint.set(value)
34
49
  return await wait_for_value(
35
50
  signal=self.position_readback,
36
51
  match=value,