ophyd-async 0.3a3__py3-none-any.whl → 0.3a5__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 +26 -11
- ophyd_async/core/async_status.py +96 -33
- ophyd_async/core/detector.py +22 -28
- ophyd_async/core/device.py +50 -14
- ophyd_async/core/mock_signal_backend.py +85 -0
- ophyd_async/core/mock_signal_utils.py +149 -0
- ophyd_async/core/signal.py +93 -53
- ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +23 -33
- ophyd_async/core/utils.py +19 -0
- ophyd_async/epics/_backend/_aioca.py +11 -7
- ophyd_async/epics/_backend/_p4p.py +11 -7
- ophyd_async/epics/_backend/common.py +17 -17
- ophyd_async/epics/areadetector/__init__.py +0 -4
- ophyd_async/epics/areadetector/aravis.py +1 -5
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +6 -1
- ophyd_async/epics/areadetector/drivers/ad_base.py +12 -10
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +6 -122
- ophyd_async/epics/areadetector/drivers/kinetix_driver.py +7 -4
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +5 -2
- ophyd_async/epics/areadetector/drivers/vimba_driver.py +12 -7
- ophyd_async/epics/areadetector/utils.py +2 -12
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +21 -19
- ophyd_async/epics/areadetector/writers/nd_plugin.py +6 -7
- ophyd_async/epics/demo/__init__.py +27 -32
- ophyd_async/epics/motion/motor.py +40 -37
- ophyd_async/epics/pvi/pvi.py +13 -13
- ophyd_async/epics/signal/__init__.py +8 -1
- ophyd_async/panda/_hdf_panda.py +3 -3
- ophyd_async/planstubs/__init__.py +5 -1
- ophyd_async/planstubs/ensure_connected.py +22 -0
- ophyd_async/protocols.py +32 -2
- ophyd_async/sim/demo/sim_motor.py +50 -35
- ophyd_async/sim/pattern_generator.py +5 -5
- {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/METADATA +2 -2
- {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/RECORD +40 -37
- {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/WHEEL +0 -0
- {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ from ._providers import (
|
|
|
5
5
|
ShapeProvider,
|
|
6
6
|
StaticDirectoryProvider,
|
|
7
7
|
)
|
|
8
|
-
from .async_status import AsyncStatus
|
|
8
|
+
from .async_status import AsyncStatus, WatchableAsyncStatus
|
|
9
9
|
from .detector import (
|
|
10
10
|
DetectorControl,
|
|
11
11
|
DetectorTrigger,
|
|
@@ -24,6 +24,18 @@ from .device_save_loader import (
|
|
|
24
24
|
walk_rw_signals,
|
|
25
25
|
)
|
|
26
26
|
from .flyer import HardwareTriggeredFlyable, TriggerLogic
|
|
27
|
+
from .mock_signal_backend import (
|
|
28
|
+
MockSignalBackend,
|
|
29
|
+
)
|
|
30
|
+
from .mock_signal_utils import (
|
|
31
|
+
assert_mock_put_called_with,
|
|
32
|
+
callback_on_mock_put,
|
|
33
|
+
mock_puts_blocked,
|
|
34
|
+
reset_mock_put_calls,
|
|
35
|
+
set_mock_put_proceeds,
|
|
36
|
+
set_mock_value,
|
|
37
|
+
set_mock_values,
|
|
38
|
+
)
|
|
27
39
|
from .signal import (
|
|
28
40
|
Signal,
|
|
29
41
|
SignalR,
|
|
@@ -36,15 +48,12 @@ from .signal import (
|
|
|
36
48
|
assert_value,
|
|
37
49
|
observe_value,
|
|
38
50
|
set_and_wait_for_value,
|
|
39
|
-
|
|
40
|
-
set_sim_put_proceeds,
|
|
41
|
-
set_sim_value,
|
|
42
|
-
soft_signal_r_and_backend,
|
|
51
|
+
soft_signal_r_and_setter,
|
|
43
52
|
soft_signal_rw,
|
|
44
53
|
wait_for_value,
|
|
45
54
|
)
|
|
46
55
|
from .signal_backend import SignalBackend
|
|
47
|
-
from .
|
|
56
|
+
from .soft_signal_backend import SoftSignalBackend
|
|
48
57
|
from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
|
|
49
58
|
from .utils import (
|
|
50
59
|
DEFAULT_TIMEOUT,
|
|
@@ -59,9 +68,15 @@ from .utils import (
|
|
|
59
68
|
)
|
|
60
69
|
|
|
61
70
|
__all__ = [
|
|
71
|
+
"assert_mock_put_called_with",
|
|
72
|
+
"callback_on_mock_put",
|
|
73
|
+
"mock_puts_blocked",
|
|
74
|
+
"set_mock_values",
|
|
75
|
+
"reset_mock_put_calls",
|
|
62
76
|
"SignalBackend",
|
|
63
|
-
"
|
|
77
|
+
"SoftSignalBackend",
|
|
64
78
|
"DetectorControl",
|
|
79
|
+
"MockSignalBackend",
|
|
65
80
|
"DetectorTrigger",
|
|
66
81
|
"DetectorWriter",
|
|
67
82
|
"StandardDetector",
|
|
@@ -73,15 +88,15 @@ __all__ = [
|
|
|
73
88
|
"SignalW",
|
|
74
89
|
"SignalRW",
|
|
75
90
|
"SignalX",
|
|
76
|
-
"
|
|
91
|
+
"soft_signal_r_and_setter",
|
|
77
92
|
"soft_signal_rw",
|
|
78
93
|
"observe_value",
|
|
79
94
|
"set_and_wait_for_value",
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"set_sim_value",
|
|
95
|
+
"set_mock_put_proceeds",
|
|
96
|
+
"set_mock_value",
|
|
83
97
|
"wait_for_value",
|
|
84
98
|
"AsyncStatus",
|
|
99
|
+
"WatchableAsyncStatus",
|
|
85
100
|
"DirectoryInfo",
|
|
86
101
|
"DirectoryProvider",
|
|
87
102
|
"NameProvider",
|
ophyd_async/core/async_status.py
CHANGED
|
@@ -2,28 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import asdict, replace
|
|
7
|
+
from typing import (
|
|
8
|
+
AsyncIterator,
|
|
9
|
+
Awaitable,
|
|
10
|
+
Callable,
|
|
11
|
+
Generic,
|
|
12
|
+
SupportsFloat,
|
|
13
|
+
Type,
|
|
14
|
+
TypeVar,
|
|
15
|
+
cast,
|
|
16
|
+
)
|
|
6
17
|
|
|
7
18
|
from bluesky.protocols import Status
|
|
8
19
|
|
|
9
|
-
from
|
|
20
|
+
from ..protocols import Watcher
|
|
21
|
+
from .utils import Callback, P, T, WatcherUpdate
|
|
10
22
|
|
|
23
|
+
AS = TypeVar("AS", bound="AsyncStatus")
|
|
24
|
+
WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
|
|
11
25
|
|
|
12
|
-
|
|
26
|
+
|
|
27
|
+
class AsyncStatusBase(Status):
|
|
13
28
|
"""Convert asyncio awaitable to bluesky Status interface"""
|
|
14
29
|
|
|
15
|
-
def __init__(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
watchers: Optional[List[Callable]] = None,
|
|
19
|
-
):
|
|
30
|
+
def __init__(self, awaitable: Awaitable, timeout: SupportsFloat | None = None):
|
|
31
|
+
if isinstance(timeout, SupportsFloat):
|
|
32
|
+
timeout = float(timeout)
|
|
20
33
|
if isinstance(awaitable, asyncio.Task):
|
|
21
34
|
self.task = awaitable
|
|
22
35
|
else:
|
|
23
|
-
self.task = asyncio.create_task(
|
|
36
|
+
self.task = asyncio.create_task(
|
|
37
|
+
asyncio.wait_for(awaitable, timeout=timeout)
|
|
38
|
+
)
|
|
24
39
|
self.task.add_done_callback(self._run_callbacks)
|
|
25
|
-
self._callbacks
|
|
26
|
-
self._watchers = watchers
|
|
40
|
+
self._callbacks: list[Callback[Status]] = []
|
|
27
41
|
|
|
28
42
|
def __await__(self):
|
|
29
43
|
return self.task.__await__()
|
|
@@ -39,15 +53,11 @@ class AsyncStatus(Status):
|
|
|
39
53
|
for callback in self._callbacks:
|
|
40
54
|
callback(self)
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
def exception( # type: ignore
|
|
44
|
-
self, timeout: Optional[float] = 0.0
|
|
45
|
-
) -> Optional[BaseException]:
|
|
56
|
+
def exception(self, timeout: float | None = 0.0) -> BaseException | None:
|
|
46
57
|
if timeout != 0.0:
|
|
47
|
-
raise
|
|
58
|
+
raise ValueError(
|
|
48
59
|
"cannot honour any timeout other than 0 in an asynchronous function"
|
|
49
60
|
)
|
|
50
|
-
|
|
51
61
|
if self.task.done():
|
|
52
62
|
try:
|
|
53
63
|
return self.task.exception()
|
|
@@ -67,22 +77,6 @@ class AsyncStatus(Status):
|
|
|
67
77
|
and self.task.exception() is None
|
|
68
78
|
)
|
|
69
79
|
|
|
70
|
-
def watch(self, watcher: Callable):
|
|
71
|
-
"""Add watcher to the list of interested parties.
|
|
72
|
-
|
|
73
|
-
Arguments as per Bluesky :external+bluesky:meth:`watch` protocol.
|
|
74
|
-
"""
|
|
75
|
-
if self._watchers is not None:
|
|
76
|
-
self._watchers.append(watcher)
|
|
77
|
-
|
|
78
|
-
@classmethod
|
|
79
|
-
def wrap(cls, f: Callable[[T], Coroutine]) -> Callable[[T], "AsyncStatus"]:
|
|
80
|
-
@functools.wraps(f)
|
|
81
|
-
def wrap_f(self) -> AsyncStatus:
|
|
82
|
-
return AsyncStatus(f(self))
|
|
83
|
-
|
|
84
|
-
return wrap_f
|
|
85
|
-
|
|
86
80
|
def __repr__(self) -> str:
|
|
87
81
|
if self.done:
|
|
88
82
|
if e := self.exception():
|
|
@@ -94,3 +88,72 @@ class AsyncStatus(Status):
|
|
|
94
88
|
return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
|
|
95
89
|
|
|
96
90
|
__str__ = __repr__
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AsyncStatus(AsyncStatusBase):
|
|
94
|
+
@classmethod
|
|
95
|
+
def wrap(cls: Type[AS], f: Callable[P, Awaitable]) -> Callable[P, AS]:
|
|
96
|
+
@functools.wraps(f)
|
|
97
|
+
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> AS:
|
|
98
|
+
# We can't type this more properly because Concatenate/ParamSpec doesn't
|
|
99
|
+
# yet support keywords
|
|
100
|
+
# https://peps.python.org/pep-0612/#concatenating-keyword-parameters
|
|
101
|
+
timeout = kwargs.get("timeout")
|
|
102
|
+
assert isinstance(timeout, SupportsFloat) or timeout is None
|
|
103
|
+
return cls(f(*args, **kwargs), timeout=timeout)
|
|
104
|
+
|
|
105
|
+
# type is actually functools._Wrapped[P, Awaitable, P, AS]
|
|
106
|
+
# but functools._Wrapped is not necessarily available
|
|
107
|
+
return cast(Callable[P, AS], wrap_f)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
111
|
+
"""Convert AsyncIterator of WatcherUpdates to bluesky Status interface."""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
iterator: AsyncIterator[WatcherUpdate[T]],
|
|
116
|
+
timeout: SupportsFloat | None = None,
|
|
117
|
+
):
|
|
118
|
+
self._watchers: list[Watcher] = []
|
|
119
|
+
self._start = time.monotonic()
|
|
120
|
+
self._last_update: WatcherUpdate[T] | None = None
|
|
121
|
+
super().__init__(self._notify_watchers_from(iterator), timeout)
|
|
122
|
+
|
|
123
|
+
async def _notify_watchers_from(self, iterator: AsyncIterator[WatcherUpdate[T]]):
|
|
124
|
+
async for update in iterator:
|
|
125
|
+
self._last_update = (
|
|
126
|
+
update
|
|
127
|
+
if update.time_elapsed is not None
|
|
128
|
+
else replace(update, time_elapsed=time.monotonic() - self._start)
|
|
129
|
+
)
|
|
130
|
+
for watcher in self._watchers:
|
|
131
|
+
self._update_watcher(watcher, self._last_update)
|
|
132
|
+
|
|
133
|
+
def _update_watcher(self, watcher: Watcher, update: WatcherUpdate[T]):
|
|
134
|
+
vals = asdict(
|
|
135
|
+
update, dict_factory=lambda d: {k: v for k, v in d if v is not None}
|
|
136
|
+
)
|
|
137
|
+
watcher(**vals)
|
|
138
|
+
|
|
139
|
+
def watch(self, watcher: Watcher):
|
|
140
|
+
self._watchers.append(watcher)
|
|
141
|
+
if self._last_update:
|
|
142
|
+
self._update_watcher(watcher, self._last_update)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def wrap(
|
|
146
|
+
cls: Type[WAS],
|
|
147
|
+
f: Callable[P, AsyncIterator[WatcherUpdate[T]]],
|
|
148
|
+
) -> Callable[P, WAS]:
|
|
149
|
+
"""Wrap an AsyncIterator in a WatchableAsyncStatus. If it takes
|
|
150
|
+
'timeout' as an argument, this must be a float or None, and it
|
|
151
|
+
will be propagated to the status."""
|
|
152
|
+
|
|
153
|
+
@functools.wraps(f)
|
|
154
|
+
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> WAS:
|
|
155
|
+
timeout = kwargs.get("timeout")
|
|
156
|
+
assert isinstance(timeout, SupportsFloat) or timeout is None
|
|
157
|
+
return cls(f(*args, **kwargs), timeout=timeout)
|
|
158
|
+
|
|
159
|
+
return cast(Callable[P, WAS], wrap_f)
|
ophyd_async/core/detector.py
CHANGED
|
@@ -31,9 +31,9 @@ from bluesky.protocols import (
|
|
|
31
31
|
|
|
32
32
|
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
|
|
33
33
|
|
|
34
|
-
from .async_status import AsyncStatus
|
|
34
|
+
from .async_status import AsyncStatus, WatchableAsyncStatus
|
|
35
35
|
from .device import Device
|
|
36
|
-
from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
|
|
36
|
+
from .utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts
|
|
37
37
|
|
|
38
38
|
T = TypeVar("T")
|
|
39
39
|
|
|
@@ -188,7 +188,7 @@ class StandardDetector(
|
|
|
188
188
|
self._trigger_info: Optional[TriggerInfo] = None
|
|
189
189
|
# For kickoff
|
|
190
190
|
self._watchers: List[Callable] = []
|
|
191
|
-
self._fly_status: Optional[
|
|
191
|
+
self._fly_status: Optional[WatchableAsyncStatus] = None
|
|
192
192
|
self._fly_start: float
|
|
193
193
|
|
|
194
194
|
self._intial_frame: int
|
|
@@ -292,43 +292,37 @@ class StandardDetector(
|
|
|
292
292
|
f"Detector {self.controller} needs at least {required}s deadtime, "
|
|
293
293
|
f"but trigger logic provides only {self._trigger_info.deadtime}s"
|
|
294
294
|
)
|
|
295
|
-
|
|
296
295
|
self._arm_status = await self.controller.arm(
|
|
297
296
|
num=self._trigger_info.num,
|
|
298
297
|
trigger=self._trigger_info.trigger,
|
|
299
298
|
exposure=self._trigger_info.livetime,
|
|
300
299
|
)
|
|
301
|
-
|
|
302
|
-
@AsyncStatus.wrap
|
|
303
|
-
async def kickoff(self) -> None:
|
|
304
|
-
self._fly_status = AsyncStatus(self._fly(), self._watchers)
|
|
305
300
|
self._fly_start = time.monotonic()
|
|
306
301
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
302
|
+
@AsyncStatus.wrap
|
|
303
|
+
async def kickoff(self):
|
|
304
|
+
if not self._arm_status:
|
|
305
|
+
raise Exception("Detector not armed!")
|
|
306
|
+
|
|
307
|
+
@WatchableAsyncStatus.wrap
|
|
308
|
+
async def complete(self):
|
|
309
|
+
assert self._arm_status, "Prepare not run"
|
|
310
|
+
assert self._trigger_info
|
|
311
311
|
async for index in self.writer.observe_indices_written(
|
|
312
312
|
self._frame_writing_timeout
|
|
313
313
|
):
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if index >= end_observation:
|
|
314
|
+
yield WatcherUpdate(
|
|
315
|
+
name=self.name,
|
|
316
|
+
current=index,
|
|
317
|
+
initial=self._initial_frame,
|
|
318
|
+
target=self._trigger_info.num,
|
|
319
|
+
unit="",
|
|
320
|
+
precision=0,
|
|
321
|
+
time_elapsed=time.monotonic() - self._fly_start,
|
|
322
|
+
)
|
|
323
|
+
if index >= self._trigger_info.num:
|
|
325
324
|
break
|
|
326
325
|
|
|
327
|
-
@AsyncStatus.wrap
|
|
328
|
-
async def complete(self) -> AsyncStatus:
|
|
329
|
-
assert self._fly_status, "Kickoff not run"
|
|
330
|
-
return await self._fly_status
|
|
331
|
-
|
|
332
326
|
async def describe_collect(self) -> Dict[str, DataKey]:
|
|
333
327
|
return self._describe
|
|
334
328
|
|
ophyd_async/core/device.py
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import sys
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from logging import LoggerAdapter, getLogger
|
|
6
9
|
from typing import (
|
|
7
10
|
Any,
|
|
8
11
|
Coroutine,
|
|
@@ -30,6 +33,9 @@ class Device(HasName):
|
|
|
30
33
|
_name: str = ""
|
|
31
34
|
#: The parent Device if it exists
|
|
32
35
|
parent: Optional[Device] = None
|
|
36
|
+
# None if connect hasn't started, a Task if it has
|
|
37
|
+
_connect_task: Optional[asyncio.Task] = None
|
|
38
|
+
_connect_mock_arg: bool = False
|
|
33
39
|
|
|
34
40
|
def __init__(self, name: str = "") -> None:
|
|
35
41
|
self.set_name(name)
|
|
@@ -39,6 +45,12 @@ class Device(HasName):
|
|
|
39
45
|
"""Return the name of the Device"""
|
|
40
46
|
return self._name
|
|
41
47
|
|
|
48
|
+
@cached_property
|
|
49
|
+
def log(self):
|
|
50
|
+
return LoggerAdapter(
|
|
51
|
+
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
52
|
+
)
|
|
53
|
+
|
|
42
54
|
def children(self) -> Iterator[Tuple[str, Device]]:
|
|
43
55
|
for attr_name, attr in self.__dict__.items():
|
|
44
56
|
if attr_name != "parent" and isinstance(attr, Device):
|
|
@@ -52,30 +64,54 @@ class Device(HasName):
|
|
|
52
64
|
name:
|
|
53
65
|
New name to set
|
|
54
66
|
"""
|
|
67
|
+
|
|
68
|
+
# Ensure self.log is recreated after a name change
|
|
69
|
+
if hasattr(self, "log"):
|
|
70
|
+
del self.log
|
|
71
|
+
|
|
55
72
|
self._name = name
|
|
56
73
|
for attr_name, child in self.children():
|
|
57
74
|
child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
|
|
58
75
|
child.set_name(child_name)
|
|
59
76
|
child.parent = self
|
|
60
77
|
|
|
61
|
-
async def connect(
|
|
78
|
+
async def connect(
|
|
79
|
+
self,
|
|
80
|
+
mock: bool = False,
|
|
81
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
82
|
+
force_reconnect: bool = False,
|
|
83
|
+
):
|
|
62
84
|
"""Connect self and all child Devices.
|
|
63
85
|
|
|
64
86
|
Contains a timeout that gets propagated to child.connect methods.
|
|
65
87
|
|
|
66
88
|
Parameters
|
|
67
89
|
----------
|
|
68
|
-
|
|
69
|
-
If True then
|
|
90
|
+
mock:
|
|
91
|
+
If True then use ``MockSignalBackend`` for all Signals
|
|
70
92
|
timeout:
|
|
71
93
|
Time to wait before failing with a TimeoutError.
|
|
72
94
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
# If previous connect with same args has started and not errored, can use it
|
|
96
|
+
can_use_previous_connect = (
|
|
97
|
+
self._connect_task
|
|
98
|
+
and not (self._connect_task.done() and self._connect_task.exception())
|
|
99
|
+
and self._connect_mock_arg == mock
|
|
100
|
+
)
|
|
101
|
+
if force_reconnect or not can_use_previous_connect:
|
|
102
|
+
# Kick off a connection
|
|
103
|
+
coros = {
|
|
104
|
+
name: child_device.connect(
|
|
105
|
+
mock, timeout=timeout, force_reconnect=force_reconnect
|
|
106
|
+
)
|
|
107
|
+
for name, child_device in self.children()
|
|
108
|
+
}
|
|
109
|
+
self._connect_task = asyncio.create_task(wait_for_connection(**coros))
|
|
110
|
+
self._connect_mock_arg = mock
|
|
111
|
+
|
|
112
|
+
assert self._connect_task, "Connect task not created, this shouldn't happen"
|
|
113
|
+
# Wait for it to complete
|
|
114
|
+
await self._connect_task
|
|
79
115
|
|
|
80
116
|
|
|
81
117
|
VT = TypeVar("VT", bound=Device)
|
|
@@ -105,9 +141,9 @@ class DeviceCollector:
|
|
|
105
141
|
If True, call ``device.set_name(variable_name)`` on all collected
|
|
106
142
|
Devices
|
|
107
143
|
connect:
|
|
108
|
-
If True, call ``device.connect(
|
|
144
|
+
If True, call ``device.connect(mock)`` in parallel on all
|
|
109
145
|
collected Devices
|
|
110
|
-
|
|
146
|
+
mock:
|
|
111
147
|
If True, connect Signals in simulation mode
|
|
112
148
|
timeout:
|
|
113
149
|
How long to wait for connect before logging an exception
|
|
@@ -129,12 +165,12 @@ class DeviceCollector:
|
|
|
129
165
|
self,
|
|
130
166
|
set_name=True,
|
|
131
167
|
connect=True,
|
|
132
|
-
|
|
168
|
+
mock=False,
|
|
133
169
|
timeout: float = 10.0,
|
|
134
170
|
):
|
|
135
171
|
self._set_name = set_name
|
|
136
172
|
self._connect = connect
|
|
137
|
-
self.
|
|
173
|
+
self._mock = mock
|
|
138
174
|
self._timeout = timeout
|
|
139
175
|
self._names_on_enter: Set[str] = set()
|
|
140
176
|
self._objects_on_exit: Dict[str, Any] = {}
|
|
@@ -168,7 +204,7 @@ class DeviceCollector:
|
|
|
168
204
|
obj.set_name(name)
|
|
169
205
|
if self._connect:
|
|
170
206
|
connect_coroutines[name] = obj.connect(
|
|
171
|
-
self.
|
|
207
|
+
self._mock, timeout=self._timeout
|
|
172
208
|
)
|
|
173
209
|
|
|
174
210
|
# Connect to all the devices
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from typing import Optional, Type
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
from bluesky.protocols import Descriptor, Reading
|
|
7
|
+
|
|
8
|
+
from ophyd_async.core.signal_backend import SignalBackend
|
|
9
|
+
from ophyd_async.core.soft_signal_backend import SoftSignalBackend
|
|
10
|
+
from ophyd_async.core.utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MockSignalBackend(SignalBackend):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
datatype: Optional[Type[T]] = None,
|
|
17
|
+
initial_backend: Optional[SignalBackend[T]] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
if isinstance(initial_backend, MockSignalBackend):
|
|
20
|
+
raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackends")
|
|
21
|
+
|
|
22
|
+
self.initial_backend = initial_backend
|
|
23
|
+
|
|
24
|
+
if datatype is None:
|
|
25
|
+
assert (
|
|
26
|
+
self.initial_backend
|
|
27
|
+
), "Must supply either initial_backend or datatype"
|
|
28
|
+
datatype = self.initial_backend.datatype
|
|
29
|
+
|
|
30
|
+
self.datatype = datatype
|
|
31
|
+
|
|
32
|
+
if not isinstance(self.initial_backend, SoftSignalBackend):
|
|
33
|
+
# If the backend is a hard signal backend, or not provided,
|
|
34
|
+
# then we create a soft signal to mimick it
|
|
35
|
+
|
|
36
|
+
self.soft_backend = SoftSignalBackend(datatype=datatype)
|
|
37
|
+
else:
|
|
38
|
+
self.soft_backend = initial_backend
|
|
39
|
+
|
|
40
|
+
def source(self, name: str) -> str:
|
|
41
|
+
if self.initial_backend:
|
|
42
|
+
return f"mock+{self.initial_backend.source(name)}"
|
|
43
|
+
return f"mock+{name}"
|
|
44
|
+
|
|
45
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def put_mock(self) -> Mock:
|
|
50
|
+
return Mock(name="put")
|
|
51
|
+
|
|
52
|
+
@cached_property
|
|
53
|
+
def put_proceeds(self) -> asyncio.Event:
|
|
54
|
+
put_proceeds = asyncio.Event()
|
|
55
|
+
put_proceeds.set()
|
|
56
|
+
return put_proceeds
|
|
57
|
+
|
|
58
|
+
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
59
|
+
self.put_mock(value, wait=wait, timeout=timeout)
|
|
60
|
+
await self.soft_backend.put(value, wait=wait, timeout=timeout)
|
|
61
|
+
|
|
62
|
+
if wait:
|
|
63
|
+
await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout)
|
|
64
|
+
|
|
65
|
+
def set_value(self, value: T):
|
|
66
|
+
self.soft_backend.set_value(value)
|
|
67
|
+
|
|
68
|
+
async def get_descriptor(self, source: str) -> Descriptor:
|
|
69
|
+
return await self.soft_backend.get_descriptor(source)
|
|
70
|
+
|
|
71
|
+
async def get_reading(self) -> Reading:
|
|
72
|
+
return await self.soft_backend.get_reading()
|
|
73
|
+
|
|
74
|
+
async def get_value(self) -> T:
|
|
75
|
+
return await self.soft_backend.get_value()
|
|
76
|
+
|
|
77
|
+
async def get_setpoint(self) -> T:
|
|
78
|
+
"""For a soft signal, the setpoint and readback values are the same."""
|
|
79
|
+
return await self.soft_backend.get_setpoint()
|
|
80
|
+
|
|
81
|
+
async def get_datakey(self, source: str) -> Descriptor:
|
|
82
|
+
return await self.soft_backend.get_datakey(source)
|
|
83
|
+
|
|
84
|
+
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
85
|
+
self.soft_backend.set_callback(callback)
|