ophyd-async 0.2.0__py3-none-any.whl → 0.3.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/__init__.py +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +52 -19
- ophyd_async/core/_providers.py +38 -5
- ophyd_async/core/async_status.py +86 -40
- ophyd_async/core/detector.py +214 -72
- ophyd_async/core/device.py +91 -50
- ophyd_async/core/device_save_loader.py +96 -23
- ophyd_async/core/flyer.py +32 -246
- ophyd_async/core/mock_signal_backend.py +82 -0
- ophyd_async/core/mock_signal_utils.py +145 -0
- ophyd_async/core/signal.py +225 -58
- ophyd_async/core/signal_backend.py +8 -5
- ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +51 -49
- ophyd_async/core/standard_readable.py +212 -23
- ophyd_async/core/utils.py +123 -30
- ophyd_async/epics/_backend/_aioca.py +42 -44
- ophyd_async/epics/_backend/_p4p.py +96 -52
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +8 -4
- ophyd_async/epics/areadetector/aravis.py +63 -0
- ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
- ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +37 -25
- ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +8 -12
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
- ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +8 -5
- ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
- ophyd_async/epics/areadetector/kinetix.py +46 -0
- ophyd_async/epics/areadetector/pilatus.py +45 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +14 -6
- ophyd_async/epics/areadetector/utils.py +2 -12
- ophyd_async/epics/areadetector/vimba.py +43 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
- ophyd_async/epics/areadetector/writers/hdf_writer.py +32 -17
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +19 -18
- ophyd_async/epics/areadetector/writers/nd_plugin.py +15 -7
- ophyd_async/epics/demo/__init__.py +75 -49
- ophyd_async/epics/motion/motor.py +67 -53
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +8 -3
- ophyd_async/epics/signal/signal.py +26 -9
- ophyd_async/log.py +130 -0
- ophyd_async/panda/__init__.py +21 -5
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/plan_stubs/__init__.py +13 -0
- ophyd_async/plan_stubs/ensure_connected.py +22 -0
- ophyd_async/plan_stubs/fly.py +149 -0
- ophyd_async/protocols.py +126 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +103 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +31 -70
- ophyd_async-0.3.0.dist-info/RECORD +86 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -294
- ophyd_async-0.2.0.dist-info/RECORD +0 -53
- /ophyd_async/panda/{table.py → _table.py} +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
2
|
+
from typing import Any, Callable, Iterable
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
from ophyd_async.core.signal import Signal
|
|
6
|
+
from ophyd_async.core.utils import T
|
|
7
|
+
|
|
8
|
+
from .mock_signal_backend import MockSignalBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
|
|
12
|
+
assert isinstance(signal._backend, MockSignalBackend), (
|
|
13
|
+
"Expected to receive a `MockSignalBackend`, instead "
|
|
14
|
+
f" received {type(signal._backend)}. "
|
|
15
|
+
)
|
|
16
|
+
return signal._backend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_mock_value(signal: Signal[T], value: T):
|
|
20
|
+
"""Set the value of a signal that is in mock mode."""
|
|
21
|
+
backend = _get_mock_signal_backend(signal)
|
|
22
|
+
backend.set_value(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_mock_put_proceeds(signal: Signal, proceeds: bool):
|
|
26
|
+
"""Allow or block a put with wait=True from proceeding"""
|
|
27
|
+
backend = _get_mock_signal_backend(signal)
|
|
28
|
+
|
|
29
|
+
if proceeds:
|
|
30
|
+
backend.put_proceeds.set()
|
|
31
|
+
else:
|
|
32
|
+
backend.put_proceeds.clear()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def mock_puts_blocked(*signals: Signal):
|
|
37
|
+
for signal in signals:
|
|
38
|
+
set_mock_put_proceeds(signal, False)
|
|
39
|
+
yield
|
|
40
|
+
for signal in signals:
|
|
41
|
+
set_mock_put_proceeds(signal, True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_mock_put(signal: Signal) -> Mock:
|
|
45
|
+
"""Get the mock associated with the put call on the signal."""
|
|
46
|
+
return _get_mock_signal_backend(signal).put_mock
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def reset_mock_put_calls(signal: Signal):
|
|
50
|
+
backend = _get_mock_signal_backend(signal)
|
|
51
|
+
backend.put_mock.reset_mock()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _SetValuesIterator:
|
|
55
|
+
# Garbage collected by the time __del__ is called unless we put it as a
|
|
56
|
+
# global attrbute here.
|
|
57
|
+
require_all_consumed: bool = False
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
signal: Signal,
|
|
62
|
+
values: Iterable[Any],
|
|
63
|
+
require_all_consumed: bool = False,
|
|
64
|
+
):
|
|
65
|
+
self.signal = signal
|
|
66
|
+
self.values = values
|
|
67
|
+
self.require_all_consumed = require_all_consumed
|
|
68
|
+
self.index = 0
|
|
69
|
+
|
|
70
|
+
self.iterator = enumerate(values, start=1)
|
|
71
|
+
|
|
72
|
+
def __iter__(self):
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __next__(self):
|
|
76
|
+
# Will propogate StopIteration
|
|
77
|
+
self.index, next_value = next(self.iterator)
|
|
78
|
+
set_mock_value(self.signal, next_value)
|
|
79
|
+
return next_value
|
|
80
|
+
|
|
81
|
+
def __del__(self):
|
|
82
|
+
if self.require_all_consumed and self.index != len(list(self.values)):
|
|
83
|
+
raise AssertionError("Not all values have been consumed.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def set_mock_values(
|
|
87
|
+
signal: Signal,
|
|
88
|
+
values: Iterable[Any],
|
|
89
|
+
require_all_consumed: bool = False,
|
|
90
|
+
) -> _SetValuesIterator:
|
|
91
|
+
"""Iterator to set a signal to a sequence of values, optionally repeating the
|
|
92
|
+
sequence.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
signal:
|
|
97
|
+
A signal with a `MockSignalBackend` backend.
|
|
98
|
+
values:
|
|
99
|
+
An iterable of the values to set the signal to, on each iteration
|
|
100
|
+
the value will be set.
|
|
101
|
+
require_all_consumed:
|
|
102
|
+
If True, an AssertionError will be raised if the iterator is deleted before
|
|
103
|
+
all values have been consumed.
|
|
104
|
+
|
|
105
|
+
Notes
|
|
106
|
+
-----
|
|
107
|
+
Example usage::
|
|
108
|
+
|
|
109
|
+
for value_set in set_mock_values(signal, [1, 2, 3]):
|
|
110
|
+
# do something
|
|
111
|
+
|
|
112
|
+
cm = set_mock_values(signal, 1, 2, 3, require_all_consumed=True):
|
|
113
|
+
next(cm)
|
|
114
|
+
# do something
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
return _SetValuesIterator(
|
|
118
|
+
signal,
|
|
119
|
+
values,
|
|
120
|
+
require_all_consumed=require_all_consumed,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@contextmanager
|
|
125
|
+
def _unset_side_effect_cm(put_mock: Mock):
|
|
126
|
+
yield
|
|
127
|
+
put_mock.side_effect = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def callback_on_mock_put(signal: Signal[T], callback: Callable[[T], None]):
|
|
131
|
+
"""For setting a callback when a backend is put to.
|
|
132
|
+
|
|
133
|
+
Can either be used in a context, with the callback being
|
|
134
|
+
unset on exit, or as an ordinary function.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
signal:
|
|
139
|
+
A signal with a `MockSignalBackend` backend.
|
|
140
|
+
callback:
|
|
141
|
+
The callback to call when the backend is put to during the context.
|
|
142
|
+
"""
|
|
143
|
+
backend = _get_mock_signal_backend(signal)
|
|
144
|
+
backend.put_mock.side_effect = callback
|
|
145
|
+
return _unset_side_effect_cm(backend.put_mock)
|
ophyd_async/core/signal.py
CHANGED
|
@@ -2,26 +2,37 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
AsyncGenerator,
|
|
8
|
+
Callable,
|
|
9
|
+
Dict,
|
|
10
|
+
Generic,
|
|
11
|
+
Mapping,
|
|
12
|
+
Optional,
|
|
13
|
+
Tuple,
|
|
14
|
+
Type,
|
|
15
|
+
Union,
|
|
16
|
+
)
|
|
6
17
|
|
|
7
18
|
from bluesky.protocols import (
|
|
8
|
-
|
|
19
|
+
DataKey,
|
|
9
20
|
Locatable,
|
|
10
21
|
Location,
|
|
11
22
|
Movable,
|
|
12
|
-
Readable,
|
|
13
23
|
Reading,
|
|
14
|
-
|
|
24
|
+
Status,
|
|
15
25
|
Subscribable,
|
|
16
26
|
)
|
|
17
27
|
|
|
28
|
+
from ophyd_async.core.mock_signal_backend import MockSignalBackend
|
|
29
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
|
|
30
|
+
|
|
18
31
|
from .async_status import AsyncStatus
|
|
19
32
|
from .device import Device
|
|
20
33
|
from .signal_backend import SignalBackend
|
|
21
|
-
from .
|
|
22
|
-
from .utils import DEFAULT_TIMEOUT,
|
|
23
|
-
|
|
24
|
-
_sim_backends: Dict[Signal, SimSignalBackend] = {}
|
|
34
|
+
from .soft_signal_backend import SoftSignalBackend
|
|
35
|
+
from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
|
|
25
36
|
|
|
26
37
|
|
|
27
38
|
def _add_timeout(func):
|
|
@@ -45,34 +56,30 @@ class Signal(Device, Generic[T]):
|
|
|
45
56
|
"""A Device with the concept of a value, with R, RW, W and X flavours"""
|
|
46
57
|
|
|
47
58
|
def __init__(
|
|
48
|
-
self,
|
|
59
|
+
self,
|
|
60
|
+
backend: SignalBackend[T],
|
|
61
|
+
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
62
|
+
name: str = "",
|
|
49
63
|
) -> None:
|
|
50
|
-
self._name = ""
|
|
51
64
|
self._timeout = timeout
|
|
52
|
-
self.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if sim:
|
|
63
|
-
self._backend = SimSignalBackend(
|
|
64
|
-
datatype=self._init_backend.datatype, source=self._init_backend.source
|
|
65
|
+
self._initial_backend = self._backend = backend
|
|
66
|
+
super().__init__(name)
|
|
67
|
+
|
|
68
|
+
async def connect(
|
|
69
|
+
self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
|
|
70
|
+
):
|
|
71
|
+
if mock and not isinstance(self._backend, MockSignalBackend):
|
|
72
|
+
# Using a soft backend, look to the initial value
|
|
73
|
+
self._backend = MockSignalBackend(
|
|
74
|
+
initial_backend=self._initial_backend,
|
|
65
75
|
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
self._backend = self._init_backend
|
|
69
|
-
_sim_backends.pop(self, None)
|
|
70
|
-
await self._backend.connect()
|
|
76
|
+
self.log.debug(f"Connecting to {self.source}")
|
|
77
|
+
await self._backend.connect(timeout=timeout)
|
|
71
78
|
|
|
72
79
|
@property
|
|
73
80
|
def source(self) -> str:
|
|
74
81
|
"""Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
|
|
75
|
-
return self._backend.source
|
|
82
|
+
return self._backend.source(self.name)
|
|
76
83
|
|
|
77
84
|
__lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail
|
|
78
85
|
|
|
@@ -91,10 +98,12 @@ class _SignalCache(Generic[T]):
|
|
|
91
98
|
self._value: Optional[T] = None
|
|
92
99
|
|
|
93
100
|
self.backend = backend
|
|
101
|
+
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
94
102
|
backend.set_callback(self._callback)
|
|
95
103
|
|
|
96
104
|
def close(self):
|
|
97
105
|
self.backend.set_callback(None)
|
|
106
|
+
self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
|
|
98
107
|
|
|
99
108
|
async def get_reading(self) -> Reading:
|
|
100
109
|
await self._valid.wait()
|
|
@@ -107,6 +116,10 @@ class _SignalCache(Generic[T]):
|
|
|
107
116
|
return self._value
|
|
108
117
|
|
|
109
118
|
def _callback(self, reading: Reading, value: T):
|
|
119
|
+
self._signal.log.debug(
|
|
120
|
+
f"Updated subscription: reading of source {self._signal.source} changed"
|
|
121
|
+
f"from {self._reading} to {reading}"
|
|
122
|
+
)
|
|
110
123
|
self._reading = reading
|
|
111
124
|
self._value = value
|
|
112
125
|
self._valid.set()
|
|
@@ -133,7 +146,7 @@ class _SignalCache(Generic[T]):
|
|
|
133
146
|
return self._staged or bool(self._listeners)
|
|
134
147
|
|
|
135
148
|
|
|
136
|
-
class SignalR(Signal[T],
|
|
149
|
+
class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
137
150
|
"""Signal that can be read from and monitored"""
|
|
138
151
|
|
|
139
152
|
_cache: Optional[_SignalCache] = None
|
|
@@ -166,14 +179,16 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
|
|
|
166
179
|
return {self.name: await self._backend_or_cache(cached).get_reading()}
|
|
167
180
|
|
|
168
181
|
@_add_timeout
|
|
169
|
-
async def describe(self) -> Dict[str,
|
|
182
|
+
async def describe(self) -> Dict[str, DataKey]:
|
|
170
183
|
"""Return a single item dict with the descriptor in it"""
|
|
171
|
-
return {self.name: await self._backend.
|
|
184
|
+
return {self.name: await self._backend.get_datakey(self.source)}
|
|
172
185
|
|
|
173
186
|
@_add_timeout
|
|
174
187
|
async def get_value(self, cached: Optional[bool] = None) -> T:
|
|
175
188
|
"""The current value"""
|
|
176
|
-
|
|
189
|
+
value = await self._backend_or_cache(cached).get_value()
|
|
190
|
+
self.log.debug(f"get_value() on source {self.source} returned {value}")
|
|
191
|
+
return value
|
|
177
192
|
|
|
178
193
|
def subscribe_value(self, function: Callback[T]):
|
|
179
194
|
"""Subscribe to updates in value of a device"""
|
|
@@ -198,18 +213,24 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
|
|
|
198
213
|
self._del_cache(self._get_cache().set_staged(False))
|
|
199
214
|
|
|
200
215
|
|
|
201
|
-
USE_DEFAULT_TIMEOUT = "USE_DEFAULT_TIMEOUT"
|
|
202
|
-
|
|
203
|
-
|
|
204
216
|
class SignalW(Signal[T], Movable):
|
|
205
217
|
"""Signal that can be set"""
|
|
206
218
|
|
|
207
|
-
def set(
|
|
219
|
+
def set(
|
|
220
|
+
self, value: T, wait=True, timeout: CalculatableTimeout = CalculateTimeout
|
|
221
|
+
) -> AsyncStatus:
|
|
208
222
|
"""Set the value and return a status saying when it's done"""
|
|
209
|
-
if timeout is
|
|
223
|
+
if timeout is CalculateTimeout:
|
|
210
224
|
timeout = self._timeout
|
|
211
|
-
|
|
212
|
-
|
|
225
|
+
|
|
226
|
+
async def do_set():
|
|
227
|
+
self.log.debug(f"Putting value {value} to backend at source {self.source}")
|
|
228
|
+
await self._backend.put(value, wait=wait, timeout=timeout)
|
|
229
|
+
self.log.debug(
|
|
230
|
+
f"Successfully put value {value} to backend at source {self.source}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return AsyncStatus(do_set())
|
|
213
234
|
|
|
214
235
|
|
|
215
236
|
class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
@@ -226,34 +247,156 @@ class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
|
226
247
|
class SignalX(Signal):
|
|
227
248
|
"""Signal that puts the default value"""
|
|
228
249
|
|
|
229
|
-
def trigger(
|
|
250
|
+
def trigger(
|
|
251
|
+
self, wait=True, timeout: CalculatableTimeout = CalculateTimeout
|
|
252
|
+
) -> AsyncStatus:
|
|
230
253
|
"""Trigger the action and return a status saying when it's done"""
|
|
231
|
-
if timeout is
|
|
254
|
+
if timeout is CalculateTimeout:
|
|
232
255
|
timeout = self._timeout
|
|
233
256
|
coro = self._backend.put(None, wait=wait, timeout=timeout)
|
|
234
257
|
return AsyncStatus(coro)
|
|
235
258
|
|
|
236
259
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
260
|
+
def soft_signal_rw(
|
|
261
|
+
datatype: Optional[Type[T]] = None,
|
|
262
|
+
initial_value: Optional[T] = None,
|
|
263
|
+
name: str = "",
|
|
264
|
+
) -> SignalRW[T]:
|
|
265
|
+
"""Creates a read-writable Signal with a SoftSignalBackend"""
|
|
266
|
+
signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name)
|
|
267
|
+
return signal
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def soft_signal_r_and_setter(
|
|
271
|
+
datatype: Optional[Type[T]] = None,
|
|
272
|
+
initial_value: Optional[T] = None,
|
|
273
|
+
name: str = "",
|
|
274
|
+
) -> Tuple[SignalR[T], Callable[[T], None]]:
|
|
275
|
+
"""Returns a tuple of a read-only Signal and a callable through
|
|
276
|
+
which the signal can be internally modified within the device. Use
|
|
277
|
+
soft_signal_rw if you want a device that is externally modifiable
|
|
278
|
+
"""
|
|
279
|
+
backend = SoftSignalBackend(datatype, initial_value)
|
|
280
|
+
signal = SignalR(backend, name=name)
|
|
240
281
|
|
|
282
|
+
return (signal, backend.set_value)
|
|
241
283
|
|
|
242
|
-
def set_sim_put_proceeds(signal: Signal[T], proceeds: bool):
|
|
243
|
-
"""Allow or block a put with wait=True from proceeding"""
|
|
244
|
-
event = _sim_backends[signal].put_proceeds
|
|
245
|
-
if proceeds:
|
|
246
|
-
event.set()
|
|
247
|
-
else:
|
|
248
|
-
event.clear()
|
|
249
284
|
|
|
285
|
+
def _generate_assert_error_msg(
|
|
286
|
+
name: str, expected_result: str, actuall_result: str
|
|
287
|
+
) -> str:
|
|
288
|
+
WARNING = "\033[93m"
|
|
289
|
+
FAIL = "\033[91m"
|
|
290
|
+
ENDC = "\033[0m"
|
|
291
|
+
return (
|
|
292
|
+
f"Expected {WARNING}{name}{ENDC} to produce"
|
|
293
|
+
+ f"\n{FAIL}{actuall_result}{ENDC}"
|
|
294
|
+
+ f"\nbut actually got \n{FAIL}{expected_result}{ENDC}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def assert_value(signal: SignalR[T], value: Any) -> None:
|
|
299
|
+
"""Assert a signal's value and compare it an expected signal.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
signal:
|
|
304
|
+
signal with get_value.
|
|
305
|
+
value:
|
|
306
|
+
The expected value from the signal.
|
|
307
|
+
|
|
308
|
+
Notes
|
|
309
|
+
-----
|
|
310
|
+
Example usage::
|
|
311
|
+
await assert_value(signal, value)
|
|
312
|
+
|
|
313
|
+
"""
|
|
314
|
+
actual_value = await signal.get_value()
|
|
315
|
+
assert actual_value == value, _generate_assert_error_msg(
|
|
316
|
+
signal.name, value, actual_value
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def assert_reading(
|
|
321
|
+
readable: AsyncReadable, expected_reading: Mapping[str, Reading]
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Assert readings from readable.
|
|
324
|
+
|
|
325
|
+
Parameters
|
|
326
|
+
----------
|
|
327
|
+
readable:
|
|
328
|
+
Callable with readable.read function that generate readings.
|
|
329
|
+
|
|
330
|
+
reading:
|
|
331
|
+
The expected readings from the readable.
|
|
332
|
+
|
|
333
|
+
Notes
|
|
334
|
+
-----
|
|
335
|
+
Example usage::
|
|
336
|
+
await assert_reading(readable, reading)
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
actual_reading = await readable.read()
|
|
340
|
+
assert expected_reading == actual_reading, _generate_assert_error_msg(
|
|
341
|
+
readable.name, expected_reading, actual_reading
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
async def assert_configuration(
|
|
346
|
+
configurable: AsyncConfigurable,
|
|
347
|
+
configuration: Mapping[str, Reading],
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Assert readings from Configurable.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
configurable:
|
|
354
|
+
Configurable with Configurable.read function that generate readings.
|
|
355
|
+
|
|
356
|
+
configuration:
|
|
357
|
+
The expected readings from configurable.
|
|
358
|
+
|
|
359
|
+
Notes
|
|
360
|
+
-----
|
|
361
|
+
Example usage::
|
|
362
|
+
await assert_configuration(configurable configuration)
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
actual_configurable = await configurable.read_configuration()
|
|
366
|
+
assert configuration == actual_configurable, _generate_assert_error_msg(
|
|
367
|
+
configurable.name, configuration, actual_configurable
|
|
368
|
+
)
|
|
250
369
|
|
|
251
|
-
def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> None:
|
|
252
|
-
"""Monitor the value of a signal that is in sim mode"""
|
|
253
|
-
return _sim_backends[signal].set_callback(callback)
|
|
254
370
|
|
|
371
|
+
def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
372
|
+
"""Assert emitted document generated by running a Bluesky plan
|
|
255
373
|
|
|
256
|
-
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
Doc:
|
|
377
|
+
A dictionary
|
|
378
|
+
|
|
379
|
+
numbers:
|
|
380
|
+
expected emission in kwarg from
|
|
381
|
+
|
|
382
|
+
Notes
|
|
383
|
+
-----
|
|
384
|
+
Example usage::
|
|
385
|
+
assert_emitted(docs, start=1, descriptor=1,
|
|
386
|
+
resource=1, datum=1, event=1, stop=1)
|
|
387
|
+
"""
|
|
388
|
+
assert list(docs) == list(numbers), _generate_assert_error_msg(
|
|
389
|
+
"documents", list(numbers), list(docs)
|
|
390
|
+
)
|
|
391
|
+
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
392
|
+
assert actual_numbers == numbers, _generate_assert_error_msg(
|
|
393
|
+
"emitted", numbers, actual_numbers
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def observe_value(
|
|
398
|
+
signal: SignalR[T], timeout: float | None = None, done_status: Status | None = None
|
|
399
|
+
) -> AsyncGenerator[T, None]:
|
|
257
400
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
258
401
|
|
|
259
402
|
Parameters
|
|
@@ -261,6 +404,12 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
|
261
404
|
signal:
|
|
262
405
|
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
263
406
|
end
|
|
407
|
+
timeout:
|
|
408
|
+
If given, how long to wait for each updated value in seconds. If an update
|
|
409
|
+
is not produced in this time then raise asyncio.TimeoutError
|
|
410
|
+
done_status:
|
|
411
|
+
If this status is complete, stop observing and make the iterator return.
|
|
412
|
+
If it raises an exception then this exception will be raised by the iterator.
|
|
264
413
|
|
|
265
414
|
Notes
|
|
266
415
|
-----
|
|
@@ -269,11 +418,29 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
|
269
418
|
async for value in observe_value(sig):
|
|
270
419
|
do_something_with(value)
|
|
271
420
|
"""
|
|
272
|
-
|
|
421
|
+
|
|
422
|
+
q: asyncio.Queue[T | Status] = asyncio.Queue()
|
|
423
|
+
if timeout is None:
|
|
424
|
+
get_value = q.get
|
|
425
|
+
else:
|
|
426
|
+
|
|
427
|
+
async def get_value():
|
|
428
|
+
return await asyncio.wait_for(q.get(), timeout)
|
|
429
|
+
|
|
430
|
+
if done_status is not None:
|
|
431
|
+
done_status.add_callback(q.put_nowait)
|
|
432
|
+
|
|
273
433
|
signal.subscribe_value(q.put_nowait)
|
|
274
434
|
try:
|
|
275
435
|
while True:
|
|
276
|
-
|
|
436
|
+
item = await get_value()
|
|
437
|
+
if done_status and item is done_status:
|
|
438
|
+
if exc := done_status.exception():
|
|
439
|
+
raise exc
|
|
440
|
+
else:
|
|
441
|
+
break
|
|
442
|
+
else:
|
|
443
|
+
yield item
|
|
277
444
|
finally:
|
|
278
445
|
signal.clear_sub(q.put_nowait)
|
|
279
446
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
from typing import Generic, Optional, Type
|
|
3
3
|
|
|
4
|
-
from bluesky.protocols import
|
|
4
|
+
from bluesky.protocols import DataKey, Reading
|
|
5
5
|
|
|
6
|
-
from .utils import ReadingValueCallback, T
|
|
6
|
+
from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class SignalBackend(Generic[T]):
|
|
@@ -13,10 +13,13 @@ class SignalBackend(Generic[T]):
|
|
|
13
13
|
datatype: Optional[Type[T]] = None
|
|
14
14
|
|
|
15
15
|
#: Like ca://PV_PREFIX:SIGNAL
|
|
16
|
-
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def source(self, name: str) -> str:
|
|
18
|
+
"""Return source of signal. Signals may pass a name to the backend, which can be
|
|
19
|
+
used or discarded."""
|
|
17
20
|
|
|
18
21
|
@abstractmethod
|
|
19
|
-
async def connect(self):
|
|
22
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
|
|
20
23
|
"""Connect to underlying hardware"""
|
|
21
24
|
|
|
22
25
|
@abstractmethod
|
|
@@ -24,7 +27,7 @@ class SignalBackend(Generic[T]):
|
|
|
24
27
|
"""Put a value to the PV, if wait then wait for completion for up to timeout"""
|
|
25
28
|
|
|
26
29
|
@abstractmethod
|
|
27
|
-
async def
|
|
30
|
+
async def get_datakey(self, source: str) -> DataKey:
|
|
28
31
|
"""Metadata like source, dtype, shape, precision, units"""
|
|
29
32
|
|
|
30
33
|
@abstractmethod
|