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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
2
|
+
from typing import Any, Callable, Generator, Iterable, Iterator, List
|
|
3
|
+
from unittest.mock import ANY, 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[T], 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: List[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 assert_mock_put_called_with(signal: Signal, value: Any, wait=ANY, timeout=ANY):
|
|
45
|
+
backend = _get_mock_signal_backend(signal)
|
|
46
|
+
backend.put_mock.assert_called_with(value, wait=wait, timeout=timeout)
|
|
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(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
|
+
) -> Iterator[Any]:
|
|
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
|
+
# linting isn't smart enought to realize @contextmanager will give use a
|
|
131
|
+
# ContextManager[None]
|
|
132
|
+
def callback_on_mock_put(
|
|
133
|
+
signal: Signal, callback: Callable[[T], None]
|
|
134
|
+
) -> Generator[None, None, None]:
|
|
135
|
+
"""For setting a callback when a backend is put to.
|
|
136
|
+
|
|
137
|
+
Can either be used in a context, with the callback being
|
|
138
|
+
unset on exit, or as an ordinary function.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
signal:
|
|
143
|
+
A signal with a `MockSignalBackend` backend.
|
|
144
|
+
callback:
|
|
145
|
+
The callback to call when the backend is put to during the context.
|
|
146
|
+
"""
|
|
147
|
+
backend = _get_mock_signal_backend(signal)
|
|
148
|
+
backend.put_mock.side_effect = callback
|
|
149
|
+
return _unset_side_effect_cm(backend.put_mock)
|
ophyd_async/core/signal.py
CHANGED
|
@@ -21,18 +21,18 @@ from bluesky.protocols import (
|
|
|
21
21
|
Location,
|
|
22
22
|
Movable,
|
|
23
23
|
Reading,
|
|
24
|
+
Status,
|
|
24
25
|
Subscribable,
|
|
25
26
|
)
|
|
26
27
|
|
|
28
|
+
from ophyd_async.core.mock_signal_backend import MockSignalBackend
|
|
27
29
|
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
|
|
28
30
|
|
|
29
31
|
from .async_status import AsyncStatus
|
|
30
32
|
from .device import Device
|
|
31
33
|
from .signal_backend import SignalBackend
|
|
32
|
-
from .
|
|
33
|
-
from .utils import DEFAULT_TIMEOUT, Callback,
|
|
34
|
-
|
|
35
|
-
_sim_backends: Dict[Signal, SimSignalBackend] = {}
|
|
34
|
+
from .soft_signal_backend import SoftSignalBackend
|
|
35
|
+
from .utils import DEFAULT_TIMEOUT, Callback, T
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _add_timeout(func):
|
|
@@ -61,17 +61,19 @@ class Signal(Device, Generic[T]):
|
|
|
61
61
|
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
62
62
|
name: str = "",
|
|
63
63
|
) -> None:
|
|
64
|
-
super().__init__(name)
|
|
65
64
|
self._timeout = timeout
|
|
66
|
-
self.
|
|
65
|
+
self._initial_backend = self._backend = backend
|
|
66
|
+
super().__init__(name)
|
|
67
67
|
|
|
68
|
-
async def connect(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
self._backend =
|
|
74
|
-
|
|
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,
|
|
75
|
+
)
|
|
76
|
+
self.log.debug(f"Connecting to {self.source}")
|
|
75
77
|
await self._backend.connect(timeout=timeout)
|
|
76
78
|
|
|
77
79
|
@property
|
|
@@ -96,10 +98,12 @@ class _SignalCache(Generic[T]):
|
|
|
96
98
|
self._value: Optional[T] = None
|
|
97
99
|
|
|
98
100
|
self.backend = backend
|
|
101
|
+
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
99
102
|
backend.set_callback(self._callback)
|
|
100
103
|
|
|
101
104
|
def close(self):
|
|
102
105
|
self.backend.set_callback(None)
|
|
106
|
+
self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
|
|
103
107
|
|
|
104
108
|
async def get_reading(self) -> Reading:
|
|
105
109
|
await self._valid.wait()
|
|
@@ -112,6 +116,10 @@ class _SignalCache(Generic[T]):
|
|
|
112
116
|
return self._value
|
|
113
117
|
|
|
114
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
|
+
)
|
|
115
123
|
self._reading = reading
|
|
116
124
|
self._value = value
|
|
117
125
|
self._valid.set()
|
|
@@ -178,7 +186,9 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
178
186
|
@_add_timeout
|
|
179
187
|
async def get_value(self, cached: Optional[bool] = None) -> T:
|
|
180
188
|
"""The current value"""
|
|
181
|
-
|
|
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
|
|
182
192
|
|
|
183
193
|
def subscribe_value(self, function: Callback[T]):
|
|
184
194
|
"""Subscribe to updates in value of a device"""
|
|
@@ -213,8 +223,15 @@ class SignalW(Signal[T], Movable):
|
|
|
213
223
|
"""Set the value and return a status saying when it's done"""
|
|
214
224
|
if timeout is USE_DEFAULT_TIMEOUT:
|
|
215
225
|
timeout = self._timeout
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
|
|
227
|
+
async def do_set():
|
|
228
|
+
self.log.debug(f"Putting value {value} to backend at source {self.source}")
|
|
229
|
+
await self._backend.put(value, wait=wait, timeout=timeout)
|
|
230
|
+
self.log.debug(
|
|
231
|
+
f"Successfully put value {value} to backend at source {self.source}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return AsyncStatus(do_set())
|
|
218
235
|
|
|
219
236
|
|
|
220
237
|
class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
@@ -239,47 +256,42 @@ class SignalX(Signal):
|
|
|
239
256
|
return AsyncStatus(coro)
|
|
240
257
|
|
|
241
258
|
|
|
242
|
-
def set_sim_value(signal: Signal[T], value: T):
|
|
243
|
-
"""Set the value of a signal that is in sim mode."""
|
|
244
|
-
_sim_backends[signal]._set_value(value)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def set_sim_put_proceeds(signal: Signal[T], proceeds: bool):
|
|
248
|
-
"""Allow or block a put with wait=True from proceeding"""
|
|
249
|
-
event = _sim_backends[signal].put_proceeds
|
|
250
|
-
if proceeds:
|
|
251
|
-
event.set()
|
|
252
|
-
else:
|
|
253
|
-
event.clear()
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> None:
|
|
257
|
-
"""Monitor the value of a signal that is in sim mode"""
|
|
258
|
-
return _sim_backends[signal].set_callback(callback)
|
|
259
|
-
|
|
260
|
-
|
|
261
259
|
def soft_signal_rw(
|
|
262
260
|
datatype: Optional[Type[T]] = None,
|
|
263
261
|
initial_value: Optional[T] = None,
|
|
264
262
|
name: str = "",
|
|
265
263
|
) -> SignalRW[T]:
|
|
266
|
-
"""Creates a read-writable Signal with a
|
|
267
|
-
signal = SignalRW(
|
|
264
|
+
"""Creates a read-writable Signal with a SoftSignalBackend"""
|
|
265
|
+
signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name)
|
|
268
266
|
return signal
|
|
269
267
|
|
|
270
268
|
|
|
271
|
-
def
|
|
269
|
+
def soft_signal_r_and_setter(
|
|
272
270
|
datatype: Optional[Type[T]] = None,
|
|
273
271
|
initial_value: Optional[T] = None,
|
|
274
272
|
name: str = "",
|
|
275
|
-
) -> Tuple[SignalR[T],
|
|
276
|
-
"""Returns a tuple of a read-only Signal and
|
|
273
|
+
) -> Tuple[SignalR[T], Callable[[T]]]:
|
|
274
|
+
"""Returns a tuple of a read-only Signal and a callable through
|
|
277
275
|
which the signal can be internally modified within the device. Use
|
|
278
276
|
soft_signal_rw if you want a device that is externally modifiable
|
|
279
277
|
"""
|
|
280
|
-
backend =
|
|
278
|
+
backend = SoftSignalBackend(datatype, initial_value)
|
|
281
279
|
signal = SignalR(backend, name=name)
|
|
282
|
-
|
|
280
|
+
|
|
281
|
+
return (signal, backend.set_value)
|
|
282
|
+
|
|
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
|
+
)
|
|
283
295
|
|
|
284
296
|
|
|
285
297
|
async def assert_value(signal: SignalR[T], value: Any) -> None:
|
|
@@ -298,11 +310,14 @@ async def assert_value(signal: SignalR[T], value: Any) -> None:
|
|
|
298
310
|
await assert_value(signal, value)
|
|
299
311
|
|
|
300
312
|
"""
|
|
301
|
-
|
|
313
|
+
actual_value = await signal.get_value()
|
|
314
|
+
assert actual_value == value, _generate_assert_error_msg(
|
|
315
|
+
signal.name, value, actual_value
|
|
316
|
+
)
|
|
302
317
|
|
|
303
318
|
|
|
304
319
|
async def assert_reading(
|
|
305
|
-
readable: AsyncReadable,
|
|
320
|
+
readable: AsyncReadable, expected_reading: Mapping[str, Reading]
|
|
306
321
|
) -> None:
|
|
307
322
|
"""Assert readings from readable.
|
|
308
323
|
|
|
@@ -320,7 +335,10 @@ async def assert_reading(
|
|
|
320
335
|
await assert_reading(readable, reading)
|
|
321
336
|
|
|
322
337
|
"""
|
|
323
|
-
|
|
338
|
+
actual_reading = await readable.read()
|
|
339
|
+
assert expected_reading == actual_reading, _generate_assert_error_msg(
|
|
340
|
+
readable.name, expected_reading, actual_reading
|
|
341
|
+
)
|
|
324
342
|
|
|
325
343
|
|
|
326
344
|
async def assert_configuration(
|
|
@@ -343,7 +361,10 @@ async def assert_configuration(
|
|
|
343
361
|
await assert_configuration(configurable configuration)
|
|
344
362
|
|
|
345
363
|
"""
|
|
346
|
-
|
|
364
|
+
actual_configurable = await configurable.read_configuration()
|
|
365
|
+
assert configuration == actual_configurable, _generate_assert_error_msg(
|
|
366
|
+
configurable.name, configuration, actual_configurable
|
|
367
|
+
)
|
|
347
368
|
|
|
348
369
|
|
|
349
370
|
def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
@@ -363,11 +384,18 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
|
363
384
|
assert_emitted(docs, start=1, descriptor=1,
|
|
364
385
|
resource=1, datum=1, event=1, stop=1)
|
|
365
386
|
"""
|
|
366
|
-
assert list(docs) == list(numbers)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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]:
|
|
371
399
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
372
400
|
|
|
373
401
|
Parameters
|
|
@@ -375,6 +403,8 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
|
|
|
375
403
|
signal:
|
|
376
404
|
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
377
405
|
end
|
|
406
|
+
done_status:
|
|
407
|
+
If this status is complete, stop observing and make the iterator return.
|
|
378
408
|
|
|
379
409
|
Notes
|
|
380
410
|
-----
|
|
@@ -383,7 +413,10 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
|
|
|
383
413
|
async for value in observe_value(sig):
|
|
384
414
|
do_something_with(value)
|
|
385
415
|
"""
|
|
386
|
-
|
|
416
|
+
|
|
417
|
+
class StatusIsDone: ...
|
|
418
|
+
|
|
419
|
+
q: asyncio.Queue[T | StatusIsDone] = asyncio.Queue()
|
|
387
420
|
if timeout is None:
|
|
388
421
|
get_value = q.get
|
|
389
422
|
else:
|
|
@@ -391,10 +424,17 @@ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, N
|
|
|
391
424
|
async def get_value():
|
|
392
425
|
return await asyncio.wait_for(q.get(), timeout)
|
|
393
426
|
|
|
427
|
+
if done_status is not None:
|
|
428
|
+
done_status.add_callback(lambda _: q.put_nowait(StatusIsDone()))
|
|
429
|
+
|
|
394
430
|
signal.subscribe_value(q.put_nowait)
|
|
395
431
|
try:
|
|
396
432
|
while True:
|
|
397
|
-
|
|
433
|
+
item = await get_value()
|
|
434
|
+
if not isinstance(item, StatusIsDone):
|
|
435
|
+
yield item
|
|
436
|
+
else:
|
|
437
|
+
break
|
|
398
438
|
finally:
|
|
399
439
|
signal.clear_sub(q.put_nowait)
|
|
400
440
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import inspect
|
|
5
4
|
import time
|
|
6
5
|
from collections import abc
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from enum import Enum
|
|
9
|
-
from typing import
|
|
8
|
+
from typing import Dict, Generic, Optional, Type, Union, cast, get_origin
|
|
10
9
|
|
|
11
10
|
import numpy as np
|
|
12
11
|
from bluesky.protocols import DataKey, Dtype, Reading
|
|
@@ -22,7 +21,7 @@ primitive_dtypes: Dict[type, Dtype] = {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
class
|
|
24
|
+
class SoftConverter(Generic[T]):
|
|
26
25
|
def value(self, value: T) -> T:
|
|
27
26
|
return value
|
|
28
27
|
|
|
@@ -55,7 +54,7 @@ class SimConverter(Generic[T]):
|
|
|
55
54
|
return datatype()
|
|
56
55
|
|
|
57
56
|
|
|
58
|
-
class
|
|
57
|
+
class SoftArrayConverter(SoftConverter):
|
|
59
58
|
def get_datakey(self, source: str, value) -> DataKey:
|
|
60
59
|
return {"source": source, "dtype": "array", "shape": [len(value)]}
|
|
61
60
|
|
|
@@ -70,7 +69,7 @@ class SimArrayConverter(SimConverter):
|
|
|
70
69
|
|
|
71
70
|
|
|
72
71
|
@dataclass
|
|
73
|
-
class
|
|
72
|
+
class SoftEnumConverter(SoftConverter):
|
|
74
73
|
enum_class: Type[Enum]
|
|
75
74
|
|
|
76
75
|
def write_value(self, value: Union[Enum, str]) -> Enum:
|
|
@@ -90,26 +89,21 @@ class SimEnumConverter(SimConverter):
|
|
|
90
89
|
return cast(T, list(datatype.__members__.values())[0]) # type: ignore
|
|
91
90
|
|
|
92
91
|
|
|
93
|
-
class DisconnectedSimConverter(SimConverter):
|
|
94
|
-
def __getattribute__(self, __name: str) -> Any:
|
|
95
|
-
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
96
|
-
|
|
97
|
-
|
|
98
92
|
def make_converter(datatype):
|
|
99
93
|
is_array = get_dtype(datatype) is not None
|
|
100
94
|
is_sequence = get_origin(datatype) == abc.Sequence
|
|
101
95
|
is_enum = issubclass(datatype, Enum) if inspect.isclass(datatype) else False
|
|
102
96
|
|
|
103
97
|
if is_array or is_sequence:
|
|
104
|
-
return
|
|
98
|
+
return SoftArrayConverter()
|
|
105
99
|
if is_enum:
|
|
106
|
-
return
|
|
100
|
+
return SoftEnumConverter(datatype)
|
|
107
101
|
|
|
108
|
-
return
|
|
102
|
+
return SoftConverter()
|
|
109
103
|
|
|
110
104
|
|
|
111
|
-
class
|
|
112
|
-
"""An
|
|
105
|
+
class SoftSignalBackend(SignalBackend[T]):
|
|
106
|
+
"""An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
|
|
113
107
|
|
|
114
108
|
_value: T
|
|
115
109
|
_initial_value: Optional[T]
|
|
@@ -122,25 +116,23 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
122
116
|
initial_value: Optional[T] = None,
|
|
123
117
|
) -> None:
|
|
124
118
|
self.datatype = datatype
|
|
125
|
-
self.converter: SimConverter = DisconnectedSimConverter()
|
|
126
119
|
self._initial_value = initial_value
|
|
127
|
-
self.
|
|
128
|
-
self.put_proceeds.set()
|
|
129
|
-
self.callback: Optional[ReadingValueCallback[T]] = None
|
|
130
|
-
|
|
131
|
-
def source(self, name: str) -> str:
|
|
132
|
-
return f"soft://{name}"
|
|
133
|
-
|
|
134
|
-
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
135
|
-
self.converter = make_converter(self.datatype)
|
|
120
|
+
self.converter: SoftConverter = make_converter(datatype)
|
|
136
121
|
if self._initial_value is None:
|
|
137
122
|
self._initial_value = self.converter.make_initial_value(self.datatype)
|
|
138
123
|
else:
|
|
139
|
-
# convert potentially unconverted initial value passed to init method
|
|
140
124
|
self._initial_value = self.converter.write_value(self._initial_value)
|
|
125
|
+
|
|
126
|
+
self.callback: Optional[ReadingValueCallback[T]] = None
|
|
141
127
|
self._severity = 0
|
|
128
|
+
self.set_value(self._initial_value)
|
|
142
129
|
|
|
143
|
-
|
|
130
|
+
def source(self, name: str) -> str:
|
|
131
|
+
return f"soft://{name}"
|
|
132
|
+
|
|
133
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
134
|
+
"""Connection isn't required for soft signals."""
|
|
135
|
+
pass
|
|
144
136
|
|
|
145
137
|
async def put(self, value: Optional[T], wait=True, timeout=None):
|
|
146
138
|
write_value = (
|
|
@@ -148,13 +140,11 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
148
140
|
if value is not None
|
|
149
141
|
else self._initial_value
|
|
150
142
|
)
|
|
151
|
-
self._set_value(write_value)
|
|
152
143
|
|
|
153
|
-
|
|
154
|
-
await asyncio.wait_for(self.put_proceeds.wait(), timeout)
|
|
144
|
+
self.set_value(write_value)
|
|
155
145
|
|
|
156
|
-
def
|
|
157
|
-
"""Method to bypass asynchronous logic
|
|
146
|
+
def set_value(self, value: T):
|
|
147
|
+
"""Method to bypass asynchronous logic."""
|
|
158
148
|
self._value = value
|
|
159
149
|
self._timestamp = time.monotonic()
|
|
160
150
|
reading: Reading = self.converter.reading(
|
|
@@ -174,7 +164,7 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
174
164
|
return self.converter.value(self._value)
|
|
175
165
|
|
|
176
166
|
async def get_setpoint(self) -> T:
|
|
177
|
-
"""For a
|
|
167
|
+
"""For a soft signal, the setpoint and readback values are the same."""
|
|
178
168
|
return await self.get_value()
|
|
179
169
|
|
|
180
170
|
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
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
|
|
|
@@ -28,7 +28,7 @@ from ophyd_async.core import (
|
|
|
28
28
|
)
|
|
29
29
|
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
|
|
30
30
|
|
|
31
|
-
from .common import
|
|
31
|
+
from .common import get_supported_values
|
|
32
32
|
|
|
33
33
|
dbr_to_dtype: Dict[Dbr, Dtype] = {
|
|
34
34
|
dbr.DBR_STRING: "string",
|
|
@@ -79,7 +79,7 @@ class CaArrayConverter(CaConverter):
|
|
|
79
79
|
|
|
80
80
|
@dataclass
|
|
81
81
|
class CaEnumConverter(CaConverter):
|
|
82
|
-
|
|
82
|
+
choices: dict[str, str]
|
|
83
83
|
|
|
84
84
|
def write_value(self, value: Union[Enum, str]):
|
|
85
85
|
if isinstance(value, Enum):
|
|
@@ -88,11 +88,15 @@ class CaEnumConverter(CaConverter):
|
|
|
88
88
|
return value
|
|
89
89
|
|
|
90
90
|
def value(self, value: AugmentedValue):
|
|
91
|
-
return self.
|
|
91
|
+
return self.choices[value]
|
|
92
92
|
|
|
93
93
|
def get_datakey(self, source: str, value: AugmentedValue) -> DataKey:
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
return {
|
|
95
|
+
"source": source,
|
|
96
|
+
"dtype": "string",
|
|
97
|
+
"shape": [],
|
|
98
|
+
"choices": list(self.choices),
|
|
99
|
+
}
|
|
96
100
|
|
|
97
101
|
|
|
98
102
|
class DisconnectedCaConverter(CaConverter):
|
|
@@ -138,8 +142,8 @@ def make_converter(
|
|
|
138
142
|
pv_choices = get_unique(
|
|
139
143
|
{k: tuple(v.enums) for k, v in values.items()}, "choices"
|
|
140
144
|
)
|
|
141
|
-
|
|
142
|
-
return CaEnumConverter(dbr.DBR_STRING, None,
|
|
145
|
+
supported_values = get_supported_values(pv, datatype, pv_choices)
|
|
146
|
+
return CaEnumConverter(dbr.DBR_STRING, None, supported_values)
|
|
143
147
|
else:
|
|
144
148
|
value = list(values.values())[0]
|
|
145
149
|
# Done the dbr check, so enough to check one of the values
|