ophyd-async 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +34 -9
- ophyd_async/core/_detector.py +5 -10
- ophyd_async/core/_device.py +170 -68
- ophyd_async/core/_device_filler.py +269 -0
- ophyd_async/core/_device_save_loader.py +6 -7
- ophyd_async/core/_mock_signal_backend.py +35 -40
- ophyd_async/core/_mock_signal_utils.py +25 -16
- ophyd_async/core/_protocol.py +28 -8
- ophyd_async/core/_readable.py +133 -134
- ophyd_async/core/_signal.py +219 -163
- ophyd_async/core/_signal_backend.py +131 -64
- ophyd_async/core/_soft_signal_backend.py +131 -194
- ophyd_async/core/_status.py +22 -6
- ophyd_async/core/_table.py +102 -100
- ophyd_async/core/_utils.py +143 -32
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adaravis/_aravis_io.py +8 -6
- ophyd_async/epics/adcore/_core_io.py +5 -7
- ophyd_async/epics/adcore/_core_logic.py +3 -1
- ophyd_async/epics/adcore/_hdf_writer.py +2 -2
- ophyd_async/epics/adcore/_single_trigger.py +6 -10
- ophyd_async/epics/adcore/_utils.py +15 -10
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
- ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
- ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
- ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
- ophyd_async/epics/advimba/__init__.py +4 -1
- ophyd_async/epics/advimba/_vimba_controller.py +6 -3
- ophyd_async/epics/advimba/_vimba_io.py +8 -9
- ophyd_async/epics/core/__init__.py +26 -0
- ophyd_async/epics/core/_aioca.py +323 -0
- ophyd_async/epics/core/_epics_connector.py +53 -0
- ophyd_async/epics/core/_epics_device.py +13 -0
- ophyd_async/epics/core/_p4p.py +383 -0
- ophyd_async/epics/core/_pvi_connector.py +91 -0
- ophyd_async/epics/core/_signal.py +171 -0
- ophyd_async/epics/core/_util.py +61 -0
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/_sensor.py +14 -13
- ophyd_async/epics/eiger/_eiger.py +1 -2
- ophyd_async/epics/eiger/_eiger_controller.py +7 -2
- ophyd_async/epics/eiger/_eiger_io.py +3 -5
- ophyd_async/epics/eiger/_odin_io.py +5 -5
- ophyd_async/epics/motor.py +4 -5
- ophyd_async/epics/signal.py +11 -0
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +105 -0
- ophyd_async/epics/testing/_utils.py +78 -0
- ophyd_async/epics/testing/test_records.db +152 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +9 -0
- ophyd_async/fastcs/panda/__init__.py +4 -4
- ophyd_async/fastcs/panda/_block.py +18 -13
- ophyd_async/fastcs/panda/_control.py +3 -5
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
- ophyd_async/fastcs/panda/_table.py +30 -52
- ophyd_async/fastcs/panda/_trigger.py +8 -8
- ophyd_async/fastcs/panda/_writer.py +2 -5
- ophyd_async/plan_stubs/_ensure_connected.py +20 -13
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +5 -4
- ophyd_async/py.typed +0 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
- ophyd_async/sim/demo/_sim_motor.py +3 -4
- ophyd_async/tango/__init__.py +0 -45
- ophyd_async/tango/{signal → core}/__init__.py +9 -6
- ophyd_async/tango/core/_base_device.py +132 -0
- ophyd_async/tango/{signal → core}/_signal.py +42 -53
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +38 -40
- ophyd_async/tango/demo/_counter.py +12 -23
- ophyd_async/tango/demo/_mover.py +13 -13
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/METADATA +52 -55
- ophyd_async-0.8.0.dist-info/RECORD +116 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/WHEEL +1 -1
- ophyd_async/epics/pvi/__init__.py +0 -3
- ophyd_async/epics/pvi/_pvi.py +0 -338
- ophyd_async/epics/signal/__init__.py +0 -21
- ophyd_async/epics/signal/_aioca.py +0 -378
- ophyd_async/epics/signal/_common.py +0 -57
- ophyd_async/epics/signal/_epics_transport.py +0 -34
- ophyd_async/epics/signal/_p4p.py +0 -518
- ophyd_async/epics/signal/_signal.py +0 -114
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async/tango/base_devices/_base_device.py +0 -225
- ophyd_async-0.7.0.dist-info/RECORD +0 -108
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/top_level.txt +0 -0
ophyd_async/core/_signal.py
CHANGED
|
@@ -2,128 +2,107 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
from collections.abc import AsyncGenerator, Callable, Mapping
|
|
6
|
-
from typing import Any, Generic,
|
|
5
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
|
|
6
|
+
from typing import Any, Generic, cast
|
|
7
7
|
|
|
8
8
|
from bluesky.protocols import (
|
|
9
9
|
Locatable,
|
|
10
10
|
Location,
|
|
11
11
|
Movable,
|
|
12
|
-
Reading,
|
|
13
12
|
Status,
|
|
14
13
|
Subscribable,
|
|
15
14
|
)
|
|
16
15
|
from event_model import DataKey
|
|
17
16
|
|
|
18
|
-
from ._device import Device
|
|
17
|
+
from ._device import Device, DeviceConnector
|
|
19
18
|
from ._mock_signal_backend import MockSignalBackend
|
|
20
|
-
from ._protocol import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
from ._protocol import (
|
|
20
|
+
AsyncConfigurable,
|
|
21
|
+
AsyncReadable,
|
|
22
|
+
AsyncStageable,
|
|
23
|
+
Reading,
|
|
24
|
+
)
|
|
25
|
+
from ._signal_backend import (
|
|
26
|
+
SignalBackend,
|
|
27
|
+
SignalDatatypeT,
|
|
28
|
+
SignalDatatypeV,
|
|
29
|
+
)
|
|
30
|
+
from ._soft_signal_backend import SoftSignalBackend
|
|
31
|
+
from ._status import AsyncStatus, completed_status
|
|
32
|
+
from ._utils import (
|
|
33
|
+
CALCULATE_TIMEOUT,
|
|
34
|
+
DEFAULT_TIMEOUT,
|
|
35
|
+
CalculatableTimeout,
|
|
36
|
+
Callback,
|
|
37
|
+
LazyMock,
|
|
38
|
+
T,
|
|
39
|
+
)
|
|
40
|
+
|
|
25
41
|
|
|
26
|
-
|
|
42
|
+
async def _wait_for(coro: Awaitable[T], timeout: float | None, source: str) -> T:
|
|
43
|
+
try:
|
|
44
|
+
return await asyncio.wait_for(coro, timeout)
|
|
45
|
+
except asyncio.TimeoutError as e:
|
|
46
|
+
raise asyncio.TimeoutError(source) from e
|
|
27
47
|
|
|
28
48
|
|
|
29
49
|
def _add_timeout(func):
|
|
30
50
|
@functools.wraps(func)
|
|
31
51
|
async def wrapper(self: Signal, *args, **kwargs):
|
|
32
|
-
return await
|
|
52
|
+
return await _wait_for(func(self, *args, **kwargs), self._timeout, self.source)
|
|
33
53
|
|
|
34
54
|
return wrapper
|
|
35
55
|
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
class SignalConnector(DeviceConnector):
|
|
58
|
+
def __init__(self, backend: SignalBackend):
|
|
59
|
+
self.backend = self._init_backend = backend
|
|
39
60
|
|
|
61
|
+
async def connect_mock(self, device: Device, mock: LazyMock):
|
|
62
|
+
self.backend = MockSignalBackend(self._init_backend, mock)
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
65
|
+
self.backend = self._init_backend
|
|
66
|
+
device.log.debug(f"Connecting to {self.backend.source(device.name, read=True)}")
|
|
67
|
+
await self.backend.connect(timeout)
|
|
45
68
|
|
|
46
69
|
|
|
47
|
-
|
|
70
|
+
class _ChildrenNotAllowed(dict[str, Device]):
|
|
71
|
+
def __setitem__(self, key: str, value: Device) -> None:
|
|
72
|
+
raise AttributeError(
|
|
73
|
+
f"Cannot add Device or Signal child {key}={value} of Signal, "
|
|
74
|
+
"make a subclass of Device instead"
|
|
75
|
+
)
|
|
48
76
|
|
|
49
77
|
|
|
50
|
-
class Signal(Device, Generic[
|
|
78
|
+
class Signal(Device, Generic[SignalDatatypeT]):
|
|
51
79
|
"""A Device with the concept of a value, with R, RW, W and X flavours"""
|
|
52
80
|
|
|
81
|
+
_connector: SignalConnector
|
|
82
|
+
_child_devices = _ChildrenNotAllowed() # type: ignore
|
|
83
|
+
|
|
53
84
|
def __init__(
|
|
54
85
|
self,
|
|
55
|
-
backend: SignalBackend[
|
|
86
|
+
backend: SignalBackend[SignalDatatypeT],
|
|
56
87
|
timeout: float | None = DEFAULT_TIMEOUT,
|
|
57
88
|
name: str = "",
|
|
58
89
|
) -> None:
|
|
90
|
+
super().__init__(name=name, connector=SignalConnector(backend))
|
|
59
91
|
self._timeout = timeout
|
|
60
|
-
self._backend = backend
|
|
61
|
-
super().__init__(name)
|
|
62
|
-
|
|
63
|
-
async def connect(
|
|
64
|
-
self,
|
|
65
|
-
mock=False,
|
|
66
|
-
timeout=DEFAULT_TIMEOUT,
|
|
67
|
-
force_reconnect: bool = False,
|
|
68
|
-
backend: SignalBackend[T] | None = None,
|
|
69
|
-
):
|
|
70
|
-
if backend:
|
|
71
|
-
if (
|
|
72
|
-
self._backend is not DISCONNECTED_BACKEND
|
|
73
|
-
and backend is not self._backend
|
|
74
|
-
):
|
|
75
|
-
raise ValueError("Backend at connection different from previous one.")
|
|
76
|
-
|
|
77
|
-
self._backend = backend
|
|
78
|
-
if (
|
|
79
|
-
self._previous_connect_was_mock is not None
|
|
80
|
-
and self._previous_connect_was_mock != mock
|
|
81
|
-
):
|
|
82
|
-
raise RuntimeError(
|
|
83
|
-
f"`connect(mock={mock})` called on a `Signal` where the previous "
|
|
84
|
-
f"connect was `mock={self._previous_connect_was_mock}`. Changing mock "
|
|
85
|
-
"value between connects is not permitted."
|
|
86
|
-
)
|
|
87
|
-
self._previous_connect_was_mock = mock
|
|
88
|
-
|
|
89
|
-
if mock and not issubclass(type(self._backend), MockSignalBackend):
|
|
90
|
-
# Using a soft backend, look to the initial value
|
|
91
|
-
self._backend = MockSignalBackend(initial_backend=self._backend)
|
|
92
|
-
|
|
93
|
-
if self._backend is None:
|
|
94
|
-
raise RuntimeError("`connect` called on signal without backend")
|
|
95
|
-
|
|
96
|
-
can_use_previous_connection: bool = self._connect_task is not None and not (
|
|
97
|
-
self._connect_task.done() and self._connect_task.exception()
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
if force_reconnect or not can_use_previous_connection:
|
|
101
|
-
self.log.debug(f"Connecting to {self.source}")
|
|
102
|
-
self._connect_task = asyncio.create_task(
|
|
103
|
-
self._backend.connect(timeout=timeout)
|
|
104
|
-
)
|
|
105
|
-
else:
|
|
106
|
-
self.log.debug(f"Reusing previous connection to {self.source}")
|
|
107
|
-
assert (
|
|
108
|
-
self._connect_task
|
|
109
|
-
), "this assert is for type analysis and will never fail"
|
|
110
|
-
await self._connect_task
|
|
111
92
|
|
|
112
93
|
@property
|
|
113
94
|
def source(self) -> str:
|
|
114
95
|
"""Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
|
|
115
|
-
return self.
|
|
96
|
+
return self._connector.backend.source(self.name, read=True)
|
|
116
97
|
|
|
117
98
|
|
|
118
|
-
class _SignalCache(Generic[
|
|
119
|
-
def __init__(self, backend: SignalBackend[
|
|
99
|
+
class _SignalCache(Generic[SignalDatatypeT]):
|
|
100
|
+
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal):
|
|
120
101
|
self._signal = signal
|
|
121
102
|
self._staged = False
|
|
122
103
|
self._listeners: dict[Callback, bool] = {}
|
|
123
104
|
self._valid = asyncio.Event()
|
|
124
|
-
self._reading: Reading | None = None
|
|
125
|
-
self._value: T | None = None
|
|
126
|
-
|
|
105
|
+
self._reading: Reading[SignalDatatypeT] | None = None
|
|
127
106
|
self.backend = backend
|
|
128
107
|
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
129
108
|
backend.set_callback(self._callback)
|
|
@@ -132,30 +111,33 @@ class _SignalCache(Generic[T]):
|
|
|
132
111
|
self.backend.set_callback(None)
|
|
133
112
|
self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
|
|
134
113
|
|
|
135
|
-
async def get_reading(self) -> Reading:
|
|
114
|
+
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
136
115
|
await self._valid.wait()
|
|
137
116
|
assert self._reading is not None, "Monitor not working"
|
|
138
117
|
return self._reading
|
|
139
118
|
|
|
140
|
-
async def get_value(self) ->
|
|
141
|
-
await self.
|
|
142
|
-
|
|
143
|
-
return self._value
|
|
119
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
120
|
+
reading = await self.get_reading()
|
|
121
|
+
return reading["value"]
|
|
144
122
|
|
|
145
|
-
def _callback(self, reading: Reading
|
|
123
|
+
def _callback(self, reading: Reading[SignalDatatypeT]):
|
|
146
124
|
self._signal.log.debug(
|
|
147
|
-
f"Updated subscription: reading of source {self._signal.source} changed"
|
|
125
|
+
f"Updated subscription: reading of source {self._signal.source} changed "
|
|
148
126
|
f"from {self._reading} to {reading}"
|
|
149
127
|
)
|
|
150
128
|
self._reading = reading
|
|
151
|
-
self._value = value
|
|
152
129
|
self._valid.set()
|
|
153
130
|
for function, want_value in self._listeners.items():
|
|
154
131
|
self._notify(function, want_value)
|
|
155
132
|
|
|
156
|
-
def _notify(
|
|
133
|
+
def _notify(
|
|
134
|
+
self,
|
|
135
|
+
function: Callback[dict[str, Reading[SignalDatatypeT]] | SignalDatatypeT],
|
|
136
|
+
want_value: bool,
|
|
137
|
+
):
|
|
138
|
+
assert self._reading, "Monitor not working"
|
|
157
139
|
if want_value:
|
|
158
|
-
function(self.
|
|
140
|
+
function(self._reading["value"])
|
|
159
141
|
else:
|
|
160
142
|
function({self._signal.name: self._reading})
|
|
161
143
|
|
|
@@ -173,12 +155,14 @@ class _SignalCache(Generic[T]):
|
|
|
173
155
|
return self._staged or bool(self._listeners)
|
|
174
156
|
|
|
175
157
|
|
|
176
|
-
class SignalR(Signal[
|
|
158
|
+
class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribable):
|
|
177
159
|
"""Signal that can be read from and monitored"""
|
|
178
160
|
|
|
179
161
|
_cache: _SignalCache | None = None
|
|
180
162
|
|
|
181
|
-
def _backend_or_cache(
|
|
163
|
+
def _backend_or_cache(
|
|
164
|
+
self, cached: bool | None = None
|
|
165
|
+
) -> _SignalCache | SignalBackend:
|
|
182
166
|
# If cached is None then calculate it based on whether we already have a cache
|
|
183
167
|
if cached is None:
|
|
184
168
|
cached = self._cache is not None
|
|
@@ -186,11 +170,11 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
186
170
|
assert self._cache, f"{self.source} not being monitored"
|
|
187
171
|
return self._cache
|
|
188
172
|
else:
|
|
189
|
-
return self.
|
|
173
|
+
return self._connector.backend
|
|
190
174
|
|
|
191
175
|
def _get_cache(self) -> _SignalCache:
|
|
192
176
|
if not self._cache:
|
|
193
|
-
self._cache = _SignalCache(self.
|
|
177
|
+
self._cache = _SignalCache(self._connector.backend, self)
|
|
194
178
|
return self._cache
|
|
195
179
|
|
|
196
180
|
def _del_cache(self, needed: bool):
|
|
@@ -206,16 +190,16 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
206
190
|
@_add_timeout
|
|
207
191
|
async def describe(self) -> dict[str, DataKey]:
|
|
208
192
|
"""Return a single item dict with the descriptor in it"""
|
|
209
|
-
return {self.name: await self.
|
|
193
|
+
return {self.name: await self._connector.backend.get_datakey(self.source)}
|
|
210
194
|
|
|
211
195
|
@_add_timeout
|
|
212
|
-
async def get_value(self, cached: bool | None = None) ->
|
|
196
|
+
async def get_value(self, cached: bool | None = None) -> SignalDatatypeT:
|
|
213
197
|
"""The current value"""
|
|
214
198
|
value = await self._backend_or_cache(cached).get_value()
|
|
215
199
|
self.log.debug(f"get_value() on source {self.source} returned {value}")
|
|
216
200
|
return value
|
|
217
201
|
|
|
218
|
-
def subscribe_value(self, function: Callback[
|
|
202
|
+
def subscribe_value(self, function: Callback[SignalDatatypeT]):
|
|
219
203
|
"""Subscribe to updates in value of a device"""
|
|
220
204
|
self._get_cache().subscribe(function, want_value=True)
|
|
221
205
|
|
|
@@ -238,84 +222,82 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
238
222
|
self._del_cache(self._get_cache().set_staged(False))
|
|
239
223
|
|
|
240
224
|
|
|
241
|
-
class SignalW(Signal[
|
|
225
|
+
class SignalW(Signal[SignalDatatypeT], Movable):
|
|
242
226
|
"""Signal that can be set"""
|
|
243
227
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
228
|
+
@AsyncStatus.wrap
|
|
229
|
+
async def set(
|
|
230
|
+
self,
|
|
231
|
+
value: SignalDatatypeT,
|
|
232
|
+
wait=True,
|
|
233
|
+
timeout: CalculatableTimeout = CALCULATE_TIMEOUT,
|
|
234
|
+
) -> None:
|
|
247
235
|
"""Set the value and return a status saying when it's done"""
|
|
248
|
-
if timeout
|
|
236
|
+
if timeout == CALCULATE_TIMEOUT:
|
|
249
237
|
timeout = self._timeout
|
|
238
|
+
source = self._connector.backend.source(self.name, read=False)
|
|
239
|
+
self.log.debug(f"Putting value {value} to backend at source {source}")
|
|
240
|
+
await _wait_for(self._connector.backend.put(value, wait=wait), timeout, source)
|
|
241
|
+
self.log.debug(f"Successfully put value {value} to backend at source {source}")
|
|
250
242
|
|
|
251
|
-
async def do_set():
|
|
252
|
-
self.log.debug(f"Putting value {value} to backend at source {self.source}")
|
|
253
|
-
await self._backend.put(value, wait=wait, timeout=timeout)
|
|
254
|
-
self.log.debug(
|
|
255
|
-
f"Successfully put value {value} to backend at source {self.source}"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
return AsyncStatus(do_set())
|
|
259
243
|
|
|
260
|
-
|
|
261
|
-
class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
244
|
+
class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
|
|
262
245
|
"""Signal that can be both read and set"""
|
|
263
246
|
|
|
247
|
+
@_add_timeout
|
|
264
248
|
async def locate(self) -> Location:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return
|
|
249
|
+
"""Return the setpoint and readback."""
|
|
250
|
+
setpoint, readback = await asyncio.gather(
|
|
251
|
+
self._connector.backend.get_setpoint(), self._backend_or_cache().get_value()
|
|
252
|
+
)
|
|
253
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
270
254
|
|
|
271
255
|
|
|
272
256
|
class SignalX(Signal):
|
|
273
257
|
"""Signal that puts the default value"""
|
|
274
258
|
|
|
275
|
-
|
|
259
|
+
@AsyncStatus.wrap
|
|
260
|
+
async def trigger(
|
|
276
261
|
self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
277
|
-
) ->
|
|
262
|
+
) -> None:
|
|
278
263
|
"""Trigger the action and return a status saying when it's done"""
|
|
279
|
-
if timeout
|
|
264
|
+
if timeout == CALCULATE_TIMEOUT:
|
|
280
265
|
timeout = self._timeout
|
|
281
|
-
|
|
282
|
-
|
|
266
|
+
source = self._connector.backend.source(self.name, read=False)
|
|
267
|
+
self.log.debug(f"Putting default value to backend at source {source}")
|
|
268
|
+
await _wait_for(self._connector.backend.put(None, wait=wait), timeout, source)
|
|
269
|
+
self.log.debug(f"Successfully put default value to backend at source {source}")
|
|
283
270
|
|
|
284
271
|
|
|
285
272
|
def soft_signal_rw(
|
|
286
|
-
datatype: type[
|
|
287
|
-
initial_value:
|
|
273
|
+
datatype: type[SignalDatatypeT],
|
|
274
|
+
initial_value: SignalDatatypeT | None = None,
|
|
288
275
|
name: str = "",
|
|
289
276
|
units: str | None = None,
|
|
290
277
|
precision: int | None = None,
|
|
291
|
-
) -> SignalRW[
|
|
278
|
+
) -> SignalRW[SignalDatatypeT]:
|
|
292
279
|
"""Creates a read-writable Signal with a SoftSignalBackend.
|
|
293
280
|
May pass metadata, which are propagated into describe.
|
|
294
281
|
"""
|
|
295
|
-
|
|
296
|
-
signal = SignalRW(
|
|
297
|
-
SoftSignalBackend(datatype, initial_value, metadata=metadata),
|
|
298
|
-
name=name,
|
|
299
|
-
)
|
|
282
|
+
backend = SoftSignalBackend(datatype, initial_value, units, precision)
|
|
283
|
+
signal = SignalRW(backend=backend, name=name)
|
|
300
284
|
return signal
|
|
301
285
|
|
|
302
286
|
|
|
303
287
|
def soft_signal_r_and_setter(
|
|
304
|
-
datatype: type[
|
|
305
|
-
initial_value:
|
|
288
|
+
datatype: type[SignalDatatypeT],
|
|
289
|
+
initial_value: SignalDatatypeT | None = None,
|
|
306
290
|
name: str = "",
|
|
307
291
|
units: str | None = None,
|
|
308
292
|
precision: int | None = None,
|
|
309
|
-
) -> tuple[SignalR[
|
|
293
|
+
) -> tuple[SignalR[SignalDatatypeT], Callable[[SignalDatatypeT], None]]:
|
|
310
294
|
"""Returns a tuple of a read-only Signal and a callable through
|
|
311
295
|
which the signal can be internally modified within the device.
|
|
312
296
|
May pass metadata, which are propagated into describe.
|
|
313
297
|
Use soft_signal_rw if you want a device that is externally modifiable
|
|
314
298
|
"""
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
signal = SignalR(backend, name=name)
|
|
318
|
-
|
|
299
|
+
backend = SoftSignalBackend(datatype, initial_value, units, precision)
|
|
300
|
+
signal = SignalR(backend=backend, name=name)
|
|
319
301
|
return (signal, backend.set_value)
|
|
320
302
|
|
|
321
303
|
|
|
@@ -330,7 +312,7 @@ def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str
|
|
|
330
312
|
)
|
|
331
313
|
|
|
332
314
|
|
|
333
|
-
async def assert_value(signal: SignalR[
|
|
315
|
+
async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
334
316
|
"""Assert a signal's value and compare it an expected signal.
|
|
335
317
|
|
|
336
318
|
Parameters
|
|
@@ -440,8 +422,10 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
|
440
422
|
|
|
441
423
|
|
|
442
424
|
async def observe_value(
|
|
443
|
-
signal: SignalR[
|
|
444
|
-
|
|
425
|
+
signal: SignalR[SignalDatatypeT],
|
|
426
|
+
timeout: float | None = None,
|
|
427
|
+
done_status: Status | None = None,
|
|
428
|
+
) -> AsyncGenerator[SignalDatatypeT, None]:
|
|
445
429
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
446
430
|
|
|
447
431
|
Parameters
|
|
@@ -464,7 +448,44 @@ async def observe_value(
|
|
|
464
448
|
do_something_with(value)
|
|
465
449
|
"""
|
|
466
450
|
|
|
467
|
-
|
|
451
|
+
async for _, value in observe_signals_value(
|
|
452
|
+
signal, timeout=timeout, done_status=done_status
|
|
453
|
+
):
|
|
454
|
+
yield value
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
async def observe_signals_value(
|
|
458
|
+
*signals: SignalR[SignalDatatypeT],
|
|
459
|
+
timeout: float | None = None,
|
|
460
|
+
done_status: Status | None = None,
|
|
461
|
+
) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
|
|
462
|
+
"""Subscribe to the value of a signal so it can be iterated from.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
signals:
|
|
467
|
+
Call subscribe_value on all the signals at the start, and clear_sub on it at the
|
|
468
|
+
end
|
|
469
|
+
timeout:
|
|
470
|
+
If given, how long to wait for each updated value in seconds. If an update
|
|
471
|
+
is not produced in this time then raise asyncio.TimeoutError
|
|
472
|
+
done_status:
|
|
473
|
+
If this status is complete, stop observing and make the iterator return.
|
|
474
|
+
If it raises an exception then this exception will be raised by the iterator.
|
|
475
|
+
|
|
476
|
+
Notes
|
|
477
|
+
-----
|
|
478
|
+
Example usage::
|
|
479
|
+
|
|
480
|
+
async for signal,value in observe_signals_values(sig1,sig2,..):
|
|
481
|
+
if signal is sig1:
|
|
482
|
+
do_something_with(value)
|
|
483
|
+
elif signal is sig2:
|
|
484
|
+
do_something_else_with(value)
|
|
485
|
+
"""
|
|
486
|
+
q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
|
|
487
|
+
asyncio.Queue()
|
|
488
|
+
)
|
|
468
489
|
if timeout is None:
|
|
469
490
|
get_value = q.get
|
|
470
491
|
else:
|
|
@@ -472,12 +493,23 @@ async def observe_value(
|
|
|
472
493
|
async def get_value():
|
|
473
494
|
return await asyncio.wait_for(q.get(), timeout)
|
|
474
495
|
|
|
496
|
+
cbs: dict[SignalR, Callback] = {}
|
|
497
|
+
for signal in signals:
|
|
498
|
+
|
|
499
|
+
def queue_value(value: SignalDatatypeT, signal=signal):
|
|
500
|
+
q.put_nowait((signal, value))
|
|
501
|
+
|
|
502
|
+
cbs[signal] = queue_value
|
|
503
|
+
signal.subscribe_value(queue_value)
|
|
504
|
+
|
|
475
505
|
if done_status is not None:
|
|
476
506
|
done_status.add_callback(q.put_nowait)
|
|
477
507
|
|
|
478
|
-
signal.subscribe_value(q.put_nowait)
|
|
479
508
|
try:
|
|
480
509
|
while True:
|
|
510
|
+
# yield here in case something else is filling the queue
|
|
511
|
+
# like in test_observe_value_times_out_with_no_external_task()
|
|
512
|
+
await asyncio.sleep(0)
|
|
481
513
|
item = await get_value()
|
|
482
514
|
if done_status and item is done_status:
|
|
483
515
|
if exc := done_status.exception():
|
|
@@ -485,24 +517,27 @@ async def observe_value(
|
|
|
485
517
|
else:
|
|
486
518
|
break
|
|
487
519
|
else:
|
|
488
|
-
yield cast(
|
|
520
|
+
yield cast(tuple[SignalR[SignalDatatypeT], SignalDatatypeT], item)
|
|
489
521
|
finally:
|
|
490
|
-
signal.
|
|
522
|
+
for signal, cb in cbs.items():
|
|
523
|
+
signal.clear_sub(cb)
|
|
491
524
|
|
|
492
525
|
|
|
493
|
-
class _ValueChecker(Generic[
|
|
494
|
-
def __init__(self, matcher: Callable[[
|
|
495
|
-
self._last_value:
|
|
526
|
+
class _ValueChecker(Generic[SignalDatatypeT]):
|
|
527
|
+
def __init__(self, matcher: Callable[[SignalDatatypeT], bool], matcher_name: str):
|
|
528
|
+
self._last_value: SignalDatatypeT | None = None
|
|
496
529
|
self._matcher = matcher
|
|
497
530
|
self._matcher_name = matcher_name
|
|
498
531
|
|
|
499
|
-
async def _wait_for_value(self, signal: SignalR[
|
|
532
|
+
async def _wait_for_value(self, signal: SignalR[SignalDatatypeT]):
|
|
500
533
|
async for value in observe_value(signal):
|
|
501
534
|
self._last_value = value
|
|
502
535
|
if self._matcher(value):
|
|
503
536
|
return
|
|
504
537
|
|
|
505
|
-
async def wait_for_value(
|
|
538
|
+
async def wait_for_value(
|
|
539
|
+
self, signal: SignalR[SignalDatatypeT], timeout: float | None
|
|
540
|
+
):
|
|
506
541
|
try:
|
|
507
542
|
await asyncio.wait_for(self._wait_for_value(signal), timeout)
|
|
508
543
|
except asyncio.TimeoutError as e:
|
|
@@ -513,8 +548,8 @@ class _ValueChecker(Generic[T]):
|
|
|
513
548
|
|
|
514
549
|
|
|
515
550
|
async def wait_for_value(
|
|
516
|
-
signal: SignalR[
|
|
517
|
-
match:
|
|
551
|
+
signal: SignalR[SignalDatatypeT],
|
|
552
|
+
match: SignalDatatypeT | Callable[[SignalDatatypeT], bool],
|
|
518
553
|
timeout: float | None,
|
|
519
554
|
):
|
|
520
555
|
"""Wait for a signal to have a matching value.
|
|
@@ -548,17 +583,18 @@ async def wait_for_value(
|
|
|
548
583
|
|
|
549
584
|
|
|
550
585
|
async def set_and_wait_for_other_value(
|
|
551
|
-
set_signal: SignalW[
|
|
552
|
-
set_value:
|
|
553
|
-
|
|
554
|
-
|
|
586
|
+
set_signal: SignalW[SignalDatatypeT],
|
|
587
|
+
set_value: SignalDatatypeT,
|
|
588
|
+
match_signal: SignalR[SignalDatatypeV],
|
|
589
|
+
match_value: SignalDatatypeV | Callable[[SignalDatatypeV], bool],
|
|
555
590
|
timeout: float = DEFAULT_TIMEOUT,
|
|
556
591
|
set_timeout: float | None = None,
|
|
592
|
+
wait_for_set_completion: bool = True,
|
|
557
593
|
) -> AsyncStatus:
|
|
558
594
|
"""Set a signal and monitor another signal until it has the specified value.
|
|
559
595
|
|
|
560
596
|
This function sets a set_signal to a specified set_value and waits for
|
|
561
|
-
a
|
|
597
|
+
a match_signal to have the match_value.
|
|
562
598
|
|
|
563
599
|
Parameters
|
|
564
600
|
----------
|
|
@@ -566,14 +602,16 @@ async def set_and_wait_for_other_value(
|
|
|
566
602
|
The signal to set
|
|
567
603
|
set_value:
|
|
568
604
|
The value to set it to
|
|
569
|
-
|
|
605
|
+
match_signal:
|
|
570
606
|
The signal to monitor
|
|
571
|
-
|
|
607
|
+
match_value:
|
|
572
608
|
The value to wait for
|
|
573
609
|
timeout:
|
|
574
610
|
How long to wait for the signal to have the value
|
|
575
611
|
set_timeout:
|
|
576
612
|
How long to wait for the set to complete
|
|
613
|
+
wait_for_set_completion:
|
|
614
|
+
This will wait for set completion #More info in how-to docs
|
|
577
615
|
|
|
578
616
|
Notes
|
|
579
617
|
-----
|
|
@@ -582,7 +620,7 @@ async def set_and_wait_for_other_value(
|
|
|
582
620
|
set_and_wait_for_value(device.acquire, 1, device.acquire_rbv, 1)
|
|
583
621
|
"""
|
|
584
622
|
# Start monitoring before the set to avoid a race condition
|
|
585
|
-
values_gen = observe_value(
|
|
623
|
+
values_gen = observe_value(match_signal)
|
|
586
624
|
|
|
587
625
|
# Get the initial value from the monitor to make sure we've created it
|
|
588
626
|
current_value = await anext(values_gen)
|
|
@@ -590,28 +628,33 @@ async def set_and_wait_for_other_value(
|
|
|
590
628
|
status = set_signal.set(set_value, timeout=set_timeout)
|
|
591
629
|
|
|
592
630
|
# If the value was the same as before no need to wait for it to change
|
|
593
|
-
if current_value !=
|
|
631
|
+
if current_value != match_value:
|
|
594
632
|
|
|
595
633
|
async def _wait_for_value():
|
|
596
634
|
async for value in values_gen:
|
|
597
|
-
if value ==
|
|
635
|
+
if value == match_value:
|
|
598
636
|
break
|
|
599
637
|
|
|
600
638
|
try:
|
|
601
639
|
await asyncio.wait_for(_wait_for_value(), timeout)
|
|
640
|
+
if wait_for_set_completion:
|
|
641
|
+
await status
|
|
642
|
+
return status
|
|
602
643
|
except asyncio.TimeoutError as e:
|
|
603
644
|
raise TimeoutError(
|
|
604
|
-
f"{
|
|
645
|
+
f"{match_signal.name} didn't match {match_value} in {timeout}s"
|
|
605
646
|
) from e
|
|
606
647
|
|
|
607
|
-
return
|
|
648
|
+
return completed_status()
|
|
608
649
|
|
|
609
650
|
|
|
610
651
|
async def set_and_wait_for_value(
|
|
611
|
-
signal: SignalRW[
|
|
612
|
-
value:
|
|
652
|
+
signal: SignalRW[SignalDatatypeT],
|
|
653
|
+
value: SignalDatatypeT,
|
|
654
|
+
match_value: SignalDatatypeT | Callable[[SignalDatatypeT], bool] | None = None,
|
|
613
655
|
timeout: float = DEFAULT_TIMEOUT,
|
|
614
656
|
status_timeout: float | None = None,
|
|
657
|
+
wait_for_set_completion: bool = True,
|
|
615
658
|
) -> AsyncStatus:
|
|
616
659
|
"""Set a signal and monitor it until it has that value.
|
|
617
660
|
|
|
@@ -626,10 +669,15 @@ async def set_and_wait_for_value(
|
|
|
626
669
|
The signal to set
|
|
627
670
|
value:
|
|
628
671
|
The value to set it to
|
|
672
|
+
match_value:
|
|
673
|
+
The expected value of the signal after the operation.
|
|
674
|
+
Used to verify that the set operation was successful.
|
|
629
675
|
timeout:
|
|
630
676
|
How long to wait for the signal to have the value
|
|
631
677
|
status_timeout:
|
|
632
678
|
How long the returned Status will wait for the set to complete
|
|
679
|
+
wait_for_set_completion:
|
|
680
|
+
This will wait for set completion #More info in how-to docs
|
|
633
681
|
|
|
634
682
|
Notes
|
|
635
683
|
-----
|
|
@@ -637,6 +685,14 @@ async def set_and_wait_for_value(
|
|
|
637
685
|
|
|
638
686
|
set_and_wait_for_value(device.acquire, 1)
|
|
639
687
|
"""
|
|
688
|
+
if match_value is None:
|
|
689
|
+
match_value = value
|
|
640
690
|
return await set_and_wait_for_other_value(
|
|
641
|
-
signal,
|
|
691
|
+
signal,
|
|
692
|
+
value,
|
|
693
|
+
signal,
|
|
694
|
+
match_value,
|
|
695
|
+
timeout,
|
|
696
|
+
status_timeout,
|
|
697
|
+
wait_for_set_completion,
|
|
642
698
|
)
|