ophyd-async 0.2.0__py3-none-any.whl → 0.3a2__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 (62) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +2 -2
  3. ophyd_async/core/__init__.py +16 -10
  4. ophyd_async/core/_providers.py +38 -5
  5. ophyd_async/core/async_status.py +3 -3
  6. ophyd_async/core/detector.py +211 -62
  7. ophyd_async/core/device.py +45 -38
  8. ophyd_async/core/device_save_loader.py +96 -23
  9. ophyd_async/core/flyer.py +30 -244
  10. ophyd_async/core/signal.py +47 -21
  11. ophyd_async/core/signal_backend.py +7 -4
  12. ophyd_async/core/sim_signal_backend.py +30 -18
  13. ophyd_async/core/standard_readable.py +4 -2
  14. ophyd_async/core/utils.py +93 -30
  15. ophyd_async/epics/_backend/_aioca.py +30 -36
  16. ophyd_async/epics/_backend/_p4p.py +75 -41
  17. ophyd_async/epics/_backend/common.py +25 -0
  18. ophyd_async/epics/areadetector/__init__.py +4 -0
  19. ophyd_async/epics/areadetector/aravis.py +69 -0
  20. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
  21. ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
  22. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +37 -25
  23. ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
  24. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
  25. ophyd_async/epics/areadetector/pilatus.py +50 -0
  26. ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
  27. ophyd_async/epics/areadetector/writers/hdf_writer.py +26 -15
  28. ophyd_async/epics/demo/__init__.py +33 -3
  29. ophyd_async/epics/motion/motor.py +20 -14
  30. ophyd_async/epics/pvi/__init__.py +3 -0
  31. ophyd_async/epics/pvi/pvi.py +318 -0
  32. ophyd_async/epics/signal/__init__.py +0 -2
  33. ophyd_async/epics/signal/signal.py +26 -9
  34. ophyd_async/panda/__init__.py +19 -5
  35. ophyd_async/panda/_common_blocks.py +49 -0
  36. ophyd_async/panda/_hdf_panda.py +48 -0
  37. ophyd_async/panda/_panda_controller.py +37 -0
  38. ophyd_async/panda/_trigger.py +39 -0
  39. ophyd_async/panda/_utils.py +15 -0
  40. ophyd_async/panda/writers/__init__.py +3 -0
  41. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  42. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  43. ophyd_async/planstubs/__init__.py +5 -0
  44. ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
  45. ophyd_async/protocols.py +73 -0
  46. ophyd_async/sim/__init__.py +11 -0
  47. ophyd_async/sim/demo/__init__.py +3 -0
  48. ophyd_async/sim/demo/sim_motor.py +116 -0
  49. ophyd_async/sim/pattern_generator.py +318 -0
  50. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  51. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  52. ophyd_async/sim/sim_pattern_generator.py +37 -0
  53. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +20 -76
  54. ophyd_async-0.3a2.dist-info/RECORD +76 -0
  55. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +1 -1
  56. ophyd_async/epics/signal/pvi_get.py +0 -22
  57. ophyd_async/panda/panda.py +0 -294
  58. ophyd_async-0.2.0.dist-info/RECORD +0 -53
  59. /ophyd_async/panda/{table.py → _table.py} +0 -0
  60. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
  61. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
  62. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/top_level.txt +0 -0
ophyd_async/__init__.py CHANGED
@@ -1,6 +1,3 @@
1
- from importlib.metadata import version # noqa
2
-
3
- __version__ = version("ophyd-async")
4
- del version
1
+ from ._version import __version__
5
2
 
6
3
  __all__ = ["__version__"]
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.2.0'
16
- __version_tuple__ = version_tuple = (0, 2, 0)
15
+ __version__ = version = '0.3a2'
16
+ __version_tuple__ = version_tuple = (0, 3)
@@ -6,22 +6,24 @@ from ._providers import (
6
6
  StaticDirectoryProvider,
7
7
  )
8
8
  from .async_status import AsyncStatus
9
- from .detector import DetectorControl, DetectorTrigger, DetectorWriter, StandardDetector
9
+ from .detector import (
10
+ DetectorControl,
11
+ DetectorTrigger,
12
+ DetectorWriter,
13
+ StandardDetector,
14
+ TriggerInfo,
15
+ )
10
16
  from .device import Device, DeviceCollector, DeviceVector
11
17
  from .device_save_loader import (
12
18
  get_signal_values,
19
+ load_device,
13
20
  load_from_yaml,
21
+ save_device,
14
22
  save_to_yaml,
15
23
  set_signal_values,
16
24
  walk_rw_signals,
17
25
  )
18
- from .flyer import (
19
- DetectorGroupLogic,
20
- HardwareTriggeredFlyable,
21
- SameTriggerDetectorGroupLogic,
22
- TriggerInfo,
23
- TriggerLogic,
24
- )
26
+ from .flyer import HardwareTriggeredFlyable, TriggerLogic
25
27
  from .signal import (
26
28
  Signal,
27
29
  SignalR,
@@ -33,6 +35,8 @@ from .signal import (
33
35
  set_sim_callback,
34
36
  set_sim_put_proceeds,
35
37
  set_sim_value,
38
+ soft_signal_r_and_backend,
39
+ soft_signal_rw,
36
40
  wait_for_value,
37
41
  )
38
42
  from .signal_backend import SignalBackend
@@ -65,6 +69,8 @@ __all__ = [
65
69
  "SignalW",
66
70
  "SignalRW",
67
71
  "SignalX",
72
+ "soft_signal_r_and_backend",
73
+ "soft_signal_rw",
68
74
  "observe_value",
69
75
  "set_and_wait_for_value",
70
76
  "set_sim_callback",
@@ -79,8 +85,6 @@ __all__ = [
79
85
  "StaticDirectoryProvider",
80
86
  "StandardReadable",
81
87
  "TriggerInfo",
82
- "DetectorGroupLogic",
83
- "SameTriggerDetectorGroupLogic",
84
88
  "TriggerLogic",
85
89
  "HardwareTriggeredFlyable",
86
90
  "DEFAULT_TIMEOUT",
@@ -97,4 +101,6 @@ __all__ = [
97
101
  "save_to_yaml",
98
102
  "set_signal_values",
99
103
  "walk_rw_signals",
104
+ "load_device",
105
+ "save_device",
100
106
  ]
@@ -1,12 +1,30 @@
1
1
  from abc import abstractmethod
2
2
  from dataclasses import dataclass
3
- from typing import Protocol, Sequence
3
+ from pathlib import Path
4
+ from typing import Optional, Protocol, Sequence, Union
4
5
 
5
6
 
6
7
  @dataclass
7
8
  class DirectoryInfo:
8
- directory_path: str
9
- filename_prefix: str
9
+ """
10
+ Information about where and how to write a file.
11
+
12
+ The bluesky event model splits the URI for a resource into two segments to aid in
13
+ different applications mounting filesystems at different mount points.
14
+ The portion of this path which is relevant only for the writer is the 'root',
15
+ while the path from an agreed upon mutual mounting is the resource_path.
16
+ The resource_dir is used with the filename to construct the resource_path.
17
+
18
+ :param root: Path of a root directory, relevant only for the file writer
19
+ :param resource_dir: Directory into which files should be written, relative to root
20
+ :param prefix: Optional filename prefix to add to all files
21
+ :param suffix: Optional filename suffix to add to all files
22
+ """
23
+
24
+ root: Path
25
+ resource_dir: Path
26
+ prefix: Optional[str] = ""
27
+ suffix: Optional[str] = ""
10
28
 
11
29
 
12
30
  class DirectoryProvider(Protocol):
@@ -16,8 +34,23 @@ class DirectoryProvider(Protocol):
16
34
 
17
35
 
18
36
  class StaticDirectoryProvider(DirectoryProvider):
19
- def __init__(self, directory_path: str, filename_prefix: str) -> None:
20
- self._directory_info = DirectoryInfo(directory_path, filename_prefix)
37
+ def __init__(
38
+ self,
39
+ directory_path: Union[str, Path],
40
+ filename_prefix: str = "",
41
+ filename_suffix: str = "",
42
+ resource_dir: Optional[Path] = None,
43
+ ) -> None:
44
+ if resource_dir is None:
45
+ resource_dir = Path(".")
46
+ if isinstance(directory_path, str):
47
+ directory_path = Path(directory_path)
48
+ self._directory_info = DirectoryInfo(
49
+ root=directory_path,
50
+ resource_dir=resource_dir,
51
+ prefix=filename_prefix,
52
+ suffix=filename_suffix,
53
+ )
21
54
 
22
55
  def __call__(self) -> DirectoryInfo:
23
56
  return self._directory_info
@@ -85,12 +85,12 @@ class AsyncStatus(Status):
85
85
 
86
86
  def __repr__(self) -> str:
87
87
  if self.done:
88
- if self.exception() is not None:
89
- status = "errored"
88
+ if e := self.exception():
89
+ status = f"errored: {repr(e)}"
90
90
  else:
91
91
  status = "done"
92
92
  else:
93
93
  status = "pending"
94
- return f"<{type(self).__name__} {status}>"
94
+ return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
95
95
 
96
96
  __str__ = __repr__
@@ -1,27 +1,47 @@
1
1
  """Module which defines abstract classes to work with detectors"""
2
+
2
3
  import asyncio
4
+ import time
3
5
  from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
4
7
  from enum import Enum
5
- from typing import AsyncIterator, Dict, Optional, Sequence
8
+ from typing import (
9
+ AsyncGenerator,
10
+ AsyncIterator,
11
+ Callable,
12
+ Dict,
13
+ Generic,
14
+ List,
15
+ Optional,
16
+ Sequence,
17
+ TypeVar,
18
+ )
6
19
 
7
20
  from bluesky.protocols import (
8
- Asset,
9
- Configurable,
21
+ Collectable,
10
22
  Descriptor,
11
- Readable,
23
+ Flyable,
24
+ Preparable,
12
25
  Reading,
13
26
  Stageable,
27
+ StreamAsset,
14
28
  Triggerable,
15
- WritesExternalAssets,
29
+ WritesStreamAssets,
16
30
  )
17
31
 
32
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
33
+
18
34
  from .async_status import AsyncStatus
19
35
  from .device import Device
20
36
  from .signal import SignalR
21
37
  from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
22
38
 
39
+ T = TypeVar("T")
40
+
23
41
 
24
42
  class DetectorTrigger(str, Enum):
43
+ """Type of mechanism for triggering a detector to take frames"""
44
+
25
45
  #: Detector generates internal trigger for given rate
26
46
  internal = "internal"
27
47
  #: Expect a series of arbitrary length trigger signals
@@ -32,7 +52,26 @@ class DetectorTrigger(str, Enum):
32
52
  variable_gate = "variable_gate"
33
53
 
34
54
 
55
+ @dataclass(frozen=True)
56
+ class TriggerInfo:
57
+ """Minimal set of information required to setup triggering on a detector"""
58
+
59
+ #: Number of triggers that will be sent
60
+ num: int
61
+ #: Sort of triggers that will be sent
62
+ trigger: DetectorTrigger
63
+ #: What is the minimum deadtime between triggers
64
+ deadtime: float
65
+ #: What is the maximum high time of the triggers
66
+ livetime: float
67
+
68
+
35
69
  class DetectorControl(ABC):
70
+ """
71
+ Classes implementing this interface should hold the logic for
72
+ arming and disarming a detector
73
+ """
74
+
36
75
  @abstractmethod
37
76
  def get_deadtime(self, exposure: float) -> float:
38
77
  """For a given exposure, how long should the time between exposures be"""
@@ -40,21 +79,36 @@ class DetectorControl(ABC):
40
79
  @abstractmethod
41
80
  async def arm(
42
81
  self,
82
+ num: int,
43
83
  trigger: DetectorTrigger = DetectorTrigger.internal,
44
- num: int = 0,
45
84
  exposure: Optional[float] = None,
46
85
  ) -> AsyncStatus:
47
- """Arm the detector and return AsyncStatus.
86
+ """
87
+ Arm detector, do all necessary steps to prepare detector for triggers.
88
+
89
+ Args:
90
+ num: Expected number of frames
91
+ trigger: Type of trigger for which to prepare the detector. Defaults to
92
+ DetectorTrigger.internal.
93
+ exposure: Exposure time with which to set up the detector. Defaults to None
94
+ if not applicable or the detector is expected to use its previously-set
95
+ exposure time.
48
96
 
49
- Awaiting the return value will wait for num frames to be written.
97
+ Returns:
98
+ AsyncStatus: Status representing the arm operation. This function returning
99
+ represents the start of the arm. The returned status completing means
100
+ the detector is now armed.
50
101
  """
51
102
 
52
103
  @abstractmethod
53
104
  async def disarm(self):
54
- """Disarm the detector"""
105
+ """Disarm the detector, return detector to an idle state"""
55
106
 
56
107
 
57
108
  class DetectorWriter(ABC):
109
+ """Logic for making a detector write data to somewhere persistent
110
+ (e.g. an HDF5 file)"""
111
+
58
112
  @abstractmethod
59
113
  async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
60
114
  """Open writer and wait for it to be ready for data.
@@ -68,40 +122,39 @@ class DetectorWriter(ABC):
68
122
  """
69
123
 
70
124
  @abstractmethod
71
- async def wait_for_index(
72
- self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
73
- ) -> None:
74
- """Wait until a specific index is ready to be collected"""
125
+ def observe_indices_written(
126
+ self, timeout=DEFAULT_TIMEOUT
127
+ ) -> AsyncGenerator[int, None]:
128
+ """Yield the index of each frame (or equivalent data point) as it is written"""
75
129
 
76
130
  @abstractmethod
77
131
  async def get_indices_written(self) -> int:
78
132
  """Get the number of indices written"""
79
133
 
80
134
  @abstractmethod
81
- def collect_stream_docs(self, indices_written: int) -> AsyncIterator[Asset]:
135
+ def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
82
136
  """Create Stream docs up to given number written"""
83
137
 
84
138
  @abstractmethod
85
139
  async def close(self) -> None:
86
- """Close writer and wait for it to be finished"""
140
+ """Close writer, blocks until I/O is complete"""
87
141
 
88
142
 
89
143
  class StandardDetector(
90
144
  Device,
91
145
  Stageable,
92
- Configurable,
93
- Readable,
146
+ AsyncConfigurable,
147
+ AsyncReadable,
94
148
  Triggerable,
95
- WritesExternalAssets,
149
+ Preparable,
150
+ Flyable,
151
+ Collectable,
152
+ WritesStreamAssets,
153
+ Generic[T],
96
154
  ):
97
- """Detector with useful default behaviour.
98
-
99
- Must be supplied instances of classes that inherit from DetectorControl and
100
- DetectorData, to dictate how the detector will be controlled (i.e. arming and
101
- disarming) as well as how the detector data will be written (i.e. opening and
102
- closing the writer, and handling data writing indices).
103
-
104
- NOTE: only for step-scans.
155
+ """
156
+ Useful detector base class for step and fly scanning detectors.
157
+ Aggregates controller and writer logic together.
105
158
  """
106
159
 
107
160
  def __init__(
@@ -113,20 +166,34 @@ class StandardDetector(
113
166
  writer_timeout: float = DEFAULT_TIMEOUT,
114
167
  ) -> None:
115
168
  """
116
- Parameters
117
- ----------
118
- control:
119
- instance of class which inherits from :class:`DetectorControl`
120
- data:
121
- instance of class which inherits from :class:`DetectorData`
122
- name:
123
- detector name
169
+ Constructor
170
+
171
+ Args:
172
+ controller: Logic for arming and disarming the detector
173
+ writer: Logic for making the detector write persistent data
174
+ config_sigs: Signals to read when describe and read
175
+ configuration are called. Defaults to ().
176
+ name: Device name. Defaults to "".
177
+ writer_timeout: Timeout for frame writing to start, if the
178
+ timeout is reached, ophyd-async assumes the detector
179
+ has a problem and raises an error.
180
+ Defaults to DEFAULT_TIMEOUT.
124
181
  """
125
182
  self._controller = controller
126
183
  self._writer = writer
127
184
  self._describe: Dict[str, Descriptor] = {}
128
185
  self._config_sigs = list(config_sigs)
129
186
  self._frame_writing_timeout = writer_timeout
187
+ # For prepare
188
+ self._arm_status: Optional[AsyncStatus] = None
189
+ self._trigger_info: Optional[TriggerInfo] = None
190
+ # For kickoff
191
+ self._watchers: List[Callable] = []
192
+ self._fly_status: Optional[AsyncStatus] = None
193
+ self._fly_start: float
194
+
195
+ self._intial_frame: int
196
+ self._last_frame: int
130
197
  super().__init__(name)
131
198
 
132
199
  @property
@@ -137,14 +204,20 @@ class StandardDetector(
137
204
  def writer(self) -> DetectorWriter:
138
205
  return self._writer
139
206
 
140
- async def check_config_sigs(self):
207
+ @AsyncStatus.wrap
208
+ async def stage(self) -> None:
209
+ # Disarm the detector, stop filewriting, and open file for writing.
210
+ await self._check_config_sigs()
211
+ await asyncio.gather(self.writer.close(), self.controller.disarm())
212
+ self._describe = await self.writer.open()
213
+
214
+ async def _check_config_sigs(self):
141
215
  """Checks configuration signals are named and connected."""
142
216
  for signal in self._config_sigs:
143
217
  if signal._name == "":
144
218
  raise Exception(
145
219
  "config signal must be named before it is passed to the detector"
146
220
  )
147
-
148
221
  try:
149
222
  await signal.get_value()
150
223
  except NotImplementedError:
@@ -154,46 +227,122 @@ class StandardDetector(
154
227
  )
155
228
 
156
229
  @AsyncStatus.wrap
157
- async def stage(self) -> None:
158
- """Disarm the detector, stop filewriting, and open file for writing."""
159
- await self.check_config_sigs()
160
- await asyncio.gather(self.writer.close(), self.controller.disarm())
161
- self._describe = await self.writer.open()
230
+ async def unstage(self) -> None:
231
+ # Stop data writing.
232
+ await self.writer.close()
233
+
234
+ async def read_configuration(self) -> Dict[str, Reading]:
235
+ return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
162
236
 
163
237
  async def describe_configuration(self) -> Dict[str, Descriptor]:
164
238
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
165
239
 
166
- async def read_configuration(self) -> Dict[str, Reading]:
167
- return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
240
+ async def read(self) -> Dict[str, Reading]:
241
+ # All data is in StreamResources, not Events, so nothing to output here
242
+ return {}
168
243
 
169
- def describe(self) -> Dict[str, Descriptor]:
244
+ async def describe(self) -> Dict[str, Descriptor]:
170
245
  return self._describe
171
246
 
172
247
  @AsyncStatus.wrap
173
248
  async def trigger(self) -> None:
174
- """Arm the detector and wait for it to finish."""
249
+ # Arm the detector and wait for it to finish.
175
250
  indices_written = await self.writer.get_indices_written()
176
- written_status = await self.controller.arm(DetectorTrigger.internal, num=1)
251
+ written_status = await self.controller.arm(
252
+ num=1,
253
+ trigger=DetectorTrigger.internal,
254
+ )
177
255
  await written_status
178
- await self.writer.wait_for_index(
179
- indices_written + 1, timeout=self._frame_writing_timeout
256
+ end_observation = indices_written + 1
257
+
258
+ async for index in self.writer.observe_indices_written(
259
+ self._frame_writing_timeout
260
+ ):
261
+ if index >= end_observation:
262
+ break
263
+
264
+ def prepare(
265
+ self,
266
+ value: T,
267
+ ) -> AsyncStatus:
268
+ # Just arm detector for the time being
269
+ return AsyncStatus(self._prepare(value))
270
+
271
+ async def _prepare(self, value: T) -> None:
272
+ """
273
+ Arm detector.
274
+
275
+ Prepare the detector with trigger information. This is determined at and passed
276
+ in from the plan level.
277
+
278
+ This currently only prepares detectors for flyscans and stepscans just use the
279
+ trigger information determined in trigger.
280
+
281
+ To do: Unify prepare to be use for both fly and step scans.
282
+
283
+ Args:
284
+ value: TriggerInfo describing how to trigger the detector
285
+ """
286
+ assert type(value) is TriggerInfo
287
+ self._trigger_info = value
288
+ self._initial_frame = await self.writer.get_indices_written()
289
+ self._last_frame = self._initial_frame + self._trigger_info.num
290
+
291
+ required = self.controller.get_deadtime(self._trigger_info.livetime)
292
+ assert required <= self._trigger_info.deadtime, (
293
+ f"Detector {self.controller} needs at least {required}s deadtime, "
294
+ f"but trigger logic provides only {self._trigger_info.deadtime}s"
180
295
  )
181
296
 
182
- async def read(self) -> Dict[str, Reading]:
183
- """Read the detector"""
184
- # All data is in StreamResources, not Events, so nothing to output here
185
- return {}
297
+ self._arm_status = await self.controller.arm(
298
+ num=self._trigger_info.num,
299
+ trigger=self._trigger_info.trigger,
300
+ exposure=self._trigger_info.livetime,
301
+ )
186
302
 
187
- async def collect_asset_docs(self) -> AsyncIterator[Asset]:
188
- """Collect stream datum documents for all indices written."""
189
- indices_written = await self.writer.get_indices_written()
303
+ @AsyncStatus.wrap
304
+ async def kickoff(self) -> None:
305
+ self._fly_status = AsyncStatus(self._fly(), self._watchers)
306
+ self._fly_start = time.monotonic()
307
+
308
+ async def _fly(self) -> None:
309
+ await self._observe_writer_indicies(self._last_frame)
310
+
311
+ async def _observe_writer_indicies(self, end_observation: int):
312
+ async for index in self.writer.observe_indices_written(
313
+ self._frame_writing_timeout
314
+ ):
315
+ for watcher in self._watchers:
316
+ watcher(
317
+ name=self.name,
318
+ current=index,
319
+ initial=self._initial_frame,
320
+ target=end_observation,
321
+ unit="",
322
+ precision=0,
323
+ time_elapsed=time.monotonic() - self._fly_start,
324
+ )
325
+ if index >= end_observation:
326
+ break
327
+
328
+ @AsyncStatus.wrap
329
+ async def complete(self) -> AsyncStatus:
330
+ assert self._fly_status, "Kickoff not run"
331
+ return await self._fly_status
332
+
333
+ async def describe_collect(self) -> Dict[str, Descriptor]:
334
+ return self._describe
190
335
 
191
- async for doc in self.writer.collect_stream_docs(indices_written):
336
+ async def collect_asset_docs(
337
+ self, index: Optional[int] = None
338
+ ) -> AsyncIterator[StreamAsset]:
339
+ # Collect stream datum documents for all indices written.
340
+ # The index is optional, and provided for fly scans, however this needs to be
341
+ # retrieved for step scans.
342
+ if not index:
343
+ index = await self.writer.get_indices_written()
344
+ async for doc in self.writer.collect_stream_docs(index):
192
345
  yield doc
193
- # async for doc in self.writer.collect_stream_docs(indices_written):
194
- # yield doc
195
346
 
196
- @AsyncStatus.wrap
197
- async def unstage(self) -> None:
198
- """Stop data writing."""
199
- await self.writer.close()
347
+ async def get_index(self) -> int:
348
+ return await self.writer.get_indices_written()
@@ -1,16 +1,24 @@
1
1
  """Base device"""
2
+
2
3
  from __future__ import annotations
3
4
 
4
- import asyncio
5
- import logging
6
5
  import sys
7
- from contextlib import suppress
8
- from typing import Any, Dict, Generator, Iterator, Optional, Set, Tuple, TypeVar
6
+ from typing import (
7
+ Any,
8
+ Coroutine,
9
+ Dict,
10
+ Generator,
11
+ Iterator,
12
+ Optional,
13
+ Set,
14
+ Tuple,
15
+ TypeVar,
16
+ )
9
17
 
10
18
  from bluesky.protocols import HasName
11
19
  from bluesky.run_engine import call_in_bluesky_event_loop
12
20
 
13
- from .utils import NotConnected, wait_for_connection
21
+ from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
14
22
 
15
23
 
16
24
  class Device(HasName):
@@ -50,16 +58,21 @@ class Device(HasName):
50
58
  child.set_name(child_name)
51
59
  child.parent = self
52
60
 
53
- async def connect(self, sim: bool = False):
61
+ async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
54
62
  """Connect self and all child Devices.
55
63
 
64
+ Contains a timeout that gets propagated to child.connect methods.
65
+
56
66
  Parameters
57
67
  ----------
58
68
  sim:
59
69
  If True then connect in simulation mode.
70
+ timeout:
71
+ Time to wait before failing with a TimeoutError.
60
72
  """
61
73
  coros = {
62
- name: child_device.connect(sim) for name, child_device in self.children()
74
+ name: child_device.connect(sim, timeout=timeout)
75
+ for name, child_device in self.children()
63
76
  }
64
77
  if coros:
65
78
  await wait_for_connection(**coros)
@@ -69,6 +82,14 @@ VT = TypeVar("VT", bound=Device)
69
82
 
70
83
 
71
84
  class DeviceVector(Dict[int, VT], Device):
85
+ """
86
+ Defines device components with indices.
87
+
88
+ In the below example, foos becomes a dictionary on the parent device
89
+ at runtime, so parent.foos[2] returns a FooDevice. For example usage see
90
+ :class:`~ophyd_async.epics.demo.DynamicSensorGroup`
91
+ """
92
+
72
93
  def children(self) -> Generator[Tuple[str, Device], None, None]:
73
94
  for attr_name, attr in self.items():
74
95
  if isinstance(attr, Device):
@@ -140,41 +161,19 @@ class DeviceCollector:
140
161
 
141
162
  async def _on_exit(self) -> None:
142
163
  # Name and kick off connect for devices
143
- tasks: Dict[asyncio.Task, str] = {}
164
+ connect_coroutines: Dict[str, Coroutine] = {}
144
165
  for name, obj in self._objects_on_exit.items():
145
166
  if name not in self._names_on_enter and isinstance(obj, Device):
146
167
  if self._set_name and not obj.name:
147
168
  obj.set_name(name)
148
169
  if self._connect:
149
- task = asyncio.create_task(obj.connect(self._sim))
150
- tasks[task] = name
151
- # Wait for all the signals to have finished
152
- if tasks:
153
- await self._wait_for_tasks(tasks)
154
-
155
- async def _wait_for_tasks(self, tasks: Dict[asyncio.Task, str]):
156
- done, pending = await asyncio.wait(tasks, timeout=self._timeout)
157
- if pending:
158
- msg = f"{len(pending)} Devices did not connect:"
159
- for t in pending:
160
- t.cancel()
161
- with suppress(Exception):
162
- await t
163
- e = t.exception()
164
- msg += f"\n {tasks[t]}: {type(e).__name__}"
165
- lines = str(e).splitlines()
166
- if len(lines) <= 1:
167
- msg += f": {e}"
168
- else:
169
- msg += "".join(f"\n {line}" for line in lines)
170
- logging.error(msg)
171
- raised = [t for t in done if t.exception()]
172
- if raised:
173
- logging.error(f"{len(raised)} Devices raised an error:")
174
- for t in raised:
175
- logging.exception(f" {tasks[t]}:", exc_info=t.exception())
176
- if pending or raised:
177
- raise NotConnected("Not all Devices connected")
170
+ connect_coroutines[name] = obj.connect(
171
+ self._sim, timeout=self._timeout
172
+ )
173
+
174
+ # Connect to all the devices
175
+ if connect_coroutines:
176
+ await wait_for_connection(**connect_coroutines)
178
177
 
179
178
  async def __aexit__(self, type, value, traceback):
180
179
  self._objects_on_exit = self._caller_locals()
@@ -182,4 +181,12 @@ class DeviceCollector:
182
181
 
183
182
  def __exit__(self, type_, value, traceback):
184
183
  self._objects_on_exit = self._caller_locals()
185
- return call_in_bluesky_event_loop(self._on_exit())
184
+ try:
185
+ fut = call_in_bluesky_event_loop(self._on_exit())
186
+ except RuntimeError:
187
+ raise NotConnected(
188
+ "Could not connect devices. Is the bluesky event loop running? See "
189
+ "https://blueskyproject.io/ophyd-async/main/"
190
+ "user/explanations/event-loop-choice.html for more info."
191
+ )
192
+ return fut