ophyd-async 0.5.0__py3-none-any.whl → 0.5.2__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 (48) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +6 -3
  3. ophyd_async/core/_detector.py +38 -28
  4. ophyd_async/core/_hdf_dataset.py +1 -5
  5. ophyd_async/core/_mock_signal_utils.py +4 -3
  6. ophyd_async/core/_providers.py +30 -39
  7. ophyd_async/core/_signal.py +73 -28
  8. ophyd_async/core/_status.py +17 -1
  9. ophyd_async/epics/adaravis/_aravis.py +1 -1
  10. ophyd_async/epics/adcore/__init__.py +16 -5
  11. ophyd_async/epics/adcore/_core_io.py +29 -5
  12. ophyd_async/epics/adcore/_core_logic.py +7 -4
  13. ophyd_async/epics/adcore/_hdf_writer.py +51 -33
  14. ophyd_async/epics/adcore/_utils.py +69 -70
  15. ophyd_async/epics/adkinetix/_kinetix.py +1 -1
  16. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -1
  17. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  18. ophyd_async/epics/adpilatus/_pilatus_controller.py +1 -1
  19. ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
  20. ophyd_async/epics/adsimdetector/_sim.py +1 -1
  21. ophyd_async/epics/advimba/_vimba.py +1 -1
  22. ophyd_async/epics/advimba/_vimba_controller.py +3 -3
  23. ophyd_async/epics/advimba/_vimba_io.py +6 -4
  24. ophyd_async/epics/eiger/__init__.py +5 -0
  25. ophyd_async/epics/eiger/_eiger.py +43 -0
  26. ophyd_async/epics/eiger/_eiger_controller.py +66 -0
  27. ophyd_async/epics/eiger/_eiger_io.py +42 -0
  28. ophyd_async/epics/eiger/_odin_io.py +125 -0
  29. ophyd_async/epics/motor.py +16 -3
  30. ophyd_async/epics/signal/_aioca.py +12 -5
  31. ophyd_async/epics/signal/_common.py +1 -1
  32. ophyd_async/epics/signal/_p4p.py +14 -11
  33. ophyd_async/fastcs/panda/__init__.py +3 -3
  34. ophyd_async/fastcs/panda/{_common_blocks.py → _block.py} +2 -0
  35. ophyd_async/fastcs/panda/{_panda_controller.py → _control.py} +1 -1
  36. ophyd_async/fastcs/panda/_hdf_panda.py +4 -4
  37. ophyd_async/fastcs/panda/_trigger.py +1 -1
  38. ophyd_async/fastcs/panda/{_hdf_writer.py → _writer.py} +29 -22
  39. ophyd_async/plan_stubs/__init__.py +3 -0
  40. ophyd_async/plan_stubs/_nd_attributes.py +63 -0
  41. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +5 -2
  42. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +1 -3
  43. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/METADATA +46 -44
  44. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/RECORD +48 -42
  45. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/WHEEL +1 -1
  46. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/LICENSE +0 -0
  47. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/entry_points.txt +0 -0
  48. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.5.0'
16
- __version_tuple__ = version_tuple = (0, 5, 0)
15
+ __version__ = version = '0.5.2'
16
+ __version_tuple__ = version_tuple = (0, 5, 2)
@@ -33,11 +33,11 @@ from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable
33
33
  from ._providers import (
34
34
  AutoIncrementFilenameProvider,
35
35
  AutoIncrementingPathProvider,
36
+ DatasetDescriber,
36
37
  FilenameProvider,
37
38
  NameProvider,
38
39
  PathInfo,
39
40
  PathProvider,
40
- ShapeProvider,
41
41
  StaticFilenameProvider,
42
42
  StaticPathProvider,
43
43
  UUIDFilenameProvider,
@@ -55,6 +55,7 @@ from ._signal import (
55
55
  assert_reading,
56
56
  assert_value,
57
57
  observe_value,
58
+ set_and_wait_for_other_value,
58
59
  set_and_wait_for_value,
59
60
  soft_signal_r_and_setter,
60
61
  soft_signal_rw,
@@ -62,7 +63,7 @@ from ._signal import (
62
63
  )
63
64
  from ._signal_backend import RuntimeSubsetEnum, SignalBackend, SubsetEnum
64
65
  from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
65
- from ._status import AsyncStatus, WatchableAsyncStatus
66
+ from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
66
67
  from ._utils import (
67
68
  DEFAULT_TIMEOUT,
68
69
  CalculatableTimeout,
@@ -116,7 +117,7 @@ __all__ = [
116
117
  "NameProvider",
117
118
  "PathInfo",
118
119
  "PathProvider",
119
- "ShapeProvider",
120
+ "DatasetDescriber",
120
121
  "StaticFilenameProvider",
121
122
  "StaticPathProvider",
122
123
  "UUIDFilenameProvider",
@@ -135,6 +136,7 @@ __all__ = [
135
136
  "assert_value",
136
137
  "observe_value",
137
138
  "set_and_wait_for_value",
139
+ "set_and_wait_for_other_value",
138
140
  "soft_signal_r_and_setter",
139
141
  "soft_signal_rw",
140
142
  "wait_for_value",
@@ -156,4 +158,5 @@ __all__ = [
156
158
  "get_unique",
157
159
  "in_micros",
158
160
  "wait_for_connection",
161
+ "completed_status",
159
162
  ]
@@ -55,11 +55,16 @@ class TriggerInfo(BaseModel):
55
55
  #: Sort of triggers that will be sent
56
56
  trigger: DetectorTrigger = Field()
57
57
  #: What is the minimum deadtime between triggers
58
- deadtime: float = Field(ge=0)
58
+ deadtime: float | None = Field(ge=0)
59
59
  #: What is the maximum high time of the triggers
60
- livetime: float = Field(ge=0)
60
+ livetime: float | None = Field(ge=0)
61
61
  #: What is the maximum timeout on waiting for a frame
62
62
  frame_timeout: float | None = Field(None, gt=0)
63
+ #: How many triggers make up a single StreamDatum index, to allow multiple frames
64
+ #: from a faster detector to be zipped with a single frame from a slow detector
65
+ #: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
66
+ #: but publish 2 indices, and describe() will show a shape of (5, h, w)
67
+ multiplier: int = 1
63
68
 
64
69
 
65
70
  class DetectorControl(ABC):
@@ -69,7 +74,7 @@ class DetectorControl(ABC):
69
74
  """
70
75
 
71
76
  @abstractmethod
72
- def get_deadtime(self, exposure: float) -> float:
77
+ def get_deadtime(self, exposure: float | None) -> float:
73
78
  """For a given exposure, how long should the time between exposures be"""
74
79
 
75
80
  @abstractmethod
@@ -196,10 +201,10 @@ class StandardDetector(
196
201
 
197
202
  @AsyncStatus.wrap
198
203
  async def stage(self) -> None:
199
- # Disarm the detector, stop filewriting, and open file for writing.
204
+ # Disarm the detector, stop filewriting.
200
205
  await self._check_config_sigs()
201
206
  await asyncio.gather(self.writer.close(), self.controller.disarm())
202
- self._describe = await self.writer.open()
207
+ self._trigger_info = None
203
208
 
204
209
  async def _check_config_sigs(self):
205
210
  """Checks configuration signals are named and connected."""
@@ -212,7 +217,7 @@ class StandardDetector(
212
217
  await signal.get_value()
213
218
  except NotImplementedError:
214
219
  raise Exception(
215
- f"config signal {signal._name} must be connected before it is "
220
+ f"config signal {signal.name} must be connected before it is "
216
221
  + "passed to the detector"
217
222
  )
218
223
 
@@ -236,10 +241,15 @@ class StandardDetector(
236
241
 
237
242
  @AsyncStatus.wrap
238
243
  async def trigger(self) -> None:
239
- # set default trigger_info
240
- self._trigger_info = TriggerInfo(
241
- number=1, trigger=DetectorTrigger.internal, deadtime=0.0, livetime=0.0
242
- )
244
+ if self._trigger_info is None:
245
+ await self.prepare(
246
+ TriggerInfo(
247
+ number=1,
248
+ trigger=DetectorTrigger.internal,
249
+ deadtime=None,
250
+ livetime=None,
251
+ )
252
+ )
243
253
  # Arm the detector and wait for it to finish.
244
254
  indices_written = await self.writer.get_indices_written()
245
255
  written_status = await self.controller.arm(
@@ -250,19 +260,15 @@ class StandardDetector(
250
260
  end_observation = indices_written + 1
251
261
 
252
262
  async for index in self.writer.observe_indices_written(
253
- DEFAULT_TIMEOUT + self._trigger_info.livetime + self._trigger_info.deadtime
263
+ DEFAULT_TIMEOUT
264
+ + (self._trigger_info.livetime or 0)
265
+ + (self._trigger_info.deadtime or 0)
254
266
  ):
255
267
  if index >= end_observation:
256
268
  break
257
269
 
258
- def prepare(
259
- self,
260
- value: T,
261
- ) -> AsyncStatus:
262
- # Just arm detector for the time being
263
- return AsyncStatus(self._prepare(value))
264
-
265
- async def _prepare(self, value: T) -> None:
270
+ @AsyncStatus.wrap
271
+ async def prepare(self, value: TriggerInfo) -> None:
266
272
  """
267
273
  Arm detector.
268
274
 
@@ -277,22 +283,26 @@ class StandardDetector(
277
283
  Args:
278
284
  value: TriggerInfo describing how to trigger the detector
279
285
  """
280
- assert type(value) is TriggerInfo
281
286
  self._trigger_info = value
287
+ if value.trigger != DetectorTrigger.internal:
288
+ assert (
289
+ value.deadtime
290
+ ), "Deadtime must be supplied when in externally triggered mode"
291
+ if value.deadtime:
292
+ required = self.controller.get_deadtime(self._trigger_info.livetime)
293
+ assert required <= value.deadtime, (
294
+ f"Detector {self.controller} needs at least {required}s deadtime, "
295
+ f"but trigger logic provides only {value.deadtime}s"
296
+ )
282
297
  self._initial_frame = await self.writer.get_indices_written()
283
298
  self._last_frame = self._initial_frame + self._trigger_info.number
284
-
285
- required = self.controller.get_deadtime(self._trigger_info.livetime)
286
- assert required <= self._trigger_info.deadtime, (
287
- f"Detector {self.controller} needs at least {required}s deadtime, "
288
- f"but trigger logic provides only {self._trigger_info.deadtime}s"
289
- )
290
299
  self._arm_status = await self.controller.arm(
291
300
  num=self._trigger_info.number,
292
301
  trigger=self._trigger_info.trigger,
293
302
  exposure=self._trigger_info.livetime,
294
303
  )
295
304
  self._fly_start = time.monotonic()
305
+ self._describe = await self.writer.open(value.multiplier)
296
306
 
297
307
  @AsyncStatus.wrap
298
308
  async def kickoff(self):
@@ -307,8 +317,8 @@ class StandardDetector(
307
317
  self._trigger_info.frame_timeout
308
318
  or (
309
319
  DEFAULT_TIMEOUT
310
- + self._trigger_info.livetime
311
- + self._trigger_info.deadtime
320
+ + (self._trigger_info.livetime or 0)
321
+ + (self._trigger_info.deadtime or 0)
312
322
  )
313
323
  ):
314
324
  yield WatcherUpdate(
@@ -10,8 +10,6 @@ from event_model import (
10
10
  StreamResource,
11
11
  )
12
12
 
13
- from ._providers import PathInfo
14
-
15
13
 
16
14
  @dataclass
17
15
  class HDFDataset:
@@ -28,14 +26,12 @@ SLICE_NAME = "AD_HDF5_SWMR_SLICE"
28
26
 
29
27
  class HDFFile:
30
28
  """
31
- :param directory_info: Contains information about how to construct a StreamResource
32
29
  :param full_file_name: Absolute path to the file to be written
33
30
  :param datasets: Datasets to write into the file
34
31
  """
35
32
 
36
33
  def __init__(
37
34
  self,
38
- path_info: PathInfo,
39
35
  full_file_name: Path,
40
36
  datasets: List[HDFDataset],
41
37
  hostname: str = "localhost",
@@ -53,7 +49,7 @@ class HDFFile:
53
49
  (
54
50
  "file",
55
51
  self._hostname,
56
- str((path_info.root / full_file_name).absolute()),
52
+ str(full_file_name.absolute()),
57
53
  "",
58
54
  "",
59
55
  None,
@@ -8,11 +8,12 @@ from ._utils import T
8
8
 
9
9
 
10
10
  def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
11
- assert isinstance(signal._backend, MockSignalBackend), (
11
+ backend = signal._backend # noqa:SLF001
12
+ assert isinstance(backend, MockSignalBackend), (
12
13
  "Expected to receive a `MockSignalBackend`, instead "
13
- f" received {type(signal._backend)}. "
14
+ f" received {type(backend)}. "
14
15
  )
15
- return signal._backend
16
+ return backend
16
17
 
17
18
 
18
19
  def set_mock_value(signal: Signal[T], value: T):
@@ -13,34 +13,24 @@ class PathInfo:
13
13
  """
14
14
  Information about where and how to write a file.
15
15
 
16
- The bluesky event model splits the URI for a resource into two segments to aid in
17
- different applications mounting filesystems at different mount points.
18
- The portion of this path which is relevant only for the writer is the 'root',
19
- while the path from an agreed upon mutual mounting is the resource_path.
20
- The resource_dir is used with the filename to construct the resource_path.
21
-
22
- :param root: Path of a root directory, relevant only for the file writer
23
- :param resource_dir: Directory into which files should be written, relative to root
16
+ :param directory_path: Directory into which files should be written
24
17
  :param filename: Base filename to use generated by FilenameProvider, w/o extension
25
18
  :param create_dir_depth: Optional depth of directories to create if they do not
26
19
  exist
27
20
  """
28
21
 
29
- root: Path
30
- resource_dir: Path
22
+ directory_path: Path
31
23
  filename: str
32
24
  create_dir_depth: int = 0
33
25
 
34
26
 
35
27
  class FilenameProvider(Protocol):
36
28
  @abstractmethod
37
- def __call__(self) -> str:
29
+ def __call__(self, device_name: Optional[str] = None) -> str:
38
30
  """Get a filename to use for output data, w/o extension"""
39
31
 
40
32
 
41
33
  class PathProvider(Protocol):
42
- _filename_provider: FilenameProvider
43
-
44
34
  @abstractmethod
45
35
  def __call__(self, device_name: Optional[str] = None) -> PathInfo:
46
36
  """Get the current directory to write files into"""
@@ -50,7 +40,7 @@ class StaticFilenameProvider(FilenameProvider):
50
40
  def __init__(self, filename: str):
51
41
  self._static_filename = filename
52
42
 
53
- def __call__(self) -> str:
43
+ def __call__(self, device_name: Optional[str] = None) -> str:
54
44
  return self._static_filename
55
45
 
56
46
 
@@ -63,7 +53,7 @@ class UUIDFilenameProvider(FilenameProvider):
63
53
  self._uuid_call_func = uuid_call_func
64
54
  self._uuid_call_args = uuid_call_args or []
65
55
 
66
- def __call__(self) -> str:
56
+ def __call__(self, device_name: Optional[str] = None) -> str:
67
57
  if (
68
58
  self._uuid_call_func in [uuid.uuid3, uuid.uuid5]
69
59
  and len(self._uuid_call_args) < 2
@@ -92,7 +82,7 @@ class AutoIncrementFilenameProvider(FilenameProvider):
92
82
  self._increment = increment
93
83
  self._inc_delimeter = inc_delimeter
94
84
 
95
- def __call__(self):
85
+ def __call__(self, device_name: Optional[str] = None) -> str:
96
86
  if len(str(self._current_value)) > self._max_digits:
97
87
  raise ValueError(
98
88
  f"Auto incrementing filename counter \
@@ -112,20 +102,17 @@ class StaticPathProvider(PathProvider):
112
102
  self,
113
103
  filename_provider: FilenameProvider,
114
104
  directory_path: Path,
115
- resource_dir: Path = Path("."),
116
105
  create_dir_depth: int = 0,
117
106
  ) -> None:
118
107
  self._filename_provider = filename_provider
119
108
  self._directory_path = directory_path
120
- self._resource_dir = resource_dir
121
109
  self._create_dir_depth = create_dir_depth
122
110
 
123
111
  def __call__(self, device_name: Optional[str] = None) -> PathInfo:
124
- filename = self._filename_provider()
112
+ filename = self._filename_provider(device_name)
125
113
 
126
114
  return PathInfo(
127
- root=self._directory_path,
128
- resource_dir=self._resource_dir,
115
+ directory_path=self._directory_path,
129
116
  filename=filename,
130
117
  create_dir_depth=self._create_dir_depth,
131
118
  )
@@ -135,7 +122,7 @@ class AutoIncrementingPathProvider(PathProvider):
135
122
  def __init__(
136
123
  self,
137
124
  filename_provider: FilenameProvider,
138
- directory_path: Path,
125
+ base_directory_path: Path,
139
126
  create_dir_depth: int = 0,
140
127
  max_digits: int = 5,
141
128
  starting_value: int = 0,
@@ -145,7 +132,7 @@ class AutoIncrementingPathProvider(PathProvider):
145
132
  base_name: str = None,
146
133
  ) -> None:
147
134
  self._filename_provider = filename_provider
148
- self._directory_path = directory_path
135
+ self._base_directory_path = base_directory_path
149
136
  self._create_dir_depth = create_dir_depth
150
137
  self._base_name = base_name
151
138
  self._starting_value = starting_value
@@ -157,15 +144,17 @@ class AutoIncrementingPathProvider(PathProvider):
157
144
  self._inc_delimeter = inc_delimeter
158
145
 
159
146
  def __call__(self, device_name: Optional[str] = None) -> PathInfo:
160
- filename = self._filename_provider()
147
+ filename = self._filename_provider(device_name)
161
148
 
162
149
  padded_counter = f"{self._current_value:0{self._max_digits}}"
163
150
 
164
- resource_dir = str(padded_counter)
151
+ auto_inc_dir_name = str(padded_counter)
165
152
  if self._base_name is not None:
166
- resource_dir = f"{self._base_name}{self._inc_delimeter}{padded_counter}"
153
+ auto_inc_dir_name = (
154
+ f"{self._base_name}{self._inc_delimeter}{padded_counter}"
155
+ )
167
156
  elif device_name is not None:
168
- resource_dir = f"{device_name}{self._inc_delimeter}{padded_counter}"
157
+ auto_inc_dir_name = f"{device_name}{self._inc_delimeter}{padded_counter}"
169
158
 
170
159
  self._inc_counter += 1
171
160
  if self._inc_counter == self._num_calls_per_inc:
@@ -173,8 +162,7 @@ class AutoIncrementingPathProvider(PathProvider):
173
162
  self._current_value += self._increment
174
163
 
175
164
  return PathInfo(
176
- root=self._directory_path,
177
- resource_dir=resource_dir,
165
+ directory_path=self._base_directory_path / auto_inc_dir_name,
178
166
  filename=filename,
179
167
  create_dir_depth=self._create_dir_depth,
180
168
  )
@@ -184,12 +172,12 @@ class YMDPathProvider(PathProvider):
184
172
  def __init__(
185
173
  self,
186
174
  filename_provider: FilenameProvider,
187
- directory_path: Path,
175
+ base_directory_path: Path,
188
176
  create_dir_depth: int = -3, # Default to -3 to create YMD dirs
189
177
  device_name_as_base_dir: bool = False,
190
178
  ) -> None:
191
179
  self._filename_provider = filename_provider
192
- self._directory_path = Path(directory_path)
180
+ self._base_directory_path = Path(base_directory_path)
193
181
  self._create_dir_depth = create_dir_depth
194
182
  self._device_name_as_base_dir = device_name_as_base_dir
195
183
 
@@ -197,22 +185,21 @@ class YMDPathProvider(PathProvider):
197
185
  sep = os.path.sep
198
186
  current_date = date.today().strftime(f"%Y{sep}%m{sep}%d")
199
187
  if device_name is None:
200
- resource_dir = current_date
188
+ ymd_dir_path = current_date
201
189
  elif self._device_name_as_base_dir:
202
- resource_dir = os.path.join(
190
+ ymd_dir_path = os.path.join(
203
191
  current_date,
204
192
  device_name,
205
193
  )
206
194
  else:
207
- resource_dir = os.path.join(
195
+ ymd_dir_path = os.path.join(
208
196
  device_name,
209
197
  current_date,
210
198
  )
211
199
 
212
- filename = self._filename_provider()
200
+ filename = self._filename_provider(device_name)
213
201
  return PathInfo(
214
- root=self._directory_path,
215
- resource_dir=resource_dir,
202
+ directory_path=self._base_directory_path / ymd_dir_path,
216
203
  filename=filename,
217
204
  create_dir_depth=self._create_dir_depth,
218
205
  )
@@ -224,7 +211,11 @@ class NameProvider(Protocol):
224
211
  """Get the name to be used as a data_key in the descriptor document"""
225
212
 
226
213
 
227
- class ShapeProvider(Protocol):
214
+ class DatasetDescriber(Protocol):
215
+ @abstractmethod
216
+ async def np_datatype(self) -> str:
217
+ """Represents the numpy datatype"""
218
+
228
219
  @abstractmethod
229
- async def __call__(self) -> tuple:
220
+ async def shape(self) -> tuple[int, ...]:
230
221
  """Get the shape of the data collection"""
@@ -12,6 +12,7 @@ from typing import (
12
12
  Optional,
13
13
  Tuple,
14
14
  Type,
15
+ TypeVar,
15
16
  Union,
16
17
  )
17
18
 
@@ -33,6 +34,8 @@ from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
33
34
  from ._status import AsyncStatus
34
35
  from ._utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
35
36
 
37
+ S = TypeVar("S")
38
+
36
39
 
37
40
  def _add_timeout(func):
38
41
  @functools.wraps(func)
@@ -42,15 +45,6 @@ def _add_timeout(func):
42
45
  return wrapper
43
46
 
44
47
 
45
- def _fail(self, other, *args, **kwargs):
46
- if isinstance(other, Signal):
47
- raise TypeError(
48
- "Can't compare two Signals, did you mean await signal.get_value() instead?"
49
- )
50
- else:
51
- return NotImplemented
52
-
53
-
54
48
  class Signal(Device, Generic[T]):
55
49
  """A Device with the concept of a value, with R, RW, W and X flavours"""
56
50
 
@@ -115,12 +109,6 @@ class Signal(Device, Generic[T]):
115
109
  """Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
116
110
  return self._backend.source(self.name)
117
111
 
118
- __lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail
119
-
120
- def __hash__(self):
121
- # Restore the default implementation so we can use in a set or dict
122
- return hash(id(self))
123
-
124
112
 
125
113
  class _SignalCache(Generic[T]):
126
114
  def __init__(self, backend: SignalBackend[T], signal: Signal):
@@ -524,7 +512,9 @@ class _ValueChecker(Generic[T]):
524
512
 
525
513
 
526
514
  async def wait_for_value(
527
- signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: Optional[float]
515
+ signal: SignalR[T],
516
+ match: Union[T, Callable[[T], bool]],
517
+ timeout: Optional[float],
528
518
  ):
529
519
  """Wait for a signal to have a matching value.
530
520
 
@@ -556,6 +546,66 @@ async def wait_for_value(
556
546
  await checker.wait_for_value(signal, timeout)
557
547
 
558
548
 
549
+ async def set_and_wait_for_other_value(
550
+ set_signal: SignalW[T],
551
+ set_value: T,
552
+ read_signal: SignalR[S],
553
+ read_value: S,
554
+ timeout: float = DEFAULT_TIMEOUT,
555
+ set_timeout: Optional[float] = None,
556
+ ) -> AsyncStatus:
557
+ """Set a signal and monitor another signal until it has the specified value.
558
+
559
+ This function sets a set_signal to a specified set_value and waits for
560
+ a read_signal to have the read_value.
561
+
562
+ Parameters
563
+ ----------
564
+ signal:
565
+ The signal to set
566
+ set_value:
567
+ The value to set it to
568
+ read_signal:
569
+ The signal to monitor
570
+ read_value:
571
+ The value to wait for
572
+ timeout:
573
+ How long to wait for the signal to have the value
574
+ set_timeout:
575
+ How long to wait for the set to complete
576
+
577
+ Notes
578
+ -----
579
+ Example usage::
580
+
581
+ set_and_wait_for_value(device.acquire, 1, device.acquire_rbv, 1)
582
+ """
583
+ # Start monitoring before the set to avoid a race condition
584
+ values_gen = observe_value(read_signal)
585
+
586
+ # Get the initial value from the monitor to make sure we've created it
587
+ current_value = await anext(values_gen)
588
+
589
+ status = set_signal.set(set_value, timeout=set_timeout)
590
+
591
+ # If the value was the same as before no need to wait for it to change
592
+ if current_value != read_value:
593
+
594
+ async def _wait_for_value():
595
+ async for value in values_gen:
596
+ if value == read_value:
597
+ break
598
+
599
+ try:
600
+ await asyncio.wait_for(_wait_for_value(), timeout)
601
+ except asyncio.TimeoutError as e:
602
+ raise TimeoutError(
603
+ f"{read_signal.name} didn't match {read_value} in {timeout}s"
604
+ ) from e
605
+
606
+ return status
607
+
608
+
559
609
  async def set_and_wait_for_value(
560
610
  signal: SignalRW[T],
561
611
  value: T,
@@ -565,19 +615,14 @@ async def set_and_wait_for_value(
565
615
  """Set a signal and monitor it until it has that value.
566
616
 
567
617
  Useful for busy record, or other Signals with pattern:
568
-
569
- - Set Signal with wait=True and stash the Status
570
- - Read the same Signal to check the operation has started
571
- - Return the Status so calling code can wait for operation to complete
572
-
573
- This function sets a signal to a specified value, optionally with or without a
574
- ca/pv put callback, and waits for the readback value of the signal to match the
575
- value it was set to.
618
+ - Set Signal with wait=True and stash the Status
619
+ - Read the same Signal to check the operation has started
620
+ - Return the Status so calling code can wait for operation to complete
576
621
 
577
622
  Parameters
578
623
  ----------
579
624
  signal:
580
- The signal to set and monitor
625
+ The signal to set
581
626
  value:
582
627
  The value to set it to
583
628
  timeout:
@@ -591,6 +636,6 @@ async def set_and_wait_for_value(
591
636
 
592
637
  set_and_wait_for_value(device.acquire, 1)
593
638
  """
594
- status = signal.set(value, timeout=status_timeout)
595
- await wait_for_value(signal, value, timeout=timeout)
596
- return status
639
+ return await set_and_wait_for_other_value(
640
+ signal, value, signal, value, timeout, status_timeout
641
+ )
@@ -4,7 +4,16 @@ import asyncio
4
4
  import functools
5
5
  import time
6
6
  from dataclasses import asdict, replace
7
- from typing import AsyncIterator, Awaitable, Callable, Generic, Type, TypeVar, cast
7
+ from typing import (
8
+ AsyncIterator,
9
+ Awaitable,
10
+ Callable,
11
+ Generic,
12
+ Optional,
13
+ Type,
14
+ TypeVar,
15
+ cast,
16
+ )
8
17
 
9
18
  from bluesky.protocols import Status
10
19
 
@@ -132,3 +141,10 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
132
141
  return cls(f(*args, **kwargs))
133
142
 
134
143
  return cast(Callable[P, WAS], wrap_f)
144
+
145
+
146
+ @AsyncStatus.wrap
147
+ async def completed_status(exception: Optional[Exception] = None):
148
+ if exception:
149
+ raise exception
150
+ return None
@@ -37,7 +37,7 @@ class AravisDetector(StandardDetector, HasHints):
37
37
  self.hdf,
38
38
  path_provider,
39
39
  lambda: self.name,
40
- adcore.ADBaseShapeProvider(self.drv),
40
+ adcore.ADBaseDatasetDescriber(self.drv),
41
41
  ),
42
42
  config_sigs=(self.drv.acquire_time,),
43
43
  name=name,
@@ -1,7 +1,13 @@
1
- from ._core_io import ADBaseIO, DetectorState, NDFileHDFIO, NDPluginStatsIO
1
+ from ._core_io import (
2
+ ADBaseIO,
3
+ DetectorState,
4
+ NDArrayBaseIO,
5
+ NDFileHDFIO,
6
+ NDPluginStatsIO,
7
+ )
2
8
  from ._core_logic import (
3
9
  DEFAULT_GOOD_STATES,
4
- ADBaseShapeProvider,
10
+ ADBaseDatasetDescriber,
5
11
  set_exposure_time_and_acquire_period_if_supplied,
6
12
  start_acquiring_driver_and_ensure_status,
7
13
  )
@@ -12,17 +18,20 @@ from ._utils import (
12
18
  FileWriteMode,
13
19
  ImageMode,
14
20
  NDAttributeDataType,
15
- NDAttributesXML,
21
+ NDAttributeParam,
22
+ NDAttributePv,
23
+ NDAttributePvDbrType,
16
24
  stop_busy_record,
17
25
  )
18
26
 
19
27
  __all__ = [
20
28
  "ADBaseIO",
21
29
  "DetectorState",
30
+ "NDArrayBaseIO",
22
31
  "NDFileHDFIO",
23
32
  "NDPluginStatsIO",
24
33
  "DEFAULT_GOOD_STATES",
25
- "ADBaseShapeProvider",
34
+ "ADBaseDatasetDescriber",
26
35
  "set_exposure_time_and_acquire_period_if_supplied",
27
36
  "start_acquiring_driver_and_ensure_status",
28
37
  "ADHDFWriter",
@@ -30,7 +39,9 @@ __all__ = [
30
39
  "ADBaseDataType",
31
40
  "FileWriteMode",
32
41
  "ImageMode",
42
+ "NDAttributePv",
43
+ "NDAttributeParam",
33
44
  "NDAttributeDataType",
34
- "NDAttributesXML",
35
45
  "stop_busy_record",
46
+ "NDAttributePvDbrType",
36
47
  ]