ophyd-async 0.1.0__py3-none-any.whl → 0.3.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.
- ophyd_async/__init__.py +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +91 -19
- ophyd_async/core/_providers.py +68 -0
- ophyd_async/core/async_status.py +90 -42
- ophyd_async/core/detector.py +341 -0
- ophyd_async/core/device.py +226 -0
- ophyd_async/core/device_save_loader.py +286 -0
- ophyd_async/core/flyer.py +85 -0
- ophyd_async/core/mock_signal_backend.py +82 -0
- ophyd_async/core/mock_signal_utils.py +145 -0
- ophyd_async/core/{_device/_signal/signal.py → signal.py} +249 -61
- ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +12 -5
- ophyd_async/core/{_device/_backend/sim_signal_backend.py → soft_signal_backend.py} +54 -48
- ophyd_async/core/standard_readable.py +261 -0
- ophyd_async/core/utils.py +127 -30
- ophyd_async/epics/_backend/_aioca.py +62 -43
- ophyd_async/epics/_backend/_p4p.py +100 -52
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +16 -15
- ophyd_async/epics/areadetector/aravis.py +63 -0
- ophyd_async/epics/areadetector/controllers/__init__.py +5 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
- ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +61 -0
- ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +21 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +107 -0
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
- ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +21 -0
- ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
- ophyd_async/epics/areadetector/kinetix.py +46 -0
- ophyd_async/epics/areadetector/pilatus.py +45 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +18 -10
- ophyd_async/epics/areadetector/utils.py +91 -13
- ophyd_async/epics/areadetector/vimba.py +43 -0
- ophyd_async/epics/areadetector/writers/__init__.py +5 -0
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +142 -0
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +40 -0
- ophyd_async/epics/areadetector/writers/nd_plugin.py +38 -0
- ophyd_async/epics/demo/__init__.py +78 -51
- ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
- ophyd_async/epics/motion/motor.py +67 -52
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +8 -3
- ophyd_async/epics/signal/signal.py +27 -10
- ophyd_async/log.py +130 -0
- ophyd_async/panda/__init__.py +24 -7
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_table.py +158 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/plan_stubs/__init__.py +13 -0
- ophyd_async/plan_stubs/ensure_connected.py +22 -0
- ophyd_async/plan_stubs/fly.py +149 -0
- ophyd_async/protocols.py +126 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +103 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +35 -67
- ophyd_async-0.3.0.dist-info/RECORD +86 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device/__init__.py +0 -0
- ophyd_async/core/_device/_backend/__init__.py +0 -0
- ophyd_async/core/_device/_signal/__init__.py +0 -0
- ophyd_async/core/_device/device.py +0 -60
- ophyd_async/core/_device/device_collector.py +0 -121
- ophyd_async/core/_device/device_vector.py +0 -14
- ophyd_async/core/_device/standard_readable.py +0 -72
- ophyd_async/epics/areadetector/ad_driver.py +0 -18
- ophyd_async/epics/areadetector/directory_provider.py +0 -18
- ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
- ophyd_async/epics/areadetector/nd_file_hdf.py +0 -22
- ophyd_async/epics/areadetector/nd_plugin.py +0 -13
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -332
- ophyd_async-0.1.0.dist-info/RECORD +0 -45
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
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
|
+
Generic,
|
|
14
|
+
List,
|
|
15
|
+
Optional,
|
|
16
|
+
Sequence,
|
|
17
|
+
TypeVar,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from bluesky.protocols import (
|
|
21
|
+
Collectable,
|
|
22
|
+
DataKey,
|
|
23
|
+
Flyable,
|
|
24
|
+
Preparable,
|
|
25
|
+
Reading,
|
|
26
|
+
Stageable,
|
|
27
|
+
StreamAsset,
|
|
28
|
+
Triggerable,
|
|
29
|
+
WritesStreamAssets,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
|
|
33
|
+
|
|
34
|
+
from .async_status import AsyncStatus, WatchableAsyncStatus
|
|
35
|
+
from .device import Device
|
|
36
|
+
from .utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts
|
|
37
|
+
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DetectorTrigger(str, Enum):
|
|
42
|
+
"""Type of mechanism for triggering a detector to take frames"""
|
|
43
|
+
|
|
44
|
+
#: Detector generates internal trigger for given rate
|
|
45
|
+
internal = "internal"
|
|
46
|
+
#: Expect a series of arbitrary length trigger signals
|
|
47
|
+
edge_trigger = "edge_trigger"
|
|
48
|
+
#: Expect a series of constant width external gate signals
|
|
49
|
+
constant_gate = "constant_gate"
|
|
50
|
+
#: Expect a series of variable width external gate signals
|
|
51
|
+
variable_gate = "variable_gate"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class TriggerInfo:
|
|
56
|
+
"""Minimal set of information required to setup triggering on a detector"""
|
|
57
|
+
|
|
58
|
+
#: Number of triggers that will be sent
|
|
59
|
+
num: int
|
|
60
|
+
#: Sort of triggers that will be sent
|
|
61
|
+
trigger: DetectorTrigger
|
|
62
|
+
#: What is the minimum deadtime between triggers
|
|
63
|
+
deadtime: float
|
|
64
|
+
#: What is the maximum high time of the triggers
|
|
65
|
+
livetime: float
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DetectorControl(ABC):
|
|
69
|
+
"""
|
|
70
|
+
Classes implementing this interface should hold the logic for
|
|
71
|
+
arming and disarming a detector
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def get_deadtime(self, exposure: float) -> float:
|
|
76
|
+
"""For a given exposure, how long should the time between exposures be"""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
async def arm(
|
|
80
|
+
self,
|
|
81
|
+
num: int,
|
|
82
|
+
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
83
|
+
exposure: Optional[float] = None,
|
|
84
|
+
) -> AsyncStatus:
|
|
85
|
+
"""
|
|
86
|
+
Arm detector, do all necessary steps to prepare detector for triggers.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
num: Expected number of frames
|
|
90
|
+
trigger: Type of trigger for which to prepare the detector. Defaults to
|
|
91
|
+
DetectorTrigger.internal.
|
|
92
|
+
exposure: Exposure time with which to set up the detector. Defaults to None
|
|
93
|
+
if not applicable or the detector is expected to use its previously-set
|
|
94
|
+
exposure time.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
AsyncStatus: Status representing the arm operation. This function returning
|
|
98
|
+
represents the start of the arm. The returned status completing means
|
|
99
|
+
the detector is now armed.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def disarm(self):
|
|
104
|
+
"""Disarm the detector, return detector to an idle state"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DetectorWriter(ABC):
|
|
108
|
+
"""Logic for making a detector write data to somewhere persistent
|
|
109
|
+
(e.g. an HDF5 file)"""
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
|
|
113
|
+
"""Open writer and wait for it to be ready for data.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
multiplier: Each StreamDatum index corresponds to this many
|
|
117
|
+
written exposures
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Output for ``describe()``
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def observe_indices_written(
|
|
125
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
126
|
+
) -> AsyncGenerator[int, None]:
|
|
127
|
+
"""Yield the index of each frame (or equivalent data point) as it is written"""
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
async def get_indices_written(self) -> int:
|
|
131
|
+
"""Get the number of indices written"""
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
|
|
135
|
+
"""Create Stream docs up to given number written"""
|
|
136
|
+
|
|
137
|
+
@abstractmethod
|
|
138
|
+
async def close(self) -> None:
|
|
139
|
+
"""Close writer, blocks until I/O is complete"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class StandardDetector(
|
|
143
|
+
Device,
|
|
144
|
+
Stageable,
|
|
145
|
+
AsyncConfigurable,
|
|
146
|
+
AsyncReadable,
|
|
147
|
+
Triggerable,
|
|
148
|
+
Preparable,
|
|
149
|
+
Flyable,
|
|
150
|
+
Collectable,
|
|
151
|
+
WritesStreamAssets,
|
|
152
|
+
Generic[T],
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
Useful detector base class for step and fly scanning detectors.
|
|
156
|
+
Aggregates controller and writer logic together.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(
|
|
160
|
+
self,
|
|
161
|
+
controller: DetectorControl,
|
|
162
|
+
writer: DetectorWriter,
|
|
163
|
+
config_sigs: Sequence[AsyncReadable] = (),
|
|
164
|
+
name: str = "",
|
|
165
|
+
writer_timeout: float = DEFAULT_TIMEOUT,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Constructor
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
controller: Logic for arming and disarming the detector
|
|
172
|
+
writer: Logic for making the detector write persistent data
|
|
173
|
+
config_sigs: Signals to read when describe and read
|
|
174
|
+
configuration are called. Defaults to ().
|
|
175
|
+
name: Device name. Defaults to "".
|
|
176
|
+
writer_timeout: Timeout for frame writing to start, if the
|
|
177
|
+
timeout is reached, ophyd-async assumes the detector
|
|
178
|
+
has a problem and raises an error.
|
|
179
|
+
Defaults to DEFAULT_TIMEOUT.
|
|
180
|
+
"""
|
|
181
|
+
self._controller = controller
|
|
182
|
+
self._writer = writer
|
|
183
|
+
self._describe: Dict[str, DataKey] = {}
|
|
184
|
+
self._config_sigs = list(config_sigs)
|
|
185
|
+
self._frame_writing_timeout = writer_timeout
|
|
186
|
+
# For prepare
|
|
187
|
+
self._arm_status: Optional[AsyncStatus] = None
|
|
188
|
+
self._trigger_info: Optional[TriggerInfo] = None
|
|
189
|
+
# For kickoff
|
|
190
|
+
self._watchers: List[Callable] = []
|
|
191
|
+
self._fly_status: Optional[WatchableAsyncStatus] = None
|
|
192
|
+
self._fly_start: float
|
|
193
|
+
|
|
194
|
+
self._intial_frame: int
|
|
195
|
+
self._last_frame: int
|
|
196
|
+
super().__init__(name)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def controller(self) -> DetectorControl:
|
|
200
|
+
return self._controller
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def writer(self) -> DetectorWriter:
|
|
204
|
+
return self._writer
|
|
205
|
+
|
|
206
|
+
@AsyncStatus.wrap
|
|
207
|
+
async def stage(self) -> None:
|
|
208
|
+
# Disarm the detector, stop filewriting, and open file for writing.
|
|
209
|
+
await self._check_config_sigs()
|
|
210
|
+
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
211
|
+
self._describe = await self.writer.open()
|
|
212
|
+
|
|
213
|
+
async def _check_config_sigs(self):
|
|
214
|
+
"""Checks configuration signals are named and connected."""
|
|
215
|
+
for signal in self._config_sigs:
|
|
216
|
+
if signal.name == "":
|
|
217
|
+
raise Exception(
|
|
218
|
+
"config signal must be named before it is passed to the detector"
|
|
219
|
+
)
|
|
220
|
+
try:
|
|
221
|
+
await signal.get_value()
|
|
222
|
+
except NotImplementedError:
|
|
223
|
+
raise Exception(
|
|
224
|
+
f"config signal {signal._name} must be connected before it is "
|
|
225
|
+
+ "passed to the detector"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
@AsyncStatus.wrap
|
|
229
|
+
async def unstage(self) -> None:
|
|
230
|
+
# Stop data writing.
|
|
231
|
+
await self.writer.close()
|
|
232
|
+
|
|
233
|
+
async def read_configuration(self) -> Dict[str, Reading]:
|
|
234
|
+
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
235
|
+
|
|
236
|
+
async def describe_configuration(self) -> Dict[str, DataKey]:
|
|
237
|
+
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
238
|
+
|
|
239
|
+
async def read(self) -> Dict[str, Reading]:
|
|
240
|
+
# All data is in StreamResources, not Events, so nothing to output here
|
|
241
|
+
return {}
|
|
242
|
+
|
|
243
|
+
async def describe(self) -> Dict[str, DataKey]:
|
|
244
|
+
return self._describe
|
|
245
|
+
|
|
246
|
+
@AsyncStatus.wrap
|
|
247
|
+
async def trigger(self) -> None:
|
|
248
|
+
# Arm the detector and wait for it to finish.
|
|
249
|
+
indices_written = await self.writer.get_indices_written()
|
|
250
|
+
written_status = await self.controller.arm(
|
|
251
|
+
num=1,
|
|
252
|
+
trigger=DetectorTrigger.internal,
|
|
253
|
+
)
|
|
254
|
+
await written_status
|
|
255
|
+
end_observation = indices_written + 1
|
|
256
|
+
|
|
257
|
+
async for index in self.writer.observe_indices_written(
|
|
258
|
+
self._frame_writing_timeout
|
|
259
|
+
):
|
|
260
|
+
if index >= end_observation:
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
def prepare(
|
|
264
|
+
self,
|
|
265
|
+
value: T,
|
|
266
|
+
) -> AsyncStatus:
|
|
267
|
+
# Just arm detector for the time being
|
|
268
|
+
return AsyncStatus(self._prepare(value))
|
|
269
|
+
|
|
270
|
+
async def _prepare(self, value: T) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Arm detector.
|
|
273
|
+
|
|
274
|
+
Prepare the detector with trigger information. This is determined at and passed
|
|
275
|
+
in from the plan level.
|
|
276
|
+
|
|
277
|
+
This currently only prepares detectors for flyscans and stepscans just use the
|
|
278
|
+
trigger information determined in trigger.
|
|
279
|
+
|
|
280
|
+
To do: Unify prepare to be use for both fly and step scans.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
value: TriggerInfo describing how to trigger the detector
|
|
284
|
+
"""
|
|
285
|
+
assert type(value) is TriggerInfo
|
|
286
|
+
self._trigger_info = value
|
|
287
|
+
self._initial_frame = await self.writer.get_indices_written()
|
|
288
|
+
self._last_frame = self._initial_frame + self._trigger_info.num
|
|
289
|
+
|
|
290
|
+
required = self.controller.get_deadtime(self._trigger_info.livetime)
|
|
291
|
+
assert required <= self._trigger_info.deadtime, (
|
|
292
|
+
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
293
|
+
f"but trigger logic provides only {self._trigger_info.deadtime}s"
|
|
294
|
+
)
|
|
295
|
+
self._arm_status = await self.controller.arm(
|
|
296
|
+
num=self._trigger_info.num,
|
|
297
|
+
trigger=self._trigger_info.trigger,
|
|
298
|
+
exposure=self._trigger_info.livetime,
|
|
299
|
+
)
|
|
300
|
+
self._fly_start = time.monotonic()
|
|
301
|
+
|
|
302
|
+
@AsyncStatus.wrap
|
|
303
|
+
async def kickoff(self):
|
|
304
|
+
if not self._arm_status:
|
|
305
|
+
raise Exception("Detector not armed!")
|
|
306
|
+
|
|
307
|
+
@WatchableAsyncStatus.wrap
|
|
308
|
+
async def complete(self):
|
|
309
|
+
assert self._arm_status, "Prepare not run"
|
|
310
|
+
assert self._trigger_info
|
|
311
|
+
async for index in self.writer.observe_indices_written(
|
|
312
|
+
self._frame_writing_timeout
|
|
313
|
+
):
|
|
314
|
+
yield WatcherUpdate(
|
|
315
|
+
name=self.name,
|
|
316
|
+
current=index,
|
|
317
|
+
initial=self._initial_frame,
|
|
318
|
+
target=self._trigger_info.num,
|
|
319
|
+
unit="",
|
|
320
|
+
precision=0,
|
|
321
|
+
time_elapsed=time.monotonic() - self._fly_start,
|
|
322
|
+
)
|
|
323
|
+
if index >= self._trigger_info.num:
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
async def describe_collect(self) -> Dict[str, DataKey]:
|
|
327
|
+
return self._describe
|
|
328
|
+
|
|
329
|
+
async def collect_asset_docs(
|
|
330
|
+
self, index: Optional[int] = None
|
|
331
|
+
) -> AsyncIterator[StreamAsset]:
|
|
332
|
+
# Collect stream datum documents for all indices written.
|
|
333
|
+
# The index is optional, and provided for fly scans, however this needs to be
|
|
334
|
+
# retrieved for step scans.
|
|
335
|
+
if index is None:
|
|
336
|
+
index = await self.writer.get_indices_written()
|
|
337
|
+
async for doc in self.writer.collect_stream_docs(index):
|
|
338
|
+
yield doc
|
|
339
|
+
|
|
340
|
+
async def get_index(self) -> int:
|
|
341
|
+
return await self.writer.get_indices_written()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Base device"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from logging import LoggerAdapter, getLogger
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Coroutine,
|
|
10
|
+
Dict,
|
|
11
|
+
Generator,
|
|
12
|
+
Iterator,
|
|
13
|
+
Optional,
|
|
14
|
+
Set,
|
|
15
|
+
Tuple,
|
|
16
|
+
TypeVar,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from bluesky.protocols import HasName
|
|
20
|
+
from bluesky.run_engine import call_in_bluesky_event_loop
|
|
21
|
+
|
|
22
|
+
from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Device(HasName):
|
|
26
|
+
"""Common base class for all Ophyd Async Devices.
|
|
27
|
+
|
|
28
|
+
By default, names and connects all Device children.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_name: str = ""
|
|
32
|
+
#: The parent Device if it exists
|
|
33
|
+
parent: Optional["Device"] = None
|
|
34
|
+
# None if connect hasn't started, a Task if it has
|
|
35
|
+
_connect_task: Optional[asyncio.Task] = None
|
|
36
|
+
_connect_mock_arg: bool = False
|
|
37
|
+
|
|
38
|
+
def __init__(self, name: str = "") -> None:
|
|
39
|
+
self.set_name(name)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def name(self) -> str:
|
|
43
|
+
"""Return the name of the Device"""
|
|
44
|
+
return self._name
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def log(self):
|
|
48
|
+
return LoggerAdapter(
|
|
49
|
+
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def children(self) -> Iterator[Tuple[str, "Device"]]:
|
|
53
|
+
for attr_name, attr in self.__dict__.items():
|
|
54
|
+
if attr_name != "parent" and isinstance(attr, Device):
|
|
55
|
+
yield attr_name, attr
|
|
56
|
+
|
|
57
|
+
def set_name(self, name: str):
|
|
58
|
+
"""Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
name:
|
|
63
|
+
New name to set
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Ensure self.log is recreated after a name change
|
|
67
|
+
if hasattr(self, "log"):
|
|
68
|
+
del self.log
|
|
69
|
+
|
|
70
|
+
self._name = name
|
|
71
|
+
for attr_name, child in self.children():
|
|
72
|
+
child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
|
|
73
|
+
child.set_name(child_name)
|
|
74
|
+
child.parent = self
|
|
75
|
+
|
|
76
|
+
async def connect(
|
|
77
|
+
self,
|
|
78
|
+
mock: bool = False,
|
|
79
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
80
|
+
force_reconnect: bool = False,
|
|
81
|
+
):
|
|
82
|
+
"""Connect self and all child Devices.
|
|
83
|
+
|
|
84
|
+
Contains a timeout that gets propagated to child.connect methods.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
mock:
|
|
89
|
+
If True then use ``MockSignalBackend`` for all Signals
|
|
90
|
+
timeout:
|
|
91
|
+
Time to wait before failing with a TimeoutError.
|
|
92
|
+
"""
|
|
93
|
+
# If previous connect with same args has started and not errored, can use it
|
|
94
|
+
can_use_previous_connect = (
|
|
95
|
+
self._connect_task
|
|
96
|
+
and not (self._connect_task.done() and self._connect_task.exception())
|
|
97
|
+
and self._connect_mock_arg == mock
|
|
98
|
+
)
|
|
99
|
+
if force_reconnect or not can_use_previous_connect:
|
|
100
|
+
# Kick off a connection
|
|
101
|
+
coros = {
|
|
102
|
+
name: child_device.connect(
|
|
103
|
+
mock, timeout=timeout, force_reconnect=force_reconnect
|
|
104
|
+
)
|
|
105
|
+
for name, child_device in self.children()
|
|
106
|
+
}
|
|
107
|
+
self._connect_task = asyncio.create_task(wait_for_connection(**coros))
|
|
108
|
+
self._connect_mock_arg = mock
|
|
109
|
+
|
|
110
|
+
assert self._connect_task, "Connect task not created, this shouldn't happen"
|
|
111
|
+
# Wait for it to complete
|
|
112
|
+
await self._connect_task
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
VT = TypeVar("VT", bound=Device)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DeviceVector(Dict[int, VT], Device):
|
|
119
|
+
"""
|
|
120
|
+
Defines device components with indices.
|
|
121
|
+
|
|
122
|
+
In the below example, foos becomes a dictionary on the parent device
|
|
123
|
+
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
124
|
+
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def children(self) -> Generator[Tuple[str, Device], None, None]:
|
|
128
|
+
for attr_name, attr in self.items():
|
|
129
|
+
if isinstance(attr, Device):
|
|
130
|
+
yield str(attr_name), attr
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DeviceCollector:
|
|
134
|
+
"""Collector of top level Device instances to be used as a context manager
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
set_name:
|
|
139
|
+
If True, call ``device.set_name(variable_name)`` on all collected
|
|
140
|
+
Devices
|
|
141
|
+
connect:
|
|
142
|
+
If True, call ``device.connect(mock)`` in parallel on all
|
|
143
|
+
collected Devices
|
|
144
|
+
mock:
|
|
145
|
+
If True, connect Signals in simulation mode
|
|
146
|
+
timeout:
|
|
147
|
+
How long to wait for connect before logging an exception
|
|
148
|
+
|
|
149
|
+
Notes
|
|
150
|
+
-----
|
|
151
|
+
Example usage::
|
|
152
|
+
|
|
153
|
+
[async] with DeviceCollector():
|
|
154
|
+
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
155
|
+
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
156
|
+
# Names and connects devices here
|
|
157
|
+
assert t1x.comm.velocity.source
|
|
158
|
+
assert t1x.name == "t1x"
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
set_name=True,
|
|
165
|
+
connect=True,
|
|
166
|
+
mock=False,
|
|
167
|
+
timeout: float = 10.0,
|
|
168
|
+
):
|
|
169
|
+
self._set_name = set_name
|
|
170
|
+
self._connect = connect
|
|
171
|
+
self._mock = mock
|
|
172
|
+
self._timeout = timeout
|
|
173
|
+
self._names_on_enter: Set[str] = set()
|
|
174
|
+
self._objects_on_exit: Dict[str, Any] = {}
|
|
175
|
+
|
|
176
|
+
def _caller_locals(self):
|
|
177
|
+
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
178
|
+
try:
|
|
179
|
+
raise ValueError
|
|
180
|
+
except ValueError:
|
|
181
|
+
_, _, tb = sys.exc_info()
|
|
182
|
+
assert tb, "Can't get traceback, this shouldn't happen"
|
|
183
|
+
caller_frame = tb.tb_frame
|
|
184
|
+
while caller_frame.f_locals.get("self", None) is self:
|
|
185
|
+
caller_frame = caller_frame.f_back
|
|
186
|
+
return caller_frame.f_locals
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> "DeviceCollector":
|
|
189
|
+
# Stash the names that were defined before we were called
|
|
190
|
+
self._names_on_enter = set(self._caller_locals())
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
async def __aenter__(self) -> "DeviceCollector":
|
|
194
|
+
return self.__enter__()
|
|
195
|
+
|
|
196
|
+
async def _on_exit(self) -> None:
|
|
197
|
+
# Name and kick off connect for devices
|
|
198
|
+
connect_coroutines: Dict[str, Coroutine] = {}
|
|
199
|
+
for name, obj in self._objects_on_exit.items():
|
|
200
|
+
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
201
|
+
if self._set_name and not obj.name:
|
|
202
|
+
obj.set_name(name)
|
|
203
|
+
if self._connect:
|
|
204
|
+
connect_coroutines[name] = obj.connect(
|
|
205
|
+
self._mock, timeout=self._timeout
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Connect to all the devices
|
|
209
|
+
if connect_coroutines:
|
|
210
|
+
await wait_for_connection(**connect_coroutines)
|
|
211
|
+
|
|
212
|
+
async def __aexit__(self, type, value, traceback):
|
|
213
|
+
self._objects_on_exit = self._caller_locals()
|
|
214
|
+
await self._on_exit()
|
|
215
|
+
|
|
216
|
+
def __exit__(self, type_, value, traceback):
|
|
217
|
+
self._objects_on_exit = self._caller_locals()
|
|
218
|
+
try:
|
|
219
|
+
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
220
|
+
except RuntimeError:
|
|
221
|
+
raise NotConnected(
|
|
222
|
+
"Could not connect devices. Is the bluesky event loop running? See "
|
|
223
|
+
"https://blueskyproject.io/ophyd-async/main/"
|
|
224
|
+
"user/explanations/event-loop-choice.html for more info."
|
|
225
|
+
)
|
|
226
|
+
return fut
|