ophyd-async 0.9.0a1__py3-none-any.whl → 0.9.0a2__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/_version.py +1 -1
- ophyd_async/core/__init__.py +13 -20
- ophyd_async/core/_detector.py +61 -37
- ophyd_async/core/_device.py +102 -80
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +55 -17
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +18 -10
- ophyd_async/core/_utils.py +5 -3
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +35 -10
- ophyd_async/epics/adcore/_core_logic.py +98 -86
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +38 -62
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +4 -3
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +27 -39
- ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +10 -24
- ophyd_async/epics/core/_aioca.py +31 -14
- ophyd_async/epics/core/_p4p.py +40 -16
- ophyd_async/epics/core/_util.py +1 -1
- ophyd_async/epics/motor.py +18 -10
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +10 -4
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +48 -65
- ophyd_async/epics/testing/_utils.py +17 -45
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_control.py +7 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +1 -1
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/tango/core/_signal.py +3 -1
- ophyd_async/tango/core/_tango_transport.py +12 -14
- ophyd_async/tango/{demo → sim}/_mover.py +5 -2
- ophyd_async/testing/__init__.py +19 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +88 -40
- ophyd_async/testing/_mock_signal_utils.py +3 -3
- ophyd_async/testing/_one_of_everything.py +126 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
- ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
from ._detector import (
|
|
2
2
|
DetectorController,
|
|
3
|
+
DetectorControllerT,
|
|
3
4
|
DetectorTrigger,
|
|
4
5
|
DetectorWriter,
|
|
5
6
|
StandardDetector,
|
|
6
7
|
TriggerInfo,
|
|
7
8
|
)
|
|
8
|
-
from ._device import Device,
|
|
9
|
+
from ._device import Device, DeviceConnector, DeviceVector, init_devices
|
|
9
10
|
from ._device_filler import DeviceFiller
|
|
10
|
-
from ._device_save_loader import (
|
|
11
|
-
all_at_once,
|
|
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
11
|
from ._flyer import FlyerController, StandardFlyer
|
|
21
12
|
from ._hdf_dataset import HDFDataset, HDFFile
|
|
22
13
|
from ._log import config_ophyd_async_logging
|
|
@@ -41,6 +32,7 @@ from ._readable import (
|
|
|
41
32
|
StandardReadable,
|
|
42
33
|
StandardReadableFormat,
|
|
43
34
|
)
|
|
35
|
+
from ._settings import Settings, SettingsProvider
|
|
44
36
|
from ._signal import (
|
|
45
37
|
Signal,
|
|
46
38
|
SignalConnector,
|
|
@@ -55,9 +47,11 @@ from ._signal import (
|
|
|
55
47
|
soft_signal_r_and_setter,
|
|
56
48
|
soft_signal_rw,
|
|
57
49
|
wait_for_value,
|
|
50
|
+
walk_rw_signals,
|
|
58
51
|
)
|
|
59
52
|
from ._signal_backend import (
|
|
60
53
|
Array1D,
|
|
54
|
+
DTypeScalar_co,
|
|
61
55
|
SignalBackend,
|
|
62
56
|
SignalDatatype,
|
|
63
57
|
SignalDatatypeT,
|
|
@@ -84,26 +78,20 @@ from ._utils import (
|
|
|
84
78
|
in_micros,
|
|
85
79
|
wait_for_connection,
|
|
86
80
|
)
|
|
81
|
+
from ._yaml_settings import YamlSettingsProvider
|
|
87
82
|
|
|
88
83
|
__all__ = [
|
|
89
84
|
"DetectorController",
|
|
85
|
+
"DetectorControllerT",
|
|
90
86
|
"DetectorTrigger",
|
|
91
87
|
"DetectorWriter",
|
|
92
88
|
"StandardDetector",
|
|
93
89
|
"TriggerInfo",
|
|
94
90
|
"Device",
|
|
95
91
|
"DeviceConnector",
|
|
96
|
-
"
|
|
92
|
+
"init_devices",
|
|
97
93
|
"DeviceVector",
|
|
98
94
|
"DeviceFiller",
|
|
99
|
-
"all_at_once",
|
|
100
|
-
"get_signal_values",
|
|
101
|
-
"load_device",
|
|
102
|
-
"load_from_yaml",
|
|
103
|
-
"save_device",
|
|
104
|
-
"save_to_yaml",
|
|
105
|
-
"set_signal_values",
|
|
106
|
-
"walk_rw_signals",
|
|
107
95
|
"StandardFlyer",
|
|
108
96
|
"FlyerController",
|
|
109
97
|
"HDFDataset",
|
|
@@ -128,6 +116,8 @@ __all__ = [
|
|
|
128
116
|
"HintedSignal",
|
|
129
117
|
"StandardReadable",
|
|
130
118
|
"StandardReadableFormat",
|
|
119
|
+
"Settings",
|
|
120
|
+
"SettingsProvider",
|
|
131
121
|
"Signal",
|
|
132
122
|
"SignalConnector",
|
|
133
123
|
"SignalR",
|
|
@@ -141,7 +131,9 @@ __all__ = [
|
|
|
141
131
|
"soft_signal_r_and_setter",
|
|
142
132
|
"soft_signal_rw",
|
|
143
133
|
"wait_for_value",
|
|
134
|
+
"walk_rw_signals",
|
|
144
135
|
"Array1D",
|
|
136
|
+
"DTypeScalar_co",
|
|
145
137
|
"SignalBackend",
|
|
146
138
|
"make_datakey",
|
|
147
139
|
"StrictEnum",
|
|
@@ -168,4 +160,5 @@ __all__ = [
|
|
|
168
160
|
"in_micros",
|
|
169
161
|
"wait_for_connection",
|
|
170
162
|
"completed_status",
|
|
163
|
+
"YamlSettingsProvider",
|
|
171
164
|
]
|
ophyd_async/core/_detector.py
CHANGED
|
@@ -5,10 +5,15 @@ import time
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
|
|
7
7
|
from functools import cached_property
|
|
8
|
+
from typing import (
|
|
9
|
+
Generic,
|
|
10
|
+
TypeVar,
|
|
11
|
+
)
|
|
8
12
|
|
|
9
13
|
from bluesky.protocols import (
|
|
10
14
|
Collectable,
|
|
11
15
|
Flyable,
|
|
16
|
+
Hints,
|
|
12
17
|
Preparable,
|
|
13
18
|
Reading,
|
|
14
19
|
Stageable,
|
|
@@ -87,7 +92,7 @@ class DetectorController(ABC):
|
|
|
87
92
|
"""For a given exposure, how long should the time between exposures be"""
|
|
88
93
|
|
|
89
94
|
@abstractmethod
|
|
90
|
-
async def prepare(self, trigger_info: TriggerInfo):
|
|
95
|
+
async def prepare(self, trigger_info: TriggerInfo) -> None:
|
|
91
96
|
"""
|
|
92
97
|
Do all necessary steps to prepare the detector for triggers.
|
|
93
98
|
|
|
@@ -157,6 +162,23 @@ class DetectorWriter(ABC):
|
|
|
157
162
|
async def close(self) -> None:
|
|
158
163
|
"""Close writer, blocks until I/O is complete"""
|
|
159
164
|
|
|
165
|
+
@property
|
|
166
|
+
def hints(self) -> Hints:
|
|
167
|
+
return {}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Add type var for controller so we can define
|
|
171
|
+
# StandardDetector[KinetixController, ADWriter] for example
|
|
172
|
+
DetectorControllerT = TypeVar("DetectorControllerT", bound=DetectorController)
|
|
173
|
+
DetectorWriterT = TypeVar("DetectorWriterT", bound=DetectorWriter)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _ensure_trigger_info_exists(trigger_info: TriggerInfo | None) -> TriggerInfo:
|
|
177
|
+
# make absolute sure we realy have a valid TriggerInfo ... mostly for pylance
|
|
178
|
+
if trigger_info is None:
|
|
179
|
+
raise RuntimeError("Trigger info must be set before calling this method.")
|
|
180
|
+
return trigger_info
|
|
181
|
+
|
|
160
182
|
|
|
161
183
|
class StandardDetector(
|
|
162
184
|
Device,
|
|
@@ -168,6 +190,7 @@ class StandardDetector(
|
|
|
168
190
|
Flyable,
|
|
169
191
|
Collectable,
|
|
170
192
|
WritesStreamAssets,
|
|
193
|
+
Generic[DetectorControllerT, DetectorWriterT],
|
|
171
194
|
):
|
|
172
195
|
"""
|
|
173
196
|
Useful detector base class for step and fly scanning detectors.
|
|
@@ -176,8 +199,8 @@ class StandardDetector(
|
|
|
176
199
|
|
|
177
200
|
def __init__(
|
|
178
201
|
self,
|
|
179
|
-
controller:
|
|
180
|
-
writer:
|
|
202
|
+
controller: DetectorControllerT,
|
|
203
|
+
writer: DetectorWriterT,
|
|
181
204
|
config_sigs: Sequence[SignalR] = (),
|
|
182
205
|
name: str = "",
|
|
183
206
|
connector: DeviceConnector | None = None,
|
|
@@ -211,19 +234,11 @@ class StandardDetector(
|
|
|
211
234
|
self._initial_frame: int = 0
|
|
212
235
|
super().__init__(name, connector=connector)
|
|
213
236
|
|
|
214
|
-
@property
|
|
215
|
-
def controller(self) -> DetectorController:
|
|
216
|
-
return self._controller
|
|
217
|
-
|
|
218
|
-
@property
|
|
219
|
-
def writer(self) -> DetectorWriter:
|
|
220
|
-
return self._writer
|
|
221
|
-
|
|
222
237
|
@AsyncStatus.wrap
|
|
223
238
|
async def stage(self) -> None:
|
|
224
239
|
# Disarm the detector, stop file writing.
|
|
225
240
|
await self._check_config_sigs()
|
|
226
|
-
await asyncio.gather(self.
|
|
241
|
+
await asyncio.gather(self._writer.close(), self._controller.disarm())
|
|
227
242
|
self._trigger_info = None
|
|
228
243
|
|
|
229
244
|
async def _check_config_sigs(self):
|
|
@@ -244,7 +259,7 @@ class StandardDetector(
|
|
|
244
259
|
@AsyncStatus.wrap
|
|
245
260
|
async def unstage(self) -> None:
|
|
246
261
|
# Stop data writing.
|
|
247
|
-
await asyncio.gather(self.
|
|
262
|
+
await asyncio.gather(self._writer.close(), self._controller.disarm())
|
|
248
263
|
|
|
249
264
|
async def read_configuration(self) -> dict[str, Reading]:
|
|
250
265
|
return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
|
|
@@ -271,15 +286,19 @@ class StandardDetector(
|
|
|
271
286
|
frame_timeout=None,
|
|
272
287
|
)
|
|
273
288
|
)
|
|
274
|
-
|
|
275
|
-
|
|
289
|
+
|
|
290
|
+
self._trigger_info = _ensure_trigger_info_exists(self._trigger_info)
|
|
291
|
+
if self._trigger_info.trigger is not DetectorTrigger.INTERNAL:
|
|
292
|
+
msg = "The trigger method can only be called with INTERNAL triggering"
|
|
293
|
+
raise ValueError(msg)
|
|
294
|
+
|
|
276
295
|
# Arm the detector and wait for it to finish.
|
|
277
|
-
indices_written = await self.
|
|
278
|
-
await self.
|
|
279
|
-
await self.
|
|
296
|
+
indices_written = await self._writer.get_indices_written()
|
|
297
|
+
await self._controller.arm()
|
|
298
|
+
await self._controller.wait_for_idle()
|
|
280
299
|
end_observation = indices_written + 1
|
|
281
300
|
|
|
282
|
-
async for index in self.
|
|
301
|
+
async for index in self._writer.observe_indices_written(
|
|
283
302
|
DEFAULT_TIMEOUT
|
|
284
303
|
+ (self._trigger_info.livetime or 0)
|
|
285
304
|
+ (self._trigger_info.deadtime or 0)
|
|
@@ -303,28 +322,29 @@ class StandardDetector(
|
|
|
303
322
|
Args:
|
|
304
323
|
value: TriggerInfo describing how to trigger the detector
|
|
305
324
|
"""
|
|
306
|
-
if value.trigger != DetectorTrigger.INTERNAL:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if value.deadtime:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
f"
|
|
314
|
-
f"but trigger logic provides only {value.deadtime}s"
|
|
325
|
+
if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
|
|
326
|
+
msg = "Deadtime must be supplied when in externally triggered mode"
|
|
327
|
+
raise ValueError(msg)
|
|
328
|
+
required_deadtime = self._controller.get_deadtime(value.livetime)
|
|
329
|
+
if value.deadtime and required_deadtime > value.deadtime:
|
|
330
|
+
msg = (
|
|
331
|
+
f"Detector {self._controller} needs at least {required_deadtime}s "
|
|
332
|
+
f"deadtime, but trigger logic provides only {value.deadtime}s"
|
|
315
333
|
)
|
|
334
|
+
raise ValueError(msg)
|
|
335
|
+
|
|
316
336
|
self._trigger_info = value
|
|
317
337
|
self._number_of_triggers_iter = iter(
|
|
318
338
|
self._trigger_info.number_of_triggers
|
|
319
339
|
if isinstance(self._trigger_info.number_of_triggers, list)
|
|
320
340
|
else [self._trigger_info.number_of_triggers]
|
|
321
341
|
)
|
|
322
|
-
self._initial_frame = await self.
|
|
342
|
+
self._initial_frame = await self._writer.get_indices_written()
|
|
323
343
|
self._describe, _ = await asyncio.gather(
|
|
324
|
-
self.
|
|
344
|
+
self._writer.open(value.multiplier), self._controller.prepare(value)
|
|
325
345
|
)
|
|
326
346
|
if value.trigger != DetectorTrigger.INTERNAL:
|
|
327
|
-
await self.
|
|
347
|
+
await self._controller.arm()
|
|
328
348
|
self._fly_start = time.monotonic()
|
|
329
349
|
|
|
330
350
|
@AsyncStatus.wrap
|
|
@@ -342,8 +362,8 @@ class StandardDetector(
|
|
|
342
362
|
|
|
343
363
|
@WatchableAsyncStatus.wrap
|
|
344
364
|
async def complete(self):
|
|
345
|
-
|
|
346
|
-
indices_written = self.
|
|
365
|
+
self._trigger_info = _ensure_trigger_info_exists(self._trigger_info)
|
|
366
|
+
indices_written = self._writer.observe_indices_written(
|
|
347
367
|
self._trigger_info.frame_timeout
|
|
348
368
|
or (
|
|
349
369
|
DEFAULT_TIMEOUT
|
|
@@ -372,7 +392,7 @@ class StandardDetector(
|
|
|
372
392
|
self._completable_frames = 0
|
|
373
393
|
self._frames_to_complete = 0
|
|
374
394
|
self._number_of_triggers_iter = None
|
|
375
|
-
await self.
|
|
395
|
+
await self._controller.wait_for_idle()
|
|
376
396
|
|
|
377
397
|
async def describe_collect(self) -> dict[str, DataKey]:
|
|
378
398
|
return self._describe
|
|
@@ -384,9 +404,13 @@ class StandardDetector(
|
|
|
384
404
|
# The index is optional, and provided for fly scans, however this needs to be
|
|
385
405
|
# retrieved for step scans.
|
|
386
406
|
if index is None:
|
|
387
|
-
index = await self.
|
|
388
|
-
async for doc in self.
|
|
407
|
+
index = await self._writer.get_indices_written()
|
|
408
|
+
async for doc in self._writer.collect_stream_docs(index):
|
|
389
409
|
yield doc
|
|
390
410
|
|
|
391
411
|
async def get_index(self) -> int:
|
|
392
|
-
return await self.
|
|
412
|
+
return await self._writer.get_indices_written()
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def hints(self) -> Hints:
|
|
416
|
+
return self._writer.hints
|
ophyd_async/core/_device.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
-
from collections.abc import
|
|
5
|
+
from collections.abc import Awaitable, Callable, Iterator, Mapping, MutableMapping
|
|
6
6
|
from functools import cached_property
|
|
7
7
|
from logging import LoggerAdapter, getLogger
|
|
8
8
|
from typing import Any, TypeVar
|
|
@@ -158,10 +158,12 @@ class Device(HasName):
|
|
|
158
158
|
timeout:
|
|
159
159
|
Time to wait before failing with a TimeoutError.
|
|
160
160
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
if not hasattr(self, "_connector"):
|
|
162
|
+
msg = (
|
|
163
|
+
f"{self}: doesn't have attribute `_connector`,"
|
|
164
|
+
" did you call `super().__init__` in your `__init__` method?"
|
|
165
|
+
)
|
|
166
|
+
raise RuntimeError(msg)
|
|
165
167
|
if mock:
|
|
166
168
|
# Always connect in mock mode serially
|
|
167
169
|
if isinstance(mock, LazyMock):
|
|
@@ -182,7 +184,9 @@ class Device(HasName):
|
|
|
182
184
|
self._mock = None
|
|
183
185
|
coro = self._connector.connect_real(self, timeout, force_reconnect)
|
|
184
186
|
self._connect_task = asyncio.create_task(coro)
|
|
185
|
-
|
|
187
|
+
if not self._connect_task:
|
|
188
|
+
msg = "Connect task not created, this shouldn't happen"
|
|
189
|
+
raise RuntimeError(msg)
|
|
186
190
|
# Wait for it to complete
|
|
187
191
|
await self._connect_task
|
|
188
192
|
|
|
@@ -206,7 +210,7 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
206
210
|
|
|
207
211
|
In the below example, foos becomes a dictionary on the parent device
|
|
208
212
|
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
209
|
-
:class:`~ophyd_async.epics.
|
|
213
|
+
:class:`~ophyd_async.epics.sim.DynamicSensorGroup`
|
|
210
214
|
"""
|
|
211
215
|
|
|
212
216
|
def __init__(
|
|
@@ -232,8 +236,12 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
232
236
|
def __setitem__(self, key: int, value: DeviceT) -> None:
|
|
233
237
|
# Check the types on entry to dict to make sure we can't accidentally
|
|
234
238
|
# make a non-integer named child
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
if not isinstance(key, int):
|
|
240
|
+
msg = f"Expected int, got {key}"
|
|
241
|
+
raise TypeError(msg)
|
|
242
|
+
if not isinstance(value, Device):
|
|
243
|
+
msg = f"Expected Device, got {value}"
|
|
244
|
+
raise TypeError(msg)
|
|
237
245
|
self._children[key] = value
|
|
238
246
|
value.parent = self
|
|
239
247
|
|
|
@@ -254,94 +262,49 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
254
262
|
return hash(id(self))
|
|
255
263
|
|
|
256
264
|
|
|
257
|
-
class
|
|
258
|
-
"""
|
|
259
|
-
|
|
260
|
-
Parameters
|
|
261
|
-
----------
|
|
262
|
-
set_name:
|
|
263
|
-
If True, call ``device.set_name(variable_name)`` on all collected
|
|
264
|
-
Devices
|
|
265
|
-
child_name_separator:
|
|
266
|
-
Use this as a separator if we call ``set_name``.
|
|
267
|
-
connect:
|
|
268
|
-
If True, call ``device.connect(mock)`` in parallel on all
|
|
269
|
-
collected Devices
|
|
270
|
-
mock:
|
|
271
|
-
If True, connect Signals in simulation mode
|
|
272
|
-
timeout:
|
|
273
|
-
How long to wait for connect before logging an exception
|
|
274
|
-
|
|
275
|
-
Notes
|
|
276
|
-
-----
|
|
277
|
-
Example usage::
|
|
278
|
-
|
|
279
|
-
[async] with DeviceCollector():
|
|
280
|
-
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
281
|
-
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
282
|
-
# Names and connects devices here
|
|
283
|
-
assert t1x.comm.velocity.source
|
|
284
|
-
assert t1x.name == "t1x"
|
|
265
|
+
class DeviceProcessor:
|
|
266
|
+
"""Sync/Async Context Manager that finds all the Devices declared within it.
|
|
285
267
|
|
|
268
|
+
Used in `init_devices`
|
|
286
269
|
"""
|
|
287
270
|
|
|
288
|
-
def __init__(
|
|
289
|
-
self
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
timeout: float = 10.0,
|
|
295
|
-
):
|
|
296
|
-
self._set_name = set_name
|
|
297
|
-
self._child_name_separator = child_name_separator
|
|
298
|
-
self._connect = connect
|
|
299
|
-
self._mock = mock
|
|
300
|
-
self._timeout = timeout
|
|
301
|
-
self._names_on_enter: set[str] = set()
|
|
302
|
-
self._objects_on_exit: dict[str, Any] = {}
|
|
303
|
-
|
|
304
|
-
def _caller_locals(self):
|
|
271
|
+
def __init__(self, process_devices: Callable[[dict[str, Device]], Awaitable[None]]):
|
|
272
|
+
self._process_devices = process_devices
|
|
273
|
+
self._locals_on_enter: dict[str, Any] = {}
|
|
274
|
+
self._locals_on_exit: dict[str, Any] = {}
|
|
275
|
+
|
|
276
|
+
def _caller_locals(self) -> dict[str, Any]:
|
|
305
277
|
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
306
278
|
try:
|
|
307
279
|
raise ValueError
|
|
308
280
|
except ValueError:
|
|
309
281
|
_, _, tb = sys.exc_info()
|
|
310
|
-
|
|
282
|
+
if not tb:
|
|
283
|
+
msg = "Can't get traceback, this shouldn't happen"
|
|
284
|
+
raise RuntimeError(msg) # noqa: B904
|
|
311
285
|
caller_frame = tb.tb_frame
|
|
312
286
|
while caller_frame.f_locals.get("self", None) is self:
|
|
313
287
|
caller_frame = caller_frame.f_back
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
288
|
+
if not caller_frame:
|
|
289
|
+
msg = (
|
|
290
|
+
"No previous frame to the one with self in it, "
|
|
291
|
+
"this shouldn't happen"
|
|
292
|
+
)
|
|
293
|
+
raise RuntimeError( # noqa: B904
|
|
294
|
+
msg
|
|
295
|
+
)
|
|
296
|
+
return caller_frame.f_locals.copy()
|
|
318
297
|
|
|
319
|
-
def __enter__(self) ->
|
|
298
|
+
def __enter__(self) -> DeviceProcessor:
|
|
320
299
|
# Stash the names that were defined before we were called
|
|
321
|
-
self.
|
|
300
|
+
self._locals_on_enter = self._caller_locals()
|
|
322
301
|
return self
|
|
323
302
|
|
|
324
|
-
async def __aenter__(self) ->
|
|
303
|
+
async def __aenter__(self) -> DeviceProcessor:
|
|
325
304
|
return self.__enter__()
|
|
326
305
|
|
|
327
|
-
async def _on_exit(self) -> None:
|
|
328
|
-
# Name and kick off connect for devices
|
|
329
|
-
connect_coroutines: dict[str, Coroutine] = {}
|
|
330
|
-
for name, obj in self._objects_on_exit.items():
|
|
331
|
-
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
332
|
-
if self._set_name and not obj.name:
|
|
333
|
-
obj.set_name(name, child_name_separator=self._child_name_separator)
|
|
334
|
-
if self._connect:
|
|
335
|
-
connect_coroutines[name] = obj.connect(
|
|
336
|
-
self._mock, timeout=self._timeout
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
# Connect to all the devices
|
|
340
|
-
if connect_coroutines:
|
|
341
|
-
await wait_for_connection(**connect_coroutines)
|
|
342
|
-
|
|
343
306
|
async def __aexit__(self, type, value, traceback):
|
|
344
|
-
self.
|
|
307
|
+
self._locals_on_exit = self._caller_locals()
|
|
345
308
|
await self._on_exit()
|
|
346
309
|
|
|
347
310
|
def __exit__(self, type_, value, traceback):
|
|
@@ -350,7 +313,7 @@ class DeviceCollector:
|
|
|
350
313
|
"Cannot use DeviceConnector inside a plan, instead use "
|
|
351
314
|
"`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
|
|
352
315
|
)
|
|
353
|
-
self.
|
|
316
|
+
self._locals_on_exit = self._caller_locals()
|
|
354
317
|
try:
|
|
355
318
|
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
356
319
|
except RuntimeError as e:
|
|
@@ -360,3 +323,62 @@ class DeviceCollector:
|
|
|
360
323
|
"user/explanations/event-loop-choice.html for more info."
|
|
361
324
|
) from e
|
|
362
325
|
return fut
|
|
326
|
+
|
|
327
|
+
async def _on_exit(self) -> None:
|
|
328
|
+
# Find all the devices
|
|
329
|
+
devices = {
|
|
330
|
+
name: obj
|
|
331
|
+
for name, obj in self._locals_on_exit.items()
|
|
332
|
+
if isinstance(obj, Device) and self._locals_on_enter.get(name) is not obj
|
|
333
|
+
}
|
|
334
|
+
# Call the provided process function on them
|
|
335
|
+
await self._process_devices(devices)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def init_devices(
|
|
339
|
+
set_name=True,
|
|
340
|
+
child_name_separator: str = "-",
|
|
341
|
+
connect=True,
|
|
342
|
+
mock=False,
|
|
343
|
+
timeout: float = 10.0,
|
|
344
|
+
) -> DeviceProcessor:
|
|
345
|
+
"""Auto initialise top level Device instances to be used as a context manager
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
set_name:
|
|
350
|
+
If True, call ``device.set_name(variable_name)`` on all Devices
|
|
351
|
+
created within the context manager that have an empty ``name``
|
|
352
|
+
child_name_separator:
|
|
353
|
+
Use this as a separator if we call ``set_name``.
|
|
354
|
+
connect:
|
|
355
|
+
If True, call ``device.connect(mock, timeout)`` in parallel on all
|
|
356
|
+
Devices created within the context manager
|
|
357
|
+
mock:
|
|
358
|
+
If True, connect Signals in mock mode
|
|
359
|
+
timeout:
|
|
360
|
+
How long to wait for connect before logging an exception
|
|
361
|
+
|
|
362
|
+
Notes
|
|
363
|
+
-----
|
|
364
|
+
Example usage::
|
|
365
|
+
|
|
366
|
+
[async] with init_devices():
|
|
367
|
+
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
368
|
+
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
369
|
+
# Names and connects devices here
|
|
370
|
+
assert t1x.name == "t1x"
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
async def process_devices(devices: dict[str, Device]):
|
|
374
|
+
if set_name:
|
|
375
|
+
for name, device in devices.items():
|
|
376
|
+
if not device.name:
|
|
377
|
+
device.set_name(name, child_name_separator=child_name_separator)
|
|
378
|
+
if connect:
|
|
379
|
+
coros = {
|
|
380
|
+
name: device.connect(mock, timeout) for name, device in devices.items()
|
|
381
|
+
}
|
|
382
|
+
await wait_for_connection(**coros)
|
|
383
|
+
|
|
384
|
+
return DeviceProcessor(process_devices)
|
|
@@ -39,6 +39,13 @@ def _logical(name: UniqueName) -> LogicalName:
|
|
|
39
39
|
return LogicalName(name.rstrip("_"))
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def _check_device_annotation(annotation: Any) -> DeviceAnnotation:
|
|
43
|
+
if not isinstance(annotation, DeviceAnnotation):
|
|
44
|
+
msg = f"Annotation {annotation} is not a DeviceAnnotation"
|
|
45
|
+
raise TypeError(msg)
|
|
46
|
+
return annotation
|
|
47
|
+
|
|
48
|
+
|
|
42
49
|
@runtime_checkable
|
|
43
50
|
class DeviceAnnotation(Protocol):
|
|
44
51
|
@abstractmethod
|
|
@@ -150,8 +157,8 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
150
157
|
yield backend, extras
|
|
151
158
|
signal = child_type(backend)
|
|
152
159
|
for anno in extras:
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
device_annotation = _check_device_annotation(annotation=anno)
|
|
161
|
+
device_annotation(self._device, signal)
|
|
155
162
|
setattr(self._device, name, signal)
|
|
156
163
|
dest = self._filled_backends if filled else self._unfilled_backends
|
|
157
164
|
dest[_logical(name)] = (backend, child_type)
|
|
@@ -167,15 +174,17 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
167
174
|
yield connector, extras
|
|
168
175
|
device = child_type(connector=connector)
|
|
169
176
|
for anno in extras:
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
device_annotation = _check_device_annotation(annotation=anno)
|
|
178
|
+
device_annotation(self._device, device)
|
|
172
179
|
setattr(self._device, name, device)
|
|
173
180
|
dest = self._filled_connectors if filled else self._unfilled_connectors
|
|
174
181
|
dest[_logical(name)] = connector
|
|
175
182
|
|
|
176
183
|
def create_device_vector_entries_to_mock(self, num: int):
|
|
177
184
|
for name, cls in self._vector_device_type.items():
|
|
178
|
-
|
|
185
|
+
if not cls:
|
|
186
|
+
msg = "Malformed device vector"
|
|
187
|
+
raise TypeError(msg)
|
|
179
188
|
for i in range(1, num + 1):
|
|
180
189
|
if issubclass(cls, Signal):
|
|
181
190
|
self.fill_child_signal(name, cls, i)
|
|
@@ -254,9 +263,9 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
254
263
|
# We need to add a new entry to a DeviceVector
|
|
255
264
|
vector = self._ensure_device_vector(name)
|
|
256
265
|
vector_device_type = self._vector_device_type[name] or device_type
|
|
257
|
-
|
|
258
|
-
vector_device_type
|
|
259
|
-
|
|
266
|
+
if not issubclass(vector_device_type, Device):
|
|
267
|
+
msg = f"{vector_device_type} is not a Device"
|
|
268
|
+
raise TypeError(msg)
|
|
260
269
|
connector = self._device_connector_factory()
|
|
261
270
|
vector[vector_index] = vector_device_type(connector=connector)
|
|
262
271
|
elif child := getattr(self._device, name, None):
|
ophyd_async/core/_flyer.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Generic
|
|
2
|
+
from typing import Any, Generic
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Flyable, Preparable, Stageable
|
|
5
5
|
|
|
@@ -10,7 +10,7 @@ from ._utils import T
|
|
|
10
10
|
|
|
11
11
|
class FlyerController(ABC, Generic[T]):
|
|
12
12
|
@abstractmethod
|
|
13
|
-
async def prepare(self, value: T):
|
|
13
|
+
async def prepare(self, value: T) -> Any:
|
|
14
14
|
"""Move to the start of the flyscan"""
|
|
15
15
|
|
|
16
16
|
@abstractmethod
|