ophyd-async 0.3a4__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 +2 -1
- ophyd_async/core/async_status.py +96 -35
- ophyd_async/core/detector.py +22 -28
- ophyd_async/core/device.py +30 -7
- ophyd_async/core/mock_signal_backend.py +14 -15
- ophyd_async/core/mock_signal_utils.py +7 -7
- ophyd_async/core/signal.py +56 -12
- ophyd_async/core/utils.py +19 -0
- ophyd_async/epics/areadetector/aravis.py +1 -5
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +6 -1
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +2 -120
- ophyd_async/epics/demo/__init__.py +27 -32
- ophyd_async/epics/motion/motor.py +39 -40
- ophyd_async/epics/pvi/pvi.py +2 -2
- ophyd_async/epics/signal/__init__.py +8 -1
- 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 +47 -32
- {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a5.dist-info}/METADATA +1 -1
- {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a5.dist-info}/RECORD +26 -25
- {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a5.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a5.dist-info}/WHEEL +0 -0
- {ophyd_async-0.3a4.dist-info → ophyd_async-0.3a5.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.3a4.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,
|
|
@@ -96,6 +96,7 @@ __all__ = [
|
|
|
96
96
|
"set_mock_value",
|
|
97
97
|
"wait_for_value",
|
|
98
98
|
"AsyncStatus",
|
|
99
|
+
"WatchableAsyncStatus",
|
|
99
100
|
"DirectoryInfo",
|
|
100
101
|
"DirectoryProvider",
|
|
101
102
|
"NameProvider",
|
ophyd_async/core/async_status.py
CHANGED
|
@@ -2,30 +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(
|
|
24
|
-
|
|
36
|
+
self.task = asyncio.create_task(
|
|
37
|
+
asyncio.wait_for(awaitable, timeout=timeout)
|
|
38
|
+
)
|
|
25
39
|
self.task.add_done_callback(self._run_callbacks)
|
|
26
|
-
|
|
27
|
-
self._callbacks = cast(List[Callback[Status]], [])
|
|
28
|
-
self._watchers = watchers
|
|
40
|
+
self._callbacks: list[Callback[Status]] = []
|
|
29
41
|
|
|
30
42
|
def __await__(self):
|
|
31
43
|
return self.task.__await__()
|
|
@@ -41,15 +53,11 @@ class AsyncStatus(Status):
|
|
|
41
53
|
for callback in self._callbacks:
|
|
42
54
|
callback(self)
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
def exception( # type: ignore
|
|
46
|
-
self, timeout: Optional[float] = 0.0
|
|
47
|
-
) -> Optional[BaseException]:
|
|
56
|
+
def exception(self, timeout: float | None = 0.0) -> BaseException | None:
|
|
48
57
|
if timeout != 0.0:
|
|
49
|
-
raise
|
|
58
|
+
raise ValueError(
|
|
50
59
|
"cannot honour any timeout other than 0 in an asynchronous function"
|
|
51
60
|
)
|
|
52
|
-
|
|
53
61
|
if self.task.done():
|
|
54
62
|
try:
|
|
55
63
|
return self.task.exception()
|
|
@@ -69,22 +77,6 @@ class AsyncStatus(Status):
|
|
|
69
77
|
and self.task.exception() is None
|
|
70
78
|
)
|
|
71
79
|
|
|
72
|
-
def watch(self, watcher: Callable):
|
|
73
|
-
"""Add watcher to the list of interested parties.
|
|
74
|
-
|
|
75
|
-
Arguments as per Bluesky :external+bluesky:meth:`watch` protocol.
|
|
76
|
-
"""
|
|
77
|
-
if self._watchers is not None:
|
|
78
|
-
self._watchers.append(watcher)
|
|
79
|
-
|
|
80
|
-
@classmethod
|
|
81
|
-
def wrap(cls, f: Callable[[T], Coroutine]) -> Callable[[T], "AsyncStatus"]:
|
|
82
|
-
@functools.wraps(f)
|
|
83
|
-
def wrap_f(self) -> AsyncStatus:
|
|
84
|
-
return AsyncStatus(f(self))
|
|
85
|
-
|
|
86
|
-
return wrap_f
|
|
87
|
-
|
|
88
80
|
def __repr__(self) -> str:
|
|
89
81
|
if self.done:
|
|
90
82
|
if e := self.exception():
|
|
@@ -96,3 +88,72 @@ class AsyncStatus(Status):
|
|
|
96
88
|
return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
|
|
97
89
|
|
|
98
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,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import sys
|
|
6
7
|
from functools import cached_property
|
|
7
8
|
from logging import LoggerAdapter, getLogger
|
|
@@ -32,6 +33,9 @@ class Device(HasName):
|
|
|
32
33
|
_name: str = ""
|
|
33
34
|
#: The parent Device if it exists
|
|
34
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
|
|
35
39
|
|
|
36
40
|
def __init__(self, name: str = "") -> None:
|
|
37
41
|
self.set_name(name)
|
|
@@ -71,7 +75,12 @@ class Device(HasName):
|
|
|
71
75
|
child.set_name(child_name)
|
|
72
76
|
child.parent = self
|
|
73
77
|
|
|
74
|
-
async def connect(
|
|
78
|
+
async def connect(
|
|
79
|
+
self,
|
|
80
|
+
mock: bool = False,
|
|
81
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
82
|
+
force_reconnect: bool = False,
|
|
83
|
+
):
|
|
75
84
|
"""Connect self and all child Devices.
|
|
76
85
|
|
|
77
86
|
Contains a timeout that gets propagated to child.connect methods.
|
|
@@ -83,12 +92,26 @@ class Device(HasName):
|
|
|
83
92
|
timeout:
|
|
84
93
|
Time to wait before failing with a TimeoutError.
|
|
85
94
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
92
115
|
|
|
93
116
|
|
|
94
117
|
VT = TypeVar("VT", bound=Device)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from functools import cached_property
|
|
2
3
|
from typing import Optional, Type
|
|
3
|
-
from unittest.mock import
|
|
4
|
+
from unittest.mock import Mock
|
|
4
5
|
|
|
5
6
|
from bluesky.protocols import Descriptor, Reading
|
|
6
7
|
|
|
@@ -36,51 +37,49 @@ class MockSignalBackend(SignalBackend):
|
|
|
36
37
|
else:
|
|
37
38
|
self.soft_backend = initial_backend
|
|
38
39
|
|
|
39
|
-
self.mock = MagicMock()
|
|
40
|
-
|
|
41
|
-
self.put_proceeds = asyncio.Event()
|
|
42
|
-
self.put_proceeds.set()
|
|
43
|
-
|
|
44
40
|
def source(self, name: str) -> str:
|
|
45
|
-
self.mock.source(name)
|
|
46
41
|
if self.initial_backend:
|
|
47
42
|
return f"mock+{self.initial_backend.source(name)}"
|
|
48
43
|
return f"mock+{name}"
|
|
49
44
|
|
|
50
45
|
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
51
|
-
|
|
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
|
|
52
57
|
|
|
53
58
|
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
54
|
-
self.
|
|
59
|
+
self.put_mock(value, wait=wait, timeout=timeout)
|
|
55
60
|
await self.soft_backend.put(value, wait=wait, timeout=timeout)
|
|
56
61
|
|
|
57
62
|
if wait:
|
|
58
63
|
await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout)
|
|
59
64
|
|
|
60
65
|
def set_value(self, value: T):
|
|
61
|
-
self.mock.set_value(value)
|
|
62
66
|
self.soft_backend.set_value(value)
|
|
63
67
|
|
|
64
68
|
async def get_descriptor(self, source: str) -> Descriptor:
|
|
65
|
-
self.mock.get_descriptor(source)
|
|
66
69
|
return await self.soft_backend.get_descriptor(source)
|
|
67
70
|
|
|
68
71
|
async def get_reading(self) -> Reading:
|
|
69
|
-
self.mock.get_reading()
|
|
70
72
|
return await self.soft_backend.get_reading()
|
|
71
73
|
|
|
72
74
|
async def get_value(self) -> T:
|
|
73
|
-
self.mock.get_value()
|
|
74
75
|
return await self.soft_backend.get_value()
|
|
75
76
|
|
|
76
77
|
async def get_setpoint(self) -> T:
|
|
77
78
|
"""For a soft signal, the setpoint and readback values are the same."""
|
|
78
|
-
self.mock.get_setpoint()
|
|
79
79
|
return await self.soft_backend.get_setpoint()
|
|
80
80
|
|
|
81
81
|
async def get_datakey(self, source: str) -> Descriptor:
|
|
82
82
|
return await self.soft_backend.get_datakey(source)
|
|
83
83
|
|
|
84
84
|
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
85
|
-
self.mock.set_callback(callback)
|
|
86
85
|
self.soft_backend.set_callback(callback)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager, contextmanager
|
|
2
2
|
from typing import Any, Callable, Generator, Iterable, Iterator, List
|
|
3
|
-
from unittest.mock import ANY
|
|
3
|
+
from unittest.mock import ANY, Mock
|
|
4
4
|
|
|
5
5
|
from ophyd_async.core.signal import Signal
|
|
6
6
|
from ophyd_async.core.utils import T
|
|
@@ -43,12 +43,12 @@ async def mock_puts_blocked(*signals: List[Signal]):
|
|
|
43
43
|
|
|
44
44
|
def assert_mock_put_called_with(signal: Signal, value: Any, wait=ANY, timeout=ANY):
|
|
45
45
|
backend = _get_mock_signal_backend(signal)
|
|
46
|
-
backend.
|
|
46
|
+
backend.put_mock.assert_called_with(value, wait=wait, timeout=timeout)
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def reset_mock_put_calls(signal: Signal):
|
|
50
50
|
backend = _get_mock_signal_backend(signal)
|
|
51
|
-
backend.
|
|
51
|
+
backend.put_mock.reset_mock()
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class _SetValuesIterator:
|
|
@@ -122,9 +122,9 @@ def set_mock_values(
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
@contextmanager
|
|
125
|
-
def _unset_side_effect_cm(
|
|
125
|
+
def _unset_side_effect_cm(put_mock: Mock):
|
|
126
126
|
yield
|
|
127
|
-
|
|
127
|
+
put_mock.side_effect = None
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
# linting isn't smart enought to realize @contextmanager will give use a
|
|
@@ -145,5 +145,5 @@ def callback_on_mock_put(
|
|
|
145
145
|
The callback to call when the backend is put to during the context.
|
|
146
146
|
"""
|
|
147
147
|
backend = _get_mock_signal_backend(signal)
|
|
148
|
-
backend.
|
|
149
|
-
return _unset_side_effect_cm(backend.
|
|
148
|
+
backend.put_mock.side_effect = callback
|
|
149
|
+
return _unset_side_effect_cm(backend.put_mock)
|
ophyd_async/core/signal.py
CHANGED
|
@@ -21,6 +21,7 @@ from bluesky.protocols import (
|
|
|
21
21
|
Location,
|
|
22
22
|
Movable,
|
|
23
23
|
Reading,
|
|
24
|
+
Status,
|
|
24
25
|
Subscribable,
|
|
25
26
|
)
|
|
26
27
|
|
|
@@ -64,7 +65,9 @@ class Signal(Device, Generic[T]):
|
|
|
64
65
|
self._initial_backend = self._backend = backend
|
|
65
66
|
super().__init__(name)
|
|
66
67
|
|
|
67
|
-
async def connect(
|
|
68
|
+
async def connect(
|
|
69
|
+
self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
|
|
70
|
+
):
|
|
68
71
|
if mock and not isinstance(self._backend, MockSignalBackend):
|
|
69
72
|
# Using a soft backend, look to the initial value
|
|
70
73
|
self._backend = MockSignalBackend(
|
|
@@ -278,6 +281,19 @@ def soft_signal_r_and_setter(
|
|
|
278
281
|
return (signal, backend.set_value)
|
|
279
282
|
|
|
280
283
|
|
|
284
|
+
def _generate_assert_error_msg(
|
|
285
|
+
name: str, expected_result: str, actuall_result: str
|
|
286
|
+
) -> str:
|
|
287
|
+
WARNING = "\033[93m"
|
|
288
|
+
FAIL = "\033[91m"
|
|
289
|
+
ENDC = "\033[0m"
|
|
290
|
+
return (
|
|
291
|
+
f"Expected {WARNING}{name}{ENDC} to produce"
|
|
292
|
+
+ f"\n{FAIL}{actuall_result}{ENDC}"
|
|
293
|
+
+ f"\nbut actually got \n{FAIL}{expected_result}{ENDC}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
281
297
|
async def assert_value(signal: SignalR[T], value: Any) -> None:
|
|
282
298
|
"""Assert a signal's value and compare it an expected signal.
|
|
283
299
|
|
|
@@ -294,11 +310,14 @@ async def assert_value(signal: SignalR[T], value: Any) -> None:
|
|
|
294
310
|
await assert_value(signal, value)
|
|
295
311
|
|
|
296
312
|
"""
|
|
297
|
-
|
|
313
|
+
actual_value = await signal.get_value()
|
|
314
|
+
assert actual_value == value, _generate_assert_error_msg(
|
|
315
|
+
signal.name, value, actual_value
|
|
316
|
+
)
|
|
298
317
|
|
|
299
318
|
|
|
300
319
|
async def assert_reading(
|
|
301
|
-
readable: AsyncReadable,
|
|
320
|
+
readable: AsyncReadable, expected_reading: Mapping[str, Reading]
|
|
302
321
|
) -> None:
|
|
303
322
|
"""Assert readings from readable.
|
|
304
323
|
|
|
@@ -316,7 +335,10 @@ async def assert_reading(
|
|
|
316
335
|
await assert_reading(readable, reading)
|
|
317
336
|
|
|
318
337
|
"""
|
|
319
|
-
|
|
338
|
+
actual_reading = await readable.read()
|
|
339
|
+
assert expected_reading == actual_reading, _generate_assert_error_msg(
|
|
340
|
+
readable.name, expected_reading, actual_reading
|
|
341
|
+
)
|
|
320
342
|
|
|
321
343
|
|
|
322
344
|
async def assert_configuration(
|
|
@@ -339,7 +361,10 @@ async def assert_configuration(
|
|
|
339
361
|
await assert_configuration(configurable configuration)
|
|
340
362
|
|
|
341
363
|
"""
|
|
342
|
-
|
|
364
|
+
actual_configurable = await configurable.read_configuration()
|
|
365
|
+
assert configuration == actual_configurable, _generate_assert_error_msg(
|
|
366
|
+
configurable.name, configuration, actual_configurable
|
|
367
|
+
)
|
|
343
368
|
|
|
344
369
|
|
|
345
370
|
def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
@@ -359,11 +384,18 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
|
359
384
|
assert_emitted(docs, start=1, descriptor=1,
|
|
360
385
|
resource=1, datum=1, event=1, stop=1)
|
|
361
386
|
"""
|
|
362
|
-
assert list(docs) == list(numbers)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
387
|
+
assert list(docs) == list(numbers), _generate_assert_error_msg(
|
|
388
|
+
"documents", list(numbers), list(docs)
|
|
389
|
+
)
|
|
390
|
+
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
391
|
+
assert actual_numbers == numbers, _generate_assert_error_msg(
|
|
392
|
+
"emitted", numbers, actual_numbers
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
async def observe_value(
|
|
397
|
+
signal: SignalR[T], timeout=None, done_status: Status | None = None
|
|
398
|
+
) -> AsyncGenerator[T, None]:
|
|
367
399
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
368
400
|
|
|
369
401
|
Parameters
|
|
@@ -371,6 +403,8 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
|
|
|
371
403
|
signal:
|
|
372
404
|
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
373
405
|
end
|
|
406
|
+
done_status:
|
|
407
|
+
If this status is complete, stop observing and make the iterator return.
|
|
374
408
|
|
|
375
409
|
Notes
|
|
376
410
|
-----
|
|
@@ -379,7 +413,10 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
|
|
|
379
413
|
async for value in observe_value(sig):
|
|
380
414
|
do_something_with(value)
|
|
381
415
|
"""
|
|
382
|
-
|
|
416
|
+
|
|
417
|
+
class StatusIsDone: ...
|
|
418
|
+
|
|
419
|
+
q: asyncio.Queue[T | StatusIsDone] = asyncio.Queue()
|
|
383
420
|
if timeout is None:
|
|
384
421
|
get_value = q.get
|
|
385
422
|
else:
|
|
@@ -387,10 +424,17 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
|
|
|
387
424
|
async def get_value():
|
|
388
425
|
return await asyncio.wait_for(q.get(), timeout)
|
|
389
426
|
|
|
427
|
+
if done_status is not None:
|
|
428
|
+
done_status.add_callback(lambda _: q.put_nowait(StatusIsDone()))
|
|
429
|
+
|
|
390
430
|
signal.subscribe_value(q.put_nowait)
|
|
391
431
|
try:
|
|
392
432
|
while True:
|
|
393
|
-
|
|
433
|
+
item = await get_value()
|
|
434
|
+
if not isinstance(item, StatusIsDone):
|
|
435
|
+
yield item
|
|
436
|
+
else:
|
|
437
|
+
break
|
|
394
438
|
finally:
|
|
395
439
|
signal.clear_sub(q.put_nowait)
|
|
396
440
|
|
ophyd_async/core/utils.py
CHANGED
|
@@ -2,13 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from typing import (
|
|
6
7
|
Awaitable,
|
|
7
8
|
Callable,
|
|
8
9
|
Dict,
|
|
10
|
+
Generic,
|
|
9
11
|
Iterable,
|
|
10
12
|
List,
|
|
11
13
|
Optional,
|
|
14
|
+
ParamSpec,
|
|
12
15
|
Type,
|
|
13
16
|
TypeVar,
|
|
14
17
|
Union,
|
|
@@ -18,6 +21,7 @@ import numpy as np
|
|
|
18
21
|
from bluesky.protocols import Reading
|
|
19
22
|
|
|
20
23
|
T = TypeVar("T")
|
|
24
|
+
P = ParamSpec("P")
|
|
21
25
|
Callback = Callable[[T], None]
|
|
22
26
|
|
|
23
27
|
#: A function that will be called with the Reading and value when the
|
|
@@ -77,6 +81,21 @@ class NotConnected(Exception):
|
|
|
77
81
|
return self.format_error_string(indent="")
|
|
78
82
|
|
|
79
83
|
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class WatcherUpdate(Generic[T]):
|
|
86
|
+
"""A dataclass such that, when expanded, it provides the kwargs for a watcher"""
|
|
87
|
+
|
|
88
|
+
current: T
|
|
89
|
+
initial: T
|
|
90
|
+
target: T
|
|
91
|
+
name: str | None = None
|
|
92
|
+
unit: str | None = None
|
|
93
|
+
precision: float | None = None
|
|
94
|
+
fraction: float | None = None
|
|
95
|
+
time_elapsed: float | None = None
|
|
96
|
+
time_remaining: float | None = None
|
|
97
|
+
|
|
98
|
+
|
|
80
99
|
async def wait_for_connection(**coros: Awaitable[None]):
|
|
81
100
|
"""Call many underlying signals, accumulating exceptions and returning them
|
|
82
101
|
|
|
@@ -2,7 +2,7 @@ from typing import get_args
|
|
|
2
2
|
|
|
3
3
|
from bluesky.protocols import HasHints, Hints
|
|
4
4
|
|
|
5
|
-
from ophyd_async.core import DirectoryProvider, StandardDetector
|
|
5
|
+
from ophyd_async.core import DirectoryProvider, StandardDetector
|
|
6
6
|
from ophyd_async.epics.areadetector.controllers.aravis_controller import (
|
|
7
7
|
AravisController,
|
|
8
8
|
)
|
|
@@ -45,10 +45,6 @@ class AravisDetector(StandardDetector, HasHints):
|
|
|
45
45
|
name=name,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
async def _prepare(self, value: TriggerInfo) -> None:
|
|
49
|
-
await self.drv.fetch_deadtime()
|
|
50
|
-
await super()._prepare(value)
|
|
51
|
-
|
|
52
48
|
def get_external_trigger_gpio(self):
|
|
53
49
|
return self._controller.gpio_number
|
|
54
50
|
|
|
@@ -14,6 +14,11 @@ from ophyd_async.epics.areadetector.drivers.aravis_driver import (
|
|
|
14
14
|
)
|
|
15
15
|
from ophyd_async.epics.areadetector.utils import ImageMode, stop_busy_record
|
|
16
16
|
|
|
17
|
+
# The deadtime of an ADaravis controller varies depending on the exact model of camera.
|
|
18
|
+
# Ideally we would maximize performance by dynamically retrieving the deadtime at
|
|
19
|
+
# runtime. See https://github.com/bluesky/ophyd-async/issues/308
|
|
20
|
+
_HIGHEST_POSSIBLE_DEADTIME = 1961e-6
|
|
21
|
+
|
|
17
22
|
|
|
18
23
|
class AravisController(DetectorControl):
|
|
19
24
|
GPIO_NUMBER = Literal[1, 2, 3, 4]
|
|
@@ -23,7 +28,7 @@ class AravisController(DetectorControl):
|
|
|
23
28
|
self.gpio_number = gpio_number
|
|
24
29
|
|
|
25
30
|
def get_deadtime(self, exposure: float) -> float:
|
|
26
|
-
return
|
|
31
|
+
return _HIGHEST_POSSIBLE_DEADTIME
|
|
27
32
|
|
|
28
33
|
async def arm(
|
|
29
34
|
self,
|