ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- 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-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
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
|
|
@@ -17,7 +17,7 @@ class DeviceConnector:
|
|
|
17
17
|
"""Defines how a `Device` should be connected and type hints processed."""
|
|
18
18
|
|
|
19
19
|
def create_children_from_annotations(self, device: Device):
|
|
20
|
-
"""
|
|
20
|
+
"""Use when children can be created from introspecting the hardware.
|
|
21
21
|
|
|
22
22
|
Some control systems allow introspection of a device to determine what
|
|
23
23
|
children it has. To allow this to work nicely with typing we add these
|
|
@@ -26,15 +26,20 @@ class DeviceConnector:
|
|
|
26
26
|
my_signal: SignalRW[int]
|
|
27
27
|
my_device: MyDevice
|
|
28
28
|
|
|
29
|
-
This method will be run during
|
|
29
|
+
This method will be run during `Device.__init__`, and is responsible
|
|
30
30
|
for turning all of those type hints into real Signal and Device instances.
|
|
31
31
|
|
|
32
32
|
Subsequent runs of this function should do nothing, to allow it to be
|
|
33
33
|
called early in Devices that need to pass references to their children
|
|
34
|
-
during
|
|
34
|
+
during `__init__`.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
37
|
async def connect_mock(self, device: Device, mock: LazyMock):
|
|
38
|
+
"""Use during [](#Device.connect) with `mock=True`.
|
|
39
|
+
|
|
40
|
+
This is called when there is no cached connect done in `mock=True`
|
|
41
|
+
mode. It connects the Device and all its children in mock mode.
|
|
42
|
+
"""
|
|
38
43
|
# Connect serially, no errors to gather up as in mock mode
|
|
39
44
|
exceptions: dict[str, Exception] = {}
|
|
40
45
|
for name, child_device in device.children():
|
|
@@ -46,11 +51,10 @@ class DeviceConnector:
|
|
|
46
51
|
raise NotConnected.with_other_exceptions_logged(exceptions)
|
|
47
52
|
|
|
48
53
|
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
49
|
-
"""
|
|
54
|
+
"""Use during [](#Device.connect) with `mock=False`.
|
|
50
55
|
|
|
51
|
-
This is called when
|
|
52
|
-
|
|
53
|
-
children.
|
|
56
|
+
This is called when there is no cached connect done in `mock=False`
|
|
57
|
+
mode. It connects the Device and all its children in real mode in parallel.
|
|
54
58
|
"""
|
|
55
59
|
# Connect in parallel, gathering up NotConnected errors
|
|
56
60
|
coros = {
|
|
@@ -61,11 +65,15 @@ class DeviceConnector:
|
|
|
61
65
|
|
|
62
66
|
|
|
63
67
|
class Device(HasName):
|
|
64
|
-
"""Common base class for all Ophyd Async Devices.
|
|
68
|
+
"""Common base class for all Ophyd Async Devices.
|
|
69
|
+
|
|
70
|
+
:param name: Optional name of the Device
|
|
71
|
+
:param connector: Optional DeviceConnector instance to use at connect()
|
|
72
|
+
"""
|
|
65
73
|
|
|
66
|
-
_name: str = ""
|
|
67
|
-
#: The parent Device if it exists
|
|
68
74
|
parent: Device | None = None
|
|
75
|
+
"""The parent Device if it exists"""
|
|
76
|
+
_name: str = ""
|
|
69
77
|
# None if connect hasn't started, a Task if it has
|
|
70
78
|
_connect_task: asyncio.Task | None = None
|
|
71
79
|
# The mock if we have connected in mock mode
|
|
@@ -83,7 +91,7 @@ class Device(HasName):
|
|
|
83
91
|
|
|
84
92
|
@property
|
|
85
93
|
def name(self) -> str:
|
|
86
|
-
"""Return the name of the Device"""
|
|
94
|
+
"""Return the name of the Device."""
|
|
87
95
|
return self._name
|
|
88
96
|
|
|
89
97
|
@cached_property
|
|
@@ -91,24 +99,26 @@ class Device(HasName):
|
|
|
91
99
|
return {}
|
|
92
100
|
|
|
93
101
|
def children(self) -> Iterator[tuple[str, Device]]:
|
|
102
|
+
"""For each attribute that is a Device, yield the name and Device.
|
|
103
|
+
|
|
104
|
+
:yields: `(attr_name, attr)` for each child attribute that is a Device.
|
|
105
|
+
"""
|
|
94
106
|
yield from self._child_devices.items()
|
|
95
107
|
|
|
96
108
|
@cached_property
|
|
97
109
|
def log(self) -> LoggerAdapter:
|
|
110
|
+
"""Return a logger configured with the device name."""
|
|
98
111
|
return LoggerAdapter(
|
|
99
112
|
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
100
113
|
)
|
|
101
114
|
|
|
102
115
|
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
103
|
-
"""Set
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
child_name_separator:
|
|
110
|
-
Use this as a separator instead of "-". Use "_" instead to make the same
|
|
111
|
-
names as the equivalent ophyd sync device.
|
|
116
|
+
"""Set `self.name=name` and each `self.child.name=name+"-child"`.
|
|
117
|
+
|
|
118
|
+
:param name: New name to set.
|
|
119
|
+
:param child_name_separator:
|
|
120
|
+
Use this as a separator instead of "-". Use "_" instead to make the
|
|
121
|
+
same names as the equivalent ophyd sync device.
|
|
112
122
|
"""
|
|
113
123
|
self._name = name
|
|
114
124
|
if child_name_separator:
|
|
@@ -147,21 +157,26 @@ class Device(HasName):
|
|
|
147
157
|
timeout: float = DEFAULT_TIMEOUT,
|
|
148
158
|
force_reconnect: bool = False,
|
|
149
159
|
) -> None:
|
|
150
|
-
"""Connect
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
mock:
|
|
157
|
-
If True then use
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
"""Connect the device and all child devices.
|
|
161
|
+
|
|
162
|
+
Successful connects will be cached so subsequent calls will return
|
|
163
|
+
immediately. Contains a timeout that gets propagated to child.connect
|
|
164
|
+
methods.
|
|
165
|
+
|
|
166
|
+
:param mock:
|
|
167
|
+
If True then use [](#MockSignalBackend) for all Signals. If passed a
|
|
168
|
+
[](#LazyMock) then pass this down for use within the Signals,
|
|
169
|
+
otherwise create one.
|
|
170
|
+
:param timeout: Time to wait before failing with a TimeoutError.
|
|
171
|
+
:param force_reconnect:
|
|
172
|
+
If True, force a reconnect even if the last connect succeeded.
|
|
160
173
|
"""
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
if not hasattr(self, "_connector"):
|
|
175
|
+
msg = (
|
|
176
|
+
f"{self}: doesn't have attribute `_connector`,"
|
|
177
|
+
" did you call `super().__init__` in your `__init__` method?"
|
|
178
|
+
)
|
|
179
|
+
raise RuntimeError(msg)
|
|
165
180
|
if mock:
|
|
166
181
|
# Always connect in mock mode serially
|
|
167
182
|
if isinstance(mock, LazyMock):
|
|
@@ -182,7 +197,9 @@ class Device(HasName):
|
|
|
182
197
|
self._mock = None
|
|
183
198
|
coro = self._connector.connect_real(self, timeout, force_reconnect)
|
|
184
199
|
self._connect_task = asyncio.create_task(coro)
|
|
185
|
-
|
|
200
|
+
if not self._connect_task:
|
|
201
|
+
msg = "Connect task not created, this shouldn't happen"
|
|
202
|
+
raise RuntimeError(msg)
|
|
186
203
|
# Wait for it to complete
|
|
187
204
|
await self._connect_task
|
|
188
205
|
|
|
@@ -201,12 +218,9 @@ DeviceT = TypeVar("DeviceT", bound=Device)
|
|
|
201
218
|
|
|
202
219
|
|
|
203
220
|
class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
204
|
-
"""
|
|
205
|
-
Defines device components with indices.
|
|
221
|
+
"""Defines a dictionary of Device children with arbitrary integer keys.
|
|
206
222
|
|
|
207
|
-
|
|
208
|
-
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
209
|
-
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
|
|
223
|
+
:see-also: [](#implementing-devices) for examples of how to use this class.
|
|
210
224
|
"""
|
|
211
225
|
|
|
212
226
|
def __init__(
|
|
@@ -232,8 +246,12 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
232
246
|
def __setitem__(self, key: int, value: DeviceT) -> None:
|
|
233
247
|
# Check the types on entry to dict to make sure we can't accidentally
|
|
234
248
|
# make a non-integer named child
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
if not isinstance(key, int):
|
|
250
|
+
msg = f"Expected int, got {key}"
|
|
251
|
+
raise TypeError(msg)
|
|
252
|
+
if not isinstance(value, Device):
|
|
253
|
+
msg = f"Expected Device, got {value}"
|
|
254
|
+
raise TypeError(msg)
|
|
237
255
|
self._children[key] = value
|
|
238
256
|
value.parent = self
|
|
239
257
|
|
|
@@ -254,94 +272,49 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
254
272
|
return hash(id(self))
|
|
255
273
|
|
|
256
274
|
|
|
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"
|
|
275
|
+
class DeviceProcessor:
|
|
276
|
+
"""Sync/Async Context Manager that finds all the Devices declared within it.
|
|
285
277
|
|
|
278
|
+
Used in `init_devices`
|
|
286
279
|
"""
|
|
287
280
|
|
|
288
|
-
def __init__(
|
|
289
|
-
self
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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):
|
|
305
|
-
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
281
|
+
def __init__(self, process_devices: Callable[[dict[str, Device]], Awaitable[None]]):
|
|
282
|
+
self._process_devices = process_devices
|
|
283
|
+
self._locals_on_enter: dict[str, Any] = {}
|
|
284
|
+
self._locals_on_exit: dict[str, Any] = {}
|
|
285
|
+
|
|
286
|
+
def _caller_locals(self) -> dict[str, Any]:
|
|
287
|
+
"""Walk up until we find a stack frame that doesn't have us as self."""
|
|
306
288
|
try:
|
|
307
289
|
raise ValueError
|
|
308
290
|
except ValueError:
|
|
309
291
|
_, _, tb = sys.exc_info()
|
|
310
|
-
|
|
292
|
+
if not tb:
|
|
293
|
+
msg = "Can't get traceback, this shouldn't happen"
|
|
294
|
+
raise RuntimeError(msg) # noqa: B904
|
|
311
295
|
caller_frame = tb.tb_frame
|
|
312
296
|
while caller_frame.f_locals.get("self", None) is self:
|
|
313
297
|
caller_frame = caller_frame.f_back
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
298
|
+
if not caller_frame:
|
|
299
|
+
msg = (
|
|
300
|
+
"No previous frame to the one with self in it, "
|
|
301
|
+
"this shouldn't happen"
|
|
302
|
+
)
|
|
303
|
+
raise RuntimeError( # noqa: B904
|
|
304
|
+
msg
|
|
305
|
+
)
|
|
306
|
+
return caller_frame.f_locals.copy()
|
|
318
307
|
|
|
319
|
-
def __enter__(self) ->
|
|
308
|
+
def __enter__(self) -> DeviceProcessor:
|
|
320
309
|
# Stash the names that were defined before we were called
|
|
321
|
-
self.
|
|
310
|
+
self._locals_on_enter = self._caller_locals()
|
|
322
311
|
return self
|
|
323
312
|
|
|
324
|
-
async def __aenter__(self) ->
|
|
313
|
+
async def __aenter__(self) -> DeviceProcessor:
|
|
325
314
|
return self.__enter__()
|
|
326
315
|
|
|
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
316
|
async def __aexit__(self, type, value, traceback):
|
|
344
|
-
self.
|
|
317
|
+
self._locals_on_exit = self._caller_locals()
|
|
345
318
|
await self._on_exit()
|
|
346
319
|
|
|
347
320
|
def __exit__(self, type_, value, traceback):
|
|
@@ -350,7 +323,7 @@ class DeviceCollector:
|
|
|
350
323
|
"Cannot use DeviceConnector inside a plan, instead use "
|
|
351
324
|
"`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
|
|
352
325
|
)
|
|
353
|
-
self.
|
|
326
|
+
self._locals_on_exit = self._caller_locals()
|
|
354
327
|
try:
|
|
355
328
|
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
356
329
|
except RuntimeError as e:
|
|
@@ -360,3 +333,58 @@ class DeviceCollector:
|
|
|
360
333
|
"user/explanations/event-loop-choice.html for more info."
|
|
361
334
|
) from e
|
|
362
335
|
return fut
|
|
336
|
+
|
|
337
|
+
async def _on_exit(self) -> None:
|
|
338
|
+
# Find all the devices
|
|
339
|
+
devices = {
|
|
340
|
+
name: obj
|
|
341
|
+
for name, obj in self._locals_on_exit.items()
|
|
342
|
+
if isinstance(obj, Device) and self._locals_on_enter.get(name) is not obj
|
|
343
|
+
}
|
|
344
|
+
# Call the provided process function on them
|
|
345
|
+
await self._process_devices(devices)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def init_devices(
|
|
349
|
+
set_name=True,
|
|
350
|
+
child_name_separator: str = "-",
|
|
351
|
+
connect=True,
|
|
352
|
+
mock=False,
|
|
353
|
+
timeout: float = 10.0,
|
|
354
|
+
):
|
|
355
|
+
"""Auto initialize top level Device instances: to be used as a context manager.
|
|
356
|
+
|
|
357
|
+
:param set_name:
|
|
358
|
+
If True, call `device.set_name(variable_name)` on all Devices created
|
|
359
|
+
within the context manager that have an empty `name`.
|
|
360
|
+
:param child_name_separator: Separator for child names if `set_name` is True.
|
|
361
|
+
:param connect:
|
|
362
|
+
If True, call `device.connect(mock, timeout)` in parallel on all Devices
|
|
363
|
+
created within the context manager.
|
|
364
|
+
:param mock: If True, connect Signals in mock mode.
|
|
365
|
+
:param timeout: How long to wait for connect before logging an exception.
|
|
366
|
+
:raises RuntimeError: If used inside a plan, use [](#ensure_connected) instead.
|
|
367
|
+
:raises NotConnected: If devices could not be connected.
|
|
368
|
+
|
|
369
|
+
For example, to connect and name 2 motors in parallel:
|
|
370
|
+
```python
|
|
371
|
+
[async] with init_devices():
|
|
372
|
+
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
373
|
+
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
374
|
+
# Names and connects devices here
|
|
375
|
+
assert t1x.name == "t1x"
|
|
376
|
+
```
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
async def process_devices(devices: dict[str, Device]):
|
|
380
|
+
if set_name:
|
|
381
|
+
for name, device in devices.items():
|
|
382
|
+
if not device.name:
|
|
383
|
+
device.set_name(name, child_name_separator=child_name_separator)
|
|
384
|
+
if connect:
|
|
385
|
+
coros = {
|
|
386
|
+
name: device.connect(mock, timeout) for name, device in devices.items()
|
|
387
|
+
}
|
|
388
|
+
await wait_for_connection(**coros)
|
|
389
|
+
|
|
390
|
+
return DeviceProcessor(process_devices)
|
|
@@ -16,7 +16,7 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
from ._device import Device, DeviceConnector, DeviceVector
|
|
19
|
-
from ._signal import Signal, SignalX
|
|
19
|
+
from ._signal import Ignore, Signal, SignalX
|
|
20
20
|
from ._signal_backend import SignalBackend, SignalDatatype
|
|
21
21
|
from ._utils import get_origin_class
|
|
22
22
|
|
|
@@ -33,12 +33,20 @@ def _get_datatype(annotation: Any) -> type | None:
|
|
|
33
33
|
args = get_args(annotation)
|
|
34
34
|
if len(args) == 1 and get_origin_class(args[0]):
|
|
35
35
|
return args[0]
|
|
36
|
+
return None
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def _logical(name: UniqueName) -> LogicalName:
|
|
39
40
|
return LogicalName(name.rstrip("_"))
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
def _check_device_annotation(annotation: Any) -> DeviceAnnotation:
|
|
44
|
+
if not isinstance(annotation, DeviceAnnotation):
|
|
45
|
+
msg = f"Annotation {annotation} is not a DeviceAnnotation"
|
|
46
|
+
raise TypeError(msg)
|
|
47
|
+
return annotation
|
|
48
|
+
|
|
49
|
+
|
|
42
50
|
@runtime_checkable
|
|
43
51
|
class DeviceAnnotation(Protocol):
|
|
44
52
|
@abstractmethod
|
|
@@ -46,6 +54,13 @@ class DeviceAnnotation(Protocol):
|
|
|
46
54
|
|
|
47
55
|
|
|
48
56
|
class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
57
|
+
"""For filling signals on introspected devices.
|
|
58
|
+
|
|
59
|
+
:param device: The device to fill.
|
|
60
|
+
:param signal_backend_factory: A callable that returns a SignalBackend.
|
|
61
|
+
:param device_connector_factory: A callable that returns a DeviceConnector.
|
|
62
|
+
"""
|
|
63
|
+
|
|
49
64
|
def __init__(
|
|
50
65
|
self,
|
|
51
66
|
device: Device,
|
|
@@ -61,6 +76,7 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
61
76
|
self._extras: dict[UniqueName, Sequence[Any]] = {}
|
|
62
77
|
self._signal_datatype: dict[LogicalName, type | None] = {}
|
|
63
78
|
self._vector_device_type: dict[LogicalName, type[Device] | None] = {}
|
|
79
|
+
self.ignored_signals: set[str] = set()
|
|
64
80
|
# Backends and Connectors stored ready for the connection phase
|
|
65
81
|
self._unfilled_backends: dict[
|
|
66
82
|
LogicalName, tuple[SignalBackendT, type[Signal]]
|
|
@@ -101,6 +117,8 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
101
117
|
# Get hints with Annotated for wrapping signals and backends
|
|
102
118
|
extra_hints = get_type_hints(cls, include_extras=True)
|
|
103
119
|
for attr_name, annotation in hints.items():
|
|
120
|
+
if annotation is Ignore:
|
|
121
|
+
self.ignored_signals.add(attr_name)
|
|
104
122
|
name = UniqueName(attr_name)
|
|
105
123
|
origin = get_origin_class(annotation)
|
|
106
124
|
if (
|
|
@@ -131,6 +149,7 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
131
149
|
self._uncreated_devices[name] = origin
|
|
132
150
|
|
|
133
151
|
def check_created(self):
|
|
152
|
+
"""Check that all Signals and Devices declared in annotations are created."""
|
|
134
153
|
uncreated = sorted(set(self._uncreated_signals).union(self._uncreated_devices))
|
|
135
154
|
if uncreated:
|
|
136
155
|
raise RuntimeError(
|
|
@@ -141,6 +160,21 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
141
160
|
self,
|
|
142
161
|
filled=True,
|
|
143
162
|
) -> Iterator[tuple[SignalBackendT, list[Any]]]:
|
|
163
|
+
"""Create all Signals from annotations.
|
|
164
|
+
|
|
165
|
+
:param filled:
|
|
166
|
+
If True then the Signals created should be considered already filled
|
|
167
|
+
with connection data. If False then `fill_child_signal` needs
|
|
168
|
+
calling at device connection time before the signal can be
|
|
169
|
+
connected.
|
|
170
|
+
:yields: `(backend, extras)`
|
|
171
|
+
The `SignalBackend` that has been created for this Signal, and the
|
|
172
|
+
list of extra annotations that could be used to customize it. For
|
|
173
|
+
example an `EpicsDeviceConnector` consumes `PvSuffix` extras to set the
|
|
174
|
+
write_pv of the backend. Any unhandled extras should be left on the
|
|
175
|
+
list so this class can handle them, e.g. `StandardReadableFormat`
|
|
176
|
+
instances.
|
|
177
|
+
"""
|
|
144
178
|
for name in list(self._uncreated_signals):
|
|
145
179
|
child_type = self._uncreated_signals.pop(name)
|
|
146
180
|
backend = self._signal_backend_factory(
|
|
@@ -150,8 +184,8 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
150
184
|
yield backend, extras
|
|
151
185
|
signal = child_type(backend)
|
|
152
186
|
for anno in extras:
|
|
153
|
-
|
|
154
|
-
|
|
187
|
+
device_annotation = _check_device_annotation(annotation=anno)
|
|
188
|
+
device_annotation(self._device, signal)
|
|
155
189
|
setattr(self._device, name, signal)
|
|
156
190
|
dest = self._filled_backends if filled else self._unfilled_backends
|
|
157
191
|
dest[_logical(name)] = (backend, child_type)
|
|
@@ -160,6 +194,17 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
160
194
|
self,
|
|
161
195
|
filled=True,
|
|
162
196
|
) -> Iterator[tuple[DeviceConnectorT, list[Any]]]:
|
|
197
|
+
"""Create all Signals from annotations.
|
|
198
|
+
|
|
199
|
+
:param filled:
|
|
200
|
+
If True then the Devices created should be considered already filled
|
|
201
|
+
with connection data. If False then `fill_child_device` needs
|
|
202
|
+
calling at parent device connection time before the child Device can
|
|
203
|
+
be connected.
|
|
204
|
+
:yields: `(connector, extras)`
|
|
205
|
+
The `DeviceConnector` that has been created for this Signal, and the list of
|
|
206
|
+
extra annotations that could be used to customize it.
|
|
207
|
+
"""
|
|
163
208
|
for name in list(self._uncreated_devices):
|
|
164
209
|
child_type = self._uncreated_devices.pop(name)
|
|
165
210
|
connector = self._device_connector_factory()
|
|
@@ -167,15 +212,21 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
167
212
|
yield connector, extras
|
|
168
213
|
device = child_type(connector=connector)
|
|
169
214
|
for anno in extras:
|
|
170
|
-
|
|
171
|
-
|
|
215
|
+
device_annotation = _check_device_annotation(annotation=anno)
|
|
216
|
+
device_annotation(self._device, device)
|
|
172
217
|
setattr(self._device, name, device)
|
|
173
218
|
dest = self._filled_connectors if filled else self._unfilled_connectors
|
|
174
219
|
dest[_logical(name)] = connector
|
|
175
220
|
|
|
176
221
|
def create_device_vector_entries_to_mock(self, num: int):
|
|
222
|
+
"""Create num entries for each `DeviceVector`.
|
|
223
|
+
|
|
224
|
+
This is used when the Device is being connected in mock mode.
|
|
225
|
+
"""
|
|
177
226
|
for name, cls in self._vector_device_type.items():
|
|
178
|
-
|
|
227
|
+
if not cls:
|
|
228
|
+
msg = "Malformed device vector"
|
|
229
|
+
raise TypeError(msg)
|
|
179
230
|
for i in range(1, num + 1):
|
|
180
231
|
if issubclass(cls, Signal):
|
|
181
232
|
self.fill_child_signal(name, cls, i)
|
|
@@ -185,6 +236,11 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
185
236
|
self._raise(name, f"Can't make {cls}")
|
|
186
237
|
|
|
187
238
|
def check_filled(self, source: str):
|
|
239
|
+
"""Check that all the created Signals and Devices are filled.
|
|
240
|
+
|
|
241
|
+
:param source: The source of the data that should have done the filling, for
|
|
242
|
+
reporting as an error message
|
|
243
|
+
"""
|
|
188
244
|
unfilled = sorted(set(self._unfilled_connectors).union(self._unfilled_backends))
|
|
189
245
|
if unfilled:
|
|
190
246
|
raise RuntimeError(
|
|
@@ -207,6 +263,15 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
207
263
|
signal_type: type[Signal],
|
|
208
264
|
vector_index: int | None = None,
|
|
209
265
|
) -> SignalBackendT:
|
|
266
|
+
"""Mark a Signal as filled, and return its backend for filling.
|
|
267
|
+
|
|
268
|
+
:param name:
|
|
269
|
+
The name without trailing underscore, the name in the control system
|
|
270
|
+
:param signal_type:
|
|
271
|
+
One of the types `SignalR`, `SignalW`, `SignalRW` or `SignalX`
|
|
272
|
+
:param vector_index: If the child is in a `DeviceVector` then what index is it
|
|
273
|
+
:return: The SignalBackend for the filled Signal.
|
|
274
|
+
"""
|
|
210
275
|
name = cast(LogicalName, name)
|
|
211
276
|
if name in self._unfilled_backends:
|
|
212
277
|
# We made it above
|
|
@@ -242,6 +307,14 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
242
307
|
device_type: type[Device] = Device,
|
|
243
308
|
vector_index: int | None = None,
|
|
244
309
|
) -> DeviceConnectorT:
|
|
310
|
+
"""Mark a Device as filled, and return its connector for filling.
|
|
311
|
+
|
|
312
|
+
:param name:
|
|
313
|
+
The name without trailing underscore, the name in the control system
|
|
314
|
+
:param device_type: The `Device` subclass to be created
|
|
315
|
+
:param vector_index: If the child is in a `DeviceVector` then what index is it
|
|
316
|
+
:return: The DeviceConnector for the filled Device.
|
|
317
|
+
"""
|
|
245
318
|
name = cast(LogicalName, name)
|
|
246
319
|
if name in self._unfilled_connectors:
|
|
247
320
|
# We made it above
|
|
@@ -254,9 +327,9 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
|
254
327
|
# We need to add a new entry to a DeviceVector
|
|
255
328
|
vector = self._ensure_device_vector(name)
|
|
256
329
|
vector_device_type = self._vector_device_type[name] or device_type
|
|
257
|
-
|
|
258
|
-
vector_device_type
|
|
259
|
-
|
|
330
|
+
if not issubclass(vector_device_type, Device):
|
|
331
|
+
msg = f"{vector_device_type} is not a Device"
|
|
332
|
+
raise TypeError(msg)
|
|
260
333
|
connector = self._device_connector_factory()
|
|
261
334
|
vector[vector_index] = vector_device_type(connector=connector)
|
|
262
335
|
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
|
|
|
@@ -9,21 +9,26 @@ from ._utils import T
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class FlyerController(ABC, Generic[T]):
|
|
12
|
+
"""Base class for controlling 'flyable' devices.
|
|
13
|
+
|
|
14
|
+
[`bluesky.protocols.Flyable`](#bluesky.protocols.Flyable).
|
|
15
|
+
"""
|
|
16
|
+
|
|
12
17
|
@abstractmethod
|
|
13
|
-
async def prepare(self, value: T):
|
|
14
|
-
"""Move to the start of the flyscan"""
|
|
18
|
+
async def prepare(self, value: T) -> Any:
|
|
19
|
+
"""Move to the start of the flyscan."""
|
|
15
20
|
|
|
16
21
|
@abstractmethod
|
|
17
22
|
async def kickoff(self):
|
|
18
|
-
"""Start the flyscan"""
|
|
23
|
+
"""Start the flyscan."""
|
|
19
24
|
|
|
20
25
|
@abstractmethod
|
|
21
26
|
async def complete(self):
|
|
22
|
-
"""Block until the flyscan is done"""
|
|
27
|
+
"""Block until the flyscan is done."""
|
|
23
28
|
|
|
24
29
|
@abstractmethod
|
|
25
30
|
async def stop(self):
|
|
26
|
-
"""Stop flying and wait everything to be stopped"""
|
|
31
|
+
"""Stop flying and wait everything to be stopped."""
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
class StandardFlyer(
|
|
@@ -33,6 +38,11 @@ class StandardFlyer(
|
|
|
33
38
|
Flyable,
|
|
34
39
|
Generic[T],
|
|
35
40
|
):
|
|
41
|
+
"""Base class for 'flyable' devices.
|
|
42
|
+
|
|
43
|
+
[`bluesky.protocols.Flyable`](#bluesky.protocols.Flyable).
|
|
44
|
+
"""
|
|
45
|
+
|
|
36
46
|
def __init__(
|
|
37
47
|
self,
|
|
38
48
|
trigger_logic: FlyerController[T],
|
|
@@ -54,7 +64,6 @@ class StandardFlyer(
|
|
|
54
64
|
await self._trigger_logic.stop()
|
|
55
65
|
|
|
56
66
|
def prepare(self, value: T) -> AsyncStatus:
|
|
57
|
-
"""Setup trajectories"""
|
|
58
67
|
return AsyncStatus(self._prepare(value))
|
|
59
68
|
|
|
60
69
|
async def _prepare(self, value: T) -> None:
|