ophyd-async 0.1.0__py3-none-any.whl → 0.3a1__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 (60) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +47 -12
  3. ophyd_async/core/_providers.py +66 -0
  4. ophyd_async/core/async_status.py +7 -5
  5. ophyd_async/core/detector.py +321 -0
  6. ophyd_async/core/device.py +184 -0
  7. ophyd_async/core/device_save_loader.py +286 -0
  8. ophyd_async/core/flyer.py +94 -0
  9. ophyd_async/core/{_device/_signal/signal.py → signal.py} +46 -18
  10. ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +6 -2
  11. ophyd_async/core/{_device/_backend/sim_signal_backend.py → sim_signal_backend.py} +6 -2
  12. ophyd_async/core/{_device/standard_readable.py → standard_readable.py} +3 -3
  13. ophyd_async/core/utils.py +79 -29
  14. ophyd_async/epics/_backend/_aioca.py +38 -25
  15. ophyd_async/epics/_backend/_p4p.py +62 -27
  16. ophyd_async/epics/_backend/common.py +20 -0
  17. ophyd_async/epics/areadetector/__init__.py +10 -13
  18. ophyd_async/epics/areadetector/controllers/__init__.py +4 -0
  19. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
  20. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +49 -0
  21. ophyd_async/epics/areadetector/drivers/__init__.py +15 -0
  22. ophyd_async/epics/areadetector/drivers/ad_base.py +111 -0
  23. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +18 -0
  24. ophyd_async/epics/areadetector/single_trigger_det.py +4 -4
  25. ophyd_async/epics/areadetector/utils.py +91 -3
  26. ophyd_async/epics/areadetector/writers/__init__.py +5 -0
  27. ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
  28. ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
  29. ophyd_async/epics/areadetector/writers/hdf_writer.py +133 -0
  30. ophyd_async/epics/areadetector/{nd_file_hdf.py → writers/nd_file_hdf.py} +22 -5
  31. ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
  32. ophyd_async/epics/demo/__init__.py +3 -2
  33. ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
  34. ophyd_async/epics/motion/motor.py +2 -1
  35. ophyd_async/epics/pvi.py +70 -0
  36. ophyd_async/epics/signal/__init__.py +0 -2
  37. ophyd_async/epics/signal/signal.py +1 -1
  38. ophyd_async/panda/__init__.py +12 -8
  39. ophyd_async/panda/panda.py +43 -134
  40. ophyd_async/panda/panda_controller.py +41 -0
  41. ophyd_async/panda/table.py +158 -0
  42. ophyd_async/panda/utils.py +15 -0
  43. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +49 -42
  44. ophyd_async-0.3a1.dist-info/RECORD +56 -0
  45. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
  46. ophyd_async/core/_device/__init__.py +0 -0
  47. ophyd_async/core/_device/_backend/__init__.py +0 -0
  48. ophyd_async/core/_device/_signal/__init__.py +0 -0
  49. ophyd_async/core/_device/device.py +0 -60
  50. ophyd_async/core/_device/device_collector.py +0 -121
  51. ophyd_async/core/_device/device_vector.py +0 -14
  52. ophyd_async/epics/areadetector/ad_driver.py +0 -18
  53. ophyd_async/epics/areadetector/directory_provider.py +0 -18
  54. ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
  55. ophyd_async/epics/areadetector/nd_plugin.py +0 -13
  56. ophyd_async/epics/signal/pvi_get.py +0 -22
  57. ophyd_async-0.1.0.dist-info/RECORD +0 -45
  58. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
  59. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
  60. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.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.1.0'
16
- __version_tuple__ = version_tuple = (0, 1, 0)
15
+ __version__ = version = '0.3a1'
16
+ __version_tuple__ = version_tuple = (0, 3)
@@ -1,6 +1,24 @@
1
- from ._device._backend.signal_backend import SignalBackend
2
- from ._device._backend.sim_signal_backend import SimSignalBackend
3
- from ._device._signal.signal import (
1
+ from ._providers import (
2
+ DirectoryInfo,
3
+ DirectoryProvider,
4
+ NameProvider,
5
+ ShapeProvider,
6
+ StaticDirectoryProvider,
7
+ )
8
+ from .async_status import AsyncStatus
9
+ from .detector import DetectorControl, DetectorTrigger, DetectorWriter, StandardDetector
10
+ from .device import Device, DeviceCollector, DeviceVector
11
+ from .device_save_loader import (
12
+ get_signal_values,
13
+ load_device,
14
+ load_from_yaml,
15
+ save_device,
16
+ save_to_yaml,
17
+ set_signal_values,
18
+ walk_rw_signals,
19
+ )
20
+ from .flyer import HardwareTriggeredFlyable, TriggerInfo, TriggerLogic
21
+ from .signal import (
4
22
  Signal,
5
23
  SignalR,
6
24
  SignalRW,
@@ -13,11 +31,9 @@ from ._device._signal.signal import (
13
31
  set_sim_value,
14
32
  wait_for_value,
15
33
  )
16
- from ._device.device import Device
17
- from ._device.device_collector import DeviceCollector
18
- from ._device.device_vector import DeviceVector
19
- from ._device.standard_readable import StandardReadable
20
- from .async_status import AsyncStatus
34
+ from .signal_backend import SignalBackend
35
+ from .sim_signal_backend import SimSignalBackend
36
+ from .standard_readable import StandardReadable
21
37
  from .utils import (
22
38
  DEFAULT_TIMEOUT,
23
39
  Callback,
@@ -33,6 +49,13 @@ from .utils import (
33
49
  __all__ = [
34
50
  "SignalBackend",
35
51
  "SimSignalBackend",
52
+ "DetectorControl",
53
+ "DetectorTrigger",
54
+ "DetectorWriter",
55
+ "StandardDetector",
56
+ "Device",
57
+ "DeviceCollector",
58
+ "DeviceVector",
36
59
  "Signal",
37
60
  "SignalR",
38
61
  "SignalW",
@@ -44,11 +67,16 @@ __all__ = [
44
67
  "set_sim_put_proceeds",
45
68
  "set_sim_value",
46
69
  "wait_for_value",
47
- "Device",
48
- "DeviceCollector",
49
- "DeviceVector",
50
- "StandardReadable",
51
70
  "AsyncStatus",
71
+ "DirectoryInfo",
72
+ "DirectoryProvider",
73
+ "NameProvider",
74
+ "ShapeProvider",
75
+ "StaticDirectoryProvider",
76
+ "StandardReadable",
77
+ "TriggerInfo",
78
+ "TriggerLogic",
79
+ "HardwareTriggeredFlyable",
52
80
  "DEFAULT_TIMEOUT",
53
81
  "Callback",
54
82
  "NotConnected",
@@ -58,4 +86,11 @@ __all__ = [
58
86
  "get_unique",
59
87
  "merge_gathered_dicts",
60
88
  "wait_for_connection",
89
+ "get_signal_values",
90
+ "load_from_yaml",
91
+ "save_to_yaml",
92
+ "set_signal_values",
93
+ "walk_rw_signals",
94
+ "load_device",
95
+ "save_device",
61
96
  ]
@@ -0,0 +1,66 @@
1
+ from abc import abstractmethod
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Optional, Protocol, Sequence, Union
5
+
6
+
7
+ @dataclass
8
+ class DirectoryInfo:
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] = ""
28
+
29
+
30
+ class DirectoryProvider(Protocol):
31
+ @abstractmethod
32
+ def __call__(self) -> DirectoryInfo:
33
+ """Get the current directory to write files into"""
34
+
35
+
36
+ class StaticDirectoryProvider(DirectoryProvider):
37
+ def __init__(
38
+ self,
39
+ directory_path: Union[str, Path],
40
+ filename_prefix: str = "",
41
+ filename_suffix: str = "",
42
+ resource_dir: Path = Path("."),
43
+ ) -> None:
44
+ if isinstance(directory_path, str):
45
+ directory_path = Path(directory_path)
46
+ self._directory_info = DirectoryInfo(
47
+ root=directory_path,
48
+ resource_dir=resource_dir,
49
+ prefix=filename_prefix,
50
+ suffix=filename_suffix,
51
+ )
52
+
53
+ def __call__(self) -> DirectoryInfo:
54
+ return self._directory_info
55
+
56
+
57
+ class NameProvider(Protocol):
58
+ @abstractmethod
59
+ def __call__(self) -> str:
60
+ """Get the name to be used as a data_key in the descriptor document"""
61
+
62
+
63
+ class ShapeProvider(Protocol):
64
+ @abstractmethod
65
+ async def __call__(self) -> Sequence[int]:
66
+ """Get the shape of the data collection"""
@@ -1,4 +1,4 @@
1
- """Equivalent of bluesky.protols.Status for asynchronous tasks."""
1
+ """Equivalent of bluesky.protocols.Status for asynchronous tasks."""
2
2
 
3
3
  import asyncio
4
4
  import functools
@@ -62,7 +62,9 @@ class AsyncStatus(Status):
62
62
  @property
63
63
  def success(self) -> bool:
64
64
  return (
65
- self.task.done() and not self.task.cancelled() and not self.task.exception()
65
+ self.task.done()
66
+ and not self.task.cancelled()
67
+ and self.task.exception() is None
66
68
  )
67
69
 
68
70
  def watch(self, watcher: Callable):
@@ -83,12 +85,12 @@ class AsyncStatus(Status):
83
85
 
84
86
  def __repr__(self) -> str:
85
87
  if self.done:
86
- if self.exception() is not None:
87
- status = "errored"
88
+ if e := self.exception():
89
+ status = f"errored: {repr(e)}"
88
90
  else:
89
91
  status = "done"
90
92
  else:
91
93
  status = "pending"
92
- return f"<{type(self).__name__} {status}>"
94
+ return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
93
95
 
94
96
  __str__ = __repr__
@@ -0,0 +1,321 @@
1
+ """Module which defines abstract classes to work with detectors"""
2
+
3
+ import asyncio
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from typing import (
9
+ AsyncGenerator,
10
+ AsyncIterator,
11
+ Callable,
12
+ Dict,
13
+ List,
14
+ Optional,
15
+ Sequence,
16
+ TypeVar,
17
+ )
18
+
19
+ from bluesky.protocols import (
20
+ Collectable,
21
+ Configurable,
22
+ Descriptor,
23
+ Flyable,
24
+ Preparable,
25
+ Readable,
26
+ Reading,
27
+ Stageable,
28
+ StreamAsset,
29
+ Triggerable,
30
+ WritesStreamAssets,
31
+ )
32
+
33
+ from .async_status import AsyncStatus
34
+ from .device import Device
35
+ from .signal import SignalR
36
+ from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
37
+
38
+ T = TypeVar("T")
39
+
40
+
41
+ class DetectorTrigger(str, Enum):
42
+ #: Detector generates internal trigger for given rate
43
+ internal = "internal"
44
+ #: Expect a series of arbitrary length trigger signals
45
+ edge_trigger = "edge_trigger"
46
+ #: Expect a series of constant width external gate signals
47
+ constant_gate = "constant_gate"
48
+ #: Expect a series of variable width external gate signals
49
+ variable_gate = "variable_gate"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class TriggerInfo:
54
+ #: Number of triggers that will be sent
55
+ num: int
56
+ #: Sort of triggers that will be sent
57
+ trigger: DetectorTrigger
58
+ #: What is the minimum deadtime between triggers
59
+ deadtime: float
60
+ #: What is the maximum high time of the triggers
61
+ livetime: float
62
+
63
+
64
+ class DetectorControl(ABC):
65
+ @abstractmethod
66
+ def get_deadtime(self, exposure: float) -> float:
67
+ """For a given exposure, how long should the time between exposures be"""
68
+
69
+ @abstractmethod
70
+ async def arm(
71
+ self,
72
+ num: int,
73
+ trigger: DetectorTrigger = DetectorTrigger.internal,
74
+ exposure: Optional[float] = None,
75
+ ) -> AsyncStatus:
76
+ """Arm the detector and return AsyncStatus.
77
+
78
+ Awaiting the return value will wait for num frames to be written.
79
+ """
80
+
81
+ @abstractmethod
82
+ async def disarm(self):
83
+ """Disarm the detector"""
84
+
85
+
86
+ class DetectorWriter(ABC):
87
+ @abstractmethod
88
+ async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
89
+ """Open writer and wait for it to be ready for data.
90
+
91
+ Args:
92
+ multiplier: Each StreamDatum index corresponds to this many
93
+ written exposures
94
+
95
+ Returns:
96
+ Output for ``describe()``
97
+ """
98
+
99
+ @abstractmethod
100
+ def observe_indices_written(
101
+ self, timeout=DEFAULT_TIMEOUT
102
+ ) -> AsyncGenerator[int, None]:
103
+ """Yield each index as it is written"""
104
+
105
+ @abstractmethod
106
+ async def get_indices_written(self) -> int:
107
+ """Get the number of indices written"""
108
+
109
+ @abstractmethod
110
+ def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
111
+ """Create Stream docs up to given number written"""
112
+
113
+ @abstractmethod
114
+ async def close(self) -> None:
115
+ """Close writer and wait for it to be finished"""
116
+
117
+
118
+ class StandardDetector(
119
+ Device,
120
+ Stageable,
121
+ Configurable,
122
+ Readable,
123
+ Triggerable,
124
+ Preparable,
125
+ Flyable,
126
+ Collectable,
127
+ WritesStreamAssets,
128
+ ):
129
+ """Detector with useful step and flyscan behaviour.
130
+
131
+ Must be supplied instances of classes that inherit from DetectorControl and
132
+ DetectorData, to dictate how the detector will be controlled (i.e. arming and
133
+ disarming) as well as how the detector data will be written (i.e. opening and
134
+ closing the writer, and handling data writing indices).
135
+
136
+ """
137
+
138
+ def __init__(
139
+ self,
140
+ controller: DetectorControl,
141
+ writer: DetectorWriter,
142
+ config_sigs: Sequence[SignalR] = (),
143
+ name: str = "",
144
+ writer_timeout: float = DEFAULT_TIMEOUT,
145
+ ) -> None:
146
+ """
147
+ Parameters
148
+ ----------
149
+ control:
150
+ instance of class which inherits from :class:`DetectorControl`
151
+ data:
152
+ instance of class which inherits from :class:`DetectorData`
153
+ name:
154
+ detector name
155
+ """
156
+ self._controller = controller
157
+ self._writer = writer
158
+ self._describe: Dict[str, Descriptor] = {}
159
+ self._config_sigs = list(config_sigs)
160
+ self._frame_writing_timeout = writer_timeout
161
+ # For prepare
162
+ self._arm_status: Optional[AsyncStatus] = None
163
+ self._trigger_info: Optional[TriggerInfo] = None
164
+ # For kickoff
165
+ self._watchers: List[Callable] = []
166
+ self._fly_status: Optional[AsyncStatus] = None
167
+ self._fly_start: float
168
+
169
+ self._intial_frame: int
170
+ self._last_frame: int
171
+ super().__init__(name)
172
+
173
+ @property
174
+ def controller(self) -> DetectorControl:
175
+ return self._controller
176
+
177
+ @property
178
+ def writer(self) -> DetectorWriter:
179
+ return self._writer
180
+
181
+ @AsyncStatus.wrap
182
+ async def stage(self) -> None:
183
+ """Disarm the detector, stop filewriting, and open file for writing."""
184
+ await self.check_config_sigs()
185
+ await asyncio.gather(self.writer.close(), self.controller.disarm())
186
+ self._describe = await self.writer.open()
187
+
188
+ async def check_config_sigs(self):
189
+ """Checks configuration signals are named and connected."""
190
+ for signal in self._config_sigs:
191
+ if signal._name == "":
192
+ raise Exception(
193
+ "config signal must be named before it is passed to the detector"
194
+ )
195
+ try:
196
+ await signal.get_value()
197
+ except NotImplementedError:
198
+ raise Exception(
199
+ f"config signal {signal._name} must be connected before it is "
200
+ + "passed to the detector"
201
+ )
202
+
203
+ @AsyncStatus.wrap
204
+ async def unstage(self) -> None:
205
+ """Stop data writing."""
206
+ await self.writer.close()
207
+
208
+ async def read_configuration(self) -> Dict[str, Reading]:
209
+ return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
210
+
211
+ async def describe_configuration(self) -> Dict[str, Descriptor]:
212
+ return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
213
+
214
+ async def read(self) -> Dict[str, Reading]:
215
+ """Read the detector"""
216
+ # All data is in StreamResources, not Events, so nothing to output here
217
+ return {}
218
+
219
+ def describe(self) -> Dict[str, Descriptor]:
220
+ return self._describe
221
+
222
+ @AsyncStatus.wrap
223
+ async def trigger(self) -> None:
224
+ """Arm the detector and wait for it to finish."""
225
+ indices_written = await self.writer.get_indices_written()
226
+ written_status = await self.controller.arm(
227
+ num=1,
228
+ trigger=DetectorTrigger.internal,
229
+ )
230
+ await written_status
231
+ end_observation = indices_written + 1
232
+
233
+ async for index in self.writer.observe_indices_written(
234
+ self._frame_writing_timeout
235
+ ):
236
+ if index >= end_observation:
237
+ break
238
+
239
+ def prepare(
240
+ self,
241
+ value: T,
242
+ ) -> AsyncStatus:
243
+ """Arm detector"""
244
+ return AsyncStatus(self._prepare(value))
245
+
246
+ async def _prepare(self, value: T) -> None:
247
+ """Arm detector.
248
+
249
+ Prepare the detector with trigger information. This is determined at and passed
250
+ in from the plan level.
251
+
252
+ This currently only prepares detectors for flyscans and stepscans just use the
253
+ trigger information determined in trigger.
254
+
255
+ To do: Unify prepare to be use for both fly and step scans.
256
+ """
257
+ assert type(value) is TriggerInfo
258
+ self._trigger_info = value
259
+ self._initial_frame = await self.writer.get_indices_written()
260
+ self._last_frame = self._initial_frame + self._trigger_info.num
261
+
262
+ required = self.controller.get_deadtime(self._trigger_info.livetime)
263
+ assert required <= self._trigger_info.deadtime, (
264
+ f"Detector {self.controller} needs at least {required}s deadtime, "
265
+ f"but trigger logic provides only {self._trigger_info.deadtime}s"
266
+ )
267
+
268
+ self._arm_status = await self.controller.arm(
269
+ num=self._trigger_info.num,
270
+ trigger=self._trigger_info.trigger,
271
+ exposure=self._trigger_info.livetime,
272
+ )
273
+
274
+ @AsyncStatus.wrap
275
+ async def kickoff(self) -> None:
276
+ self._fly_status = AsyncStatus(self._fly(), self._watchers)
277
+ self._fly_start = time.monotonic()
278
+
279
+ async def _fly(self) -> None:
280
+ await self._observe_writer_indicies(self._last_frame)
281
+
282
+ async def _observe_writer_indicies(self, end_observation: int):
283
+ async for index in self.writer.observe_indices_written(
284
+ self._frame_writing_timeout
285
+ ):
286
+ for watcher in self._watchers:
287
+ watcher(
288
+ name=self.name,
289
+ current=index,
290
+ initial=self._initial_frame,
291
+ target=end_observation,
292
+ unit="",
293
+ precision=0,
294
+ time_elapsed=time.monotonic() - self._fly_start,
295
+ )
296
+ if index >= end_observation:
297
+ break
298
+
299
+ @AsyncStatus.wrap
300
+ async def complete(self) -> AsyncStatus:
301
+ assert self._fly_status, "Kickoff not run"
302
+ return await self._fly_status
303
+
304
+ async def describe_collect(self) -> Dict[str, Descriptor]:
305
+ return self._describe
306
+
307
+ async def collect_asset_docs(
308
+ self, index: Optional[int] = None
309
+ ) -> AsyncIterator[StreamAsset]:
310
+ """Collect stream datum documents for all indices written.
311
+
312
+ The index is optional, and provided for flyscans, however this needs to be
313
+ retrieved for stepscans.
314
+ """
315
+ if not index:
316
+ index = await self.writer.get_indices_written()
317
+ async for doc in self.writer.collect_stream_docs(index):
318
+ yield doc
319
+
320
+ async def get_index(self) -> int:
321
+ return await self.writer.get_indices_written()
@@ -0,0 +1,184 @@
1
+ """Base device"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import (
7
+ Any,
8
+ Coroutine,
9
+ Dict,
10
+ Generator,
11
+ Iterator,
12
+ Optional,
13
+ Set,
14
+ Tuple,
15
+ TypeVar,
16
+ )
17
+
18
+ from bluesky.protocols import HasName
19
+ from bluesky.run_engine import call_in_bluesky_event_loop
20
+
21
+ from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
22
+
23
+
24
+ class Device(HasName):
25
+ """Common base class for all Ophyd Async Devices.
26
+
27
+ By default, names and connects all Device children.
28
+ """
29
+
30
+ _name: str = ""
31
+ #: The parent Device if it exists
32
+ parent: Optional[Device] = None
33
+
34
+ def __init__(self, name: str = "") -> None:
35
+ self.set_name(name)
36
+
37
+ @property
38
+ def name(self) -> str:
39
+ """Return the name of the Device"""
40
+ return self._name
41
+
42
+ def children(self) -> Iterator[Tuple[str, Device]]:
43
+ for attr_name, attr in self.__dict__.items():
44
+ if attr_name != "parent" and isinstance(attr, Device):
45
+ yield attr_name, attr
46
+
47
+ def set_name(self, name: str):
48
+ """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
49
+
50
+ Parameters
51
+ ----------
52
+ name:
53
+ New name to set
54
+ """
55
+ self._name = name
56
+ for attr_name, child in self.children():
57
+ child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
58
+ child.set_name(child_name)
59
+ child.parent = self
60
+
61
+ async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
62
+ """Connect self and all child Devices.
63
+
64
+ Contains a timeout that gets propagated to child.connect methods.
65
+
66
+ Parameters
67
+ ----------
68
+ sim:
69
+ If True then connect in simulation mode.
70
+ timeout:
71
+ Time to wait before failing with a TimeoutError.
72
+ """
73
+ coros = {
74
+ name: child_device.connect(sim, timeout=timeout)
75
+ for name, child_device in self.children()
76
+ }
77
+ if coros:
78
+ await wait_for_connection(**coros)
79
+
80
+
81
+ VT = TypeVar("VT", bound=Device)
82
+
83
+
84
+ class DeviceVector(Dict[int, VT], Device):
85
+ def children(self) -> Generator[Tuple[str, Device], None, None]:
86
+ for attr_name, attr in self.items():
87
+ if isinstance(attr, Device):
88
+ yield str(attr_name), attr
89
+
90
+
91
+ class DeviceCollector:
92
+ """Collector of top level Device instances to be used as a context manager
93
+
94
+ Parameters
95
+ ----------
96
+ set_name:
97
+ If True, call ``device.set_name(variable_name)`` on all collected
98
+ Devices
99
+ connect:
100
+ If True, call ``device.connect(sim)`` in parallel on all
101
+ collected Devices
102
+ sim:
103
+ If True, connect Signals in simulation mode
104
+ timeout:
105
+ How long to wait for connect before logging an exception
106
+
107
+ Notes
108
+ -----
109
+ Example usage::
110
+
111
+ [async] with DeviceCollector():
112
+ t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
113
+ t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
114
+ # Names and connects devices here
115
+ assert t1x.comm.velocity.source
116
+ assert t1x.name == "t1x"
117
+
118
+ """
119
+
120
+ def __init__(
121
+ self,
122
+ set_name=True,
123
+ connect=True,
124
+ sim=False,
125
+ timeout: float = 10.0,
126
+ ):
127
+ self._set_name = set_name
128
+ self._connect = connect
129
+ self._sim = sim
130
+ self._timeout = timeout
131
+ self._names_on_enter: Set[str] = set()
132
+ self._objects_on_exit: Dict[str, Any] = {}
133
+
134
+ def _caller_locals(self):
135
+ """Walk up until we find a stack frame that doesn't have us as self"""
136
+ try:
137
+ raise ValueError
138
+ except ValueError:
139
+ _, _, tb = sys.exc_info()
140
+ assert tb, "Can't get traceback, this shouldn't happen"
141
+ caller_frame = tb.tb_frame
142
+ while caller_frame.f_locals.get("self", None) is self:
143
+ caller_frame = caller_frame.f_back
144
+ return caller_frame.f_locals
145
+
146
+ def __enter__(self) -> "DeviceCollector":
147
+ # Stash the names that were defined before we were called
148
+ self._names_on_enter = set(self._caller_locals())
149
+ return self
150
+
151
+ async def __aenter__(self) -> "DeviceCollector":
152
+ return self.__enter__()
153
+
154
+ async def _on_exit(self) -> None:
155
+ # Name and kick off connect for devices
156
+ connect_coroutines: Dict[str, Coroutine] = {}
157
+ for name, obj in self._objects_on_exit.items():
158
+ if name not in self._names_on_enter and isinstance(obj, Device):
159
+ if self._set_name and not obj.name:
160
+ obj.set_name(name)
161
+ if self._connect:
162
+ connect_coroutines[name] = obj.connect(
163
+ self._sim, timeout=self._timeout
164
+ )
165
+
166
+ # Connect to all the devices
167
+ if connect_coroutines:
168
+ await wait_for_connection(**connect_coroutines)
169
+
170
+ async def __aexit__(self, type, value, traceback):
171
+ self._objects_on_exit = self._caller_locals()
172
+ await self._on_exit()
173
+
174
+ def __exit__(self, type_, value, traceback):
175
+ self._objects_on_exit = self._caller_locals()
176
+ try:
177
+ fut = call_in_bluesky_event_loop(self._on_exit())
178
+ except RuntimeError:
179
+ raise NotConnected(
180
+ "Could not connect devices. Is the bluesky event loop running? See "
181
+ "https://blueskyproject.io/ophyd-async/main/"
182
+ "user/explanations/event-loop-choice.html for more info."
183
+ )
184
+ return fut