ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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/_version.py +2 -2
- ophyd_async/core/__init__.py +17 -46
- ophyd_async/core/_detector.py +68 -44
- ophyd_async/core/_device.py +120 -79
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_protocol.py +0 -28
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +164 -151
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +27 -14
- ophyd_async/core/_utils.py +30 -5
- 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 +21 -30
- ophyd_async/epics/adaravis/_aravis_io.py +4 -4
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +56 -31
- ophyd_async/epics/adcore/_core_logic.py +99 -84
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +33 -59
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +5 -4
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +37 -36
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
- ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
- ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
- 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 +21 -35
- ophyd_async/epics/advimba/_vimba_io.py +23 -23
- ophyd_async/epics/core/_aioca.py +52 -21
- ophyd_async/epics/core/_p4p.py +59 -16
- ophyd_async/epics/core/_pvi_connector.py +4 -2
- ophyd_async/epics/core/_signal.py +9 -2
- ophyd_async/epics/core/_util.py +10 -1
- ophyd_async/epics/eiger/_eiger_controller.py +10 -5
- ophyd_async/epics/eiger/_eiger_io.py +3 -3
- ophyd_async/epics/motor.py +26 -15
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +12 -6
- ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +91 -0
- ophyd_async/epics/testing/_utils.py +50 -0
- ophyd_async/epics/testing/test_records.db +174 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_block.py +9 -9
- ophyd_async/fastcs/panda/_control.py +9 -4
- ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/fastcs/panda/_trigger.py +7 -7
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +2 -2
- 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/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
- ophyd_async/tango/__init__.py +0 -43
- ophyd_async/tango/{signal → core}/__init__.py +7 -2
- ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
- ophyd_async/tango/{signal → core}/_signal.py +16 -4
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_counter.py +6 -7
- ophyd_async/tango/{demo → sim}/_mover.py +13 -9
- ophyd_async/testing/__init__.py +52 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +176 -0
- ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
- ophyd_async/testing/_one_of_everything.py +126 -0
- ophyd_async/testing/_wait_for_pending.py +22 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.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/tango/base_devices/__init__.py +0 -4
- ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
- ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.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/tango/{demo → sim}/__init__.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.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.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
|
|
@@ -10,7 +10,6 @@ from typing import Any, TypeVar
|
|
|
10
10
|
from bluesky.protocols import HasName
|
|
11
11
|
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
12
12
|
|
|
13
|
-
from ._protocol import Connectable
|
|
14
13
|
from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection
|
|
15
14
|
|
|
16
15
|
|
|
@@ -61,7 +60,7 @@ class DeviceConnector:
|
|
|
61
60
|
await wait_for_connection(**coros)
|
|
62
61
|
|
|
63
62
|
|
|
64
|
-
class Device(HasName
|
|
63
|
+
class Device(HasName):
|
|
65
64
|
"""Common base class for all Ophyd Async Devices."""
|
|
66
65
|
|
|
67
66
|
_name: str = ""
|
|
@@ -71,13 +70,16 @@ class Device(HasName, Connectable):
|
|
|
71
70
|
_connect_task: asyncio.Task | None = None
|
|
72
71
|
# The mock if we have connected in mock mode
|
|
73
72
|
_mock: LazyMock | None = None
|
|
73
|
+
# The separator to use when making child names
|
|
74
|
+
_child_name_separator: str = "-"
|
|
74
75
|
|
|
75
76
|
def __init__(
|
|
76
77
|
self, name: str = "", connector: DeviceConnector | None = None
|
|
77
78
|
) -> None:
|
|
78
79
|
self._connector = connector or DeviceConnector()
|
|
79
80
|
self._connector.create_children_from_annotations(self)
|
|
80
|
-
|
|
81
|
+
if name:
|
|
82
|
+
self.set_name(name)
|
|
81
83
|
|
|
82
84
|
@property
|
|
83
85
|
def name(self) -> str:
|
|
@@ -97,21 +99,30 @@ class Device(HasName, Connectable):
|
|
|
97
99
|
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
98
100
|
)
|
|
99
101
|
|
|
100
|
-
def set_name(self, name: str):
|
|
102
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
101
103
|
"""Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
|
|
102
104
|
|
|
103
105
|
Parameters
|
|
104
106
|
----------
|
|
105
107
|
name:
|
|
106
108
|
New name to set
|
|
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.
|
|
107
112
|
"""
|
|
108
113
|
self._name = name
|
|
114
|
+
if child_name_separator:
|
|
115
|
+
self._child_name_separator = child_name_separator
|
|
109
116
|
# Ensure logger is recreated after a name change
|
|
110
117
|
if "log" in self.__dict__:
|
|
111
118
|
del self.log
|
|
112
|
-
for
|
|
113
|
-
child_name =
|
|
114
|
-
|
|
119
|
+
for attr_name, child in self.children():
|
|
120
|
+
child_name = (
|
|
121
|
+
f"{self.name}{self._child_name_separator}{attr_name}"
|
|
122
|
+
if self.name
|
|
123
|
+
else ""
|
|
124
|
+
)
|
|
125
|
+
child.set_name(child_name, child_name_separator=self._child_name_separator)
|
|
115
126
|
|
|
116
127
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
117
128
|
# Bear in mind that this function is called *a lot*, so
|
|
@@ -147,6 +158,12 @@ class Device(HasName, Connectable):
|
|
|
147
158
|
timeout:
|
|
148
159
|
Time to wait before failing with a TimeoutError.
|
|
149
160
|
"""
|
|
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)
|
|
150
167
|
if mock:
|
|
151
168
|
# Always connect in mock mode serially
|
|
152
169
|
if isinstance(mock, LazyMock):
|
|
@@ -167,7 +184,9 @@ class Device(HasName, Connectable):
|
|
|
167
184
|
self._mock = None
|
|
168
185
|
coro = self._connector.connect_real(self, timeout, force_reconnect)
|
|
169
186
|
self._connect_task = asyncio.create_task(coro)
|
|
170
|
-
|
|
187
|
+
if not self._connect_task:
|
|
188
|
+
msg = "Connect task not created, this shouldn't happen"
|
|
189
|
+
raise RuntimeError(msg)
|
|
171
190
|
# Wait for it to complete
|
|
172
191
|
await self._connect_task
|
|
173
192
|
|
|
@@ -191,7 +210,7 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
191
210
|
|
|
192
211
|
In the below example, foos becomes a dictionary on the parent device
|
|
193
212
|
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
194
|
-
:class:`~ophyd_async.epics.
|
|
213
|
+
:class:`~ophyd_async.epics.sim.DynamicSensorGroup`
|
|
195
214
|
"""
|
|
196
215
|
|
|
197
216
|
def __init__(
|
|
@@ -217,8 +236,12 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
217
236
|
def __setitem__(self, key: int, value: DeviceT) -> None:
|
|
218
237
|
# Check the types on entry to dict to make sure we can't accidentally
|
|
219
238
|
# make a non-integer named child
|
|
220
|
-
|
|
221
|
-
|
|
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)
|
|
222
245
|
self._children[key] = value
|
|
223
246
|
value.parent = self
|
|
224
247
|
|
|
@@ -239,90 +262,49 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
|
239
262
|
return hash(id(self))
|
|
240
263
|
|
|
241
264
|
|
|
242
|
-
class
|
|
243
|
-
"""
|
|
244
|
-
|
|
245
|
-
Parameters
|
|
246
|
-
----------
|
|
247
|
-
set_name:
|
|
248
|
-
If True, call ``device.set_name(variable_name)`` on all collected
|
|
249
|
-
Devices
|
|
250
|
-
connect:
|
|
251
|
-
If True, call ``device.connect(mock)`` in parallel on all
|
|
252
|
-
collected Devices
|
|
253
|
-
mock:
|
|
254
|
-
If True, connect Signals in simulation mode
|
|
255
|
-
timeout:
|
|
256
|
-
How long to wait for connect before logging an exception
|
|
257
|
-
|
|
258
|
-
Notes
|
|
259
|
-
-----
|
|
260
|
-
Example usage::
|
|
261
|
-
|
|
262
|
-
[async] with DeviceCollector():
|
|
263
|
-
t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
|
|
264
|
-
t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
|
|
265
|
-
# Names and connects devices here
|
|
266
|
-
assert t1x.comm.velocity.source
|
|
267
|
-
assert t1x.name == "t1x"
|
|
265
|
+
class DeviceProcessor:
|
|
266
|
+
"""Sync/Async Context Manager that finds all the Devices declared within it.
|
|
268
267
|
|
|
268
|
+
Used in `init_devices`
|
|
269
269
|
"""
|
|
270
270
|
|
|
271
|
-
def __init__(
|
|
272
|
-
self
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
):
|
|
278
|
-
self._set_name = set_name
|
|
279
|
-
self._connect = connect
|
|
280
|
-
self._mock = mock
|
|
281
|
-
self._timeout = timeout
|
|
282
|
-
self._names_on_enter: set[str] = set()
|
|
283
|
-
self._objects_on_exit: dict[str, Any] = {}
|
|
284
|
-
|
|
285
|
-
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]:
|
|
286
277
|
"""Walk up until we find a stack frame that doesn't have us as self"""
|
|
287
278
|
try:
|
|
288
279
|
raise ValueError
|
|
289
280
|
except ValueError:
|
|
290
281
|
_, _, tb = sys.exc_info()
|
|
291
|
-
|
|
282
|
+
if not tb:
|
|
283
|
+
msg = "Can't get traceback, this shouldn't happen"
|
|
284
|
+
raise RuntimeError(msg) # noqa: B904
|
|
292
285
|
caller_frame = tb.tb_frame
|
|
293
286
|
while caller_frame.f_locals.get("self", None) is self:
|
|
294
287
|
caller_frame = caller_frame.f_back
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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()
|
|
299
297
|
|
|
300
|
-
def __enter__(self) ->
|
|
298
|
+
def __enter__(self) -> DeviceProcessor:
|
|
301
299
|
# Stash the names that were defined before we were called
|
|
302
|
-
self.
|
|
300
|
+
self._locals_on_enter = self._caller_locals()
|
|
303
301
|
return self
|
|
304
302
|
|
|
305
|
-
async def __aenter__(self) ->
|
|
303
|
+
async def __aenter__(self) -> DeviceProcessor:
|
|
306
304
|
return self.__enter__()
|
|
307
305
|
|
|
308
|
-
async def _on_exit(self) -> None:
|
|
309
|
-
# Name and kick off connect for devices
|
|
310
|
-
connect_coroutines: dict[str, Coroutine] = {}
|
|
311
|
-
for name, obj in self._objects_on_exit.items():
|
|
312
|
-
if name not in self._names_on_enter and isinstance(obj, Device):
|
|
313
|
-
if self._set_name and not obj.name:
|
|
314
|
-
obj.set_name(name)
|
|
315
|
-
if self._connect:
|
|
316
|
-
connect_coroutines[name] = obj.connect(
|
|
317
|
-
self._mock, timeout=self._timeout
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
# Connect to all the devices
|
|
321
|
-
if connect_coroutines:
|
|
322
|
-
await wait_for_connection(**connect_coroutines)
|
|
323
|
-
|
|
324
306
|
async def __aexit__(self, type, value, traceback):
|
|
325
|
-
self.
|
|
307
|
+
self._locals_on_exit = self._caller_locals()
|
|
326
308
|
await self._on_exit()
|
|
327
309
|
|
|
328
310
|
def __exit__(self, type_, value, traceback):
|
|
@@ -331,7 +313,7 @@ class DeviceCollector:
|
|
|
331
313
|
"Cannot use DeviceConnector inside a plan, instead use "
|
|
332
314
|
"`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
|
|
333
315
|
)
|
|
334
|
-
self.
|
|
316
|
+
self._locals_on_exit = self._caller_locals()
|
|
335
317
|
try:
|
|
336
318
|
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
337
319
|
except RuntimeError as e:
|
|
@@ -341,3 +323,62 @@ class DeviceCollector:
|
|
|
341
323
|
"user/explanations/event-loop-choice.html for more info."
|
|
342
324
|
) from e
|
|
343
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
|
ophyd_async/core/_protocol.py
CHANGED
|
@@ -13,38 +13,10 @@ from typing import (
|
|
|
13
13
|
from bluesky.protocols import HasName, Reading
|
|
14
14
|
from event_model import DataKey
|
|
15
15
|
|
|
16
|
-
from ._utils import DEFAULT_TIMEOUT
|
|
17
|
-
|
|
18
16
|
if TYPE_CHECKING:
|
|
19
|
-
from unittest.mock import Mock
|
|
20
|
-
|
|
21
17
|
from ._status import AsyncStatus
|
|
22
18
|
|
|
23
19
|
|
|
24
|
-
@runtime_checkable
|
|
25
|
-
class Connectable(Protocol):
|
|
26
|
-
@abstractmethod
|
|
27
|
-
async def connect(
|
|
28
|
-
self,
|
|
29
|
-
mock: bool | Mock = False,
|
|
30
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
31
|
-
force_reconnect: bool = False,
|
|
32
|
-
):
|
|
33
|
-
"""Connect self and all child Devices.
|
|
34
|
-
|
|
35
|
-
Contains a timeout that gets propagated to child.connect methods.
|
|
36
|
-
|
|
37
|
-
Parameters
|
|
38
|
-
----------
|
|
39
|
-
mock:
|
|
40
|
-
If True then use ``MockSignalBackend`` for all Signals
|
|
41
|
-
timeout:
|
|
42
|
-
Time to wait before failing with a TimeoutError.
|
|
43
|
-
force_reconnect:
|
|
44
|
-
Reconnect even if previous connect was successful.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
|
|
48
20
|
@runtime_checkable
|
|
49
21
|
class AsyncReadable(HasName, Protocol):
|
|
50
22
|
@abstractmethod
|
ophyd_async/core/_readable.py
CHANGED
|
@@ -123,29 +123,31 @@ class StandardReadable(
|
|
|
123
123
|
# we want to combine them when they are Sequences, and ensure they are
|
|
124
124
|
# identical when string values.
|
|
125
125
|
for key, value in new_hint.hints.items():
|
|
126
|
+
# fail early for unkwon types
|
|
126
127
|
if isinstance(value, str):
|
|
127
128
|
if key in hints:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
if hints[key] != value:
|
|
130
|
+
msg = f"Hints key {key} value may not be overridden"
|
|
131
|
+
raise RuntimeError(msg)
|
|
131
132
|
else:
|
|
132
133
|
hints[key] = value # type: ignore[literal-required]
|
|
133
134
|
elif isinstance(value, Sequence):
|
|
134
135
|
if key in hints:
|
|
135
136
|
for new_val in value:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
if new_val in hints[key]:
|
|
138
|
+
msg = f"Hint {key} {new_val} overrides existing hint"
|
|
139
|
+
raise RuntimeError(msg)
|
|
139
140
|
hints[key] = ( # type: ignore[literal-required]
|
|
140
141
|
hints[key] + value # type: ignore[literal-required]
|
|
141
142
|
)
|
|
142
143
|
else:
|
|
143
144
|
hints[key] = value # type: ignore[literal-required]
|
|
144
145
|
else:
|
|
145
|
-
|
|
146
|
-
f"{new_hint.name}: Unknown type for value '{value}'
|
|
146
|
+
msg = (
|
|
147
|
+
f"{new_hint.name}: Unknown type for value '{value}'"
|
|
147
148
|
f" for key '{key}'"
|
|
148
149
|
)
|
|
150
|
+
raise TypeError(msg)
|
|
149
151
|
|
|
150
152
|
return hints
|
|
151
153
|
|
|
@@ -204,6 +206,11 @@ class StandardReadable(
|
|
|
204
206
|
`StandardReadableFormat` documentation
|
|
205
207
|
"""
|
|
206
208
|
|
|
209
|
+
def assert_device_is_signalr(device: Device) -> SignalR:
|
|
210
|
+
if not isinstance(device, SignalR):
|
|
211
|
+
raise TypeError(f"{device} is not a SignalR")
|
|
212
|
+
return device
|
|
213
|
+
|
|
207
214
|
for device in devices:
|
|
208
215
|
match format:
|
|
209
216
|
case StandardReadableFormat.CHILD:
|
|
@@ -218,24 +225,24 @@ class StandardReadable(
|
|
|
218
225
|
if isinstance(device, HasHints):
|
|
219
226
|
self._has_hints += (device,)
|
|
220
227
|
case StandardReadableFormat.CONFIG_SIGNAL:
|
|
221
|
-
|
|
222
|
-
self._describe_config_funcs += (
|
|
223
|
-
self._read_config_funcs += (
|
|
228
|
+
signalr_device = assert_device_is_signalr(device=device)
|
|
229
|
+
self._describe_config_funcs += (signalr_device.describe,)
|
|
230
|
+
self._read_config_funcs += (signalr_device.read,)
|
|
224
231
|
case StandardReadableFormat.HINTED_SIGNAL:
|
|
225
|
-
|
|
226
|
-
self._describe_funcs += (
|
|
227
|
-
self._read_funcs += (
|
|
228
|
-
self._stageables += (
|
|
229
|
-
self._has_hints += (_HintsFromName(
|
|
232
|
+
signalr_device = assert_device_is_signalr(device=device)
|
|
233
|
+
self._describe_funcs += (signalr_device.describe,)
|
|
234
|
+
self._read_funcs += (signalr_device.read,)
|
|
235
|
+
self._stageables += (signalr_device,)
|
|
236
|
+
self._has_hints += (_HintsFromName(signalr_device),)
|
|
230
237
|
case StandardReadableFormat.UNCACHED_SIGNAL:
|
|
231
|
-
|
|
232
|
-
self._describe_funcs += (
|
|
233
|
-
self._read_funcs += (_UncachedRead(
|
|
238
|
+
signalr_device = assert_device_is_signalr(device=device)
|
|
239
|
+
self._describe_funcs += (signalr_device.describe,)
|
|
240
|
+
self._read_funcs += (_UncachedRead(signalr_device),)
|
|
234
241
|
case StandardReadableFormat.HINTED_UNCACHED_SIGNAL:
|
|
235
|
-
|
|
236
|
-
self._describe_funcs += (
|
|
237
|
-
self._read_funcs += (_UncachedRead(
|
|
238
|
-
self._has_hints += (_HintsFromName(
|
|
242
|
+
signalr_device = assert_device_is_signalr(device=device)
|
|
243
|
+
self._describe_funcs += (signalr_device.describe,)
|
|
244
|
+
self._read_funcs += (_UncachedRead(signalr_device),)
|
|
245
|
+
self._has_hints += (_HintsFromName(signalr_device),)
|
|
239
246
|
|
|
240
247
|
|
|
241
248
|
class _UncachedRead:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from collections.abc import Callable, Iterator, MutableMapping
|
|
5
|
+
from typing import Any, Generic
|
|
6
|
+
|
|
7
|
+
from ._device import Device, DeviceT
|
|
8
|
+
from ._signal import SignalRW
|
|
9
|
+
from ._signal_backend import SignalDatatypeT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Settings(MutableMapping[SignalRW[Any], Any], Generic[DeviceT]):
|
|
13
|
+
def __init__(
|
|
14
|
+
self, device: DeviceT, settings: MutableMapping[SignalRW, Any] | None = None
|
|
15
|
+
):
|
|
16
|
+
self.device = device
|
|
17
|
+
self._settings = {}
|
|
18
|
+
self.update(settings or {})
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key: SignalRW[SignalDatatypeT]) -> SignalDatatypeT:
|
|
21
|
+
return self._settings[key]
|
|
22
|
+
|
|
23
|
+
def _is_in_device(self, device: Device) -> bool:
|
|
24
|
+
while device.parent and device.parent is not self.device:
|
|
25
|
+
# While we have a parent that is not the right device
|
|
26
|
+
# continue searching up the tree
|
|
27
|
+
device = device.parent
|
|
28
|
+
return device.parent is self.device
|
|
29
|
+
|
|
30
|
+
def __setitem__(
|
|
31
|
+
self, key: SignalRW[SignalDatatypeT], value: SignalDatatypeT | None
|
|
32
|
+
) -> None:
|
|
33
|
+
# Check the types on entry to dict to make sure we can't accidentally
|
|
34
|
+
# add a non-signal type
|
|
35
|
+
if not isinstance(key, SignalRW):
|
|
36
|
+
raise TypeError(f"Expected SignalRW, got {key}")
|
|
37
|
+
if not self._is_in_device(key):
|
|
38
|
+
raise KeyError(f"Signal {key} is not a child of {self.device}")
|
|
39
|
+
self._settings[key] = value
|
|
40
|
+
|
|
41
|
+
def __delitem__(self, key: SignalRW) -> None:
|
|
42
|
+
del self._settings[key]
|
|
43
|
+
|
|
44
|
+
def __iter__(self) -> Iterator[SignalRW]:
|
|
45
|
+
yield from iter(self._settings)
|
|
46
|
+
|
|
47
|
+
def __len__(self) -> int:
|
|
48
|
+
return len(self._settings)
|
|
49
|
+
|
|
50
|
+
def __or__(self, other: MutableMapping[SignalRW, Any]) -> Settings[DeviceT]:
|
|
51
|
+
"""Create a new Settings that is the union of self overridden by other.
|
|
52
|
+
|
|
53
|
+
For example::
|
|
54
|
+
|
|
55
|
+
settings1 = Settings(device, {device.sig1: 1, device.sig2: 2})
|
|
56
|
+
settings2 = Settings(device, {device.sig1: 10, device.sig3: 3})
|
|
57
|
+
settings = settings1 | settings2
|
|
58
|
+
assert dict(settings) == {
|
|
59
|
+
device.sig1: 10,
|
|
60
|
+
device.sig2: 2,
|
|
61
|
+
device.sig3: 3,
|
|
62
|
+
}
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(other, Settings) and not self._is_in_device(other.device):
|
|
65
|
+
raise ValueError(f"{other.device} is not a child of {self.device}")
|
|
66
|
+
return Settings(self.device, self._settings | dict(other))
|
|
67
|
+
|
|
68
|
+
def partition(
|
|
69
|
+
self, predicate: Callable[[SignalRW], bool]
|
|
70
|
+
) -> tuple[Settings[DeviceT], Settings[DeviceT]]:
|
|
71
|
+
"""Partition into two Settings based on a predicate.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
predicate
|
|
76
|
+
Callable that takes each signal, and returns a boolean to say if it
|
|
77
|
+
should be in the first returned Settings
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
(where_true, where_false)
|
|
82
|
+
|
|
83
|
+
For example::
|
|
84
|
+
|
|
85
|
+
settings = Settings(device, {device.special: 1, device.sig: 2})
|
|
86
|
+
specials, others = settings.partition(lambda sig: "special" in sig.name)
|
|
87
|
+
"""
|
|
88
|
+
where_true, where_false = Settings(self.device), Settings(self.device)
|
|
89
|
+
for signal, value in self.items():
|
|
90
|
+
dest = where_true if predicate(signal) else where_false
|
|
91
|
+
dest[signal] = value
|
|
92
|
+
return where_true, where_false
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class SettingsProvider:
|
|
96
|
+
@abstractmethod
|
|
97
|
+
async def store(self, name: str, data: dict[str, Any]):
|
|
98
|
+
"""Store the data, associating it with the given name."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
async def retrieve(self, name: str) -> dict[str, Any]:
|
|
103
|
+
"""Retrieve the data associated with the given name."""
|
|
104
|
+
...
|